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}