use rug::Rational;
fn zero() -> Rational {
Rational::from(0)
}
#[derive(Debug, Clone)]
pub struct AffineSolution {
pub particular: Vec<Rational>,
pub nullspace: Vec<Vec<Rational>>,
}
impl AffineSolution {
pub fn dimension(&self) -> usize {
self.nullspace.len()
}
pub fn at(&self, t: &[Rational]) -> Vec<Rational> {
let mut x = self.particular.clone();
for (k, dir) in self.nullspace.iter().enumerate() {
let Some(tk) = t.get(k) else { break };
if *tk == 0 {
continue;
}
for (xi, di) in x.iter_mut().zip(dir) {
*xi += Rational::from(tk * di);
}
}
x
}
}
pub fn solve_affine(rows: &[Vec<Rational>], rhs: &[Rational]) -> Option<AffineSolution> {
let m = rows.len();
let ncols = rows.first().map_or(0, |r| r.len());
if m == 0 {
let nullspace = (0..ncols)
.map(|k| {
let mut v = vec![zero(); ncols];
v[k] = Rational::from(1);
v
})
.collect();
return Some(AffineSolution {
particular: vec![zero(); ncols],
nullspace,
});
}
let mut a: Vec<Vec<Rational>> = Vec::with_capacity(m);
for (row, b) in rows.iter().zip(rhs) {
debug_assert_eq!(row.len(), ncols);
let mut r = row.clone();
r.push(b.clone());
a.push(r);
}
let mut pivots: Vec<usize> = Vec::new();
let mut r = 0usize;
for c in 0..ncols {
let Some(p) = (r..m).find(|&i| a[i][c] != 0) else {
continue;
};
a.swap(r, p);
let inv = a[r][c].clone();
for v in a[r].iter_mut() {
*v /= &inv;
}
let prow = a[r].clone();
for (i, row) in a.iter_mut().enumerate() {
if i == r || row[c] == 0 {
continue;
}
let f = row[c].clone();
for (t, pv) in row.iter_mut().zip(prow.iter()) {
*t -= Rational::from(&f * pv);
}
}
pivots.push(c);
r += 1;
if r == m {
break;
}
}
for row in a.iter().skip(r) {
if row[ncols] != 0 && row[..ncols].iter().all(|v| *v == 0) {
return None;
}
}
let mut particular = vec![zero(); ncols];
for (i, &c) in pivots.iter().enumerate() {
particular[c] = a[i][ncols].clone();
}
let mut nullspace = Vec::new();
for free in (0..ncols).filter(|c| !pivots.contains(c)) {
let mut v = vec![zero(); ncols];
v[free] = Rational::from(1);
for (i, &c) in pivots.iter().enumerate() {
v[c] = -a[i][free].clone();
}
nullspace.push(v);
}
Some(AffineSolution {
particular,
nullspace,
})
}
pub fn psd_decompose(q: &[Vec<Rational>]) -> Option<Vec<(Rational, Vec<Rational>)>> {
let n = q.len();
let mut a: Vec<Vec<Rational>> = q.to_vec();
let mut done = vec![false; n];
let mut out: Vec<(Rational, Vec<Rational>)> = Vec::new();
for _ in 0..n {
let mut pivot: Option<usize> = None;
for i in 0..n {
if done[i] {
continue;
}
if a[i][i] < 0 {
return None;
}
if a[i][i] > 0 && pivot.map_or(true, |p| a[i][i] > a[p][p]) {
pivot = Some(i);
}
}
let Some(p) = pivot else {
for i in (0..n).filter(|&i| !done[i]) {
for j in (0..n).filter(|&j| !done[j]) {
if a[i][j] != 0 {
return None;
}
}
}
break;
};
let d = a[p][p].clone();
let mut v = vec![zero(); n];
for j in (0..n).filter(|&j| !done[j]) {
v[j] = Rational::from(&a[p][j] / &d);
}
for i in (0..n).filter(|&i| !done[i]) {
if v[i] == 0 {
continue;
}
let scaled = Rational::from(&d * &v[i]);
for j in (0..n).filter(|&j| !done[j]) {
if v[j] == 0 {
continue;
}
a[i][j] -= Rational::from(&scaled * &v[j]);
}
}
done[p] = true;
out.push((d, v));
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn r(n: i64) -> Rational {
Rational::from(n)
}
fn mat(rows: &[&[i64]]) -> Vec<Vec<Rational>> {
rows.iter()
.map(|r| r.iter().map(|&v| Rational::from(v)).collect())
.collect()
}
#[test]
fn unique_solution_has_no_free_parameters() {
let rows = mat(&[&[1, 1], &[1, -1]]);
let sol = solve_affine(&rows, &[r(3), r(1)]).expect("consistent");
assert_eq!(sol.dimension(), 0);
assert_eq!(sol.particular, vec![r(2), r(1)]);
}
#[test]
fn underdetermined_system_reports_its_freedom() {
let rows = mat(&[&[1, 1, 1]]);
let sol = solve_affine(&rows, &[r(1)]).expect("consistent");
assert_eq!(sol.dimension(), 2);
let p = sol.at(&[r(5), r(-2)]);
let s: Rational = p.iter().fold(r(0), |a, v| a + v.clone());
assert_eq!(s, 1);
}
#[test]
fn inconsistent_system_is_rejected() {
let rows = mat(&[&[1, 1], &[2, 2]]);
assert!(solve_affine(&rows, &[r(1), r(3)]).is_none());
}
#[test]
fn redundant_rows_do_not_break_the_solver() {
let rows = mat(&[&[1, 1], &[2, 2], &[1, -1]]);
let sol = solve_affine(&rows, &[r(2), r(4), r(0)]).expect("consistent");
assert_eq!(sol.dimension(), 0);
assert_eq!(sol.particular, vec![r(1), r(1)]);
}
#[test]
fn psd_decompose_factors_an_identity_matrix() {
let q = mat(&[&[1, 0], &[0, 1]]);
let d = psd_decompose(&q).expect("PSD");
assert_eq!(d.len(), 2);
}
#[test]
fn psd_decompose_rejects_an_indefinite_matrix() {
let q = mat(&[&[1, 2], &[2, 1]]);
assert!(psd_decompose(&q).is_none());
}
#[test]
fn psd_decompose_accepts_a_singular_psd_matrix() {
let q = mat(&[&[1, 1], &[1, 1]]);
let d = psd_decompose(&q).expect("PSD");
assert_eq!(d.len(), 1);
assert_eq!(d[0].0, 1);
}
#[test]
fn psd_decompose_rejects_a_zero_diagonal_with_live_off_diagonal() {
let q = mat(&[&[0, 1], &[1, 0]]);
assert!(psd_decompose(&q).is_none());
}
#[test]
fn psd_decomposition_reproduces_the_matrix() {
let q = mat(&[&[4, 2, 0], &[2, 5, 1], &[0, 1, 3]]);
let d = psd_decompose(&q).expect("PSD");
let n = 3;
let mut acc = vec![vec![r(0); n]; n];
for (dk, v) in &d {
for i in 0..n {
for j in 0..n {
acc[i][j] += Rational::from(dk * &v[i]) * &v[j];
}
}
}
assert_eq!(acc, q);
}
}