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_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 __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_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_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 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 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 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