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 e_cm: &F<T>,
57 ) -> Self {
58 const PROBE_EVALUATIONS: usize = 4;
62 if result.num_iterations_used < PROBE_EVALUATIONS {
63 return Self::invalid();
64 }
65
66 let zero = result.solution.zero();
67 let two = result.solution.from_i64(2);
68 let four = result.solution.from_i64(4);
69 let eight = result.solution.from_i64(8);
70 let radial_scale = result
71 .solution
72 .abs()
73 .max(e_cm.abs())
74 .max(result.solution.epsilon());
75 let newton_correction =
76 result.error_of_function.abs() / result.derivative_at_solution.abs();
77 let mut near_radius_step =
78 (result.solution.epsilon().sqrt() * radial_scale).max(&four * newton_correction);
79 let available_radius = &result.solution - inside_radius;
80 if !is_finite(&available_radius) || available_radius <= zero {
81 return Self::invalid();
82 }
83 let maximum_far_radius_step = &available_radius / &two;
84 if &near_radius_step * &eight > maximum_far_radius_step {
85 near_radius_step = &maximum_far_radius_step / &eight;
86 }
87 let far_radius_step = &near_radius_step * &eight;
88 if !is_finite(&near_radius_step)
89 || near_radius_step <= zero
90 || !is_finite(&far_radius_step)
91 || far_radius_step <= near_radius_step
92 {
93 return Self::invalid();
94 }
95
96 let probe = |radius_step: &F<T>| {
97 let lower_radius = &result.solution - radius_step;
98 let upper_radius = &result.solution + radius_step;
99 let (lower_value, _) = f_x_and_df_x(&lower_radius);
100 let (upper_value, _) = f_x_and_df_x(&upper_radius);
101 let secant_derivative = (&upper_value - &lower_value) / (&two * radius_step);
102 let secant_derivative_ratio = &secant_derivative / &result.derivative_at_solution;
103 (
104 lower_value,
105 upper_value,
106 secant_derivative,
107 secant_derivative_ratio,
108 )
109 };
110 let (
111 near_lower_value,
112 near_upper_value,
113 near_secant_derivative,
114 near_secant_derivative_ratio,
115 ) = probe(&near_radius_step);
116 let (far_lower_value, far_upper_value, far_secant_derivative, far_secant_derivative_ratio) =
117 probe(&far_radius_step);
118 let secant_ratio_agreement = &far_secant_derivative_ratio / &near_secant_derivative_ratio;
119 let half = result.solution.from_i64(1) / &two;
120 let two_again = two.clone();
121 let three_quarters = result.solution.from_i64(3) / &four;
122 let five_quarters = result.solution.from_i64(5) / &four;
123 let is_valid = is_finite(&near_lower_value)
124 && is_finite(&near_upper_value)
125 && is_finite(&near_secant_derivative)
126 && is_finite(&near_secant_derivative_ratio)
127 && is_finite(&far_lower_value)
128 && is_finite(&far_upper_value)
129 && is_finite(&far_secant_derivative)
130 && is_finite(&far_secant_derivative_ratio)
131 && is_finite(&secant_ratio_agreement)
132 && near_lower_value < zero
133 && near_upper_value > zero
134 && far_lower_value < zero
135 && far_upper_value > zero
136 && near_secant_derivative > zero
137 && far_secant_derivative > zero
138 && near_secant_derivative_ratio >= half
139 && near_secant_derivative_ratio <= two_again
140 && far_secant_derivative_ratio >= half
141 && far_secant_derivative_ratio <= two_again
142 && secant_ratio_agreement >= three_quarters
143 && secant_ratio_agreement <= five_quarters;
144
145 Self {
146 near_radius_step: Some(near_radius_step.into()),
147 near_lower_value: Some(near_lower_value.into()),
148 near_upper_value: Some(near_upper_value.into()),
149 near_secant_derivative_ratio: Some(near_secant_derivative_ratio.into()),
150 far_secant_derivative_ratio: Some(far_secant_derivative_ratio.into()),
151 secant_ratio_agreement: Some(secant_ratio_agreement.into()),
152 is_valid,
153 }
154 }
155}
156
157#[derive(Clone, Debug)]
158struct RadialRootObservation {
159 precision_bits: u32,
160 precision_epsilon: SymbolicaFloat,
161 residual: SymbolicaFloat,
162 roundoff_residual_scale: SymbolicaFloat,
163 maximum_residual: SymbolicaFloat,
164 solution: SymbolicaFloat,
165 derivative: SymbolicaFloat,
166 lower_bound: Option<SymbolicaFloat>,
167 upper_bound: Option<SymbolicaFloat>,
168 bracket_is_valid: bool,
169 local_consistency: Option<LocalRootConsistency>,
170 relative_newton_correction: SymbolicaFloat,
171 relative_residual_limit: SymbolicaFloat,
172}
173
174impl RadialRootObservation {
175 fn new<T: FloatLike>(
176 result: &NewtonIterationResult<T>,
177 bracket: Option<(&F<T>, &F<T>)>,
178 local_consistency: Option<LocalRootConsistency>,
179 tolerance: &F<T>,
180 e_cm: &F<T>,
181 ) -> Self {
182 let precision_epsilon_t = result.solution.epsilon();
183 let residual_t = result.error_of_function.abs();
184 let derivative_abs_t = result.derivative_at_solution.abs();
185 let e_cm_t = e_cm.abs();
186 let tolerance_t = tolerance.abs();
187 let radial_scale_t = result
188 .solution
189 .abs()
190 .max(e_cm_t.clone())
191 .max(precision_epsilon_t.clone());
192 let roundoff_residual_scale_t = &precision_epsilon_t * &e_cm_t;
193 let maximum_residual_t = &roundoff_residual_scale_t * &tolerance_t;
194 let relative_newton_correction_t = &residual_t / &derivative_abs_t / &radial_scale_t;
195 let relative_residual_limit_t = &precision_epsilon_t * &tolerance_t;
196
197 let precision_epsilon: SymbolicaFloat = precision_epsilon_t.into();
201 let precision_bits = precision_epsilon.prec();
202 let residual = residual_t.into();
203 let solution = result.solution.clone().into();
204 let derivative = result.derivative_at_solution.clone().into();
205 let roundoff_residual_scale = roundoff_residual_scale_t.into();
206 let maximum_residual = maximum_residual_t.into();
207 let relative_newton_correction = relative_newton_correction_t.into();
208 let relative_residual_limit = relative_residual_limit_t.into();
209
210 let (lower_bound, upper_bound, bracket_is_valid) =
211 bracket.map_or((None, None, true), |(lower_bound, upper_bound)| {
212 let bracket_is_valid = lower_bound <= upper_bound
213 && &result.solution >= lower_bound
214 && &result.solution <= upper_bound;
215 (
216 Some(lower_bound.clone().into()),
217 Some(upper_bound.clone().into()),
218 bracket_is_valid,
219 )
220 });
221
222 Self {
223 precision_bits,
224 precision_epsilon,
225 residual,
226 roundoff_residual_scale,
227 maximum_residual,
228 solution,
229 derivative,
230 lower_bound,
234 upper_bound,
235 bracket_is_valid,
236 local_consistency,
237 relative_newton_correction,
238 relative_residual_limit,
239 }
240 }
241
242 fn comparison_residual(&self) -> SymbolicaFloat {
243 if self.residual >= self.roundoff_residual_scale {
244 self.residual.clone()
245 } else {
246 self.roundoff_residual_scale.clone()
247 }
248 }
249
250 fn values_are_finite(&self) -> bool {
251 self.precision_epsilon.is_finite()
252 && self.residual.is_finite()
253 && self.roundoff_residual_scale.is_finite()
254 && self.maximum_residual.is_finite()
255 && self.solution.is_finite()
256 && self.derivative.is_finite()
257 && self.relative_newton_correction.is_finite()
258 && self.relative_residual_limit.is_finite()
259 && self
260 .lower_bound
261 .as_ref()
262 .is_none_or(SymbolicaFloat::is_finite)
263 && self
264 .upper_bound
265 .as_ref()
266 .is_none_or(SymbolicaFloat::is_finite)
267 }
268
269 fn precision_rescue_improvement(&self, previous: &Self) -> Option<SymbolicaFloat> {
270 let zero = SymbolicaFloat::with_val(self.precision_bits.max(previous.precision_bits), 0);
271 if !self.values_are_finite()
272 || !previous.values_are_finite()
273 || self.precision_bits <= previous.precision_bits
274 || self.solution <= zero
275 || self.derivative <= zero
276 || !self.bracket_is_valid
277 || !self
278 .local_consistency
279 .as_ref()
280 .is_some_and(|consistency| consistency.is_valid)
281 || previous.solution <= zero
282 || previous.derivative <= zero
283 || !previous.bracket_is_valid
284 || self.residual > previous.maximum_residual
285 || self.relative_newton_correction > previous.relative_residual_limit
286 || self.residual <= zero
287 {
288 return None;
289 }
290
291 let improvement = previous.comparison_residual() / self.residual.clone();
294 let required_improvement =
295 SymbolicaFloat::with_val(improvement.prec(), MINIMUM_PRECISION_RESIDUAL_IMPROVEMENT);
296 (improvement >= required_improvement).then_some(improvement)
297 }
298}
299
300#[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 pub(crate) fn restart_precision_pass(&mut self) {
354 self.current_precision_bits = None;
355 self.current_occurrences.clear();
356 }
357
358 #[allow(clippy::too_many_arguments)]
359 pub(crate) fn solve<T: FloatLike>(
360 &mut self,
361 identity: &RadialRootIdentity,
362 inside_radius: &F<T>,
363 outside_radius_guess: &F<T>,
364 f_x_and_df_x: impl Fn(&F<T>) -> (F<T>, F<T>),
365 tolerance: &F<T>,
366 max_iterations: usize,
367 max_bracket_expansions: usize,
368 e_cm: &F<T>,
369 ) -> Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
370 let call_key = self.next_call_key(identity, inside_radius);
371 let solve_result = safeguarded_newton_iteration_and_derivative(
372 inside_radius,
373 outside_radius_guess,
374 &f_x_and_df_x,
375 tolerance,
376 max_iterations,
377 max_bracket_expansions,
378 e_cm,
379 );
380
381 let (result, lower_bound, upper_bound) = match &solve_result {
382 Ok(result) => {
383 let observation = RadialRootObservation::new(result, None, None, tolerance, e_cm);
384 self.record_observation(call_key, observation);
385 return solve_result;
386 }
387 Err(SafeguardedNewtonError::DidNotConverge {
388 result,
389 lower_bound,
390 upper_bound,
391 }) => (result, lower_bound, upper_bound),
392 Err(error) => {
393 debug!(
394 radial_root = %identity,
395 occurrence = call_key.1,
396 error = %error,
397 "radial root failed a structural safeguarded-solver check"
398 );
399 return solve_result;
400 }
401 };
402
403 let previous = self
404 .observations
405 .range(
406 (call_key.0.clone(), call_key.1, 0)..(call_key.0.clone(), call_key.1, call_key.2),
407 )
408 .next_back()
409 .map(|(_, observation)| observation.clone());
410 let local_consistency = previous
411 .is_some()
412 .then(|| LocalRootConsistency::check(result, inside_radius, &f_x_and_df_x, e_cm));
413 let current = RadialRootObservation::new(
414 result,
415 Some((lower_bound, upper_bound)),
416 local_consistency,
417 tolerance,
418 e_cm,
419 );
420 let precision_improvement = previous
421 .as_ref()
422 .and_then(|previous| current.precision_rescue_improvement(previous));
423
424 if let Some(improvement) = precision_improvement {
425 let previous = previous.expect("a precision improvement requires a prior observation");
426 debug!(
427 radial_root = %identity,
428 occurrence = call_key.1,
429 previous_epsilon = %previous.precision_epsilon,
430 current_epsilon = %current.precision_epsilon,
431 previous_residual = %previous.residual,
432 previous_comparison_residual = %previous.comparison_residual(),
433 current_residual = %current.residual,
434 residual_improvement = %improvement,
435 accepted_residual = %previous.maximum_residual,
436 relative_newton_correction = %current.relative_newton_correction,
437 local_near_radius_step = ?current
438 .local_consistency
439 .as_ref()
440 .and_then(|consistency| consistency.near_radius_step.as_ref()),
441 local_near_lower_value = ?current
442 .local_consistency
443 .as_ref()
444 .and_then(|consistency| consistency.near_lower_value.as_ref()),
445 local_near_upper_value = ?current
446 .local_consistency
447 .as_ref()
448 .and_then(|consistency| consistency.near_upper_value.as_ref()),
449 local_near_secant_derivative_ratio = ?current
450 .local_consistency
451 .as_ref()
452 .and_then(|consistency| consistency.near_secant_derivative_ratio.as_ref()),
453 local_far_secant_derivative_ratio = ?current
454 .local_consistency
455 .as_ref()
456 .and_then(|consistency| consistency.far_secant_derivative_ratio.as_ref()),
457 local_secant_ratio_agreement = ?current
458 .local_consistency
459 .as_ref()
460 .and_then(|consistency| consistency.secant_ratio_agreement.as_ref()),
461 "accepted a residual-limited radial root after precision escalation"
462 );
463 self.record_observation(call_key, current);
464 return Ok(result.clone());
465 }
466
467 if let Some(previous) = previous.as_ref() {
468 let residual_improvement = previous.comparison_residual() / current.residual.clone();
469 debug!(
470 radial_root = %identity,
471 occurrence = call_key.1,
472 previous_epsilon = %previous.precision_epsilon,
473 current_epsilon = %current.precision_epsilon,
474 previous_residual = %previous.residual,
475 previous_comparison_residual = %previous.comparison_residual(),
476 current_residual = %current.residual,
477 residual_improvement = %residual_improvement,
478 accepted_residual = %previous.maximum_residual,
479 relative_newton_correction = %current.relative_newton_correction,
480 accepted_relative_newton_correction = %previous.relative_residual_limit,
481 solution = %current.solution,
482 derivative = %current.derivative,
483 lower_bound = ?current.lower_bound,
484 upper_bound = ?current.upper_bound,
485 bracket_is_valid = current.bracket_is_valid,
486 local_consistency = ?current.local_consistency,
487 "radial root did not satisfy precision-rescue criteria"
488 );
489 } else {
490 debug!(
491 radial_root = %identity,
492 occurrence = call_key.1,
493 current_epsilon = %current.precision_epsilon,
494 current_residual = %current.residual,
495 "radial root has no matching lower-precision observation"
496 );
497 }
498
499 self.record_observation(call_key, current);
500 solve_result
501 }
502}
503
504#[derive(Clone, Debug)]
505pub(crate) enum SafeguardedNewtonError<T: FloatLike> {
506 InvalidInside {
507 radius: F<T>,
508 value: F<T>,
509 },
510 InvalidOutside {
511 radius: F<T>,
512 value: F<T>,
513 bracket_expansions: usize,
514 },
515 InvalidDerivative {
516 result: NewtonIterationResult<T>,
517 },
518 DidNotConverge {
519 result: NewtonIterationResult<T>,
520 lower_bound: F<T>,
521 upper_bound: F<T>,
522 },
523}
524
525impl<T: FloatLike> Display for SafeguardedNewtonError<T> {
526 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
527 match self {
528 Self::InvalidInside { radius, value } => write!(
529 formatter,
530 "inside radius {radius} has non-finite or non-negative value {value}"
531 ),
532 Self::InvalidOutside {
533 radius,
534 value,
535 bracket_expansions,
536 } => write!(
537 formatter,
538 "outside radius {radius} has invalid value {value} after {bracket_expansions} bracket expansions"
539 ),
540 Self::InvalidDerivative { result } => write!(
541 formatter,
542 "root candidate {} has invalid derivative {} with residual {} after {} iterations",
543 result.solution,
544 result.derivative_at_solution,
545 result.error_of_function,
546 result.num_iterations_used,
547 ),
548 Self::DidNotConverge {
549 result,
550 lower_bound,
551 upper_bound,
552 } => write!(
553 formatter,
554 "did not converge in bracket [{lower_bound}, {upper_bound}]: candidate {}, derivative {}, residual {} after {} iterations",
555 result.solution,
556 result.derivative_at_solution,
557 result.error_of_function,
558 result.num_iterations_used,
559 ),
560 }
561 }
562}
563
564fn is_finite<T: FloatLike>(value: &F<T>) -> bool {
565 !value.is_nan() && !value.is_infinite()
566}
567
568pub(crate) fn safeguarded_newton_iteration_and_derivative<T: FloatLike>(
574 inside_radius: &F<T>,
575 outside_radius_guess: &F<T>,
576 f_x_and_df_x: impl Fn(&F<T>) -> (F<T>, F<T>),
577 tolerance: &F<T>,
578 max_iterations: usize,
579 max_bracket_expansions: usize,
580 e_cm: &F<T>,
581) -> Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
582 let zero = inside_radius.zero();
583 let two = inside_radius.from_i64(2);
584 let maximum_residual = inside_radius.epsilon() * tolerance * e_cm;
585 let (inside_value, _) = f_x_and_df_x(inside_radius);
586 if !is_finite(&inside_value) || inside_value >= -maximum_residual.clone() {
587 return Err(SafeguardedNewtonError::InvalidInside {
588 radius: inside_radius.clone(),
589 value: inside_value,
590 });
591 }
592
593 let mut lower_bound = inside_radius.clone();
594 let mut upper_bound = outside_radius_guess.clone();
595 if !is_finite(&upper_bound) || upper_bound <= lower_bound {
596 let upper_value = if is_finite(&upper_bound) {
597 f_x_and_df_x(&upper_bound).0
598 } else {
599 upper_bound.clone()
600 };
601 return Err(SafeguardedNewtonError::InvalidOutside {
602 radius: upper_bound,
603 value: upper_value,
604 bracket_expansions: 0,
605 });
606 }
607
608 let (mut upper_value, _) = f_x_and_df_x(&upper_bound);
609 let mut bracket_expansions = 0;
610 while is_finite(&upper_value)
611 && upper_value <= zero
612 && bracket_expansions < max_bracket_expansions
613 {
614 upper_bound *= &two;
615 (upper_value, _) = f_x_and_df_x(&upper_bound);
616 bracket_expansions += 1;
617 }
618
619 if !is_finite(&upper_bound) || !is_finite(&upper_value) || upper_value <= zero {
620 return Err(SafeguardedNewtonError::InvalidOutside {
621 radius: upper_bound,
622 value: upper_value,
623 bracket_expansions,
624 });
625 }
626
627 let mut solution = upper_bound.clone();
628 let (mut value, mut derivative) = f_x_and_df_x(&solution);
629 for iteration in 0..=max_iterations {
630 let current_result = NewtonIterationResult {
631 solution: solution.clone(),
632 derivative_at_solution: derivative.clone(),
633 error_of_function: value.clone(),
634 num_iterations_used: iteration,
635 };
636
637 if is_finite(&value) && value.abs() <= maximum_residual {
638 if is_finite(&derivative) && derivative > zero {
639 return Ok(current_result);
640 }
641 return Err(SafeguardedNewtonError::InvalidDerivative {
642 result: current_result,
643 });
644 }
645
646 if iteration == max_iterations {
647 return Err(SafeguardedNewtonError::DidNotConverge {
648 result: current_result,
649 lower_bound,
650 upper_bound,
651 });
652 }
653
654 if is_finite(&value) {
655 if value < zero {
656 lower_bound = solution.clone();
657 } else {
658 upper_bound = solution.clone();
659 }
660 }
661
662 let midpoint = (&lower_bound + &upper_bound) / &two;
663 let newton_candidate = if is_finite(&value) && is_finite(&derivative) && derivative > zero {
664 Some(&solution - &value / &derivative)
665 } else {
666 None
667 };
668
669 solution = match newton_candidate {
670 Some(candidate)
671 if is_finite(&candidate) && candidate > lower_bound && candidate < upper_bound =>
672 {
673 candidate
674 }
675 _ => midpoint,
676 };
677 (value, derivative) = f_x_and_df_x(&solution);
678 }
679
680 unreachable!("safeguarded Newton loop always returns")
681}
682
683pub(crate) fn newton_iteration_and_derivative<T: FloatLike>(
686 guess: &F<T>,
687 f_x_and_df_x: impl Fn(&F<T>) -> (F<T>, F<T>),
688 tolerance: &F<T>,
689 max_iterations: usize,
690 e_cm: &F<T>,
691) -> NewtonIterationResult<T> {
692 let mut x = guess.clone();
693 let (mut val_f_x, mut val_df_x) = f_x_and_df_x(&x);
694
695 let mut iteration = 0;
696
697 while iteration < max_iterations && val_f_x.abs() > guess.epsilon() * tolerance * e_cm {
698 x -= val_f_x / val_df_x;
699 (val_f_x, val_df_x) = f_x_and_df_x(&x);
700 iteration += 1;
701 }
702
703 NewtonIterationResult {
704 solution: x,
705 derivative_at_solution: val_df_x,
706 error_of_function: val_f_x,
707 num_iterations_used: iteration,
708 }
709}
710
711#[allow(dead_code)]
712pub(crate) fn newton_iteration_and_derivative_dual<T: FloatLike>(
713 guess: &HyperDual<F<T>>,
714 f_x_and_df_x: impl Fn(&HyperDual<F<T>>) -> (HyperDual<F<T>>, HyperDual<F<T>>),
715 tolerance: &F<T>,
716 max_iterations: usize,
717 e_cm: &F<T>,
718) -> NewtonIterationResultDual<T> {
719 let mut x = guess.clone();
720 let (mut val_f_x, mut val_df_x) = f_x_and_df_x(&x);
721
722 let mut iteration = 0;
723
724 while iteration < max_iterations
725 && val_f_x.values[0].abs() > guess.values[0].epsilon() * tolerance * e_cm
726 {
727 x -= val_f_x / val_df_x;
728 (val_f_x, val_df_x) = f_x_and_df_x(&x);
729 iteration += 1;
730 }
731
732 NewtonIterationResultDual {
733 solution: x,
734 derivative_at_solution: val_df_x,
735 error_of_function: val_f_x.values[0].clone(),
736 num_iterations_used: iteration,
737 }
738}
739
740#[derive(Serialize, Clone, Debug)]
741pub(crate) struct NewtonIterationResult<T: FloatLike> {
742 pub solution: F<T>,
743 pub derivative_at_solution: F<T>,
744 pub error_of_function: F<T>,
745 pub num_iterations_used: usize,
746}
747
748#[derive(Clone, Debug)]
749#[allow(dead_code)]
750pub(crate) struct NewtonIterationResultDual<T: FloatLike> {
751 pub solution: HyperDual<F<T>>,
752 pub derivative_at_solution: HyperDual<F<T>>,
753 pub error_of_function: F<T>,
754 pub num_iterations_used: usize,
755}
756
757#[cfg(test)]
758mod tests {
759 use std::cell::Cell;
760
761 use super::*;
762 use crate::utils::f128;
763
764 const ROOT_TOLERANCE: i64 = 64;
765
766 fn cancellation_limited_root<T: FloatLike>(
767 diagnostics: &mut RadialRootDiagnostics,
768 identity: &RadialRootIdentity,
769 large_momentum: f64,
770 target: f64,
771 ) -> Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
772 let zero = F::<T>::default();
773 let one = zero.one();
774 let target = F::<T>::from_f64(target);
775 let k3 = F::<T>::from_f64(large_momentum);
776 let k2 = F::<T>::from_f64(large_momentum + 1.0);
777 let derivative = &k2 - &k3;
778 let tolerance = zero.from_i64(ROOT_TOLERANCE);
779
780 diagnostics.solve(
781 identity,
782 &zero,
783 &one,
784 |radius| (radius * &k2 - radius * &k3 - &target, derivative.clone()),
785 &tolerance,
786 40,
787 64,
788 &one,
789 )
790 }
791
792 fn discontinuous_root<T: FloatLike>(
793 diagnostics: &mut RadialRootDiagnostics,
794 identity: &RadialRootIdentity,
795 positive_residual: f64,
796 ) -> Result<NewtonIterationResult<T>, SafeguardedNewtonError<T>> {
797 let zero = F::<T>::default();
798 let one = zero.one();
799 let half = &one / one.from_i64(2);
800 let positive_residual = F::<T>::from_f64(positive_residual);
801 let tolerance = zero.from_i64(ROOT_TOLERANCE);
802
803 diagnostics.solve(
804 identity,
805 &zero,
806 &one,
807 |radius| {
808 let value = if radius == &zero {
809 -one.clone()
810 } else if radius < &half {
811 -positive_residual.clone()
812 } else {
813 positive_residual.clone()
814 };
815 (value, one.clone())
816 },
817 &tolerance,
818 40,
819 64,
820 &one,
821 )
822 }
823
824 #[test]
825 fn safeguarded_newton_expands_bracket_and_converges() {
826 let result = safeguarded_newton_iteration_and_derivative(
827 &F(0.0),
828 &F(0.5),
829 |x| (x * x - F(2.0), F(2.0) * x),
830 &F(8.0),
831 40,
832 64,
833 &F(1.0),
834 )
835 .unwrap();
836
837 let expected_solution = result.solution.from_i64(2).sqrt();
838 let solution_tolerance = result.solution.epsilon() * result.solution.from_i64(64);
839 assert!((result.solution - expected_solution).abs() <= solution_tolerance);
840
841 let residual_tolerance =
842 result.error_of_function.epsilon() * result.error_of_function.from_i64(8);
843 assert!(result.error_of_function.abs() <= residual_tolerance);
844 assert!(result.derivative_at_solution > result.derivative_at_solution.zero());
845 }
846
847 #[test]
848 fn safeguarded_newton_accepts_a_few_ulp_residual() {
849 let residual = 2.2737367544323206e-13;
850 let result = safeguarded_newton_iteration_and_derivative(
851 &F(0.0),
852 &F(1.0),
853 |x| {
854 let value = if x == &F(1.0) {
855 F(residual)
856 } else {
857 x - F(1.0)
858 };
859 (value, F(1.0))
860 },
861 &F(8.0),
862 40,
863 64,
864 &F(1000.0),
865 )
866 .unwrap();
867
868 assert_eq!(result.solution, F(1.0));
869 assert_eq!(result.error_of_function, F(residual));
870 }
871
872 #[test]
873 fn safeguarded_newton_rejects_a_large_discontinuous_residual() {
874 let error = safeguarded_newton_iteration_and_derivative(
875 &F(0.0),
876 &F(1.0),
877 |x| {
878 if x < &F(1.0) {
879 (F(-1.0), F(1.0))
880 } else {
881 (F(741.0), F(1.0))
882 }
883 },
884 &F(8.0),
885 20,
886 64,
887 &F(1000.0),
888 )
889 .unwrap_err();
890
891 assert!(matches!(
892 error,
893 SafeguardedNewtonError::DidNotConverge { .. }
894 ));
895 }
896
897 #[test]
898 fn safeguarded_newton_rejects_a_non_interior_center() {
899 let error = safeguarded_newton_iteration_and_derivative(
900 &F(0.0),
901 &F(1.0),
902 |x| (*x, F(1.0)),
903 &F(8.0),
904 40,
905 64,
906 &F(1.0),
907 )
908 .unwrap_err();
909
910 assert!(matches!(
911 error,
912 SafeguardedNewtonError::InvalidInside { .. }
913 ));
914 }
915
916 #[test]
917 fn safeguarded_newton_rejects_an_invalid_outside_bracket() {
918 let error = safeguarded_newton_iteration_and_derivative(
919 &F(0.0),
920 &F(1.0),
921 |_| (F(-1.0), F(1.0)),
922 &F(8.0),
923 40,
924 4,
925 &F(1.0),
926 )
927 .unwrap_err();
928
929 assert!(matches!(
930 error,
931 SafeguardedNewtonError::InvalidOutside {
932 bracket_expansions: 4,
933 ..
934 }
935 ));
936 }
937
938 #[test]
939 fn safeguarded_newton_reports_invalid_outside_endpoint_value() {
940 let error = safeguarded_newton_iteration_and_derivative(
941 &F(1.0),
942 &F(0.5),
943 |x| (x - F(2.0), F(1.0)),
944 &F(8.0),
945 40,
946 64,
947 &F(1.0),
948 )
949 .unwrap_err();
950
951 assert!(matches!(
952 error,
953 SafeguardedNewtonError::InvalidOutside {
954 radius: F(0.5),
955 value: F(-1.5),
956 bracket_expansions: 0,
957 }
958 ));
959 }
960
961 #[test]
962 fn safeguarded_newton_reports_non_finite_outside_radius() {
963 let nan = F(0.0) / F(0.0);
964 let error = safeguarded_newton_iteration_and_derivative(
965 &F(0.0),
966 &nan,
967 |x| (x - F(1.0), F(1.0)),
968 &F(8.0),
969 40,
970 64,
971 &F(1.0),
972 )
973 .unwrap_err();
974
975 match error {
976 SafeguardedNewtonError::InvalidOutside {
977 radius,
978 value,
979 bracket_expansions: 0,
980 } => {
981 assert!(radius.is_nan());
982 assert!(value.is_nan());
983 }
984 unexpected => panic!("unexpected safeguarded Newton error: {unexpected:?}"),
985 }
986 }
987
988 #[test]
989 fn safeguarded_newton_rejects_an_invalid_root_derivative() {
990 let error = safeguarded_newton_iteration_and_derivative(
991 &F(0.0),
992 &F(1.0),
993 |x| (x - F(1.0), F(0.0)),
994 &F(8.0),
995 40,
996 64,
997 &F(1.0),
998 )
999 .unwrap_err();
1000
1001 assert!(matches!(
1002 error,
1003 SafeguardedNewtonError::InvalidDerivative { .. }
1004 ));
1005 }
1006
1007 #[test]
1008 fn local_root_consistency_has_bounded_evaluation_cost() {
1009 let result = NewtonIterationResult {
1010 solution: F(0.5),
1011 derivative_at_solution: F(1.0),
1012 error_of_function: F(1.0e-20),
1013 num_iterations_used: 40,
1014 };
1015 let calls = Cell::new(0);
1016 let consistency = LocalRootConsistency::check(
1017 &result,
1018 &F(0.0),
1019 &|radius| {
1020 calls.set(calls.get() + 1);
1021 (radius - F(0.5), F(1.0))
1022 },
1023 &F(1.0),
1024 );
1025 assert!(consistency.is_valid);
1026 assert_eq!(calls.get(), 4);
1027
1028 let short_solve_result = NewtonIterationResult {
1029 num_iterations_used: 3,
1030 ..result
1031 };
1032 calls.set(0);
1033 let consistency = LocalRootConsistency::check(
1034 &short_solve_result,
1035 &F(0.0),
1036 &|radius| {
1037 calls.set(calls.get() + 1);
1038 (radius - F(0.5), F(1.0))
1039 },
1040 &F(1.0),
1041 );
1042 assert!(!consistency.is_valid);
1043 assert_eq!(calls.get(), 0);
1044 }
1045
1046 #[test]
1047 fn higher_precision_rescues_large_nearly_equal_momenta() {
1048 let mut diagnostics = RadialRootDiagnostics::default();
1049
1050 for exponent in [32, 40, 48, 52] {
1051 let identity = RadialRootIdentity::new(format!("cancellation at 2^{exponent}"));
1052 let large_momentum = 2.0_f64.powi(exponent);
1053 let f64_error = cancellation_limited_root::<f64>(
1054 &mut diagnostics,
1055 &identity,
1056 large_momentum,
1057 1.0 / 3.0,
1058 )
1059 .unwrap_err();
1060 assert!(matches!(
1061 f64_error,
1062 SafeguardedNewtonError::DidNotConverge { .. }
1063 ));
1064
1065 let rescued = cancellation_limited_root::<f128>(
1066 &mut diagnostics,
1067 &identity,
1068 large_momentum,
1069 1.0 / 3.0,
1070 )
1071 .unwrap();
1072 let active_precision_limit = rescued.solution.epsilon()
1073 * rescued.solution.from_i64(ROOT_TOLERANCE)
1074 * rescued.solution.one();
1075 assert!(
1076 rescued.error_of_function.abs() > active_precision_limit,
1077 "2^{exponent} case should exercise precision rescue rather than direct convergence"
1078 );
1079 }
1080 }
1081
1082 #[test]
1083 fn higher_precision_can_use_a_successful_lower_precision_baseline() {
1084 let mut diagnostics = RadialRootDiagnostics::default();
1085 let identity = RadialRootIdentity::new("successful f64 baseline".to_string());
1086 let target = 0.3;
1087
1088 let f64_result = diagnostics
1089 .solve(
1090 &identity,
1091 &F(0.0),
1092 &F(1.0),
1093 |radius| (radius - F(target), F(1.0)),
1094 &F(ROOT_TOLERANCE as f64),
1095 40,
1096 64,
1097 &F(1.0),
1098 )
1099 .unwrap();
1100 assert!(f64_result.error_of_function.abs() > F(0.0));
1101 assert!(
1102 f64_result.error_of_function.abs()
1103 <= f64_result.solution.epsilon()
1104 * F(ROOT_TOLERANCE as f64)
1105 * f64_result.solution.one()
1106 );
1107
1108 let rescued = cancellation_limited_root::<f128>(
1111 &mut diagnostics,
1112 &identity,
1113 2.0_f64.powi(52),
1114 target,
1115 )
1116 .unwrap();
1117 let active_precision_limit =
1118 rescued.solution.epsilon() * rescued.solution.from_i64(ROOT_TOLERANCE);
1119 assert!(
1120 rescued.error_of_function.abs() > active_precision_limit,
1121 "the f128 solve should require the successful f64 observation as its baseline"
1122 );
1123 }
1124
1125 #[test]
1126 fn higher_precision_uses_the_roundoff_floor_after_an_exact_lower_precision_zero() {
1127 let mut diagnostics = RadialRootDiagnostics::default();
1128 let identity = RadialRootIdentity::new("accidental exact f64 residual".to_string());
1129
1130 let f64_result = diagnostics
1131 .solve(
1132 &identity,
1133 &F(0.0),
1134 &F(1.0),
1135 |radius| (radius - F(0.3), F(1.0)),
1136 &F(0.1),
1137 40,
1138 64,
1139 &F(1.0),
1140 )
1141 .unwrap();
1142 assert_eq!(f64_result.error_of_function, F(0.0));
1143
1144 let rescued =
1145 cancellation_limited_root::<f128>(&mut diagnostics, &identity, 2.0_f64.powi(52), 0.3)
1146 .unwrap();
1147 let active_precision_limit =
1148 rescued.solution.epsilon() * rescued.solution.from_i64(ROOT_TOLERANCE);
1149 assert!(rescued.error_of_function.abs() > active_precision_limit);
1150 }
1151
1152 #[test]
1153 fn repeated_root_identities_are_paired_by_precision_local_occurrence() {
1154 let mut diagnostics = RadialRootDiagnostics::default();
1155 let identity = RadialRootIdentity::new("two LMB channel rays".to_string());
1156 let f64_precision: SymbolicaFloat = F::<f64>::default().into();
1157 let f128_precision: SymbolicaFloat = F::<f128>::default().into();
1158
1159 assert_eq!(
1160 diagnostics.next_call_key(&identity, &F::<f64>::default()),
1161 (identity.clone(), 0, f64_precision.prec())
1162 );
1163 assert_eq!(
1164 diagnostics.next_call_key(&identity, &F::<f64>::default()),
1165 (identity.clone(), 1, f64_precision.prec())
1166 );
1167 assert_eq!(
1168 diagnostics.next_call_key(&identity, &F::<f128>::default()),
1169 (identity.clone(), 0, f128_precision.prec())
1170 );
1171 assert_eq!(
1172 diagnostics.next_call_key(&identity, &F::<f128>::default()),
1173 (identity.clone(), 1, f128_precision.prec())
1174 );
1175 diagnostics.restart_precision_pass();
1176 assert_eq!(
1177 diagnostics.next_call_key(&identity, &F::<f128>::default()),
1178 (identity.clone(), 0, f128_precision.prec())
1179 );
1180 assert_eq!(
1181 diagnostics.next_call_key(&identity, &F::<f128>::default()),
1182 (identity, 1, f128_precision.prec())
1183 );
1184 }
1185
1186 #[test]
1187 fn precision_rescue_replays_at_the_same_precision() {
1188 let mut diagnostics = RadialRootDiagnostics::default();
1189 let identity = RadialRootIdentity::new("precise result replay".to_string());
1190 let large_momentum = 2.0_f64.powi(52);
1191
1192 assert!(matches!(
1193 cancellation_limited_root::<f64>(
1194 &mut diagnostics,
1195 &identity,
1196 large_momentum,
1197 1.0 / 3.0,
1198 ),
1199 Err(SafeguardedNewtonError::DidNotConverge { .. })
1200 ));
1201 let rescued = cancellation_limited_root::<f128>(
1202 &mut diagnostics,
1203 &identity,
1204 large_momentum,
1205 1.0 / 3.0,
1206 )
1207 .unwrap();
1208 diagnostics.restart_precision_pass();
1209 let replayed = cancellation_limited_root::<f128>(
1210 &mut diagnostics,
1211 &identity,
1212 large_momentum,
1213 1.0 / 3.0,
1214 )
1215 .unwrap();
1216
1217 for result in [rescued, replayed] {
1218 let active_precision_limit =
1219 result.solution.epsilon() * result.solution.from_i64(ROOT_TOLERANCE);
1220 assert!(result.error_of_function.abs() > active_precision_limit);
1221 }
1222 }
1223
1224 #[test]
1225 fn multiple_roots_are_rescued_in_one_higher_precision_pass() {
1226 let mut diagnostics = RadialRootDiagnostics::default();
1227 let identities = (0..3)
1228 .map(|index| RadialRootIdentity::new(format!("difficult root {index}")))
1229 .collect::<Vec<_>>();
1230
1231 for identity in &identities {
1232 assert!(matches!(
1233 cancellation_limited_root::<f64>(
1234 &mut diagnostics,
1235 identity,
1236 2.0_f64.powi(52),
1237 1.0 / 3.0,
1238 ),
1239 Err(SafeguardedNewtonError::DidNotConverge { .. })
1240 ));
1241 }
1242 for identity in &identities {
1243 cancellation_limited_root::<f128>(
1244 &mut diagnostics,
1245 identity,
1246 2.0_f64.powi(52),
1247 1.0 / 3.0,
1248 )
1249 .unwrap();
1250 }
1251 }
1252
1253 #[test]
1254 fn precision_rescue_rejects_a_stable_discontinuous_residual() {
1255 let mut diagnostics = RadialRootDiagnostics::default();
1256 let identity = RadialRootIdentity::new("stable discontinuity".to_string());
1257
1258 assert!(matches!(
1259 discontinuous_root::<f64>(&mut diagnostics, &identity, 1.0),
1260 Err(SafeguardedNewtonError::DidNotConverge { .. })
1261 ));
1262 assert!(matches!(
1263 discontinuous_root::<f128>(&mut diagnostics, &identity, 1.0),
1264 Err(SafeguardedNewtonError::DidNotConverge { .. })
1265 ));
1266 }
1267
1268 #[test]
1269 fn precision_rescue_rejects_an_improved_but_large_residual() {
1270 let mut diagnostics = RadialRootDiagnostics::default();
1271 let identity = RadialRootIdentity::new("shrinking discontinuity".to_string());
1272
1273 assert!(matches!(
1274 discontinuous_root::<f64>(&mut diagnostics, &identity, 1.0),
1275 Err(SafeguardedNewtonError::DidNotConverge { .. })
1276 ));
1277 assert!(matches!(
1278 discontinuous_root::<f128>(&mut diagnostics, &identity, 0.01),
1279 Err(SafeguardedNewtonError::DidNotConverge { .. })
1280 ));
1281 }
1282
1283 #[test]
1284 fn precision_rescue_rejects_an_improving_discontinuous_non_root() {
1285 let mut diagnostics = RadialRootDiagnostics::default();
1286 let identity = RadialRootIdentity::new("improving discontinuity".to_string());
1287 let f64_precision: SymbolicaFloat = F::<f64>::default().into();
1288 let f128_precision: SymbolicaFloat = F::<f128>::default().into();
1289 let lower_key = (identity.clone(), 0, f64_precision.prec());
1290 let higher_key = (identity.clone(), 0, f128_precision.prec());
1291
1292 assert!(matches!(
1293 discontinuous_root::<f64>(&mut diagnostics, &identity, 1.0e-12),
1294 Err(SafeguardedNewtonError::DidNotConverge { .. })
1295 ));
1296 let lower_precision = diagnostics.observations[&lower_key].clone();
1297
1298 assert!(matches!(
1299 discontinuous_root::<f128>(&mut diagnostics, &identity, 1.0e-14),
1300 Err(SafeguardedNewtonError::DidNotConverge { .. })
1301 ));
1302 let higher_precision = &diagnostics.observations[&higher_key];
1303
1304 let residual_improvement =
1308 lower_precision.comparison_residual() / &higher_precision.residual;
1309 let required_improvement = SymbolicaFloat::with_val(
1310 residual_improvement.prec(),
1311 MINIMUM_PRECISION_RESIDUAL_IMPROVEMENT,
1312 );
1313 assert!(residual_improvement >= required_improvement);
1314 assert!(higher_precision.residual <= lower_precision.maximum_residual);
1315 assert!(
1316 higher_precision.relative_newton_correction <= lower_precision.relative_residual_limit
1317 );
1318 assert!(higher_precision.bracket_is_valid);
1319 assert!(
1320 !higher_precision
1321 .local_consistency
1322 .as_ref()
1323 .expect("residual-limited candidates have local diagnostics")
1324 .is_valid
1325 );
1326 assert!(
1327 higher_precision
1328 .precision_rescue_improvement(&lower_precision)
1329 .is_none()
1330 );
1331 }
1332}