1use crate::budget::BudgetError;
4use crate::diff::{diff, DiffError};
5use crate::flint::FlintPoly;
6use crate::kernel::{subs, Domain, ExprData, ExprId, ExprPool};
7use crate::poly::{RationalFunction, UniPoly};
8use crate::simplify::simplify;
9use std::cell::Cell;
10use std::collections::HashMap;
11use std::fmt;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19pub struct Series(pub ExprId);
20
21impl Series {
22 pub fn expr(self) -> ExprId {
23 self.0
24 }
25}
26
27#[derive(Debug)]
28pub enum SeriesError {
29 Diff(DiffError),
31 InvalidOrder,
39}
40
41impl fmt::Display for SeriesError {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 SeriesError::Diff(e) => write!(f, "{e}"),
45 SeriesError::InvalidOrder => write!(
46 f,
47 "series order must be >= 1 and reachable: the expansion is not \
48 available at the order requested"
49 ),
50 }
51 }
52}
53
54impl std::error::Error for SeriesError {
55 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
56 match self {
57 SeriesError::Diff(e) => Some(e),
58 SeriesError::InvalidOrder => None,
59 }
60 }
61}
62
63impl crate::errors::AlkahestError for SeriesError {
64 fn code(&self) -> &'static str {
65 match self {
66 SeriesError::Diff(_) => "E-SERIES-001",
67 SeriesError::InvalidOrder => "E-SERIES-002",
68 }
69 }
70
71 fn remediation(&self) -> Option<&'static str> {
72 match self {
73 SeriesError::Diff(_) => {
74 Some("ensure all functions are registered primitives with differentiation rules")
75 }
76 SeriesError::InvalidOrder => Some(
77 "pass order >= 1 (exclusive truncation degree in x); if the order was \
78 already positive the expansion exceeded the work ceiling — ask for a \
79 lower order, or simplify the expression so its derivatives close",
80 ),
81 }
82 }
83}
84
85impl From<DiffError> for SeriesError {
86 fn from(e: DiffError) -> Self {
87 SeriesError::Diff(e)
88 }
89}
90
91pub fn series(
121 expr: ExprId,
122 var: ExprId,
123 point: ExprId,
124 order: u32,
125 pool: &ExprPool,
126) -> Result<Series, SeriesError> {
127 let frame = enter_series_frame();
128 let _ceiling = enter_coeff_ceiling(pool.len().saturating_add(MAX_SERIES_POOL_GROWTH));
133
134 let LocalExpansion {
135 valuation,
136 coeffs,
137 h_expr,
138 } = local_expansion(expr, var, point, order, pool)?;
139
140 if frame.refusal_pending() {
141 return Err(SeriesError::InvalidOrder);
142 }
143
144 Ok(assemble_series(&coeffs, valuation, h_expr, order, pool))
145}
146
147#[derive(Clone, Debug)]
155pub(crate) struct LocalExpansion {
156 pub valuation: i32,
157 pub coeffs: Vec<ExprId>,
158 pub h_expr: ExprId,
159}
160
161pub(crate) fn local_expansion(
162 expr: ExprId,
163 var: ExprId,
164 point: ExprId,
165 order: u32,
166 pool: &ExprPool,
167) -> Result<LocalExpansion, SeriesError> {
168 if order == 0 {
169 return Err(SeriesError::InvalidOrder);
170 }
171
172 let xi = pool.symbol("__sxp", Domain::Real);
173 let mut map = HashMap::new();
174 map.insert(var, pool.add(vec![point, xi]));
175 let shifted = subs(expr, &map, pool);
176
177 let h_expr = expansion_increment(pool, var, point);
178
179 expansion_matched_laurent(shifted, xi, h_expr, order, pool)
180}
181
182fn factorial_u32(n: u32) -> rug::Integer {
183 let mut r = rug::Integer::from(1);
184 for i in 2..=n {
185 r *= i;
186 }
187 r
188}
189
190fn expansion_increment(pool: &ExprPool, var: ExprId, point: ExprId) -> ExprId {
191 match pool.get(point) {
192 ExprData::Integer(n) if n.0 == 0 => var,
193 _ => pool.add(vec![var, pool.mul(vec![pool.integer(-1_i32), point])]),
194 }
195}
196
197fn laurent_big_o_pow(valuation: i32, order: u32) -> i64 {
198 if valuation < 0 {
199 1
200 } else {
201 order as i64
202 }
203}
204
205fn is_structural_zero(id: ExprId, pool: &ExprPool) -> bool {
206 matches!(pool.get(id), ExprData::Integer(n) if n.0 == 0)
207}
208
209fn collect_atom_factors(expr: ExprId, pool: &ExprPool) -> Option<(Vec<ExprId>, Vec<ExprId>)> {
210 match pool.get(expr) {
211 ExprData::Pow { base, exp } => {
212 let n = pool.with(exp, |d| match d {
213 ExprData::Integer(i) => Some(i.0.clone()),
214 _ => None,
215 })?;
216 if n > 0 {
217 Some((vec![expr], vec![]))
218 } else if n < 0 {
219 let mag = (-n).to_u32()?;
220 let pos_exp = pool.integer(mag as i64);
221 Some((vec![], vec![pool.pow(base, pos_exp)]))
222 } else {
223 Some((vec![pool.integer(1_i32)], vec![]))
224 }
225 }
226 ExprData::Integer(_)
227 | ExprData::Rational(_)
228 | ExprData::Float(_)
229 | ExprData::Symbol { .. }
230 | ExprData::Func { .. } => Some((vec![expr], vec![])),
231 ExprData::Add(_)
232 | ExprData::Mul(_)
233 | ExprData::Piecewise { .. }
234 | ExprData::Predicate { .. }
235 | ExprData::Forall { .. }
236 | ExprData::Exists { .. }
237 | ExprData::RootSum { .. }
238 | ExprData::BigO(_) => None,
239 }
240}
241
242fn collect_term_factors(expr: ExprId, pool: &ExprPool) -> Option<(Vec<ExprId>, Vec<ExprId>)> {
243 match pool.get(expr) {
244 ExprData::Mul(args) => {
245 let mut nums = Vec::new();
246 let mut dens = Vec::new();
247 for &a in &args {
248 let (n, d) = collect_atom_factors(a, pool)?;
249 nums.extend(n);
250 dens.extend(d);
251 }
252 Some((nums, dens))
253 }
254 _ => collect_atom_factors(expr, pool),
255 }
256}
257
258fn product_sorted(pool: &ExprPool, factors: Vec<ExprId>) -> ExprId {
259 match factors.len() {
260 0 => pool.integer(1_i32),
261 1 => factors[0],
262 _ => pool.mul(factors),
263 }
264}
265
266fn unipoly_valuation(p: &UniPoly) -> Option<u32> {
267 for (i, c) in p.coefficients().into_iter().enumerate() {
268 if c != 0 {
269 return Some(i as u32);
270 }
271 }
272 None
273}
274
275fn unipoly_strip_low(p: &UniPoly, k: u32) -> UniPoly {
276 let coeffs: Vec<rug::Integer> = p.coefficients().into_iter().skip(k as usize).collect();
277 UniPoly {
278 var: p.var,
279 coeffs: FlintPoly::from_rug_coefficients(&coeffs),
280 }
281}
282
283pub const MAX_SERIES_POOL_GROWTH: usize = 50_000;
301
302thread_local! {
303 static COEFF_POOL_CEILING: Cell<Option<usize>> = const { Cell::new(None) };
306 static IN_SERIES: Cell<bool> = const { Cell::new(false) };
310 static LAST_REFUSAL: Cell<Option<SeriesRefusal>> = const { Cell::new(None) };
314}
315
316#[derive(Clone, Copy, Debug, PartialEq, Eq)]
335pub struct SeriesRefusal {
336 requested: u32,
337 computed: u32,
338 budget: Option<BudgetError>,
339}
340
341impl SeriesRefusal {
342 pub fn requested_coefficients(&self) -> u32 {
344 self.requested
345 }
346
347 pub fn computed_coefficients(&self) -> u32 {
352 self.computed
353 }
354
355 pub fn budget(&self) -> Option<BudgetError> {
358 self.budget
359 }
360}
361
362impl fmt::Display for SeriesRefusal {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 write!(
365 f,
366 "series expansion stopped after {} of {} Taylor coefficients ({}); \
367 refusing to return a shorter series labelled with the requested \
368 order, which would understate the O(.) remainder",
369 self.computed,
370 self.requested,
371 match self.budget {
372 Some(b) => format!("budget: {b}"),
373 None => "internal work ceiling".to_string(),
374 }
375 )
376 }
377}
378
379impl std::error::Error for SeriesRefusal {}
380
381impl crate::errors::AlkahestError for SeriesRefusal {
382 fn code(&self) -> &'static str {
383 "E-SERIES-003"
384 }
385
386 fn remediation(&self) -> Option<&'static str> {
387 Some(
388 "ask for a lower order, raise the budget, or rewrite the expression so its \
389 repeated derivatives close (nested radicals grow by a constant factor per \
390 coefficient)",
391 )
392 }
393}
394
395pub(crate) struct SeriesFrame {
397 outermost: bool,
398}
399
400impl SeriesFrame {
401 fn refusal_pending(&self) -> bool {
403 LAST_REFUSAL.with(|c| c.get().is_some())
404 }
405}
406
407impl Drop for SeriesFrame {
408 fn drop(&mut self) {
409 if self.outermost {
410 IN_SERIES.with(|c| c.set(false));
411 }
412 }
413}
414
415fn enter_series_frame() -> SeriesFrame {
418 LAST_REFUSAL.with(|c| c.set(None));
419 IN_SERIES.with(|c| {
420 let already = c.get();
421 c.set(true);
422 SeriesFrame {
423 outermost: !already,
424 }
425 })
426}
427
428pub fn take_series_refusal() -> Option<SeriesRefusal> {
438 LAST_REFUSAL.with(|c| c.take())
439}
440
441pub(crate) struct CoeffCeiling(Option<usize>);
444
445impl Drop for CoeffCeiling {
446 fn drop(&mut self) {
447 COEFF_POOL_CEILING.with(|c| c.set(self.0));
448 }
449}
450
451pub(crate) fn enter_coeff_ceiling(ceiling: usize) -> CoeffCeiling {
467 COEFF_POOL_CEILING.with(|c| {
468 let prev = c.get();
469 c.set(Some(ceiling));
470 CoeffCeiling(prev)
471 })
472}
473
474fn coeff_loop_should_stop(pool: &ExprPool) -> bool {
477 match COEFF_POOL_CEILING.with(|c| c.get()) {
478 Some(ceiling) => pool.len() > ceiling || crate::budget::check().is_err(),
479 None => false,
480 }
481}
482
483fn taylor_coefficients(
484 mut cur: ExprId,
485 xi: ExprId,
486 num: u32,
487 pool: &ExprPool,
488) -> Result<Vec<ExprId>, SeriesError> {
489 let mut mapping = HashMap::new();
490 mapping.insert(xi, pool.integer(0_i32));
491 let mut out = Vec::with_capacity(num as usize);
492 for k in 0..num {
493 if k > 0 && coeff_loop_should_stop(pool) {
494 if IN_SERIES.with(|c| c.get()) {
499 let refusal = SeriesRefusal {
500 requested: num,
501 computed: k,
502 budget: crate::budget::check().err(),
503 };
504 LAST_REFUSAL.with(|c| c.set(Some(refusal)));
505 }
506 break;
507 }
508 let ev = subs(cur, &mapping, pool);
509 let simp = simplify(ev, pool).value;
510 let fc = factorial_u32(k);
511 let inv_fact = pool.rational(rug::Integer::from(1), fc);
512 let coeff = simplify(pool.mul(vec![simp, inv_fact]), pool).value;
513 out.push(coeff);
514 if k + 1 < num {
515 cur = diff(cur, xi, pool)?.value;
516 }
517 }
518 Ok(out)
519}
520
521fn assemble_series(
522 coeffs: &[ExprId],
523 valuation: i32,
524 h_expr: ExprId,
525 order: u32,
526 pool: &ExprPool,
527) -> Series {
528 let mut terms = Vec::new();
529 for (k, coeff) in coeffs.iter().enumerate() {
530 if is_structural_zero(*coeff, pool) {
531 continue;
532 }
533 let exp = valuation + k as i32;
534 let pow_term = if exp == 0 {
535 pool.integer(1_i32)
536 } else if exp == 1 {
537 h_expr
538 } else {
539 pool.pow(h_expr, pool.integer(exp as i64))
540 };
541 terms.push(pool.mul(vec![*coeff, pow_term]));
542 }
543 let big_o_pow = laurent_big_o_pow(valuation, order);
544 let o_term = pool.big_o(pool.pow(h_expr, pool.integer(big_o_pow)));
545 terms.push(o_term);
546 Series(pool.add(terms))
547}
548
549fn expansion_matched_laurent(
550 shifted: ExprId,
551 xi: ExprId,
552 h_expr: ExprId,
553 order: u32,
554 pool: &ExprPool,
555) -> Result<LocalExpansion, SeriesError> {
556 let (nums, dens) = match collect_term_factors(shifted, pool) {
557 Some(p) => p,
558 None => {
559 let coeffs = taylor_coefficients(shifted, xi, order, pool)?;
560 return Ok(LocalExpansion {
561 valuation: 0,
562 coeffs,
563 h_expr,
564 });
565 }
566 };
567
568 let n_expr = product_sorted(pool, nums);
569 let d_expr = product_sorted(pool, dens);
570
571 let rf = match RationalFunction::from_symbolic(n_expr, d_expr, vec![xi], pool) {
572 Ok(r) => r,
573 Err(_) => {
574 let coeffs = taylor_coefficients(shifted, xi, order, pool)?;
575 return Ok(LocalExpansion {
576 valuation: 0,
577 coeffs,
578 h_expr,
579 });
580 }
581 };
582
583 if rf.numer.is_zero() {
584 return Ok(LocalExpansion {
585 valuation: 0,
586 coeffs: vec![pool.integer(0_i32)],
587 h_expr,
588 });
589 }
590
591 let n_uni = match UniPoly::from_symbolic(rf.numer.to_expr(pool), xi, pool) {
592 Ok(u) => u,
593 Err(_) => {
594 let coeffs = taylor_coefficients(shifted, xi, order, pool)?;
595 return Ok(LocalExpansion {
596 valuation: 0,
597 coeffs,
598 h_expr,
599 });
600 }
601 };
602 let d_uni = match UniPoly::from_symbolic(rf.denom.to_expr(pool), xi, pool) {
603 Ok(u) => u,
604 Err(_) => {
605 let coeffs = taylor_coefficients(shifted, xi, order, pool)?;
606 return Ok(LocalExpansion {
607 valuation: 0,
608 coeffs,
609 h_expr,
610 });
611 }
612 };
613
614 let vn = match unipoly_valuation(&n_uni) {
615 Some(v) => v,
616 None => {
617 return Ok(LocalExpansion {
618 valuation: 0,
619 coeffs: vec![pool.integer(0_i32)],
620 h_expr,
621 });
622 }
623 };
624 let vd = match unipoly_valuation(&d_uni) {
625 Some(v) => v,
626 None => {
627 let coeffs = taylor_coefficients(shifted, xi, order, pool)?;
628 return Ok(LocalExpansion {
629 valuation: 0,
630 coeffs,
631 h_expr,
632 });
633 }
634 };
635
636 let valuation = vn as i32 - vd as i32;
637 let n0 = unipoly_strip_low(&n_uni, vn);
638 let d0 = unipoly_strip_low(&d_uni, vd);
639
640 let d0c = d0.coefficients();
641 if d0c.is_empty() || d0c[0] == 0 {
642 let coeffs = taylor_coefficients(shifted, xi, order, pool)?;
643 return Ok(LocalExpansion {
644 valuation: 0,
645 coeffs,
646 h_expr,
647 });
648 }
649
650 let n0_e = n0.to_symbolic_expr(pool);
651 let d0_e = d0.to_symbolic_expr(pool);
652 let inv_d = pool.pow(d0_e, pool.integer(-1_i32));
653 let g = simplify(pool.mul(vec![n0_e, inv_d]), pool).value;
654
655 let num_taylor: u32 = if valuation < 0 {
656 order
657 } else {
658 (order as i32 - valuation).max(0) as u32
659 };
660
661 if num_taylor == 0 {
662 return Ok(LocalExpansion {
663 valuation,
664 coeffs: Vec::new(),
665 h_expr,
666 });
667 }
668
669 let coeffs = taylor_coefficients(g, xi, num_taylor, pool)?;
670 Ok(LocalExpansion {
671 valuation,
672 coeffs,
673 h_expr,
674 })
675}
676
677#[cfg(test)]
678mod tests {
679 use super::*;
680 use crate::kernel::{Domain, ExprData};
681
682 fn contains_big_o(id: ExprId, pool: &ExprPool) -> bool {
683 match pool.get(id) {
684 ExprData::BigO(_) => true,
685 ExprData::Add(xs) | ExprData::Mul(xs) => xs.iter().any(|e| contains_big_o(*e, pool)),
686 ExprData::Pow { base, exp } => contains_big_o(base, pool) || contains_big_o(exp, pool),
687 ExprData::Func { args, .. } => args.iter().any(|e| contains_big_o(*e, pool)),
688 _ => false,
689 }
690 }
691
692 #[test]
693 fn series_cos_about_zero_has_big_o() {
694 let p = ExprPool::new();
695 let x = p.symbol("x", Domain::Real);
696 let z = p.integer(0);
697 let cx = p.func("cos", vec![x]);
698 let s = series(cx, x, z, 6, &p).unwrap();
699 assert!(contains_big_o(s.expr(), &p));
700 }
701
702 #[test]
703 fn series_inv_x_laurent_has_big_o() {
704 let p = ExprPool::new();
705 let x = p.symbol("x", Domain::Real);
706 let z = p.integer(0);
707 let ix = p.pow(x, p.integer(-1));
708 let s = series(ix, x, z, 4, &p).unwrap();
709 assert!(contains_big_o(s.expr(), &p));
710 }
711
712 #[test]
724 fn series_refuses_rather_than_truncating_a_runaway_radical() {
725 use crate::errors::AlkahestError;
726 let p = ExprPool::new();
727 let t = p.symbol("t", Domain::Real);
728 let inner = p.add(vec![p.pow(t, p.integer(-2)), p.pow(t, p.integer(-1))]);
729 let ex = p.func("sqrt", vec![inner]);
730
731 match series(ex, t, p.integer(0), 32, &p) {
732 Ok(_) => {
733 assert_eq!(take_series_refusal(), None);
736 }
737 Err(e) => {
738 assert!(matches!(e, SeriesError::InvalidOrder), "{e:?}");
739 let refusal = take_series_refusal().expect("work-ceiling refusal recorded");
740 assert_eq!(refusal.code(), "E-SERIES-003");
741 assert_eq!(refusal.budget(), None, "no budget was active");
742 assert!(
743 refusal.computed_coefficients() < refusal.requested_coefficients(),
744 "{refusal}"
745 );
746 }
747 }
748 }
749
750 #[test]
754 fn order_zero_is_a_user_error_not_a_refusal() {
755 let p = ExprPool::new();
756 let x = p.symbol("x", Domain::Real);
757 let cx = p.func("cos", vec![x]);
758 let err = series(cx, x, p.integer(0), 0, &p).unwrap_err();
759 assert!(matches!(err, SeriesError::InvalidOrder), "{err:?}");
760 assert_eq!(take_series_refusal(), None);
761 }
762
763 #[test]
766 fn budget_stops_a_series_and_is_attributed() {
767 use crate::budget::{self, Budget, BudgetError};
768 let p = ExprPool::new();
769 let t = p.symbol("t", Domain::Real);
770 let inner = p.add(vec![p.pow(t, p.integer(-2)), p.pow(t, p.integer(-1))]);
771 let ex = p.func("sqrt", vec![inner]);
772
773 let _guard = budget::enter(Budget::new().with_max_steps(3));
774 let err = series(ex, t, p.integer(0), 32, &p).unwrap_err();
775 assert!(matches!(err, SeriesError::InvalidOrder), "{err:?}");
776 let refusal = take_series_refusal().expect("budget refusal recorded");
777 assert!(
778 matches!(refusal.budget(), Some(BudgetError::Steps { .. })),
779 "{refusal}"
780 );
781 }
782
783 #[test]
786 fn ordinary_high_order_expansion_is_unaffected() {
787 let p = ExprPool::new();
788 let x = p.symbol("x", Domain::Real);
789 let sx = p.func("sin", vec![x]);
790 let s = series(sx, x, p.integer(0), 24, &p).unwrap();
791 assert!(contains_big_o(s.expr(), &p));
792 assert_eq!(take_series_refusal(), None);
793 }
794}