Core

The System contract: the base class every block, plant and controller extends.

class minilink.core.system.System(n=0)[source]

Static input-output shell: ports, parameters, metadata, and facades.

y = h(x, u, t; p) (via port compute / h()).

A static block is its output maps on its ports. Default n = 0 (no states). State dimension n is the number of state slots this block contributes when stacked in a diagram (zero for static blocks). Continuous evolution dx = f(x, u, t; p) is defined on DynamicSystem only, and a discrete step x_{k+1} = step(x, u, k; p) on StepSystem.

A System serves several roles at once:

  • IO contract: output ports (and optional h()) as functions of (x, u, t, params).

  • Structural model description: dimensions, ports, state metadata, labels, units, bounds, and nominal values.

  • Model defaults and metadata: default parameters (params), default initial condition (x0), and solver hints (solver_info).

  • Visualization contract: forward-kinematic geometry for rendering and animation.

  • User shortcut façade: compile(), compute_trajectory(), render(), animate() on SharedSystemFacades (continuous analysis and game() on DynamicSystemFacades).

  • Inspect: __str__() is a short summary (name, class, n, ports with dimensions; a diagram adds its keys). A notebook last expression still draws the Graphviz block.

  • Names: name is the display name, the class name unless the block sets its own; id is the key the block takes in a diagram built by +, >> or a loop (None: its name in snake case; in @ the role names plant, controller and observer win).

Overridden h() and port compute functions are functions of (x, u, t, params) alone: no memory between calls, no cached side effects. This is the first invariant of CONSTITUTION.md, and it is what lets diagrams nest, solvers step, and JAX trace. Python cannot check it, so the burden sits with whoever writes the equation.

skin = None

a callable (plant) -> dict[str, list[prim]] or None. get_kinematic_geometry delegates to skin; None means no skin (empty geometry). Set per instance/class to swap a look without touching f/tf (skin is to get_kinematic_geometry as params is to f).

Type:

Opt-in swappable look

is_random = False

Whether the block draws random numbers from a seed in its params.

property m

the sum of all input-port dimensions.

Type:

Total input dimension

property p

the dimension of the "y" port, or 0.

Type:

Primary output dimension

h(x, u, t=0, params=None)[source]

Output y = h(x, u, t; p).

Same purity contract as port compute (convention only; not enforced).

Parameters:
  • x (array of shape (n,))

  • u (array of shape (m,))

  • t (float)

  • params (dict, optional)

Returns:

y

Return type:

array of shape (p,)

compute_state(x, u, t=0, params=None)[source]

Output helper returning the state vector directly (y = x).

refresh()[source]

Recompute derived internals from current parameters/topology.

Base implementation is a no-op and can be overridden by subclasses.

realize(key)[source]

The params of one realization of the block’s randomness under key.

A block with no randomness returns its params unchanged. A random block returns a copy with a fresh seed derived from key, or with seed=None, the mean, when key is None. A diagram realizes every subsystem, each under its own stream named by its id, so the result nests like params and can be assigned to it.

add_input_port(id, *, dim=None, nominal_value=None, labels=None, units=None, lower_bound=None, upper_bound=None)[source]

Add a new input port to the system.

Parameters:
  • id (str) – The programmatic identifier for the port.

  • dim (int, optional) – Dimension of the input port. If omitted, inferred from supplied metadata or defaults to 1.

  • nominal_value (np.ndarray, optional) – The nominal value of the signal, used as the constant fallback when the port is unconnected.

add_output_port(id, *, dim=None, function=None, dependencies=(), nominal_value=None, labels=None, units=None, lower_bound=None, upper_bound=None)[source]

Add a new output port to the system.

Parameters:
  • id (str) – The programmatic identifier for the port.

  • dim (int, optional) – Dimension of the output port. If omitted, inferred from supplied metadata or defaults to 1.

  • function (callable, optional) – The function (x, u, t, params) -> value computing the port’s signal.

  • dependencies (sequence of str, or "all", optional) – The input-port ids this output directly feeds through from (default: no direct feedthrough).

rename_ports(**names)[source]

Rename ports at creation: Saturation(-1, 1).rename_ports(u="u_cmd", y="u").

A port name is the block’s outside: wiring, autowire, plot signals, drawings. The equations read the stacked u by position, so a rename changes nothing inside. The renames of one call apply at once (a chain such as u="u_cmd", y="u" is one step), the port order is kept, and the output dependency lists follow. Refused: a name that is not a port, a new name that is a port not renamed in the same call, two equal new names, and a diagram, whose edge is named with expose. Rename a block before placing it in a diagram, whose wiring refers to the names.

Returns the block, for chaining.

get_u_from_input_ports()[source]

Concatenated default values of all input ports.

This is the disconnected / nominal fallback used for standalone systems and for the evaluator IVP tier.

get_port_values_from_u(u, *port_ids)[source]

Extract named port signals from the concatenated input vector u.

  • No port ids: return a dict mapping every input-port id to its value.

  • One port id: return that port’s value.

  • Several port ids: return a tuple in the requested order.

get_input_port_slice(port_id)[source]

Return the slice of one input port inside the flat input vector u.

get_all_input_labels_and_units()[source]

Flattened component labels and units across all input ports.

get_kinematic_geometry()[source]

Static skin as dict[str, list[primitive]] (delegates to skin).

tf(x, u, t=0, params=None)[source]

World transforms as dict[str, 4x4] (the forward-kinematics hook).

Return only named articulation frames (body, link0, …). Do not emit "world": I — the animator injects an implicit identity root for geometry keyed to "world".

