agd.Eikonal.DictIn

The Eikonal.dictIn class is used to hold the input parameters to the eikonal solvers of the HFM library, CPU based or GPU based.

  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
  4"""
  5The Eikonal.dictIn class is used to hold the input parameters to the eikonal solvers of 
  6the HFM library, CPU based or GPU based.
  7"""
  8
  9import numpy as np
 10from collections.abc import MutableMapping
 11from .. import AutomaticDifferentiation as ad
 12from .. import FiniteDifferences as fd
 13from .. import LinearParallel as lp
 14from .. import Metrics
 15from . import run_detail
 16from . import DictIn_detail
 17
 18_array_float_fields = {
 19	'origin','dims','gridScale','gridScales','values',
 20	'seeds','seeds_Unoriented','tips','tips_Unoriented',
 21	'seedValues','seedValues_Unoriented','seedValueVariation','seedFlags',
 22	'cost','speed','costVariation',
 23	'inspectSensitivity','inspectSensitivityWeights','inspectSensitivityLengths',
 24	'exportVoronoiFlags','factoringIndexShift',
 25	'chart_mapping',
 26	'control','controls','state_transition_costs',
 27}
 28
 29# Alternative key for setting or getting a single element
 30_singleIn = {
 31	'seed':'seeds','seedValue':'seedValues',
 32	'seed_Unoriented':'seeds_Unoriented','seedValue_Unoriented':'seedValues_Unoriented',
 33	'tip':'tips','tip_Unoriented':'tips_Unoriented',
 34	'seedFlag':'seedFlags','seedFlag_Unoriented':'seedFlags_Unoriented',
 35}
 36
 37_readonlyIn = {
 38	'float_t'
 39}
 40
 41_array_module = {
 42	'cpu':'numpy','cpu_raw':'numpy','gpu_transfer':'numpy',
 43	'gpu':'cupy','cpu_transfer':'cupy',
 44}
 45
 46_singleOut = {
 47	'geodesic':'geodesics','geodesic_Unoriented':'geodesics_Unoriented',
 48	'geodesic_euclideanLength':'geodesics_euclideanLength',
 49}
 50
 51SEModels = {'ReedsShepp2','ReedsSheppForward2','Elastica2','Dubins2',
 52'ReedsSheppExt2','ReedsSheppForwardExt2','ElasticaExt2','ElasticaExt2_5','DubinsExt2',
 53'ConvexReedsSheppForward2','ConvexElastica2','ConvexDubins2',
 54'ReedsShepp3','ReedsSheppForward3'}
 55
 56# These models do not follow the usual dimension naming convention (physical dimension last)
 57dimModels = {'ElasticaExt2_5':3,'Riemann3_Periodic':3,'ReedsSheppGPU3':5} 
 58
 59class dictOut(MutableMapping):
 60	"""
 61	A dictionnary like structure used as output of the Eikonal solvers. 
 62	"""
 63
 64	def __init__(self,store=None):
 65		self.store=store
 66
 67	def __copy__(self): return dictOut(self.store.copy())
 68	def copy(self): return self.__copy__()
 69
 70	def __repr__(self):
 71		return f"dictOut({self.store})"
 72
 73	def __setitem__(self, key, value):
 74		if key in _singleOut: key = _singleOut[key]; value = [value]
 75		self.store[key] = value
 76
 77	def __getitem__(self, key): 
 78		if key in _singleOut:
 79			values = self.store[_singleOut[key]]
 80			if len(values)!=1: 
 81				raise ValueError(f"Found {len(values)} values for key {key}")
 82			return values[0]
 83		return self.store[key]
 84
 85	def __delitem__(self, key): 
 86		key = _singleOut.get(key,key)
 87		del self.store[key]
 88
 89	def __iter__(self): return iter(self.store)
 90	def __len__(self):  return len(self.store)
 91	def keys(self): 
 92		"""
 93		The keys of the dictionary-like structure.
 94		"""
 95		return self.store.keys()
 96
 97def CenteredLinspace(a,b,n):
 98	"""
 99	Returns a linspace shifted by half a node length.
100	Inputs : 
101	 - a,b : interval endpoints
102	 - n : number of points
103	"""
104	n_=int(n); assert(n==n_) #Allow floats for convenience
105	r,dr=np.linspace(a,b,n_,endpoint=False,retstep=True)
106	if np.any(np.isnan(dr)): assert n==1; dr=b-a #Cupy 8.6 bug
107	return r+dr/2
108
109
110class dictIn(MutableMapping):
111	"""
112	A dictionary like structure used as input of the Eikonal solvers.
113
114	__init__ arguments: 
115	- store : a dictionary, used for initialization.
116
117	See dictIn().RunHelp() for details on the eikonal solver inputs.
118	"""
119
120	default_mode = 'cpu' # class attribute
121
122	def __init__(self, store=None):
123		if store is None: store=dict()
124		self.store = {'arrayOrdering':'RowMajor'}
125		if 'mode' in store:
126			mode = store['mode']
127			self.store['mode']=mode
128		else:
129			mode = dictIn.default_mode
130		assert mode in ('cpu','cpu_raw','cpu_transfer','gpu','gpu_transfer')
131		self._mode = mode
132		if self.mode in ('gpu','cpu_transfer'):
133			import cupy as cp
134			self.xp = cp
135			caster = lambda x : cp.asarray(x,dtype=float_t)
136			self.array_float_caster = lambda x : ad.asarray(x,caster=caster) #cupy >= 8.6
137		else: 
138			self.xp = np
139			self.array_float_caster = lambda x : np.asarray(x,dtype=float_t)
140		
141		if self.mode in ('gpu','cpu_transfer','gpu_transfer'): float_t=np.float32
142		else: float_t=np.float64
143
144		if 'float_t' in store:
145			float_t = store['float_t']
146			self.store['float_t']=float_t
147
148		self._float_t = float_t
149		if store: self.update(store)
150
151	def __copy__(self):     return dictIn(self.store.copy())
152	def __deepcopy__(self): return dictIn(self.store.deepcopy())
153	def copy(self): 
154		"""
155		Returns a shallow copy of the structure.
156		"""
157		return self.__copy__()
158
159	@property
160	def mode(self): 
161		"""
162		The running mode of the eikonal solver, see the Run method.
163		The input data must be provided using a compatible array module: 
164		numpy in 'cpu' mode, cupy in 'gpu' mode.
165
166		Supported running mode for the eikonal solver : 
167		- 'cpu' : Run algorithm on host, store data on host
168		- 'cpu_transfer' : Run algorithm on host, store data on device
169		- 'cpu_raw' : Raw call to the HFM CPU library (debug purposes)
170		- 'gpu' : Run algorithm on device, store data on device
171		- 'gpu_transfer' : Run algorithm on device, store data on host
172		"""
173		return self._mode
174	@property
175	def float_t(self):
176		"""
177		The floating point type of the data arrays. Typically np.float64 in 'cpu' mode, 
178		and np.float32 in 'gpu' mode.
179		"""
180		return self._float_t
181	
182	def __repr__(self): 
183		return f"dictIn({self.store})"
184
185	def __setitem__(self, key, value):
186		"""
187		Set a key:value pair in the dictionary like structure.
188
189		Special treatment of keys and values:
190		- The values associated to some keys are converted, if needed, to numeric 
191		arrays of floats for the numpy or cupy module. (key in _array_float_fields) 
192		- The key value pair 'seed':x is converted into 'seeds':[x], (as for setting 
193		 multiple seeds, but their number is just one). (key in _singleIn)
194		- Some keys are readonly, changes will be rejected (key in _readonlyIn.)
195		"""
196		if key=='mode':
197			if _array_module[value]!=_array_module[self.mode]:
198				raise ValueError('Switching between modes with distinct array storage')
199			else: self._mode = value
200		if key in _readonlyIn and self.store[key]!=value: 
201			raise ValueError(f"Key {key} is readonly (set at init)") 
202		if key in _singleIn: 
203			key = _singleIn[key]; value = [value]
204		if key in _array_float_fields and not ad.isndarray(value):
205			value = self.array_float_caster(value)
206		self.store[key] = value
207
208	def __getitem__(self, key): 
209		"""
210		Get a value associated to a given key, in the dictionary like structure.
211
212		Special treatment of keys and values:
213		- self['seed'] = self['seeds'][0], if self['seeds'] has length one, 
214		and fails otherwise. (key in _singleIn)
215		"""
216		if key in _singleIn:
217			values = self.store[_singleIn[key]]
218			if len(values)!=1: 
219				raise ValueError(f"Found {len(values)} values for key {key}")
220			return values[0]
221		return self.store[key]
222
223	def __delitem__(self, key): 
224		key = _singleIn.get(key,key)
225		del self.store[key]
226
227	def __iter__(self): return iter(self.store)
228	
229	def __len__(self):  return len(self.store)
230
231	def keys(self): 
232		"""
233		The keys of this dictionary structure.
234		"""
235		return self.store.keys()
236
237	def RunHelp(self,mode=None):
238		"""
239		Help on the eikonal solver, depending on the running mode.
240		"""
241		if mode is None: mode = self.mode
242		if mode in ('cpu','cpu_raw','cpu_transfer'): 
243			help(run_detail.RunSmart)
244		else:
245			from . import HFM_CUDA
246			help(HFM_CUDA.RunGPU)
247
248	def Run(self,join=None,**kwargs):
249		"""
250		Calls the HFM library, prints log and returns output.
251		Inputs : 
252		- join (optional) : join the dictionary with these additional entries before running.
253		- **kwargs (optional) : passed to the run_detail.RunSmart or HFM_CUDA.RunGPU methods.
254		
255		See dictIn().RunHelp() for additional details, depending on the running mode.
256		"""
257		if join is not None:
258			other = self.copy()
259			other.update(join)
260			return other.Run(**kwargs)
261
262		if self['arrayOrdering']!='RowMajor': 
263			raise ValueError("Unsupported array ordering")
264		def to_dictOut(out):
265			if isinstance(out,tuple): return (dictOut(out[0]),) + out[1:]
266			else: return dictOut(out)
267			
268		if   self.mode=='cpu': return to_dictOut(run_detail.RunSmart(self,**kwargs))
269		elif self.mode=='cpu_raw': return to_dictOut(run_detail.RunRaw(self.store,**kwargs))
270		elif self.mode=='cpu_transfer':
271			cpuIn = ad.cupy_generic.cupy_get(self,dtype64=True,iterables=(dictIn,Metrics.Base))
272			cpuIn.xp = np; cpuIn._mode = 'cpu'; cpuIn['mode'] = 'cpu'
273			for key in list(cpuIn.keys()): 
274				if key.startswith('traits'): cpuIn.pop(key)
275			return to_dictOut(run_detail.RunSmart(cpuIn,**kwargs))
276		
277		from . import HFM_CUDA
278		if   self.mode=='gpu': return to_dictOut(HFM_CUDA.RunGPU(self,**kwargs))
279		elif self.mode=='gpu_transfer':
280			gpuStoreIn = ad.cupy_generic.cupy_set(self.store, # host->device
281				dtype32=True, iterables=(dict,Metrics.Base))
282			gpuIn = dictIn({**gpuStoreIn,'mode':'gpu'})
283			gpuOut = HFM_CUDA.RunGPU(gpuIn)
284			cpuOut = ad.cupy_generic.cupy_get(gpuOut,iterables=(dict,list))
285			return to_dictOut(cpuOut) # device->host
286
287	# ------- Grid related functions ------
288
289	@property
290	def shape(self):
291		"""
292		The shape of the discretization grid.
293		"""
294		dims = self['dims']
295		if ad.cupy_generic.from_cupy(dims): dims = dims.get()
296		return tuple(dims.astype(int))
297
298	@property
299	def size(self): 
300		"""
301		The number of points in the discretization grid.
302		"""
303		return np.prod(self.shape)
304	
305	@property
306	def SE(self):
307		"""
308		Wether the model is based on the Special Euclidean group.
309		True for curvature penalized models.
310		"""
311		return self['model'] in SEModels	
312	
313	@property
314	def vdim(self):
315		"""
316		The dimension of the ambient vector space.
317		"""
318		model = self['model']
319		if model in dimModels: return dimModels[model]
320		dim = int(model[-1])
321		return (2*dim-1) if self.SE else dim
322
323	@property
324	def nTheta(self):
325		"""
326		Number of points for discretizing the interval [0,2 pi], in the angular space 
327		discretization, for the SE models (a.k.a. curvature penalized models).
328		"""
329		if not self.SE: raise ValueError("Not an SE model")
330		shape = self.shape
331		if self.vdim!=len(self.shape): raise ValueError("Angular resolution not set")
332		n = shape[-1]
333		return (2*n) if self.get('projective',False) and self.vdim==3 else n
334
335	@nTheta.setter
336	def nTheta(self,value):
337		if not self.SE: raise ValueError("Not an SE model")
338		shape = self.shape
339		vdim = self.vdim
340		projective = self.get('projective',False)
341		if vdim==len(shape): shape=shape[:int((vdim+1)/2)] #raise ValueError("Angular resolution already set")
342		if   vdim==3: self['dims'] = (*shape, value/2 if projective else value) 
343		elif vdim==5: self['dims'] = (*shape, value/4 if projective  else value/2, value) 
344
345	@property
346	def gridScales(self):
347		"""
348		The discretization grid scale along each axis.
349		"""
350		if self.SE:
351			h = self['gridScale']
352			hTheta = 2.*np.pi / self.nTheta
353			if self.vdim==3: return self.array_float_caster( (h,h,hTheta) )
354			else: return self.array_float_caster( (h,h,h,hTheta,hTheta) )
355		elif 'gridScales' in self: return self['gridScales']
356		else: return self.array_float_caster((self['gridScale'],)*self.vdim)	
357		
358	@property
359	def corners(self):
360		"""
361		Returns the extreme points grid[:,0,...,0] and grid[:,-1,...,-1] of the 
362		discretization grid.
363		"""
364		gridScales = self.gridScales
365		dims = self['dims']
366		physdim = (len(dims)+1)//2 if self.SE else len(dims)
367		origin = self.get('origin',self.xp.zeros_like(dims[:physdim]))
368		if self.SE: 
369			tail = self.array_float_caster((-gridScales[-1]/2,)*(len(dims)-len(origin)))
370			origin = np.concatenate((origin,tail))
371		return (origin,origin+gridScales*dims)
372
373	def Axes(self,dims=None):
374		"""
375		The discretization points used along each coordinate axis.
376		"""
377		bottom,top = self.corners
378		if dims is None: dims=self['dims']
379		return [self.array_float_caster(CenteredLinspace(b,t,d)) 
380			for b,t,d in zip(bottom,top,dims)]
381
382	def Grid(self,dims=None):
383		"""
384		Returns a grid of coordinates, containing all the discretization points of the domain.
385		Similar to np.meshgrid(*self.Axes(),indexing='ij')
386		Inputs : 
387		- dims(optional) : use a different sampling of the domain
388		"""
389		axes = self.Axes(dims);
390		ordering = self['arrayOrdering']
391		if ordering=='RowMajor': return ad.array(np.meshgrid(*axes,indexing='ij',copy=False))
392		elif ordering=='YXZ_RowMajor': return ad.array(np.meshgrid(*axes,copy=False))
393		else: raise ValueError('Unsupported arrayOrdering : '+ordering)
394
395	def SetUniformTips(self,dims):
396		"""
397		Place regularly spaced tip points all over the domain, 
398		from which to backtrack minimal geodesics.
399		Inputs : 
400		- dims : number of tips to use along each dimension.
401		"""
402		self['tips'] = self.Grid(dims).reshape(self.vdim,-1).T
403
404	def SetRect(self,sides,sampleBoundary=False,gridScale=None,gridScales=None,
405		dimx=None,dims=None):
406		"""
407		Defines a box domain, for the HFM library.
408		Inputs:
409		- sides, e.g. ((a,b),(c,d),(e,f)) for the domain [a,b]x[c,d]x[e,f]
410		- sampleBoundary : switch between sampling at the pixel centers, and sampling including the boundary
411		- gridScale, gridScales : side h>0 of each pixel (alt : axis dependent)
412		- dimx, dims : number of points along the first axis (alt : along all axes)
413		"""
414		# Ok to set a new domain, or completely replace the domain
415		domain_count = sum(e in self for e in ('gridScale','gridScales','dims','origin'))
416		if domain_count not in (0,3): raise ValueError("Domain already partially set")
417		
418		caster = self.array_float_caster
419		corner0,corner1 = caster(sides).T
420		dim = len(corner0)
421		sb=float(sampleBoundary)
422		width = corner1-corner0
423		if gridScale is not None: 
424			gridScales=[gridScale]*dim; self['gridScale']=gridScale
425		elif gridScales is not None:
426			self['gridScales']=gridScales
427		elif dimx is not None:
428			gridScale=width[0]/(dimx-sb); gridScales=[gridScale]*dim; self['gridScale']=gridScale
429		elif dims is not None:
430			gridScales=width/(caster(dims)-sb); self['gridScales']=gridScales
431		else: 
432			raise ValueError('Missing argument gridScale, gridScales, dimx, or dims')
433
434		h=caster(gridScales)
435		ratios = (corner1-corner0)/h + sb
436		dims = np.round(ratios)
437		assert(np.min(dims)>0)
438		origin = corner0 + (ratios-dims-sb)*h/2
439		self['dims']   = dims
440		self['origin'] = origin
441
442	def PointFromIndex(self,index,to=False):
443		"""
444		Turns an index into a point.
445		Optional argument to: if true, inverse transformation, turning a point into a continuous index
446		"""
447		bottom,_ = self.corners
448		scale = self.gridScales
449		start = bottom +0.5*scale
450		index = self.array_float_caster(index)
451		assert index.shape[-1]==self.vdim
452		if not to: return start+scale*index
453		else: return (index-start)/scale
454
455	def IndexFromPoint(self,point):
456		"""
457		Returns the index that yields the position closest to a point, and the error.
458		"""
459		point = self.array_float_caster(point)
460		continuousIndex = self.PointFromIndex(point,to=True)
461		index = np.round(continuousIndex)
462		return index.astype(int),(continuousIndex-index)
463
464	def OrientedPoints(self,pointU):
465		"""
466		Appends all possible orientations to the point coordinates.
467		"""
468		pointU = self.array_float_caster(pointU)
469		if self['model'] not in SEModels: 
470			raise ValueError("OrientedPoints only makes sense for models SE space.")
471		if self.vdim!=3:
472			raise ValueError("Sorry, oriented point not implemented for SE(3) models.")
473		pdim = int((self.vdim+1)/2) # Number of physical dimensions
474		if pointU.shape[-1]!=pdim:
475			raise ValueError(f"Unoriented points expected to have {pdim} dimensions, "
476				f"found {pointU.shape[-1]}.")
477		theta = self.Axes()[2]
478		point = self.xp.full((len(theta),*pointU.shape[:-1],self.vdim),np.nan)
479		for i,t in enumerate(theta):
480			point[i,...,:pdim]=pointU
481			point[i,...,pdim:]=t
482		return point
483
484	def VectorFromOffset(self,offset,to=False):
485		"""
486		Turns a finite difference offset into a vector, by multiplying by the gridScale.
487		Inputs : 
488		- offset : the offset to convert.
489		- to (optional) : if True, produces an offset from a vector (reverse operation).
490		"""
491		offset = self.array_float_caster(offset)
492		if not to: return offset*self.gridScales
493		else: return offset/self.gridScales  
494
495	def GridNeighbors(self,point,gridRadius):
496		"""
497		Returns the neighbors around a point on the grid. 
498		Geometry last convention
499		Inputs: 
500		- point (array): geometry last
501		- gridRadius (scalar): given in pixels
502		"""
503		xp = self.xp
504		point = self.array_float_caster(point)
505		point_cindex = self.PointFromIndex(point,to=True)
506		aX = [xp.arange(int(np.floor(ci-gridRadius)),int(np.ceil(ci+gridRadius)+1),
507			dtype=self.float_t) for ci in point_cindex]
508		neigh_index =  xp.stack(xp.meshgrid( *aX, indexing='ij'),axis=-1)
509		neigh_index = neigh_index.reshape(-1,neigh_index.shape[-1])
510
511		# Check which neighbors are close enough
512		offset = neigh_index-point_cindex
513		close = np.sum(offset**2,axis=-1) < gridRadius**2
514
515		# Check which neighbors are in the domain (periodicity omitted)
516		neigh = self.PointFromIndex(neigh_index)
517		bottom,top = self.corners
518		inRange = np.all(np.logical_and(bottom<neigh,neigh<top),axis=-1)
519
520		return neigh[np.logical_and(close,inRange)]
521
522	SetFactor = DictIn_detail.SetFactor
523	SetSphere = DictIn_detail.SetSphere
524	@property
525	def factoringPointChoice(self): return DictIn_detail.factoringPointChoice(self)
SEModels = {'ReedsShepp3', 'ReedsSheppForwardExt2', 'ConvexReedsSheppForward2', 'DubinsExt2', 'ReedsSheppExt2', 'Dubins2', 'ReedsSheppForward3', 'ElasticaExt2_5', 'ConvexElastica2', 'ElasticaExt2', 'ReedsSheppForward2', 'Elastica2', 'ConvexDubins2', 'ReedsShepp2'}
dimModels = {'ElasticaExt2_5': 3, 'Riemann3_Periodic': 3, 'ReedsSheppGPU3': 5}
class dictOut(collections.abc.MutableMapping):
60class dictOut(MutableMapping):
61	"""
62	A dictionnary like structure used as output of the Eikonal solvers. 
63	"""
64
65	def __init__(self,store=None):
66		self.store=store
67
68	def __copy__(self): return dictOut(self.store.copy())
69	def copy(self): return self.__copy__()
70
71	def __repr__(self):
72		return f"dictOut({self.store})"
73
74	def __setitem__(self, key, value):
75		if key in _singleOut: key = _singleOut[key]; value = [value]
76		self.store[key] = value
77
78	def __getitem__(self, key): 
79		if key in _singleOut:
80			values = self.store[_singleOut[key]]
81			if len(values)!=1: 
82				raise ValueError(f"Found {len(values)} values for key {key}")
83			return values[0]
84		return self.store[key]
85
86	def __delitem__(self, key): 
87		key = _singleOut.get(key,key)
88		del self.store[key]
89
90	def __iter__(self): return iter(self.store)
91	def __len__(self):  return len(self.store)
92	def keys(self): 
93		"""
94		The keys of the dictionary-like structure.
95		"""
96		return self.store.keys()

