@@ -360,6 +360,77 @@ def sample(self, ischeme: str, degree: int) -> Sample:
360
360
361
361
raise NotImplementedError
362
362
363
+ def elementwise_stack (self , func : function .Array , * , axis : int = 0 ) -> function .Array :
364
+ '''Return the stack of the function bound to every element of this topology.
365
+
366
+ The order of the stack matches the order of the elements of this
367
+ topology. Therefor, for any :class:`Topology` ``topo``
368
+
369
+ topo.elementwise_stack(topo.f_index)
370
+
371
+ is guaranteed to be the same (after evaluation) as
372
+
373
+ numpy.arange(len(topo))
374
+
375
+ Parameters
376
+ ----------
377
+ func : :class:`nutils.function.Array`
378
+ The function to bind to elements and stack.
379
+ axis : :class:`int`
380
+ The axis in the result array along which the elements are stacked.
381
+
382
+ Returns
383
+ -------
384
+ :class:`nutils.function.Array`
385
+ The stack of function at the first element, the second, etc. until
386
+ the last.
387
+
388
+ Examples
389
+ --------
390
+
391
+ Given a ``base`` :class:`Topology` and a ``refinement``, with
392
+ :meth:`Topology.elementwise_stack` we can obtain the element index of
393
+ the ``base`` for each element in the ``refinement``:
394
+
395
+ >>> from nutils import function, mesh
396
+ >>> base, geom = mesh.line(4)
397
+ >>> refinement = base.refined_by([1, 3]).refined_by([5])
398
+ >>> function.eval(refinement.elementwise_stack(base.f_index))
399
+ array([0, 2, 1, 1, 3, 3, 3])
400
+
401
+ Note that refined elements are positioned at the end of the refined
402
+ topology.
403
+
404
+ Notes
405
+ -----
406
+
407
+ This method delegates the work to
408
+ :meth:`Topology.elementwise_stack_last_axis`. Implementations of
409
+ :class:`Topology` should implement the latter.
410
+ '''
411
+
412
+ stack = self .elementwise_stack_last_axis (func )
413
+ axis = numeric .normdim (stack .ndim , axis )
414
+ if axis != stack .ndim - 1 :
415
+ stack = numpy .transpose (stack , (* range (axis ), * range (axis + 1 , stack .ndim ), axis ))
416
+ return stack
417
+
418
+ def elementwise_stack_last_axis (self , func : function .Array ) -> function .Array :
419
+ '''Return the stack along the last axis of the function bound to every element of this topology.
420
+
421
+ Parameters
422
+ ----------
423
+ func : :class:`nutils.function.Array`
424
+ The function to bind to elements and stack.
425
+
426
+ Returns
427
+ -------
428
+ :class:`nutils.function.Array`
429
+ The stack of function at element 0, element 1, etc along the last axis.
430
+ '''
431
+
432
+ raise NotImplementedError
433
+
363
434
@single_or_multiple
364
435
def integrate_elementwise (self , funcs : Iterable [function .Array ], * , degree : int , asfunction : bool = False , ischeme : str = 'gauss' , arguments : Optional [_ArgDict ] = None ) -> Union [List [numpy .ndarray ], List [function .Array ]]:
365
436
'element-wise integration'
@@ -1042,6 +1113,10 @@ def basis_std(self, degree: int, *args, **kwargs) -> function.Array:
1042
1113
def sample (self , ischeme : str , degree : int ) -> Sample :
1043
1114
return Sample .empty (self .spaces , self .ndims )
1044
1115
1116
+ def elementwise_stack_last_axis (self , func : function .Array ) -> function .Array :
1117
+ func = function .Array .cast (func )
1118
+ return function .zeros ((* func .shape , 0 ), dtype = func .dtype )
1119
+
1045
1120
1046
1121
class _DisjointUnion (_TensorialTopology ):
1047
1122
@@ -1096,6 +1171,9 @@ def integrate_elementwise(self, funcs: Iterable[function.Array], *, degree: int,
1096
1171
def sample (self , ischeme : str , degree : int ) -> Sample :
1097
1172
return self .topo1 .sample (ischeme , degree ) + self .topo2 .sample (ischeme , degree )
1098
1173
1174
+ def elementwise_stack_last_axis (self , func : function .Array ) -> function .Array :
1175
+ return numpy .concatenate ([self .topo1 .elementwise_stack_last_axis (func ), self .topo2 .elementwise_stack_last_axis (func )], axis = - 1 )
1176
+
1099
1177
def trim (self , levelset : function .Array , maxrefine : int , ndivisions : int = 8 , name : str = 'trimmed' , leveltopo : Optional [Topology ] = None , * , arguments : Optional [_ArgDict ] = None ) -> Topology :
1100
1178
if leveltopo is not None :
1101
1179
return super ().trim (levelset , maxrefine , ndivisions , name , leveltopo , arguments = arguments )
@@ -1245,6 +1323,10 @@ def basis(self, name: str, degree: Union[int, Sequence[int]], **kwargs) -> funct
1245
1323
def sample (self , ischeme : str , degree : int ) -> Sample :
1246
1324
return self .topo1 .sample (ischeme , degree ) * self .topo2 .sample (ischeme , degree )
1247
1325
1326
+ def elementwise_stack_last_axis (self , func : function .Array ) -> function .Array :
1327
+ func = function .Array .cast (func )
1328
+ return numpy .reshape (self .topo2 .elementwise_stack_last_axis (self .topo1 .elementwise_stack_last_axis (func )), (* func .shape , len (self )))
1329
+
1248
1330
1249
1331
class _Take (_TensorialTopology ):
1250
1332
@@ -1261,6 +1343,9 @@ def __init__(self, parent: Topology, indices: numpy.ndarray) -> None:
1261
1343
def sample (self , ischeme : str , degree : int ) -> Sample :
1262
1344
return self .parent .sample (ischeme , degree ).take_elements (self .indices )
1263
1345
1346
+ def elementwise_stack_last_axis (self , func : function .Array ) -> function .Array :
1347
+ return numpy .take (self .parent .elementwise_stack_last_axis (func ), self .indices , axis = - 1 )
1348
+
1264
1349
1265
1350
class _WithGroupAliases (_TensorialTopology ):
1266
1351
@@ -1307,6 +1392,9 @@ def basis(self, name: str, *args, **kwargs) -> function.Basis:
1307
1392
def sample (self , ischeme : str , degree : int ) -> Sample :
1308
1393
return self .parent .sample (ischeme , degree )
1309
1394
1395
+ def elementwise_stack_last_axis (self , func : function .Array ) -> function .Array :
1396
+ return self .parent .elementwise_stack_last_axis (func )
1397
+
1310
1398
def refine_spaces_unchecked (self , spaces : FrozenSet [str ]) -> Topology :
1311
1399
return _WithGroupAliases (self .parent .refine_spaces (spaces ), self .vgroups , self .bgroups , self .igroups )
1312
1400
@@ -1326,6 +1414,22 @@ def interfaces_spaces_unchecked(self, spaces: FrozenSet[str]) -> Topology:
1326
1414
return _WithGroupAliases (self .parent .interfaces_spaces_unchecked (spaces ), self .igroups , types .frozendict ({}), types .frozendict ({}))
1327
1415
1328
1416
1417
+ class _ElementwiseStack (function .Array ):
1418
+
1419
+ def __init__ (self , func : function .Array , space : str , transforms : transformseq .Transforms , opposites : transformseq .Transforms ):
1420
+ self .func = func
1421
+ self .space = space
1422
+ self .transforms = transforms
1423
+ self .opposites = opposites
1424
+ self .nelems = len (self .transforms )
1425
+ super ().__init__ ((* func .shape , self .nelems ), func .dtype , func .spaces - frozenset ({space }), func .arguments )
1426
+
1427
+ def lower (self , args : function .LowerArgs ) -> evaluable .Array :
1428
+ index = evaluable .loop_index (f'_loop_{ self .space } ' , evaluable .asarray (self .nelems ))
1429
+ func = self .func .lower (args | function .LowerArgs .for_space (self .space , (self .transforms , self .opposites ), index ))
1430
+ return evaluable .loop_concatenate (evaluable .appendaxes (func , (evaluable .asarray (1 ),)), index )
1431
+
1432
+
1329
1433
class TransformChainsTopology (Topology ):
1330
1434
'base class for topologies with transform chains'
1331
1435
@@ -1433,6 +1537,9 @@ def sample(self, ischeme, degree):
1433
1537
transforms += self .opposites ,
1434
1538
return Sample .new (self .space , transforms , points )
1435
1539
1540
+ def elementwise_stack_last_axis (self , func : function .Array ) -> function .Array :
1541
+ return _ElementwiseStack (function .Array .cast (func ), self .space , self .transforms , self .opposites )
1542
+
1436
1543
def _refined_by (self , refine ):
1437
1544
fine = self .refined .transforms
1438
1545
indices0 = numpy .setdiff1d (numpy .arange (len (self )), refine , assume_unique = True )
0 commit comments