agd.Selling

This file implements Selling's algorithm in dimension $d \in \{2,3\}$, which decomposes a symmetric positive definite matrix $D$, of dimension $d\leq 3$, in the form $$ D = \sum_{0\leq i < I} a_i e_i e_i^\top, $$ where $a_i \geq 0$ and $e_i\in Z^d$ is a vector with integer coordinates, and where $I = d(d+2)/2$.

Selling's decomposition is a central tool in the design of adaptive discretization schemes for anisotropic partial differential equations, on Cartesian grids.

  1# Copyright 2020 Jean-Marie Mirebeau, University Paris-Sud, CNRS, University Paris-Saclay
  2# Distributed WITHOUT ANY WARRANTY. Licensed under the Apache License, Version 2.0, see http://www.apache.org/licenses/LICENSE-2.0
  3
  4r"""
  5This file implements Selling's algorithm in dimension $d \in \\{2,3\\}$, which decomposes a 
  6symmetric positive definite matrix $D$, of dimension $d\leq 3$, in the form 
  7$$
  8	D = \sum_{0\leq i < I} a_i e_i e_i^\top,
  9$$
 10where $a_i \geq 0$ and $e_i\in Z^d$ is a vector with integer coordinates, 
 11and where $I = d(d+2)/2$.
 12
 13Selling's decomposition is a central tool in the design of adaptive discretization schemes
 14for anisotropic partial differential equations, on Cartesian grids.
 15"""
 16
 17import numpy as np
 18from itertools import cycle
 19from .LinearParallel import dot_VAV, perp, cross
 20from . import AutomaticDifferentiation as ad
 21samesize_int_t = ad.cupy_generic.samesize_int_t
 22
 23iterMax2 = 100 
 24iterMax3 = 100 
 25
 26# -------- Dimension based dispatch -----
 27
 28def ObtuseSuperbase(m,sb=None):
 29	"""
 30	Input : 
 31	- m : symmetric positive definite matrix, defined as an
 32	 array of shape $(d,d, n_1,...,n_k)$.
 33	- sb (optional) : initial guess for the obtuse superbase.
 34
 35	Ouput : an m-obtuse superbase 
 36	"""
 37	m = ad.remove_ad(m) # The superbase is a discrete object, AD is not applicable.
 38	dim = len(m)
 39	if sb is None: sb = CanonicalSuperbase(m)
 40	if   dim==1: return _ObtuseSuperbase1(m,sb)	
 41	elif dim==2: return _ObtuseSuperbase2(m,sb)
 42	elif dim==3: return _ObtuseSuperbase3(m,sb)
 43	else: raise ValueError("Selling's decomposition only applies in dimension <=3.") 	
 44
 45def Decomposition(m,sb=None):
 46	r"""
 47	Use Selling's algorithm to decompose a symmetric positive definite matrix.
 48	Note : results unspecified if matrices are not symmetric (includes optional AD part) 
 49	or not positive definite.
 50
 51	Input : 
 52	- m : symmetric positive definite matrix, defined as an
 53	 array of shape $(d,d, n_1,...,n_k)$ where $d\leq 3$.
 54	- sb (optional) : superbase to use for the decomposition,
 55	 array of shape $(d,d+1, n_1,...,n_k)$.
 56	Output : the coefficients and offsets of the decomposition.
 57	"""
 58	dim = len(m)
 59
 60	# Strange bugs were observed when running the code below on the GPU with early cupy versions,
 61	# related with the array indexing pattern. Using a custom cuda kernel is faster and safer.
 62	if ad.cupy_generic.from_cupy(m) and dim>1:
 63		from . import Eikonal
 64		return Eikonal.VoronoiDecomposition(m,mode='gpu')
 65
 66	if sb is None: sb = ObtuseSuperbase(m,sb)
 67	if   dim==1: return _Decomposition1(m,sb)	
 68	elif dim==2: return _Decomposition2(m,sb)
 69	elif dim==3: return _Decomposition3(m,sb)
 70	else: raise ValueError("Selling's decomposition only applies in dimension <=3.") 
 71
 72
 73def GatherByOffset(T,Coefs,Offsets):
 74	"""
 75	Get the coefficient of each offset.
 76	This function is essentially used to make nice plots of how the superbase coefficients
 77	and offsets vary as the decomposed tensor varies. Opposite offsets are regarded as identical.
 78	"""
 79	T,Coefs,Offsets = map(ad.cupy_generic.cupy_get,(T,Coefs,Offsets))
 80	TimeCoef = {};
 81	for (i,t) in enumerate(T):
 82		coefs = Coefs[:,i]
 83		offsets = Offsets[:,:,i]
 84		for (j,c) in enumerate(coefs):
 85			offset = tuple(offsets[:,j].astype(int))
 86			offset_m = tuple(-offsets[:,j].astype(int))
 87			if offset<offset_m:
 88				offset=offset_m
 89			if offset in TimeCoef:
 90				TimeCoef[offset][0].append(t)
 91				TimeCoef[offset][1].append(c)
 92			else:
 93				TimeCoef[offset] = ([t],[c])
 94	return TimeCoef
 95
 96
 97def CanonicalSuperbase(m):
 98	"""
 99	Returns a superbase with the same dimensions and array type as m.
100
101	Output : 
102	 - m : array of shape $(d,d, n_1,...,n_k)$
103	"""
104	d=len(m); assert m.shape[1]==d
105	shape=m.shape[2:]
106
107	sb=np.zeros_like(m,shape=(d,d+1,*shape))
108	sb[:,0]=-1
109	for i in range(d):
110		sb[i,i+1]=1
111	return sb
112
113def SuperbasesForConditioning(cond,dim=2):
114	"""
115	Returns a family of superbases. 
116	For any positive definite matrix $M$ with condition number below the given bound,
117	one of these superbases will be $M$-obtuse.
118	(Condition number is the ratio of the largest to the smallest eigenvalue.)
119
120	Input : 
121	 - cond (scalar) : the bound on the condition number.
122	"""
123	if   dim==1: return _SuperbasesForConditioning1(cond)
124	elif dim==2: return _SuperbasesForConditioning2(cond)
125	elif dim==3: return _SuperbasesForConditioning3(cond)
126	else: raise ValueError("Selling's decomposition only applies in dimension <=3.") 
127
128
129# ------- One dimensional variant (trivial) ------
130
131def _ObtuseSuperbase1(m,sb=None):
132	return CanonicalSuperbase(m)
133
134def _Decomposition1(m,sb):
135	shape = m.shape[2:]
136	offsets = np.ones_like(ad.remove_ad(m),shape=(1,1,*shape),dtype=samesize_int_t(m.dtype))
137	coefs = m.reshape((1,*shape))
138	return coefs,offsets
139
140def _SuperbasesForConditioning1(cond):
141	sb = CanonicalSuperbase(np.eye(1))
142	return sb.reshape(sb.shape+(1,))
143
144# ------- Two dimensional variant ------
145
146# We do everyone in parallel, without selection or early abort
147def _ObtuseSuperbase2(m,sb):
148	"""
149		Use Selling's algorithm to compute an obtuse superbase.
150
151		input : symmetric positive definite matrix m, dim=2
152		input/output : superbase b (must be valid at startup)
153		
154		module variable : iterMax2, max number of iterations
155
156		output : wether the algorithm succeeded
157	"""
158	iterReduced = 0
159	for iter,(i,j,k) in zip(range(iterMax2), cycle([(0,1,2),(1,2,0),(2,0,1)]) ):
160		# Test for a positive angle, and modify superbase if necessary
161		acute = dot_VAV(sb[:,i],m,sb[:,j]) > 0
162		if np.any(acute):
163			sb[:,k,acute] = sb[:,i,acute]-sb[:,j,acute]
164			sb[:,i,acute] = -sb[:,i,acute]
165			iterReduced=0
166		elif iterReduced<3: iterReduced+=1
167		else: return sb
168
169	raise ValueError(f"Selling's algorithm did not terminate in iterMax2={iterMax2} iterations")
170	
171# Produce the matrix decomposition
172def _Decomposition2(m,sb):
173	"""
174		Use Selling's algorithm to decompose a tensor
175
176		input : symmetric positive definite tensor 
177		output : coefficients, offsets
178	"""
179	shape = m.shape[2:]
180	coef  = np.zeros_like(m,shape=(3,*shape))
181	for (i,j,k) in [(0,1,2),(1,2,0),(2,0,1)]:
182		coef[i] = -dot_VAV(sb[:,j],m, sb[:,k]) 
183	
184	return coef,perp(sb).astype(samesize_int_t(coef.dtype))
185
186def _SuperbasesForConditioning2(cond):
187	"""
188	Implementation is based on exploring the Stern-Brocot tree, 
189	with a stopping criterion based on the angle between consecutive vectors.
190	"""
191
192	mu = np.sqrt(cond)
193	theta = np.pi/2. - np.arccos( 2/(mu+1./mu))
194
195	u=np.array( (1,0) )
196	l = [np.array( (-1,0) ),np.array( (0,1) )]
197	m = []
198	superbases =[]
199
200	def angle(u,v): return np.arctan2(u[0]*v[1]-u[1]*v[0], u[0]*v[0]+u[1]*v[1])
201
202	while l:
203		v=l[-1]
204		if angle(u,v)<=theta:
205			m.append(u)
206			u=v
207			l.pop()
208		else:
209			l.append(u+v)
210			superbases.append((u,v,-u-v))
211
212
213	return np.array(superbases).transpose((2,1,0))
214
215# ------- Three dimensional variant -------
216
217# We do everyone in parallel, without selection or early abort
218def _ObtuseSuperbase3(m,sb):
219	"""
220		Use Selling's algorithm to compute an obtuse superbase.
221
222		input : symmetric positive definite matrix m, dim=3
223		input/output : superbase b (must be valid at startup)
224		
225		module variable : iterMax3, max number of iterations
226
227		output : wether the algorithm succeeded
228	"""
229#	This energy is reduced at each time step
230#	def energy(): return sum([dot_VAV(sb[:,i],m,sb[:,i]) for i in range(4)])
231#	Introducing a tolerance, for the positivity test, was not needed in our experiments
232#	tol = 100*np.finfo(m.dtype).eps*trace(m) 
233#	if(np.any(tol<=0)): raise ValueError("Matrices must be positive definite (negative trace found)") 
234	iterReduced = 0
235	sigma = cycle([(0,1,2,3),(0,2,1,3),(0,3,1,2),(1,2,0,3),(1,3,0,2),(2,3,0,1)])
236	for iter,(i,j,k,l) in zip(range(iterMax3), sigma):		
237		# Test for a positive angle, and modify superbase if necessary
238		acute = dot_VAV(sb[:,i],m,sb[:,j]) > 0
239		if np.any(acute):
240			sb[:,k,acute] += sb[:,i,acute]
241			sb[:,l,acute] += sb[:,i,acute]
242			sb[:,i,acute] = -sb[:,i,acute]
243			iterReduced=0
244		elif iterReduced<6: iterReduced+=1
245		else: return sb
246	raise ValueError(f"Selling's algorithm did not terminate in iterMax3={iterMax3} iterations")
247	
248def _Decomposition3(m,sb):
249	"""
250		Use Selling's algorithm to decompose a tensor
251
252		input : symmetric positive definite tensor, d=3
253		output : coefficients, offsets
254	"""
255	shape = m.shape[2:]
256	
257	coef   = np.zeros_like(m,shape=(6,*shape))
258	offset = np.zeros_like(ad.remove_ad(m),shape=(3,6,*shape))
259	for iter,(i,j,k,l) in enumerate(
260		[(0,1,2,3),(0,2,1,3),(0,3,1,2),(1,2,0,3),(1,3,0,2),(2,3,0,1)]):
261		coef[iter] = -dot_VAV(sb[:,i], m, sb[:,j]) 
262		offset[:,iter] = cross(sb[:,k], sb[:,l])
263		
264	return coef,offset.astype(samesize_int_t(coef.dtype))
265
266def _SuperbasesForConditioning3(cond):
267	raise ValueError("Sorry, _SuperbasesForConditioning3 is not implemented yet")
def samesize_int_t(float_t):
25def samesize_int_t(float_t):
26	"""
27	Returns an integer type of the same size (32 or 64 bits) as a given float type
28	"""
29	float_t = np.dtype(float_t).type
30	float_name = str(float_t)
31	if   float_t==np.float32: return np.int32
32	elif float_t==np.float64: return np.int64
33	else: raise ValueError(
34		f"Type {float_t} is not a float type, or has no default matching int type")

