autoemulate 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. {autoemulate-0.3.2 → autoemulate-0.3.3}/PKG-INFO +3 -1
  2. autoemulate-0.3.3/autoemulate/mcmc.py +348 -0
  3. autoemulate-0.3.3/autoemulate/mcmc_dashboard.py +818 -0
  4. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/simulations/epidemic.py +30 -0
  5. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/simulations/projectile.py +30 -0
  6. {autoemulate-0.3.2 → autoemulate-0.3.3}/pyproject.toml +3 -2
  7. {autoemulate-0.3.2 → autoemulate-0.3.3}/LICENSE +0 -0
  8. {autoemulate-0.3.2 → autoemulate-0.3.3}/README.md +0 -0
  9. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/__init__.py +0 -0
  10. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/compare.py +0 -0
  11. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/cross_validate.py +0 -0
  12. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/data_splitting.py +0 -0
  13. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/datasets.py +0 -0
  14. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/__init__.py +0 -0
  15. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/conditional_neural_process.py +0 -0
  16. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/conditional_neural_process_attn.py +0 -0
  17. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gaussian_process.py +0 -0
  18. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gaussian_process_mogp.py +0 -0
  19. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gaussian_process_mt.py +0 -0
  20. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gaussian_process_sklearn.py +0 -0
  21. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gaussian_process_utils/__init__.py +0 -0
  22. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gaussian_process_utils/early_stopping_criterion.py +0 -0
  23. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gaussian_process_utils/poly_mean.py +0 -0
  24. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gaussian_process_utils/polynomial_features.py +0 -0
  25. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/gradient_boosting.py +0 -0
  26. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/light_gbm.py +0 -0
  27. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/neural_net_sk.py +0 -0
  28. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/neural_networks/__init__.py +0 -0
  29. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/neural_networks/cnp_module.py +0 -0
  30. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/neural_networks/cnp_module_attn.py +0 -0
  31. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/neural_networks/datasets.py +0 -0
  32. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/neural_networks/gp_module.py +0 -0
  33. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/neural_networks/losses.py +0 -0
  34. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/polynomials.py +0 -0
  35. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/radial_basis_functions.py +0 -0
  36. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/random_forest.py +0 -0
  37. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/emulators/support_vector_machines.py +0 -0
  38. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/__init__.py +0 -0
  39. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/compare.py +0 -0
  40. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/data/preprocessors.py +0 -0
  41. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/data/utils.py +0 -0
  42. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/device.py +0 -0
  43. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/emulators/__init__.py +0 -0
  44. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/emulators/base.py +0 -0
  45. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/emulators/gaussian_process/__init__.py +0 -0
  46. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/emulators/gaussian_process/exact.py +0 -0
  47. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/emulators/lightgbm.py +0 -0
  48. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/emulators/neural_processes/conditional_neural_process.py +0 -0
  49. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/emulators/random_forest.py +0 -0
  50. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/emulators/svm.py +0 -0
  51. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/exploratory/active_matter/README.md +0 -0
  52. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/exploratory/active_matter/active_matter.ipynb +0 -0
  53. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/learners/__init__.py +0 -0
  54. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/learners/base.py +0 -0
  55. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/learners/membership.py +0 -0
  56. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/learners/pool.py +0 -0
  57. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/learners/stream.py +0 -0
  58. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/model_selection.py +0 -0
  59. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/simulations/__init__.py +0 -0
  60. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/tuner.py +0 -0
  61. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental/types.py +0 -0
  62. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/experimental_design.py +0 -0
  63. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/history_matching.py +0 -0
  64. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/history_matching_dashboard.py +0 -0
  65. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/hyperparam_searching.py +0 -0
  66. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/logging_config.py +0 -0
  67. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/metrics.py +0 -0
  68. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/model_processing.py +0 -0
  69. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/model_registry.py +0 -0
  70. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/plotting.py +0 -0
  71. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/preprocess_target.py +0 -0
  72. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/printing.py +0 -0
  73. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/save.py +0 -0
  74. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/sensitivity_analysis.py +0 -0
  75. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/simulations/__init__.py +0 -0
  76. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/simulations/base.py +0 -0
  77. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/simulations/circ_utils.py +0 -0
  78. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/simulations/flow_functions.py +0 -0
  79. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/simulations/naghavi_cardiac_ModularCirc.py +0 -0
  80. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/simulations/reaction_diffusion.py +0 -0
  81. {autoemulate-0.3.2 → autoemulate-0.3.3}/autoemulate/utils.py +0 -0
  82. {autoemulate-0.3.2 → autoemulate-0.3.3}/misc/AE_logo_final.png +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: autoemulate
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A python package for semi-automated emulation
5
5
  License: MIT
