Skip to content

Commit ef83901

Browse files
authored
[MISC] Add error code to rigid solver. (#1979)
* Add outlier rejection mechanism to FPS meter. * Add option to override number of broad phase collision pairs. * Add error code to rigid solver. * Fix number of contact per pair if box-box collision algo is enabled. * Fix performance drop. * Remove redundant contact points in box-box.
1 parent 9f1a98a commit ef83901

File tree

9 files changed

+262
-174
lines changed

9 files changed

+262
-174
lines changed

genesis/engine/simulator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,8 @@ def step(self, in_backward=False):
287287

288288
if self.rigid_solver.is_active:
289289
self.rigid_solver.clear_external_force()
290+
if self._cur_substep_global % 10 == 0:
291+
self.rigid_solver.check_errno()
290292

291293
self._sensor_manager.step()
292294

genesis/engine/solvers/rigid/collider_decomp.py

Lines changed: 185 additions & 151 deletions
Large diffs are not rendered by default.

genesis/engine/solvers/rigid/rigid_solver_decomp.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ def build(self):
263263
# FIXME: AvatarSolver should not inherite from RigidSolver.
264264
if type(self) is RigidSolver or self.is_active:
265265
self.data_manager = array_class.DataManager(self)
266+
self._errno = self.data_manager.errno
266267

267268
self._rigid_global_info = self.data_manager.rigid_global_info
268269
if self._use_hibernation:
@@ -871,6 +872,21 @@ def substep(self):
871872
contact_island_state=self.constraint_solver.contact_island.contact_island_state,
872873
)
873874

875+
def check_errno(self):
876+
match kernel_get_errno(self._errno):
877+
case 1:
878+
max_collision_pairs_broad = self.collider._collider_info.max_collision_pairs_broad[None]
879+
gs.raise_exception(
880+
f"Exceeding max number of broad phase candidate contact pairs ({max_contact_pairs}). Please increase "
881+
f"the value of RigidSolver's option 'multiplier_collision_broad_phase'."
882+
)
883+
case 2:
884+
max_contact_pairs = self.collider._collider_info.max_contact_pairs[None]
885+
gs.raise_exception(
886+
f"Exceeding max number of contact pairs ({max_contact_pairs}). Please increase the value of "
887+
"RigidSolver's option 'max_collision_pairs'."
888+
)
889+
874890
def _kernel_detect_collision(self):
875891
self.collider.clear()
876892
self.collider.detection()
@@ -1251,6 +1267,7 @@ def set_state(self, f, state, envs_idx=None):
12511267
static_rigid_sim_config=self._static_rigid_sim_config,
12521268
)
12531269

