Skip to content

Commit 381c572

Browse files
WIP: partition
1 parent 992d40b commit 381c572

File tree

3 files changed

+251
-10
lines changed

3 files changed

+251
-10
lines changed

nutils/element.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,18 @@ def edges(self):
8686
def children(self):
8787
return list(zip(self.child_transforms, self.child_refs))
8888

89+
def get_from_trans(self, trans):
90+
if not trans:
91+
return self
92+
if trans[0].todims != self.ndims:
93+
raise ValueError('Expected a transform chain that maps to {} dims but got {}.'.format(self.ndims, trans[0].todims))
94+
if trans[0].fromdims == self.ndims:
95+
return self.child_refs[self.child_transforms.index(trans[0])].get_from_trans(trans[1:])
96+
elif trans[0].fromdims == self.ndims-1:
97+
return self.edge_refs[self.edge_transforms.index(trans[0])].get_from_trans(trans[1:])
98+
else:
99+
raise ValueError
100+
89101
@property
90102
def connectivity(self):
91103
# Nested tuple with connectivity information about edges of children:

nutils/topology.py

Lines changed: 221 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ def refine(self, n):
334334
n = n[0]
335335
return self if n <= 0 else self.refined.refine(n-1)
336336

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

340340
if arguments is None:
@@ -367,7 +367,18 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None
367367
mask[indices] = False
368368
refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions))
369369
log.debug('cache', fcache.stats)
370-
return SubsetTopology(self, refs, newboundary=name)
370+
return refs
371+
372+
def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None, *, arguments=None):
373+
refs = self._trim(levelset, maxrefine, ndivisions, leveltopo, arguments=arguments)
374+
return SubsetTopology(self, refs, newboundary=name)
375+
376+
@log.withcontext
377+
@types.apply_annotations
378+
def partition(self, levelset:function.asarray, maxrefine:types.strictint, posname:types.strictstr, negname:types.strictstr, *, ndivisions=8, arguments=None):
379+
pos = self._trim(levelset, maxrefine=maxrefine, ndivisions=ndivisions, arguments=arguments)
380+
refs = tuple((pref, bref-pref) for bref, pref in zip(self.references, pos))
381+
return PartitionedTopology(self, refs, (posname, negname))
371382

372383
def subset(self, topo, newboundary=None, strict=False):
373384
'intersection'
@@ -2024,6 +2035,214 @@ def interfaces(self):
20242035
def getitem(self, item):
20252036
return WithIdentifierTopology(self._parent.getitem(item), self._token)
20262037

