use rug::{Integer, Rational};
#[derive(Clone)]
struct Row {
cells: Vec<(usize, Rational)>,
rhs: Rational,
}
impl Row {
fn lead(&self) -> Option<usize> {
self.cells.first().map(|(c, _)| *c)
}
fn scale(&mut self, k: &Rational) {
for (_, v) in self.cells.iter_mut() {
*v *= k.clone();
}
self.rhs *= k.clone();
}
fn axpy(&mut self, other: &Row, factor: &Rational) {
let mut out: Vec<(usize, Rational)> =
Vec::with_capacity(self.cells.len() + other.cells.len());
let (mut i, mut j) = (0usize, 0usize);
while i < self.cells.len() && j < other.cells.len() {
let (ci, cj) = (self.cells[i].0, other.cells[j].0);
match ci.cmp(&cj) {
std::cmp::Ordering::Less => {
out.push(self.cells[i].clone());
i += 1;
}
std::cmp::Ordering::Greater => {
let v = -(factor.clone() * other.cells[j].1.clone());
if v != 0 {
out.push((cj, v));
}
j += 1;
}
std::cmp::Ordering::Equal => {
let v = self.cells[i].1.clone() - factor.clone() * other.cells[j].1.clone();
if v != 0 {
out.push((ci, v));
}
i += 1;
j += 1;
}
}
}
while i < self.cells.len() {
out.push(self.cells[i].clone());
i += 1;
}
while j < other.cells.len() {
let v = -(factor.clone() * other.cells[j].1.clone());
if v != 0 {
out.push((other.cells[j].0, v));
}
j += 1;
}
self.cells = out;
self.rhs -= factor.clone() * other.rhs.clone();
}
}
pub(super) struct SparseSystem {
ncols: usize,
rows: Vec<Row>,
}
pub(super) enum SolveOutcome {
Solved(Vec<Rational>),
Inconsistent,
GaveUp,
}
const MAX_CELLS: usize = 50_000;
impl SparseSystem {
pub(super) fn new(ncols: usize) -> Self {
SparseSystem {
ncols,
rows: Vec::new(),
}
}
pub(super) fn push_row(&mut self, cells: Vec<(usize, Integer)>, rhs: Integer) {
debug_assert!(
cells.windows(2).all(|w| w[0].0 < w[1].0),
"equation cells must be sorted and duplicate-free"
);
debug_assert!(cells.iter().all(|(c, v)| *c < self.ncols && *v != 0));
self.rows.push(Row {
cells: cells
.into_iter()
.map(|(c, v)| (c, Rational::from(v)))
.collect(),
rhs: Rational::from(rhs),
});
}
pub(super) fn solve(&self) -> SolveOutcome {
let original = self.rows.clone();
match self.eliminate() {
SolveOutcome::Solved(sol) => {
if verify(&original, &sol) {
SolveOutcome::Solved(sol)
} else {
debug_assert!(false, "sparse solve produced an unverified solution");
SolveOutcome::GaveUp
}
}
other => other,
}
}
fn eliminate(&self) -> SolveOutcome {
let mut rows = self.rows.clone();
let mut cells: usize = rows.iter().map(|r| r.cells.len()).sum();
let mut peak = cells;
if cells > MAX_CELLS {
return SolveOutcome::GaveUp;
}
let mut buckets: Vec<Vec<usize>> = vec![Vec::new(); self.ncols];
for (i, r) in rows.iter().enumerate() {
match r.lead() {
Some(c) => buckets[c].push(i),
None => {
if r.rhs != 0 {
return SolveOutcome::Inconsistent;
}
}
}
}
let mut pivots: Vec<(usize, usize)> = Vec::new();
for col in 0..self.ncols {
let candidates = std::mem::take(&mut buckets[col]);
if candidates.is_empty() {
continue;
}
let p = *candidates
.iter()
.min_by_key(|&&i| rows[i].cells.len())
.expect("candidates is non-empty");
let lead = rows[p].cells[0].1.clone();
debug_assert_eq!(rows[p].cells[0].0, col);
let inv = Rational::from(1) / lead;
rows[p].scale(&inv);
let pivot_row = rows[p].clone();
for i in candidates {
if i == p {
continue;
}
let factor = rows[i].cells[0].1.clone();
let before = rows[i].cells.len();
rows[i].axpy(&pivot_row, &factor);
cells = cells + rows[i].cells.len() - before;
peak = peak.max(cells);
if cells > MAX_CELLS {
return SolveOutcome::GaveUp;
}
match rows[i].lead() {
Some(c) => {
debug_assert!(c > col, "elimination must move the leading column right");
buckets[c].push(i);
}
None => {
if rows[i].rhs != 0 {
return SolveOutcome::Inconsistent;
}
}
}
}
pivots.push((p, col));
}
super::profile::record(|s| s.peak_cells = peak);
let mut sol = vec![Rational::from(0); self.ncols];
for &(r, col) in pivots.iter().rev() {
let mut acc = rows[r].rhs.clone();
for (j, v) in rows[r].cells.iter().skip(1) {
acc -= v.clone() * sol[*j].clone();
}
sol[col] = acc;
}
SolveOutcome::Solved(sol)
}
}
fn verify(rows: &[Row], sol: &[Rational]) -> bool {
for r in rows {
let mut acc = Rational::from(0);
for (j, v) in &r.cells {
if sol[*j] != 0 {
acc += v.clone() * sol[*j].clone();
}
}
if acc != r.rhs {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
fn int(v: i64) -> Integer {
Integer::from(v)
}
fn cross_check(dense: Vec<Vec<i64>>, rhs: Vec<i64>) {
let ncols = dense[0].len();
let mut sys = SparseSystem::new(ncols);
for (row, b) in dense.iter().zip(rhs.iter()) {
let cells: Vec<(usize, Integer)> = row
.iter()
.enumerate()
.filter(|(_, v)| **v != 0)
.map(|(j, v)| (j, int(*v)))
.collect();
sys.push_row(cells, int(*b));
}
let mine = match sys.solve() {
SolveOutcome::Solved(s) => Some(s),
_ => None,
};
let dense_mat: Vec<Vec<Rational>> = dense
.iter()
.map(|r| r.iter().map(|v| Rational::from(*v)).collect())
.collect();
let dense_rhs: Vec<Rational> = rhs.iter().map(|v| Rational::from(*v)).collect();
let theirs = crate::sum::gosper::rational_gaussian_solve(dense_mat, dense_rhs);
assert_eq!(mine, theirs, "sparse and dense solvers disagree");
}
#[test]
fn agrees_with_the_dense_solver_on_a_square_system() {
cross_check(vec![vec![2, 1], vec![1, -1]], vec![5, 1]);
}
#[test]
fn agrees_with_the_dense_solver_when_overdetermined_and_consistent() {
cross_check(
vec![vec![1, 0], vec![0, 1], vec![1, 1], vec![2, 2]],
vec![3, 4, 7, 14],
);
}
#[test]
fn agrees_with_the_dense_solver_on_free_variables() {
cross_check(vec![vec![1, 0, 1], vec![0, 1, 0]], vec![1, 2]);
}
#[test]
fn agrees_with_the_dense_solver_on_a_zero_column() {
cross_check(vec![vec![0, 1], vec![0, 2]], vec![3, 6]);
}
#[test]
fn rational_solutions_are_exact() {
cross_check(vec![vec![3, 0], vec![0, 7]], vec![1, 1]);
}
#[test]
fn inconsistency_is_reported_as_inconsistent_not_as_a_give_up() {
let mut sys = SparseSystem::new(1);
sys.push_row(vec![(0, int(1))], int(1));
sys.push_row(vec![(0, int(1))], int(2));
assert!(matches!(sys.solve(), SolveOutcome::Inconsistent));
}
#[test]
fn an_all_zero_row_with_a_non_zero_rhs_is_inconsistent() {
let mut sys = SparseSystem::new(2);
sys.push_row(vec![], int(5));
assert!(matches!(sys.solve(), SolveOutcome::Inconsistent));
}
#[test]
fn an_empty_system_solves_to_all_zeros() {
let sys = SparseSystem::new(3);
match sys.solve() {
SolveOutcome::Solved(s) => assert_eq!(s, vec![Rational::from(0); 3]),
_ => panic!("an empty system is solved by zero"),
}
}
#[test]
fn a_large_bidiagonal_system_is_solved() {
let n = 300;
let mut sys = SparseSystem::new(n);
for i in 0..n {
let mut cells = vec![(i, int(1))];
if i + 1 < n {
cells.push((i + 1, int(2)));
}
sys.push_row(cells, int(1));
}
match sys.solve() {
SolveOutcome::Solved(s) => assert_eq!(s.len(), n),
_ => panic!("expected a solution"),
}
}
}