Utilities¶
shinro.utils
parse_matrix¶
Convert a TOML config value to a matrix.
Flat list (e.g. [1.0, 2.0, 3.0]) → diagonal matrix.
Nested list (e.g. [[1, 0], [0, 2]]) → full matrix.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bk
|
ArrayBackend instance. |
required | |
value
|
List or list-of-lists from a TOML config. |
required |
Returns:
| Type | Description |
|---|---|
|
Array in the backend's native type. |
Source code in src/shinro/utils/array_backend.py
ArrayBackend¶
Bases: ABC
Abstract interface for array operations used by all components.
Every component takes an optional backend parameter. If None,
NumpyBackend is used. This lets the entire control stack switch
between numpy and torch by changing one object.
The @ operator (matmul) is intentionally not wrapped — it works
identically in numpy and torch for 2D arrays.
solve_qp
abstractmethod
¶
Solve a convex QP min ½ uᵀ H u + qᵀ u s.t. lb ≤ A u ≤ ub.
The solver is C-based (OSQP) and always runs on numpy, so this is the
per-step bridge for MPC: q is the state-dependent linear cost,
H/lb/ub are backend arrays, and A is a scipy sparse
constraint matrix (from MPC_LTI.constraints). Returns the full
solution u* (length = q length); MPC slices out the first
m controls.
Returns:
| Type | Description |
|---|---|
Any
|
The full QP solution vector, in the backend's native type. |
Source code in src/shinro/utils/array_backend.py
jacobian
abstractmethod
¶
Compute the Jacobian of f at x using finite differences or autograd.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
Callable |
required | |
x
|
Point at which to evaluate the Jacobian, shape (n,). |
required | |
eps
|
Step size for finite differences (ignored by autograd backends). |
1e-06
|
Returns:
| Type | Description |
|---|---|
Any
|
Jacobian matrix of shape (m, n). |
Source code in src/shinro/utils/array_backend.py
NumpyBackend¶
Bases: ArrayBackend
ArrayBackend implementation using numpy.
All operations delegate directly to np.xxx. The to_numpy and
from_numpy methods are no-ops since the data is already numpy.
solve_qp
¶
Solve the MPC QP with OSQP (numpy-native at the C boundary).
Uses eps=1e-6 to match the codegen-baked static solver the Zig VM
drives (scripts/gen_emosqp_test.py), so the live numpy path agrees
with the shipped .so. On a non-solved status, returns zeros
(preserving MPC's original fallback).
Source code in src/shinro/utils/array_backend.py
jacobian
¶
Central finite-difference Jacobian.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
Callable |
required | |
x
|
Point at which to evaluate, shape (n,). |
required | |
eps
|
Step size for finite differences. |
1e-06
|
Returns:
| Type | Description |
|---|---|
|
Jacobian matrix of shape (m, n). |
Source code in src/shinro/utils/array_backend.py
TorchBackend¶
Bases: ArrayBackend
ArrayBackend implementation using PyTorch.
All operations delegate to torch.xxx. Data lives on the device
specified at construction time. The to_numpy and from_numpy
methods handle device transfers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
device
|
Torch device string (e.g. |
'cpu'
|
Source code in src/shinro/utils/array_backend.py
solve_qp
¶
Solve the MPC QP with OSQP, converting across the tensor boundary.
q/H/lb/ub may be torch tensors (from the config-baked matrices); A is a scipy sparse matrix. Converts to numpy, solves with eps=1e-6 (matching the codegen static solver), and returns the solution as a tensor.
Source code in src/shinro/utils/array_backend.py
jacobian
¶
Jacobian via torch.autograd.functional.jacobian.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
Callable |
required | |
x
|
Point at which to evaluate, shape (n,). |
required | |
eps
|
Ignored (used for API compatibility with NumpyBackend). |
1e-06
|
Returns:
| Type | Description |
|---|---|
|
Jacobian matrix of shape (m, n). |
Source code in src/shinro/utils/array_backend.py
BatchedDynamicsAdapter¶
Adapt a Plant's single-state dynamics/cost to batched (N, ...) arrays.
The adapter detects the dynamics path once at construction:
- If
plant.dynamicsreturnsNone(the ABC default — LTI plants need not override it), the LTI matmul path is used. - Otherwise the nonlinear path is used, integrating
plant.dynamicswith semi-implicit Euler. On torch this is vectorized withtorch.vmap; on numpy it loops over the batch.
All arrays live in the plant's backend, so torch inputs stay torch throughout and run as batched native ops.
Usage
adapter = BatchedDynamicsAdapter(plant) x_next = adapter.dynamics_fn(x_batch, u_batch, dt) cost = adapter.cost_fn(x_batch, u_batch, Q, R)
Source code in src/shinro/utils/batched_adapter.py
dynamics_fn
¶
Batched dynamics update.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_batch
|
Batch of states (N, D_x). |
required | |
u_batch
|
Batch of controls (N, D_u). |
required | |
dt
|
float
|
Time step (s). |
required |
Returns:
| Type | Description |
|---|---|
|
Batch of next states (N, D_x). |
Source code in src/shinro/utils/batched_adapter.py
cost_fn
¶
Batched quadratic stage cost.
Computes \(c(x, u) = (x - x_{ref})^T Q (x - x_{ref}) + u^T R u\)
for each sample, returning a per-sample cost vector of shape (N,).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x_batch
|
Batch of states (N, D_x). |
required | |
u_batch
|
Batch of controls (N, D_u). |
required | |
Q
|
State cost matrix (D_x, D_x) or diagonal (D_x,). |
required | |
R
|
Control cost matrix (D_u, D_u) or diagonal (D_u,). |
required | |
x_ref
|
Any | None
|
Optional reference state (D_x,) to track. If None, regulates to the origin. |
None
|
Returns:
| Type | Description |
|---|---|
|
Per-sample cost vector (N,). |
Source code in src/shinro/utils/batched_adapter.py
resolve_config_path¶
Resolve a TOML config path to an existing file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Absolute path, a |
required |
Returns:
| Type | Description |
|---|---|
Path
|
A |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If no candidate location exists. |
Source code in src/shinro/utils/config_resolver.py
get_config_path¶
Public helper: resolve a config name (without the configs/ prefix).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Package-relative config name, e.g. |
required |
Returns:
| Type | Description |
|---|---|
Path
|
A |
Source code in src/shinro/utils/config_resolver.py
DataclassProtocol¶
strict_from_dict¶
Parse a raw TOML dict into a config dataclass, strictly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[T]
|
The config dataclass to construct. |
required |
raw
|
dict[str, Any]
|
Raw TOML dict. A |
required |
type_name
|
str
|
The registered component name, used in error messages and
to validate the |
required |
Returns:
| Type | Description |
|---|---|
T
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
On unknown keys, a mismatched |
Source code in src/shinro/utils/config_spec.py
config_from_toml¶
Load a TOML file and strictly parse it into a config dataclass.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[T]
|
The config dataclass to construct. |
required |
path
|
str
|
TOML path (resolved via
|
required |
type_name
|
str
|
The registered component name. |
required |
Returns:
| Type | Description |
|---|---|
T
|
A |
Source code in src/shinro/utils/config_spec.py
strict_from_list¶
Strict-parse a list of TOML dicts into a list of config dataclasses.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cls
|
type[T]
|
The nested config dataclass to construct per entry. |
required |
raw
|
list
|
List of raw TOML dicts (e.g. |
required |
type_name
|
str
|
The component name, used in error messages. |
required |
Returns:
| Type | Description |
|---|---|
list[T]
|
A list of |
Source code in src/shinro/utils/config_spec.py
strip_runtime_keys¶
Pop runtime-injected keys from a raw TOML config dict.
Sim-backed builds inject non-TOML values into plant config dicts
(engine objects, joint_groups tables) before
shinro.components.ConfigDriven.parse_config runs; strict parsing
must not see them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw
|
dict[str, Any]
|
Raw TOML config dict. |
required |
keys
|
tuple[str, ...]
|
Runtime-injected key names to pop. |
required |
Returns:
| Type | Description |
|---|---|
tuple[dict[str, Any], dict[str, Any]]
|
Tuple of (clean dict to strict-parse, dict of popped runtime values). |
Source code in src/shinro/utils/config_spec.py
BoundsConfig¶
LTISystemsAnalyzer¶
Analyze linear time-invariant state-space systems.
Provides controllability/observability checks, Gramian computations (continuous, discrete, finite-horizon), spectral diagnostics, Hankel singular values, balanced realization, and balanced truncation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A
|
ndarray
|
State matrix (n, n). |
required |
B
|
ndarray | None
|
Input matrix (n, m). Defaults to zeros. |
None
|
C
|
ndarray | None
|
Output matrix (p, n). Defaults to zeros. |
None
|
D
|
ndarray | None
|
Feedthrough matrix (p, m). Defaults to zeros. |
None
|
dt
|
float | None
|
Sampling time for discrete-time analysis. Defaults to None. |
None
|
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Source code in src/shinro/utils/controllability_checker.py
controllabilty
¶
Build the controllability matrix C = [B, AB, A²B, ..., A^{n-1}B].
Returns:
| Type | Description |
|---|---|
|
Controllability matrix (n, n*m). |
Source code in src/shinro/utils/controllability_checker.py
observability
¶
Build the observability matrix O = [C; CA; CA²; ...; CA^{n-1}].
Returns:
| Type | Description |
|---|---|
|
Observability matrix (n*p, n). |
Source code in src/shinro/utils/controllability_checker.py
is_controllable
¶
Check whether the system is controllable via Kalman rank test.
Returns:
| Type | Description |
|---|---|
|
True if the controllability matrix has full row rank. |
Source code in src/shinro/utils/controllability_checker.py
is_observable
¶
Check whether the system is observable via Kalman rank test.
Returns:
| Type | Description |
|---|---|
|
True if the observability matrix has full column rank. |
Source code in src/shinro/utils/controllability_checker.py
controllability_gramian
¶
Infinite-horizon controllability Gramian (continuous-time).
Returns cached result unless the system has changed.
Returns:
| Type | Description |
|---|---|
ndarray
|
Controllability Gramian Wc (n, n). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If A is not Hurwitz (eigenvalues with non-negative real part). |
Source code in src/shinro/utils/controllability_checker.py
observability_gramian
¶
Infinite-horizon observability Gramian (continuous-time).
Returns cached result unless the system has changed.
Returns:
| Type | Description |
|---|---|
ndarray
|
Observability Gramian Wo (n, n). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If A is not Hurwitz (eigenvalues with non-negative real part). |
Source code in src/shinro/utils/controllability_checker.py
discrete_controllability_gramian
¶
Discrete-time infinite-horizon controllability Gramian.
Solves A Wc A^T - Wc + B B^T = 0.
Returns:
| Type | Description |
|---|---|
ndarray
|
Discrete controllability Gramian Wc (n, n). |
Source code in src/shinro/utils/controllability_checker.py
discrete_observability_gramian
¶
Discrete-time infinite-horizon observability Gramian.
Solves A^T Wo A - Wo + C^T C = 0.
Returns:
| Type | Description |
|---|---|
ndarray
|
Discrete observability Gramian Wo (n, n). |
Source code in src/shinro/utils/controllability_checker.py
controllability_gramian_finite
¶
Finite-horizon controllability Gramian: Wc(T) = ∫₀ᵀ e^{Aτ} B B^T e^{A^Tτ} dτ.
Works for any A (stable or unstable).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
T
|
float
|
Horizon length. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Finite-horizon controllability Gramian (n, n). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If T is not positive. |
Source code in src/shinro/utils/controllability_checker.py
observability_gramian_finite
¶
Finite-horizon observability Gramian: Wo(T) = ∫₀ᵀ e^{A^Tτ} C^T C e^{Aτ} dτ.
Works for any A (stable or unstable).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
T
|
float
|
Horizon length. |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
Finite-horizon observability Gramian (n, n). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If T is not positive. |
Source code in src/shinro/utils/controllability_checker.py
gramian_spectrum
¶
Return eigenvalues of a chosen Gramian.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gramian
|
str
|
Which Gramian to inspect. One of "Wc", "Wo",
"Wc_finite: |
'Wc'
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Real parts of Gramian eigenvalues (n,). |
Source code in src/shinro/utils/controllability_checker.py
gramian_condition
¶
Return the 2-norm condition number of the selected Gramian.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
gramian
|
str
|
Which Gramian to inspect. One of "Wc", "Wo". Defaults to "Wc". |
'Wc'
|
Returns:
| Type | Description |
|---|---|
float
|
Condition number, or inf if the Gramian is singular. |
Source code in src/shinro/utils/controllability_checker.py
hankel_singular_values
¶
Compute the Hankel singular values σ_i = sqrt(λ_i(Wc Wo)).
Returns:
| Type | Description |
|---|---|
ndarray
|
Hankel singular values sorted in descending order (n,). |
Source code in src/shinro/utils/controllability_checker.py
balanced_realization
¶
Return the balanced state-space matrices (Abal, Bbal, Cbal).
The transformation T satisfies
T^{-1} A T = Abal, T^{-1} B = Bbal, C T = Cbal,
and the balanced Gramians are diag(σ_1, ..., σ_n).
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray, ndarray]
|
Tuple of (Abal, Bbal, Cbal) with shapes (n, n), (n, m), (p, n). |
Source code in src/shinro/utils/controllability_checker.py
balanced_truncate
¶
Perform balanced truncation to obtain an order-r reduced model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
r
|
int
|
Desired reduced order (0 < r <= n). |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray, ndarray, ndarray]
|
Tuple of (Ar, Br, Cr, Dr) with shapes (r, r), (r, m), (p, r), (p, m). |
Source code in src/shinro/utils/controllability_checker.py
reset_cache
¶
rank_report
¶
Return rank and condition of controllability and observability matrices.
Returns:
| Type | Description |
|---|---|
dict[str, tuple[int, float]]
|
Dict with keys 'controllability' and 'observability', each a |
dict[str, tuple[int, float]]
|
tuple of (rank, condition_number). |
Source code in src/shinro/utils/controllability_checker.py
summary
¶
Produce a human-readable report of system properties.
Includes rank/condition, Gramian eigenvalues, Hankel singular values, and a balanced-truncation error bound if previously computed.
Returns:
| Type | Description |
|---|---|
str
|
Formatted summary string. |
Source code in src/shinro/utils/controllability_checker.py
linearize¶
First-order Taylor expansion of f(x, u) around (x0, u0).
Computes the Jacobians A = ∂f/∂x and B = ∂f/∂u at the operating point (x0, u0) using central finite differences. The user's dynamics function always receives and returns numpy arrays regardless of the backend.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
Callable[[Any, Any], Any]
|
Continuous-time dynamics |
required |
x0
|
Any
|
Operating point state, shape (n_x,). |
required |
u0
|
Any
|
Operating point input, shape (n_u,). |
required |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
eps
|
float
|
Step size for finite differences. |
1e-06
|
Returns:
| Type | Description |
|---|---|
Any
|
Tuple (A, B) where A = ∂f/∂x has shape (n_x, n_x) and |
Any
|
B = ∂f/∂u has shape (n_x, n_u), in the backend's native type. |
Source code in src/shinro/utils/linearization.py
as_numpy_f¶
Wrap a backend-bound f(x, u) into a numpy-in/numpy-out callable.
linearize requires f to take and return numpy arrays
regardless of the backend. This bridges a backend-native callable such
as plant.dynamics(x_b, u_b) -> dx_b into that contract via
backend.from_numpy / backend.to_numpy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
f
|
Backend-bound callable |
required | |
backend
|
ArrayBackend whose |
required |
Returns:
| Type | Description |
|---|---|
|
|
Source code in src/shinro/utils/linearization.py
discretize_euler¶
First-order Euler discretization of a continuous-time linear model.
Computes the discrete-time matrices \(A_d = I + dt \cdot A_c\) and
\(B_d = dt \cdot B_c\), matching the semi-implicit Euler integration
used by the analytical plants. At small dt the discretization error is
negligible, and unlike scipy.linalg.expm it is backend-agnostic (the
Kalman filter stays torch-capable).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
A_c
|
Continuous-time state matrix (n_x, n_x). |
required | |
B_c
|
Continuous-time input matrix (n_x, n_u). |
required | |
dt
|
Time step in seconds. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
Tuple (A_d, B_d) in the backend's native type. |
Source code in src/shinro/utils/linearization.py
linearize_plant¶
Linearize a Plant's dynamics around an operating point.
Resolves (x0, u0) defaults, bridges plant.dynamics through
as_numpy_f, and delegates to linearize.
State default: x0 = plant.bk.zeros(len(plant.get_state())).
Control default: u0 = plant.bk.zeros(plant.input_dim) when the plant
declares input_dim; otherwise raises ValueError unless u0 is
passed explicitly, so multi-input plants cannot silently get a wrong-dim
u0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plant
|
Plant with |
required | |
x0
|
Operating point state. Defaults to zeros(state_dim). |
None
|
|
u0
|
Operating point input. Defaults to zeros(plant.input_dim), or
required when |
None
|
|
eps
|
Finite-difference step size. |
1e-06
|
Returns:
| Type | Description |
|---|---|
|
Tuple (A, B) in the plant's backend native type. |
Source code in src/shinro/utils/linearization.py
derive_model¶
Return the plant's discrete-time (A_d, B_d) model.
Delegates to plant.get_model(), which every plant implements as the
discrete-time model (analytical plants linearize + Euler-discretize at
their dt; velocity-commanded plants return A = I, B = dt·I
directly). This is the single source of the derived model shared by the
simulation and compile paths.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plant
|
A plant exposing |
required |
Returns:
| Type | Description |
|---|---|
|
Tuple |
Source code in src/shinro/utils/linearization.py
inject_model¶
Fill A_dynamics/B_dynamics from the plant when a config omits them.
cfg may be a config dict or a TOML path (resolved via
shinro.utils.config_resolver.resolve_config_path); a dict is
returned either way. A config that already declares either matrix is left
untouched — explicit model wins over the derived one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cfg
|
Controller/estimator config dict or TOML path. |
required | |
plant
|
The plant to derive the model from. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The config dict with |
dict
|
were absent. |
Source code in src/shinro/utils/linearization.py
inject_plant_derived¶
Fill dt / A_dynamics / B_dynamics on a Config dataclass from the plant.
The plant is the single source of physics truth. Precedence: explicit wins, derived fills, disagreement is loud.
dt: filled fromplant.dtwhen the config omits it; a declareddtthat disagrees with the plant's is a loud error (a mismatched PID/MPPIdtsilently mis-scales integration otherwise).A_dynamics/B_dynamics: derived viaderive_modeland injected (as TOML-serializable lists) when the config declares neither — only whenwith_modelis set, so sim-backed velocity-commanded plants keep their untouchedA = I, B = dt·Idefaults.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
cfg
|
A component Config dataclass instance (from
|
required | |
plant
|
The plant to derive from. |
required | |
with_model
|
bool
|
Also derive the model when A/B are both absent. |
False
|
Returns:
| Type | Description |
|---|---|
|
The (possibly replaced) Config dataclass. |
Raises:
| Type | Description |
|---|---|
ValueError
|
On a |