use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, Zero};
use crate::api::context::Context;
use crate::base::errors::SymplexError;
use crate::domains::matrix::Matrix;
use crate::domains::ntheory::gcdex;
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::InvalidArgument {
operation,
reason: reason.into(),
}
}
fn integer_rows(m: &Matrix, operation: &'static str) -> Result<Vec<Vec<BigInt>>, SymplexError> {
if let Some(rows) = m.to_bigint_rows() {
return Ok(rows);
}
m.eval().to_bigint_rows().ok_or_else(|| {
invalid(
operation,
"every entry must be an integer literal (fractions and symbolic \
entries are not allowed)",
)
})
}
fn identity_rows(n: usize) -> Vec<Vec<BigInt>> {
(0..n)
.map(|i| {
(0..n)
.map(|j| {
if i == j {
BigInt::one()
} else {
BigInt::zero()
}
})
.collect()
})
.collect()
}
fn transpose_rows(a: &[Vec<BigInt>]) -> Vec<Vec<BigInt>> {
let m = a.len();
let n = a.first().map_or(0, Vec::len);
(0..n)
.map(|j| (0..m).map(|i| a[i][j].clone()).collect())
.collect()
}
fn to_matrix(ctx: &Context, rows: &[Vec<BigInt>]) -> Result<Matrix, SymplexError> {
Matrix::from_bigint(ctx, rows)
}
fn combine_rows(
a: &mut [Vec<BigInt>],
r: usize,
i: usize,
x: &BigInt,
y: &BigInt,
p: &BigInt,
q: &BigInt,
) {
if r == i {
return;
}
let (row_r, row_i) = two_rows_mut(a, r, i);
for (ar, ai) in row_r.iter_mut().zip(row_i.iter_mut()) {
let old_r = std::mem::take(ar);
let old_i = std::mem::take(ai);
*ar = x * &old_r + y * &old_i;
*ai = p * &old_r + q * &old_i;
}
}
fn sub_scaled_row(a: &mut [Vec<BigInt>], i: usize, r: usize, q: &BigInt) {
if r == i {
return;
}
let (row_r, row_i) = two_rows_mut(a, r, i);
for (ar, ai) in row_r.iter().zip(row_i.iter_mut()) {
if !ar.is_zero() {
*ai -= q * ar;
}
}
}
fn two_rows_mut(a: &mut [Vec<BigInt>], r: usize, i: usize) -> (&mut [BigInt], &mut [BigInt]) {
debug_assert!(r != i);
if r < i {
let (lo, hi) = a.split_at_mut(i);
(&mut lo[r], &mut hi[0])
} else {
let (lo, hi) = a.split_at_mut(r);
(&mut hi[0], &mut lo[i])
}
}
fn negate_row(a: &mut [Vec<BigInt>], r: usize) {
for v in a[r].iter_mut() {
*v = -std::mem::take(v);
}
}
fn eliminate_row_pair(
a: &mut [Vec<BigInt>],
u: &mut [Vec<BigInt>],
r: usize,
i: usize,
col: usize,
) {
if a[i][col].is_zero() {
return;
}
if a[r][col].is_zero() {
a.swap(r, i);
u.swap(r, i);
return;
}
let pivot = a[r][col].clone();
let other = a[i][col].clone();
let (rem, quot) = (&other % &pivot, &other / &pivot);
if rem.is_zero() {
sub_scaled_row(a, i, r, ");
sub_scaled_row(u, i, r, ");
return;
}
let (g, x, y) = gcdex(pivot.clone(), other.clone());
let p = -(&other / &g);
let q = &pivot / &g;
combine_rows(a, r, i, &x, &y, &p, &q);
combine_rows(u, r, i, &x, &y, &p, &q);
}
struct RowHnf {
h: Vec<Vec<BigInt>>,
u: Vec<Vec<BigInt>>,
pivots: Vec<usize>,
}
fn row_hnf(a: &[Vec<BigInt>]) -> RowHnf {
let m = a.len();
let n = a.first().map_or(0, Vec::len);
let mut h: Vec<Vec<BigInt>> = a.to_vec();
let mut u = identity_rows(m);
let mut pivots = Vec::new();
let mut r = 0usize;
for col in 0..n {
if r >= m {
break;
}
if let Some(best) = (r..m)
.filter(|&i| !h[i][col].is_zero())
.min_by(|&i, &j| h[i][col].abs().cmp(&h[j][col].abs()))
&& best != r
{
h.swap(r, best);
u.swap(r, best);
}
for i in (r + 1)..m {
eliminate_row_pair(&mut h, &mut u, r, i, col);
}
if h[r][col].is_zero() {
continue;
}
if h[r][col].is_negative() {
negate_row(&mut h, r);
negate_row(&mut u, r);
}
let pivot = h[r][col].clone();
for i in 0..r {
let q = h[i][col].div_floor(&pivot);
if !q.is_zero() {
sub_scaled_row(&mut h, i, r, &q);
sub_scaled_row(&mut u, i, r, &q);
}
}
pivots.push(col);
r += 1;
}
RowHnf { h, u, pivots }
}
pub fn hermite_normal_form(m: &Matrix) -> Result<Matrix, SymplexError> {
let rows = integer_rows(m, "hermite_normal_form")?;
let hnf = row_hnf(&rows);
to_matrix(&m.context(), &hnf.h)
}
pub fn hermite_normal_form_with_transform(m: &Matrix) -> Result<(Matrix, Matrix), SymplexError> {
let rows = integer_rows(m, "hermite_normal_form_with_transform")?;
let hnf = row_hnf(&rows);
let ctx = m.context();
Ok((to_matrix(&ctx, &hnf.h)?, to_matrix(&ctx, &hnf.u)?))
}
pub fn column_hermite_normal_form(m: &Matrix) -> Result<Matrix, SymplexError> {
let rows = integer_rows(m, "column_hermite_normal_form")?;
let h = column_hnf_rows(&rows);
to_matrix(&m.context(), &h)
}
fn column_hnf_rows(a: &[Vec<BigInt>]) -> Vec<Vec<BigInt>> {
let mut reversed: Vec<Vec<BigInt>> = a.to_vec();
reversed.reverse();
let hnf = row_hnf(&transpose_rows(&reversed));
let mut h = transpose_rows(&hnf.h);
h.reverse();
for row in &mut h {
row.reverse();
}
h
}
fn combine_cols(
a: &mut [Vec<BigInt>],
c: usize,
j: usize,
x: &BigInt,
y: &BigInt,
p: &BigInt,
q: &BigInt,
) {
for row in a.iter_mut() {
let ac = row[c].clone();
let aj = row[j].clone();
row[c] = x * &ac + y * &aj;
row[j] = p * &ac + q * &aj;
}
}
fn sub_scaled_col(a: &mut [Vec<BigInt>], j: usize, c: usize, q: &BigInt) {
for row in a.iter_mut() {
if !row[c].is_zero() {
let t = q * &row[c];
row[j] -= t;
}
}
}
fn eliminate_col_pair(s: &mut [Vec<BigInt>], v: &mut [Vec<BigInt>], t: usize, j: usize) {
if s[t][j].is_zero() {
return;
}
let pivot = s[t][t].clone();
let other = s[t][j].clone();
if pivot.is_zero() {
for row in s.iter_mut() {
row.swap(t, j);
}
for row in v.iter_mut() {
row.swap(t, j);
}
return;
}
let (rem, quot) = (&other % &pivot, &other / &pivot);
if rem.is_zero() {
sub_scaled_col(s, j, t, ");
sub_scaled_col(v, j, t, ");
return;
}
let (g, x, y) = gcdex(pivot.clone(), other.clone());
let p = -(&other / &g);
let q = &pivot / &g;
combine_cols(s, t, j, &x, &y, &p, &q);
combine_cols(v, t, j, &x, &y, &p, &q);
}
type IntRows = Vec<Vec<BigInt>>;
fn smith(a: &[Vec<BigInt>]) -> (IntRows, IntRows, IntRows) {
let m = a.len();
let n = a.first().map_or(0, Vec::len);
let mut s: Vec<Vec<BigInt>> = a.to_vec();
let mut u = identity_rows(m);
let mut v = identity_rows(n);
for t in 0..m.min(n) {
let mut best: Option<(usize, usize)> = None;
for i in t..m {
for j in t..n {
if s[i][j].is_zero() {
continue;
}
match best {
Some((bi, bj)) if s[bi][bj].abs() <= s[i][j].abs() => {}
_ => best = Some((i, j)),
}
}
}
let Some((bi, bj)) = best else {
break;
};
if bi != t {
s.swap(t, bi);
u.swap(t, bi);
}
if bj != t {
for row in s.iter_mut() {
row.swap(t, bj);
}
for row in v.iter_mut() {
row.swap(t, bj);
}
}
loop {
for i in (t + 1)..m {
eliminate_row_pair(&mut s, &mut u, t, i, t);
}
for j in (t + 1)..n {
eliminate_col_pair(&mut s, &mut v, t, j);
}
let col_clear = ((t + 1)..m).all(|i| s[i][t].is_zero());
let row_clear = ((t + 1)..n).all(|j| s[t][j].is_zero());
if !(col_clear && row_clear) {
continue;
}
let d = s[t][t].clone();
let offender = ((t + 1)..m).find(|&i| ((t + 1)..n).any(|j| !(&s[i][j] % &d).is_zero()));
match offender {
Some(i) => {
let one = BigInt::one();
let zero = BigInt::zero();
combine_rows(&mut s, t, i, &one, &one, &zero, &one);
combine_rows(&mut u, t, i, &one, &one, &zero, &one);
}
None => break,
}
}
if s[t][t].is_negative() {
negate_row(&mut s, t);
negate_row(&mut u, t);
}
}
(s, u, v)
}
pub fn smith_normal_form(m: &Matrix) -> Result<Matrix, SymplexError> {
let rows = integer_rows(m, "smith_normal_form")?;
let (s, _, _) = smith(&rows);
to_matrix(&m.context(), &s)
}
pub fn smith_normal_form_with_transforms(
m: &Matrix,
) -> Result<(Matrix, Matrix, Matrix), SymplexError> {
let rows = integer_rows(m, "smith_normal_form_with_transforms")?;
let (s, u, v) = smith(&rows);
let ctx = m.context();
Ok((
to_matrix(&ctx, &s)?,
to_matrix(&ctx, &u)?,
to_matrix(&ctx, &v)?,
))
}
pub fn integer_nullspace(m: &Matrix) -> Result<Vec<Matrix>, SymplexError> {
let rows = integer_rows(m, "integer_nullspace")?;
let nrows = rows.len();
let ncols = rows.first().map_or(0, Vec::len);
let at = transpose_rows(&rows);
let augmented: Vec<Vec<BigInt>> = at
.into_iter()
.enumerate()
.map(|(i, mut row)| {
row.extend((0..ncols).map(|j| {
if i == j {
BigInt::one()
} else {
BigInt::zero()
}
}));
row
})
.collect();
let hnf = row_hnf(&augmented);
let rank = hnf.pivots.iter().filter(|&&c| c < nrows).count();
let ctx = m.context();
let mut basis = Vec::with_capacity(ncols - rank);
for row in hnf.h.iter().skip(rank) {
debug_assert!(row[..nrows].iter().all(Zero::is_zero));
let col: Vec<Vec<BigInt>> = row[nrows..].iter().map(|v| vec![v.clone()]).collect();
basis.push(to_matrix(&ctx, &col)?);
}
Ok(basis)
}
pub fn is_unimodular(m: &Matrix) -> Result<bool, SymplexError> {
let rows = integer_rows(m, "is_unimodular")?;
if m.nrows() != m.ncols() {
return Ok(false);
}
let hnf = row_hnf(&rows);
if hnf.pivots.len() != m.nrows() {
return Ok(false);
}
Ok((0..m.nrows()).all(|i| hnf.h[i][i].is_one()))
}
pub fn lattice_determinant(m: &Matrix) -> Result<BigInt, SymplexError> {
let rows = integer_rows(m, "lattice_determinant")?;
let nrows = m.nrows();
let hnf = row_hnf(&transpose_rows(&rows));
if hnf.pivots.len() != nrows {
return Err(invalid(
"lattice_determinant",
format!(
"matrix must have full row rank (rank {} of {} rows); the column \
lattice has infinite index otherwise",
hnf.pivots.len(),
nrows
),
));
}
Ok(hnf
.pivots
.iter()
.enumerate()
.map(|(i, &c)| hnf.h[i][c].clone())
.product())
}
#[cfg(test)]
mod tests {
use super::*;
fn bi(n: i64) -> BigInt {
BigInt::from(n)
}
fn rows(data: &[&[i64]]) -> Vec<Vec<BigInt>> {
data.iter()
.map(|r| r.iter().map(|&v| bi(v)).collect())
.collect()
}
fn matmul(a: &[Vec<BigInt>], b: &[Vec<BigInt>]) -> Vec<Vec<BigInt>> {
let n = b[0].len();
a.iter()
.map(|row| {
(0..n)
.map(|j| row.iter().zip(b).map(|(x, brow)| x * &brow[j]).sum())
.collect()
})
.collect()
}
fn det(a: &[Vec<BigInt>]) -> BigInt {
let n = a.len();
let mut m: Vec<Vec<BigInt>> = a.to_vec();
let mut sign = BigInt::one();
let mut prev = BigInt::one();
for k in 0..n {
if m[k][k].is_zero() {
let Some(p) = (k + 1..n).find(|&i| !m[i][k].is_zero()) else {
return BigInt::zero();
};
m.swap(k, p);
sign = -sign;
}
for i in k + 1..n {
for j in k + 1..n {
let v = (&m[i][j] * &m[k][k] - &m[i][k] * &m[k][j]) / &prev;
m[i][j] = v;
}
}
prev = m[k][k].clone();
}
sign * m[n - 1][n - 1].clone()
}
#[test]
fn row_hnf_known_answer() {
let a = rows(&[&[2, 4, 4], &[-6, 6, 12], &[10, -4, -16]]);
let r = row_hnf(&a);
assert_eq!(r.h, rows(&[&[2, 4, 4], &[0, 6, 0], &[0, 0, 12]]));
assert_eq!(matmul(&r.u, &a), r.h);
assert_eq!(det(&r.u).abs(), bi(1));
}
#[test]
fn row_hnf_rank_deficient_and_negative() {
let a = rows(&[&[1, 2, 3], &[-2, -4, -6], &[0, 1, 1]]);
let r = row_hnf(&a);
assert_eq!(r.pivots, vec![0, 1]);
assert_eq!(r.h[2], vec![bi(0), bi(0), bi(0)]);
assert_eq!(matmul(&r.u, &a), r.h);
}
#[test]
fn smith_known_answer() {
let a = rows(&[&[12, 6, 4], &[3, 9, 6], &[2, 16, 14]]);
let (s, u, v) = smith(&a);
assert_eq!(s, rows(&[&[1, 0, 0], &[0, 10, 0], &[0, 0, 30]]));
assert_eq!(matmul(&matmul(&u, &a), &v), s);
assert_eq!(det(&u).abs(), bi(1));
assert_eq!(det(&v).abs(), bi(1));
}
#[test]
fn smith_needs_divisibility_fix() {
let a = rows(&[&[2, 0], &[0, 3]]);
let (s, u, v) = smith(&a);
assert_eq!(s, rows(&[&[1, 0], &[0, 6]]));
assert_eq!(matmul(&matmul(&u, &a), &v), s);
}
#[test]
fn column_hnf_matches_sympy() {
let a = rows(&[&[12, 6, 4], &[3, 9, 6], &[2, 16, 14]]);
assert_eq!(
column_hnf_rows(&a),
rows(&[&[10, 0, 2], &[0, 15, 3], &[0, 0, 2]])
);
}
}