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