Hyperiso 1.0.3
Modular flavour-physics calculations, Wilson coefficients and statistical inference
Loading...
Searching...
No Matches
StatisticManager.cpp
Go to the documentation of this file.
1
2#include "StatisticManager.h"
4#include "StatisticProgress.h"
5
6#include <algorithm>
7#include <type_traits>
8
9std::string param_name(const ParamId& pid) {
10 std::ostringstream oss;
11 oss << pid;
12 return oss.str();
13}
14
15static void dump_matrix_sanity(const RealMatrix& M,
16 const std::vector<ParamId>& ids,
17 const std::string& name)
18{
19 std::cout << "[FIT] Matrix " << name
20 << " shape=" << M.rows() << "x" << M.cols()
21 << " symmetric=" << M.is_symmetric() << "\n";
22
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;
26
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};
30
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;
36
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) {
40 max_abs_offdiag = a;
41 argmax_offdiag = {i,j};
42 }
43 if (a > 1.0 + 1e-10) ++n_abs_gt_1;
44 }
45 }
46
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
53 << "\n";
54
55 if (M.rows() == M.cols() && M.is_symmetric()) {
56 EigenSystem e = M.eig();
57
58 std::size_t imin = 0;
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) {
62 evmin = e.D.at(i,i);
63 imin = i;
64 }
65 }
66
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});
70 }
71 std::sort(comps.begin(), comps.end(),
72 [](const auto& a, const auto& b) { return a.first > b.first; });
73
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";
80 }
81 }
82}
83
84namespace {
85
86std::string marginal_type_name(MarginalType mt) {
87 switch (mt) {
88 case MarginalType::GAUSSIAN: return "Gaussian";
89 case MarginalType::HALF_GAUSSIAN: return "Split Gaussian";
90 case MarginalType::FLAT: return "Flat";
91 case MarginalType::LIKELIHOOD: return "Likelihood";
92 }
93 return "Unknown";
94}
95
96std::string copula_type_name(CopulaType ct) {
97 switch (ct) {
98 case CopulaType::GAUSSIAN: return "Gaussian";
99 case CopulaType::STUDENT_T: return "Student-t";
100 }
101 return "Unknown";
102}
103
104std::string likelihood_mode_name(StatisticLikelihoodMode mode) {
105 switch (mode) {
106 case StatisticLikelihoodMode::PROFILED_NUISANCE: return "profiled nuisance";
107 case StatisticLikelihoodMode::CHI2_MC_COVARIANCE: return "chi-square with MC covariance";
108 }
109 return "unknown";
110}
111
112std::string compact_double(double x) {
113 std::ostringstream oss;
114 oss << std::setprecision(8) << x;
115 return oss.str();
116}
117
118std::string marginal_config_summary(const MarginalConfig& cfg) {
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);
123
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();
134 } else {
135 oss << "custom config";
136 }
137 return oss.str();
138 }, cfg);
139}
140
141double safe_param_value(const std::shared_ptr<IStatParameterProxy>& pspp, const ParamId& pid) {
142 try {
143 return pspp->get_param(pid)->get_val().real();
144 } catch (...) {
145 return std::numeric_limits<double>::quiet_NaN();
146 }
147}
148
149double safe_param_sigma(const std::shared_ptr<IStatParameterProxy>& pspp, const ParamId& pid) {
150 try {
151 return std::abs(pspp->get_param(pid)->get_combined_std().real());
152 } catch (...) {
153 return std::numeric_limits<double>::quiet_NaN();
154 }
155}
156
157void print_nuisance_candidate_table(const std::string& title,
158 const std::vector<ParamId>& ids,
159 const std::shared_ptr<IStatParameterProxy>& pspp)
160{
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))
166 << "\n";
167 }
168}
169
170void print_nuisance_value_table(const std::string& title,
171 const std::map<ParamId, double>& values,
172 const std::shared_ptr<IStatParameterProxy>& pspp)
173{
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))
179 << "\n";
180 }
181}
182
183void print_nuisance_marginal_line(const ParamId& pid,
184 double value,
185 double sigma,
186 MarginalType mt,
187 const MarginalConfig& cfg)
188{
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";
194}
195
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,
201 unsigned seed
202) {
203 std::vector<std::map<ParamId, double>> contexts;
204 contexts.reserve(std::max<std::size_t>(1, n_contexts));
205
206 contexts.push_back(eta0);
207
208 std::mt19937 rng(seed);
209 std::normal_distribution<double> normal(0.0, context_sigma);
210
211 for (std::size_t c = 1; c < n_contexts; ++c) {
212 auto ctx = eta0;
213
214 for (auto& [pid, val] : ctx) {
215 const double sigma =
216 std::abs(pspp->get_param(pid)->get_combined_std().real());
217
218 if (!std::isfinite(sigma) || sigma <= 0.0) {
219 continue;
220 }
221
222 const double z = normal(rng);
223
224 val += z * sigma;
225 }
226
227 contexts.push_back(std::move(ctx));
228 }
229
230 return contexts;
231}
232
233double fit_parameter_offset(const StatisticConfig& config, const ParamId& pid) {
234 const auto it = config.fit_parameter_offsets.find(pid);
235 return it == config.fit_parameter_offsets.end() ? 0.0 : it->second;
236}
237
238std::vector<double> fit_coordinates_from_model_values(
239 const StatisticConfig& config,
240 const std::vector<ParamId>& p_ids,
241 std::vector<double> model_values)
242{
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]);
245 }
246 return model_values;
247}
248
249std::vector<double> model_values_from_fit_coordinates(
250 const StatisticConfig& config,
251 const std::vector<ParamId>& p_ids,
252 const std::vector<double>& fit_values)
253{
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]);
257 }
258 return model_values;
259}
260
261fit_app::ParameterDefinition make_fit_param_def(
262 const ParamId& pid,
263 double value,
264 double sigma_hint,
265 const StatisticConfig& config)
266{
268 out.name = param_name(pid);
269 out.value = value;
270 out.step_hint = (std::isfinite(sigma_hint) && sigma_hint > 0.0)
271 ? sigma_hint
272 : std::max(1e-3, 0.01 * std::max(1.0, std::abs(value)));
273
274 const auto configured_bounds = config.fit_parameter_bounds.find(pid);
275 if (configured_bounds != config.fit_parameter_bounds.end()) {
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));
279 }
280 out.limits = configured_bounds->second;
281 return out;
282 }
283
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);
287 }
288
289 return out;
290}
291
292template <class PredMapT>
293std::vector<double> ordered_prediction_vector(
294 const std::vector<ExperimentObs>& obs_ids,
295 const PredMapT& pred_map)
296{
297 std::vector<double> out;
298 out.reserve(obs_ids.size());
299
300 for (const auto& bid : obs_ids) {
301 const auto& vec = pred_map.at(bid.obs.s);
302
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;
306 });
307
308 if (it == vec.end()) {
309 throw std::runtime_error("Missing predicted observable/bin.");
310 }
311
312 out.push_back(it->value);
313 }
314
315 return out;
316}
317
318
319std::vector<BinnedObservableId> binned_ids_from_experiment_obs(
320 const std::vector<ExperimentObs>& obs_ids
321) {
322 std::vector<BinnedObservableId> out;
323 out.reserve(obs_ids.size());
324 for (const auto& oid : obs_ids) {
325 out.push_back(oid.obs);
326 }
327 return out;
328}
329
330
331RealMatrix symmetrize_covariance_matrix(RealMatrix cov) {
332 if (cov.rows() != cov.cols()) {
333 throw std::runtime_error("symmetrize_covariance_matrix: covariance must be square");
334 }
335
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);
340
341 if (!std::isfinite(a) || !std::isfinite(b)) {
342 throw std::runtime_error("symmetrize_covariance_matrix: non-finite covariance entry");
343 }
344
345 const double v = 0.5 * (a + b);
346 cov.at(i, j) = v;
347 cov.at(j, i) = v;
348 }
349
350 if (!std::isfinite(cov.at(i, i))) {
351 throw std::runtime_error("symmetrize_covariance_matrix: non-finite covariance diagonal");
352 }
353 }
354
355 return cov;
356}
357
358RealMatrix experimental_covariance_matrix(
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
362) {
363 const std::size_t n = obs_ids.size();
364 RealMatrix cov(n, n);
365
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 "
371 << obs_ids[i].str();
372 throw std::runtime_error(oss.str());
373 }
374
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");
378 }
379
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 "
385 << obs_ids[j].str();
386 throw std::runtime_error(oss.str());
387 }
388
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");
392 }
393
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;
400 }
401 }
402
403 if (!std::isfinite(corr)) {
404 throw std::runtime_error("experimental_covariance_matrix: non-finite correlation");
405 }
406
407 cov.at(i, j) = corr * sigma_i * sigma_j;
408 }
409 }
410
411 return symmetrize_covariance_matrix(std::move(cov));
412}
413
414RealMatrix inverse_covariance_with_ridge(
415 RealMatrix cov,
416 double ridge_rel,
417 double ridge_abs
418) {
419 cov = symmetrize_covariance_matrix(std::move(cov));
420
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");
424 }
425
426 std::vector<double> sigma(n);
427
428 for (std::size_t i = 0; i < n; ++i) {
429 const double vii = cov.at(i, i);
430
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());
436 }
437
438 sigma[i] = std::sqrt(vii);
439 }
440
441 RealMatrix corr(n, n);
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]);
445 }
446 }
447
448 corr = symmetrize_covariance_matrix(std::move(corr));
449
450 const double ridge = std::max(ridge_rel, ridge_abs);
451
452 for (std::size_t i = 0; i < n; ++i) {
453 corr.at(i, i) += ridge;
454 }
455
456 corr = symmetrize_covariance_matrix(std::move(corr));
457
458 RealMatrix corr_inv = corr.inv();
459
460 RealMatrix cov_inv(n, n);
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]);
464 }
465 }
466
467 return symmetrize_covariance_matrix(std::move(cov_inv));
468}
469
470MLFitOptions make_mlfit_options_from_config(const StatisticConfig& config) {
471 MLFitOptions fitopt;
472 fitopt.run_hesse = config.advanced.MLE_run_hesse;
474 fitopt.verbose = config.advanced.MLE_verbose;
475 fitopt.strategy = config.advanced.MLE_strategy;
476 fitopt.max_fcn = static_cast<unsigned>(config.advanced.MLE_max_iter);
477 fitopt.tolerance = config.advanced.MLE_tol;
478
482
485
486 return fitopt;
487}
488
489MCConfig make_mc_config_from_config(const StatisticConfig& config, bool for_chi2_covariance = false) {
490 MCConfig cfg;
491 cfg.draws = config.MC_draws;
492 cfg.n_threads = config.MC_threads;
496 cfg.covariance_ridge_rel = for_chi2_covariance
499 cfg.covariance_ridge_abs = for_chi2_covariance
508 return cfg;
509}
510
511
512}
513
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),
526 sp(std::move(sp)),
527 dp(std::move(dp)),
528 nuisance_reader_(std::move(nuisance_reader)),
529 spop(std::move(spop)),
530 config(std::move(config))
531{
532 if (!nuisance_reader_) {
533 throw std::invalid_argument("StatisticManager: nuisance_reader is null");
534 }
535
536 this->obs_int->compute_observables();
538 invalidate_fit_state();
539}
540
541std::vector<std::unique_ptr<IMarginalDistribution>> StatisticManager::build_nuisance_marginal_distributions() {
542 unsigned int seed = config.MC_seed;
543 std::vector<std::unique_ptr<IMarginalDistribution>> marginals;
544
545 marginals.reserve(cache.eta_specs_real.size());
546
547 const bool print_config = config.print_mc_config || config.print_debug;
548 if (print_config) {
549 std::cout << "[MC CONFIG] Retained nuisance marginals used by MC: "
550 << cache.eta_specs_real.size() << "\n";
551 }
552
553 for (const auto& [pid, value] : cache.eta_specs_real) {
554 const MarginalType mt = resolve_nuisance_marginal_type(pid);
555 MarginalConfig cfg = make_nuisance_marginal_config(pid, mt);
556
557 if (print_config) {
558 print_nuisance_marginal_line(
559 pid,
560 value,
561 safe_param_sigma(pspp, pid),
562 mt,
563 cfg
564 );
565 }
566
567 marginals.emplace_back(MarginalFactory::create(mt, cfg, seed));
568 }
569
570 return marginals;
571}
572
573std::unique_ptr<JointDistribution> StatisticManager::build_nuisance_distribution() {
574 unsigned int seed = config.MC_seed;
575
576 if (config.print_mc_config || config.print_debug) {
577 std::cout << "[MC CONFIG] Nuisance distribution: "
578 << cache.eta_specs_real.size() << " retained nuisance(s), copula="
579 << copula_type_name(config.advanced.nuisance_copula_type)
580 << "\n";
581 }
582
583 std::unique_ptr<ICopula> copula;
585 GaussianCopulaConfig copula_cfg;
586 copula_cfg.R = RealMatrix(unzip(cache.SigmaEta).vals);
587 copula = CopulaFactory::create(config.advanced.nuisance_copula_type, copula_cfg, seed);
589 StudentTCopulaConfig copula_cfg;
590 copula_cfg.R = RealMatrix(unzip(cache.SigmaEta).vals);
591 copula_cfg.nu = cache.eta_specs_real.size();
592 copula = CopulaFactory::create(config.advanced.nuisance_copula_type, copula_cfg, seed);
593 }
594
595 return std::make_unique<JointDistribution>(
597 std::move(copula)
598 );
599}
600
601std::unique_ptr<JointDistribution> StatisticManager::build_exp_data_distribution() {
602 unsigned int seed = config.MC_seed ^ 0x9E3779B9u;
603 std::map<ExperimentObs, MarginalType> exp_data_marginals;
604
605 for (auto& [oid, v] : cache.exp_obs) {
606 exp_data_marginals.emplace(oid, MarginalType::GAUSSIAN);
607 }
608
609 for (auto& [oid, mt] : config.advanced.override_exp_data_marginals) {
610 if (!exp_data_marginals.contains(oid)) {
611 LOG_WARN("Observable", ObservableMapper::str(oid.obs.s), "is not a taken into account in this fit.");
612 continue;
613 }
614 exp_data_marginals.at(oid) = mt;
615 }
616
617 auto unzipped = unzip(exp_data_marginals);
618 std::vector<ExperimentObs> obs_ids = unzipped.ids;
619 std::vector<std::unique_ptr<IMarginalDistribution>> marginals;
620
621 for (auto& [oid, mt] : exp_data_marginals) {
622 MarginalConfig cfg = marginal_config_factory_.create(oid, mt);
623 auto m_ptr = MarginalFactory::create(mt, cfg, seed);
624 marginals.emplace_back(std::move(m_ptr));
625 }
626
627 std::unique_ptr<ICopula> copula;
629 GaussianCopulaConfig copula_cfg;
630 copula_cfg.R = RealMatrix(unzip(cache.SigmaObs).vals);
631 copula = CopulaFactory::create(config.advanced.exp_data_copula_type, copula_cfg, seed);
633 StudentTCopulaConfig copula_cfg;
634 copula_cfg.R = RealMatrix(unzip(cache.SigmaObs).vals);
635 copula_cfg.nu = obs_int->n_observables() - 1;
636 copula = CopulaFactory::create(config.advanced.exp_data_copula_type, copula_cfg, seed);
637 }
638
639 return std::make_unique<JointDistribution>(std::move(marginals), std::move(copula));
640}
641
642std::map<BinnedObservableId, GaussianSummary> StatisticManager::compute_uncertainties() {
643 auto sums = this->compute_uncertainties_and_sampling();
644
645 if (config.print_debug) {
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";
651 }
652 }
653
654 std::map<BinnedObservableId, GaussianSummary> out;
655 if (config.print_mc_config || config.print_debug) {
656 std::cout << "[MC CONFIG] Observable MC summaries: "
657 << sums.summary.size() << "\n";
658 }
659 for (const auto& gs : sums.summary) {
660 out[gs.id] = gs;
661 if (config.print_mc_config || config.print_debug) {
662 std::cout << "[MC CONFIG] " << gs << "\n";
663 }
664 }
665 return out;
666}
667
669 if (this->config.progress_monitor) {
670 this->config.progress_monitor->reset("preparing", "Preparing Monte-Carlo uncertainty propagation");
671 }
672 update_cache();
673 auto rvg = build_nuisance_distribution();
674 std::vector<ParamId> nuisance_ids = unzip(cache.eta_specs_real).ids;
675 RvgNuisanceSampler sampler(nuisance_ids, std::move(rvg));
676 MonteCarloEngine mc(this->obs_int, sampler, make_mc_config_from_config(this->config));
677
678 auto sums = mc.summarize(this->cache.p_specs);
679
680 return sums;
681}
682
683FitResultWithMaps StatisticManager::compute_MLE(const std::vector<ParamId>& p_specs) {
684 if (this->config.progress_monitor) {
685 this->config.progress_monitor->reset("preparing", "Preparing chi-square fit");
686 }
687 update_cache(p_specs);
688
689 if (cache.p_specs.empty()) {
690 throw std::invalid_argument("compute_MLE called with an empty fit parameter list.");
691 }
692
693 auto unzipped_fit_params = unzip(cache.p_specs);
694 auto unzipped_nuisances = unzip(cache.eta_specs_real);
695 auto unzipped_exp_obs = unzip(cache.exp_obs);
696
697 if (config.print_fit_summary || config.print_debug) {
698 auto eta_ids_dbg = unzip(cache.eta_specs_real).ids;
699 RealMatrix Reta(unzip(cache.SigmaEta).vals);
700 dump_matrix_sanity(Reta, eta_ids_dbg, "SigmaEta");
701 }
702
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
706 );
707
708 const std::vector<ParamId> eta_ids = unzipped_nuisances.ids;
709 const std::vector<double> eta0 = unzipped_nuisances.vals;
710
711 RealMatrix SigmaEta(unzip(cache.SigmaEta).vals);
712
713 const std::vector<ExperimentObs> obs_ids = unzipped_exp_obs.ids;
714 const std::vector<double> exp_obs_vals = unzipped_exp_obs.vals;
715
717 if (config.print_fit_summary || config.print_debug) {
718 std::cout << "[FIT] Likelihood backend: "
719 << likelihood_mode_name(config.advanced.likelihood_mode) << ".\n";
720 }
721
722 const bool show_chi2_progress =
724 config.print_mc_progress ||
725 config.print_debug;
726 StatisticStageProgressReporter chi2_progress(
727 show_chi2_progress,
728 "CHI2 workflow",
729 7,
730 std::cout,
731 this->config.progress_monitor,
732 "chi2_pipeline"
733 );
734 chi2_progress.start(
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."
736 );
737
738 auto rvg = build_nuisance_distribution();
739 std::vector<ParamId> nuisance_ids = unzip(cache.eta_specs_real).ids;
740 RvgNuisanceSampler sampler(nuisance_ids, std::move(rvg));
741
743 this->obs_int,
744 sampler,
745 make_mc_config_from_config(this->config, true)
746 );
747
748 MCRealization mc_real = mc.sample_predictions(this->cache.p_specs);
749 chi2_progress.step(
750 1,
751 "Monte-Carlo sampling completed; building the MC covariance matrix."
752 );
753
754 const std::vector<BinnedObservableId> cov_ids =
755 binned_ids_from_experiment_obs(obs_ids);
756
758 mc_real.sampled_obss,
759 cov_ids,
760 this->config.advanced.chi2_covariance_ridge_rel,
761 this->config.advanced.chi2_covariance_ridge_abs
762 );
763 chi2_progress.step(
764 2,
765 "MC covariance ready; collecting experimental uncertainties."
766 );
767
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)) {
773 continue;
774 }
775 exp_obs_sigmas[exp_obs] =
776 std::abs(param->get_combined_std().real());
777 }
778 }
779
780 chi2_progress.step(
781 3,
782 "Experimental uncertainties ready; assembling the total covariance matrix."
783 );
784 const RealMatrix covariance_exp = experimental_covariance_matrix(
785 obs_ids,
786 this->cache.SigmaObs,
787 exp_obs_sigmas
788 );
789
790 RealMatrix covariance_total =
791 symmetrize_covariance_matrix(cov.covariance + covariance_exp);
792
793 chi2_progress.step(
794 4,
795 "Total covariance assembled; inverting the regularized covariance matrix."
796 );
797 RealMatrix covariance_total_inv = inverse_covariance_with_ridge(
798 covariance_total,
800 this->config.advanced.chi2_covariance_ridge_abs
801 );
802 chi2_progress.step(
803 5,
804 "Covariance inverse ready; constructing the chi-square likelihood."
805 );
806
807 if (config.print_fit_summary || config.print_debug) {
808 std::cout << "[FIT] Covariance model: total = MC theory covariance + experimental covariance.\n";
809 }
810
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());
815
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));
819 }
820
821
822 const std::map<ParamId, double> eta0_map = zip(eta_ids, eta0);
823
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>
827 {
828 if (!eta_vec.empty()) {
829 throw std::runtime_error("CHI2_MC_COVARIANCE model_fn expects empty eta vector");
830 }
831
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(
834 zip(p_ids, model_p),
835 eta0_map
836 );
837
838 return ordered_prediction_vector(obs_ids, pred_map);
839 };
840 last_ctx_ = ctx;
841 last_like_ = std::make_shared<ChiSquaredLikelihood>(
842 model_fn,
843 ctx,
844 p_ids.size(),
845 covariance_total_inv
846 );
847
848 chi2_progress.step(
849 6,
850 "Likelihood ready; running the maximum-likelihood fit. This backend-dependent step has no reliable ETA."
851 );
852
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.");
857
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;
863 }
864
866 out.fit_ok = !last_fit_raw_.p_hat.empty();
867 out.ell_hat = last_fit_raw_.ell_hat;
868 out.p_hat = zip(p_ids, last_fit_raw_.p_hat);
869 out.eta_hat.clear();
870 out.p_hat_std = zip(p_ids, last_fit_raw_.p_hat_std);
871 out.p_correlations = zip(p_ids, last_fit_raw_.p_hat_correlations);
872
873 cache.mle_result = out;
874 return out;
875 }
876
877 auto ctx = std::make_shared<LikelihoodContext>();
878 ctx->nuisance_dist = build_nuisance_distribution();
879 ctx->exp_obs_dist = build_exp_data_distribution();
880 ctx->exp_obs_values = exp_obs_vals;
881
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));
886 }
887
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)
893 );
894 }
895
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>
899 {
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(
902 zip(p_ids, model_p),
903 zip(eta_ids, eta_vec)
904 );
905
906 return ordered_prediction_vector(obs_ids, pred_map);
907 };
908
909 last_ctx_ = ctx;
910 last_like_ = std::make_shared<BaseLikelihood>(model_fn, ctx, p_ids.size());
911
912 MLFitOptions fitopt;
913 fitopt.run_hesse = config.advanced.MLE_run_hesse;
915 fitopt.verbose = config.advanced.MLE_verbose;
916 fitopt.strategy = config.advanced.MLE_strategy;
917 fitopt.max_fcn = static_cast<unsigned>(config.advanced.MLE_max_iter);
918 fitopt.tolerance = config.advanced.MLE_tol;
919
923
926
927 last_fitter_ = std::make_shared<MLFitter>(ctx, model_fn, fitopt);
928 last_fit_raw_ = last_fitter_->maximum_likelihood_fit(p0);
929
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;
935 }
936
938 out.fit_ok = !last_fit_raw_.p_hat.empty();
939 out.ell_hat = last_fit_raw_.ell_hat;
940 out.p_hat = zip(p_ids, last_fit_raw_.p_hat);
941 out.eta_hat = zip(eta_ids, last_fit_raw_.eta_hat);
942 out.p_hat_std = zip(p_ids, last_fit_raw_.p_hat_std);
943 out.p_correlations = zip(p_ids, last_fit_raw_.p_hat_correlations);
944
945 cache.mle_result = out;
946 return out;
947}
948
949Contour StatisticManager::confidence_contour(ParamId p1, ParamId p2, double z, std::array<double, 4> bounds, ContourOptions options) {
950 if (!cache.mle_result.fit_ok || !last_fitter_) {
951 throw std::runtime_error("Please run compute_MLE before requesting a confidence contour.");
952 }
953
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.");
956 }
957
958 if (p1 == p2) {
959 throw std::invalid_argument("Contour requires two distinct parameters.");
960 }
961
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);
964
965 Contour cl = last_fitter_->contour(
966 x_id,
967 y_id,
968 z,
969 bounds,
970 options
971 );
972
973 return cl;
974}
975
976void StatisticManager::validate_fit_parameter_sensitivity() {
977 if (!config.advanced.fit_parameter_sensitivity_check || cache.p_specs.empty()) {
978 return;
979 }
980
981 if (cache.exp_obs.empty()) {
982 throw std::invalid_argument(
983 "Fit parameter sensitivity check failed: no experimental observable is "
984 "available for the current observable/experiment selection."
985 );
986 }
987
988 const auto unzipped_exp_obs = unzip(cache.exp_obs);
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;
993
994 struct RestoreModelStateGuard {
995 StatisticManager* self = nullptr;
996 std::map<ParamId, double> p;
997 std::map<ParamId, double> eta;
998
999 ~RestoreModelStateGuard() {
1000 if (self == nullptr) {
1001 return;
1002 }
1003 try {
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;
1009 } catch (...) {
1010 std::cout << "[FIT] WARNING: failed to restore central model state after "
1011 "fit-parameter sensitivity checks: unknown exception"
1012 << std::endl;
1013 }
1014 }
1015 } restore_guard{this, p_central, eta_central};
1016
1017 std::vector<double> baseline_pred;
1018 try {
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"
1026 );
1027 }
1028 } catch (const std::exception& e) {
1030 LOG_WARN(
1031 "Fit-parameter sensitivity baseline could not be evaluated; "
1032 "continuing conservatively. Reason:", e.what()
1033 );
1034 return;
1035 }
1036 throw std::runtime_error(
1037 std::string("Fit-parameter sensitivity baseline failed: ") + e.what()
1038 );
1039 }
1040
1041 const double probe_fraction = std::clamp(
1043 1e-6,
1044 0.5
1045 );
1046 const bool verbose = config.print_fit_summary || config.print_debug;
1047 std::vector<ParamId> inactive;
1048
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);
1056
1057 double step = def.step_hint;
1058 if (def.limits.has_value()) {
1059 const auto [low, high] = def.limits.value();
1060 step = std::max(step, probe_fraction * (high - low));
1061 }
1062 if (!std::isfinite(step) || !(step > 0.0)) {
1063 step = std::max(1e-3, 0.01 * std::max(1.0, std::abs(fit_center)));
1064 }
1065
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);
1072 }
1073
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;
1079
1080 const auto probe = [&](double fit_value) {
1081 if (!std::isfinite(fit_value) ||
1082 std::abs(fit_value - fit_center) < 1e-14) {
1083 return;
1084 }
1085
1086 auto p_probe = p_central;
1087 p_probe[pid] = fit_value - fit_parameter_offset(config, pid);
1088
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);
1092
1093 evaluated = true;
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"
1098 );
1099 }
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]),
1105 });
1106 best_abs_shift = std::max(best_abs_shift, abs_shift);
1107 best_rel_shift = std::max(best_rel_shift, abs_shift / scale);
1108 }
1109 };
1110
1111 try {
1112 probe(fit_minus);
1113 probe(fit_plus);
1114 } catch (const std::exception& e) {
1115 evaluation_failed = true;
1116 failure_reason = e.what();
1117 } catch (...) {
1118 evaluation_failed = true;
1119 failure_reason = "unknown exception";
1120 }
1121
1122 if (evaluation_failed || !evaluated) {
1124 LOG_WARN(
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"
1128 );
1129 continue;
1130 }
1131 inactive.push_back(pid);
1132 continue;
1133 }
1134
1135 const bool sensitive =
1136 best_abs_shift >= config.advanced.fit_parameter_sensitivity_abs_cutoff ||
1137 best_rel_shift >= config.advanced.fit_parameter_sensitivity_rel_cutoff;
1138
1139 if (verbose) {
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")
1144 << std::endl;
1145 }
1146
1147 if (!sensitive) {
1148 inactive.push_back(pid);
1149 }
1150 }
1151
1152 if (inactive.empty()) {
1153 return;
1154 }
1155
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) {
1160 if (i != 0) {
1161 oss << ", ";
1162 }
1163 oss << inactive[i];
1164 }
1165 oss << ". Add observables that depend on these parameters, declare the missing "
1166 "observable dependencies, or choose different fit parameters.";
1167
1168 LOG_WARN(oss.str());
1169 throw std::invalid_argument(oss.str());
1170}
1171
1173{
1174 if (!(config.print_cache_summary || config.print_debug)) {
1175 return;
1176 }
1177
1178 StatisticCachePrinter::print(cache, std::cout);
1179}
1180
1181void StatisticManager::update_cache(const std::vector<ParamId>& p_specs) {
1182 if (selected_experiments_.has_value()) {
1183 for (const auto& elem : *selected_experiments_) {
1184 LOG_VERBOSE("USING SELECTED EXPERIMENT: ", elem);
1185 }
1186 } else {
1187 LOG_VERBOSE("USING ALL EXPERIMENTS");
1188 }
1189
1190 if (selected_experiment_observables_.has_value()) {
1191 LOG_VERBOSE("USING EXPLICIT EXPERIMENT-OBSERVABLE SELECTION: ",
1192 selected_experiment_observables_->size(), " entries");
1193 }
1194
1195 for (const auto& [tp, block] : last_detached_fit_blocks_) {
1196 dp->reattach_block(tp, block);
1197 }
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);
1201 }
1202 }
1203 last_detached_fit_blocks_.clear();
1204 last_detached_fit_params_.clear();
1205
1206 cache.p_specs = this->get_p_specs(p_specs);
1207
1208 std::unordered_set<std::string> seen_blocks;
1209
1210 for (const auto& [pid, _] : cache.p_specs) {
1211 if (!pid.type.has_value()) {
1212 continue;
1213 }
1214
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();
1218
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);
1223 }
1224
1225 dp->detach_parameter(tp, pid.block, pid.code);
1226 last_detached_fit_params_.push_back(pid);
1227 }
1228
1229 cache.eta_specs_real = this->get_all_obss_deps();
1230 for (const auto& [pid, _] : cache.p_specs)
1231 cache.eta_specs_real.erase(pid);
1232
1233 for (auto it = cache.eta_specs_real.begin(); it != cache.eta_specs_real.end(); ) {
1234 const ParamId& pid = it->first;
1235
1236 if (pid.block.to_string().find("__BSM") != std::string::npos) {
1237 LOG_INFO("Dropping BSM nuisance from cache", pid);
1238 it = cache.eta_specs_real.erase(it);
1239 } else {
1240 ++it;
1241 }
1242 }
1243
1244 cache.SigmaEta = this->get_all_correlations();
1245 cache.exp_obs = this->get_obs_exp();
1246 validate_fit_parameter_sensitivity();
1247 cache.SigmaObs = this->get_all_obs_correlations();
1248
1249 std::ofstream fs;
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;
1257 else
1258 fs << corr * sigma_1 * sigma_2 << ',';
1259 }
1260 fs << '\n';
1261 }
1262
1263 fs.close();
1264}
1265
1267 default_nuisance_specs_ = nuisance_reader_->load_default();
1268
1269 if (current_user_nuisance_file_.has_value()) {
1270 user_nuisance_specs_ = nuisance_reader_->load_user(*current_user_nuisance_file_);
1271 } else {
1272 user_nuisance_specs_ = nuisance_reader_->load_user();
1273 }
1274
1275 rebuild_merged_nuisance_specs();
1276 invalidate_fit_state();
1277}
1278
1279void StatisticManager::set_nuisance_user_file(const fs::path& user_yaml_path) {
1280 current_user_nuisance_file_ = user_yaml_path;
1282}
1283
1285 current_user_nuisance_file_.reset();
1287}
1288
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;
1293 }
1294
1295}
1296
1297void StatisticManager::invalidate_fit_state() {
1298 cache.mle_result = FitResultWithMaps{};
1299
1300 last_ctx_.reset();
1301 last_like_.reset();
1302 last_fitter_.reset();
1303
1304 last_fit_param_ids_.clear();
1305 last_nuisance_ids_.clear();
1306 last_fit_param_index_.clear();
1307}
1308
1309const NuisanceSpec* StatisticManager::find_nuisance_spec(const ParamId& pid) const {
1310 if (const auto it = merged_nuisance_specs_.find(pid);
1311 it != merged_nuisance_specs_.end()) {
1312 return &it->second;
1313 }
1314
1315 const ParamId untyped_pid{pid.block, pid.code};
1316 if (const auto it = merged_nuisance_specs_.find(untyped_pid);
1317 it != merged_nuisance_specs_.end()) {
1318 return &it->second;
1319 }
1320
1321 return nullptr;
1322}
1323
1324MarginalType StatisticManager::resolve_nuisance_marginal_type(const ParamId& pid) const {
1326
1327 if (const auto* spec = find_nuisance_spec(pid)) {
1328 mt = spec->marginal;
1329 }
1330
1331 if (config.advanced.override_nuisance_marginals.contains(pid)) {
1332 mt = config.advanced.override_nuisance_marginals.at(pid);
1333 }
1334
1335 return mt;
1336}
1337
1338fit_app::ParameterDefinition StatisticManager::make_nuisance_parameter_definition(const ParamId& pid,
1339 double value,
1340 double sigma_hint) const
1341{
1343 out.name = param_name(pid);
1344 out.value = value;
1345
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));
1349
1350 out.step_hint = s;
1351
1352 if (const auto* spec = find_nuisance_spec(pid)) {
1353 out.limits = spec->bounds;
1354 return out;
1355 }
1356
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);
1367 }
1368
1369 return out;
1370}
1371
1372MarginalConfig StatisticManager::make_nuisance_marginal_config(const ParamId& pid,
1373 MarginalType mt) const
1374{
1375 if (const auto* spec = find_nuisance_spec(pid)) {
1376 return marginal_config_factory_.create(pid, mt, *spec);
1377 }
1378
1379 return marginal_config_factory_.create(pid, mt);
1380}
1381
1382std::map<ParamId, double> StatisticManager::get_all_obss_deps() {
1383
1384
1385 std::unordered_set<ParamId> eta_infos;
1386
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);
1390 }
1391 }
1392
1393 std::unordered_set<ParamId> eta_infos_leaf = this->sp->get_all_leaf_sources(eta_infos);
1394
1395 if (config.print_mc_config || config.print_debug) {
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);
1399 });
1400 print_nuisance_candidate_table(
1401 "Potential nuisance candidates before pruning",
1402 candidate_ids,
1403 pspp
1404 );
1405 }
1406
1407 std::map<ParamId, double> eta_specs_real_leaf;
1408 std::map<ParamId, double> delta_rel;
1409
1410 for (const auto& paramId : eta_infos_leaf) {
1411 if (cache.p_specs.contains(paramId)) {
1412 continue;
1413 }
1414
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();
1417
1418 if (!std::isfinite(u) || fpeq(u, 0.0)) {
1419 delta_rel[paramId] = 0.0;
1420 continue;
1421 }
1422
1423 if (fpeq(val, 0.0)) {
1424 eta_specs_real_leaf[paramId] = val;
1425 delta_rel[paramId] = std::numeric_limits<double>::infinity();
1426 continue;
1427 }
1428
1429 delta_rel[paramId] = std::abs(u / val);
1430 }
1431
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);
1436 }
1437 }
1438 if (!(delta_rel_max > 0.0)) {
1439 delta_rel_max = 1.0;
1440 }
1441
1442 for (const auto& [pid, d] : delta_rel) {
1443 const double rel_to_max = std::isfinite(d) ? (d / delta_rel_max) : 1.0;
1444 if (rel_to_max > config.advanced.nuisance_relevance_cutoff || !std::isfinite(d)) {
1445 eta_specs_real_leaf[pid] = pspp->get_param(pid)->get_val();
1446 }
1447 }
1448
1449 if (config.print_mc_config || config.print_debug) {
1450 print_nuisance_value_table(
1451 "Nuisances after relative-uncertainty preselection",
1452 eta_specs_real_leaf,
1453 pspp
1454 );
1455 }
1456
1459 !eta_specs_real_leaf.empty() &&
1460 !cache.p_specs.empty())
1461 {
1462 const auto exp_obs_map = get_obs_exp();
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;
1466
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;
1469
1470 struct RestoreModelStateGuard {
1471 StatisticManager* self = nullptr;
1472 std::map<ParamId, double> p;
1473 std::map<ParamId, double> eta;
1474 bool active = true;
1475
1476 ~RestoreModelStateGuard() {
1477 if (!active || self == nullptr) return;
1478
1479 try {
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;
1484 } catch (...) {
1485 std::cout << "[FIT] WARNING: failed to restore central model state: unknown exception"
1486 << std::endl;
1487 }
1488 }
1489 };
1490
1491 RestoreModelStateGuard restore_guard{
1492 this,
1493 p_central_for_restore,
1494 eta_central_for_restore,
1495 true
1496 };
1497
1498 const auto contexts = make_sensitivity_contexts(
1499 eta_specs_real_leaf,
1500 pspp,
1504 );
1505
1506 std::map<ParamId, double> screened_eta_specs;
1507 const bool sensitivity_verbose = config.print_fit_summary || config.print_debug;
1508
1509 if (sensitivity_verbose) {
1510 std::cout << "[FIT] Model-sensitivity pruning on "
1511 << eta_specs_real_leaf.size()
1512 << " nuisance candidates using "
1513 << contexts.size()
1514 << " contexts" << std::endl;
1515 }
1516
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());
1520
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;
1526 }
1527 continue;
1528 }
1529
1531 make_nuisance_parameter_definition(pid, nominal, sigma);
1532
1533 const double step = config.advanced.nuisance_sensitivity_probe_sigmas * sigma;
1534
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;
1540 }
1541 continue;
1542 }
1543
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;
1548
1549 for (std::size_t c = 0; c < contexts.size(); ++c) {
1550 auto base_map = contexts[c];
1551
1552 double center = base_map.at(pid);
1553 double eta_plus = center + step;
1554 double eta_minus = center - step;
1555
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);
1560 }
1561
1562 if (!std::isfinite(eta_plus) ||
1563 !std::isfinite(eta_minus) ||
1564 std::abs(eta_plus - eta_minus) < 1e-14) {
1565 continue;
1566 }
1567
1568 try {
1569 const auto baseline_pred_map =
1570 this->obs_int->predict_optimized(cache.p_specs, base_map);
1571
1572 const std::vector<double> baseline_pred =
1573 ordered_prediction_vector(obs_ids, baseline_pred_map);
1574
1575 auto eta_plus_map = base_map;
1576 auto eta_minus_map = base_map;
1577
1578 eta_plus_map[pid] = eta_plus;
1579 eta_minus_map[pid] = eta_minus;
1580
1581 const auto pred_plus_map =
1582 this->obs_int->predict_optimized(cache.p_specs, eta_plus_map);
1583
1584 const auto pred_minus_map =
1585 this->obs_int->predict_optimized(cache.p_specs, eta_minus_map);
1586
1587 const std::vector<double> pred_plus =
1588 ordered_prediction_vector(obs_ids, pred_plus_map);
1589
1590 const std::vector<double> pred_minus =
1591 ordered_prediction_vector(obs_ids, pred_minus_map);
1592
1593 double max_abs_shift = 0.0;
1594 double max_rel_shift = 0.0;
1595
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]);
1599
1600 const double scale = std::max({
1601 std::abs(baseline_pred[i]),
1602 std::abs(exp_obs_vals[i]),
1604 });
1605
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);
1608 }
1609
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);
1614 best_context = c;
1615 }
1616
1617 } catch (const std::exception& e) {
1618 evaluation_failed = true;
1619
1620 if (sensitivity_verbose) {
1621 std::cout << "[FIT] sensitivity " << pid
1622 << " : context " << c
1623 << " failed with exception: "
1624 << e.what() << std::endl;
1625 }
1626
1627 break;
1628 } catch (...) {
1629 evaluation_failed = true;
1630
1631 if (sensitivity_verbose) {
1632 std::cout << "[FIT] sensitivity " << pid
1633 << " : context " << c
1634 << " failed with unknown exception"
1635 << std::endl;
1636 }
1637
1638 break;
1639 }
1640 }
1641
1642 if (evaluation_failed && config.advanced.nuisance_sensitivity_keep_on_failure) {
1643 screened_eta_specs[pid] = nominal;
1644
1645 if (sensitivity_verbose) {
1646 std::cout << "[FIT] sensitivity " << pid
1647 << " : evaluation failed -> keep" << std::endl;
1648 }
1649
1650 continue;
1651 }
1652
1653 const bool keep =
1654 (best_abs_shift >= config.advanced.nuisance_sensitivity_abs_cutoff) ||
1655 (best_rel_shift >= config.advanced.nuisance_sensitivity_rel_cutoff);
1656
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")
1663 << std::endl;
1664 }
1665
1666 if (keep) {
1667 screened_eta_specs[pid] = nominal;
1668 }
1669 }
1670
1671 eta_specs_real_leaf = std::move(screened_eta_specs);
1672 }
1673
1674 if (config.print_mc_config || config.print_debug) {
1675 print_nuisance_value_table(
1676 "Final retained nuisances passed to MC/fit",
1677 eta_specs_real_leaf,
1678 pspp
1679 );
1680 }
1681
1682 if (config.print_fit_summary || config.print_debug) {
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";
1687 }
1688 }
1689
1690 return eta_specs_real_leaf;
1691}
1692
1693std::map<ParamId, double> StatisticManager::get_p_specs(const std::vector<ParamId>& p_specs) {
1694 std::map<ParamId, double> out;
1695 for (auto elem : p_specs) {
1696 out[elem] = pspp->get_param(elem)->get_val();
1697 }
1698 return out;
1699}
1700std::map<ParamId, std::map<ParamId, double>> StatisticManager::get_all_correlations() {
1701 std::map<ParamId, std::map<ParamId, double>> res;
1703 res = ct.transform(this->cache.eta_specs_real);
1704 return res;
1705}
1706
1707std::map<ExperimentObs, std::map<ExperimentObs, double>> StatisticManager::get_all_obs_correlations() {
1708 std::map<ExperimentObs, std::map<ExperimentObs, double>> res;
1710
1711 res = ct.transform(this->cache.exp_obs);
1712 return res;
1713}
1714
1715std::map<ExperimentObs, double> StatisticManager::get_obs_exp() {
1716 std::map<ExperimentObs, double> out;
1717
1718 for (const auto& obsId : obs_int->get_obs_ids()) {
1719 auto exp_params = pspp->get_obs_param(obsId);
1720
1721 for (const auto& [exp_obs, param] : exp_params) {
1722 if (!accepts_experiment_observable(exp_obs)) {
1723 continue;
1724 }
1725
1726 out[exp_obs] = param->get_val();
1727 }
1728 }
1729
1730 return out;
1731}
1732
1733void StatisticManager::prepare_likelihood_for_scan(const std::vector<ParamId>& p_specs) {
1734 update_cache(p_specs);
1735
1736 if (cache.p_specs.empty()) {
1737 throw std::invalid_argument("prepare_likelihood_for_scan called with an empty fit parameter list.");
1738 }
1739
1740 auto unzipped_fit_params = unzip(cache.p_specs);
1741 auto unzipped_nuisances = unzip(cache.eta_specs_real);
1742 auto unzipped_exp_obs = unzip(cache.exp_obs);
1743
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
1747 );
1748
1749 const std::vector<ParamId> eta_ids = unzipped_nuisances.ids;
1750 const std::vector<double> eta0 = unzipped_nuisances.vals;
1751
1752 const std::vector<ExperimentObs> obs_ids = unzipped_exp_obs.ids;
1753 const std::vector<double> exp_obs_vals = unzipped_exp_obs.vals;
1754
1755 auto ctx = std::make_shared<LikelihoodContext>();
1756 ctx->nuisance_dist = build_nuisance_distribution();
1757 ctx->exp_obs_dist = build_exp_data_distribution();
1758 ctx->exp_obs_values = exp_obs_vals;
1759
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));
1764 }
1765
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)
1771 );
1772 }
1773
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>
1777 {
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)
1782 );
1783
1784 return ordered_prediction_vector(obs_ids, pred_map);
1785 };
1786
1787 last_ctx_ = ctx;
1788 last_like_ = std::make_shared<BaseLikelihood>(model_fn, ctx, p_ids.size());
1789 last_fitter_ = std::make_shared<MLFitter>(ctx, model_fn);
1790
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;
1796 }
1797
1798 last_scan_p_ = p0;
1799 last_scan_eta_ = eta0;
1800 has_manual_scan_point_ = false;
1801
1802 if (config.print_scan_summary || config.print_debug) {
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";
1806 }
1807}
1808
1809void StatisticManager::set_manual_scan_point(const std::map<ParamId, double>& p_hat,
1810 const std::map<ParamId, double>& eta_hat) {
1811 if (!last_like_) {
1812 throw std::runtime_error(
1813 "Please call prepare_likelihood_for_scan(...) or compute_MLE(...) before set_manual_scan_point(...)."
1814 );
1815 }
1816
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.");
1822 }
1823 last_scan_p_[i] = it->second;
1824 }
1825
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.");
1831 }
1832 last_scan_eta_[i] = it->second;
1833 }
1834
1835 has_manual_scan_point_ = true;
1836 if (config.print_scan_summary || config.print_debug) {
1837 std::cout << "[SCAN] Manual scan point loaded.\n";
1838 }
1839}
1840
1842 ParamId p1,
1843 ParamId p2,
1844 double x_half_width,
1845 double y_half_width,
1846 std::size_t nx,
1847 std::size_t ny
1848) const {
1849 if (!last_like_) {
1850 throw std::runtime_error(
1851 "Please call prepare_likelihood_for_scan(...) or compute_MLE(...) before requesting a likelihood scan."
1852 );
1853 }
1854
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."
1858 );
1859 }
1860
1861 if (p1 == p2) {
1862 throw std::invalid_argument("Likelihood scan requires two distinct parameters.");
1863 }
1864
1865 const std::size_t ix = last_fit_param_index_.at(p1);
1866 const std::size_t iy = last_fit_param_index_.at(p2);
1867
1868 std::vector<double> p_ref;
1869 std::vector<double> eta_ref;
1870
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_;
1880 } else {
1881 throw std::runtime_error(
1882 "No reference point available. Use compute_MLE(...), "
1883 "set_manual_scan_point(...), or prepare_likelihood_for_scan(...)."
1884 );
1885 }
1886
1887 if (ix >= p_ref.size() || iy >= p_ref.size()) {
1888 throw std::runtime_error("Internal error: parameter index out of range.");
1889 }
1890
1891 std::vector<double> theta0 = p_ref;
1892 theta0.insert(theta0.end(), eta_ref.begin(), eta_ref.end());
1893
1894 const double nll0 = last_like_->nll(theta0);
1895
1897 out.x_param = p1;
1898 out.y_param = p2;
1899 out.x_center = p_ref[ix];
1900 out.y_center = p_ref[iy];
1901 out.nx = nx;
1902 out.ny = ny;
1903 out.points.reserve(nx * ny);
1904
1905 double nll_min = std::numeric_limits<double>::infinity();
1906
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;
1911
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);
1914
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);
1917
1918 std::vector<double> theta = theta0;
1919 theta[ix] = x;
1920 theta[iy] = y;
1921
1922 const double nll = last_like_->nll(theta);
1923
1925 pt.x = x;
1926 pt.y = y;
1927 pt.nll = nll;
1928
1929 nll_min = std::min(nll_min, nll);
1930 out.points.push_back(pt);
1931 }
1932 }
1933
1934 for (auto& pt : out.points) {
1935 pt.delta_nll = pt.nll - nll_min;
1936 }
1937
1938 if (config.print_scan_summary || config.print_debug) {
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";
1944 }
1945
1946 return out;
1947}
1948
1950 const LikelihoodScanGrid& grid) const {
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";
1959
1960 out << std::setprecision(17);
1961 for (const auto& pt : grid.points) {
1962 out << pt.x << ","
1963 << pt.y << ","
1964 << pt.nll << ","
1965 << pt.delta_nll << "\n";
1966 }
1967}
1968
1969void StatisticManager::select_experiment(const std::string& experiment) {
1970 select_experiments(std::set<std::string>{experiment});
1971}
1972
1973void StatisticManager::select_experiments(const std::vector<std::string>& experiments) {
1974 select_experiments(std::set<std::string>(
1975 experiments.begin(),
1976 experiments.end()
1977 ));
1978}
1979
1980void StatisticManager::select_experiments(const std::set<std::string>& experiments) {
1981 if (experiments.empty()) {
1982 throw std::invalid_argument(
1983 "StatisticManager::select_experiments: empty experiment set."
1984 );
1985 }
1986
1987 selected_experiments_ = experiments;
1988 invalidate_fit_state();
1989}
1990
1992 selected_experiments_.reset();
1993 invalidate_fit_state();
1994}
1995
1997 return selected_experiments_.has_value();
1998}
1999
2000std::set<std::string> StatisticManager::selected_experiments() const {
2001 if (!selected_experiments_) {
2002 return {};
2003 }
2004
2005 return *selected_experiments_;
2006}
2007
2008bool StatisticManager::accepts_experiment_observable(const ExperimentObs& exp_obs) const {
2009 if (selected_experiments_.has_value()
2010 && !selected_experiments_->contains(exp_obs.experiment)) {
2011 return false;
2012 }
2013
2014 if (selected_experiment_observables_.has_value()
2015 && !selected_experiment_observables_->contains(exp_obs)) {
2016 return false;
2017 }
2018
2019 return true;
2020}
2021
2022void StatisticManager::select_experiment_observables(const std::vector<ExperimentObs>& observables) {
2023 select_experiment_observables(std::set<ExperimentObs>(
2024 observables.begin(),
2025 observables.end()
2026 ));
2027}
2028
2029void StatisticManager::select_experiment_observables(const std::set<ExperimentObs>& observables) {
2030 if (observables.empty()) {
2031 throw std::invalid_argument(
2032 "StatisticManager::select_experiment_observables: empty observable set."
2033 );
2034 }
2035
2036 selected_experiment_observables_ = observables;
2037 invalidate_fit_state();
2038}
2039
2041 selected_experiment_observables_.reset();
2042 invalidate_fit_state();
2043}
2044
2046 return selected_experiment_observables_.has_value();
2047}
2048
2050 if (!selected_experiment_observables_) {
2051 return {};
2052 }
2053
2054 return *selected_experiment_observables_;
2055}
CopulaType
Identifies the copula family used to model dependence.
Definition CopulaType.h:32
@ 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.
Definition Indexing.h:61
UnzipResult1D< T, U > unzip(const std::map< T, U > &indexed)
Splits a map into parallel id and value vectors.
Definition Indexing.h:176
#define LOG_INFO(...)
Macro for logging informational messages.
Definition Logger.h:39
#define LOG_VERBOSE(...)
Macro for logging verbose messages.
Definition Logger.h:47
#define LOG_WARN(...)
Macro for logging warning messages.
Definition Logger.h:40
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.
Definition MCEngine.cpp:102
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.
Definition BlockName.cpp:41
static std::unique_ptr< ICopula > create(CopulaType name, CopulaConfig config, unsigned int seed=std::random_device{}())
Creates a concrete copula instance.
Builds correlation tables/matrices from parameter or observable collections.
std::vector< std::vector< double > > transform(const std::vector< ParamId > &ids)
Builds a dense correlation matrix for a list of parameters.
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.
Definition MCEngine.h:171
MCRealization sample_predictions(const std::map< ParamId, double > &p) const
Generates accepted model predictions for a fixed fit-parameter point.
Definition MCEngine.cpp:287
MCResult summarize(const std::map< ParamId, double > &p) const
Runs Monte Carlo propagation and computes summary statistics.
Definition MCEngine.cpp:504
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
bool is_symmetric() const
Checks whether the matrix is symmetric.
Definition Matrix.cpp:684
std::size_t cols() const
Returns the number of columns.
Definition Matrix.cpp:605
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)
csl::Expr v
Definition sm.h:110
Hash specialization for SymbolId<Tag>.
Definition BlockName.h:353
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.
Definition Fit.h:93
Output of a contour extraction algorithm.
Container for an eigendecomposition.
Definition Matrix.h:489
RealMatrix D
Definition Matrix.h:490
RealMatrix P
Diagonal matrix of eigenvalues.
Definition Matrix.h:491
std::string experiment
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.
Definition MCEngine.h:33
bool force_decay_threads_to_one
Definition MCEngine.h:56
bool write_samples_csv
Definition MCEngine.h:74
std::size_t progress_probe_draws
Definition MCEngine.h:65
std::size_t n_threads
Definition MCEngine.h:53
bool print_progress
Definition MCEngine.h:62
double skew_abs_threshold
Definition MCEngine.h:38
std::size_t draws
Definition MCEngine.h:35
std::size_t forced_decay_threads
Definition MCEngine.h:59
double covariance_ridge_rel
Definition MCEngine.h:41
std::size_t progress_update_every
Definition MCEngine.h:68
std::string samples_csv_path
Definition MCEngine.h:77
std::shared_ptr< StatisticProgressMonitor > progress_monitor
Definition MCEngine.h:71
double covariance_ridge_abs
Definition MCEngine.h:44
Empirical observable covariance and its inverse.
Definition MCEngine.h:96
RealMatrix covariance
Definition MCEngine.h:104
Raw Monte Carlo samples accepted by the engine.
Definition MCEngine.h:84
ObsSamples sampled_obss
Definition MCEngine.h:86
Runtime options controlling the global maximum-likelihood fit.
Definition Fit.h:35
unsigned max_fcn
Definition Fit.h:49
bool request_minos
Definition Fit.h:40
unsigned strategy
Definition Fit.h:46
double profile_hessian_eig_floor_rel
Definition Fit.h:64
bool trace_first_evals
Definition Fit.h:67
double profile_hessian_step_scale
Definition Fit.h:61
bool run_hesse
Definition Fit.h:37
std::size_t trace_max_evals
Definition Fit.h:70
bool allow_profile_hessian_fallback
Definition Fit.h:58
double tolerance
Definition Fit.h:52
bool verbose
Definition Fit.h:43
Specification of one nuisance parameter.
Composite identifier for a single parameter.
Definition ParamID.h:57
std::optional< ParameterType > type
Optional high-level parameter category.
Definition ParamID.h:65
BlockName block
Name of the block where the parameter is stored.
Definition ParamID.h:73
LhaID code
Index or multi-index of the parameter inside the block.
Definition ParamID.h:82
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