Skip to content

Refined-submesh (coarse/fine companion) investigation#192

Open
lmoresi wants to merge 2 commits into
developmentfrom
feature/refined-submesh-pair
Open

Refined-submesh (coarse/fine companion) investigation#192
lmoresi wants to merge 2 commits into
developmentfrom
feature/refined-submesh-pair

Conversation

@lmoresi
Copy link
Copy Markdown
Member

@lmoresi lmoresi commented May 18, 2026

Summary

Adds the second submesh flavour alongside the existing rock/air subdomain (extract_region): pull any level out of a mesh's nested refinement hierarchy as a standalone, solver-ready uw.Mesh with full parent lineage, then transfer fields back and forth — same usage pattern as a subdomain submesh (get a mesh → build a solver → map back).

This is an investigation deliverable under docs/examples/submesh_investigation/. No source changes; zero regression risk to the existing rock/air path or core code. Promotion of coarsened_companion into the Mesh API proper is a follow-up.

Design contract: refine-DM mode only

  • A resolution level is extractable only when a genuine nested refinement hierarchy exists (refinement >= 1).
  • Transfer uses PETSc's nested interpolator/injector — exact, parallel-local (DMRefine preserves the coarse partition), no geometric point location.
  • No geometric/General fallback, no KDTree. A non-refined mesh raises a clear error rather than silently degrading.

Key construction detail: the transfer pair is built by refining a single-field clone of the coarse level (dm_f = dm_c.refine()), so refine() itself establishes the setCoarseDM linkage and regular-refinement flag and createInterpolation/createInjection take the exact nested branch (plex.c:10328). Independently cloning two hierarchy levels breaks this and petsc4py exposes no setter to restore it.

What's included

  • refined_pair_prototype.pycoarsened_companion + prolongate/restrict/inject/sample, refine-DM contract guard
  • example_refined_companion.py — sibling of test_region_ds_submesh.py, same annulus + radial-buoyancy Stokes so the two submesh flavours are directly comparable
  • test_refined_pair_solver.py — gating test: extract → labels intact → Stokes solves on the companion → map back; round-trip recovers the coarse solution to ~6e-16
  • test_refined_pair_contract.py — enforces no-refinement → raises (no fallback)
  • probe_hierarchy_labels.py — boundary labels survive every hierarchy level (box, annulus-with-internal-boundary, spherical shell)
  • docs/developer/design/submesh-solver-architecture.md — new "Two Submesh Flavours" section presenting subdomain vs resolution level together

Evidence

  • Stokes solves on the coarse companion; velocity maps back to the fine mesh at machine precision (max|v| relative diff 1e-15)
  • Round-trip (sample ∘ prolongate) recovers the coarse solution to ~6e-16 — with the geometric escape hatch deliberately removed, proving the transfer is genuinely the nested operator
  • Boundary labels (including Internal) confirmed present on every coarse hierarchy level for three mesh families

Test plan

  • test_refined_pair_solver.py — all stages pass
  • test_refined_pair_contract.py — contract enforced
  • example_refined_companion.py — runs clean, parallel to rock/air example
  • probe_hierarchy_labels.py — labels survive coarsening
  • Optional follow-ups: polynomial-exactness sweep (P1/P2/P3/dP1), parallel MPI test, findings write-up

Underworld development team with AI support from Claude Code

Second submesh flavour alongside the existing rock/air subdomain
(extract_region): pull any level out of a mesh's nested refinement
hierarchy as a standalone solver-ready uw.Mesh with full parent
lineage, and transfer fields back and forth.

Contract: refine-DM mode only. Available only when a genuine nested
refinement hierarchy exists; transfer uses PETSc's nested
interpolator/injector (exact, parallel-local, no point location). No
geometric/General fallback, no KDTree -- a non-refined mesh raises
rather than silently degrading.

Key construction: build the transfer pair by refining a single-field
clone of the coarse level (dm_f = dm_c.refine()), so refine() itself
establishes the setCoarseDM linkage and regular-refinement flag and
createInterpolation/createInjection take the nested exact branch.
Independently cloning two hierarchy levels breaks this and petsc4py
exposes no setter to restore it.

