Analysis

Linearization of a System about an operating point: the Jacobians (A, B, C, D) and an LTISystem.

minilink.analysis.linearization.linearize_matrices(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, method='auto', eps=1e-06)[source]

Linearize sys about (x_bar, u_bar) and return A, B, C, D.

Parameters:
  • sys (DynamicSystem) – Leaf or diagram whose f defines the dynamics.

  • x_bar (array-like, optional) – Operating point; default the nominal state and the nominal port values.

  • u_bar (array-like, optional) – Operating point; default the nominal state and the nominal port values.

  • t (float, optional) – Time at which the Jacobians are evaluated.

  • params (dict, optional) – Parameter set; default the live sys.params.

  • of (selector or list of selectors, optional) – Rows of C and D: an output port id, a diagram wire "block:port", or (selector, index) for one component. Default: every boundary output of a diagram, the y port of a leaf, or the full state (C = I) when there is no y port.

  • wrt (selector or list of selectors, optional) – Columns of B and D: an input port id, a diagram wire, or (selector, index). Default: every input port, stacked.

  • method ({"auto", "fd", "jax"}, optional) – Differentiation backend, see jacobian().

  • eps (float, optional) – Central-difference step.

Return type:

tuple[ndarray, ndarray, ndarray, ndarray]

minilink.analysis.linearization.linearize(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, method='auto', eps=1e-06)[source]

Linearize sys about (x_bar, u_bar) and return an LTISystem.

Same arguments as linearize_matrices(); x_bar=None defaults to the nominal state like every tool of the analysis family. The model carries the operating point it was taken at (xbar, ubar for the selected inputs, ybar for the selected outputs), so a design made on it (lqr, kalman) knows where it applies.

minilink.analysis.linearization.design_model(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, method='auto', eps=1e-06, measured=False)[source]

The linear model a design verb reads: sys itself when it is an LTISystem, else sys linearized about its command port.

An LTISystem carries the operating point it was taken at, so a point given here is refused: linearize the plant at x_bar instead. Any other system is linearized at (x_bar, u_bar), the nominal state and inputs by default, with B over the command port alone (u, else the one input that is not a disturbance d or a noise n; every other input is held at its nominal value) and C over the measured output y alone, so a plant with noise ports gives a gain over its command and a box exposing its state is measured through y. measured=True (the observer designs) refuses a plant without y rather than reading its state as the measurement.

Jacobians of a System at an operating point.

minilink.analysis.derivatives.jacobian(sys, of, wrt, x_bar=None, u_bar=None, t=0.0, params=None, *, method='auto', eps=1e-06)[source]

Return d(of)/d(wrt) of sys at (x_bar, u_bar, t, params).

jacobian(sys, "f", "x") is ∂f/∂x: the first argument names what is differentiated ("f", "step", an output port, or a diagram wire "block:port"), the second the variable ("x", "u", an input port, "t", "params", or a wire). The point follows in the order of f(x, u, t, params) with the textbook defaults, and each call compiles its own evaluator (about a millisecond): exact under JAX when the system traces, central finite differences otherwise. Nothing is stored on the system; keep evaluator.jacobian(of, wrt) when a loop needs the callable.

Parameters:
  • sys (System) – Leaf, diagram, static block, or step system.

  • of (str) – "f" ("step" on step systems), an output port id, or a diagram wire "block:port".

  • wrt (str) – "x", "u" (every input stacked), an input port id, "t", "params", or a diagram wire (additive perturbation of that signal).

  • x_bar (array-like, optional) – Operating point; default the nominal state and the nominal port values.

  • u_bar (array-like, optional) – Operating point; default the nominal state and the nominal port values.

  • t (float, optional) – Time (the step index k on step systems).

  • params (dict, optional) – Parameter set; default the live sys.params.

  • method ({"auto", "fd", "jax"}, optional) – "auto" is exact under JAX when it is installed and the system traces, finite differences otherwise; "jax" raises instead of falling back.

  • eps (float, optional) – Central-difference step.

Returns:

Shape (dim(of), dim(wrt)); (dim(of),) for wrt="t"; for wrt="params" a dict shaped like params with leaves (dim(of), *leaf.shape) (float leaves only). A block whose f reads instance attributes instead of its params argument reports zero sensitivity, like every parametric tier.

Return type:

ndarray or dict

Discretization: a continuous-time system as a discrete-time step model.

minilink.analysis.discretization.discretize(system, dt=None, *, integrator='rk4', params=None)[source]

Wrap a DynamicSystem as a StepSystem with hold interval dt.

x_{k+1} = step(x, u, k; p) integrates f over [k dt, (k + 1) dt] with integrator "rk4" or "euler" (the same word as Sys2Gym), the input held. p defaults to the wrapper’s params: the source’s live params when params is None, else the dict given, which replaces them (RULES 4.4). The hold interval has one owner, the wrapper’s dt, and never reaches f or h: when dt is omitted it is read once from params["dt"], and a "dt" key is dropped from the params given. Three cases are therefore refused: dt and a params["dt"] that differ, since one would be dropped unseen; a params that holds only "dt" while the source has other params, since it would replace them with {} (pass dt= to keep them); and, when params is None, a source whose own params carry "dt", since they reach f live beside dt (remove the key and pass dt=). The wrapper starts from the source’s x0 and keeps its state, input and y metadata.

Parameters:
  • system (DynamicSystem)

  • dt (float | None)

  • integrator (str)

  • params (dict | None)

Return type:

StepSystem

class minilink.analysis.discretization.DiscretizedDynamicSystem(source, dt, params=None, *, integrator)[source]

Discrete-time wrapper over a continuous DynamicSystem.

The hold interval dt lives on the wrapper and nowhere else: params are handed to the source’s f and h untouched, so building the wrapper or assigning params refuses a "dt" key in them (a key added later in place to the source’s live params is not checked). The wrapper starts from the source’s x0 and copies its state, input and y labels, units, bounds and nominal values.

Parameters:
  • source (DynamicSystem)

  • dt (float)

  • params (dict | None)

  • integrator (str)

property params: dict

the source’s live ones, or the override.

The source’s live params while params_override is None (the same dict, so an in-place edit reaches the source), else that dict. Assigning params sets params_override, and refuses a "dt" key in the params that would reach f: the value, or the source’s params when the value is None.

Type:

The params f and h receive

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

class minilink.analysis.discretization.DiscretizedEulerDynamicSystem(source, dt, params=None)[source]

x_{k+1} = x_k + dt f(x_k, u_k, t_k; p).

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

class minilink.analysis.discretization.DiscretizedRK4DynamicSystem(source, dt, params=None)[source]

One RK4 step of f over [t_k, t_k + dt] with ZOH on u.

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

Structural analysis: the controllability and observability of a system’s linear model.

minilink.analysis.structural.controllability_matrix(A, B)[source]

Return the Kalman controllability matrix 𝒞 = [B, AB, …, Aⁿ⁻¹B] of the pair (A, B).

The arrays twin of controllability(), as lqr_gain is lqr’s.

minilink.analysis.structural.observability_matrix(A, C)[source]

Return the Kalman observability matrix 𝒪 = [C; CA; …; CAⁿ⁻¹] of the pair (A, C).

The arrays twin of observability().

minilink.analysis.structural.controllability(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, tol=1e-09, method='auto', eps=1e-06)[source]

Return the controllability test of sys’s linear model.

The pair (A, B) is controllable when its Kalman controllability matrix has full row rank n. sys is an LTISystem, read as is, or any plant, linearized at (x_bar, u_bar) about its command port alone (the nominal point by default), as lqr and place read it. tol is the relative singular-value threshold below which a direction counts as missing, the same as minreal()’s. On arrays, controllability_matrix() gives 𝒞.

minilink.analysis.structural.observability(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, tol=1e-09, method='auto', eps=1e-06)[source]

Return the observability test of sys’s linear model.

The pair (A, C) is observable when its Kalman observability matrix has full column rank n. sys is an LTISystem, read as is, or any plant, linearized at (x_bar, u_bar) with C over its measured output y (a plant without y is refused), as luenberger and kalman read it; tol as in controllability(). On arrays, observability_matrix() gives 𝒪.

class minilink.analysis.structural.StructuralResult(matrix, rank, n)[source]

Result of a controllability or observability test.

Parameters:
  • matrix (ndarray)

  • rank (int)

  • n (int)

property is_full_rank: bool

the Kalman matrix has rank n.

Type:

The test passes

Equilibria of DynamicSystem models.

minilink.analysis.equilibria.find_equilibrium(sys, x_guess=None, u_bar=None, t=0.0, params=None, *, tol=1e-09)[source]

