Hyperiso 1.0.3
Modular flavour-physics calculations, Wilson coefficients and statistical inference
Loading...
Searching...
No Matches
MCEngine.cpp
Go to the documentation of this file.
1#include "MCEngine.h"
2#include "StatisticProgress.h"
4
5#include <algorithm>
6#include <atomic>
7#include <exception>
8#include <fstream>
9#include <iomanip>
10#include <limits>
11#include <mutex>
12#include <sstream>
13#include <thread>
14
16 if (cov.rows() != cov.cols()) {
17 throw std::runtime_error("symmetrize_covariance_matrix: covariance must be square");
18 }
19
20 for (std::size_t i = 0; i < cov.rows(); ++i) {
21 for (std::size_t j = i + 1; j < cov.cols(); ++j) {
22 const double a = cov.at(i, j);
23 const double b = cov.at(j, i);
24
25 if (!std::isfinite(a) || !std::isfinite(b)) {
26 throw std::runtime_error("symmetrize_covariance_matrix: non-finite covariance entry");
27 }
28
29 const double v = 0.5 * (a + b);
30 cov.at(i, j) = v;
31 cov.at(j, i) = v;
32 }
33
34 if (!std::isfinite(cov.at(i, i))) {
35 throw std::runtime_error("symmetrize_covariance_matrix: non-finite covariance diagonal");
36 }
37 }
38
39 return cov;
40}
41
42
44 RealMatrix cov,
45 double ridge_rel,
46 double ridge_abs
47) {
48 cov = symmetrize_covariance_matrix2(std::move(cov));
49
50 const std::size_t n = cov.rows();
51 if (n != cov.cols()) {
52 throw std::runtime_error("inverse_covariance_with_ridge: covariance must be square");
53 }
54
55 std::vector<double> sigma(n);
56
57 for (std::size_t i = 0; i < n; ++i) {
58 const double vii = cov.at(i, i);
59
60 if (!std::isfinite(vii) || vii <= 0.0) {
61 std::ostringstream oss;
62 oss << "inverse_covariance_with_ridge: non-positive variance at i="
63 << i << ", variance=" << vii;
64 throw std::runtime_error(oss.str());
65 }
66
67 sigma[i] = std::sqrt(vii);
68 }
69
70 // Build dimensionless correlation matrix.
71 RealMatrix corr(n, n);
72 for (std::size_t i = 0; i < n; ++i) {
73 for (std::size_t j = 0; j < n; ++j) {
74 corr.at(i, j) = cov.at(i, j) / (sigma[i] * sigma[j]);
75 }
76 }
77
78 corr = symmetrize_covariance_matrix2(std::move(corr));
79
80 // Ridge in correlation space, dimensionless.
81 const double ridge = std::max(ridge_rel, ridge_abs);
82
83 for (std::size_t i = 0; i < n; ++i) {
84 corr.at(i, i) += ridge;
85 }
86
87 corr = symmetrize_covariance_matrix2(std::move(corr));
88
89 RealMatrix corr_inv = corr.inv();
90
91 // Convert back: Cov^{-1} = D^{-1} Corr^{-1} D^{-1}
92 RealMatrix cov_inv(n, n);
93 for (std::size_t i = 0; i < n; ++i) {
94 for (std::size_t j = 0; j < n; ++j) {
95 cov_inv.at(i, j) = corr_inv.at(i, j) / (sigma[i] * sigma[j]);
96 }
97 }
98
99 return symmetrize_covariance_matrix2(std::move(cov_inv));
100}
101
103 const ObsSamples& S,
104 const std::vector<BinnedObservableId>& ids,
105 double ridge_rel,
106 double ridge_abs
107) {
108 if (S.empty()) {
109 throw std::invalid_argument("covariance_from_obs_samples: no samples");
110 }
111 if (ids.empty()) {
112 throw std::invalid_argument("covariance_from_obs_samples: no observable ids");
113 }
114 if (S.size() < 2) {
115 throw std::invalid_argument("covariance_from_obs_samples: need at least two samples");
116 }
117
118 const std::size_t N = S.size();
119 const std::size_t D = ids.size();
120
121 std::vector<double> mean(D, 0.0);
122 for (const auto& row : S) {
123 for (std::size_t d = 0; d < D; ++d) {
124 mean[d] += row.at(ids[d]);
125 }
126 }
127 for (double& v : mean) {
128 v /= static_cast<double>(N);
129 }
130
131 RealMatrix cov(D, D);
132 for (std::size_t i = 0; i < D; ++i) {
133 for (std::size_t j = 0; j < D; ++j) {
134 double s = 0.0;
135 for (const auto& row : S) {
136 s += (row.at(ids[i]) - mean[i]) * (row.at(ids[j]) - mean[j]);
137 }
138 cov.at(i, j) = s / static_cast<double>(N - 1);
139 }
140 }
141
142 // Force symmetry.
143 for (std::size_t i = 0; i < D; ++i) {
144 for (std::size_t j = i + 1; j < D; ++j) {
145 const double v = 0.5 * (cov.at(i, j) + cov.at(j, i));
146 cov.at(i, j) = v;
147 cov.at(j, i) = v;
148 }
149 }
150
152 out.ids = ids;
153 out.mean = mean;
154
155 out.covariance = cov;
157 cov,
158 ridge_rel,
159 ridge_abs
160 );
161 return out;
162}
163
164std::vector<BinnedObservableId> covariance_ids_from_first_sample(
165 const ObsSamples& S
166) {
167 if (S.empty()) {
168 throw std::invalid_argument("covariance_ids_from_first_sample: no samples");
169 }
170
171 std::vector<BinnedObservableId> ids;
172 ids.reserve(S.front().size());
173 for (const auto& [id, _] : S.front()) {
174 ids.push_back(id);
175 }
176 return ids;
177}
178
179MCRealization MonteCarloEngine::sample_predictions_serial(const std::map<ParamId, double>& p) const {
180 ObsSamples out;
181 out.reserve(cfg_.draws);
182
183 NuisanceSamples accepted_samples;
184 accepted_samples.reserve(cfg_.draws);
185
186 std::size_t accepted = 0;
187 std::size_t failures = 0;
188 std::size_t attempts = 0;
190 cfg_.print_progress,
191 cfg_.draws,
194 std::cout,
195 "Monte-Carlo",
196 "accepted",
197 cfg_.progress_monitor,
198 "monte_carlo"
199 );
200
201 model_->prepare_for_prediction();
202
203 while (accepted < cfg_.draws) {
204 ++attempts;
205
206 std::map<ParamId, double> s = sampler_.sample();
207
208 try {
209 auto res = model_->predict_optimized(p, s);
210 auto unzipped_res = flatten(res);
211 std::map<BinnedObservableId, double> value =
212 zip(unzipped_res.ids, unzipped_res.vals);
213
214 bool finite = true;
215 for (const auto& [oid, v] : value) {
216 if (!std::isfinite(v)) {
217 finite = false;
218 break;
219 }
220 }
221
222 if (!finite) {
223 throw std::runtime_error("MC prediction contains non-finite observable");
224 }
225
226 out.emplace_back(std::move(value));
227 accepted_samples.emplace_back(std::move(s));
228 ++accepted;
229 progress.accepted(accepted, attempts, failures);
230
231 } catch (const std::exception& e) {
232 ++failures;
233
234 LOG_WARN(
235 "Rejected MC nuisance sample",
236 failures,
237 "while trying to fill accepted sample",
238 accepted + 1,
239 "of",
240 cfg_.draws,
241 ":",
242 e.what()
243 );
244
245 if (!cfg_.retry_failed_predictions ||
246 failures > cfg_.max_prediction_failures) {
247 throw;
248 }
249
250 continue;
251 } catch (...) {
252 ++failures;
253
254 LOG_WARN(
255 "Rejected MC nuisance sample",
256 failures,
257 "with unknown exception while trying to fill accepted sample",
258 accepted + 1,
259 "of",
260 cfg_.draws
261 );
262
263 if (!cfg_.retry_failed_predictions ||
264 failures > cfg_.max_prediction_failures) {
265 throw;
266 }
267
268 continue;
269 }
270 }
271
272 progress.finish(accepted, attempts, failures);
273
274 if (failures > 0) {
275 LOG_WARN(
276 "MC sampling finished with",
277 failures,
278 "rejected nuisance samples over",
279 attempts,
280 "attempts."
281 );
282 }
283
284 return MCRealization{out, accepted_samples};
285}
286
287MCRealization MonteCarloEngine::sample_predictions(const std::map<ParamId, double>& p) const {
288 if (cfg_.n_threads <= 1 || cfg_.draws <= 1) {
290 }
291
292 if (!model_->can_clone_for_worker()) {
293 LOG_WARN(
294 "Parallel MC requested with",
295 cfg_.n_threads,
296 "threads, but the model cannot be cloned for workers. Falling back to serial MC."
297 );
299 }
300
302}
303
304MCRealization MonteCarloEngine::sample_predictions_parallel(const std::map<ParamId, double>& p) const {
305 const std::size_t n_workers = std::max<std::size_t>(1, std::min(cfg_.n_threads, cfg_.draws));
306
307 std::unique_ptr<IModelThreadGuard> decay_thread_guard;
309 decay_thread_guard = model_->force_decay_threads(cfg_.forced_decay_threads);
310 }
311
312 struct WorkerOutput {
313 ObsSamples obss;
314 NuisanceSamples params;
315 std::size_t failures = 0;
316 std::size_t attempts = 0;
317 };
318
319 std::vector<WorkerOutput> worker_outputs(n_workers);
320 std::vector<std::thread> workers;
321 workers.reserve(n_workers);
322
323 std::mutex sampler_mutex;
324 std::mutex progress_mutex;
325 std::mutex exception_mutex;
326 std::exception_ptr first_exception = nullptr;
327
328 std::atomic<std::size_t> accepted_total {0};
329 std::atomic<std::size_t> failures_total {0};
330 std::atomic<std::size_t> attempts_total {0};
331 std::atomic<bool> stop {false};
332
334 cfg_.print_progress,
335 cfg_.draws,
338 std::cout,
339 "Monte-Carlo",
340 "accepted",
341 cfg_.progress_monitor,
342 "monte_carlo"
343 );
344
345 auto set_exception_once = [&](std::exception_ptr eptr) {
346 std::lock_guard<std::mutex> lock(exception_mutex);
347 if (!first_exception) {
348 first_exception = eptr;
349 }
350 stop.store(true, std::memory_order_release);
351 };
352
353 const std::size_t base_target = cfg_.draws / n_workers;
354 const std::size_t remainder = cfg_.draws % n_workers;
355
356 for (std::size_t worker_id = 0; worker_id < n_workers; ++worker_id) {
357 const std::size_t target = base_target + (worker_id < remainder ? 1 : 0);
358
359 workers.emplace_back([&, worker_id, target]() {
360 auto& local = worker_outputs[worker_id];
361 local.obss.reserve(target);
362 local.params.reserve(target);
363
364 try {
365 ParameterRuntimeContext runtime_context;
366 ScopedParameterRuntimeContext runtime_guard(runtime_context);
367
368 auto worker_model = model_->clone_for_worker();
369 if (!worker_model) {
370 throw std::runtime_error("MC worker could not clone model");
371 }
372
373 std::unique_ptr<IModelThreadGuard> worker_decay_thread_guard;
375 worker_decay_thread_guard = worker_model->force_decay_threads(cfg_.forced_decay_threads);
376 }
377
378 worker_model->prepare_for_prediction();
379
380 while (local.obss.size() < target && !stop.load(std::memory_order_acquire)) {
381 ++local.attempts;
382 ++attempts_total;
383
384 std::map<ParamId, double> s;
385 {
386 std::lock_guard<std::mutex> lock(sampler_mutex);
387 s = sampler_.sample();
388 }
389
390 try {
391 auto res = worker_model->predict_optimized(p, s);
392 auto unzipped_res = flatten(res);
393 std::map<BinnedObservableId, double> value =
394 zip(unzipped_res.ids, unzipped_res.vals);
395
396 bool finite = true;
397 for (const auto& [oid, v] : value) {
398 if (!std::isfinite(v)) {
399 finite = false;
400 break;
401 }
402 }
403
404 if (!finite) {
405 throw std::runtime_error("MC prediction contains non-finite observable");
406 }
407
408 local.obss.emplace_back(std::move(value));
409 local.params.emplace_back(std::move(s));
410
411 const std::size_t accepted_now =
412 accepted_total.fetch_add(1, std::memory_order_acq_rel) + 1;
413 {
414 std::lock_guard<std::mutex> lock(progress_mutex);
415 progress.accepted(accepted_now, attempts_total.load(), failures_total.load());
416 }
417 } catch (const std::exception& e) {
418 ++local.failures;
419 const std::size_t failures_now =
420 failures_total.fetch_add(1, std::memory_order_acq_rel) + 1;
421
422 LOG_WARN(
423 "Rejected MC nuisance sample",
424 failures_now,
425 "in worker",
426 worker_id,
427 "while trying to fill local accepted sample",
428 local.obss.size() + 1,
429 "of",
430 target,
431 ":",
432 e.what()
433 );
434
435 if (!cfg_.retry_failed_predictions ||
436 failures_now > cfg_.max_prediction_failures) {
437 set_exception_once(std::current_exception());
438 break;
439 }
440 } catch (...) {
441 ++local.failures;
442 const std::size_t failures_now =
443 failures_total.fetch_add(1, std::memory_order_acq_rel) + 1;
444
445 LOG_WARN(
446 "Rejected MC nuisance sample",
447 failures_now,
448 "with unknown exception in worker",
449 worker_id
450 );
451
452 if (!cfg_.retry_failed_predictions ||
453 failures_now > cfg_.max_prediction_failures) {
454 set_exception_once(std::current_exception());
455 break;
456 }
457 }
458 }
459 } catch (...) {
460 set_exception_once(std::current_exception());
461 }
462 });
463 }
464
465 for (auto& worker : workers) {
466 if (worker.joinable()) {
467 worker.join();
468 }
469 }
470
471 if (first_exception) {
472 std::rethrow_exception(first_exception);
473 }
474
475 ObsSamples out;
476 out.reserve(cfg_.draws);
477 NuisanceSamples accepted_samples;
478 accepted_samples.reserve(cfg_.draws);
479
480 for (auto& local : worker_outputs) {
481 for (auto& row : local.obss) {
482 out.emplace_back(std::move(row));
483 }
484 for (auto& row : local.params) {
485 accepted_samples.emplace_back(std::move(row));
486 }
487 }
488
489 progress.finish(accepted_total.load(), attempts_total.load(), failures_total.load());
490
491 if (failures_total.load() > 0) {
492 LOG_WARN(
493 "MC sampling finished with",
494 failures_total.load(),
495 "rejected nuisance samples over",
496 attempts_total.load(),
497 "attempts."
498 );
499 }
500
501 return MCRealization{out, accepted_samples};
502}
503
504MCResult MonteCarloEngine::summarize(const std::map<ParamId, double>& p) const {
505 auto smpl = sample_predictions(p);
506
507 if (cfg_.write_samples_csv && !smpl.sampled_obss.empty()) {
508 std::ofstream fs(cfg_.samples_csv_path, std::ios::trunc);
509 if (!fs) {
510 throw std::runtime_error(
511 "Cannot open Monte-Carlo samples CSV: " + cfg_.samples_csv_path
512 );
513 }
514
515 const auto write_csv_field = [&fs](const std::string& field) {
516 fs << '"';
517 for (const char c : field) {
518 if (c == '"') fs << '"';
519 fs << c;
520 }
521 fs << '"';
522 };
523
524 bool first = true;
525 for (const auto& [oid, value] : smpl.sampled_obss.front()) {
526 static_cast<void>(value);
527 if (!first) fs << ',';
528 write_csv_field(oid.str());
529 first = false;
530 }
531 fs << '\n';
532
533 fs << std::setprecision(std::numeric_limits<double>::max_digits10);
534 for (const auto& observable_values : smpl.sampled_obss) {
535 first = true;
536 for (const auto& [oid, value] : observable_values) {
537 static_cast<void>(oid);
538 if (!first) fs << ',';
539 fs << value;
540 first = false;
541 }
542 fs << '\n';
543 }
544 if (!fs) {
545 throw std::runtime_error(
546 "Failed while writing Monte-Carlo samples CSV: " + cfg_.samples_csv_path
547 );
548 }
549 }
550
551 auto summary = gaussian_fit(smpl.sampled_obss, cfg_.skew_abs_threshold);
552
553 const auto covariance_ids = covariance_ids_from_first_sample(smpl.sampled_obss);
554 auto covariance = covariance_from_obs_samples(
555 smpl.sampled_obss,
556 covariance_ids,
559 );
560
561 return MCResult {smpl, summary, covariance};
562}
std::vector< GaussianSummary > gaussian_fit(const ObsSamples &S, double skew_abs_threshold=0.2)
Builds Gaussian or split-Gaussian summaries for observable samples.
UnzipResult1D< BinnedObservableId, double > flatten(std::map< ObservableId, std::vector< ObservableValue > > indexed)
Flattens observable values into binned observable ids and values.
Definition Indexing.h:118
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
#define LOG_WARN(...)
Macro for logging warning messages.
Definition Logger.h:40
std::vector< BinnedObservableId > covariance_ids_from_first_sample(const ObsSamples &S)
Extracts the observable ordering from the first Monte Carlo sample.
Definition MCEngine.cpp:164
RealMatrix inverse_covariance_with_ridge2(RealMatrix cov, double ridge_rel, double ridge_abs)
Definition MCEngine.cpp:43
RealMatrix symmetrize_covariance_matrix2(RealMatrix cov)
Definition MCEngine.cpp:15
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
Monte Carlo propagation of nuisance-parameter uncertainties.
std::vector< BinnedObservableId > covariance_ids_from_first_sample(const ObsSamples &S)
Extracts the observable ordering from the first Monte Carlo sample.
Definition MCEngine.cpp:164
MCObservableCovariance covariance_from_obs_samples(const ObsSamples &S, const std::vector< BinnedObservableId > &ids, double ridge_rel=1e-8, double ridge_abs=1e-12)
Builds a regularized empirical covariance matrix from observable samples.
Definition MCEngine.cpp:102
std::vector< std::map< BinnedObservableId, double > > ObsSamples
Definition Statistics.h:25
std::vector< std::map< ParamId, double > > NuisanceSamples
Definition Statistics.h:28
virtual std::map< ParamId, double > sample() const =0
Draws one nuisance-parameter sample.
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
MCRealization sample_predictions_parallel(const std::map< ParamId, double > &p) const
Definition MCEngine.cpp:304
MCRealization sample_predictions_serial(const std::map< ParamId, double > &p) const
Definition MCEngine.cpp:179
Per-thread parameter runtime used to isolate Monte-Carlo workers.
std::size_t rows() const
Returns the number of rows.
Definition Matrix.cpp:601
double & at(size_t i, size_t j)
Returns a mutable reference to element (i,j) with bounds checking.
Definition Matrix.cpp:587
std::size_t cols() const
Returns the number of columns.
Definition Matrix.cpp:605
RealMatrix inv() const
Computes the inverse of the matrix via LU decomposition.
Definition Matrix.cpp:750
RAII installer for a ParameterRuntimeContext on the current thread.
void accepted(std::size_t accepted_count, std::size_t attempts=0, std::size_t failures=0)
void finish(std::size_t accepted_count, std::size_t attempts, std::size_t failures)
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 max_prediction_failures
Definition MCEngine.h:50
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
bool retry_failed_predictions
Definition MCEngine.h:47
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
std::vector< double > mean
Definition MCEngine.h:101
RealMatrix covariance
Definition MCEngine.h:104
std::vector< BinnedObservableId > ids
Definition MCEngine.h:98
RealMatrix covariance_inv
Definition MCEngine.h:107
Raw Monte Carlo samples accepted by the engine.
Definition MCEngine.h:84