Skip to content

Commit edf2e36

Browse files
authored
Optimize large swarms (#125)
1 parent 2fa2241 commit edf2e36

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

‎crazyflow/sim/sim.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def __init__(
130130
self.n_worlds = n_worlds
131131
self.n_drones = n_drones
132132
self.freq = freq
133+
self.max_geom_pairs = -1 if n_drones < 64 else 2 * n_drones
133134
self.max_visual_geom = 1000
134135

135136
# Initialize MuJoCo world and data
@@ -287,6 +288,7 @@ def build_mjx_spec(self) -> mujoco.MjSpec:
287288
assert self._xml_path.exists(), f"Model file {self._xml_path} does not exist"
288289
spec = mujoco.MjSpec.from_file(str(self._xml_path))
289290
spec.option.timestep = 1 / self.freq
291+
spec.add_numeric(name="max_geom_pairs", data=[self.max_geom_pairs])
290292
spec.copy_during_attach = True
291293
drone_spec = mujoco.MjSpec.from_file(str(self.drone_path))
292294
frame = spec.worldbody.add_frame(name="world")
@@ -502,6 +504,7 @@ def build_mjx(self):
502504
if self.viewer is not None:
503505
self.viewer.close()
504506
self.viewer = None
507+
self.spec.numeric("max_geom_pairs").data = [self.max_geom_pairs]
505508
self.mj_model, self.mj_data, self.mjx_model, self.mjx_data = self.build_mjx_model(self.spec)
506509

507510
def init_data(
@@ -573,11 +576,22 @@ def controllable(self) -> Array:
573576
def contacts(self, body: str | None = None) -> Array:
574577
"""Get contact information from the simulation.
575578
579+
Note:
580+
``sim.max_geom_pairs`` limits the maximum detectable collision contacts per collision
581+
group. This is relevant for swarms, where the full pairwise collision buffer grows
582+
quadratically. By default, we allocate 2*n_drones contact pairs if the swarm size
583+
exceeds 64. That gives us enough capacity to detect all drone-drone contacts. However,
584+
if the swarm collapses e.g. into a single position, this will no longer be correct. If
585+
you need to truly detect all contacts, set ``sim.max_geom_pairs`` to -1 and rebuild the
586+
simulation.
587+
576588
Args:
577589
body: Optional body name to filter contacts for. If None, returns flags for all bodies.
578590
579591
Returns:
580-
An boolean array of shape (n_worlds,) that is True if any contact is present.
592+
A boolean array of shape (n_worlds, n_contacts), one flag per slot in the contact
593+
buffer. Which geoms a slot holds is given by the matching entries of
594+
``sim.mjx_data._impl.contact.geom1`` and ``geom2``.
581595
"""
582596
if body is None:
583597
return self.mjx_data._impl.contact.dist < 0

‎tests/unit/test_sim.py‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,3 +767,36 @@ def test_full_reset_restores_shared_arrays():
767767
assert jnp.array_equal(sim.data.params.gravity_vec, default_gravity)
768768
# The random key is the only thing that does not reset
769769
assert jnp.array_equal(jax.random.key_data(sim.data.core.rng_key), jax.random.key_data(rng_key))
770+
771+
772+
@pytest.mark.unit
773+
def test_max_geom_pairs_caps_contact_buffer():
774+
"""Small swarms check all geom pairs, large swarms cap the buffer to stay linear."""
775+
sim = Sim(n_drones=32)
776+
assert sim.mjx_data._impl.contact.dist.shape[-1] == 32 * 33 // 2
777+
sim.close()
778+
n_drones = 64
779+
sim = Sim(n_drones=n_drones)
780+
assert sim.mjx_data._impl.contact.dist.shape[-1] == n_drones + 2 * n_drones # Drones + floor
781+
sim.max_geom_pairs = -1 # applies on the next build
782+
sim.build_mjx()
783+
assert sim.mjx_data._impl.contact.dist.shape[-1] == n_drones * (n_drones + 1) // 2
784+
sim.close()
785+
786+
787+
@pytest.mark.unit
788+
def test_capped_contacts_identify_colliding_drones():
789+
"""Test that capping the contact buffer still correctly identifies drone collisions by name."""
790+
n_drones = 16
791+
sim = Sim(n_drones=n_drones)
792+
sim.max_geom_pairs = 4
793+
sim.build_mjx()
794+
sim.reset()
795+
pos = np.stack([[i * 2.0, 0.0, 1.0] for i in range(n_drones)])[None]
796+
pos[0, 4] = pos[0, 3] # overlap drones 3 and 4, leave the rest far apart
797+
sim.data = sim.data.replace(states=sim.data.states.replace(pos=jnp.array(pos)))
798+
sim.step()
799+
assert jnp.any(sim.contacts("drone:3")), "Overlapping drones should be in contact"
800+
assert jnp.any(sim.contacts("drone:4")), "Overlapping drones should be in contact"
801+
assert not jnp.any(sim.contacts("drone:5")), "Distant drones should not be in contact"
802+
sim.close()

0 commit comments

Comments
 (0)