Hyperiso 1.0.3
Modular flavour-physics calculations, Wilson coefficients and statistical inference
Loading...
Searching...
No Matches
GradientHelper.h
Go to the documentation of this file.
1#ifndef GRADIENT_HELPER_H
2#define GRADIENT_HELPER_H
3
4#include <algorithm>
5#include <cmath>
6#include <iomanip>
7#include <iostream>
8#include <limits>
9#include <sstream>
10#include <string>
11#include <set>
12#include <stdexcept>
13#include <utility>
14#include <vector>
15
16#include "Math.h"
18
41 std::vector<double> g_eta;
43};
44
50 double nll_hat = 1e300;
51 std::vector<double> eta_hat;
52 bool ok = false;
53};
54
60 // If |dNLL/deta_i| * sigma_i is above this value after the analytic step,
61 // the direction is considered locally ill-behaved and receives Newton refinement.
62 double stationarity_threshold = 5e-2;
63
64 // Hard cap to avoid accidentally making a large expensive correction problem.
65 std::size_t max_refined_eta = 4;
66
67 // Number of cheap outer correction cycles.
68 std::size_t max_refinement_iters = 2;
69
70 // Damping line-search parameters for the Newton correction.
71 std::size_t max_line_search_halvings = 8;
73
74 double hessian_eig_floor_rel = 1e-8;
76
77 bool debug_refinement = false;
78 std::size_t debug_top_eta = 8;
79 std::string debug_label {};
80};
81
93static double fd_step(double x, double step_hint) {
94 const double abs_x = std::abs(x);
95 const double abs_hint = std::abs(step_hint);
96
97 double h = 1e-5 * std::max(1.0, abs_x);
98
99 if (std::isfinite(abs_hint) && abs_hint > 0.0) {
100 h = std::min(h, 1e-2 * abs_hint);
101 }
102
103 return std::max(h, 1e-8);
104}
105
119static double eta_fd_step_with_limits(
121 double x
122) {
123 double h = fd_step(x, def.step_hint);
124
125 if (def.limits.has_value()) {
126 const auto [lo, hi] = *def.limits;
127 const double room_minus = x - lo;
128 const double room_plus = hi - x;
129
130 const double max_symmetric_h = 0.45 * std::min(room_minus, room_plus);
131 if (max_symmetric_h > 0.0 && std::isfinite(max_symmetric_h)) {
132 h = std::min(h, max_symmetric_h);
133 }
134 }
135
136 if (!(h > 0.0) || !std::isfinite(h)) {
137 throw std::runtime_error("Invalid finite-difference step for nuisance derivative");
138 }
139
140 return h;
141}
142
150static std::vector<std::size_t> all_eta_indices(std::size_t eta_dim) {
151 std::vector<std::size_t> out(eta_dim);
152 for (std::size_t i = 0; i < eta_dim; ++i) {
153 out[i] = i;
154 }
155 return out;
156}
157
166static std::vector<std::size_t> eta_index_complement(
167 std::size_t eta_dim,
168 const std::vector<std::size_t>& excluded
169) {
170 std::set<std::size_t> excluded_set(excluded.begin(), excluded.end());
171
172 std::vector<std::size_t> out;
173 out.reserve(eta_dim);
174
175 for (std::size_t i = 0; i < eta_dim; ++i) {
176 if (!excluded_set.contains(i)) {
177 out.push_back(i);
178 }
179 }
180
181 return out;
182}
183
192static bool eta_index_contains(
193 const std::vector<std::size_t>& indices,
194 std::size_t idx
195) {
196 return std::find(indices.begin(), indices.end(), idx) != indices.end();
197}
198
207static RealMatrix principal_submatrix_by_indices(
208 const RealMatrix& M,
209 const std::vector<std::size_t>& idx
210) {
211 RealMatrix out(idx.size(), idx.size());
212
213 for (std::size_t i = 0; i < idx.size(); ++i) {
214 for (std::size_t j = 0; j < idx.size(); ++j) {
215 out.at(i, j) = M.at(idx[i], idx[j]);
216 }
217 }
218
219 return out;
220}
221
237static EtaDerivatives compute_eta_derivatives_subset(
238 const IProfileableLikelihood& like,
239 const std::vector<double>& p,
240 const std::vector<double>& eta_base,
241 const std::vector<std::size_t>& eta_indices
242) {
243 const auto defs = like.get_param_defs();
244
245 const std::size_t p_dim = like.p_dimension();
246 const std::size_t eta_dim = like.eta_dimension();
247
248 const std::vector<double> f0 = like.predict(p, eta_base);
249 const std::size_t n_obs = f0.size();
250
251 EtaDerivatives out;
252 out.g_eta.assign(eta_indices.size(), 0.0);
253 out.J_eta = RealMatrix(n_obs, eta_indices.size());
254
255 for (std::size_t col = 0; col < eta_indices.size(); ++col) {
256 const std::size_t a = eta_indices[col];
257 if (a >= eta_dim) {
258 throw std::runtime_error("Eta derivative index out of range");
259 }
260
261 const std::size_t theta_index = p_dim + a;
262 const auto& def = defs[theta_index];
263
264 const double h = eta_fd_step_with_limits(def, eta_base[a]);
265
266 std::vector<double> eta_plus = eta_base;
267 std::vector<double> eta_minus = eta_base;
268
269 eta_plus[a] += h;
270 eta_minus[a] -= h;
271
272 const std::vector<double> f_plus = like.predict(p, eta_plus);
273 const std::vector<double> f_minus = like.predict(p, eta_minus);
274
275 if (f_plus.size() != n_obs || f_minus.size() != n_obs) {
276 throw std::runtime_error("Model prediction size changed during finite difference");
277 }
278
279 for (std::size_t k = 0; k < n_obs; ++k) {
280 out.J_eta.at(k, col) = (f_plus[k] - f_minus[k]) / (2.0 * h);
281 }
282
283 const double nll_plus = like.nll_from_split(p, eta_plus);
284 const double nll_minus = like.nll_from_split(p, eta_minus);
285
286 out.g_eta[col] = (nll_plus - nll_minus) / (2.0 * h);
287 }
288
289 return out;
290}
291
301static EtaDerivatives compute_eta_derivatives(
302 const IProfileableLikelihood& like,
303 const std::vector<double>& p,
304 const std::vector<double>& eta0
305) {
306 return compute_eta_derivatives_subset(
307 like,
308 p,
309 eta0,
310 all_eta_indices(like.eta_dimension())
311 );
312}
313
326static std::vector<double> eta_gradient_nll_subset(
327 const IProfileableLikelihood& like,
328 const std::vector<double>& p,
329 const std::vector<double>& eta,
330 const std::vector<std::size_t>& eta_indices
331) {
332 const auto defs = like.get_param_defs();
333 const std::size_t p_dim = like.p_dimension();
334 const std::size_t eta_dim = like.eta_dimension();
335
336 std::vector<double> g(eta_indices.size(), 0.0);
337
338 for (std::size_t col = 0; col < eta_indices.size(); ++col) {
339 const std::size_t a = eta_indices[col];
340 if (a >= eta_dim) {
341 throw std::runtime_error("eta_gradient_nll_subset: eta index out of range");
342 }
343
344 const auto& def = defs[p_dim + a];
345 const double h = eta_fd_step_with_limits(def, eta[a]);
346
347 std::vector<double> eta_plus = eta;
348 std::vector<double> eta_minus = eta;
349 eta_plus[a] += h;
350 eta_minus[a] -= h;
351
352 const double fp = like.nll_from_split(p, eta_plus);
353 const double fm = like.nll_from_split(p, eta_minus);
354
355 g[col] = (fp - fm) / (2.0 * h);
356 }
357
358 return g;
359}
360
370static std::vector<double> eta_gradient_nll(
371 const IProfileableLikelihood& like,
372 const std::vector<double>& p,
373 const std::vector<double>& eta
374) {
375 return eta_gradient_nll_subset(
376 like,
377 p,
378 eta,
379 all_eta_indices(like.eta_dimension())
380 );
381}
382
393static std::vector<double> matvec(const RealMatrix& M, const std::vector<double>& v) {
394 if (M.cols() != v.size()) {
395 throw std::runtime_error("matvec: dimension mismatch");
396 }
397
398 std::vector<double> out(M.rows(), 0.0);
399
400 for (std::size_t i = 0; i < M.rows(); ++i) {
401 for (std::size_t j = 0; j < M.cols(); ++j) {
402 out[i] += M.at(i, j) * v[j];
403 }
404 }
405
406 return out;
407}
408
419static double dot(const std::vector<double>& a, const std::vector<double>& b) {
420 if (a.size() != b.size()) {
421 throw std::runtime_error("dot: dimension mismatch");
422 }
423
424 double out = 0.0;
425
426 for (std::size_t i = 0; i < a.size(); ++i) {
427 out += a[i] * b[i];
428 }
429
430 return out;
431}
432
446static RealMatrix force_symmetric_checked(
447 const RealMatrix& H,
448 const std::string& label = "matrix"
449) {
450 if (H.rows() != H.cols()) {
451 throw std::runtime_error(label + " is not square");
452 }
453
454 RealMatrix sym(H.rows(), H.cols());
455
456 double max_asym = 0.0;
457 double max_abs = 0.0;
458
459 for (std::size_t i = 0; i < H.rows(); ++i) {
460 for (std::size_t j = 0; j < H.cols(); ++j) {
461 const double a = H.at(i, j);
462 const double b = H.at(j, i);
463
464 if (!std::isfinite(a) || !std::isfinite(b)) {
465 std::ostringstream oss;
466 oss << label
467 << " contains non-finite value at ("
468 << i << "," << j << ") or transpose entry";
469 throw std::runtime_error(oss.str());
470 }
471
472 const double v = 0.5 * (a + b);
473 sym.at(i, j) = v;
474
475 max_asym = std::max(max_asym, std::abs(a - b));
476 max_abs = std::max(max_abs, std::max(std::abs(a), std::abs(b)));
477 }
478 }
479
480 // Currently kept for easy debugging if this helper needs to report asymmetry.
481 (void)max_asym;
482 (void)max_abs;
483
484 // Re-force exact symmetry entry-by-entry, in case RealMatrix operators
485 // or storage order leave tiny inconsistencies.
486 for (std::size_t i = 0; i < H.rows(); ++i) {
487 for (std::size_t j = i + 1; j < H.cols(); ++j) {
488 const double v = 0.5 * (sym.at(i, j) + sym.at(j, i));
489 sym.at(i, j) = v;
490 sym.at(j, i) = v;
491 }
492 }
493
494 return sym;
495}
496
513static RealMatrix regularize_spd_local(
514 const RealMatrix& H,
515 double rel_floor = 1e-10,
516 const std::string& label = "Laplace Hessian"
517) {
518 RealMatrix sym = force_symmetric_checked(H, label);
519
520 EigenSystem eig;
521 try {
522 eig = sym.eig();
523 } catch (const std::exception& e) {
524 std::ostringstream oss;
525 oss << label << " eig() failed after explicit symmetrization: "
526 << e.what();
527 throw std::runtime_error(oss.str());
528 }
529
530 double max_pos = 0.0;
531 for (std::size_t i = 0; i < eig.D.rows(); ++i) {
532 const double ev = eig.D.at(i, i);
533 if (!std::isfinite(ev)) {
534 throw std::runtime_error(label + " has non-finite eigenvalue");
535 }
536 if (ev > 0.0) {
537 max_pos = std::max(max_pos, ev);
538 }
539 }
540
541 if (!(max_pos > 0.0)) {
542 throw std::runtime_error(label + " is not positive definite");
543 }
544
545 const double floor = std::max(1e-12, rel_floor * max_pos);
546
547 RealMatrix Dreg(eig.D.rows(), eig.D.cols());
548
549 for (std::size_t i = 0; i < eig.D.rows(); ++i) {
550 Dreg.at(i, i) = std::max(eig.D.at(i, i), floor);
551 }
552
553 RealMatrix out = eig.P * Dreg * eig.P.transpose();
554
555 // Force symmetry one final time.
556 return force_symmetric_checked(out, label + " regularized");
557}
558
571static std::vector<std::pair<double, std::size_t>> rank_eta_stationarity(
572 const IProfileableLikelihood& like,
573 const std::vector<double>& p,
574 const std::vector<double>& eta
575) {
576 const std::size_t p_dim = like.p_dimension();
577 const std::size_t eta_dim = like.eta_dimension();
578 const auto defs = like.get_param_defs();
579
580 const std::vector<double> g = eta_gradient_nll(like, p, eta);
581
582 std::vector<std::pair<double, std::size_t>> ranked;
583 ranked.reserve(eta_dim);
584
585 for (std::size_t a = 0; a < eta_dim; ++a) {
586 const double sigma = std::abs(defs[p_dim + a].step_hint);
587 const double scaled = (sigma > 0.0 && std::isfinite(sigma))
588 ? std::abs(g[a]) * sigma
589 : std::abs(g[a]);
590
591 if (std::isfinite(scaled)) {
592 ranked.push_back({scaled, a});
593 }
594 }
595
596 std::sort(
597 ranked.begin(),
598 ranked.end(),
599 [](const auto& lhs, const auto& rhs) {
600 return lhs.first > rhs.first;
601 }
602 );
603
604 return ranked;
605}
606
621static std::vector<std::size_t> select_nonstationary_eta_indices(
622 const IProfileableLikelihood& like,
623 const std::vector<double>& p,
624 const std::vector<double>& eta,
625 const LaplaceProfileOptions& options
626) {
627 std::vector<std::pair<double, std::size_t>> ranked =
628 rank_eta_stationarity(like, p, eta);
629
630 ranked.erase(
631 std::remove_if(
632 ranked.begin(),
633 ranked.end(),
634 [&](const auto& item) {
635 return item.first <= options.stationarity_threshold;
636 }
637 ),
638 ranked.end()
639 );
640
641 if (ranked.size() > options.max_refined_eta) {
642 ranked.resize(options.max_refined_eta);
643 }
644
645 std::vector<std::size_t> out;
646 out.reserve(ranked.size());
647
648 for (const auto& [_, idx] : ranked) {
649 out.push_back(idx);
650 }
651
652 std::sort(out.begin(), out.end());
653 return out;
654}
655
663static std::string eta_index_list_string(
664 const std::vector<std::size_t>& indices
665) {
666 std::ostringstream oss;
667 oss << "{";
668 for (std::size_t i = 0; i < indices.size(); ++i) {
669 if (i) oss << ",";
670 oss << indices[i];
671 }
672 oss << "}";
673 return oss.str();
674}
675
689static void debug_print_stationarity_summary(
690 const IProfileableLikelihood& like,
691 const std::vector<double>& p,
692 const std::vector<double>& eta,
693 const LaplaceProfileOptions& options,
694 std::size_t iter,
695 double direct_nll,
696 const std::vector<std::size_t>& bad
697) {
698 if (!options.debug_refinement) {
699 return;
700 }
701
702 const auto defs = like.get_param_defs();
703 const std::size_t p_dim = like.p_dimension();
704
705 const auto ranked = rank_eta_stationarity(like, p, eta);
706 const double max_scaled = ranked.empty() ? 0.0 : ranked.front().first;
707
708 std::cout << "[LAPLACE WARMUP] " << options.debug_label
709 << " iter=" << iter
710 << " direct_nll=" << std::setprecision(12) << direct_nll
711 << " max_scaled_grad=" << max_scaled
712 << " threshold=" << options.stationarity_threshold
713 << " status=" << (bad.empty() ? "PURE_LAPLACE_OK" : "REFINE")
714 << " refine_eta=" << eta_index_list_string(bad)
715 << std::endl;
716
717 const std::size_t n_show = std::min(options.debug_top_eta, ranked.size());
718 for (std::size_t k = 0; k < n_show; ++k) {
719 const double scaled = ranked[k].first;
720 const std::size_t a = ranked[k].second;
721 const bool will_refine = eta_index_contains(bad, a);
722
723 std::cout << " [LAPLACE WARMUP] rank=" << k
724 << " eta_idx=" << a
725 << " theta_idx=" << (p_dim + a)
726 << " name=" << defs[p_dim + a].name
727 << " scaled_grad=" << std::setprecision(12) << scaled
728 << " action=" << (will_refine ? "REFINE" : "laplace-only")
729 << std::endl;
730 }
731}
732
746static RealMatrix numerical_eta_hessian_subset(
747 const IProfileableLikelihood& like,
748 const std::vector<double>& p,
749 const std::vector<double>& eta,
750 const std::vector<std::size_t>& eta_indices
751) {
752 const std::size_t m = eta_indices.size();
753 const auto defs = like.get_param_defs();
754 const std::size_t p_dim = like.p_dimension();
755
756 RealMatrix H(m, m);
757 if (m == 0) {
758 return H;
759 }
760
761 std::vector<double> h(m, 0.0);
762 for (std::size_t c = 0; c < m; ++c) {
763 const std::size_t a = eta_indices[c];
764 h[c] = eta_fd_step_with_limits(defs[p_dim + a], eta[a]);
765 }
766
767 const double f0 = like.nll_from_split(p, eta);
768
769 for (std::size_t ci = 0; ci < m; ++ci) {
770 const std::size_t ai = eta_indices[ci];
771
772 std::vector<double> ep = eta;
773 std::vector<double> em = eta;
774 ep[ai] += h[ci];
775 em[ai] -= h[ci];
776
777 const double fp = like.nll_from_split(p, ep);
778 const double fm = like.nll_from_split(p, em);
779
780 H.at(ci, ci) = (fp - 2.0 * f0 + fm) / (h[ci] * h[ci]);
781
782 for (std::size_t cj = ci + 1; cj < m; ++cj) {
783 const std::size_t aj = eta_indices[cj];
784
785 std::vector<double> epp = eta;
786 std::vector<double> epm = eta;
787 std::vector<double> emp = eta;
788 std::vector<double> emm = eta;
789
790 epp[ai] += h[ci]; epp[aj] += h[cj];
791 epm[ai] += h[ci]; epm[aj] -= h[cj];
792 emp[ai] -= h[ci]; emp[aj] += h[cj];
793 emm[ai] -= h[ci]; emm[aj] -= h[cj];
794
795 const double fpp = like.nll_from_split(p, epp);
796 const double fpm = like.nll_from_split(p, epm);
797 const double fmp = like.nll_from_split(p, emp);
798 const double fmm = like.nll_from_split(p, emm);
799
800 const double hij = (fpp - fpm - fmp + fmm) / (4.0 * h[ci] * h[cj]);
801
802 H.at(ci, cj) = hij;
803 H.at(cj, ci) = hij;
804 }
805 }
806
807 return force_symmetric_checked(H, "Newton finite-difference H_bad raw");
808}
809
818static void clamp_eta_to_limits(
819 const IProfileableLikelihood& like,
820 std::vector<double>& eta
821) {
822 const auto defs = like.get_param_defs();
823 const std::size_t p_dim = like.p_dimension();
824
825 for (std::size_t a = 0; a < eta.size(); ++a) {
826 const auto& def = defs[p_dim + a];
827 if (def.limits.has_value()) {
828 const auto [lo, hi] = *def.limits;
829 eta[a] = std::clamp(eta[a], lo, hi);
830 }
831 }
832}
833
846static void clamp_eta_step_in_sigmas(
847 const IProfileableLikelihood& like,
848 const std::vector<std::size_t>& eta_indices,
849 std::vector<double>& delta,
850 double max_step_in_sigma
851) {
852 if (!(max_step_in_sigma > 0.0) || !std::isfinite(max_step_in_sigma)) {
853 return;
854 }
855
856 const auto defs = like.get_param_defs();
857 const std::size_t p_dim = like.p_dimension();
858
859 double scale = 1.0;
860
861 for (std::size_t c = 0; c < eta_indices.size(); ++c) {
862 const std::size_t a = eta_indices[c];
863 const double sigma = std::abs(defs[p_dim + a].step_hint);
864
865 if (sigma > 0.0 && std::isfinite(sigma)) {
866 const double allowed = max_step_in_sigma * sigma;
867 const double step = std::abs(delta[c]);
868 if (step > allowed && step > 0.0) {
869 scale = std::min(scale, allowed / step);
870 }
871 }
872 }
873
874 for (double& v : delta) {
875 v *= scale;
876 }
877}
878
898static LaplaceProfileComputation laplace_profile_eta_subset(
899 const IProfileableLikelihood& like,
900 const std::vector<double>& p,
901 const std::vector<double>& eta_base,
902 const std::vector<std::size_t>& laplace_eta_indices,
903 double hessian_eig_floor_rel = 1e-10,
904 const std::string& hessian_label = "Laplace H_eta"
905) {
906 if (eta_base.size() != like.eta_dimension()) {
907 throw std::runtime_error("laplace_profile_eta_subset: eta_base has wrong dimension");
908 }
909
911 out.eta_hat = eta_base;
912
913 const double nll0 = like.nll_from_split(p, eta_base);
914
915 if (laplace_eta_indices.empty()) {
916 out.nll_hat = nll0;
917 out.ok = std::isfinite(out.nll_hat);
918 return out;
919 }
920
921 const std::vector<double> r0 = like.residuals(p, eta_base);
922
923 const RealMatrix W_obs = force_symmetric_checked(
924 like.observable_curvature(r0),
925 "W_obs"
926 );
927
928 const RealMatrix W_eta_full = force_symmetric_checked(
929 like.nuisance_curvature(eta_base),
930 "W_eta full"
931 );
932
933 const RealMatrix W_eta = force_symmetric_checked(
934 principal_submatrix_by_indices(W_eta_full, laplace_eta_indices),
935 "W_eta subset"
936 );
937
938 const EtaDerivatives der = compute_eta_derivatives_subset(
939 like,
940 p,
941 eta_base,
942 laplace_eta_indices
943 );
944
945 const RealMatrix H_raw =
946 der.J_eta.transpose() * W_obs * der.J_eta + W_eta;
947
948 const RealMatrix H = regularize_spd_local(H_raw, hessian_eig_floor_rel, hessian_label);
949
950 const RealMatrix H_inv = H.inv();
951
952 const std::vector<double> Hinv_g = matvec(H_inv, der.g_eta);
953
954 const double correction = 0.5 * dot(der.g_eta, Hinv_g);
955
956 out.nll_hat = nll0 - correction;
957
958 for (std::size_t col = 0; col < laplace_eta_indices.size(); ++col) {
959 const std::size_t a = laplace_eta_indices[col];
960 out.eta_hat[a] -= Hinv_g[col];
961 }
962
963 clamp_eta_to_limits(like, out.eta_hat);
964
965 out.ok = std::isfinite(out.nll_hat);
966 return out;
967}
968
983static LaplaceProfileComputation laplace_profile_eta_refined(
984 const IProfileableLikelihood& like,
985 const std::vector<double>& p,
986 const LaplaceProfileOptions& options = {}
987) {
988 const std::size_t eta_dim = like.eta_dimension();
989
991 laplace_profile_eta_subset(
992 like,
993 p,
994 like.central_eta(),
995 all_eta_indices(eta_dim),
996 options.hessian_eig_floor_rel,
997 "Laplace initial H_eta"
998 );
999
1000 if (!best.ok) {
1001 return best;
1002 }
1003
1004 double best_direct = like.nll_from_split(p, best.eta_hat);
1005
1006 for (std::size_t iter = 0; iter < options.max_refinement_iters; ++iter) {
1007 const std::vector<std::size_t> bad =
1008 select_nonstationary_eta_indices(like, p, best.eta_hat, options);
1009
1010 debug_print_stationarity_summary(
1011 like,
1012 p,
1013 best.eta_hat,
1014 options,
1015 iter,
1016 best_direct,
1017 bad
1018 );
1019
1020 if (bad.empty()) {
1021 break;
1022 }
1023
1024 const std::vector<std::size_t> laplace_indices =
1025 eta_index_complement(eta_dim, bad);
1026
1027 const std::vector<double> g_bad =
1028 eta_gradient_nll_subset(like, p, best.eta_hat, bad);
1029
1030 RealMatrix H_bad =
1031 numerical_eta_hessian_subset(like, p, best.eta_hat, bad);
1032
1033 try {
1034 H_bad = regularize_spd_local(
1035 H_bad,
1036 options.hessian_eig_floor_rel,
1037 "Newton refine H_bad"
1038 );
1039 } catch (const std::exception& e) {
1040 // The Newton correction block is locally non-convex or numerically unusable.
1041 // Do not invalidate the whole profile point; keep the best point found so far.
1042 if (options.debug_refinement) {
1043 std::cout << "[LAPLACE WARMUP] " << options.debug_label
1044 << " iter=" << iter
1045 << " newton_hessian=rejected"
1046 << " reason=" << e.what()
1047 << " refined_eta=" << eta_index_list_string(bad)
1048 << std::endl;
1049 }
1050 break;
1051 }
1052
1053 std::vector<double> delta = matvec(H_bad.inv(), g_bad);
1054 for (double& v : delta) {
1055 v = -v;
1056 }
1057
1058 clamp_eta_step_in_sigmas(
1059 like,
1060 bad,
1061 delta,
1063 );
1064
1065 bool accepted = false;
1066 LaplaceProfileComputation accepted_comp = best;
1067 double accepted_direct = best_direct;
1068
1069 for (std::size_t ls = 0; ls <= options.max_line_search_halvings; ++ls) {
1070 const double alpha = std::ldexp(1.0, -static_cast<int>(ls));
1071
1072 std::vector<double> eta_trial = best.eta_hat;
1073 for (std::size_t c = 0; c < bad.size(); ++c) {
1074 eta_trial[bad[c]] += alpha * delta[c];
1075 }
1076
1077 clamp_eta_to_limits(like, eta_trial);
1078
1080 laplace_profile_eta_subset(
1081 like,
1082 p,
1083 eta_trial,
1084 laplace_indices,
1085 options.hessian_eig_floor_rel,
1086 "Laplace refined subset H_eta"
1087 );
1088
1089 if (!trial.ok) {
1090 continue;
1091 }
1092
1093 const double direct_trial = like.nll_from_split(p, trial.eta_hat);
1094
1095 if (std::isfinite(direct_trial) &&
1096 (!std::isfinite(best_direct) || direct_trial <= best_direct + 1e-10)) {
1097 accepted = true;
1098 accepted_comp = std::move(trial);
1099 accepted_direct = direct_trial;
1100 break;
1101 }
1102 }
1103
1104 if (!accepted) {
1105 if (options.debug_refinement) {
1106 std::cout << "[LAPLACE WARMUP] " << options.debug_label
1107 << " iter=" << iter
1108 << " newton_step=rejected"
1109 << " refined_eta=" << eta_index_list_string(bad)
1110 << std::endl;
1111 }
1112 break;
1113 }
1114
1115 if (options.debug_refinement) {
1116 std::cout << "[LAPLACE WARMUP] " << options.debug_label
1117 << " iter=" << iter
1118 << " newton_step=accepted"
1119 << " direct_before=" << std::setprecision(12) << best_direct
1120 << " direct_after=" << accepted_direct
1121 << " refined_eta=" << eta_index_list_string(bad)
1122 << std::endl;
1123 }
1124
1125 best = std::move(accepted_comp);
1126 best_direct = accepted_direct;
1127 }
1128
1129 if (options.use_direct_nll_for_final_value && std::isfinite(best_direct)) {
1130 best.nll_hat = best_direct;
1131 }
1132
1133 best.ok = best.ok && std::isfinite(best.nll_hat);
1134 return best;
1135}
1136
1145static LaplaceProfileComputation laplace_profile_eta(
1146 const IProfileableLikelihood& like,
1147 const std::vector<double>& p
1148) {
1149 return laplace_profile_eta_refined(like, p);
1150}
1151
1152#endif
Interface for likelihoods that separate fitted and nuisance parameters.
virtual std::vector< fit_app::ParameterDefinition > get_param_defs() const =0
Returns the metadata describing the likelihood parameters.
Extension of ILikelihood with explicit parameter-block access.
virtual std::vector< double > predict(const std::vector< double > &p, const std::vector< double > &eta) const =0
Evaluates the model prediction for split parameters.
virtual RealMatrix nuisance_curvature(const std::vector< double > &eta) const =0
Computes the nuisance-term curvature matrix.
virtual std::vector< double > central_eta() const =0
Returns the central values of the nuisance parameters.
virtual double nll_from_split(const std::vector< double > &p, const std::vector< double > &eta) const =0
Evaluates the negative log-likelihood from split parameters.
virtual std::size_t eta_dimension() const =0
Returns the dimension of the nuisance-parameter block.
virtual std::vector< double > residuals(const std::vector< double > &p, const std::vector< double > &eta) const =0
Computes observable residuals for split parameters.
virtual std::size_t p_dimension() const =0
Returns the dimension of the fitted-parameter block.
virtual RealMatrix observable_curvature(const std::vector< double > &residuals) const =0
Computes the observable-term curvature matrix.
std::size_t rows() const
Returns the number of rows.
Definition Matrix.cpp:601
EigenSystem eig() const
Computes the eigensystem of a symmetric matrix.
Definition Matrix.cpp:710
double & at(size_t i, size_t j)
Returns a mutable reference to element (i,j) with bounds checking.
Definition Matrix.cpp:587
std::size_t cols() const
Returns the number of columns.
Definition Matrix.cpp:605
RealMatrix transpose() const
Returns the transpose of the matrix.
Definition Matrix.cpp:698
RealMatrix inv() const
Computes the inverse of the matrix via LU decomposition.
Definition Matrix.cpp:750
constexpr double g
complex_t h(double s, double m_q, double mu_b)
complex_t H(double z, double r_P)
csl::Expr v
Definition sm.h:110
Container for an eigendecomposition.
Definition Matrix.h:489
RealMatrix D
Definition Matrix.h:490
RealMatrix P
Diagonal matrix of eigenvalues.
Definition Matrix.h:491
First-order derivatives of the likelihood with respect to selected nuisance parameters.
RealMatrix J_eta
Observable Jacobian with rows as observables and columns as nuisance directions.
std::vector< double > g_eta
NLL gradient restricted to the selected nuisance directions.
Result of a Laplace nuisance-profile computation.
bool ok
True when the computation produced a finite, usable result.
double nll_hat
Profiled or approximate profiled NLL value.
std::vector< double > eta_hat
Estimated profiled nuisance vector.
Numerical controls for the hybrid Laplace/Newton nuisance profiler.
bool debug_refinement
If true, print stationarity and refinement diagnostics.
std::size_t max_refinement_iters
Maximum number of outer correction cycles.
double max_newton_step_in_sigma
Maximum Newton displacement measured in nuisance standard deviations.
double stationarity_threshold
Threshold on above which a direction is refined.
std::string debug_label
Optional label appended to debug messages.
double hessian_eig_floor_rel
Relative eigenvalue floor used when regularizing Hessians.
std::size_t max_refined_eta
Maximum number of nuisance directions corrected by Newton refinement.
bool use_direct_nll_for_final_value
If true, use the direct NLL at the final profiled point as the reported value.
std::size_t max_line_search_halvings
Maximum number of backtracking halvings for a Newton step.
std::size_t debug_top_eta
Maximum number of nuisance directions shown in debug output.
std::optional< std::pair< double, double > > limits