2038+
class PartitionedTopology(DisjointUnionTopology):
2039+
2040+
__slots__ = 'basetopo', 'refs', 'names', 'nparts', '_parts'
2041+
__cache__ = 'boundary', 'interfaces'
2042+
2043+
@types.apply_annotations
2044+
def __init__(self, basetopo:stricttopology, refs:types.tuple[types.tuple[element.strictreference]], names:types.tuple[types.strictstr]):
2045+
if len(refs) != len(basetopo):
2046+
raise ValueError('Expected {} refs tuples but got {}.'.format(len(basetopo), len(refs)))
2047+
self.nparts = len(refs[0]) if refs else len(names)
2048+
if not all(len(r) == self.nparts for r in refs):
2049+
raise ValueError('Variable number of parts.')
2050+
if len(names) != self.nparts:
2051+
raise ValueError('Expected {} names, one for every part, but got {}.'.format(self.nparts, len(names)))
2052+
if any(':' in name for name in names):
2053+
raise ValueError('Names may not contain colons.')
2054+
if self.nparts == 0:
2055+
raise ValueError('A partition consists of at least one part, but got zero.')
2056+
assert all(functools.reduce(operator.or_, prefs) == bref for bref, prefs in zip(basetopo.references, refs)), 'not a partition: union of parts is smaller then base'
2057+
2058+
self.basetopo = basetopo
2059+
self.refs = refs
2060+
self.names = names
2061+
2062+
indices = tuple(types.frozenarray(numpy.where(list(map(bool, prefs)))[0]) for prefs in zip(*refs))
2063+
self._parts = tuple(WithIdentifierTopology(SubsetTopology(basetopo, prefs), name) for name, prefs in zip(names, zip(*refs)))
2064+
super().__init__(self._parts, names)
2065+
2066+
def getitem(self, item):
2067+
if item in self.names:
2068+
return _SubsetOfPartitionedTopology(self, {item})
2069+
else:
2070+
topo = self.basetopo.getitem(item)
2071+
refs = tuple(tuple(ref & bref for ref in self.refs[self.basetopo.transforms.index(trans)]) for bref, trans in zip(topo.references, topo.transforms))
2072+
return PartitionedTopology(topo, refs, self.names)
2073+
2074+
@property
2075+
def boundary(self):
2076+
baseboundary = self.basetopo.boundary
2077+
brefs = []
2078+
for bref, btrans in zip(baseboundary.references, baseboundary.transforms):
2079+
ielem, etrans = self.basetopo.transforms.index_with_tail(btrans)
2080+
brefs.append(tuple(pref.get_from_trans(etrans) for pref in self.refs[ielem]))
2081+
return PartitionedTopology(baseboundary, brefs, self.names)
2082+
2083+
@property
2084+
def interfaces(self):
2085+
baseifaces = self.basetopo.interfaces
2086+
basereferences = {(a, b): [] for a in self.names for b in self.names}
2087+
baseindices = {(a, b): [] for a in self.names for b in self.names}
2088+
for ieelem, (eref, etrans, oppetrans) in enumerate(zip(baseifaces.references, baseifaces.transforms, baseifaces.opposites)):
2089+
ielem, tail = self.basetopo.transforms.index_with_tail(etrans)
2090+
ioppelem, opptail = self.basetopo.transforms.index_with_tail(oppetrans)
2091+
erefs = tuple(filter(lambda item: item[1], ((i, ref.get_from_trans(tail)) for i, ref in zip(self.names, self.refs[ielem]))))
2092+
opperefs = tuple(filter(lambda item: item[1], ((i, ref.get_from_trans(opptail)) for i, ref in zip(self.names, self.refs[ioppelem]))))
2093+
checkeref = eref.empty
2094+
for aname, aeref in erefs:
2095+
for bname, beref in opperefs:
2096+
parteref = aeref & beref
2097+
if parteref:
2098+
basereferences[aname, bname].append(parteref)
2099+
baseindices[aname, bname].append(ieelem)
2100+
checkeref |= parteref
2101+
assert checkeref == eref
2102+
baseindices = {p: types.frozenarray(i, dtype=int) for p, i in baseindices.items()}
2103+
2104+
newreferences = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]}
2105+
newtransforms = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]}
2106+
newopposites = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]}
2107+
for baseref, partrefs, basetrans in zip(self.basetopo.references, self.refs, self.basetopo.transforms):
2108+
pool = {}
2109+
for aname, aref in zip(self.names, partrefs):
2110+
if not aref:
2111+
continue
2112+
for aetrans, aeref in aref.edges[baseref.nedges:]:
2113+
if not aeref:
2114+
continue
2115+
points = types.frozenarray(aetrans.apply(aeref.getpoints('bezier', 2).coords))
2116+
bname, beref, betrans = pool.pop((points, not aetrans.isflipped), (None, None, None))
2117+
if beref is None:
2118+
pool[(points, aetrans.isflipped)] = aname, aeref, aetrans
2119+
else:
2120+
assert aname != bname, 'elements are not supposed to count internal interfaces as edges'
2121+
assert aeref == beref
2122+
atrans = basetrans + (aetrans, transform.Identifier(self.ndims-1, aname))
2123+
btrans = basetrans + (betrans, transform.Identifier(self.ndims-1, bname))
2124+
if self.names.index(aname) <= self.names.index(bname):
2125+
iface = aname, bname
2126+
else:
2127+
iface = bname, aname
2128+
atrans, btrans = btrans, atrans
2129+
newreferences[iface].append(aeref)
2130+
newtransforms[iface].append(atrans)
2131+
newopposites[iface].append(btrans)
2132+
assert not pool, 'some interal edges have no opposites'
2133+
2134+
itopos = []
2135+
inames = []
2136+
for i, a in enumerate(self.names):
2137+
itopos.append(Topology(elementseq.asreferences(basereferences[a, a], self.ndims-1),
2138+
transformseq.WithIdentifierTransforms(baseifaces.transforms[baseindices[a, a]], a),
2139+
transformseq.WithIdentifierTransforms(baseifaces.opposites[baseindices[a, a]], a)))
2140+
inames.append('{0}:{0}'.format(a))
2141+
for b in self.names[i+1:]:
2142+
base = Topology(elementseq.asreferences(basereferences[a, b] + basereferences[b, a], self.ndims-1),
2143+
transformseq.WithIdentifierTransforms(transformseq.chain((baseifaces.transforms[baseindices[a, b]], baseifaces.opposites[baseindices[b, a]]), self.ndims-1), a),
2144+
transformseq.WithIdentifierTransforms(transformseq.chain((baseifaces.opposites[baseindices[a, b]], baseifaces.transforms[baseindices[b, a]]), self.ndims-1), b))
2145+
new = Topology(elementseq.asreferences(newreferences[a, b], self.ndims-1),
2146+
transformseq.PlainTransforms(newtransforms[a, b], self.ndims-1),
2147+
transformseq.PlainTransforms(newopposites[a, b], self.ndims-1))
2148+
itopos.append(DisjointUnionTopology((base, new)))
2149+
inames.append('{}:{}'.format(a, b))
2150+
return DisjointUnionTopology(itopos, inames)
2151+
2152+
def __sub__(self, other):
2153+
if self == other:
2154+
return EmptyTopology(self.ndims)
2155+
elif isinstance(other, _SubsetOfPartitionedTopology) and other._partition == self:
2156+
remainder = frozenset(self.names) - frozenset(other._names)
2157+
if remainder:
2158+
return _SubsetOfPartitionedTopology(self, remainder)
2159+
else:
2160+
return EmptyTopology(self.ndims)
2161+
else:
2162+
return super().__sub__(other)
2163+
2164+
class _SubsetOfPartitionedTopology(DisjointUnionTopology):
2165+
2166+
__slots__ = '_partition', '_names'
2167+
__cache__ = 'boundary', 'interfaces'
2168+
2169+
@types.apply_annotations
2170+
def __init__(self, partition: stricttopology, names: frozenset):
2171+
self._partition = partition
2172+
if not names <= frozenset(partition.names):
2173+
raise ValueError('Not a subset of the partition.')
2174+
if not all(isinstance(name, str) for name in names):
2175+
raise ValueError('All names should be str objects.')
2176+
self._names = tuple(sorted(names, key=partition.names.index))
2177+
super().__init__(tuple(self._partition._parts[self._partition.names.index(name)] for name in self._names), self._names)
2178+
2179+
def __getitem__(self, item):
2180+
if item in self._names:
2181+
return _SubsetOfPartitionedTopology(self._partition, {item})
2182+
elif item in self._partition.names:
2183+
return EmptyTopology(self.ndims)
2184+
else:
2185+
topo = self._partition.getitem(item)
2186+
assert not isinstance(topo, _SubsetOfPartitionedTopology) # this is covered by the above two conditionals
2187+
if isinstance(topo, EmptyTopology):
2188+
return topo
2189+
elif isinstance(topo, PartitionedTopology):
2190+
return _SubsetOfPartitionedTopology(topo, self._names)
2191+
else:
2192+
raise NotImplementedError
2193+
2194+
@property
2195+
def boundary(self):
2196+
# The boundary of this subset consists of the boundary of the base that
2197+
# touches this subset and the interfaces between all parts in this subset
2198+
# and all parts not in this subset. All interfaces are grouped and named by
2199+
# the parts not in this subset: given a partition A, B of Ω, then
2200+
# `Ω['A'].boundary['B']` is the same as `Ω.interfaces['A:B']` or
2201+
# `~Ω.interfaces['B:A']`, whichever exists.
2202+
topos = []
2203+
names = []
2204+
for b in self._partition.names: # parts not in this subset
2205+
if b in self._names:
2206+
continue
2207+
btopos = []
2208+
for a in self._names: # parts in this subset
2209+
if self._partition.names.index(a) <= self._partition.names.index(b):
2210+
btopos.append(self._partition.interfaces.getitem('{}:{}'.format(a, b)))
2211+
else:
2212+
btopos.append(~self._partition.interfaces.getitem('{}:{}'.format(b, a)))
2213+
topos.append(DisjointUnionTopology(btopos))
2214+
names.append(b)
2215+
for name in self._names:
2216+
topos.append(self._partition.boundary.getitem(name))
2217+
groups = {}
2218+
return DisjointUnionTopology(topos, names)
2219+
2220+
@property
2221+
def interfaces(self):
2222+
topos = []
2223+
names = []
2224+
for i, a in enumerate(self._names):
2225+
for b in self._names[i:]:
2226+
topos.append(self._partition.interfaces.getitem('{}:{}'.format(a, b)))
2227+
names.append('{}:{}'.format(a, b))
2228+
return DisjointUnionTopology(topos, names)
2229+
2230+
def __or__(self, other):
2231+
if isinstance(other, _SubsetOfPartitionedTopology) and other._partition == self._partition:
2232+
return _SubsetOfPartitionedTopology(self._partition, frozenset(self._names) | frozenset(other._names))
2233+
else:
2234+
return super().__or__(other)
2235+
2236+
def __rsub__(self, other):
2237+
if self._partition == other or self._partition.basetopo == other:
2238+
remainder = frozenset(self._partition.names) - frozenset(self._names)
2239+
if remainder:
2240+
return _SubsetOfPartitionedTopology(self._partition, remainder)
2241+
else:
2242+
return EmptyTopology(self.ndims)
2243+
else:
2244+
return super().__rsub__(other)
2245+
20272246
class PatchBoundary(types.Singleton):
20282247

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

