sid — Examples Specification¶
Version: 1.0.1
Date: 2026-04-12
Reference: Companion to SPEC.md. Where SPEC.md defines
the binding behavior of the algorithmic functions, this document defines
the binding structure of the example suite shipped with every language
port.
Implementation status. This specification is authored against the Python example suite on branch
claude/smd-util-helpers(Phase B). The MATLAB port, Julia port, and any future language port MUST conform to this document. The Python port is the v1.0.0 reference implementation of this spec.
0. Preamble¶
0.1 Scope¶
This document is the source of truth for the 11 examples that ship
with the sid toolbox in {lang}/examples/. It specifies:
- The physical plants the examples use and their parameters (§1).
- The mathematical definition of the
util_msd*helper functions that build the plants (§2). - For each example: the goal, the plant, the excitation, the required
pedagogical sections, the
sid.*function calls that MUST appear, and the plots and prints that MUST be produced (§3). - The conventions every port must follow (§4).
- The definition of "equal" across languages (§5).
- Versioning and change control rules (§6).
0.2 Requirement levels¶
This document uses the RFC 2119 / BCP 14 key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY. MUST statements are binding — any implementation that violates one is non-conformant. SHOULD statements are recommendations: violations must be justified and documented. MAY is purely informational.
Throughout this document, MUST statements apply to every language port.
0.3 Binding vs advisory aspects¶
| Aspect of an example | Requirement level |
|---|---|
| Plant parameters (m, k, c, Ts, N) | MUST match §1 |
| Plant topology (n-mass chain, SDOF, Duffing) | MUST |
| Ordered list of pedagogical sections | MUST |
| Section topic (what each section teaches) | MUST |
| Exact section-header prose | SHOULD |
| Exact markdown / block-comment body prose | SHOULD |
sid.* function calls invoked in each section |
MUST |
| Options passed to those calls (window size, grid, λ, etc.) | MUST |
| Plot kinds (Bode, spectrum, map, spectrogram, line chart) | MUST |
| Plot title topic and axis label quantities | MUST |
| Plot title/label exact wording | SHOULD |
| Plot cosmetics (colors, line widths, figure sizes, fonts) | MAY |
| Printed line semantics (what each print conveys) | MUST |
| Exact printed string format | SHOULD |
| Helper function mathematical definition | MUST |
| Helper function names per language | SHOULD (see §2) |
| RNG seed choice | MAY |
| Numerical values produced at runtime | MAY |
In short: a port MUST match the pedagogical skeleton and the sid.*
call graph; it MAY use language-idiomatic phrasing, styling, and RNG
seeds.
0.4 Why a unified spring-mass-damper plant family?¶
Every example in this specification is built on a common family of spring-mass-damper (SMD) plants, ranging from a 1-DoF oscillator to a 3-mass chain, with variants that are linear, linearly time-varying, and nonlinear (Duffing hardening). Using one physical plant family across the tutorial suite keeps the narrative concrete and cumulative: a reader working through the examples sees the same physical systems again and again, picking up new analysis tools on familiar territory.
Arbitrary lfilter coefficients or handcrafted state-space matrices
are explicitly rejected — they have no physical interpretation, their
pole locations are chosen numerically rather than from first
principles, and they make the pedagogical content artificial.
0.5 Document structure¶
| Section | Contents |
|---|---|
| §1 | Plant catalog (five plants A–E) |
| §2 | Helper API (util_msd* mathematical contract) |
| §3 | Example catalog (eleven examples) |
| §4 | Conventions (auto-discovery, self-containment, RNG, naming) |
| §5 | Cross-language "equivalence" (what "equal" means) |
| §6 | Versioning and change control |
| §7 | References |
1. Plant Catalog¶
Every example in this specification uses one or more of the five plants defined below. Each plant has a fixed set of physical parameters; the parameters MUST NOT be changed by a language port without a corresponding version bump of this specification (see §6).
The plants are referred to by their catalog letter (Plant A, Plant B, …) throughout §3. When an example refers to a plant it MUST use the parameters defined here.
1.1 Plant A — 1-DoF spring-mass-damper (SDOF)¶
Physical story. A single mass connected to a fixed wall through a linear spring and a viscous damper — the canonical textbook oscillator. A force is applied at the mass and the measured output is the mass position.
Continuous-time dynamics.
State vector. [x; v] with x position and v = x' velocity.
Parameters.
| Symbol | Value | Units | Derived |
|---|---|---|---|
m |
1.0 |
kg | |
k |
100.0 |
N/m | ω_n = sqrt(k/m) = 10 rad/s ≈ 1.59 Hz |
c |
2.0 |
N·s/m | ζ = c / (2 sqrt(k m)) = 0.1, Q = 5 |
F |
[[1]] |
(n×q) | force applied directly at the single mass |
Ts |
0.01 |
s | fs = 100 Hz, Nyquist = π/Ts ≈ 314 rad/s |
Discretization testpoint. Under util_msd(m, k, c, F, Ts) (see §2.1):
Ad ≈ [[ 0.9950372995, 0.0098841706],
[-0.9884170600, 0.9752689583]]
Bd ≈ [[ 4.9627005463e-05],
[ 9.8841705996e-03]]
A conforming implementation MUST reproduce these to at least 1e-9
relative tolerance for this exact (m, k, c, F, Ts) tuple.
Used by. §3.1 siso, §3.2 etfe, §3.5 method_comparison, §3.9
ltv_disc (main LTV section, plus the Duffing linearization reuses
Plant A's m, k_lin, c).
1.2 Plant B — 2-mass chain¶
Physical story. Two equal masses in series, anchored to a wall through the first spring. Naturally exhibits a 2×2 force-to-position MIMO response with cross-coupling through the second spring.
Topology. wall──k₁──m₁──k₂──m₂
Continuous-time dynamics.
with M = diag(m) and tridiagonal K, C built from k_spring,
c_damp per §2.1.
State vector. [x₁, x₂, v₁, v₂].
Parameters.
| Symbol | Value | Units | Notes |
|---|---|---|---|
m |
[1, 1] |
kg | equal masses |
k |
[100, 80] |
N/m | k[0] wall→m₁, k[1] m₁→m₂ |
c |
[2, 2] |
N·s/m | |
Ts |
0.01 |
s |
Normal modes. ω_modes ≈ [5.97, 14.98] rad/s (obtained as square
roots of the sorted eigenvalues of M⁻¹K).
Input distribution matrix F. Depends on example:
- For
mimo(§3.6),coherence(§3.4) sections that use a second input for an unmodeled disturbance:F = I₂(two independent force channels). - For
freq_map(§3.7),multi_trajectory(§3.10 sections 1–2, 4),output_cosmic(§3.11):F = [[1], [0]](force at mass 1 only).
Discretization testpoint. Under util_msd(m=[1,1], k=[100,80],
c=[2,2], F=[[1],[0]], Ts=0.01):
Ad ≈ [[ 9.9116061487e-01, 3.9125769853e-03, 9.7740440835e-03, 1.1093285197e-04],
[ 3.9060177716e-03, 9.9605789532e-01, 1.1093285197e-04, 9.8882565423e-03],
[-1.7504533069e+00, 7.7304889853e-01, 9.5228630424e-01, 2.3238799448e-02],
[ 7.7109261003e-01, -7.8218589523e-01, 2.3238799448e-02, 9.7650324794e-01]]
Bd ≈ [[4.9268081419e-05],
[3.6086910233e-07],
[9.7740440835e-03],
[1.1093285197e-04]]
A conforming implementation MUST reproduce these to at least 1e-9
relative tolerance.
Used by. §3.4 coherence, §3.6 mimo, §3.7 freq_map, §3.10
multi_trajectory (sections 1, 2, 4), §3.11 output_cosmic.
1.3 Plant C — 3-mass chain¶
Physical story. Three masses in series, anchored to a wall through the first spring. Three well-separated bending-mode-like resonances in the bottom decade of the spectrum; the canonical plant for demonstrating frequency-dependent resolution.
Topology. wall──k₁──m₁──k₂──m₂──k₃──m₃
Parameters.
| Symbol | Value | Units | Notes |
|---|---|---|---|
m |
[1, 1, 1] |
kg | equal masses |
k |
[300, 200, 100] |
N/m | stiffness decreases toward the free end |
c |
[8, 8, 8] |
N·s/m | |
F |
[[1], [0], [0]] |
(3×1) | force at mass 1 only |
Ts |
0.01 |
s |
Normal modes. ω_modes ≈ [6.45, 15.15, 25.08] rad/s.
Physical interpretation. A slender cantilever-like structure with diminishing stiffness toward the tip — the three modes are close enough to challenge a short-window BT estimator but far enough apart to be resolved by a long window or by frequency-dependent resolution.
Discretization testpoint. Under util_msd:
Ad ≈ [[ 0.97662704, 0.00906461, 0.00012791, 0.00917488, 0.00038846, 0.00001056],
[ 0.00882163, 0.98617894, 0.00460777, 0.00038846, 0.00921581, 0.00038430],
[ 0.00024562, 0.00448490, 0.99526154, 0.00001056, 0.00038430, 0.00960491],
[-4.50974587, 1.71949205, 0.03779075, 0.83293672, 0.07633266, 0.00315117],
[ 1.64892960, -2.64861857, 0.88315012, 0.07633266, 0.84490819, 0.07525978],
[ 0.07158313, 0.84731120, -0.92206093, 0.00315117, 0.07525978, 0.92149667]]
Bd ≈ [[4.7268e-05], [1.3055e-06], [2.6458e-08],
[9.1749e-03], [3.8846e-04], [1.0555e-05]]
Trailing digits shown to the spec's working precision. A conforming
implementation MUST reproduce these to at least 1e-6 relative
tolerance.
Used by. §3.3 freq_dep_res.
1.4 Plant D — SDOF for spectrogram (high-frequency SDOF)¶
Physical story. A 1-DoF oscillator scaled to a structural-acoustic
frequency range so it can be driven by an audible chirp and analyzed
with a short-time FFT. Models a stiff mechanical resonator
(ω_n ≈ 32 Hz) with moderate damping — the sort of plant you would
hit with an impact hammer in a lab modal test.
Parameters.
| Symbol | Value | Units | Derived |
|---|---|---|---|
m |
1.0 |
kg | |
k |
4·10⁴ |
N/m | ω_n = 200 rad/s ≈ 31.83 Hz |
c |
20.0 |
N·s/m | ζ = 0.05, Q = 10 |
F |
[[1]] |
(1×1) | force at the mass |
Ts |
0.001 |
s | fs = 1000 Hz, Nyquist = 500 Hz |
Discretization testpoint. Under util_msd:
Ad ≈ [[ 0.98019872, 0.00098348],
[-39.33916499, 0.96052913]]
Bd ≈ [[ 4.9503206884e-07],
[ 9.8347912463e-04]]
Relative tolerance 1e-9.
Used by. §3.8 spectrogram, §3.10 multi_trajectory (section 3 —
chirp-in-noise).
1.5 Plant E — Duffing hardening SDOF (nonlinear)¶
Physical story. A 1-DoF oscillator with a cubic stiffness term superposed on the linear spring. Physically models a hardening rubber mount, a geometrically-nonlinear thin plate, or a pre-stressed structure whose restoring force grows faster than linearly with displacement.
Continuous-time dynamics.
State vector. [x; v].
Parameters.
| Symbol | Value | Units | Notes |
|---|---|---|---|
m |
1.0 |
kg | same mass as Plant A |
k_lin |
100.0 |
N/m | linear component (Plant A's spring) |
k_cub |
1·10⁵ |
N/m³ | hardening; dominates at |x| ≳ 0.03 m |
c |
2.0 |
N·s/m | linear damping (Plant A's damper) |
F |
[[1]] |
(1×1) | force at the mass |
Ts |
0.01 |
s | same as Plant A |
Effective natural frequency (small-signal linearization around
displacement x):
At x = 0: ω_eff = 10 rad/s (Plant A's baseline).
At x = 0.05 m: ω_eff ≈ 21.8 rad/s (a ~2× frequency shift).
Integration. This plant cannot be discretized with exact ZOH
(expm) because it is nonlinear. It MUST be simulated with the RK4
integrator specified in §2.3, operating on the continuous-time RHS.
Integration testpoint. With m=1, k_lin=100, k_cub=1e5,
c=2, F=[[1]], Ts=0.01, substeps=1, initial state x0 = 0,
and impulse input u = [1, 0, 0, 0, 0] (one step):
x_traj[1] ≈ [4.9626666641e-05, 9.8841633080e-03]
x_traj[2] ≈ [1.4707706719e-04, 9.5906651394e-03]
x_traj[3] ≈ [2.4114284377e-04, 9.2080973779e-03]
x_traj[4] ≈ [3.3096036948e-04, 8.7419986134e-03]
x_traj[5] ≈ [4.1572503242e-04, 8.1986213117e-03]
Relative tolerance 1e-6. A conforming implementation MUST also
satisfy the reduction-to-linear test: setting k_cub = 0 and
calling the RK4 integrator with substeps ≥ 4 MUST reproduce the
exact ZOH result from §2.1 on the same input to within 1e-8
absolute (fifth-order RK4 truncation error at Ts = 0.01).
Used by. §3.7 freq_map (Duffing section), §3.9 ltv_disc
(Duffing linearization section).
2. Helper API (util_msd*)¶
Every language port MUST provide three helper functions that build the
plants defined in §1 from their physical parameters. The mathematical
definitions below are binding. Function names follow the local
language's naming convention (util_msd in Python, util_msd.m in
MATLAB, etc.); what matters is that exactly three functions with the
semantics defined here are available to the example suite.
The three helpers are:
- §2.1 The LTI helper — builds
(Ad, Bd)for a time-invariant n-mass chain via exact zero-order-hold discretization. - §2.2 The LTV helper — builds per-step
(Ad(k), Bd(k))stacks for chains whose parameters vary in time. - §2.3 The nonlinear simulator — integrates an n-mass chain with Duffing-style cubic stiffness via fixed-step RK4.
2.1 LTI helper¶
2.1.1 Construction of K and C¶
Given physical parameters m, k_spring, c_damp (all length-n
vectors with positive entries), construct n × n tridiagonal matrices:
K[i, i] = k_spring[i] + k_spring[i+1] for i = 0, …, n-2
K[n-1, n-1] = k_spring[n-1]
K[i, i+1] = K[i+1, i] = -k_spring[i+1] for i = 0, …, n-2
with C built by the same pattern from c_damp. This is the standard
chain topology wall──k₁──m₁──k₂──m₂──…──kₙ──mₙ, where k_spring[0]
is the wall-to-mass-1 spring and k_spring[i] (for i ≥ 1) connects
mass i to mass i+1.
2.1.2 Continuous-time state-space model¶
With M = diag(m), M⁻¹ = diag(1/m), and the force distribution
matrix F of shape n × q:
Ac = [[ 0_{n×n}, I_{n×n} ],
[ -M⁻¹ K, -M⁻¹ C ]] (shape 2n × 2n)
Bc = [[ 0_{n×q} ],
[ M⁻¹ F ]] (shape 2n × q)
The state vector is [x₁, x₂, …, xₙ, v₁, v₂, …, vₙ]: positions first,
velocities second.
2.1.3 Exact zero-order-hold discretization¶
For sample time Ts > 0:
The implementation MUST use a library-quality matrix exponential
(expm in MATLAB/Octave/SciPy). Taylor-series or Pade truncations
below 6th order are non-conformant because the discretization testpoints
in §1 are tabulated to 1e-9 relative tolerance.
2.1.4 Interface (binding)¶
Inputs:
| Name | Shape | Type | Constraint |
|---|---|---|---|
m |
(n,) |
real | entries > 0 |
k_spring |
(n,) |
real | entries > 0 |
c_damp |
(n,) |
real | entries ≥ 0 |
F |
(n, q) |
real | — |
Ts |
scalar | real | Ts > 0 |
Outputs:
| Name | Shape | Type |
|---|---|---|
Ad |
(2n, 2n) |
real |
Bd |
(2n, q) |
real |
The function MUST reject mismatched shapes (e.g., len(k_spring) ≠
len(m)) with an error. A one-column F passed as a (n,) vector
MAY be auto-reshaped.
2.1.5 Numerical testpoints¶
These are the same testpoints tabulated in §1. A conforming LTI helper MUST reproduce them:
| Plant | Tuple | Tolerance |
|---|---|---|
| Plant A | util_msd([1], [100], [2], [[1]], 0.01) |
rel 1e-9 |
| Plant B | util_msd([1,1], [100,80], [2,2], [[1],[0]], 0.01) |
rel 1e-9 |
| Plant C | util_msd([1,1,1], [300,200,100], [8,8,8], [[1],[0],[0]], 0.01) |
rel 1e-6 |
| Plant D | util_msd([1], [4e4], [20], [[1]], 0.001) |
rel 1e-9 |
Ports SHOULD also include n = 2 and n = 5 regression tests (not
tabulated here) whose expected outputs are computed from the
continuous closed form against the modal decomposition.
2.2 LTV helper¶
2.2.1 Contract¶
The LTV helper builds a stack of per-step (Ad(k), Bd(k)) matrices for
an n-mass chain whose parameters vary over N sample steps. It MUST
be semantically equivalent to calling the LTI helper (§2.1) once per
time index:
for k in 0..N-1:
Ad_stack[:, :, k], Bd_stack[:, :, k] = util_msd(
m(k), k_spring(k), c_damp(k), F(k), Ts
)
A conforming implementation MAY take a fast path when all of m,
k_spring, c_damp, F are time-invariant (call util_msd once and
broadcast the result along a newly introduced time axis).
2.2.2 Interface (binding)¶
Inputs:
| Name | Allowed shapes | Notes |
|---|---|---|
m |
(n,) or (n, N) |
constant or per-step |
k_spring |
(n,) or (n, N) |
constant or per-step |
c_damp |
(n,) or (n, N) |
constant or per-step |
F |
(n, q) or (n, q, N) |
constant or per-step |
Ts |
scalar | Ts > 0 |
N (optional) |
integer | required only when every other input is constant |
N inference. The function MUST infer N from the first time-
varying input it sees. If every input is constant, the function MUST
require the caller to pass N explicitly; in that case the result is
the LTI pair replicated N times along the new axis.
Dimension consistency. The function MUST raise an error if the
time axes of two different time-varying inputs disagree (e.g.,
k_spring has 80 columns but c_damp has 100).
Outputs:
| Name | Shape |
|---|---|
Ad_stack |
(2n, 2n, N) |
Bd_stack |
(2n, q, N) |
2.2.3 Testpoint¶
With m = [1], c_damp = [2], F = [[1]], Ts = 0.01, and
k_spring a single-row (1, 10) array built as
linspace(200, 50, 10) (1-DoF with k ramping 200 → 50 over 10
samples):
Ad_stack[:, :, 0] ≈ [[ 0.9900828576, 0.0098676943],
[-1.9735388683, 0.9703474690]]
Ad_stack[:, :, -1] ≈ [[ 0.9975176169, 0.0098924149],
[-0.4946207456, 0.9777327870]]
Relative tolerance 1e-9.
2.2.4 LTI collapse check¶
Calling the LTV helper with 1-D m, k_spring, c_damp, 2-D F,
and N = 50 MUST produce an Ad_stack whose every slice
Ad_stack[:, :, k] is bit-identical (not merely numerically equal)
to util_msd(m, k_spring, c_damp, F, Ts). A conforming implementation
SHOULD take the fast path in this case.
2.3 Nonlinear simulator (Duffing-style cubic stiffness)¶
2.3.1 Continuous-time RHS¶
Given a state vector state = [x; v] of length 2n with
x = state[0:n] (positions) and v = state[n:2n] (velocities), and
an instantaneous input vector u_k of length q, the RHS is:
net_force[0] = -( k_lin[0] · x[0] + k_cub[0] · x[0]³ + c_damp[0] · v[0] )
for i in 1..n-1:
Δx = x[i] - x[i-1]
Δv = v[i] - v[i-1]
f_i = k_lin[i] · Δx + k_cub[i] · Δx³ + c_damp[i] · Δv
net_force[i-1] += f_i
net_force[i] -= f_i
net_force += F · u_k (external force injection)
acc = net_force / m (element-wise)
d state / dt = [v; acc]
This implements the chain topology of §1 with a cubic stiffness
contribution on each spring. Setting k_cub = zeros(n) reduces the
RHS exactly to the linear dynamics of §2.1.2.
2.3.2 Integration — fixed-step RK4 with zero-order-hold input¶
Let h = Ts / substeps, where substeps is an integer ≥ 1.
For each sample step k = 0, 1, …, N-1, hold the input u[k]
constant over the interval [k·Ts, (k+1)·Ts) and take substeps
classic RK4 steps of size h:
for _ in 1..substeps:
k₁ = rhs(state, u[k])
k₂ = rhs(state + 0.5·h·k₁, u[k])
k₃ = rhs(state + 0.5·h·k₂, u[k])
k₄ = rhs(state + h·k₃, u[k])
state = state + (h/6) · (k₁ + 2 k₂ + 2 k₃ + k₄)
The rhs function is the one defined in §2.3.1. The same u[k] is
used for all four stage evaluations (zero-order hold).
The implementation MUST use this exact tableau. Variable-step integrators
(ode45, RK45 with adaptive step) are non-conformant because their
outputs would differ from the tabulated testpoint in §1.5.
2.3.3 Interface (binding)¶
Inputs:
| Name | Shape | Notes |
|---|---|---|
m |
(n,) |
entries > 0 |
k_lin |
(n,) |
linear spring constants |
k_cub |
(n,) |
cubic spring constants; zeros → linear plant |
c_damp |
(n,) |
entries ≥ 0 |
F |
(n, q) |
force distribution |
Ts |
scalar | Ts > 0 |
u |
(N, q) |
input time series |
x0 |
(2n,) |
initial state (MAY default to zeros) |
substeps |
integer | RK4 sub-steps per Ts (MAY default to 1) |
Output:
| Name | Shape |
|---|---|
x_traj |
(N + 1, 2n) |
x_traj[0] MUST equal x0; x_traj[k+1] is the state after stepping
through u[k] for one sample period.
2.3.4 Numerical testpoint — impulse response¶
With m = [1], k_lin = [100], k_cub = [1e5], c_damp = [2],
F = [[1]], Ts = 0.01, u = [[1], [0], [0], [0], [0]], x0 = 0,
substeps = 1:
x_traj[1] ≈ [4.9626666641e-05, 9.8841633080e-03]
x_traj[2] ≈ [1.4707706719e-04, 9.5906651394e-03]
x_traj[3] ≈ [2.4114284377e-04, 9.2080973779e-03]
x_traj[4] ≈ [3.3096036948e-04, 8.7419986134e-03]
x_traj[5] ≈ [4.1572503242e-04, 8.1986213117e-03]
Relative tolerance 1e-6.
2.3.5 Reduction-to-linear check¶
Setting k_cub = zeros(n) and calling the nonlinear simulator with
substeps ≥ 4 MUST reproduce the ZOH result of §2.1 on the same
linear (m, k_lin, c_damp, F, Ts, u) tuple to within 1e-8 absolute
tolerance. This catches tableau errors and chain-assembly mismatches
between the LTI and nonlinear helpers.
3. Example Catalog¶
This section specifies the eleven examples that MUST ship with every
language port. Each example has a canonical identifier (e.g.,
siso, freq_dep_res) that is language-neutral. The file-name mapping
for each identifier is in §4.5.
Per §0.3, the structure below is binding: the plant, excitation,
section order, sid.* calls, and plot/print outputs. The exact prose
of markdown cells or block comments is advisory.
Every example MUST:
- Start with a title cell or comment block carrying the canonical identifier and a one-paragraph description of the goal.
- Import the local
sidmodule and the localutil_msd*helpers (see §2). Additional imports (numpy/scipy, plotting library, language-idiomatic tools) are allowed. - Generate its own data via the helper API — no external data files.
- Produce the required plots and prints.
- Be reproducible within a single language invocation: fix an RNG seed at the top of the data-generation section.
3.1 siso — SISO frequency response with Blackman-Tukey¶
Goal. Estimate the frequency response of a physical SDOF oscillator
from a noisy input/output record using freq_bt, and walk through the
standard validation workflow (window-size comparison, detrending,
residual analysis, fit comparison, time-series mode).
Plant. Plant A (§1.1) with N = 2048 samples.
Excitation. Unit-variance Gaussian white force u[k] ~ N(0, 1).
Measured output y[k] = x₁[k] + ε[k] where ε[k] ~ N(0, 2·10⁻⁴) is
additive sensor noise.
Required sections (binding order, SHOULD use titles near the wording in the first column):
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | Generate test data | Build (Ad, Bd) via §2.1, simulate the state recursion, measure position with added sensor noise. |
| 2 | Estimate frequency response using Blackman-Tukey | Call freq_bt with a dense custom frequency grid and a window size large enough to resolve the narrow resonance. |
| 3 | Plot Bode diagram | Display magnitude and phase of the estimate with confidence bands. |
| 4 | Plot noise spectrum | Display the BT noise spectrum estimate. |
| 5 | Compare different window sizes | Demonstrate how the Hann window length trades bias for variance; show that a short window smears the narrow resonance. |
| 6 | Preprocessing: detrend data before estimation | Corrupt the signals with a linear drift and DC offset; show that detrend removes the low-frequency bias. |
| 7 | Model validation: residual analysis | Call residual and print whiteness/independence pass/fail. Ports MUST include a narrative note that the non-parametric BT estimator has finite-window bias on narrow resonances, so the whiteness test is expected to FAIL on this plant — the test is reporting unmodelled residual structure, not a bug in the example. Readers should treat the FAIL as the pedagogical point of the section. |
| 8 | Model validation: compare predicted vs measured | Call compare and print NRMSE fit. |
| 9 | Time-series mode (no input) | Re-simulate the plant and hand only the output to freq_bt (with u = None); show the resonance still appears in the output spectrum. |
Required sid.* invocations.
| # | Function | Options (MUST) |
|---|---|---|
| 2 | freq_bt |
window_size = 200, custom frequency grid linspace(0.005, π, 512), sample_time = Ts |
| 3 | bode_plot |
on the result from #2 |
| 4 | spectrum_plot |
on the result from #2 |
| 5 | freq_bt |
three calls with window_size ∈ {50, 100, 300}, same frequency grid as #2 |
| 6 | detrend |
applied to both the drifted output and the offset input; then freq_bt on the detrended pair with the same options as #2 |
| 7 | residual |
on the #2 result with (y, u) |
| 8 | compare |
on the #2 result with (y, u) |
| 9 | freq_bt |
called with the output only (time-series mode), window_size = 200, same frequency grid |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| Plot (Bode 2-panel) | 3 | Title conveys "Bode diagram / freq_bt estimate"; x-axis frequency, y-axes magnitude (dB) and phase (rad or deg); confidence band shown. |
| Plot (spectrum 1-panel) | 4 | Title conveys "noise spectrum"; x-axis frequency, y-axis power (dB). |
| Plot (window comparison, magnitude) | 5 | Title conveys "effect of window size on resonance resolution"; three curves labeled with M values; a dotted/dashed vertical reference line at the true ω_n = 10 rad/s. |
| 6 | Two lines: "without detrend: max | |
| 7 | Two lines: "whiteness test: PASS/FAIL", "independence test: PASS/FAIL". | |
| 8 | One line: "NRMSE fit: …%". | |
| Plot (spectrum) | 9 | Title conveys "SDOF output spectrum (time-series mode)". |
3.2 etfe — Empirical transfer function estimate¶
Goal. Estimate the frequency response of Plant A via freq_etfe
and explore the smoothing vs resolution trade-off. Contrast with the
BT estimator on the same plant (§3.1).
Plant. Plant A (§1.1) with N = 2048 samples. Same (m, k, c) as
§3.1. This is intentional: the pair of examples forms a BT-vs-ETFE
comparison on one physical system.
Excitation. Same as §3.1.
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | Generate test data | Simulate Plant A; compute and stash the exact discrete transfer function G(e^{jω}) = C (e^{jω} I − Ad)⁻¹ Bd for later overlays. |
| 2 | Basic ETFE (no smoothing) | Call freq_etfe with default options; plot the result. Note that response_std is NaN for ETFE. |
| 3 | Effect of smoothing | Call freq_etfe with three smoothing levels; overlay against the true TF reference. |
| 4 | Known FIR system: pure delay | On a synthetic y[k] = u[k-1] pair, call freq_etfe and verify |
| 5 | Time-series mode: periodogram | With u = None on a fresh simulation of Plant A, show the output periodogram. |
| 6 | Custom frequency grid and Hz display | Call freq_etfe with a log-spaced grid and bode_plot with frequency_unit='Hz'. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 2 | freq_etfe |
default (no smoothing), sample_time = Ts |
| 2 | bode_plot |
on the #2 result |
| 3 | freq_etfe |
three calls with smoothing ∈ {1, 11, 21}, sample_time = Ts |
| 4 | freq_etfe |
on the one-sample-delay pair (sanity check; no plant simulation) |
| 5 | freq_etfe |
with only output data (u = None) |
| 5 | spectrum_plot |
on the #5 result |
| 6 | freq_etfe |
smoothing = 11, custom log-spaced frequency grid covering [0.005, π] rad/sample |
| 6 | bode_plot |
with frequency_unit = 'Hz' |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| Plot (Bode) | 2 | Title conveys "ETFE raw (no smoothing)". |
| Plot (magnitude overlay) | 3 | Three ETFE curves at different smoothing levels plus the true-TF dashed reference; x-axis rad/sample, y-axis dB. |
| Plot (2-panel magnitude + phase) | 4 | Top: |G| ≈ 1 for the pure delay; bottom: phase vs −ω reference. |
| Plot (spectrum) | 5 | Title conveys "SDOF output periodogram". |
| Plot (Bode) | 6 | Title conveys "ETFE with log frequency grid (Hz)"; x-axis labeled Hz. |
3.3 freq_dep_res — Frequency-dependent resolution¶
Goal. Demonstrate freq_btfdr on a multi-mode plant where a short
fixed BT window smears the mode structure but a per-frequency window
resolves it cleanly. Show both the scalar-R and vector-R cases.
Plant. Plant C (§1.3) with N = 6000 samples.
Excitation. Unit-variance white force at mass 1. Measurement noise
ε ~ N(0, 5·10⁻⁴) on x₁.
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | Generate test data | Simulate Plant C; precompute the true TF at the default BT grid for use as a reference. |
| 2 | Fixed-window freq_bt: the resolution-variance trade-off |
Overlay freq_bt at M = 15 and M = 80 against the true TF; show the short window smears modes and the long window is noisier. |
| 3 | Scalar resolution with freq_btfdr |
Call freq_btfdr with a scalar resolution = 0.2 and plot the Bode. |
| 4 | Per-frequency resolution vector | Build an R_vec ramping from fine (0.1) at low frequencies to coarse (1.5) at high frequencies; call freq_btfdr(..., resolution=R_vec); plot window_size vs frequency (top) and the magnitude estimate vs true TF (bottom). |
| 5 | Compare BT vs BTFDR side by side | Overlay freq_bt(M=30) and freq_btfdr(R=0.3) against the true TF. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 2 | freq_bt |
two calls: window_size=15, window_size=80; sample_time = Ts |
| 3 | freq_btfdr |
resolution = 0.2, sample_time = Ts |
| 3 | bode_plot |
on the #3 result |
| 4 | freq_btfdr |
scalar call first (to get nf), then resolution = linspace(0.1, 1.5, nf) |
| 5 | freq_bt |
window_size = 30 |
| 5 | freq_btfdr |
resolution = 0.3 |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| Plot (magnitude overlay) | 2 | Two BT curves + true-TF dashed reference; title conveys "fixed window: resolution vs variance trade-off". |
| Plot (Bode) | 3 | Title conveys "freq_btfdr with scalar resolution R = 0.2". |
| Plot (2-panel: window size + magnitude) | 4 | Top panel: window_size vs frequency (line). Bottom panel: BTFDR magnitude overlaid on true TF. |
| Plot (magnitude overlay) | 5 | BT, BTFDR, true TF on one axes; title conveys "fixed vs frequency-dependent resolution". |
3.4 coherence — Coherence analysis¶
Goal. Use squared coherence γ²(ω) to diagnose frequency-local
estimate quality. Show how a colored disturbance entering a plant via
an unmeasured channel depresses coherence in its own spectral band.
Plant. Plant B (§1.2) with two input channels:
- Channel 0: commanded force at mass 1,
F_col_0 = [1, 0]. - Channel 1: unmeasured disturbance at mass 2,
F_col_1 = [0, 1].
F = [[1, 0], [0, 1]] = I₂, N = 4000 samples.
Excitation.
- Commanded force
u[k] = 10 · ξ[k]withξ[k] ~ N(0, 1). - Disturbance
d[k] = 0.5 · AR1(e)[k]wheree[k] ~ N(0, 1)is white noise andAR1denotesy[k] = 0.9 y[k-1] + e[k](AR(1) pole at 0.9, a DC-heavy colored process).
Measured output y[k] = x₁[k] (no sensor noise — the coherence drop
must come from the disturbance, not from additive noise).
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | Generate test data | Build (Ad, Bd) with F = I₂ via §2.1 so the returned Bd has both channels. Simulate x[k+1] = Ad x[k] + Bd[:,0] u[k] + Bd[:,1] d[k]. |
| 2 | Estimate with freq_bt |
Call freq_bt with the single "known" input channel u and the position output y; the disturbance is unmodeled. |
| 3 | Plot Bode magnitude and coherence together | 2-panel plot: magnitude on top, squared coherence on bottom with γ² = 0.5 and γ² = 0.9 reference lines. |
| 4 | Confidence bands reflect coherence | Call bode_plot with confidence = 2 and confidence = 3 in side-by-side subplots. |
| 5 | High-disturbance vs low-disturbance comparison | Re-run freq_bt with d scaled by 0.1× and by 2.0×; overlay the two coherence curves. |
| 6 | Note: ETFE does not provide coherence | Call freq_etfe and print a one-line confirmation that result.coherence is None. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 2 | freq_bt |
window_size = 200, custom grid linspace(0.01, π, 512), sample_time = Ts |
| 3 | (plot only) | — |
| 4 | bode_plot |
two calls: confidence = 2, confidence = 3 |
| 5 | freq_bt |
two calls, one per disturbance scale |
| 6 | freq_etfe |
one call; just to inspect coherence is None |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| Plot (2-panel) | 3 | Top: Bode magnitude; bottom: coherence γ² with 0.5 and 0.9 horizontal references. |
| Plot (2×2 subplots) | 4 | Four panels forming confidence=2 Bode and confidence=3 Bode side by side. |
| Plot (coherence overlay) | 5 | Two curves labeled "low disturbance" and "high disturbance". |
| 6 | One line: "ETFE coherence is None: True". |
3.5 method_comparison — Comparing frequency-domain estimators¶
Goal. Apply all three frequency-domain estimators (freq_bt,
freq_btfdr, freq_etfe) to one plant, compare magnitudes, noise
spectra, and NRMSE fits; end with a summary table of trade-offs.
Plant. Plant A (§1.1) with N = 2048 samples. Same plant as §3.1
and §3.2 — enabling direct BT-vs-ETFE-vs-BTFDR comparison.
Excitation. Same as §3.1.
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | Generate test data | Simulate Plant A. |
| 2 | Estimate with all three methods | Call each of freq_bt, freq_btfdr, freq_etfe on the same custom frequency grid with pedagogically-reasonable options. |
| 3 | Compare Bode magnitude plots | Four curves (freq_etfe raw, freq_etfe smoothed, freq_bt, freq_btfdr) plus the true TF overlaid. |
| 4 | Compare noise spectra | Overlay the noise-spectrum estimates from the three methods. |
| 5 | Custom logarithmic frequency grid | Re-run the three methods on a log-spaced grid. |
| 6 | Time-series comparison: periodogram vs smoothed spectrum | Fresh simulation; hand u = None to both freq_bt and freq_etfe. |
| 7 | Model output comparison using sid.compare |
Call compare on each of the non-time-series results and print the NRMSE fits. |
| 8 | Summary of method trade-offs | A markdown table summarizing window-size, uncertainty availability, coherence availability, and best-for notes. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 2 | freq_bt |
window_size = 200, custom grid linspace(0.005, π, 512), sample_time = Ts |
| 2 | freq_etfe |
two calls: unsmoothed and smoothing = 15, same grid |
| 2 | freq_btfdr |
resolution = 0.3, same grid |
| 3 | (plot only) | — |
| 4 | (plot only) | — |
| 5 | freq_bt, freq_etfe, freq_btfdr |
on a log-spaced grid logspace(log10(0.005), log10(π), 200) |
| 6 | freq_bt, freq_etfe |
each with u = None |
| 7 | compare |
one call per non-time-series result |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| Plot (magnitude overlay) | 3 | Four estimator curves + true TF; legend identifies each. |
| Plot (noise-spectrum overlay) | 4 | Three estimator curves. Ports MUST clamp the noise-spectrum array with a small positive floor (e.g. max(abs(Φ_v), eps)) before applying log10 because per §2.7 of SPEC.md the noise spectrum is non-negative and hits exact zero at frequencies where the clamp activates. |
| Plot (magnitude overlay) | 5 | Log-grid version of §3 with three estimators + true. |
| Plot (power-spectrum overlay) | 6 | ETFE periodogram (grey) and BT smoothed spectrum (blue). |
| 7 | Four NRMSE lines, one per method (including the second ETFE smoothing variant). | |
| Table | 8 | Method trade-off table (pure markdown). |
3.6 mimo — Multi-input multi-output frequency response¶
Goal. Demonstrate MIMO identification with freq_bt on a plant
that has natural 2×2 force-to-position structure. Inspect the
response.shape == (nf, ny, nu) dimension and the noise spectral
matrix's off-diagonal entries.
Plant. Plant B (§1.2). Two sub-sections use two different input-distribution matrices:
- Sub-section A (2-output, 1-input):
F = [[1], [0]],N = 4000. - Sub-section B (2-output, 2-input):
F = I₂,N = 4000.
Excitation.
- Sub-section A: white unit-variance force
uon channel 0. - Sub-section B: independent white unit-variance forces on both input channels.
Measurement noise ε ~ N(0, 2·10⁻⁴) on both position outputs.
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | 2-output, 1-input system | Build the plant with F = [[1], [0]]; simulate; call freq_bt with a custom grid. Precompute the exact discrete TFs G₁(z) (input → x₁) and G₂(z) (input → x₂) for reference overlays. |
| 2 | Inspect MIMO result dimensions | Print result.response.shape, result.noise_spectrum.shape, and the fact that result.coherence is None for MIMO. |
| 3 | Plot individual output channels | Two-panel Bode magnitude: G₁ and G₂ with BT estimate and dashed true reference on each panel. |
| 4 | Noise spectral matrix | Plot the diagonals Φ_v[0,0](ω) and Φ_v[1,1](ω) from result.noise_spectrum. |
| 5 | 2-output, 2-input system | Rebuild the plant with F = I₂; simulate with (N, 2) inputs; call freq_bt. Print the 2×2 response shape. |
| 6 | Plot the full 2×2 transfer matrix | 2×2 subplot grid of the four magnitudes G_{ij}(ω). |
| 7 | MIMO uncertainty | Print a one-line confirmation that result.response_std is all NaN for MIMO. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 1 | freq_bt |
window_size = 200, custom grid linspace(0.005, π, 512), sample_time = Ts |
| 5 | freq_bt |
on (N, 2) input and (N, 2) output arrays, same options |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| 2 | Three lines: response shape, noise-spectrum shape, coherence is None. | |
| Plot (2-panel) | 3 | Two Bode magnitudes with estimated vs true reference. |
| Plot (1-panel) | 4 | Noise-spectral-matrix diagonals. |
| 5 | 2×2 MIMO response shape. | |
| Plot (2×2 grid) | 6 | Four magnitude plots for G₁₁, G₁₂, G₂₁, G₂₂. |
| 7 | One line: "MIMO response_std contains NaN: True". |
3.7 freq_map — Time-varying frequency response maps¶
Goal. Demonstrate freq_map on four scenarios: an LTI baseline
(should look stationary), two LTV variants (ramp and step change
built via §2.2), and a nonlinear hardening Duffing oscillator whose
apparent resonance drifts purely from nonlinearity (via §2.3).
Plants. Plant B (§1.2) for sections 1–3, 5–7. Plant E (§1.5) for
section 4 (Duffing). Section lengths: N = 4000 for Plant B LTI,
N = 4000 for the LTV variants, N = 4000 for Duffing.
Excitation.
- Plant B sections: white unit-variance force at mass 1.
- Duffing section:
u[k] = amp[k] · ξ[k]whereampramps linearly from0.5to10.0over the record andξ[k] ~ N(0, 1)— a ramped-amplitude white force.
Measurement noise 5·10⁻⁴ on the Plant B sections; none on Duffing
(the pedagogical point is purely in the nonlinearity).
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | LTI baseline: constant 2-mass chain | Build Plant B via §2.1, simulate, call freq_map. The magnitude map should look constant along time. |
| 2 | Continuous LTV: ramping first-spring stiffness | Build k_spring as a (2, N) array with k[0, :] a linspace 200 → 20 and k[1, :] = 80. Use §2.2 to get the per-step stack; simulate the LTV recursion. Call freq_map. |
| 3 | Discrete LTV: step change in stiffness | Same plant but k[0, :N/2] = 200 and k[0, N/2:] = 40; otherwise identical to section 2. |
| 4 | Coherence map | Re-use the section-2 result; plot the coherence map. |
| 5 | BT vs Welch algorithm | Call freq_map on the section-2 data with algorithm='bt' and algorithm='welch' side by side. |
| 6 | Segment length and overlap tuning | Call freq_map on the section-2 data with two different segment_length choices. |
| 7 | Time-series mode: evolving output spectrum | Fresh simulation of the section-2 LTV plant; hand only the output to freq_map; plot the spectrum map. |
| 8 | Duffing hardening oscillator | Use §2.3 with k_cub = 1e5 and a ramped-amplitude input; call freq_map on the response; show the apparent resonance drifting upward over time. Print the small-amplitude and ramped-amplitude effective natural frequencies for context. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 1 | freq_map |
segment_length = 512, overlap = 384, sample_time = Ts |
| 2–3 | freq_map |
segment_length = 256, overlap = 192, sample_time = Ts |
| 4 | freq_map |
(reuse result) — just plot coherence |
| 5 | freq_map |
two calls: algorithm = 'bt', algorithm = 'welch', segment_length = 256 |
| 6 | freq_map |
two calls: segment_length = 128 and segment_length = 512 |
| 7 | freq_map |
with u = None |
| 8 | freq_map |
segment_length = 256, overlap = 192, sample_time = Ts |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| Plot (magnitude map) | 1 | Title conveys "LTI baseline: stationary map". |
| Plot (magnitude map) | 2 | Title conveys "continuous LTV: ramping k₁, first mode drifts 7→3 rad/s". |
| Plot (magnitude map) | 3 | Title conveys "discrete LTV: step change at t = T/2". |
| Plot (coherence map) | 4 | Title conveys "coherence map". |
| Plot (two magnitude maps) | 5 | Side-by-side BT vs Welch. |
| Plot (two magnitude maps) | 6 | Side-by-side short vs long segments. |
| Plot (spectrum map) | 7 | Title conveys "time-series output spectrum drifts as k₁ softens". |
| 8 | Two lines reporting early vs late position RMS and their linearized effective natural frequencies. | |
| Plot (magnitude map) | 8 | Title conveys "Duffing hardening: apparent resonance rises with amplitude". |
3.8 spectrogram — Short-time FFT spectrogram¶
Goal. Demonstrate spectrogram on a physical SDOF plant driven
by a chirp force. The output response spectrogram shows the chirp
track modulated by the plant's own resonance — the ridge lights up
when the chirp sweeps through ω_n.
Plant. Plant D (§1.4) with N = 5000, Ts = 1/Fs, Fs = 1000 Hz.
Excitation. Linear chirp force u[k] = cos(2π · φ(t_k)) with
instantaneous frequency f(t) = f₀ + (f₁ − f₀) · t / (2 T_end) and
f₀ = 20 Hz, f₁ = 60 Hz. This sweeps through Plant D's resonance
at ≈ 31.83 Hz near t ≈ 1.5 s.
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | Chirp force driving the SDOF | Build Plant D via §2.1; simulate the state recursion under the chirp input; compute the spectrogram of the position response. |
| 2 | Window length trade-off | Two spectrogram calls with short and long window lengths; side-by-side plot. |
| 3 | Window types | Three spectrogram calls with window ∈ {'hann', 'hamming', 'rect'} at the same length. |
| 4 | Multi-channel signal | Stack the chirp-response (channel 0) with a constant-tone response (channel 1) driven by u_tone[k] = cos(2π · 50 · t_k). Compute a multi-channel spectrogram and plot both channels. |
| 5 | Log frequency scale and NFFT zero-padding | Single spectrogram call with window_length = 128 and nfft = 1024; plot with frequency_scale = 'log'. |
| 6 | Accessing raw STFT data | Print the dimensions of result.time, result.frequency, result.power, and result.complex_stft. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 1 | spectrogram |
window_length = 256, sample_time = Ts |
| 2 | spectrogram |
two calls: window_length = 64, window_length = 512 |
| 3 | spectrogram |
three calls: window = 'hann', 'hamming', 'rect'; window_length = 256 |
| 4 | spectrogram |
on a two-channel signal (N, 2) stacked from the chirp response and the 50 Hz tone response |
| 5 | spectrogram |
window_length = 128, nfft = 1024, sample_time = Ts; plotted with frequency_scale = 'log' |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| Plot (spectrogram) | 1 | Title conveys "SDOF response to chirp force: resonance lights up near ~32 Hz"; x-axis time (s), y-axis frequency (Hz). |
| Plot (two spectrograms) | 2 | Side-by-side short and long window. |
| Plot (three spectrograms) | 3 | Hann / Hamming / Rectangular. |
| Plot (two spectrograms) | 4 | Two channels: chirp response and 50 Hz tone response (the tone response should show a horizontal line at 50 Hz). |
| Plot (spectrogram) | 5 | Log-frequency axis; title conveys "log frequency scale with zero-padding (NFFT = 1024)". |
| 6 | Four lines reporting len(time), len(frequency), power.shape, complex_stft.shape. |
3.9 ltv_disc — LTV state-space identification (COSMIC)¶
Goal. Walk through the full COSMIC workflow — LTI recovery, LTV recovery with ramping stiffness, multi-trajectory benefit, validation lambda tuning, cost decomposition, uncertainty quantification, frozen transfer function, compare/residual validation — all on a physical 1-DoF SMD plant. End with a nonlinear Duffing section that recovers the amplitude-dependent local linearization.
Plant. Plant A (§1.1) for all linear sections. Plant E (§1.5) for
the Duffing section (same m, k_lin, c, F as Plant A; adds
k_cub).
Dimensions. p = 2 (state = [x, v]), q = 1 (single force
input).
Removed in v1.0.1. Earlier drafts included a "Preconditioning
for numerical stability" section and a "Frequency-based lambda
tuning" section. The preconditioning code path is intentionally
disabled in v1.0 of the library (the call site emits a runtime
warning), so demonstrating it produces a noisy example. The
frequency-based tuning method compares the COSMIC frozen TF against
a non-parametric freq_map estimate; on physical SMD plants the
velocity state has a band-pass spectrum that freq_map cannot
reliably estimate at the low frequencies where the modes live, so
no lambda in any reasonable grid clears the 90% consistency
threshold and the call emits "No lambda achieved 90% consistency".
Both sections were removed to keep this example a clean demo. The
library still supports both features; they are simply not
exercised here.
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | LTI system recovery | Build (Ad, Bd) via §2.1 for Plant A; simulate L = 10 trajectories of N = 50 steps with process noise σ = 0.01; call ltv_disc with lambda_ = 1e5; print the true Ad, the mean recovered A(k) across time, and the Frobenius-norm recovery error. |
| 2 | LTV system: time-varying stiffness | Build an (n=1, N=250) k_spring array ramping 200 → 50; use §2.2 for the per-step stack; simulate L = 30 trajectories with process noise σ = 0.01; call ltv_disc with lambda_ = 'auto'. Plot the recovered A[1, 0](k) against the true curve (for a 1-DoF plant with small Ts, Ad[1, 0] ≈ −k(t)·Ts). The longer record and larger trajectory count are chosen so that the downstream residual whiteness test (section 7) passes cleanly. |
| 3 | Multi-trajectory benefit | Compare COSMIC recovery with L = 5 vs L = 30 trajectories on the section-2 LTV plant. |
| 4 | Validation-based lambda tuning | Split L = 30 trajectories into 20 train + 10 validation; call ltv_disc_tune with method='validation' and a log-spaced lambda grid; plot the validation-loss curve. |
| 5 | Cost decomposition | Print the three-element cost = [total, data_fidelity, regularization]. |
| 6 | Uncertainty quantification | Call ltv_disc with uncertainty = True and plot A[1, 0](k) ± 2σ over the true curve. |
| 7 | Frozen transfer function with ltv_disc_frozen |
Call ltv_disc_frozen on the section-6 result at three time steps [0, N/2, N-1]; overlay the three Bode magnitudes. |
| 8 | Model validation with compare and residual |
Call both on the section-2 result; print per-channel NRMSE fits and the whiteness verdict. With N = 250, L = 30 the whiteness test is expected to PASS. |
| 9 | Weakly-nonlinear Duffing oscillator | Build Plant E; simulate L = 12 trajectories over N = 400 steps with a ramped-amplitude white input (amplitude profile linspace(0.5, 8.0, N)); call ltv_disc with a small manual lambda (lambda_ = 0.1, not 'auto', because auto-tuning over-regularises this dataset); plot the recovered A[1, 0](k) and overlay the linearized Ad[1, 0] reference from §2.1. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 1 | ltv_disc |
lambda_ = 1e5 |
| 2 | ltv_disc |
lambda_ = 'auto' |
| 3 | ltv_disc |
two calls on subsets with different L |
| 4 | ltv_disc_tune |
positional args (X_train, U_train, X_val, U_val), method = 'validation', lambda_grid = logspace(-3, 6, 30) |
| 6 | ltv_disc |
lambda_ = 'auto', uncertainty = True |
| 7 | ltv_disc_frozen |
time_steps = [0, N/2, N-1] on the uncertainty-enabled result |
| 8 | compare, residual |
each on the section-2 result |
| 9 | ltv_disc |
lambda_ = 0.1 (manual) on the Duffing data |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| 1 | True Ad, mean recovered A, Frobenius recovery error. |
|
Plot (A[1,0](k)) |
2 | True curve + COSMIC recovery overlay. |
Plot (A[1,0](k)) |
3 | L_few, L_many, and true curves. |
| Plot (validation loss curve) | 4 | Loss vs λ on log scale with the minimum marked. |
| 5 | Four-line cost breakdown with a consistency check (total − data − reg ≈ 0). | |
Plot (A[1,0](k) with ±2σ band) |
6 | — |
| Plot (Bode magnitudes at 3 time steps) | 7 | — |
| 8 | Per-channel fits and whiteness PASS (expected PASS with the v1.0.1 parameter choice). | |
Plot (2-panel: amplitude profile + recovered A[1,0](k)) |
9 | Top panel: excitation amplitude ramp. Bottom panel: recovered A[1,0](k) with the small-amplitude linear reference as a horizontal dashed line. |
3.10 multi_trajectory — Multi-trajectory ensemble averaging¶
Goal. Show that ensemble averaging across L independent
trajectories reduces variance by 1/L without sacrificing frequency
resolution. Four sub-sections exercise this benefit across all four
spectral estimators: freq_bt, freq_map, spectrogram, ltv_disc.
Plants.
- Sections 1, 2, 4: Plant B (§1.2) with
F = [[1], [0]]. - Section 3: Plant D (§1.4) — chirp driving the high-frequency SDOF.
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | LTI ensemble averaging — tighter confidence bands | Simulate L = 10 trajectories of Plant B (N = 2000, measurement noise 5·10⁻⁴); compare freq_bt confidence bands between a single trajectory and the full ensemble. Print the response_std ratio and confirm it is approximately 1/sqrt(L). |
| 2 | LTV time-varying map — sharper transition detection | Simulate L = 5 trajectories of Plant B with a step change k₁: 200 → 50 at t = N/2, N = 4000; compare freq_map between a single trajectory and the ensemble. |
| 3 | Spectrogram averaging — chirp in noise | Simulate L = 8 trajectories of Plant D driven by the same chirp but with independent 1·10⁻⁴ noise; compare single-trajectory and ensemble spectrograms. |
| 4 | COSMIC + freq_map consistency |
Reuse the ltv_disc LTV plant (Plant A 1-DoF with ramping stiffness, N = 80, L = 10); call ltv_disc(lambda_='auto', uncertainty=True) and freq_map on the same dataset; print that both identifiers use the same L trajectories. |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 1 | freq_bt |
two calls: single-trajectory and ensemble, window_size = 80, sample_time = Ts |
| 2 | freq_map |
two calls: single-trajectory and ensemble, segment_length = 256, sample_time = Ts |
| 3 | spectrogram |
two calls: single-trajectory and ensemble, window_length = 128, sample_time = Ts |
| 4 | ltv_disc |
lambda_ = 'auto', uncertainty = True |
| 4 | freq_map |
segment_length = min(N, 30), sample_time = Ts |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| 1 | Three lines: single-trajectory max std, ensemble max std, ratio and the expected 1/sqrt(L). |
|
| Plot (two Bode) | 1 | Single-trajectory Bode and ensemble Bode side by side (or stacked). |
| Plot (two magnitude maps) | 2 | Single-trajectory and ensemble freq_map magnitude maps side by side. |
| Plot (two spectrograms) | 3 | Single-trajectory and ensemble spectrograms side by side. |
| 4 | Two lines: COSMIC A(0) and COSMIC A(N-1); one line confirming freq_map.num_trajectories equals L. |
3.11 output_cosmic — LTV identification from partial observations¶
Goal. Demonstrate ltv_disc_io (Output-COSMIC) on a natural
partial-observation scenario: a 2-mass mechanical plant with position
sensors on both masses but no velocity sensors. The hidden-state
dimension is n = 4, the measured dimension is py = 2, and COSMIC
must infer the hidden velocities along with the dynamics.
Plant. Plant B (§1.2) with F = [[1], [0]] (force at mass 1
only). Observation matrix H = [[1, 0, 0, 0], [0, 1, 0, 0]] (measure
both positions, velocities hidden).
Trajectories. N = 80, L = 10, u[k] = 5 · ξ[k] with ξ ~
N(0, 1) (scaled up so displacements reach a few centimetres —
displacement magnitudes that let the identification converge). Process
noise σ_proc = 1·10⁻³, measurement noise σ_meas = 1·10⁻⁴.
Important framing note (binding). Output-COSMIC recovers
(A, B, x) up to an unobservable similarity transform — element-
wise comparison of the recovered A/B against the simulation's
Ad/Bd is not a meaningful metric. The validation step MUST be
the gauge-invariant observation reconstruction error
‖H · x̂ − y‖ / ‖y‖.
Required sections.
| # | Section title (SHOULD) | Topic (MUST) |
|---|---|---|
| 1 | System setup | Build Plant B; state it is a 4th-order plant with 2 position measurements. Print Ad and H. |
| 2 | Simulate trajectories | Generate L = 10 trajectories of length N = 80 with scaled input and process/measurement noise. |
| 3 | Step 1: estimate frequency response | Call freq_bt on the first trajectory (trimming Y to match U's length) with window_size = 20, sample_time = Ts. |
| 4 | Step 2: model-order determination | Call model_order on the freq_bt result; print the estimate and note that it may overshoot the true order for short lightly-damped records. |
| 5 | Step 3: construct observation matrix | Show H and print its shape. |
| 6 | Step 4: identify the LTV model via ltv_disc_io |
Call ltv_disc_io(Y, U, H, lambda_=1e5); print iterations and final cost. |
| 7 | Convergence history | Plot cost vs iteration (semilogy). |
| 8 | State recovery: observed channels vs hidden channels | 2×2 grid: top row shows the two measured positions with true, estimated, and measurement curves; bottom row shows the two hidden velocities with true and estimated curves. |
| 9 | Validation: observation reconstruction error | Compute H · x̂ for every trajectory and compare against Y; print the relative Frobenius error. |
| 10 | Frozen-time inspection of the recovered A and B | Print A(N/2) and B(N/2) as a sanity check that magnitudes are O(1). |
Required sid.* invocations.
| # | Function | Options |
|---|---|---|
| 3 | freq_bt |
window_size = 20, sample_time = Ts |
| 4 | model_order |
default |
| 6 | ltv_disc_io |
lambda_ = 1e5 |
Required outputs.
| Kind | Section | Binding content |
|---|---|---|
| 1 | Ad matrix and H matrix. |
|
| 2 | Max absolute output and max absolute hidden-velocity value. | |
| 3 | freq_bt response shape. |
|
| 4 | n_est estimate with a note that it may overshoot. |
|
| 5 | H matrix. |
|
| 6 | Iterations and final cost. | |
| Plot (convergence) | 7 | Semilog cost vs iteration. |
| Plot (2×2 grid) | 8 | Four subplots: x₁, x₂, v₁, v₂ with true/estimated/measured overlays as appropriate. |
| 9 | One line: "Observation reconstruction error: …". | |
| 10 | A(mid) and B(mid) matrices. |
4. Conventions¶
4.1 Auto-discovery¶
Every language port MUST lay out its examples directory so that the example runner discovers files purely by glob pattern. Hardcoded manifest lists are non-conformant.
| Language | Directory | Glob pattern |
|---|---|---|
| Python | python/examples/ |
example_*.ipynb |
| MATLAB / Octave | matlab/examples/ |
example*.m |
| Julia | julia/examples/ |
example_*.jl |
The CI test runner (e.g., pytest --nbmake python/examples/ for
Python, matlab/examples/runAllExamples.m for MATLAB) MUST discover
new examples without code changes. This rule mirrors the discovery
convention in CONTRIBUTING.md at the repo root.
4.2 Self-contained¶
Every example MUST run top-to-bottom with no external data files. All
simulation data is generated inline via the helper API of §2. Loading
from testdata/*.json or any other on-disk fixture is non-conformant.
4.3 Outputs cleared¶
For languages where example files persist runtime outputs (Jupyter
notebooks, R Markdown), every example file MUST be committed with all
outputs cleared. The CI runner validates that the file executes
without error; it does not assert output content. MATLAB .m scripts,
Julia .jl scripts, and similar source-only formats have nothing to
clear.
4.4 RNG seeding¶
Every example MUST fix an RNG seed at the top of its data-generation section so that a given commit gives deterministic numerical output within one language.
Seeds are not bound across languages. Python's numpy.random and
MATLAB's randn produce different sequences from the same seed, so
numerical equivalence across languages is not a goal of this spec.
Each port picks seeds that produce visually similar plots; reviewers
compare Python and MATLAB output side by side to catch mismatches,
not by diffing numeric values.
Recommended convention (SHOULD): use the same seed value across
languages when possible (e.g., seed = 42 in both Python's
default_rng(42) and MATLAB's rng(42)). This makes differences
obvious as RNG differences rather than spec mismatches.
4.5 File-name mapping table¶
Each example's canonical identifier maps to language-specific file names as follows:
| Identifier | Python (python/examples/) |
MATLAB (matlab/examples/) |
Julia (julia/examples/) |
|---|---|---|---|
siso |
example_siso.ipynb |
exampleSISO.m |
example_siso.jl |
etfe |
example_etfe.ipynb |
exampleETFE.m |
example_etfe.jl |
freq_dep_res |
example_freq_dep_res.ipynb |
exampleFreqDepRes.m |
example_freq_dep_res.jl |
coherence |
example_coherence.ipynb |
exampleCoherence.m |
example_coherence.jl |
method_comparison |
example_method_comparison.ipynb |
exampleMethodComparison.m |
example_method_comparison.jl |
mimo |
example_mimo.ipynb |
exampleMIMO.m |
example_mimo.jl |
freq_map |
example_freq_map.ipynb |
exampleFreqMap.m |
example_freq_map.jl |
spectrogram |
example_spectrogram.ipynb |
exampleSpectrogram.m |
example_spectrogram.jl |
ltv_disc |
example_ltv_disc.ipynb |
exampleLTVdisc.m |
example_ltv_disc.jl |
multi_trajectory |
example_multi_trajectory.ipynb |
exampleMultiTrajectory.m |
example_multi_trajectory.jl |
output_cosmic |
example_output_cosmic.ipynb |
exampleOutputCOSMIC.m |
example_output_cosmic.jl |
Python pattern: example_ + snake_case + .ipynb. MATLAB pattern:
example + camelCase + .m (matching the existing MATLAB examples in
the repo). Julia pattern: same as Python but with .jl extension.
4.6 Helper module placement¶
The util_msd* helpers specified in §2 MUST live inside the
examples directory as sibling modules, not in the main package.
This reflects their purpose as example fixtures, not part of the
public sid API.
| Language | Location |
|---|---|
| Python | python/examples/util_msd.py |
| MATLAB | matlab/examples/util_msd.m, matlab/examples/util_msd_ltv.m, matlab/examples/util_msd_nl.m |
| Julia | julia/examples/util_msd.jl (module file) |
Notebooks and scripts MUST import the helpers by sibling-path reference, not by reaching into the main package's internals.
4.7 Example file templates and boilerplate¶
Every language provides templates for writing new examples. Every example file in this spec MUST follow its language's template:
- Python: notebook with a title markdown cell, an imports code cell
including
%matplotlib inline, and one or more paired markdown + code cells per §3 section. - MATLAB: script with a top
%% exampleNametitle block comment, an imports block (or a runner-stylerunner__nCompletedcounter per the MATLAB CONTRIBUTING guide), and%% Section N — Titleblock comments separating sections. Matchingmatlab/examples/example_template.m. - Julia: script or notebook per the local language convention.
4.8 Import of the local sid package¶
Every example MUST exercise functions from the local sid package
and MUST NOT call any comparable third-party implementation (e.g.,
scipy.signal.welch, control.freqresp) in places where a sid.*
function would be equivalent. Third-party imports are allowed for
utility tasks (plotting, array construction) but never for the
identification or spectral analysis that is the example's subject.
5. Cross-Language "Equivalence"¶
This section defines what it means for two language ports to implement "the same example". It is the reviewer's contract when approving a new port.
5.1 Structural equivalence (binding)¶
For every example in §3, two conformant ports MUST share:
- Section inventory. The same ordered list of pedagogical sections, identified by topic (not verbatim header prose — see §0.3). A reviewer comparing the two implementations side by side should be able to say "this section on the left corresponds to that section on the right" for every section, without ambiguity.
sid.*call graph. The same sequence ofsid.*function calls in the same sections, with the same binding options (§3.X.5 tables). A port MAY invoke additional language-idiomatic helpers (e.g., a MATLAB port might usefprintfwhere Python uses an f-string), but everysid.*call listed in the spec MUST be present.- Plot kinds. The same kinds of plots (Bode, spectrum, magnitude overlay, time-frequency map, spectrogram, time series, 2×2 grid, etc.) in the same sections.
- Printed line semantics. The same pieces of information are printed, in the same order. "Whiteness test: PASS" in Python is equivalent to "Whiteness test: PASS" in MATLAB even if the exact formatting differs.
5.2 Numerical divergence (not binding)¶
RNG draws differ across languages. Numerical outputs (fit percentages, peak magnitudes, exact curve values, iteration counts, final cost values, recovered matrix entries) are allowed to differ by any amount consistent with finite-sample variance.
A language port is NOT required to reproduce the Python port's exact numerical outputs. Bit-identity is a non-goal.
5.3 Visual similarity (recommended)¶
A reviewer comparing side-by-side plots from two ports of the same example SHOULD be able to identify which example they are looking at without reading any code or markdown. The resonance should land at the same frequency, the confidence bands should have similar widths, the time-frequency ridges should traverse the same diagonal, etc.
This is a judgment test, not a mechanical one. Mismatches that trace
entirely to RNG differences are acceptable; mismatches that trace to
wrong plant parameters or missing sid.* calls are not.
5.4 Review checklist (per example)¶
When reviewing a new language port of the example suite, confirm each item for each example:
For example `<identifier>`:
[ ] Plant parameters match §1 exactly.
[ ] The helper functions from §2 are called with the right shapes.
[ ] The ordered list of pedagogical sections matches §3.X.4.
[ ] Every sid.* call listed in §3.X.5 is present with the specified
options.
[ ] Every plot listed in §3.X.6 is produced with a title that conveys
the specified topic and with the specified axis labels.
[ ] Every printed line listed in §3.X.6 is produced with the
specified semantics.
[ ] The example runs to completion in the language's example runner
(`pytest --nbmake`, `runAllExamples`, etc.).
[ ] Side-by-side visual comparison against the Python reference shows
similar plot features (resonance peaks in the same places, bands
of similar width, time-frequency ridges traversing similar
paths). Differences consistent with RNG noise are acceptable.
[ ] No external data files are loaded (§4.2).
[ ] Outputs are cleared if the file format persists them (§4.3).
[ ] Auto-discovery picks the file up without manifest changes (§4.1).
A port that passes all eleven example checklists is conformant.
5.5 What this spec does NOT verify¶
The following are not checked by this specification, even though they are important in their own right:
- Cross-language numerical agreement at the algorithm level. That
is the job of
SPEC.mdand the reference test vectors intestdata/. The example suite exercises the algorithms as a user would; it is not a verification harness for numerical equivalence. - Plot styling (colors, fonts, figure sizes). Ports are free to adopt the idiomatic visual style of their plotting library.
- Internal variable names inside example code. A port MAY use
N_sampleswhere the Python reference usesN; no renaming test is performed. - Markdown / comment prose. As stated in §0.3, narrative prose is advisory. Only section topics are binding.
6. Versioning and Change Control¶
6.1 Semantic versioning¶
This specification uses semantic versioning (MAJOR.MINOR.PATCH).
The current version is printed at the top of the document.
| Change type | Version bump | Examples |
|---|---|---|
| Typo fix, clarification, non-binding advisory text | PATCH | Rewording §0.4, fixing a table alignment |
| New example added | MINOR | Adding a 12th example for a new sid.* function |
| Plant parameter tweak that preserves the example's pedagogy | MINOR | Changing c from 2 to 2.2 because the Bode plot looks nicer |
| New required section inside an existing example | MINOR | Adding a new pedagogical beat to siso |
New binding sid.* call inside an existing example |
MINOR | Adding a residual check where there wasn't one |
| Example removal | MAJOR | Retiring siso in favor of something new |
| Plant deletion or renumbering | MAJOR | Removing Plant C |
| Breaking change to a helper API's interface | MAJOR | Changing the argument order of the LTI helper |
6.2 Deprecation cycle¶
When an example is removed, its entry MUST stay in this document with a "Removed in v1.Y.0" note for at least one full minor cycle before being deleted. This gives language ports time to delete the corresponding file during their next catch-up pass.
6.3 Authoritative reference implementation¶
The Python example suite on main (or the latest merged PR) is the
reference implementation of this spec at any given commit. If a
conflict arises between this document and the Python port, fix the
spec first (in a dedicated commit) and then update the Python port to
match the spec. Do not silently update the spec to match a Python
drift.
This rule mirrors the "spec is source of truth" principle in the root
CONTRIBUTING.md.
6.4 Language ports lag the spec¶
New language ports MAY target any stable minor version of this spec. A port against v1.0.0 is conformant until v1.0.0 is explicitly deprecated (major version bump). A port SHOULD update to a newer minor version when practical, but is not required to chase every MINOR bump immediately.
6.5 Changelog¶
v1.0.1 — 2026-04-12. Cleanup pass motivated by spurious warnings in the v1.0.0 example suite:
- §3.5
method_comparison: clarified that ports MUST clamp the noise-spectrum array with a small positive floor before applyinglog10, so the noise-spectrum overlay plot does not emit a divide-by-zero warning at frequencies where the spectrum has been clamped to zero per §2.7 ofSPEC.md. - §3.1
siso: clarified that the residual whiteness test is expected to FAIL on the Plant A SDOF. The non-parametric BT estimator has finite-window bias that shows up as residual auto-correlation; the FAIL is the pedagogical content of the section, not a bug. - §3.9
ltv_disc: - Removed "Preconditioning for numerical stability" section. Preconditioning is intentionally disabled in the v1.0 library release, so the call emits a runtime warning with no pedagogical payload.
- Removed "Frequency-based lambda tuning" section.
ltv_disc_tune(method='frequency')relies on a non-parametricfreq_mapestimate of each state channel; the velocity component of a 1-DoF SMD has a bandpass spectrum thatfreq_mapcannot reliably estimate, so the method finds 0% consistency on any reasonable lambda grid and emits "No lambda achieved 90% consistency". The library method is not broken, but it is the wrong tool for physical SMD plants. - Changed the LTV section's simulation parameters from
N = 80, L = 15toN = 250, L = 30. With the longer record and larger ensemble, the Section 8 residual whiteness test passes cleanly in both Python and MATLAB, so the example can print PASS without narration. - Renumbered remaining sections from 11 to 9.
v1.0.0 — 2026-04-11. Initial release.
7. References¶
SPEC.md— Algorithm specification. Allsid.*functions referenced in this document are defined there.../python/CONTRIBUTING.md— Python notebook conventions, docstring template, and inline comment style.../matlab/CONTRIBUTING.md— MATLAB function header standard and template files.../CONTRIBUTING.md— Root contributing guide, including the "spec as source of truth" principle and the auto-discovery convention.../python/examples/README.md— User-facing summary of the Python example suite. This file is a downstream consumer of the spec; it describes what the spec requires in plain-English form for end users.../matlab/examples/README.md— The MATLAB counterpart to the Python examples README.- Ljung, L. System Identification: Theory for the User, 2nd ed.,
Prentice Hall, 1999. Referenced by
SPEC.mdfor the underlying theory of all spectral and state-space identification methods exercised in the examples.
End of specification.