use super::SolverError;
use crate::kernel::{ExprData, ExprId, ExprPool};
use crate::poly::groebner::GbPoly;
use rug::Rational;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub(crate) struct ClearedEquation {
pub numer: GbPoly,
pub domain: GbPoly,
}
impl ClearedEquation {
pub fn is_polynomial(&self) -> bool {
is_one(&self.domain)
}
}
fn is_one(p: &GbPoly) -> bool {
p.terms.len() == 1
&& p.terms
.iter()
.all(|(exp, c)| exp.iter().all(|&e| e == 0) && *c == 1)
}
pub(crate) fn clear_denominators(
expr: ExprId,
vars: &[ExprId],
pool: &ExprPool,
) -> Result<ClearedEquation, SolverError> {
let n = vars.len();
let (numer, _denom, domain) = rec(expr, vars, n, pool)?;
Ok(ClearedEquation { numer, domain })
}
fn rec(
expr: ExprId,
vars: &[ExprId],
n_vars: usize,
pool: &ExprPool,
) -> Result<(GbPoly, GbPoly, GbPoly), SolverError> {
let one = || GbPoly::constant(Rational::from(1), n_vars);
if let Some(idx) = vars.iter().position(|&v| v == expr) {
let mut exp = vec![0u32; n_vars];
exp[idx] = 1;
let mut terms = BTreeMap::new();
terms.insert(exp, Rational::from(1));
return Ok((GbPoly { terms, n_vars }, one(), one()));
}
enum Node {
IntConst(rug::Integer),
RatConst(Rational),
FloatConst(f64),
FreeSymbol(String),
Add(Vec<ExprId>),
Mul(Vec<ExprId>),
Pow(ExprId, ExprId),
Func(String),
Other,
}
let node = pool.with(expr, |data| match data {
ExprData::Integer(n) => Node::IntConst(n.0.clone()),
ExprData::Rational(r) => Node::RatConst(r.0.clone()),
ExprData::Float(f) => Node::FloatConst(f.inner.to_f64()),
ExprData::Symbol { name, .. } => Node::FreeSymbol(name.clone()),
ExprData::Add(args) => Node::Add(args.clone()),
ExprData::Mul(args) => Node::Mul(args.clone()),
ExprData::Pow { base, exp } => Node::Pow(*base, *exp),
ExprData::Func { name, .. } => Node::Func(name.clone()),
_ => Node::Other,
});
match node {
Node::IntConst(n) => Ok((GbPoly::constant(Rational::from(n), n_vars), one(), one())),
Node::RatConst(r) => Ok((GbPoly::constant(r, n_vars), one(), one())),
Node::FloatConst(v) => {
let r = Rational::from_f64(v).unwrap_or_else(|| Rational::from(0));
Ok((GbPoly::constant(r, n_vars), one(), one()))
}
Node::FreeSymbol(name) => Err(SolverError::NotPolynomial(format!(
"free symbol '{name}' not in variable list"
))),
Node::Add(args) => {
let mut num = GbPoly::zero(n_vars);
let mut den = one();
let mut dom = one();
for a in args {
let (n2, d2, dom2) = rec(a, vars, n_vars, pool)?;
let scaled = if is_one(&den) { n2 } else { n2.mul(&den) };
num = if is_one(&d2) { num } else { num.mul(&d2) }.add(&scaled);
if !is_one(&d2) {
den = den.mul(&d2);
}
dom = combine_domains(dom, dom2);
}
Ok((num, den, dom))
}
Node::Mul(args) => {
let mut num = one();
let mut den = one();
let mut dom = one();
for a in args {
let (n2, d2, dom2) = rec(a, vars, n_vars, pool)?;
num = num.mul(&n2);
if !is_one(&d2) {
den = den.mul(&d2);
}
dom = combine_domains(dom, dom2);
}
Ok((num, den, dom))
}
Node::Pow(base, exp_id) => {
let exp_node = pool.with(exp_id, |d| match d {
ExprData::Integer(n) => Some(n.0.clone()),
_ => None,
});
let Some(n) = exp_node else {
return Err(SolverError::NotPolynomial(
"symbolic or non-integer exponent".to_string(),
));
};
let too_wide = "integer exponent too large to expand";
let Some(n_val) = n.to_i64() else {
return Err(SolverError::NotPolynomial(too_wide.to_string()));
};
if u32::try_from(n_val.unsigned_abs()).is_err() {
return Err(SolverError::NotPolynomial(too_wide.to_string()));
}
let (bn, bd, bdom) = rec(base, vars, n_vars, pool)?;
if n_val >= 0 {
let k = n_val as u64;
let den = if is_one(&bd) { bd } else { pow_poly(&bd, k) };
Ok((pow_poly(&bn, k), den, bdom))
} else {
if bn.is_zero() {
return Err(super::refuse_undefined_equation(
"a denominator is identically zero",
));
}
let k = n_val.unsigned_abs();
let dom = combine_domains(bdom, bn.clone());
Ok((pow_poly(&bd, k), pow_poly(&bn, k), dom))
}
}
Node::Func(name) => Err(SolverError::NotPolynomial(format!(
"function '{name}' is not a polynomial"
))),
Node::Other => Err(SolverError::NotPolynomial(
"unsupported expression node".to_string(),
)),
}
}
fn combine_domains(a: GbPoly, b: GbPoly) -> GbPoly {
if is_one(&b) {
a
} else if is_one(&a) {
b
} else {
a.mul(&b)
}
}
fn pow_poly(p: &GbPoly, k: u64) -> GbPoly {
let mut result = GbPoly::constant(Rational::from(1), p.n_vars);
let mut cur = p.clone();
let mut rem = k;
while rem > 0 {
if rem & 1 == 1 {
result = result.mul(&cur);
}
rem >>= 1;
if rem > 0 {
let cur2 = cur.clone();
cur = cur.mul(&cur2);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::kernel::Domain;
use crate::solver::expr_to_gbpoly;
fn poly(expr: ExprId, vars: &[ExprId], pool: &ExprPool) -> BTreeMap<Vec<u32>, Rational> {
expr_to_gbpoly(expr, vars, pool).unwrap().terms
}
#[test]
fn a_polynomial_clears_to_itself_over_one() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.add(vec![pool.pow(x, pool.integer(2_i32)), pool.integer(-1_i32)]);
let c = clear_denominators(e, &[x], &pool).unwrap();
assert!(c.is_polynomial(), "no denominator was introduced");
assert_eq!(c.numer.terms, poly(e, &[x], &pool));
}
#[test]
fn reciprocal_moves_to_the_denominator() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.pow(x, pool.integer(-1_i32));
let c = clear_denominators(e, &[x], &pool).unwrap();
assert!(!c.is_polynomial());
assert_eq!(c.numer.terms, poly(pool.integer(1_i32), &[x], &pool));
assert_eq!(c.domain.terms, poly(x, &[x], &pool));
}
#[test]
fn a_reciprocal_of_a_quotient_keeps_the_inner_condition() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let inner = pool.add(vec![
pool.pow(x, pool.integer(-1_i32)),
pool.integer(-1_i32),
]);
let c = clear_denominators(pool.pow(inner, pool.integer(-1_i32)), &[x], &pool).unwrap();
assert_eq!(c.numer.terms, poly(x, &[x], &pool));
let one_minus_x = pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), x]),
]);
let expected = pool.mul(vec![x, one_minus_x]);
assert_eq!(c.domain.terms, poly(expected, &[x], &pool));
}
#[test]
fn a_zeroth_power_does_not_erase_its_bases_domain() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let recip = pool.pow(x, pool.integer(-1_i32));
let c = clear_denominators(pool.pow(recip, pool.integer(0_i32)), &[x], &pool).unwrap();
assert!(!c.is_polynomial(), "x ≠ 0 is still required");
assert_eq!(c.domain.terms, poly(x, &[x], &pool));
}
#[test]
fn a_removable_singularity_is_still_a_domain_condition() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.mul(vec![
pool.pow(x, pool.integer(2_i32)),
pool.pow(x, pool.integer(-1_i32)),
]);
let c = clear_denominators(e, &[x], &pool).unwrap();
let x2 = pool.pow(x, pool.integer(2_i32));
assert_eq!(c.numer.terms, poly(x2, &[x], &pool));
assert_eq!(c.domain.terms, poly(x, &[x], &pool));
}
#[test]
fn a_common_denominator_survives_in_both_halves() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let xm1 = pool.add(vec![x, pool.integer(-1_i32)]);
let inv = pool.pow(xm1, pool.integer(-1_i32));
let e = pool.add(vec![
pool.mul(vec![x, inv]),
pool.mul(vec![pool.integer(-1_i32), inv]),
]);
let c = clear_denominators(e, &[x], &pool).unwrap();
let sq = pool.pow(xm1, pool.integer(2_i32));
let expected = poly(sq, &[x], &pool);
assert_eq!(c.numer.terms, expected);
assert_eq!(c.domain.terms, expected);
}
#[test]
fn a_transcendental_keeps_its_refusal() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let e = pool.add(vec![pool.func("exp", vec![x]), pool.integer(-2_i32)]);
let err = clear_denominators(e, &[x], &pool).unwrap_err();
assert!(matches!(err, SolverError::NotPolynomial(_)), "{err}");
assert_eq!(
err.to_string(),
"not a polynomial: function 'exp' is not a polynomial"
);
assert_eq!(crate::errors::AlkahestError::code(&err), "E-SOLVE-001");
}
#[test]
fn an_exponent_wider_than_the_exponent_vector_is_refused() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
for e in [5_000_000_000_i64, -5_000_000_000_i64] {
let expr = pool.pow(x, pool.integer(e));
let err = clear_denominators(expr, &[x], &pool).unwrap_err();
assert!(matches!(err, SolverError::NotPolynomial(_)), "{err}");
assert!(err.to_string().contains("too large"), "{err}");
}
let ok = pool.pow(x, pool.integer(-3_i32));
assert!(clear_denominators(ok, &[x], &pool).is_ok());
}
#[test]
fn an_identically_zero_denominator_is_refused() {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
let zero = pool.add(vec![x, pool.mul(vec![pool.integer(-1_i32), x])]);
let e = pool.pow(zero, pool.integer(-1_i32));
let err = clear_denominators(e, &[x], &pool).unwrap_err();
assert!(matches!(err, SolverError::NotPolynomial(_)), "{err}");
let refusal = crate::solver::take_undefined_equation().expect("refusal recorded");
assert_eq!(crate::errors::AlkahestError::code(&refusal), "E-SOLVE-005");
assert!(
crate::solver::take_undefined_equation().is_none(),
"consuming"
);
}
}