use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::Zero;
use super::LogTerm;
use crate::poly::dense::Poly;
#[derive(Clone, Debug)]
pub struct LogPartResult {
pub terms: Vec<LogTerm>,
}
pub fn logarithmic_part(a: &Poly, d: &Poly) -> LogPartResult {
assert!(
!d.is_zero(),
"logarithmic_part: denominator must be nonzero"
);
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 = Poly::resultant_poly(d, a, &d_prime);
if r_poly.is_zero() {
return LogPartResult { terms: vec![] };
}
let (content, r_factors) = r_poly.factor_over_z();
let _ = content;
let mut terms: Vec<LogTerm> = Vec::new();
for (factor, multiplicity) in &r_factors {
let _ = multiplicity;
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 {
terms.push(LogTerm::Algebraic {
min_poly: factor.clone(),
});
}
}
LogPartResult { terms }
}
fn extract_linear_root(factor: &Poly) -> Ratio<BigInt> {
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_traits::Signed;
fn rat(n: i64, d: i64) -> Ratio<BigInt> {
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);
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);
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);
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);
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);
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);
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);
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<Ratio<BigInt>> =
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);
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);
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}");
}
}
#[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));
}
}