# Licensed under a 3-clause BSD style license - see LICENSE.rst"""Model parameter classes."""importcollections.abcimportcopyimporthtmlimportitertoolsimportloggingimportnumpyasnpfromastropyimportunitsasufromastropy.tableimportTablefromgammapy.utils.deprecationimportdeprecated_attributefromgammapy.utils.interpolationimportinterpolation_scale__all__=["Parameter","Parameters","PriorParameter","PriorParameters"]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 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. prior : `~gammapy.modeling.models.Prior` Prior set on the parameter. """norm_parameters=deprecated_attribute("norm_parameters","1.2")is_norm=deprecated_attribute("is_norm","1.2")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,prior=None,):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_methodself.prior=priordef__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}")@propertydefprior(self):"""Prior applied to the parameter as a `~gammapy.modeling.models.Prior`."""returnself._prior@prior.setterdefprior(self,value):ifvalueisnotNone:from.modelsimportPriorifisinstance(value,dict):from.modelsimportModelself._prior=Model.from_dict({"prior":value})elifisinstance(value,Prior):self._prior=valueelse:raiseTypeError(f"Invalid type: {value!r}")else:self._prior=value
@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 as a string."""returnself._name@propertydeffactor(self):"""Factor as a float."""returnself._factor@factor.setterdeffactor(self,val):self._factor=float(val)@propertydefscale(self):"""Scale as a float."""returnself._scale@scale.setterdefscale(self,val):self._scale=float(val)@propertydefunit(self):"""Unit as a `~astropy.units.Unit` object."""returnself._unit@unit.setterdefunit(self,val):self._unit=u.Unit(val)@propertydefmin(self):"""Minimum as a float."""returnself._min@min.setterdefmin(self,val):"""`~astropy.table.Table` has masked values for NaN. Replacing with NaN."""ifisinstance(val,np.ma.core.MaskedConstant):self._min=np.nanelse:self._min=float(val)@propertydeffactor_min(self):"""Factor minimum as a float. This ``factor_min = min / scale`` is for the optimizer interface. """returnself.min/self.scale@propertydefmax(self):"""Maximum as a float."""returnself._max@max.setterdefmax(self,val):"""`~astropy.table.Table` has masked values for NaN. Replacing with NaN."""ifisinstance(val,np.ma.core.MaskedConstant):self._max=np.nanelse:self._max=float(val)@propertydeffactor_max(self):"""Factor maximum as a 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 as a `~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 minimum value as a `float`. Return parameter minimum if defined, otherwise return the scan_min. """ifnotnp.isnan(self.min):returnself.minelse:returnself.scan_min# TODO: possibly allow to set this independently@propertydefconf_max(self):"""Confidence maximum value as a `float`. Return parameter maximum if defined, otherwise return the scan_max. """ifnotnp.isnan(self.max):returnself.maxelse:returnself.scan_max@propertydefscan_min(self):"""Stat scan minimum."""ifself._scan_minisNone:returnself.value-self.error*self.scan_n_sigmareturnself._scan_min@propertydefscan_max(self):"""Stat scan maximum."""ifself._scan_maxisNone:returnself.value+self.error*self.scan_n_sigmareturnself._scan_max@scan_min.setterdefscan_min(self,value):"""Stat scan minimum setter."""self._scan_min=value@scan_max.setterdefscan_max(self,value):"""Stat scan maximum 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 as a `~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 minimum/maximum 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]defupdate_from_dict(self,data):"""Update parameters from a dictionary."""keys=["value","unit","min","max","frozen","prior"]forkinkeys:ifk=="prior"anddata[k]=="":data[k]=Nonesetattr(self,k,data[k])
[docs]defto_dict(self):"""Convert to dictionary."""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_ioifself.priorisnotNone:output["prior"]=self.prior.to_dict()["prior"]returnoutput
[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=parametersdef_repr_html_(self):try:returnself.to_html()exceptAttributeError:returnf"<pre>{html.escape(str(self))}</pre>"
[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 minima as a `numpy.ndarray`."""returnnp.array([_.minfor_inself._parameters],dtype=np.float64)@min.setterdefmin(self,min_array):"""Parameter minima as a `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 as a `numpy.ndarray`."""returnnp.array([_.maxfor_inself._parameters],dtype=np.float64)@max.setterdefmax(self,max_array):"""Parameter maxima as a `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 as a `numpy.ndarray`."""returnnp.array([_.valuefor_inself._parameters],dtype=np.float64)@value.setterdefvalue(self,values):"""Parameter values as a `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)
@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 as a `Parameters` object."""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`."""table=self._create_default_table()forpinself._parameters:d={k:vfork,vinp.to_dict().items()ifkintable.colnames}if"prior"ind:d["prior"]=d["prior"]["type"]table.add_row(d)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, optional Name of the parameter. Default is None. type : {None, "spatial", "spectral", "temporal"} Type of models. Default is None. frozen : bool, optional Select frozen parameters if True, exclude them if False. Default is None. 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, optional Restore values if True, otherwise restore only frozen status. Default is None. 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) # doctest: +SKIP """returnrestore_parameters_status(self,restore_values)
[docs]classPriorParameter(Parameter):def__init__(self,name,value,unit="",scale=1,min=np.nan,max=np.nan,error=0,):ifnotisinstance(name,str):raiseTypeError(f"Name must be string, got '{type(name)}' instead")self._name=nameself.scale=scaleself.min=minself.max=maxself._error=errorifisinstance(value,u.Quantity)orisinstance(value,str):val=u.Quantity(value)self.value=val.valueself.unit=val.unitelse:self.factor=valueself.unit=unitself._type="prior"
[docs]defto_dict(self):"""Convert to dictionary."""output={"name":self.name,"value":self.value,"unit":self.unit.to_string("fits"),"error":self.error,"min":self.min,"max":self.max,}returnoutput
[docs]defto_table(self):"""Convert parameter attributes to `~astropy.table.Table`."""rows=[]forpinself._parameters:d=p.to_dict()rows.append({**dict(type=p.type),**d})table=Table(rows)table["value"].format=".4e"fornamein["error","min","max"]:table[name].format=".3e"returntable