1#ifndef GRADIENT_HELPER_H
2#define GRADIENT_HELPER_H
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);
97 double h = 1e-5 * std::max(1.0, abs_x);
99 if (std::isfinite(abs_hint) && abs_hint > 0.0) {
100 h = std::min(h, 1e-2 * abs_hint);
103 return std::max(h, 1e-8);
119static double eta_fd_step_with_limits(
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;
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);
136 if (!(h > 0.0) || !std::isfinite(h)) {
137 throw std::runtime_error(
"Invalid finite-difference step for nuisance derivative");
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) {
166static std::vector<std::size_t> eta_index_complement(
168 const std::vector<std::size_t>& excluded
170 std::set<std::size_t> excluded_set(excluded.begin(), excluded.end());
172 std::vector<std::size_t> out;
173 out.reserve(eta_dim);
175 for (std::size_t i = 0; i < eta_dim; ++i) {
176 if (!excluded_set.contains(i)) {
192static bool eta_index_contains(
193 const std::vector<std::size_t>& indices,
196 return std::find(indices.begin(), indices.end(), idx) != indices.end();
207static RealMatrix principal_submatrix_by_indices(
209 const std::vector<std::size_t>& idx
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]);
239 const std::vector<double>& p,
240 const std::vector<double>& eta_base,
241 const std::vector<std::size_t>& eta_indices
248 const std::vector<double> f0 = like.
predict(p, eta_base);
249 const std::size_t n_obs = f0.size();
252 out.
g_eta.assign(eta_indices.size(), 0.0);
255 for (std::size_t col = 0; col < eta_indices.size(); ++col) {
256 const std::size_t a = eta_indices[col];
258 throw std::runtime_error(
"Eta derivative index out of range");
261 const std::size_t theta_index = p_dim + a;
262 const auto& def = defs[theta_index];
264 const double h = eta_fd_step_with_limits(def, eta_base[a]);
266 std::vector<double> eta_plus = eta_base;
267 std::vector<double> eta_minus = eta_base;
272 const std::vector<double> f_plus = like.
predict(p, eta_plus);
273 const std::vector<double> f_minus = like.
predict(p, eta_minus);
275 if (f_plus.size() != n_obs || f_minus.size() != n_obs) {
276 throw std::runtime_error(
"Model prediction size changed during finite difference");
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);
286 out.
g_eta[col] = (nll_plus - nll_minus) / (2.0 * h);
303 const std::vector<double>& p,
304 const std::vector<double>& eta0
306 return compute_eta_derivatives_subset(
326static std::vector<double> eta_gradient_nll_subset(
328 const std::vector<double>& p,
329 const std::vector<double>& eta,
330 const std::vector<std::size_t>& eta_indices
336 std::vector<double>
g(eta_indices.size(), 0.0);
338 for (std::size_t col = 0; col < eta_indices.size(); ++col) {
339 const std::size_t a = eta_indices[col];
341 throw std::runtime_error(
"eta_gradient_nll_subset: eta index out of range");
344 const auto& def = defs[p_dim + a];
345 const double h = eta_fd_step_with_limits(def, eta[a]);
347 std::vector<double> eta_plus = eta;
348 std::vector<double> eta_minus = eta;
355 g[col] = (fp - fm) / (2.0 * h);
370static std::vector<double> eta_gradient_nll(
372 const std::vector<double>& p,
373 const std::vector<double>& eta
375 return eta_gradient_nll_subset(
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");
398 std::vector<double> out(M.
rows(), 0.0);
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];
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");
426 for (std::size_t i = 0; i < a.size(); ++i) {
448 const std::string& label =
"matrix"
450 if (
H.rows() !=
H.cols()) {
451 throw std::runtime_error(label +
" is not square");
456 double max_asym = 0.0;
457 double max_abs = 0.0;
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);
464 if (!std::isfinite(a) || !std::isfinite(b)) {
465 std::ostringstream oss;
467 <<
" contains non-finite value at ("
468 << i <<
"," << j <<
") or transpose entry";
469 throw std::runtime_error(oss.str());
472 const double v = 0.5 * (a + b);
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)));
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));
515 double rel_floor = 1e-10,
516 const std::string& label =
"Laplace Hessian"
518 RealMatrix sym = force_symmetric_checked(H, label);
523 }
catch (
const std::exception& e) {
524 std::ostringstream oss;
525 oss << label <<
" eig() failed after explicit symmetrization: "
527 throw std::runtime_error(oss.str());
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");
537 max_pos = std::max(max_pos, ev);
541 if (!(max_pos > 0.0)) {
542 throw std::runtime_error(label +
" is not positive definite");
545 const double floor = std::max(1e-12, rel_floor * max_pos);
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);
556 return force_symmetric_checked(out, label +
" regularized");
571static std::vector<std::pair<double, std::size_t>> rank_eta_stationarity(
573 const std::vector<double>& p,
574 const std::vector<double>& eta
580 const std::vector<double>
g = eta_gradient_nll(like, p, eta);
582 std::vector<std::pair<double, std::size_t>> ranked;
583 ranked.reserve(eta_dim);
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
591 if (std::isfinite(scaled)) {
592 ranked.push_back({scaled, a});
599 [](
const auto& lhs,
const auto& rhs) {
600 return lhs.first > rhs.first;
621static std::vector<std::size_t> select_nonstationary_eta_indices(
623 const std::vector<double>& p,
624 const std::vector<double>& eta,
627 std::vector<std::pair<double, std::size_t>> ranked =
628 rank_eta_stationarity(like, p, eta);
634 [&](
const auto& item) {
635 return item.first <= options.stationarity_threshold;
645 std::vector<std::size_t> out;
646 out.reserve(ranked.size());
648 for (
const auto& [_, idx] : ranked) {
652 std::sort(out.begin(), out.end());
663static std::string eta_index_list_string(
664 const std::vector<std::size_t>& indices
666 std::ostringstream oss;
668 for (std::size_t i = 0; i < indices.size(); ++i) {
689static void debug_print_stationarity_summary(
691 const std::vector<double>& p,
692 const std::vector<double>& eta,
696 const std::vector<std::size_t>& bad
705 const auto ranked = rank_eta_stationarity(like, p, eta);
706 const double max_scaled = ranked.empty() ? 0.0 : ranked.front().first;
708 std::cout <<
"[LAPLACE WARMUP] " << options.
debug_label
710 <<
" direct_nll=" << std::setprecision(12) << direct_nll
711 <<
" max_scaled_grad=" << max_scaled
713 <<
" status=" << (bad.empty() ?
"PURE_LAPLACE_OK" :
"REFINE")
714 <<
" refine_eta=" << eta_index_list_string(bad)
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);
723 std::cout <<
" [LAPLACE WARMUP] rank=" << k
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")
746static RealMatrix numerical_eta_hessian_subset(
748 const std::vector<double>& p,
749 const std::vector<double>& eta,
750 const std::vector<std::size_t>& eta_indices
752 const std::size_t m = eta_indices.size();
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]);
769 for (std::size_t ci = 0; ci < m; ++ci) {
770 const std::size_t ai = eta_indices[ci];
772 std::vector<double> ep = eta;
773 std::vector<double> em = eta;
780 H.at(ci, ci) = (fp - 2.0 * f0 + fm) / (h[ci] * h[ci]);
782 for (std::size_t cj = ci + 1; cj < m; ++cj) {
783 const std::size_t aj = eta_indices[cj];
785 std::vector<double> epp = eta;
786 std::vector<double> epm = eta;
787 std::vector<double> emp = eta;
788 std::vector<double> emm = eta;
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];
800 const double hij = (fpp - fpm - fmp + fmm) / (4.0 * h[ci] * h[cj]);
807 return force_symmetric_checked(H,
"Newton finite-difference H_bad raw");
818static void clamp_eta_to_limits(
820 std::vector<double>& eta
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);
846static void clamp_eta_step_in_sigmas(
848 const std::vector<std::size_t>& eta_indices,
849 std::vector<double>& delta,
850 double max_step_in_sigma
852 if (!(max_step_in_sigma > 0.0) || !std::isfinite(max_step_in_sigma)) {
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);
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);
874 for (
double& v : delta) {
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"
907 throw std::runtime_error(
"laplace_profile_eta_subset: eta_base has wrong dimension");
915 if (laplace_eta_indices.empty()) {
921 const std::vector<double> r0 = like.
residuals(p, eta_base);
923 const RealMatrix W_obs = force_symmetric_checked(
928 const RealMatrix W_eta_full = force_symmetric_checked(
933 const RealMatrix W_eta = force_symmetric_checked(
934 principal_submatrix_by_indices(W_eta_full, laplace_eta_indices),
948 const RealMatrix H = regularize_spd_local(H_raw, hessian_eig_floor_rel, hessian_label);
952 const std::vector<double> Hinv_g = matvec(H_inv, der.
g_eta);
954 const double correction = 0.5 * dot(der.
g_eta, Hinv_g);
956 out.
nll_hat = nll0 - correction;
958 for (std::size_t col = 0; col < laplace_eta_indices.size(); ++col) {
959 const std::size_t a = laplace_eta_indices[col];
963 clamp_eta_to_limits(like, out.
eta_hat);
985 const std::vector<double>& p,
991 laplace_profile_eta_subset(
995 all_eta_indices(eta_dim),
997 "Laplace initial H_eta"
1007 const std::vector<std::size_t> bad =
1008 select_nonstationary_eta_indices(like, p, best.
eta_hat, options);
1010 debug_print_stationarity_summary(
1024 const std::vector<std::size_t> laplace_indices =
1025 eta_index_complement(eta_dim, bad);
1027 const std::vector<double> g_bad =
1028 eta_gradient_nll_subset(like, p, best.
eta_hat, bad);
1031 numerical_eta_hessian_subset(like, p, best.
eta_hat, bad);
1034 H_bad = regularize_spd_local(
1037 "Newton refine H_bad"
1039 }
catch (
const std::exception& e) {
1043 std::cout <<
"[LAPLACE WARMUP] " << options.
debug_label
1045 <<
" newton_hessian=rejected"
1046 <<
" reason=" << e.what()
1047 <<
" refined_eta=" << eta_index_list_string(bad)
1053 std::vector<double> delta = matvec(H_bad.
inv(), g_bad);
1054 for (
double& v : delta) {
1058 clamp_eta_step_in_sigmas(
1065 bool accepted =
false;
1067 double accepted_direct = best_direct;
1070 const double alpha = std::ldexp(1.0, -
static_cast<int>(ls));
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];
1077 clamp_eta_to_limits(like, eta_trial);
1080 laplace_profile_eta_subset(
1086 "Laplace refined subset H_eta"
1095 if (std::isfinite(direct_trial) &&
1096 (!std::isfinite(best_direct) || direct_trial <= best_direct + 1e-10)) {
1098 accepted_comp = std::move(trial);
1099 accepted_direct = direct_trial;
1106 std::cout <<
"[LAPLACE WARMUP] " << options.
debug_label
1108 <<
" newton_step=rejected"
1109 <<
" refined_eta=" << eta_index_list_string(bad)
1116 std::cout <<
"[LAPLACE WARMUP] " << options.
debug_label
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)
1125 best = std::move(accepted_comp);
1126 best_direct = accepted_direct;
1147 const std::vector<double>& p
1149 return laplace_profile_eta_refined(like, p);
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.
EigenSystem eig() const
Computes the eigensystem of a symmetric matrix.
double & at(size_t i, size_t j)
Returns a mutable reference to element (i,j) with bounds checking.
std::size_t cols() const
Returns the number of columns.
RealMatrix transpose() const
Returns the transpose of the matrix.
RealMatrix inv() const
Computes the inverse of the matrix via LU decomposition.
complex_t h(double s, double m_q, double mu_b)
complex_t H(double z, double r_P)
Container for an eigendecomposition.
RealMatrix P
Diagonal matrix of eigenvalues.
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