Return a state x_eq near x_guess with f(x_eq, u_bar, t) ≈ 0.

Parameters:
  • sys (DynamicSystem) – Continuous-time model whose f defines the dynamics (leaf subclass or wired diagram with stacked f).

  • x_guess (array of shape (n,), optional) – Initial guess for the equilibrium state. Defaults to the nominal state.

  • u_bar (array of shape (m,), optional) – Held input. Defaults to the system’s nominal port values.

  • t (float, optional) – Time at which to evaluate f.

  • params (dict, optional) – Parameter set forwarded to f.

  • tol (float, optional) – The solver’s step tolerance (xtol); the root is accepted when ‖f‖ ≤ 10³ tol.

Returns:

x_eq – The equilibrium state.

Return type:

np.ndarray

Raises:

RuntimeError – If the solver does not drive ‖f‖ to 10³ tol or below.

Modal analysis: the poles and mode shapes of the linearized dynamics, and their animation.

minilink.analysis.modal.modal_analysis(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, method='auto', eps=1e-06)[source]

Linearize about (x_bar, u_bar) and eigendecompose A.

Parameters:
  • sys (System) – Plant or other system with f (and h).

  • x_bar (array of shape (n,), optional) – Operating-point state. Defaults to the nominal state.

  • u_bar (array of shape (m,), optional) – Operating-point input. Defaults to port nominals.

  • t (float, optional) – Time at which the Jacobian is evaluated.

  • params (dict, optional) – Parameter set; default the live sys.params.

  • method ({"auto", "fd", "jax"}) – Differentiation backend, see jacobian().

  • eps (float, optional) – Central-difference step.

Returns:

  • poles (ndarray of shape (n,)) – Eigenvalues of A.

  • modes (ndarray of shape (n, n)) – Eigenvectors (columns are mode shapes in perturbation coordinates).

minilink.analysis.modal.animate_modal(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, mode='all', method='auto', eps=1e-06, amplitude=1.0, tf=None, n_steps=2001, time_factor_video=3.0, renderer='matplotlib', is_3d=False, show=True, html=None, native=True)[source]

Linearize, excite selected mode(s), and animate them on sys.

Each mode is animated as an absolute state, the operating point plus the mode’s motion, so the system’s own kinematics draw it. Calls modal_analysis(), then animate() once per mode.

Parameters:
  • sys (System) – System used for graphics (usually the nonlinear plant).

  • x_bar (array of shape (n,), optional) – Linearization operating point. Defaults to the nominal state.

  • u_bar (array of shape (m,), optional) – Operating-point input during the animation.

  • mode (int or 'all') – Mode index to animate, or every index 0 … n-1.

Returns:

Same as modal_analysis().

Return type:

poles, modes

Linear analysis of one channel’s matrices (A, B, C, D): poles, zeros, gain, minimal realization, responses, margins, root locus.

minilink.analysis.linear.poles(A)[source]

Eigenvalues of A.

minilink.analysis.linear.zeros(A, B, C, D)[source]

Transmission zeros of the square channel (A, B, C, D): the finite eigenvalues of its Rosenbrock pencil.

minilink.analysis.linear.gain(A, B, C, D)[source]

Leading coefficient k of the factored transfer function of a SISO channel.

d when the channel has feedthrough, otherwise the first nonzero Markov parameter; read off the response at one real point beyond every pole and zero.

minilink.analysis.linear.frequency_response(A, B, C, D, w)[source]

Frequency response of a SISO channel, one complex value per frequency in w.

minilink.analysis.linear.frequency_range(A, B, C, D)[source]

(w_min, w_max) covering the dynamics and any 0 dB crossing.

One decade beyond the slowest and fastest nonzero pole or zero, then widened until the band brackets |G| = 1. The widening matters: roots at the origin carry no rate, so an integrator’s crossover sits below the pole-derived band, and a large static gain pushes it above — in both cases a band read from the roots alone misses the crossover and margins() would report an infinite margin.

minilink.analysis.linear.minreal(A, B, C, D, *, tol=1e-09)[source]

Minimal realization: drop the modes that are uncontrollable or unobservable.

The pole/zero pairs of those modes cancel; pzmap, root_locus and transfer_function run it by default. tol is the relative singular-value threshold below which a direction counts as missing, the one controllability() and observability() use. A realization that is already minimal comes back in its own coordinates.