get_dynamic_geometry(x, u, t=0, params=None)[source]

Per-frame geometry as dict[str, list[primitive]] (rebuilt each frame).

class minilink.core.system.DynamicSystem(n, *, input_dim=None, output_dim=None, expose_state=False, y_dependencies=())[source]

A system with continuous evolution dx = f(x, u, t; p).

dx = f(x, u, t; p) y = h(x, u, t; p)

Leaf plants use n > 0; DiagramSystem subclasses this type with n = 0 when the stacked subsystem states sum to zero (signal-flow diagrams still simulate via Simulator).

The constructor can create the standard ports in one line: an input u, a primary output y wired to h(), and optionally an auxiliary state output x.

f(x, u, t=0, params=None)[source]

State derivative dx = f(x, u, t; p).

Subclasses should treat this as a function of (x, u, t, params) only; the library does not verify purity.

Parameters:
  • x (array of shape (n,))

  • u (array of shape (m,))

  • t (float)

  • params (dict, optional) – None means “use self.params”.

Returns:

dx

Return type:

array of shape (n,)

h(x, u, t=0, params=None)[source]

Output y = h(x, u, t; p).

Default: the full state, y = x, when the y port has the state dimension (p == n); zeros otherwise. Override for any other measurement.

Parameters:
  • x (array of shape (n,))

  • u (array of shape (m,))

  • t (float)

  • params (dict, optional)

Returns:

y

Return type:

array of shape (p,)

class minilink.core.system.StepSystem(n, *, input_dim=None, output_dim=None, expose_state=False, y_dependencies=())[source]

A system with discrete states (n >= 1)

x_{k+1} = step(x, u, k; p) y_k = h(x, u, k; p)

The constructor can create the standard ports in one line: an input u, a primary output y wired to h(), and optionally an auxiliary state output x.

step(x, u, k=0, params=None)[source]

Discrete state update x_{k+1} = step(x, u, k; p).

Parameters:
  • x (array of shape (n,))

  • u (array of shape (m,))

  • k (int)

  • params (dict, optional) – None means “use self.params”.

Returns:

x_next

Return type:

array of shape (n,)

h(x, u, k=0, params=None)[source]

Output y_k = h(x, u, k; p).

Default: y = x when p == n; zeros otherwise.

Parameters:
  • x (array of shape (n,))

  • u (array of shape (m,))

  • k (int)

  • params (dict, optional)

Returns:

y

Return type:

array of shape (p,)

Block diagrams: systems built by connecting other systems.

class minilink.core.diagram.DiagramSystem[source]

A system composed of subsystems connected through their named ports.

A diagram is a DynamicSystem, so diagrams nest, simulate, and compile like any other continuous system. The diagram state stacks the subsystem states in insertion order,

x = [x_1; x_2; …; x_k], dx_i = f_i(x_i, u_i, t; p_i),

where each local input u_i is gathered from the output ports connected to subsystem i, from the diagram boundary inputs, or from the port’s constant nominal value when unconnected. Diagram-level params are nested by subsystem id: {"plant": {...}, "controller": {...}}. @ names its blocks controller and plant; + and >> keep the blocks’ own names.

Build diagrams with add_subsystem() + connect() (canonical wiring for any topology) or with the shortcuts +, >>, @, and autowire() from minilink.core.composition.

This class implements the wiring contract and a simple recursive evaluation of f used as the reference; compile() produces the fast ExecutionPlan-based evaluator used by simulation and optimization.

add_subsystem(sys, sys_id)[source]

Add a continuous or static subsystem under a unique id.

A StepSystem has no f to stack, so it is refused here rather than left out of the state derivative.

f(x, u, t=0, params=None)[source]

Stacked state derivative dx = [f_1(x_1, u_1, t); f_2(x_2, u_2, t); ...].

This is the interpreted reference implementation; compile() produces the fast equivalent used by simulation and optimization.

compile(backend='numpy', bind_params=False, verbose=False)[source]

Compile the diagram into a stateless evaluator for fast simulation.

Runs algebraic-loop detection internally; raises RuntimeError if a loop is found.

Parameters:
  • backend (str) – 'numpy' (default) or 'jax'.

  • bind_params (bool, optional) – If True, subsystem params are deep-copied into the plan at compile time (see minilink.core.compile.compiler.compile_diagram()).

  • verbose (bool) – If True, print timed compilation steps.

Return type:

NumpyDiagramEvaluator or JaxDiagramEvaluator

reconstruct_internal_signals(traj)[source]

Reconstruct all subsystem output-port trajectories for this diagram.

Parameters:

traj (Trajectory) – State-input trajectory sampled on a time grid.

Returns:

New trajectory enriched with one sampled signal per subsystem output port, keyed as "sys_id:port_id".

Return type:

Trajectory

trajectory_of(subsystem, traj=None)[source]

The trajectory of one block of this diagram, as that block saw it.

x is the block’s own state and u the input it received from the diagram (connected signals, boundary inputs, or nominal values), on the time grid of the diagram trajectory. The input is recomputed from the stored states with the blocks’ current params, so change them only after scoring the trajectory they produced. Anything written for the block alone applies to it, such as the cost of the plant inside a closed loop: cost.total_cost(closed_loop.trajectory_of(plant)).

Parameters:
  • subsystem (System) – A block added to this diagram (for a block inside a nested diagram, call trajectory_of once per level).

  • traj (Trajectory, optional) – Trajectory of this diagram; defaults to the last one computed.

Returns:

t of traj, x with shape (subsystem.n, N) and u with shape (subsystem.m, N).

