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), backward difference, 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 with given amplitudes, e.g. by the inverse gain of a model for a flat output spectrum, started so that a double integrator does not drift; 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_backward = 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.set_header;
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 chirp;
use Validation;
let ts = 1e-3;
let u: = chirp.unwrap; // 0.5 .. 50 Hz over 20 s (non-periodic)
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 s segments: 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 ts = 1e-3; // sampling period of the data
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!;
A model that leaves out modes on purpose (e.g. a rigid-body model of a plant whose resonances
are above the control bandwidth) fails the tests on the whole band, rightly: the residual keeps
the error of those modes. CoherenceCheck::new(segment_s, excited).set_band((low, high)) tests the
coherence only in [low, high] [Hz] (CoherenceTest::band, FrequencyResponseComparison::band
for a closer look), with a margin to the lowest mode left out; run it without the whiteness and
cross-correlation tests, which see the whole band.
Durations (evaluated_from, the segments of coherence_test and frequency_response) are in
seconds and the fundamental frequency of line_test in Hz, whole numbers of the sampling period
(the period 1 / f0 for the fundamental); the lags of autocorrelation and cross_correlation
are in samples.
With a periodic input (e.g. a multisine of fundamental frequency f0 [Hz], excited at the
harmonics lines), validation.evaluated_from(1.0 / f0)?.line_test(f0, &lines, 0.99) compares
the model error at every excited line with the noise level estimated from the period-to-period
variation (Check::Lines { fundamental_frequency: f0, lines } in check).
The structure of a continuous-time model (orders n, m, integrators q 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 f0 = 1.0; // multisine of fundamental 1 Hz (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, q = 0 (no pole at the origin), nk = 2 ..= 4
let structures = grid;
let options = SearchOptions ;
let search = search.unwrap;
println!; // one row per candidate: BIC, tests, iterations
assert_eq!;
A plant with a rigid-body mode (poles at, or very close to, the origin) is identified as
R(s) / s^q by srivc::identify_with_prefilter with a Prefilter::new(q, omega_c): the input
is pseudo-integrated, s / (s + ω_c)^(q+1), and the output high-passed, s^(q+1) / (s + ω_c)^(q+1),
so that no integrator is applied to the data. With SearchOptions::prefilter = Some(omega_c),
srivc::search searches q with the other parameters of the structure. R may be a constant
(n = 0, only with q >= 1): the pure integrator b_0 / s^q, e.g. a single inertia 1 / (J s^2)
(torque -> position) is (n, m, q) = (0, 0, 2) with b_0 = 1 / J, or the rigid-body mode alone
of a plant whose resonances are above the band of interest.
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;
The model is kept real (residues of real poles real, of conjugate pairs conjugate) also on
noisy measurements. identify_weighted(&samples, &weights, n, &options) weights the error per
sample, e.g. by 1 / |G| for the relative error of a response spanning decades.
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.