10 std::ostringstream oss;
15static void dump_matrix_sanity(
const RealMatrix& M,
16 const std::vector<ParamId>& ids,
17 const std::string& name)
19 std::cout <<
"[FIT] Matrix " <<
name
20 <<
" shape=" << M.
rows() <<
"x" << M.
cols()
23 double diag_min = std::numeric_limits<double>::infinity();
24 double diag_max = -std::numeric_limits<double>::infinity();
25 std::size_t nonpos_diag = 0;
27 double max_abs_offdiag = 0.0;
28 std::size_t n_abs_gt_1 = 0;
29 std::pair<std::size_t,std::size_t> argmax_offdiag{0,0};
31 for (std::size_t i = 0; i < M.
rows(); ++i) {
32 const double d = M.
at(i,i);
33 diag_min = std::min(diag_min, d);
34 diag_max = std::max(diag_max, d);
35 if (!(d > 0.0)) ++nonpos_diag;
37 for (std::size_t j = i + 1; j < M.
cols(); ++j) {
38 const double a = std::abs(M.
at(i,j));
39 if (a > max_abs_offdiag) {
41 argmax_offdiag = {i,j};
43 if (a > 1.0 + 1e-10) ++n_abs_gt_1;
47 std::cout <<
"[FIT] " <<
name
48 <<
" diag_min=" << diag_min
49 <<
" diag_max=" << diag_max
50 <<
" nonpos_diag=" << nonpos_diag
51 <<
" max_abs_offdiag=" << max_abs_offdiag
52 <<
" n_abs_offdiag_gt_1=" << n_abs_gt_1
59 double evmin = e.
D.
at(0,0);
60 for (std::size_t i = 1; i < M.
rows(); ++i) {
61 if (e.
D.
at(i,i) < evmin) {
67 std::vector<std::pair<double,std::size_t>> comps;
68 for (std::size_t i = 0; i < M.
rows(); ++i) {
69 comps.push_back({std::abs(e.
P.
at(i, imin)), i});
71 std::sort(comps.begin(), comps.end(),
72 [](
const auto& a,
const auto& b) { return a.first > b.first; });
74 std::cout <<
"[FIT] " <<
name <<
" smallest_eigenvalue=" << evmin <<
"\n";
75 std::cout <<
"[FIT] " <<
name <<
" dominant entries of smallest mode:\n";
76 for (std::size_t k = 0; k < std::min<std::size_t>(10, comps.size()); ++k) {
77 std::size_t i = comps[k].second;
78 std::cout <<
"[FIT] " << ids[i]
79 <<
" coeff=" << e.
P.
at(i, imin) <<
"\n";
112std::string compact_double(
double x) {
113 std::ostringstream oss;
114 oss << std::setprecision(8) << x;
119 return std::visit([](
const auto& c) -> std::string {
120 using T = std::decay_t<
decltype(c)>;
121 std::ostringstream oss;
122 oss << std::setprecision(8);
124 if constexpr (std::is_same_v<T, FlatMarginalCfg>) {
125 oss <<
"a=" << c.a <<
", b=" << c.b;
126 }
else if constexpr (std::is_same_v<T, GaussianMarginalCfg>) {
127 oss <<
"mu=" << c.mu <<
", sigma=" << c.sigma;
128 }
else if constexpr (std::is_same_v<T, SplitGaussianMarginalCfg>) {
129 oss <<
"mu=" << c.mu <<
", sigma_p=" << c.sigma_p
130 <<
", sigma_m=" << c.sigma_m;
131 }
else if constexpr (std::is_same_v<T, LikelihoodMarginalCfg>) {
132 oss <<
"values=" << c.values.size()
133 <<
", weights=" << c.weights.size();
135 oss <<
"custom config";
141double safe_param_value(
const std::shared_ptr<IStatParameterProxy>& pspp,
const ParamId& pid) {
143 return pspp->get_param(pid)->get_val().real();
145 return std::numeric_limits<double>::quiet_NaN();
149double safe_param_sigma(
const std::shared_ptr<IStatParameterProxy>& pspp,
const ParamId& pid) {
151 return std::abs(pspp->get_param(pid)->get_combined_std().real());
153 return std::numeric_limits<double>::quiet_NaN();
157void print_nuisance_candidate_table(
const std::string& title,
158 const std::vector<ParamId>& ids,
159 const std::shared_ptr<IStatParameterProxy>& pspp)
161 std::cout <<
"[MC CONFIG] " << title <<
": " << ids.size() <<
"\n";
162 for (
const auto& pid : ids) {
163 std::cout <<
"[MC CONFIG] candidate " << pid
164 <<
" | value=" << compact_double(safe_param_value(pspp, pid))
165 <<
" | sigma=" << compact_double(safe_param_sigma(pspp, pid))
170void print_nuisance_value_table(
const std::string& title,
171 const std::map<ParamId, double>& values,
172 const std::shared_ptr<IStatParameterProxy>& pspp)
174 std::cout <<
"[MC CONFIG] " << title <<
": " << values.size() <<
"\n";
175 for (
const auto& [pid, value] : values) {
176 std::cout <<
"[MC CONFIG] retained " << pid
177 <<
" | value=" << compact_double(value)
178 <<
" | sigma=" << compact_double(safe_param_sigma(pspp, pid))
183void print_nuisance_marginal_line(
const ParamId& pid,
189 std::cout <<
"[MC CONFIG] retained " << pid
190 <<
" | value=" << compact_double(value)
191 <<
" | sigma=" << compact_double(sigma)
192 <<
" | marginal=" << marginal_type_name(mt)
193 <<
" (" << marginal_config_summary(cfg) <<
")\n";
196static std::vector<std::map<ParamId, double>> make_sensitivity_contexts(
197 const std::map<ParamId, double>& eta0,
198 const std::shared_ptr<IStatParameterProxy>& pspp,
199 std::size_t n_contexts,
200 double context_sigma,
203 std::vector<std::map<ParamId, double>> contexts;
204 contexts.reserve(std::max<std::size_t>(1, n_contexts));
206 contexts.push_back(eta0);
208 std::mt19937
rng(seed);
209 std::normal_distribution<double> normal(0.0, context_sigma);
211 for (std::size_t c = 1; c < n_contexts; ++c) {
214 for (
auto& [pid, val] : ctx) {
216 std::abs(pspp->get_param(pid)->get_combined_std().real());
218 if (!std::isfinite(sigma) || sigma <= 0.0) {
222 const double z = normal(rng);
227 contexts.push_back(std::move(ctx));
238std::vector<double> fit_coordinates_from_model_values(
240 const std::vector<ParamId>& p_ids,
241 std::vector<double> model_values)
243 for (std::size_t i = 0; i < p_ids.size() && i < model_values.size(); ++i) {
244 model_values[i] += fit_parameter_offset(config, p_ids[i]);
249std::vector<double> model_values_from_fit_coordinates(
251 const std::vector<ParamId>& p_ids,
252 const std::vector<double>& fit_values)
254 std::vector<double> model_values = fit_values;
255 for (std::size_t i = 0; i < p_ids.size() && i < model_values.size(); ++i) {
256 model_values[i] -= fit_parameter_offset(config, p_ids[i]);
270 out.
step_hint = (std::isfinite(sigma_hint) && sigma_hint > 0.0)
272 :
std::max(1e-3, 0.01 *
std::max(1.0,
std::abs(value)));
276 const auto [low, high] = configured_bounds->second;
277 if (!std::isfinite(low) || !std::isfinite(high) || !(low < high)) {
278 throw std::invalid_argument(
"Invalid explicit fit bounds for " +
param_name(pid));
280 out.
limits = configured_bounds->second;
284 const std::string& nm = out.
name;
285 if (nm.find(
"FCONST") != std::string::npos) {
286 out.
limits = std::make_pair(0.05, 0.35);
292template <
class PredMapT>
293std::vector<double> ordered_prediction_vector(
294 const std::vector<ExperimentObs>& obs_ids,
295 const PredMapT& pred_map)
297 std::vector<double> out;
298 out.reserve(obs_ids.size());
300 for (
const auto& bid : obs_ids) {
301 const auto& vec = pred_map.at(bid.obs.s);
303 auto it = std::find_if(vec.begin(), vec.end(), [&](
const auto& ov) {
304 auto bin = ov.bin.value_or(std::pair<double, double>{0.0, 0.0});
305 return bin == bid.obs.p;
308 if (it == vec.end()) {
309 throw std::runtime_error(
"Missing predicted observable/bin.");
312 out.push_back(it->value);
319std::vector<BinnedObservableId> binned_ids_from_experiment_obs(
320 const std::vector<ExperimentObs>& obs_ids
322 std::vector<BinnedObservableId> out;
323 out.reserve(obs_ids.size());
324 for (
const auto& oid : obs_ids) {
325 out.push_back(oid.obs);
333 throw std::runtime_error(
"symmetrize_covariance_matrix: covariance must be square");
336 for (std::size_t i = 0; i < cov.
rows(); ++i) {
337 for (std::size_t j = i + 1; j < cov.
cols(); ++j) {
338 const double a = cov.
at(i, j);
339 const double b = cov.
at(j, i);
341 if (!std::isfinite(a) || !std::isfinite(b)) {
342 throw std::runtime_error(
"symmetrize_covariance_matrix: non-finite covariance entry");
345 const double v = 0.5 * (a + b);
350 if (!std::isfinite(cov.
at(i, i))) {
351 throw std::runtime_error(
"symmetrize_covariance_matrix: non-finite covariance diagonal");
359 const std::vector<ExperimentObs>& obs_ids,
360 const std::map<
ExperimentObs, std::map<ExperimentObs, double>>& corr_obs,
361 const std::map<ExperimentObs, double>& sigma_obs
363 const std::size_t
n = obs_ids.size();
366 for (std::size_t i = 0; i <
n; ++i) {
367 const auto sig_i_it = sigma_obs.find(obs_ids[i]);
368 if (sig_i_it == sigma_obs.end()) {
369 std::ostringstream oss;
370 oss <<
"experimental_covariance_matrix: missing sigma for observable "
372 throw std::runtime_error(oss.str());
375 const double sigma_i = std::abs(sig_i_it->second);
376 if (!std::isfinite(sigma_i)) {
377 throw std::runtime_error(
"experimental_covariance_matrix: non-finite sigma_i");
380 for (std::size_t j = 0; j <
n; ++j) {
381 const auto sig_j_it = sigma_obs.find(obs_ids[j]);
382 if (sig_j_it == sigma_obs.end()) {
383 std::ostringstream oss;
384 oss <<
"experimental_covariance_matrix: missing sigma for observable "
386 throw std::runtime_error(oss.str());
389 const double sigma_j = std::abs(sig_j_it->second);
390 if (!std::isfinite(sigma_j)) {
391 throw std::runtime_error(
"experimental_covariance_matrix: non-finite sigma_j");
394 double corr = (i == j) ? 1.0 : 0.0;
395 auto row_it = corr_obs.find(obs_ids[i]);
396 if (row_it != corr_obs.end()) {
397 auto col_it = row_it->second.find(obs_ids[j]);
398 if (col_it != row_it->second.end()) {
399 corr = col_it->second;
403 if (!std::isfinite(corr)) {
404 throw std::runtime_error(
"experimental_covariance_matrix: non-finite correlation");
407 cov.
at(i, j) = corr * sigma_i * sigma_j;
411 return symmetrize_covariance_matrix(std::move(cov));
419 cov = symmetrize_covariance_matrix(std::move(cov));
421 const std::size_t
n = cov.
rows();
422 if (n != cov.
cols()) {
423 throw std::runtime_error(
"inverse_covariance_with_ridge: covariance must be square");
426 std::vector<double> sigma(n);
428 for (std::size_t i = 0; i <
n; ++i) {
429 const double vii = cov.
at(i, i);
431 if (!std::isfinite(vii) || vii <= 0.0) {
432 std::ostringstream oss;
433 oss <<
"inverse_covariance_with_ridge: non-positive variance at i="
434 << i <<
", variance=" << vii;
435 throw std::runtime_error(oss.str());
438 sigma[i] = std::sqrt(vii);
442 for (std::size_t i = 0; i <
n; ++i) {
443 for (std::size_t j = 0; j <
n; ++j) {
444 corr.at(i, j) = cov.
at(i, j) / (sigma[i] * sigma[j]);
448 corr = symmetrize_covariance_matrix(std::move(corr));
450 const double ridge = std::max(ridge_rel, ridge_abs);
452 for (std::size_t i = 0; i <
n; ++i) {
453 corr.at(i, i) += ridge;
456 corr = symmetrize_covariance_matrix(std::move(corr));
461 for (std::size_t i = 0; i <
n; ++i) {
462 for (std::size_t j = 0; j <
n; ++j) {
463 cov_inv.at(i, j) = corr_inv.
at(i, j) / (sigma[i] * sigma[j]);
467 return symmetrize_covariance_matrix(std::move(cov_inv));
515 std::shared_ptr<IModel> obs_int,
516 std::shared_ptr<IStatCorrelationProxy> pscp,
517 std::shared_ptr<IStatParameterProxy> pspp,
518 std::shared_ptr<IStatSourcesProxy> sp,
519 std::shared_ptr<IStatDependencyPruner> dp,
520 std::shared_ptr<INuisanceReader> nuisance_reader,
521 std::shared_ptr<IStatParamOptimizerProxy> spop)
522 : obs_int(
std::move(obs_int)),
523 pscp(
std::move(pscp)),
524 pspp(
std::move(pspp)),
525 marginal_config_factory_(this->pspp),
528 nuisance_reader_(
std::move(nuisance_reader)),
529 spop(
std::move(spop)),
530 config(
std::move(config))
532 if (!nuisance_reader_) {
533 throw std::invalid_argument(
"StatisticManager: nuisance_reader is null");
536 this->obs_int->compute_observables();
538 invalidate_fit_state();
542 unsigned int seed = config.
MC_seed;
543 std::vector<std::unique_ptr<IMarginalDistribution>> marginals;
549 std::cout <<
"[MC CONFIG] Retained nuisance marginals used by MC: "
554 const MarginalType mt = resolve_nuisance_marginal_type(pid);
558 print_nuisance_marginal_line(
561 safe_param_sigma(pspp, pid),
574 unsigned int seed = config.
MC_seed;
577 std::cout <<
"[MC CONFIG] Nuisance distribution: "
578 << cache.
eta_specs_real.size() <<
" retained nuisance(s), copula="
583 std::unique_ptr<ICopula> copula;
595 return std::make_unique<JointDistribution>(
602 unsigned int seed = config.
MC_seed ^ 0x9E3779B9u;
603 std::map<ExperimentObs, MarginalType> exp_data_marginals;
605 for (
auto& [oid, v] : cache.
exp_obs) {
610 if (!exp_data_marginals.contains(oid)) {
614 exp_data_marginals.at(oid) = mt;
617 auto unzipped =
unzip(exp_data_marginals);
618 std::vector<ExperimentObs> obs_ids = unzipped.ids;
619 std::vector<std::unique_ptr<IMarginalDistribution>> marginals;
621 for (
auto& [oid, mt] : exp_data_marginals) {
624 marginals.emplace_back(std::move(m_ptr));
627 std::unique_ptr<ICopula> copula;
635 copula_cfg.
nu = obs_int->n_observables() - 1;
639 return std::make_unique<JointDistribution>(std::move(marginals), std::move(copula));
646 std::cout <<
"[DEBUG] Merged nuisance specification registry: "
647 << this->merged_nuisance_specs_.size() <<
" entry/entries\n";
648 for (
const auto& elem : this->merged_nuisance_specs_) {
649 std::cout <<
"[DEBUG] " << elem.first <<
"\n";
650 std::cout << elem.second <<
"\n";
654 std::map<BinnedObservableId, GaussianSummary> out;
656 std::cout <<
"[MC CONFIG] Observable MC summaries: "
657 << sums.summary.size() <<
"\n";
659 for (
const auto& gs : sums.summary) {
662 std::cout <<
"[MC CONFIG] " << gs <<
"\n";
670 this->config.
progress_monitor->reset(
"preparing",
"Preparing Monte-Carlo uncertainty propagation");
676 MonteCarloEngine mc(this->obs_int, sampler, make_mc_config_from_config(this->config));
685 this->config.
progress_monitor->reset(
"preparing",
"Preparing chi-square fit");
690 throw std::invalid_argument(
"compute_MLE called with an empty fit parameter list.");
700 dump_matrix_sanity(Reta, eta_ids_dbg,
"SigmaEta");
703 const std::vector<ParamId> p_ids = unzipped_fit_params.ids;
704 const std::vector<double> p0 = fit_coordinates_from_model_values(
705 config, p_ids, unzipped_fit_params.vals
708 const std::vector<ParamId> eta_ids = unzipped_nuisances.ids;
709 const std::vector<double> eta0 = unzipped_nuisances.vals;
713 const std::vector<ExperimentObs> obs_ids = unzipped_exp_obs.ids;
714 const std::vector<double> exp_obs_vals = unzipped_exp_obs.vals;
718 std::cout <<
"[FIT] Likelihood backend: "
722 const bool show_chi2_progress =
735 "Starting chi-square workflow: MC covariance, experimental covariance, likelihood construction, then maximum-likelihood fit. The MC bar estimates only the MC part; later steps are reported separately."
745 make_mc_config_from_config(this->config,
true)
751 "Monte-Carlo sampling completed; building the MC covariance matrix."
754 const std::vector<BinnedObservableId> cov_ids =
755 binned_ids_from_experiment_obs(obs_ids);
760 this->config.advanced.chi2_covariance_ridge_rel,
761 this->config.advanced.chi2_covariance_ridge_abs
765 "MC covariance ready; collecting experimental uncertainties."
768 std::map<ExperimentObs, double> exp_obs_sigmas;
769 for (
const auto& model_obs_id : this->obs_int->get_obs_ids()) {
770 auto exp_params = pspp->get_obs_param(model_obs_id);
771 for (
const auto& [exp_obs, param] : exp_params) {
772 if (!accepts_experiment_observable(exp_obs)) {
775 exp_obs_sigmas[exp_obs] =
776 std::abs(param->get_combined_std().real());
782 "Experimental uncertainties ready; assembling the total covariance matrix."
784 const RealMatrix covariance_exp = experimental_covariance_matrix(
791 symmetrize_covariance_matrix(cov.
covariance + covariance_exp);
795 "Total covariance assembled; inverting the regularized covariance matrix."
797 RealMatrix covariance_total_inv = inverse_covariance_with_ridge(
800 this->config.advanced.chi2_covariance_ridge_abs
804 "Covariance inverse ready; constructing the chi-square likelihood."
808 std::cout <<
"[FIT] Covariance model: total = MC theory covariance + experimental covariance.\n";
811 auto ctx = std::make_shared<LikelihoodContext>();
812 ctx->exp_obs_values = exp_obs_vals;
813 ctx->nuis_defs.clear();
814 ctx->fp_defs.reserve(p_ids.size());
816 for (std::size_t i = 0; i < p_ids.size(); ++i) {
817 double sigma = std::abs(pspp->get_param(p_ids[i])->get_combined_std().real());
818 ctx->fp_defs.emplace_back(make_fit_param_def(p_ids[i], p0[i], sigma, config));
822 const std::map<ParamId, double> eta0_map =
zip(eta_ids, eta0);
824 auto model_fn = [
this, obs_ids, p_ids, eta0_map](
825 const std::vector<double>& p_vec,
826 const std::vector<double>& eta_vec) -> std::vector<double>
828 if (!eta_vec.empty()) {
829 throw std::runtime_error(
"CHI2_MC_COVARIANCE model_fn expects empty eta vector");
832 const auto model_p = model_values_from_fit_coordinates(this->config, p_ids, p_vec);
833 auto pred_map = this->obs_int->predict_optimized(
838 return ordered_prediction_vector(obs_ids, pred_map);
841 last_like_ = std::make_shared<ChiSquaredLikelihood>(
850 "Likelihood ready; running the maximum-likelihood fit. This backend-dependent step has no reliable ETA."
853 MLFitOptions fitopt = make_mlfit_options_from_config(config);
854 last_fitter_ = std::make_shared<MLFitter>(last_like_, fitopt);
855 last_fit_raw_ = last_fitter_->maximum_likelihood_fit(p0);
856 chi2_progress.
finish(
"Chi-square workflow completed.");
858 last_fit_param_ids_ = p_ids;
859 last_nuisance_ids_.clear();
860 last_fit_param_index_.clear();
861 for (std::size_t i = 0; i < p_ids.size(); ++i) {
862 last_fit_param_index_[p_ids[i]] = i;
877 auto ctx = std::make_shared<LikelihoodContext>();
880 ctx->exp_obs_values = exp_obs_vals;
882 ctx->fp_defs.reserve(p_ids.size());
883 for (std::size_t i = 0; i < p_ids.size(); ++i) {
884 double sigma = std::abs(pspp->get_param(p_ids[i])->get_combined_std().real());
885 ctx->fp_defs.emplace_back(make_fit_param_def(p_ids[i], p0[i], sigma, config));
888 ctx->nuis_defs.reserve(eta_ids.size());
889 for (std::size_t i = 0; i < eta_ids.size(); ++i) {
890 double sigma = std::abs(pspp->get_param(eta_ids[i])->get_combined_std().real());
891 ctx->nuis_defs.emplace_back(
892 make_nuisance_parameter_definition(eta_ids[i], eta0[i], sigma)
896 auto model_fn = [
this, obs_ids, p_ids, eta_ids](
897 const std::vector<double>& p_vec,
898 const std::vector<double>& eta_vec) -> std::vector<double>
900 const auto model_p = model_values_from_fit_coordinates(this->config, p_ids, p_vec);
901 auto pred_map = this->obs_int->predict_optimized(
903 zip(eta_ids, eta_vec)
906 return ordered_prediction_vector(obs_ids, pred_map);
910 last_like_ = std::make_shared<BaseLikelihood>(model_fn, ctx, p_ids.size());
927 last_fitter_ = std::make_shared<MLFitter>(ctx, model_fn, fitopt);
928 last_fit_raw_ = last_fitter_->maximum_likelihood_fit(p0);
930 last_fit_param_ids_ = p_ids;
931 last_nuisance_ids_ = eta_ids;
932 last_fit_param_index_.clear();
933 for (std::size_t i = 0; i < p_ids.size(); ++i) {
934 last_fit_param_index_[p_ids[i]] = i;
951 throw std::runtime_error(
"Please run compute_MLE before requesting a confidence contour.");
954 if (!last_fit_param_index_.contains(p1) || !last_fit_param_index_.contains(p2)) {
955 throw std::invalid_argument(
"Contour requested for parameters that are not in the last fitted parameter set.");
959 throw std::invalid_argument(
"Contour requires two distinct parameters.");
962 const std::size_t x_id = last_fit_param_index_.at(p1);
963 const std::size_t y_id = last_fit_param_index_.at(p2);
965 Contour cl = last_fitter_->contour(
976void StatisticManager::validate_fit_parameter_sensitivity() {
982 throw std::invalid_argument(
983 "Fit parameter sensitivity check failed: no experimental observable is "
984 "available for the current observable/experiment selection."
989 const std::vector<ExperimentObs> obs_ids = unzipped_exp_obs.ids;
990 const std::vector<double> exp_obs_vals = unzipped_exp_obs.vals;
991 const std::map<ParamId, double> p_central = cache.
p_specs;
992 const std::map<ParamId, double> eta_central = cache.
eta_specs_real;
994 struct RestoreModelStateGuard {
996 std::map<ParamId, double> p;
997 std::map<ParamId, double> eta;
999 ~RestoreModelStateGuard() {
1000 if (self ==
nullptr) {
1004 self->obs_int->predict_optimized(p, eta);
1005 }
catch (
const std::exception& e) {
1006 std::cout <<
"[FIT] WARNING: failed to restore central model state after "
1007 "fit-parameter sensitivity checks: "
1008 << e.what() << std::endl;
1010 std::cout <<
"[FIT] WARNING: failed to restore central model state after "
1011 "fit-parameter sensitivity checks: unknown exception"
1015 } restore_guard{
this, p_central, eta_central};
1017 std::vector<double> baseline_pred;
1019 const auto baseline_pred_map =
1020 obs_int->predict_optimized(p_central, eta_central);
1021 baseline_pred = ordered_prediction_vector(obs_ids, baseline_pred_map);
1022 if (std::any_of(baseline_pred.begin(), baseline_pred.end(),
1023 [](
double value) { return !std::isfinite(value); })) {
1024 throw std::runtime_error(
1025 "non-finite observable prediction at the central fit point"
1028 }
catch (
const std::exception& e) {
1031 "Fit-parameter sensitivity baseline could not be evaluated; "
1032 "continuing conservatively. Reason:", e.what()
1036 throw std::runtime_error(
1037 std::string(
"Fit-parameter sensitivity baseline failed: ") + e.what()
1041 const double probe_fraction = std::clamp(
1047 std::vector<ParamId> inactive;
1049 for (
const auto& [pid, nominal_model_value] : p_central) {
1050 const double fit_center =
1051 nominal_model_value + fit_parameter_offset(config, pid);
1052 const double sigma =
1053 std::abs(pspp->get_param(pid)->get_combined_std().real());
1055 make_fit_param_def(pid, fit_center, sigma, config);
1058 if (def.
limits.has_value()) {
1059 const auto [low, high] = def.
limits.value();
1060 step = std::max(step, probe_fraction * (high - low));
1062 if (!std::isfinite(step) || !(step > 0.0)) {
1063 step = std::max(1e-3, 0.01 * std::max(1.0, std::abs(fit_center)));
1066 double fit_minus = fit_center - step;
1067 double fit_plus = fit_center + step;
1068 if (def.
limits.has_value()) {
1069 const auto [low, high] = def.
limits.value();
1070 fit_minus = std::clamp(fit_minus, low, high);
1071 fit_plus = std::clamp(fit_plus, low, high);
1074 double best_abs_shift = 0.0;
1075 double best_rel_shift = 0.0;
1076 bool evaluated =
false;
1077 bool evaluation_failed =
false;
1078 std::string failure_reason;
1080 const auto probe = [&](
double fit_value) {
1081 if (!std::isfinite(fit_value) ||
1082 std::abs(fit_value - fit_center) < 1e-14) {
1086 auto p_probe = p_central;
1087 p_probe[pid] = fit_value - fit_parameter_offset(config, pid);
1089 const auto pred_map = obs_int->predict_optimized(p_probe, eta_central);
1090 const std::vector<double> pred =
1091 ordered_prediction_vector(obs_ids, pred_map);
1094 for (std::size_t i = 0; i < baseline_pred.size(); ++i) {
1095 if (!std::isfinite(pred[i]) || !std::isfinite(exp_obs_vals[i])) {
1096 throw std::runtime_error(
1097 "non-finite observable value during fit-parameter probe"
1100 const double abs_shift = std::abs(pred[i] - baseline_pred[i]);
1101 const double scale = std::max({
1102 std::abs(baseline_pred[i]),
1103 std::abs(exp_obs_vals[i]),
1106 best_abs_shift = std::max(best_abs_shift, abs_shift);
1107 best_rel_shift = std::max(best_rel_shift, abs_shift / scale);
1114 }
catch (
const std::exception& e) {
1115 evaluation_failed =
true;
1116 failure_reason = e.what();
1118 evaluation_failed =
true;
1119 failure_reason =
"unknown exception";
1122 if (evaluation_failed || !evaluated) {
1125 "Fit-parameter sensitivity probe failed for",
param_name(pid),
1126 "; keeping the parameter conservatively. Reason:",
1127 evaluation_failed ? failure_reason :
"no valid probe point"
1131 inactive.push_back(pid);
1135 const bool sensitive =
1140 std::cout <<
"[FIT] fit-parameter sensitivity " << pid
1141 <<
" : max_abs_shift=" << best_abs_shift
1142 <<
", max_rel_shift=" << best_rel_shift
1143 <<
" -> " << (sensitive ?
"active" :
"inactive")
1148 inactive.push_back(pid);
1152 if (inactive.empty()) {
1156 std::ostringstream oss;
1157 oss <<
"Fit parameter sensitivity check failed: the selected observables are "
1158 "numerically insensitive to ";
1159 for (std::size_t i = 0; i < inactive.size(); ++i) {
1165 oss <<
". Add observables that depend on these parameters, declare the missing "
1166 "observable dependencies, or choose different fit parameters.";
1169 throw std::invalid_argument(oss.str());
1182 if (selected_experiments_.has_value()) {
1183 for (
const auto& elem : *selected_experiments_) {
1190 if (selected_experiment_observables_.has_value()) {
1191 LOG_VERBOSE(
"USING EXPLICIT EXPERIMENT-OBSERVABLE SELECTION: ",
1192 selected_experiment_observables_->size(),
" entries");
1195 for (
const auto& [tp, block] : last_detached_fit_blocks_) {
1196 dp->reattach_block(tp, block);
1198 for (
const auto& pid : last_detached_fit_params_) {
1199 if (pid.
type.has_value()) {
1200 dp->reattach_parameter(pid.
type.value(), pid.
block, pid.
code);
1203 last_detached_fit_blocks_.clear();
1204 last_detached_fit_params_.clear();
1208 std::unordered_set<std::string> seen_blocks;
1210 for (
const auto& [pid, _] : cache.
p_specs) {
1211 if (!pid.
type.has_value()) {
1215 const auto tp = pid.
type.value();
1216 const std::string block_key =
1217 std::to_string(
static_cast<int>(tp)) +
"::" + pid.
block.
to_string();
1219 if (!seen_blocks.contains(block_key)) {
1220 dp->detach_block(tp, pid.
block);
1221 last_detached_fit_blocks_.push_back({tp, pid.
block});
1222 seen_blocks.insert(block_key);
1225 dp->detach_parameter(tp, pid.
block, pid.
code);
1226 last_detached_fit_params_.push_back(pid);
1230 for (
const auto& [pid, _] : cache.
p_specs)
1234 const ParamId& pid = it->first;
1237 LOG_INFO(
"Dropping BSM nuisance from cache", pid);
1246 validate_fit_parameter_sensitivity();
1250 fs.open(
"covariance.csv");
1251 for (
auto& [pid1, row] : cache.
SigmaEta) {
1252 double sigma_1 = std::abs(pspp->get_param(pid1)->get_combined_std().real());
1253 for (
auto& [pid2, corr] : row) {
1254 double sigma_2 = std::abs(pspp->get_param(pid2)->get_combined_std().real());
1255 if (pid2 == (*(--row.end())).first)
1256 fs << corr * sigma_1 * sigma_2;
1258 fs << corr * sigma_1 * sigma_2 <<
',';
1267 default_nuisance_specs_ = nuisance_reader_->load_default();
1269 if (current_user_nuisance_file_.has_value()) {
1270 user_nuisance_specs_ = nuisance_reader_->load_user(*current_user_nuisance_file_);
1272 user_nuisance_specs_ = nuisance_reader_->load_user();
1275 rebuild_merged_nuisance_specs();
1276 invalidate_fit_state();
1280 current_user_nuisance_file_ = user_yaml_path;
1285 current_user_nuisance_file_.reset();
1289void StatisticManager::rebuild_merged_nuisance_specs() {
1290 merged_nuisance_specs_ = default_nuisance_specs_;
1291 for (
const auto& [pid, spec] : user_nuisance_specs_) {
1292 merged_nuisance_specs_[pid] = spec;
1297void StatisticManager::invalidate_fit_state() {
1302 last_fitter_.reset();
1304 last_fit_param_ids_.clear();
1305 last_nuisance_ids_.clear();
1306 last_fit_param_index_.clear();
1310 if (
const auto it = merged_nuisance_specs_.find(pid);
1311 it != merged_nuisance_specs_.end()) {
1316 if (
const auto it = merged_nuisance_specs_.find(untyped_pid);
1317 it != merged_nuisance_specs_.end()) {
1327 if (
const auto* spec = find_nuisance_spec(pid)) {
1328 mt = spec->marginal;
1340 double sigma_hint)
const
1346 const double s = (std::isfinite(sigma_hint) && sigma_hint > 0.0)
1347 ? std::abs(sigma_hint)
1348 : std::max(1e-3, 0.01 * std::abs(value));
1352 if (
const auto* spec = find_nuisance_spec(pid)) {
1353 out.
limits = spec->bounds;
1357 const std::string& nm = out.
name;
1358 if (nm.find(
"SMINPUTS:3") != std::string::npos) {
1359 out.
limits = std::make_pair(0.05, 0.30);
1360 }
else if (nm.find(
"MASS:") != std::string::npos ||
1361 nm.find(
"FLIFE:") != std::string::npos ||
1362 nm.find(
"FCONST:") != std::string::npos ||
1363 nm.find(
"FMASS:") != std::string::npos ||
1364 nm.find(
"SMINPUTS:5") != std::string::npos ||
1365 nm.find(
"SMINPUTS:6") != std::string::npos) {
1366 out.
limits = std::make_pair(std::max(1e-12, value - 5.0 * s), value + 5.0 * s);
1375 if (
const auto* spec = find_nuisance_spec(pid)) {
1376 return marginal_config_factory_.
create(pid, mt, *spec);
1379 return marginal_config_factory_.
create(pid, mt);
1385 std::unordered_set<ParamId> eta_infos;
1387 for (
const auto& obsId : obs_int->get_obs_ids()) {
1388 for (
auto paramId : obs_int->get_obs_deps(obsId.s)) {
1389 eta_infos.insert(paramId);
1393 std::unordered_set<ParamId> eta_infos_leaf = this->sp->get_all_leaf_sources(eta_infos);
1396 std::vector<ParamId> candidate_ids(eta_infos_leaf.begin(), eta_infos_leaf.end());
1397 std::sort(candidate_ids.begin(), candidate_ids.end(), [](
const ParamId& a,
const ParamId& b) {
1398 return param_name(a) < param_name(b);
1400 print_nuisance_candidate_table(
1401 "Potential nuisance candidates before pruning",
1407 std::map<ParamId, double> eta_specs_real_leaf;
1408 std::map<ParamId, double> delta_rel;
1410 for (
const auto& paramId : eta_infos_leaf) {
1411 if (cache.
p_specs.contains(paramId)) {
1415 const double u = std::abs(pspp->get_param(paramId)->get_combined_std().real());
1416 const double val = pspp->get_param(paramId)->get_val().real();
1418 if (!std::isfinite(u) ||
fpeq(u, 0.0)) {
1419 delta_rel[paramId] = 0.0;
1423 if (
fpeq(val, 0.0)) {
1424 eta_specs_real_leaf[paramId] = val;
1425 delta_rel[paramId] = std::numeric_limits<double>::infinity();
1429 delta_rel[paramId] = std::abs(u / val);
1432 double delta_rel_max = 0.0;
1433 for (
const auto& [pid, d] : delta_rel) {
1434 if (std::isfinite(d)) {
1435 delta_rel_max = std::max(delta_rel_max, d);
1438 if (!(delta_rel_max > 0.0)) {
1439 delta_rel_max = 1.0;
1442 for (
const auto& [pid, d] : delta_rel) {
1443 const double rel_to_max = std::isfinite(d) ? (d / delta_rel_max) : 1.0;
1445 eta_specs_real_leaf[pid] = pspp->get_param(pid)->get_val();
1450 print_nuisance_value_table(
1451 "Nuisances after relative-uncertainty preselection",
1452 eta_specs_real_leaf,
1459 !eta_specs_real_leaf.empty() &&
1463 const auto unzipped_exp_obs =
unzip(exp_obs_map);
1464 const std::vector<ExperimentObs> obs_ids = unzipped_exp_obs.ids;
1465 const std::vector<double> exp_obs_vals = unzipped_exp_obs.vals;
1467 const std::map<ParamId, double> eta_central_for_restore = eta_specs_real_leaf;
1468 const std::map<ParamId, double> p_central_for_restore = cache.
p_specs;
1470 struct RestoreModelStateGuard {
1472 std::map<ParamId, double> p;
1473 std::map<ParamId, double> eta;
1476 ~RestoreModelStateGuard() {
1477 if (!active || self ==
nullptr)
return;
1480 self->obs_int->predict_optimized(p, eta);
1481 }
catch (
const std::exception& e) {
1482 std::cout <<
"[FIT] WARNING: failed to restore central model state: "
1483 << e.what() << std::endl;
1485 std::cout <<
"[FIT] WARNING: failed to restore central model state: unknown exception"
1491 RestoreModelStateGuard restore_guard{
1493 p_central_for_restore,
1494 eta_central_for_restore,
1498 const auto contexts = make_sensitivity_contexts(
1499 eta_specs_real_leaf,
1506 std::map<ParamId, double> screened_eta_specs;
1509 if (sensitivity_verbose) {
1510 std::cout <<
"[FIT] Model-sensitivity pruning on "
1511 << eta_specs_real_leaf.size()
1512 <<
" nuisance candidates using "
1514 <<
" contexts" << std::endl;
1517 for (
const auto& [pid, nominal] : eta_specs_real_leaf) {
1518 const double sigma =
1519 std::abs(pspp->get_param(pid)->get_combined_std().real());
1521 if (!std::isfinite(sigma) ||
fpeq(sigma, 0.0)) {
1522 screened_eta_specs[pid] = nominal;
1523 if (sensitivity_verbose) {
1524 std::cout <<
"[FIT] sensitivity " << pid
1525 <<
" : sigma not finite or zero -> keep" << std::endl;
1531 make_nuisance_parameter_definition(pid, nominal, sigma);
1535 if (!std::isfinite(step) ||
fpeq(step, 0.0)) {
1536 screened_eta_specs[pid] = nominal;
1537 if (sensitivity_verbose) {
1538 std::cout <<
"[FIT] sensitivity " << pid
1539 <<
" : probe step not finite or zero -> keep" << std::endl;
1544 double best_abs_shift = 0.0;
1545 double best_rel_shift = 0.0;
1546 std::size_t best_context = 0;
1547 bool evaluation_failed =
false;
1549 for (std::size_t c = 0; c < contexts.size(); ++c) {
1550 auto base_map = contexts[c];
1552 double center = base_map.at(pid);
1553 double eta_plus = center + step;
1554 double eta_minus = center - step;
1556 if (def.
limits.has_value()) {
1557 const auto [lo, hi] = def.
limits.value();
1558 eta_plus = std::clamp(eta_plus, lo, hi);
1559 eta_minus = std::clamp(eta_minus, lo, hi);
1562 if (!std::isfinite(eta_plus) ||
1563 !std::isfinite(eta_minus) ||
1564 std::abs(eta_plus - eta_minus) < 1e-14) {
1569 const auto baseline_pred_map =
1570 this->obs_int->predict_optimized(cache.
p_specs, base_map);
1572 const std::vector<double> baseline_pred =
1573 ordered_prediction_vector(obs_ids, baseline_pred_map);
1575 auto eta_plus_map = base_map;
1576 auto eta_minus_map = base_map;
1578 eta_plus_map[pid] = eta_plus;
1579 eta_minus_map[pid] = eta_minus;
1581 const auto pred_plus_map =
1582 this->obs_int->predict_optimized(cache.
p_specs, eta_plus_map);
1584 const auto pred_minus_map =
1585 this->obs_int->predict_optimized(cache.
p_specs, eta_minus_map);
1587 const std::vector<double> pred_plus =
1588 ordered_prediction_vector(obs_ids, pred_plus_map);
1590 const std::vector<double> pred_minus =
1591 ordered_prediction_vector(obs_ids, pred_minus_map);
1593 double max_abs_shift = 0.0;
1594 double max_rel_shift = 0.0;
1596 for (std::size_t i = 0; i < baseline_pred.size(); ++i) {
1597 const double one_sigma_shift =
1598 0.5 * std::abs(pred_plus[i] - pred_minus[i]);
1600 const double scale = std::max({
1601 std::abs(baseline_pred[i]),
1602 std::abs(exp_obs_vals[i]),
1606 max_abs_shift = std::max(max_abs_shift, one_sigma_shift);
1607 max_rel_shift = std::max(max_rel_shift, one_sigma_shift / scale);
1610 if (max_rel_shift > best_rel_shift ||
1611 max_abs_shift > best_abs_shift) {
1612 best_abs_shift = std::max(best_abs_shift, max_abs_shift);
1613 best_rel_shift = std::max(best_rel_shift, max_rel_shift);
1617 }
catch (
const std::exception& e) {
1618 evaluation_failed =
true;
1620 if (sensitivity_verbose) {
1621 std::cout <<
"[FIT] sensitivity " << pid
1622 <<
" : context " << c
1623 <<
" failed with exception: "
1624 << e.what() << std::endl;
1629 evaluation_failed =
true;
1631 if (sensitivity_verbose) {
1632 std::cout <<
"[FIT] sensitivity " << pid
1633 <<
" : context " << c
1634 <<
" failed with unknown exception"
1643 screened_eta_specs[pid] = nominal;
1645 if (sensitivity_verbose) {
1646 std::cout <<
"[FIT] sensitivity " << pid
1647 <<
" : evaluation failed -> keep" << std::endl;
1657 if (sensitivity_verbose) {
1658 std::cout <<
"[FIT] sensitivity " << pid
1659 <<
" : max_abs_shift=" << best_abs_shift
1660 <<
", max_rel_shift=" << best_rel_shift
1661 <<
", best_context=" << best_context
1662 <<
" -> " << (keep ?
"keep" :
"drop")
1667 screened_eta_specs[pid] = nominal;
1671 eta_specs_real_leaf = std::move(screened_eta_specs);
1675 print_nuisance_value_table(
1676 "Final retained nuisances passed to MC/fit",
1677 eta_specs_real_leaf,
1683 std::cout <<
"[FIT] Significant nuisances retained: "
1684 << eta_specs_real_leaf.size() <<
"\n";
1685 for (
const auto& [pid, val] : eta_specs_real_leaf) {
1686 std::cout <<
"[FIT] " << pid <<
" = " << compact_double(val) <<
"\n";
1690 return eta_specs_real_leaf;
1694 std::map<ParamId, double> out;
1695 for (
auto elem : p_specs) {
1696 out[elem] = pspp->get_param(elem)->get_val();
1701 std::map<ParamId, std::map<ParamId, double>> res;
1708 std::map<ExperimentObs, std::map<ExperimentObs, double>> res;
1716 std::map<ExperimentObs, double> out;
1718 for (
const auto& obsId : obs_int->get_obs_ids()) {
1719 auto exp_params = pspp->get_obs_param(obsId);
1721 for (
const auto& [exp_obs, param] : exp_params) {
1722 if (!accepts_experiment_observable(exp_obs)) {
1726 out[exp_obs] = param->get_val();
1737 throw std::invalid_argument(
"prepare_likelihood_for_scan called with an empty fit parameter list.");
1744 const std::vector<ParamId> p_ids = unzipped_fit_params.ids;
1745 const std::vector<double> p0 = fit_coordinates_from_model_values(
1746 config, p_ids, unzipped_fit_params.vals
1749 const std::vector<ParamId> eta_ids = unzipped_nuisances.ids;
1750 const std::vector<double> eta0 = unzipped_nuisances.vals;
1752 const std::vector<ExperimentObs> obs_ids = unzipped_exp_obs.ids;
1753 const std::vector<double> exp_obs_vals = unzipped_exp_obs.vals;
1755 auto ctx = std::make_shared<LikelihoodContext>();
1758 ctx->exp_obs_values = exp_obs_vals;
1760 ctx->fp_defs.reserve(p_ids.size());
1761 for (std::size_t i = 0; i < p_ids.size(); ++i) {
1762 double sigma = std::abs(pspp->get_param(p_ids[i])->get_combined_std().real());
1763 ctx->fp_defs.emplace_back(make_fit_param_def(p_ids[i], p0[i], sigma, config));
1766 ctx->nuis_defs.reserve(eta_ids.size());
1767 for (std::size_t i = 0; i < eta_ids.size(); ++i) {
1768 double sigma = std::abs(pspp->get_param(eta_ids[i])->get_combined_std().real());
1769 ctx->nuis_defs.emplace_back(
1770 make_nuisance_parameter_definition(eta_ids[i], eta0[i], sigma)
1774 auto model_fn = [
this, obs_ids, p_ids, eta_ids](
1775 const std::vector<double>& p_vec,
1776 const std::vector<double>& eta_vec) -> std::vector<double>
1778 const auto model_p = model_values_from_fit_coordinates(this->config, p_ids, p_vec);
1779 auto pred_map = this->obs_int->predict_optimized(
1780 zip(p_ids, model_p),
1781 zip(eta_ids, eta_vec)
1784 return ordered_prediction_vector(obs_ids, pred_map);
1788 last_like_ = std::make_shared<BaseLikelihood>(model_fn, ctx, p_ids.size());
1789 last_fitter_ = std::make_shared<MLFitter>(ctx, model_fn);
1791 last_fit_param_ids_ = p_ids;
1792 last_nuisance_ids_ = eta_ids;
1793 last_fit_param_index_.clear();
1794 for (std::size_t i = 0; i < p_ids.size(); ++i) {
1795 last_fit_param_index_[p_ids[i]] = i;
1799 last_scan_eta_ = eta0;
1800 has_manual_scan_point_ =
false;
1803 std::cout <<
"[SCAN] Likelihood prepared without MLE.\n";
1804 std::cout <<
"[SCAN] n_fit_params = " << p_ids.size()
1805 <<
", n_nuisances = " << eta_ids.size() <<
"\n";
1810 const std::map<ParamId, double>& eta_hat) {
1812 throw std::runtime_error(
1813 "Please call prepare_likelihood_for_scan(...) or compute_MLE(...) before set_manual_scan_point(...)."
1817 last_scan_p_.resize(last_fit_param_ids_.size());
1818 for (std::size_t i = 0; i < last_fit_param_ids_.size(); ++i) {
1819 auto it = p_hat.find(last_fit_param_ids_[i]);
1820 if (it == p_hat.end()) {
1821 throw std::invalid_argument(
"Missing manual fit-parameter value for one scanned parameter.");
1823 last_scan_p_[i] = it->second;
1826 last_scan_eta_.resize(last_nuisance_ids_.size());
1827 for (std::size_t i = 0; i < last_nuisance_ids_.size(); ++i) {
1828 auto it = eta_hat.find(last_nuisance_ids_[i]);
1829 if (it == eta_hat.end()) {
1830 throw std::invalid_argument(
"Missing manual nuisance value for one nuisance parameter.");
1832 last_scan_eta_[i] = it->second;
1835 has_manual_scan_point_ =
true;
1837 std::cout <<
"[SCAN] Manual scan point loaded.\n";
1844 double x_half_width,
1845 double y_half_width,
1850 throw std::runtime_error(
1851 "Please call prepare_likelihood_for_scan(...) or compute_MLE(...) before requesting a likelihood scan."
1855 if (!last_fit_param_index_.contains(p1) || !last_fit_param_index_.contains(p2)) {
1856 throw std::invalid_argument(
1857 "Likelihood scan requested for parameters that are not in the current prepared parameter set."
1862 throw std::invalid_argument(
"Likelihood scan requires two distinct parameters.");
1865 const std::size_t ix = last_fit_param_index_.at(p1);
1866 const std::size_t iy = last_fit_param_index_.at(p2);
1868 std::vector<double> p_ref;
1869 std::vector<double> eta_ref;
1871 if (has_manual_scan_point_) {
1872 p_ref = last_scan_p_;
1873 eta_ref = last_scan_eta_;
1874 }
else if (!last_fit_raw_.
p_hat.empty() && !last_fit_raw_.
eta_hat.empty()) {
1875 p_ref = last_fit_raw_.
p_hat;
1876 eta_ref = last_fit_raw_.
eta_hat;
1877 }
else if (!last_scan_p_.empty() && last_scan_eta_.size() == last_nuisance_ids_.size()) {
1878 p_ref = last_scan_p_;
1879 eta_ref = last_scan_eta_;
1881 throw std::runtime_error(
1882 "No reference point available. Use compute_MLE(...), "
1883 "set_manual_scan_point(...), or prepare_likelihood_for_scan(...)."
1887 if (ix >= p_ref.size() || iy >= p_ref.size()) {
1888 throw std::runtime_error(
"Internal error: parameter index out of range.");
1891 std::vector<double> theta0 = p_ref;
1892 theta0.insert(theta0.end(), eta_ref.begin(), eta_ref.end());
1894 const double nll0 = last_like_->nll(theta0);
1903 out.
points.reserve(nx * ny);
1905 double nll_min = std::numeric_limits<double>::infinity();
1907 const double x_min = out.
x_center - x_half_width;
1908 const double x_max = out.
x_center + x_half_width;
1909 const double y_min = out.
y_center - y_half_width;
1910 const double y_max = out.
y_center + y_half_width;
1912 for (std::size_t i = 0; i < nx; ++i) {
1913 const double x = x_min + (x_max - x_min) *
static_cast<double>(i) /
static_cast<double>(nx - 1);
1915 for (std::size_t j = 0; j < ny; ++j) {
1916 const double y = y_min + (y_max - y_min) *
static_cast<double>(j) /
static_cast<double>(ny - 1);
1918 std::vector<double> theta = theta0;
1922 const double nll = last_like_->nll(theta);
1929 nll_min = std::min(nll_min, nll);
1930 out.
points.push_back(pt);
1934 for (
auto& pt : out.
points) {
1935 pt.delta_nll = pt.nll - nll_min;
1939 std::cout <<
"[SCAN] Built likelihood scan around current point\n";
1940 std::cout <<
"[SCAN] center x = " << out.
x_center <<
"\n";
1941 std::cout <<
"[SCAN] center y = " << out.
y_center <<
"\n";
1942 std::cout <<
"[SCAN] nll at reference point = " << nll0 <<
"\n";
1943 std::cout <<
"[SCAN] min nll on grid = " << nll_min <<
"\n";
1951 std::ofstream out(path);
1952 out <<
"# x=" << grid.
x_param <<
"\n";
1953 out <<
"# y=" << grid.
y_param <<
"\n";
1954 out <<
"# x_center=" << std::setprecision(17) << grid.
x_center <<
"\n";
1955 out <<
"# y_center=" << std::setprecision(17) << grid.
y_center <<
"\n";
1956 out <<
"# nx=" << grid.
nx <<
"\n";
1957 out <<
"# ny=" << grid.
ny <<
"\n";
1958 out <<
"x,y,nll,delta_nll\n";
1960 out << std::setprecision(17);
1961 for (
const auto& pt : grid.
points) {
1965 << pt.delta_nll <<
"\n";
1975 experiments.begin(),
1981 if (experiments.empty()) {
1982 throw std::invalid_argument(
1983 "StatisticManager::select_experiments: empty experiment set."
1987 selected_experiments_ = experiments;
1988 invalidate_fit_state();
1992 selected_experiments_.reset();
1993 invalidate_fit_state();
1997 return selected_experiments_.has_value();
2001 if (!selected_experiments_) {
2005 return *selected_experiments_;
2008bool StatisticManager::accepts_experiment_observable(
const ExperimentObs& exp_obs)
const {
2009 if (selected_experiments_.has_value()
2010 && !selected_experiments_->contains(exp_obs.
experiment)) {
2014 if (selected_experiment_observables_.has_value()
2015 && !selected_experiment_observables_->contains(exp_obs)) {
2024 observables.begin(),
2030 if (observables.empty()) {
2031 throw std::invalid_argument(
2032 "StatisticManager::select_experiment_observables: empty observable set."
2036 selected_experiment_observables_ = observables;
2037 invalidate_fit_state();
2041 selected_experiment_observables_.reset();
2042 invalidate_fit_state();
2046 return selected_experiment_observables_.has_value();
2050 if (!selected_experiment_observables_) {
2054 return *selected_experiment_observables_;
CopulaType
Identifies the copula family used to model dependence.
@ GAUSSIAN
Gaussian copula, defined by a correlation matrix.
@ STUDENT_T
Student-t copula, defined by a correlation matrix and degrees of freedom.
std::map< T, U > zip(const std::vector< T > &ids, const std::vector< U > &vals)
Builds a map from parallel id and value vectors.
UnzipResult1D< T, U > unzip(const std::map< T, U > &indexed)
Splits a map into parallel id and value vectors.
#define LOG_INFO(...)
Macro for logging informational messages.
#define LOG_VERBOSE(...)
Macro for logging verbose messages.
#define LOG_WARN(...)
Macro for logging warning messages.
MCObservableCovariance covariance_from_obs_samples(const ObsSamples &S, const std::vector< BinnedObservableId > &ids, double ridge_rel, double ridge_abs)
Builds a regularized empirical covariance matrix from observable samples.
MarginalType
Supported marginal-distribution families.
@ GAUSSIAN
Symmetric Gaussian marginal.
@ HALF_GAUSSIAN
Asymmetric / half-Gaussian-like marginal (currently mapped to split Gaussian logic).
@ LIKELIHOOD
Discrete likelihood-based marginal built from weighted support points.
@ FLAT
Uniform (flat) marginal on a finite interval.
std::string param_name(const ParamId &pid)
High-level orchestration of statistical uncertainty propagation, likelihood construction and fit scan...
StatisticLikelihoodMode
Selects the likelihood backend used by StatisticManager::compute_MLE().
@ PROFILED_NUISANCE
Full likelihood with explicit nuisance parameters profiled during the fit.
@ CHI2_MC_COVARIANCE
Fast chi-square likelihood using MC theory covariance plus experimental covariance.
std::string to_string() const
Returns a string representation of the block name.
static std::unique_ptr< ICopula > create(CopulaType name, CopulaConfig config, unsigned int seed=std::random_device{}())
Creates a concrete copula instance.
static std::string str(const IdOf< ObservableTag > &id)
Returns the string representation of an identifier.
MarginalConfig create(ParamId pid, MarginalType marginal) const
Builds a marginal configuration from a parameter identifier.
static std::unique_ptr< IMarginalDistribution > create(MarginalType name, MarginalConfig cfg, unsigned int seed=std::random_device{}())
Creates a concrete marginal-distribution object.
Samples nuisance parameters and propagates them through a model.
MCRealization sample_predictions(const std::map< ParamId, double > &p) const
Generates accepted model predictions for a fixed fit-parameter point.
MCResult summarize(const std::map< ParamId, double > &p) const
Runs Monte Carlo propagation and computes summary statistics.
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.
bool is_symmetric() const
Checks whether the matrix is symmetric.
std::size_t cols() const
Returns the number of columns.
Draws nuisance-parameter maps from a JointDistribution.
static void print(const StatCache &cache, std::ostream &os)
Coordinates statistical inputs, nuisance distributions, MLE fits and contour/scan computations.
void select_experiments(const std::set< std::string > &experiments)
Restricts subsequent statistics to the provided set of experiment names.
void set_manual_scan_point(const std::map< ParamId, double > &p_hat, const std::map< ParamId, double > &eta_hat)
Sets the reference point used by subsequent likelihood scans.
void print_cache()
Prints the current internal cache to standard output for debugging.
std::set< ExperimentObs > selected_experiment_observables() const
LikelihoodScanGrid scan_likelihood_around_current_point(ParamId p1, ParamId p2, double x_half_width, double y_half_width, std::size_t nx, std::size_t ny) const
Evaluates the current likelihood on a regular 2D grid around the active reference point.
std::map< ExperimentObs, std::map< ExperimentObs, double > > get_all_obs_correlations()
void reload_nuisance_specs()
Reloads default and user nuisance specifications and invalidates fit state.
std::vector< std::unique_ptr< IMarginalDistribution > > build_nuisance_marginal_distributions()
Builds marginal distributions for all currently cached nuisances.
void set_nuisance_user_file(const fs::path &user_yaml_path)
Selects a custom user nuisance-definition file and reloads nuisance specifications.
std::unique_ptr< JointDistribution > build_nuisance_distribution()
Builds the joint nuisance distribution from cached nuisance marginals and correlations.
bool has_experiment_observable_selection() const noexcept
void select_experiment_observables_all()
Clears the explicit experimental-observable selection.
void select_experiment_observables(const std::set< ExperimentObs > &observables)
Restricts subsequent statistics to an explicit list of experimental measurements.
void select_experiment(const std::string &experiment)
Restricts subsequent statistics to a single experiment name.
StatisticManager(StatisticConfig config, std::shared_ptr< IModel > obs_int, std::shared_ptr< IStatCorrelationProxy > pscp, std::shared_ptr< IStatParameterProxy > pspp, std::shared_ptr< IStatSourcesProxy > sp, std::shared_ptr< IStatDependencyPruner > dp, std::shared_ptr< INuisanceReader > nuisance_reader, std::shared_ptr< IStatParamOptimizerProxy > spop)
Constructs a statistic manager and initializes observable/nuisance state.
std::set< std::string > selected_experiments() const
std::map< ParamId, std::map< ParamId, double > > get_all_correlations()
void prepare_likelihood_for_scan(const std::vector< ParamId > &p_specs)
Prepares a likelihood object for manual scans without running a full MLE.
std::map< BinnedObservableId, GaussianSummary > compute_uncertainties()
Computes Gaussian summaries for MC-propagated observable uncertainties.
Contour confidence_contour(ParamId p1, ParamId p2, double z, std::array< double, 4 > bounds, ContourOptions options)
Computes a two-dimensional confidence contour for the last successful MLE.
std::map< ExperimentObs, double > get_obs_exp()
bool has_experiment_selection() const noexcept
FitResultWithMaps compute_MLE(const std::vector< ParamId > &p_specs)
Computes the maximum-likelihood fit for a selected set of fit parameters.
std::map< ParamId, double > get_p_specs(const std::vector< ParamId > &p_specs)
Resolves initial fit-parameter values from the parameter proxy.
void save_likelihood_scan_csv(const std::string &path, const LikelihoodScanGrid &grid) const
Writes a likelihood scan grid to a CSV file.
void update_cache(const std::vector< ParamId > &p_specs=std::vector< ParamId >())
Updates the full statistical cache for the selected fit parameters.
void select_experiments_all()
Clears any experiment selection and uses all available experiments.
std::unique_ptr< JointDistribution > build_exp_data_distribution()
Builds the joint experimental-data distribution from cached observables and correlations.
std::map< ParamId, double > get_all_obss_deps()
Selects all nuisance dependencies relevant to the current observable set.
void clear_nuisance_user_file()
Clears the custom user nuisance file and reloads the default configured user file.
MCResult compute_uncertainties_and_sampling()
Runs MC uncertainty propagation and returns both samples and summaries.
void finish(const std::string &message)
void start(const std::string &message)
void step(std::size_t completed_steps, const std::string &message)
Hash specialization for SymbolId<Tag>.
std::enable_if_t< not std::numeric_limits< T >::is_integer, bool > fpeq(T, T, std::size_t n=10)
Compares two floating point numbers with a given precision.
double T(double x)
Wilson coefficient T(x).
std::map< ExperimentObs, MarginalType > override_exp_data_marginals
Per-observable overrides for experimental-data marginals.
double chi2_covariance_ridge_abs
Absolute diagonal ridge used before inverting chi-square covariance matrices.
double nuisance_relevance_cutoff
Relative-uncertainty cutoff for the first nuisance preselection pass.
bool MLE_allow_profile_hessian_fallback
Allows numerical profile-Hessian covariance fallback if backend covariance fails.
bool MLE_trace_first_evals
Enables debug tracing of the first likelihood evaluations.
bool fit_parameter_sensitivity_keep_on_failure
Keeps a fit parameter when its sensitivity probe cannot be evaluated safely.
bool MLE_run_hesse
Whether to request HESSE/covariance estimation after the fit.
unsigned MLE_strategy
Backend minimization strategy; zero means backend default where supported.
double nuisance_sensitivity_probe_sigmas
Size of the +/- finite-difference probe in units of nuisance sigma.
double MLE_tol
Minimizer tolerance passed to the backend.
double fit_parameter_sensitivity_rel_cutoff
Relative observable shift required to regard a fit parameter as active.
double fit_parameter_sensitivity_probe_fraction
Minimum probe size as a fraction of explicit/default fit bounds.
bool MLE_verbose
Enables verbose output from the fit backend.
double nuisance_sensitivity_context_sigma
Randomized-context spread in nuisance sigma units.
int nuisance_sensitivity_contexts
Number of contexts tested by sensitivity pruning; negative disables the check.
double MLE_profile_hessian_step_scale
Step scaling used by the numerical profile-Hessian fallback.
double chi2_covariance_ridge_rel
Relative diagonal ridge used before inverting chi-square covariance matrices.
double nuisance_sensitivity_rel_cutoff
Relative observable shift required to keep a nuisance.
bool MLE_request_minos
Whether to request MINOS errors when supported by the backend build.
unsigned nuisance_sensitivity_seed
RNG seed used to build sensitivity-pruning contexts.
bool nuisance_sensitivity_keep_on_failure
Keeps a nuisance if its sensitivity probe fails.
std::map< ParamId, MarginalType > override_nuisance_marginals
Per-parameter overrides for nuisance marginal laws.
double fit_parameter_sensitivity_abs_cutoff
Absolute observable shift required to regard a fit parameter as active.
std::size_t MLE_max_iter
Maximum number of minimizer function calls/iterations.
double nuisance_sensitivity_scale_floor
Lower scale used when normalizing relative observable shifts.
double nuisance_sensitivity_abs_cutoff
Absolute observable shift required to keep a nuisance.
bool MC_force_decay_threads_to_one
Give MC priority over internal decay parallelism.
CopulaType nuisance_copula_type
Copula used to correlate nuisance parameters.
bool fit_parameter_sensitivity_check
Reject fit parameters that do not measurably change any selected observable.
std::size_t MLE_trace_max_evals
Maximum number of likelihood evaluations printed when tracing is enabled.
double MLE_profile_hessian_eig_floor_rel
Relative eigenvalue floor used to regularize the fallback Hessian.
StatisticLikelihoodMode likelihood_mode
Likelihood mode used by compute_MLE().
std::size_t MC_forced_decay_threads
Decay thread count while MC workers are running.
bool nuisance_sensitivity_pruning
Enables local model-sensitivity pruning of nuisance candidates.
CopulaType exp_data_copula_type
Copula used to correlate experimental observables.
Runtime options controlling 2D contour computation.
Output of a contour extraction algorithm.
Container for an eigendecomposition.
RealMatrix P
Diagonal matrix of eigenvalues.
User-facing MLE result keyed by physics parameter identifiers.
std::map< ParamId, double > p_hat_std
Profiled standard deviations of fitted parameters.
std::map< ParamId, double > eta_hat
Best-fit/profiler values of nuisance parameters.
std::map< ParamId, std::map< ParamId, double > > p_correlations
Correlation matrix of fitted parameters.
std::map< ParamId, double > p_hat
Best-fit values of fitted parameters.
double ell_hat
Minimum negative log-likelihood value.
bool fit_ok
True when the fit returned a usable parameter estimate.
std::vector< double > p_hat_std
Standard deviations of the parameters of interest.
std::vector< double > eta_hat
Nuisance estimates at the global maximum-likelihood point.
std::vector< double > p_hat
Maximum-likelihood estimates for parameters of interest.
RealMatrix p_hat_correlations
Correlation matrix for the parameters of interest.
double ell_hat
Minimum NLL value at the global best-fit point.
Configuration object for a Gaussian copula.
Regular two-dimensional grid of likelihood-scan evaluations.
double y_center
Reference value used as the scan center on y.
double x_center
Reference value used as the scan center on x.
ParamId y_param
Identifier of the second scanned parameter.
std::vector< LikelihoodScanPoint > points
Flattened grid points in x-major order.
ParamId x_param
Identifier of the first scanned parameter.
std::size_t nx
Number of grid points along x.
std::size_t ny
Number of grid points along y.
Single point of a two-dimensional likelihood scan.
double x
Coordinate along the first scanned fit parameter.
double nll
Negative log-likelihood value at this point.
double y
Coordinate along the second scanned fit parameter.
Runtime configuration for Monte Carlo nuisance propagation.
bool force_decay_threads_to_one
std::size_t progress_probe_draws
double skew_abs_threshold
std::size_t forced_decay_threads
double covariance_ridge_rel
std::size_t progress_update_every
std::string samples_csv_path
std::shared_ptr< StatisticProgressMonitor > progress_monitor
double covariance_ridge_abs
Empirical observable covariance and its inverse.
Raw Monte Carlo samples accepted by the engine.
Runtime options controlling the global maximum-likelihood fit.
double profile_hessian_eig_floor_rel
double profile_hessian_step_scale
std::size_t trace_max_evals
bool allow_profile_hessian_fallback
Specification of one nuisance parameter.
Composite identifier for a single parameter.
std::optional< ParameterType > type
Optional high-level parameter category.
BlockName block
Name of the block where the parameter is stored.
LhaID code
Index or multi-index of the parameter inside the block.
std::map< ParamId, double > p_specs
Selected fit parameters and their initial values.
FitResultWithMaps mle_result
Last MLE result expressed with map-based identifiers.
std::map< ExperimentObs, double > exp_obs
Experimental central values used by the current fit.
std::map< ParamId, double > eta_specs_real
Selected nuisance parameters and their central values.
std::map< ExperimentObs, std::map< ExperimentObs, double > > SigmaObs
Correlation matrix of selected experimental observables.
std::map< ParamId, std::map< ParamId, double > > SigmaEta
Correlation matrix of selected nuisances.
std::shared_ptr< StatisticProgressMonitor > progress_monitor
Optional thread-safe progress sink for GUI/notebook frontends.
std::string mc_samples_csv_path
Output CSV path used when write_mc_samples_csv is true.
bool print_scan_summary
Print likelihood-scan summaries.
bool print_fit_summary
Print high-level fit backend summaries.
unsigned int MC_seed
RNG seed used for reproducible MC nuisance and experimental-data sampling.
std::size_t mc_progress_probe_draws
Number of first accepted draws used to stabilize the first ETA.
std::map< ParamId, std::pair< double, double > > fit_parameter_bounds
Optional explicit minimizer bounds keyed by fit ParamId.
bool write_mc_samples_csv
Write accepted MC observable samples to CSV.
bool print_mc_config
Print nuisance candidates and retained MC marginal configuration.
AdvancedStatisticConfig advanced
Advanced fit/pruning/covariance configuration.
bool print_chi2_pipeline_progress
Print chi-square workflow stages after/beside the MC progress bar.
bool print_mc_progress
Print MC progress with ETA based on measured draw time.
std::size_t MC_draws
Number of accepted MC draws used for uncertainty propagation.
bool print_debug
Master debug flag for low-level diagnostic output.
std::size_t MC_threads
Number of worker threads used by MC propagation.
std::size_t mc_progress_update_every
Accepted-draw stride between progress updates.
std::map< ParamId, double > fit_parameter_offsets
Optional affine display offsets: model value = fitted value - offset.
double skew_abs_threshold
Absolute skewness threshold below which a summary is treated as symmetric.
bool print_cache_summary
Print internal cache diagnostics.
Configuration object for a Student-t copula.
int nu
Correlation matrix of the latent Student-t vector.
std::optional< std::pair< double, double > > limits