use std::collections::BTreeMap;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::Zero;
use super::LogTerm;
use super::log_to_real::poly_to_genpoly_rf;
use crate::base::numeric::Q;
use crate::poly::dense::Poly;
use crate::poly::generic::GenPoly;
use crate::poly::ratfn::RationalFn;
#[derive(Clone, Debug)]
pub struct LogPartResult {
pub terms: Vec<LogTerm>,
}
pub fn logarithmic_part(a: &Poly, d: &Poly) -> Option<LogPartResult> {
(!d.is_zero()).then(|| logarithmic_part_nonzero(a, d))
}
fn logarithmic_part_nonzero(a: &Poly, d: &Poly) -> LogPartResult {
if a.is_zero() {
return LogPartResult { terms: vec![] };
}
let d_prime = d.derivative();
if d.degree() == Some(1) {
let a_val = a.coeff(0);
let d_lc = d.coeff(1);
let coeff = a_val / d_lc;
if coeff.is_zero() {
return LogPartResult { terms: vec![] };
}
return LogPartResult {
terms: vec![LogTerm::Rational {
coeff,
argument: d.make_monic(),
}],
};
}
let (r_poly, prs) = match lrt_subresultants(a, d, &d_prime) {
Some((r, prs)) => (r, Some(prs)),
None => (Poly::resultant_poly(d, a, &d_prime), None),
};
if r_poly.is_zero() {
return LogPartResult { terms: vec![] };
}
let (_content, r_factors) = r_poly.factor_over_z();
let mut terms: Vec<LogTerm> = Vec::new();
for (factor, multiplicity) in &r_factors {
let deg = match factor.degree() {
Some(d) => d,
None => continue, };
if deg == 0 {
continue;
}
if deg == 1 {
let c = extract_linear_root(factor);
let a_minus_c_dprime = a - &d_prime.scale(&c);
let v = Poly::gcd(d, &a_minus_c_dprime);
if v.degree().unwrap_or(0) == 0 {
continue;
}
if !c.is_zero() {
terms.push(LogTerm::Rational {
coeff: c,
argument: v.make_monic(),
});
}
} else {
let i = *multiplicity as usize;
let log_arg = if d.degree() == Some(i) {
Some(poly_to_genpoly_rf(&d.make_monic()))
} else {
prs.as_ref()
.and_then(|p| p.get(&i))
.and_then(|s| reduce_log_arg(s, factor))
};
if log_arg.is_none() {
tracing::debug!(
min_poly_degree = deg,
multiplicity = i,
"logarithmic_part: no usable remainder of degree i"
);
}
terms.push(LogTerm::Algebraic {
min_poly: factor.clone(),
log_arg,
});
}
}
LogPartResult { terms }
}
fn lrt_subresultants(
a: &Poly,
d: &Poly,
d_prime: &Poly,
) -> Option<(Poly, BTreeMap<usize, GenPoly<RationalFn>>)> {
use crate::poly::zpoly::{denominator_lcm, integer_scaled, ztx_subresultant_prs};
let constant = |c: BigInt| if c.is_zero() { Vec::new() } else { vec![c] };
let d_z: Vec<Vec<BigInt>> = integer_scaled(d.coeffs())
.into_iter()
.map(constant)
.collect();
let den = denominator_lcm(a.coeffs().iter().chain(d_prime.coeffs()));
let len = a.coeffs().len().max(d_prime.coeffs().len());
let b_z: Vec<Vec<BigInt>> = (0..len)
.map(|k| {
let mut c = vec![
(a.coeff(k) * &den).to_integer(),
-(d_prime.coeff(k) * &den).to_integer(),
];
crate::poly::zpoly::z_normalize(&mut c);
c
})
.collect();
let chain = ztx_subresultant_prs(&d_z, &b_z)?;
let to_poly =
|c: Vec<BigInt>| Poly::from_coeffs(c.into_iter().map(Ratio::from_integer).collect());
let members = chain
.members
.into_iter()
.map(|(deg, member)| {
let coeffs = member
.into_iter()
.map(|c| RationalFn::from_poly(to_poly(c)))
.collect();
(deg, GenPoly::from_coeffs(coeffs))
})
.collect();
Some((to_poly(chain.resultant), members))
}
#[cfg(test)]
fn primitive_over_q_t(p: GenPoly<RationalFn>) -> GenPoly<RationalFn> {
let coeffs = &p.coeffs;
let mut lcm = Poly::from_int(1);
for c in coeffs {
let d = c.denom();
if !d.is_constant() {
let g = Poly::gcd(&lcm, d);
lcm = (&lcm * d).div(&g);
}
}
let numers: Vec<Poly> = coeffs
.iter()
.map(|c| (c.numer() * &lcm).div(c.denom()))
.collect();
let content = numers
.iter()
.filter(|n| !n.is_zero())
.fold(Poly::zero(), |g, n| Poly::gcd(&g, n));
if content.is_zero() {
return p;
}
GenPoly::from_coeffs(
numers
.iter()
.map(|n| RationalFn::from_poly(n.div(&content)))
.collect(),
)
}
fn reduce_log_arg(s: &GenPoly<RationalFn>, q: &Poly) -> Option<GenPoly<RationalFn>> {
let lc = s.leading_coeff()?.clone();
let mut coeffs = Vec::with_capacity(s.coeffs.len());
for c in &s.coeffs {
let num = (c.numer() * lc.denom()).rem(q);
let den = (c.denom() * lc.numer()).rem(q);
let inv = inverse_mod(&den, q)?;
coeffs.push(RationalFn::from_poly((&num * &inv).rem(q)));
}
Some(GenPoly::from_coeffs(coeffs))
}
fn inverse_mod(a: &Poly, q: &Poly) -> Option<Poly> {
if a.is_zero() {
return None;
}
let eg = Poly::extended_gcd(a, q);
if eg.gcd.degree() != Some(0) {
return None;
}
Some(eg.x.rem(q))
}
fn extract_linear_root(factor: &Poly) -> Q {
let a = factor.coeff(1);
let b = factor.coeff(0);
if a.is_zero() { Ratio::zero() } else { -b / a }
}
#[cfg(test)]
mod tests {
use super::*;
use num_bigint::BigInt;
use num_traits::Signed;
fn rat(n: i64, d: i64) -> Q {
Ratio::new(BigInt::from(n), BigInt::from(d))
}
fn verify_log_part(a: &Poly, d: &Poly, result: &LogPartResult) {
let mut sum_numer = Poly::zero();
let mut sum_denom = Poly::from_int(1);
for term in &result.terms {
match term {
LogTerm::Rational { coeff, argument } => {
let v = argument;
let v_prime = v.derivative();
let c_poly = Poly::constant(coeff.clone());
sum_numer = &(&sum_numer * v) + &(&(&c_poly * &v_prime) * &sum_denom);
sum_denom = &sum_denom * v;
}
LogTerm::Algebraic { .. } => {
return;
}
}
}
let lhs = &sum_numer * d;
let rhs = &(a * &sum_denom);
let diff = &lhs - rhs;
assert!(
diff.is_zero(),
"Rothstein-Trager verification failed:\n d/dx(Σ cᵢ ln(vᵢ)) ≠ A/D\n diff = {diff}"
);
}
#[test]
fn log_part_one_over_x() {
let a = Poly::from_int(1);
let d = Poly::x();
let result = logarithmic_part(&a, &d).unwrap();
assert_eq!(result.terms.len(), 1, "should have one log term");
match &result.terms[0] {
LogTerm::Rational { coeff, argument } => {
assert_eq!(*coeff, rat(1, 1), "coefficient should be 1");
assert_eq!(argument.degree(), Some(1), "argument should be linear (x)");
}
_ => panic!("expected rational log term"),
}
verify_log_part(&a, &d, &result);
}
#[test]
fn log_part_one_over_x_minus_1() {
let a = Poly::from_int(1);
let d = Poly::from_coeffs(vec![rat(-1, 1), rat(1, 1)]);
let result = logarithmic_part(&a, &d).unwrap();
assert_eq!(result.terms.len(), 1);
match &result.terms[0] {
LogTerm::Rational { coeff, argument } => {
assert_eq!(*coeff, rat(1, 1));
assert_eq!(argument.degree(), Some(1));
}
_ => panic!("expected rational log term"),
}
verify_log_part(&a, &d, &result);
}
#[test]
fn log_part_one_over_x_squared_minus_1() {
let a = Poly::from_int(1);
let d = Poly::from_coeffs(vec![rat(-1, 1), rat(0, 1), rat(1, 1)]);
let result = logarithmic_part(&a, &d).unwrap();
let rational_count = result
.terms
.iter()
.filter(|t| matches!(t, LogTerm::Rational { .. }))
.count();
assert!(
rational_count >= 1,
"should have rational log terms for 1/(x^2-1), got {} rational terms out of {} total",
rational_count,
result.terms.len()
);
verify_log_part(&a, &d, &result);
}
#[test]
fn log_part_2x_over_x_squared_plus_1() {
let a = Poly::from_coeffs(vec![rat(0, 1), rat(2, 1)]); let d = Poly::from_coeffs(vec![rat(1, 1), rat(0, 1), rat(1, 1)]);
let result = logarithmic_part(&a, &d).unwrap();
assert!(
!result.terms.is_empty(),
"should have at least one log term for 2x/(x^2+1)"
);
verify_log_part(&a, &d, &result);
}
#[test]
fn log_part_one_over_x_squared_plus_1() {
let a = Poly::from_int(1);
let d = Poly::from_coeffs(vec![rat(1, 1), rat(0, 1), rat(1, 1)]);
let result = logarithmic_part(&a, &d).unwrap();
let has_algebraic = result
.terms
.iter()
.any(|t| matches!(t, LogTerm::Algebraic { .. }));
assert!(
has_algebraic || result.terms.is_empty(),
"1/(x^2+1) should produce algebraic log terms or be handled as arctan"
);
}
#[test]
fn log_part_zero_numerator() {
let a = Poly::zero();
let d = Poly::from_coeffs(vec![rat(-1, 1), rat(0, 1), rat(1, 1)]); let result = logarithmic_part(&a, &d).unwrap();
assert!(
result.terms.is_empty(),
"zero numerator should give empty result"
);
}
#[test]
fn log_part_partial_fraction_style() {
let a = Poly::from_coeffs(vec![rat(5, 1), rat(3, 1)]); let d = Poly::from_coeffs(vec![rat(2, 1), rat(3, 1), rat(1, 1)]);
let result = logarithmic_part(&a, &d).unwrap();
verify_log_part(&a, &d, &result);
let rational_terms: Vec<_> = result
.terms
.iter()
.filter_map(|t| match t {
LogTerm::Rational { coeff, argument } => Some((coeff.clone(), argument.clone())),
_ => None,
})
.collect();
assert_eq!(
rational_terms.len(),
2,
"should have 2 rational log terms, got {}: {:?}",
rational_terms.len(),
rational_terms
);
let mut coeffs: Vec<Q> = rational_terms.iter().map(|(c, _)| c.clone()).collect();
coeffs.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert_eq!(
coeffs,
vec![rat(1, 1), rat(2, 1)],
"coefficients should be [1, 2], got {coeffs:?}"
);
}
#[test]
fn log_part_one_over_x_cubed_minus_1() {
let a = Poly::from_int(1);
let d = Poly::from_coeffs(vec![rat(-1, 1), rat(0, 1), rat(0, 1), rat(1, 1)]);
let result = logarithmic_part(&a, &d).unwrap();
assert!(!result.terms.is_empty(), "1/(x^3-1) should have log terms");
let all_rational = result
.terms
.iter()
.all(|t| matches!(t, LogTerm::Rational { .. }));
if all_rational {
verify_log_part(&a, &d, &result);
}
}
#[test]
fn log_part_x_over_x_squared_minus_1() {
let a = Poly::x(); let d = Poly::from_coeffs(vec![rat(-1, 1), rat(0, 1), rat(1, 1)]);
let result = logarithmic_part(&a, &d).unwrap();
verify_log_part(&a, &d, &result);
let rational_terms: Vec<_> = result
.terms
.iter()
.filter_map(|t| match t {
LogTerm::Rational { coeff, .. } => Some(coeff.clone()),
_ => None,
})
.collect();
for c in &rational_terms {
assert_eq!(c.abs(), rat(1, 2), "coefficients should be ±1/2, got {c}");
}
}
fn assert_log_arg_divides(d: &Poly, q: &Poly, s: &GenPoly<RationalFn>) {
let d_gp = poly_to_genpoly_rf(d);
let (_, rem) = d_gp.div_rem(s);
for c in &rem.coeffs {
assert!(c.denom().is_constant(), "remainder coefficient {c:?}");
assert!(
c.numer().rem(q).is_zero(),
"S does not divide D modulo q: remainder coefficient {c:?}"
);
}
}
fn algebraic_terms(result: &LogPartResult) -> Vec<(Poly, GenPoly<RationalFn>)> {
result
.terms
.iter()
.filter_map(|t| match t {
LogTerm::Algebraic { min_poly, log_arg } => {
Some((min_poly.clone(), log_arg.clone().expect("log argument")))
}
LogTerm::Rational { .. } => None,
})
.collect()
}
fn poly(coeffs: &[i64]) -> Poly {
Poly::from_coeffs(coeffs.iter().map(|&c| rat(c, 1)).collect())
}
#[test]
fn log_argument_has_the_degree_of_the_multiplicity() {
let a = poly(&[0, 0, 0, 1]);
let d = poly(&[1, 0, 0, 0, 0, 0, 0, 0, 1]);
let result = logarithmic_part(&a, &d).unwrap();
let alg = algebraic_terms(&result);
assert_eq!(alg.len(), 1, "{:?}", result.terms);
let (q, s) = &alg[0];
assert_eq!(q.primitive_part(), poly(&[1, 0, 64]));
assert_eq!(s.degree(), Some(4));
assert_eq!(s.coeff(0).numer(), &poly(&[0, 8]));
for k in 1..4 {
assert!(s.coeff(k).numer().is_zero());
}
assert_log_arg_divides(&d, q, s);
}
#[test]
fn log_arguments_of_mixed_factors() {
let a = poly(&[0, 1]);
let d = poly(&[1, 0, 0, 0, 0, 0, 1]);
let result = logarithmic_part(&a, &d).unwrap();
let rational: Vec<_> = result
.terms
.iter()
.filter_map(|t| match t {
LogTerm::Rational { coeff, argument } => Some((coeff.clone(), argument.clone())),
LogTerm::Algebraic { .. } => None,
})
.collect();
assert_eq!(rational, vec![(rat(1, 6), poly(&[1, 0, 1]))]);
let alg = algebraic_terms(&result);
assert_eq!(alg.len(), 1);
let (q, s) = &alg[0];
assert_eq!(q.primitive_part(), poly(&[1, 6, 36]));
assert_eq!(s.degree(), Some(2));
assert_log_arg_divides(&d, q, s);
}
#[test]
fn log_argument_of_a_simple_quadratic_factor() {
let a = poly(&[1]);
let d = poly(&[1, 1, 1]);
let alg = algebraic_terms(&logarithmic_part(&a, &d).unwrap());
assert_eq!(alg.len(), 1);
let (q, s) = &alg[0];
assert_eq!(q.primitive_part(), poly(&[1, 0, 3]));
assert_eq!(s.degree(), Some(1));
assert_eq!(
s.coeff(0).numer(),
&Poly::from_coeffs(vec![rat(1, 2), rat(3, 2)])
);
assert_log_arg_divides(&d, q, s);
}
#[test]
fn extract_linear_root_basic() {
let p = Poly::from_coeffs(vec![rat(-3, 1), rat(1, 1)]);
assert_eq!(extract_linear_root(&p), rat(3, 1));
}
#[test]
fn extract_linear_root_scaled() {
let p = Poly::from_coeffs(vec![rat(6, 1), rat(2, 1)]);
assert_eq!(extract_linear_root(&p), rat(-3, 1));
}
#[test]
fn extract_linear_root_fractional() {
let p = Poly::from_coeffs(vec![rat(-1, 1), rat(3, 1)]);
assert_eq!(extract_linear_root(&p), rat(1, 3));
}
#[test]
fn subresultant_log_arguments_match_the_euclidean_route() {
use super::super::log_to_real::poly_to_genpoly_rf_times_t;
use crate::base::rng::SplitMix64;
let p = |c: &[i64]| Poly::from_coeffs(c.iter().map(|&k| rat(k, 1)).collect());
let as_pairs = |s: &GenPoly<RationalFn>| -> Vec<(Poly, Poly)> {
s.coeffs
.iter()
.map(|c| (c.numer().clone(), c.denom().clone()))
.collect()
};
let mut cases: Vec<(Poly, Poly)> = vec![
(p(&[1]), p(&[-2, 0, 0, 1])), (p(&[0, 0, 0, 1]), p(&[1, 0, 0, 0, 0, 0, 0, 0, 1])), (p(&[1]), p(&[1, 0, 0, 0, 1])), (p(&[1, 0, 0, 0, 1]), p(&[1, 0, 0, 0, 0, 0, 1])), (p(&[0, 1]), p(&[1, 0, 0, 0, 1, 0, 0, 0, 1])), (p(&[0, 0, 1]), p(&[1, 0, 1, 0, 0, -2, 0, 0, 1])), ];
let mut rng = SplitMix64::new(20260924);
while cases.len() < 16 {
let mut rand = |deg: usize| {
let mut c: Vec<i64> = (0..deg).map(|_| (rng.next_u64() % 7) as i64 - 3).collect();
c.push(1);
p(&c)
};
let d = rand(3 + cases.len() % 5);
let a = rand(d.degree().unwrap_or(1) - 1);
if Poly::gcd(&d, &d.derivative()).degree() == Some(0)
&& Poly::gcd(&a, &d).degree() == Some(0)
{
cases.push((a, d));
}
}
for (a, d) in &cases {
let d_prime = d.derivative();
let (r_new, prs_new) = lrt_subresultants(a, d, &d_prime).unwrap();
let r_old = Poly::resultant_poly(d, a, &d_prime);
let (_, factors) = r_old.factor_over_z();
assert_eq!(r_new.factor_over_z().1, factors, "R(t) for {a:?} / {d:?}");
let d_gp = poly_to_genpoly_rf(d);
let b_gp = &poly_to_genpoly_rf(a) - &poly_to_genpoly_rf_times_t(&d_prime);
let prs_old = GenPoly::<RationalFn>::euclidean_prs_normalized(
&d_gp,
&b_gp,
None,
primitive_over_q_t,
);
for (q, i) in factors.iter().filter(|(q, _)| q.degree() >= Some(2)) {
let i = *i as usize;
if d.degree() == Some(i) {
continue;
}
let new = prs_new.get(&i).and_then(|s| reduce_log_arg(s, q));
let old = prs_old.get(&i).and_then(|s| reduce_log_arg(s, q));
assert!(
new.is_some(),
"no log argument of degree {i} for {a:?} / {d:?}"
);
assert_eq!(
new.as_ref().map(as_pairs),
old.as_ref().map(as_pairs),
"log argument of degree {i} for {a:?} / {d:?}"
);
}
}
}
}