From 19d49274e1e990a0ba2fe7a017f29151fe2ed5df Mon Sep 17 00:00:00 2001 From: mferrera Date: Mon, 30 Sep 2024 08:11:13 +0200 Subject: [PATCH] ENH: Add ert simulation mode to object metadata The simulation mode should allow contextual inference about the type of ensemble this data is derived from. --- .../definitions/0.8.0/schema/fmu_results.json | 26 +++++++ src/fmu/dataio/_model/enums.py | 14 ++++ src/fmu/dataio/_model/fields.py | 4 ++ src/fmu/dataio/providers/_fmu.py | 10 ++- tests/conftest.py | 5 ++ tests/data/snakeoil/export-a-surface | 32 +++++++++ tests/test_integration/conftest.py | 29 +++++++- .../test_simple_export_run.py | 68 +++++++++++++++++++ tests/test_units/test_dataio.py | 35 ++++++++++ 9 files changed, 219 insertions(+), 4 deletions(-) create mode 100755 tests/data/snakeoil/export-a-surface create mode 100644 tests/test_integration/test_simple_export_run.py diff --git a/schema/definitions/0.8.0/schema/fmu_results.json b/schema/definitions/0.8.0/schema/fmu_results.json index cf4aa319b..9a38509e0 100644 --- a/schema/definitions/0.8.0/schema/fmu_results.json +++ b/schema/definitions/0.8.0/schema/fmu_results.json @@ -1004,11 +1004,37 @@ } ], "default": null + }, + "simulation_mode": { + "anyOf": [ + { + "$ref": "#/$defs/ErtSimulationMode" + }, + { + "type": "null" + } + ], + "default": null } }, "title": "Ert", "type": "object" }, + "ErtSimulationMode": { + "description": "The simulation mode ert was run in. These definitions come from\n`ert.mode_definitions`.", + "enum": [ + "ensemble_experiment", + "ensemble_smoother", + "es_mda", + "evaluate_ensemble", + "iterative_ensemble_smoother", + "manual_update", + "test_run", + "workflow" + ], + "title": "ErtSimulationMode", + "type": "string" + }, "Experiment": { "description": "The ``fmu.ert.experiment`` block contains information about\nthe current ert experiment run.", "properties": { diff --git a/src/fmu/dataio/_model/enums.py b/src/fmu/dataio/_model/enums.py index e0c8f9476..0c16569b4 100644 --- a/src/fmu/dataio/_model/enums.py +++ b/src/fmu/dataio/_model/enums.py @@ -56,6 +56,20 @@ def _missing_(cls: Type[Content], value: object) -> None: ) +class ErtSimulationMode(str, Enum): + """The simulation mode ert was run in. These definitions come from + `ert.mode_definitions`.""" + + ensemble_experiment = "ensemble_experiment" + ensemble_smoother = "ensemble_smoother" + es_mda = "es_mda" + evaluate_ensemble = "evaluate_ensemble" + iterative_ensemble_smoother = "iterative_ensemble_smoother" + manual_update = "manual_update" + test_run = "test_run" + workflow = "workflow" + + class FMUClass(str, Enum): """The class of a data object by FMU convention or standards.""" diff --git a/src/fmu/dataio/_model/fields.py b/src/fmu/dataio/_model/fields.py index 14d4be03e..b60f9b7b0 100644 --- a/src/fmu/dataio/_model/fields.py +++ b/src/fmu/dataio/_model/fields.py @@ -218,6 +218,10 @@ class Ert(BaseModel): """Reference to the ert experiment. See :class:`Experiment`.""" + simulation_mode: Optional[enums.ErtSimulationMode] = Field(default=None) + """Reference to the ert simulation mode. + See :class:`SimulationMode`.""" + class Experiment(BaseModel): """The ``fmu.ert.experiment`` block contains information about diff --git a/src/fmu/dataio/providers/_fmu.py b/src/fmu/dataio/providers/_fmu.py index 9d87e9a9d..540f92cb6 100644 --- a/src/fmu/dataio/providers/_fmu.py +++ b/src/fmu/dataio/providers/_fmu.py @@ -42,7 +42,7 @@ from fmu.dataio import _utils from fmu.dataio._logging import null_logger from fmu.dataio._model import fields, schema -from fmu.dataio._model.enums import FMUContext +from fmu.dataio._model.enums import ErtSimulationMode, FMUContext from fmu.dataio.exceptions import InvalidMetadataError from ._base import Provider @@ -206,12 +206,18 @@ def _get_runpath_from_env() -> Path | None: @staticmethod def _get_ert_meta() -> fields.Ert: + try: + sim_mode = ErtSimulationMode(FmuEnv.SIMULATION_MODE.value) + except ValueError: + sim_mode = None + return fields.Ert( experiment=fields.Experiment( id=uuid.UUID(FmuEnv.EXPERIMENT_ID.value) if FmuEnv.EXPERIMENT_ID.value else None - ) + ), + simulation_mode=sim_mode, ) def _validate_and_establish_casepath(self) -> Path | None: diff --git a/tests/conftest.py b/tests/conftest.py index f4cc2dfcb..27e1b9ea7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -48,6 +48,11 @@ def _current_function_name(): return inspect.currentframe().f_back.f_code.co_name +@pytest.fixture(scope="session") +def source_root(request) -> Path: + return request.config.rootpath + + @pytest.fixture(scope="function", autouse=True) def return_to_original_directory(): # store original folder, and restore after each function (before and after yield) diff --git a/tests/data/snakeoil/export-a-surface b/tests/data/snakeoil/export-a-surface new file mode 100755 index 000000000..6e86551cf --- /dev/null +++ b/tests/data/snakeoil/export-a-surface @@ -0,0 +1,32 @@ +#!/usr/bin/env python + +import sys +from pathlib import Path + +import xtgeo + +import fmu.dataio as dataio +from fmu.config import utilities as ut + + +def main() -> None: + abs_path = Path(sys.argv[1]) + CFG = ut.yaml_load(abs_path / "fmuconfig/output/global_variables.yml") + surf = xtgeo.surface_from_file(abs_path / "ert/output/maps/props/poro_average.gri") + dataio.ExportData( + config=CFG, + name="all", + unit="fraction", + vertical_domain="depth", + domain_reference="msl", + content="property", + timedata=None, + is_prediction=True, + is_observation=False, + tagname="average_poro", + workflow="rms property model", + ).export(surf) + + +if __name__ == "__main__": + main() diff --git a/tests/test_integration/conftest.py b/tests/test_integration/conftest.py index 5ab02b3f7..0a70a7c12 100644 --- a/tests/test_integration/conftest.py +++ b/tests/test_integration/conftest.py @@ -32,7 +32,9 @@ def base_ert_config() -> str: @pytest.fixture -def fmu_snakeoil_project(tmp_path, monkeypatch, base_ert_config, global_config2_path): +def fmu_snakeoil_project( + tmp_path, monkeypatch, base_ert_config, global_config2_path, source_root +): """Makes a skeleton FMU project structure into a tmp_path, copying global_config2 into it with a basic ert config that can be appended onto.""" monkeypatch.setenv("DATAIO_TMP_PATH", str(tmp_path)) @@ -42,7 +44,7 @@ def fmu_snakeoil_project(tmp_path, monkeypatch, base_ert_config, global_config2_ os.makedirs(tmp_path / f"{app}/bin") os.makedirs(tmp_path / f"{app}/input") os.makedirs(tmp_path / f"{app}/model") - os.makedirs(tmp_path / "rms/model/snakeoil.rms13.1.2") + os.makedirs(tmp_path / "rms/model/snakeoil.rms14.2.2") os.makedirs(tmp_path / "fmuconfig/output") shutil.copy(global_config2_path, tmp_path / "fmuconfig/output/") @@ -65,6 +67,29 @@ def fmu_snakeoil_project(tmp_path, monkeypatch, base_ert_config, global_config2_ "../../share/preprocessed ", # inpath encoding="utf-8", ) + + # Add EXPORT_A_SURFACE forward model + os.makedirs(tmp_path / "ert/bin/scripts") + shutil.copy( + source_root / "tests/data/snakeoil/export-a-surface", + tmp_path / "ert/bin/scripts/export-a-surface", + ) + os.makedirs(tmp_path / "ert/output/maps/props") + shutil.copy( + source_root + / "examples/s/d/nn/xcase/realization-0/iter-0/rms" + / "output/maps/props/poro_average.gri", + tmp_path / "ert/output/maps/props/poro_average.gri", + ) + os.makedirs(tmp_path / "ert/bin/jobs") + pathlib.Path(tmp_path / "ert/bin/jobs/EXPORT_A_SURFACE").write_text( + "STDERR EXPORT_A_SURFACE.stderr\n" + "STDOUT EXPORT_A_SURFACE.stdout\n" + "EXECUTABLE ../scripts/export-a-surface\n" + "ARGLIST \n", + encoding="utf-8", + ) + pathlib.Path(tmp_path / "ert/model/snakeoil.ert").write_text( base_ert_config, encoding="utf-8" ) diff --git a/tests/test_integration/test_simple_export_run.py b/tests/test_integration/test_simple_export_run.py new file mode 100644 index 000000000..124062796 --- /dev/null +++ b/tests/test_integration/test_simple_export_run.py @@ -0,0 +1,68 @@ +import getpass +from pathlib import Path +from typing import Any + +import ert.__main__ +import pytest +import yaml + +from fmu.dataio._model import Root +from fmu.dataio._model.enums import ErtSimulationMode + + +def _add_export_a_surface(snakeoil_path: Path, ert_config: str) -> None: + with open(ert_config, "a", encoding="utf-8") as f: + f.writelines( + [ + "LOAD_WORKFLOW ../bin/workflows/xhook_create_case_metadata\n" + "HOOK_WORKFLOW xhook_create_case_metadata PRE_SIMULATION\n" + "INSTALL_JOB EXPORT_A_SURFACE ../bin/jobs/EXPORT_A_SURFACE\n" + f"FORWARD_MODEL EXPORT_A_SURFACE(={snakeoil_path})\n" + ] + ) + + +@pytest.fixture +def snakeoil_export_surface( + fmu_snakeoil_project: Path, monkeypatch: Any, mocker: Any +) -> Path: + monkeypatch.chdir(fmu_snakeoil_project / "ert/model") + _add_export_a_surface(fmu_snakeoil_project, "snakeoil.ert") + + mocker.patch( + "sys.argv", ["ert", "test_run", "snakeoil.ert", "--disable-monitoring"] + ) + ert.__main__.main() + return fmu_snakeoil_project + + +def test_simple_export_case_metadata(snakeoil_export_surface: Path) -> None: + fmu_case_yml = ( + snakeoil_export_surface / "scratch/user/snakeoil/share/metadata/fmu_case.yml" + ) + assert fmu_case_yml.exists() + + with open(fmu_case_yml, encoding="utf-8") as f: + fmu_case = yaml.safe_load(f) + + assert fmu_case["fmu"]["case"]["name"] == "snakeoil" + assert fmu_case["fmu"]["case"]["user"]["id"] == "user" + assert fmu_case["source"] == "fmu" + assert len(fmu_case["tracklog"]) == 1 + assert fmu_case["tracklog"][0]["user"]["id"] == getpass.getuser() + + +def test_simple_export_ert_environment_variables(snakeoil_export_surface: Path) -> None: + avg_poro_yml = Path( + snakeoil_export_surface + / "scratch/user/snakeoil/realization-0/iter-0" + / "share/results/maps/.all--average_poro.gri.yml" + ) + assert avg_poro_yml.exists() + + with open(avg_poro_yml, encoding="utf-8") as f: + avg_poro_metadata = yaml.safe_load(f) + + avg_poro = Root.model_validate(avg_poro_metadata) # asserts valid + assert avg_poro.root.fmu.ert.simulation_mode == ErtSimulationMode.test_run + assert avg_poro.root.fmu.ert.experiment.id is not None diff --git a/tests/test_units/test_dataio.py b/tests/test_units/test_dataio.py index 6d9ec9681..b4cc1f7a0 100644 --- a/tests/test_units/test_dataio.py +++ b/tests/test_units/test_dataio.py @@ -759,6 +759,7 @@ def test_fmucontext_case_casepath(fmurun_prehook, rmsglobalconfig, regsurf): """ assert FmuEnv.RUNPATH.value is None assert FmuEnv.EXPERIMENT_ID.value is not None + assert FmuEnv.SIMULATION_MODE.value is not None # will give warning when casepath not provided with pytest.warns(UserWarning, match="Could not auto detect"): @@ -782,6 +783,7 @@ def test_fmurun_attribute_inside_fmu(fmurun_w_casemetadata, rmsglobalconfig): # check that ERT environment variable is not set assert FmuEnv.ENSEMBLE_ID.value is not None + assert FmuEnv.SIMULATION_MODE.value is not None edata = ExportData(config=rmsglobalconfig, content="depth") assert edata._fmurun is True @@ -795,6 +797,7 @@ def test_fmu_context_not_given_fetch_from_env_realization( inside fmu and RUNPATH value is detected from the environment variables. """ assert FmuEnv.RUNPATH.value is not None + assert FmuEnv.SIMULATION_MODE.value is not None assert FmuEnv.EXPERIMENT_ID.value is not None edata = ExportData(config=rmsglobalconfig, content="depth") @@ -808,6 +811,7 @@ def test_fmu_context_not_given_fetch_from_env_case(fmurun_prehook, rmsglobalconf inside fmu and RUNPATH value not detected from the environment variables. """ assert FmuEnv.RUNPATH.value is None + assert FmuEnv.SIMULATION_MODE.value is not None assert FmuEnv.EXPERIMENT_ID.value is not None # will give warning when casepath not provided @@ -828,6 +832,7 @@ def test_fmu_context_not_given_fetch_from_env_nonfmu(rmsglobalconfig): """ assert FmuEnv.RUNPATH.value is None assert FmuEnv.EXPERIMENT_ID.value is None + assert FmuEnv.SIMULATION_MODE.value is None edata = ExportData(config=rmsglobalconfig, content="depth") assert edata._fmurun is False @@ -1160,6 +1165,36 @@ def test_ert_experiment_id_present_in_exported_metadata( assert export_meta["fmu"]["ert"]["experiment"]["id"] == expected_id +def test_ert_simulation_mode_present_in_generated_metadata( + fmurun_w_casemetadata, monkeypatch, globalconfig1, regsurf +): + """Test that the ert experiment id has been set correctly + in the generated metadata""" + + monkeypatch.chdir(fmurun_w_casemetadata) + + edata = ExportData(config=globalconfig1, content="depth") + meta = edata.generate_metadata(regsurf) + print(meta["fmu"]) + assert meta["fmu"]["ert"]["simulation_mode"] == "test_run" + + +def test_ert_simulation_mode_present_in_exported_metadata( + fmurun_w_casemetadata, monkeypatch, globalconfig1, regsurf +): + """Test that the ert experiment id has been set correctly + in the exported metadata""" + + monkeypatch.chdir(fmurun_w_casemetadata) + + edata = ExportData(config=globalconfig1, content="depth") + out = Path(edata.export(regsurf)) + with open(out.parent / f".{out.name}.yml", encoding="utf-8") as f: + export_meta = yaml.safe_load(f) + print(export_meta["fmu"]) + assert export_meta["fmu"]["ert"]["simulation_mode"] == "test_run" + + def test_offset_top_base_present_in_exported_metadata(globalconfig1, regsurf): """ Test that top, base and offset information provided from the config are