Return type:

Trajectory

class minilink.core.diagram.StepDiagramSystem[source]

A discrete-time diagram composed of StepSystem and static blocks.

The diagram state stacks subsystem states in insertion order,

x_{k+1} = step(x_k, u_k, k),

where each local input is gathered from connected output ports, boundary inputs, or port nominal values. The third gather slot is step index k (int), not simulation time.

compile() produces a StepEvaluator for fast compute_rollout().

add_subsystem(sys, sys_id)[source]

Add a step or static subsystem under a unique id.

A DynamicSystem with states has no step to stack, so it is refused here rather than left frozen by step().

step(x, u, k=0, params=None)[source]

Stacked discrete update x_{k+1} = [step_1(...); step_2(...); ...].

Interpreted reference; compile() produces the fast equivalent.

compile(backend='numpy', bind_params=False, verbose=False)[source]

Compile the step diagram into a stateless step evaluator.

Parameters:
  • backend (str) – 'numpy' (default) or 'jax'.

  • bind_params (bool, optional) – If True, subsystem params are deep-copied into the plan.

  • verbose (bool) – If True, print timed compilation steps.

Return type:

NumpyStepDiagramEvaluator or JaxStepDiagramEvaluator

minilink.core.diagram.check_diagram_params(params, subsystem_ids)[source]

Check the nested diagram params contract.

params must be None or a mapping keyed by subsystem id whose values are per-subsystem params dicts. Missing subsystem ids are allowed (those subsystems fall back to their live self.params); unknown ids raise so sys-id typos fail loudly instead of silently using defaults.

Called once at public entry points (DiagramSystem.f, tf, get_dynamic_geometry, the evaluators’ parametric tier, the params setter), not inside the recursion.

Parameters:
  • params (None or Mapping) – Candidate diagram-level params, e.g. {"plant": {"m": 1.0}}.

  • subsystem_ids (container of str) – Valid subsystem ids (e.g. diagram.subsystems).

Composition shortcuts: the +, >> and @ operators that build a diagram in one line.

minilink.core.composition.add_systems(*systems)[source]

Return a new flat diagram containing systems as subsystems.

Parameters:

*systems (System or DiagramSystem) – Systems or diagrams to add, side by side. A diagram operand is laid out flat in the result, with its ids, wiring and boundary ports; it is not modified. The first operand’s name, x0 and camera settings are carried into the result when it is a diagram.

Returns:

A diagram containing all provided systems as direct subsystems. Existing diagram-internal connections are preserved, but no new cross-connections are inferred.

Return type:

DiagramSystem

minilink.core.composition.series(left, right, *rest)[source]

Connect systems in series: one output of each stage to one input of the next.

A >> B wires one output of A to one input of B, the first rule that applies: A has one output and B one input; exactly one output of A and one input of B share a name; A has y and B has u; else it refuses. A diagram counts its edge (its boundary outputs and its free boundary inputs), a leaf its ports. Sizes are a check, never a choice. One guard: a source into a block that reads both r and y is refused, since the name match would feed the measurement. The result keeps A’s free inputs as its inputs and B’s y (else its only output) as its output y.

Parameters:
  • left (System or DiagramSystem) – The first system or diagram; a diagram is laid out flat in the result and left unchanged, its name, x0 and camera settings carried.

  • right (System or DiagramSystem) – System or diagram to connect after left.

  • *rest (System or DiagramSystem) – Additional systems or diagrams to append to the series chain.

Returns:

A new diagram with each stage connected output-to-input.

Return type:

DiagramSystem

minilink.core.composition.feedback(sys, filter=1, *, of=None, sign=-1.0, validate=True)[source]

Close sys on itself with an Error block: r -> e = r - y -> sys -> y, the textbook T = L / (1 + L H).

Negative feedback (the default) inserts Error, so the diagram shows ports +, -, and e. Positive feedback still uses a signed Sum. The junction drives the command input of sys (command_port(): u, else its one free input) — a plant, a transfer function, a compensator, or a series diagram such as C >> G; a block that reads its own measurement (y or x) is closed on its plant with @ instead. What comes back is of, else y, else the only output. sys is not modified: the result is a new diagram sharing its blocks, with boundary input r and that output as boundary output y. @ always means controller at plant; this is the one way to close an open loop on itself.

Parameters:
  • filter (1, scalar, array, or System) – The return path H: 1 is a direct wire, a scalar or matrix inserts a Gain block, a system (a sensor, a low-pass) is inserted as a block; the block is named filter either way.

  • of (str or (str, int), optional) – Which output comes back: a boundary output port id, or (port, index) for one component. Default: the whole output when its dimension matches the input; a scalar input against a vector output takes component 0 through a visible Demux; any other mismatch is an error.

  • sign (float) – -1.0 for negative feedback (default), +1.0 for positive.

  • validate (bool) – Run algebraic-loop detection before returning.

Return type:

DiagramSystem

minilink.core.composition.closed_loop(controller, plant, *, reference=True, disturbance=False, noise=False, filter=None, observer=None, validate=True)[source]

Build a controller/plant feedback diagram — the controller @ plant operator.

One rule, read from the port names (block_roles()): every controller input named y or x is wired to the plant output of the same name; a controller with neither (an error-driven block: a compensator, a transfer function, a plant, a series diagram) gets an Error block e = r - y on its command entry, the junction of feedback(). The controller’s command (u, else its one output) drives the plant’s command input, and the plant’s y (else its one output) is the loop’s output y.

