Blocks

Basic dynamic blocks: the integrator.

class minilink.blocks.basic.Integrator[source]

Single integrator block: dx = k * u, y = x.

The gain k defaults to 1.0 (a pure integrator); it also serves as the canonical scalar block parameter in tests and demos.

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,)

Source blocks: constant, step, sine, white noise and a replayed trajectory as a signal.

class minilink.blocks.sources.Source(p, *, port='y')[source]

Constant source: y = value for all t.

Also the base class for time-signal sources; subclasses override h() with their own y = h(t; p). port names the output signal: y by default, or the input it drives (Step(port="r"), WhiteNoise(port="d")), which is how autowire finds it.

property dim

The signal’s dimension, whatever the output port is named.

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,)

show_signal(t0=None, tf=None, n_pts=1000, ax=None)[source]

Plot the source output signal over a time range.

Parameters:
  • t0 (float, optional) – Start time (default 0.0).

  • tf (float, optional) – End time (default 10.0).

  • n_pts (int, optional) – Number of evaluation points used for plotting.

  • ax (matplotlib.axes.Axes, optional) – Existing axis to draw on. If None, a new figure is created and shown (same policy as minilink.graphical.signals.plot_time_signals()); if an axis is passed, the caller controls display.

class minilink.blocks.sources.Step(initial_value=None, final_value=None, step_time=1.0, *, port='y')[source]

Step source: y = initial_value if t < step_time, else final_value.

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,)

class minilink.blocks.sources.Sine(amplitude=1.0, omega=1.0, phase=0.0, offset=0.0, *, port='y')[source]

Sine source: y = offset + amplitude sin(omega t + phase), omega in rad/s.

amplitude sets the dimension; omega, phase and offset are scalars or arrays of the same shape. A frequency f in hertz is omega = 2 * np.pi * f.

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,)

class minilink.blocks.sources.WhiteNoise(p=1, *, psd=1.0, sample_period=0.01, seed=0, hold='zoh', port='y')[source]

White noise of intensity psd, held over each sample_period.

The intensity is the two-sided spectral density of the signal (a one-sided estimate such as scipy.signal.welch’s default reads twice it). The per-sample covariance is the intensity over the period, so the physics does not change with the period; a per-sample variance over a period is an intensity of variance times period. The zero-order hold keeps each sample for its whole period; hold="linear" interpolates between samples, with a mid-period variance of half the per-sample one. A fixed step that divides the period keeps RK4’s stages inside one sample except the last stage of a boundary step, an effect first order in the step. The sample counter is 32 bits: the signal repeats after 2³² samples. seed=None gives the mean (zero); the seed is the block’s default realization, realize() draws another. A scalar or vector psd is a diagonal intensity, a matrix a full one.

is_random = True

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

property psd

a (p,) diagonal or a (p, p) matrix.

Type:

The intensity W

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,)

realize(key)[source]

The params of one realization: a fresh seed under key, or the mean for None.

refresh()[source]

Publish the sample period as the solver hint of the block.

class minilink.blocks.sources.TrajectorySource(t, values, *, interpolation='linear', port='y')[source]

Replay a stored signal y(t) sampled at times t.

The open-loop policy u = pi(t): feed a planned input trajectory into a diagram, source >> plant. Between samples the signal is interpolated linearly (interpolation="linear", what a collocation or shooting transcription assumes) or held at the previous sample ("previous", the zero-order hold of a sampled controller). Values outside [t[0], t[-1]] hold the nearest endpoint. values has shape (p, N) (or (N,) for a scalar signal) aligned with the N sample times in t.

classmethod from_trajectory(traj, signal='u', *, interpolation='linear')[source]

Build a source that replays a Trajectory signal (u or x).

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,)

refresh()[source]

Rebuild the per-channel interpolators, each endpoint held outside the samples.

Transfer-function blocks: a SISO TransferFunction in state space, and Lead / Lag.

class minilink.blocks.transfer_function.TransferFunction(numerator, denominator, *, ports=None, name='Transfer Function', expose_state=True)[source]

Continuous-time SISO transfer function in state-space realization.

The default is a plant: input u, outputs y and x; expose_state=False keeps u -> y only (a filter is not a plant). Compensator layouts match the other classical blocks: ports="error" is e -> u (C @ plant inserts the Error block; C >> plant is the loop gain); ports="reference" is r, y -> u.

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,)

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.blocks.transfer_function.Lead(K=1.0, z=1.0, p=10.0, *, ports='error')[source]

Lead compensator C(s) = K (s + z) / (s + p) with z < p: phase lead between z and p.

class minilink.blocks.transfer_function.Lag(K=1.0, z=1.0, p=0.1, *, ports='error')[source]

Lag compensator C(s) = K (s + z) / (s + p) with p < z: gain at low frequency.

Signal routing blocks: the error comparison, a signed sum, a gain, mux and demux.

class minilink.blocks.routing.Error(dim=1)[source]

Tracking-error comparison e = r - y on ports +, -, e.

The automatic feedback pattern (C @ plant, feedback(L)) inserts this block so the diagram reads as a comparison, not a generic summer. All three ports share the same dimension dim.

class minilink.blocks.routing.Sum(signs=(1.0, -1.0), dim=1)[source]

Signed summing junction y = Σ sign_i · in_i on ports in0, in1, ….

The default signs=(1.0, -1.0) is a two-input difference y = in0 - in1. Use Error for the named tracking-error comparison that @ inserts. All input ports share the same dimension dim.

