use std::collections::{HashMap, HashSet};
use rug::Rational;
use crate::deriv::log::{DerivationLog, DerivedExpr, RewriteStep};
use crate::integrate::engine::IntegrationError;
use crate::integrate::gate;
use crate::integrate::risch::poly_rde::{
degree, expr_to_qpoly, is_free_of_var, poly_mul, qpoly_to_expr, trim, QPoly,
};
use crate::integrate::risch::rational_rde::{expr_to_qrational, poly_div_exact, poly_gcd};
use crate::integrate::risch::tower::literal_integer;
use crate::kernel::subs::subs;
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use crate::simplify::engine::simplify;
use super::integral_basis::squarefree_factors;
const T_NAME: &str = "$gensubst_t$";
const MAX_ATTEMPTS: usize = 8;
pub(super) fn try_generator_substitution(
expr: ExprId,
var: ExprId,
pool: &ExprPool,
) -> Option<Result<DerivedExpr<ExprId>, IntegrationError>> {
let t = pool.symbol(T_NAME, Domain::Real);
if var == t {
return None; }
if !has_transcendental_radicand(expr, var, pool) {
return None;
}
let normalized = simplify(expand_tangents(expr, var, pool), pool).value;
let mut attempts = 0usize;
let mut last_err: Option<String> = None;
let mut best: Option<(usize, ExprId)> = None;
let full = in_domain_samples(expr, var, pool).len();
'search: for theta in collect_arguments(normalized, var, pool) {
let Some(a) = linear_slope(theta, var, pool) else {
continue;
};
for kind in kinds_for(normalized, theta, pool) {
if attempts >= MAX_ATTEMPTS {
break 'search;
}
let Some(red) = reduce(normalized, expr, var, theta, &a, kind, t, pool) else {
continue;
};
attempts += 1;
match integrate_reduced(&red, var, t, expr, pool) {
Ok((cand, real)) => {
if best.map_or(true, |(seen, _)| real > seen) {
best = Some((real, cand));
}
if real >= full {
break 'search; }
}
Err(msg) => last_err = Some(msg),
}
}
}
if let Some((_, antiderivative)) = best {
let mut log = DerivationLog::new();
log.push(RewriteStep::simple(
"generator_substitution",
expr,
antiderivative,
));
return Some(Ok(DerivedExpr {
value: antiderivative,
log,
}));
}
last_err.map(|msg| {
Err(IntegrationError::NotImplemented(format!(
"generator substitution t = g(x) reduced the integrand, but {msg}"
)))
})
}
fn in_domain_samples(expr: ExprId, var: ExprId, pool: &ExprPool) -> Vec<f64> {
super::subst::x_samples()
.into_iter()
.filter(|&x| gate::eval_at(expr, var, x, pool).is_some_and(f64::is_finite))
.collect()
}
fn integrate_reduced(
red: &Reduction,
var: ExprId,
t: ExprId,
original: ExprId,
pool: &ExprPool,
) -> Result<(ExprId, usize), String> {
let f_t = {
let _guard = crate::integrate::risch::rational_integrate::RootSumSuppressed::enter();
crate::integrate::integrate(red.integrand, t, pool)
.map_err(|e| match e {
IntegrationError::NonElementary(msg) => format!(
"the reduced integral ∫h(t) dt was reported non-elementary \
({msg}); that is a statement about t and is not imported"
),
other => format!("the reduced integral ∫h(t) dt declined: {other}"),
})?
.value
};
if contains_root_sum(f_t, pool) {
return Err("the reduced integral needs an unevaluable RootSum".to_string());
}
let back = |e: ExprId| -> ExprId {
let mut map = HashMap::new();
map.insert(t, red.generator);
simplify(subs(e, &map, pool), pool).value
};
let f_x = back(f_t);
let mut candidates = vec![f_x];
for fix in red.sign_fixes(pool) {
let fixed = simplify(pool.mul(vec![f_x, back(fix)]), pool).value;
candidates.push(fixed);
}
let samples = super::subst::x_samples();
let in_domain =
|x: f64| -> bool { gate::eval_at(original, var, x, pool).is_some_and(f64::is_finite) };
let domain = gate::Domain::from_samples(samples.clone())
.with_predicate(in_domain)
.with_boxes(super::subst::domain_boxes(&samples, &|x| {
gate::eval_at(original, var, x, pool).is_some_and(f64::is_finite)
}));
let target = gate::Target::symbolic(original);
let opts = super::subst::gate_options();
let live = in_domain_samples(original, var, pool);
let mut best: Option<(usize, ExprId)> = None;
for candidate in candidates {
if !gate::verify(candidate, &target, var, &domain, &opts, pool).is_verified() {
continue;
}
let real = live
.iter()
.filter(|&&x| gate::eval_at(candidate, var, x, pool).is_some_and(f64::is_finite))
.count();
if best.map_or(true, |(seen, _)| real > seen) {
best = Some((real, candidate));
}
}
best.ok_or_else(|| {
format!(
"no back-substituted candidate for t = {} passed the gate",
pool.display(red.generator)
)
})
.map(|(real, cand)| (cand, real))
}
fn contains_root_sum(expr: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::RootSum { .. } => true,
ExprData::Add(args) | ExprData::Mul(args) => {
args.iter().any(|&a| contains_root_sum(a, pool))
}
ExprData::Func { args, .. } => args.iter().any(|&a| contains_root_sum(a, pool)),
ExprData::Pow { base, exp } => {
contains_root_sum(base, pool) || contains_root_sum(exp, pool)
}
_ => false,
}
}
struct Reduction {
integrand: ExprId,
generator: ExprId,
witness: Option<ExprId>,
w: Option<ExprId>,
}
impl Reduction {
fn sign_fixes(&self, pool: &ExprPool) -> Vec<ExprId> {
let mut out = Vec::new();
if let Some(wit) = self.witness {
out.push(wit);
}
if let Some(w) = self.w {
let sq = pool.pow(w, pool.integer(2_i32));
let root = pool.func("sqrt", vec![sq]);
out.push(pool.mul(vec![w, pool.pow(root, pool.integer(-1_i32))]));
}
out
}
}
#[allow(clippy::too_many_arguments)]
fn reduce(
normalized: ExprId,
original: ExprId,
var: ExprId,
theta: ExprId,
a: &Rational,
kind: Kind,
t: ExprId,
pool: &ExprPool,
) -> Option<Reduction> {
let generator = kind.generator(theta, pool);
let gp = kind.derivative(theta, a, pool);
let normalized = normalize_exponential_arguments(normalized, var, theta, a, kind, pool);
let h_x = kind.divide_by_derivative(normalized, theta, a, pool);
let h_t = rewrite_in_t(h_x, var, theta, kind, t, pool)?;
let norm = normalize_radicals(h_t, t, pool)?;
let red = Reduction {
integrand: norm.integrand,
generator,
witness: norm.witness,
w: norm.w,
};
check_reduction(&red, original, var, t, gp, pool).then_some(red)
}
fn normalize_exponential_arguments(
expr: ExprId,
var: ExprId,
theta: ExprId,
a: &Rational,
kind: Kind,
pool: &ExprPool,
) -> ExprId {
if kind != Kind::Exp {
return expr;
}
let Some(tp) = expr_to_qpoly(theta, var, pool) else {
return expr;
};
let b = tp.first().cloned().unwrap_or_else(|| Rational::from(0));
rewrite_exponentials(expr, var, theta, a, &b, pool)
}
fn rewrite_exponentials(
expr: ExprId,
var: ExprId,
theta: ExprId,
a: &Rational,
b: &Rational,
pool: &ExprPool,
) -> ExprId {
match pool.get(expr) {
ExprData::Func { ref name, ref args } if name == "exp" && args.len() == 1 => {
let u = args[0];
if u == theta || is_free_of_var(u, var, pool) {
return expr;
}
let Some(up) = expr_to_qpoly(u, var, pool) else {
return expr;
};
if up.len() > 2 {
return expr;
}
let Some(c) = up.get(1) else {
return expr;
};
let k_q = c / a.clone();
if *k_q.denom() != 1 {
return expr;
}
let Ok(k) = i32::try_from(k_q.numer().clone()) else {
return expr;
};
let d = up.first().cloned().unwrap_or_else(|| Rational::from(0));
let shift = d - Rational::from(k) * b.clone();
let powered = pool.pow(pool.func("exp", vec![theta]), pool.integer(k));
if shift == 0 {
powered
} else {
let s = pool.rational(shift.numer().clone(), shift.denom().clone());
pool.mul(vec![pool.func("exp", vec![s]), powered])
}
}
ExprData::Func { ref name, ref args } => pool.func(
name.clone(),
args.iter()
.map(|&x| rewrite_exponentials(x, var, theta, a, b, pool))
.collect(),
),
ExprData::Add(args) => pool.add(
args.iter()
.map(|&x| rewrite_exponentials(x, var, theta, a, b, pool))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&x| rewrite_exponentials(x, var, theta, a, b, pool))
.collect(),
),
ExprData::Pow { base, exp } => pool.pow(
rewrite_exponentials(base, var, theta, a, b, pool),
rewrite_exponentials(exp, var, theta, a, b, pool),
),
_ => expr,
}
}
fn check_reduction(
red: &Reduction,
original: ExprId,
var: ExprId,
t: ExprId,
gp: ExprId,
pool: &ExprPool,
) -> bool {
let mut checked = 0usize;
for x in super::subst::x_samples() {
let (Some(f), Some(tv), Some(d)) = (
gate::eval_at(original, var, x, pool),
gate::eval_at(red.generator, var, x, pool),
gate::eval_at(gp, var, x, pool),
) else {
continue;
};
let Some(h) = gate::eval_at(red.integrand, t, tv, pool) else {
continue;
};
let rhs = h * d;
if !f.is_finite() || !rhs.is_finite() || f.abs() < 1e-9 {
continue;
}
if (f.abs() - rhs.abs()).abs() > 1e-6 * (1.0 + f.abs()) {
return false;
}
checked += 1;
}
checked >= 6
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Kind {
Tan,
Cot,
Sin,
Csc,
Cos,
Sec,
Tanh,
Coth,
Sinh,
Csch,
Cosh,
Sech,
Exp,
}
const TRIG_KINDS: [Kind; 6] = [
Kind::Tan,
Kind::Cot,
Kind::Cos,
Kind::Sin,
Kind::Sec,
Kind::Csc,
];
const HYP_KINDS: [Kind; 6] = [
Kind::Tanh,
Kind::Coth,
Kind::Cosh,
Kind::Sinh,
Kind::Sech,
Kind::Csch,
];
impl Kind {
fn generator(self, theta: ExprId, pool: &ExprPool) -> ExprId {
let f = |n: &str| pool.func(n, vec![theta]);
let inv = |e: ExprId| pool.pow(e, pool.integer(-1_i32));
match self {
Kind::Tan => f("tan"),
Kind::Cot => inv(f("tan")),
Kind::Sin => f("sin"),
Kind::Csc => inv(f("sin")),
Kind::Cos => f("cos"),
Kind::Sec => inv(f("cos")),
Kind::Tanh => f("tanh"),
Kind::Coth => inv(f("tanh")),
Kind::Sinh => f("sinh"),
Kind::Csch => inv(f("sinh")),
Kind::Cosh => f("cosh"),
Kind::Sech => inv(f("cosh")),
Kind::Exp => f("exp"),
}
}
fn derivative_factors(self) -> (i32, &'static [(&'static str, i32)]) {
match self {
Kind::Tan => (1, &[("cos", -2)]),
Kind::Cot => (-1, &[("sin", -2)]),
Kind::Sin => (1, &[("cos", 1)]),
Kind::Csc => (-1, &[("cos", 1), ("sin", -2)]),
Kind::Cos => (-1, &[("sin", 1)]),
Kind::Sec => (1, &[("sin", 1), ("cos", -2)]),
Kind::Tanh => (1, &[("cosh", -2)]),
Kind::Coth => (-1, &[("sinh", -2)]),
Kind::Sinh => (1, &[("cosh", 1)]),
Kind::Csch => (-1, &[("cosh", 1), ("sinh", -2)]),
Kind::Cosh => (1, &[("sinh", 1)]),
Kind::Sech => (-1, &[("sinh", 1), ("cosh", -2)]),
Kind::Exp => (1, &[("exp", 1)]),
}
}
fn derivative(self, theta: ExprId, a: &Rational, pool: &ExprPool) -> ExprId {
self.derivative_power(theta, a, 1, pool)
}
fn divide_by_derivative(
self,
f: ExprId,
theta: ExprId,
a: &Rational,
pool: &ExprPool,
) -> ExprId {
let inv = self.derivative_power(theta, a, -1, pool);
simplify(pool.mul(vec![f, inv]), pool).value
}
fn derivative_power(self, theta: ExprId, a: &Rational, s: i32, pool: &ExprPool) -> ExprId {
let (sign, factors) = self.derivative_factors();
let coeff = if s > 0 {
Rational::from(sign) * a.clone()
} else {
(Rational::from(sign) * a.clone()).recip()
};
let mut parts = vec![pool.rational(coeff.numer().clone(), coeff.denom().clone())];
for &(name, e) in factors {
let base = pool.func(name, vec![theta]);
parts.push(pool.pow(base, pool.integer(e * s)));
}
pool.mul(parts)
}
fn primary(self) -> &'static str {
match self {
Kind::Tan | Kind::Sin | Kind::Csc => "sin",
Kind::Cot | Kind::Cos | Kind::Sec => "cos",
Kind::Tanh | Kind::Sinh | Kind::Csch => "sinh",
Kind::Coth | Kind::Cosh | Kind::Sech => "cosh",
Kind::Exp => "exp",
}
}
fn primary_repl(self, theta: ExprId, t: ExprId, pool: &ExprPool) -> ExprId {
let f = |n: &str| pool.func(n, vec![theta]);
let inv = |e: ExprId| pool.pow(e, pool.integer(-1_i32));
match self {
Kind::Tan => pool.mul(vec![t, f("cos")]),
Kind::Cot => pool.mul(vec![t, f("sin")]),
Kind::Tanh => pool.mul(vec![t, f("cosh")]),
Kind::Coth => pool.mul(vec![t, f("sinh")]),
Kind::Sin | Kind::Cos | Kind::Sinh | Kind::Cosh | Kind::Exp => t,
Kind::Csc | Kind::Sec | Kind::Csch | Kind::Sech => inv(t),
}
}
fn secondary(self) -> Option<&'static str> {
match self {
Kind::Tan | Kind::Sin | Kind::Csc => Some("cos"),
Kind::Cot | Kind::Cos | Kind::Sec => Some("sin"),
Kind::Tanh | Kind::Sinh | Kind::Csch => Some("cosh"),
Kind::Coth | Kind::Cosh | Kind::Sech => Some("sinh"),
Kind::Exp => None,
}
}
fn secondary_sq(self, t: ExprId, pool: &ExprPool) -> Option<ExprId> {
let one = pool.integer(1_i32);
let t2 = pool.pow(t, pool.integer(2_i32));
let t_m2 = pool.pow(t, pool.integer(-2_i32));
let neg = |e: ExprId| pool.mul(vec![pool.integer(-1_i32), e]);
let inv = |e: ExprId| pool.pow(e, pool.integer(-1_i32));
Some(match self {
Kind::Tan | Kind::Cot => inv(pool.add(vec![one, t2])),
Kind::Sin | Kind::Cos => pool.add(vec![one, neg(t2)]),
Kind::Csc | Kind::Sec => pool.add(vec![one, neg(t_m2)]),
Kind::Tanh => inv(pool.add(vec![one, neg(t2)])),
Kind::Coth => inv(pool.add(vec![t2, neg(one)])),
Kind::Sinh => pool.add(vec![one, t2]),
Kind::Cosh => pool.add(vec![t2, neg(one)]),
Kind::Csch => pool.add(vec![one, t_m2]),
Kind::Sech => pool.add(vec![t_m2, neg(one)]),
Kind::Exp => return None,
})
}
}
fn kinds_for(expr: ExprId, theta: ExprId, pool: &ExprPool) -> Vec<Kind> {
let mut names = HashSet::new();
collect_func_names(expr, theta, pool, &mut names);
let mut out = Vec::new();
if names.contains("sin") || names.contains("cos") {
out.extend_from_slice(&TRIG_KINDS);
}
if names.contains("sinh") || names.contains("cosh") {
out.extend_from_slice(&HYP_KINDS);
}
if names.contains("exp") {
out.push(Kind::Exp);
}
out
}
fn collect_func_names(
expr: ExprId,
theta: ExprId,
pool: &ExprPool,
out: &mut HashSet<&'static str>,
) {
match pool.get(expr) {
ExprData::Func { ref name, ref args } => {
if args.len() == 1 && args[0] == theta {
for known in ["sin", "cos", "sinh", "cosh", "exp"] {
if name == known {
out.insert(known);
}
}
}
for &a in args.iter() {
collect_func_names(a, theta, pool, out);
}
}
ExprData::Add(args) | ExprData::Mul(args) => {
for &a in &args {
collect_func_names(a, theta, pool, out);
}
}
ExprData::Pow { base, exp } => {
collect_func_names(base, theta, pool, out);
collect_func_names(exp, theta, pool, out);
}
_ => {}
}
}
fn has_transcendental_radicand(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
match pool.get(expr) {
ExprData::Func { ref name, ref args }
if matches!(name.as_str(), "sqrt" | "cbrt") && args.len() == 1 =>
{
(!is_free_of_var(args[0], var, pool) && expr_to_qpoly(args[0], var, pool).is_none())
|| has_transcendental_radicand(args[0], var, pool)
}
ExprData::Func { ref args, .. } => args
.iter()
.any(|&a| has_transcendental_radicand(a, var, pool)),
ExprData::Pow { base, exp } => {
if let ExprData::Rational(r) = pool.get(exp) {
if *r.0.denom() != 1
&& !is_free_of_var(base, var, pool)
&& expr_to_qpoly(base, var, pool).is_none()
{
return true;
}
}
has_transcendental_radicand(base, var, pool)
}
ExprData::Add(args) | ExprData::Mul(args) => args
.iter()
.any(|&a| has_transcendental_radicand(a, var, pool)),
_ => false,
}
}
fn collect_arguments(expr: ExprId, var: ExprId, pool: &ExprPool) -> Vec<ExprId> {
let mut seen = Vec::new();
fn walk(expr: ExprId, var: ExprId, pool: &ExprPool, out: &mut Vec<ExprId>) {
match pool.get(expr) {
ExprData::Func { ref name, ref args } => {
if args.len() == 1
&& matches!(name.as_str(), "sin" | "cos" | "sinh" | "cosh" | "exp")
&& !is_free_of_var(args[0], var, pool)
&& !out.contains(&args[0])
{
out.push(args[0]);
}
for &a in args.iter() {
walk(a, var, pool, out);
}
}
ExprData::Add(args) | ExprData::Mul(args) => {
for &a in &args {
walk(a, var, pool, out);
}
}
ExprData::Pow { base, exp } => {
walk(base, var, pool, out);
walk(exp, var, pool, out);
}
_ => {}
}
}
walk(expr, var, pool, &mut seen);
seen
}
fn linear_slope(theta: ExprId, var: ExprId, pool: &ExprPool) -> Option<Rational> {
let p = expr_to_qpoly(theta, var, pool)?;
if p.len() > 2 {
return None;
}
let a = p.get(1)?.clone();
(a != 0).then_some(a)
}
fn expand_tangents(expr: ExprId, var: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Func { ref name, ref args } if args.len() == 1 => {
let inner = expand_tangents(args[0], var, pool);
let pair = match name.as_str() {
"tan" => Some(("sin", "cos")),
"tanh" => Some(("sinh", "cosh")),
_ => None,
};
match pair {
Some((num, den)) if !is_free_of_var(inner, var, pool) => {
let n = pool.func(num, vec![inner]);
let d = pool.func(den, vec![inner]);
pool.mul(vec![n, pool.pow(d, pool.integer(-1_i32))])
}
_ => pool.func(name.clone(), vec![inner]),
}
}
ExprData::Func { ref name, ref args } => pool.func(
name.clone(),
args.iter()
.map(|&a| expand_tangents(a, var, pool))
.collect(),
),
ExprData::Add(args) => pool.add(
args.iter()
.map(|&a| expand_tangents(a, var, pool))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&a| expand_tangents(a, var, pool))
.collect(),
),
ExprData::Pow { base, exp } => pool.pow(expand_tangents(base, var, pool), exp),
_ => expr,
}
}
fn rewrite_in_t(
h_x: ExprId,
var: ExprId,
theta: ExprId,
kind: Kind,
t: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
let prim = pool.func(kind.primary(), vec![theta]);
let mut map = HashMap::new();
map.insert(prim, kind.primary_repl(theta, t, pool));
let stage = simplify(subs(h_x, &map, pool), pool).value;
let out = replace_secondary(stage, var, theta, kind, t, pool)?;
let out = simplify(out, pool).value;
is_free_of_var(out, var, pool).then_some(out)
}
fn replace_secondary(
expr: ExprId,
var: ExprId,
theta: ExprId,
kind: Kind,
t: ExprId,
pool: &ExprPool,
) -> Option<ExprId> {
if is_free_of_var(expr, var, pool) {
return Some(expr);
}
if expr == var {
return None; }
let recur = |e: ExprId| replace_secondary(e, var, theta, kind, t, pool);
match pool.get(expr) {
ExprData::Add(args) => {
let mapped: Option<Vec<ExprId>> = args.iter().map(|&a| recur(a)).collect();
Some(pool.add(mapped?))
}
ExprData::Mul(args) => {
let mapped: Option<Vec<ExprId>> = args.iter().map(|&a| recur(a)).collect();
Some(pool.mul(mapped?))
}
ExprData::Pow { base, exp } => {
if !is_free_of_var(exp, var, pool) {
return None;
}
if is_secondary(base, kind, theta, pool) {
let n = literal_integer(exp, pool)?;
if n % 2 != 0 {
return None;
}
let sq = kind.secondary_sq(t, pool)?;
return Some(pool.pow(sq, pool.integer(i32::try_from(n / 2).ok()?)));
}
if let (ExprData::Mul(args), Some(n)) = (pool.get(base), literal_integer(exp, pool)) {
let k = i32::try_from(n).ok()?;
let mapped: Option<Vec<ExprId>> = args
.iter()
.map(|&a| recur(pool.pow(a, pool.integer(k))))
.collect();
return Some(pool.mul(mapped?));
}
Some(pool.pow(recur(base)?, exp))
}
ExprData::Func { ref name, ref args } => {
if args.len() == 1 && is_secondary(expr, kind, theta, pool) {
return None;
}
if !matches!(name.as_str(), "sqrt" | "cbrt" | "abs") {
return None;
}
let mapped: Option<Vec<ExprId>> = args.iter().map(|&a| recur(a)).collect();
Some(pool.func(name.clone(), mapped?))
}
_ => None,
}
}
fn is_secondary(expr: ExprId, kind: Kind, theta: ExprId, pool: &ExprPool) -> bool {
let Some(sec) = kind.secondary() else {
return false;
};
matches!(pool.get(expr), ExprData::Func { ref name, ref args }
if name == sec && args.len() == 1 && args[0] == theta)
}
struct Normalized {
integrand: ExprId,
witness: Option<ExprId>,
w: Option<ExprId>,
}
fn normalize_radicals(expr: ExprId, t: ExprId, pool: &ExprPool) -> Option<Normalized> {
let mut radicands = Vec::new();
collect_radicands(expr, t, pool, &mut radicands);
let Some(&a_expr) = radicands.first() else {
return Some(Normalized {
integrand: expr,
witness: None,
w: None,
});
};
if radicands.len() > 1 {
return None; }
let (num, den) = expr_to_qrational(a_expr, t, pool)?;
let (num, den) = {
let g = poly_gcd(&trim(num.clone()), &trim(den.clone()));
if degree(&g) >= 1 {
(poly_div_exact(&num, &g), poly_div_exact(&den, &g))
} else {
(trim(num), trim(den))
}
};
if num.is_empty() || den.is_empty() {
return None;
}
let s = trim(poly_mul(&num, &den));
if s.is_empty() {
return None;
}
let mut e_poly: QPoly = vec![Rational::from(1)];
let mut q_poly: QPoly = vec![Rational::from(1)];
for (j, f) in squarefree_factors(&s).iter().enumerate() {
let mult = j + 1;
for _ in 0..(mult / 2) {
e_poly = poly_mul(&e_poly, f);
}
if mult % 2 == 1 {
q_poly = poly_mul(&q_poly, f);
}
}
let built = poly_mul(&poly_mul(&e_poly, &e_poly), &q_poly);
let (Some(lc_s), Some(lc_b)) = (trim(s.clone()).last().cloned(), trim(built).last().cloned())
else {
return None;
};
if lc_b == 0 {
return None;
}
let c = lc_s / lc_b;
for coeff in q_poly.iter_mut() {
*coeff *= c.clone();
}
if trim(poly_mul(&poly_mul(&e_poly, &e_poly), &q_poly)) != trim(s) {
return None;
}
let trivial = degree(&e_poly) < 1
&& e_poly.first().is_some_and(|v| *v == 1)
&& degree(&den) < 1
&& den.first().is_some_and(|v| *v == 1);
if trivial {
return Some(Normalized {
integrand: expr,
witness: None,
w: None,
});
}
let e_expr = qpoly_to_expr(&e_poly, t, pool);
let q_expr = qpoly_to_expr(&q_poly, t, pool);
let d_expr = qpoly_to_expr(&den, t, pool);
let sqrt_q = pool.func("sqrt", vec![q_expr]);
let new_sqrt = pool.mul(vec![e_expr, sqrt_q, pool.pow(d_expr, pool.integer(-1_i32))]);
let integrand = simplify(replace_radical(expr, a_expr, new_sqrt, pool), pool).value;
let sqrt_a = pool.func("sqrt", vec![a_expr]);
let witness = simplify(
pool.mul(vec![new_sqrt, pool.pow(sqrt_a, pool.integer(-1_i32))]),
pool,
)
.value;
let w = simplify(
pool.mul(vec![
qpoly_to_expr(&e_poly, t, pool),
qpoly_to_expr(&den, t, pool),
]),
pool,
)
.value;
Some(Normalized {
integrand,
witness: Some(witness),
w: Some(w),
})
}
fn collect_radicands(expr: ExprId, t: ExprId, pool: &ExprPool, out: &mut Vec<ExprId>) {
match pool.get(expr) {
ExprData::Func { ref name, ref args } => {
if name == "sqrt"
&& args.len() == 1
&& !is_free_of_var(args[0], t, pool)
&& !out.contains(&args[0])
{
out.push(args[0]);
}
for &a in args.iter() {
collect_radicands(a, t, pool, out);
}
}
ExprData::Pow { base, exp } => {
if let ExprData::Rational(r) = pool.get(exp) {
if *r.0.denom() == 2 && !is_free_of_var(base, t, pool) && !out.contains(&base) {
out.push(base);
}
}
collect_radicands(base, t, pool, out);
}
ExprData::Add(args) | ExprData::Mul(args) => {
for &a in &args {
collect_radicands(a, t, pool, out);
}
}
_ => {}
}
}
fn replace_radical(expr: ExprId, a: ExprId, new_sqrt: ExprId, pool: &ExprPool) -> ExprId {
match pool.get(expr) {
ExprData::Func { ref name, ref args }
if name == "sqrt" && args.len() == 1 && args[0] == a =>
{
new_sqrt
}
ExprData::Func { ref name, ref args } => pool.func(
name.clone(),
args.iter()
.map(|&x| replace_radical(x, a, new_sqrt, pool))
.collect(),
),
ExprData::Pow { base, exp } if base == a => {
if let ExprData::Rational(r) = pool.get(exp) {
if *r.0.denom() == 2 {
if let Some(m) = r.0.numer().to_i32() {
return pool.pow(new_sqrt, pool.integer(m));
}
}
}
pool.pow(replace_radical(base, a, new_sqrt, pool), exp)
}
ExprData::Pow { base, exp } => pool.pow(replace_radical(base, a, new_sqrt, pool), exp),
ExprData::Add(args) => pool.add(
args.iter()
.map(|&x| replace_radical(x, a, new_sqrt, pool))
.collect(),
),
ExprData::Mul(args) => pool.mul(
args.iter()
.map(|&x| replace_radical(x, a, new_sqrt, pool))
.collect(),
),
_ => expr,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn setup() -> (ExprPool, ExprId) {
let pool = ExprPool::new();
let x = pool.symbol("x", Domain::Real);
(pool, x)
}
fn check_everywhere(
f: ExprId,
integrand: ExprId,
x: ExprId,
pool: &ExprPool,
) -> (usize, usize) {
let df = simplify(crate::diff::diff(f, x, pool).unwrap().value, pool).value;
let (mut checked, mut real) = (0usize, 0usize);
for k in -820..820 {
let xv = f64::from(k) * 0.0073;
let Some(rhs) = gate::eval_at(integrand, x, xv, pool).filter(|v| v.is_finite()) else {
continue;
};
if gate::eval_at(f, x, xv, pool).is_some_and(f64::is_finite) {
real += 1;
}
let Some(lhs) = gate::eval_at(df, x, xv, pool).filter(|v| v.is_finite()) else {
continue;
};
assert!(
(lhs - rhs).abs() < 1e-6 * (1.0 + rhs.abs()),
"d/dx F ≠ f at x = {xv}: {lhs} vs {rhs}"
);
checked += 1;
}
(checked, real)
}
fn tan(x: ExprId, pool: &ExprPool) -> ExprId {
pool.func("tan", vec![x])
}
fn sec_pow(x: ExprId, k: i32, pool: &ExprPool) -> ExprId {
pool.pow(pool.func("cos", vec![x]), pool.integer(-k))
}
fn sqrt(e: ExprId, pool: &ExprPool) -> ExprId {
pool.func("sqrt", vec![e])
}
fn inv(e: ExprId, pool: &ExprPool) -> ExprId {
pool.pow(e, pool.integer(-1_i32))
}
#[test]
fn charlwood_6_tan_times_sqrt_one_plus_tan_fourth() {
let (pool, x) = setup();
let t4 = pool.pow(tan(x, &pool), pool.integer(4_i32));
let e = pool.mul(vec![
tan(x, &pool),
sqrt(pool.add(vec![pool.integer(1_i32), t4]), &pool),
]);
let got = crate::integrate::integrate(e, x, &pool).expect("Charlwood #6 should close");
let (checked, real) = check_everywhere(got.value, e, x, &pool);
assert!(checked > 1500, "only {checked} points checked");
assert_eq!(
real, checked,
"the answer should be real on the whole domain"
);
}
#[test]
fn charlwood_43_tan_over_sqrt_one_plus_tan_fourth() {
let (pool, x) = setup();
let t4 = pool.pow(tan(x, &pool), pool.integer(4_i32));
let rad = sqrt(pool.add(vec![pool.integer(1_i32), t4]), &pool);
let e = pool.mul(vec![tan(x, &pool), inv(rad, &pool)]);
let got = crate::integrate::integrate(e, x, &pool).expect("Charlwood #43 should close");
let (checked, real) = check_everywhere(got.value, e, x, &pool);
assert!(checked > 1500, "only {checked} points checked");
assert_eq!(real, checked);
}
#[test]
fn charlwood_8_sqrt_of_tan_quadratic() {
let (pool, x) = setup();
let t = tan(x, &pool);
let rad = pool.add(vec![
pool.integer(2_i32),
pool.mul(vec![pool.integer(2_i32), t]),
pool.pow(t, pool.integer(2_i32)),
]);
let e = sqrt(rad, &pool);
let got = crate::integrate::integrate(e, x, &pool).expect("Charlwood #8 should close");
assert!(
!contains_root_sum(got.value, &pool),
"the answer must not contain a RootSum: {}",
pool.display(got.value)
);
let (checked, _real) = check_everywhere(got.value, e, x, &pool);
assert!(checked > 700, "only {checked} points checked");
}
#[test]
fn charlwood_7_tan_over_sqrt_one_plus_sec_cubed() {
let (pool, x) = setup();
let rad = sqrt(
pool.add(vec![pool.integer(1_i32), sec_pow(x, 3, &pool)]),
&pool,
);
let e = pool.mul(vec![tan(x, &pool), inv(rad, &pool)]);
let got = crate::integrate::integrate(e, x, &pool).expect("Charlwood #7 should close");
let (checked, real) = check_everywhere(got.value, e, x, &pool);
assert!(checked > 700, "only {checked} points checked");
assert_eq!(real, checked);
}
#[test]
fn charlwood_44_sin_over_sqrt_one_minus_sin_sixth() {
let (pool, x) = setup();
let s6 = pool.pow(pool.func("sin", vec![x]), pool.integer(6_i32));
let rad = sqrt(
pool.add(vec![
pool.integer(1_i32),
pool.mul(vec![pool.integer(-1_i32), s6]),
]),
&pool,
);
let e = pool.mul(vec![pool.func("sin", vec![x]), inv(rad, &pool)]);
let got = crate::integrate::integrate(e, x, &pool).expect("Charlwood #44 should close");
let (checked, real) = check_everywhere(got.value, e, x, &pool);
assert!(checked > 1500, "only {checked} points checked");
assert_eq!(
real, checked,
"the sign repair is what makes this real on cos x < 0 too"
);
}
#[test]
fn a_non_elementary_reduced_integral_declines_rather_than_certifying() {
let (pool, x) = setup();
let s4 = pool.pow(pool.func("sinh", vec![x]), pool.integer(4_i32));
let e = pool.mul(vec![
pool.func("sinh", vec![x]),
sqrt(pool.add(vec![pool.integer(1_i32), s4]), &pool),
]);
match crate::integrate::integrate(e, x, &pool) {
Ok(f) => {
check_everywhere(f.value, e, x, &pool);
}
Err(err) => assert!(
matches!(err, IntegrationError::NotImplemented(_)),
"a non-elementary verdict on ∫h dt must not become one on ∫f dx: {err}"
),
}
}
#[test]
fn charlwood_42_sec_over_sqrt_sec_fourth_minus_one() {
let (pool, x) = setup();
let rad = sqrt(
pool.add(vec![pool.integer(-1_i32), sec_pow(x, 4, &pool)]),
&pool,
);
let e = pool.mul(vec![sec_pow(x, 1, &pool), inv(rad, &pool)]);
let got = crate::integrate::integrate(e, x, &pool).expect("Charlwood #42 should close");
let (checked, _real) = check_everywhere(got.value, e, x, &pool);
assert!(checked > 1500, "only {checked} points checked");
}
#[test]
fn tanh_times_sqrt_one_plus_tanh_fourth() {
let (pool, x) = setup();
let t4 = pool.pow(pool.func("tanh", vec![x]), pool.integer(4_i32));
let e = pool.mul(vec![
pool.func("tanh", vec![x]),
sqrt(pool.add(vec![pool.integer(1_i32), t4]), &pool),
]);
let got = crate::integrate::integrate(e, x, &pool).expect("∫tanh x·√(1+tanh⁴x) dx");
let (checked, _) = check_everywhere(got.value, e, x, &pool);
assert!(checked > 1000, "only {checked} points checked");
}
#[test]
fn a_bare_x_outside_the_generator_is_refused() {
let (pool, x) = setup();
let e = pool.mul(vec![x, sqrt(tan(x, &pool), &pool)]);
let t = pool.symbol(T_NAME, Domain::Real);
assert!(
rewrite_in_t(e, x, x, Kind::Tan, t, &pool).is_none(),
"a bare x must not be rewritten in t"
);
}
#[test]
fn charlwood_5_declines_without_certifying_non_elementary() {
let (pool, x) = setup();
let c2 = pool.pow(pool.func("cos", vec![x]), pool.integer(2_i32));
let c4 = pool.pow(pool.func("cos", vec![x]), pool.integer(4_i32));
let rad = sqrt(pool.add(vec![pool.integer(1_i32), c2, c4]), &pool);
let e = pool.mul(vec![c2, inv(rad, &pool)]);
match crate::integrate::integrate(e, x, &pool) {
Ok(f) => {
check_everywhere(f.value, e, x, &pool);
}
Err(err) => assert!(
matches!(err, IntegrationError::NotImplemented(_)),
"must decline, never certify: {err}"
),
}
}
#[test]
fn charlwood_45_nested_radicals_declines_cleanly() {
let (pool, x) = setup();
let sec = sec_pow(x, 1, &pool);
let a = sqrt(pool.add(vec![pool.integer(1_i32), sec]), &pool);
let b = sqrt(pool.add(vec![pool.integer(-1_i32), sec]), &pool);
let e = sqrt(
pool.add(vec![a, pool.mul(vec![pool.integer(-1_i32), b])]),
&pool,
);
match crate::integrate::integrate(e, x, &pool) {
Ok(f) => {
check_everywhere(f.value, e, x, &pool);
}
Err(err) => assert!(matches!(err, IntegrationError::NotImplemented(_)), "{err}"),
}
}
#[test]
fn a_polynomial_radicand_is_not_this_routes_business() {
let (pool, x) = setup();
let x2 = pool.pow(x, pool.integer(2_i32));
let e = sqrt(pool.add(vec![pool.integer(1_i32), x2]), &pool);
assert!(!has_transcendental_radicand(e, x, &pool));
assert!(try_generator_substitution(e, x, &pool).is_none());
}
#[test]
fn the_sign_witness_evaluates_to_the_sign_it_claims() {
let pool = ExprPool::new();
let t = pool.symbol(T_NAME, Domain::Real);
let one = pool.integer(1_i32);
let t2 = pool.pow(t, pool.integer(2_i32));
let inner = pool.add(vec![one, pool.mul(vec![pool.integer(-1_i32), t2])]);
let cube = pool.pow(inner, pool.integer(3_i32));
let a = pool.add(vec![one, pool.mul(vec![pool.integer(-1_i32), cube])]);
let e = pool.pow(pool.func("sqrt", vec![a]), pool.integer(-1_i32));
let norm = normalize_radicals(e, t, &pool).expect("radicand is rational in t");
let witness = norm.witness.expect("a square factor was taken out");
for k in [-0.9_f64, -0.4, 0.3, 0.8] {
let w = gate::eval_at(witness, t, k, &pool).expect("witness evaluates");
assert!(
(w - k.signum()).abs() < 1e-9,
"witness at t={k} is {w}, not sign(t)"
);
let lhs = gate::eval_at(norm.integrand, t, k, &pool).unwrap() * w;
let rhs = gate::eval_at(e, t, k, &pool).unwrap();
assert!(
(lhs - rhs).abs() < 1e-9 * (1.0 + rhs.abs()),
"{lhs} vs {rhs}"
);
}
}
}