Skip to content

Commit 151b903

Browse files
WIP: partition
1 parent 907059a commit 151b903

File tree

3 files changed

+308
-11
lines changed

3 files changed

+308
-11
lines changed

nutils/topology.py

Lines changed: 285 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ def refine(self, n):
391391
n = n[0]
392392
return self if n <= 0 else self.refined.refine(n-1)
393393

394-
def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None, *, arguments=None):
394+
def _trim(self, levelset, maxrefine, ndivisions=8, leveltopo=None, *, arguments=None):
395395
'trim element along levelset'
396396

397397
if arguments is None:
@@ -425,7 +425,19 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None
425425
mask[indices] = False
426426
refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions))
427427
log.debug('cache', fcache.stats)
428-
return SubsetTopology(self, refs, newboundary=name)
428+
return refs
429+
430+
def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None, *, arguments=None):
431+
refs = self._trim(levelset, maxrefine, ndivisions, leveltopo, arguments=arguments)
432+
return SubsetTopology(self, refs, newboundary=name)
433+
434+
@log.withcontext
435+
@types.apply_annotations
436+
def partition(self, levelset:function.asarray, maxrefine:types.strictint, posname:types.strictstr, negname:types.strictstr, *, ndivisions=8, arguments=None):
437+
partsroot = function.Root('parts', 0)
438+
pos = self._trim(levelset, maxrefine=maxrefine, ndivisions=ndivisions, arguments=arguments)
439+
refs = tuple((pref, bref-pref) for bref, pref in zip(self.references, pos))
440+
return PartitionedTopology(self, partsroot, refs, (posname, negname))
429441

430442
def subset(self, topo, newboundary=None, strict=False):
431443
'intersection'
@@ -1883,7 +1895,7 @@ def basis(self, name, *args, **kwargs):
18831895
if name == 'discont':
18841896
return super().basis(name, *args, **kwargs)
18851897
else:
1886-
return function.DisjointUnionBasis(topo.basis(name, *args, **kwargs) for topo in self._topos)
1898+
return function.DisjointUnionBasis(tuple(topo.basis(name, *args, **kwargs) for topo in self._topos), function.SelectChain(self.roots))
18871899

18881900
class SubsetTopology(Topology):
18891901
'trimmed'
@@ -2070,6 +2082,26 @@ def getitem(self, item):
20702082
def boundary(self):
20712083
return self.basetopo.boundary.refined
20722084

2085+
@property
2086+
def interfaces(self):
2087+
references = []
2088+
transforms = []
2089+
opposites = []
2090+
for ref, trans in zip(self.basetopo.references, self.basetopo.transforms):
2091+
for ichild, (childconn, (ctrans, cref)) in enumerate(zip(ref.connectivity, ref.children)):
2092+
for iedge, (ioppchild, (etrans, eref)) in enumerate(zip(childconn, cref.edges)):
2093+
if ioppchild >= 0:
2094+
references.append(eref)
2095+
transforms.append(trans+(ctrans,etrans))
2096+
ioppedge = ref.connectivity[ioppchild].index(ichild)
2097+
oppctrans, oppcref = ref.children[ioppchild]
2098+
oppetrans = oppcref.edge_transforms[ioppedge]
2099+
opposites.append(trans+(oppctrans,oppetrans))
2100+
newifaces = Topology(elementseq.asreferences(references, self.ndims-1),
2101+
transformseq.PlainTransforms(transforms, self.ndims-1),
2102+
transformseq.PlainTransforms(opposites, self.ndims-1))
2103+
return DisjointUnionTopology([self.basetopo.interfaces.refined, newifaces])
2104+
20732105
@property
20742106
def connectivity(self):
20752107
offsets = numpy.cumsum([0] + [ref.nchildren for ref in self.basetopo.references])
@@ -2569,6 +2601,256 @@ def interfaces(self):
25692601
def getitem(self, item):
25702602
return WithIdentifierTopology(self._parent.getitem(item), self._root, self._identifier)
25712603