6
6
  Author: AutoEmulate contributors (see our GitHub page)
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python :: 3.11
12
12
  Classifier: Programming Language :: Python :: 3.12
13
13
  Provides-Extra: docs
14
14
  Requires-Dist: anytree (>=2.12.1,<3.0.0)
15
+ Requires-Dist: corner (>=2.2.3,<3.0.0)
15
16
  Requires-Dist: gpytorch (>=1.12,<2.0)
16
17
  Requires-Dist: iprogress (>=0.4,<0.5)
17
18
  Requires-Dist: ipywidgets (>=8.1.2,<9.0.0)
@@ -21,6 +22,7 @@ Requires-Dist: matplotlib (>=3.7.2,<4.0.0)
21
22
  Requires-Dist: mogp-emulator (>=0.7.2,<0.8.0)
22
23
  Requires-Dist: numpy (>=1.24,<2.0)
23
24
  Requires-Dist: pandas (>=2.1,<3.0)
25
+ Requires-Dist: pyro-ppl (>=1.9.1,<2.0.0)
24
26
  Requires-Dist: salib (>=1.5.1,<2.0.0)
25
27
  Requires-Dist: scikit-learn (>=1.3.0,<1.6.0)
26
28
  Requires-Dist: scipy (>=1.11.3,<2.0.0)
