Skip to content

Core ABCs

shinro.components


ConfigDriven

Mixin for components constructed from a TOML config.

Subclasses declare a frozen Config dataclass whose fields ARE the config schema. parse_config strict-parses a raw TOML dict into it: unknown keys, a wrong type value, and missing required fields are loud errors naming the component, so authoring typos surface at parse time instead of mid-simulation.

Config class-attribute

Config: Any = None

The component's strict config dataclass. Subclasses must override.

parse_config classmethod

parse_config(config: Any) -> Any

Strict-parse a TOML config dict into cls.Config.

An already-built Config instance is passed through unchanged.

Parameters:

Name Type Description Default
config Any

Raw TOML dict (with optional type key) or a Config instance.

required

Returns:

Type Description
Any

A cls.Config instance.

Raises:

Type Description
NotImplementedError

If the subclass has not declared Config.

ValueError

On unknown keys, a mismatched type key, or missing required fields.

Source code in src/shinro/components.py
@classmethod
def parse_config(cls, config: Any) -> Any:
    """Strict-parse a TOML config dict into ``cls.Config``.

    An already-built Config instance is passed through unchanged.

    Args:
        config: Raw TOML dict (with optional ``type`` key) or a ``Config``
            instance.

    Returns:
        A ``cls.Config`` instance.

    Raises:
        NotImplementedError: If the subclass has not declared ``Config``.
        ValueError: On unknown keys, a mismatched ``type`` key, or missing
            required fields.
    """
    spec = cls.Config
    if spec is None:
        raise NotImplementedError(f"{cls.__name__} must define a Config dataclass")
    from shinro.utils.config_spec import strict_from_dict

    if isinstance(config, dict):
        return strict_from_dict(spec, config, getattr(cls, "_registry_name", cls.__name__))
    return config

load_config classmethod

load_config(config, plant=None, derive_model: bool = False)

Strict-parse a TOML config into cls.Config, then inject plant-derived values.

With plant, dt is filled from the plant when the config omits it and a declared dt that disagrees with the plant's is a loud error (the plant is the source of truth). With derive_model, A_dynamics/B_dynamics are additionally derived from the plant when the config declares neither. See shinro.utils.linearization.inject_plant_derived.

Parameters:

Name Type Description Default
config

Raw TOML dict (with optional type key) or a Config instance.

required
plant

Optional plant for derived-value injection.

None
derive_model bool

Also derive A_dynamics/B_dynamics from the plant when both are omitted.

False

Returns:

Type Description

A cls.Config instance.

Raises:

Type Description
ValueError

On strict-parse failure or a dt disagreement.

Source code in src/shinro/components.py
@classmethod
def load_config(cls, config, plant=None, derive_model: bool = False):
    """Strict-parse a TOML config into ``cls.Config``, then inject plant-derived values.

    With ``plant``, ``dt`` is filled from the plant when the config omits
    it and a declared ``dt`` that disagrees with the plant's is a loud
    error (the plant is the source of truth). With ``derive_model``,
    ``A_dynamics``/``B_dynamics`` are additionally derived from the plant
    when the config declares neither. See
    :func:`shinro.utils.linearization.inject_plant_derived`.

    Args:
        config: Raw TOML dict (with optional ``type`` key) or a ``Config``
            instance.
        plant: Optional plant for derived-value injection.
        derive_model: Also derive ``A_dynamics``/``B_dynamics`` from the
            plant when both are omitted.

    Returns:
        A ``cls.Config`` instance.

    Raises:
        ValueError: On strict-parse failure or a ``dt`` disagreement.
    """
    if isinstance(config, str):
        import tomllib

        from shinro.utils.config_resolver import resolve_config_path

        with open(resolve_config_path(config), "rb") as f:
            config = tomllib.load(f)
    cfg = cls.parse_config(config)
    if plant is not None:
        from shinro.utils.linearization import inject_plant_derived

        cfg = inject_plant_derived(cfg, plant, with_model=derive_model)
    return cfg

PhysicsEngine

Bases: ABC

Abstract base class for physics engines (MuJoCo, PyBullet, Drake, etc.).

Provides name-based access to joints, bodies, and actuators. Plants use this interface instead of importing a physics engine directly.

dt abstractmethod property

dt: float

Simulation timestep.

nv abstractmethod property

nv: int

Number of velocity DOFs (for Jacobian column count).

joint_names abstractmethod property

joint_names: list[str]

List of all joint names in the model.

actuator_names abstractmethod property

actuator_names: list[str]

List of all actuator names in the model.

body_names abstractmethod property

body_names: list[str]

List of all body names in the model.

has_free_joint property

has_free_joint: bool

Whether the model's first joint is a free (floating) joint.

backend property

backend: ArrayBackend

Array backend for this engine. Subclasses may override.

get_joint_qpos abstractmethod

get_joint_qpos(name: str) -> float

Get a single joint position by name.

Source code in src/shinro/components.py
@abstractmethod
def get_joint_qpos(self, name: str) -> float:
    """Get a single joint position by name."""
    pass

set_joint_qpos abstractmethod

set_joint_qpos(name: str, value: float)

Set a single joint position by name.

Source code in src/shinro/components.py
@abstractmethod
def set_joint_qpos(self, name: str, value: float):
    """Set a single joint position by name."""
    pass

get_joint_vel abstractmethod

get_joint_vel(name: str) -> float

Get a single joint velocity by name.

Source code in src/shinro/components.py
@abstractmethod
def get_joint_vel(self, name: str) -> float:
    """Get a single joint velocity by name."""
    pass

set_joint_ctrl abstractmethod

set_joint_ctrl(name: str, value: float)

Set a single actuator control signal by name.

Source code in src/shinro/components.py
@abstractmethod
def set_joint_ctrl(self, name: str, value: float):
    """Set a single actuator control signal by name."""
    pass

get_joint_limits abstractmethod

get_joint_limits(name: str) -> tuple[float, float]

Get [min, max] limits for a joint by name.

Source code in src/shinro/components.py
@abstractmethod
def get_joint_limits(self, name: str) -> tuple[float, float]:
    """Get [min, max] limits for a joint by name."""
    pass

get_body_xpos abstractmethod

get_body_xpos(name: str) -> ndarray

Get 3D position of a body by name.

Source code in src/shinro/components.py
@abstractmethod
def get_body_xpos(self, name: str) -> np.ndarray:
    """Get 3D position of a body by name."""
    pass

get_body_xquat abstractmethod

get_body_xquat(name: str) -> ndarray

Get 3D orientation of a body by name as a quaternion (w, x, y, z).

Source code in src/shinro/components.py
@abstractmethod
def get_body_xquat(self, name: str) -> np.ndarray:
    """Get 3D orientation of a body by name as a quaternion (w, x, y, z)."""
    pass

get_body_id abstractmethod

get_body_id(name: str) -> int

Get the internal body ID for a named body. Returns -1 if not found.

Source code in src/shinro/components.py
@abstractmethod
def get_body_id(self, name: str) -> int:
    """Get the internal body ID for a named body. Returns -1 if not found."""
    pass

compute_jacobian abstractmethod

compute_jacobian(body_name: str) -> tuple[ndarray, ndarray]

Return (jacp, jacr) — 3×nv position and orientation Jacobians for a body.

Source code in src/shinro/components.py
@abstractmethod
def compute_jacobian(self, body_name: str) -> tuple[np.ndarray, np.ndarray]:
    """Return (jacp, jacr) — 3×nv position and orientation Jacobians for a body."""
    pass

compute_jacobian_for_joints abstractmethod

compute_jacobian_for_joints(body_name: str, joint_names: list[str]) -> ndarray

Return the 6×k Jacobian (position + orientation) for a body, extracting only the columns corresponding to the named joints. Returns shape (6, k).

Source code in src/shinro/components.py
@abstractmethod
def compute_jacobian_for_joints(self, body_name: str, joint_names: list[str]) -> np.ndarray:
    """Return the 6×k Jacobian (position + orientation) for a body, extracting only
    the columns corresponding to the named joints. Returns shape (6, k)."""
    pass

forward abstractmethod

forward()

Run forward kinematics (mj_forward equivalent).

Source code in src/shinro/components.py
@abstractmethod
def forward(self):
    """Run forward kinematics (mj_forward equivalent)."""
    pass

step abstractmethod

step()

Advance physics by one timestep.

Source code in src/shinro/components.py
@abstractmethod
def step(self):
    """Advance physics by one timestep."""
    pass

reset abstractmethod

reset(qpos: ndarray | None = None)

Reset simulation state.

Source code in src/shinro/components.py
@abstractmethod
def reset(self, qpos: np.ndarray | None = None):
    """Reset simulation state."""
    pass

get_sensor_data abstractmethod

get_sensor_data() -> dict

Return a dict of all sensor data (qpos, qvel, ctrl, time, etc.).

Source code in src/shinro/components.py
@abstractmethod
def get_sensor_data(self) -> dict:
    """Return a dict of all sensor data (qpos, qvel, ctrl, time, etc.)."""
    pass

pin_free_base

pin_free_base(x: float, y: float) -> None

Pin a floating planar base to (x, y) with upright orientation.

For engines whose world contains a free joint driven by a self-integrating plant (see Plant.post_engine_step): writes the plant's planar pose into the base, pins its orientation upright, and zeroes the floating-base velocities. Default: no-op — engines without a floating base (or with a contact-physics-owned base) need not override.

Source code in src/shinro/components.py
def pin_free_base(self, x: float, y: float) -> None:
    """Pin a floating planar base to (x, y) with upright orientation.

    For engines whose world contains a free joint driven by a
    self-integrating plant (see :meth:`Plant.post_engine_step`): writes the
    plant's planar pose into the base, pins its orientation upright, and
    zeroes the floating-base velocities. Default: no-op — engines without a
    floating base (or with a contact-physics-owned base) need not override.
    """
    return None

Controller

Bases: ConfigDriven, ABC

Abstract base class for all controllers.

A controller computes control actions (e.g., torques, voltages) from reference signals and/or state feedback. Subclasses implement specific control laws (PID, MPC, LQR, etc.).

Usage

controller = MyController(...) action = controller.compute(reference=ref, state=x) controller.reset()

compute abstractmethod

compute(*args: Any, **kwargs: Any) -> Any

Compute the control action.

Parameters:

Name Type Description Default
*args Any

Positional arguments for computation.

()
**kwargs Any

Keyword arguments for computation.

{}

Returns:

Type Description
Any

The computed control action.

Source code in src/shinro/components.py
@abstractmethod
def compute(self,*args:Any,**kwargs:Any)-> Any:
    """
    Compute the control action.

    Args:
        *args: Positional arguments for computation.
        **kwargs: Keyword arguments for computation.

    Returns:
        The computed control action.
    """
    pass

reset

reset(*args: Any, **kwargs: Any) -> Any

Reset the controller to its initial state.

Parameters:

Name Type Description Default
*args Any

Positional arguments for reset.

()
**kwargs Any

Keyword arguments for reset.

{}

Returns:

Type Description
Any

None.

Source code in src/shinro/components.py
def reset(self,*args:Any,**kwargs:Any)-> Any:
    """
    Reset the controller to its initial state.

    Args:
        *args: Positional arguments for reset.
        **kwargs: Keyword arguments for reset.

    Returns:
        None.
    """
    pass

Plant

Bases: ConfigDriven, ABC

Abstract base class for a system plant.

A plant represents the system to be controlled (e.g., a robot, motor, or dynamical system). It provides the current state, a model for prediction/optimization, and a step method for simulation.

Usage

plant = MyPlant(...) state = plant.get_state() model = plant.get_model() next_state = plant.step(control_input)

input_dim class-attribute instance-attribute

input_dim: int | None = None

Control input dimension.

Plants that support linearization via linearize_plant should set this in __init__ so get_model() can default u0 correctly. None means the plant has not declared its input dimension; callers of linearize_plant must then pass u0 explicitly.

bk instance-attribute

Array backend the plant computes with (numpy or torch). Concrete plants set this in __init__.

dt instance-attribute

dt: float

Integration time step in seconds. Concrete plants set this in __init__.

get_state abstractmethod

get_state(*args: Any, **kwargs: Any) -> Any

Get the current state of the plant.

Returns:

Type Description
Any

The current state (e.g., joint positions, velocities).

Source code in src/shinro/components.py
@abstractmethod
def get_state(self, *args:Any, **kwargs:Any)->Any:
    """
    Get the current state of the plant.

    Returns:
        The current state (e.g., joint positions, velocities).
    """
    pass

get_model abstractmethod

get_model(*args: Any, **kwargs: Any) -> Any

Get the mathematical model of the plant.

