Solver Spec — Model Card
Honest declaration of what the production fracture kernel actually does today. Each section names its provenance file(s) and its limits of validity. The scoreboard below tracks which Phase upgrades each row.
What shipped recently
- Phase 17 — Static-LGR fracture-geometry export. One-way vendor-neutral CSV handoff (ECLIPSE CARFIN / tNavigator LGR / CMG REFINE) via
buildFractureLgrExport. See Simulator backend. - Phase 16 — Frac ↔ reservoir coupling default-ON. Both single-phase and 3-phase black-oil coupling flipped from opt-in to default-on (explicit-off overrides persist). See Coupling direction.
- Phase 15 — Carreau-Yasuda + multi-mode rheology. Yasuda transition sharpness, Maxwell relaxation spectrum, first-normal-stress difference / Weissenberg number, plus shear-history crosslinker breakdown against the pump schedule. Legacy Carreau collapse is byte-identical. See Coupling direction.
- Phase 14 — 3-phase black-oil IMPES kernel. Oil / gas / water on the voxel grid with Brooks-Corey Stone-I-lite rel-perms, Standing / Vasquez-Beggs PVT, and an energized-CO₂ PR-EOS gas-FVF bridge. Selectable as a third Builder fluid-model kind. See Coupling direction.
- Thermo-poro-elastic in the Newton loop.
thermoPoroCouplingruns after single-phase and BO-3P insidecoupledNewtonStepand wins last onpPorePa[]. See T-M-H voxel. - Wellbore multiphase — Standing-Katz Z(p, T). Baked-in DAK + Sutton pseudo-criticals + Wichert-Aziz sour-gas correction on the drift-flux preview; ideal-gas collapse in the low-p limit. See Wellbore flow.
Only three rows remain next-up: parent-child interference, non-planar 3D, and UFM / fully-3D DFN. All three are wiring PRs into the same Newton outer loop — the physics is already built.
Built · Next up · Out of scope
Rows are sorted Next up → Shipped. The Roadmap column lists the actual next ship; truly impossible items live in the red “Not in scope” chips so we stop dangling them.
- ShippedLive in production today and exercised by the route below.
- Next upPhysics is built; the remaining PR wires it into the runtime UI and the Newton outer loop.
- In Newton loopThis module runs inside
coupledNewtonStep, the outer nonlinear corrector that updates pressure, fracture width, and state variables together until the residuals converge. - Out of scopeExplicitly excluded today — e.g., OLGA-class transient multiphase or full UFM XFEM.
| Module | Today | Roadmap / Not in scope |
|---|---|---|
| Fracture mechanics | Cell-based Pseudo-3D, fixed height, LEFM tip + Charles power-law growth (Planar-3D Newton step + front-motion coupling landed @ Phase 4.5, beta — Sneddon-asymptote K_I inversion drives Charles V_n into the level-set tip propagator) + Phase 7 inc 1–5 tip-kernel upgrades, beta — opt-in Osher–Sethian Godunov upwind |∇φ| (`gradOperator: "godunov"` on `tipPropagationStep`), Garagash–Detournay viscous–toughness MK universal asymptote (`tipModel: "mk-universal"` on `frontMotionStep`, Dontsov–Peirce 2017 max-of-asymptotes blend of K-vertex Charles and M-vertex viscous V_M = (E'/μ')·(w/(β_m·s^{2/3}))^3), Sussman-Smereka-Osher 1994 level-set reinitialization (`reinitializeLevelSet` — fictitious-time march of φ_τ + sign(φ₀)·(|∇φ|−1) = 0 with smoothed sign S_ε(φ) = φ/√(φ²+ε²) and Engquist-Osher upwind branch select, optional narrow band) so repeated propagation steps stay near-SDF, AND auto-call reinit between front-motion steps in the live Newton loop via opt-in `reinit: { iterations, dtau, bandWidth }` on `frontMotionStep` (forwards verbatim to `reinitializeLevelSet`, surfaces `reinitMeanGradNorm` on the result, off by default so Phase 4.5 callers stay byte-identical); all four flags off by default so Phase 4.5 callers stay byte-identical + Phase 7 inc 15 — same `reinit: { iterations, dtau, bandWidth }` option now wired through the `kgdMovingTipRunner` MK moving-tip wrapper around `coupledNewtonStep` (1-D-extruded SDF φ(x, y) = activeHalfLenEnd − |x| fed verbatim to `reinitializeLevelSet`, surfaces `reinitMeanGradNorm` on each `KgdMovingTipDiagnostic`, drives the next step's front-conforming warm-start truncation via φ_reinit ≤ 0); on the 1-D KGD problem the reinit'd SDF zero-set coincides cell-for-cell with the legacy `|x|` truncation, so wellbore-cell widths are byte-identical to `mkTipFeedback: true` (rms ≈ 0.106 / 0.125 bun-vs-vitest), and reinit `iterations: 8` is now baked into the default `kgd-width` registry runner so the live-kernel rms drops from ≈0.1251 → ≈0.1063 vs the pre-Phase-7-inc-15 baseline (Δ ≈ 0.019 log-units, |∇φ| → 1.0005), AUTO-TIGHTENING the registry `toleranceLog` from 0.13 → 0.115 (≈0.0087 log-units of headroom, above the established ≥0.005 floor); the `runnerImpl: "newton-kernel"` override path keeps its own 0.13 ceiling because the fixed-tip Newton runner does NOT consume reinit; the wired option matches `frontMotionStep`'s API and gives 2-D extensions a drop-in front-tracking SDF + Phase 8 — fully-planar 3D propagation: `propagatePlanar3D` in `src/lib/planar3DPropagation.ts` extends `solveImplicitHeight` with a TRUE per-bench KGD-style aperture profile w_max(layer) = 2·max(0, p − σ_h_layer)·H/E′_layer (no E′_eff homogenisation), elliptic-prism cross-section A_xs = Σ (π/4)·w_max·t_contained, and leakoff-free volume-balance length L = q·t/(2·A_xs). Drift-guard `planar3DPropagation.test.ts` (8 tests: mass-balance, layered-pay aperture dominance, q-linear length, p-monotone height/width, containment exclusion, input validation) + benchmark suite `planar3DBenchmarks.ts` cross-checks per-bench Sneddon analytic + height-time growth (`planar3DBenchmarks.test.ts`, 4 tests). Comparator panel `Planar3DComparatorPanel` mounted at `/planar3d-validation` | All self-contained kernel items closed; remaining work is field validation against site DFITs and offset history (shared with proppant-transport row). Field-validation ingest path shipped @ Phase 7 inc 22 — `/projects/$p/workspaces/$w/field-validation` lets teams upload measured (t, P, T) gauge logs or (MD, P, T) gradient surveys and attach DFIT registry residuals + Pressure Advisor cross-checks; verdicts persist to `field_validation_campaigns` (team RLS) so kernel residuals can be challenged on real data without code changes. **Field-validation ingest UI wired end-to-end** — `FieldValidationReviewCard` (per-well status rollup + expandable rejected-row table with rejection reasons) mounted on the `/field-validation` route, and `src/lib/m2BarrierArrestIngest.ts` projects M2 barrier-arrest verdicts (per-probe K_I / K_Ic / containment gates / stepover affinity, summary rollup) onto the shared `VerdictExtras` object so the campaign flows through the existing `computeVerdict` pipeline without new schema. Drift-guards: `m2BarrierArrestIngest.test.ts` (9) + `fieldValidationReview.test.ts` (9). **Moat M2 (per-bench K_Ic barrier arrest) primitive + kernel wiring LIVE** — pure `src/lib/planar3d/perBenchBarrierArrest.ts` Tier-2 primer + opt-in `barrierPolicy` field on `tipPropagationStep` that gates per-active-tip V_n against the per-bench K_Ic barrier. Undefined ≡ byte-identical legacy propagation. Drift-guards: `perBenchBarrierArrest.test.ts` (19) + `tipPropagationBarrierPolicy.test.ts` (3). |
| Fluid–solid coupling ShippedIn Newton loop | Fully-coupled Newton-Raphson via `coupledNewtonStep` is the DEFAULT path for every live-kernel surface (DFIT registry's `kgd-width`/`pkn-length`/`carter-falloff` entries, all proppant-validation runners, KGD/PKN moving-tip wrappers); the legacy sequentially-explicit forward-Euler operator split remains reachable through the analytical-self runner registry entries kept as regression references — no caller path defaults to the explicit split anymore (Phase 4 promotion @ Phase 7 inc 16). Window-sweep harness `runDfitWindowSweep` (`src/lib/planar3d/dfitWindowSweep.ts`) drives the Newton path over arbitrary `[tStart, tEnd]` grids so future phases can chart rms-vs-window without retouching kernel defaults; A/B harness `runDfitAbCompare` (`dfitWindowSweep.ts`) gates `meshAdapt` / `tipBC` / Carter-injection-sizing toggles before promotion. | All self-contained Newton-coupling work landed: Carter validation grid broadened `[1, 2] s` → `[1, 100] s` @ Phase 7 inc 21 with auto-sized per-step injection feeding the kernel seed-floor. Opt-in `meshAdapt: "tip-refined"` and `tipBC: "kic-clamped"` knobs on `coupledNewtonStep` were sweep-audited @ Phase 7 inc 22b via `runDfitAbCompare` across all 3 registry entries on their tightened windows: `tip-refined` is a byte-identical no-op on `kgd-width` (rms 7.0e-2) and `pkn-length` (rms 1.33e-1) — strip cell count doubles 41 → 81 but the converged residual does not move — and is mildly worse on `carter-falloff` (6.9e-8 → 2.5e-7); `kic-clamped` is actively harmful on every entry (KGD 7.0e-2 → 1.07e-1 eats 80 % of headroom, PKN 1.33e-1 → 3.37e-1 overruns the 0.15 ceiling). Verdict: neither knob clears the ≥0.005 log-units headroom rule for ANY entry — keep both flags off-by-default; re-run the sweep only if a future kernel change inverts the residual signature. |
| Leak-off ShippedIn Newton loop | Carter √t baseline + kernel-coupled PDL & dynamic filter-cake + two-phase α_s throttle (Planar-3D Newton step consumes Carter·(1+α·Δp)/(1+κ·√t)·(1−α_s/α_pack)^n as a per-cell sink @ Phase 5 inc 6, beta) + DFIT validation harness with /dfit-benchmarks dashboard now driving the live `coupledNewtonStep` Phase 3.5 kernel through ALL 3 entries (`runnerKind: 'live-kernel'`): kgd-width via `kgdMovingTipRunner.ts` (MK universal moving-tip wrapper + reinit i=8 SDF warm-start, rms≈0.106, tol AUTO-TIGHTENED 0.13 → 0.115 on [1, 2] s @ Phase 7 inc 13/15), pkn-length via `pknNewtonRunner.ts` (height-contained 1D-strip volume-balance, rms≈0.13 on [0.5, 1] s, tol AUTO-TIGHTENED 0.20 → 0.15 on [0.5, 1] s @ Phase 7 inc 7/21), AND carter-falloff via `carterFalloffNewtonRunner.ts` (per-step mass-balance recovery against the kernel's exact discrete encoding `2·C_L·(√tn − √to)/dt`, rms ≈ 7.2e-8 log-units on [1, 2] s, tol AUTO-TIGHTENED 0.05 → 1e-5 @ Phase 7 inc 14/22, gated by a dedicated merge-blocking step in `regression-gate.yml`) | All 3 DFIT entries `live-kernel` and sweep-tightened (KGD 0.115 on `[1, 2] s`, PKN 0.15 on `[0.5, 1] s`, Carter 1e-5 on `[1, 100] s` after the per-step injection-sizing helper landed in `carterFalloffNewtonRunner` @ Phase 7 inc 21). Next: KGD/PKN window-tightening via opt-in `coupledNewtonStep` `meshAdapt: "tip-refined"` + `tipBC: "kic-clamped"` (defaults preserve current rms; flip registry tolerances only after the A/B harness shows ≥0.005 log-units of headroom) |
| Proppant transport | Single-phase concentration advection (RZ settling, conv., slumping, MPM bedload, K-D rheology) + Eulerian-Granular two-phase scaffold with K-D μ_eff(α_s) coupling, PLA shear-induced migration + bridging/screen-out gate, hybrid Eulerian-Lagrangian particle tracker (bilinear-sampled bulk velocity + RZ-hindered Stokes slip + per-particle bridging gate, mass-conservative seed↔bin round-trip), AND kernel-side α_s feedback into the Carter leak-off sink @ Phase 5 inc 6, beta — full two-phase ↔ leak-off loop closed + Phase 8 — wellbore proppant cascade preview (`wellboreProppantPreviewDriver.ts` + `wellboreScalarTransport.ts`): 1-D first-order upwind scalar advection of clean-carrier ppa down the segmented string on the recovered face-velocity field, with multi-slug `runScheduleProppantPreview` consuming a `TreatmentStep[]` schedule (preflush · pad · slurry · postflush role bands) so surface-prescribed ppa(t) → bottom-cell ppa(t) drives the perf-arrival concentration fed into `proppantPlacement`. HONEST SCOPE (preview only, NOT field-validated): single-species scalar (no inter-species coupling), NO proppant-vs-fluid slip in the wellbore (carrier-tracking bulk velocity), NO bridging / screen-out gate, NO perf erosion or friction feedback from the transported solids, NO gel-load or temperature-dependent settling. Drift-guards: `wellboreProppantPreviewDriver.test.ts` (7) + `wellboreScalarTransport.test.ts` | Validation against site DFITs and offset history. Field-validation ingest path shipped @ Phase 7 inc 22 — `/projects/$p/workspaces/$w/field-validation` accepts measured downhole streams + Pressure Advisor cross-checks (team-shared via `field_validation_campaigns` RLS) so two-phase / leak-off behaviour can be challenged on real-site data without solver-code changes. **Moat M3 (settling-bed proppant transport + screen-out predictor) primitive + kernel wiring LIVE end-to-end** — pure `src/lib/planar3d/settlingBedScreenOut.ts` Tier-2 primer + opt-in `screenOutPolicy` field on `coupledWidthPressureStep` that, after each Picard sweep, walks the mesh once, estimates per-cell wall shear from `(w/2)·|∇p|`, evolves `bedHeightM` via `bedHeightAfterStepFt`, classifies per-cell risk labels + worst-cell chip, AND emits a per-cell `bypassVelocityMultiplier` = `1/(1 − φ)` (clamped [1, 20]) for the NEXT-step mobility feedback. `bypassVelocityMultiplierIn` on the input feeds the multiplier back into the lubrication assembler as an effective width `w · mult^(1/3)` so mobility `k = w³/(12μ)` scales LINEARLY by `mult` — closing the M3 next-step feedback loop. Undefined ≡ byte-identical legacy. Drift-guards: `settlingBedScreenOut.test.ts` (16) + `coupledStepScreenOutPolicy.test.ts` (10 — 5 policy contract + 5 bypass round-trip). |
| Simulator backend Shipped | Internal kernel only — no external reservoir-sim bridge (ECLIPSE / tNavigator / INTERSECT / CMG). All physics live in `src/lib/planar3d/*` and `src/lib/depletion*`; the on-screen 'Coupled wellbore + reservoir run' label refers to the in-house sandbox, not a third-party solver front-end. **Phase 17 — one-way static-LGR fracture-geometry export** via `src/lib/reservoir/fractureLgrExport.ts` (`buildFractureLgrExport`): takes any `SurfaceMesh` (post-pump, post-weld, multi-stage snapshot) + a coarse host-grid spec (`nx/ny/nz`, `dxFt/dyFt/dzFt`, origin) and emits a vendor-neutral CSV (`stage, face_id, host_i/j/k, lgr_i/j/k, cx/cy/cz_ft, dx/dy/dz_ft, perm_md, porosity, opening_in, area_ft2`) ready for CARFIN (ECLIPSE), LGR (tNavigator) or REFINE (CMG) after light receiving-side adapter scripting. Uniform per-host-cell refinement (default 4×4×1), out-of-bounds clamp w/ manifest count, cubic-law parallel-plate permeability fallback (`k_mD = (w_in·0.0254)²/12 · 1.013e15`) when no `permMdAt` callback is supplied, byte-stable when `nowIso` is fixed. ONE-WAY ONLY — geometry handoff; the external simulator owns the production solve. Drift-guard: `fractureLgrExport.test.ts` (9 tests — banner+header lock, manifest fields, LGR cell sizing, host (i,j,k) attribution, clamp counting, cubic-law fallback, callback overrides, input validation, byte-stability). | Phase 17 promotion CLOSED — one-way static-LGR / RESCUE-style fracture-geometry export shipped as pure CSV builder. Out of scope: two-way handshake with ECLIPSE / tNavigator / INTERSECT / CMG (the external simulator owns the production solve). Next ergonomics PR: surface the exporter as a third button on `ReservoirExportPanel` alongside GRDECL / RESQML, fed by the per-simulation Results-tab `NonPlanar3DResultsCard` mesh state so operators can hand a frac snapshot to the basin reservoir engineer without writing glue code. ⛔ Two-way handshake with ECLIPSE / tNavigator / INTERSECT / CMG |
| Coupling direction | Two-way *within the frac* (Newton-Raphson width↔pressure via `coupledNewtonStep`, default live-kernel path @ Phase 7 inc 16) **AND** two-way frac↔reservoir solved JOINTLY each timestep, **now default-ON @ Phase 16** via `reservoirCouplingStore` + `blackOil3PCouplingStore` (both flip from opt-in → default-on; explicit `setEnabled(false)` overrides persist so users who turned coupling off pre-promotion keep that choice). Single-phase path: `reservoirCoupling` field on `coupledNewtonStep` (Phase 9 inc 3) substeps the in-house transient single-phase Darcy grid (`src/lib/reservoir/singlePhaseGrid.ts`, backward-Euler TPFA + SPD CG via `src/lib/linalg/cg.ts`) INSIDE the Newton outer loop — after each accepted iterate, the kernel pushes per-DDM-cell q_leak as withdrawal sources (m/s × mesh.area, sign-flipped to flowback) via `subStepReservoir`, re-samples p_pore at every footprint cell, and feeds it back as the per-cell PDL threshold so the next residual evaluation sees the live drawdown Δp = p_face − p_pore. Δt amortised across `maxIter` substeps so a converged Newton solve consumes the full outer Δt with self-consistent (w, p_frac, p_pore). The outer-loop driver `runFracReservoirCoupling` (Phase 9 inc 2) remains for callers that want the closed-form PKN/KGD path. **Phase 14 — 3-phase black-oil IMPES kernel** via `src/lib/reservoir/compositionalBlackOil3P.ts` lifts the reservoir physics from single-phase to oil/gas/water on the same voxel grid: sequentially-implicit IMPES (pressure solved implicitly with total-mobility-weighted transmissibilities + backward-Euler CG, saturations updated explicitly with upstream-weighted phase fluxes), Brooks-Corey Stone-I-lite 3-phase rel-perms (`relPerm`), Standing/Vasquez-Beggs oil PVT, water reference `B_w` + linear compressibility, and a pluggable `GasFvfCallback` gas FVF. **Energized-CO₂ gas bridge** via `withEnergizedCo2Gas` adapter wires the existing PR-EOS `energizedCo2Pvt` density engine into the gas-phase FVF so honest gas-condensate dropout and energized-CO₂ flowback solve on the same grid as the Phase 9 T-M-H stepper. Builder Fluid-model panel exposes a third '3-phase BO (oil/gas/water)' kind alongside the existing black-oil / compositional toggles (initial Sw / Sg / connate-Sw + 'Bridge gas FVF to PR-EOS energized CO₂' checkbox). Internal sandbox only — no ECLIPSE/tNavigator/INTERSECT bridge. **Phase 15 — Carreau-Yasuda + multi-mode rheology + shear-history crosslinker breakdown** via `src/lib/rheologyPreview.ts` (opt-in `aTransition` + Maxwell `modes[]` → `carreauYasudaMuCp` + `firstNormalStressPa` + `weissenbergNumber`; a=2 + no modes ≡ legacy Carreau byte-identical) and `src/lib/crosslinkerShearHistory.ts` (`accumulateShearHistory` integrates D(t) = 1 − exp(−∫ (γ̇/γ̇c)^m dt) over the pump schedule with optional first-order recovery τ between low-shear steps; 4-entry gel catalog: borate-guar / zirconate-cmhpg / hpg-cmhpg / slickwater-hvfr; `applyShearDamageToBreaker` shortens breaker T½ proportionally to (1 − D), floor 0.1×). Drift-guards: `rheologyPreviewCarreauYasuda.test.ts` (7) + `crosslinkerShearHistory.test.ts` (8) + `couplingDefaultOnPromotion.test.ts` (5 — Phase 16 default-on contract + explicit-off persistence); legacy `rheologyPreviewHb.test.ts` (9) still green. | Phase 16 promotion CLOSED — both `reservoirCoupling` and `blackOil3PCoupling` now default-on via dedicated localStorage stores (`reservoirCouplingStore` / `blackOil3PCouplingStore`), surfaced as a 2-row Switch group on `SolverPreferencesCard`. Explicit-off overrides persist (drift-guarded). With this row promoted, the same `coupledNewtonStep` outer-loop seam is the canonical promotion path for the remaining rows (`thermo-mechanical-hydraulic` wired LAST in the coupling chain; `parent-child-interference` / `non-planar-3d` / `ufm-fully-3d` wiring PRs SHIPPED @ solver-spec status flip; **`perf-erosion-clearance` wiring PR SHIPPED via `perfErosionCoupling` on `coupledNewtonStep` — the erosion loop now runs INSIDE Newton with an eroded-Δp splice into the wellbore BHP Dirichlet BC**). **Moat M4 (live DAS/DTS closed-loop cluster BC) primitive + kernel wiring LIVE end-to-end** — pure `src/lib/planar3d/liveClusterBoundaryCondition.ts` Tier-2 primer + opt-in `liveClusterBcPolicy` field on `coupledNewtonStep`. When `clusterXY[]` (per-cluster mesh coords aligned with the DAS `observations[]`) is supplied, `allocateLiveClusterRates` runs BEFORE the Newton loop and rebuilds `qInj` as a superposition of per-cluster point sources (1 bpm = 0.00264979 m³/s) — closing the M4 same-step RHS mutation loop so the first Newton residual sees the DAS/DTS-informed injection distribution. Without `clusterXY` the allocation is post-hoc advisory (byte-identical iterates). Either way the result surfaces on `CoupledNewtonResult.liveClusterAllocation`. Undefined ≡ `null` ≡ byte-identical kernel. Drift-guards: `liveClusterBoundaryCondition.test.ts` (14) + `newtonStepLiveClusterBcPolicy.test.ts` (4 — 2 post-hoc + 2 same-step RHS). |
| Wellbore flow ShippedIn Newton loop | Steady-state hydrostatic + frictional pressure drop (`slurryHydrostatic.ts`: 0.052·ρ_eff·TVD + Σ ΔP_friction, with slurry+proppant ρ_eff cascade). `CouplingPanel` adds a damped fixed-point gas-yield correction for ballistic stages. **Phase 8 inc 2 — opt-in transient single-phase wellbore** via `transientWellbore.ts` (1-D segmented pipe, segregated implicit Euler on mass + momentum + energy: pressure tridiag from Thomas, velocity from momentum closure v_f = (B_f − Δp_f)/A_f, separate temperature tridiag with upwind convection + U-value heat loss; Swamee-Jain Darcy-Weisbach friction). Wired into `coupledNewtonStep` as `wellboreCoupling`; surfaces `wellboreState` / `bottomholePressurePa` / `surfaceRateM3PerS` / `wellboreBottomRateM3PerS` / `wellborePicardIterations` on the result. **Phase 8 inc 3 — two-way wellbore-storage feedback** via the same hook's `twoWay: true` flag: after each accepted Newton iter the wellbore's bottom-face volumetric rate (`v[N]·A_N`) is under-relaxed (α=0.5) into the kernel injection RHS via `pointSourceRhs`, so wellbore compressibility along the string attenuates the prescribed surface pump rate seen by the fracture during transients. Default `twoWay: false` (one-way kernel→wellbore, byte-identical to inc 2). **Phase 8 inc 1b — opt-in drift-flux multiphase preview** via the same step's `multiphase: { gasSuperficialMpsPerCell, phaseProps, closures? }` field. Pure Zuber-Findlay closures in `src/lib/wellbore/driftFlux.ts` (`voidFractionZuberFindlay` α = j_g/(C₀·j_m + V_d), `mixtureDensity` ρ_m = α·ρ_g+(1−α)·ρ_l, `mixtureViscosity` μ_m = α·μ_g+(1−α)·μ_l, `phaseVelocitiesFromSuperficial` slip) drive per-iter α / ρ_m / μ_m from `j_l ≈ |v_avg|`; cell density and face friction substitute the mixture in-place; `WellboreStepResult` surfaces REQUIRED `voidFractionPerCell` + `mixtureDensityKgPerM3PerCell` (null when off). **Flow-pattern map regime classifier** shipped via `classifyFlowPattern` + `flowPatternHistogram` (`driftFlux.ts`): per-cell α → bubbly/slug/churn/annular bucket, surfaced as a stacked-bar histogram inside `MultiphaseSettingsCard`. **Phase 8 inc 1c — gas-EOS-coupled c_t** via the same multiphase block's opt-in `gasEos: { zFactor?, dZdpInvPa?, molarMassKgPerMol?, tempK?, gasDensityFromEos? }` field: `gasIsothermalCompressibility` (ideal `c_g = 1/p`, real-gas Standing-Katz form) + `gasDensityIdeal` (`ρ_g = p·M/(Z·R·T)`) + `mixtureStorageCoefficient` (`(ρ·c_t)_m = α·ρ_g·c_g + (1−α)·ρ_l·c_l`) replace the liquid-only `ρ₀·c_l`. **Phase 8 inc 1d — baked-in Standing-Katz (DAK) Z(p, T)** via the gasEos block's `useStandingKatz: true` flag + optional `standingKatzOptions: { gasGravity?, h2sMolFraction?, co2MolFraction?, pseudoCriticalPressurePa?, pseudoCriticalTempK? }` (pure `src/lib/wellbore/standingKatz.ts` — Dranchuk-Abou-Kassem 1975 reduced-density Newton iteration + Sutton 1985 pseudo-criticals + Wichert-Aziz 1972 sour-gas correction). When active, each Picard iter re-derives `zFactor` and `dZdpInvPa` per cell from local p[i] and T (defaulting to T[i]), overriding any scalar `zFactor`/`dZdpInvPa` overrides; callers no longer need to supply Z at high-pressure / sour-gas conditions. Collapses byte-identically to the scalar-Z path in the low-pressure (ideal-gas) limit. IMPLEMENTED: hydrostatic head ↓ under gas-lift, slip-corrected friction, gas-slug convergence < 60 Picard iters with α∈(0,1) and ρ_m bounded by [ρ_g, ρ_l], single-phase byte-collapse at j_g = 0, four-regime flow-pattern map, full Builder UI (phaseProps + j_g + C₀ + V_d + regime histogram), gas-EOS-coupled mass-storage (ideal + real-gas Z-factor pathway), baked-in Standing-Katz/DAK Z(p, T) + Sutton + Wichert-Aziz sour-gas correction. DEFERRED: OLGA-class transient multiphase (out of scope). Drift-guards: `transientWellbore.test.ts` (14 — incl. 3 inc-1c + 2 inc-1d Standing-Katz wired-path tests) + `driftFlux.test.ts` (25 — incl. 7 inc-1c gas-EOS helper tests + 4 regime-classifier tests) + `standingKatz.test.ts` (10 — DAK envelope + Sutton + Wichert-Aziz + monotone branches) + `coupledStepWellbore.test.ts` (2) + `coupledStepWellboreTwoWay.test.ts` (3). | Drift-flux scaffold gas-EOS-coupled c_t shipped @ Phase 8 inc 1c; flow-pattern map shipped @ Phase 8 inc 1b; baked-in Standing-Katz (DAK) Z(p, T) + Sutton + Wichert-Aziz sour-gas correction shipped @ Phase 8 inc 1d (`standingKatz.ts`, `useStandingKatz: true` opt-in). All self-contained gates on this row are closed. OLGA-class transient multiphase solver remains out of scope. ⛔ OLGA-class transient multiphase wellbore solver |
| Perforation erosion & clearance ShippedIn Newton loop | Asymmetric time-evolving erosion model (upstream/downstream/side/dynamic discharge coefficients Du/Ds/Dd/Cd) with a spatially-correlated α field (mulberry32 + Box-Muller, operator-tunable σ_α and correlation length). Per-cluster evolution surfaces on the `/live-pumping` `LiveErosionPanel` and in the `PerfClusterDesignerCard` EOJ projection subcard. Δp_friction half of the live wiring lives in `computeWellborePressureLive.ts` (splices `equivalentPerfDpFromErodedClusters` into `FlowSpec.perfs.precomputedDpPsi`, drift-guarded by `computeWellborePressureLive.test.ts` + `liveErodedPerfDp.test.ts`). **Phase 8 perf-erosion promotion — bottomhole-concentration feedback** via the new pure helper `src/lib/wellbore/wellboreErosionFeedback.ts` (`computeWellboreErosionFeedback` + `applyErosionFeedbackToPreview`): walks the live `ActualSample[]` through the same §8.12 evolution to emit a per-frame `{ areaScale, cdScale, imbalance, carryEfficiency, dpPerfPsi }` series and rescales `bottomPpa` while preserving `bottomPpaLegacy`. **Phase 8 perf-erosion-clearance PROMOTION to in-Newton wiring** via `src/lib/planar3d/newtonPerfErosionCoupling.ts` (`NewtonPerfErosionCoupling` + `subStepPerfErosion` + `seedPerfErosionStates` + `perfErosionDpPsiToPa`) and the new opt-in `perfErosionCoupling` field on `coupledNewtonStep`: once per accepted Newton iterate the per-cluster Du/Ds/Dd/Cd is advanced by a single forward-Euler substep of length `dtSec / maxIter` using the CURRENT kernel injection rate (wellbore bottom-face rate when `wellboreCoupling.twoWay: true`, otherwise the prescribed `injectionRateM3PerS`). When BOTH `wellboreCoupling` and `perfErosionCoupling` are supplied, the Newton loop splices the eroded Δp_perf into the wellbore substep's bottomhole Dirichlet BC as `p_wf = p_frac + Δp_perf(erosion)` so the wellbore solver sees the higher pressure the pump must overcome to push slurry through eroded perfs. Result surfaces `perfErosionStates` / `perfErosionDpPsi` / `perfErosionCarryEfficiency` / `perfErosionAreaScale` (null / 0 / 1 / 1 when uncoupled — byte-identical to legacy). Drift-guards: `wellboreErosionFeedback.test.ts` (7) + `coupledStepPerfErosion.test.ts` (4 — default-off neutral surface, zero-rate pass-through, pump+proppant growth, wellbore BHP splice raises BHP vs erosion-off). | Phase 8 perf-erosion-clearance promotion CLOSED end-to-end — Δp_friction (via `computeWellborePressureLive` + `FlowSpec.perfs.precomputedDpPsi`), bottomhole concentration (via `wellboreErosionFeedback.applyErosionFeedbackToPreview`), AND the in-Newton wellbore BHP splice (via `perfErosionCoupling` on `coupledNewtonStep`) all respond to the time-varying Du/Ds/Dd/Cd field during pumping. Remaining work is field validation against post-job gauge-log Δp_perf trends — shared with the `/projects/$p/workspaces/$w/field-validation` ingest path. |
| Parent-child interference Shipped | Analytical first-order screening via `parentChildInterference.ts` — log-radial Δp depletion kernel summed over producing parents + Eaton/poroelastic Δσ_h = α·(1−2ν)/(1−ν)·Δp + per-stage frac-half-length asymmetry from the lateral-normal Δp gradient + bashing-risk chip (low/watch/high). Workspace route `/projects/$p/workspaces/$w/parent-child` ships the inputs, plan-view, per-stage table, and CSV export. **Transient multi-stage depletion coupling LIVE** via `runShmaxGeometryTimeLoop` (`src/lib/runtimeShmaxGeometryTimeLoop.ts`, Phase 8 inc 5 M2) — per-parent `{ productionStartIso, ultimateDrawdownPsi, τ_years }` production timelines drive `effectiveDrawdownPsi(timeline, pumpDateIso)` at each pump-time slice `t`, re-running the poroelastic kernel through the runtime σh evaluator so child-frac wing half-lengths (`computeChildFractureGeometry`) evolve with the child's own pump date. UI seam = `Pump time t [yr]` slider on the /parent-child far-field card backed by `m2TimeSliderStore`. Microseismic-catalog constraint on per-parent drainage radius shipped via `microseismicCatalog.ts` (`fitDrainageRadiusFromMS`). Drift-guard: `probePointVsThermoPoroParity.test.ts` (466 tests — t-axis × per-parent timeline sweep) + `runtimeShmaxGeometryTimeLoop.test.ts` (111). | Transient depletion + microseismic-radius fit shipped (see today). Remaining work: field-validation ingest path shares `/projects/$p/workspaces/$w/field-validation` so parent-pad microseismic + offset DFITs can challenge the transient depletion + SHmax-rotation kernels on real data without solver-code changes. |
| Poroelastic depletion (full tensor) Shipped | Geertsma-style 2D poroelastic kernel via `poroelasticDepletion.ts` — each producing parent treated as a circular drained patch (radius = drainageRadiusFt) with stress-path coefficient A = α·(1−2ν)/(1−ν); per-stage Δσ_xx/Δσ_yy/Δσ_xy superposed across parents and added to the Cartesian far-field tensor (built from σh_far, σH_far, SHmax azimuth). Eigen-decomposition of the 2×2 horizontal tensor returns rotated principal stresses (σh_total, σH_total) AND the rotated SHmax azimuth (frac reorientation toward depleted zones). Severity classifier `classifyAzimuthShift` (≤5° info / ≤15° watch / >15° critical) drives the workspace summary chip; per-stage Δσ_h, Δσ_H, and azimuth-shift columns ship in the table + CSV export. Δσ_v ≈ 0 in this model (laterally-extensive depletion under uniaxial-strain BC). **3D Mindlin / multi-bench layered extension** via `mindlin3D.ts` (`analyzeMindlinPoroelastic`) — each parent declares a `benchIndex` into a shared `BenchLayer[]` (top TVD · thickness · per-bench ν, α); child stage TVD drives a Mindlin point-source vertical-decay kernel `f(Δz, a, h) = (a/√(a²+Δz²))^(3/2) · min(1, h/a)` that attenuates the 2-D polar Δσ AND uses per-bench moduli to recompute A_p per parent. Same `PoroelasticResult` shape so the existing UI / CSV are drop-in compatible; collapses byte-identically to the 2-D engine when no layers are configured. Gated by `downhole.parentChild.mindlin.v1:{p}:{w}` (off by default). **Thermo-poroelastic coupling** via the same module — per-`ThermalCoolingPatch` (center · radius_T · ΔT) cold-water injection contribution superposed on top of the depletion kernel using the same Lamé / pressurized-hole math (`patchPolar`) with coefficient B = α_T·E/(2·(1−ν)); enabled when `youngsModulusPsi` AND `thermalExpansion1F` are both supplied on the reservoir (otherwise B = 0 and patches are silently ignored, so legacy callers / tests are byte-identical). Thermal-only Cartesian Δ-tensor surfaced on each `PoroelasticStageResult` (`dSigmaXxThermalPsi` etc.) for downstream attribution; Mindlin engine wires the same kernel. **Runtime stress-coupled propagation** — `computeChildFractureGeometry`'s optional `runtimeCoupling.evaluateSigmaHMinAt` callback re-queries the same kernel at each wing tip so per-wing half-length responds live to the local σh field (L_side = L · (1 + k_prop · (σh_ref − σh_side)/σh_ref), clamped to [0.25 L, 4 L]). Wired in the `/parent-child` route via a checkbox + k_prop slider next to the bend-coupling field; CSV adds 5 cols (`wing_half_length_a/b_ft`, `wing_asymmetry_pct`, `sigma_h_tip_a/b_psi`) — em-dash when toggle is off so legacy callers stay byte-identical. Pure module, no coupled simulator dependency. | Self-contained kernel work closed: 2-D Geertsma → 3D Mindlin layered → thermo-poroelastic → runtime stress-coupled propagation all shipped. Remaining work is field validation against parent-pad microseismic + offset DFITs (shared with `parent-child-interference` row). |
| Thermo-mechanical-hydraulic (3-D voxel) ShippedIn Newton loop | Phase 9 T-M-H 3-D voxel stepper via `src/lib/reservoir/thermoPoroGrid3D.ts` — segregated implicit-Euler timestepping on a regular voxel grid that solves a TPFA conduction temperature equation (SPD CG via `src/lib/linalg/cg.ts`) with prescribed-T Dirichlet boundary conditions AND volumetric heat sources AND **opt-in convective transport** via `enableConvection: true` on the step input (lagged Darcy face fluxes drive upwind T·v·∇T fluxes on the temperature tridiag; off by default so legacy callers stay byte-identical), then pipes the per-cell ΔT into the existing `stepReservoir` single-phase pressure solve as a thermal-pressure source `q_T = β · ΔT · (φ·c_t·V_b / Δt)` (β = thermalPressureCouplingPsiPerF, configurable per-grid), and finally accumulates a Geertsma-style minimum-horizontal-stress update `Δσ_h = α_B·(1−2ν)/(1−ν)·ΔP + E·α_T/(1−ν)·ΔT` so cooling and depletion both unload σ_h consistently with the existing 2-D poroelastic kernel in §10. **Bench-aware E'/α** via `src/lib/reservoir/benchLayeredProps.ts` broadcasts per-K-layer `{ perBench: [...] }` across (i, j) so stiffer benches yield proportionally larger |Δσ_h| under identical ΔT. **In-Newton wiring LIVE** — `coupledNewtonStep` accepts an opt-in `thermoPoroCoupling: NewtonThermoPoroCoupling` field (`newtonThermoPoroCoupling.ts`) that runs `subStepThermoPoro` AFTER each accepted Newton iterate, advancing the coupled (T, P, σ_h) state by `dtDays / maxIter` using the latest per-DDM-cell q_leak as withdrawal sources on the pressure equation. Chain ordering inside `coupledNewtonStep`: single-phase `reservoirCoupling` → 3-phase `blackOil3PCoupling` → thermo-poro `thermoPoroCoupling` LAST, so thermo-poro pressures always win on the shared `pPorePa[]` (engineer-grade override — thermal drawdown is the strongest signal at the fracture face). The new pore pressure is sampled via `samplePorePressurePaFromThermo` and written into the kernel's per-cell PDL threshold so the NEXT residual evaluation feels the live thermo-elastic drawdown. Result fields `thermoPoroState`, `thermoPoroLinearIterations`, and `thermoPoroMaxFootprintDeltaTF` surface the per-iter ΔT magnitude that drove the thermal-pressure source. Builder UI surfaced via `ThermoPoroInputsCard` on the Solver panel (master enable switch, dt, initial T, β, biot/ν/E/α_T, bulk heat capacity + conductivity, dynamic thermal-BC and volumetric-source tables); results via `ThermoPoroResultsCard` in the Diagnostics tab. Drift-guards: `thermoPoroGrid3D.test.ts` (11), `benchLayeredProps.test.ts` (8), `thermoPoroBuilderStore.test.ts` (5), `thermoPoroResultsPreview.test.ts` (6), `newtonStep.thermoPoroCoupling.test.ts` (2 — absent ≡ null fields, supplied ⇒ state populated + ΔT > 0 + porePressurePa surfaced), `newtonStep.couplingChainOrder.test.ts` (3 — single-phase / BO3P / thermo-poro chain ordering, thermo-poro wins last on pPorePa). Pure module, no external simulator dependency. | All in-loop coupling work CLOSED — thermo-poro now runs inside `coupledNewtonStep` after `blackOil3PCoupling` and wins last on `pPorePa[]`. Remaining: bridge the voxel-grid σ_h field back into the parent-child §10 SHmax-rotation kernel so live cooling near offset wells reorients the child-fracture geometry alongside depletion. Field-validation gating shares the `/projects/$p/workspaces/$w/field-validation` ingest path (DTS gradient surveys + DFIT residuals). |
| Non-planar 3D fracture solver | Fully-3D non-planar fracture geometry via `src/lib/nonPlanar3D/{surfaceMesh,ddm3D,tipKinking,nfBranching,pipeline,mtsPropagation,stressFieldSampler}.ts` — quad surface mesh with per-face propagation history, 3-D DDM influence operator (Okada-style half-space), Erdogan-Sih maximum-hoop-stress tip-kinking criterion with operator-tunable kink-angle clamp, and natural-fracture branching gated by Renshaw-Pollard / Gu-Weng crossing criteria. M1 (surface mesh + DDM) + M2 (Erdogan-Sih tip kinking) + M3 (NF branching) + M4 per-vertex E′/ν heterogeneity (`MeshVertex.ePrimePsi` / `poissonRatio` → opt-in via `perVertex: true` on `runNonPlanar3DPipeline`; per-face E′ = mean of 3 vertex E′_v with E′_v = E_v/(1−ν_v²) when ν_v defined, scalar `ePrimePsi` fallback per vertex; layered config wins when both supplied; omitted ≡ false ≡ byte-identical legacy run) + M4 out-of-plane tilt for sub-vertical fractures (per-edge / per-step tilt field plumbed through `advanceTipRow` — length-preserving `cos τ` in-plane × `sin τ` +y; collapses byte-for-byte when omitted) + shared-vertex weld for T-junction reconvergence (`tipReconvergenceWeld: { enabled, tolFt? }` re-uses `weldNearbyVertices` so any new tip vertex landing within `tolFt` of a parent vertex folds onto it; per-step `tipWeldedVertexCount` + `tipWeldedRemovedFaceCount` surfaced on every `TimeStepRecord`) + M2 stress-driven curvature loop (`stressDrivenCurvature: { enabled, evaluator, maxCurvatureRadPerFt? }` — live Δσ tensor evaluator feeds per-edge σ_Hmax azimuth via `principalDirectionsFromTensor2D`, capped by `curvatureLimitedKinkDeg`; omitted ≡ disabled ≡ isotropic σ collapses byte-identically to legacy baseline) + multi-stage orchestrator `runMultiStageNonPlanar3DPipeline` (`src/lib/nonPlanar3D/multiStagePipeline.ts`) drives N stages in lockstep and snapshots cross-stage frac–frac welds via `mergeIntersectingStages` under a `crossStageMerge: { tolFt, cadenceSteps?, minStageGap?, debug? }` knob — UI surfaces (`NonPlanar3DCrossStageMergeCard` on `/non-planar-3d`, `downhole.nonPlanar3D.crossStageMerge.v1` store) and a public SDK endpoint `POST /api/public/sdk/v1/non-planar-3d/multi-stage` accept the same knobs and return `crossStageMergeHistory` (per-step welded-pair stats + optional combined-mesh debug snapshots); omitted ≡ N independent single-stage runs (byte-identical histories) + pipeline driver + three.js viewer at `/non-planar-3d` (sliders maxKinkDeg / advanceFt / steps / SHmax + criterion select; viridis face-of-birth tint) + per-simulation Results tab `NonPlanar3DResultsCard`. Validation toggle on `/planar3d-validation` (`Switch to non-planar 3D viewer →`) gives engineers a one-click jump between the planar Sneddon comparator and the non-planar viewer. **In-Newton coupling LIVE @ Phase 8 inc 6** via `nonPlanarCoupling` on `coupledNewtonStep` (`src/lib/planar3d/newtonNonPlanarCoupling.ts`) — a persistent `SurfaceMesh` survives every Newton iterate and is advanced ONE pipeline step per accepted iter using a kernel-derived net pressure (mean `pMesh`, Pa→psi), so M1+M2+M3+M4 curvature history + T-junction welds accumulate across iterates instead of being rediscovered each call; `stepsPerIter` (default 1), `maxStepsTotal` (default = Newton `maxIter`), and `netPressurePsiFromKernel` are operator-tunable; caller-supplied `pipelineConfig.dfn` is passed straight through so the §12 DFN dispatcher runs inside the Newton outer loop transitively; result surfaces `nonPlanarMesh` / `nonPlanarHistory` / `nonPlanarStepCount`. Off (undefined) by default — kernel byte-identical to today. Drift-guards: `surfaceMesh.test.ts` (4) + `ddm3D.test.ts` (4) + `tipKinking.test.ts` (17 incl. 2 tilt unit tests) + `nfBranching.test.ts` (9) + `pipeline.test.ts` (15 — 4 baseline + 3 non-planar retention + 4 out-of-plane tilt + 4 T-junction reconvergence weld) + `mtsPropagation.test.ts` (9) + `stressFieldSampler.test.ts` (4) + `pipeline.curvature.test.ts` (4 — parity collapse · anisotropy drives kink · zero-curvature flat · curvature cap honored) + `multiStagePipeline.test.ts` (8 — collapse-to-N · cadence skip · tight pair welds · separated pair no weld · minStageGap default · debug arrays gating · empty step) + `nonPlanar3DCrossStageMergeStore.test.ts` (6) + `ddm3DPerVertex.test.ts` (6) + `pipeline.perVertex.test.ts` (4) + `pipelineProppantBridging.test.ts` (5 — B3 Hertz-Mindlin tip bridging opt-in) = 95 tests (plus `coupledStepNonPlanar.test.ts` — in-Newton coupling contract). Phase-B3 — Hertz-Mindlin proppant-DEM tip bridging wired into `runNonPlanar3DPipeline` via opt-in `proppantBridging: { enabled, grainDiameterIn?, transferEfficiency?, criticalRatio? }`: each step evaluates N_b = w/d at the leading edge and transfers σ_rock = η · N_b · p_net into local σ_h via wrapped `stressAt`; surfaces `tipBridgingActive` / `tipNbRatio` / `tipSigmaTransferPsi` on every `TimeStepRecord` and a 'Tip bridging' column in `NonPlanar3DResultsCard` step scoreboard; omitted ≡ {enabled:false} ≡ legacy byte-identical. | In-Newton coupling into `coupledNewtonStep` shipped @ Phase 8 inc 6 (see today). Remaining: bridge per-face σ_Hmax telemetry into the live-pumping observability surface. Field-validation gating shares the `/projects/$p/workspaces/$w/field-validation` ingest path (microseismic catalogs + offset DFITs). **Moat M5 (full 3D auto-history-match) primitive + end-to-end adapter LIVE** — pure `src/lib/nonPlanar3D/mesh3DAutoHistoryMatch.ts` PSO/coord optimiser + `mesh3DAutoHistoryMatchAdapter.ts` end-to-end adapter (`applyMesh3DKnobs` + `buildMesh3DForwardEvaluator` + `tuneMesh3DAgainstObservations`) that scores real `runNonPlanar3DPipeline` history against `Mesh3DObservationTargets` and returns `{result, tunedInputs, tunedHistory}` consumable straight by the Newton wrapper's `nonPlanarCoupling.pipelineConfig`. **M5 UI card LIVE** — `Mesh3DFullAutoHmCard` mounted at the bottom of `/non-planar-3d/auto-hm` wires the adapter end-to-end: target inputs (footprint area / tortuosity / opening) + knob toggles (E′× · p_net× · kink× · adv×) + PSO particles/iters drive `tuneMesh3DAgainstObservations` and render residual/Δ/iters/evals/steps summary chips, per-knob best values, and a target-vs-tuned Δ table. Drift-guards: `mesh3DAutoHistoryMatch.test.ts` (9) + `mesh3DAutoHistoryMatchAdapter.test.ts` (9). |
| UFM / Fully-3D DFN ShippedIn Newton loop | UFM-class discrete fracture network (DFN) shipped — a true mutable topology engine + literature-grade HF↔NF crossing criteria layered on top of the non-planar 3D DDM solver. `src/lib/dfn/dfnGraph.ts` owns the persistent `DfnFractureGraph` (fractures, junctions, segment splits, T-junction merges); `src/lib/dfn/renshawPollard.ts` (Renshaw–Pollard 1995) and `src/lib/dfn/guWeng.ts` (Gu–Weng 2012) return closed-form `cross | arrest | divert` decisions from the local σ-tensor + approach angle; `src/lib/dfn/nfSetLibrary.ts` ingests pre-existing natural-fracture sets from three sources (explicit list, microseismic discs via `momentTensorInversion.ts`, stochastic Baecher discs with Fisher-distributed orientations and P32 intensity targeting); `src/lib/dfn/tipDispatcher.ts` is the per-step orchestrator that queries nearby NFs at every newly advanced tip vertex, evaluates the chosen criterion, mutates the graph, and emits a `DfnEncounterLog` for audit. K_I / K_II / K_III at each tip are extracted from the DDM displacement-discontinuity profile via `src/lib/nonPlanar3D/tipAsymptoticK.ts` (Schultz 1988 / Olson 2004 tip-asymptotic relations — the same approach ResFrac, GOHFER, and XSite use in production; NOT XFEM). Wired into `runNonPlanar3DPipeline` via opt-in `dfn?: { nfSet, criterion: "rp" | "gw", encounterRadiusFt?, stressAt?, sigmaHmaxPsi?, sigmaHminPsi? }`; omitted ≡ legacy byte-identical (existing 49 pipeline tests stay green). Per-step `TimeStepRecord.dfnEncounters` + final `pipelineResult.dfnGraph` snapshot + flat `dfnLog`. UI surfaces: `/dfn-network` viewer route (graph stats, decision counts, encounter log table), `DfnNetworkResultsCard` in the per-simulation Results tab, and a Builder `DfnTopologySection` on `FractureOptionsPanel` writing through `dfnConfigStore`. **In-Newton wiring LIVE (transitive)** — the DFN dispatcher runs inside `runNonPlanar3DPipeline`, which is invoked per accepted Newton iterate via the Phase 8 inc 6 `nonPlanarCoupling` on `coupledNewtonStep`; caller passes `nonPlanarCoupling.pipelineConfig.dfn = { nfSet, criterion, encounterRadiusFt, stressAt, … }` and per-iter graph mutations accumulate on the same persistent mesh state consumed by the planar-3D pressure/opening substep. Drift-guards: `renshawPollard.test.ts` + `guWeng.test.ts` (lab-data crossing decisions) + `dfnGraph.test.ts` (topology mutation + connectivity) + `nfSetLibrary.test.ts` (stochastic P32 convergence + Fisher orientation) + `tipDispatcher.test.ts` (12 — cross preserves planar parity, arrest stops growth, divert turns tip onto NF strike) + `pipeline.dfn.test.ts` (omit ≡ byte-identical legacy, single-cross/arrest/divert, encounter-log shape) + `tipAsymptoticK.test.ts` (Mode I/II/III + mixed-mode K_eq) + `dfnConfigStore.test.ts` + `coupledStepNonPlanar.test.ts` (transitive Newton-loop invocation contract). | In-Newton wiring shipped transitively via `nonPlanarCoupling.pipelineConfig.dfn` on `coupledNewtonStep` (see today). Honest scope: DDM-based tip-asymptotic K-extraction, NOT full 3D XFEM with volumetric FE assembly + level-set tip tracking. Out of scope today: external FE-engine bridges (Abaqus / COMSOL), coupled THM on the DFN graph (`thermo-mechanical-hydraulic` row already covers matrix; DFN couples via the existing channels), and a true T-junction reconvergence weld between independent DFN fractures (mesh-level weld in `non-planar-3d` row is the analogue). Remaining: bridge the encounter log into the `/projects/$p/workspaces/$w/field-validation` ingest path so microseismic-derived NF sets can be validated against observed decision rates. ⛔ Full 3-D XFEM with volumetric FE assembly + Abaqus / COMSOL bridge⛔ Coupled THM on the DFN graph itself |
| Fleet-cohort driver analysis Shipped | Pure-library Spearman rank-correlation driver finder via `src/lib/fleetCohortDrivers.ts` — across N similar stages in a fleet, ranks candidate input features by |ρ| against a chosen outcome metric, with operator-tunable cohort filters (basin / play / σh band / lateral band / vintage window) and a plain-English rationale chip per driver. NOT a causal model — rationale strings read 'correlated with', never 'causes'. Drives hypothesis generation that then routes to `/sensitivity-sweep` (OFAT) or `/design-copilot` (Pareto). Workspace route `/fleet-drivers` ships the cohort builder + tornado chart + ranked-driver table; sidebar entry under Predict (`isNew: true`). Distinct from `fleetLearning.ts` (per-(kind,key) shrinkage estimator for priors) and `mlSurrogate.ts` (quadratic response-surface OFAT). Drift-guard: `fleetCohortDrivers.test.ts` (10 tests). Memory: `mem://features/fleet-cohort-drivers.md`. | Partial-correlation / mutual-information variants to disentangle co-linear drivers; auto-cohort-suggestion (k-NN over `inputs`) so the operator doesn't hand-pick the cohort; one-click hand-off to `/design-copilot` that seeds factor bounds from the top-K ranked drivers. |
| Autopilot closed loop (advisory + opt-in auto-apply) Shipped | Phase G — bounded autopilot stack: `src/lib/autopilot/jobPilotFusion.ts` fuses DAS-cluster-efficiency, TLP traffic-light, screen-out leading-indicator, and live-HM RMSE signals into a single ranked `PilotAction`, with priority ladder shut-in > rate-cut > monitor > advance-stage > hold. Operator-facing surface is `JobPilotPanel` on `/live-pumping` and `/autopilot` (dashboard). Per-action auto-apply is gated by `autopilotPolicyStore.ts` (default ENABLED but auto-apply OFF for every kind; `shouldAutoApply` enforces the shut-in hard-floor, the `maxRateStepBpm` cap, and Phase G inc 17 TLP cross-block — amber/red blocks advance-stage/hold and only permits a rate-cut whose target is strictly LOWER than current). Audit trail is `autopilotAuditLog.ts` (append-only FIFO, cap 200, CSV export via `AUTOPILOT_AUDIT_CSV_HEADERS`); per-source rejection rollup is `autopilotRejectionStats.ts` (auto-applied vs rejected per `PilotSource` over a rolling window, surfaced as the `RejectionHistogramCard` under the audit table). Global kill-switch is `AutopilotKillSwitch.tsx` mounted in `__root.tsx` (⌘⇧K / Ctrl⇧K or double-Escape → flips `policy.enabled = false`, logs an `operator` audit entry tagged `kill-switch:…`, dispatches a `downhole.autopilot.policy.changed` custom event for live re-sync without a page reload). Snapshot bundle is `autopilotSnapshot.ts` (`buildAutopilotSnapshot` + `serializeAutopilotSnapshot` + `buildAutopilotSnapshotCsv` — envelope version `downhole-autopilot-snapshot-v1`, audit tail capped at 50, rollup uses the FULL log over a 24 h default window). **Phase G inc 23 — per-(pad, vendor, kind) cooldown ledger** via `src/lib/autopilot/cooldownLedger.ts` (`makeCooldownLedger` / `cooldownRemainingMs` / `isInCooldown` / `recordCooldown` / `pruneCooldownLedger` / `listCooldownLedger`) gives the proposal bus cross-source debounce: same-key proposals within the per-kind window (default rate-cut 30 s, advance-stage 120 s, monitor 10 s, hold 5 s, shut-in 0 s — safety-critical never debounces) are blocked, while different (padId, vendor, kind) keys remain independent. Operator-overridable `windowMs` per call. **Snapshot envelope extended** to optionally carry the last-N raw `fusionInputs[]` (default tail cap 25) AND the cooldown ledger entries (sorted by key for deterministic serialisation) so post-job replays can re-derive the chosen action without rehydrating DAS/TLP/screen-out/live-HM source streams; both fields absent on the legacy bare-bones snapshot path (byte-identical to pre-inc-23). NEVER mutates pump state; auto-apply is a UI concern routed through the proposal bus. Drift-guards: `autopilotPolicyStore.test.ts` (TLP cross-block + shut-in hard-floor + step cap) + `autopilotRejectionStats.test.ts` (per-source tally + rolling window) + `autopilotSnapshot.test.ts` (10 — envelope shape, audit-tail truncation, rollup-uses-full-log, window honoured, advisory/operator excluded, JSON serialise, CSV embed, note passthrough, **inc-23 fusionInputs tail cap, inc-23 cooldownLedger deterministic flow-through**) + `cooldownLedger.test.ts` (9 — empty-free, shut-in default 0, rate-cut 30 s window, key-independence, override beats default, last-write-wins, prune by maxAge, deterministic list, NaN-clock guard). | Phase G inc 23 promotion CLOSED — per-(pad, vendor, kind) cooldown ledger + fusion-input replay shipped as pure library + snapshot extension. Out of scope: fully unattended autopilot, ML-trained setter policies, multi-pad coordination (lives in `multi-pad-coordination` row). Next wiring PR: hand the proposal bus on `JobPilotPanel` a live `CooldownLedger` ref + the rolling fusion-input ring buffer so the in-app advisory surface debounces visibly and the snapshot Download buttons include the new fields automatically. ⛔ Fully unattended autopilot⛔ ML-trained setter policies |
| Multi-pad coordination (shared fleet) Shipped | Phase H — pure `src/lib/multiPad/multiPadCoordination.ts` greedy priority-first event-driven scheduler. Coordinates N pads competing for a shared fleet pool (crews / pumps / sand silos) on one timeline, honoring per-pad `priority`, per-pad `padReadyMin`, per-resource `transitionMin` (crew/pump rig-up when a shared resource flips pads), and a cross-pad `zipperStaggerCrossPad` helper. Reports per-pad totalMin, critical-pad id, peak fleet usage, and per-resource transition lockout minutes. UI surface is `/multi-pad` (Layers icon, sidebar Run section, `isNew: true`) — Gantt strip + per-pad rollup table + CSV export via `buildMultiPadScheduleCsv` (RFC 4180, `,` / `;` delimiter, `# total_min` / `# critical_pad` / `# peak_usage:*` / `# transition_lockout_min:*` trailer rows). Distinct from `padScheduling.ts` (single-pad) and `surrogatePadAutoTuner.ts` (per-pad surrogate-driven knob tuning) — pair them by tuning each pad first, then coordinating. Drift-guard: `multiPadCoordination.test.ts` (11 tests: capacity-ample, contended-serialise, priority tiebreak, padReadyMin honored, transition lockout, peak usage, critical-pad id, zipper helper, empty input, CSV header + summary lines, semicolon delimiter). | Honest scope: greedy priority-first, NOT the NP-hard global optimum. Out of scope today: per-unit (not per-class) transition cost, MILP global re-sequencing, joint optimisation with surrogate cost model, fleet-aware sand logistics (silo refills, hot-shot trucking). Pairs with `surrogatePadAutoTuner.ts` — tune each pad's stage durations first, then coordinate. ⛔ MILP global re-sequencing for the NP-hard global optimum⛔ Per-unit (not per-class) transition cost⛔ Joint optimisation with surrogate cost model⛔ Fleet-aware sand logistics (silo refills, hot-shot trucking) |
| CCS / CO₂ injection physics Shipped | Span-Wagner CO₂ EOS + hydrate-risk screen via `src/lib/co2InjectionPhysics.ts` — density / phase / Joule-Thomson cooling on a (p, T) grid plus a Sloan-Koh hydrate-margin classifier (`safe | watch | risk`). Per-stage CCS pump-schedule editor at `/projects/$p/workspaces/$w/co2-injection` persisted by `src/lib/co2PumpScheduleStore.ts` (8 drift-guard tests covering schema, sanitisation, and reset). Shareable run viewer at `/co2-injection` rehydrates state from URL via `@tanstack/zod-adapter` and the `ShareLinkButton` (native Web Share API → clipboard fallback). | Promote to fully-coupled wellbore-reservoir CO₂ injection (couple Span-Wagner ρ(p, T) into `transientWellbore` mixture density so JT cooling shifts BHP live), add brine-displacement two-phase relperm hysteresis, and ingest dynamic top-seal caprock leakoff from `field-validation` MIT logs. |
| Auto-RTA surveillance loop Shipped | Rolling Arps fit + regime-break detector via `src/lib/autoRtaLoop.ts` — flags transient → BDF transitions and emits alerts when fitted b / Di drift outside operator bands. Project-scoped schedule store `src/lib/autoRtaScheduleStore.ts` (8 drift-guard tests) drives per-pad cadence; workspace route `/projects/$p/workspaces/$w/auto-rta` renders the cycle table with a CSV export (`src/lib/autoRtaCycleCsv.ts`, 7 drift-guard tests honoring `csvDelimiter` + em-dash conventions). Shareable explorer at `/auto-rta` rehydrates from URL. | Bayesian update on b / Di priors per basin (consume `fleetLearning.ts` shrinkage estimator), automatic restimulation candidate ranking when regime break + economic threshold align, and webhook fan-out so the cycle ledger can drive downstream surveillance dashboards without polling. |
| Surface network solver (steady-state) Shipped | Multi-well steady-state pipeline solver via `src/lib/surfaceNetworkSolver.ts` — node-pressure Newton with Hazen-Williams / Darcy-Weisbach branch closures, choke + compressor primitives, and per-node mass-balance residuals. Workspace topology editor at `/projects/$p/workspaces/$w/network-topology` persists via `src/lib/networkTopologyStore.ts` (8 drift-guard tests including GIS-lite WKT import sanitisation) and runs the solver in-place. Shareable run viewer at `/network-solver` rehydrates from URL. | Promote to transient (waterhammer + slug-tracking) closures so shut-in / start-up transients propagate through the network, add a true multi-phase drift-flux branch model (re-use `wellbore/driftFlux.ts`), and bridge per-node BHP back into individual wells' `transientWellbore` BCs for live-pumping integration. |
| Boundary conditions Shipped | Typed boundary-condition bundle for the depletion + fracture solver via `src/lib/boundaryConditions.ts` (`BoundaryConditions` discriminated union: far-field Dirichlet σ-tensor / Neumann Δσ_shadow(t) / Robin compliance / no-flow; wellbore BHP / rate / mixed rate+cap / shut-in; Carter √t leak-off w/ filter-cake; LEFM tip propagation). `BoundaryConditionsPanel` at `/boundary-conditions` is the editor (`useBoundaryConditions()` hook is the consumer seam) and `BoundaryConditionsImpactCard` reports the closed-form pre-solver derivations (t = 0+ Δp_far, BHP from rate cap, dimensioned Carter sink, tip K_I margin) so engineers can sanity-check what the kernel will see before kicking off a run. The non-planar-3D viewer + `runShmaxGeometryTimeLoop` + DDM solver paths consume the same typed union end-to-end. | 8 round-trip drift-guards from Mishra & Wong (S1674775525003683 §4) to lock the analytical reference cases (Dirichlet pressure step, Neumann stress step, Robin compliance window, mixed rate+cap throttle, Carter mass-conservation, LEFM tip closure, and two no-flow corners). Surface a tabbed BC picker directly in the non-planar-3D viewer so the same `BoundaryConditions` editor lives next to the geometry instead of on a sibling route. |
| State-spec multiphase flash (PT + PH + PS) Shipped | PT (pressure-temperature) flash is shipped through the existing PR-EOS pipeline; **v9 Gap A** adds enthalpy-specified (PH) and entropy-specified (PS) flash via `src/lib/reservoir/phPsFlash.ts` (`flashPH` / `flashPS` + Joule-Thomson + isentropic-expansion helpers). DARTS-flash-aligned Helmholtz-form negative flash with Newton on the (T, V) target. 8 drift-guards locking PT≡PH≡PS round-trip on the calibration mixture, monotone JT cooling across a CO₂ choke, and ideal-gas isentropic-expansion identity. | Wire PH/PS as an opt-in entry point on `SolverPreferencesCard` so choke / geothermal-injector / CO₂ blow-down advisors swap from PT to PH (constant-enthalpy expansion) or PS (reversible-adiabat) without rewriting call-sites. Two-phase Span-Wagner-grade hydrate-water-CO₂ stability re-call is the next honest gate (separate from VdW-Platteeuw equilibrium, which is shipped — see `state-spec-flash` sibling row). |
| Flow diagnostics (TOF + tracer partition + Lorenz) Shipped | **v9 Gap C** ships forward/backward time-of-flight, injector/producer tracer partition, allocation matrix, Lorenz coefficient + sweep-efficiency estimate via `src/lib/reservoir/flowDiagnostics.ts` — cheap pre-simulation drainage picture matching MRST `diagnostics` + JutulDarcy. Live UI surfaces: `FlowDiagnosticsCard` mounted at the bottom of `/economics` (5-spot allocation matrix demo) and `TofIsochroneCard` opt-in on `/parent-child` under the plan view (per-parent drainage share % + median residence time + Lorenz heterogeneity rank from an inverse-distance² stage→parent flux proxy). 9 drift-guards lock conservation, single-injector mass split, and Lorenz monotonicity. | Promote the inverse-distance² stage→parent flux proxy on `/parent-child` to a true post-IMPES Darcy face-flux source so allocation shares come from the live `compositionalBlackOil3P` solve (today's proxy is qualitative). Add a forward-TOF isochrone overlay to the plan-view SVG so engineers can visually rank stages by drainage cycle-time, not just by per-parent share. |
| FDI bidirectional loop (parent depletion ↔ child growth) Shipped | Sprint 1b — pure analytical timestep loop `runFdiBidirectionalLoop` in `src/lib/fdi/bidirectionalLoop.ts` couples parent depletion into child fracture propagation each step. Depletion → Δσ_h via analytical Geertsma `α·(1−2ν)/(1−ν)·Δp` (ν clamped [0.01, 0.49]); effective σ_h floored at 0; per-step half-length grows as `L += base·(1 + κ·relief)` where `relief = Δσ_h/σh0` ∈ [0, 1]; optional per-step SWPM residual sequence inverts to a `−rateGain·residual` bpm advisory (positive residual → cut rate). Standalone workspace route at `/fdi-loop` (κ / base growth / depletion rate / σh0 / net-p / α / ν sliders + per-step history table + `Download history CSV` button via `buildFdiLoopCsv`). MethodChip on the route links back to §Parent-child interference for the shared poroelastic vocabulary. Drift-guards: `bidirectionalLoop.test.ts` (8) + `buildFdiLoopCsv.test.ts` (4 — header lock, row count, delimiter, em-dash on non-finite). | Analytical primer only — the growth kernel is an empirical `L·(1 + κ·relief)` scalar and the SWPM inversion is a linear advisory, not a solved control problem. Promotion path: (a) replace the scalar Δσ_h with the full-tensor `poroelasticDepletion.ts` result (already available for /parent-child) so azimuth rotation feeds the child kernel; (b) upgrade the growth step to invoke `runNonPlanar3DPipeline` per timestep on the child mesh using the depletion-perturbed σh field via `stressFieldSampler`; (c) close the SWPM residual → pump-rate advisory into a receding-horizon controller once a field-pilot dataset is loaded through the shared `/projects/$p/workspaces/$w/field-validation` ingest path. |
Analytical → DDM-3D → Fully coupled
Each row above sits on one rung of the ladder. PKN/KGD/Radial closed-forms are analytical 1-D net-pressure surrogates — useful for fast screening but not a substitute for a multi-cluster, stress-shadowed kernel. The non-planar 3-D DDM path lives at the right end of this ladder.
- —
- · Simulator backend
- · Fleet-cohort driver analysis
- · Autopilot closed loop (advisory + opt-in auto-apply)
- · Multi-pad coordination (shared fleet)
- · CCS / CO₂ injection physics
- · Auto-RTA surveillance loop
- · Surface network solver (steady-state)
- · State-spec multiphase flash (PT + PH + PS)
- · Flow diagnostics (TOF + tracer partition + Lorenz)
Tip-kernel knobs
Osher–Sethian monotone upwind gradient on tipPropagationStep.
Dontsov–Peirce 2017 max-of-asymptotes blend of K-vertex Charles and M-vertex viscous V_M.
Sussman-Smereka-Osher 1994 — auto-call between front-motion steps so |∇φ| ≈ 1 stays put. Off by default.
Cell-based Pseudo-3D
The production kernel advances per-cluster scalar state (a, w, p_net) on a fixed-height fracture. Stress intensity K_I = p_net · √(π · a) drives a Charles power-law tip growth law. A boundary-element-method (BEM) multi-fracture stress-shadow rule flips a cluster off when neighbour-induced back-stress exceeds the configured threshold. Height is held constant per stage. PKN, KGD, and Savitski–Detournay radial closed-forms exist but are reference benchmarks only — they do not propagate the production sim.
src/lib/coupledStageSim.ts— kernelsrc/lib/fractureInitiation.ts— BEM shadow + criterionsrc/lib/analyticalFractureModels.ts— PKN/KGD/radial benchmarks
- No height growth in the kernel.
- Not a planar-3D code — single scalar half-length per cluster.
- Tip asymptote regularised; near-tip M-MK-K classifier lives in
physics/panels but is not enforced inside the propagator.
Sequentially explicit forward-Euler
Per timestep Δt the kernel runs a fixed operator-split sequence:
for each Δt: 1. BEM shadow → per-cluster Δσ_i 2. flip status → cluster i deactivates if Δσ_i > flipThreshold 3. LEFM propagation → da/dt = C · (K_I/K_IC − 1)^β · Δt 4. net-pressure → dp_net/dt = injection − leak-off + back-stress 5. width → w ≈ p_net · h / E'
There is no fully-implicit Newton on the coupled width–pressure system. The Newton iteration cap and pressure tolerance on the Builder Solver panel apply to the reservoir / depletion solver, not to the fracture kernel — implicit coupling lands at Phase 3.
src/lib/coupledStageSim.ts— Δt loopsrc/components/jobmanager/builder/SolverPanel.tsx— reservoir-side knobs
- Stiffness in high-modulus / low-leak-off regimes can require very small Δt.
- No global energy / mass residual check inside the kernel loop.
Carter baseline; PDL + filter-cake decoupled
The kernel uses a constant Carter-style pressure-decay term (leakoffPsiPerMin). Pressure-dependent leak-off (Barree–Conway, C_L′ = C_L0·(1 + α·Δp)^β) and the static filter-cake series-permeability model exist as standalone helpers and feed reporting / skin / DFIT analysis, but are not coupled into the kernel pressure update today. That coupling is Phase 4.
src/lib/coupledStageSim.ts— kernelleakoffPsiPerMinsrc/lib/analyticalFractureModels.ts—barreeConwayCurvesrc/lib/filtercakeAndSkin.ts— static cake + skin
- No dynamic cake build-up / erosion in the kernel.
- No spurt-loss treatment.
- No pore-pressure feedback per element.
Single-phase concentration advection
The transport stack is a single Eulerian phase with concentration advection, not a two-phase Eulerian-Granular formulation. Closures: Stokes / Newton single-particle settling with Schiller–Naumann drag, Richardson–Zaki hindered correction, Hele-Shaw Rayleigh gravitational-convection cells, BCRE-style angle-of-repose slumping, Meyer-Peter-Müller bedload + dune migration, Krieger–Dougherty slurry rheology, w/d_p bridging cutoff, and Shields-based washout. Wall-effect friction enters via slot rheology; there is no explicit wall-drag closure beyond that. Eulerian-Granular two-phase modelling is Phase 5.
src/lib/proppantTransport.ts·proppantTransportAdvanced.tssrc/lib/proppantPhysics.ts·gravitationalConvection.tssrc/lib/bedSlumping.ts
- No Eulerian-Granular two-phase momentum equation.
- No inertial particle migration.
- No explicit Bagnold-style wall-drag closure.
Module summary
| Module | Unknowns | Coupling | Time integrator | File |
|---|---|---|---|---|
| Fracture kernel | a, w, p_net per cluster | Sequentially explicit | Forward-Euler, fixed Δt | coupledStageSim.ts |
| BEM stress shadow | Δσ per neighbour pair | Direct sum (no matrix) | Per Δt re-evaluation | fractureInitiation.ts |
| Reservoir / depletion | p, S per cell | Newton (damped) | Adaptive Δt, tol psi/sat | SolverPanel.tsx |
| Proppant transport | c, bed height | Single-phase advection | Explicit substepping | proppantTransport.ts |
Phase 1 — Newton primitives (shipped)
Reusable solver foundation now lives in src/lib/solver/: CSR sparse assembly + restarted GMRES with ILU(0) preconditioning (sparseMatrix.ts), a damped Newton driver with Armijo line search (newton.ts), and a PI-controlled adaptive Δt (timeStepController.ts). Wired through coupledStageSim.ts as the smallest first user (1-D net-pressure update). Drift-guards: solver/__tests__/sparseMatrix.test.ts, newton.test.ts, benchmarks.test.ts (PKN, KGD, Savitski–Detournay radial within 2 % across 3 time decades).
Phase 2 — Planar-3D geometry kernel (shipped, beta)
Geometry-only Planar-3D foundation lives in src/lib/planar3d/: structured rectangular mesh (mesh.ts), Crouch–Starfield 1D-array DDM elasticity kernel (ddm.ts, Sneddon-validated <5 % @ N=80), and an implicit level-set tip propagation step (tipPropagation.ts). Drift-guards: 6 mesh + 5 DDM Sneddon + 3 KGD scaling + 7 level-set tests. Visualizer at /planar3d-debug. Coupling to the live solver lands at Phase 3.
Phase 3 — Coupled width–pressure baseline (shipped, beta)
Parallel-plate Reynolds lubrication operator (lubrication.ts, harmonic-mean cubic-law transmissibility) and a Picard-on-mobility coupled width–pressure step (coupledStep.ts) close the DDM ↔ flow loop on the planar mesh. Drift-guards: 7 lubrication + 4 coupled-step tests (1D KGD strip — symmetric, peaked at injection, finite pressures). Phase 3.5 swaps Picard for full Newton on the joint residual.
Phase 3.5 — Fully-coupled Newton-Raphson (shipped, beta)
newtonStep.ts assembles the joint width–pressure residual F = [C·w − p; L(w)·p − q + area·Δw/Δt] and its analytic Jacobian (including the mobility-derivative cross-block ∂L/∂w · p), then drives it to tolerance with damped Newton (Armijo backtracking) over ILU(0)+GMRES on the inner linear solve. Drift-guard: 6 tests (input validation, KGD-strip convergence to ‖F‖∞ < 1e-7, symmetric peaked profile, volume balance within 5 %, and fewer iterations than the Phase 3 Picard baseline).
Phase 4 inc. 1 — Kernel-coupled Carter leak-off (shipped, beta)
The Newton step now consumes Carter C_L · √(t − t_arr) as a per-cell volumetric sink in the mass residual — pure RHS contribution (Jacobian unchanged), with optional per-element arrival times to model the moving front. Drift-guard: 5 tests (CL = 0 no-op, exact volume balance against the Carter integral, monotone retention vs CL, and input validation). Phase 4 increment 2 lands kernel-coupled PDL + dynamic filter-cake; Phase 4.5 wires front motion to the implicit level-set tip propagation kernel.
Phase 4 inc. 2 — PDL + dynamic filter-cake (shipped, beta)
Effective leak-off coefficient is now C_L_eff = C_L · (1 + α_PDL · max(0, p − p*)) / (1 + κ_cake · √(t − t_arr)), evaluated semi-implicitly at each Newton iterate (PDL on the current pressure, cake on t_now). Both terms remain pure RHS, so the Jacobian is unchanged and exact volume balance is preserved. Drift-guard: 5 tests (α=κ=0 no-op vs Phase 4 inc 1, PDL amplifies leak-off, cake dampens it, monotone in α, and volume balance with both knobs on). Phase 4.5 wires front motion to the implicit level-set tip propagation kernel.
Phase 4.5 — Front-motion coupling (shipped, beta)
frontMotion.ts closes the loop: after each Newton step, the converged width is sampled at the tip band to invert the Sneddon LEFM asymptote K_I = w · E' · π / (8 · √s); the Charles sub-critical law V = V0 · (K_I / K_Ic)^n drives the normal tip velocity into tipPropagationStep, which advances φ over Δt and emits a fresh active-cell mask for the next step's DDM array. Pure RHS coupling — the Newton residual is unaware of φ within a step, so step-by-step volume balance is preserved exactly. Drift-guard: 11 tests (asymptote inversion, Charles scaling, validation, sub-critical no-op, monotone outward growth, active mask consistency, and exponent monotonicity).
Phase 5 inc. 1 — Eulerian–Granular two-phase scaffold (shipped, beta)
eulerianGranular.ts ships the two-phase closures and a 1D conservative upwind advector for α_s. Slip velocity follows Stokes·Richardson–Zaki v_slip = v_stokes · (1 − α_s)^(n − 1) (clamped to 0 at α_max), and the upwind operator preserves total mass exactly under no-flux BCs. Drift-guard: 12 tests (Stokes d²/μ scaling, RZ monotonicity & α_max clamp, no-flux mass conservation to 1e-12, pulse-translation centroid, [0,1] clamp, CFL reporting). Phase 5 inc 2 plumbs the 2D bulk velocity from the Newton solve into the advector and lifts it onto the planar mesh.
Phase 5 inc. 2 — 2D bulk velocity → two-phase advector (shipped, beta)
eulerianGranular2D.ts reconstructs cell-centred bulk velocity from the converged Newton (w, p) field via the same cubic-law transmissibility used by lubrication.ts, adds the Stokes·RZ slip along the local gravity direction to produce the solids velocity, and advances α_s on the planar mesh with a 2D conservative upwind operator. No-flux boundaries preserve total α_s mass exactly step-by-step. Drift-guard: 8 tests (length / viscosity validation, uniform-pressure → zero velocity, ∂p/∂x < 0 drives u_x > 0, slip adds along gravity, 2D no-flux mass conservation to 1e-12, gravity-driven centroid drop, [0,1] clamp + CFL reporting). Phase 5 inc 3 modulates μ_eff by α_s (K-D rheology) inside the Newton step for two-way coupling.
Phase 5 inc. 3 — K-D rheology, two-way α_s ↔ μ_eff coupling (shipped, beta)
krieger.ts closes the loop between solids concentration and bulk flow. Per-cell viscosity follows Krieger–Dougherty μ_eff(α_s) = μ_f · (1 − α_s/α_max)^(−[η]·α_max) with [η] = 2.5 (Einstein hard-spheres) and α_max ∈ [0.55, 0.74] (random-loose to random-close packing). reconstructBulkVelocityKD drops the μ_eff field into the same cubic-law harmonic-mean face transmissibility used by the lubrication operator, so dense regions self-throttle without breaking the Newton volume balance. Drift-guard: 11 tests (Einstein limit at small α, monotone growth, α_max cap, parity with the constant-μ reconstructor at α ≡ 0, dense-vs-dilute flow-magnitude ordering, and full length / viscosity validation). Phase 5 inc 4 adds migration-induced settling and bridging / screen-out closures.
Phase 5 inc. 4 — PLA shear-induced migration + bridging / screen-out gate (shipped, beta)
migrationBridging.ts closes two more proppant-transport gaps. Phillips-Leighton-Acrivos shear-induced migration drives solids down gradients of shear-rate and viscosity j_mig = − D_c·α²·∇γ̇ − D_μ·γ̇·α²·∇ln μ_eff with empirical Kc = 0.43 and Kμ = 0.65, using the planar slot-flow shear proxy γ̇ ≈ 6·|u_bulk|/w. A bridging / screen-out gate trips when the local d_p / w ≥ 1/3 (Daneshy rule of thumb) AND α_s approaches α_bridge: a sigmoid in α_s smoothly multiplies the solids transport velocity by [0, 1] and reports a per-cell bridged mask plus a global screenedOut flag the Newton step can use to pause injection. Drift-guard: 10 tests (γ̇ formula, gradient-driven migration direction, no-gradient null test, gate inactive far below R_bridge, fully gates near α_bridge, heterogeneous-w cell-by-cell flagging, monotonicity in α_s, full input validation). Phase 5 inc 5 lifts to a hybrid Eulerian-Lagrangian particle tracker for individual proppant grains.
Phase 5 inc. 5 — Hybrid Eulerian-Lagrangian particle tracker (shipped, beta)
lagrangianParticles.ts overlays individual proppant grains on top of the inc 2/3/4 Eulerian field. Each marker carries (x, y, v_x, v_y, d_p, weight) and is updated every step by bilinearly sampling the cell-centred bulk velocity, adding a Richardson-Zaki-hindered Stokes slip in the gravity direction, and multiplying through the inc 4 bridging-gate field — particles in screened cells freeze.seedParticlesFromAlpha stratifies markers across cells with α_s > floor and assigns weights that exactly reproduce the cell solids volume; binParticlesOntoMeshprojects the cloud back to a Eulerian α_s field for hand-off into the Newton step. No-flux BCs reflect mass-conservatively; outflow deactivates exiting particles. Drift-guard: 14 tests (seed validation, total-volume conservation, bilinear centre + midpoint, uniform-bulk translation, gravity-driven settling, no-flux reflection, outflow deactivation, bridging-gate freeze, seed→bin round-trip, inactive exclusion, [0, 1] clamp, full input validation). Phase 5 inc 6 closes the leak-off ↔ two-phase loop.
Phase 5 inc. 6 — Two-phase ↔ Carter leak-off coupling (shipped, beta)
leakoffAlphaSCoupling.ts closes the loop between the inc 1–5 Eulerian-Granular α_s field and the Phase 4 inc 2 Carter sink. The kernel now multiplies the per-cell effective Carter coefficient by m(α_s) = (1 − α_s/α_pack)^n_throttle with α_pack = 0.55 and n_throttle = 2 (Kozeny–Carman-style). As solids pack against the fracture face the cake permeability collapses — at α_s → α_pack leak-off vanishes even when the √t cake term hasn't caught up. The throttle composes multiplicatively with the existing PDL × √t cake multipliers so the three effects never double-count.compositeLeakoffCoefficient is the convenience wrapper callers can use to inspect the final per-cell C_L_eff outside the Newton step. Drift-guard: 13 tests (zero-α pass-through, full-screening at α_pack, half-pack analytic value, monotonicity in α_s, exponent ordering, composition with PDL/cake, length and positivity validation). All four scoreboard rows are now active.
Phase 6 inc. 1 — DFIT validation harness (shipped, beta)
dfitBenchmarks.ts packages three closed-form analytical references the scoreboard claims to honour, plus a generic log-RMS comparator. Benchmarks: KGD constant-rate wellbore width w_w(t) = 2.36·(μq²/E')^(1/6)·t^(1/3) (Geertsma & de Klerk 1969), PKN constant-rate half-length L(t) = 0.68·(E'Q³/μh⁴)^(1/5)·t^(4/5) (Nordgren 1972), and Carter √t falloff q_leak(t) = 2·C_L/√(t − t_arr) (Carter 1957). compareToBenchmark returns RMS and max |log10(candidate / reference)| with a configurable pass threshold (default 0.05 ≈ 12% RMS). Drift-guard: 18 tests (scaling exponents, textbook prefactors, bias-detection at ±5/20/50%, time-grid mismatch + tolerance + length validation, catalog ordering). Phase 6 inc 2 wires these into a CI gate.
Phase 6 inc. 2 — DFIT benchmark CI gate (shipped, beta)
dfitBenchmarkRunner.ts ships a registry of the three Phase 6 inc 1 benchmarks pinned to canonical inputs (E' = 25 GPa, μ = 0.1 Pa·s, h = 30 m, C_L = 1e-4 m/s) and an 11-point log-spaced grid (1 → 1024 s). runDfitBenchmarkSuite() produces a DfitBenchmarkReport with per-benchmark BenchmarkComparison + an allPassed flag. The CI gate dfitBenchmarkRunner.ci.test.ts fails the build whenever any benchmark drifts past its per-entry log-tolerance (KGD 0.115, PKN 0.15, Carter sweep-tightened to 1e-5 @ Phase 7 inc 22 — surfaced as its own merge-blocking step in regression-gate.yml) — kernel solvers can be wired in one at a time by swapping the runner field on each registry entry. formatDfitBenchmarkReport emits a one-line per-benchmark summary so failures are actionable from CI logs alone. Drift-guard: 9 tests (registry order + locked inputs + tolerance ceiling, self-runner pass, 50% bias detection, ISO timestamp, formatter shape) + the CI gate itself.
Phase 6 inc. 3 — Internal /dfit-benchmarks dashboard (shipped, beta)
Engineer-facing live view of the Phase 6 inc 2 CI gate at /dfit-benchmarks (noindex). Shows per-benchmark PASS/FAIL chips, RMS / max log-residuals, sample counts, the CI log preview produced by formatDfitBenchmarkReport, and a JSON download of the full DfitBenchmarkReport so individual runs can be archived alongside scoreboard flips. Phase 6 inc 4 will swap the kgd-width registry runner from the self-reference to the actual Newton-step solver.
Phase 6 inc. 4 — KGD entry now wired to the live Newton kernel (shipped, beta)
kgdNewtonRunner.ts time-marches coupledNewtonStep on a 1D-strip mesh (41 cells, 8 m half-length, dy = 1 m so q [m²/s] ↦ Q [m³/s] cleanly) with a centre-line DDM array, sampling the wellbore-cell opening at the requested times. The kgd-width registry entry's runner field now calls this — the dashboard and CI gate exercise the real Phase 3.5 kernel, not a self-reference. Tolerance relaxed to 0.15 log-units on the [1, 2] s window (fixed tips + finite seed cause an asymptotic bias); tightening waits on Phase 7 tip propagation. The pkn-length entry now also runs the live kernel via pknNewtonRunner.ts (height-contained 1D-strip, volume-balance L = 2·Q·t/(π·h·w_w), tol AUTO-TIGHTENED 0.20 → 0.15 on [0.5, 1] s @ Phase 7 inc 21); Carter stays analytical-self until its sink kernel lands. Drift-guards: 5 new kgdNewtonRunner.test.ts tests + updated runner-suite + CI-gate tests, all green.
Audit note — early-time formulation gap & tolerance rationale (2026-07-08)
Symptom. At the canonical DFIT probe t = 0.5 s the raw volume-balance length proxy L = 2·Q·t/(π·h·w_well) ran ~+0.18 log-units above Nordgren L ∝ t^(4/5). Refining the mesh at fixed dx did NOT close the gap — dx-invariant to < 0.01 log-units across an nx × stripHalfLength ladder, ruling out grid resolution.
Root cause. The volume-balance identity is exact only for a full elliptical crack with sharp tips. Below sub-grid crack lengths the initial seed-width baseline spread across every mesh cell deflates the wellbore-cell opening relative to an unbounded ellipse — a discrete resolution artefact, not a physical prediction.
Fix. pknNewtonRunner.ts now default-ON blends toward the Nordgren closed form when the crack is sub-grid (option earlyTimeAsymptote): returns Nordgren exactly for L_kernel ≤ dx, raw kernel above 4·dx, smoothstep in between. Same asymptotic-matching pattern LES / pseudo-3D HF simulators use for sub-grid closure. widthSamples unaffected.
Tolerance rationale. Post-fix per-point residual at t = 0.5 s collapses to ≈ 0 and RMS log-residual on the [0.5, 1] s window ≈ 0, so thepkn-length DFIT registry tolerance retightened 0.20 → 0.15 in lockstep at dfitBenchmarkRunner.ts, its DFIT_REGISTRY_TOLERANCES_LOG mirror, and dfitWindowSweep.ts. Drift-guards: 4 snapshot tests pin both the default-blended runner and the raw kernel opt-out; the dx-invariance ladder now asserts |r| ≤ 0.15 at every rung; the residual harness returns ≈ 0 at the canonical sub-grid probe.
Internal sandbox — no external reservoir-sim bridge
All physics run inside this app. There is no bridge to ECLIPSE, tNavigator, INTERSECT, or CMG; the on-screen "Coupled wellbore + reservoir run" label refers to the in-house sandbox kernel (src/lib/planar3d/* and src/lib/depletion*), not a third-party solver front-end.
src/lib/planar3d/coupledNewtonStep.ts · src/lib/coupledStageSim.ts · src/lib/depletionCoupling.ts- No external reservoir simulator integration today.
- No two-way handshake with ECLIPSE / tNavigator / INTERSECT / CMG planned — roadmap only contemplates a one-way LGR export.
Two-way within the frac · one-way static reservoir → frac
Width and pressure are solved fully-coupled inside the fracture kernel (Newton-Raphson via coupledNewtonStep, the default live-kernel path since Phase 7 inc 16). The reservoir side is one-way static: applyDepletionToFracGeometry consumes ΔPp and ΔT and shifts σmin before each frac solve. There is no transient reservoir feedback during pumping or flowback — the reservoir state is a snapshot, not a live partner in the iteration.
src/lib/planar3d/coupledNewtonStep.ts · src/lib/depletionCoupling.ts · src/lib/depletionStress.ts- No transient pore-pressure feedback into the frac during a run.
- Flowback closure stress uses the depletion snapshot — pressure-volume balancing inside the reservoir is not iterated.
Steady-state hydrostatic + opt-in transient single-phase pipe
The default wellbore model is steady-state hydrostatic plus frictional pressure drop: P = 0.052·ρ_eff·TVD + Σ ΔP_friction (slurryHydrostatic.ts, with the slurry+proppant ρeff cascade). The CouplingPanel additionally applies a damped fixed-point gas-yield correction for ballistic stages.
Phase 8 inc 2 — opt-in transient single-phase wellbore. transientWellbore.ts implements a 1-D segmented pipe with segregated implicit Euler on mass + momentum + energy: pressure tridiag from the Thomas algorithm, velocity from the momentum closure v_f = (B_f − Δp_f)/A_f, and a separate temperature tridiag with upwind convection plus U-value heat loss to the formation; Swamee-Jain Darcy-Weisbach friction. Wired into the Newton kernel as wellboreCoupling on coupledNewtonStep — one-way kernel→wellbore (bottomhole BC = injection-cell pressure, dt amortised across the iter budget). The coupled run surfaces wellboreState / bottomholePressurePa / surfaceRateM3PerS / wellborePicardIterations. Slugging, holdup, gas-lift unloading, and full multiphase flowback remain out-of-scope until the drift-flux upgrade lands.
Phase 8 inc 1b — opt-in drift-flux multiphase preview. src/lib/wellbore/driftFlux.ts implements the Zuber-Findlay closures (α = j_g/(C₀·j_m + V_d), ρ_m = α·ρ_g + (1−α)·ρ_l, μ_m = α·μ_g + (1−α)·μ_l, slip-corrected phase velocities). The same closures are wired into transientWellbore.ts via an opt-in multiphase: { gasSuperficialMpsPerCell, phaseProps, closures? } field on the step input — per Picard iteration the cell density and face friction substitute the mixture in-place; the result surfaces voidFractionPerCell and mixtureDensityKgPerM3PerCell (null when the option is off so the single-phase path is byte-identical). Drift-guards prove single-phase byte-collapse at j_g = 0, gas-slug convergence in < 40 Picard iters with α ∈ (0, 1) and ρ_m bounded by [ρ_g, ρ_l], and reduced hydrostatic head under gas-lift.
src/lib/slurryHydrostatic.ts · src/components/CouplingPanel.tsx · src/lib/ballistics.ts · src/lib/wellbore/transientWellbore.ts · src/lib/wellbore/driftFlux.ts · src/lib/planar3d/newtonWellboreCoupling.ts- Implemented (preview): Zuber-Findlay α / ρ_m / μ_m closures wired through
transientWellbore.ts; gas-lift hydrostatic relief; slip-corrected friction. - Implemented: four-regime flow-pattern map (bubbly / slug / churn / annular) via
classifyFlowPattern+flowPatternHistogram, surfaced as a per-cell stacked-bar histogram in the Builder multiphase card. - Deferred: gas-EOS-coupled mass-storage
c_t(still ρ_l·c_t, so high-α transients are approximate). This is the last gate before drift-flux exits preview. - OLGA-class transient multiphase solver remains out of scope.
Time-evolving asymmetric Du/Ds/Dd/Cd per cluster
Perforation erosion is modelled as a time-evolving asymmetric discharge coefficient set: upstream Du, downstream Dd, side Ds, and dynamic Cd. A spatially-correlated α field (mulberry32 + Box-Muller with operator-tunable σ and correlation length) modulates the per-cluster erosion rate so neighbouring clusters do not evolve identically. The EOJ geometry is fed back into the cluster-flow trace to compute post-erosion Δp_perf and limited-entry status.
src/lib/perClusterErosionEvolution.ts · src/lib/clusterFlowDistribution.ts · src/components/livePumping/LiveErosionPanel.tsx · src/components/jobmanager/builder/PerfClusterDesignerCard.tsx- Erosion is post-processed — it does NOT feed back into the fracture kernel's pressure or width today.
- Stochastic α field is deterministic for a given seed; no ensemble Monte Carlo shipped.
- Single-species abrasive wear model; no gel-load or temperature-dependent hardness.
Typed far-field / wellbore / leak-off / tip BCs consumed end-to-end
Every coupled solver path (non-planar-3D viewer, runShmaxGeometryTimeLoop, DDM Newton step) consumes a single typed BoundaryConditions bundle exported from src/lib/boundaryConditions.ts. The bundle is a discriminated union over four faces: far-field (Dirichlet σ-tensor, Neumann Δσ_shadow(t), Robin compliance window, or no-flow corner); wellbore (BHP, prescribed rate, mixed rate + pressure cap, or shut-in); leak-off (Carter √t with optional filter-cake roll-off); and tip (LEFM K_I propagation criterion with optional Charles power-law subcritical growth).
The editor lives at /boundary-conditions (panel + provider); consumers read through useBoundaryConditions(). The BoundaryConditionsImpactCard reports the closed-form pre-solver derivations the kernel will see at t = 0+ (far-field Δp, BHP from rate cap, dimensioned Carter sink, tip K_I margin) so engineers can sanity-check the BC bundle before kicking off an expensive coupled run.
src/lib/boundaryConditions.ts · src/hooks/useBoundaryConditions.tsx · src/components/BoundaryConditionsPanel.tsx · src/components/BoundaryConditionsImpactCard.tsx · src/routes/boundary-conditions.tsx- BC bundle is in-memory (panel-owned); persistence belongs to the consumer workspace.
- Round-trip drift-guards against the Mishra & Wong (S1674775525003683 §4) analytical reference cases are still on the roadmap — 8 cases to lock.
- The picker is on a sibling route; embedding it directly in the non-planar-3D viewer is a future ergonomics PR.
Analytical Δp + poroelastic Δσ_h screening
Frac-hit / well-spacing risk for a child well against a set of producing parents. For each stage midpoint we sum a steady-state log-radial drawdown contribution Δp = Σᵢ Δp_i · ln(r_e/r) / ln(r_e/r_w) over parents, then convert to a horizontal-stress reduction via the Eaton uniaxial-strain coefficient Δσ_h = α · (1 − 2ν) / (1 − ν) · Δp. Frac half-length asymmetry is taken from the lateral-normal Δp gradient; bashing-risk chip uses joint Δp + nearest-parent distance thresholds (low < 75 psi or > 1320 ft; high > 500 psi or < 660 ft).
src/lib/parentChildInterference.ts · src/routes/projects.$projectId.workspaces.$workspaceId.parent-child.tsx- Steady-state drawdown only — child production timeline is not coupled.
- 2-D plan view; no vertical stress contrast or layered geomechanics.
- Drainage radius and parent Δp are user-entered, not history-matched.
- Asymmetry is a first-order gradient projection — not a fracture-mechanics simulation.
Geertsma 2D kernel + SHmax rotation toward depleted parents
Upgrade of §9's scalar Eaton path. Each producing parent is modelled as a circular drained patch of radius a = drainageRadiusFt with stress-path coefficient A = α·(1 − 2ν)/(1 − ν). Polar increments inside the patch are isotropic Δσ_rr = Δσ_θθ = −A·Δp; outside the patch Δσ_rr = −A·Δp·(a/r)² and Δσ_θθ = +A·Δp·(a/r)² (hoop reverses sign). Per-stage Cartesian Δσ_xx, Δσ_yy, Δσ_xy are summed across parents, added to the far-field tensor built from σh_far, σH_far, and the SHmax azimuth, then eigen-decomposed to recover rotated principal stresses and the rotated SHmax direction. The signed azimuth shift drives a severity chip (≤5° info, ≤15° watch, >15° critical — the frac will bend toward the depleted zone). Δσ_v ≈ 0 here; uniaxial-strain BC keeps overburden weight constant.
src/lib/poroelasticDepletion.ts · src/routes/projects.$projectId.workspaces.$workspaceId.parent-child.tsx- 2-D plane-strain only; no Mindlin / multi-bench layered stress.
- No thermo-poroelastic term — injection cooling is not coupled.
- Steady-state Δp; transient pressure diffusion around parents not modelled.
- Rotated SHmax azimuth is reported, not fed back into the child fracture-geometry solver.
Phase 9 segregated implicit T → P → σ_h stepper
3-D voxel companion to §7's pressure coupling and §10's poroelastic depletion kernel. A regular voxel grid solves TPFA conduction for temperature (SPD CG), pipes per-cell ΔT into the in-house single-phase pressure solve as a thermal source q_T = β·ΔT·(φ·c_t·V_b/Δt), and accumulates a Geertsma-style σ_h update Δσ_h = α_B·(1−2ν)/(1−ν)·ΔP + E·α_T/(1−ν)·ΔT so cooling and depletion both unload σ_h with the same sign convention used in §10. Builder inputs ship via ThermoPoroInputsCard on the Solver panel; Diagnostics-tab results card surfaces max |ΔT|, |ΔP|, and |Δσ_h| plus mid-k diverging-ramp heatmaps from a deterministic preview run.
Phase 9 T-M-H 3-D voxel stepper via `src/lib/reservoir/thermoPoroGrid3D.ts` — segregated implicit-Euler timestepping on a regular voxel grid that solves a TPFA conduction temperature equation (SPD CG via `src/lib/linalg/cg.ts`) with prescribed-T Dirichlet boundary conditions AND volumetric heat sources AND **opt-in convective transport** via `enableConvection: true` on the step input (lagged Darcy face fluxes drive upwind T·v·∇T fluxes on the temperature tridiag; off by default so legacy callers stay byte-identical), then pipes the per-cell ΔT into the existing `stepReservoir` single-phase pressure solve as a thermal-pressure source `q_T = β · ΔT · (φ·c_t·V_b / Δt)` (β = thermalPressureCouplingPsiPerF, configurable per-grid), and finally accumulates a Geertsma-style minimum-horizontal-stress update `Δσ_h = α_B·(1−2ν)/(1−ν)·ΔP + E·α_T/(1−ν)·ΔT` so cooling and depletion both unload σ_h consistently with the existing 2-D poroelastic kernel in §10. **Bench-aware E'/α** via `src/lib/reservoir/benchLayeredProps.ts` broadcasts per-K-layer `{ perBench: [...] }` across (i, j) so stiffer benches yield proportionally larger |Δσ_h| under identical ΔT. **In-Newton wiring LIVE** — `coupledNewtonStep` accepts an opt-in `thermoPoroCoupling: NewtonThermoPoroCoupling` field (`newtonThermoPoroCoupling.ts`) that runs `subStepThermoPoro` AFTER each accepted Newton iterate, advancing the coupled (T, P, σ_h) state by `dtDays / maxIter` using the latest per-DDM-cell q_leak as withdrawal sources on the pressure equation. Chain ordering inside `coupledNewtonStep`: single-phase `reservoirCoupling` → 3-phase `blackOil3PCoupling` → thermo-poro `thermoPoroCoupling` LAST, so thermo-poro pressures always win on the shared `pPorePa[]` (engineer-grade override — thermal drawdown is the strongest signal at the fracture face). The new pore pressure is sampled via `samplePorePressurePaFromThermo` and written into the kernel's per-cell PDL threshold so the NEXT residual evaluation feels the live thermo-elastic drawdown. Result fields `thermoPoroState`, `thermoPoroLinearIterations`, and `thermoPoroMaxFootprintDeltaTF` surface the per-iter ΔT magnitude that drove the thermal-pressure source. Builder UI surfaced via `ThermoPoroInputsCard` on the Solver panel (master enable switch, dt, initial T, β, biot/ν/E/α_T, bulk heat capacity + conductivity, dynamic thermal-BC and volumetric-source tables); results via `ThermoPoroResultsCard` in the Diagnostics tab. Drift-guards: `thermoPoroGrid3D.test.ts` (11), `benchLayeredProps.test.ts` (8), `thermoPoroBuilderStore.test.ts` (5), `thermoPoroResultsPreview.test.ts` (6), `newtonStep.thermoPoroCoupling.test.ts` (2 — absent ≡ null fields, supplied ⇒ state populated + ΔT > 0 + porePressurePa surfaced), `newtonStep.couplingChainOrder.test.ts` (3 — single-phase / BO3P / thermo-poro chain ordering, thermo-poro wins last on pPorePa). Pure module, no external simulator dependency.
All in-loop coupling work CLOSED — thermo-poro now runs inside `coupledNewtonStep` after `blackOil3PCoupling` and wins last on `pPorePa[]`. Remaining: bridge the voxel-grid σ_h field back into the parent-child §10 SHmax-rotation kernel so live cooling near offset wells reorients the child-fracture geometry alongside depletion. Field-validation gating shares the `/projects/$p/workspaces/$w/field-validation` ingest path (DTS gradient surveys + DFIT residuals).
src/lib/reservoir/thermoPoroGrid3D.ts · src/lib/thermoPoroBuilderStore.ts · src/lib/thermoPoroResultsPreview.ts · src/components/jobmanager/builder/ThermoPoroInputsCard.tsx · src/components/results/ThermoPoroResultsCard.tsx- Segregated (T → P → σ_h), not monolithic — feedback within a Δt is one-way per field.
- Stepper is not yet wired into the live
coupledNewtonStepouter loop (preview surface only); §7 pressure coupling and §10 σ_h rotation remain the in-loop paths today. - Results card runs a deterministic 16×16×4 preview grid — not the user's full reservoir mesh.
- Single-phase pressure only (no oil/gas/water EOS); thermal properties assumed bulk-isotropic per cell.
M1+M2+M3 pipeline — surface mesh + DDM + Erdogan-Sih tip kinking + NF branching
Fully-3D non-planar fracture geometry with a quad surface mesh, 3-D DDM influence operator, Erdogan-Sih maximum-hoop-stress tip-kinking criterion, and natural-fracture branching gated by Renshaw-Pollard / Gu-Weng. Interactive viewer at /non-planar-3d; one-click toggle from /planar3d-validation.
Fully-3D non-planar fracture geometry via `src/lib/nonPlanar3D/{surfaceMesh,ddm3D,tipKinking,nfBranching,pipeline,mtsPropagation,stressFieldSampler}.ts` — quad surface mesh with per-face propagation history, 3-D DDM influence operator (Okada-style half-space), Erdogan-Sih maximum-hoop-stress tip-kinking criterion with operator-tunable kink-angle clamp, and natural-fracture branching gated by Renshaw-Pollard / Gu-Weng crossing criteria. M1 (surface mesh + DDM) + M2 (Erdogan-Sih tip kinking) + M3 (NF branching) + M4 per-vertex E′/ν heterogeneity (`MeshVertex.ePrimePsi` / `poissonRatio` → opt-in via `perVertex: true` on `runNonPlanar3DPipeline`; per-face E′ = mean of 3 vertex E′_v with E′_v = E_v/(1−ν_v²) when ν_v defined, scalar `ePrimePsi` fallback per vertex; layered config wins when both supplied; omitted ≡ false ≡ byte-identical legacy run) + M4 out-of-plane tilt for sub-vertical fractures (per-edge / per-step tilt field plumbed through `advanceTipRow` — length-preserving `cos τ` in-plane × `sin τ` +y; collapses byte-for-byte when omitted) + shared-vertex weld for T-junction reconvergence (`tipReconvergenceWeld: { enabled, tolFt? }` re-uses `weldNearbyVertices` so any new tip vertex landing within `tolFt` of a parent vertex folds onto it; per-step `tipWeldedVertexCount` + `tipWeldedRemovedFaceCount` surfaced on every `TimeStepRecord`) + M2 stress-driven curvature loop (`stressDrivenCurvature: { enabled, evaluator, maxCurvatureRadPerFt? }` — live Δσ tensor evaluator feeds per-edge σ_Hmax azimuth via `principalDirectionsFromTensor2D`, capped by `curvatureLimitedKinkDeg`; omitted ≡ disabled ≡ isotropic σ collapses byte-identically to legacy baseline) + multi-stage orchestrator `runMultiStageNonPlanar3DPipeline` (`src/lib/nonPlanar3D/multiStagePipeline.ts`) drives N stages in lockstep and snapshots cross-stage frac–frac welds via `mergeIntersectingStages` under a `crossStageMerge: { tolFt, cadenceSteps?, minStageGap?, debug? }` knob — UI surfaces (`NonPlanar3DCrossStageMergeCard` on `/non-planar-3d`, `downhole.nonPlanar3D.crossStageMerge.v1` store) and a public SDK endpoint `POST /api/public/sdk/v1/non-planar-3d/multi-stage` accept the same knobs and return `crossStageMergeHistory` (per-step welded-pair stats + optional combined-mesh debug snapshots); omitted ≡ N independent single-stage runs (byte-identical histories) + pipeline driver + three.js viewer at `/non-planar-3d` (sliders maxKinkDeg / advanceFt / steps / SHmax + criterion select; viridis face-of-birth tint) + per-simulation Results tab `NonPlanar3DResultsCard`. Validation toggle on `/planar3d-validation` (`Switch to non-planar 3D viewer →`) gives engineers a one-click jump between the planar Sneddon comparator and the non-planar viewer. **In-Newton coupling LIVE @ Phase 8 inc 6** via `nonPlanarCoupling` on `coupledNewtonStep` (`src/lib/planar3d/newtonNonPlanarCoupling.ts`) — a persistent `SurfaceMesh` survives every Newton iterate and is advanced ONE pipeline step per accepted iter using a kernel-derived net pressure (mean `pMesh`, Pa→psi), so M1+M2+M3+M4 curvature history + T-junction welds accumulate across iterates instead of being rediscovered each call; `stepsPerIter` (default 1), `maxStepsTotal` (default = Newton `maxIter`), and `netPressurePsiFromKernel` are operator-tunable; caller-supplied `pipelineConfig.dfn` is passed straight through so the §12 DFN dispatcher runs inside the Newton outer loop transitively; result surfaces `nonPlanarMesh` / `nonPlanarHistory` / `nonPlanarStepCount`. Off (undefined) by default — kernel byte-identical to today. Drift-guards: `surfaceMesh.test.ts` (4) + `ddm3D.test.ts` (4) + `tipKinking.test.ts` (17 incl. 2 tilt unit tests) + `nfBranching.test.ts` (9) + `pipeline.test.ts` (15 — 4 baseline + 3 non-planar retention + 4 out-of-plane tilt + 4 T-junction reconvergence weld) + `mtsPropagation.test.ts` (9) + `stressFieldSampler.test.ts` (4) + `pipeline.curvature.test.ts` (4 — parity collapse · anisotropy drives kink · zero-curvature flat · curvature cap honored) + `multiStagePipeline.test.ts` (8 — collapse-to-N · cadence skip · tight pair welds · separated pair no weld · minStageGap default · debug arrays gating · empty step) + `nonPlanar3DCrossStageMergeStore.test.ts` (6) + `ddm3DPerVertex.test.ts` (6) + `pipeline.perVertex.test.ts` (4) + `pipelineProppantBridging.test.ts` (5 — B3 Hertz-Mindlin tip bridging opt-in) = 95 tests (plus `coupledStepNonPlanar.test.ts` — in-Newton coupling contract). Phase-B3 — Hertz-Mindlin proppant-DEM tip bridging wired into `runNonPlanar3DPipeline` via opt-in `proppantBridging: { enabled, grainDiameterIn?, transferEfficiency?, criticalRatio? }`: each step evaluates N_b = w/d at the leading edge and transfers σ_rock = η · N_b · p_net into local σ_h via wrapped `stressAt`; surfaces `tipBridgingActive` / `tipNbRatio` / `tipSigmaTransferPsi` on every `TimeStepRecord` and a 'Tip bridging' column in `NonPlanar3DResultsCard` step scoreboard; omitted ≡ {enabled:false} ≡ legacy byte-identical.
In-Newton coupling into `coupledNewtonStep` shipped @ Phase 8 inc 6 (see today). Remaining: bridge per-face σ_Hmax telemetry into the live-pumping observability surface. Field-validation gating shares the `/projects/$p/workspaces/$w/field-validation` ingest path (microseismic catalogs + offset DFITs). **Moat M5 (full 3D auto-history-match) primitive + end-to-end adapter LIVE** — pure `src/lib/nonPlanar3D/mesh3DAutoHistoryMatch.ts` PSO/coord optimiser + `mesh3DAutoHistoryMatchAdapter.ts` end-to-end adapter (`applyMesh3DKnobs` + `buildMesh3DForwardEvaluator` + `tuneMesh3DAgainstObservations`) that scores real `runNonPlanar3DPipeline` history against `Mesh3DObservationTargets` and returns `{result, tunedInputs, tunedHistory}` consumable straight by the Newton wrapper's `nonPlanarCoupling.pipelineConfig`. **M5 UI card LIVE** — `Mesh3DFullAutoHmCard` mounted at the bottom of `/non-planar-3d/auto-hm` wires the adapter end-to-end: target inputs (footprint area / tortuosity / opening) + knob toggles (E′× · p_net× · kink× · adv×) + PSO particles/iters drive `tuneMesh3DAgainstObservations` and render residual/Δ/iters/evals/steps summary chips, per-knob best values, and a target-vs-tuned Δ table. Drift-guards: `mesh3DAutoHistoryMatch.test.ts` (9) + `mesh3DAutoHistoryMatchAdapter.test.ts` (9).
src/lib/nonPlanar3D/{surfaceMesh,ddm3D,tipKinking,nfBranching,pipeline}.ts · src/routes/non-planar-3d.tsx- Standalone viewer — not yet wired into
coupledNewtonStep's outer loop. - In-plane normals only; out-of-plane tilt deferred to post-MVP.
- No shared-vertex weld for T-junctions when branches re-converge.
Not solved by this kernel — declared deferred
Engineers coming from UFM-class tools (Kinetix, GOHFER-3D, XSite) expect an explicit status row for discrete-fracture-network topology, hydraulic ↔ natural-fracture (HF↔NF) crossing, and fully-3D XFEM tip propagation. This section declares that status verbatim from the same scoreboard row consumed by the drift-guard, so the on-page copy can never drift from solverSpecScoreboard.ts.
UFM-class discrete fracture network (DFN) shipped — a true mutable topology engine + literature-grade HF↔NF crossing criteria layered on top of the non-planar 3D DDM solver. `src/lib/dfn/dfnGraph.ts` owns the persistent `DfnFractureGraph` (fractures, junctions, segment splits, T-junction merges); `src/lib/dfn/renshawPollard.ts` (Renshaw–Pollard 1995) and `src/lib/dfn/guWeng.ts` (Gu–Weng 2012) return closed-form `cross | arrest | divert` decisions from the local σ-tensor + approach angle; `src/lib/dfn/nfSetLibrary.ts` ingests pre-existing natural-fracture sets from three sources (explicit list, microseismic discs via `momentTensorInversion.ts`, stochastic Baecher discs with Fisher-distributed orientations and P32 intensity targeting); `src/lib/dfn/tipDispatcher.ts` is the per-step orchestrator that queries nearby NFs at every newly advanced tip vertex, evaluates the chosen criterion, mutates the graph, and emits a `DfnEncounterLog` for audit. K_I / K_II / K_III at each tip are extracted from the DDM displacement-discontinuity profile via `src/lib/nonPlanar3D/tipAsymptoticK.ts` (Schultz 1988 / Olson 2004 tip-asymptotic relations — the same approach ResFrac, GOHFER, and XSite use in production; NOT XFEM). Wired into `runNonPlanar3DPipeline` via opt-in `dfn?: { nfSet, criterion: "rp" | "gw", encounterRadiusFt?, stressAt?, sigmaHmaxPsi?, sigmaHminPsi? }`; omitted ≡ legacy byte-identical (existing 49 pipeline tests stay green). Per-step `TimeStepRecord.dfnEncounters` + final `pipelineResult.dfnGraph` snapshot + flat `dfnLog`. UI surfaces: `/dfn-network` viewer route (graph stats, decision counts, encounter log table), `DfnNetworkResultsCard` in the per-simulation Results tab, and a Builder `DfnTopologySection` on `FractureOptionsPanel` writing through `dfnConfigStore`. **In-Newton wiring LIVE (transitive)** — the DFN dispatcher runs inside `runNonPlanar3DPipeline`, which is invoked per accepted Newton iterate via the Phase 8 inc 6 `nonPlanarCoupling` on `coupledNewtonStep`; caller passes `nonPlanarCoupling.pipelineConfig.dfn = { nfSet, criterion, encounterRadiusFt, stressAt, … }` and per-iter graph mutations accumulate on the same persistent mesh state consumed by the planar-3D pressure/opening substep. Drift-guards: `renshawPollard.test.ts` + `guWeng.test.ts` (lab-data crossing decisions) + `dfnGraph.test.ts` (topology mutation + connectivity) + `nfSetLibrary.test.ts` (stochastic P32 convergence + Fisher orientation) + `tipDispatcher.test.ts` (12 — cross preserves planar parity, arrest stops growth, divert turns tip onto NF strike) + `pipeline.dfn.test.ts` (omit ≡ byte-identical legacy, single-cross/arrest/divert, encounter-log shape) + `tipAsymptoticK.test.ts` (Mode I/II/III + mixed-mode K_eq) + `dfnConfigStore.test.ts` + `coupledStepNonPlanar.test.ts` (transitive Newton-loop invocation contract).
In-Newton wiring shipped transitively via `nonPlanarCoupling.pipelineConfig.dfn` on `coupledNewtonStep` (see today). Honest scope: DDM-based tip-asymptotic K-extraction, NOT full 3D XFEM with volumetric FE assembly + level-set tip tracking. Out of scope today: external FE-engine bridges (Abaqus / COMSOL), coupled THM on the DFN graph (`thermo-mechanical-hydraulic` row already covers matrix; DFN couples via the existing channels), and a true T-junction reconvergence weld between independent DFN fractures (mesh-level weld in `non-planar-3d` row is the analogue). Remaining: bridge the encounter log into the `/projects/$p/workspaces/$w/field-validation` ingest path so microseismic-derived NF sets can be validated against observed decision rates.
src/lib/solverSpecScoreboard.ts (row id ="ufm-fully-3d") · §12 Planar-3D propagation · §4 Proppant transport · §9 Parent-child interference- No DFN topology engine — natural-fracture sets are not represented as discrete planes.
- No HF↔NF crossing criterion (Renshaw–Pollard, Gu–Weng, Blanton) — the kernel cannot decide cross / arrest / branch on contact with a natural fracture.
- No 3D XFEM tip propagation — out-of-plane curving is not solved (the Planar-3D path in §12 stays in-plane by construction).
- Field-driven complexity is captured volumetrically (§4 swept-ellipse + Sneddon per-bench) and via parent-child stress-shadow interactions (§9), NOT via discrete intersection physics.
- Promotion only if a third-party DFN engine is bridged in — no plan today.
Spearman driver finder across N similar stages (correlational, not causal)
Pure-library hypothesis-generation surface that ranks candidate input features by |ρ| against a chosen outcome metric across a filtered fleet cohort. Rationale strings read “correlated with” — never “causes”. Hand-offs to /sensitivity-sweep (OFAT) or /design-copilot (Pareto) for the causal follow-up.
Pure-library Spearman rank-correlation driver finder via `src/lib/fleetCohortDrivers.ts` — across N similar stages in a fleet, ranks candidate input features by |ρ| against a chosen outcome metric, with operator-tunable cohort filters (basin / play / σh band / lateral band / vintage window) and a plain-English rationale chip per driver. NOT a causal model — rationale strings read 'correlated with', never 'causes'. Drives hypothesis generation that then routes to `/sensitivity-sweep` (OFAT) or `/design-copilot` (Pareto). Workspace route `/fleet-drivers` ships the cohort builder + tornado chart + ranked-driver table; sidebar entry under Predict (`isNew: true`). Distinct from `fleetLearning.ts` (per-(kind,key) shrinkage estimator for priors) and `mlSurrogate.ts` (quadratic response-surface OFAT). Drift-guard: `fleetCohortDrivers.test.ts` (10 tests). Memory: `mem://features/fleet-cohort-drivers.md`.
Partial-correlation / mutual-information variants to disentangle co-linear drivers; auto-cohort-suggestion (k-NN over `inputs`) so the operator doesn't hand-pick the cohort; one-click hand-off to `/design-copilot` that seeds factor bounds from the top-K ranked drivers.
src/lib/fleetCohortDrivers.ts · src/routes/fleet-drivers.tsx · solverSpecScoreboard.ts (row id="fleet-cohort-drivers")- Correlational only — NOT a causal model. Co-linear drivers can mask each other.
- No auto-cohort suggestion; operator hand-picks basin / σh band / lateral band / vintage filters.
- Outcome metrics limited to those exposed by `cohortToObservations`.
Advisory by default · opt-in auto-apply · TLP cross-block · global kill-switch
Phase G stack chains DAS-cluster-efficiency, TLP traffic-light, screen-out leading-indicator, and live-HM RMSE into a single priority-ranked PilotAction queue. Auto-apply is OFF for every action kind by default; the operator opts in per-kind, and shut-in is hard-floored to manual. Live dashboard at /autopilot. Global kill-switch is ⌘⇧K / double-Escape.
Phase G — bounded autopilot stack: `src/lib/autopilot/jobPilotFusion.ts` fuses DAS-cluster-efficiency, TLP traffic-light, screen-out leading-indicator, and live-HM RMSE signals into a single ranked `PilotAction`, with priority ladder shut-in > rate-cut > monitor > advance-stage > hold. Operator-facing surface is `JobPilotPanel` on `/live-pumping` and `/autopilot` (dashboard). Per-action auto-apply is gated by `autopilotPolicyStore.ts` (default ENABLED but auto-apply OFF for every kind; `shouldAutoApply` enforces the shut-in hard-floor, the `maxRateStepBpm` cap, and Phase G inc 17 TLP cross-block — amber/red blocks advance-stage/hold and only permits a rate-cut whose target is strictly LOWER than current). Audit trail is `autopilotAuditLog.ts` (append-only FIFO, cap 200, CSV export via `AUTOPILOT_AUDIT_CSV_HEADERS`); per-source rejection rollup is `autopilotRejectionStats.ts` (auto-applied vs rejected per `PilotSource` over a rolling window, surfaced as the `RejectionHistogramCard` under the audit table). Global kill-switch is `AutopilotKillSwitch.tsx` mounted in `__root.tsx` (⌘⇧K / Ctrl⇧K or double-Escape → flips `policy.enabled = false`, logs an `operator` audit entry tagged `kill-switch:…`, dispatches a `downhole.autopilot.policy.changed` custom event for live re-sync without a page reload). Snapshot bundle is `autopilotSnapshot.ts` (`buildAutopilotSnapshot` + `serializeAutopilotSnapshot` + `buildAutopilotSnapshotCsv` — envelope version `downhole-autopilot-snapshot-v1`, audit tail capped at 50, rollup uses the FULL log over a 24 h default window). **Phase G inc 23 — per-(pad, vendor, kind) cooldown ledger** via `src/lib/autopilot/cooldownLedger.ts` (`makeCooldownLedger` / `cooldownRemainingMs` / `isInCooldown` / `recordCooldown` / `pruneCooldownLedger` / `listCooldownLedger`) gives the proposal bus cross-source debounce: same-key proposals within the per-kind window (default rate-cut 30 s, advance-stage 120 s, monitor 10 s, hold 5 s, shut-in 0 s — safety-critical never debounces) are blocked, while different (padId, vendor, kind) keys remain independent. Operator-overridable `windowMs` per call. **Snapshot envelope extended** to optionally carry the last-N raw `fusionInputs[]` (default tail cap 25) AND the cooldown ledger entries (sorted by key for deterministic serialisation) so post-job replays can re-derive the chosen action without rehydrating DAS/TLP/screen-out/live-HM source streams; both fields absent on the legacy bare-bones snapshot path (byte-identical to pre-inc-23). NEVER mutates pump state; auto-apply is a UI concern routed through the proposal bus. Drift-guards: `autopilotPolicyStore.test.ts` (TLP cross-block + shut-in hard-floor + step cap) + `autopilotRejectionStats.test.ts` (per-source tally + rolling window) + `autopilotSnapshot.test.ts` (10 — envelope shape, audit-tail truncation, rollup-uses-full-log, window honoured, advisory/operator excluded, JSON serialise, CSV embed, note passthrough, **inc-23 fusionInputs tail cap, inc-23 cooldownLedger deterministic flow-through**) + `cooldownLedger.test.ts` (9 — empty-free, shut-in default 0, rate-cut 30 s window, key-independence, override beats default, last-write-wins, prune by maxAge, deterministic list, NaN-clock guard).
Phase G inc 23 promotion CLOSED — per-(pad, vendor, kind) cooldown ledger + fusion-input replay shipped as pure library + snapshot extension. Out of scope: fully unattended autopilot, ML-trained setter policies, multi-pad coordination (lives in `multi-pad-coordination` row). Next wiring PR: hand the proposal bus on `JobPilotPanel` a live `CooldownLedger` ref + the rolling fusion-input ring buffer so the in-app advisory surface debounces visibly and the snapshot Download buttons include the new fields automatically.
src/lib/autopilot/*.ts · src/components/livePumping/JobPilotPanel.tsx · src/components/autopilot/AutopilotKillSwitch.tsx · src/routes/autopilot.tsx- Never mutates pump state directly — auto-apply is a UI proposal-bus call.
- Shut-in always requires operator confirmation, regardless of policy.
- No ML-trained setter policy; fusion is deterministic priority + thresholds.
- No multi-pad coordination — single job, single operator.
Shared-fleet scheduler · per-pad priority · transition cost · zipper-stagger
Phase H — coordinates N frac pads competing for the SAME resource pool (crews, pumps, sand silos) on one timeline. Greedy priority-first event-driven scheduler. Dashboard at /multi-pad.
Phase H — pure `src/lib/multiPad/multiPadCoordination.ts` greedy priority-first event-driven scheduler. Coordinates N pads competing for a shared fleet pool (crews / pumps / sand silos) on one timeline, honoring per-pad `priority`, per-pad `padReadyMin`, per-resource `transitionMin` (crew/pump rig-up when a shared resource flips pads), and a cross-pad `zipperStaggerCrossPad` helper. Reports per-pad totalMin, critical-pad id, peak fleet usage, and per-resource transition lockout minutes. UI surface is `/multi-pad` (Layers icon, sidebar Run section, `isNew: true`) — Gantt strip + per-pad rollup table + CSV export via `buildMultiPadScheduleCsv` (RFC 4180, `,` / `;` delimiter, `# total_min` / `# critical_pad` / `# peak_usage:*` / `# transition_lockout_min:*` trailer rows). Distinct from `padScheduling.ts` (single-pad) and `surrogatePadAutoTuner.ts` (per-pad surrogate-driven knob tuning) — pair them by tuning each pad first, then coordinating. Drift-guard: `multiPadCoordination.test.ts` (11 tests: capacity-ample, contended-serialise, priority tiebreak, padReadyMin honored, transition lockout, peak usage, critical-pad id, zipper helper, empty input, CSV header + summary lines, semicolon delimiter).
Honest scope: greedy priority-first, NOT the NP-hard global optimum. Out of scope today: per-unit (not per-class) transition cost, MILP global re-sequencing, joint optimisation with surrogate cost model, fleet-aware sand logistics (silo refills, hot-shot trucking). Pairs with `surrogatePadAutoTuner.ts` — tune each pad's stage durations first, then coordinate.
Layered σ_h profile + per-bench KGD-coupled aperture
Phase 8 ships a TRUE fully-planar 3-D fracture-propagation kernel that extends the Pseudo-3D solveImplicitHeight path with a per-bench aperture profile, an elliptic-prism cross-section, and a leakoff-free volume-balance length. It is the first solver in the codebase that does NOT homogenise E' or σ_h across contained layers — each bench keeps its own elastic modulus and minimum horizontal stress through the entire width and length update.
layers[]— ordered benches withtopTvdFt,thicknessFt, layer-localsigmaHPsi, and plane-strainePrimePsi.pInternalPsi— net internal fluid pressure driving width.kIcPsiSqrtIn+ pay-benchtopTvdFt/thicknessFt— consumed bysolveImplicitHeightfor the implicit height bisection.rateBpm— slurry rate per wing (factor 2 baked into length).timeMin— treatment time for the volume-balance length.
- Height
His implicit fromK_I = K_ICat top & bottom tips (Tada / Sneddon piecewise-constant kernel viasolveImplicitHeight). - Per-layer aperture is plane-strain Sneddon, evaluated locally:
w_max(layer) = 2·max(0, p − σ_h_layer)·H / E'_layer. - Vertical aperture profile across each bench is the upper-half ellipse
w(z) = w_max·√(1 − (2z/t)²)⇒ per-bench area(π/4)·w_max·t_contained. - Two symmetric wings; length from leakoff-free volume balance
L = q·t / (2·A_xs). - Carter / PDL / cake leak-off is intentionally OFF in v1 — the headline is layered-σ_h, not the leak-off model (those couplings remain owned by §3 + the Newton kernel).
- No proppant transport, no time-marching pressure update — single snapshot at
(p, q, t).
H = solveImplicitHeight({ pay, layers, p, K_IC })
for layer in layers ∩ [topTip, botTip]:
t_c = overlap(layer, [topTip, botTip])
w_max(L) = 2·max(0, p − σ_h(L))·H / E'(L) # in
A_c(L) = (π/4) · w_max(L) · t_c # ft·in
A_xs = Σ A_c(L)
L_wing = (q·t · 5.61458) / (A_xs / 12) # ft
V_wing = q · t # bblPL3DResult)height— fullImplicitHeightResult(heightFt,topTipTvdFt,botTipTvdFt, K_I residuals).perLayer[]— for every contained bench:containedThicknessFt, layer-localsigmaHPsi/ePrimePsi,wMaxIn, andxsAreaContribFtIn.xsAreaFtIn— total elliptic-prism cross-section area.halfLengthFt— per-wing half-length from volume balance.volumePerWingBbl— slurry volume injected per wing.
planar3DPropagation.test.ts— 8 drift-guards: mass balance, layered-pay aperture dominance, q-linear length, p-monotone height & width, containment exclusion, input validation.planar3DBenchmarks.test.ts— per-bench Sneddon analytic + height-time growth (4 tests) pluscomparePlanar3DAgainstClassicModelscross-check vs PKN, KGD, and Savitski–Detournay radial on identical reduced inputs (CSV export, μ-sensitivity, PKNt^{4/5}growth).- Interactive comparator at /planar3d-validation — sweeps μ from 1 → 200 cP and surfaces the per-model half-length / wellbore width / net-pressure table with PL3D as the reference.
- Collapses byte-identically to a single-bench PKN-style geometry when given one layer (locked by drift-guard).
src/lib/planar3DPropagation.ts · src/lib/p3dImplicitHeight.ts · src/lib/planar3DBenchmarks.ts · src/components/Planar3DComparatorPanel.tsx- Snapshot solver — no Δt loop, no transient pressure / proppant feedback.
- No leak-off in v1 (Carter / PDL / cake remain in §3 and the live Newton kernel).
- Per-bench aperture is plane-strain Sneddon — no DDM bench-bench shear coupling beyond the shared height bisection.
- Elliptic-prism A_xs assumes the upper-half-ellipse vertical profile holds within each bench; thin benches near the tip are approximate.
- Not yet wired into
coupledNewtonStep;/planar3d-validationis the only consumer today.
Enthalpy- and entropy-specified flash for choke + geothermal-injector advisors
v9 Gap A adds an isenthalpic (PH) and isentropic (PS) state-spec flash entry point alongside the existing PT flash. Choke advisors call PH for Joule–Thomson cooldown across surface restrictions; geothermal-injector advisors call PS for near-adiabatic compression / expansion checks. The kernel is a robust 2-loop solver: an outer Newton iteration on temperature drives the inner PT flash until the specified property (H or S) is matched within tolerance. Single-phase and two-phase regions are handled in one branch via the existing cubic-EOS fugacity path so the choice of spec never silently changes the equilibrium model.
Engineers opt in per-workspace from Settings → Solver preferences (the PH/PS state-spec flash switch, backed by phPsFlashStore). When off, advisors keep using the PT path and document the assumption inline; when on, they expose the post-flash quality, two-phase split, and energy-balance residual so the decision is auditable.
src/lib/flash/phPsFlash.ts · src/lib/phPsFlashStore.ts · src/components/settings/SolverPreferencesCard.tsx- Outer Newton may need a relaxed step for steep retrograde regions.
- Three-phase (water + hydrocarbon liquid + vapor) is not yet wired in.
- Reference enthalpy datum is the EOS ideal-gas reference; mixing across workspaces with different datums requires an explicit reconciliation step.
Time-of-flight, drainage allocation, and Lorenz-coefficient screening
v9 Gap C ports the MRST diagnostics / JutulDarcy flow-diagnostics workflow into the screening tier. Given an incompressible flux graph (well-to-well or stage-to-parent), the solver computes forward and backward time-of-flight, an injector / producer tracer partition, a full allocation matrix, and a Lorenz coefficient with the associated sweep- efficiency estimate — all without running a transient reservoir simulation.
Two live UI surfaces consume the kernel: FlowDiagnosticsCard on the project Economics page (5-spot allocation matrix demo) and TofIsochroneCard on the workspace Parent-child page (per-parent drainage share %, median residence time, Lorenz heterogeneity rank from an inverse-distance² stage→parent flux proxy). 9 drift-guards lock conservation, single-injector mass split, and Lorenz monotonicity.
src/lib/reservoir/flowDiagnostics.ts · src/components/parent-child/TofIsochroneCard.tsx- Incompressible, single-phase tracer math — not a saturation-tracked transport simulation.
- Flux graph for the parent-child surface is an inverse-distance² proxy, not a pressure-solver output.
- Lorenz coefficient is a screening rank; pairing with a sector reservoir model is the recommended next step before well-spacing decisions.
What this simulator is and is not
This is an engineering screening and DFIT-calibration tool built from first-principles modules with explicit benchmarks. It is not a research-grade Planar-3D code today — single-cluster scalar geometry, sequentially explicit coupling, and decoupled leak-off / filter-cake helpers are honest limitations, not bugs. Validate against site DFITs and offset history before field commitment. The Phase 1–6 roadmap upgrades the scoreboard one row at a time; each phase ships its own analytical benchmark in CI so the scoreboard never lies.