Skip to main content

gammalooprs/utils/
newton_solver.rs

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        // The two symmetric probes cost four function evaluations per candidate. Refuse this
58        // check when the caller attempted fewer iterations, so each consistency check costs no
59        // more than the failed solve. Alternate-endpoint certification can check both candidates.
60        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        // Radius and energy need not have the same units: for LU the root is a
70        // dimensionless rescaling. Probe and validate relative to the positive root itself.
71        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        // `SymbolicaFloat` retains the source precision. These observations cross the generic
191        // stability lanes, so keeping them in a precision-carrying type avoids feeding an f128 or
192        // arbitrary-precision residual through binary64 during the rescue decision.
193        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            // A safeguarded bracket can collapse to one representable radius after both
224            // endpoints have been updated from opposite signs. That is a localized root,
225            // not a loss of the bracket invariant.
226            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        // A lower-precision residual can be exactly zero by accidental cancellation. In that
285        // case its precision-scaled roundoff floor is the meaningful comparison baseline.
286        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/// Transient diagnostics shared by the stability levels of one sample evaluation.
294///
295/// At an exhausted sign bracket, the energy residual target is augmented by the energy
296/// change across that one representable radius interval: `|f| <= tau*epsilon*E_cm + |f'|*width`.
297/// Acceptance also requires local sign/slope consistency. This is a numerical root-resolution check under the solver's
298/// continuity assumption, not a forward-error bound for an arbitrary callback.
299/// If the last candidate remains uncertified, the other exhausted-bracket endpoint
300/// is checked under the same policy, without changing any already accepted result.
301///
302/// A higher-precision solve may also recover a residual-limited root when it reaches the
303/// original lower-precision accuracy target and clearly improves the same root. All
304/// structural solver failures remain errors in every precision.
305///
306/// For lower and higher precisions `lo` and `hi`, rescue requires
307/// `|f_hi| <= tau * epsilon_lo * E_cm`,
308/// `|f_hi / f'_hi| / |r_hi| <= tau * epsilon_lo`, and
309/// `max(|f_lo|, epsilon_lo * E_cm) / |f_hi| >= 8`, in addition to the bracket and
310/// two-scale local-root consistency checks.
311#[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        // The exhausted bracket resolves the root only to its endpoint separation.
424        // Its Newton correction may therefore exceed the original energy-residual target
425        // divided by the derivative by at most that width; no fixed ulp multiplier is used.
426        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, &current) {
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        // Preserve both accepted paths above. Exhaustion may leave the last
513        // candidate at the less accurate endpoint; test the other represented
514        // endpoint with the same residual budget and fresh consistency probes.
515        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
655/// Find a radial root while preserving a finite negative/positive bracket.
656///
657/// Newton steps are used only when their derivative and proposed radius are
658/// finite and the proposed radius stays strictly inside the bracket. Otherwise
659/// the next iterate is the bracket midpoint.
660pub(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        // Once the bracket has no representable interior point, further iterations cannot
751        // improve it. Preserve enough iterations for the existing four-probe consistency
752        // check, and let precision diagnostics decide whether the residual can be rescued.
753        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
780/// root finding, returns the derivative at the root, so that we don't have to recompute it.
781/// Also returns the value of the function whose root is being found and the number of iterations used for debug information
782pub(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        // A smooth ttH energy sum has adjacent f64 root endpoints with residuals just
1208        // above epsilon * Q. Strict residual convergence must fail without spinning for
1209        // 2000 iterations; bracket-resolution acceptance must preserve the root's accuracy.
1210        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        // A legitimate Arb-only evaluation has no lower-precision observations to inherit.
1247        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            // Force the initial midpoint, then claim the non-root has a positive slope.
1269            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        // This is algebraically the same root as `r - target`, but separately evaluating
1394        // `(K + 1) r - K r` leaves a precision-limited residual for large K.
1395        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        // 0.2 epsilon still forces an exact f64 zero, while allowing the higher-precision
1416        // candidate's 3.7e-17 relative root correction (rather than its absolute residual).
1417        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        // Residual improvement, the inherited f64 residual bound, and the Newton-correction
1592        // bound alone would accept this fabricated candidate even though the function has no
1593        // zero. The local sign/slope consistency check must be what rejects it.
1594        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}