For linear plants, returns (A, B) state-space matrices. For nonlinear plants, accepts optional x0 and u0 keyword arguments to linearize the dynamics around an operating point.

Returns:

Type Description
Any

The plant model (e.g., a dynamics function, matrices, or

Any

a callable used by the controller for prediction).

Source code in src/shinro/components.py
@abstractmethod
def get_model(self, *args:Any, **kwargs:Any)->Any:
    """
    Get the mathematical model of the plant.

    For linear plants, returns (A, B) state-space matrices.
    For nonlinear plants, accepts optional ``x0`` and ``u0`` keyword
    arguments to linearize the dynamics around an operating point.

    Returns:
        The plant model (e.g., a dynamics function, matrices, or
        a callable used by the controller for prediction).
    """
    pass

step abstractmethod

step(*args: Any, **kwargs: Any) -> Any

Perform a single time step simulation or execution of the plant.

Parameters:

Name Type Description Default
*args Any

Positional arguments (e.g., control input).

()
**kwargs Any

Keyword arguments.

{}

Returns:

Type Description
Any

The next state or result of the step.

Source code in src/shinro/components.py
@abstractmethod
def step(self, *args:Any, **kwargs:Any)->Any:
    """
    Perform a single time step simulation or execution of the plant.

    Args:
        *args: Positional arguments (e.g., control input).
        **kwargs: Keyword arguments.

    Returns:
        The next state or result of the step.
    """
    pass

physics_engine abstractmethod

physics_engine(engine: PhysicsEngine | None, *args: Any, **kwargs: Any) -> Any

Attach a physics engine to the plant.

Parameters:

Name Type Description Default
engine PhysicsEngine | None

A PhysicsEngine instance, or None to detach.

required
Source code in src/shinro/components.py
@abstractmethod
def physics_engine(self, engine: PhysicsEngine | None, *args: Any, **kwargs: Any)-> Any:
    """
    Attach a physics engine to the plant.

    Args:
        engine: A PhysicsEngine instance, or None to detach.
    """
    pass

reset_state

reset_state() -> None

Zero the plant's analytic state at a run boundary.

Concrete default preserves historical behavior (RobotSim.reset zeroed self.state whenever it was a numpy array); plants with custom reset semantics (e.g. state derived from the engine) override this. Called by the simulation factory's reset for every plant.

Source code in src/shinro/components.py
def reset_state(self) -> None:
    """Zero the plant's analytic state at a run boundary.

    Concrete default preserves historical behavior (``RobotSim.reset`` zeroed
    ``self.state`` whenever it was a numpy array); plants with custom reset
    semantics (e.g. state derived from the engine) override this. Called by
    the simulation factory's ``reset`` for every plant.
    """
    state = getattr(self, "state", None)
    if isinstance(state, np.ndarray):
        self.state = np.zeros_like(state)

post_engine_step

post_engine_step(engine: PhysicsEngine) -> None

Reconcile plant state with the engine after engine.step().

Called once per plant by the simulation factory after the shared engine advances the world. Plants that self-integrate their dynamics (e.g. a wheeled base with analytical kinematics under a sim-backed scenario) override this to write their state back into the engine — syncing a floating base, depositing visual joint deltas, etc. Plants whose dynamics are fully owned by the engine need not override.

Parameters:

Name Type Description Default
engine PhysicsEngine

The physics engine this plant is attached to.

required
Source code in src/shinro/components.py
def post_engine_step(self, engine: "PhysicsEngine") -> None:
    """Reconcile plant state with the engine after ``engine.step()``.

    Called once per plant by the simulation factory after the shared
    engine advances the world. Plants that self-integrate their dynamics
    (e.g. a wheeled base with analytical kinematics under a sim-backed
    scenario) override this to write their state back into the engine —
    syncing a floating base, depositing visual joint deltas, etc. Plants
    whose dynamics are fully owned by the engine need not override.

    Args:
        engine: The physics engine this plant is attached to.
    """
    return None

dynamics

dynamics(state: Any, control: Any, bk: Any | None = None) -> Any

Continuous-time dynamics \(\dot{x} = f(x, u)\).

Override in nonlinear plants to expose the dynamics function for linearization and for batched rollouts. Returns None by default (linear plants need not override this).