2604+
class PartitionedTopology(DisjointUnionTopology):
2605+
2606+
__slots__ = 'basetopo', 'refs', 'names', 'nparts', 'partsroot', '_parts', '_partstransforms', '_nrefined'
2607+
__cache__ = 'boundary', 'interfaces', 'refined'
2608+
2609+
@types.apply_annotations
2610+
def __init__(self, basetopo:stricttopology, partsroot:function.strictroot, refs:types.tuple[types.tuple[element.strictreference]], names:types.tuple[types.strictstr], *, _nrefined=0):
2611+
if len(refs) != len(basetopo):
2612+
raise ValueError('Expected {} refs tuples but got {}.'.format(len(basetopo), len(refs)))
2613+
self.nparts = len(refs[0]) if refs else len(names)
2614+
if not all(len(r) == self.nparts for r in refs):
2615+
raise ValueError('Variable number of parts.')
2616+
if len(names) != self.nparts:
2617+
raise ValueError('Expected {} names, one for every part, but got {}.'.format(self.nparts, len(names)))
2618+
if any(':' in name for name in names):
2619+
raise ValueError('Names may not contain colons.')
2620+
if self.nparts == 0:
2621+
raise ValueError('A partition consists of at least one part, but got zero.')
2622+
assert all(functools.reduce(operator.or_, prefs) == bref for bref, prefs in zip(basetopo.references, refs)), 'not a partition: union of parts is smaller than base'
2623+
2624+
self.basetopo = basetopo
2625+
self.refs = refs
2626+
self.names = names
2627+
self.partsroot = partsroot
2628+
self._nrefined = _nrefined
2629+
2630+
self._partstransforms = transformseq.IdentifierTransforms(0, partsroot.name, self.nparts)
2631+
for i in range(_nrefined):
2632+
self._partstransforms = self._partstransforms.refined(elementseq.asreferences([element.PointReference()], 0))
2633+
indices = tuple(types.frozenarray(numpy.where(list(map(bool, prefs)))[0]) for prefs in zip(*refs))
2634+
self._parts = tuple(WithIdentifierTopology(SubsetTopology(basetopo, prefs), partsroot, self._partstransforms[i:i+1]) for i, prefs in enumerate(zip(*refs)))
2635+
super().__init__(self._parts, names)
2636+
2637+
def getitem(self, item):
2638+
if item in self.names:
2639+
return _SubsetOfPartitionedTopology(self, {item})
2640+
else:
2641+
topo = self.basetopo.getitem(item)
2642+
if not topo:
2643+
return topo * EmptyTopology((self.partsroot,), 0)
2644+
refs = tuple(tuple(ref & bref for ref in self.refs[self.basetopo.transforms.index(trans)]) for bref, trans in zip(topo.references, topo.transforms))
2645+
return PartitionedTopology(topo, self.partsroot, refs, self.names)
2646+
2647+
@property
2648+
def boundary(self):
2649+
baseboundary = self.basetopo.boundary
2650+
brefs = []
2651+
for bref, btrans in zip(baseboundary.references, baseboundary.transforms):
2652+
ielem, etrans = self.basetopo.transforms.index_with_tail(btrans)
2653+
todims = tuple(t[-1].fromdims for t in self.basetopo.transforms[ielem])
2654+
brefs.append(tuple(pref.edge_refs[transform.index_edge_transforms(pref.edge_transforms, etrans, todims)] for pref in self.refs[ielem]))
2655+
return PartitionedTopology(baseboundary, self.partsroot, brefs, self.names, _nrefined=self._nrefined)
2656+
2657+
@property
2658+
def interfaces(self):
2659+
baseifaces = self.basetopo.interfaces
2660+
basereferences = {(a, b): [] for a in self.names for b in self.names}
2661+
baseindices = {(a, b): [] for a in self.names for b in self.names}
2662+
for ieelem, (eref, etrans, oppetrans) in enumerate(zip(baseifaces.references, baseifaces.transforms, baseifaces.opposites)):
2663+
ielem, tail = self.basetopo.transforms.index_with_tail(etrans)
2664+
ioppelem, opptail = self.basetopo.transforms.index_with_tail(oppetrans)
2665+
todims = tuple(t[-1].fromdims for t in self.basetopo.transforms[ielem])
2666+
erefs = tuple(filter(lambda item: item[1], ((i, ref.edge_refs[transform.index_edge_transforms(ref.edge_transforms, tail, todims)]) for i, ref in zip(self.names, self.refs[ielem]))))
2667+
opperefs = tuple(filter(lambda item: item[1], ((i, ref.edge_refs[transform.index_edge_transforms(ref.edge_transforms, opptail, todims)]) for i, ref in zip(self.names, self.refs[ioppelem]))))
2668+
checkeref = eref.empty
2669+
for aname, aeref in erefs:
2670+
for bname, beref in opperefs:
2671+
parteref = aeref & beref
2672+
if parteref:
2673+
basereferences[aname, bname].append(parteref)
2674+
baseindices[aname, bname].append(ieelem)
2675+
checkeref |= parteref
2676+
assert checkeref == eref
2677+
baseindices = {p: types.frozenarray(i, dtype=int) for p, i in baseindices.items()}
2678+
2679+
newedges = {}
2680+
def addnewedge(ielem, etrans):
2681+
edges = newedges.setdefault(ielem, [])
2682+
assert etrans not in edges
2683+
iedge = len(edges)
2684+
edges.append(etrans)
2685+
return ielem, iedge
2686+
newreferences = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]}
2687+
newtransforms = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]}
2688+
newopposites = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]}
2689+
for ibase, (baseref, partrefs, basetrans) in enumerate(zip(self.basetopo.references, self.refs, self.basetopo.transforms)):
2690+
todims = tuple(t[-1].fromdims for t in basetrans)
2691+
pool = {}
2692+
for aname, aref in zip(self.names, partrefs):
2693+
if not aref:
2694+
continue
2695+
for aetrans, aeref in aref.edges[baseref.nedges:]:
2696+
if not aeref:
2697+
continue
2698+
points = types.frozenarray(aetrans.apply(aeref.getpoints('bezier', 2).coords), copy=False)
2699+
bname, beref, betrans = pool.pop(points, (None, None, None))
2700+
if beref is None:
2701+
pool[points] = aname, aeref, aetrans
2702+
else:
2703+
assert aname != bname, 'elements are not supposed to count internal interfaces as edges'
2704+
# assert aeref == beref # disabled: aeref.trans is beref.trans.flipped if aeref is a ManifoldReference
2705+
if self.names.index(aname) <= self.names.index(bname):
2706+
iface = aname, bname
2707+
else:
2708+
iface = bname, aname
2709+
aetrans, betrans, aeref, beref = betrans, aetrans, beref, aeref
2710+
newreferences[iface].append(aeref)
2711+
newtransforms[iface].append(addnewedge(ibase, aetrans.separate(todims)))
2712+
newopposites[iface].append(addnewedge(ibase, betrans.separate(todims)))
2713+
2714+
assert not pool, 'some interal edges have no opposites'
2715+
2716+
if newedges:
2717+
newielems, newedges = zip(*sorted(newedges.items(), key=lambda item: item[0]))
2718+
newoffsets = dict(zip(newielems, numpy.cumsum([0, *map(len, newedges)])))
2719+
newedges = transformseq.TrimmedEdgesTransforms(self.basetopo.transforms[numpy.asarray(newielems)], newedges)
2720+
itopos = []
2721+
inames = []
2722+
T = lambda i, j: transformseq.PlainTransforms(((*self._partstransforms[i][0], self._partstransforms[j][0][0]),), 0, 0)
2723+
for i, a in enumerate(self.names):
2724+
itopos.append(Topology(self.roots,
2725+
elementseq.asreferences(basereferences[a, a], self.ndims-1),
2726+
baseifaces.transforms[baseindices[a, a]]*T(i, i),
2727+
baseifaces.opposites[baseindices[a, a]]*T(i, i)))
2728+
inames.append('{0}:{0}'.format(a))
2729+
for j, b in enumerate(self.names[i+1:], i+1):
2730+
base = Topology(self.roots,
2731+
elementseq.asreferences(basereferences[a, b] + basereferences[b, a], self.ndims-1),
2732+
transformseq.chain((baseifaces.transforms[baseindices[a, b]], baseifaces.opposites[baseindices[b, a]]), self.basetopo.transforms.todims)*T(i, j),
2733+
transformseq.chain((baseifaces.opposites[baseindices[a, b]], baseifaces.transforms[baseindices[b, a]]), self.basetopo.transforms.todims)*T(j, i))
2734+
if newreferences[a, b]:
2735+
newreferencesab = elementseq.asreferences(newreferences[a, b], self.ndims-1)
2736+
newtransformsab = newedges[numpy.fromiter((newoffsets[ielem]+iedge for ielem, iedge in newtransforms[a, b]), dtype=int)]
2737+
newoppositesab = newedges[numpy.fromiter((newoffsets[ielem]+iedge for ielem, iedge in newopposites[a, b]), dtype=int)]
2738+
new = Topology(self.roots, newreferencesab, newtransformsab*T(i, j), newoppositesab*T(j, i))
2739+
itopos.append(DisjointUnionTopology((base, new)))
2740+
else:
2741+
itopos.append(base)
2742+
inames.append('{}:{}'.format(a, b))
2743+
return DisjointUnionTopology(itopos, inames)
2744+
2745+
def __sub__(self, other):
2746+
if self == other:
2747+
return self.empty
2748+
elif isinstance(other, _SubsetOfPartitionedTopology) and other._partition == self:
2749+
remainder = frozenset(self.names) - frozenset(other._names)
2750+
if remainder:
2751+
return _SubsetOfPartitionedTopology(self, remainder)
2752+
else:
2753+
return self.empty
2754+
else:
2755+
return super().__sub__(other)
2756+
2757+
@property
2758+
def refined(self):
2759+
refbasetopo = self.basetopo.refined
2760+
refbindex = refbasetopo.transforms.index
2761+
refinedrefs = [crefs for refs in self.refs for crefs in zip(*(ref.child_refs for ref in refs))]
2762+
indices = numpy.argsort([refbindex(ctrans)
2763+
for trans, ref in zip(self.basetopo.transforms, self.references)
2764+
for ctrans in transform.child_transforms(trans, ref)])
2765+
refinedrefs = tuple(map(refinedrefs.__getitem__, indices))
2766+
return PartitionedTopology(refbasetopo, self.partsroot, refinedrefs, self.names, _nrefined=self._nrefined+1)
2767+
2768+
class _SubsetOfPartitionedTopology(DisjointUnionTopology):
2769+
2770+
__slots__ = '_partition', '_names'
2771+
__cache__ = 'boundary', 'interfaces'
2772+
2773+
@types.apply_annotations
2774+
def __init__(self, partition: stricttopology, names: frozenset):
2775+
self._partition = partition
2776+
if not names <= frozenset(partition.names):
2777+
raise ValueError('Not a subset of the partition.')
2778+
if not all(isinstance(name, str) for name in names):
2779+
raise ValueError('All names should be str objects.')
2780+
self._names = tuple(sorted(names, key=partition.names.index))
2781+
super().__init__(tuple(self._partition._parts[self._partition.names.index(name)] for name in self._names), self._names)
2782+
2783+
def __getitem__(self, item):
2784+
if item in self._names:
2785+
return _SubsetOfPartitionedTopology(self._partition, {item})
2786+
elif item in self._partition.names:
2787+
return self.empty
2788+
else:
2789+
topo = self._partition.getitem(item)
2790+
assert not isinstance(topo, _SubsetOfPartitionedTopology) # this is covered by the above two conditionals
2791+
if not topo:
2792+
return topo
2793+
elif isinstance(topo, PartitionedTopology):
2794+
return _SubsetOfPartitionedTopology(topo, self._names)
2795+
else:
2796+
raise NotImplementedError
2797+
2798+
@property
2799+
def boundary(self):
2800+
# The boundary of this subset consists of the boundary of the base that
2801+
# touches this subset and the interfaces between all parts in this subset
2802+
# and all parts not in this subset. All interfaces are grouped and named by
2803+
# the parts not in this subset: given a partition A, B of Ω, then
2804+
# `Ω['A'].boundary['B']` is the same as `Ω.interfaces['A:B']` or
2805+
# `~Ω.interfaces['B:A']`, whichever exists.
2806+
topos = []
2807+
names = []
2808+
for b in self._partition.names: # parts not in this subset
2809+
if b in self._names:
2810+
continue
2811+
btopos = []
2812+
for a in self._names: # parts in this subset
2813+
if self._partition.names.index(a) <= self._partition.names.index(b):
2814+
btopos.append(self._partition.interfaces.getitem('{}:{}'.format(a, b)))
2815+
else:
2816+
btopos.append(~self._partition.interfaces.getitem('{}:{}'.format(b, a)))
2817+
topos.append(DisjointUnionTopology(btopos))
2818+
names.append(b)
2819+
for name in self._names:
2820+
topos.append(self._partition.boundary.getitem(name))
2821+
groups = {}
2822+
return DisjointUnionTopology(topos, names)
2823+
2824+
@property
2825+
def interfaces(self):
2826+
topos = []
2827+
names = []
2828+
for i, a in enumerate(self._names):
2829+
for b in self._names[i:]:
2830+
topos.append(self._partition.interfaces.getitem('{}:{}'.format(a, b)))
2831+
names.append('{}:{}'.format(a, b))
2832+
return DisjointUnionTopology(topos, names)
2833+
2834+
def __or__(self, other):
2835+
if isinstance(other, _SubsetOfPartitionedTopology) and other._partition == self._partition:
2836+
return _SubsetOfPartitionedTopology(self._partition, frozenset(self._names) | frozenset(other._names))
2837+
else:
2838+
return super().__or__(other)
2839+
2840+
def __rsub__(self, other):
2841+
if self._partition == other or self._partition.basetopo == other:
2842+
remainder = frozenset(self._partition.names) - frozenset(self._names)
2843+
if remainder:
2844+
return _SubsetOfPartitionedTopology(self._partition, remainder)
2845+
else:
2846+
return self.empty
2847+
else:
2848+
return super().__rsub__(other)
2849+
2850+
@property
2851+
def refined(self):
2852+
return _SubsetOfPartitionedTopology(self._partition.refined, self._names)
2853+
25722854
class PatchBoundary(types.Singleton):
25732855