A dictionnary like structure used as output of the Eikonal solvers.

dictOut(store=None)
65	def __init__(self,store=None):
66		self.store=store
store
def copy(self):
69	def copy(self): return self.__copy__()
def keys(self):
92	def keys(self): 
93		"""
94		The keys of the dictionary-like structure.
95		"""
96		return self.store.keys()

The keys of the dictionary-like structure.

Inherited Members
collections.abc.MutableMapping
pop
popitem
clear
update
setdefault
collections.abc.Mapping
get
items
values
def CenteredLinspace(a, b, n):
 98def CenteredLinspace(a,b,n):
 99	"""
100	Returns a linspace shifted by half a node length.
101	Inputs : 
102	 - a,b : interval endpoints
103	 - n : number of points
104	"""
105	n_=int(n); assert(n==n_) #Allow floats for convenience
106	r,dr=np.linspace(a,b,n_,endpoint=False,retstep=True)
107	if np.any(np.isnan(dr)): assert n==1; dr=b-a #Cupy 8.6 bug
108	return r+dr/2

Returns a linspace shifted by half a node length. Inputs :

  • a,b : interval endpoints
  • n : number of points
class dictIn(collections.abc.MutableMapping):
111class dictIn(MutableMapping):
112	"""
113	A dictionary like structure used as input of the Eikonal solvers.
114
115	__init__ arguments: 
116	- store : a dictionary, used for initialization.
117
118	See dictIn().RunHelp() for details on the eikonal solver inputs.
119	"""
120
121	default_mode = 'cpu' # class attribute
122
123	def __init__(self, store=None):
124		if store is None: store=dict()
125		self.store = {'arrayOrdering':'RowMajor'}
126		if 'mode' in store:
127			mode = store['mode']
128			self.store['mode']=mode
129		else:
130			mode = dictIn.default_mode
131		assert mode in ('cpu','cpu_raw','cpu_transfer','gpu','gpu_transfer')
132		self._mode = mode
133		if self.mode in ('gpu','cpu_transfer'):
134			import cupy as cp
135			self.xp = cp
136			caster = lambda x : cp.asarray(x,dtype=float_t)
137			self.array_float_caster = lambda x : ad.asarray(x,caster=caster) #cupy >= 8.6
138		else: 
139			self.xp = np
140			self.array_float_caster = lambda x : np.asarray(x,dtype=float_t)
141		
142		if self.mode in ('gpu','cpu_transfer','gpu_transfer'): float_t=np.float32
143		else: float_t=np.float64
144
145		if 'float_t' in store:
146			float_t = store['float_t']
147			self.store['float_t']=float_t
148
149		self._float_t = float_t
150		if store: self.update(store)
151
152	def __copy__(self):     return dictIn(self.store.copy())
153	def __deepcopy__(self): return dictIn(self.store.deepcopy())
154	def copy(self): 
155		"""
156		Returns a shallow copy of the structure.
157		"""
158		return self.__copy__()
159
160	@property
161	def mode(self): 
162		"""
163		The running mode of the eikonal solver, see the Run method.
164		The input data must be provided using a compatible array module: 
165		numpy in 'cpu' mode, cupy in 'gpu' mode.
166
167		Supported running mode for the eikonal solver : 
168		- 'cpu' : Run algorithm on host, store data on host
169		- 'cpu_transfer' : Run algorithm on host, store data on device
170		- 'cpu_raw' : Raw call to the HFM CPU library (debug purposes)
171		- 'gpu' : Run algorithm on device, store data on device
172		- 'gpu_transfer' : Run algorithm on device, store data on host
173		"""
174		return self._mode
175	@property
176	def float_t(self):
177		"""
178		The floating point type of the data arrays. Typically np.float64 in 'cpu' mode, 
179		and np.float32 in 'gpu' mode.
180		"""
181		return self._float_t
182	
183	def __repr__(self): 
184		return f"dictIn({self.store})"
185
186	def __setitem__(self, key, value):
187		"""
188		Set a key:value pair in the dictionary like structure.
189
190		Special treatment of keys and values:
191		- The values associated to some keys are converted, if needed, to numeric 
192		arrays of floats for the numpy or cupy module. (key in _array_float_fields) 
193		- The key value pair 'seed':x is converted into 'seeds':[x], (as for setting 
194		 multiple seeds, but their number is just one). (key in _singleIn)
195		- Some keys are readonly, changes will be rejected (key in _readonlyIn.)
196		"""
197		if key=='mode':
198			if _array_module[value]!=_array_module[self.mode]:
199				raise ValueError('Switching between modes with distinct array storage')
200			else: self._mode = value
201		if key in _readonlyIn and self.store[key]!=value: 
202			raise ValueError(f"Key {key} is readonly (set at init)") 
203		if key in _singleIn: 
204			key = _singleIn[key]; value = [value]
205		if key in _array_float_fields and not ad.isndarray(value):
206			value = self.array_float_caster(value)
207		self.store[key] = value
208
209	def __getitem__(self, key): 
210		"""
211		Get a value associated to a given key, in the dictionary like structure.
212
213		Special treatment of keys and values:
214		- self['seed'] = self['seeds'][0], if self['seeds'] has length one, 
215		and fails otherwise. (key in _singleIn)
216		"""
217		if key in _singleIn:
218			values = self.store[_singleIn[key]]
219			if len(values)!=1: 
220				raise ValueError(f"Found {len(values)} values for key {key}")
221			return values[0]
222		return self.store[key]
223
224	def __delitem__(self, key): 
225		key = _singleIn.get(key,key)
226		del self.store[key]
227
228	def __iter__(self): return iter(self.store)
229	
230	def __len__(self):  return len(self.store)
231
232	def keys(self): 
233		"""
234		The keys of this dictionary structure.
235		"""
236		return self.store.keys()
237
238	def RunHelp(self,mode=None):
239		"""
240		Help on the eikonal solver, depending on the running mode.
241		"""
242		if mode is None: mode = self.mode
243		if mode in ('cpu','cpu_raw','cpu_transfer'): 
244			help(run_detail.RunSmart)
245		else:
246			from . import HFM_CUDA
247			help(HFM_CUDA.RunGPU)
248
249	def Run(self,join=None,**kwargs):
250		"""
251		Calls the HFM library, prints log and returns output.
252		Inputs : 
253		- join (optional) : join the dictionary with these additional entries before running.
254		- **kwargs (optional) : passed to the run_detail.RunSmart or HFM_CUDA.RunGPU methods.
255		
256		See dictIn().RunHelp() for additional details, depending on the running mode.
257		"""
258		if join is not None:
259			other = self.copy()
260			other.update(join)
261			return other.Run(**kwargs)
262
263		if self['arrayOrdering']!='RowMajor': 
264			raise ValueError("Unsupported array ordering")
265		def to_dictOut(out):
266			if isinstance(out,tuple): return (dictOut(out[0]),) + out[1:]
267			else: return dictOut(out)
268			
269		if   self.mode=='cpu': return to_dictOut(run_detail.RunSmart(self,**kwargs))
270		elif self.mode=='cpu_raw': return to_dictOut(run_detail.RunRaw(self.store,**kwargs))
271		elif self.mode=='cpu_transfer':
272			cpuIn = ad.cupy_generic.cupy_get(self,dtype64=True,iterables=(dictIn,Metrics.Base))
273			cpuIn.xp = np; cpuIn._mode = 'cpu'; cpuIn['mode'] = 'cpu'
274			for key in list(cpuIn.keys()): 
275				if key.startswith('traits'): cpuIn.pop(key)
276			return to_dictOut(run_detail.RunSmart(cpuIn,**kwargs))
277		
278		from . import HFM_CUDA
279		if   self.mode=='gpu': return to_dictOut(HFM_CUDA.RunGPU(self,**kwargs))
280		elif self.mode=='gpu_transfer':
281			gpuStoreIn = ad.cupy_generic.cupy_set(self.store, # host->device
282				dtype32=True, iterables=(dict,Metrics.Base))
283			gpuIn = dictIn({**gpuStoreIn,'mode':'gpu'})
284			gpuOut = HFM_CUDA.RunGPU(gpuIn)
285			cpuOut = ad.cupy_generic.cupy_get(gpuOut,iterables=(dict,list))
286			return to_dictOut(cpuOut) # device->host
287
288	# ------- Grid related functions ------
289
290	@property
291	def shape(self):
292		"""
293		The shape of the discretization grid.
294		"""
295		dims = self['dims']
296		if ad.cupy_generic.from_cupy(dims): dims = dims.get()
297		return tuple(dims.astype(int))
298
299	@property
300	def size(self): 
301		"""
302		The number of points in the discretization grid.
303		"""
304		return np.prod(self.shape)
305	
306	@property
307	def SE(self):
308		"""
309		Wether the model is based on the Special Euclidean group.
310		True for curvature penalized models.
311		"""
312		return self['model'] in SEModels	
313	
314	@property
315	def vdim(self):
316		"""
317		The dimension of the ambient vector space.
318		"""
319		model = self['model']
320		if model in dimModels: return dimModels[model]
321		dim = int(model[-1])
322		return (2*dim-1) if self.SE else dim
323
324	@property
325	def nTheta(self):
326		"""
327		Number of points for discretizing the interval [0,2 pi], in the angular space 
328		discretization, for the SE models (a.k.a. curvature penalized models).
329		"""
330		if not self.SE: raise ValueError("Not an SE model")
331		shape = self.shape
332		if self.vdim!=len(self.shape): raise ValueError("Angular resolution not set")
333		n = shape[-1]
334		return (2*n) if self.get('projective',False) and self.vdim==3 else n
335
336	@nTheta.setter
337	def nTheta(self,value):
338		if not self.SE: raise ValueError("Not an SE model")
339		shape = self.shape
340		vdim = self.vdim
341		projective = self.get('projective',False)
342		if vdim==len(shape): shape=shape[:int((vdim+1)/2)] #raise ValueError("Angular resolution already set")
343		if   vdim==3: self['dims'] = (*shape, value/2 if projective else value) 
344		elif vdim==5: self['dims'] = (*shape, value/4 if projective  else value/2, value) 
345
346	@property
347	def gridScales(self):
348		"""
349		The discretization grid scale along each axis.
350		"""
351		if self.SE:
352			h = self['gridScale']
353			hTheta = 2.*np.pi / self.nTheta
354			if self.vdim==3: return self.array_float_caster( (h,h,hTheta) )
355			else: return self.array_float_caster( (h,h,h,hTheta,hTheta) )
356		elif 'gridScales' in self: return self['gridScales']
357		else: return self.array_float_caster((self['gridScale'],)*self.vdim)	
358		
359	@property
360	def corners(self):
361		"""
362		Returns the extreme points grid[:,0,...,0] and grid[:,-1,...,-1] of the 
363		discretization grid.
364		"""
365		gridScales = self.gridScales
366		dims = self['dims']
367		physdim = (len(dims)+1)//2 if self.SE else len(dims)
368		origin = self.get('origin',self.xp.zeros_like(dims[:physdim]))
369		if self.SE: 
370			tail = self.array_float_caster((-gridScales[-1]/2,)*(len(dims)-len(origin)))
371			origin = np.concatenate((origin,tail))
372		return (origin,origin+gridScales*dims)
373
374	def Axes(self,dims=None):
375		"""
376		The discretization points used along each coordinate axis.
377		"""
378		bottom,top = self.corners
379		if dims is None: dims=self['dims']
380		return [self.array_float_caster(CenteredLinspace(b,t,d)) 
381			for b,t,d in zip(bottom,top,dims)]
382
383	def Grid(self,dims=None):
384		"""
385		Returns a grid of coordinates, containing all the discretization points of the domain.
386		Similar to np.meshgrid(*self.Axes(),indexing='ij')
387		Inputs : 
388		- dims(optional) : use a different sampling of the domain
389		"""
390		axes = self.Axes(dims);
391		ordering = self['arrayOrdering']
392		if ordering=='RowMajor': return ad.array(np.meshgrid(*axes,indexing='ij',copy=False))
393		elif ordering=='YXZ_RowMajor': return ad.array(np.meshgrid(*axes,copy=False))
394		else: raise ValueError('Unsupported arrayOrdering : '+ordering)
395
396	def SetUniformTips(self,dims):
397		"""
398		Place regularly spaced tip points all over the domain, 
399		from which to backtrack minimal geodesics.
400		Inputs : 
401		- dims : number of tips to use along each dimension.
402		"""
403		self['tips'] = self.Grid(dims).reshape(self.vdim,-1).T
404
405	def SetRect(self,sides,sampleBoundary=False,gridScale=None,gridScales=None,
406		dimx=None,dims=None):
407		"""
408		Defines a box domain, for the HFM library.
409		Inputs:
410		- sides, e.g. ((a,b),(c,d),(e,f)) for the domain [a,b]x[c,d]x[e,f]
411		- sampleBoundary : switch between sampling at the pixel centers, and sampling including the boundary
412		- gridScale, gridScales : side h>0 of each pixel (alt : axis dependent)
413		- dimx, dims : number of points along the first axis (alt : along all axes)
414		"""
415		# Ok to set a new domain, or completely replace the domain
416		domain_count = sum(e in self for e in ('gridScale','gridScales','dims','origin'))
417		if domain_count not in (0,3): raise ValueError("Domain already partially set")
418		
419		caster = self.array_float_caster
420		corner0,corner1 = caster(sides).T
421		dim = len(corner0)
422		sb=float(sampleBoundary)
423		width = corner1-corner0
424		if gridScale is not None: 
425			gridScales=[gridScale]*dim; self['gridScale']=gridScale
426		elif gridScales is not None:
427			self['gridScales']=gridScales
428		elif dimx is not None:
429			gridScale=width[0]/(dimx-sb); gridScales=[gridScale]*dim; self['gridScale']=gridScale
430		elif dims is not None:
431			gridScales=width/(caster(dims)-sb); self['gridScales']=gridScales
432		else: 
433			raise ValueError('Missing argument gridScale, gridScales, dimx, or dims')
434
435		h=caster(gridScales)
436		ratios = (corner1-corner0)/h + sb
437		dims = np.round(ratios)
438		assert(np.min(dims)>0)
439		origin = corner0 + (ratios-dims-sb)*h/2
440		self['dims']   = dims
441		self['origin'] = origin
442
443	def PointFromIndex(self,index,to=False):
444		"""
445		Turns an index into a point.
446		Optional argument to: if true, inverse transformation, turning a point into a continuous index
447		"""
448		bottom,_ = self.corners
449		scale = self.gridScales
450		start = bottom +0.5*scale
451		index = self.array_float_caster(index)
452		assert index.shape[-1]==self.vdim
453		if not to: return start+scale*index
454		else: return (index-start)/scale
455
456	def IndexFromPoint(self,point):
457		"""
458		Returns the index that yields the position closest to a point, and the error.
459		"""
460		point = self.array_float_caster(point)
461		continuousIndex = self.PointFromIndex(point,to=True)
462		index = np.round(continuousIndex)
463		return index.astype(int),(continuousIndex-index)
464
465	def OrientedPoints(self,pointU):
466		"""
467		Appends all possible orientations to the point coordinates.
468		"""
469		pointU = self.array_float_caster(pointU)
470		if self['model'] not in SEModels: 
471			raise ValueError("OrientedPoints only makes sense for models SE space.")
472		if self.vdim!=3:
473			raise ValueError("Sorry, oriented point not implemented for SE(3) models.")
474		pdim = int((self.vdim+1)/2) # Number of physical dimensions
475		if pointU.shape[-1]!=pdim:
476			raise ValueError(f"Unoriented points expected to have {pdim} dimensions, "
477				f"found {pointU.shape[-1]}.")
478		theta = self.Axes()[2]
479		point = self.xp.full((len(theta),*pointU.shape[:-1],self.vdim),np.nan)
480		for i,t in enumerate(theta):
481			point[i,...,:pdim]=pointU
482			point[i,...,pdim:]=t
483		return point
484
485	def VectorFromOffset(self,offset,to=False):
486		"""
487		Turns a finite difference offset into a vector, by multiplying by the gridScale.
488		Inputs : 
489		- offset : the offset to convert.
490		- to (optional) : if True, produces an offset from a vector (reverse operation).
491		"""
492		offset = self.array_float_caster(offset)
493		if not to: return offset*self.gridScales
494		else: return offset/self.gridScales  
495
496	def GridNeighbors(self,point,gridRadius):
497		"""
498		Returns the neighbors around a point on the grid. 
499		Geometry last convention
500		Inputs: 
501		- point (array): geometry last
502		- gridRadius (scalar): given in pixels
503		"""
504		xp = self.xp
505		point = self.array_float_caster(point)
506		point_cindex = self.PointFromIndex(point,to=True)
507		aX = [xp.arange(int(np.floor(ci-gridRadius)),int(np.ceil(ci+gridRadius)+1),
508			dtype=self.float_t) for ci in point_cindex]
509		neigh_index =  xp.stack(xp.meshgrid( *aX, indexing='ij'),axis=-1)
510		neigh_index = neigh_index.reshape(-1,neigh_index.shape[-1])
511
512		# Check which neighbors are close enough
513		offset = neigh_index-point_cindex
514		close = np.sum(offset**2,axis=-1) < gridRadius**2
515
516		# Check which neighbors are in the domain (periodicity omitted)
517		neigh = self.PointFromIndex(neigh_index)
518		bottom,top = self.corners
519		inRange = np.all(np.logical_and(bottom<neigh,neigh<top),axis=-1)
520
521		return neigh[np.logical_and(close,inRange)]
522
523	SetFactor = DictIn_detail.SetFactor
524	SetSphere = DictIn_detail.SetSphere
525	@property
526	def factoringPointChoice(self): return DictIn_detail.factoringPointChoice(self)

