Summary
Simulation.get_bodysegment_contact_forces reports contact forces for only
one geom per body segment — the last one registered. Any segment built from
more than one collision geom (the FlyBody model has ~15 of them: c_thorax,
c_head, the abdomen/wing/antenna segments, …) has the contacts on all of its
other geoms silently dropped, so the reported force is understated or zero.
There is no error — the number just comes out wrong.
NeuroMechFly / the musculoskeletal model are unaffected because each of their
segments maps to exactly one geom.
Root cause
In #279 fly.bodyseg_to_mjcfgeom became dict[BodySegment, list[geom]] (a
segment can own several geoms). The contact-force consumer was not updated to
match — it still assumes one geom per segment.
Simulation._map_internal_geom_ids (src/flygym/simulation.py:516-532) writes
a scalar id inside the per-geom loop, so only the last geom survives:
for bodyseg, mjcf_geom_elements in fly.bodyseg_to_mjcfgeom.items():
for mjcf_geom_element in mjcf_geom_elements:
internal_geom_id = mj.mj_name2id(self.mj_model, mj.mjtObj.mjOBJ_GEOM,
mjcf_geom_element.name)
internal_geomids_by_bodyseg_by_fly[fly_name][bodyseg] = internal_geom_id # last-wins
get_bodysegment_contact_forces (src/flygym/simulation.py:283-326) then builds
a one-id-per-segment lookup and matches contacts against it via np.isin, so
contacts on the dropped geoms never match:
geom_ids_by_segment = self._internal_geomid_by_bodyseg_by_fly[fly_name]
requested_geom_to_output = {geom_ids_by_segment[seg]: i
for i, seg in enumerate(requested_segments)}
Why this is a design issue (not a one-line patch)
The "one geom id per segment" assumption is baked into the
_internal_geomid_by_bodyseg_by_fly map and its consumer, and it implies a
semantic decision: a multi-geom segment's contact force should be the sum
over its geoms. So the fix is to (a) make the map hold all geom ids per
segment and (b) update the consumer (and any other reader of that map) to
aggregate. Worth doing deliberately with a regression test rather than slipping
into the release PR.
Suggested fix
-
Store a list of geom ids per segment in _map_internal_geom_ids:
internal_geomids_by_bodyseg_by_fly[fly_name][bodyseg] = [
mj.mj_name2id(self.mj_model, mj.mjtObj.mjOBJ_GEOM, g.name)
for g in mjcf_geom_elements
]
-
In get_bodysegment_contact_forces, build the reverse map from every
geom of each segment to that segment's output row, so a contact on any of a
segment's geoms accumulates into the same row:
requested_geom_to_output = {
geom_id: i
for i, seg in enumerate(requested_segments)
for geom_id in geom_ids_by_segment[seg]
}
The rest of the method (the np.isin filter and the per-contact
accumulation at lines 322-326) then works unchanged and naturally sums
contributions from all geoms of a segment.
-
Audit other readers of _internal_geomid_by_bodyseg_by_fly for the same
scalar assumption.
Test
Add a regression test on a FlyBody (or a synthetic multi-geom segment) that
places contacts on a non-last geom of a multi-geom segment and asserts the
reported force is non-zero / equals the analytic sum. The current code returns
~0 for that case.
Found during review of #279.
Summary
Simulation.get_bodysegment_contact_forcesreports contact forces for onlyone geom per body segment — the last one registered. Any segment built from
more than one collision geom (the FlyBody model has ~15 of them:
c_thorax,c_head, the abdomen/wing/antenna segments, …) has the contacts on all of itsother geoms silently dropped, so the reported force is understated or zero.
There is no error — the number just comes out wrong.
NeuroMechFly / the musculoskeletal model are unaffected because each of their
segments maps to exactly one geom.
Root cause
In #279
fly.bodyseg_to_mjcfgeombecamedict[BodySegment, list[geom]](asegment can own several geoms). The contact-force consumer was not updated to
match — it still assumes one geom per segment.
Simulation._map_internal_geom_ids(src/flygym/simulation.py:516-532) writesa scalar id inside the per-geom loop, so only the last geom survives:
get_bodysegment_contact_forces(src/flygym/simulation.py:283-326) then buildsa one-id-per-segment lookup and matches contacts against it via
np.isin, socontacts on the dropped geoms never match:
Why this is a design issue (not a one-line patch)
The "one geom id per segment" assumption is baked into the
_internal_geomid_by_bodyseg_by_flymap and its consumer, and it implies asemantic decision: a multi-geom segment's contact force should be the sum
over its geoms. So the fix is to (a) make the map hold all geom ids per
segment and (b) update the consumer (and any other reader of that map) to
aggregate. Worth doing deliberately with a regression test rather than slipping
into the release PR.
Suggested fix
Store a list of geom ids per segment in
_map_internal_geom_ids:In
get_bodysegment_contact_forces, build the reverse map from everygeom of each segment to that segment's output row, so a contact on any of a
segment's geoms accumulates into the same row:
The rest of the method (the
np.isinfilter and the per-contactaccumulation at lines 322-326) then works unchanged and naturally sums
contributions from all geoms of a segment.
Audit other readers of
_internal_geomid_by_bodyseg_by_flyfor the samescalar assumption.
Test
Add a regression test on a FlyBody (or a synthetic multi-geom segment) that
places contacts on a non-last geom of a multi-geom segment and asserts the
reported force is non-zero / equals the analytic sum. The current code returns
~0 for that case.
Found during review of #279.