Skip to content

ik

[ik]: fit a NeuroMechFly-style model's joint angles to the 3D pose.

The solve is QuickIK's, an optional extra (ik._quickik). deeperfly holds the model (ik.pack, ik.template, ik.articulation), where the tracked points sit on it (ik.binding), the recording's measurements (ik.align), the body plan QuickIK solves (ik.bodyplan) and the fit's joint positions (ik.forward). See ADR-0041, ADR-0065 and ADR-0066.

Alignment dataclass

The per-leg geometry registering a recording to the template.

Source code in src/deeperfly/ik/align.py
@dataclass(frozen=True)
class Alignment:
    """The per-leg geometry registering a recording to the template."""

    leg_origin: dict[str, np.ndarray]  # leg name -> (3,) coxa model-frame position
    seglens: dict[str, np.ndarray]  # leg name -> (J,) model-unit length into each joint

Articulation dataclass

The loaded head/abdomen chains plus the anchor bodies that register the body.

anchors names model bodies (from the pack manifest) and anchor_neutral holds their neutral positions, read out of bodies, which maps each body a marker may attach to onto its neutral frame and chain depth. leg_rest is every leg DOF's spring reference in radians, the plan's neutral (ADR-0065); empty for an asset without one, which falls back to zero.

Source code in src/deeperfly/ik/articulation.py
@dataclass(frozen=True)
class Articulation:
    """The loaded head/abdomen chains plus the anchor bodies that register the body.

    `anchors` names model bodies (from the pack manifest) and `anchor_neutral` holds
    their neutral positions, read out of `bodies`, which maps each body a marker may
    attach to onto its neutral frame and chain depth. `leg_rest` is every leg DOF's
    spring reference in radians, the plan's `neutral` (ADR-0065); empty for an asset
    without one, which falls back to zero.
    """

    chains: tuple[Chain, ...]
    anchors: tuple[str, ...]  # model body names
    anchor_neutral: np.ndarray  # (N, 3) neutral anchor positions, in anchor order
    _anchor_points: tuple[str, ...] = ()  # the binding's point per anchor, same order
    bodies: dict[str, dict] = field(default_factory=dict)
    leg_rest: dict[str, float] = field(default_factory=dict)

    @classmethod
    def load(
        cls,
        ref: str | Path = DEFAULT_ARTICULATION_PATH,
        *,
        binding: "Binding | str | Path | None" = "fly38@neuromechfly",
        anchors: tuple[str, ...] = _DEFAULT_ANCHORS,
        fit: tuple[str, ...] | None = None,
        bounds_overrides: dict[str, tuple[float, float]] | None = None,
        marker_overrides: dict[str, dict[str, dict]] | None = None,
    ) -> Articulation:
        """Load the baked articulation, with this run's restrictions applied.

        `binding`'s rows on the chain bodies are the chains' markers and its rows on
        the anchor bodies say which point observes each; `None` loads the structure
        with no markers, readable but not fittable. `fit` keeps a subset of the
        chains, `bounds_overrides` maps flygym angle names to degree limits
        (case-insensitive), and `marker_overrides` replaces a chain's binding rows
        (`[ik.markers]`).
        """
        if binding is not None and not isinstance(binding, Binding):
            binding = Binding.load(binding)
        spec = json.loads(Path(ref).read_text())
        overrides = {k.lower(): v for k, v in (bounds_overrides or {}).items()}
        bodies = spec.get("bodies", {})
        missing = [a for a in anchors if a not in bodies]
        if missing:
            raise ValueError(
                f"the model pack names registration anchor(s) {missing}, which its "
                f"articulation asset {Path(ref).name} does not bake a body frame for "
                f"(it has {sorted(bodies)})"
            )
        markers = marker_overrides or {}
        chains = []
        for c in spec["chains"]:
            if fit is not None and c["name"] not in fit:
                continue
            table = markers.get(c["name"])
            if table is None:
                table = (
                    {}
                    if binding is None
                    else _chain_binding_markers(c["name"], binding, bodies)
                )
            resolved, base_point = _resolve_markers(c, table, bodies)
            chains.append(_build_chain(c, overrides, resolved, base_point))
        return cls(
            chains=tuple(chains),
            anchors=tuple(anchors),
            anchor_neutral=np.asarray(
                [bodies[a]["pos"] for a in anchors], dtype=float
            ).reshape(len(anchors), 3),
            _anchor_points=tuple(
                "" if binding is None else (binding.origin_point_for(a) or "")
                for a in anchors
            ),
            bodies=bodies,
            leg_rest={str(k): float(v) for k, v in spec.get("leg_rest", {}).items()},
        )

    @property
    def anchor_points(self) -> tuple[str, ...]:
        """The tracked point observing each anchor body, from the binding.

        Empty where the binding puts no point on an anchor's origin; the registration
        needs only three finite anchors.
        """
        return self._anchor_points

    def chain(self, name: str) -> Chain | None:
        """The chain of that name, or `None` if this articulation has none."""
        return next((c for c in self.chains if c.name == name), None)

    @property
    def dof_names(self) -> list[str]:
        """Every chain's DOF names, concatenated in chain order."""
        return [n for c in self.chains for n in c.dof_names]

anchor_points property

anchor_points: tuple[str, ...]

The tracked point observing each anchor body, from the binding.

Empty where the binding puts no point on an anchor's origin; the registration needs only three finite anchors.

dof_names property

dof_names: list[str]

Every chain's DOF names, concatenated in chain order.

chain

chain(name: str) -> Chain | None

The chain of that name, or None if this articulation has none.

Source code in src/deeperfly/ik/articulation.py
def chain(self, name: str) -> Chain | None:
    """The chain of that name, or `None` if this articulation has none."""
    return next((c for c in self.chains if c.name == name), None)

load classmethod

load(
    ref: str | Path = DEFAULT_ARTICULATION_PATH,
    *,
    binding: Binding
    | str
    | Path
    | None = "fly38@neuromechfly",
    anchors: tuple[str, ...] = _DEFAULT_ANCHORS,
    fit: tuple[str, ...] | None = None,
    bounds_overrides: dict[str, tuple[float, float]]
    | None = None,
    marker_overrides: dict[str, dict[str, dict]]
    | None = None,
) -> Articulation

Load the baked articulation, with this run's restrictions applied.

binding's rows on the chain bodies are the chains' markers and its rows on the anchor bodies say which point observes each; None loads the structure with no markers, readable but not fittable. fit keeps a subset of the chains, bounds_overrides maps flygym angle names to degree limits (case-insensitive), and marker_overrides replaces a chain's binding rows ([ik.markers]).

Source code in src/deeperfly/ik/articulation.py
@classmethod
def load(
    cls,
    ref: str | Path = DEFAULT_ARTICULATION_PATH,
    *,
    binding: "Binding | str | Path | None" = "fly38@neuromechfly",
    anchors: tuple[str, ...] = _DEFAULT_ANCHORS,
    fit: tuple[str, ...] | None = None,
    bounds_overrides: dict[str, tuple[float, float]] | None = None,
    marker_overrides: dict[str, dict[str, dict]] | None = None,
) -> Articulation:
    """Load the baked articulation, with this run's restrictions applied.

    `binding`'s rows on the chain bodies are the chains' markers and its rows on
    the anchor bodies say which point observes each; `None` loads the structure
    with no markers, readable but not fittable. `fit` keeps a subset of the
    chains, `bounds_overrides` maps flygym angle names to degree limits
    (case-insensitive), and `marker_overrides` replaces a chain's binding rows
    (`[ik.markers]`).
    """
    if binding is not None and not isinstance(binding, Binding):
        binding = Binding.load(binding)
    spec = json.loads(Path(ref).read_text())
    overrides = {k.lower(): v for k, v in (bounds_overrides or {}).items()}
    bodies = spec.get("bodies", {})
    missing = [a for a in anchors if a not in bodies]
    if missing:
        raise ValueError(
            f"the model pack names registration anchor(s) {missing}, which its "
            f"articulation asset {Path(ref).name} does not bake a body frame for "
            f"(it has {sorted(bodies)})"
        )
    markers = marker_overrides or {}
    chains = []
    for c in spec["chains"]:
        if fit is not None and c["name"] not in fit:
            continue
        table = markers.get(c["name"])
        if table is None:
            table = (
                {}
                if binding is None
                else _chain_binding_markers(c["name"], binding, bodies)
            )
        resolved, base_point = _resolve_markers(c, table, bodies)
        chains.append(_build_chain(c, overrides, resolved, base_point))
    return cls(
        chains=tuple(chains),
        anchors=tuple(anchors),
        anchor_neutral=np.asarray(
            [bodies[a]["pos"] for a in anchors], dtype=float
        ).reshape(len(anchors), 3),
        _anchor_points=tuple(
            "" if binding is None else (binding.origin_point_for(a) or "")
            for a in anchors
        ),
        bodies=bodies,
        leg_rest={str(k): float(v) for k, v in spec.get("leg_rest", {}).items()},
    )

BodyPlan dataclass

A generated QuickIK body plan plus the bookkeeping its caller needs.

Attributes:

Name Type Description
plan dict

The body-plan mapping, JSON-serializable (see to_json).

joint_names tuple[str, ...]

(N,) joint names in body-plan order, which is the order QuickIK takes observations in and lays its DOF state out in.

joint_point tuple[str | None, ...]

(N,) the skeleton point each joint tracks, or None (the root, the head/abdomen hinges).

joint_row ndarray

(N,) each joint's column in the skeleton's point order, -1 where it tracks nothing.

joint_branch, dof_branch

(N,) / (D,) the limb each joint / DOF belongs to (a leg, a chain, or "" for the root).

angle_names tuple[str, ...]

(D,) the flygym angle names in flat DOF order, the names of a run's ik/angles.

neutral ndarray

(D,) each DOF's neutral value, the start pose and the prior's target.

body_sim tuple[ndarray, float, ndarray]

(R, s, t) mapping the model frame onto the recording's world frame (to_model, to_world).

chain_scales dict[str, float]

chain name -> size, baked into the plan's chain offsets.

chain_offsets dict[str, ndarray]

chain name -> (3,) model-unit shift of a chain's base onto its measured landmark, baked in likewise (ADR-0066).

Source code in src/deeperfly/ik/bodyplan.py
@dataclass(frozen=True)
class BodyPlan:
    """A generated QuickIK body plan plus the bookkeeping its caller needs.

    Attributes:
        plan: The body-plan mapping, JSON-serializable (see [to_json][]).
        joint_names: `(N,)` joint names in body-plan order, which is the order QuickIK
            takes observations in and lays its DOF state out in.
        joint_point: `(N,)` the skeleton point each joint tracks, or `None` (the root,
            the head/abdomen hinges).
        joint_row: `(N,)` each joint's column in the skeleton's point order, `-1`
            where it tracks nothing.
        joint_branch, dof_branch: `(N,)` / `(D,)` the limb each joint / DOF belongs to
            (a leg, a chain, or `""` for the root).
        angle_names: `(D,)` the flygym angle names in flat DOF order, the
            `names` of a run's `ik/angles`.
        neutral: `(D,)` each DOF's neutral value, the start pose and the prior's
            target.
        body_sim: `(R, s, t)` mapping the model frame onto the recording's world
            frame ([to_model][], [to_world][]).
        chain_scales: `chain name -> size`, baked into the plan's chain offsets.
        chain_offsets: `chain name -> (3,)` model-unit shift of a chain's base onto
            its measured landmark, baked in likewise (ADR-0066).
    """

    plan: dict
    joint_names: tuple[str, ...]
    joint_point: tuple[str | None, ...]
    joint_row: np.ndarray
    joint_branch: tuple[str, ...]
    angle_names: tuple[str, ...]
    dof_branch: tuple[str, ...]
    neutral: np.ndarray
    body_sim: tuple[np.ndarray, float, np.ndarray]
    chain_scales: dict[str, float] = field(default_factory=dict)
    chain_offsets: dict[str, np.ndarray] = field(default_factory=dict)

    # -- views ---------------------------------------------------------------

    @property
    def n_joints(self) -> int:
        """How many joints the plan has."""
        return len(self.joint_names)

    @property
    def n_dofs(self) -> int:
        """How many degrees of freedom the plan has -- the `D` axis of the angles."""
        return len(self.angle_names)

    @property
    def fixed_base(self) -> bool:
        """Whether the body is held in the model frame instead of fitted per frame."""
        return bool(self.plan["fixed_base"])

    def kinematics(self) -> PlanKinematics:
        """Compile the plan for numpy forward kinematics."""
        return PlanKinematics.from_plan(self.plan)

    @property
    def joint_is_base(self) -> Bool[np.ndarray, "N"]:
        """`(N,)` which joints are their chain's base landmark.

        A base landmark sits on its chain's rotation center, so it is no evidence about
        the chain's angles (ADR-0065). Read off the plan's own joints, so a plan rebuilt
        from JSON agrees; not "no DOF moves this joint", which a fixed base's
        thorax-coxae also satisfy.
        """
        return np.asarray(
            [bool(j.get(f"x-{_META_KEY}-base")) for j in self.plan["joints"]],
            dtype=bool,
        )

    # -- frames --------------------------------------------------------------

    def to_model(
        self, world_pts: Float[np.ndarray, "*batch 3"]
    ) -> Float[np.ndarray, "*batch 3"]:
        """World points -> the model frame: `R^T (p - t) / s`."""
        rot, scale, trans = self.body_sim
        return ((np.asarray(world_pts, dtype=float) - trans) @ rot) / max(scale, _EPS)

    def to_world(
        self, model_pts: Float[np.ndarray, "*batch 3"]
    ) -> Float[np.ndarray, "*batch 3"]:
        """Model points -> the world frame: `s R p + t` (inverse of [to_model][])."""
        rot, scale, trans = self.body_sim
        return scale * (np.asarray(model_pts, dtype=float) @ rot.T) + trans

    # -- serialization -------------------------------------------------------

    def to_json(self) -> str:
        """The plan as JSON, what QuickIK parses.

        No `default=` fallback, so a stray numpy value raises instead of being
        stringified. The `x-deeperfly` block describes what built the plan; QuickIK and
        deeperfly both ignore it.
        """
        return json.dumps(self.plan, separators=(",", ":"), sort_keys=False)

fixed_base property

fixed_base: bool

Whether the body is held in the model frame instead of fitted per frame.

joint_is_base property

joint_is_base: Bool[ndarray, N]

(N,) which joints are their chain's base landmark.

A base landmark sits on its chain's rotation center, so it is no evidence about the chain's angles (ADR-0065). Read off the plan's own joints, so a plan rebuilt from JSON agrees; not "no DOF moves this joint", which a fixed base's thorax-coxae also satisfy.

n_dofs property

n_dofs: int

How many degrees of freedom the plan has -- the D axis of the angles.

n_joints property

n_joints: int

How many joints the plan has.

kinematics

kinematics() -> PlanKinematics

Compile the plan for numpy forward kinematics.

Source code in src/deeperfly/ik/bodyplan.py
def kinematics(self) -> PlanKinematics:
    """Compile the plan for numpy forward kinematics."""
    return PlanKinematics.from_plan(self.plan)

to_json

to_json() -> str

The plan as JSON, what QuickIK parses.

No default= fallback, so a stray numpy value raises instead of being stringified. The x-deeperfly block describes what built the plan; QuickIK and deeperfly both ignore it.

Source code in src/deeperfly/ik/bodyplan.py
def to_json(self) -> str:
    """The plan as JSON, what QuickIK parses.

    No `default=` fallback, so a stray numpy value raises instead of being
    stringified. The `x-deeperfly` block describes what built the plan; QuickIK and
    deeperfly both ignore it.
    """
    return json.dumps(self.plan, separators=(",", ":"), sort_keys=False)

to_model

to_model(
    world_pts: Float[ndarray, "*batch 3"],
) -> Float[np.ndarray, "*batch 3"]

World points -> the model frame: R^T (p - t) / s.

Source code in src/deeperfly/ik/bodyplan.py
def to_model(
    self, world_pts: Float[np.ndarray, "*batch 3"]
) -> Float[np.ndarray, "*batch 3"]:
    """World points -> the model frame: `R^T (p - t) / s`."""
    rot, scale, trans = self.body_sim
    return ((np.asarray(world_pts, dtype=float) - trans) @ rot) / max(scale, _EPS)

to_world

to_world(
    model_pts: Float[ndarray, "*batch 3"],
) -> Float[np.ndarray, "*batch 3"]

Model points -> the world frame: s R p + t (inverse of to_model).

Source code in src/deeperfly/ik/bodyplan.py
def to_world(
    self, model_pts: Float[np.ndarray, "*batch 3"]
) -> Float[np.ndarray, "*batch 3"]:
    """Model points -> the world frame: `s R p + t` (inverse of [to_model][])."""
    rot, scale, trans = self.body_sim
    return scale * (np.asarray(model_pts, dtype=float) @ rot.T) + trans

IkResult dataclass

The fitted joint angles and the model pose they imply.

Attributes:

Name Type Description
angles ndarray

Joint angles in radians, NaN for un-fittable frames.

angle_names list[str]

The D angle names, flygym's <parent>-<child>-<dof> (c_thorax-rf_coxa-roll), in column order.

model_pts3d ndarray

Forward-kinematic joint positions in world coordinates, in the skeleton's point order (leg joints, antenna bases and abdomen markers filled, other points NaN), so they reproject with the skeleton's bones.

alignment Alignment

The per-leg geometry measured from the recording, in the model frame.

body_plan BodyPlan | None

The plan that was solved, to re-solve on exactly this geometry.

chain_scales dict[str, float]

chain name -> size relative to the model (ADR-0066), baked into the plan and applied to the mesh overlay; empty when no chain was fit.

chain_offsets dict[str, ndarray]

chain name -> (3,) model-unit shift of each chain's base onto its measured landmark (ADR-0066); empty for a chain with none.

residual float

The median distance from a fitted joint to its keypoint over the exactly placed points only, in model units (ADR-0065).

body_scale float

The recording's body size relative to the model, constant over the recording (ADR-0066).

Source code in src/deeperfly/ik/__init__.py
@dataclass
class IkResult:
    """The fitted joint angles and the model pose they imply.

    Attributes:
        angles: Joint angles in radians, NaN for un-fittable frames.
        angle_names: The `D` angle names, flygym's `<parent>-<child>-<dof>`
            (`c_thorax-rf_coxa-roll`), in column order.
        model_pts3d: Forward-kinematic joint positions in world coordinates, in the
            skeleton's point order (leg joints, antenna bases and abdomen markers
            filled, other points NaN), so they reproject with the skeleton's bones.
        alignment: The per-leg geometry measured from the recording, in the model
            frame.
        body_plan: The plan that was solved, to re-solve on exactly this geometry.
        chain_scales: `chain name -> size` relative to the model (ADR-0066), baked
            into the plan and applied to the mesh overlay; empty when no chain was fit.
        chain_offsets: `chain name -> (3,)` model-unit shift of each chain's base onto
            its measured landmark (ADR-0066); empty for a chain with none.
        residual: The median distance from a fitted joint to its keypoint over the
            exactly placed points only, in model units (ADR-0065).
        body_scale: The recording's body size relative to the model, constant over the
            recording (ADR-0066).
    """

    angles: np.ndarray
    angle_names: list[str]
    model_pts3d: np.ndarray
    alignment: Alignment
    body_plan: BodyPlan | None = None
    chain_scales: dict[str, float] = field(default_factory=dict)
    chain_offsets: dict[str, np.ndarray] = field(default_factory=dict)
    residual: float = float("nan")
    body_scale: float = 1.0

KinematicTemplate dataclass

A loaded, validated kinematic template (the leg chains).

Source code in src/deeperfly/ik/template.py
@dataclass(frozen=True)
class KinematicTemplate:
    """A loaded, validated kinematic template (the leg chains)."""

    name: str
    legs: tuple[LegChain, ...]
    #: The direction a segment extends along in its parent's frame, from the pack
    #: manifest: `-z` for NeuroMechFly, `+y` for flybody.
    rest_axis: tuple[float, float, float] = (0.0, 0.0, -1.0)

    # -- construction --------------------------------------------------------

    @classmethod
    def load(
        cls,
        ref: str | Path = "neuromechfly",
        *,
        legs: list[str] | None = None,
        bounds_overrides: dict[str, tuple[float, float]] | None = None,
        rest_axis: tuple[float, float, float] | None = None,
        binding: "Binding | str | Path | None" = "fly38@neuromechfly",
    ) -> KinematicTemplate:
        """Load a template by name or path, with this run's restrictions applied.

        `legs` keeps a subset, `bounds_overrides` maps flygym DOF names to degree limits
        (case-insensitive, after the per-side defaults), and `rest_axis` comes from the
        pack manifest (NeuroMechFly's `-z` when `None`). `binding` names the point
        observing each joint; `None` leaves them empty, readable but not fittable.
        """
        path = TEMPLATES.get(str(ref), None)
        path = Path(path) if path is not None else Path(ref)
        if not path.exists():
            raise FileNotFoundError(
                f"[ik] template {ref!r} not found "
                f"(known: {sorted(TEMPLATES)}, or a path to a template TOML)"
            )
        if binding is not None and not isinstance(binding, Binding):
            binding = Binding.load(binding)
        spec = tomllib.loads(path.read_text())
        return cls.from_spec(
            spec,
            legs=legs,
            bounds_overrides=bounds_overrides,
            rest_axis=rest_axis,
            binding=binding,
        )

    @classmethod
    def from_spec(
        cls,
        spec: dict,
        *,
        legs: list[str] | None = None,
        bounds_overrides: dict[str, tuple[float, float]] | None = None,
        rest_axis: tuple[float, float, float] | None = None,
        binding: "Binding | None" = None,
    ) -> KinematicTemplate:
        """Expand a parsed template mapping into per-leg chains."""
        overrides = {k.lower(): v for k, v in (bounds_overrides or {}).items()}
        # A leg entry is its id or a table declaring the side too. The id's first
        # letter is only a fallback: it gets a template named `T1_left` silently wrong.
        declared = {}
        all_legs: list[str] = []
        for entry in spec.get("legs", []):
            if isinstance(entry, dict):
                name = str(entry["name"])
                declared[name] = str(entry["side"])
            else:
                name = str(entry)
            all_legs.append(name)
        chosen = all_legs if legs is None else list(legs)
        unknown = [leg for leg in chosen if leg not in all_legs]
        if unknown:
            raise ValueError(
                f"unknown leg(s) {unknown}; the template defines {all_legs}"
            )
        # A flat [bounds] table applies to both sides; [bounds.l] / [bounds.r]
        # override it per side.
        raw = dict(spec.get("bounds", {}))
        flat = {k: v for k, v in raw.items() if k not in ("l", "r")}
        bounds_by_side = {
            side: {**flat, **dict(raw.get(side, {}))} for side in ("l", "r")
        }
        chains = tuple(
            _build_leg(
                leg,
                declared.get(leg),
                spec["joints"],
                bounds_by_side,
                overrides,
                binding,
            )
            for leg in chosen
        )
        rest = rest_axis if rest_axis is not None else (0.0, 0.0, -1.0)
        rx, ry, rz = (float(v) for v in rest)
        return cls(
            name=spec.get("name", "template"),
            legs=chains,
            rest_axis=(rx, ry, rz),
        )

    # -- views ---------------------------------------------------------------

    @property
    def dof_names(self) -> list[str]:
        """Every leg DOF name, in template order."""
        return [name for leg in self.legs for name in leg.dof_names]

    @property
    def model_point_names(self) -> list[str]:
        """The skeleton points the legs predict (every leg joint, in chain order)."""
        out: list[str] = []
        for leg in self.legs:
            for name in leg.point_names:
                if name not in out:
                    out.append(name)
        return out

