Skip to main content

geop_core_math/solvers/
linear.rs

1//! Dense linear algebra, in honest enclosures: a pivot counts as zero
2//! exactly when it could be zero. One implementation per algorithm, for
3//! every solver of the kernel:
4//!
5//! - [`solve_linear_system`]: a small fixed-size system, by Gaussian
6//!   elimination with partial pivoting on sharpened magnitudes.
7//! - [`solve_pivoted`]: a square system with several right-hand sides, by
8//!   Gaussian elimination with partial pivoting on midpoints.
9//! - [`solve_banded`]: a banded system that needs no pivoting.
10//! - [`solve_symmetric2`]: a symmetric 2x2 system — a Newton step on a
11//!   surface's two parameters — by Cramer's rule.
12//! - [`cholesky`] and [`solve_cholesky`]: a symmetric positive
13//!   semi-definite one, rows that say nothing new left out.
14//! - [`shortest_solution`]: the shortest step meeting a few linear
15//!   conditions in space — least squares where they are too many.
16//! - [`eliminate`], [`null_space`], [`rank`], [`inverse`]: Gauss-Jordan
17//!   elimination with complete pivoting over a Jacobian — which rows are
18//!   independent, what they leave free, and inverses.
19//!
20//! [`solve_linear_system`] and [`solve_pivoted`] are the same algorithm in
21//! two shapes, but not the same arithmetic: they choose their pivots by
22//! different comparisons, and the latter back-substitutes by multiplying
23//! with the pivot's reciprocal where the former divides. Their callers
24//! build curves and surfaces, which are sensitive to the last bit, so the
25//! two are kept as they are rather than merged into one that would round
26//! differently for one of them.
27
28use crate::{
29    geop_error::{GeopError, GeopResult},
30    matrix::Matrix,
31    scalars::Scalar,
32    vector::{Vector, Vector3},
33};
34
35/// Solve the dense `N x N` system `a * x = b` by Gaussian elimination with
36/// partial pivoting, returning `x`.
37///
38/// Errors if the system is singular — which, with interval scalars, means
39/// the pivot *could* be zero, not merely that it is: a pivot straddling
40/// zero carries no usable information about the solution, and dividing by
41/// it would manufacture an arbitrarily wide answer rather than report that
42/// there isn't one. Callers that can proceed without a solution (a
43/// rank-deficient configuration they have a fallback for) should handle the
44/// error rather than pre-screen the matrix.
45///
46/// Pivot selection compares the *sharpened* magnitudes of the candidates.
47/// That is a conditioning choice, not a correctness claim: any row with a
48/// nonzero pivot yields the same solution set, and picking the largest one
49/// only keeps the elimination numerically well-behaved — so resolving the
50/// comparison to a single value (rather than leaving it three-valued) costs
51/// nothing, exactly as in `NurbSurface::project`'s per-iteration sharpening.
52pub fn solve_linear_system<const N: usize>(
53    a: &Matrix<N, N>,
54    b: &Vector<N>,
55) -> GeopResult<Vector<N>> {
56    let mut a = *a;
57    let mut b = *b;
58
59    for col in 0..N {
60        let mut pivot = col;
61        for row in (col + 1)..N {
62            if a[(row, col)]
63                .abs()
64                .sharpen()
65                .definitely_greater(a[(pivot, col)].abs().sharpen())
66            {
67                pivot = row;
68            }
69        }
70        a.swap_rows(col, pivot);
71        let tmp = b[col];
72        b[col] = b[pivot];
73        b[pivot] = tmp;
74
75        for row in (col + 1)..N {
76            let factor = a[(row, col)].div(a[(col, col)]).map_err(|e| {
77                e.with_context(format!(
78                    "solve_linear_system: singular at column {col}, pivot={:?}",
79                    a[(col, col)]
80                ))
81            })?;
82            for k in col..N {
83                a[(row, k)] = a[(row, k)].sub(factor.mul(a[(col, k)]));
84            }
85            b[row] = b[row].sub(factor.mul(b[col]));
86        }
87    }
88
89    let mut x = Vector::<N>::zero();
90    for row in (0..N).rev() {
91        let mut sum = b[row];
92        for k in (row + 1)..N {
93            sum = sum.sub(a[(row, k)].mul(x[k]));
94        }
95        x[row] = sum.div(a[(row, row)]).map_err(|e| {
96            e.with_context(format!(
97                "solve_linear_system: singular at row {row}, pivot={:?}",
98                a[(row, row)]
99            ))
100        })?;
101    }
102    Ok(x)
103}
104
105/// The solution `(x, y)` of the symmetric 2x2 system
106/// `[[a11, a12], [a12, a22]] (x, y) = (b1, b2)`, by Cramer's rule: an error
107/// where the determinant could be zero.
108pub fn solve_symmetric2(
109    (a11, a12, a22): (Scalar, Scalar, Scalar),
110    (b1, b2): (Scalar, Scalar),
111) -> GeopResult<(Scalar, Scalar)> {
112    let det = a11.mul(a22).sub(a12.mul(a12));
113    Ok((
114        b1.mul(a22).sub(b2.mul(a12)).div(det)?,
115        a11.mul(b2).sub(a12.mul(b1)).div(det)?,
116    ))
117}
118
119/// Solves `a x = rhs` for the `n x n` matrix `a` and `C` right-hand sides
120/// by Gaussian elimination, pivoting on the entry of largest magnitude in
121/// each column. Which row to pivot on is a free choice — every pivot that
122/// cannot be zero gives the same enclosed solution — and the largest is the
123/// well-conditioned one. Fails if no entry of a column definitely differs
124/// from zero.
125#[allow(
126    clippy::needless_range_loop,
127    reason = "rows of one matrix, indexed alike"
128)]
129pub fn solve_pivoted<const C: usize>(
130    mut a: Vec<Vec<Scalar>>,
131    mut rhs: Vec<Vector<C>>,
132) -> GeopResult<Vec<Vector<C>>> {
133    let n = a.len();
134    for col in 0..n {
135        let pivot = (col..n)
136            .max_by(|&i, &j| {
137                a[i][col]
138                    .abs()
139                    .to_f64()
140                    .total_cmp(&a[j][col].abs().to_f64())
141            })
142            .expect("a column has rows");
143        if a[pivot][col].could_be_equal(Scalar::ZERO) {
144            return Err(GeopError::new(format!(
145                "solve_pivoted: column {col} has no entry that is definitely not zero"
146            )));
147        }
148        a.swap(col, pivot);
149        rhs.swap(col, pivot);
150        for row in (col + 1)..n {
151            let factor = a[row][col].div(a[col][col])?;
152            for c in col..n {
153                a[row][c] = a[row][c].sub(factor.mul(a[col][c]));
154            }
155            rhs[row] = rhs[row].sub(&rhs[col].prod_scalar(factor));
156        }
157    }
158    let mut x = vec![Vector::<C>::zero(); n];
159    for row in (0..n).rev() {
160        let mut sum = rhs[row];
161        for col in (row + 1)..n {
162            sum = sum.sub(&x[col].prod_scalar(a[row][col]));
163        }
164        x[row] = sum.prod_scalar(Scalar::ONE.div(a[row][row])?);
165    }
166    Ok(x)
167}
168
169/// Solves `a x = rhs` for the banded `m x m` matrix `a` and `C`
170/// right-hand sides, by Gaussian elimination without pivoting: row `k` is
171/// nonzero only in the columns `first[k] ..= last[k]`, and the bands never
172/// move left down the rows. Safe without pivoting only where the matrix
173/// says so — a B-spline collocation matrix is totally positive (Piegl &
174/// Tiller §9.2.1), the reason the curve interpolation that builds it does
175/// not pivot either.
176///
177/// Elimination without pivoting keeps the band, so it runs within it:
178/// below a pivot only the rows whose band reaches its column, and in them
179/// only the columns the pivot row's band reaches. Outside the band every
180/// factor and every entry is an exact zero, and subtracting zero times
181/// zero changes nothing but the enclosure — outward rounding widened each
182/// entry by an ulp per pivot above it, and the dense elimination took
183/// `m³ / 3` steps for what the band holds in `m (degree + 1)²`.
184#[allow(
185    clippy::needless_range_loop,
186    reason = "rows of one matrix, indexed alike"
187)]
188pub fn solve_banded<const C: usize>(
189    mut a: Vec<Vec<Scalar>>,
190    first: &[usize],
191    last: &[usize],
192    mut rhs: Vec<Vector<C>>,
193) -> GeopResult<Vec<Vector<C>>> {
194    let m = rhs.len();
195    for col in 0..m {
196        let pivot = a[col][col];
197        for row in (col + 1)..m {
198            if first[row] > col {
199                break;
200            }
201            let factor = a[row][col].div(pivot)?;
202            for c in col..=last[col] {
203                a[row][c] = a[row][c].sub(factor.mul(a[col][c]));
204            }
205            for c in 0..C {
206                rhs[row][c] = rhs[row][c].sub(factor.mul(rhs[col][c]));
207            }
208        }
209    }
210
211    let mut x = vec![Vector::<C>::zero(); m];
212    for row in (0..m).rev() {
213        let mut sum = rhs[row];
214        for col in (row + 1)..=last[row] {
215            let coef = a[row][col];
216            for c in 0..C {
217                sum[c] = sum[c].sub(coef.mul(x[col][c]));
218            }
219        }
220        for c in 0..C {
221            x[row][c] = sum[c].div(a[row][row])?;
222        }
223    }
224    Ok(x)
225}
226
227/// The Cholesky factor `L` of the symmetric `a`, `a = L Lᵀ` — leaving out
228/// every row whose pivot could be zero or less, or is too small to divide
229/// by: one that, in the light of
230/// the rows before it, says nothing new (or nothing positive). Which rows
231/// were left out is `true` in the second.
232pub fn cholesky(a: &[Vec<Scalar>]) -> GeopResult<(Vec<Vec<Scalar>>, Vec<bool>)> {
233    let n = a.len();
234    let mut l = vec![vec![Scalar::ZERO; n]; n];
235    let mut left_out = vec![false; n];
236    for i in 0..n {
237        for j in 0..=i {
238            if left_out[j] {
239                continue;
240            }
241            let sum = (0..j).fold(a[i][j], |sum, k| sum.sub(l[i][k].mul(l[j][k])));
242            if i == j {
243                // A pivot positive by so little that dividing by it gives no
244                // finite number says no more than one that could be zero:
245                // the solve through it overflowed, `0 × ∞` in the next row.
246                let root = if sum.definitely_greater(Scalar::ZERO) {
247                    Some(sum.sqrt()?).filter(|r| Scalar::ONE.div(*r).is_ok_and(|v| v.is_finite()))
248                } else {
249                    None
250                };
251                if let Some(root) = root {
252                    l[i][i] = root;
253                } else {
254                    left_out[i] = true;
255                    l[i].iter_mut().for_each(|v| *v = Scalar::ZERO);
256                }
257            } else {
258                l[i][j] = sum.div(l[j][j])?;
259            }
260        }
261    }
262    Ok((l, left_out))
263}
264
265/// The solution of `L Lᵀ x = b`, zero at the rows `left_out`.
266pub fn solve_cholesky(
267    l: &[Vec<Scalar>],
268    left_out: &[bool],
269    b: &[Scalar],
270) -> GeopResult<Vec<Scalar>> {
271    let n = b.len();
272    let mut y = vec![Scalar::ZERO; n];
273    for i in (0..n).filter(|&i| !left_out[i]) {
274        let sum = (0..i).fold(b[i], |sum, k| sum.sub(l[i][k].mul(y[k])));
275        y[i] = sum.div(l[i][i])?;
276    }
277    let mut x = vec![Scalar::ZERO; n];
278    for i in (0..n).rev().filter(|&i| !left_out[i]) {
279        let sum = (i + 1..n).fold(y[i], |sum, k| sum.sub(l[k][i].mul(x[k])));
280        x[i] = sum.div(l[i][i])?;
281    }
282    Ok(x)
283}
284
285/// The outcome of Gauss-Jordan elimination (see [`eliminate`]).
286pub struct Elimination {
287    /// The rows, reduced: row `k` has a one at `pivots[k].0` and zeros at
288    /// every other pivot column.
289    pub rows: Vec<Vec<Scalar>>,
290    /// Per pivot: its column, and the index of the row it was among the
291    /// rows eliminated.
292    pub pivots: Vec<(usize, usize)>,
293    /// Per reduced row: the index of the row it was among the rows
294    /// eliminated — for the rows past the pivots' too.
295    pub origin: Vec<usize>,
296}
297
298/// Gauss-Jordan elimination of `rows` (each over `n` variables) with
299/// complete pivoting: each pivot the entry of largest magnitude among the
300/// rows and the columns not used yet — a free choice among those that
301/// cannot be zero; elimination stops when every candidate could be zero.
302/// Which constraints are independent and which variables they determine is
303/// decided from the rows' own enclosures; where it matters for correctness,
304/// [`crate::solvers::system`]'s enclosure verifies what it is used for.
305///
306/// Complete, not column by column: going column by column takes the first
307/// column with any entry that cannot be zero, and rounding leaves entries
308/// of `1e-16` that cannot. Such a pivot determines a variable the
309/// constraints barely touch, in place of one they do — a half circle
310/// tangent to two parallel sides was left with its sweep free and two
311/// tangencies over the same coordinate, and could not be enclosed.
312pub fn eliminate(mut rows: Vec<Vec<Scalar>>, n: usize) -> Elimination {
313    super::factorised(rows.len(), n);
314    // Per row, the columns it has an entry in other than an exact zero, in
315    // order — of the whole row, which may be longer than `n`: columns past
316    // the pivots' go along, as in `inverse`'s: the only ones that can be a
317    // pivot (among the first `n`), and the only ones a pivot row changes in
318    // another row. The Jacobian of an assembly has a dozen
319    // in a row of hundreds, and a pivot row, still, a few dozen.
320    let mut support: Vec<Vec<usize>> = rows
321        .iter()
322        .map(|row| (0..row.len()).filter(|&c| !row[c].is_zero()).collect())
323        .collect();
324    let mut origin: Vec<usize> = (0..rows.len()).collect();
325    let mut pivots: Vec<(usize, usize)> = Vec::new();
326    let mut used = vec![false; n];
327    let mut r = 0;
328    loop {
329        let size = |v: Scalar| v.abs().lower();
330        let mut best: Option<(usize, usize)> = None;
331        for i in r..rows.len() {
332            for &col in support[i].iter().filter(|&&c| c < n && !used[c]) {
333                let v = rows[i][col];
334                if v.definitely_not_equal(Scalar::ZERO)
335                    && best.is_none_or(|(bi, bc)| size(v).definitely_greater(size(rows[bi][bc])))
336                {
337                    best = Some((i, col));
338                }
339            }
340        }
341        let Some((best, col)) = best else {
342            break;
343        };
344        rows.swap(r, best);
345        support.swap(r, best);
346        origin.swap(r, best);
347        let pivot = rows[r][col];
348        for &c in &support[r] {
349            rows[r][c] = rows[r][c]
350                .div(pivot)
351                .expect("a pivot is definitely not zero");
352        }
353        let pivot_support = std::mem::take(&mut support[r]);
354        for i in (0..rows.len()).filter(|&i| i != r) {
355            let factor = rows[i][col];
356            if factor.is_zero() {
357                continue;
358            }
359            for &c in &pivot_support {
360                rows[i][c] = rows[i][c].sub(factor.mul(rows[r][c]));
361            }
362            support[i] = union(&support[i], &pivot_support);
363        }
364        support[r] = pivot_support;
365        used[col] = true;
366        pivots.push((col, origin[r]));
367        r += 1;
368    }
369    Elimination {
370        rows,
371        pivots,
372        origin,
373    }
374}
375
376/// The columns of `a` and of `b`, in order.
377fn union(a: &[usize], b: &[usize]) -> Vec<usize> {
378    let mut out = Vec::with_capacity(a.len() + b.len());
379    let (mut i, mut j) = (0, 0);
380    while i < a.len() || j < b.len() {
381        match (a.get(i), b.get(j)) {
382            (Some(&x), Some(&y)) if x == y => {
383                out.push(x);
384                i += 1;
385                j += 1;
386            }
387            (Some(&x), Some(&y)) if x < y => {
388                out.push(x);
389                i += 1;
390            }
391            (Some(&x), None) => {
392                out.push(x);
393                i += 1;
394            }
395            (_, Some(&y)) => {
396                out.push(y);
397                j += 1;
398            }
399            (None, None) => unreachable!("the loop ends when both are exhausted"),
400        }
401    }
402    out
403}
404
405/// A basis of the null space of `rows` (each over `n` variables): the
406/// directions in which every row stays put, to first order. One vector per
407/// variable no pivot determines, which is one there and zero at every other
408/// such variable.
409pub fn null_space(rows: Vec<Vec<Scalar>>, n: usize) -> Vec<Vec<Scalar>> {
410    let Elimination { rows, pivots, .. } = eliminate(rows, n);
411    let is_pivot: Vec<bool> = (0..n).map(|c| pivots.iter().any(|p| p.0 == c)).collect();
412    (0..n)
413        .filter(|&f| !is_pivot[f])
414        .map(|f| {
415            let mut v = vec![Scalar::ZERO; n];
416            v[f] = Scalar::ONE;
417            for (i, &(pc, _)) in pivots.iter().enumerate() {
418                v[pc] = rows[i][f].neg();
419            }
420            v
421        })
422        .collect()
423}
424
425/// How many of `rows` (each over `n` variables) are independent: those an
426/// elimination finds a pivot that cannot be zero for.
427pub fn rank(rows: Vec<Vec<Scalar>>, n: usize) -> usize {
428    eliminate(rows, n).pivots.len()
429}
430
431/// `rows` each with the row of the identity beside it, `[row | I]`: an
432/// elimination of them reduces the identity alongside, which then says what
433/// combination of the rows each reduced row is.
434pub fn augmented(rows: impl ExactSizeIterator<Item = Vec<Scalar>>) -> Vec<Vec<Scalar>> {
435    let m = rows.len();
436    rows.enumerate()
437        .map(|(i, mut row)| {
438            row.extend((0..m).map(|j| if i == j { Scalar::ONE } else { Scalar::ZERO }));
439            row
440        })
441        .collect()
442}
443
444/// An enclosure of the inverse of the square matrix `a`, by Gauss-Jordan
445/// elimination: each entry encloses that of the inverse of every matrix `a`
446/// encloses. `None` if `a` could be singular.
447pub fn inverse(a: &[Vec<Scalar>]) -> Option<Vec<Vec<Scalar>>> {
448    let m = a.len();
449    let Elimination { rows, pivots, .. } = eliminate(augmented(a.iter().cloned()), m);
450    if pivots.len() < m {
451        return None;
452    }
453    let mut inverse = vec![Vec::new(); m];
454    for (row, &(col, _)) in rows.into_iter().zip(&pivots) {
455        inverse[col] = row[m..].to_vec();
456    }
457    Some(inverse)
458}
459
460/// The shortest `d` with `n_i . d = r_i` for every `(n_i, r_i)` — for more
461/// than three conditions, of which three can be met at most, the `d` that
462/// misses them least in the least-squares sense.
463///
464/// Three independent conditions leave only one `d`, solved for directly:
465/// the Gram matrix the fewer need squares the system's condition number.
466pub fn shortest_solution(rows: &[(Vector3, Scalar)]) -> GeopResult<Vector3> {
467    // `d = sum_i lambda_i n_i`, with the Gram matrix `G_ij = n_i . n_j`
468    // solving `G lambda = r`.
469    fn combine<const N: usize>(rows: &[(Vector3, Scalar)]) -> GeopResult<Vector3> {
470        let mut gram = Matrix::<N, N>::zero();
471        let mut r = Vector::<N>::zero();
472        for i in 0..N {
473            r[i] = rows[i].1;
474            for j in 0..N {
475                gram[(i, j)] = rows[i].0.prod_dot(&rows[j].0);
476            }
477        }
478        let lambda = solve_linear_system(&gram, &r)?;
479        Ok((0..N).fold(Vector3::zero(), |d, i| {
480            d.add(&rows[i].0.prod_scalar(lambda[i]))
481        }))
482    }
483    match rows.len() {
484        0 => Err(GeopError::new("no condition to meet")),
485        1 => combine::<1>(rows),
486        2 => combine::<2>(rows),
487        3 => solve_linear_system(
488            &Matrix::from_rows([
489                rows[0].0.to_array(),
490                rows[1].0.to_array(),
491                rows[2].0.to_array(),
492            ]),
493            &Vector3::from_array([rows[0].1, rows[1].1, rows[2].1]),
494        ),
495        _ => {
496            let mut normal = Matrix::<3, 3>::zero();
497            let mut b = Vector::<3>::zero();
498            for (n, r) in rows {
499                for i in 0..3 {
500                    b[i] = b[i].add(n[i].mul(*r));
501                    for j in 0..3 {
502                        normal[(i, j)] = normal[(i, j)].add(n[i].mul(n[j]));
503                    }
504                }
505            }
506            solve_linear_system(&normal, &b)
507        }
508    }
509}
510
511#[cfg(test)]
512mod tests {
513
514    use super::*;
515
516    /// A chain of `m` constraints, each between two neighbouring blocks of
517    /// `block` variables, with entries that are neither zero nor alike, and
518    /// each row's own variable the one that counts: the rows are independent.
519    fn chain(m: usize, block: usize) -> (Vec<Vec<Scalar>>, usize) {
520        let n = (m + 1) * block;
521        let mut rows = Vec::new();
522        for k in 0..m {
523            for e in 0..block {
524                let mut row = vec![Scalar::ZERO; n];
525                for b in 0..block {
526                    let own = if b == e { 10.0 } else { 0.0 };
527                    row[k * block + b] = Scalar::from_f64(own + ((k + e + b) % 5) as f64 * 0.1);
528                    row[(k + 1) * block + b] =
529                        Scalar::from_f64(((k * 3 + e * 5 + b) % 7) as f64 * 0.1 - 0.3);
530                }
531                rows.push(row);
532            }
533        }
534        (rows, n)
535    }
536
537    /// Keeping to the columns a row has something in finds every
538    /// independent row, and a null space that is one — every row vanishes
539    /// on each vector of it.
540    #[test]
541    fn a_sparse_elimination_finds_the_null_space() {
542        let (rows, n) = chain(12, 6);
543        assert_eq!(eliminate(rows.clone(), n).pivots.len(), rows.len());
544        let basis = null_space(rows.clone(), n);
545        assert_eq!(basis.len(), n - rows.len());
546        for v in &basis {
547            for row in &rows {
548                let dot: f64 = row
549                    .iter()
550                    .zip(v)
551                    .map(|(a, b)| a.to_f64() * b.to_f64())
552                    .sum();
553                assert!(dot.abs() < 1e-9, "a row is {dot} on a null vector");
554            }
555        }
556    }
557
558    /// A long chain of independent rows is all found.
559    #[test]
560    fn a_chain_has_its_rank() {
561        let (rows, n) = chain(150, 6);
562        let m = rows.len();
563        assert_eq!(eliminate(rows, n).pivots.len(), m);
564    }
565
566    /// The inverse of a matrix with zeros in it: eliminated with an identity
567    /// beside it, in rows longer than the variables they are pivoted on.
568    #[test]
569    fn the_inverse_of_a_sparse_matrix_is_one() {
570        let a: Vec<Vec<Scalar>> = [[2.0, 0.0, 1.0], [0.0, 3.0, 0.0], [1.0, 0.0, 4.0]]
571            .map(|row| row.map(Scalar::from_f64).to_vec())
572            .to_vec();
573        let inverse = inverse(&a).expect("the matrix is not singular");
574        for (i, row) in a.iter().enumerate() {
575            for j in 0..3 {
576                let dot: f64 = (row.iter().zip(&inverse))
577                    .map(|(a, inverse)| a.to_f64() * inverse[j].to_f64())
578                    .sum();
579                let want = if i == j { 1.0 } else { 0.0 };
580                assert!((dot - want).abs() < 1e-12, "({i}, {j}): {dot}");
581            }
582        }
583    }
584}
585
586#[cfg(test)]
587mod system_tests {
588    use super::*;
589
590    #[test]
591    fn solves_identity() {
592        let a = Matrix::from_rows([[Scalar::ONE, Scalar::ZERO], [Scalar::ZERO, Scalar::ONE]]);
593        let b = Vector::<2>::from_array([Scalar::from_f64(3.0), Scalar::from_f64(4.0)]);
594        let x = solve_linear_system(&a, &b).unwrap();
595        assert!(x[0].could_be_equal(Scalar::from_f64(3.0)));
596        assert!(x[1].could_be_equal(Scalar::from_f64(4.0)));
597    }
598
599    #[test]
600    fn solves_general_2x2() {
601        // [2 1; 1 3] x = [5; 10] -> x = [1, 3]
602        let a = Matrix::from_rows([
603            [Scalar::from_f64(2.0), Scalar::from_f64(1.0)],
604            [Scalar::from_f64(1.0), Scalar::from_f64(3.0)],
605        ]);
606        let b = Vector::from_array([Scalar::from_f64(5.0), Scalar::from_f64(10.0)]);
607        let x = solve_linear_system(&a, &b).unwrap();
608        assert!(x[0].could_be_equal(Scalar::ONE));
609        assert!(x[1].could_be_equal(Scalar::from_f64(3.0)));
610    }
611
612    #[test]
613    fn singular_errs() {
614        let a = Matrix::from_rows([[Scalar::ONE, Scalar::ONE], [Scalar::ONE, Scalar::ONE]]);
615        let b = Vector::from_array([Scalar::ONE, Scalar::TWO]);
616        assert!(solve_linear_system(&a, &b).is_err());
617    }
618}