signs is a structural attribute fixed at construction (it sets the number of input ports), deliberately not a tunable params entry.

class minilink.blocks.routing.Gain(K, dim=None)[source]

Constant gain y = K · u.

K may be a full (p, m) matrix, a 1-D vector (diagonal gain), or a scalar (needs dim to set the port size). The gain lives in params["K"] so it can be tuned or differentiated.

class minilink.blocks.routing.Mux(dims=(1, 1))[source]

Stack input signals in0, in1, … into one vector output y.

dims lists each input-port dimension; the output dimension is their sum. The combined signal is just the concatenation in port order, so the compute is the identity on the stacked input vector.

class minilink.blocks.routing.Demux(dims=(1, 1), *, port='u')[source]

Split one vector into NumPy-style slices of a named signal.

dims lists each output-port dimension; the input dimension is their sum. The input port is port (default u); each output is the corresponding slice, u[0], u[1], or u[0:2] for a wider block. Auto-wired feedback passes the plant port name so a y measurement becomes y in and y[0], y[1], … out.

Static nonlinearities: saturation, dead zone, relay, and the stateful rate limiter.

class minilink.blocks.nonlinear.Saturation(lower=-1.0, upper=1.0, dim=1)[source]

Symmetric or asymmetric clip y = clip(u, lower, upper).

class minilink.blocks.nonlinear.DeadZone(width=1.0, dim=1)[source]

Dead zone of half-width width: zero inside, shifted outside.

y = u - width for u > width, y = u + width for u < -width, and y = 0 in between — the standard backlash/stiction model.

class minilink.blocks.nonlinear.Relay(amplitude=1.0, dim=1)[source]

Bang-bang relay y = amplitude · sign(u) (sign(0) = 0).

class minilink.blocks.nonlinear.RateLimiter(rate_max=1.0, tau=0.05, lower=-inf, upper=inf, dim=1, x0=0.0)[source]

Pass a command through, limited to rate_max per second and to the end stops.

Parameters:
  • rate_max (float) – Largest rate of change of the output [unit/s]; must be positive.

  • tau (float) – Time constant of the lag the block shows for small steps [s]; must be positive. The knee between the two regimes sits at a step of about tau * rate_max.

  • lower (float) – End stops on the output [unit]; infinite by default, i.e. a rate limit only.

  • upper (float) – End stops on the output [unit]; infinite by default, i.e. a rate limit only.

  • dim (int) – Number of independent channels.

  • x0 (float or array) – Output at t = 0 [unit], one value or one per channel.

f(x, u, t=0.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.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,)

Signal-conditioning filters: Butterworth low-pass, notch and washout, from a frequency in Hz.

class minilink.blocks.filters.LowPassFilter(cutoff_hz=1.0, order=1)[source]

Butterworth low-pass with -3 dB cutoff at cutoff_hz (order 1 default).

First order is H(s) = ω_c / (s + ω_c) with ω_c = 2π·cutoff_hz; higher order uses the analog Butterworth realization. A filter is u -> y and not a plant: it exposes no state x.

class minilink.blocks.filters.NotchFilter(notch_hz=1.0, quality=10.0)[source]

Second-order notch (band-stop) at notch_hz with quality factor quality.

H(s) = (s² + ω₀²) / (s² + (ω₀/Q)·s + ω₀²) with ω₀ = 2π·notch_hz; larger quality narrows the rejected band. A filter is u -> y and not a plant: it exposes no state x.

class minilink.blocks.filters.Washout(cutoff_hz=1.0)[source]

First-order high-pass / washout H(s) = s / (s + ω_c).

Passes transients and rejects the steady component; ω_c = 2π·cutoff_hz. A filter is u -> y and not a plant: it exposes no state x.

Discrete-time wiring blocks: a command latch on the step side of a hybrid loop.

class minilink.blocks.step.ZOHHold[source]

Command latch: x_{k+1} = u_k, y_k = x_k.

Teaches the zero-order hold / hold-register pattern on the step side of hybrid boundaries (plant-side holds are handled by HybridSimulator).

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,)

Neural-network static blocks: the weights are parameters, the training lives outside.

class minilink.blocks.neural.NeuralNetwork(input_dim, output_dim, hidden_dim=8, seed=0, scale=0.1)[source]

One-hidden-layer neural network y = W2 tanh(W1 u + b1) + b2.

Parameters:
  • input_dim (int) – Dimension of the input port u.

  • output_dim (int) – Dimension of the output port y.

  • hidden_dim (int, optional) – Number of hidden units.

  • seed (int, optional) – Seed for deterministic weight initialization.

  • scale (float, optional) – Standard-deviation scale for the initial weights. Biases start at zero.

class minilink.blocks.neural.MLP(input_dim, output_dim, hidden=(64, 64), activation='tanh', seed=0, output_gain=1.0)[source]

Multilayer perceptron y = W_L a_{L-1} + b_L with a_k = act(W_k a_{k-1} + b_k).

Weights live in params as {"W0", "b0", "W1", "b1", ...} so training loops differentiate through the compiled diagram like any other parameter.

Parameters:
  • input_dim (int) – Port dimensions.

  • output_dim (int) – Port dimensions.

  • hidden (tuple of int) – Hidden layer widths; () gives an affine map.

  • activation ({"tanh", "relu"}) – Hidden nonlinearity.

  • seed (int) – Seed of the orthogonal initialization.

  • output_gain (float) – Gain of the last layer’s orthogonal init (0.01 for a policy mean that starts near zero, 1.0 for a value function).