Skip to main content

a/math/matrix/
exact.rs

1//! Exact algorithms on matrices of numbers: determinants, ranks, characteristic polynomials,
2//! adjugates, inverses and null spaces, without fractions.
3
4use super::Matrix;
5use crate::math::numbers::Number;
6
7impl Matrix<Number> {
8    /// The determinant, exactly, by Bareiss's fraction-free elimination: each step's elements
9    /// are minors of the matrix, `(a_ij·a_kk - a_ik·a_kj) / p` with the previous pivot `p`
10    /// dividing exactly, so the numbers grow no longer than the determinant. About `n³`
11    /// products. The determinant of the `0 × 0` matrix is 1.
12    ///
13    /// # Panics
14    ///
15    /// Panics if the matrix is not square.
16    ///
17    /// # Examples
18    ///
19    /// ```
20    /// use a::math::matrix::Matrix;
21    /// use a::math::numbers::Number;
22    ///
23    /// let a = Matrix::from_rows(&[[2, -1, 0], [-1, 2, -1], [0, -1, 2]]).map(|&x| Number::from(x));
24    /// assert_eq!(a.det(), 4);
25    /// ```
26    #[track_caller]
27    pub fn det(&self) -> Number {
28        assert!(self.is_square(), "the matrix is not square");
29        let n = self.rows();
30        let mut m = self.clone();
31        let (rank, swaps) = bareiss(&mut m);
32        if rank < n {
33            return Number::default();
34        }
35        if n == 0 {
36            return Number::from(1u8);
37        }
38        let d = m[(n - 1, n - 1)].clone();
39        if swaps % 2 == 1 { -d } else { d }
40    }
41
42    /// The rank, exactly: the number of pivots of Bareiss's fraction-free elimination.
43    pub fn rank(&self) -> usize {
44        bareiss(&mut self.clone()).0
45    }
46
47    /// The coefficients of the characteristic polynomial `det(x·I - A)`, from `xⁿ` down,
48    /// exactly, by Berkowitz's method: the polynomial of each leading block from the one before
49    /// by a Toeplitz matrix of `1`, `-a_kk` and `-R·A_k^j·C`, without a division. About `n⁴/2`
50    /// products.
51    ///
52    /// # Panics
53    ///
54    /// Panics if the matrix is not square.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use a::math::matrix::Matrix;
60    /// use a::math::numbers::Number;
61    ///
62    /// let a = Matrix::from_rows(&[[2, 1], [1, 2]]).map(|&x| Number::from(x));
63    /// assert_eq!(a.characteristic_polynomial(), [1, -4, 3].map(Number::from));
64    /// ```
65    #[track_caller]
66    pub fn characteristic_polynomial(&self) -> Vec<Number> {
67        assert!(self.is_square(), "the matrix is not square");
68        let n = self.rows();
69        let mut p = vec![Number::from(1u8)];
70        for r in 0..n {
71            // `t = (1, -a_rr, -R·C, -R·A_r·C, …)` for the row `R` and the column `C` beside the
72            // leading `r × r` block `A_r`.
73            let mut t = Vec::with_capacity(r + 2);
74            t.push(Number::from(1u8));
75            t.push(-&self[(r, r)]);
76            let mut v: Vec<Number> = (0..r).map(|i| self[(i, r)].clone()).collect();
77            for j in 0..r {
78                let mut q = Number::default();
79                for (k, x) in v.iter().enumerate() {
80                    if !x.is_zero() {
81                        q += &self[(r, k)] * x;
82                    }
83                }
84                t.push(-q);
85                if j + 1 < r {
86                    v = (0..r)
87                        .map(|i| {
88                            let mut s = Number::default();
89                            for (k, x) in v.iter().enumerate() {
90                                if !x.is_zero() {
91                                    s += &self[(i, k)] * x;
92                                }
93                            }
94                            s
95                        })
96                        .collect();
97                }
98            }
99            // `p ← T·p`, `T` lower triangular Toeplitz with the first column `t`.
100            let next = (0..r + 2)
101                .map(|i| {
102                    let mut s = Number::default();
103                    for (j, c) in p.iter().enumerate().take(i + 1) {
104                        if !c.is_zero() && !t[i - j].is_zero() {
105                            s += &t[i - j] * c;
106                        }
107                    }
108                    s
109                })
110                .collect();
111            p = next;
112        }
113        p
114    }
115
116    /// The adjugate `adj(A)`, the transpose of the matrix of cofactors, with
117    /// `A·adj(A) = adj(A)·A = det(A)·I`, for singular matrices too: by Cayley and Hamilton,
118    /// `(-1)ⁿ⁻¹·(Aⁿ⁻¹ + c₁·Aⁿ⁻² + … + cₙ₋₁·I)` for the characteristic polynomial's `c`.
119    ///
120    /// # Panics
121    ///
122    /// Panics if the matrix is not square.
123    #[track_caller]
124    pub fn adjugate(&self) -> Matrix<Number> {
125        assert!(self.is_square(), "the matrix is not square");
126        let n = self.rows();
127        if n == 0 {
128            return Matrix::zeros(0, 0);
129        }
130        let c = self.characteristic_polynomial();
131        let mut m = Matrix::<Number>::identity(n);
132        for c in &c[1..n] {
133            m = self * &m;
134            for i in 0..n {
135                m[(i, i)] += c;
136            }
137        }
138        if n.is_multiple_of(2) { -m } else { m }
139    }
140
141    /// The inverse as the adjugate and the determinant, `A⁻¹ = adj(A) / det(A)`, exactly, or
142    /// `None` when the determinant is zero.
143    ///
144    /// # Panics
145    ///
146    /// Panics if the matrix is not square.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use a::math::matrix::Matrix;
152    /// use a::math::numbers::Number;
153    ///
154    /// let a = Matrix::from_rows(&[[2, 1], [5, 3]]).map(|&x| Number::from(x));
155    /// let (adj, det) = a.inverse().unwrap();
156    /// assert_eq!(det, 1);
157    /// assert_eq!(adj, Matrix::from_rows(&[[3, -1], [-5, 2]]).map(|&x| Number::from(x)));
158    /// ```
159    #[track_caller]
160    pub fn inverse(&self) -> Option<(Matrix<Number>, Number)> {
161        let det = self.det();
162        if det.is_zero() {
163            return None;
164        }
165        Some((self.adjugate(), det))
166    }
167
168    /// A basis of the null space `{x : A·x = 0}` over the rationals, as the columns of an
169    /// `n × (n - rank)` matrix of integers: one vector for each column without a pivot in the
170    /// fraction-free echelon form, its free coordinate positive and its elements without a
171    /// common factor.
172    ///
173    /// # Examples
174    ///
175    /// ```
176    /// use a::math::matrix::Matrix;
177    /// use a::math::numbers::Number;
178    ///
179    /// let a = Matrix::from_rows(&[[1, 2, 3], [2, 4, 6]]).map(|&x| Number::from(x));
180    /// let null = a.null_space();
181    /// assert_eq!(null.shape(), (3, 2));
182    /// assert!((&a * &null).as_slice().iter().all(|x| x.is_zero()));
183    /// ```
184    pub fn null_space(&self) -> Matrix<Number> {
185        let n = self.cols();
186        let mut e = self.clone();
187        let (rank, _) = bareiss(&mut e);
188        // The pivot columns of the echelon form.
189        let mut pivots = Vec::with_capacity(rank);
190        let mut row = 0;
191        for c in 0..n {
192            if row < rank && !e[(row, c)].is_zero() {
193                pivots.push(c);
194                row += 1;
195            }
196        }
197        let free: Vec<usize> = (0..n).filter(|c| !pivots.contains(c)).collect();
198        let mut basis = Matrix::<Number>::zeros(n, free.len());
199        for (b, &f) in free.iter().enumerate() {
200            // `y`, a multiple of the solution with `x_f = 1` and the other free coordinates 0.
201            let mut y = vec![Number::default(); n];
202            y[f] = Number::from(1u8);
203            for i in (0..rank).rev() {
204                let mut s = Number::default();
205                for c in i..n {
206                    if c != pivots[i] && !y[c].is_zero() && !e[(i, c)].is_zero() {
207                        s -= &e[(i, c)] * &y[c];
208                    }
209                }
210                let pivot = &e[(i, pivots[i])];
211                let g = s.gcd(pivot);
212                if !g.is_zero() {
213                    let scale = pivot.div_exact(&g).abs();
214                    if scale != 1 {
215                        for x in &mut y {
216                            *x *= &scale;
217                        }
218                        s *= &scale;
219                    }
220                }
221                y[pivots[i]] = s.div_exact(pivot);
222            }
223            let g = y.iter().fold(Number::default(), |g, x| g.gcd(x));
224            for (i, x) in y.into_iter().enumerate() {
225                basis[(i, b)] = if g > 1 { x.div_exact(&g) } else { x };
226            }
227            if basis[(f, b)].is_negative() {
228                for i in 0..n {
229                    basis[(i, b)] = -&basis[(i, b)];
230                }
231            }
232        }
233        basis
234    }
235}
236
237/// Bareiss's fraction-free elimination of `m` to an echelon form in place, the pivot of each
238/// row the first non-zero element at or below it in the next column that has one: the rank,
239/// and the number of row swaps.
240fn bareiss(m: &mut Matrix<Number>) -> (usize, usize) {
241    let (rows, cols) = m.shape();
242    let mut previous = Number::from(1u8);
243    let mut r = 0;
244    let mut swaps = 0;
245    for c in 0..cols {
246        if r == rows {
247            break;
248        }
249        let Some(p) = (r..rows).find(|&i| !m[(i, c)].is_zero()) else {
250            continue;
251        };
252        if p != r {
253            m.swap_rows(p, r);
254            swaps += 1;
255        }
256        let pivot = m[(r, c)].clone();
257        for i in r + 1..rows {
258            let below = m[(i, c)].clone();
259            for j in c + 1..cols {
260                let x = &(&m[(i, j)] * &pivot) - &(&below * &m[(r, j)]);
261                m[(i, j)] = if previous == 1 {
262                    x
263                } else {
264                    x.div_exact(&previous)
265                };
266            }
267            m[(i, c)] = Number::default();
268        }
269        previous = pivot;
270        r += 1;
271    }
272    (r, swaps)
273}