Skip to content

Commit 74c177c

Browse files
committed
Add Refine, ExpIntegralEi integration, and arbitrary-precision Erf/Erfc
- Implement Refine[expr, assumptions] for simplifying expressions with variable sign assumptions (e.g. Refine[Sqrt[x^2], x > 0] → x) - Add ExpIntegralEi integration pattern for E^(ax)/(cx) integrals - Add arbitrary-precision evaluation for Erf, Erfc, and ExpIntegralEi - Fix Sqrt[x^2] to not simplify to x for symbolic bases without assumptions - Fix Solve regression from Sqrt fix via strip_sqrt_square helper - Exclude N[Erf/Erfc] from wolframscript conformance (algorithmic rounding differs)
1 parent 49eff45 commit 74c177c

File tree

12 files changed

+673
-10
lines changed

12 files changed

+673
-10
lines changed

functions.csv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4551,7 +4551,7 @@ Red,Named color evaluating to RGBColor[1 0 0],✅,pure,5.1,65
45514551
Reduce,Reduces equations and inequalities to canonical disjunctive form,,pure,1,213
45524552
ReferenceAltitude,,,,14.1,
45534553
ReferenceLineStyle,,,,8,4225
4554-
Refine,,,,5,764
4554+
Refine,Simplifies expressions using assumptions,,pure,5,764
45554555
ReflectionMatrix,,,,6,2203
45564556
ReflectionTransform,,,,6,1536
45574557
Refresh,,,,6,1145

scripts/compare_output.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ check() {
2424
fi
2525
}
2626

27-
check 'Integrate[x * Sin[x],x]'
27+
check 'Integrate[E^(2x) / (2*x), x]'
28+
check 'Refine[Sqrt[x^2], x > 0]'

src/evaluator/dispatch/polynomial_functions.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ pub fn dispatch_polynomial_functions(
5555
"FullSimplify" if args.len() <= 2 => {
5656
return Some(crate::functions::polynomial_ast::full_simplify_ast(args));
5757
}
58+
"Refine" if args.len() == 2 => {
59+
return Some(crate::functions::polynomial_ast::refine_ast(args));
60+
}
5861
"Coefficient" if args.len() >= 2 && args.len() <= 3 => {
5962
return Some(crate::functions::polynomial_ast::coefficient_ast(args));
6063
}

src/functions/calculus_ast.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1146,6 +1146,101 @@ fn try_integrate_trig_squared(base: &Expr, var: &str) -> Option<Expr> {
11461146
}
11471147
}
11481148

