From 63db500772d6b72b5cf85df0e725362df3443776 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Fri, 23 Dec 2022 19:12:02 -0500 Subject: [PATCH 01/26] update `def plot` --- peak_integration.py | 1718 +++++++++++++++++++++++++++++++++---------- 1 file changed, 1340 insertions(+), 378 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index de42892..b08b4ff 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -7,6 +7,7 @@ from scipy.optimize import minimize from scipy.linalg import svd from numpy.linalg import eig from scipy.special import loggamma, erf +from scipy.ndimage import laplace import matplotlib # non-interactive backend that can only write to files @@ -20,8 +21,11 @@ from mantid.kernel import V3D from mantid import config config['Q.convention'] = "Crystallography" +# from ellipsoid import EllipsoidTool + +np.set_printoptions(precision=2, linewidth=200) _debug_dir = 'debug' _debug = True _profile = False @@ -157,6 +161,119 @@ def marginalize_2d(arr, bin_lengths=None, mask=None, normalize=False, recover_sh +# def ellipsoid_fit(X): +# # https://github.com/aleksandrbazhin/ellipsoid_fit_python +# x = X[:, 0] +# y = X[:, 1] +# z = X[:, 2] +# D = np.array([x * x + y * y - 2 * z * z, +# x * x + z * z - 2 * y * y, +# 2 * x * y, +# 2 * x * z, +# 2 * y * z, +# 2 * x, +# 2 * y, +# 2 * z, +# 1 - 0 * x]) +# d2 = np.array(x * x + y * y + z * z).T # rhs for LLSQ +# u = np.linalg.solve(D.dot(D.T), D.dot(d2)) +# a = np.array([u[0] + 1 * u[1] - 1]) +# b = np.array([u[0] - 2 * u[1] - 1]) +# c = np.array([u[1] - 2 * u[0] - 1]) +# v = np.concatenate([a, b, c, u[2:]], axis=0).flatten() +# A = np.array([[v[0], v[3], v[4], v[6]], +# [v[3], v[1], v[5], v[7]], +# [v[4], v[5], v[2], v[8]], +# [v[6], v[7], v[8], v[9]]]) + +# center = np.linalg.solve(- A[:3, :3], v[6:9]) + +# translation_matrix = np.eye(4) +# translation_matrix[3, :3] = center.T + +# R = translation_matrix.dot(A).dot(translation_matrix.T) + +# evals, evecs = np.linalg.eig(R[:3, :3] / -R[3, 3]) +# evecs = evecs.T + +# radii = np.sqrt(1. / np.abs(evals)) +# radii *= np.sign(evals) + +# return center, evecs, radii, v + +def fitMinVolEllipsoid(points, tolerance=0.01, maxit=100): + """ Find the minimum volume ellipsoid which holds all the points + + Based on work by Nima Moshtagh + http://www.mathworks.com/matlabcentral/fileexchange/9542 and also by looking at: + http://cctbx.sourceforge.net/current/python/scitbx.math.minimum_covering_ellipsoid.html + Which is based on the first reference anyway! + + Here, P is a numpy array of N dimensional points like this: + P = [[x,y,z,...], <-- one point per line + [x,y,z,...], + [x,y,z,...]] + + Returns: + (center, radii, rotation) + + Modified from https://github.com/minillinim/ellipsoid + """ + (n, d) = np.shape(points) + d = float(d) + + # Q will be our working array + Q = np.vstack([np.copy(points.T), np.ones(n)]) + QT = Q.T + + # initializations + err = 1.0 + tolerance + u = np.ones(n) / n + + # Khachiyan Algorithm + it = 0 + while err > tolerance and it<maxit: + it+=1 + V = Q @ (u[:,np.newaxis]*QT) + M = np.diag(QT @ np.linalg.solve(V,Q)) + j = np.argmax(M) + maximum = M[j] + step_size = (maximum - d - 1.0) / ((d + 1.0) * (maximum - 1.0)) + new_u = (1.0 - step_size) * u + new_u[j] += step_size + err = np.linalg.norm(new_u - u) + u = new_u + + # center of the ellipse + center = points.T @ u + + # points in centered coordinate system + xcnt = points - center.reshape(1,-1) + + # # the A matrix for the ellipse + # A = np.linalg.inv( points.T @ (u[:,np.newaxis]*points) - np.array([[a * b for b in center] for a in center]) ) / d + # # A = np.linalg.inv( + # # np.dot(points.T, np.dot(np.diag(u), points)) - + # # np.array([[a * b for b in center] for a in center]) + # # ) / d + + # # Get the values we'd like to return + # U, s, rotation = np.linalg.svd(A) + # radii = 1.0 / np.sqrt(s) + + + # A matrix for the ellipse + A = points.T @ (u[:,np.newaxis]*points) - np.array([[a * b for b in center] for a in center]) + # print(A) + # print(center.shape, points.shape) + # print((points-center.reshape(1,-1)).T @ (u[:,np.newaxis]*(points-center.reshape(1,-1)))) + # exit() + U, s, rotation = np.linalg.svd(A) + radii = np.sqrt(d*s) + + return (center, radii, rotation) + + ############################################################################### # rebinning @@ -403,10 +520,10 @@ def squared_mahalanobis_distance(mu, sqrtP, x): sqrtP = sqrtP.reshape((1,ndims,ndims)) xmu = x.reshape(-1,ndims,1) - mu.reshape((1,ndims,1)) Px = (sqrtP @ xmu).squeeze(2) + return (Px*Px).sum(axis=1) else: Px = sqrtP.ravel() * (x.ravel()-mu[0]) - - return (Px*Px).sum(axis=1) + return Px*Px def mahalanobis_distance(mu, sqrtP, x): @@ -427,14 +544,86 @@ def mahalanobis_distance(mu, sqrtP, x): + +class Polynomial(object): + def __init__(self, ndims, order=0): + self.order = order + + assert order==0, "Cant handle order>0 atm" + + # number of parameters + from scipy.special import comb + self.nparams = comb(ndims+order,order,exact=True) + + + def __call__(self, params, x): + start = time.time() + self.func_params = np.asarray(params).copy().ravel() + if self.func_params.size!=self.nparams: + raise ValueError(f"`params` array is of wrong size, must have `len(params) = binom(n+d,d) = {self.nparams}`, got {len(params)}") + + self.val = np.array([params[0]**2]) + + if _profile: + self.func_time = time.time() - start + print(f'bkgr func: {self.func_time} sec') + return self.val + + + def gradient(self, params, x, dloss_dfit=None, *args, **kwargs): + start = time.time() + self.grad_params = np.asarray(params).copy().ravel() + if self.grad_params.size!=self.nparams: + raise ValueError(f"`params` array is of wrong size, must have `len(params) = binom(n+d,d) = {self.nparams}`, got {len(params)}") + + # function always needs to evaluated before computing gradient + if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): + self.__call__(params, x) + + self.grad = np.array([[2*params[0]]]) + + grad = self.grad + if dloss_dfit is not None: + grad = np.array([2*params[0]]) * dloss_dfit.sum() + + if _profile: + self.grad_time = time.time()-start + print(f'bkgr grad: {self.grad_time} sec, {self.grad_time/self.func_time}') + return grad + + + def hessian(self, params, x, dloss_dfit, d2loss_dfit2, *args, **kwargs): + start = time.time() + self.hess_params = np.asarray(params).copy().ravel() + if self.hess_params.size!=self.nparams: + raise ValueError(f"`params` array is of wrong size, must have `len(params) = binom(n+d,d) = {self.nparams}`, got {len(params)}") + + # function and gradient always need to evaluated before computing hessian + if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): + self.__call__(params, x) + if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): + self.gradient(params, x) + + dloss_dfit = dloss_dfit.reshape((-1,1,1)) + d2loss_dfit2 = d2loss_dfit2.reshape((-1,1)) + + hess = np.array([[2]]) * dloss_dfit.sum() + np.array([[4*params[0]**2]]) * d2loss_dfit2.sum() + + if _profile: + hess_time = time.time()-start + print(f'bkgr hess: {hess_time} sec, {hess_time/self.func_time}') + return hess + + + class Gaussian(object): - def __init__(self, ndims, covariance_parameterization='givens'): - self.covariance_parameterization = covariance_parameterization + def __init__(self, ndims, parameterization='givens'): + self.parameterization = parameterization # number of parameters self.ncnt = ndims self.nskew = 0#ndims - if covariance_parameterization=='full': + if parameterization=='full': self.ncov = ndims**2 else: self.ncov = (ndims*(ndims+1))//2 @@ -446,187 +635,334 @@ class Gaussian(object): self.nparams = 1 + self.ncnt + self.ncov + self.nskew # number of angles - if covariance_parameterization=='givens': + if parameterization=='givens': self.nangles = (ndims*(ndims-1))//2 else: self.nangles = None - def __call__(self, params, x): - if len(params)!=self.nparams: - raise ValueError(f"`params` array is of wrong size, must have `len(params) = 1 + ncnt + ncov + nskew = {self.nparams}`, got {len(params)}") + def rotation_matrix(self, params): + ''' Compute rotation matrix from parameters + https://en.wikipedia.org/wiki/Givens_rotation#Table_of_composed_rotations + ''' + # extract angles from the parameters + sqrtP = params[1+self.ncnt:1+self.ncnt+self.ncov] + angles = sqrtP[:self.nangles] - start = time.time() + if self.ndims>3: + raise NotImplementedError(f"rotation matrix can be computed only for up to 3 dimensions at the moment, got `ndims={self.ndims}`") - # convert to numpy array - params = np.array(params).ravel() + # cosines and sines of the angles + c = np.cos(angles) + s = np.sin(angles) - # parameters of the model - intst = params[0] - cnt = params[1:1+self.ncnt] + # cache cosines and sines for derivative evaluations + self.c, self.s = c, s + + # Rotation matrix of 0-1-2 rotation + if self.ndims==1: + self.R = np.array([[1]]) + elif self.ndims==2: + self.R = np.array([c,-s],[s,c]) + elif self.ndims==3: + self.Rx = np.array([[ 1, 0, 0],[ 0, c[0],-s[0]],[ 0, s[0], c[0]]]) + self.Ry = np.array([[c[1], 0,-s[1]],[ 0, 1, 0],[s[1], 0, c[1]]]) + self.Rz = np.array([[c[2],-s[2], 0],[s[2], c[2], 0],[ 0, 0, 1]]) + + self.R = self.Rz @ self.Ry @ self.Rx + + return self.R + + + def rotation_matrix_gradient(self, params): + ''' + Compute gradient of the rotation matrix from parameters. + It is assumed that `self.R` has been computed before for the same parameters + ''' + + c, s = self.c, self.s + + if self.ndims==1: + self.dR = np.array([[0]]) + elif self.ndims==2: + self.dR = np.array([-s,-c],[c,-s]) + elif self.ndims==3: + self.dRx = np.array([[ 0, 0, 0],[ 0,-s[0],-c[0]],[ 0, c[0],-s[0]]]) + self.dRy = np.array([[-s[1], 0,-c[1]],[ 0, 0, 0],[c[1], 0,-s[1]]]) + self.dRz = np.array([[-s[2],-c[2], 0],[c[2],-s[2], 0],[ 0, 0, 0]]) + + self.dR = np.stack(( + self.Rz @ self.Ry @ self.dRx, + self.Rz @ self.dRy @ self.Rx, + self.dRz @ self.Ry @ self.Rx), + axis=0) + + return self.dR + + + def rotation_matrix_hessian(self, params): + ''' + Compute Hessian of the rotation matrix from parameters. + It is assumed that `self.R` and `self.dR` have been computed before for the same parameters + ''' + + c, s = self.c, self.s + + if self.ndims==1: + self.d2R = np.array([[0]]) + elif self.ndims==2: + self.d2R = -self.R + elif self.ndims==3: + dRxx = np.array([[ 0, 0, 0],[ 0,-c[0], s[0]],[ 0,-s[0],-c[0]]]) + dRyy = np.array([[-c[1], 0, s[1]],[ 0, 0, 0],[-s[1], 0,-c[1]]]) + dRzz = np.array([[-c[2], s[2], 0],[-s[2],-c[2], 0],[ 0, 0, 0]]) + + self.d2R = np.zeros((3,3,3,3)) + + self.d2R[0,0,...] = self.Rz @ self.Ry @ dRxx + self.d2R[1,1,...] = self.Rz @ dRyy @ self.Rx + self.d2R[2,2,...] = dRzz @ self.Ry @ self.Rx + + self.d2R[0,1,...] = self.Rz @ self.dRy @ self.dRx + self.d2R[0,2,...] = self.dRz @ self.Ry @ self.dRx + self.d2R[1,2,...] = self.dRz @ self.dRy @ self.Rx + + self.d2R[1,0,...] = self.d2R[0,1,...] + self.d2R[2,0,...] = self.d2R[0,2,...] + self.d2R[2,1,...] = self.d2R[1,2,...] + + return self.d2R + + + def Cov(self, params): + _, _, sqrtP = self.get_parameters(params) + return np.linalg.inv(sqrtP.T@sqrtP) + + + def get_parameters(self, params): + ''' Compute square root of the precision matrix and cache quantities for later reuse in derivative computations ''' + + # extract parameters of the precision matrix + self.sqrtintst = params[0] + self.intst = params[0]**2 + self.cnt = params[1:1+self.ncnt] sqrtP = params[1+self.ncnt:1+self.ncnt+self.ncov] # skew = params[1+ncnt+ncov:1+ncnt+ncov+nskew].reshape((1,-1)) - start = time.time() - # square root of the precision matrix - if self.covariance_parameterization=='full': - sqrtP_i = sqrtP.reshape((self.ndims,self.ndims)) - elif self.covariance_parameterization=='cholesky': - sqrtP_i = np.zeros((self.ndims,self.ndims)) + if self.parameterization=='full': + self.sqrtP = sqrtP.reshape((self.ndims,self.ndims)) + elif self.parameterization=='cholesky': + self.sqrtP = np.zeros((self.ndims,self.ndims)) triu_ind = np.triu_indices(self.ndims) diag_ind = np.diag_indices(self.ndims) # fill upper triangular part - sqrtP_i[triu_ind] = sqrtP + self.sqrtP[triu_ind] = sqrtP # positive diagonal makes Cholesky decomposition unique - sqrtP_i[diag_ind] = np.exp(sqrtP_i[diag_ind]) - elif self.covariance_parameterization=='givens': - # inverse rotation matrix - self.R,self.dR,self.d2R = rotation_matrix(sqrtP[:self.nangles],True,True) + self.sqrtP[diag_ind] *= self.sqrtP[diag_ind] #np.exp(sqrtP_i[diag_ind]) + elif self.parameterization=='givens': # square roots of the eigenvalues of the precision matrix - self.sqrtD = sqrtP[self.nangles:] #.reshape((ndims,1)) + self.sqrtD = sqrtP[self.nangles:] # square root of the precision matrix, i.e., diag(sqrt_eig) @ R - self.sqrtP = self.sqrtD[:,np.newaxis] * self.R + self.sqrtP = self.sqrtD[:,np.newaxis] * self.rotation_matrix(params) + return self.intst, self.cnt, self.sqrtP + + + def __call__(self, params, x): + start = time.time() + self.func_params = np.asarray(params).copy().ravel() + if self.func_params.size!=self.nparams: + raise ValueError(f"`params` array is of wrong size, must have `len(params) = 1 + ncnt + ncov + nskew = {self.nparams}`, got {len(params)}") + + # get parameters + intst, cnt, sqrtP = self.get_parameters(self.func_params) # base Gaussian - self.g = intst**2 * np.exp(-0.5*squared_mahalanobis_distance(cnt, self.sqrtP, x)) + self.val = intst * np.exp(-0.5*squared_mahalanobis_distance(cnt, sqrtP, x)) # modulated Gaussian - # g = g * (1+erf(skew@(x-cnt.reshape((ndims,1))))/np.sqrt(2)).ravel() + # self.val = self.val * (1+erf(skew@(x-cnt.reshape((ndims,1))))/np.sqrt(2)).ravel() if _profile: - self.func_time = time.time()-start - print(f'func: {self.func_time} sec') - - return self.g + self.func_time = time.time() - start + print(f'peak func: {self.func_time} sec') + return self.val def gradient(self, params, x, dloss_dfit=None, *args, **kwargs): - if len(params)!=self.nparams: + start = time.time() + self.grad_params = np.asarray(params).copy().ravel() + if self.grad_params.size!=self.nparams: raise ValueError(f"`params` array is of wrong size, must have `len(params) = 1 + ncnt + ncov + nskew = {self.nparams}`, got {len(params)}") - start = time.time() + # function always needs to evaluated before computing gradient + if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): + self.__call__(params, x) - # convert to numpy array - params = np.array(params).ravel() - # parameters of the model - intst = params[0] - cnt = params[1:1+self.ncnt] - sqrtP = params[1+self.ncnt:1+self.ncnt+self.ncov] + # define useful quantities and cache quantities for reuse + self.P = self.sqrtP.T @ self.sqrtP # (ndims,ndims) + self.xcnt = x - self.cnt.reshape((1,self.ndims)) # (npoints,ndims) + self.sqrtPxcnt_ = -np.einsum('ip,kp->ki',self.sqrtP,self.xcnt) # (npoints,ndims) + self.Pxcnt = np.einsum('ip,kp->ki',self.P, self.xcnt) # (npoints,ndims) - # define useful quantities - self.P = self.sqrtP.T @ self.sqrtP # (ndims,ndims) - self.dsqrtP_dangle = self.sqrtD.reshape((1,-1,1)) * self.dR # (nangles,ndims,ndims) - - # cache quantities for reuse - self.xcnt = x - cnt.reshape((1,self.ndims)) # (npoints,ndims) - self.Rxcnt = np.einsum('ip,kp->ki',self.R,self.xcnt) # (npoints,ndims) - self.sqrtPxcnt_ = -np.einsum('ip,kp->ki',self.sqrtP,self.xcnt) # (npoints,ndims) - self.Pxcnt = np.einsum('ip,kp->ki',self.P,self.xcnt) # (npoints,ndims) - self.dsqrtPxcnt_dangle = np.einsum('ijp,kp->kij',self.dsqrtP_dangle,self.xcnt) # (npoints,nangles,ndims) - self.sqrtPxcnt_dsqrtPxcnt_dangle_ = np.einsum('kp,kip->ki',self.sqrtPxcnt_,self.dsqrtPxcnt_dangle) # (npoints,nangles) - - self.dg_dintst = self.g[:,np.newaxis] * (2/intst) # (npoints,1) - self.dg_dcnt = self.g[:,np.newaxis] * self.Pxcnt # (npoints,ndims) - self.dg_dangle = self.g[:,np.newaxis] * self.sqrtPxcnt_dsqrtPxcnt_dangle_ # (npoints,ndims) - self.dg_dsqrtD = self.g[:,np.newaxis] * (self.sqrtPxcnt_ * self.Rxcnt) # (npoints,ndims) + self.dg_dintst = self.val[:,np.newaxis] * (2/self.sqrtintst) # (npoints,1) + self.dg_dcnt = self.val[:,np.newaxis] * self.Pxcnt # (npoints,ndims) # dg_dskew = np.zeros_like(dg_dcnt) # (npoints,ndims) - self.dg = np.concatenate((self.dg_dintst,self.dg_dcnt,self.dg_dangle,self.dg_dsqrtD), axis=-1) + if self.parameterization=='full': + self.dg_dsqrtP = np.einsum('ki,kj->kij', np.einsum('k,ki->ki',self.val,self.sqrtPxcnt_), self.xcnt) + elif self.parameterization=='cholesky': + self.dg_dsqrtP = np.einsum('ki,kj->kij', np.einsum('k,ki->ki',self.val,self.sqrtPxcnt_), self.xcnt) + + # update diagonal + diag_ind = np.diag_indices(self.ndims) + self.dg_dsqrtP[:,diag_ind[0],diag_ind[1]] *= 2 * np.sqrt(self.sqrtP[np.newaxis,diag_ind[0],diag_ind[1]]) + + triu_ind = np.triu_indices(self.ndims) + self.dg_dsqrtP = self.dg_dsqrtP[:,triu_ind[0],triu_ind[1]] + elif self.parameterization=='givens': + dR = self.rotation_matrix_gradient(params) + + self.Rxcnt = np.einsum('ip,kp->ki',self.R, self.xcnt) # (npoints,ndims) + + self.dsqrtP_dangle = self.sqrtD.reshape((1,-1,1)) * dR # (nangles,ndims,ndims) + self.dsqrtPxcnt_dangle = np.einsum('ijp,kp->kij',self.dsqrtP_dangle,self.xcnt) # (npoints,nangles,ndims) + self.sqrtPxcnt_dsqrtPxcnt_dangle_ = np.einsum('kp,kip->ki',self.sqrtPxcnt_,self.dsqrtPxcnt_dangle) # (npoints,nangles) + self.dg_dangle = self.val[:,np.newaxis] * self.sqrtPxcnt_dsqrtPxcnt_dangle_ # (npoints,ndims) + self.dg_dsqrtD = self.val[:,np.newaxis] * (self.sqrtPxcnt_ * self.Rxcnt) # (npoints,ndims) + + self.dg_dsqrtP = np.concatenate((self.dg_dangle,self.dg_dsqrtD), axis=-1) + + self.grad = np.concatenate((self.dg_dintst,self.dg_dcnt,self.dg_dsqrtP.reshape((-1,self.ncov))), axis=-1) + + grad = self.grad if dloss_dfit is not None: - dg = np.einsum('k,ki->i',dloss_dfit,self.dg) + grad = np.einsum('k,ki->i',dloss_dfit,self.grad) if _profile: self.grad_time = time.time()-start - print(f'grad: {self.grad_time} sec, {self.grad_time/self.func_time}') + print(f'peak grad: {self.grad_time} sec, {self.grad_time/self.func_time}') + return grad - return dg - def hessian(self, params, x, dloss_dfit=None, d2loss_dfit2=None, *args, **kwargs): - if len(params)!=self.nparams: + def hessian(self, params, x, dloss_dfit, d2loss_dfit2, *args, **kwargs): + start = time.time() + self.hess_params = np.asarray(params).copy().ravel() + if self.hess_params.size!=self.nparams: raise ValueError(f"`params` array is of wrong size, must have `len(params) = 1 + ncnt + ncov + nskew = {self.nparams}`, got {len(params)}") - start = time.time() + # function and gradient always need to evaluated before computing hessian + if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): + self.__call__(params, x) + if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): + self.gradient(params, x) - # convert to numpy array - params = np.array(params).ravel() + # # convert to numpy array + # params = np.array(params).ravel() - # parameters of the model - intst = params[0] - cnt = params[1:1+self.ncnt] - sqrtP = params[1+self.ncnt:1+self.ncnt+self.ncov] + # # parameters of the model + # intst = params[0] + # cnt = params[1:1+self.ncnt] + # sqrtP = params[1+self.ncnt:1+self.ncnt+self.ncov] - dRxcnt_dangle = np.einsum('ijp,kp->kij',self.dR,self.xcnt) # (npoints,nangle,ndims) - d2sqrtP_dangle2 = np.einsum('m,ijmn->ijmn',self.sqrtD,self.d2R) # (nangle,nangle,ndims,ndims) - d2sqrtPxcnt_dangle2 = np.einsum('ijmn,kn->kijm',d2sqrtP_dangle2,self.xcnt) # (npoints,nangle,nangle,ndims) - # print(f'{time.time()-start} cache'); start1 = time.time() - d2g_dintst2 = self.dg_dintst[:,np.newaxis,:] / intst # (npoints,1,1) - d2g_dintst_dcnt = self.dg_dcnt[:,np.newaxis,:] * (2/intst) # (npoints,1,ndims) - d2g_dintst_dangle = self.dg_dangle[:,np.newaxis,:] * (2/intst) # (npoints,1,ndims) - d2g_dintst_dsqrtD = self.dg_dsqrtD[:,np.newaxis,:] * (2/intst) # (npoints,1,ndims) + d2g_dintst2 = self.dg_dintst[:,np.newaxis,:] / self.sqrtintst # (npoints,1,1) + d2g_dintst_dcnt = self.dg_dcnt[:,np.newaxis,:] * (2/self.sqrtintst) # (npoints,1,ndims) # print(f'{time.time()-start1} d2g_dintst'); start1 = time.time() d2g_dcnt2 = np.einsum('ki,kj->kij',self.dg_dcnt,self.Pxcnt) - d2g_dcnt2 -= np.einsum('k,ij->kij',self.g,self.P) - # print(f'{time.time()-start1} d2g_dcnt2'); start1 = time.time() - # - d2g_dcnt_dangle = np.einsum('ip,kjp->kij',self.sqrtP.T,self.dsqrtPxcnt_dangle) - d2g_dcnt_dangle -= np.einsum('kp,jpi->kij',self.sqrtPxcnt_,self.dsqrtP_dangle) - d2g_dcnt_dangle *= self.g.reshape((-1,1,1)) - d2g_dcnt_dangle += np.einsum('ki,kj->kij',self.dg_dcnt,np.einsum('kp,kjp->kj',self.sqrtPxcnt_,self.dsqrtPxcnt_dangle)) + d2g_dcnt2 -= np.einsum('k,ij->kij',self.val,self.P) # print(f'{time.time()-start1} d2g_dcnt_dangle'); start1 = time.time() - # - d2g_dcnt_dsqrtD = (np.einsum('ki,kj->kij',self.dg_dcnt,self.Rxcnt) - np.einsum('k,ji->kij',self.g,2*self.R)) * self.sqrtPxcnt_[:,np.newaxis,:] - # print(f'{time.time()-start1} d2g_dcnt_dsqrtD'); start1 = time.time() - - d2g_dangle2 = np.einsum('kp,kijp->kij',self.sqrtPxcnt_,d2sqrtPxcnt_dangle2) - d2g_dangle2 -= np.einsum('kip,kjp->kij',self.dsqrtPxcnt_dangle,self.dsqrtPxcnt_dangle) - d2g_dangle2 *= self.g.reshape((-1,1,1)) - d2g_dangle2 += np.einsum('ki,kj->kij',self.dg_dangle,self.sqrtPxcnt_dsqrtPxcnt_dangle_) - # print(f'{time.time()-start1} d2g_dangle2'); start1 = time.time() - # - d2g_dangle_dsqrtD = ( self.dg_dangle[...,np.newaxis]*self.Rxcnt[:,np.newaxis,:] + (2*self.g.reshape((-1,1,1)))*dRxcnt_dangle ) * self.sqrtPxcnt_[:,np.newaxis,:] - # print(f'{time.time()-start1} d2g_dangle_dsqrtD'); start1 = time.time() - axi,axj = np.diag_indices(self.ndims) - d2g_dsqrtD2 = self.dg_dsqrtD[...,np.newaxis] * (-self.sqrtD.reshape((1,1,-1))) - d2g_dsqrtD2[:,axi,axj] -= self.g[...,np.newaxis] - d2g_dsqrtD2 *= (self.Rxcnt**2)[:,np.newaxis,:] - # print(f'{time.time()-start1} d2g_dsqrtD2'); start1 = time.time() + if self.parameterization=='full': + d2g_dintst_dsqrtP = self.dg_dsqrtP.reshape((-1,1,self.ncov)) * (2/self.sqrtintst) # (npoints,ndims,ndims) + + axi,axm = np.diag_indices(self.ndims) + # d2g_dcnt_dsqrtP = np.einsum('ki,kj->kij', self.dg_dcnt, np.einsum('ki,kj->kij', self.sqrtPxcnt_, self.xcnt).reshape((-1,self.ncov)) ) + d2g_dcnt_dsqrtP = np.einsum('ki,knm->kinm', self.dg_dcnt, np.einsum('ki,kj->kij', self.sqrtPxcnt_, self.xcnt) ) + d2g_dcnt_dsqrtP += self.val.reshape((-1,1,1,1)) * np.einsum('ni,km->kinm', self.sqrtP, self.xcnt) + d2g_dcnt_dsqrtP[:,axi,:,axm] -= self.val.reshape((-1,1)) * self.sqrtPxcnt_ + d2g_dcnt_dsqrtP = d2g_dcnt_dsqrtP.reshape((-1,self.ncnt,self.ncov)) + + d2g_dsqrtP2 = np.einsum('kij,knm->kijnm', self.dg_dsqrtP, np.einsum('kn,km->knm', self.sqrtPxcnt_, self.xcnt) ) + d2g_dsqrtP2[:,axi,:,axm,:] -= self.val.reshape((-1,1,1)) * np.einsum('kj,km->kjm',self.xcnt,self.xcnt) + d2g_dsqrtP2 = d2g_dsqrtP2.reshape((-1,self.ncov,self.ncov)) + elif self.parameterization=='givens': + d2R = self.rotation_matrix_hessian(params) + + # useful quantities + dRxcnt_dangle = np.einsum('ijp,kp->kij',self.dR,self.xcnt) # (npoints,nangle,ndims) + d2sqrtP_dangle2 = np.einsum('m,ijmn->ijmn',self.sqrtD,d2R) # (nangle,nangle,ndims,ndims) + d2sqrtPxcnt_dangle2 = np.einsum('ijmn,kn->kijm',d2sqrtP_dangle2,self.xcnt) # (npoints,nangle,nangle,ndims) + # print(f'{time.time()-start} cache'); start1 = time.time() + + d2g_dintst_dangle = self.dg_dangle[:,np.newaxis,:] * (2/self.sqrtintst) # (npoints,1,ndims) + d2g_dintst_dsqrtD = self.dg_dsqrtD[:,np.newaxis,:] * (2/self.sqrtintst) # (npoints,1,ndims) + + d2g_dcnt_dangle = np.einsum('ip,kjp->kij',self.sqrtP.T,self.dsqrtPxcnt_dangle) + d2g_dcnt_dangle -= np.einsum('kp,jpi->kij',self.sqrtPxcnt_,self.dsqrtP_dangle) + d2g_dcnt_dangle *= self.val.reshape((-1,1,1)) + d2g_dcnt_dangle += np.einsum('ki,kj->kij',self.dg_dcnt,np.einsum('kp,kjp->kj',self.sqrtPxcnt_,self.dsqrtPxcnt_dangle)) + # + d2g_dcnt_dsqrtD = (np.einsum('ki,kj->kij',self.dg_dcnt,self.Rxcnt) - np.einsum('k,ji->kij',self.val,2*self.R)) * self.sqrtPxcnt_[:,np.newaxis,:] + + d2g_dangle2 = np.einsum('kp,kijp->kij',self.sqrtPxcnt_,d2sqrtPxcnt_dangle2) + d2g_dangle2 -= np.einsum('kip,kjp->kij',self.dsqrtPxcnt_dangle,self.dsqrtPxcnt_dangle) + d2g_dangle2 *= self.val.reshape((-1,1,1)) + d2g_dangle2 += np.einsum('ki,kj->kij',self.dg_dangle,self.sqrtPxcnt_dsqrtPxcnt_dangle_) + # print(f'{time.time()-start1} d2g_dangle2'); start1 = time.time() + # + d2g_dangle_dsqrtD = ( self.dg_dangle[...,np.newaxis]*self.Rxcnt[:,np.newaxis,:] + (2*self.val.reshape((-1,1,1)))*dRxcnt_dangle ) * self.sqrtPxcnt_[:,np.newaxis,:] + # print(f'{time.time()-start1} d2g_dangle_dsqrtD'); start1 = time.time() + axi,axj = np.diag_indices(self.ndims) + d2g_dsqrtD2 = self.dg_dsqrtD[...,np.newaxis] * (-self.sqrtD.reshape((1,1,-1))) + d2g_dsqrtD2[:,axi,axj] -= self.val[...,np.newaxis] + d2g_dsqrtD2 *= (self.Rxcnt**2)[:,np.newaxis,:] + # print(f'{time.time()-start1} d2g_dsqrtD2'); start1 = time.time() dloss_dfit = dloss_dfit.reshape((-1,1,1)) d2loss_dfit2 = d2loss_dfit2.reshape((-1,1)) - d2g_dintst2 = (dloss_dfit*d2g_dintst2).sum(axis=0) - d2g_dintst_dcnt = (dloss_dfit*d2g_dintst_dcnt).sum(axis=0) - d2g_dintst_dangle = (dloss_dfit*d2g_dintst_dangle).sum(axis=0) - d2g_dintst_dsqrtD = (dloss_dfit*d2g_dintst_dsqrtD).sum(axis=0) - + d2g_dintst2 = (dloss_dfit*d2g_dintst2).sum(axis=0) + d2g_dintst_dcnt = (dloss_dfit*d2g_dintst_dcnt).sum(axis=0) d2g_dcnt2 = (dloss_dfit*d2g_dcnt2).sum(axis=0) - d2g_dcnt_dangle = (dloss_dfit*d2g_dcnt_dangle).sum(axis=0) - d2g_dcnt_dsqrtD = (dloss_dfit*d2g_dcnt_dsqrtD).sum(axis=0) + if self.parameterization=='full': + d2g_dintst_dsqrtP = (dloss_dfit*d2g_dintst_dsqrtP).sum(axis=0) + d2g_dcnt_dsqrtP = (dloss_dfit*d2g_dcnt_dsqrtP).sum(axis=0) + d2g_dsqrtP2 = (dloss_dfit*d2g_dsqrtP2).sum(axis=0) + + d2g = np.block([ + [d2g_dintst2, d2g_dintst_dcnt, d2g_dintst_dsqrtP], + [d2g_dintst_dcnt.T, d2g_dcnt2, d2g_dcnt_dsqrtP ], + [d2g_dintst_dsqrtP.T, d2g_dcnt_dsqrtP.T, d2g_dsqrtP2 ], + ]) + elif self.parameterization=='givens': + d2g_dintst_dangle = (dloss_dfit*d2g_dintst_dangle).sum(axis=0) + d2g_dintst_dsqrtD = (dloss_dfit*d2g_dintst_dsqrtD).sum(axis=0) + + d2g_dcnt_dangle = (dloss_dfit*d2g_dcnt_dangle).sum(axis=0) + d2g_dcnt_dsqrtD = (dloss_dfit*d2g_dcnt_dsqrtD).sum(axis=0) - d2g_dangle2 = (dloss_dfit*d2g_dangle2).sum(axis=0) - d2g_dangle_dsqrtD = (dloss_dfit*d2g_dangle_dsqrtD).sum(axis=0) + d2g_dangle2 = (dloss_dfit*d2g_dangle2).sum(axis=0) + d2g_dangle_dsqrtD = (dloss_dfit*d2g_dangle_dsqrtD).sum(axis=0) - d2g_dsqrtD2 = (dloss_dfit*d2g_dsqrtD2).sum(axis=0) + d2g_dsqrtD2 = (dloss_dfit*d2g_dsqrtD2).sum(axis=0) - d2g = np.block([ - [d2g_dintst2, d2g_dintst_dcnt, d2g_dintst_dangle, d2g_dintst_dsqrtD], - [d2g_dintst_dcnt.T, d2g_dcnt2, d2g_dcnt_dangle, d2g_dcnt_dsqrtD ], - [d2g_dintst_dangle.T, d2g_dcnt_dangle.T, d2g_dangle2, d2g_dangle_dsqrtD], - [d2g_dintst_dsqrtD.T, d2g_dcnt_dsqrtD.T, d2g_dangle_dsqrtD.T, d2g_dsqrtD2 ], - ]) - d2g += ((d2loss_dfit2*self.dg)[...,np.newaxis]*self.dg[:,np.newaxis,:]).sum(axis=0) + d2g = np.block([ + [d2g_dintst2, d2g_dintst_dcnt, d2g_dintst_dangle, d2g_dintst_dsqrtD], + [d2g_dintst_dcnt.T, d2g_dcnt2, d2g_dcnt_dangle, d2g_dcnt_dsqrtD ], + [d2g_dintst_dangle.T, d2g_dcnt_dangle.T, d2g_dangle2, d2g_dangle_dsqrtD], + [d2g_dintst_dsqrtD.T, d2g_dcnt_dsqrtD.T, d2g_dangle_dsqrtD.T, d2g_dsqrtD2 ], + ]) + d2g += ((d2loss_dfit2*self.grad)[:,:,np.newaxis]*self.grad[:,np.newaxis,:]).sum(axis=0) # print(f'{time.time()-start1} block'); start1 = time.time() if _profile: hess_time = time.time()-start - print(f'hess: {hess_time} sec, {hess_time/self.func_time}') + print(f'peak hess: {hess_time} sec, {hess_time/self.func_time}') return d2g @@ -923,15 +1259,20 @@ def numerical_hessian(x, fun, *args, **kwargs): ############################################################################### -class Histogram(object): - def __init__(self, hist_ws, detector_mask=None): +class PeakHistogram(object): + def __init__(self, hist_ws, detector_mask=None, parameterization='givens'): self.hist_ws = hist_ws + # covariance parameterization + self.parameterization = parameterization + # number of dimensions in the histogram self.ndims = hist_ws.getNumDims() # histogram array self.data = hist_ws.getNumEventsArray().copy() + # self.data -= self.data.mean() + 0.5 * (self.data.max() - self.data.mean()) + # self.data[self.data<0] = 0 # from scipy.ndimage import gaussian_filter # self.data = gaussian_filter(self.data, sigma=2) @@ -963,6 +1304,12 @@ class Histogram(object): # self.detector_mask = detector_mask + # peak model + self.peak_fun = Gaussian(ndims=self.ndims, parameterization=parameterization) + + # background model + self.bkgr_fun = Polynomial(ndims=self.ndims, order=0) + def get_grid_data(self, bins=None, rebin_mode='density', return_edges=False): '''Extract coordinates of the bins and bin counts from the histogram workspace @@ -997,115 +1344,459 @@ class Histogram(object): return data, points - def fit_two_level(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='L-BFGS-B', covariance_parameterization='givens', plot_intermediate=False): - # shape of the largest subhistogram with shape as a power of 2 - shape2 = [2**int(np.log2(self.shape[d])) for d in range(self.ndims)] - - # smallest power of 2 among all dimensions - minpow2 = min([int(np.log2(self.shape[d])) for d in range(self.ndims)]) - - solver1 = solver0 + def initialize1(self, bins=None, loss='mle', covariance_parameterization='givens'): + ########################################################################### + # rebin data - params = params_lbnd = params_ubnd = None - for p in [min_level]: - start = time.time() + if isinstance(bins,str): + if bins=='knuth': + bins = knuth_bins(self.data, min_bins=4, spread=1) + # bins = knuth_bins(data, min_bins=4, max_bins=4, spread=0) + elif bins=='adaptive_knuth': + # rebin data using number of bins given by `knuth` algorithm but such that bins have comparable probability masses + bins = knuth_bins(self.data, min_bins=4, spread=1) + # 1d marginals + marginal_data = marginalize_1d(self.data, normalize=False) - # left, middle (with power of 2 shape) and right bins - binsl = [(self.shape[d]-shape2[d])//2 for d in range(self.ndims)] - binsm = [split_bins([shape2[d]],2**p,recursive=False) for d in range(self.ndims)] - binsr = [(self.shape[d]-shape2[d]) - binsl[d] for d in range(self.ndims)] + # quantiles, note len(b)+2 to make odd number of bins + quant = [ np.linspace(0,1,min(len(b)+2,self.shape[i])) for i,b in enumerate(bins) ] + edges = [ np.quantile( np.repeat(np.arange(1,md.size+1), md.astype(int)), q[1:], method='inverted_cdf' ) for md,q in zip(marginal_data,quant) ] + bins = [ np.diff(e,prepend=0).astype(int) for e in edges ] - # # combine two middle bins - # binsm = [b[:len(b)//2-1]+[b[len(b)//2-1]+b[len(b)//2+1]]+b[len(b)//2+1:] for b in binsm] + if _debug: + plt.figure(constrained_layout=True, figsize=(10,4)) + for i in range(self.ndims): + plt.subplot(1,self.ndims,i+1) + plt.hlines(np.linspace(0,marginal_data[i].sum(),len(bins[i])+1), 0, marginal_data[i].size) + plt.vlines(edges[i],0,marginal_data[i].sum()) + plt.plot(marginal_data[i].cumsum(), '.', c='red') + plt.gca().set_box_aspect(1) + plt.savefig(_debug_dir+'/adaptive_knuth_quantiles.png') + elif isinstance(bins,int): + nbins = bins + bins = [split_bins([s],nbins,recursive=False) for s in self.shape] + elif bins is None: + bins = [[1]*s for s in self.shape] - # bins at the current level - bins = [ ([bl] if bl>0 else [])+bm+([br] if br>0 else []) for bl,bm,br in zip(binsl,binsm,binsr)] + # rebinned data + fit_data, fit_points = self.get_grid_data(bins=bins, rebin_mode='density') + fit_points = fit_points.reshape((-1,self.ndims)) - # fit histogram at the current level - # params, sucess - output = self.fit(bins=bins, return_bins=return_bins, loss=loss, solver=solver1, covariance_parameterization=covariance_parameterization, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) - params, sucess = output[0], output[1] + data_min, data_max, data_mean = fit_data.min(), fit_data.max(), fit_data.mean() + fit_data -= data_mean + 0.5 * (data_max - data_mean) + fit_data[fit_data<0] = 0 + fit_data = fit_data.ravel() - # skip level if fit not successful - if not sucess and p<minpow2: - params = params_lbnd = params_ubnd = None - continue - solver1 = solver + # detector_mask = None + # if self.detector_mask is None: + # detector_mask = fit_data==fit_data + # if self.detector_mask is not None: + # detector_mask = rebin_histogram(self.detector_mask.astype(int), bins)>0 + # fit_data = fit_data.ravel()[detector_mask.ravel()] + # fit_points = fit_points[detector_mask.ravel(),:] - nangles = self.ndims*(self.ndims-1)//2 - sqrtbkgr = params[0] - sqrtintst = params[1] - cnt = params[2:2+self.ndims] - angles = params[2+self.ndims:2+self.ndims+nangles] - invrads = params[2+self.ndims+nangles:2+2*self.ndims+nangles] - # skewness = params[2+2*ndims+nangles:2+3*ndims+nangles] + ########################################################################### + # initialization and bounds on parameters - ####################################################################### - # refine search bounds + # self.initialize(bins) - # bounds for the center - dcnt = [ 10*res*2**(minpow2-p) for res in self.resolution] # search radius is 10 voxels at the current level - cnt_lbnd = [c-dc for c,dc in zip(cnt,dcnt)] - cnt_ubnd = [c+dc for c,dc in zip(cnt,dcnt)] + ################################### + # # initialization and bounds for the background intensity + # bkgr_init = [ np.sqrt(0.9*data_mean)] #+ [0]*self.ndims + # bkgr_lbnd = [-np.sqrt(1.1*data_mean)] #+ [0]*self.ndims + # bkgr_ubnd = [ np.sqrt(1.1*data_mean)] #+ [0]*self.ndims - # bounds for the precision matrix angles - if minpow2>min_level: - phi0 = np.pi/1 - phi1 = np.pi#/8 - dphi = phi0 + (p-min_level)/(minpow2-min_level) * (phi1-phi0) - else: - dphi = np.pi + ################################### + # initialization and bounds for the max peak intensity + intst_init = [ np.sqrt(data_max-data_mean)] + intst_lbnd = [-np.sqrt(data_max)] + intst_ubnd = [ np.sqrt(data_max)] - # sc = 2 + (p-min_level)/(minpow2-min_level) * (1-2) - # sc = 2 - # print(sc) + ################################### + # cnt_1d, std_1d = initial_parameters(hist_ws, bins) + # params_init1 = initial_parameters(hist_ws, bins, detector_mask) - ###################################### - # bounds for the precision matrix + # initialization and bounds for the peak center + # dcnt = [ rad/3 for rad in self.radiuses] + # cnt_init = cnt_1d + cnt_init = [(lim[0]+lim[1])/2 for lim in self.limits] + cnt_lbnd = [c-rad/3 for c,rad in zip(cnt_init,self.radiuses)] + cnt_ubnd = [c+rad/3 for c,rad in zip(cnt_init,self.radiuses)] + # initialization and bounds for the precision matrix + if covariance_parameterization=='givens': + num_angles = (self.ndims*(self.ndims-1))//2 # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix peak_std = 4 + ini_rads = [ 1/4*rad/peak_std for rad in self.radiuses] # initial 'peak_std' radius is 1/2 of the box radius max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size - # prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] - # prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] - # prec_lbnd = [max(-np.pi,phi-dphi) for phi in angles] + [ r/2.0 for r in invrads] - prec_lbnd = [max(-np.pi,phi-dphi) for phi in angles] + [ 1/r for r in max_rads] #[ max((self.limits[d][1]-self.limits[d][0])/4/(4/3),invrads[d]/2.0) for d in range(self.ndims)] - prec_ubnd = [min( np.pi,phi+dphi) for phi in angles] + [ 1/r for r in min_rads] #[ 100*r for r in invrads] - - # bounds for all parameters - # params_lbnd = [np.abs(sqrtbkgr)/2, np.abs(sqrtintst)/2] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] - # params_ubnd = [2*np.abs(sqrtbkgr), 2*np.abs(sqrtintst)] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] - params_lbnd = [-np.inf, -np.inf] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] - params_ubnd = [ np.inf, np.inf] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] - - # params[0] = np.abs(sqrtbkgr) - # params[1] = np.abs(sqrtintst) - - # params_lbnd = params_lbnd + list(params[len(params_lbnd):]) - # params_ubnd = params_ubnd + list(params[len(params_ubnd):]) - # params_lbnd = params_ubnd = None - - ####################################################################### - - print(f"Fitted histogram with {2**p:3d} bins: {time.time()-start:.3f} seconds") - - # if plot_intermediate: - # plot_fit(hist_ws, params, bins, prefix=f"{p}", peak_id=1074, peak_hkl=[2.0,-2.0,-9.0], peak_std=4, bkgr_std=7, detector_mask=None, log=True) - - start = time.time() - output = self.fit(return_bins=return_bins, loss=loss, solver=solver, covariance_parameterization=covariance_parameterization, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) - print(f"Fitted histogram with origianl bins: {time.time()-start:.3f} seconds") + # `num_angles` angles and `ndims` square roots of the eigenvalues of the precision matrix + # prec_init = [ 0]*num_angles + std_1d + prec_init = [ 0]*num_angles + [ 1/r for r in ini_rads] + prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] + prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] + elif covariance_parameterization=='cholesky': + num_chol = (self.ndims*(self.ndims+1))//2 + # upper triangular part of the Cholesky factor of the precision matrix + prec_init = list(np.eye(self.ndims)[np.triu_indices(self.ndims)]) + prec_lbnd = [-1000]*num_chol + prec_ubnd = [ 1000]*num_chol + elif covariance_parameterization=='full': + # arbitrary square root of the precision matrix + prec_init = list(np.eye(self.ndims).ravel()) + prec_lbnd = [-1000]*(self.ndims**2) + prec_ubnd = [ 1000]*(self.ndims**2) + + # # initialization and bounds for the skewness + # skew_init = [0]*self.ndims + # skew_lbnd = [0]*self.ndims + # skew_ubnd = [0]*self.ndims - return output + ################################### + # initialization and bounds for all parameters + # params_init = params_init1 + params_init = intst_init + cnt_init + prec_init #+ skew_init + params_lbnd = intst_lbnd + cnt_lbnd + prec_lbnd #+ skew_lbnd + params_ubnd = intst_ubnd + cnt_ubnd + prec_ubnd #+ skew_ubnd + # # number of background and peak parameters + # nbkgr = 1 #len(bkgr_init) + # npeak = len(params_init) - nbkgr - def fit_multilevel(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='L-BFGS-B', covariance_parameterization='givens', plot_intermediate=False): - # shape of the largest subhistogram with shape as a power of 2 - shape2 = [2**int(np.log2(self.shape[d])) for d in range(self.ndims)] + ########################################################################### - # smallest power of 2 among all dimensions + # residual to fit densities of the bins in the rebinned histogram + if loss=='pearson_chi': + def residual(params): + fit = params[0]**2 + fit = fit + gaussian_mixture(params[1:],points,npeaks=1,covariance_parameterization=covariance_parameterization).reshape(data.shape) + res = fit[nnz_mask] - nnz_data + return (res/np.sqrt(fit)).ravel() + result = least_squares(residual, #jac=jacobian_residual, + x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) + elif loss=='neumann_chi': + def residual(params): + fit = params[0]**2 + fit = fit + gaussian_mixture(params[1:],points,npeaks=1,covariance_parameterization=covariance_parameterization).reshape(data.shape) + res = fit[nnz_mask] - nnz_data + return (res/np.sqrt(nnz_data)).ravel() + # return (res/np.maximum(1,np.sqrt(nnz_data))).ravel() + # return (res/np.sqrt(data[nnz_mask].size)).ravel() + result = least_squares(residual, #jac=jacobian_residual, + x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) + elif loss=='mle': + gaussian_peak = Gaussian(ndims=3, covariance_parameterization=covariance_parameterization) + class MLELoss(object): + def __init__(self): + self.func_calls = 0 + self.grad_calls = 0 + self.hess_calls = 0 + + def __call__(self, params): + self.func_calls+=1 + self.func_params = np.asarray(params).copy() + + # fit + self.fit = 0.01 + gaussian_peak(params, fit_points) + + return (self.fit-fit_data*np.log(self.fit)).sum() + + def gradient(self, params, *args, **kwargs): + self.grad_calls += 1 + self.grad_params = np.asarray(params).copy() + if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): g = self.__call__(params) + + self.dloss_dfit = 1 - fit_data/self.fit + self.dloss_dpeak = gaussian_peak.gradient(params, fit_points, dloss_dfit=self.dloss_dfit) + + return self.dloss_dpeak + + def hessian(self, params, *args, **kwargs): + self.hess_calls += 1 + self.hess_params = np.asarray(params).copy() + if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): g = self.__call__(params) + if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): dg = self.gradient(params) + + d2loss_dfit2 = fit_data/self.fit**2 + d2loss = gaussian_peak.hessian(params,fit_points,dloss_dfit=self.dloss_dfit,d2loss_dfit2=d2loss_dfit2) + + return d2loss + peak_loss = MLELoss() + result = minimize(peak_loss, + jac = peak_loss.gradient, + hess = peak_loss.hessian, + x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), + # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} + # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} + method='Newton-CG', options={'maxiter':10, 'xtol':1.e-6, 'disp':True} + ) + + + print(params_init) + print(result.x) + center, evecs, radii, v = ellipsoid_fit(fit_points[fit_data>0,:]) + print(center) + print(evecs) + print(radii, 1/params_init[7],1/params_init[8],1/params_init[9]) + print(v) + exit() + return result.x + + + def initialize(self, points, data): + # basic statistics + data_min, data_max, data_mean = data.min(), data.max(), data.mean() + + + ################################### + # first initialization for the peak center + cnt_init = np.array([(lim[0]+lim[1])/2 for lim in self.limits]).reshape((1,-1)) + + + ################################### + # find threshold intensity that gives largets radius reduction of the enclosing sphere + nthres = 20 + thresh_rads = np.zeros((nthres,)) + thresh_vals = np.linspace(0, data_max, nthres-1, endpoint=False) + for i,thres in enumerate(thresh_vals): + thresh_data = data - thres + thresh_rads[i] = np.linalg.norm(points[thresh_data>0,...]-cnt_init,ord=2,axis=1).max() + thresh_rads[-1] = 0 + thresh_ind = np.argmax(np.abs(np.diff(thresh_rads)))+1 + thresh_val = thresh_vals[thresh_ind] + thresh_rad = thresh_rads[thresh_ind] + + # mean intensity outside threshold sphere + bkgr_mask = np.linalg.norm(points-cnt_init,ord=2,axis=1)>thresh_rad + bkgr_mean = data[bkgr_mask].mean() + + + ################################### + # initialization and bounds for the background + bkgr_init = [ np.sqrt(1.0*bkgr_mean)] #+ [0]*self.ndims + bkgr_lbnd = [-np.sqrt(1.1*bkgr_mean)] #+ [0]*self.ndims + bkgr_ubnd = [ np.sqrt(1.1*bkgr_mean)] #+ [0]*self.ndims + + + ################################### + # filter background from data + nobkgr_data = data - (thresh_val + 0.0 * (data_max - thresh_val)) + bkgr_mask = nobkgr_data<0 + peak_mask = ~bkgr_mask + nobkgr_data[bkgr_mask] = 0 + ellmask = laplace(peak_mask,mode='reflect')!=0 + + print(ellmask.sum(), peak_mask.sum()) + + # fit maximum enclosing ellipsoid + ellpoints = points[ellmask,...] + ellcnt, ellrad, ellrot = fitMinVolEllipsoid(ellpoints, tolerance=0.01, maxit=10) + + # if _debug: + # _, _, edges = self.get_grid_data(return_edges=True) + + # data_1d = marginalize_1d(data.reshape(self.shape), normalize=False) + # data_2d = marginalize_2d(data.reshape(self.shape), normalize=False) + # for i in range(self.ndims): + # plt.stairs(data_1d[i], edges=edges[i], fill=True, alpha=0.5) + # plt.gca().set_box_aspect(1) + # plt.savefig(f'{_debug_dir}/peak_1d_{i}.png', bbox_inches='tight') + # plt.clf() + # for i in range(self.ndims): + # yind, xind = [j for j in range(self.ndims) if j!=i] + # left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + # plt.imshow(data_2d[i]/(data_2d[i]!=0), interpolation='none', extent=(left,right,bottom,top), origin='lower') + # plt.savefig(f'{_debug_dir}/peak_2d_{i}.png', bbox_inches='tight') + # plt.clf() + + # nobkgr_data_1d = marginalize_1d(nobkgr_data.reshape(self.shape), normalize=False) + # nobkgr_data_2d = marginalize_2d(nobkgr_data.reshape(self.shape), normalize=False) + # for i in range(self.ndims): + # plt.stairs(nobkgr_data_1d[i], edges=edges[i], fill=True, alpha=0.5) + # plt.gca().set_box_aspect(1) + # plt.savefig(f'{_debug_dir}/no_bkgr_peak_1d_{i}.png', bbox_inches='tight') + # plt.clf() + # for i in range(self.ndims): + # yind, xind = [j for j in range(self.ndims) if j!=i] + # left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + # plt.imshow(nobkgr_data_2d[i]/(nobkgr_data_2d[i]!=0), interpolation='none', extent=(left,right,bottom,top), origin='lower') + # plt.savefig(f'{_debug_dir}/no_bkgr_peak_2d_{i}.png', bbox_inches='tight') + # plt.clf() + # # exit() + + + # print( ellrot.T@np.diag(ellrad**2)@ellrot@np.array([1,0,0]).reshape((-1,1)) ) + + # print(mahalanobis_distance(ellcnt,np.diag(1/ellrad)@ellrot, (ellrot.T@np.diag(1/ellrad**2)@ellrot@np.array([1,0,0]).reshape((-1,1))).reshape((1,-1)) ) ) + + # exit() + + ################################### + # initialization and bounds for the max peak intensity + intst_init = [ np.sqrt(data_max-bkgr_init[0]**2)] + intst_lbnd = [-np.sqrt(data_max)] + intst_ubnd = [ np.sqrt(data_max)] + + # refined initialization and bounds for the peak center + cnt_init = [(lim[0]+lim[1])/2 for lim in self.limits] + cnt_lbnd = [c-rad/3 for c,rad in zip(cnt_init,self.radiuses)] + cnt_ubnd = [c+rad/3 for c,rad in zip(cnt_init,self.radiuses)] + + # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix + peak_std = 4 + # ini_rads = [ 1/4*rad/peak_std for rad in self.radiuses] # initial 'peak_std' radius is 1/2 of the box radius + ini_rads = ellrad / np.sqrt(peak_std) + max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius + min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size + + # initialization and bounds for the precision matrix + if self.parameterization=='givens': + # `num_angles` angles and `ndims` square roots of the eigenvalues of the precision matrix + num_angles = (self.ndims*(self.ndims-1))//2 + # initial rotation angles of the ellipsoid + # ini_angles = [0]*num_angles + ini_angles = [np.arctan2(ellrot[2,1],ellrot[2,2]), np.arctan2(ellrot[2,0],np.sqrt(ellrot[2,1]**2+ellrot[2,2]**2)), np.arctan2(ellrot[1,0],ellrot[0,0])] + prec_init = ini_angles + [ 1/r for r in ini_rads] + prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] + prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] + elif self.parameterization=='cholesky': + # upper triangular part of the Cholesky factor of the precision matrix + num_chol = (self.ndims*(self.ndims+1))//2 + prec_init = list(np.sqrt(np.diag(1/ini_rads))[np.triu_indices(self.ndims)]) + prec_lbnd = [-1000]*num_chol + prec_ubnd = [ 1000]*num_chol + elif self.parameterization=='full': + # arbitrary square root of the precision matrix + num_full = self.ndims**2 + prec_init = list((ellrot.T@((1/ini_rads)[:,np.newaxis]*ellrot)).ravel()) + prec_lbnd = [-1000]*num_full + prec_ubnd = [ 1000]*num_full + + # # initialization and bounds for the skewness + # skew_init = [0]*self.ndims + # skew_lbnd = [0]*self.ndims + # skew_ubnd = [0]*self.ndims + + ################################### + # initialization and bounds for all parameters + params_init = bkgr_init + intst_init + cnt_init + prec_init #+ skew_init + params_lbnd = bkgr_lbnd + intst_lbnd + cnt_lbnd + prec_lbnd #+ skew_lbnd + params_ubnd = bkgr_ubnd + intst_ubnd + cnt_ubnd + prec_ubnd #+ skew_ubnd + + return params_init, params_lbnd, params_ubnd + + + def fit_two_level(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='L-BFGS-B', covariance_parameterization='givens', plot_intermediate=False): + # shape of the largest subhistogram with shape as a power of 2 + shape2 = [2**int(np.log2(self.shape[d])) for d in range(self.ndims)] + + # smallest power of 2 among all dimensions + minpow2 = min([int(np.log2(self.shape[d])) for d in range(self.ndims)]) + + solver1 = solver0 + + params = params_lbnd = params_ubnd = None + for p in [min_level]: + start = time.time() + + + # left, middle (with power of 2 shape) and right bins + binsl = [(self.shape[d]-shape2[d])//2 for d in range(self.ndims)] + binsm = [split_bins([shape2[d]],2**p,recursive=False) for d in range(self.ndims)] + binsr = [(self.shape[d]-shape2[d]) - binsl[d] for d in range(self.ndims)] + + # # combine two middle bins + # binsm = [b[:len(b)//2-1]+[b[len(b)//2-1]+b[len(b)//2+1]]+b[len(b)//2+1:] for b in binsm] + + # bins at the current level + bins = [ ([bl] if bl>0 else [])+bm+([br] if br>0 else []) for bl,bm,br in zip(binsl,binsm,binsr)] + + # fit histogram at the current level + # params, sucess + output = self.fit(bins=bins, return_bins=return_bins, loss=loss, solver=solver1, covariance_parameterization=covariance_parameterization, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) + params, sucess = output[0], output[1] + + # skip level if fit not successful + if not sucess and p<minpow2: + params = params_lbnd = params_ubnd = None + continue + solver1 = solver + + nangles = self.ndims*(self.ndims-1)//2 + sqrtbkgr = params[0] + sqrtintst = params[1] + cnt = params[2:2+self.ndims] + angles = params[2+self.ndims:2+self.ndims+nangles] + invrads = params[2+self.ndims+nangles:2+2*self.ndims+nangles] + # skewness = params[2+2*ndims+nangles:2+3*ndims+nangles] + + ####################################################################### + # refine search bounds + + # bounds for the center + dcnt = [ 10*res*2**(minpow2-p) for res in self.resolution] # search radius is 10 voxels at the current level + cnt_lbnd = [c-dc for c,dc in zip(cnt,dcnt)] + cnt_ubnd = [c+dc for c,dc in zip(cnt,dcnt)] + + # bounds for the precision matrix angles + if minpow2>min_level: + phi0 = np.pi/1 + phi1 = np.pi#/8 + dphi = phi0 + (p-min_level)/(minpow2-min_level) * (phi1-phi0) + else: + dphi = np.pi + + # sc = 2 + (p-min_level)/(minpow2-min_level) * (1-2) + # sc = 2 + # print(sc) + + ###################################### + # bounds for the precision matrix + + # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix + peak_std = 4 + max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius + min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size + # prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] + # prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] + # prec_lbnd = [max(-np.pi,phi-dphi) for phi in angles] + [ r/2.0 for r in invrads] + prec_lbnd = [max(-np.pi,phi-dphi) for phi in angles] + [ 1/r for r in max_rads] #[ max((self.limits[d][1]-self.limits[d][0])/4/(4/3),invrads[d]/2.0) for d in range(self.ndims)] + prec_ubnd = [min( np.pi,phi+dphi) for phi in angles] + [ 1/r for r in min_rads] #[ 100*r for r in invrads] + + # bounds for all parameters + # params_lbnd = [np.abs(sqrtbkgr)/2, np.abs(sqrtintst)/2] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] + # params_ubnd = [2*np.abs(sqrtbkgr), 2*np.abs(sqrtintst)] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] + params_lbnd = [-np.inf, -np.inf] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] + params_ubnd = [ np.inf, np.inf] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] + + # params[0] = np.abs(sqrtbkgr) + # params[1] = np.abs(sqrtintst) + + # params_lbnd = params_lbnd + list(params[len(params_lbnd):]) + # params_ubnd = params_ubnd + list(params[len(params_ubnd):]) + # params_lbnd = params_ubnd = None + + ####################################################################### + + print(f"Fitted histogram with {2**p:3d} bins: {time.time()-start:.3f} seconds") + + # if plot_intermediate: + # plot_fit(hist_ws, params, bins, prefix=f"{p}", peak_id=1074, peak_hkl=[2.0,-2.0,-9.0], peak_std=4, bkgr_std=7, detector_mask=None, log=True) + + start = time.time() + output = self.fit(return_bins=return_bins, loss=loss, solver=solver, covariance_parameterization=covariance_parameterization, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) + print(f"Fitted histogram with origianl bins: {time.time()-start:.3f} seconds") + + return output + + + def fit_multilevel(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='L-BFGS-B', covariance_parameterization='givens', plot_intermediate=False): + # shape of the largest subhistogram with shape as a power of 2 + shape2 = [2**int(np.log2(self.shape[d])) for d in range(self.ndims)] + + # smallest power of 2 among all dimensions minpow2 = min([int(np.log2(self.shape[d])) for d in range(self.ndims)]) solver1 = solver0 @@ -1205,7 +1896,7 @@ class Histogram(object): return output - def fit(self, bins=None, return_bins=False, loss='mle', solver='BFGS', covariance_parameterization='givens', params_init=None, params_lbnd=None, params_ubnd=None): + def fit(self, bins=None, return_bins=False, loss='mle', solver='BFGS', params_init=None, params_lbnd=None, params_ubnd=None): ''' Inputs ------ @@ -1256,93 +1947,24 @@ class Histogram(object): # rebinned data fit_data, fit_points = self.get_grid_data(bins=bins, rebin_mode='density') + fit_data = fit_data.ravel() fit_points = fit_points.reshape((-1,self.ndims)) - # detector_mask = None + # self.detector_mask = None # if self.detector_mask is None: # detector_mask = fit_data==fit_data if self.detector_mask is not None: detector_mask = rebin_histogram(self.detector_mask.astype(int), bins)>0 - fit_data = fit_data.ravel()[detector_mask.ravel()] + fit_data = fit_data[detector_mask.ravel()] fit_points = fit_points[detector_mask.ravel(),:] ########################################################################### # initialization and bounds on parameters - if (params_init is None) or (params_lbnd is None) or (params_ubnd is None): - data_min, data_max, data_mean = fit_data.min(), fit_data.max(), fit_data.mean() - - ################################### - # initialization and bounds for the background intensity - bkgr_init = [ np.sqrt(0.9*data_mean)] #+ [0]*self.ndims - bkgr_lbnd = [-np.sqrt(1.1*data_mean)] #+ [0]*self.ndims - bkgr_ubnd = [ np.sqrt(1.1*data_mean)] #+ [0]*self.ndims - - ################################### - # initialization and bounds for the max peak intensity - intst_init = [ np.sqrt(data_max-data_mean)] - intst_lbnd = [-np.sqrt(data_max)] - intst_ubnd = [ np.sqrt(data_max)] - - ################################### - # cnt_1d, std_1d = initial_parameters(hist_ws, bins) - # params_init1 = initial_parameters(hist_ws, bins, detector_mask) - - # initialization and bounds for the peak center - # dcnt = [ rad/3 for rad in self.radiuses] - # cnt_init = cnt_1d - cnt_init = [(lim[0]+lim[1])/2 for lim in self.limits] - cnt_lbnd = [c-rad/3 for c,rad in zip(cnt_init,self.radiuses)] - cnt_ubnd = [c+rad/3 for c,rad in zip(cnt_init,self.radiuses)] - - # initialization and bounds for the precision matrix - if covariance_parameterization=='givens': - num_angles = (self.ndims*(self.ndims-1))//2 - # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix - peak_std = 4 - ini_rads = [ 1/4*rad/peak_std for rad in self.radiuses] # initial 'peak_std' radius is 1/2 of the box radius - max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius - min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size - # `num_angles` angles and `ndims` square roots of the eigenvalues of the precision matrix - # prec_init = [ 0]*num_angles + std_1d - prec_init = [ 0]*num_angles + [ 1/r for r in ini_rads] - prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] - prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] - elif covariance_parameterization=='cholesky': - num_chol = (self.ndims*(self.ndims+1))//2 - # upper triangular part of the Cholesky factor of the precision matrix - prec_init = list(np.eye(self.ndims)[np.triu_indices(self.ndims)]) - prec_lbnd = [-1000]*num_chol - prec_ubnd = [ 1000]*num_chol - elif covariance_parameterization=='full': - # arbitrary square root of the precision matrix - prec_init = list(np.eye(self.ndims).ravel()) - prec_lbnd = [-1000]*(self.ndims**2) - prec_ubnd = [ 1000]*(self.ndims**2) - - # # initialization and bounds for the skewness - # skew_init = [0]*self.ndims - # skew_lbnd = [0]*self.ndims - # skew_ubnd = [0]*self.ndims - - ################################### - # initialization and bounds for all parameters - # params_init = params_init1 - if params_init is None: params_init = bkgr_init + intst_init + cnt_init + prec_init #+ skew_init - if params_lbnd is None: params_lbnd = bkgr_lbnd + intst_lbnd + cnt_lbnd + prec_lbnd #+ skew_lbnd - if params_ubnd is None: params_ubnd = bkgr_ubnd + intst_ubnd + cnt_ubnd + prec_ubnd #+ skew_ubnd - - # params_init = bkgr_init + intst_init + list(params_init[2:]) - # params_lbnd = bkgr_lbnd + intst_lbnd + list(params_lbnd[2:]) - # params_ubnd = bkgr_ubnd + intst_ubnd + list(params_ubnd[2:]) - - - # params_lbnd = [p-1.e-8 for p in params_init] - # params_ubnd = [p+1.e-8 for p in params_init] + # if (params_init is None) or (params_lbnd is None) or (params_ubnd is None): + if params_init is None: + params_init, params_lbnd, params_ubnd = self.initialize(fit_points, fit_data) - # number of background and peak parameters - nbkgr = 1 #len(bkgr_init) - npeak = len(params_init) - nbkgr ########################################################################### @@ -1366,69 +1988,74 @@ class Histogram(object): result = least_squares(residual, #jac=jacobian_residual, x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) elif loss=='mle': - gaussian_peak = Gaussian(ndims=3, covariance_parameterization=covariance_parameterization) class MLELoss(object): - def __init__(self): + def __init__(self, bkgr_fun, peak_fun): self.func_calls = 0 self.grad_calls = 0 self.hess_calls = 0 + # background and peak functions + self.bkgr_fun = bkgr_fun + self.peak_fun = peak_fun + + # number of background and peak parameters + self.nbkgr = self.bkgr_fun.nparams + self.npeak = self.peak_fun.nparams + def __call__(self, params): self.func_calls+=1 self.func_params = np.asarray(params).copy() - # background - bkgr = params[0]**2 - - # peak - g = gaussian_peak(params[nbkgr:], fit_points) - - # fit - self.fit = bkgr + g + # cache fit for reuse in derivative evaluations + self.fit = self.bkgr_fun(params[:self.nbkgr], fit_points) + self.peak_fun(params[self.nbkgr:], fit_points) return (self.fit-fit_data*np.log(self.fit)).sum() def gradient(self, params, *args, **kwargs): self.grad_calls += 1 self.grad_params = np.asarray(params).copy() - if not np.all(self.grad_params==self.func_params): g = self.__call__(params) + if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): g = self.__call__(params) self.dloss_dfit = 1 - fit_data/self.fit + dloss_dbkgr = self.bkgr_fun.gradient(params[:self.nbkgr], fit_points, dloss_dfit=self.dloss_dfit) + dloss_dpeak = self.peak_fun.gradient(params[self.nbkgr:], fit_points, dloss_dfit=self.dloss_dfit) - self.dloss_dbkgr = np.array([2*params[0]*self.dloss_dfit.sum()]) #,0,0,0] - self.dloss_dpeak = gaussian_peak.gradient(params[nbkgr:], fit_points, dloss_dfit=self.dloss_dfit) - - return np.concatenate((self.dloss_dbkgr,self.dloss_dpeak)) + return np.concatenate((dloss_dbkgr,dloss_dpeak)) def hessian(self, params, *args, **kwargs): self.hess_calls += 1 self.hess_params = np.asarray(params).copy() - if not np.all(self.hess_params==self.func_params): g = self.__call__(params) - if not np.all(self.hess_params==self.grad_params): dg = self.gradient(params) - - d2loss_dfit2 = fit_data/self.fit**2 + if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): g = self.__call__(params) + if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): dg = self.gradient(params) - d2loss_dbkgr2 = 2 * self.dloss_dfit.sum() + (2*params[0])**2 * d2loss_dfit2.sum() - d2loss_dpeak2 = gaussian_peak.hessian(params[nbkgr:],fit_points,dloss_dfit=self.dloss_dfit,d2loss_dfit2=d2loss_dfit2) + d2loss_dfit2 = fit_data / self.fit**2 + d2loss_dbkgr2 = self.bkgr_fun.hessian(params[:self.nbkgr], fit_points, dloss_dfit=self.dloss_dfit, d2loss_dfit2=d2loss_dfit2) + d2loss_dpeak2 = self.peak_fun.hessian(params[self.nbkgr:], fit_points, dloss_dfit=self.dloss_dfit, d2loss_dfit2=d2loss_dfit2) + d2loss_dbkgr_dpeak = ((d2loss_dfit2.reshape((-1,1,1)) * self.bkgr_fun.grad[:,:,np.newaxis]) * self.peak_fun.grad[:,np.newaxis,:]).sum(axis=0) - d2loss = np.zeros((len(params),len(params))) - d2loss[:nbkgr,:nbkgr] = d2loss_dbkgr2 - d2loss[nbkgr:,nbkgr:] = d2loss_dpeak2 - d2loss[:nbkgr,nbkgr:] = 2*params[0] * (d2loss_dfit2.reshape((-1,1)) * gaussian_peak.dg).sum(axis=0) - d2loss[nbkgr:,:nbkgr] = d2loss[:nbkgr,nbkgr:].T + d2loss = np.block([ + [d2loss_dbkgr2, d2loss_dbkgr_dpeak], + [d2loss_dbkgr_dpeak.T, d2loss_dpeak2 ] + ]) return d2loss # from scipy.optimize import BFGS - # print(BFGS) - # exit() - - peak_loss = MLELoss() - result = minimize(peak_loss, - jac = peak_loss.gradient, - hess = peak_loss.hessian, + # class myBFGS(BFGS): + # def initialize(self, n, approx_type): + # super().initialize(n, approx_type) + # if self.approx_type == 'hess': + # self.B = MLELoss().hessian(params_init) + # # self.B = np.eye(n, dtype=float) + # else: + # self.H = np.linalg.inv(MLELoss().hessian(params_init)) + # # self.H = np.eye(n, dtype=float) + loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun) + result = minimize(loss_fun, + jac = loss_fun.gradient, + hess = loss_fun.hessian, x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), - method=solver, options={'maxiter':100, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':False} + method=solver, options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':_debug} # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} # method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True} @@ -1440,10 +2067,10 @@ class Histogram(object): # method='trust-exact', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} # method='trust-constr', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} ) - # restrict parameters - if solver!='L-BFGS-B': - for i in range(len(result.x)): - result.x[i] = max(min(result.x[i],params_ubnd[i]),params_lbnd[i]) + # # restrict parameters + # if solver!='L-BFGS-B': + # for i in range(len(result.x)): + # result.x[i] = max(min(result.x[i],params_ubnd[i]),params_lbnd[i]) # print(peak_loss.func_calls, peak_loss.grad_calls, peak_loss.hess_calls) # g,dg = gaussian_mixture(result.x[nbkgr:],fit_points,npeaks=1,covariance_parameterization=covariance_parameterization,return_gradient=True) @@ -1455,44 +2082,62 @@ class Histogram(object): # print(result.jac) # print(grad) - np.set_printoptions(precision=1, linewidth=200) - - self.fit_params = result.x + self.init_params = np.array(params_init) + self.fit_params = result.x # fit_params[0] = fit_params[0] / np.sqrt(sc) # fit_params[1] = fit_params[1] / np.sqrt(sc) # return self.fit_params, True, bins # print(result) - print(result.success) + # print(params_init) + # print(result.success) + # print(result.x) # print('Chi2: ',chi2(fit_params)) # print(np.linalg.eig(peak_loss.hessian(self.fit_params))[0]) # print(np.linalg.eig(numerical_hessian(self.fit_params, lambda y: peak_loss(y)))[0]) - # # start = time.time() - # # g,dg,d2g = gaussian_mixture(self.fit_params[nbkgr:],fit_points,npeaks=1,covariance_parameterization=covariance_parameterization,return_gradient=True,return_hessian=True) - # # print(time.time()-start) - - # start = time.time() - # dg = peak_loss.gradient(self.fit_params) - # d2g = peak_loss.hessian(self.fit_params) - # print(time.time()-start) - - # start = time.time() - # grad = numerical_gradient(self.fit_params, lambda x: peak_loss(x)) - # hess = numerical_hessian(self.fit_params, lambda x: peak_loss(x)) - # print(time.time()-start) - - # print('Gradient') - # print(dg)#[15855,:]) - # print(grad) - - # print('Hessian') - # # print(np.abs((d2g[15855,:10,:10]-hess[:10,:10])/(d2g[15855,:10,:10]+1.e-10)).max()) - # print(np.abs((d2g-hess)/(d2g+1.e-10)).max()) - # # print(hess[1:4,1:]) - # print(d2g)#[15855,...]) - # print(hess) - + if _debug: + print(f'\nConverged: {result.success}') + print(f'\nInitial params: {np.array(params_init)+1e-99}') + print(f' Final params: {result.x}') + + start = time.time() + dg = loss_fun.gradient(self.fit_params) + dg_time = time.time()-start + + start = time.time() + fddg = numerical_gradient(self.fit_params, lambda x: loss_fun(x)) + fddg_time = time.time()-start + + print('\nGradient') + print('--------') + print(f'Exact: {dg}') + print(f' FD: {fddg}') + print(f'Max. diff: {np.abs(dg-fddg).max():.3e}') + print(f'Rel. diff: {np.abs((dg-fddg)/(dg+1.e-10)).max():.3e}') + print(f'Exact time: {dg_time:.3f} sec') + print(f' FD time: {fddg_time:.3f} sec, {fddg_time/dg_time:.2f} slower') + + start = time.time() + d2g = loss_fun.hessian(self.fit_params) + d2g_time = time.time()-start + + start = time.time() + fdd2g = numerical_hessian(self.fit_params, lambda x: loss_fun(x)) + fdd2g_time = time.time()-start + + print('\nHessian') + print('-------') + # print(f'Exact: \n{d2g}') + # print(f'FD: \n{fdd2g}') + # print(f'Exact: \n{d2g[:4,:4]}') + # print(f'FD: \n{fdd2g[:4,:4]}') + print(f'Exact: \n{d2g[4:,4:]}') + print(f'FD: \n{fdd2g[4:,4:]}') + print(f'Max. diff: {np.abs(d2g-fdd2g).max():.3e}') + print(f'Rel. diff: {np.abs((d2g-fdd2g)/(d2g+1.e-10)).max():.3e}') + print(f'Exact time: {d2g_time:.3f} sec') + print(f' FD time: {fdd2g_time:.3f} sec, {fdd2g_time/d2g_time:.2f} slower') # exit() if return_bins: @@ -1511,27 +2156,60 @@ class Histogram(object): # point along each dimension dim_points = [points[:,0,0,0],points[0,:,0,1],points[0,0,:,2]] + # parameters of the model nbkgr = 1 #+ self.ndims - npeak = self.fit_params.size - nbkgr - ncnt = self.ndims - ncov = (self.ndims*(self.ndims+1))//2 - nangles = (self.ndims*(self.ndims-1))//2 - nskew = self.ndims - bkgr = self.fit_params[:nbkgr] - intst = self.fit_params[nbkgr] - mu = self.fit_params[1+nbkgr:1+nbkgr+ncnt] - sqrtP = self.fit_params[1+nbkgr+ncnt:1+nbkgr+ncnt+ncov] - angles = sqrtP[:nangles] - sqrt_eig = 1 / sqrtP[nangles:] - skew = self.fit_params[1+nbkgr+ncnt+ncov:1+nbkgr+ncnt+ncov+nskew] + + ####################################################################### + # evaluate inital model + + # parameters + ini_bkgr_params = self.init_params[:nbkgr] + ini_peak_params = self.init_params[nbkgr:] + + # peak center and full covariance + # cov_3d = R.T @ np.diag(sqrt_eig**2) @ R + _,ini_mu,_ = self.peak_fun.get_parameters(ini_peak_params) + ini_cov_3d = self.peak_fun.Cov(ini_peak_params) + + # covariances and ellipsoids of 2d marginals + ini_cov_2d = [] + ini_angle_2d = [] + ini_sigma_2d = [] + for i in range(self.ndims): + yind, xind = [j for j in range(self.ndims) if j!=i] + ini_cov_2d.append( ini_cov_3d[np.ix_([xind,yind],[xind,yind])] ) + roti,eigi,_ = svd(ini_cov_2d[-1]) + # eigi, roti = eig(cov_2d[-1]) + # eigi, roti = eigen(cov_2d[-1]) + ini_angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) + ini_sigma_2d.append( np.sqrt(eigi) ) + + # fitted model + ini_fit = ini_bkgr_params[0]**2 + self.peak_fun(ini_peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) + ini_fit_masked = (1 if self.detector_mask is None else self.detector_mask) * ini_fit + + # rebinned data and fit + rebinned_data, rebinned_points, rebinned_edges = self.get_grid_data(bins=bins, rebin_mode='density', return_edges=True) + ini_rebinned_fit = rebin_histogram(ini_fit, bins, mode='density') + + + ####################################################################### + # evaluate final model + + # final parameters + bkgr_params = self.fit_params[:nbkgr] + peak_params = self.fit_params[nbkgr:] # inverse rotation matrix - R,_,_ = rotation_matrix(angles) + # R,_,_ = rotation_matrix(angles) + # R = self.peak_fun.rotation_matrix(self.fit_params[nbkgr:]) - # full covariance matrix - cov_3d = R.T @ np.diag(sqrt_eig**2) @ R + # peak center and full covariance + # cov_3d = R.T @ np.diag(sqrt_eig**2) @ R + _,mu,_ = self.peak_fun.get_parameters(peak_params) + cov_3d = self.peak_fun.Cov(peak_params) # covariances and ellipsoids of 2d marginals cov_2d = [] @@ -1547,23 +2225,307 @@ class Histogram(object): sigma_2d.append( np.sqrt(eigi) ) # fitted model - fit = bkgr[0]**2 + gaussian_mixture(self.fit_params[nbkgr:], points.reshape((-1,self.ndims)), covariance_parameterization='givens').reshape(data.shape) + fit = bkgr_params[0]**2 + self.peak_fun(peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) fit_masked = (1 if self.detector_mask is None else self.detector_mask) * fit # rebinned data and fit rebinned_data, rebinned_points, rebinned_edges = self.get_grid_data(bins=bins, rebin_mode='density', return_edges=True) rebinned_fit = rebin_histogram(fit, bins, mode='density') - # fit[data==0] = 0 - # rebinned_fit[rebinned_data==0] = 0 - # data[data>0] = data[data>0] - bkgr - # data -= bkgr - # rebinned_data -= bkgr - # rebinned_data = rebin_histogram(data, bins, mode='density') - # fit -= bkgr - # rebinned_fit -= bkgr - # fit_masked = fit.copy() + ####################################################################### + # plot + + fig = plt.figure(constrained_layout=True, figsize=(20,45)) + subfigs = fig.subfigures(7,1) #wspace=0.07 + subfig_no=-1 + + # normalize plots + normalize = False + + ######################################## + # plot 1d marginals + + data_1d = marginalize_1d(data, normalize=normalize, mask=self.detector_mask) + fit_1d = marginalize_1d(fit, normalize=normalize, mask=self.detector_mask) + ini_fit_1d = marginalize_1d(ini_fit, normalize=normalize, mask=self.detector_mask) + rebinned_data_1d = marginalize_1d(rebinned_data, normalize=normalize, bin_lengths=bins, mask=self.detector_mask) + rebinned_fit_1d = marginalize_1d(rebinned_fit, normalize=normalize, bin_lengths=bins, mask=self.detector_mask) + + subfig_no+=1 + for i,ax in enumerate(subfigs[subfig_no].subplots(1,3,sharey=True)): + ax.stairs(data_1d[i], edges=edges[i], fill=True, alpha=0.3) + ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, color='b', baseline=None) + ax.plot(dim_points[i], ini_fit_1d[i], color='g', ls='-') + ax.plot(dim_points[i], fit_1d[i], color='black') + ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') + if i==0: + ax.legend(['data','reb. data','init. fit','final fit'], framealpha=1.0, fontsize='xx-large') + ax.set_box_aspect(1) + subfigs[subfig_no].suptitle('Original data and fit', fontsize='xx-large') + + subfig_no+=1 + for i,ax in enumerate(subfigs[subfig_no].subplots(1,3,sharey=True)): + ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=True, alpha=0.5) + ax.stairs(rebinned_fit_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, baseline=None) + ax.plot(dim_points[i], fit_1d[i], color='black') + ax.vlines([mu[i]-peak_std*np.sqrt(cov_3d[i,i]),mu[i]+peak_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-') + ax.vlines([mu[i]-bkgr_std*np.sqrt(cov_3d[i,i]),mu[i]+bkgr_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-.') + ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') + if i==0: + ax.legend(['reb. data','reb. fit','final fit', f'{peak_std} sigma', f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.set_box_aspect(1) + subfigs[subfig_no].suptitle('Fit with identified peak bounds', fontsize='xx-large') + + # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}_1d.png') + + ######################################## + # plot 2d marginals + + data_2d = marginalize_2d(data, normalize=normalize) + fit_2d = marginalize_2d(fit, normalize=normalize) + ini_fit_2d = marginalize_2d(ini_fit, normalize=normalize) + fit_masked_2d = marginalize_2d(fit_masked, normalize=normalize) + rebinned_data_2d = marginalize_2d(rebinned_data, normalize=normalize, bin_lengths=bins, recover_shape=True) + rebinned_fit_2d = marginalize_2d(rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True) + ini_rebinned_fit_2d = marginalize_2d(ini_rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True) + + # show zero pixels as None + data_2d = [d/(d!=0) for d in data_2d] + fit_2d = [d/(d!=0) for d in fit_2d] + ini_fit_2d = [d/(d!=0) for d in ini_fit_2d] + fit_masked_2d = [d/(d!=0) for d in fit_masked_2d] + rebinned_data_2d = [d/(d!=0) for d in rebinned_data_2d] + rebinned_fit_2d = [d/(d!=0) for d in rebinned_fit_2d] + + if log: + data_2d = np.log(data_2d) + fit_2d = np.log(fit_2d) + ini_fit_2d = np.log(ini_fit_2d) + fit_masked_2d = np.log(fit_masked_2d) + rebinned_data_2d = np.log(rebinned_data_2d) + rebinned_fit_2d = np.log(rebinned_fit_2d) + ini_rebinned_fit_2d = np.log(ini_rebinned_fit_2d) + + + def axes_order(i): + if i==0: return (1,0) + if i==1: return (2,1) + if i==2: return (0,2) + # ax_order = lambda i: [j for j in range(3) if j!=i] + + + ######################################## + vmind = min([im.min() for im in data_2d]) + vmaxd = max([im.max() for im in data_2d]) + vminf = min([im.min() for im in fit_2d]) + vmaxf = max([im.max() for im in fit_2d]) + vmin = min([vmind, vminf]) + vmax = min([vmaxd, vmaxf]) + + # original data + subfig_no+=1 + for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): + # yind, xind = [j for j in range(3) if j!=i] + yind, xind = axes_order(i) + left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + im = ax.imshow(data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) + ax.add_patch(Ellipse((ini_mu[xind],ini_mu[yind]), 2*peak_std*ini_sigma_2d[i][0], 2*peak_std*ini_sigma_2d[i][1], ini_angle_2d[i], color='green', ls='-', fill=False)) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) + if i==0: + ax.legend([f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') + ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') + subfigs[subfig_no].suptitle('Data 2d marginals', fontsize='xx-large') + subfigs[subfig_no].colorbar(im, ax=ax, location='right') + + # gaussian fit + subfig_no+=1 + for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): + # yind, xind = [j for j in range(3) if j!=i] + yind, xind = axes_order(i) + left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + im = ax.imshow(fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) + ax.add_patch(Ellipse((ini_mu[xind],ini_mu[yind]), 2*peak_std*ini_sigma_2d[i][0], 2*peak_std*ini_sigma_2d[i][1], ini_angle_2d[i], color='green', ls='-', fill=False)) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) + if i==0: + ax.legend([f'init. {peak_std} sigma', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') + ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') + subfigs[subfig_no].suptitle('Fit 2d marginals', fontsize='xx-large') + subfigs[subfig_no].colorbar(im, ax=ax, location='right') + + + ######################################## + vmind = min([im.min() for im in rebinned_data_2d]) + vmaxd = max([im.max() for im in rebinned_data_2d]) + vminf = min([im.min() for im in rebinned_fit_2d]) + vmaxf = max([im.max() for im in rebinned_fit_2d]) + vmin = min([vmind, vminf]) + vmax = min([vmaxd, vmaxf]) + + # rebinned data + subfig_no+=1 + for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): + # yind, xind = [j for j in range(3) if j!=i] + yind, xind = axes_order(i) + left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + im = ax.imshow(rebinned_data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) + if i==0: + ax.legend([f'init. {peak_std} sigma',f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') + ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') + subfigs[subfig_no].suptitle('Rebinned data 2d marginals', fontsize='xx-large') + subfigs[subfig_no].colorbar(im, ax=ax, location='right') + + # fit to rebinned data + subfig_no+=1 + for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): + # yind, xind = [j for j in range(3) if j!=i] + yind, xind = axes_order(i) + left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + im = ax.imshow(rebinned_fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) + ax.add_patch(Ellipse((ini_mu[xind],ini_mu[yind]), 2*peak_std*ini_sigma_2d[i][0], 2*peak_std*ini_sigma_2d[i][1], ini_angle_2d[i], color='green', ls='-', fill=False)) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) + if i==0: + ax.legend([f'init. {peak_std} sigma',f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') + ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') + subfigs[subfig_no].suptitle('Rebinned fit 2d marginals', fontsize='xx-large') + subfigs[subfig_no].colorbar(im, ax=ax, location='right') + + + ######################################## + # plot grids + + # # fig = plt.figure(constrained_layout=True, figsize=(18,6)) + # # axes = fig.subplots(1,3) + # for i,ax in enumerate(axes[6]): + # yind, xind = [j for j in range(3) if j!=i] + # left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + # ax.hlines(rebinned_edges[yind], rebinned_edges[xind][0], rebinned_edges[xind][-1]) + # ax.vlines(rebinned_edges[xind], rebinned_edges[yind][0], rebinned_edges[yind][-1]) + # ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', fill=False)) + # ax.set_xlabel(hist_ws.getDimension(xind).name, fontsize=10) + # ax.set_ylabel(hist_ws.getDimension(yind).name, fontsize=10) + # # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}_grid.png') + + ######################################## + # plot difference + + subfig_no+=1 + vmin = min([np.abs(fit_masked_2d[i]-data_2d[i]).min() for i in range(3) ]) + vmax = max([np.abs(fit_masked_2d[i]-data_2d[i]).max() for i in range(3) ]) + for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): + # yind, xind = [j for j in range(3) if j!=i] + yind, xind = axes_order(i) + left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + im = ax.imshow(np.abs(fit_masked_2d[i]-data_2d[i]), interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) + ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') + ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') + subfigs[subfig_no].suptitle('Fit error', fontsize='xx-large') + subfigs[subfig_no].colorbar(im, ax=ax, location='right') + + # ######################################## + # # plot detector mask + + # if detector_mask is not None: + # detector_mask_2d = marginalize_2d(detector_mask, normalize=False) + # detector_mask_2d = [(d!=0).astype(int) for d in detector_mask_2d] + # detector_mask_2d = [d/(d!=0) for d in detector_mask_2d] + # ax_id += 1 + # for i,ax in enumerate(axes[ax_id]): + # yind, xind = [j for j in range(3) if j!=i] + # left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + # ax.imshow(detector_mask_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower') + # ax.set_xlabel(hist_ws.getDimension(xind).name, fontsize=10) + # ax.set_ylabel(hist_ws.getDimension(yind).name, fontsize=10) + + ######################################## + # save + if prefix is None: + plt.savefig(f'{plot_path}/peak.png') + else: + plt.savefig(f'{plot_path}/{prefix}_peak.png') + + # # save + # if prefix is None: + # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}.png') + # else: + # plt.savefig(f'{plot_path}/{prefix}_peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}.png') + + plt.close('all') + + + def plot1(self, bins, plot_path='output', prefix=None, peak_std=4, bkgr_std=10, log=False): + # create output directory for plots + Path(plot_path).mkdir(parents=True, exist_ok=True) + + # fit model to data + data, points, edges = self.get_grid_data(return_edges=True) + + # point along each dimension + dim_points = [points[:,0,0,0],points[0,:,0,1],points[0,0,:,2]] + + + ####################################################################### + # evaluate model + + # parameters of the model + nbkgr = 1 #+ self.ndims + # npeak = self.fit_params.size - nbkgr + # ncnt = self.ndims + # ncov = (self.ndims*(self.ndims+1))//2 + # nangles = (self.ndims*(self.ndims-1))//2 + # nskew = self.ndims + + # bkgr = self.fit_params[:nbkgr] + # intst = self.fit_params[nbkgr] + # mu = self.fit_params[1+nbkgr:1+nbkgr+ncnt] + # sqrtP = self.fit_params[1+nbkgr+ncnt:1+nbkgr+ncnt+ncov] + # angles = sqrtP[:nangles] + # sqrt_eig = 1 / sqrtP[nangles:] + # skew = self.fit_params[1+nbkgr+ncnt+ncov:1+nbkgr+ncnt+ncov+nskew] + + bkgr_params = self.fit_params[:nbkgr] + peak_params = self.fit_params[nbkgr:] + + # inverse rotation matrix + # R,_,_ = rotation_matrix(angles) + # R = self.peak_fun.rotation_matrix(self.fit_params[nbkgr:]) + + _,mu,_ = self.peak_fun.get_parameters(peak_params) + + # full covariance matrix + # cov_3d = R.T @ np.diag(sqrt_eig**2) @ R + cov_3d = self.peak_fun.Cov(peak_params) + + # covariances and ellipsoids of 2d marginals + cov_2d = [] + angle_2d = [] + sigma_2d = [] + for i in range(self.ndims): + yind, xind = [j for j in range(self.ndims) if j!=i] + cov_2d.append( cov_3d[np.ix_([xind,yind],[xind,yind])] ) + roti,eigi,_ = svd(cov_2d[-1]) + # eigi, roti = eig(cov_2d[-1]) + # eigi, roti = eigen(cov_2d[-1]) + angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) + sigma_2d.append( np.sqrt(eigi) ) + + # fitted model + # fit = bkgr[0]**2 + gaussian_mixture(self.fit_params[nbkgr:], points.reshape((-1,self.ndims)), covariance_parameterization='givens').reshape(data.shape) + fit = bkgr_params[0]**2 + self.peak_fun(peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) + fit_masked = (1 if self.detector_mask is None else self.detector_mask) * fit + + # rebinned data and fit + rebinned_data, rebinned_points, rebinned_edges = self.get_grid_data(bins=bins, rebin_mode='density', return_edges=True) + rebinned_fit = rebin_histogram(fit, bins, mode='density') normalize = False -- GitLab From d3b7f0643aa176638017530f98ccc181b8a1af35 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Mon, 26 Dec 2022 18:22:15 -0500 Subject: [PATCH 02/26] add `def check_parameters` --- peak_integration.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/peak_integration.py b/peak_integration.py index b08b4ff..00eabe8 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -641,6 +641,13 @@ class Gaussian(object): self.nangles = None + def check_parameters(self, params): + params = np.asarray(params).copy().ravel() + if params.size!=self.nparams: + raise ValueError(f"`params` array is of wrong size, must have `len(params) = 1 + ncnt + ncov + nskew = {self.nparams}`, got {len(params)}") + return params + + def rotation_matrix(self, params): ''' Compute rotation matrix from parameters https://en.wikipedia.org/wiki/Givens_rotation#Table_of_composed_rotations -- GitLab From 20bd793d3fbcabd31f61a7eb8cf0c32f0f64b164 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Mon, 26 Dec 2022 19:11:49 -0500 Subject: [PATCH 03/26] update ` def plot` --- peak_integration.py | 441 ++++++++++++++++++++++++-------------------- 1 file changed, 243 insertions(+), 198 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 00eabe8..97da56f 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -7,7 +7,6 @@ from scipy.optimize import minimize from scipy.linalg import svd from numpy.linalg import eig from scipy.special import loggamma, erf -from scipy.ndimage import laplace import matplotlib # non-interactive backend that can only write to files @@ -104,7 +103,7 @@ def marginalize_1d(arr, bin_lengths=None, mask=None, normalize=False, recover_sh return marginals -def marginalize_2d(arr, bin_lengths=None, mask=None, normalize=False, recover_shape=False): +def marginalize_2d(arr, bin_lengths=None, mask=None, normalize=False, recover_shape=False, sortx=False): ''' Compute 2d marginals of the multidimensional array Inputs @@ -157,51 +156,13 @@ def marginalize_2d(arr, bin_lengths=None, mask=None, normalize=False, recover_sh for j,ax1 in enumerate([i for i in range(ndims) if i!=ax]): marginals[ax] = np.repeat(marginals[ax], bin_lengths[ax1], axis=j) - return marginals - + if sortx: + return [marginals[2].T, marginals[0].T, marginals[1]] + else: + return marginals -# def ellipsoid_fit(X): -# # https://github.com/aleksandrbazhin/ellipsoid_fit_python -# x = X[:, 0] -# y = X[:, 1] -# z = X[:, 2] -# D = np.array([x * x + y * y - 2 * z * z, -# x * x + z * z - 2 * y * y, -# 2 * x * y, -# 2 * x * z, -# 2 * y * z, -# 2 * x, -# 2 * y, -# 2 * z, -# 1 - 0 * x]) -# d2 = np.array(x * x + y * y + z * z).T # rhs for LLSQ -# u = np.linalg.solve(D.dot(D.T), D.dot(d2)) -# a = np.array([u[0] + 1 * u[1] - 1]) -# b = np.array([u[0] - 2 * u[1] - 1]) -# c = np.array([u[1] - 2 * u[0] - 1]) -# v = np.concatenate([a, b, c, u[2:]], axis=0).flatten() -# A = np.array([[v[0], v[3], v[4], v[6]], -# [v[3], v[1], v[5], v[7]], -# [v[4], v[5], v[2], v[8]], -# [v[6], v[7], v[8], v[9]]]) - -# center = np.linalg.solve(- A[:3, :3], v[6:9]) - -# translation_matrix = np.eye(4) -# translation_matrix[3, :3] = center.T - -# R = translation_matrix.dot(A).dot(translation_matrix.T) - -# evals, evecs = np.linalg.eig(R[:3, :3] / -R[3, 3]) -# evecs = evecs.T - -# radii = np.sqrt(1. / np.abs(evals)) -# radii *= np.sign(evals) - -# return center, evecs, radii, v - -def fitMinVolEllipsoid(points, tolerance=0.01, maxit=100): +def MinVolEllipsoid(points, tolerance=0.01, maxit=100): """ Find the minimum volume ellipsoid which holds all the points Based on work by Nima Moshtagh @@ -649,7 +610,7 @@ class Gaussian(object): def rotation_matrix(self, params): - ''' Compute rotation matrix from parameters + ''' Compute rotation matrix from parameters and cache quantities for later reuse in derivative computations https://en.wikipedia.org/wiki/Givens_rotation#Table_of_composed_rotations ''' # extract angles from the parameters @@ -683,7 +644,7 @@ class Gaussian(object): def rotation_matrix_gradient(self, params): ''' - Compute gradient of the rotation matrix from parameters. + Compute gradient of the rotation matrix from parameters and cache quantities for later reuse in derivative computations It is assumed that `self.R` has been computed before for the same parameters ''' @@ -709,7 +670,7 @@ class Gaussian(object): def rotation_matrix_hessian(self, params): ''' - Compute Hessian of the rotation matrix from parameters. + Compute Hessian of the rotation matrix from parameters and cache quantities for later reuse in derivative computations It is assumed that `self.R` and `self.dR` have been computed before for the same parameters ''' @@ -759,15 +720,17 @@ class Gaussian(object): if self.parameterization=='full': self.sqrtP = sqrtP.reshape((self.ndims,self.ndims)) elif self.parameterization=='cholesky': - self.sqrtP = np.zeros((self.ndims,self.ndims)) triu_ind = np.triu_indices(self.ndims) diag_ind = np.diag_indices(self.ndims) + # dense matrix + self.sqrtP = np.zeros((self.ndims,self.ndims)) # fill upper triangular part self.sqrtP[triu_ind] = sqrtP # positive diagonal makes Cholesky decomposition unique self.sqrtP[diag_ind] *= self.sqrtP[diag_ind] #np.exp(sqrtP_i[diag_ind]) elif self.parameterization=='givens': - # square roots of the eigenvalues of the precision matrix + # square roots of the eigenvalues of the precision matrix, + # aka lengths of the ellipsoid semiaxes self.sqrtD = sqrtP[self.nangles:] # square root of the precision matrix, i.e., diag(sqrt_eig) @ R self.sqrtP = self.sqrtD[:,np.newaxis] * self.rotation_matrix(params) @@ -776,9 +739,7 @@ class Gaussian(object): def __call__(self, params, x): start = time.time() - self.func_params = np.asarray(params).copy().ravel() - if self.func_params.size!=self.nparams: - raise ValueError(f"`params` array is of wrong size, must have `len(params) = 1 + ncnt + ncov + nskew = {self.nparams}`, got {len(params)}") + self.func_params = self.check_parameters(params) # get parameters intst, cnt, sqrtP = self.get_parameters(self.func_params) @@ -797,9 +758,7 @@ class Gaussian(object): def gradient(self, params, x, dloss_dfit=None, *args, **kwargs): start = time.time() - self.grad_params = np.asarray(params).copy().ravel() - if self.grad_params.size!=self.nparams: - raise ValueError(f"`params` array is of wrong size, must have `len(params) = 1 + ncnt + ncov + nskew = {self.nparams}`, got {len(params)}") + self.grad_params = self.check_parameters(params) # function always needs to evaluated before computing gradient if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): @@ -812,21 +771,28 @@ class Gaussian(object): self.sqrtPxcnt_ = -np.einsum('ip,kp->ki',self.sqrtP,self.xcnt) # (npoints,ndims) self.Pxcnt = np.einsum('ip,kp->ki',self.P, self.xcnt) # (npoints,ndims) - self.dg_dintst = self.val[:,np.newaxis] * (2/self.sqrtintst) # (npoints,1) - self.dg_dcnt = self.val[:,np.newaxis] * self.Pxcnt # (npoints,ndims) + self.dg_dintst = self.val[:,np.newaxis] * (2/self.sqrtintst) # (npoints,1) + self.dg_dcnt = self.val[:,np.newaxis] * self.Pxcnt # (npoints,ndims) # dg_dskew = np.zeros_like(dg_dcnt) # (npoints,ndims) - if self.parameterization=='full': - self.dg_dsqrtP = np.einsum('ki,kj->kij', np.einsum('k,ki->ki',self.val,self.sqrtPxcnt_), self.xcnt) - elif self.parameterization=='cholesky': + if self.parameterization in ['full','cholesky']: + # full gradient self.dg_dsqrtP = np.einsum('ki,kj->kij', np.einsum('k,ki->ki',self.val,self.sqrtPxcnt_), self.xcnt) - # update diagonal - diag_ind = np.diag_indices(self.ndims) - self.dg_dsqrtP[:,diag_ind[0],diag_ind[1]] *= 2 * np.sqrt(self.sqrtP[np.newaxis,diag_ind[0],diag_ind[1]]) + # update cholesky gradient + if self.parameterization=='cholesky': + diag_ind = np.diag_indices(self.ndims) + triu_ind = np.triu_indices(self.ndims) + + # update diagonal, note np.sqrt due to the diagonal containing squared values + self.dg_dsqrtP[:,diag_ind[0],diag_ind[1]] *= 2 * np.sqrt(self.sqrtP[np.newaxis,diag_ind[0],diag_ind[1]]) + self.dg_dsqrtP = np.triu(self.dg_dsqrtP) + + # exrtact upper triangular part + dg_dsqrtP = self.dg_dsqrtP[:,triu_ind[0],triu_ind[1]].reshape((-1,self.ncov)) + else: + dg_dsqrtP = self.dg_dsqrtP.reshape((-1,self.ncov)) - triu_ind = np.triu_indices(self.ndims) - self.dg_dsqrtP = self.dg_dsqrtP[:,triu_ind[0],triu_ind[1]] elif self.parameterization=='givens': dR = self.rotation_matrix_gradient(params) @@ -836,12 +802,13 @@ class Gaussian(object): self.dsqrtPxcnt_dangle = np.einsum('ijp,kp->kij',self.dsqrtP_dangle,self.xcnt) # (npoints,nangles,ndims) self.sqrtPxcnt_dsqrtPxcnt_dangle_ = np.einsum('kp,kip->ki',self.sqrtPxcnt_,self.dsqrtPxcnt_dangle) # (npoints,nangles) - self.dg_dangle = self.val[:,np.newaxis] * self.sqrtPxcnt_dsqrtPxcnt_dangle_ # (npoints,ndims) + self.dg_dangle = self.val[:,np.newaxis] * self.sqrtPxcnt_dsqrtPxcnt_dangle_ # (npoints,ndims) self.dg_dsqrtD = self.val[:,np.newaxis] * (self.sqrtPxcnt_ * self.Rxcnt) # (npoints,ndims) self.dg_dsqrtP = np.concatenate((self.dg_dangle,self.dg_dsqrtD), axis=-1) + dg_dsqrtP = self.dg_dsqrtP.reshape((-1,self.ncov)) - self.grad = np.concatenate((self.dg_dintst,self.dg_dcnt,self.dg_dsqrtP.reshape((-1,self.ncov))), axis=-1) + self.grad = np.concatenate((self.dg_dintst,self.dg_dcnt,dg_dsqrtP), axis=-1) grad = self.grad if dloss_dfit is not None: @@ -855,9 +822,7 @@ class Gaussian(object): def hessian(self, params, x, dloss_dfit, d2loss_dfit2, *args, **kwargs): start = time.time() - self.hess_params = np.asarray(params).copy().ravel() - if self.hess_params.size!=self.nparams: - raise ValueError(f"`params` array is of wrong size, must have `len(params) = 1 + ncnt + ncov + nskew = {self.nparams}`, got {len(params)}") + self.hess_params = self.check_parameters(params) # function and gradient always need to evaluated before computing hessian if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): @@ -883,7 +848,7 @@ class Gaussian(object): # print(f'{time.time()-start1} d2g_dcnt_dangle'); start1 = time.time() if self.parameterization=='full': - d2g_dintst_dsqrtP = self.dg_dsqrtP.reshape((-1,1,self.ncov)) * (2/self.sqrtintst) # (npoints,ndims,ndims) + d2g_dintst_dsqrtP = self.dg_dsqrtP.reshape((-1,1,self.ncov)) * (2/self.sqrtintst) # (npoints,1,ncov) axi,axm = np.diag_indices(self.ndims) # d2g_dcnt_dsqrtP = np.einsum('ki,kj->kij', self.dg_dcnt, np.einsum('ki,kj->kij', self.sqrtPxcnt_, self.xcnt).reshape((-1,self.ncov)) ) @@ -895,6 +860,29 @@ class Gaussian(object): d2g_dsqrtP2 = np.einsum('kij,knm->kijnm', self.dg_dsqrtP, np.einsum('kn,km->knm', self.sqrtPxcnt_, self.xcnt) ) d2g_dsqrtP2[:,axi,:,axm,:] -= self.val.reshape((-1,1,1)) * np.einsum('kj,km->kjm',self.xcnt,self.xcnt) d2g_dsqrtP2 = d2g_dsqrtP2.reshape((-1,self.ncov,self.ncov)) + elif self.parameterization=='cholesky': + axi,axm = np.diag_indices(self.ndims) + triu_ind = np.triu_indices(self.ndims) + + d2g_dintst_dsqrtP = self.dg_dsqrtP[:,triu_ind[0],triu_ind[1]].reshape((-1,1,self.ncov)) * (2/self.sqrtintst) # (npoints,1,ncov) + + # upper triangular part + d2g_dcnt_dsqrtP = np.einsum('ki,knm->kinm', self.dg_dcnt, np.einsum('ki,kj->kij', self.sqrtPxcnt_, self.xcnt) ) + d2g_dcnt_dsqrtP += self.val.reshape((-1,1,1,1)) * np.einsum('ni,km->kinm', self.sqrtP, self.xcnt) + d2g_dcnt_dsqrtP[:,axi,:,axm] -= self.val.reshape((-1,1)) * self.sqrtPxcnt_ + + # update diagonal entries + d2g_dcnt_dsqrtP[:,:,axi,axm] *= 2 * self.sqrtP[axi,axm] + + d2g_dcnt_dsqrtP = d2g_dcnt_dsqrtP[:,:,triu_ind[0],triu_ind[1]].reshape((-1,self.ncnt,self.ncov)) + + + d2g_dsqrtP2 = np.einsum('kij,knm->kijnm', self.dg_dsqrtP, np.einsum('kn,km->knm', self.sqrtPxcnt_, self.xcnt) ) + d2g_dsqrtP2[:,axi,:,axm,:] -= self.val.reshape((-1,1,1)) * np.einsum('kj,km->kjm',self.xcnt,self.xcnt) + d2g_dsqrtP2[:,:,:,axi,axm] *= 2 * self.sqrtP[axi,axm] + d2g_dsqrtP2[:,axi,axm,axi,axm] += 2 * np.einsum('kij,knm->kijnm', self.dg_dsqrtP, np.einsum('kn,km->knm', self.sqrtPxcnt_, self.xcnt) )[:,axi,axm,axi,axm] + + d2g_dsqrtP2 = d2g_dsqrtP2[:,:,:,triu_ind[0],triu_ind[1]][:,triu_ind[0],triu_ind[1],:] elif self.parameterization=='givens': d2R = self.rotation_matrix_hessian(params) @@ -940,6 +928,16 @@ class Gaussian(object): d2g_dcnt_dsqrtP = (dloss_dfit*d2g_dcnt_dsqrtP).sum(axis=0) d2g_dsqrtP2 = (dloss_dfit*d2g_dsqrtP2).sum(axis=0) + d2g = np.block([ + [d2g_dintst2, d2g_dintst_dcnt, d2g_dintst_dsqrtP], + [d2g_dintst_dcnt.T, d2g_dcnt2, d2g_dcnt_dsqrtP ], + [d2g_dintst_dsqrtP.T, d2g_dcnt_dsqrtP.T, d2g_dsqrtP2 ], + ]) + if self.parameterization=='cholesky': + d2g_dintst_dsqrtP = (dloss_dfit*d2g_dintst_dsqrtP).sum(axis=0) + d2g_dcnt_dsqrtP = (dloss_dfit*d2g_dcnt_dsqrtP).sum(axis=0) + d2g_dsqrtP2 = (dloss_dfit*d2g_dsqrtP2).sum(axis=0) + d2g = np.block([ [d2g_dintst2, d2g_dintst_dcnt, d2g_dintst_dsqrtP], [d2g_dintst_dcnt.T, d2g_dcnt2, d2g_dcnt_dsqrtP ], @@ -1309,7 +1307,7 @@ class PeakHistogram(object): self.fit_params = None # - self.detector_mask = detector_mask + self.detector_mask = detector_mask.astype(bool) # peak model self.peak_fun = Gaussian(ndims=self.ndims, parameterization=parameterization) @@ -1552,35 +1550,69 @@ class PeakHistogram(object): return result.x - def initialize(self, points, data): + def initialize(self, points, data, ellipsoid=True): # basic statistics data_min, data_max, data_mean = data.min(), data.max(), data.mean() ################################### - # first initialization for the peak center + # first estimate of the peak center cnt_init = np.array([(lim[0]+lim[1])/2 for lim in self.limits]).reshape((1,-1)) ################################### # find threshold intensity that gives largets radius reduction of the enclosing sphere - nthres = 20 - thresh_rads = np.zeros((nthres,)) - thresh_vals = np.linspace(0, data_max, nthres-1, endpoint=False) - for i,thres in enumerate(thresh_vals): - thresh_data = data - thres - thresh_rads[i] = np.linalg.norm(points[thresh_data>0,...]-cnt_init,ord=2,axis=1).max() - thresh_rads[-1] = 0 - thresh_ind = np.argmax(np.abs(np.diff(thresh_rads)))+1 - thresh_val = thresh_vals[thresh_ind] - thresh_rad = thresh_rads[thresh_ind] - # mean intensity outside threshold sphere - bkgr_mask = np.linalg.norm(points-cnt_init,ord=2,axis=1)>thresh_rad - bkgr_mean = data[bkgr_mask].mean() + a = data_min + b = data_max + for _ in range(4): + nthres = 10 + thresh_rads = np.zeros((nthres,)) + thresh_vals = np.linspace(a, b, nthres-1, endpoint=True) + for i,thres in enumerate(thresh_vals): + thresh_data = data - thres + thresh_rads[i] = np.linalg.norm(points[thresh_data>=0,...]-cnt_init,ord=2,axis=1).max() + thresh_ind = np.argmax(np.abs(np.diff(thresh_rads)))+1 + thresh_val = thresh_vals[thresh_ind] + thresh_rad = thresh_rads[thresh_ind] + a = thresh_vals[max(0,thresh_ind-1)] + b = thresh_vals[min(nthres-1,thresh_ind+1)] + + thresh_add = 0.1 * (data_max - thresh_val) ################################### + # estimate covariance matrix by fitting maximum enclosing ellipsoid + + if ellipsoid: + # bin centers inside the peak enclosing sphere + ellpoints = points[data>(thresh_val+thresh_add),...] + + # replace centers of bins with corners + cnt2corner = 0.5 * np.sqrt(0.5) * self.resolution + ellpoints = np.vstack(( + ellpoints + cnt2corner * np.array([1,1,1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([-1,1,1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([1,-1,1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([1,1,-1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([-1,-1,1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([-1,1,-1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([1,-1,-1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([-1,-1,-1]).reshape((1,3)) + )) + ellcnt, ellrad, ellrot = MinVolEllipsoid(ellpoints, tolerance=0.01, maxit=10) + else: + ellcnt = cnt_init.ravel() + ellrad = np.array([thresh_rad]*self.ndims) + ellrot = np.eye(self.ndims) + + + ################################### + + # mean intensity outside threshold sphere + # bkgr_mean = data[np.linalg.norm(points-cnt_init,ord=2,axis=1)>thresh_rad].mean() + bkgr_mean = data[data<thresh_val].mean() + # initialization and bounds for the background bkgr_init = [ np.sqrt(1.0*bkgr_mean)] #+ [0]*self.ndims bkgr_lbnd = [-np.sqrt(1.1*bkgr_mean)] #+ [0]*self.ndims @@ -1588,58 +1620,62 @@ class PeakHistogram(object): ################################### - # filter background from data - nobkgr_data = data - (thresh_val + 0.0 * (data_max - thresh_val)) - bkgr_mask = nobkgr_data<0 - peak_mask = ~bkgr_mask - nobkgr_data[bkgr_mask] = 0 - ellmask = laplace(peak_mask,mode='reflect')!=0 - - print(ellmask.sum(), peak_mask.sum()) - - # fit maximum enclosing ellipsoid - ellpoints = points[ellmask,...] - ellcnt, ellrad, ellrot = fitMinVolEllipsoid(ellpoints, tolerance=0.01, maxit=10) - - # if _debug: - # _, _, edges = self.get_grid_data(return_edges=True) - - # data_1d = marginalize_1d(data.reshape(self.shape), normalize=False) - # data_2d = marginalize_2d(data.reshape(self.shape), normalize=False) - # for i in range(self.ndims): - # plt.stairs(data_1d[i], edges=edges[i], fill=True, alpha=0.5) - # plt.gca().set_box_aspect(1) - # plt.savefig(f'{_debug_dir}/peak_1d_{i}.png', bbox_inches='tight') - # plt.clf() - # for i in range(self.ndims): - # yind, xind = [j for j in range(self.ndims) if j!=i] - # left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - # plt.imshow(data_2d[i]/(data_2d[i]!=0), interpolation='none', extent=(left,right,bottom,top), origin='lower') - # plt.savefig(f'{_debug_dir}/peak_2d_{i}.png', bbox_inches='tight') - # plt.clf() - - # nobkgr_data_1d = marginalize_1d(nobkgr_data.reshape(self.shape), normalize=False) - # nobkgr_data_2d = marginalize_2d(nobkgr_data.reshape(self.shape), normalize=False) - # for i in range(self.ndims): - # plt.stairs(nobkgr_data_1d[i], edges=edges[i], fill=True, alpha=0.5) - # plt.gca().set_box_aspect(1) - # plt.savefig(f'{_debug_dir}/no_bkgr_peak_1d_{i}.png', bbox_inches='tight') - # plt.clf() - # for i in range(self.ndims): - # yind, xind = [j for j in range(self.ndims) if j!=i] - # left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - # plt.imshow(nobkgr_data_2d[i]/(nobkgr_data_2d[i]!=0), interpolation='none', extent=(left,right,bottom,top), origin='lower') - # plt.savefig(f'{_debug_dir}/no_bkgr_peak_2d_{i}.png', bbox_inches='tight') - # plt.clf() - # # exit() + if _debug: + _, _, edges = self.get_grid_data(return_edges=True) + + # full 3d covariance + cov_3d = ellrot.T @ (ellrad[:,np.newaxis]**2*ellrot) + + # covariances and ellipsoids of 2d marginals + cov_2d = [] + angle_2d = [] + sigma_2d = [] + for i in range(self.ndims): + yind, xind = [j for j in range(self.ndims) if j!=i] + cov_2d.append( cov_3d[np.ix_([xind,yind],[xind,yind])] ) + roti,eigi,_ = svd(cov_2d[-1]) + angle_2d.append( np.arctan2(roti[1,0],roti[0,0])/np.pi*180 ) + sigma_2d.append( np.sqrt(eigi) ) + + nobkgr_data = data - (thresh_val+thresh_add) + nobkgr_data[nobkgr_data<0] = 0 + + full_data = np.zeros(self.shape) + full_nobkgr_data = np.zeros(self.shape) + full_data[self.detector_mask] = data + full_nobkgr_data[self.detector_mask] = nobkgr_data + + data_1d = marginalize_1d(full_data, normalize=False) + data_2d = marginalize_2d(full_data, normalize=False) + for i in range(self.ndims): + plt.stairs(data_1d[i], edges=edges[i], fill=True, alpha=0.5) + plt.gca().set_box_aspect(1) + plt.savefig(f'{_debug_dir}/peak_1d_{i}.png', bbox_inches='tight') + plt.clf() + for i in range(self.ndims): + yind, xind = [j for j in range(self.ndims) if j!=i] + left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + plt.imshow(data_2d[i]/(data_2d[i]!=0), interpolation='none', extent=(left,right,bottom,top), origin='lower') + plt.gca().add_patch(Ellipse((ellcnt[xind],ellcnt[yind]), 2*sigma_2d[i][0], 2*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) + plt.savefig(f'{_debug_dir}/peak_2d_{i}.png', bbox_inches='tight') + plt.clf() + + nobkgr_data_1d = marginalize_1d(full_nobkgr_data.reshape(self.shape), normalize=False) + nobkgr_data_2d = marginalize_2d(full_nobkgr_data.reshape(self.shape), normalize=False) + for i in range(self.ndims): + plt.stairs(nobkgr_data_1d[i], edges=edges[i], fill=True, alpha=0.5) + plt.gca().set_box_aspect(1) + plt.savefig(f'{_debug_dir}/no_bkgr_peak_1d_{i}.png', bbox_inches='tight') + plt.clf() + for i in range(self.ndims): + yind, xind = [j for j in range(self.ndims) if j!=i] + left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] + plt.imshow(nobkgr_data_2d[i]/(nobkgr_data_2d[i]!=0), interpolation='none', extent=(left,right,bottom,top), origin='lower') + plt.gca().add_patch(Ellipse((ellcnt[xind],ellcnt[yind]), 2*sigma_2d[i][0], 2*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) + plt.savefig(f'{_debug_dir}/no_bkgr_peak_2d_{i}.png', bbox_inches='tight') + plt.clf() - # print( ellrot.T@np.diag(ellrad**2)@ellrot@np.array([1,0,0]).reshape((-1,1)) ) - - # print(mahalanobis_distance(ellcnt,np.diag(1/ellrad)@ellrot, (ellrot.T@np.diag(1/ellrad**2)@ellrot@np.array([1,0,0]).reshape((-1,1))).reshape((1,-1)) ) ) - - # exit() - ################################### # initialization and bounds for the max peak intensity intst_init = [ np.sqrt(data_max-bkgr_init[0]**2)] @@ -1647,14 +1683,16 @@ class PeakHistogram(object): intst_ubnd = [ np.sqrt(data_max)] # refined initialization and bounds for the peak center - cnt_init = [(lim[0]+lim[1])/2 for lim in self.limits] + # cnt_init = [(lim[0]+lim[1])/2 for lim in self.limits] + cnt_init = [c for c in ellcnt] cnt_lbnd = [c-rad/3 for c,rad in zip(cnt_init,self.radiuses)] cnt_ubnd = [c+rad/3 for c,rad in zip(cnt_init,self.radiuses)] # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix peak_std = 4 + # scale ellipsoid to 1 std value + ini_rads = ellrad / np.sqrt(2*np.log((data_max-thresh_val-thresh_add)/thresh_add)) # ini_rads = [ 1/4*rad/peak_std for rad in self.radiuses] # initial 'peak_std' radius is 1/2 of the box radius - ini_rads = ellrad / np.sqrt(peak_std) max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size @@ -1664,20 +1702,24 @@ class PeakHistogram(object): num_angles = (self.ndims*(self.ndims-1))//2 # initial rotation angles of the ellipsoid # ini_angles = [0]*num_angles + # extract angles from the ellipsoid rotation matrix ini_angles = [np.arctan2(ellrot[2,1],ellrot[2,2]), np.arctan2(ellrot[2,0],np.sqrt(ellrot[2,1]**2+ellrot[2,2]**2)), np.arctan2(ellrot[1,0],ellrot[0,0])] - prec_init = ini_angles + [ 1/r for r in ini_rads] + # initialize precision matrix + prec_init = ini_angles + [ 1/r for r in ini_rads] prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] elif self.parameterization=='cholesky': # upper triangular part of the Cholesky factor of the precision matrix num_chol = (self.ndims*(self.ndims+1))//2 - prec_init = list(np.sqrt(np.diag(1/ini_rads))[np.triu_indices(self.ndims)]) + prec_init = np.linalg.cholesky( ellrot.T @ np.diag(1/ini_rads**2) @ ellrot ).T + prec_init[np.diag_indices(self.ndims)] = np.sqrt(prec_init[np.diag_indices(self.ndims)]) + prec_init = list(prec_init[np.triu_indices(self.ndims)]) prec_lbnd = [-1000]*num_chol prec_ubnd = [ 1000]*num_chol elif self.parameterization=='full': # arbitrary square root of the precision matrix num_full = self.ndims**2 - prec_init = list((ellrot.T@((1/ini_rads)[:,np.newaxis]*ellrot)).ravel()) + prec_init = list( (np.diag(1/ini_rads) @ ellrot).ravel() ) prec_lbnd = [-1000]*num_full prec_ubnd = [ 1000]*num_full @@ -2060,9 +2102,9 @@ class PeakHistogram(object): loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun) result = minimize(loss_fun, jac = loss_fun.gradient, - hess = loss_fun.hessian, + hess = None, #loss_fun.hessian, x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), - method=solver, options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':_debug} + method=solver, options={'maxiter':100, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':_debug} # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} # method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True} @@ -2109,7 +2151,7 @@ class PeakHistogram(object): print(f' Final params: {result.x}') start = time.time() - dg = loss_fun.gradient(self.fit_params) + dg = loss_fun.gradient(self.fit_params) dg_time = time.time()-start start = time.time() @@ -2135,12 +2177,12 @@ class PeakHistogram(object): print('\nHessian') print('-------') - # print(f'Exact: \n{d2g}') - # print(f'FD: \n{fdd2g}') + print(f'Exact: \n{d2g}') + print(f'FD: \n{fdd2g}') # print(f'Exact: \n{d2g[:4,:4]}') # print(f'FD: \n{fdd2g[:4,:4]}') - print(f'Exact: \n{d2g[4:,4:]}') - print(f'FD: \n{fdd2g[4:,4:]}') + # print(f'Exact: \n{d2g[4:,4:]}') + # print(f'FD: \n{fdd2g[4:,4:]}') print(f'Max. diff: {np.abs(d2g-fdd2g).max():.3e}') print(f'Rel. diff: {np.abs((d2g-fdd2g)/(d2g+1.e-10)).max():.3e}') print(f'Exact time: {d2g_time:.3f} sec') @@ -2157,22 +2199,39 @@ class PeakHistogram(object): # create output directory for plots Path(plot_path).mkdir(parents=True, exist_ok=True) - # fit model to data + ####################################################################### + # plot options + + # normalize plots + normalize = False + + # H,K,L sort x axis + sortx = True + + def axes_order(i): + if sortx: + if i==0: return (1,0) + if i==1: return (2,1) + if i==2: return (0,2) + else: + if i==0: return (1,2) + if i==1: return (0,2) + if i==2: return (0,1) + + # ax_order = lambda i: [j for j in range(3) if j!=i] + + ####################################################################### + data, points, edges = self.get_grid_data(return_edges=True) # point along each dimension dim_points = [points[:,0,0,0],points[0,:,0,1],points[0,0,:,2]] - # parameters of the model - nbkgr = 1 #+ self.ndims - - ####################################################################### # evaluate inital model # parameters - ini_bkgr_params = self.init_params[:nbkgr] ini_peak_params = self.init_params[nbkgr:] # peak center and full covariance @@ -2185,12 +2244,13 @@ class PeakHistogram(object): ini_angle_2d = [] ini_sigma_2d = [] for i in range(self.ndims): - yind, xind = [j for j in range(self.ndims) if j!=i] + yind, xind = axes_order(i) ini_cov_2d.append( ini_cov_3d[np.ix_([xind,yind],[xind,yind])] ) roti,eigi,_ = svd(ini_cov_2d[-1]) # eigi, roti = eig(cov_2d[-1]) # eigi, roti = eigen(cov_2d[-1]) - ini_angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) + # ini_angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) + ini_angle_2d.append( np.arctan2(roti[1,0],roti[0,0])/np.pi*180 ) ini_sigma_2d.append( np.sqrt(eigi) ) # fitted model @@ -2206,8 +2266,8 @@ class PeakHistogram(object): # evaluate final model # final parameters - bkgr_params = self.fit_params[:nbkgr] - peak_params = self.fit_params[nbkgr:] + bkgr_params = self.fit_params[:self.bkgr_fun.nparams] + peak_params = self.fit_params[self.bkgr_fun.nparams:] # inverse rotation matrix # R,_,_ = rotation_matrix(angles) @@ -2223,12 +2283,12 @@ class PeakHistogram(object): angle_2d = [] sigma_2d = [] for i in range(self.ndims): - yind, xind = [j for j in range(self.ndims) if j!=i] + yind, xind = axes_order(i) cov_2d.append( cov_3d[np.ix_([xind,yind],[xind,yind])] ) roti,eigi,_ = svd(cov_2d[-1]) # eigi, roti = eig(cov_2d[-1]) # eigi, roti = eigen(cov_2d[-1]) - angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) + angle_2d.append( np.arctan2(roti[1,0],roti[0,0])/np.pi*180 ) sigma_2d.append( np.sqrt(eigi) ) # fitted model @@ -2247,9 +2307,6 @@ class PeakHistogram(object): subfigs = fig.subfigures(7,1) #wspace=0.07 subfig_no=-1 - # normalize plots - normalize = False - ######################################## # plot 1d marginals @@ -2289,13 +2346,13 @@ class PeakHistogram(object): ######################################## # plot 2d marginals - data_2d = marginalize_2d(data, normalize=normalize) - fit_2d = marginalize_2d(fit, normalize=normalize) - ini_fit_2d = marginalize_2d(ini_fit, normalize=normalize) - fit_masked_2d = marginalize_2d(fit_masked, normalize=normalize) - rebinned_data_2d = marginalize_2d(rebinned_data, normalize=normalize, bin_lengths=bins, recover_shape=True) - rebinned_fit_2d = marginalize_2d(rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True) - ini_rebinned_fit_2d = marginalize_2d(ini_rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True) + data_2d = marginalize_2d(data, normalize=normalize, sortx=sortx) + fit_2d = marginalize_2d(fit, normalize=normalize, sortx=sortx) + ini_fit_2d = marginalize_2d(ini_fit, normalize=normalize, sortx=sortx) + fit_masked_2d = marginalize_2d(fit_masked, normalize=normalize, sortx=sortx) + rebinned_data_2d = marginalize_2d(rebinned_data, normalize=normalize, bin_lengths=bins, recover_shape=True, sortx=sortx) + rebinned_fit_2d = marginalize_2d(rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True, sortx=sortx) + ini_rebinned_fit_2d = marginalize_2d(ini_rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True, sortx=sortx) # show zero pixels as None data_2d = [d/(d!=0) for d in data_2d] @@ -2315,25 +2372,17 @@ class PeakHistogram(object): ini_rebinned_fit_2d = np.log(ini_rebinned_fit_2d) - def axes_order(i): - if i==0: return (1,0) - if i==1: return (2,1) - if i==2: return (0,2) - # ax_order = lambda i: [j for j in range(3) if j!=i] - - ######################################## - vmind = min([im.min() for im in data_2d]) - vmaxd = max([im.max() for im in data_2d]) - vminf = min([im.min() for im in fit_2d]) - vmaxf = max([im.max() for im in fit_2d]) - vmin = min([vmind, vminf]) - vmax = min([vmaxd, vmaxf]) + vmind = min([np.nanmin(im) for im in data_2d]) + vmaxd = max([np.nanmax(im) for im in data_2d]) + vminf = min([np.nanmin(im) for im in fit_2d]) + vmaxf = max([np.nanmax(im) for im in fit_2d]) + vmin = min([vmind, vminf]) + vmax = max([vmaxd, vmaxf]) # original data subfig_no+=1 for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): - # yind, xind = [j for j in range(3) if j!=i] yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) @@ -2350,7 +2399,6 @@ class PeakHistogram(object): # gaussian fit subfig_no+=1 for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): - # yind, xind = [j for j in range(3) if j!=i] yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) @@ -2366,17 +2414,16 @@ class PeakHistogram(object): ######################################## - vmind = min([im.min() for im in rebinned_data_2d]) - vmaxd = max([im.max() for im in rebinned_data_2d]) - vminf = min([im.min() for im in rebinned_fit_2d]) - vmaxf = max([im.max() for im in rebinned_fit_2d]) - vmin = min([vmind, vminf]) - vmax = min([vmaxd, vmaxf]) + vmind = min([np.nanmin(im) for im in rebinned_data_2d]) + vmaxd = max([np.nanmax(im) for im in rebinned_data_2d]) + vminf = min([np.nanmin(im) for im in rebinned_fit_2d]) + vmaxf = max([np.nanmax(im) for im in rebinned_fit_2d]) + vmin = min([vmind, vminf]) + vmax = max([vmaxd, vmaxf]) # rebinned data subfig_no+=1 for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): - # yind, xind = [j for j in range(3) if j!=i] yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(rebinned_data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) @@ -2392,7 +2439,6 @@ class PeakHistogram(object): # fit to rebinned data subfig_no+=1 for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): - # yind, xind = [j for j in range(3) if j!=i] yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(rebinned_fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) @@ -2426,10 +2472,9 @@ class PeakHistogram(object): # plot difference subfig_no+=1 - vmin = min([np.abs(fit_masked_2d[i]-data_2d[i]).min() for i in range(3) ]) - vmax = max([np.abs(fit_masked_2d[i]-data_2d[i]).max() for i in range(3) ]) + vmin = min([np.nanmin(np.abs(fit_masked_2d[i]-data_2d[i])) for i in range(3) ]) + vmax = max([np.nanmax(np.abs(fit_masked_2d[i]-data_2d[i])) for i in range(3) ]) for i,ax in enumerate(subfigs[subfig_no].subplots(1,3)): - # yind, xind = [j for j in range(3) if j!=i] yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(np.abs(fit_masked_2d[i]-data_2d[i]), interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) -- GitLab From d1832716b3ad44a8f9cfcd9e4734318c04ed0963 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Mon, 26 Dec 2022 19:12:21 -0500 Subject: [PATCH 04/26] minor fixes --- peak_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/peak_integration.py b/peak_integration.py index 97da56f..d503bda 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -2232,7 +2232,8 @@ class PeakHistogram(object): # evaluate inital model # parameters - ini_peak_params = self.init_params[nbkgr:] + ini_bkgr_params = self.init_params[:self.bkgr_fun.nparams] + ini_peak_params = self.init_params[self.bkgr_fun.nparams:] # peak center and full covariance # cov_3d = R.T @ np.diag(sqrt_eig**2) @ R -- GitLab From 69d529659ff58c362953a8d26c9437e704cc0e2b Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Mon, 26 Dec 2022 19:26:38 -0500 Subject: [PATCH 05/26] minor fixes --- peak_integration.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index d503bda..ed4af3d 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -2505,13 +2505,6 @@ class PeakHistogram(object): plt.savefig(f'{plot_path}/peak.png') else: plt.savefig(f'{plot_path}/{prefix}_peak.png') - - # # save - # if prefix is None: - # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}.png') - # else: - # plt.savefig(f'{plot_path}/{prefix}_peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}.png') - plt.close('all') -- GitLab From 0274e1d2c2f50784fec0c421e4ed0cb3c5ea73a6 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Tue, 27 Dec 2022 11:00:23 -0500 Subject: [PATCH 06/26] remove old initialization --- peak_integration.py | 201 -------------------------------------------- 1 file changed, 201 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index ed4af3d..210be30 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1349,207 +1349,6 @@ class PeakHistogram(object): return data, points - def initialize1(self, bins=None, loss='mle', covariance_parameterization='givens'): - ########################################################################### - # rebin data - - if isinstance(bins,str): - if bins=='knuth': - bins = knuth_bins(self.data, min_bins=4, spread=1) - # bins = knuth_bins(data, min_bins=4, max_bins=4, spread=0) - elif bins=='adaptive_knuth': - # rebin data using number of bins given by `knuth` algorithm but such that bins have comparable probability masses - bins = knuth_bins(self.data, min_bins=4, spread=1) - - # 1d marginals - marginal_data = marginalize_1d(self.data, normalize=False) - - # quantiles, note len(b)+2 to make odd number of bins - quant = [ np.linspace(0,1,min(len(b)+2,self.shape[i])) for i,b in enumerate(bins) ] - edges = [ np.quantile( np.repeat(np.arange(1,md.size+1), md.astype(int)), q[1:], method='inverted_cdf' ) for md,q in zip(marginal_data,quant) ] - bins = [ np.diff(e,prepend=0).astype(int) for e in edges ] - - if _debug: - plt.figure(constrained_layout=True, figsize=(10,4)) - for i in range(self.ndims): - plt.subplot(1,self.ndims,i+1) - plt.hlines(np.linspace(0,marginal_data[i].sum(),len(bins[i])+1), 0, marginal_data[i].size) - plt.vlines(edges[i],0,marginal_data[i].sum()) - plt.plot(marginal_data[i].cumsum(), '.', c='red') - plt.gca().set_box_aspect(1) - plt.savefig(_debug_dir+'/adaptive_knuth_quantiles.png') - elif isinstance(bins,int): - nbins = bins - bins = [split_bins([s],nbins,recursive=False) for s in self.shape] - elif bins is None: - bins = [[1]*s for s in self.shape] - - # rebinned data - fit_data, fit_points = self.get_grid_data(bins=bins, rebin_mode='density') - fit_points = fit_points.reshape((-1,self.ndims)) - - data_min, data_max, data_mean = fit_data.min(), fit_data.max(), fit_data.mean() - fit_data -= data_mean + 0.5 * (data_max - data_mean) - fit_data[fit_data<0] = 0 - fit_data = fit_data.ravel() - - # detector_mask = None - # if self.detector_mask is None: - # detector_mask = fit_data==fit_data - # if self.detector_mask is not None: - # detector_mask = rebin_histogram(self.detector_mask.astype(int), bins)>0 - # fit_data = fit_data.ravel()[detector_mask.ravel()] - # fit_points = fit_points[detector_mask.ravel(),:] - - ########################################################################### - # initialization and bounds on parameters - - # self.initialize(bins) - - ################################### - # # initialization and bounds for the background intensity - # bkgr_init = [ np.sqrt(0.9*data_mean)] #+ [0]*self.ndims - # bkgr_lbnd = [-np.sqrt(1.1*data_mean)] #+ [0]*self.ndims - # bkgr_ubnd = [ np.sqrt(1.1*data_mean)] #+ [0]*self.ndims - - ################################### - # initialization and bounds for the max peak intensity - intst_init = [ np.sqrt(data_max-data_mean)] - intst_lbnd = [-np.sqrt(data_max)] - intst_ubnd = [ np.sqrt(data_max)] - - ################################### - # cnt_1d, std_1d = initial_parameters(hist_ws, bins) - # params_init1 = initial_parameters(hist_ws, bins, detector_mask) - - # initialization and bounds for the peak center - # dcnt = [ rad/3 for rad in self.radiuses] - # cnt_init = cnt_1d - cnt_init = [(lim[0]+lim[1])/2 for lim in self.limits] - cnt_lbnd = [c-rad/3 for c,rad in zip(cnt_init,self.radiuses)] - cnt_ubnd = [c+rad/3 for c,rad in zip(cnt_init,self.radiuses)] - - # initialization and bounds for the precision matrix - if covariance_parameterization=='givens': - num_angles = (self.ndims*(self.ndims-1))//2 - # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix - peak_std = 4 - ini_rads = [ 1/4*rad/peak_std for rad in self.radiuses] # initial 'peak_std' radius is 1/2 of the box radius - max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius - min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size - # `num_angles` angles and `ndims` square roots of the eigenvalues of the precision matrix - # prec_init = [ 0]*num_angles + std_1d - prec_init = [ 0]*num_angles + [ 1/r for r in ini_rads] - prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] - prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] - elif covariance_parameterization=='cholesky': - num_chol = (self.ndims*(self.ndims+1))//2 - # upper triangular part of the Cholesky factor of the precision matrix - prec_init = list(np.eye(self.ndims)[np.triu_indices(self.ndims)]) - prec_lbnd = [-1000]*num_chol - prec_ubnd = [ 1000]*num_chol - elif covariance_parameterization=='full': - # arbitrary square root of the precision matrix - prec_init = list(np.eye(self.ndims).ravel()) - prec_lbnd = [-1000]*(self.ndims**2) - prec_ubnd = [ 1000]*(self.ndims**2) - - # # initialization and bounds for the skewness - # skew_init = [0]*self.ndims - # skew_lbnd = [0]*self.ndims - # skew_ubnd = [0]*self.ndims - - ################################### - # initialization and bounds for all parameters - # params_init = params_init1 - params_init = intst_init + cnt_init + prec_init #+ skew_init - params_lbnd = intst_lbnd + cnt_lbnd + prec_lbnd #+ skew_lbnd - params_ubnd = intst_ubnd + cnt_ubnd + prec_ubnd #+ skew_ubnd - - # # number of background and peak parameters - # nbkgr = 1 #len(bkgr_init) - # npeak = len(params_init) - nbkgr - - ########################################################################### - - # residual to fit densities of the bins in the rebinned histogram - if loss=='pearson_chi': - def residual(params): - fit = params[0]**2 - fit = fit + gaussian_mixture(params[1:],points,npeaks=1,covariance_parameterization=covariance_parameterization).reshape(data.shape) - res = fit[nnz_mask] - nnz_data - return (res/np.sqrt(fit)).ravel() - result = least_squares(residual, #jac=jacobian_residual, - x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) - elif loss=='neumann_chi': - def residual(params): - fit = params[0]**2 - fit = fit + gaussian_mixture(params[1:],points,npeaks=1,covariance_parameterization=covariance_parameterization).reshape(data.shape) - res = fit[nnz_mask] - nnz_data - return (res/np.sqrt(nnz_data)).ravel() - # return (res/np.maximum(1,np.sqrt(nnz_data))).ravel() - # return (res/np.sqrt(data[nnz_mask].size)).ravel() - result = least_squares(residual, #jac=jacobian_residual, - x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) - elif loss=='mle': - gaussian_peak = Gaussian(ndims=3, covariance_parameterization=covariance_parameterization) - class MLELoss(object): - def __init__(self): - self.func_calls = 0 - self.grad_calls = 0 - self.hess_calls = 0 - - def __call__(self, params): - self.func_calls+=1 - self.func_params = np.asarray(params).copy() - - # fit - self.fit = 0.01 + gaussian_peak(params, fit_points) - - return (self.fit-fit_data*np.log(self.fit)).sum() - - def gradient(self, params, *args, **kwargs): - self.grad_calls += 1 - self.grad_params = np.asarray(params).copy() - if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): g = self.__call__(params) - - self.dloss_dfit = 1 - fit_data/self.fit - self.dloss_dpeak = gaussian_peak.gradient(params, fit_points, dloss_dfit=self.dloss_dfit) - - return self.dloss_dpeak - - def hessian(self, params, *args, **kwargs): - self.hess_calls += 1 - self.hess_params = np.asarray(params).copy() - if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): g = self.__call__(params) - if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): dg = self.gradient(params) - - d2loss_dfit2 = fit_data/self.fit**2 - d2loss = gaussian_peak.hessian(params,fit_points,dloss_dfit=self.dloss_dfit,d2loss_dfit2=d2loss_dfit2) - - return d2loss - peak_loss = MLELoss() - result = minimize(peak_loss, - jac = peak_loss.gradient, - hess = peak_loss.hessian, - x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), - # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} - # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} - method='Newton-CG', options={'maxiter':10, 'xtol':1.e-6, 'disp':True} - ) - - - print(params_init) - print(result.x) - center, evecs, radii, v = ellipsoid_fit(fit_points[fit_data>0,:]) - print(center) - print(evecs) - print(radii, 1/params_init[7],1/params_init[8],1/params_init[9]) - print(v) - exit() - return result.x - - def initialize(self, points, data, ellipsoid=True): # basic statistics data_min, data_max, data_mean = data.min(), data.max(), data.mean() -- GitLab From 869ac61f7c429aa90fc9315e15d665d765db2c1a Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 12:32:37 -0500 Subject: [PATCH 07/26] change description of `def fit` --- peak_integration.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 210be30..ae98070 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1744,19 +1744,22 @@ class PeakHistogram(object): return output - def fit(self, bins=None, return_bins=False, loss='mle', solver='BFGS', params_init=None, params_lbnd=None, params_ubnd=None): + def fit(self, bins=None, return_bins=False, loss='mle', solver='BFGS', tol=1.e-6, params_init=None, params_lbnd=None, params_ubnd=None): ''' - Inputs + Inputs, all optional ------ - hist_ws: histogram workspace - bins: rebinning algorithm, one of [None, 'knuth', 'adaptive_knuth', int, list] - loss: fitting criterion, one of ['pearson_chi', 'neumann_chi', 'mle'] - cnt: initial estimate for the peak center - dcnt: bounds for the location of the peak center + bins: rebinning algorithm, one of [None, 'knuth', 'adaptive_knuth', int, list of int] + return_bins: whether return bins or not + loss: fitting criterion, one of ['mle', 'pearson_chi', 'neumann_chi'] + solver: minimizer, one of ['BFGS', 'L-BFGS-B', 'Newton-CG'] + tol: tolerance of the solution + params_*: initialization and bound on the parameters Outputs ------- - (1+(1+ndims+ndims**2)*npeaks,) ndarray of parameters + fit_params: optimal parameters + success: convergence flag + bins: optional, returned bins ''' ########################################################################### -- GitLab From 75a4b336f1aea76cb43c51bbed3e981ab90e2c58 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 12:34:54 -0500 Subject: [PATCH 08/26] change mask rebinning in `def fit` --- peak_integration.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index ae98070..80a953f 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1802,12 +1802,14 @@ class PeakHistogram(object): fit_points = fit_points.reshape((-1,self.ndims)) # self.detector_mask = None - # if self.detector_mask is None: - # detector_mask = fit_data==fit_data if self.detector_mask is not None: - detector_mask = rebin_histogram(self.detector_mask.astype(int), bins)>0 - fit_data = fit_data[detector_mask.ravel()] - fit_points = fit_points[detector_mask.ravel(),:] + if np.any(np.array([len(b) for b in bins])!=self.shape): + # all subbins must be in detector mask, 0.99 to account for numerical error + mask = rebin_histogram(self.detector_mask.astype(float), bins, mode='density') > 0.99 + else: + mask = self.detector_mask + fit_data = fit_data[mask.ravel()] + fit_points = fit_points[mask.ravel(),:] ########################################################################### # initialization and bounds on parameters -- GitLab From bdfe43b293e1cea00303ab5dad5d25b29600983f Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 13:44:23 -0500 Subject: [PATCH 09/26] add loss class --- peak_integration.py | 151 +++++++++++++++++++++++++++----------------- 1 file changed, 93 insertions(+), 58 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 80a953f..a13688f 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1262,6 +1262,83 @@ def numerical_hessian(x, fun, *args, **kwargs): ############################################################################### ############################################################################### +# Loss functions + +class Loss(object): + def __init__(self, bkgr_fun, peak_fun, points, data): + self.func_calls = 0 + self.grad_calls = 0 + self.hess_calls = 0 + + # background and peak functions + self.bkgr_fun = bkgr_fun + self.peak_fun = peak_fun + + # number of background and peak parameters + self.nbkgr = self.bkgr_fun.nparams + self.npeak = self.peak_fun.nparams + + # points and data to fit + self.points = points + self.data = data + + def fit(self, params): + self._fit = self.bkgr_fun(params[:self.nbkgr], self.points) + self.peak_fun(params[self.nbkgr:], self.points) + return self._fit + + def __call__(self, params, *args, **kwargs): + self.func_calls+=1 + self.func_params = np.asarray(params).copy() + return self.loss(params) + + def gradient(self, params, *args, **kwargs): + self.grad_calls += 1 + self.grad_params = np.asarray(params).copy() + if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): g = self.__call__(params) + + dloss_dbkgr = self.bkgr_fun.gradient(params[:self.nbkgr], self.points, dloss_dfit=self.dloss_dfit) + dloss_dpeak = self.peak_fun.gradient(params[self.nbkgr:], self.points, dloss_dfit=self.dloss_dfit) + + return np.concatenate((dloss_dbkgr,dloss_dpeak)) + + def hessian(self, params, *args, **kwargs): + self.hess_calls += 1 + self.hess_params = np.asarray(params).copy() + if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): g = self.__call__(params) + if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): dg = self.gradient(params) + + d2loss_dfit2 = self.d2loss_dfit2 + d2loss_dbkgr2 = self.bkgr_fun.hessian(params[:self.nbkgr], self.points, dloss_dfit=self._dloss_dfit, d2loss_dfit2=d2loss_dfit2) + d2loss_dpeak2 = self.peak_fun.hessian(params[self.nbkgr:], self.points, dloss_dfit=self._dloss_dfit, d2loss_dfit2=d2loss_dfit2) + d2loss_dbkgr_dpeak = ((d2loss_dfit2.reshape((-1,1,1)) * self.bkgr_fun.grad[:,:,np.newaxis]) * self.peak_fun.grad[:,np.newaxis,:]).sum(axis=0) + + d2loss = np.block([ + [d2loss_dbkgr2, d2loss_dbkgr_dpeak], + [d2loss_dbkgr_dpeak.T, d2loss_dpeak2 ] + ]) + + return d2loss + + +class MLELoss(Loss): + def loss(self, params): + fit = self.fit(params) + return (fit-self.data*np.log(fit)).sum() + + @property + def dloss_dfit(self): + self._dloss_dfit = 1 - self.data / self._fit + return self._dloss_dfit + + @property + def d2loss_dfit2(self): + return self.data / self._fit**2 + + +############################################################################### +############################################################################### + + class PeakHistogram(object): @@ -1841,74 +1918,32 @@ class PeakHistogram(object): result = least_squares(residual, #jac=jacobian_residual, x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) elif loss=='mle': - class MLELoss(object): - def __init__(self, bkgr_fun, peak_fun): - self.func_calls = 0 - self.grad_calls = 0 - self.hess_calls = 0 - - # background and peak functions - self.bkgr_fun = bkgr_fun - self.peak_fun = peak_fun - - # number of background and peak parameters - self.nbkgr = self.bkgr_fun.nparams - self.npeak = self.peak_fun.nparams - - def __call__(self, params): - self.func_calls+=1 - self.func_params = np.asarray(params).copy() - - # cache fit for reuse in derivative evaluations - self.fit = self.bkgr_fun(params[:self.nbkgr], fit_points) + self.peak_fun(params[self.nbkgr:], fit_points) - - return (self.fit-fit_data*np.log(self.fit)).sum() - - def gradient(self, params, *args, **kwargs): - self.grad_calls += 1 - self.grad_params = np.asarray(params).copy() - if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): g = self.__call__(params) - - self.dloss_dfit = 1 - fit_data/self.fit - dloss_dbkgr = self.bkgr_fun.gradient(params[:self.nbkgr], fit_points, dloss_dfit=self.dloss_dfit) - dloss_dpeak = self.peak_fun.gradient(params[self.nbkgr:], fit_points, dloss_dfit=self.dloss_dfit) - - return np.concatenate((dloss_dbkgr,dloss_dpeak)) - - def hessian(self, params, *args, **kwargs): - self.hess_calls += 1 - self.hess_params = np.asarray(params).copy() - if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): g = self.__call__(params) - if not hasattr(self, 'grad_params') or not np.all(self.hess_params==self.grad_params): dg = self.gradient(params) - - d2loss_dfit2 = fit_data / self.fit**2 - d2loss_dbkgr2 = self.bkgr_fun.hessian(params[:self.nbkgr], fit_points, dloss_dfit=self.dloss_dfit, d2loss_dfit2=d2loss_dfit2) - d2loss_dpeak2 = self.peak_fun.hessian(params[self.nbkgr:], fit_points, dloss_dfit=self.dloss_dfit, d2loss_dfit2=d2loss_dfit2) - d2loss_dbkgr_dpeak = ((d2loss_dfit2.reshape((-1,1,1)) * self.bkgr_fun.grad[:,:,np.newaxis]) * self.peak_fun.grad[:,np.newaxis,:]).sum(axis=0) - - d2loss = np.block([ - [d2loss_dbkgr2, d2loss_dbkgr_dpeak], - [d2loss_dbkgr_dpeak.T, d2loss_dpeak2 ] - ]) - - return d2loss + loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun, points=fit_points, data=fit_data) # from scipy.optimize import BFGS # class myBFGS(BFGS): # def initialize(self, n, approx_type): # super().initialize(n, approx_type) # if self.approx_type == 'hess': - # self.B = MLELoss().hessian(params_init) - # # self.B = np.eye(n, dtype=float) + # self.B = loss_fun.hessian(params_init) # else: - # self.H = np.linalg.inv(MLELoss().hessian(params_init)) - # # self.H = np.eye(n, dtype=float) - loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun) + # self.H = np.linalg.inv(loss_fun.hessian(params_init)) + + # result = fmin_bfgs_2(loss_fun, params_init, fprime=loss_fun.gradient, gtol=1e-6, maxiter=1000, disp=2, init_hess=None)#(loss_fun.hessian(params_init)+loss_fun.hessian(params_init).T)/2) + # result = fmin_bfgs_2(loss_fun, result.x, fprime=loss_fun.gradient, gtol=1e-6, maxiter=1000, disp=2, init_hess=loss_fun.hessian(result.x)) + # result = minimize(loss_fun, + # jac = loss_fun.gradient, + # hess = loss_fun.hessian, + # x0=result.x, bounds=tuple(zip(params_lbnd,params_ubnd)), + # method='Newton-CG', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug} + # ) + result = minimize(loss_fun, jac = loss_fun.gradient, - hess = None, #loss_fun.hessian, + hess = loss_fun.hessian, + # hess = myBFGS(), #loss_fun.hessian, x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), - method=solver, options={'maxiter':100, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':_debug} + method=solver, options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug} # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} # method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True} -- GitLab From c5112f213b2afcde49313b3d3c860eb0de46a9ff Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 13:56:27 -0500 Subject: [PATCH 10/26] cleanup --- peak_integration.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index a13688f..62fd99c 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1972,17 +1972,6 @@ class PeakHistogram(object): self.init_params = np.array(params_init) self.fit_params = result.x - # fit_params[0] = fit_params[0] / np.sqrt(sc) - # fit_params[1] = fit_params[1] / np.sqrt(sc) - # return self.fit_params, True, bins - # print(result) - # print(params_init) - # print(result.success) - # print(result.x) - # print('Chi2: ',chi2(fit_params)) - - # print(np.linalg.eig(peak_loss.hessian(self.fit_params))[0]) - # print(np.linalg.eig(numerical_hessian(self.fit_params, lambda y: peak_loss(y)))[0]) if _debug: print(f'\nConverged: {result.success}') -- GitLab From 0b46e7dcfc1a6519aea24757f83b21971e3bf180 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 13:57:10 -0500 Subject: [PATCH 11/26] add minimize to loss class --- peak_integration.py | 97 +++++++++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 62fd99c..978017d 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -3,7 +3,7 @@ import time from pathlib import Path import numpy as np -from scipy.optimize import minimize +from scipy.optimize import minimize as scipy_minimize from scipy.linalg import svd from numpy.linalg import eig from scipy.special import loggamma, erf @@ -1334,6 +1334,32 @@ class MLELoss(Loss): def d2loss_dfit2(self): return self.data / self._fit**2 + def minimize(self, solver, tol, maxfun, params_init, params_lbnd, params_ubnd, disp=_debug): + # from scipy.optimize import BFGS + # class myBFGS(BFGS): + # def initialize(self, n, approx_type): + # super().initialize(n, approx_type) + # if self.approx_type == 'hess': + # self.B = loss_fun.hessian(params_init) + # else: + # self.H = np.linalg.inv(loss_fun.hessian(params_init)) + return scipy_minimize(self, + jac = self.gradient, + hess = self.hessian, + # hess = myBFGS(), + x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), + method=solver, options={'maxiter':1000, 'maxfun':maxfun, 'maxls':100, 'maxcor':100, 'ftol':1.e-10, 'gtol':tol, 'xtol':1.e-10, 'disp':disp} + # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} + # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} + # method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True} + # method='Newton-CG', options={'maxiter':1000, 'xtol':1.e-6, 'disp':True} + # method='Nelder-Mead', options={'maxiter':1000, 'disp':False} + # method='TNC', options={'scale':None, 'maxfun':1000, 'ftol':1.e-3, 'gtol':1.e-5, 'disp':True} + # method='dogleg', options={'maxiter':1000, 'tol':1.e-6, 'gtol':1.e-8, 'disp':True} + # method='trust-krylov', options={'maxiter':1000, 'tol':1.e-6, 'inexact':False, 'disp':True} + # method='trust-exact', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} + # method='trust-constr', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} + ) ############################################################################### ############################################################################### @@ -1919,15 +1945,12 @@ class PeakHistogram(object): x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) elif loss=='mle': loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun, points=fit_points, data=fit_data) - - # from scipy.optimize import BFGS - # class myBFGS(BFGS): - # def initialize(self, n, approx_type): - # super().initialize(n, approx_type) - # if self.approx_type == 'hess': - # self.B = loss_fun.hessian(params_init) - # else: - # self.H = np.linalg.inv(loss_fun.hessian(params_init)) + result = loss_fun.minimize(solver, tol, + maxfun=100, + params_init=params_init, + params_lbnd=params_lbnd, + params_ubnd=params_ubnd, + disp=True) # result = fmin_bfgs_2(loss_fun, params_init, fprime=loss_fun.gradient, gtol=1e-6, maxiter=1000, disp=2, init_hess=None)#(loss_fun.hessian(params_init)+loss_fun.hessian(params_init).T)/2) # result = fmin_bfgs_2(loss_fun, result.x, fprime=loss_fun.gradient, gtol=1e-6, maxiter=1000, disp=2, init_hess=loss_fun.hessian(result.x)) @@ -1938,39 +1961,27 @@ class PeakHistogram(object): # method='Newton-CG', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug} # ) - result = minimize(loss_fun, - jac = loss_fun.gradient, - hess = loss_fun.hessian, - # hess = myBFGS(), #loss_fun.hessian, - x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), - method=solver, options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug} - # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} - # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} - # method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True} - # method='Newton-CG', options={'maxiter':1000, 'xtol':1.e-6, 'disp':True} - # method='Nelder-Mead', options={'maxiter':1000, 'disp':False} - # method='TNC', options={'scale':None, 'maxfun':1000, 'ftol':1.e-3, 'gtol':1.e-5, 'disp':True} - # method='dogleg', options={'maxiter':1000, 'tol':1.e-6, 'gtol':1.e-8, 'disp':True} - # method='trust-krylov', options={'maxiter':1000, 'tol':1.e-6, 'inexact':False, 'disp':True} - # method='trust-exact', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} - # method='trust-constr', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} - ) - # # restrict parameters - # if solver!='L-BFGS-B': - # for i in range(len(result.x)): - # result.x[i] = max(min(result.x[i],params_ubnd[i]),params_lbnd[i]) - - # print(peak_loss.func_calls, peak_loss.grad_calls, peak_loss.hess_calls) - # g,dg = gaussian_mixture(result.x[nbkgr:],fit_points,npeaks=1,covariance_parameterization=covariance_parameterization,return_gradient=True) - # fit = result.x[0]**2 + g.reshape(fit_data.shape) - # res = (fit-fit_data) / fit - # grad = res.reshape((1,*fit_data.shape)) * dg.reshape((-1,*fit_data.shape)) - # grad = grad.reshape((grad.shape[0],-1)).sum(axis=1) - # grad = np.array( [2*result.x[0]*res.sum(),0,0,0] + list(grad) + [0]*3) - # print(result.jac) - # print(grad) - - self.init_params = np.array(params_init) + # result = minimize(loss_fun, + # jac = loss_fun.gradient, + # hess = loss_fun.hessian, + # # hess = myBFGS(), #loss_fun.hessian, + # x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), + # method=solver, options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug} + # # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} + # # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} + # # method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True} + # # method='Newton-CG', options={'maxiter':1000, 'xtol':1.e-6, 'disp':True} + # # method='Nelder-Mead', options={'maxiter':1000, 'disp':False} + # # method='TNC', options={'scale':None, 'maxfun':1000, 'ftol':1.e-3, 'gtol':1.e-5, 'disp':True} + # # method='dogleg', options={'maxiter':1000, 'tol':1.e-6, 'gtol':1.e-8, 'disp':True} + # # method='trust-krylov', options={'maxiter':1000, 'tol':1.e-6, 'inexact':False, 'disp':True} + # # method='trust-exact', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} + # # method='trust-constr', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} + # ) + + self.init_loss.append(loss_fun(params_init)) + self.init_params.append(np.array(params_init)) + self.fit_params = result.x if _debug: -- GitLab From 07d974c4d27cdd73ad41867104a752a09c4876bf Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 14:05:56 -0500 Subject: [PATCH 12/26] add solver dependent options to loss.minimize --- peak_integration.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 978017d..2c85951 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1334,7 +1334,7 @@ class MLELoss(Loss): def d2loss_dfit2(self): return self.data / self._fit**2 - def minimize(self, solver, tol, maxfun, params_init, params_lbnd, params_ubnd, disp=_debug): + def minimize(self, solver, tol, maxiter, maxfun, params_init, params_lbnd, params_ubnd, disp=_debug): # from scipy.optimize import BFGS # class myBFGS(BFGS): # def initialize(self, n, approx_type): @@ -1343,12 +1343,18 @@ class MLELoss(Loss): # self.B = loss_fun.hessian(params_init) # else: # self.H = np.linalg.inv(loss_fun.hessian(params_init)) + + if solver=='Newton-CG': + options = {'maxiter':maxiter, 'xtol':tol, 'disp':disp} + elif solver=='BFGS': + options = {'maxiter':maxiter, 'maxfun':maxfun, 'maxls':100, 'maxcor':100, 'ftol':1.e-10, 'gtol':tol, 'xtol':1.e-10, 'disp':disp} + return scipy_minimize(self, jac = self.gradient, hess = self.hessian, # hess = myBFGS(), x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), - method=solver, options={'maxiter':1000, 'maxfun':maxfun, 'maxls':100, 'maxcor':100, 'ftol':1.e-10, 'gtol':tol, 'xtol':1.e-10, 'disp':disp} + method=solver, options=options, # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} # method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True} -- GitLab From 69d42bab17a6c5ad4fc3b38e1e51bbc6ee75fd2b Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 14:06:40 -0500 Subject: [PATCH 13/26] minor fixes --- peak_integration.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 2c85951..a7cb893 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1951,8 +1951,7 @@ class PeakHistogram(object): x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) elif loss=='mle': loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun, points=fit_points, data=fit_data) - result = loss_fun.minimize(solver, tol, - maxfun=100, + result = loss_fun.minimize(solver, tol, maxiter=100, maxfun=100, params_init=params_init, params_lbnd=params_lbnd, params_ubnd=params_ubnd, -- GitLab From df02ca0d11061c4ab27bcc7b2ea028ad37959987 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 14:10:04 -0500 Subject: [PATCH 14/26] cleanup --- peak_integration.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index a7cb893..86badf3 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1957,33 +1957,6 @@ class PeakHistogram(object): params_ubnd=params_ubnd, disp=True) - # result = fmin_bfgs_2(loss_fun, params_init, fprime=loss_fun.gradient, gtol=1e-6, maxiter=1000, disp=2, init_hess=None)#(loss_fun.hessian(params_init)+loss_fun.hessian(params_init).T)/2) - # result = fmin_bfgs_2(loss_fun, result.x, fprime=loss_fun.gradient, gtol=1e-6, maxiter=1000, disp=2, init_hess=loss_fun.hessian(result.x)) - # result = minimize(loss_fun, - # jac = loss_fun.gradient, - # hess = loss_fun.hessian, - # x0=result.x, bounds=tuple(zip(params_lbnd,params_ubnd)), - # method='Newton-CG', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug} - # ) - - # result = minimize(loss_fun, - # jac = loss_fun.gradient, - # hess = loss_fun.hessian, - # # hess = myBFGS(), #loss_fun.hessian, - # x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), - # method=solver, options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-8, 'gtol':1.e-6, 'xtol':1.e-6, 'disp':True} #_debug} - # # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} - # # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} - # # method='CG', options={'maxiter':1000, 'gtol':1.e-5, 'norm':np.inf, 'disp':True} - # # method='Newton-CG', options={'maxiter':1000, 'xtol':1.e-6, 'disp':True} - # # method='Nelder-Mead', options={'maxiter':1000, 'disp':False} - # # method='TNC', options={'scale':None, 'maxfun':1000, 'ftol':1.e-3, 'gtol':1.e-5, 'disp':True} - # # method='dogleg', options={'maxiter':1000, 'tol':1.e-6, 'gtol':1.e-8, 'disp':True} - # # method='trust-krylov', options={'maxiter':1000, 'tol':1.e-6, 'inexact':False, 'disp':True} - # # method='trust-exact', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} - # # method='trust-constr', options={'maxiter':1000, 'gtol':1.e-4, 'disp':True} - # ) - self.init_loss.append(loss_fun(params_init)) self.init_params.append(np.array(params_init)) -- GitLab From 3ff60e46400e482717a2677c5d8e764eed784c57 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 14:10:16 -0500 Subject: [PATCH 15/26] minor fixes --- peak_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peak_integration.py b/peak_integration.py index 86badf3..890031e 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1964,7 +1964,7 @@ class PeakHistogram(object): if _debug: print(f'\nConverged: {result.success}') - print(f'\nInitial params: {np.array(params_init)+1e-99}') + print(f'\nInitial params: {np.array(params_init)}') print(f' Final params: {result.x}') start = time.time() -- GitLab From dd301d9584e5bae7629899d1a5a3d8c2738bba1d Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 14:11:37 -0500 Subject: [PATCH 16/26] minor fixes --- peak_integration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 890031e..e48220b 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1957,10 +1957,11 @@ class PeakHistogram(object): params_ubnd=params_ubnd, disp=True) - self.init_loss.append(loss_fun(params_init)) self.init_params.append(np.array(params_init)) + self.init_loss.append(loss_fun(params_init)) - self.fit_params = result.x + self.fit_params = result.x + self.loss = loss_fun(result.x) if _debug: print(f'\nConverged: {result.success}') -- GitLab From 19569caec17a912cf043da6c195787ccef2c1204 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 14:21:46 -0500 Subject: [PATCH 17/26] add constant to mle loss --- peak_integration.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/peak_integration.py b/peak_integration.py index e48220b..f7720fc 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1321,9 +1321,16 @@ class Loss(object): class MLELoss(Loss): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # usefull constant to make optimal loss closer to 0 + # it can affect relative convergence criteria + pos_data = self.data[self.data>0] + self.constant = -(pos_data-pos_data*np.log(pos_data)).sum() + def loss(self, params): fit = self.fit(params) - return (fit-self.data*np.log(fit)).sum() + return (fit-self.data*np.log(fit)).sum() + self.constant @property def dloss_dfit(self): -- GitLab From b0b6169152ba3478eeead55dd006cc45518cf2ef Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 16:35:08 -0500 Subject: [PATCH 18/26] update loss.minimize --- peak_integration.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index f7720fc..7976d14 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1341,7 +1341,7 @@ class MLELoss(Loss): def d2loss_dfit2(self): return self.data / self._fit**2 - def minimize(self, solver, tol, maxiter, maxfun, params_init, params_lbnd, params_ubnd, disp=_debug): + def minimize(self, solver, tol, maxiter, maxfun, params_init, params_lbnd=None, params_ubnd=None, disp=_debug): # from scipy.optimize import BFGS # class myBFGS(BFGS): # def initialize(self, n, approx_type): @@ -1350,17 +1350,20 @@ class MLELoss(Loss): # self.B = loss_fun.hessian(params_init) # else: # self.H = np.linalg.inv(loss_fun.hessian(params_init)) - if solver=='Newton-CG': options = {'maxiter':maxiter, 'xtol':tol, 'disp':disp} + hess = self.hessian + # hess = myBFGS(), elif solver=='BFGS': + options = {'maxiter':maxiter, 'gtol':tol, 'disp':disp} + hess = None + elif solver=='L-BFGS-B': options = {'maxiter':maxiter, 'maxfun':maxfun, 'maxls':100, 'maxcor':100, 'ftol':1.e-10, 'gtol':tol, 'xtol':1.e-10, 'disp':disp} + hess = None return scipy_minimize(self, - jac = self.gradient, - hess = self.hessian, - # hess = myBFGS(), - x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)), + jac = self.gradient, hess = hess, + x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)) if params_lbnd is not None and params_ubnd is not None else None, method=solver, options=options, # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} -- GitLab From c00a2f5ac4746ed53bce0d3de4ce4671ba51f95f Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Thu, 29 Dec 2022 16:36:06 -0500 Subject: [PATCH 19/26] update plot --- peak_integration.py | 767 +++++++++++++++++++++++++++++++++----------- 1 file changed, 571 insertions(+), 196 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 7976d14..e1521c7 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -23,11 +23,275 @@ config['Q.convention'] = "Crystallography" # from ellipsoid import EllipsoidTool +from scipy.optimize._optimize import _check_unknown_options, _prepare_scalar_function, vecnorm, _LineSearchError, _epsilon, _line_search_wolfe12, Inf, _status_message, OptimizeResult +from numpy import asarray -np.set_printoptions(precision=2, linewidth=200) + +np.set_printoptions(precision=2, linewidth=200, formatter={'float':lambda x: f'{x:9.2e}'}) _debug_dir = 'debug' -_debug = True -_profile = False +_debug = False +_profile = True + + + +def fmin_bfgs_2(f, x0, fprime=None, args=(), gtol=1e-5, norm=Inf, + epsilon=_epsilon, maxiter=None, full_output=0, disp=1, + retall=0, callback=None, init_hess=None): + """ + Minimize a function using the BFGS algorithm. + Parameters + ---------- + f : callable ``f(x,*args)`` + Objective function to be minimized. + x0 : ndarray + Initial guess. + fprime : callable ``f'(x,*args)``, optional + Gradient of f. + args : tuple, optional + Extra arguments passed to f and fprime. + gtol : float, optional + Gradient norm must be less than `gtol` before successful termination. + norm : float, optional + Order of norm (Inf is max, -Inf is min) + epsilon : int or ndarray, optional + If `fprime` is approximated, use this value for the step size. + callback : callable, optional + An optional user-supplied function to call after each + iteration. Called as ``callback(xk)``, where ``xk`` is the + current parameter vector. + maxiter : int, optional + Maximum number of iterations to perform. + full_output : bool, optional + If True, return ``fopt``, ``func_calls``, ``grad_calls``, and + ``warnflag`` in addition to ``xopt``. + disp : bool, optional + Print convergence message if True. + retall : bool, optional + Return a list of results at each iteration if True. + Returns + ------- + xopt : ndarray + Parameters which minimize f, i.e., ``f(xopt) == fopt``. + fopt : float + Minimum value. + gopt : ndarray + Value of gradient at minimum, f'(xopt), which should be near 0. + Bopt : ndarray + Value of 1/f''(xopt), i.e., the inverse Hessian matrix. + func_calls : int + Number of function_calls made. + grad_calls : int + Number of gradient calls made. + warnflag : integer + 1 : Maximum number of iterations exceeded. + 2 : Gradient and/or function calls not changing. + 3 : NaN result encountered. + allvecs : list + The value of `xopt` at each iteration. Only returned if `retall` is + True. + Notes + ----- + Optimize the function, `f`, whose gradient is given by `fprime` + using the quasi-Newton method of Broyden, Fletcher, Goldfarb, + and Shanno (BFGS). + See Also + -------- + minimize: Interface to minimization algorithms for multivariate + functions. See ``method='BFGS'`` in particular. + References + ---------- + Wright, and Nocedal 'Numerical Optimization', 1999, p. 198. + Examples + -------- + >>> from scipy.optimize import fmin_bfgs + >>> def quadratic_cost(x, Q): + ... return x @ Q @ x + ... + >>> x0 = np.array([-3, -4]) + >>> cost_weight = np.diag([1., 10.]) + >>> # Note that a trailing comma is necessary for a tuple with single element + >>> fmin_bfgs(quadratic_cost, x0, args=(cost_weight,)) + Optimization terminated successfully. + Current function value: 0.000000 + Iterations: 7 # may vary + Function evaluations: 24 # may vary + Gradient evaluations: 8 # may vary + array([ 2.85169950e-06, -4.61820139e-07]) + >>> def quadratic_cost_grad(x, Q): + ... return 2 * Q @ x + ... + >>> fmin_bfgs(quadratic_cost, x0, quadratic_cost_grad, args=(cost_weight,)) + Optimization terminated successfully. + Current function value: 0.000000 + Iterations: 7 + Function evaluations: 8 + Gradient evaluations: 8 + array([ 2.85916637e-06, -4.54371951e-07]) + """ + opts = {'gtol': gtol, + 'norm': norm, + 'eps': epsilon, + 'disp': disp, + 'maxiter': maxiter, + 'return_all': retall} + + res = _minimize_bfgs_2(f, x0, args, fprime, callback=callback, init_hess=init_hess, **opts) + return res + # if full_output: + # retlist = (res['x'], res['fun'], res['jac'], res['hess_inv'], + # res['nfev'], res['njev'], res['status']) + # if retall: + # retlist += (res['allvecs'], ) + # return retlist + # else: + # if retall: + # return res['x'], res['allvecs'] + # else: + # return res['x'] + + +def _minimize_bfgs_2(fun, x0, args=(), jac=None, callback=None, + gtol=1e-5, norm=np.inf, eps=1.e-6, maxiter=None, + disp=False, return_all=False, finite_diff_rel_step=None, init_hess=None, + **unknown_options): + """ + Minimization of scalar function of one or more variables using the + BFGS algorithm. + Options + ------- + disp : bool + Set to True to print convergence messages. + maxiter : int + Maximum number of iterations to perform. + gtol : float + Gradient norm must be less than `gtol` before successful + termination. + norm : float + Order of norm (Inf is max, -Inf is min). + eps : float or ndarray + If `jac is None` the absolute step size used for numerical + approximation of the jacobian via forward differences. + return_all : bool, optional + Set to True to return a list of the best solution at each of the + iterations. + finite_diff_rel_step : None or array_like, optional + If `jac in ['2-point', '3-point', 'cs']` the relative step size to + use for numerical approximation of the jacobian. The absolute step + size is computed as ``h = rel_step * sign(x) * max(1, abs(x))``, + possibly adjusted to fit into the bounds. For ``method='3-point'`` + the sign of `h` is ignored. If None (default) then step is selected + automatically. + """ + _check_unknown_options(unknown_options) + retall = return_all + + x0 = asarray(x0).flatten() + if x0.ndim == 0: + x0.shape = (1,) + if maxiter is None: + maxiter = len(x0) * 200 + + sf = _prepare_scalar_function(fun, x0, jac, args=args, epsilon=eps, + finite_diff_rel_step=finite_diff_rel_step) + + f = sf.fun + myfprime = sf.grad + + old_fval = f(x0) + gfk = myfprime(x0) + + k = 0 + N = len(x0) + I = np.eye(N, dtype=int) + if init_hess is not None: + Hk = init_hess + else: + Hk = I + + # Sets the initial step guess to dx ~ 1 + old_old_fval = old_fval + np.linalg.norm(gfk) / 2 + + xk = x0 + if retall: + allvecs = [x0] + warnflag = 0 + gnorm = vecnorm(gfk, ord=norm) + while (gnorm > gtol) and (k < maxiter): + pk = -np.dot(Hk, gfk) + try: + alpha_k, fc, gc, old_fval, old_old_fval, gfkp1 = \ + _line_search_wolfe12(f, myfprime, xk, pk, gfk, + old_fval, old_old_fval, amin=1e-100, amax=1e100) + except _LineSearchError: + # Line search failed to find a better solution. + warnflag = 2 + break + + xkp1 = xk + alpha_k * pk + if retall: + allvecs.append(xkp1) + sk = xkp1 - xk + xk = xkp1 + if gfkp1 is None: + gfkp1 = myfprime(xkp1) + + yk = gfkp1 - gfk + gfk = gfkp1 + if callback is not None: + callback(xk) + k += 1 + gnorm = vecnorm(gfk, ord=norm) + if (gnorm <= gtol): + break + + if not np.isfinite(old_fval): + # We correctly found +-Inf as optimal value, or something went + # wrong. + warnflag = 2 + break + + rhok_inv = np.dot(yk, sk) + # this was handled in numeric, let it remaines for more safety + if rhok_inv == 0.: + rhok = 1000.0 + if disp: + print("Divide-by-zero encountered: rhok assumed large") + else: + rhok = 1. / rhok_inv + + A1 = I - sk[:, np.newaxis] * yk[np.newaxis, :] * rhok + A2 = I - yk[:, np.newaxis] * sk[np.newaxis, :] * rhok + Hk = np.dot(A1, np.dot(Hk, A2)) + (rhok * sk[:, np.newaxis] * + sk[np.newaxis, :]) + + fval = old_fval + + if warnflag == 2: + msg = _status_message['pr_loss'] + elif k >= maxiter: + warnflag = 1 + msg = _status_message['maxiter'] + elif np.isnan(gnorm) or np.isnan(fval) or np.isnan(xk).any(): + warnflag = 3 + msg = _status_message['nan'] + else: + msg = _status_message['success'] + + if disp: + print("%s%s" % ("Warning: " if warnflag != 0 else "", msg)) + print(" Current function value: %f" % fval) + print(" Iterations: %d" % k) + print(" Function evaluations: %d" % sf.nfev) + print(" Gradient evaluations: %d" % sf.ngev) + + result = OptimizeResult(fun=fval, jac=gfk, hess_inv=Hk, nfev=sf.nfev, + njev=sf.ngev, status=warnflag, + success=(warnflag == 0), message=msg, x=xk, + nit=k) + if retall: + result['allvecs'] = allvecs + return result + ############################################################################### @@ -64,6 +328,7 @@ def marginalize_1d(arr, bin_lengths=None, mask=None, normalize=False, recover_sh # mask out elements of the original array if mask is not None: + mask = rebin_histogram(mask.astype(float), bin_lengths) > 0 arr = arr * mask.astype(int) # # reshape input array so that each bin has unit volume @@ -126,6 +391,7 @@ def marginalize_2d(arr, bin_lengths=None, mask=None, normalize=False, recover_sh # mask out elements of the original array if mask is not None: + mask = rebin_histogram(mask.astype(float), bin_lengths) > 0 arr = arr * mask.astype(int) if normalize: @@ -516,26 +782,32 @@ class Polynomial(object): from scipy.special import comb self.nparams = comb(ndims+order,order,exact=True) + self._profile = {} + + + def check_parameters(self, params): + params = np.asarray(params).copy().ravel() + if params.size!=self.nparams: + raise ValueError(f"`params` array is of wrong size, must have `len(params) = binom(n+d,d) = {self.nparams}`, got {len(params)}") + return params + def __call__(self, params, x): start = time.time() - self.func_params = np.asarray(params).copy().ravel() - if self.func_params.size!=self.nparams: - raise ValueError(f"`params` array is of wrong size, must have `len(params) = binom(n+d,d) = {self.nparams}`, got {len(params)}") + self.func_params = self.check_parameters(params) self.val = np.array([params[0]**2]) if _profile: - self.func_time = time.time() - start - print(f'bkgr func: {self.func_time} sec') + self._profile['bkgr func'] = time.time() - start + # self.func_time = time.time() - start + # print(f'bkgr func: {self.func_time} sec') return self.val def gradient(self, params, x, dloss_dfit=None, *args, **kwargs): start = time.time() - self.grad_params = np.asarray(params).copy().ravel() - if self.grad_params.size!=self.nparams: - raise ValueError(f"`params` array is of wrong size, must have `len(params) = binom(n+d,d) = {self.nparams}`, got {len(params)}") + self.grad_params = self.check_parameters(params) # function always needs to evaluated before computing gradient if not hasattr(self, 'func_params') or not np.all(self.grad_params==self.func_params): @@ -548,16 +820,15 @@ class Polynomial(object): grad = np.array([2*params[0]]) * dloss_dfit.sum() if _profile: - self.grad_time = time.time()-start - print(f'bkgr grad: {self.grad_time} sec, {self.grad_time/self.func_time}') + self._profile['bkgr grad'] = time.time() - start + # self.grad_time = time.time()-start + # print(f'bkgr grad: {self.grad_time} sec, {self.grad_time/self.func_time}') return grad def hessian(self, params, x, dloss_dfit, d2loss_dfit2, *args, **kwargs): start = time.time() - self.hess_params = np.asarray(params).copy().ravel() - if self.hess_params.size!=self.nparams: - raise ValueError(f"`params` array is of wrong size, must have `len(params) = binom(n+d,d) = {self.nparams}`, got {len(params)}") + self.hess_params = self.check_parameters(params) # function and gradient always need to evaluated before computing hessian if not hasattr(self, 'func_params') or not np.all(self.hess_params==self.func_params): @@ -571,35 +842,69 @@ class Polynomial(object): hess = np.array([[2]]) * dloss_dfit.sum() + np.array([[4*params[0]**2]]) * d2loss_dfit2.sum() if _profile: - hess_time = time.time()-start - print(f'bkgr hess: {hess_time} sec, {hess_time/self.func_time}') + self._profile['bkgr hess'] = time.time() - start + # hess_time = time.time()-start + # print(f'bkgr hess: {hess_time} sec, {hess_time/self.func_time}') return hess class Gaussian(object): def __init__(self, ndims, parameterization='givens'): + # number of dimensions + self.ndims = ndims + + # covariance/precision parameterization self.parameterization = parameterization - # number of parameters - self.ncnt = ndims - self.nskew = 0#ndims + # number of various parameters + self.nintst = 1 + self.ncnt = ndims if parameterization=='full': self.ncov = ndims**2 else: self.ncov = (ndims*(ndims+1))//2 + self.nskew = 0 #ndims - # number of dimensions - self.ndims = ndims - - # number of parameters - self.nparams = 1 + self.ncnt + self.ncov + self.nskew + # total number of parameters + self.nparams = self.nintst + self.ncnt + self.ncov + self.nskew # number of angles if parameterization=='givens': self.nangles = (ndims*(ndims-1))//2 else: - self.nangles = None + self.nangles = 0 + + # parameters of the model + self.params = None + + # dict for profiling data + self._profile = {} + + + @property + def intensity(self): + return self.intst + + @property + def center(self): + return self.cnt + + @property + def angles(self): + if self.parameterization=='givens': + return self.precision[:self.nangles] + else: + _,_,R = svd(self.sqrtP) + return np.array([np.arctan2(R[2,1],R[2,2]), np.arctan2(R[2,0],np.sqrt(R[2,1]**2+R[2,2]**2)), np.arctan2(R[1,0],R[0,0])]) + + @property + def semiaxes(self): + if self.parameterization=='givens': + return self.sqrtD + else: + _,d,_ = svd(self.sqrtP) + return d def check_parameters(self, params): @@ -714,24 +1019,24 @@ class Gaussian(object): self.sqrtintst = params[0] self.intst = params[0]**2 self.cnt = params[1:1+self.ncnt] - sqrtP = params[1+self.ncnt:1+self.ncnt+self.ncov] + self.precision = params[1+self.ncnt:1+self.ncnt+self.ncov] # skew = params[1+ncnt+ncov:1+ncnt+ncov+nskew].reshape((1,-1)) if self.parameterization=='full': - self.sqrtP = sqrtP.reshape((self.ndims,self.ndims)) + self.sqrtP = self.precision.reshape((self.ndims,self.ndims)) elif self.parameterization=='cholesky': triu_ind = np.triu_indices(self.ndims) diag_ind = np.diag_indices(self.ndims) # dense matrix self.sqrtP = np.zeros((self.ndims,self.ndims)) # fill upper triangular part - self.sqrtP[triu_ind] = sqrtP + self.sqrtP[triu_ind] = self.precision # positive diagonal makes Cholesky decomposition unique self.sqrtP[diag_ind] *= self.sqrtP[diag_ind] #np.exp(sqrtP_i[diag_ind]) elif self.parameterization=='givens': # square roots of the eigenvalues of the precision matrix, # aka lengths of the ellipsoid semiaxes - self.sqrtD = sqrtP[self.nangles:] + self.sqrtD = self.precision[self.nangles:] # square root of the precision matrix, i.e., diag(sqrt_eig) @ R self.sqrtP = self.sqrtD[:,np.newaxis] * self.rotation_matrix(params) return self.intst, self.cnt, self.sqrtP @@ -751,8 +1056,9 @@ class Gaussian(object): # self.val = self.val * (1+erf(skew@(x-cnt.reshape((ndims,1))))/np.sqrt(2)).ravel() if _profile: - self.func_time = time.time() - start - print(f'peak func: {self.func_time} sec') + self._profile['peak func'] = time.time() - start + # self.func_time = time.time() - start + # print(f'peak func: {self.func_time} sec') return self.val @@ -815,8 +1121,9 @@ class Gaussian(object): grad = np.einsum('k,ki->i',dloss_dfit,self.grad) if _profile: - self.grad_time = time.time()-start - print(f'peak grad: {self.grad_time} sec, {self.grad_time/self.func_time}') + self._profile['peak grad'] = time.time() - start + # self.grad_time = time.time()-start + # print(f'peak grad: {self.grad_time} sec, {self.grad_time/self.func_time}') return grad @@ -861,7 +1168,7 @@ class Gaussian(object): d2g_dsqrtP2[:,axi,:,axm,:] -= self.val.reshape((-1,1,1)) * np.einsum('kj,km->kjm',self.xcnt,self.xcnt) d2g_dsqrtP2 = d2g_dsqrtP2.reshape((-1,self.ncov,self.ncov)) elif self.parameterization=='cholesky': - axi,axm = np.diag_indices(self.ndims) + diag_ind = np.diag_indices(self.ndims) triu_ind = np.triu_indices(self.ndims) d2g_dintst_dsqrtP = self.dg_dsqrtP[:,triu_ind[0],triu_ind[1]].reshape((-1,1,self.ncov)) * (2/self.sqrtintst) # (npoints,1,ncov) @@ -869,18 +1176,17 @@ class Gaussian(object): # upper triangular part d2g_dcnt_dsqrtP = np.einsum('ki,knm->kinm', self.dg_dcnt, np.einsum('ki,kj->kij', self.sqrtPxcnt_, self.xcnt) ) d2g_dcnt_dsqrtP += self.val.reshape((-1,1,1,1)) * np.einsum('ni,km->kinm', self.sqrtP, self.xcnt) - d2g_dcnt_dsqrtP[:,axi,:,axm] -= self.val.reshape((-1,1)) * self.sqrtPxcnt_ + d2g_dcnt_dsqrtP[:,diag_ind[0],:,diag_ind[1]] -= self.val.reshape((-1,1)) * self.sqrtPxcnt_ # update diagonal entries - d2g_dcnt_dsqrtP[:,:,axi,axm] *= 2 * self.sqrtP[axi,axm] + d2g_dcnt_dsqrtP[:,:,diag_ind[0],diag_ind[1]] *= 2 * np.sqrt(self.sqrtP[diag_ind[0],diag_ind[1]]) d2g_dcnt_dsqrtP = d2g_dcnt_dsqrtP[:,:,triu_ind[0],triu_ind[1]].reshape((-1,self.ncnt,self.ncov)) - d2g_dsqrtP2 = np.einsum('kij,knm->kijnm', self.dg_dsqrtP, np.einsum('kn,km->knm', self.sqrtPxcnt_, self.xcnt) ) - d2g_dsqrtP2[:,axi,:,axm,:] -= self.val.reshape((-1,1,1)) * np.einsum('kj,km->kjm',self.xcnt,self.xcnt) - d2g_dsqrtP2[:,:,:,axi,axm] *= 2 * self.sqrtP[axi,axm] - d2g_dsqrtP2[:,axi,axm,axi,axm] += 2 * np.einsum('kij,knm->kijnm', self.dg_dsqrtP, np.einsum('kn,km->knm', self.sqrtPxcnt_, self.xcnt) )[:,axi,axm,axi,axm] + d2g_dsqrtP2[:,diag_ind[0],:,diag_ind[1],:] -= self.val.reshape((-1,1,1)) * np.einsum('kj,km->kjm',self.xcnt,self.xcnt) + d2g_dsqrtP2[:,:,:,diag_ind[0],diag_ind[1]] *= 2 * np.sqrt(self.sqrtP[diag_ind[0],diag_ind[1]]) + d2g_dsqrtP2[:,diag_ind[0],diag_ind[1],diag_ind[0],diag_ind[1]] += 2 * np.einsum('kij,knm->kijnm', self.dg_dsqrtP, np.einsum('kn,km->knm', self.sqrtPxcnt_, self.xcnt) )[:,diag_ind[0],diag_ind[1],diag_ind[0],diag_ind[1]] d2g_dsqrtP2 = d2g_dsqrtP2[:,:,:,triu_ind[0],triu_ind[1]][:,triu_ind[0],triu_ind[1],:] elif self.parameterization=='givens': @@ -923,17 +1229,7 @@ class Gaussian(object): d2g_dintst2 = (dloss_dfit*d2g_dintst2).sum(axis=0) d2g_dintst_dcnt = (dloss_dfit*d2g_dintst_dcnt).sum(axis=0) d2g_dcnt2 = (dloss_dfit*d2g_dcnt2).sum(axis=0) - if self.parameterization=='full': - d2g_dintst_dsqrtP = (dloss_dfit*d2g_dintst_dsqrtP).sum(axis=0) - d2g_dcnt_dsqrtP = (dloss_dfit*d2g_dcnt_dsqrtP).sum(axis=0) - d2g_dsqrtP2 = (dloss_dfit*d2g_dsqrtP2).sum(axis=0) - - d2g = np.block([ - [d2g_dintst2, d2g_dintst_dcnt, d2g_dintst_dsqrtP], - [d2g_dintst_dcnt.T, d2g_dcnt2, d2g_dcnt_dsqrtP ], - [d2g_dintst_dsqrtP.T, d2g_dcnt_dsqrtP.T, d2g_dsqrtP2 ], - ]) - if self.parameterization=='cholesky': + if self.parameterization in ['full','cholesky']: d2g_dintst_dsqrtP = (dloss_dfit*d2g_dintst_dsqrtP).sum(axis=0) d2g_dcnt_dsqrtP = (dloss_dfit*d2g_dcnt_dsqrtP).sum(axis=0) d2g_dsqrtP2 = (dloss_dfit*d2g_dsqrtP2).sum(axis=0) @@ -966,8 +1262,9 @@ class Gaussian(object): # print(f'{time.time()-start1} block'); start1 = time.time() if _profile: - hess_time = time.time()-start - print(f'peak hess: {hess_time} sec, {hess_time/self.func_time}') + self._profile['peak hess'] = time.time() - start + # hess_time = time.time()-start + # print(f'peak hess: {hess_time} sec, {hess_time/self.func_time}') return d2g @@ -1205,28 +1502,28 @@ def gaussian_mixture(params, x, npeaks=1, covariance_parameterization='givens', return g.ravel() -from scipy.optimize.optimize import MemoizeJac -class MemoizeJacHess(MemoizeJac): - """ - Decorator that caches the return vales of a function returning (fun, grad, hess) each time it is called. - Source: https://stackoverflow.com/a/68608349/20715633 - """ - def __init__(self, fun): - super().__init__(fun) - self.hess = None +# from scipy.optimize.optimize import MemoizeJac +# class MemoizeJacHess(MemoizeJac): +# """ +# Decorator that caches the return vales of a function returning (fun, grad, hess) each time it is called. +# Source: https://stackoverflow.com/a/68608349/20715633 +# """ +# def __init__(self, fun): +# super().__init__(fun) +# self.hess = None - def _compute_if_needed(self, x, *args, **kwargs): - if not np.all(x == self.x) or self._value is None or self.jac is None or self.hess is None: - self.x = np.asarray(x).copy() - self._value, self.jac, self.hess = self.fun(x, *args, **kwargs) +# def _compute_if_needed(self, x, *args, **kwargs): +# if not np.all(x == self.x) or self._value is None or self.jac is None or self.hess is None: +# self.x = np.asarray(x).copy() +# self._value, self.jac, self.hess = self.fun(x, *args, **kwargs) - def derivative(self, x, *args, **kwargs): - self._compute_if_needed(x, *args, **kwargs) - return self.jac +# def derivative(self, x, *args, **kwargs): +# self._compute_if_needed(x, *args, **kwargs) +# return self.jac - def hessian(self, x, *args, **kwargs): - self._compute_if_needed(x, *args, **kwargs) - return self.hess +# def hessian(self, x, *args, **kwargs): +# self._compute_if_needed(x, *args, **kwargs) +# return self.hess def numerical_gradient(x, fun, *args, **kwargs): @@ -1394,7 +1691,7 @@ class PeakHistogram(object): self.ndims = hist_ws.getNumDims() # histogram array - self.data = hist_ws.getNumEventsArray().copy() + self.data = hist_ws.getNumEventsArray().copy() #/ 8 # self.data -= self.data.mean() + 0.5 * (self.data.max() - self.data.mean()) # self.data[self.data<0] = 0 @@ -1434,6 +1731,13 @@ class PeakHistogram(object): # background model self.bkgr_fun = Polynomial(ndims=self.ndims, order=0) + # profiling data + self._profile = {} + + # track initialization + self.init_loss = [] + self.init_params = [] + def get_grid_data(self, bins=None, rebin_mode='density', return_edges=False): '''Extract coordinates of the bins and bin counts from the histogram workspace @@ -1468,7 +1772,10 @@ class PeakHistogram(object): return data, points - def initialize(self, points, data, ellipsoid=True): + def initialize(self, points, data, ellipsoid=True, detector_mask=None): + start = time.time() + + ################################### # basic statistics data_min, data_max, data_mean = data.min(), data.max(), data.mean() @@ -1480,17 +1787,18 @@ class PeakHistogram(object): ################################### # find threshold intensity that gives largets radius reduction of the enclosing sphere + start1 = time.time() a = data_min b = data_max for _ in range(4): nthres = 10 thresh_rads = np.zeros((nthres,)) - thresh_vals = np.linspace(a, b, nthres-1, endpoint=True) + thresh_vals = np.linspace(a, b, nthres, endpoint=True) for i,thres in enumerate(thresh_vals): thresh_data = data - thres thresh_rads[i] = np.linalg.norm(points[thresh_data>=0,...]-cnt_init,ord=2,axis=1).max() - thresh_ind = np.argmax(np.abs(np.diff(thresh_rads)))+1 + thresh_ind = min(np.argmax(np.abs(np.diff(thresh_rads)))+1, nthres-1) thresh_val = thresh_vals[thresh_ind] thresh_rad = thresh_rads[thresh_ind] a = thresh_vals[max(0,thresh_ind-1)] @@ -1498,9 +1806,12 @@ class PeakHistogram(object): thresh_add = 0.1 * (data_max - thresh_val) + if _profile: + self._profile['initialize sphere'] = time.time() - start1 ################################### # estimate covariance matrix by fitting maximum enclosing ellipsoid + start1 = time.time() if ellipsoid: # bin centers inside the peak enclosing sphere @@ -1524,12 +1835,23 @@ class PeakHistogram(object): ellrad = np.array([thresh_rad]*self.ndims) ellrot = np.eye(self.ndims) + # scale axes to 1 standard deviation + ellrad /= np.sqrt(2*np.log((data_max-thresh_val-thresh_add)/thresh_add)) + + if _profile: + self._profile['initialize ellipsoid'] = time.time() - start1 ################################### + # standard deviation of the peak ellipsoid + peak_std = 4 + + # background mask + # bkgr_mask = mahalanobis_distance(ellcnt, ellrad.reshape((-1,1))*ellrot, points) < peak_std + # bkgr_mask = np.linalg.norm(points-cnt_init,ord=2,axis=1)>thresh_rad + bkgr_mask = data < thresh_val - # mean intensity outside threshold sphere - # bkgr_mean = data[np.linalg.norm(points-cnt_init,ord=2,axis=1)>thresh_rad].mean() - bkgr_mean = data[data<thresh_val].mean() + # mean intensity in the background + bkgr_mean = data[bkgr_mask].mean() # initialization and bounds for the background bkgr_init = [ np.sqrt(1.0*bkgr_mean)] #+ [0]*self.ndims @@ -1538,7 +1860,7 @@ class PeakHistogram(object): ################################### - if _debug: + if _debug and detector_mask is not None: _, _, edges = self.get_grid_data(return_edges=True) # full 3d covariance @@ -1560,8 +1882,8 @@ class PeakHistogram(object): full_data = np.zeros(self.shape) full_nobkgr_data = np.zeros(self.shape) - full_data[self.detector_mask] = data - full_nobkgr_data[self.detector_mask] = nobkgr_data + full_data[detector_mask] = data + full_nobkgr_data[detector_mask] = nobkgr_data data_1d = marginalize_1d(full_data, normalize=False) data_2d = marginalize_2d(full_data, normalize=False) @@ -1607,9 +1929,8 @@ class PeakHistogram(object): cnt_ubnd = [c+rad/3 for c,rad in zip(cnt_init,self.radiuses)] # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix - peak_std = 4 # scale ellipsoid to 1 std value - ini_rads = ellrad / np.sqrt(2*np.log((data_max-thresh_val-thresh_add)/thresh_add)) + ini_rads = ellrad #/ np.sqrt(2*np.log((data_max-thresh_val-thresh_add)/thresh_add)) # ini_rads = [ 1/4*rad/peak_std for rad in self.radiuses] # initial 'peak_std' radius is 1/2 of the box radius max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size @@ -1619,27 +1940,32 @@ class PeakHistogram(object): # `num_angles` angles and `ndims` square roots of the eigenvalues of the precision matrix num_angles = (self.ndims*(self.ndims-1))//2 # initial rotation angles of the ellipsoid - # ini_angles = [0]*num_angles + # angles_init = [0]*num_angles # extract angles from the ellipsoid rotation matrix - ini_angles = [np.arctan2(ellrot[2,1],ellrot[2,2]), np.arctan2(ellrot[2,0],np.sqrt(ellrot[2,1]**2+ellrot[2,2]**2)), np.arctan2(ellrot[1,0],ellrot[0,0])] + angles_init = [np.arctan2(ellrot[2,1],ellrot[2,2]), np.arctan2(ellrot[2,0],np.sqrt(ellrot[2,1]**2+ellrot[2,2]**2)), np.arctan2(ellrot[1,0],ellrot[0,0])] + angles_lbnd = [-np.pi]*num_angles + angles_ubnd = [ np.pi]*num_angles # initialize precision matrix - prec_init = ini_angles + [ 1/r for r in ini_rads] - prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] - prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] + prec_init = angles_init + [ 1/r for r in ini_rads] + prec_lbnd = angles_lbnd + [ 1/r for r in max_rads] + prec_ubnd = angles_ubnd + [ 1/r for r in min_rads] + # restrict to bounds + prec_init = [max(i,l) for i,l in zip(prec_init,prec_lbnd)] + prec_init = [min(i,u) for i,u in zip(prec_init,prec_ubnd)] elif self.parameterization=='cholesky': # upper triangular part of the Cholesky factor of the precision matrix num_chol = (self.ndims*(self.ndims+1))//2 prec_init = np.linalg.cholesky( ellrot.T @ np.diag(1/ini_rads**2) @ ellrot ).T prec_init[np.diag_indices(self.ndims)] = np.sqrt(prec_init[np.diag_indices(self.ndims)]) prec_init = list(prec_init[np.triu_indices(self.ndims)]) - prec_lbnd = [-1000]*num_chol - prec_ubnd = [ 1000]*num_chol + prec_lbnd = [-np.inf]*num_chol + prec_ubnd = [ np.inf]*num_chol elif self.parameterization=='full': # arbitrary square root of the precision matrix num_full = self.ndims**2 prec_init = list( (np.diag(1/ini_rads) @ ellrot).ravel() ) - prec_lbnd = [-1000]*num_full - prec_ubnd = [ 1000]*num_full + prec_lbnd = [-np.inf]*num_full + prec_ubnd = [ np.inf]*num_full # # initialization and bounds for the skewness # skew_init = [0]*self.ndims @@ -1652,10 +1978,14 @@ class PeakHistogram(object): params_lbnd = bkgr_lbnd + intst_lbnd + cnt_lbnd + prec_lbnd #+ skew_lbnd params_ubnd = bkgr_ubnd + intst_ubnd + cnt_ubnd + prec_ubnd #+ skew_ubnd + + if _profile: + self._profile['initialize'] = time.time() - start + return params_init, params_lbnd, params_ubnd - def fit_two_level(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='L-BFGS-B', covariance_parameterization='givens', plot_intermediate=False): + def fit_two_level(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='L-BFGS-B', plot_intermediate=False): # shape of the largest subhistogram with shape as a power of 2 shape2 = [2**int(np.log2(self.shape[d])) for d in range(self.ndims)] @@ -1759,20 +2089,20 @@ class PeakHistogram(object): return output - def fit_multilevel(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='L-BFGS-B', covariance_parameterization='givens', plot_intermediate=False): + def fit_multilevel(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='BFGS', tol=1.e-6, plot_intermediate=False): # shape of the largest subhistogram with shape as a power of 2 shape2 = [2**int(np.log2(self.shape[d])) for d in range(self.ndims)] # smallest power of 2 among all dimensions minpow2 = min([int(np.log2(self.shape[d])) for d in range(self.ndims)]) + # solver at level 0 solver1 = solver0 params = params_lbnd = params_ubnd = None for p in range(min_level,minpow2+1): start = time.time() - # left, middle (with power of 2 shape) and right bins binsl = [(self.shape[d]-shape2[d])//2 for d in range(self.ndims)] binsm = [split_bins([shape2[d]],2**p,recursive=False) for d in range(self.ndims)] @@ -1785,76 +2115,64 @@ class PeakHistogram(object): bins = [ ([bl] if bl>0 else [])+bm+([br] if br>0 else []) for bl,bm,br in zip(binsl,binsm,binsr)] # fit histogram at the current level - # params, sucess - output = self.fit(bins=bins, return_bins=return_bins, loss=loss, solver=solver1, covariance_parameterization=covariance_parameterization, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) + output = self.fit(bins=bins, return_bins=return_bins, loss=loss, solver=solver1, tol=tol, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) params, sucess = output[0], output[1] # skip level if fit not successful # if not sucess and p<minpow2: # params = params_lbnd = params_ubnd = None # continue - solver1 = solver - nangles = self.ndims*(self.ndims-1)//2 - sqrtbkgr = params[0] - sqrtintst = params[1] - cnt = params[2:2+self.ndims] - angles = params[2+self.ndims:2+self.ndims+nangles] - invrads = params[2+self.ndims+nangles:2+2*self.ndims+nangles] - # skewness = params[2+2*ndims+nangles:2+3*ndims+nangles] + # solver at level>0 + solver1 = solver ####################################################################### # refine search bounds # bounds for the center - dcnt = [ 10*res*2**(minpow2-p) for res in self.resolution] # search radius is 10 voxels at the current level - cnt_lbnd = [c-dc for c,dc in zip(cnt,dcnt)] - cnt_ubnd = [c+dc for c,dc in zip(cnt,dcnt)] - - # bounds for the precision matrix angles - if minpow2>min_level: - phi0 = np.pi/1 - phi1 = np.pi#/8 - dphi = phi0 + (p-min_level)/(minpow2-min_level) * (phi1-phi0) - else: - dphi = np.pi - - # sc = 2 + (p-min_level)/(minpow2-min_level) * (1-2) - # sc = 2 - # print(sc) + # search radius at next level is 3 voxels at the current level + dcnt = [ 3 * res*2**(minpow2-p) for res in self.resolution] + cnt_lbnd = [c-dc for c,dc in zip(self.peak_fun.center,dcnt)] + cnt_ubnd = [c+dc for c,dc in zip(self.peak_fun.center,dcnt)] ###################################### # bounds for the precision matrix - # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix - peak_std = 4 - max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius - min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size - # prec_lbnd = [-np.pi]*num_angles + [ 1/r for r in max_rads] - # prec_ubnd = [ np.pi]*num_angles + [ 1/r for r in min_rads] - # prec_lbnd = [max(-np.pi,phi-dphi) for phi in angles] + [ r/2.0 for r in invrads] - prec_lbnd = [max(-np.pi,phi-dphi) for phi in angles] + [ 1/r for r in max_rads] #[ max((self.limits[d][1]-self.limits[d][0])/4/(4/3),invrads[d]/2.0) for d in range(self.ndims)] - prec_ubnd = [min( np.pi,phi+dphi) for phi in angles] + [ 1/r for r in min_rads] #[ 100*r for r in invrads] + if self.parameterization=='givens': + # bounds for ellipsoid angles + if minpow2>min_level: + phi0 = np.pi/1 + phi1 = np.pi/8 + dphi = phi0 + (p+1-min_level)/(minpow2+1-min_level) * (phi1-phi0) + else: + dphi = np.pi + angles_lbnd = [max(-np.pi,phi-dphi) for phi in self.peak_fun.angles] + angles_ubnd = [min( np.pi,phi+dphi) for phi in self.peak_fun.angles] + + # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix + peak_std = 4 + max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius + min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size + prec_lbnd = angles_lbnd + [ 1/r for r in max_rads] #[ max((self.limits[d][1]-self.limits[d][0])/4/(4/3),invrads[d]/2.0) for d in range(self.ndims)] + prec_ubnd = angles_ubnd + [ 1/r for r in min_rads] #[ 100*r for r in invrads] + else: + prec_lbnd = [-np.inf] * self.peak_fun.ncov + prec_ubnd = [ np.inf] * self.peak_fun.ncov # bounds for all parameters # params_lbnd = [np.abs(sqrtbkgr)/2, np.abs(sqrtintst)/2] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] # params_ubnd = [2*np.abs(sqrtbkgr), 2*np.abs(sqrtintst)] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] - params_lbnd = [-np.inf, -np.inf] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] - params_ubnd = [ np.inf, np.inf] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] - - # params[0] = np.abs(sqrtbkgr) - # params[1] = np.abs(sqrtintst) - - # params_lbnd = params_lbnd + list(params[len(params_lbnd):]) - # params_ubnd = params_ubnd + list(params[len(params_ubnd):]) - # params_lbnd = params_ubnd = None + # params_lbnd = [-np.inf, -np.inf] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] + # params_ubnd = [ np.inf, np.inf] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] ####################################################################### - print(f"Fitted histogram with {2**p:3d} bins: {time.time()-start:.3f} seconds") + if _profile: + self._profile[f'fit {2**p:3d} bins'] = time.time() - start - # if plot_intermediate: - # plot_fit(hist_ws, params, bins, prefix=f"{p}", peak_id=1074, peak_hkl=[2.0,-2.0,-9.0], peak_std=4, bkgr_std=7, detector_mask=None, log=True) + if plot_intermediate: + self.plot(bins, prefix=f"level_{p}", log=False) + # plot_fit(hist_ws, params, bins, prefix=f"{p}", peak_id=1074, peak_hkl=[2.0,-2.0,-9.0], peak_std=4, bkgr_std=7, detector_mask=None, log=True) # start = time.time() # output = self.fit(return_bins=return_bins, loss=loss, solver=solver, covariance_parameterization=covariance_parameterization, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) @@ -1881,6 +2199,8 @@ class PeakHistogram(object): bins: optional, returned bins ''' + start = time.time() + ########################################################################### # rebin data @@ -1936,6 +2256,10 @@ class PeakHistogram(object): # if (params_init is None) or (params_lbnd is None) or (params_ubnd is None): if params_init is None: params_init, params_lbnd, params_ubnd = self.initialize(fit_points, fit_data) + # else: + # params_tmp = params_init + # params_init, params_lbnd, params_ubnd = self.initialize(fit_points, fit_data) + # params_init[5:] = params_tmp[5:] ########################################################################### @@ -1975,9 +2299,17 @@ class PeakHistogram(object): if _debug: print(f'\nConverged: {result.success}') + + print('\nParameters') + print('----------') print(f'\nInitial params: {np.array(params_init)}') print(f' Final params: {result.x}') + print('\nLoss') + print('----') + print(f'\nInitial loss: {self.init_loss[-1]:.2f}') + print(f' Final loss: {self.loss:.2f}') + start = time.time() dg = loss_fun.gradient(self.fit_params) dg_time = time.time()-start @@ -2017,6 +2349,12 @@ class PeakHistogram(object): print(f' FD time: {fdd2g_time:.3f} sec, {fdd2g_time/d2g_time:.2f} slower') # exit() + if _profile: + if 'fit' in self._profile.keys(): + self._profile['fit'] += time.time() - start + else: + self._profile['fit'] = time.time() - start + if return_bins: return self.fit_params, result.success, bins else: @@ -2024,6 +2362,10 @@ class PeakHistogram(object): def plot(self, bins, plot_path='output', prefix=None, peak_std=4, bkgr_std=10, log=False): + start = time.time() + + styles = [':','-.','--','-'] + # create output directory for plots Path(plot_path).mkdir(parents=True, exist_ok=True) @@ -2057,38 +2399,42 @@ class PeakHistogram(object): ####################################################################### - # evaluate inital model - - # parameters - ini_bkgr_params = self.init_params[:self.bkgr_fun.nparams] - ini_peak_params = self.init_params[self.bkgr_fun.nparams:] + # evaluate inital models + + ini_mu = [] + ini_sigma_2d = [[] for _ in self.init_params] + ini_angle_2d = [[] for _ in self.init_params] + ini_fit = [] + ini_fit_masked = [] + ini_rebinned_fit = [] + for j,init_params in enumerate(self.init_params): + # parameters + ini_bkgr_params = init_params[:self.bkgr_fun.nparams] + ini_peak_params = init_params[self.bkgr_fun.nparams:] + + # peak center and full covariance + ini_mu.append(self.peak_fun.get_parameters(ini_peak_params)[1]) + ini_cov_3d = self.peak_fun.Cov(ini_peak_params) - # peak center and full covariance - # cov_3d = R.T @ np.diag(sqrt_eig**2) @ R - _,ini_mu,_ = self.peak_fun.get_parameters(ini_peak_params) - ini_cov_3d = self.peak_fun.Cov(ini_peak_params) - - # covariances and ellipsoids of 2d marginals - ini_cov_2d = [] - ini_angle_2d = [] - ini_sigma_2d = [] - for i in range(self.ndims): - yind, xind = axes_order(i) - ini_cov_2d.append( ini_cov_3d[np.ix_([xind,yind],[xind,yind])] ) - roti,eigi,_ = svd(ini_cov_2d[-1]) - # eigi, roti = eig(cov_2d[-1]) - # eigi, roti = eigen(cov_2d[-1]) - # ini_angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) - ini_angle_2d.append( np.arctan2(roti[1,0],roti[0,0])/np.pi*180 ) - ini_sigma_2d.append( np.sqrt(eigi) ) + # covariances and ellipsoids of 2d marginals + # ini_angle_2d.append([]*self.ndims) + # ini_sigma_2d.append([]*self.ndims) + for i in range(self.ndims): + yind, xind = axes_order(i) + ini_cov_2d = ini_cov_3d[np.ix_([xind,yind],[xind,yind])] + roti,eigi,_ = svd(ini_cov_2d) + # eigi, roti = eig(cov_2d[-1]) + # eigi, roti = eigen(cov_2d[-1]) + # ini_angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) + ini_angle_2d[j].append( np.arctan2(roti[1,0],roti[0,0])/np.pi*180 ) + ini_sigma_2d[j].append( np.sqrt(eigi) ) - # fitted model - ini_fit = ini_bkgr_params[0]**2 + self.peak_fun(ini_peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) - ini_fit_masked = (1 if self.detector_mask is None else self.detector_mask) * ini_fit + # fitted model + ini_fit.append( ini_bkgr_params[0]**2 + self.peak_fun(ini_peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) ) + ini_fit_masked.append( (1 if self.detector_mask is None else self.detector_mask) * ini_fit ) - # rebinned data and fit - rebinned_data, rebinned_points, rebinned_edges = self.get_grid_data(bins=bins, rebin_mode='density', return_edges=True) - ini_rebinned_fit = rebin_histogram(ini_fit, bins, mode='density') + # rebinned fit + ini_rebinned_fit.append( rebin_histogram(ini_fit[-1], bins, mode='density') ) ####################################################################### @@ -2141,52 +2487,60 @@ class PeakHistogram(object): data_1d = marginalize_1d(data, normalize=normalize, mask=self.detector_mask) fit_1d = marginalize_1d(fit, normalize=normalize, mask=self.detector_mask) - ini_fit_1d = marginalize_1d(ini_fit, normalize=normalize, mask=self.detector_mask) + ini_fit_1d = [ marginalize_1d(ini_fit_i, normalize=normalize, mask=self.detector_mask) for ini_fit_i in ini_fit ] rebinned_data_1d = marginalize_1d(rebinned_data, normalize=normalize, bin_lengths=bins, mask=self.detector_mask) rebinned_fit_1d = marginalize_1d(rebinned_fit, normalize=normalize, bin_lengths=bins, mask=self.detector_mask) subfig_no+=1 for i,ax in enumerate(subfigs[subfig_no].subplots(1,3,sharey=True)): ax.stairs(data_1d[i], edges=edges[i], fill=True, alpha=0.3) - ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, color='b', baseline=None) - ax.plot(dim_points[i], ini_fit_1d[i], color='g', ls='-') + if np.all(np.array([d.size for d in data_1d])!=np.array([d.size for d in rebinned_data_1d])): + ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, color='b', baseline=None) + for j in range(len(ini_fit_1d)): + ax.plot(dim_points[i], ini_fit_1d[j][i], color='g', ls=styles[-1-len(ini_fit_1d)+1+j]) ax.plot(dim_points[i], fit_1d[i], color='black') ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') if i==0: - ax.legend(['data','reb. data','init. fit','final fit'], framealpha=1.0, fontsize='xx-large') + if np.all(np.array([d.size for d in data_1d])!=np.array([d.size for d in rebinned_data_1d])): + data_reb_data = ['orig. data','reb. data'] + else: + data_reb_data = ['orig. data'] + if len(ini_fit_1d)==1: + ax.legend(data_reb_data+['init. fit','final fit'], framealpha=1.0, fontsize='xx-large') + else: + ax.legend(data_reb_data+['init. fit']+[f'fit at level {j-1}' for j in range(1,len(ini_fit_1d))]+['final fit'], framealpha=1.0, fontsize='xx-large') + # ax.legend(['orig. data','reb. data']+(['init. fit'] if len(ini_fit_1d)==0 else [f'init. fit {j}' for j in range(len(ini_fit_1d))])+['final fit'], framealpha=1.0, fontsize='xx-large') ax.set_box_aspect(1) - subfigs[subfig_no].suptitle('Original data and fit', fontsize='xx-large') + subfigs[subfig_no].suptitle('Data, initial and final fits', fontsize='xx-large') subfig_no+=1 for i,ax in enumerate(subfigs[subfig_no].subplots(1,3,sharey=True)): - ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=True, alpha=0.5) + ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=True, alpha=0.3) ax.stairs(rebinned_fit_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, baseline=None) - ax.plot(dim_points[i], fit_1d[i], color='black') + ax.plot(dim_points[i], fit_1d[i], '-', marker='.', color='black') ax.vlines([mu[i]-peak_std*np.sqrt(cov_3d[i,i]),mu[i]+peak_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-') ax.vlines([mu[i]-bkgr_std*np.sqrt(cov_3d[i,i]),mu[i]+bkgr_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-.') ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') if i==0: - ax.legend(['reb. data','reb. fit','final fit', f'{peak_std} sigma', f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.legend(['reb. data','reb. fit','fit', f'{peak_std} sigma', f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') ax.set_box_aspect(1) - subfigs[subfig_no].suptitle('Fit with identified peak bounds', fontsize='xx-large') - - # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}_1d.png') + subfigs[subfig_no].suptitle('Fits with peak bounds', fontsize='xx-large') ######################################## # plot 2d marginals data_2d = marginalize_2d(data, normalize=normalize, sortx=sortx) fit_2d = marginalize_2d(fit, normalize=normalize, sortx=sortx) - ini_fit_2d = marginalize_2d(ini_fit, normalize=normalize, sortx=sortx) + ini_fit_2d = [ marginalize_2d(ini_fit_i, normalize=normalize, sortx=sortx) for ini_fit_i in ini_fit ] fit_masked_2d = marginalize_2d(fit_masked, normalize=normalize, sortx=sortx) rebinned_data_2d = marginalize_2d(rebinned_data, normalize=normalize, bin_lengths=bins, recover_shape=True, sortx=sortx) rebinned_fit_2d = marginalize_2d(rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True, sortx=sortx) - ini_rebinned_fit_2d = marginalize_2d(ini_rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True, sortx=sortx) + ini_rebinned_fit_2d = [ marginalize_2d(ini_rebinned_fit_i, normalize=normalize, bin_lengths=bins, recover_shape=True, sortx=sortx) for ini_rebinned_fit_i in ini_rebinned_fit ] # show zero pixels as None data_2d = [d/(d!=0) for d in data_2d] fit_2d = [d/(d!=0) for d in fit_2d] - ini_fit_2d = [d/(d!=0) for d in ini_fit_2d] + ini_fit_2d = [[d/(d!=0) for d in ini_fit_2d_i] for ini_fit_2d_i in ini_fit_2d] fit_masked_2d = [d/(d!=0) for d in fit_masked_2d] rebinned_data_2d = [d/(d!=0) for d in rebinned_data_2d] rebinned_fit_2d = [d/(d!=0) for d in rebinned_fit_2d] @@ -2215,11 +2569,15 @@ class PeakHistogram(object): yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) - ax.add_patch(Ellipse((ini_mu[xind],ini_mu[yind]), 2*peak_std*ini_sigma_2d[i][0], 2*peak_std*ini_sigma_2d[i][1], ini_angle_2d[i], color='green', ls='-', fill=False)) + for j in range(len(ini_mu)): + ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[-1-len(ini_mu)+1+j], fill=False)) ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) if i==0: - ax.legend([f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + if len(ini_mu)==1: + ax.legend([f'initial', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + else: + ax.legend([f'initial']+[f'level {j-1}' for j in range(1,len(ini_mu))]+[f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') subfigs[subfig_no].suptitle('Data 2d marginals', fontsize='xx-large') @@ -2231,11 +2589,16 @@ class PeakHistogram(object): yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) - ax.add_patch(Ellipse((ini_mu[xind],ini_mu[yind]), 2*peak_std*ini_sigma_2d[i][0], 2*peak_std*ini_sigma_2d[i][1], ini_angle_2d[i], color='green', ls='-', fill=False)) + for j in range(len(ini_mu)): + ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[-1-len(ini_mu)+1+j], fill=False)) + # ax.add_patch(Ellipse((ini_mu[xind],ini_mu[yind]), 2*peak_std*ini_sigma_2d[i][0], 2*peak_std*ini_sigma_2d[i][1], ini_angle_2d[i], color='green', ls='-', fill=False)) ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) if i==0: - ax.legend([f'init. {peak_std} sigma', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + if len(ini_mu)==1: + ax.legend([f'initial', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + else: + ax.legend([f'initial']+[f'level {j-1}' for j in range(1,len(ini_mu))]+[f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') subfigs[subfig_no].suptitle('Fit 2d marginals', fontsize='xx-large') @@ -2256,10 +2619,15 @@ class PeakHistogram(object): yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(rebinned_data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) + for j in range(len(ini_mu)): + ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[-1-len(ini_mu)+1+j], fill=False)) ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) if i==0: - ax.legend([f'init. {peak_std} sigma',f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + if len(ini_mu)==1: + ax.legend([f'initial', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + else: + ax.legend([f'initial']+[f'level {j-1}' for j in range(1,len(ini_mu))]+[f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') subfigs[subfig_no].suptitle('Rebinned data 2d marginals', fontsize='xx-large') @@ -2271,11 +2639,15 @@ class PeakHistogram(object): yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(rebinned_fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) - ax.add_patch(Ellipse((ini_mu[xind],ini_mu[yind]), 2*peak_std*ini_sigma_2d[i][0], 2*peak_std*ini_sigma_2d[i][1], ini_angle_2d[i], color='green', ls='-', fill=False)) + for j in range(len(ini_mu)): + ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[-1-len(ini_mu)+1+j], fill=False)) ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) if i==0: - ax.legend([f'init. {peak_std} sigma',f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + if len(ini_mu)==1: + ax.legend([f'initial', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + else: + ax.legend([f'initial']+[f'level {j-1}' for j in range(1,len(ini_mu))]+[f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') subfigs[subfig_no].suptitle('Rebinned fit 2d marginals', fontsize='xx-large') @@ -2312,8 +2684,8 @@ class PeakHistogram(object): subfigs[subfig_no].suptitle('Fit error', fontsize='xx-large') subfigs[subfig_no].colorbar(im, ax=ax, location='right') - # ######################################## - # # plot detector mask + ######################################## + # plot detector mask # if detector_mask is not None: # detector_mask_2d = marginalize_2d(detector_mask, normalize=False) @@ -2335,6 +2707,9 @@ class PeakHistogram(object): plt.savefig(f'{plot_path}/{prefix}_peak.png') plt.close('all') + if _profile: + self._profile['plot'] = time.time() - start + def plot1(self, bins, plot_path='output', prefix=None, peak_std=4, bkgr_std=10, log=False): # create output directory for plots -- GitLab From a1ec648c3acdd49a94f28eec46e629d8bb48fa44 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Fri, 30 Dec 2022 16:19:04 -0500 Subject: [PATCH 20/26] do not rebin with bin = 1 --- peak_integration.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/peak_integration.py b/peak_integration.py index e1521c7..644b129 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -1758,7 +1758,8 @@ class PeakHistogram(object): edges = self.edges if bins is not None: # counts in each bin - data = rebin_histogram(data, bins, mode=rebin_mode) + if np.array(bins).ravel().max()>1: + data = rebin_histogram(data, bins, mode=rebin_mode) # edges along each dimension in the rebinned histogram edges = [ edges[d][np.cumsum([0]+list(bins[d]))] for d in range(self.ndims) ] # centers of the bins along each dimension -- GitLab From 98eabad77b738d88b4fd137e4e60f62f1825278c Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Sat, 31 Dec 2022 16:19:47 -0500 Subject: [PATCH 21/26] update bfgs --- peak_integration.py | 203 ++++++++++---------------------------------- 1 file changed, 47 insertions(+), 156 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 644b129..c7cb956 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -4,6 +4,7 @@ from pathlib import Path import numpy as np from scipy.optimize import minimize as scipy_minimize +from scipy.optimize._optimize import _prepare_scalar_function, _LineSearchError, _epsilon, _line_search_wolfe12, _status_message, OptimizeResult from scipy.linalg import svd from numpy.linalg import eig from scipy.special import loggamma, erf @@ -20,143 +21,24 @@ from mantid.kernel import V3D from mantid import config config['Q.convention'] = "Crystallography" -# from ellipsoid import EllipsoidTool -from scipy.optimize._optimize import _check_unknown_options, _prepare_scalar_function, vecnorm, _LineSearchError, _epsilon, _line_search_wolfe12, Inf, _status_message, OptimizeResult -from numpy import asarray - np.set_printoptions(precision=2, linewidth=200, formatter={'float':lambda x: f'{x:9.2e}'}) _debug_dir = 'debug' -_debug = False +_debug = True _profile = True +_verbose = False - -def fmin_bfgs_2(f, x0, fprime=None, args=(), gtol=1e-5, norm=Inf, - epsilon=_epsilon, maxiter=None, full_output=0, disp=1, - retall=0, callback=None, init_hess=None): +def minimize_bfgs(fun, x0, args=(), jac=None, hess=None, callback=None, + gtol=1e-5, norm=np.inf, eps=1.e-6, maxiter=None, maxfun=None, hess_reeval=20, + disp=False, return_all=False, finite_diff_rel_step=None, H0=None): """ - Minimize a function using the BFGS algorithm. - Parameters - ---------- - f : callable ``f(x,*args)`` - Objective function to be minimized. - x0 : ndarray - Initial guess. - fprime : callable ``f'(x,*args)``, optional - Gradient of f. - args : tuple, optional - Extra arguments passed to f and fprime. - gtol : float, optional - Gradient norm must be less than `gtol` before successful termination. - norm : float, optional - Order of norm (Inf is max, -Inf is min) - epsilon : int or ndarray, optional - If `fprime` is approximated, use this value for the step size. - callback : callable, optional - An optional user-supplied function to call after each - iteration. Called as ``callback(xk)``, where ``xk`` is the - current parameter vector. - maxiter : int, optional - Maximum number of iterations to perform. - full_output : bool, optional - If True, return ``fopt``, ``func_calls``, ``grad_calls``, and - ``warnflag`` in addition to ``xopt``. - disp : bool, optional - Print convergence message if True. - retall : bool, optional - Return a list of results at each iteration if True. - Returns - ------- - xopt : ndarray - Parameters which minimize f, i.e., ``f(xopt) == fopt``. - fopt : float - Minimum value. - gopt : ndarray - Value of gradient at minimum, f'(xopt), which should be near 0. - Bopt : ndarray - Value of 1/f''(xopt), i.e., the inverse Hessian matrix. - func_calls : int - Number of function_calls made. - grad_calls : int - Number of gradient calls made. - warnflag : integer - 1 : Maximum number of iterations exceeded. - 2 : Gradient and/or function calls not changing. - 3 : NaN result encountered. - allvecs : list - The value of `xopt` at each iteration. Only returned if `retall` is - True. - Notes - ----- - Optimize the function, `f`, whose gradient is given by `fprime` - using the quasi-Newton method of Broyden, Fletcher, Goldfarb, - and Shanno (BFGS). - See Also - -------- - minimize: Interface to minimization algorithms for multivariate - functions. See ``method='BFGS'`` in particular. - References - ---------- - Wright, and Nocedal 'Numerical Optimization', 1999, p. 198. - Examples - -------- - >>> from scipy.optimize import fmin_bfgs - >>> def quadratic_cost(x, Q): - ... return x @ Q @ x - ... - >>> x0 = np.array([-3, -4]) - >>> cost_weight = np.diag([1., 10.]) - >>> # Note that a trailing comma is necessary for a tuple with single element - >>> fmin_bfgs(quadratic_cost, x0, args=(cost_weight,)) - Optimization terminated successfully. - Current function value: 0.000000 - Iterations: 7 # may vary - Function evaluations: 24 # may vary - Gradient evaluations: 8 # may vary - array([ 2.85169950e-06, -4.61820139e-07]) - >>> def quadratic_cost_grad(x, Q): - ... return 2 * Q @ x - ... - >>> fmin_bfgs(quadratic_cost, x0, quadratic_cost_grad, args=(cost_weight,)) - Optimization terminated successfully. - Current function value: 0.000000 - Iterations: 7 - Function evaluations: 8 - Gradient evaluations: 8 - array([ 2.85916637e-06, -4.54371951e-07]) - """ - opts = {'gtol': gtol, - 'norm': norm, - 'eps': epsilon, - 'disp': disp, - 'maxiter': maxiter, - 'return_all': retall} - - res = _minimize_bfgs_2(f, x0, args, fprime, callback=callback, init_hess=init_hess, **opts) - return res - # if full_output: - # retlist = (res['x'], res['fun'], res['jac'], res['hess_inv'], - # res['nfev'], res['njev'], res['status']) - # if retall: - # retlist += (res['allvecs'], ) - # return retlist - # else: - # if retall: - # return res['x'], res['allvecs'] - # else: - # return res['x'] + Source: https://github.com/scipy/scipy/blob/v1.9.3/scipy/optimize/_optimize.py + Minimization of scalar function of one or more variables using the BFGS algorithm. -def _minimize_bfgs_2(fun, x0, args=(), jac=None, callback=None, - gtol=1e-5, norm=np.inf, eps=1.e-6, maxiter=None, - disp=False, return_all=False, finite_diff_rel_step=None, init_hess=None, - **unknown_options): - """ - Minimization of scalar function of one or more variables using the - BFGS algorithm. Options ------- disp : bool @@ -164,16 +46,14 @@ def _minimize_bfgs_2(fun, x0, args=(), jac=None, callback=None, maxiter : int Maximum number of iterations to perform. gtol : float - Gradient norm must be less than `gtol` before successful - termination. + Gradient norm must be less than `gtol` before successful termination. norm : float Order of norm (Inf is max, -Inf is min). eps : float or ndarray If `jac is None` the absolute step size used for numerical approximation of the jacobian via forward differences. return_all : bool, optional - Set to True to return a list of the best solution at each of the - iterations. + Set to True to return a list of the best solution at each of the iterations. finite_diff_rel_step : None or array_like, optional If `jac in ['2-point', '3-point', 'cs']` the relative step size to use for numerical approximation of the jacobian. The absolute step @@ -182,29 +62,30 @@ def _minimize_bfgs_2(fun, x0, args=(), jac=None, callback=None, the sign of `h` is ignored. If None (default) then step is selected automatically. """ - _check_unknown_options(unknown_options) retall = return_all - x0 = asarray(x0).flatten() + x0 = np.asarray(x0).flatten() if x0.ndim == 0: x0.shape = (1,) if maxiter is None: maxiter = len(x0) * 200 + if maxfun is None: + maxfun = len(x0) * 200 - sf = _prepare_scalar_function(fun, x0, jac, args=args, epsilon=eps, - finite_diff_rel_step=finite_diff_rel_step) + sf = _prepare_scalar_function(fun, x0, jac, args=args, epsilon=eps, finite_diff_rel_step=finite_diff_rel_step) - f = sf.fun - myfprime = sf.grad + f, df = sf.fun, sf.grad old_fval = f(x0) - gfk = myfprime(x0) + gfk = df(x0) k = 0 N = len(x0) I = np.eye(N, dtype=int) - if init_hess is not None: - Hk = init_hess + if H0 is not None: + Hk = np.linalg.pinv(H0) + elif hess is not None: + Hk = np.linalg.pinv(hess(x0)) else: Hk = I @@ -215,17 +96,24 @@ def _minimize_bfgs_2(fun, x0, args=(), jac=None, callback=None, if retall: allvecs = [x0] warnflag = 0 - gnorm = vecnorm(gfk, ord=norm) - while (gnorm > gtol) and (k < maxiter): + gnorm = np.linalg.norm(gfk, ord=norm) + hess_fvals = 0 + while (gnorm > gtol) and (k < maxiter) and (sf.nfev < maxfun): pk = -np.dot(Hk, gfk) try: - alpha_k, fc, gc, old_fval, old_old_fval, gfkp1 = \ - _line_search_wolfe12(f, myfprime, xk, pk, gfk, - old_fval, old_old_fval, amin=1e-100, amax=1e100) + alpha_k, fc, gc, old_fval, old_old_fval, gfkp1 = _line_search_wolfe12(f, df, xk, pk, gfk, old_fval, old_old_fval, amin=1e-100, amax=1e100) except _LineSearchError: - # Line search failed to find a better solution. - warnflag = 2 - break + # Line search failed to find a better solution + # retry with exact hessian + hess_fvals = 0 + Hk = np.linalg.pinv(hess(xk)) + pk = -np.dot(Hk, gfk) + try: + alpha_k, fc, gc, old_fval, old_old_fval, gfkp1 = _line_search_wolfe12(f, df, xk, pk, gfk, old_fval, old_old_fval, amin=1e-100, amax=1e100) + except _LineSearchError: + # break if failed again + warnflag = 2 + break xkp1 = xk + alpha_k * pk if retall: @@ -233,20 +121,19 @@ def _minimize_bfgs_2(fun, x0, args=(), jac=None, callback=None, sk = xkp1 - xk xk = xkp1 if gfkp1 is None: - gfkp1 = myfprime(xkp1) + gfkp1 = df(xkp1) - yk = gfkp1 - gfk + yk = gfkp1 - gfk gfk = gfkp1 if callback is not None: callback(xk) k += 1 - gnorm = vecnorm(gfk, ord=norm) + gnorm = np.linalg.norm(gfk, ord=norm) if (gnorm <= gtol): break if not np.isfinite(old_fval): - # We correctly found +-Inf as optimal value, or something went - # wrong. + # We correctly found +-Inf as optimal value, or something went wrong. warnflag = 2 break @@ -259,10 +146,14 @@ def _minimize_bfgs_2(fun, x0, args=(), jac=None, callback=None, else: rhok = 1. / rhok_inv - A1 = I - sk[:, np.newaxis] * yk[np.newaxis, :] * rhok - A2 = I - yk[:, np.newaxis] * sk[np.newaxis, :] * rhok - Hk = np.dot(A1, np.dot(Hk, A2)) + (rhok * sk[:, np.newaxis] * - sk[np.newaxis, :]) + hess_fvals += sf.nfev + if hess_fvals<hess_reeval: + A1 = I - sk[:, np.newaxis] * yk[np.newaxis, :] * rhok + A2 = I - yk[:, np.newaxis] * sk[np.newaxis, :] * rhok + Hk = np.dot(A1, np.dot(Hk, A2)) + (rhok * sk[:, np.newaxis] * sk[np.newaxis, :]) + else: + hess_fvals = 0 + Hk = np.linalg.pinv(hess(xk)) fval = old_fval -- GitLab From 6bb1e98ab97745fa35076448e30ad80af6dab67d Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Sat, 31 Dec 2022 18:13:36 -0500 Subject: [PATCH 22/26] update plot --- peak_integration.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/peak_integration.py b/peak_integration.py index c7cb956..4fd8644 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -2256,7 +2256,25 @@ class PeakHistogram(object): def plot(self, bins, plot_path='output', prefix=None, peak_std=4, bkgr_std=10, log=False): start = time.time() - styles = [':','-.','--','-'] + styles = OrderedDict( + [('solid', (0, ())), + # ('loosely dotted', (0, (1, 10))), + ('dotted', (0, (1, 5))), + ('densely dotted', (0, (1, 1))), + + # ('loosely dashed', (0, (5, 10))), + ('dashed', (0, (5, 5))), + ('densely dashed', (0, (5, 1))), + + # ('loosely dashdotted', (0, (3, 10, 1, 10))), + ('dashdotted', (0, (3, 5, 1, 5))), + ('densely dashdotted', (0, (3, 1, 1, 1))), + + # ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))), + ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), + ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1)))]) + styles = list(styles.values())[-1::-1] + # styles = [';',':','-.','--','-'] # create output directory for plots Path(plot_path).mkdir(parents=True, exist_ok=True) -- GitLab From 5ef7f6ed18125193e5962085ab505a7eb3b6cd04 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Sat, 31 Dec 2022 18:51:26 -0500 Subject: [PATCH 23/26] update plot --- peak_integration.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 4fd8644..36ca886 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -2258,9 +2258,6 @@ class PeakHistogram(object): styles = OrderedDict( [('solid', (0, ())), - # ('loosely dotted', (0, (1, 10))), - ('dotted', (0, (1, 5))), - ('densely dotted', (0, (1, 1))), # ('loosely dashed', (0, (5, 10))), ('dashed', (0, (5, 5))), @@ -2272,7 +2269,12 @@ class PeakHistogram(object): # ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))), ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), - ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1)))]) + ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1))), + + # ('loosely dotted', (0, (1, 10))), + ('dotted', (0, (1, 5))), + ('densely dotted', (0, (1, 1)))] + ) styles = list(styles.values())[-1::-1] # styles = [';',':','-.','--','-'] -- GitLab From 3360c2526dcc331f18f4f16dbb89e17bc4273d62 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Sat, 31 Dec 2022 18:57:16 -0500 Subject: [PATCH 24/26] update plot --- peak_integration.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 36ca886..6fca3fe 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -2256,25 +2256,25 @@ class PeakHistogram(object): def plot(self, bins, plot_path='output', prefix=None, peak_std=4, bkgr_std=10, log=False): start = time.time() - styles = OrderedDict( - [('solid', (0, ())), + styles = OrderedDict([ + ('solid', (0, ())), # ('loosely dashed', (0, (5, 10))), - ('dashed', (0, (5, 5))), ('densely dashed', (0, (5, 1))), + ('dashed', (0, (5, 5))), # ('loosely dashdotted', (0, (3, 10, 1, 10))), - ('dashdotted', (0, (3, 5, 1, 5))), ('densely dashdotted', (0, (3, 1, 1, 1))), + ('dashdotted', (0, (3, 5, 1, 5))), # ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))), - ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1))), + ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), # ('loosely dotted', (0, (1, 10))), - ('dotted', (0, (1, 5))), - ('densely dotted', (0, (1, 1)))] - ) + ('densely dotted', (0, (1, 1))), + ('dotted', (0, (1, 5))) + ]) styles = list(styles.values())[-1::-1] # styles = [';',':','-.','--','-'] -- GitLab From 96ca1c45e71b8c44850c75bcde5e74b752ac1e93 Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Sat, 31 Dec 2022 19:04:22 -0500 Subject: [PATCH 25/26] add profiling --- peak_integration.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/peak_integration.py b/peak_integration.py index 6fca3fe..5b86a98 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -2977,6 +2977,13 @@ class PeakHistogram(object): return intensity, sigma, peak_chi2, total_bkgr_intensity + def print_stat(self): + tot_time = sum(self._profile.values()) + + print('Profiling') + print('=========') + for key,val in self._profile.items(): + print(f'{key:>25s}: {val:.2e} s, {val/tot_time*100:5.2f} %') -- GitLab From b76572b616cdd980739254597cf54ce910daee1d Mon Sep 17 00:00:00 2001 From: Viktor Reshniak <revitmt@gmail.com> Date: Wed, 4 Jan 2023 10:20:09 -0500 Subject: [PATCH 26/26] Update peak_integration.py --- peak_integration.py | 945 ++++++++++++++++++++++---------------------- 1 file changed, 478 insertions(+), 467 deletions(-) diff --git a/peak_integration.py b/peak_integration.py index 5b86a98..ed488ff 100644 --- a/peak_integration.py +++ b/peak_integration.py @@ -26,8 +26,8 @@ config['Q.convention'] = "Crystallography" np.set_printoptions(precision=2, linewidth=200, formatter={'float':lambda x: f'{x:9.2e}'}) _debug_dir = 'debug' -_debug = True -_profile = True +_debug = False +_profile = False _verbose = False @@ -64,6 +64,8 @@ def minimize_bfgs(fun, x0, args=(), jac=None, hess=None, callback=None, """ retall = return_all + hess_eval = 0 + x0 = np.asarray(x0).flatten() if x0.ndim == 0: x0.shape = (1,) @@ -85,6 +87,7 @@ def minimize_bfgs(fun, x0, args=(), jac=None, hess=None, callback=None, if H0 is not None: Hk = np.linalg.pinv(H0) elif hess is not None: + hess_eval += 1 Hk = np.linalg.pinv(hess(x0)) else: Hk = I @@ -97,7 +100,7 @@ def minimize_bfgs(fun, x0, args=(), jac=None, hess=None, callback=None, allvecs = [x0] warnflag = 0 gnorm = np.linalg.norm(gfk, ord=norm) - hess_fvals = 0 + while (gnorm > gtol) and (k < maxiter) and (sf.nfev < maxfun): pk = -np.dot(Hk, gfk) try: @@ -105,7 +108,7 @@ def minimize_bfgs(fun, x0, args=(), jac=None, hess=None, callback=None, except _LineSearchError: # Line search failed to find a better solution # retry with exact hessian - hess_fvals = 0 + hess_eval += 1 Hk = np.linalg.pinv(hess(xk)) pk = -np.dot(Hk, gfk) try: @@ -146,13 +149,13 @@ def minimize_bfgs(fun, x0, args=(), jac=None, hess=None, callback=None, else: rhok = 1. / rhok_inv - hess_fvals += sf.nfev - if hess_fvals<hess_reeval: + if sf.nfev<hess_reeval: A1 = I - sk[:, np.newaxis] * yk[np.newaxis, :] * rhok A2 = I - yk[:, np.newaxis] * sk[np.newaxis, :] * rhok Hk = np.dot(A1, np.dot(Hk, A2)) + (rhok * sk[:, np.newaxis] * sk[np.newaxis, :]) else: hess_fvals = 0 + hess_eval += 1 Hk = np.linalg.pinv(hess(xk)) fval = old_fval @@ -174,6 +177,7 @@ def minimize_bfgs(fun, x0, args=(), jac=None, hess=None, callback=None, print(" Iterations: %d" % k) print(" Function evaluations: %d" % sf.nfev) print(" Gradient evaluations: %d" % sf.ngev) + print(" Inv. hessian evaluations: %d" % hess_eval) result = OptimizeResult(fun=fval, jac=gfk, hess_inv=Hk, nfev=sf.nfev, njev=sf.ngev, status=warnflag, @@ -792,10 +796,9 @@ class Gaussian(object): @property def semiaxes(self): if self.parameterization=='givens': - return self.sqrtD + return 1 / self.sqrtD else: - _,d,_ = svd(self.sqrtP) - return d + return 1 / svd(self.sqrtP)[1] def check_parameters(self, params): @@ -1509,27 +1512,32 @@ class Loss(object): class MLELoss(Loss): - def __init__(self, *args, **kwargs): + def __init__(self, scale=None, *args, **kwargs): super().__init__(*args, **kwargs) # usefull constant to make optimal loss closer to 0 # it can affect relative convergence criteria - pos_data = self.data[self.data>0] - self.constant = -(pos_data-pos_data*np.log(pos_data)).sum() + # pos_data = self.data[self.data>0] + # self.constant = -(pos_data-pos_data*np.log(pos_data)).sum() + if scale is None: + self.scale = 1 #self.data.size + else: + self.scale = scale def loss(self, params): fit = self.fit(params) - return (fit-self.data*np.log(fit)).sum() + self.constant + return (fit-self.data*np.log(fit)).sum() / self.scale #+ self.constant @property def dloss_dfit(self): self._dloss_dfit = 1 - self.data / self._fit + self._dloss_dfit /= self.scale return self._dloss_dfit @property def d2loss_dfit2(self): - return self.data / self._fit**2 + return self.data / self._fit**2 / self.scale - def minimize(self, solver, tol, maxiter, maxfun, params_init, params_lbnd=None, params_ubnd=None, disp=_debug): + def minimize(self, solver, tol, maxiter, maxfun, params_init, params_lbnd=None, params_ubnd=None, H0=None, disp=_debug): # from scipy.optimize import BFGS # class myBFGS(BFGS): # def initialize(self, n, approx_type): @@ -1537,21 +1545,23 @@ class MLELoss(Loss): # if self.approx_type == 'hess': # self.B = loss_fun.hessian(params_init) # else: - # self.H = np.linalg.inv(loss_fun.hessian(params_init)) - if solver=='Newton-CG': + # self.H = np.linalg.pinv(loss_fun.hessian(params_init)) + # hess = hessian if hessian is not None else self.hessian + if solver=='BFGS': + return minimize_bfgs(self, params_init, jac=self.gradient, hess=self.hessian, gtol=tol, maxiter=maxiter, maxfun=maxfun, hess_reeval=20, disp=disp, H0=H0) + elif solver=='Newton-CG': options = {'maxiter':maxiter, 'xtol':tol, 'disp':disp} + bounds = None hess = self.hessian # hess = myBFGS(), - elif solver=='BFGS': - options = {'maxiter':maxiter, 'gtol':tol, 'disp':disp} - hess = None elif solver=='L-BFGS-B': options = {'maxiter':maxiter, 'maxfun':maxfun, 'maxls':100, 'maxcor':100, 'ftol':1.e-10, 'gtol':tol, 'xtol':1.e-10, 'disp':disp} + bounds = tuple(zip(params_lbnd,params_ubnd)) if params_lbnd is not None and params_ubnd is not None else None hess = None return scipy_minimize(self, jac = self.gradient, hess = hess, - x0=params_init, bounds=tuple(zip(params_lbnd,params_ubnd)) if params_lbnd is not None and params_ubnd is not None else None, + x0=params_init, bounds=bounds, method=solver, options=options, # method='L-BFGS-B', options={'maxiter':1000, 'maxfun':1000, 'maxls':20, 'maxcor':100, 'ftol':1.e-6, 'gtol':1.e-6, 'disp':True} # method='BFGS', options={'maxiter':1000, 'gtol':1.e-6, 'norm':np.inf, 'disp':True} @@ -1583,12 +1593,18 @@ class PeakHistogram(object): # histogram array self.data = hist_ws.getNumEventsArray().copy() #/ 8 + # self.scale = self.data.max() / 1000 + # self.data /= self.scale # self.data -= self.data.mean() + 0.5 * (self.data.max() - self.data.mean()) # self.data[self.data<0] = 0 # from scipy.ndimage import gaussian_filter # self.data = gaussian_filter(self.data, sigma=2) + # total number of events in the histogram + # self.nevents = hist_ws.getNEvents() + self.nevents = self.data.sum().astype(int) + # limits of the box along each dimension self.limits = [(hd.getMinimum(), hd.getMaximum()) for hd in (hist_ws.getDimension(i) for i in range(self.ndims))] @@ -1626,10 +1642,18 @@ class PeakHistogram(object): self._profile = {} # track initialization + # self.loss_scale = None self.init_loss = [] self.init_params = [] + def __call__(self, params, x): + bkgr_params = params[:self.bkgr_fun.nparams] + peak_params = params[self.bkgr_fun.nparams:] + x = x.reshape((-1,self.ndims)) + return (self.bkgr_fun(bkgr_params, x) + self.peak_fun(peak_params, x)) + + def get_grid_data(self, bins=None, rebin_mode='density', return_edges=False): '''Extract coordinates of the bins and bin counts from the histogram workspace @@ -1664,7 +1688,7 @@ class PeakHistogram(object): return data, points - def initialize(self, points, data, ellipsoid=True, detector_mask=None): + def initialize(self, points, data, ellipsoid=None, detector_mask=None): start = time.time() ################################### @@ -1705,23 +1729,36 @@ class PeakHistogram(object): # estimate covariance matrix by fitting maximum enclosing ellipsoid start1 = time.time() - if ellipsoid: - # bin centers inside the peak enclosing sphere - ellpoints = points[data>(thresh_val+thresh_add),...] - - # replace centers of bins with corners - cnt2corner = 0.5 * np.sqrt(0.5) * self.resolution - ellpoints = np.vstack(( - ellpoints + cnt2corner * np.array([1,1,1]).reshape((1,3)), - ellpoints + cnt2corner * np.array([-1,1,1]).reshape((1,3)), - ellpoints + cnt2corner * np.array([1,-1,1]).reshape((1,3)), - ellpoints + cnt2corner * np.array([1,1,-1]).reshape((1,3)), - ellpoints + cnt2corner * np.array([-1,-1,1]).reshape((1,3)), - ellpoints + cnt2corner * np.array([-1,1,-1]).reshape((1,3)), - ellpoints + cnt2corner * np.array([1,-1,-1]).reshape((1,3)), - ellpoints + cnt2corner * np.array([-1,-1,-1]).reshape((1,3)) - )) - ellcnt, ellrad, ellrot = MinVolEllipsoid(ellpoints, tolerance=0.01, maxit=10) + if ellipsoid is not None: + if ellipsoid=='minvol': + # bin centers inside the peak enclosing sphere + ellpoints = points[data>(thresh_val+thresh_add),...] + + # replace centers of bins with corners + cnt2corner = 0.5 * np.sqrt(0.5) * self.resolution + ellpoints = np.vstack(( + ellpoints + cnt2corner * np.array([1,1,1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([-1,1,1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([1,-1,1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([1,1,-1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([-1,-1,1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([-1,1,-1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([1,-1,-1]).reshape((1,3)), + ellpoints + cnt2corner * np.array([-1,-1,-1]).reshape((1,3)) + )) + ellcnt, ellrad, ellrot = MinVolEllipsoid(ellpoints, tolerance=0.01, maxit=10) + + elif ellipsoid=='pca': + from sklearn.decomposition import PCA + mask = data > (thresh_val+thresh_add) + samples = self.resolution[np.newaxis,:] * np.random.rand(data[mask].sum().astype(int), self.ndims) - self.resolution/2 + points = np.repeat(points[mask,:],data[mask].astype(int),axis=0) + samples + pca = PCA(n_components=self.ndims, svd_solver='full') + pca.fit(points) + + ellcnt = cnt_init.ravel() + ellrad = np.sqrt(pca.explained_variance_) * np.sqrt(2*np.log((data_max-thresh_val-thresh_add)/thresh_add)) + ellrot = pca.components_ else: ellcnt = cnt_init.ravel() ellrad = np.array([thresh_rad]*self.ndims) @@ -1756,7 +1793,8 @@ class PeakHistogram(object): _, _, edges = self.get_grid_data(return_edges=True) # full 3d covariance - cov_3d = ellrot.T @ (ellrad[:,np.newaxis]**2*ellrot) + scale4std = np.sqrt(2*np.log((data_max-thresh_val-thresh_add)/thresh_add)) + cov_3d = ellrot.T @ ((scale4std*ellrad[:,np.newaxis])**2*ellrot) # covariances and ellipsoids of 2d marginals cov_2d = [] @@ -1981,7 +2019,7 @@ class PeakHistogram(object): return output - def fit_multilevel(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='BFGS', tol=1.e-6, plot_intermediate=False): + def fit_multilevel(self, min_level=4, return_bins=False, loss='mle', solver0='BFGS', solver='BFGS', tol=1.e-6): # shape of the largest subhistogram with shape as a power of 2 shape2 = [2**int(np.log2(self.shape[d])) for d in range(self.ndims)] @@ -1992,7 +2030,12 @@ class PeakHistogram(object): solver1 = solver0 params = params_lbnd = params_ubnd = None + self.lev_bins = [] for p in range(min_level,minpow2+1): + # if _debug: + print('\n\n==============================================================') + print(f'Level {p}: {2**p} bins\n\n') + start = time.time() # left, middle (with power of 2 shape) and right bins @@ -2006,65 +2049,85 @@ class PeakHistogram(object): # bins at the current level bins = [ ([bl] if bl>0 else [])+bm+([br] if br>0 else []) for bl,bm,br in zip(binsl,binsm,binsr)] - # fit histogram at the current level - output = self.fit(bins=bins, return_bins=return_bins, loss=loss, solver=solver1, tol=tol, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) - params, sucess = output[0], output[1] - # skip level if fit not successful - # if not sucess and p<minpow2: - # params = params_lbnd = params_ubnd = None - # continue + ####################################################################### + # update bounds - # solver at level>0 - solver1 = solver + if p>min_level: + # data_max - ####################################################################### - # refine search bounds + ###################################### + # # bounds for the intensity + # intst_lbnd = - # bounds for the center - # search radius at next level is 3 voxels at the current level - dcnt = [ 3 * res*2**(minpow2-p) for res in self.resolution] - cnt_lbnd = [c-dc for c,dc in zip(self.peak_fun.center,dcnt)] - cnt_ubnd = [c+dc for c,dc in zip(self.peak_fun.center,dcnt)] + ###################################### + # bounds for the center + # search radius is 6 voxels at the current level + dcnt = [ 6 * res*2**(minpow2-p) for res in self.resolution] + cnt_lbnd = [c-dc for c,dc in zip(self.peak_fun.center,dcnt)] + cnt_ubnd = [c+dc for c,dc in zip(self.peak_fun.center,dcnt)] - ###################################### - # bounds for the precision matrix + ###################################### + # bounds for the precision matrix - if self.parameterization=='givens': - # bounds for ellipsoid angles - if minpow2>min_level: + if self.parameterization=='givens': + # bounds for ellipsoid angles phi0 = np.pi/1 - phi1 = np.pi/8 + phi1 = np.pi/1 dphi = phi0 + (p+1-min_level)/(minpow2+1-min_level) * (phi1-phi0) + # + angles_lbnd = [phi-dphi for phi in self.peak_fun.angles] + angles_ubnd = [phi+dphi for phi in self.peak_fun.angles] + + # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix + peak_std = 4 + # max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius + max_rads = list(1/self.peak_fun.semiaxes) # largest 'peak_std' radius is radius from previous level + min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size + + prec_lbnd = angles_lbnd + [ 1/r for r in max_rads] + prec_ubnd = angles_ubnd + [ 1/r for r in min_rads] else: - dphi = np.pi - angles_lbnd = [max(-np.pi,phi-dphi) for phi in self.peak_fun.angles] - angles_ubnd = [min( np.pi,phi+dphi) for phi in self.peak_fun.angles] - - # bounds on the semiaxes of the `peak_std` ellipsoid: standard deviations (square roots of the eigenvalues) of the covariance matrix - peak_std = 4 - max_rads = [ 5/6*rad/peak_std for rad in self.radiuses] # largest 'peak_std' radius is 5/6 of the box radius - min_rads = [ 1/2*res/peak_std for res in self.resolution] # smallest 'peak_std' radius is 1/2 of the smallest bin size - prec_lbnd = angles_lbnd + [ 1/r for r in max_rads] #[ max((self.limits[d][1]-self.limits[d][0])/4/(4/3),invrads[d]/2.0) for d in range(self.ndims)] - prec_ubnd = angles_ubnd + [ 1/r for r in min_rads] #[ 100*r for r in invrads] + prec_lbnd = [-np.inf] * self.peak_fun.ncov + prec_ubnd = [ np.inf] * self.peak_fun.ncov + + # bounds for all parameters + # params_lbnd = [np.abs(sqrtbkgr)/2, np.abs(sqrtintst)/2] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] + # params_ubnd = [2*np.abs(sqrtbkgr), 2*np.abs(sqrtintst)] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] + params_lbnd = [-np.inf, -np.inf] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] + params_ubnd = [ np.inf, np.inf] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] + + + ####################################################################### + + # fit histogram at the current level + self.lev_bins.append(f'{2**p:d}_1') + output = self.fit(bins=bins, return_bins=return_bins, loss=loss, solver=solver1, tol=tol, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) + _, sucess = output[0], output[1] + + if sucess: + params = output[0] else: - prec_lbnd = [-np.inf] * self.peak_fun.ncov - prec_ubnd = [ np.inf] * self.peak_fun.ncov + self.lev_bins[-1] = f'{2**p:d}_2' + self.lev_bins.append(f'{2**p:d}_1') + output = self.fit(bins=bins, return_bins=return_bins, loss=loss, solver='Newton-CG', tol=1.e-3, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) + params, sucess = output[0], output[1] + # params = params_lbnd = params_ubnd = None + # continue + if not sucess: + return output - # bounds for all parameters - # params_lbnd = [np.abs(sqrtbkgr)/2, np.abs(sqrtintst)/2] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] - # params_ubnd = [2*np.abs(sqrtbkgr), 2*np.abs(sqrtintst)] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] - # params_lbnd = [-np.inf, -np.inf] + cnt_lbnd + prec_lbnd #+ [0 for sk in skewness] - # params_ubnd = [ np.inf, np.inf] + cnt_ubnd + prec_ubnd #+ [0 for sk in skewness] + # solver at level>0 + solver1 = solver + # self.loss_scale *= 2**self.ndims ####################################################################### if _profile: - self._profile[f'fit {2**p:3d} bins'] = time.time() - start + self._profile[f'fit {self.lev_bins[-1][:-2]} bins'] = time.time() - start - if plot_intermediate: + if _debug: self.plot(bins, prefix=f"level_{p}", log=False) - # plot_fit(hist_ws, params, bins, prefix=f"{p}", peak_id=1074, peak_hkl=[2.0,-2.0,-9.0], peak_std=4, bkgr_std=7, detector_mask=None, log=True) # start = time.time() # output = self.fit(return_bins=return_bins, loss=loss, solver=solver, covariance_parameterization=covariance_parameterization, params_init=params, params_lbnd=params_lbnd, params_ubnd=params_ubnd) @@ -2142,21 +2205,10 @@ class PeakHistogram(object): fit_data = fit_data[mask.ravel()] fit_points = fit_points[mask.ravel(),:] - ########################################################################### - # initialization and bounds on parameters - - # if (params_init is None) or (params_lbnd is None) or (params_ubnd is None): - if params_init is None: - params_init, params_lbnd, params_ubnd = self.initialize(fit_points, fit_data) - # else: - # params_tmp = params_init - # params_init, params_lbnd, params_ubnd = self.initialize(fit_points, fit_data) - # params_init[5:] = params_tmp[5:] - ########################################################################### + # loss function - # residual to fit densities of the bins in the rebinned histogram if loss=='pearson_chi': def residual(params): fit = params[0]**2 @@ -2177,69 +2229,130 @@ class PeakHistogram(object): x0=params_init, bounds=[params_lbnd,params_ubnd], method='trf', verbose=0, max_nfev=1000) elif loss=='mle': loss_fun = MLELoss(bkgr_fun=self.bkgr_fun, peak_fun=self.peak_fun, points=fit_points, data=fit_data) - result = loss_fun.minimize(solver, tol, maxiter=100, maxfun=100, - params_init=params_init, - params_lbnd=params_lbnd, - params_ubnd=params_ubnd, - disp=True) - self.init_params.append(np.array(params_init)) - self.init_loss.append(loss_fun(params_init)) - self.fit_params = result.x - self.loss = loss_fun(result.x) + ########################################################################### + # initialization and bounds on parameters - if _debug: - print(f'\nConverged: {result.success}') - - print('\nParameters') - print('----------') - print(f'\nInitial params: {np.array(params_init)}') - print(f' Final params: {result.x}') - - print('\nLoss') - print('----') - print(f'\nInitial loss: {self.init_loss[-1]:.2f}') - print(f' Final loss: {self.loss:.2f}') - - start = time.time() - dg = loss_fun.gradient(self.fit_params) - dg_time = time.time()-start - - start = time.time() - fddg = numerical_gradient(self.fit_params, lambda x: loss_fun(x)) - fddg_time = time.time()-start - - print('\nGradient') - print('--------') - print(f'Exact: {dg}') - print(f' FD: {fddg}') - print(f'Max. diff: {np.abs(dg-fddg).max():.3e}') - print(f'Rel. diff: {np.abs((dg-fddg)/(dg+1.e-10)).max():.3e}') - print(f'Exact time: {dg_time:.3f} sec') - print(f' FD time: {fddg_time:.3f} sec, {fddg_time/dg_time:.2f} slower') - - start = time.time() - d2g = loss_fun.hessian(self.fit_params) - d2g_time = time.time()-start - - start = time.time() - fdd2g = numerical_hessian(self.fit_params, lambda x: loss_fun(x)) - fdd2g_time = time.time()-start - - print('\nHessian') - print('-------') - print(f'Exact: \n{d2g}') - print(f'FD: \n{fdd2g}') - # print(f'Exact: \n{d2g[:4,:4]}') - # print(f'FD: \n{fdd2g[:4,:4]}') - # print(f'Exact: \n{d2g[4:,4:]}') - # print(f'FD: \n{fdd2g[4:,4:]}') - print(f'Max. diff: {np.abs(d2g-fdd2g).max():.3e}') - print(f'Rel. diff: {np.abs((d2g-fdd2g)/(d2g+1.e-10)).max():.3e}') - print(f'Exact time: {d2g_time:.3f} sec') - print(f' FD time: {fdd2g_time:.3f} sec, {fdd2g_time/d2g_time:.2f} slower') - # exit() + # if (params_init is None) or (params_lbnd is None) or (params_ubnd is None): + if params_init is None: + params_init, params_lbnd, params_ubnd = self.initialize(fit_points, fit_data, ellipsoid='minvol')#, detector_mask=self.detector_mask) + # loss_fun.scale = 0.01 * loss_fun(params_init) + # self.loss_scale = loss_fun.scale + else: + # default initialization + params_default, _, _ = self.initialize(fit_points, fit_data, ellipsoid='minvol') + + if loss_fun(params_init)>loss_fun(params_default): + params_init = params_default + + # # choose best parameters from default and given initializations + # def choose_params(param1, param2, loss1, ind): + # param = param1.copy() + # param[ind] = param2[ind] + # loss = loss_fun(param) + # if loss < loss1: + # param1[ind] = param2[ind] + # loss1 = loss + # return loss1 + # # def choose_params(param1, param2, ind): + # # if loss_fun(param1) < loss_fun(param2): + # # param2[ind] = param1[ind] + # # else: + # # param1[ind] = param2[ind] + + # # default initialization + # params_default, _, _ = self.initialize(fit_points, fit_data, ellipsoid='minvol') + + # best_loss = loss_fun(params_init) + # # ellipsoid + # best_loss = choose_params(params_init, params_default, best_loss, slice(self.bkgr_fun.nparams+self.peak_fun.nintst+self.peak_fun.ncnt,None)) + # # center + # best_loss = choose_params(params_init, params_default, best_loss, slice(self.bkgr_fun.nparams+self.peak_fun.nintst,self.bkgr_fun.nparams+self.peak_fun.nintst+self.peak_fun.ncnt)) + # # intensity + # best_loss = choose_params(params_init, params_default, best_loss, slice(self.bkgr_fun.nparams,self.bkgr_fun.nparams+self.peak_fun.nintst)) + # # background + # best_loss = choose_params(params_init, params_default, best_loss, slice(0,self.bkgr_fun.nparams)) + + ########################################################################### + # optimal parameters + + loss_fun.scale = 1.e0 * np.abs(loss_fun(params_init)) + # hessian = None + # if hasattr(self, 'prev_loss_fun'): + # self.prev_loss_fun.scale = loss_fun.scale / 8 + # self.prev_loss_fun.peak_fun = Gaussian(ndims=self.ndims, parameterization=self.peak_fun.parameterization) + # self.prev_loss_fun.bkgr_fun = Polynomial(ndims=self.ndims, order=0) + # hessian = self.prev_loss_fun.hessian + + result = loss_fun.minimize(solver, tol, maxiter=100, maxfun=100, + params_init=params_init, + params_lbnd=params_lbnd, + params_ubnd=params_ubnd, + # hessian=hessian, + disp=True) + + # self.prev_loss_fun = loss_fun + + self.init_params.append(np.array(params_init)) + self.init_loss.append(loss_fun(params_init)) + + self.fit_params = result.x + self.loss = loss_fun(result.x) + + ########################################################################### + + if _debug: + print(f'\n\nConverged: {result.success}') + print('---------') + + print('\n\nParameters') + print('----------') + print(f'Initial: {np.array(params_init)}') + print(f' Final: {result.x}') + + print('\n\nLoss') + print('----') + print(f'Initial: {self.init_loss[-1]:.2f}') + print(f' Final: {self.loss:.2f}') + + start1 = time.time() + dg = loss_fun.gradient(self.fit_params) + dg_time = time.time()-start1 + + start1 = time.time() + fddg = numerical_gradient(self.fit_params, lambda x: loss_fun(x)) + fddg_time = time.time()-start1 + + print('\n\nGradient') + print('--------') + print(f'Exact: {dg}') + print(f' FD: {fddg}') + print(f'Max. diff: {np.abs(dg-fddg).max():.3e}') + print(f'Rel. diff: {np.abs((dg-fddg)/(dg+1.e-10)).max():.3e}') + print(f'Exact time: {dg_time:.3f} sec') + print(f' FD time: {fddg_time:.3f} sec, {fddg_time/dg_time:.2f} slower') + + start1 = time.time() + d2g = loss_fun.hessian(self.fit_params) + d2g_time = time.time()-start1 + + start1 = time.time() + fdd2g = numerical_hessian(self.fit_params, lambda x: loss_fun(x)) + fdd2g_time = time.time()-start1 + + print('\n\nHessian') + print('-------') + print(f'Exact: \n{d2g}') + print(f'FD: \n{fdd2g}') + # print(f'Exact: \n{d2g[:4,:4]}') + # print(f'FD: \n{fdd2g[:4,:4]}') + # print(f'Exact: \n{d2g[4:,4:]}') + # print(f'FD: \n{fdd2g[4:,4:]}') + print(f'Max. diff: {np.abs(d2g-fdd2g).max():.3e}') + print(f'Rel. diff: {np.abs((d2g-fdd2g)/(d2g+1.e-10)).max():.3e}') + print(f'Exact time: {d2g_time:.3f} sec') + print(f' FD time: {fdd2g_time:.3f} sec, {fdd2g_time/d2g_time:.2f} slower') if _profile: if 'fit' in self._profile.keys(): @@ -2253,36 +2366,37 @@ class PeakHistogram(object): return self.fit_params, result.success - def plot(self, bins, plot_path='output', prefix=None, peak_std=4, bkgr_std=10, log=False): + def plot_marginals(self, bins, plot_path='output', prefix=None, peak_std=4, bkgr_std=10, log=False, show_empty=False): start = time.time() + # create output directory for plots + Path(plot_path).mkdir(parents=True, exist_ok=True) + + ####################################################################### + # plot options + styles = OrderedDict([ - ('solid', (0, ())), + ('128_1', (0, ())), + ('128_2', (0, (10,2))), # ('loosely dashed', (0, (5, 10))), - ('densely dashed', (0, (5, 1))), - ('dashed', (0, (5, 5))), + ('64_1', (0, (5, 1))), + ('64_2', (0, (5, 3))), # ('loosely dashdotted', (0, (3, 10, 1, 10))), - ('densely dashdotted', (0, (3, 1, 1, 1))), - ('dashdotted', (0, (3, 5, 1, 5))), + ('32_1', (0, (3, 1, 1, 1))), + ('32_2', (0, (3, 3, 1, 3))), # ('loosely dashdotdotted', (0, (3, 10, 1, 10, 1, 10))), - ('densely dashdotdotted', (0, (3, 1, 1, 1, 1, 1))), - ('dashdotdotted', (0, (3, 5, 1, 5, 1, 5))), + ('16_1', (0, (3, 1, 1, 1, 1, 1))), + ('16_2', (0, (3, 3, 1, 3, 1, 3))), - # ('loosely dotted', (0, (1, 10))), - ('densely dotted', (0, (1, 1))), - ('dotted', (0, (1, 5))) - ]) - styles = list(styles.values())[-1::-1] - # styles = [';',':','-.','--','-'] + # # ('loosely dotted', (0, (1, 10))), + # ('8_1', (0, (1, 1))), + # ('8_2', (0, (1, 5))) - # create output directory for plots - Path(plot_path).mkdir(parents=True, exist_ok=True) - - ####################################################################### - # plot options + ('initial', (0, (1,1))) + ]) # normalize plots normalize = False @@ -2342,7 +2456,8 @@ class PeakHistogram(object): ini_sigma_2d[j].append( np.sqrt(eigi) ) # fitted model - ini_fit.append( ini_bkgr_params[0]**2 + self.peak_fun(ini_peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) ) + ini_fit.append( self(init_params, points).reshape(data.shape) ) + # ini_fit.append( ini_bkgr_params[0]**2 + self.peak_fun(ini_peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) ) ini_fit_masked.append( (1 if self.detector_mask is None else self.detector_mask) * ini_fit ) # rebinned fit @@ -2379,7 +2494,8 @@ class PeakHistogram(object): sigma_2d.append( np.sqrt(eigi) ) # fitted model - fit = bkgr_params[0]**2 + self.peak_fun(peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) + fit = self(self.fit_params, points).reshape(data.shape) + # fit = bkgr_params[0]**2 + self.peak_fun(peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) fit_masked = (1 if self.detector_mask is None else self.detector_mask) * fit # rebinned data and fit @@ -2390,7 +2506,7 @@ class PeakHistogram(object): ####################################################################### # plot - fig = plt.figure(constrained_layout=True, figsize=(20,45)) + fig = plt.figure(constrained_layout=True, figsize=(20,45), dpi=100) subfigs = fig.subfigures(7,1) #wspace=0.07 subfig_no=-1 @@ -2405,39 +2521,39 @@ class PeakHistogram(object): subfig_no+=1 for i,ax in enumerate(subfigs[subfig_no].subplots(1,3,sharey=True)): - ax.stairs(data_1d[i], edges=edges[i], fill=True, alpha=0.3) - if np.all(np.array([d.size for d in data_1d])!=np.array([d.size for d in rebinned_data_1d])): - ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, color='b', baseline=None) - for j in range(len(ini_fit_1d)): - ax.plot(dim_points[i], ini_fit_1d[j][i], color='g', ls=styles[-1-len(ini_fit_1d)+1+j]) - ax.plot(dim_points[i], fit_1d[i], color='black') - ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') + # original data + ax.stairs(data_1d[i], edges=edges[i], fill=True, alpha=0.3, label='orig. data') + # rebinned data if any + if not np.all([d1.size==d2.size for d1,d2 in zip(data_1d,rebinned_data_1d)]): + ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, color='b', baseline=None, label='reb. data') + # initial fit + ax.plot(dim_points[i], ini_fit_1d[0][i], color='g', ls=styles['initial'], label='init. fit') + # level fits + for j in range(1,len(ini_fit_1d)): + ax.plot(dim_points[i], ini_fit_1d[j][i], color='g', ls=styles[self.lev_bins[j-1]], label=f'fit with {self.lev_bins[j-1][:-2]} bins') + # final fit + ax.plot(dim_points[i], fit_1d[i], color='black', label='final fit') if i==0: - if np.all(np.array([d.size for d in data_1d])!=np.array([d.size for d in rebinned_data_1d])): - data_reb_data = ['orig. data','reb. data'] - else: - data_reb_data = ['orig. data'] - if len(ini_fit_1d)==1: - ax.legend(data_reb_data+['init. fit','final fit'], framealpha=1.0, fontsize='xx-large') - else: - ax.legend(data_reb_data+['init. fit']+[f'fit at level {j-1}' for j in range(1,len(ini_fit_1d))]+['final fit'], framealpha=1.0, fontsize='xx-large') - # ax.legend(['orig. data','reb. data']+(['init. fit'] if len(ini_fit_1d)==0 else [f'init. fit {j}' for j in range(len(ini_fit_1d))])+['final fit'], framealpha=1.0, fontsize='xx-large') + ax.legend(framealpha=1.0, fontsize='x-large') + ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') ax.set_box_aspect(1) subfigs[subfig_no].suptitle('Data, initial and final fits', fontsize='xx-large') subfig_no+=1 for i,ax in enumerate(subfigs[subfig_no].subplots(1,3,sharey=True)): - ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=True, alpha=0.3) - ax.stairs(rebinned_fit_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, baseline=None) - ax.plot(dim_points[i], fit_1d[i], '-', marker='.', color='black') - ax.vlines([mu[i]-peak_std*np.sqrt(cov_3d[i,i]),mu[i]+peak_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-') - ax.vlines([mu[i]-bkgr_std*np.sqrt(cov_3d[i,i]),mu[i]+bkgr_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-.') + ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=True, alpha=0.3, label='reb. data') + ax.stairs(rebinned_fit_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, baseline=None, label='reb. fit') + ax.plot(dim_points[i], fit_1d[i], '-', marker='.', color='black', label='fit') + ax.vlines([mu[i]-peak_std*np.sqrt(cov_3d[i,i]),mu[i]+peak_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-', label=f'{peak_std} sigma') + ax.vlines([mu[i]-bkgr_std*np.sqrt(cov_3d[i,i]),mu[i]+bkgr_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-.', label=f'{bkgr_std} sigma') ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') if i==0: - ax.legend(['reb. data','reb. fit','fit', f'{peak_std} sigma', f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.legend(framealpha=1.0, fontsize='x-large') + # ax.legend(['reb. data','reb. fit','fit', f'{peak_std} sigma', f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') ax.set_box_aspect(1) subfigs[subfig_no].suptitle('Fits with peak bounds', fontsize='xx-large') + ######################################## # plot 2d marginals @@ -2450,12 +2566,13 @@ class PeakHistogram(object): ini_rebinned_fit_2d = [ marginalize_2d(ini_rebinned_fit_i, normalize=normalize, bin_lengths=bins, recover_shape=True, sortx=sortx) for ini_rebinned_fit_i in ini_rebinned_fit ] # show zero pixels as None - data_2d = [d/(d!=0) for d in data_2d] - fit_2d = [d/(d!=0) for d in fit_2d] - ini_fit_2d = [[d/(d!=0) for d in ini_fit_2d_i] for ini_fit_2d_i in ini_fit_2d] - fit_masked_2d = [d/(d!=0) for d in fit_masked_2d] - rebinned_data_2d = [d/(d!=0) for d in rebinned_data_2d] - rebinned_fit_2d = [d/(d!=0) for d in rebinned_fit_2d] + if not show_empty: + data_2d = [d/(d!=0) for d in data_2d] + fit_2d = [d/(d!=0) for d in fit_2d] + ini_fit_2d = [[d/(d!=0) for d in ini_fit_2d_i] for ini_fit_2d_i in ini_fit_2d] + fit_masked_2d = [d/(d!=0) for d in fit_masked_2d] + rebinned_data_2d = [d/(d!=0) for d in rebinned_data_2d] + rebinned_fit_2d = [d/(d!=0) for d in rebinned_fit_2d] if log: data_2d = np.log(data_2d) @@ -2481,15 +2598,13 @@ class PeakHistogram(object): yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) - for j in range(len(ini_mu)): - ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[-1-len(ini_mu)+1+j], fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) + ax.add_patch(Ellipse((ini_mu[0][xind],ini_mu[0][yind]), 2*peak_std*ini_sigma_2d[0][i][0], 2*peak_std*ini_sigma_2d[0][i][1], ini_angle_2d[0][i], color='green', ls=styles['initial'], fill=False, label='initial')) + for j in range(1,len(ini_mu)): + ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[self.lev_bins[j-1]], fill=False, label=f'{self.lev_bins[j-1][:-2]} bins')) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False, label=f'{peak_std} sigma')) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False, label=f'{bkgr_std} sigma')) if i==0: - if len(ini_mu)==1: - ax.legend([f'initial', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - else: - ax.legend([f'initial']+[f'level {j-1}' for j in range(1,len(ini_mu))]+[f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.legend(framealpha=1.0, fontsize='x-large') ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') subfigs[subfig_no].suptitle('Data 2d marginals', fontsize='xx-large') @@ -2501,16 +2616,13 @@ class PeakHistogram(object): yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) - for j in range(len(ini_mu)): - ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[-1-len(ini_mu)+1+j], fill=False)) - # ax.add_patch(Ellipse((ini_mu[xind],ini_mu[yind]), 2*peak_std*ini_sigma_2d[i][0], 2*peak_std*ini_sigma_2d[i][1], ini_angle_2d[i], color='green', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) + ax.add_patch(Ellipse((ini_mu[0][xind],ini_mu[0][yind]), 2*peak_std*ini_sigma_2d[0][i][0], 2*peak_std*ini_sigma_2d[0][i][1], ini_angle_2d[0][i], color='green', ls=styles['initial'], fill=False, label='initial')) + for j in range(1,len(ini_mu)): + ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[self.lev_bins[j-1]], fill=False, label=f'{self.lev_bins[j-1][:-2]} bins')) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False, label=f'{peak_std} sigma')) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False, label=f'{bkgr_std} sigma')) if i==0: - if len(ini_mu)==1: - ax.legend([f'initial', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - else: - ax.legend([f'initial']+[f'level {j-1}' for j in range(1,len(ini_mu))]+[f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.legend(framealpha=1.0, fontsize='x-large') ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') subfigs[subfig_no].suptitle('Fit 2d marginals', fontsize='xx-large') @@ -2531,15 +2643,13 @@ class PeakHistogram(object): yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(rebinned_data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) - for j in range(len(ini_mu)): - ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[-1-len(ini_mu)+1+j], fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) + ax.add_patch(Ellipse((ini_mu[0][xind],ini_mu[0][yind]), 2*peak_std*ini_sigma_2d[0][i][0], 2*peak_std*ini_sigma_2d[0][i][1], ini_angle_2d[0][i], color='green', ls=styles['initial'], fill=False, label='initial')) + for j in range(1,len(ini_mu)): + ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[self.lev_bins[j-1]], fill=False, label=f'{self.lev_bins[j-1][:-2]} bins')) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False, label=f'{peak_std} sigma')) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False, label=f'{bkgr_std} sigma')) if i==0: - if len(ini_mu)==1: - ax.legend([f'initial', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - else: - ax.legend([f'initial']+[f'level {j-1}' for j in range(1,len(ini_mu))]+[f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.legend(framealpha=1.0, fontsize='x-large') ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') subfigs[subfig_no].suptitle('Rebinned data 2d marginals', fontsize='xx-large') @@ -2551,15 +2661,13 @@ class PeakHistogram(object): yind, xind = axes_order(i) left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] im = ax.imshow(rebinned_fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower', vmin=vmin, vmax=vmax) - for j in range(len(ini_mu)): - ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[-1-len(ini_mu)+1+j], fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) + ax.add_patch(Ellipse((ini_mu[0][xind],ini_mu[0][yind]), 2*peak_std*ini_sigma_2d[0][i][0], 2*peak_std*ini_sigma_2d[0][i][1], ini_angle_2d[0][i], color='green', ls=styles['initial'], fill=False, label='initial')) + for j in range(1,len(ini_mu)): + ax.add_patch(Ellipse((ini_mu[j][xind],ini_mu[j][yind]), 2*peak_std*ini_sigma_2d[j][i][0], 2*peak_std*ini_sigma_2d[j][i][1], ini_angle_2d[j][i], color='green', ls=styles[self.lev_bins[j-1]], fill=False, label=f'{self.lev_bins[j-1][:-2]} bins')) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False, label=f'{peak_std} sigma')) + ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False, label=f'{bkgr_std} sigma')) if i==0: - if len(ini_mu)==1: - ax.legend([f'initial', f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - else: - ax.legend([f'initial']+[f'level {j-1}' for j in range(1,len(ini_mu))]+[f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') + ax.legend(framealpha=1.0, fontsize='x-large') ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') subfigs[subfig_no].suptitle('Rebinned fit 2d marginals', fontsize='xx-large') @@ -2620,253 +2728,139 @@ class PeakHistogram(object): plt.close('all') if _profile: - self._profile['plot'] = time.time() - start + if 'plot' in self._profile.keys(): + self._profile['plot'] += time.time() - start + else: + self._profile['plot'] = time.time() - start + + def plot_conditionals(self, grid_size=5, plot_path='output', prefix=None): + start = time.time() - def plot1(self, bins, plot_path='output', prefix=None, peak_std=4, bkgr_std=10, log=False): # create output directory for plots Path(plot_path).mkdir(parents=True, exist_ok=True) - # fit model to data + ####################################################################### + data, points, edges = self.get_grid_data(return_edges=True) # point along each dimension dim_points = [points[:,0,0,0],points[0,:,0,1],points[0,0,:,2]] - ####################################################################### - # evaluate model + k_ind = np.array(split_bins([data.shape[1]], grid_size-1, recursive=False)) + k_ind = np.hstack(([0],np.cumsum(k_ind)-1)) - # parameters of the model - nbkgr = 1 #+ self.ndims - # npeak = self.fit_params.size - nbkgr - # ncnt = self.ndims - # ncov = (self.ndims*(self.ndims+1))//2 - # nangles = (self.ndims*(self.ndims-1))//2 - # nskew = self.ndims - - # bkgr = self.fit_params[:nbkgr] - # intst = self.fit_params[nbkgr] - # mu = self.fit_params[1+nbkgr:1+nbkgr+ncnt] - # sqrtP = self.fit_params[1+nbkgr+ncnt:1+nbkgr+ncnt+ncov] - # angles = sqrtP[:nangles] - # sqrt_eig = 1 / sqrtP[nangles:] - # skew = self.fit_params[1+nbkgr+ncnt+ncov:1+nbkgr+ncnt+ncov+nskew] + l_ind = np.array(split_bins([data.shape[2]], grid_size-1, recursive=False)) + l_ind = np.hstack(([0],np.cumsum(l_ind)-1)) - bkgr_params = self.fit_params[:nbkgr] - peak_params = self.fit_params[nbkgr:] + h_grid = np.linspace(edges[0][0],edges[0][-1],300) + k_grid = np.array(dim_points[1][k_ind]) + l_grid = np.array(dim_points[2][l_ind]) - # inverse rotation matrix - # R,_,_ = rotation_matrix(angles) - # R = self.peak_fun.rotation_matrix(self.fit_params[nbkgr:]) + points = np.stack(np.meshgrid(h_grid,k_grid,l_grid,indexing='ij'), axis=self.ndims) - _,mu,_ = self.peak_fun.get_parameters(peak_params) - # full covariance matrix - # cov_3d = R.T @ np.diag(sqrt_eig**2) @ R - cov_3d = self.peak_fun.Cov(peak_params) + # ####################################################################### + # # evaluate inital models - # covariances and ellipsoids of 2d marginals - cov_2d = [] - angle_2d = [] - sigma_2d = [] - for i in range(self.ndims): - yind, xind = [j for j in range(self.ndims) if j!=i] - cov_2d.append( cov_3d[np.ix_([xind,yind],[xind,yind])] ) - roti,eigi,_ = svd(cov_2d[-1]) - # eigi, roti = eig(cov_2d[-1]) - # eigi, roti = eigen(cov_2d[-1]) - angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) - sigma_2d.append( np.sqrt(eigi) ) + # ini_mu = [] + # ini_sigma_2d = [[] for _ in self.init_params] + # ini_angle_2d = [[] for _ in self.init_params] + # ini_fit = [] + # ini_fit_masked = [] + # ini_rebinned_fit = [] + # for j,init_params in enumerate(self.init_params): + # # parameters + # ini_bkgr_params = init_params[:self.bkgr_fun.nparams] + # ini_peak_params = init_params[self.bkgr_fun.nparams:] - # fitted model - # fit = bkgr[0]**2 + gaussian_mixture(self.fit_params[nbkgr:], points.reshape((-1,self.ndims)), covariance_parameterization='givens').reshape(data.shape) - fit = bkgr_params[0]**2 + self.peak_fun(peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) - fit_masked = (1 if self.detector_mask is None else self.detector_mask) * fit + # # peak center and full covariance + # ini_mu.append(self.peak_fun.get_parameters(ini_peak_params)[1]) + # ini_cov_3d = self.peak_fun.Cov(ini_peak_params) - # rebinned data and fit - rebinned_data, rebinned_points, rebinned_edges = self.get_grid_data(bins=bins, rebin_mode='density', return_edges=True) - rebinned_fit = rebin_histogram(fit, bins, mode='density') + # # covariances and ellipsoids of 2d marginals + # # ini_angle_2d.append([]*self.ndims) + # # ini_sigma_2d.append([]*self.ndims) + # for i in range(self.ndims): + # yind, xind = axes_order(i) + # ini_cov_2d = ini_cov_3d[np.ix_([xind,yind],[xind,yind])] + # roti,eigi,_ = svd(ini_cov_2d) + # # eigi, roti = eig(cov_2d[-1]) + # # eigi, roti = eigen(cov_2d[-1]) + # # ini_angle_2d.append( np.sign(roti[0,1]) * np.arccos(roti[0,0])/np.pi*180 ) + # ini_angle_2d[j].append( np.arctan2(roti[1,0],roti[0,0])/np.pi*180 ) + # ini_sigma_2d[j].append( np.sqrt(eigi) ) + # # fitted model + # ini_fit.append( ini_bkgr_params[0]**2 + self.peak_fun(ini_peak_params, points.reshape((-1,self.ndims))).reshape(data.shape) ) + # ini_fit_masked.append( (1 if self.detector_mask is None else self.detector_mask) * ini_fit ) - normalize = False - ######################################## - # plot 1d marginals - data_1d = marginalize_1d(data, normalize=normalize, mask=self.detector_mask) - fit_1d = marginalize_1d(fit, normalize=normalize, mask=self.detector_mask) - rebinned_data_1d = marginalize_1d(rebinned_data, normalize=normalize, bin_lengths=bins, mask=self.detector_mask) - rebinned_fit_1d = marginalize_1d(rebinned_fit, normalize=normalize, bin_lengths=bins, mask=self.detector_mask) - # exit() - - fig = plt.figure(constrained_layout=True, figsize=(20,45)) - axes = fig.subplots(7,3) - # fig = plt.figure(constrained_layout=True, figsize=(20,35)) - # axes = fig.subplots(5,3) - ax_id = -1 - ############################## - ax_id += 1 - for i,ax in enumerate(axes[ax_id]): - ax.stairs(data_1d[i], edges=edges[i], fill=True) - ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5) - ax.plot(dim_points[i], fit_1d[i]) - ax.vlines([mu[i]-peak_std*np.sqrt(cov_3d[i,i]),mu[i]+peak_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-') - ax.vlines([mu[i]-bkgr_std*np.sqrt(cov_3d[i,i]),mu[i]+bkgr_std*np.sqrt(cov_3d[i,i])], 0, data_1d[i].max(), color='r', ls='-.') - ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') - if i==0: - ax.legend(['data','reb. data','fit', f'{peak_std} sigma', f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - ax.set_box_aspect(1) - - ax_id += 1 - for i,ax in enumerate(axes[ax_id]): - ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=True) - ax.stairs(rebinned_fit_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5) - ax.plot(dim_points[i], fit_1d[i]) - ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') - if i==0: - ax.legend(['reb. data','reb. fit', 'fit'], fontsize='xx-large') - ax.set_box_aspect(1) - - # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}_1d.png') - - ######################################## - # plot 2d marginals - data_2d = marginalize_2d(data, normalize=normalize) - fit_2d = marginalize_2d(fit, normalize=normalize) - fit_masked_2d = marginalize_2d(fit_masked, normalize=normalize) - rebinned_data_2d = marginalize_2d(rebinned_data, normalize=normalize, bin_lengths=bins, recover_shape=True) - rebinned_fit_2d = marginalize_2d(rebinned_fit, normalize=normalize, bin_lengths=bins, recover_shape=True) - - # show zero pixels as None - data_2d = [d/(d!=0) for d in data_2d] - fit_2d = [d/(d!=0) for d in fit_2d] - fit_masked_2d = [d/(d!=0) for d in fit_masked_2d] - rebinned_data_2d = [d/(d!=0) for d in rebinned_data_2d] - rebinned_fit_2d = [d/(d!=0) for d in rebinned_fit_2d] - - if log: - data_2d = np.log(data_2d) - fit_2d = np.log(fit_2d) - fit_masked_2d = np.log(fit_masked_2d) - rebinned_data_2d = np.log(rebinned_data_2d) - rebinned_fit_2d = np.log(rebinned_fit_2d) + # # rebinned fit + # ini_rebinned_fit.append( rebin_histogram(ini_fit[-1], bins, mode='density') ) - # fig = plt.figure(constrained_layout=True, figsize=(10,6)) - # (subfig1, subfig2) = fig.subfigures(2, 1) - # axes = fig.subplots(2,3) - # original data - ax_id += 1 - for i,ax in enumerate(axes[ax_id]): - yind, xind = [j for j in range(3) if j!=i] - left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - ax.imshow(data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower') - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) - if i==0: - ax.legend([f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') - ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') - # gaussian fit - ax_id += 1 - for i,ax in enumerate(axes[ax_id]): - yind, xind = [j for j in range(3) if j!=i] - left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - ax.imshow(fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower') - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) - if i==0: - ax.legend([f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') - ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') - # # subfig2.suptitle('Gaussian fit with peak/background regions for (sigma/I) criterion') - # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}_2d.png') - - - # fig = plt.figure(constrained_layout=True, figsize=(10,6)) - # axes = fig.subplots(2,3) - # rebinned data - ax_id += 1 - for i,ax in enumerate(axes[ax_id]): - yind, xind = [j for j in range(3) if j!=i] - left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - ax.imshow(rebinned_data_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower') - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) - if i==0: - ax.legend([f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') - ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') - # fit to rebinned data - ax_id += 1 - for i,ax in enumerate(axes[ax_id]): - yind, xind = [j for j in range(3) if j!=i] - left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - ax.imshow(rebinned_fit_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower') - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-', fill=False)) - ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*bkgr_std*sigma_2d[i][0], 2*bkgr_std*sigma_2d[i][1], angle_2d[i], color='red', ls='-.', fill=False)) - if i==0: - ax.legend([f'{peak_std} sigma',f'{bkgr_std} sigma'], framealpha=1.0, fontsize='xx-large') - ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') - ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') - # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}_2d_rebin.png') - + ####################################################################### + # evaluate final model - ######################################## - # plot grids + # final parameters + bkgr_params = self.fit_params[:self.bkgr_fun.nparams] + peak_params = self.fit_params[self.bkgr_fun.nparams:] - # # fig = plt.figure(constrained_layout=True, figsize=(18,6)) - # # axes = fig.subplots(1,3) - # for i,ax in enumerate(axes[6]): - # yind, xind = [j for j in range(3) if j!=i] - # left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - # ax.hlines(rebinned_edges[yind], rebinned_edges[xind][0], rebinned_edges[xind][-1]) - # ax.vlines(rebinned_edges[xind], rebinned_edges[yind][0], rebinned_edges[yind][-1]) - # ax.add_patch(Ellipse((mu[xind],mu[yind]), 2*peak_std*sigma_2d[i][0], 2*peak_std*sigma_2d[i][1], angle_2d[i], color='red', fill=False)) - # ax.set_xlabel(hist_ws.getDimension(xind).name, fontsize=10) - # ax.set_ylabel(hist_ws.getDimension(yind).name, fontsize=10) - # # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}_grid.png') + # fitted model + fit = bkgr_params[0]**2 + self.peak_fun(peak_params, points.reshape((-1,self.ndims))).reshape(points.shape[:self.ndims]) - ######################################## - # plot difference - ax_id += 1 - for i,ax in enumerate(axes[ax_id]): - yind, xind = [j for j in range(3) if j!=i] - left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - ax.imshow(np.abs(fit_masked_2d[i]-data_2d[i]), interpolation='none', extent=(left,right,bottom,top), origin='lower') - ax.set_xlabel(self.hist_ws.getDimension(xind).name, fontsize='x-large') - ax.set_ylabel(self.hist_ws.getDimension(yind).name, fontsize='x-large') + ####################################################################### + # plot - # ######################################## - # # plot detector mask + data_max = data.max() + + fig = plt.figure(constrained_layout=True, figsize=(20,20), dpi=100) + subplots = fig.subplots(ngrid,ngrid, sharex=True, sharey=True) #wspace=0.07 + for r,row in enumerate(subplots): + for c,ax in enumerate(row): + ax.stairs(data[:,k_ind[r],l_ind[c]], edges=edges[0], fill=True, alpha=1.0) + ax.plot(h_grid, fit[:,r,c]) + # ax.set_ylim(0,1.1*data_max) + + + # for i,ax in enumerate(subfigs[subfig_no].subplots(1,3,sharey=True)): + # ax.stairs(data_1d[i], edges=edges[i], fill=True, alpha=0.3) + # if np.all(np.array([d.size for d in data_1d])!=np.array([d.size for d in rebinned_data_1d])): + # ax.stairs(rebinned_data_1d[i], edges=rebinned_edges[i], fill=False, lw=1.5, color='b', baseline=None) + # for j in range(len(ini_fit_1d)): + # ax.plot(dim_points[i], ini_fit_1d[j][i], color='g', ls=styles[-1-len(ini_fit_1d)+1+j]) + # ax.plot(dim_points[i], fit_1d[i], color='black') + # ax.set_xlabel(self.hist_ws.getDimension(i).name, fontsize='x-large') + # if i==0: + # if np.all(np.array([d.size for d in data_1d])!=np.array([d.size for d in rebinned_data_1d])): + # data_reb_data = ['orig. data','reb. data'] + # else: + # data_reb_data = ['orig. data'] + # if len(ini_fit_1d)==1: + # ax.legend(data_reb_data+['init. fit','final fit'], framealpha=1.0, fontsize='xx-large') + # else: + # ax.legend(data_reb_data+['init. fit']+[f'fit at level {j-1}' for j in range(1,len(ini_fit_1d))]+['final fit'], framealpha=1.0, fontsize='xx-large') + # # ax.legend(['orig. data','reb. data']+(['init. fit'] if len(ini_fit_1d)==0 else [f'init. fit {j}' for j in range(len(ini_fit_1d))])+['final fit'], framealpha=1.0, fontsize='xx-large') + # ax.set_box_aspect(1) + # subfigs[subfig_no].suptitle('Data, initial and final fits', fontsize='xx-large') - # if detector_mask is not None: - # detector_mask_2d = marginalize_2d(detector_mask, normalize=False) - # detector_mask_2d = [(d!=0).astype(int) for d in detector_mask_2d] - # detector_mask_2d = [d/(d!=0) for d in detector_mask_2d] - # ax_id += 1 - # for i,ax in enumerate(axes[ax_id]): - # yind, xind = [j for j in range(3) if j!=i] - # left, right, bottom, top = edges[xind][0], edges[xind][-1], edges[yind][0], edges[yind][-1] - # ax.imshow(detector_mask_2d[i], interpolation='none', extent=(left,right,bottom,top), origin='lower') - # ax.set_xlabel(hist_ws.getDimension(xind).name, fontsize=10) - # ax.set_ylabel(hist_ws.getDimension(yind).name, fontsize=10) ######################################## # save if prefix is None: - plt.savefig(f'{plot_path}/peak.png') + plt.savefig(f'{plot_path}/cond_peak.png') else: - plt.savefig(f'{plot_path}/{prefix}_peak.png') - - # # save - # if prefix is None: - # plt.savefig(f'{plot_path}/peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}.png') - # else: - # plt.savefig(f'{plot_path}/{prefix}_peak_[{peak_hkl[0]},{peak_hkl[1]},{peak_hkl[2]}]_number_{peak_id}.png') - + plt.savefig(f'{plot_path}/{prefix}_cond_peak.png') plt.close('all') + if _profile: + if 'plot' in self._profile.keys(): + self._profile['plot'] += time.time() - start + else: + self._profile['plot'] = time.time() - start + def integrate(self, peak_estimate='data', background_estimate='data', peak_std=4, bkgr_std=None): r'''Integrate peak intensity with background correction @@ -2884,6 +2878,8 @@ class PeakHistogram(object): corrected_sigma ''' + start = time.time() + if bkgr_std is None: bkgr_std = peak_std + 3 # parameters of the model @@ -2909,7 +2905,8 @@ class PeakHistogram(object): # data, points, edges = self.get_grid_data(return_edges=True) points = points.reshape((-1,self.ndims)) - fit = self.fit_params[0]**2 + gaussian_mixture(self.fit_params[nbkgr:],points,npeaks=1,covariance_parameterization='givens').reshape(data.shape) + # fit = self.fit_params[0]**2 + gaussian_mixture(self.fit_params[nbkgr:],points,npeaks=1,covariance_parameterization='givens').reshape(data.shape) + fit = self.fit_params[0]**2 + self.peak_fun(self.fit_params[nbkgr:],points).reshape(data.shape) data = data.ravel() fit = fit.ravel() @@ -2974,6 +2971,9 @@ class PeakHistogram(object): sigma = sigma + ((data-fit)**2/(data+1))[peak_mask].sum() sigma = np.sqrt(sigma) + if _profile: + self._profile['integrate'] = time.time() - start + return intensity, sigma, peak_chi2, total_bkgr_intensity @@ -2996,40 +2996,51 @@ if __name__ == '__main__': loss = 'mle' bins = 64 n_std = 4 + # cov = 'full' + # cov = 'cholesky' + cov = 'givens' + + # solver0 = 'Newton-CG' + solver0 = 'BFGS' + # solver0 = 'L-BFGS-B' + + # solver = 'Newton-CG' + solver = 'BFGS' + # solver = 'L-BFGS-B' + + tol = 1.e-3 # for peak_id in [1074]: # for peak_id in [1082]: # for peak_id in [1074,1082]: # for peak_id in [1077]: - for peak_id in [1,6,8,11,17,22,74,1074,1077,1196,1239]: - print(f'Processing peak {peak_id}, loss {loss}') + # for peak_id in [4]: + # for peak_id in [1239]: + # for peak_id in [1,6,8,11,17,22,74,1074,1077,1196,1239]: + for peak_id in range(50): + print(f'\nProcessing peak {peak_id}, loss={loss}, cov={cov}, solver0={solver0}, solver={solver}\n') hist_ws = LoadMD(f'../Result/TOPAZ_{run}_peak_{peak_id}_{bins}.nxs') detector_mask = np.load(f'../Result/TOPAZ_{run}_peak_{peak_id}_mask_{bins}.npy') - # detector_mask = np.ones_like(hist_ws.getSignalArray()) - h = Histogram(hist_ws, detector_mask=detector_mask) - # h.fit(bins='knuth') - # h.fit(bins='adaptive_knuth') - # h.fit(bins=10) + h = PeakHistogram(hist_ws, detector_mask=detector_mask, parameterization=cov) - start = time.time() - # gauss_params, fit_sucess, plot_bins = h.fit(return_bins=True, solver='BFGS') - gauss_params, fit_sucess, plot_bins = h.fit_multilevel(min_level=4, return_bins=True, solver0='BFGS', solver='L-BFGS-B') + # gauss_params, fit_sucess, plot_bins = h.fit(loss=loss, return_bins=True, solver=solver, tol=tol) + gauss_params, fit_sucess, plot_bins = h.fit_multilevel(min_level=4, return_bins=True, solver0=solver0, solver=solver, tol=tol) # gauss_params, fit_sucess, plot_bins = h.fit_two_level(min_level=4, return_bins=True, solver0='Newton-CG', solver='BFGS') - print(f"Fit: {time.time()-start} sec") - start = time.time() - h.plot(bins=plot_bins, prefix=str(peak_id)) - print(f"Plot: {time.time()-start} sec") + h.plot_marginals(bins=plot_bins, prefix=str(peak_id), log=True) + # h.plot_conditionals(bins=plot_bins, prefix=str(peak_id)) - start = time.time() - print(h.integrate()) - print(f"Integrate: {time.time()-start} sec") + h.integrate() + + # print(4/h.peak_fun.sqrtD) print() + # h.print_stat() + -- GitLab