Source code for qiskit_experiments.curve_analysis.curve_data
# This code is part of Qiskit.## (C) Copyright IBM 2021.## This code is licensed under the Apache License, Version 2.0. You may# obtain a copy of this license in the LICENSE.txt file in the root directory# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.## Any modifications or derivative works of this code must retain this# copyright notice, and modified files need to carry a notice indicating# that they have been altered from the originals."""Curve data classes."""importdataclassesimportitertoolsfromtypingimportAny,Dict,Union,List,Tuple,Optional,Iterableimportnumpyasnpimportuncertaintiesfromuncertainties.unumpyimportuarrayfromqiskit_experiments.exceptionsimportAnalysisError
[docs]classCurveFitResult:"""Result of Qiskit Experiment curve analysis."""def__init__(self,method:Optional[str]=None,model_repr:Optional[Dict[str,str]]=None,success:Optional[bool]=True,nfev:Optional[int]=None,message:Optional[str]="",dof:Optional[float]=None,init_params:Optional[Dict[str,float]]=None,chisq:Optional[float]=None,reduced_chisq:Optional[float]=None,aic:Optional[float]=None,bic:Optional[float]=None,params:Optional[Dict[str,float]]=None,var_names:Optional[List[str]]=None,x_data:Optional[np.ndarray]=None,y_data:Optional[np.ndarray]=None,weighted_residuals:Optional[np.ndarray]=None,residuals:Optional[np.ndarray]=None,covar:Optional[np.ndarray]=None,):"""Create new Qiskit curve analysis result object. Args: method: A name of fitting algorithm used for the curve fitting. model_repr: String representation of fit functions of each curve. success: True when the fitting is successfully performed. nfev: Number of fit function evaluation until the solution is obtained. message: Any message from the fitting software. dof: Degree of freedom in this fitting, i.e. number of free parameters. init_params: Initial parameters provided to the fitter. chisq: Chi-squared value. reduced_chisq: Reduced Chi-squared value. aic: Akaike's information criterion. bic: Bayesian information criterion. params: Estimated fitting parameters keyed on the parameter names in the fit function. var_names: Name of variables, i.e. fixed parameters are excluded from the list. x_data: X values used for the fitting. y_data: Y values used for the fitting. weighted_residuals: The residuals from the fitting after assigning weights for each ydata. residuals: residuals of the fitted model. covar: Covariance matrix of fitting variables. """self.method=methodself.model_repr=model_reprself.success=successself.nfev=nfevself.message=messageself.dof=dofself.init_params=init_paramsself.chisq=chisqself.reduced_chisq=reduced_chisqself.aic=aicself.bic=bicself.params=paramsself.var_names=var_namesself.x_data=x_dataself.y_data=y_dataself.weighted_residuals=weighted_residualsself.residuals=residualsself.covar=covar@propertydefx_range(self)->Tuple[float,float]:"""Range of x_data values."""returnmin(self.x_data),max(self.x_data)@propertydefy_range(self)->Tuple[float,float]:"""Range of y_data values."""returnmin(self.y_data),max(self.y_data)@propertydefufloat_params(self)->Dict[str,uncertainties.UFloat]:"""UFloat representation of fit parameters."""ifhasattr(self,"_ufloat_params"):# Return cachereturngetattr(self,"_ufloat_params")ifself.paramsisNone:ufloat_params=Noneelse:ifself.covarisnotNone:ufloat_fitvals=uncertainties.correlated_values(nom_values=[self.params[name]fornameinself.var_names],covariance_mat=self.covar,tags=self.var_names,)else:# Invalid covariance matrix. Std dev is set to nan, i.e. not computed.withnp.errstate(invalid="ignore"):# Setting std_devs to NaN will trigger floating point exceptions# which we can ignore. See https://stackoverflow.com/q/75656026ufloat_fitvals=uarray(nominal_values=[self.params[name]fornameinself.var_names],std_devs=np.full(len(self.var_names),np.nan),)# Combine fixed params and fitting variables into a single dictionary# Fixed parameter has zero std_devufloat_params={}fornameinself.params.keys():try:uind=self.var_names.index(name)ufloat_params[name]=ufloat_fitvals[uind]exceptValueError:ufloat_params[name]=uncertainties.ufloat(self.params[name],std_dev=0.0)setattr(self,"_ufloat_params",ufloat_params)returnufloat_params@propertydefcorrel(self):"""Correlation matrix of fit parameters."""ifhasattr(self,"_correl"):# Return cachereturngetattr(self,"_correl")ifself.covarisnotNone:# This is how uncertainties computes correlation matrixstdevs=np.sqrt(np.diag(self.covar))correl=self.covar/stdevs/stdevs[:,np.newaxis]else:correl=Nonesetattr(self,"_correl",correl)returncorreldef__str__(self):ret="CurveFitResult:"ret+=f"\n - fitting method: {self.method}"ret+=f"\n - number of sub-models: {len(self.model_repr)}"formodel_name,model_exprinself.model_repr.items():iflen(model_expr)>60:model_expr=f"{model_expr[:60]}..."ret+=f"\n * F_{model_name}(x) = {model_expr}"ret+=f"\n - success: {self.success}"ret+=f"\n - number of function evals: {self.nfev}"ret+=f"\n - degree of freedom: {self.dof}"ret+=f"\n - chi-square: {self.chisq}"ret+=f"\n - reduced chi-square: {self.reduced_chisq}"ret+=f"\n - Akaike info crit.: {self.aic}"ret+=f"\n - Bayesian info crit.: {self.bic}"ifself.init_paramsisnotNone:ret+="\n - init params:"forname,valueinself.init_params.items():ret+=f"\n * {name} = {value}"ifself.ufloat_paramsisnotNone:ret+="\n - fit params:"forname,paraminself.ufloat_params.items():ifnp.isfinite(param.std_dev):ret+=f"\n * {name} = {param.nominal_value} ± {param.std_dev}"else:ret+=f"\n * {name} = {param.nominal_value}"ifself.correlisnotNone:ret+="\n - correlations:"correlated={}forpi,pjinitertools.combinations(range(len(self.var_names)),2):correlated[(pi,pj)]=self.correl[pi,pj]for(pi,pj),corrinsorted(correlated.items(),key=lambdaitem:item[1]):ret+=f"\n * ({self.var_names[pi]}, {self.var_names[pj]}) = {corr}"returnretdef__copy__(self):instance=CurveFitResult(**self.__json_encode__())# Copying ufloat invalidate parameter correlation.# Note that ufloat object has `self._linear_part.linear_combo` dictionary# to store parameter correlation keyed on the ufloat objects.# Copying the ufloat object may change object id, which is the identifier# of ufloat value, thus it invalidates the `linear_combo` dictionary.# To avoid missing correlation, the copy invalidate ufloat parameter object cache.returninstancedef__deepcopy__(self,memo):returnself.__copy__()def__json_encode__(self):return{"method":self.method,"model_repr":self.model_repr,"success":self.success,"nfev":self.nfev,"message":self.message,"dof":self.dof,"init_params":self.init_params,"chisq":self.chisq,"reduced_chisq":self.reduced_chisq,"aic":self.aic,"bic":self.bic,"params":self.params,"var_names":self.var_names,"x_data":self.x_data,"y_data":self.y_data,"covar":self.covar,}@classmethoddef__json_decode__(cls,value):returncls(**value)
[docs]@dataclasses.dataclassclassParameterRepr:"""Detailed description of fitting parameter. Attributes: name: Original name of the fit parameter being defined in the fit model. repr: Optional. Human-readable parameter name shown in the analysis result and in the figure. unit: Optional. Physical unit of this parameter if applicable. """# Fitter argument namename:str# Unicode representationrepr:Optional[str]=None# Unitunit:Optional[str]=None
classOptionsDict(dict):"""General extended dictionary for fit options. This dictionary provides several extra features. - A value setting method which validates the dict key and value. - Dictionary keys are limited to those specified in the constructor as ``parameters``. """def__init__(self,parameters:List[str],defaults:Optional[Union[Iterable[Any],Dict[str,Any]]]=None,):"""Create new dictionary. Args: parameters: List of parameter names used in the fit model. defaults: Default values. Raises: AnalysisError: When defaults is provided as array-like but the number of element doesn't match with the number of fit parameters. """ifdefaultsisnotNone:ifnotisinstance(defaults,dict):iflen(defaults)!=len(parameters):raiseAnalysisError(f"Default parameter {defaults} is provided with array-like ""but the number of element doesn't match. "f"This fit requires {len(parameters)} parameters.")defaults=dict(zip(parameters,defaults))full_options={p:self.format(defaults.get(p,None))forpinparameters}else:full_options={p:Noneforpinparameters}super().__init__(**full_options)def__setitem__(self,key,value):"""Set value with validations. Raises: AnalysisError: When key is not previously defined. """ifkeynotinself:raiseAnalysisError(f"Parameter {key} is not defined in this fit model.")super().__setitem__(key,self.format(value))def__hash__(self):returnhash(tuple(sorted(self.items())))defset_if_empty(self,**kwargs):"""Set value to the dictionary if not assigned. Args: kwargs: Key and new value to assign. """forkey,valueinkwargs.items():ifself.get(key)isNone:self[key]=value@staticmethoddefformat(value:Any)->Any:"""Format dictionary value. Subclasses may override this method to provide their own validation. Args: value: New value to assign. Returns: Formatted value. """returnvalueclassInitialGuesses(OptionsDict):"""Dictionary providing a float validation for initial guesses."""@staticmethoddefformat(value:Any)->Optional[float]:"""Validate that value is float a float or None. Args: value: New value to assign. Returns: Formatted value. Raises: AnalysisError: When value is not a float or None. """ifvalueisNone:returnNonetry:returnfloat(value)except(TypeError,ValueError)asex:raiseAnalysisError(f"Input value {value} is not valid initial guess. ")fromexclassBoundaries(OptionsDict):"""Dictionary providing a validation for boundaries."""@staticmethoddefformat(value:Any)->Optional[Tuple[float,float]]:"""Validate if value is a min-max value tuple. Args: value: New value to assign. Returns: Formatted value. Raises: AnalysisError: When value is invalid format. """ifvalueisNone:returnNonetry:minv,maxv=valueifminv>=maxv:raiseAnalysisError(f"The first value is greater than the second value {minv} >= {maxv}.")returnfloat(minv),float(maxv)except(TypeError,ValueError)asex:raiseAnalysisError(f"Input boundary {value} is not a min-max value tuple.")fromex# pylint: disable=invalid-name
[docs]classFitOptions:"""Collection of fitting options. This class is initialized with a list of parameter names used in the fit model and corresponding default values provided by users. This class is hashable, and generates fitter keyword arguments. """def__init__(self,parameters:List[str],default_p0:Optional[Union[Iterable[float],Dict[str,float]]]=None,default_bounds:Optional[Union[Iterable[Tuple],Dict[str,Tuple]]]=None,**extra,):# These are private members so that user cannot directly override values# without implicitly implemented validation logic. No setter will be provided.self.__p0=InitialGuesses(parameters,default_p0)self.__bounds=Boundaries(parameters,default_bounds)self.__extra=extradef__hash__(self):returnhash((self.__p0,self.__bounds,tuple(sorted(self.__extra.items()))))def__eq__(self,other):ifisinstance(other,FitOptions):checks=[self.__p0==other.__p0,self.__bounds==other.__bounds,self.__extra==other.__extra,]returnall(checks)returnFalse
[docs]defadd_extra_options(self,**kwargs):"""Add more fitter options."""self.__extra.update(kwargs)
[docs]defcopy(self):"""Create copy of this option."""returnFitOptions(parameters=list(self.__p0.keys()),default_p0=dict(self.__p0),default_bounds=dict(self.__bounds),**self.__extra,)
@propertydefp0(self)->InitialGuesses:"""Return initial guess dictionary."""returnself.__p0@propertydefbounds(self)->Boundaries:"""Return bounds dictionary."""returnself.__bounds@propertydeffitter_opts(self)->Boundaries:"""Return fitter options dictionary."""returnself.__extra@propertydefoptions(self):"""Generate keyword arguments of the curve fitter."""bounds={k:vifvisnotNoneelse(-np.inf,np.inf)fork,vinself.__bounds.items()}return{"p0":dict(self.__p0),"bounds":bounds,**self.__extra}