peteksim 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. peteksim-0.1.0/.cargo/config.toml +9 -0
  2. peteksim-0.1.0/.github/workflows/ci.yml +84 -0
  3. peteksim-0.1.0/.github/workflows/release.yml +77 -0
  4. peteksim-0.1.0/.gitignore +29 -0
  5. peteksim-0.1.0/API.md +311 -0
  6. peteksim-0.1.0/CHANGELOG.md +128 -0
  7. peteksim-0.1.0/CLAUDE.md +95 -0
  8. peteksim-0.1.0/CONTRIBUTING.md +128 -0
  9. peteksim-0.1.0/Cargo.lock +1483 -0
  10. peteksim-0.1.0/Cargo.toml +84 -0
  11. peteksim-0.1.0/LICENSE +91 -0
  12. peteksim-0.1.0/Makefile +49 -0
  13. peteksim-0.1.0/PKG-INFO +20 -0
  14. peteksim-0.1.0/README.md +193 -0
  15. peteksim-0.1.0/SPEC.md +158 -0
  16. peteksim-0.1.0/VIEWER.md +83 -0
  17. peteksim-0.1.0/crates/srs-py/Cargo.toml +25 -0
  18. peteksim-0.1.0/crates/srs-py/src/facade/framework.rs +358 -0
  19. peteksim-0.1.0/crates/srs-py/src/facade/grid.rs +319 -0
  20. peteksim-0.1.0/crates/srs-py/src/facade/mod.rs +83 -0
  21. peteksim-0.1.0/crates/srs-py/src/facade/model.rs +713 -0
  22. peteksim-0.1.0/crates/srs-py/src/facade/project.rs +367 -0
  23. peteksim-0.1.0/crates/srs-py/src/facade/specs.rs +279 -0
  24. peteksim-0.1.0/crates/srs-py/src/facade/uncertainty.rs +374 -0
  25. peteksim-0.1.0/crates/srs-py/src/lib.rs +689 -0
  26. peteksim-0.1.0/crates/srs-py/src/viewer.rs +603 -0
  27. peteksim-0.1.0/crates/srs-py/tests/acceptance_render.mjs +85 -0
  28. peteksim-0.1.0/crates/srs-py/tests/conftest.py +32 -0
  29. peteksim-0.1.0/crates/srs-py/tests/test_acceptance.py +681 -0
  30. peteksim-0.1.0/crates/srs-py/tests/test_acceptance_v2.py +266 -0
  31. peteksim-0.1.0/crates/srs-py/tests/test_facade.py +395 -0
  32. peteksim-0.1.0/crates/srs-py/tests/test_peteksim.py +412 -0
  33. peteksim-0.1.0/crates/srs-py/tests/test_realshape.py +429 -0
  34. peteksim-0.1.0/crates/srs-py/tests/test_scatter_dedup.py +115 -0
  35. peteksim-0.1.0/crates/srs-py/tests/test_spec_conformance.py +187 -0
  36. peteksim-0.1.0/crates/srs-py/tests/test_synth_asset.py +609 -0
  37. peteksim-0.1.0/crates/srs-py/tests/test_viewer.py +192 -0
  38. peteksim-0.1.0/crates/srs-py/tests/test_volume_recut.py +93 -0
  39. peteksim-0.1.0/crates/srs-py/tests/test_wells_payload.py +180 -0
  40. peteksim-0.1.0/crates/srs-py/tests/test_zonation.py +230 -0
  41. peteksim-0.1.0/pyproject.toml +41 -0
  42. peteksim-0.1.0/python/peteksim/__init__.py +209 -0
  43. peteksim-0.1.0/python/peteksim/apply.py +458 -0
  44. peteksim-0.1.0/python/peteksim/specs/__init__.py +89 -0
  45. peteksim-0.1.0/python/peteksim/specs/asset.py +119 -0
  46. peteksim-0.1.0/python/peteksim/specs/base.py +173 -0
  47. peteksim-0.1.0/python/peteksim/specs/mc.py +159 -0
  48. peteksim-0.1.0/python/peteksim/specs/props.py +150 -0
  49. peteksim-0.1.0/python/peteksim/specs/settings.py +163 -0
  50. peteksim-0.1.0/python/peteksim/specs/structure.py +320 -0
  51. peteksim-0.1.0/python/peteksim/synth_asset.py +790 -0
  52. peteksim-0.1.0/rustfmt.toml +2 -0
  53. peteksim-0.1.0/src/core/charts/bins.rs +103 -0
  54. peteksim-0.1.0/src/core/charts/crossplot.rs +206 -0
  55. peteksim-0.1.0/src/core/charts/distribution.rs +122 -0
  56. peteksim-0.1.0/src/core/charts/mod.rs +171 -0
  57. peteksim-0.1.0/src/core/charts/regression.rs +97 -0
  58. peteksim-0.1.0/src/core/charts/tornado.rs +110 -0
  59. peteksim-0.1.0/src/core/data_adapter.rs +112 -0
  60. peteksim-0.1.0/src/core/facade/charts.rs +148 -0
  61. peteksim-0.1.0/src/core/facade/framework.rs +1042 -0
  62. peteksim-0.1.0/src/core/facade/grid.rs +806 -0
  63. peteksim-0.1.0/src/core/facade/mod.rs +29 -0
  64. peteksim-0.1.0/src/core/facade/model.rs +658 -0
  65. peteksim-0.1.0/src/core/facade/project/helpers.rs +413 -0
  66. peteksim-0.1.0/src/core/facade/project/mod.rs +643 -0
  67. peteksim-0.1.0/src/core/facade/spec.rs +242 -0
  68. peteksim-0.1.0/src/core/facade/uncertainty.rs +285 -0
  69. peteksim-0.1.0/src/core/facade/wells.rs +462 -0
  70. peteksim-0.1.0/src/core/facade/zonation.rs +180 -0
  71. peteksim-0.1.0/src/core/facade/zoned_mc.rs +700 -0
  72. peteksim-0.1.0/src/core/inplace.rs +199 -0
  73. peteksim-0.1.0/src/core/mod.rs +66 -0
  74. peteksim-0.1.0/src/core/model.rs +50 -0
  75. peteksim-0.1.0/src/core/refine.rs +374 -0
  76. peteksim-0.1.0/src/core/run.rs +134 -0
  77. peteksim-0.1.0/src/core/view.rs +113 -0
  78. peteksim-0.1.0/src/lib.rs +33 -0
  79. peteksim-0.1.0/src/pvt/fvf.rs +92 -0
  80. peteksim-0.1.0/src/pvt/mod.rs +8 -0
  81. peteksim-0.1.0/src/units/error.rs +39 -0
  82. peteksim-0.1.0/src/units/mod.rs +10 -0
  83. peteksim-0.1.0/view.sh +31 -0