tests/test_finitecell.py

Lines changed: 18 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:
@@ -371,8 +372,13 @@ class partialtrim(TestCase):
371372

372373
def setUp(self):
373374
self.topo, geom = mesh.rectilinear([2,2])
374-
self.topoA = self.topo.trim(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1)
375-
self.topoB = self.topo - self.topoA
375+
if self.method == 'trim':
376+
self.topoA = self.topo.trim(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1)
377+
self.topoB = self.topo - self.topoA
378+
elif self.method == 'partition':
379+
self.partitioned = self.topo.partition(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1, posname='A', negname='B')
380+
self.topoA = self.partitioned['A']
381+
self.topoB = self.partitioned['B']
376382

377383
def test_topos(self):
378384
self.assertEqual(len(self.topoA), 4)
@@ -381,29 +387,33 @@ def test_topos(self):
381387
def test_boundaries(self):
382388
self.assertEqual(len(self.topoA.boundary), 11)
383389
self.assertEqual(len(self.topoB.boundary), 8)
384-
self.assertEqual(len(self.topoA.boundary['trimmed']), 5)
385-
self.assertEqual(len(self.topoB.boundary['trimmed']), 5)
390+
self.assertEqual(len(self.topoA.boundary['B' if self.method == 'partition' else 'trimmed']), 5)
391+
self.assertEqual(len(self.topoB.boundary['A' if self.method == 'partition' else 'trimmed']), 5)
386392

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

391397
def test_transforms(self):
392-
self.assertEqual(set(self.topoA.boundary['trimmed'].transforms), set(self.topoB.boundary['trimmed'].opposites))
393-
self.assertEqual(set(self.topoB.boundary['trimmed'].transforms), set(self.topoA.boundary['trimmed'].opposites))
398+
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))
399+
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))
394400

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

410+
@parametrize.enable_if(lambda method, **kwargs: method != 'partition')
404411
def test_baseboundaries(self):
405412
# the base implementation should create the correct boundary topology but
406413
# without interface opposites and without the trimmed group
407414
for topo in self.topoA, self.topoB:
408415
alttopo = topology.ConnectedTopology(topo.references, topo.transforms, topo.opposites, topo.connectivity)
409416
self.assertEqual(dict(zip(alttopo.boundary.transforms, alttopo.boundary.references)), dict(zip(topo.boundary.transforms, topo.boundary.references)))
417+
418+
partialtrim(method='trim')
419+
partialtrim(method='partition')

0 commit comments

Comments
 (0)