Skip to content

Contact forces silently understated for multi-geom body segments (FlyBody) #294

Description

@sibocw

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

  1. 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
    ]
  2. 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.

  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions