class minuitSolver(): def __init__( self, fcn, pars, parerrors, parnames, ndof, maxpars=50 ): if len( pars ) > maxpars: raise MinuitError( "More than 50 parameters, increase maxpars" ) self.__minuit= TMinuit( maxpars ) self.minuitCommand( "SET PRI -1" ) self.__minuit.SetFCN( fcn ) self.__pars= pars self.__parerrors= parerrors self.__parnames= parnames self.__setParameters() self.__ndof= ndof return def __setParameters( self ): for par, parerror, parname, i in zip( self.__pars, self.__parerrors, self.__parnames, range( len( self.__pars ) ) ): ierflg= self.__minuit.DefineParameter( i, parname, par, parerror, 0.0, 0.0 ) if ierflg != 0: message= "Minuit define parameter error: " + str( ierflg ) raise MinuitError( message ) return def minuitCommand( self, command ): errorcode= self.__minuit.Command( command ) if errorcode != 0: message= "Minuit command " + command + " failed: " + str( errorcode ) raise MinuitError( message ) return def solve( self, lBlobel=True ): self.__setParameters() self.minuitCommand( "MIGRAD" ) return def getChisq( self ): hstat= self.__getStat() return hstat["min"] def getNdof( self ): return self.__ndof def __printPars( self, par, parerrors, parnames, ffmt=".4f" ): for ipar in range( len( par ) ): name= parnames[ipar] print "{0:>15s}:".format( name ), fmtstr= "{0:10" + ffmt + "} +/- {1:10" + ffmt + "}" print fmtstr.format( par[ipar], parerrors[ipar] ) return def printResults( self, ffmt=".4f", cov=False, corr=False ): print "\nMinuit least squares" print "\nResults after minuit fit" hstat= self.__getStat() chisq= hstat["min"] ndof= self.__ndof fmtstr= "\nChi^2= {0:"+ffmt+"} for {1:d} d.o.f, Chi^2/d.o.f= {2:"+ffmt+"}, P-value= {3:"+ffmt+"}" print fmtstr.format( chisq, ndof, chisq/float(ndof), TMath.Prob( chisq, ndof ) ) fmtstr= "Est. dist. to min: {0:.3e}, minuit status: {1}" print fmtstr.format( hstat["edm"], hstat["status"] ) print "\nFitted parameters and errors" print " Name Value Error" pars= self.getPars() parerrors= self.getParErrors() self.__printPars( pars, parerrors, self.__parnames, ffmt=ffmt ) if cov: self.printCovariances() if corr: self.printCorrelations() return def __printMatrix( self, m, ffmt ): mshape= m.shape print "{0:>10s}".format( "" ), for i in range(mshape[0]): print "{0:>10s}".format( self.__parnames[i] ), print for i in range(mshape[0]): print "{0:>10s}".format( self.__parnames[i] ), for j in range(mshape[1]): fmtstr= "{0:10"+ffmt+"}" print fmtstr.format( m[i,j] ), print return def printCovariances( self ): print "\nCovariance matrix:" self.__printMatrix( self.getCovariancematrix(), ".3e" ) return def printCorrelations( self ): print "\nCorrelation matrix:" self.__printMatrix( self.getCorrelationmatrix(), ".3f" ) return def getPars( self ): pars, parerrors= self.__getPars() return pars def getUparv( self ): pars= self.getPars() parv= matrix( pars ) parv.shape= (len(pars),1) return parv def getParErrors( self ): pars, parerrors= self.__getPars() return parerrors def __getPars( self ): pars= [] parerrors= [] for ipar in range( len( self.__pars ) ): par= Double() pare= Double() ivarbl= self.__minuit.GetParameter( ipar, par, pare ) if ivarbl < 0: message= "Parameter " + str(ipar) + " not defined" raise MinuitError( message ) pars.append( par ) parerrors.append( pare ) return pars, parerrors def getCovariancematrix( self ): npar= len( self.__pars ) covm= array( npar**2*[ 0.0 ], dtype="double" ) self.__minuit.mnemat( covm, npar ) covm.shape= (npar,npar) return covm def getCorrelationmatrix( self ): covm= self.getCovariancematrix() corrm= covm.copy() npar= len( self.__pars ) for i in range( npar ): for j in range( npar ): corrm[i,j]= covm[i,j]/sqrt(covm[i,i]*covm[j,j]) return corrm def __getStat( self ): fmin= Double() fedm= Double() errdef= Double() npari= Long() nparx= Long() istat= Long() self.__minuit.mnstat( fmin, fedm, errdef, npari, nparx, istat ) hstat= { "min": fmin, "edm": fedm, "errdef": errdef, "npari": npari, "nparx": nparx, "status": istat } return hstat
class Minuit(object): """A wrapper class to initialize a minuit object with a numpy array. Positional args: myFCN : A python callable params : An array (or other python sequence) of parameters Keyword args: limits [None] : a nested sequence of (lower_limit,upper_limit) for each parameter. steps [[.1]*npars] : Estimated errors for the parameters, used as an initial step size. tolerance [.001] : Tolerance to test for convergence. Minuit considers convergence to be when the estimated distance to the minimum (edm) is <= .001*up*tolerance, or 5e-7 by default. up [.5] : Change in the objective function that determines 1-sigma error. .5 if the objective function is -log(Likelihood), 1 for chi-squared. max_calls [10000] : Maximum number of calls to the objective function. param_names ['p0','p1',...] : a list of names for the parameters args [()] : a tuple of extra arguments to pass to myFCN and gradient. gradient [None] : a function that takes a list of parameters and returns a list of first derivatives of myFCN. Assumed to take the same args as myFCN. force_gradient [0] : Set to 1 to force Minuit to use the user-provided gradient function. strategy[1] : Strategy for minuit to use, from 0 (fast) to 2 (safe) fixed [False, False, ...] : If passed, an array of all the parameters to fix """ def __init__(self, myFCN, params, **kwargs): from ROOT import TMinuit, Long, Double self.limits = np.zeros((len(params), 2)) self.steps = .04 * np.ones( len(params)) # about 10 percent in log10 space self.tolerance = .001 self.maxcalls = 10000 self.printMode = 0 self.up = 0.5 self.param_names = ['p%i' % i for i in xrange(len(params))] self.erflag = Long() self.npars = len(params) self.args = () self.gradient = None self.force_gradient = 0 self.strategy = 1 self.fixed = np.zeros_like(params) self.__dict__.update(kwargs) self.params = np.asarray(params, dtype='float') self.fixed = np.asarray(self.fixed, dtype=bool) self.fcn = FCN(myFCN, self.params, args=self.args, gradient=self.gradient) self.fval = self.fcn.fval self.minuit = TMinuit(self.npars) self.minuit.SetPrintLevel(self.printMode) self.minuit.SetFCN(self.fcn) if self.gradient: self.minuit.mncomd('SET GRA %i' % (self.force_gradient), self.erflag) self.minuit.mncomd('SET STR %i' % self.strategy, Long()) for i in xrange(self.npars): if self.limits[i][0] is None: self.limits[i][0] = 0.0 if self.limits[i][1] is None: self.limits[i][1] = 0.0 self.minuit.DefineParameter(i, self.param_names[i], self.params[i], self.steps[i], self.limits[i][0], self.limits[i][1]) self.minuit.SetErrorDef(self.up) for index in np.where(self.fixed)[0]: self.minuit.FixParameter(int(index)) def minimize(self, method='MIGRAD'): from ROOT import TMinuit, Long, Double self.minuit.mncomd( '%s %i %f' % (method, self.maxcalls, self.tolerance), self.erflag) for i in xrange(self.npars): val, err = Double(), Double() self.minuit.GetParameter(i, val, err) self.params[i] = val self.fval = self.fcn.fcn(self.params) return (self.params, self.fval) def errors(self, method='HESSE'): """method ['HESSE'] : how to calculate the errors; Currently, only 'HESSE' works.""" if not np.any(self.fixed): mat = np.zeros(self.npars**2) if method == 'HESSE': self.minuit.mnhess() else: raise Exception("Method %s not recognized." % method) self.minuit.mnemat(mat, self.npars) return mat.reshape((self.npars, self.npars)) else: # Kind of ugly, but for fixed parameters, you need to expand out the covariance matrix. nf = int(np.sum(~self.fixed)) mat = np.zeros(nf**2) if method == 'HESSE': self.minuit.mnhess() else: raise Exception("Method %s not recognized." % method) self.minuit.mnemat(mat, nf) # Expand out the covariance matrix. cov = np.zeros((self.npars, self.npars)) cov[np.outer(~self.fixed, ~self.fixed)] = np.ravel(mat) return cov def uncorrelated_minos_error(self): """ Kind of a kludge, but a compromise between the speed of HESSE errors and the accuracy of MINOS errors. It accounts for non-linearities in the likelihood by varying each function until the value has fallen by the desired amount using hte MINOS code. But does not account for correlations between the parameters by fixing all other parameters during the minimzation. Again kludgy, but what is returned is an effective covariance matrix. The diagonal errors are calcualted by averaging the upper and lower errors and the off diagonal terms are set to 0. """ cov = np.zeros((self.npars, self.npars)) # fix all parameters for i in range(self.npars): self.minuit.FixParameter(int(i)) # loop over free paramters getting error for i in np.where(self.fixed == False)[0]: self.minuit.Release(int(i)) # compute error self.minuit.mnmnos() low, hi, parab, gcc = ROOT.Double(), ROOT.Double(), ROOT.Double( ), ROOT.Double() # get error self.minuit.mnerrs(int(i), low, hi, parab, gcc) cov[i, i] = ((abs(low) + abs(hi)) / 2.0)**2 self.minuit.FixParameter(int(i)) for i, fixed in enumerate(self.fixed): if fixed: self.minuit.FixParameter(int(i)) else: self.minuit.Release(int(i)) return cov
def Minuit(fun, x0, args=(), bounds=None, verbose=0, step_size=0.01, n_iterations=500, delta_one_sigma=0.5, **options): """ Interface to ROOT Minuit @param bounds list of tuple with limits for each variable @param verbose -1 (silent) 0 (normal) 1 (verbose) @param step_size float or Sequence @param n_iterations maximum number of iterations @param delta_one_sigma delta in loss function which corresponds to one sigma errors """ from ROOT import TMinuit, Double, Long n_parameters = len(x0) def to_minimize(npar, deriv, f, apar, iflag): f[0] = fun(np.array([apar[i] for i in range(n_parameters)]), *args) minuit = TMinuit(n_parameters) minuit.SetFCN(to_minimize) minuit.SetPrintLevel(verbose) ierflg = np.array(0, dtype=np.int32) minuit.mnexcm("SET ERR", np.array([delta_one_sigma]), 1, ierflg) if ierflg > 0: raise RuntimeError("Error during \"SET ERR\" call") for i in range(n_parameters): low, high = 0.0, 0.0 # Zero seems to mean no limit if bounds is not None: low, high = bounds[i] minuit.mnparm( i, 'Parameter_{}'.format(i), x0[i], step_size[i] if isinstance( step_size, collections.Sequence) else step_size, low, high, ierflg) if ierflg > 0: raise RuntimeError( "Error during \"nmparm\" call for parmameter {}".format(i)) minuit.mnexcm("MIGRAD", np.array([n_iterations, 0.01], dtype=np.float64), 2, ierflg) if ierflg > 0: raise RuntimeError("Error during \"MIGRAD\" call") xbest = np.zeros(n_parameters) xbest_error = np.zeros(n_parameters) p, pe = Double(0), Double(0) for i in range(n_parameters): minuit.GetParameter(i, p, pe) xbest[i] = p xbest_error[i] = pe buf = array.array('d', [0.] * (n_parameters**2)) minuit.mnemat(buf, n_parameters) # retrieve error matrix hessian = np.array(buf).reshape(n_parameters, n_parameters) amin = Double(0.) # value of fcn at minimum edm = Double(0.) # estimated distance to mimimum errdef = Double(0.) # delta_fcn used to define 1 sigma errors nvpar = np.array(0, dtype=np.int32) # number of variable parameters nparx = np.array(0, dtype=np.int32) # total number of parameters icstat = np.array( 0, dtype=np.int32 ) # status of error matrix: 3=accurate 2=forced pos. def 1= approximative 0=not calculated minuit.mnstat(amin, edm, errdef, nvpar, nparx, icstat) return scipy.optimize.OptimizeResult(x=xbest, fun=amin, hessian=hessian, success=(ierflg == 0), status=ierflg)
class Minuit(object): """A wrapper class to initialize a minuit object with a numpy array. Positional args: myFCN : A python callable params : An array (or other python sequence) of parameters Keyword args: limits [None] : a nested sequence of (lower_limit,upper_limit) for each parameter. steps [[.1]*npars] : Estimated errors for the parameters, used as an initial step size. tolerance [.001] : Tolerance to test for convergence. Minuit considers convergence to be when the estimated distance to the minimum (edm) is <= .001*up*tolerance, or 5e-7 by default. up [.5] : Change in the objective function that determines 1-sigma error. .5 if the objective function is -log(Likelihood), 1 for chi-squared. max_calls [10000] : Maximum number of calls to the objective function. param_names ['p0','p1',...] : a list of names for the parameters args [()] : a tuple of extra arguments to pass to myFCN and gradient. gradient [None] : a function that takes a list of parameters and returns a list of first derivatives of myFCN. Assumed to take the same args as myFCN. force_gradient [0] : Set to 1 to force Minuit to use the user-provided gradient function. strategy[1] : Strategy for minuit to use, from 0 (fast) to 2 (safe) fixed [False, False, ...] : If passed, an array of all the parameters to fix """ def __init__(self,myFCN,params,**kwargs): from ROOT import TMinuit,Long,Double self.limits = np.zeros((len(params),2)) self.steps = .04*np.ones(len(params)) # about 10 percent in log10 space self.tolerance = .001 self.maxcalls = 10000 self.printMode = 0 self.up = 0.5 self.param_names = ['p%i'%i for i in xrange(len(params))] self.erflag = Long() self.npars = len(params) self.args = () self.gradient = None self.force_gradient = 0 self.strategy = 1 self.fixed = np.zeros_like(params) self.__dict__.update(kwargs) self.params = np.asarray(params,dtype='float') self.fixed=np.asarray(self.fixed,dtype=bool) self.fcn = FCN(myFCN,self.params,args=self.args,gradient=self.gradient) self.fval = self.fcn.fval self.minuit = TMinuit(self.npars) self.minuit.SetPrintLevel(self.printMode) self.minuit.SetFCN(self.fcn) if self.gradient: self.minuit.mncomd('SET GRA %i'%(self.force_gradient),self.erflag) self.minuit.mncomd('SET STR %i'%self.strategy,Long()) for i in xrange(self.npars): if self.limits[i][0] is None: self.limits[i][0] = 0.0 if self.limits[i][1] is None: self.limits[i][1] = 0.0 self.minuit.DefineParameter(i,self.param_names[i],self.params[i],self.steps[i],self.limits[i][0],self.limits[i][1]) self.minuit.SetErrorDef(self.up) for index in np.where(self.fixed)[0]: self.minuit.FixParameter(int(index)) def minimize(self,method='MIGRAD'): from ROOT import TMinuit,Long,Double self.minuit.mncomd('%s %i %f'%(method, self.maxcalls,self.tolerance),self.erflag) for i in xrange(self.npars): val,err = Double(),Double() self.minuit.GetParameter(i,val,err) self.params[i] = val self.fval = self.fcn.fcn(self.params) return (self.params,self.fval) def errors(self,method='HESSE'): """method ['HESSE'] : how to calculate the errors; Currently, only 'HESSE' works.""" if not np.any(self.fixed): mat = np.zeros(self.npars**2) if method == 'HESSE': self.minuit.mnhess() else: raise Exception("Method %s not recognized." % method) self.minuit.mnemat(mat,self.npars) return mat.reshape((self.npars,self.npars)) else: # Kind of ugly, but for fixed parameters, you need to expand out the covariance matrix. nf=int(np.sum(~self.fixed)) mat = np.zeros(nf**2) if method == 'HESSE': self.minuit.mnhess() else: raise Exception("Method %s not recognized." % method) self.minuit.mnemat(mat,nf) # Expand out the covariance matrix. cov = np.zeros((self.npars,self.npars)) cov[np.outer(~self.fixed,~self.fixed)] = np.ravel(mat) return cov def uncorrelated_minos_error(self): """ Kind of a kludge, but a compromise between the speed of HESSE errors and the accuracy of MINOS errors. It accounts for non-linearities in the likelihood by varying each function until the value has fallen by the desired amount using hte MINOS code. But does not account for correlations between the parameters by fixing all other parameters during the minimzation. Again kludgy, but what is returned is an effective covariance matrix. The diagonal errors are calcualted by averaging the upper and lower errors and the off diagonal terms are set to 0. """ cov = np.zeros((self.npars,self.npars)) # fix all parameters for i in range(self.npars): self.minuit.FixParameter(int(i)) # loop over free paramters getting error for i in np.where(self.fixed==False)[0]: self.minuit.Release(int(i)) # compute error self.minuit.mnmnos() low,hi,parab,gcc=ROOT.Double(),ROOT.Double(),ROOT.Double(),ROOT.Double() # get error self.minuit.mnerrs(int(i),low,hi,parab,gcc) cov[i,i]=((abs(low)+abs(hi))/2.0)**2 self.minuit.FixParameter(int(i)) for i,fixed in enumerate(self.fixed): if fixed: self.minuit.FixParameter(int(i)) else: self.minuit.Release(int(i)) return cov
def fit( p , perr ): global val, err, nBins val = p err = perr nBins=len(val) #name=['q0','q1','q2','q3','q4'] #name=['q0','q1','q2','q3'] #name=['q0','q1','q2'] name = [ 'q0' , 'q1' ] npar=len(name) # the initial values vstart = arr( 'd' , npar*[0.1] ) # the initial step size step = arr( 'd' , npar*[0.000001] ) # --> set up MINUIT gMinuit = TMinuit ( npar ) # initialize TMinuit with maximum of npar parameters gMinuit.SetFCN( fcn ) # set function to minimize arglist = arr( 'd' , npar*[0.01] ) # set error definition ierflg = c_int(0) arglist[0] = 1. # 1 sigma is Delta chi2 = 1 gMinuit.mnexcm("SET ERR", arglist, 1, ierflg ) # --> set starting values and step size for parameters # Define the parameters for the fit for i in range(0,npar): gMinuit.mnparm( i, name[i] , vstart[i] , step[i] , 0, 0, ierflg ) # now ready for minimization step arglist [0] = 500 # Number of calls for FCN before giving up arglist [1] = 1. # Tolerance gMinuit.mnexcm("MIGRAD" , arglist , 2 , ierflg) # execute the minimisation # --> check TMinuit status amin , edm , errdef = c_double(0.) , c_double(0.) , c_double(0.) nvpar , nparx , icstat = c_int(0) , c_int(0) , c_int(0) gMinuit.mnstat (amin , edm , errdef , nvpar , nparx , icstat ) gMinuit.mnprin(3,amin) # print-out by Minuit # meaning of parameters: # amin: value of fcn distance at minimum (=chi^2) # edm: estimated distance to minimum # errdef: delta_fcn used to define 1 sigam errors # nvpar: total number of parameters # icstat: status of error matrix: # 3 = accurate # 2 = forced pos. def # 1 = approximative # 0 = not calculated # # --> get results from MINUIT finalPar = [] finalParErr = [] p, pe = c_double(0.) , c_double(0.) for i in range(0,npar): gMinuit.GetParameter(i, p, pe) # retrieve parameters and errors finalPar.append( float(p.value) ) finalParErr.append( float(pe.value) ) # get covariance matrix buf = arr('d' , npar*npar*[0.]) gMinuit.mnemat( buf , npar ) # retrieve error matrix emat = np.array( buf ).reshape( npar , npar ) # --> provide formatted output of results print "\n" print "*==* MINUIT fit completed:" print'fcn@minimum = %.3g'%( amin.value ) , " error code =" , ierflg.value , " status =" , icstat.value , " (if its 3, mean accurate)" print " Results: \t value error corr. mat." for i in range(0,npar): print'%s: \t%10.3e +/- %.1e'%( name[i] , finalPar[i] , finalParErr[i] ) , for j in range (0,i): print'%+.3g'%( emat[i][j]/np.sqrt(emat[i][i])/np.sqrt(emat[j][j]) ), print "\n" return [ [i,j] for i,j in zip(finalPar , finalParErr) ]
class minuitSolver(): def __init__(self, fcn, pars, parerrors, parnames, ndof, maxpars=50): if len(pars) > maxpars: raise MinuitError("More than 50 parameters, increase maxpars") self.__minuit = TMinuit(maxpars) self.minuitCommand("SET PRI -1") # Hold on to fcn or python will kill it after passing to TMinuit self.__fcn = fcn self.__minuit.SetFCN(fcn) self.__pars = pars self.__parerrors = parerrors self.__parnames = parnames self.__setParameters() self.__ndof = ndof return def __setParameters(self): for par, parerror, parname, i in zip(self.__pars, self.__parerrors, self.__parnames, range(len(self.__pars))): ierflg = self.__minuit.DefineParameter(i, parname, par, parerror, 0.0, 0.0) if ierflg != 0: message = "Minuit define parameter error: " + str(ierflg) raise MinuitError(message) return def minuitCommand(self, command): errorcode = self.__minuit.Command(command) if errorcode != 0: message = "Minuit command " + command + " failed: " + str( errorcode) raise MinuitError(message) return def solve(self, lBlobel=True): self.__setParameters() self.minuitCommand("MIGRAD") return def getChisq(self): hstat = self.__getStat() return hstat["min"] def getNdof(self): return self.__ndof def __printPars(self, par, parerrors, parnames, ffmt=".4f"): for ipar in range(len(par)): name = parnames[ipar] print("{0:>15s}:".format(name), end=" ") fmtstr = "{0:10" + ffmt + "} +/- {1:10" + ffmt + "}" print(fmtstr.format(par[ipar], parerrors[ipar])) return def printResults(self, ffmt=".4f", cov=False, corr=False): print("\nMinuit least squares") print("\nResults after minuit fit") hstat = self.__getStat() chisq = hstat["min"] ndof = self.__ndof fmtstr = "\nChi^2= {0:" + ffmt + "} for {1:d} d.o.f, Chi^2/d.o.f= {2:" + ffmt + "}, P-value= {3:" + ffmt + "}" print( fmtstr.format(chisq, ndof, chisq / float(ndof), TMath.Prob(chisq, ndof))) fmtstr = "Est. dist. to min: {0:.3e}, minuit status: {1}" print(fmtstr.format(hstat["edm"], hstat["status"])) print("\nFitted parameters and errors") print(" Name Value Error") pars = self.getPars() parerrors = self.getParErrors() self.__printPars(pars, parerrors, self.__parnames, ffmt=ffmt) if cov: self.printCovariances() if corr: self.printCorrelations() return def __printMatrix(self, m, ffmt): mshape = m.shape print("{0:>10s}".format(""), end=" ") for i in range(mshape[0]): print("{0:>10s}".format(self.__parnames[i]), end=" ") print() for i in range(mshape[0]): print("{0:>10s}".format(self.__parnames[i]), end=" ") for j in range(mshape[1]): fmtstr = "{0:10" + ffmt + "}" print(fmtstr.format(m[i, j]), end=" ") print() return def printCovariances(self): print("\nCovariance matrix:") self.__printMatrix(self.getCovariancematrix(), ".3e") return def printCorrelations(self): print("\nCorrelation matrix:") self.__printMatrix(self.getCorrelationmatrix(), ".3f") return def getPars(self): pars, parerrors = self.__getPars() return pars def getUparv(self): pars = self.getPars() parv = matrix(pars) parv.shape = (len(pars), 1) return parv def getParErrors(self): pars, parerrors = self.__getPars() return parerrors def __getPars(self): pars = [] parerrors = [] for ipar in range(len(self.__pars)): par = c_double() pare = c_double() ivarbl = self.__minuit.GetParameter(ipar, par, pare) if ivarbl < 0: message = "Parameter " + str(ipar) + " not defined" raise MinuitError(message) pars.append(par.value) parerrors.append(pare.value) return pars, parerrors def getCovariancematrix(self): npar = len(self.__pars) covm = array(npar**2 * [0.0], dtype="double") self.__minuit.mnemat(covm, npar) covm.shape = (npar, npar) return covm def getCorrelationmatrix(self): covm = self.getCovariancematrix() corrm = covm.copy() npar = len(self.__pars) for i in range(npar): for j in range(npar): corrm[i, j] = covm[i, j] / sqrt(covm[i, i] * covm[j, j]) return corrm def __getStat(self): fmin = c_double() fedm = c_double() errdef = c_double() npari = c_int() nparx = c_int() istat = c_int() self.__minuit.mnstat(fmin, fedm, errdef, npari, nparx, istat) hstat = { "min": fmin.value, "edm": fedm.value, "errdef": errdef.value, "npari": npari.value, "nparx": nparx.value, "status": istat.value } return hstat
class Minuit: ''' A class for communicating with ROOT's function minimizer tool Minuit. ''' def __init__(self, number_of_parameters, function_to_minimize, parameter_names, start_parameters, parameter_errors, quiet=True, verbose=False): ''' Create a Minuit minimizer for a function `function_to_minimize`. Necessary arguments are the number of parameters and the function to be minimized `function_to_minimize`. The function `function_to_minimize`'s arguments must be numerical values. The same goes for its output. Another requirement is for every parameter of `function_to_minimize` to have a default value. These are then used to initialize Minuit. **number_of_parameters** : int The number of parameters of the function to minimize. **function_to_minimize** : function The function which `Minuit` should minimize. This must be a Python function with <``number_of_parameters``> arguments. **parameter_names** : tuple/list of strings The parameter names. These are used to keep track of the parameters in `Minuit`'s output. **start_parameters** : tuple/list of floats The start values of the parameters. It is important to have a good, if rough, estimate of the parameters at the minimum before starting the minimization. Wrong initial parameters can yield a local minimum instead of a global one. **parameter_errors** : tuple/list of floats An initial guess of the parameter errors. These errors are used to define the initial step size. *quiet* : boolean (optional, default: ``True``) If ``True``, suppresses all output from ``TMinuit``. *verbose* : boolean (optional, default: ``False``) If ``True``, sets ``TMinuit``'s print level to a high value, so that all output is logged. ''' #: the name of this minimizer type self.name = "ROOT::TMinuit" #: the actual `FCN` called in ``FCN_wrapper`` self.function_to_minimize = function_to_minimize #: number of parameters to minimize for self.number_of_parameters = number_of_parameters if not quiet: self.out_file = open(log_file("minuit.log"), 'a') else: self.out_file = null_file() # create a TMinuit instance for that number of parameters self.__gMinuit = TMinuit(self.number_of_parameters) # instruct Minuit to use this class's FCN_wrapper method as a FCN self.__gMinuit.SetFCN(self.FCN_wrapper) # set print level according to flag if quiet: self.set_print_level(-1000) # suppress output elif verbose: self.set_print_level(10) # detailed output else: self.set_print_level(0) # frugal output # initialize minimizer self.set_err() self.set_strategy() self.set_parameter_values(start_parameters) self.set_parameter_errors(parameter_errors) self.set_parameter_names(parameter_names) #: maximum number of iterations until ``TMinuit`` gives up self.max_iterations = M_MAX_ITERATIONS #: ``TMinuit`` tolerance self.tolerance = M_TOLERANCE def update_parameter_data(self, show_warnings=False): """ (Re-)Sets the parameter names, values and step size on the C++ side of Minuit. """ error_code = Long(0) try: # Set up the starting fit parameters in TMinuit for i in range(0, self.number_of_parameters): self.__gMinuit.mnparm(i, self.parameter_names[i], self.current_parameters[i], 0.1 * self.parameter_errors[i], 0, 0, error_code) # use 10% of the par. 1-sigma errors as the initial step size except AttributeError as e: if show_warnings: logger.warning("Cannot update Minuit data on the C++ side. " "AttributeError: %s" % (e, )) return error_code # Set methods ############## def set_print_level(self, print_level=P_DETAIL_LEVEL): '''Sets the print level for Minuit. *print_level* : int (optional, default: 1 (frugal output)) Tells ``TMinuit`` how much output to generate. The higher this value, the more output it generates. ''' self.__gMinuit.SetPrintLevel(print_level) # set Minuit print level self.print_level = print_level def set_strategy(self, strategy_id=1): '''Sets the strategy Minuit. *strategy_id* : int (optional, default: 1 (optimized)) Tells ``TMinuit`` to use a certain strategy. Refer to ``TMinuit``'s documentation for available strategies. ''' error_code = Long(0) # execute SET STRATEGY command self.__gMinuit.mnexcm("SET STRATEGY", arr('d', [strategy_id]), 1, error_code) def set_err(self, up_value=1.0): '''Sets the ``UP`` value for Minuit. *up_value* : float (optional, default: 1.0) This is the value by which `FCN` is expected to change. ''' # Tell TMinuit to use an up-value of 1.0 error_code = Long(0) # execute SET ERR command self.__gMinuit.mnexcm("SET ERR", arr('d', [up_value]), 1, error_code) def set_parameter_values(self, parameter_values): ''' Sets the fit parameters. If parameter_values=`None`, tries to infer defaults from the function_to_minimize. ''' if len(parameter_values) == self.number_of_parameters: self.current_parameters = parameter_values else: raise Exception("Cannot get default parameter values from the \ FCN. Not all parameters have default values given.") self.update_parameter_data() def set_parameter_names(self, parameter_names): '''Sets the fit parameters. If parameter_values=`None`, tries to infer defaults from the function_to_minimize.''' if len(parameter_names) == self.number_of_parameters: self.parameter_names = parameter_names else: raise Exception("Cannot set param names. Tuple length mismatch.") self.update_parameter_data() def set_parameter_errors(self, parameter_errors=None): '''Sets the fit parameter errors. If parameter_values=`None`, sets the error to 10% of the parameter value.''' if parameter_errors is None: # set to 0.1% of the parameter value if not self.current_parameters is None: self.parameter_errors = [max(0.1, 0.1 * par) for par in self.current_parameters] else: raise Exception("Cannot set parameter errors. No errors \ provided and no parameters initialized.") elif len(parameter_errors) != len(self.current_parameters): raise Exception("Cannot set parameter errors. \ Tuple length mismatch.") else: self.parameter_errors = parameter_errors self.update_parameter_data() # Get methods ############## def get_error_matrix(self): '''Retrieves the parameter error matrix from TMinuit. return : `numpy.matrix` ''' # set up an array of type `double' to pass to TMinuit tmp_array = arr('d', [0.0]*(self.number_of_parameters**2)) # get parameter covariance matrix from TMinuit self.__gMinuit.mnemat(tmp_array, self.number_of_parameters) # reshape into 2D array return np.asmatrix( np.reshape( tmp_array, (self.number_of_parameters, self.number_of_parameters) ) ) def get_parameter_values(self): '''Retrieves the parameter values from TMinuit. return : tuple Current `Minuit` parameter values ''' result = [] # retrieve fit parameters p, pe = Double(0), Double(0) for i in range(0, self.number_of_parameters): self.__gMinuit.GetParameter(i, p, pe) # retrieve fitresult result.append(float(p)) return tuple(result) def get_parameter_errors(self): '''Retrieves the parameter errors from TMinuit. return : tuple Current `Minuit` parameter errors ''' result = [] # retrieve fit parameters p, pe = Double(0), Double(0) for i in range(0, self.number_of_parameters): self.__gMinuit.GetParameter(i, p, pe) # retrieve fitresult result.append(float(pe)) return tuple(result) def get_parameter_info(self): '''Retrieves parameter information from TMinuit. return : list of tuples ``(parameter_name, parameter_val, parameter_error)`` ''' result = [] # retrieve fit parameters p, pe = Double(0), Double(0) for i in range(0, self.number_of_parameters): self.__gMinuit.GetParameter(i, p, pe) # retrieve fitresult result.append((self.get_parameter_name(i), float(p), float(pe))) return result def get_parameter_name(self, parameter_nr): '''Gets the name of parameter number ``parameter_nr`` **parameter_nr** : int Number of the parameter whose name to get. ''' return self.parameter_names[parameter_nr] def get_fit_info(self, info): '''Retrieves other info from `Minuit`. **info** : string Information about the fit to retrieve. This can be any of the following: - ``'fcn'``: `FCN` value at minimum, - ``'edm'``: estimated distance to minimum - ``'err_def'``: `Minuit` error matrix status code - ``'status_code'``: `Minuit` general status code ''' # declare vars in which to retrieve other info fcn_at_min = Double(0) edm = Double(0) err_def = Double(0) n_var_param = Long(0) n_tot_param = Long(0) status_code = Long(0) # Tell TMinuit to update the variables declared above self.__gMinuit.mnstat(fcn_at_min, edm, err_def, n_var_param, n_tot_param, status_code) if info == 'fcn': return fcn_at_min elif info == 'edm': return edm elif info == 'err_def': return err_def elif info == 'status_code': try: return D_MATRIX_ERROR[status_code] except: return status_code def get_chi2_probability(self, n_deg_of_freedom): ''' Returns the probability that an observed :math:`\chi^2` exceeds the calculated value of :math:`\chi^2` for this fit by chance, even for a correct model. In other words, returns the probability that a worse fit of the model to the data exists. If this is a small value (typically <5%), this means the fit is pretty bad. For values below this threshold, the model very probably does not fit the data. n_def_of_freedom : int The number of degrees of freedom. This is typically :math:`n_\text{datapoints} - n_\text{parameters}`. ''' chi2 = Double(self.get_fit_info('fcn')) ndf = Long(n_deg_of_freedom) return TMath.Prob(chi2, ndf) def get_contour(self, parameter1, parameter2, n_points=21): ''' Returns a list of points (2-tuples) representing a sampling of the :math:`1\\sigma` contour of the TMinuit fit. The ``FCN`` has to be minimized before calling this. **parameter1** : int ID of the parameter to be displayed on the `x`-axis. **parameter2** : int ID of the parameter to be displayed on the `y`-axis. *n_points* : int (optional) number of points used to draw the contour. Default is 21. *returns* : 2-tuple of tuples a 2-tuple (x, y) containing ``n_points+1`` points sampled along the contour. The first point is repeated at the end of the list to generate a closed contour. ''' self.out_file.write('\n') # entry in log-file self.out_file.write('\n') self.out_file.write('#'*(5+28)) self.out_file.write('\n') self.out_file.write('# Contour for parameters %2d, %2d #\n'\ %(parameter1, parameter2) ) self.out_file.write('#'*(5+28)) self.out_file.write('\n\n') self.out_file.flush() # # first, make sure we are at minimum self.minimize(final_fit=True, log_print_level=0) # get the TGraph object from ROOT g = self.__gMinuit.Contour(n_points, parameter1, parameter2) # extract point data into buffers xbuf, ybuf = g.GetX(), g.GetY() N = g.GetN() # generate tuples from buffers x = np.frombuffer(xbuf, dtype=float, count=N) y = np.frombuffer(ybuf, dtype=float, count=N) # return (x, y) def get_profile(self, parid, n_points=21): ''' Returns a list of points (2-tuples) the profile the :math:`\\chi^2` of the TMinuit fit. **parid** : int ID of the parameter to be displayed on the `x`-axis. *n_points* : int (optional) number of points used for profile. Default is 21. *returns* : two arrays, par. values and corresp. :math:`\\chi^2` containing ``n_points`` sampled profile points. ''' self.out_file.write('\n') # entry in log-file self.out_file.write('\n') self.out_file.write('#'*(2+26)) self.out_file.write('\n') self.out_file.write("# Profile for parameter %2d #\n" % (parid)) self.out_file.write('#'*(2+26)) self.out_file.write('\n\n') self.out_file.flush() # redirect stdout stream _redirection_target = None ## -- disable redirection completely, for now ##if log_print_level >= 0: ## _redirection_target = self.out_file with redirect_stdout_to(_redirection_target): pv = [] chi2 = [] error_code = Long(0) self.__gMinuit.mnexcm("SET PRINT", arr('d', [0.0]), 1, error_code) # no printout # first, make sure we are at minimum, i.e. re-minimize self.minimize(final_fit=True, log_print_level=0) minuit_id = Double(parid + 1) # Minuit parameter numbers start with 1 # retrieve information about parameter with id=parid pmin = Double(0) perr = Double(0) self.__gMinuit.GetParameter(parid, pmin, perr) # retrieve fitresult # fix parameter parid ... self.__gMinuit.mnexcm("FIX", arr('d', [minuit_id]), 1, error_code) # ... and scan parameter values, minimizing at each point for v in np.linspace(pmin - 3.*perr, pmin + 3.*perr, n_points): pv.append(v) self.__gMinuit.mnexcm("SET PAR", arr('d', [minuit_id, Double(v)]), 2, error_code) self.__gMinuit.mnexcm("MIGRAD", arr('d', [self.max_iterations, self.tolerance]), 2, error_code) chi2.append(self.get_fit_info('fcn')) # release parameter to back to initial value and release self.__gMinuit.mnexcm("SET PAR", arr('d', [minuit_id, Double(pmin)]), 2, error_code) self.__gMinuit.mnexcm("RELEASE", arr('d', [minuit_id]), 1, error_code) return pv, chi2 # Other methods ################ def fix_parameter(self, parameter_number): ''' Fix parameter number <`parameter_number`>. **parameter_number** : int Number of the parameter to fix. ''' error_code = Long(0) logger.info("Fixing parameter %d in Minuit" % (parameter_number,)) # execute FIX command self.__gMinuit.mnexcm("FIX", arr('d', [parameter_number+1]), 1, error_code) def release_parameter(self, parameter_number): ''' Release parameter number <`parameter_number`>. **parameter_number** : int Number of the parameter to release. ''' error_code = Long(0) logger.info("Releasing parameter %d in Minuit" % (parameter_number,)) # execute RELEASE command self.__gMinuit.mnexcm("RELEASE", arr('d', [parameter_number+1]), 1, error_code) def reset(self): '''Execute TMinuit's `mnrset` method.''' self.__gMinuit.mnrset(0) # reset TMinuit def FCN_wrapper(self, number_of_parameters, derivatives, f, parameters, internal_flag): ''' This is actually a function called in *ROOT* and acting as a C wrapper for our `FCN`, which is implemented in Python. This function is called by `Minuit` several times during a fit. It doesn't return anything but modifies one of its arguments (*f*). This is *ugly*, but it's how *ROOT*'s ``TMinuit`` works. Its argument structure is fixed and determined by `Minuit`: **number_of_parameters** : int The number of parameters of the current fit **derivatives** : C array If the user chooses to calculate the first derivative of the function inside the `FCN`, this value should be written here. This interface to `Minuit` ignores this derivative, however, so calculating this inside the `FCN` has no effect (yet). **f** : C array The desired function value is in f[0] after execution. **parameters** : C array A C array of parameters. Is cast to a Python list **internal_flag** : int A flag allowing for different behaviour of the function. Can be any integer from 1 (initial run) to 4(normal run). See `Minuit`'s specification. ''' # Retrieve the parameters from the C side of ROOT and # store them in a Python list -- resource-intensive # for many calls, but can't be improved (yet?) parameter_list = np.frombuffer(parameters, dtype=float, count=self.number_of_parameters) # call the Python implementation of FCN. f[0] = self.function_to_minimize(*parameter_list) def minimize(self, final_fit=True, log_print_level=2): '''Do the minimization. This calls `Minuit`'s algorithms ``MIGRAD`` for minimization and, if `final_fit` is `True`, also ``HESSE`` for computing/checking the parameter error matrix.''' # Set the FCN again. This HAS to be done EVERY # time the minimize method is called because of # the implementation of SetFCN, which is not # object-oriented but sets a global pointer!!! logger.debug("Updating current FCN") self.__gMinuit.SetFCN(self.FCN_wrapper) # Run minimization algorithm (MIGRAD + HESSE) error_code = Long(0) prefix = "Minuit run on" # set the timestamp prefix # insert timestamp self.out_file.write('\n') self.out_file.write('#'*(len(prefix)+4+20)) self.out_file.write('\n') self.out_file.write("# %s " % (prefix,) + strftime("%Y-%m-%d %H:%M:%S #\n", gmtime())) self.out_file.write('#'*(len(prefix)+4+20)) self.out_file.write('\n\n') self.out_file.flush() # redirect stdout stream _redirection_target = None if log_print_level >= 0: _redirection_target = self.out_file with redirect_stdout_to(_redirection_target): self.__gMinuit.SetPrintLevel(log_print_level) # set Minuit print level logger.debug("Running MIGRAD") self.__gMinuit.mnexcm("MIGRAD", arr('d', [self.max_iterations, self.tolerance]), 2, error_code) if(final_fit): logger.debug("Running HESSE") self.__gMinuit.mnexcm("HESSE", arr('d', [self.max_iterations]), 1, error_code) # return to normal print level self.__gMinuit.SetPrintLevel(self.print_level) def minos_errors(self, log_print_level=1): ''' Get (asymmetric) parameter uncertainties from MINOS algorithm. This calls `Minuit`'s algorithms ``MINOS``, which determines parameter uncertainties using profiling of the chi2 function. returns : tuple A tuple of [err+, err-, parabolic error, global correlation] ''' # Set the FCN again. This HAS to be done EVERY # time the minimize method is called because of # the implementation of SetFCN, which is not # object-oriented but sets a global pointer!!! logger.debug("Updating current FCN") self.__gMinuit.SetFCN(self.FCN_wrapper) # redirect stdout stream _redirection_target = None if log_print_level >= 0: _redirection_target = self.out_file with redirect_stdout_to(_redirection_target): self.__gMinuit.SetPrintLevel(log_print_level) logger.debug("Running MINOS") error_code = Long(0) self.__gMinuit.mnexcm("MINOS", arr('d', [self.max_iterations]), 1, error_code) # return to normal print level self.__gMinuit.SetPrintLevel(self.print_level) output = [] errpos=Double(0) # positive parameter error errneg=Double(0) # negative parameter error err=Double(0) # parabolic error gcor=Double(0) # global correlation coefficient for i in range(0, self.number_of_parameters): self.__gMinuit.mnerrs(i, errpos, errneg, err, gcor) output.append([float(errpos),float(errneg),float(err),float(gcor)]) return output
class Minuit: ''' A class for communicating with ROOT's function minimizer tool Minuit. ''' def __init__(self, number_of_parameters, function_to_minimize, parameter_names, start_parameters, parameter_errors, quiet=True, verbose=False): ''' Create a Minuit minimizer for a function `function_to_minimize`. Necessary arguments are the number of parameters and the function to be minimized `function_to_minimize`. The function `function_to_minimize`'s arguments must be numerical values. The same goes for its output. Another requirement is for every parameter of `function_to_minimize` to have a default value. These are then used to initialize Minuit. **number_of_parameters** : int The number of parameters of the function to minimize. **function_to_minimize** : function The function which `Minuit` should minimize. This must be a Python function with <``number_of_parameters``> arguments. **parameter_names** : tuple/list of strings The parameter names. These are used to keep track of the parameters in `Minuit`'s output. **start_parameters** : tuple/list of floats The start values of the parameters. It is important to have a good, if rough, estimate of the parameters at the minimum before starting the minimization. Wrong initial parameters can yield a local minimum instead of a global one. **parameter_errors** : tuple/list of floats An initial guess of the parameter errors. These errors are used to define the initial step size. *quiet* : boolean (optional, default: ``True``) If ``True``, suppresses all output from ``TMinuit``. *verbose* : boolean (optional, default: ``False``) If ``True``, sets ``TMinuit``'s print level to a high value, so that all output is logged. ''' #: the name of this minimizer type self.name = "ROOT::TMinuit" #: the actual `FCN` called in ``FCN_wrapper`` self.function_to_minimize = function_to_minimize #: number of parameters to minimize for self.number_of_parameters = number_of_parameters if not quiet: self.out_file = open(log_file("minuit.log"), 'a') else: self.out_file = null_file() # create a TMinuit instance for that number of parameters self.__gMinuit = TMinuit(self.number_of_parameters) # instruct Minuit to use this class's FCN_wrapper method as a FCN self.__gMinuit.SetFCN(self.FCN_wrapper) # set print level according to flag if quiet: self.set_print_level(-1000) # suppress output elif verbose: self.set_print_level(10) # detailed output else: self.set_print_level(0) # frugal output # initialize minimizer self.set_err() self.set_strategy() self.set_parameter_values(start_parameters) self.set_parameter_errors(parameter_errors) self.set_parameter_names(parameter_names) #: maximum number of iterations until ``TMinuit`` gives up self.max_iterations = M_MAX_ITERATIONS #: ``TMinuit`` tolerance self.tolerance = M_TOLERANCE def update_parameter_data(self, show_warnings=False): """ (Re-)Sets the parameter names, values and step size on the C++ side of Minuit. """ error_code = Long(0) try: # Set up the starting fit parameters in TMinuit for i in range(0, self.number_of_parameters): self.__gMinuit.mnparm(i, self.parameter_names[i], self.current_parameters[i], 0.1 * self.parameter_errors[i], 0, 0, error_code) # use 10% of the par. 1-sigma errors as the initial step size except AttributeError as e: if show_warnings: logger.warn("Cannot update Minuit data on the C++ side. " "AttributeError: %s" % (e, )) return error_code # Set methods ############## def set_print_level(self, print_level=P_DETAIL_LEVEL): '''Sets the print level for Minuit. *print_level* : int (optional, default: 1 (frugal output)) Tells ``TMinuit`` how much output to generate. The higher this value, the more output it generates. ''' self.__gMinuit.SetPrintLevel(print_level) # set Minuit print level self.print_level = print_level def set_strategy(self, strategy_id=1): '''Sets the strategy Minuit. *strategy_id* : int (optional, default: 1 (optimized)) Tells ``TMinuit`` to use a certain strategy. Refer to ``TMinuit``'s documentation for available strategies. ''' error_code = Long(0) # execute SET STRATEGY command self.__gMinuit.mnexcm("SET STRATEGY", arr('d', [strategy_id]), 1, error_code) def set_err(self, up_value=1.0): '''Sets the ``UP`` value for Minuit. *up_value* : float (optional, default: 1.0) This is the value by which `FCN` is expected to change. ''' # Tell TMinuit to use an up-value of 1.0 error_code = Long(0) # execute SET ERR command self.__gMinuit.mnexcm("SET ERR", arr('d', [up_value]), 1, error_code) def set_parameter_values(self, parameter_values): ''' Sets the fit parameters. If parameter_values=`None`, tries to infer defaults from the function_to_minimize. ''' if len(parameter_values) == self.number_of_parameters: self.current_parameters = parameter_values else: raise Exception("Cannot get default parameter values from the \ FCN. Not all parameters have default values given.") self.update_parameter_data() def set_parameter_names(self, parameter_names): '''Sets the fit parameters. If parameter_values=`None`, tries to infer defaults from the function_to_minimize.''' if len(parameter_names) == self.number_of_parameters: self.parameter_names = parameter_names else: raise Exception("Cannot set param names. Tuple length mismatch.") self.update_parameter_data() def set_parameter_errors(self, parameter_errors=None): '''Sets the fit parameter errors. If parameter_values=`None`, sets the error to 10% of the parameter value.''' if parameter_errors is None: # set to 0.1% of the parameter value if not self.current_parameters is None: self.parameter_errors = [max(0.1, 0.1 * par) for par in self.current_parameters] else: raise Exception("Cannot set parameter errors. No errors \ provided and no parameters initialized.") elif len(parameter_errors) != len(self.current_parameters): raise Exception("Cannot set parameter errors. \ Tuple length mismatch.") else: self.parameter_errors = parameter_errors self.update_parameter_data() # Get methods ############## def get_error_matrix(self): '''Retrieves the parameter error matrix from TMinuit. return : `numpy.matrix` ''' # set up an array of type `double' to pass to TMinuit tmp_array = arr('d', [0.0]*(self.number_of_parameters**2)) # get parameter covariance matrix from TMinuit self.__gMinuit.mnemat(tmp_array, self.number_of_parameters) # reshape into 2D array return np.asmatrix( np.reshape( tmp_array, (self.number_of_parameters, self.number_of_parameters) ) ) def get_parameter_values(self): '''Retrieves the parameter values from TMinuit. return : tuple Current `Minuit` parameter values ''' result = [] # retrieve fit parameters p, pe = Double(0), Double(0) for i in range(0, self.number_of_parameters): self.__gMinuit.GetParameter(i, p, pe) # retrieve fitresult result.append(float(p)) return tuple(result) def get_parameter_errors(self): '''Retrieves the parameter errors from TMinuit. return : tuple Current `Minuit` parameter errors ''' result = [] # retrieve fit parameters p, pe = Double(0), Double(0) for i in range(0, self.number_of_parameters): self.__gMinuit.GetParameter(i, p, pe) # retrieve fitresult result.append(float(pe)) return tuple(result) def get_parameter_info(self): '''Retrieves parameter information from TMinuit. return : list of tuples ``(parameter_name, parameter_val, parameter_error)`` ''' result = [] # retrieve fit parameters p, pe = Double(0), Double(0) for i in range(0, self.number_of_parameters): self.__gMinuit.GetParameter(i, p, pe) # retrieve fitresult result.append((self.get_parameter_name(i), float(p), float(pe))) return result def get_parameter_name(self, parameter_nr): '''Gets the name of parameter number ``parameter_nr`` **parameter_nr** : int Number of the parameter whose name to get. ''' return self.parameter_names[parameter_nr] def get_fit_info(self, info): '''Retrieves other info from `Minuit`. **info** : string Information about the fit to retrieve. This can be any of the following: - ``'fcn'``: `FCN` value at minimum, - ``'edm'``: estimated distance to minimum - ``'err_def'``: `Minuit` error matrix status code - ``'status_code'``: `Minuit` general status code ''' # declare vars in which to retrieve other info fcn_at_min = Double(0) edm = Double(0) err_def = Double(0) n_var_param = Long(0) n_tot_param = Long(0) status_code = Long(0) # Tell TMinuit to update the variables declared above self.__gMinuit.mnstat(fcn_at_min, edm, err_def, n_var_param, n_tot_param, status_code) if info == 'fcn': return fcn_at_min elif info == 'edm': return edm elif info == 'err_def': return err_def elif info == 'status_code': try: return D_MATRIX_ERROR[status_code] except: return status_code def get_chi2_probability(self, n_deg_of_freedom): ''' Returns the probability that an observed :math:`\chi^2` exceeds the calculated value of :math:`\chi^2` for this fit by chance, even for a correct model. In other words, returns the probability that a worse fit of the model to the data exists. If this is a small value (typically <5%), this means the fit is pretty bad. For values below this threshold, the model very probably does not fit the data. n_def_of_freedom : int The number of degrees of freedom. This is typically :math:`n_\text{datapoints} - n_\text{parameters}`. ''' chi2 = Double(self.get_fit_info('fcn')) ndf = Long(n_deg_of_freedom) return TMath.Prob(chi2, ndf) def get_contour(self, parameter1, parameter2, n_points=21): ''' Returns a list of points (2-tuples) representing a sampling of the :math:`1\\sigma` contour of the TMinuit fit. The ``FCN`` has to be minimized before calling this. **parameter1** : int ID of the parameter to be displayed on the `x`-axis. **parameter2** : int ID of the parameter to be displayed on the `y`-axis. *n_points* : int (optional) number of points used to draw the contour. Default is 21. *returns* : 2-tuple of tuples a 2-tuple (x, y) containing ``n_points+1`` points sampled along the contour. The first point is repeated at the end of the list to generate a closed contour. ''' self.out_file.write('\n') # entry in log-file self.out_file.write('\n') self.out_file.write('#'*(5+28)) self.out_file.write('\n') self.out_file.write('# Contour for parameters %2d, %2d #\n'\ %(parameter1, parameter2) ) self.out_file.write('#'*(5+28)) self.out_file.write('\n\n') self.out_file.flush() # # first, make sure we are at minimum self.minimize(final_fit=True, log_print_level=0) # get the TGraph object from ROOT g = self.__gMinuit.Contour(n_points, parameter1, parameter2) # extract point data into buffers xbuf, ybuf = g.GetX(), g.GetY() N = g.GetN() # generate tuples from buffers x = np.frombuffer(xbuf, dtype=float, count=N) y = np.frombuffer(ybuf, dtype=float, count=N) # return (x, y) def get_profile(self, parid, n_points=21): ''' Returns a list of points (2-tuples) the profile the :math:`\\chi^2` of the TMinuit fit. **parid** : int ID of the parameter to be displayed on the `x`-axis. *n_points* : int (optional) number of points used for profile. Default is 21. *returns* : two arrays, par. values and corresp. :math:`\\chi^2` containing ``n_points`` sampled profile points. ''' self.out_file.write('\n') # entry in log-file self.out_file.write('\n') self.out_file.write('#'*(2+26)) self.out_file.write('\n') self.out_file.write("# Profile for parameter %2d #\n" % (parid)) self.out_file.write('#'*(2+26)) self.out_file.write('\n\n') self.out_file.flush() # redirect stdout stream _redirection_target = None ## -- disable redirection completely, for now ##if log_print_level >= 0: ## _redirection_target = self.out_file with redirect_stdout_to(_redirection_target): pv = [] chi2 = [] error_code = Long(0) self.__gMinuit.mnexcm("SET PRINT", arr('d', [0.0]), 1, error_code) # no printout # first, make sure we are at minimum, i.e. re-minimize self.minimize(final_fit=True, log_print_level=0) minuit_id = Double(parid + 1) # Minuit parameter numbers start with 1 # retrieve information about parameter with id=parid pmin = Double(0) perr = Double(0) self.__gMinuit.GetParameter(parid, pmin, perr) # retrieve fitresult # fix parameter parid ... self.__gMinuit.mnexcm("FIX", arr('d', [minuit_id]), 1, error_code) # ... and scan parameter values, minimizing at each point for v in np.linspace(pmin - 3.*perr, pmin + 3.*perr, n_points): pv.append(v) self.__gMinuit.mnexcm("SET PAR", arr('d', [minuit_id, Double(v)]), 2, error_code) self.__gMinuit.mnexcm("MIGRAD", arr('d', [self.max_iterations, self.tolerance]), 2, error_code) chi2.append(self.get_fit_info('fcn')) # release parameter to back to initial value and release self.__gMinuit.mnexcm("SET PAR", arr('d', [minuit_id, Double(pmin)]), 2, error_code) self.__gMinuit.mnexcm("RELEASE", arr('d', [minuit_id]), 1, error_code) return pv, chi2 # Other methods ################ def fix_parameter(self, parameter_number): ''' Fix parameter number <`parameter_number`>. **parameter_number** : int Number of the parameter to fix. ''' error_code = Long(0) logger.info("Fixing parameter %d in Minuit" % (parameter_number,)) # execute FIX command self.__gMinuit.mnexcm("FIX", arr('d', [parameter_number+1]), 1, error_code) def release_parameter(self, parameter_number): ''' Release parameter number <`parameter_number`>. **parameter_number** : int Number of the parameter to release. ''' error_code = Long(0) logger.info("Releasing parameter %d in Minuit" % (parameter_number,)) # execute RELEASE command self.__gMinuit.mnexcm("RELEASE", arr('d', [parameter_number+1]), 1, error_code) def reset(self): '''Execute TMinuit's `mnrset` method.''' self.__gMinuit.mnrset(0) # reset TMinuit def FCN_wrapper(self, number_of_parameters, derivatives, f, parameters, internal_flag): ''' This is actually a function called in *ROOT* and acting as a C wrapper for our `FCN`, which is implemented in Python. This function is called by `Minuit` several times during a fit. It doesn't return anything but modifies one of its arguments (*f*). This is *ugly*, but it's how *ROOT*'s ``TMinuit`` works. Its argument structure is fixed and determined by `Minuit`: **number_of_parameters** : int The number of parameters of the current fit **derivatives** : C array If the user chooses to calculate the first derivative of the function inside the `FCN`, this value should be written here. This interface to `Minuit` ignores this derivative, however, so calculating this inside the `FCN` has no effect (yet). **f** : C array The desired function value is in f[0] after execution. **parameters** : C array A C array of parameters. Is cast to a Python list **internal_flag** : int A flag allowing for different behaviour of the function. Can be any integer from 1 (initial run) to 4(normal run). See `Minuit`'s specification. ''' # Retrieve the parameters from the C side of ROOT and # store them in a Python list -- resource-intensive # for many calls, but can't be improved (yet?) parameter_list = np.frombuffer(parameters, dtype=float, count=self.number_of_parameters) # call the Python implementation of FCN. f[0] = self.function_to_minimize(*parameter_list) def minimize(self, final_fit=True, log_print_level=2): '''Do the minimization. This calls `Minuit`'s algorithms ``MIGRAD`` for minimization and, if `final_fit` is `True`, also ``HESSE`` for computing/checking the parameter error matrix.''' # Set the FCN again. This HAS to be done EVERY # time the minimize method is called because of # the implementation of SetFCN, which is not # object-oriented but sets a global pointer!!! logger.debug("Updating current FCN") self.__gMinuit.SetFCN(self.FCN_wrapper) # Run minimization algorithm (MIGRAD + HESSE) error_code = Long(0) prefix = "Minuit run on" # set the timestamp prefix # insert timestamp self.out_file.write('\n') self.out_file.write('#'*(len(prefix)+4+20)) self.out_file.write('\n') self.out_file.write("# %s " % (prefix,) + strftime("%Y-%m-%d %H:%M:%S #\n", gmtime())) self.out_file.write('#'*(len(prefix)+4+20)) self.out_file.write('\n\n') self.out_file.flush() # redirect stdout stream _redirection_target = None if log_print_level >= 0: _redirection_target = self.out_file with redirect_stdout_to(_redirection_target): self.__gMinuit.SetPrintLevel(log_print_level) # set Minuit print level logger.debug("Running MIGRAD") self.__gMinuit.mnexcm("MIGRAD", arr('d', [self.max_iterations, self.tolerance]), 2, error_code) if(final_fit): logger.debug("Running HESSE") self.__gMinuit.mnexcm("HESSE", arr('d', [self.max_iterations]), 1, error_code) # return to normal print level self.__gMinuit.SetPrintLevel(self.print_level) def minos_errors(self, log_print_level=1): ''' Get (asymmetric) parameter uncertainties from MINOS algorithm. This calls `Minuit`'s algorithms ``MINOS``, which determines parameter uncertainties using profiling of the chi2 function. returns : tuple A tuple of [err+, err-, parabolic error, global correlation] ''' # Set the FCN again. This HAS to be done EVERY # time the minimize method is called because of # the implementation of SetFCN, which is not # object-oriented but sets a global pointer!!! logger.debug("Updating current FCN") self.__gMinuit.SetFCN(self.FCN_wrapper) # redirect stdout stream _redirection_target = None if log_print_level >= 0: _redirection_target = self.out_file with redirect_stdout_to(_redirection_target): self.__gMinuit.SetPrintLevel(log_print_level) logger.debug("Running MINOS") error_code = Long(0) self.__gMinuit.mnexcm("MINOS", arr('d', [self.max_iterations]), 1, error_code) # return to normal print level self.__gMinuit.SetPrintLevel(self.print_level) output = [] errpos=Double(0) # positive parameter error errneg=Double(0) # negative parameter error err=Double(0) # parabolic error gcor=Double(0) # global correlation coefficient for i in range(0, self.number_of_parameters): self.__gMinuit.mnerrs(i, errpos, errneg, err, gcor) output.append([float(errpos),float(errneg),float(err),float(gcor)]) return output
# 3 = accurate # 2 = forced pos.def # 1 = approximative # 0 = not calculated myMinuit.mnprin(3,amin) # print−out by Minuit #−−> get results from MINUIT finalPar=[] finalParErr=[] p, pe=Double(0), Double(0) for i in range(0, npar): myMinuit.GetParameter(i, p, pe) # retrieve parameters and errors finalPar.append(float(p)) finalParErr.append(float(pe)) #get covariance matrix buf=arr('d',npar*npar*[0.]) myMinuit.mnemat(buf,npar) # retrieve error matrix emat=np.array(buf).reshape(npar,npar) #−−> provide formatted output of results print "\n" print "*==*MINUIT fit completed:" print 'fcn@minimum = %.3g'%(amin), "error code =", ierflg, "status = ", icstat print "Results: \t value error corr.mat." for i in range(0,npar): print' %s: \t%10.3e +/- %.1e '%(name[i], finalPar[i], finalParErr[i]), for j in range(0,i): print'%+.3g'%(emat[i][j]/np.sqrt(emat[i][i])/np.sqrt(emat[j][j])), print'' #−−> plot result using matplotlib plt.figure() plt.errorbar(ax, ay, yerr=ey, fmt="o", label='data') # the data
class MinimizerROOTTMinuit(MinimizerBase): def __init__(self, parameter_names, parameter_values, parameter_errors, function_to_minimize, strategy = 1): self._par_names = parameter_names self._strategy = strategy self._func_handle = function_to_minimize self._err_def = 1.0 self._tol = 0.001 # initialize the minimizer parameter specification self._minimizer_param_dict = {} assert len(parameter_names) == len(parameter_values) == len(parameter_errors) self._par_val = [] self._par_err = [] self._par_fixed_mask = [] self._par_limits = [] for _pn, _pv, _pe in zip(parameter_names, parameter_values, parameter_errors): self._par_val.append(_pv) self._par_err.append(_pe) self._par_fixed_mask.append(False) self._par_limits.append(None) # TODO: limits/fixed parameters self.__gMinuit = None # cache for calculations self._hessian = None self._hessian_inv = None self._fval = None self._par_cov_mat = None self._par_cor_mat = None self._par_asymm_err = None self._fmin_struct = None self._pars_contour = None self._min_result_stale = True self._printed_inf_cost_warning = False # -- private methods # def _invalidate_cache(self): # self._par_val = None # self._par_err = None # self._hessian = None # self._hessian_inv = None # self._fval = None # self._par_cov_mat = None # self._par_cor_mat = None # self._par_asymm_err_dn = None # self._par_asymm_err_up = None # self._fmin_struct = None # self._pars_contour = None def _recreate_gMinuit(self): self.__gMinuit = TMinuit(self.n_pars) self.__gMinuit.SetPrintLevel(-1) self.__gMinuit.mncomd("SET STRATEGY {}".format(self._strategy), Long(0)) self.__gMinuit.SetFCN(self._minuit_fcn) self.__gMinuit.SetErrorDef(self._err_def) # set gMinuit parameters error_code = Long(0) for _pid, (_pn, _pv, _pe) in enumerate(zip(self._par_names, self._par_val, self._par_err)): self.__gMinuit.mnparm(_pid, _pn, _pv, 0.1 * _pe, 0, 0, error_code) err_code = Long(0) # set fixed parameters for _par_id, _pf in enumerate(self._par_fixed_mask): if _pf: self.__gMinuit.mnfixp(_par_id, err_code) # set parameter limits for _par_id, _pl in enumerate(self._par_limits): if _pl is not None: _lo_lim, _up_lim = _pl self.__gMinuit.mnexcm("SET LIM", arr('d', [_par_id + 1, _lo_lim, _up_lim]), 3, error_code) def _get_gMinuit(self): if self.__gMinuit is None: self._recreate_gMinuit() return self.__gMinuit def _migrad(self, max_calls=6000): # need to set the FCN explicitly before every call self._get_gMinuit().SetFCN(self._minuit_fcn) error_code = Long(0) self._get_gMinuit().mnexcm("MIGRAD", arr('d', [max_calls, self.tolerance]), 2, error_code) def _hesse(self, max_calls=6000): # need to set the FCN explicitly before every call self._get_gMinuit().SetFCN(self._minuit_fcn) error_code = Long(0) self._get_gMinuit().mnexcm("HESSE", arr('d', [max_calls]), 1, error_code) def _minuit_fcn(self, number_of_parameters, derivatives, f, parameters, internal_flag): """ This is actually a function called in *ROOT* and acting as a C wrapper for our `FCN`, which is implemented in Python. This function is called by `Minuit` several times during a fitters. It doesn't return anything but modifies one of its arguments (*f*). This is *ugly*, but it's how *ROOT*'s ``TMinuit`` works. Its argument structure is fixed and determined by `Minuit`: **number_of_parameters** : int The number of parameters of the current fitters **derivatives** : C array If the user chooses to calculate the first derivative of the function inside the `FCN`, this value should be written here. This interface to `Minuit` ignores this derivative, however, so calculating this inside the `FCN` has no effect (yet). **f** : C array The desired function value is in f[0] after execution. **parameters** : C array A C array of parameters. Is cast to a Python list **internal_flag** : int A flag allowing for different behaviour of the function. Can be any integer from 1 (initial run) to 4(normal run). See `Minuit`'s specification. """ # Retrieve the parameters from the C side of ROOT and # store them in a Python list -- resource-intensive # for many calls, but can't be improved (yet?) parameter_list = np.frombuffer(parameters, dtype=float, count=self.n_pars) # call the Python implementation of FCN. f[0] = self._func_wrapper(*parameter_list) def _insert_zeros_for_fixed(self, submatrix): """ Takes the partial error matrix (submatrix) and adds rows and columns with 0.0 where the fixed parameters should go. """ _mat = submatrix # reduce the matrix before inserting zeros _n_pars_free = self.n_pars_free _mat = _mat[0:_n_pars_free,0:_n_pars_free] _fparam_ids = [_par_id for _par_id, _p in enumerate(self._par_fixed_mask) if _p] for _id in _fparam_ids: _mat = np.insert(np.insert(_mat, _id, 0., axis=0), _id, 0., axis=1) return _mat # -- public properties def get_fit_info(self, info): '''Retrieves other info from `Minuit`. **info** : string Information about the fit to retrieve. This can be any of the following: - ``'fcn'``: `FCN` value at minimum, - ``'edm'``: estimated distance to minimum - ``'err_def'``: `Minuit` error matrix status code - ``'status_code'``: `Minuit` general status code ''' # declare vars in which to retrieve other info fcn_at_min = Double(0) edm = Double(0) err_def = Double(0) n_var_param = Long(0) n_tot_param = Long(0) status_code = Long(0) # Tell TMinuit to update the variables declared above self.__gMinuit.mnstat(fcn_at_min, edm, err_def, n_var_param, n_tot_param, status_code) if info == 'fcn': return fcn_at_min elif info == 'edm': return edm elif info == 'err_def': return err_def elif info == 'status_code': try: return D_MATRIX_ERROR[status_code] except: return status_code @property def n_pars(self): return len(self.parameter_names) @property def n_pars_free(self): return len([_p for _p in self._par_fixed_mask if not _p]) @property def errordef(self): return self._err_def @errordef.setter def errordef(self, err_def): assert err_def > 0 self._err_def = err_def if self.__gMinuit is not None: self.__gMinuit.set_errordef(err_def) self._min_result_stale = True @property def tolerance(self): return self._tol @tolerance.setter def tolerance(self, tolerance): assert tolerance > 0 self._tol = tolerance self._min_result_stale = True @property def hessian(self): # TODO: cache this return 2.0 * self.errordef * np.linalg.inv(self.cov_mat) @property def cov_mat(self): if self._min_result_stale: raise MinimizerROOTTMinuitException("Cannot get cov_mat: Minimizer result is outdated.") if self._par_cov_mat is None: _n_pars_total = self.n_pars _n_pars_free = self.n_pars_free _tmp_mat_array = arr('d', [0.0]*(_n_pars_total**2)) # get parameter covariance matrix from TMinuit self.__gMinuit.mnemat(_tmp_mat_array, _n_pars_total) # reshape into 2D array _sub_cov_mat = np.asarray( np.reshape( _tmp_mat_array, (_n_pars_total, _n_pars_total) ) ) self._par_cov_mat = self._insert_zeros_for_fixed(_sub_cov_mat) return self._par_cov_mat @property def cor_mat(self): if self._min_result_stale: raise MinimizerROOTTMinuitException("Cannot get cor_mat: Minimizer result is outdated.") if self._par_cor_mat is None: _cov_mat = self.cov_mat # TODO: use CovMat object! # Note: for zeros on cov_mat diagonals (which occur for fixed parameters) -> overwrite with 1.0 _sqrt_diag = np.array([_err if _err>0 else 1.0 for _err in np.sqrt(np.diag(_cov_mat))]) self._par_cor_mat = np.asarray(_cov_mat) / np.outer(_sqrt_diag, _sqrt_diag) return self._par_cor_mat @property def hessian_inv(self): return self.cov_mat / 2.0 / self.errordef @property def parameter_values(self): return self._par_val @property def parameter_errors(self): return self._par_err @property def parameter_names(self): return self._par_names # -- private "properties" # -- public methods def fix(self, parameter_name): # set local flag _par_id = self.parameter_names.index(parameter_name) if self._par_fixed_mask[_par_id]: return # par is already fixed self._par_fixed_mask[_par_id] = True if self.__gMinuit is not None: # also update Minuit instance err_code = Long(0) self.__gMinuit.mnfixp(_par_id, err_code) # self.__gMinuit.mnexcm("FIX", # arr('d', [_par_id+1]), 1, error_code) self._min_result_stale = True def fix_several(self, parameter_names): for _pn in parameter_names: self.fix(_pn) def release(self, parameter_name): # set local flag _par_id = self.parameter_names.index(parameter_name) if not self._par_fixed_mask[_par_id]: return # par is already released self._par_fixed_mask[_par_id] = False if self.__gMinuit is not None: # also update Minuit instance self.__gMinuit.mnfree(-_par_id-1) # self.__gMinuit.mnexcm("RELEASE", # arr('d', [_par_id+1]), 1, error_code) self._min_result_stale = True def release_several(self, parameter_names): for _pn in parameter_names: self.release(_pn) def limit(self, parameter_name, parameter_bounds): assert len(parameter_bounds) == 2 # set local flag _par_id = self.parameter_names.index(parameter_name) if self._par_limits[_par_id] == parameter_bounds: return # same limits already set self._par_limits[_par_id] = parameter_bounds if self.__gMinuit is not None: _lo_lim, _up_lim = self._par_limits[_par_id] # also update Minuit instance error_code = Long(0) self.__gMinuit.mnexcm("SET LIM", arr('d', [_par_id+1, _lo_lim, _up_lim]), 3, error_code) self._min_result_stale = True def unlimit(self, parameter_name): # set local flag _par_id = self.parameter_names.index(parameter_name) if self._par_limits[_par_id] is None: return # parameter is already unlimited self._par_limits[_par_id] = None if self.__gMinuit is not None: # also update Minuit instance error_code = Long(0) self.__gMinuit.mnexcm("SET LIM", arr('d', [_par_id+1]), 1, error_code) self._min_result_stale = True def minimize(self, max_calls=6000): self._migrad(max_calls=max_calls) # retrieve fitters parameters self._par_val = [] self._par_err = [] _pv, _pe = Double(0), Double(0) for _par_id in six.moves.range(0, self.n_pars): self.__gMinuit.GetParameter(_par_id, _pv, _pe) # retrieve fitresult self._par_val.append(float(_pv)) self._par_err.append(float(_pe)) self._min_result_stale = False def contour(self, parameter_name_1, parameter_name_2, sigma=1.0, **minimizer_contour_kwargs): if self.__gMinuit is None: raise MinimizerROOTTMinuitException("Need to perform a fit before calling contour()!") _numpoints = minimizer_contour_kwargs.pop("numpoints", 100) if minimizer_contour_kwargs: raise MinimizerROOTTMinuitException("Unknown parameters: {}".format(minimizer_contour_kwargs)) _id_1 = self.parameter_names.index(parameter_name_1) _id_2 = self.parameter_names.index(parameter_name_2) self.__gMinuit.SetErrorDef(sigma ** 2) _t_graph = self.__gMinuit.Contour(_numpoints, _id_1, _id_2) self.__gMinuit.SetErrorDef(self._err_def) _x_buffer, _y_buffer = _t_graph.GetX(), _t_graph.GetY() _N = _t_graph.GetN() _x = np.frombuffer(_x_buffer, dtype=float, count=_N) _y = np.frombuffer(_y_buffer, dtype=float, count=_N) self._func_handle(*self.parameter_values) return ContourFactory.create_xy_contour((_x, _y), sigma) def profile(self, parameter_name, bins=21, bound=2, args=None, subtract_min=False): if self.__gMinuit is None: raise MinimizerROOTTMinuitException("Need to perform a fit before calling profile()!") MAX_ITERATIONS = 6000 _error_code = Long(0) _minuit_id = Long(self.parameter_names.index(parameter_name) + 1) _par_min = Double(0) _par_err = Double(0) self.__gMinuit.GetParameter(_minuit_id - 1, _par_min, _par_err) _x = np.linspace(start=_par_min - bound * _par_err, stop=_par_min + bound * _par_err, num=bins, endpoint=True) self.__gMinuit.mnexcm("FIX", arr('d', [_minuit_id]), 1, _error_code) _y = np.zeros(bins) for i in range(bins): self.__gMinuit.mnexcm("SET PAR", arr('d', [_minuit_id, Double(_x[i])]), 2, _error_code) self.__gMinuit.mnexcm("MIGRAD", arr('d', [MAX_ITERATIONS, self.tolerance]), 2, _error_code) _y[i] = self.get_fit_info("fcn") self.__gMinuit.mnexcm("RELEASE", arr('d', [_minuit_id]), 1, _error_code) self._migrad() self.__gMinuit.mnexcm("SET PAR", arr('d', [_minuit_id, Double(_par_min)]), 2, _error_code) return np.asarray((_x, _y))