diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 34ebe33ae273291063fc884cb3bd4d157271a499..8ec4e2344bd54b6705cf1ccecbaeef2a1fa54571 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,7 @@ Python 2.7 POCL: script: - export PY_EXE=python2.7 - - export PYOPENCL_TEST=portable + - export PYOPENCL_TEST=portable:pthread - export EXTRA_INSTALL="pybind11 cython numpy mako mpi4py" # cython is here because pytential (for now, for TS) depends on it - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project.sh @@ -36,7 +36,7 @@ Python 3 Nvidia K40: Python 3 POCL: script: - export PY_EXE=python3 - - export PYOPENCL_TEST=portable + - export PYOPENCL_TEST=portable:pthread # cython is here because pytential (for now, for TS) depends on it - export EXTRA_INSTALL="pybind11 cython numpy mako mpi4py" - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project.sh @@ -54,7 +54,7 @@ Python 3 POCL: Python 3 POCL: script: - export PY_EXE=python3 - - export PYOPENCL_TEST=portable + - export PYOPENCL_TEST=portable:pthread # cython is here because pytential (for now, for TS) depends on it - export EXTRA_INSTALL="pybind11 cython numpy mako mpi4py" - curl -L -O -k https://gitlab.tiker.net/inducer/ci-support/raw/master/build-and-test-py-project.sh diff --git a/.test-conda-env-py3.yml b/.test-conda-env-py3.yml index f277a48523488996c4296fac3c29d0ee4ee5ab0f..1b35907b017e46cc11e6d7c503b85f929dccf2f9 100644 --- a/.test-conda-env-py3.yml +++ b/.test-conda-env-py3.yml @@ -1,12 +1,12 @@ name: test-conda-env channels: - conda-forge -- defaults +- nodefaults dependencies: - python=3 - git -- conda-forge::numpy +- numpy - pocl - mako - pyopencl diff --git a/meshmode/discretization/__init__.py b/meshmode/discretization/__init__.py index 6a4fcda7565487d44ee49045871d9226762af2db..a3847f2c3ab6c262c390f3c805619d74c85ab401 100644 --- a/meshmode/discretization/__init__.py +++ b/meshmode/discretization/__init__.py @@ -67,6 +67,12 @@ class ElementGroupBase(object): Returns an array of length :attr:`nunit_nodes` containing quadrature weights. + + .. attribute:: is_affine + + A :class:`bool` flag that is *True* if the local-to-global + parametrization of all the elements in the group is affine. Based on + :attr:`meshmode.mesh.MeshElementGroup.is_affine`. """ def __init__(self, mesh_el_group, order, node_nr_base): @@ -78,6 +84,10 @@ class ElementGroupBase(object): self.order = order self.node_nr_base = node_nr_base + @property + def is_affine(self): + return self.mesh_el_group.is_affine + @property def nelements(self): return self.mesh_el_group.nelements diff --git a/meshmode/mesh/__init__.py b/meshmode/mesh/__init__.py index 19ef8e5ddaf46b283ea9ca3431d672f57deca4fb..9df70d387b5d40d435982024c1dd0b78429915d8 100644 --- a/meshmode/mesh/__init__.py +++ b/meshmode/mesh/__init__.py @@ -26,9 +26,10 @@ from six.moves import range import six import numpy as np -import modepy as mp import numpy.linalg as la -from pytools import Record + +import modepy as mp +from pytools import Record, memoize_method __doc__ = """ @@ -162,6 +163,11 @@ class MeshElementGroup(Record): *Not* the ambient dimension, see :attr:`Mesh.ambient_dim` for that. + .. attribute:: is_affine + + A :class:`bool` flag that is *True* if the local-to-global + parametrization of all the elements in the group is affine. + .. automethod:: face_vertex_indices .. automethod:: vertex_unit_coordinates @@ -227,6 +233,10 @@ class MeshElementGroup(Record): def nunit_nodes(self): return self.unit_nodes.shape[-1] + @property + def is_affine(self): + raise NotImplementedError() + def face_vertex_indices(self): """Return a tuple of tuples indicating which vertices (in mathematically positive ordering) make up each face @@ -307,6 +317,11 @@ class SimplexElementGroup(MeshElementGroup): super(SimplexElementGroup, self).__init__(order, vertex_indices, nodes, element_nr_base, node_nr_base, unit_nodes, dim) + @property + @memoize_method + def is_affine(self): + return is_affine_simplex_group(self) + def face_vertex_indices(self): if self.dim == 1: return ( @@ -1367,4 +1382,44 @@ def is_boundary_tag_empty(mesh, boundary_tag): # }}} +# {{{ + +def is_affine_simplex_group(group, abs_tol=None): + if abs_tol is None: + abs_tol = 1.0e-13 + + if not isinstance(group, SimplexElementGroup): + raise TypeError("expected a 'SimplexElementGroup' not '%s'" % + type(group).__name__) + + # get matrices + basis = mp.simplex_best_available_basis(group.dim, group.order) + grad_basis = mp.grad_simplex_best_available_basis(group.dim, group.order) + + vinv = la.inv(mp.vandermonde(basis, group.unit_nodes)) + diff = mp.differentiation_matrices(basis, grad_basis, group.unit_nodes) + if not isinstance(diff, tuple): + diff = (diff,) + + # construct all second derivative matrices (including cross terms) + from itertools import product + mats = [] + for n in product(range(group.dim), repeat=2): + if n[0] > n[1]: + continue + mats.append(vinv.dot(diff[n[0]].dot(diff[n[1]]))) + + # check just the first element for a non-affine local-to-global mapping + ddx_coeffs = np.einsum("aij,bj->abi", mats, group.nodes[:, 0, :]) + norm_inf = np.max(np.abs(ddx_coeffs)) + if norm_inf > abs_tol: + return False + + # check all elements for a non-affine local-to-global mapping + ddx_coeffs = np.einsum("aij,bcj->abci", mats, group.nodes) + norm_inf = np.max(np.abs(ddx_coeffs)) + return norm_inf < abs_tol + +# }}} + # vim: foldmethod=marker diff --git a/test/test_meshmode.py b/test/test_meshmode.py index a0248712a7f052d697cbc99b6d09caa81fea3013..038637842fcadbe38de1bd2b334b895d77d27449 100644 --- a/test/test_meshmode.py +++ b/test/test_meshmode.py @@ -1265,6 +1265,73 @@ def test_open_curved_mesh(curve_name): closed=closed) +def _generate_cross_warped_rect_mesh(dim, order, n): + from meshmode.mesh.generation import generate_regular_rect_mesh + mesh = generate_regular_rect_mesh( + a=(0,)*dim, b=(1,)*dim, + n=(n,)*dim, order=order) + + def m(x): + results = np.empty_like(x) + results[0] = 1 + 1.5 * (x[0] + 0.25) * (x[1] + 0.3) + results[1] = x[1] + return results + + from meshmode.mesh.processing import map_mesh + return map_mesh(mesh, m) + + +@pytest.mark.parametrize("mesh_name", [ + "box2d", "box3d", + "warped_box2d", "warped_box3d", "cross_warped_box", + "circle", "ellipse", + "sphere", "torus" + ]) +def test_is_affine_group_check(mesh_name): + from meshmode.mesh.generation import ( + generate_regular_rect_mesh, generate_warped_rect_mesh, + make_curve_mesh, ellipse, + generate_icosphere, generate_torus) + + order = 4 + nelements = 16 + + if mesh_name.startswith("box"): + dim = int(mesh_name[-2]) + is_affine = True + mesh = generate_regular_rect_mesh( + a=(-0.5,)*dim, b=(0.5,)*dim, + n=(nelements,)*dim, order=order) + elif mesh_name.startswith("warped_box"): + dim = int(mesh_name[-2]) + is_affine = False + mesh = generate_warped_rect_mesh(dim, order, nelements) + elif mesh_name == "cross_warped_box": + dim = 2 + is_affine = False + mesh = _generate_cross_warped_rect_mesh(dim, order, nelements) + elif mesh_name == "circle": + is_affine = False + mesh = make_curve_mesh( + lambda t: ellipse(1.0, t), + np.linspace(0.0, 1.0, nelements + 1), order=order) + elif mesh_name == "ellipse": + is_affine = False + mesh = make_curve_mesh( + lambda t: ellipse(2.0, t), + np.linspace(0.0, 1.0, nelements + 1), order=order) + elif mesh_name == "sphere": + is_affine = False + mesh = generate_icosphere(r=1.0, order=order) + elif mesh_name == "torus": + is_affine = False + mesh = generate_torus(10.0, 2.0, order=order) + else: + raise ValueError("unknown mesh name: {}".format(mesh_name)) + + assert all(grp.is_affine for grp in mesh.groups) == is_affine + + if __name__ == "__main__": import sys if len(sys.argv) > 1: