control_sys/
model.rs

1extern crate nalgebra as na;
2
3/// A trait representing a state-space model in control systems.
4///
5/// This trait provides methods to access the state-space matrices A, B, C, and D,
6/// which are fundamental components of the state-space representation of a system.
7///
8pub trait StateSpaceModel {
9    /// Returns a reference to the state matrix A.
10    fn mat_a(&self) -> &na::DMatrix<f64>;
11
12    /// Returns a reference to the input matrix B.
13    fn mat_b(&self) -> &na::DMatrix<f64>;
14
15    /// Returns a reference to the output matrix C.
16    fn mat_c(&self) -> &na::DMatrix<f64>;
17
18    /// Returns a reference to the feedthrough matrix D.
19    fn mat_d(&self) -> &na::DMatrix<f64>;
20}
21
22/// A trait representing a discrete system with a specific sampling time.
23///
24/// This trait should be implemented by any type that represents a discrete system
25/// and provides a method to retrieve the sampling time interval (`dt`).
26///
27/// # Examples
28///
29/// ```
30/// use control_sys::model::Discrete;
31///
32/// struct MyDiscreteSystem {
33///     sampling_dt: f64,
34/// }
35///
36/// impl Discrete for MyDiscreteSystem {
37///     fn sampling_dt(&self) -> f64 {
38///         self.sampling_dt
39///     }
40/// }
41///
42/// let system = MyDiscreteSystem { sampling_dt: 0.1 };
43/// assert_eq!(system.sampling_dt(), 0.1);
44/// ```
45///
46pub trait Discrete {
47    /// Returns the sampling time interval (`dt`) of the discrete system.
48    fn sampling_dt(&self) -> f64;
49}
50
51/// A trait representing a system that has poles in the complex plane.
52///
53/// # Examples
54///
55/// ```
56/// use nalgebra as na;
57/// use control_sys::model::Pole;
58///
59/// struct MySystem;
60///
61/// impl Pole for MySystem {
62///     fn poles(&self) -> Vec<na::Complex<f64>> {
63///         vec![na::Complex::new(1.0, 2.0), na::Complex::new(3.0, 4.0)]
64///     }
65/// }
66///
67/// let system = MySystem;
68/// let poles = system.poles();
69/// assert_eq!(poles, vec![na::Complex::new(1.0, 2.0), na::Complex::new(3.0, 4.0)]);
70/// ```
71///
72pub trait Pole {
73    /// Returnes a vector of complex numbers representing the poles of the system.
74    fn poles(&self) -> Vec<na::Complex<f64>>;
75}
76
77#[derive(Debug, Clone)]
78/// A struct representing a continuous state-space model.
79///
80/// This model is defined by the following matrices:
81/// - `mat_a`: The state matrix (A), which defines the system dynamics.
82/// - `mat_b`: The input matrix (B), which defines how the input affects the state.
83/// - `mat_c`: The output matrix (C), which defines how the state is mapped to the output.
84/// - `mat_d`: The feedthrough matrix (D), which defines the direct path from input to output.
85pub struct ContinuousStateSpaceModel {
86    mat_a: na::DMatrix<f64>,
87    mat_b: na::DMatrix<f64>,
88    mat_c: na::DMatrix<f64>,
89    mat_d: na::DMatrix<f64>,
90}
91
92/// Represents a continuous state-space model.
93impl ContinuousStateSpaceModel {
94    /// Creates a new `ContinuousStateSpaceModel` with the given matrices.
95    ///
96    /// # Arguments
97    ///
98    /// * `mat_a` - State matrix (A).
99    /// * `mat_b` - Input matrix (B).
100    /// * `mat_c` - Output matrix (C).
101    /// * `mat_d` - Feedthrough matrix (D).
102    ///
103    /// # Returns
104    ///
105    /// A new instance of `ContinuousStateSpaceModel`.
106    pub fn from_matrices(
107        mat_a: &na::DMatrix<f64>,
108        mat_b: &na::DMatrix<f64>,
109        mat_c: &na::DMatrix<f64>,
110        mat_d: &na::DMatrix<f64>,
111    ) -> ContinuousStateSpaceModel {
112        ContinuousStateSpaceModel {
113            mat_a: mat_a.clone(),
114            mat_b: mat_b.clone(),
115            mat_c: mat_c.clone(),
116            mat_d: mat_d.clone(),
117        }
118    }
119
120    /// Builds a controllable canonical form state-space model from a transfer function.
121    ///
122    /// # Arguments
123    ///
124    /// * `tf` - The transfer function to convert.
125    ///
126    /// # Returns
127    ///
128    /// A `ContinuousStateSpaceModel` in controllable canonical form.
129    fn build_controllable_canonical_form(tf: &TransferFunction) -> ContinuousStateSpaceModel {
130        // TODO: Still need to normalize coefficients and check for size
131        let n_states = tf.denominator_coeffs.len();
132
133        let mut mat_a = na::DMatrix::<f64>::zeros(n_states, n_states);
134        mat_a
135            .view_range_mut(0..n_states - 1, 1..)
136            .copy_from(&na::DMatrix::<f64>::identity(n_states - 1, n_states - 1));
137        for (i, value) in tf.denominator_coeffs.iter().rev().enumerate() {
138            mat_a[(n_states - 1, i)] = -value.clone();
139        }
140
141        let mut mat_b = na::DMatrix::<f64>::zeros(tf.numerator_coeffs.len(), 1);
142        mat_b[(tf.numerator_coeffs.len() - 1, 0)] = 1.0f64;
143
144        let mut mat_c = na::DMatrix::<f64>::zeros(tf.numerator_coeffs.len(), 1);
145        for (i, value) in tf.numerator_coeffs.iter().rev().enumerate() {
146            mat_c[(i, 0)] = value.clone();
147        }
148
149        let mat_d = na::dmatrix![tf.constant];
150
151        ContinuousStateSpaceModel {
152            mat_a: mat_a,
153            mat_b: mat_b,
154            mat_c: mat_c,
155            mat_d: mat_d,
156        }
157    }
158
159    /// Returns the size of the state-space model.
160    ///
161    /// # Returns
162    ///
163    /// The number of states in the state-space model.
164    pub fn state_space_size(&self) -> usize {
165        return self.mat_a.ncols();
166    }
167}
168
169impl StateSpaceModel for ContinuousStateSpaceModel {
170    fn mat_a(&self) -> &na::DMatrix<f64> {
171        return &self.mat_a;
172    }
173
174    fn mat_b(&self) -> &na::DMatrix<f64> {
175        return &self.mat_b;
176    }
177
178    fn mat_c(&self) -> &na::DMatrix<f64> {
179        return &self.mat_c;
180    }
181
182    fn mat_d(&self) -> &na::DMatrix<f64> {
183        return &self.mat_d;
184    }
185}
186
187impl Pole for ContinuousStateSpaceModel {
188    fn poles(&self) -> Vec<na::Complex<f64>> {
189        self.mat_a.complex_eigenvalues().iter().cloned().collect()
190    }
191}
192
193#[derive(Debug, Clone)]
194/// A struct representing a discrete state-space model.
195///
196/// This model is defined by the following matrices:
197/// - `mat_a`: The state transition matrix.
198/// - `mat_b`: The control input matrix.
199/// - `mat_c`: The output matrix.
200/// - `mat_d`: The feedthrough (or direct transmission) matrix.
201///
202/// Additionally, the model includes a sampling time `sampling_dt` which represents the time interval between each discrete step.
203///
204/// # Fields
205/// - `mat_a` (`na::DMatrix<f64>`): The state transition matrix.
206/// - `mat_b` (`na::DMatrix<f64>`): The control input matrix.
207/// - `mat_c` (`na::DMatrix<f64>`): The output matrix.
208/// - `mat_d` (`na::DMatrix<f64>`): The feedthrough matrix.
209/// - `sampling_dt` (f64): The sampling time interval.
210pub struct DiscreteStateSpaceModel {
211    mat_a: na::DMatrix<f64>,
212    mat_b: na::DMatrix<f64>,
213    mat_c: na::DMatrix<f64>,
214    mat_d: na::DMatrix<f64>,
215    sampling_dt: f64,
216}
217
218impl StateSpaceModel for DiscreteStateSpaceModel {
219    fn mat_a(&self) -> &na::DMatrix<f64> {
220        return &self.mat_a;
221    }
222
223    fn mat_b(&self) -> &na::DMatrix<f64> {
224        return &self.mat_b;
225    }
226
227    fn mat_c(&self) -> &na::DMatrix<f64> {
228        return &self.mat_c;
229    }
230
231    fn mat_d(&self) -> &na::DMatrix<f64> {
232        return &self.mat_d;
233    }
234}
235
236impl DiscreteStateSpaceModel {
237    /// Creates a new `DiscreteStateSpaceModel` with the given state-space matrices and sampling time.
238    ///
239    /// # Arguments
240    ///
241    /// * `mat_a` - State transition matrix.
242    /// * `mat_b` - Control input matrix.
243    /// * `mat_c` - Observation matrix.
244    /// * `mat_d` - Feedforward matrix.
245    /// * `sampling_dt` - Sampling time interval.
246    ///
247    /// # Returns
248    ///
249    /// A new `DiscreteStateSpaceModel` instance.
250    pub fn from_matrices(
251        mat_a: &na::DMatrix<f64>,
252        mat_b: &na::DMatrix<f64>,
253        mat_c: &na::DMatrix<f64>,
254        mat_d: &na::DMatrix<f64>,
255        sampling_dt: f64,
256    ) -> DiscreteStateSpaceModel {
257        DiscreteStateSpaceModel {
258            mat_a: mat_a.clone(),
259            mat_b: mat_b.clone(),
260            mat_c: mat_c.clone(),
261            mat_d: mat_d.clone(),
262            sampling_dt: sampling_dt,
263        }
264    }
265
266    /// Converts a continuous state-space model to a discrete state-space model using the forward Euler method.
267    ///
268    /// # Arguments
269    ///
270    /// * `mat_ac` - Continuous state transition matrix.
271    /// * `mat_bc` - Continuous control input matrix.
272    /// * `mat_cc` - Continuous observation matrix.
273    /// * `mat_dc` - Continuous feedforward matrix.
274    /// * `sampling_dt` - Sampling time interval.
275    ///
276    /// # Returns
277    ///
278    /// A new `DiscreteStateSpaceModel` instance.
279    pub fn from_continuous_matrix_forward_euler(
280        mat_ac: &na::DMatrix<f64>,
281        mat_bc: &na::DMatrix<f64>,
282        mat_cc: &na::DMatrix<f64>,
283        mat_dc: &na::DMatrix<f64>,
284        sampling_dt: f64,
285    ) -> DiscreteStateSpaceModel {
286        let mat_i = na::DMatrix::<f64>::identity(mat_ac.nrows(), mat_ac.nrows());
287        let mat_a = (mat_i - mat_ac.scale(sampling_dt)).try_inverse().unwrap();
288        let mat_b = &mat_a * mat_bc.scale(sampling_dt);
289        let mat_c = mat_cc.clone();
290        let mat_d = mat_dc.clone();
291
292        DiscreteStateSpaceModel {
293            mat_a: mat_a,
294            mat_b: mat_b,
295            mat_c: mat_c,
296            mat_d: mat_d,
297            sampling_dt: sampling_dt,
298        }
299    }
300
301    /// Converts a continuous state-space model to a discrete state-space model using the forward Euler method.
302    ///
303    /// # Arguments
304    ///
305    /// * `model` - A reference to a `ContinuousStateSpaceModel` instance.
306    /// * `sampling_dt` - Sampling time interval.
307    ///
308    /// # Returns
309    ///
310    /// A new `DiscreteStateSpaceModel` instance.
311    pub fn from_continuous_ss_forward_euler(
312        model: &ContinuousStateSpaceModel,
313        sampling_dt: f64,
314    ) -> DiscreteStateSpaceModel {
315        Self::from_continuous_matrix_forward_euler(
316            model.mat_a(),
317            model.mat_b(),
318            model.mat_c(),
319            model.mat_d(),
320            sampling_dt,
321        )
322    }
323}
324
325impl Pole for DiscreteStateSpaceModel {
326    fn poles(&self) -> Vec<na::Complex<f64>> {
327        self.mat_a.complex_eigenvalues().iter().cloned().collect()
328    }
329}
330
331impl Discrete for DiscreteStateSpaceModel {
332    fn sampling_dt(&self) -> f64 {
333        return self.sampling_dt;
334    }
335}
336
337struct TransferFunction {
338    numerator_coeffs: Vec<f64>,
339    denominator_coeffs: Vec<f64>,
340    constant: f64,
341}
342
343impl TransferFunction {
344    fn new(
345        numerator_coeffs: &[f64],
346        denominator_coeffs: &[f64],
347        constant: f64,
348    ) -> TransferFunction {
349        TransferFunction {
350            numerator_coeffs: numerator_coeffs.to_vec(),
351            denominator_coeffs: denominator_coeffs.to_vec(),
352            constant: constant,
353        }
354    }
355}
356
357#[cfg(test)]
358/// This module contains unit tests for the control system models.
359///
360/// # Tests
361///
362/// - `test_compute_state_space_model_nominal`: Tests the construction of a continuous state-space model in controllable canonical form from a transfer function and verifies the matrices A, B, C, and D.
363/// - `test_compute_poles_pure_real`: Tests the computation of poles for a discrete state-space model with purely real eigenvalues.
364/// - `test_compute_poles_pure_im`: Tests the computation of poles for a discrete state-space model with purely imaginary eigenvalues.
365/// - `test_compute_poles_real_and_imaginary_part`: Tests the computation of poles for a discrete state-space model with both real and imaginary parts.
366/// - `test_compute_controllability_matrix_nominal`: Tests the computation of the controllability matrix for a given state-space model.
367/// - `test_controllability_2x2_controllable`: Tests the controllability of a 2x2 discrete state-space model that is controllable.
368/// - `test_controllability_3x3_not_controllable`: Tests the controllability of a 3x3 discrete state-space model that is not controllable.
369mod tests {
370    use super::*;
371
372    #[test]
373    fn test_compute_state_space_model_nominal() {
374        let tf = TransferFunction::new(&[1.0, 2.0, 3.0], &[1.0, 4.0, 6.0], 8.0);
375
376        let ss_model = ContinuousStateSpaceModel::build_controllable_canonical_form(&tf);
377
378        // Check mat A
379        assert_eq!(ss_model.mat_a().shape(), (3, 3));
380        assert_eq!(ss_model.mat_a()[(2, 0)], -6.0f64);
381        assert_eq!(ss_model.mat_a()[(2, 1)], -4.0f64);
382        assert_eq!(ss_model.mat_a()[(2, 2)], -1.0f64);
383        assert_eq!(ss_model.mat_a()[(0, 1)], 1.0f64);
384        assert_eq!(ss_model.mat_a()[(1, 2)], 1.0f64);
385
386        // Check mat B
387        assert_eq!(ss_model.mat_b().shape(), (3, 1));
388        assert_eq!(ss_model.mat_b()[(0, 0)], 0.0f64);
389        assert_eq!(ss_model.mat_b()[(1, 0)], 0.0f64);
390        assert_eq!(ss_model.mat_b()[(2, 0)], 1.0f64);
391
392        // Check mat C
393        assert_eq!(ss_model.mat_c().shape(), (3, 1));
394        assert_eq!(ss_model.mat_c()[(0, 0)], 3.0f64);
395        assert_eq!(ss_model.mat_c()[(1, 0)], 2.0f64);
396        assert_eq!(ss_model.mat_c()[(2, 0)], 1.0f64);
397
398        // Check mat D
399        assert_eq!(ss_model.mat_d().shape(), (1, 1));
400        assert_eq!(ss_model.mat_d()[(0, 0)], 8.0f64);
401    }
402
403    #[test]
404    fn test_compute_poles_pure_real() {
405        let ss_model = DiscreteStateSpaceModel::from_matrices(
406            &nalgebra::dmatrix![2.0, 0.0; 0.0, 1.0],
407            &nalgebra::dmatrix![],
408            &nalgebra::dmatrix![],
409            &nalgebra::dmatrix![],
410            0.05,
411        );
412
413        let poles = ss_model.poles();
414
415        assert_eq!(poles.len(), 2);
416        assert_eq!(poles[0].re, 2.0);
417        assert_eq!(poles[0].im, 0.0);
418        assert_eq!(poles[1].re, 1.0);
419        assert_eq!(poles[1].im, 0.0);
420    }
421
422    #[test]
423    fn test_compute_poles_pure_im() {
424        let ss_model = DiscreteStateSpaceModel::from_matrices(
425            &nalgebra::dmatrix![0.0, -1.0; 1.0, 0.0],
426            &nalgebra::dmatrix![],
427            &nalgebra::dmatrix![],
428            &nalgebra::dmatrix![],
429            0.05,
430        );
431
432        let poles = ss_model.poles();
433
434        assert_eq!(poles.len(), 2);
435        assert_eq!(poles[0].re, 0.0);
436        assert_eq!(poles[0].im, 1.0);
437        assert_eq!(poles[1].re, 0.0);
438        assert_eq!(poles[1].im, -1.0);
439    }
440
441    #[test]
442    fn test_compute_poles_real_and_imaginary_part() {
443        let ss_model = DiscreteStateSpaceModel::from_matrices(
444            &nalgebra::dmatrix![1.0, -2.0; 2.0, 1.0],
445            &nalgebra::dmatrix![],
446            &nalgebra::dmatrix![],
447            &nalgebra::dmatrix![],
448            0.05,
449        );
450
451        let poles = ss_model.poles();
452
453        assert_eq!(poles.len(), 2);
454        assert_eq!(poles[0].re, 1.0);
455        assert_eq!(poles[0].im, 2.0);
456        assert_eq!(poles[1].re, 1.0);
457        assert_eq!(poles[1].im, -2.0);
458    }
459}