The port names are the whole declaration: there is no port-name keyword. A mechanical plant’s y is its state [q; dq], read straight; a block with other names is renamed (rename_ports) or wired by hand. A controller that reads x needs a plant that exposes it: a leaf built with expose_state=True, or a box, whose own state is read.

A box (a diagram) on either side is nested under its role name as one block; a leaf stays a leaf. @ needs a plant on the right: feedback() closes an open loop on itself (feedback(L), feedback(L, K)).

Neither operand is modified: the result is a new diagram sharing their blocks; a plant box whose state is read is copied, with its blocks shared.

Parameters:
  • controller (System) – Controller subsystem with reference, measurement, and control ports.

  • plant (System) – Plant subsystem with control input and measurement output ports.

  • reference (bool or System, optional) – The loop’s inputs, on the ports r, d and n: the reference (default on), the load disturbance and the measurement noise (both off by default). True puts the port on the loop’s edge, False leaves it out (the reference then reads its nominal value), and a source block (Step(), WhiteNoise(...)) drives it inside the loop, named reference, disturbance or noise. A plant that declares d or n owns it: the input drives the plant’s own port. Otherwise a Sum adds the disturbance to the plant’s command (disturbance_sum) and the noise to the measurement before it returns (noise_sum).

  • disturbance (bool or System, optional) – The loop’s inputs, on the ports r, d and n: the reference (default on), the load disturbance and the measurement noise (both off by default). True puts the port on the loop’s edge, False leaves it out (the reference then reads its nominal value), and a source block (Step(), WhiteNoise(...)) drives it inside the loop, named reference, disturbance or noise. A plant that declares d or n owns it: the input drives the plant’s own port. Otherwise a Sum adds the disturbance to the plant’s command (disturbance_sum) and the noise to the measurement before it returns (noise_sum).

  • noise (bool or System, optional) – The loop’s inputs, on the ports r, d and n: the reference (default on), the load disturbance and the measurement noise (both off by default). True puts the port on the loop’s edge, False leaves it out (the reference then reads its nominal value), and a source block (Step(), WhiteNoise(...)) drives it inside the loop, named reference, disturbance or noise. A plant that declares d or n owns it: the input drives the plant’s own port. Otherwise a Sum adds the disturbance to the plant’s command (disturbance_sum) and the noise to the measurement before it returns (noise_sum).

  • filter (System, scalar or array, optional) – The return path: a sensor or filter block, or a gain (a Gain block), named filter, between the plant’s y and what reads the measurement (the controller’s y, the Error block, or the observer). The plant’s y stays the loop’s output and the noise n enters before the filter. A controller that reads x without an observer is refused: a filter on the state is an observer.

  • observer (System, optional) – The state estimator, named observer, for a controller that reads x: the plant’s y and the controller’s u go in, and its x (the estimate) goes to the controller’s x, so the plant’s own state is never read. A kalman or luenberger observer with a state-feedback gain is the LQG loop.

  • validate (bool, optional) – If True, run algebraic-loop detection before returning.

Returns:

A diagram with the plant’s y as output and the inputs left on its edge (r, d, n) as inputs.

Return type:

DiagramSystem

minilink.core.composition.autowire(diagram, *, strict=False, validate=True)[source]

Connect unconnected inputs by name: each takes the one output with its name and size.

Parameters:
  • diagram (DiagramSystem) – Diagram to modify in place.

  • strict (bool, optional) – If True, raise when an input stays open (no match, or several). No new connections are applied before that error is raised. If False, the open inputs are left unconnected and a warning lists them.

  • validate (bool, optional) – If True, run algebraic-loop detection after wiring.

Returns:

The same diagram, for fluent use.

Return type:

DiagramSystem

Notes

Names only, no guessing: an output may feed several inputs, existing connections are never overwritten, and a disturbance d or a noise n left open reads its nominal value without a word. Name a source’s signal after the input it drives (Step(port="r")) and a block’s ports at creation (rename_ports).

minilink.core.composition.resolve_standard_feedback(controller, plant, *, controller_id='controller', plant_id='plant')[source]

Choose the feedback port names shared by closed_loop and the sampled loop.

The controller’s port names decide (minilink.core.feedback.block_roles()): a block that reads y closes on the plant’s y, one that reads x on its x, and one that reads both on the first the plant can serve. The command leaves on u (u_ff for a solver, else the one output) and enters the plant on u (else its one input). Dimensions are a check, never a choice: a mechanical plant’s y is its state [q; dq].

Parameters:
  • controller_id (str)

  • plant_id (str)

Return type:

StandardFeedbackWiring

class minilink.core.composition.StandardFeedbackWiring(control_out, measurement_in, plant_in, plant_out)[source]

Resolved port names for controller/computer ↔ plant feedback.

Parameters:
  • control_out (str)

  • measurement_in (str)

  • plant_in (str)

  • plant_out (str)

minilink.core.composition.default_computer_boundary_ports(controller)[source]

Return (measurement_in, control_out) for step-diagram boundary expose.

Return type:

tuple[str, str]

Trajectory: sampled state and input on a shared time grid.

class minilink.core.trajectory.SampledSignals[source]

The named channels of a sampled record: x, u and the extra signals.

property n: int

State dimension.

property m: int

Input dimension.

property signal_names: tuple[str, ...]

All available sampled channel names.

has_signal(name)[source]

Return True when a named signal is available.

Parameters:

name (str)

Return type:

bool

get_signal(name)[source]

Return a sampled signal by name.

Parameters:

name (str)

Return type:

ndarray

