/home/runner/work/DiFfRG_current/DiFfRG_current/DiFfRG/include/DiFfRG/common/root_finding.hh Source File#

DiFfRG: /home/runner/work/DiFfRG_current/DiFfRG_current/DiFfRG/include/DiFfRG/common/root_finding.hh Source File
DiFfRG
Discretization Framework for functional Renormalization Group flows
root_finding.hh
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <array>
5#include <cmath>
6#include <functional>
7#include <limits>
8#include <stdexcept>
9#include <utility>
10#include <vector>
11
12#include <spdlog/spdlog.h>
13
14namespace DiFfRG
15{
16 template <int dim> class AbstractRootFinder
17 {
18 protected:
19 using FUN = std::function<bool(const std::array<double, dim> &)>;
20
21 public:
22 AbstractRootFinder(const FUN &f, const double abs_tol = 1e-4, const int max_iter = 1000)
24 {
25 }
26
27 void set_abs_tol(const double abs_tol) { this->abs_tol = abs_tol; }
28
29 void set_max_iter(const uint max_iter) { this->max_iter = max_iter; }
30
31 uint get_iter() const { return iter; }
32
33 std::array<double, dim> search() { return this->search_impl(); }
34
35 protected:
37
38 double abs_tol;
41
42 virtual std::array<double, dim> search_impl() = 0;
43 };
44
45 // explicit specialization for 1D search
46 template <> class AbstractRootFinder<1>
47 {
48 protected:
49 using FUN = std::function<bool(const double)>;
50 using FUN_ARR = std::function<bool(const std::array<double, 1> &)>;
51
52 public:
53 AbstractRootFinder(const FUN &f, const double abs_tol = 1e-4, const int max_iter = 1000)
54 : f([f](const std::array<double, 1> &x) { return f(x[0]); }), abs_tol(abs_tol), max_iter(max_iter), iter(0)
55 {
56 }
57
58 AbstractRootFinder(const FUN_ARR &f, const double abs_tol = 1e-4, const int max_iter = 1000)
60 {
61 }
62
63 void set_abs_tol(const double abs_tol) { this->abs_tol = abs_tol; }
64
65 void set_max_iter(const uint max_iter) { this->max_iter = max_iter; }
66
67 uint get_iter() const { return iter; }
68
69 double search() { return this->search_impl()[0]; }
70
71 protected:
73
74 double abs_tol;
77
78 virtual std::array<double, 1> search_impl() = 0;
79 };
80
82 {
83 public:
84 BisectionRootFinder(const FUN &f, const double abs_tol = 1e-4, const int max_iter = 1000)
86 {
87 }
88
89 void set_x_min(const double x_min) { this->x_min = x_min; }
90 void set_x_max(const double x_max) { this->x_max = x_max; }
91
92 void set_bounds(const double x_min, const double x_max)
93 {
94 this->x_min = x_min;
95 this->x_max = x_max;
96 }
97
98 void set_next_x(const std::function<double(double, double)> &next_x) { this->next_x = next_x; }
99
100 protected:
101 std::array<double, 1> search_impl() override
102 {
103 double x_min = this->x_min;
104 double x_max = this->x_max;
105 double x_test = 0.;
106
107 bool success = false;
108
109 for (uint i = 0; i < this->max_iter; i++) {
110 x_test = next_x(x_min, x_max);
111 success = this->f({{x_test}});
112 if (success)
113 x_max = x_test;
114 else
115 x_min = x_test;
116
117 this->iter = i;
118 if (success && std::abs(x_max - x_min) < this->abs_tol) break;
119 }
120
121 if (!success) throw std::runtime_error("BisectionRootFinder: search did not converge");
122
123 return {{x_test}};
124 }
125
126 double x_min;
127 double x_max;
128
129 std::function<double(double, double)> next_x = [](const double x_min, const double x_max) {
130 return (x_min + x_max) / 2.;
131 };
132 };
133
173 {
174 protected:
175 using FUN = std::function<bool(const double, double &)>;
176
177 public:
178 // the callback is taken by value and moved: callers pass a temporary lambda, and the copy from
179 // a const reference also trips a spurious -Wmaybe-uninitialized inside std::function on GCC 16
180 BisectionRootFinderTarget(FUN f, const double abs_tol = 1e-4, const int max_iter = 1000)
181 : f(std::move(f)), abs_tol(abs_tol), max_iter(max_iter), iter(0)
182 {
183 }
184
185 void set_abs_tol(const double abs_tol) { this->abs_tol = abs_tol; }
186
187 void set_max_iter(const uint max_iter) { this->max_iter = max_iter; }
188
189 void set_x_min(const double x_min) { this->x_min = x_min; }
190 void set_x_max(const double x_max) { this->x_max = x_max; }
191
192 void set_bounds(const double x_min, const double x_max)
193 {
194 this->x_min = x_min;
195 this->x_max = x_max;
196 }
197
198 void set_next_x(const std::function<double(double, double)> &next_x) { this->next_x = next_x; }
199
204 void set_x_collapse_tol(const double x_collapse_tol) { this->x_collapse_tol = x_collapse_tol; }
205
206 uint get_iter() const { return iter; }
207
211 double get_target() const { return target; }
212
217 bool converged() const { return m_converged; }
218
219 double search()
220 {
221 if (!(std::isfinite(this->x_min) && std::isfinite(this->x_max)))
222 throw std::runtime_error("BisectionRootFinderTarget: search bounds were not set");
223
224 double x_lo = this->x_min;
225 double x_hi = this->x_max;
226
227 double best_x = std::numeric_limits<double>::quiet_NaN();
228 double best_target = std::numeric_limits<double>::quiet_NaN();
229 bool have_success = false;
230
231 this->m_converged = false;
232 this->target = std::numeric_limits<double>::quiet_NaN();
233
234 for (uint i = 0; i < this->max_iter; ++i) {
235 this->iter = i;
236
237 const double x_test = next_x(x_lo, x_hi);
238 double x_target = std::numeric_limits<double>::quiet_NaN();
239 const bool success = this->f(x_test, x_target);
240
241 if (success) {
242 x_hi = x_test;
243 // Two consecutive successes whose targets agree to abs_tol: the observable is resolved.
244 const bool target_converged = have_success && std::abs(x_target - best_target) < this->abs_tol;
245 best_x = x_test;
246 best_target = x_target;
247 have_success = true;
248 if (target_converged) {
249 this->m_converged = true;
250 break;
251 }
252 } else
253 x_lo = x_test;
254
255 const double collapse_tol =
256 this->x_collapse_tol > 0.
257 ? this->x_collapse_tol
258 : 4. * std::numeric_limits<double>::epsilon() * std::max(1., std::max(std::abs(x_lo), std::abs(x_hi)));
259 if (std::abs(x_hi - x_lo) <= collapse_tol) break;
260 }
261
262 if (!have_success)
263 throw std::runtime_error("BisectionRootFinderTarget: no evaluation of the search interval succeeded");
264
265 this->target = best_target;
266
267 if (!this->m_converged)
268 spdlog::warn("BisectionRootFinderTarget: stopped after {} iterations without reaching the target tolerance "
269 "{:.6e}; returning x = {:.12e} with target = {:.12e}",
270 this->iter + 1, this->abs_tol, best_x, best_target);
271
272 return best_x;
273 }
274
275 protected:
277
278 double abs_tol;
281
282 double x_min = std::numeric_limits<double>::quiet_NaN();
283 double x_max = std::numeric_limits<double>::quiet_NaN();
284 double x_collapse_tol = 0.;
285
286 double target = std::numeric_limits<double>::quiet_NaN();
287 bool m_converged = false;
288
289 std::function<double(double, double)> next_x = [](const double x_min, const double x_max) {
290 return (x_min + x_max) / 2.;
291 };
292 };
293
294 namespace detail
295 {
304 struct ScalingFit {
305 bool valid = false;
306 double shift = std::numeric_limits<double>::quiet_NaN();
307 double exponent = std::numeric_limits<double>::quiet_NaN();
308 double log_amplitude = std::numeric_limits<double>::quiet_NaN();
309 };
310
341 inline double solve_scaling_shift(const double A, const double B, const double d1, const double d2)
342 {
343 if (!(std::isfinite(A) && std::isfinite(B) && std::isfinite(d1) && std::isfinite(d2))) return NAN;
344 if (!(A > 0. && B > 0. && d2 > 0. && d1 > d2)) return NAN;
345
346 // A and B are log-ratios of the sampled values. Below this floor the three values are equal
347 // to within rounding, both logs are noise, and every guard downstream is being applied to
348 // garbage. In any regime the finder actually uses, A and B are O(0.1..1).
349 if (!(A > 1e-10 && B > 1e-10)) return NAN;
350
351 // Asymptotic sign test: without it there is no root and the bracketing below cannot
352 // converge. The margin also measures the conditioning. For a power law sampled at spacing
353 // d far from x_c, both sides agree at leading order and the gap is only O(d/s) relative --
354 // so a vanishing margin means the data cannot distinguish the power law from a straight
355 // line, and any root found is cancellation noise. Without this, a triple sampled ~1e12
356 // spacings from criticality yields a confident, wholly spurious shift.
357 const double lhs = A * d2, rhs = B * (d1 - d2);
358 if (!(lhs < rhs)) return NAN;
359 if (!(rhs - lhs > 1e-9 * std::max(std::abs(lhs), std::abs(rhs)))) return NAN;
360
361 const auto F = [&](const double s) { return A * std::log1p(d2 / s) - B * std::log((s + d1) / (s + d2)); };
362
363 // F is +inf at 0+ and negative far out; walk outwards from the natural scale d2.
364 double s_hi = d2;
365 for (int i = 0; i < 60 && F(s_hi) > 0.; ++i)
366 s_hi *= 16.;
367 if (F(s_hi) > 0.) return NAN;
368
369 // Floor: below this the bisection cannot make progress. It has to be relative to the point
370 // spacing, not absolute -- a probe that lands very close to criticality legitimately gives
371 // s / d2 ~ 1e-14, and an absolute floor rejects exactly the most informative triple there is.
372 const double s_floor = 1e-18 * d2;
373 double s_lo = s_hi / 16.;
374 for (int i = 0; i < 60 && s_lo > s_floor && F(s_lo) < 0.; ++i)
375 s_lo /= 16.;
376 if (F(s_lo) < 0.) return NAN;
377
378 for (int i = 0; i < 200; ++i) {
379 const double s_mid = std::sqrt(s_lo * s_hi);
380 if (!(s_mid > s_lo && s_mid < s_hi)) break; // converged to the last representable interval
381 if (F(s_mid) > 0.)
382 s_lo = s_mid;
383 else
384 s_hi = s_mid;
385 }
386 const double s = std::sqrt(s_lo * s_hi);
387 if (!(std::isfinite(s) && s > s_floor)) return NAN;
388
389 // Precision ceiling. Once s >> d2 the three points sit far from x_c, the power law is
390 // locally indistinguishable from a straight line, and A*d2 - B*(d1-d2) is a difference of
391 // nearly equal doubles. Measured against exact data the result is good to 1e-8 relative up
392 // to s ~ 1e4 d2 and degrades to tens of percent beyond it -- while still looking perfectly
393 // finite. Reject rather than hand back a confident wrong critical point.
394 //
395 // The comparison carries a guard band because the quantity being tested is the bisection's
396 // own output, and at the ceiling that output already carries O(1e-8) relative error. A bare
397 // `s > 1e4 * d2` therefore decides data sitting exactly on the threshold by which side
398 // rounding happens to land on -- for the canonical d1=3, d2=1 triple the root at s = 1e4
399 // comes out as 1e4*(1 +/- 1.3e-8), so the verdict flips with optimisation flags rather than
400 // with the data. Widening the rejection by 1e-6 relative, ~100x the noise, makes the
401 // documented ceiling a hard boundary. Everything the accurate branch relies on sits at
402 // s <= 1e3 * d2, a full decade below, so nothing that was accepted becomes rejected.
403 if (!(s < 1e4 * d2 * (1. - 1e-6))) return NAN;
404
405 return s;
406 }
407
415 inline ScalingFit fit_power_law_3(std::array<double, 3> x, std::array<double, 3> y)
416 {
417 std::array<std::size_t, 3> idx{{0, 1, 2}};
418 std::sort(idx.begin(), idx.end(), [&](std::size_t a, std::size_t b) { return x[a] > x[b]; });
419 const double x1 = x[idx[0]], x2 = x[idx[1]], x3 = x[idx[2]];
420 const double y1 = y[idx[0]], y2 = y[idx[1]], y3 = y[idx[2]];
421
422 ScalingFit fit;
423 if (!(std::isfinite(y1) && std::isfinite(y2) && std::isfinite(y3))) return fit;
424 if (!(x1 > x2 && x2 > x3)) return fit;
425 if (!(y1 > y2 && y2 > y3 && y3 > 0.)) return fit;
426
427 const double A = std::log(y1 / y2), B = std::log(y2 / y3);
428 const double d1 = x1 - x3, d2 = x2 - x3;
429 const double s = solve_scaling_shift(A, B, d1, d2);
430 if (!std::isfinite(s)) return fit;
431
432 const double beta = A / std::log((s + d1) / (s + d2));
433 if (!std::isfinite(beta) || beta <= 0.) return fit;
434
435 fit.valid = true;
436 fit.shift = s;
437 fit.exponent = beta;
438 fit.log_amplitude = std::log(y3) - beta * std::log(s);
439 return fit;
440 }
441 } // namespace detail
442
477 {
478 public:
490
491 protected:
492 using FUN = std::function<bool(const double, double &, double &)>;
494 using FUN_NO_RESIDUAL = std::function<bool(const double, double &)>;
495
496 public:
497 ScalingRootFinder(FUN f, const double target, const double rel_tol = 1e-3, const uint max_iter = 40)
498 : f(std::move(f)), target(target), rel_tol(rel_tol), max_iter(max_iter)
499 {
500 }
501
502 ScalingRootFinder(FUN_NO_RESIDUAL f, const double target, const double rel_tol = 1e-3, const uint max_iter = 40)
503 : f([f = std::move(f)](const double x, double &obs, double &) { return f(x, obs); }), target(target),
505 {
506 }
507
513 void set_bounds(const double x_lo, const double x_hi)
514 {
515 this->x_lo_init = x_lo;
516 this->x_hi_init = x_hi;
517 }
519 void set_expansion_factor(const double rho) { expansion_factor = rho; }
525 void set_rel_tol(const double v) { rel_tol = v; }
528 void set_one_sided(const bool v) { one_sided = v; }
529 void set_acceptance(const Acceptance a) { acceptance = a; }
533 void set_aim_fraction(const double v) { aim_fraction = v; }
535 void set_x_rel_floor(const double v) { x_rel_floor = v; }
536 void set_max_iter(const uint v) { max_iter = v; }
545 void set_obs_window(const double v) { obs_window = v; }
548 void set_residual_window(const double v) { residual_window = v; }
549 void set_exponent_bounds(const double lo, const double hi)
550 {
551 beta_min = lo;
552 beta_max = hi;
553 }
557 void set_theta_bounds(const double lo, const double hi)
558 {
559 theta_min = lo;
560 theta_max = hi;
561 }
563 void set_endpoint_standoff(const double v) { endpoint_standoff = v; }
565 void set_approach_factor(const double v) { approach_factor = v; }
568 void seed_exponent(const double beta) { seeded_exponent = beta; }
573 double get_obs() const { return best_obs; }
576 double get_x_critical() const { return model_valid ? model_x_c : (divergent_valid ? divergent_x_c : NAN); }
577 double get_exponent() const { return model_beta; }
578 double get_theta() const { return model_theta; }
579 bool have_model() const { return model_valid; }
581 double get_x_extrapolated() const { return model_valid ? extrapolate(model_x_c) : NAN; }
582 bool converged() const { return m_converged; }
583 uint get_iter() const { return iter; }
585 double bracket_width() const { return x_hi - x_lo_soft; }
586
587 const std::vector<std::pair<double, double>> &successes() const { return S; }
588 const std::vector<std::pair<double, double>> &failures() const { return F; }
589
590 struct Counts {
593 };
594 Counts get_counts() const { return counts; }
597 double search();
598
599 protected:
603 double effective_target() const
604 {
606 return one_sided ? target * (1. + 0.5 * rel_tol) : target;
607 }
608
610 double extrapolate(const double x_c) const
611 {
612 if (!(target > 0.)) return NAN;
613 return x_c + std::exp((std::log(effective_target()) - model_log_C) / model_beta);
614 }
615
625 static std::array<std::size_t, 3> select_triple(const std::size_t) { return {{0, 1, 2}}; }
626
629 void refit();
631 void fit_convergent();
633 void fit_divergent();
635 double propose();
637 void record(double x, bool ok, double obs, double residual);
638
640 double target;
641 double rel_tol;
643
644 double x_lo_init = NAN, x_hi_init = NAN;
646 double expansion_factor = 2.;
648
649 bool one_sided = true;
651 double aim_fraction = 0.5;
652 bool answer_found = false;
653 double x_rel_floor = 1e-13;
654
655 double obs_window = 20.;
656 double residual_window = 0.;
657 double beta_min = 0.02, beta_max = 5.;
658 double theta_min = 0.5, theta_max = 8.;
659 double endpoint_standoff = 0.02;
660 double approach_factor = 0.1;
661 double seeded_exponent = NAN;
662
663 // Bracket. x_lo_soft is the bisection end: the largest x known to be invalid OR below the
664 // target. x_lo_hard is the largest x known to be *invalid*, which is the hard constraint on
665 // x_c -- a probe that merely came in under target is still a perfectly good sample of the
666 // convergent branch and must not be mistaken for evidence about where x_c lies.
667 double x_lo_soft = -std::numeric_limits<double>::infinity();
668 double x_lo_hard = -std::numeric_limits<double>::infinity();
669 double x_hi = std::numeric_limits<double>::infinity();
670
671 std::vector<std::pair<double, double>> S;
672 std::vector<std::pair<double, double>> F;
673
674 // Convergent-branch model: obs = exp(model_log_C) * (x - model_x_c)^model_beta.
675 bool model_valid = false;
676 double model_x_c = NAN, model_beta = NAN, model_log_C = NAN, model_theta = NAN;
677 double model_x_c_prev = NAN;
678 // Divergent-branch model: residual = a - ln(divergent_x_c - x) / model_theta. Only its
679 // critical point is used; the intercept a is not identifiable and not needed.
680 bool divergent_valid = false;
681 double divergent_x_c = NAN, divergent_shift = NAN;
682
683 double best_x = NAN, best_obs = NAN;
684 bool have_success = false;
685 bool m_converged = false;
688
689 double w_prev = std::numeric_limits<double>::infinity();
690 double w_prevprev = std::numeric_limits<double>::infinity();
691
692 bool probed_hi_init = false, probed_lo_init = false;
694 double w_init = NAN;
695 };
696
697 inline void ScalingRootFinder::record(const double x, const bool ok, const double obs, const double residual)
698 {
699 counts.total++;
700 (void)ok; // the outcome is decided by which output the callback wrote, not by its return
701
702 // Branch on the observable, NOT on the return value. A probe that converges to an observable
703 // below the target returns false -- it is not an answer -- but it is still a sample of the
704 // convergent branch, and filing it as a divergence both discards that sample and feeds a
705 // bogus residual into the divergent fit.
706 if (std::isfinite(obs)) {
707 S.emplace_back(x, obs);
708 std::sort(S.begin(), S.end());
709
710 if (obs >= target) {
711 if (x < x_hi) x_hi = x;
712 // With one_sided the answer is the admissible probe closest to the target from above,
713 // which is exactly the tightest upper bracket end. In BelowTarget mode nothing at or
714 // above the target is admissible at all, so the best-so-far here is only a fallback for
715 // a search that runs out of budget.
716 const bool by_magnitude = one_sided || acceptance == Acceptance::BelowTarget;
717 const bool better =
718 !have_success || (by_magnitude ? obs < best_obs : std::abs(obs - target) < std::abs(best_obs - target));
719 if (better) {
720 best_x = x;
721 best_obs = obs;
722 }
723 have_success = true;
724 } else if (acceptance == Acceptance::BelowTarget && target > 0.) {
725 // Strictly between the critical point and the target crossing: this IS the answer.
726 best_x = x;
727 best_obs = obs;
728 have_success = true;
729 answer_found = true;
730 } else {
731 // Below target, but still a convergent flow: a bracket update, NOT evidence about x_c.
732 if (x > x_lo_soft) x_lo_soft = x;
733 if (!one_sided && (!have_success || std::abs(obs - target) < std::abs(best_obs - target))) {
734 best_x = x;
735 best_obs = obs;
736 have_success = true;
737 }
738 }
739 } else {
740 if (x > x_lo_soft) x_lo_soft = x;
741 if (x > x_lo_hard) x_lo_hard = x;
742 // A failure with no residual (a different failure mode entirely) still bounds the bracket,
743 // but must never enter the divergent fit -- one NaN in a triple poisons it silently.
744 if (std::isfinite(residual)) {
745 F.emplace_back(x, residual);
746 std::sort(F.begin(), F.end());
747 }
748 }
749
750 const double w = x_hi - x_lo_soft;
752 w_prev = w;
753 }
754
756 {
759 }
760
762 {
763 model_valid = false;
764
765 // The power law is asymptotic: fit the three points closest to criticality, and only those
766 // inside the trust window.
767 std::vector<std::pair<double, double>> pts; // (obs, x)
768 for (const auto &[x, obs] : S)
769 if (!(target > 0. && obs_window > 0.) || obs <= obs_window * target) pts.emplace_back(obs, x);
770 if (pts.size() < 3) return;
771 std::sort(pts.begin(), pts.end()); // ascending in obs: closest to criticality first
772
773 const auto k = select_triple(pts.size());
774 const std::array<double, 3> xs{{pts[k[0]].second, pts[k[1]].second, pts[k[2]].second}};
775 const std::array<double, 3> ys{{pts[k[0]].first, pts[k[1]].first, pts[k[2]].first}};
776 const auto fit = detail::fit_power_law_3(xs, ys);
777 if (!fit.valid) return;
778 if (!(fit.exponent >= beta_min && fit.exponent <= beta_max)) return;
779
780 const double x_c = *std::min_element(xs.begin(), xs.end()) - fit.shift;
781 if (!(x_c > x_lo_hard && x_c < x_hi)) return;
782
783 // Two fits that disagree by an appreciable fraction of the bracket mean the model is still
784 // picking up corrections to scaling; keep the newer one but do not act on it this step.
785 const double w = x_hi - x_lo_soft;
786 const bool agrees = !std::isfinite(model_x_c) || !std::isfinite(w) || std::abs(x_c - model_x_c) < 0.1 * w;
787
789 model_x_c = x_c;
790 model_beta = fit.exponent;
791 model_log_C = fit.log_amplitude;
792 model_valid = agrees;
793 }
794
796 {
797 divergent_valid = false;
798
799 std::vector<std::pair<double, double>> pts; // (residual, x)
800 for (const auto &[x, r] : F)
801 if (r >= residual_window) pts.emplace_back(r, x);
802 if (pts.size() < 3) return;
803 std::sort(pts.begin(), pts.end(), [](const auto &a, const auto &b) { return a.first > b.first; });
804
805 // Largest residual (closest to the critical point) plus two spread across the rest.
806 // Order them by x ascending, so x3 is the closest from below and r3 the largest.
807 const auto k = select_triple(pts.size());
808 std::array<std::pair<double, double>, 3> t{{pts[k[0]], pts[k[1]], pts[k[2]]}}; // (r, x)
809 std::sort(t.begin(), t.end(), [](const auto &a, const auto &b) { return a.second < b.second; });
810 const double x1 = t[0].second, x2 = t[1].second, x3 = t[2].second;
811 const double r1 = t[0].first, r2 = t[1].first, r3 = t[2].first;
812 if (!(x1 < x2 && x2 < x3)) return;
813 if (!(r1 < r2 && r2 < r3)) return;
814
815 // Mirror onto the convergent form: X = -x, y = exp(-r). Only residual *differences* enter,
816 // so the exponentials are never formed and a large final_time cannot underflow the fit.
817 const double A = r2 - r1, B = r3 - r2;
818 const double d1 = x3 - x1, d2 = x3 - x2;
819 const double s = detail::solve_scaling_shift(A, B, d1, d2);
820 if (!std::isfinite(s)) return;
821
822 const double beta = A / std::log((s + d1) / (s + d2));
823 if (!std::isfinite(beta) || beta <= 0.) return;
824 const double theta = 1. / beta;
825 if (!(theta >= theta_min && theta <= theta_max)) return;
826
827 const double x_c = x3 + s;
828 if (!(x_c > x_lo_hard && x_c < x_hi)) return;
829
830 model_theta = theta;
831 divergent_x_c = x_c;
832 divergent_shift = s;
833 divergent_valid = true;
834 }
835
837 {
838 // 1. Seed with the two hypothesised bounds.
839 if (!probed_hi_init) {
840 probed_hi_init = true;
841 return x_hi_init;
842 }
843 if (!probed_lo_init) {
844 probed_lo_init = true;
845 return x_lo_init;
846 }
847
848 const bool have_hi = std::isfinite(x_hi);
849 const bool have_lo = std::isfinite(x_lo_soft);
850
851 // 2. One-sided: widen. The divergent fit, when available, replaces blind doubling with a
852 // targeted jump -- this is what pays for the approach phase.
853 if (!have_hi || !have_lo) {
855 if (!have_hi) {
856 if (divergent_valid) {
857 const double cand = divergent_x_c + divergent_shift;
858 if (cand > x_lo_soft && std::isfinite(cand)) {
860 return cand;
861 }
862 }
863 ++expansions;
864 counts.expand++;
865 return x_lo_soft + std::pow(expansion_factor, (double)expansions) * w_init;
866 }
867 ++expansions;
868 counts.expand++;
869 return x_hi - std::pow(expansion_factor, (double)expansions) * w_init;
870 }
871
872 const double w = x_hi - x_lo_soft;
873 double cand = NAN;
874 enum Source { NONE, MODEL_OBS, MODEL_MIXED, APPROACH, MODEL_RESIDUAL, SECANT } source = NONE;
875
876 // 3. Convergent fit: the endgame.
877 if (model_valid) {
878 cand = (target > 0.) ? extrapolate(model_x_c)
879 // Deep scaling: the target IS the singular point. Approach the fitted
880 // x_c geometrically from the convergent side and never land on it.
881 : model_x_c + 0.1 * (x_hi - model_x_c);
882 if (std::isfinite(cand)) source = MODEL_OBS;
883 }
884
885 // 4. Divergent x_c plus two convergent points. The divergent branch pins x_c long before the
886 // convergent one does, and with x_c known the exponent and amplitude follow from any two
887 // convergent points -- no third point needed. They must still be inside the scaling window:
888 // an amplitude read off at obs ~ 100x the target predicts the target's location decades out.
889 if (source == NONE && divergent_valid && target > 0. && S.size() >= 2) {
890 const double window = (obs_window > 0.) ? obs_window * target : std::numeric_limits<double>::infinity();
891 std::vector<std::pair<double, double>> in; // (obs, x), closest to criticality first
892 for (const auto &[x, y] : S)
893 if (y <= window && x > divergent_x_c) in.emplace_back(y, x);
894 std::sort(in.begin(), in.end());
895 if (in.size() >= 2) {
896 const auto [ya, xa] = in[0];
897 const auto [yb, xb] = in[1];
898 const double da = xa - divergent_x_c, db = xb - divergent_x_c;
899 if (da > 0. && db > 0. && ya > 0. && yb > 0. && da != db && ya != yb) {
900 const double beta = std::log(ya / yb) / std::log(da / db);
901 if (beta >= beta_min && beta <= beta_max) {
902 cand = divergent_x_c + da * std::exp((std::log(effective_target()) - std::log(ya)) / beta);
903 if (std::isfinite(cand)) source = MODEL_MIXED;
904 }
905 }
906 }
907 }
908
909 // 5. A critical point is known but nothing convergent lies inside the scaling window yet.
910 // This is the approach phase, and it is where a bisection is worst: the target can sit a
911 // billionth of x_c away (measured: 1.4e-9 relative on a cold SP tune), so halving the bracket
912 // needs ~30 flows to cover ground a geometric ladder covers in a handful. Step a fixed
913 // fraction of the remaining distance to x_c -- one decade per flow -- from the safe side.
914 if (source == NONE && target > 0. && std::isfinite(get_x_critical())) {
915 const double x_c = get_x_critical();
916 if (x_hi > x_c) {
917 // Never take a step a bisection would beat: capping at the midpoint means the bracket
918 // still halves every iteration, so this cannot stall and cannot trip the stagnation
919 // rule, while a good critical point makes it cover a decade instead of a factor two.
920 cand = std::min(x_c + approach_factor * (x_hi - x_c), 0.5 * (x_lo_soft + x_hi));
921 if (std::isfinite(cand)) source = APPROACH;
922 }
923 }
924
925 // 6. Divergent fit alone, no convergent point at all yet: step to the mirror of the
926 // closest failing probe across x_c. Scale-free, and needs no amplitude.
927 if (source == NONE && divergent_valid) {
929 if (std::isfinite(cand)) source = MODEL_RESIDUAL;
930 }
931
932 // 7. Two convergent points, no critical point yet: secant in log obs.
933 if (source == NONE && target > 0. && S.size() >= 2) {
934 const auto &[xa, ya] = S[S.size() - 2];
935 const auto &[xb, yb] = S[S.size() - 1];
936 if (ya > 0. && yb > 0. && ya != yb) {
937 const double la = std::log(ya / effective_target()), lb = std::log(yb / effective_target());
938 cand = xb - lb * (xb - xa) / (lb - la);
939 if (std::isfinite(cand)) source = SECANT;
940 }
941 }
942
943 // Safeguards: containment, then standoff, then Brent stagnation.
944 if (source != NONE) {
945 if (!(cand > x_lo_soft && cand < x_hi) || cand <= x_lo_hard)
946 source = NONE; // outside the live bracket, or contradicts a known-divergent point
947 else if (std::isfinite(w_prevprev) && w > 0.5 * w_prevprev) {
948 // If the bracket has not halved over the last two steps the model is not earning its
949 // place. One forced bisection caps its cost at a factor two over plain bisection.
950 source = NONE;
952 } else
953 cand = std::min(std::max(cand, x_lo_soft + endpoint_standoff * w), x_hi - endpoint_standoff * w);
954 }
955
956 switch (source) {
957 case MODEL_OBS:
959 return cand;
960 case MODEL_MIXED:
962 return cand;
963 case APPROACH:
965 return cand;
966 case MODEL_RESIDUAL:
968 return cand;
969 case SECANT:
970 counts.secant++;
971 return cand;
972 default:
973 counts.bisect++;
974 return 0.5 * (x_lo_soft + x_hi);
975 }
976 }
977
979 {
980 if (!(std::isfinite(x_lo_init) && std::isfinite(x_hi_init)))
981 throw std::runtime_error("ScalingRootFinder: search bounds were not set");
982 if (!(x_lo_init < x_hi_init))
983 throw std::runtime_error("ScalingRootFinder: search bounds are not ordered (x_lo < x_hi)");
984 if (!(target >= 0.)) throw std::runtime_error("ScalingRootFinder: target must be non-negative");
985
987 m_converged = false;
988 answer_found = false;
989 if (std::isfinite(seeded_exponent) && seeded_exponent > 0.) model_beta = seeded_exponent;
990
991 for (iter = 0; iter < max_iter; ++iter) {
992 const double x = propose();
993 if (!std::isfinite(x)) {
994 spdlog::warn("ScalingRootFinder: no admissible proposal at iteration {}; stopping", iter);
995 break;
996 }
997
998 double obs = NAN, residual = NAN;
999 const bool ok = f(x, obs, residual);
1000 record(x, ok, obs, residual);
1001 refit();
1002
1003 if (answer_found) {
1004 m_converged = true;
1005 break;
1006 }
1007
1008 if (target > 0. && acceptance == Acceptance::AtTarget) {
1009 if (have_success && std::isfinite(best_obs) && std::abs(best_obs - target) <= rel_tol * target &&
1010 (!one_sided || best_obs >= target)) {
1011 m_converged = true;
1012 break;
1013 }
1014 } else if (target <= 0. && model_valid && std::isfinite(model_x_c_prev)) {
1015 // Deep scaling: the observable tolerance is vacuous, so converge the fitted critical
1016 // point instead.
1017 if (std::abs(model_x_c - model_x_c_prev) <= rel_tol * std::abs(model_x_c)) {
1018 m_converged = true;
1019 break;
1020 }
1021 }
1022
1023 if (std::isfinite(x_hi) && std::isfinite(x_lo_soft)) {
1024 const double scale = std::max({std::abs(x_hi), std::abs(x_lo_soft), 1.});
1025 if ((x_hi - x_lo_soft) <= x_rel_floor * scale) {
1026 spdlog::warn("ScalingRootFinder: bracket collapsed to [{:.12e}, {:.12e}] after {} evaluations "
1027 "without reaching the tolerance {:.3e}",
1029 break;
1030 }
1031 }
1032
1033 // Three consecutive successes whose observable does not move: obs is insensitive to x
1034 // here, so no amount of further bracketing will resolve the target.
1035 if (S.size() >= 3) {
1036 const double y1 = S[S.size() - 1].second, y2 = S[S.size() - 2].second, y3 = S[S.size() - 3].second;
1037 const double scale = std::max(std::abs(y1), 1e-300);
1038 if (std::abs(y1 - y2) < 1e-14 * scale && std::abs(y2 - y3) < 1e-14 * scale) {
1039 spdlog::warn("ScalingRootFinder: observable stagnated at {:.12e}; stopping", y1);
1040 break;
1041 }
1042 }
1043 }
1044
1045 if (!have_success) throw std::runtime_error("ScalingRootFinder: no evaluation of the search interval succeeded");
1046
1047 if (!m_converged)
1048 spdlog::warn("ScalingRootFinder: stopped after {} evaluations without reaching the tolerance {:.3e}; "
1049 "returning x = {:.12e} with obs = {:.12e}",
1051
1052 return best_x;
1053 }
1054} // namespace DiFfRG
AbstractRootFinder(const FUN_ARR &f, const double abs_tol=1e-4, const int max_iter=1000)
Definition root_finding.hh:58
AbstractRootFinder(const FUN &f, const double abs_tol=1e-4, const int max_iter=1000)
Definition root_finding.hh:53
std::function< bool(const std::array< double, 1 > &)> FUN_ARR
Definition root_finding.hh:50
virtual std::array< double, 1 > search_impl()=0
uint iter
Definition root_finding.hh:76
FUN_ARR f
Definition root_finding.hh:72
std::function< bool(const double)> FUN
Definition root_finding.hh:49
void set_max_iter(const uint max_iter)
Definition root_finding.hh:65
void set_abs_tol(const double abs_tol)
Definition root_finding.hh:63
uint max_iter
Definition root_finding.hh:75
uint get_iter() const
Definition root_finding.hh:67
double abs_tol
Definition root_finding.hh:74
double search()
Definition root_finding.hh:69
Definition root_finding.hh:17
void set_abs_tol(const double abs_tol)
Definition root_finding.hh:27
FUN f
Definition root_finding.hh:36
virtual std::array< double, dim > search_impl()=0
void set_max_iter(const uint max_iter)
Definition root_finding.hh:29
std::function< bool(const std::array< double, dim > &)> FUN
Definition root_finding.hh:19
uint max_iter
Definition root_finding.hh:39
uint iter
Definition root_finding.hh:40
uint get_iter() const
Definition root_finding.hh:31
AbstractRootFinder(const FUN &f, const double abs_tol=1e-4, const int max_iter=1000)
Definition root_finding.hh:22
std::array< double, dim > search()
Definition root_finding.hh:33
double abs_tol
Definition root_finding.hh:38
Bisection search which converges a target value rather than the search variable.
Definition root_finding.hh:173
void set_next_x(const std::function< double(double, double)> &next_x)
Definition root_finding.hh:198
void set_abs_tol(const double abs_tol)
Definition root_finding.hh:185
uint get_iter() const
Definition root_finding.hh:206
double x_max
Definition root_finding.hh:283
BisectionRootFinderTarget(FUN f, const double abs_tol=1e-4, const int max_iter=1000)
Definition root_finding.hh:180
double abs_tol
Definition root_finding.hh:278
double x_min
Definition root_finding.hh:282
double search()
Definition root_finding.hh:219
bool m_converged
Definition root_finding.hh:287
void set_bounds(const double x_min, const double x_max)
Definition root_finding.hh:192
double get_target() const
Target value belonging to the point returned by search(). NaN before search() ran.
Definition root_finding.hh:211
std::function< bool(const double, double &)> FUN
Definition root_finding.hh:175
void set_max_iter(const uint max_iter)
Definition root_finding.hh:187
FUN f
Definition root_finding.hh:276
double x_collapse_tol
Definition root_finding.hh:284
bool converged() const
Whether the last search() met the target tolerance. False if it stopped early because the bracket col...
Definition root_finding.hh:217
void set_x_max(const double x_max)
Definition root_finding.hh:190
std::function< double(double, double)> next_x
Definition root_finding.hh:289
uint max_iter
Definition root_finding.hh:279
double target
Definition root_finding.hh:286
uint iter
Definition root_finding.hh:280
void set_x_min(const double x_min)
Definition root_finding.hh:189
void set_x_collapse_tol(const double x_collapse_tol)
Width of the x-bracket below which the search gives up. Zero (the default) selects an automatic,...
Definition root_finding.hh:204
Definition root_finding.hh:82
BisectionRootFinder(const FUN &f, const double abs_tol=1e-4, const int max_iter=1000)
Definition root_finding.hh:84
void set_x_max(const double x_max)
Definition root_finding.hh:90
void set_bounds(const double x_min, const double x_max)
Definition root_finding.hh:92
void set_x_min(const double x_min)
Definition root_finding.hh:89
std::function< double(double, double)> next_x
Definition root_finding.hh:129
void set_next_x(const std::function< double(double, double)> &next_x)
Definition root_finding.hh:98
double x_max
Definition root_finding.hh:127
std::array< double, 1 > search_impl() override
Definition root_finding.hh:101
double x_min
Definition root_finding.hh:126
Bracketed root find accelerated by the critical scaling of the observable.
Definition root_finding.hh:477
void set_bounds(const double x_lo, const double x_hi)
Definition root_finding.hh:513
double divergent_x_c
Definition root_finding.hh:681
bool bounds_are_hypotheses
Definition root_finding.hh:645
void record(double x, bool ok, double obs, double residual)
Record a probe outcome and update the bracket.
Definition root_finding.hh:697
double get_exponent() const
Definition root_finding.hh:577
ScalingRootFinder(FUN_NO_RESIDUAL f, const double target, const double rel_tol=1e-3, const uint max_iter=40)
Definition root_finding.hh:502
bool one_sided
Definition root_finding.hh:649
void set_expansion_factor(const double rho)
Definition root_finding.hh:519
double approach_factor
Definition root_finding.hh:660
double x_lo_soft
Definition root_finding.hh:667
void set_x_rel_floor(const double v)
Bracket width, relative to the magnitude of the bounds, below which the search gives up.
Definition root_finding.hh:535
FUN f
Definition root_finding.hh:639
const std::vector< std::pair< double, double > > & successes() const
Definition root_finding.hh:587
double expansion_factor
Definition root_finding.hh:646
double w_prev
Definition root_finding.hh:689
std::vector< std::pair< double, double > > F
(x, residual) for every informative failure
Definition root_finding.hh:672
void set_exponent_bounds(const double lo, const double hi)
Definition root_finding.hh:549
void set_approach_factor(const double v)
Fraction of the remaining distance to the critical point kept per approach step.
Definition root_finding.hh:565
uint expansions
Definition root_finding.hh:693
bool have_model() const
Definition root_finding.hh:579
void set_bounds_are_hypotheses(const bool v)
Definition root_finding.hh:518
void set_expansion_max_iter(const uint n)
Definition root_finding.hh:520
uint expansion_max_iter
Definition root_finding.hh:647
bool have_success
Definition root_finding.hh:684
double get_theta() const
Definition root_finding.hh:578
uint max_iter
Definition root_finding.hh:642
static std::array< std::size_t, 3 > select_triple(const std::size_t)
The three candidates closest to criticality, out of n sorted by distance to it.
Definition root_finding.hh:625
Counts counts
Definition root_finding.hh:687
double beta_max
Definition root_finding.hh:657
double extrapolate(const double x_c) const
x at which the model predicts obs == effective_target(), given a critical point.
Definition root_finding.hh:610
double obs_window
Definition root_finding.hh:655
double get_x_critical() const
Definition root_finding.hh:576
double model_x_c_prev
Definition root_finding.hh:677
double aim_fraction
Definition root_finding.hh:651
void set_residual_window(const double v)
Definition root_finding.hh:548
void set_acceptance(const Acceptance a)
Definition root_finding.hh:529
double w_prevprev
Definition root_finding.hh:690
double residual_window
Definition root_finding.hh:656
double best_obs
Definition root_finding.hh:683
void seed_exponent(const double beta)
Definition root_finding.hh:568
double model_theta
Definition root_finding.hh:676
double best_x
Definition root_finding.hh:683
void set_max_iter(const uint v)
Definition root_finding.hh:536
void refit()
Definition root_finding.hh:755
void fit_convergent()
Refit from the three convergent points closest to criticality (smallest obs).
Definition root_finding.hh:761
double effective_target() const
Definition root_finding.hh:603
bool model_valid
Definition root_finding.hh:675
void set_rel_tol(const double v)
Definition root_finding.hh:525
double bracket_width() const
Live bracket width; infinite until both sides are known.
Definition root_finding.hh:585
double seeded_exponent
Definition root_finding.hh:661
double endpoint_standoff
Definition root_finding.hh:659
void set_aim_fraction(const double v)
Definition root_finding.hh:533
double beta_min
Definition root_finding.hh:657
uint iter
Definition root_finding.hh:686
double propose()
Propose the next x.
Definition root_finding.hh:836
double get_x_extrapolated() const
Model extrapolation of the target point. NaN when no model was ever trusted.
Definition root_finding.hh:581
double x_lo_hard
Definition root_finding.hh:668
bool divergent_valid
Definition root_finding.hh:680
std::function< bool(const double, double &, double &)> FUN
Definition root_finding.hh:492
void fit_divergent()
Refit from the three divergent points closest to criticality (largest residual).
Definition root_finding.hh:795
bool m_converged
Definition root_finding.hh:685
void set_obs_window(const double v)
Fit only points with obs <= window * target. Ignored when target == 0.
Definition root_finding.hh:545
bool probed_lo_init
Definition root_finding.hh:692
double model_x_c
Definition root_finding.hh:676
double model_beta
Definition root_finding.hh:676
bool converged() const
Definition root_finding.hh:582
Counts get_counts() const
Definition root_finding.hh:594
void set_one_sided(const bool v)
Definition root_finding.hh:528
const std::vector< std::pair< double, double > > & failures() const
Definition root_finding.hh:588
std::vector< std::pair< double, double > > S
(x, obs) for every finite evaluation
Definition root_finding.hh:671
Acceptance
What counts as an answer.
Definition root_finding.hh:489
double rel_tol
Definition root_finding.hh:641
double divergent_shift
Definition root_finding.hh:681
ScalingRootFinder(FUN f, const double target, const double rel_tol=1e-3, const uint max_iter=40)
Definition root_finding.hh:497
void set_theta_bounds(const double lo, const double hi)
Definition root_finding.hh:557
uint get_iter() const
Definition root_finding.hh:583
void set_endpoint_standoff(const double v)
Fraction of the bracket width kept clear of either endpoint when clamping a proposal.
Definition root_finding.hh:563
bool probed_hi_init
Definition root_finding.hh:692
double x_hi
Definition root_finding.hh:669
Acceptance acceptance
Definition root_finding.hh:650
double theta_max
Definition root_finding.hh:658
double search()
Definition root_finding.hh:978
double get_obs() const
Definition root_finding.hh:573
double theta_min
Definition root_finding.hh:658
double w_init
Definition root_finding.hh:694
double x_rel_floor
Definition root_finding.hh:653
double x_hi_init
Definition root_finding.hh:644
bool answer_found
Definition root_finding.hh:652
std::function< bool(const double, double &)> FUN_NO_RESIDUAL
Compatibility shape matching BisectionRootFinderTarget. Disables the divergent-branch model.
Definition root_finding.hh:494
double x_lo_init
Definition root_finding.hh:644
double model_log_C
Definition root_finding.hh:676
double target
Definition root_finding.hh:640
double solve_scaling_shift(const double A, const double B, const double d1, const double d2)
Solve the three-point power-law condition for the shift .
Definition root_finding.hh:341
ScalingFit fit_power_law_3(std::array< double, 3 > x, std::array< double, 3 > y)
Three-point power-law fit. Points need not be pre-sorted.
Definition root_finding.hh:415
Definition complex_math.hh:10
unsigned int uint
Definition utils.hh:24
Definition root_finding.hh:590
uint expand
Definition root_finding.hh:591
uint model_residual
Definition root_finding.hh:591
uint secant
Definition root_finding.hh:591
uint model_mixed
Definition root_finding.hh:591
uint approach
Definition root_finding.hh:591
uint forced_bisect
Definition root_finding.hh:592
uint model_obs
Definition root_finding.hh:591
uint bisect
Definition root_finding.hh:592
uint total
Definition root_finding.hh:591
Result of a three-point power-law fit .
Definition root_finding.hh:304
double log_amplitude
ln C
Definition root_finding.hh:308
bool valid
Definition root_finding.hh:305
double exponent
beta
Definition root_finding.hh:307
double shift
s = x_3 - x_c > 0
Definition root_finding.hh:306