# Licensed under a 3-clause BSD style license - see LICENSE.rst"""Model parameter classes."""importcollections.abcimportcopyimportitertoolsimportloggingimportnumpyasnpfromastropyimportunitsasufromastropy.tableimportTablefromgammapy.utils.interpolationimportinterpolation_scale__all__=["Parameter","Parameters"]log=logging.getLogger(__name__)def_get_parameters_str(parameters):str_=""forparinparameters:ifpar.name=="amplitude":value_format,error_format="{:10.2e}","{:7.1e}"else:value_format,error_format="{:10.3f}","{:7.2f}"line="\t{:21}{:8}: "+value_format+"\t{}{:<12s}\n"ifpar._link_label_ioisnotNone:name=par._link_label_ioelse:name=par.nameifpar.frozen:frozen,error="(frozen)","\t\t"else:frozen=""try:error="+/- "+error_format.format(par.error)exceptAttributeError:error=""str_+=line.format(name,frozen,par.value,error,par.unit)returnstr_.expandtabs(tabsize=2)
[docs]classParameter:"""A model parameter. Note that the parameter value has been split into a factor and scale like this:: value = factor x scale Users should interact with the ``value``, ``quantity`` or ``min`` and ``max`` properties and consider the fact that there is a ``factor``` and ``scale`` an implementation detail. That was introduced for numerical stability in parameter and error estimation methods, only in the Gammapy optimiser interface do we interact with the ``factor``, ``factor_min`` and ``factor_max`` properties, i.e. the optimiser "sees" the well-scaled problem. Parameters ---------- name : str Name value : float or `~astropy.units.Quantity` Value scale : float, optional Scale (sometimes used in fitting) unit : `~astropy.units.Unit` or str, optional Unit min : float, optional Minimum (sometimes used in fitting) max : float, optional Maximum (sometimes used in fitting) frozen : bool, optional Frozen? (used in fitting) error : float Parameter error scan_min : float Minimum value for the parameter scan. Overwrites scan_n_sigma. scan_max : float Minimum value for the parameter scan. Overwrites scan_n_sigma. scan_n_values: int Number of values to be used for the parameter scan. scan_n_sigma : int Number of sigmas to scan. scan_values: `numpy.array` Scan values. Overwrites all of the scan keywords before. scale_method : {'scale10', 'factor1', None} Method used to set ``factor`` and ``scale`` interp : {"lin", "sqrt", "log"} Parameter scaling to use for the scan. is_norm : bool Whether the parameter represents the flux norm of the model. """def__init__(self,name,value,unit="",scale=1,min=np.nan,max=np.nan,frozen=False,error=0,scan_min=None,scan_max=None,scan_n_values=11,scan_n_sigma=2,scan_values=None,scale_method="scale10",interp="lin",is_norm=False,):ifnotisinstance(name,str):raiseTypeError(f"Name must be string, got '{type(name)}' instead")self._name=nameself._link_label_io=Noneself.scale=scaleself.min=minself.max=maxself.frozen=frozenself._error=errorself._is_norm=is_normself._type=None# TODO: move this to a setter method that can be called from `__set__` also!# Having it here is bad: behaviour not clear if Quantity and `unit` is passed.ifisinstance(value,u.Quantity)orisinstance(value,str):val=u.Quantity(value)self.value=val.valueself.unit=val.unitelse:self.value=float(value)self.unit=unitself.scan_min=scan_minself.scan_max=scan_maxself.scan_values=scan_valuesself.scan_n_values=scan_n_valuesself.scan_n_sigma=scan_n_sigmaself.interp=interpself.scale_method=scale_methoddef__get__(self,instance,owner):ifinstanceisNone:returnselfpar=instance.__dict__[self.name]par._type=getattr(instance,"type",None)returnpardef__set__(self,instance,value):ifisinstance(value,Parameter):instance.__dict__[self.name]=valueelse:par=instance.__dict__[self.name]raiseTypeError(f"Cannot assign {value!r} to parameter {par!r}")def__set_name__(self,owner,name):ifnotself._name==name:raiseValueError(f"Expected parameter name '{name}', got {self._name}")@propertydefis_norm(self):"""Whether the parameter represents the norm of the model"""returnself._is_norm@propertydeftype(self):returnself._type@propertydeferror(self):returnself._error@error.setterdeferror(self,value):self._error=float(u.Quantity(value,unit=self.unit).value)@propertydefname(self):"""Name (str)."""returnself._name@propertydeffactor(self):"""Factor (float)."""returnself._factor@factor.setterdeffactor(self,val):self._factor=float(val)@propertydefscale(self):"""Scale (float)."""returnself._scale@scale.setterdefscale(self,val):self._scale=float(val)@propertydefunit(self):"""Unit (`~astropy.units.Unit`)."""returnself._unit@unit.setterdefunit(self,val):self._unit=u.Unit(val)@propertydefmin(self):"""Minimum (float)."""returnself._min@min.setterdefmin(self,val):"Astropy Table has masked values for NaN. Replacing with np.nan."ifisinstance(val,np.ma.core.MaskedConstant):self._min=np.nanelse:self._min=float(val)@propertydeffactor_min(self):"""Factor min (float). This ``factor_min = min / scale`` is for the optimizer interface. """returnself.min/self.scale@propertydefmax(self):"""Maximum (float)."""returnself._max@max.setterdefmax(self,val):"Astropy Table has masked values for NaN. Replacing with np.nan."ifisinstance(val,np.ma.core.MaskedConstant):self._max=np.nanelse:self._max=float(val)@propertydeffactor_max(self):"""Factor max (float). This ``factor_max = max / scale`` is for the optimizer interface. """returnself.max/self.scale@propertydefscale_method(self):"""Method used to set ``factor`` and ``scale``"""returnself._scale_method@scale_method.setterdefscale_method(self,val):ifvalnotin["scale10","factor1"]andvalisnotNone:raiseValueError(f"Invalid method: {val}")self._scale_method=val@propertydeffrozen(self):"""Frozen? (used in fitting) (bool)."""returnself._frozen@frozen.setterdeffrozen(self,val):ifvalin["True","False"]:val=bool(val)ifnotisinstance(val,bool)andnotisinstance(val,np.bool_):raiseTypeError(f"Invalid type: {val}, {type(val)}")self._frozen=val@propertydefvalue(self):"""Value = factor x scale (float)."""returnself._factor*self._scale@value.setterdefvalue(self,val):self.factor=float(val)/self._scale@propertydefquantity(self):"""Value times unit (`~astropy.units.Quantity`)."""returnself.value*self.unit@quantity.setterdefquantity(self,val):val=u.Quantity(val)ifnotval.unit.is_equivalent(self.unit):raiseu.UnitConversionError(f"Unit must be equivalent to {self.unit} for parameter {self.name}")self.value=val.valueself.unit=val.unit# TODO: possibly allow to set this independently@propertydefconf_min(self):"""Confidence min value (`float`) Returns parameter minimum if defined else the scan_min """ifnotnp.isnan(self.min):returnself.minelse:returnself.scan_min# TODO: possibly allow to set this independently@propertydefconf_max(self):"""Confidence max value (`float`) Returns parameter maximum if defined else the scan_max """ifnotnp.isnan(self.max):returnself.maxelse:returnself.scan_max@propertydefscan_min(self):"""Stat scan min"""ifself._scan_minisNone:returnself.value-self.error*self.scan_n_sigmareturnself._scan_min@propertydefscan_max(self):"""Stat scan max"""ifself._scan_maxisNone:returnself.value+self.error*self.scan_n_sigmareturnself._scan_max@scan_min.setterdefscan_min(self,value):"""Stat scan min setter"""self._scan_min=value@scan_max.setterdefscan_max(self,value):"""Stat scan max setter"""self._scan_max=value@propertydefscan_n_sigma(self):"""Stat scan n sigma"""returnself._scan_n_sigma@scan_n_sigma.setterdefscan_n_sigma(self,n_sigma):"""Stat scan n sigma"""self._scan_n_sigma=int(n_sigma)@propertydefscan_values(self):"""Stat scan values (`~numpy.ndarray`)"""ifself._scan_valuesisNone:scale=interpolation_scale(self.interp)parmin,parmax=scale([self.scan_min,self.scan_max])values=np.linspace(parmin,parmax,self.scan_n_values)returnscale.inverse(values)returnself._scan_values@scan_values.setterdefscan_values(self,values):"""Set scan values"""self._scan_values=values
[docs]defcheck_limits(self):"""Emit a warning or error if value is outside the min/max range"""ifnotself.frozen:if(~np.isnan(self.min)and(self.value<=self.min))or(~np.isnan(self.max)and(self.value>=self.max)):log.warning(f"Value {self.value} is outside bounds [{self.min}, {self.max}]"f" for parameter '{self.name}'")
[docs]defcopy(self):"""A deep copy"""returncopy.deepcopy(self)
[docs]defupdate_from_dict(self,data):"""Update parameters from a dict. Protection against changing parameter model, type, name."""keys=["value","unit","min","max","frozen"]forkinkeys:setattr(self,k,data[k])
[docs]defto_dict(self):"""Convert to dict."""output={"name":self.name,"value":self.value,"unit":self.unit.to_string("fits"),"error":self.error,"min":self.min,"max":self.max,"frozen":self.frozen,"interp":self.interp,"scale_method":self.scale_method,"is_norm":self.is_norm,}ifself._link_label_ioisnotNone:output["link"]=self._link_label_ioreturnoutput
[docs]defautoscale(self):"""Autoscale the parameters. Set ``factor`` and ``scale`` according to ``scale_method`` attribute Available ``scale_method`` * ``scale10`` sets ``scale`` to power of 10, so that abs(factor) is in the range 1 to 10 * ``factor1`` sets ``factor, scale = 1, value`` In both cases the sign of value is stored in ``factor``, i.e. the ``scale`` is always positive. If ``scale_method`` is None the scaling is ignored. """ifself.scale_method=="scale10":value=self.valueifvalue!=0:exponent=np.floor(np.log10(np.abs(value)))scale=np.power(10.0,exponent)self.factor=value/scaleself.scale=scaleelifself.scale_method=="factor1":self.factor,self.scale=1,self.value
[docs]classParameters(collections.abc.Sequence):"""Parameters container. - List of `Parameter` objects. - Covariance matrix. Parameters ---------- parameters : list of `Parameter` List of parameters """def__init__(self,parameters=None):ifparametersisNone:parameters=[]else:parameters=list(parameters)self._parameters=parameters
[docs]defcheck_limits(self):"""Check parameter limits and emit a warning"""forparinself:par.check_limits()
@propertydeftypes(self):"""Parameter types"""return[par.typeforparinself]@propertydefmin(self):"""Parameter mins (`numpy.ndarray`)."""returnnp.array([_.minfor_inself._parameters],dtype=np.float64)@min.setterdefmin(self,min_array):"""Parameter minima (`numpy.ndarray`)."""ifnotlen(self)==len(min_array):raiseValueError("Minima must have same length as parameter list")formin_,parinzip(min_array,self):par.min=min_@propertydefmax(self):"""Parameter maxima (`numpy.ndarray`)."""returnnp.array([_.maxfor_inself._parameters],dtype=np.float64)@max.setterdefmax(self,max_array):"""Parameter maxima (`numpy.ndarray`)."""ifnotlen(self)==len(max_array):raiseValueError("Maxima must have same length as parameter list")formax_,parinzip(max_array,self):par.max=max_@propertydefvalue(self):"""Parameter values (`numpy.ndarray`)."""returnnp.array([_.valuefor_inself._parameters],dtype=np.float64)@value.setterdefvalue(self,values):"""Parameter values (`numpy.ndarray`)."""ifnotlen(self)==len(values):raiseValueError("Values must have same length as parameter list")forvalue,parinzip(values,self):par.value=value
[docs]@classmethoddeffrom_stack(cls,parameters_list):"""Create `Parameters` by stacking a list of other `Parameters` objects. Parameters ---------- parameters_list : list of `Parameters` List of `Parameters` objects """pars=itertools.chain(*parameters_list)returncls(pars)
[docs]defcopy(self):"""A deep copy"""returncopy.deepcopy(self)
@propertydefnorm_parameters(self):"""List of norm parameters"""returnself.__class__([parforparinself._parametersifpar.is_norm])@propertydeffree_parameters(self):"""List of free parameters"""returnself.__class__([parforparinself._parametersifnotpar.frozen])@propertydefunique_parameters(self):"""Unique parameters (`Parameters`)."""returnself.__class__(dict.fromkeys(self._parameters))@propertydefnames(self):"""List of parameter names"""return[par.nameforparinself._parameters]
[docs]defindex(self,val):"""Get position index for a given parameter. The input can be a parameter object, parameter name (str) or if a parameter index (int) is passed in, it is simply returned. """ifisinstance(val,int):returnvalelifisinstance(val,Parameter):returnself._parameters.index(val)elifisinstance(val,str):foridx,parinenumerate(self._parameters):ifval==par.name:returnidxraiseIndexError(f"No parameter: {val!r}")else:raiseTypeError(f"Invalid type: {type(val)!r}")
def__getitem__(self,key):"""Access parameter by name, index or boolean mask"""ifisinstance(key,np.ndarray)andkey.dtype==bool:returnself.__class__(list(np.array(self._parameters)[key]))else:idx=self.index(key)returnself._parameters[idx]def__len__(self):returnlen(self._parameters)def__add__(self,other):ifisinstance(other,Parameters):returnParameters.from_stack([self,other])else:raiseTypeError(f"Invalid type: {other!r}")
[docs]defto_table(self):"""Convert parameter attributes to `~astropy.table.Table`."""rows=[]forpinself._parameters:d=p.to_dict()if"link"notind:d["link"]=""forkeyin["scale_method","interp"]:ifkeyind:deld[key]rows.append({**dict(type=p.type),**d})table=Table(rows)table["value"].format=".4e"fornamein["error","min","max"]:table[name].format=".3e"returntable
[docs]defset_parameter_factors(self,factors):"""Set factor of all parameters. Used in the optimizer interface. """idx=0forparameterinself._parameters:ifnotparameter.frozen:parameter.factor=factors[idx]idx+=1
[docs]defautoscale(self):"""Autoscale all parameters. See :func:`~gammapy.modeling.Parameter.autoscale` """forparinself._parameters:par.autoscale()
[docs]defselect(self,name=None,type=None,frozen=None,):"""Create a mask of models, true if all conditions are verified Parameters ---------- name : str or list Name of the parameter type : {None, spatial, spectral, temporal} type of models frozen : bool Select frozen parameters if True, exclude them if False. Returns ------- parameters : `Parameters` Selected parameters """selection=np.ones(len(self),dtype=bool)ifnameandnotisinstance(name,list):name=[name]foridx,parinenumerate(self):ifname:selection[idx]&=np.any([_==par.namefor_inname])iftype:selection[idx]&=type==par.typeiffrozenisnotNone:iffrozen:selection[idx]&=par.frozenelse:selection[idx]&=~par.frozenreturnself[selection]
[docs]deffreeze_all(self):"""Freeze all parameters"""forparinself._parameters:par.frozen=True
[docs]defunfreeze_all(self):"""Unfreeze all parameters (even those frozen by default)"""forparinself._parameters:par.frozen=False
[docs]defrestore_status(self,restore_values=True):"""Context manager to restore status. A copy of the values is made on enter, and those values are restored on exit. Parameters ---------- restore_values : bool Restore values if True, otherwise restore only frozen status. Examples -------- :: from gammapy.modeling.models import PowerLawSpectralModel pwl = PowerLawSpectralModel(index=2) with pwl.parameters.restore_status(): pwl.parameters["index"].value = 3 print(pwl.parameters["index"].value) """returnrestore_parameters_status(self,restore_values)