Skip to content

musculoskeletal

Experimental

Support for the FlyMimic musculoskeletal body model is experimental. The API may change in future releases, and only the left-front leg is muscle-driven in the current model. Not all features available for the default NeuroMechFly model are currently supported (e.g. per-leg ground-contact sensors).

Musculoskeletal (FlyMimic) body model as a FlyGym composition element.

Unlike NeuroMechFly and FlyBody, which compose a body from meshes and YAML rigging configs via BaseFly, the musculoskeletal model is a pre-authored self-contained MJCF: FlyMimic's muscle-driven fly ships its own floor, lighting, 15 left-front-leg Hill-type muscles, 15 spatial tendons, and passive joint properties. Rather than overlaying muscles onto FlyGym's composed body, the musculoskeletal path switches the body model: it loads that MJCF into a mujoco.MjSpec (FlyGym's model-editing backend since the v2.1.0 PyMJCF -> MjSpec migration) and wraps it so that flygym.Simulation and its sensor suite work unchanged.

MusculoskeletalFly therefore subclasses BaseCompositionElement directly (not BaseFly) but exposes the same dicts/accessors Simulation reads, keyed by the model's own MJCF element-name strings (e.g. "LFFemur", "joint_LFCoxa_yaw", "LFTibia_flex_93434"), since FlyMimic's body topology does not line up 1:1 with FlyGym's BodySegment names:

  • bodyseg_to_mjcfbody, bodyseg_to_mjcfgeom
  • jointdof_to_mjcfjoint, jointdof_to_mjcfactuator_by_type
  • leg_to_adhesionactuator, anatomicaljoint_to_mjcfsites
  • eyecameraname_to_mjcfcamera plus the get_*_order accessors.

Pair it with MusculoskeletalWorld (see flygym.compose.world), or use build_musculoskeletal_simulation (defined below) for the common case. build_musculoskeletal_simulation returns a plain flygym.Simulation (CPU, single world) — not flygym.warp.GPUSimulation. Guarded GPU/MuJoCo-Warp helpers (check_mjwarp_compatibility, build_musculoskeletal_gpu_simulation) live here too — they lazily import mujoco_warp so they are safe to import on machines without a GPU, and require the [warp] extra to actually run.

DEFAULT_MUSCULOSKELETAL_XML = MUSCULOSKELETAL_MODEL_DIR / 'best_combined_arm_damping_stiff_cvt3.xml' module-attribute

FlyMimic's muscle-driven fly with passive joint stiffness + spring refs.

This is the arm_damping_stiff variant: 15 left-front-leg muscles and body geometry, plus biologically-motivated passive joint stiffness (0.4) and per-joint spring reference angles.

DEFAULT_SCENE_CAMERA = 'scene' module-attribute

Name assigned to FlyMimic's (otherwise unnamed) world camera so it can be selected for rendering via Simulation.set_renderer — e.g. when recording a rollout video of a trained policy.

MUSCULOSKELETAL_MODEL_DIR = assets_dir / 'model/musculoskeletal' module-attribute

Directory holding FlyMimic's musculoskeletal MJCF. The body meshes it references are not bundled here (see MUSCULOSKELETAL_MESH_DIR), and the imitation-learning mocap clips live with the demo, in flygym_demo.muscle_imitation.

MjWarpCompatibilityReport dataclass

Outcome of probing whether MuJoCo-Warp accepts the muscle model.

Attributes:

Name Type Description
mjwarp_available bool

Whether mujoco_warp could be imported.

put_model_ok bool | None

Whether mjw.put_model succeeded on the compiled model. None if mjwarp was unavailable (probe not run).

error str | None

The exception text if put_model failed, else None.

message str

A human-readable summary.

Source code in src/flygym/compose/fly/musculoskeletal.py
@dataclass
class MjWarpCompatibilityReport:
    """Outcome of probing whether MuJoCo-Warp accepts the muscle model.

    Attributes:
        mjwarp_available: Whether ``mujoco_warp`` could be imported.
        put_model_ok: Whether ``mjw.put_model`` succeeded on the compiled
            model. ``None`` if mjwarp was unavailable (probe not run).
        error: The exception text if ``put_model`` failed, else ``None``.
        message: A human-readable summary.
    """

    mjwarp_available: bool
    put_model_ok: bool | None
    error: str | None
    message: str

    def __bool__(self) -> bool:
        """True only if mjwarp is available *and* accepted the model."""
        return bool(self.mjwarp_available and self.put_model_ok)

__bool__()

True only if mjwarp is available and accepted the model.

Source code in src/flygym/compose/fly/musculoskeletal.py
def __bool__(self) -> bool:
    """True only if mjwarp is available *and* accepted the model."""
    return bool(self.mjwarp_available and self.put_model_ok)

MusculoskeletalFly

Bases: BaseCompositionElement

FlyGym-compatible wrapper around FlyMimic's musculoskeletal MJCF.

The musculoskeletal body model is published in:

Ozdil, P. G., et al. (2026). Musculoskeletal simulation of limb
movement biomechanics in *Drosophila melanogaster*. *ICLR 2026*.
https://arxiv.org/abs/2509.06426

Source code for the original model: https://github.com/gizemozd/FlyMimic

Plain flygym — not GPU-accelerated

MusculoskeletalFly works with plain flygym.Simulation (CPU, single world). It does not require flygym.warp.

Unlike NeuroMechFly and FlyBody, which compose a body from meshes and YAML rigging configs via BaseFly, this class loads FlyMimic's pre-authored self-contained MJCF and wraps it so that Simulation and its sensor suite work unchanged. Dict keys on all tracking attributes are the model's own MJCF element-name strings (e.g. "LFFemur", "joint_LFCoxa_yaw").

Parameters:

Name Type Description Default
xml_path PathLike

Path to the FlyMimic MJCF. Defaults to the bundled arm_damping_stiff musculoskeletal model (DEFAULT_MUSCULOSKELETAL_XML).

DEFAULT_MUSCULOSKELETAL_XML
name str

Logical fly name used by Simulation lookups. Defaults to "nmf".

'nmf'

Attributes:

Name Type Description
bodyseg_to_mjcfbody dict[str, MjsBody]

Maps body-segment name → MJCF body element.

bodyseg_to_mjcfgeom dict[str, list[MjsGeom]]

Maps body-segment name → list of MJCF geom elements (one entry per geom on that body).

jointdof_to_mjcfjoint dict[str, MjsJoint]

Maps joint-DoF name → MJCF joint element.

jointdof_to_mjcfactuator_by_type

Maps ActuatorType → dict of actuator-name → MJCF actuator element.

leg_to_adhesionactuator dict[str, MjsActuator]

Always empty (FlyMimic has no adhesion).

anatomicaljoint_to_mjcfsites dict[str, MjsSite]

Always empty.

eyecameraname_to_mjcfcamera dict[str, MjsCamera]

Camera elements added via add_vision; empty until add_vision is called.

cameraname_to_mjcfcamera dict[str, MjsCamera]

All scene cameras, including the world camera named DEFAULT_SCENE_CAMERA.

muscle_names list[str]

Names of the 15 Hill-type muscle actuators, in MJCF order (read-only property).

Source code in src/flygym/compose/fly/musculoskeletal.py
class MusculoskeletalFly(BaseCompositionElement):
    """FlyGym-compatible wrapper around FlyMimic's musculoskeletal MJCF.

    The musculoskeletal body model is published in:

        Ozdil, P. G., et al. (2026). Musculoskeletal simulation of limb
        movement biomechanics in *Drosophila melanogaster*. *ICLR 2026*.
        https://arxiv.org/abs/2509.06426

    Source code for the original model: https://github.com/gizemozd/FlyMimic

    !!! info "Plain flygym — not GPU-accelerated"

        `MusculoskeletalFly` works with plain `flygym.Simulation` (CPU,
        single world). It does **not** require `flygym.warp`.

    Unlike `NeuroMechFly` and `FlyBody`, which compose a body from meshes
    and YAML rigging configs via `BaseFly`, this class loads FlyMimic's
    pre-authored self-contained MJCF and wraps it so that `Simulation` and
    its sensor suite work unchanged. Dict keys on all tracking attributes
    are the model's own MJCF element-name strings (e.g. ``"LFFemur"``,
    ``"joint_LFCoxa_yaw"``).

    Args:
        xml_path: Path to the FlyMimic MJCF. Defaults to the bundled
            ``arm_damping_stiff`` musculoskeletal model
            (`DEFAULT_MUSCULOSKELETAL_XML`).
        name: Logical fly name used by `Simulation` lookups. Defaults to
            ``"nmf"``.

    Attributes:
        bodyseg_to_mjcfbody: Maps body-segment name → MJCF body element.
        bodyseg_to_mjcfgeom: Maps body-segment name → list of MJCF geom
            elements (one entry per geom on that body).
        jointdof_to_mjcfjoint: Maps joint-DoF name → MJCF joint element.
        jointdof_to_mjcfactuator_by_type: Maps `ActuatorType` → dict of
            actuator-name → MJCF actuator element.
        leg_to_adhesionactuator: Always empty (FlyMimic has no adhesion).
        anatomicaljoint_to_mjcfsites: Always empty.
        eyecameraname_to_mjcfcamera: Camera elements added via `add_vision`;
            empty until `add_vision` is called.
        cameraname_to_mjcfcamera: All scene cameras, including the world
            camera named `DEFAULT_SCENE_CAMERA`.
        muscle_names: Names of the 15 Hill-type muscle actuators, in MJCF
            order (read-only property).
    """

    def __init__(
        self,
        xml_path: PathLike = DEFAULT_MUSCULOSKELETAL_XML,
        *,
        name: str = "nmf",
    ) -> None:
        self._name = name
        self._mjcf_root = _load_mjcf(xml_path)

        # Make the simulation's reset target ("neutral") resolve: rename the
        # model's existing default pose keyframe.
        self._neutral_keyframe = self._mjcf_root.key("default-pose")
        if self._neutral_keyframe is not None:
            self._neutral_keyframe.name = "neutral"
        else:
            self._neutral_keyframe = self._mjcf_root.add_key(name="neutral", time=0)

        # Build the tracking dicts that Simulation introspects. Like
        # `BaseFly`, ``bodyseg_to_mjcfgeom`` maps each segment to the *list* of
        # its geoms (Simulation and the sensor APIs iterate over them).
        # ``spec.bodies`` includes the unnamed worldbody (its MjSpec name is
        # ``"world"``); skip it so only real body segments are registered. Its
        # only direct geom is the floor, which the world exposes via
        # ``ground_geoms`` instead.
        worldbody = self._mjcf_root.worldbody
        self.bodyseg_to_mjcfbody: dict[str, mj.MjsBody] = {}
        self.bodyseg_to_mjcfgeom: dict[str, list[mj.MjsGeom]] = {}
        for body in self._mjcf_root.bodies:
            if body.name == worldbody.name or not body.name:
                continue
            self.bodyseg_to_mjcfbody[body.name] = body
            geoms = list(body.geoms)
            if geoms:
                self.bodyseg_to_mjcfgeom[body.name] = geoms

        self.jointdof_to_mjcfjoint: dict[str, mj.MjsJoint] = {
            j.name: j for j in self._mjcf_root.joints if j.name
        }

        self.jointdof_to_mjcfactuator_by_type = {ty: {} for ty in ActuatorType}
        for actuator in self._mjcf_root.actuators:
            if not actuator.name:
                continue
            ty = self._classify_actuator(actuator)
            self.jointdof_to_mjcfactuator_by_type[ty][actuator.name] = actuator

        # FlyMimic has no adhesion, no anatomical-joint sites, no eye cameras
        # by default.
        self.leg_to_adhesionactuator: dict[str, mj.MjsActuator] = {}
        self.anatomicaljoint_to_mjcfsites: dict[str, mj.MjsSite] = {}
        self.eyecameraname_to_mjcfcamera: dict[str, mj.MjsCamera] = {}

        # Register scene cameras so they can be selected for rendering. FlyMimic
        # ships a single unnamed world camera; name any unnamed camera so
        # ``Simulation.set_renderer(DEFAULT_SCENE_CAMERA)`` resolves it. (Eye
        # cameras are added later, named, via add_vision().) MjSpec gives an
        # unnamed camera an empty-string name rather than ``None``.
        self.cameraname_to_mjcfcamera: dict[str, mj.MjsCamera] = {}
        for idx, cam in enumerate(self._mjcf_root.cameras):
            if not cam.name:
                cam.name = (
                    DEFAULT_SCENE_CAMERA
                    if idx == 0
                    else f"{DEFAULT_SCENE_CAMERA}_{idx}"
                )
            self.cameraname_to_mjcfcamera[cam.name] = cam

    @property
    def mjcf_root(self) -> mj.MjSpec:
        return self._mjcf_root

    @property
    def name(self) -> str:
        return self._name

    @staticmethod
    def _classify_actuator(actuator: mj.MjsActuator) -> ActuatorType:
        """Best-effort mapping of an MjSpec actuator element to an ActuatorType.

        MjSpec exposes every actuator as a low-level "general" element, so there
        is no shortcut tag (``motor``/``position``/...) to read. The dynamics
        type is the reliable discriminator: muscles use ``mjDYN_MUSCLE`` (FlyMimic
        sets this on the ``"muscle"`` default class, and MjSpec resolves the class
        default onto the element). Everything else falls back to ``MOTOR``, which
        is all FlyMimic's non-muscle actuators are.
        """
        if actuator.dyntype == mj.mjtDyn.mjDYN_MUSCLE:
            return ActuatorType.MUSCLE
        return ActuatorType.MOTOR

    # ---- Fly-compatible accessors used by Simulation / the imitation env ----

    def get_bodysegs_order(self) -> list[str]:
        """Return all body-segment names in MJCF order."""
        return list(self.bodyseg_to_mjcfbody.keys())

    def get_jointdofs_order(self) -> list[str]:
        """Return all joint-DoF names in MJCF order."""
        return list(self.jointdof_to_mjcfjoint.keys())

    def get_actuated_jointdofs_order(
        self, actuator_type: "ActuatorType | str"
    ) -> list[str]:
        """Return actuator names of the given type, in MJCF order.

        Args:
            actuator_type: An `ActuatorType` value or its string name (e.g.
                ``"muscle"`` or ``ActuatorType.MUSCLE``).
        """
        actuator_type = ActuatorType(actuator_type)
        return list(self.jointdof_to_mjcfactuator_by_type[actuator_type].keys())

    def get_sites_order(self) -> list[str]:
        """Return anatomical-joint site names (always empty for this model)."""
        return list(self.anatomicaljoint_to_mjcfsites.keys())

    def get_legs_order(self) -> list[str]:
        """Return leg names (always empty — no ground-contact grouping defined)."""
        return []

    @property
    def muscle_names(self) -> list[str]:
        """Names of the muscle actuators, in MJCF order."""
        return self.get_actuated_jointdofs_order(ActuatorType.MUSCLE)

    def add_vision(
        self,
        *,
        fovy: float = 145.0,
        draw_sensor_markers: bool = False,
    ) -> dict[str, mj.MjsCamera]:
        """Attach left/right eye cameras to the model's eye bodies.

        Enables `Simulation.get_raw_vision` / `get_ommatidia_readouts`. Note
        that FlyGym's fisheye `Retina` is calibrated for FlyGym's own eye
        placement, so ommatidia readouts on this body are approximate.
        """
        added: dict[str, mj.MjsCamera] = {}
        for eye_body_name, rgba in _EYE_BODIES.items():
            body = self.bodyseg_to_mjcfbody.get(eye_body_name)
            if body is None:
                continue
            cam = body.add_camera(
                name=f"{eye_body_name}_camera",
                mode=CAMERA_MODES["fixed"],
                # Point the camera laterally outward from each eye.
                euler=(0.0, 0.0, 0.0),
                fovy=fovy,
            )
            if draw_sensor_markers:
                body.add_site(
                    name=f"{eye_body_name}_marker",
                    type=GEOM_TYPES["sphere"],
                    size=(0.02, 0.02, 0.02),
                    rgba=rgba,
                    group=1,
                )
            added[eye_body_name] = cam
        self.eyecameraname_to_mjcfcamera.update(added)
        return added

muscle_names property

Names of the muscle actuators, in MJCF order.

add_vision(*, fovy=145.0, draw_sensor_markers=False)

Attach left/right eye cameras to the model's eye bodies.

Enables Simulation.get_raw_vision / get_ommatidia_readouts. Note that FlyGym's fisheye Retina is calibrated for FlyGym's own eye placement, so ommatidia readouts on this body are approximate.

Source code in src/flygym/compose/fly/musculoskeletal.py
def add_vision(
    self,
    *,
    fovy: float = 145.0,
    draw_sensor_markers: bool = False,
) -> dict[str, mj.MjsCamera]:
    """Attach left/right eye cameras to the model's eye bodies.

    Enables `Simulation.get_raw_vision` / `get_ommatidia_readouts`. Note
    that FlyGym's fisheye `Retina` is calibrated for FlyGym's own eye
    placement, so ommatidia readouts on this body are approximate.
    """
    added: dict[str, mj.MjsCamera] = {}
    for eye_body_name, rgba in _EYE_BODIES.items():
        body = self.bodyseg_to_mjcfbody.get(eye_body_name)
        if body is None:
            continue
        cam = body.add_camera(
            name=f"{eye_body_name}_camera",
            mode=CAMERA_MODES["fixed"],
            # Point the camera laterally outward from each eye.
            euler=(0.0, 0.0, 0.0),
            fovy=fovy,
        )
        if draw_sensor_markers:
            body.add_site(
                name=f"{eye_body_name}_marker",
                type=GEOM_TYPES["sphere"],
                size=(0.02, 0.02, 0.02),
                rgba=rgba,
                group=1,
            )
        added[eye_body_name] = cam
    self.eyecameraname_to_mjcfcamera.update(added)
    return added

get_actuated_jointdofs_order(actuator_type)

Return actuator names of the given type, in MJCF order.

Parameters:

Name Type Description Default
actuator_type ActuatorType | str

An ActuatorType value or its string name (e.g. "muscle" or ActuatorType.MUSCLE).

required
Source code in src/flygym/compose/fly/musculoskeletal.py
def get_actuated_jointdofs_order(
    self, actuator_type: "ActuatorType | str"
) -> list[str]:
    """Return actuator names of the given type, in MJCF order.

    Args:
        actuator_type: An `ActuatorType` value or its string name (e.g.
            ``"muscle"`` or ``ActuatorType.MUSCLE``).
    """
    actuator_type = ActuatorType(actuator_type)
    return list(self.jointdof_to_mjcfactuator_by_type[actuator_type].keys())

get_bodysegs_order()

Return all body-segment names in MJCF order.

Source code in src/flygym/compose/fly/musculoskeletal.py
def get_bodysegs_order(self) -> list[str]:
    """Return all body-segment names in MJCF order."""
    return list(self.bodyseg_to_mjcfbody.keys())

get_jointdofs_order()

Return all joint-DoF names in MJCF order.

Source code in src/flygym/compose/fly/musculoskeletal.py
def get_jointdofs_order(self) -> list[str]:
    """Return all joint-DoF names in MJCF order."""
    return list(self.jointdof_to_mjcfjoint.keys())

get_legs_order()

Return leg names (always empty — no ground-contact grouping defined).

Source code in src/flygym/compose/fly/musculoskeletal.py
def get_legs_order(self) -> list[str]:
    """Return leg names (always empty — no ground-contact grouping defined)."""
    return []

get_sites_order()

Return anatomical-joint site names (always empty for this model).

Source code in src/flygym/compose/fly/musculoskeletal.py
def get_sites_order(self) -> list[str]:
    """Return anatomical-joint site names (always empty for this model)."""
    return list(self.anatomicaljoint_to_mjcfsites.keys())

build_musculoskeletal_gpu_simulation(n_worlds, *, xml_path=DEFAULT_MUSCULOSKELETAL_XML, name='nmf', add_vision=False, **gpu_kwargs)

Build a GPUSimulation of the muscle model with n_worlds parallel copies for vectorized RL on a CUDA machine.

Call check_mjwarp_compatibility first to verify that the installed mujoco_warp version supports the muscle model's Hill-type actuators, spatial tendons, and joint-equality constraints.

Parameters:

Name Type Description Default
n_worlds int

Number of parallel simulation worlds.

required
xml_path PathLike

Path to the FlyMimic MJCF. Defaults to DEFAULT_MUSCULOSKELETAL_XML.

DEFAULT_MUSCULOSKELETAL_XML
name str

Logical fly name. Defaults to "nmf".

'nmf'
add_vision bool

Attach eye cameras (approximate; see build_musculoskeletal_simulation).

False
**gpu_kwargs Any

Forwarded to GPUSimulation.

{}

Returns:

Type Description
GPUSimulation

(gpu_simulation, fly) — a GPUSimulation and the

MusculoskeletalFly

MusculoskeletalFly.

Raises:

Type Description
ImportError

If the [warp] extra or an NVIDIA CUDA GPU is not available.

Source code in src/flygym/compose/fly/musculoskeletal.py
def build_musculoskeletal_gpu_simulation(
    n_worlds: int,
    *,
    xml_path: PathLike = DEFAULT_MUSCULOSKELETAL_XML,
    name: str = "nmf",
    add_vision: bool = False,
    **gpu_kwargs: Any,
) -> "tuple[GPUSimulation, MusculoskeletalFly]":
    """Build a `GPUSimulation` of the muscle model with *n_worlds* parallel
    copies for vectorized RL on a CUDA machine.

    Call `check_mjwarp_compatibility` first to verify that the installed
    ``mujoco_warp`` version supports the muscle model's Hill-type actuators,
    spatial tendons, and joint-equality constraints.

    Args:
        n_worlds: Number of parallel simulation worlds.
        xml_path: Path to the FlyMimic MJCF. Defaults to
            `DEFAULT_MUSCULOSKELETAL_XML`.
        name: Logical fly name. Defaults to ``"nmf"``.
        add_vision: Attach eye cameras (approximate; see
            `build_musculoskeletal_simulation`).
        **gpu_kwargs: Forwarded to `GPUSimulation`.

    Returns:
        ``(gpu_simulation, fly)`` — a `GPUSimulation` and the
        `MusculoskeletalFly`.

    Raises:
        ImportError: If the ``[warp]`` extra or an NVIDIA CUDA GPU is not
            available.
    """
    try:
        from flygym.warp.simulation import GPUSimulation
    except ImportError as e:
        raise ImportError(
            "GPU simulation requires the '[warp]' extra (warp-lang + "
            "mujoco_warp) and an NVIDIA CUDA GPU. On such a machine, install "
            "with `pip install 'flygym[warp]'`."
        ) from e
    from flygym.compose.world.musculoskeletal import MusculoskeletalWorld

    fly = MusculoskeletalFly(xml_path, name=name)
    if add_vision:
        fly.add_vision()
    world = MusculoskeletalWorld(fly)
    sim = GPUSimulation(world, n_worlds=n_worlds, **gpu_kwargs)
    return sim, fly

build_musculoskeletal_simulation(*, xml_path=DEFAULT_MUSCULOSKELETAL_XML, name='nmf', add_vision=False)

Build a MusculoskeletalFly + MusculoskeletalWorld + Simulation.

Equivalent to the standard composition flow::

fly = MusculoskeletalFly(xml_path, name=name)
world = MusculoskeletalWorld(fly)
sim = Simulation(world)

Parameters:

Name Type Description Default
xml_path PathLike

Path to the FlyMimic MJCF. Defaults to DEFAULT_MUSCULOSKELETAL_XML.

DEFAULT_MUSCULOSKELETAL_XML
name str

Logical fly name. Defaults to "nmf".

'nmf'
add_vision bool

If True, attach left/right eye cameras so Simulation.get_raw_vision / get_ommatidia_readouts work. Note that ommatidia readouts are approximate because FlyGym's Retina is calibrated for its own eye placement.

False

Plain flygym — not GPU-accelerated

Returns a plain flygym.Simulation (CPU, single world). For the GPU path use build_musculoskeletal_gpu_simulation with the [warp] extra.

Returns:

Type Description
Simulation

(simulation, fly) where simulation is a flygym.Simulation

MusculoskeletalFly

and fly is the MusculoskeletalFly instance (useful for

tuple[Simulation, MusculoskeletalFly]

inspecting fly.muscle_names or calling fly.add_vision).

Source code in src/flygym/compose/fly/musculoskeletal.py
def build_musculoskeletal_simulation(
    *,
    xml_path: PathLike = DEFAULT_MUSCULOSKELETAL_XML,
    name: str = "nmf",
    add_vision: bool = False,
) -> "tuple[Simulation, MusculoskeletalFly]":
    """Build a `MusculoskeletalFly` + `MusculoskeletalWorld` + `Simulation`.

    Equivalent to the standard composition flow::

        fly = MusculoskeletalFly(xml_path, name=name)
        world = MusculoskeletalWorld(fly)
        sim = Simulation(world)

    Args:
        xml_path: Path to the FlyMimic MJCF. Defaults to
            `DEFAULT_MUSCULOSKELETAL_XML`.
        name: Logical fly name. Defaults to ``"nmf"``.
        add_vision: If True, attach left/right eye cameras so
            `Simulation.get_raw_vision` / `get_ommatidia_readouts` work.
            Note that ommatidia readouts are approximate because FlyGym's
            `Retina` is calibrated for its own eye placement.

    !!! info "Plain flygym — not GPU-accelerated"

        Returns a plain `flygym.Simulation` (CPU, single world). For the
        GPU path use `build_musculoskeletal_gpu_simulation` with the
        `[warp]` extra.

    Returns:
        ``(simulation, fly)`` where *simulation* is a `flygym.Simulation`
        and *fly* is the `MusculoskeletalFly` instance (useful for
        inspecting ``fly.muscle_names`` or calling ``fly.add_vision``).
    """
    from flygym.simulation import Simulation
    from flygym.compose.world.musculoskeletal import MusculoskeletalWorld

    fly = MusculoskeletalFly(xml_path, name=name)
    if add_vision:
        fly.add_vision()
    world = MusculoskeletalWorld(fly)
    sim = Simulation(world)
    return sim, fly

check_mjwarp_compatibility(fly=None, *, xml_path=DEFAULT_MUSCULOSKELETAL_XML)

Probe whether MuJoCo-Warp can ingest the muscle model.

Safe to call anywhere: if mujoco_warp is not installed (the usual case off a CUDA machine), it returns a report with mjwarp_available=False rather than raising.

Parameters:

Name Type Description Default
fly MusculoskeletalFly | None

A MusculoskeletalFly to probe. If None, a default one is built.

None
xml_path PathLike

XML to use when fly is None.

DEFAULT_MUSCULOSKELETAL_XML

Returns:

Type Description
MjWarpCompatibilityReport

A MjWarpCompatibilityReport.

Source code in src/flygym/compose/fly/musculoskeletal.py
def check_mjwarp_compatibility(
    fly: "MusculoskeletalFly | None" = None,
    *,
    xml_path: PathLike = DEFAULT_MUSCULOSKELETAL_XML,
) -> MjWarpCompatibilityReport:
    """Probe whether MuJoCo-Warp can ingest the muscle model.

    Safe to call anywhere: if ``mujoco_warp`` is not installed (the usual case
    off a CUDA machine), it returns a report with ``mjwarp_available=False``
    rather than raising.

    Args:
        fly: A `MusculoskeletalFly` to probe. If None, a default one is built.
        xml_path: XML to use when ``fly`` is None.

    Returns:
        A `MjWarpCompatibilityReport`.
    """
    try:
        import mujoco_warp as mjw
    except ImportError:
        return MjWarpCompatibilityReport(
            mjwarp_available=False,
            put_model_ok=None,
            error=None,
            message=(
                "mujoco_warp is not installed; cannot probe GPU compatibility. "
                "This is expected without an NVIDIA CUDA GPU. Install the "
                "'[warp]' extra on a Linux+CUDA machine to enable the GPU path."
            ),
        )

    if fly is None:
        fly = MusculoskeletalFly(xml_path)
    mj_model, _ = fly.compile()
    try:
        mjw.put_model(mj_model)
    except Exception as e:  # noqa: BLE001 - we want to report any failure mode
        return MjWarpCompatibilityReport(
            mjwarp_available=True,
            put_model_ok=False,
            error=f"{type(e).__name__}: {e}",
            message=(
                "mujoco_warp is installed but rejected the muscle model. The "
                "likely culprits are muscle actuators, spatial tendons, or "
                "joint-equality constraints not yet supported by this mjwarp "
                "version. See the error field."
            ),
        )
    return MjWarpCompatibilityReport(
        mjwarp_available=True,
        put_model_ok=True,
        error=None,
        message="mujoco_warp accepted the muscle model (put_model succeeded).",
    )