Skip to content

SLM synthesis: 3D packing

Final math derivation

Typeset derivations: EQUATIONS.md (renders on GitHub and the docs site). Headline objective:

\[\rho = \frac{\sum_i \operatorname{vol}(S_i)}{A\,H_{\mathrm{used}}},\qquad H_{\mathrm{used}}=\max_i\,\max_{v\in T_i S_i} v_z\]

CONSOLIDATED 3D MATH (verified against the plugin source). Three feature classes, ONE honest objective.

OBJECTIVE (the single comparison basis; equations.tex eq:rho line 92-101). For placements \(T_i=(R_i,t_i)\in SE(3)\) of closed meshes \(S_i\) in container \(\Omega\) (box \([0,W]\times[0,D]\times[0,H]\) = PackContainer, or IrregularMeshContainer interval set), honest density \(\rho = \sum_i \operatorname{vol}(S_i)\,/\,(A\cdot H_{\mathrm{used}})\), with \(H_{\mathrm{used}} = \max\) over all placed vertices of \(v_z\), counting only non-interpenetrating (\(\phi_{ij} \ge -\varepsilon\)) stones. \(\operatorname{vol}(S_i)\) is TRUE mesh volume (divergence/signed-tetra). NOTE: Core MeshPackItem.VolumeEstimate is bbox volume (MeshPackingModels.cs line 41: Bounds.Size.Volume), so a real signed-tetra volume must be added to Core for the numerator and the volumetric COM (PRISMA risk 7). The harness --pack3d already computes true mesh volume via RhinoCommon VolumeMassProperties.Compute (Validator.cs line 1346,1376); the Core-side rho needs the managed signed-tetra equivalent.

CLASS 1 -- MESH-ACCURATE vs PROXY (irregular-container packing). WHAT THE PROXY COMPUTES (verified). MeshPileHeightmap.TryGetLowestZ (MeshPileHeightmap.cs line 79-106) gives resting height \(z^\ast(x,y,o) = \max_{(m_x,m_y)\ \mathrm{occ}} \big(\mathtt{top}[x{+}m_x,\,y{+}m_y] - B_o(m_x,m_y)\big)\) (P1), with \(B_o\) the underside profile from OrientedMeshHeightmap. ScorePlacement (line 108-126): \(w_h \sum_{\mathrm{col}} \max\big(0,\,(z{+}U_o)-\mathtt{top}\big) + w_c\,(x{+}y)\). The collision predicate replaces \(S_i\) by its column-wise vertical closure \(VC(S_i) = \{(x,y,z): B_i(x,y) \le z \le U_i(x,y)\}\). LOST-FILL LEMMA (KEEP as set-containment only, per verifier). \(S_i \subseteq VC(S_i)\), strict for any stone with a column whose mesh cross-section is not a single interval (concave/overhanging/holed). The proxy tests collision on \(VC\), so legal poses with \(C_{\mathrm{mesh}}=\mathrm{false}\) but \(C_{\mathrm{col}}(VC)=\mathrm{true}\) are discarded. This is a correct structural statement of why the proxy CANNOT express lateral interlock or under-overhang nesting (P1 is a pure max-over-columns; never decreases). WITHDRAWN per verifier: the monotonicity-implies-rho-rises THEOREM. Greedy sequential DBL is NOT density-monotone in the feasible-pose set. The density gain is an EMPIRICAL hypothesis, to be measured. EXACT REPLACEMENT. Voxel occupancy \(O\) over \(\Omega\) at pitch \(h\), stone mask \(M_i^o\); feasible set \(\mathcal F_i^o = \{a : (O \star M_i^o)[a] = 0\}\) (eq:feas). DBL placement \(a^\ast = \operatorname{argmin}\,(a_z,a_x,a_y)\) over \(\mathcal F\) (eq:dbl), with the orientation chosen by lowest resulting top \(a_z+h_z^o\). CAVEAT (verifier, CHECKPOINT_4 line 23): voxel feasibility at pitch h introduces air gaps the continuous skyline does not have; pitch = CellSize/2 can be COARSER in z than the proxy. So (F) is NOT a no-penalty exact analogue of the continuous skyline; it admits the discarded interlock poses but pays a quantization gap that the physics-settle stage is what actually closes (CHECKPOINT_4: seed 32.6% -> settle 35.7%). Zero-interpenetration is by VERIFICATION (CD)+EPA on the ORIGINAL mesh (PRISMA risk 3), never by voxel construction. NARROW PHASE (eq:cd). \(S_i \approx \bigcup_k C_{ik}\) (CoACD); \((T_i S_i)\) meets \((T_j S_j)\) iff some convex pair \((T_i C_{ik},\,T_j C_{jl})\) meets, decided exactly by GJK + EPA. Reuse existing CoacdMeshDecompose.Decompose (Frahan.Masonry.Geometry, returns MeshSnapshot) for proxies; the verify nudge needs a managed GJK/SAT over double[] hulls (net48, no native). HONEST C# BASELINE. The number to beat for this class is NOT 33.8% (that is the Python pybullet harness). The C# measured mesh-honest baseline is the V2-skyline port at 23.7% (30/30, --pack3d, Validator.cs RunPack3D), and the Core GreedyMeshHeightmapPacker itself reaches only ~25% (15/30) because ScorePlacement minimizes added-height-mass and TryGetLowestZ rests on lowest bottom-Z, not resulting top (Validator.cs line 1420-1431 documents this defect). The seed-alone floor is ~32-34% in the Python prototype and BELOW the skyline there; in C# it is unmeasured. Pure-managed voxel seed does NOT beat the benchmark; the win needs the physics-settle stage.