@@ -0,0 +1,348 @@
1
+ from typing import Dict
2
+ from typing import List
3
+ from typing import Optional
4
+ from typing import Tuple
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ import pyro
9
+ import pyro.distributions as dist
10
+ import torch
11
+ from pyro.infer import MCMC
12
+ from pyro.infer import NUTS
13
+
14
+
15
+ class MCMCCalibrator:
16
+ def __init__(
17
+ self,
18
+ emulator,
19
+ sensitivity_results: pd.DataFrame,
20
+ observations: Dict[str, Tuple[float, float]],
21
+ parameter_bounds: Dict[str, List[float]],
22
+ nroy_samples: Optional[np.ndarray] = None,
23
+ nroy_indices: Optional[List[int]] = None,
24
+ all_samples: Optional[np.ndarray] = None,
25
+ top_n_params: int = 5,
26
+ device: str = "cpu",
27
+ ):
28
+ self.emulator = emulator
29
+ self.observations = observations
30
+ self.device = device
31
+ self.parameter_bounds = parameter_bounds
32
+ self.nroy_samples = nroy_samples
33
+ self.nroy_indices = nroy_indices
34
+ self.all_samples = all_samples
35
+
36
+ # Get important parameters from SA and set up bounds
37
+ self.important_params = self._get_important_parameters(
38
+ sensitivity_results, top_n_params
39
+ )
40
+
41
+ self.param_names = list(parameter_bounds.keys())
42
+ self.important_param_indices = [
43
+ self.param_names.index(p) for p in self.important_params
44
+ ]
45
+
46
+ # Set up parameter bounds (refined by NROY if available)
47
+ self.calibration_bounds = self._setup_calibration_bounds()
48
+
49
+ print(f"Calibrating top {len(self.important_params)} parameters:")
50
+ print("\n".join(f" - {param}" for param in self.important_params))
51
+
52
+ def _get_important_parameters(
53
+ self, sensitivity_results: pd.DataFrame, top_n: int
54
+ ) -> List[str]:
55
+ """Get top parameters based on total Sobol indices."""
56
+ st_results = sensitivity_results[sensitivity_results["index"] == "ST"]
57
+ return (
58
+ st_results.groupby("parameter")["value"]
59
+ .mean()
60
+ .nlargest(top_n)
61
+ .index.tolist()
62
+ )
63
+
64
+ def _setup_calibration_bounds(self) -> Dict[str, List[float]]:
65
+ """Set up calibration bounds, refining with NROY samples if available."""
66
+ if not self._has_valid_nroy_samples():
67
+ print("No valid NROY samples. Using original parameter bounds.")
68
+ return {p: self.parameter_bounds[p] for p in self.important_params}
69
+
70
+ print(f"Using {len(self.nroy_samples)} NROY samples to refine bounds")
71
+ return self._refine_bounds_from_nroy()
72
+
73
+ def _has_valid_nroy_samples(self) -> bool:
74
+ """Check if NROY samples are valid and usable."""
75
+ if self.nroy_samples is None:
76
+ return False
77
+ if len(self.nroy_samples) == 0:
78
+ print("Warning: Empty NROY samples provided.")
79
+ return False
80
+ if self.nroy_samples.shape[1] != len(self.parameter_bounds):
81
+ print(
82
+ f"Warning: NROY samples has {self.nroy_samples.shape[1]} parameters, "
83
+ f"but expected {len(self.parameter_bounds)}."
84
+ )
85
+ return False
86
+ return True
87
+
88
+ def _refine_bounds_from_nroy(self) -> Dict[str, List[float]]:
89
+ """Refine parameter bounds using NROY samples."""
90
+ refined_bounds = {}
91
+
92
+ for i, param_name in enumerate(self.param_names):
93
+ if param_name not in self.important_params:
94
+ continue
95
+
96
+ try:
97
+ nroy_values = self.nroy_samples[:, i]
98
+ if len(nroy_values) == 0:
99
+ raise ValueError("No NROY values")
100
+
101
+ # Calculate refined bounds with buffer
102
+ min_val = max(np.min(nroy_values), self.parameter_bounds[param_name][0])
103
+ max_val = min(np.max(nroy_values), self.parameter_bounds[param_name][1])
104
+
105
+ if min_val >= max_val:
106
+ raise ValueError("Degenerate bounds")
107
+
108
+ buffer = (max_val - min_val) * 0.05
109
+ refined_bounds[param_name] = [
110
+ max(min_val - buffer, self.parameter_bounds[param_name][0]),
111
+ min(max_val + buffer, self.parameter_bounds[param_name][1]),
112
+ ]
113
+
114
+ print(
115
+ f"Parameter {param_name}: {self.parameter_bounds[param_name]} -> {refined_bounds[param_name]}"
116
+ )
117
+
118
+ except Exception as e:
119
+ print(
120
+ f"Error refining bounds for {param_name}: {e}. Using original bounds."
121
+ )
122
+ refined_bounds[param_name] = self.parameter_bounds[param_name]
123
+
124
+ return refined_bounds
125
+
126
+ def _get_initial_values(self) -> Optional[Dict[str, torch.Tensor]]:
127
+ """Get initial MCMC values, preferring NROY samples if available."""
128
+ if not self._has_valid_nroy_samples():
129
+ return None
130
+
131
+ try:
132
+ # Sample from NROY points
133
+ sample_idx = np.random.choice(len(self.nroy_samples))
134
+ selected_sample = self.nroy_samples[sample_idx]
135
+
136
+ init_values = {}
137
+ for param_name in self.important_params:
138
+ param_idx = self.param_names.index(param_name)
139
+ # Convert to tensor and ensure it's on the correct device
140
+ init_values[param_name] = torch.tensor(
141
+ float(selected_sample[param_idx]),
142
+ device=self.device,
143
+ dtype=torch.float32,
144
+ )
145
+
146
+ return init_values
147
+
148
+ except Exception as e:
149
+ print(
150
+ f"Error getting NROY initial values: {e}. Using default initialization."
151
+ )
152
+ return None
153
+
154
+ def _create_full_params(self, reduced_params: torch.Tensor) -> torch.Tensor:
155
+ """Create full parameter vector with defaults for non-calibrated params."""
156
+ full_params = torch.zeros(len(self.parameter_bounds), device=self.device)
157
+
158
+ # Set calibrated parameters
159
+ for i, param_name in enumerate(self.important_params):
160
+ param_idx = self.param_names.index(param_name)
161
+ full_params[param_idx] = reduced_params[i]
162
+
163
+ # Set non-calibrated parameters to midpoint
164
+ for i, param_name in enumerate(self.param_names):
165
+ if param_name not in self.important_params:
166
+ bounds = self.parameter_bounds[param_name]
167
+ full_params[i] = (bounds[0] + bounds[1]) / 2
168
+
169
+ return full_params
170
+
171
+ def model(self):
172
+ """Pyro model for MCMC calibration."""
173
+ reduced_params = []
174
+
175
+ for param_name in self.important_params:
176
+ bounds = self.calibration_bounds[param_name]
177
+ param_value = pyro.sample(
178
+ param_name,
179
+ dist.Uniform(
180
+ torch.tensor(bounds[0], device=self.device),
181
+ torch.tensor(bounds[1], device=self.device),
182
+ ),
183
+ )
184
+ reduced_params.append(param_value)
185
+
186
+ # Create full parameter vector and get predictions
187
+ reduced_params_tensor = torch.stack(reduced_params)
188
+ full_params = self._create_full_params(reduced_params_tensor)
189
+
190
+ with torch.no_grad():
191
+ pred_mean = torch.tensor(
192
+ self.emulator.predict(
193
+ full_params.cpu().numpy().reshape(1, -1)
194
+ ).flatten(),
195
+ device=self.device,
196
+ )
197
+
198
+ # setup likelihood from observations
199
+
200
+ # Pre-convert observations to tensors
201
+ obs_means = torch.tensor(
202
+ [obs[0] for obs in self.observations.values()], device=self.device
203
+ )
204
+ obs_stds = torch.tensor(
205
+ [obs[1] for obs in self.observations.values()], device=self.device
206
+ )
207
+
208
+ # One-liner comparison
209
+ [
210
+ pyro.sample(
211
+ f"obs_{name}", dist.Normal(pred_mean[i], obs_stds[i]), obs=obs_means[i]
212
+ )
213
+ for i, name in enumerate(self.observations.keys())
214
+ ]
215
+
216
+ def run_mcmc(
217
+ self,
218
+ num_samples: int = 1000,
219
+ warmup_steps: int = 500,
220
+ num_chains: int = 1,
221
+ use_nroy_init: bool = True,
222
+ ) -> Dict:
223
+ """Run MCMC sampling with optional NROY initialization."""
224
+ nuts_kernel = NUTS(self.model)
225
+
226
+ # Get initial parameters if requested and available
227
+ initial_params = None
228
+ if use_nroy_init:
229
+ initial_params = self._get_initial_values()
230
+ if initial_params is None:
231
+ print("Using default MCMC initialization.")
232
+ else:
233
+ print("Initializing MCMC chains from NROY samples...")
234
+
235
+ mcmc = MCMC(
236
+ nuts_kernel,
237
+ num_samples=num_samples,
238
+ warmup_steps=warmup_steps,
239
+ num_chains=num_chains,
240
+ initial_params=initial_params,
241
+ )
242
+
243
+ print("Running MCMC...")
244
+ mcmc.run()
245
+
246
+ # Store and summarize results
247
+ self.mcmc_results = {
248
+ p: samples.cpu().numpy() for p, samples in mcmc.get_samples().items()
249
+ }
250
+ self.mcmc_summary = self._create_summary()
251
+
252
+ print("MCMC completed!\nPosterior Summary:")
253
+ print(self.mcmc_summary)
254
+ return self.mcmc_results
255
+
256
+ def _create_summary(self) -> pd.DataFrame:
257
+ """Create summary statistics for MCMC results."""
258
+ summary_data = []
259
+ for param, samples in self.mcmc_results.items():
260
+ summary_data.append(
261
+ {
262
+ "parameter": param,
263
+ "mean": np.mean(samples),
264
+ "std": np.std(samples),
265
+ **{
266
+ f"q{p}": np.percentile(samples, p)
267
+ for p in [2.5, 25, 50, 75, 97.5]
268
+ },
269
+ }
270
+ )
271
+ return pd.DataFrame(summary_data).round(4)
272
+
273
+ def get_calibrated_parameters(self) -> Dict[str, float]:
274
+ """Get posterior means of calibrated parameters."""
275
+ return {param: np.mean(samples) for param, samples in self.mcmc_results.items()}
276
+
277
+ def predict_with_uncertainty(
278
+ self, X_test: np.ndarray, n_posterior_samples: int = 100
279
+ ) -> Tuple[np.ndarray, np.ndarray]:
280
+ """Make predictions using posterior uncertainty."""
281
+ n_samples = len(next(iter(self.mcmc_results.values())))
282
+ sample_indices = np.random.choice(
283
+ n_samples, min(n_posterior_samples, n_samples), False
284
+ )
285
+
286
+ predictions = []
287
+ for sample_idx in sample_indices:
288
+ # Get calibrated parameters for this sample
289
+ calibrated_params = {
290
+ p: samples[sample_idx] for p, samples in self.mcmc_results.items()
291
+ }
292
+
293
+ # Make predictions for all test points
294
+ sample_predictions = []
295
+ for x_test in X_test:
296
+ param_vector = self._create_param_vector(x_test, calibrated_params)
297
+ pred = self.emulator.predict(param_vector.reshape(1, -1))
298
+ sample_predictions.append(pred)
299
+
300
+ predictions.append(np.vstack(sample_predictions))
301
+
302
+ predictions = np.array(predictions)
303
+ return np.mean(predictions, axis=0), np.std(predictions, axis=0)
304
+
305
+ def _create_param_vector(
306
+ self, x_test: np.ndarray, calibrated: Dict[str, float]
307
+ ) -> np.ndarray:
308
+ """Create parameter vector mixing test values and calibrated parameters."""
309
+ params = np.zeros(len(self.parameter_bounds))
310
+
311
+ for i, param_name in enumerate(self.param_names):
312
+ if param_name in calibrated:
313
+ params[i] = calibrated[param_name]
314
+ elif i < len(x_test):
315
+ params[i] = x_test[i]
316
+ else:
317
+ # Default to midpoint for missing parameters
318
+ bounds = self.parameter_bounds[param_name]
319
+ params[i] = (bounds[0] + bounds[1]) / 2
320
+
321
+ return params
322
+
323
+ def compare_with_nroy(self) -> Optional[pd.DataFrame]:
324
+ """Compare MCMC results with NROY bounds."""
325
+ if not self._has_valid_nroy_samples():
326
+ print("No valid NROY samples available for comparison")
327
+ return None
328
+
329
+ comparison_data = []
330
+ for param_name in self.important_params:
331
+ param_idx = self.param_names.index(param_name)
332
+ nroy_values = self.nroy_samples[:, param_idx]
333
+ mcmc_values = self.mcmc_results[param_name]
334
+
335
+ comparison_data.append(
336
+ {
337
+ "parameter": param_name,
338
+ "nroy_min": np.min(nroy_values),
339
+ "nroy_max": np.max(nroy_values),
340
+ "nroy_mean": np.mean(nroy_values),
341
+ "mcmc_mean": np.mean(mcmc_values),
342
+ "mcmc_std": np.std(mcmc_values),
343
+ "mcmc_q2.5": np.percentile(mcmc_values, 2.5),
344
+ "mcmc_q97.5": np.percentile(mcmc_values, 97.5),
345
+ }
346
+ )
347
+
348
+ return pd.DataFrame(comparison_data)