From 11bd11fd142738e79f9e0a782fac221f2a652e28 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 3 Nov 2020 17:09:30 -0600 Subject: [PATCH 01/68] make face mass matrices work on tensor products --- modepy/matrices.py | 70 +++++++++--- modepy/nodes.py | 4 + modepy/quadrature/__init__.py | 26 +++++ modepy/tools.py | 48 +++++++++ test/test_tools.py | 194 ++++++++++++++++++++++++---------- 5 files changed, 272 insertions(+), 70 deletions(-) diff --git a/modepy/matrices.py b/modepy/matrices.py index 52d2fa1f..0185d776 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -242,7 +242,7 @@ def mass_matrix(basis, nodes): return la.inv(inverse_mass_matrix(basis, nodes)) -class _FaceMap: +class _SimplexFaceMap: def __init__(self, face_vertices): """ :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* @@ -252,33 +252,68 @@ def __init__(self, face_vertices): if npts != vol_dim: raise ValueError("face_vertices has wrong shape") - self.origin = face_vertices[:, 0] - self.span = face_vertices[:, 1:] - self.origin[:, np.newaxis] + self.origin = face_vertices[:, 0].reshape(-1, 1) + self.span = face_vertices[:, 1:] - self.origin self.face_dim = vol_dim - 1 def __call__(self, points): - return (self.origin[:, np.newaxis] - + np.einsum("ad,dn->an", self.span, points*0.5 + 0.5)) + return self.origin + np.einsum("ad,dn->an", self.span, points*0.5 + 0.5) -def modal_face_mass_matrix(trial_basis, order, face_vertices, test_basis=None): +class _HypercubeFaceMap: + def __init__(self, face_vertices): + """ + :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* + should equal `2**(dim - 1)`. + """ + vol_dim, npts = face_vertices.shape + if npts != 2**(vol_dim-1): + raise ValueError("face_vertices has wrong shape") + + self.origin = face_vertices[:, 0].reshape(-1, 1) + self.span = face_vertices[:, -2:0:-1] - self.origin + + self.face_dim = vol_dim - 1 + + def __call__(self, points): + return self.origin + np.einsum("ad,dn->an", self.span, points*0.5 + 0.5) + + +def modal_face_mass_matrix(trial_basis, order, face_vertices, + test_basis=None, domain=None): """ :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* should equal *dim*. + :arg domain: identifier for the reference element, can be one of + `"simplex"` or `"hypercube"`. .. versionadded :: 2016.1 + + .. versionchanged:: 2020.5 + + Added *domain* parameter and support for :math:`[-1, 1]^d` domains. """ if test_basis is None: test_basis = trial_basis - fmap = _FaceMap(face_vertices) + if domain is None: + domain = "simplex" + + if domain == "simplex": + from modepy.quadrature.grundmann_moeller import \ + GrundmannMoellerSimplexQuadrature + fmap = _SimplexFaceMap(face_vertices) + quad = GrundmannMoellerSimplexQuadrature(order, fmap.face_dim) + elif domain == "hypercube": + from modepy.quadrature import LegendreGaussTensorProductQuadrature + fmap = _HypercubeFaceMap(face_vertices) + quad = LegendreGaussTensorProductQuadrature(fmap.face_dim, order) + else: + raise ValueError(f"unknown domain: '{domain}'") - from modepy.quadrature.grundmann_moeller import GrundmannMoellerSimplexQuadrature - quad = GrundmannMoellerSimplexQuadrature(order, fmap.face_dim) assert quad.exact_to > order*2 - mapped_nodes = fmap(quad.nodes) nrows = len(test_basis) @@ -296,7 +331,7 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, test_basis=None): def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, - face_vertices, test_basis=None): + face_vertices, test_basis=None, domain=None): """ :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* should equal *dim*. @@ -307,13 +342,22 @@ def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, if test_basis is None: test_basis = trial_basis - fmap = _FaceMap(face_vertices) + if domain is None: + domain = "simplex" + + if domain == "simplex": + fmap = _SimplexFaceMap(face_vertices) + elif domain == "hypercube": + fmap = _HypercubeFaceMap(face_vertices) + else: + raise ValueError(f"unknown domain: '{domain}'") face_vdm = vandermonde(trial_basis, fmap(face_nodes)) # /!\ non-square vol_vdm = vandermonde(test_basis, volume_nodes) modal_fmm = modal_face_mass_matrix( - trial_basis, order, face_vertices, test_basis=test_basis) + trial_basis, order, face_vertices, + test_basis=test_basis, domain=domain) return la.inv(vol_vdm.T).dot(modal_fmm).dot(la.pinv(face_vdm)) diff --git a/modepy/nodes.py b/modepy/nodes.py index e8d0dfcc..de92a7d3 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -302,6 +302,10 @@ def tensor_product_nodes(dims, nodes_1d): .. versionadded:: 2017.1 """ + if dims == 0: + # NOTE: using this to maintain consistency in the 0d case + return warp_and_blend_nodes(dims, 1) + nnodes_1d = len(nodes_1d) result = np.empty((dims,) + (nnodes_1d,) * dims) for d in range(dims): diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 7ca637c8..321629af 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -82,3 +82,29 @@ def __init__(self, quad, left, right): Quadrature.__init__(self, left + (quad.nodes+1) / 2 * length, quad.weights * half_length) + + +class TensorProductQuadrature(Quadrature): + def __init__(self, dims, quad): + """ + :arg quad: a :class:`Quadrature` class for one-dimensional intervals. + """ + + from modepy.nodes import tensor_product_nodes + x = tensor_product_nodes(dims, quad.nodes) + from itertools import product + w = np.fromiter( + (np.prod(w) for w in product(quad.weights, repeat=dims)), + dtype=np.float, + count=quad.weights.size**dims) + assert w.size == x.shape[1] + + super().__init__(x, w) + self.exact_to = quad.exact_to + + +class LegendreGaussTensorProductQuadrature(TensorProductQuadrature): + def __init__(self, dims, N, backend=None): # noqa: N803 + from modepy.quadrature.jacobi_gauss import LegendreGaussQuadrature + super().__init__( + dims, LegendreGaussQuadrature(N, backend=backend)) diff --git a/modepy/tools.py b/modepy/tools.py index 008b67a0..613eae77 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -146,6 +146,10 @@ def inverse(self): """The inverse :class:`AffineMap` of *self*.""" return AffineMap(la.inv(self.a), -la.solve(self.a, self.b)) +# }}} + + +# {{{ simplex coordinate mapping EQUILATERAL_TO_UNIT_MAP = { 1: AffineMap([[1]], [0]), @@ -226,6 +230,50 @@ def barycentric_to_equilateral(bary): dims = len(bary)-1 return np.dot(EQUILATERAL_VERTICES[dims].T, bary) + +def simplex_face_vertex_indices(dims): + result = np.empty((dims + 1, dims), dtype=np.int) + indices = np.arange(dims + 1) + + for iface in range(dims + 1): + result[iface, :] = np.hstack([indices[:iface], indices[iface + 1:]]) + + return result + +# }}} + + +# {{{ hypercube coordinate mapping + +_HYPERCUBE_UNIT_FACE_VERTICES = { + 1: ((0b0,), (0b1,)), + 2: ((0b00, 0b01), (0b10, 0b11), (0b00, 0b10), (0b01, 0b11)), + 3: ( + (0b000, 0b001, 0b010, 0b011,), + (0b100, 0b101, 0b110, 0b111,), + + (0b000, 0b010, 0b100, 0b110,), + (0b001, 0b011, 0b101, 0b111,), + + (0b000, 0b001, 0b100, 0b101,), + (0b010, 0b011, 0b110, 0b111,), + ) + } + + +def hypercube_unit_vertices(dims): + from pytools import flatten, generate_nonnegative_integer_tuples_below as gnitb + vertices_01 = np.fromiter( + flatten(gnitb(2, dims)), + dtype=np.float64, + count=dims * 2**dims) + + return -1.0 + 2.0 * vertices_01.reshape(-1, dims) + + +def hypercube_face_vertex_indices(dims): + return np.array(_HYPERCUBE_UNIT_FACE_VERTICES[dims]) + # }}} diff --git a/test/test_tools.py b/test/test_tools.py index b65e7b61..dca405ba 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -138,6 +138,82 @@ def estimate_resid(inner_n): # }}} +# {{{ bases and nodes and things + +class _SimplexElement: + def __init__(self, dims, order): + self.dims = dims + self.order = order + + @property + def basis(self): + return mp.simplex_onb(self.dims, self.order) + + @property + def grad_basis(self): + return mp.grad_simplex_onb(self.dims, self.order) + + @property + def nodes(self): + return mp.warp_and_blend_nodes(dims, self.order) + + @property + def nfaces(self): + return self.dims + 1 + + @property + def domain(self): + return "simplex" + + @property + def unit_vertices(self): + from modepy.tools import unit_vertices + return unit_vertices(self.dims).T + + @property + def face_vertex_indices(self): + from modepy.tools import simplex_face_vertex_indices + return simplex_face_vertex_indices(self.dims) + + +class _TensorProductElement: + def __init__(self, dims, order): + self.dims = dims + self.order = order + + @property + def basis(self): + return mp.legendre_tensor_product_basis(self.dims, self.order) + + @property + def grad_basis(self): + return mp.grad_legendre_tensor_product_basis(self.dims, self.order) + + @property + def nodes(self): + return mp.legendre_gauss_lobatto_tensor_product_nodes(self.dims, self.order) + + @property + def nfaces(self): + return 2 * self.dims + + @property + def domain(self): + return "hypercube" + + @property + def unit_vertices(self): + from modepy.tools import hypercube_unit_vertices + return hypercube_unit_vertices(self.dims).T + + @property + def face_vertex_indices(self): + from modepy.tools import hypercube_face_vertex_indices + return hypercube_face_vertex_indices(self.dims) + +# }}} + + # {{{ test_resampling_matrix @pytest.mark.parametrize("dims", [1, 2, 3]) @@ -147,30 +223,24 @@ def test_resampling_matrix(dims, eltype): nfine = 10 if eltype == "simplex": - coarse_nodes = mp.warp_and_blend_nodes(dims, ncoarse) - fine_nodes = mp.warp_and_blend_nodes(dims, nfine) - - coarse_basis = mp.simplex_onb(dims, ncoarse) - fine_basis = mp.simplex_onb(dims, nfine) + coarse = _SimplexElement(dims, ncoarse) + fine = _SimplexElement(dims, nfine) elif eltype == "tensor": - coarse_nodes = mp.legendre_gauss_lobatto_tensor_product_nodes(dims, ncoarse) - fine_nodes = mp.legendre_gauss_lobatto_tensor_product_nodes(dims, nfine) - - coarse_basis = mp.legendre_tensor_product_basis(dims, ncoarse) - fine_basis = mp.legendre_tensor_product_basis(dims, nfine) + coarse = _TensorProductElement(dims, ncoarse) + fine = _TensorProductElement(dims, nfine) else: raise ValueError(f"unknown element type: {eltype}") my_eye = np.dot( - mp.resampling_matrix(fine_basis, coarse_nodes, fine_nodes), - mp.resampling_matrix(coarse_basis, fine_nodes, coarse_nodes)) + mp.resampling_matrix(fine.basis, coarse.nodes, fine.nodes), + mp.resampling_matrix(coarse.basis, fine.nodes, coarse.nodes)) assert la.norm(my_eye - np.eye(len(my_eye))) < 3e-13 my_eye_least_squares = np.dot( - mp.resampling_matrix(coarse_basis, coarse_nodes, fine_nodes, + mp.resampling_matrix(coarse.basis, coarse.nodes, fine.nodes, least_squares_ok=True), - mp.resampling_matrix(coarse_basis, fine_nodes, coarse_nodes), + mp.resampling_matrix(coarse.basis, fine.nodes, coarse.nodes), ) assert la.norm(my_eye_least_squares - np.eye(len(my_eye_least_squares))) < 4e-13 @@ -186,23 +256,19 @@ def test_diff_matrix(dims, eltype): n = 5 if eltype == "simplex": - nodes = mp.warp_and_blend_nodes(dims, n) - basis = mp.simplex_onb(dims, n) - grad_basis = mp.grad_simplex_onb(dims, n) + el = _SimplexElement(dims, n) elif eltype == "tensor": - nodes = mp.legendre_gauss_lobatto_tensor_product_nodes(dims, n) - basis = mp.legendre_tensor_product_basis(dims, n) - grad_basis = mp.grad_legendre_tensor_product_basis(dims, n) + el = _TensorProductElement(dims, n) else: raise ValueError(f"unknown element type: {eltype}") - diff_mat = mp.differentiation_matrices(basis, grad_basis, nodes) + diff_mat = mp.differentiation_matrices(el.basis, el.grad_basis, el.nodes) if isinstance(diff_mat, tuple): diff_mat = diff_mat[0] - f = np.sin(nodes[0]) + f = np.sin(el.nodes[0]) - df_dx = np.cos(nodes[0]) + df_dx = np.cos(el.nodes[0]) df_dx_num = np.dot(diff_mat, f) error = la.norm(df_dx - df_dx_num) / la.norm(df_dx) @@ -236,62 +302,76 @@ def test_diff_matrix_permutation(dims): # {{{ test_face_mass_matrix @pytest.mark.parametrize("dim", [1, 2, 3]) -def test_modal_face_mass_matrix(dim, order=3): - from modepy.tools import unit_vertices - all_verts = unit_vertices(dim).T +@pytest.mark.parametrize("eltype", ["simplex", "tensor"]) +def test_modal_face_mass_matrix(dim, eltype, order=3): + np.set_printoptions(linewidth=200) - basis = mp.simplex_onb(dim, order) + if eltype == "simplex": + el = _SimplexElement(dim, order) + elif eltype == "tensor": + el = _TensorProductElement(dim, order) + else: + raise ValueError(f"unknown element type: '{eltype}'") - # np.set_printoptions(linewidth=200) + all_verts = el.unit_vertices + fvi = el.face_vertex_indices from modepy.matrices import modal_face_mass_matrix - for iface in range(dim+1): - verts = np.hstack([all_verts[:, :iface], all_verts[:, iface+1:]]) + for iface in range(el.nfaces): + verts = all_verts[:, fvi[iface]] - fmm = modal_face_mass_matrix(basis, order, verts) - fmm2 = modal_face_mass_matrix(basis, order+1, verts) + fmm = modal_face_mass_matrix(el.basis, order, verts, domain=el.domain) + fmm2 = modal_face_mass_matrix(el.basis, order+1, verts, domain=el.domain) - assert la.norm(fmm-fmm2, np.inf) < 1e-11 + error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) + logger.info("fmm error: %.5e", error) + assert error < 1e-11 fmm[np.abs(fmm) < 1e-13] = 0 - - print(fmm) nnz = np.sum(fmm > 0) - print(nnz) + + logger.info("fmm: nnz %d\n%s", nnz, fmm) @pytest.mark.parametrize("dim", [1, 2, 3]) -def test_nodal_face_mass_matrix(dim, order=3): - from modepy.tools import unit_vertices - all_verts = unit_vertices(dim).T +@pytest.mark.parametrize("eltype", ["simplex", "tensor"]) +def test_nodal_face_mass_matrix(dim, eltype, order=3): + np.set_printoptions(linewidth=200) - basis = mp.simplex_onb(dim, order) + if eltype == "simplex": + volume = _SimplexElement(dim, order) + face = _SimplexElement(dim - 1, order) + elif eltype == "tensor": + volume = _TensorProductElement(dim, order) + face = _TensorProductElement(dim - 1, order) + else: + raise ValueError(f"unknown element type: '{eltype}'") - np.set_printoptions(linewidth=200) + all_verts = volume.unit_vertices + fvi = volume.face_vertex_indices from modepy.matrices import nodal_face_mass_matrix - volume_nodes = mp.warp_and_blend_nodes(dim, order) - face_nodes = mp.warp_and_blend_nodes(dim-1, order) + for iface in range(volume.nfaces): + verts = all_verts[:, fvi[iface]] - for iface in range(dim+1): - verts = np.hstack([all_verts[:, :iface], all_verts[:, iface+1:]]) + fmm = nodal_face_mass_matrix( + volume.basis, volume.nodes, face.nodes, order, verts, + domain=volume.domain) + fmm2 = nodal_face_mass_matrix( + volume.basis, volume.nodes, face.nodes, order+1, verts, + domain=volume.domain) - fmm = nodal_face_mass_matrix(basis, volume_nodes, face_nodes, order, - verts) - fmm2 = nodal_face_mass_matrix(basis, volume_nodes, face_nodes, order+1, - verts) - - assert la.norm(fmm-fmm2, np.inf) < 1e-11 + error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) + logger.info("fmm error: %.5e", error) + assert error < 1e-11 fmm[np.abs(fmm) < 1e-13] = 0 + nnz = np.sum(fmm > 0) - print(fmm) - nnz = np.sum(np.abs(fmm) > 0) - print(nnz) + logger.info("fmm: nnz %d\n%s", nnz, fmm) - print(mp.mass_matrix( - mp.simplex_onb(dim-1, order), - mp.warp_and_blend_nodes(dim-1, order), )) + logger.info("mass matrix:\n%s", + mp.mass_matrix(face.basis, face.nodes)) # }}} From cfd8fbf65e7177cb4b2314560fce20988fc59303 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 3 Nov 2020 19:07:21 -0600 Subject: [PATCH 02/68] fix 0d case for tensor products --- modepy/modes.py | 8 ++++++++ test/test_tools.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/modepy/modes.py b/modepy/modes.py index db59380d..6fe9e01e 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -658,6 +658,10 @@ def tensor_product_basis(dims, basis_1d): .. versionadded:: 2017.1 """ + if dims == 0: + # NOTE: using to maintain consistency in the 0d case + return simplex_onb(dims, len(basis_1d)) + from pytools import generate_nonnegative_integer_tuples_below as gnitb return tuple( _TensorProductBasisFunction(order, [basis_1d[i] for i in order]) @@ -673,6 +677,10 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): .. versionadded:: 2020.2 """ + if dims == 0: + # NOTE: using to maintain consistency in the 0d case + return grad_simplex_onb(dims, len(basis_1d)) + from pytools import ( wandering_element, generate_nonnegative_integer_tuples_below as gnitb) diff --git a/test/test_tools.py b/test/test_tools.py index dca405ba..c8a986c6 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -155,7 +155,7 @@ def grad_basis(self): @property def nodes(self): - return mp.warp_and_blend_nodes(dims, self.order) + return mp.warp_and_blend_nodes(self.dims, self.order) @property def nfaces(self): From ffcb91baf3794db7a597c21c62ef3fdcc8bc70bd Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 3 Nov 2020 19:12:23 -0600 Subject: [PATCH 03/68] remove face index stuff --- modepy/tools.py | 30 ------------------------------ test/test_tools.py | 27 +++++++++++++++++++++++---- 2 files changed, 23 insertions(+), 34 deletions(-) diff --git a/modepy/tools.py b/modepy/tools.py index 613eae77..ec4f83fb 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -230,37 +230,11 @@ def barycentric_to_equilateral(bary): dims = len(bary)-1 return np.dot(EQUILATERAL_VERTICES[dims].T, bary) - -def simplex_face_vertex_indices(dims): - result = np.empty((dims + 1, dims), dtype=np.int) - indices = np.arange(dims + 1) - - for iface in range(dims + 1): - result[iface, :] = np.hstack([indices[:iface], indices[iface + 1:]]) - - return result - # }}} # {{{ hypercube coordinate mapping -_HYPERCUBE_UNIT_FACE_VERTICES = { - 1: ((0b0,), (0b1,)), - 2: ((0b00, 0b01), (0b10, 0b11), (0b00, 0b10), (0b01, 0b11)), - 3: ( - (0b000, 0b001, 0b010, 0b011,), - (0b100, 0b101, 0b110, 0b111,), - - (0b000, 0b010, 0b100, 0b110,), - (0b001, 0b011, 0b101, 0b111,), - - (0b000, 0b001, 0b100, 0b101,), - (0b010, 0b011, 0b110, 0b111,), - ) - } - - def hypercube_unit_vertices(dims): from pytools import flatten, generate_nonnegative_integer_tuples_below as gnitb vertices_01 = np.fromiter( @@ -270,10 +244,6 @@ def hypercube_unit_vertices(dims): return -1.0 + 2.0 * vertices_01.reshape(-1, dims) - -def hypercube_face_vertex_indices(dims): - return np.array(_HYPERCUBE_UNIT_FACE_VERTICES[dims]) - # }}} diff --git a/test/test_tools.py b/test/test_tools.py index c8a986c6..80b9c8b7 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -172,8 +172,13 @@ def unit_vertices(self): @property def face_vertex_indices(self): - from modepy.tools import simplex_face_vertex_indices - return simplex_face_vertex_indices(self.dims) + result = np.empty((self.dims + 1, self.dims), dtype=np.int) + indices = np.arange(self.dims + 1) + + for iface in range(self.nfaces): + result[iface, :] = np.hstack([indices[:iface], indices[iface + 1:]]) + + return result class _TensorProductElement: @@ -208,8 +213,22 @@ def unit_vertices(self): @property def face_vertex_indices(self): - from modepy.tools import hypercube_face_vertex_indices - return hypercube_face_vertex_indices(self.dims) + fvi = { + 1: ((0b0,), (0b1,)), + 2: ((0b00, 0b01), (0b10, 0b11), (0b00, 0b10), (0b01, 0b11)), + 3: ( + (0b000, 0b001, 0b010, 0b011,), + (0b100, 0b101, 0b110, 0b111,), + + (0b000, 0b010, 0b100, 0b110,), + (0b001, 0b011, 0b101, 0b111,), + + (0b000, 0b001, 0b100, 0b101,), + (0b010, 0b011, 0b110, 0b111,), + ) + }[self.dims] + + return np.array(fvi) # }}} From 47dec4d4bef26c5edf661eb0fe1e719362b690be Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 3 Nov 2020 19:37:05 -0600 Subject: [PATCH 04/68] simplify cube unit vertex construction --- modepy/tools.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/modepy/tools.py b/modepy/tools.py index ec4f83fb..48cf231c 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -236,13 +236,8 @@ def barycentric_to_equilateral(bary): # {{{ hypercube coordinate mapping def hypercube_unit_vertices(dims): - from pytools import flatten, generate_nonnegative_integer_tuples_below as gnitb - vertices_01 = np.fromiter( - flatten(gnitb(2, dims)), - dtype=np.float64, - count=dims * 2**dims) - - return -1.0 + 2.0 * vertices_01.reshape(-1, dims) + from modepy.nodes import tensor_product_nodes + return tensor_product_nodes(dims, np.array([-1.0, 1.0])).T # }}} From 56d7f139c46f0491acc93522004f24b97b32512d Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 3 Nov 2020 19:43:26 -0600 Subject: [PATCH 05/68] simplify face maps a bit --- modepy/matrices.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/modepy/matrices.py b/modepy/matrices.py index 0185d776..a78e04b6 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -242,7 +242,17 @@ def mass_matrix(basis, nodes): return la.inv(inverse_mass_matrix(basis, nodes)) -class _SimplexFaceMap: +class _FaceMap: + def __init__(self, origin, span): + self.origin = origin + self.span = span + self.face_dim = span.shape[0] - 1 + + def __call__(self, points): + return self.origin + np.einsum("ad,dn->an", self.span, points*0.5 + 0.5) + + +class _SimplexFaceMap(_FaceMap): def __init__(self, face_vertices): """ :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* @@ -250,18 +260,14 @@ def __init__(self, face_vertices): """ vol_dim, npts = face_vertices.shape if npts != vol_dim: - raise ValueError("face_vertices has wrong shape") - - self.origin = face_vertices[:, 0].reshape(-1, 1) - self.span = face_vertices[:, 1:] - self.origin + raise ValueError("'face_vertices' has wrong shape") - self.face_dim = vol_dim - 1 - - def __call__(self, points): - return self.origin + np.einsum("ad,dn->an", self.span, points*0.5 + 0.5) + origin = face_vertices[:, 0].reshape(-1, 1) + span = face_vertices[:, 1:] - origin + super().__init__(origin, span) -class _HypercubeFaceMap: +class _HypercubeFaceMap(_FaceMap): def __init__(self, face_vertices): """ :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* @@ -269,15 +275,12 @@ def __init__(self, face_vertices): """ vol_dim, npts = face_vertices.shape if npts != 2**(vol_dim-1): - raise ValueError("face_vertices has wrong shape") + raise ValueError("'face_vertices' has wrong shape") - self.origin = face_vertices[:, 0].reshape(-1, 1) - self.span = face_vertices[:, -2:0:-1] - self.origin + origin = face_vertices[:, 0].reshape(-1, 1) + span = face_vertices[:, -2:0:-1] - origin - self.face_dim = vol_dim - 1 - - def __call__(self, points): - return self.origin + np.einsum("ad,dn->an", self.span, points*0.5 + 0.5) + super().__init__(origin, span) def modal_face_mass_matrix(trial_basis, order, face_vertices, From ddb7e6ddc47fc80a959c02a7d5d734d40f2d548c Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Wed, 4 Nov 2020 09:37:30 -0600 Subject: [PATCH 06/68] simplify test setup --- test/test_tools.py | 56 ++++++++++++---------------------------------- 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/test/test_tools.py b/test/test_tools.py index 80b9c8b7..60dad99b 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -145,30 +145,16 @@ def __init__(self, dims, order): self.dims = dims self.order = order - @property - def basis(self): - return mp.simplex_onb(self.dims, self.order) + self.basis = mp.simplex_onb(dims, order) + if dims > 0: + self.grad_basis = mp.grad_simplex_onb(dims, order) + self.nodes = mp.warp_and_blend_nodes(dims, order) - @property - def grad_basis(self): - return mp.grad_simplex_onb(self.dims, self.order) + self.nfaces = dims + 1 + self.domain = "simplex" - @property - def nodes(self): - return mp.warp_and_blend_nodes(self.dims, self.order) - - @property - def nfaces(self): - return self.dims + 1 - - @property - def domain(self): - return "simplex" - - @property - def unit_vertices(self): from modepy.tools import unit_vertices - return unit_vertices(self.dims).T + self.unit_vertices = unit_vertices(dims).T @property def face_vertex_indices(self): @@ -186,30 +172,16 @@ def __init__(self, dims, order): self.dims = dims self.order = order - @property - def basis(self): - return mp.legendre_tensor_product_basis(self.dims, self.order) + self.basis = mp.legendre_tensor_product_basis(dims, order) + if dims > 0: + self.grad_basis = mp.grad_legendre_tensor_product_basis(dims, order) + self.nodes = mp.legendre_gauss_lobatto_tensor_product_nodes(dims, order) - @property - def grad_basis(self): - return mp.grad_legendre_tensor_product_basis(self.dims, self.order) + self.nfaces = 2 * dims + self.domain = "hypercube" - @property - def nodes(self): - return mp.legendre_gauss_lobatto_tensor_product_nodes(self.dims, self.order) - - @property - def nfaces(self): - return 2 * self.dims - - @property - def domain(self): - return "hypercube" - - @property - def unit_vertices(self): from modepy.tools import hypercube_unit_vertices - return hypercube_unit_vertices(self.dims).T + self.unit_vertices = hypercube_unit_vertices(dims).T @property def face_vertex_indices(self): From e9b52eb059b117cfe5c274330fbe797e84907aec Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Wed, 4 Nov 2020 18:24:04 -0600 Subject: [PATCH 07/68] update docs --- modepy/matrices.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/modepy/matrices.py b/modepy/matrices.py index a78e04b6..37d9ca8d 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -243,10 +243,12 @@ def mass_matrix(basis, nodes): class _FaceMap: - def __init__(self, origin, span): - self.origin = origin - self.span = span - self.face_dim = span.shape[0] - 1 + def __init__(self, face_vertices): + vol_dim = face_vertices.shape[0] + + self.origin = face_vertices[:, 0].reshape(-1, 1) + self.span = face_vertices[:, 1:vol_dim] - self.origin + self.face_dim = vol_dim - 1 def __call__(self, points): return self.origin + np.einsum("ad,dn->an", self.span, points*0.5 + 0.5) @@ -256,38 +258,32 @@ class _SimplexFaceMap(_FaceMap): def __init__(self, face_vertices): """ :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* - should equal *dim*. + should equal ``dim``. """ vol_dim, npts = face_vertices.shape if npts != vol_dim: raise ValueError("'face_vertices' has wrong shape") - origin = face_vertices[:, 0].reshape(-1, 1) - span = face_vertices[:, 1:] - origin - super().__init__(origin, span) + super().__init__(face_vertices) class _HypercubeFaceMap(_FaceMap): def __init__(self, face_vertices): """ :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* - should equal `2**(dim - 1)`. + should equal ``2**(dim - 1)``. """ vol_dim, npts = face_vertices.shape if npts != 2**(vol_dim-1): raise ValueError("'face_vertices' has wrong shape") - origin = face_vertices[:, 0].reshape(-1, 1) - span = face_vertices[:, -2:0:-1] - origin - - super().__init__(origin, span) + super().__init__(face_vertices) def modal_face_mass_matrix(trial_basis, order, face_vertices, test_basis=None, domain=None): """ - :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* - should equal *dim*. + :arg face_vertices: an array of shape ``[dim, npts]``. :arg domain: identifier for the reference element, can be one of `"simplex"` or `"hypercube"`. @@ -336,10 +332,15 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, face_vertices, test_basis=None, domain=None): """ - :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* - should equal *dim*. + :arg face_vertices: an array of shape ``[dim, npts]``. + :arg domain: identifier for the reference element, can be one of + `"simplex"` or `"hypercube"`. .. versionadded :: 2016.1 + + .. versionchanged:: 2020.5 + + Added *domain* parameter and support for :math:`[-1, 1]^d` domains. """ if test_basis is None: From 704e6861f97ad587d27c32042645d295c167d2f2 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Sat, 14 Nov 2020 14:30:42 -0600 Subject: [PATCH 08/68] introduce a shapes module --- modepy/matrices.py | 94 +++---------- modepy/nodes.py | 3 +- modepy/quadrature/__init__.py | 2 +- modepy/shapes.py | 251 ++++++++++++++++++++++++++++++++++ modepy/tools.py | 10 +- setup.py | 2 + test/test_quadrature.py | 9 +- test/test_tools.py | 195 +++++++++----------------- 8 files changed, 344 insertions(+), 222 deletions(-) create mode 100644 modepy/shapes.py diff --git a/modepy/matrices.py b/modepy/matrices.py index 37d9ca8d..caae2594 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -242,75 +242,30 @@ def mass_matrix(basis, nodes): return la.inv(inverse_mass_matrix(basis, nodes)) -class _FaceMap: - def __init__(self, face_vertices): - vol_dim = face_vertices.shape[0] - - self.origin = face_vertices[:, 0].reshape(-1, 1) - self.span = face_vertices[:, 1:vol_dim] - self.origin - self.face_dim = vol_dim - 1 - - def __call__(self, points): - return self.origin + np.einsum("ad,dn->an", self.span, points*0.5 + 0.5) - - -class _SimplexFaceMap(_FaceMap): - def __init__(self, face_vertices): - """ - :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* - should equal ``dim``. - """ - vol_dim, npts = face_vertices.shape - if npts != vol_dim: - raise ValueError("'face_vertices' has wrong shape") - - super().__init__(face_vertices) - - -class _HypercubeFaceMap(_FaceMap): - def __init__(self, face_vertices): - """ - :arg face_vertices: an array of shape ``[dim, npts]``, where *npts* - should equal ``2**(dim - 1)``. - """ - vol_dim, npts = face_vertices.shape - if npts != 2**(vol_dim-1): - raise ValueError("'face_vertices' has wrong shape") - - super().__init__(face_vertices) - - def modal_face_mass_matrix(trial_basis, order, face_vertices, - test_basis=None, domain=None): + test_basis=None, shape=None): """ - :arg face_vertices: an array of shape ``[dim, npts]``. - :arg domain: identifier for the reference element, can be one of - `"simplex"` or `"hypercube"`. + :arg face_vertices: an array of shape ``(dims, nvertices)``. + :arg shape: a :class:`~modepy.shapes.Shape` that identifies the + reference face element. .. versionadded :: 2016.1 .. versionchanged:: 2020.5 - Added *domain* parameter and support for :math:`[-1, 1]^d` domains. + Added *shape* parameter and support for :math:`[-1, 1]^d` domains. """ if test_basis is None: test_basis = trial_basis - if domain is None: - domain = "simplex" - - if domain == "simplex": - from modepy.quadrature.grundmann_moeller import \ - GrundmannMoellerSimplexQuadrature - fmap = _SimplexFaceMap(face_vertices) - quad = GrundmannMoellerSimplexQuadrature(order, fmap.face_dim) - elif domain == "hypercube": - from modepy.quadrature import LegendreGaussTensorProductQuadrature - fmap = _HypercubeFaceMap(face_vertices) - quad = LegendreGaussTensorProductQuadrature(fmap.face_dim, order) - else: - raise ValueError(f"unknown domain: '{domain}'") + from modepy import shapes + if shape is None: + shape = shapes.Simplex(face_vertices.shape[0] - 1) + + face = type(shape)(shape.dims - 1) + fmap = shapes.get_face_map(shape, face_vertices) + quad = shapes.get_quadrature(face, order) assert quad.exact_to > order*2 mapped_nodes = fmap(quad.nodes) @@ -330,38 +285,33 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, - face_vertices, test_basis=None, domain=None): + face_vertices, test_basis=None, shape=None): """ - :arg face_vertices: an array of shape ``[dim, npts]``. - :arg domain: identifier for the reference element, can be one of - `"simplex"` or `"hypercube"`. + :arg face_vertices: an array of shape ``(dims, nvertices)``. + :arg shape: a :class:`~modepy.shapes.Shape` that identifies the + reference face element. .. versionadded :: 2016.1 .. versionchanged:: 2020.5 - Added *domain* parameter and support for :math:`[-1, 1]^d` domains. + Added *shape* parameter and support for :math:`[-1, 1]^d` domains. """ if test_basis is None: test_basis = trial_basis - if domain is None: - domain = "simplex" - - if domain == "simplex": - fmap = _SimplexFaceMap(face_vertices) - elif domain == "hypercube": - fmap = _HypercubeFaceMap(face_vertices) - else: - raise ValueError(f"unknown domain: '{domain}'") + from modepy import shapes + if shape is None: + shape = shapes.Simplex(face_vertices.shape[0]) + fmap = shapes.get_face_map(shape, face_vertices) face_vdm = vandermonde(trial_basis, fmap(face_nodes)) # /!\ non-square vol_vdm = vandermonde(test_basis, volume_nodes) modal_fmm = modal_face_mass_matrix( trial_basis, order, face_vertices, - test_basis=test_basis, domain=domain) + test_basis=test_basis, shape=shape) return la.inv(vol_vdm.T).dot(modal_fmm).dot(la.pinv(face_vdm)) diff --git a/modepy/nodes.py b/modepy/nodes.py index de92a7d3..36c44737 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -316,8 +316,7 @@ def tensor_product_nodes(dims, nodes_1d): def legendre_gauss_lobatto_tensor_product_nodes(dims, n): from modepy.quadrature.jacobi_gauss import legendre_gauss_lobatto_nodes - return tensor_product_nodes(dims, - legendre_gauss_lobatto_nodes(n)) + return tensor_product_nodes(dims, legendre_gauss_lobatto_nodes(n)) # }}} diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 321629af..c3ea2af5 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -104,7 +104,7 @@ def __init__(self, dims, quad): class LegendreGaussTensorProductQuadrature(TensorProductQuadrature): - def __init__(self, dims, N, backend=None): # noqa: N803 + def __init__(self, N, dims, backend=None): # noqa: N803 from modepy.quadrature.jacobi_gauss import LegendreGaussQuadrature super().__init__( dims, LegendreGaussQuadrature(N, backend=backend)) diff --git a/modepy/shapes.py b/modepy/shapes.py new file mode 100644 index 00000000..e69f30cb --- /dev/null +++ b/modepy/shapes.py @@ -0,0 +1,251 @@ +__copyright__ = """ +Copyright (c) 2020 Alexandru Fikl +""" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + +import numpy as np + +from functools import singledispatch +from dataclasses import dataclass, field + +__doc__ = """ +Shapes +------ + +.. currentmodule:: modepy + +.. autoclass:: Shape +.. autoclass:: Simplex +.. autoclass:: Hypercube + +.. autofunction:: get_unit_vertices +.. autofunction:: get_face_map +.. autofunction:: get_quadrature +""" + + +# {{{ interface + +@dataclass(frozen=True) +class Shape: + dims: int + nfaces: int = field(init=False) + + +@singledispatch +def get_unit_vertices(shape: Shape): + """ + :returns: an :class:`~numpy.ndarray` of shape ``(nvertices, dims)``. + """ + raise NotImplementedError + + +@singledispatch +def get_face_vertex_indices(shape: Shape): + """ + :results: indices into the vertices returned by :func:`get_unit_vertices` + belonging to each face. + """ + + raise NotImplementedError + + +@singledispatch +def get_face_map(shape: Shape, face_vertices: np.ndarray): + """ + :returns: a :class:`~collections.abc.Callable` that takes an array of + unit nodes on the face represented by *face_vertices* and maps + them to the volume. + """ + raise NotImplementedError + + +@singledispatch +def get_quadrature(shape: Shape, order: int): + """ + :returns: a :class:`~modepy.Quadrature` instance of the given *order*. + """ + raise NotImplementedError + + +@singledispatch +def get_unit_nodes(shape: Shape, order: int): + raise NotImplementedError + + +@singledispatch +def get_basis(shape: Shape, order: int): + raise NotImplementedError + + +@singledispatch +def get_grad_basis(shape: Shape, order: int): + raise NotImplementedError + +# }}} + + +# {{{ simplex + +class Simplex(Shape): + @property + def nfaces(self): + return self.dims + 1 + + +@get_unit_vertices.register +def _(shape: Simplex): + from modepy.tools import unit_vertices + return unit_vertices(shape.dims) + + +@get_face_vertex_indices.register +def _(shape: Simplex): + fvi = np.empty((shape.dims + 1, shape.dims), dtype=np.int) + indices = np.arange(shape.dims + 1) + + for iface in range(shape.nfaces): + fvi[iface, :] = np.hstack([indices[:iface], indices[iface + 1:]]) + + return fvi + + +@get_face_map.register +def _(shape: Simplex, face_vertices: np.ndarray): + dims, npoints = face_vertices.shape + if npoints != dims: + raise ValueError("'face_vertices' has wrong shape") + + origin = face_vertices[:, 0].reshape(-1, 1) + face_basis = face_vertices[:, 1:] - origin + + return lambda p: origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) + + +@get_quadrature.register +def _(shape: Simplex, order: int): + import modepy as mp + if shape.dims == 0: + quad = mp.Quadrature(np.empty((0, 1)), np.empty((0, 1))) + else: + try: + quad = mp.XiaoGimbutasSimplexQuadrature(2*order + 1, shape.dims) + except (mp.QuadratureRuleUnavailable, ValueError): + quad = mp.GrundmannMoellerSimplexQuadrature(order, shape.dims) + + return quad + + +@get_unit_nodes.register +def _(shape: Simplex, order: int): + import modepy as mp + return mp.warp_and_blend_nodes(shape.dims, order) + + +@get_basis.register +def _(shape: Simplex, order: int): + import modepy as mp + return mp.simplex_onb(shape.dims, order) + + +@get_grad_basis.register +def _(shape: Simplex, order: int): + import modepy as mp + return mp.grad_simplex_onb(shape.dims, order) + +# }}} + + +# {{{ hypercube + +class Hypercube(Shape): + @property + def nfaces(self): + return 2 * self.dims + + +@get_unit_vertices.register +def _(shape: Hypercube): + from modepy.nodes import tensor_product_nodes + return tensor_product_nodes(shape.dims, np.array([-1.0, 1.0])).T + + +@get_face_vertex_indices.register +def _(shape: Hypercube): + return { + 1: ((0b0,), (0b1,)), + 2: ((0b00, 0b01), (0b10, 0b11), (0b00, 0b10), (0b01, 0b11)), + 3: ( + (0b000, 0b001, 0b010, 0b011,), + (0b100, 0b101, 0b110, 0b111,), + + (0b000, 0b010, 0b100, 0b110,), + (0b001, 0b011, 0b101, 0b111,), + + (0b000, 0b001, 0b100, 0b101,), + (0b010, 0b011, 0b110, 0b111,), + ) + }[shape.dims] + + +@get_face_map.register +def _(shape: Hypercube, face_vertices: np.ndarray): + dims, npoints = face_vertices.shape + if npoints != 2**(dims - 1): + raise ValueError("'face_vertices' has wrong shape") + + origin = face_vertices[:, 0].reshape(-1, 1) + face_basis = face_vertices[:, -2:0:-1] - origin + + return lambda p: origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) + + +@get_quadrature.register +def _(shape: Hypercube, order: int): + import modepy as mp + if shape.dims == 0: + quad = mp.Quadrature(np.empty((0, 1)), np.empty((0, 1))) + else: + from modepy.quadrature import LegendreGaussTensorProductQuadrature + quad = LegendreGaussTensorProductQuadrature(order, shape.dims) + + return quad + + +@get_unit_nodes.register +def _(shape: Hypercube, order: int): + import modepy as mp + return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dims, order) + + +@get_basis.register +def _(shape: Hypercube, order: int): + import modepy as mp + return mp.legendre_tensor_product_basis(shape.dims, order) + + +@get_grad_basis.register +def _(shape: Hypercube, order: int): + import modepy as mp + return mp.grad_legendre_tensor_product_basis(shape.dims, order) + +# }}} diff --git a/modepy/tools.py b/modepy/tools.py index 48cf231c..ea3b7b06 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -233,15 +233,6 @@ def barycentric_to_equilateral(bary): # }}} -# {{{ hypercube coordinate mapping - -def hypercube_unit_vertices(dims): - from modepy.nodes import tensor_product_nodes - return tensor_product_nodes(dims, np.array([-1.0, 1.0])).T - -# }}} - - def pick_random_simplex_unit_coordinate(rng, dims): offset = 0.05 base = -1 + offset @@ -257,6 +248,7 @@ def pick_random_simplex_unit_coordinate(rng, dims): def pick_random_hypercube_unit_coordinate(rng, dims): return np.array([rng.uniform(-1.0, 1.0) for _ in range(dims)]) + # {{{ accept_scalar_or_vector decorator class accept_scalar_or_vector: # noqa diff --git a/setup.py b/setup.py index 337a13a8..05c8d197 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,8 @@ def main(): "numpy", "pytools>=2013.1", "pytest>=2.3", + + "dataclasses; python_version<='3.6'", ]) diff --git a/test/test_quadrature.py b/test/test_quadrature.py index dcfa0e27..4c9b2f9d 100644 --- a/test/test_quadrature.py +++ b/test/test_quadrature.py @@ -23,6 +23,7 @@ import numpy as np import pytest + import modepy as mp import logging @@ -144,11 +145,11 @@ def test_simplex_quadrature(quad_class, highest_order, dim): break -@pytest.mark.parametrize("cls", [ +@pytest.mark.parametrize("quad_cls", [ mp.WitherdenVincentQuadrature ]) @pytest.mark.parametrize("dim", [2, 3]) -def test_hypercube_quadrature(cls, dim): +def test_hypercube_quadrature(quad_cls, dim): from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam from modepy.tools import Monomial @@ -167,14 +168,14 @@ def _check_monomial(quad, comb): order = 1 while True: try: - quad = cls(order, dim) + quad = quad_cls(order, dim) except mp.QuadratureRuleUnavailable: logger.info("UNAVAILABLE at order %d", order) break assert np.all(quad.weights > 0) - logger.info("quadrature: %s %d %d", cls, order, quad.exact_to) + logger.info("quadrature: %s %d %d", quad_cls, order, quad.exact_to) for comb in gnitstam(quad.exact_to, dim): assert _check_monomial(quad, comb) < 5.0e-15 diff --git a/test/test_tools.py b/test/test_tools.py index 60dad99b..4cbb0965 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -23,6 +23,7 @@ import numpy as np import numpy.linalg as la import modepy as mp +from modepy.shapes import Simplex, Hypercube from functools import partial import pytest @@ -138,100 +139,30 @@ def estimate_resid(inner_n): # }}} -# {{{ bases and nodes and things - -class _SimplexElement: - def __init__(self, dims, order): - self.dims = dims - self.order = order - - self.basis = mp.simplex_onb(dims, order) - if dims > 0: - self.grad_basis = mp.grad_simplex_onb(dims, order) - self.nodes = mp.warp_and_blend_nodes(dims, order) - - self.nfaces = dims + 1 - self.domain = "simplex" - - from modepy.tools import unit_vertices - self.unit_vertices = unit_vertices(dims).T - - @property - def face_vertex_indices(self): - result = np.empty((self.dims + 1, self.dims), dtype=np.int) - indices = np.arange(self.dims + 1) - - for iface in range(self.nfaces): - result[iface, :] = np.hstack([indices[:iface], indices[iface + 1:]]) - - return result - - -class _TensorProductElement: - def __init__(self, dims, order): - self.dims = dims - self.order = order - - self.basis = mp.legendre_tensor_product_basis(dims, order) - if dims > 0: - self.grad_basis = mp.grad_legendre_tensor_product_basis(dims, order) - self.nodes = mp.legendre_gauss_lobatto_tensor_product_nodes(dims, order) - - self.nfaces = 2 * dims - self.domain = "hypercube" - - from modepy.tools import hypercube_unit_vertices - self.unit_vertices = hypercube_unit_vertices(dims).T - - @property - def face_vertex_indices(self): - fvi = { - 1: ((0b0,), (0b1,)), - 2: ((0b00, 0b01), (0b10, 0b11), (0b00, 0b10), (0b01, 0b11)), - 3: ( - (0b000, 0b001, 0b010, 0b011,), - (0b100, 0b101, 0b110, 0b111,), - - (0b000, 0b010, 0b100, 0b110,), - (0b001, 0b011, 0b101, 0b111,), - - (0b000, 0b001, 0b100, 0b101,), - (0b010, 0b011, 0b110, 0b111,), - ) - }[self.dims] - - return np.array(fvi) - -# }}} - - # {{{ test_resampling_matrix @pytest.mark.parametrize("dims", [1, 2, 3]) -@pytest.mark.parametrize("eltype", ["simplex", "tensor"]) -def test_resampling_matrix(dims, eltype): - ncoarse = 5 - nfine = 10 - - if eltype == "simplex": - coarse = _SimplexElement(dims, ncoarse) - fine = _SimplexElement(dims, nfine) - elif eltype == "tensor": - coarse = _TensorProductElement(dims, ncoarse) - fine = _TensorProductElement(dims, nfine) - else: - raise ValueError(f"unknown element type: {eltype}") +@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): + from modepy.shapes import get_unit_nodes, get_basis + shape = shape_cls(dims) + + coarse_nodes = get_unit_nodes(shape, ncoarse) + coarse_basis = get_basis(shape, ncoarse) + + fine_nodes = get_unit_nodes(shape, nfine) + fine_basis = get_basis(shape, nfine) my_eye = np.dot( - mp.resampling_matrix(fine.basis, coarse.nodes, fine.nodes), - mp.resampling_matrix(coarse.basis, fine.nodes, coarse.nodes)) + mp.resampling_matrix(fine_basis, coarse_nodes, fine_nodes), + mp.resampling_matrix(coarse_basis, fine_nodes, coarse_nodes)) assert la.norm(my_eye - np.eye(len(my_eye))) < 3e-13 my_eye_least_squares = np.dot( - mp.resampling_matrix(coarse.basis, coarse.nodes, fine.nodes, + mp.resampling_matrix(coarse_basis, coarse_nodes, fine_nodes, least_squares_ok=True), - mp.resampling_matrix(coarse.basis, fine.nodes, coarse.nodes), + mp.resampling_matrix(coarse_basis, fine_nodes, coarse_nodes), ) assert la.norm(my_eye_least_squares - np.eye(len(my_eye_least_squares))) < 4e-13 @@ -242,24 +173,22 @@ def test_resampling_matrix(dims, eltype): # {{{ test_diff_matrix @pytest.mark.parametrize("dims", [1, 2, 3]) -@pytest.mark.parametrize("eltype", ["simplex", "tensor"]) -def test_diff_matrix(dims, eltype): - n = 5 - - if eltype == "simplex": - el = _SimplexElement(dims, n) - elif eltype == "tensor": - el = _TensorProductElement(dims, n) - else: - raise ValueError(f"unknown element type: {eltype}") +@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +def test_diff_matrix(dims, shape_cls, order=5): + from modepy.shapes import get_unit_nodes, get_basis, get_grad_basis + shape = shape_cls(dims) - diff_mat = mp.differentiation_matrices(el.basis, el.grad_basis, el.nodes) + nodes = get_unit_nodes(shape, order) + basis = get_basis(shape, order) + grad_basis = get_grad_basis(shape, order) + + diff_mat = mp.differentiation_matrices(basis, grad_basis, nodes) if isinstance(diff_mat, tuple): diff_mat = diff_mat[0] - f = np.sin(el.nodes[0]) + f = np.sin(nodes[0]) - df_dx = np.cos(el.nodes[0]) + df_dx = np.cos(nodes[0]) df_dx_num = np.dot(diff_mat, f) error = la.norm(df_dx - df_dx_num) / la.norm(df_dx) @@ -292,31 +221,29 @@ def test_diff_matrix_permutation(dims): # {{{ test_face_mass_matrix -@pytest.mark.parametrize("dim", [1, 2, 3]) -@pytest.mark.parametrize("eltype", ["simplex", "tensor"]) -def test_modal_face_mass_matrix(dim, eltype, order=3): +@pytest.mark.parametrize("dims", [2, 3]) +@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +def test_modal_face_mass_matrix(dims, shape_cls, order=3): np.set_printoptions(linewidth=200) + shape = shape_cls(dims) - if eltype == "simplex": - el = _SimplexElement(dim, order) - elif eltype == "tensor": - el = _TensorProductElement(dim, order) - else: - raise ValueError(f"unknown element type: '{eltype}'") + from modepy.shapes import get_unit_vertices, get_basis + vertices = get_unit_vertices(shape).T + basis = get_basis(shape, order - 1) - all_verts = el.unit_vertices - fvi = el.face_vertex_indices + from modepy.shapes import get_face_vertex_indices + fvi = get_face_vertex_indices(shape) from modepy.matrices import modal_face_mass_matrix - for iface in range(el.nfaces): - verts = all_verts[:, fvi[iface]] + for iface in range(shape.nfaces): + face_vertices = vertices[:, fvi[iface]] - fmm = modal_face_mass_matrix(el.basis, order, verts, domain=el.domain) - fmm2 = modal_face_mass_matrix(el.basis, order+1, verts, domain=el.domain) + fmm = modal_face_mass_matrix(basis, order, face_vertices, shape=shape) + fmm2 = modal_face_mass_matrix(basis, order+1, face_vertices, shape=shape) error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) logger.info("fmm error: %.5e", error) - assert error < 1e-11 + assert error < 1e-11, f"error {error:.5e} on face {iface}" fmm[np.abs(fmm) < 1e-13] = 0 nnz = np.sum(fmm > 0) @@ -324,45 +251,45 @@ def test_modal_face_mass_matrix(dim, eltype, order=3): logger.info("fmm: nnz %d\n%s", nnz, fmm) -@pytest.mark.parametrize("dim", [1, 2, 3]) -@pytest.mark.parametrize("eltype", ["simplex", "tensor"]) -def test_nodal_face_mass_matrix(dim, eltype, order=3): +@pytest.mark.parametrize("dims", [2, 3]) +@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +def test_nodal_face_mass_matrix(dims, shape_cls, order=3): np.set_printoptions(linewidth=200) + volume = shape_cls(dims) + face = shape_cls(dims - 1) - if eltype == "simplex": - volume = _SimplexElement(dim, order) - face = _SimplexElement(dim - 1, order) - elif eltype == "tensor": - volume = _TensorProductElement(dim, order) - face = _TensorProductElement(dim - 1, order) - else: - raise ValueError(f"unknown element type: '{eltype}'") + from modepy.shapes import get_unit_vertices, get_unit_nodes, get_basis + vertices = get_unit_vertices(volume).T + volume_nodes = get_unit_nodes(volume, order) + volume_basis = get_basis(volume, order) + face_nodes = get_unit_nodes(face, order) - all_verts = volume.unit_vertices - fvi = volume.face_vertex_indices + from modepy.shapes import get_face_vertex_indices + fvi = get_face_vertex_indices(volume) from modepy.matrices import nodal_face_mass_matrix for iface in range(volume.nfaces): - verts = all_verts[:, fvi[iface]] + face_vertices = vertices[:, fvi[iface]] fmm = nodal_face_mass_matrix( - volume.basis, volume.nodes, face.nodes, order, verts, - domain=volume.domain) + volume_basis, volume_nodes, face_nodes, order, face_vertices, + shape=volume) fmm2 = nodal_face_mass_matrix( - volume.basis, volume.nodes, face.nodes, order+1, verts, - domain=volume.domain) + volume_basis, volume_nodes, face_nodes, order+1, face_vertices, + shape=volume) error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) logger.info("fmm error: %.5e", error) - assert error < 1e-11 + assert error < 1e-11, f"error {error:.5e} on face {iface}" fmm[np.abs(fmm) < 1e-13] = 0 nnz = np.sum(fmm > 0) logger.info("fmm: nnz %d\n%s", nnz, fmm) - logger.info("mass matrix:\n%s", - mp.mass_matrix(face.basis, face.nodes)) + logger.info("mass matrix:\n%s", mp.mass_matrix( + get_basis(face, order), + get_unit_nodes(face, order))) # }}} From 16180daef8a35e78af406485bf275cac7b7de4a3 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Sat, 14 Nov 2020 15:01:05 -0600 Subject: [PATCH 09/68] use shapes in estimate_lebesgue_constant --- modepy/shapes.py | 34 ++++++++++++++++++++++++-------- modepy/tools.py | 48 ++++++++++++++++++---------------------------- test/test_tools.py | 21 +++++++++----------- 3 files changed, 54 insertions(+), 49 deletions(-) diff --git a/modepy/shapes.py b/modepy/shapes.py index e69f30cb..6f2354b1 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -56,7 +56,7 @@ def get_unit_vertices(shape: Shape): """ :returns: an :class:`~numpy.ndarray` of shape ``(nvertices, dims)``. """ - raise NotImplementedError + raise NotImplementedError(type(shape).__name__) @singledispatch @@ -65,8 +65,7 @@ def get_face_vertex_indices(shape: Shape): :results: indices into the vertices returned by :func:`get_unit_vertices` belonging to each face. """ - - raise NotImplementedError + raise NotImplementedError(type(shape).__name__) @singledispatch @@ -76,7 +75,7 @@ def get_face_map(shape: Shape, face_vertices: np.ndarray): unit nodes on the face represented by *face_vertices* and maps them to the volume. """ - raise NotImplementedError + raise NotImplementedError(type(shape).__name__) @singledispatch @@ -84,22 +83,27 @@ def get_quadrature(shape: Shape, order: int): """ :returns: a :class:`~modepy.Quadrature` instance of the given *order*. """ - raise NotImplementedError + raise NotImplementedError(type(shape).__name__) + + +@singledispatch +def get_node_tuples(shape: Shape, order: int): + raise NotImplementedError(type(shape).__name__) @singledispatch def get_unit_nodes(shape: Shape, order: int): - raise NotImplementedError + raise NotImplementedError(type(shape).__name__) @singledispatch def get_basis(shape: Shape, order: int): - raise NotImplementedError + raise NotImplementedError(type(shape).__name__) @singledispatch def get_grad_basis(shape: Shape, order: int): - raise NotImplementedError + raise NotImplementedError(type(shape).__name__) # }}} @@ -155,6 +159,13 @@ def _(shape: Simplex, order: int): return quad +@get_node_tuples.register +def _(shape: Simplex, order: int): + from pytools import \ + generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam + return list(gnitsam(order, shape.dims)) + + @get_unit_nodes.register def _(shape: Simplex, order: int): import modepy as mp @@ -231,6 +242,13 @@ def _(shape: Hypercube, order: int): return quad +@get_node_tuples.register +def _(shape: Hypercube, order: int): + from pytools import \ + generate_nonnegative_integer_tuples_below as gnitb + return list(gnitb(order, shape.dims)) + + @get_unit_nodes.register def _(shape: Hypercube, order: int): import modepy as mp diff --git a/modepy/tools.py b/modepy/tools.py index ea3b7b06..eedbc3ee 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -493,26 +493,12 @@ def plot_element_values(n, nodes, values, resample_n=None, # {{{ lebesgue constant -def _evaluate_lebesgue_function(n, nodes, domain): - dims = len(nodes) +def _evaluate_lebesgue_function(n, nodes, shape): huge_n = 30*n - if domain == "simplex": - from modepy.modes import simplex_onb as domain_basis_onb - from pytools import ( - generate_nonnegative_integer_tuples_summing_to_at_most - as generate_node_tuples) - elif domain == "hypercube": - from modepy.modes import ( - legendre_tensor_product_basis as domain_basis_onb) - from pytools import ( - generate_nonnegative_integer_tuples_below - as generate_node_tuples) - else: - raise ValueError(f"unknown domain: '{domain}'") - - basis = domain_basis_onb(dims, n) - equi_node_tuples = list(generate_node_tuples(huge_n, dims)) + from modepy.shapes import get_basis, get_node_tuples + basis = get_basis(shape, n) + equi_node_tuples = get_node_tuples(shape, huge_n) equi_nodes = (np.array(equi_node_tuples, dtype=np.float64)/huge_n*2 - 1).T from modepy.matrices import vandermonde @@ -526,7 +512,7 @@ def _evaluate_lebesgue_function(n, nodes, domain): return lebesgue_worst, equi_node_tuples, equi_nodes -def estimate_lebesgue_constant(n, nodes, domain=None, visualize=False): +def estimate_lebesgue_constant(n, nodes, shape=None, visualize=False): """Estimate the `Lebesgue constant `_ @@ -534,8 +520,7 @@ def estimate_lebesgue_constant(n, nodes, domain=None, visualize=False): :arg nodes: an array of shape *(dims, nnodes)* as returned by :func:`modepy.warp_and_blend_nodes`. - :arg domain: represents the domain of the reference element and can be - either ``"simplex"`` or ``"hypercube"``. + :arg shape: a :class:`~modepy.shapes.Shape`. :arg visualize: visualize the function that gives rise to the returned Lebesgue constant. (2D only for now) :return: the Lebesgue constant, a scalar. @@ -546,24 +531,29 @@ def estimate_lebesgue_constant(n, nodes, domain=None, visualize=False): *domain* parameter was added with support for nodes on the unit hypercube (i.e. unit square in 2D and unit cube in 3D). + + .. versionchanged:: 2020.3 + + Renamed *domain* to *shape*. """ - if domain is None: - domain = "simplex" + if shape is None: + from modepy.shapes import Simplex + shape = Simplex(len(nodes)) - dims = len(nodes) lebesgue_worst, equi_node_tuples, equi_nodes = \ - _evaluate_lebesgue_function(n, nodes, domain) + _evaluate_lebesgue_function(n, nodes, shape) lebesgue_constant = np.max(lebesgue_worst) if not visualize: return lebesgue_constant - if dims == 2: + if shape.dims == 2: print(f"Lebesgue constant: {lebesgue_constant}") - if domain == "simplex": + from modepy.shapes import Simplex, Hypercube + if isinstance(shape, Simplex): triangles = simplex_submesh(equi_node_tuples) - elif domain == "hypercube": + elif isinstance(shape, Hypercube): triangles = hypercube_submesh(equi_node_tuples) else: triangles = None @@ -601,7 +591,7 @@ def estimate_lebesgue_constant(n, nodes, domain=None, visualize=False): ax.set_aspect("equal") plt.show() else: - raise ValueError(f"visualization is not supported in {dims}D") + raise ValueError(f"visualization is not supported in {shape.dims}D") return lebesgue_constant diff --git a/test/test_tools.py b/test/test_tools.py index 4cbb0965..20827b55 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -298,28 +298,24 @@ def test_nodal_face_mass_matrix(dims, shape_cls, order=3): @pytest.mark.parametrize("dims", [1, 2]) @pytest.mark.parametrize("order", [3, 5, 8]) -@pytest.mark.parametrize("domain", ["simplex", "hypercube"]) -def test_estimate_lebesgue_constant(dims, order, domain, visualize=False): +@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): logging.basicConfig(level=logging.INFO) + shape = shape_cls(dims) - if domain == "simplex": - nodes = mp.warp_and_blend_nodes(dims, order) - elif domain == "hypercube": - from modepy.nodes import legendre_gauss_lobatto_tensor_product_nodes - nodes = legendre_gauss_lobatto_tensor_product_nodes(dims, order) - else: - raise ValueError(f"unknown domain: '{domain}'") + from modepy.shapes import get_unit_nodes + nodes = get_unit_nodes(shape, order) from modepy.tools import estimate_lebesgue_constant - lebesgue_constant = estimate_lebesgue_constant(order, nodes, domain=domain) - logger.info("%s-%d/%s: %.5e", domain, dims, order, lebesgue_constant) + lebesgue_constant = estimate_lebesgue_constant(order, nodes, shape=shape) + logger.info("%s-%d/%s: %.5e", shape, dims, order, lebesgue_constant) if not visualize: return from modepy.tools import _evaluate_lebesgue_function lebesgue, equi_node_tuples, equi_nodes = \ - _evaluate_lebesgue_function(order, nodes, domain) + _evaluate_lebesgue_function(order, nodes, shape) import matplotlib.pyplot as plt fig = plt.figure() @@ -340,6 +336,7 @@ def test_estimate_lebesgue_constant(dims, order, domain, visualize=False): else: raise ValueError(f"unsupported dimension: {dims}") + domain = type(shape).__name__.lower() fig.savefig(f"estimate_lebesgue_constant_{domain}_{dims}_order_{order}") # }}} From 3df2de1b3d1e736bacb0c75355daff40318cba6b Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Sat, 14 Nov 2020 15:34:10 -0600 Subject: [PATCH 10/68] add docs and some py3.6 fixes --- doc/index.rst | 1 + doc/nodes.rst | 93 ++++---------------------- doc/shapes.rst | 166 +++++++++++++++++++++++++++++++++++++++++++++++ modepy/shapes.py | 47 +++++--------- 4 files changed, 196 insertions(+), 111 deletions(-) create mode 100644 doc/shapes.rst diff --git a/doc/index.rst b/doc/index.rst index 7dfe6cd2..38a65a5e 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -42,6 +42,7 @@ Contents .. toctree:: :maxdepth: 2 + shapes modes nodes quadrature diff --git a/doc/nodes.rst b/doc/nodes.rst index a461abe3..daf21889 100644 --- a/doc/nodes.rst +++ b/doc/nodes.rst @@ -1,85 +1,8 @@ Interpolation Nodes =================== -Coordinate systems on simplices -------------------------------- - -.. _tri-coords: - -Coordinates on the triangle -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Unit coordinates :math:`(r,s)`:: - - C - |\ - | \ - | O - | \ - | \ - A-----B - -Vertices in unit coordinates:: - - O = (0,0) - A = (-1,-1) - B = (1,-1) - C = (-1,1) - -Equilateral coordinates :math:`(x,y)`:: - - C - / \ - / \ - / \ - / O \ - / \ - A-----------B - -Vertices in equilateral coordinates:: - - O = (0,0) - A = (-1,-1/sqrt(3)) - B = (1,-1/sqrt(3)) - C = (0,2/sqrt(3)) - -.. _tet-coords: - -Coordinates on the tetrahedron -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Unit coordinates :math:`(r,s,t)`:: - - ^ s - | - C - /|\ - / | \ - / | \ - / | \ - / O| \ - / __A-----B---> r - /_--^ ___--^^ - ,D--^^^ - t L - -(squint, and it might start making sense...) - -Vertices in unit coordinates:: - - O=( 0, 0, 0) - A=(-1,-1,-1) - B=(+1,-1,-1) - C=(-1,+1,-1) - D=(-1,-1,+1) - -Vertices in equilateral coordinates :math:`(x,y,z)`:: - - O = (0,0,0) - A = (-1,-1/sqrt(3),-1/sqrt(6)) - B = ( 1,-1/sqrt(3),-1/sqrt(6)) - C = ( 0, 2/sqrt(3),-1/sqrt(6)) - D = ( 0, 0, 3/sqrt(6)) +Simplices +^^^^^^^^^ Transformations between coordinate systems ------------------------------------------ @@ -100,7 +23,17 @@ Node sets for interpolation .. autofunction:: equidistant_nodes .. autofunction:: warp_and_blend_nodes -.. autofunction:: tensor_product_nodes Also see :class:`modepy.VioreanuRokhlinSimplexQuadrature` if nodes on the boundary are not required. + +Hypercubes +^^^^^^^^^^ + +Node sets for interpolation +--------------------------- + +.. currentmodule:: modepy + +.. autofunction:: tensor_product_nodes +.. autofunction:: legendre_gauss_lobatto_tensor_product_nodes diff --git a/doc/shapes.rst b/doc/shapes.rst new file mode 100644 index 00000000..2546fdef --- /dev/null +++ b/doc/shapes.rst @@ -0,0 +1,166 @@ +Shapes +====== + +`modepy.shapes` provides a generic description of the supported shapes +(i.e. reference elements). + +Interface +^^^^^^^^^ + +.. currentmodule:: modepy.shapes + +.. autoclass:: Shape +.. autofunction:: get_unit_vertices +.. autofunction:: get_node_tuples +.. autofunction:: get_face_map + +Simplices +^^^^^^^^^ + +.. autoclass:: Simplex + +.. _tri-coords: + +Coordinates on the triangle +--------------------------- + +Unit coordinates :math:`(r, s)`:: + + ^ s + | + C + |\ + | \ + | O + | \ + | \ + A-----B--> r + +Vertices in unit coordinates:: + + O = ( 0, 0) + A = (-1, -1) + B = ( 1, -1) + C = (-1, 1) + +Equilateral coordinates :math:`(x, y)`:: + + C + / \ + / \ + / \ + / O \ + / \ + A-----------B + +Vertices in equilateral coordinates:: + + O = ( 0, 0) + A = (-1, -1/sqrt(3)) + B = ( 1, -1/sqrt(3)) + C = ( 0, 2/sqrt(3)) + +.. _tet-coords: + +Coordinates on the tetrahedron +------------------------------ + +Unit coordinates :math:`(r, s, t)`:: + + ^ s + | + C + /|\ + / | \ + / | \ + / | \ + / O| \ + / __A-----B---> r + /_--^ ___--^^ + ,D--^^^ + t L + +(squint, and it might start making sense...) + +Vertices in unit coordinates :math:`(r, s, t)`:: + + O = ( 0, 0, 0) + A = (-1, -1, -1) + B = ( 1, -1, -1) + C = (-1, 1, -1) + D = (-1, -1, 1) + +Vertices in equilateral coordinates :math:`(x, y, z)`:: + + O = ( 0, 0, 0) + A = (-1, -1/sqrt(3), -1/sqrt(6)) + B = ( 1, -1/sqrt(3), -1/sqrt(6)) + C = ( 0, 2/sqrt(3), -1/sqrt(6)) + D = ( 0, 0, 3/sqrt(6)) + +Hypercubes +^^^^^^^^^^ + +.. autoclass:: Hypercube + +.. _square-coords: + +Coordinates on the square +------------------------- + +Unit coordinates on :math:`(r, s)`:: + + ^ s + | + C---------D + | | + | | + | O | + | | + | | + A---------B --> r + + +Vertices in unit coordinates:: + + O = ( 0, 0) + A = (-1, -1) + B = ( 1, -1) + C = (-1, 1) + D = ( 1, 1) + +.. _cube-coords: + +Coordinates on the cube +----------------------- + +Unit coordinates on :math:`(r, s, t)`:: + + t + ^ + | + E----------G + |\ |\ + | \ | \ + | \ | \ + | F------+---H + | | O | | + A---+------C---|--> s + \ | \ | + \ | \ | + \| \| + B----------D + \ + v r + +Verties in unit coordinates:: + + O = ( 0, 0, 0) + A = (-1, -1, -1) + B = ( 1, -1, -1) + C = (-1, 1, -1) + D = ( 1, 1, -1) + E = (-1, -1, 1) + F = ( 1, -1, 1) + G = (-1, 1, 1) + H = ( 1, 1, 1) diff --git a/modepy/shapes.py b/modepy/shapes.py index 6f2354b1..d2e2bd31 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -27,21 +27,6 @@ from functools import singledispatch from dataclasses import dataclass, field -__doc__ = """ -Shapes ------- - -.. currentmodule:: modepy - -.. autoclass:: Shape -.. autoclass:: Simplex -.. autoclass:: Hypercube - -.. autofunction:: get_unit_vertices -.. autofunction:: get_face_map -.. autofunction:: get_quadrature -""" - # {{{ interface @@ -116,13 +101,13 @@ def nfaces(self): return self.dims + 1 -@get_unit_vertices.register +@get_unit_vertices.register(Simplex) def _(shape: Simplex): from modepy.tools import unit_vertices return unit_vertices(shape.dims) -@get_face_vertex_indices.register +@get_face_vertex_indices.register(Simplex) def _(shape: Simplex): fvi = np.empty((shape.dims + 1, shape.dims), dtype=np.int) indices = np.arange(shape.dims + 1) @@ -133,7 +118,7 @@ def _(shape: Simplex): return fvi -@get_face_map.register +@get_face_map.register(Simplex) def _(shape: Simplex, face_vertices: np.ndarray): dims, npoints = face_vertices.shape if npoints != dims: @@ -145,7 +130,7 @@ def _(shape: Simplex, face_vertices: np.ndarray): return lambda p: origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) -@get_quadrature.register +@get_quadrature.register(Simplex) def _(shape: Simplex, order: int): import modepy as mp if shape.dims == 0: @@ -159,26 +144,26 @@ def _(shape: Simplex, order: int): return quad -@get_node_tuples.register +@get_node_tuples.register(Simplex) def _(shape: Simplex, order: int): from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam return list(gnitsam(order, shape.dims)) -@get_unit_nodes.register +@get_unit_nodes.register(Simplex) def _(shape: Simplex, order: int): import modepy as mp return mp.warp_and_blend_nodes(shape.dims, order) -@get_basis.register +@get_basis.register(Simplex) def _(shape: Simplex, order: int): import modepy as mp return mp.simplex_onb(shape.dims, order) -@get_grad_basis.register +@get_grad_basis.register(Simplex) def _(shape: Simplex, order: int): import modepy as mp return mp.grad_simplex_onb(shape.dims, order) @@ -194,13 +179,13 @@ def nfaces(self): return 2 * self.dims -@get_unit_vertices.register +@get_unit_vertices.register(Hypercube) def _(shape: Hypercube): from modepy.nodes import tensor_product_nodes return tensor_product_nodes(shape.dims, np.array([-1.0, 1.0])).T -@get_face_vertex_indices.register +@get_face_vertex_indices.register(Hypercube) def _(shape: Hypercube): return { 1: ((0b0,), (0b1,)), @@ -218,7 +203,7 @@ def _(shape: Hypercube): }[shape.dims] -@get_face_map.register +@get_face_map.register(Hypercube) def _(shape: Hypercube, face_vertices: np.ndarray): dims, npoints = face_vertices.shape if npoints != 2**(dims - 1): @@ -230,7 +215,7 @@ def _(shape: Hypercube, face_vertices: np.ndarray): return lambda p: origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) -@get_quadrature.register +@get_quadrature.register(Hypercube) def _(shape: Hypercube, order: int): import modepy as mp if shape.dims == 0: @@ -242,26 +227,26 @@ def _(shape: Hypercube, order: int): return quad -@get_node_tuples.register +@get_node_tuples.register(Hypercube) def _(shape: Hypercube, order: int): from pytools import \ generate_nonnegative_integer_tuples_below as gnitb return list(gnitb(order, shape.dims)) -@get_unit_nodes.register +@get_unit_nodes.register(Hypercube) def _(shape: Hypercube, order: int): import modepy as mp return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dims, order) -@get_basis.register +@get_basis.register(Hypercube) def _(shape: Hypercube, order: int): import modepy as mp return mp.legendre_tensor_product_basis(shape.dims, order) -@get_grad_basis.register +@get_grad_basis.register(Hypercube) def _(shape: Hypercube, order: int): import modepy as mp return mp.grad_legendre_tensor_product_basis(shape.dims, order) From 7acf2b0361b63c8216a7571f361c1d52c15a47c8 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 17 Nov 2020 17:15:21 -0600 Subject: [PATCH 11/68] move functions out of shapes module --- modepy/modes.py | 73 +++++++++++++++++++++++-------- modepy/nodes.py | 111 ++++++++++++++++++++++++++++++++++++----------- modepy/shapes.py | 77 ++++++++------------------------ 3 files changed, 159 insertions(+), 102 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index 6fe9e01e..505073c1 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -24,7 +24,10 @@ from math import sqrt import numpy as np + from modepy.tools import accept_scalar_or_vector +from modepy.shapes import Simplex, Hypercube +from modepy.shapes import get_basis, get_grad_basis __doc__ = """:mod:`modepy.modes` provides orthonormal bases and their @@ -95,6 +98,46 @@ """ +# {{{ shape basis functions + +# {{{ simplex + +@get_basis.register(Simplex) +def _(shape: Simplex, order: int): + if shape.dims <= 3: + return simplex_onb(shape.dims, order) + else: + return simplex_monomial_basis(shape.dims, order) + + +@get_grad_basis.register(Simplex) +def _(shape: Simplex, order: int): + if shape.dims <= 3: + return grad_simplex_onb(shape.dims, order) + else: + return grad_simplex_monomial_basis(shape.dims, order) + +# }}} + + +# {{{ hypercube + +@get_basis.register(Hypercube) +def _(shape: Hypercube, order: int): + import modepy as mp + return mp.legendre_tensor_product_basis(shape.dims, order) + + +@get_grad_basis.register(Hypercube) +def _(shape: Hypercube, order: int): + import modepy as mp + return mp.grad_legendre_tensor_product_basis(shape.dims, order) + +# }}} + +# }}} + + # {{{ jacobi polynomials def jacobi(alpha, beta, n, x): @@ -462,10 +505,10 @@ def simplex_onb_with_mode_ids(dims, n): ... versionadded: 2018.1 """ - from functools import partial - from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ - as gnitstam + from modepy.shapes import get_node_tuples + shape = Simplex(dims) + from functools import partial if dims == 0: def zerod_basis(x): if len(x.shape) == 1: @@ -473,16 +516,18 @@ def zerod_basis(x): else: return np.ones(x.shape[1]) - return ((0,),), (zerod_basis,) + mode_ids = get_node_tuples(shape, n) + return mode_ids, (zerod_basis,) elif dims == 1: + # FIXME: should also use get_node_tuples mode_ids = tuple(range(n+1)) return mode_ids, tuple(partial(jacobi, 0, 0, i) for i in mode_ids) elif dims == 2: - mode_ids = tuple(gnitstam(n, dims)) + mode_ids = get_node_tuples(shape, n) return mode_ids, tuple(partial(pkdo_2d, order) for order in mode_ids) elif dims == 3: - mode_ids = tuple(gnitstam(n, dims)) + mode_ids = get_node_tuples(shape, n) return mode_ids, tuple(partial(pkdo_3d, order) for order in mode_ids) else: raise NotImplementedError("%d-dimensional bases" % dims) @@ -560,10 +605,10 @@ def simplex_monomial_basis_with_mode_ids(dims, n): .. versionadded:: 2018.1 """ + from modepy.shapes import get_node_tuples + mode_ids = get_node_tuples(Simplex(dims), n) + from functools import partial - from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ - as gnitstam - mode_ids = tuple(gnitstam(n, dims)) return mode_ids, tuple(partial(monomial, order) for order in mode_ids) @@ -605,18 +650,12 @@ def grad_simplex_monomial_basis(dims, n): # undocumented for now def simplex_best_available_basis(dims, n): - if dims <= 3: - return simplex_onb(dims, n) - else: - return simplex_monomial_basis(dims, n) + return get_basis(Simplex(dims), n) # undocumented for now def grad_simplex_best_available_basis(dims, n): - if dims <= 3: - return grad_simplex_onb(dims, n) - else: - return grad_simplex_monomial_basis(dims, n) + return get_grad_basis(Simplex(dims), n) # }}} diff --git a/modepy/nodes.py b/modepy/nodes.py index 36c44737..ba94232a 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -24,6 +24,74 @@ import numpy as np import numpy.linalg as la +from modepy.shapes import Simplex, Hypercube +from modepy.shapes import get_node_count, get_node_tuples, get_unit_nodes + + +# {{{ shape nodes + +# {{{ simplex + +@get_node_count.register(Simplex) +def _(shape: Simplex, order: int): + try: + from math import comb # comb is v3.8+ + node_count = comb(order + shape.dims, shape.dims) + except ImportError: + from functools import reduce + from operator import mul + node_count = reduce(mul, range(order + 1, order + shape.dims + 1), 1) \ + // reduce(mul, range(1, shape.dims + 1), 1) + + return node_count + + +@get_node_tuples.register(Simplex) +def _(shape: Simplex, order: int): + from pytools import \ + generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam + if shape.dims == 0: + return ((0,),) + else: + return tuple(gnitsam(order, shape.dims)) + + +@get_unit_nodes.register(Simplex) +def _(shape: Simplex, order: int): + import modepy as mp + return mp.warp_and_blend_nodes(shape.dims, order) + +# }}} + + +# {{{ hypercube + +@get_node_count.register(Hypercube) +def _(shape: Hypercube, order: int): + return (order + 1)**shape.dims + + +@get_node_tuples.register(Hypercube) +def _(shape: Hypercube, order: int): + from pytools import \ + generate_nonnegative_integer_tuples_below as gnitb + if shape.dims == 0: + return ((0,),) + else: + return tuple(gnitb(order, shape.dims)) + + +@get_unit_nodes.register(Hypercube) +def _(shape: Hypercube, order: int): + import modepy as mp + return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dims, order) + +# }}} + +# }}} + + +# {{{ equidistant nodes def equidistant_nodes(dims, n, node_tuples=None): """ @@ -37,26 +105,20 @@ def equidistant_nodes(dims, n, node_tuples=None): of the interpolation nodes. (see :ref:`tri-coords` and :ref:`tet-coords`) """ + shape = Simplex(dims) if node_tuples is None: - from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ - as gnitstam - node_tuples = list(gnitstam(n, dims)) + node_tuples = get_node_tuples(shape, n) else: - try: - from math import comb # comb is v3.8+ - node_count = comb(n + dims, dims) - except ImportError: - from functools import reduce - from operator import mul - node_count = reduce(mul, range(n + 1, n + dims + 1), 1) \ - // reduce(mul, range(1, dims + 1), 1) - - if len(node_tuples) != node_count: + if len(node_tuples) != get_node_count(shape, n): raise ValueError("'node_tuples' list does not have the correct length") - # shape: (2, nnodes) + # shape: (dims, nnodes) return (np.array(node_tuples, dtype=np.float64)/n*2 - 1).T +# }}} + + +# {{{ warp and blend simplex nodes def warp_factor(n, output_nodes, scaled=True): """Compute warp function at order *n* and evaluate it at @@ -126,13 +188,12 @@ def warp_and_blend_nodes_2d(n, node_tuples=None): except IndexError: alpha = 5/3 + shape = Simplex(2) if node_tuples is None: - from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ - as gnitstam - node_tuples = list(gnitstam(n, 2)) + node_tuples = get_node_tuples(shape, n) else: - if len(node_tuples) != (n+1)*(n+2)//2: - raise ValueError("node_tuples list does not have the correct length") + if len(node_tuples) != get_node_count(shape, n): + raise ValueError("'node_tuples' list does not have the correct length") # shape: (2, nnodes) unit_nodes = (np.array(node_tuples, dtype=np.float64)/n*2 - 1).T @@ -163,13 +224,12 @@ def warp_and_blend_nodes_3d(n, node_tuples=None): except IndexError: alpha = 1. + shape = Simplex(3) if node_tuples is None: - from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ - as gnitstam - node_tuples = list(gnitstam(n, 3)) + node_tuples = get_node_tuples(shape, n) else: - if len(node_tuples) != (n+1)*(n+2)*(n+3)//6: - raise ValueError("node_tuples list does not have the correct length") + if len(node_tuples) != get_node_count(shape, n): + raise ValueError("'node_tuples' list does not have the correct length") # shape: (3, nnodes) unit_nodes = (np.array(node_tuples, dtype=np.float64)/n*2 - 1).T @@ -291,6 +351,8 @@ def warp_and_blend_nodes(dims, n, node_tuples=None): # }}} +# }}} + # {{{ tensor product nodes @@ -320,5 +382,4 @@ def legendre_gauss_lobatto_tensor_product_nodes(dims, n): # }}} - # vim: foldmethod=marker diff --git a/modepy/shapes.py b/modepy/shapes.py index d2e2bd31..85c5446d 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -1,4 +1,5 @@ __copyright__ = """ +Copyright (c) 2013 Andreas Kloeckner Copyright (c) 2020 Alexandru Fikl """ @@ -25,15 +26,18 @@ import numpy as np from functools import singledispatch -from dataclasses import dataclass, field +from dataclasses import dataclass # {{{ interface @dataclass(frozen=True) class Shape: + """ + .. attribute :: dims + .. attribute :: nfaces + """ dims: int - nfaces: int = field(init=False) @singledispatch @@ -66,11 +70,16 @@ def get_face_map(shape: Shape, face_vertices: np.ndarray): @singledispatch def get_quadrature(shape: Shape, order: int): """ - :returns: a :class:`~modepy.Quadrature` instance of the given *order*. + :returns: a :class:`~modepy.Quadrature` that is exact up to ``2 * order + 1``. """ raise NotImplementedError(type(shape).__name__) +@singledispatch +def get_node_count(shape: Shape, order: int): + raise NotImplementedError(type(shape).__name__) + + @singledispatch def get_node_tuples(shape: Shape, order: int): raise NotImplementedError(type(shape).__name__) @@ -133,41 +142,13 @@ def _(shape: Simplex, face_vertices: np.ndarray): @get_quadrature.register(Simplex) def _(shape: Simplex, order: int): import modepy as mp - if shape.dims == 0: - quad = mp.Quadrature(np.empty((0, 1)), np.empty((0, 1))) - else: - try: - quad = mp.XiaoGimbutasSimplexQuadrature(2*order + 1, shape.dims) - except (mp.QuadratureRuleUnavailable, ValueError): - quad = mp.GrundmannMoellerSimplexQuadrature(order, shape.dims) + try: + quad = mp.XiaoGimbutasSimplexQuadrature(2*order + 1, shape.dims) + except (mp.QuadratureRuleUnavailable, ValueError): + quad = mp.GrundmannMoellerSimplexQuadrature(order, shape.dims) return quad - -@get_node_tuples.register(Simplex) -def _(shape: Simplex, order: int): - from pytools import \ - generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam - return list(gnitsam(order, shape.dims)) - - -@get_unit_nodes.register(Simplex) -def _(shape: Simplex, order: int): - import modepy as mp - return mp.warp_and_blend_nodes(shape.dims, order) - - -@get_basis.register(Simplex) -def _(shape: Simplex, order: int): - import modepy as mp - return mp.simplex_onb(shape.dims, order) - - -@get_grad_basis.register(Simplex) -def _(shape: Simplex, order: int): - import modepy as mp - return mp.grad_simplex_onb(shape.dims, order) - # }}} @@ -187,6 +168,7 @@ def _(shape: Hypercube): @get_face_vertex_indices.register(Hypercube) def _(shape: Hypercube): + # FIXME: replace by nicer n-dimensional formula return { 1: ((0b0,), (0b1,)), 2: ((0b00, 0b01), (0b10, 0b11), (0b00, 0b10), (0b01, 0b11)), @@ -226,29 +208,4 @@ def _(shape: Hypercube, order: int): return quad - -@get_node_tuples.register(Hypercube) -def _(shape: Hypercube, order: int): - from pytools import \ - generate_nonnegative_integer_tuples_below as gnitb - return list(gnitb(order, shape.dims)) - - -@get_unit_nodes.register(Hypercube) -def _(shape: Hypercube, order: int): - import modepy as mp - return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dims, order) - - -@get_basis.register(Hypercube) -def _(shape: Hypercube, order: int): - import modepy as mp - return mp.legendre_tensor_product_basis(shape.dims, order) - - -@get_grad_basis.register(Hypercube) -def _(shape: Hypercube, order: int): - import modepy as mp - return mp.grad_legendre_tensor_product_basis(shape.dims, order) - # }}} From b014fbee7d6ab2c8a69f8f953fc8566d9b43d398 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 17 Nov 2020 17:40:11 -0600 Subject: [PATCH 12/68] update docs and some fixes --- doc/shapes.rst | 2 +- modepy/matrices.py | 16 +++++++++------- modepy/modes.py | 40 ++++++++++++++++------------------------ modepy/shapes.py | 5 +++-- 4 files changed, 29 insertions(+), 34 deletions(-) diff --git a/doc/shapes.rst b/doc/shapes.rst index 2546fdef..d685dbaf 100644 --- a/doc/shapes.rst +++ b/doc/shapes.rst @@ -11,7 +11,7 @@ Interface .. autoclass:: Shape .. autofunction:: get_unit_vertices -.. autofunction:: get_node_tuples +.. autofunction:: get_face_vertex_indices .. autofunction:: get_face_map Simplices diff --git a/modepy/matrices.py b/modepy/matrices.py index caae2594..d999aafe 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -259,13 +259,14 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, if test_basis is None: test_basis = trial_basis - from modepy import shapes if shape is None: - shape = shapes.Simplex(face_vertices.shape[0] - 1) + from modepy.shapes import Simplex + shape = Simplex(face_vertices.shape[0]) + from modepy.shapes import get_face_map, get_quadrature face = type(shape)(shape.dims - 1) - fmap = shapes.get_face_map(shape, face_vertices) - quad = shapes.get_quadrature(face, order) + fmap = get_face_map(shape, face_vertices) + quad = get_quadrature(face, order) assert quad.exact_to > order*2 mapped_nodes = fmap(quad.nodes) @@ -301,11 +302,12 @@ def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, if test_basis is None: test_basis = trial_basis - from modepy import shapes if shape is None: - shape = shapes.Simplex(face_vertices.shape[0]) + from modepy.shapes import Simplex + shape = Simplex(face_vertices.shape[0]) - fmap = shapes.get_face_map(shape, face_vertices) + from modepy.shapes import get_face_map + fmap = get_face_map(shape, face_vertices) face_vdm = vandermonde(trial_basis, fmap(face_nodes)) # /!\ non-square vol_vdm = vandermonde(test_basis, volume_nodes) diff --git a/modepy/modes.py b/modepy/modes.py index 505073c1..bdf00b00 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -124,14 +124,12 @@ def _(shape: Simplex, order: int): @get_basis.register(Hypercube) def _(shape: Hypercube, order: int): - import modepy as mp - return mp.legendre_tensor_product_basis(shape.dims, order) + return legendre_tensor_product_basis(shape.dims, order) @get_grad_basis.register(Hypercube) def _(shape: Hypercube, order: int): - import modepy as mp - return mp.grad_legendre_tensor_product_basis(shape.dims, order) + return grad_legendre_tensor_product_basis(shape.dims, order) # }}} @@ -485,6 +483,13 @@ def diff_monomial(r, o): # {{{ dimension-independent interface for simplices +def zerod_basis(x): + if len(x.shape) == 1: + return 1 + else: + return np.ones(x.shape[1]) + + def simplex_onb_with_mode_ids(dims, n): """Return a list of orthonormal basis functions in dimension *dims* of maximal total degree *n*. @@ -510,15 +515,8 @@ def simplex_onb_with_mode_ids(dims, n): from functools import partial if dims == 0: - def zerod_basis(x): - if len(x.shape) == 1: - return 1 - else: - return np.ones(x.shape[1]) - mode_ids = get_node_tuples(shape, n) return mode_ids, (zerod_basis,) - elif dims == 1: # FIXME: should also use get_node_tuples mode_ids = tuple(range(n+1)) @@ -697,14 +695,12 @@ def tensor_product_basis(dims, basis_1d): .. versionadded:: 2017.1 """ - if dims == 0: - # NOTE: using to maintain consistency in the 0d case - return simplex_onb(dims, len(basis_1d)) + from modepy.shapes import Hypercube, get_node_tuples + mode_ids = get_node_tuples(Hypercube(dims), len(basis_1d)) - from pytools import generate_nonnegative_integer_tuples_below as gnitb return tuple( _TensorProductBasisFunction(order, [basis_1d[i] for i in order]) - for order in gnitb(len(basis_1d), dims)) + for order in mode_ids) def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): @@ -716,13 +712,9 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): .. versionadded:: 2020.2 """ - if dims == 0: - # NOTE: using to maintain consistency in the 0d case - return grad_simplex_onb(dims, len(basis_1d)) - - from pytools import ( - wandering_element, - generate_nonnegative_integer_tuples_below as gnitb) + from pytools import wandering_element + from modepy.shapes import Hypercube, get_node_tuples + mode_ids = get_node_tuples(Hypercube(dims), len(basis_1d)) func = (basis_1d, grad_basis_1d) return tuple( @@ -730,7 +722,7 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): [func[i][k] for i, k in zip(iderivative, order)] for iderivative in wandering_element(dims) ]) - for order in gnitb(len(basis_1d), dims)) + for order in mode_ids) def legendre_tensor_product_basis(dims, order): diff --git a/modepy/shapes.py b/modepy/shapes.py index 85c5446d..336b90e5 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -51,8 +51,9 @@ def get_unit_vertices(shape: Shape): @singledispatch def get_face_vertex_indices(shape: Shape): """ - :results: indices into the vertices returned by :func:`get_unit_vertices` - belonging to each face. + :results: a tuple of the length :attr:`Shape.nfaces`, where each entry + is a tuple of indices into the vertices returned by + :func:`get_unit_vertices`. """ raise NotImplementedError(type(shape).__name__) From fe407137962fc13a712252aeae14aa39fab1da4a Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 17 Nov 2020 17:49:00 -0600 Subject: [PATCH 13/68] add basis_with_mode_ids --- modepy/modes.py | 17 ++++++++++++++++- modepy/shapes.py | 5 +++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/modepy/modes.py b/modepy/modes.py index bdf00b00..7e995e7d 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -27,7 +27,7 @@ from modepy.tools import accept_scalar_or_vector from modepy.shapes import Simplex, Hypercube -from modepy.shapes import get_basis, get_grad_basis +from modepy.shapes import get_basis, get_grad_basis, get_basis_with_mode_ids __doc__ = """:mod:`modepy.modes` provides orthonormal bases and their @@ -117,6 +117,14 @@ def _(shape: Simplex, order: int): else: return grad_simplex_monomial_basis(shape.dims, order) + +@get_basis_with_mode_ids.register(Simplex) +def _(shape: Simplex, order: int): + if shape.dims <= 3: + return simplex_onb_with_mode_ids(shape.dims, order) + else: + return simplex_monomial_basis_with_mode_ids(shape.dims, order) + # }}} @@ -131,6 +139,13 @@ def _(shape: Hypercube, order: int): def _(shape: Hypercube, order: int): return grad_legendre_tensor_product_basis(shape.dims, order) + +@get_basis_with_mode_ids.register(Hypercube) +def _(shape: Hypercube, order: int): + from modepy.shapes import get_node_tuples + mode_ids = get_node_tuples(shape, order) + return mode_ids, get_basis(shape, order) + # }}} # }}} diff --git a/modepy/shapes.py b/modepy/shapes.py index 336b90e5..85766dd1 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -100,6 +100,11 @@ def get_basis(shape: Shape, order: int): def get_grad_basis(shape: Shape, order: int): raise NotImplementedError(type(shape).__name__) + +@singledispatch +def get_basis_with_mode_ids(shape: Shape, order: int): + raise NotImplementedError(type(shape).__name__) + # }}} From 3b3bada3b6c0245addc03b0c8743c89eb88b46c4 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 17 Nov 2020 20:24:11 -0600 Subject: [PATCH 14/68] update docs --- doc/quadrature.rst | 11 +++++++++++ doc/shapes.rst | 25 +++++++++++++------------ modepy/__init__.py | 6 +++++- modepy/matrices.py | 4 ++-- modepy/modes.py | 6 +++--- modepy/quadrature/witherden_vincent.py | 6 +++--- modepy/shapes.py | 17 +++++++++++++---- 7 files changed, 50 insertions(+), 25 deletions(-) diff --git a/doc/quadrature.rst b/doc/quadrature.rst index 008bf447..6d9045f2 100644 --- a/doc/quadrature.rst +++ b/doc/quadrature.rst @@ -55,4 +55,15 @@ Quadratures on the simplex .. autoclass:: VioreanuRokhlinSimplexQuadrature + +Quadratures on the hypercube +---------------------------- + +.. currentmodule:: modepy + +.. autoclass:: WitherdenVincentQuadrature + +.. autoclass:: TensorProductQuadrature +.. autoclass:: LegendreGaussTensorProductQuadrature + .. vim: sw=4 diff --git a/doc/shapes.rst b/doc/shapes.rst index d685dbaf..ffc1f7c0 100644 --- a/doc/shapes.rst +++ b/doc/shapes.rst @@ -1,14 +1,12 @@ Shapes ====== -`modepy.shapes` provides a generic description of the supported shapes -(i.e. reference elements). - -Interface -^^^^^^^^^ - +.. automodule:: modepy.shapes .. currentmodule:: modepy.shapes +:mod:`modepy.shapes` provides a generic description of the supported shapes +(i.e. reference elements). + .. autoclass:: Shape .. autofunction:: get_unit_vertices .. autofunction:: get_face_vertex_indices @@ -139,7 +137,7 @@ Unit coordinates on :math:`(r, s, t)`:: t ^ | - E----------G + B----------D |\ |\ | \ | \ | \ | \ @@ -149,7 +147,7 @@ Unit coordinates on :math:`(r, s, t)`:: \ | \ | \ | \ | \| \| - B----------D + E----------G \ v r @@ -157,10 +155,13 @@ Verties in unit coordinates:: O = ( 0, 0, 0) A = (-1, -1, -1) - B = ( 1, -1, -1) + B = (-1, -1, 1) C = (-1, 1, -1) - D = ( 1, 1, -1) - E = (-1, -1, 1) + D = (-1, 1, 1) + E = ( 1, -1, -1) F = ( 1, -1, 1) - G = (-1, 1, 1) + G = ( 1, 1, -1) H = ( 1, 1, 1) + +The order of the vertices in the hypercubes follows binary counting +in ``rst``. For example, in 3D, ``A, B, C, D, ...`` is ``000, 001, 010, 011, ...``. diff --git a/modepy/__init__.py b/modepy/__init__.py index 64228b8c..b951e888 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -38,7 +38,9 @@ diff_matrix_permutation, inverse_mass_matrix, mass_matrix, modal_face_mass_matrix, nodal_face_mass_matrix) -from modepy.quadrature import Quadrature, QuadratureRuleUnavailable +from modepy.quadrature import ( + Quadrature, QuadratureRuleUnavailable, + TensorProductQuadrature, LegendreGaussTensorProductQuadrature) from modepy.quadrature.jacobi_gauss import ( JacobiGaussQuadrature, LegendreGaussQuadrature, ChebyshevGaussQuadrature, GaussGegenbauerQuadrature) @@ -79,7 +81,9 @@ "XiaoGimbutasSimplexQuadrature", "GrundmannMoellerSimplexQuadrature", "VioreanuRokhlinSimplexQuadrature", "ClenshawCurtisQuadrature", "FejerQuadrature", + "WitherdenVincentQuadrature", + "TensorProductQuadrature", "LegendreGaussTensorProductQuadrature", ] from pytools import MovedFunctionDeprecationWrapper diff --git a/modepy/matrices.py b/modepy/matrices.py index d999aafe..91a40b8d 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -251,7 +251,7 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, .. versionadded :: 2016.1 - .. versionchanged:: 2020.5 + .. versionchanged:: 2020.3 Added *shape* parameter and support for :math:`[-1, 1]^d` domains. """ @@ -294,7 +294,7 @@ def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, .. versionadded :: 2016.1 - .. versionchanged:: 2020.5 + .. versionchanged:: 2020.3 Added *shape* parameter and support for :math:`[-1, 1]^d` domains. """ diff --git a/modepy/modes.py b/modepy/modes.py index 7e995e7d..1cea11f7 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -523,7 +523,7 @@ def simplex_onb_with_mode_ids(dims, n): * |koornwinder-ref| * |dubiner-ref| - ... versionadded: 2018.1 + .. versionadded:: 2018.1 """ from modepy.shapes import get_node_tuples shape = Simplex(dims) @@ -550,7 +550,7 @@ def simplex_onb(dims, n): """Return a list of orthonormal basis functions in dimension *dims* of maximal total degree *n*. - :returns: a class:`tuple` of functions, each of which + :returns: a :class:`tuple` of functions, each of which accepts arrays of shape *(dims, npts)* and return the function values as an array of size *npts*. 'Scalar' evaluation, by passing just one vector of length *dims*, @@ -629,7 +629,7 @@ def simplex_monomial_basis(dims, n): """Return a list of monomial basis functions in dimension *dims* of maximal total degree *n*. - :returns: a class:`tuple` of functions, each of which + :returns: a :class:`tuple` of functions, each of which accepts arrays of shape *(dims, npts)* and return the function values as an array of size *npts*. 'Scalar' evaluation, by passing just one vector of length *dims*, diff --git a/modepy/quadrature/witherden_vincent.py b/modepy/quadrature/witherden_vincent.py index 9d8b9a4c..2731d083 100644 --- a/modepy/quadrature/witherden_vincent.py +++ b/modepy/quadrature/witherden_vincent.py @@ -28,15 +28,15 @@ class WitherdenVincentQuadrature(Quadrature): hexahedra. The integration domain is the unit hypercube :math:`[-1, 1]^d`, where :math:`d` - is the dimension. The quadrature rules are adapted from:: + is the dimension. The quadrature rules are adapted from: F. D. Witherden, P. E. Vincent, On the Identification of Symmetric Quadrature Rules for Finite Element Methods, Computers & Mathematics with Applications, Vol. 69, pp. 1232--1241, 2015, - `10.1016/j.camwa.2015.03.017 http://dx.doi.org/10.1016/j.camwa.2015.03.017`_. + `DOI `_. - .. versionadded: 2020.5 + .. versionadded: 2020.3 .. automethod:: __init__ .. automethod:: __call__ diff --git a/modepy/shapes.py b/modepy/shapes.py index 85766dd1..ca76d1b7 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -36,6 +36,7 @@ class Shape: """ .. attribute :: dims .. attribute :: nfaces + .. attribute :: nvertices """ dims: int @@ -43,7 +44,7 @@ class Shape: @singledispatch def get_unit_vertices(shape: Shape): """ - :returns: an :class:`~numpy.ndarray` of shape ``(nvertices, dims)``. + :returns: an :class:`~numpy.ndarray` of shape `(nvertices, dims)`. """ raise NotImplementedError(type(shape).__name__) @@ -62,8 +63,8 @@ def get_face_vertex_indices(shape: Shape): def get_face_map(shape: Shape, face_vertices: np.ndarray): """ :returns: a :class:`~collections.abc.Callable` that takes an array of - unit nodes on the face represented by *face_vertices* and maps - them to the volume. + size `(dims, nnodes)` of unit nodes on the face represented by + *face_vertices* and maps them to the volume. """ raise NotImplementedError(type(shape).__name__) @@ -71,7 +72,7 @@ def get_face_map(shape: Shape, face_vertices: np.ndarray): @singledispatch def get_quadrature(shape: Shape, order: int): """ - :returns: a :class:`~modepy.Quadrature` that is exact up to ``2 * order + 1``. + :returns: a :class:`~modepy.Quadrature` that is exact up to :math:`2 N + 1`. """ raise NotImplementedError(type(shape).__name__) @@ -115,6 +116,10 @@ class Simplex(Shape): def nfaces(self): return self.dims + 1 + @property + def nvertices(self): + return self.dim + 1 + @get_unit_vertices.register(Simplex) def _(shape: Simplex): @@ -165,6 +170,10 @@ class Hypercube(Shape): def nfaces(self): return 2 * self.dims + @property + def nvertices(self): + return 2**self.dims + @get_unit_vertices.register(Hypercube) def _(shape: Hypercube): From b90c25c046eefe1dec5351dd0371a06d69133783 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 17 Nov 2020 20:28:53 -0600 Subject: [PATCH 15/68] bump version --- modepy/quadrature/__init__.py | 4 ++++ modepy/version.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index c3ea2af5..4f213846 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -85,6 +85,10 @@ def __init__(self, quad, left, right): class TensorProductQuadrature(Quadrature): + """ + .. automethod:: __init__ + """ + def __init__(self, dims, quad): """ :arg quad: a :class:`Quadrature` class for one-dimensional intervals. diff --git a/modepy/version.py b/modepy/version.py index 82ea6fb1..acb402a2 100644 --- a/modepy/version.py +++ b/modepy/version.py @@ -1,2 +1,2 @@ -VERSION = (2020, 2) +VERSION = (2020, 3) VERSION_TEXT = ".".join(str(i) for i in VERSION) From 0a8972570285902b628dbbe954d6cb042c27d169 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 17 Nov 2020 20:38:52 -0600 Subject: [PATCH 16/68] estimate_lebesgue_constant: raise if dimensions do not match --- modepy/tools.py | 6 +++++- test/test_quadrature.py | 9 +++++---- test/test_tools.py | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/modepy/tools.py b/modepy/tools.py index eedbc3ee..1b2b3d1b 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -536,9 +536,13 @@ def estimate_lebesgue_constant(n, nodes, shape=None, visualize=False): Renamed *domain* to *shape*. """ + dims = len(nodes) if shape is None: from modepy.shapes import Simplex - shape = Simplex(len(nodes)) + shape = Simplex(dims) + else: + if shape.dims != dims: + raise ValueError(f"expected {shape.dims}-dimensional nodes") lebesgue_worst, equi_node_tuples, equi_nodes = \ _evaluate_lebesgue_function(n, nodes, shape) diff --git a/test/test_quadrature.py b/test/test_quadrature.py index 4c9b2f9d..8a3912df 100644 --- a/test/test_quadrature.py +++ b/test/test_quadrature.py @@ -145,11 +145,11 @@ def test_simplex_quadrature(quad_class, highest_order, dim): break -@pytest.mark.parametrize("quad_cls", [ +@pytest.mark.parametrize("quad_class", [ mp.WitherdenVincentQuadrature ]) @pytest.mark.parametrize("dim", [2, 3]) -def test_hypercube_quadrature(quad_cls, dim): +def test_hypercube_quadrature(quad_class, dim): from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam from modepy.tools import Monomial @@ -168,14 +168,15 @@ def _check_monomial(quad, comb): order = 1 while True: try: - quad = quad_cls(order, dim) + quad = quad_class(order, dim) except mp.QuadratureRuleUnavailable: logger.info("UNAVAILABLE at order %d", order) break assert np.all(quad.weights > 0) - logger.info("quadrature: %s %d %d", quad_cls, order, quad.exact_to) + logger.info("quadrature: %s %d %d", + quad_class.__name__.lower(), order, quad.exact_to) for comb in gnitstam(quad.exact_to, dim): assert _check_monomial(quad, comb) < 5.0e-15 diff --git a/test/test_tools.py b/test/test_tools.py index 20827b55..fb096474 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -336,8 +336,8 @@ def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): else: raise ValueError(f"unsupported dimension: {dims}") - domain = type(shape).__name__.lower() - fig.savefig(f"estimate_lebesgue_constant_{domain}_{dims}_order_{order}") + shape_name = shape_cls.__name__.lower() + fig.savefig(f"estimate_lebesgue_constant_{shape_name}_{dims}_order_{order}") # }}} From 00488bddb8cbf8bc0806c2d371126526d66ac2f5 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 25 Nov 2020 17:16:12 -0600 Subject: [PATCH 17/68] Move shape-basd functions to bottom in modes/nodes --- modepy/modes.py | 106 ++++++++++++++++++++-------------------- modepy/nodes.py | 126 ++++++++++++++++++++++++------------------------ 2 files changed, 116 insertions(+), 116 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index b842ef3b..f24ce4cb 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -93,59 +93,6 @@ """ -# {{{ shape basis functions - -# {{{ simplex - -@get_basis.register(Simplex) -def _(shape: Simplex, order: int): - if shape.dims <= 3: - return simplex_onb(shape.dims, order) - else: - return simplex_monomial_basis(shape.dims, order) - - -@get_grad_basis.register(Simplex) -def _(shape: Simplex, order: int): - if shape.dims <= 3: - return grad_simplex_onb(shape.dims, order) - else: - return grad_simplex_monomial_basis(shape.dims, order) - - -@get_basis_with_mode_ids.register(Simplex) -def _(shape: Simplex, order: int): - if shape.dims <= 3: - return simplex_onb_with_mode_ids(shape.dims, order) - else: - return simplex_monomial_basis_with_mode_ids(shape.dims, order) - -# }}} - - -# {{{ hypercube - -@get_basis.register(Hypercube) -def _(shape: Hypercube, order: int): - return legendre_tensor_product_basis(shape.dims, order) - - -@get_grad_basis.register(Hypercube) -def _(shape: Hypercube, order: int): - return grad_legendre_tensor_product_basis(shape.dims, order) - - -@get_basis_with_mode_ids.register(Hypercube) -def _(shape: Hypercube, order: int): - from modepy.shapes import get_node_tuples - mode_ids = get_node_tuples(shape, order) - return mode_ids, get_basis(shape, order) - -# }}} - -# }}} - - # {{{ helpers for symbolic evaluation def _cse(expr, prefix): @@ -790,4 +737,57 @@ def symbolicize_basis(basis, dims, ref_coord_var_name="r"): # }}} + +# {{{ shape basis functions + +# {{{ simplex + +@get_basis.register(Simplex) +def _(shape: Simplex, order: int): + if shape.dims <= 3: + return simplex_onb(shape.dims, order) + else: + return simplex_monomial_basis(shape.dims, order) + + +@get_grad_basis.register(Simplex) +def _(shape: Simplex, order: int): + if shape.dims <= 3: + return grad_simplex_onb(shape.dims, order) + else: + return grad_simplex_monomial_basis(shape.dims, order) + + +@get_basis_with_mode_ids.register(Simplex) +def _(shape: Simplex, order: int): + if shape.dims <= 3: + return simplex_onb_with_mode_ids(shape.dims, order) + else: + return simplex_monomial_basis_with_mode_ids(shape.dims, order) + +# }}} + + +# {{{ hypercube + +@get_basis.register(Hypercube) +def _(shape: Hypercube, order: int): + return legendre_tensor_product_basis(shape.dims, order) + + +@get_grad_basis.register(Hypercube) +def _(shape: Hypercube, order: int): + return grad_legendre_tensor_product_basis(shape.dims, order) + + +@get_basis_with_mode_ids.register(Hypercube) +def _(shape: Hypercube, order: int): + from modepy.shapes import get_node_tuples + mode_ids = get_node_tuples(shape, order) + return mode_ids, get_basis(shape, order) + +# }}} + +# }}} + # vim: foldmethod=marker diff --git a/modepy/nodes.py b/modepy/nodes.py index ba94232a..e9cb216c 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -28,69 +28,6 @@ from modepy.shapes import get_node_count, get_node_tuples, get_unit_nodes -# {{{ shape nodes - -# {{{ simplex - -@get_node_count.register(Simplex) -def _(shape: Simplex, order: int): - try: - from math import comb # comb is v3.8+ - node_count = comb(order + shape.dims, shape.dims) - except ImportError: - from functools import reduce - from operator import mul - node_count = reduce(mul, range(order + 1, order + shape.dims + 1), 1) \ - // reduce(mul, range(1, shape.dims + 1), 1) - - return node_count - - -@get_node_tuples.register(Simplex) -def _(shape: Simplex, order: int): - from pytools import \ - generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam - if shape.dims == 0: - return ((0,),) - else: - return tuple(gnitsam(order, shape.dims)) - - -@get_unit_nodes.register(Simplex) -def _(shape: Simplex, order: int): - import modepy as mp - return mp.warp_and_blend_nodes(shape.dims, order) - -# }}} - - -# {{{ hypercube - -@get_node_count.register(Hypercube) -def _(shape: Hypercube, order: int): - return (order + 1)**shape.dims - - -@get_node_tuples.register(Hypercube) -def _(shape: Hypercube, order: int): - from pytools import \ - generate_nonnegative_integer_tuples_below as gnitb - if shape.dims == 0: - return ((0,),) - else: - return tuple(gnitb(order, shape.dims)) - - -@get_unit_nodes.register(Hypercube) -def _(shape: Hypercube, order: int): - import modepy as mp - return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dims, order) - -# }}} - -# }}} - - # {{{ equidistant nodes def equidistant_nodes(dims, n, node_tuples=None): @@ -382,4 +319,67 @@ def legendre_gauss_lobatto_tensor_product_nodes(dims, n): # }}} + +# {{{ shape nodes + +# {{{ simplex + +@get_node_count.register(Simplex) +def _(shape: Simplex, order: int): + try: + from math import comb # comb is v3.8+ + node_count = comb(order + shape.dims, shape.dims) + except ImportError: + from functools import reduce + from operator import mul + node_count = reduce(mul, range(order + 1, order + shape.dims + 1), 1) \ + // reduce(mul, range(1, shape.dims + 1), 1) + + return node_count + + +@get_node_tuples.register(Simplex) +def _(shape: Simplex, order: int): + from pytools import \ + generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam + if shape.dims == 0: + return ((0,),) + else: + return tuple(gnitsam(order, shape.dims)) + + +@get_unit_nodes.register(Simplex) +def _(shape: Simplex, order: int): + import modepy as mp + return mp.warp_and_blend_nodes(shape.dims, order) + +# }}} + + +# {{{ hypercube + +@get_node_count.register(Hypercube) +def _(shape: Hypercube, order: int): + return (order + 1)**shape.dims + + +@get_node_tuples.register(Hypercube) +def _(shape: Hypercube, order: int): + from pytools import \ + generate_nonnegative_integer_tuples_below as gnitb + if shape.dims == 0: + return ((0,),) + else: + return tuple(gnitb(order, shape.dims)) + + +@get_unit_nodes.register(Hypercube) +def _(shape: Hypercube, order: int): + import modepy as mp + return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dims, order) + +# }}} + +# }}} + # vim: foldmethod=marker From f227cac6e0e0f088bf969a745ca3157efcc6d694 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 25 Nov 2020 17:57:07 -0600 Subject: [PATCH 18/68] Move shapes docstring into shapes module --- doc/shapes.rst | 163 ---------------------------------------------- modepy/shapes.py | 165 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 165 insertions(+), 163 deletions(-) diff --git a/doc/shapes.rst b/doc/shapes.rst index ffc1f7c0..453642e5 100644 --- a/doc/shapes.rst +++ b/doc/shapes.rst @@ -2,166 +2,3 @@ Shapes ====== .. automodule:: modepy.shapes -.. currentmodule:: modepy.shapes - -:mod:`modepy.shapes` provides a generic description of the supported shapes -(i.e. reference elements). - -.. autoclass:: Shape -.. autofunction:: get_unit_vertices -.. autofunction:: get_face_vertex_indices -.. autofunction:: get_face_map - -Simplices -^^^^^^^^^ - -.. autoclass:: Simplex - -.. _tri-coords: - -Coordinates on the triangle ---------------------------- - -Unit coordinates :math:`(r, s)`:: - - ^ s - | - C - |\ - | \ - | O - | \ - | \ - A-----B--> r - -Vertices in unit coordinates:: - - O = ( 0, 0) - A = (-1, -1) - B = ( 1, -1) - C = (-1, 1) - -Equilateral coordinates :math:`(x, y)`:: - - C - / \ - / \ - / \ - / O \ - / \ - A-----------B - -Vertices in equilateral coordinates:: - - O = ( 0, 0) - A = (-1, -1/sqrt(3)) - B = ( 1, -1/sqrt(3)) - C = ( 0, 2/sqrt(3)) - -.. _tet-coords: - -Coordinates on the tetrahedron ------------------------------- - -Unit coordinates :math:`(r, s, t)`:: - - ^ s - | - C - /|\ - / | \ - / | \ - / | \ - / O| \ - / __A-----B---> r - /_--^ ___--^^ - ,D--^^^ - t L - -(squint, and it might start making sense...) - -Vertices in unit coordinates :math:`(r, s, t)`:: - - O = ( 0, 0, 0) - A = (-1, -1, -1) - B = ( 1, -1, -1) - C = (-1, 1, -1) - D = (-1, -1, 1) - -Vertices in equilateral coordinates :math:`(x, y, z)`:: - - O = ( 0, 0, 0) - A = (-1, -1/sqrt(3), -1/sqrt(6)) - B = ( 1, -1/sqrt(3), -1/sqrt(6)) - C = ( 0, 2/sqrt(3), -1/sqrt(6)) - D = ( 0, 0, 3/sqrt(6)) - -Hypercubes -^^^^^^^^^^ - -.. autoclass:: Hypercube - -.. _square-coords: - -Coordinates on the square -------------------------- - -Unit coordinates on :math:`(r, s)`:: - - ^ s - | - C---------D - | | - | | - | O | - | | - | | - A---------B --> r - - -Vertices in unit coordinates:: - - O = ( 0, 0) - A = (-1, -1) - B = ( 1, -1) - C = (-1, 1) - D = ( 1, 1) - -.. _cube-coords: - -Coordinates on the cube ------------------------ - -Unit coordinates on :math:`(r, s, t)`:: - - t - ^ - | - B----------D - |\ |\ - | \ | \ - | \ | \ - | F------+---H - | | O | | - A---+------C---|--> s - \ | \ | - \ | \ | - \| \| - E----------G - \ - v r - -Verties in unit coordinates:: - - O = ( 0, 0, 0) - A = (-1, -1, -1) - B = (-1, -1, 1) - C = (-1, 1, -1) - D = (-1, 1, 1) - E = ( 1, -1, -1) - F = ( 1, -1, 1) - G = ( 1, 1, -1) - H = ( 1, 1, 1) - -The order of the vertices in the hypercubes follows binary counting -in ``rst``. For example, in 3D, ``A, B, C, D, ...`` is ``000, 001, 010, 011, ...``. diff --git a/modepy/shapes.py b/modepy/shapes.py index ca76d1b7..9ff9c7be 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -1,3 +1,168 @@ +r""" +:mod:`modepy.shapes` provides a generic description of the supported shapes +(i.e. reference elements). + +.. autoclass:: Shape +.. autofunction:: get_unit_vertices +.. autofunction:: get_face_vertex_indices +.. autofunction:: get_face_map + +Simplices +^^^^^^^^^ + +.. autoclass:: Simplex + +.. _tri-coords: + +Coordinates on the triangle +--------------------------- + +Unit coordinates :math:`(r, s)`:: + + ^ s + | + C + |\ + | \ + | O + | \ + | \ + A-----B--> r + +Vertices in unit coordinates:: + + O = ( 0, 0) + A = (-1, -1) + B = ( 1, -1) + C = (-1, 1) + +Equilateral coordinates :math:`(x, y)`:: + + C + / \ + / \ + / \ + / O \ + / \ + A-----------B + +Vertices in equilateral coordinates:: + + O = ( 0, 0) + A = (-1, -1/sqrt(3)) + B = ( 1, -1/sqrt(3)) + C = ( 0, 2/sqrt(3)) + +.. _tet-coords: + +Coordinates on the tetrahedron +------------------------------ + +Unit coordinates :math:`(r, s, t)`:: + + ^ s + | + C + /|\ + / | \ + / | \ + / | \ + / O| \ + / __A-----B---> r + /_--^ ___--^^ + ,D--^^^ + t L + +(squint, and it might start making sense...) + +Vertices in unit coordinates :math:`(r, s, t)`:: + + O = ( 0, 0, 0) + A = (-1, -1, -1) + B = ( 1, -1, -1) + C = (-1, 1, -1) + D = (-1, -1, 1) + +Vertices in equilateral coordinates :math:`(x, y, z)`:: + + O = ( 0, 0, 0) + A = (-1, -1/sqrt(3), -1/sqrt(6)) + B = ( 1, -1/sqrt(3), -1/sqrt(6)) + C = ( 0, 2/sqrt(3), -1/sqrt(6)) + D = ( 0, 0, 3/sqrt(6)) + +Hypercubes +^^^^^^^^^^ + +.. autoclass:: Hypercube + +.. _square-coords: + +Coordinates on the square +------------------------- + +Unit coordinates on :math:`(r, s)`:: + + ^ s + | + C---------D + | | + | | + | O | + | | + | | + A---------B --> r + + +Vertices in unit coordinates:: + + O = ( 0, 0) + A = (-1, -1) + B = ( 1, -1) + C = (-1, 1) + D = ( 1, 1) + +.. _cube-coords: + +Coordinates on the cube +----------------------- + +Unit coordinates on :math:`(r, s, t)`:: + + t + ^ + | + B----------D + |\ |\ + | \ | \ + | \ | \ + | F------+---H + | | O | | + A---+------C---|--> s + \ | \ | + \ | \ | + \| \| + E----------G + \ + v r + +Verties in unit coordinates:: + + O = ( 0, 0, 0) + A = (-1, -1, -1) + B = (-1, -1, 1) + C = (-1, 1, -1) + D = (-1, 1, 1) + E = ( 1, -1, -1) + F = ( 1, -1, 1) + G = ( 1, 1, -1) + H = ( 1, 1, 1) + +The order of the vertices in the hypercubes follows binary counting +in ``rst``. For example, in 3D, ``A, B, C, D, ...`` is ``000, 001, 010, 011, ...``. +""" + + __copyright__ = """ Copyright (c) 2013 Andreas Kloeckner Copyright (c) 2020 Alexandru Fikl From f8209ce19ec43160b3a2df23d6efae20acb06d40 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 25 Nov 2020 17:57:39 -0600 Subject: [PATCH 19/68] Shapes: Add foldmethod comment --- modepy/shapes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modepy/shapes.py b/modepy/shapes.py index 9ff9c7be..7de126f5 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -389,3 +389,5 @@ def _(shape: Hypercube, order: int): return quad # }}} + +# vim: foldmethod=marker From 7238c6c4baefb6b66be61a408fa0ef16d54d8b84 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 25 Nov 2020 17:58:25 -0600 Subject: [PATCH 20/68] Remove spurious blank line in modes docstring --- modepy/modes.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modepy/modes.py b/modepy/modes.py index f24ce4cb..c0fdf498 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -39,7 +39,6 @@ .. currentmodule:: modepy .. autofunction:: jacobi(alpha, beta, n, x) - .. autofunction:: grad_jacobi(alpha, beta, n, x) Dimension-independent basis getters for simplices From 918342ad89f1cf395199f7cea0226cb8c34aa131 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 25 Nov 2020 17:58:50 -0600 Subject: [PATCH 21/68] Use _cse in PKDO gradients --- modepy/modes.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index c0fdf498..db9a7cbb 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -253,10 +253,10 @@ def grad_pkdo_2d(order, rs): a, b = _rstoab(*rs) i, j = order - fa = jacobi(0, 0, i, a) - dfa = grad_jacobi(0, 0, i, a) - gb = jacobi(2*i+1, 0, j, b) - dgb = grad_jacobi(2*i+1, 0, j, b) + fa = _cse(jacobi(0, 0, i, a), f"leg_{i}") + dfa = _cse(grad_jacobi(0, 0, i, a), "dleg_{i}") + gb = _cse(jacobi(2*i+1, 0, j, b), f"jac_{2*i+1}_{j}") + dgb = _cse(grad_jacobi(2*i+1, 0, j, b), f"djac_{2*i+1}_{j}") # r-derivative # d/dr @@ -343,12 +343,12 @@ def grad_pkdo_3d(order, rst): a, b, c = _rsttoabc(*rst) i, j, k = order - fa = jacobi(0, 0, i, a) - dfa = grad_jacobi(0, 0, i, a) - gb = jacobi(2*i+1, 0, j, b) - dgb = grad_jacobi(2*i+1, 0, j, b) - hc = jacobi(2*(i+j)+2, 0, k, c) - dhc = grad_jacobi(2*(i+j)+2, 0, k, c) + fa = _cse(jacobi(0, 0, i, a), f"leg_{i}") + dfa = _cse(grad_jacobi(0, 0, i, a), f"dleg_{i}") + gb = _cse(jacobi(2*i+1, 0, j, b), f"jac_{2*i+1}") + dgb = _cse(grad_jacobi(2*i+1, 0, j, b), f"djac_{2*i+1}") + hc = _cse(jacobi(2*(i+j)+2, 0, k, c), f"jac_{2*(i+j)+2}") + dhc = _cse(grad_jacobi(2*(i+j)+2, 0, k, c), f"djac_{2*(i+j)+2}") # r-derivative # d/dr = da/dr d/da + db/dr d/db + dc/dr d/dx From 59198d84bc69305d486c5456b80b793124929fd8 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 26 Nov 2020 00:34:20 -0600 Subject: [PATCH 22/68] Deprecate current dimension-independent basis getters --- modepy/modes.py | 92 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index 8e7c7240..f90000ff 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -41,8 +41,10 @@ .. autofunction:: jacobi(alpha, beta, n, x) .. autofunction:: grad_jacobi(alpha, beta, n, x) -Dimension-independent basis getters for simplices -------------------------------------------------- +PKDO basis functions +-------------------- + +.. currentmodule:: modepy.modes .. |proriol-ref| replace:: Proriol, Joseph. "Sur une famille de polynomes á deux variables orthogonaux @@ -57,23 +59,6 @@ Scientific Computing 6, no. 4 (December 1, 1991): 345–390. http://dx.doi.org/10.1007/BF01060030 -.. autofunction:: simplex_onb_with_mode_ids -.. autofunction:: simplex_onb -.. autofunction:: grad_simplex_onb -.. autofunction:: simplex_monomial_basis_with_mode_ids -.. autofunction:: simplex_monomial_basis -.. autofunction:: grad_simplex_monomial_basis - -Dimension-independent basis getters for tensor-product bases ------------------------------------------------------------- - -.. autofunction:: tensor_product_basis -.. autofunction:: grad_tensor_product_basis - -Dimension-specific functions ----------------------------- - -.. currentmodule:: modepy.modes .. autofunction:: pkdo_2d .. autofunction:: grad_pkdo_2d @@ -442,7 +427,7 @@ def diff_monomial(r, o): # }}} -# {{{ dimension-independent interface for simplices +# {{{ DEPRECATED dimension-independent interface for simplices def zerod_basis(x): if len(x.shape) == 1: @@ -471,6 +456,11 @@ def simplex_onb_with_mode_ids(dims, n): .. versionadded:: 2018.1 """ + warn("simplex_onb_with_mode_ids is deprecated. " + "Use orthonormal_basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + from modepy.shapes import get_node_tuples shape = Simplex(dims) @@ -512,6 +502,11 @@ def simplex_onb(dims, n): Made return value a tuple, to make bases hashable. """ + warn("simplex_onb is deprecated. " + "Use orthonormal_basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + mode_ids, basis = simplex_onb_with_mode_ids(dims, n) return basis @@ -536,6 +531,11 @@ def grad_simplex_onb(dims, n): Made return value a tuple, to make bases hashable. """ + warn("grad_simplex_onb is deprecated. " + "Use orthonormal_basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + from functools import partial from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ as gnitstam @@ -564,6 +564,11 @@ def simplex_monomial_basis_with_mode_ids(dims, n): .. versionadded:: 2018.1 """ + warn("simplex_monomial_basis_with_mode_ids is deprecated. " + "Use monomial_basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + from modepy.shapes import get_node_tuples mode_ids = get_node_tuples(Simplex(dims), n) @@ -601,25 +606,38 @@ def grad_simplex_monomial_basis(dims, n): .. versionadded:: 2016.1 """ + warn("grad_simplex_monomial_basis_with_mode_ids is deprecated. " + "Use monomial_basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + from functools import partial from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ as gnitstam return tuple(partial(grad_monomial, order) for order in gnitstam(n, dims)) -# undocumented for now def simplex_best_available_basis(dims, n): - return get_basis(Simplex(dims), n) + warn("simplex_best_available_basis is deprecated. " + "Use basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + + return basis_for_shape(Simplex(dims), n).functions -# undocumented for now def grad_simplex_best_available_basis(dims, n): - return get_grad_basis(Simplex(dims), n) + warn("grad_simplex_best_available_basis is deprecated. " + "Use basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + + return basis_for_shape(Simplex(dims), n).gradients # }}} -# {{{ tensor product basis +# {{{ tensor product basis helpers class _TensorProductBasisFunction: def __init__(self, multi_index, per_dim_functions): @@ -647,6 +665,10 @@ def __call__(self, x): return tuple(result) +# }}} + + +# {{{ DEPRECATED dimension-independent basis getters def tensor_product_basis(dims, basis_1d): """Adapt any iterable *basis_1d* of 1D basis functions into a *dims*-dimensional @@ -656,6 +678,11 @@ def tensor_product_basis(dims, basis_1d): .. versionadded:: 2017.1 """ + warn("tensor_product_basis is deprecated. " + "Use TensorProductBasis instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + from modepy.shapes import Hypercube, get_node_tuples mode_ids = get_node_tuples(Hypercube(dims), len(basis_1d)) @@ -673,6 +700,11 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): .. versionadded:: 2020.2 """ + warn("grad_tensor_product_basis is deprecated. " + "Use TensorProductBasis instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + from pytools import wandering_element from modepy.shapes import Hypercube, get_node_tuples mode_ids = get_node_tuples(Hypercube(dims), len(basis_1d)) @@ -687,12 +719,22 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): def legendre_tensor_product_basis(dims, order): + warn("legendre_tensor_product_basis is deprecated. " + "Use orthonormal_basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + from functools import partial basis = [partial(jacobi, 0, 0, n) for n in range(order + 1)] return tensor_product_basis(dims, basis) def grad_legendre_tensor_product_basis(dims, order): + warn("grad_legendre_tensor_product_basis is deprecated. " + "Use orthonormal_basis_for_shape instead. " + "This function will go away in 2022.", + DeprecationWarning, stacklevel=2) + from functools import partial basis = [partial(jacobi, 0, 0, n) for n in range(order + 1)] grad_basis = [partial(grad_jacobi, 0, 0, n) for n in range(order + 1)] From cf11d52f3bc672c201cca8479d306bee75e4748f Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 15:31:42 -0600 Subject: [PATCH 23/68] resampling_matrix: Impove count mismatch error message --- modepy/matrices.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modepy/matrices.py b/modepy/matrices.py index 91a40b8d..4161ccf7 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -148,8 +148,9 @@ def is_square(m): np.dot(resample_vdm_new, la.pinv(vdm_old)), order="C") else: - raise RuntimeError("number of input nodes and number " - "of basis functions " + raise RuntimeError( + f"number of input nodes ({old_nodes.shape[1]}) " + f"and number of basis functions ({len(basis)}) " "do not agree--perhaps use least_squares_ok") From cd750b8a24eedcdaafa28dc46bb8d1c16d5cda42 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 15:34:24 -0600 Subject: [PATCH 24/68] Refactor towards improved shape-based interface --- doc/nodes.rst | 37 +---- doc/quadrature.rst | 4 - doc/tools.rst | 7 +- modepy/__init__.py | 30 +++- modepy/matrices.py | 42 +++-- modepy/modes.py | 294 +++++++++++++++++++++++++++------- modepy/nodes.py | 142 ++++++++++++---- modepy/quadrature/__init__.py | 47 ++++++ modepy/shapes.py | 135 +++++----------- modepy/tools.py | 155 +++++++++++------- test/test_modes.py | 135 ++++++++-------- test/test_nodes.py | 27 ++-- test/test_tools.py | 108 +++++++------ 13 files changed, 716 insertions(+), 447 deletions(-) diff --git a/doc/nodes.rst b/doc/nodes.rst index daf21889..c6d60997 100644 --- a/doc/nodes.rst +++ b/doc/nodes.rst @@ -1,39 +1,4 @@ Interpolation Nodes =================== -Simplices -^^^^^^^^^ - -Transformations between coordinate systems ------------------------------------------- - -.. currentmodule:: modepy.tools - -All of these expect and return arrays of shape *(dims, npts)*. - -.. autofunction:: equilateral_to_unit -.. autofunction:: barycentric_to_unit -.. autofunction:: unit_to_barycentric -.. autofunction:: barycentric_to_equilateral - -Node sets for interpolation ---------------------------- - -.. currentmodule:: modepy - -.. autofunction:: equidistant_nodes -.. autofunction:: warp_and_blend_nodes - -Also see :class:`modepy.VioreanuRokhlinSimplexQuadrature` if nodes on the -boundary are not required. - -Hypercubes -^^^^^^^^^^ - -Node sets for interpolation ---------------------------- - -.. currentmodule:: modepy - -.. autofunction:: tensor_product_nodes -.. autofunction:: legendre_gauss_lobatto_tensor_product_nodes +.. automodule:: modepy.nodes diff --git a/doc/quadrature.rst b/doc/quadrature.rst index 6d9045f2..dfc8eea1 100644 --- a/doc/quadrature.rst +++ b/doc/quadrature.rst @@ -6,10 +6,6 @@ Base classes .. automodule:: modepy.quadrature -.. currentmodule:: modepy - -.. autoclass:: Quadrature - Jacobi-Gauss quadrature in one dimension ---------------------------------------- diff --git a/doc/tools.rst b/doc/tools.rst index b5b00069..85d1a5c1 100644 --- a/doc/tools.rst +++ b/doc/tools.rst @@ -24,11 +24,6 @@ Modal decay/residual .. automodule:: modepy.modal_decay -Interpolation quality ---------------------- - -.. currentmodule:: modepy.tools - -.. autofunction:: estimate_lebesgue_constant +.. automodule:: modepy.tools .. vim: sw=4 diff --git a/modepy/__init__.py b/modepy/__init__.py index b951e888..a94e9f81 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -22,6 +22,11 @@ """ +from modepy.shapes import ( + Shape, Simplex, Hypercube, + + biunit_vertices_for_shape + ) from modepy.modes import ( jacobi, grad_jacobi, simplex_onb, grad_simplex_onb, simplex_onb_with_mode_ids, @@ -29,10 +34,15 @@ simplex_monomial_basis_with_mode_ids, simplex_best_available_basis, grad_simplex_best_available_basis, tensor_product_basis, grad_tensor_product_basis, - legendre_tensor_product_basis, grad_legendre_tensor_product_basis) + legendre_tensor_product_basis, grad_legendre_tensor_product_basis, + + basis_for_shape, orthonormal_basis_for_shape, monomial_basis_for_shape) from modepy.nodes import ( equidistant_nodes, warp_and_blend_nodes, - tensor_product_nodes, legendre_gauss_lobatto_tensor_product_nodes) + tensor_product_nodes, legendre_gauss_lobatto_tensor_product_nodes, + + node_count_for_shape, node_tuples_for_shape, edge_clustered_nodes_for_shape, + random_nodes_for_shape) from modepy.matrices import (vandermonde, resampling_matrix, differentiation_matrices, diff_matrix_permutation, @@ -40,10 +50,12 @@ modal_face_mass_matrix, nodal_face_mass_matrix) from modepy.quadrature import ( Quadrature, QuadratureRuleUnavailable, - TensorProductQuadrature, LegendreGaussTensorProductQuadrature) + TensorProductQuadrature, LegendreGaussTensorProductQuadrature, + quadrature_for_shape) from modepy.quadrature.jacobi_gauss import ( JacobiGaussQuadrature, LegendreGaussQuadrature, ChebyshevGaussQuadrature, - GaussGegenbauerQuadrature) + GaussGegenbauerQuadrature, + ) from modepy.quadrature.xiao_gimbutas import XiaoGimbutasSimplexQuadrature from modepy.quadrature.vioreanu_rokhlin import VioreanuRokhlinSimplexQuadrature from modepy.quadrature.grundmann_moeller import GrundmannMoellerSimplexQuadrature @@ -58,6 +70,9 @@ __all__ = [ "__version__", + "Shape", "Simplex", "Hypercube", + "biunit_vertices_for_shape", + "jacobi", "grad_jacobi", "simplex_onb", "grad_simplex_onb", "simplex_onb_with_mode_ids", "simplex_monomial_basis", "grad_simplex_monomial_basis", @@ -65,9 +80,12 @@ "simplex_best_available_basis", "grad_simplex_best_available_basis", "tensor_product_basis", "grad_tensor_product_basis", "legendre_tensor_product_basis", "grad_legendre_tensor_product_basis", + "basis_for_shape", "orthonormal_basis_for_shape", "monomial_basis_for_shape", "equidistant_nodes", "warp_and_blend_nodes", "tensor_product_nodes", "legendre_gauss_lobatto_tensor_product_nodes", + "node_count_for_shape", "node_tuples_for_shape", + "edge_clustered_nodes_for_shape", "random_nodes_for_shape", "vandermonde", "resampling_matrix", "differentiation_matrices", "diff_matrix_permutation", @@ -75,6 +93,9 @@ "nodal_face_mass_matrix", "Quadrature", "QuadratureRuleUnavailable", + "TensorProductQuadrature", "LegendreGaussTensorProductQuadrature", + "quadrature_for_shape", + "JacobiGaussQuadrature", "LegendreGaussQuadrature", "GaussLegendreQuadrature", "ChebyshevGaussQuadrature", "GaussGegenbauerQuadrature", @@ -83,7 +104,6 @@ "ClenshawCurtisQuadrature", "FejerQuadrature", "WitherdenVincentQuadrature", - "TensorProductQuadrature", "LegendreGaussTensorProductQuadrature", ] from pytools import MovedFunctionDeprecationWrapper diff --git a/modepy/matrices.py b/modepy/matrices.py index 4161ccf7..fdd56be0 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -244,30 +244,36 @@ def mass_matrix(basis, nodes): def modal_face_mass_matrix(trial_basis, order, face_vertices, - test_basis=None, shape=None): + test_basis=None, volume_shape=None): """ :arg face_vertices: an array of shape ``(dims, nvertices)``. :arg shape: a :class:`~modepy.shapes.Shape` that identifies the - reference face element. + reference volume element. .. versionadded :: 2016.1 .. versionchanged:: 2020.3 - Added *shape* parameter and support for :math:`[-1, 1]^d` domains. + Added *volume_shape* parameter and support for :math:`[-1, 1]^d` domains. """ if test_basis is None: test_basis = trial_basis - if shape is None: + if volume_shape is None: + from warnings import warn + warn("Not passing volume_shape is deprecated and will stop working " + "in 2022.", DeprecationWarning, stacklevel=2) + from modepy.shapes import Simplex - shape = Simplex(face_vertices.shape[0]) + volume_shape = Simplex(face_vertices.shape[0]) - from modepy.shapes import get_face_map, get_quadrature - face = type(shape)(shape.dims - 1) - fmap = get_face_map(shape, face_vertices) - quad = get_quadrature(face, order) + from modepy.shapes import face_map_for_shape + from modepy.quadrature import quadrature_for_shape + # FIXME NOPE + face_shape = type(volume_shape)(volume_shape.dim - 1) + fmap = face_map_for_shape(volume_shape, face_vertices) + quad = quadrature_for_shape(face_shape, order) assert quad.exact_to > order*2 mapped_nodes = fmap(quad.nodes) @@ -287,7 +293,7 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, - face_vertices, test_basis=None, shape=None): + face_vertices, test_basis=None, volume_shape=None): """ :arg face_vertices: an array of shape ``(dims, nvertices)``. :arg shape: a :class:`~modepy.shapes.Shape` that identifies the @@ -297,24 +303,28 @@ def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, .. versionchanged:: 2020.3 - Added *shape* parameter and support for :math:`[-1, 1]^d` domains. + Added *volume_shape* parameter and support for :math:`[-1, 1]^d` domains. """ if test_basis is None: test_basis = trial_basis - if shape is None: + if volume_shape is None: + from warnings import warn + warn("Not passing volume_shape is deprecated and will stop working " + "in 2022.", DeprecationWarning, stacklevel=2) + from modepy.shapes import Simplex - shape = Simplex(face_vertices.shape[0]) + volume_shape = Simplex(face_vertices.shape[0]) - from modepy.shapes import get_face_map - fmap = get_face_map(shape, face_vertices) + from modepy.shapes import face_map_for_shape + fmap = face_map_for_shape(volume_shape, face_vertices) face_vdm = vandermonde(trial_basis, fmap(face_nodes)) # /!\ non-square vol_vdm = vandermonde(test_basis, volume_nodes) modal_fmm = modal_face_mass_matrix( trial_basis, order, face_vertices, - test_basis=test_basis, shape=shape) + test_basis=test_basis, volume_shape=volume_shape) return la.inv(vol_vdm.T).dot(modal_fmm).dot(la.pinv(face_vdm)) diff --git a/modepy/modes.py b/modepy/modes.py index f90000ff..1fb9e3d3 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -22,16 +22,30 @@ """ +from warnings import warn import sys from math import sqrt +from functools import singledispatch, partial + import numpy as np -from modepy.shapes import Simplex, Hypercube -from modepy.shapes import get_basis, get_grad_basis, get_basis_with_mode_ids +from modepy.shapes import Shape, Simplex, Hypercube + + +__doc__ = """This functionality provides sets of basis functions for the +reference elements in :mod:`modepy.shapes`. + +.. currentmodule:: modepy +Generic Basis Retrieval based on :mod:`~modepy.shapes` +------------------------------------------------------ -__doc__ = """:mod:`modepy.modes` provides orthonormal bases and their -derivatives on unit simplices. +.. autoexception:: BasisNotOrthonormal +.. autoclass:: Basis + +.. autofunction:: basis_for_shape +.. autofunction:: orthonormal_basis_for_shape +.. autofunction:: monomial_basis_for_shape Jacobi polynomials ------------------ @@ -430,10 +444,7 @@ def diff_monomial(r, o): # {{{ DEPRECATED dimension-independent interface for simplices def zerod_basis(x): - if len(x.shape) == 1: - return 1 - else: - return np.ones(x.shape[1]) + return 1 + 0*x[()] def simplex_onb_with_mode_ids(dims, n): @@ -461,25 +472,13 @@ def simplex_onb_with_mode_ids(dims, n): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from modepy.shapes import get_node_tuples - shape = Simplex(dims) - - from functools import partial - if dims == 0: - mode_ids = get_node_tuples(shape, n) - return mode_ids, (zerod_basis,) - elif dims == 1: + if dims == 1: # FIXME: should also use get_node_tuples mode_ids = tuple(range(n+1)) return mode_ids, tuple(partial(jacobi, 0, 0, i) for i in mode_ids) - elif dims == 2: - mode_ids = get_node_tuples(shape, n) - return mode_ids, tuple(partial(pkdo_2d, order) for order in mode_ids) - elif dims == 3: - mode_ids = get_node_tuples(shape, n) - return mode_ids, tuple(partial(pkdo_3d, order) for order in mode_ids) else: - raise NotImplementedError("%d-dimensional bases" % dims) + b = _SimplexONB(dims, n) + return b.mode_ids, b.functions def simplex_onb(dims, n): @@ -536,7 +535,6 @@ def grad_simplex_onb(dims, n): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from functools import partial from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ as gnitstam @@ -572,7 +570,6 @@ def simplex_monomial_basis_with_mode_ids(dims, n): from modepy.shapes import get_node_tuples mode_ids = get_node_tuples(Simplex(dims), n) - from functools import partial return mode_ids, tuple(partial(monomial, order) for order in mode_ids) @@ -611,7 +608,6 @@ def grad_simplex_monomial_basis(dims, n): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from functools import partial from pytools import generate_nonnegative_integer_tuples_summing_to_at_most \ as gnitstam return tuple(partial(grad_monomial, order) for order in gnitstam(n, dims)) @@ -640,6 +636,8 @@ def grad_simplex_best_available_basis(dims, n): # {{{ tensor product basis helpers class _TensorProductBasisFunction: + # multi_index is here just for debugging. + def __init__(self, multi_index, per_dim_functions): self.multi_index = multi_index self.per_dim_functions = per_dim_functions @@ -653,6 +651,8 @@ def __call__(self, x): class _TensorProductGradientBasisFunction: + # multi_index is here just for debugging. + def __init__(self, multi_index, per_dim_derivatives): self.multi_index = multi_index self.per_dim_derivatives = tuple(per_dim_derivatives) @@ -724,7 +724,6 @@ def legendre_tensor_product_basis(dims, order): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from functools import partial basis = [partial(jacobi, 0, 0, n) for n in range(order + 1)] return tensor_product_basis(dims, basis) @@ -735,7 +734,6 @@ def grad_legendre_tensor_product_basis(dims, order): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from functools import partial basis = [partial(jacobi, 0, 0, n) for n in range(order + 1)] grad_basis = [partial(grad_jacobi, 0, 0, n) for n in range(order + 1)] return grad_tensor_product_basis(dims, basis, grad_basis) @@ -745,22 +743,22 @@ def grad_legendre_tensor_product_basis(dims, order): # {{{ conversion to symbolic -def symbolicize_function(f, dims, ref_coord_var_name="r"): +def symbolicize_function(f, dim, ref_coord_var_name="r"): """For a function *f* (basis or gradient) returned by one of the functions in this module, return a :mod:`pymbolic` expression representing the same function. - :arg dims: the number of dimensions of the reference element on which + :arg dim: the number of dimensions of the reference element on which *basis* is defined. .. versionadded:: 2020.2 """ import pymbolic.primitives as p - r_sym = p.make_sym_vector(ref_coord_var_name, dims) + r_sym = p.make_sym_vector(ref_coord_var_name, dim) result = f(r_sym) - if dims == 1: + if dim == 1: # Work around inconsistent 1D stupidity. Grrrr! # (We fed it an object array, and it gave one back, i.e. it treated its # argument as a scalar instead of indexing into it. That tends to @@ -778,55 +776,233 @@ def symbolicize_function(f, dims, ref_coord_var_name="r"): # }}} -# {{{ shape basis functions +# {{{ basis interface + +class BasisNotOrthonormal(Exception): + pass + + +class Basis: + """ + .. automethod:: orthonormality_weight(r) + .. autoattribute:: mode_ids + .. autoattribute:: functions + .. autoattribute:: gradients + """ + + def orthonormality_weight(self, rvec): + raise NotImplementedError + + @property + def mode_ids(self): + """Return a tuple of of mode (basis function) identifiers, one for + each basis function. Mode identifiers should generally be viewed as opaque. + They are hashable. For some bases, they are tuples of length matching + the number of dimensions and indicating the order along each reference + axis. + """ + raise NotImplementedError + + @property + def functions(self): + """Return a tuple of (callable) basis functions of length matching + :attr:`mode_ids`. Each function takes a vector of (r,s,t) reference + coordinates as input. + """ + raise NotImplementedError + + @property + def gradients(self): + """Return a tuple of basis functions of length matching :attr:`mode_ids`. + Each function takes a vector of (r,s,t) reference coordinates as input. + """ + raise NotImplementedError + +# }}} + + +# {{{ shape-based basis retrieval + +@singledispatch +def basis_for_shape(shape: Shape, order: int) -> Basis: + raise NotImplementedError(type(shape).__name__) + + +@singledispatch +def orthonormal_basis_for_shape(shape: Shape, order: int) -> Basis: + raise NotImplementedError(type(shape).__name__) + + +@singledispatch +def monomial_basis_for_shape(shape: Shape, order: int) -> Basis: + raise NotImplementedError(type(shape).__name__) + +# }}} + + +# {{{ shape: simplex + +def _pkdo_1d(order, r): + i, = order + r0, = r + return jacobi(0, 0, i, r0) + + +def _grad_pkdo_1d(order, r): + i, = order + r0, = r + return (grad_jacobi(0, 0, i, r0),) + + +class _SimplexBasis(Basis): + def __init__(self, dim, order): + self._dim = dim + self._order = order + + @property + def mode_ids(self): + from pytools import \ + generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam + return tuple(gnitsam(self._order, self._dim)) + + +class _SimplexONB(_SimplexBasis): + is_orthonormal = True + + def orthonormality_weight(self, rvec): + return 1 + + @property + def functions(self): + if self._dim == 0: + return (zerod_basis,) + elif self._dim == 1: + return tuple(partial(_pkdo_1d, mid) for mid in self.mode_ids) + elif self._dim == 2: + return tuple(partial(pkdo_2d, mid) for mid in self.mode_ids) + elif self._dim == 3: + return tuple(partial(pkdo_3d, mid) for mid in self.mode_ids) + else: + raise NotImplementedError("basis in {self._dim} dimensions") + + @property + def gradients(self): + if self._dim == 1: + return tuple(partial(_grad_pkdo_1d, mid) for mid in self.mode_ids) + elif self._dim == 2: + return tuple(partial(grad_pkdo_2d, mid) for mid in self.mode_ids) + elif self._dim == 3: + return tuple(partial(grad_pkdo_3d, mid) for mid in self.mode_ids) + else: + raise NotImplementedError("gradient in {self._dim} dimensions") + -# {{{ simplex +class _SimplexMonomialBasis(_SimplexBasis): + def orthonormality_weight(self, rvec): + raise BasisNotOrthonormal -@get_basis.register(Simplex) + @property + def functions(self): + return tuple(partial(monomial, mid) for mid in self.mode_ids) + + @property + def gradients(self): + return tuple(partial(grad_monomial, mid) for mid in self.mode_ids) + + +@basis_for_shape.register def _(shape: Simplex, order: int): - if shape.dims <= 3: - return simplex_onb(shape.dims, order) + if shape.dim <= 3: + return _SimplexONB(shape.dim, order) else: - return simplex_monomial_basis(shape.dims, order) + return _SimplexMonomialBasis(shape.dim, order) -@get_grad_basis.register(Simplex) +@orthonormal_basis_for_shape.register def _(shape: Simplex, order: int): - if shape.dims <= 3: - return grad_simplex_onb(shape.dims, order) - else: - return grad_simplex_monomial_basis(shape.dims, order) + return _SimplexONB(shape.dim, order) -@get_basis_with_mode_ids.register(Simplex) +@monomial_basis_for_shape.register def _(shape: Simplex, order: int): - if shape.dims <= 3: - return simplex_onb_with_mode_ids(shape.dims, order) - else: - return simplex_monomial_basis_with_mode_ids(shape.dims, order) + return _SimplexMonomialBasis(shape.dim, order) # }}} -# {{{ hypercube +# {{{ shape: hypercube -@get_basis.register(Hypercube) -def _(shape: Hypercube, order: int): - return legendre_tensor_product_basis(shape.dims, order) +class _TensorProductBasis(Basis): + def __init__(self, dim, basis_1d, grad_basis_1d, orth_weight): + self._dim = dim + self._basis_1d = basis_1d + self._grad_basis_1d = grad_basis_1d + self._orth_weight = orth_weight + @property + def _order(self): + return len(self._basis_1d)-1 -@get_grad_basis.register(Hypercube) + def orthonormality_weight(self): + if self._orth_weight is None: + raise BasisNotOrthonormal + else: + return self._orth_weight + + @property + def mode_ids(self): + from pytools import generate_nonnegative_integer_tuples_below as gnitb + return tuple(gnitb(self._order+1, self._dim)) + + @property + def functions(self): + return tuple( + _TensorProductBasisFunction(mid, + [self._basis_1d[i] for i in mid]) + for mid in self.mode_ids) + + @property + def gradients(self): + from pytools import wandering_element + func = (self._basis_1d, self._grad_basis_1d) + return tuple( + _TensorProductGradientBasisFunction(mid, [ + [func[i][k] for i, k in zip(iderivative, mid)] + for iderivative in wandering_element(self._dim) + ]) + for mid in self.mode_ids) + + +@orthonormal_basis_for_shape.register def _(shape: Hypercube, order: int): - return grad_legendre_tensor_product_basis(shape.dims, order) + return _TensorProductBasis(shape.dim, + [partial(jacobi, 0, 0, n) for n in range(order + 1)], + [partial(grad_jacobi, 0, 0, n) for n in range(order + 1)], + orth_weight=1) -@get_basis_with_mode_ids.register(Hypercube) +@basis_for_shape.register def _(shape: Hypercube, order: int): - from modepy.shapes import get_node_tuples - mode_ids = get_node_tuples(shape, order) - return mode_ids, get_basis(shape, order) + return orthonormal_basis_for_shape(shape, order) -# }}} + +def _monomial_1d(order, r): + return r**order + + +def _grad_monomial_1d(order, r): + if order == 0: + return 0*r + else: + return order*r**(order-1) + + +@monomial_basis_for_shape.register +def _(shape: Hypercube, order: int): + return _TensorProductBasis(shape.dim, + [partial(_monomial_1d, n) for n in range(order + 1)], + [partial(_grad_monomial_1d, n) for n in range(order + 1)], + orth_weight=None) # }}} diff --git a/modepy/nodes.py b/modepy/nodes.py index e9cb216c..d0e89e2f 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -1,3 +1,32 @@ +""" +Generic Shape-Based Interface +----------------------------- + +.. autofunction:: node_count_for_shape +.. autofunction:: node_tuples_for_shape +.. autofunction:: equispaced_nodes_for_shape +.. autofunction:: edge_clustered_nodes_for_shape + +Simplices +--------- + +.. currentmodule:: modepy + +.. autofunction:: equidistant_nodes +.. autofunction:: warp_and_blend_nodes + +Also see :class:`modepy.VioreanuRokhlinSimplexQuadrature` if nodes on the +boundary are not required. + +Hypercubes +---------- + +.. currentmodule:: modepy + +.. autofunction:: tensor_product_nodes +.. autofunction:: legendre_gauss_lobatto_tensor_product_nodes +""" + __copyright__ = "Copyright (C) 2009, 2010, 2013 Andreas Kloeckner, " \ "Tim Warburton, Jan Hesthaven, Xueyu Zhu" @@ -24,8 +53,9 @@ import numpy as np import numpy.linalg as la -from modepy.shapes import Simplex, Hypercube -from modepy.shapes import get_node_count, get_node_tuples, get_unit_nodes +from functools import singledispatch, partial + +from modepy.shapes import Shape, Simplex, Hypercube # {{{ equidistant nodes @@ -38,15 +68,15 @@ def equidistant_nodes(dims, n, node_tuples=None): :arg node_tuples: a list of tuples of integers indicating the node order. Use default order if *None*, see :func:`pytools.generate_nonnegative_integer_tuples_summing_to_at_most`. - :returns: An array of shape *(dims, nnodes)* containing unit coordinates + :returns: An array of shape *(dims, nnodes)* containing bi-unit coordinates of the interpolation nodes. (see :ref:`tri-coords` and :ref:`tet-coords`) """ shape = Simplex(dims) if node_tuples is None: - node_tuples = get_node_tuples(shape, n) + node_tuples = node_tuples_for_shape(shape, n) else: - if len(node_tuples) != get_node_count(shape, n): + if len(node_tuples) != node_count_for_shape(shape, n): raise ValueError("'node_tuples' list does not have the correct length") # shape: (dims, nnodes) @@ -68,9 +98,9 @@ def warp_factor(n, output_nodes, scaled=True): equi_nodes = np.linspace(-1, 1, n+1) from modepy.matrices import vandermonde - from modepy.modes import simplex_onb + from modepy.modes import jacobi - basis = simplex_onb(1, n) + basis = [partial(jacobi, 0, 0, n) for n in range(n + 1)] Veq = vandermonde(basis, equi_nodes) # noqa # create interpolator from equi_nodes to output_nodes @@ -127,9 +157,9 @@ def warp_and_blend_nodes_2d(n, node_tuples=None): shape = Simplex(2) if node_tuples is None: - node_tuples = get_node_tuples(shape, n) + node_tuples = node_tuples_for_shape(shape, n) else: - if len(node_tuples) != get_node_count(shape, n): + if len(node_tuples) != node_count_for_shape(shape, n): raise ValueError("'node_tuples' list does not have the correct length") # shape: (2, nnodes) @@ -163,9 +193,9 @@ def warp_and_blend_nodes_3d(n, node_tuples=None): shape = Simplex(3) if node_tuples is None: - node_tuples = get_node_tuples(shape, n) + node_tuples = node_tuples_for_shape(shape, n) else: - if len(node_tuples) != get_node_count(shape, n): + if len(node_tuples) != node_count_for_shape(shape, n): raise ValueError("'node_tuples' list does not have the correct length") # shape: (3, nnodes) @@ -320,63 +350,115 @@ def legendre_gauss_lobatto_tensor_product_nodes(dims, n): # }}} -# {{{ shape nodes +# {{{ shape-based interface + +@singledispatch +def node_count_for_shape(shape: Shape, order: int): + raise NotImplementedError(type(shape).__name__) + + +@singledispatch +def node_tuples_for_shape(shape: Shape, order: int): + raise NotImplementedError(type(shape).__name__) + + +@singledispatch +def equispaced_nodes_for_shape(shape: Shape, order: int): + raise NotImplementedError(type(shape).__name__) + + +@singledispatch +def edge_clustered_nodes_for_shape(shape: Shape, order: int): + raise NotImplementedError(type(shape).__name__) + + +@singledispatch +def random_nodes_for_shape(shape: Shape, nnodes: int, rng=None): + """ + :arg generator: a :class:`numpy.random.Generator`. + :returns: a :class:`numpy.ndarray` that returns an array of + shape `(dim, nnodes)` of random nodes in the reference element. + """ + raise NotImplementedError(type(shape).__name__) + # {{{ simplex -@get_node_count.register(Simplex) +@node_count_for_shape.register def _(shape: Simplex, order: int): try: from math import comb # comb is v3.8+ - node_count = comb(order + shape.dims, shape.dims) + node_count = comb(order + shape.dim, shape.dim) except ImportError: from functools import reduce from operator import mul - node_count = reduce(mul, range(order + 1, order + shape.dims + 1), 1) \ - // reduce(mul, range(1, shape.dims + 1), 1) + node_count = reduce(mul, range(order + 1, order + shape.dim + 1), 1) \ + // reduce(mul, range(1, shape.dim + 1), 1) return node_count -@get_node_tuples.register(Simplex) +@node_tuples_for_shape.register def _(shape: Simplex, order: int): from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam - if shape.dims == 0: - return ((0,),) - else: - return tuple(gnitsam(order, shape.dims)) + return tuple(gnitsam(order, shape.dim)) -@get_unit_nodes.register(Simplex) +@edge_clustered_nodes_for_shape.register def _(shape: Simplex, order: int): import modepy as mp - return mp.warp_and_blend_nodes(shape.dims, order) + return mp.warp_and_blend_nodes(shape.dim, order) + + +@random_nodes_for_shape.register +def _(shape: Simplex, nnodes: int, rng=None): + if rng is None: + rng = np.random + + result = np.zeros((shape.dim, nnodes)) + nnodes_obtained = 0 + while True: + new_nodes = rng.uniform(-1.0, 1.0, size=(shape.dim, nnodes-nnodes_obtained)) + new_nodes = new_nodes[:, new_nodes.sum(axis=0) < 2-shape.dim] + nnew_nodes = new_nodes.shape[1] + result[:, nnodes_obtained:nnodes_obtained+nnew_nodes] = new_nodes + nnodes_obtained += nnew_nodes + + if nnodes_obtained == nnodes: + return result # }}} # {{{ hypercube -@get_node_count.register(Hypercube) +@node_count_for_shape.register def _(shape: Hypercube, order: int): - return (order + 1)**shape.dims + return (order + 1)**shape.dim -@get_node_tuples.register(Hypercube) +@node_tuples_for_shape.register def _(shape: Hypercube, order: int): from pytools import \ generate_nonnegative_integer_tuples_below as gnitb - if shape.dims == 0: + if shape.dim == 0: return ((0,),) else: - return tuple(gnitb(order, shape.dims)) + return tuple(gnitb(order, shape.dim)) -@get_unit_nodes.register(Hypercube) +@edge_clustered_nodes_for_shape.register def _(shape: Hypercube, order: int): import modepy as mp - return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dims, order) + return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dim, order) + + +@random_nodes_for_shape.register +def _(shape: Hypercube, nnodes: int, rng=None): + if rng is None: + rng = np.random + return rng.uniform(-1.0, 1.0, size=(shape.dim, nnodes)) # }}} diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 4f213846..064ce5f2 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -1,3 +1,11 @@ +""" +.. currentmodule:: modepy + +.. autoclass:: Quadrature + +.. autoclass:: quadrature_for_shape +""" + __copyright__ = ("Copyright (C) 2009, 2010, 2013 Andreas Kloeckner, Tim Warburton, " "Jan Hesthaven, Xueyu Zhu") @@ -21,8 +29,10 @@ THE SOFTWARE. """ +from functools import singledispatch import numpy as np +from modepy.shapes import Shape, Simplex, Hypercube class QuadratureRuleUnavailable(RuntimeError): @@ -112,3 +122,40 @@ def __init__(self, N, dims, backend=None): # noqa: N803 from modepy.quadrature.jacobi_gauss import LegendreGaussQuadrature super().__init__( dims, LegendreGaussQuadrature(N, backend=backend)) + + +# {{{ quadrature_for_shape + +@singledispatch +def quadrature_for_shape(shape: Shape, order: int) -> Quadrature: + """ + :returns: a :class:`~modepy.Quadrature` that is exact up to *order* + (as passed to the functions in :mod:`modepy.modes`) :math:`2 N + 1`. + """ + raise NotImplementedError(type(shape).__name__) + + +@quadrature_for_shape.register(Simplex) +def _(shape: Simplex, order: int): + import modepy as mp + try: + quad = mp.XiaoGimbutasSimplexQuadrature(2*order + 1, shape.dim) + except (mp.QuadratureRuleUnavailable, ValueError): + quad = mp.GrundmannMoellerSimplexQuadrature(order, shape.dim) + + return quad + + +@quadrature_for_shape.register(Hypercube) +def _(shape: Hypercube, order: int): + import modepy as mp + if shape.dim == 0: + quad = mp.Quadrature(np.empty((0, 1)), np.empty((0, 1))) + else: + from modepy.quadrature import LegendreGaussTensorProductQuadrature + quad = LegendreGaussTensorProductQuadrature(order, shape.dim) + + return quad +# }}} + +# vim: foldmethod=marker diff --git a/modepy/shapes.py b/modepy/shapes.py index 7de126f5..f34ad5c8 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -3,9 +3,9 @@ (i.e. reference elements). .. autoclass:: Shape -.. autofunction:: get_unit_vertices -.. autofunction:: get_face_vertex_indices -.. autofunction:: get_face_map +.. autofunction:: biunit_vertices_for_shape +.. autofunction:: face_vertex_indices_for_shape +.. autofunction:: face_map_for_shape Simplices ^^^^^^^^^ @@ -17,7 +17,7 @@ Coordinates on the triangle --------------------------- -Unit coordinates :math:`(r, s)`:: +Bi-unit coordinates :math:`(r, s)` (also called 'unit' coordinates):: ^ s | @@ -29,7 +29,7 @@ | \ A-----B--> r -Vertices in unit coordinates:: +Vertices in bi-unit coordinates:: O = ( 0, 0) A = (-1, -1) @@ -58,7 +58,7 @@ Coordinates on the tetrahedron ------------------------------ -Unit coordinates :math:`(r, s, t)`:: +Bi-unit coordinates :math:`(r, s, t)` (also called 'unit' coordinates):: ^ s | @@ -75,7 +75,7 @@ (squint, and it might start making sense...) -Vertices in unit coordinates :math:`(r, s, t)`:: +Vertices in bi-unit coordinates :math:`(r, s, t)`:: O = ( 0, 0, 0) A = (-1, -1, -1) @@ -101,7 +101,7 @@ Coordinates on the square ------------------------- -Unit coordinates on :math:`(r, s)`:: +Bi-unit coordinates on :math:`(r, s)` (also called 'unit' coordinates):: ^ s | @@ -114,7 +114,7 @@ A---------B --> r -Vertices in unit coordinates:: +Vertices in bi-unit coordinates:: O = ( 0, 0) A = (-1, -1) @@ -127,7 +127,7 @@ Coordinates on the cube ----------------------- -Unit coordinates on :math:`(r, s, t)`:: +Unit coordinates on :math:`(r, s, t)` (also called 'unit' coordinates):: t ^ @@ -146,7 +146,7 @@ \ v r -Verties in unit coordinates:: +Vertices in unit coordinates:: O = ( 0, 0, 0) A = (-1, -1, -1) @@ -199,78 +199,40 @@ @dataclass(frozen=True) class Shape: """ - .. attribute :: dims + .. attribute :: dim .. attribute :: nfaces .. attribute :: nvertices """ - dims: int + dim: int @singledispatch -def get_unit_vertices(shape: Shape): +def biunit_vertices_for_shape(shape: Shape): """ - :returns: an :class:`~numpy.ndarray` of shape `(nvertices, dims)`. + :returns: a :class:`~numpy.ndarray` of shape `(dim, nvertices)`. """ raise NotImplementedError(type(shape).__name__) @singledispatch -def get_face_vertex_indices(shape: Shape): +def face_vertex_indices_for_shape(shape: Shape): """ :results: a tuple of the length :attr:`Shape.nfaces`, where each entry is a tuple of indices into the vertices returned by - :func:`get_unit_vertices`. + :func:`biunit_vertices_for_shape`. """ raise NotImplementedError(type(shape).__name__) @singledispatch -def get_face_map(shape: Shape, face_vertices: np.ndarray): +def face_map_for_shape(shape: Shape, face_vertices: np.ndarray): """ :returns: a :class:`~collections.abc.Callable` that takes an array of - size `(dims, nnodes)` of unit nodes on the face represented by + size `(dim, nnodes)` of unit nodes on the face represented by *face_vertices* and maps them to the volume. """ raise NotImplementedError(type(shape).__name__) - -@singledispatch -def get_quadrature(shape: Shape, order: int): - """ - :returns: a :class:`~modepy.Quadrature` that is exact up to :math:`2 N + 1`. - """ - raise NotImplementedError(type(shape).__name__) - - -@singledispatch -def get_node_count(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) - - -@singledispatch -def get_node_tuples(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) - - -@singledispatch -def get_unit_nodes(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) - - -@singledispatch -def get_basis(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) - - -@singledispatch -def get_grad_basis(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) - - -@singledispatch -def get_basis_with_mode_ids(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) - # }}} @@ -279,23 +241,23 @@ def get_basis_with_mode_ids(shape: Shape, order: int): class Simplex(Shape): @property def nfaces(self): - return self.dims + 1 + return self.dim + 1 @property def nvertices(self): return self.dim + 1 -@get_unit_vertices.register(Simplex) +@biunit_vertices_for_shape.register def _(shape: Simplex): from modepy.tools import unit_vertices - return unit_vertices(shape.dims) + return unit_vertices(shape.dim).T.copy() -@get_face_vertex_indices.register(Simplex) +@face_vertex_indices_for_shape.register def _(shape: Simplex): - fvi = np.empty((shape.dims + 1, shape.dims), dtype=np.int) - indices = np.arange(shape.dims + 1) + fvi = np.empty((shape.dim + 1, shape.dim), dtype=np.int) + indices = np.arange(shape.dim + 1) for iface in range(shape.nfaces): fvi[iface, :] = np.hstack([indices[:iface], indices[iface + 1:]]) @@ -303,10 +265,10 @@ def _(shape: Simplex): return fvi -@get_face_map.register(Simplex) +@face_map_for_shape.register def _(shape: Simplex, face_vertices: np.ndarray): - dims, npoints = face_vertices.shape - if npoints != dims: + dim, npoints = face_vertices.shape + if npoints != dim: raise ValueError("'face_vertices' has wrong shape") origin = face_vertices[:, 0].reshape(-1, 1) @@ -314,17 +276,6 @@ def _(shape: Simplex, face_vertices: np.ndarray): return lambda p: origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) - -@get_quadrature.register(Simplex) -def _(shape: Simplex, order: int): - import modepy as mp - try: - quad = mp.XiaoGimbutasSimplexQuadrature(2*order + 1, shape.dims) - except (mp.QuadratureRuleUnavailable, ValueError): - quad = mp.GrundmannMoellerSimplexQuadrature(order, shape.dims) - - return quad - # }}} @@ -333,20 +284,20 @@ def _(shape: Simplex, order: int): class Hypercube(Shape): @property def nfaces(self): - return 2 * self.dims + return 2 * self.dim @property def nvertices(self): - return 2**self.dims + return 2**self.dim -@get_unit_vertices.register(Hypercube) +@biunit_vertices_for_shape.register def _(shape: Hypercube): from modepy.nodes import tensor_product_nodes - return tensor_product_nodes(shape.dims, np.array([-1.0, 1.0])).T + return tensor_product_nodes(shape.dim, np.array([-1.0, 1.0])) -@get_face_vertex_indices.register(Hypercube) +@face_vertex_indices_for_shape.register def _(shape: Hypercube): # FIXME: replace by nicer n-dimensional formula return { @@ -362,13 +313,13 @@ def _(shape: Hypercube): (0b000, 0b001, 0b100, 0b101,), (0b010, 0b011, 0b110, 0b111,), ) - }[shape.dims] + }[shape.dim] -@get_face_map.register(Hypercube) +@face_map_for_shape.register def _(shape: Hypercube, face_vertices: np.ndarray): - dims, npoints = face_vertices.shape - if npoints != 2**(dims - 1): + dim, npoints = face_vertices.shape + if npoints != 2**(dim - 1): raise ValueError("'face_vertices' has wrong shape") origin = face_vertices[:, 0].reshape(-1, 1) @@ -376,18 +327,6 @@ def _(shape: Hypercube, face_vertices: np.ndarray): return lambda p: origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) - -@get_quadrature.register(Hypercube) -def _(shape: Hypercube, order: int): - import modepy as mp - if shape.dims == 0: - quad = mp.Quadrature(np.empty((0, 1)), np.empty((0, 1))) - else: - from modepy.quadrature import LegendreGaussTensorProductQuadrature - quad = LegendreGaussTensorProductQuadrature(order, shape.dims) - - return quad - # }}} # vim: foldmethod=marker diff --git a/modepy/tools.py b/modepy/tools.py index 79acb876..99f1f11f 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -1,3 +1,22 @@ +""" +Transformations between coordinate systems on the simplex +--------------------------------------------------------- + +All of these expect and return arrays of shape *(dims, npts)*. + +.. autofunction:: equilateral_to_unit +.. autofunction:: barycentric_to_unit +.. autofunction:: unit_to_barycentric +.. autofunction:: barycentric_to_equilateral +.. autofunction:: submesh_for_shape + +Interpolation quality +--------------------- + +.. autofunction:: estimate_lebesgue_constant + +""" + __copyright__ = "Copyright (C) 2013 Andreas Kloeckner" __license__ = """ @@ -20,12 +39,13 @@ THE SOFTWARE. """ -from functools import reduce +from functools import reduce, singledispatch import numpy as np import numpy.linalg as la from math import sqrt from pytools import memoize_method, MovedFunctionDeprecationWrapper +import modepy.shapes as shp try: @@ -233,25 +253,10 @@ def barycentric_to_equilateral(bary): # }}} -def pick_random_simplex_unit_coordinate(rng, dims): - offset = 0.05 - base = -1 + offset - remaining = 2 - dims*offset - r = np.zeros(dims, np.float64) - for j in range(dims): - rn = rng.uniform(0, remaining) - r[j] = base + rn - remaining -= rn - return r - - -def pick_random_hypercube_unit_coordinate(rng, dims): - return np.array([rng.uniform(-1.0, 1.0) for _ in range(dims)]) +# {{{ submeshes - -# {{{ submeshes, plotting helpers - -def simplex_submesh(node_tuples): +@singledispatch +def submesh_for_shape(shape: shp.Shape, node_tuples): """Return a list of tuples of indices into the node list that generate a tesselation of the reference element. @@ -259,9 +264,15 @@ def simplex_submesh(node_tuples): indicating node positions inside the unit element. The returned list references indices in this list. - :func:`pytools.generate_nonnegative_integer_tuples_summing_to_at_most` - may be used to generate *node_tuples*. + :func:`modepy.node_tuples_for_shape` may be used to generate *node_tuples*. + + .. versionadded:: 2020.3 """ + raise NotImplementedError(type(shape).__name__) + + +@submesh_for_shape.register +def _(shape: shp.Simplex, node_tuples): from pytools import single_valued, add_tuples dims = single_valued(len(nt) for nt in node_tuples) @@ -346,25 +357,8 @@ def try_add_tet(d1, d2, d3, d4): raise NotImplementedError("%d-dimensional sub-meshes" % dims) -submesh = MovedFunctionDeprecationWrapper(simplex_submesh) - - -def hypercube_submesh(node_tuples): - """Return a list of tuples of indices into the node list that - generate a tesselation of the reference element. - - :arg node_tuples: A list of tuples *(i, j, ...)* of integers - indicating node positions inside the unit element. The - returned list references indices in this list. - - :func:`pytools.generate_nonnegative_integer_tuples_below` - may be used to generate *node_tuples*. - - See also :func:`simplex_submesh`. - - .. versionadded:: 2020.2 - """ - +@submesh_for_shape.register +def _(shape: shp.Hypercube, node_tuples): from pytools import single_valued, add_tuples dims = single_valued(len(nt) for nt in node_tuples) @@ -387,6 +381,50 @@ def hypercube_submesh(node_tuples): return result +def simplex_submesh(node_tuples): + """Return a list of tuples of indices into the node list that + generate a tesselation of the reference element. + + :arg node_tuples: A list of tuples *(i, j, ...)* of integers + indicating node positions inside the unit element. The + returned list references indices in this list. + + :func:`pytools.generate_nonnegative_integer_tuples_summing_to_at_most` + may be used to generate *node_tuples*. + """ + return submesh_for_shape(shp.Simplex(len(node_tuples[0])), node_tuples) + + +submesh = MovedFunctionDeprecationWrapper(simplex_submesh) + + +def hypercube_submesh(node_tuples): + """Return a list of tuples of indices into the node list that + generate a tesselation of the reference element. + + :arg node_tuples: A list of tuples *(i, j, ...)* of integers + indicating node positions inside the unit element. The + returned list references indices in this list. + + :func:`pytools.generate_nonnegative_integer_tuples_below` + may be used to generate *node_tuples*. + + See also :func:`simplex_submesh`. + + .. versionadded:: 2020.2 + """ + from warnings import warn + warn("hypercube_submesh is deprecated. " + "Use submesh_for_shape instead. " + "hypercube_submesh will go away in 2022.", + DeprecationWarning, stacklevel=2) + + return submesh_for_shape(shp.Hypercube(len(node_tuples[0])), node_tuples) + +# }}} + + +# {{{ plotting helpers def plot_element_values(n, nodes, values, resample_n=None, node_tuples=None, show_nodes=False): dims = len(nodes) @@ -431,15 +469,16 @@ def plot_element_values(n, nodes, values, resample_n=None, def _evaluate_lebesgue_function(n, nodes, shape): huge_n = 30*n - from modepy.shapes import get_basis, get_node_tuples - basis = get_basis(shape, n) - equi_node_tuples = get_node_tuples(shape, huge_n) + from modepy.modes import basis_for_shape + from modepy.nodes import node_tuples_for_shape + basis = basis_for_shape(shape, n) + equi_node_tuples = node_tuples_for_shape(shape, huge_n) equi_nodes = (np.array(equi_node_tuples, dtype=np.float64)/huge_n*2 - 1).T from modepy.matrices import vandermonde - vdm = vandermonde(basis, nodes) + vdm = vandermonde(basis.functions, nodes) - eq_vdm = vandermonde(basis, equi_nodes) + eq_vdm = vandermonde(basis.functions, equi_nodes) eq_to_out = la.solve(vdm.T, eq_vdm.T).T lebesgue_worst = np.sum(np.abs(eq_to_out), axis=1) @@ -453,7 +492,7 @@ def estimate_lebesgue_constant(n, nodes, shape=None, visualize=False): `_ of the *nodes* at polynomial order *n*. - :arg nodes: an array of shape *(dims, nnodes)* as returned by + :arg nodes: an array of shape *(dim, nnodes)* as returned by :func:`modepy.warp_and_blend_nodes`. :arg shape: a :class:`~modepy.shapes.Shape`. :arg visualize: visualize the function that gives rise to the @@ -471,13 +510,16 @@ def estimate_lebesgue_constant(n, nodes, shape=None, visualize=False): Renamed *domain* to *shape*. """ - dims = len(nodes) + dim = len(nodes) if shape is None: + from warnings import warn + warn("Not passing shape is deprecated and will stop working " + "in 2022.", DeprecationWarning, stacklevel=2) from modepy.shapes import Simplex - shape = Simplex(dims) + shape = Simplex(dim) else: - if shape.dims != dims: - raise ValueError(f"expected {shape.dims}-dimensional nodes") + if shape.dim != dim: + raise ValueError(f"expected {shape.dim}-dimensional nodes") lebesgue_worst, equi_node_tuples, equi_nodes = \ _evaluate_lebesgue_function(n, nodes, shape) @@ -486,16 +528,9 @@ def estimate_lebesgue_constant(n, nodes, shape=None, visualize=False): if not visualize: return lebesgue_constant - if shape.dims == 2: + if shape.dim == 2: print(f"Lebesgue constant: {lebesgue_constant}") - - from modepy.shapes import Simplex, Hypercube - if isinstance(shape, Simplex): - triangles = simplex_submesh(equi_node_tuples) - elif isinstance(shape, Hypercube): - triangles = hypercube_submesh(equi_node_tuples) - else: - triangles = None + triangles = submesh_for_shape(shape, equi_node_tuples) try: import mayavi.mlab as mlab @@ -530,7 +565,7 @@ def estimate_lebesgue_constant(n, nodes, shape=None, visualize=False): ax.set_aspect("equal") plt.show() else: - raise ValueError(f"visualization is not supported in {shape.dims}D") + raise ValueError(f"visualization is not supported in {shape.dim}D") return lebesgue_constant diff --git a/test/test_modes.py b/test/test_modes.py index ffe61906..35c43a7b 100644 --- a/test/test_modes.py +++ b/test/test_modes.py @@ -24,7 +24,9 @@ import numpy as np import numpy.linalg as la import pytest -import modepy.modes as m +import modepy.modes as md +import modepy.nodes as nd +import modepy.shapes as shp from pymbolic.mapper.stringifier import ( CSESplittingStringifyMapperMixin, StringifyMapper) from pymbolic.mapper.evaluator import EvaluationMapper @@ -51,7 +53,7 @@ def test_orthonormality_jacobi_1d(alpha, beta, ebound): quad = JacobiGaussQuadrature(alpha, beta, 4*max_n) from functools import partial - jac_f = [partial(m.jacobi, alpha, beta, n) for n in range(max_n)] + jac_f = [partial(md.jacobi, alpha, beta, n) for n in range(max_n)] maxerr = 0 for i, fi in enumerate(jac_f): @@ -76,19 +78,23 @@ def test_orthonormality_jacobi_1d(alpha, beta, ebound): # (7, 3e-14), # (9, 2e-13), ]) -@pytest.mark.parametrize("dims", [2, 3]) -def test_pkdo_orthogonality(dims, order, ebound): - """Test orthogonality of simplicial bases using Grundmann-Moeller cubature.""" +@pytest.mark.parametrize("shape", [ + shp.Simplex(2), + shp.Simplex(3), + shp.Hypercube(2), + shp.Hypercube(3), + ]) +def test_orthogonality(shape, order, ebound): + """Test orthogonality of ONBs using cubature.""" - from modepy.quadrature.grundmann_moeller import GrundmannMoellerSimplexQuadrature - from modepy.modes import simplex_onb + from modepy.quadrature import quadrature_for_shape - cub = GrundmannMoellerSimplexQuadrature(order, dims) - basis = simplex_onb(dims, order) + cub = quadrature_for_shape(shape, order) + basis = md.orthonormal_basis_for_shape(shape, order) maxerr = 0 - for i, f in enumerate(basis): - for j, g in enumerate(basis): + for i, f in enumerate(basis.functions): + for j, g in enumerate(basis.functions): if i == j: true_result = 1 else: @@ -103,52 +109,56 @@ def test_pkdo_orthogonality(dims, order, ebound): # print(order, maxerr) -@pytest.mark.parametrize("dims", [1, 2, 3]) +@pytest.mark.parametrize("shape", [ + shp.Simplex(1), + shp.Simplex(2), + shp.Simplex(3), + shp.Hypercube(1), + shp.Hypercube(2), + shp.Hypercube(3), + ]) @pytest.mark.parametrize("order", [5, 8]) -@pytest.mark.parametrize(("eltype", "basis_getter", "grad_basis_getter"), [ - ("simplex", m.simplex_onb, m.grad_simplex_onb), - ("simplex", m.simplex_monomial_basis, m.grad_simplex_monomial_basis), - ("tensor", m.legendre_tensor_product_basis, m.grad_legendre_tensor_product_basis) +@pytest.mark.parametrize("basis_getter", [ + (md.basis_for_shape), + (md.orthonormal_basis_for_shape), + (md.monomial_basis_for_shape), ]) -def test_basis_grad(dims, order, eltype, basis_getter, grad_basis_getter): +def test_basis_grad(shape, order, basis_getter): """Do a simplistic FD-style check on the gradients of the basis.""" h = 1.0e-4 - if eltype == "simplex" and order == 8 and dims == 3: - factor = 3.0 - else: - factor = 1.0 - - if eltype == "simplex": - from modepy.tools import \ - pick_random_simplex_unit_coordinate as pick_random_unit_coordinate - elif eltype == "tensor": - from modepy.tools import \ - pick_random_hypercube_unit_coordinate as pick_random_unit_coordinate - else: - raise ValueError(f"unknown element type: {eltype}") - from random import Random - rng = Random(17) + rng = np.random.Generator(np.random.PCG64(17)) + basis = basis_getter(shape, order) + from pytools.convergence import EOCRecorder from pytools import wandering_element for i_bf, (bf, gradbf) in enumerate(zip( - basis_getter(dims, order), - grad_basis_getter(dims, order), + basis.functions, + basis.gradients, )): - for i in range(10): - r = pick_random_unit_coordinate(rng, dims) + eoc_rec = EOCRecorder() + for h in [1e-2, 1e-3]: + r = nd.random_nodes_for_shape(shape, nnodes=1000, rng=rng) gradbf_v = np.array(gradbf(r)) gradbf_v_num = np.array([ (bf(r+h*unit) - bf(r-h*unit))/(2*h) - for unit_tuple in wandering_element(dims) - for unit in (np.array(unit_tuple),) + for unit_tuple in wandering_element(shape.dim) + for unit in (np.array(unit_tuple).reshape(-1, 1),) ]) - err = la.norm(gradbf_v_num - gradbf_v) + ref_norm = la.norm((gradbf_v).reshape(-1), np.inf) + err = la.norm((gradbf_v_num - gradbf_v).reshape(-1), np.inf) + if ref_norm > 1e-13: + err = err/ref_norm + logger.info("error: %.5", err) - assert err < factor * h, (err, i_bf) + eoc_rec.add_data_point(h, err) + + print(eoc_rec) + assert (eoc_rec.max_error() < 1e-8 + or eoc_rec.order_estimate() >= 1.5) # {{{ test symbolic modes @@ -163,20 +173,23 @@ def map_if(self, expr): self.rec(expr.then), self.rec(expr.else_)) -@pytest.mark.parametrize(("domain", "get_basis", "get_grad_basis"), [ - ("simplex", m.simplex_onb, m.grad_simplex_onb), - ("simplex", m.simplex_monomial_basis, m.grad_simplex_monomial_basis), - ("hypercube", m.legendre_tensor_product_basis, - m.grad_legendre_tensor_product_basis), +@pytest.mark.parametrize("shape", [ + shp.Simplex(1), + shp.Simplex(2), + shp.Simplex(3), + shp.Hypercube(1), + shp.Hypercube(2), + shp.Hypercube(3), ]) -@pytest.mark.parametrize("dims", [1, 2, 3]) -@pytest.mark.parametrize("n", [5, 10]) -def test_symbolic_basis(domain, dims, n, - get_basis, - get_grad_basis): - - basis = get_basis(dims, n) - sym_basis = [m.symbolicize_function(f, dims) for f in basis] +@pytest.mark.parametrize("order", [5, 8]) +@pytest.mark.parametrize("basis_getter", [ + (md.basis_for_shape), + (md.orthonormal_basis_for_shape), + (md.monomial_basis_for_shape), + ]) +def test_symbolic_basis(shape, order, basis_getter): + basis = basis_getter(shape, order) + sym_basis = [md.symbolicize_function(f, shape.dim) for f in basis.functions] # {{{ test symbolic against direct eval @@ -184,15 +197,10 @@ def test_symbolic_basis(domain, dims, n, print("VALUES") print(75*"#") - r = np.random.rand(dims, 10000)*2 - 1 - if domain == "simplex": - r = r[:, r.sum(axis=0) < 0] - elif domain == "hypercube": - pass - else: - raise ValueError(f"unexpected domain: {domain}") + rng = np.random.Generator(np.random.PCG64(17)) + r = nd.random_nodes_for_shape(shape, 10000, rng=rng) - for func, sym_func in zip(basis, sym_basis): + for func, sym_func in zip(basis.functions, sym_basis): strmap = MyStringifyMapper() s = strmap(sym_func) for name, val in strmap.cse_name_list: @@ -218,10 +226,9 @@ def test_symbolic_basis(domain, dims, n, print("GRADIENTS") print(75*"#") - grad_basis = get_grad_basis(dims, n) - sym_grad_basis = [m.symbolicize_function(f, dims) for f in grad_basis] + sym_grad_basis = [md.symbolicize_function(f, shape.dim) for f in basis.gradients] - for grad, sym_grad in zip(grad_basis, sym_grad_basis): + for grad, sym_grad in zip(basis.gradients, sym_grad_basis): strmap = MyStringifyMapper() s = strmap(sym_grad) for name, val in strmap.cse_name_list: diff --git a/test/test_nodes.py b/test/test_nodes.py index c7b52092..d39b8161 100644 --- a/test/test_nodes.py +++ b/test/test_nodes.py @@ -24,24 +24,21 @@ import numpy as np import numpy.linalg as la import pytest +import modepy.shapes as shp +import modepy.nodes as nd @pytest.mark.parametrize("dims", [1, 2, 3]) def test_barycentric_coordinate_map(dims): - from random import Random - rng = Random(17) - - n = 5 - unit = np.empty((dims, n)) + n = 100 from modepy.tools import ( - pick_random_simplex_unit_coordinate, unit_to_barycentric, barycentric_to_unit, barycentric_to_equilateral, equilateral_to_unit,) - for i in range(n): - unit[:, i] = pick_random_simplex_unit_coordinate(rng, dims) + rng = np.random.Generator(np.random.PCG64(17)) + unit = nd.random_nodes_for_shape(shp.Simplex(dims), n, rng=rng) bary = unit_to_barycentric(unit) assert (np.abs(np.sum(bary, axis=0) - 1) < 1e-15).all() @@ -57,11 +54,10 @@ def test_barycentric_coordinate_map(dims): def test_warp(): """Check some assumptions on the node warp factor calculator""" n = 17 - from modepy.nodes import warp_factor from functools import partial def wfc(x): - return warp_factor(n, np.array([x]), scaled=False)[0] + return nd.warp_factor(n, np.array([x]), scaled=False)[0] assert abs(wfc(-1)) < 1e-12 assert abs(wfc(1)) < 1e-12 @@ -69,7 +65,7 @@ def wfc(x): from modepy.quadrature.jacobi_gauss import LegendreGaussQuadrature lgq = LegendreGaussQuadrature(n) - assert abs(lgq(partial(warp_factor, n, scaled=False))) < 6e-14 + assert abs(lgq(partial(nd.warp_factor, n, scaled=False))) < 6e-14 def test_tri_face_node_distribution(): @@ -85,8 +81,7 @@ def test_tri_face_node_distribution(): as gnitstam node_tuples = list(gnitstam(n, 2)) - from modepy.nodes import warp_and_blend_nodes - unodes = warp_and_blend_nodes(2, n, node_tuples) + unodes = nd.warp_and_blend_nodes(2, n, node_tuples) faces = [ [i for i, nt in enumerate(node_tuples) if nt[0] == 0], @@ -116,8 +111,7 @@ def test_simp_nodes(dims, n): eps = 1e-10 - from modepy.nodes import warp_and_blend_nodes - unodes = warp_and_blend_nodes(dims, n) + unodes = nd.warp_and_blend_nodes(dims, n) assert (unodes >= -1-eps).all() assert (np.sum(unodes) <= eps).all() @@ -139,10 +133,9 @@ def test_affine_map(): @pytest.mark.parametrize("dim", [1, 2, 3, 4]) def test_tensor_product_nodes(dim): - from modepy.nodes import tensor_product_nodes nnodes = 10 nodes_1d = np.arange(nnodes) - nodes = tensor_product_nodes(dim, nodes_1d) + nodes = nd.tensor_product_nodes(dim, nodes_1d) assert np.allclose( nodes[-1], np.array(nodes_1d.tolist() * nnodes**(dim - 1))) diff --git a/test/test_tools.py b/test/test_tools.py index fb096474..b04974d1 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -23,7 +23,10 @@ import numpy as np import numpy.linalg as la import modepy as mp -from modepy.shapes import Simplex, Hypercube + +import modepy.shapes as shp +import modepy.nodes as nd +import modepy.modes as md from functools import partial import pytest @@ -89,9 +92,10 @@ def constant(x): ("c1-2d", partial(c1, -0.1), 2, 15, -2.3), ]) def test_modal_decay(case_name, test_func, dims, n, expected_expn): + shape = shp.Simplex(dims) nodes = mp.warp_and_blend_nodes(dims, n) - basis = mp.simplex_onb(dims, n) - vdm = mp.vandermonde(basis, nodes) + basis = mp.orthonormal_basis_for_shape(shape, n) + vdm = mp.vandermonde(basis.functions, nodes) f = test_func(nodes[0]) coeffs = la.solve(vdm, f) @@ -119,10 +123,12 @@ def test_modal_decay(case_name, test_func, dims, n, expected_expn): ("const-2d", constant, 2, 5), ]) def test_residual_estimation(case_name, test_func, dims, n): + shape = shp.Simplex(dims) + def estimate_resid(inner_n): nodes = mp.warp_and_blend_nodes(dims, inner_n) - basis = mp.simplex_onb(dims, inner_n) - vdm = mp.vandermonde(basis, nodes) + basis = mp.orthonormal_basis_for_shape(shape, inner_n) + vdm = mp.vandermonde(basis.functions, nodes) f = test_func(nodes[0]) coeffs = la.solve(vdm, f) @@ -142,27 +148,26 @@ def estimate_resid(inner_n): # {{{ test_resampling_matrix @pytest.mark.parametrize("dims", [1, 2, 3]) -@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): - from modepy.shapes import get_unit_nodes, get_basis shape = shape_cls(dims) - coarse_nodes = get_unit_nodes(shape, ncoarse) - coarse_basis = get_basis(shape, ncoarse) + coarse_nodes = nd.edge_clustered_nodes_for_shape(shape, ncoarse) + coarse_basis = md.basis_for_shape(shape, ncoarse) - fine_nodes = get_unit_nodes(shape, nfine) - fine_basis = get_basis(shape, nfine) + fine_nodes = nd.edge_clustered_nodes_for_shape(shape, nfine) + fine_basis = md.basis_for_shape(shape, nfine) my_eye = np.dot( - mp.resampling_matrix(fine_basis, coarse_nodes, fine_nodes), - mp.resampling_matrix(coarse_basis, fine_nodes, coarse_nodes)) + mp.resampling_matrix(fine_basis.functions, coarse_nodes, fine_nodes), + mp.resampling_matrix(coarse_basis.functions, fine_nodes, coarse_nodes)) assert la.norm(my_eye - np.eye(len(my_eye))) < 3e-13 my_eye_least_squares = np.dot( - mp.resampling_matrix(coarse_basis, coarse_nodes, fine_nodes, + mp.resampling_matrix(coarse_basis.functions, coarse_nodes, fine_nodes, least_squares_ok=True), - mp.resampling_matrix(coarse_basis, fine_nodes, coarse_nodes), + mp.resampling_matrix(coarse_basis.functions, fine_nodes, coarse_nodes), ) assert la.norm(my_eye_least_squares - np.eye(len(my_eye_least_squares))) < 4e-13 @@ -173,16 +178,14 @@ def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): # {{{ test_diff_matrix @pytest.mark.parametrize("dims", [1, 2, 3]) -@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) def test_diff_matrix(dims, shape_cls, order=5): - from modepy.shapes import get_unit_nodes, get_basis, get_grad_basis shape = shape_cls(dims) - nodes = get_unit_nodes(shape, order) - basis = get_basis(shape, order) - grad_basis = get_grad_basis(shape, order) + nodes = nd.edge_clustered_nodes_for_shape(shape, order) + basis = md.basis_for_shape(shape, order) - diff_mat = mp.differentiation_matrices(basis, grad_basis, nodes) + diff_mat = mp.differentiation_matrices(basis.functions, basis.gradients, nodes) if isinstance(diff_mat, tuple): diff_mat = diff_mat[0] @@ -198,16 +201,17 @@ def test_diff_matrix(dims, shape_cls, order=5): @pytest.mark.parametrize("dims", [2, 3]) def test_diff_matrix_permutation(dims): + shape = shp.Simplex(dims) order = 5 from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam node_tuples = list(gnitstam(order, dims)) - simplex_onb = mp.simplex_onb(dims, order) - grad_simplex_onb = mp.grad_simplex_onb(dims, order) + simplex_onb = mp.orthonormal_basis_for_shape(shape, order) nodes = np.array(mp.warp_and_blend_nodes(dims, order, node_tuples=node_tuples)) - diff_matrices = mp.differentiation_matrices(simplex_onb, grad_simplex_onb, nodes) + diff_matrices = mp.differentiation_matrices( + simplex_onb.functions, simplex_onb.gradients, nodes) for iref_axis in range(dims): perm = mp.diff_matrix_permutation(node_tuples, iref_axis) @@ -222,24 +226,24 @@ def test_diff_matrix_permutation(dims): # {{{ test_face_mass_matrix @pytest.mark.parametrize("dims", [2, 3]) -@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) def test_modal_face_mass_matrix(dims, shape_cls, order=3): np.set_printoptions(linewidth=200) shape = shape_cls(dims) - from modepy.shapes import get_unit_vertices, get_basis - vertices = get_unit_vertices(shape).T - basis = get_basis(shape, order - 1) + vertices = shp.biunit_vertices_for_shape(shape) + basis = md.basis_for_shape(shape, order - 1) - from modepy.shapes import get_face_vertex_indices - fvi = get_face_vertex_indices(shape) + fvi = shp.face_vertex_indices_for_shape(shape) from modepy.matrices import modal_face_mass_matrix for iface in range(shape.nfaces): face_vertices = vertices[:, fvi[iface]] - fmm = modal_face_mass_matrix(basis, order, face_vertices, shape=shape) - fmm2 = modal_face_mass_matrix(basis, order+1, face_vertices, shape=shape) + fmm = modal_face_mass_matrix( + basis.functions, order, face_vertices, volume_shape=shape) + fmm2 = modal_face_mass_matrix( + basis.functions, order+1, face_vertices, volume_shape=shape) error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) logger.info("fmm error: %.5e", error) @@ -252,35 +256,35 @@ def test_modal_face_mass_matrix(dims, shape_cls, order=3): @pytest.mark.parametrize("dims", [2, 3]) -@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) def test_nodal_face_mass_matrix(dims, shape_cls, order=3): np.set_printoptions(linewidth=200) volume = shape_cls(dims) face = shape_cls(dims - 1) - from modepy.shapes import get_unit_vertices, get_unit_nodes, get_basis - vertices = get_unit_vertices(volume).T - volume_nodes = get_unit_nodes(volume, order) - volume_basis = get_basis(volume, order) - face_nodes = get_unit_nodes(face, order) + vertices = nd.edge_clustered_nodes_for_shape(volume, order) + volume_nodes = nd.edge_clustered_nodes_for_shape(volume, order) + volume_basis = md.basis_for_shape(volume, order) + face_nodes = nd.edge_clustered_nodes_for_shape(face, order) - from modepy.shapes import get_face_vertex_indices - fvi = get_face_vertex_indices(volume) + fvi = shp.face_vertex_indices_for_shape(volume) from modepy.matrices import nodal_face_mass_matrix for iface in range(volume.nfaces): face_vertices = vertices[:, fvi[iface]] fmm = nodal_face_mass_matrix( - volume_basis, volume_nodes, face_nodes, order, face_vertices, - shape=volume) + volume_basis.functions, volume_nodes, + face_nodes, order, face_vertices, + volume_shape=volume) fmm2 = nodal_face_mass_matrix( - volume_basis, volume_nodes, face_nodes, order+1, face_vertices, - shape=volume) + volume_basis.functions, + volume_nodes, face_nodes, order+1, face_vertices, + volume_shape=volume) error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) logger.info("fmm error: %.5e", error) - assert error < 1e-11, f"error {error:.5e} on face {iface}" + assert error < 5e-11, f"error {error:.5e} on face {iface}" fmm[np.abs(fmm) < 1e-13] = 0 nnz = np.sum(fmm > 0) @@ -288,8 +292,8 @@ def test_nodal_face_mass_matrix(dims, shape_cls, order=3): logger.info("fmm: nnz %d\n%s", nnz, fmm) logger.info("mass matrix:\n%s", mp.mass_matrix( - get_basis(face, order), - get_unit_nodes(face, order))) + md.basis_for_shape(face, order).functions, + nd.edge_clustered_nodes_for_shape(face, order))) # }}} @@ -298,13 +302,12 @@ def test_nodal_face_mass_matrix(dims, shape_cls, order=3): @pytest.mark.parametrize("dims", [1, 2]) @pytest.mark.parametrize("order", [3, 5, 8]) -@pytest.mark.parametrize("shape_cls", [Simplex, Hypercube]) +@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): logging.basicConfig(level=logging.INFO) shape = shape_cls(dims) - from modepy.shapes import get_unit_nodes - nodes = get_unit_nodes(shape, order) + nodes = nd.edge_clustered_nodes_for_shape(shape, order) from modepy.tools import estimate_lebesgue_constant lebesgue_constant = estimate_lebesgue_constant(order, nodes, shape=shape) @@ -346,8 +349,9 @@ def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): @pytest.mark.parametrize("dims", [2, 3, 4]) def test_hypercube_submesh(dims): - from modepy.tools import hypercube_submesh + from modepy.tools import submesh_for_shape from pytools import generate_nonnegative_integer_tuples_below as gnitb + shape = shp.Hypercube(dims) node_tuples = list(gnitb(3, dims)) @@ -356,7 +360,7 @@ def test_hypercube_submesh(dims): assert len(node_tuples) == 3**dims - elements = hypercube_submesh(node_tuples) + elements = submesh_for_shape(shape, node_tuples) for e in elements: logger.info("element: %s", e) From 560a0275ad8718c3656ee22404284c403b7702eb Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 17:32:59 -0600 Subject: [PATCH 25/68] Refactor face information retrieval and face mass matrix computation --- modepy/matrices.py | 107 ++++++++++++++++++++++---------------- modepy/shapes.py | 125 ++++++++++++++++++++++++++++++--------------- test/test_tools.py | 110 +++++++++++++++++++++++++++++---------- 3 files changed, 229 insertions(+), 113 deletions(-) diff --git a/modepy/matrices.py b/modepy/matrices.py index fdd56be0..7b4444da 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -21,9 +21,12 @@ """ +from warnings import warn import numpy as np import numpy.linalg as la +import modepy.shapes as shp + __doc__ = r""" .. currentmodule:: modepy @@ -48,12 +51,9 @@ where :math:`(\phi_i)_i` is the basis of functions underlying :math:`V`. .. autofunction:: inverse_mass_matrix - .. autofunction:: mass_matrix - -.. autofunction:: modal_face_mass_matrix - -.. autofunction:: nodal_face_mass_matrix +.. autofunction:: modal_mass_matrix_for_face +.. autofunction:: nodal_mass_matrix_for_face Differentiation is also convenient to express by using :math:`V^{-1}` to obtain modal values and then using a Vandermonde matrix for the derivatives @@ -243,40 +243,68 @@ def mass_matrix(basis, nodes): return la.inv(inverse_mass_matrix(basis, nodes)) -def modal_face_mass_matrix(trial_basis, order, face_vertices, - test_basis=None, volume_shape=None): +def modal_mass_matrix_for_face(face, trial_functions, test_functions, order): + """ + .. versionadded:: 2020.3 + """ + + from modepy.quadrature import quadrature_for_shape + quad = quadrature_for_shape(face, order) + + assert quad.exact_to > order*2 + mapped_nodes = face.map_to_volume(quad.nodes) + + result = np.empty((len(test_functions), len(trial_functions))) + + for i, test_f in enumerate(test_functions): + test_vals = test_f(mapped_nodes) + for j, trial_f in enumerate(trial_functions): + result[i, j] = (test_vals*trial_f(quad.nodes)).dot(quad.weights) + + return result + + +def nodal_mass_matrix_for_face(face: shp.Face, trial_functions, test_functions, + volume_nodes, face_nodes, order): + """ + .. versionadded :: 2020.3 + """ + face_vdm = vandermonde(trial_functions, face_nodes) + vol_vdm = vandermonde(test_functions, volume_nodes) + + modal_fmm = modal_mass_matrix_for_face( + face, trial_functions, test_functions, order) + return la.inv(vol_vdm.T).dot(modal_fmm).dot(la.pinv(face_vdm)) + + +# {{{ deprecated junk + +def modal_face_mass_matrix(trial_basis, order, face_vertices, test_basis=None): """ :arg face_vertices: an array of shape ``(dims, nvertices)``. :arg shape: a :class:`~modepy.shapes.Shape` that identifies the reference volume element. .. versionadded :: 2016.1 - - .. versionchanged:: 2020.3 - - Added *volume_shape* parameter and support for :math:`[-1, 1]^d` domains. """ + warn("modal_face_mass_matrix is deprecated and will go away in 2022. " + "Use modal_mass_matrix_for_face instead.", + DeprecationWarning, stacklevel=2) + if test_basis is None: test_basis = trial_basis - if volume_shape is None: - from warnings import warn - warn("Not passing volume_shape is deprecated and will stop working " - "in 2022.", DeprecationWarning, stacklevel=2) + vol_dims = face_vertices.shape[0] + face_shape = shp.Simplex(vol_dims - 1) - from modepy.shapes import Simplex - volume_shape = Simplex(face_vertices.shape[0]) - - from modepy.shapes import face_map_for_shape from modepy.quadrature import quadrature_for_shape - # FIXME NOPE - face_shape = type(volume_shape)(volume_shape.dim - 1) - fmap = face_map_for_shape(volume_shape, face_vertices) quad = quadrature_for_shape(face_shape, order) assert quad.exact_to > order*2 - mapped_nodes = fmap(quad.nodes) + + from modepy.shapes import _simplex_face_to_vol_map + mapped_nodes = _simplex_face_to_vol_map(face_vertices, quad.nodes) nrows = len(test_basis) ncols = len(trial_basis) @@ -285,47 +313,38 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, for i, test_f in enumerate(test_basis): test_vals = test_f(mapped_nodes) for j, trial_f in enumerate(trial_basis): - trial_vals = trial_f(mapped_nodes) - - result[i, j] = (test_vals*trial_vals).dot(quad.weights) + result[i, j] = (test_vals*trial_f(mapped_nodes)).dot(quad.weights) return result def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, - face_vertices, test_basis=None, volume_shape=None): + face_vertices, test_basis=None): """ :arg face_vertices: an array of shape ``(dims, nvertices)``. :arg shape: a :class:`~modepy.shapes.Shape` that identifies the reference face element. .. versionadded :: 2016.1 - - .. versionchanged:: 2020.3 - - Added *volume_shape* parameter and support for :math:`[-1, 1]^d` domains. """ + warn("nodal_face_mass_matrix is deprecated and will go away in 2022. " + "Use nodal_mass_matrix_for_face instead.", + DeprecationWarning, stacklevel=2) + if test_basis is None: test_basis = trial_basis - if volume_shape is None: - from warnings import warn - warn("Not passing volume_shape is deprecated and will stop working " - "in 2022.", DeprecationWarning, stacklevel=2) - - from modepy.shapes import Simplex - volume_shape = Simplex(face_vertices.shape[0]) - - from modepy.shapes import face_map_for_shape - fmap = face_map_for_shape(volume_shape, face_vertices) - face_vdm = vandermonde(trial_basis, fmap(face_nodes)) # /!\ non-square + from modepy.shapes import _simplex_face_to_vol_map + face_vdm = vandermonde( + trial_basis, + _simplex_face_to_vol_map(face_vertices, face_nodes)) vol_vdm = vandermonde(test_basis, volume_nodes) modal_fmm = modal_face_mass_matrix( - trial_basis, order, face_vertices, - test_basis=test_basis, volume_shape=volume_shape) + trial_basis, order, face_vertices, test_basis=test_basis) return la.inv(vol_vdm.T).dot(modal_fmm).dot(la.pinv(face_vdm)) +# }}} # vim: foldmethod=marker diff --git a/modepy/shapes.py b/modepy/shapes.py index f34ad5c8..3695a1a3 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -1,11 +1,13 @@ +# {{{ docstring + r""" :mod:`modepy.shapes` provides a generic description of the supported shapes (i.e. reference elements). .. autoclass:: Shape +.. autoclass:: Face .. autofunction:: biunit_vertices_for_shape -.. autofunction:: face_vertex_indices_for_shape -.. autofunction:: face_map_for_shape +.. autofunction:: faces_for_shape Simplices ^^^^^^^^^ @@ -162,6 +164,7 @@ in ``rst``. For example, in 3D, ``A, B, C, D, ...`` is ``000, 001, 010, 011, ...``. """ +# }}} __copyright__ = """ Copyright (c) 2013 Andreas Kloeckner @@ -189,8 +192,9 @@ """ import numpy as np +from typing import Tuple, Callable -from functools import singledispatch +from functools import singledispatch, partial from dataclasses import dataclass @@ -214,22 +218,35 @@ def biunit_vertices_for_shape(shape: Shape): raise NotImplementedError(type(shape).__name__) -@singledispatch -def face_vertex_indices_for_shape(shape: Shape): - """ - :results: a tuple of the length :attr:`Shape.nfaces`, where each entry - is a tuple of indices into the vertices returned by - :func:`biunit_vertices_for_shape`. +@dataclass(frozen=True) +class Face: + """Inherits from :class:`Shape`. + + .. attribute:: volume_shape + The volume_shape :class:`Shape` from which this face descends. + + .. attribute:: face_index + The face index in :attr:`volume_shape` of this face. + + .. attribute:: volume_vertex_indices + a tuple of indices into the vertices returned by + :func:`biunit_vertices_for_shape` for the :attr:`volume_shape`. + + .. attribute:: map_to_volume + a :class:`~collections.abc.Callable` that takes an array of + size `(dim, nnodes)` of unit nodes on the face represented by + *face_vertices* and maps them to the :attr:`volume_shape`. """ - raise NotImplementedError(type(shape).__name__) + volume_shape: Shape + face_index: int + volume_vertex_indices: Tuple[int] + map_to_volume: Callable[[np.ndarray], np.ndarray] @singledispatch -def face_map_for_shape(shape: Shape, face_vertices: np.ndarray): +def faces_for_shape(shape: Shape): """ - :returns: a :class:`~collections.abc.Callable` that takes an array of - size `(dim, nnodes)` of unit nodes on the face represented by - *face_vertices* and maps them to the volume. + :results: a tuple of :class:`Face` representing the faces of *shape*. """ raise NotImplementedError(type(shape).__name__) @@ -248,25 +265,18 @@ def nvertices(self): return self.dim + 1 +@dataclass(frozen=True) +class _SimplexFace(Simplex, Face): + pass + + @biunit_vertices_for_shape.register def _(shape: Simplex): from modepy.tools import unit_vertices return unit_vertices(shape.dim).T.copy() -@face_vertex_indices_for_shape.register -def _(shape: Simplex): - fvi = np.empty((shape.dim + 1, shape.dim), dtype=np.int) - indices = np.arange(shape.dim + 1) - - for iface in range(shape.nfaces): - fvi[iface, :] = np.hstack([indices[:iface], indices[iface + 1:]]) - - return fvi - - -@face_map_for_shape.register -def _(shape: Simplex, face_vertices: np.ndarray): +def _simplex_face_to_vol_map(face_vertices, p: np.ndarray): dim, npoints = face_vertices.shape if npoints != dim: raise ValueError("'face_vertices' has wrong shape") @@ -274,7 +284,26 @@ def _(shape: Simplex, face_vertices: np.ndarray): origin = face_vertices[:, 0].reshape(-1, 1) face_basis = face_vertices[:, 1:] - origin - return lambda p: origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) + return origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) + + +@faces_for_shape.register +def _(shape: Simplex): + face_vertex_indices = np.empty((shape.dim + 1, shape.dim), dtype=np.int) + indices = np.arange(shape.dim + 1) + + for iface in range(shape.nfaces): + face_vertex_indices[iface, :] = \ + np.hstack([indices[:iface], indices[iface + 1:]]) + + vertices = biunit_vertices_for_shape(shape) + return [ + _SimplexFace( + dim=shape.dim-1, + volume_shape=shape, face_index=iface, + volume_vertex_indices=fvi, + map_to_volume=partial(_simplex_face_to_vol_map, vertices[:, fvi])) + for iface, fvi in enumerate(face_vertex_indices)] # }}} @@ -291,16 +320,33 @@ def nvertices(self): return 2**self.dim +@dataclass(frozen=True) +class _HypercubeFace(Hypercube, Face): + pass + + @biunit_vertices_for_shape.register def _(shape: Hypercube): from modepy.nodes import tensor_product_nodes return tensor_product_nodes(shape.dim, np.array([-1.0, 1.0])) -@face_vertex_indices_for_shape.register +def _hypercube_face_to_vol_map(face_vertices: np.ndarray, p: np.ndarray): + dim, npoints = face_vertices.shape + if npoints != 2**(dim - 1): + raise ValueError("'face_vertices' has wrong shape") + + origin = face_vertices[:, 0].reshape(-1, 1) + # FIXME Remove yucky flip + face_basis = face_vertices[:, -2:0:-1] - origin + + return origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) + + +@faces_for_shape.register def _(shape: Hypercube): # FIXME: replace by nicer n-dimensional formula - return { + face_vertex_indices = { 1: ((0b0,), (0b1,)), 2: ((0b00, 0b01), (0b10, 0b11), (0b00, 0b10), (0b01, 0b11)), 3: ( @@ -315,17 +361,14 @@ def _(shape: Hypercube): ) }[shape.dim] - -@face_map_for_shape.register -def _(shape: Hypercube, face_vertices: np.ndarray): - dim, npoints = face_vertices.shape - if npoints != 2**(dim - 1): - raise ValueError("'face_vertices' has wrong shape") - - origin = face_vertices[:, 0].reshape(-1, 1) - face_basis = face_vertices[:, -2:0:-1] - origin - - return lambda p: origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) + vertices = biunit_vertices_for_shape(shape) + return [ + _HypercubeFace( + dim=shape.dim-1, + volume_shape=shape, face_index=iface, + volume_vertex_indices=fvi, + map_to_volume=partial(_hypercube_face_to_vol_map, vertices[:, fvi])) + for iface, fvi in enumerate(face_vertex_indices)] # }}} diff --git a/test/test_tools.py b/test/test_tools.py index b04974d1..ef6fec52 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -223,31 +223,28 @@ def test_diff_matrix_permutation(dims): # }}} -# {{{ test_face_mass_matrix +# {{{ face mass matrices (deprecated) @pytest.mark.parametrize("dims", [2, 3]) -@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) -def test_modal_face_mass_matrix(dims, shape_cls, order=3): - np.set_printoptions(linewidth=200) - shape = shape_cls(dims) +def test_deprecated_modal_face_mass_matrix(dims, order=3): + # FIXME DEPRECATED remove along with modal_face_mass_matrix (>=2022) + shape = shp.Simplex(dims) vertices = shp.biunit_vertices_for_shape(shape) basis = md.basis_for_shape(shape, order - 1) - fvi = shp.face_vertex_indices_for_shape(shape) - from modepy.matrices import modal_face_mass_matrix - for iface in range(shape.nfaces): - face_vertices = vertices[:, fvi[iface]] + for face in shp.faces_for_shape(shape): + face_vertices = vertices[:, face.volume_vertex_indices] fmm = modal_face_mass_matrix( - basis.functions, order, face_vertices, volume_shape=shape) + basis.functions, order, face_vertices) fmm2 = modal_face_mass_matrix( - basis.functions, order+1, face_vertices, volume_shape=shape) + basis.functions, order+1, face_vertices) error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) logger.info("fmm error: %.5e", error) - assert error < 1e-11, f"error {error:.5e} on face {iface}" + assert error < 1e-11, f"error {error:.5e} on face {face.face_index}" fmm[np.abs(fmm) < 1e-13] = 0 nnz = np.sum(fmm > 0) @@ -256,35 +253,92 @@ def test_modal_face_mass_matrix(dims, shape_cls, order=3): @pytest.mark.parametrize("dims", [2, 3]) -@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) -def test_nodal_face_mass_matrix(dims, shape_cls, order=3): - np.set_printoptions(linewidth=200) - volume = shape_cls(dims) - face = shape_cls(dims - 1) +def test_deprecated_nodal_face_mass_matrix(dims, order=3): + # FIXME DEPRECATED remove along with nodal_face_mass_matrix (>=2022) + volume = shp.Simplex(dims) - vertices = nd.edge_clustered_nodes_for_shape(volume, order) + vertices = shp.biunit_vertices_for_shape(volume) volume_nodes = nd.edge_clustered_nodes_for_shape(volume, order) volume_basis = md.basis_for_shape(volume, order) - face_nodes = nd.edge_clustered_nodes_for_shape(face, order) - - fvi = shp.face_vertex_indices_for_shape(volume) from modepy.matrices import nodal_face_mass_matrix - for iface in range(volume.nfaces): - face_vertices = vertices[:, fvi[iface]] + for face in shp.faces_for_shape(volume): + face_nodes = nd.edge_clustered_nodes_for_shape(face, order) + face_vertices = vertices[:, face.volume_vertex_indices] fmm = nodal_face_mass_matrix( volume_basis.functions, volume_nodes, - face_nodes, order, face_vertices, - volume_shape=volume) + face_nodes, order, face_vertices) fmm2 = nodal_face_mass_matrix( volume_basis.functions, - volume_nodes, face_nodes, order+1, face_vertices, - volume_shape=volume) + volume_nodes, face_nodes, order+1, face_vertices) + + error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) + logger.info("fmm error: %.5e", error) + assert error < 5e-11, f"error {error:.5e} on face {face.face_index}" + + fmm[np.abs(fmm) < 1e-13] = 0 + nnz = np.sum(fmm > 0) + + logger.info("fmm: nnz %d\n%s", nnz, fmm) + + logger.info("mass matrix:\n%s", mp.mass_matrix( + md.basis_for_shape(face, order).functions, + nd.edge_clustered_nodes_for_shape(face, order))) + +# }}} + + +# {{{ face mass matrices + +@pytest.mark.parametrize("dims", [2, 3]) +@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) +def test_modal_mass_matrix_for_face(dims, shape_cls, order=3): + shape = shape_cls(dims) + + vol_basis = md.basis_for_shape(shape, order) + + from modepy.matrices import modal_mass_matrix_for_face + for face in shp.faces_for_shape(shape): + face_basis = md.basis_for_shape(face, order) + fmm = modal_mass_matrix_for_face( + face, face_basis.functions, vol_basis.functions, order) + fmm2 = modal_mass_matrix_for_face( + face, face_basis.functions, vol_basis.functions, order+1) + + error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) + logger.info("fmm error: %.5e", error) + assert error < 1e-11, f"error {error:.5e} on face {face.face_index}" + + fmm[np.abs(fmm) < 1e-13] = 0 + nnz = np.sum(fmm > 0) + + logger.info("fmm: nnz %d\n%s", nnz, fmm) + + +@pytest.mark.parametrize("dims", [2, 3]) +@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) +def test_nodal_mass_matrix_for_face(dims, shape_cls, order=3): + volume = shape_cls(dims) + face = shape_cls(dims - 1) + + volume_nodes = nd.edge_clustered_nodes_for_shape(volume, order) + volume_basis = md.basis_for_shape(volume, order) + face_nodes = nd.edge_clustered_nodes_for_shape(face, order) + + from modepy.matrices import nodal_mass_matrix_for_face + for face in shp.faces_for_shape(volume): + face_basis = md.basis_for_shape(face, order) + fmm = nodal_mass_matrix_for_face( + face, face_basis.functions, volume_basis.functions, + volume_nodes, face_nodes, order) + fmm2 = nodal_mass_matrix_for_face( + face, face_basis.functions, volume_basis.functions, + volume_nodes, face_nodes, order+1) error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) logger.info("fmm error: %.5e", error) - assert error < 5e-11, f"error {error:.5e} on face {iface}" + assert error < 5e-11, f"error {error:.5e} on face {face.face_index}" fmm[np.abs(fmm) < 1e-13] = 0 nnz = np.sum(fmm > 0) From 598145f1294bb276004ca16d7a2df0307f6609ee Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 17:35:15 -0600 Subject: [PATCH 26/68] Avoid log(0) warnings in test_basis_grad --- test/test_modes.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/test_modes.py b/test/test_modes.py index 35c43a7b..23abd091 100644 --- a/test/test_modes.py +++ b/test/test_modes.py @@ -156,9 +156,10 @@ def test_basis_grad(shape, order, basis_getter): logger.info("error: %.5", err) eoc_rec.add_data_point(h, err) - print(eoc_rec) - assert (eoc_rec.max_error() < 1e-8 - or eoc_rec.order_estimate() >= 1.5) + tol = 1e-8 + if eoc_rec.max_error() >= tol: + print(eoc_rec) + assert (eoc_rec.max_error() < tol or eoc_rec.order_estimate() >= 1.5) # {{{ test symbolic modes From 73e8d565213e13e2e07cc5ec4a189c48c58b34be Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 18:02:28 -0600 Subject: [PATCH 27/68] Reverse vertex/node order for hypercubes --- modepy/nodes.py | 14 ++++++-------- modepy/shapes.py | 26 +++++++++++++++----------- test/test_nodes.py | 2 +- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/modepy/nodes.py b/modepy/nodes.py index d0e89e2f..df95bdf5 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -325,20 +325,18 @@ def warp_and_blend_nodes(dims, n, node_tuples=None): def tensor_product_nodes(dims, nodes_1d): """ - :returns: an array of shape ``(dims, nnodes_1d**dims)``. The - order of nodes is such that the nodes along the last - axis vary fastest. + :returns: an array of shape ``(dims, nnodes_1d**dims)``. .. versionadded:: 2017.1 - """ - if dims == 0: - # NOTE: using this to maintain consistency in the 0d case - return warp_and_blend_nodes(dims, 1) + .. versionchanged:: 2020.3 + + The node ordering has changed and is no longer documented. + """ nnodes_1d = len(nodes_1d) result = np.empty((dims,) + (nnodes_1d,) * dims) for d in range(dims): - result[-d - 1] = nodes_1d.reshape(*((-1,) + (1,)*d)) + result[d] = nodes_1d.reshape(*((-1,) + (1,)*d)) return result.reshape(dims, -1) diff --git a/modepy/shapes.py b/modepy/shapes.py index 3695a1a3..890ca029 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -129,12 +129,12 @@ Coordinates on the cube ----------------------- -Unit coordinates on :math:`(r, s, t)` (also called 'unit' coordinates):: +Bi-unit coordinates on :math:`(r, s, t)` (also called 'unit' coordinates):: t ^ | - B----------D + E----------G |\ |\ | \ | \ | \ | \ @@ -144,24 +144,25 @@ \ | \ | \ | \ | \| \| - E----------G + B----------D \ v r -Vertices in unit coordinates:: +Vertices in bi-unit coordinates:: O = ( 0, 0, 0) A = (-1, -1, -1) - B = (-1, -1, 1) + B = ( 1, -1, -1) C = (-1, 1, -1) - D = (-1, 1, 1) - E = ( 1, -1, -1) + D = ( 1, 1, -1) + E = (-1, -1, 1) F = ( 1, -1, 1) - G = ( 1, 1, -1) + G = (-1, 1, 1) H = ( 1, 1, 1) The order of the vertices in the hypercubes follows binary counting -in ``rst``. For example, in 3D, ``A, B, C, D, ...`` is ``000, 001, 010, 011, ...``. +in ``tsr`` (i.e. in reverse axis order). +For example, in 3D, ``A, B, C, D, ...`` is ``000, 001, 010, 011, ...``. """ # }}} @@ -337,8 +338,11 @@ def _hypercube_face_to_vol_map(face_vertices: np.ndarray, p: np.ndarray): raise ValueError("'face_vertices' has wrong shape") origin = face_vertices[:, 0].reshape(-1, 1) - # FIXME Remove yucky flip - face_basis = face_vertices[:, -2:0:-1] - origin + + # works up to (and including) 3D: + # - no-op for 1D, 2D + # - For square faces, eliminate middle node + face_basis = face_vertices[:, 1:3] - origin return origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) diff --git a/test/test_nodes.py b/test/test_nodes.py index d39b8161..05f6941c 100644 --- a/test/test_nodes.py +++ b/test/test_nodes.py @@ -137,7 +137,7 @@ def test_tensor_product_nodes(dim): nodes_1d = np.arange(nnodes) nodes = nd.tensor_product_nodes(dim, nodes_1d) assert np.allclose( - nodes[-1], + nodes[0], np.array(nodes_1d.tolist() * nnodes**(dim - 1))) From f6c57559f99c57113f42e404f1ad2ff551c6d8de Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 18:06:58 -0600 Subject: [PATCH 28/68] Export face query functionality in root namespace --- modepy/__init__.py | 8 ++++---- modepy/shapes.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/modepy/__init__.py b/modepy/__init__.py index a94e9f81..2edd1fcb 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -23,9 +23,9 @@ from modepy.shapes import ( - Shape, Simplex, Hypercube, + Shape, Face, Simplex, Hypercube, - biunit_vertices_for_shape + biunit_vertices_for_shape, faces_for_shape ) from modepy.modes import ( jacobi, grad_jacobi, @@ -70,8 +70,8 @@ __all__ = [ "__version__", - "Shape", "Simplex", "Hypercube", - "biunit_vertices_for_shape", + "Shape", "Face", "Simplex", "Hypercube", + "biunit_vertices_for_shape", "faces_for_shape", "jacobi", "grad_jacobi", "simplex_onb", "grad_simplex_onb", "simplex_onb_with_mode_ids", diff --git a/modepy/shapes.py b/modepy/shapes.py index 890ca029..5fb74c4b 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -4,6 +4,8 @@ :mod:`modepy.shapes` provides a generic description of the supported shapes (i.e. reference elements). +.. currentmodule:: modepy + .. autoclass:: Shape .. autoclass:: Face .. autofunction:: biunit_vertices_for_shape From e7b82697238bf1e82bf2d210bb19137ac388ef14 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 18:17:51 -0600 Subject: [PATCH 29/68] Add redundant @singledispatch.register arguments for Py3.6 --- modepy/modes.py | 12 ++++++------ modepy/nodes.py | 16 ++++++++-------- modepy/shapes.py | 8 ++++---- modepy/tools.py | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index 1fb9e3d3..d9cc69ea 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -910,7 +910,7 @@ def gradients(self): return tuple(partial(grad_monomial, mid) for mid in self.mode_ids) -@basis_for_shape.register +@basis_for_shape.register(Simplex) def _(shape: Simplex, order: int): if shape.dim <= 3: return _SimplexONB(shape.dim, order) @@ -918,12 +918,12 @@ def _(shape: Simplex, order: int): return _SimplexMonomialBasis(shape.dim, order) -@orthonormal_basis_for_shape.register +@orthonormal_basis_for_shape.register(Simplex) def _(shape: Simplex, order: int): return _SimplexONB(shape.dim, order) -@monomial_basis_for_shape.register +@monomial_basis_for_shape.register(Simplex) def _(shape: Simplex, order: int): return _SimplexMonomialBasis(shape.dim, order) @@ -973,7 +973,7 @@ def gradients(self): for mid in self.mode_ids) -@orthonormal_basis_for_shape.register +@orthonormal_basis_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): return _TensorProductBasis(shape.dim, [partial(jacobi, 0, 0, n) for n in range(order + 1)], @@ -981,7 +981,7 @@ def _(shape: Hypercube, order: int): orth_weight=1) -@basis_for_shape.register +@basis_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): return orthonormal_basis_for_shape(shape, order) @@ -997,7 +997,7 @@ def _grad_monomial_1d(order, r): return order*r**(order-1) -@monomial_basis_for_shape.register +@monomial_basis_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): return _TensorProductBasis(shape.dim, [partial(_monomial_1d, n) for n in range(order + 1)], diff --git a/modepy/nodes.py b/modepy/nodes.py index df95bdf5..b3663494 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -382,7 +382,7 @@ def random_nodes_for_shape(shape: Shape, nnodes: int, rng=None): # {{{ simplex -@node_count_for_shape.register +@node_count_for_shape.register(Simplex) def _(shape: Simplex, order: int): try: from math import comb # comb is v3.8+ @@ -396,20 +396,20 @@ def _(shape: Simplex, order: int): return node_count -@node_tuples_for_shape.register +@node_tuples_for_shape.register(Simplex) def _(shape: Simplex, order: int): from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam return tuple(gnitsam(order, shape.dim)) -@edge_clustered_nodes_for_shape.register +@edge_clustered_nodes_for_shape.register(Simplex) def _(shape: Simplex, order: int): import modepy as mp return mp.warp_and_blend_nodes(shape.dim, order) -@random_nodes_for_shape.register +@random_nodes_for_shape.register(Simplex) def _(shape: Simplex, nnodes: int, rng=None): if rng is None: rng = np.random @@ -431,12 +431,12 @@ def _(shape: Simplex, nnodes: int, rng=None): # {{{ hypercube -@node_count_for_shape.register +@node_count_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): return (order + 1)**shape.dim -@node_tuples_for_shape.register +@node_tuples_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): from pytools import \ generate_nonnegative_integer_tuples_below as gnitb @@ -446,13 +446,13 @@ def _(shape: Hypercube, order: int): return tuple(gnitb(order, shape.dim)) -@edge_clustered_nodes_for_shape.register +@edge_clustered_nodes_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): import modepy as mp return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dim, order) -@random_nodes_for_shape.register +@random_nodes_for_shape.register(Hypercube) def _(shape: Hypercube, nnodes: int, rng=None): if rng is None: rng = np.random diff --git a/modepy/shapes.py b/modepy/shapes.py index 5fb74c4b..7e702f72 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -273,7 +273,7 @@ class _SimplexFace(Simplex, Face): pass -@biunit_vertices_for_shape.register +@biunit_vertices_for_shape.register(Simplex) def _(shape: Simplex): from modepy.tools import unit_vertices return unit_vertices(shape.dim).T.copy() @@ -290,7 +290,7 @@ def _simplex_face_to_vol_map(face_vertices, p: np.ndarray): return origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) -@faces_for_shape.register +@faces_for_shape.register(Simplex) def _(shape: Simplex): face_vertex_indices = np.empty((shape.dim + 1, shape.dim), dtype=np.int) indices = np.arange(shape.dim + 1) @@ -328,7 +328,7 @@ class _HypercubeFace(Hypercube, Face): pass -@biunit_vertices_for_shape.register +@biunit_vertices_for_shape.register(Hypercube) def _(shape: Hypercube): from modepy.nodes import tensor_product_nodes return tensor_product_nodes(shape.dim, np.array([-1.0, 1.0])) @@ -349,7 +349,7 @@ def _hypercube_face_to_vol_map(face_vertices: np.ndarray, p: np.ndarray): return origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2) -@faces_for_shape.register +@faces_for_shape.register(Hypercube) def _(shape: Hypercube): # FIXME: replace by nicer n-dimensional formula face_vertex_indices = { diff --git a/modepy/tools.py b/modepy/tools.py index 99f1f11f..88ac5928 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -271,7 +271,7 @@ def submesh_for_shape(shape: shp.Shape, node_tuples): raise NotImplementedError(type(shape).__name__) -@submesh_for_shape.register +@submesh_for_shape.register(shp.Simplex) def _(shape: shp.Simplex, node_tuples): from pytools import single_valued, add_tuples dims = single_valued(len(nt) for nt in node_tuples) @@ -357,7 +357,7 @@ def try_add_tet(d1, d2, d3, d4): raise NotImplementedError("%d-dimensional sub-meshes" % dims) -@submesh_for_shape.register +@submesh_for_shape.register(shp.Hypercube) def _(shape: shp.Hypercube, node_tuples): from pytools import single_valued, add_tuples dims = single_valued(len(nt) for nt in node_tuples) From c15c4532a0140363c7f760543c613dc057e13bdd Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 18:27:16 -0600 Subject: [PATCH 30/68] Fix doc references --- modepy/__init__.py | 16 +++++++++++----- modepy/matrices.py | 8 ++++---- modepy/modal_decay.py | 2 +- modepy/modes.py | 9 +++++++++ modepy/nodes.py | 9 +++++++-- modepy/shapes.py | 21 +++++++++++++++++++++ 6 files changed, 53 insertions(+), 12 deletions(-) diff --git a/modepy/__init__.py b/modepy/__init__.py index 2edd1fcb..9ff1145d 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -36,18 +36,21 @@ tensor_product_basis, grad_tensor_product_basis, legendre_tensor_product_basis, grad_legendre_tensor_product_basis, + Basis, BasisNotOrthonormal, basis_for_shape, orthonormal_basis_for_shape, monomial_basis_for_shape) from modepy.nodes import ( equidistant_nodes, warp_and_blend_nodes, tensor_product_nodes, legendre_gauss_lobatto_tensor_product_nodes, - node_count_for_shape, node_tuples_for_shape, edge_clustered_nodes_for_shape, + node_count_for_shape, node_tuples_for_shape, + equispaced_nodes_for_shape, edge_clustered_nodes_for_shape, random_nodes_for_shape) from modepy.matrices import (vandermonde, resampling_matrix, differentiation_matrices, diff_matrix_permutation, inverse_mass_matrix, mass_matrix, - modal_face_mass_matrix, nodal_face_mass_matrix) + modal_face_mass_matrix, nodal_face_mass_matrix, + modal_mass_matrix_for_face, nodal_mass_matrix_for_face) from modepy.quadrature import ( Quadrature, QuadratureRuleUnavailable, TensorProductQuadrature, LegendreGaussTensorProductQuadrature, @@ -80,17 +83,20 @@ "simplex_best_available_basis", "grad_simplex_best_available_basis", "tensor_product_basis", "grad_tensor_product_basis", "legendre_tensor_product_basis", "grad_legendre_tensor_product_basis", + "Basis", "BasisNotOrthonormal", "basis_for_shape", "orthonormal_basis_for_shape", "monomial_basis_for_shape", "equidistant_nodes", "warp_and_blend_nodes", "tensor_product_nodes", "legendre_gauss_lobatto_tensor_product_nodes", "node_count_for_shape", "node_tuples_for_shape", - "edge_clustered_nodes_for_shape", "random_nodes_for_shape", + "edge_clustered_nodes_for_shape", "equispaced_nodes_for_shape", + "random_nodes_for_shape", "vandermonde", "resampling_matrix", "differentiation_matrices", "diff_matrix_permutation", - "inverse_mass_matrix", "mass_matrix", "modal_face_mass_matrix", - "nodal_face_mass_matrix", + "inverse_mass_matrix", "mass_matrix", + "modal_face_mass_matrix", "nodal_face_mass_matrix", + "modal_mass_matrix_for_face", "nodal_mass_matrix_for_face", "Quadrature", "QuadratureRuleUnavailable", "TensorProductQuadrature", "LegendreGaussTensorProductQuadrature", diff --git a/modepy/matrices.py b/modepy/matrices.py index 7b4444da..a9fbf2d4 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -75,7 +75,7 @@ def vandermonde(functions, nodes): *functions* are allowed to return :class:`tuple` instances. In this case, a tuple of matrices is returned--i.e. this function - works directly on :func:`modepy.grad_simplex_onb` and returns + works directly on :func:`modepy.Basis.gradients` and returns a tuple of matrices. """ @@ -110,7 +110,7 @@ def resampling_matrix(basis, new_nodes, old_nodes, least_squares_ok=False): :arg basis: A sequence of basis functions accepting arrays of shape *(dims, npts)*, like those returned by - :func:`modepy.simplex_onb`. + :func:`modepy.orthonormal_basis_for_shape`. :arg new_nodes: An array of shape *(dims, n_new_nodes)* :arg old_nodes: An array of shape *(dims, n_old_nodes)* :arg least_squares_ok: If *False*, then nodal values at *old_nodes* @@ -161,10 +161,10 @@ def differentiation_matrices(basis, grad_basis, nodes, from_nodes=None): :arg basis: A sequence of basis functions accepting arrays of shape *(dims, npts)*, - like those returned by :func:`modepy.simplex_onb`. + like those returned by :func:`modepy.orthonormal_basis_for_shape`. :arg grad_basis: A sequence of functions returning the gradients of *basis*, - like those returned by :func:`modepy.grad_simplex_onb`. + like those in :attr:`modepy.Basis.gradients`. :arg nodes: An array of shape *(dims, n_nodes)* :arg from_nodes: An array of shape *(dims, n_from_nodes)*. If *None*, assumed to be the same as *nodes*. diff --git a/modepy/modal_decay.py b/modepy/modal_decay.py index b255e6b4..08e042a0 100644 --- a/modepy/modal_decay.py +++ b/modepy/modal_decay.py @@ -25,7 +25,7 @@ import numpy.linalg as la __doc__ = """Estimate the smoothness of a function represented in a basis -returned by :func:`modepy.simplex_onb`. +returned by :func:`modepy.orthonormal_basis_for_shape`. The method implemented in this module follows this article: diff --git a/modepy/modes.py b/modepy/modes.py index d9cc69ea..96bb0582 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -88,6 +88,15 @@ Conversion to Symbolic ---------------------- .. autofunction:: symbolicize_function + +Redirections to Canonical Names +------------------------------- + +.. currentmodule:: modepy.modes + +.. class:: Basis + + See :class:`modepy.Basis`. """ diff --git a/modepy/nodes.py b/modepy/nodes.py index b3663494..1e41540a 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -1,17 +1,20 @@ +# {{{ docstring + """ Generic Shape-Based Interface ----------------------------- +.. currentmodule:: modepy + .. autofunction:: node_count_for_shape .. autofunction:: node_tuples_for_shape .. autofunction:: equispaced_nodes_for_shape .. autofunction:: edge_clustered_nodes_for_shape +.. autofunction:: random_nodes_for_shape Simplices --------- -.. currentmodule:: modepy - .. autofunction:: equidistant_nodes .. autofunction:: warp_and_blend_nodes @@ -50,6 +53,8 @@ THE SOFTWARE. """ +# }}} + import numpy as np import numpy.linalg as la diff --git a/modepy/shapes.py b/modepy/shapes.py index 7e702f72..f2c9ac3d 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -165,6 +165,27 @@ The order of the vertices in the hypercubes follows binary counting in ``tsr`` (i.e. in reverse axis order). For example, in 3D, ``A, B, C, D, ...`` is ``000, 001, 010, 011, ...``. + +Redirections to Canonical Names +------------------------------- + +.. currentmodule:: modepy.shapes + +.. class:: Shape + + See :class:`modepy.Shape`. + +.. class:: Face + + See :class:`modepy.Face`. + +.. class:: Simplex + + See :class:`modepy.Simplex`. + +.. class:: Hypercube + + See :class:`modepy.Hypercube`. """ # }}} From a15fcfa7470f124fff6d70387b7068d70807cfda Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 18:39:22 -0600 Subject: [PATCH 31/68] Fix quadrature_for_shape docstring --- modepy/quadrature/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 064ce5f2..7a663fb4 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -130,7 +130,7 @@ def __init__(self, N, dims, backend=None): # noqa: N803 def quadrature_for_shape(shape: Shape, order: int) -> Quadrature: """ :returns: a :class:`~modepy.Quadrature` that is exact up to *order* - (as passed to the functions in :mod:`modepy.modes`) :math:`2 N + 1`. + (as passed to the functions in :mod:`modepy.modes`) :math:`2 N + 1`. """ raise NotImplementedError(type(shape).__name__) From 8ffc3d4f4ef50d07340c2ea6e1194fb1a92ebdc4 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 18:50:43 -0600 Subject: [PATCH 32/68] Eliminate references to (removed) get_node_tuples --- modepy/modes.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index 96bb0582..5f5ea779 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -482,7 +482,6 @@ def simplex_onb_with_mode_ids(dims, n): DeprecationWarning, stacklevel=2) if dims == 1: - # FIXME: should also use get_node_tuples mode_ids = tuple(range(n+1)) return mode_ids, tuple(partial(jacobi, 0, 0, i) for i in mode_ids) else: @@ -576,8 +575,8 @@ def simplex_monomial_basis_with_mode_ids(dims, n): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from modepy.shapes import get_node_tuples - mode_ids = get_node_tuples(Simplex(dims), n) + from modepy.nodes import node_tuples_for_shape + mode_ids = node_tuples_for_shape(Simplex(dims), n) return mode_ids, tuple(partial(monomial, order) for order in mode_ids) @@ -692,8 +691,8 @@ def tensor_product_basis(dims, basis_1d): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from modepy.shapes import Hypercube, get_node_tuples - mode_ids = get_node_tuples(Hypercube(dims), len(basis_1d)) + from modepy.nodes import node_tuples_for_shape + mode_ids = node_tuples_for_shape(Hypercube(dims), len(basis_1d)) return tuple( _TensorProductBasisFunction(order, [basis_1d[i] for i in order]) @@ -715,8 +714,8 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): DeprecationWarning, stacklevel=2) from pytools import wandering_element - from modepy.shapes import Hypercube, get_node_tuples - mode_ids = get_node_tuples(Hypercube(dims), len(basis_1d)) + from modepy.nodes import node_tuples_for_shape + mode_ids = node_tuples_for_shape(Hypercube(dims), len(basis_1d)) func = (basis_1d, grad_basis_1d) return tuple( From 9061bcc9f46232960839c597fbdf330e1a2f8334 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 19:03:55 -0600 Subject: [PATCH 33/68] Fix zerod_basis --- modepy/modes.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index 5f5ea779..bce642c6 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -452,10 +452,6 @@ def diff_monomial(r, o): # {{{ DEPRECATED dimension-independent interface for simplices -def zerod_basis(x): - return 1 + 0*x[()] - - def simplex_onb_with_mode_ids(dims, n): """Return a list of orthonormal basis functions in dimension *dims* of maximal total degree *n*. @@ -848,6 +844,12 @@ def monomial_basis_for_shape(shape: Shape, order: int) -> Basis: # }}} +def zerod_basis(x): + assert len(x) == 0 + x_sub = np.ones(x.shape[1:], x.dtype) + return 1 + x_sub + + # {{{ shape: simplex def _pkdo_1d(order, r): From bceb113d52e2e75325bb5457d9de7f3cece2397f Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Fri, 27 Nov 2020 19:04:08 -0600 Subject: [PATCH 34/68] Remove spurious import from modepy.nodes --- modepy/nodes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modepy/nodes.py b/modepy/nodes.py index 1e41540a..d1fff600 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -410,8 +410,7 @@ def _(shape: Simplex, order: int): @edge_clustered_nodes_for_shape.register(Simplex) def _(shape: Simplex, order: int): - import modepy as mp - return mp.warp_and_blend_nodes(shape.dim, order) + return warp_and_blend_nodes(shape.dim, order) @random_nodes_for_shape.register(Simplex) From 299b32ffb7062b6ab8eea7738f5c87fb3d80b958 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 15:43:45 -0600 Subject: [PATCH 35/68] Remove an extraneous import --- modepy/nodes.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modepy/nodes.py b/modepy/nodes.py index d1fff600..2d939d16 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -452,8 +452,7 @@ def _(shape: Hypercube, order: int): @edge_clustered_nodes_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): - import modepy as mp - return mp.legendre_gauss_lobatto_tensor_product_nodes(shape.dim, order) + return legendre_gauss_lobatto_tensor_product_nodes(shape.dim, order) @random_nodes_for_shape.register(Hypercube) From c75d79981dde5a601bb2780f3d8304062c3a8225 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 15:46:29 -0600 Subject: [PATCH 36/68] Make new section for submeshes --- modepy/tools.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modepy/tools.py b/modepy/tools.py index 88ac5928..015425ec 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -8,6 +8,9 @@ .. autofunction:: barycentric_to_unit .. autofunction:: unit_to_barycentric .. autofunction:: barycentric_to_equilateral + +Submeshes +--------- .. autofunction:: submesh_for_shape Interpolation quality From 92e3e8350e6f5ab9d618bdb6c323440bf95484d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Mon, 30 Nov 2020 15:50:09 -0600 Subject: [PATCH 37/68] Doc suggestions for refactor-shapes from review by @alexfikl Co-authored-by: Alex Fikl --- modepy/matrices.py | 2 +- modepy/shapes.py | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/modepy/matrices.py b/modepy/matrices.py index a9fbf2d4..16eab405 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -161,7 +161,7 @@ def differentiation_matrices(basis, grad_basis, nodes, from_nodes=None): :arg basis: A sequence of basis functions accepting arrays of shape *(dims, npts)*, - like those returned by :func:`modepy.orthonormal_basis_for_shape`. + like those returned by :func:`modepy.Basis.functions`. :arg grad_basis: A sequence of functions returning the gradients of *basis*, like those in :attr:`modepy.Basis.gradients`. diff --git a/modepy/shapes.py b/modepy/shapes.py index f2c9ac3d..2596ac7e 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -237,7 +237,7 @@ class Shape: @singledispatch def biunit_vertices_for_shape(shape: Shape): """ - :returns: a :class:`~numpy.ndarray` of shape `(dim, nvertices)`. + :returns: an :class:`~numpy.ndarray` of shape `(dim, nvertices)`. """ raise NotImplementedError(type(shape).__name__) @@ -247,17 +247,21 @@ class Face: """Inherits from :class:`Shape`. .. attribute:: volume_shape - The volume_shape :class:`Shape` from which this face descends. + + The volume :class:`Shape` from which this face descends. .. attribute:: face_index + The face index in :attr:`volume_shape` of this face. .. attribute:: volume_vertex_indices - a tuple of indices into the vertices returned by + + A tuple of indices into the vertices returned by :func:`biunit_vertices_for_shape` for the :attr:`volume_shape`. .. attribute:: map_to_volume - a :class:`~collections.abc.Callable` that takes an array of + + A :class:`~collections.abc.Callable` that takes an array of size `(dim, nnodes)` of unit nodes on the face represented by *face_vertices* and maps them to the :attr:`volume_shape`. """ From 5e3550a1f7c81f7b141789c55b2e55eaebae7028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Mon, 30 Nov 2020 15:52:31 -0600 Subject: [PATCH 38/68] Cast volume_vertex_indices to tuple in _SimplexFace Co-authored-by: Alex Fikl --- modepy/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modepy/shapes.py b/modepy/shapes.py index 2596ac7e..7ce029eb 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -329,7 +329,7 @@ def _(shape: Simplex): _SimplexFace( dim=shape.dim-1, volume_shape=shape, face_index=iface, - volume_vertex_indices=fvi, + volume_vertex_indices=tuple(fvi), map_to_volume=partial(_simplex_face_to_vol_map, vertices[:, fvi])) for iface, fvi in enumerate(face_vertex_indices)] From 22ae991d53e31cdf34c024b8cd85d2b380b93f5c Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 16:18:55 -0600 Subject: [PATCH 39/68] Remove whitespace: placate flake8 --- modepy/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modepy/shapes.py b/modepy/shapes.py index 7ce029eb..62718a4e 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -260,7 +260,7 @@ class Face: :func:`biunit_vertices_for_shape` for the :attr:`volume_shape`. .. attribute:: map_to_volume - + A :class:`~collections.abc.Callable` that takes an array of size `(dim, nnodes)` of unit nodes on the face represented by *face_vertices* and maps them to the :attr:`volume_shape`. From 7f4e1a108247c7097f8e8087a17a9dc63d7f1134 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 17:06:11 -0600 Subject: [PATCH 40/68] Generalize TensorProductBasis for nD inhomogeneity --- modepy/modes.py | 65 ++++++++++++++++++++++++++++++++-------------- test/test_modes.py | 39 ++++++++++++++++++---------- 2 files changed, 72 insertions(+), 32 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index bce642c6..2c41651e 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -85,6 +85,11 @@ .. autofunction:: monomial .. autofunction:: grad_monomial +Tensor product adapter +---------------------- + +.. autoclass:: TensorProductBasis + Conversion to Symbolic ---------------------- .. autofunction:: symbolicize_function @@ -942,16 +947,32 @@ def _(shape: Simplex, order: int): # {{{ shape: hypercube -class _TensorProductBasis(Basis): - def __init__(self, dim, basis_1d, grad_basis_1d, orth_weight): - self._dim = dim - self._basis_1d = basis_1d - self._grad_basis_1d = grad_basis_1d +class TensorProductBasis(Basis): + """Adapts multiple one-dimensional bases into a tensor product basis. + + .. automethod:: __init__ + """ + + def __init__(self, bases_1d, grad_bases_1d, orth_weight): + """ + :arg bases_1d: a sequence (one entry per axis/dimension) + of sequences (representing the basis) of 1D functions + representing the approximation basis. + :arg grad_bases_1d: a sequence (one entry per axis/dimension) + representing the derivatives of *bases_1d*. + """ + self._bases_1d = bases_1d + self._grad_bases_1d = grad_bases_1d self._orth_weight = orth_weight - @property - def _order(self): - return len(self._basis_1d)-1 + if len(bases_1d) != len(grad_bases_1d): + raise ValueError("bases_1d and grad_bases_1d must have the same length") + + for i, (b, gb) in enumerate(zip(bases_1d, grad_bases_1d)): + if len(b) != len(gb): + raise ValueError( + f"bases_1d[{i}] and grad_bases_1d[{i}] " + "must have the same length") def orthonormality_weight(self): if self._orth_weight is None: @@ -959,25 +980,31 @@ def orthonormality_weight(self): else: return self._orth_weight + @property + def _dim(self): + return len(self._bases_1d) + @property def mode_ids(self): from pytools import generate_nonnegative_integer_tuples_below as gnitb - return tuple(gnitb(self._order+1, self._dim)) + return tuple(gnitb([len(b) for b in self._bases_1d])) @property def functions(self): return tuple( _TensorProductBasisFunction(mid, - [self._basis_1d[i] for i in mid]) - for mid in self.mode_ids) + [basis[i] for i in mid]) + for mid, basis in zip(self.mode_ids, self._bases_1d)) @property def gradients(self): from pytools import wandering_element - func = (self._basis_1d, self._grad_basis_1d) + func = (self._bases_1d, self._grad_bases_1d) return tuple( _TensorProductGradientBasisFunction(mid, [ - [func[i][k] for i, k in zip(iderivative, mid)] + [func[is_deriv][iaxis][mid_i] + for iaxis, (is_deriv, mid_i) in + enumerate(zip(iderivative, mid))] for iderivative in wandering_element(self._dim) ]) for mid in self.mode_ids) @@ -985,9 +1012,9 @@ def gradients(self): @orthonormal_basis_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): - return _TensorProductBasis(shape.dim, - [partial(jacobi, 0, 0, n) for n in range(order + 1)], - [partial(grad_jacobi, 0, 0, n) for n in range(order + 1)], + return TensorProductBasis( + [[partial(jacobi, 0, 0, n) for n in range(order + 1)]] * shape.dim, + [[partial(grad_jacobi, 0, 0, n) for n in range(order + 1)]] * shape.dim, orth_weight=1) @@ -1009,9 +1036,9 @@ def _grad_monomial_1d(order, r): @monomial_basis_for_shape.register(Hypercube) def _(shape: Hypercube, order: int): - return _TensorProductBasis(shape.dim, - [partial(_monomial_1d, n) for n in range(order + 1)], - [partial(_grad_monomial_1d, n) for n in range(order + 1)], + return TensorProductBasis( + [[partial(_monomial_1d, n) for n in range(order + 1)]] * shape.dim, + [[partial(_grad_monomial_1d, n) for n in range(order + 1)]] * shape.dim, orth_weight=None) # }}} diff --git a/test/test_modes.py b/test/test_modes.py index 23abd091..8e0dcb33 100644 --- a/test/test_modes.py +++ b/test/test_modes.py @@ -21,6 +21,7 @@ """ +from functools import partial import numpy as np import numpy.linalg as la import pytest @@ -109,25 +110,37 @@ def test_orthogonality(shape, order, ebound): # print(order, maxerr) -@pytest.mark.parametrize("shape", [ - shp.Simplex(1), - shp.Simplex(2), - shp.Simplex(3), - shp.Hypercube(1), - shp.Hypercube(2), - shp.Hypercube(3), - ]) +def get_inhomogeneous_tensor_prod_basis(shape, _): + assert isinstance(shape, md.Hypercube) + orders = (3, 5, 7)[:shape.dim] + + return md.TensorProductBasis( + [[partial(md.jacobi, 0, 0, n) for n in range(o)] + for o in orders], + [[partial(md.grad_jacobi, 0, 0, n) for n in range(o)] + for o in orders], + orth_weight=1) + + +@pytest.mark.parametrize("dim", [1, 2, 3]) @pytest.mark.parametrize("order", [5, 8]) -@pytest.mark.parametrize("basis_getter", [ - (md.basis_for_shape), - (md.orthonormal_basis_for_shape), - (md.monomial_basis_for_shape), +@pytest.mark.parametrize(("shape_cls", "basis_getter"), [ + (shp.Simplex, md.basis_for_shape), + (shp.Simplex, md.orthonormal_basis_for_shape), + (shp.Simplex, md.monomial_basis_for_shape), + + (shp.Hypercube, md.basis_for_shape), + (shp.Hypercube, md.orthonormal_basis_for_shape), + (shp.Hypercube, md.monomial_basis_for_shape), + + (shp.Hypercube, get_inhomogeneous_tensor_prod_basis), ]) -def test_basis_grad(shape, order, basis_getter): +def test_basis_grad(dim, shape_cls, order, basis_getter): """Do a simplistic FD-style check on the gradients of the basis.""" h = 1.0e-4 + shape = shape_cls(dim) rng = np.random.Generator(np.random.PCG64(17)) basis = basis_getter(shape, order) From 347027b370555454c7c060ae9164816b58672bb9 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 17:07:13 -0600 Subject: [PATCH 41/68] Make TensorProductBasis public in root module --- modepy/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modepy/__init__.py b/modepy/__init__.py index 9ff1145d..695a49e7 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -36,7 +36,7 @@ tensor_product_basis, grad_tensor_product_basis, legendre_tensor_product_basis, grad_legendre_tensor_product_basis, - Basis, BasisNotOrthonormal, + Basis, BasisNotOrthonormal, TensorProductBasis, basis_for_shape, orthonormal_basis_for_shape, monomial_basis_for_shape) from modepy.nodes import ( equidistant_nodes, warp_and_blend_nodes, @@ -83,7 +83,7 @@ "simplex_best_available_basis", "grad_simplex_best_available_basis", "tensor_product_basis", "grad_tensor_product_basis", "legendre_tensor_product_basis", "grad_legendre_tensor_product_basis", - "Basis", "BasisNotOrthonormal", + "Basis", "BasisNotOrthonormal", "TensorProductBasis", "basis_for_shape", "orthonormal_basis_for_shape", "monomial_basis_for_shape", "equidistant_nodes", "warp_and_blend_nodes", From c872de9a19cb34319be701d657060076e059d0c4 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 17:11:38 -0600 Subject: [PATCH 42/68] Do not claim that Face subclasses Shape --- modepy/shapes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modepy/shapes.py b/modepy/shapes.py index 62718a4e..ba37b771 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -244,7 +244,8 @@ def biunit_vertices_for_shape(shape: Shape): @dataclass(frozen=True) class Face: - """Inherits from :class:`Shape`. + """Mix-in to be used with a concrete :class:`Shape` subclass to represent + geometry information about a face of a shape. .. attribute:: volume_shape From 676d38aadedb18b181ba8cd47d5fc6aa1a690a09 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 17:21:23 -0600 Subject: [PATCH 43/68] Fix TensorProductBasis facepalm --- modepy/modes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index 2c41651e..f75e9df9 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -993,8 +993,9 @@ def mode_ids(self): def functions(self): return tuple( _TensorProductBasisFunction(mid, - [basis[i] for i in mid]) - for mid, basis in zip(self.mode_ids, self._bases_1d)) + [self._bases_1d[iaxis][mid_i] + for iaxis, mid_i in enumerate(mid)]) + for mid in self.mode_ids) @property def gradients(self): From 9a282c04b7fda397e6cd4a3d9366643021cf4da9 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 19:37:38 -0600 Subject: [PATCH 44/68] Introduce function spaces as first-class objects --- doc/modes.rst | 2 + modepy/__init__.py | 23 +++-- modepy/matrices.py | 30 +++--- modepy/modal_decay.py | 2 +- modepy/modes.py | 137 ++++++++++++++------------- modepy/nodes.py | 99 ++++++++----------- modepy/quadrature/__init__.py | 44 +++++---- modepy/quadrature/xiao_gimbutas.py | 2 +- modepy/spaces.py | 146 +++++++++++++++++++++++++++++ modepy/tools.py | 14 ++- test/test_modes.py | 78 ++++++++------- test/test_tools.py | 124 ++++++++++++------------ 12 files changed, 430 insertions(+), 271 deletions(-) create mode 100644 modepy/spaces.py diff --git a/doc/modes.rst b/doc/modes.rst index 25465d69..21a75af7 100644 --- a/doc/modes.rst +++ b/doc/modes.rst @@ -1,4 +1,6 @@ Modes (Basis functions) ======================= +.. automodule:: modepy.spaces + .. automodule:: modepy.modes diff --git a/modepy/__init__.py b/modepy/__init__.py index 695a49e7..ff8d212b 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -27,6 +27,8 @@ biunit_vertices_for_shape, faces_for_shape ) +from modepy.spaces import ( + FunctionSpace, PN, QN, space_for_shape) from modepy.modes import ( jacobi, grad_jacobi, simplex_onb, grad_simplex_onb, simplex_onb_with_mode_ids, @@ -35,15 +37,16 @@ simplex_best_available_basis, grad_simplex_best_available_basis, tensor_product_basis, grad_tensor_product_basis, legendre_tensor_product_basis, grad_legendre_tensor_product_basis, + symbolicize_function, Basis, BasisNotOrthonormal, TensorProductBasis, - basis_for_shape, orthonormal_basis_for_shape, monomial_basis_for_shape) + basis_for_space, orthonormal_basis_for_space, monomial_basis_for_space) from modepy.nodes import ( equidistant_nodes, warp_and_blend_nodes, tensor_product_nodes, legendre_gauss_lobatto_tensor_product_nodes, - node_count_for_shape, node_tuples_for_shape, - equispaced_nodes_for_shape, edge_clustered_nodes_for_shape, + node_tuples_for_space, + equispaced_nodes_for_space, edge_clustered_nodes_for_space, random_nodes_for_shape) from modepy.matrices import (vandermonde, resampling_matrix, differentiation_matrices, @@ -54,7 +57,7 @@ from modepy.quadrature import ( Quadrature, QuadratureRuleUnavailable, TensorProductQuadrature, LegendreGaussTensorProductQuadrature, - quadrature_for_shape) + quadrature_for_space) from modepy.quadrature.jacobi_gauss import ( JacobiGaussQuadrature, LegendreGaussQuadrature, ChebyshevGaussQuadrature, GaussGegenbauerQuadrature, @@ -76,6 +79,8 @@ "Shape", "Face", "Simplex", "Hypercube", "biunit_vertices_for_shape", "faces_for_shape", + "FunctionSpace", "PN", "QN", "space_for_shape", + "jacobi", "grad_jacobi", "simplex_onb", "grad_simplex_onb", "simplex_onb_with_mode_ids", "simplex_monomial_basis", "grad_simplex_monomial_basis", @@ -83,13 +88,15 @@ "simplex_best_available_basis", "grad_simplex_best_available_basis", "tensor_product_basis", "grad_tensor_product_basis", "legendre_tensor_product_basis", "grad_legendre_tensor_product_basis", + "symbolicize_function", + "Basis", "BasisNotOrthonormal", "TensorProductBasis", - "basis_for_shape", "orthonormal_basis_for_shape", "monomial_basis_for_shape", + "basis_for_space", "orthonormal_basis_for_space", "monomial_basis_for_space", "equidistant_nodes", "warp_and_blend_nodes", "tensor_product_nodes", "legendre_gauss_lobatto_tensor_product_nodes", - "node_count_for_shape", "node_tuples_for_shape", - "edge_clustered_nodes_for_shape", "equispaced_nodes_for_shape", + "node_tuples_for_space", + "edge_clustered_nodes_for_space", "equispaced_nodes_for_space", "random_nodes_for_shape", "vandermonde", "resampling_matrix", "differentiation_matrices", @@ -100,7 +107,7 @@ "Quadrature", "QuadratureRuleUnavailable", "TensorProductQuadrature", "LegendreGaussTensorProductQuadrature", - "quadrature_for_shape", + "quadrature_for_space", "JacobiGaussQuadrature", "LegendreGaussQuadrature", "GaussLegendreQuadrature", "ChebyshevGaussQuadrature", diff --git a/modepy/matrices.py b/modepy/matrices.py index 16eab405..523f865d 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -25,7 +25,9 @@ import numpy as np import numpy.linalg as la -import modepy.shapes as shp +from modepy.shapes import Face +from modepy.spaces import PN +from modepy.quadrature import Quadrature __doc__ = r""" @@ -110,7 +112,7 @@ def resampling_matrix(basis, new_nodes, old_nodes, least_squares_ok=False): :arg basis: A sequence of basis functions accepting arrays of shape *(dims, npts)*, like those returned by - :func:`modepy.orthonormal_basis_for_shape`. + :func:`modepy.orthonormal_basis_for_space`. :arg new_nodes: An array of shape *(dims, n_new_nodes)* :arg old_nodes: An array of shape *(dims, n_old_nodes)* :arg least_squares_ok: If *False*, then nodal values at *old_nodes* @@ -243,29 +245,26 @@ def mass_matrix(basis, nodes): return la.inv(inverse_mass_matrix(basis, nodes)) -def modal_mass_matrix_for_face(face, trial_functions, test_functions, order): +def modal_mass_matrix_for_face(face: Face, face_quad: Quadrature, + trial_functions, test_functions): """ .. versionadded:: 2020.3 """ - from modepy.quadrature import quadrature_for_shape - quad = quadrature_for_shape(face, order) - - assert quad.exact_to > order*2 - mapped_nodes = face.map_to_volume(quad.nodes) + mapped_nodes = face.map_to_volume(face_quad.nodes) result = np.empty((len(test_functions), len(trial_functions))) for i, test_f in enumerate(test_functions): test_vals = test_f(mapped_nodes) for j, trial_f in enumerate(trial_functions): - result[i, j] = (test_vals*trial_f(quad.nodes)).dot(quad.weights) + result[i, j] = (test_vals*trial_f(face_quad.nodes)) @ face_quad.weights return result -def nodal_mass_matrix_for_face(face: shp.Face, trial_functions, test_functions, - volume_nodes, face_nodes, order): +def nodal_mass_matrix_for_face(face: Face, face_quad: Quadrature, + trial_functions, test_functions, volume_nodes, face_nodes): """ .. versionadded :: 2020.3 """ @@ -273,7 +272,7 @@ def nodal_mass_matrix_for_face(face: shp.Face, trial_functions, test_functions, vol_vdm = vandermonde(test_functions, volume_nodes) modal_fmm = modal_mass_matrix_for_face( - face, trial_functions, test_functions, order) + face, face_quad, trial_functions, test_functions) return la.inv(vol_vdm.T).dot(modal_fmm).dot(la.pinv(face_vdm)) @@ -296,12 +295,11 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, test_basis=None): test_basis = trial_basis vol_dims = face_vertices.shape[0] - face_shape = shp.Simplex(vol_dims - 1) - from modepy.quadrature import quadrature_for_shape - quad = quadrature_for_shape(face_shape, order) + from modepy.quadrature import quadrature_for_space + quad = quadrature_for_space(PN(vol_dims - 1, order*2)) - assert quad.exact_to > order*2 + assert quad.exact_to >= order*2 from modepy.shapes import _simplex_face_to_vol_map mapped_nodes = _simplex_face_to_vol_map(face_vertices, quad.nodes) diff --git a/modepy/modal_decay.py b/modepy/modal_decay.py index 08e042a0..8d456378 100644 --- a/modepy/modal_decay.py +++ b/modepy/modal_decay.py @@ -25,7 +25,7 @@ import numpy.linalg as la __doc__ = """Estimate the smoothness of a function represented in a basis -returned by :func:`modepy.orthonormal_basis_for_shape`. +returned by :func:`modepy.orthonormal_basis_for_space`. The method implemented in this module follows this article: diff --git a/modepy/modes.py b/modepy/modes.py index f75e9df9..52680048 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -29,7 +29,7 @@ import numpy as np -from modepy.shapes import Shape, Simplex, Hypercube +from modepy.spaces import FunctionSpace, PN, QN __doc__ = """This functionality provides sets of basis functions for the @@ -37,15 +37,15 @@ .. currentmodule:: modepy -Generic Basis Retrieval based on :mod:`~modepy.shapes` ------------------------------------------------------- +Basis Retrieval +--------------- .. autoexception:: BasisNotOrthonormal .. autoclass:: Basis -.. autofunction:: basis_for_shape -.. autofunction:: orthonormal_basis_for_shape -.. autofunction:: monomial_basis_for_shape +.. autofunction:: basis_for_space +.. autofunction:: orthonormal_basis_for_space +.. autofunction:: monomial_basis_for_space Jacobi polynomials ------------------ @@ -55,6 +55,15 @@ .. autofunction:: jacobi(alpha, beta, n, x) .. autofunction:: grad_jacobi(alpha, beta, n, x) +Conversion to Symbolic +---------------------- +.. autofunction:: symbolicize_function + +Tensor product adapter +---------------------- + +.. autoclass:: TensorProductBasis + PKDO basis functions -------------------- @@ -85,15 +94,6 @@ .. autofunction:: monomial .. autofunction:: grad_monomial -Tensor product adapter ----------------------- - -.. autoclass:: TensorProductBasis - -Conversion to Symbolic ----------------------- -.. autofunction:: symbolicize_function - Redirections to Canonical Names ------------------------------- @@ -478,7 +478,7 @@ def simplex_onb_with_mode_ids(dims, n): .. versionadded:: 2018.1 """ warn("simplex_onb_with_mode_ids is deprecated. " - "Use orthonormal_basis_for_shape instead. " + "Use orthonormal_basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) @@ -511,7 +511,7 @@ def simplex_onb(dims, n): Made return value a tuple, to make bases hashable. """ warn("simplex_onb is deprecated. " - "Use orthonormal_basis_for_shape instead. " + "Use orthonormal_basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) @@ -540,7 +540,7 @@ def grad_simplex_onb(dims, n): Made return value a tuple, to make bases hashable. """ warn("grad_simplex_onb is deprecated. " - "Use orthonormal_basis_for_shape instead. " + "Use orthonormal_basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) @@ -572,12 +572,12 @@ def simplex_monomial_basis_with_mode_ids(dims, n): .. versionadded:: 2018.1 """ warn("simplex_monomial_basis_with_mode_ids is deprecated. " - "Use monomial_basis_for_shape instead. " + "Use monomial_basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from modepy.nodes import node_tuples_for_shape - mode_ids = node_tuples_for_shape(Simplex(dims), n) + from modepy.nodes import node_tuples_for_space + mode_ids = node_tuples_for_space(PN(dims, n)) return mode_ids, tuple(partial(monomial, order) for order in mode_ids) @@ -613,7 +613,7 @@ def grad_simplex_monomial_basis(dims, n): """ warn("grad_simplex_monomial_basis_with_mode_ids is deprecated. " - "Use monomial_basis_for_shape instead. " + "Use monomial_basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) @@ -624,20 +624,20 @@ def grad_simplex_monomial_basis(dims, n): def simplex_best_available_basis(dims, n): warn("simplex_best_available_basis is deprecated. " - "Use basis_for_shape instead. " + "Use basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - return basis_for_shape(Simplex(dims), n).functions + return basis_for_space(PN(dims, n)).functions def grad_simplex_best_available_basis(dims, n): warn("grad_simplex_best_available_basis is deprecated. " - "Use basis_for_shape instead. " + "Use basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - return basis_for_shape(Simplex(dims), n).gradients + return basis_for_space(PN(dims, n)).gradients # }}} @@ -692,8 +692,8 @@ def tensor_product_basis(dims, basis_1d): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - from modepy.nodes import node_tuples_for_shape - mode_ids = node_tuples_for_shape(Hypercube(dims), len(basis_1d)) + from modepy.nodes import node_tuples_for_space + mode_ids = node_tuples_for_space(QN(dims, len(basis_1d))) return tuple( _TensorProductBasisFunction(order, [basis_1d[i] for i in order]) @@ -715,8 +715,8 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): DeprecationWarning, stacklevel=2) from pytools import wandering_element - from modepy.nodes import node_tuples_for_shape - mode_ids = node_tuples_for_shape(Hypercube(dims), len(basis_1d)) + from modepy.nodes import node_tuples_for_space + mode_ids = node_tuples_for_space(QN(dims, len(basis_1d))) func = (basis_1d, grad_basis_1d) return tuple( @@ -729,7 +729,7 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): def legendre_tensor_product_basis(dims, order): warn("legendre_tensor_product_basis is deprecated. " - "Use orthonormal_basis_for_shape instead. " + "Use orthonormal_basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) @@ -739,7 +739,7 @@ def legendre_tensor_product_basis(dims, order): def grad_legendre_tensor_product_basis(dims, order): warn("grad_legendre_tensor_product_basis is deprecated. " - "Use orthonormal_basis_for_shape instead. " + "Use orthonormal_basis_for_space instead. " "This function will go away in 2022.", DeprecationWarning, stacklevel=2) @@ -830,21 +830,21 @@ def gradients(self): # }}} -# {{{ shape-based basis retrieval +# {{{ space-based basis retrieval @singledispatch -def basis_for_shape(shape: Shape, order: int) -> Basis: - raise NotImplementedError(type(shape).__name__) +def basis_for_space(space: FunctionSpace) -> Basis: + raise NotImplementedError(type(space).__name__) @singledispatch -def orthonormal_basis_for_shape(shape: Shape, order: int) -> Basis: - raise NotImplementedError(type(shape).__name__) +def orthonormal_basis_for_space(space: FunctionSpace) -> Basis: + raise NotImplementedError(type(space).__name__) @singledispatch -def monomial_basis_for_shape(shape: Shape, order: int) -> Basis: - raise NotImplementedError(type(shape).__name__) +def monomial_basis_for_space(space: FunctionSpace) -> Basis: + raise NotImplementedError(type(space).__name__) # }}} @@ -855,7 +855,7 @@ def zerod_basis(x): return 1 + x_sub -# {{{ shape: simplex +# {{{ PN bases def _pkdo_1d(order, r): i, = order @@ -874,6 +874,9 @@ def __init__(self, dim, order): self._dim = dim self._order = order + assert isinstance(dim, int) + assert isinstance(order, int) + @property def mode_ids(self): from pytools import \ @@ -898,7 +901,7 @@ def functions(self): elif self._dim == 3: return tuple(partial(pkdo_3d, mid) for mid in self.mode_ids) else: - raise NotImplementedError("basis in {self._dim} dimensions") + raise NotImplementedError(f"basis in {self._dim} dimensions") @property def gradients(self): @@ -909,7 +912,7 @@ def gradients(self): elif self._dim == 3: return tuple(partial(grad_pkdo_3d, mid) for mid in self.mode_ids) else: - raise NotImplementedError("gradient in {self._dim} dimensions") + raise NotImplementedError(f"gradient in {self._dim} dimensions") class _SimplexMonomialBasis(_SimplexBasis): @@ -925,27 +928,27 @@ def gradients(self): return tuple(partial(grad_monomial, mid) for mid in self.mode_ids) -@basis_for_shape.register(Simplex) -def _(shape: Simplex, order: int): - if shape.dim <= 3: - return _SimplexONB(shape.dim, order) +@basis_for_space.register(PN) +def _(space: PN): + if space.spatial_dim <= 3: + return _SimplexONB(space.spatial_dim, space.order) else: - return _SimplexMonomialBasis(shape.dim, order) + return _SimplexMonomialBasis(space.spatial_dim, space.order) -@orthonormal_basis_for_shape.register(Simplex) -def _(shape: Simplex, order: int): - return _SimplexONB(shape.dim, order) +@orthonormal_basis_for_space.register(PN) +def _(space: PN): + return _SimplexONB(space.spatial_dim, space.order) -@monomial_basis_for_shape.register(Simplex) -def _(shape: Simplex, order: int): - return _SimplexMonomialBasis(shape.dim, order) +@monomial_basis_for_space.register(PN) +def _(space: PN): + return _SimplexMonomialBasis(space.spatial_dim, space.order) # }}} -# {{{ shape: hypercube +# {{{ QN bases class TensorProductBasis(Basis): """Adapts multiple one-dimensional bases into a tensor product basis. @@ -1011,17 +1014,19 @@ def gradients(self): for mid in self.mode_ids) -@orthonormal_basis_for_shape.register(Hypercube) -def _(shape: Hypercube, order: int): +@orthonormal_basis_for_space.register(QN) +def _(space: QN): + order = space.order + dim = space.spatial_dim return TensorProductBasis( - [[partial(jacobi, 0, 0, n) for n in range(order + 1)]] * shape.dim, - [[partial(grad_jacobi, 0, 0, n) for n in range(order + 1)]] * shape.dim, + [[partial(jacobi, 0, 0, n) for n in range(order + 1)]] * dim, + [[partial(grad_jacobi, 0, 0, n) for n in range(order + 1)]] * dim, orth_weight=1) -@basis_for_shape.register(Hypercube) -def _(shape: Hypercube, order: int): - return orthonormal_basis_for_shape(shape, order) +@basis_for_space.register(QN) +def _(space: QN): + return orthonormal_basis_for_space(space) def _monomial_1d(order, r): @@ -1035,11 +1040,13 @@ def _grad_monomial_1d(order, r): return order*r**(order-1) -@monomial_basis_for_shape.register(Hypercube) -def _(shape: Hypercube, order: int): +@monomial_basis_for_space.register(QN) +def _(space: QN): + order = space.order + dim = space.spatial_dim return TensorProductBasis( - [[partial(_monomial_1d, n) for n in range(order + 1)]] * shape.dim, - [[partial(_grad_monomial_1d, n) for n in range(order + 1)]] * shape.dim, + [[partial(_monomial_1d, n) for n in range(order + 1)]] * dim, + [[partial(_grad_monomial_1d, n) for n in range(order + 1)]] * dim, orth_weight=None) # }}} diff --git a/modepy/nodes.py b/modepy/nodes.py index 2d939d16..30094cd6 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -6,10 +6,9 @@ .. currentmodule:: modepy -.. autofunction:: node_count_for_shape -.. autofunction:: node_tuples_for_shape -.. autofunction:: equispaced_nodes_for_shape -.. autofunction:: edge_clustered_nodes_for_shape +.. autofunction:: node_tuples_for_space +.. autofunction:: equispaced_nodes_for_space +.. autofunction:: edge_clustered_nodes_for_space .. autofunction:: random_nodes_for_shape Simplices @@ -55,12 +54,14 @@ # }}} +from typing import List, Tuple import numpy as np import numpy.linalg as la from functools import singledispatch, partial from modepy.shapes import Shape, Simplex, Hypercube +from modepy.spaces import FunctionSpace, PN, QN # {{{ equidistant nodes @@ -77,11 +78,11 @@ def equidistant_nodes(dims, n, node_tuples=None): of the interpolation nodes. (see :ref:`tri-coords` and :ref:`tet-coords`) """ - shape = Simplex(dims) + space = PN(dims, n) if node_tuples is None: - node_tuples = node_tuples_for_shape(shape, n) + node_tuples = node_tuples_for_space(space) else: - if len(node_tuples) != node_count_for_shape(shape, n): + if len(node_tuples) != space.space_dim: raise ValueError("'node_tuples' list does not have the correct length") # shape: (dims, nnodes) @@ -160,11 +161,11 @@ def warp_and_blend_nodes_2d(n, node_tuples=None): except IndexError: alpha = 5/3 - shape = Simplex(2) + space = PN(2, n) if node_tuples is None: - node_tuples = node_tuples_for_shape(shape, n) + node_tuples = node_tuples_for_space(space) else: - if len(node_tuples) != node_count_for_shape(shape, n): + if len(node_tuples) != space.space_dim: raise ValueError("'node_tuples' list does not have the correct length") # shape: (2, nnodes) @@ -196,11 +197,11 @@ def warp_and_blend_nodes_3d(n, node_tuples=None): except IndexError: alpha = 1. - shape = Simplex(3) + space = PN(3, n) if node_tuples is None: - node_tuples = node_tuples_for_shape(shape, n) + node_tuples = node_tuples_for_space(space) else: - if len(node_tuples) != node_count_for_shape(shape, n): + if len(node_tuples) != space.space_dim: raise ValueError("'node_tuples' list does not have the correct length") # shape: (3, nnodes) @@ -353,26 +354,21 @@ def legendre_gauss_lobatto_tensor_product_nodes(dims, n): # }}} -# {{{ shape-based interface +# {{{ space-based interface @singledispatch -def node_count_for_shape(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) - - -@singledispatch -def node_tuples_for_shape(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) +def node_tuples_for_space(space: FunctionSpace) -> List[Tuple[int]]: + raise NotImplementedError(type(space).__name__) @singledispatch -def equispaced_nodes_for_shape(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) +def equispaced_nodes_for_space(space: FunctionSpace): + raise NotImplementedError(type(space).__name__) @singledispatch -def edge_clustered_nodes_for_shape(shape: Shape, order: int): - raise NotImplementedError(type(shape).__name__) +def edge_clustered_nodes_for_space(space: FunctionSpace): + raise NotImplementedError(type(space).__name__) @singledispatch @@ -384,33 +380,21 @@ def random_nodes_for_shape(shape: Shape, nnodes: int, rng=None): """ raise NotImplementedError(type(shape).__name__) +# }}} -# {{{ simplex - -@node_count_for_shape.register(Simplex) -def _(shape: Simplex, order: int): - try: - from math import comb # comb is v3.8+ - node_count = comb(order + shape.dim, shape.dim) - except ImportError: - from functools import reduce - from operator import mul - node_count = reduce(mul, range(order + 1, order + shape.dim + 1), 1) \ - // reduce(mul, range(1, shape.dim + 1), 1) - - return node_count +# {{{ PN -@node_tuples_for_shape.register(Simplex) -def _(shape: Simplex, order: int): +@node_tuples_for_space.register(PN) +def _(space: PN): from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitsam - return tuple(gnitsam(order, shape.dim)) + return tuple(gnitsam(space.order, space.spatial_dim)) -@edge_clustered_nodes_for_shape.register(Simplex) -def _(shape: Simplex, order: int): - return warp_and_blend_nodes(shape.dim, order) +@edge_clustered_nodes_for_space.register(PN) +def _(space: PN): + return warp_and_blend_nodes(space.spatial_dim, space.order) @random_nodes_for_shape.register(Simplex) @@ -433,26 +417,23 @@ def _(shape: Simplex, nnodes: int, rng=None): # }}} -# {{{ hypercube - -@node_count_for_shape.register(Hypercube) -def _(shape: Hypercube, order: int): - return (order + 1)**shape.dim +# {{{ QN - -@node_tuples_for_shape.register(Hypercube) -def _(shape: Hypercube, order: int): +@node_tuples_for_space.register(QN) +def _(space: QN): from pytools import \ generate_nonnegative_integer_tuples_below as gnitb - if shape.dim == 0: + # FIXME: Why? + if space.spatial_dim == 0: return ((0,),) else: - return tuple(gnitb(order, shape.dim)) + return tuple(gnitb(space.order, space.spatial_dim)) -@edge_clustered_nodes_for_shape.register(Hypercube) -def _(shape: Hypercube, order: int): - return legendre_gauss_lobatto_tensor_product_nodes(shape.dim, order) +@edge_clustered_nodes_for_space.register(QN) +def _(space: QN): + return legendre_gauss_lobatto_tensor_product_nodes( + space.spatial_dim, space.order) @random_nodes_for_shape.register(Hypercube) @@ -463,6 +444,4 @@ def _(shape: Hypercube, nnodes: int, rng=None): # }}} -# }}} - # vim: foldmethod=marker diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 7a663fb4..ded3aa93 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -3,7 +3,16 @@ .. autoclass:: Quadrature -.. autoclass:: quadrature_for_shape +.. autoclass:: quadrature_for_space + +Redirections to Canonical Names +------------------------------- + +.. currentmodule:: modepy.quadrature + +.. class:: Quadrature + + See :class:`modepy.Quadrature`. """ __copyright__ = ("Copyright (C) 2009, 2010, 2013 Andreas Kloeckner, Tim Warburton, " @@ -32,7 +41,7 @@ from functools import singledispatch import numpy as np -from modepy.shapes import Shape, Simplex, Hypercube +from modepy.spaces import FunctionSpace, PN, QN class QuadratureRuleUnavailable(RuntimeError): @@ -124,36 +133,39 @@ def __init__(self, N, dims, backend=None): # noqa: N803 dims, LegendreGaussQuadrature(N, backend=backend)) -# {{{ quadrature_for_shape +# {{{ quadrature_for_space @singledispatch -def quadrature_for_shape(shape: Shape, order: int) -> Quadrature: +def quadrature_for_space(space: FunctionSpace) -> Quadrature: """ - :returns: a :class:`~modepy.Quadrature` that is exact up to *order* - (as passed to the functions in :mod:`modepy.modes`) :math:`2 N + 1`. + :returns: a :class:`~modepy.Quadrature` that exactly integrates the functions + in *space*. """ - raise NotImplementedError(type(shape).__name__) + raise NotImplementedError(type(space).__name__) -@quadrature_for_shape.register(Simplex) -def _(shape: Simplex, order: int): +@quadrature_for_space.register(PN) +def _(space: PN): import modepy as mp try: - quad = mp.XiaoGimbutasSimplexQuadrature(2*order + 1, shape.dim) - except (mp.QuadratureRuleUnavailable, ValueError): - quad = mp.GrundmannMoellerSimplexQuadrature(order, shape.dim) + quad = mp.XiaoGimbutasSimplexQuadrature(space.order, space.spatial_dim) + except mp.QuadratureRuleUnavailable: + quad = mp.GrundmannMoellerSimplexQuadrature( + space.order//2, space.spatial_dim) + + assert quad.exact_to >= space.order return quad -@quadrature_for_shape.register(Hypercube) -def _(shape: Hypercube, order: int): +@quadrature_for_space.register(QN) +def _(space: QN): import modepy as mp - if shape.dim == 0: + if space.spatial_dim == 0: quad = mp.Quadrature(np.empty((0, 1)), np.empty((0, 1))) else: from modepy.quadrature import LegendreGaussTensorProductQuadrature - quad = LegendreGaussTensorProductQuadrature(order, shape.dim) + quad = LegendreGaussTensorProductQuadrature(space.order, space.spatial_dim) return quad # }}} diff --git a/modepy/quadrature/xiao_gimbutas.py b/modepy/quadrature/xiao_gimbutas.py index 31e7bbe7..caa98e58 100644 --- a/modepy/quadrature/xiao_gimbutas.py +++ b/modepy/quadrature/xiao_gimbutas.py @@ -64,7 +64,7 @@ def __init__(self, order, dims): elif dims == 3: from modepy.quadrature.xg_quad_data import tetrahedron_table as table else: - raise ValueError("invalid dimensionality") + raise QuadratureRuleUnavailable("invalid dimensionality") try: order_table = table[order] except KeyError: diff --git a/modepy/spaces.py b/modepy/spaces.py new file mode 100644 index 00000000..f0898242 --- /dev/null +++ b/modepy/spaces.py @@ -0,0 +1,146 @@ +""" +.. currentmodule:: modepy + +Function Spaces +--------------- + +.. autoclass:: FunctionSpace +.. autoclass:: PN +.. autoclass:: QN +.. autoclass:: space_for_shape + +Redirections to Canonical Names +------------------------------- + +.. currentmodule:: modepy.spaces + +.. class:: FunctionSpace + + See :class:`modepy.FunctionSpace`. + +.. class:: PN + + See :class:`modepy.PN`. + +.. class:: QN + + See :class:`modepy.QN`. +""" + +__copyright__ = "Copyright (C) 2020 Andreas Kloeckner" + +__license__ = """ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +""" + + +from functools import singledispatch +from modepy.shapes import Shape, Simplex, Hypercube + + +# {{{ function spaces + +class FunctionSpace: + r"""An opaque object representing a finite-dimensional function space + of functions :math:`\mathbb R^n \to :math:`\mathbb R`. + + .. attribute:: spatial_dim + + :math:`n` in the above definition, the number of spatial dimensions + in which the functions in the space operate. + + .. attribute:: space_dim + + The number of dimensions of the function space. + """ + + +class PN(FunctionSpace): + r"""The function space of polynomials with total degree :math:`N`=:attr:`order`. + + .. math:: + + P^N:=\operatorname{span}\left\{\prod_{i=1}^d x_i^{n_i}:\sum n_i\le N\right\}. + + .. automethod:: __init__ + .. attribute:: order + """ + def __init__(self, spatial_dim, order): + super().__init__() + self.spatial_dim = spatial_dim + self.order = order + + @property + def space_dim(self): + spdim = self.spatial_dim + order = self.order + try: + from math import comb # comb is v3.8+ + return comb(order + spdim, spdim) + except ImportError: + from functools import reduce + from operator import mul + return reduce(mul, range(order + 1, order + spdim + 1), 1) \ + // reduce(mul, range(1, spdim + 1), 1) + + +class QN(FunctionSpace): + r"""The function space of polynomials with maximum degree + :math:`N`=:attr:`order`: + + .. math:: + + Q^N:=\operatorname{span} + \left \{\prod_{i=1}^d x_i^{n_i}:\max n_i\le N\right\}. + + .. automethod:: __init__ + .. attribute:: order + """ + def __init__(self, spatial_dim, order): + super().__init__() + self.spatial_dim = spatial_dim + self.order = order + + @property + def space_dim(self): + return (self.order + 1)**self.spatial_dim + + +@singledispatch +def space_for_shape(shape: Shape, order: int) -> FunctionSpace: + r"""Return an unspecified instance of :class:`FunctionSpace` suitable + for approximation on *shape* attaining interpolation error of + :math:`O(h^{\text{order}+1})`. + """ + raise NotImplementedError(type(shape).__name__) + + +@space_for_shape.register(Simplex) +def _(shape: Simplex, order: int): + return PN(shape.dim, order) + + +@space_for_shape.register(Hypercube) +def _(shape: Hypercube, order: int): + return QN(shape.dim, order) + +# }}} + + +# vim: foldmethod=marker diff --git a/modepy/tools.py b/modepy/tools.py index 015425ec..b6184fda 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -267,7 +267,7 @@ def submesh_for_shape(shape: shp.Shape, node_tuples): indicating node positions inside the unit element. The returned list references indices in this list. - :func:`modepy.node_tuples_for_shape` may be used to generate *node_tuples*. + :func:`modepy.node_tuples_for_space` may be used to generate *node_tuples*. .. versionadded:: 2020.3 """ @@ -472,10 +472,14 @@ def plot_element_values(n, nodes, values, resample_n=None, def _evaluate_lebesgue_function(n, nodes, shape): huge_n = 30*n - from modepy.modes import basis_for_shape - from modepy.nodes import node_tuples_for_shape - basis = basis_for_shape(shape, n) - equi_node_tuples = node_tuples_for_shape(shape, huge_n) + from modepy.spaces import space_for_shape + from modepy.modes import basis_for_space + from modepy.nodes import node_tuples_for_space + space = space_for_shape(shape, n) + huge_space = space_for_shape(shape, huge_n) + + basis = basis_for_space(space) + equi_node_tuples = node_tuples_for_space(huge_space) equi_nodes = (np.array(equi_node_tuples, dtype=np.float64)/huge_n*2 - 1).T from modepy.matrices import vandermonde diff --git a/test/test_modes.py b/test/test_modes.py index 8e0dcb33..06588fc9 100644 --- a/test/test_modes.py +++ b/test/test_modes.py @@ -25,9 +25,7 @@ import numpy as np import numpy.linalg as la import pytest -import modepy.modes as md -import modepy.nodes as nd -import modepy.shapes as shp +import modepy as mp from pymbolic.mapper.stringifier import ( CSESplittingStringifyMapperMixin, StringifyMapper) from pymbolic.mapper.evaluator import EvaluationMapper @@ -54,7 +52,7 @@ def test_orthonormality_jacobi_1d(alpha, beta, ebound): quad = JacobiGaussQuadrature(alpha, beta, 4*max_n) from functools import partial - jac_f = [partial(md.jacobi, alpha, beta, n) for n in range(max_n)] + jac_f = [partial(mp.jacobi, alpha, beta, n) for n in range(max_n)] maxerr = 0 for i, fi in enumerate(jac_f): @@ -80,18 +78,17 @@ def test_orthonormality_jacobi_1d(alpha, beta, ebound): # (9, 2e-13), ]) @pytest.mark.parametrize("shape", [ - shp.Simplex(2), - shp.Simplex(3), - shp.Hypercube(2), - shp.Hypercube(3), + mp.Simplex(2), + mp.Simplex(3), + mp.Hypercube(2), + mp.Hypercube(3), ]) def test_orthogonality(shape, order, ebound): """Test orthogonality of ONBs using cubature.""" - from modepy.quadrature import quadrature_for_shape - - cub = quadrature_for_shape(shape, order) - basis = md.orthonormal_basis_for_shape(shape, order) + qspace = mp.space_for_shape(shape, 2*order) + cub = mp.quadrature_for_space(qspace) + basis = mp.orthonormal_basis_for_space(mp.space_for_shape(shape, order)) maxerr = 0 for i, f in enumerate(basis.functions): @@ -110,14 +107,15 @@ def test_orthogonality(shape, order, ebound): # print(order, maxerr) -def get_inhomogeneous_tensor_prod_basis(shape, _): - assert isinstance(shape, md.Hypercube) - orders = (3, 5, 7)[:shape.dim] +def get_inhomogeneous_tensor_prod_basis(space): + # FIXME: Yuck. A total lie. Not a basis for the space at all. + assert isinstance(space, mp.QN) + orders = (3, 5, 7)[:space.spatial_dim] - return md.TensorProductBasis( - [[partial(md.jacobi, 0, 0, n) for n in range(o)] + return mp.TensorProductBasis( + [[partial(mp.jacobi, 0, 0, n) for n in range(o)] for o in orders], - [[partial(md.grad_jacobi, 0, 0, n) for n in range(o)] + [[partial(mp.grad_jacobi, 0, 0, n) for n in range(o)] for o in orders], orth_weight=1) @@ -125,15 +123,15 @@ def get_inhomogeneous_tensor_prod_basis(shape, _): @pytest.mark.parametrize("dim", [1, 2, 3]) @pytest.mark.parametrize("order", [5, 8]) @pytest.mark.parametrize(("shape_cls", "basis_getter"), [ - (shp.Simplex, md.basis_for_shape), - (shp.Simplex, md.orthonormal_basis_for_shape), - (shp.Simplex, md.monomial_basis_for_shape), + (mp.Simplex, mp.basis_for_space), + (mp.Simplex, mp.orthonormal_basis_for_space), + (mp.Simplex, mp.monomial_basis_for_space), - (shp.Hypercube, md.basis_for_shape), - (shp.Hypercube, md.orthonormal_basis_for_shape), - (shp.Hypercube, md.monomial_basis_for_shape), + (mp.Hypercube, mp.basis_for_space), + (mp.Hypercube, mp.orthonormal_basis_for_space), + (mp.Hypercube, mp.monomial_basis_for_space), - (shp.Hypercube, get_inhomogeneous_tensor_prod_basis), + (mp.Hypercube, get_inhomogeneous_tensor_prod_basis), ]) def test_basis_grad(dim, shape_cls, order, basis_getter): """Do a simplistic FD-style check on the gradients of the basis.""" @@ -142,7 +140,7 @@ def test_basis_grad(dim, shape_cls, order, basis_getter): shape = shape_cls(dim) rng = np.random.Generator(np.random.PCG64(17)) - basis = basis_getter(shape, order) + basis = basis_getter(mp.space_for_shape(shape, order)) from pytools.convergence import EOCRecorder from pytools import wandering_element @@ -152,7 +150,7 @@ def test_basis_grad(dim, shape_cls, order, basis_getter): )): eoc_rec = EOCRecorder() for h in [1e-2, 1e-3]: - r = nd.random_nodes_for_shape(shape, nnodes=1000, rng=rng) + r = mp.random_nodes_for_shape(shape, nnodes=1000, rng=rng) gradbf_v = np.array(gradbf(r)) gradbf_v_num = np.array([ @@ -188,22 +186,22 @@ def map_if(self, expr): @pytest.mark.parametrize("shape", [ - shp.Simplex(1), - shp.Simplex(2), - shp.Simplex(3), - shp.Hypercube(1), - shp.Hypercube(2), - shp.Hypercube(3), + mp.Simplex(1), + mp.Simplex(2), + mp.Simplex(3), + mp.Hypercube(1), + mp.Hypercube(2), + mp.Hypercube(3), ]) @pytest.mark.parametrize("order", [5, 8]) @pytest.mark.parametrize("basis_getter", [ - (md.basis_for_shape), - (md.orthonormal_basis_for_shape), - (md.monomial_basis_for_shape), + (mp.basis_for_space), + (mp.orthonormal_basis_for_space), + (mp.monomial_basis_for_space), ]) def test_symbolic_basis(shape, order, basis_getter): - basis = basis_getter(shape, order) - sym_basis = [md.symbolicize_function(f, shape.dim) for f in basis.functions] + basis = basis_getter(mp.space_for_shape(shape, order)) + sym_basis = [mp.symbolicize_function(f, shape.dim) for f in basis.functions] # {{{ test symbolic against direct eval @@ -212,7 +210,7 @@ def test_symbolic_basis(shape, order, basis_getter): print(75*"#") rng = np.random.Generator(np.random.PCG64(17)) - r = nd.random_nodes_for_shape(shape, 10000, rng=rng) + r = mp.random_nodes_for_shape(shape, 10000, rng=rng) for func, sym_func in zip(basis.functions, sym_basis): strmap = MyStringifyMapper() @@ -240,7 +238,7 @@ def test_symbolic_basis(shape, order, basis_getter): print("GRADIENTS") print(75*"#") - sym_grad_basis = [md.symbolicize_function(f, shape.dim) for f in basis.gradients] + sym_grad_basis = [mp.symbolicize_function(f, shape.dim) for f in basis.gradients] for grad, sym_grad in zip(basis.gradients, sym_grad_basis): strmap = MyStringifyMapper() diff --git a/test/test_tools.py b/test/test_tools.py index ef6fec52..06b707c7 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -24,10 +24,6 @@ import numpy.linalg as la import modepy as mp -import modepy.shapes as shp -import modepy.nodes as nd -import modepy.modes as md - from functools import partial import pytest @@ -92,9 +88,9 @@ def constant(x): ("c1-2d", partial(c1, -0.1), 2, 15, -2.3), ]) def test_modal_decay(case_name, test_func, dims, n, expected_expn): - shape = shp.Simplex(dims) + space = mp.PN(dims, n) nodes = mp.warp_and_blend_nodes(dims, n) - basis = mp.orthonormal_basis_for_shape(shape, n) + basis = mp.orthonormal_basis_for_space(space) vdm = mp.vandermonde(basis.functions, nodes) f = test_func(nodes[0]) @@ -123,11 +119,9 @@ def test_modal_decay(case_name, test_func, dims, n, expected_expn): ("const-2d", constant, 2, 5), ]) def test_residual_estimation(case_name, test_func, dims, n): - shape = shp.Simplex(dims) - def estimate_resid(inner_n): nodes = mp.warp_and_blend_nodes(dims, inner_n) - basis = mp.orthonormal_basis_for_shape(shape, inner_n) + basis = mp.orthonormal_basis_for_space(mp.PN(dims, inner_n)) vdm = mp.vandermonde(basis.functions, nodes) f = test_func(nodes[0]) @@ -148,15 +142,18 @@ def estimate_resid(inner_n): # {{{ test_resampling_matrix @pytest.mark.parametrize("dims", [1, 2, 3]) -@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) +@pytest.mark.parametrize("shape_cls", [mp.Simplex, mp.Hypercube]) def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): shape = shape_cls(dims) - coarse_nodes = nd.edge_clustered_nodes_for_shape(shape, ncoarse) - coarse_basis = md.basis_for_shape(shape, ncoarse) + coarse_space = mp.space_for_shape(shape, ncoarse) + fine_space = mp.space_for_shape(shape, nfine) + + coarse_nodes = mp.edge_clustered_nodes_for_space(coarse_space) + coarse_basis = mp.basis_for_space(coarse_space) - fine_nodes = nd.edge_clustered_nodes_for_shape(shape, nfine) - fine_basis = md.basis_for_shape(shape, nfine) + fine_nodes = mp.edge_clustered_nodes_for_space(fine_space) + fine_basis = mp.basis_for_space(fine_space) my_eye = np.dot( mp.resampling_matrix(fine_basis.functions, coarse_nodes, fine_nodes), @@ -178,12 +175,12 @@ def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): # {{{ test_diff_matrix @pytest.mark.parametrize("dims", [1, 2, 3]) -@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) +@pytest.mark.parametrize("shape_cls", [mp.Simplex, mp.Hypercube]) def test_diff_matrix(dims, shape_cls, order=5): - shape = shape_cls(dims) + space = mp.space_for_shape(shape_cls(dims), order) - nodes = nd.edge_clustered_nodes_for_shape(shape, order) - basis = md.basis_for_shape(shape, order) + nodes = mp.edge_clustered_nodes_for_space(space) + basis = mp.basis_for_space(space) diff_mat = mp.differentiation_matrices(basis.functions, basis.gradients, nodes) if isinstance(diff_mat, tuple): @@ -201,14 +198,14 @@ def test_diff_matrix(dims, shape_cls, order=5): @pytest.mark.parametrize("dims", [2, 3]) def test_diff_matrix_permutation(dims): - shape = shp.Simplex(dims) order = 5 + space = mp.PN(dims, order) from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam node_tuples = list(gnitstam(order, dims)) - simplex_onb = mp.orthonormal_basis_for_shape(shape, order) + simplex_onb = mp.orthonormal_basis_for_space(space) nodes = np.array(mp.warp_and_blend_nodes(dims, order, node_tuples=node_tuples)) diff_matrices = mp.differentiation_matrices( simplex_onb.functions, simplex_onb.gradients, nodes) @@ -228,13 +225,14 @@ def test_diff_matrix_permutation(dims): @pytest.mark.parametrize("dims", [2, 3]) def test_deprecated_modal_face_mass_matrix(dims, order=3): # FIXME DEPRECATED remove along with modal_face_mass_matrix (>=2022) - shape = shp.Simplex(dims) + shape = mp.Simplex(dims) + space = mp.space_for_shape(shape, order) - vertices = shp.biunit_vertices_for_shape(shape) - basis = md.basis_for_shape(shape, order - 1) + vertices = mp.biunit_vertices_for_shape(shape) + basis = mp.basis_for_space(space) from modepy.matrices import modal_face_mass_matrix - for face in shp.faces_for_shape(shape): + for face in mp.faces_for_shape(shape): face_vertices = vertices[:, face.volume_vertex_indices] fmm = modal_face_mass_matrix( @@ -255,15 +253,17 @@ def test_deprecated_modal_face_mass_matrix(dims, order=3): @pytest.mark.parametrize("dims", [2, 3]) def test_deprecated_nodal_face_mass_matrix(dims, order=3): # FIXME DEPRECATED remove along with nodal_face_mass_matrix (>=2022) - volume = shp.Simplex(dims) + volume = mp.Simplex(dims) + vol_space = mp.space_for_shape(volume, order) - vertices = shp.biunit_vertices_for_shape(volume) - volume_nodes = nd.edge_clustered_nodes_for_shape(volume, order) - volume_basis = md.basis_for_shape(volume, order) + vertices = mp.biunit_vertices_for_shape(volume) + volume_nodes = mp.edge_clustered_nodes_for_space(vol_space) + volume_basis = mp.basis_for_space(vol_space) from modepy.matrices import nodal_face_mass_matrix - for face in shp.faces_for_shape(volume): - face_nodes = nd.edge_clustered_nodes_for_shape(face, order) + for face in mp.faces_for_shape(volume): + face_space = mp.space_for_shape(face, order) + face_nodes = mp.edge_clustered_nodes_for_space(face_space) face_vertices = vertices[:, face.volume_vertex_indices] fmm = nodal_face_mass_matrix( @@ -282,9 +282,9 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): logger.info("fmm: nnz %d\n%s", nnz, fmm) - logger.info("mass matrix:\n%s", mp.mass_matrix( - md.basis_for_shape(face, order).functions, - nd.edge_clustered_nodes_for_shape(face, order))) + logger.info("mass matrix:\n%s", mp.mass_matrix( + mp.basis_for_space(face_space).functions, + mp.edge_clustered_nodes_for_space(face_space))) # }}} @@ -292,19 +292,22 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): # {{{ face mass matrices @pytest.mark.parametrize("dims", [2, 3]) -@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) +@pytest.mark.parametrize("shape_cls", [mp.Simplex, mp.Hypercube]) def test_modal_mass_matrix_for_face(dims, shape_cls, order=3): - shape = shape_cls(dims) - - vol_basis = md.basis_for_shape(shape, order) + vol_shape = shape_cls(dims) + vol_space = mp.space_for_shape(vol_shape, order) + vol_basis = mp.basis_for_space(vol_space) from modepy.matrices import modal_mass_matrix_for_face - for face in shp.faces_for_shape(shape): - face_basis = md.basis_for_shape(face, order) + for face in mp.faces_for_shape(vol_shape): + face_space = mp.space_for_shape(face, order) + face_basis = mp.basis_for_space(face_space) + face_quad = mp.quadrature_for_space(mp.space_for_shape(face, 2*order)) + face_quad2 = mp.quadrature_for_space(mp.space_for_shape(face, 2*order+2)) fmm = modal_mass_matrix_for_face( - face, face_basis.functions, vol_basis.functions, order) + face, face_quad, face_basis.functions, vol_basis.functions) fmm2 = modal_mass_matrix_for_face( - face, face_basis.functions, vol_basis.functions, order+1) + face, face_quad2, face_basis.functions, vol_basis.functions) error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) logger.info("fmm error: %.5e", error) @@ -317,24 +320,27 @@ def test_modal_mass_matrix_for_face(dims, shape_cls, order=3): @pytest.mark.parametrize("dims", [2, 3]) -@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) +@pytest.mark.parametrize("shape_cls", [mp.Simplex, mp.Hypercube]) def test_nodal_mass_matrix_for_face(dims, shape_cls, order=3): - volume = shape_cls(dims) - face = shape_cls(dims - 1) + vol_shape = shape_cls(dims) + vol_space = mp.space_for_shape(vol_shape, order) - volume_nodes = nd.edge_clustered_nodes_for_shape(volume, order) - volume_basis = md.basis_for_shape(volume, order) - face_nodes = nd.edge_clustered_nodes_for_shape(face, order) + volume_nodes = mp.edge_clustered_nodes_for_space(vol_space) + volume_basis = mp.basis_for_space(vol_space) from modepy.matrices import nodal_mass_matrix_for_face - for face in shp.faces_for_shape(volume): - face_basis = md.basis_for_shape(face, order) + for face in mp.faces_for_shape(vol_shape): + face_space = mp.space_for_shape(face, order) + face_basis = mp.basis_for_space(face_space) + face_nodes = mp.edge_clustered_nodes_for_space(face_space) + face_quad = mp.quadrature_for_space(mp.space_for_shape(face, 2*order)) + face_quad2 = mp.quadrature_for_space(mp.space_for_shape(face, 2*order+2)) fmm = nodal_mass_matrix_for_face( - face, face_basis.functions, volume_basis.functions, - volume_nodes, face_nodes, order) + face, face_quad, face_basis.functions, volume_basis.functions, + volume_nodes, face_nodes) fmm2 = nodal_mass_matrix_for_face( - face, face_basis.functions, volume_basis.functions, - volume_nodes, face_nodes, order+1) + face, face_quad2, face_basis.functions, volume_basis.functions, + volume_nodes, face_nodes) error = la.norm(fmm - fmm2, np.inf) / la.norm(fmm2, np.inf) logger.info("fmm error: %.5e", error) @@ -345,9 +351,8 @@ def test_nodal_mass_matrix_for_face(dims, shape_cls, order=3): logger.info("fmm: nnz %d\n%s", nnz, fmm) - logger.info("mass matrix:\n%s", mp.mass_matrix( - md.basis_for_shape(face, order).functions, - nd.edge_clustered_nodes_for_shape(face, order))) + logger.info("mass matrix:\n%s", + mp.mass_matrix(face_basis.functions, face_nodes)) # }}} @@ -356,12 +361,13 @@ def test_nodal_mass_matrix_for_face(dims, shape_cls, order=3): @pytest.mark.parametrize("dims", [1, 2]) @pytest.mark.parametrize("order", [3, 5, 8]) -@pytest.mark.parametrize("shape_cls", [shp.Simplex, shp.Hypercube]) +@pytest.mark.parametrize("shape_cls", [mp.Simplex, mp.Hypercube]) def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): logging.basicConfig(level=logging.INFO) shape = shape_cls(dims) + space = mp.space_for_shape(shape, order) - nodes = nd.edge_clustered_nodes_for_shape(shape, order) + nodes = mp.edge_clustered_nodes_for_space(space) from modepy.tools import estimate_lebesgue_constant lebesgue_constant = estimate_lebesgue_constant(order, nodes, shape=shape) @@ -405,7 +411,7 @@ def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): def test_hypercube_submesh(dims): from modepy.tools import submesh_for_shape from pytools import generate_nonnegative_integer_tuples_below as gnitb - shape = shp.Hypercube(dims) + shape = mp.Hypercube(dims) node_tuples = list(gnitb(3, dims)) From 6bf5735826cb769024d7271cfdb287f1510eb882 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 19:42:05 -0600 Subject: [PATCH 45/68] Move submesh functionality into shapes --- modepy/__init__.py | 4 +- modepy/shapes.py | 135 ++++++++++++++++++++++++++++++++++++++++++++ modepy/tools.py | 138 ++------------------------------------------- test/test_tools.py | 3 +- 4 files changed, 143 insertions(+), 137 deletions(-) diff --git a/modepy/__init__.py b/modepy/__init__.py index ff8d212b..50832c47 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -25,7 +25,8 @@ from modepy.shapes import ( Shape, Face, Simplex, Hypercube, - biunit_vertices_for_shape, faces_for_shape + biunit_vertices_for_shape, faces_for_shape, + submesh_for_shape, ) from modepy.spaces import ( FunctionSpace, PN, QN, space_for_shape) @@ -78,6 +79,7 @@ "Shape", "Face", "Simplex", "Hypercube", "biunit_vertices_for_shape", "faces_for_shape", + "submesh_for_shape", "FunctionSpace", "PN", "QN", "space_for_shape", diff --git a/modepy/shapes.py b/modepy/shapes.py index ba37b771..3fb9578a 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -166,6 +166,10 @@ in ``tsr`` (i.e. in reverse axis order). For example, in 3D, ``A, B, C, D, ...`` is ``000, 001, 010, 011, ...``. +Submeshes +--------- +.. autofunction:: submesh_for_shape + Redirections to Canonical Names ------------------------------- @@ -404,4 +408,135 @@ def _(shape: Hypercube): # }}} + +# {{{ submeshes + +@singledispatch +def submesh_for_shape(shape: Shape, node_tuples): + """Return a list of tuples of indices into the node list that + generate a tesselation of the reference element. + + :arg node_tuples: A list of tuples *(i, j, ...)* of integers + indicating node positions inside the unit element. The + returned list references indices in this list. + + :func:`modepy.node_tuples_for_space` may be used to generate *node_tuples*. + + .. versionadded:: 2020.3 + """ + raise NotImplementedError(type(shape).__name__) + + +@submesh_for_shape.register(Simplex) +def _(shape: Simplex, node_tuples): + from pytools import single_valued, add_tuples + dims = single_valued(len(nt) for nt in node_tuples) + + node_dict = { + ituple: idx + for idx, ituple in enumerate(node_tuples)} + + if dims == 1: + result = [] + + def try_add_line(d1, d2): + try: + result.append(( + node_dict[add_tuples(current, d1)], + node_dict[add_tuples(current, d2)], + )) + except KeyError: + pass + + for current in node_tuples: + try_add_line((0,), (1,),) + + return result + elif dims == 2: + # {{{ triangle sub-mesh + result = [] + + def try_add_tri(d1, d2, d3): + try: + result.append(( + node_dict[add_tuples(current, d1)], + node_dict[add_tuples(current, d2)], + node_dict[add_tuples(current, d3)], + )) + except KeyError: + pass + + for current in node_tuples: + # this is a tesselation of a square into two triangles. + # subtriangles that fall outside of the master triangle are + # simply not added. + + # positively oriented + try_add_tri((0, 0), (1, 0), (0, 1)) + try_add_tri((1, 0), (1, 1), (0, 1)) + + return result + + # }}} + elif dims == 3: + # {{{ tet sub-mesh + + def try_add_tet(d1, d2, d3, d4): + try: + result.append(( + node_dict[add_tuples(current, d1)], + node_dict[add_tuples(current, d2)], + node_dict[add_tuples(current, d3)], + node_dict[add_tuples(current, d4)], + )) + except KeyError: + pass + + result = [] + for current in node_tuples: + # this is a tesselation of a cube into six tets. + # subtets that fall outside of the master tet are simply not added. + + # positively oriented + try_add_tet((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) + try_add_tet((1, 0, 1), (1, 0, 0), (0, 0, 1), (0, 1, 0)) + try_add_tet((1, 0, 1), (0, 1, 1), (0, 1, 0), (0, 0, 1)) + + try_add_tet((1, 0, 0), (0, 1, 0), (1, 0, 1), (1, 1, 0)) + try_add_tet((0, 1, 1), (0, 1, 0), (1, 1, 0), (1, 0, 1)) + try_add_tet((0, 1, 1), (1, 1, 1), (1, 0, 1), (1, 1, 0)) + + return result + + # }}} + else: + raise NotImplementedError("%d-dimensional sub-meshes" % dims) + + +@submesh_for_shape.register(Hypercube) +def _(shape: Hypercube, node_tuples): + from pytools import single_valued, add_tuples + dims = single_valued(len(nt) for nt in node_tuples) + + node_dict = { + ituple: idx + for idx, ituple in enumerate(node_tuples)} + + from pytools import generate_nonnegative_integer_tuples_below as gnitb + + result = [] + for current in node_tuples: + try: + result.append(tuple( + node_dict[add_tuples(current, offset)] + for offset in gnitb(2, dims))) + + except KeyError: + pass + + return result + +# }}} + + # vim: foldmethod=marker diff --git a/modepy/tools.py b/modepy/tools.py index b6184fda..a68c0451 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -9,10 +9,6 @@ .. autofunction:: unit_to_barycentric .. autofunction:: barycentric_to_equilateral -Submeshes ---------- -.. autofunction:: submesh_for_shape - Interpolation quality --------------------- @@ -42,7 +38,7 @@ THE SOFTWARE. """ -from functools import reduce, singledispatch +from functools import reduce import numpy as np import numpy.linalg as la @@ -258,132 +254,6 @@ def barycentric_to_equilateral(bary): # {{{ submeshes -@singledispatch -def submesh_for_shape(shape: shp.Shape, node_tuples): - """Return a list of tuples of indices into the node list that - generate a tesselation of the reference element. - - :arg node_tuples: A list of tuples *(i, j, ...)* of integers - indicating node positions inside the unit element. The - returned list references indices in this list. - - :func:`modepy.node_tuples_for_space` may be used to generate *node_tuples*. - - .. versionadded:: 2020.3 - """ - raise NotImplementedError(type(shape).__name__) - - -@submesh_for_shape.register(shp.Simplex) -def _(shape: shp.Simplex, node_tuples): - from pytools import single_valued, add_tuples - dims = single_valued(len(nt) for nt in node_tuples) - - node_dict = { - ituple: idx - for idx, ituple in enumerate(node_tuples)} - - if dims == 1: - result = [] - - def try_add_line(d1, d2): - try: - result.append(( - node_dict[add_tuples(current, d1)], - node_dict[add_tuples(current, d2)], - )) - except KeyError: - pass - - for current in node_tuples: - try_add_line((0,), (1,),) - - return result - elif dims == 2: - # {{{ triangle sub-mesh - result = [] - - def try_add_tri(d1, d2, d3): - try: - result.append(( - node_dict[add_tuples(current, d1)], - node_dict[add_tuples(current, d2)], - node_dict[add_tuples(current, d3)], - )) - except KeyError: - pass - - for current in node_tuples: - # this is a tesselation of a square into two triangles. - # subtriangles that fall outside of the master triangle are - # simply not added. - - # positively oriented - try_add_tri((0, 0), (1, 0), (0, 1)) - try_add_tri((1, 0), (1, 1), (0, 1)) - - return result - - # }}} - elif dims == 3: - # {{{ tet sub-mesh - - def try_add_tet(d1, d2, d3, d4): - try: - result.append(( - node_dict[add_tuples(current, d1)], - node_dict[add_tuples(current, d2)], - node_dict[add_tuples(current, d3)], - node_dict[add_tuples(current, d4)], - )) - except KeyError: - pass - - result = [] - for current in node_tuples: - # this is a tesselation of a cube into six tets. - # subtets that fall outside of the master tet are simply not added. - - # positively oriented - try_add_tet((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)) - try_add_tet((1, 0, 1), (1, 0, 0), (0, 0, 1), (0, 1, 0)) - try_add_tet((1, 0, 1), (0, 1, 1), (0, 1, 0), (0, 0, 1)) - - try_add_tet((1, 0, 0), (0, 1, 0), (1, 0, 1), (1, 1, 0)) - try_add_tet((0, 1, 1), (0, 1, 0), (1, 1, 0), (1, 0, 1)) - try_add_tet((0, 1, 1), (1, 1, 1), (1, 0, 1), (1, 1, 0)) - - return result - - # }}} - else: - raise NotImplementedError("%d-dimensional sub-meshes" % dims) - - -@submesh_for_shape.register(shp.Hypercube) -def _(shape: shp.Hypercube, node_tuples): - from pytools import single_valued, add_tuples - dims = single_valued(len(nt) for nt in node_tuples) - - node_dict = { - ituple: idx - for idx, ituple in enumerate(node_tuples)} - - from pytools import generate_nonnegative_integer_tuples_below as gnitb - - result = [] - for current in node_tuples: - try: - result.append(tuple( - node_dict[add_tuples(current, offset)] - for offset in gnitb(2, dims))) - - except KeyError: - pass - - return result - - def simplex_submesh(node_tuples): """Return a list of tuples of indices into the node list that generate a tesselation of the reference element. @@ -395,7 +265,7 @@ def simplex_submesh(node_tuples): :func:`pytools.generate_nonnegative_integer_tuples_summing_to_at_most` may be used to generate *node_tuples*. """ - return submesh_for_shape(shp.Simplex(len(node_tuples[0])), node_tuples) + return shp.submesh_for_shape(shp.Simplex(len(node_tuples[0])), node_tuples) submesh = MovedFunctionDeprecationWrapper(simplex_submesh) @@ -422,7 +292,7 @@ def hypercube_submesh(node_tuples): "hypercube_submesh will go away in 2022.", DeprecationWarning, stacklevel=2) - return submesh_for_shape(shp.Hypercube(len(node_tuples[0])), node_tuples) + return shp.submesh_for_shape(shp.Hypercube(len(node_tuples[0])), node_tuples) # }}} @@ -537,7 +407,7 @@ def estimate_lebesgue_constant(n, nodes, shape=None, visualize=False): if shape.dim == 2: print(f"Lebesgue constant: {lebesgue_constant}") - triangles = submesh_for_shape(shape, equi_node_tuples) + triangles = shp.submesh_for_shape(shape, equi_node_tuples) try: import mayavi.mlab as mlab diff --git a/test/test_tools.py b/test/test_tools.py index 06b707c7..784e4305 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -409,7 +409,6 @@ def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): @pytest.mark.parametrize("dims", [2, 3, 4]) def test_hypercube_submesh(dims): - from modepy.tools import submesh_for_shape from pytools import generate_nonnegative_integer_tuples_below as gnitb shape = mp.Hypercube(dims) @@ -420,7 +419,7 @@ def test_hypercube_submesh(dims): assert len(node_tuples) == 3**dims - elements = submesh_for_shape(shape, node_tuples) + elements = mp.submesh_for_shape(shape, node_tuples) for e in elements: logger.info("element: %s", e) From 62524e291b5d4c048a3a7ba772f1fd55f9b3aab4 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 22:00:30 -0600 Subject: [PATCH 46/68] Quadrature, notes: require space *and* shape arguments --- modepy/__init__.py | 8 +++--- modepy/matrices.py | 10 ++----- modepy/nodes.py | 52 ++++++++++++++++++++++++++++------- modepy/quadrature/__init__.py | 28 +++++++++++++------ test/test_modes.py | 2 +- test/test_tools.py | 38 ++++++++++++------------- 6 files changed, 89 insertions(+), 49 deletions(-) diff --git a/modepy/__init__.py b/modepy/__init__.py index 50832c47..abface7b 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -47,7 +47,7 @@ tensor_product_nodes, legendre_gauss_lobatto_tensor_product_nodes, node_tuples_for_space, - equispaced_nodes_for_space, edge_clustered_nodes_for_space, + equispaced_nodes, edge_clustered_nodes, random_nodes_for_shape) from modepy.matrices import (vandermonde, resampling_matrix, differentiation_matrices, @@ -58,7 +58,7 @@ from modepy.quadrature import ( Quadrature, QuadratureRuleUnavailable, TensorProductQuadrature, LegendreGaussTensorProductQuadrature, - quadrature_for_space) + quadrature) from modepy.quadrature.jacobi_gauss import ( JacobiGaussQuadrature, LegendreGaussQuadrature, ChebyshevGaussQuadrature, GaussGegenbauerQuadrature, @@ -98,7 +98,7 @@ "equidistant_nodes", "warp_and_blend_nodes", "tensor_product_nodes", "legendre_gauss_lobatto_tensor_product_nodes", "node_tuples_for_space", - "edge_clustered_nodes_for_space", "equispaced_nodes_for_space", + "edge_clustered_nodes", "equispaced_nodes", "random_nodes_for_shape", "vandermonde", "resampling_matrix", "differentiation_matrices", @@ -109,7 +109,7 @@ "Quadrature", "QuadratureRuleUnavailable", "TensorProductQuadrature", "LegendreGaussTensorProductQuadrature", - "quadrature_for_space", + "quadrature", "JacobiGaussQuadrature", "LegendreGaussQuadrature", "GaussLegendreQuadrature", "ChebyshevGaussQuadrature", diff --git a/modepy/matrices.py b/modepy/matrices.py index 523f865d..f7234a0c 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -25,7 +25,7 @@ import numpy as np import numpy.linalg as la -from modepy.shapes import Face +from modepy.shapes import Face, Simplex from modepy.spaces import PN from modepy.quadrature import Quadrature @@ -281,8 +281,6 @@ def nodal_mass_matrix_for_face(face: Face, face_quad: Quadrature, def modal_face_mass_matrix(trial_basis, order, face_vertices, test_basis=None): """ :arg face_vertices: an array of shape ``(dims, nvertices)``. - :arg shape: a :class:`~modepy.shapes.Shape` that identifies the - reference volume element. .. versionadded :: 2016.1 """ @@ -296,8 +294,8 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, test_basis=None): vol_dims = face_vertices.shape[0] - from modepy.quadrature import quadrature_for_space - quad = quadrature_for_space(PN(vol_dims - 1, order*2)) + from modepy.quadrature import quadrature + quad = quadrature(PN(vol_dims - 1, order*2), Simplex(vol_dims-1)) assert quad.exact_to >= order*2 @@ -320,8 +318,6 @@ def nodal_face_mass_matrix(trial_basis, volume_nodes, face_nodes, order, face_vertices, test_basis=None): """ :arg face_vertices: an array of shape ``(dims, nvertices)``. - :arg shape: a :class:`~modepy.shapes.Shape` that identifies the - reference face element. .. versionadded :: 2016.1 """ diff --git a/modepy/nodes.py b/modepy/nodes.py index 30094cd6..40e60d01 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -7,8 +7,8 @@ .. currentmodule:: modepy .. autofunction:: node_tuples_for_space -.. autofunction:: equispaced_nodes_for_space -.. autofunction:: edge_clustered_nodes_for_space +.. autofunction:: equispaced_nodes +.. autofunction:: edge_clustered_nodes .. autofunction:: random_nodes_for_shape Simplices @@ -362,13 +362,13 @@ def node_tuples_for_space(space: FunctionSpace) -> List[Tuple[int]]: @singledispatch -def equispaced_nodes_for_space(space: FunctionSpace): - raise NotImplementedError(type(space).__name__) +def equispaced_nodes(space: FunctionSpace, shape: Shape): + raise NotImplementedError((type(space).__name__, type(shape).__name)) @singledispatch -def edge_clustered_nodes_for_space(space: FunctionSpace): - raise NotImplementedError(type(space).__name__) +def edge_clustered_nodes(space: FunctionSpace, shape: Shape): + raise NotImplementedError((type(space).__name__, type(shape).__name)) @singledispatch @@ -392,8 +392,24 @@ def _(space: PN): return tuple(gnitsam(space.order, space.spatial_dim)) -@edge_clustered_nodes_for_space.register(PN) -def _(space: PN): +@equispaced_nodes.register(PN) +def _(space: PN, shape: Simplex): + if not isinstance(shape, Simplex): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + if space.spatial_dim != shape.dim: + raise ValueError("spatial dimensions of shape and space must match") + + return (np.array(node_tuples_for_space(space), dtype=np.float64) + / space.order*2 - 1).T + + +@edge_clustered_nodes.register(PN) +def _(space: PN, shape: Simplex): + if not isinstance(shape, Simplex): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + if space.spatial_dim != shape.dim: + raise ValueError("spatial dimensions of shape and space must match") + return warp_and_blend_nodes(space.spatial_dim, space.order) @@ -430,8 +446,24 @@ def _(space: QN): return tuple(gnitb(space.order, space.spatial_dim)) -@edge_clustered_nodes_for_space.register(QN) -def _(space: QN): +@equispaced_nodes.register(QN) +def _(space: QN, shape: Hypercube): + if not isinstance(shape, Hypercube): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + if space.spatial_dim != shape.dim: + raise ValueError("spatial dimensions of shape and space must match") + + return (np.array(node_tuples_for_space(space), dtype=np.float64) + / space.order*2 - 1).T + + +@edge_clustered_nodes.register(QN) +def _(space: QN, shape: Hypercube): + if not isinstance(shape, Hypercube): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + if space.spatial_dim != shape.dim: + raise ValueError("spatial dimensions of shape and space must match") + return legendre_gauss_lobatto_tensor_product_nodes( space.spatial_dim, space.order) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index ded3aa93..66bab796 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -3,7 +3,7 @@ .. autoclass:: Quadrature -.. autoclass:: quadrature_for_space +.. autoclass:: quadrature Redirections to Canonical Names ------------------------------- @@ -41,6 +41,7 @@ from functools import singledispatch import numpy as np +from modepy.shapes import Shape, Simplex, Hypercube from modepy.spaces import FunctionSpace, PN, QN @@ -133,19 +134,24 @@ def __init__(self, N, dims, backend=None): # noqa: N803 dims, LegendreGaussQuadrature(N, backend=backend)) -# {{{ quadrature_for_space +# {{{ quadrature @singledispatch -def quadrature_for_space(space: FunctionSpace) -> Quadrature: +def quadrature(space: FunctionSpace, shape: Shape) -> Quadrature: """ :returns: a :class:`~modepy.Quadrature` that exactly integrates the functions in *space*. """ - raise NotImplementedError(type(space).__name__) + raise NotImplementedError((type(space).__name__, type(shape).__name)) -@quadrature_for_space.register(PN) -def _(space: PN): +@quadrature.register(PN) +def _(space: PN, shape: Simplex): + if not isinstance(shape, Simplex): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + if space.spatial_dim != shape.dim: + raise ValueError("spatial dimensions of shape and space must match") + import modepy as mp try: quad = mp.XiaoGimbutasSimplexQuadrature(space.order, space.spatial_dim) @@ -158,8 +164,13 @@ def _(space: PN): return quad -@quadrature_for_space.register(QN) -def _(space: QN): +@quadrature.register(QN) +def _(space: QN, shape: Hypercube): + if not isinstance(shape, Hypercube): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + if space.spatial_dim != shape.dim: + raise ValueError("spatial dimensions of shape and space must match") + import modepy as mp if space.spatial_dim == 0: quad = mp.Quadrature(np.empty((0, 1)), np.empty((0, 1))) @@ -168,6 +179,7 @@ def _(space: QN): quad = LegendreGaussTensorProductQuadrature(space.order, space.spatial_dim) return quad + # }}} # vim: foldmethod=marker diff --git a/test/test_modes.py b/test/test_modes.py index 06588fc9..a8de734a 100644 --- a/test/test_modes.py +++ b/test/test_modes.py @@ -87,7 +87,7 @@ def test_orthogonality(shape, order, ebound): """Test orthogonality of ONBs using cubature.""" qspace = mp.space_for_shape(shape, 2*order) - cub = mp.quadrature_for_space(qspace) + cub = mp.quadrature(qspace, shape) basis = mp.orthonormal_basis_for_space(mp.space_for_shape(shape, order)) maxerr = 0 diff --git a/test/test_tools.py b/test/test_tools.py index 784e4305..3894ed77 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -149,10 +149,10 @@ def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): coarse_space = mp.space_for_shape(shape, ncoarse) fine_space = mp.space_for_shape(shape, nfine) - coarse_nodes = mp.edge_clustered_nodes_for_space(coarse_space) + coarse_nodes = mp.edge_clustered_nodes(coarse_space, shape) coarse_basis = mp.basis_for_space(coarse_space) - fine_nodes = mp.edge_clustered_nodes_for_space(fine_space) + fine_nodes = mp.edge_clustered_nodes(fine_space, shape) fine_basis = mp.basis_for_space(fine_space) my_eye = np.dot( @@ -177,9 +177,9 @@ def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): @pytest.mark.parametrize("dims", [1, 2, 3]) @pytest.mark.parametrize("shape_cls", [mp.Simplex, mp.Hypercube]) def test_diff_matrix(dims, shape_cls, order=5): - space = mp.space_for_shape(shape_cls(dims), order) - - nodes = mp.edge_clustered_nodes_for_space(space) + shape = shape_cls(dims) + space = mp.space_for_shape(shape, order) + nodes = mp.edge_clustered_nodes(space, shape) basis = mp.basis_for_space(space) diff_mat = mp.differentiation_matrices(basis.functions, basis.gradients, nodes) @@ -253,17 +253,17 @@ def test_deprecated_modal_face_mass_matrix(dims, order=3): @pytest.mark.parametrize("dims", [2, 3]) def test_deprecated_nodal_face_mass_matrix(dims, order=3): # FIXME DEPRECATED remove along with nodal_face_mass_matrix (>=2022) - volume = mp.Simplex(dims) - vol_space = mp.space_for_shape(volume, order) + vol_shape = mp.Simplex(dims) + vol_space = mp.space_for_shape(vol_shape, order) - vertices = mp.biunit_vertices_for_shape(volume) - volume_nodes = mp.edge_clustered_nodes_for_space(vol_space) + vertices = mp.biunit_vertices_for_shape(vol_shape) + volume_nodes = mp.edge_clustered_nodes(vol_space, vol_shape) volume_basis = mp.basis_for_space(vol_space) from modepy.matrices import nodal_face_mass_matrix - for face in mp.faces_for_shape(volume): + for face in mp.faces_for_shape(vol_shape): face_space = mp.space_for_shape(face, order) - face_nodes = mp.edge_clustered_nodes_for_space(face_space) + face_nodes = mp.edge_clustered_nodes(face_space, face) face_vertices = vertices[:, face.volume_vertex_indices] fmm = nodal_face_mass_matrix( @@ -284,7 +284,7 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): logger.info("mass matrix:\n%s", mp.mass_matrix( mp.basis_for_space(face_space).functions, - mp.edge_clustered_nodes_for_space(face_space))) + mp.edge_clustered_nodes(face_space, face))) # }}} @@ -302,8 +302,8 @@ def test_modal_mass_matrix_for_face(dims, shape_cls, order=3): for face in mp.faces_for_shape(vol_shape): face_space = mp.space_for_shape(face, order) face_basis = mp.basis_for_space(face_space) - face_quad = mp.quadrature_for_space(mp.space_for_shape(face, 2*order)) - face_quad2 = mp.quadrature_for_space(mp.space_for_shape(face, 2*order+2)) + face_quad = mp.quadrature(mp.space_for_shape(face, 2*order), face) + face_quad2 = mp.quadrature(mp.space_for_shape(face, 2*order+2), face) fmm = modal_mass_matrix_for_face( face, face_quad, face_basis.functions, vol_basis.functions) fmm2 = modal_mass_matrix_for_face( @@ -325,16 +325,16 @@ def test_nodal_mass_matrix_for_face(dims, shape_cls, order=3): vol_shape = shape_cls(dims) vol_space = mp.space_for_shape(vol_shape, order) - volume_nodes = mp.edge_clustered_nodes_for_space(vol_space) + volume_nodes = mp.edge_clustered_nodes(vol_space, vol_shape) volume_basis = mp.basis_for_space(vol_space) from modepy.matrices import nodal_mass_matrix_for_face for face in mp.faces_for_shape(vol_shape): face_space = mp.space_for_shape(face, order) face_basis = mp.basis_for_space(face_space) - face_nodes = mp.edge_clustered_nodes_for_space(face_space) - face_quad = mp.quadrature_for_space(mp.space_for_shape(face, 2*order)) - face_quad2 = mp.quadrature_for_space(mp.space_for_shape(face, 2*order+2)) + face_nodes = mp.edge_clustered_nodes(face_space, face) + face_quad = mp.quadrature(mp.space_for_shape(face, 2*order), face) + face_quad2 = mp.quadrature(mp.space_for_shape(face, 2*order+2), face) fmm = nodal_mass_matrix_for_face( face, face_quad, face_basis.functions, volume_basis.functions, volume_nodes, face_nodes) @@ -367,7 +367,7 @@ def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): shape = shape_cls(dims) space = mp.space_for_shape(shape, order) - nodes = mp.edge_clustered_nodes_for_space(space) + nodes = mp.edge_clustered_nodes(space, shape) from modepy.tools import estimate_lebesgue_constant lebesgue_constant = estimate_lebesgue_constant(order, nodes, shape=shape) From 7f5ec1e25f24cd074e53c78c3a6da117898c62ca Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 22:02:24 -0600 Subject: [PATCH 47/68] Drop a special case in node_tuples_for_space:Simplex --- modepy/nodes.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/modepy/nodes.py b/modepy/nodes.py index 40e60d01..40ef470e 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -439,11 +439,7 @@ def _(shape: Simplex, nnodes: int, rng=None): def _(space: QN): from pytools import \ generate_nonnegative_integer_tuples_below as gnitb - # FIXME: Why? - if space.spatial_dim == 0: - return ((0,),) - else: - return tuple(gnitb(space.order, space.spatial_dim)) + return tuple(gnitb(space.order, space.spatial_dim)) @equispaced_nodes.register(QN) From b1f5de41672885ff56a5e18be338bdaa2e81f447 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 22:10:28 -0600 Subject: [PATCH 48/68] Bring back _for_space names for quadrature, nodes --- modepy/__init__.py | 8 ++++---- modepy/matrices.py | 4 ++-- modepy/nodes.py | 16 ++++++++-------- modepy/quadrature/__init__.py | 8 ++++---- test/test_modes.py | 2 +- test/test_tools.py | 28 +++++++++++++++------------- 6 files changed, 34 insertions(+), 32 deletions(-) diff --git a/modepy/__init__.py b/modepy/__init__.py index abface7b..50832c47 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -47,7 +47,7 @@ tensor_product_nodes, legendre_gauss_lobatto_tensor_product_nodes, node_tuples_for_space, - equispaced_nodes, edge_clustered_nodes, + equispaced_nodes_for_space, edge_clustered_nodes_for_space, random_nodes_for_shape) from modepy.matrices import (vandermonde, resampling_matrix, differentiation_matrices, @@ -58,7 +58,7 @@ from modepy.quadrature import ( Quadrature, QuadratureRuleUnavailable, TensorProductQuadrature, LegendreGaussTensorProductQuadrature, - quadrature) + quadrature_for_space) from modepy.quadrature.jacobi_gauss import ( JacobiGaussQuadrature, LegendreGaussQuadrature, ChebyshevGaussQuadrature, GaussGegenbauerQuadrature, @@ -98,7 +98,7 @@ "equidistant_nodes", "warp_and_blend_nodes", "tensor_product_nodes", "legendre_gauss_lobatto_tensor_product_nodes", "node_tuples_for_space", - "edge_clustered_nodes", "equispaced_nodes", + "edge_clustered_nodes_for_space", "equispaced_nodes_for_space", "random_nodes_for_shape", "vandermonde", "resampling_matrix", "differentiation_matrices", @@ -109,7 +109,7 @@ "Quadrature", "QuadratureRuleUnavailable", "TensorProductQuadrature", "LegendreGaussTensorProductQuadrature", - "quadrature", + "quadrature_for_space", "JacobiGaussQuadrature", "LegendreGaussQuadrature", "GaussLegendreQuadrature", "ChebyshevGaussQuadrature", diff --git a/modepy/matrices.py b/modepy/matrices.py index f7234a0c..b237e7ce 100644 --- a/modepy/matrices.py +++ b/modepy/matrices.py @@ -294,8 +294,8 @@ def modal_face_mass_matrix(trial_basis, order, face_vertices, test_basis=None): vol_dims = face_vertices.shape[0] - from modepy.quadrature import quadrature - quad = quadrature(PN(vol_dims - 1, order*2), Simplex(vol_dims-1)) + from modepy.quadrature import quadrature_for_space + quad = quadrature_for_space(PN(vol_dims - 1, order*2), Simplex(vol_dims-1)) assert quad.exact_to >= order*2 diff --git a/modepy/nodes.py b/modepy/nodes.py index 40ef470e..a2fc8146 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -7,8 +7,8 @@ .. currentmodule:: modepy .. autofunction:: node_tuples_for_space -.. autofunction:: equispaced_nodes -.. autofunction:: edge_clustered_nodes +.. autofunction:: equispaced_nodes_for_space +.. autofunction:: edge_clustered_nodes_for_space .. autofunction:: random_nodes_for_shape Simplices @@ -362,12 +362,12 @@ def node_tuples_for_space(space: FunctionSpace) -> List[Tuple[int]]: @singledispatch -def equispaced_nodes(space: FunctionSpace, shape: Shape): +def equispaced_nodes_for_space(space: FunctionSpace, shape: Shape): raise NotImplementedError((type(space).__name__, type(shape).__name)) @singledispatch -def edge_clustered_nodes(space: FunctionSpace, shape: Shape): +def edge_clustered_nodes_for_space(space: FunctionSpace, shape: Shape): raise NotImplementedError((type(space).__name__, type(shape).__name)) @@ -392,7 +392,7 @@ def _(space: PN): return tuple(gnitsam(space.order, space.spatial_dim)) -@equispaced_nodes.register(PN) +@equispaced_nodes_for_space.register(PN) def _(space: PN, shape: Simplex): if not isinstance(shape, Simplex): raise NotImplementedError((type(space).__name__, type(shape).__name)) @@ -403,7 +403,7 @@ def _(space: PN, shape: Simplex): / space.order*2 - 1).T -@edge_clustered_nodes.register(PN) +@edge_clustered_nodes_for_space.register(PN) def _(space: PN, shape: Simplex): if not isinstance(shape, Simplex): raise NotImplementedError((type(space).__name__, type(shape).__name)) @@ -442,7 +442,7 @@ def _(space: QN): return tuple(gnitb(space.order, space.spatial_dim)) -@equispaced_nodes.register(QN) +@equispaced_nodes_for_space.register(QN) def _(space: QN, shape: Hypercube): if not isinstance(shape, Hypercube): raise NotImplementedError((type(space).__name__, type(shape).__name)) @@ -453,7 +453,7 @@ def _(space: QN, shape: Hypercube): / space.order*2 - 1).T -@edge_clustered_nodes.register(QN) +@edge_clustered_nodes_for_space.register(QN) def _(space: QN, shape: Hypercube): if not isinstance(shape, Hypercube): raise NotImplementedError((type(space).__name__, type(shape).__name)) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 66bab796..67f37be9 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -3,7 +3,7 @@ .. autoclass:: Quadrature -.. autoclass:: quadrature +.. autofunction:: quadrature_for_space Redirections to Canonical Names ------------------------------- @@ -137,7 +137,7 @@ def __init__(self, N, dims, backend=None): # noqa: N803 # {{{ quadrature @singledispatch -def quadrature(space: FunctionSpace, shape: Shape) -> Quadrature: +def quadrature_for_space(space: FunctionSpace, shape: Shape) -> Quadrature: """ :returns: a :class:`~modepy.Quadrature` that exactly integrates the functions in *space*. @@ -145,7 +145,7 @@ def quadrature(space: FunctionSpace, shape: Shape) -> Quadrature: raise NotImplementedError((type(space).__name__, type(shape).__name)) -@quadrature.register(PN) +@quadrature_for_space.register(PN) def _(space: PN, shape: Simplex): if not isinstance(shape, Simplex): raise NotImplementedError((type(space).__name__, type(shape).__name)) @@ -164,7 +164,7 @@ def _(space: PN, shape: Simplex): return quad -@quadrature.register(QN) +@quadrature_for_space.register(QN) def _(space: QN, shape: Hypercube): if not isinstance(shape, Hypercube): raise NotImplementedError((type(space).__name__, type(shape).__name)) diff --git a/test/test_modes.py b/test/test_modes.py index a8de734a..f50ca904 100644 --- a/test/test_modes.py +++ b/test/test_modes.py @@ -87,7 +87,7 @@ def test_orthogonality(shape, order, ebound): """Test orthogonality of ONBs using cubature.""" qspace = mp.space_for_shape(shape, 2*order) - cub = mp.quadrature(qspace, shape) + cub = mp.quadrature_for_space(qspace, shape) basis = mp.orthonormal_basis_for_space(mp.space_for_shape(shape, order)) maxerr = 0 diff --git a/test/test_tools.py b/test/test_tools.py index 3894ed77..b1921171 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -149,10 +149,10 @@ def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): coarse_space = mp.space_for_shape(shape, ncoarse) fine_space = mp.space_for_shape(shape, nfine) - coarse_nodes = mp.edge_clustered_nodes(coarse_space, shape) + coarse_nodes = mp.edge_clustered_nodes_for_space(coarse_space, shape) coarse_basis = mp.basis_for_space(coarse_space) - fine_nodes = mp.edge_clustered_nodes(fine_space, shape) + fine_nodes = mp.edge_clustered_nodes_for_space(fine_space, shape) fine_basis = mp.basis_for_space(fine_space) my_eye = np.dot( @@ -179,7 +179,7 @@ def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): def test_diff_matrix(dims, shape_cls, order=5): shape = shape_cls(dims) space = mp.space_for_shape(shape, order) - nodes = mp.edge_clustered_nodes(space, shape) + nodes = mp.edge_clustered_nodes_for_space(space, shape) basis = mp.basis_for_space(space) diff_mat = mp.differentiation_matrices(basis.functions, basis.gradients, nodes) @@ -257,13 +257,13 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): vol_space = mp.space_for_shape(vol_shape, order) vertices = mp.biunit_vertices_for_shape(vol_shape) - volume_nodes = mp.edge_clustered_nodes(vol_space, vol_shape) + volume_nodes = mp.edge_clustered_nodes_for_space(vol_space, vol_shape) volume_basis = mp.basis_for_space(vol_space) from modepy.matrices import nodal_face_mass_matrix for face in mp.faces_for_shape(vol_shape): face_space = mp.space_for_shape(face, order) - face_nodes = mp.edge_clustered_nodes(face_space, face) + face_nodes = mp.edge_clustered_nodes_for_space(face_space, face) face_vertices = vertices[:, face.volume_vertex_indices] fmm = nodal_face_mass_matrix( @@ -284,7 +284,7 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): logger.info("mass matrix:\n%s", mp.mass_matrix( mp.basis_for_space(face_space).functions, - mp.edge_clustered_nodes(face_space, face))) + mp.edge_clustered_nodes_for_space(face_space, face))) # }}} @@ -302,8 +302,9 @@ def test_modal_mass_matrix_for_face(dims, shape_cls, order=3): for face in mp.faces_for_shape(vol_shape): face_space = mp.space_for_shape(face, order) face_basis = mp.basis_for_space(face_space) - face_quad = mp.quadrature(mp.space_for_shape(face, 2*order), face) - face_quad2 = mp.quadrature(mp.space_for_shape(face, 2*order+2), face) + face_quad = mp.quadrature_for_space(mp.space_for_shape(face, 2*order), face) + face_quad2 = mp.quadrature_for_space( + mp.space_for_shape(face, 2*order+2), face) fmm = modal_mass_matrix_for_face( face, face_quad, face_basis.functions, vol_basis.functions) fmm2 = modal_mass_matrix_for_face( @@ -325,16 +326,17 @@ def test_nodal_mass_matrix_for_face(dims, shape_cls, order=3): vol_shape = shape_cls(dims) vol_space = mp.space_for_shape(vol_shape, order) - volume_nodes = mp.edge_clustered_nodes(vol_space, vol_shape) + volume_nodes = mp.edge_clustered_nodes_for_space(vol_space, vol_shape) volume_basis = mp.basis_for_space(vol_space) from modepy.matrices import nodal_mass_matrix_for_face for face in mp.faces_for_shape(vol_shape): face_space = mp.space_for_shape(face, order) face_basis = mp.basis_for_space(face_space) - face_nodes = mp.edge_clustered_nodes(face_space, face) - face_quad = mp.quadrature(mp.space_for_shape(face, 2*order), face) - face_quad2 = mp.quadrature(mp.space_for_shape(face, 2*order+2), face) + face_nodes = mp.edge_clustered_nodes_for_space(face_space, face) + face_quad = mp.quadrature_for_space(mp.space_for_shape(face, 2*order), face) + face_quad2 = mp.quadrature_for_space( + mp.space_for_shape(face, 2*order+2), face) fmm = nodal_mass_matrix_for_face( face, face_quad, face_basis.functions, volume_basis.functions, volume_nodes, face_nodes) @@ -367,7 +369,7 @@ def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): shape = shape_cls(dims) space = mp.space_for_shape(shape, order) - nodes = mp.edge_clustered_nodes(space, shape) + nodes = mp.edge_clustered_nodes_for_space(space, shape) from modepy.tools import estimate_lebesgue_constant lebesgue_constant = estimate_lebesgue_constant(order, nodes, shape=shape) From 4917e6213339121276f604a10cae3db0699b058d Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Mon, 30 Nov 2020 23:33:15 -0600 Subject: [PATCH 49/68] Deprecate tools.unit_vertices, fix biunit_vertices_for_shape for simplex --- modepy/shapes.py | 9 +++++++-- modepy/tools.py | 25 ++++++++----------------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/modepy/shapes.py b/modepy/shapes.py index 3fb9578a..5cf162b8 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -305,8 +305,13 @@ class _SimplexFace(Simplex, Face): @biunit_vertices_for_shape.register(Simplex) def _(shape: Simplex): - from modepy.tools import unit_vertices - return unit_vertices(shape.dim).T.copy() + result = np.empty((shape.dim, shape.dim+1), np.float64) + result.fill(-1) + + for i in range(shape.dim): + result[i, i+1] = 1 + + return result def _simplex_face_to_vol_map(face_vertices, p: np.ndarray): diff --git a/modepy/tools.py b/modepy/tools.py index a68c0451..9c99da3b 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -189,30 +189,21 @@ def equilateral_to_unit(equi): def unit_vertices(dim): - result = np.empty((dim+1, dim), np.float64) - result.fill(-1) - - for i in range(dim): - result[i+1, i] = 1 - - return result - + from warnings import warn + warn("unit_vertices is deprecated. " + "Use modepy.biunit_vertices_for_shape instead. " + "unit_vertices will go away in 2022.", + DeprecationWarning, stacklevel=2) -# this should go away -UNIT_VERTICES = { - 0: unit_vertices(0), - 1: unit_vertices(1), - 2: unit_vertices(2), - 3: unit_vertices(3), - } + return shp.biunit_vertices_for_shape(shp.Simplex(dim)).T def barycentric_to_unit(bary): """ - :arg bary: shaped ``(dims+1,npoints)`` + :arg bary: shaped ``(dims+1, npoints)`` """ dims = len(bary)-1 - return np.dot(unit_vertices(dims).T, bary) + return np.dot(shp.biunit_vertices_for_shape(shp.Simplex(dims)), bary) def unit_to_barycentric(unit): From 0e04d2cbfb8d259c08e3b9806ee8775008805506 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 1 Dec 2020 20:23:58 -0600 Subject: [PATCH 50/68] make face vertex index order positively oriented --- modepy/shapes.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/modepy/shapes.py b/modepy/shapes.py index 5cf162b8..20765690 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -327,19 +327,19 @@ def _simplex_face_to_vol_map(face_vertices, p: np.ndarray): @faces_for_shape.register(Simplex) def _(shape: Simplex): - face_vertex_indices = np.empty((shape.dim + 1, shape.dim), dtype=np.int) - indices = np.arange(shape.dim + 1) - - for iface in range(shape.nfaces): - face_vertex_indices[iface, :] = \ - np.hstack([indices[:iface], indices[iface + 1:]]) + # NOTE: order is chosen to maintain a positive orientation + face_vertex_indices = { + 1: ((0,), (1,)), + 2: ((0, 1), (2, 0), (1, 2)), + 3: ((0, 2, 1), (0, 1, 3), (0, 3, 2), (1, 2, 3)) + }[shape.dim] vertices = biunit_vertices_for_shape(shape) return [ _SimplexFace( dim=shape.dim-1, volume_shape=shape, face_index=iface, - volume_vertex_indices=tuple(fvi), + volume_vertex_indices=fvi, map_to_volume=partial(_simplex_face_to_vol_map, vertices[:, fvi])) for iface, fvi in enumerate(face_vertex_indices)] @@ -386,19 +386,19 @@ def _hypercube_face_to_vol_map(face_vertices: np.ndarray, p: np.ndarray): @faces_for_shape.register(Hypercube) def _(shape: Hypercube): - # FIXME: replace by nicer n-dimensional formula + # NOTE: order is chosen to maintain a positive orientation face_vertex_indices = { 1: ((0b0,), (0b1,)), - 2: ((0b00, 0b01), (0b10, 0b11), (0b00, 0b10), (0b01, 0b11)), + 2: ((0b00, 0b01), (0b11, 0b10), (0b10, 0b00), (0b01, 0b11)), 3: ( - (0b000, 0b001, 0b010, 0b011,), + (0b000, 0b010, 0b001, 0b011,), (0b100, 0b101, 0b110, 0b111,), - (0b000, 0b010, 0b100, 0b110,), + (0b000, 0b100, 0b010, 0b110,), (0b001, 0b011, 0b101, 0b111,), (0b000, 0b001, 0b100, 0b101,), - (0b010, 0b011, 0b110, 0b111,), + (0b010, 0b110, 0b011, 0b111,), ) }[shape.dim] From b60addc54b05c31ed1d2ed2331eb1254052fad83 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 1 Dec 2020 20:24:11 -0600 Subject: [PATCH 51/68] flip hypercube node tuple order --- modepy/nodes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modepy/nodes.py b/modepy/nodes.py index a2fc8146..ae0cd221 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -439,7 +439,9 @@ def _(shape: Simplex, nnodes: int, rng=None): def _(space: QN): from pytools import \ generate_nonnegative_integer_tuples_below as gnitb - return tuple(gnitb(space.order, space.spatial_dim)) + return tuple([ + tp[::-1] for tp in gnitb(space.order + 1, space.spatial_dim) + ]) @equispaced_nodes_for_space.register(QN) From 542b534744200191f0ceb15ff25bf2ee8a9901e4 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 1 Dec 2020 20:30:47 -0600 Subject: [PATCH 52/68] fix tensor_product_basis order --- modepy/modes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index 52680048..debd0a3e 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -693,7 +693,7 @@ def tensor_product_basis(dims, basis_1d): DeprecationWarning, stacklevel=2) from modepy.nodes import node_tuples_for_space - mode_ids = node_tuples_for_space(QN(dims, len(basis_1d))) + mode_ids = node_tuples_for_space(QN(dims, len(basis_1d) - 1)) return tuple( _TensorProductBasisFunction(order, [basis_1d[i] for i in order]) @@ -716,7 +716,7 @@ def grad_tensor_product_basis(dims, basis_1d, grad_basis_1d): from pytools import wandering_element from modepy.nodes import node_tuples_for_space - mode_ids = node_tuples_for_space(QN(dims, len(basis_1d))) + mode_ids = node_tuples_for_space(QN(dims, len(basis_1d) - 1)) func = (basis_1d, grad_basis_1d) return tuple( From 991432f34e6f5703d4ff78e2d33d946a9e0aa042 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 1 Dec 2020 20:36:50 -0600 Subject: [PATCH 53/68] fall back to equidistant nodes for simplex dim > 3 --- modepy/nodes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/modepy/nodes.py b/modepy/nodes.py index ae0cd221..73c8d486 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -410,7 +410,10 @@ def _(space: PN, shape: Simplex): if space.spatial_dim != shape.dim: raise ValueError("spatial dimensions of shape and space must match") - return warp_and_blend_nodes(space.spatial_dim, space.order) + if space.spatial_dim <= 3: + return warp_and_blend_nodes(space.spatial_dim, space.order) + else: + return equidistant_nodes(space.spatial_dim, space.order) @random_nodes_for_shape.register(Simplex) From 752b3aeb2221849663a0b979fb2d6ea5db188b09 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Tue, 1 Dec 2020 20:40:31 -0600 Subject: [PATCH 54/68] fix sphinx directive --- modepy/quadrature/__init__.py | 5 ++--- modepy/spaces.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 67f37be9..ca1c05de 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -155,7 +155,7 @@ def _(space: PN, shape: Simplex): import modepy as mp try: quad = mp.XiaoGimbutasSimplexQuadrature(space.order, space.spatial_dim) - except mp.QuadratureRuleUnavailable: + except QuadratureRuleUnavailable: quad = mp.GrundmannMoellerSimplexQuadrature( space.order//2, space.spatial_dim) @@ -171,9 +171,8 @@ def _(space: QN, shape: Hypercube): if space.spatial_dim != shape.dim: raise ValueError("spatial dimensions of shape and space must match") - import modepy as mp if space.spatial_dim == 0: - quad = mp.Quadrature(np.empty((0, 1)), np.empty((0, 1))) + quad = Quadrature(np.empty((0, 1)), np.empty((0, 1))) else: from modepy.quadrature import LegendreGaussTensorProductQuadrature quad = LegendreGaussTensorProductQuadrature(space.order, space.spatial_dim) diff --git a/modepy/spaces.py b/modepy/spaces.py index f0898242..71718c00 100644 --- a/modepy/spaces.py +++ b/modepy/spaces.py @@ -7,7 +7,7 @@ .. autoclass:: FunctionSpace .. autoclass:: PN .. autoclass:: QN -.. autoclass:: space_for_shape +.. autofunction:: space_for_shape Redirections to Canonical Names ------------------------------- From 97abd453a35c3975b2ebe6c28b7d2b9ac4976634 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Wed, 2 Dec 2020 10:54:53 -0600 Subject: [PATCH 55/68] add shape to basis_for_space getters --- modepy/modes.py | 44 +++++++++++++++++++++++++++++++------------- modepy/shapes.py | 4 +++- modepy/tools.py | 2 +- test/test_modes.py | 11 +++++++---- test/test_tools.py | 27 ++++++++++++++------------- 5 files changed, 56 insertions(+), 32 deletions(-) diff --git a/modepy/modes.py b/modepy/modes.py index debd0a3e..5ee65ac5 100644 --- a/modepy/modes.py +++ b/modepy/modes.py @@ -30,7 +30,7 @@ import numpy as np from modepy.spaces import FunctionSpace, PN, QN - +from modepy.shapes import Shape, Simplex, Hypercube __doc__ = """This functionality provides sets of basis functions for the reference elements in :mod:`modepy.shapes`. @@ -628,7 +628,7 @@ def simplex_best_available_basis(dims, n): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - return basis_for_space(PN(dims, n)).functions + return basis_for_space(PN(dims, n), Simplex(dims)).functions def grad_simplex_best_available_basis(dims, n): @@ -637,7 +637,7 @@ def grad_simplex_best_available_basis(dims, n): "This function will go away in 2022.", DeprecationWarning, stacklevel=2) - return basis_for_space(PN(dims, n)).gradients + return basis_for_space(PN(dims, n), Simplex(dims)).gradients # }}} @@ -833,17 +833,17 @@ def gradients(self): # {{{ space-based basis retrieval @singledispatch -def basis_for_space(space: FunctionSpace) -> Basis: +def basis_for_space(space: FunctionSpace, shape: Shape) -> Basis: raise NotImplementedError(type(space).__name__) @singledispatch -def orthonormal_basis_for_space(space: FunctionSpace) -> Basis: +def orthonormal_basis_for_space(space: FunctionSpace, shape: Shape) -> Basis: raise NotImplementedError(type(space).__name__) @singledispatch -def monomial_basis_for_space(space: FunctionSpace) -> Basis: +def monomial_basis_for_space(space: FunctionSpace, shape: Shape) -> Basis: raise NotImplementedError(type(space).__name__) # }}} @@ -929,7 +929,10 @@ def gradients(self): @basis_for_space.register(PN) -def _(space: PN): +def _(space: PN, shape: Simplex): + if not isinstance(shape, Simplex): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + if space.spatial_dim <= 3: return _SimplexONB(space.spatial_dim, space.order) else: @@ -937,12 +940,18 @@ def _(space: PN): @orthonormal_basis_for_space.register(PN) -def _(space: PN): +def _(space: PN, shape: Simplex): + if not isinstance(shape, Simplex): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + return _SimplexONB(space.spatial_dim, space.order) @monomial_basis_for_space.register(PN) -def _(space: PN): +def _(space: PN, shape: Simplex): + if not isinstance(shape, Simplex): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + return _SimplexMonomialBasis(space.spatial_dim, space.order) # }}} @@ -1015,7 +1024,10 @@ def gradients(self): @orthonormal_basis_for_space.register(QN) -def _(space: QN): +def _(space: QN, shape: Hypercube): + if not isinstance(shape, Hypercube): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + order = space.order dim = space.spatial_dim return TensorProductBasis( @@ -1025,8 +1037,11 @@ def _(space: QN): @basis_for_space.register(QN) -def _(space: QN): - return orthonormal_basis_for_space(space) +def _(space: QN, shape: Hypercube): + if not isinstance(shape, Hypercube): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + + return orthonormal_basis_for_space(space, shape) def _monomial_1d(order, r): @@ -1041,7 +1056,10 @@ def _grad_monomial_1d(order, r): @monomial_basis_for_space.register(QN) -def _(space: QN): +def _(space: QN, shape: Hypercube): + if not isinstance(shape, Hypercube): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + order = space.order dim = space.spatial_dim return TensorProductBasis( diff --git a/modepy/shapes.py b/modepy/shapes.py index 20765690..884a6abf 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -528,13 +528,15 @@ def _(shape: Hypercube, node_tuples): for idx, ituple in enumerate(node_tuples)} from pytools import generate_nonnegative_integer_tuples_below as gnitb + vertex_node_tuples = [nt[::-1] for nt in gnitb(2, dims)] result = [] for current in node_tuples: try: result.append(tuple( node_dict[add_tuples(current, offset)] - for offset in gnitb(2, dims))) + for offset in vertex_node_tuples + )) except KeyError: pass diff --git a/modepy/tools.py b/modepy/tools.py index 9c99da3b..70a1da19 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -339,7 +339,7 @@ def _evaluate_lebesgue_function(n, nodes, shape): space = space_for_shape(shape, n) huge_space = space_for_shape(shape, huge_n) - basis = basis_for_space(space) + basis = basis_for_space(space, shape) equi_node_tuples = node_tuples_for_space(huge_space) equi_nodes = (np.array(equi_node_tuples, dtype=np.float64)/huge_n*2 - 1).T diff --git a/test/test_modes.py b/test/test_modes.py index f50ca904..29fc7c1b 100644 --- a/test/test_modes.py +++ b/test/test_modes.py @@ -88,7 +88,7 @@ def test_orthogonality(shape, order, ebound): qspace = mp.space_for_shape(shape, 2*order) cub = mp.quadrature_for_space(qspace, shape) - basis = mp.orthonormal_basis_for_space(mp.space_for_shape(shape, order)) + basis = mp.orthonormal_basis_for_space(mp.space_for_shape(shape, order), shape) maxerr = 0 for i, f in enumerate(basis.functions): @@ -107,7 +107,10 @@ def test_orthogonality(shape, order, ebound): # print(order, maxerr) -def get_inhomogeneous_tensor_prod_basis(space): +def get_inhomogeneous_tensor_prod_basis(space, shape): + if not isinstance(shape, mp.Hypercube): + raise NotImplementedError((type(space).__name__, type(shape).__name)) + # FIXME: Yuck. A total lie. Not a basis for the space at all. assert isinstance(space, mp.QN) orders = (3, 5, 7)[:space.spatial_dim] @@ -140,7 +143,7 @@ def test_basis_grad(dim, shape_cls, order, basis_getter): shape = shape_cls(dim) rng = np.random.Generator(np.random.PCG64(17)) - basis = basis_getter(mp.space_for_shape(shape, order)) + basis = basis_getter(mp.space_for_shape(shape, order), shape) from pytools.convergence import EOCRecorder from pytools import wandering_element @@ -200,7 +203,7 @@ def map_if(self, expr): (mp.monomial_basis_for_space), ]) def test_symbolic_basis(shape, order, basis_getter): - basis = basis_getter(mp.space_for_shape(shape, order)) + basis = basis_getter(mp.space_for_shape(shape, order), shape) sym_basis = [mp.symbolicize_function(f, shape.dim) for f in basis.functions] # {{{ test symbolic against direct eval diff --git a/test/test_tools.py b/test/test_tools.py index b1921171..c9c2e55b 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -90,7 +90,7 @@ def constant(x): def test_modal_decay(case_name, test_func, dims, n, expected_expn): space = mp.PN(dims, n) nodes = mp.warp_and_blend_nodes(dims, n) - basis = mp.orthonormal_basis_for_space(space) + basis = mp.orthonormal_basis_for_space(space, mp.Simplex(dims)) vdm = mp.vandermonde(basis.functions, nodes) f = test_func(nodes[0]) @@ -121,7 +121,8 @@ def test_modal_decay(case_name, test_func, dims, n, expected_expn): def test_residual_estimation(case_name, test_func, dims, n): def estimate_resid(inner_n): nodes = mp.warp_and_blend_nodes(dims, inner_n) - basis = mp.orthonormal_basis_for_space(mp.PN(dims, inner_n)) + basis = mp.orthonormal_basis_for_space( + mp.PN(dims, inner_n), mp.Simplex(dims)) vdm = mp.vandermonde(basis.functions, nodes) f = test_func(nodes[0]) @@ -150,10 +151,10 @@ def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): fine_space = mp.space_for_shape(shape, nfine) coarse_nodes = mp.edge_clustered_nodes_for_space(coarse_space, shape) - coarse_basis = mp.basis_for_space(coarse_space) + coarse_basis = mp.basis_for_space(coarse_space, shape) fine_nodes = mp.edge_clustered_nodes_for_space(fine_space, shape) - fine_basis = mp.basis_for_space(fine_space) + fine_basis = mp.basis_for_space(fine_space, shape) my_eye = np.dot( mp.resampling_matrix(fine_basis.functions, coarse_nodes, fine_nodes), @@ -180,7 +181,7 @@ def test_diff_matrix(dims, shape_cls, order=5): shape = shape_cls(dims) space = mp.space_for_shape(shape, order) nodes = mp.edge_clustered_nodes_for_space(space, shape) - basis = mp.basis_for_space(space) + basis = mp.basis_for_space(space, shape) diff_mat = mp.differentiation_matrices(basis.functions, basis.gradients, nodes) if isinstance(diff_mat, tuple): @@ -205,7 +206,7 @@ def test_diff_matrix_permutation(dims): generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam node_tuples = list(gnitstam(order, dims)) - simplex_onb = mp.orthonormal_basis_for_space(space) + simplex_onb = mp.orthonormal_basis_for_space(space, mp.Simplex(dims)) nodes = np.array(mp.warp_and_blend_nodes(dims, order, node_tuples=node_tuples)) diff_matrices = mp.differentiation_matrices( simplex_onb.functions, simplex_onb.gradients, nodes) @@ -229,7 +230,7 @@ def test_deprecated_modal_face_mass_matrix(dims, order=3): space = mp.space_for_shape(shape, order) vertices = mp.biunit_vertices_for_shape(shape) - basis = mp.basis_for_space(space) + basis = mp.basis_for_space(space, shape) from modepy.matrices import modal_face_mass_matrix for face in mp.faces_for_shape(shape): @@ -258,7 +259,7 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): vertices = mp.biunit_vertices_for_shape(vol_shape) volume_nodes = mp.edge_clustered_nodes_for_space(vol_space, vol_shape) - volume_basis = mp.basis_for_space(vol_space) + volume_basis = mp.basis_for_space(vol_space, vol_shape) from modepy.matrices import nodal_face_mass_matrix for face in mp.faces_for_shape(vol_shape): @@ -283,7 +284,7 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): logger.info("fmm: nnz %d\n%s", nnz, fmm) logger.info("mass matrix:\n%s", mp.mass_matrix( - mp.basis_for_space(face_space).functions, + mp.basis_for_space(face_space, face).functions, mp.edge_clustered_nodes_for_space(face_space, face))) # }}} @@ -296,12 +297,12 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): def test_modal_mass_matrix_for_face(dims, shape_cls, order=3): vol_shape = shape_cls(dims) vol_space = mp.space_for_shape(vol_shape, order) - vol_basis = mp.basis_for_space(vol_space) + vol_basis = mp.basis_for_space(vol_space, vol_shape) from modepy.matrices import modal_mass_matrix_for_face for face in mp.faces_for_shape(vol_shape): face_space = mp.space_for_shape(face, order) - face_basis = mp.basis_for_space(face_space) + face_basis = mp.basis_for_space(face_space, face) face_quad = mp.quadrature_for_space(mp.space_for_shape(face, 2*order), face) face_quad2 = mp.quadrature_for_space( mp.space_for_shape(face, 2*order+2), face) @@ -327,12 +328,12 @@ def test_nodal_mass_matrix_for_face(dims, shape_cls, order=3): vol_space = mp.space_for_shape(vol_shape, order) volume_nodes = mp.edge_clustered_nodes_for_space(vol_space, vol_shape) - volume_basis = mp.basis_for_space(vol_space) + volume_basis = mp.basis_for_space(vol_space, vol_shape) from modepy.matrices import nodal_mass_matrix_for_face for face in mp.faces_for_shape(vol_shape): face_space = mp.space_for_shape(face, order) - face_basis = mp.basis_for_space(face_space) + face_basis = mp.basis_for_space(face_space, face) face_nodes = mp.edge_clustered_nodes_for_space(face_space, face) face_quad = mp.quadrature_for_space(mp.space_for_shape(face, 2*order), face) face_quad2 = mp.quadrature_for_space( From 338e1aae8e6989f54dd0d509097e9c33a22c0a0f Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Wed, 2 Dec 2020 11:09:45 -0600 Subject: [PATCH 56/68] rename to unit_vertices_for_shape for consistency --- modepy/__init__.py | 4 ++-- modepy/shapes.py | 14 +++++++------- modepy/tools.py | 6 +++--- test/test_tools.py | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/modepy/__init__.py b/modepy/__init__.py index 50832c47..d26a6bd5 100644 --- a/modepy/__init__.py +++ b/modepy/__init__.py @@ -25,7 +25,7 @@ from modepy.shapes import ( Shape, Face, Simplex, Hypercube, - biunit_vertices_for_shape, faces_for_shape, + unit_vertices_for_shape, faces_for_shape, submesh_for_shape, ) from modepy.spaces import ( @@ -78,7 +78,7 @@ "__version__", "Shape", "Face", "Simplex", "Hypercube", - "biunit_vertices_for_shape", "faces_for_shape", + "unit_vertices_for_shape", "faces_for_shape", "submesh_for_shape", "FunctionSpace", "PN", "QN", "space_for_shape", diff --git a/modepy/shapes.py b/modepy/shapes.py index 884a6abf..e69919a2 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -8,7 +8,7 @@ .. autoclass:: Shape .. autoclass:: Face -.. autofunction:: biunit_vertices_for_shape +.. autofunction:: unit_vertices_for_shape .. autofunction:: faces_for_shape Simplices @@ -239,7 +239,7 @@ class Shape: @singledispatch -def biunit_vertices_for_shape(shape: Shape): +def unit_vertices_for_shape(shape: Shape): """ :returns: an :class:`~numpy.ndarray` of shape `(dim, nvertices)`. """ @@ -262,7 +262,7 @@ class Face: .. attribute:: volume_vertex_indices A tuple of indices into the vertices returned by - :func:`biunit_vertices_for_shape` for the :attr:`volume_shape`. + :func:`unit_vertices_for_shape` for the :attr:`volume_shape`. .. attribute:: map_to_volume @@ -303,7 +303,7 @@ class _SimplexFace(Simplex, Face): pass -@biunit_vertices_for_shape.register(Simplex) +@unit_vertices_for_shape.register(Simplex) def _(shape: Simplex): result = np.empty((shape.dim, shape.dim+1), np.float64) result.fill(-1) @@ -334,7 +334,7 @@ def _(shape: Simplex): 3: ((0, 2, 1), (0, 1, 3), (0, 3, 2), (1, 2, 3)) }[shape.dim] - vertices = biunit_vertices_for_shape(shape) + vertices = unit_vertices_for_shape(shape) return [ _SimplexFace( dim=shape.dim-1, @@ -363,7 +363,7 @@ class _HypercubeFace(Hypercube, Face): pass -@biunit_vertices_for_shape.register(Hypercube) +@unit_vertices_for_shape.register(Hypercube) def _(shape: Hypercube): from modepy.nodes import tensor_product_nodes return tensor_product_nodes(shape.dim, np.array([-1.0, 1.0])) @@ -402,7 +402,7 @@ def _(shape: Hypercube): ) }[shape.dim] - vertices = biunit_vertices_for_shape(shape) + vertices = unit_vertices_for_shape(shape) return [ _HypercubeFace( dim=shape.dim-1, diff --git a/modepy/tools.py b/modepy/tools.py index 70a1da19..a7b0e0a7 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -191,11 +191,11 @@ def equilateral_to_unit(equi): def unit_vertices(dim): from warnings import warn warn("unit_vertices is deprecated. " - "Use modepy.biunit_vertices_for_shape instead. " + "Use modepy.unit_vertices_for_shape instead. " "unit_vertices will go away in 2022.", DeprecationWarning, stacklevel=2) - return shp.biunit_vertices_for_shape(shp.Simplex(dim)).T + return shp.unit_vertices_for_shape(shp.Simplex(dim)).T def barycentric_to_unit(bary): @@ -203,7 +203,7 @@ def barycentric_to_unit(bary): :arg bary: shaped ``(dims+1, npoints)`` """ dims = len(bary)-1 - return np.dot(shp.biunit_vertices_for_shape(shp.Simplex(dims)), bary) + return np.dot(shp.unit_vertices_for_shape(shp.Simplex(dims)), bary) def unit_to_barycentric(unit): diff --git a/test/test_tools.py b/test/test_tools.py index c9c2e55b..4b5ce126 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -229,7 +229,7 @@ def test_deprecated_modal_face_mass_matrix(dims, order=3): shape = mp.Simplex(dims) space = mp.space_for_shape(shape, order) - vertices = mp.biunit_vertices_for_shape(shape) + vertices = mp.unit_vertices_for_shape(shape) basis = mp.basis_for_space(space, shape) from modepy.matrices import modal_face_mass_matrix @@ -257,7 +257,7 @@ def test_deprecated_nodal_face_mass_matrix(dims, order=3): vol_shape = mp.Simplex(dims) vol_space = mp.space_for_shape(vol_shape, order) - vertices = mp.biunit_vertices_for_shape(vol_shape) + vertices = mp.unit_vertices_for_shape(vol_shape) volume_nodes = mp.edge_clustered_nodes_for_space(vol_space, vol_shape) volume_basis = mp.basis_for_space(vol_space, vol_shape) From a6f8939a0cb694495b6c55465979374ba0a63521 Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 2 Dec 2020 11:22:17 -0600 Subject: [PATCH 57/68] Add FIXMEs for order, dims arg order convention --- modepy/quadrature/grundmann_moeller.py | 2 ++ modepy/quadrature/vioreanu_rokhlin.py | 2 ++ modepy/quadrature/witherden_vincent.py | 2 ++ modepy/quadrature/xiao_gimbutas.py | 2 ++ 4 files changed, 8 insertions(+) diff --git a/modepy/quadrature/grundmann_moeller.py b/modepy/quadrature/grundmann_moeller.py index 3fcc392b..94e0f19c 100644 --- a/modepy/quadrature/grundmann_moeller.py +++ b/modepy/quadrature/grundmann_moeller.py @@ -84,6 +84,8 @@ class GrundmannMoellerSimplexQuadrature(Quadrature): .. automethod:: __call__ """ + # FIXME: most other functionality in modepy uses 'dims, order' as the + # argument order convention. def __init__(self, order, dimension): """ :arg order: A parameter correlated with the total degree of polynomials diff --git a/modepy/quadrature/vioreanu_rokhlin.py b/modepy/quadrature/vioreanu_rokhlin.py index 1f6bc5ee..83f73244 100644 --- a/modepy/quadrature/vioreanu_rokhlin.py +++ b/modepy/quadrature/vioreanu_rokhlin.py @@ -71,6 +71,8 @@ class VioreanuRokhlinSimplexQuadrature(Quadrature): .. automethod:: __call__ """ + # FIXME: most other functionality in modepy uses 'dims, order' as the + # argument order convention. def __init__(self, order, dims): """ :arg order: The total degree to which the quadrature rule is exact diff --git a/modepy/quadrature/witherden_vincent.py b/modepy/quadrature/witherden_vincent.py index 2731d083..9091b27b 100644 --- a/modepy/quadrature/witherden_vincent.py +++ b/modepy/quadrature/witherden_vincent.py @@ -42,6 +42,8 @@ class WitherdenVincentQuadrature(Quadrature): .. automethod:: __call__ """ + # FIXME: most other functionality in modepy uses 'dims, order' as the + # argument order convention. def __init__(self, order, dims): if dims == 2: from modepy.quadrature.witherden_vincent_quad_data import \ diff --git a/modepy/quadrature/xiao_gimbutas.py b/modepy/quadrature/xiao_gimbutas.py index caa98e58..c61d61bd 100644 --- a/modepy/quadrature/xiao_gimbutas.py +++ b/modepy/quadrature/xiao_gimbutas.py @@ -52,6 +52,8 @@ class XiaoGimbutasSimplexQuadrature(Quadrature): .. automethod:: __call__ """ + # FIXME: most other functionality in modepy uses 'dims, order' as the + # argument order convention. def __init__(self, order, dims): """ :arg order: The total degree to which the quadrature rule is exact. From 10b7d27886b481fed2a582872d80b7568fa4bf7a Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Wed, 2 Dec 2020 11:48:11 -0600 Subject: [PATCH 58/68] Bring back tools.UNIT_VERTICES to make unmodified meshmode happy --- modepy/tools.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modepy/tools.py b/modepy/tools.py index a7b0e0a7..38663df2 100644 --- a/modepy/tools.py +++ b/modepy/tools.py @@ -198,6 +198,17 @@ def unit_vertices(dim): return shp.unit_vertices_for_shape(shp.Simplex(dim)).T +# FIXME This should go away, but as of 2020-12-02, it is still being used by +# meshmode. (The use is being removed in +# https://github.com/inducer/meshmode/pull/70, around the same time.) +UNIT_VERTICES = { + 0: unit_vertices(0), + 1: unit_vertices(1), + 2: unit_vertices(2), + 3: unit_vertices(3), + } + + def barycentric_to_unit(bary): """ :arg bary: shaped ``(dims+1, npoints)`` From 368bd7450731e009e3d759649e7c76680495ffef Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Thu, 3 Dec 2020 14:31:52 -0600 Subject: [PATCH 59/68] expand tensor_product_nodes --- modepy/nodes.py | 19 +++++++++++++++---- modepy/shapes.py | 6 ++---- test/test_nodes.py | 14 ++++++++++++++ 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/modepy/nodes.py b/modepy/nodes.py index 73c8d486..db82295b 100644 --- a/modepy/nodes.py +++ b/modepy/nodes.py @@ -329,7 +329,7 @@ def warp_and_blend_nodes(dims, n, node_tuples=None): # {{{ tensor product nodes -def tensor_product_nodes(dims, nodes_1d): +def tensor_product_nodes(dims_or_nodes, nodes_1d=None): """ :returns: an array of shape ``(dims, nnodes_1d**dims)``. @@ -338,11 +338,22 @@ def tensor_product_nodes(dims, nodes_1d): .. versionchanged:: 2020.3 The node ordering has changed and is no longer documented. + """ - nnodes_1d = len(nodes_1d) - result = np.empty((dims,) + (nnodes_1d,) * dims) + from numbers import Number + if isinstance(dims_or_nodes, Number): + nodes = [nodes_1d] * dims_or_nodes + dims = dims_or_nodes + else: + assert nodes_1d is None + nodes = dims_or_nodes + dims = len(nodes) + + nnodes = tuple(len(n) for n in nodes) + result = np.empty((dims,) + nnodes) + for d in range(dims): - result[d] = nodes_1d.reshape(*((-1,) + (1,)*d)) + result[d] = nodes[dims-1-d].reshape((-1,) + (1,)*d) return result.reshape(dims, -1) diff --git a/modepy/shapes.py b/modepy/shapes.py index e69919a2..a200bc0d 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -523,14 +523,12 @@ def _(shape: Hypercube, node_tuples): from pytools import single_valued, add_tuples dims = single_valued(len(nt) for nt in node_tuples) - node_dict = { - ituple: idx - for idx, ituple in enumerate(node_tuples)} - + # NOTE: nodes use "first coordinate varies faster" (see node_tuples_for_space) from pytools import generate_nonnegative_integer_tuples_below as gnitb vertex_node_tuples = [nt[::-1] for nt in gnitb(2, dims)] result = [] + node_dict = {ituple: idx for idx, ituple in enumerate(node_tuples)} for current in node_tuples: try: result.append(tuple( diff --git a/test/test_nodes.py b/test/test_nodes.py index 05f6941c..2df809fe 100644 --- a/test/test_nodes.py +++ b/test/test_nodes.py @@ -136,11 +136,25 @@ def test_tensor_product_nodes(dim): nnodes = 10 nodes_1d = np.arange(nnodes) nodes = nd.tensor_product_nodes(dim, nodes_1d) + assert np.allclose( nodes[0], np.array(nodes_1d.tolist() * nnodes**(dim - 1))) +@pytest.mark.parametrize("dim", [1, 2, 3, 4]) +def test_nonhomogeneous_tensor_product_nodes(dim): + nnodes = (3, 7, 5, 4)[:dim] + nodes = nd.tensor_product_nodes([ + np.arange(n) for n in nnodes + ]) + + assert np.allclose( + nodes[0], + list(range(nnodes[-1])) * int(np.prod(nnodes[:-1])) + ) + + # You can test individual routines by typing # $ python test_nodes.py 'test_routine()' From ef01010e3face20864f788b9a316714d2ca450cc Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Thu, 3 Dec 2020 14:32:17 -0600 Subject: [PATCH 60/68] expand tensor product quadrature --- modepy/quadrature/__init__.py | 18 +++++++----------- test/test_quadrature.py | 11 ++++++----- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index ca1c05de..43701b4f 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -109,29 +109,25 @@ class TensorProductQuadrature(Quadrature): .. automethod:: __init__ """ - def __init__(self, dims, quad): + def __init__(self, quads): """ - :arg quad: a :class:`Quadrature` class for one-dimensional intervals. + :arg quad: a :class:`tuple` of :class:`Quadrature` for one-dimensional + intervals, one for each dimension of the tensor product. """ from modepy.nodes import tensor_product_nodes - x = tensor_product_nodes(dims, quad.nodes) - from itertools import product - w = np.fromiter( - (np.prod(w) for w in product(quad.weights, repeat=dims)), - dtype=np.float, - count=quad.weights.size**dims) + x = tensor_product_nodes([quad.nodes for quad in quads]) + w = np.prod(tensor_product_nodes([quad.weights for quad in quads]), axis=0) assert w.size == x.shape[1] super().__init__(x, w) - self.exact_to = quad.exact_to + self.exact_to = min(quad.exact_to for quad in quads) class LegendreGaussTensorProductQuadrature(TensorProductQuadrature): def __init__(self, N, dims, backend=None): # noqa: N803 from modepy.quadrature.jacobi_gauss import LegendreGaussQuadrature - super().__init__( - dims, LegendreGaussQuadrature(N, backend=backend)) + super().__init__([LegendreGaussQuadrature(N, backend=backend)] * dims) # {{{ quadrature diff --git a/test/test_quadrature.py b/test/test_quadrature.py index 8a3912df..12ae1a2c 100644 --- a/test/test_quadrature.py +++ b/test/test_quadrature.py @@ -145,11 +145,12 @@ def test_simplex_quadrature(quad_class, highest_order, dim): break -@pytest.mark.parametrize("quad_class", [ - mp.WitherdenVincentQuadrature - ]) @pytest.mark.parametrize("dim", [2, 3]) -def test_hypercube_quadrature(quad_class, dim): +@pytest.mark.parametrize(("quad_class", "max_order"), [ + (mp.WitherdenVincentQuadrature, np.inf), + (mp.LegendreGaussTensorProductQuadrature, 6), + ]) +def test_hypercube_quadrature(dim, quad_class, max_order): from pytools import \ generate_nonnegative_integer_tuples_summing_to_at_most as gnitstam from modepy.tools import Monomial @@ -166,7 +167,7 @@ def _check_monomial(quad, comb): return error order = 1 - while True: + while order < max_order: try: quad = quad_class(order, dim) except mp.QuadratureRuleUnavailable: From a17c5440fdc5916cb69d5242103f88f1db58d034 Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Thu, 3 Dec 2020 14:32:46 -0600 Subject: [PATCH 61/68] update test_hypercube_submesh --- test/test_tools.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/test_tools.py b/test/test_tools.py index 4b5ce126..b8d1469c 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -411,23 +411,22 @@ def test_estimate_lebesgue_constant(dims, order, shape_cls, visualize=False): # {{{ test_hypercube_submesh @pytest.mark.parametrize("dims", [2, 3, 4]) -def test_hypercube_submesh(dims): - from pytools import generate_nonnegative_integer_tuples_below as gnitb +def test_hypercube_submesh(dims, order=3): shape = mp.Hypercube(dims) + space = mp.space_for_shape(shape, order) - node_tuples = list(gnitb(3, dims)) - + node_tuples = mp.node_tuples_for_space(space) for i, nt in enumerate(node_tuples): logger.info("[%4d] nodes %s", i, nt) - assert len(node_tuples) == 3**dims + assert len(node_tuples) == (order + 1)**dims elements = mp.submesh_for_shape(shape, node_tuples) for e in elements: logger.info("element: %s", e) - assert len(elements) == 2**dims + assert len(elements) == order**dims # }}} From 1fd772ec468a69c35a41c022ad05b59b3c4cb3dd Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Thu, 3 Dec 2020 15:12:20 -0600 Subject: [PATCH 62/68] add a nicer repr to function spaces --- modepy/spaces.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/modepy/spaces.py b/modepy/spaces.py index 71718c00..dc9ef53e 100644 --- a/modepy/spaces.py +++ b/modepy/spaces.py @@ -49,7 +49,6 @@ THE SOFTWARE. """ - from functools import singledispatch from modepy.shapes import Shape, Simplex, Hypercube @@ -70,6 +69,11 @@ class FunctionSpace: The number of dimensions of the function space. """ + def __repr__(self): + return (f"{type(self).__name__}(" + f"spatial_dim={self.spatial_dim}, space_dim={self.space_dim}" + ")") + class PN(FunctionSpace): r"""The function space of polynomials with total degree :math:`N`=:attr:`order`. @@ -99,6 +103,11 @@ def space_dim(self): return reduce(mul, range(order + 1, order + spdim + 1), 1) \ // reduce(mul, range(1, spdim + 1), 1) + def __repr__(self): + return (f"{type(self).__name__}(" + f"spatial_dim={self.spatial_dim}, order={self.order}" + ")") + class QN(FunctionSpace): r"""The function space of polynomials with maximum degree @@ -121,6 +130,11 @@ def __init__(self, spatial_dim, order): def space_dim(self): return (self.order + 1)**self.spatial_dim + def __repr__(self): + return (f"{type(self).__name__}(" + f"spatial_dim={self.spatial_dim}, order={self.order}" + ")") + @singledispatch def space_for_shape(shape: Shape, order: int) -> FunctionSpace: From 099e587c74a9dff9aff6112cd7e5c018a3b6261e Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Fri, 4 Dec 2020 13:14:27 -0600 Subject: [PATCH 63/68] add some docs for exact_to on TensorProductQuadrature --- modepy/quadrature/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 43701b4f..f53c5df8 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -106,6 +106,10 @@ def __init__(self, quad, left, right): class TensorProductQuadrature(Quadrature): """ + .. attribute:: exact_to + + The tensor product quadrature is exact up to this summed polynomial degree. + .. automethod:: __init__ """ From 877018ed470602e38acf9fb35d4950f1df8d802f Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Fri, 4 Dec 2020 15:01:47 -0600 Subject: [PATCH 64/68] add force_dim_axis to 1d quadratures --- modepy/quadrature/__init__.py | 4 -- modepy/quadrature/clenshaw_curtis.py | 25 ++++++++++++- modepy/quadrature/jacobi_gauss.py | 56 +++++++++++++++++++--------- modepy/spaces.py | 11 ++++-- 4 files changed, 69 insertions(+), 27 deletions(-) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index f53c5df8..43701b4f 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -106,10 +106,6 @@ def __init__(self, quad, left, right): class TensorProductQuadrature(Quadrature): """ - .. attribute:: exact_to - - The tensor product quadrature is exact up to this summed polynomial degree. - .. automethod:: __init__ """ diff --git a/modepy/quadrature/clenshaw_curtis.py b/modepy/quadrature/clenshaw_curtis.py index d5b4c2f1..63c18a17 100644 --- a/modepy/quadrature/clenshaw_curtis.py +++ b/modepy/quadrature/clenshaw_curtis.py @@ -112,8 +112,19 @@ class ClenshawCurtisQuadrature(Quadrature): Gauss-Legendre quadrature which is exact for polynomials of degree up to :math:`2N + 1`. """ - def __init__(self, N): # noqa + def __init__(self, N, force_dim_axis=False): # noqa: N803 + if not force_dim_axis: + from warnings import warn + warn("setting 'force_dim_axis' to 'False' is deprecated and " + "makes 1d rules inconsistent with higher dimensions. " + "This option will go away in 2022", + DeprecationWarning, stacklevel=2) + x, w = _fejer(N, "cc") + + if force_dim_axis: + x = x.reshape(1, -1) + self.exact_to = N Quadrature.__init__(self, x, w) @@ -135,7 +146,14 @@ class FejerQuadrature(Quadrature): Integrates on the interval :math:`(-1, 1)`. """ - def __init__(self, N, kind=1): # noqa + def __init__(self, N, kind=1, force_dim_axis=False): # noqa + if not force_dim_axis: + from warnings import warn + warn("setting 'force_dim_axis' to 'False' is deprecated and " + "makes 1d rules inconsistent with higher dimensions. " + "This option will go away in 2022", + DeprecationWarning, stacklevel=2) + if kind == 1: x, w = _fejer(N, "f1") elif kind == 2: @@ -143,6 +161,9 @@ def __init__(self, N, kind=1): # noqa else: raise ValueError("kind must be either 1 or 2") + if force_dim_axis: + x = x.reshape(1, -1) + super().__init__(x, w) @property diff --git a/modepy/quadrature/jacobi_gauss.py b/modepy/quadrature/jacobi_gauss.py index 96aa6486..be868787 100644 --- a/modepy/quadrature/jacobi_gauss.py +++ b/modepy/quadrature/jacobi_gauss.py @@ -44,7 +44,8 @@ class JacobiGaussQuadrature(Quadrature): .. automethod:: __init__ """ - def __init__(self, alpha, beta, N, backend=None): # noqa: N803 + def __init__(self, alpha, beta, N, # noqa: N803 + backend=None, force_dim_axis=False): r""" :arg backend: Either ``"builtin"`` or ``"scipy"``. When the ``"builtin"`` backend is in use, there is an additional @@ -52,6 +53,13 @@ def __init__(self, alpha, beta, N, backend=None): # noqa: N803 of the Chebyshev quadrature :math:`\alpha = \beta = -1/2`. The ``"scipy"`` backend has no such restriction. """ + if not force_dim_axis: + from warnings import warn + warn("setting 'force_dim_axis' to 'False' is deprecated and " + "makes 1d rules inconsistent with higher dimensions. " + "This option will go away in 2022", + DeprecationWarning, stacklevel=2) + if backend is None: backend = "builtin" @@ -63,6 +71,9 @@ def __init__(self, alpha, beta, N, backend=None): # noqa: N803 else: raise NotImplementedError("Unsupported backend: %s" % backend) + if force_dim_axis: + x = x.reshape(1, -1) + self.exact_to = 2*N + 1 Quadrature.__init__(self, x, w) @@ -147,8 +158,9 @@ class LegendreGaussQuadrature(JacobiGaussQuadrature): :math:`\alpha = \beta = 0`. """ - def __init__(self, N, backend=None): # noqa: N803 - JacobiGaussQuadrature.__init__(self, 0, 0, N, backend) + def __init__(self, N, backend=None, force_dim_axis=False): # noqa: N803 + super().__init__(0, 0, N, + backend=backend, force_dim_axis=force_dim_axis) class ChebyshevGaussQuadrature(JacobiGaussQuadrature): @@ -162,12 +174,16 @@ class ChebyshevGaussQuadrature(JacobiGaussQuadrature): .. versionadded:: 2019.1 """ - def __init__(self, N, kind=1, backend=None): # noqa: N803 + def __init__(self, N, kind=1, backend=None, force_dim_axis=False): # noqa: N803 if kind == 1: - # FIXME: division by zero using built-in backend - JacobiGaussQuadrature.__init__(self, -0.5, -0.5, N, backend="scipy") + alpha = beta = -0.5 elif kind == 2: - JacobiGaussQuadrature.__init__(self, 0.5, 0.5, N, backend=backend) + alpha = beta = +0.5 + else: + raise ValueError(f"unsupported kind: '{kind}'") + + super().__init__(alpha, beta, N, + backend=backend, force_dim_axis=force_dim_axis) class GaussGegenbauerQuadrature(JacobiGaussQuadrature): @@ -177,11 +193,13 @@ class GaussGegenbauerQuadrature(JacobiGaussQuadrature): .. versionadded:: 2019.1 """ - def __init__(self, alpha, N, backend=None): # noqa: N803 - JacobiGaussQuadrature.__init__(self, alpha, alpha, N, backend) + def __init__(self, alpha, N, backend=None, force_dim_axis=False): # noqa: N803 + super().__init__(self, alpha, alpha, N, + backend=backend, force_dim_axis=force_dim_axis) -def jacobi_gauss_lobatto_nodes(alpha, beta, N, backend=None): # noqa: N803 +def jacobi_gauss_lobatto_nodes(alpha, beta, N, # noqa: N803 + backend=None, force_dim_axis=False): """Compute the Gauss-Lobatto quadrature nodes corresponding to the :class:`~modepy.JacobiGaussQuadrature` with the same parameters. @@ -193,18 +211,22 @@ def jacobi_gauss_lobatto_nodes(alpha, beta, N, backend=None): # noqa: N803 x[0] = -1 x[-1] = 1 - if N == 1: - return x + if N > 1: + quad = JacobiGaussQuadrature(alpha + 1, beta + 1, N - 2, + backend=backend, force_dim_axis=True) + x[1:-1] = quad.nodes[0].real + + if force_dim_axis: + x = x.reshape(1, -1) - x[1:-1] = np.array( - JacobiGaussQuadrature(alpha + 1, beta + 1, N - 2, backend).nodes - ).real return x -def legendre_gauss_lobatto_nodes(N, backend=None): # noqa: N803 +def legendre_gauss_lobatto_nodes(N, # noqa: N803 + backend=None, force_dim_axis=False): """Compute the Legendre-Gauss-Lobatto quadrature nodes. Exact to degree :math:`2N - 1`. """ - return jacobi_gauss_lobatto_nodes(0, 0, N, backend) + return jacobi_gauss_lobatto_nodes(0, 0, N, + backend=backend, force_dim_axis=force_dim_axis) diff --git a/modepy/spaces.py b/modepy/spaces.py index dc9ef53e..ba0f5e32 100644 --- a/modepy/spaces.py +++ b/modepy/spaces.py @@ -76,14 +76,16 @@ def __repr__(self): class PN(FunctionSpace): - r"""The function space of polynomials with total degree :math:`N`=:attr:`order`. + r"""The function space of polynomials with total degree + :math:`N` = :attr:`order`. .. math:: P^N:=\operatorname{span}\left\{\prod_{i=1}^d x_i^{n_i}:\sum n_i\le N\right\}. - .. automethod:: __init__ .. attribute:: order + + .. automethod:: __init__ """ def __init__(self, spatial_dim, order): super().__init__() @@ -111,15 +113,16 @@ def __repr__(self): class QN(FunctionSpace): r"""The function space of polynomials with maximum degree - :math:`N`=:attr:`order`: + :math:`N` = :attr:`order`: .. math:: Q^N:=\operatorname{span} \left \{\prod_{i=1}^d x_i^{n_i}:\max n_i\le N\right\}. - .. automethod:: __init__ .. attribute:: order + + .. automethod:: __init__ """ def __init__(self, spatial_dim, order): super().__init__() From 10a17d52a06d9869ac64cc9f03a6d3b49da4c2dc Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Fri, 4 Dec 2020 15:07:01 -0600 Subject: [PATCH 65/68] clarify exact_to --- modepy/quadrature/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 43701b4f..acce0946 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -66,7 +66,9 @@ class Quadrature: .. attribute :: exact_to - Total polynomial degree up to which the quadrature is exact. + Summed polynomial degree up to which the quadrature is exact. + In higher-dimensions, the quadrature is supposed to be exact on (at least) + :math:`P^N`, where :math:`N` = :attr:`exact_to`. .. automethod:: __call__ """ From 231524fb4cce43596175498efca2b326c8dacb1a Mon Sep 17 00:00:00 2001 From: Alexandru Fikl Date: Thu, 10 Dec 2020 17:07:34 -0600 Subject: [PATCH 66/68] handle missing exact_to in tensor product quadrature --- modepy/quadrature/__init__.py | 9 ++++++++- modepy/quadrature/clenshaw_curtis.py | 2 +- modepy/quadrature/vioreanu_rokhlin.py | 2 +- modepy/quadrature/witherden_vincent.py | 2 +- modepy/quadrature/xiao_gimbutas.py | 3 ++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index acce0946..4287c5cd 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -123,7 +123,14 @@ def __init__(self, quads): assert w.size == x.shape[1] super().__init__(x, w) - self.exact_to = min(quad.exact_to for quad in quads) + + try: + exact_to = min(quad.exact_to for quad in quads) + except AttributeError: + # e.g. FejerQuadrature does not have any 'exact_to' + pass + else: + self.exact_to = exact_to class LegendreGaussTensorProductQuadrature(TensorProductQuadrature): diff --git a/modepy/quadrature/clenshaw_curtis.py b/modepy/quadrature/clenshaw_curtis.py index 63c18a17..92839ade 100644 --- a/modepy/quadrature/clenshaw_curtis.py +++ b/modepy/quadrature/clenshaw_curtis.py @@ -168,5 +168,5 @@ def __init__(self, N, kind=1, force_dim_axis=False): # noqa @property def exact_to(self): - raise ValueError("%s has no known exact_to information" + raise AttributeError("%s has no known exact_to information" % type(self).__name__) diff --git a/modepy/quadrature/vioreanu_rokhlin.py b/modepy/quadrature/vioreanu_rokhlin.py index 83f73244..edae4d9a 100644 --- a/modepy/quadrature/vioreanu_rokhlin.py +++ b/modepy/quadrature/vioreanu_rokhlin.py @@ -88,7 +88,7 @@ def __init__(self, order, dims): from modepy.quadrature.vr_quad_data_tet import tetrahedron_data as table ref_volume = 4/3 else: - raise ValueError("invalid dimensionality") + raise QuadratureRuleUnavailable(f"invalid domension: '{dims}'") from modepy.tools import EQUILATERAL_TO_UNIT_MAP e2u = EQUILATERAL_TO_UNIT_MAP[dims] diff --git a/modepy/quadrature/witherden_vincent.py b/modepy/quadrature/witherden_vincent.py index 9091b27b..40afe6f6 100644 --- a/modepy/quadrature/witherden_vincent.py +++ b/modepy/quadrature/witherden_vincent.py @@ -52,7 +52,7 @@ def __init__(self, order, dims): from modepy.quadrature.witherden_vincent_quad_data import \ hex_data as table else: - raise ValueError(f"unsupported domension: {dims}") + raise QuadratureRuleUnavailable(f"invalid domension: '{dims}'") try: rule = table[order] diff --git a/modepy/quadrature/xiao_gimbutas.py b/modepy/quadrature/xiao_gimbutas.py index c61d61bd..3cf83456 100644 --- a/modepy/quadrature/xiao_gimbutas.py +++ b/modepy/quadrature/xiao_gimbutas.py @@ -66,7 +66,8 @@ def __init__(self, order, dims): elif dims == 3: from modepy.quadrature.xg_quad_data import tetrahedron_table as table else: - raise QuadratureRuleUnavailable("invalid dimensionality") + raise QuadratureRuleUnavailable(f"invalid dimension: '{dims}'") + try: order_table = table[order] except KeyError: From 836ba97a1a384d86dafe5b552c22a3d4b38b05d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andreas=20Kl=C3=B6ckner?= Date: Thu, 10 Dec 2020 18:08:57 -0600 Subject: [PATCH 67/68] quadrature_for_space doc tweak --- modepy/quadrature/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modepy/quadrature/__init__.py b/modepy/quadrature/__init__.py index 4287c5cd..dddb0085 100644 --- a/modepy/quadrature/__init__.py +++ b/modepy/quadrature/__init__.py @@ -145,7 +145,7 @@ def __init__(self, N, dims, backend=None): # noqa: N803 def quadrature_for_space(space: FunctionSpace, shape: Shape) -> Quadrature: """ :returns: a :class:`~modepy.Quadrature` that exactly integrates the functions - in *space*. + in *space* over *shape*. """ raise NotImplementedError((type(space).__name__, type(shape).__name)) From 081077b5e4cb302e1518d7c8ce4e4bebcba13c1f Mon Sep 17 00:00:00 2001 From: Andreas Kloeckner Date: Thu, 10 Dec 2020 18:12:34 -0600 Subject: [PATCH 68/68] Better handle current limitations of _hypercube_face_to_vol_map --- modepy/shapes.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modepy/shapes.py b/modepy/shapes.py index a200bc0d..54ea53c8 100644 --- a/modepy/shapes.py +++ b/modepy/shapes.py @@ -376,10 +376,13 @@ def _hypercube_face_to_vol_map(face_vertices: np.ndarray, p: np.ndarray): origin = face_vertices[:, 0].reshape(-1, 1) - # works up to (and including) 3D: - # - no-op for 1D, 2D - # - For square faces, eliminate middle node - face_basis = face_vertices[:, 1:3] - origin + if dim <= 3: + # works up to (and including) 3D: + # - no-op for 1D, 2D + # - For square faces, eliminate node opposite origin + face_basis = face_vertices[:, 1:3] - origin + else: + raise NotImplementedError(f"_hypercube_face_to_vol_map in {dim} dimensions") return origin + np.einsum("ij,jk->ik", face_basis, (1 + p) / 2)