Controllers¶
shinro.controllers
Control algorithms for state regulation and trajectory tracking.
Provides discrete-time controllers that compute control actions from state feedback. All controllers implement the Controller ABC.
Available controllers: LQR — Linear Quadratic Regulator (DARE-based optimal gain) PIDController — Proportional-Integral-Derivative with anti-windup MPC_LTI — Linear Time-Invariant MPC with OSQP QP solver MPC_LTI_DeltaU — MPC with Δu (control rate) regularization MPPIController — Model Predictive Path Integral (sampling-based) SlidingModeController — Sliding Mode Control (robust nonlinear)
LQR¶
Bases: Controller
Linear Quadratic Regulator for discrete-time systems.
Computes the optimal state-feedback control law \(u = -K (x - x_t)\) that minimizes:
The gain K is computed once via the Discrete Algebraic Riccati Equation (DARE) and applied online as a single matrix-vector multiply.
The DARE solve uses scipy (numpy-only) since there is no equivalent in
PyTorch. The conversion is handled transparently via bk.to_numpy /
bk.from_numpy.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state_cost_matrix
|
Q — penalizes state deviation (n_x, n_x). |
required | |
control_cost_matrix
|
R — penalizes control effort (n_u, n_u). |
required | |
dynamics_state_matrix
|
A — discrete-time state transition (n_x, n_x). |
required | |
dynamics_control_matrix
|
B — control input matrix (n_x, n_u). |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Source code in src/shinro/controllers/lqr.py
gain_calculation
¶
Solve DARE and compute the optimal LQR gain matrix K.
Solves \(P = A^T P A - A^T P B (R + B^T P B)^{-1} B^T P A + Q\)
via scipy.linalg.solve_discrete_are, then computes:
The gain K is stored as self.K and used in compute().
Source code in src/shinro/controllers/lqr.py
compute
¶
Compute the optimal control input \(u = -K (x - x_t)\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_state
|
Current state vector (n_x,). |
required | |
target_state
|
Any | None
|
Desired state vector (n_x,). Defaults to zeros. |
None
|
Returns:
| Type | Description |
|---|---|
|
Control input vector (n_u,). |
Source code in src/shinro/controllers/lqr.py
reset
¶
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create an LQR controller from a TOML config dict or LQRConfig.
Config fields
state_cost: Diagonal Q weights (n_x,) or full Q matrix (n_x, n_x). control_cost: Diagonal R weights (n_u,) or full R matrix (n_u, n_u). dt: Time step (used for validation against the plant). A_dynamics: Full A matrix (n_x, n_x). Required unless a scenario with a plant derives it. B_dynamics: Full B matrix (n_x, n_u). Required unless a scenario with a plant derives it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or LQRConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
LQR instance. |
Source code in src/shinro/controllers/lqr.py
PIDController¶
Bases: Controller
Proportional-Integral-Derivative controller with anti-windup.
Computes control effort as:
Features: - Independent gains per channel (Kp, Ki, Kd as vectors). - Output clamping with integral anti-windup back-calculation. - Derivative on error (standard form).
When output is clamped, the integral term is back-calculated on saturated channels only to prevent integral windup.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kp
|
Proportional gain vector (n,). |
required | |
ki
|
Integral gain vector (n,). |
required | |
kd
|
Derivative gain vector (n,). |
required | |
dt
|
float
|
Time step in seconds. |
required |
output_limits
|
tuple | None
|
Optional (min_limits, max_limits) for output clamping. Each is an array of shape (n,). |
None
|
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Source code in src/shinro/controllers/pid.py
compute
¶
Compute the PID control effort.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_state
|
Measured current state (n,). |
required | |
target_state
|
Desired target state (n,). |
required |
Returns:
| Type | Description |
|---|---|
|
Control effort vector (n,). |
Source code in src/shinro/controllers/pid.py
reset
¶
Reset the controller's internal state (integral and previous error).
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create a PID controller from a TOML config dict or PIDConfig.
Config fields
kp: List of proportional gains (n,). Defaults to [1.0].
ki: List of integral gains (n,). Defaults to zeros.
kd: List of derivative gains (n,). Defaults to zeros.
dt: Time step. Required at runtime; injected from the plant in
scenario builds.
output_limits: Optional dict with min and max lists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or PIDConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
PIDController instance. |
Source code in src/shinro/controllers/pid.py
MPC_LTI_DeltaU¶
Bases: MPC_LTI
MPC with \(\Delta u\) (control rate) regularization.
Augments the state to \([x; u_{\text{prev}}]\) so the control variable becomes \(\Delta u = u_k - u_{k-1}\). The cost penalizes \(\Delta u^T S \Delta u\), smoothing chatter.
The augmented dynamics are:
where \(z = [x; u_{\text{prev}}]\).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
delta_u_penalty
|
S — cost matrix for \(\Delta u\) (n_u, n_u). |
required | |
**kwargs
|
Passed to MPC_LTI.init. |
{}
|
Source code in src/shinro/controllers/mpc_lti.py
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create an MPC_DeltaU controller from a TOML config dict or MPCDeltaUConfig.
Config fields
delta_u_penalty: Diagonal S weights (n_u,) or full S matrix (n_u, n_u).
horizon: Prediction horizon.
state_cost: Diagonal Q weights (n_x,) or full Q matrix (n_x, n_x).
control_cost: Diagonal R weights (n_u,) or full R matrix (n_u, n_u).
dt: Time step (used for validation against the plant).
A_dynamics: Full A matrix (n_x, n_x). Required unless a scenario with a
plant derives it.
B_dynamics: Full B matrix (n_x, n_u). Required unless a scenario with a
plant derives it.
constraints: Optional dict with upper and lower bound lists.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or MPCDeltaUConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
MPC_LTI_DeltaU instance. |
Source code in src/shinro/controllers/mpc_lti.py
compute
¶
Solve MPC with \(\Delta u\) regularization.
Regulates the tracking error e = current_state - target_state to
zero, augmenting the state with the previous control input. The QP
variable is the control rate \(\Delta u\); this method returns the
applied control \(u_k = u_{\text{prev}} + \Delta u_k\), which is
what the host feeds back as u_prev next tick.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_state
|
Original (non-augmented) state vector (n_x,). |
required | |
target_state
|
Any | None
|
Optional reference state (n_x,) to track. |
None
|
u_prev
|
Any | None
|
Previous control input (n_u,). Defaults to zeros. |
None
|
Returns:
| Type | Description |
|---|---|
|
Applied control \(u_k = u_{\text{prev}} + \Delta u_k\) (n_u,). |
Source code in src/shinro/controllers/mpc_lti.py
MPPIController¶
Bases: Controller
Model Predictive Path Integral controller.
Each compute() call samples \(N\) Gaussian control perturbations,
rolls out the dynamics over a horizon of \(K\) steps, weights the
perturbations by the softmax of their total cost, and advances the
receding-horizon nominal control sequence by one step. The returned action
is the first element of the updated nominal sequence, clipped to the
configured control bounds.
The dynamics and cost callables receive batched arrays of shape
(N, D_x) / (N, D_u) and may be injected directly or produced by
attach_plant from a plant's model.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
dynamics_fn
|
Any | None
|
Callable |
None
|
cost_fn
|
Any | None
|
Callable |
None
|
num_samples
|
int
|
Number of sampled perturbations N. |
100
|
temperature
|
float
|
Softmax temperature \(\lambda\) (> 0). |
1.0
|
dt
|
float
|
Time step passed to the dynamics. |
0.01
|
horizon
|
int
|
Prediction horizon K. |
10
|
noise_sigma
|
Standard deviation of the control perturbation per
input channel |
None
|
|
u_min
|
Optional lower control bound |
None
|
|
u_max
|
Optional upper control bound |
None
|
|
seed
|
int | None
|
Optional RNG seed for reproducible sampling. |
None
|
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. The rollout loop runs on this backend, so a TorchBackend executes batched tensor ops over the sample dimension. |
None
|
Source code in src/shinro/controllers/mppi.py
host_input_shapes
¶
Free C-ABI input ports the host fills each tick.
Sampling is host-side (see the module docstring), so in a lowered graph
epsilon — the (N, K*D_u) sampled perturbations, sample-major —
is a free input port rather than something the kernel generates. The
compile pipeline (shinro.codegen.build.build_composed_graph)
reads this to declare the port and pass it through
compose(host_inputs=...); the eager path ignores it, because
compute samples internally.
Returns:
| Type | Description |
|---|---|
dict[str, tuple[int, ...]]
|
Maps the port name to its shape. |
Source code in src/shinro/controllers/mppi.py
attach_plant
¶
Wire a plant into the controller via a batched dynamics adapter.
Builds a BatchedDynamicsAdapter from the plant and sets
dynamics_fn / cost_fn from it. The cost uses the quadratic
stage cost \(x^T Q x + u^T R u\), optionally tracking the reference
passed to compute — that reference is threaded through
self._x_ref_holder, a plain list, so the tracer does not mistake it
for recurrent state. Q and R may be diagonal (D,) or full
(D, D) matrices; they default to identity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plant
|
A |
required | |
Q
|
Any | None
|
State cost matrix (D_x, D_x) or diagonal (D_x,). |
None
|
R
|
Any | None
|
Control cost matrix (D_u, D_u) or diagonal (D_u,). |
None
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If the plant's control dimension disagrees with the
controller's |
Source code in src/shinro/controllers/mppi.py
compute
¶
Compute the MPPI control action for a given initial state.
Draws \(N\) Gaussian perturbation sequences (host-side — see the module docstring), rolls out the dynamics over the horizon, computes the softmax-weighted update, and returns the first action of the updated nominal sequence (clipped to bounds if configured).
Everything after sampling runs through self.bk on batched tensors,
so it evaluates identically on numpy, torch, and the tracing backend.
Every reduction is written as a matmul identity — a sum over an axis is
a contraction with a ones vector or with the softmax weights — except
the softmax shift beta = min(costs), which is the min graph op.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current_state
|
Initial state vector (D_x,). Accepts the backend's native array type (numpy or torch). |
required | |
target_state
|
Any | None
|
Optional reference state (D_x,) to track. When given,
the cost penalizes deviation |
None
|
epsilon
|
Any | None
|
Optional pre-drawn perturbations, shape |
None
|
Returns:
| Type | Description |
|---|---|
|
First control action (D_u,) in the backend's native type. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If |
Source code in src/shinro/controllers/mppi.py
260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 | |
reset
¶
Reset the controller to its initial state.
Zeros the nominal control sequence and clears the last-sample bookkeeping attributes.
Source code in src/shinro/controllers/mppi.py
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create an MPPI controller from a TOML config dict or MPPIConfig.
Config fields
num_samples: Number of sampled perturbations N. temperature: Softmax temperature. horizon: Prediction horizon K. noise_sigma: Per-channel perturbation std dev (D_u,). dt: Time step. Required at runtime; injected from the plant in scenario builds. u_min: Optional lower bound list (D_u,). u_max: Optional upper bound list (D_u,). seed: Optional RNG seed. state_cost: Optional diagonal Q weights (D_x,) or full Q matrix. control_cost: Optional diagonal R weights (D_u,) or full R matrix.
The dynamics_fn and cost_fn callables cannot be serialized to
TOML. They are created as None and must be injected after
construction, either by setting the attributes directly or by calling
attach_plant(plant):
.. code-block:: python
ctrl = MPPIController.from_config(config)
ctrl.dynamics_fn = my_dynamics
ctrl.cost_fn = my_cost
# or:
ctrl.attach_plant(plant)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or MPPIConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
MPPIController instance. |
Source code in src/shinro/controllers/mppi.py
LeRobotDiffusionAdapter¶
Bases: Controller
Wrap a LeRobot diffusion policy as a Controller.
The policy is loaded from a Hugging Face checkpoint or local path.
The compute() method converts the plant state into LeRobot's
observation dict format, runs the policy, and returns the action.
Accepts both numpy arrays and torch tensors for state and camera frames, so it works with any ArrayBackend.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
policy
|
Loaded LeRobot policy instance. |
required | |
use_camera
|
bool
|
Whether to expect camera observations. |
False
|
device
|
str
|
Torch device for policy inference. |
'cpu'
|
Source code in src/shinro/controllers/lerobot_adapter.py
update_camera
¶
Feed a camera frame for the next policy step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame
|
Camera image as numpy array (HWC) or torch tensor (CHW or HWC). |
required |
compute
¶
Run the LeRobot policy on the current state.
Builds a LeRobot observation dict from the state vector, runs the policy, and returns the action as a numpy array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
Plant state vector (n_x,). Can be numpy or torch tensor. |
required | |
target
|
Ignored for learned policies — they generate actions from observation alone. |
None
|
Returns:
| Type | Description |
|---|---|
|
Action vector (n_u,) as numpy array. |
Source code in src/shinro/controllers/lerobot_adapter.py
reset
¶
from_config
classmethod
¶
Load a LeRobot policy from a config dict.
Config fields
policy_type: Type of policy ("diffusion", "act", "pi0", etc.).
checkpoint: Hugging Face repo ID or local path.
use_camera: Whether to expect camera observations (default: false).
device: Torch device (default: "cpu").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict. |
required |
Returns:
| Type | Description |
|---|---|
|
LeRobotDiffusionAdapter instance. |
Source code in src/shinro/controllers/lerobot_adapter.py
OnnxRLAdapter¶
Bases: Controller
Run an ONNX-exported RL policy as a Controller.
The policy is prepared once (imported + interpreted, or a compiled .so
is loaded) and each compute call runs one tick on the plant state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
policy
|
_GraphPolicy | _CompiledPolicy
|
A loaded policy artifact — |
required |
seed
|
int
|
RNG seed for action sampling. Sampling action spaces (discrete non-deterministic, stochastic non-deterministic) draw their noise from a generator seeded here. |
0
|
backend
|
ArrayBackend | None
|
Array backend for the state input and action output. The kernel itself always works on numpy f64; the adapter converts at the boundary, so a torch state yields a torch action. |
None
|
Source code in src/shinro/controllers/onnx_rl_adapter.py
compute
¶
Run the policy on the current plant state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
Plant state vector in the configured backend's native type
(numpy array, torch tensor, or a sequence). Must have the
compiled graph's |
required | |
target
|
Ignored — learned policies act on the observation alone. |
None
|
Returns:
| Type | Description |
|---|---|
|
The action vector (n_u,) in the backend's native type. |
Source code in src/shinro/controllers/onnx_rl_adapter.py
reset
¶
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create an OnnxRLAdapter from a TOML config dict or OnnxRLConfig.
Config fields
model_path: Path to the .onnx model (eager mode).
artifact_dir: A make compile --out directory (compiled mode).
action_space: "continuous", "discrete", or "stochastic"
(default: continuous).
deterministic: Return argmax/mean instead of sampling (default true).
action_scale / action_bias: Post-policy affine transform.
action_clip_low / action_clip_high: Clip the final action; both are
required together (a missing bound would be ±inf, which the
lowerer cannot represent).
seed: RNG seed for sampling action spaces.
n_x: Plant state dimension, when the observation sub-table does not
reach the last state entry.
output_name: ONNX tensor to import as the action.
observation: The importer's observation table (state_keys,
normalize, obs_mean, obs_std, clip, ...).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or |
required | |
backend
|
ArrayBackend | None
|
Array backend for state input and action output. |
None
|
Returns:
| Type | Description |
|---|---|
|
OnnxRLAdapter instance. |
Raises:
| Type | Description |
|---|---|
ValueError
|
On a missing/invalid mode or an invalid action config. |
FileNotFoundError
|
If a compiled artifact is missing. |
Source code in src/shinro/controllers/onnx_rl_adapter.py
SlidingModeController¶
Bases: Controller
Sliding Mode Controller for nonlinear systems.
Implements the equivalent control approach with a switching term and
optional boundary-layer smoothing. The sliding surface coefficients
c must form a Hurwitz polynomial.
One surface per instance: c is a single surface vector, so the class is
instantiated once per controlled axis (a bank of instances is the
multi-axis pattern). Multi-surface SMC — a surface matrix solved jointly —
is intentionally not implemented; see the module docstring's "Scope" note.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
c
|
Sliding surface coefficients (n,). The polynomial
|
required | |
k1
|
float
|
Discontinuous (switching) gain — drives the state to the surface. |
required |
phi
|
float
|
Boundary layer thickness for chattering suppression. If 0, uses sign (pure switching). |
0.0
|
k2
|
float
|
Linear (proportional) gain on the sliding variable. |
0.0
|
smoother
|
str
|
Boundary-layer smoothing function. One of |
'sat'
|
alpha
|
float
|
Fractional power exponent for the switching term \(|s|^\alpha\). 0 gives sign-only; 1 gives linear. |
0.0
|
controllability_eps
|
float
|
Near-zero threshold on the controllability
denominator: \(|c^T g(x)|\) for a scalar input, and its
multi-input generalization \(\|c^T g(x)\|\) (the two agree
when |
1e-12
|
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Source code in src/shinro/controllers/smc.py
compute
¶
Compute the sliding mode control action.
Evaluates \(u = (c^T g)^{-1} ( -c^T f - k_1 |s|^\alpha \, \text{smooth}(s) - k_2 s )\).
For scalar input (c^T g is scalar), uses direct division. For
vector input the equivalent-control equation (c^T g) u = num is
underdetermined (one surface, n_u unknowns), so the law takes its
minimum-norm solution u = cg^T (cg cg^T)^{-1} num — the same
point np.linalg.lstsq returned, but written as matmul/transpose/
divide so it traces and lowers.
The computation is written so it traces as well as it evaluates: the
u/n_u branch keys off g_x's shape (known at trace time),
the dot products use column-vector form (the tracer has no 1D @ 1D
contraction), and the near-zero c^T g guard is an exception only
when g_x is concrete — a graph cannot raise, so the traced path
emits a fail-safe instead (see _scalar_control).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Current state vector (n,). |
required | |
f_x
|
Drift dynamics \(f(x)\) evaluated at x (n,). |
required | |
g_x
|
Control matrix \(g(x)\) evaluated at x (n, n_u). |
required |
Returns:
| Type | Description |
|---|---|
|
Control input vector (n_u,). |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If \(c^T g(x)\) is near-zero and |
Source code in src/shinro/controllers/smc.py
reset
¶
from_config
classmethod
¶
from_config(config, backend: ArrayBackend | None = None)
Create an SMC controller from a TOML config dict or SMCConfig.
Config fields
c: List of sliding surface coefficients (n,).
k1: Discontinuous (switching) gain.
phi: Boundary layer thickness (default 0.0).
k2: Linear gain on sliding variable (default 0.0).
smoother: Smoothing function — "sat", "tanh", or
"sigmoid" (default "sat").
alpha: Fractional power exponent (default 0.0).
controllability_eps: Near-zero \(|c^T g|\) threshold,
plant-scaled (default 1e-12 = arithmetic-only guard); see
SlidingModeController for how to choose it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
config
|
TOML config dict or SMCConfig. |
required | |
backend
|
ArrayBackend | None
|
Array backend. Defaults to NumpyBackend. |
None
|
Returns:
| Type | Description |
|---|---|
|
SlidingModeController instance. |