Skip to content

Simulator: MiniRes, a pure-Python two-phase forward model - #167

Open
patnr wants to merge 2 commits into
Python-Ensemble-Toolbox:mainfrom
patnr:minires-simulator
Open

patnr wants to merge 2 commits into
Python-Ensemble-Toolbox:mainfrom
patnr:minires-simulator

Conversation

@patnr

@patnr patnr commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Adds MiniRes -- a 2D, two-phase (water/oil), immiscible
TPFA reservoir simulator -- as a forward simulator for PET, plus a tutorial that runs it.

Why

  1. A physical forward model that needs nothing installed. Pure Python,
    pip-installable: no OPM/Eclipse binary, no licence, no deck, no En_* scratch folders,
    no file I/O at all. Tutorials, tests and CI can exercise actual two-phase flow anywhere,
    Colab included. Until now the only models that ran unattended were analytical ones.
  2. Fast enough for real ensembles. ~20 ms per member on 16x16 x 12 steps; the bundled
    tutorial does 100 members over 3 ESMDA steps in ~10 s.
  3. The first field-scale case for the adjoint path. PET reads self.sim adjoints in
    enkf/enrml/esmda, but no in-repo simulator supplies them. MiniRes differentiates
    its own time stepper (a hand-derived adjoint, minires.tlm), giving d(datum)/d(log K)
    over the whole grid at about one simulation per backward sweep. Checked here against
    central differences to ~1e-7 relative.
  4. Likewise for the multilevel machinery: setup_fwd_run(level=...) becomes a real
    fidelity switch (grid, dt), not a stub.
  5. A reference case for developing schemes, localization and QA/QC -- heterogeneous,
    nonlinear, with a known truth, reproducible on any machine.
  6. It costs PET almost nothing: one optional extra, one module, no change to any
    existing code path. The ForwardSimulator contract already fitted; nothing in the
    ensemble or the schemes was touched.

What's here

  • src/simulator/minires.py -- the wrapper. [simulator.model] is passed to
    minires.ResSim as it stands (grid, porosity, wells), less the permeability, which is
    what the ensemble state supplies, one field per member. Each member runs on its own
    deepcopy, so parallel > 1 works through p_map unchanged (MiniRes drops its cached
    pressure preconditioner on copy, which is what makes that safe).
  • tests/test_minires_sim.py -- 14 tests: the records' shape and keys, determinism,
    failure returning False rather than raising, the state/grid-ordering mapping, the
    water-cut identity, config errors refused at construction, the fidelity switch, and the
    adjoints against central differences.
  • tests/test_simulator_protocol.py -- MiniRes added to the existing parametrisation.
  • docs/tutorials/pipt/MiniRes/ -- a history-matching tutorial: a 16x16 five-spot, the
    log-permeability field as the unknown, water cut and oil rate at four producers as the
    data. make_case.py draws the "true" field from the config's own prior and runs MiniRes
    on it, so the observations are made where the reader can see them. The notebook is
    committed with its outputs, as the others are (mkdocs-jupyter does not execute).
  • pyproject.toml -- a [minires] extra. It is optional because MiniRes requires
    Python >= 3.12 while PET supports older; the tests skip without it.

Conventions the wrapper owns

MiniRes is agnostic about all three; the module docstring states them.

  • Report points are step indices (reporttype = "steps"), MiniRes taking a uniform dt.
    reportdates relabels them if a case wants dates.
  • Grid ordering: MiniRes is C-major (x first); Eclipse, and most of PET's tooling, is
    Fortran-ordered. field_order says which the state vector is in, and is applied on the
    way in and on the way out.
  • Data types are Eclipse summary names (WOPR:PRD1, FWIR, ...), so observed-data
    files, datatype filters and localization tooling need no adaptation. Rates are positive
    as produced/injected, and areal -- MiniRes has no thickness. Units are whatever the config
    poses the model in (cdarcy = 0.008527 for metric, as Eclipse does).

Verified

  • The full suite passes, with the 20 new/changed tests among them; ruff clean.
  • End-to-end through the public entry point (read_config.read -> ESMDA.assimilate):
    data misfit 691.6 -> 108.5, and the correlation of the ensemble mean with the truth
    0.169 -> 0.313, in ~10 s. That is the tutorial's own run.
  • Adjoints reach the analysis as (nd, nx, ne) -- verified (48, 256, 8) -- and a run with
    compute_adjoints = true logs [approx_update] Using adjoint-based update.
  • The gradients agree with central differences to ~1e-7 relative for WWCT/WWPR/WOPR.
    Only rate-controlled completions are covered: a BHP-controlled well's rate is itself a
    function of the state, so it is refused with a message rather than silently wrong.

