def test_deprecated_nodal_face_mass_matrix(dims, order=3): # FIXME DEPRECATED remove along with nodal_face_mass_matrix (>=2022) vol_shape = mp.Simplex(dims) vol_space = mp.space_for_shape(vol_shape, order) 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 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_for_space(face_space, face) face_vertices = vertices[:, face.volume_vertex_indices] fmm = nodal_face_mass_matrix( volume_basis.functions, volume_nodes, face_nodes, order, face_vertices) fmm2 = nodal_face_mass_matrix( volume_basis.functions, 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( mp.basis_for_space(face_space, face).functions, mp.edge_clustered_nodes_for_space(face_space, face)))
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_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) 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( 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) 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)
def test_resampling_matrix(dims, shape_cls, ncoarse=5, nfine=10): shape = shape_cls(dims) 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, shape) 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, shape) my_eye = np.dot( 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.functions, coarse_nodes, fine_nodes, least_squares_ok=True), 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
def test_deprecated_modal_face_mass_matrix(dims, order=3): # FIXME DEPRECATED remove along with modal_face_mass_matrix (>=2022) shape = mp.Simplex(dims) space = mp.space_for_shape(shape, order) vertices = mp.unit_vertices_for_shape(shape) basis = mp.basis_for_space(space, shape) from modepy.matrices import modal_face_mass_matrix for face in mp.faces_for_shape(shape): face_vertices = vertices[:, face.volume_vertex_indices] fmm = modal_face_mass_matrix( basis.functions, order, face_vertices) fmm2 = modal_face_mass_matrix( 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 {face.face_index}" fmm[np.abs(fmm) < 1e-13] = 0 nnz = np.sum(fmm > 0) logger.info("fmm: nnz %d\n%s", nnz, fmm)
def tesselate_simplex(dim): import modepy as mp shape = mp.Simplex(dim) space = mp.space_for_shape(shape, 2) node_tuples = mp.node_tuples_for_space(space) return node_tuples, mp.submesh_for_shape(shape, node_tuples)
def __init__(self, order, vertex_indices, nodes, element_nr_base=None, node_nr_base=None, unit_nodes=None, dim=None, **kwargs): """ :arg order: the maximum total degree used for interpolation. :arg nodes: ``(ambient_dim, nelements, nunit_nodes)`` The nodes are assumed to be mapped versions of *unit_nodes*. :arg unit_nodes: ``(dim, nunit_nodes)`` The unit nodes of which *nodes* is a mapped version. If unspecified, the nodes from :func:`modepy.edge_clustered_nodes_for_space` are assumed. These must be in unit coordinates as defined in :mod:`modepy`. :arg dim: only used if *unit_nodes* is *None*, to get the default unit nodes. Do not supply *element_nr_base* and *node_nr_base*, they will be automatically assigned. """ if unit_nodes is not None: _dim = unit_nodes.shape[0] if dim is not None and _dim != dim: raise ValueError("'dim' does not match 'unit_nodes' dimension") else: dim = _dim else: if dim is None: raise TypeError("'dim' must be passed if 'unit_nodes' is not passed") # dim is now usable shape = self._modepy_shape_cls(dim) space = mp.space_for_shape(shape, order) if unit_nodes is None: unit_nodes = mp.edge_clustered_nodes_for_space(space, shape) if nodes is not None: if unit_nodes.shape[-1] != nodes.shape[-1]: raise ValueError( "'nodes' has wrong number of unit nodes per element." f" expected {unit_nodes.shape[-1]}, " f" but got {nodes.shape[-1]}.") if vertex_indices is not None: if not issubclass(vertex_indices.dtype.type, np.integer): raise TypeError("'vertex_indices' must be integral") if vertex_indices.shape[-1] != shape.nvertices: raise ValueError( "'vertex_indices' has wrong number of vertices per element." f" expected {shape.nvertices}," f" got {vertex_indices.shape[-1]}") super().__init__(order, vertex_indices, nodes, element_nr_base=element_nr_base, node_nr_base=node_nr_base, unit_nodes=unit_nodes, dim=dim, _modepy_shape=shape, _modepy_space=space)
def test_modal_coefficients_by_projection(actx_factory, quad_group_factory): group_cls = SimplexElementGroup modal_group_factory = ModalSimplexGroupFactory actx = actx_factory() order = 10 m_order = 5 # Make a regular rectangle mesh mesh = mgen.generate_regular_rect_mesh(a=(0, 0), b=(5, 3), npoints_per_axis=(10, 6), order=order, group_cls=group_cls) # Make discretizations nodal_disc = Discretization(actx, mesh, quad_group_factory(order)) modal_disc = Discretization(actx, mesh, modal_group_factory(m_order)) # Make connections one using quadrature projection nodal_to_modal_conn_quad = NodalToModalDiscretizationConnection( nodal_disc, modal_disc, allow_approximate_quad=True) def f(x): return 2 * actx.np.sin(5 * x) x_nodal = thaw(nodal_disc.nodes()[0], actx) nodal_f = f(x_nodal) # Compute modal coefficients we expect to get import modepy as mp grp, = nodal_disc.groups shape = mp.Simplex(grp.dim) space = mp.space_for_shape(shape, order=m_order) basis = mp.orthonormal_basis_for_space(space, shape) quad = grp.quadrature_rule() nodal_f_data = actx.to_numpy(nodal_f[0]) vdm = mp.vandermonde(basis.functions, quad.nodes) w_diag = np.diag(quad.weights) modal_data = [] for _, nodal_data in enumerate(nodal_f_data): # Compute modal data in each element: V.T * W * nodal_data elem_modal_f = np.dot(vdm.T, np.dot(w_diag, nodal_data)) modal_data.append(elem_modal_f) modal_data = actx.from_numpy(np.asarray(modal_data)) modal_f_expected = DOFArray(actx, data=(modal_data, )) # Map nodal coefficients using the quadrature-based projection modal_f_computed = nodal_to_modal_conn_quad(nodal_f) err = flat_norm(modal_f_expected - modal_f_computed) assert err <= 1e-13
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, shape) basis = mp.orthonormal_basis_for_space(mp.space_for_shape(shape, order), shape) maxerr = 0 for i, f in enumerate(basis.functions): for j, g in enumerate(basis.functions): if i == j: true_result = 1 else: true_result = 0 result = cub(lambda x: f(x) * g(x)) err = abs(result - true_result) print((maxerr, err)) maxerr = max(maxerr, err) if err > ebound: print("bad", order, i, j, err) assert err < ebound
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, vol_shape) volume_basis = mp.basis_for_space(vol_space, vol_shape) from modepy.matrices import (nodal_mass_matrix_for_face, nodal_quad_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) 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) fmm2 = nodal_quad_mass_matrix_for_face( face, face_quad2, volume_basis.functions, volume_nodes) for f_face in face_basis.functions: fval_nodal = f_face(face_nodes) fval_quad = f_face(face_quad2.nodes) assert ( la.norm(fmm@fval_nodal - fmm2@fval_quad, np.inf) / la.norm(fval_quad)) < 3e-15 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.functions, face_nodes))
def test_hypercube_submesh(dims, order=3): shape = mp.Hypercube(dims) space = mp.space_for_shape(shape, order) 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) == (order + 1)**dims elements = mp.submesh_for_shape(shape, node_tuples) for e in elements: logger.info("element: %s", e) assert len(elements) == order**dims
def test_modal_coeffs_by_projection(dim): shape = mp.Simplex(dim) space = mp.space_for_shape(shape, order=5) basis = mp.orthonormal_basis_for_space(space, shape) quad = mp.XiaoGimbutasSimplexQuadrature(10, dim) assert quad.exact_to >= 2 * space.order modal_coeffs = np.random.randn(space.space_dim) vdm = mp.vandermonde(basis.functions, quad.nodes) evaluated = vdm @ modal_coeffs modal_coeffs_2 = vdm.T @ (evaluated * quad.weights) diff = modal_coeffs - modal_coeffs_2 assert la.norm(diff, 2) < 3e-13
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, shape) diff_mat = mp.differentiation_matrices(basis.functions, basis.gradients, nodes) if isinstance(diff_mat, tuple): diff_mat = diff_mat[0] f = np.sin(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) logger.info("error: %.5e", error) assert error < 2.0e-4, error
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 = mp.edge_clustered_nodes_for_space(space, shape) from modepy.tools import estimate_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, shape) import matplotlib.pyplot as plt fig = plt.figure() ax = fig.gca() ax.grid() if dims == 1: ax.plot(equi_nodes[0], lebesgue) ax.set_xlabel("$x$") ax.set_ylabel(fr"$\lambda_{order}$") elif dims == 2: ax.plot(nodes[0], nodes[1], "ko") p = ax.tricontourf(equi_nodes[0], equi_nodes[1], lebesgue, levels=16) fig.colorbar(p) ax.set_xlim([-1.1, 1.1]) ax.set_ylim([-1.1, 1.1]) ax.set_aspect("equal") else: raise ValueError(f"unsupported dimension: {dims}") shape_name = shape_cls.__name__.lower() fig.savefig(f"estimate_lebesgue_constant_{shape_name}_{dims}_order_{order}")
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(mp.space_for_shape(shape, order), shape) from pytools.convergence import EOCRecorder from pytools import wandering_element for i_bf, (bf, gradbf) in enumerate(zip( basis.functions, basis.gradients, )): eoc_rec = EOCRecorder() for h in [1e-2, 1e-3]: r = mp.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(shape.dim) for unit in (np.array(unit_tuple).reshape(-1, 1), ) ]) 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) eoc_rec.add_data_point(h, err) 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)
def _get_ref_midpoints(shape, ref_vertices): r"""The reference element is considered to be, e.g. for a 2-simplex:: F | \ | \ D----E | /| \ | / | \ A----B----C where the midpoints are ``(B, E, D)``. The same applies to other shapes and higher dimensions. :arg ref_vertices: a :class:`list` of node index :class:`tuple`\ s on :math:`[0, 2]^d`. """ from pytools import add_tuples space = mp.space_for_shape(shape, 1) orig_vertices = [ add_tuples(vt, vt) for vt in mp.node_tuples_for_space(space) ] return [rv for rv in ref_vertices if rv not in orig_vertices]
def make_face_restriction(actx, discr, group_factory, boundary_tag, per_face_groups=False): """Create a mesh, a discretization and a connection to restrict a function on *discr* to its values on the edges of element faces denoted by *boundary_tag*. :arg boundary_tag: The boundary tag for which to create a face restriction. May be :class:`FACE_RESTR_INTERIOR` to indicate interior faces, or :class:`FACE_RESTR_ALL` to make a discretization consisting of all (interior and boundary) faces. :arg per_face_groups: If *True*, the resulting discretization is guaranteed to have groups organized as:: (grp0, face0), (grp0, face1), ... (grp0, faceN), (grp1, face0), (grp1, face1), ... (grp1, faceN), ... each with the elements in the same order as the originating group. If *False*, volume and boundary groups correspond with each other one-to-one, and an interpolation batch is created per face. :return: a :class:`meshmode.discretization.connection.DirectDiscretizationConnection` representing the new connection. The new boundary discretization can be obtained from the :attr:`meshmode.discretization.connection.DirectDiscretizationConnection.to_discr` attribute of the return value, and the corresponding new boundary mesh from that. """ if boundary_tag is None: boundary_tag = FACE_RESTR_INTERIOR from warnings import warn warn("passing *None* for boundary_tag is deprecated--pass " "FACE_RESTR_INTERIOR instead", DeprecationWarning, stacklevel=2) logger.info("building face restriction: start") # {{{ gather boundary vertices bdry_vertex_vol_nrs = _get_face_vertices(discr.mesh, boundary_tag) vol_to_bdry_vertices = np.empty( discr.mesh.vertices.shape[-1], discr.mesh.vertices.dtype) vol_to_bdry_vertices.fill(-1) vol_to_bdry_vertices[bdry_vertex_vol_nrs] = np.arange( len(bdry_vertex_vol_nrs), dtype=np.intp) bdry_vertices = discr.mesh.vertices[:, bdry_vertex_vol_nrs] # }}} from meshmode.mesh import Mesh, _ModepyElementGroup bdry_mesh_groups = [] connection_data = {} if boundary_tag not in [FACE_RESTR_ALL, FACE_RESTR_INTERIOR]: btag_bit = discr.mesh.boundary_tag_bit(boundary_tag) for igrp, (grp, fagrp_map) in enumerate( zip(discr.groups, discr.mesh.facial_adjacency_groups)): mgrp = grp.mesh_el_group if not isinstance(mgrp, _ModepyElementGroup): raise NotImplementedError("can only take boundary of " "meshes based on SimplexElementGroup and " "TensorProductElementGroup") # {{{ pull together per-group face lists group_boundary_faces = [] if boundary_tag is FACE_RESTR_INTERIOR: for fagrp in fagrp_map.values(): if fagrp.ineighbor_group is None: # boundary faces -> not looking for those continue group_boundary_faces.extend( zip(fagrp.elements, fagrp.element_faces)) elif boundary_tag is FACE_RESTR_ALL: group_boundary_faces.extend( (iel, iface) for iface in range(grp.mesh_el_group.nfaces) for iel in range(grp.nelements) ) else: bdry_grp = fagrp_map.get(None) if bdry_grp is not None: nb_el_bits = -bdry_grp.neighbors face_relevant_flags = (nb_el_bits & btag_bit) != 0 group_boundary_faces.extend( zip( bdry_grp.elements[face_relevant_flags], bdry_grp.element_faces[face_relevant_flags])) # }}} batch_base = 0 # group by face_index for face in mgrp._modepy_faces: batch_boundary_el_numbers_in_grp = np.array([ ibface_el for ibface_el, ibface_face in group_boundary_faces if ibface_face == face.face_index ], dtype=np.intp) # {{{ preallocate arrays for mesh group nbatch_elements = len(batch_boundary_el_numbers_in_grp) if per_face_groups or face.face_index == 0: if per_face_groups: ngroup_bdry_elements = nbatch_elements else: ngroup_bdry_elements = len(group_boundary_faces) # make up some not-terrible nodes for the boundary Mesh space = mp.space_for_shape(face, mgrp.order) bdry_unit_nodes = mp.edge_clustered_nodes_for_space(space, face) vol_basis = mp.basis_for_space( mgrp._modepy_space, mgrp._modepy_shape).functions vertex_indices = np.empty( (ngroup_bdry_elements, face.nvertices), mgrp.vertex_indices.dtype) nbdry_unit_nodes = bdry_unit_nodes.shape[-1] nodes = np.empty( (discr.ambient_dim, ngroup_bdry_elements, nbdry_unit_nodes), dtype=np.float64) # }}} new_el_numbers = batch_base + np.arange(nbatch_elements) if not per_face_groups: batch_base += nbatch_elements # {{{ no per-element axes in these computations face_unit_nodes = face.map_to_volume(bdry_unit_nodes) resampling_mat = mp.resampling_matrix( vol_basis, face_unit_nodes, mgrp.unit_nodes) # }}} # {{{ build information for mesh element group # Find vertex_indices glob_face_vertices = mgrp.vertex_indices[ batch_boundary_el_numbers_in_grp][:, face.volume_vertex_indices] vertex_indices[new_el_numbers] = \ vol_to_bdry_vertices[glob_face_vertices] # Find nodes nodes[:, new_el_numbers, :] = np.einsum( "ij,dej->dei", resampling_mat, mgrp.nodes[:, batch_boundary_el_numbers_in_grp, :]) # }}} connection_data[igrp, face.face_index] = _ConnectionBatchData( group_source_element_indices=batch_boundary_el_numbers_in_grp, group_target_element_indices=new_el_numbers, face=face, ) is_last_face = face.face_index + 1 == mgrp.nfaces if per_face_groups or is_last_face: bdry_mesh_group = type(mgrp)( mgrp.order, vertex_indices, nodes, unit_nodes=bdry_unit_nodes) bdry_mesh_groups.append(bdry_mesh_group) bdry_mesh = Mesh(bdry_vertices, bdry_mesh_groups) from meshmode.discretization import Discretization bdry_discr = Discretization( actx, bdry_mesh, group_factory) connection = _build_boundary_connection( actx, discr, bdry_discr, connection_data, per_face_groups) logger.info("building face restriction: done") return connection
def test_symbolic_basis(shape, order, basis_getter): 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 print(75 * "#") print("VALUES") print(75 * "#") rng = np.random.Generator(np.random.PCG64(17)) r = mp.random_nodes_for_shape(shape, 10000, rng=rng) for func, sym_func in zip(basis.functions, sym_basis): strmap = MyStringifyMapper() s = strmap(sym_func) for name, val in strmap.cse_name_list: print(f"{name} <- {val}") print(s) sym_val = MyEvaluationMapper({"r": r, "abs": abs})(sym_func) ref_val = func(r) ref_norm = la.norm(ref_val, np.inf) err = la.norm(sym_val - ref_val, np.inf) if ref_norm: err = err / ref_norm print(f"ERROR: {err}") print() assert np.allclose(sym_val, ref_val) # }}} # {{{ test gradients print(75 * "#") print("GRADIENTS") print(75 * "#") 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() s = strmap(sym_grad) for name, val in strmap.cse_name_list: print(f"{name} <- {val}") print(s) sym_val = MyEvaluationMapper({"r": r, "abs": abs})(sym_grad) ref_val = grad(r) if not isinstance(ref_val, tuple): assert not isinstance(sym_val, tuple) sym_val = (sym_val, ) ref_val = (ref_val, ) for sv_i, rv_i in zip(sym_val, ref_val): ref_norm = la.norm(rv_i, np.inf) err = la.norm(sv_i - rv_i, np.inf) if ref_norm: err = err / ref_norm print(f"ERROR: {err}") print() assert np.allclose(sv_i, rv_i)
def make_group_from_vertices(vertices, vertex_indices, order, group_cls=None, unit_nodes=None): # shape: (ambient_dim, nelements, nvertices) ambient_dim = vertices.shape[0] el_vertices = vertices[:, vertex_indices] from meshmode.mesh import SimplexElementGroup, TensorProductElementGroup if group_cls is None: group_cls = SimplexElementGroup if issubclass(group_cls, SimplexElementGroup): if order < 1: raise ValueError("can't represent simplices with mesh order < 1") el_origins = el_vertices[:, :, 0][:, :, np.newaxis] # ambient_dim, nelements, nspan_vectors spanning_vectors = (el_vertices[:, :, 1:] - el_origins) nspan_vectors = spanning_vectors.shape[-1] dim = nspan_vectors # dim, nunit_nodes if unit_nodes is None: shape = mp.Simplex(dim) space = mp.space_for_shape(shape, order) unit_nodes = mp.edge_clustered_nodes_for_space(space, shape) unit_nodes_01 = 0.5 + 0.5 * unit_nodes nodes = np.einsum("si,des->dei", unit_nodes_01, spanning_vectors) + el_origins elif issubclass(group_cls, TensorProductElementGroup): nelements, nvertices = vertex_indices.shape dim = nvertices.bit_length() - 1 if nvertices != 2**dim: raise ValueError("invalid number of vertices for tensor-product " "elements, must be power of two") shape = mp.Hypercube(dim) space = mp.space_for_shape(shape, order) if unit_nodes is None: unit_nodes = mp.edge_clustered_nodes_for_space(space, shape) # shape: (dim, nnodes) unit_nodes_01 = 0.5 + 0.5 * unit_nodes _, nnodes = unit_nodes.shape vertex_tuples = mp.node_tuples_for_space(type(space)(dim, 1)) assert len(vertex_tuples) == nvertices vdm = np.empty((nvertices, nvertices)) for i, vertex_tuple in enumerate(vertex_tuples): for j, func_tuple in enumerate(vertex_tuples): vertex_ref = np.array(vertex_tuple, dtype=np.float64) vdm[i, j] = np.prod(vertex_ref**func_tuple) # shape: (ambient_dim, nelements, nvertices) coeffs = np.empty((ambient_dim, nelements, nvertices)) for d in range(ambient_dim): coeffs[d] = la.solve(vdm, el_vertices[d].T).T vdm_nodes = np.zeros((nnodes, nvertices)) for j, func_tuple in enumerate(vertex_tuples): vdm_nodes[:, j] = np.prod(unit_nodes_01**np.array(func_tuple).reshape( -1, 1), axis=0) nodes = np.einsum("ij,dej->dei", vdm_nodes, coeffs) else: raise ValueError(f"unsupported value for 'group_cls': {group_cls}") # make contiguous nodes = nodes.copy() return group_cls(order, vertex_indices, nodes, unit_nodes=unit_nodes)
def get_mesh(self): el_type_hist = {} for el_type in self.element_types: el_type_hist[el_type] = el_type_hist.get(el_type, 0) + 1 if not el_type_hist: raise RuntimeError("empty mesh in gmsh input") groups = self.groups = [] ambient_dim = self.points.shape[-1] mesh_bulk_dim = max(el_type.dimensions for el_type in el_type_hist) # {{{ build vertex numbering # map set of face vertex indices to list of tags associated to face face_vertex_indices_to_tags = {} vertex_gmsh_index_to_mine = {} for element, el_vertices in enumerate(self.element_vertices): for gmsh_vertex_nr in el_vertices: if gmsh_vertex_nr not in vertex_gmsh_index_to_mine: vertex_gmsh_index_to_mine[gmsh_vertex_nr] = \ len(vertex_gmsh_index_to_mine) if self.tags: el_tag_indexes = [self.gmsh_tag_index_to_mine[t] for t in self.element_markers[element]] # record tags of boundary dimension el_tags = [self.tags[i][0] for i in el_tag_indexes if self.tags[i][1] == mesh_bulk_dim - 1] el_grp_verts = {vertex_gmsh_index_to_mine[e] for e in el_vertices} face_vertex_indices = frozenset(el_grp_verts) if face_vertex_indices not in face_vertex_indices_to_tags: face_vertex_indices_to_tags[face_vertex_indices] = [] face_vertex_indices_to_tags[face_vertex_indices] += el_tags # }}} # {{{ build vertex array gmsh_vertex_indices, my_vertex_indices = \ list(zip(*vertex_gmsh_index_to_mine.items())) vertices = np.empty( (ambient_dim, len(vertex_gmsh_index_to_mine)), dtype=np.float64) vertices[:, np.array(my_vertex_indices, np.intp)] = \ self.points[np.array(gmsh_vertex_indices, np.intp)].T # }}} from meshmode.mesh import (Mesh, SimplexElementGroup, TensorProductElementGroup) bulk_el_types = set() for group_el_type, ngroup_elements in el_type_hist.items(): if group_el_type.dimensions != mesh_bulk_dim: continue bulk_el_types.add(group_el_type) nodes = np.empty( (ambient_dim, ngroup_elements, group_el_type.node_count()), np.float64) el_vertex_count = group_el_type.vertex_count() vertex_indices = np.empty( (ngroup_elements, el_vertex_count), np.int32) i = 0 for el_vertices, el_nodes, el_type in zip( self.element_vertices, self.element_nodes, self.element_types): if el_type is not group_el_type: continue nodes[:, i] = self.points[el_nodes].T vertex_indices[i] = [ vertex_gmsh_index_to_mine[v_nr] for v_nr in el_vertices ] i += 1 import modepy as mp if isinstance(group_el_type, GmshSimplexElementBase): shape = mp.Simplex(group_el_type.dimensions) elif isinstance(group_el_type, GmshTensorProductElementBase): shape = mp.Hypercube(group_el_type.dimensions) else: raise NotImplementedError( f"gmsh element type: {type(group_el_type).__name__}") space = mp.space_for_shape(shape, group_el_type.order) unit_nodes = mp.equispaced_nodes_for_space(space, shape) if isinstance(group_el_type, GmshSimplexElementBase): group = SimplexElementGroup( group_el_type.order, vertex_indices, nodes, unit_nodes=unit_nodes ) if group.dim == 2: from meshmode.mesh.processing import flip_simplex_element_group group = flip_simplex_element_group(vertices, group, np.ones(ngroup_elements, bool)) elif isinstance(group_el_type, GmshTensorProductElementBase): vertex_shuffle = type(group_el_type)( order=1).get_lexicographic_gmsh_node_indices() group = TensorProductElementGroup( group_el_type.order, vertex_indices[:, vertex_shuffle], nodes, unit_nodes=unit_nodes ) else: # NOTE: already checked above raise AssertionError() groups.append(group) # FIXME: This is heuristic. if len(bulk_el_types) == 1: is_conforming = True else: is_conforming = mesh_bulk_dim < 3 # construct boundary tags for mesh from meshmode.mesh import BTAG_ALL, BTAG_REALLY_ALL boundary_tags = [BTAG_ALL, BTAG_REALLY_ALL] if self.tags: boundary_tags += [tag for tag, dim in self.tags if dim == mesh_bulk_dim-1] # compute facial adjacency for Mesh if there is tag information facial_adjacency_groups = None if is_conforming and self.tags: from meshmode.mesh import _compute_facial_adjacency_from_vertices facial_adjacency_groups = _compute_facial_adjacency_from_vertices( groups, boundary_tags, np.int32, np.int8, face_vertex_indices_to_tags) return Mesh( vertices, groups, is_conforming=is_conforming, facial_adjacency_groups=facial_adjacency_groups, boundary_tags=boundary_tags, **self.mesh_construction_kwargs)
def test_sanity_single_element(actx_factory, dim, mesh_order, group_cls, visualize=False): pytest.importorskip("pytential") actx = actx_factory() if group_cls is SimplexElementGroup: group_factory = PolynomialWarpAndBlendGroupFactory(mesh_order + 3) elif group_cls is TensorProductElementGroup: group_factory = LegendreGaussLobattoTensorProductGroupFactory( mesh_order + 3) else: raise TypeError import modepy as mp shape = group_cls._modepy_shape_cls(dim) space = mp.space_for_shape(shape, mesh_order) vertices = mp.unit_vertices_for_shape(shape) nodes = mp.edge_clustered_nodes_for_space(space, shape).reshape(dim, 1, -1) vertex_indices = np.arange(shape.nvertices, dtype=np.int32).reshape(1, -1) center = np.empty(dim, np.float64) center.fill(-0.5) mg = group_cls(mesh_order, vertex_indices, nodes, dim=dim) mesh = Mesh(vertices, [mg], is_conforming=True) from meshmode.discretization import Discretization vol_discr = Discretization(actx, mesh, group_factory) # {{{ volume calculation check if isinstance(mg, SimplexElementGroup): from pytools import factorial true_vol = 1 / factorial(dim) * 2**dim elif isinstance(mg, TensorProductElementGroup): true_vol = 2**dim else: raise TypeError nodes = thaw(vol_discr.nodes(), actx) vol_one = 1 + 0 * nodes[0] from pytential import norm, integral # noqa comp_vol = integral(vol_discr, vol_one) rel_vol_err = abs(true_vol - comp_vol) / true_vol assert rel_vol_err < 1e-12 # }}} # {{{ boundary discretization from meshmode.discretization.connection import make_face_restriction bdry_connection = make_face_restriction(actx, vol_discr, group_factory, BTAG_ALL) bdry_discr = bdry_connection.to_discr # }}} from pytential import bind, sym bdry_normals = bind(bdry_discr, sym.normal(dim).as_vector())(actx) if visualize: from meshmode.discretization.visualization import make_visualizer bdry_vis = make_visualizer(actx, bdry_discr, 4) bdry_vis.write_vtk_file("sanity_single_element_boundary.vtu", [("normals", bdry_normals)]) normal_outward_check = bind( bdry_discr, sym.normal(dim) | (sym.nodes(dim) + 0.5 * sym.ones_vec(dim)), )(actx).as_scalar() normal_outward_check = flatten_to_numpy(actx, normal_outward_check > 0) assert normal_outward_check.all(), normal_outward_check
def matrix(self, afgrp, volgrp, dtype): nfaces = volgrp.mesh_el_group.nfaces assert afgrp.nelements == nfaces * volgrp.nelements matrix = np.empty((volgrp.nunit_dofs, nfaces, afgrp.nunit_dofs), dtype=dtype) import modepy as mp from meshmode.discretization import ElementGroupWithBasis from meshmode.discretization.poly_element import \ QuadratureSimplexElementGroup n = volgrp.order m = afgrp.order vol_basis = volgrp.basis_obj() faces = mp.faces_for_shape(volgrp.shape) for iface, face in enumerate(faces): # If the face group is defined on a higher-order # quadrature grid, use the underlying quadrature rule if isinstance(afgrp, QuadratureSimplexElementGroup): face_quadrature = afgrp.quadrature_rule() if face_quadrature.exact_to < m: raise ValueError( "The face quadrature rule is only exact for polynomials " f"of total degree {face_quadrature.exact_to}. Please " "ensure a quadrature rule is used that is at least " f"exact for degree {m}.") else: # NOTE: This handles the general case where # volume and surface quadrature rules may have different # integration orders face_quadrature = mp.quadrature_for_space( mp.space_for_shape(face, 2 * max(n, m)), face) # If the group has a nodal basis and is unisolvent, # we use the basis on the face to compute the face mass matrix if (isinstance(afgrp, ElementGroupWithBasis) and afgrp.space.space_dim == afgrp.nunit_dofs): face_basis = afgrp.basis_obj() # Sanity check for face quadrature accuracy. Not integrating # degree N + M polynomials here is asking for a bad time. if face_quadrature.exact_to < m + n: raise ValueError( "The face quadrature rule is only exact for polynomials " f"of total degree {face_quadrature.exact_to}. Please " "ensure a quadrature rule is used that is at least " f"exact for degree {n+m}.") matrix[:, iface, :] = mp.nodal_mass_matrix_for_face( face, face_quadrature, face_basis.functions, vol_basis.functions, volgrp.unit_nodes, afgrp.unit_nodes, ) else: # Otherwise, we use a routine that is purely quadrature-based # (no need for explicit face basis functions) matrix[:, iface, :] = mp.nodal_quad_mass_matrix_for_face( face, face_quadrature, vol_basis.functions, volgrp.unit_nodes, ) return matrix