Fix Submesh to propagate "Edge Sets" / "Vertex Sets" to codimension-1 submesh exterior facet markers#4950
Conversation
connorjward
left a comment
There was a problem hiding this comment.
I think that this is conceptually correct but the code as written is very very complicated. It would really benefit from a refactor to make it more readable.
| # Parallel | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| @pytest.mark.parallel(nprocs=2) |
There was a problem hiding this comment.
I would like to see more parallel tests, especially some that involve calculation, as that is where this functionality is really stressed.
There was a problem hiding this comment.
I added parallel computation tests at 2 and 3 ranks: boundary length integration via ds(tag), coordinate expression integrals over tagged boundaries, CG2 interpolation + boundary integration, and 2D→1D vertex set propagation with ds(tag).
Let me know if there are more specific tests you would like to see
Thanks for the feedback! I tried to make it more readable, but please let me know if there are any other changes you would like me to do. |
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
Co-authored-by: Pablo Brubeck <brubeck@protonmail.com>
…rake into fix-submesh-labels
connorjward
left a comment
There was a problem hiding this comment.
In general I think this is really good.
firedrake/cython/dmcommon.pyx
Outdated
| the parent's "Vertex Sets" are used. Exterior facets that already carry | ||
| an inherited "Face Sets" value (from ``DMPlexFilter``) are left alone; |
There was a problem hiding this comment.
I don't understand this. Can you explain? I would expect that the given "Face Sets" is now just wrong and it seems a bit dodgy to be partially editing the "Face Sets" label.
There was a problem hiding this comment.
DMPlexFilter copies every label from the parent into the subdm, including "Face Sets" values that DMPlexLabelComplete propagated to closure entities (edges, vertices). For a codim-1 submesh these inherited edge-level values carry useful boundary information.
I tried discarding the inherited "Face Sets" and rebuilding from scratch, but this breaks test_submesh_solve_2d_1d_poisson_hermite (which chains 3D→2D→1D submesh extractions): removing and recreating the label loses PETSc's internal parallel migration state, so the rebuilt label doesn't survive redistribution correctly.
I therefore preserve inherited values and only fill in exterior facets that don't already have one, using the parent's lower-dimensional label or a fresh default. The docstring now explains this.
There was a problem hiding this comment.
This seems quite counter intuitive and might cause some confusion down the line. I wonder if there's a simpler solution:
When we propagate "Face Sets" to the rest of the facet closure could we at the same time set "Edge Sets" and "Vertex Sets" for the extra entities.
Consider one of our 3D utility meshes with some marked boundary. "Face Sets" for this boundary would contain all facets, edges and vertices on the boundary, "Edge Sets" would contain all edges and vertices, and "Vertex Sets" just the vertices.
This way when we lose a dimension via submesh all the necessary boundary information is already encoded in "Edge Sets" and so "Face Sets" is safe to discard (or push to "Cell Sets"?).
There was a problem hiding this comment.
I've implemented part 1 of your suggestion: complete_facet_labels now populates "Edge Sets" and "Vertex Sets" from the "Face Sets" closure (entities that already carry a value, e.g. from Gmsh, are not overwritten). So after mesh setup a 3D utility mesh with a marked boundary will have:
- "Face Sets": all facets, edges and vertices on the boundary
- "Edge Sets": all edges and vertices
- "Vertex Sets": just the vertices
For part 2 I also switched _propagate_parent_facet_labels to read from the subdm's own inherited "Edge Sets" / "Vertex Sets" instead of doing cross-DM queries against the parent via subpoint_indices. This simplifies the function signature and removes the parent-DM dependency entirely.
However, I was not able to fully discard inherited "Face Sets" on the subdm (the removeLabel / createLabel approach). Even when reading exclusively from subdm-local labels, removing and recreating "Face Sets" still breaks test_submesh_solve_2d_1d_poisson_hermite (which chains 3D→2D→1D). The issue is that removeLabel / createLabel loses PETSc's internal parallel label-migration state regardless of where the replacement values come from.
So the current implementation preserves inherited "Face Sets" and only fills in exterior facets that don't already have a value. I think fully discarding inherited "Face Sets" would require PETSc-level changes (e.g. a way to reset label values without destroying the migration SF), which is out of scope here.
There was a problem hiding this comment.
The issue that I am having is that there is some quite complicated implicit union behaviour going on between the closure-propagated "Face Sets" and any new "Edge Sets" entries. For instance consider having a cube:
+-------+
/ /|
/ / |
1 / / |
/ / |
/ / |
+-------+ 1 +
| | /
| | /
1 | | /
| | /
| |/
+-------+
where the left-most edges are labelled 1 in "Edge Sets" and the right-most face is labelled 1 in "Face Sets". If you take some sort of codim-1 submesh it is not obvious which edges (now faces) should be labelled 1.
I think my preferred approach is:
- Only "Edge Sets" are used to build "Face Sets" for the submesh
- Build "Edge Sets" and "Vertex Sets" only for our utility meshes, instead of always propagating from "Face Sets" - this is optional for this PR since my guess is you are mostly using gmsh.
Sorry for making this such a big deal, but I think we have to be careful about this.
However, I was not able to fully discard inherited "Face Sets" on the subdm (the removeLabel / createLabel approach).
What about something like DMLabelClearStratum?
Even when reading exclusively from subdm-local labels, removing and recreating "Face Sets" still breaks test_submesh_solve_2d_1d_poisson_hermite
This is weird. I wonder what is happening there.
There was a problem hiding this comment.
Thanks for the suggestion — we've now implemented the approach you described:
1. Populate "Edge Sets" and "Vertex Sets" from "Face Sets" on the parent
Before DMPlexFilter, a new helper _populate_lower_dim_labels iterates each positive "Face Sets" stratum on the parent mesh and copies the value to "Edge Sets" (depth-1 entities) and "Vertex Sets" (depth-0 entities). Entities that already carry a positive value (e.g. user-defined labels from Gmsh) are preserved.
2. Clear inherited "Face Sets" on the subdm and rebuild from "Edge Sets"
_propagate_parent_facet_labels now uses DMLabelClearStratum to clear every stratum of the inherited "Face Sets" on the subdm, then rebuilds it purely from the subdm's own "Edge Sets" (3D→2D) or "Vertex Sets" (2D→1D). DMPlexLabelComplete is called afterwards to propagate to closure entities and synchronise ghost points via the point SF. When no source label exists, a fallback path preserves inherited values.
3. Handling mesh-generator artifacts (value 0)
The main difficulty we hit was that mesh files (and RelabeledMesh) can leave "Face Sets" value 0 on many boundary entities. Since DMLabelGetValue returns the minimum value for a point, edges at face intersections appeared to have value 0 rather than their meaningful positive value. The fix was to:
- Iterate per-stratum (via
DMLabelGetStratumIS) rather than relying onDMLabelGetValue - Clear stale value-0 entries with
DMLabelClearValuebefore setting the correct positive value - Skip stratum 0 entirely during derivation
All existing submesh tests pass, includingtest_submesh_solve_2d_1d_poisson_hermite(nprocs=7) which chains 3D→2D→1D submesh extractions viaRelabeledMesh.
| # Parallel – 2D -> 1D (Vertex Sets) | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| def _check_vertex_sets_ds(ncells=2): |
There was a problem hiding this comment.
Just make this the test and stick the pytest.mark.parallel here.
There was a problem hiding this comment.
There are a couple more instances of this elsewhere.
|
@lorenzoCorintis would you say that this PR closes #4835? |
Co-authored-by: Connor Ward <c.ward20@imperial.ac.uk>
…rake into fix-submesh-labels
@connorjward The issue in #4835 is different from what this PR fixes. What #4835 reports: when creating a codim-1 submesh via Why this PR doesn't fix it: the root cause is that What this PR does fix: propagation of lower-dimensional labels ("Edge Sets" → "Face Sets" for 3D→2D, "Vertex Sets" → "Face Sets" for 2D→1D) on codimension-1 submeshes, which previously wasn't handled at all. |
|
I only skimmed at the description, but it sounds more like a PETSc issue as PETSc basically reserves "{Cell, Face, Edge, Vertex} Sets" labels. I think the actual issue is simply that these label names are not correctly mapped when making codim-1 submeshes in PETSc. The following simple naive patch almost fixes "Face Sets" (of parent) -> "Cell Sets" (of submesh) case: You can remove redundant labels/points (label 1 and 2 in "Cell Sets" in the above) somewhere. At least, it is worth speaking to Matt about this, I think |
That's a great point — a PETSc-level label remapping in Our approach here is effectively doing the same thing from the Firedrake side: we derive "Edge Sets" / "Vertex Sets" from "Face Sets" on the parent (via A PETSc patch that shifts the label names during
Agreed that it's worth discussing it further. In the meantime, this PR provides a working Firedrake-level solution — if PETSc gains native support we can simplify later. |
| CHKERR(DMLabelDestroyIndex(lbl_ghost)) | ||
|
|
||
|
|
||
| cdef void _populate_lower_dim_labels(PETSc.DM dm, PetscInt tdim): |
There was a problem hiding this comment.
In my feedback I suggested that this only be done for our utility meshes (i.e. not gmsh). This still has the confusing intersection behaviour.
There was a problem hiding this comment.
Maybe this code isn't even necessary because if "Edge Sets" isn't defined then we can use bits from "Face Sets". The thing is we have to avoid blending the two which means if "Edge Sets" is defined we shouldn't be using the original "Face Sets" to build the "Face Sets" for the sub dm.
Are there cases where this doesn't work? If this is critical functionality we could consider relabelling "Edge Sets" with some offset to preserve uniqueness. WDYT?
| if nvals == 0: | ||
| return | ||
|
|
||
| cdef PetscInt *vals = <PetscInt *>malloc(nvals * sizeof(PetscInt)) |
There was a problem hiding this comment.
We prefer PetscMalloc and PetscFree
Description
Submeshdoes not propagate "Edge Sets" to submesh exterior facet markers #4948 --Submeshnow propagates parent "Edge Sets" (3D→2D) and "Vertex Sets" (2D→1D) labels to"Face Sets"on codimension-1 submeshes, so thatsubmesh.exterior_facets.unique_markerscontains the correct boundary markers andds(tag)integration works as expected.submesh_update_facet_labelsreturned early forsubdim != dim, leaving codimension-1 submeshes with no meaningful facet labels. The newelsebranch maps each submesh exterior facet back to its parent point viasubpoint_is, looks up the parent label value, and writes it into"Face Sets"on the subdm. Exterior facets without a parent label receive a fresh default value (max(existing_values) + 1)."Face Sets"fromDMPlexFilter(which sit on cells, not facets) are removed before relabeling to prevent contamination of exterior facet markers.