use num_traits::Signed;
use ocas_atom::{Atom, AtomArena, AtomNode, Symbol};
use ocas_domain::{Domain, Integer, IntegerDomain, Rational, RationalDomain};
use ocas_poly::{Lex, SparseMultivariatePolynomial};
use crate::tower::convert::{GeneratorField, atom_to_rational, rational_to_atom};
type Sparse = SparseMultivariatePolynomial<ocas_domain::RationalDomain, Lex>;
#[derive(Clone, Debug)]
struct FPoly {
terms: Vec<(usize, GeneratorField)>,
}
fn rat_const(n: i64, n_vars: usize) -> GeneratorField {
GeneratorField::from_polynomial(Sparse::from_terms(
RationalDomain,
n_vars,
vec![(vec![0; n_vars], Rational::new(n, 1))],
))
}
fn fpoly_one(n_vars: usize) -> FPoly {
FPoly {
terms: vec![(0, rat_const(1, n_vars))],
}
}
fn fpoly_zero(n_vars: usize) -> FPoly {
FPoly {
terms: vec![(0, GeneratorField::zero(&RationalDomain, n_vars))],
}
}
fn mono_reduce(mut g: GeneratorField) -> GeneratorField {
if g.numerator.n_terms() != 1 || g.denominator.n_terms() != 1 {
return g;
}
let (en, cn) = match g.numerator.terms_ref().iter().next() {
Some((e, c)) => (e.clone(), c.clone()),
None => return g,
};
let (ed, cd) = match g.denominator.terms_ref().iter().next() {
Some((e, c)) => (e.clone(), c.clone()),
None => return g,
};
if en.len() != ed.len() {
return g;
}
let common: Vec<usize> = en.iter().zip(&ed).map(|(a, b)| (*a).min(*b)).collect();
if common.iter().all(|&v| v == 0) {
return g;
}
let new_en: Vec<usize> = en.iter().zip(&common).map(|(a, b)| a - b).collect();
let new_ed: Vec<usize> = ed.iter().zip(&common).map(|(a, b)| a - b).collect();
g.numerator = Sparse::from_terms(RationalDomain, en.len(), vec![(new_en, cn)]);
g.denominator = Sparse::from_terms(RationalDomain, ed.len(), vec![(new_ed, cd)]);
g
}
impl FPoly {
fn n_vars(&self) -> usize {
self.terms[0].1.n_vars()
}
fn is_zero(&self) -> bool {
self.terms.iter().all(|(_, c)| c.is_zero())
}
fn degree(&self) -> Option<usize> {
self.terms
.iter()
.rev()
.find(|(_, c)| !c.is_zero())
.map(|(p, _)| *p)
}
fn leading_coeff(&self) -> Option<GeneratorField> {
self.terms
.iter()
.rev()
.find(|(_, c)| !c.is_zero())
.map(|(_, c)| c.clone())
}
fn trim(&mut self) {
let n_vars = self.n_vars();
self.terms.retain(|(_, c)| !c.is_zero());
self.terms.sort_by_key(|(p, _)| *p);
if self.terms.is_empty() {
self.terms
.push((0, GeneratorField::zero(&RationalDomain, n_vars)));
}
}
fn add(&self, other: &Self) -> Self {
let mut out = self.clone();
for (p, c) in &other.terms {
match out.terms.iter_mut().find(|(q, _)| q == p) {
Some((_, acc)) => *acc = acc.add(c),
None => out.terms.push((*p, c.clone())),
}
}
out.trim();
out
}
fn neg(&self) -> Self {
Self {
terms: self.terms.iter().map(|(p, c)| (*p, c.neg())).collect(),
}
}
fn sub(&self, other: &Self) -> Self {
self.add(&other.neg())
}
fn mul(&self, other: &Self) -> Self {
let mut out = fpoly_zero(self.n_vars());
for (p, c) in &self.terms {
for (q, d) in &other.terms {
let pow = p + q;
let prod = mono_reduce(c.mul(d));
match out.terms.iter_mut().find(|(r, _)| *r == pow) {
Some((_, acc)) => *acc = mono_reduce(acc.add(&prod)),
None => out.terms.push((pow, prod)),
}
}
}
out.trim();
out
}
fn derivative(&self) -> Self {
let mut out: Vec<(usize, GeneratorField)> = self
.terms
.iter()
.filter(|(p, _)| *p > 0)
.map(|(p, c)| {
(
*p - 1,
mono_reduce(c.mul(&rat_const(*p as i64, self.n_vars()))),
)
})
.collect();
if out.is_empty() {
out.push((0, GeneratorField::zero(&RationalDomain, self.n_vars())));
}
Self { terms: out }
}
fn to_sparse(&self) -> Sparse {
let n_vars = self.n_vars() + 1;
let mut terms: Vec<(Vec<usize>, _)> = Vec::new();
for (pow, c) in &self.terms {
for (e, coeff) in c.numerator.terms_ref() {
let mut e2 = vec![*pow];
e2.extend_from_slice(e);
terms.push((e2, coeff.clone()));
}
for (e, coeff) in c.denominator.terms_ref() {
let mut e2 = vec![*pow];
e2.extend_from_slice(e);
terms.push((e2, RationalDomain.neg(coeff)));
}
}
Sparse::from_terms(RationalDomain, n_vars, terms)
}
fn from_sparse(p: &Sparse) -> Self {
let nsym = p.n_vars() - 1;
let mut out = fpoly_zero(nsym);
for (e, c) in p.terms_ref() {
let sym = GeneratorField::from_polynomial(Sparse::from_terms(
RationalDomain,
nsym,
vec![(e[1..].to_vec(), c.clone())],
));
out = out.add(&FPoly {
terms: vec![(e[0], sym)],
});
}
out
}
fn div_rem(&self, den: &Self) -> Option<(Self, Self)> {
let mut q = fpoly_zero(self.n_vars());
let mut r = self.clone();
let dd = den.degree()?;
let lc_d = den.leading_coeff()?;
while let Some(dr) = r.degree() {
if dr < dd {
break;
}
let lc_r = r.leading_coeff()?;
let c = mono_reduce(lc_r.div(&lc_d)?);
let t = FPoly {
terms: vec![(dr - dd, c)],
};
q = q.add(&t);
r = r.sub(&den.mul(&t));
}
Some((q, r))
}
fn rem(&self, den: &Self) -> Option<Self> {
self.div_rem(den).map(|(_, r)| r)
}
fn eval(&self, v: &GeneratorField) -> Option<GeneratorField> {
let mut acc = GeneratorField::zero(&RationalDomain, self.n_vars());
let mut prev: Option<usize> = None;
for (exp, c) in self.terms.iter().rev() {
let gap = prev.map(|p| p - *exp).unwrap_or(0);
for _ in 0..=gap {
acc = acc.mul(v);
}
acc = acc.add(c);
prev = Some(*exp);
}
if let Some(p) = prev {
for _ in 0..p {
acc = acc.mul(v);
}
}
Some(acc)
}
fn to_atom<'a>(&self, ctx: &'a AtomArena<'a>, gens: &[Atom<'a>]) -> Option<Atom<'a>> {
let mut terms: Vec<Atom<'a>> = Vec::new();
for (pow, c) in &self.terms {
let c_atom = rational_to_atom(ctx, c, &gens[1..])?;
let x = gens[0];
let xp = if *pow == 0 {
ctx.num(1)
} else {
ctx.pow(x, ctx.num(*pow as i64))
};
terms.push(ctx.mul(&[c_atom, xp]));
}
if terms.is_empty() {
return Some(ctx.num(0));
}
Some(ocas_atom::normalize::normalize(ctx, ctx.add(&terms)))
}
}
fn fpoly_gcd(a: &FPoly, b: &FPoly) -> Option<FPoly> {
if a.is_zero() {
return Some(b.clone());
}
if b.is_zero() {
return Some(a.clone());
}
let mut old_r = a.clone();
let mut r = b.clone();
let mut steps = 0usize;
while !r.is_zero() {
steps += 1;
if steps > 512 {
return None;
}
let (_, rem) = old_r.div_rem(&r)?;
old_r = r;
r = rem;
}
let mut g = old_r;
if let Some(lc) = g.leading_coeff() {
if let Some(inv) = lc.inv() {
g = g.scale(&inv);
}
}
g.trim();
Some(g)
}
impl FPoly {
fn scale(&self, c: &GeneratorField) -> Self {
Self {
terms: self
.terms
.iter()
.map(|(p, q)| (*p, mono_reduce(q.mul(c))))
.collect(),
}
}
}
fn square_free_factors(p: &FPoly) -> Option<Vec<(FPoly, usize)>> {
if p.degree() == Some(0) {
return Some(Vec::new());
}
let p_prime = p.derivative();
let a0 = fpoly_gcd(p, &p_prime)?;
if a0.degree() == Some(0) {
return Some(vec![(p.clone(), 1)]);
}
let (b1, _) = p.div_rem(&a0)?;
let (c1, _) = p_prime.div_rem(&a0)?;
let mut b = b1;
let mut d = c1.sub(&b.derivative());
let mut result: Vec<(FPoly, usize)> = Vec::new();
let mut i = 1usize;
while b.degree() != Some(0) {
let ai = fpoly_gcd(&b, &d)?;
let (b_next, _) = b.div_rem(&ai)?;
let (c_next, _) = d.div_rem(&ai)?;
let d_next = c_next.sub(&b_next.derivative());
if ai.degree() != Some(0) {
result.push((ai, i));
}
b = b_next;
d = d_next;
i += 1;
}
if result.is_empty() {
result.push((p.clone(), 1));
}
Some(result)
}
type HermiteParts = (Vec<(FPoly, FPoly)>, FPoly, FPoly);
fn hermite_reduce(num: &FPoly, den: &FPoly) -> Option<HermiteParts> {
let mut b_parts: Vec<(FPoly, FPoly)> = Vec::new();
let mut a = num.clone();
let mut d = den.clone();
loop {
let factors = square_free_factors(&d)?;
let Some((f, m)) = factors.iter().find(|(_, m)| *m >= 2).cloned() else {
break;
};
let mut f_pow = fpoly_one(d.n_vars());
for _ in 0..m {
f_pow = f_pow.mul(&f);
}
let (d1, r) = d.div_rem(&f_pow)?;
if !r.is_zero() {
return None;
}
let f_prime = f.derivative();
let w = f_prime
.scale(&rat_const((m - 1) as i64, d.n_vars()))
.mul(&d1);
let (_, t) = extended_gcd(&f, &w)?;
let b = a.mul(&t).neg().rem(&f)?;
let b_prime = b.derivative();
let inner = b_prime
.mul(&f)
.sub(
&b.mul(&f_prime)
.scale(&rat_const((m - 1) as i64, d.n_vars())),
)
.mul(&d1);
let (c, r2) = a.sub(&inner).div_rem(&f)?;
if !r2.is_zero() {
return None;
}
let mut f_pow_m1 = fpoly_one(d.n_vars());
for _ in 0..(m - 1) {
f_pow_m1 = f_pow_m1.mul(&f);
}
b_parts.push((b, f_pow_m1.clone()));
a = c;
d = d1.mul(&f_pow_m1);
}
Some((b_parts, a, d))
}
fn extended_gcd(a: &FPoly, b: &FPoly) -> Option<(FPoly, FPoly)> {
let mut old_r = a.clone();
let mut r = b.clone();
let mut old_s = fpoly_one(a.n_vars());
let mut s = fpoly_zero(a.n_vars());
let mut old_t = fpoly_zero(a.n_vars());
let mut t = fpoly_one(a.n_vars());
while !r.is_zero() {
let (q, rem) = old_r.div_rem(&r)?;
old_r = r;
r = rem;
let new_s = old_s.sub(&q.mul(&s));
old_s = s;
s = new_s;
let new_t = old_t.sub(&q.mul(&t));
old_t = t;
t = new_t;
}
if let Some(lc) = old_r.leading_coeff() {
if let Some(inv) = lc.inv() {
old_s = old_s.scale(&inv);
old_t = old_t.scale(&inv);
}
}
Some((old_s, old_t))
}
fn linear_coeffs(f: &FPoly) -> Option<(GeneratorField, GeneratorField)> {
let a1 = f
.terms
.iter()
.find(|(p, _)| *p == 1)
.map(|(_, c)| c.clone())
.unwrap_or_else(|| GeneratorField::zero(&RationalDomain, f.n_vars()));
let a0 = f
.terms
.iter()
.find(|(p, _)| *p == 0)
.map(|(_, c)| c.clone())
.unwrap_or_else(|| GeneratorField::zero(&RationalDomain, f.n_vars()));
Some((a1, a0))
}
fn collect_symbols(expr: Atom<'_>, var: Symbol, out: &mut Vec<Symbol>) {
match expr.node() {
AtomNode::Var(v) => {
if *v != var && !out.contains(v) {
out.push(*v);
}
}
AtomNode::Num(_) => {}
AtomNode::Fun(_, args) => {
for a in *args {
collect_symbols(*a, var, out);
}
}
AtomNode::Add(args) | AtomNode::Mul(args) => {
for a in *args {
collect_symbols(*a, var, out);
}
}
AtomNode::Pow(base, exp) => {
collect_symbols(*base, var, out);
collect_symbols(*exp, var, out);
}
}
}
fn discriminant(a: &GeneratorField, b: &GeneratorField, c: &GeneratorField) -> GeneratorField {
let four = rat_const(4, a.n_vars());
four.mul(a).mul(c).sub(&b.mul(b))
}
fn rational_square_root(delta: &GeneratorField) -> Option<GeneratorField> {
let sqrt_sparse = |p: &Sparse| -> Option<Sparse> {
let mut terms: Vec<(Vec<usize>, Rational)> = Vec::new();
for (e, c) in p.terms_ref() {
if e.iter().any(|&v| v % 2 != 0) {
return None;
}
let rp = isqrt_i64(c.numer().to_i64()?)?;
terms.push((e.iter().map(|v| v / 2).collect(), Rational::new(rp, 1)));
}
Some(Sparse::from_terms(RationalDomain, p.n_vars(), terms))
};
let n = sqrt_sparse(&delta.numerator)?;
let d = sqrt_sparse(&delta.denominator)?;
Some(GeneratorField::from_num_den(n, d))
}
fn factor_via_integer(f: &FPoly) -> Option<Vec<(FPoly, usize)>> {
let sparse = f.to_sparse();
for e in sparse.terms_ref().keys() {
if e[1..].iter().any(|&v| v != 0) {
return None;
}
}
let mut d: i64 = 1;
for c in sparse.terms_ref().values() {
d = d / gcd_i64(d, c.denom().to_i64()?) * c.denom().to_i64()?;
}
let mut int_terms: Vec<(Vec<usize>, Integer)> = Vec::new();
for (e, c) in sparse.terms_ref() {
let den = c.denom().to_i64()?;
let num = c.numer().to_i64()? * (d / den);
int_terms.push((e.to_vec(), Integer::from(num)));
}
let int_poly: SparseMultivariatePolynomial<IntegerDomain, Lex> =
SparseMultivariatePolynomial::from_terms(IntegerDomain, sparse.n_vars(), int_terms);
let fac = int_poly.factor();
let mut out: Vec<(FPoly, usize)> = Vec::new();
for (g, k) in fac {
let mut terms: Vec<(Vec<usize>, Rational)> = Vec::new();
for (e, c) in g.terms_ref() {
let num = c.to_i64()?;
terms.push((e.to_vec(), Rational::new(num, 1)));
}
let gq = Sparse::from_terms(RationalDomain, sparse.n_vars(), terms);
out.push((FPoly::from_sparse(&gq), k));
}
let _ = d;
if out.is_empty() {
return None;
}
Some(out)
}
fn gcd_i64(mut a: i64, mut b: i64) -> i64 {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a.abs().max(1)
}
fn field_divisors(el: &GeneratorField) -> Option<Vec<GeneratorField>> {
let mut out: Vec<GeneratorField> = Vec::new();
for e in el.numerator.terms_ref().keys() {
let mut exps: Vec<Vec<usize>> = vec![vec![0; e.len()]];
for (i, &v) in e.iter().enumerate() {
let mut next = Vec::new();
for cur in &exps {
for k in 0..=v {
let mut c = cur.clone();
c[i] = k;
next.push(c);
}
}
exps = next;
}
for exp in exps {
let p = Sparse::from_terms(
RationalDomain,
el.numerator.n_vars(),
vec![(exp, Rational::new(1, 1))],
);
out.push(GeneratorField::from_polynomial(p));
}
}
for e in el.denominator.terms_ref().keys() {
let mut exps: Vec<Vec<usize>> = vec![vec![0; e.len()]];
for (i, &v) in e.iter().enumerate() {
let mut next = Vec::new();
for cur in &exps {
for k in 0..=v {
let mut c = cur.clone();
c[i] = k;
next.push(c);
}
}
exps = next;
}
for exp in exps {
let p = Sparse::from_terms(
RationalDomain,
el.denominator.n_vars(),
vec![(exp, Rational::new(1, 1))],
);
out.push(GeneratorField::from_polynomial(p).inv()?);
}
}
Some(out)
}
fn split_linear_candidates(f: &FPoly) -> Option<Vec<(FPoly, usize)>> {
let lc = f.leading_coeff()?;
let a0 = f
.terms
.iter()
.find(|(p, _)| *p == 0)
.map(|(_, c)| c.clone())
.unwrap_or_else(|| GeneratorField::zero(&RationalDomain, f.n_vars()));
let mut candidates: Vec<GeneratorField> = Vec::new();
if a0.is_zero() {
candidates.push(GeneratorField::zero(&RationalDomain, f.n_vars()));
} else {
for da in field_divisors(&a0)? {
for dl in field_divisors(&lc)? {
if !dl.is_zero() {
let r = da.div(&dl)?;
candidates.push(r.neg());
candidates.push(r);
}
}
}
}
for r in candidates {
if f.eval(&r)?.is_zero() {
let one = GeneratorField::one(&RationalDomain, f.n_vars());
let mut g = FPoly {
terms: vec![(1, one), (0, r.neg())],
};
g.trim();
let (q, rem) = f.div_rem(&g)?;
if rem.is_zero() && q.degree()? >= 1 {
return Some(vec![(g, 1), (q, 1)]);
}
}
}
None
}
fn split_squarefree_factors(factors: Vec<(FPoly, usize)>) -> Option<Vec<(FPoly, usize)>> {
let mut out: Vec<(FPoly, usize)> = Vec::new();
for (f, m) in factors {
let deg = f.degree()?;
if deg > 2 && deg <= 3 {
if let Some(sub) = split_linear_candidates(&f) {
let mut sub = split_squarefree_factors(sub)?;
out.append(&mut sub);
continue;
}
if let Some(sub) = factor_via_integer(&f) {
let mut sub = split_squarefree_factors(sub)?;
out.append(&mut sub);
continue;
}
out.push((f, m));
continue;
}
if deg == 2 {
let (a, b, c) = quadratic_coeffs_fpoly(&f)?;
let delta = b.mul(&b).sub(&rat_const(4, f.n_vars()).mul(&a).mul(&c));
if let Some(s) = rational_square_root(&delta) {
let two_a = a.mul(&rat_const(2, f.n_vars()));
let r1 = b.neg().add(&s).div(&two_a)?;
let r2 = b.neg().sub(&s).div(&two_a)?;
let one = GeneratorField::one(&RationalDomain, f.n_vars());
let mut g1 = FPoly {
terms: vec![(1, a.clone()), (0, a.mul(&r1).neg())],
};
g1.trim();
let mut g2 = FPoly {
terms: vec![(1, one), (0, r2.neg())],
};
g2.trim();
out.push((g1, m));
out.push((g2, m));
continue;
}
}
out.push((f, m));
}
Some(out)
}
fn isqrt_i64(n: i64) -> Option<i64> {
if n < 0 {
return None;
}
let r = (n as f64).sqrt() as i64;
[r - 1, r, r + 1]
.into_iter()
.find(|&c| c >= 0 && c * c == n)
}
fn constant_rational(delta: &GeneratorField) -> Option<ocas_domain::Rational> {
if delta.numerator.n_terms() == 1 && delta.denominator.n_terms() == 1 {
let (e, c) = delta.numerator.terms_ref().iter().next()?;
if e.iter().all(|&v| v == 0) {
return Some(c.clone());
}
}
None
}
pub(crate) fn rational_complexity_ok<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> bool {
let mut symbols: Vec<Symbol> = Vec::new();
collect_symbols(expr, var, &mut symbols);
if symbols.len() > 5 {
return false;
}
let x = ctx.var(var.as_str());
let mut gens: Vec<Atom<'a>> = vec![x];
for s in &symbols {
gens.push(ctx.var(s.as_str()));
}
let Some(rf) = atom_to_rational(expr, &gens) else {
return false;
};
let num = FPoly::from_sparse(&rf.numerator);
let mut den = FPoly::from_sparse(&rf.denominator);
if let Some(g) = fpoly_gcd(&num, &den) {
if g.degree() != Some(0) {
if let (Some((_, r1)), Some((dq, r2))) = (num.div_rem(&g), den.div_rem(&g)) {
if r1.is_zero() && r2.is_zero() {
den = dq;
}
}
}
}
den.degree().is_some_and(|d| d <= 6)
}
pub(crate) fn integrate_rational_symbolic<'a>(
ctx: &'a AtomArena<'a>,
expr: Atom<'a>,
var: Symbol,
) -> Option<Atom<'a>> {
let x = ctx.var(var.as_str());
let mut symbols: Vec<Symbol> = Vec::new();
collect_symbols(expr, var, &mut symbols);
symbols.sort_by_key(|s| s.as_str().to_string());
let mut gens: Vec<Atom<'a>> = vec![x];
for s in &symbols {
gens.push(ctx.var(s.as_str()));
}
let rf = atom_to_rational(expr, &gens)?;
let mut num = FPoly::from_sparse(&rf.numerator);
let mut den = FPoly::from_sparse(&rf.denominator);
if den.degree()? > 6 {
return None;
}
if let Some(g) = fpoly_gcd(&num, &den) {
if g.degree() != Some(0) {
if let (Some((nq, r1)), Some((dq, r2))) = (num.div_rem(&g), den.div_rem(&g)) {
if r1.is_zero() && r2.is_zero() {
num = nq;
den = dq;
}
}
}
}
let n_vars = den.n_vars();
let mut parts: Vec<Atom<'a>> = Vec::new();
let (quotient, remainder) = num.div_rem(&den)?;
for (p, c) in quotient.terms {
if p == 0 {
let c_atom = rational_to_atom(ctx, &c, &gens[1..])?;
parts.push(ctx.mul(&[c_atom, x]));
} else {
let p1 = p as i64 + 1;
let c_atom = rational_to_atom(ctx, &c, &gens[1..])?;
parts.push(ctx.mul(&[
c_atom,
ctx.pow(x, ctx.num(p1)),
ctx.pow(ctx.num(p1), ctx.num(-1)),
]));
}
}
let (b_parts, c_num, squarefree_den) = match hermite_reduce(&remainder, &den) {
Some(v) => v,
None => {
return None;
}
};
let mut c_num = c_num;
let mut squarefree_den = squarefree_den;
if let Some(lc) = squarefree_den.leading_coeff() {
if let Some(inv) = lc.inv() {
c_num = c_num.scale(&inv);
squarefree_den = squarefree_den.scale(&inv);
}
}
for (b, d2) in b_parts {
let b_atom = b.to_atom(ctx, &gens)?;
let d2_atom = d2.to_atom(ctx, &gens)?;
parts.push(ctx.mul(&[b_atom, ctx.pow(d2_atom, ctx.num(-1))]));
}
let factors = split_squarefree_factors(square_free_factors(&squarefree_den)?)?;
let has_high_degree = factors.iter().any(|(f, _)| f.degree().unwrap_or(0) > 2);
if has_high_degree {
let integrand = ctx.mul(&[
c_num.to_atom(ctx, &gens)?,
ctx.pow(squarefree_den.to_atom(ctx, &gens)?, ctx.num(-1)),
]);
parts.push(ctx.fun("Integral", &[integrand, x]));
return assemble(ctx, parts);
}
for (f, m) in &factors {
debug_assert_eq!(*m, 1);
let f_deg = f.degree()?;
if f_deg == 1 {
let (alpha, beta) = linear_coeffs(f)?;
let r = beta.neg().div(&alpha)?;
let mut denom = alpha.clone();
for (g, _) in &factors {
if g.terms == f.terms {
continue;
}
denom = denom.mul(&g.eval(&r)?);
}
let coeff = c_num.eval(&r)?.div(&denom)?;
let coeff_atom = rational_to_atom(ctx, &coeff, &gens[1..])?;
let f_atom = f.to_atom(ctx, &gens)?;
parts.push(ctx.mul(&[coeff_atom, ctx.fun("log", &[f_atom])]));
} else {
let (m, n) = quadratic_coeffs(&c_num, f, &factors)?;
let (a, b, c) = quadratic_coeffs_fpoly(f)?;
let two_a = a.mul(&rat_const(2, n_vars));
let m_over = m.div(&two_a)?;
let f_atom = f.to_atom(ctx, &gens)?;
let m_atom = rational_to_atom(ctx, &m_over, &gens[1..])?;
if !m_over.is_zero() {
parts.push(ctx.mul(&[m_atom, ctx.fun("log", &[f_atom])]));
}
let delta = discriminant(&a, &b, &c);
let mb_over = m.mul(&b).div(&two_a)?;
let n_shift = n.sub(&mb_over);
let fun = match constant_rational(&delta) {
Some(d) if d.inner().is_positive() => "atan",
Some(_) => "atanh",
None => {
let lead = c
.numerator
.terms_ref()
.iter()
.next()
.map(|(_, cc)| cc.clone());
match lead {
Some(cc) if cc.numer().is_negative() => "atanh",
_ => "atan",
}
}
};
let two = if fun == "atanh" { -2 } else { 2 };
let coeff = n_shift.mul(&rat_const(two, n_vars));
let coeff_atom = rational_to_atom(ctx, &coeff, &gens[1..])?;
let mag = if fun == "atanh" {
delta.neg()
} else {
delta.clone()
};
let sqrt_atom: Atom<'a> = if let Some(r) = rational_square_root(&mag) {
rational_to_atom(ctx, &r, &gens[1..])?
} else {
let d_atom = rational_to_atom(ctx, &mag, &gens[1..])?;
ctx.pow(d_atom, ctx.pow(ctx.num(2), ctx.num(-1)))
};
let lin = FPoly {
terms: vec![(1, two_a), (0, b)],
};
let arg_atom = ctx.mul(&[lin.to_atom(ctx, &gens)?, ctx.pow(sqrt_atom, ctx.num(-1))]);
parts.push(ctx.mul(&[
coeff_atom,
ctx.pow(sqrt_atom, ctx.num(-1)),
ctx.fun(fun, &[arg_atom]),
]));
}
}
assemble(ctx, parts)
}
fn assemble<'a>(ctx: &'a AtomArena<'a>, parts: Vec<Atom<'a>>) -> Option<Atom<'a>> {
if parts.is_empty() {
return Some(ctx.num(0));
}
Some(ocas_atom::normalize::normalize(ctx, ctx.add(&parts)))
}
fn quadratic_coeffs_fpoly(f: &FPoly) -> Option<(GeneratorField, GeneratorField, GeneratorField)> {
let get = |p: usize| -> GeneratorField {
f.terms
.iter()
.find(|(q, _)| *q == p)
.map(|(_, c)| c.clone())
.unwrap_or_else(|| GeneratorField::zero(&RationalDomain, f.n_vars()))
};
Some((get(2), get(1), get(0)))
}
fn quadratic_coeffs(
num: &FPoly,
f: &FPoly,
factors: &[(FPoly, usize)],
) -> Option<(GeneratorField, GeneratorField)> {
let mut q = fpoly_one(f.n_vars());
for (g, _) in factors {
if g.terms == f.terms {
continue;
}
q = q.mul(g);
}
let (a, b, c) = quadratic_coeffs_fpoly(f)?;
let num_mod = num.rem(f)?;
let q_mod = q.rem(f)?;
let (q1, q0) = linear_coeffs(&q_mod)?;
let (p1, p0) = linear_coeffs(&num_mod)?;
let a_inv = a.inv()?;
let m11 = q0.sub(&q1.mul(&b).mul(&a_inv));
let m21 = q1.mul(&c).mul(&a_inv).neg();
let det = m11.mul(&q0).sub(&q1.mul(&m21));
let det_inv = det.inv()?;
let m = p1.mul(&q0).sub(&q1.mul(&p0)).mul(&det_inv);
let n = m11.mul(&p0).sub(&p1.mul(&m21)).mul(&det_inv);
Some((m, n))
}
#[cfg(test)]
mod tests {
use super::*;
use ocas_core::arena::Arena;
fn int_str(input: &str, var: &str) -> String {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let expr = ocas_parse::parse(&ctx, input).unwrap();
crate::integrate(&ctx, expr, Symbol::new(var)).to_string()
}
fn assert_solved(input: &str) {
let r = int_str(input, "x");
assert!(!r.contains("Integral("), "{input} left a residue: {r}");
}
#[test]
fn symbolic_rationals() {
assert_solved("1/(a+b*x^2)");
assert_solved("1/(a-b*x^2)");
assert_solved("1/(x*(a+b*x)^2)");
assert_solved("1/(x^2*(a-b*x^2))");
assert_solved("(d+e*x)/(x^3*(a+c*x^2))");
assert_solved("(A+B*x)/(a+b*x+c*x^2)");
assert_solved("x^2/(a-b*x^2)^3");
}
#[test]
fn numeric_quadratic_irreducible() {
assert_solved("1/(x^2+2*x+3)");
}
}