Skip to main content

logicaffeine_proof/
linalg.rs

1//! Exact linear algebra over ℚ — the certified core of the LOGOS numerical stack.
2//!
3//! Floating-point LAPACK *runs* a solve; this *proves* one. Every entry is an exact BigInt-backed
4//! [`Rational`], so `A·x = b` is decided as an EXACT identity — the residual is identically zero,
5//! not "close enough" — and `det A ≠ 0` certifies nonsingularity outright. These are the primitives
6//! the proof engine reasons about, and they are FAIL-CLOSED in the same discipline as the mod-p /
7//! SoS routes: the certificate (`residual_zero`) is an independent re-multiplication, never trust in
8//! the solver. An ill-conditioned system (e.g. a Hilbert matrix) that floating point cannot solve to
9//! zero residual is solved here EXACTLY, and the exactness is a checkable fact.
10
11use logicaffeine_base::Rational;
12
13/// A·x over the rationals, computed exactly.
14pub fn matvec(a: &[Vec<Rational>], x: &[Rational]) -> Vec<Rational> {
15    a.iter()
16        .map(|row| {
17            row.iter()
18                .zip(x)
19                .fold(Rational::zero(), |acc, (aij, xj)| acc.add(&aij.mul(xj)))
20        })
21        .collect()
22}
23
24/// Is `A·x == b` exactly, component by component? The independent re-check behind a certified solve.
25pub fn residual_is_zero(a: &[Vec<Rational>], x: &[Rational], b: &[Rational]) -> bool {
26    let ax = matvec(a, x);
27    ax.len() == b.len() && ax.iter().zip(b).all(|(l, r)| l == r)
28}
29
30/// Solve `A·x = b` exactly by Gauss–Jordan elimination with nonzero pivoting (n×n).
31/// Returns `None` iff `A` is singular. All arithmetic is exact — the answer `1/3` stays `1/3`.
32pub fn solve(a: &[Vec<Rational>], b: &[Rational]) -> Option<Vec<Rational>> {
33    let n = a.len();
34    if n == 0 || a.iter().any(|r| r.len() != n) || b.len() != n {
35        return None;
36    }
37    // augmented [A | b]
38    let mut m: Vec<Vec<Rational>> = a
39        .iter()
40        .zip(b)
41        .map(|(row, bi)| {
42            let mut r = row.clone();
43            r.push(bi.clone());
44            r
45        })
46        .collect();
47    for col in 0..n {
48        // pivot: the first row at or below the diagonal with a nonzero entry in this column
49        let piv = (col..n).find(|&r| !m[r][col].is_zero())?;
50        m.swap(col, piv);
51        let inv = m[col][col].recip()?; // pivot is nonzero → Some
52                                        // scale the pivot row so the pivot becomes exactly 1
53        for j in col..=n {
54            m[col][j] = m[col][j].mul(&inv);
55        }
56        // eliminate this column from every other row
57        for r in 0..n {
58            if r == col {
59                continue;
60            }
61            let f = m[r][col].clone();
62            if f.is_zero() {
63                continue;
64            }
65            for j in col..=n {
66                m[r][j] = m[r][j].sub(&f.mul(&m[col][j]));
67            }
68        }
69    }
70    Some((0..n).map(|i| m[i][n].clone()).collect())
71}
72
73/// Exact determinant via forward elimination (product of pivots × permutation sign).
74pub fn det(a: &[Vec<Rational>]) -> Rational {
75    let n = a.len();
76    if n == 0 || a.iter().any(|r| r.len() != n) {
77        return Rational::zero();
78    }
79    let mut m: Vec<Vec<Rational>> = a.to_vec();
80    let mut d = Rational::one();
81    for col in 0..n {
82        let piv = match (col..n).find(|&r| !m[r][col].is_zero()) {
83            Some(p) => p,
84            None => return Rational::zero(),
85        };
86        if piv != col {
87            m.swap(col, piv);
88            d = d.negated();
89        }
90        d = d.mul(&m[col][col]);
91        let inv = m[col][col].recip().expect("nonzero pivot");
92        for r in (col + 1)..n {
93            let f = m[r][col].mul(&inv);
94            if f.is_zero() {
95                continue;
96            }
97            for j in col..n {
98                m[r][j] = m[r][j].sub(&f.mul(&m[col][j]));
99            }
100        }
101    }
102    d
103}
104
105/// A solve carrying its own certificate: the exact solution plus independently re-checked flags that
106/// `A·x = b` holds exactly and `det A ≠ 0`. This is what "prove, not just run" means for a solve.
107pub struct Certified {
108    pub x: Vec<Rational>,
109    /// `A·x == b` verified by re-multiplication over exact rationals (not solver trust).
110    pub residual_zero: bool,
111    /// `det A ≠ 0` — the system has a unique solution.
112    pub nonsingular: bool,
113}
114
115/// Solve and certify in one call. `None` iff singular.
116pub fn certify_solve(a: &[Vec<Rational>], b: &[Rational]) -> Option<Certified> {
117    let x = solve(a, b)?;
118    let residual_zero = residual_is_zero(a, &x, b);
119    let nonsingular = !det(a).is_zero();
120    Some(Certified {
121        x,
122        residual_zero,
123        nonsingular,
124    })
125}
126
127/// An exact LU factorization `P·A = L·U`: `perm` is the row permutation (P), `l` is unit
128/// lower-triangular, `u` is upper-triangular. `None` iff singular.
129pub struct Lu {
130    pub perm: Vec<usize>,
131    pub l: Vec<Vec<Rational>>,
132    pub u: Vec<Vec<Rational>>,
133}
134
135/// Exact LU with partial (nonzero) pivoting. Every multiplier is an exact rational.
136pub fn lu(a: &[Vec<Rational>]) -> Option<Lu> {
137    let n = a.len();
138    if n == 0 || a.iter().any(|r| r.len() != n) {
139        return None;
140    }
141    let mut u = a.to_vec();
142    let mut l: Vec<Vec<Rational>> = (0..n)
143        .map(|i| {
144            (0..n)
145                .map(|j| {
146                    if i == j {
147                        Rational::one()
148                    } else {
149                        Rational::zero()
150                    }
151                })
152                .collect()
153        })
154        .collect();
155    let mut perm: Vec<usize> = (0..n).collect();
156    for k in 0..n {
157        let piv = (k..n).find(|&r| !u[r][k].is_zero())?;
158        if piv != k {
159            u.swap(k, piv);
160            perm.swap(k, piv);
161            // the already-computed multipliers (columns < k) travel with their rows
162            for j in 0..k {
163                let tmp = l[k][j].clone();
164                l[k][j] = l[piv][j].clone();
165                l[piv][j] = tmp;
166            }
167        }
168        let inv = u[k][k].recip()?;
169        for i in (k + 1)..n {
170            let f = u[i][k].mul(&inv);
171            l[i][k] = f.clone();
172            for j in k..n {
173                u[i][j] = u[i][j].sub(&f.mul(&u[k][j]));
174            }
175        }
176    }
177    Some(Lu { perm, l, u })
178}
179
180/// Exact matrix product `A·B`.
181pub fn matmul(a: &[Vec<Rational>], b: &[Vec<Rational>]) -> Vec<Vec<Rational>> {
182    let (n, m, p) = (a.len(), b.len(), b[0].len());
183    (0..n)
184        .map(|i| {
185            (0..p)
186                .map(|j| (0..m).fold(Rational::zero(), |acc, k| acc.add(&a[i][k].mul(&b[k][j]))))
187                .collect()
188        })
189        .collect()
190}
191
192/// Certify a factorization: is `P·A == L·U` exactly? (P·A is A with its rows permuted by `perm`.)
193pub fn lu_is_exact(a: &[Vec<Rational>], f: &Lu) -> bool {
194    let pa: Vec<Vec<Rational>> = f.perm.iter().map(|&i| a[i].clone()).collect();
195    matmul(&f.l, &f.u) == pa
196}
197
198/// The exact n×n identity matrix.
199pub fn identity(n: usize) -> Vec<Vec<Rational>> {
200    (0..n)
201        .map(|i| {
202            (0..n)
203                .map(|j| {
204                    if i == j {
205                        Rational::one()
206                    } else {
207                        Rational::zero()
208                    }
209                })
210                .collect()
211        })
212        .collect()
213}
214
215/// Exact transpose.
216pub fn transpose(a: &[Vec<Rational>]) -> Vec<Vec<Rational>> {
217    if a.is_empty() {
218        return Vec::new();
219    }
220    let (n, m) = (a.len(), a[0].len());
221    (0..m)
222        .map(|j| (0..n).map(|i| a[i][j].clone()).collect())
223        .collect()
224}
225
226/// Exact matrix inverse via Gauss–Jordan on `[A | I]`. `None` iff singular.
227pub fn inverse(a: &[Vec<Rational>]) -> Option<Vec<Vec<Rational>>> {
228    let n = a.len();
229    if n == 0 || a.iter().any(|r| r.len() != n) {
230        return None;
231    }
232    // augmented [A | I], width 2n
233    let mut m: Vec<Vec<Rational>> = a
234        .iter()
235        .enumerate()
236        .map(|(i, row)| {
237            let mut r = row.clone();
238            for j in 0..n {
239                r.push(if i == j {
240                    Rational::one()
241                } else {
242                    Rational::zero()
243                });
244            }
245            r
246        })
247        .collect();
248    for col in 0..n {
249        let piv = (col..n).find(|&r| !m[r][col].is_zero())?;
250        m.swap(col, piv);
251        let inv = m[col][col].recip()?;
252        for j in col..(2 * n) {
253            m[col][j] = m[col][j].mul(&inv);
254        }
255        for r in 0..n {
256            if r == col {
257                continue;
258            }
259            let f = m[r][col].clone();
260            if f.is_zero() {
261                continue;
262            }
263            for j in col..(2 * n) {
264                m[r][j] = m[r][j].sub(&f.mul(&m[col][j]));
265            }
266        }
267    }
268    Some(
269        (0..n)
270            .map(|i| (0..n).map(|j| m[i][n + j].clone()).collect())
271            .collect(),
272    )
273}
274
275/// Exact rank (works for rectangular matrices) via row reduction — the number of nonzero pivots.
276pub fn rank(a: &[Vec<Rational>]) -> usize {
277    if a.is_empty() {
278        return 0;
279    }
280    let (rows, cols) = (a.len(), a[0].len());
281    let mut m = a.to_vec();
282    let mut prow = 0usize;
283    for col in 0..cols {
284        if prow == rows {
285            break;
286        }
287        let piv = match (prow..rows).find(|&r| !m[r][col].is_zero()) {
288            Some(p) => p,
289            None => continue,
290        };
291        m.swap(prow, piv);
292        let inv = m[prow][col].recip().expect("nonzero pivot");
293        for r in 0..rows {
294            if r == prow {
295                continue;
296            }
297            let f = m[r][col].mul(&inv);
298            if f.is_zero() {
299                continue;
300            }
301            for j in col..cols {
302                m[r][j] = m[r][j].sub(&f.mul(&m[prow][j]));
303            }
304        }
305        prow += 1;
306    }
307    prow
308}
309
310/// Is `A·B == I` exactly? (independent re-check behind a certified inverse)
311pub fn is_inverse(a: &[Vec<Rational>], b: &[Vec<Rational>]) -> bool {
312    matmul(a, b) == identity(a.len())
313}
314
315#[cfg(test)]
316#[path = "../../logicaffeine_tests/unit/logicaffeine_proof/linalg/tests.rs"]
317mod tests;