@@ -0,0 +1,9 @@
1
+ # PyO3 builds `srs-py` as a cdylib that resolves CPython symbols at load time.
2
+ # Plain `cargo build`/`cargo test` (no maturin) must tell the macOS linker to
3
+ # defer those symbols, or the link fails with "_Py* not found". Maturin sets
4
+ # this itself; this config makes the bare cargo gate work too.
5
+ [target.x86_64-apple-darwin]
6
+ rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
7
+
8
+ [target.aarch64-apple-darwin]
9
+ rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"]
@@ -0,0 +1,84 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ env:
9
+ CARGO_TERM_COLOR: always
10
+
11
+ jobs:
12
+ rust:
13
+ name: cargo build / clippy / test
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: dtolnay/rust-toolchain@stable
18
+ with:
19
+ components: clippy, rustfmt
20
+ - uses: Swatinem/rust-cache@v2
21
+ - name: fmt
22
+ run: cargo fmt --all --check
23
+ - name: clippy (warnings = errors)
24
+ run: cargo clippy --workspace --all-targets -- -D warnings
25
+ - name: build
26
+ run: cargo build --workspace
27
+ - name: test
28
+ run: cargo test --workspace
29
+
30
+ python:
31
+ name: maturin develop + import
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@v4
35
+ - uses: dtolnay/rust-toolchain@stable
36
+ - uses: actions/setup-python@v5
37
+ with:
38
+ python-version: "3.11"
39
+ - name: install maturin
40
+ run: pip install maturin
41
+ - name: build + import smoke test
42
+ run: |
43
+ maturin develop -m crates/srs-py/Cargo.toml
44
+ python -c "import peteksim; print(peteksim.version())"
45
+
46
+ wheels:
47
+ # Build the abi3 wheel and import + run it across a Python matrix so a
48
+ # toolchain regression (e.g. a pyo3 that can't link a new CPython — the
49
+ # 0.24-vs-3.14 blocker) is caught here, not by users. abi3-py39 means one
50
+ # wheel spans 3.9+, but we still build AND import on each interpreter so a
51
+ # per-version link/ABI break surfaces.
52
+ name: wheel build + run (py ${{ matrix.python-version }})
53
+ runs-on: ubuntu-latest
54
+ strategy:
55
+ fail-fast: false
56
+ matrix:
57
+ # 3.9 = oldest abi3 floor; through the latest stable CPython.
58
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ - uses: dtolnay/rust-toolchain@stable
62
+ - uses: Swatinem/rust-cache@v2
63
+ - uses: actions/setup-python@v5
64
+ with:
65
+ python-version: ${{ matrix.python-version }}
66
+ allow-prereleases: true
67
+ - name: install maturin
68
+ run: pip install maturin
69
+ - name: build release wheel
70
+ run: maturin build --release -m crates/srs-py/Cargo.toml --out dist
71
+ - name: install wheel + run run_box_model
72
+ run: |
73
+ pip install --find-links dist --no-index peteksim
74
+ python - <<'PY'
75
+ import peteksim
76
+ r = peteksim.run_box_model(
77
+ area_acres=(80, 100, 130), gross_height_ft=(40, 50, 65),
78
+ porosity=0.25, net_to_gross=0.8, water_saturation=0.3, fvf=1.25,
79
+ fluid="oil", contact_depth_ft=9000, realizations=2000, seed=1,
80
+ )
81
+ assert r.p90 < r.p50 < r.p10, (r.p90, r.p50, r.p10)
82
+ assert len(r.samples) == r.realizations == 2000
83
+ print("peteksim", peteksim.version(), "ok:", r)
84
+ PY
@@ -0,0 +1,77 @@
1
+ name: Release
2
+
3
+ # Tag-triggered PyPI publish (v* tags) via OIDC trusted publishing — no secret.
4
+ # The `peteksim` wheel is built from crates/srs-py (its pyproject.toml). The
5
+ # crates.io publish of the consolidated `peteksim` library crate is done from the
6
+ # local runbook (ordered, cross-repo deps), not here.
7
+ #
8
+ # Configure a pending publisher on PyPI: project `peteksim`, owner kkollsga, repo
9
+ # peteksim, workflow release.yml, environment pypi.
10
+ #
11
+ # NOTE: before pushing a v* tag, the sibling path dep on `petekstatic`
12
+ # (`../petekStatic` in the root Cargo.toml [workspace.dependencies]) must resolve
13
+ # from crates.io — its version pin (0.1.0) does; by release time the upstream
14
+ # family crates are all published so the registry pins resolve on the CI runner.
15
+ on:
16
+ push:
17
+ tags: ["v*"]
18
+ workflow_dispatch:
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ sdist:
25
+ name: build sdist
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: PyO3/maturin-action@v1
30
+ with:
31
+ command: sdist
32
+ args: --out dist
33
+ working-directory: crates/srs-py
34
+ - uses: actions/upload-artifact@v4
35
+ with:
36
+ name: wheels-sdist
37
+ path: crates/srs-py/dist
38
+
39
+ wheels:
40
+ name: build wheels ${{ matrix.runner }} ${{ matrix.target }}
41
+ runs-on: ${{ matrix.runner }}
42
+ strategy:
43
+ fail-fast: false
44
+ matrix:
45
+ include:
46
+ - { runner: ubuntu-latest, target: x86_64 }
47
+ - { runner: ubuntu-latest, target: aarch64 }
48
+ - { runner: macos-14, target: universal2-apple-darwin }
49
+ - { runner: windows-latest, target: x64 }
50
+ steps:
51
+ - uses: actions/checkout@v4
52
+ - uses: PyO3/maturin-action@v1
53
+ with:
54
+ target: ${{ matrix.target }}
55
+ args: --release --out dist
56
+ sccache: "true"
57
+ manylinux: auto
58
+ working-directory: crates/srs-py
59
+ - uses: actions/upload-artifact@v4
60
+ with:
61
+ name: wheels-${{ matrix.runner }}-${{ matrix.target }}
62
+ path: crates/srs-py/dist
63
+
64
+ publish-pypi:
65
+ name: PyPI (trusted publishing)
66
+ needs: [sdist, wheels]
67
+ runs-on: ubuntu-latest
68
+ environment: pypi
69
+ permissions:
70
+ id-token: write
71
+ steps:
72
+ - uses: actions/download-artifact@v4
73
+ with:
74
+ pattern: wheels-*
75
+ path: dist
76
+ merge-multiple: true
77
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,29 @@
1
+ # Local working folders + internal tooling (not part of the published library)
2
+ /dev-docs/
3
+ /inbox/
4
+ /research/
5
+ /tools/
6
+ /notes/
7
+ /.claude/
8
+ /.mcp.json
9
+ /CLAUDE.local.md
10
+ /Makefile.local
11
+
12
+ # Python env / caches
13
+ /.venv-srs/
14
+ **/__pycache__/
15
+ *.pyc
16
+
17
+ # Build artifacts
18
+ /target/
19
+ **/*.rs.bk
20
+ *.tmp
21
+
22
+ # maturin editable-install artifact (built extension placed in the source pkg)
23
+ crates/srs-py/python/peteksim/_core*.so
24
+
25
+ # macOS
26
+ .DS_Store
27
+ **/.DS_Store
28
+ dist/
29
+ dist-launch/
peteksim-0.1.0/API.md ADDED
@@ -0,0 +1,311 @@
1
+ # petekSim — locked public API (`peteksim`)
2
+
3
+ > **This file is the contract.** The `peteksim` wheel must expose exactly these
4
+ > names and signatures (arguments, defaults, return shapes). Bodies are the
5
+ > implementer's; the *surface* is fixed. Changing a signature here requires
6
+ > sign-off (coordinator + any downstream consumer for a cross-library seam) and an
7
+ > edit to this file — the code must never silently drift from it. See
8
+ > [SPEC.md](SPEC.md) for the design constitution.
9
+
10
+ **Rust is canonical; the Python surface mirrors it.** The compute lives in the Rust
11
+ core (`peteksim._core`, over the petekStatic/petekIO/petekTools crates); this
12
+ document specifies the **Python facade** — the product surface. The v2 spec layer is
13
+ a thin Python facade over `_core`; a handful of names (`Project`, `collocated`)
14
+ override the raw `_core` binding with a v2 wrapper.
15
+
16
+ **Conventions:**
17
+ - **SI / metric everywhere** (`decision_si_units_standard`): areas km², lengths /
18
+ depths **metres, positive-down**, volumes Sm³ (reported MSm³ oil / bcm gas), GRV
19
+ mcm (10⁶ m³), FVF dimensionless Rm³/Sm³. Imperial is caller-side, never a default.
20
+ - **A spec holds NAMES, resolved at apply.** A spec value is declarative and
21
+ project-independent: it references horizons / surfaces / picks / properties by
22
+ name, and those names are resolved against a loaded project only at the apply
23
+ moment (`geom.build`, `grid.model`, `model.zoned_uncertainty`). Resolution errors
24
+ are loud and name **both** the missing project object and the spec entry.
25
+ - **Value semantics.** Every spec supports `to_dict()` / `from_dict()` /
26
+ `ps.spec_from_dict()`, value `==` + `hash`, `.replace(...)` derivation, and a
27
+ domain-table `repr`. A scenario is a savable, diffable file.
28
+ - **`import peteksim as ps`** throughout.
29
+
30
+ ---
31
+
32
+ ## Module
33
+
34
+ ```python
35
+ ps.version() -> str # the peteksim/crate version string
36
+ ```
37
+
38
+ **Exceptions** (both `from peteksim import ...`):
39
+
40
+ ```python
41
+ ps.NotYetSupported(NotImplementedError) # a spec field serializes but the engine
42
+ # capability has not landed — raised loudly
43
+ # at apply, naming the carrying task
44
+ ps.ApplyError(ValueError) # a spec could not resolve against the
45
+ # project (missing name / illegal combo);
46
+ # the message names object AND spec entry
47
+ ```
48
+
49
+ ## Project & geometry
50
+
51
+ ```python
52
+ ps.Project.load(path: str, crs: str | None = None,
53
+ aliases: dict[str, str] | None = None,
54
+ settings: ps.LoadSettings | None = None) -> Project
55
+ # Load a project tree. Pass settings=ps.LoadSettings(...) (v2) OR the legacy
56
+ # crs=/aliases= kwargs — not both (ApplyError on both).
57
+
58
+ proj.grid_geometry(cell, extent=None, orient: float = 0.0) -> GridGeometry
59
+ # Open the v2 build. `cell` is the output cell size (scalar or (dx, dy);
60
+ # anisotropic dx != dy -> NotYetSupported). `orient` must be 0 (rotation is
61
+ # NotYetSupported). `extent` is advisory.
62
+
63
+ # v1 / shared project accessors (forwarded unchanged):
64
+ proj.inventory() -> Inventory # what loaded + what was skipped-with-reason
65
+ proj.wells() -> Wells # .ids(), .heads() -> [(id, x, y)]
66
+ proj.surface(name: str) -> Surface # .value_at(x, y), .values_at(points)
67
+ proj.tops -> Tops # .pick(name, wells=None) -> TopsPick
68
+ proj.crossplot_bundle(x, y, wells=None, color_by="well",
69
+ x_log=False, y_log=False, regression=False)
70
+ proj.framework(...) -> Framework # DEPRECATED (v1 chain); see "v1" below
71
+
72
+ geom.build(horizons: Horizons, subzones: Subzones | None = None,
73
+ layering: Layering | None = None, collapse_negative: bool = True,
74
+ outline: str = "ModelEdge", min_thickness_m: float = 0.0,
75
+ ties: TieSettings | None = None,
76
+ gridding: Gridding | None = None) -> Grid
77
+ # Freeze geometry + structure specs. Resolves horizon names early (loud on a
78
+ # miss); the engine build is deferred to grid.model so contacts + props land
79
+ # in one construction. `ties`/`gridding` default to the ones on `horizons`.
80
+ ```
81
+
82
+ ## Structure specs
83
+
84
+ ```python
85
+ ps.Horizons(*rows: HorizonRow, zones=None, ties=None, gridding=None) -> Horizons
86
+ # The ordered stratigraphic column (top->down) + the zones between horizons;
87
+ # zone i sits between rows[i] and rows[i+1]. .replace("H1"|glob, surface=...)
88
+ # derives a changed column.
89
+ ps.hz(name: str, surface: str | None = None, tie: str | None = None,
90
+ sd: float = 0.0, vgm: tuple[str, float] | None = None) -> HorizonRow
91
+ # One horizon. `surface` defaults to `name` (a loaded point-set -> Scatter, a
92
+ # loaded grid -> Mapped). `tie` names the pick set (defaults to `name`).
93
+ # `sd`(m) + `vgm`=(model, range) declare the structural-uncertainty field
94
+ # (applied through the zoned MC path).
95
+
96
+ ps.Subzones(mapping: dict[str, Split] | None = None) -> Subzones # per-zone splits
97
+ ps.splits(*entries, conformity: str = "proportional") -> Split
98
+ # each entry: a name, or (name, dict(surface=, tie=)).
99
+ # conformity: "proportional" | "follow_top" | "follow_base".
100
+ ps.zone(name: str, color: str | None = None) -> ZoneColor # a zone's colour
101
+
102
+ ps.Layering(dz: float | None = None, nk: int | None = None,
103
+ min_cell: float | None = None) -> Layering
104
+ # Layer allocation; dz XOR nk set the default. .replace("Z*", dz=0.5) adds a
105
+ # per-glob override. min_cell(m) = sub-threshold cell-collapse floor.
106
+
107
+ ps.Contacts(mapping: dict[str, dict[str, float]] | None = None) -> Contacts
108
+ # Per-zone fluid contacts by glob: {"Z4": dict(goc=.., fwl=..), "Z2": dict(owc=..)}.
109
+ # A zone with no matching entry is contactless. .replace("Z4", fwl=...) derives.
110
+ ```
111
+
112
+ ## Settings specs (the HOW objects)
113
+
114
+ ```python
115
+ ps.TieSettings(method: str = "convergent", radius_m: float | None = None) -> TieSettings
116
+ # method: "convergent" (control-replacement) | "radius" (tie-locality; radius_m).
117
+
118
+ ps.Gridding(fidelity_m=None, extrapolation: Extrapolation | None = None,
119
+ collapse: bool = True, min_cell: float | None = None) -> Gridding
120
+ ps.decay_to_flat(range_m: float) -> Extrapolation
121
+ ps.flat() -> Extrapolation
122
+ ps.nearest() -> Extrapolation
123
+
124
+ ps.Run(memory_budget: int | None = None, workers: int = 0) -> Run
125
+ # Run resources. memory_budget (BYTES) forwards to the engine out-of-core
126
+ # switch (loud spill, never an OOM kill); workers shards the MC realize loop.
127
+
128
+ ps.LoadSettings(crs: str | None = None,
129
+ aliases: dict[str, str] | None = None) -> LoadSettings
130
+ ps.ViewSettings(property=None, open_browser: bool = True,
131
+ port: int = 0, block: bool = False) -> ViewSettings
132
+ ```
133
+
134
+ ## Property specs
135
+
136
+ ```python
137
+ ps.Props(*items: Prop) -> Props # the set applied at grid.model(props=)
138
+ ps.Prop(name: str, zone: str | None = None, upscale_method: str = "arithmetic",
139
+ net_only: bool = False, net_cutoff: float = 0.5,
140
+ propagate: Propagate | None = None) -> Prop
141
+ # One cube's population: upscale (from wells) then propagate (SGS). `zone`
142
+ # scopes the pipe to one zone of a stack.
143
+ ps.Propagate(variogram: Variogram = Variogram(), seed: int = 1,
144
+ max_neighbours: int | None = None, radius_m: float | None = None,
145
+ trend: CollocatedTrend | None = None, mode: str = "level_shift",
146
+ allow_mean_fill: bool = False) -> Propagate
147
+ # mode: "level_shift" | "resimulate".
148
+ ps.variogram(model: str, range_m: float, sill: float = 1.0,
149
+ nugget: float = 0.0) -> Variogram
150
+ # model: "spherical" | "exponential" | "gaussian".
151
+ ps.collocated(surface, corr: float, as_depth: bool = False)
152
+ # surface a NAME (str) -> a CollocatedTrend spec resolved at apply (the v2 form).
153
+ # surface a _core.Surface -> the v1 eager trend (DEPRECATED — bind by name).
154
+ ```
155
+
156
+ ## Monte-Carlo specs
157
+
158
+ ```python
159
+ ps.Mc(porosity=None, net_to_gross=None, water_saturation=None,
160
+ fvf=None, gas_fvf=None, contacts=None, goc=None,
161
+ per_zone: dict[str, Mc] | None = None,
162
+ settings: McSettings | None = None, n: int = 10_000, seed: int = 42) -> Mc
163
+ # Property fields take a scalar sd / ps.shift(...) / ps.dist(...); contact
164
+ # fields (contacts = lower FWL/OWC, goc) take a scalar sd_m / ps.pick_spread(...).
165
+ # per_zone overrides by zone. Auto-routes to zoned_uncertainty on a zoned model,
166
+ # else uncertainty.
167
+ ps.McSettings(lo_pct: float = 10.0, hi_pct: float = 90.0, workers: int = 0) -> McSettings
168
+ ps.shift(sd: float) -> Uncertain # a zero-mean level shift
169
+ ps.dist(kind: str, *params: float) -> Uncertain
170
+ # kind: "level_shift"(sd) | "normal"(mean,sd) | "lognormal"(mean,sd) |
171
+ # "uniform"(lo,hi) | "triangular"(lo,mode,hi) |
172
+ # "truncated_normal"(mean,sd,lo,hi).
173
+
174
+ # Distribution builders (the _core samplers, for the v1 kwargs and Uncertain.resolve):
175
+ ps.normal(mean, sd); ps.lognormal(mean, sd); ps.uniform(lo, hi)
176
+ ps.triangular(lo, mode, hi); ps.truncated_normal(mean, sd, lo, hi)
177
+ ps.level_shift(sd); ps.pick_spread(sd_m) # .clamped(lo, hi) on samplers
178
+ ```
179
+
180
+ ## Chart specs + the asset bundle
181
+
182
+ ```python
183
+ ps.Crossplot(x, y, wells=(), color_by="well", x_log=False, y_log=False,
184
+ regression=False) -> Crossplot # applied on proj.crossplot_bundle
185
+ ps.Tornado(base=None, units: str = "MSm³", fold_count: int = 8) -> Tornado
186
+ ps.Distribution(gas=False, zone=None, name=None) -> Distribution
187
+
188
+ ps.AssetSpec(name="", load=None, horizons=None, subzones=None, layering=None,
189
+ contacts=None, ties=None, gridding=None, props=None, mc=None,
190
+ run=None, view=None) -> AssetSpec
191
+ # A whole modelling scenario as one durable value; every field is a spec, so
192
+ # asset.to_dict() is a total, savable scenario file.
193
+ ```
194
+
195
+ ## The apply moments
196
+
197
+ ```python
198
+ grid.model(props: Props | None = None, con: Contacts | dict | None = None, *,
199
+ fluid: str = "oil", fvf: float = 1.25, gas_fvf: float | None = None,
200
+ wells=None, run: Run | None = None, sugar_cube: bool = False) -> Model
201
+ # Execute the build with the property + contact specs -> a populated Model.
202
+ # Zoned (Horizons.zones present) -> contacts fold into the zonation; non-zoned
203
+ # -> contacts go to the model. `run` carries memory_budget + workers.
204
+
205
+ model.zoned_uncertainty(mc: Mc | None = None, **legacy) -> ZonedUncertainty
206
+ model.uncertainty(mc: Mc | None = None, **legacy) -> Uncertainty
207
+ model.mc(spec: Mc) # auto-routes on model.is_zoned()
208
+ # Pass EITHER a ps.Mc spec OR the legacy kwargs — not both. Legacy kwargs emit
209
+ # a DeprecationWarning.
210
+ ```
211
+
212
+ ## The result surface — `Model`
213
+
214
+ ```python
215
+ model.summary() -> dict # STOIIP / GIIP [MSm³], GRV [mcm]
216
+ model.in_place_by_zone() -> dict # {"zones": [...], "total": {...}}
217
+ model.zone_stats(property: str) -> list # per-zone count/mean/min/max
218
+ model.well_tie_residuals() -> list # [{well, horizon, measured_depth_m,
219
+ # model_depth_m, residual_m}]
220
+ model.property_names() -> list[str]
221
+ model.is_zoned() -> bool
222
+ model.well_ids() -> list[str]
223
+ model.warnings() -> list
224
+
225
+ # viewer bundles + render:
226
+ model.map_bundle(property=None, k_slice=None) -> dict
227
+ model.intersection_bundle(line=None, well=None, property=None) -> dict
228
+ model.volume_bundle(property=None) -> dict
229
+ model.wells_bundle() -> dict
230
+ model.view(settings: ViewSettings | None = None, *, open_browser=True, port=0,
231
+ block=False, property=None, lines=None, charts=None) -> str # URL
232
+ model.save_view(path: str, property=None, lines=None, charts=None) -> None
233
+ ```
234
+
235
+ ## The result surface — uncertainty
236
+
237
+ ```python
238
+ # flat (non-zoned) — model.uncertainty(...):
239
+ unc.stoiip -> dict # {p90, p50, p10, mean, *_msm3, samples}
240
+ unc.giip -> dict
241
+ unc.tornado() -> list # ranked input swings
242
+ unc.tornado_bundle(base=None, units="MSm³", fold_count=8) -> dict
243
+ unc.distribution_bundle(gas=False, name=None) -> dict
244
+ unc.view(open_browser=True, port=0, block=False, charts=None) -> str
245
+ unc.save_view(path, charts=None) -> None
246
+
247
+ # zoned — model.zoned_uncertainty(...):
248
+ zunc.total -> dict # {"stoiip": {..}, "giip": {..}, "two_contact": bool}
249
+ zunc.zones -> list # [{"zone", "stoiip", "giip", "two_contact"}, ...]
250
+ zunc.distribution_bundle(gas=False, zone=None, name=None) -> dict
251
+ ```
252
+
253
+ ## Spec value semantics (shared)
254
+
255
+ ```python
256
+ spec.to_dict() -> dict # tagged with "spec"; JSON-able
257
+ Spec.from_dict(d) -> Spec # per class
258
+ ps.spec_from_dict(d) -> Spec # dispatch on the "spec" tag
259
+ ps.registered_specs() -> tuple[type, ...]
260
+ spec.replace(**changes) -> Spec # collection specs also accept a leading
261
+ # name/glob: hz.replace("H1", surface=...)
262
+ spec == other; hash(spec); repr(spec) # value equality; domain-table repr
263
+ ```
264
+
265
+ ## The analytic box model
266
+
267
+ ```python
268
+ ps.run_box_model(area_km2, gross_height_m, porosity, net_to_gross,
269
+ water_saturation, fvf, *, fluid="oil", top_m=0.0,
270
+ contact_m=math.inf, ni=10, nj=10, nk=5,
271
+ realizations=10_000, seed=1) -> ModelResult
272
+ # Each volumetric input: a number (constant) | (min, mode, max) triangular |
273
+ # {"normal"|"lognormal"|"uniform"|"triangular": [...]} tagged dict.
274
+ # contact_m is REQUIRED and finite (a non-finite contact is a loud error).
275
+
276
+ m.samples -> list[float] # per-realization in-place [Sm³]
277
+ m.summary_msm3 -> dict # {p90, p50, p10, mean} in MSm³ (oil)
278
+ m.summary_bcm -> dict # the same in bcm (gas)
279
+ m.scaled_summary(per: float) -> dict
280
+ m.view(open_browser=True, port=0, block=False, property=None) -> str
281
+ m.save_view(path, property=None) -> None
282
+ m.save_json(path, property=None) -> None
283
+ repr(m) # P90 / P50 / P10 / mean / deterministic [Sm³]
284
+
285
+ ps.Model(area_km2, gross_height_m, *, ni=20, nj=20, nk=8, top_m=1500.0,
286
+ contact_m=math.inf, porosity=0.25, net_to_gross=0.8,
287
+ water_saturation=0.3, fvf=1.25, fluid="oil") # a structured box
288
+ sm.add_control(ip: int, jp: int, depth_m: float) -> None # a structural high
289
+ sm.solve() -> Refined
290
+ sm.view(...) ; sm.save_view(...) ; sm.save_json(...)
291
+ ```
292
+
293
+ ## Aggregation + standalone charts
294
+
295
+ ```python
296
+ ps.aggregate(segments, correlation: str = "independent") -> list[float]
297
+ # correlation: "independent" | "comonotonic". Sum per-segment realization
298
+ # vectors under an explicit dependence assumption.
299
+ ps.distribution_bundle(segments, aggregate=None, names=None, gas=False,
300
+ title=None) -> dict
301
+ ```
302
+
303
+ ## v1 (deprecated)
304
+
305
+ The v1 eight-call staged chain — `proj.framework(horizons=[...])` → `set_zones` /
306
+ `set_zonation` / `set_layering` / `set_well_ties` → `build_grid` → per-property
307
+ `grid.property(...).upscale(...).propagate(...)` → `grid.model(contacts=...)` →
308
+ `model.uncertainty(...)` / `model.zoned_uncertainty(...)` → `mc.tornado()` — remains
309
+ callable for a **two-minor** window and emits a `DeprecationWarning`. New code uses
310
+ the declarative v2 surface above. The runnable staged example is
311
+ `examples/staged_build.py`.
@@ -0,0 +1,128 @@
1
+ # Changelog
2
+
3
+ All notable changes to petekSim are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres
5
+ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ### Changed — packaging (internal, behaviour-neutral)
10
+
11
+ - **Consolidated the three published library crates into one.** `srs-units`,
12
+ `srs-pvt` and `srs-core` were merged into a single crate, **`peteksim`**
13
+ (0.1.0), at the repo root. Their boundaries are preserved as modules —
14
+ `peteksim::{units, pvt, core}` — and the headline Rust API (`run_model`, the
15
+ appraisal facade types, the `Distribution` seam, the view bundles) is
16
+ re-exported at the crate root. File moves + mechanical path rewrites only; no
17
+ logic changed. The workspace keeps its `crates/srs-py` member (the `peteksim`
18
+ wheel source), which now binds the `peteksim` crate; the published PyPI wheel
19
+ surface (`peteksim`) is unchanged.
20
+ - **Repointed the geomodel dependency at petekStatic's consolidated crate.** The
21
+ former per-crate path deps on petekStatic's `srs-*` crates are now a single
22
+ `petekstatic = "0.1.0"` (path `../petekStatic`); the `srs_model::` /
23
+ `srs_grid::` / `srs_wireframe::` (…) imports rewrote to
24
+ `petekstatic::{model, grid, wireframe, …}::`. petekio / petekTools pins
25
+ unchanged.
26
+
27
+ ## [0.1.0] - 2026-07-05
28
+
29
+ First public release of **`peteksim`** — the Python-facing appraisal toolkit: a
30
+ pure-Rust reservoir core with thin bindings that presents the whole
31
+ subsurface-modelling stack (ingest → geomodel → volumetrics → uncertainty) as one
32
+ facade. From a Petrel export to a STOIIP P-curve in a handful of calls.
33
+
34
+ All inputs and outputs are **SI/metric**: areas in km², lengths/depths in metres
35
+ (positive-down), volumes in Sm³ (reported in MSm³ for oil, bcm for gas), GRV in
36
+ mcm (10⁶ m³), FVF as dimensionless Rm³/Sm³. Imperial is opt-in on your side, never
37
+ a default.
38
+
39
+ ### Added — the declarative modelling API (v2, the primary surface)
40
+
41
+ - **Specs applied at explicit moments.** A model is built from immutable, declarative
42
+ **spec** values that say WHAT (`Horizons`, `Subzones`, `Layering`, `Contacts`,
43
+ `Props`, `Mc`) or HOW (`TieSettings`, `Gridding`, `Run`, `LoadSettings`,
44
+ `ViewSettings`). A spec holds **names**, not project objects — so it is
45
+ project-independent, reusable across re-exports and synthetic assets. It is applied
46
+ at three explicit moments: `proj.grid_geometry(...)` → `geom.build(...)` →
47
+ `grid.model(...)` → `model.zoned_uncertainty(...)`.
48
+ - **Loud at apply.** Errors when a spec is applied name **both** the missing project
49
+ object and the spec entry. A spec field the engine cannot yet honour raises
50
+ `NotYetSupported` (never a silent no-op); an unresolvable name raises `ApplyError`.
51
+ - **Value semantics.** Every spec round-trips through a dict (`to_dict` / `from_dict`
52
+ / `ps.spec_from_dict`), compares and hashes by value, derives with `.replace(...)`,
53
+ and pretty-prints as its own domain table — so a scenario is a savable, diffable
54
+ file. `ps.AssetSpec` bundles a whole scenario (load + structure + props + mc) into
55
+ one durable value.
56
+ - **Structure specs:** `ps.Horizons(ps.hz(...), zones=[...])` (the ordered
57
+ stratigraphic column + the zones between horizons; per-row well ties and structural
58
+ uncertainty via `ps.hz(tie=, sd=, vgm=)`), `ps.Subzones` / `ps.splits` (intra-zone
59
+ splits + conformity), `ps.Layering(dz= | nk=)`, `ps.Contacts({zone: {...}})`, and
60
+ `ps.zone(name, color=)`.
61
+ - **Property specs:** `ps.Props(ps.Prop("PORO", net_only=True, propagate=...))` — a
62
+ visible per-property pipeline of well upscaling then SGS propagation
63
+ (`ps.Propagate` + `ps.variogram(...)` + optional `ps.collocated(name, corr=)` trend,
64
+ `level_shift` / `resimulate` MC modes).
65
+
66
+ ### Added — volumetrics, zoned Monte-Carlo, and P-curves
67
+
68
+ - **In-place volumes.** `grid.model(...)` returns a populated model; `model.summary()`
69
+ reports STOIIP / GIIP (MSm³) and GRV (mcm). Two-contact columns (gas cap + oil rim)
70
+ split automatically from the contact picks.
71
+ - **Zoned uncertainty.** `model.zoned_uncertainty(ps.Mc(...))` runs Monte-Carlo over
72
+ static-model realizations — per-zone contact draws and per-zone property level shifts
73
+ — and returns per-zone and total **P-curves** (`p90` / `p50` / `p10` / `mean`, with
74
+ `*_msm3` reporting scales) plus the full per-realization sample vectors.
75
+ - **Multi-zone stacks.** A multi-horizon stack unlocks per-zone layering, per-zone
76
+ contacts (a contactless zone contributes GRV with zero hydrocarbon), and per-zone
77
+ property pipelines. `model.in_place_by_zone()`, `model.zone_stats(prop)`, and
78
+ `model.well_tie_residuals()` report the breakdown; well ties tie each horizon to its
79
+ tops picks.
80
+ - **Tornado + aggregation.** A flat `model.uncertainty(...)` result exposes
81
+ `.tornado()` (ranked input swings); `ps.aggregate([...], correlation=...)` sums
82
+ segment realizations under an explicit dependence assumption.
83
+
84
+ ### Added — the analytic box model
85
+
86
+ - **`ps.run_box_model(...)`** — a quick STOIIP/GIIP estimate with Monte-Carlo on each
87
+ volumetric input (a constant, a `(min, mode, max)` triangular, or a tagged
88
+ `{"normal"|"lognormal"|"uniform"|"triangular": [...]}` distribution). Returns
89
+ P90/P50/P10/mean/deterministic plus the full sample vector.
90
+ - **`ps.Model(...)`** — a structured box with real structural relief built in code
91
+ (`add_control(i, j, depth_m)` seeds a high), for a first look before a full project.
92
+
93
+ ### Added — the browser viewer
94
+
95
+ - **`model.view()`** opens a tabbed, bundle-driven inspection viewer: **Map** (areal
96
+ rasters, outline, contact subcrop masks, well markers, draw-a-fence to cut a
97
+ section), **Intersection** (the vertical cross-section with horizon + contact traces
98
+ and the bore path), and **Volume** (the corner-point mesh in three.js with property
99
+ colouring, threshold, zone toggles, i/j/k clip planes). `view()` is non-blocking
100
+ (prints a URL and returns).
101
+ - **`model.save_view("m.html")`** writes ONE self-contained HTML file that opens off
102
+ `file://` with all data + JS inlined — no server, no network (confidential-data
103
+ safe). Bundle accessors `map_bundle` / `intersection_bundle` / `volume_bundle`
104
+ return the JSON directly. The renderer is petekTools' horizontal `petektools.viewer`
105
+ unit, which `peteksim` consumes.
106
+
107
+ ### Added — resources and out-of-core
108
+
109
+ - **`ps.Run(memory_budget=<bytes>, workers=N)`** carries the run resources: `workers`
110
+ shards the MC realize loop; `memory_budget` forwards to the engine's out-of-core
111
+ switch (a larger-than-memory model spills to disk with a loud notice, never an OOM
112
+ kill).
113
+
114
+ ### Deprecated
115
+
116
+ - **The v1 eight-call model-build chain** (`proj.framework(...)` → `set_zones` →
117
+ `build_grid` → per-property `upscale`/`propagate` → `grid.model` → `uncertainty` →
118
+ `tornado`) is deprecated in favour of the declarative v2 API, with a two-minor
119
+ window. It keeps working and emits a `DeprecationWarning`; new code should use
120
+ `proj.grid_geometry(...).build(ps.Horizons(...))`.
121
+
122
+ ### Licensing
123
+
124
+ - petekSim is licensed under the **Business Source License 1.1** (BUSL-1.1); each
125
+ released version converts to Apache-2.0 four years after publication. See `LICENSE`.
126
+
127
+ [Unreleased]: https://github.com/kkollsga/peteksim/compare/v0.1.0...HEAD
128
+ [0.1.0]: https://github.com/kkollsga/peteksim/releases/tag/v0.1.0