Skip to content

Placed surfaces (2-D serial) + the reconciled reconnect.py - #511

Merged
lmoresi merged 6 commits into
developmentfrom
feature/placement-reconciled
Aug 9, 2026
Merged

Placed surfaces (2-D serial) + the reconciled reconnect.py#511
lmoresi merged 6 commits into
developmentfrom
feature/placement-reconciled

Conversation

@lmoresi

@lmoresi lmoresi commented Aug 8, 2026

Copy link
Copy Markdown
Member

Stacked on #510; retarget to development when it lands. Second step of the untangling ledger (planning hub, liaison item, 2026-08-09).

What this adds

utilities/place_surface.py — embed a surface by placing its own points, deleting the mesh vertices in the way, and retriangulating the cavity. Unlike the cut it handles a fault ending inside the mesh (the tip is an angle-interpolated fan, no width floor) and a surface finer than the local h (spacing is a parameter). 2-D, serial; parallel is refused with the reason (placement adds points, and the chart-expansion star-forest rebuild does not exist).

Guards learned the hard way, all tested: cells owning an interior labelled facet are held out of the cavity (protecting a vertex does not protect a facet — both cells of a facet can be cleared while every corner is protected, silently destroying an earlier surface); every placement re-reads each earlier surface's facet count off the result mesh; the walk carries a quality floor because collinear wall triangles pass every positivity test at area 1e-15.

