diff --git a/firedrake/mesh.py b/firedrake/mesh.py index 64c929c210..04e0d24528 100644 --- a/firedrake/mesh.py +++ b/firedrake/mesh.py @@ -4869,10 +4869,11 @@ def Submesh(mesh, subdim=None, subdomain_id=None, label_name=None, name=None, ig subdim : int | None Topological dimension of the submesh. Defaults to ``mesh.topological_dimension``. - subdomain_id : int | None + subdomain_id : int | Sequence | None Subdomain ID representing the submesh. - If `None` the submesh will cover the entire domain. - This is useful to obtain a codim-1 submesh over all facets or + If multiple subdomain IDs are provided, their union is taken. + If `None` the submesh will cover the entire domain, + this is useful to obtain a codim-1 submesh over all facets or a submesh over a different communicator. label_name : str | None Name of the label to search ``subdomain_id`` in. @@ -4915,6 +4916,47 @@ def Submesh(mesh, subdim=None, subdomain_id=None, label_name=None, name=None, ig ridges to be contained in the quad mesh are shared by at most two facets to make the quad mesh orientation algorithm work. + Examples + -------- + >>> mesh = UnitSquareMesh(4, 4) + >>> x, y = SpatialCoordinate(mesh) + >>> DG = FunctionSpace(mesh, "DG", 0) + >>> DGT = FunctionSpace(mesh, "DGT", 0) + + Mark a cell subdomain and construct a codim-0 submesh from all cells in the subdomain + + >>> cell_marker = assemble(interpolate(conditional(lt(x, 0.5), 1, 0), DG)) + >>> mesh.mark_entities(cell_marker, 111) + >>> submesh = Submesh(mesh, subdomain_id=111) + + Mark a facet subdomain and construct a codim-1 submesh from all facets in the subdomain + + >>> facet_marker = assemble(interpolate(conditional(lt(abs(x-0.5), 1E-12), 1, 0), DGT)) + >>> mesh.mark_entities(facet_marker, 222) + >>> submesh = Submesh(mesh, subdim=mesh.topological_dimension-1, subdomain_id=222) + + Construct a codim-0 submesh of the union of multiple subdomains by passing a list + + >>> mesh.mark_entities(assemble(interpolate(conditional(lt(x, 0.5), 1, 0), DG)), 1) + >>> mesh.mark_entities(assemble(interpolate(conditional(lt(y, 0.5), 1, 0), DG)), 2) + >>> submesh = Submesh(mesh, subdomain_id=[1, 2]) + + Construct a codim-1 submesh of all the facets (the skeleton mesh) + + >>> submesh = Submesh(mesh, subdim=1) + + Construct a codim-1 submesh of the entire boundary + + >>> submesh = Submesh(mesh, subdomain_id="on_boundary") + + Construct a codim-1 submesh of the union of multiple boundaries + + >>> submesh = Submesh(mesh, subdim=mesh.topological_dimension-1, subdomain_id=[1, 2, 3]) + + Construct a codim-0 submesh of the part of the mesh owned by each MPI rank + + >>> submesh = Submesh(mesh, ignore_halo=True, comm=COMM_SELF) + """ if not isinstance(mesh, MeshGeometry): raise TypeError("Parent mesh must be a `MeshGeometry`") @@ -4922,6 +4964,18 @@ def Submesh(mesh, subdim=None, subdomain_id=None, label_name=None, name=None, ig raise NotImplementedError("Can not create a submesh of an ``ExtrudedMesh``") elif isinstance(mesh.topology, VertexOnlyMeshTopology): raise NotImplementedError("Can not create a submesh of a ``VertexOnlyMesh``") + + if subdomain_id == "on_boundary": + if subdim is None: + subdim = mesh.topological_dimension - 1 + elif subdim != mesh.topological_dimension - 1: + raise ValueError('subdomain_id="on_boundary" requires subdim=dim-1') + if label_name is None: + label_name = "exterior_facets" + elif label_name != "exterior_facets": + raise ValueError('subdomain_id="on_boundary" requires label_name="exterior_facets"') + subdomain_id = 1 + if subdim is None: subdim = mesh.topological_dimension plex = mesh.topology_dm @@ -4939,15 +4993,47 @@ def Submesh(mesh, subdim=None, subdomain_id=None, label_name=None, name=None, ig label_name = dmcommon.CELL_SETS_LABEL elif subdim == dim - 1: label_name = dmcommon.FACE_SETS_LABEL + + # Parse non-integer subdomain_id + if isinstance(subdomain_id, str): + raise ValueError(f"Submesh construction got invalid subdomain_id {subdomain_id}.") + elif isinstance(subdomain_id, Sequence): + label = plex.getLabel(label_name) + if subdim != dim: + plex.labelComplete(label) + + def get_points(sub): + if sub == "on_boundary": + return plex.getStratumIS("exterior_facets", 1) + elif isinstance(sub, str): + raise ValueError(f"Submesh construction got invalid subdomain_id {sub}.") + else: + return label.getStratumIS(sub) + + # Take the union of the labels in the list + iset = PETSc.IS().createGeneral([], comm=mesh.comm) + for sub in subdomain_id: + cur = get_points(sub) + iset = iset.union(cur) + # Create a temporary label + label_name = "temp_label" + subdomain_id = 1 + plex.createLabel(label_name) + label = plex.getLabel(label_name) + label.setStratumIS(subdomain_id, iset) + subplex = dmcommon.submesh_create(plex, subdim, label_name, subdomain_id, ignore_halo, comm=comm) + if label_name == "temp_label": + plex.removeLabel(label_name) + comm = comm or mesh.comm name = name or _generate_default_submesh_name(mesh.name) subplex.setName(_generate_default_mesh_topology_name(name)) if subplex.getDimension() != subdim: raise RuntimeError(f"Found subplex dim ({subplex.getDimension()}) != expected ({subdim})") if reorder is None: - # Ideally we should set perm_is = mesh.dm_reordering[label_indices] + # Ideally we should set perm_is = mesh._dm_renumbering[label_indices] reorder = mesh._did_reordering submesh = Mesh( diff --git a/tests/firedrake/submesh/test_submesh_interface.py b/tests/firedrake/submesh/test_submesh_interface.py new file mode 100644 index 0000000000..394738fab8 --- /dev/null +++ b/tests/firedrake/submesh/test_submesh_interface.py @@ -0,0 +1,45 @@ +import pytest +import numpy as np +from firedrake import * + + +def test_submesh_subdomain_id_union(): + mesh = UnitSquareMesh(4, 4) + x, y = SpatialCoordinate(mesh) + M = FunctionSpace(mesh, "DG", 0) + m1 = Function(M).interpolate(conditional(lt(x, 0.5), 1, 0)) + m2 = Function(M).interpolate(conditional(lt(y, 0.5), 1, 0)) + mesh.mark_entities(m1, 111) + mesh.mark_entities(m2, 222) + + subdomain_id = [111, 222] + submesh1 = Submesh(mesh, mesh.topological_dimension, subdomain_id=subdomain_id) + + m3 = Function(M).interpolate(m1 + m2 - m1 * m2) + expected = assemble(m3*dx) + assert abs(assemble(1*dx(domain=submesh1)) - expected) < 1E-12 + + mesh.mark_entities(m3, 333) + submesh2 = Submesh(mesh, mesh.topological_dimension, 333) + assert submesh2.cell_set.size == submesh1.cell_set.size + assert np.allclose(submesh2.coordinates.dat.data, submesh1.coordinates.dat.data) + + +@pytest.mark.parametrize("subdomain_id", ["on_boundary", (1, 3, 6)]) +def test_submesh_facet_subdomain_id_union(subdomain_id): + mesh = UnitCubeMesh(2, 2, 2) + submesh1 = Submesh(mesh, mesh.topological_dimension - 1, subdomain_id=subdomain_id) + if subdomain_id == "on_boundary": + area = assemble(1*ds(domain=mesh)) + else: + area = assemble(1*ds(subdomain_id, domain=mesh)) + assert abs(assemble(1*dx(domain=submesh1)) - area) < 1E-12 + + DGT = FunctionSpace(mesh, "DGT", 0) + facet_function = Function(DGT) + DirichletBC(DGT, 1, subdomain_id).apply(facet_function) + facet_value = 999 + rmesh = RelabeledMesh(mesh, [facet_function], [facet_value]) + submesh2 = Submesh(rmesh, mesh.topological_dimension - 1, facet_value) + assert submesh2.cell_set.size == submesh1.cell_set.size + assert np.allclose(submesh2.coordinates.dat.data, submesh1.coordinates.dat.data)