control_sys/
analysis.rs

1extern crate nalgebra as na;
2
3use crate::model::StateSpaceModel;
4
5/// Computes the controllability matrix for a given state-space representation.
6///
7/// # Arguments
8///
9/// * `mat_a` - The state matrix (A) of the system.
10/// * `mat_b` - The input matrix (B) of the system.
11///
12/// # Returns
13///
14/// * `Result<na::DMatrix<f64>, &'static str>` - The controllability matrix if successful, or an error message if the A matrix is not square.
15///
16/// # Errors
17///
18/// This function will return an error if the A matrix is not square.
19///
20/// # Panics
21///
22/// This function does not panic.
23///
24/// # Examples
25///
26/// ```
27/// use control_sys::analysis;
28///
29/// let mat_a = nalgebra::dmatrix![1.0, -2.0;
30///                                2.0, 1.0];
31/// let mat_b = nalgebra::dmatrix![1.0;
32///                                2.0];
33/// match analysis::compute_controllability_matrix(&mat_a, &mat_b) {
34///     Ok(result) => { println!("Controllability matrix is {}", result); },
35///     Err(msg) => { println!("{}", msg)}
36/// }
37/// ```
38///
39pub fn compute_controllability_matrix(
40    mat_a: &na::DMatrix<f64>,
41    mat_b: &na::DMatrix<f64>,
42) -> Result<na::DMatrix<f64>, &'static str> {
43    if !mat_a.is_square() {
44        return Err("Error when computing controllability matrix. The A matrix is not square.");
45    }
46
47    let n = mat_a.nrows(); // Number of states
48    let mut controllability_matrix = mat_b.clone_owned();
49
50    // Generate [B, AB, A^2B, ...]
51    for i in 1..n {
52        let column_block = mat_a.pow(i as u32) * mat_b; // Compute A^i * B
53        controllability_matrix = na::stack![controllability_matrix, column_block];
54    }
55
56    Ok(controllability_matrix)
57}
58
59/// Checks if a given state-space model is controllable.
60///
61/// # Arguments
62///
63/// * `model` - A reference to a state-space model that implements the `StateSpaceModel` trait.
64///
65/// # Returns
66///
67/// * `bool` - `true` if the system is controllable, `false` otherwise.
68///
69/// # Panics
70///
71/// This function will panic if the computation of the controllability matrix fails.
72///
73/// # Examples
74///
75/// ```
76/// use control_sys::analysis;
77/// use control_sys::model;
78///
79/// let ss_model = model::DiscreteStateSpaceModel::from_matrices(
80/// &nalgebra::dmatrix![1.0, -2.0;
81///                     2.0, 1.0],
82/// &nalgebra::dmatrix![1.0;
83///                     2.0],
84/// &nalgebra::dmatrix![],
85/// &nalgebra::dmatrix![],
86/// 0.05,
87/// );
88/// let (is_controllable, controllability_matrix) = analysis::is_ss_controllable(&ss_model);
89/// ```
90pub fn is_ss_controllable<T: StateSpaceModel>(model: &T) -> (bool, na::DMatrix<f64>) {
91    let mat_a = model.mat_a();
92    let mat_b = model.mat_b();
93
94    // We know that mat_a is square so we simply unwrap() the result
95    let mat_contr = compute_controllability_matrix(&mat_a, &mat_b)
96        .expect("State matrix of the model is not square");
97
98    // Since the input is a state space model, we expect A to be square
99    return (mat_contr.rank(1e-3) == mat_a.nrows(), mat_contr);
100}
101
102/// Computes the observability matrix for a given state-space representation.
103///
104/// # Arguments
105///
106/// * `mat_a` - The state matrix (A) of the system.
107/// * `mat_c` - The input matrix (B) of the system.
108///
109/// # Returns
110///
111/// * `Result<na::DMatrix<f64>, &'static str>` - The observability matrix if successful, or an error message if the A matrix is not square.
112///
113/// # Errors
114///
115/// This function will return an error if the A matrix is not square.
116///
117/// # Panics
118///
119/// This function does not panic.
120///
121/// # Examples
122///
123/// ```
124/// use control_sys::analysis;
125///
126/// let mat_a = nalgebra::dmatrix![1.0, -2.0;
127///                                2.0, 1.0];
128/// let mat_c = nalgebra::dmatrix![1.0, 2.0];
129///
130/// match analysis::compute_observability_matrix(&mat_a, &mat_c) {
131///     Ok(result) => { println!("Observability matrix is {}", result); },
132///     Err(msg) => { println!("{}", msg)}
133/// }
134/// ```
135pub fn compute_observability_matrix(
136    mat_a: &na::DMatrix<f64>,
137    mat_c: &na::DMatrix<f64>,
138) -> Result<na::DMatrix<f64>, &'static str> {
139    if !mat_a.is_square() {
140        return Err("Error when computing observability matrix. The A matrix is not square.");
141    }
142
143    let n = mat_a.nrows(); // Number of states
144    let mut observability_mat = mat_c.clone_owned();
145
146    // Generate [C; CA; CA^2;...]
147    for i in 1..n {
148        let column_block = mat_c * mat_a.pow(i as u32);
149        observability_mat = na::stack![observability_mat; column_block];
150    }
151
152    Ok(observability_mat)
153}
154
155/// Checks if a given state-space model is observable.
156///
157/// # Arguments
158///
159/// * `model` - A reference to a state-space model that implements the `StateSpaceModel` trait.
160///
161/// # Returns
162///
163/// * A tuple with a `bool` which is `true` if the system is controllable, `false` otherwise and the observability matrix.
164///
165/// # Panics
166///
167/// This function will panic if the computation of the controllability matrix fails.
168///
169/// # Examples
170///
171/// ```
172/// use control_sys::analysis;
173/// use control_sys::model;
174///
175/// let ss_model = model::DiscreteStateSpaceModel::from_matrices(
176/// &nalgebra::dmatrix![1.0, -2.0;
177///                     2.0, 1.0],
178/// &nalgebra::dmatrix![],
179/// &nalgebra::dmatrix![1.0, 2.0],
180/// &nalgebra::dmatrix![],
181/// 0.05,
182/// );
183/// let (is_observable, observability_matrix) = analysis::is_ss_observable(&ss_model);
184/// ```
185pub fn is_ss_observable<T: StateSpaceModel>(model: &T) -> (bool, na::DMatrix<f64>) {
186    let mat_a = model.mat_a();
187    let mat_c = model.mat_c();
188
189    // StateSpaceModel performs check of the matrices so computing observability matrix should not fail
190    let mat_obs = compute_observability_matrix(&mat_a, &mat_c)
191        .expect("State matrix of the model is not square");
192
193    // Since the input is a state space model, we expect A to be square
194    return (mat_obs.rank(1e-3) == mat_a.nrows(), mat_obs);
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    use crate::model::DiscreteStateSpaceModel;
202
203    #[test]
204    fn test_compute_controllability_matrix_2x2() {
205        let mat_a = nalgebra::dmatrix![1.0, -2.0;
206                                       2.0, 1.0];
207        let mat_b = nalgebra::dmatrix![1.0; 
208                                       2.0];
209
210        let result = compute_controllability_matrix(&mat_a, &mat_b).unwrap();
211
212        assert_eq!(result.nrows(), mat_b.ncols() * mat_a.nrows());
213        assert_eq!(result.ncols(), mat_a.ncols());
214        assert_eq!(result, na::stack![mat_b, mat_a * &mat_b])
215    }
216
217    #[test]
218    fn test_compute_controllability_matrix_mat_a_not_square() {
219        let mat_a = nalgebra::dmatrix![1.0, -2.0];
220        let mat_b = nalgebra::dmatrix![1.0; 
221                                       2.0];
222
223        let result = compute_controllability_matrix(&mat_a, &mat_b);
224
225        assert_eq!(
226            result,
227            Err("Error when computing controllability matrix. The A matrix is not square.")
228        );
229    }
230
231    #[test]
232    fn test_controllability_2x2_controllable() {
233        let ss_model = DiscreteStateSpaceModel::from_matrices(
234            &nalgebra::dmatrix![1.0, -2.0; 
235                            2.0, 1.0],
236            &nalgebra::dmatrix![1.0;
237                            2.0],
238            &nalgebra::dmatrix![],
239            &nalgebra::dmatrix![],
240            0.05,
241        );
242
243        let (result, _) = is_ss_controllable(&ss_model);
244
245        assert!(result);
246    }
247
248    #[test]
249    fn test_controllability_3x3_not_controllable() {
250        let ss_model = DiscreteStateSpaceModel::from_matrices(
251            &na::dmatrix![
252            0.0, 1.0, 0.0;
253            0.0, 0.0, 1.0;
254            0.0, 0.0, 0.0],
255            &na::dmatrix![
256            1.0;
257            0.0;
258            0.0],
259            &nalgebra::dmatrix![],
260            &nalgebra::dmatrix![],
261            0.05,
262        );
263
264        assert!(!is_ss_controllable(&ss_model).0);
265    }
266
267    #[test]
268    fn test_compute_observability_matrix_2x2() {
269        let mat_a = nalgebra::dmatrix![1.0, -2.0;
270                                       2.0, 1.0];
271        let mat_c = nalgebra::dmatrix![1.0, 2.0];
272
273        let result = compute_observability_matrix(&mat_a, &mat_c).unwrap();
274
275        assert_eq!(result.nrows(), mat_a.ncols());
276        assert_eq!(result.ncols(), mat_c.ncols());
277        assert_eq!(result, na::stack![&mat_c; &mat_c * mat_a]);
278    }
279
280    #[test]
281    fn test_compute_observability_matrix_mat_a_not_square() {
282        let mat_a = nalgebra::dmatrix![1.0, -2.0];
283        let mat_b = nalgebra::dmatrix![1.0; 
284                                       2.0];
285
286        let result = compute_observability_matrix(&mat_a, &mat_b);
287
288        assert_eq!(
289            result,
290            Err("Error when computing observability matrix. The A matrix is not square.")
291        );
292    }
293
294    #[test]
295    fn test_is_observable_2x2_observable_system() {
296        let ss_model = DiscreteStateSpaceModel::from_matrices(
297            &nalgebra::dmatrix![1.0, -2.0; 
298                            2.0, 1.0],
299            &nalgebra::dmatrix![],
300            &nalgebra::dmatrix![1.0, 2.0],
301            &nalgebra::dmatrix![],
302            0.05,
303        );
304
305        let (result, _) = is_ss_observable(&ss_model);
306
307        assert!(result);
308    }
309
310    #[test]
311    fn test_observability_3x3_not_observable() {
312        let ss_model = DiscreteStateSpaceModel::from_matrices(
313            &na::dmatrix![
314            1.0, 0.0, 0.0;
315            0.0, 1.0, 0.0;
316            0.0, 0.0, 0.0],
317            &na::dmatrix![],
318            &nalgebra::dmatrix![1.0, 0.0, 0.0],
319            &nalgebra::dmatrix![],
320            0.05,
321        );
322
323        let (observable, obs_mat) = is_ss_observable(&ss_model);
324
325        assert!(!observable);
326        assert_eq!(
327            obs_mat,
328            na::dmatrix![
329            1.0, 0.0, 0.0;
330            1.0, 0.0, 0.0;
331            1.0, 0.0, 0.0]
332        );
333    }
334}