1use serde::Serialize;
2use std::{
3 collections::BTreeMap,
4 fmt::{Display, Formatter},
5};
6use symbolica::domains::{dual::HyperDual, float::Float as SymbolicaFloat};
7use tracing::debug;
8
9use crate::utils::{F, FloatLike};
10
11const MINIMUM_PRECISION_RESIDUAL_IMPROVEMENT: i64 = 8;
12
13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
14pub(crate) struct RadialRootIdentity(String);
15
16impl RadialRootIdentity {
17 pub(crate) fn new(description: String) -> Self {
18 Self(description)
19 }
20}
21
22impl Display for RadialRootIdentity {
23 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
24 self.0.fmt(formatter)
25 }
26}
27
28#[derive(Clone, Debug)]
29struct LocalRootConsistency {
30 near_radius_step: Option<SymbolicaFloat>,
31 near_lower_value: Option<SymbolicaFloat>,
32 near_upper_value: Option<SymbolicaFloat>,
33 near_secant_derivative_ratio: Option<SymbolicaFloat>,
34 far_secant_derivative_ratio: Option<SymbolicaFloat>,
35 secant_ratio_agreement: Option<SymbolicaFloat>,
36 is_valid: bool,
37}
38
39impl LocalRootConsistency {
40 fn invalid() -> Self {
41 Self {
42 near_radius_step: None,
43 near_lower_value: None,
44 near_upper_value: None,
45 near_secant_derivative_ratio: None,
46 far_secant_derivative_ratio: None,
47 secant_ratio_agreement: None,
48 is_valid: false,
49 }
50 }
51
52 fn check<T: FloatLike>(
53 result: &NewtonIterationResult<T>,
54 inside_radius: &F<T>,
55 f_x_and_df_x: &impl Fn(&F<T>) -> (F<T>, F<T>),
56 ) -> Self {
57 const PROBE_EVALUATIONS: usize = 4;
61 if result.num_iterations_used < PROBE_EVALUATIONS {
62 return Self::invalid();
63 }
64
65 let zero = result.solution.zero();
66 let two = result.solution.from_i64(2);
67 let four = result.solution.from_i64(4);
68 let eight = result.solution.from_i64(8);
69 let radial_scale = result.solution.abs();
72 let newton_correction =
73 result.error_of_function.abs() / result.derivative_at_solution.abs();
74 let mut near_radius_step =
75 (result.solution.epsilon().sqrt() * radial_scale).max(&four * newton_correction);
76 let available_radius = &result.solution - inside_radius;
77 if !is_finite(&available_radius) || available_radius <= zero {
78 return Self::invalid();
79 }
80 let maximum_far_radius_step = &available_radius / &two;
81 if &near_radius_step * &eight > maximum_far_radius_step {
82 near_radius_step = &maximum_far_radius_step / &eight;
83 }
84 let far_radius_step = &near_radius_step * &eight;
85 if !is_finite(&near_radius_step)
86 || near_radius_step <= zero
87 || !is_finite(&far_radius_step)
88 || far_radius_step <= near_radius_step
89 {
90 return Self::invalid();
91 }
92
93 let probe = |radius_step: &F<T>| {
94 let lower_radius = &result.solution - radius_step;
95 let upper_radius = &result.solution + radius_step;
96 let (lower_value, _) = f_x_and_df_x(&lower_radius);
97 let (upper_value, _) = f_x_and_df_x(&upper_radius);
98 let secant_derivative = (&upper_value - &lower_value) / (&two * radius_step);
99 let secant_derivative_ratio = &secant_derivative / &result.derivative_at_solution;
100 (
101 lower_value,
102 upper_value,
103 secant_derivative,
104 secant_derivative_ratio,
105 )
106 };
107 let (
108 near_lower_value,
109 near_upper_value,
110 near_secant_derivative,
111 near_secant_derivative_ratio,
112 ) = probe(&near_radius_step);
113 let (far_lower_value, far_upper_value, far_secant_derivative, far_secant_derivative_ratio) =
114 probe(&far_radius_step);
115 let secant_ratio_agreement = &far_secant_derivative_ratio / &near_secant_derivative_ratio;
116 let half = result.solution.from_i64(1) / &two;
117 let two_again = two.clone();
118 let three_quarters = result.solution.from_i64(3) / &four;
119 let five_quarters = result.solution.from_i64(5) / &four;
120 let is_valid = is_finite(&near_lower_value)
121 && is_finite(&near_upper_value)
122 && is_finite(&near_secant_derivative)
123 && is_finite(&near_secant_derivative_ratio)
124 && is_finite(&far_lower_value)
125 && is_finite(&far_upper_value)
126 && is_finite(&far_secant_derivative)
127 && is_finite(&far_secant_derivative_ratio)
128 && is_finite(&secant_ratio_agreement)
129 && near_lower_value < zero
130 && near_upper_value > zero
131 && far_lower_value < zero
132 && far_upper_value > zero
133 && near_secant_derivative > zero
134 && far_secant_derivative > zero
135 && near_secant_derivative_ratio >= half
136 && near_secant_derivative_ratio <= two_again
137 && far_secant_derivative_ratio >= half
138 && far_secant_derivative_ratio <= two_again
139 && secant_ratio_agreement >= three_quarters
140 && secant_ratio_agreement <= five_quarters;
141
142 Self {
143 near_radius_step: Some(near_radius_step.into()),
144 near_lower_value: Some(near_lower_value.into()),
145 near_upper_value: Some(near_upper_value.into()),
146 near_secant_derivative_ratio: Some(near_secant_derivative_ratio.into()),
147 far_secant_derivative_ratio: Some(far_secant_derivative_ratio.into()),
148 secant_ratio_agreement: Some(secant_ratio_agreement.into()),
149 is_valid,
150 }
151 }
152}
153
154#[derive(Clone, Debug)]
155struct RadialRootObservation {
156 precision_bits: u32,
157 precision_epsilon: SymbolicaFloat,
158 residual: SymbolicaFloat,
159 roundoff_residual_scale: SymbolicaFloat,
160 maximum_residual: SymbolicaFloat,
161 solution: SymbolicaFloat,
162 derivative: SymbolicaFloat,
163 lower_bound: Option<SymbolicaFloat>,
164 upper_bound: Option<SymbolicaFloat>,
165 bracket_is_valid: bool,
166 local_consistency: Option<LocalRootConsistency>,
167 relative_newton_correction: SymbolicaFloat,
168 relative_residual_limit: SymbolicaFloat,
169}
170
171impl RadialRootObservation {
172 fn new<T: FloatLike>(
173 result: &NewtonIterationResult<T>,
174 bracket: Option<(&F<T>, &F<T>)>,
175 local_consistency: Option<LocalRootConsistency>,
176 tolerance: &F<T>,
177 e_cm: &F<T>,
178 ) -> Self {
179 let precision_epsilon_t = result.solution.epsilon();
180 let residual_t = result.error_of_function.abs();
181 let derivative_abs_t = result.derivative_at_solution.abs();
182 let e_cm_t = e_cm.abs();
183 let tolerance_t = tolerance.abs();
184 let radial_scale_t = result.solution.abs();
185 let roundoff_residual_scale_t = &precision_epsilon_t * &e_cm_t;
186 let maximum_residual_t = &roundoff_residual_scale_t * &tolerance_t;
187 let relative_newton_correction_t = &residual_t / &derivative_abs_t / &radial_scale_t;
188 let relative_residual_limit_t = &precision_epsilon_t * &tolerance_t;
189
190 let precision_epsilon: SymbolicaFloat = precision_epsilon_t.into();
194 let precision_bits = precision_epsilon.prec();
195 let residual = residual_t.into();
196 let solution = result.solution.clone().into();
197 let derivative = result.derivative_at_solution.clone().into();
198 let roundoff_residual_scale = roundoff_residual_scale_t.into();
199 let maximum_residual = maximum_residual_t.into();
200 let relative_newton_correction = relative_newton_correction_t.into();
201 let relative_residual_limit = relative_residual_limit_t.into();
202
203 let (lower_bound, upper_bound, bracket_is_valid) =
204 bracket.map_or((None, None, true), |(lower_bound, upper_bound)| {
205 let bracket_is_valid = lower_bound <= upper_bound
206 && &result.solution >= lower_bound
207 && &result.solution <= upper_bound;
208 (
209 Some(lower_bound.clone().into()),
210 Some(upper_bound.clone().into()),
211 bracket_is_valid,
212 )
213 });
214
215 Self {
216 precision_bits,
217 precision_epsilon,
218 residual,
219 roundoff_residual_scale,
220 maximum_residual,
221 solution,
222 derivative,
223 lower_bound,
227 upper_bound,
228 bracket_is_valid,
229 local_consistency,
230 relative_newton_correction,
231 relative_residual_limit,
232 }
233 }
234
235 fn comparison_residual(&self) -> SymbolicaFloat {
236 if self.residual >= self.roundoff_residual_scale {
237 self.residual.clone()
238 } else {
239 self.roundoff_residual_scale.clone()
240 }
241 }
242
243 fn values_are_finite(&self) -> bool {
244 self.precision_epsilon.is_finite()
245 && self.residual.is_finite()
246 && self.roundoff_residual_scale.is_finite()
247 && self.maximum_residual.is_finite()
248 && self.solution.is_finite()
249 && self.derivative.is_finite()
250 && self.relative_newton_correction.is_finite()
251 && self.relative_residual_limit.is_finite()
252 && self
253 .lower_bound
254 .as_ref()
255 .is_none_or(SymbolicaFloat::is_finite)
256 && self
257 .upper_bound
258 .as_ref()
259 .is_none_or(SymbolicaFloat::is_finite)
260 }
261
262 fn precision_rescue_improvement(&self, previous: &Self) -> Option<SymbolicaFloat> {
263 let zero = SymbolicaFloat::with_val(self.precision_bits.max(previous.precision_bits), 0);
264 if !self.values_are_finite()
265 || !previous.values_are_finite()
266 || self.precision_bits <= previous.precision_bits
267 || self.solution <= zero
268 || self.derivative <= zero
269 || !self.bracket_is_valid
270 || !self
271 .local_consistency
272 .as_ref()
273 .is_some_and(|consistency| consistency.is_valid)
274 || previous.solution <= zero
275 || previous.derivative <= zero
276 || !previous.bracket_is_valid
277 || self.residual > previous.maximum_residual
278 || self.relative_newton_correction > previous.relative_residual_limit
279 || self.residual <= zero
280 {
281 return None;
282 }
283
284 let improvement = previous.comparison_residual() / self.residual.clone();
287 let required_improvement =
288 SymbolicaFloat::with_val(improvement.prec(), MINIMUM_PRECISION_RESIDUAL_IMPROVEMENT);
289 (improvement >= required_improvement).then_some(improvement)
290 }
291}
292
293#[derive(Clone, Debug, Default)]
312pub(crate) struct RadialRootDiagnostics {
313 observations: BTreeMap<(RadialRootIdentity, usize, u32), RadialRootObservation>,
314 current_precision_bits: Option<u32>,
315 current_occurrences: BTreeMap<RadialRootIdentity, usize>,
316}
317
318impl RadialRootDiagnostics {
319 fn next_call_key<T: FloatLike>(
320 &mut self,
321 identity: &RadialRootIdentity,
322 precision_source: &F<T>,
323 ) -> (RadialRootIdentity, usize, u32) {
324 let precision: SymbolicaFloat = precision_source.clone().into();
325 let precision_bits = precision.prec();
326 if self.current_precision_bits != Some(precision_bits) {
327 self.current_precision_bits = Some(precision_bits);
328 self.current_occurrences.clear();
329 }
330
331 let occurrence = self
332 .current_occurrences
333 .entry(identity.clone())
334 .or_default();
335 let key = (identity.clone(), *occurrence, precision_bits);
336 *occurrence += 1;
337 key
338 }
339
340 fn record_observation(
341 &mut self,
342 key: (RadialRootIdentity, usize, u32),
343 observation: RadialRootObservation,
344 ) {
345 match self.observations.get(&key) {
346 Some(previous) if observation.residual >= previous.residual => {}
347 _ => {
348 self.observations.insert(key, observation);
349 }
350 }
351 }
352
353 #[cfg(test)]
354 pub(crate) fn restart_precision_pass(&mut self) {
355 self.current_precision_bits = None;
356 self.current_occurrences.clear();
357 }
358
359 #[allow(clippy::too_many_arguments)]
360 pub(crate) fn solve<T: FloatLike>(
361 &mut self,
362 identity: &RadialRootIdentity,
363 inside_radius: &F<T>,
364 outside_radius_guess: &F<T>,
365 f_x_and_df_x: impl Fn(&F<T>) -> (F<T>, F<T>),
366 tolerance: &F<T>,
367 max_iterations: usize,
368 max_bracket_expansions: usize,
369 e_cm: &F<T>,
370 ) -> Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
371 let call_key = self.next_call_key(identity, inside_radius);
372 let solve_result = safeguarded_newton_iteration_and_derivative(
373 inside_radius,
374 outside_radius_guess,
375 &f_x_and_df_x,
376 tolerance,
377 max_iterations,
378 max_bracket_expansions,
379 e_cm,
380 );
381
382 let (result, lower_bound, upper_bound) = match &solve_result {
383 Ok(result) => {
384 let observation = RadialRootObservation::new(result, None, None, tolerance, e_cm);
385 self.record_observation(call_key, observation);
386 return solve_result;
387 }
388 Err(SafeguardedNewtonError::DidNotConverge {
389 result,
390 lower_bound,
391 upper_bound,
392 }) => (result, lower_bound, upper_bound),
393 Err(error) => {
394 debug!(
395 radial_root = %identity,
396 occurrence = call_key.1,
397 error = %error,
398 "radial root failed a structural safeguarded-solver check"
399 );
400 return solve_result;
401 }
402 };
403
404 let previous = self
405 .observations
406 .range(
407 (call_key.0.clone(), call_key.1, 0)..(call_key.0.clone(), call_key.1, call_key.2),
408 )
409 .next_back()
410 .map(|(_, observation)| observation.clone());
411 let bracket_width = upper_bound - lower_bound;
412 let midpoint = lower_bound + &bracket_width / inside_radius.from_i64(2);
413 let bracket_is_exhausted = midpoint == *lower_bound || midpoint == *upper_bound;
414 let local_consistency = (previous.is_some() || bracket_is_exhausted)
415 .then(|| LocalRootConsistency::check(result, inside_radius, &f_x_and_df_x));
416 let current = RadialRootObservation::new(
417 result,
418 Some((lower_bound, upper_bound)),
419 local_consistency,
420 tolerance,
421 e_cm,
422 );
423 let accepts_resolution =
427 |candidate: &NewtonIterationResult<T>, observation: &RadialRootObservation| {
428 let resolution_residual_limit = inside_radius.epsilon() * tolerance * e_cm
429 + candidate.derivative_at_solution.abs() * &bracket_width;
430 let accepted = bracket_is_exhausted
431 && is_finite(&resolution_residual_limit)
432 && observation.values_are_finite()
433 && observation.bracket_is_valid
434 && candidate.solution > inside_radius.zero()
435 && candidate.derivative_at_solution > inside_radius.zero()
436 && observation
437 .local_consistency
438 .as_ref()
439 .is_some_and(|consistency| consistency.is_valid)
440 && candidate.error_of_function.abs() <= resolution_residual_limit;
441 if accepted {
442 debug!(
443 radial_root = %identity,
444 occurrence = call_key.1,
445 current_epsilon = %observation.precision_epsilon,
446 current_residual = %observation.residual,
447 original_residual_limit = %observation.maximum_residual,
448 resolution_residual_limit = %resolution_residual_limit,
449 bracket_width = %bracket_width,
450 solution = %observation.solution,
451 derivative = %observation.derivative,
452 lower_bound = ?observation.lower_bound,
453 upper_bound = ?observation.upper_bound,
454 relative_newton_correction = %observation.relative_newton_correction,
455 local_consistency = ?observation.local_consistency,
456 "accepted a radial root at the representable bracket resolution"
457 );
458 }
459 accepted
460 };
461 if accepts_resolution(result, ¤t) {
462 self.record_observation(call_key, current);
463 return Ok(result.clone());
464 }
465 let precision_improvement = previous
466 .as_ref()
467 .and_then(|previous| current.precision_rescue_improvement(previous));
468
469 if let Some(improvement) = precision_improvement {
470 let previous = previous.expect("a precision improvement requires a prior observation");
471 debug!(
472 radial_root = %identity,
473 occurrence = call_key.1,
474 previous_epsilon = %previous.precision_epsilon,
475 current_epsilon = %current.precision_epsilon,
476 previous_residual = %previous.residual,
477 previous_comparison_residual = %previous.comparison_residual(),
478 current_residual = %current.residual,
479 residual_improvement = %improvement,
480 accepted_residual = %previous.maximum_residual,
481 relative_newton_correction = %current.relative_newton_correction,
482 local_near_radius_step = ?current
483 .local_consistency
484 .as_ref()
485 .and_then(|consistency| consistency.near_radius_step.as_ref()),
486 local_near_lower_value = ?current
487 .local_consistency
488 .as_ref()
489 .and_then(|consistency| consistency.near_lower_value.as_ref()),
490 local_near_upper_value = ?current
491 .local_consistency
492 .as_ref()
493 .and_then(|consistency| consistency.near_upper_value.as_ref()),
494 local_near_secant_derivative_ratio = ?current
495 .local_consistency
496 .as_ref()
497 .and_then(|consistency| consistency.near_secant_derivative_ratio.as_ref()),
498 local_far_secant_derivative_ratio = ?current
499 .local_consistency
500 .as_ref()
501 .and_then(|consistency| consistency.far_secant_derivative_ratio.as_ref()),
502 local_secant_ratio_agreement = ?current
503 .local_consistency
504 .as_ref()
505 .and_then(|consistency| consistency.secant_ratio_agreement.as_ref()),
506 "accepted a residual-limited radial root after precision escalation"
507 );
508 self.record_observation(call_key, current);
509 return Ok(result.clone());
510 }
511
512 let alternate_radius = if result.solution == *lower_bound {
516 Some(upper_bound)
517 } else if result.solution == *upper_bound {
518 Some(lower_bound)
519 } else {
520 None
521 };
522 if bracket_is_exhausted
523 && lower_bound < upper_bound
524 && current.values_are_finite()
525 && current.bracket_is_valid
526 && result.solution > inside_radius.zero()
527 && result.derivative_at_solution > inside_radius.zero()
528 && let Some(radius) = alternate_radius
529 {
530 let (value, derivative) = f_x_and_df_x(radius);
531 let alternate = NewtonIterationResult {
532 solution: radius.clone(),
533 derivative_at_solution: derivative,
534 error_of_function: value,
535 num_iterations_used: result.num_iterations_used,
536 };
537 let observation = RadialRootObservation::new(
538 &alternate,
539 Some((lower_bound, upper_bound)),
540 Some(LocalRootConsistency::check(
541 &alternate,
542 inside_radius,
543 &f_x_and_df_x,
544 )),
545 tolerance,
546 e_cm,
547 );
548 if accepts_resolution(&alternate, &observation) {
549 self.record_observation(call_key, observation);
550 return Ok(alternate);
551 }
552 }
553
554 if let Some(previous) = previous.as_ref() {
555 let residual_improvement = previous.comparison_residual() / current.residual.clone();
556 debug!(
557 radial_root = %identity,
558 occurrence = call_key.1,
559 previous_epsilon = %previous.precision_epsilon,
560 current_epsilon = %current.precision_epsilon,
561 previous_residual = %previous.residual,
562 previous_comparison_residual = %previous.comparison_residual(),
563 current_residual = %current.residual,
564 residual_improvement = %residual_improvement,
565 accepted_residual = %previous.maximum_residual,
566 relative_newton_correction = %current.relative_newton_correction,
567 accepted_relative_newton_correction = %previous.relative_residual_limit,
568 solution = %current.solution,
569 derivative = %current.derivative,
570 lower_bound = ?current.lower_bound,
571 upper_bound = ?current.upper_bound,
572 bracket_is_valid = current.bracket_is_valid,
573 local_consistency = ?current.local_consistency,
574 "radial root did not satisfy precision-rescue criteria"
575 );
576 } else {
577 debug!(
578 radial_root = %identity,
579 occurrence = call_key.1,
580 current_epsilon = %current.precision_epsilon,
581 current_residual = %current.residual,
582 "radial root has no matching lower-precision observation"
583 );
584 }
585
586 self.record_observation(call_key, current);
587 solve_result
588 }
589}
590
591#[derive(Clone, Debug)]
592pub(crate) enum SafeguardedNewtonError<T: FloatLike> {
593 InvalidInside {
594 radius: F<T>,
595 value: F<T>,
596 },
597 InvalidOutside {
598 radius: F<T>,
599 value: F<T>,
600 bracket_expansions: usize,
601 },
602 InvalidDerivative {
603 result: NewtonIterationResult<T>,
604 },
605 DidNotConverge {
606 result: NewtonIterationResult<T>,
607 lower_bound: F<T>,
608 upper_bound: F<T>,
609 },
610}
611
612impl<T: FloatLike> Display for SafeguardedNewtonError<T> {
613 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
614 match self {
615 Self::InvalidInside { radius, value } => write!(
616 formatter,
617 "inside radius {radius} has non-finite or non-negative value {value}"
618 ),
619 Self::InvalidOutside {
620 radius,
621 value,
622 bracket_expansions,
623 } => write!(
624 formatter,
625 "outside radius {radius} has invalid value {value} after {bracket_expansions} bracket expansions"
626 ),
627 Self::InvalidDerivative { result } => write!(
628 formatter,
629 "root candidate {} has invalid derivative {} with residual {} after {} iterations",
630 result.solution,
631 result.derivative_at_solution,
632 result.error_of_function,
633 result.num_iterations_used,
634 ),
635 Self::DidNotConverge {
636 result,
637 lower_bound,
638 upper_bound,
639 } => write!(
640 formatter,
641 "did not converge in bracket [{lower_bound}, {upper_bound}]: candidate {}, derivative {}, residual {} after {} iterations",
642 result.solution,
643 result.derivative_at_solution,
644 result.error_of_function,
645 result.num_iterations_used,
646 ),
647 }
648 }
649}
650
651fn is_finite<T: FloatLike>(value: &F<T>) -> bool {
652 !value.is_nan() && !value.is_infinite()
653}
654
655pub(crate) fn safeguarded_newton_iteration_and_derivative<T: FloatLike>(
661 inside_radius: &F<T>,
662 outside_radius_guess: &F<T>,
663 f_x_and_df_x: impl Fn(&F<T>) -> (F<T>, F<T>),
664 tolerance: &F<T>,
665 max_iterations: usize,
666 max_bracket_expansions: usize,
667 e_cm: &F<T>,
668) -> Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
669 let zero = inside_radius.zero();
670 let two = inside_radius.from_i64(2);
671 let maximum_residual = inside_radius.epsilon() * tolerance * e_cm;
672 let (inside_value, _) = f_x_and_df_x(inside_radius);
673 if !is_finite(&inside_value) || inside_value >= -maximum_residual.clone() {
674 return Err(SafeguardedNewtonError::InvalidInside {
675 radius: inside_radius.clone(),
676 value: inside_value,
677 });
678 }
679
680 let mut lower_bound = inside_radius.clone();
681 let mut upper_bound = outside_radius_guess.clone();
682 if !is_finite(&upper_bound) || upper_bound <= lower_bound {
683 let upper_value = if is_finite(&upper_bound) {
684 f_x_and_df_x(&upper_bound).0
685 } else {
686 upper_bound.clone()
687 };
688 return Err(SafeguardedNewtonError::InvalidOutside {
689 radius: upper_bound,
690 value: upper_value,
691 bracket_expansions: 0,
692 });
693 }
694
695 let (mut upper_value, _) = f_x_and_df_x(&upper_bound);
696 let mut bracket_expansions = 0;
697 while is_finite(&upper_value)
698 && upper_value <= zero
699 && bracket_expansions < max_bracket_expansions
700 {
701 upper_bound *= &two;
702 (upper_value, _) = f_x_and_df_x(&upper_bound);
703 bracket_expansions += 1;
704 }
705
706 if !is_finite(&upper_bound) || !is_finite(&upper_value) || upper_value <= zero {
707 return Err(SafeguardedNewtonError::InvalidOutside {
708 radius: upper_bound,
709 value: upper_value,
710 bracket_expansions,
711 });
712 }
713
714 let mut solution = upper_bound.clone();
715 let (mut value, mut derivative) = f_x_and_df_x(&solution);
716 for iteration in 0..=max_iterations {
717 let current_result = NewtonIterationResult {
718 solution: solution.clone(),
719 derivative_at_solution: derivative.clone(),
720 error_of_function: value.clone(),
721 num_iterations_used: iteration,
722 };
723
724 if is_finite(&value) && value.abs() <= maximum_residual {
725 if is_finite(&derivative) && derivative > zero {
726 return Ok(current_result);
727 }
728 return Err(SafeguardedNewtonError::InvalidDerivative {
729 result: current_result,
730 });
731 }
732
733 if iteration == max_iterations {
734 return Err(SafeguardedNewtonError::DidNotConverge {
735 result: current_result,
736 lower_bound,
737 upper_bound,
738 });
739 }
740
741 if is_finite(&value) {
742 if value < zero {
743 lower_bound = solution.clone();
744 } else {
745 upper_bound = solution.clone();
746 }
747 }
748
749 let midpoint = (&lower_bound + &upper_bound) / &two;
750 if iteration >= 4 && (midpoint == lower_bound || midpoint == upper_bound) {
754 return Err(SafeguardedNewtonError::DidNotConverge {
755 result: current_result,
756 lower_bound,
757 upper_bound,
758 });
759 }
760 let newton_candidate = if is_finite(&value) && is_finite(&derivative) && derivative > zero {
761 Some(&solution - &value / &derivative)
762 } else {
763 None
764 };
765
766 solution = match newton_candidate {
767 Some(candidate)
768 if is_finite(&candidate) && candidate > lower_bound && candidate < upper_bound =>
769 {
770 candidate
771 }
772 _ => midpoint,
773 };
774 (value, derivative) = f_x_and_df_x(&solution);
775 }
776
777 unreachable!("safeguarded Newton loop always returns")
778}
779
780pub(crate) fn newton_iteration_and_derivative<T: FloatLike>(
783 guess: &F<T>,
784 f_x_and_df_x: impl Fn(&F<T>) -> (F<T>, F<T>),
785 tolerance: &F<T>,
786 max_iterations: usize,
787 e_cm: &F<T>,
788) -> NewtonIterationResult<T> {
789 let mut x = guess.clone();
790 let (mut val_f_x, mut val_df_x) = f_x_and_df_x(&x);
791
792 let mut iteration = 0;
793
794 while iteration < max_iterations && val_f_x.abs() > guess.epsilon() * tolerance * e_cm {
795 x -= val_f_x / val_df_x;
796 (val_f_x, val_df_x) = f_x_and_df_x(&x);
797 iteration += 1;
798 }
799
800 NewtonIterationResult {
801 solution: x,
802 derivative_at_solution: val_df_x,
803 error_of_function: val_f_x,
804 num_iterations_used: iteration,
805 }
806}
807
808#[allow(dead_code)]
809pub(crate) fn newton_iteration_and_derivative_dual<T: FloatLike>(
810 guess: &HyperDual<F<T>>,
811 f_x_and_df_x: impl Fn(&HyperDual<F<T>>) -> (HyperDual<F<T>>, HyperDual<F<T>>),
812 tolerance: &F<T>,
813 max_iterations: usize,
814 e_cm: &F<T>,
815) -> NewtonIterationResultDual<T> {
816 let mut x = guess.clone();
817 let (mut val_f_x, mut val_df_x) = f_x_and_df_x(&x);
818
819 let mut iteration = 0;
820
821 while iteration < max_iterations
822 && val_f_x.values[0].abs() > guess.values[0].epsilon() * tolerance * e_cm
823 {
824 x -= val_f_x / val_df_x;
825 (val_f_x, val_df_x) = f_x_and_df_x(&x);
826 iteration += 1;
827 }
828
829 NewtonIterationResultDual {
830 solution: x,
831 derivative_at_solution: val_df_x,
832 error_of_function: val_f_x.values[0].clone(),
833 num_iterations_used: iteration,
834 }
835}
836
837#[derive(Serialize, Clone, Debug)]
838pub(crate) struct NewtonIterationResult<T: FloatLike> {
839 pub solution: F<T>,
840 pub derivative_at_solution: F<T>,
841 pub error_of_function: F<T>,
842 pub num_iterations_used: usize,
843}
844
845#[derive(Clone, Debug)]
846#[allow(dead_code)]
847pub(crate) struct NewtonIterationResultDual<T: FloatLike> {
848 pub solution: HyperDual<F<T>>,
849 pub derivative_at_solution: HyperDual<F<T>>,
850 pub error_of_function: F<T>,
851 pub num_iterations_used: usize,
852}
853
854#[cfg(test)]
855mod tests {
856 use std::cell::Cell;
857
858 use super::*;
859 use crate::{
860 momentum::ThreeMomentum,
861 utils::{ArbPrec, f128},
862 };
863
864 const ROOT_TOLERANCE: i64 = 64;
865
866 fn cancellation_limited_root<T: FloatLike>(
867 diagnostics: &mut RadialRootDiagnostics,
868 identity: &RadialRootIdentity,
869 large_momentum: f64,
870 target: f64,
871 ) -> Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
872 let zero = F::<T>::default();
873 let one = zero.one();
874 let target = F::<T>::from_f64(target);
875 let k3 = F::<T>::from_f64(large_momentum);
876 let k2 = F::<T>::from_f64(large_momentum + 1.0);
877 let derivative = &k2 - &k3;
878 let tolerance = zero.from_i64(ROOT_TOLERANCE);
879
880 diagnostics.solve(
881 identity,
882 &zero,
883 &one,
884 |radius| (radius * &k2 - radius * &k3 - &target, derivative.clone()),
885 &tolerance,
886 40,
887 64,
888 &one,
889 )
890 }
891
892 fn discontinuous_root<T: FloatLike>(
893 diagnostics: &mut RadialRootDiagnostics,
894 identity: &RadialRootIdentity,
895 positive_residual: f64,
896 ) -> Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
897 let zero = F::<T>::default();
898 let one = zero.one();
899 let half = &one / one.from_i64(2);
900 let positive_residual = F::<T>::from_f64(positive_residual);
901 let tolerance = zero.from_i64(ROOT_TOLERANCE);
902
903 diagnostics.solve(
904 identity,
905 &zero,
906 &one,
907 |radius| {
908 let value = if radius == &zero {
909 -one.clone()
910 } else if radius < &half {
911 -positive_residual.clone()
912 } else {
913 positive_residual.clone()
914 };
915 (value, one.clone())
916 },
917 &tolerance,
918 40,
919 64,
920 &one,
921 )
922 }
923
924 #[test]
925 fn safeguarded_newton_expands_bracket_and_converges() {
926 let result = safeguarded_newton_iteration_and_derivative(
927 &F(0.0),
928 &F(0.5),
929 |x| (x * x - F(2.0), F(2.0) * x),
930 &F(8.0),
931 40,
932 64,
933 &F(1.0),
934 )
935 .unwrap();
936
937 let expected_solution = result.solution.from_i64(2).sqrt();
938 let solution_tolerance = result.solution.epsilon() * result.solution.from_i64(64);
939 assert!((result.solution - expected_solution).abs() <= solution_tolerance);
940
941 let residual_tolerance =
942 result.error_of_function.epsilon() * result.error_of_function.from_i64(8);
943 assert!(result.error_of_function.abs() <= residual_tolerance);
944 assert!(result.derivative_at_solution > result.derivative_at_solution.zero());
945 }
946
947 #[test]
948 fn safeguarded_newton_accepts_a_few_ulp_residual() {
949 let residual = 2.2737367544323206e-13;
950 let result = safeguarded_newton_iteration_and_derivative(
951 &F(0.0),
952 &F(1.0),
953 |x| {
954 let value = if x == &F(1.0) {
955 F(residual)
956 } else {
957 x - F(1.0)
958 };
959 (value, F(1.0))
960 },
961 &F(8.0),
962 40,
963 64,
964 &F(1000.0),
965 )
966 .unwrap();
967
968 assert_eq!(result.solution, F(1.0));
969 assert_eq!(result.error_of_function, F(residual));
970 }
971
972 #[test]
973 fn safeguarded_newton_rejects_a_large_discontinuous_residual() {
974 let error = safeguarded_newton_iteration_and_derivative(
975 &F(0.0),
976 &F(1.0),
977 |x| {
978 if x < &F(1.0) {
979 (F(-1.0), F(1.0))
980 } else {
981 (F(741.0), F(1.0))
982 }
983 },
984 &F(8.0),
985 20,
986 64,
987 &F(1000.0),
988 )
989 .unwrap_err();
990
991 assert!(matches!(
992 error,
993 SafeguardedNewtonError::DidNotConverge { .. }
994 ));
995 }
996
997 #[test]
998 fn safeguarded_newton_rejects_a_non_interior_center() {
999 let error = safeguarded_newton_iteration_and_derivative(
1000 &F(0.0),
1001 &F(1.0),
1002 |x| (*x, F(1.0)),
1003 &F(8.0),
1004 40,
1005 64,
1006 &F(1.0),
1007 )
1008 .unwrap_err();
1009
1010 assert!(matches!(
1011 error,
1012 SafeguardedNewtonError::InvalidInside { .. }
1013 ));
1014 }
1015
1016 #[test]
1017 fn safeguarded_newton_rejects_an_invalid_outside_bracket() {
1018 let error = safeguarded_newton_iteration_and_derivative(
1019 &F(0.0),
1020 &F(1.0),
1021 |_| (F(-1.0), F(1.0)),
1022 &F(8.0),
1023 40,
1024 4,
1025 &F(1.0),
1026 )
1027 .unwrap_err();
1028
1029 assert!(matches!(
1030 error,
1031 SafeguardedNewtonError::InvalidOutside {
1032 bracket_expansions: 4,
1033 ..
1034 }
1035 ));
1036 }
1037
1038 #[test]
1039 fn safeguarded_newton_reports_invalid_outside_endpoint_value() {
1040 let error = safeguarded_newton_iteration_and_derivative(
1041 &F(1.0),
1042 &F(0.5),
1043 |x| (x - F(2.0), F(1.0)),
1044 &F(8.0),
1045 40,
1046 64,
1047 &F(1.0),
1048 )
1049 .unwrap_err();
1050
1051 assert!(matches!(
1052 error,
1053 SafeguardedNewtonError::InvalidOutside {
1054 radius: F(0.5),
1055 value: F(-1.5),
1056 bracket_expansions: 0,
1057 }
1058 ));
1059 }
1060
1061 #[test]
1062 fn safeguarded_newton_reports_non_finite_outside_radius() {
1063 let nan = F(0.0) / F(0.0);
1064 let error = safeguarded_newton_iteration_and_derivative(
1065 &F(0.0),
1066 &nan,
1067 |x| (x - F(1.0), F(1.0)),
1068 &F(8.0),
1069 40,
1070 64,
1071 &F(1.0),
1072 )
1073 .unwrap_err();
1074
1075 match error {
1076 SafeguardedNewtonError::InvalidOutside {
1077 radius,
1078 value,
1079 bracket_expansions: 0,
1080 } => {
1081 assert!(radius.is_nan());
1082 assert!(value.is_nan());
1083 }
1084 unexpected => panic!("unexpected safeguarded Newton error: {unexpected:?}"),
1085 }
1086 }
1087
1088 #[test]
1089 fn safeguarded_newton_rejects_an_invalid_root_derivative() {
1090 let error = safeguarded_newton_iteration_and_derivative(
1091 &F(0.0),
1092 &F(1.0),
1093 |x| (x - F(1.0), F(0.0)),
1094 &F(8.0),
1095 40,
1096 64,
1097 &F(1.0),
1098 )
1099 .unwrap_err();
1100
1101 assert!(matches!(
1102 error,
1103 SafeguardedNewtonError::InvalidDerivative { .. }
1104 ));
1105 let diagnosed = RadialRootDiagnostics::default()
1106 .solve(
1107 &RadialRootIdentity::new("structurally invalid derivative".into()),
1108 &F(0.0),
1109 &F(1.0),
1110 |x| (x - F(1.0), F(0.0)),
1111 &F(8.0),
1112 40,
1113 64,
1114 &F(1.0),
1115 )
1116 .unwrap_err();
1117 assert_eq!(format!("{diagnosed:?}"), format!("{error:?}"));
1118 }
1119
1120 #[test]
1121 fn local_root_consistency_has_bounded_evaluation_cost() {
1122 let result = NewtonIterationResult {
1123 solution: F(0.5),
1124 derivative_at_solution: F(1.0),
1125 error_of_function: F(1.0e-20),
1126 num_iterations_used: 40,
1127 };
1128 let calls = Cell::new(0);
1129 let consistency = LocalRootConsistency::check(&result, &F(0.0), &|radius| {
1130 calls.set(calls.get() + 1);
1131 (radius - F(0.5), F(1.0))
1132 });
1133 assert!(consistency.is_valid);
1134 assert_eq!(calls.get(), 4);
1135
1136 let short_solve_result = NewtonIterationResult {
1137 num_iterations_used: 3,
1138 ..result
1139 };
1140 calls.set(0);
1141 let consistency = LocalRootConsistency::check(&short_solve_result, &F(0.0), &|radius| {
1142 calls.set(calls.get() + 1);
1143 (radius - F(0.5), F(1.0))
1144 });
1145 assert!(!consistency.is_valid);
1146 assert_eq!(calls.get(), 0);
1147 }
1148
1149 #[test]
1150 fn exhausted_bracket_resolves_a_stalled_lu_energy_root() {
1151 fn solve_cut<T: FloatLike>(
1152 diagnostics: &mut RadialRootDiagnostics,
1153 identity: &RadialRootIdentity,
1154 ) -> [Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>>; 2] {
1155 let zero = F::<T>::default();
1156 let p = ThreeMomentum::new(
1157 zero.from_i64(-1086),
1158 zero.from_i64(679),
1159 zero.from_i64(1658),
1160 );
1161 let q = ThreeMomentum::new(
1162 zero.from_i64(-1141),
1163 zero.from_i64(-1647),
1164 zero.from_i64(-110),
1165 );
1166 let pq = &p + &q;
1167 let e_cm = zero.from_i64(1000);
1168 let guess = &e_cm / (p.norm() + q.norm() + pq.norm());
1169 let function = |radius: &F<T>| {
1170 let rp = &p * radius;
1171 let rq = &q * radius;
1172 let rpq = &rp + &rq;
1173 let (energy_sum, derivative) = [(rp, &p, 125), (rq, &q, 173), (rpq, &pq, 173)]
1174 .into_iter()
1175 .fold(
1176 (zero.clone(), zero.clone()),
1177 |(sum, derivative), (mom, ray, mass)| {
1178 let mass = zero.from_i64(mass);
1179 let energy = (mom.norm_squared() + &mass * &mass).sqrt();
1180 (sum + &energy, derivative + mom * ray / energy)
1181 },
1182 );
1183 (energy_sum - &e_cm, derivative)
1184 };
1185 let strict = safeguarded_newton_iteration_and_derivative(
1186 &zero,
1187 &guess,
1188 function,
1189 &zero.one(),
1190 2000,
1191 64,
1192 &e_cm,
1193 );
1194 let diagnosed = diagnostics.solve(
1195 identity,
1196 &zero,
1197 &guess,
1198 function,
1199 &zero.one(),
1200 2000,
1201 64,
1202 &e_cm,
1203 );
1204 [strict, diagnosed]
1205 }
1206
1207 let identity = RadialRootIdentity::new("roundoff-limited ttH LU cut".to_string());
1211 let mut diagnostics = RadialRootDiagnostics::default();
1212 let [strict, diagnosed] = solve_cut::<f64>(&mut diagnostics, &identity);
1213 let error = strict.unwrap_err();
1214 let SafeguardedNewtonError::DidNotConverge {
1215 result,
1216 lower_bound,
1217 upper_bound,
1218 } = error
1219 else {
1220 panic!("expected a localized roundoff-limited root, got {error:?}");
1221 };
1222 assert!(result.num_iterations_used < 100);
1223 assert!(result.error_of_function.abs() > F(f64::EPSILON * 1000.0));
1224 let midpoint = (lower_bound + upper_bound) / F(2.0);
1225 assert!(midpoint == lower_bound || midpoint == upper_bound);
1226 assert!(result.derivative_at_solution > F(0.0));
1227
1228 let certified = diagnosed.unwrap();
1229 assert_eq!(certified.solution, result.solution);
1230 assert_eq!(certified.error_of_function, result.error_of_function);
1231 assert!(
1232 certified.error_of_function.abs()
1233 <= F(f64::EPSILON * 1000.0)
1234 + certified.derivative_at_solution * (upper_bound - lower_bound)
1235 );
1236
1237 let [_, resolved] = solve_cut::<f128>(&mut diagnostics, &identity);
1238 let resolved = resolved.unwrap();
1239 assert!(resolved.error_of_function.abs() < F::from_f64(f64::EPSILON * 1000.0));
1240 assert!(resolved.derivative_at_solution > resolved.solution.zero());
1241 assert!(
1242 (resolved.solution - F::from_f64(result.solution.0)).abs()
1243 < F::from_f64(f64::EPSILON * result.solution.0)
1244 );
1245
1246 let [_, independent_arb] =
1248 solve_cut::<ArbPrec>(&mut RadialRootDiagnostics::default(), &identity);
1249 let independent_arb = independent_arb.unwrap();
1250 assert!(independent_arb.derivative_at_solution > independent_arb.solution.zero());
1251 assert!(
1252 (independent_arb.solution - F::from_f64(result.solution.0)).abs()
1253 < F::from_f64(f64::EPSILON * result.solution.0)
1254 );
1255 }
1256
1257 #[test]
1258 fn exhausted_bracket_rejects_a_small_discontinuous_residual() {
1259 let residual = F(1.125 * f64::EPSILON);
1260 let function = |radius: &F<f64>| {
1261 let value = if *radius == F(0.0) {
1262 F(-1.0)
1263 } else if *radius < F(0.5) {
1264 -residual
1265 } else {
1266 residual
1267 };
1268 let derivative = if *radius == F(1.0) { F(0.0) } else { F(1.0) };
1270 (value, derivative)
1271 };
1272 let error = RadialRootDiagnostics::default()
1273 .solve(
1274 &RadialRootIdentity::new("adjacent discontinuous non-root".to_string()),
1275 &F(0.0),
1276 &F(1.0),
1277 function,
1278 &F(1.0),
1279 2000,
1280 64,
1281 &F(1.0),
1282 )
1283 .unwrap_err();
1284 let SafeguardedNewtonError::DidNotConverge {
1285 result,
1286 lower_bound,
1287 upper_bound,
1288 } = error
1289 else {
1290 panic!("expected rejection of the discontinuity, got {error:?}");
1291 };
1292 let midpoint = (lower_bound + upper_bound) / F(2.0);
1293 assert!(midpoint == lower_bound || midpoint == upper_bound);
1294 assert!(residual > F(f64::EPSILON));
1295 assert!(
1296 residual
1297 <= F(f64::EPSILON) + result.derivative_at_solution * (upper_bound - lower_bound)
1298 );
1299 assert!(
1300 !LocalRootConsistency::check(&result, &F(0.0), &function).is_valid,
1301 "an exhausted bracket and a sufficiently small residual do not certify a discontinuity"
1302 );
1303 }
1304
1305 #[test]
1306 fn radial_root_accuracy_is_independent_of_energy_units() {
1307 let result = NewtonIterationResult {
1308 solution: F(0.25),
1309 derivative_at_solution: F(2.0),
1310 error_of_function: F(1.0e-15),
1311 num_iterations_used: 4,
1312 };
1313 let observation = RadialRootObservation::new(&result, None, None, &F(1.0), &F(1.0));
1314 let rescaled_result = NewtonIterationResult {
1315 derivative_at_solution: result.derivative_at_solution * F(1024.0),
1316 error_of_function: result.error_of_function * F(1024.0),
1317 ..result
1318 };
1319 let rescaled_observation =
1320 RadialRootObservation::new(&rescaled_result, None, None, &F(1.0), &F(1024.0));
1321 assert_eq!(
1322 observation.relative_newton_correction, rescaled_observation.relative_newton_correction,
1323 "changing the energy unit cannot change the accuracy of dimensionless LU t"
1324 );
1325 assert_eq!(
1326 observation.relative_residual_limit,
1327 rescaled_observation.relative_residual_limit
1328 );
1329 }
1330
1331 #[test]
1332 fn higher_precision_rescues_large_nearly_equal_momenta() {
1333 let mut diagnostics = RadialRootDiagnostics::default();
1334
1335 for exponent in [32, 40, 48, 52] {
1336 let identity = RadialRootIdentity::new(format!("cancellation at 2^{exponent}"));
1337 let large_momentum = 2.0_f64.powi(exponent);
1338 let f64_error = cancellation_limited_root::<f64>(
1339 &mut diagnostics,
1340 &identity,
1341 large_momentum,
1342 1.0 / 3.0,
1343 )
1344 .unwrap_err();
1345 assert!(matches!(
1346 f64_error,
1347 SafeguardedNewtonError::DidNotConverge { .. }
1348 ));
1349
1350 let rescued = cancellation_limited_root::<f128>(
1351 &mut diagnostics,
1352 &identity,
1353 large_momentum,
1354 1.0 / 3.0,
1355 )
1356 .unwrap();
1357 let active_precision_limit = rescued.solution.epsilon()
1358 * rescued.solution.from_i64(ROOT_TOLERANCE)
1359 * rescued.solution.one();
1360 assert!(
1361 rescued.error_of_function.abs() > active_precision_limit,
1362 "2^{exponent} case should exercise precision rescue rather than direct convergence"
1363 );
1364 }
1365 }
1366
1367 #[test]
1368 fn higher_precision_can_use_a_successful_lower_precision_baseline() {
1369 let mut diagnostics = RadialRootDiagnostics::default();
1370 let identity = RadialRootIdentity::new("successful f64 baseline".to_string());
1371 let target = 0.3;
1372
1373 let f64_result = diagnostics
1374 .solve(
1375 &identity,
1376 &F(0.0),
1377 &F(1.0),
1378 |radius| (radius - F(target), F(1.0)),
1379 &F(ROOT_TOLERANCE as f64),
1380 40,
1381 64,
1382 &F(1.0),
1383 )
1384 .unwrap();
1385 assert!(f64_result.error_of_function.abs() > F(0.0));
1386 assert!(
1387 f64_result.error_of_function.abs()
1388 <= f64_result.solution.epsilon()
1389 * F(ROOT_TOLERANCE as f64)
1390 * f64_result.solution.one()
1391 );
1392
1393 let rescued = cancellation_limited_root::<f128>(
1396 &mut diagnostics,
1397 &identity,
1398 2.0_f64.powi(52),
1399 target,
1400 )
1401 .unwrap();
1402 let active_precision_limit =
1403 rescued.solution.epsilon() * rescued.solution.from_i64(ROOT_TOLERANCE);
1404 assert!(
1405 rescued.error_of_function.abs() > active_precision_limit,
1406 "the f128 solve should require the successful f64 observation as its baseline"
1407 );
1408 }
1409
1410 #[test]
1411 fn higher_precision_uses_the_roundoff_floor_after_an_exact_lower_precision_zero() {
1412 let mut diagnostics = RadialRootDiagnostics::default();
1413 let identity = RadialRootIdentity::new("accidental exact f64 residual".to_string());
1414
1415 let f64_result = diagnostics
1418 .solve(
1419 &identity,
1420 &F(0.0),
1421 &F(1.0),
1422 |radius| (radius - F(0.3), F(1.0)),
1423 &F(0.2),
1424 40,
1425 64,
1426 &F(1.0),
1427 )
1428 .unwrap();
1429 assert_eq!(f64_result.error_of_function, F(0.0));
1430
1431 let rescued =
1432 cancellation_limited_root::<f128>(&mut diagnostics, &identity, 2.0_f64.powi(52), 0.3)
1433 .unwrap();
1434 let active_precision_limit =
1435 rescued.solution.epsilon() * rescued.solution.from_i64(ROOT_TOLERANCE);
1436 assert!(rescued.error_of_function.abs() > active_precision_limit);
1437 }
1438
1439 #[test]
1440 fn repeated_root_identities_are_paired_by_precision_local_occurrence() {
1441 let mut diagnostics = RadialRootDiagnostics::default();
1442 let identity = RadialRootIdentity::new("two LMB channel rays".to_string());
1443 let f64_precision: SymbolicaFloat = F::<f64>::default().into();
1444 let f128_precision: SymbolicaFloat = F::<f128>::default().into();
1445
1446 assert_eq!(
1447 diagnostics.next_call_key(&identity, &F::<f64>::default()),
1448 (identity.clone(), 0, f64_precision.prec())
1449 );
1450 assert_eq!(
1451 diagnostics.next_call_key(&identity, &F::<f64>::default()),
1452 (identity.clone(), 1, f64_precision.prec())
1453 );
1454 assert_eq!(
1455 diagnostics.next_call_key(&identity, &F::<f128>::default()),
1456 (identity.clone(), 0, f128_precision.prec())
1457 );
1458 assert_eq!(
1459 diagnostics.next_call_key(&identity, &F::<f128>::default()),
1460 (identity.clone(), 1, f128_precision.prec())
1461 );
1462 diagnostics.restart_precision_pass();
1463 assert_eq!(
1464 diagnostics.next_call_key(&identity, &F::<f128>::default()),
1465 (identity.clone(), 0, f128_precision.prec())
1466 );
1467 assert_eq!(
1468 diagnostics.next_call_key(&identity, &F::<f128>::default()),
1469 (identity, 1, f128_precision.prec())
1470 );
1471 }
1472
1473 #[test]
1474 fn precision_rescue_replays_at_the_same_precision() {
1475 let mut diagnostics = RadialRootDiagnostics::default();
1476 let identity = RadialRootIdentity::new("precise result replay".to_string());
1477 let large_momentum = 2.0_f64.powi(52);
1478
1479 assert!(matches!(
1480 cancellation_limited_root::<f64>(
1481 &mut diagnostics,
1482 &identity,
1483 large_momentum,
1484 1.0 / 3.0,
1485 ),
1486 Err(SafeguardedNewtonError::DidNotConverge { .. })
1487 ));
1488 let rescued = cancellation_limited_root::<f128>(
1489 &mut diagnostics,
1490 &identity,
1491 large_momentum,
1492 1.0 / 3.0,
1493 )
1494 .unwrap();
1495 diagnostics.restart_precision_pass();
1496 let replayed = cancellation_limited_root::<f128>(
1497 &mut diagnostics,
1498 &identity,
1499 large_momentum,
1500 1.0 / 3.0,
1501 )
1502 .unwrap();
1503
1504 for result in [rescued, replayed] {
1505 let active_precision_limit =
1506 result.solution.epsilon() * result.solution.from_i64(ROOT_TOLERANCE);
1507 assert!(result.error_of_function.abs() > active_precision_limit);
1508 }
1509 }
1510
1511 #[test]
1512 fn multiple_roots_are_rescued_in_one_higher_precision_pass() {
1513 let mut diagnostics = RadialRootDiagnostics::default();
1514 let identities = (0..3)
1515 .map(|index| RadialRootIdentity::new(format!("difficult root {index}")))
1516 .collect::<Vec<_>>();
1517
1518 for identity in &identities {
1519 assert!(matches!(
1520 cancellation_limited_root::<f64>(
1521 &mut diagnostics,
1522 identity,
1523 2.0_f64.powi(52),
1524 1.0 / 3.0,
1525 ),
1526 Err(SafeguardedNewtonError::DidNotConverge { .. })
1527 ));
1528 }
1529 for identity in &identities {
1530 cancellation_limited_root::<f128>(
1531 &mut diagnostics,
1532 identity,
1533 2.0_f64.powi(52),
1534 1.0 / 3.0,
1535 )
1536 .unwrap();
1537 }
1538 }
1539
1540 #[test]
1541 fn precision_rescue_rejects_a_stable_discontinuous_residual() {
1542 let mut diagnostics = RadialRootDiagnostics::default();
1543 let identity = RadialRootIdentity::new("stable discontinuity".to_string());
1544
1545 assert!(matches!(
1546 discontinuous_root::<f64>(&mut diagnostics, &identity, 1.0),
1547 Err(SafeguardedNewtonError::DidNotConverge { .. })
1548 ));
1549 assert!(matches!(
1550 discontinuous_root::<f128>(&mut diagnostics, &identity, 1.0),
1551 Err(SafeguardedNewtonError::DidNotConverge { .. })
1552 ));
1553 }
1554
1555 #[test]
1556 fn precision_rescue_rejects_an_improved_but_large_residual() {
1557 let mut diagnostics = RadialRootDiagnostics::default();
1558 let identity = RadialRootIdentity::new("shrinking discontinuity".to_string());
1559
1560 assert!(matches!(
1561 discontinuous_root::<f64>(&mut diagnostics, &identity, 1.0),
1562 Err(SafeguardedNewtonError::DidNotConverge { .. })
1563 ));
1564 assert!(matches!(
1565 discontinuous_root::<f128>(&mut diagnostics, &identity, 0.01),
1566 Err(SafeguardedNewtonError::DidNotConverge { .. })
1567 ));
1568 }
1569
1570 #[test]
1571 fn precision_rescue_rejects_an_improving_discontinuous_non_root() {
1572 let mut diagnostics = RadialRootDiagnostics::default();
1573 let identity = RadialRootIdentity::new("improving discontinuity".to_string());
1574 let f64_precision: SymbolicaFloat = F::<f64>::default().into();
1575 let f128_precision: SymbolicaFloat = F::<f128>::default().into();
1576 let lower_key = (identity.clone(), 0, f64_precision.prec());
1577 let higher_key = (identity.clone(), 0, f128_precision.prec());
1578
1579 assert!(matches!(
1580 discontinuous_root::<f64>(&mut diagnostics, &identity, 1.0e-12),
1581 Err(SafeguardedNewtonError::DidNotConverge { .. })
1582 ));
1583 let lower_precision = diagnostics.observations[&lower_key].clone();
1584
1585 assert!(matches!(
1586 discontinuous_root::<f128>(&mut diagnostics, &identity, 5.0e-15),
1587 Err(SafeguardedNewtonError::DidNotConverge { .. })
1588 ));
1589 let higher_precision = &diagnostics.observations[&higher_key];
1590
1591 let residual_improvement =
1595 lower_precision.comparison_residual() / &higher_precision.residual;
1596 let required_improvement = SymbolicaFloat::with_val(
1597 residual_improvement.prec(),
1598 MINIMUM_PRECISION_RESIDUAL_IMPROVEMENT,
1599 );
1600 assert!(residual_improvement >= required_improvement);
1601 assert!(higher_precision.residual <= lower_precision.maximum_residual);
1602 assert!(
1603 higher_precision.relative_newton_correction <= lower_precision.relative_residual_limit
1604 );
1605 assert!(higher_precision.bracket_is_valid);
1606 assert!(
1607 !higher_precision
1608 .local_consistency
1609 .as_ref()
1610 .expect("residual-limited candidates have local diagnostics")
1611 .is_valid
1612 );
1613 assert!(
1614 higher_precision
1615 .precision_rescue_improvement(&lower_precision)
1616 .is_none()
1617 );
1618 }
1619}