A dictionary like structure used as input of the Eikonal solvers.

__init__ arguments:

  • store : a dictionary, used for initialization.

See dictIn().RunHelp() for details on the eikonal solver inputs.

dictIn(store=None)
123	def __init__(self, store=None):
124		if store is None: store=dict()
125		self.store = {'arrayOrdering':'RowMajor'}
126		if 'mode' in store:
127			mode = store['mode']
128			self.store['mode']=mode
129		else:
130			mode = dictIn.default_mode
131		assert mode in ('cpu','cpu_raw','cpu_transfer','gpu','gpu_transfer')
132		self._mode = mode
133		if self.mode in ('gpu','cpu_transfer'):
134			import cupy as cp
135			self.xp = cp
136			caster = lambda x : cp.asarray(x,dtype=float_t)
137			self.array_float_caster = lambda x : ad.asarray(x,caster=caster) #cupy >= 8.6
138		else: 
139			self.xp = np
140			self.array_float_caster = lambda x : np.asarray(x,dtype=float_t)
141		
142		if self.mode in ('gpu','cpu_transfer','gpu_transfer'): float_t=np.float32
143		else: float_t=np.float64
144
145		if 'float_t' in store:
146			float_t = store['float_t']
147			self.store['float_t']=float_t
148
149		self._float_t = float_t
150		if store: self.update(store)
default_mode = 'cpu'
store
def copy(self):
154	def copy(self): 
155		"""
156		Returns a shallow copy of the structure.
157		"""
158		return self.__copy__()

Returns a shallow copy of the structure.

mode
160	@property
161	def mode(self): 
162		"""
163		The running mode of the eikonal solver, see the Run method.
164		The input data must be provided using a compatible array module: 
165		numpy in 'cpu' mode, cupy in 'gpu' mode.
166
167		Supported running mode for the eikonal solver : 
168		- 'cpu' : Run algorithm on host, store data on host
169		- 'cpu_transfer' : Run algorithm on host, store data on device
170		- 'cpu_raw' : Raw call to the HFM CPU library (debug purposes)
171		- 'gpu' : Run algorithm on device, store data on device
172		- 'gpu_transfer' : Run algorithm on device, store data on host
173		"""
174		return self._mode

The running mode of the eikonal solver, see the Run method. The input data must be provided using a compatible array module: numpy in 'cpu' mode, cupy in 'gpu' mode.

Supported running mode for the eikonal solver :

  • 'cpu' : Run algorithm on host, store data on host
  • 'cpu_transfer' : Run algorithm on host, store data on device
  • 'cpu_raw' : Raw call to the HFM CPU library (debug purposes)
  • 'gpu' : Run algorithm on device, store data on device
  • 'gpu_transfer' : Run algorithm on device, store data on host
float_t
175	@property
176	def float_t(self):
177		"""
178		The floating point type of the data arrays. Typically np.float64 in 'cpu' mode, 
179		and np.float32 in 'gpu' mode.
180		"""
181		return self._float_t

The floating point type of the data arrays. Typically np.float64 in 'cpu' mode, and np.float32 in 'gpu' mode.

def keys(self):
232	def keys(self): 
233		"""
234		The keys of this dictionary structure.
235		"""
236		return self.store.keys()

The keys of this dictionary structure.

def RunHelp(self, mode=None):
238	def RunHelp(self,mode=None):
239		"""
240		Help on the eikonal solver, depending on the running mode.
241		"""
242		if mode is None: mode = self.mode
243		if mode in ('cpu','cpu_raw','cpu_transfer'): 
244			help(run_detail.RunSmart)
245		else:
246			from . import HFM_CUDA
247			help(HFM_CUDA.RunGPU)

Help on the eikonal solver, depending on the running mode.

def Run(self, join=None, **kwargs):
249	def Run(self,join=None,**kwargs):
250		"""
251		Calls the HFM library, prints log and returns output.
252		Inputs : 
253		- join (optional) : join the dictionary with these additional entries before running.
254		- **kwargs (optional) : passed to the run_detail.RunSmart or HFM_CUDA.RunGPU methods.
255		
256		See dictIn().RunHelp() for additional details, depending on the running mode.
257		"""
258		if join is not None:
259			other = self.copy()
260			other.update(join)
261			return other.Run(**kwargs)
262
263		if self['arrayOrdering']!='RowMajor': 
264			raise ValueError("Unsupported array ordering")
265		def to_dictOut(out):
266			if isinstance(out,tuple): return (dictOut(out[0]),) + out[1:]
267			else: return dictOut(out)
268			
269		if   self.mode=='cpu': return to_dictOut(run_detail.RunSmart(self,**kwargs))
270		elif self.mode=='cpu_raw': return to_dictOut(run_detail.RunRaw(self.store,**kwargs))
271		elif self.mode=='cpu_transfer':
272			cpuIn = ad.cupy_generic.cupy_get(self,dtype64=True,iterables=(dictIn,Metrics.Base))
273			cpuIn.xp = np; cpuIn._mode = 'cpu'; cpuIn['mode'] = 'cpu'
274			for key in list(cpuIn.keys()): 
275				if key.startswith('traits'): cpuIn.pop(key)
276			return to_dictOut(run_detail.RunSmart(cpuIn,**kwargs))
277		
278		from . import HFM_CUDA
279		if   self.mode=='gpu': return to_dictOut(HFM_CUDA.RunGPU(self,**kwargs))
280		elif self.mode=='gpu_transfer':
281			gpuStoreIn = ad.cupy_generic.cupy_set(self.store, # host->device
282				dtype32=True, iterables=(dict,Metrics.Base))
283			gpuIn = dictIn({**gpuStoreIn,'mode':'gpu'})
284			gpuOut = HFM_CUDA.RunGPU(gpuIn)
285			cpuOut = ad.cupy_generic.cupy_get(gpuOut,iterables=(dict,list))
286			return to_dictOut(cpuOut) # device->host

Calls the HFM library, prints log and returns output. Inputs :

  • join (optional) : join the dictionary with these additional entries before running.
  • **kwargs (optional) : passed to the run_detail.RunSmart or HFM_CUDA.RunGPU methods.

See dictIn().RunHelp() for additional details, depending on the running mode.

shape
290	@property
291	def shape(self):
292		"""
293		The shape of the discretization grid.
294		"""
295		dims = self['dims']
296		if ad.cupy_generic.from_cupy(dims): dims = dims.get()
297		return tuple(dims.astype(int))

The shape of the discretization grid.

size
299	@property
300	def size(self): 
301		"""
302		The number of points in the discretization grid.
303		"""
304		return np.prod(self.shape)

The number of points in the discretization grid.

SE
306	@property
307	def SE(self):
308		"""
309		Wether the model is based on the Special Euclidean group.
310		True for curvature penalized models.
311		"""
312		return self['model'] in SEModels	

Wether the model is based on the Special Euclidean group. True for curvature penalized models.

vdim
314	@property
315	def vdim(self):
316		"""
317		The dimension of the ambient vector space.
318		"""
319		model = self['model']
320		if model in dimModels: return dimModels[model]
321		dim = int(model[-1])
322		return (2*dim-1) if self.SE else dim

The dimension of the ambient vector space.

nTheta
324	@property
325	def nTheta(self):
326		"""
327		Number of points for discretizing the interval [0,2 pi], in the angular space 
328		discretization, for the SE models (a.k.a. curvature penalized models).
329		"""
330		if not self.SE: raise ValueError("Not an SE model")
331		shape = self.shape
332		if self.vdim!=len(self.shape): raise ValueError("Angular resolution not set")
333		n = shape[-1]
334		return (2*n) if self.get('projective',False) and self.vdim==3 else n

Number of points for discretizing the interval [0,2 pi], in the angular space discretization, for the SE models (a.k.a. curvature penalized models).

gridScales
346	@property
347	def gridScales(self):
348		"""
349		The discretization grid scale along each axis.
350		"""
351		if self.SE:
352			h = self['gridScale']
353			hTheta = 2.*np.pi / self.nTheta
354			if self.vdim==3: return self.array_float_caster( (h,h,hTheta) )
355			else: return self.array_float_caster( (h,h,h,hTheta,hTheta) )
356		elif 'gridScales' in self: return self['gridScales']
357		else: return self.array_float_caster((self['gridScale'],)*self.vdim)	

The discretization grid scale along each axis.

corners
359	@property
360	def corners(self):
361		"""
362		Returns the extreme points grid[:,0,...,0] and grid[:,-1,...,-1] of the 
363		discretization grid.
364		"""
365		gridScales = self.gridScales
366		dims = self['dims']
367		physdim = (len(dims)+1)//2 if self.SE else len(dims)
368		origin = self.get('origin',self.xp.zeros_like(dims[:physdim]))
369		if self.SE: 
370			tail = self.array_float_caster((-gridScales[-1]/2,)*(len(dims)-len(origin)))
371			origin = np.concatenate((origin,tail))
372		return (origin,origin+gridScales*dims)

Returns the extreme points grid[:,0,...,0] and grid[:,-1,...,-1] of the discretization grid.

def Axes(self, dims=None):
374	def Axes(self,dims=None):
375		"""
376		The discretization points used along each coordinate axis.
377		"""
378		bottom,top = self.corners
379		if dims is None: dims=self['dims']
380		return [self.array_float_caster(CenteredLinspace(b,t,d)) 
381			for b,t,d in zip(bottom,top,dims)]

The discretization points used along each coordinate axis.

def Grid(self, dims=None):
383	def Grid(self,dims=None):
384		"""
385		Returns a grid of coordinates, containing all the discretization points of the domain.
386		Similar to np.meshgrid(*self.Axes(),indexing='ij')
387		Inputs : 
388		- dims(optional) : use a different sampling of the domain
389		"""
390		axes = self.Axes(dims);
391		ordering = self['arrayOrdering']
392		if ordering=='RowMajor': return ad.array(np.meshgrid(*axes,indexing='ij',copy=False))
393		elif ordering=='YXZ_RowMajor': return ad.array(np.meshgrid(*axes,copy=False))
394		else: raise ValueError('Unsupported arrayOrdering : '+ordering)

Returns a grid of coordinates, containing all the discretization points of the domain. Similar to np.meshgrid(*self.Axes(),indexing='ij') Inputs :

  • dims(optional) : use a different sampling of the domain
def SetUniformTips(self, dims):
396	def SetUniformTips(self,dims):
397		"""
398		Place regularly spaced tip points all over the domain, 
399		from which to backtrack minimal geodesics.
400		Inputs : 
401		- dims : number of tips to use along each dimension.
402		"""
403		self['tips'] = self.Grid(dims).reshape(self.vdim,-1).T

Place regularly spaced tip points all over the domain, from which to backtrack minimal geodesics. Inputs :

  • dims : number of tips to use along each dimension.
def SetRect( self, sides, sampleBoundary=False, gridScale=None, gridScales=None, dimx=None, dims=None):
405	def SetRect(self,sides,sampleBoundary=False,gridScale=None,gridScales=None,
406		dimx=None,dims=None):
407		"""
408		Defines a box domain, for the HFM library.
409		Inputs:
410		- sides, e.g. ((a,b),(c,d),(e,f)) for the domain [a,b]x[c,d]x[e,f]
411		- sampleBoundary : switch between sampling at the pixel centers, and sampling including the boundary
412		- gridScale, gridScales : side h>0 of each pixel (alt : axis dependent)
413		- dimx, dims : number of points along the first axis (alt : along all axes)
414		"""
415		# Ok to set a new domain, or completely replace the domain
416		domain_count = sum(e in self for e in ('gridScale','gridScales','dims','origin'))
417		if domain_count not in (0,3): raise ValueError("Domain already partially set")
418		
419		caster = self.array_float_caster
420		corner0,corner1 = caster(sides).T
421		dim = len(corner0)
422		sb=float(sampleBoundary)
423		width = corner1-corner0
424		if gridScale is not None: 
425			gridScales=[gridScale]*dim; self['gridScale']=gridScale
426		elif gridScales is not None:
427			self['gridScales']=gridScales
428		elif dimx is not None:
429			gridScale=width[0]/(dimx-sb); gridScales=[gridScale]*dim; self['gridScale']=gridScale
430		elif dims is not None:
431			gridScales=width/(caster(dims)-sb); self['gridScales']=gridScales
432		else: 
433			raise ValueError('Missing argument gridScale, gridScales, dimx, or dims')
434
435		h=caster(gridScales)
436		ratios = (corner1-corner0)/h + sb
437		dims = np.round(ratios)
438		assert(np.min(dims)>0)
439		origin = corner0 + (ratios-dims-sb)*h/2
440		self['dims']   = dims
441		self['origin'] = origin

Defines a box domain, for the HFM library. Inputs:

  • sides, e.g. ((a,b),(c,d),(e,f)) for the domain [a,b]x[c,d]x[e,f]
  • sampleBoundary : switch between sampling at the pixel centers, and sampling including the boundary
  • gridScale, gridScales : side h>0 of each pixel (alt : axis dependent)
  • dimx, dims : number of points along the first axis (alt : along all axes)
def PointFromIndex(self, index, to=False):
443	def PointFromIndex(self,index,to=False):
444		"""
445		Turns an index into a point.
446		Optional argument to: if true, inverse transformation, turning a point into a continuous index
447		"""
448		bottom,_ = self.corners
449		scale = self.gridScales
450		start = bottom +0.5*scale
451		index = self.array_float_caster(index)
452		assert index.shape[-1]==self.vdim
453		if not to: return start+scale*index
454		else: return (index-start)/scale

Turns an index into a point. Optional argument to: if true, inverse transformation, turning a point into a continuous index

def IndexFromPoint(self, point):
456	def IndexFromPoint(self,point):
457		"""
458		Returns the index that yields the position closest to a point, and the error.
459		"""
460		point = self.array_float_caster(point)
461		continuousIndex = self.PointFromIndex(point,to=True)
462		index = np.round(continuousIndex)
463		return index.astype(int),(continuousIndex-index)

Returns the index that yields the position closest to a point, and the error.

def OrientedPoints(self, pointU):
465	def OrientedPoints(self,pointU):
466		"""
467		Appends all possible orientations to the point coordinates.
468		"""
469		pointU = self.array_float_caster(pointU)
470		if self['model'] not in SEModels: 
471			raise ValueError("OrientedPoints only makes sense for models SE space.")
472		if self.vdim!=3:
473			raise ValueError("Sorry, oriented point not implemented for SE(3) models.")
474		pdim = int((self.vdim+1)/2) # Number of physical dimensions
475		if pointU.shape[-1]!=pdim:
476			raise ValueError(f"Unoriented points expected to have {pdim} dimensions, "
477				f"found {pointU.shape[-1]}.")
478		theta = self.Axes()[2]
479		point = self.xp.full((len(theta),*pointU.shape[:-1],self.vdim),np.nan)
480		for i,t in enumerate(theta):
481			point[i,...,:pdim]=pointU
482			point[i,...,pdim:]=t
483		return point

Appends all possible orientations to the point coordinates.

def VectorFromOffset(self, offset, to=False):
485	def VectorFromOffset(self,offset,to=False):
486		"""
487		Turns a finite difference offset into a vector, by multiplying by the gridScale.
488		Inputs : 
489		- offset : the offset to convert.
490		- to (optional) : if True, produces an offset from a vector (reverse operation).
491		"""
492		offset = self.array_float_caster(offset)
493		if not to: return offset*self.gridScales
494		else: return offset/self.gridScales  

Turns a finite difference offset into a vector, by multiplying by the gridScale. Inputs :

  • offset : the offset to convert.
  • to (optional) : if True, produces an offset from a vector (reverse operation).
def GridNeighbors(self, point, gridRadius):
496	def GridNeighbors(self,point,gridRadius):
497		"""
498		Returns the neighbors around a point on the grid. 
499		Geometry last convention
500		Inputs: 
501		- point (array): geometry last
502		- gridRadius (scalar): given in pixels
503		"""
504		xp = self.xp
505		point = self.array_float_caster(point)
506		point_cindex = self.PointFromIndex(point,to=True)
507		aX = [xp.arange(int(np.floor(ci-gridRadius)),int(np.ceil(ci+gridRadius)+1),
508			dtype=self.float_t) for ci in point_cindex]
509		neigh_index =  xp.stack(xp.meshgrid( *aX, indexing='ij'),axis=-1)
510		neigh_index = neigh_index.reshape(-1,neigh_index.shape[-1])
511
512		# Check which neighbors are close enough
513		offset = neigh_index-point_cindex
514		close = np.sum(offset**2,axis=-1) < gridRadius**2
515
516		# Check which neighbors are in the domain (periodicity omitted)
517		neigh = self.PointFromIndex(neigh_index)
518		bottom,top = self.corners
519		inRange = np.all(np.logical_and(bottom<neigh,neigh<top),axis=-1)
520
521		return neigh[np.logical_and(close,inRange)]

Returns the neighbors around a point on the grid. Geometry last convention Inputs:

  • point (array): geometry last
  • gridRadius (scalar): given in pixels
def SetFactor(self, radius=None, value=None, gradient=None):
 25def SetFactor(self,radius=None,value=None,gradient=None):
 26	"""
 27	This function setups additive factorization around the seeds.
 28	Inputs (optional): 
 29	- radius.
 30		Positive number -> approximate radius, in pixels, of source factorization.
 31		-1 -> Factorization over all the domain. 
 32		None -> source factorization over all the domain
 33	- value (optional).
 34	  callable, array -> approximate values of the solution. 
 35	  None -> reconstructed from the metric.
 36	- gradient (optional) 
 37	  callable, array -> approximate gradient of the solution.
 38	  Obtained from the values by automatic differentiation if unspecified.
 39
 40	Outputs : the subgrid used for factorization
 41	Side effect : sets 'factoringValues', 'factoringGradients', 
 42	   and in the case of a subgrid 'factoringIndexShift'
 43	"""
 44	# Set the factoring grid points
 45	if radius is None:
 46		radius = self.get('factoringRadius',10)
 47
 48	if radius<0 or radius>=np.max(self.shape):
 49		factGrid = self.Grid()
 50		self.pop('factoringIndexShift',None)
 51	else:
 52		seed_ci =  self.PointFromIndex(self['seed'],to=True)
 53		bottom = [max(0,int(np.floor(ci)-radius)) for ci in seed_ci]
 54		top = [min(si,int(np.ceil(ci)+radius)+1) for ci,si in zip(seed_ci,self.shape)]
 55		self['factoringIndexShift'] = bottom
 56		aX = [self.xp.arange(bi,ti,dtype=self.float_t) for bi,ti in zip(bottom,top)]
 57		factGrid = ad.array(np.meshgrid(*aX,indexing='ij'))
 58		factGrid = np.moveaxis(self.PointFromIndex(np.moveaxis(factGrid,0,-1)),-1,0)
 59#			raise ValueError("dictIn.SetFactor : unsupported radius type")
 60
 61	# Set the values
 62	if value is None:
 63		value = self.get('factoringPointChoice','Seed')
 64
 65	if isinstance(value,str):
 66		seed = self['seed']
 67		if 'metric' in self: metric = self['metric']
 68		elif self['model'].startswith('Isotropic'): metric = Metrics.Isotropic(1)
 69		else: raise ValueError("dictIn.SetFactor error : no metric found")
 70		if 'cost' in self: metric = metric.with_cost(self['cost'])
 71		fullGrid = factGrid if factGrid.shape[1:]==self.shape else self.Grid()
 72
 73		diff = lambda x : x-fd.as_field(seed,x.shape[1:],depth=1)
 74		if value in ('Key','Seed'):
 75			metric.set_interpolation(fullGrid) # Default order 1 interpolation suffices
 76			value = metric.at(seed)
 77		elif value=='Current': # Not really working ?
 78			# Strictly speaking, we are not interpolating the metric, 
 79			# since the point x at which it is evaluated lies on the grid
 80			metric.set_interpolation(fullGrid) 
 81			value = lambda x : metric.at(x).norm(diff(x))
 82			#Cheating : not differentiating w.r.t position, but should be enough here
 83			gradient = lambda x: metric.at(x).gradient(diff(x)) 
 84		elif value=='Both':
 85			# Pray that AD goes well ...
 86			metric.set_interpolation(fullGrid,order=3) # Need to differentiate w.r.t x
 87			value = lambda x : 0.5*(metric.at(seed).norm(diff(x)) + 
 88				metric.at(x).norm(diff(x)))
 89		else:
 90			raise ValueError(f"dictIn.SetFactor error : unsupported "
 91				"factoringPointChoice : {value} .")
 92
 93	if callable(value):
 94		if gradient is None:
 95			factGrid_ad = ad.Dense.identity(constant=factGrid, shape_free=(self.vdim,))
 96			value = value(factGrid_ad)
 97		else:
 98			value = value(factGrid)
 99
100	if isinstance(value,Metrics.Base):
101		factDiff = factGrid - fd.as_field(self['seed'],factGrid.shape[1:],depth=1)
102		if gradient is None: 
103			gradient = value.gradient(factDiff)
104			# Avoids recomputation, but generates NaNs at the seed points.
105			value = lp.dot_VV(gradient,factDiff)
106			value[np.isnan(value)]=0 
107		else:
108			value = value.norm(factDiff)
109
110	if ad.is_ad(value):
111		gradient = value.gradient()
112		value = value.value
113
114	if not ad.isndarray(value): 
115		raise ValueError(f"dictIn.SetFactor : unsupported value type {type(value)}")
116
117	#Set the gradient
118	if callable(gradient):
119		gradient = gradient(factGrid)
120
121	if not ad.isndarray(gradient): 
122		raise ValueError(f"dictIn.SetFactor : unsupported gradient type {type(gradient)}")
123
124	self["factoringValues"] = value
125	self["factoringGradients"] = np.moveaxis(gradient,0,-1) # Geometry last in c++ code...
126
127	for key in ('factoringRadius', 'factoringPointChoice'): self.pop(key,None)
128
129	return factGrid

This function setups additive factorization around the seeds. Inputs (optional):

  • radius. Positive number -> approximate radius, in pixels, of source factorization. -1 -> Factorization over all the domain. None -> source factorization over all the domain
  • value (optional). callable, array -> approximate values of the solution. None -> reconstructed from the metric.
  • gradient (optional) callable, array -> approximate gradient of the solution. Obtained from the values by automatic differentiation if unspecified.

Outputs : the subgrid used for factorization Side effect : sets 'factoringValues', 'factoringGradients', and in the case of a subgrid 'factoringIndexShift'

def SetSphere(self, dimsp, separation=5, radius=1.1):
162def SetSphere(self,dimsp,separation=5,radius=1.1):
163	"""
164	Setups the manifold R^(d-k) x S^k, using the equatorial projection for the sphere S^k.
165	Only compatible with the GPU accelerated eikonal solver. 
166	Inputs : 
167		dimsp (int): the discretization of a half sphere involves dimsp^k pixels.
168		radius (optional, float > 1): each local chart has base domain [-radius,radius]^k.
169		  (This is NOT the radius of the sphere, which is 1, but of the parametrization.)
170		separation (optional, int) : number of pixels separating the two local charts.
171			Set to false to define a projective space using a single chart. 
172	Side effects : 
173		Sets chart_mapping,chart_jump, dimensions, origin, gridScales.
174	Output : 
175		- Conversion utilities, between the equatorial plane and the sphere 
176			(and grid in the non-projective case)
177	"""
178	if 'chart_mapping' in self: # Some space R^(d-k) x S^k is already set
179		vdim_rect = self.vdim - self['chart_mapping'].ndim+1
180		dims_rect = self['dims'][:vdim_rect]
181		origin_rect = self['origin'][:vdim_rect]
182		gridScales_rect = self['gridScales'][:vdim_rect]
183	elif 'dims' in self: # Some rectangle R^(d-k) is already set
184		dims_rect = self['dims']
185		origin_rect = self.get('origin',np.zeros_like(dims_rect))
186		if 'gridScales' in self: gridScales_rect = self['gridScales']
187		else: gridScales_rect = self['gridScale']*np.ones_like(dims_rect) 
188	else: # Nothing set. All coordinates are sphere like, d=k
189		dims_rect = self.xp.array(tuple(),dtype=self.float_t)
190		origin_rect = dims_rect.copy()
191		gridScales_rect = dims_rect.copy()
192
193	vdim_rect = len(dims_rect)
194	vdim_sphere = self.vdim-vdim_rect # Dimension of the sphere
195	gridScale_sphere = 2*radius/dimsp
196
197	if separation: # Sphere manifold, described using two charts
198		separation_radius = separation*gridScale_sphere /2
199		center_radius = radius + separation_radius
200
201		dims_sphere = (2*dimsp + separation,) + (dimsp,)*(vdim_sphere-1)
202		origin_sphere = (-2*radius-separation_radius,) + (-radius,)*(vdim_sphere-1)
203
204		def sphere_fromgrid(x):
205			"""
206			Produces a point of the equatorial plane, and a boolean indicating which projection to 
207			use (False:south pole, True:north pole), from a grid point.
208			"""
209			assert len(x)==vdim_sphere
210			x = x.copy()
211			chart = np.where(np.abs(x[0])>=separation_radius,np.sign(x[0]),np.nan)
212			x[0] -= center_radius*chart
213			return x,chart
214
215		def sphere_togrid(x,chart):
216			"""
217			Produces a point of the original grid, from a point of the equatorial plane 
218			and a boolean indicating the active chart.
219			"""
220			assert len(x)==vdim_sphere
221			x=x.copy()
222			mixed_chart = x[0]*chart < -radius # The two coordinate charts would get mixed
223			x[:,mixed_chart] = np.nan
224			x[0] += chart*center_radius
225			return x
226	else: # Projective space, described using a single chart
227		dims_sphere = (dimsp,)*vdim_sphere
228		origin_sphere = (-radius,)*vdim_sphere
229
230	dims_sphere,origin_sphere,gridScales_sphere = [self.array_float_caster(e) 
231		for e in (dims_sphere,origin_sphere, (gridScale_sphere,)*vdim_sphere)]
232
233	self['dims'] = np.concatenate((dims_rect,dims_sphere),axis=0)
234	self['gridScales'] = np.concatenate(
235		(gridScales_rect,gridScales_sphere),axis=0)
236	self.pop('gridScale',None)
237	self['origin'] = np.concatenate((origin_rect,origin_sphere),axis=0)
238
239	# Produce a coordinate system, and set the jumps, etc
240	aX = self.Axes()[vdim_rect:]
241	X = self.array_float_caster(np.meshgrid(*aX,indexing='ij'))
242	if separation: X,chart = sphere_fromgrid(X)
243	X2 = lp.dot_VV(X,X)
244
245	# Geodesics jump when the are sufficiently far away from the fundamental domain.
246	radius_jump = (1+radius)/2
247	self['chart_jump'] = X2 > radius_jump**2
248	if separation:
249		self['chart_mapping'] = sphere_togrid(
250			sphere_fromsphere(sphere_tosphere(X,chart),-chart),-chart)
251		return {'from_grid':sphere_fromgrid,'to_grid':sphere_togrid,
252			'from_sphere':sphere_fromsphere,'to_sphere':sphere_tosphere} 
253	else:
254		self['chart_mapping'] = proj_fromsphere(-proj_tosphere(X))
255		return {'from_sphere':proj_fromsphere,'to_sphere':proj_tosphere}

Setups the manifold R^(d-k) x S^k, using the equatorial projection for the sphere S^k. Only compatible with the GPU accelerated eikonal solver. Inputs : dimsp (int): the discretization of a half sphere involves dimsp^k pixels. radius (optional, float > 1): each local chart has base domain [-radius,radius]^k. (This is NOT the radius of the sphere, which is 1, but of the parametrization.) separation (optional, int) : number of pixels separating the two local charts. Set to false to define a projective space using a single chart. Side effects : Sets chart_mapping,chart_jump, dimensions, origin, gridScales. Output : - Conversion utilities, between the equatorial plane and the sphere (and grid in the non-projective case)

factoringPointChoice
525	@property
526	def factoringPointChoice(self): return DictIn_detail.factoringPointChoice(self)
Inherited Members
collections.abc.MutableMapping
pop
popitem
clear
update
setdefault
collections.abc.Mapping
get
items
values