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}