CLASS 2 -- DROP-SETTLE + STABILITY (rubble wall). SHIPPING DROP (RubbleWallSettle.cs, USES Rhino.Geometry line 4; Mesh/Transform). \(z^\ast(v,k) = \max\big({-}\min_{\mathrm{cells}} z_{\min},\ \max_{\mathrm{cell}}(\mathtt{top}[i_x{+}k,i_y]-z_{\min}(\mathrm{cell}))\big)\) is the 2.5D max-of-columns drop; penetration bounded by one cell \(h\). COM gate: support polygon \(Q_i = \operatorname{conv}(\mathrm{contact\ cells})\), signed clearance \(d_i = \min\) over CCW hull edges of inward-normal distance of \(\pi_{xy}(c_i)\); Stable iff \(d_i \ge \mathrm{margin}\). This is single-block Heyman 1966 limit state, frictionless, gravity-only, per-stone. COM is the VERTEX centroid (RubbleWallSettle.cs line 166), not volumetric -- a documented approximation. THE DEFECT (synthesis_n60.json): physics settle RAISED density 36.93->38.7% but LOWERED com_stable% 78.1->71.9. The per-stone COM gate is not monotone with densification. EVOLVED STABILITY METRIC (RBE feasibility, not load factor unless new LP). Contact graph nodes = stones+ground; per-block 6-row equilibrium \(\sum F=0\), \(\sum \tau=0\) with non-tension \(\lambda\ge 0\) and a linearized Coulomb pyramid. CRITICAL FIX (verifier): the existing FrictionConeBuilder (FrictionConeBuilder.cs line 222-237) emits the OUTER pyramid \(\lvert f_t\rvert \le \mu f_n\). For a lower-bound CERTIFICATE the cone must be the INNER pyramid \(\lvert\beta_{\mathrm{axis}}\rvert \le (\mu/\sqrt{2})\,\lambda\). Either scale \(\mu\) by \(1/\sqrt{2}\), or raise \(K\ge 8\) and document residual outer error, or DROP the words certificate/lower-bound/conservative. SECOND FIX (verifier): ManagedQpSolver returns NotImplemented for non-uniform diagonal H (MASONRY_RBE_FIX.md line 48) and is a min-norm QP, so it CANNOT compute s = max load factor. A scalar safety factor s requires NEW pure-managed dense-simplex LP code (own that it is new, ~hundreds of vars). Otherwise deliver only a binary RBE-feasibility verdict. The single-block LP recovers the SIGN of d_i (feasibility-equivalence), not its numeric value. DEPENDENCY: Step 2 depends on task #29 (RBE sign fix, in_progress; production must use RbeQpFormulation.BuildPhysicsCorrected, MASONRY_RBE_FIX.md). VOID-FILL RELAX (Step 1): \(a_i \leftarrow \operatorname{argmin}_{a\ \mathrm{feasible}}(a_z)\) over occupancy with \(S_i\) removed; DOWNGRADE to directional/heuristic (verifier): removing a lower stone can de-support a stone above; needs dependency-order re-settle, and the top map stores only maxima so removing one stone's contribution needs a per-cell contributor list (O(cells_i) bookkeeping), not a single delta. Step 1 is net48 + NO new dependency but NOT Rhino-free (RubbleWallSettle uses RhinoCommon). ALREADY SHIPPED: BulletSettleService (Frahan.Masonry.Physics, Friction default 0.85) and the Settle 3D (Physics) GH component (PackSettle3DComponent.cs, GUID 134785ac-19cb-4f14-85f8-e2f666bd14f6) with CoACD+Bullet wiring; BulletSharp.x64 0.12.0 is already a PackageReference (Core.csproj line 43). Step 3 is contact-manifold harvest on the EXISTING service (Dispatcher.NumManifolds -> points/normals) feeding the RBE input, NOT a new service or a new GUID. RECONCILE friction: Bullet 0.85 vs FrictionConeBuilder 0.84 vs proposal 0.6 -> pick one dry-stone value (0.84 = 40deg, the compas_cra default) and use it consistently.