1270+
self._errno[None] = 0
12541271
self.collider.reset(envs_idx)
12551272
self.collider.clear(envs_idx)
12561273
if self.constraint_solver is not None:
@@ -1609,6 +1626,7 @@ def set_qpos(self, qpos, qs_idx=None, envs_idx=None, *, skip_forward=False, unsa
16091626
qpos = qpos.unsqueeze(0)
16101627
kernel_set_qpos(qpos, qs_idx, envs_idx, self._rigid_global_info, self._static_rigid_sim_config)
16111628

1629+
self._errno[None] = 0
16121630
self.collider.reset(envs_idx)
16131631
self.collider.clear(envs_idx)
16141632
if self.constraint_solver is not None:
@@ -1837,6 +1855,7 @@ def set_dofs_position(self, position, dofs_idx=None, envs_idx=None, *, skip_forw
18371855
self._static_rigid_sim_config,
18381856
)
18391857

1858+
self._errno[None] = 0
18401859
self.collider.reset(envs_idx)
18411860
self.collider.clear(envs_idx)
18421861
if self.constraint_solver is not None:
@@ -6923,3 +6942,8 @@ def kernel_set_geoms_friction(
69236942
ti.loop_config(serialize=ti.static(static_rigid_sim_config.para_level < gs.PARA_LEVEL.ALL))
69246943
for i_g_ in ti.ndrange(geoms_idx.shape[0]):
69256944
geoms_info.friction[geoms_idx[i_g_]] = friction[i_g_]
6945+
6946+
6947+
@ti.kernel(fastcache=gs.use_fastcache)
6948+
def kernel_get_errno(errno: array_class.V_ANNOTATION) -> ti.i32:
6949+
return errno[None]

genesis/options/solvers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ class RigidOptions(Options):
364364
enable_adjacent_collision: bool = False
365365
disable_constraint: bool = False
366366
max_collision_pairs: int = 300
367+
multiplier_collision_broad_phase: int = 8
367368
integrator: gs.integrator = gs.integrator.approximate_implicitfast
368369
IK_max_targets: int = 6
369370

genesis/utils/array_class.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,14 @@ def get_constraint_state(constraint_solver, solver):
232232

233233
efc_AR_shape = maybe_shape((len_constraints_, len_constraints_, _B), solver._options.noslip_iterations > 0)
234234
efc_b_shape = maybe_shape((len_constraints_, _B), solver._options.noslip_iterations > 0)
235+
jac_relevant_dofs_shape = (len_constraints_, solver.n_dofs_, _B)
236+
jac_n_relevant_dofs_shape = (len_constraints_, _B)
235237

236238
return StructConstraintState(
237239
n_constraints=V(dtype=gs.ti_int, shape=(_B,)),
238240
ti_n_equalities=V(dtype=gs.ti_int, shape=(_B,)),
239-
jac=V(dtype=gs.ti_float, shape=(len_constraints_, solver.n_dofs_, _B)),
240241
diag=V(dtype=gs.ti_float, shape=(len_constraints_, _B)),
241242
aref=V(dtype=gs.ti_float, shape=(len_constraints_, _B)),
242-
jac_relevant_dofs=V(dtype=gs.ti_int, shape=(len_constraints_, solver.n_dofs_, _B)),
243-
jac_n_relevant_dofs=V(dtype=gs.ti_int, shape=(len_constraints_, _B)),
244243
n_constraints_equality=V(dtype=gs.ti_int, shape=(_B,)),
245244
n_constraints_frictionloss=V(dtype=gs.ti_int, shape=(_B,)),
246245
improved=V(dtype=gs.ti_int, shape=(_B,)),
@@ -293,6 +292,10 @@ def get_constraint_state(constraint_solver, solver):
293292
bw_Ju=V(dtype=gs.ti_float, shape=maybe_shape((len_constraints_, _B), solver._requires_grad)),
294293
bw_y=V(dtype=gs.ti_float, shape=maybe_shape((len_constraints_, _B), solver._requires_grad)),
295294
bw_w=V(dtype=gs.ti_float, shape=maybe_shape((len_constraints_, _B), solver._requires_grad)),
295+
# /!\ Moving allocation of these tensors at the end improves runtime speed by ~5-10% /!\
296+
jac=V(dtype=gs.ti_float, shape=(len_constraints_, solver.n_dofs_, _B)),
297+
jac_relevant_dofs=V(dtype=gs.ti_int, shape=jac_relevant_dofs_shape),
298+
jac_n_relevant_dofs=V(dtype=gs.ti_int, shape=jac_n_relevant_dofs_shape),
296299
)
297300

298301

@@ -518,8 +521,8 @@ def get_collider_state(
518521
broad_collision_pairs=V_VEC(2, dtype=gs.ti_int, shape=(max(max_collision_pairs_broad, 1), _B)),
519522
active_buffer_awake=V(dtype=gs.ti_int, shape=(n_geoms, _B)),
520523
active_buffer_hib=V(dtype=gs.ti_int, shape=(n_geoms, _B)),
521-
box_depth=V(dtype=gs.ti_float, shape=(collider_info.box_MAXCONPAIR[None], _B)),
522-
box_points=V_VEC(3, dtype=gs.ti_float, shape=(collider_info.box_MAXCONPAIR[None], _B)),
524+
box_depth=V(dtype=gs.ti_float, shape=(collider_static_config.n_contacts_per_pair, _B)),
525+
box_points=V_VEC(3, dtype=gs.ti_float, shape=(collider_static_config.n_contacts_per_pair, _B)),
523526
box_pts=V_VEC(3, dtype=gs.ti_float, shape=(6, _B)),
524527
box_lines=V_VEC(6, dtype=gs.ti_float, shape=(4, _B)),
525528
box_linesu=V_VEC(6, dtype=gs.ti_float, shape=(4, _B)),
@@ -555,8 +558,6 @@ class StructColliderInfo(metaclass=BASE_METACLASS):
555558
mc_perturbation: V_ANNOTATION
556559
mc_tolerance: V_ANNOTATION
557560
mpr_to_sdf_overlap_ratio: V_ANNOTATION
558-
# maximum number of contact points for box-box collision detection
559-
box_MAXCONPAIR: V_ANNOTATION
560561
# differentiable contact tolerance
561562
diff_pos_tolerance: V_ANNOTATION
562563
diff_normal_tolerance: V_ANNOTATION
@@ -586,7 +587,6 @@ def get_collider_info(solver, n_vert_neighbors, collider_static_config, **kwargs
586587
mc_perturbation=V_SCALAR_FROM(dtype=gs.ti_float, value=kwargs["mc_perturbation"]),
587588
mc_tolerance=V_SCALAR_FROM(dtype=gs.ti_float, value=kwargs["mc_tolerance"]),
588589
mpr_to_sdf_overlap_ratio=V_SCALAR_FROM(dtype=gs.ti_float, value=kwargs["mpr_to_sdf_overlap_ratio"]),
589-
box_MAXCONPAIR=V_SCALAR_FROM(dtype=gs.ti_int, value=kwargs["box_MAXCONPAIR"]),
590590
diff_pos_tolerance=V_SCALAR_FROM(dtype=gs.ti_float, value=kwargs["diff_pos_tolerance"]),
591591
diff_normal_tolerance=V_SCALAR_FROM(dtype=gs.ti_float, value=kwargs["diff_normal_tolerance"]),
592592
)
@@ -1731,6 +1731,7 @@ class StructRigidSimStaticConfig(metaclass=AutoInitMeta):
17311731
class DataManager:
17321732
def __init__(self, solver):
17331733
self.rigid_global_info = get_rigid_global_info(solver)
1734+
17341735
self.dofs_info = get_dofs_info(solver)
17351736
self.dofs_state = get_dofs_state(solver)
17361737
self.links_info = get_links_info(solver)
@@ -1758,6 +1759,8 @@ def __init__(self, solver):
17581759
self.entities_info = get_entities_info(solver)
17591760
self.entities_state = get_entities_state(solver)
17601761

1762+
self.errno = V_SCALAR_FROM(dtype=gs.ti_int, value=0)
1763+
17611764

17621765
DofsState = StructDofsState if gs.use_ndarray else ti.template()
17631766
DofsInfo = StructDofsInfo if gs.use_ndarray else ti.template()

genesis/utils/tools.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,17 @@ def sleep(self):
177177

178178

179179
class FPSTracker:
180-
def __init__(self, n_envs, alpha=0.95, minimum_interval_seconds: float | None = 0.1):
180+
def __init__(
181+
self, n_envs, alpha=0.95, minimum_interval_seconds: float | None = 0.05, outlier_threshold: float = 1.5
182+
):
181183
self.last_time = None
182184
self.n_envs = n_envs
183185
self.dt_ema = None
184186
self.alpha = alpha
185187
self.minimum_interval_seconds = minimum_interval_seconds
188+
self.outlier_threshold = outlier_threshold
186189
self.steps_since_last_print: int = 0
190+
self.total_fps = 0.0
187191

188192
def step(self, current_time: float | None = None) -> float | None:
189193
if not current_time:
@@ -197,13 +201,21 @@ def step(self, current_time: float | None = None) -> float | None:
197201

198202
self.steps_since_last_print += 1
199203

204+
# Skip if update is too soon
200205
if self.minimum_interval_seconds and current_time - self.last_time < self.minimum_interval_seconds:
201206
return None
202207

208+
# Outlier rejection
209+
if self.dt_ema is not None:
210+
if dt > self.dt_ema * self.outlier_threshold or dt * self.outlier_threshold < self.dt_ema:
211+
self.dt_ema = dt
212+
213+
# EMA update
203214
if self.dt_ema:
204215
self.dt_ema = self.alpha * self.dt_ema + (1 - self.alpha) * dt
205216
else:
206217
self.dt_ema = dt
218+
207219
fps = 1 / self.dt_ema * self.steps_since_last_print
208220
if self.n_envs > 0:
209221
self.total_fps = fps * self.n_envs

tests/test_deformable_physics.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import torch
77

88
import genesis as gs
9+
from genesis.utils.misc import tensor_to_array
910

1011
from .utils import assert_allclose
1112

@@ -75,7 +76,7 @@ def test_muscle(n_envs, muscle_material, show_viewer):
7576
pos = worm.get_state().pos[0, worm.get_el2v()].mean(1)
7677
n_units = worm.n_elements
7778

78-
pos = pos.cpu().numpy()
79+
pos = tensor_to_array(pos)
7980
pos_max, pos_min = pos.max(0), pos.min(0)
8081
pos_range = pos_max - pos_min
8182
lu_thresh, fh_thresh = 0.3, 0.6

tests/test_rigid_physics.py

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,9 @@ def test_dynamic_weld(show_viewer, tol):
470470
size=(0.04, 0.04, 0.04),
471471
pos=(0.65, 0.0, 0.02),
472472
),
473-
surface=gs.surfaces.Plastic(color=(1, 0, 0)),
473+
surface=gs.surfaces.Plastic(
474+
color=(1, 0, 0),
475+
),
474476
)
475477
robot = scene.add_entity(
476478
gs.morphs.MJCF(
@@ -2114,7 +2116,6 @@ def test_terrain_generation(request, show_viewer):
21142116
name="my_terrain",
21152117
)
21162118
# FIXME: Collision detection is very unstable for 'stepping_stones' pattern.
2117-
# np.random.seed(4)
21182119
terrain = scene.add_entity(gs.morphs.Terrain(**terrain_kwargs))
21192120
obj = scene.add_entity(
21202121
morph=gs.morphs.Box(
@@ -2133,7 +2134,7 @@ def test_terrain_generation(request, show_viewer):
21332134
obj.set_pos(obj_pos_init_rel + torch.tensor(TERRAIN_OFFSET))
21342135

21352136
# Drop the objects and simulate for a while.
2136-
for _ in range(500):
2137+
for _ in range(600):
21372138
scene.step()
21382139

21392140
# Check that objects are not moving anymore
@@ -2161,7 +2162,7 @@ def test_terrain_generation(request, show_viewer):
21612162

21622163

21632164
@pytest.mark.required
2164-
def test_discrete_obstacles_terrain():
2165+
def test_terrain_discrete_obstacles():
21652166
scene = gs.Scene()
21662167
terrain = scene.add_entity(
21672168
gs.morphs.Terrain(
@@ -2182,9 +2183,9 @@ def test_discrete_obstacles_terrain():
21822183
height_field = terrain.geoms[0].metadata["height_field"]
21832184
platform = height_field[5:7, 5:7]
21842185

2185-
assert height_field.max().item() == 2.0
2186-
assert height_field.min().item() == -2.0
2187-
assert (platform == 0.0).all()
2186+
assert height_field.max() == 2.0
2187+
assert height_field.min() == -2.0
2188+
assert (platform < gs.EPS).all()
21882189

21892190

21902191
def test_mesh_to_heightfield(tmp_path, show_viewer):
@@ -3175,7 +3176,13 @@ def test_noslip_iterations(scale, show_viewer, tol):
31753176
rigid_options=gs.options.RigidOptions(
31763177
noslip_iterations=5,
31773178
),
3178-
profiling_options=gs.options.ProfilingOptions(show_FPS=False),
3179+
viewer_options=gs.options.ViewerOptions(
3180+
camera_pos=(3 * scale, 3 * scale, 3 * scale),
3181+
camera_lookat=(scale, 0.0, 0.0),
3182+
),
3183+
profiling_options=gs.options.ProfilingOptions(
3184+
show_FPS=False,
3185+
),
31793186
show_viewer=show_viewer,
31803187
)
31813188

@@ -3188,6 +3195,10 @@ def test_noslip_iterations(scale, show_viewer, tol):
31883195
pos=(i * scale, 0, 0),
31893196
fixed=(i == 0),
31903197
),
3198+
surface=gs.surfaces.Default(
3199+
color=(*np.random.rand(3), 0.7),
3200+
),
3201+
visualize_contact=True,
31913202
)
31923203
)
31933204
scene.build()
@@ -3204,7 +3215,7 @@ def test_noslip_iterations(scale, show_viewer, tol):
32043215
boxes[2].control_dofs_force(np.array([-safety / coeff_f * n_box * rho * scale**3 * g]), np.array([0]))
32053216
scene.step()
32063217

3207-
box_1_z = boxes[1].get_qpos().cpu().numpy()[2]
3218+
box_1_z = tensor_to_array(boxes[1].get_qpos())[2]
32083219
# allow some small sliding due to first few frames
32093220
# scale = 0.1 is less stable than bigger scale
32103221
assert_allclose(box_1_z, 0.0, atol=4e-2 * scale)
@@ -3216,7 +3227,7 @@ def test_noslip_iterations(scale, show_viewer, tol):
32163227
boxes[2].control_dofs_force(np.array([-safety / coeff_f * n_box * rho * scale**3 * g]), np.array([0]))
32173228
scene.step()
32183229

3219-
box_1_z = boxes[1].get_qpos().cpu().numpy()[2]
3230+
box_1_z = tensor_to_array(boxes[1].get_qpos())[2]
32203231
# it will slip away
32213232
assert box_1_z < -scale
32223233

tests/test_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ def test_pyrender_vec3():
362362
# repr and tensor conversion
363363
t = a.as_tensor()
364364
assert isinstance(t, torch.Tensor)
365-
assert_allclose(t.cpu().numpy(), a.v, tol=gs.EPS)
365+
assert_allclose(t, a.v, tol=gs.EPS)
366366

367367
# --- Quat tests ---
368368
q = Quat.from_wxyz(1.0, 0.0, 0.0, 0.0) # identity
@@ -407,12 +407,12 @@ def test_pyrender_vec3():
407407
# tensor conversion
408408
tq = qz.as_tensor()
409409
assert isinstance(tq, torch.Tensor)
410-
assert_allclose(tq.cpu().numpy(), qz.v, tol=gs.EPS)
410+
assert_allclose(tq, qz.v, tol=gs.EPS)
411411

412412

413413
def test_fps_tracker():
414414
n_envs = 23
415-
tracker = FPSTracker(alpha=0, n_envs=n_envs)
415+
tracker = FPSTracker(alpha=0.0, minimum_interval_seconds=0.1, n_envs=n_envs)
416416
tracker.step(current_time=10.0)
417417
assert not tracker.step(current_time=10.0)
418418
assert not tracker.step(current_time=10.0)

0 commit comments

Comments
 (0)