dsmc: Digitalservo Motion Control Library
A Rust library for designing and analyzing motion control systems: transfer functions and state space models, discretization, frequency analysis, trajectory generation, feedforward control (perfect tracking control), signal processing and system identification.
Features
| Area | What you get |
|---|---|
| Systems | TransferFunction in s or z (the domain is a type parameter, so they cannot be mixed up), the tf! macro, arithmetic, pole-zero cancellation, poles / zeros, partial fractions; TransferFunctionWithDelay (dead time: frequency response, exact simulation of a held input); StateSpace |
| Discretization | Zero-order hold (exact), bilinear (Tustin), matched z-transform in both directions, each for transfer functions and state-space models, chosen by type or at run time (DiscretizeMethod); running a discrete-time TransferFunction or StateSpace sample by sample (DiscreteSystem: plant simulation, controllers, filters) |
| Frequency analysis | FrequencyTransferFunction (ω -> G(jω), including dead time), Bode diagram, Nyquist plot, FFT, Welch's method |
| Laplace transform | Inverse Laplace transform, stable (non-causal) inverse of nonminimum-phase systems, piecewise-polynomial signals |
| Trajectories | Modified trapezoid / sine / constant velocity, cycloid, harmonic, smoothstep of any smoothness |
| Feedforward | Multirate perfect tracking control (PTC), with pre-actuation for nonminimum-phase plants |
| Signal processing | Pseudo-differentiator, delay; excitation signals (random-phase multisine, chirp) |
| System identification | ARX models and linear regressions by least squares or Kalman filter, instrumental variables (IV) for ARX models, SRIVC for continuous-time models, model validation (BIC / AIC, whiteness, residual-input cross-correlation and coherence, test at the excited lines of a periodic input, comparison with the nonparametric frequency response; output error or one-step prediction error; several tests at once with pass / fail), search of the SRIVC model structure (orders and delay) by validation, high-pass preprocessing against drifts, Levy / Sanathanan-Koerner, vector fitting, Gaussian process regression |
| Logging | CSV output of any Serialize value |
Installation
or in Cargo.toml:
[]
= "0.1"
Examples
Transfer functions
use ;
// From a string: the variable (`s` or `z`) selects continuous / discrete time at compile time.
let wn = 100.0;
let plant = tf!;
let controller = tf!;
// Arithmetic (common poles / zeros are cancelled)
let open_loop = &controller * &plant;
println!;
// Closed loop under unity negative feedback: L / (1 + L), same as `&open_loop / &(1.0 + &open_loop)`
let closed_loop = open_loop.unity_feedback;
// From coefficients (descending order), or parsed at runtime with error handling
let g = continuous;
let h: = "1 / (s + 1)^2".parse.unwrap;
// Poles, zeros, partial fractions and the inverse Laplace transform
let poles = g.pz_map.poles;
println!; // x(t) = exp(-1.000t) - exp(-2.000t)
let x = h.inverse_laplace;
assert!;
Discretization and simulation
use ;
use ;
let g = tf!;
let ts = 1e-3;
let g_zoh = g.discretize.unwrap;
let g_tustin = g.discretize.unwrap;
let g_matched = g.discretize.unwrap;
let ss_tustin = try_from.unwrap.discretize.unwrap;
// The method chosen at run time
let method = MatchedZ;
let g_z = g.discretize.unwrap;
// Step response, one sample at a time: a DiscreteSystem runs a discrete-time transfer
// function (its difference equation; single input and output: scalar in, scalar out)...
let mut system = try_from.unwrap;
let y: = .map.collect;
// ...or a discrete-time state-space model (any number of inputs / outputs)
let mut system = from;
let y: = .map.collect;
Bode diagram and Nyquist plot
use ;
use Complex;
let plant = tf!;
let controller = tf!;
// Any element can be put in series, e.g. a dead time of 1 ms
let delay = new;
let open_loop = &.frequency_transfer_function * &delay;
// Gain [dB] and phase [rad] from 0.1 Hz to 1 kHz
let bode = new.plot;
// The same in rad/s
let bode_rad = new.plot;
let nyquist = new.plot;
// Single points
let c = open_loop.;
println!;
Results can be written to CSV directly (one row per point: frequency, gain, phase or omega, re, im):
use ;
use DataStorage;
let bode = new.plot;
let mut csv = new.unwrap;
for point in &bode
csv.close.unwrap;
Trajectory and perfect tracking control
use ;
use LiftedDiscretizedSystem;
use ;
// Normalized motion profiles: position, velocity and acceleration over x = 0..1
let profile = ModifiedSine.generate;
// Feedforward input that makes the output of a nonminimum-phase plant follow a smooth move
// exactly at every frame (with pre-actuation before the move starts)
let ts = 1e-4;
let plant = tf!;
let y_d = piecewise; // distance, duration, start, smoothness
let model = normalized_controllable_canonical.unwrap;
let lifted = new.unwrap;
let u = lifted.calculate_ptc_input_for_reference_output.unwrap;
System identification
use tf;
use DiscreteSystem;
use Zoh;
use lsm;
let ts = 1e-3;
let mut plant = try_from.unwrap;
// ARX model y[k] = a1 y[k-1] + a2 y[k-2] + b0 u[k-1] + b1 u[k-2]
let mut arx = new.with_input_delay;
let mut y_prev = 0.0;
for k in 0..1000
let g_z = arx.identify.unwrap;
println!;
With measurement noise on the output, least squares is biased. The instrumental variable (IV)
method removes the bias, either for the discrete-time ARX model or, with SRIVC (simplified refined
instrumental variable method for continuous-time systems), directly for G(s) from sampled data:
use tf;
use DiscreteSystem;
use Zoh;
use ;
use ;
let ts = 1e-3;
let mut plant = try_from.unwrap;
let u: =
.map
.collect;
let y: = u.iter.map.collect; // + noise
// ARX model by least squares, then 3 IV iterations (each with the previous estimate as the
// auxiliary model generating the instruments)
let model = identify.unwrap;
let g_z = model.transfer_function;
// Continuous-time B(s) / A(s) with deg A = 2, deg B = 0 (orders: denominator first), started
// from a state-variable filter 1 / (s + 30)^2
let result = identify.unwrap;
let g_s = result.model;
An identified model is validated on another experiment (system_identification::validation)
by its residual ε = y - ŷ, with ŷ the output of the model simulated with the measured input
(output error: Validation::continuous, Validation::discrete) or, for an ARX model with its
noise model, the one-step prediction (Validation::one_step_prediction):
| Method | What it checks | Input |
|---|---|---|
bic, aic, aicc |
fit against the number of parameters, to compare model structures | any |
cross_correlation |
the residual does not depend on past inputs (wrong dynamics or delay) | any |
coherence_test |
the same, per frequency | any |
line_test |
the model error is at the noise level at every excited line | periodic |
autocorrelation |
the residual is white (Ljung-Box); for the output error, it tests the noise, not G |
any |
frequency_response |
gain / phase error against the nonparametric estimate and its uncertainty (diagnostic) | any |
use tf;
use DiscreteSystem;
use Zoh;
use Validation;
let ts = 1e-3;
// Chirp 0.5 .. 50 Hz over 20 s (non-periodic)
let u: =
.map
.collect;
let g_s = tf!;
let mut plant = try_from.unwrap;
let y: = u.iter.map.collect; // + noise
let validation = continuous.unwrap;
let = ; // p = n + m + 1
let correlation = validation.cross_correlation; // r(τ) within ±bound at 99 % per lag
let coherence = validation.coherence_test.unwrap.excited; // 1 Hz bins
let response = validation.frequency_response.unwrap.excited;
println!;
For an ARX model, the one-step prediction error must be white and independent of the input:
use ;
let noise = ;
let u: = .map.collect;
let mut y = vec!;
for k in 2..u.len
let mut arx = new.with_input_delay;
for k in 0..u.len
arx.identify.unwrap;
let validation = one_step_prediction.unwrap;
let whiteness = validation.autocorrelation;
let white = whiteness.ljung_box <= whiteness.ljung_box_bound; // χ²(20) at 99 %
let independent = validation.cross_correlation.fraction_outside < 0.05;
let bic = validation.bic;
assert!;
The tests can be run together with one confidence level by Validation::check, which also
decides pass / fail (whiteness by Ljung-Box, cross-correlation with a Bonferroni bound over the
lags, coherence and lines by the number outside against a binomial quantile) and prints a report:
use tf;
use ;
let ts = 1e-3;
let g_s = tf!;
let noise = ;
let u: = .map.collect;
let y0 = continuous.unwrap.simulated; // plant output
let y: = y0.iter.enumerate.map.collect;
let report = continuous
.unwrap
.check
.unwrap;
println!; // one line per check, then "overall: passed"
assert!;
With a periodic input (e.g. a multisine of period samples, excited at the harmonics lines),
validation.evaluated_from(period).line_test(period, &lines, 0.99) compares the model error at
every excited line with the noise level estimated from the period-to-period variation
(Check::Lines { period, lines } in check).
The structure of a continuous-time model (orders n, m and input delay nk, searched together:
missing poles are made up for by a longer delay) is chosen by srivc::search: every candidate is
identified, validated on another experiment by check, and the lowest BIC among the candidates
passing every test is selected:
use ;
use multisine;
use ;
use Check;
let ts = 1e-3;
let period = 1000; // multisine of period 1 s over 1 .. 40 Hz
let lines: = .collect;
let plant = new;
let noise = ;
let experiment = ;
let = experiment;
let = experiment;
// n = 1 ..= 3, m = 0 ..= 1, nk = 2 ..= 4
let structures = grid;
let options = SearchOptions ;
let search = search.unwrap;
println!; // one row per candidate: BIC, tests, iterations
assert_eq!;
Data with a drift (e.g. an integrating plant driven by an unknown input offset) are high-passed
before the identification by preprocessing::high_pass(&u, &y, cutoff, ts, order), which filters
the input and the output alike from rest, so that they are still related by the same G(s).
From measured frequency responses (e.g. dsmc::fft::welch), a continuous-time model can be fitted:
use ;
use ;
let g = tf!.frequency_transfer_function;
let samples: =
.map
.collect;
let fitted = identify.unwrap;
let model: = fitted.into;
More examples are in tests/ and in the API documentation.
License
This project is licensed under the MIT License. You are free to use, copy, modify, merge, publish, distribute, sublicense and sell copies of the software, provided that the copyright notice and the permission notice are included in all copies or substantial portions of the software. The software is provided "as is", without warranty of any kind.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you shall be licensed under the MIT License, without any additional terms or conditions.