1"""Public Python interface to the statistical analysis backend.
3``StatisticInterface`` is a high-level wrapper around the C++ statistic manager.
4It connects a configured ``ObservableInterface`` to uncertainty propagation,
5maximum-likelihood fitting, confidence-contour computation and likelihood scans.
8from __future__
import annotations
10from dataclasses
import dataclass, field
12from pathlib
import Path
13from typing
import Dict, List, Mapping, Optional, Sequence
15from pyhyperiso.phyperiso.pyhyperiso
import statistic
as st
19from pyhyperiso.core.Statistic.ExperimentObs
import ExperimentObs
20from pyhyperiso.core.Statistic.GaussianSummary
import GaussianSummary
21from pyhyperiso.core.Statistic.MCResult
import MCResult
22from pyhyperiso.core.Statistic.StatisticConfig
import StatisticConfig
26 """Strategy used to reduce a fit with more than two parameters to 2D.
29 SLICE: Vary the selected axes while keeping the other fit parameters at
30 their best-fit values.
31 FREE_PROJECTION: Profile all non-displayed fit parameters freely.
32 PRIOR_CONSTRAINED_PROJECTION: Profile non-displayed fit parameters with
33 Gaussian constraints derived from the global fit.
36 SLICE = st.ProfilingMethod.SLICE
37 FREE_PROJECTION = st.ProfilingMethod.FREE_PROJECTION
38 PRIOR_CONSTRAINED_PROJECTION = st.ProfilingMethod.PRIOR_CONSTRAINED_PROJECTION
41 """Convert to the bound C++ ``ProfilingMethod`` enum."""
46 """Algorithm used to extract the requested confidence contour."""
48 AMS = st.ContourAlgorithm.AMS
49 MINUIT = st.ContourAlgorithm.MINUIT
52 """Convert to the bound C++ ``ContourAlgorithm`` enum."""
57 """Backend used while profiling hidden fit or nuisance parameters."""
59 MINUIT = st.ProfileBackend.MINUIT
60 LAPLACE_NUISANCE = st.ProfileBackend.LAPLACE_NUISANCE
63 """Convert to the C++ contour ``ProfileBackend`` enum."""
69ProfileBackend = ProfilerMode
73 """Validate a typed argument and return it unchanged."""
74 if not isinstance(value, typ):
75 raise TypeError(f
"{name} must be {typ.__name__}, received {type(value)!r}.")
80 """Convert a Python ``ParamId`` to C++."""
81 return _require(pid, ParamId,
"ParamId").to_cpp()
85 """Convert a C++ ``ParamId`` to the Python wrapper."""
86 return ParamId.from_cpp(cpp_obj)
90 """Convert a C++ ``map<ParamId, double>`` to a Python dictionary."""
95 """Convert a Python parameter-value mapping to C++."""
96 return {
_cpp_param_id(k): float(v)
for k, v
in values.items()}
100 """Convert a nested C++ parameter matrix map to Python dictionaries."""
105 """Convert a Python ``ExperimentObs`` to its bound C++ value."""
106 return _require(obs, ExperimentObs,
"ExperimentObs").to_cpp()
110 """Convert a C++ ``map<ExperimentObs, double>`` to Python."""
111 return {ExperimentObs.from_cpp(k): float(v)
for k, v
in dict(cpp_map).items()}
116) -> Dict[ExperimentObs, Dict[ExperimentObs, float]]:
117 """Convert a nested experimental-observable correlation map to Python."""
120 for k, v
in dict(cpp_map).items()
125 """Convert a Python profiling method to C++."""
126 return _require(value, ProfilingMethod,
"profiling_method").to_cpp()
130 """Convert a Python profile backend to C++."""
131 return _require(value, ProfilerMode,
"profile_backend").to_cpp()
135 """Convert a Python contour algorithm to C++."""
136 return _require(value, ContourAlgorithm,
"contour_algorithm").to_cpp()
139@dataclass(frozen=True)
141 """Maximum-likelihood fit result keyed by Python parameter identifiers.
144 fit_ok: Whether the C++ fit result is considered usable.
145 p_hat: Best-fit values for fit parameters.
146 eta_hat: Profiled nuisance values at the best-fit point.
147 p_hat_std: Standard deviations for fit parameters, usually from the
149 p_correlations: Fit-parameter correlation matrix as nested maps.
150 ell_hat: Minimum negative log-likelihood value.
154 p_hat: Dict[ParamId, float] = field(default_factory=dict)
155 eta_hat: Dict[ParamId, float] = field(default_factory=dict)
156 p_hat_std: Dict[ParamId, float] = field(default_factory=dict)
157 p_correlations: Dict[ParamId, Dict[ParamId, float]] = field(default_factory=dict)
162 """Create a Python fit result from the bound C++ result."""
164 fit_ok=bool(cpp_obj.fit_ok),
169 ell_hat=float(cpp_obj.ell_hat),
173 """Convert this result to the bound C++ representation."""
174 cpp = st.FitResultWithMaps()
175 cpp.fit_ok = bool(self.
fit_ok)
179 cpp.p_correlations = {
182 cpp.ell_hat = float(self.
ell_hat)
186@dataclass(frozen=True)
188 """Low-level maximum-likelihood fitter options.
190 This mirrors the C++ ``MLFitOptions`` struct. In most user workflows these
191 settings are configured through :class:`StatisticConfig`; this class is kept
192 available for direct conversions and advanced integrations.
195 run_hesse: Ask the minimizer to compute a covariance/HESSE estimate.
196 request_minos: Request MINOS if the compiled backend supports it.
197 verbose: Enable verbose minimizer output.
198 strategy: Backend strategy; ``0`` means backend default.
199 max_fcn: Maximum number of function calls; ``0`` means backend default.
200 tolerance: Minimizer tolerance; ``0.0`` means backend default.
201 allow_profile_hessian_fallback: Allow numerical profile-Hessian fallback.
202 profile_hessian_step_scale: Step scale for the fallback Hessian.
203 profile_hessian_eig_floor_rel: Relative eigenvalue floor for Hessian
205 trace_first_evals: Print the first likelihood evaluations.
206 trace_max_evals: Maximum number of traced evaluations.
209 run_hesse: bool =
True
210 request_minos: bool =
False
211 verbose: bool =
False
214 tolerance: float = 0.0
215 allow_profile_hessian_fallback: bool =
True
216 profile_hessian_step_scale: float = 1.0
217 profile_hessian_eig_floor_rel: float = 1e-8
218 trace_first_evals: bool =
False
219 trace_max_evals: int = 25
223 """Create Python fitter options from C++ options."""
225 run_hesse=bool(cpp_obj.run_hesse),
226 request_minos=bool(cpp_obj.request_minos),
227 verbose=bool(cpp_obj.verbose),
228 strategy=int(cpp_obj.strategy),
229 max_fcn=int(cpp_obj.max_fcn),
230 tolerance=float(cpp_obj.tolerance),
231 allow_profile_hessian_fallback=bool(cpp_obj.allow_profile_hessian_fallback),
232 profile_hessian_step_scale=float(cpp_obj.profile_hessian_step_scale),
233 profile_hessian_eig_floor_rel=float(cpp_obj.profile_hessian_eig_floor_rel),
234 trace_first_evals=bool(cpp_obj.trace_first_evals),
235 trace_max_evals=int(cpp_obj.trace_max_evals),
239 """Convert this options object to C++."""
240 cpp = st.MLFitOptions()
243 cpp.verbose = bool(self.
verbose)
245 cpp.max_fcn = int(self.
max_fcn)
255@dataclass(frozen=True)
257 """Options controlling confidence-contour computation.
260 profiling_method: How the two-dimensional contour fixes or profiles
262 profile_backend: Backend used to evaluate profiled NLL points.
263 primary_contour_method: Contour extraction algorithm.
264 fallback_contour_method: Optional fallback algorithm used by the C++
265 engine if the primary extraction fails.
266 resolution: Resolution parameter passed to the contour extractor.
269 >>> opts = ContourOptions(profile_backend=ProfilerMode.LAPLACE_NUISANCE, resolution=60)
272 profiling_method: ProfilingMethod = ProfilingMethod.SLICE
273 profile_backend: ProfilerMode = ProfilerMode.LAPLACE_NUISANCE
274 primary_contour_method: ContourAlgorithm = ContourAlgorithm.MINUIT
275 fallback_contour_method: Optional[ContourAlgorithm] =
None
280 """Create Python contour options from C++ options."""
285 fallback_contour_method=
None
286 if cpp_obj.fallback_contour_method
is None
288 resolution=int(cpp_obj.resolution),
292 """Convert this contour options object to C++."""
293 cpp = st.ContourOptions()
297 cpp.fallback_contour_method = (
307 """Python view of a C++ confidence contour.
309 The bound object exposes one or more disconnected paths. Each path is a
310 sequence of ``(x, y)`` points in the plane of the two displayed fit
314 __slots__ = (
"_cpp_obj",)
321 """Create a Python contour view from a bound C++ contour object."""
325 def paths(self) -> List[List[tuple[float, float]]]:
326 """Return all disconnected contour paths as ``(x, y)`` coordinates."""
328 [(float(point[0]), float(point[1]))
for point
in path]
for path
in self.
_cpp_obj.paths
333 """Return the likelihood or confidence level represented by the contour."""
338 """Report whether the native contour extraction completed successfully."""
342 """Return the underlying C++ contour object."""
346 return f
"Contour(success={self.success}, level={self.level:.6g}, paths={len(self.paths)})"
349@dataclass(frozen=True)
351 """One grid point of a two-dimensional likelihood scan.
354 x: Value of the first scanned parameter.
355 y: Value of the second scanned parameter.
356 nll: Absolute negative log-likelihood value.
357 delta_nll: Difference with respect to the scan reference point.
363 delta_nll: float = 0.0
366 def from_cpp(cls, cpp_obj) -> "LikelihoodScanPoint":
367 """Create a Python scan point from a bound C++ scan point."""
371 nll=float(cpp_obj.nll),
372 delta_nll=float(cpp_obj.delta_nll),
376 """Convert this scan point to C++."""
377 cpp = st.LikelihoodScanPoint()
378 cpp.x = float(self.
x)
379 cpp.y = float(self.
y)
380 cpp.nll = float(self.
nll)
385@dataclass(frozen=True)
387 """Rectangular two-dimensional likelihood scan result.
390 x_param: First scanned fit parameter.
391 y_param: Second scanned fit parameter.
392 x_center: Reference value used to center the scan in x.
393 y_center: Reference value used to center the scan in y.
394 nx: Number of x grid points.
395 ny: Number of y grid points.
396 points: Flattened list of scan points.
401 x_center: float = 0.0
402 y_center: float = 0.0
405 points: List[LikelihoodScanPoint] = field(default_factory=list)
408 """Validate scan parameter identifiers."""
413 def from_cpp(cls, cpp_obj) -> "LikelihoodScanGrid":
414 """Create a Python scan grid from the bound C++ result."""
418 x_center=float(cpp_obj.x_center),
419 y_center=float(cpp_obj.y_center),
422 points=[LikelihoodScanPoint.from_cpp(p)
for p
in cpp_obj.points],
426 """Convert this scan grid to the bound C++ representation."""
427 cpp = st.LikelihoodScanGrid()
432 cpp.nx = int(self.
nx)
433 cpp.ny = int(self.
ny)
434 cpp.points = [p.to_cpp()
for p
in self.
points]
439 """High-level Python facade for statistical fits and scans.
442 config: Statistic configuration. Python-only fields such as ``p_specs``
443 and ``selected_experiments`` are used by this wrapper.
444 observable_interface: Observable interface already configured with the
445 observables to compute.
448 A typical workflow is::
450 stat_cfg = StatisticConfig(MC_draws=1000)
451 stat_cfg.p_specs = [my_param_id]
452 stat = StatisticInterface(stat_cfg, observable_interface=obs_int)
454 fit = stat.compute_MLE()
455 print(fit.fit_ok, fit.p_hat)
457 contour = stat.compute_confidence_contour(
461 bounds=[-1.0, 1.0, -1.0, 1.0],
465 def __init__(self, config: StatisticConfig, observable_interface: ObservableInterface) ->
None:
466 """Create the C++ statistic interface and apply initial selections."""
467 _require(config, StatisticConfig,
"config")
468 _require(observable_interface, ObservableInterface,
"observable_interface")
470 self.
_cpp = st.StatisticInterface(config.to_cpp(), observable_interface._to_cpp())
475 """Return the underlying C++ statistic interface."""
479 """Restrict the statistic manager to one experiment.
482 experiment: Experiment name as stored in the experimental database.
487 """Restrict the statistic manager to a set of experiments.
490 experiments: Experiment names to keep.
495 """Clear any experiment selection and use all available experiments."""
499 """Return whether an experiment filter is currently active."""
503 """Return the currently selected experiment names."""
507 """Restrict statistics to exact experiment/observable/bin entries.
510 observables: Exact measurements to retain. Each item combines an
511 experiment label with a binned observable identifier.
516 """Clear the exact experimental-observable selection."""
520 """Return whether an exact measurement filter is active."""
524 """Return the active exact measurement selection."""
528 """Reload default and user nuisance specifications in the C++ manager."""
532 """Use a custom user nuisance-specification file.
535 user_yaml_path: Path to the YAML file overriding default nuisance
541 """Return to the default user nuisance-specification path."""
544 def update_cache(self, p_specs: Optional[Sequence[ParamId]] =
None) ->
None:
545 """Refresh the C++ statistic cache.
548 p_specs: Fit parameters to detach from the nuisance set. When
549 omitted, ``self.config.p_specs`` is used.
551 p_specs = self.
config.p_specs
if p_specs
is None else p_specs
555 """Propagate nuisance uncertainties and return Gaussian summaries.
558 Mapping from observable id to Gaussian or split-Gaussian summary.
561 BinnedObservableId.from_cpp(k): GaussianSummary.from_cpp(v)
566 """Run Monte-Carlo uncertainty propagation and keep raw samples.
569 ``MCResult`` containing raw samples, summaries and covariance.
573 def compute_MLE(self, p_specs: Optional[Sequence[ParamId]] =
None) -> FitResultWithMaps:
574 """Compute the maximum-likelihood estimate for selected fit parameters.
577 p_specs: Fit parameters. When omitted, ``self.config.p_specs`` is
581 Fit result containing best-fit values, profiled nuisances,
582 uncertainties, correlations and minimum NLL.
585 Exception: Propagates C++ errors raised by cache construction,
586 likelihood evaluation or minimization.
588 p_specs = self.
config.p_specs
if p_specs
is None else p_specs
589 return FitResultWithMaps.from_cpp(
598 bounds: Sequence[float],
599 options: Optional[ContourOptions] =
None,
601 """Compute a two-dimensional confidence contour.
604 p1: First fit parameter.
605 p2: Second fit parameter.
606 z: Gaussian-equivalent confidence level, for example ``1.0`` for an
607 approximate one-sigma contour.
608 bounds: Four values ``[xmin, xmax, ymin, ymax]`` defining the search
610 options: Optional contour/profiling options. Defaults to
611 ``ContourOptions()``.
614 ``Contour`` wrapper exposing ``paths``, ``level`` and ``success``.
617 ValueError: If ``bounds`` does not contain exactly four values.
618 Exception: Propagates backend failures, for example if MLE has not
619 been computed before requesting the contour.
622 raise ValueError(
"bounds must contain exactly 4 values: [xmin, xmax, ymin, ymax].")
626 else _require(options, ContourOptions,
"options").to_cpp()
628 return Contour.from_cpp(
633 [float(x)
for x
in bounds],
639 """Prepare and cache a likelihood object for manual two-dimensional scans.
642 p_specs: Fit parameters used by the likelihood. Defaults to
643 ``self.config.p_specs``.
645 p_specs = self.
config.p_specs
if p_specs
is None else p_specs
649 self, p_hat: Mapping[ParamId, float], eta_hat: Mapping[ParamId, float]
651 """Override the scan reference point manually.
654 p_hat: Fit-parameter values used as scan center/reference.
655 eta_hat: Nuisance values used as scan reference.
669 ) -> LikelihoodScanGrid:
670 """Evaluate the likelihood on a rectangular grid around the current point.
673 p1: First scanned parameter.
674 p2: Second scanned parameter.
675 x_half_width: Half-width of the scan range in the first parameter.
676 y_half_width: Half-width of the scan range in the second parameter.
677 nx: Number of grid points in the x direction.
678 ny: Number of grid points in the y direction.
681 A ``LikelihoodScanGrid`` containing all evaluated points.
683 return LikelihoodScanGrid.from_cpp(
695 """Save a likelihood scan grid to a CSV file.
698 path: Output CSV path.
699 grid: Scan grid returned by :meth:`scan_likelihood_around_current_point`.
702 str(path),
_require(grid, LikelihoodScanGrid,
"grid").to_cpp()
706 """Return all selected observable dependencies after nuisance pruning."""
710 """Return dependencies currently visible to the Statistic manager.
712 This method is useful for runtime/lambda observables: it verifies that
713 dependencies declared on ``LambdaObservableConfig`` and propagated from
714 custom Wilson lambdas reached the statistic layer.
718 def get_p_specs(self, p_specs: Optional[Sequence[ParamId]] =
None) -> Dict[ParamId, float]:
719 """Return central values for selected fit parameters.
722 p_specs: Fit parameters to query. Defaults to ``self.config.p_specs``.
724 p_specs = self.
config.p_specs
if p_specs
is None else p_specs
728 """Return the nuisance correlation matrix as nested parameter maps."""
732 """Return experimental-observable correlations as nested maps."""
736 """Return experimental central values used by the statistic manager."""
740 """Print the current C++ statistic cache for debugging."""
745 "StatisticInterface",
754 "LikelihoodScanPoint",
755 "LikelihoodScanGrid",
"ContourOptions" from_cpp(cls, cpp_obj)
ProfilingMethod profiling_method
Optional fallback_contour_method
ContourAlgorithm primary_contour_method
ProfilerMode profile_backend
None __init__(self, cpp_obj)
"Contour" from_cpp(cls, cpp_obj)
List[List[tuple[float, float]]] paths(self)
"FitResultWithMaps" from_cpp(cls, cpp_obj)
"LikelihoodScanGrid" from_cpp(cls, cpp_obj)
"LikelihoodScanPoint" from_cpp(cls, cpp_obj)
float profile_hessian_step_scale
float profile_hessian_eig_floor_rel
bool allow_profile_hessian_fallback
"MLFitOptions" from_cpp(cls, cpp_obj)
Dict[ExperimentObs, float] get_obs_exp(self)
None set_manual_scan_point(self, Mapping[ParamId, float] p_hat, Mapping[ParamId, float] eta_hat)
FitResultWithMaps compute_MLE(self, Optional[Sequence[ParamId]] p_specs=None)
Dict[ParamId, float] get_all_obss_deps(self)
Contour compute_confidence_contour(self, ParamId p1, ParamId p2, float z, Sequence[float] bounds, Optional[ContourOptions] options=None)
MCResult compute_uncertainties_and_sampling(self)
Dict[ParamId, float] get_active_observable_dependencies(self)
Dict[ExperimentObs, Dict[ExperimentObs, float]] get_all_obs_correlations(self)
None set_nuisance_user_file(self, str|Path user_yaml_path)
LikelihoodScanGrid scan_likelihood_around_current_point(self, ParamId p1, ParamId p2, float x_half_width, float y_half_width, int nx, int ny)
None update_cache(self, Optional[Sequence[ParamId]] p_specs=None)
None prepare_likelihood_for_scan(self, Optional[Sequence[ParamId]] p_specs=None)
List[str] selected_experiments(self)
Dict[ParamId, Dict[ParamId, float]] get_all_correlations(self)
None __init__(self, StatisticConfig config, ObservableInterface observable_interface)
None select_experiment_observables(self, Sequence[ExperimentObs] observables)
None select_experiments_all(self)
None clear_nuisance_user_file(self)
None reload_nuisance_specs(self)
None select_experiment(self, str experiment)
Dict[ParamId, float] get_p_specs(self, Optional[Sequence[ParamId]] p_specs=None)
None select_experiments(self, Sequence[str] experiments)
None select_experiment_observables_all(self)
bool has_experiment_observable_selection(self)
Dict[BinnedObservableId, GaussianSummary] compute_uncertainties(self)
List[ExperimentObs] selected_experiment_observables(self)
bool has_experiment_selection(self)
None save_likelihood_scan_csv(self, str|Path path, LikelihoodScanGrid grid)
_cpp_profile_backend(ProfilerMode value)
Dict[ExperimentObs, float] _experiment_float_map_from_cpp(cpp_map)
ParamId _param_from_cpp(cpp_obj)
_cpp_param_id(ParamId pid)
_require(value, typ, str name)
_param_float_map_to_cpp(Mapping[ParamId, float] values)
Dict[ParamId, Dict[ParamId, float]] _param_nested_float_map_from_cpp(cpp_map)
_cpp_experiment_obs(ExperimentObs obs)
_cpp_profiling_method(ProfilingMethod value)
Dict[ParamId, float] _param_float_map_from_cpp(cpp_map)
_cpp_contour_algorithm(ContourAlgorithm value)
Dict[ExperimentObs, Dict[ExperimentObs, float]] _experiment_nested_float_map_from_cpp(cpp_map)