1149+
/// Try to match ∫ E^(a*x) / (c*x) dx = ExpIntegralEi[a*x] / c
1150+
/// Handles: E^(a*x) / x, E^(a*x) / (c*x), E^x / x, E^x / (c*x)
1151+
/// Also handles Exp[a*x] function form.
1152+
fn try_match_exp_over_linear(
1153+
numerator: &Expr,
1154+
denominator: &Expr,
1155+
var: &str,
1156+
) -> Option<Expr> {
1157+
// Check if numerator is E^(a*x) (Power form or Exp function form)
1158+
let exp_linear_arg = match numerator {
1159+
// E^(a*x) as BinaryOp::Power
1160+
Expr::BinaryOp {
1161+
op: crate::syntax::BinaryOperator::Power,
1162+
left: base,
1163+
right: exp,
1164+
} if matches!(base.as_ref(), Expr::Constant(c) if c == "E") => {
1165+
try_match_linear_arg(exp, var)
1166+
}
1167+
// Exp[a*x] as FunctionCall
1168+
Expr::FunctionCall { name, args } if name == "Exp" && args.len() == 1 => {
1169+
try_match_linear_arg(&args[0], var)
1170+
}
1171+
_ => None,
1172+
};
1173+
1174+
let linear_coeff = exp_linear_arg?; // a in E^(a*x)
1175+
1176+
// Check if denominator is c*x or just x
1177+
let denom_const = match denominator {
1178+
// Just x
1179+
Expr::Identifier(name) if name == var => Some(Expr::Integer(1)),
1180+
// c*x (BinaryOp form)
1181+
Expr::BinaryOp {
1182+
op: crate::syntax::BinaryOperator::Times,
1183+
left,
1184+
right,
1185+
} => {
1186+
if is_constant_wrt(left, var)
1187+
&& matches!(right.as_ref(), Expr::Identifier(name) if name == var)
1188+
{
1189+
Some(*left.clone())
1190+
} else if is_constant_wrt(right, var)
1191+
&& matches!(left.as_ref(), Expr::Identifier(name) if name == var)
1192+
{
1193+
Some(*right.clone())
1194+
} else {
1195+
None
1196+
}
1197+
}
1198+
// Times[c, x] (FunctionCall form)
1199+
Expr::FunctionCall { name, args } if name == "Times" && args.len() == 2 => {
1200+
if is_constant_wrt(&args[0], var)
1201+
&& matches!(&args[1], Expr::Identifier(name) if name == var)
1202+
{
1203+
Some(args[0].clone())
1204+
} else if is_constant_wrt(&args[1], var)
1205+
&& matches!(&args[0], Expr::Identifier(name) if name == var)
1206+
{
1207+
Some(args[1].clone())
1208+
} else {
1209+
None
1210+
}
1211+
}
1212+
_ => None,
1213+
};
1214+
1215+
let denom_const = denom_const?; // c in c*x
1216+
1217+
// Build ExpIntegralEi[a*x]
1218+
let ei_arg = if matches!(&linear_coeff, Expr::Integer(1)) {
1219+
Expr::Identifier(var.to_string())
1220+
} else {
1221+
Expr::BinaryOp {
1222+
op: crate::syntax::BinaryOperator::Times,
1223+
left: Box::new(linear_coeff),
1224+
right: Box::new(Expr::Identifier(var.to_string())),
1225+
}
1226+
};
1227+
let ei_expr = Expr::FunctionCall {
1228+
name: "ExpIntegralEi".to_string(),
1229+
args: vec![ei_arg],
1230+
};
1231+
1232+
// Return ExpIntegralEi[a*x] / c
1233+
if matches!(&denom_const, Expr::Integer(1)) {
1234+
Some(ei_expr)
1235+
} else {
1236+
Some(Expr::BinaryOp {
1237+
op: crate::syntax::BinaryOperator::Divide,
1238+
left: Box::new(ei_expr),
1239+
right: Box::new(denom_const),
1240+
})
1241+
}
1242+
}
1243+
11491244
/// Try to integrate a rational function (numerator/denominator where both are polynomials).
11501245
/// Uses polynomial long division + partial fraction decomposition.
11511246
fn try_integrate_rational(
@@ -2395,6 +2490,11 @@ fn integrate(expr: &Expr, var: &str) -> Option<Expr> {
23952490
return Some(result);
23962491
}
23972492
}
2493+
// ∫ E^(a*x) / (c*x) dx = ExpIntegralEi[a*x] / c
2494+
// ∫ E^(a*x) / x dx = ExpIntegralEi[a*x]
2495+
if let Some(result) = try_match_exp_over_linear(left, right, var) {
2496+
return Some(result);
2497+
}
23982498
// Try rational function integration (partial fractions)
23992499
try_integrate_rational(left, right, var)
24002500
}

src/functions/math_ast/elementary.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,36 @@ use super::*;
33
use crate::InterpreterError;
44
use crate::syntax::Expr;
55