CLASS 3 -- MIXED-SIZE VALUE + GUILLOTINE. VALUE GREEDY (Dlbf3dMixedSizePacker.cs, Rhino-free Core, AABB area). \(\rho_p = r_p/\max(v_p,\varepsilon)\); sort desc; multi-pass first-free in \((k,j,i)\) lexicographic order; floorOnly fixes \(k{=}0\); ceil-cell footprint \(w_{\mathrm{cells}}=\lceil \dim/h\rceil\). \(J_{\mathrm{rev}} = \sum_p r_p x_p\) is the fractional-knapsack LP-greedy (Dantzig). QUANTIZATION LOSS (Part C, one-sided): \(\lceil \dim/h\rceil \ge \dim/h\) always, equality only on exact division, so the cell mask blocks strictly MORE than the piece occupies. Sub-cell (half-open) reservation or finer/gcd-snapped h tightens the feasible set. CORRECTION (verifier): the (1.25)^3~95% figure needs ALL THREE axes worst-case simultaneously; a single misaligned axis is 25%. The code's forbidden marking is floor..ceil-1 (Dlbf3dMixedSizePacker.cs line 154-159), under-conservative, not the exact ceil-cell. WITHDRAW monotonicity-of-yield: finer h shrinks reserved volume but NOT greedy yield (per-piece best-of-orientation inside one greedy pass is not yield-monotone); state yield gains as --packbench targets. GUILLOTINE (FractureBlockPackComponent.cs, GH, mesh-conforming via mesh.IsPointInside). CountSeparable/GuillotineSeparableFraction live IN the GH component on Rhino BoundingBox (line 459-488). Separable iff |G|<=1 or exists axis+coord splitting into non-straddling lo/hi both recursively separable. TreePackForest guarantees phi=1 by slab-split construction. Measured (grid3 marble, Rhino LIVE, FRACTURE_BLOCK_PACK_DECISION.md): mode-4 voxel-DLBF+multi 53.3% phi<1, mode-5 staged guillotine 49.3% phi=1. These are mesh-voxel (IsPointInside), a DIFFERENT truth domain from the Core AABB Dlbf3dMixedSizePacker, so the Core path will NOT inherit 53.3%; it must be re-baselined on its own AABB fixture. HARD ARCHITECTURE BLOCKER (verifier, confirmed): TreePackForest.cs line 6 using Rhino.Geometry and uses Box/Point3d/Transform despite namespace Frahan.Core.Packing. So a Rhino-free Core Dlbf3dMixedSizePacker CANNOT delegate to TreePackForest for the Hard path. Keep the Hard/guillotine path in the GH layer, OR port a genuinely Rhino-free AABB guillotine slab-tree to Core first. GuillotinePackOptions default forestCount is 256 (GuillotinePackOptions.cs line 44), not 1/64 as the proposal stated. UNIFIED J (scope honestly). \(J = \sum_p r_p x_p + \phi_{\mathrm{bonus}}[\mathrm{allPacked}] - w_{\mathrm{cut}} A_{\mathrm{cut}} - w_{\mathrm{sep}}(1-\phi)\cdot\mathrm{revenue}\). \(w_{\mathrm{sep}}=+\infty\) = Hard (\(\phi=1\)), \(0\) = free pack. The pseudocode only evaluates the Off and Hard ENDPOINTS, so Soft is a 2-way argmax(Off,Hard) selector, not a continuum (verifier). FractureBlockPack GUID is A7E0B0F3-0C0F-4A16-9E3D-0FACE0FACE04; mode-6 Auto + lifting CountSeparable across modules is a section-6 HITL gate (>5 files, shipped GUID, new input).

Ranked evolutions

[1] Class 3 (Core, Rhino-free, lowest risk): sub-cell reservation + best-of-orientation DBL in Dlbf3dMixedSizePacker, with a Core AABB --packbench row

Expected gain: On its own Core AABB bench: sub-cell pitch recovers part of the one-sided ~10-25% per-axis over-reservation; best-of-6-orientation lowers resulting tops. Both are >= baseline-or-equal targets to confirm via --packbench, NOT the inherited +47% mesh-bench lever. Honest target: a few points of fill over the first-free single-orientation Core box baseline once measured.

Math: Replace ceil-cell MarkBlocked with half-open span reservation: cover only cells whose center lies in \([\mathrm{anchor},\ \mathrm{anchor}+\dim+(\mathrm{kerf\ if\ separable}))\). Add orientation loop over the 6 axis-permutations (Size3Perm helper, Rhino-free); replace first-free with best-of-orientation deepest-bottom-left scored by resulting top \((a_z+h_o,\ a_x,\ a_y)\). Adaptive \(h = \max(\min\dim/8,\ \mathrm{gcd\ snap})\). One-sided quantization: \(\lceil \dim/h\rceil \ge \dim/h\), equality only on exact division. NO yield-monotonicity theorem; gains are empirical.