class minilink.analysis.linear.Margins(gain_margin_db, phase_margin_deg, w_gain_crossover, w_phase_crossover)[source]

Gain and phase margins of a loop transfer function L(jw).

Parameters:
  • gain_margin_db (float)

  • phase_margin_deg (float)

  • w_gain_crossover (float)

  • w_phase_crossover (float)

minilink.analysis.linear.margins(w, G, response=None)[source]

Margins from a sampled response G = L(jw); crossings found by interpolation.

With response, a callable w -> L(jw), each bracketed crossing is solved exactly on it (Brent’s method), so the margins do not depend on the grid, which only has to bracket them.

minilink.analysis.linear.closed_loop_poles(A, B, C, D, K)[source]

Poles of the SISO loop closed with u = -K y.

minilink.analysis.linear.root_locus(A, B, C, D, gains=None, *, n_gains=400)[source]

Closed-loop poles over a gain sweep, one continuous branch per column.

gains defaults to 0 followed by six log-spaced decades ending where the far branches reach ten times the radius of the open-loop poles and zeros (n_gains points); consecutive gain points are refined while any branch moves by more than two percent of that radius. Returns (gains, roots) with roots of shape (len(gains), n_states).

minilink.analysis.linear.step_response(A, B, C, D, t)[source]

Unit-step response from rest, exact on a uniform time grid t.

One matrix exponential gives the zero-order-hold pair (A_d, B_d); the state is then marched exactly.

minilink.analysis.linear.settling_horizon(A)[source]

Default step-response horizon: eight times the slowest stable time constant.

The Riccati equations of linear-quadratic design: the algebraic solution and the exact step of the differential one.

minilink.analysis.riccati.care(A, B, Q, R)[source]

Return P, the stabilizing solution of the continuous algebraic Riccati equation.

minilink.analysis.riccati.hamiltonian_matrix(A, B, Q, R_inv)[source]

The Hamiltonian matrix: with the co-state λ = S x, z = [x; λ] follows ż = H z.

minilink.analysis.riccati.riccati_transition(A, B, Q, R_inv, dt)[source]

The transition E of the Hamiltonian system over one step of dt backward in time.

minilink.analysis.riccati.riccati_step(A, B, Q, R_inv, S, dt)[source]

S(t) → S(t − dt) with the dynamics frozen over the interval, exactly.

The same step on the dual pair (Aᵀ, Cᵀ) carries a filter covariance from P(t) forward to P(t + dt): the filter equation is the control equation with time reversed and the matrices transposed.

minilink.analysis.riccati.riccati_map(E, S)[source]

One exact step S(t) → S(t − dt) of the Riccati equation, from the transition E.

Step response of one input–output channel of the linear model, and its textbook figures.

class minilink.analysis.time_response.StepInfo(rise_time, settling_time, overshoot, peak, peak_time, steady_state)[source]

Textbook step-response figures (MATLAB stepinfo conventions).

Parameters:
  • rise_time (float)

  • settling_time (float)

  • overshoot (float)

  • peak (float)

  • peak_time (float)

  • steady_state (float)

minilink.analysis.time_response.step_response(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, tf=None, n=500, method='auto', eps=1e-06)[source]

Return (time, y): the unit-step response of the selected channel from rest.

The response of the linear model, in deviation variables: sys as is when it is an LTISystem, else linearized at (x_bar, u_bar). Same channel arguments as bode(); tf defaults to eight times the slowest stable time constant, and n samples span [0, tf]. The step of the nonlinear loop itself is a simulation, closed_loop(C, plant, reference=Step(...)) then compute_trajectory, and step_info() reads its sampled output the same way.

Parameters:
  • n (int)

  • method (str)

  • eps (float)

Return type:

tuple[ndarray, ndarray]

minilink.analysis.time_response.step_info(time, y)[source]

Rise time, settling time, overshoot, peak and steady state of a sampled step response.

Reads any sampled response: the linear one from step_response(), or a simulated trajectory’s time and output arrays. The settling band is 2 % of the response’s peak deviation from its final value: 2 % of the final value for a step from rest that neither dips below its start nor overshoots by 100 %, and still defined for a response that returns to zero.

Return type:

StepInfo

minilink.analysis.time_response.plot_step_response(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, tf=None, n=500, method='auto', eps=1e-06, backend='matplotlib', show=True)[source]