The override must accept a single state (n_x,) or a batch (N, n_x) and return the derivative with the same rank — a single state is a batch of one, so one implementation serves both. It must also route every backend call through bk (which defaults to self.bk), because tracing swaps only the traced component's backend, never the plant's. That is what lets the same function back the eager per-sample rollout, the finite-difference linearization, and the lowered graph — there is no second, separately-maintained batched formula to drift. See shinro.utils.batching for the rank helpers.

Parameters:

Name Type Description Default
state Any

Current state vector (n_x,) or batch (N, n_x).

required
control Any

Control input (n_u,), batch (N, n_u), or scalar.

required
bk Any | None

Backend to evaluate with. Defaults to self.bk.

None

Returns:

Type Description
Any

Time derivative with the rank of state, or None if not

Any

implemented.

Source code in src/shinro/components.py
def dynamics(self, state: Any, control: Any, bk: Any | None = None) -> Any:
    """Continuous-time dynamics :math:`\\dot{x} = f(x, u)`.

    Override in nonlinear plants to expose the dynamics function for
    linearization *and* for batched rollouts. Returns None by default
    (linear plants need not override this).

    The override must accept a single state ``(n_x,)`` **or** a batch
    ``(N, n_x)`` and return the derivative with the same rank — a single
    state is a batch of one, so one implementation serves both. It must
    also route every backend call through ``bk`` (which defaults to
    ``self.bk``), because tracing swaps only the traced component's
    backend, never the plant's. That is what lets the *same* function back
    the eager per-sample rollout, the finite-difference linearization, and
    the lowered graph — there is no second, separately-maintained batched
    formula to drift. See :mod:`shinro.utils.batching` for the rank
    helpers.

    Args:
        state: Current state vector (n_x,) or batch (N, n_x).
        control: Control input (n_u,), batch (N, n_u), or scalar.
        bk: Backend to evaluate with. Defaults to ``self.bk``.

    Returns:
        Time derivative with the rank of ``state``, or None if not
        implemented.
    """
    return None

measurement

measurement(state: Any, bk: Any | None = None) -> Any | None

