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; \
164 power-scale (rational/algebraic) and single log/exp peels are supported"
165 }
166 })
167 }
168}
169
170impl From<DiffError> for AsymptoticError {
171 fn from(e: DiffError) -> Self {
172 AsymptoticError::Diff(e)
173 }
174}
175
176pub fn asymptotic_expand(
186 f: ExprId,
187 var: ExprId,
188 n_terms: usize,
189 pool: &ExprPool,
190) -> Result<AsymptoticExpansion, AsymptoticError> {
191 if n_terms == 0 {
192 return Err(AsymptoticError::InvalidTermCount);
193 }
194
195 let f = simplify(f, pool).value;
196
197 if let Ok(exp) = power_scale_expand(f, var, n_terms, pool) {
199 if !exp.terms.is_empty() {
200 return Ok(exp);
201 }
202 }
203
204 if let Some(exp) = try_log_peel(f, var, n_terms, pool)? {
206 if !exp.terms.is_empty() {
207 return Ok(exp);
208 }
209 }
210
211 Err(AsymptoticError::UnsupportedScale)
212}
213
214fn power_scale_terms_raw(
224 f: ExprId,
225 var: ExprId,
226 order: u32,
227 pool: &ExprPool,
228) -> Result<Vec<(i64, ExprId)>, AsymptoticError> {
229 let t = pool.symbol("__asy_t", Domain::Positive);
230 let inv_t = pool.pow(t, pool.integer(-1_i32));
231 let mut m = HashMap::new();
232 m.insert(var, inv_t);
233 let f_of_t = simplify(subs(f, &m, pool), pool).value;
234
235 let (val, analytic) =
241 regularize_at_zero(f_of_t, t, pool).ok_or(AsymptoticError::SeriesFailed)?;
242
243 let LocalExpansion {
244 valuation: tay_val,
245 coeffs,
246 ..
247 } = local_expansion(analytic, t, pool.integer(0_i32), order, pool)
248 .map_err(|_| AsymptoticError::SeriesFailed)?;
249
250 let mut out: Vec<(i64, ExprId)> = Vec::new();
254 for (i, &c) in coeffs.iter().enumerate() {
255 let c = fold_constant(c, pool);
258 if is_numeric_zero(c, pool) {
259 continue;
260 }
261 let t_pow = val + tay_val as i64 + i as i64;
262 let x_pow = -t_pow;
263 let term = make_power_term(c, var, x_pow, pool);
264 out.push((x_pow, term));
265 }
266 out.sort_by_key(|p| std::cmp::Reverse(p.0));
268 Ok(out)
269}
270
271fn regularize_at_zero(g: ExprId, t: ExprId, pool: &ExprPool) -> Option<(i64, ExprId)> {
281 let g = simplify(g, pool).value;
282 match pool.get(g) {
283 ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Float(_) => Some((0, g)),
285 ExprData::Symbol { .. } => {
286 if g == t {
287 Some((1, pool.integer(1_i32)))
288 } else {
289 Some((0, g))
290 }
291 }
292 ExprData::Mul(xs) => {
293 let mut val = 0i64;
294 let mut parts: Vec<ExprId> = Vec::new();
295 for x in xs {
296 let (v, u) = regularize_at_zero(x, t, pool)?;
297 val += v;
298 parts.push(u);
299 }
300 Some((val, simplify(pool.mul(parts), pool).value))
301 }
302 ExprData::Pow { base, exp } => {
303 let (vb, ub) = regularize_at_zero(base, t, pool)?;
305 let exp_q = rational_value(exp, pool)?;
306 let num = exp_q.numer().clone();
308 let den = exp_q.denom().clone();
309 let prod = rug::Integer::from(vb) * #
310 if den == 0 {
312 return None;
313 }
314 let (q, r) = prod.div_rem(den.clone());
315 if r != 0 {
316 return None; }
318 let val = q.to_i64()?;
319 let analytic = simplify(pool.pow(ub, exp), pool).value;
321 Some((val, analytic))
322 }
323 ExprData::Add(xs) => {
324 let mut pieces: Vec<(i64, ExprId)> = Vec::with_capacity(xs.len());
326 let mut vmin = i64::MAX;
327 for x in &xs {
328 let (v, u) = regularize_at_zero(*x, t, pool)?;
329 vmin = vmin.min(v);
330 pieces.push((v, u));
331 }
332 if vmin == i64::MAX {
333 return None;
334 }
335 let mut summands: Vec<ExprId> = Vec::with_capacity(pieces.len());
336 for (v, u) in pieces {
337 let shift = v - vmin; let term = if shift == 0 {
339 u
340 } else {
341 simplify(pool.mul(vec![pool.pow(t, pool.integer(shift)), u]), pool).value
342 };
343 summands.push(term);
344 }
345 let analytic = simplify(pool.add(summands), pool).value;
346 Some((vmin, analytic))
349 }
350 ExprData::Func { name, args } if args.len() == 1 => {
351 let (va, ua) = regularize_at_zero(args[0], t, pool)?;
352 match name.as_str() {
353 "sqrt" => {
356 if va % 2 != 0 {
357 return None;
358 }
359 let analytic = simplify(pool.func("sqrt", vec![ua]), pool).value;
363 Some((va / 2, analytic))
364 }
365 "sin" | "cos" | "tan" | "exp" | "cosh" | "sinh" | "tanh" | "gamma" => {
371 if va < 0 {
372 return None;
373 }
374 let full_arg = if va == 0 {
376 ua
377 } else {
378 simplify(pool.mul(vec![pool.pow(t, pool.integer(va)), ua]), pool).value
379 };
380 let analytic = simplify(pool.func(name, vec![full_arg]), pool).value;
381 Some((0, analytic))
382 }
383 "log" => {
387 if va != 0 {
388 return None;
389 }
390 let at0 = eval_at_zero(ua, t, pool)?;
391 if at0 == 0.0 {
392 return None;
393 }
394 let analytic = simplify(pool.func("log", vec![ua]), pool).value;
395 Some((0, analytic))
396 }
397 _ => None,
398 }
399 }
400 _ => None,
401 }
402}
403
404fn fold_constant(e: ExprId, pool: &ExprPool) -> ExprId {
407 let e = simplify(e, pool).value;
408 match pool.get(e) {
409 ExprData::Add(xs) => {
410 let ys: Vec<ExprId> = xs.iter().map(|x| fold_constant(*x, pool)).collect();
411 simplify(pool.add(ys), pool).value
412 }
413 ExprData::Mul(xs) => {
414 let ys: Vec<ExprId> = xs.iter().map(|x| fold_constant(*x, pool)).collect();
415 simplify(pool.mul(ys), pool).value
416 }
417 ExprData::Pow { base, exp } => {
418 let b = fold_constant(base, pool);
419 let x = fold_constant(exp, pool);
420 simplify(pool.pow(b, x), pool).value
421 }
422 ExprData::Func { name, args } if args.len() == 1 => {
423 let inner = fold_constant(args[0], pool);
424 if matches!(pool.get(inner), ExprData::Integer(n) if n.0 == 0) {
425 match name.as_str() {
426 "sin" | "tan" | "sinh" | "tanh" => return pool.integer(0_i32),
427 "cos" | "cosh" | "exp" => return pool.integer(1_i32),
428 _ => {}
429 }
430 }
431 simplify(pool.func(name, vec![inner]), pool).value
432 }
433 _ => e,
434 }
435}
436
437fn is_numeric_zero(e: ExprId, pool: &ExprPool) -> bool {
440 if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 0) {
441 return true;
442 }
443 if let ExprData::Rational(r) = pool.get(e) {
444 return r.0 == 0;
445 }
446 match eval_interp(e, &HashMap::new(), pool) {
447 Some(v) => v == 0.0,
448 None => false,
449 }
450}
451
452fn eval_at_zero(e: ExprId, t: ExprId, pool: &ExprPool) -> Option<f64> {
455 let mut env = HashMap::new();
456 env.insert(t, 1.0e-6f64);
457 eval_interp(e, &env, pool).filter(|v| v.is_finite())
458}
459
460fn rational_value(e: ExprId, pool: &ExprPool) -> Option<rug::Rational> {
462 match pool.get(e) {
463 ExprData::Integer(n) => Some(rug::Rational::from((n.0.clone(), rug::Integer::from(1)))),
464 ExprData::Rational(r) => Some(r.0.clone()),
465 _ => None,
466 }
467}
468
469fn make_power_term(coeff: ExprId, var: ExprId, x_pow: i64, pool: &ExprPool) -> ExprId {
470 let pow = if x_pow == 0 {
471 pool.integer(1_i32)
472 } else if x_pow == 1 {
473 var
474 } else {
475 pool.pow(var, pool.integer(x_pow))
476 };
477 simplify(pool.mul(vec![coeff, pow]), pool).value
478}
479
480fn power_scale_expand(
481 f: ExprId,
482 var: ExprId,
483 n_terms: usize,
484 pool: &ExprPool,
485) -> Result<AsymptoticExpansion, AsymptoticError> {
486 let order = (n_terms as u32).saturating_mul(2).saturating_add(8).min(40);
488 let raw = power_scale_terms_raw(f, var, order, pool)?;
489 if raw.is_empty() {
490 return Err(AsymptoticError::SeriesFailed);
491 }
492 let candidate: Vec<ExprId> = raw.iter().map(|(_, e)| *e).take(n_terms).collect();
493 let gated = gate_terms(f, var, &candidate, pool);
494 if gated.is_empty() {
495 return Err(AsymptoticError::GateFailed);
496 }
497 Ok(AsymptoticExpansion {
498 terms: gated
499 .into_iter()
500 .map(|expr| AsymptoticTerm { expr })
501 .collect(),
502 })
503}
504
505fn try_log_peel(
519 f: ExprId,
520 var: ExprId,
521 n_terms: usize,
522 pool: &ExprPool,
523) -> Result<Option<AsymptoticExpansion>, AsymptoticError> {
524 if let ExprData::Func { name, args } = pool.get(f) {
526 if name == "log" && args.len() == 1 {
527 return log_of_arg_peel(f, args[0], var, n_terms, pool);
528 }
529 }
530
531 if let ExprData::Mul(factors) = pool.get(f) {
534 if let Some(exp) = mul_with_scale_factor(&factors, var, n_terms, pool)? {
535 return Ok(Some(exp));
536 }
537 }
538
539 Ok(None)
540}
541
542fn log_of_arg_peel(
545 f: ExprId,
546 arg: ExprId,
547 var: ExprId,
548 n_terms: usize,
549 pool: &ExprPool,
550) -> Result<Option<AsymptoticExpansion>, AsymptoticError> {
551 let t = pool.symbol("__asy_t", Domain::Positive);
554 let inv_t = pool.pow(t, pool.integer(-1_i32));
555 let mut m = HashMap::new();
556 m.insert(var, inv_t);
557 let arg_t = simplify(subs(arg, &m, pool), pool).value;
558 let order = (n_terms as u32).saturating_add(6).min(40);
559 let (val, _analytic) = match regularize_at_zero(arg_t, t, pool) {
560 Some(p) => p,
561 None => return Ok(None),
562 };
563 let d = -val; if d <= 0 {
565 return Ok(None);
567 }
568
569 let x_d = pool.pow(var, pool.integer(d));
572 let cofactor = simplify(
573 pool.mul(vec![arg, pool.pow(x_d, pool.integer(-1_i32))]),
574 pool,
575 )
576 .value;
577 let log_cofactor = pool.func("log", vec![cofactor]);
578
579 let log_x = pool.func("log", vec![var]);
581 let lead = simplify(pool.mul(vec![pool.integer(d), log_x]), pool).value;
582
583 let remainder = power_scale_terms_raw(log_cofactor, var, order, pool)?;
586 let mut candidate: Vec<ExprId> = vec![lead];
587 for (_, e) in remainder.into_iter().take(n_terms.saturating_sub(1)) {
588 if matches!(pool.get(e), ExprData::Integer(n) if n.0 == 0) {
590 continue;
591 }
592 candidate.push(e);
593 }
594
595 let gated = gate_terms(f, var, &candidate, pool);
596 if gated.is_empty() {
597 return Ok(None);
598 }
599 Ok(Some(AsymptoticExpansion {
600 terms: gated
601 .into_iter()
602 .map(|expr| AsymptoticTerm { expr })
603 .collect(),
604 }))
605}
606
607fn mul_with_scale_factor(
610 factors: &[ExprId],
611 var: ExprId,
612 n_terms: usize,
613 pool: &ExprPool,
614) -> Result<Option<AsymptoticExpansion>, AsymptoticError> {
615 let mut scale: Option<ExprId> = None;
617 let mut rest: Vec<ExprId> = Vec::new();
618 for &fac in factors {
619 let is_scale = match pool.get(fac) {
620 ExprData::Func { name, args } if name == "log" && args.len() == 1 => args[0] == var,
621 ExprData::Func { name, args } if name == "exp" && args.len() == 1 => {
622 depends_on(args[0], var, pool)
623 }
624 _ => false,
625 };
626 if is_scale && scale.is_none() {
627 scale = Some(fac);
628 } else {
629 rest.push(fac);
630 }
631 }
632 let Some(scale) = scale else {
633 return Ok(None);
634 };
635 let cofactor = if rest.is_empty() {
636 pool.integer(1_i32)
637 } else {
638 simplify(pool.mul(rest), pool).value
639 };
640 let order = (n_terms as u32).saturating_mul(2).saturating_add(8).min(40);
642 let co_terms = power_scale_terms_raw(cofactor, var, order, pool)?;
643 if co_terms.is_empty() {
644 return Ok(None);
645 }
646 let candidate: Vec<ExprId> = co_terms
647 .into_iter()
648 .take(n_terms)
649 .map(|(_, e)| simplify(pool.mul(vec![scale, e]), pool).value)
650 .collect();
651
652 let f = simplify(pool.mul(factors.to_vec()), pool).value;
653 let gated = gate_terms(f, var, &candidate, pool);
654 if gated.is_empty() {
655 return Ok(None);
656 }
657 Ok(Some(AsymptoticExpansion {
658 terms: gated
659 .into_iter()
660 .map(|expr| AsymptoticTerm { expr })
661 .collect(),
662 }))
663}
664
665fn depends_on(expr: ExprId, var: ExprId, pool: &ExprPool) -> bool {
666 if expr == var {
667 return true;
668 }
669 match pool.get(expr) {
670 ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|a| depends_on(*a, var, pool)),
671 ExprData::Pow { base, exp } => depends_on(base, var, pool) || depends_on(exp, var, pool),
672 ExprData::Func { args, .. } => args.iter().any(|a| depends_on(*a, var, pool)),
673 _ => false,
674 }
675}
676
677const GATE_POINTS: [f64; 3] = [1.0e2, 1.0e4, 1.0e6];
683
684const GATE_SLACK: f64 = 8.0;
688
689fn gate_terms(f: ExprId, var: ExprId, candidate: &[ExprId], pool: &ExprPool) -> Vec<ExprId> {
695 if candidate.is_empty() {
696 return Vec::new();
697 }
698
699 let mut f_vals = [0.0f64; GATE_POINTS.len()];
701 for (j, &xv) in GATE_POINTS.iter().enumerate() {
702 let mut env = HashMap::new();
703 env.insert(var, xv);
704 match eval_interp(f, &env, pool) {
705 Some(v) if v.is_finite() => f_vals[j] = v,
706 _ => return Vec::new(), }
708 }
709
710 let mut term_vals: Vec<[f64; GATE_POINTS.len()]> = Vec::with_capacity(candidate.len());
712 for &term in candidate {
713 let mut row = [0.0f64; GATE_POINTS.len()];
714 for (j, &xv) in GATE_POINTS.iter().enumerate() {
715 let mut env = HashMap::new();
716 env.insert(var, xv);
717 match eval_interp(term, &env, pool) {
718 Some(v) if v.is_finite() => row[j] = v,
719 _ => {
720 row = [f64::NAN; GATE_POINTS.len()];
722 break;
723 }
724 }
725 }
726 term_vals.push(row);
727 }
728
729 let mut accepted = 0usize;
730 let mut partial = [0.0f64; GATE_POINTS.len()];
731
732 for k in 0..candidate.len() {
733 let row = term_vals[k];
734 if row.iter().any(|v| !v.is_finite()) {
735 break;
736 }
737
738 if k > 0 {
741 let prev = term_vals[k - 1];
742 let mut ok = true;
743 let mut last_ratio = f64::INFINITY;
744 for j in 0..GATE_POINTS.len() {
745 let denom = prev[j].abs();
746 if denom == 0.0 {
747 ok = false;
748 break;
749 }
750 let ratio = row[j].abs() / denom;
751 if ratio > 1.0 {
752 ok = false;
753 break;
754 }
755 if j > 0 && ratio > last_ratio * (1.0 + 1e-9) {
756 ok = false;
758 break;
759 }
760 last_ratio = ratio;
761 }
762 if !ok {
763 break;
764 }
765 }
766
767 let mut next_partial = partial;
769 for j in 0..GATE_POINTS.len() {
770 next_partial[j] += row[j];
771 }
772
773 let mut residual_ok = true;
774 let mut last_rel = f64::INFINITY;
775 for j in 0..GATE_POINTS.len() {
776 let residual = (f_vals[j] - next_partial[j]).abs();
777 let scale = row[j].abs();
778 if residual > scale * GATE_SLACK + 1e-12 {
782 residual_ok = false;
783 break;
784 }
785 let rel = if scale > 0.0 {
788 residual / scale
789 } else {
790 residual
791 };
792 if j > 0 && rel > last_rel * GATE_SLACK + 1e-12 {
793 residual_ok = false;
794 break;
795 }
796 last_rel = rel;
797 }
798 if !residual_ok {
799 break;
800 }
801
802 partial = next_partial;
803 accepted = k + 1;
804 }
805
806 candidate[..accepted].to_vec()
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use crate::kernel::Domain;
813
814 fn approx_eq_expr(a: ExprId, b: ExprId, var: ExprId, pool: &ExprPool) -> bool {
815 if simplify(a, pool).value == simplify(b, pool).value {
817 return true;
818 }
819 let mut any = false;
820 for &xv in &[2.5f64, 7.0, 13.0] {
821 let mut env = HashMap::new();
822 env.insert(var, xv);
823 let (Some(va), Some(vb)) = (eval_interp(a, &env, pool), eval_interp(b, &env, pool))
824 else {
825 return false;
826 };
827 if (va - vb).abs() > 1e-9 * (1.0 + va.abs()) {
828 return false;
829 }
830 any = true;
831 }
832 any
833 }
834
835 fn const_val(e: ExprId, pool: &ExprPool) -> Option<f64> {
837 eval_interp(e, &HashMap::new(), pool)
838 }
839
840 fn residual_small(f: ExprId, terms: &[ExprId], var: ExprId, pool: &ExprPool) -> bool {
842 let xv = 1.0e5;
843 let mut env = HashMap::new();
844 env.insert(var, xv);
845 let fv = eval_interp(f, &env, pool).unwrap();
846 let mut sum = 0.0;
847 let mut last = 0.0;
848 for &t in terms {
849 let v = eval_interp(t, &env, pool).unwrap();
850 sum += v;
851 last = v;
852 }
853 (fv - sum).abs() <= last.abs() * 8.0 + 1e-9
854 }
855
856 #[test]
857 fn rational_x_plus_1_over_x_minus_1() {
858 let p = ExprPool::new();
860 let x = p.symbol("x", Domain::Positive);
861 let num = p.add(vec![x, p.integer(1)]);
862 let den = p.add(vec![x, p.integer(-1)]);
863 let f = p.mul(vec![num, p.pow(den, p.integer(-1))]);
864 let exp = asymptotic_expand(f, x, 4, &p).unwrap();
865 let terms = exp.term_exprs();
866 assert!(terms.len() >= 3, "got {} terms", terms.len());
867 assert_eq!(const_val(terms[0], &p), Some(1.0));
869 let two_over_x = p.mul(vec![p.integer(2), p.pow(x, p.integer(-1))]);
871 assert!(approx_eq_expr(terms[1], two_over_x, x, &p));
872 assert!(residual_small(f, &terms, x, &p));
873 }
874
875 #[test]
876 fn sqrt_x_squared_plus_one() {
877 let p = ExprPool::new();
879 let x = p.symbol("x", Domain::Positive);
880 let inside = p.add(vec![p.pow(x, p.integer(2)), p.integer(1)]);
881 let f = p.func("sqrt", vec![inside]);
882 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
883 let terms = exp.term_exprs();
884 assert!(terms.len() >= 2, "got {} terms", terms.len());
885 assert!(approx_eq_expr(terms[0], x, x, &p));
886 let half_over_x = p.mul(vec![
888 p.rational(rug::Integer::from(1), rug::Integer::from(2)),
889 p.pow(x, p.integer(-1)),
890 ]);
891 assert!(approx_eq_expr(terms[1], half_over_x, x, &p));
892 assert!(residual_small(f, &terms, x, &p));
893 }
894
895 #[test]
896 fn x_sin_one_over_x() {
897 let p = ExprPool::new();
899 let x = p.symbol("x", Domain::Positive);
900 let inv = p.pow(x, p.integer(-1));
901 let f = p.mul(vec![x, p.func("sin", vec![inv])]);
902 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
903 let terms = exp.term_exprs();
904 assert!(!terms.is_empty());
905 assert_eq!(const_val(terms[0], &p), Some(1.0));
906 assert!(residual_small(f, &terms, x, &p));
907 }
908
909 #[test]
910 fn sqrt_third_coefficient() {
911 let p = ExprPool::new();
913 let x = p.symbol("x", Domain::Positive);
914 let inside = p.add(vec![p.pow(x, p.integer(2)), p.integer(1)]);
915 let f = p.func("sqrt", vec![inside]);
916 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
917 let terms = exp.term_exprs();
918 assert!(terms.len() >= 3, "got {}", terms.len());
919 let mut env = HashMap::new();
921 env.insert(x, 2.0f64);
922 let third = eval_interp(terms[2], &env, &p).unwrap();
923 let expected = -1.0 / 8.0 * 2.0f64.powi(-3);
924 assert!((third - expected).abs() < 1e-12, "third={third}");
925 }
926
927 #[test]
928 fn oscillatory_declines() {
929 let p = ExprPool::new();
932 let x = p.symbol("x", Domain::Positive);
933 let f = p.func("sin", vec![x]);
934 assert!(asymptotic_expand(f, x, 3, &p).is_err());
935 }
936
937 #[test]
938 fn exp_one_over_x_times_x() {
939 let p = ExprPool::new();
941 let x = p.symbol("x", Domain::Positive);
942 let inv = p.pow(x, p.integer(-1));
943 let f = p.mul(vec![p.func("exp", vec![inv]), x]);
944 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
945 let terms = exp.term_exprs();
946 assert!(terms.len() >= 2, "got {}", terms.len());
947 assert!(approx_eq_expr(terms[0], x, x, &p));
948 assert_eq!(const_val(terms[1], &p), Some(1.0));
949 assert!(residual_small(f, &terms, x, &p));
950 }
951
952 #[test]
953 fn log_x_plus_one() {
954 let p = ExprPool::new();
956 let x = p.symbol("x", Domain::Positive);
957 let arg = p.add(vec![x, p.integer(1)]);
958 let f = p.func("log", vec![arg]);
959 let exp = asymptotic_expand(f, x, 3, &p).unwrap();
960 let terms = exp.term_exprs();
961 assert!(terms.len() >= 2, "got {}", terms.len());
962 let log_x = p.func("log", vec![x]);
964 assert_eq!(simplify(terms[0], &p).value, simplify(log_x, &p).value);
965 let inv = p.pow(x, p.integer(-1));
967 assert!(approx_eq_expr(terms[1], inv, x, &p));
968 assert!(residual_small(f, &terms, x, &p));
969 }
970
971 #[test]
972 fn sqrt_x_plus_sqrt_x_leading() {
973 let p = ExprPool::new();
979 let x = p.symbol("x", Domain::Positive);
980 let inner = p.func("sqrt", vec![x]);
981 let arg = p.add(vec![x, inner]);
982 let f = p.func("sqrt", vec![arg]);
983 match asymptotic_expand(f, x, 2, &p) {
984 Ok(exp) => {
985 let terms = exp.term_exprs();
986 assert!(residual_small(f, &terms, x, &p));
987 let mut env = HashMap::new();
989 env.insert(x, 1.0e6f64);
990 let lead = eval_interp(terms[0], &env, &p).unwrap();
991 let sx = 1.0e3f64; assert!((lead / sx - 1.0).abs() < 1e-2, "lead={lead}");
993 }
994 Err(_) => { }
995 }
996 }
997
998 #[test]
999 fn x_over_log_x_peels() {
1000 let p = ExprPool::new();
1003 let x = p.symbol("x", Domain::Positive);
1004 let logx = p.func("log", vec![x]);
1005 let f = p.mul(vec![x, p.pow(logx, p.integer(-1))]);
1006 match asymptotic_expand(f, x, 2, &p) {
1007 Ok(exp) => {
1008 let terms = exp.term_exprs();
1009 assert!(!terms.is_empty());
1010 assert!(residual_small(f, &terms, x, &p));
1011 }
1012 Err(_) => { }
1013 }
1014 }
1015
1016 #[test]
1017 fn invalid_term_count() {
1018 let p = ExprPool::new();
1019 let x = p.symbol("x", Domain::Positive);
1020 let err = asymptotic_expand(x, x, 0, &p).unwrap_err();
1021 assert!(matches!(err, AsymptoticError::InvalidTermCount));
1022 }
1023
1024 #[test]
1025 fn x_over_log_x_gate_is_honest() {
1026 let p = ExprPool::new();
1029 let x = p.symbol("x", Domain::Positive);
1030 let logx = p.func("log", vec![x]);
1031 let f = p.mul(vec![p.pow(x, p.integer(-1)), p.pow(logx, p.integer(-1))]);
1032 match asymptotic_expand(f, x, 3, &p) {
1033 Ok(exp) => {
1034 let terms = exp.term_exprs();
1036 assert!(residual_small(f, &terms, x, &p));
1037 }
1038 Err(_) => { }
1039 }
1040 }
1041}