Returns an integer type of the same size (32 or 64 bits) as a given float type

iterMax2 = 100
iterMax3 = 100
def ObtuseSuperbase(m, sb=None):
29def ObtuseSuperbase(m,sb=None):
30	"""
31	Input : 
32	- m : symmetric positive definite matrix, defined as an
33	 array of shape $(d,d, n_1,...,n_k)$.
34	- sb (optional) : initial guess for the obtuse superbase.
35
36	Ouput : an m-obtuse superbase 
37	"""
38	m = ad.remove_ad(m) # The superbase is a discrete object, AD is not applicable.
39	dim = len(m)
40	if sb is None: sb = CanonicalSuperbase(m)
41	if   dim==1: return _ObtuseSuperbase1(m,sb)	
42	elif dim==2: return _ObtuseSuperbase2(m,sb)
43	elif dim==3: return _ObtuseSuperbase3(m,sb)
44	else: raise ValueError("Selling's decomposition only applies in dimension <=3.") 	

Input :

  • m : symmetric positive definite matrix, defined as an array of shape $(d,d, n_1,...,n_k)$.
  • sb (optional) : initial guess for the obtuse superbase.

Ouput : an m-obtuse superbase

def Decomposition(m, sb=None):
46def Decomposition(m,sb=None):
47	r"""
48	Use Selling's algorithm to decompose a symmetric positive definite matrix.
49	Note : results unspecified if matrices are not symmetric (includes optional AD part) 
50	or not positive definite.
51
52	Input : 
53	- m : symmetric positive definite matrix, defined as an
54	 array of shape $(d,d, n_1,...,n_k)$ where $d\leq 3$.
55	- sb (optional) : superbase to use for the decomposition,
56	 array of shape $(d,d+1, n_1,...,n_k)$.
57	Output : the coefficients and offsets of the decomposition.
58	"""
59	dim = len(m)
60
61	# Strange bugs were observed when running the code below on the GPU with early cupy versions,
62	# related with the array indexing pattern. Using a custom cuda kernel is faster and safer.
63	if ad.cupy_generic.from_cupy(m) and dim>1:
64		from . import Eikonal
65		return Eikonal.VoronoiDecomposition(m,mode='gpu')
66
67	if sb is None: sb = ObtuseSuperbase(m,sb)
68	if   dim==1: return _Decomposition1(m,sb)	
69	elif dim==2: return _Decomposition2(m,sb)
70	elif dim==3: return _Decomposition3(m,sb)
71	else: raise ValueError("Selling's decomposition only applies in dimension <=3.") 

