petekstatic 0.1.5

petekStatic — the GEOMODEL layer: structural framework, grid construction, property modelling, volumetrics and static uncertainty, consolidated into one crate.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
# petekStatic — design constitution (build spec)

> Repo: `Koding/Rust/petekSuite/petekStatic` · crate: `petekstatic`
> (modules preserve the historical `srs-*` layer labels). The **GEOMODEL** layer of the petek subsurface-modelling
> ecosystem. This is the design constitution (the *why*/*how*); the locked public
> contract is `API.md`; the shared conventions are the petek family house style
> (`petekSuite/dev-docs/petek-house-style.md`). Cross-library seams + lifecycle
> live in the suite planning graph (served by the `contract` MCP).

petekStatic turns **model-ready inputs** (petekIO's `ModelInputs` / `.pproj`)
into a populated **`StaticModel`** — a structural framework (horizons + faults +
zones), a modelling grid, per-cell property cubes — **and owns the volumetrics +
static-uncertainty stack over it**: GRV / in-place (OOIP/OGIP) off the model,
and Monte-Carlo regeneration over static-model realizations. petekSim (the
dynamic/engineering + product layer) consumes the model and its volumetric
results across the seam.

**Litmus test for what belongs here (re-scoped 2026-07-03, graph
`decision_layer_charters`):** it is a *static-modelling* concern — anything a
geomodeller or a static-uncertainty study needs: the populated grid, its
in-place volumes, and the MC loop over static realizations (tornado later).
**PVT/fluid correlations, dynamic flow, decline/material-balance, economics, and
the Python product facade are petekSim's.** FVF crosses this seam **as an
uncertain scalar input** (a validated value type), never as PVT code. Nothing
input-data-specific (parsing, QC, interpretation) lives here; that is petekIO's
job. We **consume** petekIO and **call** petekTools kernels; we **never** depend
on petekSim (our downstream consumer).

---

## 1. Design constitution (this library's slice of the family house style)

1. **Strictly layered, one-way internal deps; no cycles.** The current module DAG
   inside the single `petekstatic` crate:

   ```
   error                     # the one workspace error enum (StaticError) + Result
   srs-grid                  # i,j,k corner-point grid: Grid/Cell/KLayer/Property
   srs-wireframe             # the constraining wireframe (boundary+horizons+contacts)
   srs-gridder               # convergent gridder: min-curvature surface + layering
   srs-petro                 # petrophysics: log upscaling (power-law means)
   srs-data                  # thin petekio adapter: ModelInputs -> wireframe + logs
   srs-volumetrics           # GRV + in-place (OOIP/OGIP) + FVF value types + range validation
   srs-uncertainty           # Monte Carlo: distributions, SplitMix64, P90/P50/P10 (leaf)
   srs-spill                 # out-of-core backing mode: memory budget + k-slab spill
                             #   onto a petekTools store (imports srs-grid + srs-volumetrics)
   srs-model                 # TOP OF DAG: the StaticModel aggregate + builder +
                             #   the MC-regeneration template (relocated from petekSim)
   ```

   A module imports only from layers above it. `srs-volumetrics` and
   `srs-uncertainty` **relocated here from petekSim 2026-07-03** (origin SHA
   `fe6343c`, `task_relocate_refine_orchestration`) under the layer-charter
   re-scope; `srs-spill` **split out of `srs-model` 2026-07-05**
   (`task_petekstatic_organize`, P10 organize wave) as the out-of-core
   backing-storage mode below the aggregate; `srs-model` composes grid + gridder +
   petro + wireframe + volumetrics + spill into the `StaticModel` and owns the
   regeneration orchestration relocated from petekSim's `RefiningModel`. `srs-model`
   re-exports `srs-spill`'s surface, so the split is source-compatible.

2. **One error enum** (`StaticError`, `thiserror`) + `Result<T>` everywhere;
   downstream composes it with a `#[from]` variant on its own enum (house §1).
   petekSim does this: `SrsError::Static(#[from] petekstatic::error::StaticError)`.
   Upstream composes IN the same way: `StaticError::Geo(#[from] petekio::GeoError)`
   — so `?` chains DATA→GEOMODEL→SIM and `source()` reaches the origin.

3. **Domain objects carry their operations** — fluent, chainable, immutable; ops
   return *new* objects, mutation is explicit `set_*`. `f64::NAN` = undefined.

4. **Compartmentalized.** One module/topic, one type/responsibility; split before a
   file owns two jobs. Boundaries are traits where backends are plural, enums where
   the set is small and closed.

5. **Compose deps, don't reinvent.** The convergent gridder / kriging / warm-start
   kernels are petekTools' job; unit conversions are petekTools'; input-data work is
   petekIO's. We map at the seam and convert small types rather than share code
   sideways. (The FVF value types in `srs-volumetrics` are the sanctioned
   duplicate-at-the-seam: petekSim's `srs-pvt` keeps its own copies for the
   dynamic work; no dependency crosses.)

6. **Physical-range validation is typed, at the seam.** Every volumetrics input
   passes `validate_fraction` (φ/NG/Sw ∈ [0,1]) / `validate_positive`
   (area/height) / the FVF constructors — a non-physical input is a
   `StaticError`, never silent garbage (`-117 MMSTB`, `inf`).

7. **PyO3-ready by construction.** Owned types on the public surface, no public
   lifetimes, plain numeric types / `ndarray`. Bindings (petekSim's facade today)
   only marshal.

---

## 2. The `StaticModel` — the contract (LANDED, `srs-model`)

A **`StaticModel` is a *populated* static reservoir model**: the single, complete,
self-describing artifact petekStatic produces. It is a value (owned, `Clone`,
`Send`), not a handle to a live builder. Its anatomy:

| part | type | what it carries |
|---|---|---|
| **structural framework** | `Wireframe` (`srs-wireframe`) | areal boundary + depth horizons (`Top`/`Intermediate`/`Base`) + fluid contacts, each tagged with `Hardness` |
| **grid** | `Grid` (`srs-grid`) | corner-point geometry + `nk` `KLayer`s. The unfaulted box is the degenerate case; the same type upgrades in place to the faulted grid |
| **property cubes** | `Properties` (`srs-grid`) | name → `f64[cell_count]` per-cell scalar fields (`PORO`, `SW`, `NTG`, …); `NaN` = undefined |
| **zones** | `ZoneTable` (`srs-model`) | named stratigraphic intervals bounding k-ranges; MVP ships the single implicit whole-column zone (§4) |
| **contacts** | `Vec<Contact>` (on the framework) | OWC / GOC / GWC depths — the model's own volumetrics clip against these |
| **provenance** | `Provenance` (`srs-model`) | inputs identity, gridder settings, population mode, and the `RealizationDraw` (with `seed_index`) for an MC realization (§6) |

**Invariant:** every property cube's `values.len() == grid.cell_count()`; every
zone's k-range lies within `[0, nk)`; the grid's areal lattice matches the
framework's. Enforced at construction (the constructor is crate-internal; only
the builder/template build models) — the consumer never re-validates.

**Units — SI everywhere, one convention** (`decision_si_units_standard`,
2026-07-04). All coordinates, depths and lengths are **metres**; areas **m²**;
volumes **m³** internally, reported in **mcm** (`1e6 m³`, GRV), **MSm³** (oil)
and **bcm** (`1e9 Sm³`, gas). Vertical is **positive-down depth** (clearly-named
`*_m` accessors), the domain-natural framing the ruling permits, and it is the
**one datum inside the `Wireframe`**: petekIO delivers surfaces (and well-curve
positions) as negative-down subsea elevation, so `srs-data` **negates them at the
ingest boundary** (`surface_depths`) onto positive-down `depth_m`, matching the
contacts petekIO already delivers positive-down. This unifies horizons and
contacts on one convention (so structural role assignment reads `Top` =
shallowest correctly) and retires the coordinate-flip deferral — its blocker,
petekIO's imperial `SummaryInputs` contacts, resolved when petekIO went SI
(2026-07-04). FVF (`OilFvf`/`GasFvf`) is
dimensionless **Rm³/Sm³** — numerically identical to the legacy rb/STB & rcf/scf,
so it is a **relabel, not a conversion**. Imperial is opt-in only (petekTools
factors), never a default.

**The model OWNS its volumetric answer** (inverted 2026-07-03 by
`decision_layer_charters`, superseding the earlier "computes no volumes"
stance): `model.in_place()` clips the column against the model's contact(s) and
returns `InPlace` (GRV + HCPV + per-cell HCPV); the caller applies FVF through
the validated `OilFvf`/`GasFvf` value types (`ooip_sm3`/`ogip_sm3`, +
`oil_msm3`/`gas_bcm` reporting scales). P-curves aggregate per-realization
in-place values via `srs-uncertainty`'s `PercentileSummary`.

**Two-contact columns (gas cap + oil rim).** A framework carrying a GOC *and* a
lower contact (OWC/FWL or GWC) makes `in_place()` partition cells by centroid
depth — gas cap (`z < GOC`) / oil leg (`GOC <= z < lower`) / water — and report
per-zone GRV + HCPV (`InPlace::gas` / `::oil`, `gas_zone_ogip_sm3` /
`oil_zone_ooip_sm3`). A lone contact stays the generic single-column answer.
**The split is geometry + in-place only**: free-gas OGIP off the cap, STOIIP off
the leg. What the model still does NOT carry: PVT correlations (solution gas Rs,
gas-cap expansion, condensate), economics, dynamic behaviour — petekSim's; FVF
still enters only as a validated scalar per zone.

---

## 3. Construction path — how a `StaticModel` is built

The build pipeline, ingest → framework → grid → population → model:

```
petekIO ModelInputs (.pproj)
   │  srs-data (thin adapter, no data processing)
   ▼
Wireframe  {boundary, horizons, contacts}         + PetroSample logs [(tvd,φ,Sw)]
   │  warm_surface  (petekTools warm kernel; the SAME path the MC template uses — R2)
   ▼
solved Top/Base surfaces
   │  Surface::guard_below  (base-above-top → CrossedSurfaces, or clamp — R1)
   │  srs-gridder::layer_grid  (conformable k-layering -> corner-point Grid + report)
   │    Conformity: Proportional (equal fraction, honours nk) | FollowTop{dz_m} |
   │    FollowBase{dz_m} (layers parallel to top/base at constant dz; nk dz-derived
   │    = ceil(max thickness/dz), capped MAX_NK; deep/shallow layers TRUNCATE at the
   │    pinch-out horizon → zero-thickness cells, counted in provenance warnings)
   ▼
Grid (geometry only)
   │  srs-petro upscaling  (φ length-weighted, Sw pore-volume-weighted)
   │  population: logs-in-cell-range -> upscaled cube values; else priors
   │  + optional areal trend: lateral NTG (+φ) shape (§3b)
   ▼
StaticModel  {framework, grid, cubes, zones, contacts, provenance}
```

This pipeline is `srs_model::StaticModelBuilder` (relocated from petekSim's
`RefiningModel` 2026-07-03; petekSim's `srs-core` now keeps only a thin facade
that delegates here and applies FVF). Entry points: `from_wireframe(wf, opts)`
(the data-layer hand-off) and `flat(...)` (the model-first flat-box start);
`add_top_control` grows the control set for the live-refine loop; `build()`
converges the current state. See `API.md`.

**One declarative `BuildSpec` (the suite api-consistency contract,
`task_petekstatic_spec_mirror`).** The builder and the MC template share a single
declarative configuration value — `BuildSpec` (georef, boundary, extrapolation,
repair/clamp/collapse floors, sugar-cube, sw_gas, well ties + `TieSettings`,
inputs_ref) — held internally by both; every `with_*` setter is thin sugar
mutating it and `with_spec` installs a whole one. It serializes (a scenario is a
savable file; the whole config layer derives serde — R7 battery in
`tests/spec_conformance.rs`; `McInputs` joins once petekTools' samplers derive
serde), compares by value, and is `#[non_exhaustive]` so the structural-
uncertainty per-zone isochore leg (`decision_structural_uncertainty_isochore`)
lands additively — as do new fields on the `#[non_exhaustive]`
`RealizationDraw`/`ZoneDraw`. Well ties fold in per `TieSettings`: `Replace`
(default; datum substitution — the tie moves exactly the tied node on a dense
lattice) or `Radius { radius_m }` (bounded locality: the residual decays linearly
to zero at the radius across defined datums; beyond is bit-untouched; voids stay
the solve's to taper). One tie authority (`substitute_tie_datums`) serves both
consumers. MC run resources are the separate `McSettings { n, seed, workers,
spill_dir }` behind the single `run_mc` entry (the four historical drivers are
deprecated thin wrappers).

### 3a. Base-horizon relief (Fix S1)

`from_wireframe` wires a supplied `Base` horizon's **real relief** through the
build: its defined nodes solve a base surface the pillars follow, so gross
thickness varies spatially. `BuildOpts.gross_height_m` is the **fallback**
when no Base horizon is supplied (backward-compatible constant offset). Supplied
horizons the build cannot consume yet (Intermediate; a second or
lattice-mismatched Base) are reported non-blockingly on
`Provenance.warnings` (`BuildWarning::UnusedHorizon`), never silently dropped.
The MC template honours base relief too (`decision_template_gross_scaling`):
built from a wireframe carrying a Base horizon, it extracts the nominal per-node
gross field `g(x,y)` (base solved once in kernel space against the nominal top)
and each draw's `gross_height_m` acts as a **level** on that **shape** —
per-node gross = `g × gross_height_m / mean(g)`, so a draw at `mean(g)`
reproduces the deterministic build. Without a Base horizon the draw keeps the
constant-offset behaviour exactly (backward compatible).

**Builder/template kernel unification (R2, 2026-07-04).** Both the deterministic
build and the template's `realize` solve their surfaces through one shared
petekTools warm-kernel path (`warm_surface`: flat bootstrap at the mean control
depth, then a seeded solve). Previously `build()` used the cold `solve_surface`
(natural-dip reference kernel) while the template used the warm kernel; the two
kernels interpolate **unpinned** interior nodes differently, so on thin columns —
where gross is a small difference of two large surfaces — GRV diverged ~20× (a
fully-pinned lattice hid it, since both kernels then just return the control
values). Unifying on the warm kernel makes `realize(mean-gross draw)` reproduce
`build()` within tight tolerance on all column shapes; cold `solve_surface`
remains the accuracy reference, off the build path
(`decision_gridder_kernel_unification`).

**Base-above-top guard (R1, 2026-07-04).** After the surfaces solve, a per-node
`Surface::guard_below` asserts the base sits at/below the top (positive gross). A
crossing is a typed `StaticError::CrossedSurfaces { nodes, worst_m }` (default),
or — with `with_clamp_base_to_top(true)` — the offending columns clamp to zero
gross and the rest are untouched. Thin/crossing frameworks no longer silently
collapse GRV.

**Post-gridding order-repair (R-c, 2026-07-04).** The `CrossedSurfaces` guard is
loud but leaves the caller stuck when a real thin margin genuinely crosses:
independent gridding of Top and Base overshoots at the edges and re-introduces a
crossing a pointwise pre-repair had removed. `with_min_thickness_m(f64)` (on both
the builder and the template) opts into a remedy: `Surface::repair_min_thickness`
pulls every node where `base_z - top_z < min_thickness` **down** to exactly
`top_z + min_thickness`, **preserving the top** — the top is the better-constrained
seismic pick, so the softer base yields to it (repair direction = downward). Off
by default (the guard stays the default); when enabled, `Provenance.warnings`
gains a `BuildWarning::ThinColumnsRepaired { columns, worst_m }` recording the
repaired-node count and the worst (most-negative) original separation. It takes
precedence over `with_clamp_base_to_top` when both are set (repair, don't zero).

**Solve fidelity (2026-07-04, structure-fidelity audit S1/S2).** The stack
surface solve honours every **defined input node exactly** (hard controls,
bit-level) — the on-data contract. Between nodes, the solve is iterated to a
true **fixed point**: the kernel's per-sweep stopping rule can stall metres
short of convergence on a sparse-control lattice (the smooth modes under the
natural-dip boundary relax slowly), so `warm_surface` re-seeds the solve from
its own output until a whole re-solve moves < 1 mm (a converged re-solve exits
in ~1 sweep, so the loop is cheap once settled). Scatter→node **conditioning is
upstream of this seam**: the builder consumes `GriddedDepth` node data; if the
upstream authoring snaps scatter to nearest nodes on a coarser lattice, the
resulting on-data misfit is an authoring artifact the audit fixture quantifies
(`tests/structure_fidelity.rs` S1) — author node values by local interpolation.

**Extrapolation policy (2026-07-04, audit S3).** Every stack surface/isochore
solve is subject to an explicit [`ExtrapolationPolicy`]
(`with_extrapolation` on builder + template). Default **`DecayToData`**: within
`start_cells` (2) of the nearest datum the solve is untouched; beyond, it blends
linearly toward the **nearest datum's value** over `decay_cells` (4), so a data
void inside the model extent flattens to nearest-data instead of running the
kernel's natural-dip linear extension unbounded (or, under-converged, freezing
at the seed mean — both uncontrolled). A merged envelope's margin therefore
STAYS merged (nearest isochore datum is 0). `NaturalDip` opts back into the
legacy unbounded extension for callers who KNOW the regional dip continues.

**Repair precedence — mapped over derived (2026-07-04, `task_petekstatic_topsonly_envelope`).**
The downward repair above (base yields, top preserved) is the *mapped-vs-mapped*
rule. In a horizon stack the repair also distinguishes **mapped vs derived**
(tops-only) surfaces: a mapped horizon is measured and authoritative, so when a
*derived* surface crosses a *mapped* one the **derived surface yields** — the
`Surface::guard_above` / `repair_min_thickness_from_below` twins pull the derived
**upper** *up* to the mapped lower, leaving the mapped surface bit-unchanged. A
mapped-vs-mapped (or derived-vs-derived) crossing keeps the historical
lower-yields direction. With the build-down isochore construction (§4a) a
consistent stack never crosses, so this precedence only guards inconsistent inputs
or a `min_thickness` floor.

### 3b. Areal trend hook — external-drift-lite (Fix 2; INTERIM)

`with_areal_trend(TrendSurface)` (on both the builder and the template) applies a
gridded areal multiplier field, nearest-node resampled to the model column
lattice and **mean-normalized**, per-column to NTG (and φ if the trend flags it)
after population. Semantics (ratified logs-give-shape / draw-gives-level,
`decision_staticmodel_regen_seam`): the **trend gives lateral shape, the
prior/draw gives the level**; because multipliers are mean-normalized the
property field-mean is preserved. Undefined (`NaN`) nodes fall back to `1.0`.
Resampling is index-fraction by default; `TrendSurface::with_georef(origin,
node_dx, node_dy)` maps the field to world coordinates so population resamples by
each column's **world** `(x, y)` (from the grid cell centroids) — the trend then
lands where its data is instead of being aligned to the model lattice by luck
(R4). **This is the INTERIM trend mechanism** (coordinator design
`python-model-build-api.md`): its successor is the per-property pipeline (§3c),
where trends enter as collocated cokriging inside propagation. The hook remains
for the deterministic `with_areal_trend` path; its resampler is now the shared
`petektools::resample` kernel (the nearest-node sampler was retired 2026-07-04).

### 3c. Per-property geostatistical pipeline (P5, `task_petekstatic_property_modelling`)

Properties are modelled **one at a time** through a visible pipeline (coordinator
design `python-model-build-api.md`, owner-refined):

```
PropertyPipeline::new("PHIE")
    .upscale(wells, UpscaleMethod::Arithmetic)   // logs -> per-cell conditioned values (+ QC)
    .propagate(Gaussian::new(variogram, seed))   // SGS fills every cell, conditioned on the upscaled cells
```

- **upscale** is a first-class inspectable step: each positioned `WellLog` snaps
  to its areal column, and its samples that fall in a cell's depth range are
  upscaled (`srs-petro` power means) into that cell — a per-cell conditioned field
  (`NaN` where no log passes) plus an `UpscaleQc` report (upscaled-vs-log stats).
  The cell's depth range is interpolated **at the well's `(x, y)`** (bilinear over
  the cell corners), not the column-centroid 4-corner mean, so an off-centroid well
  on a dipping zone boundary bins each sample into the zone its depth truly falls in
  — without this, near-boundary samples were mis-assigned across the zone and the
  per-zone upscale compressed the zone proportions toward the mid-range.
- **propagate** runs petekTools' sequential Gaussian simulation
  (`geostat::sgs`) **per k-layer**, conditioned exactly on that layer's upscaled
  cells, so every cell is filled with a seeded, reproducible draw that honours the
  wells and reproduces the data histogram. `Gaussian::with_trend(surface, corr)`
  folds a trend in as a collocated (Markov-1) secondary via cokriging, the surface
  resampled to the model lattice through `petektools::resample`; `corr = 0`
  recovers plain SGS bit-for-bit. The moving-neighbourhood search defaults to a
  **bounded** window (16 nodes within `max(1.5·range, 4·spacing)`) — the old
  whole-grid default was pathologically slow on real lattices, and beyond a range a
  node's kriging weight is ~0, so the bounded field matches within simulation
  tolerance; `Gaussian::with_unbounded_search` restores the old window. A simulated
  layer with **no conditioning data** is a hard error naming the property (and, via
  the zone-scoped callers, the zone) — a silent constant mean-fill erased spatial
  structure; `Gaussian::allow_mean_fill` opts back into the fill. **Frame seam:** a
  **world**-georeferenced trend is resampled at
  each column's world position through the model `Georef` (the grid geometry is
  local; the georef maps it to world) — resampling a world trend onto the local
  lattice yields an all-`NaN` secondary that the kernel silently drops to plain SGS,
  so a secondary covering **< 50%** of the model frame is a hard error, never a
  silent no-op.
- Wired into `StaticModelBuilder::with_property` and `StaticModelTemplate::
  with_property` / `with_property_mode`; each run's `PropertyReport` (upscale QC +
  `propagated`) lands on `Provenance.property_reports`. Regular / axis-aligned
  column lattices (the `layer_grid` box/conformable grids); rotated pillars are
  future work.

**MC composition (`decision_mc_composition`).** Per property, the template chooses
a mode:
- **`LevelShift`** (default) propagates once (cached at the first realization) and
  reuses the pattern each draw, adding only the draw's per-property level shift
  (`RealizationDraw::with_property_shift`) — same spatial pattern, moved level; the
  cheap ~ms-class MC path (level uncertainty dominates this asset class). The three
  **fraction cubes** (`PORO`/`NTG`/`SW`) are **shift-then-clamped** to `[0,1]` per
  cell (F9, 2026-07-04): a conditioned real cube legitimately holds boundary cells
  (a non-net `NTG=0`, an aquifer `SW=1`), so a shift that would push them out of
  range **saturates** them instead — an `SW=1` cell stays 1 under a positive shift
  (physically right) and slides interior under a negative one; interior cells move
  by the full shift; `NaN` (undefined) cells are untouched. This is a per-cell
  *application* clamp only — H2 still validates the **drawn inputs** at the seam
  (`realize`), so a garbage sampler is still a typed error. Non-fraction cubes
  (e.g. permeability) are shifted without a clamp.
- **`Resimulate`** re-runs SGS with a fresh per-draw reseed (from `seed_index`) —
  a new pattern each draw (heterogeneity uncertainty), a simulation per draw.

Both are bit-reproducible for identical draws. Bench (50×50×20): `LevelShift`
realize ~320 µs, `Resimulate` realize ~139 ms.

---

## 4. The zones concept

Zones are the **stratigraphic index** of the model. A **zone** is a named
vertical interval of the column, bounded by two framework horizons (or the
top/contact), spanning a contiguous range of `k`-layers — the grid addressable
by geology instead of raw cell index.

Landed shape (`srs-model`): `Zone { name, top_horizon, base_horizon, k_range }`
+ `ZoneTable` (ordered top→base; k-ranges partition `[0, nk)`), with
`ZoneTable::single(nk)` the degenerate whole-column zone and
`ZoneTable::from_stack(...)` the real multi-zone table.

### 4a. The multi-zone regional framework (`task_petekstatic_multizone`, LANDED)

The canonical framework is an **ordered horizon stack** — `N` horizons top→down
define `N − 1` named zones (intra-zone splits are simply more horizons):
`StaticModelBuilder::from_horizon_stack(HorizonStack, BuildOpts)`. A
`HorizonStack` carries the ordered `horizons` and the per-zone `zone_layers`
(`StackZone { name, color, conformity, nk, contacts }` — each zone names itself and
carries an optional display colour; the old `zone_names` parallel array is folded
into `StackZone::name`, and `color` flows to the model's `Zone` and the section
bundle's colour-by-zone `zones` list).

- **Build-down via non-negative CUMULATIVE isochores, anchored at the TOP
  (2026-07-04, `task_petekstatic_topsonly_envelope` + the structure-build rider).**
  The stack is resolved by **constructing downward**, not by gridding each horizon
  independently: the **top** mapped horizon is gridded once; each deeper **mapped**
  horizon carries a **cumulative isochore vs the TOP anchor** — thickness samples
  `(z_k − z_top).max(0)` at co-located defined nodes, min-curvature gridded,
  clamped `≥ 0`, tapered per the extrapolation policy, then made **monotone** down
  the stack (`cum_k = max(cum_{k−1}, iso_k)`); the horizon sits at `top + cum_k`.
  Ordering therefore **can never invert**; where two mapped horizons merge the
  input thickness is exactly `0` and the zone collapses to **genuine zero**; and —
  the **internal-swap invariance convention** — each mapped horizon depends only on
  the TOP grid and its OWN grid, so replacing an internal mapped horizon leaves
  every other horizon (and the total envelope GRV) **bit-unchanged**, except where
  the swapped data genuinely crosses a deeper horizon (the monotone clamp then
  collapses that zone toward the data — never adds rock). Fallbacks when a horizon
  shares no defined node with the TOP: co-locate with the nearest shallower mapped
  horizon (samples lifted by its cum), else an independent solve differenced
  pointwise vs the horizon above, clamped `≥ 0`. **Well ties substitute the
  measured top into the horizon's gridded datum at the tie node and re-run this
  same resolution** — a tie on a fully-defined lattice moves exactly the tied node
  (radius of influence 0 cells; audit S4). *This changed built geometry for
  stacked models (mapped horizons now match the input scatter within gridding
  tolerance rather than being solved in isolation).*
- **The engine owns scatter gridding** (`task_petekstatic_facade_engagement`): a
  **Scatter** horizon (`HorizonSource::Scatter(Vec<WorldPoint>)`, world coords) is
  gridded **inside** the stack build — the single scatter-gridding authority — via
  `from_scatter_stack(stack, opts, StackFrame)`. The engine conditions the points
  onto the model lattice with petekTools **bilinear** off-node conditioning
  (`StackFrame.georef` maps world→node), leaving genuine data voids **`NaN`**; the
  converged solve + `ExtrapolationPolicy` + isochore build-down then act on the
  actual observations, so a data-void margin between two exactly-merged horizons
  **collapses to zero** rather than inheriting an independently-extrapolated fill.
  No caller pre-grids scatter onto the lattice. `Mapped` remains the **pre-gridded
  escape hatch** and is documented as **bypassing** the engine's solve/conditioning
  fidelity (loaded grids only).
  - **Direct-solve conditioning + the factor-once seam** (`task_suite_scatter_perf`):
    per-horizon conditioning uses petekTools' direct band-LU `MinCurvatureOperator`
    (the cap-bound iterative SOR is the degenerate-system fallback), driven through a
    crate-private `ScatterConditioner` that splits **factor** (assemble + factor the
    operator for a horizon's fixed off-node sample geometry — the dominant, once-per-
    surface cost) from **resolve** (back-substitute a fresh depth vector, ~O(n·bw)).
    Because the sample `(x, y)` — and thus the factorization — is fixed across depth
    vectors, re-seating the same geometry with new depths is a cheap resolve, not a
    re-conditioning. Conditioning is **bit-identical** to the prior one-shot path.
    The 11-horizon `condition_scatter` map stays rayon-parallel (each horizon factors
    independently). *The per-draw resolve lever has no live consumer while the MC
    template conditions each horizon once and perturbs surfaces by additive field
    (not by re-conditioning perturbed scatter); a data-perturbation MC mode that uses
    it is a coordinator decision.*
- **Three horizon source kinds** (`HorizonSource`): the raw **Scatter** above; a
  **Mapped** depth surface already on the lattice (tied or untied — the tie is a
  well-tie concern, not a source distinction); and a **TopsOnly** internal horizon
  with *no* mapped surface, defined solely by well **picks**. A tops-only internal split is
  **subordinate to its mapped envelope**: it is seated at the mapped horizon above
  it **+ `min(pick isochore, envelope isochore)`** — its own pick-thickness field
  (min-curvature gridded, constant-thickness fallback for a single pick), plainly
  **clamped** (a `min`, not a redistribution) against the zone's mapped envelope
  thickness. Consequences by construction: envelope `0` ⇒ split `0`**both
  sub-zones collapse** to zero exactly (no phantom slab); the split lives strictly
  inside `[mapped top, mapped base]`, so a **derived surface can never displace a
  mapped one**. A *trailing* tops-only horizon with no mapped horizon beneath it
  has no envelope and falls back to the legacy absolute drape. *The geological
  statement: an untied internal split follows the mapped horizon above it at the
  thickness its well picks record, but is never allowed to breach the measured
  horizons that bound its zone.* (Redundant mapped surfaces aliasing one horizon —
  the same `GriddedDepth` given twice — is tolerated; it yields a zero-thickness
  zone by construction.)
- **Per-interface order-repair (precedence: mapped over derived).** On a
  consistent isochore-built stack no interface crosses, so the repair is a **no-op**
  safety net; it still fires under a `min_thickness` floor or genuinely inconsistent
  inputs. When it does, it honours **mapped-over-derived precedence**: a *mapped*
  horizon is authoritative, so a crossing *derived* (tops-only) surface **yields**
  (is moved), never the mapped one; a mapped-vs-mapped (or derived-vs-derived) tie
  keeps the historical behaviour (the lower yields, top preserved). Per-interface
  repair records land in provenance (`StackProvenance::interface_repairs`).
- **Per-zone layering + conformity.** Each zone is layered by its own
  `Conformity` + `dz` (the conformity engine, made per-zone); total `nk` = sum of
  the per-zone counts. The **200-layer cap applies to the TOTAL** (per-zone
  breakdown retained; proportional scale-down when exceeded).
- **Per-zone contacts (a domain statement).** The real framework assigns
  **separate** contact sets to different zones — distinct accumulations in one
  stack — and some zones have **no** contact at all. So contacts are scoped per
  zone: `in_place_by_zone()` clips each zone's hydrocarbons against *its* contacts
  (two-contact gas/oil split where a zone has both), and a **contactless zone
  contributes its gross bulk volume but ZERO hydrocarbon in-place** — no contact
  means no *known* accumulation, so it is explicitly **not** a full hydrocarbon
  column. The rollup `ZonedInPlace::total` sums the zones, so
  `sum(zone volumes) == total` to FP tolerance (conservation).
- **Per-zone stats.** `StaticModel::zone_stats(property)` reports per-zone
  active-cell count/mean/min/max over the full-grid cubes (cubes stay full-grid,
  indexed by zone). Per-zone priors / per-zone SGS variograms are the noted
  follow-up; MC draws stay global-per-property this wave (per-zone contact draws =
  the documented follow-up).
- **Provenance records the stack.** `Provenance::stack: Option<StackProvenance>`
  carries the ordered horizon names, the per-zone layering (`ZoneProvenance`:
  name, bounding horizons, conformity, nk, k_start, truncated_cells), and the
  interface repairs.

The classic Top+Base wireframe (`from_wireframe`) is the 2-horizon / single-zone
degenerate case — unchanged and backward-compatible.

### 4b. Cell-collapse threshold (Petrel-style, LANDED)

`with_collapse_below_m(f64)` (builder **and** template; default OFF): after
layering, any cell thinner than the threshold **collapses** — but
**volume-conservingly**. The layer interface is snapped so a **thicker
zone-interior neighbour absorbs the sliver's thickness** (merge into the thicker
vertical neighbour; rock is never deleted). The merge **never crosses a zone
boundary** (a zone's bounding interfaces are pinned), so each zone's volume is
individually conserved; a single-layer zone has no interior neighbour and is left
untouched. Collapse is applied per pillar, so a cell zeroes only where all four
pillars collapse it (the same max-pillar rule as truncation). Per-zone collapsed
counts land in provenance (`BuildWarning::CellsCollapsed`); a collapsed
(zero-thickness) cell is excluded from volumetrics and `NaN`-marked in the view
bundles by the existing `dz ≤ ε` rule. It bites hardest on sub-`dz` slivers under
fixed-count proportional layering in thin columns and on Follow-style truncation
partials.

---

## 5. Framework, faults, and the wireframe

The `Wireframe` is the structural skeleton the grid conforms to. Today it carries
boundary + horizons + contacts; **faults are deferred** (`question_gridder_spec` /
P5 `task_petekstatic_zones_faults`: split pillars + throw + non-neighbour
connections). The `StaticModel` framework is designed so a fault set is *additive* —
it lands as a new framework member and a grid that splits pillars, without changing
the property-cube or zones contracts. Open/closed: extend by adding, not editing.

---

## 6. Provenance — the reproducibility record (LANDED)

A `StaticModel` records **what produced it**: the input bundle identity
(`inputs_ref`), the gridder settings (`SolveOpts`, `Conformity`, `nk`), the
population mode (`Priors` | `Logs`), and — for a Monte-Carlo realization — the
full **`RealizationDraw`** including its `seed_index`. Provenance is metadata:
it never affects geometry or cubes, but it lets a consumer label a realization
and answer "which inputs gave me this P10 model?". `Hardness` per constraint
(on the wireframe) is the per-node half of this lineage.

---

## 7. The regeneration seam — RATIFIED (graph `decision_staticmodel_regen_seam`)

### 7a. Monte-Carlo regenerates the model per realization

The MC loop does **not** perturb a shared grid in place — it **regenerates a
whole `StaticModel` per realization** from that realization's sampled inputs.
petekStatic is the single owner of *how a model is built*; each realization is a
clean, independently-valid `StaticModel`. (Since the charter re-scope the MC
driver itself is also petekStatic's — `task_peteksim_mc_structured`, retargeted
— which keeps the whole loop below the product facade.)

**Warm-start makes regeneration cheap.** The reusable vs per-realization split:

| reusable across realizations (in the template) | regenerated per realization |
|---|---|
| areal lattice (dims fixed) + conformity (+ `nk` for Proportional) | sampled scalar draw (area, gross height, φ, Sw, NTG, contact, optional GOC via `with_goc`); under a Follow conformity `nk` is dz-derived per realization from the drawn thickness (deterministic in the draw → still bit-reproducible) |
| control-point **topology** (which nodes are pinned) | control-point **depths** (structural shifts, typed `Option`) |
| the previous realization's **converged surface** (the warm seed) | the re-converged surface (a few sweeps from the seed vs 20k cold) |
| attached logs / population inputs | the re-layered grid + repopulated cubes |

Landed as `StaticModelTemplate::new(wf, opts)` (holds the fixed geometry +
warm-start state) with `realize(&mut self, &RealizationDraw) ->
Result<StaticModel>` the cheap call. Measured warm-start win: **~14×** (94 ms vs
1.34 s at 50×50) — the number that makes per-realization regeneration viable.

**Stack-aware MC (P8, `task_petekstatic_multizone_2`).**
`StaticModelTemplate::from_horizon_stack(stack, opts)` runs MC over the multizone
framework: the horizon surfaces + interface repair are resolved **once** (they do
not vary per draw), so `realize`/`realize_into` vary only spacing, the **per-zone
contacts**, and the **per-zone property levels** (`RealizationDraw.zones:
Vec<ZoneDraw>`). Geometry topology is draw-invariant ⇒ bit-deterministic by
construction and `realize_into`-recyclable (the stale-buffer bit-compare holds); a
contactless zone contributes GRV, zero HC.

**The ratified amendments (all landed):**
1. **`StaticModelTemplate: Send` + `StaticModel: Send`** (Sync not required),
   compile-checked via `static_assertions` — consumers shard realizations
   one-template-per-worker; the warm chain makes `realize` serial per template.
2. **`RealizationDraw`**: `#[non_exhaustive]` + `::new(...)` (P5 structural
   fields stay additive, no re-ratification); concrete `pub seed_index: u64`;
   derives `Clone + Debug`; structural perturbation as a concretely typed
   `Option<StructuralPerturbation>` (empty for MVP). **`fvf` deliberately
   EXCLUDED** — FVF is drawn separately and applied at the volumetrics output
   surface, never rides the draw.
3. **Perf:** grid-per-realization is its own (heavier) budget — the 422µs/100k
   figure is the sampler's only. Only property-cube `Vec<f64>`s conceptually vary
   per draw; room is left for an additive
   `realize_into(&mut self, &draw, &mut StaticModel) -> Result<()>` buffer-reuse
   variant (not yet implemented).
4. **`&mut self` on `realize`** (the warm-start chain) and **`Result` per draw**
   (warm-started solves can fail to converge; `StaticError` composes into
   `SrsError` via `#[from]`).

### 7a-bis. Per-horizon correlated structural uncertainty (LANDED — `decision_structural_uncertainty_isochore`)

The MC template's structural draws are **correlated perturbation fields**, applied
**in the space the geometry is built in** so ordering + exact merges survive every
draw *by construction* (no repair rules):

- **TOP surface** takes a correlated **DEPTH** field per draw
  (`RealizationDraw::top_structural`): an unconditional Gaussian random field
  (petekTools `sgs_unconditional`, marginal `N(0, sd_m²)`, the row's variogram)
  added to the converged top. The warm-start chain still advances on the
  **unperturbed** surface (per-draw noise, not structural drift).
- **Every deeper horizon** perturbs via its zone's **ISOCHORE (thickness)** field
  (`ZoneDraw::isochore_structural`): the fixed base thickness plus a correlated
  field, **clamped `>= 0`** and **zero-masked where the base isochore is exactly 0**.
  Deeper horizons are reseated `top + Σ isochores`, so a perturbed stack can never
  invert or resurrect a collapsed zone.
- **Determinism + ties:** each field's seed = `draw.seed_index` salted by the
  horizon index (bit-reproducible per seed; independent across horizons). The stack
  structural `realize` is a **pure function of the draw** → sharded == serial and
  `realize_into` recycling stay bit-exact. Perturbation is pinned to **zero at
  well-tie nodes** (radius-0 locality).
- **Clamp bias (measured + bounded):** the `>= 0` clamp biases mean thickness up near
  pinchouts by `E[max(0, T+P)] − T = sd·φ(T/sd) − T·Φ(−T/sd) ∈ [0, sd/√(2π)]`,
  variogram-independent, → 0 as `T/sd → ∞`. Pinned by a planted-truth GRV recovery
  test (`tests/structural_uncertainty.rs`).

### 7b. Kernel constraint (from the ConvergentGridder adoption — ENFORCED BY TYPE)

The template's warm-start surface lives **in petekTools kernel space**:
`solve_surface_seeded` accepts only the `KernelSurface` newtype, whose only
sources are `KernelSurface::flat` (a constant field — a kernel fixed point, the
safe bootstrap) and a prior `solve_surface_seeded` output.

**Kernel unification landed (2026-07-04, petekTools f81b6a6).** The newtype's
original justification — warm==cold holding only *within one kernel* because
petekTools lacked the natural-dip (linear-extrapolation) boundary of the cold
`solve_surface` (12.48 ft interior sag vs 0.0 on the plane reference) — is
**retired**: petekTools now carries the natural-dip boundary, so the cold reference
and the warm kernel share one fixed point. This is gated **per-node** (not
aggregate — the old min/max/mean cross-check passed while per-node drift was
12 ft): `srs-gridder/tests/adoption_readiness.rs` now asserts per-node plane
parity, and `seeded_warm_equals_cold_of_same_kernel` re-verifies warm==cold to
1e-6 on a well-determined framework (`decision_gridder_kernel_unification`).

**The newtype is KEPT** (reassessed 2026-07-04) on its second, still-live
justification: it is a **provenance / staleness guard** on the warm chain. A seed
must be a genuine same-kernel output — never an arbitrary `Surface` (a cold solve
carries caller-chosen `SolveOpts` whose fixed point can differ from the kernel's
fixed defaults; a hand-built/foreign field is stale). The barrier is zero-cost
(open/closed) and keeps the chain sound; it intentionally has no `From<Surface>`.
A subtlety surfaced while re-verifying: a **single** flat-bootstrap solve is not
itself converged on a *sparse*-control lattice (the smooth level mode relaxes
slowly under the near-Neumann boundary), so `build()` and `realize(mean draw)`
can differ there — tracked as an R2 follow-up, not a natural-dip regression.

### 7c. What the consumer reads off a `StaticModel`

The read-only accessor contract: `model.grid()` (geometry + `bulk_volume` +
`cells()`), `model.property("PORO")` / `"SW"` / `"NTG"` + `property_names()`,
`model.contacts()`, `model.zones()`, `model.provenance()` — **plus the
volumetric answer itself**: `model.in_place()` → `InPlace`, FVF applied by the
caller (`ooip_sm3(OilFvf)` / `ogip_sm3(GasFvf)`). petekSim's facade presents
these results; it re-computes nothing.

### 7d. The structured MC driver + tornado (LANDED, `mc` / `tornado`)

The uncertainty loop over the seam (`task_peteksim_mc_structured` /
`task_peteksim_tornado`). `run_structured_mc(&mut template, &McInputs, n, seed)`
samples every uncertain input from a petekTools `Sampler` (raw or `.clamped` —
petekTools' hard limiter, so a wild fraction prior can't trip §7's H2 guard),
realizes each draw, and **keeps** the per-draw `oil_sm3`/`gas_sm3`/`grv_m3`
vectors (W17 — the realizations are the product; tornado and cross-checks reuse
them). `boi`/`bgi` are sampled but applied at the volumetrics surface, never on
the draw (amendment 2). Reproducible: one seeded stream, fixed field order,
`seed_index = seed + i` per draw.

**Error policy — fail-fast with the draw index.** The first draw whose
realization or volumetrics fails stops the run and surfaces as the typed
`StaticError::McDraw { index, source }`: the §7 H2 rejection is carried to the
caller *with the offending draw identified*, deliberately over collect-and-report
(a bad draw means the input distribution strays outside the physical range — fix
it at the sampler). `aggregate_field` brackets a multi-segment field between
petekTools' `Correlation::Independent` (narrow) and `Comonotonic` (wide downside).

`tornado(&mut template, &McInputs, n, seed, lo_pct, hi_pct)` swings each input
one-at-a-time between its **realized** percentiles (others at P50), re-realizes,
and ranks the oil-in-place swings descending. Pivots are percentiles of the
realized input vectors — not the analytic distribution — so the ranks stay
consistent with the MC P-curve; the same `(n, seed)` reuses the MC's draws.

### 7e. View bundles — the viewer export seam (LANDED, `view`; graph `decision_viewer_home_product`)

The model exports typed, JSON-stable **inspection bundles** for a *separate*
viewer codebase (the `peteksim` viewer). The division is strict — **petekStatic
exports; the viewer renders, it never computes.** Everything a map /
cross-section / 3-D view needs is pre-computed here off the populated
`StaticModel` and handed over as serde-`Serialize` value types. The viewer scales
for display (vertical exaggeration, colour ramps, threshold filtering) but runs
no reservoir computation.

Three bundles, one shared frame:

- **`MapBundle` = `model.map_bundle(&MapSpec)`** — areal (plan-view): realized
  structural depth surfaces (top + base, grid-georeferenced, named after the
  framework horizons), property maps as a single **k-slice** and the
  **zone/interval average** (the useful default), the outline ring(s) in world
  coords (a `from_horizon_stack` build with no wireframe boundary derives the world
  outline from its georef, or takes an explicit `with_boundary` ring — never the
  degenerate unit square against a world frame), well surface markers (with tie
  residuals where provenance carries them
  — none in the MVP), and per-contact **subcrop masks** (the columns the contact
  plane crosses — the honest, marked-mask form of a contact contour). Every layer
  carries its value range for a legend.
- **`IntersectionBundle` = `model.intersection_bundle(&SectionSpec)`** — a
  vertical section walked through the areal lattice (§7e below in API). Since v4
  it also emits `horizon_traces` — one depth polyline per *interior* framework
  horizon (zone boundaries between the structural top/base), parallel to `columns`.
- **`VolumeBundle` = `model.volume_bundle(...)`** — the corner-point cell mesh
  (this is where the mesh builder lives now — moved DOWN from petekSim's
  `srs-core/mesh.rs` per the layer charter; peteksim retires its copy onto this
  bundle at the viewer wave).

**Contract conventions.** All areal layers share one regular `GridFrame`
(origin + spacing + `ncol=ni` / `nrow=nj`), reconstructed from the grid's
column-centroid lattice (the `xy↔ij` map — the same frame the property pipeline
uses); row-major `values[j*ncol + i]`; `f64::NAN` = undefined/outside (JSON
`null`). SI units, positive-down depth. The serialized **key structure** is the
cross-codebase seam — documented in `API.md`, version-tagged (`SCHEMA_VERSION`),
and locked by a schema-snapshot test per bundle.

**The world/local frame discipline (a standing seam rule).** The canonical
real-data configuration is a **LOCAL-origin cell lattice + a registered world
`Georef`** (the "F5" convention): a trace/spec arrives in **world** coordinates
(the view `GridFrame`), but a cell's ZCORN `corners` live in the grid's **local**
lattice. Any geometry that consumes both a spec point *and* the raw corners —
notably `intersection_bundle`'s `fence_edge_depths` areal clip — **must convert to
one frame first** (map the world point + direction into the local lattice via the
`WorldToLattice` affine, or the corners into world). Mixing frames does not error;
it silently produces garbage — a world line misses every local cell rectangle and
the section collapses to a flat centroid trace. This has now bitten **three** times
(the bundle-frame F5 zero-columns bug, the collocated-trend no-op, and the
section-edge centroid collapse), so it is a hard convention, not a caution.

  - **A miss is loud, not silent.** Where a frame-consistent computation cannot
    legitimately miss (e.g. a non-degenerate fence line through the cell it was
    matched to), the miss path is a `debug_assert` + a visible warning, not a quiet
    fallback. The single honest degenerate that keeps the centroid (`l == r`) is a
    zero-areal-extent trace (a single-point / vertical bore) — explicit and distinct.
  - **Test convention (mandatory): every frame-sensitive view/population test must
    carry a world-georeferenced variant** — a local-origin lattice + a world georef,
    spec in world coordinates, asserted against the local-frame image of the same
    geometry. An axis-aligned world fence is *not* sufficient (its single bounding
    slab clip is offset-invariant and hides the bug); use an **azimuthed/diagonal**
    trace. See `view::section` tests
    (`georef_world_fence_and_bore_edges_are_frame_invariant_and_dip`).

---

## 8. Contract history — ratified calls (was: open questions)

- **Q1 — Volumetrics home: RESOLVED, INVERTED (2026-07-03).**
  `decision_layer_charters` supersedes `decision_volumetrics_home_peteksim`: the
  static layer owns volumetrics AND static uncertainty (the industry split).
  `srs-volumetrics` + `srs-uncertainty` relocated here; `StaticModel` owns its
  volumes (§2); petekSim keeps `srs-pvt` for its dynamic work and the product
  facade. FVF crosses as a validated scalar input.
- **Q2 — The regeneration API: RATIFIED + LANDED** (§7a;
  `decision_staticmodel_regen_seam` with peteksim's four amendments).
- **Q3 — Draw construction: RESOLVED.** petekStatic defines the neutral
  `RealizationDraw`; whoever owns the sampler fills it (today that is also
  petekStatic — `srs-uncertainty` — but the type stays sampler-agnostic; no
  sampler dependency in `srs-model`'s seam types).
- **Q4 — Faults / zones depth (P5) do not change this contract.** Confirmed:
  the framework/zones/cube contracts absorb faults + multi-zone layering
  additively (`task_petekstatic_zones_faults`); `RealizationDraw` absorbs
  structural sampling via its `#[non_exhaustive]` + typed-Option design.

Open: kernel unification (`decision_gridder_kernel_unification` /
`task_petektools_natural_dip_boundary`) — until then §7b's type guard stands.

---

## 9. What exists today vs planned

**Exists (green):** `error` (`StaticError` + `#[from] GeoError`),
`srs-grid`, `srs-gridder` (`solve_surface`, `solve_surface_seeded` +
`KernelSurface`, `layer_grid`), `srs-petro`, `srs-wireframe`, `srs-data`,
`srs-volumetrics` (in-place + FVF types + range validation), `srs-uncertainty`
(distributions, SplitMix64, fallible P90/P50/P10), `srs-model` (`StaticModel`,
`StaticModelBuilder`, `StaticModelTemplate`, `RealizationDraw`, `ZoneTable`,
`Provenance`). The full pipeline runs here; petekSim's `srs-core` keeps a thin
`RefiningModel` facade over it.

**Exists (green), continued:** the per-property geostatistical pipeline (§3c,
`task_petekstatic_property_modelling`) — `PropertyPipeline` (upscale + per-layer
SGS + collocated cokriging) on both the builder and the template, with the
`LevelShift`/`Resimulate` MC modes (`decision_mc_composition`); the structured MC
driver + tornado (§7d, `task_peteksim_mc_structured` / `task_peteksim_tornado`) —
`run_structured_mc` / `McInputs` / `McResult` / `aggregate_field` / `tornado`.

**Planned:** real boundary rings
(`task_petekstatic_boundary_rings`, ready); multi-zone + faults + **per-zone**
geostatistical population (P5 `task_petekstatic_zones_faults`); `realize_into`
buffer recycling; a petekStatic-side PyO3 mirror (today Python access is via
petekSim's facade); 0.1 release (`task_petekstatic_release_0_1`).

---

## 10. Out-of-core — the backing-storage mode (`task_petekstatic_slab_streaming`)

Larger-than-memory models must build, populate, run MC, and answer volumetrics by
**spilling to disk**, not by dying (the P9 engine half; coordinator design
`petekSuite/dev-docs/designs/out-of-core-strategy.md`, rulings R2/R3/R4/R5). The
design is **budget-driven and loud**, and — critically — the in-core StaticModel
type keeps its API: **spilled is a backing-storage mode, not a new type.**

**The budget + the loud switch (R5).** The build/template API carries a declared
`MemoryBudget` (default: a documented fraction of physical RAM). `build()` compares
a live-set estimate (`live_set_bytes`: ZCORN f64 + cubes f64, ×2 for the warm/MC
state) against it. **Below → the in-core path, byte-identical** (zero behaviour
change; the golden/determinism suite is untouched and a generous-budget build is
bit-identical to the pre-budget one). **Above → the spilled path**, and the engine
**says so**: a `SpillNotice` on stderr naming the mode, budget, estimate, and store
path. Never an OOM kill; never a silent switch.

**The abstraction (the enum, cleanly).** A `StaticModel` carries an internal
`spill: Option<Arc<SpillBacking>>`. `None` is the in-core model — geometry + cubes
owned by the `Grid`, every accessor byte-identical to before. `Some(_)` is a spilled
model whose heavy per-cell arrays live in a memory-mapped petekTools `store` (the
R1 lane store); its `grid` is a unit placeholder. The volumetric surface
(`in_place`, `in_place_summary`, `bulk_volume`, the two-contact split) **dispatches
on the backing**: in-core reads the `Grid`; spilled **streams** the store one k-slab
at a time. Shared behind an `Arc` so the model stays `Clone`/`Send` (the mmap is
shared, not copied) and the store file is cleaned up when the last clone drops
(`Drop`), unless the caller detaches it (`with_spill_persist`). `grid()`/`property()`
on a spilled model return the placeholder — a spilled model is read through its
volumetric methods; the raw borrowed `&Grid` is the in-core representation only.

**The lane layout maps the grid's natural order (R1/R2).** ZCORN is already
k-slab-major (`zcorn[k·ni·nj·8 ..]`) and each cube is `cube[k·ni·nj ..]`, so a
k-window of any lane is **one contiguous zero-copy slice** — exactly the store's
chunked-along-k shape. ZCORN → a slab f32 lane; each cube → a slab f32 lane; COORD
(small, k-invariant) → a resident f64 flat lane. The spilled cell rebuild is
bit-parallel to the in-core vertical-lattice fast path except ZCORN is narrowed to
f32 (the sole parity delta).

**Precision + parity (R4, the honesty clause).** Storage lanes are **f32** at spill
scale — the out-of-core enabler and the MC bandwidth lever (halves the bytes on the
streaming summary read). So in-core↔spilled parity is **tolerance-based, not bit
parity**: measured on a 2500 m fixture, GRV/bulk are bit-exact, HCPV ≤ ~2e-9, and MC
oil ≤ ~3.1e-6 relative (larger because a near-contact cell can flip in/out under f32
centroid rounding) — asserted ≤ 1e-5, documented here. **Accumulations stay f64**
regardless; only storage lanes narrow. Bit-determinism holds **within** a mode
(sharded MC == serial at every worker count, in-core and spilled).

**MC never spills per-draw state (R3).** The structured-MC loop keeps its
one-reusable-model / realize → summary → discard shape. In the spilled mode each
draw realizes in-core into the one reusable model, is flushed to **one reused
per-shard store** (overwritten every draw — never a new file per draw, so no
per-draw disk accumulation), its summary streamed from the f32 lanes, then the
backing is detached (the in-core grid stays intact for the next `realize_into`).
`run_structured_mc_spilled` / `_parallel_spilled` prove the determinism +
stale-buffer contracts hold in the spilled mode.

**Slab-incremental build — true O(slab) build peak (v2, item 1).** The gridder
path now builds a spilled model **slab-incrementally**: a `StreamingLayering`
(srs-gridder) produces ZCORN one k-slab at a time from two interface planes (the
same f64 `boundary_depth` as the in-core path, narrowed to f32), and the builder
writes each ZCORN + constant-cube slab **straight into the store**
(`slab_mut_f32`), never materializing a whole in-core grid. The heap/anonymous
working set is **O(slab)** (a slab + two O(area) interface planes), so a model
whose in-core live set exceeds RAM can now be **built**. Bit-identical to
build-then-spill (same f64→f32 narrowing, same constant cubes). Measured
(`dev-docs/bench/scripts/rss_probe.sh`, 3.2M cells, `/usr/bin/time -l` max RSS):
in-core 90 B/cell (the whole f64 grid — unbuildable above RAM), v1 build-then-spill
135 B/cell (grid + store resident together), **v2 streaming 47 B/cell** (≈ the f32
store alone) — a 2.9× cut, and v2 sits *below* the in-core build. Eligibility: the
per-slab-safe population (constant priors / per-zone constant), collapse **off**
(the volume-conserving collapse pass is a whole-zone-column carry). Ineligible
spilled builds (logs/trend/SGS pipelines, or collapse) fall back to build-then-spill.

**v2 deferrals (honest).**
- **True O(slab) *resident* RSS** would need the petekTools `store` writer to shed
  written pages (`madvise(MADV_DONTNEED)`); today the mmap'd f32 store's written
  pages stay in the page cache, so total RSS is O(store_f32) = the on-disk model
  itself (file-backed, sheddable) + O(slab) heap. Raised to petekTools as
  `question_petektools_store_page_evict`.
- **Streaming SGS-pipeline population** (logs/trend/SGS builds spill via
  build-then-spill) and **writable-store MC realize** (spilled MC realizes in-core
  into the one reused model then flushes — R3: MC never spills per-draw state, so
  its peak is the bounded reused model, not a scaling wall) — both need per-slab SGS
  population + must preserve the `realize_into` budget/determinism contracts.
- Spilled `zone_stats` (arbitrary-property per-zone cube statistics) and spilled
  view-bundle windowing. (Spilled `in_place_by_zone` **shipped** in v2 — item 3.)

---

## References (suite planning graph)

- `task_relocate_refine_orchestration` (this wave) · `task_peteksim_mc_structured`
  (retargeted) · `task_peteksim_tornado` (retargeted) ·
  `task_petekstatic_zones_faults` · `task_petekstatic_property_modelling` ·
  `task_petekstatic_boundary_rings` · `task_petekstatic_release_0_1`
- `decision_layer_charters` (the 2026-07-03 re-scope) ·
  `decision_staticmodel_regen_seam` (the ratified seam) ·
  `decision_gridder_kernel_unification` · `decision_srs_core_seam_pathdeps`
- Specs: `convergent_gridder_spec`, `layer_interpolation_spec`,
  `log_upscaling_spec`, `data_to_wireframe_spec`, `grv_from_grid_spec`,
  `monte_carlo_volumetrics_spec`, `distribution_sampling_spec`
- Cross-library design: `petekSuite/dev-docs/designs/staticmodel-regen-seam.md`