Measurement model \(h(x)\) (the plant's sensor), or None.

The nonlinear counterpart of a filter's measurement_fn: override in a plant to expose its sensor, so a config/scenario-driven EKF/UKF derives h from the plant instead of a hand-wired callable — exactly as dynamics supplies \(f\). Return the measurement vector (n_y,).

Contract, mirroring dynamics: route every backend call through bk (defaults to self.bk) and accept a leading batch axis — (n_x,) or (N, n_x) in, same rank out — because the EKF differentiates h with a probe batch and the UKF pushes its sigma batch through it. The default returns None (no sensor model); a filter attached to such a plant keeps taking h from its config's measurement_matrix.

Parameters:

Name Type Description Default
state Any

State vector (n_x,) or batch (N, n_x).

required
bk Any | None

Backend to evaluate with. Defaults to self.bk.

None

Returns:

Type Description
Any | None

Measurement vector (n_y,) or batch (N, n_y), or None.

Source code in src/shinro/components.py
def measurement(self, state: Any, bk: Any | None = None) -> Any | None:
    """Measurement model :math:`h(x)` (the plant's sensor), or ``None``.

    The nonlinear counterpart of a filter's ``measurement_fn``: override in
    a plant to expose its sensor, so a config/scenario-driven EKF/UKF
    derives ``h`` from the plant instead of a hand-wired callable — exactly
    as :meth:`dynamics` supplies :math:`f`. Return the measurement vector
    ``(n_y,)``.

    Contract, mirroring :meth:`dynamics`: route every backend call through
    ``bk`` (defaults to ``self.bk``) and accept a leading **batch** axis —
    ``(n_x,)`` or ``(N, n_x)`` in, same rank out — because the EKF
    differentiates ``h`` with a probe batch and the UKF pushes its sigma
    batch through it. The default returns ``None`` (no sensor model); a
    filter attached to such a plant keeps taking ``h`` from its config's
    ``measurement_matrix``.

    Args:
        state: State vector (n_x,) or batch (N, n_x).
        bk: Backend to evaluate with. Defaults to ``self.bk``.

    Returns:
        Measurement vector (n_y,) or batch (N, n_y), or ``None``.
    """
    return None

control_matrix

control_matrix(state: Any, control: Any, bk: Any | None = None) -> Any

Control matrix \(g(x, u) = \partial f/\partial u\) at (state, control).

This is the input map a control law contracts against (SMC's \(c^T g\)); for a control-affine plant \(\dot{x} = f(x) + g(x)u\) it is the whole control dependence. Shape (n_x, n_u).

The default derives it from dynamics by central finite differences — the same arithmetic linearize_plant returns as B — so every plant with a dynamics gets a correct g with no extra code. Override it to be exact (a closed form, u-independent for force/torque plants) or cheaper; the default doubles as the test oracle for such an override.

Contract, mirroring dynamics: route every backend call through bk (defaults to self.bk), and keep a leading probe batch when one arrives — the tracing backend probes with a (2*n_u, n_u) block whose batch axis is shared with the state, so the state is broadcast onto it rather than evaluated alone.

Parameters:

Name Type Description Default
state Any

Current state vector (n_x,).

required
control Any

Current control (n_u,) — the point g is evaluated at. For plants whose actuation is nonlinear in u (e.g. rotor thrusts ~ w^2) this matters: g is zero at u = 0.

required
bk Any | None

Backend to evaluate with. Defaults to self.bk.

None

Returns:

Type Description
Any

The control matrix (n_x, n_u).

Raises:

Type Description
NotImplementedError

If the plant defines no dynamics to differentiate and does not override this method.

Source code in src/shinro/components.py
def control_matrix(self, state: Any, control: Any, bk: Any | None = None) -> Any:
    """Control matrix :math:`g(x, u) = \\partial f/\\partial u` at ``(state, control)``.

    This is the input map a control law contracts against (SMC's
    :math:`c^T g`); for a control-affine plant :math:`\\dot{x} = f(x) +
    g(x)u` it is the whole control dependence. Shape ``(n_x, n_u)``.

    The default **derives it from** :meth:`dynamics` by central finite
    differences — the same arithmetic ``linearize_plant`` returns as ``B``
    — so every plant with a ``dynamics`` gets a correct ``g`` with no extra
    code. Override it to be exact (a closed form, ``u``-independent for
    force/torque plants) or cheaper; the default doubles as the test oracle
    for such an override.

    Contract, mirroring :meth:`dynamics`: route every backend call through
    ``bk`` (defaults to ``self.bk``), and keep a leading **probe batch** when
    one arrives — the tracing backend probes with a ``(2*n_u, n_u)`` block
    whose batch axis is shared with the state, so the state is broadcast
    onto it rather than evaluated alone.

    Args:
        state: Current state vector (n_x,).
        control: Current control (n_u,) — the point ``g`` is evaluated at.
            For plants whose actuation is *nonlinear* in ``u`` (e.g. rotor
            thrusts ~ ``w^2``) this matters: ``g`` is zero at ``u = 0``.
        bk: Backend to evaluate with. Defaults to ``self.bk``.

    Returns:
        The control matrix ``(n_x, n_u)``.

    Raises:
        NotImplementedError: If the plant defines no :meth:`dynamics` to
            differentiate and does not override this method.
    """
    if type(self).dynamics is Plant.dynamics:
        raise NotImplementedError(
            f"{type(self).__name__}.control_matrix has no source: the plant defines no "
            f"dynamics() to differentiate and does not override control_matrix()."
        )
    backend: ArrayBackend = self.bk if bk is None else bk
    n_x = state.shape[0]

    def probe(u: Any) -> Any:
        if len(u.shape) == 2:
            # Traced probe batch: the backend's batch axis is shared, so the
            # state must be broadcast onto the same rows as the control probes.
            x = backend.reshape(state, 1, n_x) + backend.zeros((u.shape[0], n_x))
            return self.dynamics(x, u, bk=backend)
        return self.dynamics(state, u, bk=backend)

    return backend.jacobian(probe, backend.ravel(backend.array(control)))

operating_control

operating_control() -> Any | None

Control at the plant's operating point, or None for zero.

model_terms evaluates the control matrix \(g(x, u)\) at this point. For a plant whose actuation is linear in u (force/torque inputs) the default None is correct — g is u-independent, so the drift convention f(x, 0) / g(x, 0) holds. A plant whose actuation is nonlinear in u (e.g. rotor thrusts ~ w^2) has g(x, 0) = 0 and must override this to return the equilibrium control (e.g. hover rotor speeds), so the tangent model is taken about the real operating point.

Returns:

Type Description
Any | None

Operating control (n_u,), or None to use zero.

Source code in src/shinro/components.py
def operating_control(self) -> Any | None:
    """Control at the plant's operating point, or ``None`` for zero.

    :meth:`model_terms` evaluates the control matrix :math:`g(x, u)` at this
    point. For a plant whose actuation is *linear* in ``u`` (force/torque
    inputs) the default ``None`` is correct — ``g`` is ``u``-independent, so
    the drift convention ``f(x, 0)`` / ``g(x, 0)`` holds. A plant whose
    actuation is *nonlinear* in ``u`` (e.g. rotor thrusts ~ ``w^2``) has
    ``g(x, 0) = 0`` and must override this to return the equilibrium control
    (e.g. hover rotor speeds), so the tangent model is taken about the real
    operating point.

    Returns:
        Operating control (n_u,), or ``None`` to use zero.
    """
    return None

model_terms

model_terms(state: Any, bk: Any | None = None, n_u: int | None = None) -> tuple[Any, Any]

Control-affine model pair (f_x, g_x) at the operating point.

Returns the first-order (tangent) model of dynamics about operating_control, in the control-affine form the SMC law assumes:

\[ \dot{x} \approx f_x + g_x u, \quad g_x = \left.\frac{\partial f}{\partial u}\right|_{(x, u_{op})}, \quad f_x = f(x, u_{op}) - g_x u_{op} \]

so at u = u_op it reproduces the true dynamics f(x, u_op). With the default zero operating control this is exactly f(x, 0) and g(x, 0) — the drift convention — so control-affine plants are unaffected.

This is the single site the codegen (shinro.codegen.build._trace_plant_model) and gate A evaluate, so the live and lowered models cannot drift apart.

Parameters:

Name Type Description Default
state Any

State vector (n_x,).

required
bk Any | None

Backend to evaluate with. Defaults to self.bk.

None
n_u int | None

Control dimension — used to size the zero control when operating_control returns None. Falls back to the historical single-channel scalar probe when omitted.

None

Returns:

Type Description
tuple[Any, Any]

Tuple (f_x, g_x) with shapes (n_x,) and (n_x, n_u).

Raises:

Type Description
NotImplementedError

If the plant defines no dynamics to differentiate and does not override control_matrix.

Source code in src/shinro/components.py
def model_terms(self, state: Any, bk: Any | None = None, n_u: int | None = None) -> tuple[Any, Any]:
    """Control-affine model pair ``(f_x, g_x)`` at the operating point.

    Returns the first-order (tangent) model of :meth:`dynamics` about
    :meth:`operating_control`, in the control-affine form the SMC law
    assumes:

    .. math::

        \\dot{x} \\approx f_x + g_x u, \\quad
        g_x = \\left.\\frac{\\partial f}{\\partial u}\\right|_{(x, u_{op})},
        \\quad f_x = f(x, u_{op}) - g_x u_{op}

    so at ``u = u_op`` it reproduces the true dynamics ``f(x, u_op)``. With
    the default zero operating control this is exactly ``f(x, 0)`` and
    ``g(x, 0)`` — the drift convention — so control-affine plants are
    unaffected.

    This is the single site the codegen (:func:`shinro.codegen.build._trace_plant_model`)
    and gate A evaluate, so the live and lowered models cannot drift apart.

    Args:
        state: State vector (n_x,).
        bk: Backend to evaluate with. Defaults to ``self.bk``.
        n_u: Control dimension — used to size the zero control when
            :meth:`operating_control` returns ``None``. Falls back to the
            historical single-channel scalar probe when omitted.

    Returns:
        Tuple ``(f_x, g_x)`` with shapes ``(n_x,)`` and ``(n_x, n_u)``.

    Raises:
        NotImplementedError: If the plant defines no :meth:`dynamics` to
            differentiate and does not override :meth:`control_matrix`.
    """
    backend: ArrayBackend = self.bk if bk is None else bk
    u_op = self.operating_control()
    if u_op is None:
        u_op = 0.0 if n_u is None else backend.zeros(n_u)
    g_x = self.control_matrix(state, u_op, bk=backend)
    f_x = self.dynamics(state, u_op, bk=backend) - g_x @ backend.ravel(backend.array(u_op))
    return f_x, g_x

StateEstimator

Bases: ConfigDriven, ABC

Abstract base class for state estimators.

A state estimator reconstructs the full system state from noisy measurements and known control inputs. Subclasses implement filters such as Kalman filters, observers, or complementary filters.

Usage

estimator = MyEstimator(...) state = estimator.estimate(measurement=y, control_input=u) estimator.reset()

estimate abstractmethod

estimate(measurement: Any, control_input: Any) -> Any

Estimate the current state from a measurement and control input.

Parameters:

Name Type Description Default
measurement Any

Sensor measurement (e.g., encoder reading, IMU).

required
control_input Any

Control input applied to the system.

required

Returns:

Type Description
Any

The estimated state.

Source code in src/shinro/components.py
@abstractmethod
def estimate(self,measurement:Any,control_input:Any)->Any:
    """
    Estimate the current state from a measurement and control input.

    Args:
        measurement: Sensor measurement (e.g., encoder reading, IMU).
        control_input: Control input applied to the system.

    Returns:
        The estimated state.
    """
    pass

measure

measure(state: Any) -> Any

Expected measurement y = h(x) for a true state.

The simulation synthesizes each sensor reading by driving the ground-truth state through this model (then adding [noise.measurement]), so the estimator and the simulated sensor stay consistent. The default is the identity (the sensor observes the full state); a filter with a measurement model overrides it — the linear C for KF/Luenberger, or the nonlinear h (a config C or the plant's shinro.components.Plant.measurement) for EKF/UKF.

Parameters:

Name Type Description Default
state Any

True state (n_x,).

required

Returns:

Type Description
Any

Expected measurement (n_y,).

Source code in src/shinro/components.py
def measure(self, state: Any) -> Any:
    """Expected measurement ``y = h(x)`` for a true ``state``.

    The simulation synthesizes each sensor reading by driving the
    ground-truth state through this model (then adding
    ``[noise.measurement]``), so the estimator and the simulated sensor stay
    consistent. The default is the **identity** (the sensor observes the full
    state); a filter with a measurement model overrides it — the linear
    ``C`` for KF/Luenberger, or the nonlinear ``h`` (a config ``C`` or the
    plant's :meth:`~shinro.components.Plant.measurement`) for EKF/UKF.

    Args:
        state: True state (n_x,).

    Returns:
        Expected measurement (n_y,).
    """
    return state

reset

reset()

Reset the estimator to its initial condition.

Clears any internal buffer or covariance state.

Source code in src/shinro/components.py
def reset(self):
    """
    Reset the estimator to its initial condition.

    Clears any internal buffer or covariance state.
    """
    pass

TrajectoryGenerator

Bases: ConfigDriven, ABC

Abstract base class for trajectory generators.

A trajectory generator produces a reference path (position, velocity, acceleration) from a start to an end configuration. Subclasses implement splines, minimum-jerk, or motion-primitive generators.

Usage

generator = MyTrajectoryGenerator(...) trajectory = generator.generate(start=start_pos, end=end_pos) generator.reset()

generate abstractmethod

generate(start_position: Any, end_position: Any, duration: Any, *args: Any, **kwargs: Any) -> Any

Generate a trajectory from start to end position over a given duration.

Parameters:

Name Type Description Default
start_position Any

Initial configuration.

required
end_position Any

Final target configuration.

required
duration Any

Total time duration of the trajectory in seconds.

required

Returns:

Type Description
Any

The generated trajectory (e.g., a sequence of waypoints or

Any

a callable that returns setpoints at a given time).

Source code in src/shinro/components.py
@abstractmethod
def generate(self, start_position: Any, end_position: Any, duration: Any, *args: Any, **kwargs: Any) -> Any:
    """
    Generate a trajectory from start to end position over a given duration.

    Args:
        start_position: Initial configuration.
        end_position: Final target configuration.
        duration: Total time duration of the trajectory in seconds.

    Returns:
        The generated trajectory (e.g., a sequence of waypoints or
        a callable that returns setpoints at a given time).
    """
    pass

reset

reset()

Reset the trajectory generator to its initial state.

Clears any cached or ongoing trajectory data.

Source code in src/shinro/components.py
def reset(self):
    """
    Reset the trajectory generator to its initial state.

    Clears any cached or ongoing trajectory data.
    """
    pass