Includes:
- refined_pair_prototype.py: coarsened_companion + prolongate/restrict/
  inject/sample, refine-DM contract guard
- example_refined_companion.py: sibling of test_region_ds_submesh.py,
  same annulus+radial-buoyancy Stokes so the two submesh flavours are
  directly comparable
- test_refined_pair_solver.py: gating test (extract -> labels -> solve
  -> map back), round-trip recovers coarse to ~6e-16
- test_refined_pair_contract.py: enforces no-refinement -> raises
- probe_hierarchy_labels.py: boundary labels survive every hierarchy
  level (box, annulus-with-internal-boundary, spherical shell)
- submesh-solver-architecture.md: "Two Submesh Flavours" section

No source changes; investigation lives under docs/examples/.

Underworld development team with AI support from Claude Code
Copilot AI review requested due to automatic review settings May 18, 2026 06:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an investigation bundle under docs/examples/submesh_investigation/ for a second “submesh flavour”: extracting a coarser refinement-hierarchy level as a solver-ready uw.Mesh companion to a refined parent mesh, with nested PETSc FE transfer operators for mapping fields between levels.

Changes:

  • Adds a prototype implementation (coarsened_companion + prolongate/restrict/inject/sample) that enforces a refine-DM-only contract (no geometric/KDTree fallback).
  • Adds runnable gating/contract scripts plus an annulus Stokes example to validate solver viability and round-trip transfer accuracy.
  • Updates the developer design doc to describe “subdomain vs resolution-level” submesh approaches together.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
docs/examples/submesh_investigation/refined_pair_prototype.py Prototype companion-mesh extraction and nested PETSc interpolation/injection transfer utilities.
docs/examples/submesh_investigation/test_refined_pair_solver.py Gating script: solve Stokes on a coarse companion and prolongate/sample back to fine.
docs/examples/submesh_investigation/test_refined_pair_contract.py Contract script: non-refined meshes must raise (no fallback), excessive levels must raise.
docs/examples/submesh_investigation/example_refined_companion.py Annulus Stokes example mirroring the subdomain submesh example for direct comparison.
docs/examples/submesh_investigation/probe_hierarchy_labels.py Probe script to confirm boundary labels persist across hierarchy levels.
docs/developer/design/submesh-solver-architecture.md New section documenting “Two Submesh Flavours” and the refine-DM-only contract.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +253 to +259
def prolongate(companion, coarse_var, fine_var):
"""coarse -> fine, FE prolongation (fills all fine DOFs)."""
matInterp, _, dm_c, dm_f = _get_interpolation(companion, coarse_var, fine_var)
gc = _var_global_vec(coarse_var, dm_c)
gf = dm_f.createGlobalVector()
matInterp.mult(gc, gf)
_write_global_vec_to_var(fine_var, dm_f, gf)
Comment on lines +195 to +200
var._set_vec(available=True)
var.vec.array[...] = loc.array
var._lvec.array[...] = var.vec.array
sfdm.restoreLocalVec(loc)
if hasattr(var, "_canonical_data"):
var._canonical_data = None
Comment on lines +231 to +234
key = (id(coarse_var), id(fine_var))
if key in companion._interp_cache:
return companion._interp_cache[key]

Comment on lines +127 to +128
companion._interp_cache = {} # (id(cv), id(fv)) -> (Mat, Vec)
companion._inject_cache = {} # (id(cv), id(fv)) -> Mat (MATSCATTER)

from enum import Enum

import numpy as np
- Drop unused numpy import
- Fix stale cache-value-shape comments to match the stored tuples
- Key interp/inject caches by variable objects via nested
  WeakKeyDictionary instead of id() (id reuse after GC could alias a
  stale entry with a mismatched FE layout)
- Invalidate _canonical_data AND _cached_data_array on direct PETSc
  writes (matches the core _re_extract_from_parent invalidation; .array
  /.data are copies that otherwise go stale)
- Destroy scratch global Vecs after each transfer (try/finally) so
  repeated per-timestep transfers don't accumulate PETSc objects;
  cached Mat/DM are built once and kept

Verified: contract, gating (round-trip 5.9e-16) and annulus example
all still pass unchanged.

Underworld development team with AI support from Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants