Placed surfaces (2-D serial) + the reconciled reconnect.py - #511
Conversation
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
left a comment
There was a problem hiding this comment.
Adversarial review (authoring session).
Where we attacked it:
- 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.
- Vacuous-test check. Stubbing
place_along_linesto 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. - The reconciliation. We diffed the reconciled
reconnect.pyagainstfeature/fault-split-node's copy hunk by hunk: the residual is exactly therebuild_cavitiesrename +placed_coordsmachinery, i.e. their file + our delta. Their_install_point_sfhunks 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
Stacked on #510; retarget to
developmentwhen 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 (spacingis 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.pyafter the shared branch point — this stream renamedrebuild_without_vertices→rebuild_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 (fromc680e010/c8693579/1d487319), and routesrebuild_with_cones' verbatim SF handoff through the same helper since it carries the same trap. After this lands,reconnect.pychanges only viadevelopment(ledger rule 1), andfeature/fault-split-node's merge diff on it is the one-line rename adaptation plus three doc references.Test renumbered
0848→0853(0848is claimed by the contact stream's pushedtest_0848_fault_split_3d.py; registry in the ledger).Verification
test_0853_place_surface(21),test_0844_line_cut(37),test_0844_reconnect_repair,test_0843,test_0845— all pass.ptest_0844_reconnect_parallel+ptest_0844_line_cut_parallelat np=2 and np=3 (23 each) — pass.place_along_linesto a no-op fails the suite.Underworld development team with AI support from Claude Code