Honest capability note, in the docs: for closely spaced surfaces the cut is currently more capable (accepts 1.0 h separation vs placement's 1.5 h); placement's limit is implementational (one cavity, one surface) where the cut's is inherent.

The reconciliation

Both fault streams evolved utilities/reconnect.py after the shared branch point — this stream renamed rebuild_without_verticesrebuild_cavities (it now adds placed vertices as well as deleting), the contact stream added _install_point_sf (a rebuilt star-forest must also be installed on the coordinate DM or parallel HDF5 saves are unloadable serially; their measurement). This PR folds their hunks in verbatim (from c680e010/c8693579/1d487319), and routes rebuild_with_cones' verbatim SF handoff through the same helper since it carries the same trap. After this lands, reconnect.py changes only via development (ledger rule 1), and feature/fault-split-node's merge diff on it is the one-line rename adaptation plus three doc references.

Test renumbered 08480853 (0848 is claimed by the contact stream's pushed test_0848_fault_split_3d.py; registry in the ledger).

Verification

  • 82 tests: test_0853_place_surface (21), test_0844_line_cut (37), test_0844_reconnect_repair, test_0843, test_0845 — all pass.
  • Parallel: ptest_0844_reconnect_parallel + ptest_0844_line_cut_parallel at np=2 and np=3 (23 each) — pass.
  • Style gate clean. Negative control: stubbing place_along_lines to a no-op fails the suite.

Underworld development team with AI support from Claude Code

lmoresi added 3 commits August 9, 2026 08:41
The cut represents a surface by splitting every edge it crosses, and every
restriction it carries follows from that: an edge can be split at one point, so
two flanks closer than one element compete for the same edge and the cut is
refused; the surface can never be finer than the local h; and a triangle the
surface enters but does not leave has no split that represents it.

place_along_lines does the same job with the opposite move. It asserts the
surface's own points as vertices, deletes the mesh vertices in the way, and
retriangulates the cavity so the placed segments survive as element edges. A
fault tip terminates inside the mesh, the point spacing is a parameter, and two
surfaces may run at any separation. Measured on a 1/16 box: the cut accepts two
parallel surfaces one element apart and refuses them at half an element;
placement carries them to a tenth of an element.

How the cavity is filled. How many ends reach the domain wall decides its
shape - none an annulus, one a disc, two a disc per flank - and all three are
one walk between two chains, ordered by arc length around the surface's own
boundary. At a zero-thickness tip the two flanks meet at a point, so the turn
through 180 degrees is given a window of that parameter to itself and is
interpolated across by angle about the tip: the tip comes out as a fan of one
placed vertex against many cavity vertices, with no width floor.

Two things measurement forced that the prototype never met. The walk needs a
third move, because a cavity ring is not convex: clipping a protruding corner
off as an ear, and where even that fails, swallowing the spike of surviving mesh
the walk wedged on and re-clearing. Over 100 random traces on a uniform mesh and
100 on a graded one, that is 2 and 8 failures without it against none with, area
exact to 2e-16 throughout. And the walk needs a quality floor rather than only
an orientation test - where the cavity reaches the wall the ring runs along it,
and two wall vertices plus the surface's end on that wall are three collinear
points, which a wall differing in the last bit resolves confidently into a cell
of area 1e-15 and a zero angle that every positivity test passes.

An end reaching the wall slides the boundary VERTEX along the wall onto it
rather than moving the surface, so the surface stays where it was asked for, and
the slide is refused where the wall turns so the domain is never deformed.
Snapping the trace instead put the chain 9 % of h off. A split wall facet
inherits the labels the whole one carried, or a boundary condition steps over
the hole left behind.

reconnect.rebuild_without_vertices becomes rebuild_cavities and takes placed
coordinates: it is now the one rebuild that changes the point chart in both
directions. It does not extend the star-forest's leaf set, so placement refuses
in parallel rather than returning a mesh whose forest is silently wrong. The cut
remains the parallel path, and add_conforming_surface is untouched.

Underworld development team with AI support from Claude Code
…irst

Placing a surface against one already embedded destroyed it, silently. The
cavity protects interface VERTICES from deletion, but a cell is not a vertex:
both cells supporting an interface facet could be cleared while every one of
their corners was protected. The facet then has no support left, the refill has
no reason to recreate that edge, and the earlier surface loses a facet out of
the middle of its chain. Measured on a T junction: a trunk of 21 facets came
back with 20, the junction vertex carried only the branch's label, and nothing
raised.

Three changes. Cells owning an INTERIOR labelled facet are held out of the
cavity - interior, because the domain's own walls carry edge labels too and
holding their cells would forbid clearing anything against a wall, which is what
a surface crossing the domain must do. A vertex may only be deleted if every
cell of its star can be cleared, so the cavity stays the union of its victims'
stars. And every placement re-reads each earlier surface's facet count off the
RESULT mesh and refuses if one dropped; the label being written may grow, since
several polylines may share a name, but no other may change.

This retracts a capability claim rather than adding one. The suite asserted that
two surfaces a tenth of an element apart could be placed - an identity summed
over each placement's own counts, which a partial corruption still satisfies.
Re-measured with both surfaces checked intact afterwards, on a 1/16 box: placing
one at a time accepts 1.5 h separation and refuses 1.0 h, while the cut accepts
1.0 h and refuses 0.5 h. For closely spaced surfaces the cut is currently the
more capable of the two, and the docs and module docstring now say so.

The two limits are not the same kind, which is what survives of the argument.
The cut's is inherent: converging flanks cross the same edge and an edge splits
at one point. This one is an implementation limit - one cavity holds one surface
- and lifting it means placing both into a single cavity, which is the
finite-width ribbon and is not built.

Junctions now refuse loudly at every resolution tested (T, Y and X alike)
instead of T corrupting quietly. Interior end-snapping is what will turn that
refusal into an abutment.

Underworld development team with AI support from Claude Code
… placement test

The two fault streams evolved utilities/reconnect.py independently after the
6d494db branch point: the contact stream added _install_point_sf (a rebuilt
star-forest must also be installed on the coordinate DM, or a parallel HDF5
save writes every shared vertex as owned on every rank and the serial reload
dies in coordinatesLoad) while this stream renamed rebuild_without_vertices to
rebuild_cavities and taught it to add placed vertices as well as delete.

This commit folds their three hunks in verbatim - the helper, the two call
sites in _rebuild_point_sf, and the docstring sentence - so ONE version of the
file exists and their branch's later merge diff on it is exactly the rename
adaptation and nothing else. rebuild_with_cones' verbatim star-forest handoff
is routed through the same helper: it creates its coordinate DM before the SF
is installed, so it carries the same trap, and leaving one of three sites on
the old pattern is how the next divergence starts.

The placement test moves from test_0848 to test_0853: 0848 was claimed twice
across the branches (their test_0848_fault_split_3d.py is pushed), and the
number registry in the planning-hub liaison ledger now records 0848 as the
contact stream's and 0853 as this stream's.

Cross-branch takings, per the ledger rules: the _install_point_sf hunks are
from c680e01 / c869357 / 1d48731 on feature/fault-split-node.

Underworld development team with AI support from Claude Code

@lmoresi lmoresi left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Adversarial review (authoring session).

Where we attacked it:

  1. The silent-corruption class. The reason this PR exists in this shape: protecting interface vertices does not protect interface facets — both cells of a labelled facet can be cleared while every corner is protected, and an earlier surface loses a link with nothing raised. The guard holds those cells, requires a victim's whole star to be clearable, and re-reads every earlier surface's facet count off the result mesh. The regression test reproduces the original corruption (trunk 21→20 facets) when the guard is reverted.
  2. Vacuous-test check. Stubbing place_along_lines to a no-op fails the suite (negative control run before commit). The close-pair test asserts counts off the result, not summed bookkeeping — the earlier version of that test asserted an identity a partial corruption still satisfies, and the file now documents that mistake so it is not re-made.
  3. The reconciliation. We diffed the reconciled reconnect.py against feature/fault-split-node's copy hunk by hunk: the residual is exactly the rebuild_cavities rename + placed_coords machinery, i.e. their file + our delta. Their _install_point_sf hunks are verbatim (from c680e01/c8693579/1d487319). One deliberate extension: rebuild_with_cones' verbatim SF handoff goes through the same helper — it creates its coordinate DM before the SF is installed, so it carries the identical trap; leaving one of three sites on the old pattern is how the next divergence starts.

Honest limitations, in the docs rather than discovered later: serial only (parallel refuses with the reason — placement adds points and the chart-expansion SF rebuild does not exist); for closely spaced surfaces the CUT is currently more capable (1.0 h vs 1.5 h separation, measured, both surfaces verified intact); junctions refuse loudly — no junction capability is claimed.

Efficiency note: the per-placement passes are O(cells) Python loops and _interface_facet_counts scans all labels; fine at study scale (~10⁴ cells, <1 s), and the right time to optimise is when the 3-D generalisation forces the data structures anyway.

Recommend merge after #510; the contact stream's adaptation is the one-line rename + two→three return values, recorded in the planning ledger.

Underworld development team with AI support from Claude Code

@lmoresi
lmoresi changed the base branch from feature/mesh-surgery-base to development August 9, 2026 02:23
@lmoresi
lmoresi merged commit 9eee174 into development Aug 9, 2026
1 check passed
@lmoresi
lmoresi deleted the feature/placement-reconciled branch August 9, 2026 02:24
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.

1 participant