class minilink.core.trajectory.Trajectory(t, x, u, signals=<factory>)[source]

Sampled state-input trajectory on a shared time grid.

The canonical sampled record of simulation, planning, control, and rendering workflows.

Parameters:
  • t (np.ndarray) – Time samples with shape (N,).

  • x (np.ndarray) – State samples with shape (n, N).

  • u (np.ndarray) – Input samples with shape (m, N).

  • signals (dict of str to np.ndarray, optional) – Additional sampled signals, each with shape (dim, N).

Notes

The trajectory semantic core is (t, x, u). Extra signals such as outputs, derivatives, or reconstructed internal ports can be attached through with_signal() or with_signals().

property n_samples: int

Number of time samples.

property t0: float

Initial time.

property tf: float

Final time.

property time_duration: float

Total covered time span tf - t0.

with_signal(name, values)[source]

Return a new trajectory with one additional sampled signal.

Parameters:
  • name (str) – Signal name. Cannot be "x" or "u".

  • values (np.ndarray) – Signal samples with shape (dim, N).

Return type:

Trajectory

with_signals(values)[source]

Return a new trajectory with multiple additional sampled signals.

Parameters:

values (dict[str, ndarray])

Return type:

Trajectory

sample(name, t)[source]

Sample a signal by nearest time index.

Parameters:
  • name (str)

  • t (float)

Return type:

ndarray

t2x(t)[source]

Return the state sample nearest to time t.

Parameters:

t (float)

Return type:

ndarray

t2u(t)[source]

Return the input sample nearest to time t.

Parameters:

t (float)

Return type:

ndarray

copy()[source]

Return a deep copy of the trajectory.

Return type:

Trajectory

save(path)[source]

Save the sampled trajectory to an .npz file.

Return type:

None

classmethod load(path)[source]

Load a trajectory saved with save().

Return type:

Trajectory

resample(*, t_new=None, n_samples=None)[source]

Return a new trajectory interpolated on a new time grid.

Parameters:
  • t_new (np.ndarray, optional) – New 1-D time vector.

  • n_samples (int, optional) – Number of uniformly spaced samples between t0 and tf.

Return type:

Trajectory

minilink.core.trajectory.interpolate_schedule(xp, t_samples, values, t)[source]

Value of a sampled schedule at time t: linear between samples, held outside.

t_samples holds N nondecreasing sample times and values the samples, shape (N, ...); the result has the shape of one sample. At a repeated time the later sample holds, and a single sample holds at every t. xp is the array module of the caller (NumPy or JAX).

Allowable sets: x(t) in X(t), u(t) in U(x, t) and boundary sets x(tf) in Xf.

class minilink.core.sets.Set[source]

Base class for deterministic vector-valued allowable sets.

A point is feasible when all components of margin() are nonnegative. The set may depend on time and optional parameters, so the generic notation is z in Z(t). margin() is a native-array equation path (NumPy in, NumPy out; JAX in, JAX out); contains() is a NumPy boundary utility, and sample() follows the draw convention of Distribution.

abstract margin(z, t=0.0, params=None)[source]

Return nonnegative feasibility margins for z, shape (k,).

contains(z, t=0.0, params=None)[source]

Return True when z belongs to the set.

Parameters:
  • z (ndarray)

  • t (float)

Return type:

bool

sample(key=None, n=None, params=None)[source]

Draw one point (dim,) or n points (n, dim) from the set when supported.

key is a NumPy generator, an integer seed, None (unseeded) or a JAX PRNG key (JAX arrays out, traceable): the draw convention of Distribution. Search planners use this optional method on samplable sets such as boxes; other sets leave it unimplemented and are sampled by rejection from a box.

bounding_box()[source]

The tightest BoxSet containing the set, or None when it has none.

class minilink.core.sets.InputSet[source]

Base class for admissible input sets u in U(x, t).

Input feasibility often depends on the current state, for example actuator envelopes or contact-dependent controls. The state argument is optional so constant input boxes stay simple. margin() and contains() follow the conventions of Set.

abstract margin(u, x=None, t=0.0, params=None)[source]

Return nonnegative feasibility margins for u, shape (k,).

contains(u, x=None, t=0.0, params=None)[source]

Return True when u is admissible at (x, t).

Parameters:
  • u (ndarray)

  • x (ndarray | None)

  • t (float)

Return type:

bool

sample(key=None, n=None, x=None, t=0.0, params=None)[source]

Draw one input (m,) or n inputs (n, m) when supported (same convention as Set.sample()).

bounding_box()[source]

The tightest BoxSet of inputs containing the set, or None when it has none.

class minilink.core.sets.BoxSet(lower, upper)[source]

Axis-aligned box lower <= z <= upper.

Parameters:
  • lower (array_like) – Lower and upper bounds with shape (n,).

  • upper (array_like) – Lower and upper bounds with shape (n,).

property dim: int

Dimension of the boxed vector.

classmethod from_system_state(sys)[source]

The state box of sys: its state bounds (sys.state.box).

Return type:

BoxSet

margin(z, t=0.0, params=None)[source]

Return the margins to the lower and upper faces, shape (2n,).

sample(key=None, n=None, params=None)[source]

Draw uniformly from a finite box: (dim,) for one point, (n, dim) with n.

bounding_box()[source]

A box is its own bounding box.

class minilink.core.sets.BoxInputSet(box)[source]

State-independent input box lower <= u <= upper.

Parameters:

box (BoxSet)

classmethod from_bounds(lower, upper)[source]

Create an input box from lower and upper arrays.

Parameters:
  • lower (ndarray)

  • upper (ndarray)