Questions for the maintainers

  1. Placement. src/simulator/README.md points simulator wrappers at SimulatorWrap.
    MiniRes is not a wrapper around a binary -- it is a pip dependency with no external
    anything -- so bundling it beside the analytical models seems right, and is what makes
    the tutorial and CI story work. Happy to move it if you disagree.
  2. Data-type separator: WOPR:PRD1 here (matching the refactor's tutorials); older
    configs use WOPR PRD1. Say which and I'll follow.
  3. Adjoint coverage: currently the quantities that are a function of saturation at the
    well's cells. Extending to BHP-controlled wells means differentiating the well model into
    the seed; worth doing now, or when someone needs it?

Noticed in passing (not fixed here)

docs/tutorials/pipt/TinyBox/tutorial_pipt.ipynb still calls
DataReader({'truedata': ...}), but DataReader reads info.get('data'), so that cell now
raises ValueError: Data missing: the 'data' key is missing in info dictionary. It is
committed with outputs, which is presumably why it went unnoticed. Not touched here, since
re-executing it needs OPM.

pipt.misc_tools.extract_tools.extract_prior_info mutates the dict it is given and is not
idempotent: it wraps every non-list value in a list, so the nx/ny/nz ints it writes on
the first pass become [16], [16], [1] on the second. Building two ensembles from one
parsed config in the same process therefore fails in StateLayout.from_prior_info with
TypeError: 'list' object cannot be interpreted as an integer. Happy to open an issue or a
separate PR.

🤖 Generated with Claude Code

patnr and others added 2 commits September 21, 2026 15:42
…alled

MiniRes (https://github.com/patnr/MiniRes) is a pure-Python, two-phase
(water/oil) TPFA reservoir simulator: no binary, no licence, no deck, no
scratch folder. Wrapping it gives PET a *physical* forward model that a
tutorial, a test or CI can run anywhere -- where until now the only models
that ran unattended were analytical ones.

The wrapper satisfies `ForwardSimulator` as it stands; nothing in the ensemble
or the schemes needed changing. Each member runs on its own deepcopy of the
model, so it parallelizes through `p_map` unchanged (MiniRes drops its cached
pressure preconditioner on copy, which is what makes that safe).

Three conventions are the wrapper's own, since MiniRes is agnostic about them,
and its module docstring states them: report points are step indices (MiniRes
takes a uniform dt); `field_order` reconciles MiniRes's C-major grid with the
Fortran ordering the rest of PET's tooling assumes; and data types are named
as Eclipse summary vectors (WOPR:PRD1, FWIR, ...) so observed-data files and
datatype filters need no adaptation. Unknown quantities and unknown well
names are refused when the simulator is built, not mid-forecast.

With `compute_adjoints`, it also returns each datum's sensitivity to the
state, so the adjoint-based analyses get their first field-scale case:
MiniRes differentiates its own time stepper, one backward sweep per datum.
Covered for WWCT/WWPR/WOPR at rate-controlled wells; a BHP-controlled well's
rate is itself a function of the state, so it is refused rather than silently
wrong. The tests check the sensitivities against central differences.

MiniRes is an optional dependency: it requires Python >= 3.12, whereas PET
supports 3.10, so it is a `[minires]` extra and the tests skip without it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The same ESMDA loop as the TinyBox tutorial, on a 16x16 five-spot run by
MiniRes: `pip install PET[minires]` and it runs -- no OPM, no deck, no
external data. `make_case.py` draws the "true" log-permeability from the
config's own prior and runs MiniRes on it, so the observations are made where
the reader can see them (a twin experiment).

Committed with its outputs, as the other notebooks are (mkdocs-jupyter does
not execute). It plots the truth with MiniRes's own `plt_field`, the misfit
per iteration, the truth/prior/posterior fields on one colour scale, and the
water-cut match per producer. ~18 s to run, 100 members over 3 ESMDA steps.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@patnr
patnr marked this pull request as ready for review September 21, 2026 14:01

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant