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)
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.
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
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
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.
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)
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.
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
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.
232 def keys(self): 233 """ 234 The keys of this dictionary structure. 235 """ 236 return self.store.keys()
The keys of this dictionary structure.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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
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.
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)
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
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.
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.
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).
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
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'
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)
Inherited Members
- collections.abc.MutableMapping
- pop
- popitem
- clear
- update
- setdefault
- collections.abc.Mapping
- get
- items
- values