Return type:

BoxInputSet

classmethod from_system_inputs(sys)[source]

The input box of sys: its input ports’ bounds, stacked in port order.

Return type:

BoxInputSet

margin(u, x=None, t=0.0, params=None)[source]

Return the margins to the input box faces, shape (2m,).

sample(key=None, n=None, x=None, t=0.0, params=None)[source]

Draw uniformly from the input box: (m,) for one input, (n, m) with n.

bounding_box()[source]

The input box itself.

class minilink.core.sets.SingletonSet(point)[source]

Singleton boundary set z == point.

The margin uses a zero-tolerance absolute residual so exact equality can still be checked through contains(). Optimizer equality constraints should use residual() directly.

Parameters:

point (ndarray)

property dim: int

Dimension of the singleton point.

margin(z, t=0.0, params=None)[source]

Return zero only when z equals the singleton point.

residual(z)[source]

Return the equality residual z - point, shape (n,).

bounding_box()[source]

The degenerate box point <= z <= point.

class minilink.core.sets.BallSet(center, radius)[source]

Euclidean ball ||z - center|| <= radius.

Parameters:
  • center (ndarray)

  • radius (float)

property dim: int

Dimension of the ball center.

margin(z, t=0.0, params=None)[source]

Return the signed distance to the ball boundary, shape (1,).

bounding_box()[source]

The box of half-width radius about the center.

class minilink.core.sets.CallableSet(margin_fn=None, contains_fn=None)[source]

Set backed by user callables.

Parameters:
  • margin_fn (callable, optional) – Function margin_fn(z, t, params) -> array.

  • contains_fn (callable, optional) – Function contains_fn(z, t, params) -> bool.

margin(z, t=0.0, params=None)[source]

Evaluate the user-supplied margin function.

contains(z, t=0.0, params=None)[source]

Evaluate membership through the supplied callable when present, on a flat z.

Parameters:
  • z (ndarray)

  • t (float)

Return type:

bool

class minilink.core.sets.IntersectionSet(sets)[source]

Intersection of several sets, z in Z_1 and z in Z_2 and ....

Built by the & operator on Set; a & b & c is one flat intersection of three members.

Parameters:

sets (tuple)

classmethod of(*sets)[source]

Build an intersection, flattening nested intersections into one member list.

Parameters:

sets (Set)

Return type:

IntersectionSet

margin(z, t=0.0, params=None)[source]

Concatenate the margins of all member sets (a scalar margin counts as one).

bounding_box()[source]

The intersection of the members’ boxes; None when no member has one.

Members whose boxes do not intersect make the set empty, which is refused.

minilink.core.sets.is_finite_box(box)[source]

True for a box with finite bounds on every axis.

Return type:

bool

Cost functions: the running cost g(x, u, t) and the terminal cost h(x, t) of J.

class minilink.core.costs.CostFunction[source]

Mother class for deterministic planning cost functions.

Subclasses define a running cost g(x, u, t) and a terminal cost h(x, t) of the objective J = ∫ exp(-rho t) g dt + h(x(tf), tf). The horizon is the planning problem’s: a finite tf ends the integral and charges h there, an infinite one never does. Leaving the allowed set is priced by the problem (infeasible_cost), not by the cost.

g and h are native-array equation paths: NumPy in, NumPy out; JAX in, JAX out. Reporting helpers such as total_cost() convert to Python floats at the boundary. A cost lives in minilink.core, not on a System, so one model serves many planning problems.

Class attribute (override it in the body of a subclass):

  • discount_rate: continuous rate rho >= 0; 0 is undiscounted. Planners convert it with discount_factor() (value iteration’s alpha and reinforcement learning’s gamma are both exp(-rho dt)). The library costs are frozen dataclasses, so assigning it on one of their instances raises. weight * cost keeps the cost’s rate and a + b requires equal rates. No constructor field or params entry sets the rate; a subclass does.

abstract g(x, u, t=0.0, params=None)[source]

Return the native scalar running cost density g(x, u, t).

abstract h(x, t=0.0, params=None)[source]

Return the native scalar terminal cost h(x, t).

discount_factor(dt)[source]

Per-step factor exp(-rho dt) for a planner with time step dt.

Return type:

float

evaluate_trajectory(traj, params=None)[source]

Return traj with sampled running and cumulative costs.

The added signals are "cost_rate" and "cost" with shape (1, N). cost_rate is the discounted integrand exp(-rho t) g(x, u, t); the cumulative integral uses the trapezoidal rule and excludes the terminal cost; callers can add h(x(tf), tf) when they need the full objective scalar.

Parameters:

traj (Trajectory)

Return type:

Trajectory

terminal_cost(traj, params=None)[source]

Return h evaluated at the final state of traj.

Parameters:

traj (Trajectory)

Return type:

float

total_cost(traj, params=None)[source]

Return the trapezoidal running cost plus terminal cost.

Parameters:

traj (Trajectory)

Return type:

float

class minilink.core.costs.QuadraticCost(Q, R, S, xbar, ubar)[source]

Quadratic running and terminal cost.

The running cost is (x - xbar).T @ Q @ (x - xbar) + (u - ubar).T @ R @ (u - ubar). The terminal cost is (x - xbar).T @ S @ (x - xbar).

Parameters:
  • Q (ndarray)

  • R (ndarray)

  • S (ndarray)

  • xbar (ndarray)

  • ubar (ndarray)

classmethod from_system(sys, *, Q=None, R=None, S=None, xbar=None, ubar=None)[source]