dof_names property

dof_names: list[str]

Every leg DOF name, in template order.

model_point_names property

model_point_names: list[str]

The skeleton points the legs predict (every leg joint, in chain order).

from_spec classmethod

from_spec(
    spec: dict,
    *,
    legs: list[str] | None = None,
    bounds_overrides: dict[str, tuple[float, float]]
    | None = None,
    rest_axis: tuple[float, float, float] | None = None,
    binding: Binding | None = None,
) -> KinematicTemplate

Expand a parsed template mapping into per-leg chains.

Source code in src/deeperfly/ik/template.py
@classmethod
def from_spec(
    cls,
    spec: dict,
    *,
    legs: list[str] | None = None,
    bounds_overrides: dict[str, tuple[float, float]] | None = None,
    rest_axis: tuple[float, float, float] | None = None,
    binding: "Binding | None" = None,
) -> KinematicTemplate:
    """Expand a parsed template mapping into per-leg chains."""
    overrides = {k.lower(): v for k, v in (bounds_overrides or {}).items()}
    # A leg entry is its id or a table declaring the side too. The id's first
    # letter is only a fallback: it gets a template named `T1_left` silently wrong.
    declared = {}
    all_legs: list[str] = []
    for entry in spec.get("legs", []):
        if isinstance(entry, dict):
            name = str(entry["name"])
            declared[name] = str(entry["side"])
        else:
            name = str(entry)
        all_legs.append(name)
    chosen = all_legs if legs is None else list(legs)
    unknown = [leg for leg in chosen if leg not in all_legs]
    if unknown:
        raise ValueError(
            f"unknown leg(s) {unknown}; the template defines {all_legs}"
        )
    # A flat [bounds] table applies to both sides; [bounds.l] / [bounds.r]
    # override it per side.
    raw = dict(spec.get("bounds", {}))
    flat = {k: v for k, v in raw.items() if k not in ("l", "r")}
    bounds_by_side = {
        side: {**flat, **dict(raw.get(side, {}))} for side in ("l", "r")
    }
    chains = tuple(
        _build_leg(
            leg,
            declared.get(leg),
            spec["joints"],
            bounds_by_side,
            overrides,
            binding,
        )
        for leg in chosen
    )
    rest = rest_axis if rest_axis is not None else (0.0, 0.0, -1.0)
    rx, ry, rz = (float(v) for v in rest)
    return cls(
        name=spec.get("name", "template"),
        legs=chains,
        rest_axis=(rx, ry, rz),
    )

load classmethod

load(
    ref: str | Path = "neuromechfly",
    *,
    legs: list[str] | None = None,
    bounds_overrides: dict[str, tuple[float, float]]
    | None = None,
    rest_axis: tuple[float, float, float] | None = None,
    binding: Binding
    | str
    | Path
    | None = "fly38@neuromechfly",
) -> KinematicTemplate

Load a template by name or path, with this run's restrictions applied.

