1use crate::calculus::series::{local_expansion, LocalExpansion};
43use crate::diff::DiffError;
44use crate::jit::eval_interp;
45use crate::kernel::{subs, Domain, ExprData, ExprId, ExprPool};
46use crate::simplify::simplify;
47use std::collections::HashMap;
48use std::fmt;
49
50#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
56pub struct AsymptoticTerm {
57 pub expr: ExprId,
59}
60
61impl AsymptoticTerm {
62 pub fn expr(self) -> ExprId {
64 self.expr
65 }
66}
67
68#[derive(Clone, Debug)]
71pub struct AsymptoticExpansion {
72 pub terms: Vec<AsymptoticTerm>,
74}
75
76impl AsymptoticExpansion {
77 pub fn term_exprs(&self) -> Vec<ExprId> {
79 self.terms.iter().map(|t| t.expr).collect()
80 }
81
82 pub fn partial_sum(&self, pool: &ExprPool) -> ExprId {
85 if self.terms.is_empty() {
86 return pool.integer(0_i32);
87 }
88 let xs: Vec<ExprId> = self.terms.iter().map(|t| t.expr).collect();
89 simplify(pool.add(xs), pool).value
90 }
91}
92
93#[derive(Debug)]
95pub enum AsymptoticError {
96 InvalidTermCount,
98 SeriesFailed,
101 Diff(DiffError),
103 GateFailed,
106 UnsupportedScale,
108}
109
110impl fmt::Display for AsymptoticError {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 match self {
113 AsymptoticError::InvalidTermCount => write!(f, "n_terms must be >= 1"),
114 AsymptoticError::SeriesFailed => {
115 write!(f, "could not form a series of f(1/t) at t -> 0")
116 }
117 AsymptoticError::Diff(e) => write!(f, "{e}"),
118 AsymptoticError::GateFailed => {
119 write!(f, "numeric o()-gate rejected the asymptotic expansion")
120 }
121 AsymptoticError::UnsupportedScale => {
122 write!(f, "asymptotic scale not supported by current rules")
123 }
124 }
125 }
126}
127
128impl std::error::Error for AsymptoticError {
129 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
130 match self {
131 AsymptoticError::Diff(e) => Some(e),
132 _ => None,
133 }
134 }
135}
136
137impl crate::errors::AlkahestError for AsymptoticError {
138 fn code(&self) -> &'static str {
139 match self {
140 AsymptoticError::InvalidTermCount => "E-ASYMPT-001",
141 AsymptoticError::SeriesFailed => "E-ASYMPT-002",
142 AsymptoticError::Diff(_) => "E-ASYMPT-003",
143 AsymptoticError::GateFailed => "E-ASYMPT-004",
144 AsymptoticError::UnsupportedScale => "E-ASYMPT-005",
145 }
146 }
147
148 fn remediation(&self) -> Option<&'static str> {
149 Some(match self {
150 AsymptoticError::InvalidTermCount => "pass n_terms >= 1",
151 AsymptoticError::SeriesFailed => {
152 "the function may not be analytic/Laurent-expandable at infinity; \
153 try a simpler form or fewer terms"
154 }
155 AsymptoticError::Diff(_) => {
156 "ensure all functions are registered primitives with differentiation rules"
157 }
158 AsymptoticError::GateFailed => {
159 "the expansion could not be numerically verified at large x; \
160 the function may have an oscillatory or non-power-scale tail"
161 }
162 AsymptoticError::UnsupportedScale => {
163 "exp/log scale hierarchies and Gamma/Stirling asymptotics are out of scope for \
164 asymptotic_expand; power-scale (rational/algebraic) and single log/exp peels \
165 are supported. For the asymptotics of a *sum* use \
166 experimental.euler_maclaurin, which also reaches Stirling via the sum of log k"
167 }
168 })
169 }
170}
171
172impl From<DiffError> for AsymptoticError {
173 fn from(e: DiffError) -> Self {
174 AsymptoticError::Diff(e)
175 }
176}
177
178pub fn asymptotic_expand(
188 f: ExprId,
189 var: ExprId,
190 n_terms: usize,
191 pool: &ExprPool,
192) -> Result<AsymptoticExpansion, AsymptoticError> {
193 if n_terms == 0 {
194 return Err(AsymptoticError::InvalidTermCount);
195 }
196
197 let f = simplify(f, pool).value;
198
199 if let Ok(exp) = power_scale_expand(f, var, n_terms, pool) {
201 if !exp.terms.is_empty() {
202 return Ok(exp);
203 }
204 }
205
206 if let Some(exp) = try_log_peel(f, var, n_terms, pool)? {
208 if !exp.terms.is_empty() {
209 return Ok(exp);
210 }
211 }
212
213 Err(AsymptoticError::UnsupportedScale)
214}
215
216fn power_scale_terms_raw(
226 f: ExprId,
227 var: ExprId,
228 order: u32,
229 pool: &ExprPool,
230) -> Result<Vec<(i64, ExprId)>, AsymptoticError> {
231 let t = pool.symbol("__asy_t", Domain::Positive);
232 let inv_t = pool.pow(t, pool.integer(-1_i32));
233 let mut m = HashMap::new();
234 m.insert(var, inv_t);
235 let f_of_t = simplify(subs(f, &m, pool), pool).value;
236
237 let (val, analytic) =
243 regularize_at_zero(f_of_t, t, pool).ok_or(AsymptoticError::SeriesFailed)?;
244
245 let LocalExpansion {
246 valuation: tay_val,
247 coeffs,
248 ..
249 } = local_expansion(analytic, t, pool.integer(0_i32), order, pool)
250 .map_err(|_| AsymptoticError::SeriesFailed)?;
251
252 let mut out: Vec<(i64, ExprId)> = Vec::new();
256 for (i, &c) in coeffs.iter().enumerate() {
257 let c = fold_constant(c, pool);
260 if is_numeric_zero(c, pool) {
261 continue;
262 }
263 let t_pow = val + tay_val as i64 + i as i64;
264 let x_pow = -t_pow;
265 let term = make_power_term(c, var, x_pow, pool);
266 out.push((x_pow, term));
267 }
268 out.sort_by_key(|p| std::cmp::Reverse(p.0));
270 Ok(out)
271}
272
273pub(crate) fn regularize_at_zero(g: ExprId, t: ExprId, pool: &ExprPool) -> Option<(i64, ExprId)> {
283 let g = simplify(g, pool).value;
284 match pool.get(g) {
285 ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => Some((0, g)),
287 ExprData::Symbol { .. } => {
288 if g == t {
289 Some((1, pool.integer(1_i32)))
290 } else {
291 Some((0, g))
292 }
293 }
294 ExprData::Mul(xs) => {
295 let mut val = 0i64;
296 let mut parts: Vec<ExprId> = Vec::new();
297 for x in xs {
298 let (v, u) = regularize_at_zero(x, t, pool)?;
299 val += v;
300 parts.push(u);
301 }
302 Some((val, simplify(pool.mul(parts), pool).value))
303 }
304 ExprData::Pow { base, exp } => {
305 let (vb, ub) = regularize_at_zero(base, t, pool)?;
307 let exp_q = rational_value(exp, pool)?;
308 let num = exp_q.numer().clone();
310 let den = exp_q.denom().clone();
311 let prod = rug::Integer::from(vb) * #
312 if den == 0 {
314 return None;
315 }
316 let (q, r) = prod.div_rem(den.clone());
317 if r != 0 {
318 return None; }
320 let val = q.to_i64()?;
321 let analytic = simplify(pool.pow(ub, exp), pool).value;
323 Some((val, analytic))
324 }
325 ExprData::Add(xs) => {
326 let mut pieces: Vec<(i64, ExprId)> = Vec::with_capacity(xs.len());
328 let mut vmin = i64::MAX;
329 for x in &xs {
330 let (v, u) = regularize_at_zero(*x, t, pool)?;
331 vmin = vmin.min(v);
332 pieces.push((v, u));
333 }
334 if vmin == i64::MAX {
335 return None;
336 }
337 let mut summands: Vec<ExprId> = Vec::with_capacity(pieces.len());
338 for (v, u) in pieces {
339 let shift = v - vmin; let term = if shift == 0 {
341 u
342 } else {
343 simplify(pool.mul(vec![pool.pow(t, pool.integer(shift)), u]), pool).value
344 };
345 summands.push(term);
346 }
347 let analytic = simplify(pool.add(summands), pool).value;
348 Some((vmin, analytic))
351 }
352 ExprData::Func { name, args } if args.len() == 1 => {
353 let (va, ua) = regularize_at_zero(args[0], t, pool)?;
354 match name.as_str() {
355 "sqrt" => {
358 if va % 2 != 0 {
359 return None;
360 }
361 let analytic = simplify(pool.func("sqrt", vec![ua]), pool).value;
365 Some((va / 2, analytic))
366 }
367 "sin" | "cos" | "tan" | "exp" | "cosh" | "sinh" | "tanh" | "gamma" => {
373 if va < 0 {
374 return None;
375 }
376 let full_arg = if va == 0 {
378 ua
379 } else {
380 simplify(pool.mul(vec![pool.pow(t, pool.integer(va)), ua]), pool).value
381 };
382 let analytic = simplify(pool.func(name, vec![full_arg]), pool).value;
383 Some((0, analytic))
384 }
385 "log" => {
389 if va != 0 {
390 return None;
391 }
392 let at0 = eval_at_zero(ua, t, pool)?;
393 if at0 == 0.0 {
394 return None;
395 }
396 let analytic = simplify(pool.func("log", vec![ua]), pool).value;
397 Some((0, analytic))
398 }
399 _ => None,
400 }
401 }
402 _ => None,
403 }
404}
405
406fn fold_constant(e: ExprId, pool: &ExprPool) -> ExprId {
409 let e = simplify(e, pool).value;
410 match pool.get(e) {
411 ExprData::Add(xs) => {
412 let ys: Vec<ExprId> = xs.iter().map(|x| fold_constant(*x, pool)).collect();
413 simplify(pool.add(ys), pool).value
414 }
415 ExprData::Mul(xs) => {
416 let ys: Vec<ExprId> = xs.iter().map(|x| fold_constant(*x, pool)).collect();
417 simplify(pool.mul(ys), pool).value
418 }
419 ExprData::Pow { base, exp } => {
420 let b = fold_constant(base, pool);
421 let x = fold_constant(exp, pool);
422 simplify(pool.pow(b, x), pool).value
423 }
424 ExprData::Func { name, args } if args.len() == 1 => {
425 let inner = fold_constant(args[0], pool);
426 if matches!(pool.get(inner), ExprData::Integer(n) if n.0 == 0) {
427 match name.as_str() {
428 "sin" | "tan" | "sinh" | "tanh" => return pool.integer(0_i32),
429 "cos" | "cosh" | "exp" => return pool.integer(1_i32),
430 _ => {}
431 }
432 }
433 simplify(pool.func(name, vec![inner]), pool).value
434 }
435 _ => e,
436 }
437}
438
439fn is_numeric_zero(e: ExprId, pool: &ExprPool) -> bool {
442 if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 0) {
443 return true;
444 }
445 if let ExprData::Rational(r) = pool.get(e) {
446 return r.0 == 0;
447 }
448 match eval_interp(e, &HashMap::new(), pool) {
449 Some(v) => v == 0.0,
450 None => false,
451 }
452}
453
454fn eval_at_zero(e: ExprId, t: ExprId, pool: &ExprPool) -> Option<f64> {
457 let mut env = HashMap::new();
458 env.insert(t, 1.0e-6f64);
459 eval_interp(e, &env, pool).filter(|v| v.is_finite())
460}
461
462fn rational_value(e: ExprId, pool: &ExprPool) -> Option<rug::Rational> {
464 match pool.get(e) {
465 ExprData::Integer(n) => Some(rug::Rational::from((n.0.clone(), rug::Integer::from(1)))),
466 ExprData::Rational(r) => Some(r.0.clone()),
467 _ => None,
468 }
469}
470
471fn make_power_term(coeff: ExprId, var: ExprId, x_pow: i64, pool: &ExprPool) -> ExprId {
472 let pow = if x_pow == 0 {
473 pool.integer(1_i32)
474 } else if x_pow == 1 {
475 var
476 } else {
477 pool.pow(var, pool.integer(x_pow))
478 };
479 simplify(pool.mul(vec![coeff, pow]), pool).value
480}
481
482fn power_scale_expand(
483 f: ExprId,
484 var: ExprId,
485 n_terms: usize,
486 pool: &ExprPool,
487) -> Result<AsymptoticExpansion, AsymptoticError> {
488 let order = (n_terms as u32).saturating_mul(2).saturating_add(8).min(40);
490 let raw = power_scale_terms_raw(f, var, order, pool)?;
491 if raw.is_empty() {
492 return Err(AsymptoticError::SeriesFailed);
493 }
494 let candidate: Vec<ExprId> = raw.iter().map(|(_, e)| *e).take(n_terms).collect();
495 let gated = gate_terms(f, var, &candidate, pool);
496 if gated.is_empty() {
497 return Err(AsymptoticError::GateFailed);
498 }
499 Ok(AsymptoticExpansion {
500 terms: gated
501 .into_iter()
502 .map(|expr| AsymptoticTerm { expr })
503 .collect(),
504 })
505}
506
507fn try_log_peel(
521 f: ExprId,
522 var: ExprId,
523 n_terms: usize,
524 pool: &ExprPool,
525) -> Result<Option<AsymptoticExpansion>, AsymptoticError> {
526 if let ExprData::Func { name, args } = pool.get(f) {
528 if name == "log" && args.len() == 1 {
529 return log_of_arg_peel(f, args[0], var, n_terms, pool);
530 }
531 }
532
533 if let ExprData::Mul(factors) = pool.get(f) {
536 if let Some(exp) = mul_with_scale_factor(&factors, var, n_terms, pool)? {
537 return Ok(Some(exp));
538 }
539 }
540
541 Ok(None)
542}
543
544fn log_of_arg_peel(
547 f: ExprId,
548 arg: ExprId,
549 var: ExprId,
550 n_terms: usize,
551 pool: &ExprPool,
552) -> Result<Option<AsymptoticExpansion>, AsymptoticError> {
553 let t = pool.symbol("__asy_t", Domain::Positive);
556 let inv_t = pool.pow(t, pool.integer(-1_i32));
557 let mut m = HashMap::new();
558 m.insert(var, inv_t);
559 let arg_t = simplify(subs(arg, &m, pool), pool).value;
560 let order = (n_terms as u32).saturating_add(6).min(40);
561 let (val, _analytic) = match regularize_at_zero(arg_t, t, pool) {
562 Some(p) => p,
563 None => return Ok(None),
564 };
565 let d = -val; if d <= 0 {
567 return Ok(None);
569 }
570
571 let x_d = pool.pow(var, pool.integer(d));
574 let cofactor = simplify(
575 pool.mul(vec![arg, pool.pow(x_d, pool.integer(-1_i32))]),
576 pool,
577 )
578 .value;
579 let log_cofactor = pool.func("log", vec![cofactor]);
580
581 let log_x = pool.func("log", vec![var]);
583 let lead = simplify(pool.mul(vec![pool.integer(d), log_x]), pool).value;
584
585 let remainder = power_scale_terms_raw(log_cofactor, var, order, pool)?;
588 let mut candidate: Vec<ExprId> = vec![lead];
589 for (_, e) in remainder.into_iter().take(n_terms.saturating_sub(1)) {
590 if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 0) {
592 continue;
593 }
594 candidate.push(e);
595 }
596
597 let gated = gate_terms(f, var, &candidate, pool);
598 if gated.is_empty() {
599 return Ok(None);
600 }
601 Ok(Some(AsymptoticExpansion {
602 terms: gated
603 .into_iter()
604 .map(|expr| AsymptoticTerm { expr })
605 .collect(),
606 }))
607}
608
609fn mul_with_scale_factor(
612 factors: &[ExprId],
613 var: ExprId,
614 n_terms: usize,
615 pool: &ExprPool,
616) -> Result<Option<AsymptoticExpansion>, AsymptoticError> {
617 let mut scale: Option<ExprId> = None;
619 let mut rest: Vec<ExprId> = Vec::new();
620 for &fac in factors {
621 let is_scale = match pool.get(fac) {
622 ExprData::Func { name, args } if name == "log" && args.len() == 1 => args[0] == var,
623 ExprData::Func { name, args } if name == "exp" && args.len() == 1 => {
624 depends_on(args[0], var, pool)
625 }
626 _ => false,
627 };
628 if is_scale && scale.is_none() {
629 scale = Some(fac);
630 } else {
631 rest.push(fac);
632 }
633 }
634 let Some(scale) = scale else {
635 return Ok(None);
636 };
637 let cofactor = if rest.is_empty() {
638 pool.integer(1_i32)
639 } else {
640 simplify(pool.mul(rest), pool).value
641 };
642 let order = (n_terms as u32).saturating_mul(2).saturating_add(8).min(40);
644 let co_terms = power_scale_terms_raw(cofactor, var, order, pool)?;
645 if co_terms.is_empty() {
646 return Ok(None);
647 }
648 let candidate: Vec<ExprId> = co_terms
649 .into_iter()
650 .take(n_terms)
651 .map(|(_, e)| simplify(pool.mul(vec![scale, e]), pool).value)
652 .collect();
653
654 let f = simplify(pool.mul(factors.to_vec()), pool).value;
655 let gated = gate_terms(f, var, &candidate, pool);
656 if gated.is_empty() {
657 return Ok(None);
658 }
659 Ok(Some(AsymptoticExpansion {
660 terms: gated
661 .into_iter()
662 .map(|expr| AsymptoticTerm { expr })
663 .collect(),
664 }))
665}
666
667fn depends_on(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
668 if expr == var {
669 return true;
670 }
671 match pool.get(expr) {
672 ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|a| depends_on(*a, var, pool)),
673 ExprData::Pow { base, exp } => depends_on(base, var, pool) || depends_on(exp, var, pool),
674 ExprData::Func { args, .. } => args.iter().any(|a| depends_on(*a, var, pool)),
675 _ => false,
676 }
677}
678
679const GATE_POINTS: [f64; 3] = [1.0e2, 1.0e4, 1.0e6];
685
686const GATE_SLACK: f64 = 8.0;
690
691fn gate_terms(f: ExprId, var: ExprId, candidate: &[ExprId], pool: &ExprPool) -> Vec<ExprId> {
697 if candidate.is_empty() {
698 return Vec::new();
699 }
700
701 let mut f_vals = [0.0f64; GATE_POINTS.len()];
703 for (j, &xv) in GATE_POINTS.iter().enumerate() {
704 let mut env = HashMap::new();
705 env.insert(var, xv);
706 match eval_interp(f, &env, pool) {
707 Some(v) if v.is_finite() => f_vals[j] = v,
708 _ => return Vec::new(), }
710 }
711
712 let mut term_vals: Vec<[f64; GATE_POINTS.len()]> = Vec::with_capacity(candidate.len());
714 for &term in candidate {
715 let mut row = [0.0f64; GATE_POINTS.len()];
716 for (j, &xv) in GATE_POINTS.iter().enumerate() {
717 let mut env = HashMap::new();
718 env.insert(var, xv);
719 match eval_interp(term, &env, pool) {
720 Some(v) if v.is_finite() => row[j] = v,
721 _ => {
722 row = [f64::NAN; GATE_POINTS.len()];
724 break;
725 }
726 }
727 }
728 term_vals.push(row);
729 }
730
731 let mut accepted = 0usize;
732 let mut partial = [0.0f64; GATE_POINTS.len()];
733
734 for k in 0..candidate.len() {
735 let row = term_vals[k];
736 if row.iter().any(|v| !v.is_finite()) {
737 break;
738 }
739
740 if k > 0 {
743 let prev = term_vals[k - 1];
744 let mut ok = true;
745 let mut last_ratio = f64::INFINITY;
746 for j in 0..GATE_POINTS.len() {
747 let denom = prev[j].abs();
748 if denom == 0.0 {
749 ok = false;
750 break;
751 }
752 let ratio = row[j].abs() / denom;
753 if ratio > 1.0 {
754 ok = false;
755 break;
756 }
757 if j > 0 && ratio > last_ratio * (1.0 + 1e-9) {
758 ok = false;
760 break;
761 }
762 last_ratio = ratio;
763 }
764 if !ok {
765 break;
766 }
767 }
768
769 let mut next_partial = partial;
771 for j in 0..GATE_POINTS.len() {
772 next_partial[j] += row[j];
773 }
774
775 let mut residual_ok = true;
776 let mut last_rel = f64::INFINITY;
777 for j in 0..GATE_POINTS.len() {
778 let residual = (f_vals[j] - next_partial[j]).abs();
779 let scale = row[j].abs();
780 if residual > scale * GATE_SLACK + 1e-12 {
784 residual_ok = false;
785 break;
786 }
787 let rel = if scale > 0.0 {
790 residual / scale
791 } else {
792 residual
793 };
794 if j > 0 && rel > last_rel * GATE_SLACK + 1e-12 {
795 residual_ok = false;
796 break;
797 }
798 last_rel = rel;
799 }
800 if !residual_ok {
801 break;
802 }
803
804 partial = next_partial;
805 accepted = k + 1;
806 }
807
808 candidate[..accepted].to_vec()
809}
810
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use crate::kernel::Domain;
815
816 fn approx_eq_expr(a: ExprId, b: ExprId, var: ExprId, pool: &ExprPool) -> bool {
817 if simplify(a, pool).value == simplify(b, pool).value {
819 return true;
820 }
821 let mut any = false;
822 for &xv in &[2.5f64, 7.0, 13.0] {
823 let mut env = HashMap::new();
824 env.insert(var, xv);
825 let (Some(va), Some(vb)) = (eval_interp(a, &env, pool), eval_interp(b, &env, pool))
826 else {
827 return false;
828 };
829 if (va - vb).abs() > 1e-9 * (1.0 + va.abs()) {
830 return false;
831 }
832 any = true;
833 }
834 any
835 }
836
837 fn const_val(e: ExprId, pool: &ExprPool) -> Option<f64> {
839 eval_interp(e, &HashMap::new(), pool)
840 }
841
842 fn residual_small(f: ExprId, terms: &[ExprId], var: ExprId, pool: &ExprPool) -> bool {
844 let xv = 1.0e5;
845 let mut env = HashMap::new();
846 env.insert(var, xv);
847 let fv = eval_interp(f, &env, pool).unwrap();
848 let mut sum = 0.0;
849 let mut last = 0.0;
850 for &t in terms {
851 let v = eval_interp(t, &env, pool).unwrap();
852 sum += v;
853 last = v;
854 }
855 (fv - sum).abs() <= last.abs() * 8.0 + 1e-9
856 }
857
858 #[test]
859 fn rational_x_plus_1_over_x_minus_1() {
860 let p = ExprPool::new();
862 let x = p.symbol("x", Domain::Positive);
863 let num = p.add(vec![x, p.integer(1)]);
864 let den = p.add(vec![x, p.integer(-1)]);
865 let f = p.mul(vec![num, p.pow(den, p.integer(-1))]);
866 let exp = asymptotic_expand(f, x, 4, &p).unwrap();
867 let terms = exp.term_exprs();
868 assert!(terms.len() >= 3, "got {} terms", terms.len());
869 assert_eq!(const_val(terms[0], &p), Some(1.0));
871 let two_over_x = p.mul(vec![p.integer(2), p.pow(x, p.integer(-1))]);
873 assert!(approx_eq_expr(terms[1], two_over_x, x, &p));
874 assert!(residual_small(f, &terms, x, &p));
875 }
876
877 #[test]
878 fn sqrt_x_squared_plus_one() {
879 let p = ExprPool::new();
881 let x = p.symbol("x", Domain::Positive);
882 let inside = p.add(vec![p.pow(x, p.integer(2)), p.integer(1)]);
883 let f = p.func("sqrt", vec![inside]);
884 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
885 let terms = exp.term_exprs();
886 assert!(terms.len() >= 2, "got {} terms", terms.len());
887 assert!(approx_eq_expr(terms[0], x, x, &p));
888 let half_over_x = p.mul(vec![
890 p.rational(rug::Integer::from(1), rug::Integer::from(2)),
891 p.pow(x, p.integer(-1)),
892 ]);
893 assert!(approx_eq_expr(terms[1], half_over_x, x, &p));
894 assert!(residual_small(f, &terms, x, &p));
895 }
896
897 #[test]
898 fn x_sin_one_over_x() {
899 let p = ExprPool::new();
901 let x = p.symbol("x", Domain::Positive);
902 let inv = p.pow(x, p.integer(-1));
903 let f = p.mul(vec![x, p.func("sin", vec![inv])]);
904 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
905 let terms = exp.term_exprs();
906 assert!(!terms.is_empty());
907 assert_eq!(const_val(terms[0], &p), Some(1.0));
908 assert!(residual_small(f, &terms, x, &p));
909 }
910
911 #[test]
912 fn sqrt_third_coefficient() {
913 let p = ExprPool::new();
915 let x = p.symbol("x", Domain::Positive);
916 let inside = p.add(vec![p.pow(x, p.integer(2)), p.integer(1)]);
917 let f = p.func("sqrt", vec![inside]);
918 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
919 let terms = exp.term_exprs();
920 assert!(terms.len() >= 3, "got {}", terms.len());
921 let mut env = HashMap::new();
923 env.insert(x, 2.0f64);
924 let third = eval_interp(terms[2], &env, &p).unwrap();
925 let expected = -1.0 / 8.0 * 2.0f64.powi(-3);
926 assert!((third - expected).abs() < 1e-12, "third={third}");
927 }
928
929 #[test]
930 fn oscillatory_declines() {
931 let p = ExprPool::new();
934 let x = p.symbol("x", Domain::Positive);
935 let f = p.func("sin", vec![x]);
936 assert!(asymptotic_expand(f, x, 3, &p).is_err());
937 }
938
939 #[test]
940 fn exp_one_over_x_times_x() {
941 let p = ExprPool::new();
943 let x = p.symbol("x", Domain::Positive);
944 let inv = p.pow(x, p.integer(-1));
945 let f = p.mul(vec![p.func("exp", vec![inv]), x]);
946 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
947 let terms = exp.term_exprs();
948 assert!(terms.len() >= 2, "got {}", terms.len());
949 assert!(approx_eq_expr(terms[0], x, x, &p));
950 assert_eq!(const_val(terms[1], &p), Some(1.0));
951 assert!(residual_small(f, &terms, x, &p));
952 }
953
954 #[test]
955 fn log_x_plus_one() {
956 let p = ExprPool::new();
958 let x = p.symbol("x", Domain::Positive);
959 let arg = p.add(vec![x, p.integer(1)]);
960 let f = p.func("log", vec![arg]);
961 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
962 let terms = exp.term_exprs();
963 assert!(terms.len() >= 2, "got {}", terms.len());
964 let log_x = p.func("log", vec![x]);
966 assert_eq!(simplify(terms[0], &p).value, simplify(log_x, &p).value);
967 let inv = p.pow(x, p.integer(-1));
969 assert!(approx_eq_expr(terms[1], inv, x, &p));
970 assert!(residual_small(f, &terms, x, &p));
971 }
972
973 #[test]
974 fn sqrt_x_plus_sqrt_x_leading() {
975 let p = ExprPool::new();
981 let x = p.symbol("x", Domain::Positive);
982 let inner = p.func("sqrt", vec![x]);
983 let arg = p.add(vec![x, inner]);
984 let f = p.func("sqrt", vec![arg]);
985 match asymptotic_expand(f, x, 2, &p) {
986 Ok(exp) => {
987 let terms = exp.term_exprs();
988 assert!(residual_small(f, &terms, x, &p));
989 let mut env = HashMap::new();
991 env.insert(x, 1.0e6f64);
992 let lead = eval_interp(terms[0], &env, &p).unwrap();
993 let sx = 1.0e3f64; assert!((lead / sx - 1.0).abs() < 1e-2, "lead={lead}");
995 }
996 Err(_) => { }
997 }
998 }
999
1000 #[test]
1001 fn x_over_log_x_peels() {
1002 let p = ExprPool::new();
1005 let x = p.symbol("x", Domain::Positive);
1006 let logx = p.func("log", vec![x]);
1007 let f = p.mul(vec![x, p.pow(logx, p.integer(-1))]);
1008 match asymptotic_expand(f, x, 2, &p) {
1009 Ok(exp) => {
1010 let terms = exp.term_exprs();
1011 assert!(!terms.is_empty());
1012 assert!(residual_small(f, &terms, x, &p));
1013 }
1014 Err(_) => { }
1015 }
1016 }
1017
1018 #[test]
1019 fn invalid_term_count() {
1020 let p = ExprPool::new();
1021 let x = p.symbol("x", Domain::Positive);
1022 let err = asymptotic_expand(x, x, 0, &p).unwrap_err();
1023 assert!(matches!(err, AsymptoticError::InvalidTermCount));
1024 }
1025
1026 #[test]
1027 fn x_over_log_x_gate_is_honest() {
1028 let p = ExprPool::new();
1031 let x = p.symbol("x", Domain::Positive);
1032 let logx = p.func("log", vec![x]);
1033 let f = p.mul(vec![p.pow(x, p.integer(-1)), p.pow(logx, p.integer(-1))]);
1034 match asymptotic_expand(f, x, 3, &p) {
1035 Ok(exp) => {
1036 let terms = exp.term_exprs();
1038 assert!(residual_small(f, &terms, x, &p));
1039 }
1040 Err(_) => { }
1041 }
1042 }
1043}