25742856
__slots__ = 'id', 'dim', 'side', 'reverse', 'transpose'

nutils/transform.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,10 @@ def __init__(self, ndims:types.strictint, trans:stricttransformitem):
635635
def flipped(self):
636636
return Manifold(self.fromdims, self.trans.flipped)
637637

638+
@property
639+
def isflipped(self):
640+
return self.trans.isflipped
641+
638642
def swapdown(self, other):
639643
if isinstance(other, (TensorChild, SimplexChild)):
640644
return ScaledUpdim(other, self), Identity(self.fromdims)

tests/test_finitecell.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ def test_trimtopright(self):
358358
self.assertEqual(len(self.domain5.boundary['trimtopright']), 6)
359359

360360

361+
@parametrize
361362
class partialtrim(TestCase):
362363

363364
# Test setup:
@@ -372,8 +373,13 @@ class partialtrim(TestCase):
372373
def setUp(self):
373374
self.topo, self.geom = mesh.rectilinear([2,2])
374375
geom = self.geom
375-
self.topoA = self.topo.trim(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1)
376-
self.topoB = self.topo - self.topoA
376+
if self.method == 'trim':
377+
self.topoA = self.topo.trim(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1)
378+
self.topoB = self.topo - self.topoA
379+
elif self.method == 'partition':
380+
self.partitioned = self.topo.partition(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1, posname='A', negname='B')
381+
self.topoA = self.partitioned['A']
382+
self.topoB = self.partitioned['B']
377383

378384
def test_topos(self):
379385
self.assertEqual(len(self.topoA), 4)
@@ -382,26 +388,27 @@ def test_topos(self):
382388
def test_boundaries(self):
383389
self.assertEqual(len(self.topoA.boundary), 11)
384390
self.assertEqual(len(self.topoB.boundary), 8)
385-
self.assertEqual(len(self.topoA.boundary['trimmed']), 5)
386-
self.assertEqual(len(self.topoB.boundary['trimmed']), 5)
391+
self.assertEqual(len(self.topoA.boundary['B' if self.method == 'partition' else 'trimmed']), 5)
392+
self.assertEqual(len(self.topoB.boundary['A' if self.method == 'partition' else 'trimmed']), 5)
387393

388394
def test_interfaces(self):
389395
self.assertEqual(len(self.topoA.interfaces), 4)
390396
self.assertEqual(len(self.topoB.interfaces), 1)
391397

392398
def test_transforms(self):
393-
self.assertEqual(set(self.topoA.boundary['trimmed'].transforms), set(self.topoB.boundary['trimmed'].opposites))
394-
self.assertEqual(set(self.topoB.boundary['trimmed'].transforms), set(self.topoA.boundary['trimmed'].opposites))
399+
self.assertEqual(set(self.topoA.boundary['B' if self.method == 'partition' else 'trimmed'].transforms), set(self.topoB.boundary['A' if self.method == 'partition' else 'trimmed'].opposites))
400+
self.assertEqual(set(self.topoB.boundary['A' if self.method == 'partition' else 'trimmed'].transforms), set(self.topoA.boundary['B' if self.method == 'partition' else 'trimmed'].opposites))
395401