Create a default quadratic cost from a Minilink system.

Parameters not provided default to identity running weights, zero terminal weight, and the system’s operating point as the target: its nominal state and nominal input.

Parameters:
  • Q (ndarray | None)

  • R (ndarray | None)

  • S (ndarray | None)

  • xbar (ndarray | None)

  • ubar (ndarray | None)

g(x, u, t=0.0, params=None)[source]

Return the quadratic running cost.

h(x, t=0.0, params=None)[source]

Return the quadratic terminal cost.

class minilink.core.costs.TimeCost(xbar, eps=0.001)[source]

Minimum-time cost J = T.

The running cost is g = 1 everywhere except within eps of the target xbar, where it is 0 (the absorbing goal); the terminal cost is 0. Under dynamic programming the cost-to-go becomes the time-to-go and the optimal policy is bang-bang.

Parameters:
  • xbar (ndarray)

  • eps (float)

classmethod from_system(sys, *, xbar=None, eps=0.001)[source]

Create a time cost targeting xbar (default the system’s nominal state).

Parameters:
  • xbar (ndarray | None)

  • eps (float)

g(x, u, t=0.0, params=None)[source]

Return unit running cost away from the target, zero on it.

h(x, t=0.0, params=None)[source]

Return zero terminal cost.

class minilink.core.costs.SumCost(terms)[source]

Additive cost J = sum_i J_i over several cost functions.

Built by the + operator on CostFunction; use it to add an obstacle or traversability term to a base objective. The terms share one discount_rate; terms whose rates differ raise ValueError.

Parameters:

terms (tuple)

property discount_rate: float

The terms’ common rate rho (0 for an empty sum).

classmethod of(*costs)[source]

Build a sum cost, flattening nested sums into a single term list.

Parameters:

costs (CostFunction)

Return type:

SumCost

g(x, u, t=0.0, params=None)[source]

Return the summed running cost.

h(x, t=0.0, params=None)[source]

Return the summed terminal cost.

class minilink.core.costs.ScaledCost(cost, weight)[source]

Cost scaled by a weight, J = weight * J0.

Built by the * operator on CostFunction, so a weighted sum reads as base + weight * obstacle_cost. The weight leaves the discount_rate of J0 unchanged.

Parameters:
property discount_rate: float

The scaled cost’s rate rho.

g(x, u, t=0.0, params=None)[source]

Return the weighted running cost.

h(x, t=0.0, params=None)[source]

Return the weighted terminal cost.

Who is who in a loop: the roles the port names and the reserved subsystem names declare.

minilink.core.feedback.RESERVED_IDS = ('plant', 'controller', 'observer')

The three subsystem names that say what role a block plays in a diagram. One of each per diagram level; a second one is refused, never suffixed.

class minilink.core.feedback.BlockRoles(measurements, reference, command, entry, stateful)[source]

What a block reads and commands, read from its port names alone.

A controller is an ordinary System (a DynamicSystem when it has state): its roles change nothing in how it computes. @, feedback, the sampled loop, plot_control_law and the evaluators read them; a block with other port names is wired by hand.

Parameters:
  • measurements (tuple)

  • reference (str | None)

  • command (str | None)

  • entry (str | None)

  • stateful (bool)

measurements

The plant signals the block reads, "y" and "x" among its inputs, in the order a loop tries them. Empty for an error-driven block or a source.

Type:

tuple of str

reference

"r" when the block has a reference input.

Type:

str or None

command

"u" when the block commands one, else None.

Type:

str or None

entry

The input an Error block e = r - y drives when the block reads no measurement itself: "e", or the one input of a plain compensator; a disturbance d or a noise n input is never the entry.

Type:

str or None

stateful

Whether the block has internal state (n > 0).

Type:

bool

property measurement

The first measurement the block reads, or None.

property error_driven

Whether an Error block drives the block’s entry.

minilink.core.feedback.block_roles(block)[source]

What block reads and commands, from its port names (see BlockRoles).

Return type:

BlockRoles

minilink.core.feedback.command_port(sys)[source]

The input a decision drives on sys: u, else its one input that is not d or n.

The design tools (LQR, pole placement, value iteration, trajectory optimization, MPC), the cost and the Gym bridge decide this port only; a disturbance d and a measurement noise n are never decision variables, and a cascade’s outer loop drives the inner loop’s one free input.

Return type:

str

minilink.core.feedback.require_command(block)[source]

The command output of block (u, else u_ff), or None to fall back on its default output.

A block that reads a measurement (y or x) and gives the state x back, with no u and no y to command through, is an observer: it is refused as a controller, with the recipe that makes it one. A compensator (no measurement) or a box that commands through an output named y (a gain in series with a saturation) is left to its default output.

Return type:

str | None

class minilink.core.feedback.Roles(plant, controller, observer)[source]

The plant, the controller and the observer of a diagram, or None each.

Parameters:
  • plant (object | None)

  • controller (object | None)

  • observer (object | None)

minilink.core.feedback.roles(system)[source]

The plant, the controller and the observer of system, from the reserved names.

A leaf is its own plant. A diagram answers from its top-level keys plant, controller and observer (the observer may also sit inside the controller); a sampled loop from its plant and computer sides. A missing name is None: a tool that needs that block raises, and the plots and the camera keep their plain defaults.

Return type:

Roles

minilink.core.feedback.check_reserved_id(sys_id, existing)[source]

Refuse a second block under a reserved name: one of each per diagram level.

Return type:

None

minilink.core.feedback.plot_space_of(block)[source]

The sweep space of block’s law for plot_control_law, or None.