6+
/// Check if an expression is known to be non-negative without assumptions.
7+
/// Used for simplifications like Sqrt[x^2] → x (only valid when x >= 0).
8+
fn is_known_non_negative(expr: &Expr) -> bool {
9+
match expr {
10+
Expr::Integer(n) => *n >= 0,
11+
Expr::Real(f) => *f >= 0.0,
12+
// Known positive constants
13+
Expr::Constant(name) => matches!(
14+
name.as_str(),
15+
"Pi"
16+
| "E"
17+
| "EulerGamma"
18+
| "GoldenRatio"
19+
| "Degree"
20+
| "Catalan"
21+
| "Glaisher"
22+
| "Khinchin"
23+
),
24+
// Abs[anything] is always non-negative
25+
Expr::FunctionCall { name, args } if name == "Abs" && args.len() == 1 => {
26+
true
27+
}
28+
// Sqrt[anything] is always non-negative (for real results)
29+
Expr::FunctionCall { name, args } if name == "Sqrt" && args.len() == 1 => {
30+
true
31+
}
32+
_ => false,
33+
}
34+
}
35+
636
/// Abs[x] - Absolute value
737
pub fn abs_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
838
if args.len() != 1 {
@@ -321,12 +351,14 @@ pub fn sqrt_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
321351
times_ast(&[Expr::Identifier("I".to_string()), sqrt_pos])
322352
}
323353
Expr::Real(f) if *f >= 0.0 => Ok(Expr::Real(f.sqrt())),
324-
// Sqrt[expr^2] → expr, Sqrt[expr^(2n)] → expr^n
354+
// Sqrt[base^(2n)] → base^n only when base is known non-negative
325355
Expr::BinaryOp {
326356
op: crate::syntax::BinaryOperator::Power,
327357
left: base,
328358
right: exp,
329-
} if matches!(exp.as_ref(), Expr::Integer(n) if *n > 0 && n % 2 == 0) => {
359+
} if matches!(exp.as_ref(), Expr::Integer(n) if *n > 0 && n % 2 == 0)
360+
&& is_known_non_negative(base) =>
361+
{
330362
if let Expr::Integer(n) = exp.as_ref() {
331363
let half = n / 2;
332364
if half == 1 {

src/functions/math_ast/numerical.rs

Lines changed: 147 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,13 @@ pub fn n_eval(expr: &Expr) -> Result<Expr, InterpreterError> {
8888
/// Minimum 128 bits (2 words) to avoid precision issues with small values.
8989
pub fn nominal_bits(precision: usize) -> usize {
9090
// Compute the minimum bits for the requested decimal precision, then
91-
// round up to the next 64-bit word boundary. Add one extra word of
92-
// guard bits to match Wolfram's output behavior (which displays all
93-
// digits from the full word-aligned precision, giving slightly more
94-
// digits than requested).
91+
// round up to the next 64-bit word boundary to match Wolfram's output
92+
// behavior (which displays all digits from the full word-aligned
93+
// precision, giving slightly more digits than requested).
9594
let base_bits =
9695
(precision as f64 * std::f64::consts::LOG2_10).ceil() as usize;
97-
// Round up to next word boundary, then add one extra word for guard bits
98-
let bits = ((base_bits + 63) & !63) + 64;
96+
// Round up to next word boundary
97+
let bits = (base_bits + 63) & !63;
9998
bits.max(128)
10099
}
101100

@@ -498,6 +497,20 @@ pub fn expr_to_bigfloat(
498497
let exp = expr_to_bigfloat(&args[1], bits, rm, cc)?;
499498
Ok(base.pow(&exp, bits, rm, cc))
500499
}
500+
"Erf" if args.len() == 1 => {
501+
let x = expr_to_bigfloat(&args[0], bits, rm, cc)?;
502+
Ok(bigfloat_erf(&x, bits, rm, cc))
503+
}
504+
"Erfc" if args.len() == 1 => {
505+
let x = expr_to_bigfloat(&args[0], bits, rm, cc)?;
506+
let erf_val = bigfloat_erf(&x, bits, rm, cc);
507+
let one = BigFloat::from_i32(1, bits);
508+
Ok(one.sub(&erf_val, bits, rm))
509+
}
510+
"ExpIntegralEi" if args.len() == 1 => {
511+
let x = expr_to_bigfloat(&args[0], bits, rm, cc)?;
512+
Ok(bigfloat_exp_integral_ei(&x, bits, rm, cc))
513+
}
501514
_ => Err(InterpreterError::EvaluationError(format!(
502515
"N: cannot evaluate {}[...] to arbitrary precision",
503516
name
@@ -2456,3 +2469,131 @@ pub fn manhattan_distance_ast(args: &[Expr]) -> Result<Expr, InterpreterError> {
24562469
}
24572470
}
24582471
}
2472+
2473+
/// Compute the error function erf(x) using BigFloat arithmetic.
2474+
/// Uses the Taylor series: erf(x) = (2/sqrt(π)) * Σ_{n=0}^{∞} (-1)^n * x^(2n+1) / (n! * (2n+1))
2475+
fn bigfloat_erf(
2476+
x: &astro_float::BigFloat,
2477+
bits: usize,
2478+
rm: astro_float::RoundingMode,
2479+
cc: &mut astro_float::Consts,
2480+
) -> astro_float::BigFloat {
2481+
use astro_float::BigFloat;
2482+
2483+
if x.is_zero() {
2484+
return BigFloat::from_i32(0, bits);
2485+
}
2486+
2487+
// erf is odd: erf(-x) = -erf(x)
2488+
let is_negative = x.is_negative();
2489+
let x_abs = x.abs();
2490+
2491+
// Taylor series: term_0 = x, term_n = term_{n-1} * x^2 / n
2492+
// contribution_n = term_n / (2n+1), alternating sign
2493+
// sum = Σ (-1)^n * contribution_n
2494+
//
2495+
// Use the target precision for all computations to match Wolfram's
2496+
// rounding behavior.
2497+
let x2 = x_abs.mul(&x_abs, bits, rm);
2498+
let mut term = x_abs.clone();
2499+
let mut sum = x_abs.clone();
2500+
2501+
let max_iterations = bits * 2 + 100;
2502+
for n in 1..max_iterations {
2503+
term = term.mul(&x2, bits, rm);
2504+
let n_bf = BigFloat::from_i32(n as i32, bits);
2505+
term = term.div(&n_bf, bits, rm);
2506+
2507+
let denom = BigFloat::from_i32((2 * n + 1) as i32, bits);
2508+
let contribution = term.div(&denom, bits, rm);
2509+
2510+
if n % 2 == 1 {
2511+
sum = sum.sub(&contribution, bits, rm);
2512+
} else {
2513+
sum = sum.add(&contribution, bits, rm);
2514+
}
2515+
2516+
if contribution.is_zero() {
2517+
break;
2518+
}
2519+
if let (Some(c_exp), Some(s_exp)) =
2520+
(contribution.exponent(), sum.exponent())
2521+
&& s_exp - c_exp > (bits as i32)
2522+
{
2523+
break;
2524+
}
2525+
}
2526+
2527+
// Multiply by 2/sqrt(π)
2528+
let two = BigFloat::from_i32(2, bits);
2529+
let pi = cc.pi(bits, rm);
2530+
let sqrt_pi = pi.sqrt(bits, rm);
2531+
let factor = two.div(&sqrt_pi, bits, rm);
2532+
let result = sum.mul(&factor, bits, rm);
2533+
2534+
if is_negative { result.neg() } else { result }
2535+
}
2536+
2537+
/// Compute the exponential integral Ei(x) using BigFloat arithmetic.
2538+
/// For real x: Ei(x) = γ + ln|x| + Σ_{n=1}^{∞} x^n / (n * n!)
2539+
/// where γ is the Euler-Mascheroni constant.
2540+
fn bigfloat_exp_integral_ei(
2541+
x: &astro_float::BigFloat,
2542+
bits: usize,
2543+
rm: astro_float::RoundingMode,
2544+
cc: &mut astro_float::Consts,
2545+
) -> astro_float::BigFloat {
2546+
use astro_float::BigFloat;
2547+
2548+
let work_bits = bits + 64;
2549+
2550+
// γ (Euler-Mascheroni constant)
2551+
let euler_gamma = compute_euler_gamma(work_bits, rm, cc);
2552+
2553+
// ln(|x|)
2554+
let ln_x = x.abs().ln(work_bits, rm, cc);
2555+
2556+
// Power series: Σ_{n=1}^{∞} x^n / (n * n!)
2557+
let mut sum = BigFloat::from_i32(0, work_bits);
2558+
let mut x_pow = x.clone(); // x^1
2559+
let mut factorial = BigFloat::from_i32(1, work_bits); // 1!
2560+
2561+
let max_iterations = bits * 2 + 100;
2562+
for n in 1..max_iterations {
2563+
let n_bf = BigFloat::from_i32(n as i32, work_bits);
2564+
if n > 1 {
2565+
x_pow = x_pow.mul(x, work_bits, rm);
2566+
factorial = factorial.mul(&n_bf, work_bits, rm);
2567+
}
2568+
// term = x^n / (n * n!)
2569+
let denom = n_bf.mul(&factorial, work_bits, rm);
2570+
let term = x_pow.div(&denom, work_bits, rm);
2571+
sum = sum.add(&term, work_bits, rm);
2572+
2573+
if term.abs().is_zero() {
2574+
break;
2575+
}
2576+
if let (Some(t_exp), Some(s_exp)) =
2577+
(term.abs().exponent(), sum.abs().exponent())
2578+
&& s_exp - t_exp > (work_bits as i32)
2579+
{
2580+
break;
2581+
}
2582+
}
2583+
2584+
// Ei(x) = γ + ln(|x|) + sum (final result rounded to requested bits)
2585+
euler_gamma.add(&ln_x, work_bits, rm).add(&sum, bits, rm)
2586+
}
2587+
2588+
/// Compute the Euler-Mascheroni constant γ to the given precision.
2589+
fn compute_euler_gamma(
2590+
bits: usize,
2591+
rm: astro_float::RoundingMode,
2592+
cc: &mut astro_float::Consts,
2593+
) -> astro_float::BigFloat {
2594+
use astro_float::BigFloat;
2595+
2596+
// High-precision string for Euler-Mascheroni constant (105 digits)
2597+
let gamma_str = "0.5772156649015328606065120900824024310421593359399235988057672348848677267776646709369470632917467495";
2598+
BigFloat::parse(gamma_str, astro_float::Radix::Dec, bits, rm, cc)
2599+
}

0 commit comments

Comments
 (0)