From fe1a100f3434252395f5ec038665ea0ac59061d3 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 28 Apr 2020 14:47:28 +0200 Subject: [PATCH 001/119] fix missing kwargs in Immutable.{edit,__reduce__} Fixes #497. --- nutils/types.py | 12 ++++++------ tests/test_types.py | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/nutils/types.py b/nutils/types.py index d7e86840e..4374ec5f8 100644 --- a/nutils/types.py +++ b/nutils/types.py @@ -560,9 +560,9 @@ def __call__(*args, **kwargs): for preprocess in cls._pre_init: args, kwargs = preprocess(*args, **kwargs) args = args[1:] - return cls._new(*args, **kwargs) + return cls._new(args, kwargs) - def _new(cls, *args, **kwargs): + def _new(cls, args, kwargs): self = cls.__new__(cls) self._args = args self._kwargs = kwargs @@ -626,7 +626,7 @@ class Immutable(metaclass=ImmutableMeta): __cache__ = '__nutils_hash__', def __reduce__(self): - return self.__class__._new, self._args + return self.__class__._new, (self._args, self._kwargs) def __hash__(self): return self._hash @@ -654,7 +654,7 @@ def __str__(self): return '{}({})'.format(self.__class__.__name__, ','.join(str(arg) for arg in self._args)) def edit(self, op): - return self.__class__(*[op(arg) for arg in self._args]) + return self.__class__(*[op(arg) for arg in self._args], **{name: op(value) for name, value in self._kwargs.items()}) class SingletonMeta(ImmutableMeta): @@ -663,12 +663,12 @@ def __new__(mcls, name, bases, namespace, **kwargs): cls._cache = weakref.WeakValueDictionary() return cls - def _new(cls, *args, **kwargs): + def _new(cls, args, kwargs): key = args + tuple((key, kwargs[key]) for key in sorted(kwargs)) try: self = cls._cache[key] except KeyError: - cls._cache[key] = self = super()._new(*args, **kwargs) + cls._cache[key] = self = super()._new(args, kwargs) return self class Singleton(Immutable, metaclass=SingletonMeta): diff --git a/tests/test_types.py b/tests/test_types.py index aed90a38b..8ba765fd8 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -904,11 +904,11 @@ def test_wo_getitem(self): nutils.types.c_array() class T_Immutable(nutils.types.Immutable): - def __init__(self, x, y): + def __init__(self, x, y, *, z): pass class T_Singleton(nutils.types.Singleton): - def __init__(self, x, y): + def __init__(self, x, y, *, z): pass @parametrize @@ -916,7 +916,7 @@ class ImmutableFamily(TestCase): def test_pickle(self): T = {nutils.types.Immutable: T_Immutable, nutils.types.Singleton: T_Singleton}[self.cls] - a = T(1, 2) + a = T(1, 2, z=3) b = pickle.loads(pickle.dumps(a)) self.assertEqual(a, b) @@ -995,6 +995,16 @@ def __init__(self, x, y): self.assertIsNot(a, d) self.assertNotEqual(a, d) + def test_edit(self): + class T(self.cls): + def __init__(self, x, *, y): + self.x = x + self.y = y + + a = T(1, y=2).edit(lambda v: v+1) + self.assertEqual(a.x, 2) + self.assertEqual(a.y, 3) + ImmutableFamily(cls=nutils.types.Immutable) ImmutableFamily(cls=nutils.types.Singleton) From 6e0a644500689b47105aba1859c639498ebdb811 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 28 Apr 2020 15:00:08 +0200 Subject: [PATCH 002/119] fix parsing power expr. with const scalar base Fixes #484. --- nutils/expression.py | 16 ++++++++++++++++ tests/test_expression.py | 3 +++ 2 files changed, 19 insertions(+) diff --git a/nutils/expression.py b/nutils/expression.py index 5d4acd309..97806cf44 100644 --- a/nutils/expression.py +++ b/nutils/expression.py @@ -829,6 +829,22 @@ def parse_const(self): if self._next.type == 'indices': self._consume() raise _IntermediateError('Taking a derivative of a constant is not allowed.') + if self._next.type == '^': + token = self._consume() + if self._next.type == '(': + self._consume() + exponent = self.parse_subexpression() + self._consume_assert_equal(')') + else: + if self._next.type == '-': + self._consume() + negate = True + else: + negate = False + exponent = self.parse_const_scalar() + if negate: + exponent = -exponent + value = value**exponent return value @highlight diff --git a/tests/test_expression.py b/tests/test_expression.py index 11b4643e7..b706cb591 100644 --- a/tests/test_expression.py +++ b/tests/test_expression.py @@ -332,6 +332,9 @@ def test_add_sub_unmatched_indices(self): def test_array_pow_pos(self): self.assert_ast('a2_i^2', 'i', ('pow', v._a2, _(2))) def test_array_pow_neg(self): self.assert_ast('a2_i^-2', 'i', ('pow', v._a2, ('neg', _(2)))) def test_array_pow_scalar_expr(self): self.assert_ast('a2_i^(1 / 3)', 'i', ('pow', v._a2, ('truediv', _(1), _(3)))) + def test_scalar_pow_pos(self): self.assert_ast('2^3', '', ('pow', _(2), _(3))) + def test_scalar_pow_neg(self): self.assert_ast('2^-3', '', ('pow', _(2), ('neg', _(3)))) + def test_scalar_pow_scalar_expr(self): self.assert_ast('2^(1 / 3)', '', ('pow', _(2), ('truediv', _(1), _(3)))) def test_array_pow_nonconst(self): self.assert_syntax_error( From f1a5923acd7c3cd5a8b1e3c2896e81c0133395ed Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 28 Apr 2020 15:31:59 +0200 Subject: [PATCH 003/119] support scientific notation in expressions --- nutils/expression.py | 6 +++--- tests/test_expression.py | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/nutils/expression.py b/nutils/expression.py index 97806cf44..4bd391e54 100644 --- a/nutils/expression.py +++ b/nutils/expression.py @@ -791,7 +791,7 @@ def parse_var(self): @highlight def parse_const_scalar(self): - 'parse a constant scalar, e.g. "1", "1.0", "0.1"' + 'parse a constant scalar, e.g. "1", "1.0", "0.1", "1e3", ".1e0", "1.2e03"' token = self._consume() if token.type == 'int': @@ -1003,9 +1003,9 @@ def tokenize(self): tokens.append(_Token('variable', m_variable, pos)) pos += len(m_variable) continue - m = re.match(r'[0-9]*[.][0-9]*', self.expression[pos:]) + m = re.match(r'[0-9]+e-?[0-9]+|([0-9]+[.][0-9]*|[.][0-9]+)(e-?[0-9]+)?', self.expression[pos:]) if m: - if m.group(0).startswith('0') and not m.group(0).startswith('0.'): + if m.group(0).startswith('0') and not (m.group(0).startswith('0.') or m.group(0).startswith('0e')): raise _IntermediateError('Leading zeros are forbidden.', at=pos, count=len(m.group(0))) tokens.append(_Token('float', m.group(0), pos)) pos += m.end() diff --git a/tests/test_expression.py b/tests/test_expression.py index b706cb591..938f43145 100644 --- a/tests/test_expression.py +++ b/tests/test_expression.py @@ -215,9 +215,17 @@ def test_missing_whitespace_sub_left(self): "a2_i- a2_i", "i", " ^") - def test_int_float_syntax(self): - self.assert_ast('1 + 1.1 + 1. + 0.12', '', - ('add', ('add', ('add', _(1), _(1.1)), _(1.)), _(0.12))) + def test_int(self): + self.assert_ast('1', '', _(1)) + + def test_float(self): + for f in '10', '1', '1.', '.1', '1.2', '0.01', '10.0': + self.assert_ast(f, '', _(float(f))) + + def test_scientific(self): + for base in '0', '1', '10', '1.', '.1', '.01', '1.2': + for exp in '-1', '0', '1', '10': + self.assert_ast(base+'e'+exp, '', _(float(base+'e'+exp))) def test_jump_mean(self): self.assert_ast('[a2_i,i] + {a2_j,j}', '', @@ -331,6 +339,7 @@ def test_add_sub_unmatched_indices(self): def test_array_pow_pos(self): self.assert_ast('a2_i^2', 'i', ('pow', v._a2, _(2))) def test_array_pow_neg(self): self.assert_ast('a2_i^-2', 'i', ('pow', v._a2, ('neg', _(2)))) + def test_array_pow_scientific(self): self.assert_ast('a2_i^1e1', 'i', ('pow', v._a2, _(1e1))) def test_array_pow_scalar_expr(self): self.assert_ast('a2_i^(1 / 3)', 'i', ('pow', v._a2, ('truediv', _(1), _(3)))) def test_scalar_pow_pos(self): self.assert_ast('2^3', '', ('pow', _(2), _(3))) def test_scalar_pow_neg(self): self.assert_ast('2^-3', '', ('pow', _(2), ('neg', _(3)))) From ef29e4a281c44b8f141260b551c197b045ba6bf9 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 28 Apr 2020 15:33:32 +0200 Subject: [PATCH 004/119] doc fixes --- docs/notes/binary_operations.rst | 2 +- docs/tutorial.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/notes/binary_operations.rst b/docs/notes/binary_operations.rst index 4d498a51b..d591b607a 100644 --- a/docs/notes/binary_operations.rst +++ b/docs/notes/binary_operations.rst @@ -25,4 +25,4 @@ Binary Operations on Arrays 1. In the above table the summation axes are numbered backward. For example, ``sum(-1)`` is used to sum over the last axis of an array. Although forward numbering is possible in many situations, backward numbering is generally preferred in Nutils code. 2. When a summation over multiple axes is performed (#6), these axes are to be listed. In the case of single-axis summations listing is optional (for example ``sum(-1)`` is equivalent to ``sum([-1])``). The shorter notation ``sum(-1)`` is preferred. -3. When the numer of dimensions of the two arguments of a binary operation mismatch, singleton axes are automatically prepended to the "shorter" argument. This property can be used to shorten notation. For example, #3 can be written as ``(A*b).sum(-1)``. To avoid ambiguities, in general, such abbreviations are discouraged. +3. When the number of dimensions of the two arguments of a binary operation mismatch, singleton axes are automatically prepended to the "shorter" argument. This property can be used to shorten notation. For example, #3 can be written as ``(A*b).sum(-1)``. To avoid ambiguities, in general, such abbreviations are discouraged. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 0418f6a6f..409ff8df4 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -47,8 +47,8 @@ Note that discretization inevitably implies approximation, i.e. :math:`u ≠ space of piecewise linears, which contains the exact solution. We therefore expect our Finite Element solution to be exact. -Wetting your appetite ---------------------- +Whetting your appetite +---------------------- The computation can be set up in about 20 lines of Nutils code, including visualization. The entire script is presented below, in copy-pasteable form From a6164468ef69c17481db2c3a07286dcb5f516643 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 12 Mar 2020 14:20:23 +0100 Subject: [PATCH 005/119] remove Topology.elem_project --- nutils/topology.py | 50 ------------------------------------------ tests/test_topology.py | 32 --------------------------- 2 files changed, 82 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 319ea6b46..054a0c1f4 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -394,56 +394,6 @@ def withgroups(self, vgroups={}, bgroups={}, igroups={}, pgroups={}): withinterfaces = lambda self, **kwargs: self.withgroups(igroups=kwargs) withpoints = lambda self, **kwargs: self.withgroups(pgroups=kwargs) - @util.single_or_multiple - def elem_project(self, funcs, degree, ischeme=None, check_exact=False, *, arguments=None): - - if arguments is None: - arguments = {} - - if ischeme is None: - ischeme = 'gauss{}'.format(degree*2) - - blocks = function.Tuple([function.Tuple([function.Tuple((function.Tuple(ind), f.simplified)) - for ind, f in function.blocks(func.prepare_eval())]) - for func in funcs]) - - bases = {} - extractions = [[] for ifunc in range(len(funcs))] - - with log.iter.percentage('projecting', self.references, self.transforms, self.opposites) as items: - for ref, trans, opp in items: - - try: - points, projector, basis = bases[ref] - except KeyError: - points, weights = ref.getischeme(ischeme) - coeffs = ref.get_poly_coeffs('bernstein', degree=degree) - basis = numeric.poly_eval(coeffs[_], points) - npoints, nfuncs = basis.shape - A = numeric.dot(weights, basis[:,:,_] * basis[:,_,:]) - projector = numpy.linalg.solve(A, basis.T * weights) - bases[ref] = points, projector, basis - - for ifunc, ind_val in enumerate(blocks.eval(_transforms=(trans, opp), _points=points, **arguments)): - - if len(ind_val) == 1: - (allind, sumval), = ind_val - else: - allind, where = zip(*[numpy.unique([i for ind, val in ind_val for i in ind[iax]], return_inverse=True) for iax in range(funcs[ifunc].ndim)]) - sumval = numpy.zeros([len(n) for n in (points,) + allind]) - for ind, val in ind_val: - I, where = zip(*[(w[:len(n)], w[len(n):]) for w, n in zip(where, ind)]) - numpy.add.at(sumval, numpy.ix_(range(len(points)), *I), val) - assert not any(where) - - ex = numeric.dot(projector, sumval) - if check_exact: - numpy.testing.assert_almost_equal(sumval, numeric.dot(basis, ex), decimal=15) - - extractions[ifunc].append((allind, ex)) - - return extractions - @log.withcontext def volume(self, geometry, ischeme='gauss', degree=1, *, arguments=None): return self.integrate(function.J(geometry, self.ndims), ischeme=ischeme, degree=degree, arguments=arguments) diff --git a/tests/test_topology.py b/tests/test_topology.py index 655bd77f8..53fff405d 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -66,38 +66,6 @@ def assertInterfaces(self, domain, geom, periodic, interfaces=None, elemindicato numpy.testing.assert_array_almost_equal(lhs, rhs) -@parametrize -class elem_project(TestCase): - - def test_extraction(self): - topo, geom = mesh.rectilinear([numpy.linspace(-1,1,4)]*self.ndims) - - splinebasis = topo.basis('spline', degree=self.degree) - bezierbasis = topo.basis('spline', degree=self.degree, knotmultiplicities=[numpy.array([self.degree+1]+[self.degree]*(n-1)+[self.degree+1]) for n in topo.shape]) - - sample = topo.sample('uniform', 2) - splinevals, beziervals = sample.eval([splinebasis,bezierbasis]) - sextraction = topo.elem_project(splinebasis, degree=self.degree, check_exact=True) - bextraction = topo.elem_project(bezierbasis, degree=self.degree, check_exact=True) - self.assertEqual(len(sample.index), len(sextraction)) - self.assertEqual(len(sample.index), len(bextraction)) - for index, (sien,sext), (bien,bext) in zip(sample.index,sextraction,bextraction): - svals, bvals = splinevals[index], beziervals[index] - sien, bien = sien[0][0], bien[0][0] - self.assertEqual(len(sien), len(bien)) - self.assertEqual(len(sien), sext.shape[0]) - self.assertEqual(len(sien), sext.shape[1]) - self.assertEqual(len(sien), bext.shape[0]) - self.assertEqual(len(sien), bext.shape[1]) - self.assertEqual(len(sien), (self.degree+1)**self.ndims) - numpy.testing.assert_array_almost_equal(bext, numpy.eye((self.degree+1)**self.ndims)) - numpy.testing.assert_array_almost_equal(svals[:,sien], bvals[:,bien].dot(sext)) - -for ndims in range(1, 4): - for degree in [2] if ndims == 3 else range(1, 4): - elem_project(ndims=ndims, degree=degree) - - @parametrize class structure(TestCase, TopologyAssertions): From edce9dc0f4922234f13e426605660b42da45d989 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 3 Feb 2020 23:59:06 +0100 Subject: [PATCH 006/119] disable multipatch unittests The multipatch topology is rather difficult to maintain in its current form while transitioning to tensorial topologies. We simply disable the tests involving multipatch topologies for now. --- nutils/mesh.py | 80 +++++++++++++++++++++--------------------- tests/test_topology.py | 12 +++++++ 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/nutils/mesh.py b/nutils/mesh.py index 36910e91b..5bc753843 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -159,60 +159,60 @@ def multipatch(patches, nelems, patchverts=None, name='multipatch'): An L-shaped domain can be generated by:: - >>> # connectivity: 2──5 - >>> # │ | - >>> # 1──4─────7 y - >>> # │ │ │ │ - >>> # 0──3─────6 └──x + # connectivity: 2──5 + # │ | + # 1──4─────7 y + # │ │ │ │ + # 0──3─────6 └──x - >>> domain, geom = multipatch( - ... patches=[[0,1,3,4], [1,2,4,5], [3,4,6,7]], - ... patchverts=[[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [3,0], [3,1]], - ... nelems={None: 4, (3,6): 8, (4,7): 8}) + domain, geom = multipatch( + patches=[[0,1,3,4], [1,2,4,5], [3,4,6,7]], + patchverts=[[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [3,0], [3,1]], + nelems={None: 4, (3,6): 8, (4,7): 8}) The number of elements is chosen such that all elements in the domain have the same size. A topology and geometry describing the surface of a sphere can be generated - by creating a multipatch cube surface and inflating the cube to a sphere: - - >>> # connectivity: 3────7 - >>> # ╱│ ╱│ - >>> # 2────6 │ y - >>> # │ │ │ │ │ - >>> # │ 1──│─5 │ z - >>> # │╱ │╱ │╱ - >>> # 0────4 *────x - - >>> import itertools - >>> from nutils import function - >>> topo, cube = multipatch( - ... patches=[ - ... [0,1,2,3], # left, normal: x - ... [4,5,6,7], # right, normal: x - ... [0,1,4,5], # bottom, normal: -y - ... [2,3,6,7], # top, normal: -y - ... [0,2,4,6], # front, normal: z - ... [1,3,5,7], # back, normal: z - ... ], - ... patchverts=tuple(itertools.product(*([[-1,1]]*3))), - ... nelems=1) - >>> sphere = function.normalized(cube) + by creating a multipatch cube surface and inflating the cube to a sphere:: + + # connectivity: 3────7 + # ╱│ ╱│ + # 2────6 │ y + # │ │ │ │ │ + # │ 1──│─5 │ z + # │╱ │╱ │╱ + # 0────4 *────x + + import itertools + from nutils import function + topo, cube = multipatch( + patches=[ + [0,1,2,3], # left, normal: x + [4,5,6,7], # right, normal: x + [0,1,4,5], # bottom, normal: -y + [2,3,6,7], # top, normal: -y + [0,2,4,6], # front, normal: z + [1,3,5,7], # back, normal: z + ], + patchverts=tuple(itertools.product(*([[-1,1]]*3))), + nelems=1) + sphere = function.normalized(cube) The normals of the patches are determined by the order of the vertex numbers. An outward normal for the cube is obtained by flipping the left, top and - front faces: + front faces:: - >>> cubenormal = cube.normal(exterior=True) * topo.basis('patch').dot([-1,1,1,-1,-1,1]) + cubenormal = cube.normal(exterior=True) * topo.basis('patch').dot([-1,1,1,-1,-1,1]) - At the centroids of the faces the outward normal should equal the cube geometry: + At the centroids of the faces the outward normal should equal the cube geometry:: - >>> numpy.testing.assert_allclose(*topo.sample('gauss', 1).eval([cubenormal, cube])) + numpy.testing.assert_allclose(*topo.sample('gauss', 1).eval([cubenormal, cube])) - Similarly, the outward normal of the sphere is obtained by: + Similarly, the outward normal of the sphere is obtained by:: - >>> spherenormal = sphere.normal(exterior=True) * topo.basis('patch').dot([-1,1,1,-1,-1,1]) - >>> numpy.testing.assert_allclose(*topo.sample('gauss', 1).eval([spherenormal, cube])) + spherenormal = sphere.normal(exterior=True) * topo.basis('patch').dot([-1,1,1,-1,-1,1]) + numpy.testing.assert_allclose(*topo.sample('gauss', 1).eval([spherenormal, cube])) Args ---- diff --git a/tests/test_topology.py b/tests/test_topology.py index 53fff405d..e6ea084a6 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -427,6 +427,7 @@ class multipatch_hyperrect(TestCase, TopologyAssertions): def setUp(self): super().setUp() + self.skipTest('disabled during transition to tensorial topologies') npatches = numpy.array(self.npatches) indices = numpy.arange((npatches+1).prod()).reshape(npatches+1) @@ -471,6 +472,7 @@ def setUp(self): # 0---3------6 super().setUp() + self.skipTest('disabled during transition to tensorial topologies') self.domain, self.geom = mesh.multipatch( patches=[[0,1,3,4], [1,2,4,5], [3,4,6,7]], patchverts=[[0,0], [0,1], [0,2], [1,0], [1,1], [1,2], [3,0], [3,1]], @@ -527,6 +529,16 @@ def test_connectivity(self): class multipatch_errors(TestCase): + def setUp(self): + # 2---5 + # | | + # 1---4------7 + # | | | + # 0---3------6 + + super().setUp() + self.skipTest('disabled during transition to tensorial topologies') + def test_reverse(self): with self.assertRaises(NotImplementedError): mesh.multipatch( From 81449b59b5a3f7d24b5c22ff1d1e06dc3a86407e Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 13 Jun 2019 14:17:38 +0200 Subject: [PATCH 007/119] disable unittests involving bifurcate Bifurcate is being replaced by the new tensorial topologies approach. We disable unittests involving bifurcate pending the replacement. --- tests/test_basis.py | 1 + tests/test_mesh.py | 1 + tests/test_topology.py | 1 + 3 files changed, 3 insertions(+) diff --git a/tests/test_basis.py b/tests/test_basis.py index d49f707ff..1cdcfc458 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -96,6 +96,7 @@ def setUp(self): if not self.product: self.domain, self.geom = mesh.rectilinear([2,3]) else: + self.skipTest('in between bifurcate and tensorial') domain1, geom1 = mesh.rectilinear([2]) domain2, geom2 = mesh.rectilinear([3]) self.domain = domain1 * domain2 diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 20a9c7583..ac10e585c 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -103,6 +103,7 @@ def test_length(self): gmshmanifold(version=version, degree=degree) @parametrize +@unittest.skip('in between bifurcate and tensorial') class rectilinear(TestCase): def setUp(self): diff --git a/tests/test_topology.py b/tests/test_topology.py index e6ea084a6..31a75d102 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -178,6 +178,7 @@ def test_bnd(self): self.assertAlmostEqual(iface.integrate(function.J(geom), degree=1), 3) @parametrize +@unittest.skip('in between bifurcate and tensorial') class revolved(TestCase): def setUp(self): From 75b8c744363d4ac5ee462d3aa06f5d5c16752234 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 13 Feb 2020 14:32:44 +0100 Subject: [PATCH 008/119] remove {transform,function}.Bifurcate and friends --- nutils/function.py | 28 ---------------------------- nutils/transform.py | 45 ++------------------------------------------- 2 files changed, 2 insertions(+), 71 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 86c1fa8c9..72e51ccd4 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -356,23 +356,6 @@ def evalf(self, trans): assert trans[0].fromdims == self.todims return trans[1:] -class SelectBifurcation(TransformChain): - - __slots__ = 'trans', 'first' - - @types.apply_annotations - def __init__(self, trans:strictevaluable, first:bool, todims:types.strictint=None): - self.trans = trans - self.first = first - super().__init__(args=[trans], todims=todims) - - def evalf(self, trans): - assert isinstance(trans, tuple) - bf = trans[0] - assert isinstance(bf, transform.Bifurcate) - selected = bf.trans1 if self.first else bf.trans2 - return selected + trans[1:] - class TransformChainFromTuple(TransformChain): __slots__ = 'index', @@ -4031,17 +4014,6 @@ def rootcoords(ndims): def opposite(arg): return Opposite(arg) -@replace -def _bifurcate(arg, side): - if isinstance(arg, SelectChain): - return SelectBifurcation(arg, side) - -bifurcate1 = functools.partial(_bifurcate, side=True) -bifurcate2 = functools.partial(_bifurcate, side=False) - -def bifurcate(arg1, arg2): - return bifurcate1(arg1), bifurcate2(arg2) - def curvature(geom, ndims=-1): return geom.normal().div(geom, ndims=ndims) diff --git a/nutils/transform.py b/nutils/transform.py index b798a1900..30cc0ce4a 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -33,19 +33,9 @@ def apply(chain, points): points = trans.apply(points) return points -def n_ascending(chain): - # number of ascending transform items counting from root (0). this is a - # temporary hack required to deal with Bifurcate/Slice; as soon as we have - # proper tensorial topologies we can switch back to strictly ascending - # transformation chains. - for n, trans in enumerate(chain): - if trans.todims is not None and trans.todims < trans.fromdims: - return n - return len(chain) - def canonical(chain): # keep at lowest ndims possible; this is the required form for bisection - n = n_ascending(chain) + n = len(chain) if n < 2: return tuple(chain) items = list(chain) @@ -61,7 +51,7 @@ def canonical(chain): def uppermost(chain): # bring to highest ndims possible - n = n_ascending(chain) + n = len(chain) if n < 2: return tuple(chain) items = list(chain) @@ -134,23 +124,6 @@ def swapdown(self, other): stricttransformitem = types.strict[TransformItem] stricttransform = types.tuple[stricttransformitem] -class Bifurcate(TransformItem): - - __slots__ = 'trans1', 'trans2' - - @types.apply_annotations - def __init__(self, trans1:canonical, trans2:canonical): - fromdims = trans1[-1].fromdims + trans2[-1].fromdims - self.trans1 = trans1 + (Slice(0, trans1[-1].fromdims, fromdims),) - self.trans2 = trans2 + (Slice(trans1[-1].fromdims, fromdims, fromdims),) - super().__init__(todims=trans1[0].todims if trans1[0].todims == trans2[0].todims else None, fromdims=fromdims) - - def __str__(self): - return '{}<>{}'.format(self.trans1, self.trans2) - - def apply(self, points): - return apply(self.trans1, points), apply(self.trans2, points) - class Matrix(TransformItem): '''Affine transformation :math:`x ↦ A x + b`, with :math:`A` an :math:`n×m` matrix, :math:`n≥m` @@ -436,20 +409,6 @@ def __init__(self, ndims, ichild): raise NotImplementedError('SimplexChild(ndims={}, ichild={})'.format(ndims, ichild)) super().__init__(linear, offset) -class Slice(Matrix): - - __slots__ = 's', - - @types.apply_annotations - def __init__(self, i1:int, i2:int, fromdims:int): - todims = i2-i1 - assert 0 <= todims <= fromdims - self.s = slice(i1,i2) - super().__init__(numpy.eye(fromdims)[self.s], numpy.zeros(todims)) - - def apply(self, points): - return types.frozenarray(points[:,self.s]) - class ScaledUpdim(Updim): __slots__ = 'trans1', 'trans2' From d3ee438d6fc25fb2e69db5a8a9453aaf683c028a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 8 Jan 2020 20:44:30 +0100 Subject: [PATCH 009/119] remove first of two function.{normal,grad} --- nutils/function.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 72e51ccd4..fa40a27c5 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -3842,9 +3842,6 @@ def ones_like(arr): def reciprocal(arg): return power(arg, -1) -def grad(arg, coords, ndims=0): - return asarray(arg).grad(coords, ndims) - def symgrad(arg, coords, ndims=0): return asarray(arg).symgrad(coords, ndims) @@ -4177,9 +4174,6 @@ def dotnorm(arg, coords): return sum(arg * coords.normal(), -1) -def normal(geom): - return geom.normal() - def kronecker(arg, axis, length, pos): arg = asarray(arg) return Kronecker(arg, axis=numeric.normdim(arg.ndim+1, axis), length=length, pos=pos) From 7680c391b1363fc41e16f7f5381b2d2fced0e41a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 7 Jan 2020 13:00:54 +0100 Subject: [PATCH 010/119] restructure function.Basis tests The tests for `function.Basis` make heavily use of parametrize. This has two drawbacks: an exception raised while creating an argument, e.g. a transforms sequence, makes the entire module not importable, and we can't create helper variables for defining arguments unless we place them in the global scope. This patch solves both problems by replacing parametrize with dedicated subclasses. --- tests/test_function.py | 113 ++++++++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 48 deletions(-) diff --git a/tests/test_function.py b/tests/test_function.py index eea3ef0a4..3dd7ea20d 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -914,8 +914,7 @@ def test_zeroderivative(self): jacobian(delayed=True) jacobian(delayed=False) -@parametrize -class basis(TestCase): +class CommonBasis: def setUp(self): super().setUp() @@ -1088,49 +1087,67 @@ def test_f_coefficients(self): b, = self.basis.f_coefficients(ielem).eval() self.assertAllEqual(a, b) -basis( - 'PlainBasis', - basis=function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0)), - checkcoeffs=[[1],[2,3],[4,5],[6]], - checkdofs=[[0],[2,3],[1,3],[2]], - checkndofs=4) -basis( - 'DiscontBasis', - basis=function.DiscontBasis([[1],[2,3],[4,5],[6]], transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0)), - checkcoeffs=[[1],[2,3],[4,5],[6]], - checkdofs=[[0],[1,2],[3,4],[5]], - checkndofs=6) -basis( - 'MaskedBasis', - basis=function.MaskedBasis(function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0)), [0,2]), - checkcoeffs=[[1],[2],[],[6]], - checkdofs=[[0],[1],[],[1]], - checkndofs=2) -basis( - 'PrunedBasis', - basis=function.PrunedBasis(function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0)), [0,2]), - checkcoeffs=[[1],[4,5]], - checkdofs=[[0],[1,2]], - checkndofs=3) - -structtrans4 = transformseq.StructuredTransforms(transform.Identifier(1, 'test'), [transformseq.DimAxis(0,4,False)], 0) -structtrans4p = transformseq.StructuredTransforms(transform.Identifier(1, 'test'), [transformseq.DimAxis(0,4,True)], 0) -structtrans22 = transformseq.StructuredTransforms(transform.Identifier(2, 'test'), [transformseq.DimAxis(0,2,False),transformseq.DimAxis(0,2,False)], 0) -basis( - 'StructuredBasis1D', - basis=function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [5], structtrans4, [4]), - checkcoeffs=[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]], - checkdofs=[[0,1],[1,2],[2,3],[3,4]], - checkndofs=5) -basis( - 'StructuredBasis1DPeriodic', - basis=function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [4], structtrans4p, [4]), - checkcoeffs=[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]], - checkdofs=[[0,1],[1,2],[2,3],[3,0]], - checkndofs=4) -basis( - 'StructuredBasis2D', - basis=function.StructuredBasis([[[[1],[2]],[[3],[4]]],[[[5],[6]],[[7],[8]]]], [[0,1],[0,1]], [[2,3],[2,3]], [3,3], structtrans22, [2,2]), - checkcoeffs=[[[[5]],[[6]],[[10]],[[12]]],[[[7]],[[8]],[[14]],[[16]]],[[[15]],[[18]],[[20]],[[24]]],[[[21]],[[24]],[[28]],[[32]]]], - checkdofs=[[0,1,3,4],[1,2,4,5],[3,4,6,7],[4,5,7,8]], - checkndofs=9) +class PlainBasis(CommonBasis, TestCase): + def setUp(self): + transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) + self.checkcoeffs = [[1],[2,3],[4,5],[6]] + self.checkdofs = [[0],[2,3],[1,3],[2]] + self.basis = function.PlainBasis(self.checkcoeffs, self.checkdofs, 4, transforms) + self.checkndofs = 4 + super().setUp() + +class DiscontBasis(CommonBasis, TestCase): + def setUp(self): + transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) + self.checkcoeffs = [[1],[2,3],[4,5],[6]] + self.basis = function.DiscontBasis(self.checkcoeffs, transforms) + self.checkdofs = [[0],[1,2],[3,4],[5]] + self.checkndofs = 6 + super().setUp() + +class MaskedBasis(CommonBasis, TestCase): + def setUp(self): + transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) + parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transforms) + self.basis = function.MaskedBasis(parent, [0,2]) + self.checkcoeffs = [[1],[2],[],[6]] + self.checkdofs = [[0],[1],[],[1]] + self.checkndofs = 2 + super().setUp() + +class PrunedBasis(CommonBasis, TestCase): + def setUp(self): + parent_transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) + parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, parent_transforms) + self.basis = function.PrunedBasis(parent, [0,2]) + self.checkcoeffs = [[1],[4,5]] + self.checkdofs = [[0],[1,2]] + self.checkndofs = 3 + super().setUp() + +class StructuredBasis1D(CommonBasis, TestCase): + def setUp(self): + transforms = transformseq.StructuredTransforms(transform.Identifier(1, 'test'), [transformseq.DimAxis(0,4,False)], 0) + self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [5], transforms, [4]) + self.checkcoeffs = [[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]] + self.checkdofs = [[0,1],[1,2],[2,3],[3,4]] + self.checkndofs = 5 + super().setUp() + +class StructuredBasis1DPeriodic(CommonBasis, TestCase): + def setUp(self): + transforms = transformseq.StructuredTransforms(transform.Identifier(1, 'test'), [transformseq.DimAxis(0,4,True)], 0) + self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [4], transforms, [4]) + self.checkcoeffs = [[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]] + self.checkdofs = [[0,1],[1,2],[2,3],[3,0]] + self.checkndofs = 4 + super().setUp() + +class StructuredBasis2D(CommonBasis, TestCase): + def setUp(self): + transforms = transformseq.StructuredTransforms(transform.Identifier(2, 'test'), [transformseq.DimAxis(0,2,False),transformseq.DimAxis(0,2,False)], 0) + self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]]],[[[5],[6]],[[7],[8]]]], [[0,1],[0,1]], [[2,3],[2,3]], [3,3], transforms, [2,2]) + self.checkcoeffs = [[[[5]],[[6]],[[10]],[[12]]],[[[7]],[[8]],[[14]],[[16]]],[[[15]],[[18]],[[20]],[[24]]],[[[21]],[[24]],[[28]],[[32]]]] + self.checkdofs = [[0,1,3,4],[1,2,4,5],[3,4,6,7],[4,5,7,8]] + self.checkndofs = 9 + super().setUp() From c001db86dafd3d0b4ab875fca9e7161576e2e998 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 13 Feb 2020 12:02:48 +0100 Subject: [PATCH 011/119] use strict type checking in Transforms sequences Currently a `transform.Identity` is a subclass of `transform.Shift`. In `StructuredTransforms.index_with_tail` we test if the supplied transform chain starts with a `transform.Shift` using `isinstance`, which gives a false positive if the transform chain starts with `transform.Identity`. This patch resolves this problem by replacing `isinstance(t, T)` with `type(t) == T`. --- nutils/transformseq.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 9f879ead1..32b32238a 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -425,7 +425,7 @@ def __len__(self): def index_with_tail(self, trans): root = trans[0] - if root.fromdims == self.fromdims and isinstance(root, transform.Identifier) and isinstance(root.token, tuple) and len(root.token) == 2 and root.token[0] == self._name and 0 <= root.token[1] < self._length: + if root.fromdims == self.fromdims and type(root) == transform.Identifier and isinstance(root.token, tuple) and len(root.token) == 2 and root.token[0] == self._name and 0 <= root.token[1] < self._length: return root.token[1], trans[1:] raise ValueError @@ -581,7 +581,7 @@ def index_with_tail(self, trans): if root != self._root: raise ValueError - if not isinstance(shift, transform.Shift) or len(shift.offset) != len(self._axes) or not numpy.equal(shift.offset.astype(int), shift.offset).all(): + if not type(shift) == transform.Shift or len(shift.offset) != len(self._axes) or not numpy.equal(shift.offset.astype(int), shift.offset).all(): raise ValueError indices = numpy.array(shift.offset, dtype=int) From 6da6396d9f0916031f49bb5bb1a83edee78dc1f7 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 13 Feb 2020 13:24:15 +0100 Subject: [PATCH 012/119] don't pop the head of transform chains In the past transform chains had a special root transform item. This concept does not exist anymore and we can safely remove `function.PopHead`. `function.PopHead` was only used in `ApplyTransforms(PopHead(ndims, SelectChain())`, where `SelectChain` is implicit. Since `function.SelectChain` does not yet know the dimension of the root, we temporarily replace `PopHead` with `TransformChainWithTodims`, with the sole purpose of defining the `todims`. In a follow-up commit `function.SelectChain` will gain this knowledge and `TransformChainWithTodims` will be removed in its entirety. --- nutils/function.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index fa40a27c5..06384b69c 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -343,7 +343,7 @@ def prepare_eval(self, *, opposite=False, kwargs=...): TRANS = SelectChain() -class PopHead(TransformChain): +class TransformChainWithTodims(TransformChain): __slots__ = 'trans', @@ -353,8 +353,8 @@ def __init__(self, todims:types.strictint, trans=TRANS): super().__init__(args=[self.trans], todims=todims) def evalf(self, trans): - assert trans[0].fromdims == self.todims - return trans[1:] + assert trans[0].todims == self.todims + return trans class TransformChainFromTuple(TransformChain): @@ -4006,7 +4006,7 @@ def blocks(arg): return asarray(arg).simplified.blocks def rootcoords(ndims): - return ApplyTransforms(PopHead(ndims)) + return ApplyTransforms(TransformChainWithTodims(ndims)) def opposite(arg): return Opposite(arg) From 59958798d7e28ab59b2ec01b3d3748611276862a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 13 Feb 2020 13:54:43 +0100 Subject: [PATCH 013/119] remove root trans item from StructuredTransforms Now that we don't pop the head of a transform chain anymore, we can safely remove the root transform item from `StructuredTransforms`. --- nutils/topology.py | 8 ++--- nutils/transformseq.py | 15 ++++------ tests/test_function.py | 6 ++-- tests/test_transformseq.py | 60 +++++++++++++++++++------------------- 4 files changed, 42 insertions(+), 47 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 054a0c1f4..5146b0a75 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -841,13 +841,13 @@ def __init__(self, root:transform.stricttransformitem, axes:types.tuple[types.st self._bnames = bnames references = elementseq.asreferences([util.product(element.getsimplex(1 if axis.isdim else 0) for axis in self.axes)], len(self.shape))*len(self) - transforms = transformseq.StructuredTransforms(self.root, self.axes, self.nrefine) + transforms = transformseq.StructuredTransforms(self.axes, self.nrefine) nbounds = len(self.axes) - len(self.shape) if nbounds == 0: opposites = transforms else: axes = [transformseq.BndAxis(axis.i, axis.j, axis.ibound, not axis.side) if not axis.isdim and axis.ibound==nbounds-1 else axis for axis in self.axes] - opposites = transformseq.StructuredTransforms(self.root, axes, self.nrefine) + opposites = transformseq.StructuredTransforms(axes, self.nrefine) super().__init__(references, transforms, opposites) @@ -924,8 +924,8 @@ def interfaces(self): intaxis = lambda side: (transformseq.PIntAxis if idim in self.periodic else transformseq.IntAxis)(axis.i, axis.j, nbounds, side) axes = (*self.axes[:idim], intaxis(True), *self.axes[idim+1:]) oppaxes = (*self.axes[:idim], intaxis(False), *self.axes[idim+1:]) - itransforms = transformseq.StructuredTransforms(self.root, axes, self.nrefine) - iopposites = transformseq.StructuredTransforms(self.root, oppaxes, self.nrefine) + itransforms = transformseq.StructuredTransforms(axes, self.nrefine) + iopposites = transformseq.StructuredTransforms(oppaxes, self.nrefine) ireferences = elementseq.asreferences([util.product(element.getsimplex(1 if a.isdim else 0) for a in axes)], self.ndims-1)*len(itransforms) itopos.append(Topology(ireferences, itransforms, iopposites)) assert len(itopos) == self.ndims diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 32b32238a..6294e390c 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -520,19 +520,16 @@ class StructuredTransforms(Transforms): Parameters ---------- - root : :class:`~nutils.transform.TransformItem` - Root transform of the :class:`~nutils.topology.StructuredTopology`. axes : :class:`tuple` of :class:`Axis` objects The axes defining the :class:`~nutils.topology.StructuredTopology`. nrefine : :class:`int` Number of structured refinements. ''' - __slots__ = '_root', '_axes', '_nrefine', '_etransforms', '_ctransforms', '_cindices' + __slots__ = '_axes', '_nrefine', '_etransforms', '_ctransforms', '_cindices' @types.apply_annotations - def __init__(self, root:transform.stricttransformitem, axes:types.tuple[types.strict[Axis]], nrefine:types.strictint): - self._root = root + def __init__(self, axes:types.tuple[types.strict[Axis]], nrefine:types.strictint): self._axes = axes self._nrefine = nrefine @@ -568,18 +565,16 @@ def __getitem__(self, index): indices, r = divmod(indices, self._ctransforms.shape) ctransforms.insert(0, self._ctransforms[tuple(r)]) trans0 = transform.Shift(types.frozenarray(indices, dtype=float, copy=False)) - return (self._root, trans0, *ctransforms, *self._etransforms) + return (trans0, *ctransforms, *self._etransforms) def __len__(self): return util.product(map(len, self._axes)) def index_with_tail(self, trans): - if len(trans) < 2 + self._nrefine + len(self._etransforms): + if len(trans) < 1 + self._nrefine + len(self._etransforms): raise ValueError - root, shift, tail = trans[0], trans[1], transform.uppermost(trans[2:]) - if root != self._root: - raise ValueError + shift, tail = trans[0], transform.uppermost(trans[1:]) if not type(shift) == transform.Shift or len(shift.offset) != len(self._axes) or not numpy.equal(shift.offset.astype(int), shift.offset).all(): raise ValueError diff --git a/tests/test_function.py b/tests/test_function.py index 3dd7ea20d..b26442a53 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -1127,7 +1127,7 @@ def setUp(self): class StructuredBasis1D(CommonBasis, TestCase): def setUp(self): - transforms = transformseq.StructuredTransforms(transform.Identifier(1, 'test'), [transformseq.DimAxis(0,4,False)], 0) + transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,4,False)], 0) self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [5], transforms, [4]) self.checkcoeffs = [[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]] self.checkdofs = [[0,1],[1,2],[2,3],[3,4]] @@ -1136,7 +1136,7 @@ def setUp(self): class StructuredBasis1DPeriodic(CommonBasis, TestCase): def setUp(self): - transforms = transformseq.StructuredTransforms(transform.Identifier(1, 'test'), [transformseq.DimAxis(0,4,True)], 0) + transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,4,True)], 0) self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [4], transforms, [4]) self.checkcoeffs = [[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]] self.checkdofs = [[0,1],[1,2],[2,3],[3,0]] @@ -1145,7 +1145,7 @@ def setUp(self): class StructuredBasis2D(CommonBasis, TestCase): def setUp(self): - transforms = transformseq.StructuredTransforms(transform.Identifier(2, 'test'), [transformseq.DimAxis(0,2,False),transformseq.DimAxis(0,2,False)], 0) + transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,2,False),transformseq.DimAxis(0,2,False)], 0) self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]]],[[[5],[6]],[[7],[8]]]], [[0,1],[0,1]], [[2,3],[2,3]], [3,3], transforms, [2,2]) self.checkcoeffs = [[[[5]],[[6]],[[10]],[[12]]],[[[7]],[[8]],[[14]],[[16]]],[[[15]],[[18]],[[20]],[[24]]],[[[21]],[[24]],[[28]],[[32]]]] self.checkdofs = [[0,1,3,4],[1,2,4,5],[3,4,6,7],[4,5,7,8]] diff --git a/tests/test_transformseq.py b/tests/test_transformseq.py index 2ec8fecdc..f41d0ce3b 100644 --- a/tests/test_transformseq.py +++ b/tests/test_transformseq.py @@ -226,81 +226,81 @@ def setUp(self): class StructuredTransforms1D(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x1, [nutils.transformseq.DimAxis(0,4,False)], 0) - self.check = (x1,s0),(x1,s1),(x1,s2),(x1,s3) - self.checkmissing = (l1,s0),(x1,s4),(r1,s0),(x1,c1) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(0,4,False)], 0) + self.check = (s0,),(s1,),(s2,),(s3,) + self.checkmissing = (l1,s0),(s4,),(r1,s0),(c1,) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) self.checkfromdims = 1 class StructuredTransforms1DRefined(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x1, [nutils.transformseq.DimAxis(0,4,False)], 1) - self.check = (x1,s0,c0),(x1,s0,c1),(x1,s1,c0),(x1,s1,c1) - self.checkmissing = (l1,s0),(x1,s0),(x1,s1),(x1,s0,s1),(r1,s0) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(0,4,False)], 1) + self.check = (s0,c0),(s0,c1),(s1,c0),(s1,c1) + self.checkmissing = (l1,s0),(s0,),(s1,),(s0,s1),(r1,s0) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) self.checkfromdims = 1 class StructuredTransforms1DLeft(TestCase, Common): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x1, [nutils.transformseq.BndAxis(3,3,0,False)], 0) - self.check = (x1,s3,e1), - self.checkmissing = (x1,s0,e0),(x1,s2,e0),(x1,s4,e0) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.BndAxis(3,3,0,False)], 0) + self.check = (s3,e1), + self.checkmissing = (s0,e0),(s2,e0),(s4,e0) self.checkrefs = nutils.elementseq.asreferences([point], 0) self.checkfromdims = 0 class StructuredTransforms1DRight(TestCase, Common): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x1, [nutils.transformseq.BndAxis(3,3,0,True)], 0) - self.check = (x1,s2,e0), - self.checkmissing = (x1,s0,e0),(x1,s3,e1),(x1,s4,e0) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.BndAxis(3,3,0,True)], 0) + self.check = (s2,e0), + self.checkmissing = (s0,e0),(s3,e1),(s4,e0) self.checkrefs = nutils.elementseq.asreferences([point], 0) self.checkfromdims = 0 class StructuredTransforms1DInterfacesLeft(TestCase, Common): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x1, [nutils.transformseq.IntAxis(0,4,0,False)], 0) - self.check = (x1,s1,e1),(x1,s2,e1),(x1,s3,e1) - self.checkmissing = (x1,s0,e1),(x1,s0,e0),(x1,s1,e0),(x1,s2,e0),(x1,s3,e0) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.IntAxis(0,4,0,False)], 0) + self.check = (s1,e1),(s2,e1),(s3,e1) + self.checkmissing = (s0,e1),(s0,e0),(s1,e0),(s2,e0),(s3,e0) self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) self.checkfromdims = 0 class StructuredTransforms1DInterfacesRight(TestCase, Common): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x1, [nutils.transformseq.IntAxis(0,4,0,True)], 0) - self.check = (x1,s0,e0),(x1,s1,e0),(x1,s2,e0) - self.checkmissing = (x1,s3,e0),(x1,s0,e1),(x1,s1,e1),(x1,s2,e1),(x1,s3,e1) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.IntAxis(0,4,0,True)], 0) + self.check = (s0,e0),(s1,e0),(s2,e0) + self.checkmissing = (s3,e0),(s0,e1),(s1,e1),(s2,e1),(s3,e1) self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) self.checkfromdims = 0 class StructuredTransforms1DPeriodicInterfacesLeft(TestCase, Common): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x1, [nutils.transformseq.PIntAxis(0,4,0,False)], 0) - self.check = (x1,s1,e1),(x1,s2,e1),(x1,s3,e1),(x1,s0,e1) - self.checkmissing = (x1,s0,e0),(x1,s1,e0),(x1,s2,e0),(x1,s3,e0),(x1,s4,e0) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.PIntAxis(0,4,0,False)], 0) + self.check = (s1,e1),(s2,e1),(s3,e1),(s0,e1) + self.checkmissing = (s0,e0),(s1,e0),(s2,e0),(s3,e0),(s4,e0) self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) self.checkfromdims = 0 class StructuredTransforms1DPeriodicInterfacesRight(TestCase, Common): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x1, [nutils.transformseq.PIntAxis(0,4,0,True)], 0) - self.check = (x1,s0,e0),(x1,s1,e0),(x1,s2,e0),(x1,s3,e0) - self.checkmissing = (x1,s0,e1),(x1,s1,e1),(x1,s2,e1),(x1,s3,e1),(x1,s4,e1) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.PIntAxis(0,4,0,True)], 0) + self.check = (s0,e0),(s1,e0),(s2,e0),(s3,e0) + self.checkmissing = (s0,e1),(s1,e1),(s2,e1),(s3,e1),(s4,e1) self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) self.checkfromdims = 0 class StructuredTransforms2D(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x2, [nutils.transformseq.DimAxis(0,2,False),nutils.transformseq.DimAxis(2,4,False)], 0) - self.check = (x2,s02),(x2,s03),(x2,s12),(x2,s13) - self.checkmissing = (x2,s00),(x2,s01),(x2,s10),(x2,s11) + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(0,2,False),nutils.transformseq.DimAxis(2,4,False)], 0) + self.check = (s02,),(s03,),(s12,),(s13,) + self.checkmissing = (s00,),(s01,),(s10,),(s11,) self.checkrefs = nutils.elementseq.asreferences([square]*4, 2) self.checkfromdims = 2 class StructuredTransforms2DRefined(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms(x2, [nutils.transformseq.DimAxis(0,2,False),nutils.transformseq.DimAxis(2,4,False)], 1) - self.check = (x2,s01,c00),(x2,s01,c01),(x2,s01,c10),(x2,s01,c11) - self.checkmissing = (x2,s00,c00), + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(0,2,False),nutils.transformseq.DimAxis(2,4,False)], 1) + self.check = (s01,c00),(s01,c01),(s01,c10),(s01,c11) + self.checkmissing = (s00,c00), self.checkrefs = nutils.elementseq.asreferences([square]*4, 2) self.checkfromdims = 2 From 616b08982c08e9cd67968d28abbaeac16204b012 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 11 Feb 2020 22:11:25 +0100 Subject: [PATCH 014/119] don't use Transforms.fromdims In a future commit the transforms may have inhomogeneous `fromdims`. This patch replaces all usage of the `fromdims` attribute. --- nutils/function.py | 38 +++++++++++++++++++++----------------- nutils/mesh.py | 2 +- nutils/sample.py | 11 +++++++---- nutils/topology.py | 36 +++++++++++++++++++----------------- tests/test_basis.py | 2 +- tests/test_finitecell.py | 2 +- tests/test_function.py | 14 +++++++------- 7 files changed, 57 insertions(+), 48 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 06384b69c..846e0c74c 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -370,11 +370,12 @@ def evalf(self, values): class TransformsIndexWithTail(Evaluable): - __slots__ = '_transforms' + __slots__ = '_transforms', '_fromdims' @types.apply_annotations - def __init__(self, transforms, trans:types.strict[TransformChain]): + def __init__(self, transforms:transformseq.stricttransforms, fromdims:types.strictint, trans:types.strict[TransformChain]): self._transforms = transforms + self._fromdims = fromdims super().__init__(args=[trans]) def evalf(self, trans): @@ -390,7 +391,7 @@ def index(self): @property def tail(self): - return TransformChainFromTuple(self, index=1, todims=self._transforms.fromdims) + return TransformChainFromTuple(self, index=1, todims=self._fromdims) def __iter__(self): yield self.index @@ -3270,6 +3271,8 @@ class Basis(Array): The number of functions in this basis. transforms : :class:`nutils.transformseq.Transforms` The transforms on which this basis is defined. + ndims : :class:`int` + Dimension of the topology on which this basis is defined. trans : :class:`TransformChain` Notes @@ -3278,15 +3281,16 @@ class Basis(Array): if possible should redefine :meth:`get_support`. ''' - __slots__ = 'ndofs', 'transforms', '_index', '_points' + __slots__ = 'ndofs', 'transforms', 'ndimsdomain', '_index', '_points' __cache__ = '_computed_support' @types.apply_annotations - def __init__(self, ndofs:types.strictint, transforms:transformseq.stricttransforms, trans:types.strict[TransformChain]=TRANS): + def __init__(self, ndofs:types.strictint, transforms:transformseq.stricttransforms, ndims:types.strictint, trans:types.strict[TransformChain]=TRANS): self.ndofs = ndofs self.transforms = transforms + self.ndimsdomain = ndims - self._index, tail = TransformsIndexWithTail(self.transforms, trans) + self._index, tail = TransformsIndexWithTail(self.transforms, ndims, trans) self._points = ApplyTransforms(tail) super().__init__(args=(self._index, self._points), shape=(ndofs,), dtype=float) @@ -3463,13 +3467,13 @@ class PlainBasis(Basis): __slots__ = '_coeffs', '_dofs' @types.apply_annotations - def __init__(self, coefficients:types.tuple[types.frozenarray], dofs:types.tuple[types.frozenarray], ndofs:types.strictint, transforms:transformseq.stricttransforms, trans=TRANS): + def __init__(self, coefficients:types.tuple[types.frozenarray], dofs:types.tuple[types.frozenarray], ndofs:types.strictint, transforms:transformseq.stricttransforms, ndims:types.strictint, trans=TRANS): self._coeffs = coefficients self._dofs = dofs assert len(self._coeffs) == len(self._dofs) == len(transforms) - assert all(c.ndim == 1+transforms.fromdims for c in self._coeffs) + assert all(c.ndim == 1+ndims for c in self._coeffs) assert all(len(c) == len(d) for c, d in zip(self._coeffs, self._dofs)) - super().__init__(ndofs=ndofs, transforms=transforms, trans=trans) + super().__init__(ndofs=ndofs, transforms=transforms, ndims=ndims, trans=trans) def get_dofs(self, ielem): if not numeric.isint(ielem): @@ -3505,12 +3509,12 @@ class DiscontBasis(Basis): __slots__ = '_coeffs', '_offsets' @types.apply_annotations - def __init__(self, coefficients:types.tuple[types.frozenarray], transforms:transformseq.stricttransforms, trans=TRANS): + def __init__(self, coefficients:types.tuple[types.frozenarray], transforms:transformseq.stricttransforms, ndims:types.strictint, trans=TRANS): self._coeffs = coefficients assert len(self._coeffs) == len(transforms) - assert all(c.ndim == 1+transforms.fromdims for c in self._coeffs) + assert all(c.ndim == 1+ndims for c in self._coeffs) self._offsets = types.frozenarray(numpy.cumsum([0, *map(len, self._coeffs)]), copy=False) - super().__init__(ndofs=self._offsets[-1], transforms=transforms, trans=trans) + super().__init__(ndofs=self._offsets[-1], transforms=transforms, ndims=ndims, trans=trans) def get_support(self, dof): if not numeric.isint(dof): @@ -3565,7 +3569,7 @@ def __init__(self, parent:strictbasis, indices:types.frozenarray[types.strictint raise ValueError('`indices` out of range \x5b0,{}\x29'.format(0, len(parent))) self._parent = parent self._indices = indices - super().__init__(ndofs=len(self._indices), transforms=parent.transforms, trans=trans) + super().__init__(ndofs=len(self._indices), transforms=parent.transforms, ndims=parent.ndimsdomain, trans=trans) def get_dofs(self, ielem): return numeric.sorted_index(self._indices, self._parent.get_dofs(ielem), missing='mask') @@ -3612,7 +3616,7 @@ def __init__(self, coeffs:types.tuple[types.tuple[types.frozenarray]], start_dof self._stop_dofs = stop_dofs self._dofs_shape = dofs_shape self._transforms_shape = transforms_shape - super().__init__(ndofs=util.product(dofs_shape), transforms=transforms, trans=trans) + super().__init__(ndofs=util.product(dofs_shape), transforms=transforms, ndims=len(dofs_shape), trans=trans) def _get_indices(self, ielem): ielem = numeric.normdim(len(self.transforms), ielem) @@ -3700,7 +3704,7 @@ def __init__(self, parent:strictbasis, transmap:types.frozenarray[types.strictin self._parent = parent self._transmap = transmap self._dofmap = parent.get_dofs(self._transmap) - super().__init__(len(self._dofmap), parent.transforms[transmap], trans) + super().__init__(len(self._dofmap), parent.transforms[transmap], parent.ndimsdomain, trans) def get_dofs(self, ielem): if numeric.isintarray(ielem) and ielem.ndim == 1 and numpy.any(numpy.less(ielem, 0)): @@ -4222,8 +4226,8 @@ def eig(arg, axes=(-2,-1), symmetric=False): return Tuple([transpose(diagonalize(eigval), _invtrans(trans)), transpose(eigvec, _invtrans(trans))]) @types.apply_annotations -def elemwise(transforms:transformseq.stricttransforms, values:types.tuple[types.frozenarray]): - index, tail = TransformsIndexWithTail(transforms, TRANS) +def elemwise(transforms:transformseq.stricttransforms, ndims:types.strictint, values:types.tuple[types.frozenarray]): + index, tail = TransformsIndexWithTail(transforms, ndims, TRANS) return Elemwise(values, index, dtype=float) def take(arg, index, axis): diff --git a/nutils/mesh.py b/nutils/mesh.py index 5bc753843..460a0f6a2 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -544,7 +544,7 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): transforms = transformseq.IdentifierTransforms(ndims=ndims, name=name, length=nelems) topo = topology.SimplexTopology(nodes, transforms, transforms) coeffs = element.getsimplex(ndims).get_poly_coeffs('lagrange', degree=degree) - basis = function.PlainBasis([coeffs] * nelems, cnodes, nverts, topo.transforms) + basis = function.PlainBasis([coeffs] * nelems, cnodes, nverts, topo.transforms, ndims) geom = (basis[:,_] * coords).sum(0) connectivity = topo.connectivity diff --git a/nutils/sample.py b/nutils/sample.py index 0635e0900..c93dd5b23 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -72,6 +72,9 @@ class Sample(types.Singleton): Args ---- + ndims : :class:`int` + The dimension of the :class:`~nutils.topology.Topology` from which this + sample is created. transforms : :class:`tuple` or transformation chains List of transformation chains leading to local coordinate systems that contain points. @@ -85,7 +88,7 @@ class Sample(types.Singleton): __cache__ = 'allcoords' @types.apply_annotations - def __init__(self, transforms:types.tuple[transformseq.stricttransforms], points:types.tuple[points.strictpoints], index:types.tuple[types.frozenarray[types.strictint]]): + def __init__(self, ndims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], points:types.tuple[points.strictpoints], index:types.tuple[types.frozenarray[types.strictint]]): assert len(points) == len(index) assert len(transforms) >= 1 assert all(len(t) == len(points) for t in transforms) @@ -94,7 +97,7 @@ def __init__(self, transforms:types.tuple[transformseq.stricttransforms], points self.points = points self.index = index self.npoints = sum(p.npoints for p in points) - self.ndims = transforms[0].fromdims + self.ndims = ndims def __repr__(self): return '{}<{}D, {} elems, {} points>'.format(type(self).__qualname__, self.ndims, self.nelems, self.npoints) @@ -237,7 +240,7 @@ def basis(self): '''Basis-like function that for every point in the sample evaluates to the unit vector corresponding to its index.''' - index, tail = function.TransformsIndexWithTail(self.transforms[0], function.TRANS) + index, tail = function.TransformsIndexWithTail(self.transforms[0], self.ndims, function.TRANS) I = function.Elemwise(self.index, index, dtype=int) B = function.Sampled(function.ApplyTransforms(tail), expect=function.take(self.allcoords, I, axis=0)) return function.Inflate(func=B, dofmap=I, length=self.npoints, axis=0) @@ -310,7 +313,7 @@ def subset(self, mask): transforms = tuple(transform[selection] for transform in self.transforms) points = [self.points[ielem] for ielem in selection] offset = numpy.cumsum([0] + [p.npoints for p in points]) - return Sample(transforms, points, map(numpy.arange, offset[:-1], offset[1:])) + return Sample(self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) strictsample = types.strict[Sample] diff --git a/nutils/topology.py b/nutils/topology.py index 5146b0a75..8a5fd0724 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -47,12 +47,11 @@ class Topology(types.Singleton): @types.apply_annotations def __init__(self, references:elementseq.strictreferences, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms): - assert references.ndims == opposites.fromdims == transforms.fromdims assert len(references) == len(transforms) == len(opposites) self.references = references self.transforms = transforms self.opposites = opposites - self.ndims = transforms.fromdims + self.ndims = references.ndims super().__init__() def __str__(self): @@ -149,7 +148,7 @@ def basis(self, name, *args, **kwargs): Create a basis. ''' if self.ndims == 0: - return function.PlainBasis([[1]], [[0]], 1, self.transforms) + return function.PlainBasis([[1]], [[0]], 1, self.transforms, self.ndims) split = name.split('-', 1) if len(split) == 2 and split[0] in ('h', 'th'): name = split[1] # default to non-hierarchical bases @@ -167,17 +166,17 @@ def sample(self, ischeme, degree): transforms = self.transforms, if len(self.transforms) == 0 or self.opposites != self.transforms: transforms += self.opposites, - return sample.Sample(transforms, points, map(numpy.arange, offset[:-1], offset[1:])) + return sample.Sample(self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) @util.single_or_multiple def integrate_elementwise(self, funcs, *, asfunction=False, **kwargs): 'element-wise integration' - ielem = function.TransformsIndexWithTail(self.transforms, function.TRANS).index + ielem = function.TransformsIndexWithTail(self.transforms, self.ndims, function.TRANS).index with matrix.Numpy(): retvals = self.integrate([function.Inflate(function.asarray(func)[_], dofmap=ielem[_], length=len(self), axis=0) for func in funcs], **kwargs) retvals = [retval.export('dense') if len(retval.shape) == 2 else retval for retval in retvals] - return [function.elemwise(self.transforms, retval) for retval in retvals] if asfunction \ + return [function.elemwise(self.transforms, self.ndims, retval) for retval in retvals] if asfunction \ else retvals @util.single_or_multiple @@ -416,7 +415,7 @@ def indicator(self, subtopo): subtopo = self[subtopo] values = numpy.zeros([len(self)], dtype=int) values[numpy.fromiter(map(self.transforms.index, subtopo.transforms), dtype=int)] = 1 - return function.Get(values, axis=0, item=function.TransformsIndexWithTail(self.transforms, function.TRANS).index) + return function.Get(values, axis=0, item=function.TransformsIndexWithTail(self.transforms, self.ndims, function.TRANS).index) def select(self, indicator, ischeme='bezier2', **kwargs): sample = self.sample(*element.parse_legacy_ischeme(ischeme)) @@ -533,7 +532,7 @@ def _sample(self, ielems, coords): transforms = self.transforms[uielems], if len(self.transforms) == 0 or self.opposites != self.transforms: transforms += self.opposites[uielems], - return sample.Sample(transforms, points_, index) + return sample.Sample(self.ndims, transforms, points_, index) def revolved(self, geom): assert geom.ndim == 1 @@ -630,7 +629,7 @@ def basis_discont(self, degree): coeffs = [self.references[0].get_poly_coeffs('bernstein', degree=degree)]*len(self.references) else: coeffs = [ref.get_poly_coeffs('bernstein', degree=degree) for ref in self.references] - return function.DiscontBasis(coeffs, self.transforms) + return function.DiscontBasis(coeffs, self.transforms, self.ndims) def _basis_c0_structured(self, name, degree): 'C^0-continuous shape functions with lagrange stucture' @@ -668,7 +667,7 @@ def _basis_c0_structured(self, name, degree): elem_slices = map(slice, offsets[:-1], offsets[1:]) dofs = tuple(types.frozenarray(dofmap[s]) for s in elem_slices) - return function.PlainBasis(coeffs, dofs, ndofs, self.transforms) + return function.PlainBasis(coeffs, dofs, ndofs, self.transforms, self.ndims) def basis_lagrange(self, degree): 'lagrange shape functions' @@ -1285,11 +1284,13 @@ def _renumber(simplices): @types.apply_annotations def __init__(self, simplices:_renumber, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms): - assert simplices.shape == (len(transforms), transforms.fromdims+1) + assert simplices.ndim == 2 + assert simplices.shape[0] == len(transforms) assert numpy.greater(simplices[:,1:], simplices[:,:-1]).all(), 'nodes should be sorted' assert not numpy.equal(simplices[:,1:], simplices[:,:-1]).all(), 'duplicate nodes' + ndims = simplices.shape[1] - 1 self.simplices = simplices - references = elementseq.asreferences([element.getsimplex(transforms.fromdims)], transforms.fromdims)*len(transforms) + references = elementseq.asreferences([element.getsimplex(ndims)], ndims)*len(transforms) super().__init__(references, transforms, opposites) @property @@ -1310,7 +1311,7 @@ def connectivity(self): def basis_std(self, degree): if degree == 1: coeffs = element.getsimplex(self.ndims).get_poly_coeffs('bernstein', degree=1) - return function.PlainBasis([coeffs] * len(self), self.simplices, self.simplices.max()+1, self.transforms) + return function.PlainBasis([coeffs] * len(self), self.simplices, self.simplices.max()+1, self.transforms, self.ndims) return super().basis_std(degree) def basis_bubble(self): @@ -1326,7 +1327,7 @@ def basis_bubble(self): nverts = self.simplices.max() + 1 ndofs = nverts + len(self) nmap = [types.frozenarray(numpy.hstack([idofs, nverts+ielem]), copy=False) for ielem, idofs in enumerate(self.simplices)] - return function.PlainBasis([coeffs] * len(self), nmap, ndofs, self.transforms) + return function.PlainBasis([coeffs] * len(self), nmap, ndofs, self.transforms, self.ndims) class UnionTopology(Topology): 'grouped topology' @@ -1866,7 +1867,7 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): hbasis_dofs.append(numpy.concatenate(trans_dofs)) hbasis_coeffs.append(numeric.poly_concatenate(trans_coeffs)) - return function.PlainBasis(hbasis_coeffs, hbasis_dofs, ndofs, self.transforms) + return function.PlainBasis(hbasis_coeffs, hbasis_dofs, ndofs, self.transforms, self.ndims) class ProductTopology(Topology): 'product topology' @@ -2155,14 +2156,15 @@ def basis_spline(self, degree, patchcontinuous=True, knotvalues=None, knotmultip dofmap = tuple(types.frozenarray(tuple(renumber[merge.get(dof, dof)] for dof in v.flat), dtype=int).reshape(v.shape) for v in dofmap) dofcount = len(remainder) - return function.PlainBasis(coeffs, dofmap, dofcount, self.transforms) + return function.PlainBasis(coeffs, dofmap, dofcount, self.transforms, self.ndims) def basis_patch(self): 'degree zero patchwise discontinuous basis' return function.DiscontBasis( [types.frozenarray(1, dtype=int).reshape(1, *(1,)*self.ndims)]*len(self.patches), - transformseq.PlainTransforms(tuple((patch.topo.root,) for patch in self.patches), self.ndims)) + transformseq.PlainTransforms(tuple((patch.topo.root,) for patch in self.patches), self.ndims), + self.ndims) @property def boundary(self): diff --git a/tests/test_basis.py b/tests/test_basis.py index 1cdcfc458..31dbb6ccc 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -297,7 +297,7 @@ def test_pum_range(self): def test_poly(self): target = self.geom.sum(-1) if self.btype == 'bubble' \ - else (self.geom**self.degree).sum(-1) + function.TransformsIndexWithTail(self.domain.transforms, function.TRANS).index if self.btype == 'discont' \ + else (self.geom**self.degree).sum(-1) + function.TransformsIndexWithTail(self.domain.transforms, self.domain.ndims, function.TRANS).index if self.btype == 'discont' \ else (self.geom**self.degree).sum(-1) projection = self.domain.projection(target, onto=self.basis, geometry=self.geom, ischeme='gauss', degree=2*self.degree, droptol=0) error2 = self.domain.integrate((target-projection)**2*function.J(self.geom), ischeme='gauss', degree=2*self.degree) diff --git a/tests/test_finitecell.py b/tests/test_finitecell.py index 53970a15f..6c81937d9 100644 --- a/tests/test_finitecell.py +++ b/tests/test_finitecell.py @@ -393,7 +393,7 @@ def test_transforms(self): self.assertEqual(set(self.topoB.boundary['trimmed'].transforms), set(self.topoA.boundary['trimmed'].opposites)) def test_opposites(self): - ielem = function.elemwise(self.topo.transforms, numpy.arange(4)) + ielem = function.elemwise(self.topo.transforms, self.topo.ndims, numpy.arange(4)) sampleA = self.topoA.boundary['trimmed'].sample('uniform', 1) sampleB = self.topoB.boundary['trimmed'].sample('uniform', 1) self.assertEqual(set(sampleB.eval(ielem)), {0,1}) diff --git a/tests/test_function.py b/tests/test_function.py index b26442a53..7e4ac270e 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -614,7 +614,7 @@ class elemwise(TestCase): def setUp(self): super().setUp() self.domain, geom = mesh.rectilinear([5]) - self.index = function.TransformsIndexWithTail(self.domain.transforms, function.TRANS).index + self.index = function.TransformsIndexWithTail(self.domain.transforms, self.domain.ndims, function.TRANS).index self.data = tuple(map(types.frozenarray, ( numpy.arange(1, dtype=float).reshape(1,1), numpy.arange(2, dtype=float).reshape(1,2), @@ -1051,14 +1051,14 @@ def checkeval(self, ielem, points): return result.tolist() def test_evalf(self): - ref = element.PointReference() if self.basis.transforms.fromdims == 0 else element.LineReference()**self.basis.transforms.fromdims + ref = element.PointReference() if self.basis.ndimsdomain == 0 else element.LineReference()**self.basis.ndimsdomain points = ref.getpoints('bezier', 4).coords with self.assertWarnsRegex(function.ExpensiveEvaluationWarning, 'using explicit basis evaluation.*'): for ielem in range(self.checknelems): self.assertEqual(self.basis.evalf([ielem], points).tolist(), self.checkeval(ielem, points)) def test_simplified(self): - ref = element.PointReference() if self.basis.transforms.fromdims == 0 else element.LineReference()**self.basis.transforms.fromdims + ref = element.PointReference() if self.basis.ndimsdomain == 0 else element.LineReference()**self.basis.ndimsdomain points = ref.getpoints('bezier', 4).coords simplified = self.basis.simplified with _builtin_warnings.catch_warnings(): @@ -1092,7 +1092,7 @@ def setUp(self): transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) self.checkcoeffs = [[1],[2,3],[4,5],[6]] self.checkdofs = [[0],[2,3],[1,3],[2]] - self.basis = function.PlainBasis(self.checkcoeffs, self.checkdofs, 4, transforms) + self.basis = function.PlainBasis(self.checkcoeffs, self.checkdofs, 4, transforms, 0) self.checkndofs = 4 super().setUp() @@ -1100,7 +1100,7 @@ class DiscontBasis(CommonBasis, TestCase): def setUp(self): transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) self.checkcoeffs = [[1],[2,3],[4,5],[6]] - self.basis = function.DiscontBasis(self.checkcoeffs, transforms) + self.basis = function.DiscontBasis(self.checkcoeffs, transforms, 0) self.checkdofs = [[0],[1,2],[3,4],[5]] self.checkndofs = 6 super().setUp() @@ -1108,7 +1108,7 @@ def setUp(self): class MaskedBasis(CommonBasis, TestCase): def setUp(self): transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) - parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transforms) + parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transforms, 0) self.basis = function.MaskedBasis(parent, [0,2]) self.checkcoeffs = [[1],[2],[],[6]] self.checkdofs = [[0],[1],[],[1]] @@ -1118,7 +1118,7 @@ def setUp(self): class PrunedBasis(CommonBasis, TestCase): def setUp(self): parent_transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) - parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, parent_transforms) + parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, parent_transforms, 0) self.basis = function.PrunedBasis(parent, [0,2]) self.checkcoeffs = [[1],[4,5]] self.checkdofs = [[0],[1,2]] From 310110bf55aa1402a4c5ed8f3401c886faf8d484 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 16 Mar 2020 13:27:16 +0100 Subject: [PATCH 015/119] update documentation TransformItem --- nutils/transform.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nutils/transform.py b/nutils/transform.py index 30cc0ce4a..39d3813c8 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -98,6 +98,13 @@ class TransformItem(types.Singleton): Args ---- + todims : :class:`int` + Dimension of the affine transformation domain. + fromdims : :class:`int` + Dimension of the affine transformation range. + + Attributes + ---------- todims : :class:`int` Dimension of the affine transformation domain. fromdims : :class:`int` From e335abc34b94d39318149bb7bb5dcb70d4439e3d Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 17 Mar 2020 21:22:35 +0100 Subject: [PATCH 016/119] remove Opposite.simplified --- nutils/function.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 846e0c74c..6744b2d0e 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -3105,7 +3105,6 @@ def prepare_eval(self, kwargs=...): class Opposite(Array): __slots__ = '_value' - __cache__ = 'simplified' @types.apply_annotations def __init__(self, value:asarray): @@ -3115,13 +3114,6 @@ def __init__(self, value:asarray): def evalf(self, evalargs): raise Exception('Opposite should not be evaluated') - @property - def simplified(self): - value = self._value.simplified - if not any(isinstance(arg, SelectChain) for arg in value.dependencies): - return value - return Opposite(value) - @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): return self._value.prepare_eval(opposite=not opposite, **kwargs) From 4a81ed81349481b575143687feadd95101496859 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 3 Feb 2020 16:32:33 +0100 Subject: [PATCH 017/119] add function.Root --- nutils/function.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index 6744b2d0e..7044e2328 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -45,6 +45,48 @@ from . import util, types, numpy, numeric, cache, transform, transformseq, expression, warnings, _ import sys, itertools, functools, operator, inspect, numbers, builtins, re, types as builtin_types, abc, collections.abc, math, treelog as log +class Root(types.Singleton): + '''Root + + A root can be seen as a real coordinate space of dimension :attr:`ndims`, + identified by a :attr:`name`. An :class:`Evaluable` lives on zero roots, in + which case the evaluable is constant, one root or multiple roots, in which + case all roots must have unique names and the combined coordinate space is + the product of all root coordinate spaces. + + Every transform chain belongs to precisely one root and, if unempty, the + chain should start with a :class:`~nutils.transform.TransformItem` with + :attr:`~nutils.transform.TransformItem.todims` equal to :attr:`ndims`. + + parameters + ---------- + name : :class:`str` + The name of this root. + ndims : :class:`int` + The dimension of the space. Every transform chain beloning to this root + should start with a :class:`~nutils.transform.TransformItem` with + :attr:`~nutils.transform.TransformItem.todims` equal to ``ndims``. + + attributes + ---------- + name : :class:`str` + The name of this root. + ndims : :class:`int` + The dimension of the space. + ''' + + __slots__ = 'name', 'ndims' + + @types.apply_annotations + def __init__(self, name:types.strictstr, ndims: types.strictint): + self.name = name + self.ndims = ndims + + def __repr__(self): + return 'Root({},{})'.format(self.name, self.ndims) + +strictroot = types.strict[Root] + isevaluable = lambda arg: isinstance(arg, Evaluable) def strictevaluable(value): From 4d6a85b899ea14b840e154325de9472e13de0e28 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 16 Mar 2020 13:50:56 +0100 Subject: [PATCH 018/119] add function.Subsample --- nutils/function.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index 7044e2328..c0ace9e02 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -87,6 +87,50 @@ def __repr__(self): strictroot = types.strict[Root] +class Subsample: + '''Subsample + + Parameters + ---------- + roots : :class:`tuple` of :class:`Root` + transforms : :class:`tuple` of transform chains + points : :class:`~nutils.points.Points` + ielem : :class:`int`, optional + + Attributes + ---------- + roots : :class:`tuple` of :class:`Root` + transforms : :class:`tuple` of transform chains + points : :class:`~nutils.points.Points` + ielem : :class:`int` or ``None`` + ''' + + __slots__ = 'roots', 'transforms', 'points', 'ielem' + + def __init__(self, roots: types.tuple[strictroot], transforms: types.tuple[transform.stricttransform], points: points.strictpoints, ielem: types.strictint = None): + if len(roots) != 1: + raise NotImplementedError + self.roots = roots + self.transforms = tuple((chain,) for chain in transforms) + self.points = points + self.ielem = ielem + + @property + def npoints(self): + return self.points.npoints + + @property + def ndims(self): + return builtins.sum(root.ndims for root in self.roots) + + @property + def ndimsmanifold(self): + return self.points.ndimsmanifold + + @property + def ndimsnormal(self): + return self.ndims - self.ndimsmanifold + isevaluable = lambda arg: isinstance(arg, Evaluable) def strictevaluable(value): From 353476d50c0f1d7f95ed0de7bd184b8943abc1c3 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 16 Mar 2020 13:51:48 +0100 Subject: [PATCH 019/119] add function.SubsampleMeta --- nutils/function.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/nutils/function.py b/nutils/function.py index c0ace9e02..9728a7316 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -42,7 +42,7 @@ expensive and currently unsupported operation. """ -from . import util, types, numpy, numeric, cache, transform, transformseq, expression, warnings, _ +from . import util, types, numpy, numeric, cache, transform, transformseq, points, expression, warnings, _ import sys, itertools, functools, operator, inspect, numbers, builtins, re, types as builtin_types, abc, collections.abc, math, treelog as log class Root(types.Singleton): @@ -131,6 +131,42 @@ def ndimsmanifold(self): def ndimsnormal(self): return self.ndims - self.ndimsmanifold +class SubsampleMeta: + '''Subsample meta information + + Parameters + ---------- + roots : :class:`tuple` of :class:`Root` + ndimsnormal : :class:`int` + transforms : :class:`tuple` of :class:`~nutils.transformseq.Transforms`, optional + points : :class:`~nutils.points.Points`, options + The points object if invariant, otherwise ``None``. + + Attributes + ---------- + roots : :class:`tuple` of :class:`Root` + ndimsnormal : :class:`int` + transforms : :class:`tuple` of :class:`~nutils.transformseq.Transforms` or ``None`` + points : :class:`~nutils.points.Points`, options + The points object if invariant, otherwise ``None``. + ''' + + __slots__ = 'roots', 'ndimsnormal', 'transforms', 'points' + + def __init__(self, roots:types.tuple[strictroot], ndimsnormal:types.strictint, transforms:types.tuple[transformseq.stricttransforms]=None, points:points.strictpoints=None): + self.roots = roots + self.ndimsnormal = ndimsnormal + self.transforms = transforms + self.points = points + + @property + def ndims(self): + return builtins.sum(root.ndims for root in self.roots) + + @property + def ndimsmanifold(self): + return self.ndims - self.ndimsnormal + isevaluable = lambda arg: isinstance(arg, Evaluable) def strictevaluable(value): From 40e6bb760ea53d0aa1ea9a1bb85fe4f81fe0f5ae Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Fri, 7 Feb 2020 13:15:53 +0100 Subject: [PATCH 020/119] attach Root to Topology, Sample, Function --- nutils/function.py | 102 ++++++++++++++++----------- nutils/mesh.py | 42 +++++------ nutils/sample.py | 13 ++-- nutils/topology.py | 148 ++++++++++++++++++++++----------------- tests/test_basis.py | 17 ++--- tests/test_finitecell.py | 4 +- tests/test_function.py | 29 +++++--- tests/test_topology.py | 9 +-- 8 files changed, 207 insertions(+), 157 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 9728a7316..af32c88bb 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -202,7 +202,7 @@ class Evaluable(types.Singleton): 'Base class' __slots__ = '__args', - __cache__ = 'dependencies', 'ordereddeps', 'dependencytree', 'simplified', 'prepare_eval', 'optimized_for_numpy' + __cache__ = 'dependencies', 'ordereddeps', 'dependencytree', 'simplified', 'prepare_eval', 'optimized_for_numpy', 'roots' @types.apply_annotations def __init__(self, args:types.tuple[strictevaluable]): @@ -220,6 +220,10 @@ def dependencies(self): deps.extend(func.dependencies) return frozenset(deps) + @property + def roots(self): + return frozenset(root for arg in self.__args for root in arg.roots) + @property def isconstant(self): return EVALARGS not in self.dependencies @@ -260,7 +264,7 @@ def asciitree(self, richoutput=False): if prefix: s = prefix[:-2] + select[bridge.index(prefix[-2:])] + s # locally change prefix into selector if ordereddeps[n] is not None: - s += ' = ' + ordereddeps[n]._asciitree_str() + s += ' = {} {}'.format(ordereddeps[n]._asciitree_str(), ','.join(sorted(tuple(map('{0.name}:{0.ndims}'.format, ordereddeps[n].roots))))) pool.extend((prefix + bridge[i==0], arg) for i, arg in enumerate(reversed(self.dependencytree[n]))) ordereddeps[n] = None lines.append(s) @@ -438,21 +442,28 @@ class TransformChain(Evaluable): Evaluates to a tuple of :class:`nutils.transform.TransformItem` objects. ''' - __slots__ = 'todims', + __slots__ = 'ordered_roots', 'todims' @types.apply_annotations - def __init__(self, args:types.tuple[strictevaluable], todims:types.strictint=None): + def __init__(self, roots:types.tuple[strictroot], args:types.tuple[strictevaluable], todims:types.strictint=None): + self.ordered_roots = roots self.todims = todims super().__init__(args) + @property + def roots(self): + return frozenset(self.ordered_roots) + class SelectChain(TransformChain): __slots__ = 'n' @types.apply_annotations - def __init__(self, n:types.strictint=0): + def __init__(self, roots:types.tuple[strictroot], n:types.strictint=0): + if len(roots) != 1: + raise NotImplementedError self.n = n - super().__init__(args=[EVALARGS]) + super().__init__(roots, args=[EVALARGS], todims=builtins.sum(root.ndims for root in roots)) def evalf(self, evalargs): trans = evalargs['_transforms'][self.n] @@ -461,45 +472,35 @@ def evalf(self, evalargs): @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): - return SelectChain(1-self.n) if opposite else self - -TRANS = SelectChain() - -class TransformChainWithTodims(TransformChain): - - __slots__ = 'trans', - - @types.apply_annotations - def __init__(self, todims:types.strictint, trans=TRANS): - self.trans = trans - super().__init__(args=[self.trans], todims=todims) - - def evalf(self, trans): - assert trans[0].todims == self.todims - return trans + return SelectChain(self.ordered_roots, 1-self.n) if opposite else self class TransformChainFromTuple(TransformChain): - __slots__ = 'index', + __slots__ = 'index' - def __init__(self, values:strictevaluable, index:types.strictint, todims:types.strictint=None): + def __init__(self, roots:types.tuple[strictroot], values:strictevaluable, index:types.strictint, todims:types.strictint=None): assert 0 <= index < len(values) self.index = index - super().__init__(args=[values], todims=todims) + super().__init__(roots, args=[values], todims=todims) def evalf(self, values): return values[self.index] class TransformsIndexWithTail(Evaluable): - __slots__ = '_transforms', '_fromdims' + __slots__ = '_transforms', '_fromdims', '_trans' @types.apply_annotations def __init__(self, transforms:transformseq.stricttransforms, fromdims:types.strictint, trans:types.strict[TransformChain]): self._transforms = transforms self._fromdims = fromdims + self._trans = trans super().__init__(args=[trans]) + @property + def roots(self): + return self._trans.roots + def evalf(self, trans): index, tail = self._transforms.index_with_tail(trans) return numpy.array(index)[None], tail @@ -513,7 +514,7 @@ def index(self): @property def tail(self): - return TransformChainFromTuple(self, index=1, todims=self._fromdims) + return TransformChainFromTuple(self._trans.ordered_roots, self, index=1, todims=self._fromdims) def __iter__(self): yield self.index @@ -1178,13 +1179,17 @@ def _takediag(self, axis, rmaxis): class ApplyTransforms(Array): - __slots__ = 'trans', + __slots__ = 'trans' @types.apply_annotations def __init__(self, trans:types.strict[TransformChain], points:strictevaluable=POINTS): self.trans = trans super().__init__(args=[points, trans], shape=[trans.todims], dtype=float) + @property + def roots(self): + return self.trans.roots + def evalf(self, points, chain): return transform.apply(chain, points) @@ -1195,12 +1200,17 @@ def _derivative(self, var, seen): class LinearFrom(Array): - __slots__ = () + __slots__ = 'trans' @types.apply_annotations def __init__(self, trans:types.strict[TransformChain], fromdims:types.strictint): + self.trans = trans super().__init__(args=[trans], shape=(trans.todims, fromdims), dtype=float) + @property + def roots(self): + return self.trans.roots + def evalf(self, chain): todims, fromdims = self.shape assert not chain or chain[0].todims == todims @@ -3395,19 +3405,24 @@ class Basis(Array): if possible should redefine :meth:`get_support`. ''' - __slots__ = 'ndofs', 'transforms', 'ndimsdomain', '_index', '_points' + __slots__ = 'ndofs', 'transforms', 'ndimsdomain', '_index', '_points', '_trans' __cache__ = '_computed_support' @types.apply_annotations - def __init__(self, ndofs:types.strictint, transforms:transformseq.stricttransforms, ndims:types.strictint, trans:types.strict[TransformChain]=TRANS): + def __init__(self, ndofs:types.strictint, transforms:transformseq.stricttransforms, ndims:types.strictint, trans:types.strict[TransformChain]): self.ndofs = ndofs self.transforms = transforms self.ndimsdomain = ndims self._index, tail = TransformsIndexWithTail(self.transforms, ndims, trans) self._points = ApplyTransforms(tail) + self._trans = trans super().__init__(args=(self._index, self._points), shape=(ndofs,), dtype=float) + @property + def roots(self): + return self._trans.roots + def evalf(self, index, points): warnings.warn('using explicit basis evaluation; this is usually a bug.', ExpensiveEvaluationWarning) index, = index @@ -3550,9 +3565,9 @@ def _derivative(self, var, seen): def __getitem__(self, index): if numeric.isintarray(index) and index.ndim == 1 and numpy.all(numpy.greater(numpy.diff(index), 0)): - return MaskedBasis(self, index) + return MaskedBasis(self, index, self._trans) elif numeric.isboolarray(index) and index.shape == (self.ndofs,): - return MaskedBasis(self, numpy.where(index)[0]) + return MaskedBasis(self, numpy.where(index)[0], self._trans) else: return super().__getitem__(index) @@ -3581,7 +3596,7 @@ class PlainBasis(Basis): __slots__ = '_coeffs', '_dofs' @types.apply_annotations - def __init__(self, coefficients:types.tuple[types.frozenarray], dofs:types.tuple[types.frozenarray], ndofs:types.strictint, transforms:transformseq.stricttransforms, ndims:types.strictint, trans=TRANS): + def __init__(self, coefficients:types.tuple[types.frozenarray], dofs:types.tuple[types.frozenarray], ndofs:types.strictint, transforms:transformseq.stricttransforms, ndims:types.strictint, trans:types.strict[TransformChain]): self._coeffs = coefficients self._dofs = dofs assert len(self._coeffs) == len(self._dofs) == len(transforms) @@ -3623,7 +3638,7 @@ class DiscontBasis(Basis): __slots__ = '_coeffs', '_offsets' @types.apply_annotations - def __init__(self, coefficients:types.tuple[types.frozenarray], transforms:transformseq.stricttransforms, ndims:types.strictint, trans=TRANS): + def __init__(self, coefficients:types.tuple[types.frozenarray], transforms:transformseq.stricttransforms, ndims:types.strictint, trans:types.strict[TransformChain]): self._coeffs = coefficients assert len(self._coeffs) == len(transforms) assert all(c.ndim == 1+ndims for c in self._coeffs) @@ -3674,7 +3689,7 @@ class MaskedBasis(Basis): __slots__ = '_parent', '_indices' @types.apply_annotations - def __init__(self, parent:strictbasis, indices:types.frozenarray[types.strictint], trans=TRANS): + def __init__(self, parent:strictbasis, indices:types.frozenarray[types.strictint], trans:types.strict[TransformChain]): if indices.ndim != 1: raise ValueError('`indices` should have one dimension but got {}'.format(indices.ndim)) if len(indices) and not numpy.all(numpy.greater(numpy.diff(indices), 0)): @@ -3724,7 +3739,7 @@ class StructuredBasis(Basis): __slots__ = '_coeffs', '_start_dofs', '_stop_dofs', '_dofs_shape', '_transforms_shape' @types.apply_annotations - def __init__(self, coeffs:types.tuple[types.tuple[types.frozenarray]], start_dofs:types.tuple[types.frozenarray[types.strictint]], stop_dofs:types.tuple[types.frozenarray[types.strictint]], dofs_shape:types.tuple[types.strictint], transforms:transformseq.stricttransforms, transforms_shape:types.tuple[types.strictint], trans=TRANS): + def __init__(self, coeffs:types.tuple[types.tuple[types.frozenarray]], start_dofs:types.tuple[types.frozenarray[types.strictint]], stop_dofs:types.tuple[types.frozenarray[types.strictint]], dofs_shape:types.tuple[types.strictint], transforms:transformseq.stricttransforms, transforms_shape:types.tuple[types.strictint], trans:types.strict[TransformChain]): self._coeffs = coeffs self._start_dofs = start_dofs self._stop_dofs = stop_dofs @@ -3814,7 +3829,7 @@ class PrunedBasis(Basis): __slots__ = '_parent', '_transmap', '_dofmap' @types.apply_annotations - def __init__(self, parent:strictbasis, transmap:types.frozenarray[types.strictint], trans=TRANS): + def __init__(self, parent:strictbasis, transmap:types.frozenarray[types.strictint], trans:types.strict[TransformChain]): self._parent = parent self._transmap = transmap self._dofmap = parent.get_dofs(self._transmap) @@ -4123,8 +4138,11 @@ def add_T(arg, axes=(-2,-1)): def blocks(arg): return asarray(arg).simplified.blocks -def rootcoords(ndims): - return ApplyTransforms(TransformChainWithTodims(ndims)) +def rootcoords(roots): + if isinstance(roots, Root): + return ApplyTransforms(SelectChain((roots,))) + else: + return concatenate([rootcoords(root) for root in roots], axis=0) def opposite(arg): return Opposite(arg) @@ -4340,8 +4358,8 @@ def eig(arg, axes=(-2,-1), symmetric=False): return Tuple([transpose(diagonalize(eigval), _invtrans(trans)), transpose(eigvec, _invtrans(trans))]) @types.apply_annotations -def elemwise(transforms:transformseq.stricttransforms, ndims:types.strictint, values:types.tuple[types.frozenarray]): - index, tail = TransformsIndexWithTail(transforms, ndims, TRANS) +def elemwise(roots:types.tuple[strictroot], transforms:transformseq.stricttransforms, ndims:types.strictint, values:types.tuple[types.frozenarray]): + index, tail = TransformsIndexWithTail(transforms, ndims, SelectChain(roots)) return Elemwise(values, index, dtype=float) def take(arg, index, axis): diff --git a/nutils/mesh.py b/nutils/mesh.py index 460a0f6a2..c0c814827 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -54,7 +54,7 @@ def rectilinear(richshape, periodic=(), name='rect'): shape.append(len(v)-1) uniform = False - root = transform.Identifier(ndims, name) + root = function.Root(name, ndims) axes = [transformseq.DimAxis(0,n,idim in periodic) for idim, n in enumerate(shape)] topo = topology.StructuredTopology(root, axes) @@ -63,7 +63,7 @@ def rectilinear(richshape, periodic=(), name='rect'): offset = offset[0] if all(s == scale[0] for s in scale[1:]): scale = scale[0] - geom = function.rootcoords(ndims) * scale + offset + geom = function.rootcoords(root) * scale + offset else: funcsp = topo.basis('spline', degree=1, periodic=()) coords = numeric.meshgrid(*richshape).reshape(ndims, -1) @@ -71,7 +71,7 @@ def rectilinear(richshape, periodic=(), name='rect'): return topo, geom -def line(nodes, periodic=False, bnames=None): +def line(nodes, periodic=False, bnames=None, *, rootid='line'): if isinstance(nodes, int): uniform = True assert nodes > 0 @@ -83,9 +83,9 @@ def line(nodes, periodic=False, bnames=None): scale = (nodes[-1]-nodes[0]) / nelems offset = nodes[0] uniform = numpy.equal(nodes, offset + numpy.arange(nelems+1) * scale).all() - root = transform.Identifier(1, 'line') + root = function.Root(rootid, 1) domain = topology.StructuredLine(root, 0, nelems, periodic=periodic, bnames=bnames) - geom = function.rootcoords(1) * scale + offset if uniform else domain.basis('std', degree=1, periodic=False).dot(nodes) + geom = function.rootcoords(root) * scale + offset if uniform else domain.basis('std', degree=1, periodic=False).dot(nodes) return domain, geom def newrectilinear(nodes, periodic=None, bnames=[['left','right'],['bottom','top'],['front','back']]): @@ -94,7 +94,7 @@ def newrectilinear(nodes, periodic=None, bnames=[['left','right'],['bottom','top else: periodic = numpy.asarray(periodic) assert len(periodic) == len(nodes) and periodic.ndim == 1 and periodic.dtype == bool - dims = [line(nodesi, periodici, bnamesi) for nodesi, periodici, bnamesi in zip(nodes, periodic, tuple(bnames)+(None,)*len(nodes))] + dims = [line(nodesi, periodici, bnamesi, rootid=rootid) for nodesi, periodici, bnamesi, rootid in zip(nodes, periodic, tuple(bnames)+(None,)*len(nodes), 'XYZABC')] domain, geom = dims.pop(0) for domaini, geomi in dims: domain = domain * domaini @@ -542,9 +542,10 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): assert vnodes.shape == nodes.shape transforms = transformseq.IdentifierTransforms(ndims=ndims, name=name, length=nelems) - topo = topology.SimplexTopology(nodes, transforms, transforms) + root = function.Root(name, ndims) + topo = topology.SimplexTopology(root, nodes, transforms, transforms) coeffs = element.getsimplex(ndims).get_poly_coeffs('lagrange', degree=degree) - basis = function.PlainBasis([coeffs] * nelems, cnodes, nverts, topo.transforms, ndims) + basis = function.PlainBasis([coeffs] * nelems, cnodes, nverts, topo.transforms, ndims, function.SelectChain(topo.roots)) geom = (basis[:,_] * coords).sum(0) connectivity = topo.connectivity @@ -565,7 +566,7 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): if simplices: transforms = transformseq.PlainTransforms(transforms, ndims-1) opposites = transforms if opposites is None else transformseq.PlainTransforms(opposites, ndims-1) - groups[name] = topology.SimplexTopology(simplices, transforms, opposites) + groups[name] = topology.SimplexTopology(root, simplices, transforms, opposites) pgroups = {} if ptags: @@ -574,7 +575,7 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): for pname, inodes in ptags.items(): ptransforms = transformseq.PlainTransforms([topo.transforms[ielem] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode]], 0) preferences = elementseq.asreferences([element.getsimplex(0)], 0)*len(ptransforms) - pgroups[pname] = topology.Topology(preferences, ptransforms, ptransforms) + pgroups[pname] = topology.Topology((root,), preferences, ptransforms, ptransforms) vgroups = {} for name, ielems in tags.items(): @@ -582,7 +583,7 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): vgroups[name] = topo.withgroups(bgroups=bgroups, igroups=igroups, pgroups=pgroups) continue transforms = topo.transforms[ielems] - vtopo = topology.SimplexTopology(nodes[ielems], transforms, transforms) + vtopo = topology.SimplexTopology(root, nodes[ielems], transforms, transforms) keep = numpy.zeros(nelems, dtype=bool) keep[ielems] = True vbgroups = {} @@ -613,12 +614,12 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): if simplices: transforms = transformseq.PlainTransforms(transforms, ndims-1) opposites = transformseq.PlainTransforms(opposites, ndims-1) if len(opposites) == len(transforms) else transforms - groups[bname] = topology.SimplexTopology(simplices, transforms, opposites) + groups[bname] = topology.SimplexTopology(root, simplices, transforms, opposites) vpgroups = {} for pname, inodes in ptags.items(): ptransforms = transformseq.PlainTransforms([topo.transforms[ielem] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode] if keep[ielem]], 0) preferences = elementseq.asreferences([element.getsimplex(0)], 0)*len(ptransforms) - vpgroups[pname] = topology.Topology(preferences, ptransforms, ptransforms) + vpgroups[pname] = topology.Topology((root,), preferences, ptransforms, ptransforms) vgroups[name] = vtopo.withgroups(bgroups=vbgroups, igroups=vigroups, pgroups=vpgroups) return topo.withgroups(vgroups=vgroups, bgroups=bgroups, igroups=igroups, pgroups=pgroups), geom @@ -658,7 +659,8 @@ def unitsquare(nelems, etype): The geometry function. ''' - root = transform.Identifier(2, 'unitsquare') + root = function.Root('unitsquare', 2) + idtrans = transform.Identifier(2, 'unitsquare') if etype == 'square': topo = topology.StructuredTopology(root, [transformseq.DimAxis(0, nelems, False)] * 2) @@ -670,8 +672,8 @@ def unitsquare(nelems, etype): v = numpy.arange(nelems+1, dtype=float) coords = numeric.meshgrid(v, v).reshape(2,-1).T - transforms = transformseq.PlainTransforms([(root, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], 2) - topo = topology.SimplexTopology(simplices, transforms, transforms) + transforms = transformseq.PlainTransforms([(idtrans, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], 2) + topo = topology.SimplexTopology(root, simplices, transforms, transforms) if etype == 'mixed': references = list(topo.references) @@ -682,18 +684,18 @@ def unitsquare(nelems, etype): for n in sorted(isquares, reverse=True): i, j = divmod(n, nelems) references[n*2:(n+1)*2] = square, - transforms[n*2:(n+1)*2] = (root, transform.Shift([float(i),float(j)])), + transforms[n*2:(n+1)*2] = (idtrans, transform.Shift([float(i),float(j)])), connectivity[n*2:(n+1)*2] = numpy.concatenate(connectivity[n*2:(n+1)*2])[[3,2,4,1] if i%2==j%2 else [3,2,0,5]], connectivity = [c-numpy.greater(c,n*2) for c in connectivity] - topo = topology.ConnectedTopology(elementseq.asreferences(references, 2), transformseq.PlainTransforms(transforms, 2),transformseq.PlainTransforms(transforms, 2), tuple(types.frozenarray(c, copy=False) for c in connectivity)) + topo = topology.ConnectedTopology((root,), elementseq.asreferences(references, 2), transformseq.PlainTransforms(transforms, 2),transformseq.PlainTransforms(transforms, 2), tuple(types.frozenarray(c, copy=False) for c in connectivity)) - x, y = topo.boundary.elem_mean(function.rootcoords(2), degree=1).T + x, y = topo.boundary.elem_mean(function.rootcoords(root), degree=1).T bgroups = dict(left=x==0, right=x==nelems, bottom=y==0, top=y==nelems) topo = topo.withboundary(**{name: topo.boundary[numpy.where(mask)[0]] for name, mask in bgroups.items()}) else: raise Exception('invalid element type {!r}'.format(etype)) - return topo, function.rootcoords(2) / nelems + return topo, function.rootcoords(root) / nelems # vim:sw=2:sts=2:et diff --git a/nutils/sample.py b/nutils/sample.py index c93dd5b23..4f01078f9 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -70,8 +70,10 @@ class Sample(types.Singleton): respectively. Availability of these properties depends on the selected sample points, and is typically used in combination with the "bezier" set. - Args - ---- + Parameters + ---------- + roots : :class:`tuple` of :class:`~nutils.function.Root` + The roots of this sample. ndims : :class:`int` The dimension of the :class:`~nutils.topology.Topology` from which this sample is created. @@ -88,7 +90,7 @@ class Sample(types.Singleton): __cache__ = 'allcoords' @types.apply_annotations - def __init__(self, ndims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], points:types.tuple[points.strictpoints], index:types.tuple[types.frozenarray[types.strictint]]): + def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], points:types.tuple[points.strictpoints], index:types.tuple[types.frozenarray[types.strictint]]): assert len(points) == len(index) assert len(transforms) >= 1 assert all(len(t) == len(points) for t in transforms) @@ -98,6 +100,7 @@ def __init__(self, ndims:types.strictint, transforms:types.tuple[transformseq.st self.index = index self.npoints = sum(p.npoints for p in points) self.ndims = ndims + self.roots = roots def __repr__(self): return '{}<{}D, {} elems, {} points>'.format(type(self).__qualname__, self.ndims, self.nelems, self.npoints) @@ -240,7 +243,7 @@ def basis(self): '''Basis-like function that for every point in the sample evaluates to the unit vector corresponding to its index.''' - index, tail = function.TransformsIndexWithTail(self.transforms[0], self.ndims, function.TRANS) + index, tail = function.TransformsIndexWithTail(self.transforms[0], self.ndims, function.SelectChain(self.roots)) I = function.Elemwise(self.index, index, dtype=int) B = function.Sampled(function.ApplyTransforms(tail), expect=function.take(self.allcoords, I, axis=0)) return function.Inflate(func=B, dofmap=I, length=self.npoints, axis=0) @@ -313,7 +316,7 @@ def subset(self, mask): transforms = tuple(transform[selection] for transform in self.transforms) points = [self.points[ielem] for ielem in selection] offset = numpy.cumsum([0] + [p.npoints for p in points]) - return Sample(self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) + return Sample(self.roots, self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) strictsample = types.strict[Sample] diff --git a/nutils/topology.py b/nutils/topology.py index 8a5fd0724..80b807e64 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -42,12 +42,15 @@ class Topology(types.Singleton): 'topology base class' - __slots__ = 'references', 'transforms', 'opposites', 'ndims' + __slots__ = 'references', 'transforms', 'opposites', 'ndims', 'roots' __cache__ = 'border_transforms', 'boundary', 'interfaces' @types.apply_annotations - def __init__(self, references:elementseq.strictreferences, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms): + def __init__(self, roots:types.tuple[function.strictroot], references:elementseq.strictreferences, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms): assert len(references) == len(transforms) == len(opposites) + if len(roots) != 1: + raise NotImplementedError + self.roots = roots self.references = references self.transforms = transforms self.opposites = opposites @@ -63,18 +66,18 @@ def __len__(self): return len(self.references) def getitem(self, item): - return EmptyTopology(self.ndims) + return EmptyTopology(self.roots, self.ndims) def __getitem__(self, item): if numeric.isintarray(item): item = types.frozenarray(item) - return Topology(self.references[item], self.transforms[item], self.opposites[item]) + return Topology(self.roots, self.references[item], self.transforms[item], self.opposites[item]) if not isinstance(item, tuple): item = item, if all(it in (...,slice(None)) for it in item): return self topo = self.getitem(item) if len(item) != 1 or not isinstance(item[0],str) \ - else functools.reduce(operator.or_, map(self.getitem, item[0].split(',')), EmptyTopology(self.ndims)) + else functools.reduce(operator.or_, map(self.getitem, item[0].split(',')), EmptyTopology(self.roots, self.ndims)) if not topo: raise KeyError(item) return topo @@ -83,7 +86,7 @@ def __invert__(self): return OppositeTopology(self) def __or__(self, other): - assert isinstance(other, Topology) and other.ndims == self.ndims + assert isinstance(other, Topology) and other.roots == self.roots and other.ndims == self.ndims return other if not self \ else self if not other \ else NotImplemented if isinstance(other, UnionTopology) \ @@ -92,6 +95,8 @@ def __or__(self, other): __ror__ = lambda self, other: self.__or__(other) def __and__(self, other): + if not isinstance(other, Topology) or self.roots != other.roots: + return NotImplemented keep_self = numpy.array(list(map(other.transforms.contains_with_tail, self.transforms)), dtype=bool) if keep_self.all(): return self @@ -106,7 +111,7 @@ def __and__(self, other): references = elementseq.chain([self.references[ind_self], other.references[ind_other]], self.ndims) transforms = transformseq.chain([self.transforms[ind_self], other.transforms[ind_other]], self.ndims) opposites = transformseq.chain([self.opposites[ind_self], other.opposites[ind_other]], self.ndims) - return Topology(references, transforms, opposites) + return Topology(self.roots, references, transforms, opposites) __rand__ = lambda self, other: self.__and__(other) @@ -114,11 +119,11 @@ def __add__(self, other): return self | other def __sub__(self, other): - assert isinstance(other, Topology) and other.ndims == self.ndims + assert isinstance(other, Topology) and self.roots == other.roots and other.ndims == self.ndims return other.__rsub__(self) def __rsub__(self, other): - assert isinstance(other, Topology) and other.ndims == self.ndims + assert isinstance(other, Topology) and self.roots == other.roots and other.ndims == self.ndims return other - other.subset(self, newboundary=getattr(self,'boundary',None)) def __mul__(self, other): @@ -148,7 +153,7 @@ def basis(self, name, *args, **kwargs): Create a basis. ''' if self.ndims == 0: - return function.PlainBasis([[1]], [[0]], 1, self.transforms, self.ndims) + return function.PlainBasis([[1]], [[0]], 1, self.transforms, self.ndims, function.SelectChain(self.roots)) split = name.split('-', 1) if len(split) == 2 and split[0] in ('h', 'th'): name = split[1] # default to non-hierarchical bases @@ -166,17 +171,17 @@ def sample(self, ischeme, degree): transforms = self.transforms, if len(self.transforms) == 0 or self.opposites != self.transforms: transforms += self.opposites, - return sample.Sample(self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) + return sample.Sample(self.roots, self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) @util.single_or_multiple def integrate_elementwise(self, funcs, *, asfunction=False, **kwargs): 'element-wise integration' - ielem = function.TransformsIndexWithTail(self.transforms, self.ndims, function.TRANS).index + ielem = function.TransformsIndexWithTail(self.transforms, self.ndims, function.SelectChain(self.roots)).index with matrix.Numpy(): retvals = self.integrate([function.Inflate(function.asarray(func)[_], dofmap=ielem[_], length=len(self), axis=0) for func in funcs], **kwargs) retvals = [retval.export('dense') if len(retval.shape) == 2 else retval for retval in retvals] - return [function.elemwise(self.transforms, self.ndims, retval) for retval in retvals] if asfunction \ + return [function.elemwise(self.roots, self.transforms, self.ndims, retval) for retval in retvals] if asfunction \ else retvals @util.single_or_multiple @@ -370,6 +375,8 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None def subset(self, topo, newboundary=None, strict=False): 'intersection' + if topo.roots != self.roots: + raise ValueError('different roots') refs = [ref.empty for ref in self.references] for ref, trans in zip(topo.references, topo.transforms): try: @@ -382,7 +389,7 @@ def subset(self, topo, newboundary=None, strict=False): assert subref == ref, 'elements do not form a strict subset' refs[ielem] = subref if not any(refs): - return EmptyTopology(self.ndims) + return EmptyTopology(self.roots, self.ndims) return SubsetTopology(self, refs, newboundary) def withgroups(self, vgroups={}, bgroups={}, igroups={}, pgroups={}): @@ -415,7 +422,7 @@ def indicator(self, subtopo): subtopo = self[subtopo] values = numpy.zeros([len(self)], dtype=int) values[numpy.fromiter(map(self.transforms.index, subtopo.transforms), dtype=int)] = 1 - return function.Get(values, axis=0, item=function.TransformsIndexWithTail(self.transforms, self.ndims, function.TRANS).index) + return function.Get(values, axis=0, item=function.TransformsIndexWithTail(self.transforms, self.ndims, function.SelectChain(self.roots)).index) def select(self, indicator, ischeme='bezier2', **kwargs): sample = self.sample(*element.parse_legacy_ischeme(ischeme)) @@ -532,7 +539,7 @@ def _sample(self, ielems, coords): transforms = self.transforms[uielems], if len(self.transforms) == 0 or self.opposites != self.transforms: transforms += self.opposites[uielems], - return sample.Sample(self.ndims, transforms, points_, index) + return sample.Sample(self.roots, self.ndims, transforms, points_, index) def revolved(self, geom): assert geom.ndim == 1 @@ -545,9 +552,9 @@ def revolved(self, geom): def extruded(self, geom, nelems, periodic=False, bnames=('front','back')): assert geom.ndim == 1 - root = transform.Identifier(self.ndims+1, 'extrude') + root = transform.Identifier('extrude', 1) extopo = self * StructuredLine(root, i=0, j=nelems, periodic=periodic, bnames=bnames) - exgeom = function.concatenate(function.bifurcate(geom, function.rootcoords(1))) + exgeom = function.concatenate(function.bifurcate(geom, function.rootcoords(root))) return extopo, exgeom @property @@ -581,7 +588,7 @@ def boundary(self): else: references = self.references.edges[selection] transforms = self.transforms.edges(self.references)[selection] - return Topology(references, transforms, transforms) + return Topology(self.roots, references, transforms, transforms) @property @log.withcontext @@ -615,7 +622,7 @@ def interfaces(self): references = elementseq.asreferences(references, self.ndims-1) else: references = self.references.edges[selection] - return Topology(references, edges[selection], edges[oppselection]) + return Topology(self.roots, references, edges[selection], edges[oppselection]) def basis_spline(self, degree): assert degree == 1 @@ -629,7 +636,7 @@ def basis_discont(self, degree): coeffs = [self.references[0].get_poly_coeffs('bernstein', degree=degree)]*len(self.references) else: coeffs = [ref.get_poly_coeffs('bernstein', degree=degree) for ref in self.references] - return function.DiscontBasis(coeffs, self.transforms, self.ndims) + return function.DiscontBasis(coeffs, self.transforms, self.ndims, function.SelectChain(self.roots)) def _basis_c0_structured(self, name, degree): 'C^0-continuous shape functions with lagrange stucture' @@ -667,7 +674,7 @@ def _basis_c0_structured(self, name, degree): elem_slices = map(slice, offsets[:-1], offsets[1:]) dofs = tuple(types.frozenarray(dofmap[s]) for s in elem_slices) - return function.PlainBasis(coeffs, dofs, ndofs, self.transforms, self.ndims) + return function.PlainBasis(coeffs, dofs, ndofs, self.transforms, self.ndims, function.SelectChain(self.roots)) def basis_lagrange(self, degree): 'lagrange shape functions' @@ -698,8 +705,8 @@ def __init__(self, basetopo:stricttopology, vgroups:types.frozendict={}, bgroups self.bgroups = bgroups self.igroups = igroups self.pgroups = pgroups - super().__init__(basetopo.references, basetopo.transforms, basetopo.opposites) - assert all(topo is Ellipsis or isinstance(topo, str) or isinstance(topo, Topology) and topo.ndims == basetopo.ndims for topo in self.vgroups.values()) + super().__init__(basetopo.roots, basetopo.references, basetopo.transforms, basetopo.opposites) + assert all(topo is Ellipsis or isinstance(topo, str) or isinstance(topo, Topology) and topo.roots == basetopo.roots and topo.ndims == basetopo.ndims for topo in self.vgroups.values()) def __len__(self): return len(self.basetopo) @@ -740,7 +747,7 @@ def interfaces(self): else: raise ValueError('group is not a subset of topology') s = types.frozenarray(tuple(sorted(s)), dtype=int) - igroups[name] = Topology(baseitopo.references[s], baseitopo.transforms[s], baseitopo.opposites[s]) + igroups[name] = Topology(self.roots, baseitopo.references[s], baseitopo.transforms[s], baseitopo.opposites[s]) return baseitopo.withgroups(igroups) @property @@ -771,7 +778,7 @@ class OppositeTopology(Topology): def __init__(self, basetopo): self.basetopo = basetopo - super().__init__(basetopo.references, basetopo.opposites, basetopo.transforms) + super().__init__(basetopo.roots, basetopo.references, basetopo.opposites, basetopo.transforms) def getitem(self, item): return ~(self.basetopo.getitem(item)) @@ -788,8 +795,8 @@ class EmptyTopology(Topology): __slots__ = () @types.apply_annotations - def __init__(self, ndims:types.strictint): - super().__init__(elementseq.EmptyReferences(ndims), transformseq.EmptyTransforms(ndims), transformseq.EmptyTransforms(ndims)) + def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint): + super().__init__(roots, elementseq.EmptyReferences(ndims), transformseq.EmptyTransforms(ndims), transformseq.EmptyTransforms(ndims)) def __or__(self, other): assert self.ndims == other.ndims @@ -805,18 +812,18 @@ class Point(Topology): @types.aspreprocessor @types.apply_annotations - def _preprocess_init(self, trans:transform.stricttransform, opposite:transform.stricttransform=None): - return (self, trans, trans if opposite is None else opposite), {} + def _preprocess_init(self, roots:types.tuple[function.strictroot], trans:transform.stricttransform, opposite:transform.stricttransform=None): + return (self, roots, trans, trans if opposite is None else opposite), {} @_preprocess_init - def __init__(self, trans, opposite): + def __init__(self, roots, trans, opposite): assert trans[-1].fromdims == 0 references = elementseq.asreferences([element.getsimplex(0)], 0) transforms = transformseq.PlainTransforms((trans,), 0) opposites = transforms if opposite is None else transformseq.PlainTransforms((opposite,), 0) - super().__init__(references, transforms, opposites) + super().__init__(roots, references, transforms, opposites) -def StructuredLine(root:transform.stricttransformitem, i:types.strictint, j:types.strictint, periodic:bool=False, bnames:types.tuple[types.strictstr]=None): +def StructuredLine(root:function.strictroot, i:types.strictint, j:types.strictint, periodic:bool=False, bnames:types.tuple[types.strictstr]=None): if bnames is None: bnames = ('_structured_line_dummy_boundary_name_',) * 2 return StructuredTopology(root, axes=(transformseq.DimAxis(i,j,periodic),), nrefine=0, bnames=(bnames,)) @@ -828,7 +835,7 @@ class StructuredTopology(Topology): __cache__ = 'connectivity', 'boundary', 'interfaces' @types.apply_annotations - def __init__(self, root:transform.stricttransformitem, axes:types.tuple[types.strict[transformseq.Axis]], nrefine:types.strictint=0, bnames:types.tuple[types.tuple[types.strictstr]]=(('left', 'right'), ('bottom', 'top'), ('front', 'back'))): + def __init__(self, root:function.strictroot, axes:types.tuple[types.strict[transformseq.Axis]], nrefine:types.strictint=0, bnames:types.tuple[types.tuple[types.strictstr]]=(('left', 'right'), ('bottom', 'top'), ('front', 'back'))): 'constructor' assert all(len(bname) == 2 for bname in bnames) @@ -848,7 +855,7 @@ def __init__(self, root:transform.stricttransformitem, axes:types.tuple[types.st axes = [transformseq.BndAxis(axis.i, axis.j, axis.ibound, not axis.side) if not axis.isdim and axis.ibound==nbounds-1 else axis for axis in self.axes] opposites = transformseq.StructuredTransforms(axes, self.nrefine) - super().__init__(references, transforms, opposites) + super().__init__((root,), references, transforms, opposites) def __repr__(self): return '{}<{}>'.format(type(self).__qualname__, 'x'.join(str(axis.j-axis.i)+('p' if axis.isperiodic else '') for axis in self.axes if isinstance(axis, transformseq.DimAxis))) @@ -858,7 +865,7 @@ def __len__(self): def getitem(self, item): if not isinstance(item, tuple): - return EmptyTopology(self.ndims) + return EmptyTopology(self.roots, self.ndims) assert all(isinstance(it,slice) for it in item) and len(item) <= self.ndims if all(it == slice(None) for it in item): # shortcut return self @@ -906,7 +913,7 @@ def boundary(self): for idim, axis in enumerate(self.axes) if axis.isdim and not axis.isperiodic for side, n in enumerate((axis.i,axis.j))] if not btopos: - return EmptyTopology(self.ndims-1) + return EmptyTopology(self.roots, self.ndims-1) bnames = [bname for bnames, axis in zip(self._bnames, self.axes) if axis.isdim and not axis.isperiodic for bname in bnames] return DisjointUnionTopology(btopos, bnames) @@ -926,7 +933,7 @@ def interfaces(self): itransforms = transformseq.StructuredTransforms(axes, self.nrefine) iopposites = transformseq.StructuredTransforms(oppaxes, self.nrefine) ireferences = elementseq.asreferences([util.product(element.getsimplex(1 if a.isdim else 0) for a in axes)], self.ndims-1)*len(itransforms) - itopos.append(Topology(ireferences, itransforms, iopposites)) + itopos.append(Topology(self.roots, ireferences, itransforms, iopposites)) assert len(itopos) == self.ndims return DisjointUnionTopology(itopos, names=['dir{}'.format(idim) for idim in range(self.ndims)]) @@ -1166,7 +1173,7 @@ def basis_spline(self, degree, removedofs=None, knotvalues=None, knotmultiplicit coeffs.append(tuple(coeffs_i)) transforms_shape = tuple(axis.j-axis.i for axis in self.axes if axis.isdim) - func = function.StructuredBasis(coeffs, start_dofs, stop_dofs, dofshape, self.transforms, transforms_shape) + func = function.StructuredBasis(coeffs, start_dofs, stop_dofs, dofshape, self.transforms, transforms_shape, function.SelectChain(self.roots)) if not any(removedofs): return func @@ -1236,7 +1243,7 @@ def locate(self, geom, coords, *, eps=0, tol=None, **kwargs): coords = coords[...,_] if not geom.shape == coords.shape[1:] == (self.ndims,): raise Exception('invalid geometry or point shape for {}D topology'.format(self.ndims)) - index = function.rootcoords(len(self.axes))[[axis.isdim for axis in self.axes]] + index = function.rootcoords(self.root)[[axis.isdim for axis in self.axes]] basis = function.concatenate([function.eye(self.ndims), function.diagonalize(index)], axis=0) A, b = self.integrate([(basis[:,_,:] * basis[_,:,:]).sum(-1), (basis * geom).sum(-1)], degree=2) x = A.solve(b) @@ -1265,10 +1272,10 @@ class ConnectedTopology(Topology): __slots__ = 'connectivity', @types.apply_annotations - def __init__(self, references:elementseq.strictreferences, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms, connectivity): + def __init__(self, roots:types.tuple[function.strictroot], references:elementseq.strictreferences, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms, connectivity): assert len(connectivity) == len(references) and all(len(c) == e.nedges for c, e in zip(connectivity, references)) self.connectivity = connectivity - super().__init__(references, transforms, opposites) + super().__init__(roots, references, transforms, opposites) class SimplexTopology(Topology): 'simpex topology' @@ -1283,7 +1290,7 @@ def _renumber(simplices): return types.frozenarray(simplices if keep.all() else (numpy.cumsum(keep)-1)[simplices], copy=False) @types.apply_annotations - def __init__(self, simplices:_renumber, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms): + def __init__(self, root:function.strictroot, simplices:_renumber, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms): assert simplices.ndim == 2 assert simplices.shape[0] == len(transforms) assert numpy.greater(simplices[:,1:], simplices[:,:-1]).all(), 'nodes should be sorted' @@ -1291,7 +1298,7 @@ def __init__(self, simplices:_renumber, transforms:transformseq.stricttransforms ndims = simplices.shape[1] - 1 self.simplices = simplices references = elementseq.asreferences([element.getsimplex(ndims)], ndims)*len(transforms) - super().__init__(references, transforms, opposites) + super().__init__((root,), references, transforms, opposites) @property def connectivity(self): @@ -1311,7 +1318,7 @@ def connectivity(self): def basis_std(self, degree): if degree == 1: coeffs = element.getsimplex(self.ndims).get_poly_coeffs('bernstein', degree=1) - return function.PlainBasis([coeffs] * len(self), self.simplices, self.simplices.max()+1, self.transforms, self.ndims) + return function.PlainBasis([coeffs] * len(self), self.simplices, self.simplices.max()+1, self.transforms, self.ndims, function.SelectChain(self.roots)) return super().basis_std(degree) def basis_bubble(self): @@ -1327,7 +1334,7 @@ def basis_bubble(self): nverts = self.simplices.max() + 1 ndofs = nverts + len(self) nmap = [types.frozenarray(numpy.hstack([idofs, nverts+ielem]), copy=False) for ielem, idofs in enumerate(self.simplices)] - return function.PlainBasis([coeffs] * len(self), nmap, ndofs, self.transforms, self.ndims) + return function.PlainBasis([coeffs] * len(self), nmap, ndofs, self.transforms, self.ndims, function.SelectChain(self.roots)) class UnionTopology(Topology): 'grouped topology' @@ -1339,8 +1346,9 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st self._topos = topos self._names = tuple(names)[:len(self._topos)] assert len(set(self._names)) == len(self._names), 'duplicate name' + roots = self._topos[0].roots ndims = self._topos[0].ndims - assert all(topo.ndims == ndims for topo in self._topos) + assert all(topo.roots == roots and topo.ndims == ndims for topo in self._topos) references = [] selections = [[] for topo in topos] @@ -1371,13 +1379,14 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st selections = tuple(map(types.frozenarray[int], selections)) super().__init__( + roots, elementseq.asreferences(references, ndims), transformseq.chain((topo.transforms[selection] for topo, selection in zip(topos, selections)), ndims), transformseq.chain((topo.opposites[selection] for topo, selection in zip(topos, selections)), ndims)) def getitem(self, item): topos = [topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)] - return functools.reduce(operator.or_, topos, EmptyTopology(self.ndims)) + return functools.reduce(operator.or_, topos, EmptyTopology(self.roots, self.ndims)) def __or__(self, other): if not isinstance(other, UnionTopology): @@ -1398,9 +1407,11 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st self._topos = topos self._names = tuple(names)[:len(self._topos)] assert len(set(self._names)) == len(self._names), 'duplicate name' + roots = self._topos[0].roots ndims = self._topos[0].ndims - assert all(topo.ndims == ndims for topo in self._topos) + assert all(topo.roots == roots and topo.ndims == ndims for topo in self._topos) super().__init__( + roots, elementseq.chain((topo.references for topo in self._topos), ndims), transformseq.chain((topo.transforms for topo in self._topos), ndims), transformseq.chain((topo.opposites for topo in self._topos), ndims)) @@ -1409,7 +1420,7 @@ def getitem(self, item): topos = [topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)] topos = [topo for topo in topos if not isinstance(topo, EmptyTopology)] if len(topos) == 0: - return EmptyTopology(self.ndims) + return EmptyTopology(self.roots, self.ndims) elif len(topos) == 1: return topos[0] else: @@ -1438,7 +1449,7 @@ def __init__(self, basetopo:stricttopology, refs:types.tuple[element.strictrefer references = elementseq.asreferences(self.refs, self.basetopo.ndims)[self._indices] transforms = self.basetopo.transforms[self._indices] opposites = self.basetopo.opposites[self._indices] - super().__init__(references, transforms, opposites) + super().__init__(basetopo.roots, references, transforms, opposites) def getitem(self, item): return self.basetopo.getitem(item).subset(self, strict=False) @@ -1469,7 +1480,7 @@ def refined(self): child_refs = self.references.children indices = types.frozenarray(numpy.array([i for i, ref in enumerate(child_refs) if ref], dtype=int), copy=False) refined_transforms = self.transforms.refined(self.references)[indices] - self_refined = Topology(child_refs[indices], refined_transforms, refined_transforms) + self_refined = Topology(self.roots, child_refs[indices], refined_transforms, refined_transforms) return self.basetopo.refined.subset(self_refined, self.newboundary.refined if isinstance(self.newboundary,Topology) else self.newboundary, strict=True) @property @@ -1519,7 +1530,7 @@ def boundary(self): trimmedbrefs[self.newboundary.transforms.index(trans)] = ref trimboundary = SubsetTopology(self.newboundary, trimmedbrefs) else: - trimboundary = Topology(elementseq.asreferences(trimmedreferences, self.ndims-1), transformseq.PlainTransforms(trimmedtransforms, self.ndims-1), transformseq.PlainTransforms(trimmedopposites, self.ndims-1)) + trimboundary = Topology(self.roots, elementseq.asreferences(trimmedreferences, self.ndims-1), transformseq.PlainTransforms(trimmedtransforms, self.ndims-1), transformseq.PlainTransforms(trimmedopposites, self.ndims-1)) return DisjointUnionTopology([trimboundary, origboundary], names=[self.newboundary] if isinstance(self.newboundary,str) else []) @property @@ -1540,7 +1551,7 @@ def basis(self, name, *args, **kwargs): if isinstance(self.basetopo, HierarchicalTopology): warnings.warn('basis may be linearly dependent; a linearly indepent basis is obtained by trimming first, then creating hierarchical refinements') basis = self.basetopo.basis(name, *args, **kwargs) - return function.PrunedBasis(basis, self._indices) + return function.PrunedBasis(basis, self._indices, function.SelectChain(self.roots)) def locate(self, geom, coords, *, eps=0, **kwargs): sample = self.basetopo.locate(geom, coords, eps=eps, **kwargs) @@ -1563,6 +1574,7 @@ class RefinedTopology(Topology): def __init__(self, basetopo:stricttopology): self.basetopo = basetopo super().__init__( + self.basetopo.roots, self.basetopo.references.children, self.basetopo.transforms.refined(self.basetopo.references), self.basetopo.opposites.refined(self.basetopo.references)) @@ -1619,7 +1631,7 @@ def __init__(self, basetopo:stricttopology, indices_per_level:types.tuple[types. opposites.append(level.opposites[indices]) self.levels = tuple(levels) - super().__init__(elementseq.chain(references, basetopo.ndims), transformseq.chain(transforms, basetopo.ndims), transformseq.chain(opposites, basetopo.ndims)) + super().__init__(basetopo.roots, elementseq.chain(references, basetopo.ndims), transformseq.chain(transforms, basetopo.ndims), transformseq.chain(opposites, basetopo.ndims)) def getitem(self, item): itemtopo = self.basetopo.getitem(item) @@ -1719,7 +1731,7 @@ def interfaces(self): hreferences.append(level.interfaces.references[selection]) htransforms.append(level.interfaces.transforms[selection]) hopposites.append(level.interfaces.opposites[selection]) - return Topology(elementseq.chain(hreferences, self.ndims-1), transformseq.chain(htransforms, self.ndims-1), transformseq.chain(hopposites, self.ndims-1)) + return Topology(self.roots, elementseq.chain(hreferences, self.ndims-1), transformseq.chain(htransforms, self.ndims-1), transformseq.chain(hopposites, self.ndims-1)) @log.withcontext def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): @@ -1867,7 +1879,7 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): hbasis_dofs.append(numpy.concatenate(trans_dofs)) hbasis_coeffs.append(numeric.poly_concatenate(trans_coeffs)) - return function.PlainBasis(hbasis_coeffs, hbasis_dofs, ndofs, self.transforms, self.ndims) + return function.PlainBasis(hbasis_coeffs, hbasis_dofs, ndofs, self.transforms, self.ndims, function.SelectChain(self.roots)) class ProductTopology(Topology): 'product topology' @@ -1886,7 +1898,10 @@ def __init__(self, topo1:stricttopology, topo2:stricttopology): opposites = transformseq.ProductTransforms(self.topo1.opposites, self.topo2.opposites) else: opposites = transforms - super().__init__(references, transforms, opposites) + if len(self.topo1.roots) != 1 or len(self.topo2.roots) != 1 or self.topo1.roots == self.topo2.roots: + raise NotImplementedError + root = function.Root('{}*{}'.format(self.topo1.roots[0].id, self.topo2.roots[0].id), self.topo1.roots[0].ndims+self.topo2.roots[0].ndims) + super().__init__((root,), references, transforms, opposites) def __mul__(self, other): return ProductTopology(self.topo1, self.topo2 * other) @@ -1936,10 +1951,11 @@ class RevolutionTopology(Topology): def __init__(self): self._root = transform.Identifier(1, 'angle') - self.boundary = EmptyTopology(ndims=0) + roots = function.Root('angle', 1), + self.boundary = EmptyTopology(roots, ndims=0) transforms = transformseq.PlainTransforms([(self._root,)], 1) references = elementseq.asreferences([element.RevolutionReference()], 1) - super().__init__(references, transforms, transforms) + super().__init__(roots, references, transforms, transforms) @property def refined(self): @@ -2029,6 +2045,7 @@ def __init__(self, patches:types.tuple[types.strict[Patch]]): raise NotImplementedError('patch interfaces must have the same order of axes and the same orientation per axis') super().__init__( + patches[0].topo.roots, elementseq.chain([patch.topo.references for patch in self.patches], self.patches[0].topo.ndims), transformseq.chain([patch.topo.transforms for patch in self.patches], self.patches[0].topo.ndims), transformseq.chain([patch.topo.opposites for patch in self.patches], self.patches[0].topo.ndims)) @@ -2156,7 +2173,7 @@ def basis_spline(self, degree, patchcontinuous=True, knotvalues=None, knotmultip dofmap = tuple(types.frozenarray(tuple(renumber[merge.get(dof, dof)] for dof in v.flat), dtype=int).reshape(v.shape) for v in dofmap) dofcount = len(remainder) - return function.PlainBasis(coeffs, dofmap, dofcount, self.transforms, self.ndims) + return function.PlainBasis(coeffs, dofmap, dofcount, self.transforms, self.ndims, function.SelectChain(self.roots)) def basis_patch(self): 'degree zero patchwise discontinuous basis' @@ -2164,7 +2181,8 @@ def basis_patch(self): return function.DiscontBasis( [types.frozenarray(1, dtype=int).reshape(1, *(1,)*self.ndims)]*len(self.patches), transformseq.PlainTransforms(tuple((patch.topo.root,) for patch in self.patches), self.ndims), - self.ndims) + self.ndims, + function.SelectChain(self.roots)) @property def boundary(self): @@ -2180,7 +2198,7 @@ def boundary(self): subtopos.append(patch.topo.boundary[name]) subnames.append('patch{}-{}'.format(i, name)) if len(subtopos) == 0: - return EmptyTopology(self.ndims-1) + return EmptyTopology(self.roots, self.ndims-1) else: return DisjointUnionTopology(subtopos, subnames) @@ -2193,7 +2211,7 @@ def interfaces(self): patch via ``'intrapatch'``. ''' - intrapatchtopo = EmptyTopology(self.ndims-1) if not self.patches else \ + intrapatchtopo = EmptyTopology(self.roots, self.ndims-1) if not self.patches else \ DisjointUnionTopology(patch.topo.interfaces for patch in self.patches) btopos = [] @@ -2219,7 +2237,7 @@ def interfaces(self): transforms, opposites = pairs transforms = transformseq.PlainTransforms(transforms, self.ndims-1) opposites = transformseq.PlainTransforms(opposites, self.ndims-1) - btopos.append(Topology(references, transforms, opposites)) + btopos.append(Topology(self.roots, references, transforms, opposites)) bconnectivity.append(numpy.array(boundaryid).reshape((2,)*(self.ndims-1))) # create multipatch topology of interpatch boundaries interpatchtopo = MultipatchTopology(tuple(map(Patch, btopos, bconnectivity, self.build_boundarydata(bconnectivity)))) diff --git a/tests/test_basis.py b/tests/test_basis.py index 31dbb6ccc..7d115dc03 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -97,8 +97,8 @@ def setUp(self): self.domain, self.geom = mesh.rectilinear([2,3]) else: self.skipTest('in between bifurcate and tensorial') - domain1, geom1 = mesh.rectilinear([2]) - domain2, geom2 = mesh.rectilinear([3]) + domain1, geom1 = mesh.rectilinear([2], name='rect1') + domain2, geom2 = mesh.rectilinear([3], name='rect2') self.domain = domain1 * domain2 self.geom = function.concatenate(function.bifurcate(geom1, geom2), axis=0) @@ -250,17 +250,18 @@ def setUp(self): nverts = 25 elif self.variant == 'tensor': structured, geom = mesh.rectilinear([numpy.linspace(0, 1, 5-i) for i in range(self.ndims)]) - domain = topology.ConnectedTopology(structured.references, structured.transforms, structured.opposites, structured.connectivity) + domain = topology.ConnectedTopology(structured.roots, structured.references, structured.transforms, structured.opposites, structured.connectivity) nverts = numpy.product([5-i for i in range(self.ndims)]) elif self.variant == 'simplex': numpy.random.seed(0) nverts = 20 simplices = numeric.overlapping(numpy.arange(nverts), n=self.ndims+1) coords = numpy.random.normal(size=(nverts, self.ndims)) - root = transform.Identifier(self.ndims, 'test') - transforms = transformseq.PlainTransforms([(root, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], self.ndims) - domain = topology.SimplexTopology(simplices, transforms, transforms) - geom = function.rootcoords(self.ndims) + roottrans = transform.Identifier(self.ndims, 'test') + root = function.Root('X', self.ndims) + transforms = transformseq.PlainTransforms([(roottrans, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], self.ndims) + domain = topology.SimplexTopology(root, simplices, transforms, transforms) + geom = function.rootcoords(root) else: raise NotImplementedError self.domain = domain @@ -297,7 +298,7 @@ def test_pum_range(self): def test_poly(self): target = self.geom.sum(-1) if self.btype == 'bubble' \ - else (self.geom**self.degree).sum(-1) + function.TransformsIndexWithTail(self.domain.transforms, self.domain.ndims, function.TRANS).index if self.btype == 'discont' \ + else (self.geom**self.degree).sum(-1) + function.TransformsIndexWithTail(self.domain.transforms, self.domain.ndims, function.SelectChain(self.domain.roots)).index if self.btype == 'discont' \ else (self.geom**self.degree).sum(-1) projection = self.domain.projection(target, onto=self.basis, geometry=self.geom, ischeme='gauss', degree=2*self.degree, droptol=0) error2 = self.domain.integrate((target-projection)**2*function.J(self.geom), ischeme='gauss', degree=2*self.degree) diff --git a/tests/test_finitecell.py b/tests/test_finitecell.py index 6c81937d9..8dfe320ab 100644 --- a/tests/test_finitecell.py +++ b/tests/test_finitecell.py @@ -393,7 +393,7 @@ def test_transforms(self): self.assertEqual(set(self.topoB.boundary['trimmed'].transforms), set(self.topoA.boundary['trimmed'].opposites)) def test_opposites(self): - ielem = function.elemwise(self.topo.transforms, self.topo.ndims, numpy.arange(4)) + ielem = function.elemwise(self.topo.roots, self.topo.transforms, self.topo.ndims, numpy.arange(4)) sampleA = self.topoA.boundary['trimmed'].sample('uniform', 1) sampleB = self.topoB.boundary['trimmed'].sample('uniform', 1) self.assertEqual(set(sampleB.eval(ielem)), {0,1}) @@ -405,5 +405,5 @@ def test_baseboundaries(self): # the base implementation should create the correct boundary topology but # without interface opposites and without the trimmed group for topo in self.topoA, self.topoB: - alttopo = topology.ConnectedTopology(topo.references, topo.transforms, topo.opposites, topo.connectivity) + alttopo = topology.ConnectedTopology(topo.roots, topo.references, topo.transforms, topo.opposites, topo.connectivity) self.assertEqual(dict(zip(alttopo.boundary.transforms, alttopo.boundary.references)), dict(zip(topo.boundary.transforms, topo.boundary.references))) diff --git a/tests/test_function.py b/tests/test_function.py index 7e4ac270e..ca84dbcd2 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -19,7 +19,7 @@ def setUp(self): else: raise Exception('invalid ndim {!r}'.format(self.ndim)) numpy.random.seed(0) - self.args = [function.Guard(function.Polyval(numeric.dot(numpy.random.uniform(size=shape+poly.shape[:1], low=self.low, high=self.high), poly), function.rootcoords(self.ndim))) for shape in self.shapes] + self.args = [function.Guard(function.Polyval(numeric.dot(numpy.random.uniform(size=shape+poly.shape[:1], low=self.low, high=self.high), poly), function.rootcoords(domain.root))) for shape in self.shapes] if self.pass_geom: self.args += [self.geom] self.sample = domain.sample('uniform', 2) @@ -614,7 +614,7 @@ class elemwise(TestCase): def setUp(self): super().setUp() self.domain, geom = mesh.rectilinear([5]) - self.index = function.TransformsIndexWithTail(self.domain.transforms, self.domain.ndims, function.TRANS).index + self.index = function.TransformsIndexWithTail(self.domain.transforms, self.domain.ndims, function.SelectChain(self.domain.roots)).index self.data = tuple(map(types.frozenarray, ( numpy.arange(1, dtype=float).reshape(1,1), numpy.arange(2, dtype=float).reshape(1,2), @@ -1089,27 +1089,30 @@ def test_f_coefficients(self): class PlainBasis(CommonBasis, TestCase): def setUp(self): + root = function.Root('X', 0) transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) self.checkcoeffs = [[1],[2,3],[4,5],[6]] self.checkdofs = [[0],[2,3],[1,3],[2]] - self.basis = function.PlainBasis(self.checkcoeffs, self.checkdofs, 4, transforms, 0) + self.basis = function.PlainBasis(self.checkcoeffs, self.checkdofs, 4, transforms, 0, function.SelectChain((root,))) self.checkndofs = 4 super().setUp() class DiscontBasis(CommonBasis, TestCase): def setUp(self): + root = function.Root('X', 0) transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) self.checkcoeffs = [[1],[2,3],[4,5],[6]] - self.basis = function.DiscontBasis(self.checkcoeffs, transforms, 0) + self.basis = function.DiscontBasis(self.checkcoeffs, transforms, 0, function.SelectChain((root,))) self.checkdofs = [[0],[1,2],[3,4],[5]] self.checkndofs = 6 super().setUp() class MaskedBasis(CommonBasis, TestCase): def setUp(self): + root = function.Root('X', 0) transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) - parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transforms, 0) - self.basis = function.MaskedBasis(parent, [0,2]) + parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transforms, 0, function.SelectChain((root,))) + self.basis = function.MaskedBasis(parent, [0,2], function.SelectChain((root,))) self.checkcoeffs = [[1],[2],[],[6]] self.checkdofs = [[0],[1],[],[1]] self.checkndofs = 2 @@ -1117,9 +1120,10 @@ def setUp(self): class PrunedBasis(CommonBasis, TestCase): def setUp(self): + root = function.Root('X', 0) parent_transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) - parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, parent_transforms, 0) - self.basis = function.PrunedBasis(parent, [0,2]) + parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, parent_transforms, 0, function.SelectChain((root,))) + self.basis = function.PrunedBasis(parent, [0,2], function.SelectChain((root,))) self.checkcoeffs = [[1],[4,5]] self.checkdofs = [[0],[1,2]] self.checkndofs = 3 @@ -1127,8 +1131,9 @@ def setUp(self): class StructuredBasis1D(CommonBasis, TestCase): def setUp(self): + root = function.Root('X', 1) transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,4,False)], 0) - self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [5], transforms, [4]) + self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [5], transforms, [4], function.SelectChain((root,))) self.checkcoeffs = [[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]] self.checkdofs = [[0,1],[1,2],[2,3],[3,4]] self.checkndofs = 5 @@ -1136,8 +1141,9 @@ def setUp(self): class StructuredBasis1DPeriodic(CommonBasis, TestCase): def setUp(self): + root = function.Root('X', 1) transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,4,True)], 0) - self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [4], transforms, [4]) + self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [4], transforms, [4], function.SelectChain((root,))) self.checkcoeffs = [[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]] self.checkdofs = [[0,1],[1,2],[2,3],[3,0]] self.checkndofs = 4 @@ -1145,8 +1151,9 @@ def setUp(self): class StructuredBasis2D(CommonBasis, TestCase): def setUp(self): + root = function.Root('X', 2) transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,2,False),transformseq.DimAxis(0,2,False)], 0) - self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]]],[[[5],[6]],[[7],[8]]]], [[0,1],[0,1]], [[2,3],[2,3]], [3,3], transforms, [2,2]) + self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]]],[[[5],[6]],[[7],[8]]]], [[0,1],[0,1]], [[2,3],[2,3]], [3,3], transforms, [2,2], function.SelectChain((root,))) self.checkcoeffs = [[[[5]],[[6]],[[10]],[[12]]],[[[7]],[[8]],[[14]],[[16]]],[[[15]],[[18]],[[20]],[[24]]],[[[21]],[[24]],[[28]],[[32]]]] self.checkdofs = [[0,1,3,4],[1,2,4,5],[3,4,6,7],[4,5,7,8]] self.checkndofs = 9 diff --git a/tests/test_topology.py b/tests/test_topology.py index 31a75d102..faaf5de48 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -258,8 +258,9 @@ class refined(TestCase): def test_boundary_gradient(self): ref = _refined_refs[self.etype] trans = (transform.Identifier(ref.ndims, 'root'),) - domain = topology.ConnectedTopology(elementseq.asreferences([ref], ref.ndims), transformseq.PlainTransforms([trans], ref.ndims), transformseq.PlainTransforms([trans], ref.ndims), ((-1,)*ref.nedges,)).refine(self.ref0) - geom = function.rootcoords(ref.ndims) + root = function.Root('root', ref.ndims) + domain = topology.ConnectedTopology((root,), elementseq.asreferences([ref], ref.ndims), transformseq.PlainTransforms([trans], ref.ndims), transformseq.PlainTransforms([trans], ref.ndims), ((-1,)*ref.nedges,)).refine(self.ref0) + geom = function.rootcoords(root) basis = domain.basis('std', degree=1) u = domain.projection(geom.sum(), onto=basis, geometry=geom, degree=2) bpoints = domain.refine(self.ref1).boundary.refine(self.ref2).sample('uniform', 1) @@ -280,7 +281,7 @@ def setUp(self): super().setUp() self.domain, self.geom = mesh.rectilinear([3,4,5], periodic=[] if self.periodic is False else [self.periodic]) if not self.isstructured: - self.domain = topology.ConnectedTopology(self.domain.references, self.domain.transforms, self.domain.opposites, self.domain.connectivity) + self.domain = topology.ConnectedTopology(self.domain.roots, self.domain.references, self.domain.transforms, self.domain.opposites, self.domain.connectivity) def test_connectivity(self): nboundaries = 0 @@ -616,7 +617,7 @@ def test_refine_iter(self): common( 'Topology', - topo=topology.Topology(elementseq.asreferences([element.PointReference()], 0), transformseq.PlainTransforms([(transform.Identifier(0, 'test'),)], 0), transformseq.PlainTransforms([(transform.Identifier(0, 'test'),)], 0)), + topo=topology.Topology((function.Root('point', 0),), elementseq.asreferences([element.PointReference()], 0), transformseq.PlainTransforms([(transform.Identifier(0, 'test'),)], 0), transformseq.PlainTransforms([(transform.Identifier(0, 'test'),)], 0)), hasboundary=False) common( 'StructuredTopology:2D', From 46f9adfa0352810c88d7f6b420209e027bc156bf Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 16 Mar 2020 21:50:32 +0100 Subject: [PATCH 021/119] replace _transforms, _points with subsamples --- nutils/function.py | 88 +++++++++++++++++++++++++++--------------- nutils/sample.py | 19 ++++++--- nutils/topology.py | 23 ++++++----- tests/test_function.py | 42 ++++++++++++-------- tests/test_topology.py | 6 +-- 5 files changed, 109 insertions(+), 69 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index af32c88bb..5bcaf7ef8 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -226,13 +226,13 @@ def roots(self): @property def isconstant(self): - return EVALARGS not in self.dependencies + return SUBSAMPLES not in self.dependencies and EVALARGS not in self.dependencies @property def ordereddeps(self): '''collection of all function arguments such that the arguments to dependencies[i] can be found in dependencies[:i]''' - return tuple([EVALARGS] + sorted(self.dependencies - {EVALARGS}, key=lambda f: len(f.dependencies))) + return tuple([SUBSAMPLES, EVALARGS] + sorted(self.dependencies - {SUBSAMPLES, EVALARGS}, key=lambda f: len(f.dependencies))) @property def dependencytree(self): @@ -244,7 +244,7 @@ def dependencytree(self): @property def serialized(self): - return zip(self.ordereddeps[1:]+(self,), self.dependencytree[1:]) + return zip(self.ordereddeps[2:]+(self,), self.dependencytree[2:]) def asciitree(self, richoutput=False): 'string representation' @@ -276,8 +276,8 @@ def _asciitree_str(self): def __str__(self): return self.__class__.__name__ - def eval(self, **evalargs): - values = [evalargs] + def eval(self, *subsamples, **evalargs): + values = [subsamples, evalargs] for op, indices in self.serialized: try: args = [values[i] for i in indices] @@ -313,7 +313,7 @@ def graphviz(self, dotpath='dot', imgtype='png'): def stackstr(self, nlines=-1): 'print stack' - lines = [' %0 = EVALARGS'] + lines = [' %0 = SUBSAMPLES', ' %1 = EVALARGS'] for op, indices in self.serialized: args = ['%{}'.format(idx) for idx in indices] try: @@ -364,18 +364,15 @@ def __str__(self): return '\n{} --> {}: {}'.format(self.evaluable.stackstr(nlines=len(self.values)), self.etype.__name__, self.evalue) -EVALARGS = Evaluable(args=()) - -class Points(Evaluable): - __slots__ = () +class SUBSAMPLES(Evaluable): def __init__(self): - super().__init__(args=[EVALARGS]) - def evalf(self, evalargs): - points = evalargs['_points'] - assert numeric.isarray(points) and points.ndim == 2 - return types.frozenarray(points) + super().__init__(args=()) +SUBSAMPLES = SUBSAMPLES() -POINTS = Points() +class EVALARGS(Evaluable): + def __init__(self): + super().__init__(args=()) +EVALARGS = EVALARGS() class Tuple(Evaluable): @@ -460,15 +457,19 @@ class SelectChain(TransformChain): @types.apply_annotations def __init__(self, roots:types.tuple[strictroot], n:types.strictint=0): - if len(roots) != 1: - raise NotImplementedError self.n = n - super().__init__(roots, args=[EVALARGS], todims=builtins.sum(root.ndims for root in roots)) - - def evalf(self, evalargs): - trans = evalargs['_transforms'][self.n] - assert isinstance(trans, tuple) - return trans + super().__init__(roots, args=[SUBSAMPLES], todims=builtins.sum(root.ndims for root in roots)) + + def evalf(self, subsamples): + trans = [] + for root in self.ordered_roots: + for subsample in subsamples: + if root in subsample.roots: + trans.append(subsample.transforms[self.n if len(subsample.transforms) > 1 else 0][subsample.roots.index(root)]) + break + else: + raise ValueError('no such root: {!r}'.format(root)) + return tuple(trans) @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): @@ -501,9 +502,12 @@ def __init__(self, transforms:transformseq.stricttransforms, fromdims:types.stri def roots(self): return self._trans.roots - def evalf(self, trans): + def evalf(self, chains): + if len(chains) != 1: + raise NotImplementedError + trans, = chains index, tail = self._transforms.index_with_tail(trans) - return numpy.array(index)[None], tail + return numpy.array(index)[None], (tail,) def __len__(self): return 2 @@ -1182,16 +1186,35 @@ class ApplyTransforms(Array): __slots__ = 'trans' @types.apply_annotations - def __init__(self, trans:types.strict[TransformChain], points:strictevaluable=POINTS): + def __init__(self, trans:types.strict[TransformChain]): self.trans = trans - super().__init__(args=[points, trans], shape=[trans.todims], dtype=float) + super().__init__(args=[SUBSAMPLES, trans], shape=[trans.todims], dtype=float) @property def roots(self): return self.trans.roots - def evalf(self, points, chain): - return transform.apply(chain, points) + def evalf(self, subsamples, chains): + slices = {} + isubsamples = {} + for isubsample, subsample in enumerate(subsamples): + from0 = 0 + for root, chain in zip(subsample.roots, subsample.transforms[0]): + isubsamples[root] = isubsample + from1 = from0 + (chain[-1].fromdims if chain else root.ndims) + slices[root] = slice(from0, from1) + from0 = from1 + + result = numpy.zeros((*(subsample.npoints for subsample in subsamples), self.shape[0]), dtype=float) + to0 = 0 + for root, chain in zip(self.trans.ordered_roots, chains): + to1 = to0 + (chain[0].todims if chain else slices[root].stop - slices[root].start) + isubsample = isubsamples[root] + expand = tuple(slice(None) if i == isubsample else numpy.newaxis for i in range(len(subsamples))) + result[...,to0:to1] = transform.apply(chain, subsamples[isubsample].points.coords[:,slices[root]])[expand] + to0 = to1 + assert to0 == self.shape[0] + return result.reshape((-1, self.shape[0])) def _derivative(self, var, seen): if isinstance(var, LocalCoords) and len(var) > 0: @@ -1204,6 +1227,8 @@ class LinearFrom(Array): @types.apply_annotations def __init__(self, trans:types.strict[TransformChain], fromdims:types.strictint): + if len(trans.roots) != 1: + raise NotImplementedError self.trans = trans super().__init__(args=[trans], shape=(trans.todims, fromdims), dtype=float) @@ -1211,8 +1236,9 @@ def __init__(self, trans:types.strict[TransformChain], fromdims:types.strictint) def roots(self): return self.trans.roots - def evalf(self, chain): + def evalf(self, chains): todims, fromdims = self.shape + chain, = chains assert not chain or chain[0].todims == todims return transform.linearfrom(chain, fromdims)[_] diff --git a/nutils/sample.py b/nutils/sample.py index 4f01078f9..f4b5379c6 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -163,8 +163,8 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. offsets = numpy.zeros((len(blocks), self.nelems+1), dtype=int) if blocks: sizefunc = function.stack([f.size for ifunc, ind, f in blocks]).simplified - for ielem, transforms in enumerate(zip(*self.transforms)): - n, = sizefunc.eval(_transforms=transforms, **arguments) + for ielem in range(self.nelems): + n, = sizefunc.eval(*self.getsubsamples(ielem), **arguments) offsets[:,ielem+1] = offsets[:,ielem] + n # Since several blocks may belong to the same function, we post process the @@ -184,10 +184,14 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. valueindexfunc = function.Tuple(function.Tuple([value]+list(index)) for value, index in zip(values, indices)) with parallel.ctxrange('integrating', self.nelems) as ielems: for ielem in ielems: - points = self.points[ielem] - for iblock, (intdata, *indices) in enumerate(valueindexfunc.eval(_transforms=tuple(t[ielem] for t in self.transforms), _points=points.coords, **arguments)): + subsamples = self.getsubsamples(ielem) + weights = numpy.ones((1,)) + for subsample in reversed(subsamples): + weights = weights[...,numpy.newaxis] * subsample.points.weights + weights = weights.ravel() + for iblock, (intdata, *indices) in enumerate(valueindexfunc.eval(*subsamples, **arguments)): data = datas[block2func[iblock]][offsets[iblock,ielem]:offsets[iblock,ielem+1]].reshape(intdata.shape[1:]) - numpy.einsum('p,p...->...', points.weights, intdata, out=data['value']) + numpy.einsum('p,p...->...', weights, intdata, out=data['value']) for idim, ii in enumerate(indices): data['index']['i'+str(idim)] = ii.reshape([-1]+[1]*(data.ndim-1-idim)) @@ -227,7 +231,7 @@ def eval(self, funcs, arguments:argdict=...): with parallel.ctxrange('evaluating', self.nelems) as ielems: for ielem in ielems: - for ifunc, inds, data in idata.eval(_transforms=tuple(t[ielem] for t in self.transforms), _points=self.points[ielem].coords, **arguments): + for ifunc, inds, data in idata.eval(*self.getsubsamples(ielem), **arguments): numpy.add.at(retvals[ifunc], numpy.ix_(self.index[ielem], *[ind for (ind,) in inds]), data) return retvals @@ -318,6 +322,9 @@ def subset(self, mask): offset = numpy.cumsum([0] + [p.npoints for p in points]) return Sample(self.roots, self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) + def getsubsamples(self, ielem): + return function.Subsample(roots=self.roots, transforms=tuple(t[ielem] for t in self.transforms), points=self.points[ielem], ielem=ielem), + strictsample = types.strict[Sample] class Integral(types.Singleton): diff --git a/nutils/topology.py b/nutils/topology.py index 80b807e64..5b8644f92 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -293,9 +293,9 @@ def project(self, fun, onto, geometry, ischeme='gauss', degree=None, droptol=1e- I = numpy.zeros(onto.shape[0], dtype=bool) fun = function.asarray(fun).prepare_eval() data = function.Tuple(function.Tuple([fun, onto_f.simplified, function.Tuple(onto_ind)]) for onto_ind, onto_f in function.blocks(onto.prepare_eval())) - for ref, trans, opp in zip(self.references, self.transforms, self.opposites): - ipoints, iweights = ref.getischeme('bezier2') - for fun_, onto_f_, onto_ind_ in data.eval(_transforms=(trans, opp), _points=ipoints, **arguments or {}): + for ielem, (ref, trans, opp) in enumerate(zip(self.references, self.transforms, self.opposites)): + points = ref.getpoints('bezier', 2) + for fun_, onto_f_, onto_ind_ in data.eval(function.Subsample(roots=self.roots, transforms=(trans, opp), points=points, ielem=ielem), **arguments or {}): onto_f_ = onto_f_.swapaxes(0,1) # -> dof axis, point axis, ... indfun_ = fun_[(slice(None),)+numpy.ix_(*onto_ind_[1:])] assert onto_f_.shape[0] == len(onto_ind_[0]) @@ -347,10 +347,9 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None levelset = levelset.prepare_eval().simplified refs = [] if leveltopo is None: - with log.iter.percentage('trimming', self.references, self.transforms, self.opposites) as items: - for ref, trans, opp in items: - levels = levelset.eval(_transforms=(trans, opp), _points=ref.getpoints('vertex', maxrefine).coords, **arguments) - refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) + verts = self.sample('vertex', maxrefine) + levels = verts.eval(levelset) + refs = [ref.trim(levels[verts.index[ielem]], maxrefine=maxrefine, ndivisions=ndivisions) for ielem, ref in enumerate(self.references)] else: log.info('collecting leveltopo elements') bins = [set() for ielem in range(len(self))] @@ -359,15 +358,15 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None bins[ielem].add(tail) fcache = cache.WrapperCache() with log.iter.percentage('trimming', self.references, self.transforms, bins) as items: - for ref, trans, ctransforms in items: + for ielem, (ref, trans, ctransforms) in enumerate(items): levels = numpy.empty(ref.nvertices_by_level(maxrefine)) cover = list(fcache[ref.vertex_cover](frozenset(ctransforms), maxrefine)) # confirm cover and greedily optimize order mask = numpy.ones(len(levels), dtype=bool) while mask.any(): - imax = numpy.argmax([mask[indices].sum() for tail, points, indices in cover]) - tail, points, indices = cover.pop(imax) - levels[indices] = levelset.eval(_transforms=(trans + tail,), _points=points, **arguments) + imax = numpy.argmax([mask[indices].sum() for tail, cpoints, indices in cover]) + tail, cpoints, indices = cover.pop(imax) + levels[indices] = levelset.eval(function.Subsample(roots=self.roots, transforms=(trans + tail,), points=points.CoordsPoints(cpoints), ielem=ielem), **arguments) mask[indices] = False refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) log.debug('cache', fcache.stats) @@ -511,7 +510,7 @@ def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, ma w = p.weights xi = (numpy.dot(w,xi) / w.sum())[_] if len(xi) > 1 else xi.copy() for iiter in range(maxiter): - coord_xi, J_xi = geom_J.eval(_transforms=(self.transforms[ielem], self.opposites[ielem]), _points=xi, **arguments or {}) + coord_xi, J_xi = geom_J.eval(function.Subsample(roots=self.roots, transforms=(self.transforms[ielem], self.opposites[ielem]), points=points.CoordsPoints(xi), ielem=ielem), **arguments or {}) err = numpy.linalg.norm(coord - coord_xi) if err < tol: converged = True diff --git a/tests/test_function.py b/tests/test_function.py index ca84dbcd2..74f2d9f6b 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -1,5 +1,6 @@ import itertools, pickle, warnings as _builtin_warnings from nutils import * +from nutils.points import CoordsPoints from nutils.testing import * @@ -50,13 +51,13 @@ def assertArrayAlmostEqual(self, actual, desired, decimal): self.fail(''.join(lines)) def assertFunctionAlmostEqual(self, actual, desired, decimal): - evalargs = dict(_transforms=[trans[0] for trans in self.sample.transforms], _points=self.sample.points[0].coords) + subsample = function.Subsample(roots=self.sample.roots, transforms=[trans[0] for trans in self.sample.transforms], points=self.sample.points[0], ielem=0) with self.subTest('vanilla'): - self.assertArrayAlmostEqual(actual.eval(**evalargs), desired, decimal) + self.assertArrayAlmostEqual(actual.eval(subsample), desired, decimal) with self.subTest('simplified'): - self.assertArrayAlmostEqual(actual.simplified.eval(**evalargs), desired, decimal) + self.assertArrayAlmostEqual(actual.simplified.eval(subsample), desired, decimal) with self.subTest('optimized'): - self.assertArrayAlmostEqual(actual.simplified.optimized_for_numpy.eval(**evalargs), desired, decimal) + self.assertArrayAlmostEqual(actual.simplified.optimized_for_numpy.eval(subsample), desired, decimal) with self.subTest('sample'): self.assertArrayAlmostEqual(self.sample.eval(actual), desired, decimal) @@ -292,10 +293,10 @@ def find(self, target, xi0): target = target.reshape(-1, target.shape[-1]) xi = xi0.reshape(-1, xi0.shape[-1]) while countdown: - err = target - self.geom.prepare_eval().eval(_transforms=[elemtrans], _points=xi) + err = target - self.geom.prepare_eval().eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(xi), ielem=0)) if numpy.less(numpy.abs(err), 1e-12).all(): countdown -= 1 - dxi_root = (Jinv.eval(_transforms=[elemtrans], _points=xi) * err[...,_,:]).sum(-1) + dxi_root = (Jinv.eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(xi), ielem=0)) * err[...,_,:]).sum(-1) #xi = xi + numpy.dot(dxi_root, self.elem.inv_root_transform.T) xi = xi + dxi_root iiter += 1 @@ -313,7 +314,7 @@ def test_localgradient(self): eps = 1e-5 while not numpy.all(good): fdpoints = points[_,_,:,:] + D[:,:,_,:] * eps - tmp = self.n_op(*argsfun.eval(_transforms=[elemtrans], _points=fdpoints.reshape(-1,fdpoints.shape[-1]))) + tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) if len(tmp) == 1 or tmp.dtype.kind in 'bi' or self.zerograd: error = exact else: @@ -352,7 +353,7 @@ def test_gradient(self): eps = 1e-4 while not numpy.all(good): fdpoints = self.find(self.sample.eval(self.geom)[_,_,:,:] + D[:,:,_,:] * eps, points[_,_,:,:]) - tmp = self.n_op(*argsfun.eval(_transforms=[elemtrans], _points=fdpoints.reshape(-1,fdpoints.shape[-1]))) + tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) if len(tmp) == 1 or tmp.dtype.kind in 'bi' or self.zerograd: error = exact else: @@ -379,7 +380,7 @@ def test_doublegradient(self): eps = 1e-4 while not numpy.all(good): fdpoints = self.find(self.sample.eval(self.geom)[_,_,_,_,:,:] + DD[:,:,:,:,_,:] * eps, points[_,_,_,_,:,:]) - tmp = self.n_op(*argsfun.eval(_transforms=[elemtrans], _points=fdpoints.reshape(-1,fdpoints.shape[-1]))) + tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) if len(tmp) == 1 or tmp.dtype.kind in 'bi' or self.zerograd: error = exact else: @@ -625,14 +626,14 @@ def setUp(self): self.func = function.Elemwise(self.data, self.index, float) def test_evalf(self): - for i, trans in enumerate(self.domain.transforms): + for i, (ref, trans) in enumerate(zip(self.domain.references, self.domain.transforms)): with self.subTest(i=i): - numpy.testing.assert_array_almost_equal(self.func.prepare_eval().eval(_transforms=(trans,)), self.data[i][_]) + numpy.testing.assert_array_almost_equal(self.func.prepare_eval().eval(function.Subsample(roots=self.domain.roots, transforms=(trans,), points=ref.getpoints('gauss', 1)), ielem=i), self.data[i][_]) def test_shape(self): - for i, trans in enumerate(self.domain.transforms): + for i, (ref, trans) in enumerate(zip(self.domain.references, self.domain.transforms)): with self.subTest(i=i): - self.assertEqual(self.func.size.prepare_eval().eval(_transforms=(trans,))[0], self.data[i].size) + self.assertEqual(self.func.size.prepare_eval().eval(function.Subsample(roots=self.domain.roots, transforms=(trans,), points=ref.getpoints('gauss', 1), ielem=i))[0], self.data[i].size) def test_derivative(self): self.assertTrue(function.iszero(function.localgradient(self.func, self.domain.ndims))) @@ -1059,15 +1060,15 @@ def test_evalf(self): def test_simplified(self): ref = element.PointReference() if self.basis.ndimsdomain == 0 else element.LineReference()**self.basis.ndimsdomain - points = ref.getpoints('bezier', 4).coords + points = ref.getpoints('bezier', 4) simplified = self.basis.simplified with _builtin_warnings.catch_warnings(): _builtin_warnings.simplefilter('ignore', category=function.ExpensiveEvaluationWarning) for ielem in range(self.checknelems): - value = simplified.prepare_eval().eval(_transforms=(self.basis.transforms[ielem],), _points=points) + value = simplified.prepare_eval().eval(function.Subsample(roots=self.roots, transforms=(self.basis.transforms[ielem],), points=points, ielem=ielem)) if value.shape[0] == 1: - value = numpy.tile(value, (points.shape[0], 1)) - self.assertEqual(value.tolist(), self.checkeval(ielem, points)) + value = numpy.tile(value, (points.npoints, 1)) + self.assertEqual(value.tolist(), self.checkeval(ielem, points.coords)) def test_f_ndofs(self): for ielem in range(self.checknelems): @@ -1090,6 +1091,7 @@ def test_f_coefficients(self): class PlainBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) + self.roots = root, transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) self.checkcoeffs = [[1],[2,3],[4,5],[6]] self.checkdofs = [[0],[2,3],[1,3],[2]] @@ -1100,6 +1102,7 @@ def setUp(self): class DiscontBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) + self.roots = root, transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) self.checkcoeffs = [[1],[2,3],[4,5],[6]] self.basis = function.DiscontBasis(self.checkcoeffs, transforms, 0, function.SelectChain((root,))) @@ -1110,6 +1113,7 @@ def setUp(self): class MaskedBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) + self.roots = root, transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transforms, 0, function.SelectChain((root,))) self.basis = function.MaskedBasis(parent, [0,2], function.SelectChain((root,))) @@ -1121,6 +1125,7 @@ def setUp(self): class PrunedBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) + self.roots = root, parent_transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, parent_transforms, 0, function.SelectChain((root,))) self.basis = function.PrunedBasis(parent, [0,2], function.SelectChain((root,))) @@ -1132,6 +1137,7 @@ def setUp(self): class StructuredBasis1D(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 1) + self.roots = root, transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,4,False)], 0) self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [5], transforms, [4], function.SelectChain((root,))) self.checkcoeffs = [[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]] @@ -1142,6 +1148,7 @@ def setUp(self): class StructuredBasis1DPeriodic(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 1) + self.roots = root, transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,4,True)], 0) self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]]], [[0,1,2,3]], [[2,3,4,5]], [4], transforms, [4], function.SelectChain((root,))) self.checkcoeffs = [[[1],[2]],[[3],[4]],[[5],[6]],[[7],[8]]] @@ -1152,6 +1159,7 @@ def setUp(self): class StructuredBasis2D(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 2) + self.roots = root, transforms = transformseq.StructuredTransforms([transformseq.DimAxis(0,2,False),transformseq.DimAxis(0,2,False)], 0) self.basis = function.StructuredBasis([[[[1],[2]],[[3],[4]]],[[[5],[6]],[[7],[8]]]], [[0,1],[0,1]], [[2,3],[2,3]], [3,3], transforms, [2,2], function.SelectChain((root,))) self.checkcoeffs = [[[[5]],[[6]],[[10]],[[12]]],[[[7]],[[8]],[[14]],[[16]]],[[[15]],[[18]],[[20]],[[24]]],[[[21]],[[24]],[[28]],[[32]]]] diff --git a/tests/test_topology.py b/tests/test_topology.py index faaf5de48..c20cb4a19 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -29,9 +29,9 @@ def assertConnectivity(self, domain, geom): self.assertEqual(interfaces.opposites[index], opptrans) imask[index] += 1 self.assertEqual(eref, opperef) - points = eref.getpoints('gauss', 2).coords - a0 = geom.prepare_eval().eval(_transforms=[trans], _points=points) - a1 = geom.prepare_eval().eval(_transforms=[opptrans], _points=points) + points = eref.getpoints('gauss', 2) + a0 = geom.prepare_eval().eval(function.Subsample(roots=domain.roots, transforms=[trans], points=points)) + a1 = geom.prepare_eval().eval(function.Subsample(roots=domain.roots, transforms=[opptrans], points=points)) numpy.testing.assert_array_almost_equal(a0, a1) self.assertTrue(numpy.equal(bmask, 1).all()) self.assertTrue(numpy.equal(imask, 2).all()) From d0963ada842a3511565ca5811243c6cfccfac0d1 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 16 Mar 2020 22:29:46 +0100 Subject: [PATCH 022/119] replace ndims with subsamples in prepare_eval --- nutils/function.py | 3 ++- nutils/sample.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 5bcaf7ef8..bb1fe843d 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -2864,7 +2864,8 @@ def _derivative(self, var, seen): return DelayedJacobian(self._geom, *self._derivativestack, var) @util.positional_only - def prepare_eval(self, *, ndims, kwargs=...): + def prepare_eval(self, *, subsamples, kwargs=...): + ndims = builtins.sum(subsample.ndimsmanifold for subsample in subsamples) jac = functools.reduce(derivative, self._derivativestack, asarray(jacobian(self._geom, ndims))) return jac.prepare_eval(ndims=ndims, **kwargs) diff --git a/nutils/sample.py b/nutils/sample.py index f4b5379c6..03c947297 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -106,7 +106,7 @@ def __repr__(self): return '{}<{}D, {} elems, {} points>'.format(type(self).__qualname__, self.ndims, self.nelems, self.npoints) def _prepare_funcs(self, funcs): - return [function.asarray(func).prepare_eval(ndims=self.ndims) for func in funcs] + return [function.asarray(func).prepare_eval(subsamples=self.subsamplemetas) for func in funcs] @util.positional_only @util.single_or_multiple @@ -325,6 +325,10 @@ def subset(self, mask): def getsubsamples(self, ielem): return function.Subsample(roots=self.roots, transforms=tuple(t[ielem] for t in self.transforms), points=self.points[ielem], ielem=ielem), + @property + def subsamplemetas(self): + return function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims, transforms=self.transforms), + strictsample = types.strict[Sample] class Integral(types.Singleton): From fd53983488b85909f674925b78f12446ee6865a3 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 20 Jun 2019 11:36:03 +0200 Subject: [PATCH 023/119] extend TransformsIndexWithTail with head Currently derivatives and determinants are computed in the local coordinate system. In a future commit this will be changed to the root coordinate system. In preparation, we add `TransformsIndexWithTail.head` (the transform chain in the sequence that combined with the tail forms transform chain being sought). --- nutils/function.py | 34 +++++++++++++++++++++++++++------- nutils/sample.py | 2 +- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index bb1fe843d..0aa22834e 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -439,12 +439,13 @@ class TransformChain(Evaluable): Evaluates to a tuple of :class:`nutils.transform.TransformItem` objects. ''' - __slots__ = 'ordered_roots', 'todims' + __slots__ = 'ordered_roots', 'todims', 'fromdims' @types.apply_annotations - def __init__(self, roots:types.tuple[strictroot], args:types.tuple[strictevaluable], todims:types.strictint=None): + def __init__(self, roots:types.tuple[strictroot], args:types.tuple[strictevaluable], todims:types.strictint=None, fromdims:types.strictint=None): self.ordered_roots = roots self.todims = todims + self.fromdims = fromdims super().__init__(args) @property @@ -479,10 +480,10 @@ class TransformChainFromTuple(TransformChain): __slots__ = 'index' - def __init__(self, roots:types.tuple[strictroot], values:strictevaluable, index:types.strictint, todims:types.strictint=None): + def __init__(self, roots:types.tuple[strictroot], values:strictevaluable, index:types.strictint, todims:types.strictint=None, fromdims:types.strictint=None): assert 0 <= index < len(values) self.index = index - super().__init__(roots, args=[values], todims=todims) + super().__init__(roots, args=[values], todims=todims, fromdims=fromdims) def evalf(self, values): return values[self.index] @@ -510,20 +511,39 @@ def evalf(self, chains): return numpy.array(index)[None], (tail,) def __len__(self): - return 2 + return 3 @property def index(self): return ArrayFromTuple(self, index=0, shape=(), dtype=int) + @property + def head(self): + return GetTransform(self._trans.ordered_roots, self._transforms, self.index, self._fromdims) + @property def tail(self): return TransformChainFromTuple(self._trans.ordered_roots, self, index=1, todims=self._fromdims) def __iter__(self): yield self.index + yield self.head yield self.tail +class GetTransform(TransformChain): + + __slots__ = 'transforms', 'index' + + @types.apply_annotations + def __init__(self, roots:types.tuple[strictroot], transforms:transformseq.stricttransforms, index:asarray, fromdims:types.strictint): + assert index.ndim == 0 and index.dtype == int + self.transforms = transforms + super().__init__(args=[index], roots=roots, fromdims=fromdims, todims=builtins.sum(root.ndims for root in roots)) + + def evalf(self, index): + index, = index + return self.transforms[index][1:] + # ARRAYFUNC # # The main evaluable. Closely mimics a numpy array. @@ -3441,7 +3461,7 @@ def __init__(self, ndofs:types.strictint, transforms:transformseq.stricttransfor self.transforms = transforms self.ndimsdomain = ndims - self._index, tail = TransformsIndexWithTail(self.transforms, ndims, trans) + self._index, head, tail = TransformsIndexWithTail(self.transforms, ndims, trans) self._points = ApplyTransforms(tail) self._trans = trans super().__init__(args=(self._index, self._points), shape=(ndofs,), dtype=float) @@ -4386,7 +4406,7 @@ def eig(arg, axes=(-2,-1), symmetric=False): @types.apply_annotations def elemwise(roots:types.tuple[strictroot], transforms:transformseq.stricttransforms, ndims:types.strictint, values:types.tuple[types.frozenarray]): - index, tail = TransformsIndexWithTail(transforms, ndims, SelectChain(roots)) + index = TransformsIndexWithTail(transforms, ndims, SelectChain(roots)).index return Elemwise(values, index, dtype=float) def take(arg, index, axis): diff --git a/nutils/sample.py b/nutils/sample.py index 03c947297..21f097dba 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -247,7 +247,7 @@ def basis(self): '''Basis-like function that for every point in the sample evaluates to the unit vector corresponding to its index.''' - index, tail = function.TransformsIndexWithTail(self.transforms[0], self.ndims, function.SelectChain(self.roots)) + index, head, tail = function.TransformsIndexWithTail(self.transforms[0], self.ndims, function.SelectChain(self.roots)) I = function.Elemwise(self.index, index, dtype=int) B = function.Sampled(function.ApplyTransforms(tail), expect=function.take(self.allcoords, I, axis=0)) return function.Inflate(func=B, dofmap=I, length=self.npoints, axis=0) From e5092767f7b56ac555d9e310d867c32e0f6b60e3 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 20 Jun 2019 11:45:43 +0200 Subject: [PATCH 024/119] generalize derivatives of function.Argument In the tensorial-era we can take derivatives to (tensor products of) multiple roots. Currently the Argument records derivatives with respect to the localcoords only. This patch makes an Argument record not only the number of derivatives, but also the specific derivative targets. --- nutils/function.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 0aa22834e..104ca76f0 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -2807,15 +2807,19 @@ class Argument(DerivativeTargetBase): ``0``. ''' - __slots__ = '_name', '_nderiv' + __slots__ = '_name', '_derivs' __cache__ = 'prepare_eval' @types.apply_annotations - def __init__(self, name:types.strictstr, shape:asshape, nderiv:types.strictint=0): + def __init__(self, name:types.strictstr, shape:asshape, derivs:types.tuple[types.strict[DerivativeTargetBase]]=()): self._name = name - self._nderiv = nderiv + self._derivs = derivs super().__init__(args=[EVALARGS], shape=shape, dtype=float) + @property + def _nderiv(self): + return len(self._derivs) + def evalf(self, evalargs): assert self._nderiv == 0 try: @@ -2836,7 +2840,7 @@ def _derivative(self, var, seen): result = diagonalize(result, i, i+self.ndim) return result elif isinstance(var, LocalCoords): - return Argument(self._name, self.shape+var.shape, self._nderiv+1) + return Argument(self._name, self.shape+var.shape, self._derivs+(var,)) else: return zeros(self.shape+var.shape) @@ -4489,8 +4493,9 @@ def replace_arguments(value, arguments): if isinstance(value, Argument) and value._name in arguments: v = asarray(arguments[value._name]) assert value.shape[:value.ndim-value._nderiv] == v.shape - for ndims in value.shape[value.ndim-value._nderiv:]: - v = localgradient(v, ndims) + for target in value._derivs: + v = derivative(v, target) + assert v.shape == value.shape return v def _eval_ast(ast, functions): From c08174ce255e26d4fbfb265816a9d6812daf2116 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 24 Jun 2019 15:25:56 +0200 Subject: [PATCH 025/119] add transform.linear, function.Linear --- nutils/function.py | 12 ++++++++++++ nutils/transform.py | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index 104ca76f0..539070fa1 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -1262,6 +1262,18 @@ def evalf(self, chains): assert not chain or chain[0].todims == todims return transform.linearfrom(chain, fromdims)[_] +class Linear(Array): + + __slots__ = '_trans' + + @types.apply_annotations + def __init__(self, trans:types.strict[TransformChain], fromdims:types.strictint, todims:types.strictint=None): + self._trans = trans + super().__init__(args=[trans], shape=(todims or trans.todims, fromdims), dtype=float) + + def evalf(self, chain): + return transform.linear(chain, self.shape[0])[_] + class Inverse(Array): ''' Matrix inverse of ``func`` over the last two axes. All other axes are diff --git a/nutils/transform.py b/nutils/transform.py index 39d3813c8..52512801d 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -89,6 +89,12 @@ def linearfrom(chain, fromdims): return linear[:,:fromdims] if linear.shape[1] >= fromdims \ else numpy.concatenate([linear, numpy.zeros((todims, fromdims-linear.shape[1]))], axis=1) +def linear(chain, fromdims): + if len(chain) == 0: + return numpy.eye(fromdims) + else: + return functools.reduce(numpy.dot, (trans.linear for trans in chain)) + ## TRANSFORM ITEMS class TransformItem(types.Singleton): From 5f0e6d9ab5e3cbe0cc3c2fadb1bc620e7eb8b329 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 8 Jan 2020 21:47:23 +0100 Subject: [PATCH 026/119] add numeric.gramschmidt, function.GramSchmidt --- nutils/function.py | 19 +++++++++++++++++++ nutils/numeric.py | 8 ++++++++ 2 files changed, 27 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index 539070fa1..8935ce988 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -760,6 +760,25 @@ def _derivative(self, var, seen): return Zeros(self.shape + var.shape, dtype=self.dtype) raise NotImplementedError('derivative not defined for {}'.format(self.__class__.__name__)) +class GramSchmidt(Array): + + __slots__ = '_arg' + + @types.apply_annotations + def __init__(self, arg:asarray): + self._arg = arg + super().__init__(args=[arg], shape=arg.shape, dtype=float) + + def evalf(self, arg): + arg = arg.copy() + numeric.gramschmidt(arg) + return arg + + def _derivative(self, var, seen): + if not iszero(derivative(self._arg, var, seen)): + raise NotImplementedError + return zeros(self.shape + var.shape) + class Normal(Array): 'normal' diff --git a/nutils/numeric.py b/nutils/numeric.py index cfbb3f9d2..e9b9930d8 100644 --- a/nutils/numeric.py +++ b/nutils/numeric.py @@ -520,4 +520,12 @@ def asboolean(array, size, ordered=True): barray[array] = True return barray +def gramschmidt(V): + assert V.ndim >= 2 + assert V.shape[-2] >= V.shape[-1] + for i in range(V.shape[-1]): + if i > 0: + V[...,i] -= numpy.einsum('...ij,...j->...i', V[...,:i], numpy.einsum('...ji,...j->...i', V[...,:i], V[...,i])) + V[...,i] /= numpy.linalg.norm(V[...,i], axis=-1)[...,numpy.newaxis] + # vim:sw=2:sts=2:et From 82a3d5c2bfd5e15ac4dc12e683cf55c9b8614654 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 20 Jun 2019 12:11:15 +0200 Subject: [PATCH 027/119] add derivatives to rootcoords --- nutils/function.py | 144 +++++++++++++++++++++++++++++++++++++++++---- nutils/sample.py | 2 +- 2 files changed, 135 insertions(+), 11 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 8935ce988..dd4a9de78 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -476,6 +476,17 @@ def evalf(self, subsamples): def prepare_eval(self, *, opposite=False, kwargs=...): return SelectChain(self.ordered_roots, 1-self.n) if opposite else self +class EmptyTransformChain(TransformChain): + + __slots__ = () + + @types.apply_annotations + def __init__(self, roots:types.tuple[strictroot], todims:types.strictint=None, fromdims:types.strictint=None): + super().__init__(roots=roots, args=[], todims=todims, fromdims=fromdims) + + def evalf(self): + return () + class TransformChainFromTuple(TransformChain): __slots__ = 'index' @@ -760,6 +771,50 @@ def _derivative(self, var, seen): return Zeros(self.shape + var.shape, dtype=self.dtype) raise NotImplementedError('derivative not defined for {}'.format(self.__class__.__name__)) +class RootBasis(Array): + '''Orthonormal vectors spanning the tangent space and the complement. + + The first ``ndimstangent`` vectors span the tangent space of the manifold, + the remainders span the complement. + + Parameters + ---------- + roots : :class:`tuple` of :class:`Root` objects + The roots to compute the tangent and normal vectors for. + ndimstangent : int + The dimension of the tangent space. + trans : :class:`TransformChain` + ''' + + __slots__ = '_root', '_ndimstangent', '_opposite', '_trans' + + @types.apply_annotations + def __init__(self, roots:types.tuple[strictroot], ndimstangent:types.strictint, trans:types.strict[TransformChain], _opposite:bool=None): + # NOTE: `trans` is only required because of the `SelectChain` test in `Opposite.simplified`. + self._root, = roots + self._ndimstangent = ndimstangent + self._opposite = _opposite + self._trans = trans + super().__init__(args=[EVALARGS, trans], shape=[self._root.ndims, self._root.ndims], dtype=float) + + def evalf(self, evalargs, _trans): + trans = evalargs['_transforms'][1 if self._opposite else 0] + points = evalargs['_points'] + + ndims = self._root.ndims + assert trans == _trans + assert (trans[0].todims if trans else points.ndims) == ndims + if points.ndims != self._ndimstangent: + raise ValueError('expected a {}D tangent space, but got a {}D space'.format(self._ndimstangent, points.ndimstangent)) + + return transform.linearfrom(trans, ndims)[_] + + @util.positional_only + def prepare_eval(self, *, opposite=False, kwargs=...): + if self._opposite is not None: + raise ValueError('prepare already called') + return RootBasis((self._root,), self._ndimstangent, self._trans.prepare_eval(opposite=opposite, **kwargs), _opposite=opposite) + class GramSchmidt(Array): __slots__ = '_arg' @@ -1222,16 +1277,18 @@ def _takediag(self, axis, rmaxis): class ApplyTransforms(Array): - __slots__ = 'trans' + __slots__ = '_head', '_tail' @types.apply_annotations - def __init__(self, trans:types.strict[TransformChain]): - self.trans = trans - super().__init__(args=[SUBSAMPLES, trans], shape=[trans.todims], dtype=float) + def __init__(self, head:types.strict[TransformChain], tail:types.strict[TransformChain]): + assert head.roots == tail.roots + self._head = head + self._tail = tail + super().__init__(args=[SUBSAMPLES, tail], shape=[self._tail.todims], dtype=float) @property def roots(self): - return self.trans.roots + return self._tail.roots def evalf(self, subsamples, chains): slices = {} @@ -1246,7 +1303,7 @@ def evalf(self, subsamples, chains): result = numpy.zeros((*(subsample.npoints for subsample in subsamples), self.shape[0]), dtype=float) to0 = 0 - for root, chain in zip(self.trans.ordered_roots, chains): + for root, chain in zip(self._tail.ordered_roots, chains): to1 = to0 + (chain[0].todims if chain else slices[root].stop - slices[root].start) isubsample = isubsamples[root] expand = tuple(slice(None) if i == isubsample else numpy.newaxis for i in range(len(subsamples))) @@ -1257,7 +1314,17 @@ def evalf(self, subsamples, chains): def _derivative(self, var, seen): if isinstance(var, LocalCoords) and len(var) > 0: - return LinearFrom(self.trans, len(var)) + return LinearFrom(self._tail, len(var)) + elif isinstance(var, RootCoords) and var.root in self.roots: + if self._head.fromdims != builtins.sum(root.ndims for root in self.roots): + raise NotImplementedError('transform contains updims') + to0 = 0 + for root in self._head.ordered_roots: + to1 = to0 + root.ndims + if root == var.root: + return Inverse(Linear(self._head, self._head.fromdims))[:,to0:to1] + to0 = to1 + raise Exception return zeros(self.shape+var.shape) class LinearFrom(Array): @@ -2870,7 +2937,7 @@ def _derivative(self, var, seen): for i, sh in enumerate(self.shape): result = diagonalize(result, i, i+self.ndim) return result - elif isinstance(var, LocalCoords): + elif isinstance(var, (RootCoords, LocalCoords)): return Argument(self._name, self.shape+var.shape, self._derivs+(var,)) else: return zeros(self.shape+var.shape) @@ -2894,6 +2961,23 @@ def __init__(self, ndims:types.strictint): def evalf(self): raise Exception('LocalCoords should not be evaluated') +class RootCoords(DerivativeTargetBase): + 'root coords derivative target' + + __slots__ = 'root' + + @types.apply_annotations + def __init__(self, root:strictroot): + self.root = root + super().__init__(args=[], shape=[root.ndims], dtype=float) + + @property + def roots(self): + return frozenset((self.root,)) + + def evalf(self): + raise Exception('RootCoords should not be evaluated') + class DelayedJacobian(Array): ''' Placeholder for :func:`jacobian` until the dimension of the @@ -3497,7 +3581,7 @@ def __init__(self, ndofs:types.strictint, transforms:transformseq.stricttransfor self.ndimsdomain = ndims self._index, head, tail = TransformsIndexWithTail(self.transforms, ndims, trans) - self._points = ApplyTransforms(tail) + self._points = ApplyTransforms(head, tail) self._trans = trans super().__init__(args=(self._index, self._points), shape=(ndofs,), dtype=float) @@ -4222,7 +4306,7 @@ def blocks(arg): def rootcoords(roots): if isinstance(roots, Root): - return ApplyTransforms(SelectChain((roots,))) + return ApplyTransforms(EmptyTransformChain(roots=(roots,), todims=roots.ndims, fromdims=roots.ndims), SelectChain((roots,))) else: return concatenate([rootcoords(root) for root in roots], axis=0) @@ -4387,6 +4471,46 @@ def localgradient(arg, ndims): return derivative(arg, LocalCoords(ndims)) +def rootgradient(arg, roots): + return concatenate([derivative(arg, RootCoords(root)) for root in roots], axis=-1) + +def roottangent(roots, ndimstangent): + '''Returns the orthonormal vectors spanning the tangent space. + + Parameters + ---------- + roots : :class:`tuple` or :class:`Root` objects + The roots to compute the tangent vectors for. + ndimstangent : int + The dimension of the tangent space. + + Returns + ------- + :class:`Array` + The tangent vectors with shape `(sum(root.ndims for root in roots), ndims)`. + ''' + + return GramSchmidt(RootBasis(roots, ndimstangent, SelectChain(roots)))[:,:ndimstangent] + +def rootnormal(roots, ndimsnormal): + '''Returns the orthonormal vectors spanning the normal space. + + Parameters + ---------- + roots : :class:`tuple` or :class:`Root` objects + The roots to compute the normal vectors for. + ndimstangent : int + The dimension of the normal space. + + Returns + ------- + :class:`Array` + The tangent vectors with shape `(sum(root.ndims for root in roots), ndims)`. + ''' + + ndimstangent = builtins.sum(root.ndims for root in roots) - ndimsnormal + return GramSchmidt(RootBasis(roots, ndimstangent, SelectChain(roots)))[:,ndimstangent:] + def dotnorm(arg, coords): 'normal component' diff --git a/nutils/sample.py b/nutils/sample.py index 21f097dba..c6f16f890 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -249,7 +249,7 @@ def basis(self): index, head, tail = function.TransformsIndexWithTail(self.transforms[0], self.ndims, function.SelectChain(self.roots)) I = function.Elemwise(self.index, index, dtype=int) - B = function.Sampled(function.ApplyTransforms(tail), expect=function.take(self.allcoords, I, axis=0)) + B = function.Sampled(function.ApplyTransforms(head, tail), expect=function.take(self.allcoords, I, axis=0)) return function.Inflate(func=B, dofmap=I, length=self.npoints, axis=0) def asfunction(self, array): From b5512b0d21f059d2a0a1145c8469effb4396fff6 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 20 Jun 2019 12:16:37 +0200 Subject: [PATCH 028/119] compute grad(geom) using rootgradient --- nutils/function.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index dd4a9de78..c82c036a1 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4970,10 +4970,38 @@ def __rmatmul__(self, expr): def normal(arg, exterior=False): assert arg.ndim == 1 + # Order the roots deterministically. In the future we should use the order + # of `Sample.roots` (during `prepare_eval` or a successor). + roots = tuple(sorted(arg.roots, key=lambda root: (root.name, root.ndims))) if not exterior: - lgrad = localgradient(arg, len(arg)) - return Normal(lgrad) - lgrad = localgradient(arg, len(arg)-1) + assert len(arg) <= builtins.sum(root.ndims for root in roots) + ndimstangent = len(arg) - 1 + ndimsnormal = builtins.sum(root.ndims for root in roots) - ndimstangent + V = concatenate([roottangent(roots, ndimstangent), rootnormal(roots, ndimsnormal)[:,:1]], axis=1) + # TODO: Truncating the second axis of the rootnormal to one is a bit + # fishy, and should probably be disallowed when tensorial is fully + # implemented. Consider the following case: + # + # topo0, geom0 = mesh.rectilinear([1]*3) + # topo, geom = topo.boundary['top'], function.stack([geom[0], geom0[2]]) + # + # Now we compute the normal of `geom` on the boundaries of `topo`: + # + # topo.boundary['left'].sample('gauss', 1).eval(geom.normal()) + # topo.boundary['front'].sample('gauss', 1).eval(geom.normal()) + # + # Since the root of `topo` is 3D and `geom` only 2D, the `rootgradient` of + # `geom` has shape (2,3). The first element of the second axis + # spans the 1D tangent space of the manifold, the remaining elements span + # the normal space. The problem is: can we rely on the order of the + # normals. A trim boundary is now always the first element of the normals + # section. The remaining normals are ordered by the `Topology.boundary` + # operations: the first boundary, in the example `boundary['top']`, will + # have the normal in the last position. + lgrad = dot(rootgradient(arg, roots)[:,:,_], V[_,:,:], 1) + return Normal(lgrad[:,:len(arg)]) + raise NotImplementedError + lgrad = rootgradient(arg, roots) if len(arg) == 2: return asarray([lgrad[1,0], -lgrad[0,0]]).normalized() if len(arg) == 3: @@ -4984,7 +5012,10 @@ def grad(self, geom, ndims=0): assert geom.ndim == 1 if ndims <= 0: ndims += geom.shape[0] - J = localgradient(geom, ndims) + # Order the roots deterministically. In the future we should use the order + # of `Sample.roots` (during `prepare_eval` or a successor). + roots = tuple(sorted(geom.roots, key=lambda root: (root.name, root.ndims))) + J = rootgradient(geom, roots) if J.shape[0] == J.shape[1]: Jinv = inverse(J) elif J.shape[0] == J.shape[1] + 1: # gamma gradient @@ -4993,7 +5024,7 @@ def grad(self, geom, ndims=0): Jinv = dot(J[_,:,:], Ginv[:,_,:], -1) else: raise Exception('cannot invert {}x{} jacobian'.format(J.shape)) - return dot(localgradient(self, ndims)[...,_], Jinv, -2) + return dot(rootgradient(self, roots)[...,_], Jinv, -2) def dotnorm(arg, geom, axis=-1): axis = numeric.normdim(arg.ndim, axis) From d9761b3c74f98eb7a88c2ddf817c9a98fe8ad293 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 24 Jun 2019 11:43:03 +0200 Subject: [PATCH 029/119] compute jacobian using rootgradient --- nutils/function.py | 31 +++++++++++++++++++------------ nutils/sample.py | 7 +++++++ 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index c82c036a1..e65b2b442 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4405,19 +4405,27 @@ def get(arg, iax, item): def jacobian(geom, ndims): ''' Return :math:`\\sqrt{|J^T J|}` with :math:`J` the gradient of ``geom`` to the - local coordinate system with ``ndims`` dimensions (``localgradient(geom, - ndims)``). + root coordinate system with ``ndims`` dimensions. ''' + # Compute the jacobian `abs(det(J.T @ V.T @ V @ J))**0.5` with `J` the + # gradient of `geom` to the root coordinates and `V` the `ndims` vectors + # spanning the tangent space. assert geom.ndim == 1 - J = localgradient(geom, ndims) - cndims, = geom.shape - assert J.shape == (cndims,ndims), 'wrong jacobian shape: got {}, expected {}'.format(J.shape, (cndims, ndims)) - assert cndims >= ndims, 'geometry dimension < topology dimension' - detJ = abs(determinant(J)) if cndims == ndims \ - else 1. if ndims == 0 \ - else abs(determinant((J[:,:,_] * J[:,_,:]).sum(0)))**.5 - return detJ + if ndims == 0: + return 1. + # Order the roots deterministically. In the future we should use the order + # of `Sample.roots` (during `prepare_eval` or a successor). + roots = tuple(sorted(geom.roots, key=lambda root: (root.name, root.ndims))) + J = rootgradient(geom, roots) + if J.shape == (ndims, ndims): + # Since `J` and `V` are square, the determinant `det(J @ V)` is equal to + # `det(J) * det(V)`. The root vectors `V` are orthonormal, hence the + # determinant simplifies to `det(J)`. + return abs(determinant(J)) + else: + J = dot(J[:,:,_], roottangent(roots, ndims)[_], 1) + return abs(determinant((J[:,:,_] * J[:,_,:]).sum(0)))**.5 def matmat(arg0, *args): 'helper function, contracts last axis of arg0 with first axis of arg1, etc' @@ -4604,8 +4612,7 @@ def mask(arg, mask, axis=0): def J(geometry, ndims=None): ''' Return :math:`\\sqrt{|J^T J|}` with :math:`J` the gradient of ``geometry`` to - the local coordinate system with ``ndims`` dimensions (``localgradient(geom, - ndims)``). + the root coordinate system with ``ndims`` dimensions. ''' if ndims is None: return DelayedJacobian(geometry) diff --git a/nutils/sample.py b/nutils/sample.py index c6f16f890..d0eececaf 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -142,6 +142,13 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. if arguments is None: arguments = {} + J = function.RootBasis(self.roots, self.ndims, function.SelectChain(self.roots))[:,:self.ndims] + if J.shape[0] == J.shape[1]: + detJ = abs(function.determinant(J)) + else: + detJ = abs(function.determinant((J[:,:,None] * J[:,None,:]).sum(0)))**.5 + funcs = [func * detJ for func in funcs] + # Functions may consist of several blocks, such as originating from # chaining. Here we make a list of all blocks consisting of triplets of # argument id, evaluable index, and evaluable values. From 412aae8d9ef1e06ad45810c68fa2c20bcc37a43a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 24 Jun 2019 15:26:18 +0200 Subject: [PATCH 030/119] compute topo.locate with rootgradient --- nutils/topology.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nutils/topology.py b/nutils/topology.py index 5b8644f92..1d22a6c94 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -496,7 +496,7 @@ def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, ma vref = element.getsimplex(0) ielems = parallel.shempty(len(coords), dtype=int) xis = parallel.shempty((len(coords),len(geom)), dtype=float) - J = function.localgradient(geom, self.ndims) + J = function.dot(function.rootgradient(geom, self.roots)[:,:,_], function.RootBasis(self.roots, self.ndims, function.SelectChain(self.roots))[_,:,:self.ndims], 1) geom_J = function.Tuple((geom, J)).prepare_eval().simplified with parallel.ctxrange('locating', len(coords)) as ipoints: for ipoint in ipoints: From df91c2380bf4d181ef5387f0f94e087d57e05f71 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 24 Jun 2019 14:16:26 +0200 Subject: [PATCH 031/119] drop localgradient, LocalCoords --- nutils/function.py | 44 ++---------------------------------------- tests/test_function.py | 15 +++++++------- 2 files changed, 10 insertions(+), 49 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index e65b2b442..35a6be9bd 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -1313,9 +1313,7 @@ def evalf(self, subsamples, chains): return result.reshape((-1, self.shape[0])) def _derivative(self, var, seen): - if isinstance(var, LocalCoords) and len(var) > 0: - return LinearFrom(self._tail, len(var)) - elif isinstance(var, RootCoords) and var.root in self.roots: + if isinstance(var, RootCoords) and var.root in self.roots: if self._head.fromdims != builtins.sum(root.ndims for root in self.roots): raise NotImplementedError('transform contains updims') to0 = 0 @@ -1327,27 +1325,6 @@ def _derivative(self, var, seen): raise Exception return zeros(self.shape+var.shape) -class LinearFrom(Array): - - __slots__ = 'trans' - - @types.apply_annotations - def __init__(self, trans:types.strict[TransformChain], fromdims:types.strictint): - if len(trans.roots) != 1: - raise NotImplementedError - self.trans = trans - super().__init__(args=[trans], shape=(trans.todims, fromdims), dtype=float) - - @property - def roots(self): - return self.trans.roots - - def evalf(self, chains): - todims, fromdims = self.shape - chain, = chains - assert not chain or chain[0].todims == todims - return transform.linearfrom(chain, fromdims)[_] - class Linear(Array): __slots__ = '_trans' @@ -2937,7 +2914,7 @@ def _derivative(self, var, seen): for i, sh in enumerate(self.shape): result = diagonalize(result, i, i+self.ndim) return result - elif isinstance(var, (RootCoords, LocalCoords)): + elif isinstance(var, RootCoords): return Argument(self._name, self.shape+var.shape, self._derivs+(var,)) else: return zeros(self.shape+var.shape) @@ -2949,18 +2926,6 @@ def __str__(self): def prepare_eval(self, kwargs=...): return zeros_like(self) if self._nderiv > 0 else self -class LocalCoords(DerivativeTargetBase): - 'local coords derivative target' - - __slots__ = () - - @types.apply_annotations - def __init__(self, ndims:types.strictint): - super().__init__(args=[], shape=[ndims], dtype=float) - - def evalf(self): - raise Exception('LocalCoords should not be evaluated') - class RootCoords(DerivativeTargetBase): 'root coords derivative target' @@ -4474,11 +4439,6 @@ def derivative(func, var, seen=None): assert result.shape == func.shape+var.shape, 'bug in {}._derivative'.format(func) return result -def localgradient(arg, ndims): - 'local derivative' - - return derivative(arg, LocalCoords(ndims)) - def rootgradient(arg, roots): return concatenate([derivative(arg, RootCoords(root)) for root in roots], axis=-1) diff --git a/tests/test_function.py b/tests/test_function.py index 74f2d9f6b..e295de528 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -281,7 +281,7 @@ def test_opposite(self): def find(self, target, xi0): elemtrans, = self.sample.transforms[0] ndim, = self.geom.shape - J = function.localgradient(self.geom, ndim) + J = function.dot(function.rootgradient(self.geom, self.sample.roots)[:,:,_], transform.linear(elemtrans[1:], elemtrans[-1].fromdims)[_,:,:], 1) Jinv = function.inverse(J).prepare_eval() countdown = 5 iiter = 0 @@ -304,12 +304,13 @@ def find(self, target, xi0): return xi.reshape(xi0.shape) @parametrize.enable_if(lambda hasgrad, **kwargs: hasgrad) - def test_localgradient(self): + def test_rootgradient(self): elemtrans, = self.sample.transforms[0] points = self.sample.points[0].coords argsfun = function.Tuple(self.args).prepare_eval() - exact = self.sample.eval(function.localgradient(self.op_args, ndims=self.ndim)) + exact = self.sample.eval(function.rootgradient(self.op_args, self.sample.roots)) D = numpy.array([-.5,.5])[:,_,_] * numpy.eye(self.ndim) + invlinear = numpy.linalg.inv(transform.linear(elemtrans[1:], elemtrans[-1].fromdims)) good = False eps = 1e-5 while not numpy.all(good): @@ -319,12 +320,12 @@ def test_localgradient(self): error = exact else: fdgrad, = numpy.diff(tmp.reshape(fdpoints.shape[:-1] + tmp.shape[1:]), axis=0) / eps - error = exact - fdgrad.transpose(numpy.roll(numpy.arange(fdgrad.ndim),-1)) + error = exact - fdgrad.transpose(numpy.roll(numpy.arange(fdgrad.ndim),-1)) @ invlinear good |= numpy.less(abs(error / exact), 1e-8) good |= numpy.less(abs(error), 1e-14) eps *= .8 if eps < 1e-10: - self.fail('local gradient failed to reach tolerance ({}/{})'.format((~good).sum(), good.size)) + self.fail('root gradient failed to reach tolerance ({}/{})'.format((~good).sum(), good.size)) @parametrize.enable_if(lambda hasgrad, **kwargs: hasgrad) def test_jacobian(self): @@ -636,10 +637,10 @@ def test_shape(self): self.assertEqual(self.func.size.prepare_eval().eval(function.Subsample(roots=self.domain.roots, transforms=(trans,), points=ref.getpoints('gauss', 1), ielem=i))[0], self.data[i].size) def test_derivative(self): - self.assertTrue(function.iszero(function.localgradient(self.func, self.domain.ndims))) + self.assertTrue(function.iszero(function.rootgradient(self.func, self.domain.roots))) def test_shape_derivative(self): - self.assertEqual(function.localgradient(self.func, self.domain.ndims).shape, self.func.shape+(self.domain.ndims,)) + self.assertEqual(function.rootgradient(self.func, self.domain.roots).shape, self.func.shape+(self.domain.ndims,)) class namespace(TestCase): From 78f0240a81d470ac3911dbdc762744358a1f7381 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Fri, 14 Feb 2020 00:01:43 +0100 Subject: [PATCH 032/119] replace Transforms.fromdims with todims --- nutils/mesh.py | 16 ++--- nutils/topology.py | 32 +++++---- nutils/transformseq.py | 139 +++++++++++++++++++------------------ tests/test_basis.py | 2 +- tests/test_function.py | 8 +-- tests/test_topology.py | 4 +- tests/test_transformseq.py | 105 ++++++++++++++-------------- 7 files changed, 157 insertions(+), 149 deletions(-) diff --git a/nutils/mesh.py b/nutils/mesh.py index c0c814827..216a0dc25 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -564,8 +564,8 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): opposites.append(topo.transforms[ioppelem] + (transform.SimplexEdge(ndims, tuple(connectivity[ioppelem]).index(ielem)),)) for groups, (simplices, transforms, opposites) in (bgroups, bitems), (igroups, iitems): if simplices: - transforms = transformseq.PlainTransforms(transforms, ndims-1) - opposites = transforms if opposites is None else transformseq.PlainTransforms(opposites, ndims-1) + transforms = transformseq.PlainTransforms(transforms, ndims, ndims-1) + opposites = transforms if opposites is None else transformseq.PlainTransforms(opposites, ndims, ndims-1) groups[name] = topology.SimplexTopology(root, simplices, transforms, opposites) pgroups = {} @@ -573,7 +573,7 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): ptrans = [transform.Matrix(linear=numpy.zeros(shape=(ndims,0)), offset=offset) for offset in numpy.eye(ndims+1)[:,1:]] pmap = {inode: numpy.array(numpy.equal(nodes, inode).nonzero()).T for inode in set.union(*map(set, ptags.values()))} for pname, inodes in ptags.items(): - ptransforms = transformseq.PlainTransforms([topo.transforms[ielem] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode]], 0) + ptransforms = transformseq.PlainTransforms([topo.transforms[ielem] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode]], ndims, 0) preferences = elementseq.asreferences([element.getsimplex(0)], 0)*len(ptransforms) pgroups[pname] = topology.Topology((root,), preferences, ptransforms, ptransforms) @@ -612,12 +612,12 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): opposites.append(topo.transforms[ioppelem] + (transform.SimplexEdge(ndims, ioppedge),)) for groups, (simplices, transforms, opposites) in (vbgroups, bitems), (vigroups, iitems): if simplices: - transforms = transformseq.PlainTransforms(transforms, ndims-1) - opposites = transformseq.PlainTransforms(opposites, ndims-1) if len(opposites) == len(transforms) else transforms + transforms = transformseq.PlainTransforms(transforms, ndims, ndims-1) + opposites = transformseq.PlainTransforms(opposites, ndims, ndims-1) if len(opposites) == len(transforms) else transforms groups[bname] = topology.SimplexTopology(root, simplices, transforms, opposites) vpgroups = {} for pname, inodes in ptags.items(): - ptransforms = transformseq.PlainTransforms([topo.transforms[ielem] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode] if keep[ielem]], 0) + ptransforms = transformseq.PlainTransforms([topo.transforms[ielem] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode] if keep[ielem]], ndims, 0) preferences = elementseq.asreferences([element.getsimplex(0)], 0)*len(ptransforms) vpgroups[pname] = topology.Topology((root,), preferences, ptransforms, ptransforms) vgroups[name] = vtopo.withgroups(bgroups=vbgroups, igroups=vigroups, pgroups=vpgroups) @@ -672,7 +672,7 @@ def unitsquare(nelems, etype): v = numpy.arange(nelems+1, dtype=float) coords = numeric.meshgrid(v, v).reshape(2,-1).T - transforms = transformseq.PlainTransforms([(idtrans, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], 2) + transforms = transformseq.PlainTransforms([(idtrans, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], 2, 2) topo = topology.SimplexTopology(root, simplices, transforms, transforms) if etype == 'mixed': @@ -687,7 +687,7 @@ def unitsquare(nelems, etype): transforms[n*2:(n+1)*2] = (idtrans, transform.Shift([float(i),float(j)])), connectivity[n*2:(n+1)*2] = numpy.concatenate(connectivity[n*2:(n+1)*2])[[3,2,4,1] if i%2==j%2 else [3,2,0,5]], connectivity = [c-numpy.greater(c,n*2) for c in connectivity] - topo = topology.ConnectedTopology((root,), elementseq.asreferences(references, 2), transformseq.PlainTransforms(transforms, 2),transformseq.PlainTransforms(transforms, 2), tuple(types.frozenarray(c, copy=False) for c in connectivity)) + topo = topology.ConnectedTopology((root,), elementseq.asreferences(references, 2), transformseq.PlainTransforms(transforms, 2, 2),transformseq.PlainTransforms(transforms, 2, 2), tuple(types.frozenarray(c, copy=False) for c in connectivity)) x, y = topo.boundary.elem_mean(function.rootcoords(root), degree=1).T bgroups = dict(left=x==0, right=x==nelems, bottom=y==0, top=y==nelems) diff --git a/nutils/topology.py b/nutils/topology.py index 1d22a6c94..65d473cee 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -108,9 +108,10 @@ def __and__(self, other): # The last condition is to avoid duplicate elements. Note that we could # have reused the result of an earlier lookup to avoid a new (using index # instead of contains) but we choose to trade some speed for simplicity. + root, = self.roots references = elementseq.chain([self.references[ind_self], other.references[ind_other]], self.ndims) - transforms = transformseq.chain([self.transforms[ind_self], other.transforms[ind_other]], self.ndims) - opposites = transformseq.chain([self.opposites[ind_self], other.opposites[ind_other]], self.ndims) + transforms = transformseq.chain([self.transforms[ind_self], other.transforms[ind_other]], root.ndims) + opposites = transformseq.chain([self.opposites[ind_self], other.opposites[ind_other]], root.ndims) return Topology(self.roots, references, transforms, opposites) __rand__ = lambda self, other: self.__and__(other) @@ -795,7 +796,8 @@ class EmptyTopology(Topology): @types.apply_annotations def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint): - super().__init__(roots, elementseq.EmptyReferences(ndims), transformseq.EmptyTransforms(ndims), transformseq.EmptyTransforms(ndims)) + root, = roots + super().__init__(roots, elementseq.EmptyReferences(ndims), transformseq.EmptyTransforms(root.ndims), transformseq.EmptyTransforms(root.ndims)) def __or__(self, other): assert self.ndims == other.ndims @@ -817,9 +819,10 @@ def _preprocess_init(self, roots:types.tuple[function.strictroot], trans:transfo @_preprocess_init def __init__(self, roots, trans, opposite): assert trans[-1].fromdims == 0 + root, = roots references = elementseq.asreferences([element.getsimplex(0)], 0) - transforms = transformseq.PlainTransforms((trans,), 0) - opposites = transforms if opposite is None else transformseq.PlainTransforms((opposite,), 0) + transforms = transformseq.PlainTransforms((trans,), root.ndims, 0) + opposites = transforms if opposite is None else transformseq.PlainTransforms((opposite,), root.ndims, 0) super().__init__(roots, references, transforms, opposites) def StructuredLine(root:function.strictroot, i:types.strictint, j:types.strictint, periodic:bool=False, bnames:types.tuple[types.strictstr]=None): @@ -1377,11 +1380,12 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st assert len(set(self._topos[itopo].opposites[itrans] for itopo, itrans in indices)) == 1 selections = tuple(map(types.frozenarray[int], selections)) + root, = roots super().__init__( roots, elementseq.asreferences(references, ndims), - transformseq.chain((topo.transforms[selection] for topo, selection in zip(topos, selections)), ndims), - transformseq.chain((topo.opposites[selection] for topo, selection in zip(topos, selections)), ndims)) + transformseq.chain((topo.transforms[selection] for topo, selection in zip(topos, selections)), root.ndims), + transformseq.chain((topo.opposites[selection] for topo, selection in zip(topos, selections)), root.ndims)) def getitem(self, item): topos = [topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)] @@ -1409,11 +1413,12 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st roots = self._topos[0].roots ndims = self._topos[0].ndims assert all(topo.roots == roots and topo.ndims == ndims for topo in self._topos) + root, = roots super().__init__( roots, elementseq.chain((topo.references for topo in self._topos), ndims), - transformseq.chain((topo.transforms for topo in self._topos), ndims), - transformseq.chain((topo.opposites for topo in self._topos), ndims)) + transformseq.chain((topo.transforms for topo in self._topos), root.ndims), + transformseq.chain((topo.opposites for topo in self._topos), root.ndims)) def getitem(self, item): topos = [topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)] @@ -1529,7 +1534,8 @@ def boundary(self): trimmedbrefs[self.newboundary.transforms.index(trans)] = ref trimboundary = SubsetTopology(self.newboundary, trimmedbrefs) else: - trimboundary = Topology(self.roots, elementseq.asreferences(trimmedreferences, self.ndims-1), transformseq.PlainTransforms(trimmedtransforms, self.ndims-1), transformseq.PlainTransforms(trimmedopposites, self.ndims-1)) + root, = self.roots + trimboundary = Topology(self.roots, elementseq.asreferences(trimmedreferences, self.ndims-1), transformseq.PlainTransforms(trimmedtransforms, root.ndims, self.ndims-1), transformseq.PlainTransforms(trimmedopposites, root.ndims, self.ndims-1)) return DisjointUnionTopology([trimboundary, origboundary], names=[self.newboundary] if isinstance(self.newboundary,str) else []) @property @@ -1630,7 +1636,8 @@ def __init__(self, basetopo:stricttopology, indices_per_level:types.tuple[types. opposites.append(level.opposites[indices]) self.levels = tuple(levels) - super().__init__(basetopo.roots, elementseq.chain(references, basetopo.ndims), transformseq.chain(transforms, basetopo.ndims), transformseq.chain(opposites, basetopo.ndims)) + root, = basetopo.roots + super().__init__(basetopo.roots, elementseq.chain(references, basetopo.ndims), transformseq.chain(transforms, root.ndims), transformseq.chain(opposites, root.ndims)) def getitem(self, item): itemtopo = self.basetopo.getitem(item) @@ -1730,7 +1737,8 @@ def interfaces(self): hreferences.append(level.interfaces.references[selection]) htransforms.append(level.interfaces.transforms[selection]) hopposites.append(level.interfaces.opposites[selection]) - return Topology(self.roots, elementseq.chain(hreferences, self.ndims-1), transformseq.chain(htransforms, self.ndims-1), transformseq.chain(hopposites, self.ndims-1)) + root, = self.roots + return Topology(self.roots, elementseq.chain(hreferences, self.ndims-1), transformseq.chain(htransforms, root.ndims), transformseq.chain(hopposites, root.ndims)) @log.withcontext def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 6294e390c..6f6fa1de3 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -37,13 +37,13 @@ class supports indexing, iterating and has an :meth:`index` method. In Parameters ---------- - fromdims : :class:`int` - The number of dimensions all transforms in this sequence map from. + todims : :class:`int` + The dimension all transforms in this sequence map to. Attributes ---------- - fromdims : :class:`int` - The number of dimensions all transforms in this sequence map from. + todims : :class:`int` + The dimension all transforms in this sequence map to. Notes ----- @@ -51,11 +51,11 @@ class supports indexing, iterating and has an :meth:`index` method. In :meth:`index_with_tail`. ''' - __slots__ = 'fromdims' + __slots__ = 'todims' @types.apply_annotations - def __init__(self, fromdims:types.strictint): - self.fromdims = fromdims + def __init__(self, todims:types.strictint): + self.todims = todims super().__init__() @abc.abstractmethod @@ -90,7 +90,7 @@ def __getitem__(self, index): s = numpy.argsort(index) return ReorderedTransforms(self[index[s]], numpy.argsort(s)) if len(index) == 0: - return EmptyTransforms(self.fromdims) + return EmptyTransforms(self.todims) if len(index) == len(self): return self return MaskedTransforms(self, index) @@ -98,7 +98,7 @@ def __getitem__(self, index): if index.shape != (len(self),): raise IndexError('mask has invalid shape') if not numpy.any(index): - return EmptyTransforms(self.fromdims) + return EmptyTransforms(self.todims) if numpy.all(index): return self index, = numpy.where(index) @@ -139,7 +139,7 @@ def index_with_tail(self, trans): Consider the following plain sequence of two shift transforms: >>> from nutils.transform import Shift, Scale - >>> transforms = PlainTransforms([(Shift([0.]),), (Shift([1.]),)], fromdims=1) + >>> transforms = PlainTransforms([(Shift([0.]),), (Shift([1.]),)], 1, 1) Calling :meth:`index_with_tail` with the first transform gives index ``0`` and no tail: @@ -184,7 +184,7 @@ def index(self, trans): Consider the following plain sequence of two shift transforms: >>> from nutils.transform import Shift, Scale - >>> transforms = PlainTransforms([(Shift([0.]),), (Shift([1.]),)], fromdims=1) + >>> transforms = PlainTransforms([(Shift([0.]),), (Shift([1.]),)], 1, 1) Calling :meth:`index` with the first transform gives index ``0``: @@ -268,9 +268,9 @@ def refined(self, references): ''' if references.isuniform: - return UniformDerivedTransforms(self, references[0], 'child_transforms', self.fromdims) + return UniformDerivedTransforms(self, references[0], 'child_transforms', False) else: - return DerivedTransforms(self, references, 'child_transforms', self.fromdims) + return DerivedTransforms(self, references, 'child_transforms', False) def edges(self, references): '''Return the sequence of edge transforms given ``references``. @@ -289,16 +289,18 @@ def edges(self, references): ''' if references.isuniform: - return UniformDerivedTransforms(self, references[0], 'edge_transforms', self.fromdims-1) + return UniformDerivedTransforms(self, references[0], 'edge_transforms', True) else: - return DerivedTransforms(self, references, 'edge_transforms', self.fromdims-1) + return DerivedTransforms(self, references, 'edge_transforms', True) def __add__(self, other): '''Return ``self+other``.''' - if not isinstance(other, Transforms) or self.fromdims != other.fromdims: + if not isinstance(other, Transforms): return NotImplemented - return chain((self, other), self.fromdims) + if self.todims != other.todims: + raise ValueError('Cannot add two Transforms with different todims.') + return chain((self, other), self.todims) def unchain(self): '''Iterator of unchained :class:`Transforms` items. @@ -348,14 +350,19 @@ class PlainTransforms(Transforms): ---------- transforms : :class:`tuple` of :class:`~nutils.transform.TransformItem` objects The sequence of transforms. + todims : :class:`int` + The dimension all transforms in this sequence map to. fromdims : :class:`int` - The number of dimensions all ``transforms`` map from. + The dimension all transforms in this sequence map from. ''' - __slots__ = '_transforms', '_sorted', '_indices' + __slots__ = '_transforms', '_sorted', '_indices', '_fromdims' @types.apply_annotations - def __init__(self, transforms:types.tuple[transform.canonical], fromdims:types.strictint): + def __init__(self, transforms:types.tuple[transform.canonical], todims:types.strictint, fromdims:types.strictint): + transforms_todims = set(trans[0].todims for trans in transforms) + if not (transforms_todims <= {todims}): + raise ValueError('expected transforms with todims={}, but got {}'.format(todims, transforms_todims)) transforms_fromdims = set(trans[-1].fromdims for trans in transforms) if not (transforms_fromdims <= {fromdims}): raise ValueError('expected transforms with fromdims={}, but got {}'.format(fromdims, transforms_fromdims)) @@ -365,7 +372,8 @@ def __init__(self, transforms:types.tuple[transform.canonical], fromdims:types.s self._sorted[i] = tuple(map(id, trans)) self._indices = numpy.argsort(self._sorted) self._sorted = self._sorted[self._indices] - super().__init__(fromdims) + self._fromdims = fromdims + super().__init__(todims) def __iter__(self): return iter(self._transforms) @@ -379,7 +387,7 @@ def __len__(self): return len(self._transforms) def index_with_tail(self, trans): - trans, orig_trans = transform.promote(trans, self.fromdims), trans + trans, orig_trans = transform.promote(trans, self._fromdims), trans transid_array = numpy.empty((), dtype=object) transid_array[()] = transid = tuple(map(id, trans)) i = numpy.searchsorted(self._sorted, transid_array, side='right') - 1 @@ -406,10 +414,11 @@ class IdentifierTransforms(Transforms): Length of the sequence. ''' - __slots__ = '_name', '_length' + __slots__ = '_ndims', '_name', '_length' @types.apply_annotations def __init__(self, ndims:types.strictint, name:str, length:int): + self._ndims = ndims self._name = name self._length = length super().__init__(ndims) @@ -418,14 +427,14 @@ def __getitem__(self, index): if not numeric.isint(index): return super().__getitem__(index) index = int(index) # make sure that index is a Python integer rather than numpy.intxx - return transform.Identifier(self.fromdims, (self._name, numeric.normdim(self._length, index))), + return transform.Identifier(self._ndims, (self._name, numeric.normdim(self._length, index))), def __len__(self): return self._length def index_with_tail(self, trans): root = trans[0] - if root.fromdims == self.fromdims and type(root) == transform.Identifier and isinstance(root.token, tuple) and len(root.token) == 2 and root.token[0] == self._name and 0 <= root.token[1] < self._length: + if root.todims == self._ndims and type(root) == transform.Identifier and isinstance(root.token, tuple) and len(root.token) == 2 and root.token[0] == self._name and 0 <= root.token[1] < self._length: return root.token[1], trans[1:] raise ValueError @@ -526,7 +535,7 @@ class StructuredTransforms(Transforms): Number of structured refinements. ''' - __slots__ = '_axes', '_nrefine', '_etransforms', '_ctransforms', '_cindices' + __slots__ = '_axes', '_nrefine', '_etransforms', '_ctransforms', '_cindices', '_fromdims' @types.apply_annotations def __init__(self, axes:types.tuple[types.strict[Axis]], nrefine:types.strictint): @@ -546,7 +555,8 @@ def __init__(self, axes:types.tuple[types.strict[Axis]], nrefine:types.strictint rmdims[idim] = True self._etransforms = tuple(etransforms) - super().__init__(sum(axis.isdim for axis in self._axes)) + self._fromdims = sum(axis.isdim for axis in self._axes) + super().__init__(len(self._axes)) def __getitem__(self, index): if not numeric.isint(index): @@ -593,7 +603,7 @@ def index_with_tail(self, trans): flatindex = flatindex*len(axis) + axis.unmap(index) # Promote the remainder and match the edge transforms. - tail = transform.promote(tail[self._nrefine:], self.fromdims) + tail = transform.promote(tail[self._nrefine:], self._fromdims) if tail[:len(self._etransforms)] != self._etransforms: raise ValueError tail = tail[len(self._etransforms):] @@ -617,7 +627,7 @@ class MaskedTransforms(Transforms): def __init__(self, parent:stricttransforms, indices:types.frozenarray[types.strictint]): self._parent = parent self._indices = indices - super().__init__(parent.fromdims) + super().__init__(parent.todims) def __iter__(self): for itrans in self._indices: @@ -657,7 +667,7 @@ class ReorderedTransforms(Transforms): def __init__(self, parent:stricttransforms, indices:types.frozenarray[types.strictint]): self._parent = parent self._indices = indices - super().__init__(parent.fromdims) + super().__init__(parent.todims) @property def _rindices(self): @@ -696,23 +706,22 @@ class DerivedTransforms(Transforms): derived_attribute : :class:`str` The name of the attribute of a :class:`nutils.element.Reference` that contains the derived references. - fromdims : :class:`int` - The number of dimensions all transforms in this sequence map from. + updim : :class:`bool` + ``True`` if the derived transform items are updims. ''' - __slots__ = '_parent', '_parent_references', '_derived_transforms' + __slots__ = '_parent', '_parent_references', '_derived_transforms', '_updim' __cache__ = '_offsets' @types.apply_annotations - def __init__(self, parent:stricttransforms, parent_references:elementseq.strictreferences, derived_attribute:types.strictstr, fromdims:types.strictint): + def __init__(self, parent:stricttransforms, parent_references:elementseq.strictreferences, derived_attribute:types.strictstr, updim:types.strict[bool]): if len(parent) != len(parent_references): raise ValueError('`parent` and `parent_references` should have the same length') - if parent.fromdims != parent_references.ndims: - raise ValueError('`parent` and `parent_references` have different dimensions') self._parent = parent self._parent_references = parent_references self._derived_transforms = operator.attrgetter(derived_attribute) - super().__init__(fromdims) + self._updim = updim + super().__init__(self._parent.todims) @property def _offsets(self): @@ -739,10 +748,7 @@ def index_with_tail(self, trans): iparent, tail = self._parent.index_with_tail(trans) if not tail: raise ValueError - if self.fromdims == self._parent.fromdims: - tail = transform.uppermost(tail) - else: - tail = transform.canonical(tail) + tail = (transform.canonical if self._updim else transform.uppermost)(tail) iderived = self._derived_transforms(self._parent_references[iparent]).index(tail[0]) return self._offsets[iparent]+iderived, tail[1:] @@ -763,19 +769,18 @@ class UniformDerivedTransforms(Transforms): derived_attribute : :class:`str` The name of the attribute of a :class:`nutils.element.Reference` that contains the derived references. - fromdims : :class:`int` - The number of dimensions all transforms in this sequence map from. + updim : :class:`bool` + ``True`` if the derived transform items are updims. ''' - __slots__ = '_parent', '_derived_transforms' + __slots__ = '_parent', '_derived_transforms', '_updim' @types.apply_annotations - def __init__(self, parent:stricttransforms, parent_reference:element.strictreference, derived_attribute:types.strictstr, fromdims:types.strictint): - if parent.fromdims != parent_reference.ndims: - raise ValueError('`parent` and `parent_reference` have different dimensions') + def __init__(self, parent:stricttransforms, parent_reference:element.strictreference, derived_attribute:types.strictstr, updim:types.strict[bool]): self._parent = parent self._derived_transforms = getattr(parent_reference, derived_attribute) - super().__init__(fromdims) + self._updim = updim + super().__init__(self._parent.todims) def __len__(self): return len(self._parent)*len(self._derived_transforms) @@ -795,10 +800,7 @@ def index_with_tail(self, trans): iparent, tail = self._parent.index_with_tail(trans) if not tail: raise ValueError - if self.fromdims == self._parent.fromdims: - tail = transform.uppermost(tail) - else: - tail = transform.canonical(tail) + tail = (transform.canonical if self._updim else transform.uppermost)(tail) iderived = self._derived_transforms.index(tail[0]) return iparent*len(self._derived_transforms) + iderived, tail[1:] @@ -862,10 +864,10 @@ class ChainedTransforms(Transforms): def __init__(self, items:types.tuple[stricttransforms]): if len(items) == 0: raise ValueError('Empty chain.') - if len(set(item.fromdims for item in items)) != 1: - raise ValueError('Cannot chain Transforms with different fromdims.') + if len(set(item.todims for item in items)) != 1: + raise ValueError('Cannot chain Transforms with different todims.') self._items = items - super().__init__(self._items[0].fromdims) + super().__init__(self._items[0].todims) @property def _offsets(self): @@ -885,17 +887,17 @@ def __getitem__(self, index): if index == range(len(self)): return self elif index.start == index.stop: - return EmptyTransforms(self.fromdims) + return EmptyTransforms(self.todims) ostart = numpy.searchsorted(self._offsets, index.start, side='right') - 1 ostop = numpy.searchsorted(self._offsets, index.stop, side='left') - return chain((item[max(0,index.start-istart):min(istop-istart,index.stop-istart)] for item, (istart, istop) in zip(self._items[ostart:ostop], util.pairwise(self._offsets[ostart:ostop+1]))), self.fromdims) + return chain((item[max(0,index.start-istart):min(istop-istart,index.stop-istart)] for item, (istart, istop) in zip(self._items[ostart:ostop], util.pairwise(self._offsets[ostart:ostop+1]))), self.todims) elif numeric.isintarray(index) and index.ndim == 1 and len(index) and numpy.all(numpy.greater(numpy.diff(index), 0)): if index[0] < 0 or index[-1] >= len(self): raise IndexError('index out of bounds') split = numpy.searchsorted(index, self._offsets, side='left') - return chain((item[index[start:stop]-offset] for item, offset, (start, stop) in zip(self._items, self._offsets, util.pairwise(split)) if stop > start), self.fromdims) + return chain((item[index[start:stop]-offset] for item, offset, (start, stop) in zip(self._items, self._offsets, util.pairwise(split)) if stop > start), self.todims) elif numeric.isboolarray(index) and index.shape == (len(self),): - return chain((item[index[start:stop]] for item, (start, stop) in zip(self._items, util.pairwise(self._offsets))), self.fromdims) + return chain((item[index[start:stop]] for item, (start, stop) in zip(self._items, util.pairwise(self._offsets))), self.todims) else: return super().__getitem__(index) @@ -914,23 +916,24 @@ def index_with_tail(self, trans): raise ValueError def refined(self, references): - return chain((item.refined(references[start:stop]) for item, start, stop in zip(self._items, self._offsets[:-1], self._offsets[1:])), self.fromdims) + return chain((item.refined(references[start:stop]) for item, start, stop in zip(self._items, self._offsets[:-1], self._offsets[1:])), self.todims) def edges(self, references): - return chain((item.edges(references[start:stop]) for item, start, stop in zip(self._items, self._offsets[:-1], self._offsets[1:])), self.fromdims-1) + return chain((item.edges(references[start:stop]) for item, start, stop in zip(self._items, self._offsets[:-1], self._offsets[1:])), self.todims) def unchain(self): yield from self._items -def chain(items, fromdims): +@types.apply_annotations +def chain(items:types.tuple[stricttransforms], todims:types.strictint): '''Return the chained transforms sequence of ``items``. Parameters ---------- items : iterable of :class:`Transforms` objects The :class:`Transforms` objects to chain. - fromdims : :class:`int` - The number of dimensions all transforms in this sequence map from. + todims : :class:`int` + The dimension all transforms of all sequences map to. Returns ------- @@ -939,11 +942,11 @@ def chain(items, fromdims): ''' unchained = tuple(filter(len, itertools.chain.from_iterable(item.unchain() for item in items))) - items_fromdims = set(item.fromdims for item in unchained) - if not (items_fromdims <= {fromdims}): - raise ValueError('expected transforms with fromdims={}, but got {}'.format(fromdims, items_fromdims)) + items_todims = set(item.todims for item in unchained) + if not (items_todims <= {todims}): + raise ValueError('expected transforms with todims={}, but got {}'.format(todims, items_todims)) if len(unchained) == 0: - return EmptyTransforms(fromdims) + return EmptyTransforms(todims) elif len(unchained) == 1: return unchained[0] else: diff --git a/tests/test_basis.py b/tests/test_basis.py index 7d115dc03..310e7fd62 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -259,7 +259,7 @@ def setUp(self): coords = numpy.random.normal(size=(nverts, self.ndims)) roottrans = transform.Identifier(self.ndims, 'test') root = function.Root('X', self.ndims) - transforms = transformseq.PlainTransforms([(roottrans, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], self.ndims) + transforms = transformseq.PlainTransforms([(roottrans, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], root.ndims, self.ndims) domain = topology.SimplexTopology(root, simplices, transforms, transforms) geom = function.rootcoords(root) else: diff --git a/tests/test_function.py b/tests/test_function.py index e295de528..2cc6419fe 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -1093,7 +1093,7 @@ class PlainBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) self.roots = root, - transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) + transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0, 0) self.checkcoeffs = [[1],[2,3],[4,5],[6]] self.checkdofs = [[0],[2,3],[1,3],[2]] self.basis = function.PlainBasis(self.checkcoeffs, self.checkdofs, 4, transforms, 0, function.SelectChain((root,))) @@ -1104,7 +1104,7 @@ class DiscontBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) self.roots = root, - transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) + transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0, 0) self.checkcoeffs = [[1],[2,3],[4,5],[6]] self.basis = function.DiscontBasis(self.checkcoeffs, transforms, 0, function.SelectChain((root,))) self.checkdofs = [[0],[1,2],[3,4],[5]] @@ -1115,7 +1115,7 @@ class MaskedBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) self.roots = root, - transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) + transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0, 0) parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, transforms, 0, function.SelectChain((root,))) self.basis = function.MaskedBasis(parent, [0,2], function.SelectChain((root,))) self.checkcoeffs = [[1],[2],[],[6]] @@ -1127,7 +1127,7 @@ class PrunedBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) self.roots = root, - parent_transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0) + parent_transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0, 0) parent = function.PlainBasis([[1],[2,3],[4,5],[6]], [[0],[2,3],[1,3],[2]], 4, parent_transforms, 0, function.SelectChain((root,))) self.basis = function.PrunedBasis(parent, [0,2], function.SelectChain((root,))) self.checkcoeffs = [[1],[4,5]] diff --git a/tests/test_topology.py b/tests/test_topology.py index c20cb4a19..34f777f1f 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -259,7 +259,7 @@ def test_boundary_gradient(self): ref = _refined_refs[self.etype] trans = (transform.Identifier(ref.ndims, 'root'),) root = function.Root('root', ref.ndims) - domain = topology.ConnectedTopology((root,), elementseq.asreferences([ref], ref.ndims), transformseq.PlainTransforms([trans], ref.ndims), transformseq.PlainTransforms([trans], ref.ndims), ((-1,)*ref.nedges,)).refine(self.ref0) + domain = topology.ConnectedTopology((root,), elementseq.asreferences([ref], ref.ndims), transformseq.PlainTransforms([trans], root.ndims, ref.ndims), transformseq.PlainTransforms([trans], root.ndims, ref.ndims), ((-1,)*ref.nedges,)).refine(self.ref0) geom = function.rootcoords(root) basis = domain.basis('std', degree=1) u = domain.projection(geom.sum(), onto=basis, geometry=geom, degree=2) @@ -617,7 +617,7 @@ def test_refine_iter(self): common( 'Topology', - topo=topology.Topology((function.Root('point', 0),), elementseq.asreferences([element.PointReference()], 0), transformseq.PlainTransforms([(transform.Identifier(0, 'test'),)], 0), transformseq.PlainTransforms([(transform.Identifier(0, 'test'),)], 0)), + topo=topology.Topology((function.Root('point', 0),), elementseq.asreferences([element.PointReference()], 0), transformseq.PlainTransforms([(transform.Identifier(0, 'test'),)], 0, 0), transformseq.PlainTransforms([(transform.Identifier(0, 'test'),)], 0, 0)), hasboundary=False) common( 'StructuredTopology:2D', diff --git a/tests/test_transformseq.py b/tests/test_transformseq.py index f41d0ce3b..ffeaed166 100644 --- a/tests/test_transformseq.py +++ b/tests/test_transformseq.py @@ -4,10 +4,18 @@ class Common: + def test_todims(self): + self.assertEqual(self.seq.todims, self.checktodims) + for trans in self.seq: + self.assertEqual(trans[0].todims, self.checktodims) + def test_fromdims(self): - self.assertEqual(self.seq.fromdims, self.checkfromdims) + self.assertEqual(len(self.seq), len(self.checkrefs)) + for trans, ref in zip(self.seq, self.checkrefs): + self.assertEqual(trans[-1].fromdims, ref.ndims) def test_len(self): + self.assertEqual(len(self.check), len(self.checkrefs)) self.assertEqual(len(self.seq), len(self.check)) def test_getitem_scalar_pos(self): @@ -62,7 +70,7 @@ def test_iter(self): self.assertEqual(tuple(self.seq), tuple(self.check)) def test_add(self): - self.assertEqual(tuple(self.seq+nutils.transformseq.EmptyTransforms(self.checkfromdims)), tuple(self.check)) + self.assertEqual(tuple(self.seq+nutils.transformseq.EmptyTransforms(self.checktodims)), tuple(self.check)) self.assertEqual(tuple(self.seq+self.seq), tuple(self.check)+tuple(self.check)) def test_index_with_tail(self): @@ -70,7 +78,7 @@ def test_index_with_tail(self): self.assertEqual(self.seq.index_with_tail(trans), (i, ())) for ctrans in ref.child_transforms: self.assertEqual(self.seq.index_with_tail(trans+(ctrans,)), (i, (ctrans,))) - if self.checkfromdims > 0: + if ref.ndims > 0: for etrans in ref.edge_transforms: for shuffle in lambda t: t, nutils.transform.canonical: self.assertEqual(self.seq.index_with_tail(shuffle(trans+(ctrans,))), (i, (ctrans,))) @@ -98,7 +106,7 @@ def test_contains_with_tail(self): self.assertEqual(self.seq.index_with_tail(trans), (i, ())) for ctrans in ref.child_transforms: self.assertTrue(self.seq.contains_with_tail(trans+(ctrans,))) - if self.checkfromdims > 0: + if ref.ndims > 0: for etrans in ref.edge_transforms: for shuffle in lambda t: t, nutils.transform.canonical: self.assertTrue(self.seq.contains_with_tail(trans+(etrans,))) @@ -162,67 +170,67 @@ def test_edges(self): class EmptyTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.EmptyTransforms(fromdims=1) + self.seq = nutils.transformseq.EmptyTransforms(1) self.check = () self.checkmissing = (l1,s0),(x1,s4),(r1,s0) self.checkrefs = nutils.elementseq.EmptyReferences(1) - self.checkfromdims = 1 + self.checktodims = 1 class PlainTransforms1D(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1),(x1,s2),(x1,s3)], fromdims=1) + self.seq = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1),(x1,s2),(x1,s3)], 1, 1) self.check = (x1,s0),(x1,s1),(x1,s2),(x1,s3) self.checkmissing = (l1,s0),(x1,s4),(r1,s0) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checkfromdims = 1 + self.checktodims = 1 class PlainTransforms2D(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], fromdims=2) + self.seq = nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], 2, 2) self.check = (x2,s00),(x2,s01),(x2,s10),(x2,s11) self.checkmissing = (l2,s00),(x2,s02),(x2,s12),(r2,s00) self.checkrefs = nutils.elementseq.asreferences([square,square,triangle,triangle], 2) - self.checkfromdims = 2 + self.checktodims = 2 class MaskedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.MaskedTransforms(nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], fromdims=2), [0,2]) + self.seq = nutils.transformseq.MaskedTransforms(nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], 2, 2), [0,2]) self.check = (x2,s00),(x2,s10) self.checkmissing = (l2,s00),(x2,s01),(x2,s11),(x2,s02),(x2,s12),(r2,s00) self.checkrefs = nutils.elementseq.asreferences([square,triangle], 2) - self.checkfromdims = 2 + self.checktodims = 2 class ReorderedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.ReorderedTransforms(nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], fromdims=2), [0,2,3,1]) + self.seq = nutils.transformseq.ReorderedTransforms(nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], 2, 2), [0,2,3,1]) self.check = (x2,s00),(x2,s10),(x2,s11),(x2,s01) self.checkmissing = (l2,s00),(x2,s02),(x2,s12),(r2,s00) self.checkrefs = nutils.elementseq.asreferences([square]*4, 2) - self.checkfromdims = 2 + self.checktodims = 2 class DerivedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.DerivedTransforms(nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], fromdims=1), nutils.elementseq.asreferences([line,line], 1), 'child_transforms', 1) + self.seq = nutils.transformseq.DerivedTransforms(nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1), nutils.elementseq.asreferences([line,line], 1), 'child_transforms', False) self.check = (x1,s0,c0),(x1,s0,c1),(x1,s1,c0),(x1,s1,c1) self.checkmissing = (l1,s0),(x1,s0),(x1,s1),(r1,s0) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checkfromdims = 1 + self.checktodims = 1 class UniformDerivedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.UniformDerivedTransforms(nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], fromdims=1), line, 'child_transforms', 1) + self.seq = nutils.transformseq.UniformDerivedTransforms(nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1), line, 'child_transforms', False) self.check = (x1,s0,c0),(x1,s0,c1),(x1,s1,c0),(x1,s1,c1) self.checkmissing = (l1,s0),(x1,s0),(x1,s1),(r1,s0) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checkfromdims = 1 + self.checktodims = 1 class ChainedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.ChainedTransforms([nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], fromdims=1), nutils.transformseq.PlainTransforms([(x1,s2),(x1,s3)], fromdims=1)]) + self.seq = nutils.transformseq.ChainedTransforms([nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1), nutils.transformseq.PlainTransforms([(x1,s2),(x1,s3)], 1, 1)]) self.check = (x1,s0),(x1,s1),(x1,s2),(x1,s3) self.checkmissing = (l1,s0),(x1,s4),(r1,s0) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checkfromdims = 1 + self.checktodims = 1 class StructuredTransforms1D(TestCase, Common, Edges): def setUp(self): @@ -230,7 +238,7 @@ def setUp(self): self.check = (s0,),(s1,),(s2,),(s3,) self.checkmissing = (l1,s0),(s4,),(r1,s0),(c1,) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checkfromdims = 1 + self.checktodims = 1 class StructuredTransforms1DRefined(TestCase, Common, Edges): def setUp(self): @@ -238,7 +246,7 @@ def setUp(self): self.check = (s0,c0),(s0,c1),(s1,c0),(s1,c1) self.checkmissing = (l1,s0),(s0,),(s1,),(s0,s1),(r1,s0) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checkfromdims = 1 + self.checktodims = 1 class StructuredTransforms1DLeft(TestCase, Common): def setUp(self): @@ -246,7 +254,7 @@ def setUp(self): self.check = (s3,e1), self.checkmissing = (s0,e0),(s2,e0),(s4,e0) self.checkrefs = nutils.elementseq.asreferences([point], 0) - self.checkfromdims = 0 + self.checktodims = 1 class StructuredTransforms1DRight(TestCase, Common): def setUp(self): @@ -254,7 +262,7 @@ def setUp(self): self.check = (s2,e0), self.checkmissing = (s0,e0),(s3,e1),(s4,e0) self.checkrefs = nutils.elementseq.asreferences([point], 0) - self.checkfromdims = 0 + self.checktodims = 1 class StructuredTransforms1DInterfacesLeft(TestCase, Common): def setUp(self): @@ -262,7 +270,7 @@ def setUp(self): self.check = (s1,e1),(s2,e1),(s3,e1) self.checkmissing = (s0,e1),(s0,e0),(s1,e0),(s2,e0),(s3,e0) self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) - self.checkfromdims = 0 + self.checktodims = 1 class StructuredTransforms1DInterfacesRight(TestCase, Common): def setUp(self): @@ -270,23 +278,23 @@ def setUp(self): self.check = (s0,e0),(s1,e0),(s2,e0) self.checkmissing = (s3,e0),(s0,e1),(s1,e1),(s2,e1),(s3,e1) self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) - self.checkfromdims = 0 + self.checktodims = 1 class StructuredTransforms1DPeriodicInterfacesLeft(TestCase, Common): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.PIntAxis(0,4,0,False)], 0) self.check = (s1,e1),(s2,e1),(s3,e1),(s0,e1) self.checkmissing = (s0,e0),(s1,e0),(s2,e0),(s3,e0),(s4,e0) - self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) - self.checkfromdims = 0 + self.checkrefs = nutils.elementseq.asreferences([point]*4, 0) + self.checktodims = 1 class StructuredTransforms1DPeriodicInterfacesRight(TestCase, Common): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.PIntAxis(0,4,0,True)], 0) self.check = (s0,e0),(s1,e0),(s2,e0),(s3,e0) self.checkmissing = (s0,e1),(s1,e1),(s2,e1),(s3,e1),(s4,e1) - self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) - self.checkfromdims = 0 + self.checkrefs = nutils.elementseq.asreferences([point]*4, 0) + self.checktodims = 1 class StructuredTransforms2D(TestCase, Common, Edges): def setUp(self): @@ -294,7 +302,7 @@ def setUp(self): self.check = (s02,),(s03,),(s12,),(s13,) self.checkmissing = (s00,),(s01,),(s10,),(s11,) self.checkrefs = nutils.elementseq.asreferences([square]*4, 2) - self.checkfromdims = 2 + self.checktodims = 2 class StructuredTransforms2DRefined(TestCase, Common, Edges): def setUp(self): @@ -302,7 +310,7 @@ def setUp(self): self.check = (s01,c00),(s01,c01),(s01,c10),(s01,c11) self.checkmissing = (s00,c00), self.checkrefs = nutils.elementseq.asreferences([square]*4, 2) - self.checkfromdims = 2 + self.checktodims = 2 class IdentifierTransforms(TestCase, Common, Edges): def setUp(self): @@ -310,43 +318,32 @@ def setUp(self): self.check = [(nutils.transform.Identifier(2, ('foo', i)),) for i in range(4)] self.checkmissing = (nutils.transform.Identifier(1, ('foo', 0)),), (nutils.transform.Identifier(2, ('foo', -1)),), (nutils.transform.Identifier(2, ('foo', 4)),), (nutils.transform.Identifier(2, ('bar', 0)),) self.checkrefs = nutils.elementseq.asreferences([triangle]*4, 2) - self.checkfromdims = 2 + self.checktodims = 2 class exceptions(TestCase): - def test_PlainTransforms_invalid_fromdims(self): - with self.assertRaisesRegex(ValueError, 'expected transforms with fromdims=2, but got .*'): - nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 2) + def test_PlainTransforms_invalid_todims(self): + with self.assertRaisesRegex(ValueError, 'expected transforms with todims=2, but got .*'): + nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 2, 1) - def test_PlainTransforms_multiple_fromdims(self): + def test_PlainTransforms_invalid_fromdims(self): with self.assertRaisesRegex(ValueError, 'expected transforms with fromdims=2, but got .*'): - nutils.transformseq.PlainTransforms([(x1,s0),(x2,s00)], 2) + nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 2) def test_DerivedTransforms_length_mismatch(self): - transforms = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1) + transforms = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1) references = nutils.elementseq.PlainReferences([line]*3, 1) with self.assertRaisesRegex(ValueError, '`parent` and `parent_references` should have the same length'): - nutils.transformseq.DerivedTransforms(transforms, references, 'child_transforms', 1) - - def test_DerivedTransforms_ndims_mismatch(self): - transforms = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1) - references = nutils.elementseq.PlainReferences([square]*2, 2) - with self.assertRaisesRegex(ValueError, '`parent` and `parent_references` have different dimensions'): - nutils.transformseq.DerivedTransforms(transforms, references, 'child_transforms', 1) - - def test_UniformDerivedTransforms_ndims_mismatch(self): - transforms = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1) - with self.assertRaisesRegex(ValueError, '`parent` and `parent_reference` have different dimensions'): - nutils.transformseq.UniformDerivedTransforms(transforms, square, 'child_transforms', 1) + nutils.transformseq.DerivedTransforms(transforms, references, 'child_transforms', False) def test_ChainedTransforms_no_items(self): with self.assertRaisesRegex(ValueError, 'Empty chain.'): nutils.transformseq.ChainedTransforms([]) def test_ChainedTransforms_multiple_ndims(self): - a = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1) - b = nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01)], 2) - with self.assertRaisesRegex(ValueError, 'Cannot chain Transforms with different fromdims.'): + a = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1) + b = nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01)], 2, 2) + with self.assertRaisesRegex(ValueError, 'Cannot chain Transforms with different todims.'): nutils.transformseq.ChainedTransforms([a, b]) # vim:sw=2:sts=2:et From fecab4f1ce4589ac58f52024be01310b3b1ee5e1 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sun, 12 Jan 2020 21:45:13 +0100 Subject: [PATCH 033/119] impl det for transform.Updim,ScaledUpdim --- nutils/transform.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nutils/transform.py b/nutils/transform.py index 52512801d..a07ab0ea9 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -339,6 +339,10 @@ def __init__(self, linear:types.frozenarray, offset:types.frozenarray, isflipped self.isflipped = isflipped super().__init__(linear, offset) + @property + def det(self): + return numpy.sqrt(numpy.linalg.det(numpy.einsum('ki,kj->ij', self.linear, self.linear))) if self.fromdims else 1 + @property def ext(self): ext = numeric.ext(self.linear) @@ -440,6 +444,10 @@ def swapup(self, other): def flipped(self): return ScaledUpdim(self.trans1, self.trans2.flipped) + @property + def det(self): + return numpy.sqrt(numpy.linalg.det(numpy.einsum('ki,kj->ij', self.linear, self.linear))) if self.fromdims else 1 + class TensorEdge1(Updim): __slots__ = 'trans', From f0451ab59d87984103188cfa73198c2aedb93173 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 8 Jan 2020 21:12:12 +0100 Subject: [PATCH 034/119] add basis for manifold, normal space to Points --- nutils/element.py | 23 ++++++++++++++-- nutils/function.py | 9 ++++-- nutils/points.py | 68 ++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/nutils/element.py b/nutils/element.py index 02caaa370..2deacd2df 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -238,10 +238,27 @@ def check_edges(self, tol=1e-15, print=print): zero = 0 for trans, edge in self.edges: if edge: - gauss = edge.getpoints('gauss', 1) - w_normal = gauss.weights[:,_] * trans.ext + points = edge.getpoints('gauss', 1) + TJ = numpy.empty((self.ndims, self.ndims), dtype=float) + TJ[:,:trans.fromdims] = trans.linear + if trans.todims == trans.fromdims: + pass + elif trans.todims == trans.fromdims + 1: + TJ[:,-1] = trans.ext + else: + raise NotImplementedError + J = numpy.empty((points.npoints, self.ndims, self.ndims), dtype=float) + PJ = points.basis + numpy.einsum('ij,njk->nik', TJ[:,:points.ndims], points.basis, out=J[:,:,:PJ.shape[2]]) + if PJ.shape[2] < self.ndims: + assert PJ.shape[2] == points.ndims + J[:,:,PJ.shape[2]:] = TJ[_,:,points.ndims:self.ndims] + detJ = abs(numpy.linalg.det(J[:,:,:self.ndims-1].transpose(0,2,1) @ J[:,:,:self.ndims-1]))**0.5 + numeric.gramschmidt(J) + n = J[:,:,-1] + w_normal = (points.weights * detJ)[:,_] * n zero += w_normal.sum(0) - volume += numeric.contract(trans.apply(gauss.coords), w_normal, axis=0) + volume += numeric.contract(trans.apply(points.coords), w_normal, axis=0) if numpy.greater(abs(zero), tol).any(): print('divergence check failed: {} != 0'.format(zero)) if numpy.greater(abs(volume - self.volume), tol).any(): diff --git a/nutils/function.py b/nutils/function.py index 35a6be9bd..2489f8b61 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -804,10 +804,15 @@ def evalf(self, evalargs, _trans): ndims = self._root.ndims assert trans == _trans assert (trans[0].todims if trans else points.ndims) == ndims - if points.ndims != self._ndimstangent: + if points.ndimsmanifold != self._ndimstangent: raise ValueError('expected a {}D tangent space, but got a {}D space'.format(self._ndimstangent, points.ndimstangent)) - return transform.linearfrom(trans, ndims)[_] + linear = numpy.empty((points.npoints, ndims, ndims), dtype=float) + translinear = transform.linearfrom(trans, ndims) + numpy.einsum('ij,njk->nik', translinear[:,:points.ndims], points.basis, out=linear[:,:,:points.ndims]) + if points.ndims < ndims: + linear[:,:,points.ndims:] = translinear[_,:,points.ndims:] + return linear @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): diff --git a/nutils/points.py b/nutils/points.py index b374f7183..1b030b321 100644 --- a/nutils/points.py +++ b/nutils/points.py @@ -55,12 +55,16 @@ class Points(types.Singleton): Number of spatial dimensions. ''' - __cache__ = 'hull', 'onhull' + __cache__ = 'hull', 'onhull', 'basis' @types.apply_annotations - def __init__(self, npoints:types.strictint, ndims:types.strictint): + def __init__(self, npoints:types.strictint, ndims:types.strictint, ndimsnormal:types.strictint=0): self.npoints = npoints self.ndims = ndims + if not 0 <= ndimsnormal <= ndims: + raise ValueError('the dimension of the normal space should be in [0,{}] but got {}'.format(ndims, ndimsnormal)) + self.ndimsnormal = ndimsnormal + self.ndimsmanifold = ndims - ndimsnormal @property def tri(self): @@ -101,23 +105,30 @@ def onhull(self): onhull[numpy.ravel(self.hull)] = True # not clear why ravel is necessary but setitem seems to require it return types.frozenarray(onhull, copy=False) + @property + def basis(self): + if self.ndimsnormal == 0: + return types.frozenarray(numpy.eye(self.ndims)[numpy.newaxis], dtype=float, copy=False) + else: + raise NotImplementedError + strictpoints = types.strict[Points] class CoordsPoints(Points): '''Manually supplied points.''' @types.apply_annotations - def __init__(self, coords:types.frozenarray[float]): + def __init__(self, coords:types.frozenarray[float], ndimsnormal:types.strictint=0): self.coords = coords - super().__init__(*coords.shape) + super().__init__(*coords.shape, ndimsnormal) class CoordsWeightsPoints(CoordsPoints): '''Manually supplied points and weights.''' @types.apply_annotations - def __init__(self, coords:types.frozenarray[float], weights:types.frozenarray[float]): + def __init__(self, coords:types.frozenarray[float], weights:types.frozenarray[float], ndimsnormal:types.strictint=0): self.weights = weights - super().__init__(coords) + super().__init__(coords, ndimsnormal) class CoordsUniformPoints(CoordsPoints): '''Manually supplied points with uniform weights.''' @@ -136,7 +147,7 @@ class TensorPoints(Points): def __init__(self, points1:strictpoints, points2:strictpoints): self.points1 = points1 self.points2 = points2 - super().__init__(points1.npoints * points2.npoints, points1.ndims + points2.ndims) + super().__init__(points1.npoints * points2.npoints, points1.ndims + points2.ndims, points1.ndimsnormal + points2.ndimsnormal) @property def coords(self): @@ -228,13 +239,15 @@ def tri(self): class TransformPoints(Points): '''Affinely transformed Points.''' - __cache__ = 'coords', 'weights' + __cache__ = 'coords', 'weights', 'basis' @types.apply_annotations def __init__(self, points:strictpoints, trans:transform.stricttransformitem): self.points = points self.trans = trans - super().__init__(points.npoints, points.ndims) + if trans.fromdims != points.ndims: + raise ValueError('the dimension of the domain of the transform should match the dimension of the points but got {} and {} respectively'.format(trans.fromdims, points.ndims)) + super().__init__(points.npoints, trans.todims, trans.todims-points.ndimsmanifold) @property def coords(self): @@ -242,7 +255,14 @@ def coords(self): @property def weights(self): - return self.points.weights * abs(float(self.trans.det)) + if self.points.ndimsmanifold < self.points.ndims: + P = numpy.array(self.points.basis[:,:,:self.points.ndimsmanifold], dtype=float, copy=True) + numeric.gramschmidt(P) + TP = numpy.einsum('ij,njk->nik', self.trans.linear, P) + det = numpy.sqrt(numpy.linalg.det(numpy.einsum('nki,nkj->nij', TP, TP))) + else: + det = abs(float(self.trans.det)) + return self.points.weights * det @property def tri(self): @@ -252,6 +272,17 @@ def tri(self): def hull(self): return self.points.hull + @property + def basis(self): + b = numpy.empty((self.npoints, self.ndims, self.ndims), dtype=float) + numpy.einsum('ij,njk->nik', self.trans.linear, self.points.basis, out=b[:,:,:self.points.ndims]) + if self.points.ndims == self.ndims-1: + b[:,:,-1] = self.trans.ext + elif self.points.ndims != self.ndims: + raise ValueError('`trans.fromdims` should equal `trans.todims` or `trans.todims-1`') + numeric.gramschmidt(b) + return types.frozenarray(b, dtype=float, copy=False) + class ConcatPoints(Points): '''Concatenation of several Points objects. @@ -259,13 +290,20 @@ class ConcatPoints(Points): triggering deduplication and resulting in a smaller total point count. ''' - __cache__ = 'coords', 'weights', 'tri', 'masks' + __cache__ = 'coords', 'weights', 'tri', 'masks', 'basis' @types.apply_annotations def __init__(self, allpoints:types.tuple[strictpoints], duplicates:frozenset=frozenset()): self.allpoints = allpoints self.duplicates = duplicates - super().__init__(sum(points.npoints for points in allpoints) - sum(len(d)-1 for d in duplicates), allpoints[0].ndims) + ndimsmanifolds = set(p.ndimsmanifold for p in allpoints) + ndimsnormals = set(p.ndimsnormal for p in allpoints) + if len(ndimsmanifolds) != 1: + raise ValueError('the dimension of the manifold space of the points to be concatenated is inhomongeneous') + if len(ndimsnormals) != 1: + raise ValueError('the dimension of the normal space of the points to be concatenated is inhomongeneous') + ndimsnormal, = ndimsnormals + super().__init__(sum(points.npoints for points in allpoints) - sum(len(d)-1 for d in duplicates), allpoints[0].ndims, ndimsnormal) @property def masks(self): @@ -307,6 +345,10 @@ def tri(self): renumber[i][j] = renumber[I][J] return types.frozenarray(numpy.concatenate([renum.take(points.tri) for renum, points in zip(renumber, self.allpoints)]), copy=False) + @property + def basis(self): + return types.frozenarray(numpy.concatenate([numpy.broadcast_to(points.basis, [points.npoints,self.ndims,self.ndims])[mask] for mask, points in zip(self.masks, self.allpoints)] if self.duplicates else [numpy.broadcast_to(points.basis, [points.npoints,self.ndims,self.ndims]) for points in self.allpoints]), copy=False) + class ConePoints(Points): '''Affinely transformed lower-dimensional points plus tip. @@ -323,6 +365,8 @@ def __init__(self, edgepoints:strictpoints, edgeref:transform.stricttransformite self.edgepoints = edgepoints self.edgeref = edgeref self.tip = tip + if edgepoints.ndimsnormal > 0: + raise NotImplementedError super().__init__(edgepoints.npoints+1, edgepoints.ndims+1) @property From 24421af823b2edc859ffd6c1a4d17c4d1dbca0cf Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 28 Jan 2020 11:24:00 +0100 Subject: [PATCH 035/119] add ndimsnormal param, attr to Element --- nutils/element.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/nutils/element.py b/nutils/element.py index 2deacd2df..c41e8571a 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -37,13 +37,14 @@ class Reference(types.Singleton): 'reference element' - __slots__ = 'ndims', + __slots__ = 'ndims', 'ndimsnormal' __cache__ = 'connectivity', 'edgechildren', 'ribbons', 'volume', 'centroid', '_linear_bernstein', 'getpoints' @types.apply_annotations - def __init__(self, ndims:int): + def __init__(self, ndims:int, ndimsnormal:int=0): super().__init__() self.ndims = ndims + self.ndimsnormal = ndimsnormal @property def nverts(self): @@ -316,7 +317,7 @@ def empty(self): @types.apply_annotations def __init__(self, baseref:strictreference): self.baseref = baseref - super().__init__(baseref.ndims) + super().__init__(baseref.ndims, baseref.ndimsnormal) @property def vertices(self): @@ -642,7 +643,7 @@ def __init__(self, ref1, ref2): assert not isinstance(ref1, TensorReference) self.ref1 = ref1 self.ref2 = ref2 - super().__init__(ref1.ndims + ref2.ndims) + super().__init__(ref1.ndims + ref2.ndims, ref1.ndimsnormal + ref2.ndimsnormal) def __mul__(self, other): assert isinstance(other, Reference) @@ -650,10 +651,10 @@ def __mul__(self, other): @property def vertices(self): - vertices = numpy.empty((self.ref1.nverts, self.ref2.nverts, self.ndims), dtype=float) - vertices[:,:,:self.ref1.ndims] = self.ref1.vertices[:,_] - vertices[:,:,self.ref1.ndims:] = self.ref2.vertices[_,:] - return types.frozenarray(vertices.reshape(self.ref1.nverts*self.ref2.nverts, self.ndims), copy=False) + vertices = numpy.empty((self.ref1.nverts, self.ref2.nverts, self.ndims+self.ndimsnormal), dtype=float) + vertices[:,:,:self.ref1.ndims+self.ref1.ndimsnormal] = self.ref1.vertices[:,_] + vertices[:,:,self.ref1.ndims+self.ref1.ndimsnormal:] = self.ref2.vertices[_,:] + return types.frozenarray(vertices.reshape(self.ref1.nverts*self.ref2.nverts, self.ndims+self.ndimsnormal), copy=False) @property def centroid(self): @@ -682,6 +683,8 @@ def getpoints(self, ischeme, degree): degree1 = degree if not isinstance(degree, tuple) else degree[0] degree2 = degree if not isinstance(degree, tuple) else degree[1] if len(degree) == 2 else degree[1:] return points.TensorPoints(self.ref1.getpoints(ischeme1, degree1), self.ref2.getpoints(ischeme2, degree2)) + if self.ndimsnormal != 0: + raise NotImplementedError if self.ref1.ndims == self.ref2.ndims == 1: coords = numpy.empty([2, 2, 2]) coords[...,:1] = self.ref1.vertices[:,_] @@ -703,9 +706,9 @@ def getpoints(self, ischeme, degree): def edge_transforms(self): edge_transforms = [] if self.ref1.ndims: - edge_transforms.extend(transform.TensorEdge1(trans1, self.ref2.ndims) for trans1 in self.ref1.edge_transforms) + edge_transforms.extend(transform.TensorEdge1(trans1, self.ref2.ndims+self.ref2.ndimsnormal) for trans1 in self.ref1.edge_transforms) if self.ref2.ndims: - edge_transforms.extend(transform.TensorEdge2(self.ref1.ndims, trans2) for trans2 in self.ref2.edge_transforms) + edge_transforms.extend(transform.TensorEdge2(self.ref1.ndims+self.ref1.ndimsnormal, trans2) for trans2 in self.ref2.edge_transforms) return tuple(edge_transforms) @property @@ -749,7 +752,7 @@ def child_refs(self): return tuple(child1 * child2 for child1 in self.ref1.child_refs for child2 in self.ref2.child_refs) def inside(self, point, eps=0): - return self.ref1.inside(point[:self.ref1.ndims],eps) and self.ref2.inside(point[self.ref1.ndims:],eps) + return self.ref1.inside(point[:self.ref1.ndims+self.ref1.ndimsnormal],eps) and self.ref2.inside(point[self.ref1.ndims+self.ref1.ndimsnormal:],eps) @property def simplices(self): @@ -789,6 +792,8 @@ class Cone(Reference): @types.apply_annotations def __init__(self, edgeref, etrans, tip:types.frozenarray): + if edgeref.ndimsnormal != 0: + raise NotImplementedError assert etrans.fromdims == edgeref.ndims assert etrans.todims == len(tip) super().__init__(len(tip)) @@ -870,8 +875,8 @@ class OwnChildReference(Reference): def __init__(self, baseref): self.baseref = baseref self.child_refs = baseref, - self.child_transforms = transform.Identity(baseref.ndims), - super().__init__(baseref.ndims) + self.child_transforms = transform.Identity(baseref.ndims+baseref.ndimsnormal), + super().__init__(baseref.ndims, baseref.ndimsnormal) @property def vertices(self): @@ -911,11 +916,11 @@ class WithChildrenReference(Reference): def __init__(self, baseref, child_refs:tuple): assert len(child_refs) == baseref.nchildren and any(child_refs) and child_refs != baseref.child_refs assert all(isinstance(child_ref,Reference) for child_ref in child_refs) - assert all(child_ref.ndims == baseref.ndims for child_ref in child_refs) + assert all(child_ref.ndims == baseref.ndims and child_ref.ndimsnormal == baseref.ndimsnormal for child_ref in child_refs) self.baseref = baseref self.child_transforms = baseref.child_transforms self.child_refs = child_refs - super().__init__(baseref.ndims) + super().__init__(baseref.ndims, baseref.ndimsnormal) def check_edges(self, tol=1e-15, print=print): super().check_edges(tol=tol, print=print) From 38eef353c94cf727e226ee8ec660283d70d47519 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Fri, 10 Jan 2020 23:30:58 +0100 Subject: [PATCH 036/119] WIP: wrap MosaicRef in WithManifoldEdgesRef --- nutils/element.py | 185 +++++++++++++++++++++++++++++++++++++++++++- nutils/topology.py | 22 ++++-- nutils/transform.py | 38 +++++++-- 3 files changed, 232 insertions(+), 13 deletions(-) diff --git a/nutils/element.py b/nutils/element.py index c41e8571a..5090c91f1 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -176,7 +176,7 @@ def trim(self, levels, maxrefine, ndivisions): else self.empty if numpy.less_equal(levels, 0).all() \ else self.with_children(cref.trim(clevels, maxrefine-1, ndivisions) for cref, clevels in zip(self.child_refs, self.child_divide(levels,maxrefine))) if maxrefine > 0 \ - else self.slice(lambda vertices: numeric.dot(numeric.poly_eval(self._linear_bernstein[_], vertices), levels), ndivisions) + else self.slice(lambda vertices: numeric.dot(numeric.poly_eval(self._linear_bernstein[_], vertices), levels), ndivisions).withmanifoldedges @property def _linear_bernstein(self): @@ -301,6 +301,13 @@ def get_poly_coeffs(self, basis, **kwargs): def get_edge_dofs(self, degree, iedge): raise NotImplementedError + @property + def withmanifoldedges(self): + if any(map(WithManifoldEdgesReference._ismanifold, self.edge_transforms)): + return WithManifoldEdgesReference(self) + else: + return self + strictreference = types.strict[Reference] class EmptyLike(Reference): @@ -1152,6 +1159,182 @@ def get_poly_coeffs(self, basis, **kwargs): def get_edge_dofs(self, degree, iedge): return self.baseref.get_edge_dofs(degree, iedge) +class WithManifoldEdgesReference(Reference): + + __slots__ = 'baseref' + __cache__ = 'edges' + + def __init__(self, baseref:strictreference): + self.baseref = baseref + super().__init__(baseref.ndims, baseref.ndimsnormal) + + def __bool__(self): + return bool(self.baseref) + + @property + def volume(self): + return self.baseref.volume + + @property + def vertices(self): + return self.baseref.vertices + + def nvertices_by_level(self, n): + return self.baseref.nvertices_by_level(n) + + def __and__(self, other): + if isinstance(other, WithManifoldEdgesReference): + other = other.baseref + result = self.baseref & other + return result if result is NotImplemented else result.withmanifoldedges + + __rand__ = __and__ + + def __or__(self, other): + if isinstance(other, WithManifoldEdgesReference): + other = other.baseref + result = self.baseref | other + return result if result is NotImplemented else result.withmanifoldedges + + __ror__ = __or__ + + def __sub__(self, other): + if isinstance(other, WithManifoldEdgesReference): + other = other.baseref + result = self.baseref - other + return result if result is NotImplemented else result.withmanifoldedges + + def __rsub__(self, other): + if isinstance(other, WithManifoldEdgesReference): + other = other.baseref + result = other - self.baseref + return result if result is NotImplemented else result.withmanifoldedges + + def getpoints(self, ischeme, degree): + return self.baseref.getpoints(ischeme, degree) + + def slice(self, levelfunc, ndivisions): + return self.baseref.slice(levelfunc, ndivisions).withmanifoldedges + + @property + def withmanifoldedges(self): + return self + + @property + def child_transforms(self): + return self.baseref.child_transforms + + @property + def child_refs(self): + return tuple(cref.withmanifoldedges for cref in self.baseref.child_refs) + + @classmethod + def _ismanifold(cls, etrans): + if isinstance(etrans, transform.ScaledUpdim): + return cls._ismanifold(etrans.trans2) + else: + return not isinstance(etrans, (transform.TensorEdge1, transform.TensorEdge2, transform.SimplexEdge)) + + @property + def edges(self): + edges = [] + for etrans, eref in self.baseref.edges: + if self._ismanifold(etrans): + eref = ManifoldReference(eref, etrans) + etrans = transform.Manifold(self.baseref.ndims, etrans) + edges.append((etrans, eref)) + return tuple(edges) + + @property + def edge_transforms(self): + return tuple(etrans for etrans, eref in self.edges) + + @property + def edge_refs(self): + return tuple(eref for etrans, eref in self.edges) + + def get_ndofs(self, degree): + return self.baseref.get_ndofs(degree) + + def get_poly_coeffs(self, basis, **kwargs): + return self.baseref.get_poly_coeffs(basis, **kwargs) + + def get_edge_dofs(self, degree, iedge): + return self.baseref.get_edge_dofs(degree, iedge) + + def inside(self, point, eps=0): + return self.baseref.inside(point, eps=eps) + +class ManifoldReference(Reference): + + __slots__ = 'ref', 'trans' + + @types.apply_annotations + def __init__(self, ref: strictreference, trans: transform.stricttransformitem): + self.ref = ref + self.trans = trans + assert self.ref.ndims + self.ref.ndimsnormal == self.trans.fromdims + super().__init__(self.ref.ndims, self.ref.ndimsnormal+trans.todims-trans.fromdims) + + def __bool__(self): + return bool(self.ref) + + def __and__(self, other): + if not isinstance(other, Reference): + return NotImplemented + if isinstance(other, ManifoldReference) and other.trans == self.trans: + return ManifoldReference(self.ref & other.ref, self.trans) + else: + return self.empty + + __rand__ = __and__ + + @property + def vertices(self): + verts = types.frozenarray(self.trans.apply(self.ref.vertices)) + assert len(verts) == len(self.ref.vertices) + return verts + + # def nvertices_by_level(self, n): + + @property + def child_transforms(self): + if isinstance(self.ref, OwnChildReference): + assert isinstance(self.trans, transform.ScaledUpdim) + return self.trans.trans1, + else: + assert self.ref.nchildren == 0 + return () + + @property + def child_refs(self): + if isinstance(self.ref, OwnChildReference): + assert isinstance(self.trans, transform.ScaledUpdim) + return ManifoldReference(self.ref.baseref, self.trans.trans2), + else: + assert self.ref.nchildren == 0 + return () + + @property + def edge_transforms(self): + return (transform.Identity(self.ndims+self.ndimsnormal),)*len(self.ref.edges) + + @property + def edge_refs(self): + return tuple(ManifoldReference(eref, self.trans*etrans) for etrans, eref in self.ref.edges) + + @property + def simplices(self): + return tuple((trans, ManifoldReference(simplex, self.trans)) for trans, simplex in self.ref.simplices) + + def getpoints(self, ischeme, degree): + return points.TransformPoints(self.ref.getpoints(ischeme, degree), self.trans) + + def inside(self, point, eps=0): + return self.ref.inside(self.trans.invapply(point), eps=0) + + def slice(self, levelfunc, ndivisions): + return ManifoldReference(self.ref.slice(lambda vertices: levelfunc(self.trans.apply(vertices)), ndivisions), self.trans) ## UTILITY FUNCTIONS diff --git a/nutils/topology.py b/nutils/topology.py index 65d473cee..8d9153c7e 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -1495,6 +1495,9 @@ def boundary(self): trimmedreferences = [] trimmedtransforms = [] trimmedopposites = [] + manifoldreferences = [] + manifoldtransforms = [] + manifoldopposites = [] for ielem, newref in enumerate(self.refs): if not newref: continue @@ -1524,18 +1527,27 @@ def boundary(self): # The last edges of newref (beyond the number of edges of the original) # cannot have opposites and are added to the trimmed group directly. for edgetrans, edgeref in newref.edges[len(ioppelems):]: - trimmedreferences.append(edgeref) - trimmedtransforms.append(elemtrans+(edgetrans,)) - trimmedopposites.append(elemtrans+(edgetrans.flipped,)) + if edgetrans.fromdims == edgetrans.todims: + manifoldreferences.append(edgeref) + manifoldtransforms.append(elemtrans+(edgetrans,)) + manifoldopposites.append(elemtrans+(edgetrans.flipped,)) + else: + trimmedreferences.append(edgeref) + trimmedtransforms.append(elemtrans+(edgetrans,)) + trimmedopposites.append(elemtrans+(edgetrans.flipped,)) origboundary = SubsetTopology(baseboundary, brefs) if isinstance(self.newboundary, Topology): trimmedbrefs = [ref.empty for ref in self.newboundary.references] for ref, trans in zip(trimmedreferences, trimmedtransforms): trimmedbrefs[self.newboundary.transforms.index(trans)] = ref + for ref, trans in zip(manifoldreferences, manifoldtransforms): + trimmedbrefs[self.newboundary.transforms.index(trans)] = ref trimboundary = SubsetTopology(self.newboundary, trimmedbrefs) else: root, = self.roots - trimboundary = Topology(self.roots, elementseq.asreferences(trimmedreferences, self.ndims-1), transformseq.PlainTransforms(trimmedtransforms, root.ndims, self.ndims-1), transformseq.PlainTransforms(trimmedopposites, root.ndims, self.ndims-1)) + trimmedboundary = Topology(self.roots, elementseq.asreferences(trimmedreferences, self.ndims-1), transformseq.PlainTransforms(trimmedtransforms, root.ndims, self.ndims-1), transformseq.PlainTransforms(trimmedopposites, root.ndims, self.ndims-1)) + manifoldboundary = Topology(self.roots, elementseq.asreferences(manifoldreferences, self.ndims-1), transformseq.PlainTransforms(manifoldtransforms, root.ndims, self.ndims), transformseq.PlainTransforms(manifoldopposites, root.ndims, self.ndims)) + trimboundary = DisjointUnionTopology([trimmedboundary, manifoldboundary]) return DisjointUnionTopology([trimboundary, origboundary], names=[self.newboundary] if isinstance(self.newboundary,str) else []) @property @@ -1697,7 +1709,7 @@ def boundary(self): for index in indices: for etrans, eref in level.references[index].edges: if eref: - trans = level.transforms[index]+(etrans,) + trans = transform.uppermost(level.transforms[index]+(etrans,)) try: bindices.append(bindex(trans)) except ValueError: diff --git a/nutils/transform.py b/nutils/transform.py index a07ab0ea9..addc4f2da 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -40,7 +40,7 @@ def canonical(chain): return tuple(chain) items = list(chain) i = 0 - while items[i].fromdims > items[n-1].fromdims: + while i < len(items)-1: swapped = items[i+1].swapdown(items[i]) if swapped: items[i:i+2] = swapped @@ -56,7 +56,7 @@ def uppermost(chain): return tuple(chain) items = list(chain) i = n - while items[i-1].todims < items[0].todims: + while i > 1: swapped = items[i-2].swapup(items[i-1]) if swapped: items[i-2:i] = swapped @@ -68,7 +68,7 @@ def uppermost(chain): def promote(chain, ndims): # swap transformations such that ndims is reached as soon as possible, and # then maintained as long as possible (i.e. proceeds as canonical). - for i, item in enumerate(chain): # NOTE possible efficiency gain using bisection + for i, item in reversed(tuple(enumerate(chain))): if item.fromdims == ndims: return canonical(chain[:i+1]) + uppermost(chain[i+1:]) return chain # NOTE at this point promotion essentially failed, maybe it's better to raise an exception @@ -426,15 +426,15 @@ def __init__(self, ndims, ichild): raise NotImplementedError('SimplexChild(ndims={}, ichild={})'.format(ndims, ichild)) super().__init__(linear, offset) -class ScaledUpdim(Updim): +class ScaledUpdim(Matrix): - __slots__ = 'trans1', 'trans2' + __slots__ = 'trans1', 'trans2', 'isflipped' def __init__(self, trans1, trans2): - assert trans1.todims == trans1.fromdims == trans2.todims == trans2.fromdims + 1 self.trans1 = trans1 self.trans2 = trans2 - super().__init__(numpy.dot(trans1.linear, trans2.linear), trans1.apply(trans2.offset), trans1.isflipped^trans2.isflipped) + self.isflipped = trans1.isflipped^trans2.isflipped + super().__init__(numpy.dot(trans1.linear, trans2.linear), trans1.apply(trans2.offset)) def swapup(self, other): if type(other) is Identity: @@ -448,6 +448,15 @@ def flipped(self): def det(self): return numpy.sqrt(numpy.linalg.det(numpy.einsum('ki,kj->ij', self.linear, self.linear))) if self.fromdims else 1 + @property + def ext(self): + ext = numeric.ext(self.linear) + return types.frozenarray(-ext if self.isflipped else ext, copy=False) + + def swapdown(self, other): + if isinstance(other, TensorChild): + return ScaledUpdim(other, self), Identity(self.fromdims) + class TensorEdge1(Updim): __slots__ = 'trans', @@ -553,4 +562,19 @@ def __init__(self, ndims:int, token): def __str__(self): return ':'.join(map(str, self._args)) +class Manifold(Identity): + + @types.apply_annotations + def __init__(self, ndims:types.strictint, trans:stricttransformitem): + self.trans = trans + super().__init__(ndims) + + @property + def flipped(self): + return Manifold(self.fromdims, self.trans.flipped) + + def swapdown(self, other): + if isinstance(other, TensorChild): + return ScaledUpdim(other, self), Identity(self.fromdims) + # vim:sw=2:sts=2:et From eb9c0e09ef8d90539da7e415e74efddde60471cf Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 28 Jan 2020 15:15:33 +0100 Subject: [PATCH 037/119] add TrimmedEdgesTransforms --- nutils/topology.py | 41 ++++++++++++++++++++--------------------- nutils/transformseq.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 21 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 8d9153c7e..a7ee21002 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -1492,12 +1492,16 @@ def boundary(self): baseboundary = self.basetopo.boundary baseconnectivity = self.basetopo.connectivity brefs = [ref.empty for ref in baseboundary.references] + trimmededges = {} + def addtrimmededge(ielem, etrans): + edges = trimmededges.setdefault(ielem, []) + assert etrans not in edges + iedge = len(edges) + edges.append(etrans) + return ielem, iedge trimmedreferences = [] trimmedtransforms = [] trimmedopposites = [] - manifoldreferences = [] - manifoldtransforms = [] - manifoldopposites = [] for ielem, newref in enumerate(self.refs): if not newref: continue @@ -1522,32 +1526,27 @@ def boundary(self): edgeref -= oppref.edge_refs[ioppedge] if edgeref: trimmedreferences.append(edgeref) - trimmedtransforms.append(elemtrans+(edgetrans,)) - trimmedopposites.append(self.basetopo.transforms[ioppelem]+(oppref.edge_transforms[ioppedge],)) + trimmedtransforms.append(addtrimmededge(ielem, edgetrans)) + trimmedopposites.append(addtrimmededge(ioppelem, oppref.edge_transforms[ioppedge])) # The last edges of newref (beyond the number of edges of the original) # cannot have opposites and are added to the trimmed group directly. for edgetrans, edgeref in newref.edges[len(ioppelems):]: - if edgetrans.fromdims == edgetrans.todims: - manifoldreferences.append(edgeref) - manifoldtransforms.append(elemtrans+(edgetrans,)) - manifoldopposites.append(elemtrans+(edgetrans.flipped,)) - else: - trimmedreferences.append(edgeref) - trimmedtransforms.append(elemtrans+(edgetrans,)) - trimmedopposites.append(elemtrans+(edgetrans.flipped,)) + trimmedreferences.append(edgeref) + trimmedtransforms.append(addtrimmededge(ielem, edgetrans)) + trimmedopposites.append(addtrimmededge(ielem, edgetrans.flipped)) + trimmedreferences = elementseq.asreferences(trimmedreferences, self.ndims-1) + trimmedielems, trimmededges = zip(*sorted(trimmededges.items(), key=lambda item: item[0])) + trimmedoffsets = dict(zip(trimmedielems, numpy.cumsum([0, *map(len, trimmededges)]))) + trimmededges = transformseq.TrimmedEdgesTransforms(self.basetopo.transforms[numpy.asarray(trimmedielems)], trimmededges) + trimmedtransforms = trimmededges[numpy.fromiter((trimmedoffsets[ielem]+iedge for ielem, iedge in trimmedtransforms), dtype=int)] + trimmedopposites = trimmededges[numpy.fromiter((trimmedoffsets[ielem]+iedge for ielem, iedge in trimmedopposites), dtype=int)] + trimboundary = Topology(self.roots, trimmedreferences, trimmedtransforms, trimmedopposites) origboundary = SubsetTopology(baseboundary, brefs) if isinstance(self.newboundary, Topology): trimmedbrefs = [ref.empty for ref in self.newboundary.references] - for ref, trans in zip(trimmedreferences, trimmedtransforms): - trimmedbrefs[self.newboundary.transforms.index(trans)] = ref - for ref, trans in zip(manifoldreferences, manifoldtransforms): + for ref, trans in zip(trimboundary.references, trimboundary.transforms): trimmedbrefs[self.newboundary.transforms.index(trans)] = ref trimboundary = SubsetTopology(self.newboundary, trimmedbrefs) - else: - root, = self.roots - trimmedboundary = Topology(self.roots, elementseq.asreferences(trimmedreferences, self.ndims-1), transformseq.PlainTransforms(trimmedtransforms, root.ndims, self.ndims-1), transformseq.PlainTransforms(trimmedopposites, root.ndims, self.ndims-1)) - manifoldboundary = Topology(self.roots, elementseq.asreferences(manifoldreferences, self.ndims-1), transformseq.PlainTransforms(manifoldtransforms, root.ndims, self.ndims), transformseq.PlainTransforms(manifoldopposites, root.ndims, self.ndims)) - trimboundary = DisjointUnionTopology([trimmedboundary, manifoldboundary]) return DisjointUnionTopology([trimboundary, origboundary], names=[self.newboundary] if isinstance(self.newboundary,str) else []) @property diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 6f6fa1de3..bf00a8fed 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -804,6 +804,47 @@ def index_with_tail(self, trans): iderived = self._derived_transforms.index(tail[0]) return iparent*len(self._derived_transforms) + iderived, tail[1:] +class TrimmedEdgesTransforms(Transforms): + + __slots__ = '_parent', '_edges' + __cache__ = '_offsets' + + @types.apply_annotations + def __init__(self, parent:stricttransforms, edges:types.tuple[types.tuple[transform.stricttransformitem]]): + assert len(edges) == len(parent) + self._parent = parent + self._edges = edges + super().__init__(parent.todims) + + @property + def _offsets(self): + return types.frozenarray(numpy.cumsum([0, *map(len, self._edges)]), copy=False) + + def __len__(self): + return self._offsets[-1] + + def __iter__(self): + for ptrans, edges in zip(self._parent, self._edges): + for etrans in edges: + yield ptrans+(etrans,) + + def __getitem__(self, index): + if not numeric.isint(index): + return super().__getitem__(index) + index = numeric.normdim(len(self), index) + iparent = numpy.searchsorted(self._offsets, index, side='right')-1 + assert 0 <= iparent < len(self._offsets)-1 + iedge = index - self._offsets[iparent] + return self._parent[iparent] + (self._edges[iparent][iedge],) + + def index_with_tail(self, trans): + iparent, tail = self._parent.index_with_tail(trans) + if not tail: + raise ValueError + tail = transform.canonical(tail) + iedge = self._edges[iparent].index(tail[0]) + return self._offsets[iparent]+iedge, tail[1:] + class ProductTransforms(Transforms): '''The product of two :class:`Transforms` objects. From ebe9b52c45cc66332a6d32db615841f6d8b231a8 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 18 Feb 2020 13:24:11 +0100 Subject: [PATCH 038/119] WIP --- nutils/element.py | 18 ++-- nutils/function.py | 107 ++++++++++++------- nutils/mesh.py | 19 ++-- nutils/points.py | 46 ++++++--- nutils/topology.py | 109 ++++++++++---------- nutils/transform.py | 127 +++++++++++++++++++++-- nutils/transformseq.py | 171 ++++++++++++++++++------------- tests/test_function.py | 5 +- tests/test_mesh.py | 6 +- tests/test_topology.py | 10 +- tests/test_transformseq.py | 204 ++++++++++++++++++------------------- 11 files changed, 510 insertions(+), 312 deletions(-) diff --git a/nutils/element.py b/nutils/element.py index 5090c91f1..e7ab6a465 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -265,27 +265,31 @@ def check_edges(self, tol=1e-15, print=print): if numpy.greater(abs(volume - self.volume), tol).any(): print('divergence check failed: {} != {}'.format(volume, self.volume)) - def vertex_cover(self, ctransforms, maxrefine): + def vertex_cover(self, ctransforms, maxrefine, todims): if maxrefine < 0: raise Exception('maxrefine is too low') npoints = self.nvertices_by_level(maxrefine) allindices = numpy.arange(npoints) if len(ctransforms) == 1: ctrans, = ctransforms - assert not ctrans - return ((), self.getpoints('vertex', maxrefine).coords, allindices), + assert not any(ctrans) + return (ctrans, self.getpoints('vertex', maxrefine).coords, allindices), if maxrefine == 0: raise Exception('maxrefine is too low') cbins = [set() for ichild in range(self.nchildren)] for ctrans in ctransforms: - ichild = self.child_transforms.index(ctrans[0]) - cbins[ichild].add(ctrans[1:]) + for ichild, child in enumerate(self.child_transforms): + if child.separate(todims) == tuple(t[0] for t in ctrans): + break + else: + raise ValueError('child not found') + cbins[ichild].add(tuple(t[1:] for t in ctrans)) if not all(cbins): raise Exception('transformations to not form an element cover') fcache = cache.WrapperCache() - return tuple(((ctrans,) + trans, points, cindices[indices]) + return tuple((tuple((a,)+b for a, b in zip(ctrans.separate(todims), trans)), points, cindices[indices]) for ctrans, cref, cbin, cindices in zip(self.child_transforms, self.child_refs, cbins, self.child_divide(allindices,maxrefine)) - for trans, points, indices in fcache[cref.vertex_cover](frozenset(cbin), maxrefine-1)) + for trans, points, indices in fcache[cref.vertex_cover](frozenset(cbin), maxrefine-1, todims)) def __str__(self): return self.__class__.__name__ diff --git a/nutils/function.py b/nutils/function.py index 2489f8b61..1f7b31f27 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -107,11 +107,9 @@ class Subsample: __slots__ = 'roots', 'transforms', 'points', 'ielem' - def __init__(self, roots: types.tuple[strictroot], transforms: types.tuple[transform.stricttransform], points: points.strictpoints, ielem: types.strictint = None): - if len(roots) != 1: - raise NotImplementedError + def __init__(self, roots: types.tuple[strictroot], transforms: types.tuple[types.tuple[transform.stricttransform]], points: points.strictpoints, ielem: types.strictint = None): self.roots = roots - self.transforms = tuple((chain,) for chain in transforms) + self.transforms = transforms self.points = points self.ielem = ielem @@ -470,7 +468,7 @@ def evalf(self, subsamples): break else: raise ValueError('no such root: {!r}'.format(root)) - return tuple(trans) + return tuple(trans), tuple(t[0].todims for t in trans), tuple(t[-1].fromdims for t in trans) @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): @@ -478,14 +476,17 @@ def prepare_eval(self, *, opposite=False, kwargs=...): class EmptyTransformChain(TransformChain): - __slots__ = () + __slots__ = '_todims', '_fromdims' @types.apply_annotations - def __init__(self, roots:types.tuple[strictroot], todims:types.strictint=None, fromdims:types.strictint=None): - super().__init__(roots=roots, args=[], todims=todims, fromdims=fromdims) + def __init__(self, roots:types.tuple[strictroot], todims:types.tuple[types.strictint], fromdims:types.tuple[types.strictint]): + assert len(roots) == len(todims) == len(fromdims) + self._todims = todims + self._fromdims = fromdims + super().__init__(roots=roots, args=[], todims=builtins.sum(todims), fromdims=builtins.sum(fromdims)) def evalf(self): - return () + return ((),)*len(self.roots), self._todims, self._fromdims class TransformChainFromTuple(TransformChain): @@ -515,11 +516,11 @@ def roots(self): return self._trans.roots def evalf(self, chains): - if len(chains) != 1: - raise NotImplementedError - trans, = chains - index, tail = self._transforms.index_with_tail(trans) - return numpy.array(index)[None], (tail,) + chains, todims, fromdims = chains + index, tails = self._transforms.index_with_tail(chains) + tailtodims = tuple(t[0].todims if t else n for n, t in zip(fromdims, tails)) + assert builtins.sum(tailtodims) == self._fromdims + return numpy.array(index)[None], (tails, tailtodims, fromdims) def __len__(self): return 3 @@ -553,7 +554,8 @@ def __init__(self, roots:types.tuple[strictroot], transforms:transformseq.strict def evalf(self, index): index, = index - return self.transforms[index][1:] + trans = self.transforms[index] + return trans, tuple(t[0].todims for t in trans), tuple(t[-1].fromdims for t in trans) # ARRAYFUNC # @@ -786,39 +788,57 @@ class RootBasis(Array): trans : :class:`TransformChain` ''' - __slots__ = '_root', '_ndimstangent', '_opposite', '_trans' + __slots__ = '_roots', '_ndimstangent', '_opposite', '_trans' @types.apply_annotations def __init__(self, roots:types.tuple[strictroot], ndimstangent:types.strictint, trans:types.strict[TransformChain], _opposite:bool=None): # NOTE: `trans` is only required because of the `SelectChain` test in `Opposite.simplified`. - self._root, = roots + self._roots = roots self._ndimstangent = ndimstangent self._opposite = _opposite self._trans = trans - super().__init__(args=[EVALARGS, trans], shape=[self._root.ndims, self._root.ndims], dtype=float) + assert self._trans.ordered_roots == roots + ndims = sum(root.ndims for root in roots) + super().__init__(args=[EVALARGS, trans], shape=[ndims, ndims], dtype=float) def evalf(self, evalargs, _trans): - trans = evalargs['_transforms'][1 if self._opposite else 0] + mtrans = evalargs['_transforms'][1 if self._opposite else 0] points = evalargs['_points'] - ndims = self._root.ndims - assert trans == _trans - assert (trans[0].todims if trans else points.ndims) == ndims + ndims = self.shape[0] + assert mtrans == _trans[0] if points.ndimsmanifold != self._ndimstangent: raise ValueError('expected a {}D tangent space, but got a {}D space'.format(self._ndimstangent, points.ndimstangent)) - linear = numpy.empty((points.npoints, ndims, ndims), dtype=float) - translinear = transform.linearfrom(trans, ndims) - numpy.einsum('ij,njk->nik', translinear[:,:points.ndims], points.basis, out=linear[:,:,:points.ndims]) - if points.ndims < ndims: - linear[:,:,points.ndims:] = translinear[_,:,points.ndims:] + linear = numpy.zeros((points.npoints, ndims, ndims), dtype=float) + mtranslinear = numpy.zeros((ndims, points.ndims), dtype=float) + to0 = from0 = 0 + n0 = points.ndims + for root, trans in zip(self._roots, mtrans): + to1 = to0 + root.ndims + fromdims = trans[-1].fromdims if trans else root.ndims + translinear = transform.linearfrom(trans, root.ndims) + if fromdims: + from1 = from0 + fromdims + mtranslinear[to0:to1,from0:from1] = translinear[:,:fromdims] + from0 = from1 + if fromdims < root.ndims: + n1 = n0 + root.ndims - fromdims + linear[:,to0:to1,n0:n1] = translinear[:,fromdims:] + n0 = n1 + to0 = to1 + assert to0 == self.shape[0] + assert from0 == points.ndims + assert n0 == self.shape[0] + + numpy.einsum('ij,njk->nik', mtranslinear, points.basis, out=linear[:,:,:points.ndims]) return linear @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): if self._opposite is not None: raise ValueError('prepare already called') - return RootBasis((self._root,), self._ndimstangent, self._trans.prepare_eval(opposite=opposite, **kwargs), _opposite=opposite) + return RootBasis(self._roots, self._ndimstangent, self._trans.prepare_eval(opposite=opposite, **kwargs), _opposite=opposite) class GramSchmidt(Array): @@ -1308,7 +1328,7 @@ def evalf(self, subsamples, chains): result = numpy.zeros((*(subsample.npoints for subsample in subsamples), self.shape[0]), dtype=float) to0 = 0 - for root, chain in zip(self._tail.ordered_roots, chains): + for root, (chain, chaintodims, chainfromdims) in zip(self._tail.ordered_roots, chains): to1 = to0 + (chain[0].todims if chain else slices[root].stop - slices[root].start) isubsample = isubsamples[root] expand = tuple(slice(None) if i == isubsample else numpy.newaxis for i in range(len(subsamples))) @@ -1321,26 +1341,35 @@ def _derivative(self, var, seen): if isinstance(var, RootCoords) and var.root in self.roots: if self._head.fromdims != builtins.sum(root.ndims for root in self.roots): raise NotImplementedError('transform contains updims') - to0 = 0 - for root in self._head.ordered_roots: - to1 = to0 + root.ndims - if root == var.root: - return Inverse(Linear(self._head, self._head.fromdims))[:,to0:to1] - to0 = to1 - raise Exception + return Inverse(Linear(self._head, len(var))) return zeros(self.shape+var.shape) class Linear(Array): __slots__ = '_trans' + __cache__ = 'simplified' @types.apply_annotations def __init__(self, trans:types.strict[TransformChain], fromdims:types.strictint, todims:types.strictint=None): self._trans = trans super().__init__(args=[trans], shape=(todims or trans.todims, fromdims), dtype=float) - def evalf(self, chain): - return transform.linear(chain, self.shape[0])[_] + def evalf(self, chains): + result = numpy.zeros((1, *self.shape), dtype=float) + to0 = from0 = 0 + for chain, todims, fromdims in zip(*chains): + to1, from1 = to0 + todims, from0 + fromdims + result[:,to0:to1,from0:from1] = transform.linear(chain, todims) + to0, from0 = to1, from1 + assert (to0, from0) == self.shape + return result + + @property + def simplified(self): + if isinstance(self._trans, EmptyTransformChain) and self._trans.todims == self._trans.fromdims: + return eye(self._trans.todims) + else: + return self class Inverse(Array): ''' @@ -4276,7 +4305,7 @@ def blocks(arg): def rootcoords(roots): if isinstance(roots, Root): - return ApplyTransforms(EmptyTransformChain(roots=(roots,), todims=roots.ndims, fromdims=roots.ndims), SelectChain((roots,))) + return ApplyTransforms(EmptyTransformChain(roots=(roots,), todims=(roots.ndims,), fromdims=(roots.ndims,)), SelectChain((roots,))) else: return concatenate([rootcoords(root) for root in roots], axis=0) diff --git a/nutils/mesh.py b/nutils/mesh.py index 216a0dc25..8450aba2c 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -559,9 +559,9 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): ioppelem = connectivity[ielem, iedge] simplices, transforms, opposites = bitems if ioppelem == -1 else iitems simplices.append(tuple(nodes[ielem][:iedge])+tuple(nodes[ielem][iedge+1:])) - transforms.append(topo.transforms[ielem] + (transform.SimplexEdge(ndims, iedge),)) + transforms.append(topo.transforms[ielem][0] + (transform.SimplexEdge(ndims, iedge),)) if opposites is not None: - opposites.append(topo.transforms[ioppelem] + (transform.SimplexEdge(ndims, tuple(connectivity[ioppelem]).index(ielem)),)) + opposites.append(topo.transforms[ioppelem][0] + (transform.SimplexEdge(ndims, tuple(connectivity[ioppelem]).index(ielem)),)) for groups, (simplices, transforms, opposites) in (bgroups, bitems), (igroups, iitems): if simplices: transforms = transformseq.PlainTransforms(transforms, ndims, ndims-1) @@ -573,7 +573,7 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): ptrans = [transform.Matrix(linear=numpy.zeros(shape=(ndims,0)), offset=offset) for offset in numpy.eye(ndims+1)[:,1:]] pmap = {inode: numpy.array(numpy.equal(nodes, inode).nonzero()).T for inode in set.union(*map(set, ptags.values()))} for pname, inodes in ptags.items(): - ptransforms = transformseq.PlainTransforms([topo.transforms[ielem] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode]], ndims, 0) + ptransforms = transformseq.PlainTransforms([topo.transforms[ielem][0] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode]], ndims, 0) preferences = elementseq.asreferences([element.getsimplex(0)], 0)*len(ptransforms) pgroups[pname] = topology.Topology((root,), preferences, ptransforms, ptransforms) @@ -607,9 +607,9 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): else: continue simplices.append(tuple(nodes[ielem][:iedge])+tuple(nodes[ielem][iedge+1:])) - transforms.append(topo.transforms[ielem] + (transform.SimplexEdge(ndims, iedge),)) + transforms.append(topo.transforms[ielem][0] + (transform.SimplexEdge(ndims, iedge),)) if ioppelem != -1: - opposites.append(topo.transforms[ioppelem] + (transform.SimplexEdge(ndims, ioppedge),)) + opposites.append(topo.transforms[ioppelem][0] + (transform.SimplexEdge(ndims, ioppedge),)) for groups, (simplices, transforms, opposites) in (vbgroups, bitems), (vigroups, iitems): if simplices: transforms = transformseq.PlainTransforms(transforms, ndims, ndims-1) @@ -617,7 +617,7 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): groups[bname] = topology.SimplexTopology(root, simplices, transforms, opposites) vpgroups = {} for pname, inodes in ptags.items(): - ptransforms = transformseq.PlainTransforms([topo.transforms[ielem] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode] if keep[ielem]], ndims, 0) + ptransforms = transformseq.PlainTransforms([topo.transforms[ielem][0] + (ptrans[ivertex],) for inode in inodes for ielem, ivertex in pmap[inode] if keep[ielem]], ndims, 0) preferences = elementseq.asreferences([element.getsimplex(0)], 0)*len(ptransforms) vpgroups[pname] = topology.Topology((root,), preferences, ptransforms, ptransforms) vgroups[name] = vtopo.withgroups(bgroups=vbgroups, igroups=vigroups, pgroups=vpgroups) @@ -660,7 +660,6 @@ def unitsquare(nelems, etype): ''' root = function.Root('unitsquare', 2) - idtrans = transform.Identifier(2, 'unitsquare') if etype == 'square': topo = topology.StructuredTopology(root, [transformseq.DimAxis(0, nelems, False)] * 2) @@ -672,19 +671,19 @@ def unitsquare(nelems, etype): v = numpy.arange(nelems+1, dtype=float) coords = numeric.meshgrid(v, v).reshape(2,-1).T - transforms = transformseq.PlainTransforms([(idtrans, transform.Square((c[1:]-c[0]).T, c[0])) for c in coords[simplices]], 2, 2) + transforms = transformseq.PlainTransforms([(transform.Square((c[1:]-c[0]).T, c[0]),) for c in coords[simplices]], 2, 2) topo = topology.SimplexTopology(root, simplices, transforms, transforms) if etype == 'mixed': references = list(topo.references) - transforms = list(topo.transforms) + transforms = list(trans[0] for trans in topo.transforms) square = element.getsimplex(1)**2 connectivity = list(topo.connectivity) isquares = [i * nelems + j for i in range(nelems) for j in range(nelems) if i%2==j%3] for n in sorted(isquares, reverse=True): i, j = divmod(n, nelems) references[n*2:(n+1)*2] = square, - transforms[n*2:(n+1)*2] = (idtrans, transform.Shift([float(i),float(j)])), + transforms[n*2:(n+1)*2] = (transform.Shift([float(i),float(j)]),), connectivity[n*2:(n+1)*2] = numpy.concatenate(connectivity[n*2:(n+1)*2])[[3,2,4,1] if i%2==j%2 else [3,2,0,5]], connectivity = [c-numpy.greater(c,n*2) for c in connectivity] topo = topology.ConnectedTopology((root,), elementseq.asreferences(references, 2), transformseq.PlainTransforms(transforms, 2, 2),transformseq.PlainTransforms(transforms, 2, 2), tuple(types.frozenarray(c, copy=False) for c in connectivity)) diff --git a/nutils/points.py b/nutils/points.py index 1b030b321..db4d29d2f 100644 --- a/nutils/points.py +++ b/nutils/points.py @@ -36,23 +36,34 @@ class Points(types.Singleton): The :class:`Points` base class bundles point coordinates, point weights, a local triangulation and hull triangulation. Of these only the coordinates are mandatory, and should be provided by the derived class in the form of the - ``coords`` attribute. Of the remaining properties only :func:`hull` has a - functional base implementation that relies on the availability of ``tri``. + :attr:`coords` attribute. Of the remaining properties only :meth:`hull` has a + functional base implementation that relies on the availability of :meth:`tri`. - .. attribute:: coords - - Coordinates of the points as a :class:`float` array. - - .. attribute:: weights - - Weights of the points as a :class:`float` array. - - Args - ---- + Parameters + ---------- npoints : :class:`int` Number of discrete points. ndims : :class:`int` - Number of spatial dimensions. + Dimension of the coordinates. + ndimsnormal : :class:`int` + Dimension of the normal space. This is zero unless the points describe a + manifold. + + Attributes + ---------- + coords : :class:`numpy.ndarray`, shape: [:attr:`npoints`, :attr:`ndims`] + Coordinates of the points. + weights : :class:`numpy.ndarray`, shape: [:attr:`npoints`] + Weights of the points. + npoints : :class:`int` + Number of points. + ndims : :class:`int` + Dimension of the :attr:`coords`. This is always the sum of + :attr:`ndimsmanifold` and :attr:`ndimsnormal`. + ndimsmanifold : :class:`int` + Dimension of the manifold space. + ndimsnormal : :class:`int` + Dimension of the normal space. ''' __cache__ = 'hull', 'onhull', 'basis' @@ -107,6 +118,15 @@ def onhull(self): @property def basis(self): + '''An orthonormal basis for the tangent and normal space per point. + + The first :attr:`ndimsnormal` vectors of the basis space the tangent space + (``basis[:.:,:ndimsnormal]``), the remainder spans the normal space. The + basis is always the identity matrix per point if the normal space has + dimension zero. + + :type: :class:`numpy.ndarray`, shape: [:attr:`npoints`, :attr:`ndims`, :attr:`ndims`] + ''' if self.ndimsnormal == 0: return types.frozenarray(numpy.eye(self.ndims)[numpy.newaxis], dtype=float, copy=False) else: diff --git a/nutils/topology.py b/nutils/topology.py index a7ee21002..fd4091590 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -48,8 +48,12 @@ class Topology(types.Singleton): @types.apply_annotations def __init__(self, roots:types.tuple[function.strictroot], references:elementseq.strictreferences, transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms): assert len(references) == len(transforms) == len(opposites) - if len(roots) != 1: - raise NotImplementedError + if len(set(roots)) < len(roots): + raise ValueError('duplicate roots: {!r}'.format(roots)) + if transforms.todims != tuple(root.ndims for root in roots): + raise ValueError("The todims of 'transforms' does not match the ndims for 'roots'.") + if opposites.todims != tuple(root.ndims for root in roots): + raise ValueError("The todims of 'opposites' does not match the ndims for 'roots'.") self.roots = roots self.references = references self.transforms = transforms @@ -86,7 +90,7 @@ def __invert__(self): return OppositeTopology(self) def __or__(self, other): - assert isinstance(other, Topology) and other.roots == self.roots and other.ndims == self.ndims + assert isinstance(other, Topology) and other.ndims == self.ndims return other if not self \ else self if not other \ else NotImplemented if isinstance(other, UnionTopology) \ @@ -95,8 +99,6 @@ def __or__(self, other): __ror__ = lambda self, other: self.__or__(other) def __and__(self, other): - if not isinstance(other, Topology) or self.roots != other.roots: - return NotImplemented keep_self = numpy.array(list(map(other.transforms.contains_with_tail, self.transforms)), dtype=bool) if keep_self.all(): return self @@ -108,10 +110,9 @@ def __and__(self, other): # The last condition is to avoid duplicate elements. Note that we could # have reused the result of an earlier lookup to avoid a new (using index # instead of contains) but we choose to trade some speed for simplicity. - root, = self.roots references = elementseq.chain([self.references[ind_self], other.references[ind_other]], self.ndims) - transforms = transformseq.chain([self.transforms[ind_self], other.transforms[ind_other]], root.ndims) - opposites = transformseq.chain([self.opposites[ind_self], other.opposites[ind_other]], root.ndims) + transforms = transformseq.chain([self.transforms[ind_self], other.transforms[ind_other]], tuple(root.ndims for root in self.roots)) + opposites = transformseq.chain([self.opposites[ind_self], other.opposites[ind_other]], tuple(root.ndims for root in self.roots)) return Topology(self.roots, references, transforms, opposites) __rand__ = lambda self, other: self.__and__(other) @@ -120,11 +121,11 @@ def __add__(self, other): return self | other def __sub__(self, other): - assert isinstance(other, Topology) and self.roots == other.roots and other.ndims == self.ndims + assert isinstance(other, Topology) and other.ndims == self.ndims return other.__rsub__(self) def __rsub__(self, other): - assert isinstance(other, Topology) and self.roots == other.roots and other.ndims == self.ndims + assert isinstance(other, Topology) and other.ndims == self.ndims return other - other.subset(self, newboundary=getattr(self,'boundary',None)) def __mul__(self, other): @@ -361,13 +362,14 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None with log.iter.percentage('trimming', self.references, self.transforms, bins) as items: for ielem, (ref, trans, ctransforms) in enumerate(items): levels = numpy.empty(ref.nvertices_by_level(maxrefine)) - cover = list(fcache[ref.vertex_cover](frozenset(ctransforms), maxrefine)) + todims = tuple(t[-1].fromdims for t in trans) + cover = list(fcache[ref.vertex_cover](frozenset(ctransforms), maxrefine, todims)) # confirm cover and greedily optimize order mask = numpy.ones(len(levels), dtype=bool) while mask.any(): imax = numpy.argmax([mask[indices].sum() for tail, cpoints, indices in cover]) tail, cpoints, indices = cover.pop(imax) - levels[indices] = levelset.eval(function.Subsample(roots=self.roots, transforms=(trans + tail,), points=points.CoordsPoints(cpoints), ielem=ielem), **arguments) + levels[indices] = levelset.eval(function.Subsample(roots=self.roots, transforms=(tuple(a+b for a, b in zip(trans, tail)),), points=points.CoordsPoints(cpoints), ielem=ielem), **arguments) mask[indices] = False refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) log.debug('cache', fcache.stats) @@ -375,8 +377,6 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None def subset(self, topo, newboundary=None, strict=False): 'intersection' - if topo.roots != self.roots: - raise ValueError('different roots') refs = [ref.empty for ref in self.references] for ref, trans in zip(topo.references, topo.transforms): try: @@ -706,7 +706,7 @@ def __init__(self, basetopo:stricttopology, vgroups:types.frozendict={}, bgroups self.igroups = igroups self.pgroups = pgroups super().__init__(basetopo.roots, basetopo.references, basetopo.transforms, basetopo.opposites) - assert all(topo is Ellipsis or isinstance(topo, str) or isinstance(topo, Topology) and topo.roots == basetopo.roots and topo.ndims == basetopo.ndims for topo in self.vgroups.values()) + assert all(topo is Ellipsis or isinstance(topo, str) or isinstance(topo, Topology) and topo.ndims == basetopo.ndims for topo in self.vgroups.values()) def __len__(self): return len(self.basetopo) @@ -796,8 +796,7 @@ class EmptyTopology(Topology): @types.apply_annotations def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint): - root, = roots - super().__init__(roots, elementseq.EmptyReferences(ndims), transformseq.EmptyTransforms(root.ndims), transformseq.EmptyTransforms(root.ndims)) + super().__init__(roots, elementseq.EmptyReferences(ndims), transformseq.EmptyTransforms(tuple(root.ndims for root in roots)), transformseq.EmptyTransforms(tuple(root.ndims for root in roots))) def __or__(self, other): assert self.ndims == other.ndims @@ -813,17 +812,16 @@ class Point(Topology): @types.aspreprocessor @types.apply_annotations - def _preprocess_init(self, roots:types.tuple[function.strictroot], trans:transform.stricttransform, opposite:transform.stricttransform=None): - return (self, roots, trans, trans if opposite is None else opposite), {} + def _preprocess_init(self, root:function.strictroot, trans:transform.stricttransform, opposite:transform.stricttransform=None): + return (self, root, trans, trans if opposite is None else opposite), {} @_preprocess_init - def __init__(self, roots, trans, opposite): + def __init__(self, root, trans, opposite): assert trans[-1].fromdims == 0 - root, = roots references = elementseq.asreferences([element.getsimplex(0)], 0) transforms = transformseq.PlainTransforms((trans,), root.ndims, 0) opposites = transforms if opposite is None else transformseq.PlainTransforms((opposite,), root.ndims, 0) - super().__init__(roots, references, transforms, opposites) + super().__init__((root,), references, transforms, opposites) def StructuredLine(root:function.strictroot, i:types.strictint, j:types.strictint, periodic:bool=False, bnames:types.tuple[types.strictstr]=None): if bnames is None: @@ -1380,12 +1378,11 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st assert len(set(self._topos[itopo].opposites[itrans] for itopo, itrans in indices)) == 1 selections = tuple(map(types.frozenarray[int], selections)) - root, = roots super().__init__( roots, elementseq.asreferences(references, ndims), - transformseq.chain((topo.transforms[selection] for topo, selection in zip(topos, selections)), root.ndims), - transformseq.chain((topo.opposites[selection] for topo, selection in zip(topos, selections)), root.ndims)) + transformseq.chain((topo.transforms[selection] for topo, selection in zip(topos, selections)), tuple(root.ndims for root in roots)), + transformseq.chain((topo.opposites[selection] for topo, selection in zip(topos, selections)), tuple(root.ndims for root in roots))) def getitem(self, item): topos = [topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)] @@ -1413,12 +1410,11 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st roots = self._topos[0].roots ndims = self._topos[0].ndims assert all(topo.roots == roots and topo.ndims == ndims for topo in self._topos) - root, = roots super().__init__( roots, elementseq.chain((topo.references for topo in self._topos), ndims), - transformseq.chain((topo.transforms for topo in self._topos), root.ndims), - transformseq.chain((topo.opposites for topo in self._topos), root.ndims)) + transformseq.chain((topo.transforms for topo in self._topos), tuple(root.ndims for root in roots)), + transformseq.chain((topo.opposites for topo in self._topos), tuple(root.ndims for root in roots))) def getitem(self, item): topos = [topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)] @@ -1516,7 +1512,7 @@ def addtrimmededge(ielem, etrans): if ioppelem == -1: # If the edge had no opposite in basetopology then it must already by # in baseboundary, so we can use index to locate it. - brefs[baseboundary.transforms.index(elemtrans+(edgetrans,))] = edgeref + brefs[baseboundary.transforms.index(transform.append_edge(elemtrans, edgetrans))] = edgeref else: # If the edge did have an opposite in basetopology then there is a # possibility this opposite (partially) disappeared, in which case @@ -1525,15 +1521,18 @@ def addtrimmededge(ielem, etrans): oppref = self.refs[ioppelem] edgeref -= oppref.edge_refs[ioppedge] if edgeref: + elemfromdims = tuple(t[-1].fromdims for t in elemtrans) + oppelemfromdims = tuple(t[-1].fromdims for t in self.basetopo.transforms[ioppelem]) trimmedreferences.append(edgeref) - trimmedtransforms.append(addtrimmededge(ielem, edgetrans)) - trimmedopposites.append(addtrimmededge(ioppelem, oppref.edge_transforms[ioppedge])) + trimmedtransforms.append(addtrimmededge(ielem, edgetrans.separate(elemfromdims))) + trimmedopposites.append(addtrimmededge(ioppelem, oppref.edge_transforms[ioppedge].separate(oppelemfromdims))) # The last edges of newref (beyond the number of edges of the original) # cannot have opposites and are added to the trimmed group directly. for edgetrans, edgeref in newref.edges[len(ioppelems):]: + elemfromdims = tuple(t[-1].fromdims for t in elemtrans) trimmedreferences.append(edgeref) - trimmedtransforms.append(addtrimmededge(ielem, edgetrans)) - trimmedopposites.append(addtrimmededge(ielem, edgetrans.flipped)) + trimmedtransforms.append(addtrimmededge(ielem, edgetrans.separate(elemfromdims))) + trimmedopposites.append(addtrimmededge(ielem, edgetrans.flipped.separate(elemfromdims))) trimmedreferences = elementseq.asreferences(trimmedreferences, self.ndims-1) trimmedielems, trimmededges = zip(*sorted(trimmededges.items(), key=lambda item: item[0])) trimmedoffsets = dict(zip(trimmedielems, numpy.cumsum([0, *map(len, trimmededges)]))) @@ -1647,8 +1646,7 @@ def __init__(self, basetopo:stricttopology, indices_per_level:types.tuple[types. opposites.append(level.opposites[indices]) self.levels = tuple(levels) - root, = basetopo.roots - super().__init__(basetopo.roots, elementseq.chain(references, basetopo.ndims), transformseq.chain(transforms, root.ndims), transformseq.chain(opposites, root.ndims)) + super().__init__(basetopo.roots, elementseq.chain(references, basetopo.ndims), transformseq.chain(transforms, tuple(root.ndims for root in basetopo.roots)), transformseq.chain(opposites, tuple(root.ndims for root in basetopo.roots))) def getitem(self, item): itemtopo = self.basetopo.getitem(item) @@ -1677,7 +1675,7 @@ def refined_by(self, refine): coarse_indices = tuple(map(indices_per_level[ilevel].pop, reversed(refine[start:stop]-self._offsets[ilevel]))) coarse_transforms = map(coarse.transforms.__getitem__, coarse_indices) coarse_references = map(coarse.references.__getitem__, coarse_indices) - fine_transforms = (trans+(ctrans,) for trans, ref in zip(coarse_transforms, coarse_references) for ctrans, cref in ref.children if cref) + fine_transforms = itertools.chain.from_iterable(map(transform.unempty_child_transforms, coarse_transforms, coarse_references)) indices_per_level[ilevel+1].extend(map(fine.transforms.index, fine_transforms)) if not indices_per_level[-1]: indices_per_level.pop(-1) @@ -1691,7 +1689,7 @@ def refined(self): coarse, fine = fine, fine.refined coarse_transforms = map(coarse.transforms.__getitem__, coarse_indices) coarse_references = map(coarse.references.__getitem__, coarse_indices) - fine_transforms = (trans+(ctrans,) for trans, ref in zip(coarse_transforms, coarse_references) for ctrans, cref in ref.children if cref) + fine_transforms = itertools.chain.from_iterable(map(transform.unempty_child_transforms, coarse_transforms, coarse_references)) refined_indices_per_level.append(numpy.unique(numpy.fromiter(map(fine.transforms.index, fine_transforms), dtype=int))) return HierarchicalTopology(self.basetopo, refined_indices_per_level) @@ -1706,13 +1704,12 @@ def boundary(self): bindex = blevel.transforms.index bindices = [] for index in indices: - for etrans, eref in level.references[index].edges: - if eref: - trans = transform.uppermost(level.transforms[index]+(etrans,)) - try: - bindices.append(bindex(trans)) - except ValueError: - pass + for trans in transform.unempty_edge_transforms(level.transforms[index], level.references[index]): + trans = tuple(map(transform.uppermost, trans)) + try: + bindices.append(bindex(trans)) + except ValueError: + pass bindices = numpy.array(bindices, dtype=int) if len(bindices) > 1: bindices.sort() @@ -1732,12 +1729,10 @@ def interfaces(self): selection = [] to = level.interfaces.transforms, level.interfaces.opposites for trans, ref in zip(map(level.transforms.__getitem__, indices), map(level.references.__getitem__, indices)): - for etrans, eref in ref.edges: - if not eref: - continue + for trans_etrans in transform.unempty_edge_transforms(trans, ref): for transforms, opposites in to, to[::-1]: try: - i = transforms.index(trans+(etrans,)) + i = transforms.index(trans_etrans) except ValueError: continue if self.transforms.contains_with_tail(opposites[i]): @@ -1748,8 +1743,7 @@ def interfaces(self): hreferences.append(level.interfaces.references[selection]) htransforms.append(level.interfaces.transforms[selection]) hopposites.append(level.interfaces.opposites[selection]) - root, = self.roots - return Topology(self.roots, elementseq.chain(hreferences, self.ndims-1), transformseq.chain(htransforms, root.ndims), transformseq.chain(hopposites, root.ndims)) + return Topology(self.roots, elementseq.chain(hreferences, self.ndims-1), transformseq.chain(htransforms, tuple(root.ndims for root in self.roots)), transformseq.chain(hopposites, tuple(root.ndims for root in self.roots))) @log.withcontext def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): @@ -1841,8 +1835,11 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): for ilevel, (level, indices) in enumerate(zip(self.levels, self._indices_per_level)): for ilocal in indices: - hbasis_trans = transform.canonical(level.transforms[ilocal]) - tail = hbasis_trans[len(hbasis_trans)-ilevel:] + hbasis_trans = tuple(map(transform.canonical, level.transforms[ilocal])) + tail = tuple(t[len(t)-ilevel:] for t in hbasis_trans) + lentail = len(tail[0]) + if not all(len(t) == lentail for t in tail): + raise NotImplementedError('variable length tails, possibly caused by anisotropic refinements, are not supported') trans_dofs = [] trans_coeffs = [] @@ -1863,8 +1860,8 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): mypoly = ubases[h].get_coefficients(ilocal) trans_coeffs.append(mypoly[myactive]) - if h < len(tail): - trans_coeffs = [tail[h].transform_poly(c) for c in trans_coeffs] + if h < lentail: + trans_coeffs = [transform.transform_poly(tuple(t[h] for t in tail), c) for c in trans_coeffs] else: # truncated hierarchical basis @@ -1872,8 +1869,8 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): mydofs = ubases[h].get_dofs(ilocal) mypoly = ubases[h].get_coefficients(ilocal) - truncpoly = mypoly if h == len(tail) \ - else numpy.tensordot(numpy.tensordot(tail[h].transform_poly(mypoly), project[...,mypassive], self.ndims), truncpoly[mypassive], 1) + truncpoly = mypoly if h == lentail \ + else numpy.tensordot(numpy.tensordot(transform.transform_poly(tuple(t[h] for t in tail), mypoly), project[...,mypassive], self.ndims), truncpoly[mypassive], 1) imyactive = numeric.sorted_index(ubasis_active[h], mydofs, missing=-1) myactive = numpy.greater_equal(imyactive, 0) & numpy.greater(abs(truncpoly), truncation_tolerance).any(axis=tuple(range(1,truncpoly.ndim))) diff --git a/nutils/transform.py b/nutils/transform.py index addc4f2da..18123ac5a 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -134,6 +134,16 @@ def swapup(self, other): def swapdown(self, other): return None + def separate(self, septodims): + if sum(septodims) != self.todims: + raise ValueError("'septodims' does not add up to 'todims'") + if septodims == (self.todims,): + return self, + elif self.todims in septodims: + return tuple(self if todims == self.todims else Identity(0) for todims in septodims) + else: + raise ValueError('Cannot separate {} into TransformItems with todims {}.'.format(self, septodims)) + stricttransformitem = types.strict[TransformItem] stricttransform = types.tuple[stricttransformitem] @@ -207,10 +217,11 @@ def det(self): def isflipped(self): return self.fromdims > 0 and self.det < 0 - def transform_poly(self, coeffs): - assert coeffs.ndim == self.fromdims + 1 - degree = coeffs.shape[1] - 1 - assert all(n == degree+1 for n in coeffs.shape[2:]) + def transform_poly(self, coeffs, startdim): + assert startdim >= 0 + assert coeffs.ndim >= startdim + self.fromdims + 1 + degree = coeffs.shape[startdim + 1] - 1 + assert all(n == degree+1 for n in coeffs.shape[startdim + 1:startdim + self.fromdims + 1]) try: M = self._transform_matrix[degree] except KeyError: @@ -229,7 +240,7 @@ def transform_poly(self, coeffs): M_power = functools.reduce(numeric.poly_mul, [numeric.poly_pow(poly, power) for poly, power in zip(polys, powers)]) M[tuple(slice(n) for n in M_power.shape)+powers] += M_power self._transform_matrix[degree] = M - return numpy.einsum('jk,ik', M.reshape([(degree+1)**self.fromdims]*2), coeffs.reshape(coeffs.shape[0],-1)).reshape(coeffs.shape) + return numpy.einsum('ij,njm->nim', M.reshape([(degree+1)**self.fromdims]*2), coeffs.reshape([util.product(coeffs.shape[i:j], 1) for i, j in util.pairwise([0,startdim+1,startdim+self.fromdims+1,coeffs.ndim])])).reshape(coeffs.shape) class Shift(Square): '''Shift transformation :math:`x ↦ x + b` @@ -258,6 +269,11 @@ def invapply(self, points): def __str__(self): return '{}+x'.format(util.obj2str(self.offset)) + def separate(self, septodims): + if sum(septodims) != self.todims: + raise ValueError("'septodims' does not add up to 'todims'") + return tuple(Shift(self.offset[l:r]) for l, r in util.pairwise(numpy.cumsum([0, *septodims]))) + class Identity(Shift): '''Identity transformation :math:`x ↦ x` @@ -281,6 +297,11 @@ def invapply(self, points): def __str__(self): return 'x' + def separate(self, septodims): + if sum(septodims) != self.todims: + raise ValueError("'septodims' does not add up to 'todims'") + return tuple(map(Identity, septodims)) + class Scale(Square): '''Affine transformation :math:`x ↦ a x + b`, with :math:`a` a scalar @@ -319,6 +340,11 @@ def __mul__(self, other): return Scale(self.scale * other.scale, self.apply(other.offset)) return super().__mul__(other) + def separate(self, septodims): + if sum(septodims) != self.todims: + raise ValueError("'septodims' does not add up to 'todims'") + return tuple(Scale(self.scale, self.offset[l:r]) for l, r in util.pairwise(numpy.cumsum([0, *septodims]))) + class Updim(Matrix): '''Affine transformation :math:`x ↦ A x + b`, with :math:`A` an :math:`n×(n-1)` matrix @@ -457,12 +483,16 @@ def swapdown(self, other): if isinstance(other, TensorChild): return ScaledUpdim(other, self), Identity(self.fromdims) + def separate(self, septodims): + return tuple(map(ScaledUpdim, self.trans1.separate(septodims), self.trans2.separate(septodims))) + class TensorEdge1(Updim): - __slots__ = 'trans', + __slots__ = 'trans', '_ndims2' def __init__(self, trans1, ndims2): self.trans = trans1 + self._ndims2 = ndims2 super().__init__(linear=numeric.blockdiag([trans1.linear, numpy.eye(ndims2)]), offset=numpy.concatenate([trans1.offset, numpy.zeros(ndims2)]), isflipped=trans1.isflipped) def swapup(self, other): @@ -492,12 +522,19 @@ def swapdown(self, other): def flipped(self): return TensorEdge1(self.trans.flipped, self.fromdims-self.trans.fromdims) + def separate(self, septodims): + if self.todims in septodims: + return super().separate(septodims) + else: + return separate_tensor(self.trans, Identity(self._ndims2), septodims) + class TensorEdge2(Updim): - __slots__ = 'trans' + __slots__ = 'trans', '_ndims1' def __init__(self, ndims1, trans2): self.trans = trans2 + self._ndims1 = ndims1 super().__init__(linear=numeric.blockdiag([numpy.eye(ndims1), trans2.linear]), offset=numpy.concatenate([numpy.zeros(ndims1), trans2.offset]), isflipped=trans2.isflipped^(ndims1%2)) def swapup(self, other): @@ -527,6 +564,12 @@ def swapdown(self, other): def flipped(self): return TensorEdge2(self.fromdims-self.trans.fromdims, self.trans.flipped) + def separate(self, septodims): + if self.todims in septodims: + return super().separate(septodims) + else: + return separate_tensor(Identity(self._ndims1), self.trans, septodims) + class TensorChild(Square): __slots__ = 'trans1', 'trans2' @@ -544,6 +587,12 @@ def __init__(self, trans1, trans2): def det(self): return self.trans1.det * self.trans2.det + def separate(self, septodims): + if self.todims in septodims: + return super().separate(septodims) + else: + return separate_tensor(self.trans1, self.trans2, septodims) + class Identifier(Identity): '''Generic identifier @@ -562,8 +611,15 @@ def __init__(self, ndims:int, token): def __str__(self): return ':'.join(map(str, self._args)) + def separate(self, septodims): + if sum(septodims) != self.todims: + raise ValueError("'septodims' does not add up to 'todims'") + return tuple(Identifier(todims, self.token) for todims in septodims) + class Manifold(Identity): + __slots__ = 'trans' + @types.apply_annotations def __init__(self, ndims:types.strictint, trans:stricttransformitem): self.trans = trans @@ -577,4 +633,61 @@ def swapdown(self, other): if isinstance(other, TensorChild): return ScaledUpdim(other, self), Identity(self.fromdims) + def separate(self, septodims): + if sum(septodims) != self.todims: + raise ValueError("'septodims' does not add up to 'todims'") + return tuple(Manifold(todims, self.trans) for todims in septodims) + +def separate_tensor(trans1, trans2, septodims): + i = 0 + while i < len(septodims) and sum(septodims[:i+1]) <= trans1.todims: + i += 1 + s = sum(septodims[:i]) + if s == trans1.todims: + septodims1 = septodims[:i] + septodims2 = septodims[i:] + else: + septodims1 = septodims[:i]+(trans1.todims-s,) + septodims2 = (septodims[i]-trans1.todims+s,)+septodims[i+1:] + + septrans1 = trans1.separate(septodims1) + septrans2 = trans2.separate(septodims2) + if s == trans1.todims: + return septrans1 + septrans2 + else: + return septrans1[:-1] + (join(septrans1[-1],septrans2[0]),) + septrans2[1:] + +def append_joined_item(trans, item, *, kind): + assert isinstance(trans, tuple) and all(isinstance(t, tuple) for t in trans) + assert isinstance(item, TransformItem) + sepitem = item.separate(tuple(t[-1].fromdims for t in trans)) + #return tuple(t if type(i) is Identity else t+(i,) for t, i in zip(trans, sepitem)) + return tuple(t if type(i) is Identity and kind == 'edge' else t+(SimplexChild(0,0),) if i == Identity(0) and kind == 'child' else t+(i,) for t, i in zip(trans, sepitem)) + +def append_edge(trans, edge): + return append_joined_item(trans, edge, kind='edge') + +def append_child(trans, child): + return append_joined_item(trans, child, kind='child') + +def child_transforms(trans, ref): + return (append_child(trans, ctrans) for ctrans in ref.child_transforms) + +def unempty_child_transforms(trans, ref): + return (append_child(trans, ctrans) for ctrans, cref in ref.children if cref) + +def edge_transforms(trans, ref): + return (append_edge(trans, etrans) for etrans in ref.edge_transforms) + +def unempty_edge_transforms(trans, ref): + return (append_edge(trans, etrans) for etrans, eref in ref.edges if eref) + +def transform_poly(items, coeffs): + startdim = 0 + for item in items: + coeffs = item.transform_poly(coeffs, startdim) + startdim += item.fromdims + assert coeffs.ndim == startdim+1 + return coeffs + # vim:sw=2:sts=2:et diff --git a/nutils/transformseq.py b/nutils/transformseq.py index bf00a8fed..c90d953c5 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -37,13 +37,13 @@ class supports indexing, iterating and has an :meth:`index` method. In Parameters ---------- - todims : :class:`int` - The dimension all transforms in this sequence map to. + todims : :class:`tuple` of :class:`int` + The todims of the transform chains in this sequence. Attributes ---------- - todims : :class:`int` - The dimension all transforms in this sequence map to. + todims : :class:`tuple` of :class:`int` + The todims of the transform chains in this sequence. Notes ----- @@ -54,7 +54,7 @@ class supports indexing, iterating and has an :meth:`index` method. In __slots__ = 'todims' @types.apply_annotations - def __init__(self, todims:types.strictint): + def __init__(self, todims:types.tuple[types.strictint]): self.todims = todims super().__init__() @@ -118,14 +118,14 @@ def index_with_tail(self, trans): Parameters ---------- - trans : :class:`tuple` of :class:`nutils.transform.TransformItem` objects + trans : :class:`tuple` of :class:`tuple` of :class:`nutils.transform.TransformItem` objects The transform to find up to a possibly empty tail. Returns ------- index : :class:`int` The index of ``trans`` without tail in this sequence. - tail : :class:`tuple` of :class:`nutils.transform.TransformItem` objects + tail : :class:`tuple` of :class:`tuple` of :class:`nutils.transform.TransformItem` objects The tail: ``trans[len(self[index]):]``. Raises @@ -144,13 +144,13 @@ def index_with_tail(self, trans): Calling :meth:`index_with_tail` with the first transform gives index ``0`` and no tail: - >>> transforms.index_with_tail((Shift([0.]),)) - (0, ()) + >>> transforms.index_with_tail(((Shift([0.]),),)) + (0, ((),)) Calling with an additional scale gives: - >>> transforms.index_with_tail((Shift([0.]), Scale(0.5, [0.]))) - (0, (Scale([0]+0.5*x),)) + >>> transforms.index_with_tail(((Shift([0.]), Scale(0.5, [0.])),)) + (0, ((Scale([0]+0.5*x),),)) ''' raise NotImplementedError @@ -166,7 +166,7 @@ def index(self, trans): Parameters ---------- - trans : :class:`tuple` of :class:`nutils.transform.TransformItem` objects + trans : :class:`tuple` of :class:`tuple` of :class:`nutils.transform.TransformItem` objects Returns ------- @@ -188,20 +188,20 @@ def index(self, trans): Calling :meth:`index` with the first transform gives index ``0``: - >>> transforms.index((Shift([0.]),)) + >>> transforms.index(((Shift([0.]),),)) 0 Calling with an additional scale raises an exception, because the transform is not present in ``transforms``. - >>> transforms.index((Shift([0.]), Scale(0.5, [0.]))) + >>> transforms.index(((Shift([0.]), Scale(0.5, [0.])),)) Traceback (most recent call last): ... - ValueError: (Shift([0]+x), Scale([0]+0.5*x)) not in sequence of transforms + ValueError: ((Shift([0]+x), Scale([0]+0.5*x)),) not in sequence of transforms ''' index, tail = self.index_with_tail(trans) - if tail: + if any(tail): raise ValueError('{!r} not in sequence of transforms'.format(trans)) return index @@ -210,7 +210,7 @@ def contains(self, trans): Parameters ---------- - trans : :class:`tuple` of :class:`nutils.transform.TransformItem` objects + trans : :class:`tuple` of :class:`tuple` of :class:`nutils.transform.TransformItem` objects Returns ------- @@ -234,7 +234,7 @@ def contains_with_tail(self, trans): Parameters ---------- - trans : :class:`tuple` of :class:`nutils.transform.TransformItem` objects + trans : :class:`tuple` of :class:`tuple` of :class:`nutils.transform.TransformItem` objects Returns ------- @@ -302,6 +302,11 @@ def __add__(self, other): raise ValueError('Cannot add two Transforms with different todims.') return chain((self, other), self.todims) + def __mul__(self, other): + if not isinstance(other, Transforms): + return NotImplemented + return ProductTransforms(self, other) + def unchain(self): '''Iterator of unchained :class:`Transforms` items. @@ -373,30 +378,32 @@ def __init__(self, transforms:types.tuple[transform.canonical], todims:types.str self._indices = numpy.argsort(self._sorted) self._sorted = self._sorted[self._indices] self._fromdims = fromdims - super().__init__(todims) + super().__init__((todims,)) def __iter__(self): - return iter(self._transforms) + for trans in self._transforms: + yield trans, def __getitem__(self, index): if not numeric.isint(index): return super().__getitem__(index) - return self._transforms[numeric.normdim(len(self), index)] + return self._transforms[numeric.normdim(len(self), index)], def __len__(self): return len(self._transforms) - def index_with_tail(self, trans): - trans, orig_trans = transform.promote(trans, self._fromdims), trans + def index_with_tail(self, mtrans): + assert len(mtrans) == 1 + trans = transform.promote(mtrans[0], self._fromdims) transid_array = numpy.empty((), dtype=object) transid_array[()] = transid = tuple(map(id, trans)) i = numpy.searchsorted(self._sorted, transid_array, side='right') - 1 if i < 0: - raise ValueError('{!r} not in sequence of transforms'.format(orig_trans)) + raise ValueError('{!r} not in sequence of transforms'.format(mtrans)) match = self._sorted[i] if transid[:len(match)] != match: - raise ValueError('{!r} not in sequence of transforms'.format(orig_trans)) - return self._indices[i], trans[len(match):] + raise ValueError('{!r} not in sequence of transforms'.format(mtrans)) + return self._indices[i], (trans[len(match):],) class IdentifierTransforms(Transforms): '''A sequence of :class:`nutils.transform.Identifier` singletons. @@ -421,21 +428,24 @@ def __init__(self, ndims:types.strictint, name:str, length:int): self._ndims = ndims self._name = name self._length = length - super().__init__(ndims) + super().__init__((ndims,)) def __getitem__(self, index): if not numeric.isint(index): return super().__getitem__(index) index = int(index) # make sure that index is a Python integer rather than numpy.intxx - return transform.Identifier(self._ndims, (self._name, numeric.normdim(self._length, index))), + return (transform.Identifier(self._ndims, (self._name, numeric.normdim(self._length, index))),), def __len__(self): return self._length - def index_with_tail(self, trans): + def index_with_tail(self, mtrans): + trans, = mtrans + if not trans: + raise ValueError root = trans[0] if root.todims == self._ndims and type(root) == transform.Identifier and isinstance(root.token, tuple) and len(root.token) == 2 and root.token[0] == self._name and 0 <= root.token[1] < self._length: - return root.token[1], trans[1:] + return root.token[1], (trans[1:],) raise ValueError class Axis(types.Singleton): @@ -556,7 +566,7 @@ def __init__(self, axes:types.tuple[types.strict[Axis]], nrefine:types.strictint self._etransforms = tuple(etransforms) self._fromdims = sum(axis.isdim for axis in self._axes) - super().__init__(len(self._axes)) + super().__init__((len(self._axes),)) def __getitem__(self, index): if not numeric.isint(index): @@ -575,12 +585,16 @@ def __getitem__(self, index): indices, r = divmod(indices, self._ctransforms.shape) ctransforms.insert(0, self._ctransforms[tuple(r)]) trans0 = transform.Shift(types.frozenarray(indices, dtype=float, copy=False)) - return (trans0, *ctransforms, *self._etransforms) + return (trans0, *ctransforms, *self._etransforms), def __len__(self): return util.product(map(len, self._axes)) - def index_with_tail(self, trans): + def index_with_tail(self, mtrans): + trans, = mtrans + # FIXME + #if trans and trans[-1].fromdims == 0 and len(trans) < 1 + self._nrefine + len(self._etransforms): + # trans += (transform.SimplexChild(0, 0),)*(1 + self._nrefine + len(self._etransforms) - len(trans)) if len(trans) < 1 + self._nrefine + len(self._etransforms): raise ValueError @@ -608,7 +622,10 @@ def index_with_tail(self, trans): raise ValueError tail = tail[len(self._etransforms):] - return flatindex, tail + return flatindex, (tail,) + + def linear_evaluable(self, index, roots): + return function.asarray(transform.linear(self[0][0], self.todims[0])), class MaskedTransforms(Transforms): '''An order preserving subset of another :class:`Transforms` object. @@ -733,7 +750,7 @@ def __len__(self): def __iter__(self): for reference, trans in zip(self._parent_references, self._parent): for dtrans in self._derived_transforms(reference): - yield trans+(dtrans,) + yield transform.append_joined_item(trans, dtrans, kind='edge' if self._updim else 'child') def __getitem__(self, index): if not numeric.isint(index): @@ -742,15 +759,24 @@ def __getitem__(self, index): iparent = numpy.searchsorted(self._offsets, index, side='right')-1 assert 0 <= iparent < len(self._offsets)-1 iderived = index - self._offsets[iparent] - return self._parent[iparent] + (self._derived_transforms(self._parent_references[iparent])[iderived],) + trans = self._parent[iparent] + derived = self._derived_transforms(self._parent_references[iparent])[iderived] + return transform.append_joined_item(trans, derived, kind='edge' if self._updim else 'child') def index_with_tail(self, trans): - iparent, tail = self._parent.index_with_tail(trans) - if not tail: + iparent, parenttail = self._parent.index_with_tail(trans) + if not any(parenttail): + raise ValueError + parenttail = tuple(map(transform.canonical if self._updim else transform.uppermost, parenttail)) + todims = [a[0].todims if a else b[-1].fromdims for a, b in zip(parenttail, trans)] + for iderived, derivedtrans in enumerate(self._derived_transforms(self._parent_references[iparent])): + sepderivedtrans = tuple(None if type(t) is transform.Identity and self._updim else transform.SimplexChild(0,0) if t == transform.Identity(0) and not self._updim else t for t in derivedtrans.separate(todims)) + if all(b is None or a and a[0] == b for a, b in zip(parenttail, sepderivedtrans)): + tail = tuple(a if b is None else a[1:] for a, b in zip(parenttail, sepderivedtrans)) + break + else: raise ValueError - tail = (transform.canonical if self._updim else transform.uppermost)(tail) - iderived = self._derived_transforms(self._parent_references[iparent]).index(tail[0]) - return self._offsets[iparent]+iderived, tail[1:] + return self._offsets[iparent]+iderived, tail class UniformDerivedTransforms(Transforms): '''A sequence of refined transforms from a uniform sequence of references. @@ -788,21 +814,30 @@ def __len__(self): def __iter__(self): for trans in self._parent: for dtrans in self._derived_transforms: - yield trans+(dtrans,) + yield transform.append_joined_item(trans, dtrans, kind='edge' if self._updim else 'child') def __getitem__(self, index): if not numeric.isint(index): return super().__getitem__(index) iparent, iderived = divmod(numeric.normdim(len(self), index), len(self._derived_transforms)) - return self._parent[iparent] + (self._derived_transforms[iderived],) + trans = self._parent[iparent] + derived = self._derived_transforms[iderived] + return transform.append_joined_item(trans, derived, kind='edge' if self._updim else 'child') def index_with_tail(self, trans): - iparent, tail = self._parent.index_with_tail(trans) - if not tail: + iparent, parenttail = self._parent.index_with_tail(trans) + if not any(parenttail): + raise ValueError + parenttail = tuple(map(transform.canonical if self._updim else transform.uppermost, parenttail)) + todims = [a[0].todims if a else b[-1].fromdims for a, b in zip(parenttail, trans)] + for iderived, derivedtrans in enumerate(self._derived_transforms): + sepderivedtrans = tuple(None if type(t) is transform.Identity and self._updim else transform.SimplexChild(0,0) if t == transform.Identity(0) and not self._updim else t for t in derivedtrans.separate(todims)) + if all(b is None or a and a[0] == b for a, b in zip(parenttail, sepderivedtrans)): + tail = tuple(a if b is None else a[1:] for a, b in zip(parenttail, sepderivedtrans)) + break + else: raise ValueError - tail = (transform.canonical if self._updim else transform.uppermost)(tail) - iderived = self._derived_transforms.index(tail[0]) - return iparent*len(self._derived_transforms) + iderived, tail[1:] + return iparent*len(self._derived_transforms) + iderived, tail class TrimmedEdgesTransforms(Transforms): @@ -810,7 +845,7 @@ class TrimmedEdgesTransforms(Transforms): __cache__ = '_offsets' @types.apply_annotations - def __init__(self, parent:stricttransforms, edges:types.tuple[types.tuple[transform.stricttransformitem]]): + def __init__(self, parent:stricttransforms, edges:types.tuple[types.tuple[types.tuple[transform.stricttransformitem]]]): assert len(edges) == len(parent) self._parent = parent self._edges = edges @@ -826,7 +861,7 @@ def __len__(self): def __iter__(self): for ptrans, edges in zip(self._parent, self._edges): for etrans in edges: - yield ptrans+(etrans,) + yield tuple(h+(t,) for h, t in zip(ptrans, etrans)) def __getitem__(self, index): if not numeric.isint(index): @@ -835,22 +870,22 @@ def __getitem__(self, index): iparent = numpy.searchsorted(self._offsets, index, side='right')-1 assert 0 <= iparent < len(self._offsets)-1 iedge = index - self._offsets[iparent] - return self._parent[iparent] + (self._edges[iparent][iedge],) + return tuple(h+(t,) for h, t in zip(self._parent[iparent], self._edges[iparent][iedge])) def index_with_tail(self, trans): - iparent, tail = self._parent.index_with_tail(trans) - if not tail: + iparent, tails = self._parent.index_with_tail(trans) + if not all(tails): raise ValueError - tail = transform.canonical(tail) - iedge = self._edges[iparent].index(tail[0]) - return self._offsets[iparent]+iedge, tail[1:] + tails = tuple(map(transform.canonical, tails)) + iedge = self._edges[iparent].index(tuple(tail[0] for tail in tails)) + return self._offsets[iparent]+iedge, tuple(tail[1:] for tail in tails) class ProductTransforms(Transforms): '''The product of two :class:`Transforms` objects. - The order of the resulting transforms is: ``transforms1[0]*transforms2[0], - transforms1[0]*transforms2[1], ..., transforms1[1]*transforms2[0], - transforms1[1]*transforms2[1], ...``. + The order of the resulting transforms is: ``transforms1[0]+transforms2[0], + transforms1[0]+transforms2[1], ..., transforms1[1]+transforms2[0], + transforms1[1]+transforms2[1], ...``. Parameters ---------- @@ -866,28 +901,26 @@ class ProductTransforms(Transforms): def __init__(self, transforms1:stricttransforms, transforms2:stricttransforms): self._transforms1 = transforms1 self._transforms2 = transforms2 - super().__init__(transforms1.fromdims+transforms2.fromdims) + super().__init__(transforms1.todims+transforms2.todims) def __iter__(self): for trans1 in self._transforms1: for trans2 in self._transforms2: - yield transform.Bifurcate(trans1, trans2), + yield trans1+trans2 def __getitem__(self, index): if not numeric.isint(index): return super().__getitem__(index) index1, index2 = divmod(numeric.normdim(len(self), index), len(self._transforms2)) - return transform.Bifurcate(self._transforms1[index1], self._transforms2[index2]), + return self._transforms1[index1]+self._transforms2[index2] def __len__(self): return len(self._transforms1) * len(self._transforms2) def index_with_tail(self, trans): - bf = trans[0] - assert isinstance(bf, transform.Bifurcate) - index1, tail1 = self._transforms1.index_with_tail(bf.trans1[:-1]) - index2, tail2 = self._transforms2.index_with_tail(bf.trans2[:-1]) - return index1*len(self._transforms2)+index2, None # FIXME + index1, tail1 = self._transforms1.index_with_tail(trans[:len(self._transforms1.todims)]) + index2, tail2 = self._transforms2.index_with_tail(trans[len(self._transforms1.todims):]) + return index1*len(self._transforms2)+index2, tail1+tail2 class ChainedTransforms(Transforms): '''A sequence of chained :class:`Transforms` objects. @@ -966,14 +999,14 @@ def unchain(self): yield from self._items @types.apply_annotations -def chain(items:types.tuple[stricttransforms], todims:types.strictint): +def chain(items:types.tuple[stricttransforms], todims:types.tuple[types.strictint]): '''Return the chained transforms sequence of ``items``. Parameters ---------- items : iterable of :class:`Transforms` objects The :class:`Transforms` objects to chain. - todims : :class:`int` + todims : :class:`tuple` of :class:`int` The dimension all transforms of all sequences map to. Returns diff --git a/tests/test_function.py b/tests/test_function.py index 2cc6419fe..fcbde03c2 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -281,7 +281,8 @@ def test_opposite(self): def find(self, target, xi0): elemtrans, = self.sample.transforms[0] ndim, = self.geom.shape - J = function.dot(function.rootgradient(self.geom, self.sample.roots)[:,:,_], transform.linear(elemtrans[1:], elemtrans[-1].fromdims)[_,:,:], 1) + rootlinear = numeric.blockdiag([transform.linear(trans, root.ndims) for root, trans in zip(self.sample.roots, elemtrans)]) + J = function.dot(function.rootgradient(self.geom, self.sample.roots)[:,:,_], rootlinear[_,:,:], 1) Jinv = function.inverse(J).prepare_eval() countdown = 5 iiter = 0 @@ -310,7 +311,7 @@ def test_rootgradient(self): argsfun = function.Tuple(self.args).prepare_eval() exact = self.sample.eval(function.rootgradient(self.op_args, self.sample.roots)) D = numpy.array([-.5,.5])[:,_,_] * numpy.eye(self.ndim) - invlinear = numpy.linalg.inv(transform.linear(elemtrans[1:], elemtrans[-1].fromdims)) + invlinear = numeric.blockdiag([numpy.linalg.inv(transform.linear(trans, root.ndims)) for root, trans in zip(self.sample.roots, elemtrans)]) good = False eps = 1e-5 while not numpy.all(good): diff --git a/tests/test_mesh.py b/tests/test_mesh.py index ac10e585c..80894317a 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -64,7 +64,8 @@ def test_refine(self): boundary1 = self.domain.refined.boundary boundary2 = self.domain.boundary.refined assert len(boundary1) == len(boundary2) == len(self.domain.boundary) * element.getsimplex(self.domain.ndims-1).nchildren - assert set(map(transform.canonical, boundary1.transforms)) == set(map(transform.canonical, boundary2.transforms)) + canonical = lambda transtuple: tuple(map(transform.canonical, transtuple)) + assert set(map(canonical, boundary1.transforms)) == set(map(canonical, boundary2.transforms)) assert all(boundary2.references[boundary2.transforms.index(trans)] == ref for ref, trans in zip(boundary1.references, boundary1.transforms)) @requires('meshio') @@ -73,7 +74,8 @@ def test_refinesubset(self): boundary1 = domain.refined.boundary boundary2 = domain.boundary.refined assert len(boundary1) == len(boundary2) == len(domain.boundary) * element.getsimplex(domain.ndims-1).nchildren - assert set(map(transform.canonical, boundary1.transforms)) == set(map(transform.canonical, boundary2.transforms)) + canonical = lambda transtuple: tuple(map(transform.canonical, transtuple)) + assert set(map(canonical, boundary1.transforms)) == set(map(canonical, boundary2.transforms)) assert all(boundary2.references[boundary2.transforms.index(trans)] == ref for ref, trans in zip(boundary1.references, boundary1.transforms)) for ndims in 2, 3: diff --git a/tests/test_topology.py b/tests/test_topology.py index 34f777f1f..74c73b184 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -12,14 +12,14 @@ def assertConnectivity(self, domain, geom): for ielem, ioppelems in enumerate(domain.connectivity): for iedge, ioppelem in enumerate(ioppelems): etrans, eref = domain.references[ielem].edges[iedge] - trans = domain.transforms[ielem] + (etrans,) + trans = transform.append_edge(domain.transforms[ielem], etrans) if ioppelem == -1: index = boundary.transforms.index(trans) bmask[index] += 1 else: ioppedge = domain.connectivity[ioppelem].index(ielem) oppetrans, opperef = domain.references[ioppelem].edges[ioppedge] - opptrans = domain.transforms[ioppelem] + (oppetrans,) + opptrans = transform.append_edge(domain.transforms[ioppelem], oppetrans) try: index = interfaces.transforms.index(trans) except ValueError: @@ -302,7 +302,7 @@ def test_connectivity(self): def test_boundary(self): for trans in self.domain.boundary.transforms: ielem, tail = self.domain.transforms.index_with_tail(trans) - etrans, = tail + (etrans,), = tail iedge = self.domain.references[ielem].edge_transforms.index(etrans) self.assertEqual(self.domain.connectivity[ielem][iedge], -1) @@ -310,10 +310,10 @@ def test_interfaces(self): itopo = self.domain.interfaces for trans, opptrans in zip(itopo.transforms, itopo.opposites): ielem, tail = self.domain.transforms.index_with_tail(trans) - etrans, = tail + (etrans,), = tail iedge = self.domain.references[ielem].edge_transforms.index(etrans) ioppelem, opptail = self.domain.transforms.index_with_tail(opptrans) - eopptrans, = opptail + (eopptrans,), = opptail ioppedge = self.domain.references[ioppelem].edge_transforms.index(eopptrans) self.assertEqual(self.domain.connectivity[ielem][iedge], ioppelem) self.assertEqual(self.domain.connectivity[ioppelem][ioppedge], ielem) diff --git a/tests/test_transformseq.py b/tests/test_transformseq.py index ffeaed166..910d8454d 100644 --- a/tests/test_transformseq.py +++ b/tests/test_transformseq.py @@ -7,15 +7,14 @@ class Common: def test_todims(self): self.assertEqual(self.seq.todims, self.checktodims) for trans in self.seq: - self.assertEqual(trans[0].todims, self.checktodims) + self.assertEqual(tuple(t[0].todims for t in trans), self.checktodims) def test_fromdims(self): self.assertEqual(len(self.seq), len(self.checkrefs)) for trans, ref in zip(self.seq, self.checkrefs): - self.assertEqual(trans[-1].fromdims, ref.ndims) + self.assertEqual(sum(t[-1].fromdims for t in trans), ref.ndims) def test_len(self): - self.assertEqual(len(self.check), len(self.checkrefs)) self.assertEqual(len(self.seq), len(self.check)) def test_getitem_scalar_pos(self): @@ -74,14 +73,15 @@ def test_add(self): self.assertEqual(tuple(self.seq+self.seq), tuple(self.check)+tuple(self.check)) def test_index_with_tail(self): + assert len(self.check) == len(self.checkrefs) for i, (trans, ref) in enumerate(zip(self.check, self.checkrefs)): - self.assertEqual(self.seq.index_with_tail(trans), (i, ())) - for ctrans in ref.child_transforms: - self.assertEqual(self.seq.index_with_tail(trans+(ctrans,)), (i, (ctrans,))) + self.assertEqual(self.seq.index_with_tail(trans), (i, ((),))) + for ctrans in nutils.transform.child_transforms(trans, ref): + self.assertEqual(self.seq.index_with_tail(ctrans), (i, tuple(t[-1:] for t in ctrans))) if ref.ndims > 0: - for etrans in ref.edge_transforms: - for shuffle in lambda t: t, nutils.transform.canonical: - self.assertEqual(self.seq.index_with_tail(shuffle(trans+(ctrans,))), (i, (ctrans,))) + for etrans in nutils.transform.edge_transforms(trans, ref): + for variant in etrans, nutils.transform.canonical(etrans): + self.assertEqual(self.seq.index_with_tail(variant), (i, tuple(t[-1:] for t in etrans))) def test_index_with_tail_missing(self): for trans in self.checkmissing: @@ -96,20 +96,22 @@ def test_index_missing(self): for trans in self.checkmissing: with self.assertRaises(ValueError): self.seq.index(trans) + assert len(self.check) == len(self.checkrefs) for trans, ref in zip(self.check, self.checkrefs): - for ctrans in ref.child_transforms: + for ctrans in nutils.transform.child_transforms(trans, ref): with self.assertRaises(ValueError): - self.seq.index(trans+(ctrans,)) + self.seq.index(ctrans) def test_contains_with_tail(self): + assert len(self.check) == len(self.checkrefs) for i, (trans, ref) in enumerate(zip(self.check, self.checkrefs)): - self.assertEqual(self.seq.index_with_tail(trans), (i, ())) - for ctrans in ref.child_transforms: - self.assertTrue(self.seq.contains_with_tail(trans+(ctrans,))) + self.assertEqual(self.seq.index_with_tail(trans), (i, ((),))) + for ctrans in nutils.transform.child_transforms(trans, ref): + self.assertTrue(self.seq.contains_with_tail(ctrans)) if ref.ndims > 0: - for etrans in ref.edge_transforms: - for shuffle in lambda t: t, nutils.transform.canonical: - self.assertTrue(self.seq.contains_with_tail(trans+(etrans,))) + for etrans in nutils.transform.edge_transforms(trans, ref): + for variant in etrans, nutils.transform.canonical(etrans): + self.assertTrue(self.seq.contains_with_tail(variant)) def test_contains_with_tail_missing(self): for trans in self.checkmissing: @@ -122,31 +124,30 @@ def test_contains(self): def test_contains_missing(self): for trans in self.checkmissing: self.assertFalse(self.seq.contains(trans)) + assert len(self.check) == len(self.checkrefs) for trans, ref in zip(self.check, self.checkrefs): - for ctrans in ref.child_transforms: - self.assertFalse(self.seq.contains(trans+(ctrans,))) + for ctrans in nutils.transform.child_transforms(trans, ref): + self.assertFalse(self.seq.contains(ctrans)) def test_refined(self): refined = self.seq.refined(self.checkrefs) - ctransforms = (trans+(ctrans,) for trans, ref in zip(self.check, self.checkrefs) for ctrans in ref.child_transforms) - for i, trans in enumerate(ctransforms): - self.assertEqual(refined.index(trans), i) + assert len(self.check) == len(self.checkrefs) + for i, ctrans in enumerate((ctrans for trans, ref in zip(self.check, self.checkrefs) for ctrans in nutils.transform.child_transforms(trans, ref))): + self.assertEqual(refined.index(ctrans), i) class Edges: def test_edges(self): edges = self.seq.edges(self.checkrefs) - etransforms = (trans+(etrans,) for trans, ref in zip(self.check, self.checkrefs) for etrans in ref.edge_transforms) - for i, trans in enumerate(etransforms): - self.assertEqual(edges.index(trans), i) + assert len(self.check) == len(self.checkrefs) + for i, etrans in enumerate((etrans for trans, ref in zip(self.check, self.checkrefs) for etrans in nutils.transform.edge_transforms(trans, ref))): + self.assertEqual(edges.index(etrans), i) point = nutils.element.PointReference() line = nutils.element.LineReference() square = line*line triangle = nutils.element.TriangleReference() -l1, x1, r1 = sorted([nutils.transform.Identifier(1, n) for n in ('l1', 'x1', 'r1')], key=id) - s0 = nutils.transform.Shift([0.]) s1 = nutils.transform.Shift([1.]) s2 = nutils.transform.Shift([2.]) @@ -156,7 +157,6 @@ def test_edges(self): c0,c1 = line.child_transforms e0,e1 = line.edge_transforms -l2, x2, r2 = sorted([nutils.transform.Identifier(2, n) for n in ('l2', 'x2', 'r2')], key=id) s00 = nutils.transform.Shift([0.,0.]) s01 = nutils.transform.Shift([0.,1.]) s02 = nutils.transform.Shift([0.,2.]) @@ -170,168 +170,168 @@ def test_edges(self): class EmptyTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.EmptyTransforms(1) + self.seq = nutils.transformseq.EmptyTransforms((1,)) self.check = () - self.checkmissing = (l1,s0),(x1,s4),(r1,s0) + self.checkmissing = ((s0,),),((s4,),) self.checkrefs = nutils.elementseq.EmptyReferences(1) - self.checktodims = 1 + self.checktodims = 1, class PlainTransforms1D(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1),(x1,s2),(x1,s3)], 1, 1) - self.check = (x1,s0),(x1,s1),(x1,s2),(x1,s3) - self.checkmissing = (l1,s0),(x1,s4),(r1,s0) - self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checktodims = 1 + self.seq = nutils.transformseq.PlainTransforms([(s1,),(s2,),(s3,)], 1, 1) + self.check = ((s1,),),((s2,),),((s3,),) + self.checkmissing = ((s0,),),((s4,),),((c0,),) + self.checkrefs = nutils.elementseq.asreferences([line]*3, 1) + self.checktodims = 1, class PlainTransforms2D(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], 2, 2) - self.check = (x2,s00),(x2,s01),(x2,s10),(x2,s11) - self.checkmissing = (l2,s00),(x2,s02),(x2,s12),(r2,s00) + self.seq = nutils.transformseq.PlainTransforms([(s00,),(s01,),(s10,),(s11,)], 2, 2) + self.check = ((s00,),),((s01,),),((s10,),),((s11,),) + self.checkmissing = ((s02,),),((s12,),),((c00,),) self.checkrefs = nutils.elementseq.asreferences([square,square,triangle,triangle], 2) - self.checktodims = 2 + self.checktodims = 2, class MaskedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.MaskedTransforms(nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], 2, 2), [0,2]) - self.check = (x2,s00),(x2,s10) - self.checkmissing = (l2,s00),(x2,s01),(x2,s11),(x2,s02),(x2,s12),(r2,s00) + self.seq = nutils.transformseq.MaskedTransforms(nutils.transformseq.PlainTransforms([(s00,),(s01,),(s10,),(s11,)], 2, 2), [0,2]) + self.check = ((s00,),),((s10,),) + self.checkmissing = ((s01,),),((s11,),),((s02,),),((s12,),),((c00,),) self.checkrefs = nutils.elementseq.asreferences([square,triangle], 2) - self.checktodims = 2 + self.checktodims = 2, class ReorderedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.ReorderedTransforms(nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01),(x2,s10),(x2,s11)], 2, 2), [0,2,3,1]) - self.check = (x2,s00),(x2,s10),(x2,s11),(x2,s01) - self.checkmissing = (l2,s00),(x2,s02),(x2,s12),(r2,s00) + self.seq = nutils.transformseq.ReorderedTransforms(nutils.transformseq.PlainTransforms([(s00,),(s01,),(s10,),(s11,)], 2, 2), [0,2,3,1]) + self.check = ((s00,),),((s10,),),((s11,),),((s01,),) + self.checkmissing = ((s02,),),((s12,),),((c00,),) self.checkrefs = nutils.elementseq.asreferences([square]*4, 2) - self.checktodims = 2 + self.checktodims = 2, class DerivedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.DerivedTransforms(nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1), nutils.elementseq.asreferences([line,line], 1), 'child_transforms', False) - self.check = (x1,s0,c0),(x1,s0,c1),(x1,s1,c0),(x1,s1,c1) - self.checkmissing = (l1,s0),(x1,s0),(x1,s1),(r1,s0) + self.seq = nutils.transformseq.DerivedTransforms(nutils.transformseq.PlainTransforms([(s0,),(s1,)], 1, 1), nutils.elementseq.asreferences([line,line], 1), 'child_transforms', False) + self.check = ((s0,c0),),((s0,c1),),((s1,c0),),((s1,c1),) + self.checkmissing = ((s0,),),((s1,),),((s2,c0),) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checktodims = 1 + self.checktodims = 1, class UniformDerivedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.UniformDerivedTransforms(nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1), line, 'child_transforms', False) - self.check = (x1,s0,c0),(x1,s0,c1),(x1,s1,c0),(x1,s1,c1) - self.checkmissing = (l1,s0),(x1,s0),(x1,s1),(r1,s0) + self.seq = nutils.transformseq.UniformDerivedTransforms(nutils.transformseq.PlainTransforms([(s0,),(s1,)], 1, 1), line, 'child_transforms', False) + self.check = ((s0,c0),),((s0,c1),),((s1,c0),),((s1,c1),) + self.checkmissing = ((s0,),),((s1,),),((s2,c0),) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checktodims = 1 + self.checktodims = 1, class ChainedTransforms(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.ChainedTransforms([nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1), nutils.transformseq.PlainTransforms([(x1,s2),(x1,s3)], 1, 1)]) - self.check = (x1,s0),(x1,s1),(x1,s2),(x1,s3) - self.checkmissing = (l1,s0),(x1,s4),(r1,s0) + self.seq = nutils.transformseq.ChainedTransforms([nutils.transformseq.PlainTransforms([(s0,),(s1,)], 1, 1), nutils.transformseq.PlainTransforms([(s2,),(s3,)], 1, 1)]) + self.check = ((s0,),),((s1,),),((s2,),),((s3,),) + self.checkmissing = ((s4,),),((c0,),) self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checktodims = 1 + self.checktodims = 1, class StructuredTransforms1D(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(0,4,False)], 0) - self.check = (s0,),(s1,),(s2,),(s3,) - self.checkmissing = (l1,s0),(s4,),(r1,s0),(c1,) - self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checktodims = 1 + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(1,4,False)], 0) + self.check = ((s1,),),((s2,),),((s3,),) + self.checkmissing = ((s0,),),((s4,),),((c0,),) + self.checkrefs = nutils.elementseq.asreferences([line]*3, 1) + self.checktodims = 1, class StructuredTransforms1DRefined(TestCase, Common, Edges): def setUp(self): - self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(0,4,False)], 1) - self.check = (s0,c0),(s0,c1),(s1,c0),(s1,c1) - self.checkmissing = (l1,s0),(s0,),(s1,),(s0,s1),(r1,s0) - self.checkrefs = nutils.elementseq.asreferences([line]*4, 1) - self.checktodims = 1 + self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(1,4,False)], 1) + self.check = ((s0,c1),),((s1,c0),),((s1,c1),) + self.checkmissing = ((s0,),),((s0,c0),),((s2,),),((s2,c0),) + self.checkrefs = nutils.elementseq.asreferences([line]*3, 1) + self.checktodims = 1, class StructuredTransforms1DLeft(TestCase, Common): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.BndAxis(3,3,0,False)], 0) - self.check = (s3,e1), - self.checkmissing = (s0,e0),(s2,e0),(s4,e0) + self.check = ((s3,e1),), + self.checkmissing = ((s0,e0),),((s2,e0),),((s4,e0),) self.checkrefs = nutils.elementseq.asreferences([point], 0) - self.checktodims = 1 + self.checktodims = 1, class StructuredTransforms1DRight(TestCase, Common): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.BndAxis(3,3,0,True)], 0) - self.check = (s2,e0), - self.checkmissing = (s0,e0),(s3,e1),(s4,e0) + self.check = ((s2,e0),), + self.checkmissing = ((s0,e0),),((s3,e1),),((s4,e0),) self.checkrefs = nutils.elementseq.asreferences([point], 0) - self.checktodims = 1 + self.checktodims = 1, class StructuredTransforms1DInterfacesLeft(TestCase, Common): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.IntAxis(0,4,0,False)], 0) - self.check = (s1,e1),(s2,e1),(s3,e1) - self.checkmissing = (s0,e1),(s0,e0),(s1,e0),(s2,e0),(s3,e0) + self.check = ((s1,e1),),((s2,e1),),((s3,e1),) + self.checkmissing = ((s0,e1),),((s0,e0),),((s1,e0),),((s2,e0),),((s3,e0),) self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) - self.checktodims = 1 + self.checktodims = 1, class StructuredTransforms1DInterfacesRight(TestCase, Common): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.IntAxis(0,4,0,True)], 0) - self.check = (s0,e0),(s1,e0),(s2,e0) - self.checkmissing = (s3,e0),(s0,e1),(s1,e1),(s2,e1),(s3,e1) + self.check = ((s0,e0),),((s1,e0),),((s2,e0),) + self.checkmissing = ((s3,e0),),((s0,e1),),((s1,e1),),((s2,e1),),((s3,e1),) self.checkrefs = nutils.elementseq.asreferences([point]*3, 0) - self.checktodims = 1 + self.checktodims = 1, class StructuredTransforms1DPeriodicInterfacesLeft(TestCase, Common): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.PIntAxis(0,4,0,False)], 0) - self.check = (s1,e1),(s2,e1),(s3,e1),(s0,e1) - self.checkmissing = (s0,e0),(s1,e0),(s2,e0),(s3,e0),(s4,e0) + self.check = ((s1,e1),),((s2,e1),),((s3,e1),),((s0,e1),) + self.checkmissing = ((s0,e0),),((s1,e0),),((s2,e0),),((s3,e0),),((s4,e0),) self.checkrefs = nutils.elementseq.asreferences([point]*4, 0) - self.checktodims = 1 + self.checktodims = 1, class StructuredTransforms1DPeriodicInterfacesRight(TestCase, Common): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.PIntAxis(0,4,0,True)], 0) - self.check = (s0,e0),(s1,e0),(s2,e0),(s3,e0) - self.checkmissing = (s0,e1),(s1,e1),(s2,e1),(s3,e1),(s4,e1) + self.check = ((s0,e0),),((s1,e0),),((s2,e0),),((s3,e0),) + self.checkmissing = ((s0,e1),),((s1,e1),),((s2,e1),),((s3,e1),),((s4,e1),) self.checkrefs = nutils.elementseq.asreferences([point]*4, 0) - self.checktodims = 1 + self.checktodims = 1, class StructuredTransforms2D(TestCase, Common, Edges): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(0,2,False),nutils.transformseq.DimAxis(2,4,False)], 0) - self.check = (s02,),(s03,),(s12,),(s13,) - self.checkmissing = (s00,),(s01,),(s10,),(s11,) + self.check = ((s02,),),((s03,),),((s12,),),((s13,),) + self.checkmissing = ((s00,),),((s01,),),((s10,),),((s11,),) self.checkrefs = nutils.elementseq.asreferences([square]*4, 2) - self.checktodims = 2 + self.checktodims = 2, class StructuredTransforms2DRefined(TestCase, Common, Edges): def setUp(self): self.seq = nutils.transformseq.StructuredTransforms([nutils.transformseq.DimAxis(0,2,False),nutils.transformseq.DimAxis(2,4,False)], 1) - self.check = (s01,c00),(s01,c01),(s01,c10),(s01,c11) - self.checkmissing = (s00,c00), + self.check = ((s01,c00),),((s01,c01),),((s01,c10),),((s01,c11),) + self.checkmissing = ((s00,c00),), self.checkrefs = nutils.elementseq.asreferences([square]*4, 2) - self.checktodims = 2 + self.checktodims = 2, class IdentifierTransforms(TestCase, Common, Edges): def setUp(self): self.seq = nutils.transformseq.IdentifierTransforms(ndims=2, name='foo', length=4) - self.check = [(nutils.transform.Identifier(2, ('foo', i)),) for i in range(4)] - self.checkmissing = (nutils.transform.Identifier(1, ('foo', 0)),), (nutils.transform.Identifier(2, ('foo', -1)),), (nutils.transform.Identifier(2, ('foo', 4)),), (nutils.transform.Identifier(2, ('bar', 0)),) + self.check = [((nutils.transform.Identifier(2, ('foo', i)),),) for i in range(4)] + self.checkmissing = ((nutils.transform.Identifier(1, ('foo', 0)),),), ((nutils.transform.Identifier(2, ('foo', -1)),),), ((nutils.transform.Identifier(2, ('foo', 4)),),), ((nutils.transform.Identifier(2, ('bar', 0)),),) self.checkrefs = nutils.elementseq.asreferences([triangle]*4, 2) - self.checktodims = 2 + self.checktodims = 2, class exceptions(TestCase): def test_PlainTransforms_invalid_todims(self): with self.assertRaisesRegex(ValueError, 'expected transforms with todims=2, but got .*'): - nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 2, 1) + nutils.transformseq.PlainTransforms([(s0,),(s1,)], 2, 1) def test_PlainTransforms_invalid_fromdims(self): with self.assertRaisesRegex(ValueError, 'expected transforms with fromdims=2, but got .*'): - nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 2) + nutils.transformseq.PlainTransforms([(s0,),(s1,)], 1, 2) def test_DerivedTransforms_length_mismatch(self): - transforms = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1) + transforms = nutils.transformseq.PlainTransforms([(s0,),(s1,)], 1, 1) references = nutils.elementseq.PlainReferences([line]*3, 1) with self.assertRaisesRegex(ValueError, '`parent` and `parent_references` should have the same length'): nutils.transformseq.DerivedTransforms(transforms, references, 'child_transforms', False) @@ -341,8 +341,8 @@ def test_ChainedTransforms_no_items(self): nutils.transformseq.ChainedTransforms([]) def test_ChainedTransforms_multiple_ndims(self): - a = nutils.transformseq.PlainTransforms([(x1,s0),(x1,s1)], 1, 1) - b = nutils.transformseq.PlainTransforms([(x2,s00),(x2,s01)], 2, 2) + a = nutils.transformseq.PlainTransforms([(s0,),(s1,)], 1, 1) + b = nutils.transformseq.PlainTransforms([(s00,),(s01,)], 2, 2) with self.assertRaisesRegex(ValueError, 'Cannot chain Transforms with different todims.'): nutils.transformseq.ChainedTransforms([a, b]) From f7d011729b74fe4e9d3d9782eba7c54b20e8bb37 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sun, 1 Mar 2020 14:25:42 +0100 Subject: [PATCH 039/119] replace points evalarg with *subsamples --- nutils/function.py | 72 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 22 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 1f7b31f27..a68c34943 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -799,41 +799,63 @@ def __init__(self, roots:types.tuple[strictroot], ndimstangent:types.strictint, self._trans = trans assert self._trans.ordered_roots == roots ndims = sum(root.ndims for root in roots) - super().__init__(args=[EVALARGS, trans], shape=[ndims, ndims], dtype=float) - - def evalf(self, evalargs, _trans): - mtrans = evalargs['_transforms'][1 if self._opposite else 0] - points = evalargs['_points'] - - ndims = self.shape[0] - assert mtrans == _trans[0] - if points.ndimsmanifold != self._ndimstangent: - raise ValueError('expected a {}D tangent space, but got a {}D space'.format(self._ndimstangent, points.ndimstangent)) + super().__init__(args=[SUBSAMPLES, trans], shape=[ndims, ndims], dtype=float) + def _evalf_subsample(self, roots, chains, points): + ndims = builtins.sum(root.ndims for root in roots) linear = numpy.zeros((points.npoints, ndims, ndims), dtype=float) - mtranslinear = numpy.zeros((ndims, points.ndims), dtype=float) + chainslinear = numpy.zeros((ndims, points.ndims), dtype=float) to0 = from0 = 0 n0 = points.ndims - for root, trans in zip(self._roots, mtrans): + for root, chain in zip(roots, chains): to1 = to0 + root.ndims - fromdims = trans[-1].fromdims if trans else root.ndims - translinear = transform.linearfrom(trans, root.ndims) + fromdims = chain[-1].fromdims if chain else root.ndims + chainlinear = transform.linearfrom(chain, root.ndims) if fromdims: from1 = from0 + fromdims - mtranslinear[to0:to1,from0:from1] = translinear[:,:fromdims] + chainslinear[to0:to1,from0:from1] = chainlinear[:,:fromdims] from0 = from1 if fromdims < root.ndims: n1 = n0 + root.ndims - fromdims - linear[:,to0:to1,n0:n1] = translinear[:,fromdims:] + linear[:,to0:to1,n0:n1] = chainlinear[:,fromdims:] n0 = n1 to0 = to1 - assert to0 == self.shape[0] + assert to0 == ndims assert from0 == points.ndims - assert n0 == self.shape[0] - - numpy.einsum('ij,njk->nik', mtranslinear, points.basis, out=linear[:,:,:points.ndims]) + assert n0 == ndims + numpy.einsum('ij,njk->nik', chainslinear, points.basis, out=linear[:,:,:points.ndims]) return linear + def evalf(self, subsamples, _trans): + ndims = self.shape[0] + linear = numpy.zeros((*(subsample.points.npoints for subsample in subsamples), ndims, ndims), dtype=float) + to0 = from0 = 0 + n0 = self._ndimstangent + slices = {} + for ipoints, subsample in enumerate(subsamples): + if frozenset(subsample.roots).isdisjoint(self.roots): + continue + subsamplelinear = self._evalf_subsample(subsample.roots, subsample.transforms[1 if self._opposite else 0], subsample.points) + subsamplelinear = subsamplelinear[tuple(slice(None) if j == ipoints else _ for j in range(len(subsamples)))] + to1 = to0 + subsamplelinear.shape[-1] + from1 = from0 + subsample.ndimsmanifold + n1 = n0 + subsamplelinear.shape[-1] - subsample.ndimsmanifold + linear[...,to0:to1,from0:from1] = subsamplelinear[...,:subsample.ndimsmanifold] + linear[...,to0:to1,n0:n1] = subsamplelinear[...,subsample.ndimsmanifold:] + r0 = to0 + for root in subsample.roots: + r1 = r0 + root.ndims + slices[root] = slice(r0, r1) + r0 = r1 + to0, from0, n0 = to1, from1, n1 + assert to0 == ndims + assert from0 == self._ndimstangent + assert n0 == ndims + + # reorder to `self._roots` + linear = linear.reshape((-1, ndims, ndims)) + return numpy.concatenate([linear[:,slices[root]] for root in self._roots], axis=1) + @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): if self._opposite is not None: @@ -1328,7 +1350,7 @@ def evalf(self, subsamples, chains): result = numpy.zeros((*(subsample.npoints for subsample in subsamples), self.shape[0]), dtype=float) to0 = 0 - for root, (chain, chaintodims, chainfromdims) in zip(self._tail.ordered_roots, chains): + for root, chain in zip(self._tail.ordered_roots, chains[0]): to1 = to0 + (chain[0].todims if chain else slices[root].stop - slices[root].start) isubsample = isubsamples[root] expand = tuple(slice(None) if i == isubsample else numpy.newaxis for i in range(len(subsamples))) @@ -1341,7 +1363,13 @@ def _derivative(self, var, seen): if isinstance(var, RootCoords) and var.root in self.roots: if self._head.fromdims != builtins.sum(root.ndims for root in self.roots): raise NotImplementedError('transform contains updims') - return Inverse(Linear(self._head, len(var))) + to0 = 0 + for root in self._tail.ordered_roots: + to1 = to0 + root.ndims + if root == var.root: + return Inverse(Linear(self._head, self._head.fromdims))[:,to0:to1] + to0 = to1 + raise Exception return zeros(self.shape+var.shape) class Linear(Array): From 57de5366303ed4869f90fa8f416917cedce45143 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sun, 15 Mar 2020 20:58:17 +0100 Subject: [PATCH 040/119] add function.ProductBasis --- nutils/function.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index a68c34943..35edfaf5d 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4047,6 +4047,38 @@ def f_ndofs(self, index): def f_coefficients(self, index): return self._parent.f_coefficients(get(self._transmap, 0, index)) +class ProductBasis(Basis): + + __slots__ = '_basis1', '_basis2' + + @types.apply_annotations + def __init__(self, basis1:strictbasis, basis2:strictbasis, trans:types.strict[TransformChain]): + self._basis1 = basis1 + self._basis2 = basis2 + super().__init__(basis1.ndofs*basis2.ndofs, basis1.transforms*basis2.transforms, basis1.ndimsdomain+basis2.ndimsdomain, trans) + + def get_dofs(self, ielem): + if not numeric.isint(ielem): + return super().get_dofs(ielem) + ielem1, ielem2 = divmod(ielem, len(self._basis2.transforms)) + dofs1 = self._basis1.get_dofs(ielem1) + dofs2 = self._basis2.get_dofs(ielem2) + return (dofs1[:,_] * self._basis2.ndofs + dofs2[_,:]).ravel() + + def get_coefficients(self, ielem): + if not numeric.isint(ielem): + return super().get_coefficients(ielem) + ielem1, ielem2 = divmod(ielem, len(self._basis2.transforms)) + coeffs1 = self._basis1.get_coefficients(ielem1) + coeffs2 = self._basis2.get_coefficients(ielem2) + return numeric.poly_outer_product(coeffs1, coeffs2) + + def get_support(self, dof): + dof1, dof2 = divmod(dof, self._basis2.ndofs) + supp1 = self._basis1.get_support(dof1) + supp2 = self._basis2.get_support(dof2) + return (supp1[:,_] * len(self._basis2.transforms) + supp2[_,:]).ravel() + # AUXILIARY FUNCTIONS (FOR INTERNAL USE) _ascending = lambda arg: numpy.greater(numpy.diff(arg), 0).all() From 8994a38a625ee85d87454cb0ef5cfad4d8e992dd Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sun, 15 Mar 2020 21:10:47 +0100 Subject: [PATCH 041/119] reimplement ProductTopology --- nutils/topology.py | 178 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 134 insertions(+), 44 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index fd4091590..7441023e8 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -128,8 +128,28 @@ def __rsub__(self, other): assert isinstance(other, Topology) and other.ndims == self.ndims return other - other.subset(self, newboundary=getattr(self,'boundary',None)) + def mul(self, other, leftopp, rightopp): + if not isinstance(other, Topology): + return NotImplemented + if not set(self.roots).isdisjoint(other.roots): + raise ValueError('cannot multiply topologies with common roots') + if isinstance(self, EmptyTopology) or isinstance(other, EmptyTopology): + return EmptyTopology(self.roots+other.roots, self.ndims+other.ndims) + else: + return ProductTopology(self, other, leftopp, rightopp) + def __mul__(self, other): - return ProductTopology(self, other) + leftopp = self.transforms != self.opposites + rightopp = other.transforms != other.opposites + if leftopp and rightopp: + raise ValueError('Cannot multiply two topologies, both having opposites. Use :meth:`mul_leftopp` or :meth:`mul_rightopp` instead.') + return self.mul(other, leftopp, rightopp) + + def mul_leftopp(self, other): + return self.mul(other, True, False) + + def mul_rightopp(self, other): + return self.mul(other, False, True) @property def border_transforms(self): @@ -1899,63 +1919,133 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): class ProductTopology(Topology): 'product topology' - __slots__ = 'topo1', 'topo2' - __cache__ = 'boundary', 'interfaces' + __slots__ = '_left', '_right', '_leftopp', '_rightopp' @types.apply_annotations - def __init__(self, topo1:stricttopology, topo2:stricttopology): - assert not isinstance(topo1, ProductTopology) - self.topo1 = topo1 - self.topo2 = topo2 - references = self.topo1.references * self.topo2.references - transforms = transformseq.ProductTransforms(self.topo1.transforms, self.topo2.transforms) - if (self.topo1.opposites != self.topo1.transforms) != (self.topo2.opposites != self.topo2.transforms): - opposites = transformseq.ProductTransforms(self.topo1.opposites, self.topo2.opposites) - else: - opposites = transforms - if len(self.topo1.roots) != 1 or len(self.topo2.roots) != 1 or self.topo1.roots == self.topo2.roots: - raise NotImplementedError - root = function.Root('{}*{}'.format(self.topo1.roots[0].id, self.topo2.roots[0].id), self.topo1.roots[0].ndims+self.topo2.roots[0].ndims) - super().__init__((root,), references, transforms, opposites) + def __init__(self, left:stricttopology, right:stricttopology, leftopp:bool, rightopp:bool): + self._left = left + self._right = right + self._leftopp = leftopp + self._rightopp = rightopp + super().__init__(left.roots+right.roots, + references=left.references*right.references, + transforms=left.transforms*right.transforms, + opposites=(left.opposites if leftopp else left.transforms)*(right.opposites if rightopp else right.transforms)) - def __mul__(self, other): - return ProductTopology(self.topo1, self.topo2 * other) + def __repr__(self): + return '{!r}*{!r}'.format(self._left, self._right) @property - def refined(self): - return self.topo1.refined * self.topo2.refined + def shape(self): + return self._left.shape + self._right.shape - def refine(self, n): - if numpy.iterable(n): - assert len(n) == self.ndims - else: - n = (n,)*self.ndims - return self.topo1.refine(n[:self.topo1.ndims]) * self.topo2.refine(n[self.topo1.ndims:]) + @property + def connectivity(self): + s = len(self._right) + return tuple(tuple(ir+cli*s if cli >= 0 else -1 for cli in cl)+tuple(il*s+cri if cri >= 0 else -1 for cri in cr) for (il,cl), (ir,cr) in itertools.product(enumerate(self._left.connectivity), enumerate(self._right.connectivity))) def getitem(self, item): - return self.topo1.getitem(item) * self.topo2 | self.topo1 * self.topo2.getitem(item) if isinstance(item, str) \ - else self.topo1[item[:self.topo1.ndims]] * self.topo2[item[self.topo1.ndims:]] - - def basis(self, name, *args, **kwargs): - def _split(arg): - if not numpy.iterable(arg): - return arg, arg - assert len(arg) == self.ndims - return tuple(a[0] if all(ai == a[0] for ai in a[1:]) else a for a in (arg[:self.topo1.ndims], arg[self.topo1.ndims:])) - splitargs = [_split(arg) for arg in args] - splitkwargs = [(name,)+_split(arg) for name, arg in kwargs.items()] - basis1, basis2 = function.bifurcate( - self.topo1.basis(name, *[arg1 for arg1, arg2 in splitargs], **{name: arg1 for name, arg1, arg2 in splitkwargs}), - self.topo2.basis(name, *[arg2 for arg1, arg2 in splitargs], **{name: arg2 for name, arg1, arg2 in splitkwargs})) - return function.ravel(function.outer(basis1,basis2), axis=0) + if isinstance(item, tuple) and all(isinstance(it, slice) for it in item): + left = self._left.getitem(item[:self._left.ndims]) + if len(item) > self._left.ndims: + right = self._right.getitem(item[self._left.ndims:]) + else: + right = self._right + return left.mul(right, self._leftopp, self._rightopp) + left = self._left.getitem(item) + right = self._right.getitem(item) + if not left and not right: + return left*right + else: + return (left or self._left).mul(right or self._right, self._leftopp, self._rightopp) @property def boundary(self): - return self.topo1 * self.topo2.boundary + self.topo1.boundary * self.topo2 + boundaries = [] + if self._right.ndims: + boundaries.append(self._left.mul_rightopp(self._right.boundary)) + if self._left.ndims: + boundaries.append(self._left.boundary.mul_leftopp(self._right)) + if not boundaries: + return EmptyTopology(self.roots, ndims=0) + elif len(boundaries) == 1: + return boundaries[0] + else: + return DisjointUnionTopology(boundaries) @property def interfaces(self): - return self.topo1 * self.topo2.interfaces + self.topo1.interfaces * self.topo2 + interfaces = [] + if self._right.ndims: + interfaces.append(self._left.mul_rightopp(self._right.interfaces)) + if self._left.ndims: + interfaces.append(self._left.interfaces.mul_leftopp(self._right)) + if not interfaces: + return EmptyTopology(self.roots, ndims=0) + elif len(interfaces) == 1: + return interfaces[0] + else: + return DisjointUnionTopology(interfaces) + + def _productbasis(self, lbasis, rbasis): + if not lbasis: + return rbasis + if not rbasis: + return lbasis + return function.ProductBasis(lbasis, rbasis, function.SelectChain(self.roots)) + + def basis(self, name, *args, **kwargs): + if name in ('spline', 'h-spline', 'th-spline'): + return self.basis_spline(*args, _variant=name, **kwargs) + elif name == 'std': + return self.basis_std(*args, **kwargs) + lbasis = self._left.basis(name, *args, **kwargs) if self._left.ndims else None + rbasis = self._right.basis(name, *args, **kwargs) if self._right.ndims else None + return self._productbasis(lbasis, rbasis) + + def _split_list(self, value, scalar_type): + if value is None or isinstance(value[0], scalar_type): + lvalue = rvalue = value + else: + assert len(value) == self.ndims + lvalue = value[:self._left.ndims] + rvalue = value[self._left.ndims:] + return lvalue, rvalue + + def _split_scalar(self, value, scalar_type): + if value is None or isinstance(value, scalar_type): + lvalue = rvalue = value + else: + assert len(value) == self.ndims + lvalue = value[:self._left.ndims] + rvalue = value[self._left.ndims:] + return lvalue, rvalue + + def basis_spline(self, degree, removedofs=None, knotvalues=None, knotmultiplicities=None, continuity=-1, periodic=None, _variant='spline'): + lremovedofs, rremovedofs = self._split_list(removedofs, int) + lknotvalues, rknotvalues = self._split_list(knotvalues, (int, float)) + lknotmultiplicities, rknotmultiplicities = self._split_list(knotmultiplicities, int) + lcontinuity, rcontinuity = self._split_scalar(continuity, int) + ldegree, rdegree = self._split_scalar(degree, int) + if periodic is None: + lperiodic = rperiodic = None + else: + lperiodic = [i for i in periodic if i < self._left.ndims] + rperiodic = [i-self._left.ndims for i in periodic if i >= self._left.ndims] + + lbasis = self._left.basis(_variant, degree=ldegree, removedofs=lremovedofs, knotvalues=lknotvalues, knotmultiplicities=lknotmultiplicities, continuity=lcontinuity, periodic=lperiodic) if self._left.ndims else None + rbasis = self._right.basis(_variant, degree=rdegree, removedofs=rremovedofs, knotvalues=rknotvalues, knotmultiplicities=rknotmultiplicities, continuity=rcontinuity, periodic=rperiodic) if self._right.ndims else None + return self._productbasis(lbasis, rbasis) + + def basis_std(self, degree): + ldegree, rdegree = self._split_scalar(degree, int) + lbasis = self._left.basis('std', degree=ldegree) if self._left.ndims else None + rbasis = self._right.basis('std', degree=rdegree) if self._right.ndims else None + return self._productbasis(lbasis, rbasis) + + @property + def refined(self): + return self._left.refined.mul(self._right.refined, self._leftopp, self._rightopp) class RevolutionTopology(Topology): 'topology consisting of a single revolution element' From a2333788b7fcfd059764b59c7890212b87bac998 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sun, 15 Mar 2020 20:54:09 +0100 Subject: [PATCH 042/119] make mesh.rectilinear,.unitsquare tensorial --- nutils/mesh.py | 69 ++++++++++---------------------------------------- 1 file changed, 13 insertions(+), 56 deletions(-) diff --git a/nutils/mesh.py b/nutils/mesh.py index 8450aba2c..e95f8035d 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -27,50 +27,10 @@ """ from . import topology, function, util, element, elementseq, numpy, numeric, transform, transformseq, warnings, types, cache, _ -import os, itertools, re, math, treelog as log, io, contextlib +import os, itertools, re, math, treelog as log, io, contextlib, functools # MESH GENERATORS -@log.withcontext -def rectilinear(richshape, periodic=(), name='rect'): - 'rectilinear mesh' - - ndims = len(richshape) - shape = [] - offset = [] - scale = [] - uniform = True - for v in richshape: - if numeric.isint(v): - assert v > 0 - shape.append(v) - scale.append(1) - offset.append(0) - elif numpy.equal(v, numpy.linspace(v[0],v[-1],len(v))).all(): - shape.append(len(v)-1) - scale.append((v[-1]-v[0]) / float(len(v)-1)) - offset.append(v[0]) - else: - shape.append(len(v)-1) - uniform = False - - root = function.Root(name, ndims) - axes = [transformseq.DimAxis(0,n,idim in periodic) for idim, n in enumerate(shape)] - topo = topology.StructuredTopology(root, axes) - - if uniform: - if all(o == offset[0] for o in offset[1:]): - offset = offset[0] - if all(s == scale[0] for s in scale[1:]): - scale = scale[0] - geom = function.rootcoords(root) * scale + offset - else: - funcsp = topo.basis('spline', degree=1, periodic=()) - coords = numeric.meshgrid(*richshape).reshape(ndims, -1) - geom = (funcsp * coords).sum(-1) - - return topo, geom - def line(nodes, periodic=False, bnames=None, *, rootid='line'): if isinstance(nodes, int): uniform = True @@ -88,18 +48,15 @@ def line(nodes, periodic=False, bnames=None, *, rootid='line'): geom = function.rootcoords(root) * scale + offset if uniform else domain.basis('std', degree=1, periodic=False).dot(nodes) return domain, geom -def newrectilinear(nodes, periodic=None, bnames=[['left','right'],['bottom','top'],['front','back']]): +@log.withcontext +def rectilinear(nodes, periodic=None, bnames=[['left','right'],['bottom','top'],['front','back']], rootnames='XYZABX'): + 'rectilinear mesh' if periodic is None: - periodic = numpy.zeros(len(nodes), dtype=bool) - else: - periodic = numpy.asarray(periodic) - assert len(periodic) == len(nodes) and periodic.ndim == 1 and periodic.dtype == bool - dims = [line(nodesi, periodici, bnamesi, rootid=rootid) for nodesi, periodici, bnamesi, rootid in zip(nodes, periodic, tuple(bnames)+(None,)*len(nodes), 'XYZABC')] - domain, geom = dims.pop(0) - for domaini, geomi in dims: - domain = domain * domaini - geom = function.concatenate(function.bifurcate(geom,geomi)) - return domain, geom + periodic = [] + domains, geoms = zip(*(line(nodesi, idim in periodic, bnamesi, rootid=rootid) for idim, (nodesi, bnamesi, rootid) in enumerate(zip(nodes, tuple(bnames)+(None,)*len(nodes), rootnames)))) + return functools.reduce(lambda l, r: topology.ProductTopology(l, r, False, False), domains), function.concatenate(geoms, axis=0) + +newrectilinear = rectilinear @log.withcontext def multipatch(patches, nelems, patchverts=None, name='multipatch'): @@ -659,12 +616,11 @@ def unitsquare(nelems, etype): The geometry function. ''' - root = function.Root('unitsquare', 2) - if etype == 'square': - topo = topology.StructuredTopology(root, [transformseq.DimAxis(0, nelems, False)] * 2) + topo, geom = rectilinear([nelems]*2) elif etype in ('triangle', 'mixed'): + root = function.Root('unitsquare', 2) simplices = numpy.concatenate([ numpy.take([i*(nelems+1)+j, i*(nelems+1)+j+1, (i+1)*(nelems+1)+j, (i+1)*(nelems+1)+j+1], [[0,1,2],[1,2,3]] if i%2==j%2 else [[0,1,3],[0,2,3]], axis=0) for i in range(nelems) for j in range(nelems)]) @@ -691,10 +647,11 @@ def unitsquare(nelems, etype): x, y = topo.boundary.elem_mean(function.rootcoords(root), degree=1).T bgroups = dict(left=x==0, right=x==nelems, bottom=y==0, top=y==nelems) topo = topo.withboundary(**{name: topo.boundary[numpy.where(mask)[0]] for name, mask in bgroups.items()}) + geom = function.rootcoords(root) else: raise Exception('invalid element type {!r}'.format(etype)) - return topo, function.rootcoords(root) / nelems + return topo, geom/nelems # vim:sw=2:sts=2:et From 3065e8d616721b0cb54b97d400dcff0b3cdf8f1f Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sun, 15 Mar 2020 22:15:12 +0100 Subject: [PATCH 043/119] reenable mesh.newrectilinear tests --- tests/test_mesh.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_mesh.py b/tests/test_mesh.py index 80894317a..9f48a5a44 100644 --- a/tests/test_mesh.py +++ b/tests/test_mesh.py @@ -105,7 +105,6 @@ def test_length(self): gmshmanifold(version=version, degree=degree) @parametrize -@unittest.skip('in between bifurcate and tensorial') class rectilinear(TestCase): def setUp(self): From 86d82d9d129b153ad21ae2d052dc8a5580bc5288 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 2 Mar 2020 12:29:08 +0100 Subject: [PATCH 044/119] WIP: ProductSample, ProductBasis --- nutils/sample.py | 18 +++++++++++++++++- nutils/topology.py | 3 +++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/nutils/sample.py b/nutils/sample.py index d0eececaf..a9d8d85b8 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -44,7 +44,7 @@ class which represents postponed integration. Integrals are internally ''' from . import types, points, util, function, parallel, numeric, matrix, transformseq, sparse -import numpy, numbers, collections.abc, os, treelog as log +import numpy, numbers, collections.abc, os, treelog as log, operator graphviz = os.environ.get('NUTILS_GRAPHVIZ') @@ -338,6 +338,22 @@ def subsamplemetas(self): strictsample = types.strict[Sample] +class ProductSample(Sample): + + @types.apply_annotations + def __init__(self, sample1:strictsample, sample2:strictsample): + self._sample1 = sample1 + self._sample2 = sample2 + super().__init__(sample1.roots+sample2.roots, + sample1.ndims+sample2.ndims, + tuple(map(operator.mul, sample1.transforms, sample2.transforms)), + tuple(points.TensorPoints(points1, points2) for points1 in sample1.points for points2 in sample2.points), # TODO: make lazy + tuple((index1[:,numpy.newaxis]*sample2.npoints + index2[numpy.newaxis,:]).ravel() for index1 in sample1.index for index2 in sample2.index)) + + def getsubsamples(self, ielem): + ielem1, ielem2 = divmod(ielem, self._sample2.nelems) + return self._sample1.getsubsamples(ielem1) + self._sample2.getsubsamples(ielem2) + class Integral(types.Singleton): '''Postponed integration. diff --git a/nutils/topology.py b/nutils/topology.py index 7441023e8..4fc2d0c16 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -2047,6 +2047,9 @@ def basis_std(self, degree): def refined(self): return self._left.refined.mul(self._right.refined, self._leftopp, self._rightopp) + def sample(self, ischeme, degree): + return sample.ProductSample(self._left.sample(ischeme, degree), self._right.sample(ischeme, degree)) + class RevolutionTopology(Topology): 'topology consisting of a single revolution element' From 29ee6e1cd4bc856758089343c385cb210518e451 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 2 Mar 2020 16:33:58 +0100 Subject: [PATCH 045/119] fixes --- nutils/topology.py | 4 +++- nutils/transformseq.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 4fc2d0c16..f79881e5c 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -1725,7 +1725,7 @@ def boundary(self): bindices = [] for index in indices: for trans in transform.unempty_edge_transforms(level.transforms[index], level.references[index]): - trans = tuple(map(transform.uppermost, trans)) + #trans = tuple(map(transform.uppermost, trans)) try: bindices.append(bindex(trans)) except ValueError: @@ -1735,6 +1735,8 @@ def boundary(self): bindices.sort() assert not numpy.equal(bindices[1:], bindices[:-1]).any() bindices_per_level.append(bindices) + foo = HierarchicalTopology(basebtopo, bindices_per_level) + assert foo.integral(function.asarray(1), degree=1).eval() == basebtopo.integral(function.asarray(1), degree=1).eval() return HierarchicalTopology(basebtopo, bindices_per_level) @property diff --git a/nutils/transformseq.py b/nutils/transformseq.py index c90d953c5..e65110ebb 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -861,7 +861,7 @@ def __len__(self): def __iter__(self): for ptrans, edges in zip(self._parent, self._edges): for etrans in edges: - yield tuple(h+(t,) for h, t in zip(ptrans, etrans)) + yield tuple(h if type(t) is transform.Identity else h+(t,) for h, t in zip(ptrans, etrans)) def __getitem__(self, index): if not numeric.isint(index): @@ -870,15 +870,20 @@ def __getitem__(self, index): iparent = numpy.searchsorted(self._offsets, index, side='right')-1 assert 0 <= iparent < len(self._offsets)-1 iedge = index - self._offsets[iparent] - return tuple(h+(t,) for h, t in zip(self._parent[iparent], self._edges[iparent][iedge])) + return tuple(h if type(t) is transform.Identity else h+(t,) for h, t in zip(self._parent[iparent], self._edges[iparent][iedge])) def index_with_tail(self, trans): iparent, tails = self._parent.index_with_tail(trans) if not all(tails): raise ValueError tails = tuple(map(transform.canonical, tails)) - iedge = self._edges[iparent].index(tuple(tail[0] for tail in tails)) - return self._offsets[iparent]+iedge, tuple(tail[1:] for tail in tails) + for iedge, edges in enumerate(self._edges[iparent]): + if all(type(e) is transform.Identity or t and t[0] == e for e, t in zip(edges, tails)): + break + else: + raise ValueError + #iedge = self._edges[iparent].index(tuple(tail[0] for tail in tails)) + return self._offsets[iparent]+iedge, tuple(tail if type(e) is transform.Identity else tail[1:] for tail, e in zip(tails, edges)) class ProductTransforms(Transforms): '''The product of two :class:`Transforms` objects. From cf9d19d20691487972fe85281da629e35b9e4b93 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 3 Mar 2020 15:14:51 +0100 Subject: [PATCH 046/119] WIP --- examples/burgers.py | 2 +- examples/cylinderflow.py | 2 +- examples/drivencavity-compatible.py | 2 +- examples/laplace.py | 2 + examples/platewithhole-nurbs.py | 2 +- nutils/function.py | 152 ++++++++++++++-------------- nutils/sample.py | 8 +- nutils/topology.py | 8 ++ 8 files changed, 98 insertions(+), 80 deletions(-) diff --git a/examples/burgers.py b/examples/burgers.py index 29995420d..8fee0aa8f 100644 --- a/examples/burgers.py +++ b/examples/burgers.py @@ -27,7 +27,7 @@ def main(nelems:int, ndims:int, degree:int, timescale:float, newtontol:float, en Stopping time. ''' - domain, geom = mesh.rectilinear([numpy.linspace(0,1,nelems+1)]*ndims, periodic=range(ndims)) + domain, geom = mesh.newrectilinear([numpy.linspace(0,1,nelems+1)]*ndims, periodic=range(ndims)) ns = function.Namespace() ns.x = geom diff --git a/examples/cylinderflow.py b/examples/cylinderflow.py index 1923206c2..8dbfb3ba0 100644 --- a/examples/cylinderflow.py +++ b/examples/cylinderflow.py @@ -41,7 +41,7 @@ def main(nelems:int, degree:int, reynolds:float, rotation:float, timestep:float, elemangle = 2 * numpy.pi / nelems melems = int(numpy.log(2*maxradius) / elemangle + .5) treelog.info('creating {}x{} mesh, outer radius {:.2f}'.format(melems, nelems, .5*numpy.exp(elemangle*melems))) - domain, geom = mesh.rectilinear([melems, nelems], periodic=(1,)) + domain, geom = mesh.newrectilinear([melems, nelems], periodic=(1,)) domain = domain.withboundary(inner='left', outer='right') ns = function.Namespace() diff --git a/examples/drivencavity-compatible.py b/examples/drivencavity-compatible.py index 9709bf005..f7bcf3375 100644 --- a/examples/drivencavity-compatible.py +++ b/examples/drivencavity-compatible.py @@ -27,7 +27,7 @@ def main(nelems:int, degree:int, reynolds:float): ''' verts = numpy.linspace(0, 1, nelems+1) - domain, geom = mesh.rectilinear([verts, verts]) + domain, geom = mesh.newrectilinear([verts, verts]) ns = function.Namespace() ns.x = geom diff --git a/examples/laplace.py b/examples/laplace.py index f72a1c711..47448aba0 100644 --- a/examples/laplace.py +++ b/examples/laplace.py @@ -70,6 +70,8 @@ def main(nelems:int, etype:str, btype:str, degree:int): res = domain.integral('basis_n,i u_,i d:x' @ ns, degree=degree*2) res -= domain.boundary['right'].integral('basis_n cos(1) cosh(x_1) d:x' @ ns, degree=degree*2) + print(('basis_n,i u_,i d:x' @ ns).prepare_eval(subsamples=domain.sample('gauss', 1).subsamplemetas).simplified.asciitree()) + # The Dirichlet constraints are set by finding the coefficients that minimize # the error: # diff --git a/examples/platewithhole-nurbs.py b/examples/platewithhole-nurbs.py index 4dbb39911..ecdaf884b 100644 --- a/examples/platewithhole-nurbs.py +++ b/examples/platewithhole-nurbs.py @@ -27,7 +27,7 @@ def main(nrefine:int, traction:float, radius:float, poisson:float): ''' # create the coarsest level parameter domain - domain, geom0 = mesh.rectilinear([1, 2]) + domain, geom0 = mesh.newrectilinear([1, 2]) bsplinebasis = domain.basis('spline', degree=2) controlweights = numpy.ones(12) controlweights[1:3] = .5 + .25 * numpy.sqrt(2) diff --git a/nutils/function.py b/nutils/function.py index 35edfaf5d..6806a8143 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -468,26 +468,12 @@ def evalf(self, subsamples): break else: raise ValueError('no such root: {!r}'.format(root)) - return tuple(trans), tuple(t[0].todims for t in trans), tuple(t[-1].fromdims for t in trans) + return tuple(trans) @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): return SelectChain(self.ordered_roots, 1-self.n) if opposite else self -class EmptyTransformChain(TransformChain): - - __slots__ = '_todims', '_fromdims' - - @types.apply_annotations - def __init__(self, roots:types.tuple[strictroot], todims:types.tuple[types.strictint], fromdims:types.tuple[types.strictint]): - assert len(roots) == len(todims) == len(fromdims) - self._todims = todims - self._fromdims = fromdims - super().__init__(roots=roots, args=[], todims=builtins.sum(todims), fromdims=builtins.sum(fromdims)) - - def evalf(self): - return ((),)*len(self.roots), self._todims, self._fromdims - class TransformChainFromTuple(TransformChain): __slots__ = 'index' @@ -516,11 +502,10 @@ def roots(self): return self._trans.roots def evalf(self, chains): - chains, todims, fromdims = chains index, tails = self._transforms.index_with_tail(chains) - tailtodims = tuple(t[0].todims if t else n for n, t in zip(fromdims, tails)) + tailtodims = tuple(t[0].todims if t else c[-1].fromdims for t, c in zip(tails, chains)) assert builtins.sum(tailtodims) == self._fromdims - return numpy.array(index)[None], (tails, tailtodims, fromdims) + return numpy.array(index)[None], tails def __len__(self): return 3 @@ -529,34 +514,26 @@ def __len__(self): def index(self): return ArrayFromTuple(self, index=0, shape=(), dtype=int) - @property - def head(self): - return GetTransform(self._trans.ordered_roots, self._transforms, self.index, self._fromdims) - @property def tail(self): return TransformChainFromTuple(self._trans.ordered_roots, self, index=1, todims=self._fromdims) + @property + def linear(self): + return Linear(self._trans.ordered_roots, self._transforms, self.index, self._fromdims) + def __iter__(self): yield self.index - yield self.head yield self.tail - -class GetTransform(TransformChain): - - __slots__ = 'transforms', 'index' - - @types.apply_annotations - def __init__(self, roots:types.tuple[strictroot], transforms:transformseq.stricttransforms, index:asarray, fromdims:types.strictint): - assert index.ndim == 0 and index.dtype == int - self.transforms = transforms - super().__init__(args=[index], roots=roots, fromdims=fromdims, todims=builtins.sum(root.ndims for root in roots)) - - def evalf(self, index): - index, = index - trans = self.transforms[index] - return trans, tuple(t[0].todims for t in trans), tuple(t[-1].fromdims for t in trans) - + yield self.linear + +# @util.positional_only +# def prepare_eval(self, *, subsamples, kwargs=...): +# self = TransformsIndexWithTail(self._transforms, self._fromdims, self._trans.prepare_eval(subsamples=subsamples, **kwargs)) +# for subsample in subsamples: +# if self._trans.ordered_roots == subsample.roots and isinstance(self._trans, SelectChain) and self._transforms == subsamples.transforms[self._trans.n]: +# return +# # ARRAYFUNC # # The main evaluable. Closely mimics a numpy array. @@ -1324,14 +1301,15 @@ def _takediag(self, axis, rmaxis): class ApplyTransforms(Array): - __slots__ = '_head', '_tail' + __slots__ = '_tail', '_linear' @types.apply_annotations - def __init__(self, head:types.strict[TransformChain], tail:types.strict[TransformChain]): - assert head.roots == tail.roots - self._head = head + def __init__(self, tail:types.strict[TransformChain], linear:asarray): + assert linear.ndim == 2 + assert tail.todims == linear.shape[1] self._tail = tail - super().__init__(args=[SUBSAMPLES, tail], shape=[self._tail.todims], dtype=float) + self._linear = linear + super().__init__(args=[SUBSAMPLES, tail], shape=[tail.todims], dtype=float) @property def roots(self): @@ -1350,7 +1328,7 @@ def evalf(self, subsamples, chains): result = numpy.zeros((*(subsample.npoints for subsample in subsamples), self.shape[0]), dtype=float) to0 = 0 - for root, chain in zip(self._tail.ordered_roots, chains[0]): + for root, chain in zip(self._tail.ordered_roots, chains): to1 = to0 + (chain[0].todims if chain else slices[root].stop - slices[root].start) isubsample = isubsamples[root] expand = tuple(slice(None) if i == isubsample else numpy.newaxis for i in range(len(subsamples))) @@ -1361,43 +1339,38 @@ def evalf(self, subsamples, chains): def _derivative(self, var, seen): if isinstance(var, RootCoords) and var.root in self.roots: - if self._head.fromdims != builtins.sum(root.ndims for root in self.roots): + if self._linear.shape[0] != self._linear.shape[1]: raise NotImplementedError('transform contains updims') to0 = 0 for root in self._tail.ordered_roots: to1 = to0 + root.ndims if root == var.root: - return Inverse(Linear(self._head, self._head.fromdims))[:,to0:to1] + return Inverse(self._linear)[:,to0:to1] to0 = to1 raise Exception return zeros(self.shape+var.shape) class Linear(Array): - __slots__ = '_trans' - __cache__ = 'simplified' + __slots__ = '_roots', '_transforms' @types.apply_annotations - def __init__(self, trans:types.strict[TransformChain], fromdims:types.strictint, todims:types.strictint=None): - self._trans = trans - super().__init__(args=[trans], shape=(todims or trans.todims, fromdims), dtype=float) - - def evalf(self, chains): - result = numpy.zeros((1, *self.shape), dtype=float) - to0 = from0 = 0 - for chain, todims, fromdims in zip(*chains): - to1, from1 = to0 + todims, from0 + fromdims - result[:,to0:to1,from0:from1] = transform.linear(chain, todims) - to0, from0 = to1, from1 - assert (to0, from0) == self.shape - return result + def __init__(self, roots:types.tuple[strictroot], transforms:transformseq.stricttransforms, index:asarray, fromdims:types.strictint): + assert frozenset(roots) == index.roots + self._roots = roots + self._transforms = transforms + super().__init__(args=[index], shape=(builtins.sum(root.ndims for root in roots), fromdims), dtype=float) - @property - def simplified(self): - if isinstance(self._trans, EmptyTransformChain) and self._trans.todims == self._trans.fromdims: - return eye(self._trans.todims) - else: - return self + def evalf(self, index): + index, = index + result = numpy.zeros(self.shape, dtype=float) + to1 = from1 = 0 + for root, chain in zip(self._roots, self._transforms[index]): + assert chain[0].todims == root.ndims + to0, from0, to1, from1 = to1, from1, to1 + root.ndims, from1 + chain[-1].fromdims + result[to0:to1,from0:from1] = transform.linear(chain, root.ndims) + assert (to1, from1) == self.shape + return result[_] class Inverse(Array): ''' @@ -1421,6 +1394,8 @@ def simplified(self): if retval is not None: assert retval.shape == self.shape return retval.simplified + if func.shape[-1] == func.shape[-2] == 1: + return (1 / func).simplified return Inverse(func) def evalf(self, arr): @@ -1608,6 +1583,27 @@ def _unravel(self, axis, shape): if axis != self.axis: return Concatenate([Unravel(func, axis, shape) for func in self.funcs], self.axis+(self.axis>axis)) + def _squareblockdiagonal(self): + if self.axis < self.ndim-2: + return None, None + sizes = tuple(func1.shape[self.axis] for func1 in self.funcs) + axis2 = self.ndim-1 if self.axis == self.ndim-2 else self.ndim-2 + if not all(isinstance(func1, Concatenate) and func1.axis == axis2 and tuple(func2.shape[axis2] for func2 in func1.funcs) == sizes and all(iszero(func2) for j, func2 in enumerate(func1.funcs) if i != j) for i, func1 in enumerate(self.funcs)): + return None, None + return tuple(func1.funcs[i] for i, func1 in enumerate(self.funcs)), sizes + + def _inverse(self): + blocks, sizes = self._squareblockdiagonal() + if blocks is None: + return + return concatenate([concatenate([inverse(b) if i == j else zeros((*self.shape[:-2], n, n), self.dtype) for j, n in enumerate(sizes)], self.ndim-2) for i, b in enumerate(blocks)], self.ndim-1) + + def _determinant(self): + blocks, sizes = self._squareblockdiagonal() + if blocks is None: + return + return functools.reduce(operator.mul, map(determinant, blocks)) + class Interpolate(Array): 'interpolate uniformly spaced data; stepwise for now' @@ -1646,6 +1642,8 @@ def simplified(self): if retval is not None: assert retval.shape == self.shape return retval.simplified + if func.shape[-1] == func.shape[-2] == 1: + return func[...,0,0] return Determinant(func) def evalf(self, arr): @@ -3031,9 +3029,15 @@ def _derivative(self, var, seen): @util.positional_only def prepare_eval(self, *, subsamples, kwargs=...): - ndims = builtins.sum(subsample.ndimsmanifold for subsample in subsamples) - jac = functools.reduce(derivative, self._derivativestack, asarray(jacobian(self._geom, ndims))) - return jac.prepare_eval(ndims=ndims, **kwargs) + ndimsmanifold = 0 + for subsample in subsamples: + if self.roots.isdisjoint(subsample.roots): + continue + if not frozenset(subsample.roots) <= self.roots: + raise ValueError('Cannot compute jacobian.') + ndimsmanifold += subsample.ndimsmanifold + jac = functools.reduce(derivative, self._derivativestack, asarray(jacobian(self._geom, ndimsmanifold))) + return jac.prepare_eval(subsamples=subsamples, **kwargs) class Ravel(Array): @@ -3607,8 +3611,8 @@ def __init__(self, ndofs:types.strictint, transforms:transformseq.stricttransfor self.transforms = transforms self.ndimsdomain = ndims - self._index, head, tail = TransformsIndexWithTail(self.transforms, ndims, trans) - self._points = ApplyTransforms(head, tail) + self._index, tail, linear = TransformsIndexWithTail(self.transforms, ndims, trans) + self._points = ApplyTransforms(tail, linear) self._trans = trans super().__init__(args=(self._index, self._points), shape=(ndofs,), dtype=float) @@ -4365,7 +4369,7 @@ def blocks(arg): def rootcoords(roots): if isinstance(roots, Root): - return ApplyTransforms(EmptyTransformChain(roots=(roots,), todims=(roots.ndims,), fromdims=(roots.ndims,)), SelectChain((roots,))) + return ApplyTransforms(SelectChain((roots,)), eye(roots.ndims)) else: return concatenate([rootcoords(root) for root in roots], axis=0) diff --git a/nutils/sample.py b/nutils/sample.py index a9d8d85b8..2bf0a6ccc 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -254,9 +254,9 @@ def basis(self): '''Basis-like function that for every point in the sample evaluates to the unit vector corresponding to its index.''' - index, head, tail = function.TransformsIndexWithTail(self.transforms[0], self.ndims, function.SelectChain(self.roots)) + index, tail, linear = function.TransformsIndexWithTail(self.transforms[0], self.ndims, function.SelectChain(self.roots)) I = function.Elemwise(self.index, index, dtype=int) - B = function.Sampled(function.ApplyTransforms(head, tail), expect=function.take(self.allcoords, I, axis=0)) + B = function.Sampled(function.ApplyTransforms(tail, linear), expect=function.take(self.allcoords, I, axis=0)) return function.Inflate(func=B, dofmap=I, length=self.npoints, axis=0) def asfunction(self, array): @@ -354,6 +354,10 @@ def getsubsamples(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) return self._sample1.getsubsamples(ielem1) + self._sample2.getsubsamples(ielem2) + @property + def subsamplemetas(self): + return self._sample1.subsamplemetas + self._sample2.subsamplemetas + class Integral(types.Singleton): '''Postponed integration. diff --git a/nutils/topology.py b/nutils/topology.py index f79881e5c..26a1f008d 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -1450,6 +1450,14 @@ def getitem(self, item): def refined(self): return DisjointUnionTopology([topo.refined for topo in self._topos], self._names) + @property + def boundary(self): + return DisjointUnionTopology([topo.boundary for topo in self._topos]) + + @property + def interfaces(self): + return DisjointUnionTopology([topo.interfaces for topo in self._topos]) + class SubsetTopology(Topology): 'trimmed' From db19da2254bc353701788579f16105e32ddb1e9c Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 3 Mar 2020 15:39:18 +0100 Subject: [PATCH 047/119] WIP --- nutils/function.py | 108 +++++++++++++++++++++++++++++++++------------ 1 file changed, 81 insertions(+), 27 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 6806a8143..9b4690136 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -474,66 +474,81 @@ def evalf(self, subsamples): def prepare_eval(self, *, opposite=False, kwargs=...): return SelectChain(self.ordered_roots, 1-self.n) if opposite else self -class TransformChainFromTuple(TransformChain): +class EmptyTransformChain(TransformChain): - __slots__ = 'index' + __slots__ = () - def __init__(self, roots:types.tuple[strictroot], values:strictevaluable, index:types.strictint, todims:types.strictint=None, fromdims:types.strictint=None): - assert 0 <= index < len(values) - self.index = index - super().__init__(roots, args=[values], todims=todims, fromdims=fromdims) + @types.apply_annotations + def __init__(self, roots:types.tuple[strictroot], ndims=types.strictint): + super().__init__(roots=roots, args=[], todims=ndims, fromdims=ndims) - def evalf(self, values): - return values[self.index] + def evalf(self): + return ((),)*len(self.roots) class TransformsIndexWithTail(Evaluable): - __slots__ = '_transforms', '_fromdims', '_trans' + __slots__ = 'transforms', 'ndims', 'trans' @types.apply_annotations - def __init__(self, transforms:transformseq.stricttransforms, fromdims:types.strictint, trans:types.strict[TransformChain]): - self._transforms = transforms - self._fromdims = fromdims - self._trans = trans + def __init__(self, transforms:transformseq.stricttransforms, ndims:types.strictint, trans:types.strict[TransformChain]): + self.transforms = transforms + self.ndims = ndims + self.trans = trans super().__init__(args=[trans]) @property def roots(self): - return self._trans.roots + return self.trans.roots def evalf(self, chains): - index, tails = self._transforms.index_with_tail(chains) + index, tails = self.transforms.index_with_tail(chains) tailtodims = tuple(t[0].todims if t else c[-1].fromdims for t, c in zip(tails, chains)) - assert builtins.sum(tailtodims) == self._fromdims - return numpy.array(index)[None], tails + assert builtins.sum(tailtodims) == self.ndims + return index, tails def __len__(self): return 3 @property def index(self): - return ArrayFromTuple(self, index=0, shape=(), dtype=int) + return IndexFromTransformsIndexWithTail(self) @property def tail(self): - return TransformChainFromTuple(self._trans.ordered_roots, self, index=1, todims=self._fromdims) + return TransformChainFromTransformsIndexWithTail(self) @property def linear(self): - return Linear(self._trans.ordered_roots, self._transforms, self.index, self._fromdims) + return Linear(self.trans.ordered_roots, self.transforms, self.index, self.ndims) def __iter__(self): yield self.index yield self.tail yield self.linear -# @util.positional_only -# def prepare_eval(self, *, subsamples, kwargs=...): -# self = TransformsIndexWithTail(self._transforms, self._fromdims, self._trans.prepare_eval(subsamples=subsamples, **kwargs)) -# for subsample in subsamples: -# if self._trans.ordered_roots == subsample.roots and isinstance(self._trans, SelectChain) and self._transforms == subsamples.transforms[self._trans.n]: -# return -# +class TransformChainFromTransformsIndexWithTail(TransformChain): + + __slots__ = '_indextail' + + @types.apply_annotations + def __init__(self, indextail:types.strict[TransformsIndexWithTail]): + self._indextail = indextail + super().__init__(roots=indextail.trans.ordered_roots, args=[indextail], todims=indextail.ndims) + + def evalf(self, indextail): + index, tail = indextail + return tail + + @util.positional_only + def prepare_eval(self, *, subsamples, kwargs=...): + self = TransformChainFromTransformsIndexWithTail(self._indextail.prepare_eval(subsamples=subsamples, **kwargs)) + trans = self._indextail.trans + if isinstance(trans, SelectChain): + for isubsample, subsample in enumerate(subsamples): + if trans.ordered_roots == subsample.roots and self._indextail.transforms == subsample.transforms[trans.n]: + return EmptyTransformChain(roots=self.ordered_roots, ndims=self.todims) + return self + # ARRAYFUNC # # The main evaluable. Closely mimics a numpy array. @@ -2483,6 +2498,45 @@ def evalf(self, arrays): assert isinstance(arrays, tuple) return arrays[self.index] +class IndexFromTransformsIndexWithTail(Array): + + __slots__ = '_indextail' + + def __init__(self, indextail:types.strict[TransformsIndexWithTail]): + self._indextail = indextail + super().__init__(args=[indextail], shape=(), dtype=int) + + def evalf(self, indextail): + index, tail = indextail + return numpy.array([index], int) + + @util.positional_only + def prepare_eval(self, *, subsamples, kwargs=...): + self = IndexFromTransformsIndexWithTail(self._indextail.prepare_eval(subsamples=subsamples, **kwargs)) + trans = self._indextail.trans + if isinstance(trans, SelectChain): + for isubsample, subsample in enumerate(subsamples): + if trans.ordered_roots == subsample.roots and self._indextail.transforms == subsample.transforms[trans.n]: + return IndexFromSubsample(isubsample, trans.ordered_roots) + return self + +class IndexFromSubsample(Array): + + __slots__ = '_isubsample', '_roots' + + @types.apply_annotations + def __init__(self, isubsample:types.strictint, roots:types.tuple[strictroot]): + self._isubsample = isubsample + self._roots = roots + super().__init__(args=[SUBSAMPLES], shape=(), dtype=int) + + @property + def roots(self): + return frozenset(self._roots) + + def evalf(self, subsamples): + return numpy.array([subsamples[self._isubsample].ielem], int) + class Zeros(Array): 'zero' From bf55a5ef93a1daa00da66755a2144d124cf69e7f Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 3 Mar 2020 15:53:05 +0100 Subject: [PATCH 048/119] WIP --- examples/burgers.py | 8 ++++---- examples/cylinderflow.py | 2 +- examples/drivencavity-compatible.py | 2 +- examples/laplace.py | 2 -- examples/platewithhole-nurbs.py | 2 +- nutils/function.py | 4 ++-- 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/examples/burgers.py b/examples/burgers.py index 8fee0aa8f..b23493778 100644 --- a/examples/burgers.py +++ b/examples/burgers.py @@ -27,7 +27,7 @@ def main(nelems:int, ndims:int, degree:int, timescale:float, newtontol:float, en Stopping time. ''' - domain, geom = mesh.newrectilinear([numpy.linspace(0,1,nelems+1)]*ndims, periodic=range(ndims)) + domain, geom = mesh.rectilinear([numpy.linspace(0,1,nelems+1)]*ndims, periodic=range(ndims)) ns = function.Namespace() ns.x = geom @@ -93,6 +93,6 @@ def test_1d_p2(self): def test_2d_p1(self): lhs = main(ndims=2, nelems=4, timescale=.1, degree=1, endtime=.01, newtontol=1e-5) self.assertAlmostEqual64(lhs, ''' - eNoNyKENhEAQRuGEQsCv2SEzyQZHDbRACdsDJNsBjqBxSBxBHIgJ9xsqQJ1Drro1L1/eYBZceGz8njrR - yacm8UQLBvPYCw1airpyUVYSJLhKijK4IC01WDnqqxvX8OTl427aU73sctPGr3qqceBnRzOjo0xy9JpJ - R73m6R6YMZo/Q+FCLQ==''') + eNoNx6ENhjAQBtCEQcDX9Mhd0uCYgRUYoTtAwga4P2gcEkcQUHGBzzABCoes+slTrzcT4hk0aDwn9ObA + bQcKHHig2x6oUFOWF1JIltdUIerMnXTuIzNHfXVhL5vbnJeFXy3h6aJVVrnIU4kdj20okUQaeuyOnxmR + otVWU4zf/jlhQi0=''') diff --git a/examples/cylinderflow.py b/examples/cylinderflow.py index 8dbfb3ba0..1923206c2 100644 --- a/examples/cylinderflow.py +++ b/examples/cylinderflow.py @@ -41,7 +41,7 @@ def main(nelems:int, degree:int, reynolds:float, rotation:float, timestep:float, elemangle = 2 * numpy.pi / nelems melems = int(numpy.log(2*maxradius) / elemangle + .5) treelog.info('creating {}x{} mesh, outer radius {:.2f}'.format(melems, nelems, .5*numpy.exp(elemangle*melems))) - domain, geom = mesh.newrectilinear([melems, nelems], periodic=(1,)) + domain, geom = mesh.rectilinear([melems, nelems], periodic=(1,)) domain = domain.withboundary(inner='left', outer='right') ns = function.Namespace() diff --git a/examples/drivencavity-compatible.py b/examples/drivencavity-compatible.py index f7bcf3375..9709bf005 100644 --- a/examples/drivencavity-compatible.py +++ b/examples/drivencavity-compatible.py @@ -27,7 +27,7 @@ def main(nelems:int, degree:int, reynolds:float): ''' verts = numpy.linspace(0, 1, nelems+1) - domain, geom = mesh.newrectilinear([verts, verts]) + domain, geom = mesh.rectilinear([verts, verts]) ns = function.Namespace() ns.x = geom diff --git a/examples/laplace.py b/examples/laplace.py index 47448aba0..f72a1c711 100644 --- a/examples/laplace.py +++ b/examples/laplace.py @@ -70,8 +70,6 @@ def main(nelems:int, etype:str, btype:str, degree:int): res = domain.integral('basis_n,i u_,i d:x' @ ns, degree=degree*2) res -= domain.boundary['right'].integral('basis_n cos(1) cosh(x_1) d:x' @ ns, degree=degree*2) - print(('basis_n,i u_,i d:x' @ ns).prepare_eval(subsamples=domain.sample('gauss', 1).subsamplemetas).simplified.asciitree()) - # The Dirichlet constraints are set by finding the coefficients that minimize # the error: # diff --git a/examples/platewithhole-nurbs.py b/examples/platewithhole-nurbs.py index ecdaf884b..4dbb39911 100644 --- a/examples/platewithhole-nurbs.py +++ b/examples/platewithhole-nurbs.py @@ -27,7 +27,7 @@ def main(nrefine:int, traction:float, radius:float, poisson:float): ''' # create the coarsest level parameter domain - domain, geom0 = mesh.newrectilinear([1, 2]) + domain, geom0 = mesh.rectilinear([1, 2]) bsplinebasis = domain.basis('spline', degree=2) controlweights = numpy.ones(12) controlweights[1:3] = .5 + .25 * numpy.sqrt(2) diff --git a/nutils/function.py b/nutils/function.py index 9b4690136..1d233a41d 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -545,7 +545,7 @@ def prepare_eval(self, *, subsamples, kwargs=...): trans = self._indextail.trans if isinstance(trans, SelectChain): for isubsample, subsample in enumerate(subsamples): - if trans.ordered_roots == subsample.roots and self._indextail.transforms == subsample.transforms[trans.n]: + if trans.ordered_roots == subsample.roots and self._indextail.transforms == subsample.transforms[trans.n if len(subsample.transforms) > 1 else 0]: return EmptyTransformChain(roots=self.ordered_roots, ndims=self.todims) return self @@ -2516,7 +2516,7 @@ def prepare_eval(self, *, subsamples, kwargs=...): trans = self._indextail.trans if isinstance(trans, SelectChain): for isubsample, subsample in enumerate(subsamples): - if trans.ordered_roots == subsample.roots and self._indextail.transforms == subsample.transforms[trans.n]: + if trans.ordered_roots == subsample.roots and self._indextail.transforms == subsample.transforms[trans.n if len(subsample.transforms) > 1 else 0]: return IndexFromSubsample(isubsample, trans.ordered_roots) return self From 596697c15b2a10aeb9384f5956194486c7b7a5b2 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 4 Mar 2020 14:16:15 +0100 Subject: [PATCH 049/119] update --- nutils/function.py | 14 ++++++++---- nutils/mesh.py | 2 +- nutils/topology.py | 15 +++++++------ nutils/transform.py | 40 ++++++++++++++++++++++++++++++++- nutils/transformseq.py | 50 ++++++++++++++---------------------------- tests/test_function.py | 2 +- tests/test_topology.py | 31 +++++++------------------- 7 files changed, 83 insertions(+), 71 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 1d233a41d..4d2e2212f 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -540,8 +540,11 @@ def evalf(self, indextail): return tail @util.positional_only - def prepare_eval(self, *, subsamples, kwargs=...): - self = TransformChainFromTransformsIndexWithTail(self._indextail.prepare_eval(subsamples=subsamples, **kwargs)) + def prepare_eval(self, *, kwargs=...): + self = TransformChainFromTransformsIndexWithTail(self._indextail.prepare_eval(**kwargs)) + if 'subsamples' not in kwargs: + return self + subsamples = kwargs['subsamples'] trans = self._indextail.trans if isinstance(trans, SelectChain): for isubsample, subsample in enumerate(subsamples): @@ -2511,8 +2514,11 @@ def evalf(self, indextail): return numpy.array([index], int) @util.positional_only - def prepare_eval(self, *, subsamples, kwargs=...): - self = IndexFromTransformsIndexWithTail(self._indextail.prepare_eval(subsamples=subsamples, **kwargs)) + def prepare_eval(self, *, kwargs=...): + self = IndexFromTransformsIndexWithTail(self._indextail.prepare_eval(**kwargs)) + if 'subsamples' not in kwargs: + return self + subsamples = kwargs['subsamples'] trans = self._indextail.trans if isinstance(trans, SelectChain): for isubsample, subsample in enumerate(subsamples): diff --git a/nutils/mesh.py b/nutils/mesh.py index e95f8035d..8fd8eb301 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -45,7 +45,7 @@ def line(nodes, periodic=False, bnames=None, *, rootid='line'): uniform = numpy.equal(nodes, offset + numpy.arange(nelems+1) * scale).all() root = function.Root(rootid, 1) domain = topology.StructuredLine(root, 0, nelems, periodic=periodic, bnames=bnames) - geom = function.rootcoords(root) * scale + offset if uniform else domain.basis('std', degree=1, periodic=False).dot(nodes) + geom = function.rootcoords(root) * scale + offset if uniform else domain.basis('std', degree=1, periodic=[]).dot(nodes)[_] return domain, geom @log.withcontext diff --git a/nutils/topology.py b/nutils/topology.py index 26a1f008d..1c862bf56 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -1552,15 +1552,15 @@ def addtrimmededge(ielem, etrans): elemfromdims = tuple(t[-1].fromdims for t in elemtrans) oppelemfromdims = tuple(t[-1].fromdims for t in self.basetopo.transforms[ioppelem]) trimmedreferences.append(edgeref) - trimmedtransforms.append(addtrimmededge(ielem, edgetrans.separate(elemfromdims))) - trimmedopposites.append(addtrimmededge(ioppelem, oppref.edge_transforms[ioppedge].separate(oppelemfromdims))) + trimmedtransforms.append(addtrimmededge(ielem, edgetrans)) + trimmedopposites.append(addtrimmededge(ioppelem, oppref.edge_transforms[ioppedge])) # The last edges of newref (beyond the number of edges of the original) # cannot have opposites and are added to the trimmed group directly. for edgetrans, edgeref in newref.edges[len(ioppelems):]: elemfromdims = tuple(t[-1].fromdims for t in elemtrans) trimmedreferences.append(edgeref) - trimmedtransforms.append(addtrimmededge(ielem, edgetrans.separate(elemfromdims))) - trimmedopposites.append(addtrimmededge(ielem, edgetrans.flipped.separate(elemfromdims))) + trimmedtransforms.append(addtrimmededge(ielem, edgetrans)) + trimmedopposites.append(addtrimmededge(ielem, edgetrans.flipped)) trimmedreferences = elementseq.asreferences(trimmedreferences, self.ndims-1) trimmedielems, trimmededges = zip(*sorted(trimmededges.items(), key=lambda item: item[0])) trimmedoffsets = dict(zip(trimmedielems, numpy.cumsum([0, *map(len, trimmededges)]))) @@ -1733,7 +1733,6 @@ def boundary(self): bindices = [] for index in indices: for trans in transform.unempty_edge_transforms(level.transforms[index], level.references[index]): - #trans = tuple(map(transform.uppermost, trans)) try: bindices.append(bindex(trans)) except ValueError: @@ -1743,8 +1742,6 @@ def boundary(self): bindices.sort() assert not numpy.equal(bindices[1:], bindices[:-1]).any() bindices_per_level.append(bindices) - foo = HierarchicalTopology(basebtopo, bindices_per_level) - assert foo.integral(function.asarray(1), degree=1).eval() == basebtopo.integral(function.asarray(1), degree=1).eval() return HierarchicalTopology(basebtopo, bindices_per_level) @property @@ -1949,6 +1946,10 @@ def __repr__(self): def shape(self): return self._left.shape + self._right.shape + @property + def shape(self): + return self._left.shape + self._right.shape + @property def connectivity(self): s = len(self._right) diff --git a/nutils/transform.py b/nutils/transform.py index 18123ac5a..4e9c525dd 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -218,6 +218,8 @@ def isflipped(self): return self.fromdims > 0 and self.det < 0 def transform_poly(self, coeffs, startdim): + if self.fromdims == 0: + return coeffs assert startdim >= 0 assert coeffs.ndim >= startdim + self.fromdims + 1 degree = coeffs.shape[startdim + 1] - 1 @@ -630,7 +632,7 @@ def flipped(self): return Manifold(self.fromdims, self.trans.flipped) def swapdown(self, other): - if isinstance(other, TensorChild): + if isinstance(other, (TensorChild, SimplexChild)): return ScaledUpdim(other, self), Identity(self.fromdims) def separate(self, septodims): @@ -682,6 +684,42 @@ def edge_transforms(trans, ref): def unempty_edge_transforms(trans, ref): return (append_edge(trans, etrans) for etrans, eref in ref.edges if eref) +def index_child_transforms(children, chains, septodims): + assert len(chains) == len(septodims) + chains = tuple(map(uppermost, chains)) + for ichild, child in enumerate(children): + child = child.separate(septodims) + if all(chain == (SimplexChild(0,0) if ctrans == Identity(0) else ctrans,) for chain, ctrans in zip(chains, child)): + return ichild + raise ValueError + +def index_edge_transforms(edges, chains, septodims): + assert len(chains) == len(septodims) + chains = tuple(map(canonical, chains)) + for iedge, edge in enumerate(edges): + edge = edge.separate(septodims) + if all(chain == () if type(etrans) is Identity else chain == (etrans,) for chain, etrans in zip(chains, edge)): + return iedge + raise ValueError + +def index_child_transforms_with_tail(children, chains, septodims): + assert len(chains) == len(septodims) + chains = tuple(map(uppermost, chains)) + for ichild, child in enumerate(children): + child = child.separate(septodims) + if all(chain and chain[0] == (SimplexChild(0,0) if ctrans == Identity(0) else ctrans) for chain, ctrans in zip(chains, child)): + return ichild, tuple(chain[1:] for chain in chains) + raise ValueError + +def index_edge_transforms_with_tail(edges, chains, septodims): + assert len(chains) == len(septodims) + chains = tuple(map(canonical, chains)) + for iedge, edge in enumerate(edges): + edge = edge.separate(septodims) + if all(chain and chain[0] == etrans for chain, etrans in zip(chains, edge) if type(etrans) != Identity): + return iedge, tuple(chain if type(etrans) == Identity else chain[1:] for chain, etrans in zip(chains, edge)) + raise ValueError + def transform_poly(items, coeffs): startdim = 0 for item in items: diff --git a/nutils/transformseq.py b/nutils/transformseq.py index e65110ebb..868b2373b 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -767,15 +767,8 @@ def index_with_tail(self, trans): iparent, parenttail = self._parent.index_with_tail(trans) if not any(parenttail): raise ValueError - parenttail = tuple(map(transform.canonical if self._updim else transform.uppermost, parenttail)) todims = [a[0].todims if a else b[-1].fromdims for a, b in zip(parenttail, trans)] - for iderived, derivedtrans in enumerate(self._derived_transforms(self._parent_references[iparent])): - sepderivedtrans = tuple(None if type(t) is transform.Identity and self._updim else transform.SimplexChild(0,0) if t == transform.Identity(0) and not self._updim else t for t in derivedtrans.separate(todims)) - if all(b is None or a and a[0] == b for a, b in zip(parenttail, sepderivedtrans)): - tail = tuple(a if b is None else a[1:] for a, b in zip(parenttail, sepderivedtrans)) - break - else: - raise ValueError + iderived, tail = (transform.index_edge_transforms_with_tail if self._updim else transform.index_child_transforms_with_tail)(self._derived_transforms(self._parent_references[iparent]), parenttail, todims) return self._offsets[iparent]+iderived, tail class UniformDerivedTransforms(Transforms): @@ -828,15 +821,8 @@ def index_with_tail(self, trans): iparent, parenttail = self._parent.index_with_tail(trans) if not any(parenttail): raise ValueError - parenttail = tuple(map(transform.canonical if self._updim else transform.uppermost, parenttail)) todims = [a[0].todims if a else b[-1].fromdims for a, b in zip(parenttail, trans)] - for iderived, derivedtrans in enumerate(self._derived_transforms): - sepderivedtrans = tuple(None if type(t) is transform.Identity and self._updim else transform.SimplexChild(0,0) if t == transform.Identity(0) and not self._updim else t for t in derivedtrans.separate(todims)) - if all(b is None or a and a[0] == b for a, b in zip(parenttail, sepderivedtrans)): - tail = tuple(a if b is None else a[1:] for a, b in zip(parenttail, sepderivedtrans)) - break - else: - raise ValueError + iderived, tail = (transform.index_edge_transforms_with_tail if self._updim else transform.index_child_transforms_with_tail)(self._derived_transforms, parenttail, todims) return iparent*len(self._derived_transforms) + iderived, tail class TrimmedEdgesTransforms(Transforms): @@ -845,7 +831,7 @@ class TrimmedEdgesTransforms(Transforms): __cache__ = '_offsets' @types.apply_annotations - def __init__(self, parent:stricttransforms, edges:types.tuple[types.tuple[types.tuple[transform.stricttransformitem]]]): + def __init__(self, parent:stricttransforms, edges:types.tuple[types.tuple[transform.stricttransformitem]]): assert len(edges) == len(parent) self._parent = parent self._edges = edges @@ -859,9 +845,10 @@ def __len__(self): return self._offsets[-1] def __iter__(self): - for ptrans, edges in zip(self._parent, self._edges): - for etrans in edges: - yield tuple(h if type(t) is transform.Identity else h+(t,) for h, t in zip(ptrans, etrans)) + for pchains, edges in zip(self._parent, self._edges): + todims = tuple(pchain[-1].fromdims for pchain in pchains) + for edge in edges: + yield tuple(pchain if type(etrans) == transform.Identity else pchain+(etrans,) for pchain, etrans in zip(pchains, edge.separate(todims))) def __getitem__(self, index): if not numeric.isint(index): @@ -870,20 +857,15 @@ def __getitem__(self, index): iparent = numpy.searchsorted(self._offsets, index, side='right')-1 assert 0 <= iparent < len(self._offsets)-1 iedge = index - self._offsets[iparent] - return tuple(h if type(t) is transform.Identity else h+(t,) for h, t in zip(self._parent[iparent], self._edges[iparent][iedge])) - - def index_with_tail(self, trans): - iparent, tails = self._parent.index_with_tail(trans) - if not all(tails): - raise ValueError - tails = tuple(map(transform.canonical, tails)) - for iedge, edges in enumerate(self._edges[iparent]): - if all(type(e) is transform.Identity or t and t[0] == e for e, t in zip(edges, tails)): - break - else: - raise ValueError - #iedge = self._edges[iparent].index(tuple(tail[0] for tail in tails)) - return self._offsets[iparent]+iedge, tuple(tail if type(e) is transform.Identity else tail[1:] for tail, e in zip(tails, edges)) + pchains = self._parent[iparent] + todims = tuple(pchain[-1].fromdims for pchain in pchains) + return tuple(pchain if type(etrans) == transform.Identity else pchain+(etrans,) for pchain, etrans in zip(pchains, self._edges[iparent][iedge].separate(todims))) + + def index_with_tail(self, chains): + iparent, parenttails = self._parent.index_with_tail(chains) + todims = tuple(tail[0].todims if tail else chain[-1].fromdims for tail, chain in zip(parenttails, chains)) + iedge, tails = transform.index_edge_transforms_with_tail(self._edges[iparent], parenttails, todims) + return self._offsets[iparent]+iedge, tails class ProductTransforms(Transforms): '''The product of two :class:`Transforms` objects. diff --git a/tests/test_function.py b/tests/test_function.py index fcbde03c2..e0ceb5823 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -20,7 +20,7 @@ def setUp(self): else: raise Exception('invalid ndim {!r}'.format(self.ndim)) numpy.random.seed(0) - self.args = [function.Guard(function.Polyval(numeric.dot(numpy.random.uniform(size=shape+poly.shape[:1], low=self.low, high=self.high), poly), function.rootcoords(domain.root))) for shape in self.shapes] + self.args = [function.Guard(function.Polyval(numeric.dot(numpy.random.uniform(size=shape+poly.shape[:1], low=self.low, high=self.high), poly), function.rootcoords(domain.roots))) for shape in self.shapes] if self.pass_geom: self.args += [self.geom] self.sample = domain.sample('uniform', 2) diff --git a/tests/test_topology.py b/tests/test_topology.py index 74c73b184..c79314f02 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -89,20 +89,6 @@ def test_boundaries(self): structure(ndims=3, refine=1) -@parametrize -class structured_prop_periodic(TestCase): - - def test(self): - bnames = 'left', 'top', 'front' - side = bnames[self.sdim] - domain, geom = mesh.rectilinear([2]*self.ndim, periodic=self.periodic) - self.assertEqual(list(domain.boundary[side].periodic), [i if i < self.sdim else i-1 for i in self.periodic if i != self.sdim]) - -structured_prop_periodic('2d_1_0', ndim=2, periodic=[1], sdim=0) -structured_prop_periodic('2d_0_1', ndim=2, periodic=[0], sdim=1) -structured_prop_periodic('3d_0,2_1', ndim=3, periodic=[0,2], sdim=1) - - class picklability(TestCase): def assert_pickle_dump_load(self, data): @@ -301,20 +287,19 @@ def test_connectivity(self): def test_boundary(self): for trans in self.domain.boundary.transforms: - ielem, tail = self.domain.transforms.index_with_tail(trans) - (etrans,), = tail - iedge = self.domain.references[ielem].edge_transforms.index(etrans) + ielem, tails = self.domain.transforms.index_with_tail(trans) + todims = tuple(t[-1].fromdims for t in self.domain.transforms[ielem]) + iedge = transform.index_edge_transforms(self.domain.references[ielem].edge_transforms, tails, todims) self.assertEqual(self.domain.connectivity[ielem][iedge], -1) def test_interfaces(self): itopo = self.domain.interfaces for trans, opptrans in zip(itopo.transforms, itopo.opposites): - ielem, tail = self.domain.transforms.index_with_tail(trans) - (etrans,), = tail - iedge = self.domain.references[ielem].edge_transforms.index(etrans) - ioppelem, opptail = self.domain.transforms.index_with_tail(opptrans) - (eopptrans,), = opptail - ioppedge = self.domain.references[ioppelem].edge_transforms.index(eopptrans) + ielem, tails = self.domain.transforms.index_with_tail(trans) + todims = tuple(t[-1].fromdims for t in self.domain.transforms[ielem]) + iedge = transform.index_edge_transforms(self.domain.references[ielem].edge_transforms, tails, todims) + ioppelem, opptails = self.domain.transforms.index_with_tail(opptrans) + ioppedge = transform.index_edge_transforms(self.domain.references[ioppelem].edge_transforms, opptails, todims) self.assertEqual(self.domain.connectivity[ielem][iedge], ioppelem) self.assertEqual(self.domain.connectivity[ioppelem][ioppedge], ielem) From 6f882baf50ef33566475bfee18d442894a7d2b79 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Fri, 6 Mar 2020 15:39:06 +0100 Subject: [PATCH 050/119] update --- nutils/element.py | 2 +- nutils/topology.py | 8 ++++---- nutils/transform.py | 25 ++++++++++++++----------- nutils/transformseq.py | 18 ++++++++++-------- tests/test_topology.py | 6 +++--- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/nutils/element.py b/nutils/element.py index e7ab6a465..95f47e488 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -434,7 +434,7 @@ def child_refs(self): @property def child_transforms(self): - return tuple(transform.SimplexChild(self.ndims, ichild) for ichild in range(2**self.ndims)) + return tuple(transform.SimplexChild(self.ndims, ichild) for ichild in range(2**self.ndims)) if self.ndims else (transform.Identity(0),) @property def ribbons(self): diff --git a/nutils/topology.py b/nutils/topology.py index 1c862bf56..264467fa3 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -1552,15 +1552,15 @@ def addtrimmededge(ielem, etrans): elemfromdims = tuple(t[-1].fromdims for t in elemtrans) oppelemfromdims = tuple(t[-1].fromdims for t in self.basetopo.transforms[ioppelem]) trimmedreferences.append(edgeref) - trimmedtransforms.append(addtrimmededge(ielem, edgetrans)) - trimmedopposites.append(addtrimmededge(ioppelem, oppref.edge_transforms[ioppedge])) + trimmedtransforms.append(addtrimmededge(ielem, edgetrans.separate(elemfromdims))) + trimmedopposites.append(addtrimmededge(ioppelem, oppref.edge_transforms[ioppedge].separate(oppelemfromdims))) # The last edges of newref (beyond the number of edges of the original) # cannot have opposites and are added to the trimmed group directly. for edgetrans, edgeref in newref.edges[len(ioppelems):]: elemfromdims = tuple(t[-1].fromdims for t in elemtrans) trimmedreferences.append(edgeref) - trimmedtransforms.append(addtrimmededge(ielem, edgetrans)) - trimmedopposites.append(addtrimmededge(ielem, edgetrans.flipped)) + trimmedtransforms.append(addtrimmededge(ielem, edgetrans.separate(elemfromdims))) + trimmedopposites.append(addtrimmededge(ielem, edgetrans.flipped.separate(elemfromdims))) trimmedreferences = elementseq.asreferences(trimmedreferences, self.ndims-1) trimmedielems, trimmededges = zip(*sorted(trimmededges.items(), key=lambda item: item[0])) trimmedoffsets = dict(zip(trimmedielems, numpy.cumsum([0, *map(len, trimmededges)]))) diff --git a/nutils/transform.py b/nutils/transform.py index 4e9c525dd..28e26d139 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -140,7 +140,7 @@ def separate(self, septodims): if septodims == (self.todims,): return self, elif self.todims in septodims: - return tuple(self if todims == self.todims else Identity(0) for todims in septodims) + return tuple(Identity(0) if todims == 0 else self for todims in septodims) else: raise ValueError('Cannot separate {} into TransformItems with todims {}.'.format(self, septodims)) @@ -413,6 +413,9 @@ def swapup(self, other): if isinstance(other, SimplexChild): ichild, iedge = self.swap[self.iedge][other.ichild] return SimplexChild(self.todims, ichild), SimplexEdge(self.todims, iedge, self.inverted) + elif self.fromdims == 0 and other == Identity(0): + ichild, iedge = self.swap[self.iedge][0] + return SimplexChild(self.todims, ichild), SimplexEdge(self.todims, iedge, self.inverted) def swapdown(self, other): # prioritize decending transformations, i.e. change scale << updim to updim << scale @@ -424,13 +427,14 @@ def swapdown(self, other): except ValueError: pass else: - return SimplexEdge(self.todims, iedge, self.inverted), SimplexChild(self.fromdims, ichild) + return SimplexEdge(self.todims, iedge, self.inverted), SimplexChild(self.fromdims, ichild) if self.fromdims else Identity(0) class SimplexChild(Square): __slots__ = 'ichild', def __init__(self, ndims, ichild): + assert ndims > 0, 'use `Identity(0)` instead' self.ichild = ichild if ichild <= ndims: linear = numpy.eye(ndims) * .5 @@ -482,11 +486,11 @@ def ext(self): return types.frozenarray(-ext if self.isflipped else ext, copy=False) def swapdown(self, other): - if isinstance(other, TensorChild): + if isinstance(other, (TensorChild, SimplexChild)): return ScaledUpdim(other, self), Identity(self.fromdims) def separate(self, septodims): - return tuple(map(ScaledUpdim, self.trans1.separate(septodims), self.trans2.separate(septodims))) + return tuple(ScaledUpdim(a, b) if todims else Identity(0) for todims, a, b in zip(septodims, self.trans1.separate(septodims), self.trans2.separate(septodims))) class TensorEdge1(Updim): @@ -503,7 +507,7 @@ def swapup(self, other): swapped = self.trans.swapup(other.trans1) trans2 = other.trans2 elif isinstance(other, (TensorChild, SimplexChild)) and other.fromdims == other.todims and not self.trans.fromdims: - swapped = self.trans.swapup(SimplexChild(0, 0)) + swapped = self.trans.swapup(Identity(0)) trans2 = other else: swapped = None @@ -545,7 +549,7 @@ def swapup(self, other): swapped = self.trans.swapup(other.trans2) trans1 = other.trans1 elif isinstance(other, (TensorChild, SimplexChild)) and other.fromdims == other.todims and not self.trans.fromdims: - swapped = self.trans.swapup(SimplexChild(0, 0)) + swapped = self.trans.swapup(Identity(0)) trans1 = other else: swapped = None @@ -638,7 +642,7 @@ def swapdown(self, other): def separate(self, septodims): if sum(septodims) != self.todims: raise ValueError("'septodims' does not add up to 'todims'") - return tuple(Manifold(todims, self.trans) for todims in septodims) + return tuple(Manifold(todims, self.trans) if todims else Identity(0) for todims in septodims) def separate_tensor(trans1, trans2, septodims): i = 0 @@ -663,8 +667,7 @@ def append_joined_item(trans, item, *, kind): assert isinstance(trans, tuple) and all(isinstance(t, tuple) for t in trans) assert isinstance(item, TransformItem) sepitem = item.separate(tuple(t[-1].fromdims for t in trans)) - #return tuple(t if type(i) is Identity else t+(i,) for t, i in zip(trans, sepitem)) - return tuple(t if type(i) is Identity and kind == 'edge' else t+(SimplexChild(0,0),) if i == Identity(0) and kind == 'child' else t+(i,) for t, i in zip(trans, sepitem)) + return tuple(t if type(i) is Identity and kind == 'edge' else t+(i,) for t, i in zip(trans, sepitem)) def append_edge(trans, edge): return append_joined_item(trans, edge, kind='edge') @@ -689,7 +692,7 @@ def index_child_transforms(children, chains, septodims): chains = tuple(map(uppermost, chains)) for ichild, child in enumerate(children): child = child.separate(septodims) - if all(chain == (SimplexChild(0,0) if ctrans == Identity(0) else ctrans,) for chain, ctrans in zip(chains, child)): + if all(chain == (ctrans,) for chain, ctrans in zip(chains, child)): return ichild raise ValueError @@ -707,7 +710,7 @@ def index_child_transforms_with_tail(children, chains, septodims): chains = tuple(map(uppermost, chains)) for ichild, child in enumerate(children): child = child.separate(septodims) - if all(chain and chain[0] == (SimplexChild(0,0) if ctrans == Identity(0) else ctrans) for chain, ctrans in zip(chains, child)): + if all(chain and chain[0] == ctrans for chain, ctrans in zip(chains, child)): return ichild, tuple(chain[1:] for chain in chains) raise ValueError diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 868b2373b..0c5db853a 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -831,7 +831,7 @@ class TrimmedEdgesTransforms(Transforms): __cache__ = '_offsets' @types.apply_annotations - def __init__(self, parent:stricttransforms, edges:types.tuple[types.tuple[transform.stricttransformitem]]): + def __init__(self, parent:stricttransforms, edges:types.tuple[types.tuple[types.tuple[transform.stricttransformitem]]]): assert len(edges) == len(parent) self._parent = parent self._edges = edges @@ -846,9 +846,8 @@ def __len__(self): def __iter__(self): for pchains, edges in zip(self._parent, self._edges): - todims = tuple(pchain[-1].fromdims for pchain in pchains) for edge in edges: - yield tuple(pchain if type(etrans) == transform.Identity else pchain+(etrans,) for pchain, etrans in zip(pchains, edge.separate(todims))) + yield tuple(pchain if type(etrans) == transform.Identity else pchain+(etrans,) for pchain, etrans in zip(pchains, edge)) def __getitem__(self, index): if not numeric.isint(index): @@ -858,14 +857,17 @@ def __getitem__(self, index): assert 0 <= iparent < len(self._offsets)-1 iedge = index - self._offsets[iparent] pchains = self._parent[iparent] - todims = tuple(pchain[-1].fromdims for pchain in pchains) - return tuple(pchain if type(etrans) == transform.Identity else pchain+(etrans,) for pchain, etrans in zip(pchains, self._edges[iparent][iedge].separate(todims))) + return tuple(pchain if type(etrans) == transform.Identity else pchain+(etrans,) for pchain, etrans in zip(pchains, self._edges[iparent][iedge])) def index_with_tail(self, chains): iparent, parenttails = self._parent.index_with_tail(chains) - todims = tuple(tail[0].todims if tail else chain[-1].fromdims for tail, chain in zip(parenttails, chains)) - iedge, tails = transform.index_edge_transforms_with_tail(self._edges[iparent], parenttails, todims) - return self._offsets[iparent]+iedge, tails + parenttails = tuple(map(transform.canonical, parenttails)) + for iedge, edge in enumerate(self._edges[iparent]): + if all(tail and tail[0] == etrans for tail, etrans in zip(parenttails, edge) if type(etrans) != transform.Identity): + break + else: + raise ValueError + return self._offsets[iparent]+iedge, tuple(tail if type(etrans) == transform.Identity else tail[1:] for tail, etrans in zip(parenttails, edge)) class ProductTransforms(Transforms): '''The product of two :class:`Transforms` objects. diff --git a/tests/test_topology.py b/tests/test_topology.py index c79314f02..6766de40a 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -392,17 +392,17 @@ def setUp(self): self.domain0, self.geom = mesh.rectilinear([2]*self.ndims) self.domain1 = self.domain0.trim((1.1 - self.geom).sum(), maxrefine=2) self.domain2 = self.domain1.refined_by(filter(self.domain1.transforms.contains, self.domain0[:1].transforms)) - self.domain3 = self.domain2.refined_by(filter(self.domain2.transforms.contains, self.domain0.refined[:1].transforms)) + #self.domain3 = self.domain2.refined_by(filter(self.domain2.transforms.contains, self.domain0.refined[:1].transforms)) def test_boundaries(self): self.assertBoundaries(self.domain1, self.geom) self.assertBoundaries(self.domain2, self.geom) - self.assertBoundaries(self.domain3, self.geom) + #self.assertBoundaries(self.domain3, self.geom) def test_interfaces(self): self.assertInterfaces(self.domain1, self.geom, periodic=False) self.assertInterfaces(self.domain2, self.geom, periodic=False) - self.assertInterfaces(self.domain3, self.geom, periodic=False) + #self.assertInterfaces(self.domain3, self.geom, periodic=False) trimmedhierarchical('1d', ndims=1) trimmedhierarchical('2d', ndims=2) From adcf692971b5f0b4062cb64c0590b3ab266fdba6 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Fri, 6 Mar 2020 17:04:56 +0100 Subject: [PATCH 051/119] update --- docs/tutorial.rst | 4 ++-- nutils/topology.py | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 409ff8df4..5c579d863 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -142,7 +142,7 @@ is generated by .. console:: >>> nutils.mesh.rectilinear([numpy.linspace(0, 1, 5), numpy.linspace(0, 1, 9)]) - (StructuredTopology<4x8>, Array<2>) + (StructuredTopology<4>*StructuredTopology<8>, Array<2>) Any topology defines a boundary via the :attr:`Topology.boundary ` attribute. Optionally, a topology can @@ -161,7 +161,7 @@ two-dimensional mesh periodic, add ``periodic=[1]``: .. console:: >>> nutils.mesh.rectilinear([numpy.linspace(0, 1, 5), numpy.linspace(0, 1, 9)], periodic=[1]) - (StructuredTopology<4x8p>, Array<2>) + (StructuredTopology<4>*StructuredTopology<8p>, Array<2>) Note that in this case the boundary topology, though still available, is empty. diff --git a/nutils/topology.py b/nutils/topology.py index 264467fa3..67a8e00e2 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -1946,10 +1946,6 @@ def __repr__(self): def shape(self): return self._left.shape + self._right.shape - @property - def shape(self): - return self._left.shape + self._right.shape - @property def connectivity(self): s = len(self._right) From 67675d2b646b73924d50508d3c7c55a0ec74c08a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 9 Mar 2020 09:30:09 +0100 Subject: [PATCH 052/119] f compute jacobian using rootgradient after introducing subsamplemeta --- nutils/sample.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/nutils/sample.py b/nutils/sample.py index 2bf0a6ccc..2cd1d0387 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -142,11 +142,13 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. if arguments is None: arguments = {} - J = function.RootBasis(self.roots, self.ndims, function.SelectChain(self.roots))[:,:self.ndims] - if J.shape[0] == J.shape[1]: - detJ = abs(function.determinant(J)) - else: - detJ = abs(function.determinant((J[:,:,None] * J[:,None,:]).sum(0)))**.5 + detJ = 1 + for subsample in self.subsamplemetas: + J = function.RootBasis(subsample.roots, subsample.ndimsmanifold, function.SelectChain(subsample.roots))[:,:subsample.ndimsmanifold] + if J.shape[0] == J.shape[1]: + detJ *= abs(function.determinant(J)) + else: + detJ *= abs(function.determinant((J[:,:,None] * J[:,None,:]).sum(0)))**.5 funcs = [func * detJ for func in funcs] # Functions may consist of several blocks, such as originating from From a5f803102bf084859246cee7a302d67bc55e6f2a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 9 Mar 2020 14:56:57 +0100 Subject: [PATCH 053/119] update --- nutils/function.py | 281 ++++++++++++++++++++++----------------------- nutils/sample.py | 4 +- nutils/topology.py | 2 +- 3 files changed, 139 insertions(+), 148 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 4d2e2212f..68626e53b 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -776,18 +776,22 @@ class RootBasis(Array): Parameters ---------- + isubsample : :class:`int` + The index of the subsample to compute the basis for. roots : :class:`tuple` of :class:`Root` objects - The roots to compute the tangent and normal vectors for. + The roots to compute the basis for. ndimstangent : int The dimension of the tangent space. trans : :class:`TransformChain` ''' - __slots__ = '_roots', '_ndimstangent', '_opposite', '_trans' + __slots__ = '_isubsample', '_roots', '_ndimstangent', '_opposite', '_trans' + __cache__ = 'prepare_eval' @types.apply_annotations - def __init__(self, roots:types.tuple[strictroot], ndimstangent:types.strictint, trans:types.strict[TransformChain], _opposite:bool=None): + def __init__(self, isubsample:types.strictint, roots:types.tuple[strictroot], ndimstangent:types.strictint, trans:types.strict[TransformChain], _opposite:bool=None): # NOTE: `trans` is only required because of the `SelectChain` test in `Opposite.simplified`. + self._isubsample = isubsample self._roots = roots self._ndimstangent = ndimstangent self._opposite = _opposite @@ -796,13 +800,20 @@ def __init__(self, roots:types.tuple[strictroot], ndimstangent:types.strictint, ndims = sum(root.ndims for root in roots) super().__init__(args=[SUBSAMPLES, trans], shape=[ndims, ndims], dtype=float) - def _evalf_subsample(self, roots, chains, points): - ndims = builtins.sum(root.ndims for root in roots) - linear = numpy.zeros((points.npoints, ndims, ndims), dtype=float) + def evalf(self, subsamples, _trans): + subsample = subsamples[self._isubsample] + assert subsample.roots == self._roots + assert subsample.ndimsmanifold == self._ndimstangent + assert subsample.ndims == self.shape[0] + ndims = subsample.ndims + points = subsample.points + npointsbefore = functools.reduce(operator.mul, (s.points.npoints for s in subsamples[:self._isubsample]), 1) + npointsafter = functools.reduce(operator.mul, (s.points.npoints for s in subsamples[self._isubsample+1:]), 1) + linear = numpy.zeros((npointsbefore, points.npoints, npointsafter, ndims, ndims), dtype=float) chainslinear = numpy.zeros((ndims, points.ndims), dtype=float) to0 = from0 = 0 n0 = points.ndims - for root, chain in zip(roots, chains): + for root, chain in zip(subsample.roots, subsample.transforms[1 if self._opposite else 0]): to1 = to0 + root.ndims fromdims = chain[-1].fromdims if chain else root.ndims chainlinear = transform.linearfrom(chain, root.ndims) @@ -812,54 +823,25 @@ def _evalf_subsample(self, roots, chains, points): from0 = from1 if fromdims < root.ndims: n1 = n0 + root.ndims - fromdims - linear[:,to0:to1,n0:n1] = chainlinear[:,fromdims:] + linear[:,:,:,to0:to1,n0:n1] = chainlinear[_,_,_,:,fromdims:] n0 = n1 to0 = to1 assert to0 == ndims assert from0 == points.ndims assert n0 == ndims - numpy.einsum('ij,njk->nik', chainslinear, points.basis, out=linear[:,:,:points.ndims]) - return linear - - def evalf(self, subsamples, _trans): - ndims = self.shape[0] - linear = numpy.zeros((*(subsample.points.npoints for subsample in subsamples), ndims, ndims), dtype=float) - to0 = from0 = 0 - n0 = self._ndimstangent - slices = {} - for ipoints, subsample in enumerate(subsamples): - if frozenset(subsample.roots).isdisjoint(self.roots): - continue - subsamplelinear = self._evalf_subsample(subsample.roots, subsample.transforms[1 if self._opposite else 0], subsample.points) - subsamplelinear = subsamplelinear[tuple(slice(None) if j == ipoints else _ for j in range(len(subsamples)))] - to1 = to0 + subsamplelinear.shape[-1] - from1 = from0 + subsample.ndimsmanifold - n1 = n0 + subsamplelinear.shape[-1] - subsample.ndimsmanifold - linear[...,to0:to1,from0:from1] = subsamplelinear[...,:subsample.ndimsmanifold] - linear[...,to0:to1,n0:n1] = subsamplelinear[...,subsample.ndimsmanifold:] - r0 = to0 - for root in subsample.roots: - r1 = r0 + root.ndims - slices[root] = slice(r0, r1) - r0 = r1 - to0, from0, n0 = to1, from1, n1 - assert to0 == ndims - assert from0 == self._ndimstangent - assert n0 == ndims - - # reorder to `self._roots` - linear = linear.reshape((-1, ndims, ndims)) - return numpy.concatenate([linear[:,slices[root]] for root in self._roots], axis=1) + numpy.einsum('ij,bnajk->bnaik', chainslinear, points.basis[_,:,_], out=linear[:,:,:,:,:points.ndims]) + return linear.reshape((npointsbefore*points.npoints*npointsafter, ndims, ndims)) @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): if self._opposite is not None: raise ValueError('prepare already called') - return RootBasis(self._roots, self._ndimstangent, self._trans.prepare_eval(opposite=opposite, **kwargs), _opposite=opposite) + return RootBasis(self._isubsample, self._roots, self._ndimstangent, self._trans.prepare_eval(opposite=opposite, **kwargs), _opposite=opposite) class GramSchmidt(Array): __slots__ = '_arg' + __cache__ = 'simplified' @types.apply_annotations def __init__(self, arg:asarray): @@ -876,6 +858,16 @@ def _derivative(self, var, seen): raise NotImplementedError return zeros(self.shape + var.shape) + @property + def simplified(self): + arg = self._arg.simplified + if arg == eye(self.shape[-1]): + return arg + elif self.shape[-2:] == (1,1): + return sign(arg).simplified + else: + return GramSchmidt(arg) + class Normal(Array): 'normal' @@ -3064,18 +3056,17 @@ def evalf(self): raise Exception('RootCoords should not be evaluated') class DelayedJacobian(Array): - ''' - Placeholder for :func:`jacobian` until the dimension of the - :class:`nutils.topology.Topology` where this functions is being evaluated is - known. The replacing is carried out by :meth:`Evaluable.prepare_eval`. - ''' + '''Jacobian of a geometry.''' - __slots__ = '_geom', '_derivativestack' + __slots__ = '_geom', '_ndimsmanifold', '_derivativestack' __cache__ = 'prepare_eval' @types.apply_annotations - def __init__(self, geom:asarray, *derivativestack): + def __init__(self, geom:asarray, ndimsmanifold, *derivativestack:types.tuple[types.strict[DerivativeTargetBase]]): + if geom.ndim != 1: + raise ValueError('the geometry should have dimension 1 but got {}'.format(geom.ndim)) self._geom = geom + self._ndimsmanifold = ndimsmanifold self._derivativestack = derivativestack super().__init__(args=[geom], shape=[n for var in derivativestack for n in var.shape], dtype=float) @@ -3085,19 +3076,78 @@ def evalf(self): def _derivative(self, var, seen): if iszero(derivative(self._geom, var, seen)): return zeros(self.shape + var.shape) - return DelayedJacobian(self._geom, *self._derivativestack, var) + return DelayedJacobian(self._geom, self._ndimsmanifold, *self._derivativestack, var) @util.positional_only def prepare_eval(self, *, subsamples, kwargs=...): ndimsmanifold = 0 - for subsample in subsamples: + J = [] + for isubsample, subsample in enumerate(subsamples): if self.roots.isdisjoint(subsample.roots): continue if not frozenset(subsample.roots) <= self.roots: raise ValueError('Cannot compute jacobian.') ndimsmanifold += subsample.ndimsmanifold - jac = functools.reduce(derivative, self._derivativestack, asarray(jacobian(self._geom, ndimsmanifold))) - return jac.prepare_eval(subsamples=subsamples, **kwargs) + if subsample.ndims == 0: + continue + J.append(dot(rootgradient(self._geom, subsample.roots)[:,:,_], rootbasis(subsamples, isubsample, orthonormal=True)[_:,:subsample.ndimsmanifold], 1)) + if self._ndimsmanifold is not None and ndimsmanifold != self._ndimsmanifold: + raise ValueError('jacobian will be evaluated on a manifold of dimension {} but {} was requested'.format(ndimsmanifold, self._ndimsmanifold)) + J = concatenate(J, axis=1) + if J.shape[0] == J.shape[1]: + detJ = abs(determinant(J)) + else: + detJ = abs(determinant((J[:,:,_] * J[:,_,:]).sum(0)))**.5 + detJ = functools.reduce(derivative, self._derivativestack, asarray(detJ)) + return detJ.prepare_eval(subsamples=subsamples, **kwargs) + +class DelayedNormal(Array): + '''Normal of a geometry.''' + + __slots__ = '_geom', '_derivativestack' + __cache__ = 'prepare_eval' + + @types.apply_annotations + def __init__(self, geom:asarray, *derivativestack:types.tuple[types.strict[DerivativeTargetBase]]): + if geom.ndim != 1: + raise ValueError('the geometry should have dimension 1 but got {}'.format(geom.ndim)) + assert len(geom) <= builtins.sum(root.ndims for root in geom.roots) + self._geom = geom + self._derivativestack = derivativestack + super().__init__(args=[geom], shape=[len(geom)]+[n for var in derivativestack for n in var.shape], dtype=float) + + def evalf(self): + raise Exception('DelayedNormal should not be evaluated') + + def _derivative(self, var, seen): + if iszero(derivative(self._geom, var, seen)): + return zeros(self.shape + var.shape) + return DelayedNormal(self._geom, *self._derivativestack, var) + + @util.positional_only + def prepare_eval(self, *, subsamples, kwargs=...): + tangents = [] + normals = [] + ndimsnormal = 0 + for isubsample, subsample in enumerate(subsamples): + if self.roots.isdisjoint(subsample.roots): + continue + if not frozenset(subsample.roots) <= self.roots: + raise ValueError('Cannot compute normal.') + ndimsnormal += subsample.ndimsnormal + basis = rootbasis(subsamples, isubsample) + grad = dot(rootgradient(self._geom, subsample.roots)[:,:,_], basis[_,:,:], 1) + if subsample.ndimsmanifold: + tangents.append(grad[:,:subsample.ndimsmanifold]) + if subsample.ndimsnormal: + normals.append(grad[:,subsample.ndimsmanifold:]) + if ndimsnormal == 0: + raise ValueError('cannot compute normal: the normal space has dimension zero') + elif ndimsnormal > 1: + raise ValueError('cannot compute normal: the normal space has dimension larger then one') + n = Normal(concatenate(tangents+normals, axis=1)[:,:len(self._geom)]) + n = functools.reduce(derivative, self._derivativestack, asarray(n)) + return n.prepare_eval(subsamples=subsamples, **kwargs) class Ravel(Array): @@ -4525,31 +4575,6 @@ def get(arg, iax, item): item = numeric.normdim(sh, item.eval()[0]) return Get(arg, iax, item) -def jacobian(geom, ndims): - ''' - Return :math:`\\sqrt{|J^T J|}` with :math:`J` the gradient of ``geom`` to the - root coordinate system with ``ndims`` dimensions. - ''' - - # Compute the jacobian `abs(det(J.T @ V.T @ V @ J))**0.5` with `J` the - # gradient of `geom` to the root coordinates and `V` the `ndims` vectors - # spanning the tangent space. - assert geom.ndim == 1 - if ndims == 0: - return 1. - # Order the roots deterministically. In the future we should use the order - # of `Sample.roots` (during `prepare_eval` or a successor). - roots = tuple(sorted(geom.roots, key=lambda root: (root.name, root.ndims))) - J = rootgradient(geom, roots) - if J.shape == (ndims, ndims): - # Since `J` and `V` are square, the determinant `det(J @ V)` is equal to - # `det(J) * det(V)`. The root vectors `V` are orthonormal, hence the - # determinant simplifies to `det(J)`. - return abs(determinant(J)) - else: - J = dot(J[:,:,_], roottangent(roots, ndims)[_], 1) - return abs(determinant((J[:,:,_] * J[:,_,:]).sum(0)))**.5 - def matmat(arg0, *args): 'helper function, contracts last axis of arg0 with first axis of arg1, etc' retval = asarray(arg0) @@ -4600,42 +4625,24 @@ def derivative(func, var, seen=None): def rootgradient(arg, roots): return concatenate([derivative(arg, RootCoords(root)) for root in roots], axis=-1) -def roottangent(roots, ndimstangent): - '''Returns the orthonormal vectors spanning the tangent space. - - Parameters - ---------- - roots : :class:`tuple` or :class:`Root` objects - The roots to compute the tangent vectors for. - ndimstangent : int - The dimension of the tangent space. - - Returns - ------- - :class:`Array` - The tangent vectors with shape `(sum(root.ndims for root in roots), ndims)`. - ''' - - return GramSchmidt(RootBasis(roots, ndimstangent, SelectChain(roots)))[:,:ndimstangent] - -def rootnormal(roots, ndimsnormal): - '''Returns the orthonormal vectors spanning the normal space. - - Parameters - ---------- - roots : :class:`tuple` or :class:`Root` objects - The roots to compute the normal vectors for. - ndimstangent : int - The dimension of the normal space. - - Returns - ------- - :class:`Array` - The tangent vectors with shape `(sum(root.ndims for root in roots), ndims)`. - ''' - - ndimstangent = builtins.sum(root.ndims for root in roots) - ndimsnormal - return GramSchmidt(RootBasis(roots, ndimstangent, SelectChain(roots)))[:,ndimstangent:] +def rootbasis(subsamples, index_or_roots, *, orthonormal=False): + subsamples = tuple(subsamples) + if isinstance(index_or_roots, int): + isubsample = index_or_roots + subsample = subsamples[isubsample] + elif isinstance(index_or_roots, tuple): + roots = index_or_roots + for isubsample, subsample in enumerate(subsamples): + if subsample.roots == roots: + break + else: + raise ValueError('no subsample with roots {}, candidates: {}'.format(roots, ', '.join(str(subsample.roots) for subsample in subsamples))) + else: + raise ValueError('expected an `int` or `tuple` of `Root` objects but got {!r}'.format(index_or_roots)) + basis = RootBasis(isubsample, subsample.roots, subsample.ndimsmanifold, SelectChain(subsample.roots)) + if orthonormal: + basis = GramSchmidt(basis) + return basis def dotnorm(arg, coords): 'normal component' @@ -4653,6 +4660,18 @@ def diagonalize(arg, axis=-1, newaxis=-1): assert axis < newaxis return Diagonalize(arg, axis, newaxis) +def blockdiagonalize(blocks): + blocks = tuple(map(asarray, blocks)) + if not blocks: + raise ValueError('`blockdiagonalize` requires at least one block') + shape = blocks[0].shape[:-2] + ndim = blocks[0].ndim + if not all(block.ndim == ndim and block.shape[:-2] == shape for block in blocks): + raise ValueError('blocks have inhomogeneous dimensions or leading shapes') + dtype = _jointdtype(*[block.dtype for block in blocks]) + size1, size2 = zip(*(block.shape[-2:] for block in blocks)) + return concatenate([concatenate([block if i == j else zeros(shape+(size1[i],size2[j]), dtype) for j in range(len(blocks))], -1) for i, block in enumerate(blocks)], -2) + def concatenate(args, axis=0): args = _matchndim(*args) axis = numeric.normdim(args[0].ndim, axis) @@ -4732,11 +4751,9 @@ def J(geometry, ndims=None): Return :math:`\\sqrt{|J^T J|}` with :math:`J` the gradient of ``geometry`` to the root coordinate system with ``ndims`` dimensions. ''' - if ndims is None: - return DelayedJacobian(geometry) - elif ndims < 0: + if ndims is not None and ndims < 0: ndims += len(geometry) - return jacobian(geometry, ndims) + return DelayedJacobian(geometry, ndims) def unravel(func, axis, shape): func = asarray(func) @@ -5095,37 +5112,11 @@ def __rmatmul__(self, expr): def normal(arg, exterior=False): assert arg.ndim == 1 + if not exterior: + return DelayedNormal(arg) # Order the roots deterministically. In the future we should use the order # of `Sample.roots` (during `prepare_eval` or a successor). roots = tuple(sorted(arg.roots, key=lambda root: (root.name, root.ndims))) - if not exterior: - assert len(arg) <= builtins.sum(root.ndims for root in roots) - ndimstangent = len(arg) - 1 - ndimsnormal = builtins.sum(root.ndims for root in roots) - ndimstangent - V = concatenate([roottangent(roots, ndimstangent), rootnormal(roots, ndimsnormal)[:,:1]], axis=1) - # TODO: Truncating the second axis of the rootnormal to one is a bit - # fishy, and should probably be disallowed when tensorial is fully - # implemented. Consider the following case: - # - # topo0, geom0 = mesh.rectilinear([1]*3) - # topo, geom = topo.boundary['top'], function.stack([geom[0], geom0[2]]) - # - # Now we compute the normal of `geom` on the boundaries of `topo`: - # - # topo.boundary['left'].sample('gauss', 1).eval(geom.normal()) - # topo.boundary['front'].sample('gauss', 1).eval(geom.normal()) - # - # Since the root of `topo` is 3D and `geom` only 2D, the `rootgradient` of - # `geom` has shape (2,3). The first element of the second axis - # spans the 1D tangent space of the manifold, the remaining elements span - # the normal space. The problem is: can we rely on the order of the - # normals. A trim boundary is now always the first element of the normals - # section. The remaining normals are ordered by the `Topology.boundary` - # operations: the first boundary, in the example `boundary['top']`, will - # have the normal in the last position. - lgrad = dot(rootgradient(arg, roots)[:,:,_], V[_,:,:], 1) - return Normal(lgrad[:,:len(arg)]) - raise NotImplementedError lgrad = rootgradient(arg, roots) if len(arg) == 2: return asarray([lgrad[1,0], -lgrad[0,0]]).normalized() diff --git a/nutils/sample.py b/nutils/sample.py index 2cd1d0387..49a3ca845 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -143,8 +143,8 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. arguments = {} detJ = 1 - for subsample in self.subsamplemetas: - J = function.RootBasis(subsample.roots, subsample.ndimsmanifold, function.SelectChain(subsample.roots))[:,:subsample.ndimsmanifold] + for isubsample, subsample in enumerate(self.subsamplemetas): + J = function.RootBasis(isubsample, subsample.roots, subsample.ndimsmanifold, function.SelectChain(subsample.roots))[:,:subsample.ndimsmanifold] if J.shape[0] == J.shape[1]: detJ *= abs(function.determinant(J)) else: diff --git a/nutils/topology.py b/nutils/topology.py index 67a8e00e2..9edbbb9d2 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -517,7 +517,7 @@ def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, ma vref = element.getsimplex(0) ielems = parallel.shempty(len(coords), dtype=int) xis = parallel.shempty((len(coords),len(geom)), dtype=float) - J = function.dot(function.rootgradient(geom, self.roots)[:,:,_], function.RootBasis(self.roots, self.ndims, function.SelectChain(self.roots))[_,:,:self.ndims], 1) + J = function.dot(function.rootgradient(geom, self.roots)[:,:,_], function.RootBasis(0, self.roots, self.ndims, function.SelectChain(self.roots))[_,:,:self.ndims], 1) geom_J = function.Tuple((geom, J)).prepare_eval().simplified with parallel.ctxrange('locating', len(coords)) as ipoints: for ipoint in ipoints: From 9b04cb726d9bc2b3098b6eddda3dc93859e9f1c8 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 10 Mar 2020 09:11:25 +0100 Subject: [PATCH 054/119] updates --- nutils/transformseq.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 0c5db853a..19e170ad0 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -767,7 +767,7 @@ def index_with_tail(self, trans): iparent, parenttail = self._parent.index_with_tail(trans) if not any(parenttail): raise ValueError - todims = [a[0].todims if a else b[-1].fromdims for a, b in zip(parenttail, trans)] + todims = tuple(a[0].todims if a else b[-1].fromdims for a, b in zip(parenttail, trans)) iderived, tail = (transform.index_edge_transforms_with_tail if self._updim else transform.index_child_transforms_with_tail)(self._derived_transforms(self._parent_references[iparent]), parenttail, todims) return self._offsets[iparent]+iderived, tail @@ -821,7 +821,7 @@ def index_with_tail(self, trans): iparent, parenttail = self._parent.index_with_tail(trans) if not any(parenttail): raise ValueError - todims = [a[0].todims if a else b[-1].fromdims for a, b in zip(parenttail, trans)] + todims = tuple(a[0].todims if a else b[-1].fromdims for a, b in zip(parenttail, trans)) iderived, tail = (transform.index_edge_transforms_with_tail if self._updim else transform.index_child_transforms_with_tail)(self._derived_transforms, parenttail, todims) return iparent*len(self._derived_transforms) + iderived, tail From d1bc94e4251fbd6832e399fbfa05dbfbcb0a6bff Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 10 Mar 2020 09:23:58 +0100 Subject: [PATCH 055/119] add special case Sample implementations --- nutils/sample.py | 132 +++++++++++++++++++++++++++++++++++---------- nutils/topology.py | 26 +++++---- 2 files changed, 118 insertions(+), 40 deletions(-) diff --git a/nutils/sample.py b/nutils/sample.py index 49a3ca845..a28b2fb5e 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -43,7 +43,7 @@ class which represents postponed integration. Integrals are internally efficiently combine common substructures. ''' -from . import types, points, util, function, parallel, numeric, matrix, transformseq, sparse +from . import types, points, util, function, parallel, numeric, matrix, transformseq, sparse, warnings import numpy, numbers, collections.abc, os, treelog as log, operator graphviz = os.environ.get('NUTILS_GRAPHVIZ') @@ -77,30 +77,22 @@ class Sample(types.Singleton): ndims : :class:`int` The dimension of the :class:`~nutils.topology.Topology` from which this sample is created. + npoints : :class:`int` + The number of points in this sample. transforms : :class:`tuple` or transformation chains List of transformation chains leading to local coordinate systems that contain points. - points : :class:`tuple` of point sets - List of point sets matching ``transforms``. - index : :class:`tuple` of integer arrays - List of indices matching ``transforms``, defining the order on which - points show up in the evaluation. ''' - __cache__ = 'allcoords' + __cache__ = 'allcoords', 'index' @types.apply_annotations - def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], points:types.tuple[points.strictpoints], index:types.tuple[types.frozenarray[types.strictint]]): - assert len(points) == len(index) - assert len(transforms) >= 1 - assert all(len(t) == len(points) for t in transforms) - self.nelems = len(transforms[0]) - self.transforms = transforms - self.points = points - self.index = index - self.npoints = sum(p.npoints for p in points) - self.ndims = ndims + def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint, npoints:types.strictint, transforms:types.tuple[transformseq.stricttransforms]): self.roots = roots + self.ndims = ndims + self.npoints = npoints + self.transforms = transforms + self.nelems = len(transforms[0]) def __repr__(self): return '{}<{}D, {} elems, {} points>'.format(type(self).__qualname__, self.ndims, self.nelems, self.npoints) @@ -108,6 +100,16 @@ def __repr__(self): def _prepare_funcs(self, funcs): return [function.asarray(func).prepare_eval(subsamples=self.subsamplemetas) for func in funcs] + @property + def index(self): + warnings.deprecation('`Sample.index` is deprecated; replace `Sample.index[ielem]` with `Sample.getindex(ielem)`') + return tuple(self.getindex(ielem) for ielem in range(self.nelems)) + + @property + def points(self): + warnings.deprecation('`Sample.points` is deprecated; replace `Sample.points[ielem]` with `Sample.getpoints(ielem)`') + return tuple(self.getpoints(ielem) for ielem in range(self.nelems)) + @util.positional_only @util.single_or_multiple @types.apply_annotations @@ -241,15 +243,15 @@ def eval(self, funcs, arguments:argdict=...): with parallel.ctxrange('evaluating', self.nelems) as ielems: for ielem in ielems: for ifunc, inds, data in idata.eval(*self.getsubsamples(ielem), **arguments): - numpy.add.at(retvals[ifunc], numpy.ix_(self.index[ielem], *[ind for (ind,) in inds]), data) + numpy.add.at(retvals[ifunc], numpy.ix_(self.getindex(ielem), *[ind for (ind,) in inds]), data) return retvals @property def allcoords(self): coords = numpy.empty([self.npoints, self.ndims]) - for points, index in zip(self.points, self.index): - coords[index] = points.coords + for ielem in range(self.nelems): + coords[self.getindex(ielem)] = self.getpoints(ielem).coords return types.frozenarray(coords, copy=False) def basis(self): @@ -292,7 +294,7 @@ def tri(self): row defines a simplex by mapping vertices into the list of points. ''' - return numpy.concatenate([index.take(points.tri) for points, index in zip(self.points, self.index)]) + return numpy.concatenate([self.getindex(ielem).take(self.getpoints(ielem).tri) for ielem in range(self.nelems)]) @property def hull(self): @@ -304,7 +306,7 @@ def hull(self): triangulations originating from separate elements are disconnected. ''' - return numpy.concatenate([index.take(points.hull) for points, index in zip(self.points, self.index)]) + return numpy.concatenate([self.getindex(ielem).take(self.getpoints(ielem).hull) for ielem in range(self.nelems)]) def subset(self, mask): '''Reduce the number of points. @@ -325,14 +327,14 @@ def subset(self, mask): subset : :class:`Sample` ''' - selection = types.frozenarray([ielem for ielem in range(self.nelems) if mask[self.index[ielem]].any()]) + selection = types.frozenarray([ielem for ielem in range(self.nelems) if mask[self.getindex(ielem)].any()]) transforms = tuple(transform[selection] for transform in self.transforms) - points = [self.points[ielem] for ielem in selection] + points = [self.getpoints(ielem) for ielem in selection] offset = numpy.cumsum([0] + [p.npoints for p in points]) - return Sample(self.roots, self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) + return PlainSample(self.roots, self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) def getsubsamples(self, ielem): - return function.Subsample(roots=self.roots, transforms=tuple(t[ielem] for t in self.transforms), points=self.points[ielem], ielem=ielem), + return function.Subsample(roots=self.roots, transforms=tuple(t[ielem] for t in self.transforms), points=self.getpoints(ielem), ielem=ielem), @property def subsamplemetas(self): @@ -340,6 +342,71 @@ def subsamplemetas(self): strictsample = types.strict[Sample] +class PlainSample(Sample): + '''A general purpose implementation of :class:`Sample`. + + Parameters + ---------- + roots : :class:`tuple` of :class:`~nutils.function.Root` + The roots of this sample. + ndims : :class:`int` + The dimension of the :class:`~nutils.topology.Topology` from which this + sample is created. + transforms : :class:`tuple` or transformation chains + List of transformation chains leading to local coordinate systems that + contain points. + points : :class:`tuple` of point sets + List of point sets matching ``transforms``. + index : :class:`tuple` of integer arrays + List of indices matching ``transforms``, defining the order on which + points show up in the evaluation. + ''' + + @types.apply_annotations + def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], points:types.tuple[points.strictpoints], index:types.tuple[types.frozenarray[types.strictint]]): + assert len(points) == len(index) + assert len(transforms) >= 1 + assert all(len(t) == len(points) for t in transforms) + self._points = points + self._index = index + npoints = sum(p.npoints for p in points) + super().__init__(roots, ndims, npoints, transforms) + + def getpoints(self, ielem): + return self._points[ielem] + + def getindex(self, ielem): + return self._index[ielem] + +class UniformSample(Sample): + '''A sample with uniform points. + + Parameters + ---------- + roots : :class:`tuple` of :class:`~nutils.function.Root` + The roots of this sample. + ndims : :class:`int` + The dimension of the :class:`~nutils.topology.Topology` from which this + sample is created. + transforms : :class:`tuple` or transformation chains + List of transformation chains leading to local coordinate systems that + contain points. + points : :class:`~nutils.points.Points` + Point set. + ''' + + @types.apply_annotations + def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], points:points.strictpoints): + assert len(transforms) >= 1 + self._points = points + super().__init__(roots, ndims, points.npoints*len(transforms[0]), transforms) + + def getpoints(self, ielem): + return self._points + + def getindex(self, ielem): + return numpy.arange(ielem*self._points.npoints, (ielem+1)*self._points.npoints) + class ProductSample(Sample): @types.apply_annotations @@ -348,9 +415,16 @@ def __init__(self, sample1:strictsample, sample2:strictsample): self._sample2 = sample2 super().__init__(sample1.roots+sample2.roots, sample1.ndims+sample2.ndims, - tuple(map(operator.mul, sample1.transforms, sample2.transforms)), - tuple(points.TensorPoints(points1, points2) for points1 in sample1.points for points2 in sample2.points), # TODO: make lazy - tuple((index1[:,numpy.newaxis]*sample2.npoints + index2[numpy.newaxis,:]).ravel() for index1 in sample1.index for index2 in sample2.index)) + sample1.npoints*sample2.npoints, + tuple(map(operator.mul, sample1.transforms, sample2.transforms))) + + def getpoints(self, ielem): + ielem1, ielem2 = divmod(ielem, self._sample2.nelems) + return points.TensorPoints(self._sample1.getpoints(ielem1), self._sample2.getpoints(ielem2)) + + def getindex(self, ielem): + ielem1, ielem2 = divmod(ielem, self._sample2.nelems) + return (self._sample1.getindex(ielem1)[:,numpy.newaxis]*self._sample2.npoints + self._sample2.getindex(ielem2)[numpy.newaxis,:]).ravel() def getsubsamples(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) diff --git a/nutils/topology.py b/nutils/topology.py index 9edbbb9d2..b3d6bdd13 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -187,13 +187,17 @@ def basis(self, name, *args, **kwargs): def sample(self, ischeme, degree): 'Create sample.' - points = [ischeme(reference, degree) for reference in self.references] if callable(ischeme) \ - else self.references.getpoints(ischeme, degree) - offset = numpy.cumsum([0] + [p.npoints for p in points]) transforms = self.transforms, if len(self.transforms) == 0 or self.opposites != self.transforms: transforms += self.opposites, - return sample.Sample(self.roots, self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) + if self.references.isuniform: + points = ischeme(self.references[0], degree) if callable(ischeme) else self.references[0].getpoints(ischeme, degree) + return sample.UniformSample(self.roots, self.ndims, transforms, points) + else: + points = [ischeme(reference, degree) for reference in self.references] if callable(ischeme) \ + else self.references.getpoints(ischeme, degree) + offset = numpy.cumsum([0] + [p.npoints for p in points]) + return sample.PlainSample(self.roots, self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) @util.single_or_multiple def integrate_elementwise(self, funcs, *, asfunction=False, **kwargs): @@ -559,7 +563,7 @@ def _sample(self, ielems, coords): transforms = self.transforms[uielems], if len(self.transforms) == 0 or self.opposites != self.transforms: transforms += self.opposites[uielems], - return sample.Sample(self.roots, self.ndims, transforms, points_, index) + return sample.PlainSample(self.roots, self.ndims, transforms, points_, index) def revolved(self, geom): assert geom.ndim == 1 @@ -1598,13 +1602,13 @@ def basis(self, name, *args, **kwargs): def locate(self, geom, coords, *, eps=0, **kwargs): sample = self.basetopo.locate(geom, coords, eps=eps, **kwargs) - for transforms, points, index in zip(sample.transforms[0], sample.points, sample.index): - ielem = self.basetopo.transforms.index(transforms) - ref = self.refs[ielem] - if ref != self.basetopo.references[ielem]: - for i, coord in enumerate(points.coords): + for ielem in range(sample.nelems): + baseielem = self.basetopo.transforms.index(sample.transforms[0][ielem]) + ref = self.refs[baseielem] + if ref != self.basetopo.references[baseielem]: + for i, coord in enumerate(sample.getpoints(ielem).coords): if not ref.inside(coord, eps): - raise LocateError('failed to locate point: {}'.format(coords[index[i]])) + raise LocateError('failed to locate point: {}'.format(coords[sample.getindex(ielem)[i]])) return sample class RefinedTopology(Topology): From 16838694447575ae3257e1ce9a9530e93bc091d2 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 10 Mar 2020 16:01:13 +0100 Subject: [PATCH 056/119] add topology.StructuredLine --- docs/tutorial.rst | 14 +- nutils/function.py | 48 +++++++ nutils/mesh.py | 9 +- nutils/topology.py | 331 +++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 384 insertions(+), 18 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 5c579d863..9072e3d53 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -123,7 +123,7 @@ size between 0 and 1 is generated by .. console:: >>> nutils.mesh.rectilinear([[0, 0.25, 0.5, 0.75, 1.0]]) - (StructuredTopology<4>, Array<1>) + (StructuredLine<4>, Array<1>) Alternatively we could have used :func:`numpy.linspace` to generate a sequence of equidistant vertices, and unpack the resulting tuple: @@ -142,7 +142,7 @@ is generated by .. console:: >>> nutils.mesh.rectilinear([numpy.linspace(0, 1, 5), numpy.linspace(0, 1, 9)]) - (StructuredTopology<4>*StructuredTopology<8>, Array<2>) + (StructuredLine<4>*StructuredLine<8>, Array<2>) Any topology defines a boundary via the :attr:`Topology.boundary ` attribute. Optionally, a topology can @@ -152,7 +152,7 @@ dimension, making the left boundary accessible as: .. console:: >>> topo.boundary['left'] - StructuredTopology<> + PointsTopology<1> Optionally, a topology can be made periodic in one or more dimensions by passing a list of dimension indices to be periodic via the keyword argument @@ -161,7 +161,7 @@ two-dimensional mesh periodic, add ``periodic=[1]``: .. console:: >>> nutils.mesh.rectilinear([numpy.linspace(0, 1, 5), numpy.linspace(0, 1, 9)], periodic=[1]) - (StructuredTopology<4>*StructuredTopology<8p>, Array<2>) + (StructuredLine<4>*StructuredLine<8p>, Array<2>) Note that in this case the boundary topology, though still available, is empty. @@ -177,7 +177,7 @@ it helps to think of a basis as evaluating always to the full array. Several :class:`~nutils.topology.Topology` objects support creating bases via the :meth:`Topology.basis() ` method. A -:class:`~nutils.topology.StructuredTopology`, as generated by +:class:`~nutils.topology.StructuredLine`, as generated by :func:`nutils.mesh.rectilinear`, can create a spline basis with arbitrary degree and arbitrary continuity. The following generates a degree one spline basis on our previously created unit line topology ``topo``: @@ -609,7 +609,7 @@ The optimization problem can also be solved by the .. console:: >>> nutils.solver.optimize('lhs', sqr) optimize > solve > solving 5 dof system to machine precision using direct solver - optimize > solve > solver returned with residual 0e+00 + optimize > solve > solver returned with residual 0e+00±1e-15 optimize > optimum value 0.00e+00±1e-15 array([0. , 0.25, 0.5 , 0.75, 1. ])±1e-15 @@ -676,7 +676,7 @@ second argument of :class:`Topology.sample() .. console:: >>> bezier = topo.sample('bezier', 2) >>> bezier - Sample<1D, 4 elems, 8 points> + UniformSample<1D, 4 elems, 8 points> The resulting :class:`nutils.sample.Sample` object can be used to evaluate :class:`~nutils.function.Array` functions via the :meth:`Sample.eval(func) diff --git a/nutils/function.py b/nutils/function.py index 68626e53b..e977a3f4b 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4022,6 +4022,54 @@ def get_support(self, dof): raise IndexError('dof out of bounds') return self._parent.get_support(self._indices[dof]) +class StructuredLineBasis(Basis): + '''A basis for a structured line. + + Parameters + ---------- + coeffs : :class:`tuple` of arrays + Per dimension the coefficients of the basis functions per transform. + start_dofs : array of :class:`int`\\s + Per dimension the dof of the first entry in ``coeffs`` per transform. + stop_dofs : array of :class:`int`\\s + Per dimension one plus the dof of the last entry in ``coeffs`` per + transform. + ndofs : :class:`int` + The number of dofs. + transforms : :class:`nutils.transformseq.Transforms` + The transforms on which this basis is defined. + trans : :class:`TransformChain` + ''' + + __slots__ = '_coeffs', '_start_dofs', '_stop_dofs' + + @types.apply_annotations + def __init__(self, coeffs:types.tuple[types.frozenarray], start_dofs:types.frozenarray[types.strictint], stop_dofs:types.frozenarray[types.strictint], ndofs:types.strictint, transforms:transformseq.stricttransforms, trans:types.strict[TransformChain]): + self._coeffs = coeffs + self._start_dofs = start_dofs + self._stop_dofs = stop_dofs + super().__init__(ndofs=ndofs, transforms=transforms, ndims=1, trans=trans) + + def get_dofs(self, ielem): + if not numeric.isint(ielem): + return super().get_dofs(ielem) + return types.frozenarray(numpy.arange(self._start_dofs[ielem], self._stop_dofs[ielem]) % self.ndofs, copy=False) + + def get_coefficients(self, ielem): + return self._coeffs[ielem] + + def get_support(self, dof): + if not numeric.isint(dof): + return super().get_support(dof) + dof = numeric.normdim(self.ndofs, dof) + supports = [] + while dof < self._stop_dofs[-1]: + stop_ielem = numpy.searchsorted(self._start_dofs, dof, side='right') + start_ielem = numpy.searchsorted(self._stop_dofs, dof, side='right') + supports.append(numpy.arange(start_ielem, stop_ielem)) + dof += self.ndofs + return types.frozenarray(numpy.unique(numpy.concatenate(supports)), dtype=int, copy=False) + class StructuredBasis(Basis): '''A basis for class:`nutils.transformseq.StructuredTransforms`. diff --git a/nutils/mesh.py b/nutils/mesh.py index 8fd8eb301..1868f2ec9 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -44,8 +44,13 @@ def line(nodes, periodic=False, bnames=None, *, rootid='line'): offset = nodes[0] uniform = numpy.equal(nodes, offset + numpy.arange(nelems+1) * scale).all() root = function.Root(rootid, 1) - domain = topology.StructuredLine(root, 0, nelems, periodic=periodic, bnames=bnames) - geom = function.rootcoords(root) * scale + offset if uniform else domain.basis('std', degree=1, periodic=[]).dot(nodes)[_] + transforms = transformseq.IdentifierTransforms(1, rootid, nelems) + domain = topology.StructuredLine(root, transforms, periodic, bnames) + if uniform: + ielem = function.TransformsIndexWithTail(domain.transforms, 1, function.SelectChain((root,))).index + geom = (function.rootcoords(root) + ielem) * scale + offset + else: + geom = domain.basis('std', degree=1, periodic=False).dot(nodes) return domain, geom @log.withcontext diff --git a/nutils/topology.py b/nutils/topology.py index b3d6bdd13..89cb73a0c 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -20,7 +20,7 @@ """ The topology module defines the topology objects, notably the -:class:`StructuredTopology`. Maintaining strict separation of topological and +:class:`StructuredLine`. Maintaining strict separation of topological and geometrical information, the topology represents a set of elements and their interconnectivity, boundaries, refinements, subtopologies etc, but not their positioning in physical space. The dimension of the topology represents the @@ -375,7 +375,7 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None if leveltopo is None: verts = self.sample('vertex', maxrefine) levels = verts.eval(levelset) - refs = [ref.trim(levels[verts.index[ielem]], maxrefine=maxrefine, ndivisions=ndivisions) for ielem, ref in enumerate(self.references)] + refs = [ref.trim(levels[verts.getindex(ielem)], maxrefine=maxrefine, ndivisions=ndivisions) for ielem, ref in enumerate(self.references)] else: log.info('collecting leveltopo elements') bins = [set() for ielem in range(len(self))] @@ -577,8 +577,9 @@ def revolved(self, geom): def extruded(self, geom, nelems, periodic=False, bnames=('front','back')): assert geom.ndim == 1 root = transform.Identifier('extrude', 1) - extopo = self * StructuredLine(root, i=0, j=nelems, periodic=periodic, bnames=bnames) - exgeom = function.concatenate(function.bifurcate(geom, function.rootcoords(root))) + extransforms = transformseq.IdentifierTransforms(1, 'extrude', nelems) + extopo = self * StructuredLine(root, extransforms, periodic, bnames) + exgeom = extopo.basis('std', degree=1).dot(numpy.arange(nelems+1)) return extopo, exgeom @property @@ -847,10 +848,319 @@ def __init__(self, root, trans, opposite): opposites = transforms if opposite is None else transformseq.PlainTransforms((opposite,), root.ndims, 0) super().__init__((root,), references, transforms, opposites) -def StructuredLine(root:function.strictroot, i:types.strictint, j:types.strictint, periodic:bool=False, bnames:types.tuple[types.strictstr]=None): - if bnames is None: - bnames = ('_structured_line_dummy_boundary_name_',) * 2 - return StructuredTopology(root, axes=(transformseq.DimAxis(i,j,periodic),), nrefine=0, bnames=(bnames,)) +class PointsTopology(Topology): + 'points' + + __slots__ = () + __cache__ = 'connectivity', 'refined' + + @types.apply_annotations + def __init__(self, roots:types.tuple[function.strictroot], transforms:transformseq.stricttransforms, opposites:transformseq.stricttransforms): + references = elementseq.asreferences([element.getsimplex(0)], 0)*len(transforms) + super().__init__(roots, references, transforms, opposites) + + def __repr__(self): + return 'PointsTopology<{}>'.format(len(self)) + + def getitem(self, item): + if isinstance(item, tuple): + if len(item) != 1: + raise ValueError('expected a tuple of length 1 but got length {}'.format(len(item))) + item = item[0] + if not isinstance(item, slice): + return EmptyTopology(self.roots, self.ndims) + if item == slice(None): + return self + else: + return PointsTopology(self.roots, self.transforms[item], self.opposites[item]) + + @property + def connectivity(self): + return types.frozenarray(numpy.zeros((len(self), 0), int)) + + @property + def boundary(self): + raise ValueError('a 0D topology has no boundary') + + @property + def interfaces(self): + raise ValueError('a 0D topology has no interfaces') + + @property + def refined(self): + return PointsTopology(self.roots, self.transforms.refined(self.references), self.opposites.refined(self.references)) + +class StructuredLine(Topology): + '''StructuredLine''' + + __slots__ = '_bnames', 'periodic' + __cache__ = 'connectivity', 'boundary', 'interfaces' + + @types.apply_annotations + def __init__(self, root:function.strictroot, transforms:transformseq.stricttransforms, periodic:bool=False, bnames:types.tuple[types.strictstr]=None): + self._bnames = bnames + self.periodic = periodic + references = elementseq.asreferences([element.LineReference()], 1)*len(transforms) + super().__init__((root,), references, transforms, transforms) + + def __repr__(self): + return '{}<{}{}>'.format(type(self).__qualname__, len(self), 'p' if self.periodic else '') + + def getitem(self, item): + if isinstance(item, tuple): + if len(item) != 1: + raise ValueError('expected a tuple of length 1 but got length {}'.format(len(item))) + item = item[0] + if not isinstance(item, slice): + return EmptyTopology(self.roots, self.ndims) + start, stop, step = item.indices(len(self)) + if item == slice(None): + return self + elif step != 1: + return super().getitem(item) + elif start == 0 and stop == len(self): + return StructuredLine(self.roots[0], self.transforms, False, self._bnames) + else: + return SliceOfStructuredLine(self, start, stop) + + @property + def connectivity(self): + connectivity = numpy.stack([numpy.arange(1, len(self)+1), numpy.arange(-1, len(self)-1)], axis=1) + if self.periodic: + connectivity %= len(self) + else: + connectivity[-1,0] = -1 + return types.frozenarray(connectivity) + + @property + def boundary(self): + if self.periodic: + return EmptyTopology(self.roots, 0) + idx = types.frozenarray([1, 2*len(self)-2], dtype=int) + btransforms = self.transforms.edges(self.references)[idx] + btopo = PointsTopology(self.roots, btransforms, btransforms) + if self._bnames: + btopo = btopo.withgroups(vgroups={bname: btopo[i:i+1] for i, bname in enumerate(self._bnames)}) + return btopo + + @property + def interfaces(self): + if self.periodic: + idx = types.frozenarray(numpy.arange(0, len(self)*2, 2)) + oppidx = types.frozenarray(numpy.arange(3, len(self)*2+2, 2)%(len(self)*2)) + elif len(self) == 1: + return EmptyTopology(self.roots, 0) + else: + idx = types.frozenarray(numpy.arange(0, len(self)*2-2, 2)) + oppidx = types.frozenarray(numpy.arange(3, len(self)*2, 2)) + edges = self.transforms.edges(self.references) + return PointsTopology(self.roots, edges[idx], edges[oppidx]) + + @property + def refined(self): + return StructuredLine(self.roots[0], self.transforms.refined(self.references), self.periodic, self._bnames) + + # TODO: locate + + def basis_spline(self, degree, removedofs=None, knotvalues=None, knotmultiplicities=None, continuity=-1, periodic=None): + 'spline basis' + + if numpy.iterable(removedofs): + if len(removedofs) != 1: + raise ValueError('removedofs should be a tuple or list of length 1 but got {}'.format(len(removedofs))) + removedofs = removedofs[0] + + if numpy.iterable(periodic): + if len(periodic) != 1: + raise ValueError('periodic should be a tuple or list of length 1 but got {}'.format(len(periodic))) + periodic = periodic[0] + if periodic is None: + periodic = self.periodic + elif not isinstance(periodic, bool): + raise NotImplementedError + + if numpy.iterable(degree): + if len(degree) != 1: + raise ValueError('degree should be a tuple or list of length 1 but got {}'.format(len(degree))) + degree = degree[0] + + if numpy.iterable(knotvalues) and all(v is None or numpy.iterable(v) for v in knotvalues): + if len(knotvalues) != 1: + raise ValueError('knotvalues should be a tuple or list of length 1 but got {}'.format(len(knotvalues))) + knotvalues = knotvalues[0] + if knotvalues: + knotvalues = numpy.array(knotvalues) + assert knotvalues.ndim == 1 + + if numpy.iterable(knotmultiplicities) and all(v is None or numpy.iterable(v) for v in knotmultiplicities): + if len(knotmultiplicities) != 1: + raise ValueError('knotmultiplicities should be a tuple or list of length 1 but got {}'.format(len(knotmultiplicities))) + knotmultiplicities = knotmultiplicities[0] + if knotmultiplicities: + knotmultiplicities = numpy.array(knotmultiplicities) + assert knotmultiplicities.ndim == 1 and knotmultiplicities.dtype.kind == 'i' + + if numpy.iterable(continuity): + if len(continuity) != 1: + raise ValueError('continuity should be a tuple or list of length 1 but got {}'.format(len(continuity))) + continuity = continuity[0] + + p = degree + n = len(self) + + c = continuity + if c < 0: + c += p + assert -1 <= c < p + + k = knotvalues + if k is None: + k = numpy.arange(n+1) # default to uniform spacing + else: + k = numpy.array(k) + while len(k) < n+1: + k_ = numpy.empty(len(k)*2-1) + k_[::2] = k + k_[1::2] = (k[:-1] + k[1:]) / 2 + k = k_ + assert len(k) == n+1, 'knot values do not match the topology size' + + m = knotmultiplicities + if m is None: + m = numpy.repeat(p-c, n+1) # default to open spline without internal repetitions + else: + m = numpy.array(m) + assert min(m) > 0 and max(m) <= p+1, 'incorrect multiplicity encountered' + while len(m) < n+1: + m_ = numpy.empty(len(m)*2-1, dtype=int) + m_[::2] = m + m_[1::2] = p-c + m = m_ + assert len(m) == n+1, 'knot multiplicity do not match the topology size' + + if periodic and not m[0] == m[n] == p+1: # if m[0] == m[n] == p+1 the spline is discontinuous at the boundary + assert m[0] == m[n], 'periodic spline multiplicity expected' + dk = k[n] - k[0] + m = m[:n] + k = k[:n] + nd = m.sum() + while m[n:].sum() < p - m[0] + 2: + k = numpy.concatenate([k, k+dk]) + m = numpy.concatenate([m, m]) + dk *= 2 + km = numpy.array([ki for ki, mi in zip(k, m) for cnt in range(mi)], dtype=float) + if p > m[0]: + km = numpy.concatenate([km[-p+m[0]:] - dk, km]) + else: + m[0] = m[-1] = p + nd = m[:n].sum()+1 + km = numpy.array([ki for ki, mi in zip(k, m) for cnt in range(mi)], dtype=float) + + offsets = numpy.cumsum(m[:n]) - m[0] + start_dofs = offsets + stop_dofs = offsets+p+1 + dofshape = nd + + coeffs = [] + cache = {} + for offset in offsets: + lknots = km[offset:offset+2*p] + key = tuple(numeric.round((lknots[1:-1]-lknots[0])/(lknots[-1]-lknots[0])*numpy.iinfo(numpy.int32).max)) if lknots.size else (), p + try: + local_coeffs = cache[key] + except KeyError: + local_coeffs = cache[key] = self._localsplinebasis(lknots) + coeffs.append(local_coeffs) + coeffs = tuple(coeffs) + + func = function.StructuredLineBasis(coeffs, start_dofs, stop_dofs, nd, self.transforms, function.SelectChain(self.roots)) + if not removedofs: + return func + + mask = numpy.ones((nd,), dtype=bool) + mask[[numeric.normdim(nd,idof) for idof in removedofs]] = False + return func[mask] + + @staticmethod + def _localsplinebasis(lknots): + + assert numeric.isarray(lknots), 'Local knot vector should be numpy array' + p, rem = divmod(len(lknots), 2) + assert rem == 0 + + #Based on Algorithm A2.2 Piegl and Tiller + N = [None]*(p+1) + N[0] = numpy.poly1d([1.]) + + if p > 0: + + assert numpy.less(lknots[:-1]-lknots[1:], numpy.spacing(1)).all(), 'Local knot vector should be non-decreasing' + assert lknots[p]-lknots[p-1]>numpy.spacing(1), 'Element size should be positive' + + lknots = lknots.astype(float) + + xi = numpy.poly1d([lknots[p]-lknots[p-1],lknots[p-1]]) + + left = [None]*p + right = [None]*p + + for i in range(p): + left[i] = xi - lknots[p-i-1] + right[i] = -xi + lknots[p+i] + saved = 0. + for r in range(i+1): + temp = N[r]/(lknots[p+r]-lknots[p+r-i-1]) + N[r] = saved+right[r]*temp + saved = left[i-r]*temp + N[i+1] = saved + + assert all(Ni.order==p for Ni in N) + + return types.frozenarray([Ni.coeffs[::-1] for Ni in N]) + + def basis_std(self, *args, **kwargs): + return __class__.basis_spline(self, *args, continuity=0, **kwargs) + +class SliceOfStructuredLine(StructuredLine): + + __slots__ = '_line', '_start', '_stop' + __cache__ = 'boundary' + + @types.apply_annotations + def __init__(self, line:types.strict[StructuredLine], start:types.strictint, stop:types.strictint): + assert type(line) == StructuredLine + self._line = line + self._start = start + self._stop = stop + # TODO: copy bnames? + super().__init__(line.roots[0], line.transforms[start:stop], False, line._bnames) + + def getitem(self, item): + if isinstance(item, tuple): + if len(item) != 1: + raise ValueError('expected a tuple of length 1 but got length {}'.format(len(item))) + item = item[0] + if not isinstance(item, slice): + return EmptyTopology(self.roots, self.ndims) + r = range(self._start, self._stop)[item] + if r.step != 1: + return super().getitem(item) + return self._line[r.start:r.stop] + + @property + def boundary(self): + idx = types.frozenarray([2*self._start+1, 2*self._stop-2], dtype=int) + n = len(self._line) + oppidx = types.frozenarray([1 if self._start == 0 else 2*self._start-2, 2*n-2 if self._stop == n else 2*self._stop+1]) + edges = self._line.transforms.edges(self.references) + btopo = PointsTopology(self.roots, edges[idx], edges[oppidx]) + if self._bnames: + btopo = btopo.withgroups(vgroups={bname: btopo[i:i+1] for i, bname in enumerate(self._bnames)}) + return btopo + + @property + def refined(self): + return SliceOfStructuredLine(self._line.refined, self._start*2, self._stop*2) class StructuredTopology(Topology): 'structured topology' @@ -862,6 +1172,8 @@ class StructuredTopology(Topology): def __init__(self, root:function.strictroot, axes:types.tuple[types.strict[transformseq.Axis]], nrefine:types.strictint=0, bnames:types.tuple[types.tuple[types.strictstr]]=(('left', 'right'), ('bottom', 'top'), ('front', 'back'))): 'constructor' + if root.ndims != 1: + raise ValueError('the `StructuredTopology` must have a 1D root but got a {}D root'.format(root.ndims)) assert all(len(bname) == 2 for bname in bnames) self.root = root @@ -1318,7 +1630,8 @@ def __init__(self, root:function.strictroot, simplices:_renumber, transforms:tra assert simplices.ndim == 2 assert simplices.shape[0] == len(transforms) assert numpy.greater(simplices[:,1:], simplices[:,:-1]).all(), 'nodes should be sorted' - assert not numpy.equal(simplices[:,1:], simplices[:,:-1]).all(), 'duplicate nodes' + if simplices.shape[1] > 1: + assert not numpy.equal(simplices[:,1:], simplices[:,:-1]).all(), 'duplicate nodes' ndims = simplices.shape[1] - 1 self.simplices = simplices references = elementseq.asreferences([element.getsimplex(ndims)], ndims)*len(transforms) From 775a93581a4c87cf3ea70cd790e8aa9cbb4ee5b0 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 10 Mar 2020 16:54:38 +0100 Subject: [PATCH 057/119] add Transforms.linear --- nutils/function.py | 19 +++++++------ nutils/transformseq.py | 62 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index e977a3f4b..bfce2d925 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -1363,6 +1363,7 @@ def _derivative(self, var, seen): class Linear(Array): __slots__ = '_roots', '_transforms' + __cache__ = 'simplified' @types.apply_annotations def __init__(self, roots:types.tuple[strictroot], transforms:transformseq.stricttransforms, index:asarray, fromdims:types.strictint): @@ -1373,14 +1374,16 @@ def __init__(self, roots:types.tuple[strictroot], transforms:transformseq.strict def evalf(self, index): index, = index - result = numpy.zeros(self.shape, dtype=float) - to1 = from1 = 0 - for root, chain in zip(self._roots, self._transforms[index]): - assert chain[0].todims == root.ndims - to0, from0, to1, from1 = to1, from1, to1 + root.ndims, from1 + chain[-1].fromdims - result[to0:to1,from0:from1] = transform.linear(chain, root.ndims) - assert (to1, from1) == self.shape - return result[_] + if self._transforms.linear_is_uniform: + numpy.testing.assert_allclose(self._transforms.linear(index), self._transforms.linear(0)) + return self._transforms.linear(index)[_] + + @property + def simplified(self): + if self._transforms.linear_is_uniform: + return asarray(self._transforms.linear(0)) + else: + return self class Inverse(Array): ''' diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 19e170ad0..b2425874c 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -20,7 +20,7 @@ """The transformseq module.""" -from . import types, numeric, util, transform, element, elementseq +from . import types, numeric, util, transform, element, elementseq, numeric import abc, itertools, operator, numpy class Transforms(types.Singleton): @@ -318,6 +318,13 @@ def unchain(self): yield self + @property + def linear_is_uniform(self): + return False + + def linear(self, index): + return numeric.blockdiag(map(transform.linear, self[index], self.todims)) + stricttransforms = types.strict[Transforms] class EmptyTransforms(Transforms): @@ -448,6 +455,13 @@ def index_with_tail(self, mtrans): return root.token[1], (trans[1:],) raise ValueError + @property + def linear_is_uniform(self): + return True + + def linear(self, ielem): + return numpy.eye(self._ndims) + class Axis(types.Singleton): '''Abstract base class for axes of :class:`~nutils.topology.StructuredTopology`.''' @@ -624,9 +638,6 @@ def index_with_tail(self, mtrans): return flatindex, (tail,) - def linear_evaluable(self, index, roots): - return function.asarray(transform.linear(self[0][0], self.todims[0])), - class MaskedTransforms(Transforms): '''An order preserving subset of another :class:`Transforms` object. @@ -666,6 +677,13 @@ def index_with_tail(self, trans): else: return int(index), tail + @property + def linear_is_uniform(self): + return self._parent.linear_is_uniform + + def linear(self, index): + return self._parent.linear(self._indices[index]) + class ReorderedTransforms(Transforms): '''A reordered :class:`Transforms` object. @@ -706,6 +724,13 @@ def index_with_tail(self, trans): parent_index, tail = self._parent.index_with_tail(trans) return int(self._rindices[parent_index]), tail + @property + def linear_is_uniform(self): + return self._parent.linear_is_uniform + + def linear(self, index): + return self._parent.linear(self._indices[index]) + class DerivedTransforms(Transforms): '''A sequence of derived transforms. @@ -793,6 +818,7 @@ class UniformDerivedTransforms(Transforms): ''' __slots__ = '_parent', '_derived_transforms', '_updim' + __cache__ = 'linear_is_uniform' @types.apply_annotations def __init__(self, parent:stricttransforms, parent_reference:element.strictreference, derived_attribute:types.strictstr, updim:types.strict[bool]): @@ -825,6 +851,13 @@ def index_with_tail(self, trans): iderived, tail = (transform.index_edge_transforms_with_tail if self._updim else transform.index_child_transforms_with_tail)(self._derived_transforms, parenttail, todims) return iparent*len(self._derived_transforms) + iderived, tail + @property + def linear_is_uniform(self): + if not self._parent.linear_is_uniform or len(self._derived_transforms) == 0: + return False + linear = self._derived_transforms[0].linear + return all(numpy.allclose(linear, trans.linear) for trans in self._derived_transforms[1:]) + class TrimmedEdgesTransforms(Transforms): __slots__ = '_parent', '_edges' @@ -911,6 +944,14 @@ def index_with_tail(self, trans): index2, tail2 = self._transforms2.index_with_tail(trans[len(self._transforms1.todims):]) return index1*len(self._transforms2)+index2, tail1+tail2 + @property + def linear_is_uniform(self): + return self._transforms1.linear_is_uniform and self._transforms2.linear_is_uniform + + def linear(self, index): + index1, index2 = divmod(numeric.normdim(len(self), index), len(self._transforms2)) + return numeric.blockdiag([self._transforms1.linear(index1), self._transforms2.linear(index2)]) + class ChainedTransforms(Transforms): '''A sequence of chained :class:`Transforms` objects. @@ -987,6 +1028,19 @@ def edges(self, references): def unchain(self): yield from self._items + @property + def linear_is_uniform(self): + if not all(item.linear_is_uniform for item in self._items): + return False + linear = self._items[0].linear(0) + return all(numpy.allclose(item.linear(0), linear) for item in self._items[1:]) + + def linear(self, index): + index = numeric.normdim(len(self), index) + outer = numpy.searchsorted(self._offsets, index, side='right') - 1 + assert outer >= 0 and outer < len(self._items) + return self._items[outer].linear(index-self._offsets[outer]) + @types.apply_annotations def chain(items:types.tuple[stricttransforms], todims:types.tuple[types.strictint]): '''Return the chained transforms sequence of ``items``. From f2df8e82ed1547149d299ff51b57626bec1524ed Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sat, 21 Mar 2020 22:28:47 +0100 Subject: [PATCH 058/119] updates --- nutils/function.py | 107 +++++++++++++++++++++++++++++------------ nutils/mesh.py | 2 +- nutils/points.py | 14 +++--- nutils/sample.py | 106 +++++++++++++++++++++++++++++++++++++--- nutils/topology.py | 13 ++++- nutils/transformseq.py | 28 ++++++++--- 6 files changed, 215 insertions(+), 55 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index bfce2d925..e16ed4f0c 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -107,7 +107,7 @@ class Subsample: __slots__ = 'roots', 'transforms', 'points', 'ielem' - def __init__(self, roots: types.tuple[strictroot], transforms: types.tuple[types.tuple[transform.stricttransform]], points: points.strictpoints, ielem: types.strictint = None): + def __init__(self, *, roots: types.tuple[strictroot], transforms: types.tuple[types.tuple[transform.stricttransform]], points: points.strictpoints, ielem: types.strictint = None): self.roots = roots self.transforms = transforms self.points = points @@ -139,6 +139,8 @@ class SubsampleMeta: transforms : :class:`tuple` of :class:`~nutils.transformseq.Transforms`, optional points : :class:`~nutils.points.Points`, options The points object if invariant, otherwise ``None``. + ndimspoints : :class:`int` + The dimension of the points object if invariant, otherwise ``None``. Attributes ---------- @@ -147,15 +149,18 @@ class SubsampleMeta: transforms : :class:`tuple` of :class:`~nutils.transformseq.Transforms` or ``None`` points : :class:`~nutils.points.Points`, options The points object if invariant, otherwise ``None``. + ndimspoints : :class:`int` + The dimension of the points object if invariant, otherwise ``None``. ''' - __slots__ = 'roots', 'ndimsnormal', 'transforms', 'points' + __slots__ = 'roots', 'ndimsnormal', 'transforms', 'points', 'ndimspoints' - def __init__(self, roots:types.tuple[strictroot], ndimsnormal:types.strictint, transforms:types.tuple[transformseq.stricttransforms]=None, points:points.strictpoints=None): + def __init__(self, *, roots:types.tuple[strictroot], ndimsnormal:types.strictint, transforms:types.tuple[transformseq.stricttransforms]=None, points:points.strictpoints=None, ndimspoints:types.strictint=None): self.roots = roots self.ndimsnormal = ndimsnormal self.transforms = transforms self.points = points + self.ndimspoints = ndimspoints @property def ndims(self): @@ -262,7 +267,8 @@ def asciitree(self, richoutput=False): if prefix: s = prefix[:-2] + select[bridge.index(prefix[-2:])] + s # locally change prefix into selector if ordereddeps[n] is not None: - s += ' = {} {}'.format(ordereddeps[n]._asciitree_str(), ','.join(sorted(tuple(map('{0.name}:{0.ndims}'.format, ordereddeps[n].roots))))) + dep = ordereddeps[n] + s += ' = {} {}'.format(dep._asciitree_str(), 'CONST' if dep.isconstant else ','.join(sorted(tuple(map('{0.name}:{0.ndims}'.format, dep.roots))))) pool.extend((prefix + bridge[i==0], arg) for i, arg in enumerate(reversed(self.dependencytree[n]))) ordereddeps[n] = None lines.append(s) @@ -519,7 +525,7 @@ def tail(self): @property def linear(self): - return Linear(self.trans.ordered_roots, self.transforms, self.index, self.ndims) + return Linear(self.trans.ordered_roots, self.ndims, (self.transforms,), self.index)[:,:self.ndims] def __iter__(self): yield self.index @@ -782,25 +788,21 @@ class RootBasis(Array): The roots to compute the basis for. ndimstangent : int The dimension of the tangent space. - trans : :class:`TransformChain` ''' - __slots__ = '_isubsample', '_roots', '_ndimstangent', '_opposite', '_trans' + __slots__ = '_isubsample', '_roots', '_ndimstangent', '_opposite' __cache__ = 'prepare_eval' @types.apply_annotations - def __init__(self, isubsample:types.strictint, roots:types.tuple[strictroot], ndimstangent:types.strictint, trans:types.strict[TransformChain], _opposite:bool=None): - # NOTE: `trans` is only required because of the `SelectChain` test in `Opposite.simplified`. + def __init__(self, isubsample:types.strictint, roots:types.tuple[strictroot], ndimstangent:types.strictint, _opposite:bool=None): self._isubsample = isubsample self._roots = roots self._ndimstangent = ndimstangent self._opposite = _opposite - self._trans = trans - assert self._trans.ordered_roots == roots ndims = sum(root.ndims for root in roots) - super().__init__(args=[SUBSAMPLES, trans], shape=[ndims, ndims], dtype=float) + super().__init__(args=[SUBSAMPLES], shape=[ndims, ndims], dtype=float) - def evalf(self, subsamples, _trans): + def evalf(self, subsamples): subsample = subsamples[self._isubsample] assert subsample.roots == self._roots assert subsample.ndimsmanifold == self._ndimstangent @@ -836,7 +838,7 @@ def evalf(self, subsamples, _trans): def prepare_eval(self, *, opposite=False, kwargs=...): if self._opposite is not None: raise ValueError('prepare already called') - return RootBasis(self._isubsample, self._roots, self._ndimstangent, self._trans.prepare_eval(opposite=opposite, **kwargs), _opposite=opposite) + return RootBasis(self._isubsample, self._roots, self._ndimstangent, _opposite=opposite) class GramSchmidt(Array): @@ -935,6 +937,12 @@ def simplified(self): def evalf(self): return self.value[_] + def _asciitree_str(self): + if self.size < 10: + return '{} {}'.format(super()._asciitree_str(), self.value.tolist()) + else: + return super()._asciitree_str() + @property def _isunit(self): return numpy.equal(self.value, 1).all() @@ -1362,28 +1370,40 @@ def _derivative(self, var, seen): class Linear(Array): - __slots__ = '_roots', '_transforms' + __slots__ = '_roots', '_fromdims', '_transforms', '_ielem', '_itransforms' __cache__ = 'simplified' @types.apply_annotations - def __init__(self, roots:types.tuple[strictroot], transforms:transformseq.stricttransforms, index:asarray, fromdims:types.strictint): - assert frozenset(roots) == index.roots + def __init__(self, roots:types.tuple[strictroot], fromdims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], ielem:asarray, _itransforms:types.strictint=None): self._roots = roots + self._fromdims = fromdims self._transforms = transforms - super().__init__(args=[index], shape=(builtins.sum(root.ndims for root in roots), fromdims), dtype=float) + self._ielem = ielem + self._itransforms = _itransforms + super().__init__(args=[ielem], shape=(builtins.sum(root.ndims for root in roots),)*2, dtype=float) - def evalf(self, index): - index, = index - if self._transforms.linear_is_uniform: - numpy.testing.assert_allclose(self._transforms.linear(index), self._transforms.linear(0)) - return self._transforms.linear(index)[_] + @property + def roots(self): + return frozenset(self._roots) + + def evalf(self, ielem): + ielem, = ielem + linear, ismanifold = self._transforms[self._itransforms].linear(ielem) + assert ismanifold.sum() == self._fromdims + return numpy.concatenate([linear[:,ismanifold], linear[:,~ismanifold]], axis=1)[_] @property def simplified(self): - if self._transforms.linear_is_uniform: - return asarray(self._transforms.linear(0)) - else: - return self + ielem = 0 if self._itransforms is not None and self._transforms[self._itransforms].linear_is_uniform else self._ielem.simplified + return Linear(self._roots, self._fromdims, self._transforms, ielem, self._itransforms) + + @util.positional_only + def prepare_eval(self, *, kwargs=...): + ielem = self._ielem.prepare_eval(**kwargs) + if self._itransforms is not None: + raise RuntimeError('prepare_eval called twice') + itransforms = 1 if kwargs.get('opposite', False) and len(self._transforms) > 1 else 0 + return Linear(self._roots, self._fromdims, self._transforms, ielem, itransforms) class Inverse(Array): ''' @@ -1721,8 +1741,8 @@ def optimized_for_numpy(self): mask[axis] &= 1 func2 = func2.func if all(mask): # should always be the case after simplify - return Einsum(func1, func2, mask) - return Multiply([func1, func2]) + return Einsum(func1, func2, mask).optimized_for_numpy + return Multiply([func1.optimized_for_numpy, func2.optimized_for_numpy]) def evalf(self, arr1, arr2): return arr1 * arr2 @@ -1933,6 +1953,25 @@ def __init__(self, func1:asarray, func2:asarray, mask:types.tuple[types.strictin def evalf(self, arr1, arr2): return numpy.core.multiarray.c_einsum(self._einsumfmt, arr1, arr2) + def _asciitree_str(self): + return '{} {}'.format(super()._asciitree_str(), self._einsumfmt) + + @property + def optimized_for_numpy(self): + func1 = self.func1.optimized_for_numpy + func2 = self.func2.optimized_for_numpy + if func1.isconstant and isinstance(func2, Einsum) and (func2.func1.isconstant or func2.func2.isconstant) and not self.mask and not func2.mask: + if func2.func1.isconstant: + return Einsum(func2.func2, Einsum(func1, func2.func1, ()).optimized_for_numpy, ()) + else: + return Einsum(func2.func1, Einsum(func1, func2.func2, ()).optimized_for_numpy, ()) + elif func2.isconstant and isinstance(func1, Einsum) and (func1.func1.isconstant or func1.func2.isconstant) and not self.mask and not func1.mask: + if func1.func2.isconstant: + return Einsum(func1.func2, Einsum(func2, func1.func1, ()).optimized_for_numpy, ()) + else: + return Einsum(func1.func1, Einsum(func2, func1.func2, ()).optimized_for_numpy, ()) + return super().optimized_for_numpy + class Sum(Array): __slots__ = 'axis', 'func' @@ -1964,7 +2003,7 @@ def optimized_for_numpy(self): axis = axes[self.axis] if mask[axis] == 3: mask[axis] = 0 - return Einsum(func.func1, func.func2, mask) + return Einsum(func.func1, func.func2, mask).optimized_for_numpy return Sum(func, self.axis) def evalf(self, arr): @@ -4690,8 +4729,14 @@ def rootbasis(subsamples, index_or_roots, *, orthonormal=False): raise ValueError('no subsample with roots {}, candidates: {}'.format(roots, ', '.join(str(subsample.roots) for subsample in subsamples))) else: raise ValueError('expected an `int` or `tuple` of `Root` objects but got {!r}'.format(index_or_roots)) - basis = RootBasis(isubsample, subsample.roots, subsample.ndimsmanifold, SelectChain(subsample.roots)) + if subsample.ndimspoints == subsample.ndimsmanifold: + # `points.basis` is the identity. + basis = Linear(subsample.roots, subsample.ndimspoints, subsample.transforms, IndexFromSubsample(isubsample, subsample.roots)) + else: + basis = RootBasis(isubsample, subsample.roots, subsample.ndimsmanifold) if orthonormal: + if subsample.ndimsnormal == 0: + return eye(subsample.ndims) basis = GramSchmidt(basis) return basis diff --git a/nutils/mesh.py b/nutils/mesh.py index 1868f2ec9..5d7a858ca 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -50,7 +50,7 @@ def line(nodes, periodic=False, bnames=None, *, rootid='line'): ielem = function.TransformsIndexWithTail(domain.transforms, 1, function.SelectChain((root,))).index geom = (function.rootcoords(root) + ielem) * scale + offset else: - geom = domain.basis('std', degree=1, periodic=False).dot(nodes) + geom = domain.basis('std', degree=1, periodic=False).dot(nodes)[_] return domain, geom @log.withcontext diff --git a/nutils/points.py b/nutils/points.py index db4d29d2f..32307db1f 100644 --- a/nutils/points.py +++ b/nutils/points.py @@ -182,7 +182,7 @@ def weights(self): @property def tri(self): - if self.points1.ndims == 1: + if self.points1.ndimsmanifold == 1: # For an n-dimensional simplex with vertices a0,a1,..,an, the extruded # element has vertices a0,a1,..,an,b0,b1,..,bn. These can be divided in # simplices by selecting a0,a1,..,an,b0; a1,..,an,b0,n1; and so on until @@ -191,18 +191,18 @@ def tri(self): # of triangulations and raveling, effectively achieving vectorized # concatenation. The overlapping vertex subsets then follow directly from # numeric.overlapping. - tri12 = self.points1.tri[:,_,:,_] * self.points2.npoints + self.points2.tri[_,:,_,:] # ntri1 x ntri2 x 2 x ndims - return types.frozenarray(numeric.overlapping(tri12.reshape(-1, 2*self.ndims), n=self.ndims+1).reshape(-1, self.ndims+1), copy=False) + tri12 = self.points1.tri[:,_,:,_] * self.points2.npoints + self.points2.tri[_,:,_,:] # ntri1 x ntri2 x 2 x ndimsmanifold + return types.frozenarray(numeric.overlapping(tri12.reshape(-1, 2*self.ndimsmanifold), n=self.ndimsmanifold+1).reshape(-1, self.ndimsmanifold+1), copy=False) return super().tri @property def hull(self): - if self.points1.ndims == 1: - hull1 = self.points1.hull[:,_,:,_] * self.points2.npoints + self.points2.tri[_,:,_,:] # 2 x ntri2 x 1 x ndims - hull2 = self.points1.tri[:,_,:,_] * self.points2.npoints + self.points2.hull[_,:,_,:] # ntri1 x nhull2 x 2 x ndims-1 + if self.points1.ndimsmanifold == 1: + hull1 = self.points1.hull[:,_,:,_] * self.points2.npoints + self.points2.tri[_,:,_,:] # 2 x ntri2 x 1 x ndimsmanifold + hull2 = self.points1.tri[:,_,:,_] * self.points2.npoints + self.points2.hull[_,:,_,:] # ntri1 x nhull2 x 2 x ndimsmanifold-1 # The subdivision of hull2 into simplices follows identical logic to that # used in the construction of self.tri. - hull = numpy.concatenate([hull1.reshape(-1, self.ndims), numeric.overlapping(hull2.reshape(-1, 2*(self.ndims-1)), n=self.ndims).reshape(-1, self.ndims)]) + hull = numpy.concatenate([hull1.reshape(-1, self.ndimsmanifold), numeric.overlapping(hull2.reshape(-1, 2*(self.ndimsmanifold-1)), n=self.ndimsmanifold).reshape(-1, self.ndimsmanifold)]) return types.frozenarray(hull, copy=False) return super().hull diff --git a/nutils/sample.py b/nutils/sample.py index a28b2fb5e..1e805e51d 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -44,7 +44,7 @@ class which represents postponed integration. Integrals are internally ''' from . import types, points, util, function, parallel, numeric, matrix, transformseq, sparse, warnings -import numpy, numbers, collections.abc, os, treelog as log, operator +import numpy, numbers, collections.abc, os, treelog as log, operator, functools graphviz = os.environ.get('NUTILS_GRAPHVIZ') @@ -84,7 +84,7 @@ class Sample(types.Singleton): contain points. ''' - __cache__ = 'allcoords', 'index' + __cache__ = 'allcoords', 'index', 'subsamplemetas' @types.apply_annotations def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint, npoints:types.strictint, transforms:types.tuple[transformseq.stricttransforms]): @@ -146,7 +146,7 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. detJ = 1 for isubsample, subsample in enumerate(self.subsamplemetas): - J = function.RootBasis(isubsample, subsample.roots, subsample.ndimsmanifold, function.SelectChain(subsample.roots))[:,:subsample.ndimsmanifold] + J = function.RootBasis(isubsample, subsample.roots, subsample.ndimsmanifold)[:,:subsample.ndimsmanifold] if J.shape[0] == J.shape[1]: detJ *= abs(function.determinant(J)) else: @@ -294,7 +294,7 @@ def tri(self): row defines a simplex by mapping vertices into the list of points. ''' - return numpy.concatenate([self.getindex(ielem).take(self.getpoints(ielem).tri) for ielem in range(self.nelems)]) + return types.frozenarray(numpy.concatenate([self.getindex(ielem).take(self.getpoints(ielem).tri) for ielem in range(self.nelems)]), copy=False) @property def hull(self): @@ -306,7 +306,7 @@ def hull(self): triangulations originating from separate elements are disconnected. ''' - return numpy.concatenate([self.getindex(ielem).take(self.getpoints(ielem).hull) for ielem in range(self.nelems)]) + return types.frozenarray(numpy.concatenate([self.getindex(ielem).take(self.getpoints(ielem).hull) for ielem in range(self.nelems)]), copy=False) def subset(self, mask): '''Reduce the number of points. @@ -338,7 +338,13 @@ def getsubsamples(self, ielem): @property def subsamplemetas(self): - return function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims, transforms=self.transforms), + if self.nelems: + ndimspoints = self.getpoints(0).ndims + if not all(self.getpoints(ielem).ndims == ndimspoints for ielem in range(self.nelems)): + ndimspoints = None + else: + ndimspoints = None + return function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims, transforms=self.transforms, ndimspoints=ndimspoints), strictsample = types.strict[Sample] @@ -395,6 +401,8 @@ class UniformSample(Sample): Point set. ''' + __cache__ = 'tri', 'hull', 'subsamplemetas' + @types.apply_annotations def __init__(self, roots:types.tuple[function.strictroot], ndims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], points:points.strictpoints): assert len(transforms) >= 1 @@ -407,16 +415,32 @@ def getpoints(self, ielem): def getindex(self, ielem): return numpy.arange(ielem*self._points.npoints, (ielem+1)*self._points.npoints) + @property + def tri(self): + tri = self._points.tri + return types.frozenarray((numpy.arange(0, self.nelems*self._points.npoints, self._points.npoints)[:,None,None] + tri).reshape(-1,tri.shape[-1]), copy=False) + + @property + def hull(self): + hull = self._points.hull + return types.frozenarray((numpy.arange(0, self.nelems*self._points.npoints, self._points.npoints)[:,None,None] + hull).reshape(-1,hull.shape[-1]), copy=False) + + @property + def subsamplemetas(self): + return function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims, transforms=self.transforms, points=self._points, ndimspoints=self._points.ndims), + class ProductSample(Sample): + __cache__ = 'subsamplemetas', 'tri', 'hull' + @types.apply_annotations - def __init__(self, sample1:strictsample, sample2:strictsample): + def __init__(self, sample1:strictsample, sample2:strictsample, transforms:types.tuple[transformseq.stricttransforms]): self._sample1 = sample1 self._sample2 = sample2 super().__init__(sample1.roots+sample2.roots, sample1.ndims+sample2.ndims, sample1.npoints*sample2.npoints, - tuple(map(operator.mul, sample1.transforms, sample2.transforms))) + transforms) def getpoints(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) @@ -426,6 +450,25 @@ def getindex(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) return (self._sample1.getindex(ielem1)[:,numpy.newaxis]*self._sample2.npoints + self._sample2.getindex(ielem2)[numpy.newaxis,:]).ravel() + @property + def tri(self): + if self._sample1.ndims == 1: + tri12 = self._sample1.tri[:,None,:,None] * self._sample2.npoints + self._sample2.tri[None,:,None,:] # ntri1 x ntri2 x 2 x ndims + return types.frozenarray(numeric.overlapping(tri12.reshape(-1, 2*self.ndims), n=self.ndims+1).reshape(-1, self.ndims+1), copy=False) + return super().tri + + @property + def hull(self): + # NOTE: the order differs from `super().hull` + if self._sample1.ndims == 1: + hull1 = self._sample1.hull[:,None,:,None] * self._sample2.npoints + self._sample2.tri[None,:,None,:] # 2 x ntri2 x 1 x ndims + hull2 = self._sample1.tri[:,None,:,None] * self._sample2.npoints + self._sample2.hull[None,:,None,:] # ntri1 x nhull2 x 2 x ndims-1 + # The subdivision of hull2 into simplices follows identical logic to that + # used in the construction of self.tri. + hull = numpy.concatenate([hull1.reshape(-1, self.ndims), numeric.overlapping(hull2.reshape(-1, 2*(self.ndims-1)), n=self.ndims).reshape(-1, self.ndims)]) + return types.frozenarray(hull, copy=False) + return super().hull + def getsubsamples(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) return self._sample1.getsubsamples(ielem1) + self._sample2.getsubsamples(ielem2) @@ -434,6 +477,53 @@ def getsubsamples(self, ielem): def subsamplemetas(self): return self._sample1.subsamplemetas + self._sample2.subsamplemetas +class ChainedSample(Sample): + + __cache__ = 'tri', 'hull' + + @types.apply_annotations + def __init__(self, samples:types.tuple[strictsample], transforms:types.tuple[transformseq.stricttransforms]): + if not len(samples): + raise ValueError('cannot chain zero samples') + roots = samples[0].roots + ndims = samples[0].ndims + if not all(sample.roots == roots for sample in samples): + raise ValueError('all samples to be chained should have the same (order of) roots') + if not all(sample.ndims == ndims for sample in samples): + raise ValueError('all samples to be chained should have the same dimension') + todims = tuple(root.ndims for root in roots) + self._samples = samples + self._elemoffsets = numpy.cumsum([0, *(sample.nelems for sample in samples[:-1])]) + self._pointsoffsets = numpy.cumsum([0, *(sample.npoints for sample in samples[:-1])]) + super().__init__(roots, ndims, sum(sample.npoints for sample in samples), transforms) + + def _findelem(self, ielem): + if ielem < 0 or ielem >= self.nelems: + raise IndexError('element index out of range') + isample = numpy.searchsorted(self._elemoffsets[1:], ielem, side='right') + return isample, ielem - self._elemoffsets[isample] + + def getpoints(self, ielem): + isample, ielem = self._findelem(ielem) + return self._samples[isample].getpoints(ielem) + + def getindex(self, ielem): + isample, ielem = self._findelem(ielem) + return self._samples[isample].getindex(ielem) + self._pointsoffsets[isample] + + def integral(self, func): + return functools.reduce(operator.add, (sample.integral(func) for sample in self._samples)) + + @property + def tri(self): + offsets = util.cumsum(sample.npoints for sample in self._samples) + return types.frozenarray(numpy.concatenate([sample.tri+offset for sample, offset in zip(self._samples, offsets)], axis=0), copy=False) + + @property + def hull(self): + offsets = util.cumsum(sample.npoints for sample in self._samples) + return types.frozenarray(numpy.concatenate([sample.hull+offset for sample, offset in zip(self._samples, offsets)], axis=0), copy=False) + class Integral(types.Singleton): '''Postponed integration. diff --git a/nutils/topology.py b/nutils/topology.py index 89cb73a0c..1f3ec2c8b 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -521,7 +521,7 @@ def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, ma vref = element.getsimplex(0) ielems = parallel.shempty(len(coords), dtype=int) xis = parallel.shempty((len(coords),len(geom)), dtype=float) - J = function.dot(function.rootgradient(geom, self.roots)[:,:,_], function.RootBasis(0, self.roots, self.ndims, function.SelectChain(self.roots))[_,:,:self.ndims], 1) + J = function.dot(function.rootgradient(geom, self.roots)[:,:,_], function.RootBasis(0, self.roots, self.ndims)[_,:,:self.ndims], 1) geom_J = function.Tuple((geom, J)).prepare_eval().simplified with parallel.ctxrange('locating', len(coords)) as ipoints: for ipoint in ipoints: @@ -1775,6 +1775,12 @@ def boundary(self): def interfaces(self): return DisjointUnionTopology([topo.interfaces for topo in self._topos]) + def sample(self, ischeme, degree): + transforms = self.transforms, + if len(self.transforms) == 0 or self.opposites != self.transforms: + transforms += self.opposites, + return sample.ChainedSample(tuple(topo.sample(ischeme, degree) for topo in self._topos), transforms) + class SubsetTopology(Topology): 'trimmed' @@ -2372,7 +2378,10 @@ def refined(self): return self._left.refined.mul(self._right.refined, self._leftopp, self._rightopp) def sample(self, ischeme, degree): - return sample.ProductSample(self._left.sample(ischeme, degree), self._right.sample(ischeme, degree)) + transforms = self.transforms, + if len(self.transforms) == 0 or self.opposites != self.transforms: + transforms += self.opposites, + return sample.ProductSample(self._left.sample(ischeme, degree), self._right.sample(ischeme, degree), transforms) class RevolutionTopology(Topology): 'topology consisting of a single revolution element' diff --git a/nutils/transformseq.py b/nutils/transformseq.py index b2425874c..9fda09ca3 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -323,7 +323,15 @@ def linear_is_uniform(self): return False def linear(self, index): - return numeric.blockdiag(map(transform.linear, self[index], self.todims)) + ndims = sum(self.todims) + linears = numpy.zeros((ndims, ndims), float) + ismanifold = numpy.zeros(sum(self.todims), dtype=bool) + i = 0 + for chain, todims in zip(self[index], self.todims): + linears[i:i+todims,i:i+todims] = transform.linearfrom(chain, todims) + ismanifold[i:i+chain[-1].fromdims] = True + i += todims + return linears, ismanifold stricttransforms = types.strict[Transforms] @@ -460,7 +468,7 @@ def linear_is_uniform(self): return True def linear(self, ielem): - return numpy.eye(self._ndims) + return numpy.eye(self._ndims), numpy.ones(self._ndims, dtype=bool) class Axis(types.Singleton): '''Abstract base class for axes of :class:`~nutils.topology.StructuredTopology`.''' @@ -856,7 +864,13 @@ def linear_is_uniform(self): if not self._parent.linear_is_uniform or len(self._derived_transforms) == 0: return False linear = self._derived_transforms[0].linear - return all(numpy.allclose(linear, trans.linear) for trans in self._derived_transforms[1:]) + if not all(numpy.allclose(linear, trans.linear) for trans in self._derived_transforms[1:]): + return False + if self._derived_transforms[0].todims == self._derived_transforms[0].fromdims + 1: + ext = self._derived_transforms[0].ext + if not all(numpy.allclose(ext, trans.ext) for trans in self._derived_transforms[1:]): + return False + return True class TrimmedEdgesTransforms(Transforms): @@ -950,7 +964,9 @@ def linear_is_uniform(self): def linear(self, index): index1, index2 = divmod(numeric.normdim(len(self), index), len(self._transforms2)) - return numeric.blockdiag([self._transforms1.linear(index1), self._transforms2.linear(index2)]) + linear1, ismanifold1 = self._transforms1.linear(index1) + linear2, ismanifold2 = self._transforms2.linear(index2) + return numeric.blockdiag([linear1, linear2]), numpy.concatenate([ismanifold1, ismanifold2]) class ChainedTransforms(Transforms): '''A sequence of chained :class:`Transforms` objects. @@ -1032,8 +1048,8 @@ def unchain(self): def linear_is_uniform(self): if not all(item.linear_is_uniform for item in self._items): return False - linear = self._items[0].linear(0) - return all(numpy.allclose(item.linear(0), linear) for item in self._items[1:]) + linear, ismanifold = self._items[0].linear(0) + return all(numpy.allclose(item.linear(0)[0], linear) and numpy.equal(item.linear(0)[1], ismanifold).all() for item in self._items[1:]) def linear(self, index): index = numeric.normdim(len(self), index) From b2c312a59894bdab44f12008636b0f6334152a22 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sat, 21 Mar 2020 22:39:41 +0100 Subject: [PATCH 059/119] improve Constant.simplified invariant axes check --- nutils/function.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index e16ed4f0c..7b316036e 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -917,16 +917,15 @@ def simplified(self): if not self.value.any(): return zeros_like(self) # Find and replace invariant axes with InsertAxis. - value = self.value + value = numpy.asarray(self.value) invariant = [] for i in reversed(range(self.ndim)): # Since `self.value.any()` is False for arrays with a zero-length axis, - # we can arrive here only if all axes have at least length one, hence the - # following statement should work. - first = numeric.get(value, i, 0) - if all(numpy.equal(first, numeric.get(value, i, j)).all() for j in range(1, value.shape[i])): + # we can arrive here only if all axes have at least length one, hence + # `value[...,0,...]` should work. + if numpy.equal(value, value[(slice(None),)*i+(0,_)]).all(): invariant.append(i) - value = first + value = value[(slice(None),)*i+(0,)] if invariant: value = Constant(value) for i in reversed(invariant): From 05acfa9faa4ebeef47d71a07063695e6bd724ae8 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sun, 22 Mar 2020 13:33:16 +0100 Subject: [PATCH 060/119] implement intdata dot weights using function.* --- nutils/function.py | 76 ++++++++++++++++++++++++++++++++++++++++++++-- nutils/sample.py | 13 +++----- nutils/topology.py | 2 +- 3 files changed, 80 insertions(+), 11 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 7b316036e..47a73f3fb 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -765,8 +765,8 @@ def _asciitree_str(self): @property def optimized_for_numpy(self): if self.isconstant: - const, = self.eval() - return Constant(const) + const = self.eval() + return Constant(const[0]) if const.shape[0] == 1 else ConstantPoints(const) return super().optimized_for_numpy def _derivative(self, var, seen): @@ -1002,6 +1002,18 @@ def _determinant(self): # NOTE: numpy <= 1.12 cannot compute the determinant of an array with shape [...,0,0] return Constant(numpy.linalg.det(self.value) if self.value.shape[-1] else numpy.ones(self.value.shape[:-2])) +class ConstantPoints(Array): + + __slots__ = 'value' + + @types.apply_annotations + def __init__(self, value:types.frozenarray): + self.value = value + super().__init__(args=[], shape=value.shape[1:], dtype=value.dtype) + + def evalf(self): + return self.value + class InsertAxis(Array): __slots__ = 'func', 'axis', 'length' @@ -1367,6 +1379,38 @@ def _derivative(self, var, seen): raise Exception return zeros(self.shape+var.shape) + @util.positional_only + def prepare_eval(self, *, kwargs=...): + tail = self._tail.prepare_eval(**kwargs) + if 'subsamples' in kwargs and isinstance(tail, EmptyTransformChain): + subsamples = kwargs['subsamples'] + # TODO: Follow the same procedure if `tail` is `SelectChain` from `IdentifierTransforms` + slices = {} + isubsamples = {} + for isubsample, subsample in enumerate(subsamples): + if not self.roots.isdisjoint(subsample.roots) and subsample.points is None: + return super().prepare_eval(**kwargs) + + from0 = 0 + for root in subsample.roots: + isubsamples[root] = isubsample + from1 = from0 + root.ndims + slices[root] = slice(from0, from1) + from0 = from1 + + result = numpy.zeros((*(subsample.points.npoints for subsample in subsamples), self.shape[0]), dtype=float) + to0 = 0 + for root in tail.ordered_roots: + to1 = to0 + slices[root].stop - slices[root].start + isubsample = isubsamples[root] + expand = tuple(slice(None) if i == isubsample else numpy.newaxis for i in range(len(subsamples))) + result[...,to0:to1] = subsamples[isubsample].points.coords[:,slices[root]][expand] + to0 = to1 + assert to0 == self.shape[0] + return ConstantPoints(result.reshape((-1, self.shape[0]))) + + return super().prepare_eval(**kwargs) + class Linear(Array): __slots__ = '_roots', '_fromdims', '_transforms', '_ielem', '_itransforms' @@ -1404,6 +1448,34 @@ def prepare_eval(self, *, kwargs=...): itransforms = 1 if kwargs.get('opposite', False) and len(self._transforms) > 1 else 0 return Linear(self._roots, self._fromdims, self._transforms, ielem, itransforms) +class Weights(Array): + + def __init__(self): + super().__init__(args=[SUBSAMPLES], shape=(), dtype=float) + + def evalf(self, subsamples): + weights = numpy.ones((1,)) + for subsample in reversed(subsamples): + weights = weights[...,numpy.newaxis] * subsample.points.weights + return weights.ravel() + + @util.positional_only + def prepare_eval(self, *, subsamples, kwargs=...): + if all(subsample.points is not None for subsample in subsamples): + return ConstantPoints(self.evalf(subsamples)) + else: + return self + +class DotWeights(Array): + + @types.apply_annotations + def __init__(self, value:asarray, weights:asarray): + assert weights.ndim == 0 + super().__init__(args=[value, weights], shape=value.shape, dtype=float) + + def evalf(self, value, weights): + return numpy.einsum('a...,a->...', value, weights)[_] + class Inverse(Array): ''' Matrix inverse of ``func`` over the last two axes. All other axes are diff --git a/nutils/sample.py b/nutils/sample.py index 1e805e51d..e8dc5c9dc 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -158,7 +158,8 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. # argument id, evaluable index, and evaluable values. funcs = self._prepare_funcs(funcs) - blocks = [(ifunc, function.Tuple(ind), f.simplified.optimized_for_numpy) for ifunc, func in enumerate(funcs) for ind, f in function.blocks(func)] + weights = function.Weights().prepare_eval(subsamples=self.subsamplemetas) + blocks = [(ifunc, function.Tuple(ind), function.DotWeights(f, weights).simplified.optimized_for_numpy) for ifunc, func in enumerate(funcs) for ind, f in function.blocks(func)] block2func, indices, values = zip(*blocks) if blocks else ([],[],[]) log.debug('integrating {} distinct blocks'.format('+'.join( @@ -196,13 +197,9 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. with parallel.ctxrange('integrating', self.nelems) as ielems: for ielem in ielems: subsamples = self.getsubsamples(ielem) - weights = numpy.ones((1,)) - for subsample in reversed(subsamples): - weights = weights[...,numpy.newaxis] * subsample.points.weights - weights = weights.ravel() - for iblock, (intdata, *indices) in enumerate(valueindexfunc.eval(*subsamples, **arguments)): - data = datas[block2func[iblock]][offsets[iblock,ielem]:offsets[iblock,ielem+1]].reshape(intdata.shape[1:]) - numpy.einsum('p,p...->...', weights, intdata, out=data['value']) + for iblock, ((intdata,), *indices) in enumerate(valueindexfunc.eval(*subsamples, **arguments)): + data = datas[block2func[iblock]][offsets[iblock,ielem]:offsets[iblock,ielem+1]].reshape(intdata.shape) + data['value'] = intdata for idim, ii in enumerate(indices): data['index']['i'+str(idim)] = ii.reshape([-1]+[1]*(data.ndim-1-idim)) diff --git a/nutils/topology.py b/nutils/topology.py index 1f3ec2c8b..6cae09291 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -370,7 +370,6 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None if arguments is None: arguments = {} - levelset = levelset.prepare_eval().simplified refs = [] if leveltopo is None: verts = self.sample('vertex', maxrefine) @@ -378,6 +377,7 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None refs = [ref.trim(levels[verts.getindex(ielem)], maxrefine=maxrefine, ndivisions=ndivisions) for ielem, ref in enumerate(self.references)] else: log.info('collecting leveltopo elements') + levelset = levelset.prepare_eval(subsamples=(function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims),), transforms=(self.transforms, self.opposites)).simplified bins = [set() for ielem in range(len(self))] for trans in leveltopo.transforms: ielem, tail = self.transforms.index_with_tail(trans) From 260c059961025902db6ca432fa89556155ab4267 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sun, 22 Mar 2020 14:18:18 +0100 Subject: [PATCH 061/119] update --- nutils/function.py | 13 +++++-------- nutils/sample.py | 2 +- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 47a73f3fb..52272fb18 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -1380,16 +1380,14 @@ def _derivative(self, var, seen): return zeros(self.shape+var.shape) @util.positional_only - def prepare_eval(self, *, kwargs=...): - tail = self._tail.prepare_eval(**kwargs) - if 'subsamples' in kwargs and isinstance(tail, EmptyTransformChain): - subsamples = kwargs['subsamples'] - # TODO: Follow the same procedure if `tail` is `SelectChain` from `IdentifierTransforms` + def prepare_eval(self, *, subsamples, kwargs=...): + tail = self._tail.prepare_eval(subsamples=subsamples, **kwargs) + if isinstance(tail, EmptyTransformChain) or isinstance(tail, SelectChain) and any(subsample.roots == tail.ordered_roots and subsample.transforms == transformseq.IdentifierTransforms for subsample in subsamples): slices = {} isubsamples = {} for isubsample, subsample in enumerate(subsamples): if not self.roots.isdisjoint(subsample.roots) and subsample.points is None: - return super().prepare_eval(**kwargs) + return super().prepare_eval(subsamples=subsamples, **kwargs) from0 = 0 for root in subsample.roots: @@ -1408,8 +1406,7 @@ def prepare_eval(self, *, kwargs=...): to0 = to1 assert to0 == self.shape[0] return ConstantPoints(result.reshape((-1, self.shape[0]))) - - return super().prepare_eval(**kwargs) + return super().prepare_eval(subsamples=subsamples, **kwargs) class Linear(Array): diff --git a/nutils/sample.py b/nutils/sample.py index e8dc5c9dc..39120b3ef 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -146,7 +146,7 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. detJ = 1 for isubsample, subsample in enumerate(self.subsamplemetas): - J = function.RootBasis(isubsample, subsample.roots, subsample.ndimsmanifold)[:,:subsample.ndimsmanifold] + J = function.rootbasis(self.subsamplemetas, isubsample)[:,:subsample.ndimsmanifold] if J.shape[0] == J.shape[1]: detJ *= abs(function.determinant(J)) else: From be5a7fe34701c7c0890223e912d7c9234559e1f4 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 23 Mar 2020 16:41:56 +0100 Subject: [PATCH 062/119] update --- nutils/function.py | 186 +++++++++++++++++++-------------------------- nutils/sample.py | 50 ++++++++++-- nutils/topology.py | 7 +- 3 files changed, 124 insertions(+), 119 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 52272fb18..c52d1dca1 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -478,7 +478,7 @@ def evalf(self, subsamples): @util.positional_only def prepare_eval(self, *, opposite=False, kwargs=...): - return SelectChain(self.ordered_roots, 1-self.n) if opposite else self + return SelectChain(self.ordered_roots, 1) if opposite else SelectChain(self.ordered_roots, 0) class EmptyTransformChain(TransformChain): @@ -525,7 +525,7 @@ def tail(self): @property def linear(self): - return Linear(self.trans.ordered_roots, self.ndims, (self.transforms,), self.index)[:,:self.ndims] + return TransformsBasisFromSequence(self.trans.ordered_roots, self.transforms, self.index, self.ndims)[:,:self.ndims] def __iter__(self): yield self.index @@ -774,72 +774,6 @@ def _derivative(self, var, seen): return Zeros(self.shape + var.shape, dtype=self.dtype) raise NotImplementedError('derivative not defined for {}'.format(self.__class__.__name__)) -class RootBasis(Array): - '''Orthonormal vectors spanning the tangent space and the complement. - - The first ``ndimstangent`` vectors span the tangent space of the manifold, - the remainders span the complement. - - Parameters - ---------- - isubsample : :class:`int` - The index of the subsample to compute the basis for. - roots : :class:`tuple` of :class:`Root` objects - The roots to compute the basis for. - ndimstangent : int - The dimension of the tangent space. - ''' - - __slots__ = '_isubsample', '_roots', '_ndimstangent', '_opposite' - __cache__ = 'prepare_eval' - - @types.apply_annotations - def __init__(self, isubsample:types.strictint, roots:types.tuple[strictroot], ndimstangent:types.strictint, _opposite:bool=None): - self._isubsample = isubsample - self._roots = roots - self._ndimstangent = ndimstangent - self._opposite = _opposite - ndims = sum(root.ndims for root in roots) - super().__init__(args=[SUBSAMPLES], shape=[ndims, ndims], dtype=float) - - def evalf(self, subsamples): - subsample = subsamples[self._isubsample] - assert subsample.roots == self._roots - assert subsample.ndimsmanifold == self._ndimstangent - assert subsample.ndims == self.shape[0] - ndims = subsample.ndims - points = subsample.points - npointsbefore = functools.reduce(operator.mul, (s.points.npoints for s in subsamples[:self._isubsample]), 1) - npointsafter = functools.reduce(operator.mul, (s.points.npoints for s in subsamples[self._isubsample+1:]), 1) - linear = numpy.zeros((npointsbefore, points.npoints, npointsafter, ndims, ndims), dtype=float) - chainslinear = numpy.zeros((ndims, points.ndims), dtype=float) - to0 = from0 = 0 - n0 = points.ndims - for root, chain in zip(subsample.roots, subsample.transforms[1 if self._opposite else 0]): - to1 = to0 + root.ndims - fromdims = chain[-1].fromdims if chain else root.ndims - chainlinear = transform.linearfrom(chain, root.ndims) - if fromdims: - from1 = from0 + fromdims - chainslinear[to0:to1,from0:from1] = chainlinear[:,:fromdims] - from0 = from1 - if fromdims < root.ndims: - n1 = n0 + root.ndims - fromdims - linear[:,:,:,to0:to1,n0:n1] = chainlinear[_,_,_,:,fromdims:] - n0 = n1 - to0 = to1 - assert to0 == ndims - assert from0 == points.ndims - assert n0 == ndims - numpy.einsum('ij,bnajk->bnaik', chainslinear, points.basis[_,:,_], out=linear[:,:,:,:,:points.ndims]) - return linear.reshape((npointsbefore*points.npoints*npointsafter, ndims, ndims)) - - @util.positional_only - def prepare_eval(self, *, opposite=False, kwargs=...): - if self._opposite is not None: - raise ValueError('prepare already called') - return RootBasis(self._isubsample, self._roots, self._ndimstangent, _opposite=opposite) - class GramSchmidt(Array): __slots__ = '_arg' @@ -1380,14 +1314,18 @@ def _derivative(self, var, seen): return zeros(self.shape+var.shape) @util.positional_only - def prepare_eval(self, *, subsamples, kwargs=...): - tail = self._tail.prepare_eval(subsamples=subsamples, **kwargs) + def prepare_eval(self, *, kwargs=...): + tail = self._tail.prepare_eval(**kwargs) + if 'subsamples' not in kwargs: + return ApplyTransforms(tail, self._linear) + subsamples = kwargs['subsamples'] + if isinstance(tail, EmptyTransformChain) or isinstance(tail, SelectChain) and any(subsample.roots == tail.ordered_roots and subsample.transforms == transformseq.IdentifierTransforms for subsample in subsamples): slices = {} isubsamples = {} for isubsample, subsample in enumerate(subsamples): if not self.roots.isdisjoint(subsample.roots) and subsample.points is None: - return super().prepare_eval(subsamples=subsamples, **kwargs) + return ApplyTransforms(tail, self._linear) from0 = 0 for root in subsample.roots: @@ -1406,20 +1344,43 @@ def prepare_eval(self, *, subsamples, kwargs=...): to0 = to1 assert to0 == self.shape[0] return ConstantPoints(result.reshape((-1, self.shape[0]))) - return super().prepare_eval(subsamples=subsamples, **kwargs) -class Linear(Array): + return ApplyTransforms(tail, self._linear) - __slots__ = '_roots', '_fromdims', '_transforms', '_ielem', '_itransforms' +class TransformsBasisFromChains(Array): + + __slots__ = '_todims', '_fromdims' + + @types.apply_annotations + def __init__(self, chains:types.strict[TransformChain], fromdims:types.strictint=None): + self._todims = tuple(root.ndims for root in chains.ordered_roots) + self._fromdims = fromdims + super().__init__(args=[chains], shape=(builtins.sum(root.ndims for root in chains.roots),)*2, dtype=float) + + def evalf(self, chains): + ndims = self.shape[0] + linear = numpy.zeros((ndims, ndims), float) + ismanifold = numpy.zeros(ndims, dtype=bool) + i = 0 + for chain, todims in zip(chains, self._todims): + linear[i:i+todims,i:i+todims] = transform.linearfrom(chain, todims) + ismanifold[i:i+chain[-1].fromdims] = True + i += todims + if self._fromdims is not None: + assert ismanifold.sum() == self._fromdims + return numpy.concatenate([linear[:,ismanifold], linear[:,~ismanifold]], axis=1)[_] + +class TransformsBasisFromSequence(Array): + + __slots__ = '_roots', '_transforms', '_ielem', '_fromdims' __cache__ = 'simplified' @types.apply_annotations - def __init__(self, roots:types.tuple[strictroot], fromdims:types.strictint, transforms:types.tuple[transformseq.stricttransforms], ielem:asarray, _itransforms:types.strictint=None): + def __init__(self, roots:types.tuple[strictroot], transforms:transformseq.stricttransforms, ielem:asarray, fromdims:types.strictint=None): self._roots = roots - self._fromdims = fromdims self._transforms = transforms self._ielem = ielem - self._itransforms = _itransforms + self._fromdims = fromdims super().__init__(args=[ielem], shape=(builtins.sum(root.ndims for root in roots),)*2, dtype=float) @property @@ -1428,24 +1389,37 @@ def roots(self): def evalf(self, ielem): ielem, = ielem - linear, ismanifold = self._transforms[self._itransforms].linear(ielem) - assert ismanifold.sum() == self._fromdims + linear, ismanifold = self._transforms.linear(ielem) + if self._fromdims is not None: + assert ismanifold.sum() == self._fromdims return numpy.concatenate([linear[:,ismanifold], linear[:,~ismanifold]], axis=1)[_] @property def simplified(self): - ielem = 0 if self._itransforms is not None and self._transforms[self._itransforms].linear_is_uniform else self._ielem.simplified - return Linear(self._roots, self._fromdims, self._transforms, ielem, self._itransforms) + ielem = 0 if self._transforms.linear_is_uniform else self._ielem.simplified + return TransformsBasisFromSequence(self._roots, self._transforms, ielem, self._fromdims) - @util.positional_only - def prepare_eval(self, *, kwargs=...): - ielem = self._ielem.prepare_eval(**kwargs) - if self._itransforms is not None: - raise RuntimeError('prepare_eval called twice') - itransforms = 1 if kwargs.get('opposite', False) and len(self._transforms) > 1 else 0 - return Linear(self._roots, self._fromdims, self._transforms, ielem, itransforms) +class PointsBasis(Array): -class Weights(Array): + __slots__ = '_isubsample', '_roots', '_ndims' + + @types.apply_annotations + def __init__(self, isubsample:types.strictint, roots:types.tuple[strictroot], ndims:types.strictint): + self._isubsample = isubsample + self._roots = roots + self._ndims = ndims + super().__init__(args=[SUBSAMPLES], shape=[ndims, ndims], dtype=float) + + def evalf(self, subsamples): + points = subsamples[self._isubsample].points + npointsbefore = functools.reduce(operator.mul, (s.points.npoints for s in subsamples[:self._isubsample]), 1) + npointsafter = functools.reduce(operator.mul, (s.points.npoints for s in subsamples[self._isubsample+1:]), 1) + basis = numpy.zeros((npointsbefore, points.npoints, npointsafter, self._ndims, self._ndims), dtype=float) + basis[:,:,:,:points.ndims,:points.ndims] = points.basis[_,:,_] + basis[:,:,:,points.ndims:,points.ndims:] = numpy.eye(self._ndims-points.ndims)[_] + return basis.reshape((npointsbefore*points.npoints*npointsafter, self._ndims, self._ndims)) + +class PointsWeights(Array): def __init__(self): super().__init__(args=[SUBSAMPLES], shape=(), dtype=float) @@ -3200,7 +3174,7 @@ def prepare_eval(self, *, subsamples, kwargs=...): ndimsmanifold += subsample.ndimsmanifold if subsample.ndims == 0: continue - J.append(dot(rootgradient(self._geom, subsample.roots)[:,:,_], rootbasis(subsamples, isubsample, orthonormal=True)[_:,:subsample.ndimsmanifold], 1)) + J.append(dot(rootgradient(self._geom, subsample.roots)[:,:,_], rootbasis(subsamples, isubsample, orthonormal=True, opposite=kwargs.get('opposite', False))[_:,:subsample.ndimsmanifold], 1)) if self._ndimsmanifold is not None and ndimsmanifold != self._ndimsmanifold: raise ValueError('jacobian will be evaluated on a manifold of dimension {} but {} was requested'.format(ndimsmanifold, self._ndimsmanifold)) J = concatenate(J, axis=1) @@ -3245,7 +3219,7 @@ def prepare_eval(self, *, subsamples, kwargs=...): if not frozenset(subsample.roots) <= self.roots: raise ValueError('Cannot compute normal.') ndimsnormal += subsample.ndimsnormal - basis = rootbasis(subsamples, isubsample) + basis = rootbasis(subsamples, isubsample, opposite=kwargs.get('opposite', False)) grad = dot(rootgradient(self._geom, subsample.roots)[:,:,_], basis[_,:,:], 1) if subsample.ndimsmanifold: tangents.append(grad[:,:subsample.ndimsmanifold]) @@ -3254,7 +3228,7 @@ def prepare_eval(self, *, subsamples, kwargs=...): if ndimsnormal == 0: raise ValueError('cannot compute normal: the normal space has dimension zero') elif ndimsnormal > 1: - raise ValueError('cannot compute normal: the normal space has dimension larger then one') + warnings.warn('cannot umambiguously compute the normal: the normal space has dimension larger then one') n = Normal(concatenate(tangents+normals, axis=1)[:,:len(self._geom)]) n = functools.reduce(derivative, self._derivativestack, asarray(n)) return n.prepare_eval(subsamples=subsamples, **kwargs) @@ -4783,25 +4757,19 @@ def derivative(func, var, seen=None): def rootgradient(arg, roots): return concatenate([derivative(arg, RootCoords(root)) for root in roots], axis=-1) -def rootbasis(subsamples, index_or_roots, *, orthonormal=False): - subsamples = tuple(subsamples) - if isinstance(index_or_roots, int): - isubsample = index_or_roots - subsample = subsamples[isubsample] - elif isinstance(index_or_roots, tuple): - roots = index_or_roots - for isubsample, subsample in enumerate(subsamples): - if subsample.roots == roots: - break - else: - raise ValueError('no subsample with roots {}, candidates: {}'.format(roots, ', '.join(str(subsample.roots) for subsample in subsamples))) +def rootbasis(subsamples, isubsample, *, orthonormal=False, opposite=False): + subsample = subsamples[isubsample] + if subsample.transforms is None: + transformsbasis = TransformsBasisFromChains(SelectChain(subsample.roots, n=1 if opposite else 0), subsample.ndimspoints) else: - raise ValueError('expected an `int` or `tuple` of `Root` objects but got {!r}'.format(index_or_roots)) - if subsample.ndimspoints == subsample.ndimsmanifold: - # `points.basis` is the identity. - basis = Linear(subsample.roots, subsample.ndimspoints, subsample.transforms, IndexFromSubsample(isubsample, subsample.roots)) + transformsbasis = TransformsBasisFromSequence(subsample.roots, subsample.transforms[1 if opposite and len(subsample.transforms) > 1 else 0], IndexFromSubsample(isubsample, subsample.roots), subsample.ndimspoints) + if subsample.ndimsmanifold == subsample.ndimspoints: + basis = transformsbasis else: - basis = RootBasis(isubsample, subsample.roots, subsample.ndimsmanifold) + pointsbasis = PointsBasis(isubsample, subsample.roots, subsample.ndims) + if subsample.points is not None: + pointsbasis = ConstantPoints(pointsbasis.evalf(subsamples)) + basis = (transformsbasis[:,:,_]*pointsbasis[_]).sum(axis=1) if orthonormal: if subsample.ndimsnormal == 0: return eye(subsample.ndims) diff --git a/nutils/sample.py b/nutils/sample.py index 39120b3ef..9abd51151 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -44,7 +44,7 @@ class which represents postponed integration. Integrals are internally ''' from . import types, points, util, function, parallel, numeric, matrix, transformseq, sparse, warnings -import numpy, numbers, collections.abc, os, treelog as log, operator, functools +import numpy, numbers, collections.abc, os, treelog as log, operator, functools, itertools graphviz = os.environ.get('NUTILS_GRAPHVIZ') @@ -110,6 +110,14 @@ def points(self): warnings.deprecation('`Sample.points` is deprecated; replace `Sample.points[ielem]` with `Sample.getpoints(ielem)`') return tuple(self.getpoints(ielem) for ielem in range(self.nelems)) + @property + def indexiter(self): + return map(self.getindex, range(self.nelems)) + + @property + def pointsiter(self): + return map(self.getpoints, range(self.nelems)) + @util.positional_only @util.single_or_multiple @types.apply_annotations @@ -158,7 +166,7 @@ def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types. # argument id, evaluable index, and evaluable values. funcs = self._prepare_funcs(funcs) - weights = function.Weights().prepare_eval(subsamples=self.subsamplemetas) + weights = function.PointsWeights().prepare_eval(subsamples=self.subsamplemetas) blocks = [(ifunc, function.Tuple(ind), function.DotWeights(f, weights).simplified.optimized_for_numpy) for ifunc, func in enumerate(funcs) for ind, f in function.blocks(func)] block2func, indices, values = zip(*blocks) if blocks else ([],[],[]) @@ -247,8 +255,8 @@ def eval(self, funcs, arguments:argdict=...): @property def allcoords(self): coords = numpy.empty([self.npoints, self.ndims]) - for ielem in range(self.nelems): - coords[self.getindex(ielem)] = self.getpoints(ielem).coords + for index, points in zip(self.indexiter, self.pointsiter): + coords[index] = points.coords return types.frozenarray(coords, copy=False) def basis(self): @@ -291,7 +299,7 @@ def tri(self): row defines a simplex by mapping vertices into the list of points. ''' - return types.frozenarray(numpy.concatenate([self.getindex(ielem).take(self.getpoints(ielem).tri) for ielem in range(self.nelems)]), copy=False) + return types.frozenarray(numpy.concatenate([index.take(points.tri) for index, points in zip(self.indexiter, self.pointsiter)]), copy=False) @property def hull(self): @@ -303,7 +311,7 @@ def hull(self): triangulations originating from separate elements are disconnected. ''' - return types.frozenarray(numpy.concatenate([self.getindex(ielem).take(self.getpoints(ielem).hull) for ielem in range(self.nelems)]), copy=False) + return types.frozenarray(numpy.concatenate([index.take(points.hull) for index, points in zip(self.indexiter, self.pointsiter)]), copy=False) def subset(self, mask): '''Reduce the number of points. @@ -324,7 +332,7 @@ def subset(self, mask): subset : :class:`Sample` ''' - selection = types.frozenarray([ielem for ielem in range(self.nelems) if mask[self.getindex(ielem)].any()]) + selection = types.frozenarray([ielem for ielem, index in enumerate(self.indexiter) if mask[index].any()]) transforms = tuple(transform[selection] for transform in self.transforms) points = [self.getpoints(ielem) for ielem in selection] offset = numpy.cumsum([0] + [p.npoints for p in points]) @@ -381,6 +389,14 @@ def getpoints(self, ielem): def getindex(self, ielem): return self._index[ielem] + @property + def pointsiter(self): + return iter(self._points) + + @property + def indexiter(self): + return iter(self._index) + class UniformSample(Sample): '''A sample with uniform points. @@ -412,6 +428,10 @@ def getpoints(self, ielem): def getindex(self, ielem): return numpy.arange(ielem*self._points.npoints, (ielem+1)*self._points.npoints) + @property + def pointsiter(self): + return itertools.repeat(self._points, self.nelems) + @property def tri(self): tri = self._points.tri @@ -447,6 +467,14 @@ def getindex(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) return (self._sample1.getindex(ielem1)[:,numpy.newaxis]*self._sample2.npoints + self._sample2.getindex(ielem2)[numpy.newaxis,:]).ravel() + @property + def pointsiter(self): + return (points.TensorPoints(points1, points2) for points1 in self._sample1.pointsiter for points2 in self._sample2.pointsiter) + + @property + def indexiter(self): + return ((index1[:,numpy.newaxis]*self._sample2.npoints + index2[numpy.newaxis,:]).ravel() for index1 in self._sample1.indexiter for index2 in self._sample2.indexiter) + @property def tri(self): if self._sample1.ndims == 1: @@ -508,6 +536,14 @@ def getindex(self, ielem): isample, ielem = self._findelem(ielem) return self._samples[isample].getindex(ielem) + self._pointsoffsets[isample] + @property + def pointsiter(self): + return itertools.chain.fromiterable(sample.pointsiter for sample in self._samples) + + @property + def indexiter(self): + return (index+offset for sample, offset in zip(self._samples, self._pointsoffsets) for index in sample.indexiter) + def integral(self, func): return functools.reduce(operator.add, (sample.integral(func) for sample in self._samples)) diff --git a/nutils/topology.py b/nutils/topology.py index 6cae09291..7dab960e9 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -515,14 +515,15 @@ def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, ma if not geom.shape == coords.shape[1:] == (self.ndims,): raise Exception('invalid geometry or point shape for {}D topology'.format(self.ndims)) bboxsample = self.sample(*element.parse_legacy_ischeme(ischeme)) - vertices = map(bboxsample.eval(geom, **arguments or {}).__getitem__, bboxsample.index) + vertices = map(bboxsample.eval(geom, **arguments or {}).__getitem__, bboxsample.indexiter) bboxes = numpy.array([numpy.mean(v,axis=0) * (1-scale) + numpy.array([numpy.min(v,axis=0), numpy.max(v,axis=0)]) * scale for v in vertices]) # nelems x {min,max} x ndims vref = element.getsimplex(0) ielems = parallel.shempty(len(coords), dtype=int) xis = parallel.shempty((len(coords),len(geom)), dtype=float) - J = function.dot(function.rootgradient(geom, self.roots)[:,:,_], function.RootBasis(0, self.roots, self.ndims)[_,:,:self.ndims], 1) - geom_J = function.Tuple((geom, J)).prepare_eval().simplified + subsamplemetas = function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims, ndimspoints=self.ndims), + J = function.dot(function.rootgradient(geom, self.roots)[:,:,_], function.rootbasis(subsamplemetas, 0)[_,:,:self.ndims], 1) + geom_J = function.Tuple((geom, J)).prepare_eval(subsamples=subsamplemetas).simplified with parallel.ctxrange('locating', len(coords)) as ipoints: for ipoint in ipoints: coord = coords[ipoint] From a769469f655beadc8c64c57c3488e802cab97bf3 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 23 Mar 2020 21:50:32 +0100 Subject: [PATCH 063/119] rename Transforms.linear to Transforms.basis --- nutils/function.py | 12 ++++---- nutils/transformseq.py | 63 +++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index c52d1dca1..b5ab1c48e 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -1359,16 +1359,16 @@ def __init__(self, chains:types.strict[TransformChain], fromdims:types.strictint def evalf(self, chains): ndims = self.shape[0] - linear = numpy.zeros((ndims, ndims), float) + basis = numpy.zeros((ndims, ndims), float) ismanifold = numpy.zeros(ndims, dtype=bool) i = 0 for chain, todims in zip(chains, self._todims): - linear[i:i+todims,i:i+todims] = transform.linearfrom(chain, todims) + basis[i:i+todims,i:i+todims] = transform.linearfrom(chain, todims) ismanifold[i:i+chain[-1].fromdims] = True i += todims if self._fromdims is not None: assert ismanifold.sum() == self._fromdims - return numpy.concatenate([linear[:,ismanifold], linear[:,~ismanifold]], axis=1)[_] + return numpy.concatenate([basis[:,ismanifold], basis[:,~ismanifold]], axis=1)[_] class TransformsBasisFromSequence(Array): @@ -1389,14 +1389,14 @@ def roots(self): def evalf(self, ielem): ielem, = ielem - linear, ismanifold = self._transforms.linear(ielem) + basis, ismanifold = self._transforms.basis(ielem) if self._fromdims is not None: assert ismanifold.sum() == self._fromdims - return numpy.concatenate([linear[:,ismanifold], linear[:,~ismanifold]], axis=1)[_] + return numpy.concatenate([basis[:,ismanifold], basis[:,~ismanifold]], axis=1)[_] @property def simplified(self): - ielem = 0 if self._transforms.linear_is_uniform else self._ielem.simplified + ielem = 0 if self._transforms.basis_is_uniform else self._ielem.simplified return TransformsBasisFromSequence(self._roots, self._transforms, ielem, self._fromdims) class PointsBasis(Array): diff --git a/nutils/transformseq.py b/nutils/transformseq.py index 9fda09ca3..9a90ed40b 100644 --- a/nutils/transformseq.py +++ b/nutils/transformseq.py @@ -319,19 +319,19 @@ def unchain(self): yield self @property - def linear_is_uniform(self): + def basis_is_uniform(self): return False - def linear(self, index): + def basis(self, index): ndims = sum(self.todims) - linears = numpy.zeros((ndims, ndims), float) + basis = numpy.zeros((ndims, ndims), float) ismanifold = numpy.zeros(sum(self.todims), dtype=bool) i = 0 for chain, todims in zip(self[index], self.todims): - linears[i:i+todims,i:i+todims] = transform.linearfrom(chain, todims) + basis[i:i+todims,i:i+todims] = transform.linearfrom(chain, todims) ismanifold[i:i+chain[-1].fromdims] = True i += todims - return linears, ismanifold + return basis, ismanifold stricttransforms = types.strict[Transforms] @@ -464,10 +464,10 @@ def index_with_tail(self, mtrans): raise ValueError @property - def linear_is_uniform(self): + def basis_is_uniform(self): return True - def linear(self, ielem): + def basis(self, ielem): return numpy.eye(self._ndims), numpy.ones(self._ndims, dtype=bool) class Axis(types.Singleton): @@ -686,11 +686,11 @@ def index_with_tail(self, trans): return int(index), tail @property - def linear_is_uniform(self): - return self._parent.linear_is_uniform + def basis_is_uniform(self): + return self._parent.basis_is_uniform - def linear(self, index): - return self._parent.linear(self._indices[index]) + def basis(self, index): + return self._parent.basis(self._indices[index]) class ReorderedTransforms(Transforms): '''A reordered :class:`Transforms` object. @@ -733,11 +733,11 @@ def index_with_tail(self, trans): return int(self._rindices[parent_index]), tail @property - def linear_is_uniform(self): - return self._parent.linear_is_uniform + def basis_is_uniform(self): + return self._parent.basis_is_uniform - def linear(self, index): - return self._parent.linear(self._indices[index]) + def basis(self, index): + return self._parent.basis(self._indices[index]) class DerivedTransforms(Transforms): '''A sequence of derived transforms. @@ -826,7 +826,7 @@ class UniformDerivedTransforms(Transforms): ''' __slots__ = '_parent', '_derived_transforms', '_updim' - __cache__ = 'linear_is_uniform' + __cache__ = 'basis_is_uniform' @types.apply_annotations def __init__(self, parent:stricttransforms, parent_reference:element.strictreference, derived_attribute:types.strictstr, updim:types.strict[bool]): @@ -860,8 +860,8 @@ def index_with_tail(self, trans): return iparent*len(self._derived_transforms) + iderived, tail @property - def linear_is_uniform(self): - if not self._parent.linear_is_uniform or len(self._derived_transforms) == 0: + def basis_is_uniform(self): + if not self._parent.basis_is_uniform or len(self._derived_transforms) == 0: return False linear = self._derived_transforms[0].linear if not all(numpy.allclose(linear, trans.linear) for trans in self._derived_transforms[1:]): @@ -932,6 +932,7 @@ class ProductTransforms(Transforms): ''' __slots__ = '_transforms1', '_transforms2' + __cache__ = 'basis_is_uniform' @types.apply_annotations def __init__(self, transforms1:stricttransforms, transforms2:stricttransforms): @@ -959,14 +960,14 @@ def index_with_tail(self, trans): return index1*len(self._transforms2)+index2, tail1+tail2 @property - def linear_is_uniform(self): - return self._transforms1.linear_is_uniform and self._transforms2.linear_is_uniform + def basis_is_uniform(self): + return self._transforms1.basis_is_uniform and self._transforms2.basis_is_uniform - def linear(self, index): + def basis(self, index): index1, index2 = divmod(numeric.normdim(len(self), index), len(self._transforms2)) - linear1, ismanifold1 = self._transforms1.linear(index1) - linear2, ismanifold2 = self._transforms2.linear(index2) - return numeric.blockdiag([linear1, linear2]), numpy.concatenate([ismanifold1, ismanifold2]) + basis1, ismanifold1 = self._transforms1.basis(index1) + basis2, ismanifold2 = self._transforms2.basis(index2) + return numeric.blockdiag([basis1, basis2]), numpy.concatenate([ismanifold1, ismanifold2]) class ChainedTransforms(Transforms): '''A sequence of chained :class:`Transforms` objects. @@ -978,7 +979,7 @@ class ChainedTransforms(Transforms): ''' __slots__ = '_items' - __cache__ = '_offsets' + __cache__ = '_offsets', 'basis_is_uniform' @types.apply_annotations def __init__(self, items:types.tuple[stricttransforms]): @@ -1045,17 +1046,17 @@ def unchain(self): yield from self._items @property - def linear_is_uniform(self): - if not all(item.linear_is_uniform for item in self._items): + def basis_is_uniform(self): + if not all(item.basis_is_uniform for item in self._items): return False - linear, ismanifold = self._items[0].linear(0) - return all(numpy.allclose(item.linear(0)[0], linear) and numpy.equal(item.linear(0)[1], ismanifold).all() for item in self._items[1:]) + basis, ismanifold = self._items[0].basis(0) + return all(numpy.allclose(item.basis(0)[0], basis) and numpy.equal(item.basis(0)[1], ismanifold).all() for item in self._items[1:]) - def linear(self, index): + def basis(self, index): index = numeric.normdim(len(self), index) outer = numpy.searchsorted(self._offsets, index, side='right') - 1 assert outer >= 0 and outer < len(self._items) - return self._items[outer].linear(index-self._offsets[outer]) + return self._items[outer].basis(index-self._offsets[outer]) @types.apply_annotations def chain(items:types.tuple[stricttransforms], todims:types.tuple[types.strictint]): From 2ed163211d3f97000ee7ce0b48b40769a7d9fb81 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 23 Mar 2020 21:55:26 +0100 Subject: [PATCH 064/119] update --- nutils/mesh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nutils/mesh.py b/nutils/mesh.py index 5d7a858ca..8e60a1fe4 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -44,7 +44,7 @@ def line(nodes, periodic=False, bnames=None, *, rootid='line'): offset = nodes[0] uniform = numpy.equal(nodes, offset + numpy.arange(nelems+1) * scale).all() root = function.Root(rootid, 1) - transforms = transformseq.IdentifierTransforms(1, rootid, nelems) + transforms = transformseq.IdentifierTransforms(1, 'line', nelems) domain = topology.StructuredLine(root, transforms, periodic, bnames) if uniform: ielem = function.TransformsIndexWithTail(domain.transforms, 1, function.SelectChain((root,))).index From 14a926392859d4274cfca2aa248319db1eef2e8e Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 23 Mar 2020 22:02:40 +0100 Subject: [PATCH 065/119] fixes --- nutils/element.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nutils/element.py b/nutils/element.py index 95f47e488..89d2480ef 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -254,7 +254,8 @@ def check_edges(self, tol=1e-15, print=print): if PJ.shape[2] < self.ndims: assert PJ.shape[2] == points.ndims J[:,:,PJ.shape[2]:] = TJ[_,:,points.ndims:self.ndims] - detJ = abs(numpy.linalg.det(J[:,:,:self.ndims-1].transpose(0,2,1) @ J[:,:,:self.ndims-1]))**0.5 + JTJ = numpy.einsum('nij,nik->njk', J[:,:,:self.ndims-1], J[:,:,:self.ndims-1]) + detJ = abs(numpy.linalg.det(JTJ)**0.5) if JTJ.size else 1 numeric.gramschmidt(J) n = J[:,:,-1] w_normal = (points.weights * detJ)[:,_] * n From 678214be429754738fe30bc29cea9b6317b56e9c Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 23 Mar 2020 21:55:31 +0100 Subject: [PATCH 066/119] implement revolved --- nutils/function.py | 41 ++++++++++++++++++++++++++++++++++------- nutils/topology.py | 15 ++++++++------- tests/test_topology.py | 15 +++++---------- 3 files changed, 47 insertions(+), 24 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index b5ab1c48e..e2f06a5a5 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -87,6 +87,12 @@ def __repr__(self): strictroot = types.strict[Root] +class RevolutionRoot(Root): + + @types.apply_annotations + def __init__(self, name:types.strictstr): + super().__init__(name, 1) + class Subsample: '''Subsample @@ -3166,15 +3172,23 @@ def _derivative(self, var, seen): def prepare_eval(self, *, subsamples, kwargs=...): ndimsmanifold = 0 J = [] + roots = set(self.roots) for isubsample, subsample in enumerate(subsamples): - if self.roots.isdisjoint(subsample.roots): + if roots.isdisjoint(subsample.roots): continue - if not frozenset(subsample.roots) <= self.roots: + if not frozenset(subsample.roots) <= roots: raise ValueError('Cannot compute jacobian.') + roots -= set(subsample.roots) ndimsmanifold += subsample.ndimsmanifold if subsample.ndims == 0: continue J.append(dot(rootgradient(self._geom, subsample.roots)[:,:,_], rootbasis(subsamples, isubsample, orthonormal=True, opposite=kwargs.get('opposite', False))[_:,:subsample.ndimsmanifold], 1)) + for root in tuple(roots): + if isinstance(root, RevolutionRoot): + roots.remove(root) + J.append(rootgradient(self._geom, (root,))*(2*numpy.pi)) + if roots: + raise ValueError('extra roots: {}'.format(roots)) if self._ndimsmanifold is not None and ndimsmanifold != self._ndimsmanifold: raise ValueError('jacobian will be evaluated on a manifold of dimension {} but {} was requested'.format(ndimsmanifold, self._ndimsmanifold)) J = concatenate(J, axis=1) @@ -3213,11 +3227,13 @@ def prepare_eval(self, *, subsamples, kwargs=...): tangents = [] normals = [] ndimsnormal = 0 + roots = set(self.roots) for isubsample, subsample in enumerate(subsamples): - if self.roots.isdisjoint(subsample.roots): + if roots.isdisjoint(subsample.roots): continue - if not frozenset(subsample.roots) <= self.roots: + if not frozenset(subsample.roots) <= roots: raise ValueError('Cannot compute normal.') + roots -= set(subsample.roots) ndimsnormal += subsample.ndimsnormal basis = rootbasis(subsamples, isubsample, opposite=kwargs.get('opposite', False)) grad = dot(rootgradient(self._geom, subsample.roots)[:,:,_], basis[_,:,:], 1) @@ -3225,6 +3241,12 @@ def prepare_eval(self, *, subsamples, kwargs=...): tangents.append(grad[:,:subsample.ndimsmanifold]) if subsample.ndimsnormal: normals.append(grad[:,subsample.ndimsmanifold:]) + for root in tuple(roots): + if isinstance(root, RevolutionRoot): + roots.remove(root) + tangents.append(rootgradient(self._geom, (root,))) + if roots: + raise ValueError('extra roots: {}'.format(roots)) if ndimsnormal == 0: raise ValueError('cannot compute normal: the normal space has dimension zero') elif ndimsnormal > 1: @@ -3605,12 +3627,17 @@ class RevolutionAngle(Array): Pseudo coordinates of a :class:`nutils.topology.RevolutionTopology`. ''' - __slots__ = () + __slots__ = '_root' __cache__ = 'prepare_eval' - def __init__(self): + def __init__(self, root): + self._root = root super().__init__(args=[], shape=[], dtype=float) + @property + def roots(self): + return frozenset((self._root,)) + @property def isconstant(self): return False @@ -3619,7 +3646,7 @@ def evalf(self): raise Exception('RevolutionAngle should not be evaluated') def _derivative(self, var, seen): - return (ones_like if isinstance(var, LocalCoords) and len(var) > 0 else zeros_like)(var) + return (ones_like if isinstance(var, RootCoords) and var.root == self._root else zeros_like)(var) @util.positional_only def prepare_eval(self, kwargs=...): diff --git a/nutils/topology.py b/nutils/topology.py index 7dab960e9..ebcdb5a17 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -566,14 +566,15 @@ def _sample(self, ielems, coords): transforms += self.opposites[uielems], return sample.PlainSample(self.roots, self.ndims, transforms, points_, index) - def revolved(self, geom): + def revolved_geometry(self, geom, *, name='rev'): assert geom.ndim == 1 - revdomain = self * RevolutionTopology() - angle = function.RevolutionAngle() - geom, angle = function.bifurcate(geom, angle) - revgeom = function.concatenate([geom[0] * function.trignormal(angle), geom[1:]]) - simplify = _identity - return revdomain, revgeom, simplify + revroot = function.RevolutionRoot(name) + angle = function.RevolutionAngle(revroot) + return function.concatenate([geom[0] * function.trignormal(angle), geom[1:]]) + + def revolved(self, geom): + warnings.deprecation('`Topology.revolved` is deprecated; use Topology.revolved_geometry instead') + return self, self.revolved_geometry(geom), _identity def extruded(self, geom, nelems, periodic=False, bnames=('front','back')): assert geom.ndim == 1 diff --git a/tests/test_topology.py b/tests/test_topology.py index 6766de40a..14c3a9acc 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -164,35 +164,30 @@ def test_bnd(self): self.assertAlmostEqual(iface.integrate(function.J(geom), degree=1), 3) @parametrize -@unittest.skip('in between bifurcate and tensorial') class revolved(TestCase): def setUp(self): super().setUp() if self.domtype == 'circle': - self.domain0, self.geom0 = mesh.rectilinear([2]) + self.domain, self.geom0 = mesh.rectilinear([2]) self.exact_volume = 4 * numpy.pi self.exact_surface = 4 * numpy.pi self.exact_groups = {} elif self.domtype == 'cylinder': - self.domain0, self.geom0 = mesh.rectilinear([1,2]) + self.domain, self.geom0 = mesh.rectilinear([1,2]) self.exact_volume = 2 * numpy.pi self.exact_surface = 6 * numpy.pi self.exact_groups = dict(right=4*numpy.pi, left=0) elif self.domtype == 'hollowcylinder': - self.domain0, self.geom0 = mesh.rectilinear([[.5,1],2]) + self.domain, self.geom0 = mesh.rectilinear([[.5,1],2]) self.exact_volume = 1.5 * numpy.pi self.exact_surface = 7.5 * numpy.pi self.exact_groups = dict(right=4*numpy.pi, left=2*numpy.pi) else: raise Exception('unknown domain type {!r}'.format(self.domtype)) - self.domain, self.geom, self.simplify = self.domain0.revolved(self.geom0) + self.geom = self.domain.revolved_geometry(self.geom0) if self.refined: self.domain = self.domain.refined - self.domain0 = self.domain0.refined - - def test_revolved(self): - self.assertEqual(len(self.domain), len(self.domain0)) def test_volume(self): vol = self.domain.integrate(function.J(self.geom), ischeme='gauss1') @@ -202,7 +197,7 @@ def test_volume_bydiv(self): boundary = self.domain.boundary if self.domtype != 'hollowcylinder': boundary = boundary['bottom,right,top'] - v = boundary.integrate(self.geom.dotnorm(self.geom)*function.J(self.geom), ischeme='gauss1') / self.domain.ndims + v = boundary.integrate(self.geom.dotnorm(self.geom)*function.J(self.geom), ischeme='gauss1') / (self.domain.ndims+1) numpy.testing.assert_array_almost_equal(v, self.exact_volume) def test_surface(self): From 7df268b7c07ad1de41a52c4d9032354ea49ec913 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 24 Mar 2020 21:09:13 +0100 Subject: [PATCH 067/119] update --- nutils/function.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index e2f06a5a5..0e3b88dcb 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -310,7 +310,7 @@ def graphviz(self, dotpath='dot', imgtype='png'): lines = [] lines.append('digraph {') lines.append('graph [dpi=72];') - lines.extend('{0:} [label="{0:}. {1:}"];'.format(i, name._asciitree_str()) for i, name in enumerate(self.ordereddeps+(self,))) + lines.extend('{0:} [label="{0:}. {1:} {2:}"];'.format(i, name._asciitree_str(), 'CONST' if name.isconstant else ','.join(sorted(tuple(map('{0.name}:{0.ndims}'.format, name.roots))))) for i, name in enumerate(self.ordereddeps+(self,))) lines.extend('{} -> {};'.format(j, i) for i, indices in enumerate(self.dependencytree) for j in indices) lines.append('}') @@ -560,8 +560,11 @@ def prepare_eval(self, *, kwargs=...): trans = self._indextail.trans if isinstance(trans, SelectChain): for isubsample, subsample in enumerate(subsamples): - if trans.ordered_roots == subsample.roots and self._indextail.transforms == subsample.transforms[trans.n if len(subsample.transforms) > 1 else 0]: - return EmptyTransformChain(roots=self.ordered_roots, ndims=self.todims) + if trans.ordered_roots == subsample.roots: + if self._indextail.transforms == subsample.transforms[trans.n if len(subsample.transforms) > 1 else 0]: + return EmptyTransformChain(roots=self.ordered_roots, ndims=self.todims) + else: + break return self # ARRAYFUNC From c0b49122875fabd65506f01ecf1956d1d5da611a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 25 Mar 2020 16:09:12 +0100 Subject: [PATCH 068/119] fixes --- nutils/function.py | 6 +++--- nutils/topology.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 0e3b88dcb..15ebadc4c 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -3179,7 +3179,7 @@ def prepare_eval(self, *, subsamples, kwargs=...): for isubsample, subsample in enumerate(subsamples): if roots.isdisjoint(subsample.roots): continue - if not frozenset(subsample.roots) <= roots: + if not frozenset(root for root in subsample.roots if root.ndims) <= roots: raise ValueError('Cannot compute jacobian.') roots -= set(subsample.roots) ndimsmanifold += subsample.ndimsmanifold @@ -3234,7 +3234,7 @@ def prepare_eval(self, *, subsamples, kwargs=...): for isubsample, subsample in enumerate(subsamples): if roots.isdisjoint(subsample.roots): continue - if not frozenset(subsample.roots) <= roots: + if not frozenset(root for root in subsample.roots if root.ndims) <= roots: raise ValueError('Cannot compute normal.') roots -= set(subsample.roots) ndimsnormal += subsample.ndimsnormal @@ -4785,7 +4785,7 @@ def derivative(func, var, seen=None): return result def rootgradient(arg, roots): - return concatenate([derivative(arg, RootCoords(root)) for root in roots], axis=-1) + return concatenate([derivative(arg, RootCoords(root)) for root in roots if root.ndims], axis=-1) def rootbasis(subsamples, isubsample, *, orthonormal=False, opposite=False): subsample = subsamples[isubsample] diff --git a/nutils/topology.py b/nutils/topology.py index ebcdb5a17..0b48100a5 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -990,7 +990,7 @@ def basis_spline(self, degree, removedofs=None, knotvalues=None, knotmultiplicit if len(knotvalues) != 1: raise ValueError('knotvalues should be a tuple or list of length 1 but got {}'.format(len(knotvalues))) knotvalues = knotvalues[0] - if knotvalues: + if knotvalues is not None: knotvalues = numpy.array(knotvalues) assert knotvalues.ndim == 1 @@ -998,7 +998,7 @@ def basis_spline(self, degree, removedofs=None, knotvalues=None, knotmultiplicit if len(knotmultiplicities) != 1: raise ValueError('knotmultiplicities should be a tuple or list of length 1 but got {}'.format(len(knotmultiplicities))) knotmultiplicities = knotmultiplicities[0] - if knotmultiplicities: + if knotmultiplicities is not None: knotmultiplicities = numpy.array(knotmultiplicities) assert knotmultiplicities.ndim == 1 and knotmultiplicities.dtype.kind == 'i' From 4c0f0a20aa0cd6d29692aca2a366d24a84490881 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 25 Mar 2020 21:21:16 +0100 Subject: [PATCH 069/119] update --- nutils/function.py | 17 ++++++++++++----- nutils/sample.py | 2 +- nutils/topology.py | 24 ++++++++++++------------ tests/test_function.py | 27 ++++++++++++++++----------- tests/test_topology.py | 8 ++++++-- 5 files changed, 47 insertions(+), 31 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 15ebadc4c..89db8c220 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -106,14 +106,14 @@ class Subsample: Attributes ---------- roots : :class:`tuple` of :class:`Root` - transforms : :class:`tuple` of transform chains + transforms : :class:`tuple` of :class:`~nutils.transformseq.Transforms` points : :class:`~nutils.points.Points` ielem : :class:`int` or ``None`` ''' __slots__ = 'roots', 'transforms', 'points', 'ielem' - def __init__(self, *, roots: types.tuple[strictroot], transforms: types.tuple[types.tuple[transform.stricttransform]], points: points.strictpoints, ielem: types.strictint = None): + def __init__(self, *, roots: types.tuple[strictroot], transforms: types.tuple[transformseq.stricttransforms], points: points.strictpoints, ielem: types.strictint = None): self.roots = roots self.transforms = transforms self.points = points @@ -473,10 +473,15 @@ def __init__(self, roots:types.tuple[strictroot], n:types.strictint=0): def evalf(self, subsamples): trans = [] + subsamplechains = {} for root in self.ordered_roots: - for subsample in subsamples: + for isubsample, subsample in enumerate(subsamples): if root in subsample.roots: - trans.append(subsample.transforms[self.n if len(subsample.transforms) > 1 else 0][subsample.roots.index(root)]) + if isubsample in subsamplechains: + chains = subsamplechains[isubsample] + else: + subsamplechains[isubsample] = chains = subsample.transforms[self.n if len(subsample.transforms) > 1 else 0][subsample.ielem] + trans.append(chains[subsample.roots.index(root)]) break else: raise ValueError('no such root: {!r}'.format(root)) @@ -1291,8 +1296,10 @@ def evalf(self, subsamples, chains): slices = {} isubsamples = {} for isubsample, subsample in enumerate(subsamples): + if self.roots.isdisjoint(subsample.roots): + continue from0 = 0 - for root, chain in zip(subsample.roots, subsample.transforms[0]): + for root, chain in zip(subsample.roots, subsample.transforms[0][subsample.ielem]): isubsamples[root] = isubsample from1 = from0 + (chain[-1].fromdims if chain else root.ndims) slices[root] = slice(from0, from1) diff --git a/nutils/sample.py b/nutils/sample.py index 9abd51151..8f1cf0fb0 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -339,7 +339,7 @@ def subset(self, mask): return PlainSample(self.roots, self.ndims, transforms, points, map(numpy.arange, offset[:-1], offset[1:])) def getsubsamples(self, ielem): - return function.Subsample(roots=self.roots, transforms=tuple(t[ielem] for t in self.transforms), points=self.getpoints(ielem), ielem=ielem), + return function.Subsample(roots=self.roots, transforms=self.transforms, points=self.getpoints(ielem), ielem=ielem), @property def subsamplemetas(self): diff --git a/nutils/topology.py b/nutils/topology.py index 0b48100a5..da17aec49 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -317,11 +317,11 @@ def project(self, fun, onto, geometry, ischeme='gauss', degree=None, droptol=1e- F = numpy.zeros(onto.shape[0]) W = numpy.zeros(onto.shape[0]) I = numpy.zeros(onto.shape[0], dtype=bool) - fun = function.asarray(fun).prepare_eval() - data = function.Tuple(function.Tuple([fun, onto_f.simplified, function.Tuple(onto_ind)]) for onto_ind, onto_f in function.blocks(onto.prepare_eval())) - for ielem, (ref, trans, opp) in enumerate(zip(self.references, self.transforms, self.opposites)): - points = ref.getpoints('bezier', 2) - for fun_, onto_f_, onto_ind_ in data.eval(function.Subsample(roots=self.roots, transforms=(trans, opp), points=points, ielem=ielem), **arguments or {}): + sample = self.sample('bezier', 2) + fun = function.asarray(fun).prepare_eval(subsamples=sample.subsamplemetas).simplified + data = function.Tuple(function.Tuple([fun, onto_f.simplified, function.Tuple(onto_ind)]) for onto_ind, onto_f in function.blocks(onto.prepare_eval(subsamples=sample.subsamplemetas))) + for ielem in range(sample.nelems): + for fun_, onto_f_, onto_ind_ in data.eval(*sample.getsubsample(ielem), **arguments or {}): onto_f_ = onto_f_.swapaxes(0,1) # -> dof axis, point axis, ... indfun_ = fun_[(slice(None),)+numpy.ix_(*onto_ind_[1:])] assert onto_f_.shape[0] == len(onto_ind_[0]) @@ -378,22 +378,22 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None else: log.info('collecting leveltopo elements') levelset = levelset.prepare_eval(subsamples=(function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims),), transforms=(self.transforms, self.opposites)).simplified - bins = [set() for ielem in range(len(self))] - for trans in leveltopo.transforms: + bins = [dict() for ielem in range(len(self))] + for ielemlevel, trans in enumerate(leveltopo.transforms): ielem, tail = self.transforms.index_with_tail(trans) - bins[ielem].add(tail) + bins[ielem][tail] = ielemlevel fcache = cache.WrapperCache() with log.iter.percentage('trimming', self.references, self.transforms, bins) as items: - for ielem, (ref, trans, ctransforms) in enumerate(items): + for ielem, (ref, trans, bin) in enumerate(items): levels = numpy.empty(ref.nvertices_by_level(maxrefine)) todims = tuple(t[-1].fromdims for t in trans) - cover = list(fcache[ref.vertex_cover](frozenset(ctransforms), maxrefine, todims)) + cover = list(fcache[ref.vertex_cover](frozenset(bin), maxrefine, todims)) # confirm cover and greedily optimize order mask = numpy.ones(len(levels), dtype=bool) while mask.any(): imax = numpy.argmax([mask[indices].sum() for tail, cpoints, indices in cover]) tail, cpoints, indices = cover.pop(imax) - levels[indices] = levelset.eval(function.Subsample(roots=self.roots, transforms=(tuple(a+b for a, b in zip(trans, tail)),), points=points.CoordsPoints(cpoints), ielem=ielem), **arguments) + levels[indices] = levelset.eval(function.Subsample(roots=self.roots, transforms=(leveltopo.transforms,), points=points.CoordsPoints(cpoints), ielem=bin[tail]), **arguments) mask[indices] = False refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) log.debug('cache', fcache.stats) @@ -536,7 +536,7 @@ def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, ma w = p.weights xi = (numpy.dot(w,xi) / w.sum())[_] if len(xi) > 1 else xi.copy() for iiter in range(maxiter): - coord_xi, J_xi = geom_J.eval(function.Subsample(roots=self.roots, transforms=(self.transforms[ielem], self.opposites[ielem]), points=points.CoordsPoints(xi), ielem=ielem), **arguments or {}) + coord_xi, J_xi = geom_J.eval(function.Subsample(roots=self.roots, transforms=(self.transforms, self.opposites), points=points.CoordsPoints(xi), ielem=ielem), **arguments or {}) err = numpy.linalg.norm(coord - coord_xi) if err < tol: converged = True diff --git a/tests/test_function.py b/tests/test_function.py index e0ceb5823..1365b40bf 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -51,7 +51,7 @@ def assertArrayAlmostEqual(self, actual, desired, decimal): self.fail(''.join(lines)) def assertFunctionAlmostEqual(self, actual, desired, decimal): - subsample = function.Subsample(roots=self.sample.roots, transforms=[trans[0] for trans in self.sample.transforms], points=self.sample.points[0], ielem=0) + subsample = function.Subsample(roots=self.sample.roots, transforms=self.sample.transforms, points=self.sample.points[0], ielem=0) with self.subTest('vanilla'): self.assertArrayAlmostEqual(actual.eval(subsample), desired, decimal) with self.subTest('simplified'): @@ -293,11 +293,12 @@ def find(self, target, xi0): xi0 = tmp target = target.reshape(-1, target.shape[-1]) xi = xi0.reshape(-1, xi0.shape[-1]) + geom = self.geom.prepare_eval() while countdown: - err = target - self.geom.prepare_eval().eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(xi), ielem=0)) + err = target - geom.eval(function.Subsample(roots=self.sample.roots, transforms=self.sample.transforms, points=CoordsPoints(xi), ielem=0)) if numpy.less(numpy.abs(err), 1e-12).all(): countdown -= 1 - dxi_root = (Jinv.eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(xi), ielem=0)) * err[...,_,:]).sum(-1) + dxi_root = (Jinv.eval(function.Subsample(roots=self.sample.roots, transforms=self.sample.transforms, points=CoordsPoints(xi), ielem=0)) * err[...,_,:]).sum(-1) #xi = xi + numpy.dot(dxi_root, self.elem.inv_root_transform.T) xi = xi + dxi_root iiter += 1 @@ -316,7 +317,7 @@ def test_rootgradient(self): eps = 1e-5 while not numpy.all(good): fdpoints = points[_,_,:,:] + D[:,:,_,:] * eps - tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) + tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=self.sample.transforms, points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) if len(tmp) == 1 or tmp.dtype.kind in 'bi' or self.zerograd: error = exact else: @@ -355,7 +356,7 @@ def test_gradient(self): eps = 1e-4 while not numpy.all(good): fdpoints = self.find(self.sample.eval(self.geom)[_,_,:,:] + D[:,:,_,:] * eps, points[_,_,:,:]) - tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) + tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=self.sample.transforms, points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) if len(tmp) == 1 or tmp.dtype.kind in 'bi' or self.zerograd: error = exact else: @@ -382,7 +383,7 @@ def test_doublegradient(self): eps = 1e-4 while not numpy.all(good): fdpoints = self.find(self.sample.eval(self.geom)[_,_,_,_,:,:] + DD[:,:,:,:,_,:] * eps, points[_,_,_,_,:,:]) - tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=[elemtrans], points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) + tmp = self.n_op(*argsfun.eval(function.Subsample(roots=self.sample.roots, transforms=self.sample.transforms, points=CoordsPoints(fdpoints.reshape(-1,fdpoints.shape[-1])), ielem=0))) if len(tmp) == 1 or tmp.dtype.kind in 'bi' or self.zerograd: error = exact else: @@ -628,14 +629,18 @@ def setUp(self): self.func = function.Elemwise(self.data, self.index, float) def test_evalf(self): - for i, (ref, trans) in enumerate(zip(self.domain.references, self.domain.transforms)): + sample = self.domain.sample('gauss', 1) + func = self.func.prepare_eval(subsamples=sample.subsamplemetas) + for i in range(sample.nelems): with self.subTest(i=i): - numpy.testing.assert_array_almost_equal(self.func.prepare_eval().eval(function.Subsample(roots=self.domain.roots, transforms=(trans,), points=ref.getpoints('gauss', 1)), ielem=i), self.data[i][_]) + numpy.testing.assert_array_almost_equal(func.eval(*sample.getsubsamples(i)), self.data[i][_]) def test_shape(self): + sample = self.domain.sample('gauss', 1) + size = self.func.size.prepare_eval(subsamples=sample.subsamplemetas) for i, (ref, trans) in enumerate(zip(self.domain.references, self.domain.transforms)): with self.subTest(i=i): - self.assertEqual(self.func.size.prepare_eval().eval(function.Subsample(roots=self.domain.roots, transforms=(trans,), points=ref.getpoints('gauss', 1), ielem=i))[0], self.data[i].size) + self.assertEqual(size.eval(*sample.getsubsamples(i))[0], self.data[i].size) def test_derivative(self): self.assertTrue(function.iszero(function.rootgradient(self.func, self.domain.roots))) @@ -1063,11 +1068,11 @@ def test_evalf(self): def test_simplified(self): ref = element.PointReference() if self.basis.ndimsdomain == 0 else element.LineReference()**self.basis.ndimsdomain points = ref.getpoints('bezier', 4) - simplified = self.basis.simplified + simplified = self.basis.simplified.prepare_eval() with _builtin_warnings.catch_warnings(): _builtin_warnings.simplefilter('ignore', category=function.ExpensiveEvaluationWarning) for ielem in range(self.checknelems): - value = simplified.prepare_eval().eval(function.Subsample(roots=self.roots, transforms=(self.basis.transforms[ielem],), points=points, ielem=ielem)) + value = simplified.eval(function.Subsample(roots=self.roots, transforms=(self.basis.transforms,), points=points, ielem=ielem)) if value.shape[0] == 1: value = numpy.tile(value, (points.npoints, 1)) self.assertEqual(value.tolist(), self.checkeval(ielem, points.coords)) diff --git a/tests/test_topology.py b/tests/test_topology.py index 14c3a9acc..b7644600f 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -9,11 +9,13 @@ def assertConnectivity(self, domain, geom): interfaces = domain.interfaces bmask = numpy.zeros(len(boundary), dtype=int) imask = numpy.zeros(len(interfaces), dtype=int) + geom = geom.prepare_eval() for ielem, ioppelems in enumerate(domain.connectivity): for iedge, ioppelem in enumerate(ioppelems): etrans, eref = domain.references[ielem].edges[iedge] trans = transform.append_edge(domain.transforms[ielem], etrans) if ioppelem == -1: + transforms = boundary.transforms, boundary.opposites index = boundary.transforms.index(trans) bmask[index] += 1 else: @@ -25,13 +27,15 @@ def assertConnectivity(self, domain, geom): except ValueError: index = interfaces.transforms.index(opptrans) self.assertEqual(interfaces.opposites[index], trans) + transforms = interfaces.opposites, interfaces.transforms else: self.assertEqual(interfaces.opposites[index], opptrans) + transforms = interfaces.transforms, interfaces.opposites imask[index] += 1 self.assertEqual(eref, opperef) points = eref.getpoints('gauss', 2) - a0 = geom.prepare_eval().eval(function.Subsample(roots=domain.roots, transforms=[trans], points=points)) - a1 = geom.prepare_eval().eval(function.Subsample(roots=domain.roots, transforms=[opptrans], points=points)) + a0 = geom.eval(function.Subsample(roots=domain.roots, transforms=transforms[:1], points=points, ielem=index)) + a1 = geom.eval(function.Subsample(roots=domain.roots, transforms=transforms[1:], points=points, ielem=index)) numpy.testing.assert_array_almost_equal(a0, a1) self.assertTrue(numpy.equal(bmask, 1).all()) self.assertTrue(numpy.equal(imask, 2).all()) From 906104d0dcee8804d7865c03d604d103f5147052 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 14 Apr 2020 21:36:08 +0200 Subject: [PATCH 070/119] fix subsamplemeta with points is None --- nutils/function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nutils/function.py b/nutils/function.py index 89db8c220..e3888176c 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -1340,7 +1340,7 @@ def prepare_eval(self, *, kwargs=...): slices = {} isubsamples = {} for isubsample, subsample in enumerate(subsamples): - if not self.roots.isdisjoint(subsample.roots) and subsample.points is None: + if not self.roots.isdisjoint(subsample.roots) or subsample.points is None: return ApplyTransforms(tail, self._linear) from0 = 0 From 3b4883051bca4ddde119957051de54726d27943c Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 11 May 2020 15:49:51 +0200 Subject: [PATCH 071/119] fix trimming --- nutils/topology.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index da17aec49..d49c186eb 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -371,13 +371,14 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None arguments = {} refs = [] + levelset = levelset.prepare_eval(subsamples=(function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims),), transforms=(self.transforms, self.opposites)).simplified if leveltopo is None: - verts = self.sample('vertex', maxrefine) - levels = verts.eval(levelset) - refs = [ref.trim(levels[verts.getindex(ielem)], maxrefine=maxrefine, ndivisions=ndivisions) for ielem, ref in enumerate(self.references)] + with log.iter.percentage('trimming', self.references, self.transforms, self.opposites) as items: + for ielem, (ref, trans, opp) in enumerate(items): + levels = levelset.eval(function.Subsample(roots=self.roots, transforms=(self.transforms, self.opposites), points=ref.getpoints('vertex', maxrefine), ielem=ielem), **arguments) + refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) else: log.info('collecting leveltopo elements') - levelset = levelset.prepare_eval(subsamples=(function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims),), transforms=(self.transforms, self.opposites)).simplified bins = [dict() for ielem in range(len(self))] for ielemlevel, trans in enumerate(leveltopo.transforms): ielem, tail = self.transforms.index_with_tail(trans) From f404370f0e1612107c3b93e411d0b90152ff11b1 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 15 Jun 2020 13:03:15 +0200 Subject: [PATCH 072/119] preserve topological structure of empty topos --- nutils/topology.py | 56 +++++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index d49c186eb..8f3827282 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -69,9 +69,13 @@ def __str__(self): def __len__(self): return len(self.references) - def getitem(self, item): + @property + def empty(self): return EmptyTopology(self.roots, self.ndims) + def getitem(self, item): + return self.empty + def __getitem__(self, item): if numeric.isintarray(item): item = types.frozenarray(item) @@ -81,7 +85,7 @@ def __getitem__(self, item): if all(it in (...,slice(None)) for it in item): return self topo = self.getitem(item) if len(item) != 1 or not isinstance(item[0],str) \ - else functools.reduce(operator.or_, map(self.getitem, item[0].split(',')), EmptyTopology(self.roots, self.ndims)) + else functools.reduce(operator.or_, map(self.getitem, item[0].split(',')), self.empty) if not topo: raise KeyError(item) return topo @@ -133,10 +137,7 @@ def mul(self, other, leftopp, rightopp): return NotImplemented if not set(self.roots).isdisjoint(other.roots): raise ValueError('cannot multiply topologies with common roots') - if isinstance(self, EmptyTopology) or isinstance(other, EmptyTopology): - return EmptyTopology(self.roots+other.roots, self.ndims+other.ndims) - else: - return ProductTopology(self, other, leftopp, rightopp) + return ProductTopology(self, other, leftopp, rightopp) def __mul__(self, other): leftopp = self.transforms != self.opposites @@ -414,7 +415,7 @@ def subset(self, topo, newboundary=None, strict=False): assert subref == ref, 'elements do not form a strict subset' refs[ielem] = subref if not any(refs): - return EmptyTopology(self.roots, self.ndims) + return self.empty return SubsetTopology(self, refs, newboundary) def withgroups(self, vgroups={}, bgroups={}, igroups={}, pgroups={}): @@ -871,7 +872,7 @@ def getitem(self, item): raise ValueError('expected a tuple of length 1 but got length {}'.format(len(item))) item = item[0] if not isinstance(item, slice): - return EmptyTopology(self.roots, self.ndims) + return self.empty if item == slice(None): return self else: @@ -915,7 +916,7 @@ def getitem(self, item): raise ValueError('expected a tuple of length 1 but got length {}'.format(len(item))) item = item[0] if not isinstance(item, slice): - return EmptyTopology(self.roots, self.ndims) + return self.empty start, stop, step = item.indices(len(self)) if item == slice(None): return self @@ -1204,7 +1205,7 @@ def __len__(self): def getitem(self, item): if not isinstance(item, tuple): - return EmptyTopology(self.roots, self.ndims) + return self.empty assert all(isinstance(it,slice) for it in item) and len(item) <= self.ndims if all(it == slice(None) for it in item): # shortcut return self @@ -1251,8 +1252,6 @@ def boundary(self): btopos = [StructuredTopology(root=self.root, axes=self.axes[:idim] + (transformseq.BndAxis(n,n if not axis.isperiodic else 0,nbounds,side),) + self.axes[idim+1:], nrefine=self.nrefine, bnames=self._bnames) for idim, axis in enumerate(self.axes) if axis.isdim and not axis.isperiodic for side, n in enumerate((axis.i,axis.j))] - if not btopos: - return EmptyTopology(self.roots, self.ndims-1) bnames = [bname for bnames, axis in zip(self._bnames, self.axes) if axis.isdim and not axis.isperiodic for bname in bnames] return DisjointUnionTopology(btopos, bnames) @@ -1726,7 +1725,7 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st def getitem(self, item): topos = [topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)] - return functools.reduce(operator.or_, topos, EmptyTopology(self.roots, self.ndims)) + return functools.reduce(operator.or_, topos, self.empty) def __or__(self, other): if not isinstance(other, UnionTopology): @@ -1757,14 +1756,7 @@ def __init__(self, topos:types.tuple[stricttopology], names:types.tuple[types.st transformseq.chain((topo.opposites for topo in self._topos), tuple(root.ndims for root in roots))) def getitem(self, item): - topos = [topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)] - topos = [topo for topo in topos if not isinstance(topo, EmptyTopology)] - if len(topos) == 0: - return EmptyTopology(self.roots, self.ndims) - elif len(topos) == 1: - return topos[0] - else: - return DisjointUnionTopology(topos) + return DisjointUnionTopology([topo if name == item else topo.getitem(item) for topo, name in itertools.zip_longest(self._topos, self._names)]) @property def refined(self): @@ -1778,11 +1770,19 @@ def boundary(self): def interfaces(self): return DisjointUnionTopology([topo.interfaces for topo in self._topos]) + @property + def empty(self): + return DisjointUnionTopology([topo.empty for topo in self._topos]) + def sample(self, ischeme, degree): transforms = self.transforms, if len(self.transforms) == 0 or self.opposites != self.transforms: transforms += self.opposites, - return sample.ChainedSample(tuple(topo.sample(ischeme, degree) for topo in self._topos), transforms) + if any(self._topos): + samples = tuple(topo.sample(ischeme, degree) for topo in self._topos if topo) + else: + samples = tuple(topo.sample(ischeme, degree) for topo in self._topos) + return sample.ChainedSample(samples, transforms) class SubsetTopology(Topology): 'trimmed' @@ -2277,6 +2277,10 @@ def connectivity(self): s = len(self._right) return tuple(tuple(ir+cli*s if cli >= 0 else -1 for cli in cl)+tuple(il*s+cri if cri >= 0 else -1 for cri in cr) for (il,cl), (ir,cr) in itertools.product(enumerate(self._left.connectivity), enumerate(self._right.connectivity))) + @property + def empty(self): + return ProductTopology(self._left.empty, self._right.empty, False, False) + def getitem(self, item): if isinstance(item, tuple) and all(isinstance(it, slice) for it in item): left = self._left.getitem(item[:self._left.ndims]) @@ -2300,9 +2304,7 @@ def boundary(self): if self._left.ndims: boundaries.append(self._left.boundary.mul_leftopp(self._right)) if not boundaries: - return EmptyTopology(self.roots, ndims=0) - elif len(boundaries) == 1: - return boundaries[0] + raise ValueError('a 0D topology has no boundary') else: return DisjointUnionTopology(boundaries) @@ -2314,9 +2316,7 @@ def interfaces(self): if self._left.ndims: interfaces.append(self._left.interfaces.mul_leftopp(self._right)) if not interfaces: - return EmptyTopology(self.roots, ndims=0) - elif len(interfaces) == 1: - return interfaces[0] + raise ValueError('a 0D topology has no interfaces') else: return DisjointUnionTopology(interfaces) From 0ba778e9a9da07102a99591841234c197c8a2613 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 9 Jun 2020 14:52:19 +0200 Subject: [PATCH 073/119] fix igroups orientation --- nutils/topology.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 8f3827282..46c10a542 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -769,14 +769,17 @@ def interfaces(self): for transs in zip(topo.transforms, topo.opposites): for trans in transs: try: - s.append(baseitopo.transforms.index(trans)) - break + ielem = baseitopo.transforms.index(trans) except ValueError: + ref = ref.flipped continue + assert baseitopo.references[ielem] == ref + s.append(ielem) + break else: raise ValueError('group is not a subset of topology') s = types.frozenarray(tuple(sorted(s)), dtype=int) - igroups[name] = Topology(self.roots, baseitopo.references[s], baseitopo.transforms[s], baseitopo.opposites[s]) + igroups[name] = Topology(baseitopo.roots, baseitopo.references[s], baseitopo.transforms[s], baseitopo.opposites[s]) return baseitopo.withgroups(igroups) @property From 11f417db4ad2a47252f84e3dcd58e2dc1c3b8c2a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 9 Jun 2020 14:51:11 +0200 Subject: [PATCH 074/119] fix tri, hull of empty Sample --- nutils/sample.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nutils/sample.py b/nutils/sample.py index 8f1cf0fb0..f0a780253 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -299,6 +299,8 @@ def tri(self): row defines a simplex by mapping vertices into the list of points. ''' + if self.npoints == 0: + return types.frozenarray(numpy.zeros((0,self.ndims+1), int)) return types.frozenarray(numpy.concatenate([index.take(points.tri) for index, points in zip(self.indexiter, self.pointsiter)]), copy=False) @property @@ -311,6 +313,8 @@ def hull(self): triangulations originating from separate elements are disconnected. ''' + if self.npoints == 0: + return types.frozenarray(numpy.zeros((0,self.ndims), int)) return types.frozenarray(numpy.concatenate([index.take(points.hull) for index, points in zip(self.indexiter, self.pointsiter)]), copy=False) def subset(self, mask): @@ -549,11 +553,15 @@ def integral(self, func): @property def tri(self): + if self.npoints == 0: + return types.frozenarray(numpy.zeros((0,self.ndims+1), int)) offsets = util.cumsum(sample.npoints for sample in self._samples) return types.frozenarray(numpy.concatenate([sample.tri+offset for sample, offset in zip(self._samples, offsets)], axis=0), copy=False) @property def hull(self): + if self.npoints == 0: + return types.frozenarray(numpy.zeros((0,self.ndims), int)) offsets = util.cumsum(sample.npoints for sample in self._samples) return types.frozenarray(numpy.concatenate([sample.hull+offset for sample, offset in zip(self._samples, offsets)], axis=0), copy=False) From 4aa87da0857b876767cca1b4ae57ac6ebbf5825a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Thu, 14 May 2020 08:42:02 +0200 Subject: [PATCH 075/119] WIP: fix flipping normal of manifold refs --- nutils/element.py | 12 ++++++++++++ nutils/topology.py | 3 ++- tests/test_finitecell.py | 13 ++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/nutils/element.py b/nutils/element.py index 89d2480ef..cf6f04367 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -313,6 +313,10 @@ def withmanifoldedges(self): else: return self + @property + def flipped(self): + return self + strictreference = types.strict[Reference] class EmptyLike(Reference): @@ -918,6 +922,10 @@ def get_poly_coeffs(self, basis, **kwargs): def get_edge_dofs(self, degree, iedge): return self.baseref.get_edge_dofs(degree, iedge) + @property + def flipped(self): + return OwnChildReference(self.baseref.flipped) + class WithChildrenReference(Reference): 'base reference with explicit children' @@ -1341,6 +1349,10 @@ def inside(self, point, eps=0): def slice(self, levelfunc, ndivisions): return ManifoldReference(self.ref.slice(lambda vertices: levelfunc(self.trans.apply(vertices)), ndivisions), self.trans) + @property + def flipped(self): + return ManifoldReference(self.ref, self.trans.flipped) + ## UTILITY FUNCTIONS def parse_legacy_ischeme(ischeme): diff --git a/nutils/topology.py b/nutils/topology.py index 46c10a542..dc41001fe 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -810,7 +810,8 @@ class OppositeTopology(Topology): def __init__(self, basetopo): self.basetopo = basetopo - super().__init__(basetopo.roots, basetopo.references, basetopo.opposites, basetopo.transforms) + refs = elementseq.asreferences((ref.flipped for ref in basetopo.references), self.basetopo.ndims) + super().__init__(basetopo.roots, refs, basetopo.opposites, basetopo.transforms) def getitem(self, item): return ~(self.basetopo.getitem(item)) diff --git a/tests/test_finitecell.py b/tests/test_finitecell.py index 8dfe320ab..23ced2c23 100644 --- a/tests/test_finitecell.py +++ b/tests/test_finitecell.py @@ -370,7 +370,8 @@ class partialtrim(TestCase): # +-----+-----+ def setUp(self): - self.topo, geom = mesh.rectilinear([2,2]) + self.topo, self.geom = mesh.rectilinear([2,2]) + geom = self.geom self.topoA = self.topo.trim(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1) self.topoB = self.topo - self.topoA @@ -407,3 +408,13 @@ def test_baseboundaries(self): for topo in self.topoA, self.topoB: alttopo = topology.ConnectedTopology(topo.roots, topo.references, topo.transforms, topo.opposites, topo.connectivity) self.assertEqual(dict(zip(alttopo.boundary.transforms, alttopo.boundary.references)), dict(zip(topo.boundary.transforms, topo.boundary.references))) + + def test_volumes(self): + geom = self.geom + f = ((0.5 - geom)**2).sum(axis=0) + lhs = self.topoA.integrate(f.grad(geom)*function.J(geom), ischeme='gauss2') + rhs = self.topoA.boundary.integrate(f*function.normal(geom)*function.J(geom), ischeme='gauss2') + numpy.testing.assert_array_almost_equal(lhs, rhs) + lhs = self.topoB.integrate(f.grad(geom)*function.J(geom), ischeme='gauss2') + rhs = self.topoB.boundary.integrate(f*function.normal(geom)*function.J(geom), ischeme='gauss2') + numpy.testing.assert_array_almost_equal(lhs, rhs) From fa9b380ba646e9741e7457fb59332b89c585279c Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 15 Jun 2020 17:17:34 +0200 Subject: [PATCH 076/119] fix prepare_eval with subsample.transforms=None --- nutils/function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nutils/function.py b/nutils/function.py index e3888176c..433018ab8 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -2614,7 +2614,7 @@ def prepare_eval(self, *, kwargs=...): trans = self._indextail.trans if isinstance(trans, SelectChain): for isubsample, subsample in enumerate(subsamples): - if trans.ordered_roots == subsample.roots and self._indextail.transforms == subsample.transforms[trans.n if len(subsample.transforms) > 1 else 0]: + if trans.ordered_roots == subsample.roots and subsample.transforms is not None and self._indextail.transforms == subsample.transforms[trans.n if len(subsample.transforms) > 1 else 0]: return IndexFromSubsample(isubsample, trans.ordered_roots) return self From 057b049159f25984a0b36a0bb810e2d1d6a14cb2 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 15 Jun 2020 17:22:23 +0200 Subject: [PATCH 077/119] add boundary and interfaces to empty topology --- nutils/topology.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nutils/topology.py b/nutils/topology.py index dc41001fe..462a0f4ad 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -838,6 +838,22 @@ def __or__(self, other): def __rsub__(self, other): return other + @property + def boundary(self): + if self.ndims == 0: + raise ValueError('a 0D topology has no boundary') + return EmptyTopology(self.roots, self.ndims-1) + + @property + def interfaces(self): + if self.ndims == 0: + raise ValueError('a 0D topology has no interfaces') + return EmptyTopology(self.roots, self.ndims-1) + + @property + def refined(self): + return self + class Point(Topology): 'point' From 111e2bb6d57100762f33bb4071c5145849513b26 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 16 Jun 2020 16:50:58 +0200 Subject: [PATCH 078/119] make Topology.integrate_elementwise tensorial --- nutils/topology.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 462a0f4ad..d2453ff24 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -204,9 +204,10 @@ def sample(self, ischeme, degree): def integrate_elementwise(self, funcs, *, asfunction=False, **kwargs): 'element-wise integration' - ielem = function.TransformsIndexWithTail(self.transforms, self.ndims, function.SelectChain(self.roots)).index + funcs = tuple(map(function.asarray, funcs)) + elems = self.basis('discont', 0) with matrix.Numpy(): - retvals = self.integrate([function.Inflate(function.asarray(func)[_], dofmap=ielem[_], length=len(self), axis=0) for func in funcs], **kwargs) + retvals = self.integrate([func[_] * elems[(slice(None),)+(_,)*func.ndim] for func in funcs], **kwargs) retvals = [retval.export('dense') if len(retval.shape) == 2 else retval for retval in retvals] return [function.elemwise(self.roots, self.transforms, self.ndims, retval) for retval in retvals] if asfunction \ else retvals From e43627f0128166dee165399e95513afda4112d29 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 16 Jun 2020 16:51:35 +0200 Subject: [PATCH 079/119] add CompressedSample, CompressedTopology --- nutils/sample.py | 90 ++++++++++++++++++++++++++++++++++++++++++++-- nutils/topology.py | 28 +++++++++++++-- 2 files changed, 113 insertions(+), 5 deletions(-) diff --git a/nutils/sample.py b/nutils/sample.py index f0a780253..9906fc989 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -100,6 +100,13 @@ def __repr__(self): def _prepare_funcs(self, funcs): return [function.asarray(func).prepare_eval(subsamples=self.subsamplemetas) for func in funcs] + def compress(self, indices): + assert indices.ndim == 1 + assert not len(indices) or numpy.less(indices[:-1], indices[1:]).all() and 0 <= indices[0] and indices[-1] < self.nelems + if self.nelems == 0 or len(indices) == self.nelems: + return self + return CompressedSample(self, indices) + @property def index(self): warnings.deprecation('`Sample.index` is deprecated; replace `Sample.index[ielem]` with `Sample.getindex(ielem)`') @@ -463,6 +470,13 @@ def __init__(self, sample1:strictsample, sample2:strictsample, transforms:types. sample1.npoints*sample2.npoints, transforms) + def compress(self, indices): + assert indices.ndim == 1 + assert not len(indices) or numpy.less(indices[:-1], indices[1:]).all() and 0 <= indices[0] and indices[-1] < self.nelems + if self.nelems == 0 or len(indices) == self.nelems: + return self + return CompressedProductSample(self._sample1, self._sample2, self.transforms, indices) + def getpoints(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) return points.TensorPoints(self._sample1.getpoints(ielem1), self._sample2.getpoints(ielem2)) @@ -522,14 +536,22 @@ def __init__(self, samples:types.tuple[strictsample], transforms:types.tuple[tra raise ValueError('all samples to be chained should have the same dimension') todims = tuple(root.ndims for root in roots) self._samples = samples - self._elemoffsets = numpy.cumsum([0, *(sample.nelems for sample in samples[:-1])]) - self._pointsoffsets = numpy.cumsum([0, *(sample.npoints for sample in samples[:-1])]) + self._elemoffsets = numpy.cumsum([0, *(sample.nelems for sample in samples)]) + self._pointsoffsets = numpy.cumsum([0, *(sample.npoints for sample in samples)]) super().__init__(roots, ndims, sum(sample.npoints for sample in samples), transforms) + def compress(self, indices): + assert indices.ndim == 1 + assert not len(indices) or numpy.less(indices[:-1], indices[1:]).all() and 0 <= indices[0] and indices[-1] < self.nelems + if self.nelems == 0 or len(indices) == self.nelems: + return self + splits = numpy.searchsorted(indices, self._elemoffsets) + return ChainedSample([s.compress(indices[l:r] - o) for s, l, r, o in zip(self._samples, splits[:-1], splits[1:], self._elemoffsets[:-1])], tuple(t[indices] for t in self.transforms)) + def _findelem(self, ielem): if ielem < 0 or ielem >= self.nelems: raise IndexError('element index out of range') - isample = numpy.searchsorted(self._elemoffsets[1:], ielem, side='right') + isample = numpy.searchsorted(self._elemoffsets[1:-1], ielem, side='right') return isample, ielem - self._elemoffsets[isample] def getpoints(self, ielem): @@ -565,6 +587,68 @@ def hull(self): offsets = util.cumsum(sample.npoints for sample in self._samples) return types.frozenarray(numpy.concatenate([sample.hull+offset for sample, offset in zip(self._samples, offsets)], axis=0), copy=False) + @util.single_or_multiple + @types.apply_annotations + def integrate_sparse(self, funcs:types.tuple[function.asarray], arguments:types.frozendict[str,types.frozenarray]=None): + results = [] + for smpl in self._samples: + results.append(smpl.integrate_sparse(funcs, arguments)) + return tuple(map(sparse.add, zip(*results))) + +class CompressedSample(Sample): + + @types.apply_annotations + def __init__(self, base:strictsample, indices:types.frozenarray[types.strictint]): + assert indices.ndim == 1 + assert not len(indices) or numpy.less(indices[:-1], indices[1:]).all() and indices[-1] < base.nelems + self.base = base + self.indices = indices + self._renumber = numpy.full((base.npoints,), base.npoints, int) + offset = 0 + for baseindex in map(base.getindex, indices): + self._renumber[baseindex] = numpy.arange(offset, offset+len(baseindex)) + offset += len(baseindex) + super().__init__(base.roots, base.ndims, sum(base.getpoints(i).npoints for i in indices), tuple(t[indices] for t in base.transforms)) + + def compress(self, indices): + assert indices.ndim == 1 + assert not len(indices) or numpy.less(indices[:-1], indices[1:]).all() and 0 <= indices[0] and indices[-1] < self.nelems + if self.nelems == 0 or len(indices) == self.nelems: + return self + return CompressedSample(self.base, self.indices[indices]) + + def getpoints(self, ielem): + return self.base.getpoints(self.indices[ielem]) + + def getindex(self, ielem): + return self._renumber[self.base.getindex(self.indices[ielem])] + +class CompressedProductSample(CompressedSample): + + @types.apply_annotations + def __init__(self, sample1:strictsample, sample2:strictsample, transforms:types.tuple[transformseq.stricttransforms], indices:types.frozenarray[types.strictint]): + self._sample1 = sample1 + self._sample2 = sample2 + self._prod_transforms = transforms + self._prod_indices = indices + super().__init__(ProductSample(sample1, sample2, transforms), indices) + + def compress(self, indices): + assert indices.ndim == 1 + assert not len(indices) or numpy.less(indices[:-1], indices[1:]).all() and 0 <= indices[0] and indices[-1] < self.nelems + if self.nelems == 0 or len(indices) == self.nelems: + return self + return CompressedProductSample(self._sample1, self._sample2, self._prod_transforms, self._prod_indices[indices]) + + def getsubsamples(self, ielem): + ibase = self.indices[ielem] + ielem1, ielem2 = divmod(ibase, self._sample2.nelems) + return self._sample1.getsubsamples(ielem1) + self._sample2.getsubsamples(ielem2) + + @property + def subsamplemetas(self): + return self._sample1.subsamplemetas + self._sample2.subsamplemetas + class Integral(types.Singleton): '''Postponed integration. diff --git a/nutils/topology.py b/nutils/topology.py index d2453ff24..e50f91190 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -73,13 +73,16 @@ def __len__(self): def empty(self): return EmptyTopology(self.roots, self.ndims) + def compress(self, indices): + indices = types.frozenarray(indices) + return CompressedTopology(self, indices) + def getitem(self, item): return self.empty def __getitem__(self, item): if numeric.isintarray(item): - item = types.frozenarray(item) - return Topology(self.roots, self.references[item], self.transforms[item], self.opposites[item]) + return self.compress(item) if not isinstance(item, tuple): item = item, if all(it in (...,slice(None)) for it in item): @@ -839,6 +842,10 @@ def __or__(self, other): def __rsub__(self, other): return other + @property + def connectivity(self): + return types.frozenarray(numpy.zeros((0, 2**self.ndims), int)) + @property def boundary(self): if self.ndims == 0: @@ -1954,6 +1961,20 @@ def locate(self, geom, coords, *, eps=0, **kwargs): raise LocateError('failed to locate point: {}'.format(coords[sample.getindex(ielem)[i]])) return sample +class CompressedTopology(Topology): + + @types.apply_annotations + def __init__(self, basetopo: stricttopology, indices: types.frozenarray[types.strictint]): + self.basetopo = basetopo + self.indices = indices + super().__init__(basetopo.roots, + basetopo.references[indices], + basetopo.transforms[indices], + basetopo.opposites[indices]) + + def sample(self, ischeme, degree): + return self.basetopo.sample(ischeme, degree).compress(self.indices) + class RefinedTopology(Topology): 'refinement' @@ -2270,6 +2291,9 @@ def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): return function.PlainBasis(hbasis_coeffs, hbasis_dofs, ndofs, self.transforms, self.ndims, function.SelectChain(self.roots)) + def sample(self, ischeme, degree): + return DisjointUnionTopology([level[ind] for level, ind in zip(self.levels, self._indices_per_level)]).sample(ischeme, degree) + class ProductTopology(Topology): 'product topology' From 362145e225fd6dc4761cdcf3bcc68644656613f4 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 16 Jun 2020 16:52:17 +0200 Subject: [PATCH 080/119] update --- tests/test_topology.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_topology.py b/tests/test_topology.py index b7644600f..4830ac7be 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -391,17 +391,17 @@ def setUp(self): self.domain0, self.geom = mesh.rectilinear([2]*self.ndims) self.domain1 = self.domain0.trim((1.1 - self.geom).sum(), maxrefine=2) self.domain2 = self.domain1.refined_by(filter(self.domain1.transforms.contains, self.domain0[:1].transforms)) - #self.domain3 = self.domain2.refined_by(filter(self.domain2.transforms.contains, self.domain0.refined[:1].transforms)) + self.domain3 = self.domain2.refined_by(filter(self.domain2.transforms.contains, self.domain0.refined[:1].transforms)) def test_boundaries(self): self.assertBoundaries(self.domain1, self.geom) self.assertBoundaries(self.domain2, self.geom) - #self.assertBoundaries(self.domain3, self.geom) + self.assertBoundaries(self.domain3, self.geom) def test_interfaces(self): self.assertInterfaces(self.domain1, self.geom, periodic=False) self.assertInterfaces(self.domain2, self.geom, periodic=False) - #self.assertInterfaces(self.domain3, self.geom, periodic=False) + self.assertInterfaces(self.domain3, self.geom, periodic=False) trimmedhierarchical('1d', ndims=1) trimmedhierarchical('2d', ndims=2) From 099c6e3e628f2a39d08a054b566d532737fd17b3 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 16 Jun 2020 23:51:44 +0200 Subject: [PATCH 081/119] revert ProductTopology.basis simplifications --- nutils/topology.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index e50f91190..39dedfc85 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -862,6 +862,9 @@ def interfaces(self): def refined(self): return self + def basis(self, *args, **kwargs): + return function.PlainBasis(numpy.zeros((0,)*(self.ndims+1)), numpy.zeros((0,)*(self.ndims+1)), 0, self.transforms, self.ndims, function.SelectChain(self.roots)) + class Point(Topology): 'point' @@ -2366,10 +2369,6 @@ def interfaces(self): return DisjointUnionTopology(interfaces) def _productbasis(self, lbasis, rbasis): - if not lbasis: - return rbasis - if not rbasis: - return lbasis return function.ProductBasis(lbasis, rbasis, function.SelectChain(self.roots)) def basis(self, name, *args, **kwargs): @@ -2377,8 +2376,8 @@ def basis(self, name, *args, **kwargs): return self.basis_spline(*args, _variant=name, **kwargs) elif name == 'std': return self.basis_std(*args, **kwargs) - lbasis = self._left.basis(name, *args, **kwargs) if self._left.ndims else None - rbasis = self._right.basis(name, *args, **kwargs) if self._right.ndims else None + lbasis = self._left.basis(name, *args, **kwargs) + rbasis = self._right.basis(name, *args, **kwargs) return self._productbasis(lbasis, rbasis) def _split_list(self, value, scalar_type): @@ -2411,14 +2410,14 @@ def basis_spline(self, degree, removedofs=None, knotvalues=None, knotmultiplicit lperiodic = [i for i in periodic if i < self._left.ndims] rperiodic = [i-self._left.ndims for i in periodic if i >= self._left.ndims] - lbasis = self._left.basis(_variant, degree=ldegree, removedofs=lremovedofs, knotvalues=lknotvalues, knotmultiplicities=lknotmultiplicities, continuity=lcontinuity, periodic=lperiodic) if self._left.ndims else None - rbasis = self._right.basis(_variant, degree=rdegree, removedofs=rremovedofs, knotvalues=rknotvalues, knotmultiplicities=rknotmultiplicities, continuity=rcontinuity, periodic=rperiodic) if self._right.ndims else None + lbasis = self._left.basis(_variant, degree=ldegree, removedofs=lremovedofs, knotvalues=lknotvalues, knotmultiplicities=lknotmultiplicities, continuity=lcontinuity, periodic=lperiodic) + rbasis = self._right.basis(_variant, degree=rdegree, removedofs=rremovedofs, knotvalues=rknotvalues, knotmultiplicities=rknotmultiplicities, continuity=rcontinuity, periodic=rperiodic) return self._productbasis(lbasis, rbasis) def basis_std(self, degree): ldegree, rdegree = self._split_scalar(degree, int) - lbasis = self._left.basis('std', degree=ldegree) if self._left.ndims else None - rbasis = self._right.basis('std', degree=rdegree) if self._right.ndims else None + lbasis = self._left.basis('std', degree=ldegree) + rbasis = self._right.basis('std', degree=rdegree) return self._productbasis(lbasis, rbasis) @property From b09c92479c561c69f51728fe506ef5c5185c799f Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 9 Jun 2020 23:34:37 +0200 Subject: [PATCH 082/119] move normal, grad, dotnorm above Namespace --- nutils/function.py | 74 +++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 433018ab8..daa6aa5c1 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4936,6 +4936,43 @@ def ravel(func, axis): axis = numeric.normdim(func.ndim-1, axis) return Ravel(func, axis) +def normal(arg, exterior=False): + assert arg.ndim == 1 + if not exterior: + return DelayedNormal(arg) + # Order the roots deterministically. In the future we should use the order + # of `Sample.roots` (during `prepare_eval` or a successor). + roots = tuple(sorted(arg.roots, key=lambda root: (root.name, root.ndims))) + lgrad = rootgradient(arg, roots) + if len(arg) == 2: + return asarray([lgrad[1,0], -lgrad[0,0]]).normalized() + if len(arg) == 3: + return cross(lgrad[:,0], lgrad[:,1], axis=0).normalized() + raise NotImplementedError + +def grad(self, geom, ndims=0): + assert geom.ndim == 1 + if ndims <= 0: + ndims += geom.shape[0] + # Order the roots deterministically. In the future we should use the order + # of `Sample.roots` (during `prepare_eval` or a successor). + roots = tuple(sorted(geom.roots, key=lambda root: (root.name, root.ndims))) + J = rootgradient(geom, roots) + if J.shape[0] == J.shape[1]: + Jinv = inverse(J) + elif J.shape[0] == J.shape[1] + 1: # gamma gradient + G = dot(J[:,:,_], J[:,_,:], 0) + Ginv = inverse(G) + Jinv = dot(J[_,:,:], Ginv[:,_,:], -1) + else: + raise Exception('cannot invert {}x{} jacobian'.format(J.shape)) + return dot(rootgradient(self, roots)[...,_], Jinv, -2) + +def dotnorm(arg, geom, axis=-1): + axis = numeric.normdim(arg.ndim, axis) + assert geom.ndim == 1 and geom.shape[0] == arg.shape[axis] + return dot(arg, normal(geom)[(slice(None),)+(_,)*(arg.ndim-axis-1)], axis) + @replace def replace_arguments(value, arguments): '''Replace :class:`Argument` objects in ``value``. @@ -5279,43 +5316,6 @@ def __rmatmul__(self, expr): raise ValueError('`expression @ Namespace` cannot be used because the expression has more than one dimension. Use `Namespace.eval_...(expression)` instead') return _eval_ast(ast, self._functions) -def normal(arg, exterior=False): - assert arg.ndim == 1 - if not exterior: - return DelayedNormal(arg) - # Order the roots deterministically. In the future we should use the order - # of `Sample.roots` (during `prepare_eval` or a successor). - roots = tuple(sorted(arg.roots, key=lambda root: (root.name, root.ndims))) - lgrad = rootgradient(arg, roots) - if len(arg) == 2: - return asarray([lgrad[1,0], -lgrad[0,0]]).normalized() - if len(arg) == 3: - return cross(lgrad[:,0], lgrad[:,1], axis=0).normalized() - raise NotImplementedError - -def grad(self, geom, ndims=0): - assert geom.ndim == 1 - if ndims <= 0: - ndims += geom.shape[0] - # Order the roots deterministically. In the future we should use the order - # of `Sample.roots` (during `prepare_eval` or a successor). - roots = tuple(sorted(geom.roots, key=lambda root: (root.name, root.ndims))) - J = rootgradient(geom, roots) - if J.shape[0] == J.shape[1]: - Jinv = inverse(J) - elif J.shape[0] == J.shape[1] + 1: # gamma gradient - G = dot(J[:,:,_], J[:,_,:], 0) - Ginv = inverse(G) - Jinv = dot(J[_,:,:], Ginv[:,_,:], -1) - else: - raise Exception('cannot invert {}x{} jacobian'.format(J.shape)) - return dot(rootgradient(self, roots)[...,_], Jinv, -2) - -def dotnorm(arg, geom, axis=-1): - axis = numeric.normdim(arg.ndim, axis) - assert geom.ndim == 1 and geom.shape[0] == arg.shape[axis] - return dot(arg, normal(geom)[(slice(None),)+(_,)*(arg.ndim-axis-1)], axis) - if __name__ == '__main__': # Diagnostics for the development for simplify operations. simplify_priority = ( From 6b5e75d7ab76b655767538fa78aca17d622c1149 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sat, 6 Jun 2020 14:51:53 +0200 Subject: [PATCH 083/119] add {append,prepend}axes helper functions --- nutils/function.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index daa6aa5c1..20f23f1ac 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4973,6 +4973,22 @@ def dotnorm(arg, geom, axis=-1): assert geom.ndim == 1 and geom.shape[0] == arg.shape[axis] return dot(arg, normal(geom)[(slice(None),)+(_,)*(arg.ndim-axis-1)], axis) +def prependaxes(func, shape): + 'Prepend axes with specified `shape` to `func`.' + + func = asarray(func) + for i, n in enumerate(shape): + func = insertaxis(func, i, n) + return func + +def appendaxes(func, shape): + 'Append axes with specified `shape` to `func`.' + + func = asarray(func) + for n in shape: + func = insertaxis(func, func.ndim, n) + return func + @replace def replace_arguments(value, arguments): '''Replace :class:`Argument` objects in ``value``. From 11acf2d65c659c468cd762911d99c061cf7f888a Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 8 Jun 2020 10:48:45 +0200 Subject: [PATCH 084/119] add Pointwise.outer constructor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To prepare for the upcoming change in interpretation of multiple arguments of functions in expressions — given pointwise product `mul`, `mul(x_i, y_j)` is equal to `x_i y_j` — this patch adds an `outer` constructor to all subclasses of `Pointwise` that applies the required alignment to its arguments. --- nutils/function.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index 20f23f1ac..ab8a5e66e 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -2322,6 +2322,23 @@ def __init__(self, *args:asarrays): self.args = args super().__init__(args=args, shape=shape, dtype=retval.dtype) + @classmethod + def outer(cls, *args): + '''Alternative constructor that outer-aligns the arguments. + + The output shape of this pointwise function is the sum of all shapes of its + arguments. When called with multiple arguments, the first argument will be + appended with singleton axes to match the output shape, the second argument + will be prepended with as many singleton axes as the dimension of the + original first argument and appended to match the output shape, and so + forth and so on. + ''' + + args = tuple(map(asarray, args)) + shape = builtins.sum((arg.shape for arg in args), ()) + offsets = numpy.cumsum([0]+[arg.ndim for arg in args]) + return cls(*(prependaxes(appendaxes(arg, shape[r:]), shape[:l]) for arg, l, r in zip(args, offsets[:-1], offsets[1:]))) + @property def simplified(self): args = [arg.simplified for arg in self.args] From 94746ed05ef4c76099b1bed962033c8ea9b9f0aa Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Fri, 5 Jun 2020 23:24:07 +0200 Subject: [PATCH 085/119] don't verify existence of func during expr parsing --- nutils/expression.py | 28 +++++++++++----------------- nutils/function.py | 7 +++---- tests/test_expression.py | 17 ++--------------- 3 files changed, 16 insertions(+), 36 deletions(-) diff --git a/nutils/expression.py b/nutils/expression.py index 4bd391e54..b10d74962 100644 --- a/nutils/expression.py +++ b/nutils/expression.py @@ -24,6 +24,7 @@ ''' import re, collections, functools +from . import warnings # Convenience function to create a constant in ExpressionAST (details in @@ -486,8 +487,6 @@ class _ExpressionParser: See argument ``expression`` of :func:`parse`. variables : :class:`dict` of :class:`str` and :class:`nutils.function.Array` pairs See argument ``variables`` of :func:`parse`. - functions : :class:`dict` of :class:`str` and :class:`int` pairs - See argument ``functions`` of :func:`parse`. arg_shapes : :class:`dict` of :class:`str` and :class:`tuple` or :class:`int`\\s pairs See argument ``arg_shapes`` of :func:`parse`. default_geometry_name : class:`str` @@ -499,10 +498,9 @@ class _ExpressionParser: eye_symbols = '$', 'δ' normal_symbols = 'n', - def __init__(self, expression, variables, functions, arg_shapes, default_geometry_name, fixed_lengths): + def __init__(self, expression, variables, arg_shapes, default_geometry_name, fixed_lengths): self.expression = expression self.variables = variables - self.functions = functions self.arg_shapes = dict(arg_shapes) self.default_geometry_name = default_geometry_name self.fixed_lengths = fixed_lengths @@ -721,17 +719,14 @@ def parse_var(self): elif self._next.type == 'variable': token = self._consume() name = token.data - if name in self.functions and name not in self.variables: # function (and not overriden as variable) - self._consume_assert_equal('(', msg="Expected '(' for function {}.".format(name)) + if name.startswith('?'): + indices = self._consume() if self._next.type == 'indices' else '' + value = self._get_arg(name[1:], indices) + elif name not in self.variables and self._next.type == '(': # assume function + self._consume() args = self.parse_comma_separated(end=')', parse_item=self.parse_subexpression) - nargs = self.functions[name] - if len(args) != nargs: - raise _IntermediateError('Function {!r} takes {}, got {}.'.format(name, _sp(nargs, 'argument', 'arguments'), len(args))) args = _Array.align(*args) value = args[0].replace(ast=('call', _(name))+tuple(arg.ast for arg in args)) - elif name.startswith('?'): - indices = self._consume() if self._next.type == 'indices' else '' - value = self._get_arg(name[1:], indices) else: raw = self._get_variable(name) indices = self._consume() if self._next.type == 'indices' else '' @@ -1078,7 +1073,7 @@ def _replace_lengths(ast, lengths): return ast -def parse(expression, variables, functions, indices, arg_shapes={}, default_geometry_name='x', fixed_lengths=None, fallback_length=None): +def parse(expression, variables, indices, arg_shapes={}, default_geometry_name='x', fixed_lengths=None, fallback_length=None, functions=None): '''Parse ``expression`` and return AST. This function parses a tensor expression with `Einstein Summation @@ -1232,9 +1227,6 @@ def parse(expression, variables, functions, indices, arg_shapes={}, default_geom variables : :class:`dict` of :class:`str` and :class:`nutils.function.Array` pairs A :class:`dict` of variable names and array pairs. All variables used in the ``expression`` should exist in ``variables``. - functions : :class:`dict` of :class:`str` and :class:`int` pairs - A :class:`dict` of function names and number of arguments pairs. All - functions used in the ``expression`` should exist in ``functions``. indices : :class:`str` The indices used for aligning the resulting array. For example, let ``expression`` be ``'a_ij'``. If ``indices`` is ``'ij'``, then the @@ -1295,7 +1287,9 @@ def parse(expression, variables, functions, indices, arg_shapes={}, default_geom ``expression``. ''' - parser = _ExpressionParser(expression, variables, functions, arg_shapes, default_geometry_name, fixed_lengths or {}) + if functions is not None: + warnings.deprecation('argument `functions` is deprecated; the existence and number of arguments is not checked during parsing') + parser = _ExpressionParser(expression, variables, arg_shapes, default_geometry_name, fixed_lengths or {}) parser.tokenize() value = parser.parse_subexpression() parser._consume_assert_equal('EOF', msg='Unexpected symbol at end of expression.') diff --git a/nutils/function.py b/nutils/function.py index ab8a5e66e..e1cb5789e 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -5220,7 +5220,6 @@ class Namespace: exp=exp, abs=abs, ln=ln, log=ln, log2=log2, log10=log10, sqrt=sqrt, sign=sign, ) - _functions_nargs = {k: len(inspect.signature(v).parameters) for k, v in _functions.items()} @types.apply_annotations def __init__(self, *, default_geometry_name='x', fallback_length:types.strictint=None, **kwargs): @@ -5300,7 +5299,7 @@ def __getattr__(self, name): '''Get attribute ``name``.''' if name.startswith('eval_'): - return lambda expr: _eval_ast(expression.parse(expr, variables=self._attributes, functions=self._functions_nargs, indices=name[5:], arg_shapes=self._arg_shapes, default_geometry_name=self.default_geometry_name, fixed_lengths=self._fixed_lengths, fallback_length=self._fallback_length)[0], self._functions) + return lambda expr: _eval_ast(expression.parse(expr, variables=self._attributes, indices=name[5:], arg_shapes=self._arg_shapes, default_geometry_name=self.default_geometry_name, fixed_lengths=self._fixed_lengths, fallback_length=self._fallback_length)[0], self._functions) try: return self._attributes[name] except KeyError: @@ -5319,7 +5318,7 @@ def __setattr__(self, name, value): name, indices = m.groups() indices = indices[1:] if indices else '' if isinstance(value, str): - ast, arg_shapes = expression.parse(value, variables=self._attributes, functions=self._functions_nargs, indices=indices, arg_shapes=self._arg_shapes, default_geometry_name=self.default_geometry_name, fixed_lengths=self._fixed_lengths, fallback_length=self._fallback_length) + ast, arg_shapes = expression.parse(value, variables=self._attributes, indices=indices, arg_shapes=self._arg_shapes, default_geometry_name=self.default_geometry_name, fixed_lengths=self._fixed_lengths, fallback_length=self._fallback_length) value = _eval_ast(ast, self._functions) self._arg_shapes.update(arg_shapes) else: @@ -5344,7 +5343,7 @@ def __rmatmul__(self, expr): if not isinstance(expr, str): return NotImplemented try: - ast = expression.parse(expr, variables=self._attributes, functions=self._functions_nargs, indices=None, arg_shapes=self._arg_shapes, default_geometry_name=self.default_geometry_name, fixed_lengths=self._fixed_lengths, fallback_length=self._fallback_length)[0] + ast = expression.parse(expr, variables=self._attributes, indices=None, arg_shapes=self._arg_shapes, default_geometry_name=self.default_geometry_name, fixed_lengths=self._fixed_lengths, fallback_length=self._fallback_length)[0] except expression.AmbiguousAlignmentError: raise ValueError('`expression @ Namespace` cannot be used because the expression has more than one dimension. Use `Namespace.eval_...(expression)` instead') return _eval_ast(ast, self._functions) diff --git a/tests/test_expression.py b/tests/test_expression.py index 938f43145..a6762d09a 100644 --- a/tests/test_expression.py +++ b/tests/test_expression.py @@ -52,18 +52,17 @@ def get(self, name, default): return default v = Variables(x=Array('x', [2]), altgeom=Array('altgeom', [3]), funcoverride=Array('funcoverride', [])) -functions = dict(func1=1, func2=2, func3=3, funcoverride=1) class parse(TestCase): def assert_ast(self, expression, indices, ast, variables=None, **parse_kwargs): if variables is None: variables = v - self.assertEqual(nutils.expression.parse(expression, variables, functions, indices, **parse_kwargs)[0], ast) + self.assertEqual(nutils.expression.parse(expression, variables, indices, **parse_kwargs)[0], ast) def assert_syntax_error(self, msg, expression, indices, highlight, arg_shapes={}, fixed_lengths=None, exccls=nutils.expression.ExpressionSyntaxError): with self.assertRaises(exccls) as cm: - nutils.expression.parse(expression, v, functions, indices, arg_shapes, fixed_lengths=fixed_lengths) + nutils.expression.parse(expression, v, indices, arg_shapes, fixed_lengths=fixed_lengths) self.assertEqual(str(cm.exception), msg + '\n' + expression + '\n' + highlight) # OTHER @@ -753,12 +752,6 @@ def test_function_1d_1d(self): self.assert_ast('func2(a2_i, a2_i)', 'i', ('call' def test_function_2d_2d(self): self.assert_ast('func2(a23_ij, a32_ji)', 'ij', ('call', _('func2'), v._a23, ('transpose', v._a32, _((1,0))))) def test_function_2d_2d_2d(self): self.assert_ast('func3(a23_ij, a22_ik a23_kj, a23_ij)', 'ij', ('call', _('func3'), v._a23, ('sum', ('mul', ('append_axis', v._a22, _(3)), ('transpose', ('append_axis', v._a23, _(2)), _((2,0,1)))), _(1)), v._a23)) - def test_function_invalid_nargs(self): - self.assert_syntax_error( - "Function 'func1' takes 1 argument, got 2.", - "1 + func1(a, a) + 1", "", - " ^^^^^^^^^^^") - def test_function_unmatched_indices(self): self.assert_syntax_error( "Cannot align arrays with unmatched indices: ij, ij, jk.", @@ -771,12 +764,6 @@ def test_function_unmatched_shape(self): "1_ij + func2(a23_ij, a33_ij) + 1_ij", "ij", " ^^^^^^^^^^^^^^^^^^^^^") - def test_function_unknown(self): - self.assert_syntax_error( - "Unknown variable: 'funcX'.", - "1_ij + funcX(a23_ij) + 1_ij", "ij", - " ^^^^^") - def test_function_override(self): self.assert_syntax_error( "Expected '='.", From c75701b17b5583787d73fe1850f997e89d668b15 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sat, 6 Jun 2020 14:06:36 +0200 Subject: [PATCH 086/119] use summation convention for func calls Currently the expression syntax supports function calls with multiple arguments where each argument has the same shape. In a follow-up commit the function syntax will be used for derivatives: `d(u_i, x_j)` and `d(v, x_i, x_i)` as alternatives for `u_i,j` and `v_,ii`. This commit applies summation of repeated indices to function calls, which is necessary for the derivatives. Since `arctan2` is the only function available in the namespace that takes more than one argument, the impact of this change is assumed to be negligible. --- nutils/expression.py | 20 ++++++++++++-------- nutils/function.py | 9 +++++++-- tests/test_expression.py | 21 +++++++++++---------- tests/test_function.py | 5 +++++ 4 files changed, 35 insertions(+), 20 deletions(-) diff --git a/nutils/expression.py b/nutils/expression.py index b10d74962..0d746b1c2 100644 --- a/nutils/expression.py +++ b/nutils/expression.py @@ -23,7 +23,7 @@ expression. ''' -import re, collections, functools +import re, collections, functools, operator from . import warnings @@ -725,8 +725,12 @@ def parse_var(self): elif name not in self.variables and self._next.type == '(': # assume function self._consume() args = self.parse_comma_separated(end=')', parse_item=self.parse_subexpression) - args = _Array.align(*args) - value = args[0].replace(ast=('call', _(name))+tuple(arg.ast for arg in args)) + value = _Array._apply_indices(ast=('call', _(name), *(arg.ast for arg in args)), + offset=0, + indices=''.join(arg.indices for arg in args), + shape=sum((arg.shape for arg in args), ()), + summed=functools.reduce(operator.or_, (arg.summed for arg in args), frozenset()), + linked_lengths=functools.reduce(operator.or_, (arg.linked_lengths for arg in args), frozenset())) else: raw = self._get_variable(name) indices = self._consume() if self._next.type == 'indices' else '' @@ -1203,11 +1207,11 @@ def parse(expression, variables, indices, arg_shapes={}, default_geometry_name=' for a variable name — directly followed by the left parenthesis ``(``, without a space. The arguments to the function are separated by a comma and at least one space. The function is applied pointwise to the - arguments and all arguments should have the same shape. Example: - ``f(x_i, y_i)``.denotes the call to function ``f`` with arguments ``x_i`` - and ``y_i``. Functions and variables share a namespace: defining a - variable with the same name as a function renders the function - inaccessible. + arguments and summation convection is applied to the result. Example: + assume ``mul(...)`` returns the product of its arguments, then ``mul(x_i, + y_j)`` is equivalent to ``x_i y_j`` and ``mul(x_i, y_i)`` to ``x_i y_i``. + Functions and variables share a namespace: defining a variable with the + same name as a function renders the function inaccessible. * A **stack** of two or more arrays along an axis is denoted by a ``<`` followed by comma and space separated arrays followed by ``>`` and an diff --git a/nutils/function.py b/nutils/function.py index e1cb5789e..83e3d2dd9 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -5060,7 +5060,12 @@ def _eval_ast(ast, functions): return replace_arguments(array, subs) elif op == 'call': func, *args = args - return functions[func](*args) + args = tuple(map(asarray, args)) + shape = builtins.sum((arg.shape for arg in args), ()) + result = functions[func](*args) + if result.shape != shape: + raise ValueError('expected an array with shape {} when calling {} but got {}'.format(shape, func, result.shape)) + return result elif op == 'jacobian': geom, ndims = args return J(geom, ndims) @@ -5216,7 +5221,7 @@ class Namespace: _functions = dict( opposite=opposite, sin=sin, cos=cos, tan=tan, sinh=sinh, cosh=cosh, - tanh=tanh, arcsin=arcsin, arccos=arccos, arctan=arctan, arctan2=arctan2, arctanh=arctanh, + tanh=tanh, arcsin=arcsin, arccos=arccos, arctan=arctan, arctan2=ArcTan2.outer, arctanh=arctanh, exp=exp, abs=abs, ln=ln, log=ln, log2=log2, log10=log10, sqrt=sqrt, sign=sign, ) diff --git a/tests/test_expression.py b/tests/test_expression.py index a6762d09a..a6479e2b9 100644 --- a/tests/test_expression.py +++ b/tests/test_expression.py @@ -744,25 +744,26 @@ def test_stack_whitespace_before_comma(self): self.assert_ast('_i', 'i', # FUNCTION - def test_function_0d(self): self.assert_ast('func1(a)', '', ('call', _('func1'), v._a)) + def test_function(self): self.assert_ast('func1(a)', '', ('call', _('func1'), v._a)) def test_function_1d(self): self.assert_ast('func1(a2_i)', 'i', ('call', _('func1'), v._a2)) def test_function_2d(self): self.assert_ast('func1(a23_ij)', 'ij', ('call', _('func1'), v._a23)) def test_function_0d_0d(self): self.assert_ast('func2(a, a)', '', ('call', _('func2'), v._a, v._a)) - def test_function_1d_1d(self): self.assert_ast('func2(a2_i, a2_i)', 'i', ('call', _('func2'), v._a2, v._a2)) - def test_function_2d_2d(self): self.assert_ast('func2(a23_ij, a32_ji)', 'ij', ('call', _('func2'), v._a23, ('transpose', v._a32, _((1,0))))) - def test_function_2d_2d_2d(self): self.assert_ast('func3(a23_ij, a22_ik a23_kj, a23_ij)', 'ij', ('call', _('func3'), v._a23, ('sum', ('mul', ('append_axis', v._a22, _(3)), ('transpose', ('append_axis', v._a23, _(2)), _((2,0,1)))), _(1)), v._a23)) + def test_function_1d_1d(self): self.assert_ast('func2(a2_i, a2_j)', 'ij', ('call', _('func2'), v._a2, v._a2)) + def test_function_1d_1d_trace(self): self.assert_ast('func2(a2_i, a2_i)', '', ('trace', ('call', _('func2'), v._a2, v._a2), _(0), _(1))) + def test_function_2d_2d(self): self.assert_ast('func2(a23_ij, a32_kl)', 'ijkl', ('call', _('func2'), v._a23, v._a32)) + def test_function_1d_1d_2d(self): self.assert_ast('func3(a2_i, a2_j, a23_kl)', 'ijkl', ('call', _('func3'), v._a2, v._a2, v._a23)) - def test_function_unmatched_indices(self): + def test_function_triple_index(self): self.assert_syntax_error( - "Cannot align arrays with unmatched indices: ij, ij, jk.", - "1_ij + func3(a23_ij, a23_ij, a23_jk) + 1_ij", "ij", - " ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^") + "Index 'i' occurs more than twice.", + "1_i + func(a2_i, a2_i, a2_i) + 1_i", "i", + " ^^^^^^^^^^^^^^^^^^^^^^") def test_function_unmatched_shape(self): self.assert_syntax_error( "Shapes at index 'i' differ: 2, 3.", - "1_ij + func2(a23_ij, a33_ij) + 1_ij", "ij", - " ^^^^^^^^^^^^^^^^^^^^^") + "1 + func2(a23_ij, a33_ij) + 1", "", + " ^^^^^^^^^^^^^^^^^^^^^") def test_function_override(self): self.assert_syntax_error( diff --git a/tests/test_function.py b/tests/test_function.py index 1365b40bf..fa8c3a34c 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -860,6 +860,7 @@ def test_arg(self): self.assertIdentical('a2_i ?x_i', function.dot(self.ns.a2, f def test_substitute(self): self.assertIdentical('(?x_i^2)(x_i=a2_i)', self.ns.a2**2) def test_multisubstitute(self): self.assertIdentical('(a2_i + ?x_i + ?y_i)(x_i=?y_i, y_i=?x_i)', self.ns.a2 + function.Argument('y', [2]) + function.Argument('x', [2])) def test_call(self): self.assertIdentical('sin(a)', function.sin(self.ns.a)) + def test_call2(self): self.assertEqual(self.ns.eval_ij('arctan2(a2_i, a3_j)').simplified, function.arctan2(self.ns.a2[:,None], self.ns.a3[None,:]).simplified) def test_eye(self): self.assertIdentical('δ_ij a2_i', function.dot(function.eye(2), self.ns.a2, axes=[0])) def test_normal(self): self.assertIdentical('n_i', self.ns.x.normal()) def test_getitem(self): self.assertIdentical('a2_0', self.ns.a2[0]) @@ -884,6 +885,10 @@ def test_unknown_opcode(self): with self.assertRaises(ValueError): function._eval_ast(('invalid-opcode',), {}) + def test_call_invalid_shape(self): + with self.assertRaisesRegex(ValueError, '^expected an array with shape'): + function._eval_ast(('call', (None, 'f'), (None, function.Zeros((2,), float)), (None, function.Zeros((3,), float))), + dict(f=lambda a, b: a[None,:] * b[:,None])) # result is transposed @parametrize class jacobian(TestCase): From debb180303558d7fd650bf8dd8ed7231a6a10360 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 9 Jun 2020 23:24:51 +0200 Subject: [PATCH 087/119] allow n as function in expressions Currently `n` in an expression is treated as a special token in the expression parser, which renders using `n` as a function impossible. This patch removes the normal token and labels `n` as variable(/function) instead. --- nutils/expression.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/nutils/expression.py b/nutils/expression.py index 0d746b1c2..e5e247fa9 100644 --- a/nutils/expression.py +++ b/nutils/expression.py @@ -707,15 +707,6 @@ def parse_var(self): indices = self._consume() if self._next.type == 'indices' else '' length = _Length(self._current.pos) value = self._asarray(('eye', _(length)), indices, (length, length)) - elif self._next.type == 'normal': - self._consume() - if self._next.type == 'geometry': - geometry_name = self._consume().data - else: - geometry_name = self.default_geometry_name - geom = self._get_geometry(geometry_name) - indices = self._consume() if self._next.type == 'indices' else '' - value = self._asarray(('normal', _(geom)), indices, geom.shape) elif self._next.type == 'variable': token = self._consume() name = token.data @@ -731,6 +722,14 @@ def parse_var(self): shape=sum((arg.shape for arg in args), ()), summed=functools.reduce(operator.or_, (arg.summed for arg in args), frozenset()), linked_lengths=functools.reduce(operator.or_, (arg.linked_lengths for arg in args), frozenset())) + elif name in self.normal_symbols: + if self._next.type == 'geometry': + geometry_name = self._consume().data + else: + geometry_name = self.default_geometry_name + geom = self._get_geometry(geometry_name) + indices = self._consume() if self._next.type == 'indices' else '' + value = self._asarray(('normal', _(geom)), indices, geom.shape) else: raw = self._get_variable(name) indices = self._consume() if self._next.type == 'indices' else '' @@ -980,7 +979,7 @@ def tokenize(self): continue m = re.match(r'({}):([a-zA-Zα-ωΑ-Ω][a-zA-Zα-ωΑ-Ω0-9]*)_([a-zA-Z0-9])'.format('|'.join(map(re.escape, self.normal_symbols))), self.expression[pos:]) if m: - tokens.append(_Token('normal', m.group(1), pos)) + tokens.append(_Token('variable', m.group(1), pos)) tokens.append(_Token('geometry', m.group(2), pos+m.start(2))) tokens.append(_Token('indices', m.group(3), pos+m.start(3))) pos += m.end() @@ -994,10 +993,6 @@ def tokenize(self): pos += len(m_eye) continue m_normal = _string_startswith(self.expression, self.normal_symbols, start=pos) - if m_normal and len(m_variable) <= len(m_normal): - tokens.append(_Token('normal', m_normal, pos)) - pos += len(m_normal) - continue if m_variable: tokens.append(_Token('variable', m_variable, pos)) pos += len(m_variable) From 2c4cf8b15f3faac6f326a5754419c11bd1bc8882 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 8 Jun 2020 11:04:05 +0200 Subject: [PATCH 088/119] add `d(func, geom, ...)` as alt syntax for derivs --- nutils/function.py | 9 ++++++++- tests/test_function.py | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/nutils/function.py b/nutils/function.py index 83e3d2dd9..8d399be8e 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4990,6 +4990,13 @@ def dotnorm(arg, geom, axis=-1): assert geom.ndim == 1 and geom.shape[0] == arg.shape[axis] return dot(arg, normal(geom)[(slice(None),)+(_,)*(arg.ndim-axis-1)], axis) +def _d1(arg, var): + return (derivative if isinstance(var, Argument) else grad)(arg, var) + +def d(arg, *vars): + 'derivative of `arg` to `vars`' + return functools.reduce(_d1, vars, arg) + def prependaxes(func, shape): 'Prepend axes with specified `shape` to `func`.' @@ -5223,7 +5230,7 @@ class Namespace: opposite=opposite, sin=sin, cos=cos, tan=tan, sinh=sinh, cosh=cosh, tanh=tanh, arcsin=arcsin, arccos=arccos, arctan=arctan, arctan2=ArcTan2.outer, arctanh=arctanh, exp=exp, abs=abs, ln=ln, log=ln, log2=log2, log10=log10, sqrt=sqrt, - sign=sign, + sign=sign, d=d ) @types.apply_annotations diff --git a/tests/test_function.py b/tests/test_function.py index fa8c3a34c..2552a6195 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -836,6 +836,16 @@ def test_unexpected_keyword_argument(self): with self.assertRaisesRegex(TypeError, r"^__init__\(\) got an unexpected keyword argument 'test'$"): function.Namespace(test=2) + def test_d_geom(self): + ns = function.Namespace() + topo, ns.x = mesh.rectilinear([1]) + self.assertEqual(ns.eval_ij('d(x_i, x_j)'), function.grad(ns.x, ns.x)) + + def test_d_arg(self): + ns = function.Namespace() + ns.a = '?a' + self.assertEqual(ns.eval_('d(2 ?a + 1, ?a)').simplified, function.asarray(2.)) + class eval_ast(TestCase): def setUp(self): From e384cbf81c1955e1fea2f7392821e13930e7dcd7 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 9 Jun 2020 23:11:22 +0200 Subject: [PATCH 089/119] add `n(x_i)` as alt syntax for normal --- nutils/function.py | 2 +- tests/test_function.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/nutils/function.py b/nutils/function.py index 8d399be8e..7b99d312e 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -5230,7 +5230,7 @@ class Namespace: opposite=opposite, sin=sin, cos=cos, tan=tan, sinh=sinh, cosh=cosh, tanh=tanh, arcsin=arcsin, arccos=arccos, arctan=arctan, arctan2=ArcTan2.outer, arctanh=arctanh, exp=exp, abs=abs, ln=ln, log=ln, log2=log2, log10=log10, sqrt=sqrt, - sign=sign, d=d + sign=sign, d=d, n=normal, ) @types.apply_annotations diff --git a/tests/test_function.py b/tests/test_function.py index 2552a6195..9f91a4eaa 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -846,6 +846,11 @@ def test_d_arg(self): ns.a = '?a' self.assertEqual(ns.eval_('d(2 ?a + 1, ?a)').simplified, function.asarray(2.)) + def test_n(self): + ns = function.Namespace() + topo, ns.x = mesh.rectilinear([1]) + self.assertEqual(ns.eval_i('n(x_i)'), function.normal(ns.x)) + class eval_ast(TestCase): def setUp(self): From 2a1891074d15c86554f2a19e8a28bd886239da98 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 8 Jun 2020 11:31:16 +0200 Subject: [PATCH 090/119] support user-defined functions in `Namespace` --- nutils/function.py | 31 +++++++++++++++++++++++++++---- tests/test_function.py | 16 ++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 7b99d312e..20d13ee30 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -5202,6 +5202,24 @@ class Namespace: ``tanh``, ``arcsin``, ``arccos``, ``arctan2``, ``arctanh``, ``exp``, ``abs``, ``ln``, ``log``, ``log2``, ``log10``, ``sqrt`` and ``sign``. + Additional pointwise functions can be passed to argument ``functions``. All + functions should take :class:`Array` objects as arguments and must return an + :class:`Array` with as shape the sum of all shapes of the arguments. + + >>> def sqr(a): + ... return a**2 + >>> def mul(a, b): + ... return a[(...,)+(None,)*b.ndim] * b[(None,)*a.ndim] + >>> ns_funcs = function.Namespace(functions=dict(sqr=sqr, mul=mul)) + >>> ns_funcs.a = numpy.array([1,2,3]) + >>> ns_funcs.b = numpy.array([4,5]) + >>> 'sqr(a_i)' @ ns_funcs # same as 'a_i^2' + Array<3> + >>> ns_funcs.eval_ij('mul(a_i, b_j)') # same as 'a_i b_j' + Array<3,2> + >>> 'mul(a_i, a_i)' @ ns_funcs # same as 'a_i a_i' + Array<> + Args ---- default_geometry_name : :class:`str` @@ -5213,6 +5231,10 @@ class Namespace: length_ : :class:`int` The fixed length of ````. All axes in the expression marked with one of the ```` are asserted to have the specified length. + functions : :class:`dict`, optional + Pointwise functions that should be available in the namespace, + supplementing the default functions listed above. All functions should + return arrays with as shape the sum of all shapes of the arguments. Attributes ---------- @@ -5222,11 +5244,11 @@ class Namespace: The name of the default geometry. See argument with the same name. ''' - __slots__ = '_attributes', '_arg_shapes', 'default_geometry_name', '_fixed_lengths', '_fallback_length' + __slots__ = '_attributes', '_arg_shapes', 'default_geometry_name', '_fixed_lengths', '_fallback_length', '_functions' _re_assign = re.compile('^([a-zA-Zα-ωΑ-Ω][a-zA-Zα-ωΑ-Ω0-9]*)(_[a-z]+)?$') - _functions = dict( + _default_functions = dict( opposite=opposite, sin=sin, cos=cos, tan=tan, sinh=sinh, cosh=cosh, tanh=tanh, arcsin=arcsin, arccos=arccos, arctan=arctan, arctan2=ArcTan2.outer, arctanh=arctanh, exp=exp, abs=abs, ln=ln, log=ln, log2=log2, log10=log10, sqrt=sqrt, @@ -5234,7 +5256,7 @@ class Namespace: ) @types.apply_annotations - def __init__(self, *, default_geometry_name='x', fallback_length:types.strictint=None, **kwargs): + def __init__(self, *, default_geometry_name='x', fallback_length:types.strictint=None, functions=None, **kwargs): if not isinstance(default_geometry_name, str): raise ValueError('default_geometry_name: Expected a str, got {!r}.'.format(default_geometry_name)) if '_' in default_geometry_name or not self._re_assign.match(default_geometry_name): @@ -5252,11 +5274,12 @@ def __init__(self, *, default_geometry_name='x', fallback_length:types.strictint super().__setattr__('_fixed_lengths', types.frozendict({i: l for indices, l in fixed_lengths.items() for i in indices} if fixed_lengths else {})) super().__setattr__('_fallback_length', fallback_length) super().__setattr__('default_geometry_name', default_geometry_name) + super().__setattr__('_functions', dict(itertools.chain(self._default_functions.items(), () if functions is None else functions.items()))) super().__init__() def __getstate__(self): 'Pickle instructions' - attrs = '_arg_shapes', '_attributes', 'default_geometry_name', '_fixed_lengths', '_fallback_length' + attrs = '_arg_shapes', '_attributes', 'default_geometry_name', '_fixed_lengths', '_fallback_length', '_functions' return {k: getattr(self, k) for k in attrs} def __setstate__(self, d): diff --git a/tests/test_function.py b/tests/test_function.py index 9f91a4eaa..b0ef4c3c4 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -851,6 +851,22 @@ def test_n(self): topo, ns.x = mesh.rectilinear([1]) self.assertEqual(ns.eval_i('n(x_i)'), function.normal(ns.x)) + def test_functions(self): + def sqr(a): + return a**2 + def mul(*args): + if len(args) == 2: + return args[0][(...,)+(None,)*args[1].ndim] * args[1][(None,)*args[0].ndim] + else: + return mul(mul(args[0], args[1]), *args[2:]) + ns = function.Namespace(functions=dict(sqr=sqr, mul=mul)) + ns.a = numpy.array([1, 2, 3]) + ns.b = numpy.array([4, 5]) + ns.A = numpy.array([[6, 7, 8], [9, 10, 11]]) + self.assertEqual(ns.eval_i('sqr(a_i)').shape, (3,)) + self.assertEqual(ns.eval_ij('mul(a_i, b_j)').shape, (3,2)) + self.assertEqual(ns.eval_('mul(b_i, A_ij, a_j)').shape, ()) + class eval_ast(TestCase): def setUp(self): From 87c81ebee8471ffe8f99ea673ff48e5f8a4bae99 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 9 Jun 2020 22:37:49 +0200 Subject: [PATCH 091/119] support grad to nd geom Given a product topology of space and time, it is more convenient to write a derivative to time as `d(f, t)` (expression syntax) than as `d(f, t_0)`, where `t` is the 0D time or the 1D time with shape `(1,)` respectively. This patch adds support for the former and for completeness for an nD geometry. --- nutils/function.py | 39 +++++++++++++++++++++++---------------- tests/test_function.py | 24 ++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index 20d13ee30..a0e02b178 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4967,23 +4967,30 @@ def normal(arg, exterior=False): return cross(lgrad[:,0], lgrad[:,1], axis=0).normalized() raise NotImplementedError -def grad(self, geom, ndims=0): - assert geom.ndim == 1 - if ndims <= 0: - ndims += geom.shape[0] - # Order the roots deterministically. In the future we should use the order - # of `Sample.roots` (during `prepare_eval` or a successor). - roots = tuple(sorted(geom.roots, key=lambda root: (root.name, root.ndims))) - J = rootgradient(geom, roots) - if J.shape[0] == J.shape[1]: - Jinv = inverse(J) - elif J.shape[0] == J.shape[1] + 1: # gamma gradient - G = dot(J[:,:,_], J[:,_,:], 0) - Ginv = inverse(G) - Jinv = dot(J[_,:,:], Ginv[:,_,:], -1) +def grad(func, geom, ndims=0): + geom = asarray(geom) + if geom.ndim == 0: + return grad(func, insertaxis(geom, 0, 1), ndims)[...,0] + elif geom.ndim > 1: + func = asarray(func) + sh = geom.shape[-2:] + return unravel(grad(func, ravel(geom, geom.ndim-2), ndims), func.ndim+geom.ndim-2, sh) else: - raise Exception('cannot invert {}x{} jacobian'.format(J.shape)) - return dot(rootgradient(self, roots)[...,_], Jinv, -2) + if ndims <= 0: + ndims += geom.shape[0] + # Order the roots deterministically. In the future we should use the order + # of `Sample.roots` (during `prepare_eval` or a successor). + roots = tuple(sorted(geom.roots, key=lambda root: (root.name, root.ndims))) + J = rootgradient(geom, roots) + if J.shape[0] == J.shape[1]: + Jinv = inverse(J) + elif J.shape[0] == J.shape[1] + 1: # gamma gradient + G = dot(J[:,:,_], J[:,_,:], 0) + Ginv = inverse(G) + Jinv = dot(J[_,:,:], Ginv[:,_,:], -1) + else: + raise Exception('cannot invert {}x{} jacobian'.format(J.shape)) + return dot(rootgradient(func, roots)[...,_], Jinv, -2) def dotnorm(arg, geom, axis=-1): axis = numeric.normdim(arg.ndim, axis) diff --git a/tests/test_function.py b/tests/test_function.py index b0ef4c3c4..e7d9007fb 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -958,6 +958,30 @@ def test_zeroderivative(self): jacobian(delayed=True) jacobian(delayed=False) +class grad(TestCase): + + def assertEvalAlmostEqual(self, topo, factual, fdesired): + actual, desired = topo.sample('uniform', 2).eval([function.asarray(factual), function.asarray(fdesired)]) + self.assertAllAlmostEqual(actual, desired) + + def test_0d(self): + domain, (x,) = mesh.rectilinear([1]) + self.assertEvalAlmostEqual(domain, function.grad(x**2, x), 2*x) + + def test_1d(self): + domain, x = mesh.rectilinear([1]*2) + self.assertEvalAlmostEqual(domain, function.grad([x[0]**2, x[1]**2], x), [[2*x[0], 0], [0, 2*x[1]]]) + + def test_2d(self): + domain, x = mesh.rectilinear([1]*4) + x = function.unravel(x, 0, (2, 2)) + self.assertEvalAlmostEqual(domain, function.grad(x, x), numpy.eye(4, 4).reshape(2, 2, 2, 2)) + + def test_3d(self): + domain, x = mesh.rectilinear([1]*4) + x = function.unravel(function.unravel(x, 0, (2, 2)), 0, (2, 1)) + self.assertEvalAlmostEqual(domain, function.grad(x, x), numpy.eye(4, 4).reshape(2, 1, 2, 2, 1, 2)) + class CommonBasis: def setUp(self): From e9ad4c219f0ac07ed68fa2b1f2bce1bb712ced55 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 9 Jun 2020 23:00:07 +0200 Subject: [PATCH 092/119] support normal of nd geom Given a product topology of space and time, it is more convenient to write the normal of time as `n(t)` (expression syntax) than as `n(t_0)`, where `t` is the 0D time or the 1D time with shape `(1,)` respectively. This patch adds support for the former and for completeness for an nD geometry. --- nutils/function.py | 31 +++++++++++++++++++------------ tests/test_function.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/nutils/function.py b/nutils/function.py index a0e02b178..eab137658 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4954,18 +4954,25 @@ def ravel(func, axis): return Ravel(func, axis) def normal(arg, exterior=False): - assert arg.ndim == 1 - if not exterior: - return DelayedNormal(arg) - # Order the roots deterministically. In the future we should use the order - # of `Sample.roots` (during `prepare_eval` or a successor). - roots = tuple(sorted(arg.roots, key=lambda root: (root.name, root.ndims))) - lgrad = rootgradient(arg, roots) - if len(arg) == 2: - return asarray([lgrad[1,0], -lgrad[0,0]]).normalized() - if len(arg) == 3: - return cross(lgrad[:,0], lgrad[:,1], axis=0).normalized() - raise NotImplementedError + arg = asarray(arg) + if arg.ndim == 0: + return normal(insertaxis(arg, 0, 1), exterior)[...,0] + elif arg.ndim > 1: + arg = asarray(arg) + sh = arg.shape[-2:] + return unravel(normal(ravel(arg, arg.ndim-2), exterior), arg.ndim-2, sh) + else: + if not exterior: + return DelayedNormal(arg) + # Order the roots deterministically. In the future we should use the order + # of `Sample.roots` (during `prepare_eval` or a successor). + roots = tuple(sorted(arg.roots, key=lambda root: (root.name, root.ndims))) + lgrad = rootgradient(arg, roots) + if len(arg) == 2: + return asarray([lgrad[1,0], -lgrad[0,0]]).normalized() + if len(arg) == 3: + return cross(lgrad[:,0], lgrad[:,1], axis=0).normalized() + raise NotImplementedError def grad(func, geom, ndims=0): geom = asarray(geom) diff --git a/tests/test_function.py b/tests/test_function.py index e7d9007fb..3ed939459 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -982,6 +982,34 @@ def test_3d(self): x = function.unravel(function.unravel(x, 0, (2, 2)), 0, (2, 1)) self.assertEvalAlmostEqual(domain, function.grad(x, x), numpy.eye(4, 4).reshape(2, 1, 2, 2, 1, 2)) +class normal(TestCase): + + def assertEvalAlmostEqual(self, topo, factual, fdesired): + actual, desired = topo.sample('uniform', 2).eval([function.asarray(factual), function.asarray(fdesired)]) + self.assertAllAlmostEqual(actual, desired) + + def test_0d(self): + domain, (x,) = mesh.rectilinear([1]) + self.assertEvalAlmostEqual(domain.boundary['right'], function.normal(x), 1) + self.assertEvalAlmostEqual(domain.boundary['left'], function.normal(x), -1) + + def test_1d(self): + domain, x = mesh.rectilinear([1]*2) + for bnd, n in ('right', [1, 0]), ('left', [-1, 0]), ('top', [0, 1]), ('bottom', [0, -1]): + self.assertEvalAlmostEqual(domain.boundary[bnd], function.normal(x), n) + + def test_2d(self): + domain, x = mesh.rectilinear([1]*2) + x = function.unravel(x, 0, [2, 1]) + for bnd, n in ('right', [1, 0]), ('left', [-1, 0]), ('top', [0, 1]), ('bottom', [0, -1]): + self.assertEvalAlmostEqual(domain.boundary[bnd], function.normal(x), numpy.array(n)[:,_]) + + def test_3d(self): + domain, x = mesh.rectilinear([1]*2) + x = function.unravel(function.unravel(x, 0, [2, 1]), 0, [1, 2]) + for bnd, n in ('right', [1, 0]), ('left', [-1, 0]), ('top', [0, 1]), ('bottom', [0, -1]): + self.assertEvalAlmostEqual(domain.boundary[bnd], function.normal(x), numpy.array(n)[_,:,_]) + class CommonBasis: def setUp(self): From a7831dac637a7275c5c13b3858f3ab1941439f98 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 10 Jun 2020 17:05:51 +0200 Subject: [PATCH 093/119] add Levi-Civita symbol --- nutils/function.py | 4 ++++ nutils/numeric.py | 22 ++++++++++++++++++++++ tests/test_numeric.py | 21 ++++++++++++++++++++- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/nutils/function.py b/nutils/function.py index eab137658..d738da485 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4713,6 +4713,10 @@ def trigtangent(angle): def eye(n, dtype=float): return diagonalize(ones([n], dtype=dtype)) +def levicivita(n: int, dtype=float): + 'n-dimensional Levi-Civita symbol.' + return Constant(numeric.levicivita(n)) + def insertaxis(arg, n, length): arg = asarray(arg) n = numeric.normdim(arg.ndim+1, n) diff --git a/nutils/numeric.py b/nutils/numeric.py index e9b9930d8..e3f0531ab 100644 --- a/nutils/numeric.py +++ b/nutils/numeric.py @@ -528,4 +528,26 @@ def gramschmidt(V): V[...,i] -= numpy.einsum('...ij,...j->...i', V[...,:i], numpy.einsum('...ji,...j->...i', V[...,:i], V[...,i])) V[...,i] /= numpy.linalg.norm(V[...,i], axis=-1)[...,numpy.newaxis] +def levicivita(n: int, dtype=float): + 'n-dimensional Levi-Civita symbol.' + if n < 2: + raise ValueError('The Levi-Civita symbol is undefined for dimensions lower than 2.') + # Generate all possible permutations of `{0,1,...,n-1}` in array `I`, where + # the second axis runs over the permutations, and determine the number of + # permutations (`nperms`). First, `I[k] ∈ {k,...,n-1}` becomes the index of + # dimension `k` for the partial permutation `I[k:]`. + I = numpy.mgrid[tuple(slice(k, n) for k in range(n))].reshape(n, -1) + # The number of permutations is equal to the number of deviations from the + # unpermuted case. + nperms = numpy.sum(numpy.not_equal(I, numpy.arange(n)[:,None]), 0) + # Make all partial permutations `I[k+1:]` unique by replacing `I[j]` with `k` + # if `I[j]` equals `I[k]`, `j > k`. Example with `n = 4`: if `I[2:] = [3,2]` and + # `I[1] = 2` then `I[3]` must be replaced with `1` to give `I[1:] = [2,3,1]`. + for k in reversed(range(n-1)): + I[k+1:][numpy.equal(I[k+1:], I[k,None])] = k + # Inflate with `1` if `nperms` is even and `-1` if odd. + result = numpy.zeros((n,)*n, dtype=dtype) + result[tuple(I)] = 1 - 2*(nperms % 2) + return result + # vim:sw=2:sts=2:et diff --git a/tests/test_numeric.py b/tests/test_numeric.py index 3e4d48e02..4bc6229bd 100644 --- a/tests/test_numeric.py +++ b/tests/test_numeric.py @@ -1,6 +1,7 @@ -from nutils import numeric +from nutils import numeric, util import numpy from nutils.testing import * +import itertools @parametrize class pack(TestCase): @@ -131,3 +132,21 @@ def test_unordered(self): self.assertAllEqual(numeric.asboolean([2,1], 3, ordered=False), [False, True, True]) with self.assertRaises(Exception): numeric.asboolean([2,1], 3) + +class levicivita(TestCase): + + def test_1d(self): + with self.assertRaisesRegex(ValueError, '^The Levi-Civita symbol is undefined for dimensions lower than 2.'): + numeric.levicivita(1) + + def test_2d(self): + self.assertAllEqual(numeric.levicivita(2, int), numpy.array([[0, 1], [-1, 0]])) + + def test_nd(self): + sign = lambda v: -1 if v < 0 else 1 if v > 0 else 0 + for n in range(2, 6): + with self.subTest(n=n): + desired = numpy.empty((n,)*n, int) + for I in itertools.product(*[range(n)]*n): + desired[I] = util.product(sign(b-a) for a, b in itertools.combinations(I, 2)) + self.assertAllEqual(numeric.levicivita(n, int), desired) From 9ec5551ec9af5f0b5c09cf9049cf6bad1cc8fa35 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 10 Jun 2020 17:07:57 +0200 Subject: [PATCH 094/119] update examples to d(func, geom), n(geom) syntax --- examples/adaptivity.py | 8 ++++---- examples/burgers.py | 4 ++-- examples/cahnhilliard.py | 4 ++-- examples/cylinderflow.py | 6 +++--- examples/drivencavity-compatible.py | 13 +++++++------ examples/drivencavity.py | 9 +++++---- examples/elasticity.py | 4 ++-- examples/finitestrain.py | 4 ++-- examples/laplace.py | 4 ++-- examples/platewithhole-nurbs.py | 6 +++--- examples/platewithhole.py | 6 +++--- 11 files changed, 35 insertions(+), 33 deletions(-) diff --git a/examples/adaptivity.py b/examples/adaptivity.py index ff39d052d..0ac35b5bb 100644 --- a/examples/adaptivity.py +++ b/examples/adaptivity.py @@ -45,8 +45,8 @@ def main(etype:str, btype:str, degree:int, nrefine:int): if irefine: refdom = domain.refined ns.refbasis = refdom.basis(btype, degree=degree) - indicator = refdom.integral('refbasis_n,k u_,k d:x' @ ns, degree=degree*2).eval(lhs=lhs) - indicator -= refdom.boundary.integral('refbasis_n u_,k n_k d:x' @ ns, degree=degree*2).eval(lhs=lhs) + indicator = refdom.integral('d(refbasis_n, x_k) d(u, x_k) d:x' @ ns, degree=degree*2).eval(lhs=lhs) + indicator -= refdom.boundary.integral('refbasis_n d(u, x_k) n(x_k) d:x' @ ns, degree=degree*2).eval(lhs=lhs) supp = ns.refbasis.get_support(indicator**2 > numpy.mean(indicator**2)) domain = domain.refined_by(ns.refbasis.transforms[supp]) @@ -62,11 +62,11 @@ def main(etype:str, btype:str, degree:int, nrefine:int): sqr = domain.boundary.integral('du^2 d:x' @ ns, degree=7) cons = solver.optimize('lhs', sqr, droptol=1e-15, constrain=cons) - res = domain.integral('basis_n,k u_,k d:x' @ ns, degree=degree*2) + res = domain.integral('d(basis_n, x_k) d(u, x_k) d:x' @ ns, degree=degree*2) lhs = solver.solve_linear('lhs', res, constrain=cons) ndofs = len(ns.basis) - error = domain.integral('_i d:x' @ ns, degree=7).eval(lhs=lhs)**.5 + error = domain.integral('_i d:x' @ ns, degree=7).eval(lhs=lhs)**.5 rate, offset = linreg.add(numpy.log(len(ns.basis)), numpy.log(error)) treelog.user('ndofs: {ndofs}, L2 error: {error[0]:.2e} ({rate[0]:.2f}), H1 error: {error[1]:.2e} ({rate[1]:.2f})'.format(ndofs=len(ns.basis), error=error, rate=rate)) diff --git a/examples/burgers.py b/examples/burgers.py index b23493778..345246615 100644 --- a/examples/burgers.py +++ b/examples/burgers.py @@ -36,8 +36,8 @@ def main(nelems:int, ndims:int, degree:int, timescale:float, newtontol:float, en ns.f = '.5 u^2' ns.C = 1 - res = domain.integral('-basis_n,0 f d:x' @ ns, degree=5) - res += domain.interfaces.integral('-[basis_n] n_0 ({f} - .5 C [u] n_0) d:x' @ ns, degree=degree*2) + res = domain.integral('-d(basis_n, x_i) δ_i0 f d:x' @ ns, degree=5) + res += domain.interfaces.integral('-[basis_n] n(x_i) δ_i0 ({f} - .5 C [u] n(x_j) δ_j0) d:x' @ ns, degree=degree*2) inertia = domain.integral('basis_n u d:x' @ ns, degree=5) sqr = domain.integral('(u - exp(-?y_i ?y_i)(y_i = 5 (x_i - 0.5_i)))^2 d:x' @ ns, degree=5) diff --git a/examples/cahnhilliard.py b/examples/cahnhilliard.py index 1685f2287..7426d7c74 100644 --- a/examples/cahnhilliard.py +++ b/examples/cahnhilliard.py @@ -72,9 +72,9 @@ def main(nelems:int, etype:str, btype:str, degree:int, epsilon:typing.Optional[f ns.dt = timestep nrg_mix = domain.integral('F d:x' @ ns, degree=7) - nrg_iface = domain.integral('.5 c_,k c_,k d:x' @ ns, degree=7) + nrg_iface = domain.integral('.5 d(c, x_k) d(c, x_k) d:x' @ ns, degree=7) nrg_wall = domain.boundary.integral('(abs(ewall) + c ewall) d:x' @ ns, degree=7) - nrg = nrg_mix + nrg_iface + nrg_wall + domain.integral('(dF - m dc - .5 dt epsilon^2 m_,k m_,k) d:x' @ ns, degree=7) + nrg = nrg_mix + nrg_iface + nrg_wall + domain.integral('(dF - m dc - .5 dt epsilon^2 d(m, x_k) d(m, x_k)) d:x' @ ns, degree=7) numpy.random.seed(seed) lhs = numpy.random.normal(0, .5, ns.cbasis.shape) # initial condition diff --git a/examples/cylinderflow.py b/examples/cylinderflow.py index 1923206c2..c1211e2ac 100644 --- a/examples/cylinderflow.py +++ b/examples/cylinderflow.py @@ -59,9 +59,9 @@ def main(nelems:int, degree:int, reynolds:float, rotation:float, timestep:float, ns.ubasis_ni = 'unbasis_n J_i0 + utbasis_n J_i1' # piola transformation ns.u_i = 'ubasis_ni ?lhs_n' ns.p = 'pbasis_n ?lhs_n' - ns.sigma_ij = '(u_i,j + u_j,i) / Re - p δ_ij' + ns.sigma_ij = '(d(u_i, x_j) + d(u_j, x_i)) / Re - p δ_ij' ns.N = 10 * degree / elemangle # Nitsche constant based on element size = elemangle/2 - ns.nitsche_ni = '(N ubasis_ni - (ubasis_ni,j + ubasis_nj,i) n_j) / Re' + ns.nitsche_ni = '(N ubasis_ni - (d(ubasis_ni, x_j) + d(ubasis_nj, x_i)) n_j) / Re' ns.rotation = rotation ns.uwall_i = '0.5 rotation <-sin(phi), cos(phi)>_i' @@ -75,7 +75,7 @@ def main(nelems:int, degree:int, reynolds:float, rotation:float, timestep:float, numpy.random.seed(seed) lhs0 *= numpy.random.normal(1, .1, lhs0.shape) # add small velocity noise - res = domain.integral('(ubasis_ni u_i,j u_j + ubasis_ni,j sigma_ij + pbasis_n u_k,k) d:x' @ ns, degree=9) + res = domain.integral('(ubasis_ni d(u_i, x_j) u_j + d(ubasis_ni, x_j) sigma_ij + pbasis_n d(u_k, x_k)) d:x' @ ns, degree=9) res += domain.boundary['inner'].integral('(nitsche_ni (u_i - uwall_i) - ubasis_ni sigma_ij n_j) d:x' @ ns, degree=9) inertia = domain.integral('ubasis_ni u_i d:x' @ ns, degree=9) diff --git a/examples/drivencavity-compatible.py b/examples/drivencavity-compatible.py index 9709bf005..111b51ae0 100644 --- a/examples/drivencavity-compatible.py +++ b/examples/drivencavity-compatible.py @@ -42,18 +42,18 @@ def main(nelems:int, degree:int, reynolds:float): ns.u_i = 'ubasis_ni ?lhs_n' ns.p = 'pbasis_n ?lhs_n' ns.l = 'lbasis_n ?lhs_n' - ns.stress_ij = '(u_i,j + u_j,i) / Re - p δ_ij' + ns.stress_ij = '(d(u_i, x_j) + d(u_j, x_i)) / Re - p δ_ij' ns.uwall = domain.boundary.indicator('top'), 0 ns.N = 5 * degree * nelems # nitsche constant based on element size = 1/nelems - ns.nitsche_ni = '(N ubasis_ni - (ubasis_ni,j + ubasis_nj,i) n_j) / Re' + ns.nitsche_ni = '(N ubasis_ni - (d(ubasis_ni, x_j) + d(ubasis_nj, x_i)) n(x_j)) / Re' - res = domain.integral('(ubasis_ni,j stress_ij + pbasis_n (u_k,k + l) + lbasis_n p) d:x' @ ns, degree=2*degree) + res = domain.integral('(d(ubasis_ni, x_j) stress_ij + pbasis_n (d(u_k, x_k) + l) + lbasis_n p) d:x' @ ns, degree=2*degree) res += domain.boundary.integral('(nitsche_ni (u_i - uwall_i) - ubasis_ni stress_ij n_j) d:x' @ ns, degree=2*degree) with treelog.context('stokes'): lhs0 = solver.solve_linear('lhs', res) postprocess(domain, ns, lhs=lhs0) - res += domain.integral('ubasis_ni u_i,j u_j d:x' @ ns, degree=3*degree) + res += domain.integral('ubasis_ni d(u_i, x_j) u_j d:x' @ ns, degree=3*degree) with treelog.context('navierstokes'): lhs1 = solver.newton('lhs', res, lhs0=lhs0).solve(tol=1e-10) postprocess(domain, ns, lhs=lhs1) @@ -66,13 +66,14 @@ def main(nelems:int, degree:int, reynolds:float): def postprocess(domain, ns, every=.05, spacing=.01, **arguments): - div = domain.integral('(u_k,k)^2 d:x' @ ns, degree=1).eval(**arguments)**.5 + div = domain.integral('d(u_k, x_k)^2 d:x' @ ns, degree=1).eval(**arguments)**.5 treelog.info('velocity divergence: {:.2e}'.format(div)) # confirm that velocity is pointwise divergence-free ns = ns.copy_() # copy namespace so that we don't modify the calling argument ns.streambasis = domain.basis('std', degree=2)[1:] # remove first dof to obtain non-singular system ns.stream = 'streambasis_n ?streamdofs_n' # stream function - sqr = domain.integral('((u_0 - stream_,1)^2 + (u_1 + stream_,0)^2) d:x' @ ns, degree=4) + ns.ε = function.levicivita(2) + sqr = domain.integral('(u_i - ε_ij d(stream, x_j)) (u_i - ε_ij d(stream, x_j)) d:x' @ ns, degree=4) arguments['streamdofs'] = solver.optimize('streamdofs', sqr, arguments=arguments) # compute streamlines bezier = domain.sample('bezier', 9) diff --git a/examples/drivencavity.py b/examples/drivencavity.py index 4ce8a976a..b08eecc00 100644 --- a/examples/drivencavity.py +++ b/examples/drivencavity.py @@ -35,7 +35,7 @@ def main(nelems:int, etype:str, degree:int, reynolds:float): ]) ns.u_i = 'ubasis_ni ?lhs_n' ns.p = 'pbasis_n ?lhs_n' - ns.stress_ij = '(u_i,j + u_j,i) / Re - p δ_ij' + ns.stress_ij = '(d(u_i, x_j) + d(u_j, x_i)) / Re - p δ_ij' sqr = domain.boundary.integral('u_k u_k d:x' @ ns, degree=degree*2) wallcons = solver.optimize('lhs', sqr, droptol=1e-15) @@ -46,12 +46,12 @@ def main(nelems:int, etype:str, degree:int, reynolds:float): cons = numpy.choose(numpy.isnan(lidcons), [lidcons, wallcons]) cons[-1] = 0 # pressure point constraint - res = domain.integral('(ubasis_ni,j stress_ij + pbasis_n u_k,k) d:x' @ ns, degree=degree*2) + res = domain.integral('(d(ubasis_ni, x_j) stress_ij + pbasis_n d(u_k, x_k)) d:x' @ ns, degree=degree*2) with treelog.context('stokes'): lhs0 = solver.solve_linear('lhs', res, constrain=cons) postprocess(domain, ns, lhs=lhs0) - res += domain.integral('.5 (ubasis_ni u_i,j - ubasis_ni,j u_i) u_j d:x' @ ns, degree=degree*3) + res += domain.integral('.5 (ubasis_ni d(u_i, x_j) - d(ubasis_ni, x_j) u_i) u_j d:x' @ ns, degree=degree*3) with treelog.context('navierstokes'): lhs1 = solver.newton('lhs', res, lhs0=lhs0, constrain=cons).solve(tol=1e-10) postprocess(domain, ns, lhs=lhs1) @@ -67,7 +67,8 @@ def postprocess(domain, ns, every=.05, spacing=.01, **arguments): ns = ns.copy_() # copy namespace so that we don't modify the calling argument ns.streambasis = domain.basis('std', degree=2)[1:] # remove first dof to obtain non-singular system ns.stream = 'streambasis_n ?streamdofs_n' # stream function - sqr = domain.integral('((u_0 - stream_,1)^2 + (u_1 + stream_,0)^2) d:x' @ ns, degree=4) + ns.ε = function.levicivita(2) + sqr = domain.integral('(u_i - ε_ij d(stream, x_j)) (u_i - ε_ij d(stream, x_j)) d:x' @ ns, degree=4) arguments['streamdofs'] = solver.optimize('streamdofs', sqr, arguments=arguments) # compute streamlines bezier = domain.sample('bezier', 9) diff --git a/examples/elasticity.py b/examples/elasticity.py index 99a4535d5..9947d6afe 100644 --- a/examples/elasticity.py +++ b/examples/elasticity.py @@ -34,14 +34,14 @@ def main(nelems:int, etype:str, btype:str, degree:int, poisson:float): ns.X_i = 'x_i + u_i' ns.lmbda = 2 * poisson ns.mu = 1 - 2 * poisson - ns.strain_ij = '(u_i,j + u_j,i) / 2' + ns.strain_ij = '(d(u_i, x_j) + d(u_j, x_i)) / 2' ns.stress_ij = 'lmbda strain_kk δ_ij + 2 mu strain_ij' sqr = domain.boundary['left'].integral('u_k u_k d:x' @ ns, degree=degree*2) sqr += domain.boundary['right'].integral('(u_0 - .5)^2 d:x' @ ns, degree=degree*2) cons = solver.optimize('lhs', sqr, droptol=1e-15) - res = domain.integral('basis_ni,j stress_ij d:x' @ ns, degree=degree*2) + res = domain.integral('d(basis_ni, x_j) stress_ij d:x' @ ns, degree=degree*2) lhs = solver.solve_linear('lhs', res, constrain=cons) bezier = domain.sample('bezier', 5) diff --git a/examples/finitestrain.py b/examples/finitestrain.py index 51706187f..11eb7347b 100644 --- a/examples/finitestrain.py +++ b/examples/finitestrain.py @@ -46,7 +46,7 @@ def main(nelems:int, etype:str, btype:str, degree:int, poisson:float, angle:floa ns.ubasis = domain.basis(btype, degree=degree).vector(2) ns.u_i = 'ubasis_ki ?lhs_k' ns.X_i = 'x_i + u_i' - ns.strain_ij = '.5 (u_i,j + u_j,i)' + ns.strain_ij = '.5 (d(u_i, x_j) + d(u_j, x_i))' ns.energy = 'lmbda strain_ii strain_jj + 2 mu strain_ij strain_ij' sqr = domain.boundary['left'].integral('u_k u_k d:x' @ ns, degree=degree*2) @@ -58,7 +58,7 @@ def main(nelems:int, etype:str, btype:str, degree:int, poisson:float, angle:floa X, energy = bezier.eval(['X_i', 'energy'] @ ns, lhs=lhs0) export.triplot('linear.png', X, energy, tri=bezier.tri, hull=bezier.hull) - ns.strain_ij = '.5 (u_i,j + u_j,i + u_k,i u_k,j)' + ns.strain_ij = '.5 (d(u_i, x_j) + d(u_j, x_i) + d(u_k, x_i) d(u_k, x_j))' ns.energy = 'lmbda strain_ii strain_jj + 2 mu strain_ij strain_ij' energy = domain.integral('energy d:x' @ ns, degree=degree*2) diff --git a/examples/laplace.py b/examples/laplace.py index f72a1c711..ba62ccda9 100644 --- a/examples/laplace.py +++ b/examples/laplace.py @@ -61,13 +61,13 @@ def main(nelems:int, etype:str, btype:str, degree:int): # We are now ready to implement the Laplace equation. In weak form, the # solution is a scalar field :math:`u` for which: # - # .. math:: ∀ v: ∫_Ω v_{,k} u_{,k} - ∫_{Γ_n} v f = 0. + # .. math:: ∀ v: ∫_Ω \frac{dv}{dx_i} \frac{du}{dx_i} - ∫_{Γ_n} v f = 0. # # By linearity the test function :math:`v` can be replaced by the basis that # spans its space. The result is an integral ``res`` that evaluates to a # vector matching the size of the function space. - res = domain.integral('basis_n,i u_,i d:x' @ ns, degree=degree*2) + res = domain.integral('d(basis_n, x_i) d(u, x_i) d:x' @ ns, degree=degree*2) res -= domain.boundary['right'].integral('basis_n cos(1) cosh(x_1) d:x' @ ns, degree=degree*2) # The Dirichlet constraints are set by finding the coefficients that minimize diff --git a/examples/platewithhole-nurbs.py b/examples/platewithhole-nurbs.py index 4dbb39911..036efa286 100644 --- a/examples/platewithhole-nurbs.py +++ b/examples/platewithhole-nurbs.py @@ -59,7 +59,7 @@ def main(nrefine:int, traction:float, radius:float, poisson:float): ns.ubasis = nurbsbasis.vector(2) ns.u_i = 'ubasis_ni ?lhs_n' ns.X_i = 'x_i + u_i' - ns.strain_ij = '(u_i,j + u_j,i) / 2' + ns.strain_ij = '(d(u_i, x_j) + d(u_j, x_i)) / 2' ns.stress_ij = 'lmbda strain_kk δ_ij + 2 mu strain_ij' ns.r2 = 'x_k x_k' ns.R2 = radius**2 / ns.r2 @@ -74,7 +74,7 @@ def main(nrefine:int, traction:float, radius:float, poisson:float): cons = solver.optimize('lhs', sqr, droptol=1e-15, constrain=cons) # construct residual - res = domain.integral('ubasis_ni,j stress_ij d:x' @ ns, degree=9) + res = domain.integral('d(ubasis_ni, x_j) stress_ij d:x' @ ns, degree=9) # solve system lhs = solver.solve_linear('lhs', res, constrain=cons) @@ -85,7 +85,7 @@ def main(nrefine:int, traction:float, radius:float, poisson:float): export.triplot('stressxx.png', X, stressxx, tri=bezier.tri, hull=bezier.hull, clim=(numpy.nanmin(stressxx), numpy.nanmax(stressxx))) # evaluate error - err = domain.integral('_n d:x' @ ns, degree=9).eval(lhs=lhs)**.5 + err = domain.integral('_n d:x' @ ns, degree=9).eval(lhs=lhs)**.5 treelog.user('errors: L2={:.2e}, H1={:.2e}'.format(*err)) return err, cons, lhs diff --git a/examples/platewithhole.py b/examples/platewithhole.py index 0163deb21..232e23883 100644 --- a/examples/platewithhole.py +++ b/examples/platewithhole.py @@ -45,7 +45,7 @@ def main(nelems:int, etype:str, btype:str, degree:int, traction:float, maxrefine ns.ubasis = domain.basis(btype, degree=degree).vector(2) ns.u_i = 'ubasis_ni ?lhs_n' ns.X_i = 'x_i + u_i' - ns.strain_ij = '(u_i,j + u_j,i) / 2' + ns.strain_ij = '(d(u_i, x_j) + d(u_j, x_i)) / 2' ns.stress_ij = 'lmbda strain_kk δ_ij + 2 mu strain_ij' ns.r2 = 'x_k x_k' ns.R2 = radius**2 / ns.r2 @@ -59,14 +59,14 @@ def main(nelems:int, etype:str, btype:str, degree:int, traction:float, maxrefine sqr = domain.boundary['top,right'].integral('du_k du_k d:x' @ ns, degree=20) cons = solver.optimize('lhs', sqr, droptol=1e-15, constrain=cons) - res = domain.integral('ubasis_ni,j stress_ij d:x' @ ns, degree=degree*2) + res = domain.integral('d(ubasis_ni, x_j) stress_ij d:x' @ ns, degree=degree*2) lhs = solver.solve_linear('lhs', res, constrain=cons) bezier = domain.sample('bezier', 5) X, stressxx = bezier.eval(['X_i', 'stress_00'] @ ns, lhs=lhs) export.triplot('stressxx.png', X, stressxx, tri=bezier.tri, hull=bezier.hull) - err = domain.integral('_n d:x' @ ns, degree=max(degree,3)*2).eval(lhs=lhs)**.5 + err = domain.integral('_n d:x' @ ns, degree=max(degree,3)*2).eval(lhs=lhs)**.5 treelog.user('errors: L2={:.2e}, H1={:.2e}'.format(*err)) return err, cons, lhs From 9b1d847f70ab01603baf1e8a16171d2e4be56e8f Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 16 Jun 2020 10:22:17 +0200 Subject: [PATCH 095/119] deprecate [f]_i and [f]_x_i expression syntax --- nutils/expression.py | 1 + tests/test_expression.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/nutils/expression.py b/nutils/expression.py index e5e247fa9..d4438a71b 100644 --- a/nutils/expression.py +++ b/nutils/expression.py @@ -660,6 +660,7 @@ def parse_var(self): geometry_name = self.default_geometry_name geom = self._get_geometry(geometry_name) if self._next.type == 'indices': + warnings.deprecation('`[f]_i` and `[f]_x_i` are deprecated; use `[f] n({x}_i)` instead'.format(x=geometry_name)) value *= self._asarray(('normal', _(geom)), self._consume(), geom.shape) elif self._next.type == '{': self._consume() diff --git a/tests/test_expression.py b/tests/test_expression.py index a6479e2b9..356d6a196 100644 --- a/tests/test_expression.py +++ b/tests/test_expression.py @@ -232,8 +232,13 @@ def test_jump_mean(self): ('jump', ('trace', ('grad', v._a2, v._x), _(0), _(1))), ('mean', ('trace', ('grad', v._a2, v._x), _(0), _(1))))) - def test_jump_normal(self): self.assert_ast('[a]_i', 'i', ('mul', ('append_axis', ('jump', v._a), _(2)), ('normal', v._x))) - def test_jump_normal_altgeom(self): self.assert_ast('[a]_altgeom_i', 'i', ('mul', ('append_axis', ('jump', v._a), _(3)), ('normal', v._altgeom))) + def test_jump_normal(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('[a]_i', 'i', ('mul', ('append_axis', ('jump', v._a), _(2)), ('normal', v._x))) + + def test_jump_normal_altgeom(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('[a]_altgeom_i', 'i', ('mul', ('append_axis', ('jump', v._a), _(3)), ('normal', v._altgeom))) def test_laplace_of_group(self): self.assert_ast('(2 a2_i)_,jj', 'i', From 6e2d55fe6051cb6088906d3a56b87e03edc8cd6d Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 16 Jun 2020 11:11:39 +0200 Subject: [PATCH 096/119] deprecate grad, deriv, normals with explicit geom This patch deprecates the following expression syntax: * Gradient with explicit geometry: `u_,x_i` and `dx_i:u`. * Derivative to argument: `u_,?a` and `d?a:u`. * Normal with explicit geometry: `n:x_i`. --- nutils/expression.py | 25 +++---- tests/test_expression.py | 144 ++++++++++++++++++++++++++++----------- 2 files changed, 112 insertions(+), 57 deletions(-) diff --git a/nutils/expression.py b/nutils/expression.py index d4438a71b..e72d5d87d 100644 --- a/nutils/expression.py +++ b/nutils/expression.py @@ -694,9 +694,11 @@ def parse_var(self): assert target.type in ('geometry', 'argument') indices = self._consume() if self._next.type == 'indices' else '' if target.type == 'geometry': + warnings.deprecation('the gradient syntax `dx_i:u` is deprecated; use `d(u, x_i)` instead') geom = self._get_geometry(target.data) elif target.type == 'argument': assert target.data.startswith('?') + warnings.deprecation('the derivative syntax `d?a:u` is deprecated; use `d(u, ?a)` instead') arg = self._get_arg(target.data[1:], indices) func = self.parse_var() if target.type == 'geometry': @@ -725,6 +727,7 @@ def parse_var(self): linked_lengths=functools.reduce(operator.or_, (arg.linked_lengths for arg in args), frozenset())) elif name in self.normal_symbols: if self._next.type == 'geometry': + warnings.deprecation('the normal syntax with explicitly geometry `n:x_i` is deprecated; use `n(x_i)` instead') geometry_name = self._consume().data else: geometry_name = self.default_geometry_name @@ -746,12 +749,15 @@ def parse_var(self): if target.type == 'geometry': assert indices gradtype = {',': 'grad', ';': 'surfgrad'}[gradient.data] - geom = self._get_geometry(target.data) + if target.data: + warnings.deprecation('the gradient syntax with explicit geometry `u_,x_i` is deprecated; use `d(u, x_i)` instead') + geom = self._get_geometry(target.data or self.default_geometry_name) for i, index in enumerate(indices.data): value = value.grad(index, geom, gradtype) elif target.type == 'argument': assert gradient.data == ',' assert target.data.startswith('?') + warnings.deprecation('the derivative to argument syntax `u_,?a` is deprecated; use `d(u, ?a)` instead') arg = self._get_arg(target.data[1:], indices) value = value.derivative(arg) elif self._next.type == 'indices': @@ -1042,7 +1048,7 @@ def tokenize(self): variant_default = m_geom.group(1) + self.default_geometry_name + '_' + m_geom.group(4) raise _IntermediateError('Missing geometry, e.g. {!r} or {!r}.'.format(variant_geom, variant_default), at=pos) tokens.append(_Token('gradient', m_geom.group(1), pos)) - tokens.append(_Token('geometry', m_geom.group(3) or self.default_geometry_name, pos+m_geom.start(3))) + tokens.append(_Token('geometry', m_geom.group(3), pos+m_geom.start(3))) tokens.append(_Token('indices', m_geom.group(4), pos+m_geom.start(4))) pos += m_geom.end() parts += 1 @@ -1172,22 +1178,9 @@ def parse(expression, variables, indices, arg_shapes={}, default_geometry_name=' a numeral as index. The **surface gradient** is denoted with a semicolon instead of a comma, but follows the same rules as the gradient otherwise. Example: ``a_i;j`` is the sufrace gradient of ``a_i`` to the geometry. - It is also possible to take the gradient to another geometry by appending - the name of the geometry, which should exist as a variable, and an - underscore directly after the comma of semicolon. Example: - ``a_i,altgeom_j`` denotes the gradient of ``a_i`` to ``altgeom`` and the - gradient axis has index ``j``. Futhermore, it is possible to take the - **derivative** to an argument by adding the argument with appropriate - indices after the comma. Example: ``(?x^2)_,?x`` denotes the derivative - of ``?x^2`` to ``?x``, which is equivalent to ``2 ?x``, and ``(?y_i - ?y_i),?y_j`` is the derivative of ``?y_i ?y_i`` to ``?y_j``, which is - equivalent to ``2 ?y_j``. * The **normal** of the default geometry is denoted by ``n_i``, where the - index ``i`` may be replaced with an index of choice. The normal with - respect to different geometry is denoted by appending an underscore with - the name of the geometry right after ``n``. Example: ``n_altgeom_j`` is - the normal with respect to geometry ``altgeom``. + index ``i`` may be replaced with an index of choice. * A **dirac** is denoted by ``δ`` or ``$`` and takes two indices. The shape of the dirac is deduced from the expression. Example: let ``A`` be diff --git a/tests/test_expression.py b/tests/test_expression.py index 356d6a196..0fab22a50 100644 --- a/tests/test_expression.py +++ b/tests/test_expression.py @@ -447,23 +447,35 @@ def test_gradient_default(self): self.assert_ast('a2_i,j', 'ij', ('grad', v._a2, def test_gradient_other_default(self): self.assert_ast('a2_i,j', 'ij', ('grad', v._a2, v._altgeom), default_geometry_name='altgeom') def test_gradient_default_trace(self): self.assert_ast('a2_i,i', '', ('trace', ('grad', v._a2, v._x), _(0), _(1))) def test_gradient_default_double_trace(self): self.assert_ast('a422_ijk,jk', 'i', ('trace', ('grad', ('trace', ('grad', v._a422, v._x), _(1), _(3)), v._x), _(1), _(2))) - def test_gradient_altgeom(self): self.assert_ast('a3_i,altgeom_j', 'ij', ('grad', v._a3, v._altgeom)) - def test_gradient_altgeom_trace(self): self.assert_ast('a3_i,altgeom_i', '', ('trace', ('grad', v._a3, v._altgeom), _(0), _(1))) - def test_gradient_altgeom_double_trace(self): self.assert_ast('a433_ijk,altgeom_jk', 'i', ('trace', ('grad', ('trace', ('grad', v._a433, v._altgeom), _(1), _(3)), v._altgeom), _(1), _(2))) + + def test_gradient_altgeom(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('a3_i,altgeom_j', 'ij', ('grad', v._a3, v._altgeom)) + + def test_gradient_altgeom_trace(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('a3_i,altgeom_i', '', ('trace', ('grad', v._a3, v._altgeom), _(0), _(1))) + + def test_gradient_altgeom_double_trace(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('a433_ijk,altgeom_jk', 'i', ('trace', ('grad', ('trace', ('grad', v._a433, v._altgeom), _(1), _(3)), v._altgeom), _(1), _(2))) + def test_surfgrad_default(self): self.assert_ast('a2_i;j', 'ij', ('surfgrad', v._a2, v._x)) def test_surfgrad_default_trace(self): self.assert_ast('a2_i;i', '', ('trace', ('surfgrad', v._a2, v._x), _(0), _(1))) def test_gradient_invalid_geom_0dim(self): - self.assert_syntax_error( - "Invalid geometry: expected 1 dimension, but 'a' has 0.", - "1 + a2_i,a_i + 1", "", - " ^^^^^^^^") + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_syntax_error( + "Invalid geometry: expected 1 dimension, but 'a' has 0.", + "1 + a2_i,a_i + 1", "", + " ^^^^^^^^") def test_gradient_invalid_geom_2dim(self): - self.assert_syntax_error( - "Invalid geometry: expected 1 dimension, but 'a22' has 2.", - "1 + a2_i,a22_i + 1", "", - " ^^^^^^^^^^") + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_syntax_error( + "Invalid geometry: expected 1 dimension, but 'a22' has 2.", + "1 + a2_i,a22_i + 1", "", + " ^^^^^^^^^^") def test_gradient_const_scalar(self): self.assert_syntax_error( @@ -479,31 +491,66 @@ def test_gradient_const_array(self): # NEW GRAD - def test_newgradient(self): self.assert_ast('dx_j:a2_i', 'ij', ('grad', v._a2, v._x)) - def test_newgradient_trace(self): self.assert_ast('dx_i:a2_i', '', ('trace', ('grad', v._a2, v._x), _(0), _(1))) - def test_newgradient_double_trace(self): self.assert_ast('dx_k:(dx_j:a422_ijk)', 'i', ('trace', ('grad', ('group', ('trace', ('grad', v._a422, v._x), _(1), _(3))), v._x), _(1), _(2))) + def test_newgradient(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('dx_j:a2_i', 'ij', ('grad', v._a2, v._x)) + + def test_newgradient_trace(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('dx_i:a2_i', '', ('trace', ('grad', v._a2, v._x), _(0), _(1))) + + def test_newgradient_double_trace(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('dx_k:(dx_j:a422_ijk)', 'i', ('trace', ('grad', ('group', ('trace', ('grad', v._a422, v._x), _(1), _(3))), v._x), _(1), _(2))) # DERIVATIVE - def test_derivative0(self): self.assert_ast('(2 ?arg + 1)_,?arg', '', ('derivative', ('group', ('add', ('mul', _(2), ('arg', _('arg'))), _(1))), ('arg', _('arg')))) - def test_derivative1(self): self.assert_ast('(a2_i + ?arg_i)_,?arg_j', 'ij', ('derivative', ('group', ('add', v._a2, ('arg', _('arg'), _(2)))), ('arg', _('arg'), _(2)))) - def test_derivative2(self): self.assert_ast('(a23_ij + ?arg_ij)_,?arg_kj', 'ik', ('trace', ('derivative', ('group', ('add', v._a23, ('arg', _('arg'), _(2), _(3)))), ('arg', _('arg'), _(2), _(3))), _(1), _(3))) + def test_derivative0(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('(2 ?arg + 1)_,?arg', '', ('derivative', ('group', ('add', ('mul', _(2), ('arg', _('arg'))), _(1))), ('arg', _('arg')))) + + def test_derivative1(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('(a2_i + ?arg_i)_,?arg_j', 'ij', ('derivative', ('group', ('add', v._a2, ('arg', _('arg'), _(2)))), ('arg', _('arg'), _(2)))) + + def test_derivative2(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('(a23_ij + ?arg_ij)_,?arg_kj', 'ik', ('trace', ('derivative', ('group', ('add', v._a23, ('arg', _('arg'), _(2), _(3)))), ('arg', _('arg'), _(2), _(3))), _(1), _(3))) # NEW DERIVATIVE - def test_newderivative0(self): self.assert_ast('d?arg:(2 ?arg + 1)', '', ('derivative', ('group', ('add', ('mul', _(2), ('arg', _('arg'))), _(1))), ('arg', _('arg')))) - def test_newderivative1(self): self.assert_ast('d?arg_j:(a2_i + ?arg_i)', 'ij', ('derivative', ('group', ('add', v._a2, ('arg', _('arg'), _(2)))), ('arg', _('arg'), _(2)))) - def test_newderivative2(self): self.assert_ast('d?arg_kj:(a23_ij + ?arg_ij)', 'ik', ('trace', ('derivative', ('group', ('add', v._a23, ('arg', _('arg'), _(2), _(3)))), ('arg', _('arg'), _(2), _(3))), _(1), _(3))) + def test_newderivative0(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('d?arg:(2 ?arg + 1)', '', ('derivative', ('group', ('add', ('mul', _(2), ('arg', _('arg'))), _(1))), ('arg', _('arg')))) - # NORMAL + def test_newderivative1(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('d?arg_j:(a2_i + ?arg_i)', 'ij', ('derivative', ('group', ('add', v._a2, ('arg', _('arg'), _(2)))), ('arg', _('arg'), _(2)))) + + def test_newderivative2(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('d?arg_kj:(a23_ij + ?arg_ij)', 'ik', ('trace', ('derivative', ('group', ('add', v._a23, ('arg', _('arg'), _(2), _(3)))), ('arg', _('arg'), _(2), _(3))), _(1), _(3))) - def test_normal(self): self.assert_ast('n:x_i', 'i', ('normal', v._x)) + # NORMAL def test_normal_default(self): self.assert_ast('n_i', 'i', ('normal', v._x)) - def test_normal_altgeom(self): self.assert_ast('n_altgeom_i', 'i', ('normal', v._altgeom)) def test_normal_default_grad_default(self): self.assert_ast('n_i,j', 'ij', ('grad', ('normal', v._x), v._x)) - def test_normal_altgeom_grad_default(self): self.assert_ast('n_altgeom_i,x_j', 'ij', ('grad', ('normal', v._altgeom), v._x)) - def test_normal_altgeom_grad_altgeom(self): self.assert_ast('n_altgeom_i,altgeom_j', 'ij', ('grad', ('normal', v._altgeom), v._altgeom)) + + def test_normal(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('n:x_i', 'i', ('normal', v._x)) + + def test_normal_altgeom(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('n_altgeom_i', 'i', ('normal', v._altgeom)) + + def test_normal_altgeom_grad_default(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('n_altgeom_i,x_j', 'ij', ('grad', ('normal', v._altgeom), v._x)) + + def test_normal_altgeom_grad_altgeom(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('n_altgeom_i,altgeom_j', 'ij', ('grad', ('normal', v._altgeom), v._altgeom)) def test_normal_altgeom_grad_nogeom(self): self.assert_syntax_error( @@ -524,16 +571,18 @@ def test_normal_too_many_indices(self): " ^^^^") def test_normal_invalid_geom_0dim(self): - self.assert_syntax_error( - "Invalid geometry: expected 1 dimension, but 'a' has 0.", - "1 + n_a_i + 1", "", - " ^^^^") + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_syntax_error( + "Invalid geometry: expected 1 dimension, but 'a' has 0.", + "1 + n_a_i + 1", "", + " ^^^^") def test_normal_invalid_geom_2dim(self): - self.assert_syntax_error( - "Invalid geometry: expected 1 dimension, but 'a22' has 2.", - "1 + n_a22_i + 1", "", - " ^^^^^^") + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_syntax_error( + "Invalid geometry: expected 1 dimension, but 'a22' has 2.", + "1 + n_a22_i + 1", "", + " ^^^^^^") def test_variable_startswith_normal(self): nx = Array('nx', [2]) @@ -621,17 +670,30 @@ def test_arg_index_pos1(self): " ^") def test_arg_index_pos2(self): - self.assert_syntax_error( - "Length of axis cannot be determined from the expression.", - "?foo_,?bar_n", "n", - " ^") + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_syntax_error( + "Length of axis cannot be determined from the expression.", + "?foo_,?bar_n", "n", + " ^") # SUBSTITUTE - def test_arg_subs_0d_const(self): self.assert_ast('?arg_,?arg(arg=1)', '', ('substitute', ('derivative', ('arg', _('arg')), ('arg', _('arg'))), ('arg', _('arg')), _(1))) - def test_arg_subs_0d_var(self): self.assert_ast('?arg_,?arg(arg=a )', '', ('substitute', ('derivative', ('arg', _('arg')), ('arg', _('arg'))), ('arg', _('arg')), v._a)) - def test_arg_subs_1d_var(self): self.assert_ast('?arg_i,?arg_j(arg_i = a2_i)', 'ij', ('substitute', ('derivative', ('arg', _('arg'), _(2)), ('arg', _('arg'), _(2))), ('arg', _('arg'), _(2)), v._a2)) - def test_arg_subs_2d_var(self): self.assert_ast('?arg_ij,?arg_kl( arg_ij =a23_ji)', 'ijkl', ('substitute', ('derivative', ('arg', _('arg'), _(3), _(2)), ('arg', _('arg'), _(3), _(2))), ('arg', _('arg'), _(3), _(2)), ('transpose', v._a23, _((1,0))))) + def test_arg_subs_0d_const(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('?arg_,?arg(arg=1)', '', ('substitute', ('derivative', ('arg', _('arg')), ('arg', _('arg'))), ('arg', _('arg')), _(1))) + + def test_arg_subs_0d_var(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('?arg_,?arg(arg=a )', '', ('substitute', ('derivative', ('arg', _('arg')), ('arg', _('arg'))), ('arg', _('arg')), v._a)) + + def test_arg_subs_1d_var(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('?arg_i,?arg_j(arg_i = a2_i)', 'ij', ('substitute', ('derivative', ('arg', _('arg'), _(2)), ('arg', _('arg'), _(2))), ('arg', _('arg'), _(2)), v._a2)) + + def test_arg_subs_2d_var(self): + with self.assertWarns(warnings.NutilsDeprecationWarning): + self.assert_ast('?arg_ij,?arg_kl( arg_ij =a23_ji)', 'ijkl', ('substitute', ('derivative', ('arg', _('arg'), _(3), _(2)), ('arg', _('arg'), _(3), _(2))), ('arg', _('arg'), _(3), _(2)), ('transpose', v._a23, _((1,0))))) + def test_arg_multisubs(self): self.assert_ast('(1 + ?x + ?y)(x=1 + a, y=2)', '', ('substitute', ('group', ('add', ('add', _(1), ('arg', _('x'))), ('arg', _('y')))), ('arg', _('x')), ('add', _(1), v._a), ('arg', _('y')), _(2))) def test_arg_subs_missing_equals(self): From 2c1214c7a01a30c0c8b4010ca7f8195ec6601f26 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 30 Jun 2020 15:09:55 +0200 Subject: [PATCH 097/119] extend support for TensorPoints.tri --- nutils/points.py | 14 ++++++++++++++ nutils/sample.py | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/nutils/points.py b/nutils/points.py index 32307db1f..7a3092008 100644 --- a/nutils/points.py +++ b/nutils/points.py @@ -132,6 +132,11 @@ def basis(self): else: raise NotImplementedError + def __mul__(self, other): + if not isinstance(other, Points): + return NotImplemented + return TensorPoints(self, other) + strictpoints = types.strict[Points] class CoordsPoints(Points): @@ -182,6 +187,10 @@ def weights(self): @property def tri(self): + if self.points2.npoints == 1: + return self.points1.tri + if self.points1.npoints == 1: + return self.points2.tri if self.points1.ndimsmanifold == 1: # For an n-dimensional simplex with vertices a0,a1,..,an, the extruded # element has vertices a0,a1,..,an,b0,b1,..,bn. These can be divided in @@ -206,6 +215,11 @@ def hull(self): return types.frozenarray(hull, copy=False) return super().hull + def __mul__(self, other): + if not isinstance(other, Points): + return NotImplemented + return TensorPoints(self.points1, self.points2 * other) + class SimplexGaussPoints(CoordsWeightsPoints): '''Gauss quadrature points on a simplex.''' diff --git a/nutils/sample.py b/nutils/sample.py index 9906fc989..bd389676e 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -479,7 +479,7 @@ def compress(self, indices): def getpoints(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) - return points.TensorPoints(self._sample1.getpoints(ielem1), self._sample2.getpoints(ielem2)) + return self._sample1.getpoints(ielem1) * self._sample2.getpoints(ielem2) def getindex(self, ielem): ielem1, ielem2 = divmod(ielem, self._sample2.nelems) @@ -487,7 +487,7 @@ def getindex(self, ielem): @property def pointsiter(self): - return (points.TensorPoints(points1, points2) for points1 in self._sample1.pointsiter for points2 in self._sample2.pointsiter) + return (points1 * points2 for points1 in self._sample1.pointsiter for points2 in self._sample2.pointsiter) @property def indexiter(self): From 0d36635f8aa94eab48ef6dd19a6dd29237003436 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Fri, 10 Jul 2020 22:39:34 +0200 Subject: [PATCH 098/119] fix --- nutils/topology.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nutils/topology.py b/nutils/topology.py index 39dedfc85..29d27f1ae 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -770,7 +770,9 @@ def interfaces(self): if isinstance(topo, Topology): # last minute orientation fix s = [] - for transs in zip(topo.transforms, topo.opposites): + for ref, *transs in zip(topo.references, topo.transforms, topo.opposites): + if not ref: + continue for trans in transs: try: ielem = baseitopo.transforms.index(trans) From 0973d673055d9f02f5b266d2a638ac4e9512260c Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 13 Jul 2020 14:54:13 +0200 Subject: [PATCH 099/119] restore multidimensional slicing of tensor topos --- nutils/topology.py | 77 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 74 insertions(+), 3 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 29d27f1ae..87e807e47 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -80,6 +80,15 @@ def compress(self, indices): def getitem(self, item): return self.empty + def slice(self, items): + # items: tuple[slice, ...] + # len(items): self.ndims + if len(items) != self.ndims: + raise ValueError('expected {} slices but got {}'.format(self.ndims, len(items))) + if all(item == slice(None) for item in items): + return self + raise ValueError('cannot slice this topology') + def __getitem__(self, item): if numeric.isintarray(item): return self.compress(item) @@ -87,6 +96,19 @@ def __getitem__(self, item): item = item, if all(it in (...,slice(None)) for it in item): return self + if all(it == ... or isinstance(it, slice) for it in item): + # Expand ellipsis, append implicit `slice(None)`. + item = list(item) + if ... in item: + iell = item.index(...) + item[iell:iell+1] = [slice(None)]*max(0, self.ndims-len(item)-1) + if ... in item: + raise IndexError('an index can only have a single ellipsis') + else: + item.extend([slice(None)]*max(0, self.ndims-len(item))) + if len(item) != self.ndims: + raise IndexError('expected at most {} slices but got {}'.format(self.ndims, len(item))) + return self.slice(tuple(item)) topo = self.getitem(item) if len(item) != 1 or not isinstance(item[0],str) \ else functools.reduce(operator.or_, map(self.getitem, item[0].split(',')), self.empty) if not topo: @@ -750,6 +772,14 @@ def getitem(self, item): return itemtopo if isinstance(itemtopo, Topology) else self.basetopo[itemtopo] return self.basetopo.getitem(item) + def slice(self, items): + if len(items) != self.ndims: + raise ValueError('expected {} slices but got {}'.format(self.ndims, len(items))) + if all(item == slice(None) for item in items): + return self + # Otherwise slice base. TODO: maintain groups + return self.basetopo.slice(items) + @property def border_transforms(self): return self.basetopo.border_transforms @@ -960,6 +990,22 @@ def getitem(self, item): else: return SliceOfStructuredLine(self, start, stop) + def slice(self, items): + if len(items) != 1: + raise ValueError('expected 1 slice but got {}'.format(len(items))) + item = items[0] + if item == slice(None): + return self + start, stop, step = item.indices(len(self)) + if step != 1: + raise ValueError('expected a slice with unit step but got {}'.format(item)) + if start == stop: + return self.empty + elif start == 0 and stop == len(self): + return StructuredLine(self.roots[0], self.transforms, False, self._bnames) + else: + return SliceOfStructuredLine(self, start, stop) + @property def connectivity(self): connectivity = numpy.stack([numpy.arange(1, len(self)+1), numpy.arange(-1, len(self)-1)], axis=1) @@ -977,7 +1023,8 @@ def boundary(self): btransforms = self.transforms.edges(self.references)[idx] btopo = PointsTopology(self.roots, btransforms, btransforms) if self._bnames: - btopo = btopo.withgroups(vgroups={bname: btopo[i:i+1] for i, bname in enumerate(self._bnames)}) + btopos = (PointsTopology(self.roots, btransforms[:1], btransforms[:1]), PointsTopology(self.roots, btransforms[1:], btransforms[1:])) + btopo = btopo.withgroups(vgroups={bname: btopos[i] for i, bname in enumerate(self._bnames)}) return btopo @property @@ -1184,15 +1231,30 @@ def getitem(self, item): return super().getitem(item) return self._line[r.start:r.stop] + def slice(self, items): + if len(items) != 1: + raise ValueError('expected 1 slice but got {}'.format(len(items))) + item = items[0] + start, stop, step = item.indices(len(self)) + if step != 1: + raise ValueError('expected a slice with unit step but got {}'.format(item)) + if start == stop: + return self.empty + else: + return SliceOfStructuredLine(self._line, self._start+start, self._start+stop) + @property def boundary(self): idx = types.frozenarray([2*self._start+1, 2*self._stop-2], dtype=int) n = len(self._line) oppidx = types.frozenarray([1 if self._start == 0 else 2*self._start-2, 2*n-2 if self._stop == n else 2*self._stop+1]) edges = self._line.transforms.edges(self.references) - btopo = PointsTopology(self.roots, edges[idx], edges[oppidx]) + btransforms = edges[idx] + bopposites = edges[oppidx] + btopo = PointsTopology(self.roots, btransforms, bopposites) if self._bnames: - btopo = btopo.withgroups(vgroups={bname: btopo[i:i+1] for i, bname in enumerate(self._bnames)}) + btopos = (PointsTopology(self.roots, btransforms[:1], btransforms[:1]), PointsTopology(self.roots, btransforms[1:], btransforms[1:])) + btopo = btopo.withgroups(vgroups={bname: btopos[i] for i, bname in enumerate(self._bnames)}) return btopo @property @@ -2346,6 +2408,15 @@ def getitem(self, item): else: return (left or self._left).mul(right or self._right, self._leftopp, self._rightopp) + def slice(self, items): + if len(items) != self.ndims: + raise ValueError('expected {} slices but got {}'.format(self.ndims, len(items))) + if all(item == slice(None) for item in items): + return self + left = self._left.slice(items[:self._left.ndims]) + right = self._right.slice(items[self._left.ndims:]) + return ProductTopology(left, right, self._leftopp, self._rightopp) + @property def boundary(self): boundaries = [] From 77952ce6e040fdb7c4500776ef21673f60291fc0 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 15 Jul 2020 01:07:13 +0200 Subject: [PATCH 100/119] impl TensorPoints.basis --- nutils/points.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/nutils/points.py b/nutils/points.py index 7a3092008..3c4262296 100644 --- a/nutils/points.py +++ b/nutils/points.py @@ -215,6 +215,23 @@ def hull(self): return types.frozenarray(hull, copy=False) return super().hull + @property + def basis(self): + if self.ndimsnormal == 0: + return super().basis + basis1 = self.points1.basis + basis2 = self.points2.basis + if basis1.shape[0] != 1 or basis2.shape[0] != 1: + pointsshape = self.points1.npoints, self.points2.npoints + else: + pointsshape = 1, 1 + basis = numpy.zeros(pointsshape+(self.ndims, self.ndims), dtype=float) + basis[:,:,:self.points1.ndims,:self.points1.ndimsmanifold] = basis1[:,numpy.newaxis,:,:self.points1.ndimsmanifold] + basis[:,:,:self.points1.ndims,self.ndimsmanifold:self.ndimsmanifold+self.points1.ndimsnormal] = basis1[:,numpy.newaxis,:,self.points1.ndimsmanifold:] + basis[:,:,self.points1.ndims:,self.points1.ndimsmanifold:self.ndimsmanifold] = basis2[numpy.newaxis,:,:,:self.points2.ndimsmanifold] + basis[:,:,self.points1.ndims:,self.ndimsmanifold+self.points1.ndimsnormal:] = basis2[numpy.newaxis,:,:,self.points2.ndimsmanifold:] + return types.frozenarray(basis.reshape(-1, self.ndims, self.ndims), copy=False) + def __mul__(self, other): if not isinstance(other, Points): return NotImplemented From 4259762f173d4fa4e3b378b71b5e322adfebaa5e Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 15 Jul 2020 01:07:23 +0200 Subject: [PATCH 101/119] support TensorPoints.tri with points2.ndims=1 --- nutils/points.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nutils/points.py b/nutils/points.py index 3c4262296..562e870a1 100644 --- a/nutils/points.py +++ b/nutils/points.py @@ -202,6 +202,9 @@ def tri(self): # numeric.overlapping. tri12 = self.points1.tri[:,_,:,_] * self.points2.npoints + self.points2.tri[_,:,_,:] # ntri1 x ntri2 x 2 x ndimsmanifold return types.frozenarray(numeric.overlapping(tri12.reshape(-1, 2*self.ndimsmanifold), n=self.ndimsmanifold+1).reshape(-1, self.ndimsmanifold+1), copy=False) + if self.points2.ndimsmanifold == 1: + tri12 = self.points1.tri[:,_,_,:] * self.points2.npoints + self.points2.tri[_,:,:,_] # ntri1 x ntri2 x 2 x ndimsmanifold + return types.frozenarray(numeric.overlapping(tri12.reshape(-1, 2*self.ndimsmanifold), n=self.ndimsmanifold+1).reshape(-1, self.ndimsmanifold+1), copy=False) return super().tri @property From 6bf212bf7f788eb5bc561ad65d912d4c2c1ee52d Mon Sep 17 00:00:00 2001 From: Clemens Verhoosel Date: Fri, 8 May 2020 07:51:40 +0200 Subject: [PATCH 102/119] add support for line segments in export.vtk --- nutils/export.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nutils/export.py b/nutils/export.py index 41ff43ad9..474dbe65f 100644 --- a/nutils/export.py +++ b/nutils/export.py @@ -119,6 +119,7 @@ def vtk(name, cells, points, kwargs=...): ''' vtkcelltype = { + 2: numpy.array( 3, dtype='>u4'), # VTK_LINE 3: numpy.array( 5, dtype='>u4'), # VTK_TRIANGLE 4: numpy.array(10, dtype='>u4')} # VTK_TETRA vtkndim = { From cc56ad0ebf9a8b9ea2f2898d348f5e02b2facb8b Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 22 Jul 2020 11:24:58 +0200 Subject: [PATCH 103/119] fix typo --- nutils/sample.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nutils/sample.py b/nutils/sample.py index bd389676e..9ddb6afeb 100644 --- a/nutils/sample.py +++ b/nutils/sample.py @@ -564,7 +564,7 @@ def getindex(self, ielem): @property def pointsiter(self): - return itertools.chain.fromiterable(sample.pointsiter for sample in self._samples) + return itertools.chain.from_iterable(sample.pointsiter for sample in self._samples) @property def indexiter(self): From 8a4ae7c2ff53a9901bc240f5f278e6924d5d8954 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 7 Jan 2020 10:35:17 +0100 Subject: [PATCH 104/119] implement Element.__bool__ The default implementation of `__bool__` tests whether the volume is strict positive. For trimmed elements this can be rather expensive. This patch adds specialized implementations of `__bool__` for all element types. --- nutils/element.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/nutils/element.py b/nutils/element.py index cf6f04367..10f9aa3a3 100644 --- a/nutils/element.py +++ b/nutils/element.py @@ -326,6 +326,9 @@ class EmptyLike(Reference): volume = 0 + def __bool__(self): + return False + @property def empty(self): return self @@ -419,6 +422,9 @@ class SimplexReference(Reference): __slots__ = () __cache__ = 'edge_refs', 'edge_transforms', 'ribbons', '_get_poly_coeffs_bernstein', '_get_poly_coeffs_lagrange', '_integer_barycentric_coordinates' + def __bool__(self): + return True + @property def vertices(self): return types.frozenarray(numpy.concatenate([numpy.zeros(self.ndims)[_,:], numpy.eye(self.ndims)], axis=0), copy=False) @@ -661,6 +667,9 @@ def __init__(self, ref1, ref2): self.ref2 = ref2 super().__init__(ref1.ndims + ref2.ndims, ref1.ndimsnormal + ref2.ndimsnormal) + def __bool__(self): + return bool(self.ref1) and bool(self.ref2) + def __mul__(self, other): assert isinstance(other, Reference) return TensorReference(self.ref1, self.ref2 * other) @@ -821,6 +830,9 @@ def __init__(self, edgeref, etrans, tip:types.frozenarray): self.height = numpy.dot(etrans.offset - tip, ext) / self.extnorm assert self.height >= 0, 'tip is positioned at the negative side of edge' + def __bool__(self): + return bool(self.edgeref) and bool(self.height) + @property def vertices(self): return types.frozenarray(numpy.vstack([[self.tip], self.etrans.apply(self.edgeref.vertices)]), copy=False) @@ -894,6 +906,9 @@ def __init__(self, baseref): self.child_transforms = transform.Identity(baseref.ndims+baseref.ndimsnormal), super().__init__(baseref.ndims, baseref.ndimsnormal) + def __bool__(self): + return bool(self.baseref) + @property def vertices(self): return self.baseref.vertices @@ -942,6 +957,9 @@ def __init__(self, baseref, child_refs:tuple): self.child_refs = child_refs super().__init__(baseref.ndims, baseref.ndimsnormal) + def __bool__(self): + return bool(self.baseref) + def check_edges(self, tol=1e-15, print=print): super().check_edges(tol=tol, print=print) for cref in self.child_refs: @@ -1096,6 +1114,9 @@ def __init__(self, baseref, edge_refs:tuple, midpoint:types.frozenarray): super().__init__(baseref.ndims) + def __bool__(self): + return any(self.subrefs) + @property def vertices(self): vertices = [] From bf06b47fe6d755a45ebf40e7c97860d31fa0f10e Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 25 Mar 2020 22:08:14 +0100 Subject: [PATCH 105/119] inherit groups in HierarchicalTopology.interfaces --- nutils/topology.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 87e807e47..585e42b2f 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -2184,11 +2184,9 @@ def boundary(self): def interfaces(self): 'interfaces' - hreferences = [] - htransforms = [] - hopposites = [] + levelsifaces = [] for level, indices in zip(self.levels, self._indices_per_level): - selection = [] + selection = set() to = level.interfaces.transforms, level.interfaces.opposites for trans, ref in zip(map(level.transforms.__getitem__, indices), map(level.references.__getitem__, indices)): for trans_etrans in transform.unempty_edge_transforms(trans, ref): @@ -2198,14 +2196,11 @@ def interfaces(self): except ValueError: continue if self.transforms.contains_with_tail(opposites[i]): - selection.append(i) + selection.add(i) break if selection: - selection = types.frozenarray(numpy.unique(selection)) - hreferences.append(level.interfaces.references[selection]) - htransforms.append(level.interfaces.transforms[selection]) - hopposites.append(level.interfaces.opposites[selection]) - return Topology(self.roots, elementseq.chain(hreferences, self.ndims-1), transformseq.chain(htransforms, tuple(root.ndims for root in self.roots)), transformseq.chain(hopposites, tuple(root.ndims for root in self.roots))) + levelsifaces.append(SubsetTopology(level.interfaces, tuple(ref if i in selection else ref.empty for i, ref in enumerate(level.interfaces.references)))) + return DisjointUnionTopology(levelsifaces) @log.withcontext def basis(self, name, *args, truncation_tolerance=1e-15, **kwargs): From 736189812ad609a4e18a248de80ac37f5f624b62 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 7 Jan 2020 13:28:10 +0100 Subject: [PATCH 106/119] add DisjointUnion.basis --- nutils/function.py | 48 ++++++++++++++++++++++++++++++++++++++++++ nutils/topology.py | 6 ++++++ tests/test_basis.py | 25 ++++++++++++++++++++++ tests/test_function.py | 16 ++++++++++++++ 4 files changed, 95 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index d738da485..350863e84 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4379,6 +4379,54 @@ def get_support(self, dof): supp2 = self._basis2.get_support(dof2) return (supp1[:,_] * len(self._basis2.transforms) + supp2[_,:]).ravel() +class DisjointUnionBasis(Basis): + + __slots__ = '_bases', '_dofsplits', '_elemsplits' + + @types.apply_annotations + def __init__(self, bases:types.tuple[strictbasis], trans:types.strict[TransformChain]): + self._bases = bases + self._dofsplits = numpy.cumsum([0, *map(len, bases)]) + self._elemsplits = numpy.cumsum([0, *(len(basis.transforms) for basis in bases)]) + ndims = bases[0].ndimsdomain + if not all(basis.ndimsdomain == ndims for basis in bases): + raise ValueError + transforms = transformseq.chain((basis.transforms for basis in bases), bases[0].transforms.todims) + super().__init__(ndofs=self._dofsplits[-1], transforms=transforms, ndims=ndims, trans=trans) + + def get_support(self, dof): + if numeric.isint(dof): + dof = numeric.normdim(self.ndofs, dof) + ibasis = numpy.searchsorted(self._dofsplits[1:-1], dof, 'right') + return self._bases[ibasis].get_support(dof - self._dofsplits[ibasis]) + self._elemsplits[ibasis] + elif numeric.isboolarray(dof) and dof.shape == (len(self),): + return numpy.concatenate([basis.get_support(dof[l:r]) + m for basis, l, r, m in zip(self._bases, self._dofsplits[:-1], self._dofsplits[1:], self._elemsplits)]) + elif numeric.isintarray(dof) and dof.ndim == 1: + dof = numpy.unique(dof) + splits = numpy.searchsorted(dof, self._dofsplits[1:-1]) + return numpy.concatenate([basis.get_support(dof[l:r] - n) + m for basis, l, r, n, m in zip(self._bases, [0,*splits], [*splits, None], self._dofsplits[:-1], self._elemsplits[:-1])]) + else: + return super().get_support(dof) + + def get_dofs(self, ielem): + if numeric.isint(ielem): + ielem = numeric.normdim(len(self.transforms), ielem) + ibasis = numpy.searchsorted(self._elemsplits[1:-1], ielem, 'right') + return self._bases[ibasis].get_dofs(ielem - self._elemsplits[ibasis]) + self._dofsplits[ibasis] + elif numeric.isboolarray(ielem) and ielem.shape == (len(self.transforms),): + return numpy.concatenate([basis.get_dofs(ielem[l:r]) + n for basis, l, r, n in zip(self._bases, self._elemsplits[:-1], self._elemsplits[1:], self._dofsplits)]) + elif numeric.isintarray(ielem) and ielem.ndim == 1: + ielem = numpy.unique(ielem) + splits = numpy.searchsorted(ielem, self._elemsplits[1:-1]) + return numpy.concatenate([basis.get_dofs(ielem[l:r] - m) + n for basis, l, r, m, n in zip(self._bases, [0,*splits], [*splits, None], self._elemsplits[:-1], self._dofsplits[:-1])]) + else: + return super().get_dofs(ielem) + + def get_coefficients(self, ielem): + ielem = numeric.normdim(len(self.transforms), ielem) + ibasis = numpy.searchsorted(self._elemsplits[1:-1], ielem, 'right') + return self._bases[ibasis].get_coefficients(ielem - self._elemsplits[ibasis]) + # AUXILIARY FUNCTIONS (FOR INTERNAL USE) _ascending = lambda arg: numpy.greater(numpy.diff(arg), 0).all() diff --git a/nutils/topology.py b/nutils/topology.py index 585e42b2f..feea6b4cb 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -1879,6 +1879,12 @@ def sample(self, ischeme, degree): samples = tuple(topo.sample(ischeme, degree) for topo in self._topos) return sample.ChainedSample(samples, transforms) + def basis(self, name, *args, **kwargs): + if name == 'discont': + return super().basis(name, *args, **kwargs) + else: + return function.DisjointUnionBasis(topo.basis(name, *args, **kwargs) for topo in self._topos) + class SubsetTopology(Topology): 'trimmed' diff --git a/tests/test_basis.py b/tests/test_basis.py index 310e7fd62..056d198b3 100644 --- a/tests/test_basis.py +++ b/tests/test_basis.py @@ -309,3 +309,28 @@ def test_poly(self): for btype in ['discont', 'bernstein', 'lagrange', 'std', 'bubble'][:5 if variant in ('simplex', 'triangle') else 4]: for degree in [0,1,2,3] if btype == 'discont' else [2] if btype == 'bubble' else [1,2,3]: unstructured_topology(ndims=ndims, btype=btype, degree=degree, variant=variant) + +class disjointunion(basisTest): + + def setUp(self): + self.base, self.geom = mesh.rectilinear([3]) + self.left, self.right = self.base[:2], self.base[2:] + self.domain = topology.DisjointUnionTopology((self.left, self.right)) + self.degree = 2 + self.basis = self.domain.basis('spline', self.degree) + + def test_pum(self): + self.assertPartitionOfUnity(topo=self.domain, basis=self.basis) + + def test_poly(self): + self.assertPolynomial(topo=self.domain, geom=self.geom, basis=self.basis, degree=self.degree) + + def test_discontinuity(self): + ns = function.Namespace() + ns.x = self.geom + ns.basis = self.basis + ns.u = 'basis_n ?coeffs_n' + ns.f = function.sign(self.geom[0] - 2) + coeffs = solver.optimize('coeffs', self.domain.integral('(u - f)^2 d:x' @ ns, degree=2*self.degree)) + actual = self.domain.sample('bezier', 2).eval('u' @ ns, coeffs=coeffs) + self.assertAllAlmostEqual(actual, [-1]*4+[1]*2) diff --git a/tests/test_function.py b/tests/test_function.py index 3ed939459..783aa5de5 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -1229,6 +1229,22 @@ def setUp(self): self.checkndofs = 3 super().setUp() +class DisjointUnionBasis(CommonBasis, TestCase): + def setUp(self): + root = function.Root('X', 0) + self.roots = root, + transforms0 = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'a'], 0, 0) + transforms1 = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'bc'], 0, 0) + transforms2 = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'd'], 0, 0) + basis0 = function.PlainBasis([[1]], [[0]], 1, transforms0, 0, function.SelectChain(self.roots)) + basis1 = function.PlainBasis([[2,3],[4,5]], [[0,1],[0,2]], 3, transforms1, 0, function.SelectChain(self.roots)) + basis2 = function.PlainBasis([[6]], [[0]], 1, transforms2, 0, function.SelectChain(self.roots)) + self.basis = function.DisjointUnionBasis((basis0, basis1, basis2), function.SelectChain(self.roots)) + self.checkcoeffs = [[1],[2,3],[4,5],[6]] + self.checkdofs = [[0],[1,2],[1,3],[4]] + self.checkndofs = 5 + super().setUp() + class StructuredBasis1D(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 1) From 5ff9d44c02388b774a5ed50535848f9e7569085b Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 7 Jan 2020 13:36:16 +0100 Subject: [PATCH 107/119] add function.WithTransformsBasis --- nutils/function.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_function.py | 13 +++++++++++++ 2 files changed, 48 insertions(+) diff --git a/nutils/function.py b/nutils/function.py index 350863e84..4ea7941ed 100644 --- a/nutils/function.py +++ b/nutils/function.py @@ -4379,6 +4379,41 @@ def get_support(self, dof): supp2 = self._basis2.get_support(dof2) return (supp1[:,_] * len(self._basis2.transforms) + supp2[_,:]).ravel() +class WithTransformsBasis(Basis): + '''Replace the transforms sequence of a basis. + + Parameters + ---------- + parent : :class:`Basis` + The basis to wrap. + transforms : :class:`nutils.transformseq.Transforms` + The new transforms sequence. + ''' + + @types.apply_annotations + def __init__(self, parent:strictbasis, transforms:transformseq.stricttransforms, trans:types.strict[TransformChain]): + self._parent = parent + assert len(self._parent.transforms) == len(transforms) + super().__init__(ndofs=parent.ndofs, transforms=transforms, ndims=parent.ndimsdomain, trans=trans) + + def get_support(self, dof): + return self._parent.get_support(dof) + + def get_dofs(self, ielem): + return self._parent.get_dofs(ielem) + + def get_coefficients(self, ielem): + return self._parent.get_coefficients(ielem) + + def f_ndofs(self, index): + return self._parent.f_ndofs(index) + + def f_dofs(self, index): + return self._parent.f_dofs(index) + + def f_coefficients(self, index): + return self._parent.f_coefficients(index) + class DisjointUnionBasis(Basis): __slots__ = '_bases', '_dofsplits', '_elemsplits' diff --git a/tests/test_function.py b/tests/test_function.py index 783aa5de5..ad38799b5 100644 --- a/tests/test_function.py +++ b/tests/test_function.py @@ -1229,6 +1229,19 @@ def setUp(self): self.checkndofs = 3 super().setUp() +class WithTransformsBasis(CommonBasis, TestCase): + def setUp(self): + root = function.Root('X', 0) + self.roots = root, + self.checkcoeffs = [[1],[2,3],[4,5],[6]] + self.checkdofs = [[0],[2,3],[1,3],[2]] + self.checkndofs = 4 + parent_transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'abcd'], 0, 0) + parent = function.PlainBasis(self.checkcoeffs, self.checkdofs, 4, parent_transforms, 0, function.SelectChain(self.roots)) + transforms = transformseq.PlainTransforms([(transform.Identifier(0,k),) for k in 'efgh'], 0, 0) + self.basis = function.WithTransformsBasis(parent, transforms, function.SelectChain(self.roots)) + super().setUp() + class DisjointUnionBasis(CommonBasis, TestCase): def setUp(self): root = function.Root('X', 0) From 55634b02ba7dd52c5c24225cf6c5ff132091d486 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Tue, 7 Jan 2020 13:43:41 +0100 Subject: [PATCH 108/119] add topology.WithIdentifierTopology --- nutils/topology.py | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/nutils/topology.py b/nutils/topology.py index feea6b4cb..364722739 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -2526,6 +2526,49 @@ def refined(self): def basis(self, name, *args, **kwargs): return function.asarray([1.]) +class WithIdentifierTopology(Topology): + '''A topology that appends an :class:`nutils.transform.Identifier` to the ``transforms`` and ``opposites`` of another topology. + + Parameters + ---------- + parent : :class:`Topology` + The parent topology. + token : :class:`object` + An immutable token that will be used to create the + :class:`nutils.transform.Identifier`. + ''' + + __slots__ = '_parent', '_root', '_identifier' + + @types.apply_annotations + def __init__(self, parent:stricttopology, root:function.strictroot, identifier:transformseq.stricttransforms): + assert len(identifier) == 1 and sum(identifier.todims) == 0 + self._parent = parent + self._root = root + self._identifier = identifier + super().__init__(parent.roots+(root,), + parent.references, + parent.transforms*identifier, + parent.opposites*identifier) + + def basis(self, *args, **kwargs): + return function.WithTransformsBasis(self._parent.basis(*args, **kwargs), self.transforms, function.SelectChain(self.roots)) + + @property + def refined(self): + return WithIdentifierTopology(self._parent.refined, self._root, self._identifier.refined(elementseq.asreferences([element.PointReference()], 0))) + + @property + def boundary(self): + return WithIdentifierTopology(self._parent.boundary, self._root, self._identifier) + + @property + def interfaces(self): + return WithIdentifierTopology(self._parent.interfaces, self._root, self._identifier) + + def getitem(self, item): + return WithIdentifierTopology(self._parent.getitem(item), self._root, self._identifier) + class PatchBoundary(types.Singleton): __slots__ = 'id', 'dim', 'side', 'reverse', 'transpose' From 7380aa15de04c8f838c1520af8a5b99664d163f0 Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 16 Dec 2019 16:27:50 +0100 Subject: [PATCH 109/119] WIP: partition --- nutils/topology.py | 288 ++++++++++++++++++++++++++++++++++++++- nutils/transform.py | 4 + tests/test_finitecell.py | 27 ++-- 3 files changed, 308 insertions(+), 11 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 364722739..b68beec2d 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -391,7 +391,7 @@ def refine(self, n): n = n[0] return self if n <= 0 else self.refined.refine(n-1) - def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None, *, arguments=None): + def _trim(self, levelset, maxrefine, ndivisions=8, leveltopo=None, *, arguments=None): 'trim element along levelset' if arguments is None: @@ -425,7 +425,19 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None mask[indices] = False refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) log.debug('cache', fcache.stats) - return SubsetTopology(self, refs, newboundary=name) + return refs + + def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None, *, arguments=None): + refs = self._trim(levelset, maxrefine, ndivisions, leveltopo, arguments=arguments) + return SubsetTopology(self, refs, newboundary=name) + + @log.withcontext + @types.apply_annotations + def partition(self, levelset:function.asarray, maxrefine:types.strictint, posname:types.strictstr, negname:types.strictstr, *, ndivisions=8, arguments=None): + partsroot = function.Root('parts', 0) + pos = self._trim(levelset, maxrefine=maxrefine, ndivisions=ndivisions, arguments=arguments) + refs = tuple((pref, bref-pref) for bref, pref in zip(self.references, pos)) + return PartitionedTopology(self, partsroot, refs, (posname, negname)) def subset(self, topo, newboundary=None, strict=False): 'intersection' @@ -1883,7 +1895,7 @@ def basis(self, name, *args, **kwargs): if name == 'discont': return super().basis(name, *args, **kwargs) else: - return function.DisjointUnionBasis(topo.basis(name, *args, **kwargs) for topo in self._topos) + return function.DisjointUnionBasis(tuple(topo.basis(name, *args, **kwargs) for topo in self._topos), function.SelectChain(self.roots)) class SubsetTopology(Topology): 'trimmed' @@ -2070,6 +2082,26 @@ def getitem(self, item): def boundary(self): return self.basetopo.boundary.refined + @property + def interfaces(self): + references = [] + transforms = [] + opposites = [] + for ref, trans in zip(self.basetopo.references, self.basetopo.transforms): + for ichild, (childconn, (ctrans, cref)) in enumerate(zip(ref.connectivity, ref.children)): + for iedge, (ioppchild, (etrans, eref)) in enumerate(zip(childconn, cref.edges)): + if ioppchild >= 0: + references.append(eref) + transforms.append(trans+(ctrans,etrans)) + ioppedge = ref.connectivity[ioppchild].index(ichild) + oppctrans, oppcref = ref.children[ioppchild] + oppetrans = oppcref.edge_transforms[ioppedge] + opposites.append(trans+(oppctrans,oppetrans)) + newifaces = Topology(elementseq.asreferences(references, self.ndims-1), + transformseq.PlainTransforms(transforms, self.ndims-1), + transformseq.PlainTransforms(opposites, self.ndims-1)) + return DisjointUnionTopology([self.basetopo.interfaces.refined, newifaces]) + @property def connectivity(self): offsets = numpy.cumsum([0] + [ref.nchildren for ref in self.basetopo.references]) @@ -2569,6 +2601,256 @@ def interfaces(self): def getitem(self, item): return WithIdentifierTopology(self._parent.getitem(item), self._root, self._identifier) +class PartitionedTopology(DisjointUnionTopology): + + __slots__ = 'basetopo', 'refs', 'names', 'nparts', 'partsroot', '_parts', '_partstransforms', '_nrefined' + __cache__ = 'boundary', 'interfaces', 'refined' + + @types.apply_annotations + def __init__(self, basetopo:stricttopology, partsroot:function.strictroot, refs:types.tuple[types.tuple[element.strictreference]], names:types.tuple[types.strictstr], *, _nrefined=0): + if len(refs) != len(basetopo): + raise ValueError('Expected {} refs tuples but got {}.'.format(len(basetopo), len(refs))) + self.nparts = len(refs[0]) if refs else len(names) + if not all(len(r) == self.nparts for r in refs): + raise ValueError('Variable number of parts.') + if len(names) != self.nparts: + raise ValueError('Expected {} names, one for every part, but got {}.'.format(self.nparts, len(names))) + if any(':' in name for name in names): + raise ValueError('Names may not contain colons.') + if self.nparts == 0: + raise ValueError('A partition consists of at least one part, but got zero.') + 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' + + self.basetopo = basetopo + self.refs = refs + self.names = names + self.partsroot = partsroot + self._nrefined = _nrefined + + self._partstransforms = transformseq.IdentifierTransforms(0, partsroot.name, self.nparts) + for i in range(_nrefined): + self._partstransforms = self._partstransforms.refined(elementseq.asreferences([element.PointReference()], 0)) + indices = tuple(types.frozenarray(numpy.where(list(map(bool, prefs)))[0]) for prefs in zip(*refs)) + self._parts = tuple(WithIdentifierTopology(SubsetTopology(basetopo, prefs), partsroot, self._partstransforms[i:i+1]) for i, prefs in enumerate(zip(*refs))) + super().__init__(self._parts, names) + + def getitem(self, item): + if item in self.names: + return _SubsetOfPartitionedTopology(self, {item}) + else: + topo = self.basetopo.getitem(item) + if not topo: + return topo * EmptyTopology((self.partsroot,), 0) + refs = tuple(tuple(ref & bref for ref in self.refs[self.basetopo.transforms.index(trans)]) for bref, trans in zip(topo.references, topo.transforms)) + return PartitionedTopology(topo, self.partsroot, refs, self.names) + + @property + def boundary(self): + baseboundary = self.basetopo.boundary + brefs = [] + for bref, btrans in zip(baseboundary.references, baseboundary.transforms): + ielem, etrans = self.basetopo.transforms.index_with_tail(btrans) + todims = tuple(t[-1].fromdims for t in self.basetopo.transforms[ielem]) + brefs.append(tuple(pref.edge_refs[transform.index_edge_transforms(pref.edge_transforms, etrans, todims)] for pref in self.refs[ielem])) + return PartitionedTopology(baseboundary, self.partsroot, brefs, self.names, _nrefined=self._nrefined) + + @property + def interfaces(self): + baseifaces = self.basetopo.interfaces + basereferences = {(a, b): [] for a in self.names for b in self.names} + baseindices = {(a, b): [] for a in self.names for b in self.names} + for ieelem, (eref, etrans, oppetrans) in enumerate(zip(baseifaces.references, baseifaces.transforms, baseifaces.opposites)): + ielem, tail = self.basetopo.transforms.index_with_tail(etrans) + ioppelem, opptail = self.basetopo.transforms.index_with_tail(oppetrans) + todims = tuple(t[-1].fromdims for t in self.basetopo.transforms[ielem]) + 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])))) + 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])))) + checkeref = eref.empty + for aname, aeref in erefs: + for bname, beref in opperefs: + parteref = aeref & beref + if parteref: + basereferences[aname, bname].append(parteref) + baseindices[aname, bname].append(ieelem) + checkeref |= parteref + assert checkeref == eref + baseindices = {p: types.frozenarray(i, dtype=int) for p, i in baseindices.items()} + + newedges = {} + def addnewedge(ielem, etrans): + edges = newedges.setdefault(ielem, []) + assert etrans not in edges + iedge = len(edges) + edges.append(etrans) + return ielem, iedge + newreferences = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]} + newtransforms = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]} + newopposites = {(a, b): [] for i, a in enumerate(self.names) for b in self.names[i+1:]} + for ibase, (baseref, partrefs, basetrans) in enumerate(zip(self.basetopo.references, self.refs, self.basetopo.transforms)): + todims = tuple(t[-1].fromdims for t in basetrans) + pool = {} + for aname, aref in zip(self.names, partrefs): + if not aref: + continue + for aetrans, aeref in aref.edges[baseref.nedges:]: + if not aeref: + continue + points = types.frozenarray(aetrans.apply(aeref.getpoints('bezier', 2).coords), copy=False) + bname, beref, betrans = pool.pop(points, (None, None, None)) + if beref is None: + pool[points] = aname, aeref, aetrans + else: + assert aname != bname, 'elements are not supposed to count internal interfaces as edges' + # assert aeref == beref # disabled: aeref.trans is beref.trans.flipped if aeref is a ManifoldReference + if self.names.index(aname) <= self.names.index(bname): + iface = aname, bname + else: + iface = bname, aname + aetrans, betrans, aeref, beref = betrans, aetrans, beref, aeref + newreferences[iface].append(aeref) + newtransforms[iface].append(addnewedge(ibase, aetrans.separate(todims))) + newopposites[iface].append(addnewedge(ibase, betrans.separate(todims))) + + assert not pool, 'some interal edges have no opposites' + + if newedges: + newielems, newedges = zip(*sorted(newedges.items(), key=lambda item: item[0])) + newoffsets = dict(zip(newielems, numpy.cumsum([0, *map(len, newedges)]))) + newedges = transformseq.TrimmedEdgesTransforms(self.basetopo.transforms[numpy.asarray(newielems)], newedges) + itopos = [] + inames = [] + T = lambda i, j: transformseq.PlainTransforms(((*self._partstransforms[i][0], self._partstransforms[j][0][0]),), 0, 0) + for i, a in enumerate(self.names): + itopos.append(Topology(self.roots, + elementseq.asreferences(basereferences[a, a], self.ndims-1), + baseifaces.transforms[baseindices[a, a]]*T(i, i), + baseifaces.opposites[baseindices[a, a]]*T(i, i))) + inames.append('{0}:{0}'.format(a)) + for j, b in enumerate(self.names[i+1:], i+1): + base = Topology(self.roots, + elementseq.asreferences(basereferences[a, b] + basereferences[b, a], self.ndims-1), + transformseq.chain((baseifaces.transforms[baseindices[a, b]], baseifaces.opposites[baseindices[b, a]]), self.basetopo.transforms.todims)*T(i, j), + transformseq.chain((baseifaces.opposites[baseindices[a, b]], baseifaces.transforms[baseindices[b, a]]), self.basetopo.transforms.todims)*T(j, i)) + if newreferences[a, b]: + newreferencesab = elementseq.asreferences(newreferences[a, b], self.ndims-1) + newtransformsab = newedges[numpy.fromiter((newoffsets[ielem]+iedge for ielem, iedge in newtransforms[a, b]), dtype=int)] + newoppositesab = newedges[numpy.fromiter((newoffsets[ielem]+iedge for ielem, iedge in newopposites[a, b]), dtype=int)] + new = Topology(self.roots, newreferencesab, newtransformsab*T(i, j), newoppositesab*T(j, i)) + itopos.append(DisjointUnionTopology((base, new))) + else: + itopos.append(base) + inames.append('{}:{}'.format(a, b)) + return DisjointUnionTopology(itopos, inames) + + def __sub__(self, other): + if self == other: + return self.empty + elif isinstance(other, _SubsetOfPartitionedTopology) and other._partition == self: + remainder = frozenset(self.names) - frozenset(other._names) + if remainder: + return _SubsetOfPartitionedTopology(self, remainder) + else: + return self.empty + else: + return super().__sub__(other) + + @property + def refined(self): + refbasetopo = self.basetopo.refined + refbindex = refbasetopo.transforms.index + refinedrefs = [crefs for refs in self.refs for crefs in zip(*(ref.child_refs for ref in refs))] + indices = numpy.argsort([refbindex(ctrans) + for trans, ref in zip(self.basetopo.transforms, self.references) + for ctrans in transform.child_transforms(trans, ref)]) + refinedrefs = tuple(map(refinedrefs.__getitem__, indices)) + return PartitionedTopology(refbasetopo, self.partsroot, refinedrefs, self.names, _nrefined=self._nrefined+1) + +class _SubsetOfPartitionedTopology(DisjointUnionTopology): + + __slots__ = '_partition', '_names' + __cache__ = 'boundary', 'interfaces' + + @types.apply_annotations + def __init__(self, partition: stricttopology, names: frozenset): + self._partition = partition + if not names <= frozenset(partition.names): + raise ValueError('Not a subset of the partition.') + if not all(isinstance(name, str) for name in names): + raise ValueError('All names should be str objects.') + self._names = tuple(sorted(names, key=partition.names.index)) + super().__init__(tuple(self._partition._parts[self._partition.names.index(name)] for name in self._names), self._names) + + def __getitem__(self, item): + if item in self._names: + return _SubsetOfPartitionedTopology(self._partition, {item}) + elif item in self._partition.names: + return self.empty + else: + topo = self._partition.getitem(item) + assert not isinstance(topo, _SubsetOfPartitionedTopology) # this is covered by the above two conditionals + if not topo: + return topo + elif isinstance(topo, PartitionedTopology): + return _SubsetOfPartitionedTopology(topo, self._names) + else: + raise NotImplementedError + + @property + def boundary(self): + # The boundary of this subset consists of the boundary of the base that + # touches this subset and the interfaces between all parts in this subset + # and all parts not in this subset. All interfaces are grouped and named by + # the parts not in this subset: given a partition A, B of Ω, then + # `Ω['A'].boundary['B']` is the same as `Ω.interfaces['A:B']` or + # `~Ω.interfaces['B:A']`, whichever exists. + topos = [] + names = [] + for b in self._partition.names: # parts not in this subset + if b in self._names: + continue + btopos = [] + for a in self._names: # parts in this subset + if self._partition.names.index(a) <= self._partition.names.index(b): + btopos.append(self._partition.interfaces.getitem('{}:{}'.format(a, b))) + else: + btopos.append(~self._partition.interfaces.getitem('{}:{}'.format(b, a))) + topos.append(DisjointUnionTopology(btopos)) + names.append(b) + for name in self._names: + topos.append(self._partition.boundary.getitem(name)) + groups = {} + return DisjointUnionTopology(topos, names) + + @property + def interfaces(self): + topos = [] + names = [] + for i, a in enumerate(self._names): + for b in self._names[i:]: + topos.append(self._partition.interfaces.getitem('{}:{}'.format(a, b))) + names.append('{}:{}'.format(a, b)) + return DisjointUnionTopology(topos, names) + + def __or__(self, other): + if isinstance(other, _SubsetOfPartitionedTopology) and other._partition == self._partition: + return _SubsetOfPartitionedTopology(self._partition, frozenset(self._names) | frozenset(other._names)) + else: + return super().__or__(other) + + def __rsub__(self, other): + if self._partition == other or self._partition.basetopo == other: + remainder = frozenset(self._partition.names) - frozenset(self._names) + if remainder: + return _SubsetOfPartitionedTopology(self._partition, remainder) + else: + return self.empty + else: + return super().__rsub__(other) + + @property + def refined(self): + return _SubsetOfPartitionedTopology(self._partition.refined, self._names) + class PatchBoundary(types.Singleton): __slots__ = 'id', 'dim', 'side', 'reverse', 'transpose' diff --git a/nutils/transform.py b/nutils/transform.py index 28e26d139..e91445a53 100644 --- a/nutils/transform.py +++ b/nutils/transform.py @@ -635,6 +635,10 @@ def __init__(self, ndims:types.strictint, trans:stricttransformitem): def flipped(self): return Manifold(self.fromdims, self.trans.flipped) + @property + def isflipped(self): + return self.trans.isflipped + def swapdown(self, other): if isinstance(other, (TensorChild, SimplexChild)): return ScaledUpdim(other, self), Identity(self.fromdims) diff --git a/tests/test_finitecell.py b/tests/test_finitecell.py index 23ced2c23..940ff2c1a 100644 --- a/tests/test_finitecell.py +++ b/tests/test_finitecell.py @@ -358,6 +358,7 @@ def test_trimtopright(self): self.assertEqual(len(self.domain5.boundary['trimtopright']), 6) +@parametrize class partialtrim(TestCase): # Test setup: @@ -372,8 +373,13 @@ class partialtrim(TestCase): def setUp(self): self.topo, self.geom = mesh.rectilinear([2,2]) geom = self.geom - self.topoA = self.topo.trim(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1) - self.topoB = self.topo - self.topoA + if self.method == 'trim': + self.topoA = self.topo.trim(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1) + self.topoB = self.topo - self.topoA + elif self.method == 'partition': + self.partitioned = self.topo.partition(geom[0]-1+geom[1]*(geom[1]-.5), maxrefine=1, posname='A', negname='B') + self.topoA = self.partitioned['A'] + self.topoB = self.partitioned['B'] def test_topos(self): self.assertEqual(len(self.topoA), 4) @@ -382,26 +388,27 @@ def test_topos(self): def test_boundaries(self): self.assertEqual(len(self.topoA.boundary), 11) self.assertEqual(len(self.topoB.boundary), 8) - self.assertEqual(len(self.topoA.boundary['trimmed']), 5) - self.assertEqual(len(self.topoB.boundary['trimmed']), 5) + self.assertEqual(len(self.topoA.boundary['B' if self.method == 'partition' else 'trimmed']), 5) + self.assertEqual(len(self.topoB.boundary['A' if self.method == 'partition' else 'trimmed']), 5) def test_interfaces(self): self.assertEqual(len(self.topoA.interfaces), 4) self.assertEqual(len(self.topoB.interfaces), 1) def test_transforms(self): - self.assertEqual(set(self.topoA.boundary['trimmed'].transforms), set(self.topoB.boundary['trimmed'].opposites)) - self.assertEqual(set(self.topoB.boundary['trimmed'].transforms), set(self.topoA.boundary['trimmed'].opposites)) + 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)) + 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)) def test_opposites(self): ielem = function.elemwise(self.topo.roots, self.topo.transforms, self.topo.ndims, numpy.arange(4)) - sampleA = self.topoA.boundary['trimmed'].sample('uniform', 1) - sampleB = self.topoB.boundary['trimmed'].sample('uniform', 1) + sampleA = self.topoA.boundary['B' if self.method == 'partition' else 'trimmed'].sample('uniform', 1) + sampleB = self.topoB.boundary['A' if self.method == 'partition' else 'trimmed'].sample('uniform', 1) self.assertEqual(set(sampleB.eval(ielem)), {0,1}) self.assertEqual(set(sampleB.eval(function.opposite(ielem))), {0,1,2}) self.assertEqual(set(sampleA.eval(ielem)), {0,1,2}) self.assertEqual(set(sampleA.eval(function.opposite(ielem))), {0,1}) + @parametrize.enable_if(lambda method, **kwargs: method != 'partition') def test_baseboundaries(self): # the base implementation should create the correct boundary topology but # without interface opposites and without the trimmed group @@ -418,3 +425,7 @@ def test_volumes(self): lhs = self.topoB.integrate(f.grad(geom)*function.J(geom), ischeme='gauss2') rhs = self.topoB.boundary.integrate(f*function.normal(geom)*function.J(geom), ischeme='gauss2') numpy.testing.assert_array_almost_equal(lhs, rhs) + + +partialtrim(method='trim') +partialtrim(method='partition') From d2a4ca6137b8ad375135d2afbe182d3a6ff75a5e Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Sat, 17 Apr 2021 00:32:06 +0200 Subject: [PATCH 110/119] support alternate root name in partition --- nutils/topology.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index b68beec2d..721de653f 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -433,8 +433,8 @@ def trim(self, levelset, maxrefine, ndivisions=8, name='trimmed', leveltopo=None @log.withcontext @types.apply_annotations - def partition(self, levelset:function.asarray, maxrefine:types.strictint, posname:types.strictstr, negname:types.strictstr, *, ndivisions=8, arguments=None): - partsroot = function.Root('parts', 0) + def partition(self, levelset:function.asarray, maxrefine:types.strictint, posname:types.strictstr, negname:types.strictstr, *, ndivisions=8, arguments=None, rootname:types.strictstr='parts'): + partsroot = function.Root(rootname, 0) pos = self._trim(levelset, maxrefine=maxrefine, ndivisions=ndivisions, arguments=arguments) refs = tuple((pref, bref-pref) for bref, pref in zip(self.references, pos)) return PartitionedTopology(self, partsroot, refs, (posname, negname)) @@ -2644,6 +2644,13 @@ def getitem(self, item): refs = tuple(tuple(ref & bref for ref in self.refs[self.basetopo.transforms.index(trans)]) for bref, trans in zip(topo.references, topo.transforms)) return PartitionedTopology(topo, self.partsroot, refs, self.names) + def slice(self, items): + topo = self.basetopo.slice(items) + if not topo: + return topo * EmptyTopology((self.partsroot,), 0) + refs = tuple(tuple(ref & bref for ref in self.refs[self.basetopo.transforms.index(trans)]) for bref, trans in zip(topo.references, topo.transforms)) + return PartitionedTopology(topo, self.partsroot, refs, self.names) + @property def boundary(self): baseboundary = self.basetopo.boundary From 302159162b96f5140f934d5a9ae57a8214ead0c3 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Wed, 23 Sep 2020 17:01:25 +0200 Subject: [PATCH 111/119] prune gmsh periodic table of nonexisting vertices --- nutils/mesh.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nutils/mesh.py b/nutils/mesh.py index 8e60a1fe4..86c30fae5 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -341,6 +341,14 @@ def parsegmsh(mshdata): identities = numpy.zeros((0, 2), dtype=int) if not msh.gmsh_periodic \ else numpy.concatenate([d for a, b, c, d in msh.gmsh_periodic], axis=0) + # It may happen that meshio provides periodicity relations for nodes that + # have no associated coordinate, typically because they are not part of any + # physical group. We need to filter these out to avoid errors further down. + mask = identities < len(coords) + keep = mask.any(axis=1) + assert mask[keep].all() + identities = identities[keep] + # Tags is a list of (nd, name, ndelems) tuples that define topological groups # per dimension. Since meshio associates group names with cells, which are # concatenated in nodes, element ids are offset and concatenated to match. From 1aaf319935a6b11a88991a44b864761c305c8d8f Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Wed, 23 Sep 2020 17:02:49 +0200 Subject: [PATCH 112/119] skip gmsh edge elements with no adjacent area --- nutils/mesh.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nutils/mesh.py b/nutils/mesh.py index 86c30fae5..6eba5ce12 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -417,11 +417,13 @@ def parsegmsh(mshdata): if nd == ndims: vtags[name] = numpy.array(ielems) elif nd == ndims-1: - edgenodes = bnodes[ielems] - nodemask = numeric.asboolean(edgenodes.ravel(), size=nnodes, ordered=False) - ielems, = (nodemask[vnodes].sum(axis=1) >= ndims).nonzero() # all elements sharing at least ndims edgenodes + edgenodes = bnodes[ielems] # all edge elements in msh file + nodemask = numeric.asboolean(edgenodes.ravel(), size=nnodes, ordered=False) # all elements sharing at least 1 edge node + ielems, = (nodemask[vnodes].sum(axis=1) >= ndims).nonzero() # all elements sharing at least ndims edge nodes edgemap = {tuple(b): (ielem, iedge) for ielem, a in zip(ielems, vnodes[ielems[:,_,_], edge_vertices[_,:,:]]) for iedge, b in enumerate(a)} - btags[name] = numpy.array([edgemap[tuple(sorted(n))] for n in edgenodes]) + belems = (edgemap.get(tuple(sorted(n))) for n in edgenodes) # map every edge element to its corresponding (ielem, iedge) combination + belems = filter(None, belems) # remove spurious edge elements that have no adjacent volume element + btags[name] = numpy.array(list(belems)) elif nd == 0: ptags[name] = pnodes[ielems][...,0] From 92ada06bd69fcda312b9a0d9e1befc2ee6df45a6 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Wed, 23 Sep 2020 17:03:23 +0200 Subject: [PATCH 113/119] remove unused vnodes and tidy up --- nutils/mesh.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/nutils/mesh.py b/nutils/mesh.py index 6eba5ce12..2562802c0 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -499,20 +499,12 @@ def simplex(nodes, cnodes, coords, tags, btags, ptags, name='simplex'): nverts = len(coords) nelems, ncnodes = cnodes.shape ndims = nodes.shape[1] - 1 - assert len(nodes) == nelems - assert numpy.greater(nodes[:,1:], nodes[:,:-1]).all(), 'nodes must be sorted' + degree = 1 if ncnodes == ndims+1 else int((ncnodes * math.factorial(ndims))**(1/ndims))-1 - if ncnodes == ndims+1: - degree = 1 - vnodes = cnodes - else: - degree = int((ncnodes * math.factorial(ndims))**(1/ndims))-1 # degree**ndims/ndims! < ncnodes < (degree+1)**ndims/ndims! - dims = numpy.arange(ndims) - strides = (dims+1+degree).cumprod() // (dims+1).cumprod() # (i+1+degree)!/(i+1)! - assert strides[-1] == ncnodes - vnodes = cnodes[:,(0,*strides-1)] + assert len(nodes) == nelems, 'number of simplex vertices and coordinates do not match' + assert numpy.greater(nodes[:,1:], nodes[:,:-1]).all(), 'nodes must be sorted' + assert ncnodes == _comb(ndims + degree, degree), 'number of coordinate nodes does not correspond to uniformly refined simplex' - assert vnodes.shape == nodes.shape transforms = transformseq.IdentifierTransforms(ndims=ndims, name=name, length=nelems) root = function.Root(name, ndims) topo = topology.SimplexTopology(root, nodes, transforms, transforms) @@ -669,4 +661,9 @@ def unitsquare(nelems, etype): return topo, geom/nelems +try: + from math import comb as _comb # new in Python 3.8 +except ImportError: + _comb = lambda n, k: numpy.arange(1+max(k,n-k),1+n).prod() // math.factorial(min(k,n-k)) + # vim:sw=2:sts=2:et From a2ccd39d87967e0a8a24c21d038215d3d8140a1a Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Thu, 15 Oct 2020 15:23:03 +0200 Subject: [PATCH 114/119] refactor parsegmsh to support meshio < and >=4.3.1 --- nutils/mesh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nutils/mesh.py b/nutils/mesh.py index 2562802c0..b53951740 100644 --- a/nutils/mesh.py +++ b/nutils/mesh.py @@ -352,10 +352,10 @@ def parsegmsh(mshdata): # Tags is a list of (nd, name, ndelems) tuples that define topological groups # per dimension. Since meshio associates group names with cells, which are # concatenated in nodes, element ids are offset and concatenated to match. - tags = [(msh.field_data[name][1], name, numpy.concatenate([selection + tags = [(nd, name, numpy.concatenate([selection + sum(len(cells.data) for cells in msh.cells[:icell] if cells.type == msh.cells[icell].type) # offset into nodes - for icell, selection in enumerate(selections)])) - for name, selections in msh.cell_sets.items()] + for icell, selection in enumerate(msh.cell_sets[name])])) + for name, (itag, nd) in msh.field_data.items()] # determine the dimension of the topology ndims = max(nodes) From bdd429bc887848a305b4375377a8a66812c47aae Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Wed, 19 May 2021 01:20:09 +0200 Subject: [PATCH 115/119] fixes --- nutils/topology.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 721de653f..9d9191868 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -2670,8 +2670,23 @@ def interfaces(self): ielem, tail = self.basetopo.transforms.index_with_tail(etrans) ioppelem, opptail = self.basetopo.transforms.index_with_tail(oppetrans) todims = tuple(t[-1].fromdims for t in self.basetopo.transforms[ielem]) - 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])))) - 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])))) + def get_sub_ref(ref, chains, todims): + if not any(chains): + return ref + else: + fromdims = tuple(chain[0].fromdims if chain else todim for chain, todim in zip(chains, todims)) + if fromdims == todims: + ichild, chains = transform.index_child_transforms_with_tail(ref.child_transforms, chains, todims) + return get_sub_ref(ref.child_refs[ichild], chains, fromdims) + elif sum(todims) == sum(fromdims) + 1: + ichild, chains = transform.index_edge_transforms_with_tail(ref.edge_transforms, chains, todims) + return get_sub_ref(ref.edge_refs[ichild], chains, fromdims) + else: + raise NotImplementedError + tail = tuple(map(transform.canonical, tail)) + opptail = tuple(map(transform.canonical, opptail)) + erefs = tuple(filter(lambda item: item[1], ((i, get_sub_ref(ref, tail, todims)) for i, ref in zip(self.names, self.refs[ielem])))) + opperefs = tuple(filter(lambda item: item[1], ((i, get_sub_ref(ref, opptail, todims)) for i, ref in zip(self.names, self.refs[ioppelem])))) checkeref = eref.empty for aname, aeref in erefs: for bname, beref in opperefs: @@ -2718,7 +2733,7 @@ def addnewedge(ielem, etrans): newtransforms[iface].append(addnewedge(ibase, aetrans.separate(todims))) newopposites[iface].append(addnewedge(ibase, betrans.separate(todims))) - assert not pool, 'some interal edges have no opposites' + assert not pool, 'some internal edges have no opposites' if newedges: newielems, newedges = zip(*sorted(newedges.items(), key=lambda item: item[0])) @@ -2772,6 +2787,19 @@ def refined(self): refinedrefs = tuple(map(refinedrefs.__getitem__, indices)) return PartitionedTopology(refbasetopo, self.partsroot, refinedrefs, self.names, _nrefined=self._nrefined+1) + def refined_by_base(self, base_indices): + refbasetopo = self.basetopo.refined_by(base_indices) + refbindex = refbasetopo.transforms.index + refinedrefs = [None]*len(refbasetopo) + for ibase, (bref, btrans) in enumerate(zip(self.basetopo.references, self.basetopo.transforms)): + prefs = self.refs[ibase] + if ibase in base_indices: + for ichild, ctrans in enumerate(bref.child_transforms): + refinedrefs[refbindex(transform.append_child(btrans, ctrans))] = tuple(ref.child_refs[ichild] for ref in prefs) + else: + refinedrefs[refbindex(btrans)] = prefs + return PartitionedTopology(refbasetopo, self.partsroot, refinedrefs, self.names, _nrefined=0) + class _SubsetOfPartitionedTopology(DisjointUnionTopology): __slots__ = '_partition', '_names' From 21a144bcd32884bd9c69c3e9a34812d96abfa196 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Wed, 29 Apr 2020 12:31:47 +0200 Subject: [PATCH 116/119] make tol a mandatory argument for locate --- nutils/topology.py | 12 +++--------- tests/test_finitecell.py | 2 +- tests/test_topology.py | 12 ++++++------ 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 9d9191868..8e1b08f71 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -495,7 +495,7 @@ def select(self, indicator, ischeme='bezier2', **kwargs): return self[selected] @log.withcontext - def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, maxiter=100, arguments=None): + def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter=100, arguments=None): '''Create a sample based on physical coordinates. In a finite element application, functions are commonly evaluated in points @@ -510,7 +510,7 @@ def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, ma >>> from . import mesh >>> domain, geom = mesh.unitsquare(nelems=3, etype='mixed') - >>> sample = domain.locate(geom, [[.9, .4]]) + >>> sample = domain.locate(geom, [[.9, .4]], tol=1e-12) >>> sample.eval(geom).tolist() [[0.9, 0.4]] @@ -545,9 +545,6 @@ def locate(self, geom, coords, *, ischeme='vertex', scale=1, tol=None, eps=0, ma located : :class:`nutils.sample.Sample` ''' - if tol is None: - warnings.deprecation('locate without tol argument is deprecated, please provide an explicit tolerance') - tol = 1e-12 coords = numpy.asarray(coords, dtype=float) if geom.ndim == 0: geom = geom[_] @@ -1678,10 +1675,7 @@ def refined(self): else transformseq.BndAxis(i=axis.i*2,j=axis.j*2,ibound=axis.ibound,side=axis.side) for axis in self.axes] return StructuredTopology(self.root, axes, self.nrefine+1, bnames=self._bnames) - def locate(self, geom, coords, *, eps=0, tol=None, **kwargs): - if tol is None: - warnings.deprecation('locate without tol argument is deprecated, please provide an explicit tolerance') - tol = 1e-12 + def locate(self, geom, coords, *, tol, eps=0, **kwargs): coords = numpy.asarray(coords, dtype=float) if geom.ndim == 0: geom = geom[_] diff --git a/tests/test_finitecell.py b/tests/test_finitecell.py index 940ff2c1a..1a453f8be 100644 --- a/tests/test_finitecell.py +++ b/tests/test_finitecell.py @@ -247,7 +247,7 @@ def test_locate(self): point = p * .5**numpy.arange(self.domain.ndims) r = numpy.linalg.norm(point) try: - sample = self.pos.locate(curvegeom, [point]) + sample = self.pos.locate(curvegeom, [point], tol=1e-12) except topology.LocateError: self.assertGreater(r, self.radius) else: diff --git a/tests/test_topology.py b/tests/test_topology.py index 4830ac7be..3a0407a21 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -322,35 +322,35 @@ def setUp(self): def test(self): target = numpy.array([(.2,.3), (.1,.9), (0,1)]) - sample = self.domain.locate(self.geom, target, eps=1e-15) + sample = self.domain.locate(self.geom, target, eps=1e-15, tol=1e-12) located = sample.eval(self.geom) self.assertAllAlmostEqual(located, target) def test_invalidargs(self): target = numpy.array([(.2,), (.1,), (0,)]) with self.assertRaises(Exception): - self.domain.locate(self.geom, target, eps=1e-15) + self.domain.locate(self.geom, target, eps=1e-15, tol=1e-12) def test_invalidpoint(self): target = numpy.array([(.3, 1)]) # outside domain, but inside basetopo for mode==trimmed with self.assertRaises(topology.LocateError): - self.domain.locate(self.geom, target, eps=1e-15) + self.domain.locate(self.geom, target, eps=1e-15, tol=1e-12) def test_boundary(self): target = numpy.array([(.2,), (.1,), (0,)]) - sample = self.domain.boundary['bottom'].locate(self.geom[:1], target, eps=1e-15) + sample = self.domain.boundary['bottom'].locate(self.geom[:1], target, eps=1e-15, tol=1e-12) located = sample.eval(self.geom[:1]) self.assertAllAlmostEqual(located, target) def test_boundary_scalar(self): target = numpy.array([.3, .9, 1]) - sample = self.domain.boundary['left'].locate(self.geom[1], target, eps=1e-15) + sample = self.domain.boundary['left'].locate(self.geom[1], target, eps=1e-15, tol=1e-12) located = sample.eval(self.geom[1]) self.assertAllAlmostEqual(located, target) for etype in 'square', 'triangle', 'mixed': for mode in 'linear', 'nonlinear', 'trimmed': - locate(etype=etype, mode=mode) + locate(etype=etype, mode=mode, tol=1e-12) @parametrize From 480ff6749b159c3c5686a86db50abcf771a18995 Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Sun, 5 Jul 2020 20:36:02 +0200 Subject: [PATCH 117/119] add weights argument to locate --- nutils/topology.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 8e1b08f71..74df0c866 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -495,7 +495,7 @@ def select(self, indicator, ischeme='bezier2', **kwargs): return self[selected] @log.withcontext - def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter=100, arguments=None): + def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter=100, arguments=None, weights=None): '''Create a sample based on physical coordinates. In a finite element application, functions are commonly evaluated in points @@ -539,6 +539,8 @@ def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter Maximum allowed number of Newton iterations. arguments : :class:`dict` (default: None) Arguments for function evaluation. + weights : :class:`float` array (default: None) + Optional weights, in case ``coords`` are quadrature points. Returns ------- @@ -588,15 +590,16 @@ def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter break else: raise LocateError('failed to locate point: {}'.format(coord)) - return self._sample(ielems, xis) + return self._sample(ielems, xis, weights) - def _sample(self, ielems, coords): + def _sample(self, ielems, coords, weights=None): uielems = numpy.unique(ielems) points_ = [] index = [] for ielem in uielems: w, = numpy.equal(ielems, ielem).nonzero() - points_.append(points.CoordsPoints(coords[w])) + points_.append(points.CoordsPoints(coords[w]) if weights is None + else points.CoordsWeightsPoints(coords[w], weights[w])) index.append(w) transforms = self.transforms[uielems], if len(self.transforms) == 0 or self.opposites != self.transforms: @@ -1675,7 +1678,7 @@ def refined(self): else transformseq.BndAxis(i=axis.i*2,j=axis.j*2,ibound=axis.ibound,side=axis.side) for axis in self.axes] return StructuredTopology(self.root, axes, self.nrefine+1, bnames=self._bnames) - def locate(self, geom, coords, *, tol, eps=0, **kwargs): + def locate(self, geom, coords, *, tol, eps=0, weights=None, **kwargs): coords = numpy.asarray(coords, dtype=float) if geom.ndim == 0: geom = geom[_] @@ -1690,7 +1693,7 @@ def locate(self, geom, coords, *, tol, eps=0, **kwargs): scale = x[self.ndims:] e = self.sample('uniform', 2).eval(function.norm2(geom0 + index * scale - geom)).max() # inf-norm on non-gauss sample if e > tol: - return super().locate(geom, coords, eps=eps, tol=tol, **kwargs) + return super().locate(geom, coords, eps=eps, tol=tol, weights=weights, **kwargs) log.info('locate detected linear geometry: x = {} + {} xi ~{:+.1e}'.format(geom0, scale, e)) mincoords, maxcoords = numpy.sort([geom0, geom0 + scale * self.shape], axis=0) outofbounds = numpy.less(coords, mincoords - eps) | numpy.greater(coords, maxcoords + eps) @@ -1698,7 +1701,7 @@ def locate(self, geom, coords, *, tol, eps=0, **kwargs): raise LocateError('failed to locate {}/{} points'.format(outofbounds.sum(), len(coords))) xi = (coords - geom0) / scale ielem = numpy.minimum(numpy.maximum(xi.astype(int), 0), numpy.array(self.shape)-1) - return self._sample(numpy.ravel_multi_index(ielem.T, self.shape), xi - ielem) + return self._sample(numpy.ravel_multi_index(ielem.T, self.shape), xi - ielem, weights) def __str__(self): 'string representation' From 1d7b8028536d643472245a9e4a335ac83aa7d16a Mon Sep 17 00:00:00 2001 From: Gertjan van Zwieten Date: Thu, 10 Dec 2020 16:27:39 +0100 Subject: [PATCH 118/119] remove locate args ischeme and scale, add maxdist Formerly locate made an initial selection of candidate elements based on (scaled) bounding boxes. The new implementation simply orders elements by centroid proximity, with an option to set a cutoff radius in case fast failure is required. --- nutils/topology.py | 27 +++++++++++++-------------- tests/test_topology.py | 12 +++++++++++- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 74df0c866..29bb4e54a 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -495,7 +495,7 @@ def select(self, indicator, ischeme='bezier2', **kwargs): return self[selected] @log.withcontext - def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter=100, arguments=None, weights=None): + def locate(self, geom, coords, *, tol, eps=0, maxiter=100, arguments=None, weights=None, maxdist=None, ischeme=None, scale=None): '''Create a sample based on physical coordinates. In a finite element application, functions are commonly evaluated in points @@ -526,12 +526,6 @@ def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter Array of coordinates with ``ndims`` columns. tol : :class:`float` Maximum allowed distance between original and located coordinate. - ischeme : :class:`str` (default: "vertex") - Sample points used to determine bounding boxes. - scale : :class:`float` (default: 1) - Bounding box amplification factor, useful when element shapes are - distorted. Setting this to >1 can increase computational effort but is - otherwise harmless. eps : :class:`float` (default: 0) Epsilon radius around element within which a point is considered to be inside. @@ -541,23 +535,27 @@ def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter Arguments for function evaluation. weights : :class:`float` array (default: None) Optional weights, in case ``coords`` are quadrature points. + maxdist : :class:`float` (default: None) + Speed up failure by setting a distance between point and element + centroid above which the element is rejected immediately. If all points + are expected to be located then this can safely be left unspecified. Returns ------- located : :class:`nutils.sample.Sample` ''' + if ischeme is not None: + warnings.deprecation('the ischeme argument is deprecated and will be removed in future') + if scale is not None: + warnings.deprecation('the scale argument is deprecated and will be removed in future') coords = numpy.asarray(coords, dtype=float) if geom.ndim == 0: geom = geom[_] coords = coords[...,_] if not geom.shape == coords.shape[1:] == (self.ndims,): raise Exception('invalid geometry or point shape for {}D topology'.format(self.ndims)) - bboxsample = self.sample(*element.parse_legacy_ischeme(ischeme)) - vertices = map(bboxsample.eval(geom, **arguments or {}).__getitem__, bboxsample.indexiter) - bboxes = numpy.array([numpy.mean(v,axis=0) * (1-scale) + numpy.array([numpy.min(v,axis=0), numpy.max(v,axis=0)]) * scale - for v in vertices]) # nelems x {min,max} x ndims - vref = element.getsimplex(0) + centroids = self.elem_mean(geom, geometry=geom, degree=2) ielems = parallel.shempty(len(coords), dtype=int) xis = parallel.shempty((len(coords),len(geom)), dtype=float) subsamplemetas = function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims, ndimspoints=self.ndims), @@ -566,8 +564,9 @@ def locate(self, geom, coords, *, tol, ischeme='vertex', scale=1, eps=0, maxiter with parallel.ctxrange('locating', len(coords)) as ipoints: for ipoint in ipoints: coord = coords[ipoint] - ielemcandidates, = numpy.logical_and(numpy.greater_equal(coord, bboxes[:,0,:]), numpy.less_equal(coord, bboxes[:,1,:])).all(axis=-1).nonzero() - for ielem in sorted(ielemcandidates, key=lambda i: numpy.linalg.norm(bboxes[i].mean(0)-coord)): + dist = numpy.linalg.norm(centroids - coord, axis=1) + for ielem in numpy.argsort(dist) if maxdist is None \ + else sorted((dist < maxdist).nonzero()[0], key=dist.__getitem__): converged = False ref = self.references[ielem] p = ref.getpoints('gauss', 1) diff --git a/tests/test_topology.py b/tests/test_topology.py index 3a0407a21..d0f776825 100644 --- a/tests/test_topology.py +++ b/tests/test_topology.py @@ -1,6 +1,7 @@ from nutils import * from nutils.testing import * -import numpy, copy, sys, pickle, subprocess, base64, itertools, os +from nutils.elementseq import References +import numpy, copy, sys, pickle, subprocess, base64, itertools, os, unittest class TopologyAssertions: @@ -326,6 +327,15 @@ def test(self): located = sample.eval(self.geom) self.assertAllAlmostEqual(located, target) + @parametrize.enable_if(lambda etype, mode, **kwargs: etype != 'square' or mode == 'nonlinear') + def test_maxdist(self): + target = numpy.array([(.2,.3), (.1,.9), (0,1)]) + with self.assertRaises(topology.LocateError): + self.domain.locate(self.geom, [(0, .3)], eps=1e-15, tol=1e-12, maxdist=.001) + sample = self.domain.locate(self.geom, target, eps=1e-15, tol=1e-12, maxdist=.5) + located = sample.eval(self.geom) + self.assertAllAlmostEqual(located, target) + def test_invalidargs(self): target = numpy.array([(.2,), (.1,), (0,)]) with self.assertRaises(Exception): From 38dbbce722b779984646581a16332a7adb2a08ba Mon Sep 17 00:00:00 2001 From: Joost van Zwieten Date: Mon, 24 May 2021 22:33:49 +0200 Subject: [PATCH 119/119] parallelize Topology.trim --- nutils/topology.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/nutils/topology.py b/nutils/topology.py index 29bb4e54a..50397497e 100644 --- a/nutils/topology.py +++ b/nutils/topology.py @@ -400,10 +400,15 @@ def _trim(self, levelset, maxrefine, ndivisions=8, leveltopo=None, *, arguments= refs = [] levelset = levelset.prepare_eval(subsamples=(function.SubsampleMeta(roots=self.roots, ndimsnormal=sum(root.ndims for root in self.roots)-self.ndims),), transforms=(self.transforms, self.opposites)).simplified if leveltopo is None: - with log.iter.percentage('trimming', self.references, self.transforms, self.opposites) as items: - for ielem, (ref, trans, opp) in enumerate(items): - levels = levelset.eval(function.Subsample(roots=self.roots, transforms=(self.transforms, self.opposites), points=ref.getpoints('vertex', maxrefine), ielem=ielem), **arguments) - refs.append(ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions)) + from multiprocessing import Manager + with Manager() as manager: + refs = manager.list([None]*len(self)) + with parallel.ctxrange('trimming', len(self)) as ielems: + for ielem in ielems: + ref, trans, opp = self.references[ielem], self.transforms[ielem], self.opposites[ielem] + levels = levelset.eval(function.Subsample(roots=self.roots, transforms=(self.transforms, self.opposites), points=ref.getpoints('vertex', maxrefine), ielem=ielem), **arguments) + refs[ielem] = ref.trim(levels, maxrefine=maxrefine, ndivisions=ndivisions) + refs = list(refs) else: log.info('collecting leveltopo elements') bins = [dict() for ielem in range(len(self))]