Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.10", 3.11, 3.12]
python-version: ["3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v4
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ at NORCE Norwegian Research Centre AS.

## Installation

PET requires Python 3.12 through 3.14. The standard installation includes
EnIF and EnIF-MDA with their dependencies.

Before installing ensure you have python3 pre-requisites. On a Debian system run:

```
Expand Down
7 changes: 4 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ An analysis (`pipt.update_schemes.analysis`) is a class with
`update(enX, enY, enE, **kwargs) -> AnalysisResult`, returning exactly one of
a state-space `step`, a weight-space `w_step` or a `W_step`. The scheme turns
it into a proposal with `propose_state(result, step_scale)`. The flavours are
`approx`, `full`, `subspace`, `margis` and the multilevel `hybrid`;
`register_analysis` adds one. Analyses read what they need from the scheme:
`state_scaling`, `scale_data`, `proj`, `cov_data`, `trunc_energy`, `lam`.
`approx`, `full`, `subspace`, `margis`, the multilevel `hybrid` and `enif`
(ES-MDA); `register_analysis` adds one. Analyses read what they need from the
scheme: `state_scaling`, `scale_data`, `proj`, `cov_data`, `trunc_energy`,
`lam`.

## Data on the analysis path

Expand Down
15 changes: 14 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Flags accept `true`/`false`, `yes`/`no` and the Python booleans. A key marked
| Key | Meaning | Default |
| --- | --- | --- |
| `scheme` | Algorithm: `esmda`, `es`, `enkf`, `lmenrml`, `gnenrml`. `pipt.available_schemes()` lists every `(scheme, analysis)` pair. | required |
| `analysis` | Analysis flavour the scheme runs: `approx`, `full`, `subspace` (all schemes); `subspace2` (ES-MDA, LM-EnRML, GN-EnRML); `margis` (GN-EnRML). `subspace2` solves for the ensemble transform directly and uses the analytic data covariance, so it reads neither `energy` nor `iteration.energy`. | `approx` |
| `analysis` | Analysis flavour the scheme runs: `approx`, `full`, `subspace` (all schemes); `subspace2` (ES-MDA, LM-EnRML, GN-EnRML); `margis` (GN-EnRML); `enif` (ES-MDA, see the `[dataassim.enif]` block below). `subspace2` solves for the ensemble transform directly and uses the analytic data covariance, so it reads neither `energy` nor `iteration.energy`. | `approx` |
| `energy` | Truncation energy of the SVD in ES-MDA, ES and EnKF; a fraction, or a percentage when greater than 1. The iterative schemes read `iteration.energy`. | `0.98` |
| `emp_cov` | The variance file holds an ensemble of observation errors; the analyses use that empirical covariance. Flag. | off |

Expand Down Expand Up @@ -59,6 +59,19 @@ Flags accept `true`/`false`, `yes`/`no` and the Python booleans. A key marked
| `tot_assim_steps` | Number of inflated assimilation steps; one update each. | required |
| `inflation_param` | Inflation factor per step (a list) or one factor for all. The inverses must sum to 1. | `tot_assim_steps` for every step |

### EnIF: the `[dataassim.enif]` block

Settings for the `enif` analysis flavour of ES-MDA (see
[the EnIF tutorial](tutorials/enif.md)). Spatial dependence is specified by
parameter graphs rather than localization: the flavour rejects `localization`,
`localanalysis`, `multilevel` and `emp_cov`.

| Key | Meaning | Default |
| --- | --- | --- |
| `parameter_graphs` | Maps state names to NetworkX graphs, SciPy sparse adjacency arrays, or `.npz` files written with `scipy.sparse.save_npz`. Without one, a state with `grid` metadata in `prior_<name>` gets nearest-neighbour connectivity, and a state without it independent nodes. | none |
| `neighbourhood_expansion` | Graph hops used when fitting the prior precision. | `2` |
| `neighbor_propagation_order` | Graph hops the update propagates through. | `15` |

### Localization: the `[dataassim.localization]` block

`name` selects the strategy; `pipt.localization.available_localizations()`
Expand Down
1 change: 1 addition & 0 deletions docs/tutorials/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ Here are some tutorials.

- [`adding_an_analysis.ipynb`](pipt/extending/adding_an_analysis): Write a new analysis flavour and bind it to a scheme
- [`adding_a_scheme.ipynb`](pipt/extending/adding_a_scheme): Write a new scheme and register it for config-driven use
- [EnIF, an out-of-tree analysis brought in-tree](enif.md): The graph-informed information-filter flavour for ES-MDA -- installation, settings and parameter graphs
117 changes: 117 additions & 0 deletions docs/tutorials/enif.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Ensemble information filter (EnIF)

PET offers the EnIF analysis as a flavour of ES-MDA. It uses the sparse
regression and precision estimation from ERT's
[`_enif_update.py`](https://github.com/equinor/ert/blob/main/src/ert/analysis/_enif_update.py)
through `graphite-maps`.

## Installation

Install PET from your checkout, including EnIF and its dependencies:

```sh
python -m pip install -e .
```

PET requires Python 3.12 through 3.14, matching its `graphite-maps` dependency.

## Select the analysis

Keep your existing ensemble, observation and simulator settings. EnIF is an
analysis flavour of ES-MDA, so select it in the `dataassim` section:

```yaml
scheme: esmda
analysis: enif
mda:
tot_assim_steps: 3
inflation_param: [2, 4, 4]
```

The original, single-update EnIF is the one-step schedule:

```yaml
scheme: esmda
analysis: enif
mda:
tot_assim_steps: 1
```

The equivalent Python entry point is `ESMDA(keys_da, keys_en, sim,
analysis="enif")`, and `("esmda", "enif")` resolves through the scheme
registry like any other combination.

EnIF-MDA reruns the simulator and refits the regression and state precision
after each update. MDA requires positive, finite inflation factors satisfying
`sum(1 / alpha) = 1`. If you omit `inflation_param`, PET uses
`tot_assim_steps` for each factor. A scalar factor repeats across the schedule.
The schedule retains its original indexing on restart.

## Parameter graphs

EnIF estimates a separate prior precision block for each state in `idX`.
By default:

- A state with `grid` metadata in `prior_<state>` uses nearest-neighbour
connectivity. PET's prior parser converts `grid` to `nx`, `ny` and `nz`.
- A state without grid metadata uses independent graph nodes.

The regular-grid ordering matches PET's layered prior generator:
`row = z * nx * ny + x * ny + y` (y varies fastest). For imported ensembles
with a different ordering, reduced active-cell arrays or irregular geometry,
provide a graph whose node numbers match the imported parameter rows.

You can configure graphs and neighbourhood sizes under `enif`:

```yaml
enif:
parameter_graphs:
perm: perm_graph.npz
neighbourhood_expansion: 2
neighbor_propagation_order: 15
```

Write a graph file as a symmetric sparse adjacency array with
`scipy.sparse.save_npz`. For example, for five parameters arranged in a chain:

```python
import networkx as nx
from scipy import sparse

graph = nx.path_graph(5)
sparse.save_npz('perm_graph.npz', nx.to_scipy_sparse_array(graph, format='csc'))
```

Python configurations can also supply NetworkX graphs or SciPy sparse
adjacency arrays directly in `parameter_graphs`. Use local node numbers
`0` through `number_of_parameter_rows - 1` for each state. Graph weights do
not affect the fit; EnIF uses connectivity.

EnIF excludes rows containing non-finite values and rows with zero ensemble
spread from estimation. It removes their graph nodes without connecting
neighbours across the resulting gaps. The analysis gives those rows a zero
increment, then applies PET's configured state limits.

## Update and diagnostics

EnIF uses PET's perturbed observations, random-number stream, state clipping,
forecast loop and misfit reporting. It scales observation covariance by the
current MDA factor once. It also estimates the unexplained response variance,
as in ERT. For a correlated observation covariance, it whitens the observations,
forecasts and perturbations before fitting the response map.

The analysis lives in `pipt/update_schemes/analysis/enif.py` and binds to the
ES-MDA scheme like the `approx`, `full` and `subspace` flavours; it returns an
additive state-space step and uses the direct sparse solver, matching ERT's
non-iterative transport setting.

After an update, the bound analysis object (`scheme.analysis`) exposes the
fitted `H`, `Prec_u`, `Prec_eps` and `Prec_posterior`. These matrices use
standardized, retained state rows; `enif_active_rows` maps them back to the
full state. With correlated observation errors, `H` and `Prec_eps` use
whitened observation coordinates.

This analysis requires at least two ensemble members and positive observation
variances. It does not support PET's covariance localization, local analysis,
multilevel ensembles or `emp_cov` sample input. Use parameter graphs to specify
spatial dependence.
9 changes: 5 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ maintainers = [
]
license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.12,<3.15"
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering",
]
dependencies = [
Expand All @@ -34,6 +34,7 @@ dependencies = [
"tqdm",
"PyWavelets",
"geostat @ git+https://github.com/Python-Ensemble-Toolbox/Geostatistics@3f9f0c876815db140fae3404d322892190cb6728",
"graphite-maps>=0.0.11,<0.1",
"pandas",
"p_tqdm",
"tomli",
Expand All @@ -48,7 +49,7 @@ pet = "pet_cli.__main__:main"
[project.optional-dependencies]
minires = [
# Pure-Python two-phase TPFA simulator, ref src/simulator/minires.py.
# Optional: it needs Python >= 3.12, whereas PET supports 3.10.
# Optional: keeps the toy simulator out of the core dependency set.
"minires>=0.3.2",
]
dev = [
Expand Down
4 changes: 2 additions & 2 deletions src/input_output/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def pairs_to_dict(entries) -> dict:
ALIASES_DATAASSIM = {"truedata": "data", "var": "datavar", "save_folder": "savefolder", "restartfile": "restart_file"}
FLAGS_DATAASSIM = ("emp_cov", "restart", "restartsave", "obsvarsave", "screendata", "post_process_forecast",
"scale_data", "logit")
BLOCKS_DATAASSIM = ("iteration", "mda", "compress", "localization", "localanalysis")
BLOCKS_DATAASSIM = ("iteration", "mda", "compress", "localization", "localanalysis", "enif")

ALIASES_ENSEMBLE = {"importstaticvar": "importstate", "save_folder": "savefolder"}
FLAGS_ENSEMBLE = ("save_prior", "disable_tqdm", "natural_gradient")
Expand All @@ -86,7 +86,7 @@ def pairs_to_dict(entries) -> dict:
KNOWN_DATAASSIM = frozenset({
"scheme", "analysis", "data", "datavar", "obsname", "datatype", "truedataindex", "assimindex", "energy",
"emp_cov", "iteration", "mda", "compress", "localization", "localanalysis", "actnum", "scale_data", "scale",
"screendata", "post_process_forecast", "remove_outliers",
"screendata", "post_process_forecast", "remove_outliers", "enif",
"savefolder", "nosave", "savedata", "analysisdebug", "iterinfo", "obsvarsave", "qa", "qc",
"restart", "restartsave", "restart_file", "logit", "logger_name",
# legacy text files keep the ensemble's keys in DATAASSIM
Expand Down
11 changes: 7 additions & 4 deletions src/pipt/update_schemes/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
:class:`AnalysisBase` -- the shared contract and helpers.
``approx``, ``full``, ``subspace``, ``subspace2``
The four registered flavours.
``hybrid``, ``margis``
Flavours consumed as mixins rather than through the registry: ``hybrid``
belongs to the multilevel scheme and ``margis`` is backed by a private
package when installed.
``hybrid``, ``margis``, ``enif``
Flavours that live outside the registry: ``hybrid`` belongs to the
multilevel scheme, ``margis`` is backed by a private package when
installed, and ``enif`` (ES-MDA only) uses the graphite-maps
estimators -- see :mod:`pipt.update_schemes.analysis.enif`.
``registry``
Name-to-class lookup, plus :func:`register_analysis` for out-of-tree
flavours.
Expand All @@ -35,6 +36,7 @@
from .hybrid import hybrid_update
from .subspace import subspace_update
from .subspace2 import subspace2_update
from .enif import enif_update
from .registry import (
ANALYSES,
available_analyses,
Expand All @@ -50,6 +52,7 @@
"subspace_update",
"subspace2_update",
"hybrid_update",
"enif_update",
"ANALYSES",
"available_analyses",
"get_analysis",
Expand Down
Loading
Loading