1use std::sync::Arc;
22
23use crate::algebraic::Algebraic;
24use crate::expansion::{Expansion, ExpansionError, Source};
25use crate::multi::Multi;
26use crate::numeric::{BigInt, Rational};
27use crate::ordered_field::{OrderedField, PolyOver};
28use crate::puiseux::{expansion_term, remainder, remainder_after, Closed, Puiseux};
29use crate::tower::Scale;
30
31#[derive(Clone, Debug)]
33pub(crate) enum Recipe {
34 Field(Multi),
36 Closed(Puiseux),
38 Add(Arc<AlgValue>, Arc<AlgValue>),
39 Neg(Arc<AlgValue>),
40 Mul(Arc<AlgValue>, Arc<AlgValue>),
41 Recip(Arc<AlgValue>),
42 Root(Arc<AlgValue>, u32),
43 Pow(Arc<AlgValue>, u32),
44}
45
46#[derive(Clone, Debug)]
49pub struct AlgValue {
50 pub(crate) scale: Arc<Scale>,
51 pub(crate) recipe: Recipe,
52 pub(crate) poly: PolyOver<Multi>,
54 pub(crate) expansion: Expansion<Multi>,
55}
56
57#[derive(Debug)]
59struct ClosedSource(Puiseux);
60
61impl Source<Multi> for ClosedSource {
62 fn coefficient(&self, k: i64) -> Result<Multi, ExpansionError> {
63 Ok(Multi::Standard(self.0.coefficient(k)))
64 }
65}
66
67fn gcd_u64(a: u64, b: u64) -> u64 {
68 if b == 0 {
69 a
70 } else {
71 gcd_u64(b, a % b)
72 }
73}
74
75fn lcm_u32(a: u32, b: u32) -> u32 {
76 let g = gcd_u64(u64::from(a), u64::from(b));
77 u32::try_from(u64::from(a) / g * u64::from(b)).expect("a root of an infinitesimal of moderate index")
78}
79
80fn lcm_i64(a: i64, b: i64) -> i64 {
81 a / gcd_u64(a.unsigned_abs(), b.unsigned_abs()) as i64 * b
82}
83
84fn lower_hull(points: &[(i64, i64)]) -> Vec<(i64, i64)> {
86 let mut hull: Vec<(i64, i64)> = Vec::new();
87 for &p in points {
88 while hull.len() >= 2 {
89 let (o, a) = (hull[hull.len() - 2], hull[hull.len() - 1]);
90 let cross = (a.0 - o.0) * (p.1 - o.1) - (a.1 - o.1) * (p.0 - o.0);
91 if cross <= 0 {
92 hull.pop();
93 } else {
94 break;
95 }
96 }
97 hull.push(p);
98 }
99 hull
100}
101
102fn valuation(a: &Multi, scale: &Arc<Scale>) -> Rational {
105 match a.top() {
106 Some(t) if t.same_infinitesimal(scale) => a.leading().expect("a nonzero value has a leading term").0,
107 _ => Rational::zero(),
108 }
109}
110
111const SHOWN_REACH: i64 = 64;
113
114const EQUALS_ORDERS: i64 = 8;
116
117const PADE_CHECK_ORDERS: usize = 4;
121
122impl AlgValue {
123 pub(crate) fn field(x: &Multi, scale: &Arc<Scale>) -> AlgValue {
126 let poly = PolyOver::from(vec![x.neg(), Multi::one()]);
127 let expansion = match x.top() {
128 Some(t) if t.same_infinitesimal(scale) => {
129 let (q, num, den) = x.parts_at(scale);
130 Expansion::fraction(num.c, den.c, q).expect("a value's denominator is nonzero")
131 }
132 _ => Expansion::constant(x.clone()),
133 };
134 AlgValue {
135 scale: scale.clone(),
136 recipe: Recipe::Field(x.clone()),
137 poly,
138 expansion,
139 }
140 }
141
142 pub(crate) fn closed(p: &Puiseux, scale: &Arc<Scale>) -> AlgValue {
145 let (coefficients, q) = p.annihilator();
146 let poly = PolyOver::from(
147 coefficients
148 .into_iter()
149 .map(|h| Multi::of_closed(Closed::of_field(h, q), scale))
150 .collect(),
151 )
152 .monic();
153 let expansion = Expansion::source(Arc::new(ClosedSource(p.clone())), p.order(), p.ramification());
154 AlgValue {
155 scale: scale.clone(),
156 recipe: Recipe::Closed(p.clone()),
157 poly,
158 expansion,
159 }
160 }
161
162 pub(crate) fn at(m: &Multi, scale: &Arc<Scale>) -> AlgValue {
164 match m {
165 Multi::Alg(a) if a.scale.same_infinitesimal(scale) => (**a).clone(),
166 Multi::One(Closed::Series(p), s) if s.same_infinitesimal(scale) && p.rational_function().is_none() => {
167 AlgValue::closed(p, scale)
168 }
169 _ => AlgValue::field(m, scale),
170 }
171 }
172
173 fn reach(&self) -> Option<i64> {
177 let p = self.poly.without_zero_root();
178 if p.degree().unwrap_or(0) == 0 {
179 return None;
180 }
181 let points: Vec<(i64, Rational)> = p
182 .c
183 .iter()
184 .enumerate()
185 .filter(|(_, a)| !a.is_zero())
186 .map(|(i, a)| (i as i64, valuation(a, &self.scale)))
187 .collect();
188 let common = points
189 .iter()
190 .map(|(_, v)| v.denominator().to_i64().expect("a small denominator"))
191 .fold(1i64, lcm_i64);
192 let scaled: Vec<(i64, i64)> = points
193 .iter()
194 .map(|(i, v)| {
195 let w = v.mul(&Rational::from_i64(common));
196 (*i, w.to_bigint().and_then(|b| b.to_i64()).expect("a small order"))
197 })
198 .collect();
199 let hull = lower_hull(&scaled);
200 let largest = hull
201 .windows(2)
202 .map(|e| Rational::from_ratio_i64(e[0].1 - e[1].1, (e[1].0 - e[0].0) * common).expect("a positive run"))
203 .max()?;
204 let on_grid = largest.mul(&Rational::from_i64(i64::from(self.expansion.ramification())));
205 Some(on_grid.ceil().to_i64().expect("a small order"))
206 }
207
208 pub(crate) fn is_zero(&self) -> bool {
211 if !self.poly.coeff(0).is_zero() {
212 return false;
213 }
214 match self.reach() {
215 None => true,
216 Some(reach) => self
217 .expansion
218 .leading_through(reach)
219 .expect("exact coefficients")
220 .is_none(),
221 }
222 }
223
224 pub(crate) fn leading(&self) -> (i64, Multi) {
226 let reach = self.reach().expect("a nonzero value is a nonzero root of its polynomial");
227 self.expansion
228 .leading_through(reach)
229 .expect("exact coefficients")
230 .expect("a nonzero root's leading term lies at or below the largest order of the polynomial's roots")
231 }
232
233 pub(crate) fn order(&self) -> Rational {
235 let (k, _) = self.leading();
236 Rational::from_ratio_i64(k, i64::from(self.expansion.ramification())).expect("q is positive")
237 }
238
239 pub(crate) fn sign(&self) -> i32 {
240 OrderedField::signum(&self.leading().1)
241 }
242
243 fn of(&self, recipe: Recipe, poly: PolyOver<Multi>, expansion: Expansion<Multi>) -> AlgValue {
244 AlgValue {
245 scale: self.scale.clone(),
246 recipe,
247 poly: poly.squarefree(),
248 expansion,
249 }
250 }
251
252 pub(crate) fn add(a: &Arc<AlgValue>, b: &Arc<AlgValue>) -> Multi {
253 let poly = PolyOver::sum_poly(&a.poly, &b.poly);
254 a.of(Recipe::Add(a.clone(), b.clone()), poly, a.expansion.add(&b.expansion)).canonical()
255 }
256
257 pub(crate) fn neg(a: &Arc<AlgValue>) -> Multi {
258 a.of(Recipe::Neg(a.clone()), a.poly.neg_var(), a.expansion.neg()).canonical()
259 }
260
261 pub(crate) fn mul(a: &Arc<AlgValue>, b: &Arc<AlgValue>) -> Multi {
263 let poly = PolyOver::product_poly(&a.poly.without_zero_root(), &b.poly.without_zero_root());
264 a.of(Recipe::Mul(a.clone(), b.clone()), poly, a.expansion.mul(&b.expansion)).canonical()
265 }
266
267 pub(crate) fn recip(a: &Arc<AlgValue>) -> Multi {
269 let (k, c) = a.leading();
270 a.of(Recipe::Recip(a.clone()), a.poly.reversed(), a.expansion.recip(k, c)).canonical()
271 }
272
273 pub(crate) fn root(a: &Arc<AlgValue>, n: u32) -> Result<Multi, String> {
275 let (k, c) = a.leading();
276 let expansion = a.expansion.root(n, k, c).map_err(|e| e.to_string())?;
277 Ok(a.of(Recipe::Root(a.clone(), n), a.poly.spread(n as usize), expansion).canonical())
278 }
279
280 pub(crate) fn pow(a: &Arc<AlgValue>, k: u32) -> Multi {
283 let poly = PolyOver::power_poly(&a.poly, k);
284 let mut base = a.expansion.clone();
285 let mut acc: Option<Expansion<Multi>> = None;
286 let mut e = k;
287 while e > 0 {
288 if e & 1 == 1 {
289 acc = Some(match acc {
290 None => base.clone(),
291 Some(x) => x.mul(&base),
292 });
293 }
294 e >>= 1;
295 if e > 0 {
296 base = base.mul(&base);
297 }
298 }
299 a.of(Recipe::Pow(a.clone(), k), poly, acc.expect("k ≥ 1")).canonical()
300 }
301
302 fn canonical(self) -> Multi {
304 if self.is_zero() {
305 return <Multi as OrderedField>::zero();
306 }
307 if self.poly.degree() == Some(1) {
308 return self.poly.coeff(0).neg().mul(&self.poly.coeff(1).recip().expect("a leading coefficient is nonzero"));
309 }
310 if let Some(x) = self.field_value() {
311 return x;
312 }
313 Multi::Alg(Arc::new(self))
314 }
315
316 fn field_value(&self) -> Option<Multi> {
319 let scale = &self.scale;
320 let monic = self.poly.monic();
321 let parts: Vec<(u32, PolyOver<Multi>, PolyOver<Multi>)> = monic
322 .c
323 .iter()
324 .map(|a| match a {
325 Multi::Alg(_) => None,
326 Multi::One(c, s) if s.same_infinitesimal(scale) && c.field().is_none() => None,
327 _ => Some(a.parts_at(scale)),
328 })
329 .collect::<Option<_>>()?;
330 let qe = self.expansion.ramification();
331 let q = parts.iter().fold(qe, |l, (qa, _, _)| lcm_u32(l, *qa));
332 let degrees: Vec<(usize, usize)> = parts
333 .iter()
334 .map(|(qa, n, d)| {
335 let m = (q / qa) as usize;
336 (n.degree().unwrap_or(0) * m, d.degree().unwrap_or(0) * m)
337 })
338 .collect();
339 let lcm_bound: usize = degrees.iter().map(|(_, d)| d).sum();
340 let (n0, d0) = degrees[0];
341 let num_bound = (lcm_bound + n0).checked_sub(d0)?;
342 let refined = self.expansion.refined(q / qe);
343 let (k, _) = self.leading();
344 let v = k * i64::from(q / qe);
345 let (l, m) = if v >= 0 {
346 (num_bound.checked_sub(usize::try_from(v).ok()?)?, lcm_bound)
347 } else {
348 (num_bound, lcm_bound.checked_sub(usize::try_from(-v).ok()?)?)
349 };
350 let series: Vec<Multi> = (0..=(l + m) as i64)
351 .map(|i| refined.coefficient(v + i))
352 .collect::<Result<_, _>>()
353 .ok()?;
354 let (num, den) = PolyOver::pade(&series, l, m)?;
355 'past: for k in (l + m + 1)..=(l + m + PADE_CHECK_ORDERS) {
356 let mut past = OrderedField::neg(&num.coeff(k));
357 for (j, d) in den.c.iter().enumerate().take(k + 1) {
358 let Ok(term) = refined.coefficient(v + (k - j) as i64) else {
359 break 'past;
360 };
361 past = OrderedField::add(&past, &OrderedField::mul(d, &term));
362 }
363 if !OrderedField::is_zero(&past) {
364 return None;
365 }
366 }
367 let shift = |p: &PolyOver<Multi>, by: usize| {
368 let mut c = vec![<Multi as OrderedField>::zero(); by];
369 c.extend(p.c.iter().cloned());
370 PolyOver::from(c)
371 };
372 let (num, den) = if v >= 0 {
373 (shift(&num, v as usize), den)
374 } else {
375 (num, shift(&den, (-v) as usize))
376 };
377 let candidate = Multi::make(scale, q, num, den);
378 if !OrderedField::is_zero(&monic.eval(&candidate)) {
379 return None;
380 }
381 let at_candidate = AlgValue::field(&candidate, scale);
382 let difference = AlgValue {
383 scale: scale.clone(),
384 recipe: Recipe::Field(candidate.clone()),
385 poly: monic.shift(&candidate),
386 expansion: self.expansion.sub(&at_candidate.expansion),
387 };
388 difference.is_zero().then_some(candidate)
389 }
390
391 fn same_recipe(a: &Arc<AlgValue>, b: &Arc<AlgValue>) -> bool {
393 if Arc::ptr_eq(a, b) {
394 return true;
395 }
396 if !a.scale.same_infinitesimal(&b.scale) {
397 return false;
398 }
399 let same = AlgValue::same_recipe;
400 match (&a.recipe, &b.recipe) {
401 (Recipe::Field(x), Recipe::Field(y)) => x == y,
402 (Recipe::Closed(x), Recipe::Closed(y)) => Closed::Series(x.clone()) == Closed::Series(y.clone()),
403 (Recipe::Add(a1, a2), Recipe::Add(b1, b2)) | (Recipe::Mul(a1, a2), Recipe::Mul(b1, b2)) => {
404 same(a1, b1) && same(a2, b2)
405 }
406 (Recipe::Neg(x), Recipe::Neg(y)) | (Recipe::Recip(x), Recipe::Recip(y)) => same(x, y),
407 (Recipe::Root(x, n), Recipe::Root(y, m)) | (Recipe::Pow(x, n), Recipe::Pow(y, m)) => n == m && same(x, y),
408 _ => false,
409 }
410 }
411
412 pub(crate) fn equals(a: &Arc<AlgValue>, b: &Arc<AlgValue>) -> bool {
417 if !a.scale.same_infinitesimal(&b.scale) {
418 return false;
419 }
420 if AlgValue::same_recipe(a, b) {
421 return true;
422 }
423 let difference = a.expansion.sub(&b.expansion);
424 let through = difference.floor() + EQUALS_ORDERS * i64::from(difference.ramification());
425 if let Ok(Some(_)) = difference.leading_through(through) {
426 return false;
427 }
428 OrderedField::is_zero(&OrderedField::sub(&Multi::Alg(a.clone()), &Multi::Alg(b.clone())))
429 }
430
431 pub(crate) fn render(a: &Arc<AlgValue>) -> String {
439 if let Some(text) = AlgValue::radical(a) {
440 return text;
441 }
442 a.leading_terms()
443 }
444
445 fn radical(a: &Arc<AlgValue>) -> Option<String> {
449 let degree = a.poly.degree().unwrap_or(0) as u32;
450 let own = a.poly.without_zero_root();
451 if own.degree() == Some(2) && !OrderedField::is_zero(&own.coeff(1)) {
452 return None;
453 }
454 let negative = a.sign() < 0;
455 for n in 2..=degree.max(2) {
456 let kappa = AlgValue::pow(a, n);
457 if matches!(kappa, Multi::Alg(_)) {
458 continue;
459 }
460 let (num, den) = kappa.fraction_parts();
461 let inverted = num.top().is_none() && den.top().is_some();
462 let kappa = if inverted { kappa.recip()? } else { kappa };
463 let (num, den) = kappa.fraction_parts();
464 let (cn, num) = integer_content(&num);
465 let (cd, den) = integer_content(&den);
466 let constant = Rational::new(cn, cd).expect("a content is positive");
467 let root = Algebraic::from_rational(constant.clone())
468 .nth_root(n)
469 .and_then(|r| r.to_rational());
470 let (outside, inside) = match root {
471 Some(r) => (r, num.mul(&den.recip().expect("a denominator is nonzero"))),
472 None => (
473 Rational::one(),
474 num.mul(&den.recip().expect("a denominator is nonzero"))
475 .mul(&Multi::Standard(Algebraic::from_rational(constant))),
476 ),
477 };
478 let outside = if n % 2 == 0 && negative { outside.negated() } else { outside };
479 let radicand = inside.render();
480 let radicand = if radicand.contains([' ', '/', '·', '^']) { format!("({radicand})") } else { radicand };
481 let sign = radical_sign(n);
482 let factor = outside.to_string();
483 let (minus, factor) = match factor.strip_prefix('-') {
484 Some(rest) => ("-", rest.to_string()),
485 None => ("", factor),
486 };
487 let factor = if factor.contains('/') { format!("({factor})") } else { factor };
488 let root = if factor == "1" { format!("{sign}{radicand}") } else { format!("{factor}·{sign}{radicand}") };
489 return Some(if inverted {
490 let root = if root.contains('·') { format!("({root})") } else { root };
491 format!("{minus}1/{root}")
492 } else {
493 format!("{minus}{root}")
494 });
495 }
496 None
497 }
498
499 fn leading_terms(&self) -> String {
501 let name = &self.scale.name;
502 let q = i64::from(self.expansion.ramification());
503 let (k0, c0) = self.leading();
504 let next_after = |from: i64| -> Option<(i64, Multi)> {
505 for k in from + 1..=from + SHOWN_REACH * q {
506 let c = self.expansion.coefficient(k).expect("exact coefficients");
507 if !OrderedField::is_zero(&c) {
508 return Some((k, c));
509 }
510 }
511 None
512 };
513 let exponent = |k: i64| Rational::from_ratio_i64(k, q).expect("q is positive");
514 let mut terms = vec![(k0, c0)];
515 if let Some(second) = next_after(k0) {
516 terms.push(second);
517 }
518 let mut out = String::new();
519 for (i, (k, c)) in terms.iter().enumerate() {
520 let t = expansion_term(name, &c.render(), &exponent(*k));
521 match (i, t.strip_prefix('-')) {
522 (0, _) => out.push_str(&t),
523 (_, Some(rest)) => {
524 out.push_str(" - ");
525 out.push_str(rest);
526 }
527 (_, None) => {
528 out.push_str(" + ");
529 out.push_str(&t);
530 }
531 }
532 }
533 let shown: Vec<Rational> = terms.iter().map(|(k, _)| exponent(*k)).collect();
534 let last = terms.last().expect("a term is shown").0;
535 let next = next_after(last).map(|(k, _)| exponent(k)).unwrap_or_else(|| {
536 exponent(last + SHOWN_REACH * q)
537 });
538 format!("{out} + {}", remainder(name, &remainder_after(&shown, &next)))
539 }
540}
541
542fn radical_sign(n: u32) -> String {
544 match n {
545 2 => "√".to_string(),
546 3 => "∛".to_string(),
547 4 => "∜".to_string(),
548 n => {
549 let digits: String = n
550 .to_string()
551 .chars()
552 .map(|d| ['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹'][d.to_digit(10).expect("a digit") as usize])
553 .collect();
554 format!("{digits}√")
555 }
556 }
557}
558
559fn integer_content(p: &Multi) -> (BigInt, Multi) {
562 let mut leaves: Vec<Rational> = Vec::new();
563 if !collect_leaves(p, &mut leaves) {
564 return (BigInt::from_i64(1), p.clone());
565 }
566 let g = leaves
567 .iter()
568 .filter(|r| !r.is_zero())
569 .fold(BigInt::from_i64(0), |g, r| g.gcd(r.numerator()));
570 if g.is_zero() || g == BigInt::from_i64(1) {
571 return (BigInt::from_i64(1), p.clone());
572 }
573 let inv = Multi::Standard(Algebraic::from_rational(Rational::new(BigInt::from_i64(1), g.clone()).expect("g ≠ 0")));
574 (g, p.mul(&inv))
575}
576
577fn collect_leaves(p: &Multi, out: &mut Vec<Rational>) -> bool {
579 match p {
580 Multi::Standard(a) => match a.to_rational().filter(Rational::is_integer) {
581 Some(r) => {
582 out.push(r);
583 true
584 }
585 None => false,
586 },
587 Multi::One(c, _) => match c.field() {
588 Some((h, _)) => {
589 let (num, den) = h.parts();
590 if *den != PolyOver::one() {
591 return false;
592 }
593 num.c.iter().all(|a| match a.to_rational().filter(Rational::is_integer) {
594 Some(r) => {
595 out.push(r);
596 true
597 }
598 None => false,
599 })
600 }
601 None => false,
602 },
603 Multi::Over { num, den, .. } => {
604 *den == PolyOver::one() && num.c.iter().all(|c| collect_leaves(c, out))
605 }
606 Multi::Alg(_) => false,
607 }
608}
609
610#[cfg(test)]
611#[path = "../../logicaffeine_tests/unit/logicaffeine_base/closure/tests.rs"]
612mod tests;