1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//! Solves `Aᵀ·P·A − P + Q = 0` for `P`, by adding the terms of its series in doubling blocks.
//!
//! The answer is the sum `P = Q + Aᵀ·Q·A + (Aᵀ)²·Q·A² + …`, which only adds up to something finite
//! when repeated application of `A` shrinks every direction. Squaring `A` on each pass doubles how
//! many terms the running total covers, so a handful of passes covers an enormous number of terms.
//! When `A` does not shrink, the running total grows without bound instead, and the solver says so
//! rather than returning a number. Reference values for the tests come from SciPy.
use crateLinalgError;
use crateMatrix;
use crateNumeric;
/// How many doubling passes to allow. Each pass doubles how many terms the total covers, so this
/// is far more than any settling problem needs; reaching it means the total is not settling.
const MAXIMUM_PASSES: usize = 64;
/// Finds the `P` that satisfies `Aᵀ·P·A − P + Q = 0`, given a `Q` that reads the same across the
/// diagonal.
///
/// This is the standard way to certify that a closed loop settles: a solution exists only when
/// repeated application of `A` shrinks every direction, so
/// [`LinalgError::DidNotConverge`](crate::error::LinalgError::DidNotConverge) is the verdict that
/// it does not, not a numerical failure. Costs `O(n³)` per pass with a budget of 64 passes, so run
/// it once at design time rather than inside a control loop.
///
/// Returns [`LinalgError::NonFinite`](crate::error::LinalgError::NonFinite) if any entry is not
/// finite, [`LinalgError::NotSymmetric`](crate::error::LinalgError::NotSymmetric) if `q` does not
/// read the same across the diagonal, or
/// [`LinalgError::DidNotConverge`](crate::error::LinalgError::DidNotConverge) if the total has not
/// settled within the budget.
///
/// ```
/// use multicalc::linear_algebra::{Matrix, solve_discrete_lyapunov};
///
/// // A single state that keeps half of itself each step, with Q = 1. The series is
/// // 1 + 1/4 + 1/16 + ... = 4/3.
/// let a = Matrix::<1, 1>::new([[0.5]]);
/// let q = Matrix::<1, 1>::new([[1.0]]);
/// let p = solve_discrete_lyapunov(a, q).unwrap();
/// assert!((p[(0, 0)] - 4.0 / 3.0).abs() < 1e-12);
///
/// // A state that grows has no answer.
/// let unstable = Matrix::<1, 1>::new([[1.5]]);
/// assert!(solve_discrete_lyapunov(unstable, q).is_err());
/// ```