Use Selling's algorithm to decompose a symmetric positive definite matrix. Note : results unspecified if matrices are not symmetric (includes optional AD part) or not positive definite.

Input :

  • m : symmetric positive definite matrix, defined as an array of shape $(d,d, n_1,...,n_k)$ where $d\leq 3$.
  • sb (optional) : superbase to use for the decomposition, array of shape $(d,d+1, n_1,...,n_k)$. Output : the coefficients and offsets of the decomposition.
def GatherByOffset(T, Coefs, Offsets):
74def GatherByOffset(T,Coefs,Offsets):
75	"""
76	Get the coefficient of each offset.
77	This function is essentially used to make nice plots of how the superbase coefficients
78	and offsets vary as the decomposed tensor varies. Opposite offsets are regarded as identical.
79	"""
80	T,Coefs,Offsets = map(ad.cupy_generic.cupy_get,(T,Coefs,Offsets))
81	TimeCoef = {};
82	for (i,t) in enumerate(T):
83		coefs = Coefs[:,i]
84		offsets = Offsets[:,:,i]
85		for (j,c) in enumerate(coefs):
86			offset = tuple(offsets[:,j].astype(int))
87			offset_m = tuple(-offsets[:,j].astype(int))
88			if offset<offset_m:
89				offset=offset_m
90			if offset in TimeCoef:
91				TimeCoef[offset][0].append(t)
92				TimeCoef[offset][1].append(c)
93			else:
94				TimeCoef[offset] = ([t],[c])
95	return TimeCoef

Get the coefficient of each offset. This function is essentially used to make nice plots of how the superbase coefficients and offsets vary as the decomposed tensor varies. Opposite offsets are regarded as identical.

def CanonicalSuperbase(m):
 98def CanonicalSuperbase(m):
 99	"""
100	Returns a superbase with the same dimensions and array type as m.
101
102	Output : 
103	 - m : array of shape $(d,d, n_1,...,n_k)$
104	"""
105	d=len(m); assert m.shape[1]==d
106	shape=m.shape[2:]
107
108	sb=np.zeros_like(m,shape=(d,d+1,*shape))
109	sb[:,0]=-1
110	for i in range(d):
111		sb[i,i+1]=1
112	return sb

Returns a superbase with the same dimensions and array type as m.

Output :

  • m : array of shape $(d,d, n_1,...,n_k)$
def SuperbasesForConditioning(cond, dim=2):
114def SuperbasesForConditioning(cond,dim=2):
115	"""
116	Returns a family of superbases. 
117	For any positive definite matrix $M$ with condition number below the given bound,
118	one of these superbases will be $M$-obtuse.
119	(Condition number is the ratio of the largest to the smallest eigenvalue.)
120
121	Input : 
122	 - cond (scalar) : the bound on the condition number.
123	"""
124	if   dim==1: return _SuperbasesForConditioning1(cond)
125	elif dim==2: return _SuperbasesForConditioning2(cond)
126	elif dim==3: return _SuperbasesForConditioning3(cond)
127	else: raise ValueError("Selling's decomposition only applies in dimension <=3.") 

Returns a family of superbases. For any positive definite matrix $M$ with condition number below the given bound, one of these superbases will be $M$-obtuse. (Condition number is the ratio of the largest to the smallest eigenvalue.)

Input :

  • cond (scalar) : the bound on the condition number.