Source code for spey.base.model_config

"""Configuration class for Statistical Models"""

import copy
from dataclasses import dataclass
from typing import Dict, List, Optional, Tuple, Union


[docs] @dataclass class ModelConfig: r""" Container to hold certain properties of the backend and statistical model. This will ensure the consistency of the computation through out the package. Args: poi_index (:obj:`int`): index of the parameter of interest within the parameter list. minimum_poi (:obj:`float`): minimum value parameter of interest can take to ensure :math:`N^{\rm bkg}+\mu N^{\rm sig}\geq0`. This value can be set to :math:`-\infty` but note that optimiser will take it as a lower bound so such low value might effect the convergence of the optimisation algorithm especially for relatively flat objective surfaceses. Hence we suggest setting the minimum value to .. math:: \mu_{\rm min} = - \min\left( \frac{N^{\rm bkg}_i}{N^{\rm sig}_i} \right)\ ,\ i\in {\rm bins} suggested_init (:obj:`List[float]`): suggested initial parameters for the optimiser. suggested_bounds (:obj:`List[Tuple[float, float]]`): suggested parameter bounds for the optimiser. Returns: :obj:~spey.base.model_config.ModelConfig: Model configuration container for optimiser. """ poi_index: int """Index of the parameter of interest wihin the parameter list""" minimum_poi: float r""" minimum value parameter of interest can take to ensure :math:`N^{\rm bkg}+\mu N^{\rm sig}\geq0`. This value can be set to :math:`-\infty` but note that optimiser will take it as a lower bound so such low value might effect the convergence of the optimisation algorithm especially for relatively flat objective surfaceses. Hence we suggest setting the minimum value to .. math:: \mu_{\rm min} = - \min\left( \frac{N^{\rm bkg}_i}{N^{\rm sig}_i} \right)\ ,\ i\in {\rm bins} """ suggested_init: List[float] """Suggested initialisation for parameters""" suggested_bounds: List[Tuple[float, float]] """Suggested upper and lower bounds for parameters""" parameter_names: Optional[List[str]] = None """Names of the parameters""" suggested_fixed: Optional[List[bool]] = None """Suggested fixed values""" @property def npar(self) -> int: """Number of parameters""" return len(self.suggested_init) def fixed_poi_bounds( self, poi_value: Optional[float] = None ) -> List[Tuple[float, float]]: r""" Adjust the bounds for the parameter of interest for fixed POI fit. Args: poi_value (:obj:`Optional[float]`, default :obj:`None`): parameter of interest, :math:`\mu`. Returns: :obj:`List[Tuple[float, float]]`: Updated bounds. """ if poi_value is None: return self.suggested_bounds bounds = copy.deepcopy(self.suggested_bounds) if any(b is None for b in bounds[self.poi_index]): return bounds if not bounds[self.poi_index][0] < poi_value < bounds[self.poi_index][1]: bounds[self.poi_index] = ( self.minimum_poi if poi_value < 0.0 else 0.0, max([poi_value + 1, self.minimum_poi if poi_value < 0.0 else 0.0]), ) return bounds def resolve_poi_indices( self, poi_test: Union[float, Dict[Union[int, str], float]], ) -> Dict[int, float]: r""" Resolve ``poi_test`` to a mapping of ``{parameter_index: fixed_value}``. Args: poi_test (:obj:`Union[float, Dict[Union[int, str], float]]`): parameter of interest value, or a dictionary mapping POI indices (``int``) or names (``str``) to their fixed values. Raises: :obj:`ValueError`: If a string key cannot be found in :attr:`parameter_names`. Returns: :obj:`Dict[int, float]`: Mapping from parameter index to the value it should be fixed at. """ if not isinstance(poi_test, dict): return {self.poi_index: float(poi_test)} resolved: Dict[int, float] = {} for key, val in poi_test.items(): if isinstance(key, str): if self.parameter_names is None: raise ValueError( "Cannot resolve POI name: parameter_names not set in ModelConfig." ) if key not in self.parameter_names: raise ValueError( f"POI name '{key}' not found in parameter_names: " f"{self.parameter_names}" ) resolved[self.parameter_names.index(key)] = float(val) else: resolved[int(key)] = float(val) return resolved def fixed_poi_bounds_multi( self, poi_values: Optional[Dict[int, float]] = None ) -> List[Tuple[float, float]]: r""" Adjust bounds for multiple fixed parameters. Args: poi_values (:obj:`Optional[Dict[int, float]]`, default :obj:`None`): mapping of parameter index to fixed value. If ``None`` the suggested bounds are returned unchanged. Returns: :obj:`List[Tuple[float, float]]`: Updated bounds. """ if not poi_values: return self.suggested_bounds bounds = copy.deepcopy(self.suggested_bounds) for idx, val in poi_values.items(): if any(b is None for b in bounds[idx]): continue if not bounds[idx][0] < val < bounds[idx][1]: bounds[idx] = ( self.minimum_poi if val < 0.0 else 0.0, val + 1, ) return bounds def change_parameter_names(self, name_map: Dict[str, str]) -> None: """Change the parameter names""" new_names = [] for old, new in name_map.items(): if old not in self.parameter_names: raise ValueError(f"Parameter '{old}' does not exist.") if new in new_names: raise ValueError("Each name has to be unique.") new_names.append(new) self.parameter_names[self.parameter_names.index(old)] = new def rescale_poi_bounds( self, allow_negative_signal: bool = True, poi_upper_bound: Optional[float] = None ) -> List[Tuple[float, float]]: r""" Rescale bounds for POI. Args: allow_negative_signal (:obj:`bool`, default :obj:`True`): If :obj:`True` :math:`\hat\mu` value will be allowed to be negative. poi_upper_bound (:obj:`float`, default :obj:`None`): Maximum value POI can take during optimisation. Returns: :obj:`List[Tuple[float, float]]`: Updated bounds. """ bounds = copy.deepcopy(self.suggested_bounds) if poi_upper_bound: bounds[self.poi_index] = ( self.minimum_poi if allow_negative_signal else 0.0, poi_upper_bound, ) else: bounds[self.poi_index] = ( self.minimum_poi if allow_negative_signal else 0.0, bounds[self.poi_index], ) return bounds