def __init__(self, orig=None, description='unknown'): if orig is None: pyobs.check_type(description, 'text', str) self.description = description self.www = [ pwd.getpwuid(os.getuid())[0], os.uname()[1], datetime.datetime.now().strftime('%c') ] self.shape = [] self.size = 0 self.mean = [] self.ename = [] self.delta = {} self.cdata = {} else: if isinstance(orig, observable): self.description = orig.description self.www = orig.www self.shape = orig.shape self.size = numpy.prod(self.shape) self.mean = numpy.array(orig.mean) # or orig.mean.copy() self.ename = [e for e in orig.ename] self.delta = {} for key in orig.delta: self.delta[key] = orig.delta[key].copy() self.cdata = {} for key in orig.cdata: self.cdata[key] = cdata(orig.cdata[key].cov) pyobs.memory.add(self) else: raise pyobs.PyobsError('Unexpected orig argument') pyobs.memory.add(self)
def add_syst_err(self, name, err): """ Add a systematic error to the observable Parameters: name (str): label that uniquely identifies the syst. error err (array): array with the same dimensions of the observable with the systematic error Examples: >>> data = [0.198638, 0.403983, 1.215960, 1.607684, 0.199049, ... ] >>> vec = pyobs.observable(description='vector') >>> vec.create('A',data,shape=(4,)) >>> print(vec) 0.201(13) 0.399(26) 1.199(24) 1.603(47) >>> vec.add_syst_err('syst.err',[0.05,0.05,0,0]) >>> print(vec) 0.201(52) 0.399(56) 1.199(24) 1.603(47) """ pyobs.check_type(name, 'name', str) if name in self.cdata: raise pyobs.PyobsError(f'Label {name} already used') if numpy.shape(err) != self.shape: raise pyobs.PyobsError( f'Unexpected error, dimensions do not match {self.shape}') cov = numpy.reshape(numpy.array(err)**2, (self.size, )) self.cdata[name] = cdata(cov) pyobs.memory.update(self)
def create_from_cov(self, cname, value, covariance): """ Create observables based on covariance matrices Parameters: cname (str): label that uniquely identifies the data set value (array): central value of the observable; only 1-D arrays are supported covariance (array): a 2-D covariance matrix; if `covariance` is a 1-D array of same length as `value`, a diagonal covariance matrix is assumed. Examples: >>> mpi = pyobs.observable(description='pion masses, charged and neutral') >>> mpi.create_cd('mpi-pdg18',[139.57061,134.9770],[0.00023**2,0.0005**2]) >>> print(mpi) 139.57061(23) 134.97700(50) """ if isinstance(value, (int, float, numpy.float64, numpy.float32)): self.mean = numpy.reshape(value, (1, )) cov = numpy.reshape(covariance, (1, )) else: self.mean = numpy.array(value) cov = numpy.array(covariance) self.shape = numpy.shape(self.mean) if numpy.ndim(self.shape) != 1: raise pyobs.PyobsError( f'Unexpected value, only 1-D arrays are supported') self.size = numpy.prod(self.shape) if cov.shape != (self.size, ) and cov.shape != (self.size, self.size): raise pyobs.PyobsError( f'Unexpected shape for covariance {cov.shape}') pyobs.check_type(cname, 'cname', str) self.cdata[cname] = cdata(cov) pyobs.memory.update(self)
def __call__(self, yobs, p0=None, min_search=None): if len(self.csq) > 1: pyobs.check_type(yobs, 'yobs', list) else: if isinstance(yobs, pyobs.observable): yobs = [yobs] if len(yobs) != len(self.csq): raise pyobs.PyobsError( f'Unexpected number of observables for {len(self.csq)} fits') if p0 is None: p0 = [1.0] * len(self.pdict) if min_search is None: min_search = lm def csq(p0): res = 0.0 for i in range(len(yobs)): self.csq[i].set_pars(self.pdict, p0) res += self.csq[i](yobs[i].mean) return res dcsq = lambda x: sum([ self.csq[i].grad(yobs[i].mean, self.pdict) for i in range(len(yobs)) ]) ddcsq = lambda x: sum([ self.csq[i].hess(yobs[i].mean, self.pdict) for i in range(len(yobs)) ]) t0 = time() res = min_search(csq, p0, jac=dcsq, hess=ddcsq) # properly create gradients H = self.csq[0].Hmat(self.pdict, res.x) for i in range(1, len(yobs)): H += self.csq[i].Hmat(self.pdict, res.x) Hinv = numpy.linalg.inv(H) g = [] for i in range(len(yobs)): tmp = self.csq[i].gvec(self.pdict, res.x) g.append(pyobs.gradient(Hinv @ tmp)) if pyobs.is_verbose('mfit.run') or pyobs.is_verbose('mfit'): print(f'chisquare = {res.fun}') print(f'minimizer iterations = {res.nit}') print(f'minimizer status: {res.message}') print(f'mfit.run executed in {time()-t0:g} secs') return pyobs.derobs(yobs, res.x, g)
def error_bias4(x, f): pyobs.check_type(x, 'x', pyobs.observable) [x0, dx0] = x.error() bias4 = numpy.zeros((x.size, )) hess = num_hess(x0, f) for key in x.delta: oid = numpy.array(x.delta[key].mask) idx = numpy.ix_(oid, oid) d2 = numpy.einsum('abc,bj,cj->aj', hess[:, idx[0], idx[1]], x.delta[key].delta, x.delta[key].delta) dd2 = numpy.sum(d2, axis=1) bias4 += dd2**2 / x.delta[key].n**4 return numpy.sqrt(bias4)
def eval(self, xax, pars): """ Evaluates the function on a list of coordinates using the parameters obtained from a :math:`\chi^2` minimization. Parameters: xax (array,list of arrays) : the coordinates :math:`x_i^\mu` where the function must be evaluated. For combined fits, a list of arrays must be passed, one for each fit. pars (obs) : the observable returned by calling this class Returns: list of obs : observables corresponding to the functions evaluated at the coordinates `xax`. Examples: >>> fit1 = mfit(xax,W,f,df) >>> pars = fit1(yobs1) >>> print(pars) 0.925(35) 2.050(19) >>> xax = numpy.arange(0,10,0.2) >>> yeval = fit1.eval(xax, pars) """ if not type(xax) is list: xax = [xax] pyobs.check_type(pars, 'pars', pyobs.observable) N = len(xax) if N != len(self.csq): raise pyobs.PyobsError( f'Coordinates and Paramters do not match number of internal functions' ) out = [] for ic in self.csq: [m, g] = self.csq[ic].eval(xax[ic], self.pdict, pars.mean) out.append(pyobs.derobs([pars], m, [pyobs.gradient(g)])) return out
def derobs(inps, mean, grads, description=None): t0 = time() pyobs.check_type(inps, 'inps', list) pyobs.check_type(mean, 'mean', numpy.ndarray, int, float, numpy.float32, numpy.float64) pyobs.check_type(grads, 'grads', list) if len(inps) != len(grads): raise pyobs.PyobsError('Incompatible inps and grads') if description is None: description = ', '.join(set([i.description for i in inps])) res = pyobs.observable(description=description) res.set_mean(mean) allkeys = [] for i in inps: for dn in i.delta: if not dn in allkeys: allkeys.append(dn) for key in allkeys: new_idx = [] new_mask = [] lat = None for i in range(len(inps)): if key in inps[i].delta: data = inps[i].delta[key] h = grads[i].get_mask(data.mask) if not h is None: new_mask += h if not new_idx: new_idx = data.idx else: new_idx = merge_idx(new_idx, data.idx) if lat is None: lat = data.lat else: if numpy.any(lat != data.lat): # pragma: no cover raise pyobs.PyobsError( f'Unexpected lattice size for master fields with same tag' ) if len(new_mask) > 0: res.delta[key] = delta(list(set(new_mask)), new_idx, lat=lat) for i in range(len(inps)): if key in inps[i].delta: res.delta[key].axpy(grads[i], inps[i].delta[key]) res.ename = [] for key in res.delta: name = key.split(':')[0] if not name in res.ename: res.ename.append(name) res.cdata = {} allkeys = [] for i in inps: for cd in i.cdata: if not cd in allkeys: allkeys.append(cd) for key in allkeys: for i in range(len(inps)): if key in inps[i].cdata: if not key in res.cdata: res.cdata[key] = cdata(numpy.zeros(res.size)) res.cdata[key].axpy(grads[i], inps[i].cdata[key]) pyobs.memory.update(res) if pyobs.is_verbose('derobs'): print(f'derobs executed in {time()-t0:g} secs') return res
def create(self, ename, data, icnfg=None, rname=None, shape=(1, ), lat=None): """ Create an observable Parameters: ename (str): label of the ensemble data (array, list of arrays): the data generated from a single or multiple replica icnfg (array of ints or list of arrays of ints, optional): indices of the configurations corresponding to data; if not passed the measurements are assumed to be contiguous rname (str or list of str, optional): identifier of the replica; if not passed integers from 0 are automatically assigned shape (tuple, optional): shape of the observable, data must be passed accordingly lat (list of ints, optional): the size of each dimension of the master-field; if passed data is assumed to be obtained from observables measured at different sites and `icnfg` is re-interpreted as the index labeling the sites; if `icnfg` is not passed data is assumed to be contiguous on all sites. Note: For data and icnfg array can mean either a list or a 1-D numpy.array. If the observable has already been created, calling create again will add a new replica to the same ensemble. Examples: >>> data = [0.43, 0.42, ... ] # a scalar observable >>> a = pyobs.observable(description='test') >>> a.create('EnsembleA',data) >>> data0 = [0.43,0.42, ... ] # replica 0 >>> data1 = [0.40,0.41, ... ] # replica 1 >>> a = pyobs.observable(description='test') >>> a.create('EnsembleA',[data0,data1],rname=['r0','r1']) >>> data = [0.43, 0.42, 0.44, ... ] >>> icnfg= [ 10, 11, 13, ... ] >>> a = pyobs.observable(description='test') >>> a.create('EnsembleA',data,icnfg=icnfg) >>> data = [1.0, 2.0, 3.0, 4.0, ... ] >>> a = pyobs.observable(description='matrix') >>> a.create('EnsembleA',data,shape=(2,2)) Examples: >>> data = [0.43, 0.42, 0.44, ... ] >>> lat = [64,32,32,32] >>> a = pyobs.observable(description='test-mf') >>> a.create('EnsembleA',data,lat=lat) >>> data = [0.43, 0.42, 0.44, ... ] >>> idx = [0, 2, 4, 6, ...] # measurements on all even points of time-slice >>> lat = [32, 32, 32] >>> a = pyobs.observable(description='test-mf') >>> a.create('EnsembleA',data,lat=lat,icnfg=idx) """ t0 = time() pyobs.check_type(ename, 'ename', str) if ':' in ename: raise pyobs.PyobsError( f'Column symbol not allowed in ename {ename}') pyobs.check_type(shape, 'shape', tuple) self.shape = shape self.size = numpy.prod(shape) mask = range(self.size) if not ename in self.ename: self.ename.append(ename) if isinstance(data[0], (list, numpy.ndarray)): R = len(data) elif isinstance(data[0], (int, float, numpy.float64, numpy.float32)): R = 1 else: raise pyobs.PyobsError(f'Unexpected data type') if R == 1: pyobs.check_type(data, f'data', list, numpy.ndarray) nc = int(len(data) / self.size) if rname is None: rname = 0 else: pyobs.check_not_type(rname, 'rname', list) if icnfg is None: icnfg = range(nc) else: pyobs.check_type(icnfg, 'icnfg', list, range) pyobs.check_type(icnfg[0], 'icnfg[:]', int, numpy.int32, numpy.int64) if len(icnfg) * self.size != len(data): raise pyobs.PyobsError( f'Incompatible icnfg and data, for shape={shape}') if numpy.size(self.mean) != 0: N0 = sum([self.delta[key].n for key in self.delta]) mean0 = numpy.reshape(self.mean, (self.size, )) mean1 = numpy.mean(numpy.reshape(data, (nc, self.size)), 0) self.mean = (N0 * mean0 + nc * mean1) / (N0 + nc) shift = nc * (mean0 - mean1) / (N0 + nc) for key in self.delta: self.delta[key].delta += shift[:, None] else: self.mean = numpy.mean(numpy.reshape(data, (nc, self.size)), 0) key = f'{ename}:{rname}' self.delta[key] = delta(mask, icnfg, data, self.mean, lat) else: if numpy.size(self.mean) != 0: raise pyobs.PyobsError( 'Only a single replica can be added to existing observables' ) for ir in range(R): pyobs.check_type(data[ir], f'data[{ir}]', list, numpy.ndarray) self.mean = numpy.zeros((self.size, )) nt = 0 for dd in data: nc = int(len(dd) / self.size) self.mean += numpy.sum(numpy.reshape(dd, (nc, self.size)), 0) nt += nc self.mean *= 1.0 / float(nt) if rname is None: rname = range(R) else: pyobs.check_type(rname, 'rname', list) if len(rname) != R: raise pyobs.PyobsError('Incompatible rname and data') if not icnfg is None: pyobs.check_type(icnfg, 'icnfg', list) if icnfg is None: icnfg = [] for ir in range(len(data)): nc = int(len(data[ir]) / self.size) icnfg.append(range(nc)) else: for ir in range(len(data)): if len(icnfg[ir]) * self.size != len(data[ir]): raise pyobs.PyobsError( f'Incompatible icnfg[{ir}] and data[{ir}], for shape={shape}' ) for ir in range(len(data)): key = f'{ename}:{rname[ir]}' self.delta[key] = delta(mask, icnfg[ir], data[ir], self.mean, lat) self.mean = numpy.reshape(self.mean, self.shape) pyobs.memory.update(self) if pyobs.is_verbose('create'): print(f'create executed in {time()-t0:g} secs')
def diff(f, x, dx): """ Utility function to compute the gradient and hessian of a function using symbolic calculus Parameters: f (string): the reference function x (string): variables that are not differentiated; different variables must be separated by a comma dx (string): variables that are differentiated; different variables must be separated by a comma Returns: lambda : (scalar) function `f` lambda : (vector) function of the gradient of `f` lambda : (matrix) function of the hessian of `f` Notes: The symbolic manipulation is based on the library `sympy` and the user must follow the `sympy` syntax when passing the argument `f`. The analytic form of the gradient and hessian can be printed by activating the 'diff' verbose flag. Examples: >>> res = diff('a+b*x','x','a,b') # differentiate wrt to a and b a + b*x [1, x] [[0, 0], [0, 0]] >>> for i in range(3): >>> print(res[i](0.4,1.,2.)) 1.8 [1, 0.4] [[0, 0], [0, 0]] """ pyobs.check_type(f, 'f', str) pyobs.check_type(x, 'x', str) pyobs.check_type(dx, 'dx', str) sym = {} for y in dx.rsplit(','): sym[y] = sympy.Symbol(y) expr = parse_expr(f, local_dict=sym) allvars = f'{x},{dx}' func = sympy.lambdify(allvars, expr, 'numpy') dexpr = [] ddexpr = [] for y in sym: dexpr.append(sympy.diff(expr, sym[y])) tmp = [] for z in sym: tmp.append(sympy.diff(dexpr[-1], sym[z])) ddexpr.append(tmp) if pyobs.is_verbose('diff') or pyobs.is_verbose('symbolic.diff'): display(expr) display(dexpr) display(ddexpr) dfunc = sympy.lambdify(allvars, dexpr, 'numpy') ddfunc = sympy.lambdify(allvars, ddexpr, 'numpy') return [func, dfunc, ddfunc]