legs keeps a subset, bounds_overrides maps flygym DOF names to degree limits (case-insensitive, after the per-side defaults), and rest_axis comes from the pack manifest (NeuroMechFly's -z when None). binding names the point observing each joint; None leaves them empty, readable but not fittable.

Source code in src/deeperfly/ik/template.py
@classmethod
def load(
    cls,
    ref: str | Path = "neuromechfly",
    *,
    legs: list[str] | None = None,
    bounds_overrides: dict[str, tuple[float, float]] | None = None,
    rest_axis: tuple[float, float, float] | None = None,
    binding: "Binding | str | Path | None" = "fly38@neuromechfly",
) -> KinematicTemplate:
    """Load a template by name or path, with this run's restrictions applied.

    `legs` keeps a subset, `bounds_overrides` maps flygym DOF names to degree limits
    (case-insensitive, after the per-side defaults), and `rest_axis` comes from the
    pack manifest (NeuroMechFly's `-z` when `None`). `binding` names the point
    observing each joint; `None` leaves them empty, readable but not fittable.
    """
    path = TEMPLATES.get(str(ref), None)
    path = Path(path) if path is not None else Path(ref)
    if not path.exists():
        raise FileNotFoundError(
            f"[ik] template {ref!r} not found "
            f"(known: {sorted(TEMPLATES)}, or a path to a template TOML)"
        )
    if binding is not None and not isinstance(binding, Binding):
        binding = Binding.load(binding)
    spec = tomllib.loads(path.read_text())
    return cls.from_spec(
        spec,
        legs=legs,
        bounds_overrides=bounds_overrides,
        rest_axis=rest_axis,
        binding=binding,
    )

MissingQuickIK

Bases: ImportError

Raised when inverse kinematics is asked for without the ik extra installed.

An ImportError, so a caller that can fall back to a stored fit catches it like any optional import.

Source code in src/deeperfly/ik/_quickik.py
class MissingQuickIK(ImportError):
    """Raised when inverse kinematics is asked for without the `ik` extra installed.

    An `ImportError`, so a caller that can fall back to a stored fit catches it like any
    optional import.
    """

Model dataclass

A [ik] model resolved against one skeleton: the pack and its three assets.

Loaded as one unit, since a template from one pack with an articulation from another would be silently wrong; the run config's bounds and markers are applied.

Attributes:

Name Type Description
pack ModelPack

The manifest and its asset paths.

binding Binding

The (skeleton, model) pairing.

template KinematicTemplate

The leg chains.

articulation Articulation | None

The baked head/abdomen chains, or None when the run config's chains selects none of them.

Source code in src/deeperfly/ik/__init__.py
@dataclass(frozen=True)
class Model:
    """A `[ik]` model resolved against one skeleton: the pack and its three assets.

    Loaded as one unit, since a template from one pack with an articulation from
    another would be silently wrong; the run config's `bounds` and `markers` are
    applied.

    Attributes:
        pack: The manifest and its asset paths.
        binding: The (skeleton, model) pairing.
        template: The leg chains.
        articulation: The baked head/abdomen chains, or `None` when the run config's
            `chains` selects none of them.
    """

    pack: ModelPack
    binding: Binding
    template: KinematicTemplate
    articulation: Articulation | None

ModelMesh dataclass

The baked NeuroMechFly overlay mesh and the data to pose it from a fit.

Attributes:

Name Type Description
vertices Float[ndarray, 'Nv 3']

(Nv, 3) neutral-pose vertices in the model's world frame.

faces Int[ndarray, 'Nf 3']

(Nf, 3) triangle vertex indices into vertices, counterclockwise seen from outside; every part is closed (ADR-0099).

face_rgb ndarray

(Nf, 3) uint8 per-face color.

vert_slot Int[ndarray, Nv]

(Nv,) the render slot of each vertex: 0 is the rigid body, >= 1 are leg bones.

slot_prox Int[ndarray, S]

(n_slots,) the proximal keypoint index of each leg bone, in the skeleton's order; -1 for the body and articulated-node slots.

slot_dist Int[ndarray, S]

(n_slots,) the distal keypoint index, same convention.

slot_chain Int[ndarray, S]

(n_slots,) the articulation chain index of each node slot (the head and abdomen-segment meshes); -1 for body and leg-bone slots.

slot_depth Int[ndarray, S]

(n_slots,) that node slot's depth along its chain, same convention.

kp_neutral Float[ndarray, 'P 3']

(P, 3) the model's neutral keypoint positions (skeleton order).

anchor_idx Int[ndarray, 6]

(6,) the keypoint indices observing the model's anchor bodies, which place the rigid body transform.

vert_part Int[ndarray, Nv]

(Nv,) each vertex's body-part index into part_names (so the overlay can hide whole parts -- e.g. the wings -- per the render config).

part_names tuple[str, ...]

The body-part names, indexed by vert_part ("wings", "legs", "head", "thorax", "abdomen", "eyes", "antennae", "halteres").

Source code in src/deeperfly/ik/mesh.py
@dataclass(frozen=True)
class ModelMesh:
    """The baked NeuroMechFly overlay mesh and the data to pose it from a fit.

    Attributes:
        vertices: `(Nv, 3)` neutral-pose vertices in the model's world frame.
        faces: `(Nf, 3)` triangle vertex indices into `vertices`, counterclockwise
            seen from outside; every part is closed (ADR-0099).
        face_rgb: `(Nf, 3)` uint8 per-face color.
        vert_slot: `(Nv,)` the render slot of each vertex: `0` is the rigid body,
            `>= 1` are leg bones.
        slot_prox: `(n_slots,)` the proximal keypoint index of each leg bone, in the
            skeleton's order; `-1` for the body and articulated-node slots.
        slot_dist: `(n_slots,)` the distal keypoint index, same convention.
        slot_chain: `(n_slots,)` the articulation chain index of each node slot (the
            head and abdomen-segment meshes); `-1` for body and leg-bone slots.
        slot_depth: `(n_slots,)` that node slot's depth along its chain, same
            convention.
        kp_neutral: `(P, 3)` the model's neutral keypoint positions (skeleton order).
        anchor_idx: `(6,)` the keypoint indices observing the model's anchor bodies,
            which place the rigid body transform.
        vert_part: `(Nv,)` each vertex's body-part index into `part_names` (so
            the overlay can hide whole parts -- e.g. the wings -- per the render
            config).
        part_names: The body-part names, indexed by `vert_part` (`"wings"`,
            `"legs"`, `"head"`, `"thorax"`, `"abdomen"`, `"eyes"`, `"antennae"`,
            `"halteres"`).
    """

    vertices: Float[np.ndarray, "Nv 3"]
    faces: Int[np.ndarray, "Nf 3"]
    face_rgb: np.ndarray
    vert_slot: Int[np.ndarray, "Nv"]
    slot_prox: Int[np.ndarray, "S"]
    slot_dist: Int[np.ndarray, "S"]
    slot_chain: Int[np.ndarray, "S"]
    slot_depth: Int[np.ndarray, "S"]
    kp_neutral: Float[np.ndarray, "P 3"]
    anchor_idx: Int[np.ndarray, "6"]
    vert_part: Int[np.ndarray, "Nv"]
    part_names: tuple[str, ...]

    @classmethod
    def load(cls, path: str | Path = DEFAULT_MESH_PATH) -> "ModelMesh":
        """Read a baked mesh asset. Absent optional arrays fall back to "none"."""
        z = np.load(Path(path), allow_pickle=True)
        n_slots = z["slot_prox"].shape[0]
        nofill = np.full(n_slots, -1, dtype=np.int64)
        n_verts = z["vertices"].shape[0]
        return cls(
            vertices=z["vertices"].astype(float),
            faces=z["faces"].astype(np.int64),
            face_rgb=z["face_rgb"],
            vert_slot=z["vert_slot"].astype(np.int64),
            slot_prox=z["slot_prox"].astype(np.int64),
            slot_dist=z["slot_dist"].astype(np.int64),
            slot_chain=(
                z["slot_chain"].astype(np.int64) if "slot_chain" in z else nofill
            ),
            slot_depth=(
                z["slot_depth"].astype(np.int64) if "slot_depth" in z else nofill
            ),
            kp_neutral=z["kp_neutral"].astype(float),
            anchor_idx=z["anchor_idx"].astype(np.int64),
            vert_part=(
                z["vert_part"].astype(np.int64)
                if "vert_part" in z
                else np.zeros(n_verts, dtype=np.int64)
            ),
            part_names=(
                tuple(str(p) for p in z["part_names"]) if "part_names" in z else ()
            ),
        )

    def hidden_face_mask(self, hide_parts: "Sequence[str]") -> np.ndarray:
        """`(Nf,)` bool: faces belonging to a body part named in `hide_parts`.

        An unknown name hides nothing.
        """
        key = tuple(hide_parts or ())
        hit = self._hidden_masks.get(key)
        if hit is None:
            hit = self._hidden_masks.setdefault(key, self._compute_hidden_mask(key))
        return hit

    @functools.cached_property
    def _hidden_masks(self) -> dict[tuple[str, ...], np.ndarray]:
        """Per-part-list mask cache, a dict because this dataclass is not hashable."""
        return {}

    def _compute_hidden_mask(self, hide_parts: tuple[str, ...]) -> np.ndarray:
        """[hidden_face_mask][] without the cache."""
        n_faces = self.faces.shape[0]
        if not hide_parts or not self.part_names:
            return np.zeros(n_faces, dtype=bool)
        index = {name: i for i, name in enumerate(self.part_names)}
        hide_ids = [index[p] for p in hide_parts if p in index]
        if not hide_ids:
            return np.zeros(n_faces, dtype=bool)
        face_part = self.vert_part[self.faces[:, 0]]
        return np.isin(face_part, hide_ids)

    @functools.cached_property
    def _slot_verts(self) -> list[np.ndarray]:
        """Vertex-row indices for each slot (so posing is a few vectorized blocks)."""
        n = int(self.slot_prox.shape[0])
        return [np.flatnonzero(self.vert_slot == s) for s in range(n)]

    @functools.cached_property
    def _dist_anchor(self) -> Float[np.ndarray, "S 3"]:
        """`(n_slots, 3)` neutral distal endpoint each leg bone is skinned to.

        A bone's distal keypoint, except on the terminal segment, whose baked mesh stops
        short of the pretarsus: there it is the mesh's farthest point along the bone, so
        skinning stretches the tip onto the live pretarsus.
        """
        n = int(self.slot_prox.shape[0])
        anchor = np.full((n, 3), np.nan)
        prox = {int(p) for p in self.slot_prox.tolist() if p >= 0}
        for s in range(n):
            pi, di = int(self.slot_prox[s]), int(self.slot_dist[s])
            if pi < 0 or di < 0:
                continue
            a0, b0 = self.kp_neutral[pi], self.kp_neutral[di]
            anchor[s] = b0
            if di in prox:  # a mid-chain joint shared with the next segment: keep it
                continue
            axis = b0 - a0
            length = float(np.linalg.norm(axis))
            v = self.vertices[self._slot_verts[s]]
            if length < _EPS or v.shape[0] == 0:
                continue
            reach = float(
                (((v - a0) @ axis) / length**2).max()
            )  # farthest vertex / |axis|
            if reach > _EPS:
                anchor[s] = a0 + reach * axis
        return anchor

    def pose(
        self,
        pts3d: Float[np.ndarray, "P 3"],
        angles: Float[np.ndarray, "D"] | None = None,
        angle_names: list[str] | None = None,
        *,
        chain_scales: dict[str, float] | None = None,
        chain_offsets: dict[str, np.ndarray] | None = None,
        body_scale: float | None = None,
    ) -> tuple[Float[np.ndarray, "Nv 3"], np.ndarray]:
        """Pose the mesh to one frame's fitted joints (and optional chain angles).

        `pts3d` places the rigid body and skins the leg bones. With `angles` (named by
        `angle_names`) the head and abdomen nodes articulate; without, they ride the
        body at their neutral pose. `chain_scales`, `chain_offsets` and `body_scale`
        are the fit's (ADR-0066), so the drawn chains sit where the angles were fitted;
        `body_scale=None` re-fits the scale per frame.

        Returns:
            verts: `(Nv, 3)` posed world vertices, NaN for un-poseable segments.
            valid_faces: `(Nf,)` whether each face's three vertices are finite.
        """
        pts3d = np.asarray(pts3d, dtype=float)
        out = np.full_like(self.vertices, np.nan)

        rot, scale, trans = self._body_transform(pts3d, fixed_scale=body_scale)
        up = rot @ np.array([0.0, 0.0, 1.0])  # live dorsal axis fixes the bone roll
        node_xform = self._node_transforms(
            angles, angle_names, chain_scales, chain_offsets
        )

        for slot, rows in enumerate(self._slot_verts):
            if rows.size == 0:
                continue
            v = self.vertices[rows]
            if self.slot_chain[slot] >= 0:  # articulated head / abdomen node
                a, b = node_xform[
                    int(self.slot_chain[slot]), int(self.slot_depth[slot])
                ]
                out[rows] = scale * ((v @ a.T + b) @ rot.T) + trans
            elif self.slot_prox[slot] >= 0:  # leg bone -> skin between its endpoints
                a0 = self.kp_neutral[self.slot_prox[slot]]
                b0 = self._dist_anchor[
                    slot
                ]  # tarsus tip is stretched out to the pretarsus
                a1 = pts3d[self.slot_prox[slot]]
                b1 = pts3d[self.slot_dist[slot]]
                if not (np.isfinite(a1).all() and np.isfinite(b1).all()):
                    continue  # an occluded leg segment stays NaN -> dropped
                out[rows] = _skin_bone(v, a0, b0, a1, b1, up)
            else:  # slot 0: rigid body (thorax / wings / halteres)
                out[rows] = scale * (v @ rot.T) + trans

        valid_faces = np.isfinite(out[self.faces]).all(axis=(1, 2))
        return out, valid_faces

    def _node_transforms(
        self,
        angles: np.ndarray | None,
        angle_names: list[str] | None,
        chain_scales: dict[str, float] | None = None,
        chain_offsets: dict[str, np.ndarray] | None = None,
    ) -> dict[tuple[int, int], tuple[np.ndarray, np.ndarray]]:
        """Model-frame affine `(A, b)` per `(chain, depth)` node from the angles.

        Missing angles leave a node at its neutral pose. A chain scale `f` folds in as
        `A' = f A`, `b' = f b + (1 - f) base` and a chain offset `d` as `b + d`, the
        terms the body plan bakes in. Driven by the packaged articulation and the angle
        names rather than the recording's plan, so a result file from any version
        articulates.
        """
        from .articulation import load_articulation
        from .forward import chain_affine

        wanted = {
            (int(c), int(d))
            for c, d in zip(self.slot_chain, self.slot_depth, strict=True)
            if c >= 0
        }
        out: dict[tuple[int, int], tuple[np.ndarray, np.ndarray]] = {}
        if not wanted:  # no articulated node: do not read the articulation asset
            return out
        art = load_articulation()
        col = {name: i for i, name in enumerate(angle_names or [])}
        for chain_idx, depth in wanted:
            a, b = np.eye(3), np.zeros(3)
            if chain_idx < len(art.chains):
                chain = art.chains[chain_idx]
                if angles is not None and angle_names is not None:
                    theta = np.array(
                        [
                            float(angles[col[n]]) if n in col else np.nan
                            for n in chain.dof_names
                        ]
                    )
                    if np.isfinite(theta).all():
                        a, b = chain_affine(chain, depth, theta)
                f = float((chain_scales or {}).get(chain.name, 1.0))
                if f != 1.0:  # grow the node about its chain base anchor
                    base = chain.anchors[0]
                    a, b = f * a, f * b + (1.0 - f) * base
                shift = (chain_offsets or {}).get(chain.name)
                if shift is not None:  # then put that base where it was measured
                    b = b + np.asarray(shift, dtype=float)
            out[(chain_idx, depth)] = (a, b)
        return out

    def _body_transform(
        self,
        pts3d: Float[np.ndarray, "P 3"],
        fixed_scale: float | None = None,
    ) -> tuple[np.ndarray, float, np.ndarray]:
        """Body placement `(R, s, t)` from the neutral coxae to the live coxae.

        `fixed_scale` holds the size at the recording's constant (ADR-0066); `None` fits
        it per frame.
        """
        src = self.kp_neutral[self.anchor_idx]
        dst = pts3d[self.anchor_idx]
        good = np.isfinite(dst).all(axis=1)
        if int(good.sum()) < 3:  # too few anchors to place the body
            fallback = 1.0 if fixed_scale is None else float(fixed_scale)
            return np.eye(3), fallback, np.zeros(3)
        from .articulation import umeyama

        rot, scale, trans = umeyama(src[good], dst[good])
        if fixed_scale is not None:
            scale = float(fixed_scale)
            trans = dst[good].mean(axis=0) - scale * (rot @ src[good].mean(axis=0))
        return rot, scale, trans

hidden_face_mask

hidden_face_mask(hide_parts: Sequence[str]) -> np.ndarray

(Nf,) bool: faces belonging to a body part named in hide_parts.

An unknown name hides nothing.

Source code in src/deeperfly/ik/mesh.py
def hidden_face_mask(self, hide_parts: "Sequence[str]") -> np.ndarray:
    """`(Nf,)` bool: faces belonging to a body part named in `hide_parts`.

    An unknown name hides nothing.
    """
    key = tuple(hide_parts or ())
    hit = self._hidden_masks.get(key)
    if hit is None:
        hit = self._hidden_masks.setdefault(key, self._compute_hidden_mask(key))
    return hit

load classmethod

load(path: str | Path = DEFAULT_MESH_PATH) -> ModelMesh

Read a baked mesh asset. Absent optional arrays fall back to "none".

Source code in src/deeperfly/ik/mesh.py
@classmethod
def load(cls, path: str | Path = DEFAULT_MESH_PATH) -> "ModelMesh":
    """Read a baked mesh asset. Absent optional arrays fall back to "none"."""
    z = np.load(Path(path), allow_pickle=True)
    n_slots = z["slot_prox"].shape[0]
    nofill = np.full(n_slots, -1, dtype=np.int64)
    n_verts = z["vertices"].shape[0]
    return cls(
        vertices=z["vertices"].astype(float),
        faces=z["faces"].astype(np.int64),
        face_rgb=z["face_rgb"],
        vert_slot=z["vert_slot"].astype(np.int64),
        slot_prox=z["slot_prox"].astype(np.int64),
        slot_dist=z["slot_dist"].astype(np.int64),
        slot_chain=(
            z["slot_chain"].astype(np.int64) if "slot_chain" in z else nofill
        ),
        slot_depth=(
            z["slot_depth"].astype(np.int64) if "slot_depth" in z else nofill
        ),
        kp_neutral=z["kp_neutral"].astype(float),
        anchor_idx=z["anchor_idx"].astype(np.int64),
        vert_part=(
            z["vert_part"].astype(np.int64)
            if "vert_part" in z
            else np.zeros(n_verts, dtype=np.int64)
        ),
        part_names=(
            tuple(str(p) for p in z["part_names"]) if "part_names" in z else ()
        ),
    )

pose

pose(
    pts3d: Float[ndarray, "P 3"],
    angles: Float[ndarray, D] | None = None,
    angle_names: list[str] | None = None,
    *,
    chain_scales: dict[str, float] | None = None,
    chain_offsets: dict[str, ndarray] | None = None,
    body_scale: float | None = None,
) -> tuple[Float[np.ndarray, "Nv 3"], np.ndarray]

Pose the mesh to one frame's fitted joints (and optional chain angles).

pts3d places the rigid body and skins the leg bones. With angles (named by angle_names) the head and abdomen nodes articulate; without, they ride the body at their neutral pose. chain_scales, chain_offsets and body_scale are the fit's (ADR-0066), so the drawn chains sit where the angles were fitted; body_scale=None re-fits the scale per frame.

Returns:

Name Type Description
verts Float[ndarray, 'Nv 3']

(Nv, 3) posed world vertices, NaN for un-poseable segments.

valid_faces ndarray

(Nf,) whether each face's three vertices are finite.

Source code in src/deeperfly/ik/mesh.py
def pose(
    self,
    pts3d: Float[np.ndarray, "P 3"],
    angles: Float[np.ndarray, "D"] | None = None,
    angle_names: list[str] | None = None,
    *,
    chain_scales: dict[str, float] | None = None,
    chain_offsets: dict[str, np.ndarray] | None = None,
    body_scale: float | None = None,
) -> tuple[Float[np.ndarray, "Nv 3"], np.ndarray]:
    """Pose the mesh to one frame's fitted joints (and optional chain angles).

    `pts3d` places the rigid body and skins the leg bones. With `angles` (named by
    `angle_names`) the head and abdomen nodes articulate; without, they ride the
    body at their neutral pose. `chain_scales`, `chain_offsets` and `body_scale`
    are the fit's (ADR-0066), so the drawn chains sit where the angles were fitted;
    `body_scale=None` re-fits the scale per frame.

    Returns:
        verts: `(Nv, 3)` posed world vertices, NaN for un-poseable segments.
        valid_faces: `(Nf,)` whether each face's three vertices are finite.
    """
    pts3d = np.asarray(pts3d, dtype=float)
    out = np.full_like(self.vertices, np.nan)

    rot, scale, trans = self._body_transform(pts3d, fixed_scale=body_scale)
    up = rot @ np.array([0.0, 0.0, 1.0])  # live dorsal axis fixes the bone roll
    node_xform = self._node_transforms(
        angles, angle_names, chain_scales, chain_offsets
    )

    for slot, rows in enumerate(self._slot_verts):
        if rows.size == 0:
            continue
        v = self.vertices[rows]
        if self.slot_chain[slot] >= 0:  # articulated head / abdomen node
            a, b = node_xform[
                int(self.slot_chain[slot]), int(self.slot_depth[slot])
            ]
            out[rows] = scale * ((v @ a.T + b) @ rot.T) + trans
        elif self.slot_prox[slot] >= 0:  # leg bone -> skin between its endpoints
            a0 = self.kp_neutral[self.slot_prox[slot]]
            b0 = self._dist_anchor[
                slot
            ]  # tarsus tip is stretched out to the pretarsus
            a1 = pts3d[self.slot_prox[slot]]
            b1 = pts3d[self.slot_dist[slot]]
            if not (np.isfinite(a1).all() and np.isfinite(b1).all()):
                continue  # an occluded leg segment stays NaN -> dropped
            out[rows] = _skin_bone(v, a0, b0, a1, b1, up)
        else:  # slot 0: rigid body (thorax / wings / halteres)
            out[rows] = scale * (v @ rot.T) + trans

    valid_faces = np.isfinite(out[self.faces]).all(axis=(1, 2))
    return out, valid_faces

load_model

load_model(
    skeleton: Skeleton,
    *,
    model: str = "neuromechfly",
    binding: str | None = None,
    chains: Sequence[str] = (),
    bounds: Mapping[str, Sequence[float]] | None = None,
    markers: Mapping[str, Mapping[str, Any]] | None = None,
) -> Model

Load a body-model pack, bound to skeleton.

model is a packaged pack name or a path to its model.toml, and binding defaults to the pair skeleton and the pack name; the rest are [ik]'s keys.

Raises:

Type Description
FileNotFoundError

If the pack or the binding cannot be found; an unbound pair names both halves.

ValueError

If the model predicts a leg joint this skeleton does not track, which would leave that joint fitted to nothing.

Source code in src/deeperfly/ik/__init__.py
def load_model(
    skeleton: Skeleton,
    *,
    model: str = "neuromechfly",
    binding: str | None = None,
    chains: Sequence[str] = (),
    bounds: Mapping[str, Sequence[float]] | None = None,
    markers: Mapping[str, Mapping[str, Any]] | None = None,
) -> Model:
    """Load a body-model pack, bound to `skeleton`.

    `model` is a packaged pack name or a path to its `model.toml`, and `binding`
    defaults to the pair `skeleton` and the pack name; the rest are `[ik]`'s keys.

    Raises:
        FileNotFoundError: If the pack or the binding cannot be found; an unbound pair
            names both halves.
        ValueError: If the model predicts a leg joint this skeleton does not track,
            which would leave that joint fitted to nothing.
    """
    pack = ModelPack.load(model)
    bound = Binding.load(binding or f"{skeleton.name}@{pack.name}")
    limits = {k: (float(v[0]), float(v[1])) for k, v in (bounds or {}).items()}
    template = KinematicTemplate.load(
        pack.template_path,
        bounds_overrides=limits,
        rest_axis=pack.rest_axis,
        binding=bound,
    )
    tracked = set(skeleton.point_names)
    missing = [n for n in template.model_point_names if n not in tracked]
    if missing:
        raise ValueError(
            f"[ik] model {model!r} predicts point(s) {missing}, which skeleton "
            f"{skeleton.name!r} does not track. Every observation of them would be "
            "missing, so the fit would be pinned by its neutral pose rather than by "
            "the data."
        )
    articulation = Articulation.load(
        pack.articulation_path,
        binding=bound,
        anchors=pack.anchors,
        fit=tuple(chains) or None,
        bounds_overrides=limits,
        marker_overrides=dict(markers or {}),
    )
    return Model(pack=pack, binding=bound, template=template, articulation=articulation)

load_model_mesh cached

load_model_mesh(
    path: str | Path = DEFAULT_MESH_PATH,
) -> ModelMesh

Load (and cache) the packaged NeuroMechFly overlay mesh.

Source code in src/deeperfly/ik/mesh.py
@functools.lru_cache(maxsize=2)
def load_model_mesh(path: str | Path = DEFAULT_MESH_PATH) -> ModelMesh:
    """Load (and cache) the packaged NeuroMechFly overlay mesh."""
    return ModelMesh.load(path)

observations

observations(
    pts3d: ndarray, plan: BodyPlan, weights: ndarray | None
) -> tuple[np.ndarray, np.ndarray]

Gather the plan's observations: (T, N, 3) model positions and (T, N) weights.

QuickIK reads a missing observation (a joint that tracks no keypoint, a NaN, a non-positive weight) from its zero weight, and its position is filled with zero.

Source code in src/deeperfly/ik/__init__.py
def observations(
    pts3d: np.ndarray, plan: BodyPlan, weights: np.ndarray | None
) -> tuple[np.ndarray, np.ndarray]:
    """Gather the plan's observations: `(T, N, 3)` model positions and `(T, N)` weights.

    QuickIK reads a missing observation (a joint that tracks no keypoint, a NaN, a
    non-positive weight) from its zero weight, and its position is filled with zero.
    """
    n_frames = pts3d.shape[0]
    rows = plan.joint_row
    tracked = rows >= 0

    model = plan.to_model(pts3d)  # (T, P, 3)
    positions = np.zeros((n_frames, plan.n_joints, 3))
    positions[:, tracked] = model[:, rows[tracked]]

    obs = np.zeros((n_frames, plan.n_joints))
    if weights is None:
        obs[:, tracked] = 1.0
    else:
        w = np.asarray(weights, dtype=float)
        obs[:, tracked] = np.where(
            np.isfinite(w[:, rows[tracked]]), w[:, rows[tracked]], 0.0
        )
    obs[~np.isfinite(positions).all(axis=-1)] = 0.0
    positions[~np.isfinite(positions)] = 0.0
    return positions, np.clip(obs, 0.0, None)

solve

solve(
    pts3d: Float[ndarray, "T P 3"],
    skeleton: Skeleton,
    template: KinematicTemplate,
    *,
    articulation: Articulation | None = None,
    weights: Float[ndarray, "T P"] | None = None,
    iterations: int = 60,
    neutral_weight: float = 0.001,
    damping: float = 0.1,
    position_tolerance: float = 0.001,
    angle_tolerance: float = 0.001,
    fixed_body: bool = True,
    symmetric_segments: bool = True,
    absent_points: Bool[ndarray, "T P"] | None = None,
    approximate_points: Sequence[str] | None = None,
) -> IkResult

Fit the model's joint angles to a 3D pose sequence.

pts3d is in world coordinates, in skeleton's point order. articulation adds the head and abdomen chains; None fits the legs only. weights are per-observation, and a non-finite or non-positive one marks the point unobserved. absent_points declares keypoints that are not on this animal, per frame or for the whole recording, and approximate_points the ones placed by a modeling decision, read off articulation when None (ADR-0065). The rest are [ik]'s keys.

Raises:

Type Description
MissingQuickIK

If the optional deeperfly[ik] extra is not installed.

Source code in src/deeperfly/ik/__init__.py
def solve(
    pts3d: Float[np.ndarray, "T P 3"],
    skeleton: Skeleton,
    template: KinematicTemplate,
    *,
    articulation: Articulation | None = None,
    weights: Float[np.ndarray, "T P"] | None = None,
    iterations: int = 60,
    neutral_weight: float = 1e-3,
    damping: float = 0.1,
    position_tolerance: float = 1e-3,
    angle_tolerance: float = 1e-3,
    fixed_body: bool = True,
    symmetric_segments: bool = True,
    absent_points: Bool[np.ndarray, "T P"] | None = None,
    approximate_points: Sequence[str] | None = None,
) -> IkResult:
    """Fit the model's joint angles to a 3D pose sequence.

    `pts3d` is in world coordinates, in `skeleton`'s point order. `articulation` adds
    the head and abdomen chains; `None` fits the legs only. `weights` are
    per-observation, and a non-finite or non-positive one marks the point unobserved.
    `absent_points` declares keypoints that are not on this animal, per frame or for
    the whole recording, and `approximate_points` the ones placed by a modeling
    decision, read off `articulation` when `None` (ADR-0065). The rest are `[ik]`'s
    keys.

    Raises:
        MissingQuickIK: If the optional `deeperfly[ik]` extra is not installed.
    """
    quickik = require_quickik()
    pts3d = np.asarray(pts3d, dtype=float)
    n_frames, n_points = pts3d.shape[0], pts3d.shape[1]

    # The anchors live with the baked articulation; load a chain-less copy when no
    # chains are being fit, so a legs-only run still registers to the model frame.
    reference = articulation if articulation is not None else load_articulation(fit=())
    registered, sim = _register(pts3d, skeleton, reference, fixed_body)
    alignment = body_alignment(
        registered, skeleton, template, symmetric_segments=symmetric_segments
    )
    plan = _plan_for(
        registered, skeleton, template, alignment, articulation, sim, fixed_body
    )

    positions, obs_weights = observations(pts3d, plan, weights)
    angles, root_pos, root_rot = _solve(
        quickik,
        plan,
        positions,
        obs_weights,
        iterations=iterations,
        neutral_weight=neutral_weight,
        damping=damping,
        position_tolerance=position_tolerance,
        angle_tolerance=angle_tolerance,
    )

    never_placed = frozenset(
        leg.name for leg in template.legs if leg.name not in alignment.leg_origin
    )
    for name in sorted(never_placed):
        log.warning(
            "ik: leg %r has no observed thorax-coxa; its angles are left unset",
            name,
        )
    unfittable = unfittable_branches(
        obs_weights, plan, never_placed=never_placed, absent_points=absent_points
    )
    for d, branch in enumerate(plan.dof_branch):
        bad = unfittable.get(branch)
        if bad is not None:
            angles[bad, d] = np.nan

    joints_model = plan.kinematics().joint_positions(angles, root_pos, root_rot)
    joints_world = plan.to_world(joints_model)
    model_pts3d = np.full((n_frames, n_points, 3), np.nan)
    rows = plan.joint_row
    tracked = rows >= 0
    model_pts3d[:, rows[tracked]] = joints_world[:, tracked]

    finite = int(np.isfinite(angles).all(axis=0).sum())
    log.info(
        "ik: solved %d frames, %d/%d joint-angle tracks complete",
        n_frames,
        finite,
        angles.shape[1],
    )
    residual = _report_residual(
        pts3d,
        model_pts3d,
        skeleton,
        approximate_points
        if approximate_points is not None
        else _approximate_from(articulation),
    )
    _warn_about_pinned_limits(angles, plan)
    return IkResult(
        angles=angles,
        angle_names=list(plan.angle_names),
        model_pts3d=model_pts3d,
        alignment=alignment,
        body_plan=plan,
        chain_scales=plan.chain_scales,
        chain_offsets=plan.chain_offsets,
        residual=residual,
        body_scale=float(plan.body_sim[1]),
    )

unfittable_branches

unfittable_branches(
    obs_weights: Float[ndarray, "T N"],
    plan: BodyPlan,
    *,
    never_placed: frozenset[str] = frozenset(),
    absent_points: Bool[ndarray, "T P"] | None = None,
) -> dict[str, np.ndarray]

branch -> (T,) bool frames whose observations cannot support a fit.

A limb needs _MIN_VALID_JOINTS observed keypoints in a frame, and a leg in never_placed (its coxa was never seen) is fitted in no frame. absent_points, the (T, P) or (P,) declaration of keypoints not on this animal, lowers a branch's bar to what it can still determine; a missed detection never does, and a chain's base landmark never counts (ADR-0065).

Source code in src/deeperfly/ik/__init__.py
def unfittable_branches(
    obs_weights: Float[np.ndarray, "T N"],
    plan: BodyPlan,
    *,
    never_placed: frozenset[str] = frozenset(),
    absent_points: Bool[np.ndarray, "T P"] | None = None,
) -> dict[str, np.ndarray]:
    """`branch -> (T,) bool` frames whose observations cannot support a fit.

    A limb needs `_MIN_VALID_JOINTS` observed keypoints in a frame, and a leg in
    `never_placed` (its coxa was never seen) is fitted in no frame. `absent_points`,
    the `(T, P)` or `(P,)` declaration of keypoints not on this animal, lowers a
    branch's bar to what it can still determine; a missed detection never does, and a
    chain's base landmark never counts (ADR-0065).
    """
    branches = np.asarray(plan.joint_branch)
    # A joint can contribute an observation only if it maps to a keypoint at all
    # (`joint_row < 0` is a pure kinematic joint, e.g. two of the three head DOFs),
    # it is not its chain's base landmark, and that keypoint is on this animal.
    n_frames = np.asarray(obs_weights).shape[0]
    rows = np.asarray(plan.joint_row)
    tracked = (rows >= 0) & ~plan.joint_is_base
    observed = (np.asarray(obs_weights) > 0) & tracked[None, :]
    fittable_joint = np.broadcast_to(tracked, (n_frames, tracked.size)).copy()
    if absent_points is not None:
        absent = np.asarray(absent_points, dtype=bool)
        if absent.ndim == 1:  # a whole-recording declaration broadcasts over time
            absent = np.broadcast_to(absent.reshape(1, -1), (n_frames, absent.size))
        # (T, N): for each joint that maps to a keypoint, is that keypoint on the animal
        # in this frame?
        present = ~absent[:, np.clip(rows, 0, absent.shape[1] - 1)]
        fittable_joint &= np.where(tracked[None, :], present, False)
    dof_branches = np.asarray(plan.dof_branch)
    out: dict[str, np.ndarray] = {}
    for branch in sorted(set(plan.dof_branch)):
        if not branch:
            continue
        in_branch = branches == branch
        n_fittable = fittable_joint[:, in_branch].sum(axis=1)  # (T,)
        n_dofs = int((dof_branches == branch).sum())
        count = observed[:, in_branch].sum(axis=1)  # (T,)
        # Relax the bar only as far as the frame's own markers determine the branch:
        # 3 * n_fittable >= n_dofs.
        determinable = (n_fittable >= _MIN_VALID_JOINTS) | (3 * n_fittable >= n_dofs)
        threshold = np.minimum(_MIN_VALID_JOINTS, n_fittable)
        # n_fittable == 0 is the genuine "nothing to fit" case: lowering the bar to zero
        # would report neutral-pose angles as if they were measured.
        bad = np.where((n_fittable == 0) | ~determinable, True, count < threshold)
        if branch in never_placed:
            bad = np.ones_like(bad)
        if bad.any():
            out[branch] = bad
    return out

The model pack: the mechanical model the IK stage fits, selected by name.

A directory holding a model.toml manifest, the three assets it names (the leg template, the baked articulation, the overlay mesh) and the model's rest axis and registration anchors. A pack names no skeleton point: that is the binding's (ADR-0041), and a test checks it.

ModelPack dataclass

A loaded model manifest: the three asset paths plus the model-wide facts.

Source code in src/deeperfly/ik/pack.py
@dataclass(frozen=True)
class ModelPack:
    """A loaded model manifest: the three asset paths plus the model-wide facts."""

    name: str
    #: The manifest's own directory -- the three assets are resolved against it.
    root: Path
    template_path: Path
    articulation_path: Path
    mesh_path: Path
    #: The direction a segment extends along in its parent joint's frame.
    rest_axis: tuple[float, float, float]
    #: The model bodies whose observed positions register the recording to the model.
    anchors: tuple[str, ...]

    @classmethod
    def load(cls, ref: str | Path = "neuromechfly") -> ModelPack:
        """Load a pack by packaged name, or by path to its `model.toml` or directory.

        Raises:
            FileNotFoundError: If the pack cannot be found, or its manifest names an
                asset that is not beside it.
        """
        path = MODELS.get(str(ref))
        if path is None:
            path = Path(ref)
            if path.is_dir():
                path = path / "model.toml"
        if not path.is_file():
            raise FileNotFoundError(
                f"[ik] model {ref!r} not found "
                f"(known: {sorted(MODELS)}, or a path to a model.toml)"
            )
        spec = tomllib.loads(path.read_text())
        root = path.parent

        def asset(key: str) -> Path:
            if key not in spec:
                raise FileNotFoundError(
                    f"model pack {path} declares no {key!r}; a pack is a template, an "
                    "articulation and a mesh"
                )
            p = root / str(spec[key])
            if not p.is_file():
                raise FileNotFoundError(
                    f"model pack {path} names {key} = {spec[key]!r}, which is not "
                    f"beside it ({p})"
                )
            return p

        rx, ry, rz = (float(v) for v in spec.get("rest_axis", (0.0, 0.0, -1.0)))
        return cls(
            name=str(spec.get("name", root.name)),
            root=root,
            template_path=asset("template"),
            articulation_path=asset("articulation"),
            mesh_path=asset("mesh"),
            rest_axis=(rx, ry, rz),
            anchors=tuple(str(a) for a in spec.get("anchors", ())),
        )

load classmethod

load(ref: str | Path = 'neuromechfly') -> ModelPack

Load a pack by packaged name, or by path to its model.toml or directory.

Raises:

Type Description
FileNotFoundError

If the pack cannot be found, or its manifest names an asset that is not beside it.

Source code in src/deeperfly/ik/pack.py
@classmethod
def load(cls, ref: str | Path = "neuromechfly") -> ModelPack:
    """Load a pack by packaged name, or by path to its `model.toml` or directory.

    Raises:
        FileNotFoundError: If the pack cannot be found, or its manifest names an
            asset that is not beside it.
    """
    path = MODELS.get(str(ref))
    if path is None:
        path = Path(ref)
        if path.is_dir():
            path = path / "model.toml"
    if not path.is_file():
        raise FileNotFoundError(
            f"[ik] model {ref!r} not found "
            f"(known: {sorted(MODELS)}, or a path to a model.toml)"
        )
    spec = tomllib.loads(path.read_text())
    root = path.parent

    def asset(key: str) -> Path:
        if key not in spec:
            raise FileNotFoundError(
                f"model pack {path} declares no {key!r}; a pack is a template, an "
                "articulation and a mesh"
            )
        p = root / str(spec[key])
        if not p.is_file():
            raise FileNotFoundError(
                f"model pack {path} names {key} = {spec[key]!r}, which is not "
                f"beside it ({p})"
            )
        return p

    rx, ry, rz = (float(v) for v in spec.get("rest_axis", (0.0, 0.0, -1.0)))
    return cls(
        name=str(spec.get("name", root.name)),
        root=root,
        template_path=asset("template"),
        articulation_path=asset("articulation"),
        mesh_path=asset("mesh"),
        rest_axis=(rx, ry, rz),
        anchors=tuple(str(a) for a in spec.get("anchors", ())),
    )

The binding: where each tracked point of one skeleton sits on one model (ADR-0041).

A fact about the (skeleton, model) pair, so it is a file of its own, data/bindings/<skeleton>@<model>.toml, and the only artifact where a skeleton point name and a model body name appear together (a test checks it). One row per point, lf_pretarsus = { body = "lf_tarsus5", offset = [0.0, 0.0, -0.107] }, the offset in the body's frame; base = true nominates a chain's base landmark and approximate = true a placement that is a modeling decision (ADR-0065).

Binding dataclass

A loaded binding: the rows, in the file's order, plus both halves' names.

Source code in src/deeperfly/ik/binding.py
@dataclass(frozen=True)
class Binding:
    """A loaded binding: the rows, in the file's order, plus both halves' names."""

    skeleton: str
    model: str
    rows: tuple[BindingRow, ...]
    #: `body -> rows`, for the reverse lookups; a body may carry several points.
    by_body: dict[str, tuple[BindingRow, ...]] = field(default_factory=dict)

    @classmethod
    def load(cls, ref: str | Path) -> Binding:
        """Load a binding by `"<skeleton>@<model>"` or by path.

        Raises:
            FileNotFoundError: If the pair is not packaged and the path does not exist;
                the message names both halves.
            ValueError: If two rows claim one body at its origin.
        """
        known = bindings()
        path = known.get(str(ref))
        if path is None:
            path = Path(ref)
        if not path.is_file():
            skeleton, _, model = str(ref).partition("@")
            raise FileNotFoundError(
                f"no binding for skeleton {skeleton!r} on model {model or '?'!r}: "
                f"nothing at data/bindings/{ref}.toml and no such path. A binding says "
                "where each tracked point sits on the model, so the pair cannot be "
                f"fitted without one. Packaged: {sorted(known)}"
            )
        spec = tomllib.loads(path.read_text())
        rows = []
        for point, r in spec.get("points", {}).items():
            rows.append(
                BindingRow(
                    point=str(point),
                    body=str(r["body"]),
                    offset=np.asarray(r.get("offset", (0.0, 0.0, 0.0)), dtype=float),
                    approximate=bool(r.get("approximate", False)),
                    base=bool(r.get("base", False)),
                )
            )
        by_body: dict[str, tuple[BindingRow, ...]] = {}
        for row in rows:
            by_body[row.body] = by_body.get(row.body, ()) + (row,)
        for body, group in by_body.items():
            at_origin = [r for r in group if r.at_body_origin]
            if len(at_origin) > 1:
                names = ", ".join(repr(r.point) for r in at_origin)
                raise ValueError(
                    f"binding {path.name} has two points at the origin of body "
                    f"{body!r} ({names}); which one observes it would then be arbitrary"
                )
        return cls(
            skeleton=str(spec.get("skeleton", "")),
            model=str(spec.get("model", "")),
            rows=tuple(rows),
            by_body=by_body,
        )

    def point_for(self, body: str) -> str | None:
        """The single point attached to `body`, or `None` (none, or more than one).

        What a leg joint resolves through; a pretarsus resolves despite its offset,
        which is mesh geometry while the segment length is measured.
        """
        group = self.by_body.get(body, ())
        return group[0].point if len(group) == 1 else None

    def origin_point_for(self, body: str) -> str | None:
        """The point sitting exactly on `body`'s origin, or `None`.

        The stricter lookup, for the registration anchors: a point at an offset on a
        body does not observe its position.
        """
        row = next((r for r in self.by_body.get(body, ()) if r.at_body_origin), None)
        return None if row is None else row.point

    def row(self, point: str) -> BindingRow | None:
        """The row for a tracked point, or `None` when the binding names it nowhere."""
        return next((r for r in self.rows if r.point == point), None)

    @property
    def approximate(self) -> tuple[str, ...]:
        """The points whose placement is a modelling decision, not a measurement."""
        return tuple(r.point for r in self.rows if r.approximate)

approximate property

approximate: tuple[str, ...]

The points whose placement is a modelling decision, not a measurement.

load classmethod

load(ref: str | Path) -> Binding

Load a binding by "<skeleton>@<model>" or by path.

Raises:

Type Description
FileNotFoundError

If the pair is not packaged and the path does not exist; the message names both halves.

ValueError

If two rows claim one body at its origin.

Source code in src/deeperfly/ik/binding.py
@classmethod
def load(cls, ref: str | Path) -> Binding:
    """Load a binding by `"<skeleton>@<model>"` or by path.

    Raises:
        FileNotFoundError: If the pair is not packaged and the path does not exist;
            the message names both halves.
        ValueError: If two rows claim one body at its origin.
    """
    known = bindings()
    path = known.get(str(ref))
    if path is None:
        path = Path(ref)
    if not path.is_file():
        skeleton, _, model = str(ref).partition("@")
        raise FileNotFoundError(
            f"no binding for skeleton {skeleton!r} on model {model or '?'!r}: "
            f"nothing at data/bindings/{ref}.toml and no such path. A binding says "
            "where each tracked point sits on the model, so the pair cannot be "
            f"fitted without one. Packaged: {sorted(known)}"
        )
    spec = tomllib.loads(path.read_text())
    rows = []
    for point, r in spec.get("points", {}).items():
        rows.append(
            BindingRow(
                point=str(point),
                body=str(r["body"]),
                offset=np.asarray(r.get("offset", (0.0, 0.0, 0.0)), dtype=float),
                approximate=bool(r.get("approximate", False)),
                base=bool(r.get("base", False)),
            )
        )
    by_body: dict[str, tuple[BindingRow, ...]] = {}
    for row in rows:
        by_body[row.body] = by_body.get(row.body, ()) + (row,)
    for body, group in by_body.items():
        at_origin = [r for r in group if r.at_body_origin]
        if len(at_origin) > 1:
            names = ", ".join(repr(r.point) for r in at_origin)
            raise ValueError(
                f"binding {path.name} has two points at the origin of body "
                f"{body!r} ({names}); which one observes it would then be arbitrary"
            )
    return cls(
        skeleton=str(spec.get("skeleton", "")),
        model=str(spec.get("model", "")),
        rows=tuple(rows),
        by_body=by_body,
    )

origin_point_for

origin_point_for(body: str) -> str | None

The point sitting exactly on body's origin, or None.

The stricter lookup, for the registration anchors: a point at an offset on a body does not observe its position.

Source code in src/deeperfly/ik/binding.py
def origin_point_for(self, body: str) -> str | None:
    """The point sitting exactly on `body`'s origin, or `None`.

    The stricter lookup, for the registration anchors: a point at an offset on a
    body does not observe its position.
    """
    row = next((r for r in self.by_body.get(body, ()) if r.at_body_origin), None)
    return None if row is None else row.point

point_for

point_for(body: str) -> str | None

The single point attached to body, or None (none, or more than one).

What a leg joint resolves through; a pretarsus resolves despite its offset, which is mesh geometry while the segment length is measured.

Source code in src/deeperfly/ik/binding.py
def point_for(self, body: str) -> str | None:
    """The single point attached to `body`, or `None` (none, or more than one).

    What a leg joint resolves through; a pretarsus resolves despite its offset,
    which is mesh geometry while the segment length is measured.
    """
    group = self.by_body.get(body, ())
    return group[0].point if len(group) == 1 else None

row

row(point: str) -> BindingRow | None

The row for a tracked point, or None when the binding names it nowhere.

Source code in src/deeperfly/ik/binding.py
def row(self, point: str) -> BindingRow | None:
    """The row for a tracked point, or `None` when the binding names it nowhere."""
    return next((r for r in self.rows if r.point == point), None)

BindingRow dataclass

One tracked point's placement on the model.

Source code in src/deeperfly/ik/binding.py
@dataclass(frozen=True)
class BindingRow:
    """One tracked point's placement on the model."""

    point: str
    body: str
    offset: np.ndarray  # (3,) in the body's own frame
    approximate: bool = False
    base: bool = False

    @property
    def at_body_origin(self) -> bool:
        """Whether the point sits exactly on the body's origin (its parent joint)."""
        return not bool(np.any(self.offset))

at_body_origin property

at_body_origin: bool

Whether the point sits exactly on the body's origin (its parent joint).

bindings

bindings() -> dict[str, Path]

The packaged bindings, "<skeleton>@<model>" -> file.

Source code in src/deeperfly/ik/binding.py
def bindings() -> dict[str, Path]:
    """The packaged bindings, `"<skeleton>@<model>" -> file`."""
    if not BINDING_DIR.is_dir():
        return {}
    return {p.stem: p for p in sorted(BINDING_DIR.glob("*.toml"))}

The kinematic template: the NeuroMechFly-style leg model the IK fits.

A KinematicTemplate is what to fit, independent of a recording: the per-leg chains of revolute DOFs with their axes and limits, and the model body each joint sits at. Segment lengths are measured per recording (ik.align). The packaged data/models/neuromechfly/template.toml (from NeLy-EPFL/sequential-inverse-kinematics' body_config) expands one generic leg over the six; a run overrides any bound from [ik.bounds].

Dof dataclass

One revolute degree of freedom: a rotation about axis bounded to [lo, hi].

axis is in the model frame and the limits are in radians. A DOF marked mirror has its axis negated on the right side, as the model's is, so a fitted angle is flygym's own on both sides.

Source code in src/deeperfly/ik/template.py
@dataclass(frozen=True)
class Dof:
    """One revolute degree of freedom: a rotation about `axis` bounded to `[lo, hi]`.

    `axis` is in the model frame and the limits are in radians. A DOF marked `mirror`
    has its axis negated on the right side, as the model's is, so a fitted angle is
    flygym's own on both sides.
    """

    name: str
    axis: tuple[float, float, float]
    lo: float
    hi: float
    #: The model's name for this DOF's angle, the key it is reported, overridden and
    #: spring-referenced under; flygym's `<joint>-<dof>` by default.
    angle: str = ""

Joint dataclass

A joint in a leg chain: the model body it sits at, and its DOFs.

point is the tracked point observing it, resolved through the binding. segment names the segment leading into it ("" for the root thorax-coxa), measured at solve time. joint is flygym's <parent_body>-<child_body>. quat is the constant (w, x, y, z) rotation from the parent's post-DOF frame, identity on NeuroMechFly and not on flybody.

Source code in src/deeperfly/ik/template.py
@dataclass(frozen=True)
class Joint:
    """A joint in a leg chain: the model `body` it sits at, and its DOFs.

    `point` is the tracked point observing it, resolved through the binding. `segment`
    names the segment leading into it (`""` for the root thorax-coxa), measured at
    solve time. `joint` is flygym's `<parent_body>-<child_body>`. `quat` is the
    constant `(w, x, y, z)` rotation from the parent's post-DOF frame, identity on
    NeuroMechFly and not on flybody.
    """

    name: str
    body: str
    point: str
    segment: str
    joint: str
    dofs: tuple[Dof, ...]
    quat: tuple[float, float, float, float] = (1.0, 0.0, 0.0, 0.0)

KinematicTemplate dataclass

A loaded, validated kinematic template (the leg chains).

Source code in src/deeperfly/ik/template.py
@dataclass(frozen=True)
class KinematicTemplate:
    """A loaded, validated kinematic template (the leg chains)."""

    name: str
    legs: tuple[LegChain, ...]
    #: The direction a segment extends along in its parent's frame, from the pack
    #: manifest: `-z` for NeuroMechFly, `+y` for flybody.
    rest_axis: tuple[float, float, float] = (0.0, 0.0, -1.0)

    # -- construction --------------------------------------------------------

    @classmethod
    def load(
        cls,
        ref: str | Path = "neuromechfly",
        *,
        legs: list[str] | None = None,
        bounds_overrides: dict[str, tuple[float, float]] | None = None,
        rest_axis: tuple[float, float, float] | None = None,
        binding: "Binding | str | Path | None" = "fly38@neuromechfly",
    ) -> KinematicTemplate:
        """Load a template by name or path, with this run's restrictions applied.

        `legs` keeps a subset, `bounds_overrides` maps flygym DOF names to degree limits
        (case-insensitive, after the per-side defaults), and `rest_axis` comes from the
        pack manifest (NeuroMechFly's `-z` when `None`). `binding` names the point
        observing each joint; `None` leaves them empty, readable but not fittable.
        """
        path = TEMPLATES.get(str(ref), None)
        path = Path(path) if path is not None else Path(ref)
        if not path.exists():
            raise FileNotFoundError(
                f"[ik] template {ref!r} not found "
                f"(known: {sorted(TEMPLATES)}, or a path to a template TOML)"
            )
        if binding is not None and not isinstance(binding, Binding):
            binding = Binding.load(binding)
        spec = tomllib.loads(path.read_text())
        return cls.from_spec(
            spec,
            legs=legs,
            bounds_overrides=bounds_overrides,
            rest_axis=rest_axis,
            binding=binding,
        )

    @classmethod
    def from_spec(
        cls,
        spec: dict,
        *,
        legs: list[str] | None = None,
        bounds_overrides: dict[str, tuple[float, float]] | None = None,
        rest_axis: tuple[float, float, float] | None = None,
        binding: "Binding | None" = None,
    ) -> KinematicTemplate:
        """Expand a parsed template mapping into per-leg chains."""
        overrides = {k.lower(): v for k, v in (bounds_overrides or {}).items()}
        # A leg entry is its id or a table declaring the side too. The id's first
        # letter is only a fallback: it gets a template named `T1_left` silently wrong.
        declared = {}
        all_legs: list[str] = []
        for entry in spec.get("legs", []):
            if isinstance(entry, dict):
                name = str(entry["name"])
                declared[name] = str(entry["side"])
            else:
                name = str(entry)
            all_legs.append(name)
        chosen = all_legs if legs is None else list(legs)
        unknown = [leg for leg in chosen if leg not in all_legs]
        if unknown:
            raise ValueError(
                f"unknown leg(s) {unknown}; the template defines {all_legs}"
            )
        # A flat [bounds] table applies to both sides; [bounds.l] / [bounds.r]
        # override it per side.
        raw = dict(spec.get("bounds", {}))
        flat = {k: v for k, v in raw.items() if k not in ("l", "r")}
        bounds_by_side = {
            side: {**flat, **dict(raw.get(side, {}))} for side in ("l", "r")
        }
        chains = tuple(
            _build_leg(
                leg,
                declared.get(leg),
                spec["joints"],
                bounds_by_side,
                overrides,
                binding,
            )
            for leg in chosen
        )
        rest = rest_axis if rest_axis is not None else (0.0, 0.0, -1.0)
        rx, ry, rz = (float(v) for v in rest)
        return cls(
            name=spec.get("name", "template"),
            legs=chains,
            rest_axis=(rx, ry, rz),
        )

    # -- views ---------------------------------------------------------------

    @property
    def dof_names(self) -> list[str]:
        """Every leg DOF name, in template order."""
        return [name for leg in self.legs for name in leg.dof_names]

    @property
    def model_point_names(self) -> list[str]:
        """The skeleton points the legs predict (every leg joint, in chain order)."""
        out: list[str] = []
        for leg in self.legs:
            for name in leg.point_names:
                if name not in out:
                    out.append(name)
        return out

dof_names property

dof_names: list[str]

Every leg DOF name, in template order.

model_point_names property

model_point_names: list[str]

The skeleton points the legs predict (every leg joint, in chain order).

from_spec classmethod

from_spec(
    spec: dict,
    *,
    legs: list[str] | None = None,
    bounds_overrides: dict[str, tuple[float, float]]
    | None = None,
    rest_axis: tuple[float, float, float] | None = None,
    binding: Binding | None = None,
) -> KinematicTemplate

Expand a parsed template mapping into per-leg chains.

Source code in src/deeperfly/ik/template.py
@classmethod
def from_spec(
    cls,
    spec: dict,
    *,
    legs: list[str] | None = None,
    bounds_overrides: dict[str, tuple[float, float]] | None = None,
    rest_axis: tuple[float, float, float] | None = None,
    binding: "Binding | None" = None,
) -> KinematicTemplate:
    """Expand a parsed template mapping into per-leg chains."""
    overrides = {k.lower(): v for k, v in (bounds_overrides or {}).items()}
    # A leg entry is its id or a table declaring the side too. The id's first
    # letter is only a fallback: it gets a template named `T1_left` silently wrong.
    declared = {}
    all_legs: list[str] = []
    for entry in spec.get("legs", []):
        if isinstance(entry, dict):
            name = str(entry["name"])
            declared[name] = str(entry["side"])
        else:
            name = str(entry)
        all_legs.append(name)
    chosen = all_legs if legs is None else list(legs)
    unknown = [leg for leg in chosen if leg not in all_legs]
    if unknown:
        raise ValueError(
            f"unknown leg(s) {unknown}; the template defines {all_legs}"
        )
    # A flat [bounds] table applies to both sides; [bounds.l] / [bounds.r]
    # override it per side.
    raw = dict(spec.get("bounds", {}))
    flat = {k: v for k, v in raw.items() if k not in ("l", "r")}
    bounds_by_side = {
        side: {**flat, **dict(raw.get(side, {}))} for side in ("l", "r")
    }
    chains = tuple(
        _build_leg(
            leg,
            declared.get(leg),
            spec["joints"],
            bounds_by_side,
            overrides,
            binding,
        )
        for leg in chosen
    )
    rest = rest_axis if rest_axis is not None else (0.0, 0.0, -1.0)
    rx, ry, rz = (float(v) for v in rest)
    return cls(
        name=spec.get("name", "template"),
        legs=chains,
        rest_axis=(rx, ry, rz),
    )

load classmethod

load(
    ref: str | Path = "neuromechfly",
    *,
    legs: list[str] | None = None,
    bounds_overrides: dict[str, tuple[float, float]]
    | None = None,
    rest_axis: tuple[float, float, float] | None = None,
    binding: Binding
    | str
    | Path
    | None = "fly38@neuromechfly",
) -> KinematicTemplate

Load a template by name or path, with this run's restrictions applied.

legs keeps a subset, bounds_overrides maps flygym DOF names to degree limits (case-insensitive, after the per-side defaults), and rest_axis comes from the pack manifest (NeuroMechFly's -z when None). binding names the point observing each joint; None leaves them empty, readable but not fittable.

Source code in src/deeperfly/ik/template.py
@classmethod
def load(
    cls,
    ref: str | Path = "neuromechfly",
    *,
    legs: list[str] | None = None,
    bounds_overrides: dict[str, tuple[float, float]] | None = None,
    rest_axis: tuple[float, float, float] | None = None,
    binding: "Binding | str | Path | None" = "fly38@neuromechfly",
) -> KinematicTemplate:
    """Load a template by name or path, with this run's restrictions applied.

    `legs` keeps a subset, `bounds_overrides` maps flygym DOF names to degree limits
    (case-insensitive, after the per-side defaults), and `rest_axis` comes from the
    pack manifest (NeuroMechFly's `-z` when `None`). `binding` names the point
    observing each joint; `None` leaves them empty, readable but not fittable.
    """
    path = TEMPLATES.get(str(ref), None)
    path = Path(path) if path is not None else Path(ref)
    if not path.exists():
        raise FileNotFoundError(
            f"[ik] template {ref!r} not found "
            f"(known: {sorted(TEMPLATES)}, or a path to a template TOML)"
        )
    if binding is not None and not isinstance(binding, Binding):
        binding = Binding.load(binding)
    spec = tomllib.loads(path.read_text())
    return cls.from_spec(
        spec,
        legs=legs,
        bounds_overrides=bounds_overrides,
        rest_axis=rest_axis,
        binding=binding,
    )

LegChain dataclass

One leg's serial chain (ThC -> CTr -> FTi -> TiTa -> Pretarsus).

Source code in src/deeperfly/ik/template.py
@dataclass(frozen=True)
class LegChain:
    """One leg's serial chain (ThC -> CTr -> FTi -> TiTa -> Pretarsus)."""

    name: str  # "rf", "lm", ...
    side: str  # "r" or "l"
    joints: tuple[Joint, ...]

    @property
    def point_names(self) -> tuple[str, ...]:
        """The tracked point marking each joint, in chain order."""
        return tuple(j.point for j in self.joints)

    @property
    def dof_names(self) -> list[str]:
        """Every DOF's declared angle name, in chain order."""
        return [d.angle for j in self.joints for d in j.dofs]

    @property
    def axes(self) -> np.ndarray:
        """`(D, 3)` unit rotation axes, one per DOF in chain order."""
        out = [d.axis for j in self.joints for d in j.dofs]
        return np.asarray(out, dtype=float) if out else np.zeros((0, 3))

    @property
    def bounds(self) -> tuple[np.ndarray, np.ndarray]:
        """`(lo, hi)` radian bound arrays of shape `(D,)`."""
        lo = [d.lo for j in self.joints for d in j.dofs]
        hi = [d.hi for j in self.joints for d in j.dofs]
        return np.asarray(lo, dtype=float), np.asarray(hi, dtype=float)

axes property

axes: ndarray

(D, 3) unit rotation axes, one per DOF in chain order.

bounds property

bounds: tuple[ndarray, ndarray]

(lo, hi) radian bound arrays of shape (D,).

dof_names property

dof_names: list[str]

Every DOF's declared angle name, in chain order.

point_names property

point_names: tuple[str, ...]

The tracked point marking each joint, in chain order.

The baked head and abdomen chains the IK fits beyond the legs.

Their joints are not keypoints (a neck pivot, the abdominal hinges), so their geometry comes from the model, baked once from the NeuroMechFly MJCF into the pack's articulation.json. The head is a 3-DOF chain carrying the antenna bases, the abdomen a 5-DOF pitch chain carrying the abdomen markers. This module loads that geometry and measures a chain's size (ADR-0066); the forward kinematics are ik.forward's.

Articulation dataclass

The loaded head/abdomen chains plus the anchor bodies that register the body.

anchors names model bodies (from the pack manifest) and anchor_neutral holds their neutral positions, read out of bodies, which maps each body a marker may attach to onto its neutral frame and chain depth. leg_rest is every leg DOF's spring reference in radians, the plan's neutral (ADR-0065); empty for an asset without one, which falls back to zero.

Source code in src/deeperfly/ik/articulation.py
@dataclass(frozen=True)
class Articulation:
    """The loaded head/abdomen chains plus the anchor bodies that register the body.

    `anchors` names model bodies (from the pack manifest) and `anchor_neutral` holds
    their neutral positions, read out of `bodies`, which maps each body a marker may
    attach to onto its neutral frame and chain depth. `leg_rest` is every leg DOF's
    spring reference in radians, the plan's `neutral` (ADR-0065); empty for an asset
    without one, which falls back to zero.
    """

    chains: tuple[Chain, ...]
    anchors: tuple[str, ...]  # model body names
    anchor_neutral: np.ndarray  # (N, 3) neutral anchor positions, in anchor order
    _anchor_points: tuple[str, ...] = ()  # the binding's point per anchor, same order
    bodies: dict[str, dict] = field(default_factory=dict)
    leg_rest: dict[str, float] = field(default_factory=dict)

    @classmethod
    def load(
        cls,
        ref: str | Path = DEFAULT_ARTICULATION_PATH,
        *,
        binding: "Binding | str | Path | None" = "fly38@neuromechfly",
        anchors: tuple[str, ...] = _DEFAULT_ANCHORS,
        fit: tuple[str, ...] | None = None,
        bounds_overrides: dict[str, tuple[float, float]] | None = None,
        marker_overrides: dict[str, dict[str, dict]] | None = None,
    ) -> Articulation:
        """Load the baked articulation, with this run's restrictions applied.

        `binding`'s rows on the chain bodies are the chains' markers and its rows on
        the anchor bodies say which point observes each; `None` loads the structure
        with no markers, readable but not fittable. `fit` keeps a subset of the
        chains, `bounds_overrides` maps flygym angle names to degree limits
        (case-insensitive), and `marker_overrides` replaces a chain's binding rows
        (`[ik.markers]`).
        """
        if binding is not None and not isinstance(binding, Binding):
            binding = Binding.load(binding)
        spec = json.loads(Path(ref).read_text())
        overrides = {k.lower(): v for k, v in (bounds_overrides or {}).items()}
        bodies = spec.get("bodies", {})
        missing = [a for a in anchors if a not in bodies]
        if missing:
            raise ValueError(
                f"the model pack names registration anchor(s) {missing}, which its "
                f"articulation asset {Path(ref).name} does not bake a body frame for "
                f"(it has {sorted(bodies)})"
            )
        markers = marker_overrides or {}
        chains = []
        for c in spec["chains"]:
            if fit is not None and c["name"] not in fit:
                continue
            table = markers.get(c["name"])
            if table is None:
                table = (
                    {}
                    if binding is None
                    else _chain_binding_markers(c["name"], binding, bodies)
                )
            resolved, base_point = _resolve_markers(c, table, bodies)
            chains.append(_build_chain(c, overrides, resolved, base_point))
        return cls(
            chains=tuple(chains),
            anchors=tuple(anchors),
            anchor_neutral=np.asarray(
                [bodies[a]["pos"] for a in anchors], dtype=float
            ).reshape(len(anchors), 3),
            _anchor_points=tuple(
                "" if binding is None else (binding.origin_point_for(a) or "")
                for a in anchors
            ),
            bodies=bodies,
            leg_rest={str(k): float(v) for k, v in spec.get("leg_rest", {}).items()},
        )

    @property
    def anchor_points(self) -> tuple[str, ...]:
        """The tracked point observing each anchor body, from the binding.

        Empty where the binding puts no point on an anchor's origin; the registration
        needs only three finite anchors.
        """
        return self._anchor_points

    def chain(self, name: str) -> Chain | None:
        """The chain of that name, or `None` if this articulation has none."""
        return next((c for c in self.chains if c.name == name), None)

    @property
    def dof_names(self) -> list[str]:
        """Every chain's DOF names, concatenated in chain order."""
        return [n for c in self.chains for n in c.dof_names]

anchor_points property

anchor_points: tuple[str, ...]

The tracked point observing each anchor body, from the binding.

Empty where the binding puts no point on an anchor's origin; the registration needs only three finite anchors.

dof_names property

dof_names: list[str]

Every chain's DOF names, concatenated in chain order.

chain

chain(name: str) -> Chain | None

The chain of that name, or None if this articulation has none.

Source code in src/deeperfly/ik/articulation.py
def chain(self, name: str) -> Chain | None:
    """The chain of that name, or `None` if this articulation has none."""
    return next((c for c in self.chains if c.name == name), None)

load classmethod

load(
    ref: str | Path = DEFAULT_ARTICULATION_PATH,
    *,
    binding: Binding
    | str
    | Path
    | None = "fly38@neuromechfly",
    anchors: tuple[str, ...] = _DEFAULT_ANCHORS,
    fit: tuple[str, ...] | None = None,
    bounds_overrides: dict[str, tuple[float, float]]
    | None = None,
    marker_overrides: dict[str, dict[str, dict]]
    | None = None,
) -> Articulation

Load the baked articulation, with this run's restrictions applied.

binding's rows on the chain bodies are the chains' markers and its rows on the anchor bodies say which point observes each; None loads the structure with no markers, readable but not fittable. fit keeps a subset of the chains, bounds_overrides maps flygym angle names to degree limits (case-insensitive), and marker_overrides replaces a chain's binding rows ([ik.markers]).

Source code in src/deeperfly/ik/articulation.py
@classmethod
def load(
    cls,
    ref: str | Path = DEFAULT_ARTICULATION_PATH,
    *,
    binding: "Binding | str | Path | None" = "fly38@neuromechfly",
    anchors: tuple[str, ...] = _DEFAULT_ANCHORS,
    fit: tuple[str, ...] | None = None,
    bounds_overrides: dict[str, tuple[float, float]] | None = None,
    marker_overrides: dict[str, dict[str, dict]] | None = None,
) -> Articulation:
    """Load the baked articulation, with this run's restrictions applied.

    `binding`'s rows on the chain bodies are the chains' markers and its rows on
    the anchor bodies say which point observes each; `None` loads the structure
    with no markers, readable but not fittable. `fit` keeps a subset of the
    chains, `bounds_overrides` maps flygym angle names to degree limits
    (case-insensitive), and `marker_overrides` replaces a chain's binding rows
    (`[ik.markers]`).
    """
    if binding is not None and not isinstance(binding, Binding):
        binding = Binding.load(binding)
    spec = json.loads(Path(ref).read_text())
    overrides = {k.lower(): v for k, v in (bounds_overrides or {}).items()}
    bodies = spec.get("bodies", {})
    missing = [a for a in anchors if a not in bodies]
    if missing:
        raise ValueError(
            f"the model pack names registration anchor(s) {missing}, which its "
            f"articulation asset {Path(ref).name} does not bake a body frame for "
            f"(it has {sorted(bodies)})"
        )
    markers = marker_overrides or {}
    chains = []
    for c in spec["chains"]:
        if fit is not None and c["name"] not in fit:
            continue
        table = markers.get(c["name"])
        if table is None:
            table = (
                {}
                if binding is None
                else _chain_binding_markers(c["name"], binding, bodies)
            )
        resolved, base_point = _resolve_markers(c, table, bodies)
        chains.append(_build_chain(c, overrides, resolved, base_point))
    return cls(
        chains=tuple(chains),
        anchors=tuple(anchors),
        anchor_neutral=np.asarray(
            [bodies[a]["pos"] for a in anchors], dtype=float
        ).reshape(len(anchors), 3),
        _anchor_points=tuple(
            "" if binding is None else (binding.origin_point_for(a) or "")
            for a in anchors
        ),
        bodies=bodies,
        leg_rest={str(k): float(v) for k, v in spec.get("leg_rest", {}).items()},
    )

Chain dataclass

One baked articulation chain (the head or the abdomen).

Attributes:

Name Type Description
name str

"head" or "abdomen".

dof_names tuple[str, ...]

(D,) the fitted angle names (flygym's), in chain order.

anchors, axes

(D, 3) neutral world anchor and unit axis of each joint.

bounds tuple[ndarray, ndarray]

(lo, hi) radian bound arrays of shape (D,).

marker_names tuple[str, ...]

(M,) the skeleton points this chain predicts.

marker_neutral ndarray

(M, 3) each marker's neutral model position.

marker_depth tuple[int, ...]

(M,) each marker's chain depth (how many proximal joints move it).

marker_approximate tuple[bool, ...]

(M,) whether a marker's placement is a modeling decision (the binding's approximate); reported, never acted on.

base_point str | None

The marker that measures where this chain's base sits, or None (ADR-0066). It lies on the rotation center, so it constrains no DOF.

Source code in src/deeperfly/ik/articulation.py
@dataclass(frozen=True)
class Chain:
    """One baked articulation chain (the head or the abdomen).

    Attributes:
        name: `"head"` or `"abdomen"`.
        dof_names: `(D,)` the fitted angle names (flygym's), in chain order.
        anchors, axes: `(D, 3)` neutral world anchor and unit axis of each joint.
        bounds: `(lo, hi)` radian bound arrays of shape `(D,)`.
        marker_names: `(M,)` the skeleton points this chain predicts.
        marker_neutral: `(M, 3)` each marker's neutral model position.
        marker_depth: `(M,)` each marker's chain depth (how many proximal joints move
            it).
        marker_approximate: `(M,)` whether a marker's placement is a modeling decision
            (the binding's `approximate`); reported, never acted on.
        base_point: The marker that measures where this chain's base sits, or `None`
            (ADR-0066). It lies on the rotation center, so it constrains no DOF.
    """

    name: str
    dof_names: tuple[str, ...]
    anchors: np.ndarray
    axes: np.ndarray
    bounds: tuple[np.ndarray, np.ndarray]
    marker_names: tuple[str, ...]
    marker_neutral: np.ndarray
    marker_depth: tuple[int, ...]
    marker_approximate: tuple[bool, ...] = ()
    base_point: str | None = None

    def marker_index(self, point: str) -> int | None:
        """The column of `point` in this chain's marker arrays, or `None`."""
        return self.marker_names.index(point) if point in self.marker_names else None

marker_index

marker_index(point: str) -> int | None

The column of point in this chain's marker arrays, or None.

Source code in src/deeperfly/ik/articulation.py
def marker_index(self, point: str) -> int | None:
    """The column of `point` in this chain's marker arrays, or `None`."""
    return self.marker_names.index(point) if point in self.marker_names else None

body_similarity

body_similarity(
    neutral_anchors: ndarray, measured_anchors: ndarray
) -> tuple[np.ndarray, float, np.ndarray] | None

Similarity transform (R, s, t) from the neutral anchors onto the measured.

measured_anchors is (N, 3) (NaN where an anchor was not observed); at least three finite anchors are needed. Returns None when too few are available.

Source code in src/deeperfly/ik/articulation.py
def body_similarity(
    neutral_anchors: np.ndarray, measured_anchors: np.ndarray
) -> tuple[np.ndarray, float, np.ndarray] | None:
    """Similarity transform `(R, s, t)` from the neutral anchors onto the measured.

    `measured_anchors` is `(N, 3)` (NaN where an anchor was not observed); at least
    three finite anchors are needed. Returns `None` when too few are available.
    """
    good = np.isfinite(measured_anchors).all(axis=1)
    if int(good.sum()) < 3:
        return None
    return umeyama(neutral_anchors[good], measured_anchors[good])

calibrate_chain

calibrate_chain(
    local_markers: ndarray,
    chain: Chain,
    *,
    seed_scale: float,
    base_shift: ndarray | None = None,
    fit_root: bool = False,
    n_frames: int = _CALIBRATION_FRAMES,
    clamp: tuple[float, float] = (0.3, 3.0),
) -> tuple[float, np.ndarray]

A chain's size (and, optionally, its root) from its whole marker set.

Fits the chain's angles and size together against every marker, posture being the nuisance parameter, seeded from seed_scale (the ruler) and reduced by the median over n_frames sampled frames. fit_root also frees the root on top of base_shift, for a chain with no base landmark; its lateral component is not fitted on a midline chain. The measurements and the null direction: ADR-0066.

Returns:

Name Type Description
scale float

The median fitted size, clamped.

shift ndarray

(3,) the chain's root shift (base_shift when fit_root is false).

Source code in src/deeperfly/ik/articulation.py
def calibrate_chain(
    local_markers: np.ndarray,
    chain: Chain,
    *,
    seed_scale: float,
    base_shift: np.ndarray | None = None,
    fit_root: bool = False,
    n_frames: int = _CALIBRATION_FRAMES,
    clamp: tuple[float, float] = (0.3, 3.0),
) -> tuple[float, np.ndarray]:
    """A chain's size (and, optionally, its root) from its whole marker set.

    Fits the chain's angles and size together against every marker, posture being the
    nuisance parameter, seeded from `seed_scale` (the ruler) and reduced by the median
    over `n_frames` sampled frames. `fit_root` also frees the root on top of
    `base_shift`, for a chain with no base landmark; its lateral component is not
    fitted on a midline chain. The measurements and the null direction: ADR-0066.

    Returns:
        scale: The median fitted size, clamped.
        shift: `(3,)` the chain's root shift (`base_shift` when `fit_root` is false).
    """
    from scipy.optimize import least_squares

    local = np.asarray(local_markers, dtype=float)
    base = np.zeros(3) if base_shift is None else np.asarray(base_shift, dtype=float)
    lo, hi = chain.bounds
    n_dof = len(lo)
    if local.ndim != 3 or local.shape[0] == 0 or n_dof == 0:
        return float(np.clip(seed_scale, *clamp)), base
    usable = np.flatnonzero(np.isfinite(local).all(axis=(1, 2)))
    if usable.size == 0:
        log.warning(
            "ik: the %s chain was never fully observed, so its size "
            "cannot be fitted; keeping the ruler estimate %.4f",
            chain.name,
            seed_scale,
        )
        return float(np.clip(seed_scale, *clamp)), base
    take = usable[np.unique(np.linspace(0, usable.size - 1, n_frames).astype(int))]

    anchors = np.asarray(chain.anchors, dtype=float)
    span = float(np.linalg.norm(anchors[-1] - anchors[0])) or 1.0
    reach = _ROOT_SHIFT_FRACTION * max(span, float(np.abs(chain.marker_neutral).max()))
    # A midline chain's root is on the plane of symmetry, and the registration error
    # that moves it (a pitch about the coxa centroid) is too: a free lateral component
    # would only stand in for the chain's own lateral joints.
    free_axes = (0, 2) if (fit_root and _is_midline(chain)) else (0, 1, 2)
    n_extra = len(free_axes) if fit_root else 0
    mid = 0.5 * (lo + hi)
    x0 = np.concatenate([mid, [seed_scale], np.zeros(n_extra)])
    blo = np.concatenate([lo, [clamp[0]], np.full(n_extra, -reach)])
    bhi = np.concatenate([hi, [clamp[1]], np.full(n_extra, reach)])

    def _shift(p: np.ndarray) -> np.ndarray:
        """The root shift a parameter vector encodes: the base plus its free axes."""
        if not fit_root:
            return base
        out = np.array(base, dtype=float)
        out[list(free_axes)] += p[n_dof + 1 :]
        return out

    scales: list[float] = []
    shifts: list[np.ndarray] = []
    for t in take:
        target = local[t]

        def residual(p, target=target):
            return (
                chain_markers(chain, p[:n_dof], p[n_dof], _shift(p)) - target
            ).ravel()

        fit = least_squares(residual, x0, bounds=(blo, bhi), xtol=1e-10, ftol=1e-10)
        scales.append(float(fit.x[n_dof]))
        shifts.append(_shift(fit.x))
    scale = float(np.clip(np.median(scales), *clamp))
    shift = np.median(np.stack(shifts), axis=0) if fit_root else base
    return scale, shift

chain_affine

chain_affine(
    chain, depth: int, theta: Float[ndarray, D]
) -> tuple[Float[np.ndarray, "3 3"], Float[np.ndarray, 3]]

The model-frame affine (A, b) carrying a depth-d point: A p + b.

The baked chains rotate about their neutral world anchors: A_d = A_{d-1} R_d and b_d = A_{d-1}(c_d - R_d c_d) + b_{d-1}, so at zero angles every point stays at its neutral position.

Source code in src/deeperfly/ik/forward.py
def chain_affine(
    chain, depth: int, theta: Float[np.ndarray, "D"]
) -> tuple[Float[np.ndarray, "3 3"], Float[np.ndarray, "3"]]:
    """The model-frame affine `(A, b)` carrying a depth-`d` point: `A p + b`.

    The baked chains rotate about their neutral world anchors: `A_d = A_{d-1} R_d` and
    `b_d = A_{d-1}(c_d - R_d c_d) + b_{d-1}`, so at zero angles every point stays at its
    neutral position.
    """
    a_cum = np.eye(3)
    b_cum = np.zeros(3)
    for i in range(int(depth)):
        r = axis_rmat(chain.axes[i], float(theta[i]))
        c = np.asarray(chain.anchors[i], dtype=float)
        b_cum = a_cum @ (c - r @ c) + b_cum
        a_cum = a_cum @ r
    return a_cum, b_cum

chain_markers

chain_markers(
    chain: Chain,
    angles: ndarray,
    scale: float,
    shift: ndarray | None = None,
) -> np.ndarray

(M, 3) model-frame marker positions of a chain at angles, grown and shifted.

The one place the size and root conventions meet chain_fk: composing the rotations by hand in the other order is invisible on the abdomen, whose axes commute, and off by ~0.9 model units on the head.

Source code in src/deeperfly/ik/articulation.py
def chain_markers(
    chain: Chain, angles: np.ndarray, scale: float, shift: np.ndarray | None = None
) -> np.ndarray:
    """`(M, 3)` model-frame marker positions of a chain at `angles`, grown and shifted.

    The one place the size and root conventions meet
    [chain_fk][deeperfly.ik.forward.chain_fk]: composing the rotations by hand in the
    other order is invisible on the abdomen, whose axes commute, and off by ~0.9 model
    units on the head.
    """
    from .forward import chain_fk

    anchors, markers = _scaled_chain(chain, scale)
    posed = chain_fk(
        anchors,
        np.asarray(chain.axes, dtype=float),
        tuple(int(d) for d in chain.marker_depth),
        np.asarray(angles, dtype=float),
        markers,
    )
    return posed if shift is None else posed + np.asarray(shift, dtype=float)

estimate_chain_scale

estimate_chain_scale(
    local_markers: ndarray,
    chain: Chain,
    *,
    clamp: tuple[float, float] = (0.3, 3.0),
    warn_if_unmeasurable: bool = True,
) -> float

A chain's size relative to the model, from lengths its joints cannot change.

The median over frames of the least-squares scale sum(D d) / sum(D^2) over the chain's rigid rulers, with mirror pairs folded to their midpoint first; the rulers, the fold and why the abdomen has none are ADR-0066. It seeds calibrate_chain, which a fit uses. local_markers is the chain's markers in the model frame (R^T (world - t) / s), NaN where unobserved. Returns 1.0 when no ruler was observed, with a warning unless warn_if_unmeasurable is false, as wherever a fitted size follows.

Source code in src/deeperfly/ik/articulation.py
def estimate_chain_scale(
    local_markers: np.ndarray,
    chain: Chain,
    *,
    clamp: tuple[float, float] = (0.3, 3.0),
    warn_if_unmeasurable: bool = True,
) -> float:
    """A chain's size relative to the model, from lengths its joints cannot change.

    The median over frames of the least-squares scale `sum(D d) / sum(D^2)` over the
    chain's rigid rulers, with mirror pairs folded to their midpoint first; the
    rulers, the fold and why the abdomen has none are ADR-0066. It seeds
    [calibrate_chain][], which a fit uses. `local_markers` is the chain's markers in
    the model frame (`R^T (world - t) / s`), NaN where unobserved. Returns `1.0` when
    no ruler was observed, with a warning unless `warn_if_unmeasurable` is false, as
    wherever a fitted size follows.
    """
    local = np.asarray(local_markers, dtype=float)
    groups = _midline_groups(chain)
    pairs = _rigid_ruler(chain, groups)
    if not pairs:
        log.log(
            logging.WARNING if warn_if_unmeasurable else logging.DEBUG,
            "ik: the %s chain has no articulation-invariant marker "
            "separation (markers %s at depths %s), so this ruler cannot measure its "
            "size; seeding from model size",
            chain.name,
            list(chain.marker_names),
            list(chain.marker_depth),
        )
        return 1.0
    # Mean, not nanmean: a pair missing one side is the biased point the fold removes,
    # so the ruler drops out of that frame.
    folded = np.stack(
        [local[..., list(g), :].mean(axis=-2) for g in groups], axis=-2
    )  # (T, G, 3)
    model_len = np.asarray([d for _, _, d in pairs], dtype=float)
    measured = np.stack(
        [
            np.linalg.norm(folded[..., i, :] - folded[..., j, :], axis=-1)
            for i, j, _ in pairs
        ],
        axis=-1,
    )  # (T, P)
    weight = np.where(np.isfinite(measured), model_len, 0.0)
    num = np.nansum(weight * measured, axis=-1)
    den = (weight * model_len).sum(axis=-1)
    ratio = np.divide(num, den, out=np.full(np.shape(num), np.nan), where=den > _EPS)
    ratio = np.atleast_1d(ratio)
    ratio = ratio[np.isfinite(ratio)]
    if ratio.size == 0:
        log.log(
            logging.WARNING if warn_if_unmeasurable else logging.DEBUG,
            "ik: the %s chain's size ruler (%s) was never observed, so "
            "this ruler cannot measure its size; seeding from model size",
            chain.name,
            ", ".join(
                f"{chain.marker_names[groups[i][0]]}-{chain.marker_names[groups[j][0]]}"
                for i, j, _ in pairs
            ),
        )
        return 1.0
    return float(np.clip(np.median(ratio), clamp[0], clamp[1]))

load_articulation cached

load_articulation(
    ref: str | Path = DEFAULT_ARTICULATION_PATH,
    fit: tuple[str, ...] | None = None,
    binding: str | Path | None = "fly38@neuromechfly",
) -> Articulation

Load (and cache) an articulation with no bounds overrides.

The default is the packaged pair, which is what the overlay and the leg spring references read; a run resolves its own from [ik] binding.

Source code in src/deeperfly/ik/articulation.py
@functools.lru_cache(maxsize=4)
def load_articulation(
    ref: str | Path = DEFAULT_ARTICULATION_PATH,
    fit: tuple[str, ...] | None = None,
    binding: str | Path | None = "fly38@neuromechfly",
) -> Articulation:
    """Load (and cache) an articulation with no bounds overrides.

    The default is the packaged pair, which is what the overlay and the leg spring
    references read; a run resolves its own from `[ik] binding`.
    """
    return Articulation.load(ref, fit=fit, binding=binding)

umeyama

umeyama(
    src: ndarray, dst: ndarray
) -> tuple[np.ndarray, float, np.ndarray]

Least-squares similarity transform (rotation, scale, translation) src -> dst.

Umeyama (1991). Shared with ik.mesh, which fits the same transform to register a mesh's anchor vertices.

Source code in src/deeperfly/ik/articulation.py
def umeyama(src: np.ndarray, dst: np.ndarray) -> tuple[np.ndarray, float, np.ndarray]:
    """Least-squares similarity transform (rotation, scale, translation) `src -> dst`.

    Umeyama (1991). Shared with [ik.mesh][deeperfly.ik.mesh], which fits the same
    transform to register a mesh's anchor vertices.
    """
    mu_s, mu_d = src.mean(axis=0), dst.mean(axis=0)
    s0, d0 = src - mu_s, dst - mu_d
    cov = (d0.T @ s0) / len(src)
    u, sigma, vt = np.linalg.svd(cov)
    correction = np.eye(3)
    if np.linalg.det(u) * np.linalg.det(vt) < 0:
        correction[2, 2] = -1.0
    rot = u @ correction @ vt
    var = (s0**2).sum() / len(src)
    scale = float(np.trace(np.diag(sigma) @ correction) / var) if var > _EPS else 1.0
    trans = mu_d - scale * (rot @ mu_s)
    return rot, scale, trans

Measure the per-recording leg geometry the kinematic template is built around.

body_alignment takes what does not change over a recording: each leg's coxa origin (the median thorax-coxa) and its measured segment lengths, optionally shared with its mirror image (ADR-0066). It measures in the model frame, where the body holds still: solve registers a free body there frame by frame (ADR-0098).

Alignment dataclass

The per-leg geometry registering a recording to the template.

Source code in src/deeperfly/ik/align.py
@dataclass(frozen=True)
class Alignment:
    """The per-leg geometry registering a recording to the template."""

    leg_origin: dict[str, np.ndarray]  # leg name -> (3,) coxa model-frame position
    seglens: dict[str, np.ndarray]  # leg name -> (J,) model-unit length into each joint

body_alignment

body_alignment(
    pts3d: ndarray,
    skeleton: Skeleton,
    template: KinematicTemplate,
    *,
    symmetric_segments: bool = False,
) -> Alignment

Estimate the per-leg origins and segment lengths from the pose (ADR-0066).

pts3d is the pose in the model frame, as solve registers it; a pose the body moves in has no single coxa origin to measure.

Source code in src/deeperfly/ik/align.py
def body_alignment(
    pts3d: np.ndarray,
    skeleton: Skeleton,
    template: KinematicTemplate,
    *,
    symmetric_segments: bool = False,
) -> Alignment:
    """Estimate the per-leg origins and segment lengths from the pose (ADR-0066).

    `pts3d` is the pose in the model frame, as [solve][deeperfly.ik.solve] registers
    it; a pose the body moves in has no single coxa origin to measure.
    """
    index = {name: i for i, name in enumerate(skeleton.point_names)}

    def coxa(leg) -> np.ndarray | None:
        name = leg.joints[0].point
        if name not in index:
            return None
        return _nanmedian_point(pts3d[:, index[name]])

    origins = {leg.name: coxa(leg) for leg in template.legs}

    leg_origin: dict[str, np.ndarray] = {}
    seglens: dict[str, np.ndarray] = {}
    for leg in template.legs:
        origin = origins[leg.name]
        if origin is None or not np.all(np.isfinite(origin)):
            continue
        leg_origin[leg.name] = origin
        seglens[leg.name] = _measured_seglens(pts3d, index, leg)

    if symmetric_segments:
        seglens = symmetrize_seglens(seglens, template, skeleton)

    return Alignment(leg_origin=leg_origin, seglens=seglens)

mirror_leg_pairs

mirror_leg_pairs(
    template: KinematicTemplate, skeleton: Skeleton
) -> tuple[tuple[str, str], ...]

The template's legs paired with their mirror images, per the skeleton.

Two legs pair when every joint of one mirrors the joint at the same depth of the other by the skeleton's symmetries, not by the l/r prefix: a skeleton that declares none says its subject is not bilaterally symmetric. Each pair comes once, in template order, with the partner second.

Source code in src/deeperfly/ik/align.py
def mirror_leg_pairs(
    template: KinematicTemplate, skeleton: Skeleton
) -> tuple[tuple[str, str], ...]:
    """The template's legs paired with their mirror images, per the skeleton.

    Two legs pair when every joint of one mirrors the joint at the same depth of the
    other by the skeleton's `symmetries`, not by the `l`/`r` prefix: a skeleton that
    declares none says its subject is not bilaterally symmetric. Each pair comes once,
    in template order, with the partner second.
    """
    by_name = {leg.name: leg for leg in template.legs}
    out: list[tuple[str, str]] = []
    seen: set[str] = set()
    for leg in template.legs:
        if leg.name in seen:
            continue
        partners = {
            skeleton.partner(point) if point in skeleton.point_names else None
            for point in leg.point_names
        }
        if None in partners:  # some joint of this leg has no mirror point
            continue
        names = {skeleton.point_names[i] for i in partners if i is not None}
        match = next(
            (
                other.name
                for other in template.legs
                if other.name != leg.name and set(other.point_names) == names
            ),
            None,
        )
        if match is None:
            continue
        # Same depth as well as the same set: a chain paired point-for-point in the
        # wrong ORDER would share lengths between different bones.
        if any(
            skeleton.partner(a) != skeleton.point_names.index(b)
            for a, b in zip(leg.point_names, by_name[match].point_names, strict=True)
        ):
            continue
        out.append((leg.name, match))
        seen |= {leg.name, match}
    return tuple(out)

symmetrize_seglens

symmetrize_seglens(
    seglens: dict[str, ndarray],
    template: KinematicTemplate,
    skeleton: Skeleton,
) -> dict[str, np.ndarray]

Give each leg and its mirror image one shared length per segment (ADR-0066).

The shared length is the mean of the two sides' medians. A segment measured on one side only takes that side's length; one measured on neither stays 0.0 for the model fallback. Returns a new mapping.

Source code in src/deeperfly/ik/align.py
def symmetrize_seglens(
    seglens: dict[str, np.ndarray],
    template: KinematicTemplate,
    skeleton: Skeleton,
) -> dict[str, np.ndarray]:
    """Give each leg and its mirror image one shared length per segment (ADR-0066).

    The shared length is the mean of the two sides' medians. A segment measured on one
    side only takes that side's length; one measured on neither stays `0.0` for the
    model fallback. Returns a new mapping.
    """
    pairs = mirror_leg_pairs(template, skeleton)
    out = {name: np.array(v, dtype=float) for name, v in seglens.items()}
    if not pairs:
        log.warning(
            "ik: symmetric_segments is on, but no leg of template %r "
            "pairs with a mirror image under skeleton %r's symmetries, so the segment "
            "lengths are unchanged",
            template.name,
            skeleton.name,
        )
        return out

    report: list[str] = []
    for left, right in pairs:
        la, lb = out.get(left), out.get(right)
        if la is None or lb is None:  # a leg with no observed coxa has no lengths
            continue
        gaps: list[float] = []
        for j in range(1, min(la.size, lb.size)):
            a, b = float(la[j]), float(lb[j])
            ok_a = np.isfinite(a) and a > _EPS
            ok_b = np.isfinite(b) and b > _EPS
            if ok_a and ok_b:
                shared = 0.5 * (a + b)
                gaps.append(abs(a - b) / shared)
            elif ok_a or ok_b:
                shared = a if ok_a else b
            else:
                continue  # never measured on either side: leave the model fallback
            la[j] = lb[j] = shared
        if gaps:
            report.append(f"{left}|{right} {max(gaps):.1%} worst")
    # The gap the sides had is the only check on the premise, and says how much of the
    # residual was two triplets disagreeing about one bone.
    log.info(
        "ik: segment lengths shared across %d mirror leg pair(s); "
        "the sides had differed by %s",
        len(pairs),
        ", ".join(report) if report else "(nothing measured on both sides)",
    )
    return out

Build the QuickIK body plan [ik] solves.

A body plan is a JSON kinematic tree in which every joint is a tracked keypoint, with its offset from its parent, its ordered DOFs and any zero-DOF pseudo-joints for keypoints mid-segment. build_body_plan assembles one per recording from the leg template, the baked chains and the recording's measured geometry, all in the model frame: the rig's world frame is about 23 degrees of pitch and a scale away from the model's, and QuickIK is f32, which wants order-1 coordinates. A DOF's neutral is the model's spring reference (ADR-0065).

BodyPlan dataclass

A generated QuickIK body plan plus the bookkeeping its caller needs.

Attributes:

Name Type Description
plan dict

The body-plan mapping, JSON-serializable (see to_json).

joint_names tuple[str, ...]

(N,) joint names in body-plan order, which is the order QuickIK takes observations in and lays its DOF state out in.

joint_point tuple[str | None, ...]

(N,) the skeleton point each joint tracks, or None (the root, the head/abdomen hinges).

joint_row ndarray

(N,) each joint's column in the skeleton's point order, -1 where it tracks nothing.

joint_branch, dof_branch

(N,) / (D,) the limb each joint / DOF belongs to (a leg, a chain, or "" for the root).

angle_names tuple[str, ...]

(D,) the flygym angle names in flat DOF order, the names of a run's ik/angles.

neutral ndarray

(D,) each DOF's neutral value, the start pose and the prior's target.

body_sim tuple[ndarray, float, ndarray]

(R, s, t) mapping the model frame onto the recording's world frame (to_model, to_world).

chain_scales dict[str, float]

chain name -> size, baked into the plan's chain offsets.

chain_offsets dict[str, ndarray]

chain name -> (3,) model-unit shift of a chain's base onto its measured landmark, baked in likewise (ADR-0066).

Source code in src/deeperfly/ik/bodyplan.py
@dataclass(frozen=True)
class BodyPlan:
    """A generated QuickIK body plan plus the bookkeeping its caller needs.

    Attributes:
        plan: The body-plan mapping, JSON-serializable (see [to_json][]).
        joint_names: `(N,)` joint names in body-plan order, which is the order QuickIK
            takes observations in and lays its DOF state out in.
        joint_point: `(N,)` the skeleton point each joint tracks, or `None` (the root,
            the head/abdomen hinges).
        joint_row: `(N,)` each joint's column in the skeleton's point order, `-1`
            where it tracks nothing.
        joint_branch, dof_branch: `(N,)` / `(D,)` the limb each joint / DOF belongs to
            (a leg, a chain, or `""` for the root).
        angle_names: `(D,)` the flygym angle names in flat DOF order, the
            `names` of a run's `ik/angles`.
        neutral: `(D,)` each DOF's neutral value, the start pose and the prior's
            target.
        body_sim: `(R, s, t)` mapping the model frame onto the recording's world
            frame ([to_model][], [to_world][]).
        chain_scales: `chain name -> size`, baked into the plan's chain offsets.
        chain_offsets: `chain name -> (3,)` model-unit shift of a chain's base onto
            its measured landmark, baked in likewise (ADR-0066).
    """

    plan: dict
    joint_names: tuple[str, ...]
    joint_point: tuple[str | None, ...]
    joint_row: np.ndarray
    joint_branch: tuple[str, ...]
    angle_names: tuple[str, ...]
    dof_branch: tuple[str, ...]
    neutral: np.ndarray
    body_sim: tuple[np.ndarray, float, np.ndarray]
    chain_scales: dict[str, float] = field(default_factory=dict)
    chain_offsets: dict[str, np.ndarray] = field(default_factory=dict)

    # -- views ---------------------------------------------------------------

    @property
    def n_joints(self) -> int:
        """How many joints the plan has."""
        return len(self.joint_names)

    @property
    def n_dofs(self) -> int:
        """How many degrees of freedom the plan has -- the `D` axis of the angles."""
        return len(self.angle_names)

    @property
    def fixed_base(self) -> bool:
        """Whether the body is held in the model frame instead of fitted per frame."""
        return bool(self.plan["fixed_base"])

    def kinematics(self) -> PlanKinematics:
        """Compile the plan for numpy forward kinematics."""
        return PlanKinematics.from_plan(self.plan)

    @property
    def joint_is_base(self) -> Bool[np.ndarray, "N"]:
        """`(N,)` which joints are their chain's base landmark.

        A base landmark sits on its chain's rotation center, so it is no evidence about
        the chain's angles (ADR-0065). Read off the plan's own joints, so a plan rebuilt
        from JSON agrees; not "no DOF moves this joint", which a fixed base's
        thorax-coxae also satisfy.
        """
        return np.asarray(
            [bool(j.get(f"x-{_META_KEY}-base")) for j in self.plan["joints"]],
            dtype=bool,
        )

    # -- frames --------------------------------------------------------------

    def to_model(
        self, world_pts: Float[np.ndarray, "*batch 3"]
    ) -> Float[np.ndarray, "*batch 3"]:
        """World points -> the model frame: `R^T (p - t) / s`."""
        rot, scale, trans = self.body_sim
        return ((np.asarray(world_pts, dtype=float) - trans) @ rot) / max(scale, _EPS)

    def to_world(
        self, model_pts: Float[np.ndarray, "*batch 3"]
    ) -> Float[np.ndarray, "*batch 3"]:
        """Model points -> the world frame: `s R p + t` (inverse of [to_model][])."""
        rot, scale, trans = self.body_sim
        return scale * (np.asarray(model_pts, dtype=float) @ rot.T) + trans

    # -- serialization -------------------------------------------------------

    def to_json(self) -> str:
        """The plan as JSON, what QuickIK parses.

        No `default=` fallback, so a stray numpy value raises instead of being
        stringified. The `x-deeperfly` block describes what built the plan; QuickIK and
        deeperfly both ignore it.
        """
        return json.dumps(self.plan, separators=(",", ":"), sort_keys=False)

fixed_base property

fixed_base: bool

Whether the body is held in the model frame instead of fitted per frame.

joint_is_base property

joint_is_base: Bool[ndarray, N]

(N,) which joints are their chain's base landmark.

A base landmark sits on its chain's rotation center, so it is no evidence about the chain's angles (ADR-0065). Read off the plan's own joints, so a plan rebuilt from JSON agrees; not "no DOF moves this joint", which a fixed base's thorax-coxae also satisfy.

n_dofs property

n_dofs: int

How many degrees of freedom the plan has -- the D axis of the angles.

n_joints property

n_joints: int

How many joints the plan has.

kinematics

kinematics() -> PlanKinematics

Compile the plan for numpy forward kinematics.

Source code in src/deeperfly/ik/bodyplan.py
def kinematics(self) -> PlanKinematics:
    """Compile the plan for numpy forward kinematics."""
    return PlanKinematics.from_plan(self.plan)

to_json

to_json() -> str

The plan as JSON, what QuickIK parses.

No default= fallback, so a stray numpy value raises instead of being stringified. The x-deeperfly block describes what built the plan; QuickIK and deeperfly both ignore it.

Source code in src/deeperfly/ik/bodyplan.py
def to_json(self) -> str:
    """The plan as JSON, what QuickIK parses.

    No `default=` fallback, so a stray numpy value raises instead of being
    stringified. The `x-deeperfly` block describes what built the plan; QuickIK and
    deeperfly both ignore it.
    """
    return json.dumps(self.plan, separators=(",", ":"), sort_keys=False)

to_model

to_model(
    world_pts: Float[ndarray, "*batch 3"],
) -> Float[np.ndarray, "*batch 3"]

World points -> the model frame: R^T (p - t) / s.

Source code in src/deeperfly/ik/bodyplan.py
def to_model(
    self, world_pts: Float[np.ndarray, "*batch 3"]
) -> Float[np.ndarray, "*batch 3"]:
    """World points -> the model frame: `R^T (p - t) / s`."""
    rot, scale, trans = self.body_sim
    return ((np.asarray(world_pts, dtype=float) - trans) @ rot) / max(scale, _EPS)

to_world

to_world(
    model_pts: Float[ndarray, "*batch 3"],
) -> Float[np.ndarray, "*batch 3"]

Model points -> the world frame: s R p + t (inverse of to_model).

Source code in src/deeperfly/ik/bodyplan.py
def to_world(
    self, model_pts: Float[np.ndarray, "*batch 3"]
) -> Float[np.ndarray, "*batch 3"]:
    """Model points -> the world frame: `s R p + t` (inverse of [to_model][])."""
    rot, scale, trans = self.body_sim
    return scale * (np.asarray(model_pts, dtype=float) @ rot.T) + trans

build_body_plan

build_body_plan(
    template: KinematicTemplate,
    skeleton,
    alignment: Alignment,
    body_sim: tuple[ndarray, float, ndarray],
    *,
    articulation: Articulation | None = None,
    chain_scales: dict[str, float] | None = None,
    chain_offsets: dict[str, ndarray] | None = None,
    fixed_body: bool = True,
) -> BodyPlan

Assemble the body plan for one recording.

alignment is the measured leg geometry in the model frame, body_sim maps that frame onto the world frame, and articulation adds the head/abdomen chains (None builds a legs-only plan). chain_scales and chain_offsets grow and shift each chain about its base (ADR-0066); a missing entry is model size on the registered base. fixed_body holds the root in the model frame (a tethered fly) instead of giving QuickIK a 6-DOF root per frame.

Source code in src/deeperfly/ik/bodyplan.py
def build_body_plan(
    template: KinematicTemplate,
    skeleton,
    alignment: Alignment,
    body_sim: tuple[np.ndarray, float, np.ndarray],
    *,
    articulation: Articulation | None = None,
    chain_scales: dict[str, float] | None = None,
    chain_offsets: dict[str, np.ndarray] | None = None,
    fixed_body: bool = True,
) -> BodyPlan:
    """Assemble the body plan for one recording.

    `alignment` is the measured leg geometry in the model frame, `body_sim` maps that
    frame onto the world frame, and `articulation` adds the head/abdomen chains
    (`None` builds a legs-only plan). `chain_scales` and `chain_offsets` grow and
    shift each chain about its base (ADR-0066); a missing entry is model size on the
    registered base. `fixed_body` holds the root in the model frame (a tethered fly)
    instead of giving QuickIK a 6-DOF root per frame.
    """
    scales = dict(chain_scales or {})
    shifts = {
        str(k): np.asarray(v, dtype=float) for k, v in (chain_offsets or {}).items()
    }
    joints: list[dict] = [
        {
            "name": ROOT_NAME,
            "parent": None,
            "offset_pos": [0.0, 0.0, 0.0],
            "offset_quat": list(_IDENTITY_QUAT),
            "dofs": [],
            "x-deeperfly-point": None,
            "x-deeperfly-branch": "",
        }
    ]
    for leg in template.legs:
        joints += _leg_joints(leg, skeleton, alignment, template.rest_axis)
    for chain in articulation.chains if articulation is not None else ():
        joints += _chain_joints(
            chain, scales.get(chain.name, 1.0), shifts.get(chain.name)
        )

    plan = {
        "fixed_base": bool(fixed_body),
        f"x-{_META_KEY}": {
            "template": template.name,
            "chain_scales": {k: float(v) for k, v in scales.items()},
            "chain_offsets": {
                k: [float(x) for x in np.asarray(v).reshape(3)]
                for k, v in shifts.items()
            },
            "body_sim": {
                "rot": [[float(v) for v in row] for row in np.asarray(body_sim[0])],
                "scale": float(body_sim[1]),
                "trans": [float(v) for v in np.asarray(body_sim[2])],
            },
        },
        "joints": joints,
    }

    index = {name: i for i, name in enumerate(skeleton.point_names)}
    points = tuple(j["x-deeperfly-point"] for j in joints)
    return BodyPlan(
        plan=plan,
        joint_names=tuple(str(j["name"]) for j in joints),
        joint_point=points,
        joint_row=np.asarray(
            [-1 if p is None else index.get(p, -1) for p in points], dtype=np.int64
        ),
        joint_branch=tuple(str(j["x-deeperfly-branch"]) for j in joints),
        angle_names=tuple(
            str(d["x-deeperfly-angle"]) for j in joints for d in j["dofs"]
        ),
        dof_branch=tuple(
            str(j["x-deeperfly-branch"]) for j in joints for _ in j["dofs"]
        ),
        neutral=np.asarray(
            [float(d["neutral"]) for j in joints for d in j["dofs"]], dtype=float
        ),
        body_sim=(
            np.asarray(body_sim[0], dtype=float),
            float(body_sim[1]),
            np.asarray(body_sim[2], dtype=float),
        ),
        chain_scales=scales,
        chain_offsets=shifts,
    )

Forward kinematics in numpy: body plans, baked chains, and legs.

QuickIK's Python bindings return only angles and a root pose, and every consumer of the fit needs joint positions, so deeperfly evaluates them here. PlanKinematics walks a body plan by QuickIK's own convention (src/forward.rs): a joint's offset_pos applies in its parent's frame after the parent's DOFs, its own DOFs compose in listed order, and its keypoint sits at its origin before them. chain_affine and chain_fk are the baked chains' kinematics and leg_fk a leg's. Pure numpy, so a stored fit draws without the solver (ADR-0041).

PlanKinematics dataclass

A QuickIK body plan compiled for numpy forward kinematics over many frames.

Attributes:

Name Type Description
names tuple[str, ...]

(N,) joint names, in body-plan array order (which is also the observation order QuickIK expects and the DOF state layout).

parent ndarray

(N,) parent joint index, -1 for the root.

order ndarray

(N,) a traversal order in which every joint follows its parent.

offset_pos ndarray

(N, 3) each joint's constant translation from its parent's post-DOF frame.

offset_quat ndarray

(N, 4) that offset's rotation, a quaternion in (w, x, y, z).

dof_start ndarray

(N,) where each joint's slice of the flat DOF vector begins.

dof_count ndarray

(N,) how long that slice is.

dof_axis ndarray

(D, 3) each DOF's axis in its own joint's post-offset_quat frame.

dof_hinge ndarray

(D,) True for a hinge (rotation), False for a slide (translation).

Source code in src/deeperfly/ik/forward.py
@dataclass(frozen=True)
class PlanKinematics:
    """A QuickIK body plan compiled for numpy forward kinematics over many frames.

    Attributes:
        names: `(N,)` joint names, in body-plan array order (which is also the
            observation order QuickIK expects and the DOF state layout).
        parent: `(N,)` parent joint index, `-1` for the root.
        order: `(N,)` a traversal order in which every joint follows its parent.
        offset_pos: `(N, 3)` each joint's constant translation from its parent's
            post-DOF frame.
        offset_quat: `(N, 4)` that offset's rotation, a quaternion in `(w, x, y, z)`.
        dof_start: `(N,)` where each joint's slice of the flat DOF vector begins.
        dof_count: `(N,)` how long that slice is.
        dof_axis: `(D, 3)` each DOF's axis in its own joint's post-`offset_quat`
            frame.
        dof_hinge: `(D,)` True for a hinge (rotation), False for a slide
            (translation).
    """

    names: tuple[str, ...]
    parent: np.ndarray
    order: np.ndarray
    offset_pos: np.ndarray
    offset_quat: np.ndarray
    dof_start: np.ndarray
    dof_count: np.ndarray
    dof_axis: np.ndarray
    dof_hinge: np.ndarray

    @classmethod
    def from_plan(cls, plan: dict) -> PlanKinematics:
        """Compile a body-plan mapping (the parsed JSON) for evaluation."""
        joints = list(plan["joints"])
        names = tuple(str(j["name"]) for j in joints)
        index = {name: i for i, name in enumerate(names)}
        if len(index) != len(names):
            raise ValueError("body plan has duplicate joint names")
        parent = np.full(len(joints), -1, dtype=np.int64)
        for i, j in enumerate(joints):
            p = j["parent"]
            if p is None:
                continue
            if str(p) not in index:
                raise ValueError(
                    f"body-plan joint {names[i]!r} names unknown parent {p!r}"
                )
            parent[i] = index[str(p)]
        if int((parent < 0).sum()) != 1:
            raise ValueError(
                f"body plan needs exactly one root joint (parent=null), "
                f"found {int((parent < 0).sum())}"
            )

        # Parents before children, without relying on QuickIK's own ordering rule, so
        # a plan read back from an older result evaluates too.
        order: list[int] = []
        pending = {i: int(parent[i]) for i in range(len(joints))}
        placed: set[int] = set()
        while pending:
            ready = [i for i, p in pending.items() if p < 0 or p in placed]
            if not ready:
                raise ValueError("body-plan joints do not form a tree (cycle)")
            for i in ready:
                order.append(i)
                placed.add(i)
                del pending[i]

        axes: list[list[float]] = []
        hinge: list[bool] = []
        starts = np.zeros(len(joints), dtype=np.int64)
        counts = np.zeros(len(joints), dtype=np.int64)
        for i, j in enumerate(joints):
            dofs: list[dict] = list(j.get("dofs") or ())
            starts[i] = len(axes)
            counts[i] = len(dofs)
            for d in dofs:
                axes.append([float(v) for v in d["axis"]])
                hinge.append(str(d["type"]) == "hinge")
        return cls(
            names=names,
            parent=parent,
            order=np.asarray(order, dtype=np.int64),
            offset_pos=np.asarray(
                [[float(v) for v in j["offset_pos"]] for j in joints], dtype=float
            ),
            offset_quat=np.asarray(
                [[float(v) for v in j["offset_quat"]] for j in joints], dtype=float
            ),
            dof_start=starts,
            dof_count=counts,
            dof_axis=np.asarray(axes, dtype=float).reshape(-1, 3),
            dof_hinge=np.asarray(hinge, dtype=bool),
        )

    @property
    def n_joints(self) -> int:
        """How many joints the plan has, the `N` axis."""
        return len(self.names)

    @property
    def n_dofs(self) -> int:
        """How many degrees of freedom the plan has, the `D` axis."""
        return int(self.dof_axis.shape[0])

    def dof_names(self, plan: dict) -> list[str]:
        """The `x-deeperfly-angle` label of every DOF, in flat DOF order.

        QuickIK's parser ignores a DOF's own `name`, so the plan carries the flygym
        angle name in an `x-` key.
        """
        out: list[str] = []
        for j in plan["joints"]:
            for d in j.get("dofs") or ():
                out.append(str(d["x-deeperfly-angle"]))
        return out

    def joint_positions(
        self,
        dof_angles: Float[np.ndarray, "T D"],
        root_pos: Float[np.ndarray, "T 3"] | None = None,
        root_rot: Float[np.ndarray, "T 4"] | None = None,
    ) -> Float[np.ndarray, "T N 3"]:
        """Every joint's position for a batch of poses, as QuickIK's `evaluate_fwdkin`.

        The root frame is `(root_pos, root_rot)`, identity when `None` (a fixed base).
        A NaN angle makes its joint and everything distal NaN, so an unfittable limb
        stays visibly absent. A single `(D,)` pose returns `(1, N, 3)`.
        """
        angles = np.atleast_2d(np.asarray(dof_angles, dtype=float))
        if angles.shape[1] != self.n_dofs:
            raise ValueError(
                f"expected {self.n_dofs} DOF angles per frame, got {angles.shape[1]}"
            )
        n_frames = angles.shape[0]

        if root_rot is None:
            root_mat = np.broadcast_to(np.eye(3), (n_frames, 3, 3))
        else:
            rr = np.atleast_2d(np.asarray(root_rot, dtype=float))
            root_mat = np.stack([quat_to_rmat(q) for q in rr])
        if root_pos is None:
            root_org = np.zeros((n_frames, 3))
        else:
            root_org = np.atleast_2d(np.asarray(root_pos, dtype=float))

        n = self.n_joints
        frame_rot = np.empty((n, n_frames, 3, 3))
        frame_org = np.empty((n, n_frames, 3))
        out = np.empty((n, n_frames, 3))

        for j in self.order:
            p = int(self.parent[j])
            prot = root_mat if p < 0 else frame_rot[p]
            porg = root_org if p < 0 else frame_org[p]
            # The joint's own keypoint: parent frame + this joint's constant offset,
            # unmoved by its own DOFs.
            own = porg + prot @ self.offset_pos[j]
            rot = prot @ quat_to_rmat(self.offset_quat[j])
            org = own
            start, count = int(self.dof_start[j]), int(self.dof_count[j])
            for d in range(start, start + count):
                value = angles[:, d]
                if self.dof_hinge[d]:
                    rot = rot @ axis_rmat(self.dof_axis[d], value)
                else:
                    org = org + (rot @ self.dof_axis[d]) * value[:, None]
            out[j] = own
            frame_rot[j] = rot
            frame_org[j] = org
        return np.moveaxis(out, 0, 1)

n_dofs property

n_dofs: int

How many degrees of freedom the plan has, the D axis.

n_joints property

n_joints: int

How many joints the plan has, the N axis.

dof_names

dof_names(plan: dict) -> list[str]

The x-deeperfly-angle label of every DOF, in flat DOF order.

QuickIK's parser ignores a DOF's own name, so the plan carries the flygym angle name in an x- key.

Source code in src/deeperfly/ik/forward.py
def dof_names(self, plan: dict) -> list[str]:
    """The `x-deeperfly-angle` label of every DOF, in flat DOF order.

    QuickIK's parser ignores a DOF's own `name`, so the plan carries the flygym
    angle name in an `x-` key.
    """
    out: list[str] = []
    for j in plan["joints"]:
        for d in j.get("dofs") or ():
            out.append(str(d["x-deeperfly-angle"]))
    return out

from_plan classmethod

from_plan(plan: dict) -> PlanKinematics

Compile a body-plan mapping (the parsed JSON) for evaluation.

Source code in src/deeperfly/ik/forward.py
@classmethod
def from_plan(cls, plan: dict) -> PlanKinematics:
    """Compile a body-plan mapping (the parsed JSON) for evaluation."""
    joints = list(plan["joints"])
    names = tuple(str(j["name"]) for j in joints)
    index = {name: i for i, name in enumerate(names)}
    if len(index) != len(names):
        raise ValueError("body plan has duplicate joint names")
    parent = np.full(len(joints), -1, dtype=np.int64)
    for i, j in enumerate(joints):
        p = j["parent"]
        if p is None:
            continue
        if str(p) not in index:
            raise ValueError(
                f"body-plan joint {names[i]!r} names unknown parent {p!r}"
            )
        parent[i] = index[str(p)]
    if int((parent < 0).sum()) != 1:
        raise ValueError(
            f"body plan needs exactly one root joint (parent=null), "
            f"found {int((parent < 0).sum())}"
        )

    # Parents before children, without relying on QuickIK's own ordering rule, so
    # a plan read back from an older result evaluates too.
    order: list[int] = []
    pending = {i: int(parent[i]) for i in range(len(joints))}
    placed: set[int] = set()
    while pending:
        ready = [i for i, p in pending.items() if p < 0 or p in placed]
        if not ready:
            raise ValueError("body-plan joints do not form a tree (cycle)")
        for i in ready:
            order.append(i)
            placed.add(i)
            del pending[i]

    axes: list[list[float]] = []
    hinge: list[bool] = []
    starts = np.zeros(len(joints), dtype=np.int64)
    counts = np.zeros(len(joints), dtype=np.int64)
    for i, j in enumerate(joints):
        dofs: list[dict] = list(j.get("dofs") or ())
        starts[i] = len(axes)
        counts[i] = len(dofs)
        for d in dofs:
            axes.append([float(v) for v in d["axis"]])
            hinge.append(str(d["type"]) == "hinge")
    return cls(
        names=names,
        parent=parent,
        order=np.asarray(order, dtype=np.int64),
        offset_pos=np.asarray(
            [[float(v) for v in j["offset_pos"]] for j in joints], dtype=float
        ),
        offset_quat=np.asarray(
            [[float(v) for v in j["offset_quat"]] for j in joints], dtype=float
        ),
        dof_start=starts,
        dof_count=counts,
        dof_axis=np.asarray(axes, dtype=float).reshape(-1, 3),
        dof_hinge=np.asarray(hinge, dtype=bool),
    )

joint_positions

joint_positions(
    dof_angles: Float[ndarray, "T D"],
    root_pos: Float[ndarray, "T 3"] | None = None,
    root_rot: Float[ndarray, "T 4"] | None = None,
) -> Float[np.ndarray, "T N 3"]

Every joint's position for a batch of poses, as QuickIK's evaluate_fwdkin.

The root frame is (root_pos, root_rot), identity when None (a fixed base). A NaN angle makes its joint and everything distal NaN, so an unfittable limb stays visibly absent. A single (D,) pose returns (1, N, 3).

Source code in src/deeperfly/ik/forward.py
def joint_positions(
    self,
    dof_angles: Float[np.ndarray, "T D"],
    root_pos: Float[np.ndarray, "T 3"] | None = None,
    root_rot: Float[np.ndarray, "T 4"] | None = None,
) -> Float[np.ndarray, "T N 3"]:
    """Every joint's position for a batch of poses, as QuickIK's `evaluate_fwdkin`.

    The root frame is `(root_pos, root_rot)`, identity when `None` (a fixed base).
    A NaN angle makes its joint and everything distal NaN, so an unfittable limb
    stays visibly absent. A single `(D,)` pose returns `(1, N, 3)`.
    """
    angles = np.atleast_2d(np.asarray(dof_angles, dtype=float))
    if angles.shape[1] != self.n_dofs:
        raise ValueError(
            f"expected {self.n_dofs} DOF angles per frame, got {angles.shape[1]}"
        )
    n_frames = angles.shape[0]

    if root_rot is None:
        root_mat = np.broadcast_to(np.eye(3), (n_frames, 3, 3))
    else:
        rr = np.atleast_2d(np.asarray(root_rot, dtype=float))
        root_mat = np.stack([quat_to_rmat(q) for q in rr])
    if root_pos is None:
        root_org = np.zeros((n_frames, 3))
    else:
        root_org = np.atleast_2d(np.asarray(root_pos, dtype=float))

    n = self.n_joints
    frame_rot = np.empty((n, n_frames, 3, 3))
    frame_org = np.empty((n, n_frames, 3))
    out = np.empty((n, n_frames, 3))

    for j in self.order:
        p = int(self.parent[j])
        prot = root_mat if p < 0 else frame_rot[p]
        porg = root_org if p < 0 else frame_org[p]
        # The joint's own keypoint: parent frame + this joint's constant offset,
        # unmoved by its own DOFs.
        own = porg + prot @ self.offset_pos[j]
        rot = prot @ quat_to_rmat(self.offset_quat[j])
        org = own
        start, count = int(self.dof_start[j]), int(self.dof_count[j])
        for d in range(start, start + count):
            value = angles[:, d]
            if self.dof_hinge[d]:
                rot = rot @ axis_rmat(self.dof_axis[d], value)
            else:
                org = org + (rot @ self.dof_axis[d]) * value[:, None]
        out[j] = own
        frame_rot[j] = rot
        frame_org[j] = org
    return np.moveaxis(out, 0, 1)

axis_rmat

axis_rmat(
    axis: Float[ndarray, 3],
    angle: float | Float[ndarray, "*batch"],
) -> Float[np.ndarray, "*batch 3 3"]

Rotation(s) by angle about the fixed unit axis, in Rodrigues' sin/cos form.

Smooth at angle = 0, where a joint routinely sits, unlike the axis-angle form.

Source code in src/deeperfly/ik/forward.py
def axis_rmat(
    axis: Float[np.ndarray, "3"], angle: float | Float[np.ndarray, "*batch"]
) -> Float[np.ndarray, "*batch 3 3"]:
    """Rotation(s) by `angle` about the fixed unit `axis`, in Rodrigues' sin/cos form.

    Smooth at `angle = 0`, where a joint routinely sits, unlike the axis-angle form.
    """
    ax, ay, az = (float(v) for v in np.asarray(axis, dtype=float))
    k = np.array([[0.0, -az, ay], [az, 0.0, -ax], [-ay, ax, 0.0]])
    angle = np.asarray(angle, dtype=float)
    s = np.sin(angle)[..., None, None]
    c = np.cos(angle)[..., None, None]
    return np.eye(3) + s * k + (1.0 - c) * (k @ k)

chain_affine

chain_affine(
    chain, depth: int, theta: Float[ndarray, D]
) -> tuple[Float[np.ndarray, "3 3"], Float[np.ndarray, 3]]

The model-frame affine (A, b) carrying a depth-d point: A p + b.

The baked chains rotate about their neutral world anchors: A_d = A_{d-1} R_d and b_d = A_{d-1}(c_d - R_d c_d) + b_{d-1}, so at zero angles every point stays at its neutral position.

Source code in src/deeperfly/ik/forward.py
def chain_affine(
    chain, depth: int, theta: Float[np.ndarray, "D"]
) -> tuple[Float[np.ndarray, "3 3"], Float[np.ndarray, "3"]]:
    """The model-frame affine `(A, b)` carrying a depth-`d` point: `A p + b`.

    The baked chains rotate about their neutral world anchors: `A_d = A_{d-1} R_d` and
    `b_d = A_{d-1}(c_d - R_d c_d) + b_{d-1}`, so at zero angles every point stays at its
    neutral position.
    """
    a_cum = np.eye(3)
    b_cum = np.zeros(3)
    for i in range(int(depth)):
        r = axis_rmat(chain.axes[i], float(theta[i]))
        c = np.asarray(chain.anchors[i], dtype=float)
        b_cum = a_cum @ (c - r @ c) + b_cum
        a_cum = a_cum @ r
    return a_cum, b_cum

chain_fk

chain_fk(
    anchors: Float[ndarray, "D 3"],
    axes: Float[ndarray, "D 3"],
    depths: tuple[int, ...],
    angles: Float[ndarray, D],
    markers: Float[ndarray, "M 3"],
) -> Float[np.ndarray, "M 3"]

A baked chain's posed markers, chain_affine at each marker's depth.

Source code in src/deeperfly/ik/forward.py
def chain_fk(
    anchors: Float[np.ndarray, "D 3"],
    axes: Float[np.ndarray, "D 3"],
    depths: tuple[int, ...],
    angles: Float[np.ndarray, "D"],
    markers: Float[np.ndarray, "M 3"],
) -> Float[np.ndarray, "M 3"]:
    """A baked chain's posed markers, [chain_affine][] at each marker's depth."""
    anchors = np.asarray(anchors, dtype=float)
    axes = np.asarray(axes, dtype=float)
    angles = np.asarray(angles, dtype=float)
    markers = np.asarray(markers, dtype=float)
    a_at = [np.eye(3)]
    b_at = [np.zeros(3)]
    for i in range(anchors.shape[0]):
        r = axis_rmat(axes[i], float(angles[i]))
        c = anchors[i]
        b_at.append(a_at[-1] @ (c - r @ c) + b_at[-1])
        a_at.append(a_at[-1] @ r)
    if markers.shape[0] == 0:
        return np.zeros((0, 3))
    return np.stack(
        [a_at[int(d)] @ markers[k] + b_at[int(d)] for k, d in enumerate(depths)]
    )

leg_fk

leg_fk(
    angles: Float[ndarray, D],
    axes: Float[ndarray, "D 3"],
    seglens: Float[ndarray, J],
    dof_counts: tuple[int, ...],
    *,
    rest_axis: Float[ndarray, 3] | None = None,
    quats: Float[ndarray, "J 4"] | None = None,
) -> Float[np.ndarray, "J 3"]

One leg's joint positions in the leg-local frame, from its joint angles.

Each joint enters its parent's post-DOF frame through quats (identity when None, as for NeuroMechFly) and rotates about its DOF axes in order, and the next segment runs along rest_axis; all angles zero is the rest pose, by the body plan's own convention. seglens[0] is unused (the root sits at the origin), and dof_counts is the DOFs per joint ((3, 2, 1, 1, 0)).

Source code in src/deeperfly/ik/forward.py
def leg_fk(
    angles: Float[np.ndarray, "D"],
    axes: Float[np.ndarray, "D 3"],
    seglens: Float[np.ndarray, "J"],
    dof_counts: tuple[int, ...],
    *,
    rest_axis: Float[np.ndarray, "3"] | None = None,
    quats: Float[np.ndarray, "J 4"] | None = None,
) -> Float[np.ndarray, "J 3"]:
    """One leg's joint positions in the leg-local frame, from its joint angles.

    Each joint enters its parent's post-DOF frame through `quats` (identity when
    `None`, as for NeuroMechFly) and rotates about its DOF axes in order, and the next
    segment runs along `rest_axis`; all angles zero is the rest pose, by the body
    plan's own convention. `seglens[0]` is unused (the root sits at the origin), and
    `dof_counts` is the DOFs per joint (`(3, 2, 1, 1, 0)`).
    """
    angles = np.asarray(angles, dtype=float)
    axes = np.asarray(axes, dtype=float)
    seglens = np.asarray(seglens, dtype=float)
    axis = REST_AXIS if rest_axis is None else np.asarray(rest_axis, dtype=float)
    quats = None if quats is None else np.asarray(quats, dtype=float)
    rot = np.eye(3)
    pos = np.zeros(3)
    out = []
    d = 0
    for j, n_dofs in enumerate(dof_counts):
        pos = pos + rot @ (axis * seglens[j])
        out.append(pos)
        if quats is not None:
            rot = rot @ quat_to_rmat(quats[j])
        for _ in range(n_dofs):
            rot = rot @ axis_rmat(axes[d], angles[d])
            d += 1
    return np.stack(out) if out else np.zeros((0, 3))

quat_to_rmat

quat_to_rmat(
    quat: Float[ndarray, 4],
) -> Float[np.ndarray, "3 3"]

Rotation matrix of a quaternion given as (w, x, y, z) (the body-plan order).

Source code in src/deeperfly/ik/forward.py
def quat_to_rmat(quat: Float[np.ndarray, "4"]) -> Float[np.ndarray, "3 3"]:
    """Rotation matrix of a quaternion given as `(w, x, y, z)` (the body-plan order)."""
    w, x, y, z = (float(v) for v in np.asarray(quat, dtype=float))
    n = np.sqrt(w * w + x * x + y * y + z * z)
    if n < _EPS:
        return np.eye(3)
    w, x, y, z = w / n, x / n, y / n, z / n
    return np.array(
        [
            [1 - 2 * (y * y + z * z), 2 * (x * y - w * z), 2 * (x * z + w * y)],
            [2 * (x * y + w * z), 1 - 2 * (x * x + z * z), 2 * (y * z - w * x)],
            [2 * (x * z - w * y), 2 * (y * z + w * x), 1 - 2 * (x * x + y * y)],
        ]
    )

rmat_to_quat

rmat_to_quat(
    rmat: Float[ndarray, "3 3"],
) -> Float[np.ndarray, 4]

Quaternion (w, x, y, z) of a rotation matrix (Shepperd's method).

Picks the branch off the largest diagonal term rather than always solving for w, so it stays conditioned at a 180-degree rotation (where w -> 0).

Source code in src/deeperfly/ik/forward.py
def rmat_to_quat(rmat: Float[np.ndarray, "3 3"]) -> Float[np.ndarray, "4"]:
    """Quaternion `(w, x, y, z)` of a rotation matrix (Shepperd's method).

    Picks the branch off the largest diagonal term rather than always solving for `w`,
    so it stays conditioned at a 180-degree rotation (where `w -> 0`).
    """
    m = np.asarray(rmat, dtype=float)
    trace = m[0, 0] + m[1, 1] + m[2, 2]
    if trace > 0.0:
        s = np.sqrt(trace + 1.0) * 2.0
        q = np.array(
            [
                0.25 * s,
                (m[2, 1] - m[1, 2]) / s,
                (m[0, 2] - m[2, 0]) / s,
                (m[1, 0] - m[0, 1]) / s,
            ]
        )
    else:
        i = int(np.argmax(np.diag(m)))
        j, k = (i + 1) % 3, (i + 2) % 3
        s = np.sqrt(1.0 + m[i, i] - m[j, j] - m[k, k]) * 2.0
        q = np.zeros(4)
        q[0] = (m[k, j] - m[j, k]) / s
        q[1 + i] = 0.25 * s
        q[1 + j] = (m[j, i] + m[i, j]) / s
        q[1 + k] = (m[k, i] + m[i, k]) / s
    q /= max(float(np.linalg.norm(q)), _EPS)
    return q if q[0] >= 0.0 else -q  # canonical sign, so equal rotations compare equal

Pose the bundled NeuroMechFly mesh to a fitted pose, for the 2D overlay.

The pack's baked mesh.npz is posed straight from a fit's joint positions and chain angles, with no MuJoCo at runtime. A leg bone is mapped from its neutral endpoints onto the live ones, its roll fixed by the body's dorsal axis; the head and abdomen nodes follow their chains' forward kinematics; the rigid body is one similarity fit to the six thorax-coxa keypoints. ModelMesh.pose returns one frame's vertices.

ModelMesh dataclass

The baked NeuroMechFly overlay mesh and the data to pose it from a fit.

Attributes:

Name Type Description
vertices Float[ndarray, 'Nv 3']

(Nv, 3) neutral-pose vertices in the model's world frame.

faces Int[ndarray, 'Nf 3']

(Nf, 3) triangle vertex indices into vertices, counterclockwise seen from outside; every part is closed (ADR-0099).

face_rgb ndarray

(Nf, 3) uint8 per-face color.

vert_slot Int[ndarray, Nv]

(Nv,) the render slot of each vertex: 0 is the rigid body, >= 1 are leg bones.

slot_prox Int[ndarray, S]

(n_slots,) the proximal keypoint index of each leg bone, in the skeleton's order; -1 for the body and articulated-node slots.

slot_dist Int[ndarray, S]

(n_slots,) the distal keypoint index, same convention.

slot_chain Int[ndarray, S]

(n_slots,) the articulation chain index of each node slot (the head and abdomen-segment meshes); -1 for body and leg-bone slots.

slot_depth Int[ndarray, S]

(n_slots,) that node slot's depth along its chain, same convention.

kp_neutral Float[ndarray, 'P 3']

(P, 3) the model's neutral keypoint positions (skeleton order).

anchor_idx Int[ndarray, 6]

(6,) the keypoint indices observing the model's anchor bodies, which place the rigid body transform.

vert_part Int[ndarray, Nv]

(Nv,) each vertex's body-part index into part_names (so the overlay can hide whole parts -- e.g. the wings -- per the render config).

part_names tuple[str, ...]

The body-part names, indexed by vert_part ("wings", "legs", "head", "thorax", "abdomen", "eyes", "antennae", "halteres").

Source code in src/deeperfly/ik/mesh.py
@dataclass(frozen=True)
class ModelMesh:
    """The baked NeuroMechFly overlay mesh and the data to pose it from a fit.

    Attributes:
        vertices: `(Nv, 3)` neutral-pose vertices in the model's world frame.
        faces: `(Nf, 3)` triangle vertex indices into `vertices`, counterclockwise
            seen from outside; every part is closed (ADR-0099).
        face_rgb: `(Nf, 3)` uint8 per-face color.
        vert_slot: `(Nv,)` the render slot of each vertex: `0` is the rigid body,
            `>= 1` are leg bones.
        slot_prox: `(n_slots,)` the proximal keypoint index of each leg bone, in the
            skeleton's order; `-1` for the body and articulated-node slots.
        slot_dist: `(n_slots,)` the distal keypoint index, same convention.
        slot_chain: `(n_slots,)` the articulation chain index of each node slot (the
            head and abdomen-segment meshes); `-1` for body and leg-bone slots.
        slot_depth: `(n_slots,)` that node slot's depth along its chain, same
            convention.
        kp_neutral: `(P, 3)` the model's neutral keypoint positions (skeleton order).
        anchor_idx: `(6,)` the keypoint indices observing the model's anchor bodies,
            which place the rigid body transform.
        vert_part: `(Nv,)` each vertex's body-part index into `part_names` (so
            the overlay can hide whole parts -- e.g. the wings -- per the render
            config).
        part_names: The body-part names, indexed by `vert_part` (`"wings"`,
            `"legs"`, `"head"`, `"thorax"`, `"abdomen"`, `"eyes"`, `"antennae"`,
            `"halteres"`).
    """

    vertices: Float[np.ndarray, "Nv 3"]
    faces: Int[np.ndarray, "Nf 3"]
    face_rgb: np.ndarray
    vert_slot: Int[np.ndarray, "Nv"]
    slot_prox: Int[np.ndarray, "S"]
    slot_dist: Int[np.ndarray, "S"]
    slot_chain: Int[np.ndarray, "S"]
    slot_depth: Int[np.ndarray, "S"]
    kp_neutral: Float[np.ndarray, "P 3"]
    anchor_idx: Int[np.ndarray, "6"]
    vert_part: Int[np.ndarray, "Nv"]
    part_names: tuple[str, ...]

    @classmethod
    def load(cls, path: str | Path = DEFAULT_MESH_PATH) -> "ModelMesh":
        """Read a baked mesh asset. Absent optional arrays fall back to "none"."""
        z = np.load(Path(path), allow_pickle=True)
        n_slots = z["slot_prox"].shape[0]
        nofill = np.full(n_slots, -1, dtype=np.int64)
        n_verts = z["vertices"].shape[0]
        return cls(
            vertices=z["vertices"].astype(float),
            faces=z["faces"].astype(np.int64),
            face_rgb=z["face_rgb"],
            vert_slot=z["vert_slot"].astype(np.int64),
            slot_prox=z["slot_prox"].astype(np.int64),
            slot_dist=z["slot_dist"].astype(np.int64),
            slot_chain=(
                z["slot_chain"].astype(np.int64) if "slot_chain" in z else nofill
            ),
            slot_depth=(
                z["slot_depth"].astype(np.int64) if "slot_depth" in z else nofill
            ),
            kp_neutral=z["kp_neutral"].astype(float),
            anchor_idx=z["anchor_idx"].astype(np.int64),
            vert_part=(
                z["vert_part"].astype(np.int64)
                if "vert_part" in z
                else np.zeros(n_verts, dtype=np.int64)
            ),
            part_names=(
                tuple(str(p) for p in z["part_names"]) if "part_names" in z else ()
            ),
        )

    def hidden_face_mask(self, hide_parts: "Sequence[str]") -> np.ndarray:
        """`(Nf,)` bool: faces belonging to a body part named in `hide_parts`.

        An unknown name hides nothing.
        """
        key = tuple(hide_parts or ())
        hit = self._hidden_masks.get(key)
        if hit is None:
            hit = self._hidden_masks.setdefault(key, self._compute_hidden_mask(key))
        return hit

    @functools.cached_property
    def _hidden_masks(self) -> dict[tuple[str, ...], np.ndarray]:
        """Per-part-list mask cache, a dict because this dataclass is not hashable."""
        return {}

    def _compute_hidden_mask(self, hide_parts: tuple[str, ...]) -> np.ndarray:
        """[hidden_face_mask][] without the cache."""
        n_faces = self.faces.shape[0]
        if not hide_parts or not self.part_names:
            return np.zeros(n_faces, dtype=bool)
        index = {name: i for i, name in enumerate(self.part_names)}
        hide_ids = [index[p] for p in hide_parts if p in index]
        if not hide_ids:
            return np.zeros(n_faces, dtype=bool)
        face_part = self.vert_part[self.faces[:, 0]]
        return np.isin(face_part, hide_ids)

    @functools.cached_property
    def _slot_verts(self) -> list[np.ndarray]:
        """Vertex-row indices for each slot (so posing is a few vectorized blocks)."""
        n = int(self.slot_prox.shape[0])
        return [np.flatnonzero(self.vert_slot == s) for s in range(n)]

    @functools.cached_property
    def _dist_anchor(self) -> Float[np.ndarray, "S 3"]:
        """`(n_slots, 3)` neutral distal endpoint each leg bone is skinned to.

        A bone's distal keypoint, except on the terminal segment, whose baked mesh stops
        short of the pretarsus: there it is the mesh's farthest point along the bone, so
        skinning stretches the tip onto the live pretarsus.
        """
        n = int(self.slot_prox.shape[0])
        anchor = np.full((n, 3), np.nan)
        prox = {int(p) for p in self.slot_prox.tolist() if p >= 0}
        for s in range(n):
            pi, di = int(self.slot_prox[s]), int(self.slot_dist[s])
            if pi < 0 or di < 0:
                continue
            a0, b0 = self.kp_neutral[pi], self.kp_neutral[di]
            anchor[s] = b0
            if di in prox:  # a mid-chain joint shared with the next segment: keep it
                continue
            axis = b0 - a0
            length = float(np.linalg.norm(axis))
            v = self.vertices[self._slot_verts[s]]
            if length < _EPS or v.shape[0] == 0:
                continue
            reach = float(
                (((v - a0) @ axis) / length**2).max()
            )  # farthest vertex / |axis|
            if reach > _EPS:
                anchor[s] = a0 + reach * axis
        return anchor

    def pose(
        self,
        pts3d: Float[np.ndarray, "P 3"],
        angles: Float[np.ndarray, "D"] | None = None,
        angle_names: list[str] | None = None,
        *,
        chain_scales: dict[str, float] | None = None,
        chain_offsets: dict[str, np.ndarray] | None = None,
        body_scale: float | None = None,
    ) -> tuple[Float[np.ndarray, "Nv 3"], np.ndarray]:
        """Pose the mesh to one frame's fitted joints (and optional chain angles).

        `pts3d` places the rigid body and skins the leg bones. With `angles` (named by
        `angle_names`) the head and abdomen nodes articulate; without, they ride the
        body at their neutral pose. `chain_scales`, `chain_offsets` and `body_scale`
        are the fit's (ADR-0066), so the drawn chains sit where the angles were fitted;
        `body_scale=None` re-fits the scale per frame.

        Returns:
            verts: `(Nv, 3)` posed world vertices, NaN for un-poseable segments.
            valid_faces: `(Nf,)` whether each face's three vertices are finite.
        """
        pts3d = np.asarray(pts3d, dtype=float)
        out = np.full_like(self.vertices, np.nan)

        rot, scale, trans = self._body_transform(pts3d, fixed_scale=body_scale)
        up = rot @ np.array([0.0, 0.0, 1.0])  # live dorsal axis fixes the bone roll
        node_xform = self._node_transforms(
            angles, angle_names, chain_scales, chain_offsets
        )

        for slot, rows in enumerate(self._slot_verts):
            if rows.size == 0:
                continue
            v = self.vertices[rows]
            if self.slot_chain[slot] >= 0:  # articulated head / abdomen node
                a, b = node_xform[
                    int(self.slot_chain[slot]), int(self.slot_depth[slot])
                ]
                out[rows] = scale * ((v @ a.T + b) @ rot.T) + trans
            elif self.slot_prox[slot] >= 0:  # leg bone -> skin between its endpoints
                a0 = self.kp_neutral[self.slot_prox[slot]]
                b0 = self._dist_anchor[
                    slot
                ]  # tarsus tip is stretched out to the pretarsus
                a1 = pts3d[self.slot_prox[slot]]
                b1 = pts3d[self.slot_dist[slot]]
                if not (np.isfinite(a1).all() and np.isfinite(b1).all()):
                    continue  # an occluded leg segment stays NaN -> dropped
                out[rows] = _skin_bone(v, a0, b0, a1, b1, up)
            else:  # slot 0: rigid body (thorax / wings / halteres)
                out[rows] = scale * (v @ rot.T) + trans

        valid_faces = np.isfinite(out[self.faces]).all(axis=(1, 2))
        return out, valid_faces

    def _node_transforms(
        self,
        angles: np.ndarray | None,
        angle_names: list[str] | None,
        chain_scales: dict[str, float] | None = None,
        chain_offsets: dict[str, np.ndarray] | None = None,
    ) -> dict[tuple[int, int], tuple[np.ndarray, np.ndarray]]:
        """Model-frame affine `(A, b)` per `(chain, depth)` node from the angles.

        Missing angles leave a node at its neutral pose. A chain scale `f` folds in as
        `A' = f A`, `b' = f b + (1 - f) base` and a chain offset `d` as `b + d`, the
        terms the body plan bakes in. Driven by the packaged articulation and the angle
        names rather than the recording's plan, so a result file from any version
        articulates.
        """
        from .articulation import load_articulation
        from .forward import chain_affine

        wanted = {
            (int(c), int(d))
            for c, d in zip(self.slot_chain, self.slot_depth, strict=True)
            if c >= 0
        }
        out: dict[tuple[int, int], tuple[np.ndarray, np.ndarray]] = {}
        if not wanted:  # no articulated node: do not read the articulation asset
            return out
        art = load_articulation()
        col = {name: i for i, name in enumerate(angle_names or [])}
        for chain_idx, depth in wanted:
            a, b = np.eye(3), np.zeros(3)
            if chain_idx < len(art.chains):
                chain = art.chains[chain_idx]
                if angles is not None and angle_names is not None:
                    theta = np.array(
                        [
                            float(angles[col[n]]) if n in col else np.nan
                            for n in chain.dof_names
                        ]
                    )
                    if np.isfinite(theta).all():
                        a, b = chain_affine(chain, depth, theta)
                f = float((chain_scales or {}).get(chain.name, 1.0))
                if f != 1.0:  # grow the node about its chain base anchor
                    base = chain.anchors[0]
                    a, b = f * a, f * b + (1.0 - f) * base
                shift = (chain_offsets or {}).get(chain.name)
                if shift is not None:  # then put that base where it was measured
                    b = b + np.asarray(shift, dtype=float)
            out[(chain_idx, depth)] = (a, b)
        return out

    def _body_transform(
        self,
        pts3d: Float[np.ndarray, "P 3"],
        fixed_scale: float | None = None,
    ) -> tuple[np.ndarray, float, np.ndarray]:
        """Body placement `(R, s, t)` from the neutral coxae to the live coxae.

        `fixed_scale` holds the size at the recording's constant (ADR-0066); `None` fits
        it per frame.
        """
        src = self.kp_neutral[self.anchor_idx]
        dst = pts3d[self.anchor_idx]
        good = np.isfinite(dst).all(axis=1)
        if int(good.sum()) < 3:  # too few anchors to place the body
            fallback = 1.0 if fixed_scale is None else float(fixed_scale)
            return np.eye(3), fallback, np.zeros(3)
        from .articulation import umeyama

        rot, scale, trans = umeyama(src[good], dst[good])
        if fixed_scale is not None:
            scale = float(fixed_scale)
            trans = dst[good].mean(axis=0) - scale * (rot @ src[good].mean(axis=0))
        return rot, scale, trans

hidden_face_mask

hidden_face_mask(hide_parts: Sequence[str]) -> np.ndarray

(Nf,) bool: faces belonging to a body part named in hide_parts.

An unknown name hides nothing.

Source code in src/deeperfly/ik/mesh.py
def hidden_face_mask(self, hide_parts: "Sequence[str]") -> np.ndarray:
    """`(Nf,)` bool: faces belonging to a body part named in `hide_parts`.

    An unknown name hides nothing.
    """
    key = tuple(hide_parts or ())
    hit = self._hidden_masks.get(key)
    if hit is None:
        hit = self._hidden_masks.setdefault(key, self._compute_hidden_mask(key))
    return hit

load classmethod

load(path: str | Path = DEFAULT_MESH_PATH) -> ModelMesh

Read a baked mesh asset. Absent optional arrays fall back to "none".

Source code in src/deeperfly/ik/mesh.py
@classmethod
def load(cls, path: str | Path = DEFAULT_MESH_PATH) -> "ModelMesh":
    """Read a baked mesh asset. Absent optional arrays fall back to "none"."""
    z = np.load(Path(path), allow_pickle=True)
    n_slots = z["slot_prox"].shape[0]
    nofill = np.full(n_slots, -1, dtype=np.int64)
    n_verts = z["vertices"].shape[0]
    return cls(
        vertices=z["vertices"].astype(float),
        faces=z["faces"].astype(np.int64),
        face_rgb=z["face_rgb"],
        vert_slot=z["vert_slot"].astype(np.int64),
        slot_prox=z["slot_prox"].astype(np.int64),
        slot_dist=z["slot_dist"].astype(np.int64),
        slot_chain=(
            z["slot_chain"].astype(np.int64) if "slot_chain" in z else nofill
        ),
        slot_depth=(
            z["slot_depth"].astype(np.int64) if "slot_depth" in z else nofill
        ),
        kp_neutral=z["kp_neutral"].astype(float),
        anchor_idx=z["anchor_idx"].astype(np.int64),
        vert_part=(
            z["vert_part"].astype(np.int64)
            if "vert_part" in z
            else np.zeros(n_verts, dtype=np.int64)
        ),
        part_names=(
            tuple(str(p) for p in z["part_names"]) if "part_names" in z else ()
        ),
    )

pose

pose(
    pts3d: Float[ndarray, "P 3"],
    angles: Float[ndarray, D] | None = None,
    angle_names: list[str] | None = None,
    *,
    chain_scales: dict[str, float] | None = None,
    chain_offsets: dict[str, ndarray] | None = None,
    body_scale: float | None = None,
) -> tuple[Float[np.ndarray, "Nv 3"], np.ndarray]

Pose the mesh to one frame's fitted joints (and optional chain angles).

pts3d places the rigid body and skins the leg bones. With angles (named by angle_names) the head and abdomen nodes articulate; without, they ride the body at their neutral pose. chain_scales, chain_offsets and body_scale are the fit's (ADR-0066), so the drawn chains sit where the angles were fitted; body_scale=None re-fits the scale per frame.

Returns:

Name Type Description
verts Float[ndarray, 'Nv 3']

(Nv, 3) posed world vertices, NaN for un-poseable segments.

valid_faces ndarray

(Nf,) whether each face's three vertices are finite.

Source code in src/deeperfly/ik/mesh.py
def pose(
    self,
    pts3d: Float[np.ndarray, "P 3"],
    angles: Float[np.ndarray, "D"] | None = None,
    angle_names: list[str] | None = None,
    *,
    chain_scales: dict[str, float] | None = None,
    chain_offsets: dict[str, np.ndarray] | None = None,
    body_scale: float | None = None,
) -> tuple[Float[np.ndarray, "Nv 3"], np.ndarray]:
    """Pose the mesh to one frame's fitted joints (and optional chain angles).

    `pts3d` places the rigid body and skins the leg bones. With `angles` (named by
    `angle_names`) the head and abdomen nodes articulate; without, they ride the
    body at their neutral pose. `chain_scales`, `chain_offsets` and `body_scale`
    are the fit's (ADR-0066), so the drawn chains sit where the angles were fitted;
    `body_scale=None` re-fits the scale per frame.

    Returns:
        verts: `(Nv, 3)` posed world vertices, NaN for un-poseable segments.
        valid_faces: `(Nf,)` whether each face's three vertices are finite.
    """
    pts3d = np.asarray(pts3d, dtype=float)
    out = np.full_like(self.vertices, np.nan)

    rot, scale, trans = self._body_transform(pts3d, fixed_scale=body_scale)
    up = rot @ np.array([0.0, 0.0, 1.0])  # live dorsal axis fixes the bone roll
    node_xform = self._node_transforms(
        angles, angle_names, chain_scales, chain_offsets
    )

    for slot, rows in enumerate(self._slot_verts):
        if rows.size == 0:
            continue
        v = self.vertices[rows]
        if self.slot_chain[slot] >= 0:  # articulated head / abdomen node
            a, b = node_xform[
                int(self.slot_chain[slot]), int(self.slot_depth[slot])
            ]
            out[rows] = scale * ((v @ a.T + b) @ rot.T) + trans
        elif self.slot_prox[slot] >= 0:  # leg bone -> skin between its endpoints
            a0 = self.kp_neutral[self.slot_prox[slot]]
            b0 = self._dist_anchor[
                slot
            ]  # tarsus tip is stretched out to the pretarsus
            a1 = pts3d[self.slot_prox[slot]]
            b1 = pts3d[self.slot_dist[slot]]
            if not (np.isfinite(a1).all() and np.isfinite(b1).all()):
                continue  # an occluded leg segment stays NaN -> dropped
            out[rows] = _skin_bone(v, a0, b0, a1, b1, up)
        else:  # slot 0: rigid body (thorax / wings / halteres)
            out[rows] = scale * (v @ rot.T) + trans

    valid_faces = np.isfinite(out[self.faces]).all(axis=(1, 2))
    return out, valid_faces

load_model_mesh cached

load_model_mesh(
    path: str | Path = DEFAULT_MESH_PATH,
) -> ModelMesh

Load (and cache) the packaged NeuroMechFly overlay mesh.

Source code in src/deeperfly/ik/mesh.py
@functools.lru_cache(maxsize=2)
def load_model_mesh(path: str | Path = DEFAULT_MESH_PATH) -> ModelMesh:
    """Load (and cache) the packaged NeuroMechFly overlay mesh."""
    return ModelMesh.load(path)

Access to the optional QuickIK solver, behind one explicit gate.

QuickIK publishes no wheels, so the deeperfly[ik] extra builds a Rust extension and a plain install stays Rust-free (ADR-0041). Everything that solves calls require_quickik inside a function body, never at module scope; the body plan, the forward kinematics and the mesh import nothing from here, so a stored fit renders without the extra.

MissingQuickIK

Bases: ImportError

Raised when inverse kinematics is asked for without the ik extra installed.

An ImportError, so a caller that can fall back to a stored fit catches it like any optional import.

Source code in src/deeperfly/ik/_quickik.py
class MissingQuickIK(ImportError):
    """Raised when inverse kinematics is asked for without the `ik` extra installed.

    An `ImportError`, so a caller that can fall back to a stored fit catches it like any
    optional import.
    """

require_quickik cached

require_quickik() -> ModuleType

The quickik module, or MissingQuickIK saying how to install it.

Source code in src/deeperfly/ik/_quickik.py
@functools.cache
def require_quickik() -> ModuleType:
    """The `quickik` module, or [MissingQuickIK][] saying how to install it."""
    try:
        import quickik
    except ImportError as exc:  # pragma: no cover -- exercised by the install-less path
        raise MissingQuickIK(
            "inverse kinematics needs the optional QuickIK solver, which is not "
            f"installed. Install the extra with:\n    {INSTALL_HINT}\n"
            "It builds a Rust extension, so a Rust toolchain is required "
            "(https://rustup.rs). Rendering an existing fit does not need it."
        ) from exc
    return quickik