#![allow(clippy::needless_range_loop)]
use crate::governance::poly::Poly;
use crate::scalar::Rat;
#[derive(Clone, Debug)]
pub struct TriangularSet {
pub polys: Vec<TriangularPoly>,
pub free_vars: Vec<usize>,
pub num_vars: usize,
}
#[derive(Clone, Debug)]
pub struct TriangularPoly {
pub leading_var: usize,
pub poly: Poly,
pub is_linear: bool,
}
#[derive(Clone, Debug)]
pub enum TriangularError {
NotTriangularizable,
Inconsistent,
}
impl std::fmt::Display for TriangularError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TriangularError::NotTriangularizable => {
write!(f, "system does not admit triangular decomposition")
}
TriangularError::Inconsistent => {
write!(f, "inconsistent polynomial system (no solutions)")
}
}
}
}
pub fn triangular_decompose(eqs: &[Poly]) -> Result<TriangularSet, TriangularError> {
if eqs.is_empty() {
let num_vars = 0;
return Ok(TriangularSet {
polys: vec![],
free_vars: vec![],
num_vars,
});
}
let num_vars = eqs[0].num_vars();
let mut linear: Vec<Poly> = Vec::new();
let mut nonlinear: Vec<Poly> = Vec::new();
for eq in eqs {
if eq.is_zero() {
continue;
}
if is_linear(eq) {
linear.push(eq.clone());
} else {
nonlinear.push(eq.clone());
}
}
let (solved_vars, row_echelon) = gaussian_elimination(&linear, num_vars);
let mut triangular_polys: Vec<TriangularPoly> = Vec::new();
for (leading_var, poly) in solved_vars.iter().zip(row_echelon.iter()) {
triangular_polys.push(TriangularPoly {
leading_var: *leading_var,
poly: poly.clone(),
is_linear: true,
});
}
for nl in &nonlinear {
let substituted = substitute_linear_solutions(nl, &solved_vars, &row_echelon, num_vars);
if substituted.is_zero() {
continue; }
let active_vars = active_variables(&substituted);
if active_vars.is_empty() {
return Err(TriangularError::Inconsistent);
}
if active_vars.len() == 1 {
triangular_polys.push(TriangularPoly {
leading_var: active_vars[0],
poly: substituted,
is_linear: false,
});
} else {
return Err(TriangularError::NotTriangularizable);
}
}
triangular_polys.sort_by_key(|tp| tp.leading_var);
let determined: Vec<usize> = triangular_polys.iter().map(|tp| tp.leading_var).collect();
let free_vars: Vec<usize> = (0..num_vars).filter(|v| !determined.contains(v)).collect();
Ok(TriangularSet {
polys: triangular_polys,
free_vars,
num_vars,
})
}
fn is_linear(p: &Poly) -> bool {
for (exp, coeff) in p.terms_iter() {
if coeff.is_zero() {
continue;
}
let total_deg: u8 = exp.iter().sum();
if total_deg > 1 {
return false;
}
}
true
}
fn gaussian_elimination(linears: &[Poly], num_vars: usize) -> (Vec<usize>, Vec<Poly>) {
if linears.is_empty() {
return (vec![], vec![]);
}
let n_eq = linears.len();
let n_col = num_vars + 1; let mut matrix: Vec<Vec<Rat>> = Vec::with_capacity(n_eq);
for p in linears {
let mut row = vec![Rat::ZERO; n_col];
for (exp, &coeff) in p.terms_iter() {
let total_deg: u8 = exp.iter().sum();
if total_deg == 0 {
row[num_vars] -= coeff;
} else {
for (v, &e) in exp.iter().enumerate() {
if e == 1 {
row[v] += coeff;
}
}
}
}
matrix.push(row);
}
let mut pivot_cols: Vec<usize> = Vec::new();
let mut pivot_row = 0;
for col in 0..num_vars {
let mut best = None;
for row in pivot_row..n_eq {
if !matrix[row][col].is_zero() {
best = Some(row);
break;
}
}
let Some(best_row) = best else { continue };
matrix.swap(pivot_row, best_row);
pivot_cols.push(col);
let pivot_val = matrix[pivot_row][col];
for row in 0..n_eq {
if row == pivot_row {
continue;
}
if matrix[row][col].is_zero() {
continue;
}
let factor = matrix[row][col] / pivot_val;
for c in 0..n_col {
let sub = factor * matrix[pivot_row][c];
matrix[row][c] -= sub;
}
}
pivot_row += 1;
}
let mut solved_vars = Vec::new();
let mut row_echelon = Vec::new();
for (i, &pivot_col) in pivot_cols.iter().enumerate() {
let row = &matrix[i];
let pivot_val = row[pivot_col];
if pivot_val.is_zero() {
continue;
}
let mut poly = Poly::zero(num_vars);
let mut exp = vec![0u8; num_vars];
exp[pivot_col] = 1;
poly.add_term(exp, pivot_val);
for col in 0..num_vars {
if col == pivot_col {
continue;
}
if row[col].is_zero() {
continue;
}
let mut exp = vec![0u8; num_vars];
exp[col] = 1;
poly.add_term(exp, row[col]);
}
if !row[num_vars].is_zero() {
poly.add_term(vec![0u8; num_vars], -row[num_vars]);
}
solved_vars.push(pivot_col);
row_echelon.push(poly);
}
(solved_vars, row_echelon)
}
fn substitute_linear_solutions(
poly: &Poly,
solved_vars: &[usize],
solutions: &[Poly],
num_vars: usize,
) -> Poly {
if solved_vars.is_empty() {
return poly.clone();
}
let mut result = Poly::zero(num_vars);
for (exp, &coeff) in poly.terms_iter() {
if coeff.is_zero() {
continue;
}
let mut term_poly = Poly::constant(coeff, num_vars);
for (v, &e) in exp.iter().enumerate() {
if e == 0 {
continue;
}
if let Some(sol_idx) = solved_vars.iter().position(|&sv| sv == v) {
let sol = &solutions[sol_idx];
let pivot_coeff = sol.coefficient_of_var(v);
if pivot_coeff.is_zero() {
continue;
}
let mut subst = Poly::zero(num_vars);
for (sexp, &scoeff) in sol.terms_iter() {
if scoeff.is_zero() {
continue;
}
if sexp[v] > 0 {
continue;
} subst.add_term(sexp.clone(), -scoeff / pivot_coeff);
}
let mut powered = Poly::constant(Rat::ONE, num_vars);
for _ in 0..e {
powered = poly_mul_poly(&powered, &subst);
}
term_poly = poly_mul_poly(&term_poly, &powered);
} else {
let mut var_exp = vec![0u8; num_vars];
var_exp[v] = e;
let var_poly = Poly::from_term(var_exp, Rat::ONE, num_vars);
term_poly = poly_mul_poly(&term_poly, &var_poly);
}
}
result = poly_add_poly(&result, &term_poly);
}
result
}
fn active_variables(p: &Poly) -> Vec<usize> {
let mut active = vec![false; p.num_vars()];
for (exp, coeff) in p.terms_iter() {
if coeff.is_zero() {
continue;
}
for (v, &e) in exp.iter().enumerate() {
if e > 0 {
active[v] = true;
}
}
}
active
.iter()
.enumerate()
.filter_map(|(i, &a)| if a { Some(i) } else { None })
.collect()
}
fn poly_add_poly(a: &Poly, b: &Poly) -> Poly {
let mut result = a.clone();
for (exp, &coeff) in b.terms_iter() {
if !coeff.is_zero() {
result.add_term(exp.clone(), coeff);
}
}
result
}
fn poly_mul_poly(a: &Poly, b: &Poly) -> Poly {
let num_vars = a.num_vars();
let mut result = Poly::zero(num_vars);
for (ea, &ca) in a.terms_iter() {
if ca.is_zero() {
continue;
}
for (eb, &cb) in b.terms_iter() {
if cb.is_zero() {
continue;
}
let mut exp = vec![0u8; num_vars];
for i in 0..num_vars {
exp[i] = ea[i] + eb[i];
}
result.add_term(exp, ca * cb);
}
}
result
}
pub fn dimension(eqs: &[Poly], num_vars: usize) -> usize {
match triangular_decompose(eqs) {
Ok(ts) => ts.free_vars.len(),
Err(TriangularError::NotTriangularizable) => {
match crate::governance::groebner::groebner_basis(eqs.to_vec()) {
Ok(basis) => crate::governance::groebner::free_variables(&basis, num_vars).len(),
Err(_) => num_vars, }
}
Err(TriangularError::Inconsistent) => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn lin(coeffs: &[(usize, i64)], constant: i64, num_vars: usize) -> Poly {
let mut p = Poly::zero(num_vars);
for &(var, coeff) in coeffs {
let mut exp = vec![0u8; num_vars];
exp[var] = 1;
p.add_term(exp, Rat::from(coeff));
}
if constant != 0 {
p.add_term(vec![0u8; num_vars], Rat::from(constant));
}
p
}
#[test]
fn empty_system() {
let ts = triangular_decompose(&[]).unwrap();
assert!(ts.polys.is_empty());
}
#[test]
fn single_linear() {
let eq = lin(&[(0, 1), (1, 1)], -1, 2);
let ts = triangular_decompose(&[eq]).unwrap();
assert_eq!(ts.polys.len(), 1);
assert!(ts.polys[0].is_linear);
assert_eq!(ts.free_vars.len(), 1);
}
#[test]
fn two_linears_fully_determined() {
let eq1 = lin(&[(0, 1), (1, 1)], -3, 2);
let eq2 = lin(&[(0, 1), (1, -1)], -1, 2);
let ts = triangular_decompose(&[eq1, eq2]).unwrap();
assert_eq!(ts.polys.len(), 2);
assert_eq!(ts.free_vars.len(), 0);
}
#[test]
fn linear_plus_quadratic_cga_style() {
let mut eq1 = Poly::zero(4);
eq1.add_term(vec![1, 0, 0, 0], Rat::from(1));
eq1.add_term(vec![0, 1, 0, 0], Rat::from(1));
eq1.add_term(vec![0, 0, 0, 0], Rat::from(-1));
let mut eq2 = Poly::zero(4);
eq2.add_term(vec![2, 0, 0, 0], Rat::from(-1));
eq2.add_term(vec![0, 2, 0, 0], Rat::from(1));
eq2.add_term(vec![0, 0, 2, 0], Rat::from(1));
eq2.add_term(vec![0, 0, 0, 2], Rat::from(1));
let result = triangular_decompose(&[eq1, eq2]);
assert!(matches!(result, Err(TriangularError::NotTriangularizable)));
}
#[test]
fn pure_linear_3vars() {
let eq1 = lin(&[(0, 1), (1, 1), (2, 1)], -6, 3);
let eq2 = lin(&[(0, 1), (1, -1)], -2, 3);
let eq3 = lin(&[(1, 1), (2, -1)], -1, 3);
let ts = triangular_decompose(&[eq1, eq2, eq3]).unwrap();
assert_eq!(ts.polys.len(), 3);
assert_eq!(ts.free_vars.len(), 0);
}
#[test]
fn dimension_cga_point() {
let mut eq1 = Poly::zero(4);
eq1.add_term(vec![1, 0, 0, 0], Rat::from(1));
eq1.add_term(vec![0, 1, 0, 0], Rat::from(1));
eq1.add_term(vec![0, 0, 0, 0], Rat::from(-1));
let mut eq2 = Poly::zero(4);
eq2.add_term(vec![2, 0, 0, 0], Rat::from(-1));
eq2.add_term(vec![0, 2, 0, 0], Rat::from(1));
eq2.add_term(vec![0, 0, 2, 0], Rat::from(1));
eq2.add_term(vec![0, 0, 0, 2], Rat::from(1));
let dim = dimension(&[eq1, eq2], 4);
assert_eq!(dim, 2, "CGA2 Point should have 2 free parameters");
}
#[test]
fn inconsistent_system() {
let mut p = Poly::zero(2);
p.add_term(vec![0, 0], Rat::from(1));
let result = triangular_decompose(&[p]);
let _ = result;
}
}