Implementation: New Pack overload Pack(area, catalog, forbidden, h, floorOnly, OrientationMode, kerf) that the existing 6-test-green Pack delegates to with OrientationMode=None, kerf=0 (binary+behaviour compatible). Add Size3Perm Core helper. Keep Dlbf3dMixedSizePacker Rhino-free; DO NOT call TreePackForest (it imports Rhino.Geometry). Add a Core AABB Dlbf3dMixedSizePacker section to Validator.RunPackBench (new deterministic synthetic cuboidal-container + mixed-size catalog fixture) reporting J_rev/fill, so the sub-cell + orientation deltas are measurable. Re-baseline on this AABB fixture; do NOT claim the 53.3% mesh-bench number for the AABB path.

[2] Class 2 Step 1 (Core, no new dependency): void-fill relaxation appended to RubbleWallSettle, measured pre/post on ETH

Expected gain: Recovers a measurable fraction of the heightmap drop's lost density (cascades stones into voids the single pass left). Directional, target a few fill points on ETH rubble piles, demonstrated by the pre/post harness measurement, not asserted.

Math: After the greedy course pass, run bounded relaxation \(a_i \leftarrow \operatorname{argmin}_{a\ \mathrm{feasible}}(a_z)\) over occupancy with \(S_i\) removed, KShifts2 \(\{-3..3\}\), \(\varepsilon_{\mathrm{rel}}=10^{-4}\bar W\), maxSweeps=3, accept a move only if \(z^\ast\) drops by \(> \varepsilon_{\mathrm{rel}}\). Directional/heuristic (NOT proven H-monotone): guard the support-coupling failure mode by re-settling in dependency order with a contact-graph check. Per-cell contributor list (multiset), not a single delta, so remove-i is O(cells_i).

Implementation: Edit Core/Masonry/RubbleWallSettle.cs (net48, no new NuGet, but NOT Rhino-free: it uses RhinoCommon). Reuse RestZ/Contacts/SupportClearance unchanged; deterministic fixed sweep order. Re-run Settle_NoTwoPlacedStonesInterpenetrate and Settle_Deterministic_TwoRunsIdentical after relax to prove 0-penetration-by-construction and bit-identical guarantees survive. Add a RubbleWallSettle row to the harness (rho + Clearance/Stable counts) with relaxation off vs on, on the same ETH stone set.

[3] Class 1 (the actual 3D mesh-density win, needs build+measure): MeshAccuratePacker = voxel constructive seed + verify + optional existing Bullet settle, with a Core --pack3d/--packbench mesh-honest row

Expected gain: HYPOTHESIS, not fact: the Python prototype measured constructive-seed 32.6% (N=30, BELOW the Python 33.8% skyline) and synthesis+physics-settle 35.7% (N=30) / 38.7% (N=60) at zero interpenetration. These are pybullet+VHACD Python numbers; CoACD differs from VHACD and the C# net48 port number is UNMEASURED. The pure-managed voxel seed is parity/below (~32-34%), NOT benchmark-beating. The settle stage is what lifts it. The C# acceptance bar is a re-measured C# rho on ETH1100, not the inherited Python figure. Resolve the 6-orientation (Core GetOrientations) vs 24-orientation (prototype) gap by re-running the prototype at 6 orientations and reporting that honest number.

Math: Stage 1 voxel seed: \(O\) over container AABB at pitch \(h\), \(\mathcal F_i^o = \{a:(O \star M_i^o)[a]=0\}\), DBL \(a^\ast=\operatorname{argmin}(a_z,a_x,a_y)\), orientation by lowest resulting top (eq:feas, eq:dbl). Bounded void-fill relax (eq:relax). CAVEAT: pitch-\(h\) air gaps; \(\mathcal F\) is NOT a no-penalty analogue of the continuous skyline. Stage 2 verify: managed GJK/SAT (eq:cd) on cached CoACD pieces; nudge later stone up to first separation. Zero-interpenetration by VERIFICATION on the original mesh, never by voxel construction. Add managed signed-tetra true mesh volume to Core (MeshPackItem.VolumeEstimate is bbox only) for rho and volumetric COM.

Implementation: New Rhino-free Core files at Core ROOT (NOT Core/Packing -- the mesh packer files OrientedMeshHeightmap.cs/MeshPileHeightmap.cs/GreedyMeshHeightmapPacker.cs/MeshPackingModels.cs all live at Core root): MeshAccuratePacker.cs, VoxelOccupancy.cs (bit-packed ulong[] O, per-column run-length, StoneMask cached by id+orientation), ConvexCollision.cs (managed GJK+SAT over double[] hulls). Extend MeshPackSettings (VoxelPitch=0=>CellSize/2 clamp grid<=256^3, RelaxRounds=2, VerifyInterpenetration=true, UsePhysicsSettle=false, InterpenEps=1e-4). Type-conversion seam: Frahan.Core Vec3/MeshTriangle <-> Frahan.Masonry.Interfaces.MeshSnapshot (double[] xyz + int[] tris) used by CoacdMeshDecompose/BulletSettleService; carry world transforms for verify+settle. Settle stage REUSES the existing BulletSettleService (Frahan.Masonry.Physics) and existing PackSettle3DComponent; degrade to pure-managed voxel seed when CoacdMeshDecompose.IsAvailable / BulletSettleService.IsAvailable are false. Add a mesh-accurate row to harness reporting rho = sum true-mesh-vol / (A*H_used) at verified-zero interpenetration alongside the existing 23.7% V2-skyline port. BUILD AND MEASURE in C# BEFORE any beats-benchmark claim enters a Frahan doc.

[4] Class 2 Step 2/3 (HITL-gated, depends on #29): RBE feasibility verdict via contact-manifold harvest on the EXISTING Bullet service, with the inner-cone fix

Expected gain: Converts the per-stone COM count (which dropped 78.1->71.9 under densification) into a load-path verdict that credits friction + overburden, so reported stability stops decreasing under densification. If the new LP is written, one auditable scalar s*>=0 per assembly. No density change; this is the verifiable-stability-metric deliverable.

Math: Contact graph from at-rest manifolds (normal \(\hat n_c\), tangents \(\hat t_1,\hat t_2\)). Per-block 6-row equilibrium \(\sum F=0\), \(\sum \tau=0\); non-tension \(\lambda\ge 0\); INNER Coulomb pyramid \(\lvert\beta_{\mathrm{axis}}\rvert \le (\mu/\sqrt{2})\,\lambda\) (or \(K\ge 8\)) for a true lower-bound certificate. Deliver a binary RBE-feasibility verdict via the existing min-norm QP machinery, OR write a NEW pure-managed dense-simplex LP to get scalar s* = max load factor (own that it is new code, not reuse: ManagedQpSolver returns NotImplemented for the non-uniform H and cannot maximize s). Single-block LP recovers the SIGN of COM clearance, not its numeric value.

Implementation: Harvest Dispatcher.NumManifolds -> contact points/normals in BulletSettleService (currently returns transforms only). Feed the existing PackSettle3DComponent (GUID 134785ac, already wires CoACD+Bullet; BulletSharp.x64 0.12.0 already a PackageReference). Fix FrictionConeBuilder to the inner pyramid before any certificate claim. Keep Stability Mode COM-only as the DEFAULT (the 2026-05-25 RubbleWallSettle sign-off + Settle_StabilityFlagsComputed_MatchClearance must stay green); Load-path is opt-in. Redefining the existing Stable output semantics is a section-6 deviation from a validated approach (HITL stop). Gate on #29 landing AND an end-to-end test of the horizontal-bed heightmap contact graph. Reconcile mu to 0.84 across Bullet + cone. HITL before editing wiki/research/slm_cards/rubble-drop-settle.md (gated).

[5] Class 3 GH (HITL-gated, >5 files + shipped GUID): unified J + mode-6 Auto in FractureBlockPackComponent, lift CountSeparable to Rhino-free Core

Expected gain: Soft returns max(Off,Hard) by J, so by construction never worse than either hardcoded mode, with the manufacturability trade selected by economics instead of a flag. On the marble mesh-bench the endpoints stay 53.3% (Off) / 49.3% phi=1 (Hard); no measured number contradicted. Lower priority because it is GH-domain, HITL-heavy, and does not move the headline 3D density.

Math: \(J = \sum_p r_p x_p + \phi_{\mathrm{bonus}}[\mathrm{allPacked}] - w_{\mathrm{cut}} A_{\mathrm{cut}} - w_{\mathrm{sep}}(1-\phi)\cdot\mathrm{revenue}\). Soft = 2-way \(\operatorname{argmax}(\mathrm{Off},\mathrm{Hard})\) at the user's \((w_{\mathrm{cut}},w_{\mathrm{sep}})\), NOT a continuum. \(\phi\) via lifted Core SeparabilityFraction on BoundingBox3.

Implementation: Lift CountSeparable/GuillotineSeparableFraction from the GH component to a new Rhino-free Core SeparabilityFraction.cs on BoundingBox3 (new work + its own unit tests, NOT verbatim reuse); GH keeps a thin BoundingBox->BoundingBox3 adapter. Map FractureBlockPack flag: mode 4 -> Off, mode 5 -> Hard, NEW mode 6 -> Soft argmax-J. Keep the Hard/guillotine engine in GH (TreePackForest + the mesh VoxelDlbf/PackStagedGuillotine are Rhino-bound). HITL gate (AGENTS.md s6): shipped GUID A7E0B0F3, new input slot (confirm da.GetData re-indexing keeps canvases green), cross-module lift > 5 files. Get explicit approval before implementing/committing.

Implementation plan

  1. STEP 0 (gate). Confirm scope with Libish: this touches Core packing paths, a shipped GH component (FractureBlockPack GUID A7E0B0F3) and the shipped Settle 3D component (GUID 134785ac), and spans more than 5 files across modules. AGENTS.md s6 requires HITL approval before committing >5 files, before reusing/changing a GUID, and before deviating from the 2026-05-25 RubbleWallSettle validated approach. Get explicit per-item approval. Do NOT launch Rhino (agents never do). Do NOT run dotnet add package (BulletSharp.x64 0.12.0 is already referenced; no new package is needed for the planned work).
  2. STEP 1 (Priority 1, Core, Rhino-free, no new dep). In Dlbf3dMixedSizePacker.cs add a Pack overload with OrientationMode + half-open sub-cell reservation + best-of-orientation deepest-bottom-left; the existing Pack delegates with OrientationMode=None to stay binary/behaviour compatible (keep the 6 existing tests green). Add a Rhino-free Size3Perm helper for the 6 axis permutations. Do NOT delegate to TreePackForest (it imports Rhino.Geometry). Add a Core AABB section to Validator.RunPackBench with a deterministic cuboidal fixture reporting J_rev/fill. Build Core + tests; run RUN_TESTS=1 dotnet run. Re-baseline the Core packer on its own AABB fixture.
  3. STEP 2 (Priority 2, Core, no new dep, NOT Rhino-free). Append a bounded void-fill relaxation pass to RubbleWallSettle.Settle with a per-cell contributor list (multiset) so remove-i is O(cells_i), dependency-order re-settle to guard support coupling. Re-run Settle_NoTwoPlacedStonesInterpenetrate + Settle_Deterministic_TwoRunsIdentical. Add a RubbleWallSettle harness row (relax off vs on) on the ETH set; report pre/post rho + Clearance/Stable.
  4. STEP 3 (Priority 3, the real 3D mesh-density win; build+measure). Create MeshAccuratePacker.cs, VoxelOccupancy.cs, ConvexCollision.cs at the Core ROOT (where OrientedMeshHeightmap/MeshPileHeightmap/MeshPackingModels live -- NOT Core/Packing). Add managed signed-tetra true mesh volume to Core. Extend MeshPackSettings (VoxelPitch, RelaxRounds, VerifyInterpenetration, UsePhysicsSettle, InterpenEps). Implement Stage-1 voxel seed + Stage-2 managed GJK/SAT verify; reuse the EXISTING BulletSettleService (Frahan.Masonry.Physics) and PackSettle3DComponent for the optional settle via the Vec3/MeshTriangle<->MeshSnapshot seam. Degrade gracefully when CoACD/Bullet IsAvailable is false. Add a mesh-honest Core row to the harness reporting rho at verified-zero interpenetration next to the 23.7% V2-skyline port. Re-run the Python prototype at the 6-orientation set to get an honest expected gain.
  5. STEP 4 (Priority 4, HITL + depends on #29). Add contact-manifold harvest (Dispatcher.NumManifolds) to the existing BulletSettleService; feed an RBE feasibility verdict. Fix FrictionConeBuilder to the inner pyramid (mu/sqrt2 or K>=8) before any certificate language. Keep COM-only the default Stability Mode; Load-path opt-in. If a scalar s* is required, write a NEW pure-managed dense-simplex LP (own it as new code). Reconcile mu=0.84 across Bullet + cone. HITL before touching wiki/research/slm_cards/rubble-drop-settle.md.
  6. STEP 5 (Priority 5, GH, HITL-heavy). Lift CountSeparable/GuillotineSeparableFraction to a Rhino-free Core SeparabilityFraction.cs on BoundingBox3 with its own tests; GH keeps a thin adapter. Add FractureBlockPack mode-6 Auto = argmax(Off,Hard) J. Confirm da.GetData slot re-indexing keeps existing canvases green. Get HITL approval before implement/commit.
  7. STEP 6 (validation, AGENTS.md s4). For every class, build + green tests is necessary but NOT sufficient (truth criterion c). Run the harness, record the C# numbers, and request Libish visual validation in Rhino before any beats-benchmark claim enters a Frahan doc. Promote durable results to wiki only after the user chooses the destination.

Benchmark targets

  • GreedyMeshHeightmapPacker / V2-skyline port (mesh-honest density, Core, --pack3d): 23.7% C# mesh-honest (V2-skyline port, 30/30); Core packer itself ~25% (15/30) due to added-mass scoring defect. (33.8% is the Python pybullet harness, not C#.) -> Re-measured C# rho on ETH1100 above 23.7% via MeshAccuratePacker; seed-alone parity ~32-34% then physics-settle target ~35-38% at verified-zero interpenetration. Bar = a C# --pack3d/--packbench number, NOT the inherited Python figure.
  • TreePackForest (3D box guillotine, --packbench): 37.2% fill, 19/20, 100% guillotine-separable, 30 ms (Rhino-bound; AABB ETH) -> Add a rho-desc seed forest before the random forests (default preserves current deterministic output); best-of-F value score >= random-only at equal F and equal phi=1. Re-measure on the same AABB --packbench fixture.
  • Dlbf3dMixedSizePacker (Core AABB mixed-size value, currently NO --packbench row): unmeasured in harness (first-free, no rotation, ceil-cell over-reservation) -> New Core AABB --packbench fixture; sub-cell reservation + best-of-6-orientation DBL >= first-free single-orientation baseline on J_rev/fill (a few points, to confirm). Do NOT claim the 53.3% mesh-bench number for the AABB path.
  • RubbleWallSettle (drop-settle + stability, currently NO --packbench row): unmeasured in harness; defect: com_stable% 78.1->71.9 under densification (synthesis_n60.json) -> New RubbleWallSettle harness row (relax off vs on) on ETH: report rho before/after void-fill relax (target a few fill points up) and a load-path/RBE-feasibility stable count that stops decreasing under densification. Default Stability Mode stays COM-only.
  • FractureBlockPack (mesh-bench yield + separability, GH, Rhino-live): mode-4 voxel-DLBF 53.3% (phi<1, 0% separable); mode-5 staged guillotine 49.3% (phi=1, 100% separable) on grid3 marble (IsPointInside) -> mode-6 Auto = argmax(Off,Hard) J so result >= max(53.3% Off, 49.3% Hard) at the user weights; phi via lifted Core SeparabilityFraction. Endpoints unchanged; selection by economics. HITL-gated.

Code change map

  • src/Frahan.StonePack.Core/Masonry/Quarry/BlockCutOpt/Dlbf3dMixedSizePacker.cs: Add a Pack overload with OrientationMode + half-open sub-cell reservation (cover cells whose center is in [anchor, anchor+dim+kerf)) + best-of-orientation deepest-bottom-left scored by resulting top; existing Pack delegates with OrientationMode=None, kerf=0 (binary/behaviour compatible, 6 tests stay green). Adaptive h default minDim/8 with gcd-snap. Add a Rhino-free Size3Perm helper. Fix the floor..ceil-1 forbidden marking note. Do NOT call TreePackForest (Rhino-bound).
  • src/Frahan.StonePack.Core/MeshAccuratePacker.cs: NEW Rhino-free Core file at Core ROOT (not Core/Packing). Stage-1 voxel constructive seed (feasibleAnchors via sparse cross-correlation + deepest-bottom-left + bounded void-fill relax) returning MeshPackResult; Stage-2 optional managed GJK/SAT verify + optional settle via the EXISTING BulletSettleService. Degrades to pure-managed voxel packer when CoacdMeshDecompose/BulletSettleService IsAvailable is false.
  • src/Frahan.StonePack.Core/VoxelOccupancy.cs: NEW Rhino-free helper: bit-packed ulong[] dense occupancy O over the container AABB at pitch h; mark-outside-Omega from PackContainer or IrregularMeshContainer intervals; per-column run-length for fast vertical free-run scans; shift/OR/XOR of a StoneMask; StoneMask build (rasterize MeshPackItem verts+tris) cached by id+orientation key. Clamp grid <= 256^3 and document graceful degrade.
  • src/Frahan.StonePack.Core/ConvexCollision.cs: NEW Rhino-free managed GJK + SAT over double[] convex-hull point sets (eq:cd narrow-phase) returning disjoint/penetrating + an approximate separation axis for the verify nudge. eps matched to InterpenEps. Used only in Stage-2 verify when CoACD proxies exist.
  • src/Frahan.StonePack.Core/MeshPackingModels.cs: Add a managed signed-tetra true mesh volume (replace/augment VolumeEstimate which is bbox only, line 41) for the honest rho numerator and volumetric COM. Extend MeshPackSettings with VoxelPitch (0=>CellSize/2), RelaxRounds=2, VerifyInterpenetration=true, UsePhysicsSettle=false, InterpenEps=1e-4 (additive; keep existing ctors).
  • src/Frahan.StonePack.Core/Masonry/RubbleWallSettle.cs: Append a bounded void-fill relaxation pass (maxSweeps=3, KShifts2 {-3..3}, releps=1e-4*meanW) with a per-cell contributor multiset so remove-i is O(cells_i) and dependency-order re-settle to guard support coupling. Add an optional Rhino-free LoadPathStability submodule (contact graph + inner Coulomb pyramid + RBE feasibility verdict). NOT Rhino-free (uses RhinoCommon). Keep COM-only default; Load-path opt-in (the 2026-05-25 sign-off and Settle_StabilityFlagsComputed_MatchClearance must stay green).
  • src/Frahan.StonePack.Core/Masonry/Equilibrium/FrictionConeBuilder.cs: Add an inner-pyramid option (scale mu by 1/sqrt2, or default K>=8) so the RBE feasibility result is a true lower-bound certificate. The current rows emit the OUTER pyramid |f_t|<=muf_n (line 222-237). Do NOT call s a certificate until this lands.
  • src/Frahan.StonePack.Core/Masonry/Physics/BulletSettleService.cs: Add at-rest contact-manifold harvest (Dispatcher.NumManifolds -> contact points + normals) so the RBE-LP input exists; the service currently returns transforms only. Reconcile Friction default (0.85) with FrictionConeBuilder (0.84) to one dry-stone value. EXISTING service; do not recreate.
  • src/Frahan.StonePack.Core/Packing/SeparabilityFraction.cs: NEW Rhino-free Core file: lift CountSeparable/GuillotineSeparableFraction from FractureBlockPackComponent (currently GH, on Rhino BoundingBox) to operate on BoundingBox3, with its own unit tests (new work, not verbatim reuse). GH keeps a thin BoundingBox->BoundingBox3 adapter.
  • src/Frahan.StonePack.GH/Quarry/FractureBlockPackComponent.cs: Map the Packer flag: mode 4 -> Off, mode 5 -> Hard, NEW mode 6 'Auto' -> Soft argmax(Off,Hard) J. Route through the lifted Core SeparabilityFraction. Keep GUID A7E0B0F3-0C0F-4A16-9E3D-0FACE0FACE04 (append-only inputs/outputs). HITL: shipped GUID + new input slot + >5-file cross-module change.
  • tools/Frahan.StonePack.Harness/Validator.cs: Add three new --packbench/--pack3d rows: (1) MeshAccuratePacker mesh-honest rho on the ETH OBJ meshes next to the 23.7% V2-skyline port, with verified-overlap count; (2) a Core AABB Dlbf3dMixedSizePacker section on a new deterministic cuboidal+mixed-catalog fixture reporting J_rev/fill; (3) a RubbleWallSettle row (relax off vs on) reporting rho + Clearance/Stable. Without these the measurable-via-packbench claim is false for all three classes.
  • tests/Frahan.StonePack.Tests/MeshAccuratePackerTests.cs: NEW headless tests: lost-fill regression (notch+tongue the skyline rejects but voxel interlocks), determinism (Seed=0 two runs identical), container-cavity fill via IrregularMeshContainer, zero verified-interpenetration on a small ETH subset, graceful degrade when native libs absent (IsAvailable=false).
  • tests/Frahan.StonePack.Tests/Dlbf3dMixedSizePackerTests.cs: Add tests: sub-cell reservation places >= baseline count on a misaligned-dimension AABB bench; ThreeAxis places a tall piece None cannot on a thin slab; default overload unchanged so the existing 6 tests stay green.
  • tests/Frahan.StonePack.Tests/RubbleWallSettleTests.cs: Add tests: void-fill relax leaves used-height <= before (directional); non-penetration still holds (PairPenetration <= 1.05*cell); determinism bit-identical with relax on; single-box-on-floor LP feasibility-sign equals COM clearance sign; overburden case reports lower feasibility than the COM gate would.

Open risks

  • The C# numbers for every 3D class are UNMEASURED. 33.8% / 35.7% / 38.7% / 53.3% / 49.3% are Python pybullet+VHACD or Rhino-live-mesh numbers, not the C# net48 Core engines. CoACD != VHACD; the C# acceptance bar is a re-measured harness number, per AGENTS.md s4 (green tests are not correctness; HITL visual validation required).
  • The pure-managed voxel seed does NOT beat the benchmark on its own: CHECKPOINT_3/4 put it at 32.6% (N=30), BELOW the Python 33.8% skyline. The density win depends on the physics-settle stage. State the floor honestly as parity/below.
  • Rhino-free architecture blocker: TreePackForest.cs imports Rhino.Geometry, so a Rhino-free Core Dlbf3dMixedSizePacker cannot delegate to it. The Hard/guillotine path must stay in GH or be re-ported Rhino-free first. RubbleWallSettle also uses RhinoCommon, so Step-1/2 are net48-no-new-dep but NOT Rhino-free.
  • Type-conversion seam risk: Core MeshPackItem (Vec3/MeshTriangle, Frahan.Core) vs the MeshSnapshot (Frahan.Masonry.Interfaces) consumed by CoacdMeshDecompose/BulletSettleService. World transforms for verify+settle must be carried through the seam correctly (BulletSettleService returns world=R*local with R as the transpose of the BulletSharp basis, line 153-160).
  • RBE certificate validity: FrictionConeBuilder emits the OUTER pyramid; without the inner-pyramid fix s is not a lower bound. ManagedQpSolver cannot maximize a load factor (NotImplemented for non-uniform H), so a scalar s needs NEW dense-simplex LP code, and Step 2 depends on task #29 (in_progress).
  • HITL gates (AGENTS.md s6): the full plan touches >5 files across modules, two shipped GUIDs (A7E0B0F3 FractureBlockPack, 134785ac Settle 3D), the validated 2026-05-25 RubbleWallSettle approach, and gated wiki/research cards. Each is a mandatory stop before commit. Redefining the existing Stable output is a documented-approach deviation requiring explicit approval.
  • Cost/time risk: adaptive h=minDim/8 multiplies grid cells ~8x and ThreeAxis adds ~6x; the 256^3 bit-packed grid + managed GJK + Cooley-Tukey must run within harness/MCP time budgets on net48 single-thread. The Python prototype used scipy fftconvolve. Benchmark before defaulting ThreeAxis / fine h.
  • Determinism: voxel seed + integer argmin + fixed orientation order is reproducible, but BulletSettleService physics is only deterministic for fixed step count single-thread; mark settle determinism P1 and keep it opt-in.