Step response of the selected channel with rise time, settling time and overshoot.

The linear model’s response, as step_response(); the title reads “Linear Step Response” for an LTISystem and “Linearized Step Response” for a system linearized here.

Parameters:
  • n (int)

  • method (str)

  • eps (float)

  • show (bool)

Return type:

PlotResult

Frequency-domain analysis of one input–output channel of a linearized model.

minilink.analysis.frequency.frequency_response(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, w=None, n=200, method='auto', eps=1e-06)[source]

Return (w, G) with G(jw) complex on the selected channel.

Parameters:
  • sys (System) – System or diagram to linearize before computing the frequency response.

  • x_bar (array-like, optional) – Operating point; default the nominal state and the nominal port values.

  • u_bar (array-like, optional) – Operating point; default the nominal state and the nominal port values.

  • t (float, optional) – Time at which the Jacobians are evaluated.

  • params (dict, optional) – Parameter set; default the live sys.params.

  • of (str or (str, int), optional) – Output of the channel: a port id, a diagram wire "block:port", or (selector, index) for one component (a bare id means component 0). Default: component 0 of the primary output.

  • wrt (str or (str, int), optional) – Input of the channel, same forms. Default: component 0 of the first input port.

  • w (array, optional) – Frequencies in rad/s. When omitted, a logarithmic grid runs one decade below the slowest pole or zero to one decade above the fastest, widened until it brackets the 0 dB crossing.

  • n (int, optional) – Number of frequencies for the automatic grid.

  • method ({"auto", "fd", "jax"}, optional) – Differentiation backend, see jacobian().

  • eps (float, optional) – Central-difference step.

Return type:

tuple[ndarray, ndarray]

minilink.analysis.frequency.bode(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, w=None, n=200, method='auto', eps=1e-06)[source]

Return the SISO Bode response (w, magnitude_db, phase_deg).

Same arguments as frequency_response(); the phase is unwrapped.

Parameters:
  • n (int)

  • method (str)

  • eps (float)

Return type:

tuple[ndarray, ndarray, ndarray]

minilink.analysis.frequency.margins(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, w=None, n=2000, method='auto', eps=1e-06)[source]

Gain and phase margins of the selected channel taken as a loop gain.

Same arguments as frequency_response(); returns Margins (inf where a crossover does not exist).

Parameters:
  • n (int)

  • method (str)

  • eps (float)

Return type:

Margins

minilink.analysis.frequency.nyquist(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, w=None, n=500, method='auto', eps=1e-06)[source]

Return (w, G) for the Nyquist contour, positive frequencies only.

Same arguments as frequency_response(). The plot mirrors the conjugate branch; poles on the imaginary axis make |G| blow up at those frequencies (the indented contour is not drawn).

Parameters:
  • n (int)

  • method (str)

  • eps (float)

Return type:

tuple[ndarray, ndarray]

minilink.analysis.frequency.pzmap(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, method='auto', eps=1e-06, minimal=None)[source]

Return the selected SISO channel as (zeros, poles, gain).

Same arguments as frequency_response() without the frequency grid: poles are the eigenvalues of A, zeros the transmission zeros of the channel, gain the leading coefficient of its factored transfer function. minimal (default: yes, with a one-time notice when a pair cancels) first reduces the channel to its minimal realization, so the uncontrollable and unobservable modes cancel; minimal=False keeps every mode of the realization.

Parameters:
  • method (str)

  • eps (float)

  • minimal (bool | None)

Return type:

tuple[ndarray, ndarray, float]

minilink.analysis.frequency.root_locus(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, gains=None, method='auto', eps=1e-06, minimal=None)[source]

Closed-loop poles of u = -K y on the selected channel over a gain sweep.

Same arguments as frequency_response() without the frequency grid; gains defaults to an adaptive sweep from K = 0 to the gain where the far branches leave the picture. Returns (gains, roots) with roots of shape (len(gains), n), one continuous branch per column. minimal as in pzmap().

Parameters:
  • method (str)

  • eps (float)

  • minimal (bool | None)

Return type:

tuple[ndarray, ndarray]

minilink.analysis.frequency.transfer_function(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, method='auto', eps=1e-06, minimal=None)[source]

Return the selected SISO channel as a TransferFunction block.