An explicit plot_space attribute wins, the one hint the ports cannot give (None says the block has no static law to draw, a solver call such as MPC); else the ports decide: a block that reads x sweeps the state, one that reads y and a reference of the same size the error e = r - y with r pinned, one that reads y otherwise the measurement, and an error-driven block its entry port.

class minilink.core.feedback.ErrorDriven[source]

Port-layout switch for laws written on the tracking error e = r - y.

ports="error" declares one input e — the compensator form, where block @ plant inserts the Error block and block >> plant is the loop gain. ports="reference" declares r and y — the controller form, where @ wires them to the plant. The law reads e = self.error(u) either way, so it is written once.

add_error_ports(ports, dim, *, nominal_value=None)[source]

Declare the input ports of the chosen layout, and return the layout.

The constructor assigns the layout it returns to self.port_layout.

property error_dependencies

Input ports the command feeds through from.

error(u)[source]

The tracking error from the flat input: u itself, or r - y.

Probability distributions: x ~ p with sample(key) on NumPy or JAX, mean() and an optional support.

class minilink.core.distributions.Distribution[source]

Vector-valued distribution: dim, mean (an array), sample(key, n=None), optional support.

A duck type, not a probability library: anything with dim, sample(key) and mean works. key is a NumPy generator, an integer seed, or a JAX PRNG key (JAX arrays out, traceable under jit / vmap / scan).

abstract sample(key, n=None)[source]

Draw one sample (shape (dim,)) or n samples ((n, dim)).

key is a numpy.random.Generator, an integer seed, or a JAX PRNG key (JAX arrays out, traceable).

class minilink.core.distributions.Gaussian(mean, std)[source]

Diagonal Gaussian x ~ N(mean, diag(std**2)).

Parameters:

mean (ndarray)

sample(key, n=None)[source]

Draw one sample (shape (dim,)) or n samples ((n, dim)).

key is a numpy.random.Generator, an integer seed, or a JAX PRNG key (JAX arrays out, traceable).

class minilink.core.distributions.Uniform(lower, upper)[source]

Uniform law on the box lower <= x <= upper; support is that box.

sample(key, n=None)[source]

Draw one sample (shape (dim,)) or n samples ((n, dim)).

key is a numpy.random.Generator, an integer seed, or a JAX PRNG key (JAX arrays out, traceable).

class minilink.core.distributions.Particles(points)[source]

Empirical distribution: a uniform choice among fixed points (N, dim).

sample(key, n=None)[source]

Draw one sample (shape (dim,)) or n samples ((n, dim)).

key is a numpy.random.Generator, an integer seed, or a JAX PRNG key (JAX arrays out, traceable).

class minilink.core.distributions.Sampler(draw, mean, support=None)[source]

Distribution defined by a sampling function draw(key) -> x.

For task-specific starts (a random point along a track, a random pose in free space). draw must accept a JAX key and trace; mean is the representative point given explicitly.

Parameters:
  • mean (ndarray)

  • support (Set | None)

sample(key, n=None)[source]

Draw one sample (shape (dim,)) or n samples ((n, dim)).

key is a numpy.random.Generator, an integer seed, or a JAX PRNG key (JAX arrays out, traceable).

Workspace solids by their signed distance sdf(p): negative inside, zero on the surface.

class minilink.core.geometry.Shape[source]

Base class for workspace solids defined by a signed-distance function.

A point p is inside the solid when sdf() is negative. The solid may depend on time and optional parameters, so the generic signature is sdf(p, t, params).

abstract property dim: int

Workspace dimension (2 or 3).

abstract sdf(p, t=0.0, params=None)[source]

Return the signed distance from p to the shape surface.

contains(p, t=0.0, params=None)[source]

Return True when p lies inside the solid (sdf < 0).

Parameters:

t (float)

Return type:

bool

inflate(radius)[source]

Return this shape grown by radius (Minkowski clearance).

Parameters:

radius (float)

Return type:

Inflated

class minilink.core.geometry.Sphere(center, radius)[source]

Ball ||p - center|| <= radius (a disc in 2-D, a sphere in 3-D).

Parameters:
  • center (array_like) – Center with shape (dim,).

  • radius (float) – Nonnegative radius.

property dim: int

Workspace dimension (2 or 3).

sdf(p, t=0.0, params=None)[source]

Return the signed distance from p to the shape surface.

class minilink.core.geometry.Box(lower, upper)[source]

Axis-aligned box lower <= p <= upper.

Parameters:
  • lower (array_like) – Opposite corners with shape (dim,).

  • upper (array_like) – Opposite corners with shape (dim,).

property dim: int

Workspace dimension (2 or 3).

sdf(p, t=0.0, params=None)[source]

Return the signed distance from p to the shape surface.

class minilink.core.geometry.Union(shapes)[source]

Union of several shapes, with sdf = min over the members.

This is the obstacle-field primitive: a point is occupied when it lies inside any member shape.

Parameters:

shapes (tuple)

property dim: int

Workspace dimension (2 or 3).

sdf(p, t=0.0, params=None)[source]

Return the signed distance from p to the shape surface.

class minilink.core.geometry.Inflated(base, radius)[source]

Shape grown outward by radius (Minkowski sum with a ball).

Subtracting the radius from a base signed distance gives the clearance for a disc-shaped robot, or a safety margin around a hard obstacle.

Parameters:
  • base (Shape)

  • radius (float)

property dim: int

Workspace dimension (2 or 3).

sdf(p, t=0.0, params=None)[source]

Return the signed distance from p to the shape surface.