MaCh3  2.6.1
Reference Guide
UmbrellaSolver.cpp
Go to the documentation of this file.
1 
6 #include <filesystem>
8 #include "Manager/Manager.h"
10 
12 #include "TSystem.h"
13 #include "TChain.h"
14 #include "TSystemDirectory.h"
16 
17 bool debug_mode = false;
20 struct WindowConfig {
21  std::string name;
23  double center;
24  double width;
25  std::string input_file;
27 };
28 
32  std::vector<WindowConfig> windows;
33  std::string output_file;
34  std::string variable_of_interest;
36  std::string dynamic_pattern;
39  double tolerance;
41  bool use_openmp;
42 };
43 
45 UmbrellaConfig parseYAMLConfig(const std::string &filename) {
46  UmbrellaConfig config;
47 
48  try {
49  YAML::Node yaml_diag_config = YAML::LoadFile(filename);
50  YAML::Node yaml_config = yaml_diag_config["UmbrellaSolver"];
51 
52  // Parse other configuration
53  config.output_file = Get<std::string>(yaml_config["output_file"], __FILE__ , __LINE__);
54  config.variable_of_interest = Get<std::string>(yaml_config["variable_of_interest"], __FILE__ , __LINE__);
55  config.max_iterations = Get<int>(yaml_config["max_iterations"], __FILE__ , __LINE__);
56  config.tolerance = Get<double>(yaml_config["tolerance"], __FILE__ , __LINE__);
57  config.print_frequency = GetFromManager<int>(yaml_config["print_frequency"], 0, __FILE__ , __LINE__);
58  config.dynamic_files = GetFromManager<bool>(yaml_config["dynamic_files"], false, __FILE__ , __LINE__);
59  config.dynamic_pattern = Get<std::string>(yaml_config["dynamic_pattern"], __FILE__ , __LINE__);
60  config.dynamic_n_windows = Get<int>(yaml_config["dynamic_n_windows"], __FILE__ , __LINE__);
61  config.use_openmp = GetFromManager<bool>(yaml_config["use_openmp"], true, __FILE__ , __LINE__);
62 
63  if (!config.dynamic_files) {
64  // TODO: these are mostly redundant now that the MaCh3_Config macro is
65  // being parsed, work out if its safe to remove them at somepoint!!! Parse
66  // windows
67  if (yaml_config["windows"]) {
68  const YAML::Node &windows = yaml_config["windows"];
69  for (size_t i = 0; i < windows.size(); i++) {
70  WindowConfig window;
71  window.name = Get<std::string>(windows[i]["name"], __FILE__, __LINE__);
72  window.center = Get<double>(windows[i]["center"], __FILE__, __LINE__);
73  window.width = Get<double>(windows[i]["width"], __FILE__, __LINE__);
74  config.windows.push_back(window);
75  }
76  }
77 
78  // Parse input files
79  if (yaml_config["input_files"]) {
80  const YAML::Node &input_files = yaml_config["input_files"];
81  for (size_t i = 0; i < input_files.size() && i < config.windows.size(); i++) {
82  config.windows[i].input_file = Get<std::string>(input_files[i], __FILE__, __LINE__);
83  }
84  }
85  } else {
86  // set up placeholders to be filled later
87  for (int i = 0; i < config.dynamic_n_windows; i++) {
88  WindowConfig window;
89  window.name = "Window_" + std::to_string(i);
90  window.center =
91  0.0; // Placeholder, will be updated from MaCh3_Config macro
92  window.width =
93  1.0; // Placeholder, will be updated from MaCh3_Config macro
94  config.windows.push_back(window);
95  }
96  }
97 
98  } catch (const YAML::Exception &e) {
99  MACH3LOG_ERROR("Error parsing YAML file {}: {}", filename, e.what());
100  throw MaCh3Exception(__FILE__, __LINE__);
101  }
102 
103  return config;
104 }
105 
106 // Gaussian window function
107 double gaussianWindow(double x, double center, double width) {
108  return exp(-0.5 * pow((x - center) / width, 2)) / (width * sqrt(2 * TMath::Pi()));
109 }
110 
111 // von Mises window function (circular analog of Gaussian)
112 double vonMisesWindow(double x, double center, double kappa) {
113  // von Mises PDF: exp(kappa * cos(x - center)) / (2*pi*I0(kappa))
114  // For numerical stability, compute in log space when possible
115  double I0_kappa;
116  if (kappa > 700) {
117  // Use asymptotic approximation for large kappa to avoid overflow
118  // log(I0(kappa)) ≈ kappa - 0.5*log(2*pi*kappa)
119  double log_I0 = kappa - 0.5 * log(2 * TMath::Pi() * kappa);
120  return exp(kappa * cos(x - center) - log_I0 - log(2 * TMath::Pi()));
121  } else {
122  I0_kappa = TMath::BesselI0(kappa);
123  return exp(kappa * cos(x - center)) / (2 * TMath::Pi() * I0_kappa);
124  }
125 }
126 
127 // Generalised Gaussian window allows harsher constraints on the tails of the distribution, better for controlling windows in highly disfavoured regions
128 double generalisedGaussian2(double x, double mean, double width) {
129  constexpr int n = 2; // this controls the tightness of the gaussian fixed at 2
130  // for now due to normalisation
131  const double normFactor = 1 / ((M3::UmbrellaGaussianNormFactor) * 2 * std::sqrt(2) * width); // the normalisation is a little ugly (uses gamma functions),
132  // im just going to hardcode them for now
133  double likelihood = normFactor * std::exp(-std::pow((std::pow(x - mean, 2) / (2 * std::pow(width, 2))), n));
134  return likelihood;
135 }
136 
138 double GetMulticanonicalWeightGenGaussian(double deltacp, double mean, double width) {
139  // implementation of the generalised gaussian as a bias function
140  // for now with a fixed n = 2 for simplicity
141 
142  double g0 = generalisedGaussian2(deltacp, mean, width);
143  double g1 = generalisedGaussian2(deltacp, mean - 2 * TMath::Pi(),width); // these two repeats are required for wrapping the gaussian around -+pi
144  double g2 = generalisedGaussian2(deltacp, mean + 2 * TMath::Pi(), width);
145  double multicanonicalBeta = 1.0;
146  return (g0 + g1 + g2) * (multicanonicalBeta);
147 }
148 
149 // A sub calculation for the overlap matrix
150 // Sum of all windows weighted by z values
151 double summedWindowsWeighted(double x, const std::vector<WindowConfig> &windows, const std::vector<double> &z_values) {
152  double sum = 0.0;
153  for (size_t k = 0; k < windows.size(); k++) {
154  double window_val;
155  if (windows[k].umbrellaBiasFunction == M3::BiasFunction::kVonMises) {
156  window_val = vonMisesWindow(x, windows[k].center, windows[k].vonMises_kappa);
157  } else if (windows[k].umbrellaBiasFunction == M3::BiasFunction::kGaussian) {
158  window_val = gaussianWindow(x, windows[k].center, windows[k].width);
159  } else if (windows[k].umbrellaBiasFunction == M3::BiasFunction::kGeneralisedGaussian) {
160  window_val = GetMulticanonicalWeightGenGaussian(x, windows[k].center, windows[k].width);
161  } else {
162  MACH3LOG_ERROR("Unrecognised BiasFunction!!");
163  throw MaCh3Exception(__FILE__, __LINE__);
164  }
165  sum += window_val / z_values[k];
166  }
167  return sum;
168 }
169 
170 // Precompute all window evaluations once: cache[i][j][s] = window_j evaluated
171 // at samples[i][s]
172 // Memory heavy depending on number of steps/cores/windows
173 std::vector<std::vector<std::vector<double>>> buildWindowCache(const std::vector<WindowConfig> &windows,
174  const std::vector<std::vector<double>> &samples, bool use_openmp = true) {
175  int n_windows = static_cast<int>(windows.size());
176  std::vector<std::vector<std::vector<double>>> cache(n_windows);
177 
178  for (int i = 0; i < n_windows; i++) {
179  cache[i].resize(n_windows);
180  for (int j = 0; j < n_windows; j++) {
181  cache[i][j].resize(samples[i].size());
182  }
183  }
184 
185  if (use_openmp) {
186  #ifdef MULTITHREAD
187  #pragma omp parallel for collapse(2) schedule(dynamic)
188  #endif
189  for (int i = 0; i < n_windows; i++) {
190  for (int j = 0; j < n_windows; j++) {
191  for (size_t s = 0; s < samples[i].size(); s++) {
192  if (windows[j].umbrellaBiasFunction == M3::BiasFunction::kVonMises) {
193  cache[i][j][s] = vonMisesWindow(samples[i][s], windows[j].center, windows[j].vonMises_kappa);
194  } else if (windows[j].umbrellaBiasFunction == M3::BiasFunction::kGaussian) {
195  cache[i][j][s] = gaussianWindow(samples[i][s], windows[j].center, windows[j].width);
196  } else if (windows[j].umbrellaBiasFunction == M3::BiasFunction::kGeneralisedGaussian) {
197  cache[i][j][s] = GetMulticanonicalWeightGenGaussian(samples[i][s], windows[j].center, windows[j].width);
198  } else {
199  MACH3LOG_ERROR("Unrecognised BiasFunction!!");
200  throw MaCh3Exception(__FILE__, __LINE__);
201  }
202  }
203  }
204  }
205  } else {
206  for (int i = 0; i < n_windows; i++) {
207  for (int j = 0; j < n_windows; j++) {
208  for (size_t s = 0; s < samples[i].size(); s++) {
209  if (windows[j].umbrellaBiasFunction == M3::BiasFunction::kVonMises) {
210  cache[i][j][s] = vonMisesWindow(samples[i][s], windows[j].center, windows[j].vonMises_kappa);
211  } else if (windows[j].umbrellaBiasFunction == M3::BiasFunction::kGaussian) {
212  cache[i][j][s] = gaussianWindow(samples[i][s], windows[j].center, windows[j].width);
213  } else if (windows[j].umbrellaBiasFunction == M3::BiasFunction::kGeneralisedGaussian) {
214  cache[i][j][s] = GetMulticanonicalWeightGenGaussian(samples[i][s], windows[j].center, windows[j].width);
215  } else {
216  MACH3LOG_ERROR("Unrecognised function!!!!!");
217  throw MaCh3Exception(__FILE__, __LINE__);
218  }
219  }
220  }
221  }
222  }
223 
224  if (debug_mode) { // this should be TH1D of cache against sample value in the
225  // variable of interest for each window, to check the cache
226  // is being built correctly and the windows look correct
227  // across the range of samples
228  // TODO break this into a standalone function
229  // first print the size in memory of the cache to check it is reasonable and
230  // not being built incorrectly with an extra dimension or something
231  size_t cache_size_bytes = 0;
232  for (int i = 0; i < n_windows; i++) {
233  for (int j = 0; j < n_windows; j++) {
234  cache_size_bytes += cache[i][j].size() * sizeof(double);
235  }
236  }
237  MACH3LOG_INFO("Window cache size: {:.2f} MB", static_cast<double>(cache_size_bytes) / (1024.0 * 1024.0));
238  MACH3LOG_INFO("Window cache built successfully.");
239  TFile *cache_file = TFile::Open("window_cache_debug_histograms.root", "RECREATE");
240  for (int i = 0; i < n_windows; i++) {
241  for (int j = 0; j < n_windows; j++) {
242  std::string hist_name = "Sample" + std::to_string(i) + "_window" + std::to_string(j);
243  TH1D *window_cache_hist = new TH1D(hist_name.c_str(), hist_name.c_str(), 100, -3.1415, 3.1415);
244  for (size_t s = 0; s < cache[i][j].size(); s++) {
245  window_cache_hist->AddBinContent(window_cache_hist->FindBin(samples[i][s]));
246  }
247  window_cache_hist->Write();
248  }
249  }
250 
251  // additionally save a TH2D of the values in the cache against the sample
252  // values for a specific window to check the shape of the windows is correct
253  // across the range of samples
254  for (int i = 0; i < n_windows; i++) {
255  for (int j = 0; j < n_windows; j++) {
256  std::string hist_name = "Sample" + std::to_string(i) + "_window" + std::to_string(j) + "_cache_2D";
257  TH2D *window_cache_2D_hist = new TH2D(hist_name.c_str(), hist_name.c_str(), 100, -3.1415, 3.1415, 100, -1000, 10);
258  for (size_t s = 0; s < cache[i][j].size(); s++) {
259  // take the log of the likelihood values
260  window_cache_2D_hist->Fill(samples[i][s], log(cache[i][j][s]));
261  }
262  // show overflow numbers on the hist
263  window_cache_2D_hist->SetStats(1);
264  window_cache_2D_hist->Write();
265  }
266  }
267 
268  cache_file->Close();
269  }
270 
271  return cache;
272 }
273 
274 // Main function to calculate the F matrix from a given set of samples plus weights
275 std::vector<std::vector<double>> calcFmatrix(std::vector<double> &z_current,
276  const std::vector<WindowConfig> &windows,
277  const std::vector<std::vector<double>> &samples,
278  const std::vector<std::vector<std::vector<double>>> &window_cache) {
279  int n_windows = static_cast<int>(windows.size());
280  std::vector<std::vector<double>> F(n_windows, std::vector<double>(n_windows, 0.0));
281 
282  std::vector<double> z_inv = z_current; // make a copy to avoid modifying the original z_current
283  for (size_t i = 0; i < z_current.size(); i++) {
284  if (z_current[i] > 0) {
285  z_inv[i] = 1.0 / z_current[i];
286  } else {
287  z_inv[i] = 0.0; // Handle zero or negative z values gracefully
288  if (debug_mode) {
289  MACH3LOG_WARN("Warning: z_current[{}] is non-positive ({}). Setting its inverse to 0 in F matrix calculation.", i, z_current[i]);
290  }
291  }
292  }
293  #ifdef MULTITHREAD
294  #pragma omp parallel for schedule(dynamic)
295  #endif
296  for (int i = 0; i < n_windows; i++) {
297  std::vector<double> denominator_cache(samples[i].size(), 0.0);
298  for (size_t s = 0; s < samples[i].size(); s++) {
299  double denominator = 0.0;
300  for (int k = 0; k < n_windows; k++) {
301  denominator += window_cache[i][k][s] * z_inv[k];
302  }
303  denominator_cache[s] = 1 / denominator;
304  }
305 
306  for (int j = 0; j < n_windows; j++) {
307  double sum = 0.0;
308  int count = 0;
309 
310  for (size_t s = 0; s < samples[i].size(); s++) {
311  double sample = samples[i][s];
312  double window_j = window_cache[i][j][s];
313  double denominator = denominator_cache[s];
314 
315  if (denominator > 0) {
316  double integrand = (window_j * z_inv[i]) * denominator;
317  sum += integrand;
318  count++;
319  } else if (debug_mode) {
320  MACH3LOG_WARN("Denominator is zero for sample {} in window {}, skipping...", sample, i);
321  }
322  }
323 
324  if (debug_mode) {
325  MACH3LOG_INFO("F[{}][{}] sum: {}, count: {}", i, j, sum, count);
326  }
327 
328  if (count > 0) {
329  F[i][j] = sum / count;
330  }
331  }
332  }
333 
334  return F;
335 }
336 
337 // Z-solver function implementing the fixed point matrix iteration algorithm
338 std::vector<double> zSolver(const std::vector<double> &z_current,
339  const std::vector<WindowConfig> &windows,
340  const std::vector<std::vector<double>> &samples,
341  const std::vector<std::vector<std::vector<double>>> &window_cache,
342  bool use_openmp = true, bool verbose = false,
343  [[maybe_unused]] int *total_lines = nullptr) {
344  int n_windows = static_cast<int>(windows.size());
345  if (verbose && !use_openmp) {
346  MACH3LOG_INFO("Using single-threaded computation for F matrix...");
347  }
348 
349  // F matrix and update z values
350  std::vector<double> z_working = z_current;
351  std::vector<std::vector<double>> F =
352  calcFmatrix(z_working, windows, samples, window_cache);
353 
354  // if (verbose) {
355  // if (total_lines) *total_lines = 1; // Start counting from F matrix
356  // header std::cout << "F matrix:" << std::endl; for (int i = 0; i <
357  // n_windows; i++) {
358  // if (total_lines) (*total_lines)++; // Count each row
359  // std::cout << "[";
360  // for (int j = 0; j < n_windows; j++) {
361  // std::cout << std::setw(10) << std::fixed <<
362  // std::setprecision(5) << F[i][j]; if (j < n_windows - 1)
363  // std::cout << ", ";
364  // }
365  // std::cout << "]" << std::endl;
366  // }
367  // std::cout << std::flush;
368  // }
369 
370  // Compute z_new = z_current * F
371  // could do faster matrix multiplication here?
372  std::vector<double> z_new(n_windows, 0.0);
373  for (int i = 0; i < n_windows; i++) {
374  for (int j = 0; j < n_windows; j++) {
375  z_new[i] += z_current[j] * F[j][i];
376  }
377  }
378 
391  //}
392 
393  // normalise the z values so that magnitude of the vector is 1
394  double z_magnitude = 0.0;
395  for (int i = 0; i < n_windows; i++) {
396  z_magnitude += z_new[i] * z_new[i];
397  }
398  z_magnitude = sqrt(z_magnitude);
399  if (z_magnitude > 0) {
400  for (int i = 0; i < n_windows; i++) {
401  z_new[i] /= z_magnitude;
402  }
403  }
404 
405  return z_new;
406 }
407 
409 // Check convergence
410 bool checkConvergence(const std::vector<double> &z_current, const std::vector<double> &z_prev, double tolerance) {
411  double sum_diffs = 0.0;
412  for (size_t i = 0; i < z_current.size(); i++) {
413  // if (std::abs(z_current[i] - z_prev[i]) > tolerance *
414  // std::max(std::abs(z_current[i]), std::abs(z_prev[i]))) { // intention
415  // here: is the difference between any of the z elements more than the
416  // tolerance*the magnitude of the largest element in z? in this way the
417  // tolerance sets the number of decimal points below the largest element we
418  // are targeting.
419  // return false;
420  // }
421  sum_diffs += std::abs(z_current[i] - z_prev[i]);
422  // check the average difference across all elements instead
423  // of requiring every element to meet the condition, this
424  // should be more robust to individual elements fluctuating
425  // around their target value while the overall z
426  // distribution is still converging
427  if (sum_diffs / static_cast<double>(z_current.size()) > tolerance) {
428  return false;
429  }
430  }
431  return true;
432 }
433 
434 std::vector<double> getZDiffs(const std::vector<double> &z_current, const std::vector<double> &z_prev) {
435  std::vector<double> diffs(z_current.size(), 0.0);
436  for (size_t i = 0; i < z_current.size(); i++) {
437  diffs[i] = std::abs(z_current[i] - z_prev[i]);
438  }
439  return diffs;
440 }
441 
442 // This implements a check on stalling of the evolution of the window weights. Seems to be a better definition for convergence that above
443 // moving-average stalled-convergence check on z values
444 bool checkConvergenceStalled(const std::vector<double> &z_current, const std::vector<double> &z_prev, double tolerance) {
445  (void)z_prev;
446 
447  static std::deque<std::vector<double>> z_history;
448  static std::vector<double> previous_moving_average;
449  static int stagnant_iterations = 0;
450 
451  constexpr int moving_average_window = 500;
452  constexpr int stagnant_required = 500;
453  const double bound = tolerance;
454 
455  if (z_current.empty()) {
456  return false;
457  }
458 
459  // Reset state safely if number of windows changes between solver runs.
460  if (!z_history.empty() && z_history.front().size() != z_current.size()) {
461  z_history.clear();
462  previous_moving_average.clear();
463  stagnant_iterations = 0;
464  }
465 
466  z_history.push_back(z_current);
467  if (static_cast<int>(z_history.size()) > moving_average_window) {
468  z_history.pop_front();
469  }
470 
471  // Wait until the moving-average window is fully populated.
472  if (static_cast<int>(z_history.size()) < moving_average_window) {
473  return false;
474  }
475 
476  std::vector<double> moving_average(z_current.size(), 0.0);
477  for (const auto &z_vec : z_history) {
478  for (size_t i = 0; i < z_vec.size(); i++) {
479  moving_average[i] += z_vec[i];
480  }
481  }
482  for (size_t i = 0; i < moving_average.size(); i++) {
483  moving_average[i] /= moving_average_window;
484  }
485 
486  if (previous_moving_average.empty()) {
487  previous_moving_average = moving_average;
488  return false;
489  }
490 
491  bool all_within_bound = true;
492  for (size_t i = 0; i < moving_average.size(); i++) {
493  if (std::abs(moving_average[i] - previous_moving_average[i]) > bound) {
494  all_within_bound = false;
495  break;
496  }
497  }
498 
499  if (all_within_bound) {
500  stagnant_iterations++;
501  } else {
502  stagnant_iterations = 0;
503  }
504 
505  previous_moving_average = moving_average;
506 
507  if (stagnant_iterations == stagnant_required) {
508  MACH3LOG_WARN("Convergence appears stalled: moving-average change stayed within {} for {} iterations.",
509  bound, stagnant_required);
510  }
511 
512  return stagnant_iterations >= stagnant_required;
513 }
514 
515 // Main function to run the umbrella sampling solver
516 void UmbrellaSolver(const std::string &config_file) {
517  MACH3LOG_INFO("=== Umbrella Sampling Z-Factor Solver ===");
518  // Debug OpenMP status first
519  MACH3LOG_INFO("Debugging OpenMP availability...");
520 
521  #ifdef MULTITHREAD
522  MACH3LOG_INFO("Max threads available: {}", omp_get_max_threads());
523  #else
524  MACH3LOG_WARN("_OPENMP is NOT defined - OpenMP not available");
525  #endif
526  MACH3LOG_INFO("Loading configuration from: {}", config_file);
527 
528  // Parse configuration
529  UmbrellaConfig config = parseYAMLConfig(config_file);
530 
531  if (config.windows.empty() && !config.dynamic_files) {
532  MACH3LOG_ERROR("No windows defined in configuration and dynamic file loading is disabled.");
533  return;
534  }
535 
536  MACH3LOG_INFO("Variable of interest: {}", config.variable_of_interest);
537  MACH3LOG_INFO("Output file: {}", config.output_file);
538 
539 // Check OpenMP status with detailed debugging
540 #ifdef MULTITHREAD
541  MACH3LOG_INFO("OpenMP: AVAILABLE");
542  if (config.use_openmp) {
543  MACH3LOG_INFO("OpenMP: ENABLED (using {} threads)", omp_get_max_threads());
544  }
545 #else
546  MACH3LOG_WARN("OpenMP: NOT AVAILABLE");
547  if (config.use_openmp) {
548  MACH3LOG_WARN("OpenMP: NOT AVAILABLE - falling back to single-threaded execution");
549  MACH3LOG_INFO("Note: For OpenMP support, try compiling with: g++ -fopenmp ...");
550  MACH3LOG_INFO("Or ensure OpenMP library is properly loaded in ROOT");
551  config.use_openmp = false;
552  } else {
553  MACH3LOG_INFO("OpenMP: DISABLED (single-threaded execution)");
554  }
555 #endif
556 
557  // Load data from input files
558  std::vector<std::vector<double>> samples; // Declare here to ensure it exists in the scope of the entire
559  // function
560  if (!config.dynamic_files) {
561  samples.resize(config.windows.size());
562  } else {
563  samples.resize(config.dynamic_n_windows);
564  }
565  std::vector<TFile *> input_files;
566  std::vector<TTree *> input_trees;
567 
568  if (!config.dynamic_files) {
569  MACH3LOG_INFO("Using static input files from configuration.");
570  for (size_t i = 0; i < config.windows.size(); i++) {
571  MACH3LOG_INFO("Loading file: {}", config.windows[i].input_file);
572  TFile *file = M3::Open(config.windows[i].input_file.c_str(), "READ", __FILE__, __LINE__);
573 
574  TTree *tree = static_cast<TTree*>(file->Get("posteriors"));
575  if (!tree) {
576  MACH3LOG_ERROR("Cannot find 'posteriors' tree in {}", config.windows[i].input_file);
577  file->Close();
578  continue;
579  }
580 
581  input_files.push_back(file);
582  input_trees.push_back(tree);
583  }
584  } else {
585  MACH3LOG_INFO("Dynamic file loading enabled. Searching for files in directory: {}", config.dynamic_pattern);
586  // Use ROOT's TSystem to find all files in the directory
587  TSystemDirectory dir("", config.dynamic_pattern.c_str());
588  TList *files = dir.GetListOfFiles();
589  int file_count = 0;
590  if (files) {
591  TIter next(files);
592  TSystemFile *file;
593  while ((file = static_cast<TSystemFile*>(next()))) {
594  std::string filename = file->GetName();
595  if (filename.find(".root") == std::string::npos) {
596  MACH3LOG_INFO("Skipping non-root file: {}", filename);
597  continue;
598  }
599 
600  std::string full_path = config.dynamic_pattern + "/" + filename;
601  MACH3LOG_INFO("Found file: {}", full_path);
602 
603  TFile *root_file = M3::Open(full_path, "READ", __FILE__, __LINE__);
604 
605  TTree *tree = static_cast<TTree*>(root_file->Get("posteriors"));
606  if (!tree) {
607  MACH3LOG_ERROR("Cannot find 'posteriors' tree in {}", full_path);
608  root_file->Close();
609  throw MaCh3Exception(__FILE__, __LINE__, "Missing 'posteriors' tree in file: " + full_path);
610  }
611  file_count++;
612 
613  input_files.push_back(root_file);
614  input_trees.push_back(tree);
615  MACH3LOG_INFO("Loaded tree 'posteriors' from file: {}", full_path);
616  }
617  } else {
618  MACH3LOG_ERROR("No files found matching pattern: {}", config.dynamic_pattern);
619  throw MaCh3Exception(__FILE__, __LINE__, "No files found matching pattern: " + config.dynamic_pattern);
620  }
621 
622  if (file_count != config.dynamic_n_windows) {
623  MACH3LOG_ERROR("Number of files found ({}) does not match expected dynamic_n_windows ({}).",
624  file_count, config.dynamic_n_windows);
625  throw MaCh3Exception(__FILE__, __LINE__, "File count mismatch for dynamic loading.");
626  }
627  }
628 
629  for (size_t i = 0; i < input_trees.size(); i++) {
630  TTree *tree = input_trees[i];
631  TFile *file = input_files[i];
632 
633  MACH3LOG_INFO("Processing file {}/{}: {}", i + 1, input_trees.size(), file->GetName());
634  TMacro *macro = static_cast<TMacro*>(file->Get("MaCh3_Config"));
635  if (macro) {
636  MACH3LOG_INFO("Found MaCh3_Config macro in file.");
637 
638  // Convert TMacro to YAML: concatenate all lines and parse
639  std::stringstream yaml_text;
640  TList *lines = macro->GetListOfLines();
641  for (int iline = 0; iline < lines->GetEntries(); ++iline) {
642  TObjString *line = static_cast<TObjString*>(lines->At(iline));
643  yaml_text << line->GetString().Data() << "\n";
644  }
645 
646  try {
647  YAML::Node macro_yaml = YAML::Load(yaml_text.str());
648  YAML::Node umbrellaConfig =
649  macro_yaml["General"]["MCMC"]["Multicanonical"];
650  // Extract window parameters
651  config.windows[i].center = Get<double>(umbrellaConfig["Umbrella"]["UmbrellaMean"], __FILE__, __LINE__);
652  MACH3LOG_INFO("Window {} center updated to {}", i, config.windows[i].center);
653 
654  // Check if using von Mises distribution
655  auto biasString = Get<std::string>(umbrellaConfig["Umbrella"]["UmbrellaBiasFunction"], __FILE__, __LINE__);
656  M3::BiasFunction biasMode;
657  if (biasString == "gaussian") {
658  biasMode = M3::BiasFunction::kGaussian;
659  MACH3LOG_INFO("Window weighted with gaussian");
660  } else if (biasString == "generalisedGaussian") {
662  MACH3LOG_INFO("Window weighted with generalised gaussian");
663  } else if (biasString == "vonMises") {
664  biasMode = M3::BiasFunction::kVonMises;
665  MACH3LOG_INFO("Window weighted with vonMises");
666  } else {
667  MACH3LOG_ERROR("Unrecognised Bias");
668  throw MaCh3Exception(__FILE__, __LINE__);
669  }
670  config.windows[i].umbrellaBiasFunction = biasMode;
671 
672  if (config.windows[i].umbrellaBiasFunction == M3::BiasFunction::kVonMises) {
673  // Extract von Mises sigma and compute kappa
674  double vonMises_sigma = Get<double>(umbrellaConfig["Umbrella"]["UmbrellaWidth"], __FILE__, __LINE__);
675  config.windows[i].vonMises_kappa = 1.0 / (vonMises_sigma * vonMises_sigma);
676  config.windows[i].width = vonMises_sigma; // Store sigma in width for reference
677  MACH3LOG_INFO("Window {} using von Mises: sigma = {}, kappa = {}",
678  i, vonMises_sigma, config.windows[i].vonMises_kappa);
679  } else {
680  // Extract Gaussian sigma
681  config.windows[i].width = Get<double>(umbrellaConfig["Umbrella"]["UmbrellaWidth"], __FILE__, __LINE__);
682  config.windows[i].vonMises_kappa = -1.0; // Not using von Mises
683  MACH3LOG_INFO("Window {} using Gaussian: width = {}", i, config.windows[i].width);
684  }
685  } catch (const std::exception &e) {
686  MACH3LOG_WARN("Could not parse macro as YAML: {}", e.what());
687  }
688  }
689 
690  double var_value;
691  double logL_value;
692  tree->SetBranchAddress(config.variable_of_interest.c_str(), &var_value);
693  tree->SetBranchAddress("LogL", &logL_value);
694 
695  Long64_t nentries = tree->GetEntries();
696  //Long64_t filtered_entries = 0;
697  MACH3LOG_INFO("Window {}: {} entries", i, nentries);
698 
699  for (Long64_t entry = 0; entry < nentries; entry++) {
700  tree->GetEntry(entry);
701  //if (logL_value > 50.0) { // logl cut no longer needed as the posterior
702  // // chain start has been fixed
703  // filtered_entries++;
704  // continue;
705  //}
706  samples[i].push_back(var_value);
707  }
708  //if (filtered_entries > 0) {
709  // std::cout << "Filtered " << filtered_entries
710  // << " entries with LogL > 500 from window " << i << std::endl;
711  //}
712  }
713 
714  // Sort by window center and keep all per-window containers aligned.
715  // The final weighting stage loops over input_trees by index, so those indices
716  // must track the same sorted window order used by z_current.
717  for (size_t i = 0; i < config.windows.size(); i++) {
718  for (size_t j = i + 1; j < config.windows.size(); j++) {
719  if (config.windows[i].center > config.windows[j].center) {
720  std::swap(config.windows[i], config.windows[j]);
721  std::swap(samples[i], samples[j]);
722  std::swap(input_trees[i], input_trees[j]);
723  std::swap(input_files[i], input_files[j]);
724  }
725  }
726  }
727 
728  // verify the order and file associations after sorting
729  MACH3LOG_INFO("Final window configurations after sorting:");
730  for (size_t i = 0; i < config.windows.size(); i++) {
731  MACH3LOG_INFO("Window {}: center = {}, width = {}, vonMises_mode = {}, vonMises_kappa = {}, samples = {}",
732  i, config.windows[i].center, config.windows[i].width,
733  (config.windows[i].umbrellaBiasFunction == M3::BiasFunction::kVonMises ? "Yes" : "No"),
734  config.windows[i].vonMises_kappa, samples[i].size());
735  }
736 
737  // Initialize z values
738  std::vector<double> z_current(config.windows.size(), 1.0);
739  std::vector<double> z_prev(config.windows.size(), 1.0);
740 
741  // this should be used to pick up a solve that failed partway through due to reaching iteration max or job cancellation
742  bool hacky_start = false; // Set to true to use the hardcoded starting vector, false to start with all ones
743  if (hacky_start) {
744  // z_current = { 0.02593, 0.02676, 0.02764, 0.03305, 0.03527,
745  // 0.04275, 0.05171, 0.04808, 0.04978, 0.04560, 0.04627,
746  // 0.05187, 0.04993, 0.04656, 0.04690, 0.04954, 0.04940,
747  // 0.04315, 0.03616, 0.02832, 0.01998, 0.01482, 0.01167,
748  // 0.00902, 0.00646, 0.00474, 0.00439, 0.00421, 0.00470,
749  // 0.00553, 0.00626, 0.00822, 0.01070, 0.01477, 0.01750,
750  // 0.02235};
751  z_current = {
752  0.023968629123202176, 0.024927005713178161, 0.026030888791054529,
753  0.036203405237770721, 0.04004944137212621, 0.055993616350153479,
754  0.079251266929094608, 0.065860139904686643, 0.067944181615205768,
755  0.055237804689517243, 0.054778778031073304, 0.066196102148964917,
756  0.059298667596959342, 0.049864361722134341, 0.048890393249559315,
757  0.05284144211204099, 0.050606183191239794, 0.037329936801427918,
758  0.025159187577401387, 0.01487697249782439, 0.0074395236463036573,
759  0.0040535024095969992, 0.0025347709967512371, 0.0015590961074484638,
760  0.00083136243905723782, 0.00047599269828616062, 0.0004454414833514952,
761  0.00045205403592812914, 0.00062098732584897902, 0.00091722187629238541,
762  0.0012407301891732216, 0.0023494590898020503, 0.0041640866720417773,
763  0.0080599626292932776, 0.011429554834689368, 0.018117848911520615};
764  z_prev = z_current; // Start with the same values for previous to avoid
765  // large initial changes
766  MACH3LOG_WARN("!!!!!!!starting from hacky start vector!!!!!!");
767  }
768 
769  std::vector<std::vector<double>> z_evolution;
770 
771  MACH3LOG_INFO("Starting iterative z-solver...");
772  // Test OpenMP functionality once before the main loop this can probably be wrapped in debug TODO
773  bool openmp_works = false;
774  if (config.use_openmp) {
775  MACH3LOG_INFO("Testing OpenMP parallelization...");
776  #ifdef MULTITHREAD
777  int max_threads = omp_get_max_threads();
778  #else
779  int max_threads = 1;
780  #endif
781 
782  MACH3LOG_INFO("Max threads reported: {}", max_threads);
783 
784  // Test parallel region
785  int actual_threads = 1;
786  #ifdef MULTITHREAD
787  #pragma omp parallel
788  #endif
789  {
790  #ifdef MULTITHREAD
791  #pragma omp master
792  #endif
793  {
794  #ifdef MULTITHREAD
795  actual_threads = omp_get_num_threads();
796  #else
797  actual_threads = 1;
798  #endif
799  MACH3LOG_INFO("Actual threads in parallel region: {}", actual_threads);
800  }
801  }
802 
803  if (actual_threads > 1) {
804  MACH3LOG_INFO("OpenMP is working correctly with {} threads", actual_threads);
805  openmp_works = true;
806  } else {
807  openmp_works = false;
808  }
809  }
810 
811  MACH3LOG_INFO("Precomputing window cache...");
812  // TODO: this is slow as hell and causes massive memory usage, what's a smarter
813  // way to do this? break out to a file? RDataFrame?
814  std::vector<std::vector<std::vector<double>>> window_cache = buildWindowCache(config.windows, samples, openmp_works);
815 
816  // TFile to hold the F matrix evolution for the first 15 iterations if needed
817  bool save_matrix = true; // Set to true to enable saving F matrix evolution
818  // Create F_file here to avoid reopening it multiple times in the loop
819  TFile *F_file = nullptr;
820  if (save_matrix) {
821  size_t pos = config.output_file.find(".root");
822  std::string base_name = (pos != std::string::npos) ? config.output_file.substr(0, pos) : config.output_file;
823  F_file = TFile::Open((base_name + "_matrix_evolution.root").c_str(), "RECREATE"); // use name from config with .root subtracted with _matrix_evolution suffix added
824  if (!F_file || F_file->IsZombie()) {
825  MACH3LOG_ERROR("Cannot create file {}", base_name + "_matrix_evolution.root");
826  save_matrix = false; // Disable saving if file cannot be created
827  }
828  // add an initial FMatrix with the initial z values for reference
829  std::vector<std::vector<double>> initial_F = calcFmatrix(z_current, config.windows, samples, window_cache);
830  int n_windows = static_cast<int>(config.windows.size());
831  TH2D initial_F_TH2D("F_matrix_initial", "Initial F matrix;Window j;Window i", n_windows, 0, n_windows, n_windows, 0, n_windows);
832  for (int i = 0; i < n_windows; i++) {
833  for (int j = 0; j < n_windows; j++) {initial_F_TH2D.SetBinContent(j + 1, i + 1, initial_F[i][j]); // Note the order of i and j for correct axis labeling
834  }
835  }
836  F_file->cd();
837  MACH3LOG_INFO("Saving initial F matrix to file...");
838  initial_F_TH2D.Write();
839  }
840 
841  // Timing variables
842  auto start_time = std::chrono::high_resolution_clock::now();
843  auto last_print_time = start_time;
844 
845  bool converged_robustness_check = false; // Flag to indicate if convergence check has been passed at least
846  // once, used to control when to start checking for stalled
847  // convergence
848 
849  if (!converged_robustness_check) {
850  MACH3LOG_INFO("Starting iterative solver with convergence checks...");
851  }
852  TRandom3 gRandom3;
853  // Iterative solver
854  int total_output_lines = 0; // Track total lines printed for clearing
855  for (int iteration = 0; iteration < config.max_iterations; iteration++) {
856  if (iteration % config.print_frequency == 0 || iteration == 1) {
857  auto current_time = std::chrono::high_resolution_clock::now();
858 
859  // Clear previous output if not the first iteration
860  if (iteration > 0) {
861  // Move cursor up and clear previous output
862  std::cout << "\033[" << total_output_lines << "A"; // Move up
863  std::cout << "\033[J"; // Clear from cursor to end of screen
864  }
865 
866  // Calculate average relative change (precision metric)
867  double avg_relative_change = 0.0;
868  if (iteration > 0) {
869  for (size_t i = 0; i < z_current.size(); i++) {
870  double rel_change = std::abs(z_current[i] - z_prev[i]) / std::max(std::abs(z_current[i]), 1e-10);
871  avg_relative_change += rel_change;
872  }
873  avg_relative_change /= static_cast<double>(z_current.size());
874  }
875 
876  // Print z-values
877  std::cout << "Iteration " << std::setw(6) << iteration << ", z values: [";
878  for (size_t i = 0; i < z_current.size(); i++) {
879  std::cout << std::setw(10) << std::fixed << std::setprecision(5) << z_current[i];
880  if (i < z_current.size() - 1)
881  std::cout << ", ";
882  }
883  if (iteration > 0) {
884  auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(current_time - last_print_time);
885  double avg_time_per_iteration = static_cast<double>(duration.count()) /
886  static_cast<double>(config.print_frequency);
887  std::cout << "] (avg: " << std::setw(6) << std::setprecision(1) << avg_time_per_iteration << " ms/iter)" << std::endl;
888  std::cout << "Avg relative change: " << std::scientific << std::setprecision(3) << avg_relative_change << " (target: " << config.tolerance << ")" << std::endl;
889  total_output_lines = 2;
890  } else {
891  std::cout << "]" << std::endl;
892  total_output_lines = 1;
893  }
894  last_print_time = current_time;
895  }
896 
897  z_prev = z_current;
898  z_current = zSolver(z_current, config.windows, samples, window_cache, openmp_works, iteration % config.print_frequency == 0, &total_output_lines);
899  z_evolution.push_back(z_current);
900 
901  // save the first 15 iterations of the F matrix to check convergence
902  // behaviour and debug if needed should be a root file with Th2D for easy
903  // plotting in root, with axes of iteration number and window index, and the
904  // value being the F matrix element
905  if (save_matrix && (iteration < 15 || iteration % config.print_frequency == 0)) {
906  std::vector<std::vector<double>> F_matrix = calcFmatrix(z_current, config.windows, samples, window_cache);
907  // convert F_matrix to Th2D for saving to root file
908  int n_windows = static_cast<int>(config.windows.size());
909  TH2D F_TH2D(Form("F_matrix_iter_%02d", iteration),Form("F matrix at iteration %02d;Window j;Window i", iteration), n_windows, 0, n_windows, n_windows, 0, n_windows);
910  for (int i = 0; i < n_windows; i++) {
911  for (int j = 0; j < n_windows; j++) {
912  F_TH2D.SetBinContent(j + 1, i + 1, F_matrix[i][j]); // Note the order of i and j for correct axis labeling
913  }
914  }
915  // for the purposes of picking back up a solve after it has been
916  // interrupted also save the std::vector of z_current. You can put this into hacky start to pick up
917  TTree *z_tree = new TTree(Form("z_saved_iter_%02d", iteration), Form("Z vector at iteration %02d", iteration));
918  z_tree->Branch("z_saved", &z_current);
919  z_tree->Fill();
920  F_file->cd();
921  MACH3LOG_INFO("Saving F matrix for iteration {} to file...", iteration);
922  F_TH2D.Write();
923  z_tree->Write();
924  }
925  // if (save_matrix && iteration == 15) {
926  // F_file->Close();
927  // }
928 
929  // after the first convergence check has been passed, randomly perturb the z
930  // values to see if they return to the same values, this is a robustness
931  // check to see if the solution is stable or if it is just meeting the
932  // convergence criteria by chance due to small changes in z values
933  bool apply_robustness_check = true;
934  if (iteration % 100 == 0 &&
935  (checkConvergence(z_current, z_prev, config.tolerance) || checkConvergenceStalled(z_current, z_prev, config.tolerance))) {
936  if (!converged_robustness_check && apply_robustness_check) {
937  MACH3LOG_INFO("Convergence check passed at iteration {}. Starting robustness check with random perturbation...", iteration);
938  converged_robustness_check = true;
939 
940  // Apply random perturbation to z_current
941  std::vector<double> z_perturbed = z_current;
942  for (size_t i = 0; i < z_perturbed.size(); i++) {
943  // Random perturbation up to 10 times the tolerance
944  double perturbation = gRandom3.Uniform(-0.5, 0.5) * z_perturbed[i];
945  MACH3LOG_INFO("Applying perturbation of {:.6e} to z[{}] = {:.6e}", perturbation, i, z_perturbed[i]);
946  z_perturbed[i] += perturbation;
947  if (z_perturbed[i] < 0)
948  z_perturbed[i] = abs(z_perturbed[i]); // Ensure no negative values
949  }
950  z_current = z_perturbed;
951  MACH3LOG_INFO("Applied random perturbation to z values for robustness check.");
952  } else {
953  if (checkConvergence(z_current, z_prev, config.tolerance)) {
954  MACH3LOG_INFO("Convergence achieved at iteration {}", iteration);
955  } else {
956  MACH3LOG_WARN("Convergence appears to be stalled at iteration {}", iteration);
957  }
958 
959  if (iteration == config.max_iterations - 1) {
960  MACH3LOG_WARN("Reached maximum iterations without convergence.");
961  }
962 
963  auto end_time = std::chrono::high_resolution_clock::now();
964  auto total_duration = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time);
965  double avg_time_total = static_cast<double>(total_duration.count()) /
966  static_cast<double>(iteration + 1);
967  if (save_matrix) {
968  F_file->Close();
969  }
970  MACH3LOG_INFO("Terminating at iteration {}", iteration);
971  MACH3LOG_INFO("Average time per iteration: {} ms", avg_time_total);
972  break;
973  }
974  }
975  }
976 
977  std::ostringstream oss;
978  oss << "Final z values: [";
979  for (size_t i = 0; i < z_current.size(); i++) {
980  oss << std::fixed << std::setprecision(5) << z_current[i];
981  if (i < z_current.size() - 1)
982  oss << ", ";
983  }
984  oss << "]";
985  MACH3LOG_INFO("{}", oss.str());
986 
987  std::filesystem::copy_file(input_files[0]->GetName(), config.output_file,
988  std::filesystem::copy_options::overwrite_existing);
989  // Create output file
990  TFile *output_file = M3::Open(config.output_file.c_str(), "UPDATE", __FILE__, __LINE__);
991  output_file->cd();
992 
993  TTree *input_tree = dynamic_cast<TTree*>(output_file->Get("posteriors"));
994  // Create combined tree with weights
995  TTree *combined_tree = input_tree->CloneTree(0);
996 
997  // Variables for the combined tree
998  double umbrella_weight;
999  int window_id;
1000  double delta_cp;
1001 
1002  combined_tree->Branch("umbrella_weight", &umbrella_weight, "umbrella_weight/D");
1003  combined_tree->Branch("window_id", &window_id, "window_id/I");
1004 
1005  // Fill combined tree
1006  for (size_t i = 0; i < input_trees.size(); i++) {
1007  TTree *tree = input_trees[i];
1008 
1009  Long64_t nentries = tree->GetEntries();
1010 
1011  // KS: This is to avoid warnings about missing umbrella branches...
1012  int oldLevel = gErrorIgnoreLevel;
1013  gErrorIgnoreLevel = kError;
1014  combined_tree->CopyAddresses(tree);
1015  gErrorIgnoreLevel = oldLevel;
1016 
1018  tree->SetBranchAddress("delta_cp", &delta_cp);
1019  // KS: SetBranchAddress above decouples the input branch address, so update the
1020  // copied output branch address to use the current delta_cp value.
1021  combined_tree->GetBranch("delta_cp")->SetAddress(&delta_cp);
1022  if (z_current[i] == 0) {
1023  MACH3LOG_WARN("Z value for window {} is zero, skipping weighting for this window to avoid division by zero.", i);
1024  }
1025  window_id = static_cast<int>(i);
1026 
1027  for (Long64_t entry = 0; entry < nentries; entry++) {
1028  tree->GetEntry(entry);
1029 
1030  if (z_current[i] == 0) {
1031  umbrella_weight = 0.0; // If z is zero, we cannot apply the umbrella weight, so we set it to 0 (completely downweigh this window's contribution)
1032  } else {
1033  // Calculate umbrella weight for this event
1034  // The umbrella weight corrects for the bias introduced by the window function Weight is 1 / sum of all window contributions (equation 4 from paper)
1035  double denominator = 1 / summedWindowsWeighted(delta_cp, config.windows, z_current);
1036 
1037  // umbrella_weight = z_current[i] / denominator; // with or without z_current[i] / denominator? why did I have this originally
1038  umbrella_weight = denominator; // This is the correct form based on the paper - the z_current[i] factor is already included in the summedWindowsWeighted function
1039  }
1040 
1041  if (combined_tree->Fill() < 0) {
1042  MACH3LOG_WARN("Failed writing output tree. Check disk quota/space and write permissions for: {}", config.output_file);
1043  throw MaCh3Exception(__FILE__, __LINE__);
1044  }
1045  }
1046  }
1047  // Write final results
1048  combined_tree->Write(input_tree->GetName(), TObject::kOverwrite);
1049 
1050  TDirectory* UmbreallaDir = output_file->mkdir("Umbrealla");
1051  UmbreallaDir->cd();
1052  // Save diagnostics
1053  TCanvas c1("c1", "Z Evolution", 800, 600);
1054  std::vector<TGraph*> z_graphs(config.windows.size());
1055  TLegend legend(0.7, 0.7, 0.9, 0.9);
1056 
1057  double ymax = 0.0;
1058  double ymin = std::numeric_limits<double>::max();
1059 
1060  for (size_t i = 0; i < config.windows.size(); i++) {
1061  std::vector<double> iterations, z_vals;
1062  for (size_t j = 0; j < z_evolution.size(); j++) {
1063  iterations.push_back(static_cast<int>(j));
1064  z_vals.push_back(z_evolution[j][i]);
1065  }
1066 
1067  for (double val : z_vals) {
1068  if (val > ymax)
1069  ymax = val;
1070  if (val < ymin)
1071  ymin = val;
1072  }
1073 
1074  z_graphs[i] = new TGraph(static_cast<int>(iterations.size()), &iterations[0], &z_vals[0]);
1075  z_graphs[i]->SetLineColor(static_cast<Color_t>(i + 1));
1076  z_graphs[i]->SetLineWidth(2);
1077  z_graphs[i]->SetName(Form("z_evolution_window_%lu", i));
1078  z_graphs[i]->SetTitle("Evolution of Z Values");
1079 
1080  if (i == 0) {
1081  z_graphs[i]->GetXaxis()->SetTitle("Iteration");
1082  z_graphs[i]->GetYaxis()->SetTitle("Z Value");
1083  z_graphs[i]->Draw("AL");
1084  } else {
1085  z_graphs[i]->Draw("L SAME");
1086  }
1087 
1088  legend.AddEntry(z_graphs[i], Form("Window %lu", i), "l");
1089  z_graphs[i]->Write();
1090  }
1091 
1092  legend.Draw();
1093  z_graphs[0]->SetMaximum(ymax);
1094  z_graphs[0]->SetMinimum(ymin);
1095  c1.Update();
1096  c1.SetLogy();
1097  c1.Write();
1098 
1099  // Create summary histogram of delta_cp distribution
1100  TH1D *h_delta_cp = new TH1D("h_delta_cp_weighted", "Weighted Delta CP Distribution", 100, -TMath::Pi(), TMath::Pi());
1101  TH1D *h_delta_cp_unweighted = new TH1D("h_delta_cp_unweighted", "Unweighted Delta CP Distribution", 100, -TMath::Pi(), TMath::Pi());
1102 
1103  combined_tree->Draw("delta_cp>>h_delta_cp_weighted", "umbrella_weight", "goff");
1104  combined_tree->Draw("delta_cp>>h_delta_cp_unweighted", "", "goff");
1105 
1106  h_delta_cp->Write();
1107  h_delta_cp_unweighted->Write();
1108 
1109  UmbreallaDir->Close();
1110  delete UmbreallaDir;
1111 
1112  // Get entry count before closing the file
1113  Long64_t total_entries = combined_tree->GetEntries();
1114 
1115  output_file->cd();
1116 
1117  YAML::Node yaml_config = M3OpenConfig(config_file);
1118  YAML::Node umbrella_config;
1119  umbrella_config["UmbrellaSolver"] = yaml_config["UmbrellaSolver"];
1120 
1121  // Convert YAML -> TMacro
1122  TMacro UmbrellaHeader = YAMLtoTMacro(umbrella_config, "Umbrella_Config");
1123  UmbrellaHeader.Write();
1124 
1125  output_file->Close();
1126 
1127  // Close input files
1128  for (TFile *file : input_files) {
1129  file->Close();
1130  }
1131 
1132  MACH3LOG_INFO("Umbrella sampling outputs created (make sure to check for convergence issues)!");
1133  MACH3LOG_INFO("Output written to: {}", config.output_file);
1134  MACH3LOG_INFO("Combined tree contains {} entries with umbrella weights.", total_entries);
1135 }
1136 
1137 // Main function for compiled version
1138 int main(int argc, char *argv[]) {
1140  std::string config_file = "umbrella_config.yaml";
1141  if (argc > 1) {
1142  config_file = argv[1];
1143  }
1144 
1145  MACH3LOG_INFO("Running compiled version with OpenMP support");
1146  try {
1147  UmbrellaSolver(config_file);
1148  } catch (const std::exception &e) {
1149  MACH3LOG_ERROR("Error: {}", e.what());
1150  return 1;
1151  }
1152 
1153  // Ensure all OpenMP threads are properly terminated
1154  #ifdef MULTITHREAD
1155  #pragma omp barrier
1156  #endif
1157 
1158  return 0;
1159 }
#define _MaCh3_Safe_Include_Start_
KS: Avoiding warning checking for headers.
Definition: Core.h:126
#define _MaCh3_Safe_Include_End_
#define MACH3LOG_ERROR
Definition: MaCh3Logger.h:37
#define MACH3LOG_INFO
Definition: MaCh3Logger.h:35
void SetMaCh3LoggerFormat()
Set messaging format of the logger.
Definition: MaCh3Logger.h:60
#define MACH3LOG_WARN
Definition: MaCh3Logger.h:36
std::vector< double > getZDiffs(const std::vector< double > &z_current, const std::vector< double > &z_prev)
int main(int argc, char *argv[])
double summedWindowsWeighted(double x, const std::vector< WindowConfig > &windows, const std::vector< double > &z_values)
double vonMisesWindow(double x, double center, double kappa)
double generalisedGaussian2(double x, double mean, double width)
void UmbrellaSolver(const std::string &config_file)
bool checkConvergence(const std::vector< double > &z_current, const std::vector< double > &z_prev, double tolerance)
A few different convergence checks.
UmbrellaConfig parseYAMLConfig(const std::string &filename)
YAML-based config parser using yaml-cpp library.
bool checkConvergenceStalled(const std::vector< double > &z_current, const std::vector< double > &z_prev, double tolerance)
double GetMulticanonicalWeightGenGaussian(double deltacp, double mean, double width)
std::vector< std::vector< std::vector< double > > > buildWindowCache(const std::vector< WindowConfig > &windows, const std::vector< std::vector< double >> &samples, bool use_openmp=true)
double gaussianWindow(double x, double center, double width)
_MaCh3_Safe_Include_Start_ _MaCh3_Safe_Include_End_ bool debug_mode
std::vector< double > zSolver(const std::vector< double > &z_current, const std::vector< WindowConfig > &windows, const std::vector< std::vector< double >> &samples, const std::vector< std::vector< std::vector< double >>> &window_cache, bool use_openmp=true, bool verbose=false, [[maybe_unused]] int *total_lines=nullptr)
std::vector< std::vector< double > > calcFmatrix(std::vector< double > &z_current, const std::vector< WindowConfig > &windows, const std::vector< std::vector< double >> &samples, const std::vector< std::vector< std::vector< double >>> &window_cache)
TMacro YAMLtoTMacro(const YAML::Node &yaml_node, const std::string &name)
Convert a YAML node to a ROOT TMacro object.
Definition: YamlHelper.h:167
#define M3OpenConfig(filename)
Macro to simplify calling LoadYaml with file and line info.
Definition: YamlHelper.h:590
Custom exception class used throughout MaCh3.
TFile * Open(const std::string &Name, const std::string &Type, const std::string &File, const int Line)
Opens a ROOT file with the given name and mode.
constexpr double UmbrellaGaussianNormFactor
@ kGaussian
Assumes gaussian prior.
std::string variable_of_interest
std::vector< WindowConfig > windows
std::string output_file
std::string dynamic_pattern
std::string name
std::string input_file
double vonMises_kappa
M3::BiasFunction umbrellaBiasFunction