396402
def test_opposites(self):
397403
ielem = function.elemwise(self.topo.roots, self.topo.transforms, self.topo.ndims, numpy.arange(4))
398-
sampleA = self.topoA.boundary['trimmed'].sample('uniform', 1)
399-
sampleB = self.topoB.boundary['trimmed'].sample('uniform', 1)
404+
sampleA = self.topoA.boundary['B' if self.method == 'partition' else 'trimmed'].sample('uniform', 1)
405+
sampleB = self.topoB.boundary['A' if self.method == 'partition' else 'trimmed'].sample('uniform', 1)
400406
self.assertEqual(set(sampleB.eval(ielem)), {0,1})
401407
self.assertEqual(set(sampleB.eval(function.opposite(ielem))), {0,1,2})
402408
self.assertEqual(set(sampleA.eval(ielem)), {0,1,2})
403409
self.assertEqual(set(sampleA.eval(function.opposite(ielem))), {0,1})
404410

411+
@parametrize.enable_if(lambda method, **kwargs: method != 'partition')
405412
def test_baseboundaries(self):
406413
# the base implementation should create the correct boundary topology but
407414
# without interface opposites and without the trimmed group
@@ -418,3 +425,7 @@ def test_volumes(self):
418425
lhs = self.topoB.integrate(f.grad(geom)*function.J(geom), ischeme='gauss2')
419426
rhs = self.topoB.boundary.integrate(f*function.normal(geom)*function.J(geom), ischeme='gauss2')
420427
numpy.testing.assert_array_almost_equal(lhs, rhs)
428+
429+
430+
partialtrim(method='trim')
431+
partialtrim(method='partition')

0 commit comments

Comments
 (0)