Same arguments as frequency_response() without the frequency grid; minimal as in pzmap(). The numerator and denominator come from the zeros, poles and gain of pzmap(); the block is their state-space realization and carries numerator, denominator, poles and zeros, so it can be plotted, simulated, or wired like any other block.

Parameters:
  • method (str)

  • eps (float)

  • minimal (bool | None)

minilink.analysis.frequency.plot_bode(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, w=None, n=200, margins=True, method='auto', eps=1e-06, backend='matplotlib', show=True, title=None)[source]

Bode diagram of the selected channel; margins=True marks the crossovers.

The crossovers and the printed margins are those of margins(), read on its grid (or on w), not on the n plotted frequencies. title overrides the figure heading (default "Bode Diagram").

Parameters:
  • n (int)

  • margins (bool)

  • method (str)

  • eps (float)

  • show (bool)

  • title (str | None)

Return type:

PlotResult

minilink.analysis.frequency.plot_pzmap(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, method='auto', eps=1e-06, minimal=None, radius=None, backend='matplotlib', show=True)[source]

Pole-zero map of the selected channel: x poles, o zeros.

minimal as in pzmap(). radius frames the roots within that distance of the origin, the window a stiff model needs to show its slow roots; the default frames every root.

Parameters:
  • method (str)

  • eps (float)

  • minimal (bool | None)

  • radius (float | None)

  • show (bool)

Return type:

PlotResult

minilink.analysis.frequency.plot_root_locus(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, gains=None, method='auto', eps=1e-06, minimal=None, radius=None, backend='matplotlib', show=True)[source]

Root locus of the selected channel closed with u = -K y.

minimal as in pzmap(). radius frames the branches within that distance of the origin; the default frames three times the radius of the open-loop roots, so the far tails of the asymptotes leave the picture.

Parameters:
  • method (str)

  • eps (float)

  • minimal (bool | None)

  • radius (float | None)

  • show (bool)

Return type:

PlotResult

minilink.analysis.frequency.plot_nyquist(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, of=None, wrt=None, w=None, n=500, method='auto', eps=1e-06, backend='matplotlib', show=True)[source]

Nyquist diagram of the selected channel with the critical point -1.

Parameters:
  • n (int)

  • method (str)

  • eps (float)

  • show (bool)

Return type:

PlotResult

Sensitivity functions of a feedback loop, built from its plant, controller and filter.

minilink.analysis.sensitivity_functions.sensitivity(*, plant, controller, filter=None, x_bar=None, u_bar=None, t=0.0, params=None, method='auto', eps=1e-06)[source]

Return the sensitivity function S of the loop as a TransferFunction.

S maps an output disturbance to y and the reference r to the error e. The loop is the plant H, the controller C and the optional filter F on the measurement, each a SISO System linearized first.

Parameters:
  • plant (System) – The plant H, from its one input besides d and n to its output.

  • controller (System) – The controller C, from the error e to the command u. A controller in the reference form (inputs r and y) is refused: pass its channel from r, transfer_function(C, wrt="r").

  • filter (System, optional) – The filter F on the measurement; F = 1 when omitted.

  • x_bar (optional) – The plant’s operating point, as in bode(); the controller and the filter are taken at their nominal point.

  • u_bar (optional) – The plant’s operating point, as in bode(); the controller and the filter are taken at their nominal point.

  • t (optional) – The plant’s operating point, as in bode(); the controller and the filter are taken at their nominal point.

  • params (optional) – The plant’s operating point, as in bode(); the controller and the filter are taken at their nominal point.

  • method ({"auto", "fd", "jax"}, optional) – Differentiation backend of the linearizations.

  • eps (float, optional) – Central-difference step.

Returns:

S(s), ready for bode(), plot_bode or a simulation; its ports are labelled r and e (T: r to y, PS: w to y, CS: v to u).

Return type:

TransferFunction

minilink.analysis.sensitivity_functions.complementary_sensitivity(*, plant, controller, filter=None, x_bar=None, u_bar=None, t=0.0, params=None, method='auto', eps=1e-06)[source]

Return the complementary sensitivity T, the map from r to y.

Same arguments as sensitivity(); without a filter, S + T = 1.

Parameters:
  • method (str)

  • eps (float)

minilink.analysis.sensitivity_functions.load_sensitivity(*, plant, controller, filter=None, x_bar=None, u_bar=None, t=0.0, params=None, method='auto', eps=1e-06)[source]

Return the load sensitivity PS, the map from a disturbance d at the plant input to y.

Same arguments as sensitivity().

Parameters:
  • method (str)

  • eps (float)

minilink.analysis.sensitivity_functions.noise_sensitivity(*, plant, controller, filter=None, x_bar=None, u_bar=None, t=0.0, params=None, method='auto', eps=1e-06)[source]

Return the noise sensitivity CS, the map from measurement noise n to the command u.

Same arguments as sensitivity(). In the loop, n reaches u as -CS: the sign of the negative feedback is left out.

Parameters:
  • method (str)

  • eps (float)

Region of attraction of a closed loop: a quadratic Lyapunov certificate V = (x − x̄)ᵀ P (x − x̄) ≤ c.

minilink.analysis.lyapunov.METHODS = ('quadratic', 'sos')

Families of Lyapunov function this tool can build.

minilink.analysis.lyapunov.region_of_attraction(sys, x_bar=None, u_bar=None, t=0.0, params=None, *, method='quadratic', Q=None, window=None, samples=None, search='auto')[source]

Certify a region of attraction of sys around an equilibrium.

P solves the Lyapunov equation at the equilibrium the tool finds; the level c is the largest sampled sublevel set on which V̇ < 0 inside the state box, so it is a sharp estimate, not a proof, and verify() is its simulation counter-check.

Parameters:
  • sys (DynamicSystem) – Autonomous system, normally a closed loop such as ctl @ plant.

  • x_bar (array of shape (n,), optional) – Guess of the equilibrium; refined by find_equilibrium(). Defaults to the nominal state. The refined point is what the certificate reports: a law tuned by learning often settles beside the state it was aimed at.

  • u_bar (optional) – Held input, time, and parameter set, as in every analysis tool.

  • t (optional) – Held input, time, and parameter set, as in every analysis tool.

  • params (optional) – Held input, time, and parameter set, as in every analysis tool.

  • method ({"quadratic", "sos"}) – Family the Lyapunov function comes from. "quadratic" solves the Lyapunov equation of the linearization, the textbook construction and the only one implemented; "sos" is reserved for a sum-of-squares certificate, which would also make the level a proof rather than an estimate.

  • Q (array of shape (n, n), optional) – Right-hand side of the Lyapunov equation; the identity by default. A larger Q in one direction weighs that direction more in V.

  • window (BoxSet or (lower, upper), optional) – Box the sublevel search covers. The default searches twice, first on the natural scale of V and then on a box sized to that answer, so the samples land where the level is decided.

  • samples (int, optional) – Sampling budget per pass (default 201**2 in one or two states, 50 000 above).

  • search ({"auto", "grid", "random"}) – How the sublevel set is sampled. "grid" sweeps a regular grid, the default in one or two states and what the plots draw; "random" uses a low-discrepancy sequence, which scales better.

Return type:

LyapunovCertificate

Raises:
  • ValueError – If the linearization is not Hurwitz: an unstable equilibrium has no region of attraction to certify.

  • NotImplementedError – For method="sos".

class minilink.analysis.lyapunov.LyapunovCertificate(sys, x_bar, P, level, Q, poles, window, limiting_state, sample_spread=0.0, method='quadratic', u_bar=None, t=0.0, params=None, backend=None, evaluator=None)[source]

Lyapunov function and the level that certifies a region of attraction.

Returned by region_of_attraction(). The certified region is {x : V(x) <= level}: every state in it converges to x_bar.

Parameters:
  • sys (object)

  • x_bar (ndarray)

  • P (ndarray)

  • level (float)

  • Q (ndarray)

  • poles (ndarray)

  • window (BoxSet)

  • limiting_state (ndarray)

  • sample_spread (float)

  • method (str)

  • u_bar (ndarray)

  • t (float)

  • params (object)

  • backend (str)

  • evaluator (object)

sys

The closed loop the certificate belongs to.

Type:

DynamicSystem

x_bar

Equilibrium the region is centered on. It is found, not assumed, so it may differ from the operating point that was asked for.

Type:

array of shape (n,)

P

Lyapunov matrix of the quadratic family, V(x) = (x - x_bar)ᵀ P (x - x_bar).

Type:

array of shape (n, n)

level

Certified value c of V.

Type:

float

Q

Right-hand side of AᵀP + PA = -Q.

Type:

array of shape (n, n)

poles

Eigenvalues of the linearization, kept as evidence.

Type:

array of shape (n,)

window

Box the sublevel search covered.

Type:

BoxSet

limiting_state

Sampled state that pinned the level: the nearest place, in V, where the loop stops making progress. Reading the control input there usually explains the certificate.

Type:

array of shape (n,)

sample_spread

Relative disagreement between the levels two halves of the samples give. Near zero means the sweep had enough samples to decide; see sample_limited.

Type:

float

method

Family the Lyapunov function came from (see METHODS).

Type:

str

V(x)[source]

Lyapunov function at one state (n,) or a stack of states (N, n).

V_dot(x)[source]

Derivative of V along the nonlinear dynamics, same shapes as V().

contains(x)[source]

True where the state lies in the certified region.

property extent: ndarray

Half-width of the certified region along each state.

slice_extent(x_axis=0, y_axis=1)[source]

Half-widths of the region’s slice through the equilibrium in two states.

With the other states pinned at x_bar the certified set cuts a smaller ellipse than extent, which is its shadow on each axis. The slice is what a phase-plane plot draws.

Parameters:
  • x_axis (int)

  • y_axis (int)

Return type:

ndarray

property sample_limited: bool

True when the level moved between two halves of the samples.

The sublevel search is only as good as its coverage, and coverage thins out fast with the number of states. When this is True the level is an optimistic guess rather than a sharp estimate: raise samples, narrow window, or read it as indicative only.

property rate: float

the natural time scale of the loop.

Type:

Slowest linearized decay rate

verify(n=200, tf=None, tol=None)[source]

Integrate n states drawn inside the certified region: all must converge.

States are drawn uniformly in the ellipsoid {V <= level}. tf defaults to ten time constants of the slowest linearized mode and tol to a hundredth of the certified extent. A failure means the sublevel search was too coarse, or the model is not what it seems, and the report carries the offending state.

Parameters:
  • n (int)

  • tf (float | None)

  • tol (float | None)

plot(x_axis=0, y_axis=1, *, basin=False, detail=True, verified=0, trajectories=(), n=201, limits=None, ax=None, show=True)[source]

Phase-plane slice of the certified region. See plot_region_of_attraction().

Parameters:
  • x_axis (int)

  • y_axis (int)

  • basin (bool)

  • detail (bool)

  • verified (int)

  • n (int)

  • show (bool)

dynamics()[source]

f_many(X) -> dX: the closed-loop dynamics on a stack of states.

rollout(tf)[source]

rollout(X0) -> X_final: integrate a stack of states for tf.

Parameters:

tf (float)

class minilink.analysis.lyapunov.VerificationReport(converged, final_distance, counterexample=None)[source]

Simulation’s verdict on a certificate: every trial inside it must converge.

Parameters:
  • converged (ndarray)

  • final_distance (ndarray)

  • counterexample (ndarray | None)

property holds: bool

True when every sampled state converged to the equilibrium.

minilink.analysis.lyapunov.plot_region_of_attraction(certificate, x_axis=0, y_axis=1, *, basin=False, detail=True, verified=0, trajectories=(), n=201, limits=None, ax=None, show=True)[source]

Draw a slice of a certified region: the level set, and what pins it.

The two swept states are x_axis and y_axis; the others are pinned at the equilibrium, so what is drawn is the slice of the certified set through x_bar (smaller than its shadow on the two axes whenever the states are coupled).

Layers, each in the legend: basin=True shades the states of the slice that really converge, by simulation, which shows how much a quadratic V gives up. detail=True adds the curve where V stops decreasing, and the sample that set the level when it lies on this slice — on a slice the two need not touch, since the binding state usually sits off the plane. verified=200 draws the states LyapunovCertificate.verify() would test, marked by whether they converged. They lie inside the certified set, because the claim is what is on trial and a state there that fails is a counterexample; whether states outside converge is what basin answers. Two-state loops only, where the draw lies on the plane. trajectories is a sequence of initial states to overlay.

limits is an explicit ((x_min, x_max), (y_min, y_max)) window; the default frames the slices being drawn.

Parameters:
  • certificate (LyapunovCertificate)

  • x_axis (int)

  • y_axis (int)

  • basin (bool)

  • detail (bool)

  • verified (int)

  • n (int)

  • show (bool)

minilink.analysis.lyapunov.EVEN = slice(0, None, 2)

The two disjoint halves the level search scores itself on.