From 95c47f19942e9e1dadc5d2d03098b437787686e4 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 | 34 ++++++++++ tests/test_integration/conftest.py | 29 ++++++++- tests/test_integration/ert_config_utils.py | 35 +++++++++++ .../test_simple_export_run.py | 62 +++++++++++++++++++ .../test_wf_copy_preprocessed_data.py | 43 +++++-------- .../test_wf_create_case_metadata.py | 16 ++--- tests/test_units/test_dataio.py | 35 +++++++++++ tests/test_units/test_fmuprovider_class.py | 19 +++++- 13 files changed, 286 insertions(+), 46 deletions(-) create mode 100755 tests/data/snakeoil/export-a-surface create mode 100644 tests/test_integration/ert_config_utils.py 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..a9d340ecf --- /dev/null +++ b/tests/data/snakeoil/export-a-surface @@ -0,0 +1,34 @@ +#!/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: + snakeoil_path = Path(sys.argv[1]) + CFG = ut.yaml_load(snakeoil_path / "fmuconfig/output/global_variables.yml") + surf = xtgeo.surface_from_file( + snakeoil_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/ert_config_utils.py b/tests/test_integration/ert_config_utils.py new file mode 100644 index 000000000..8658bed92 --- /dev/null +++ b/tests/test_integration/ert_config_utils.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from pathlib import Path + + +def add_create_case_workflow(filepath: Path | str) -> None: + with open(filepath, "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" + ] + ) + + +def add_copy_preprocessed_workflow(filepath: Path | str) -> None: + with open(filepath, "a", encoding="utf-8") as f: + f.writelines( + [ + "LOAD_WORKFLOW ../bin/workflows/xhook_copy_preprocessed_data\n" + "HOOK_WORKFLOW xhook_copy_preprocessed_data PRE_SIMULATION\n" + ] + ) + + +def add_export_a_surface_forward_model( + snakeoil_path: Path, filepath: Path | str +) -> None: + with open(filepath, "a", encoding="utf-8") as f: + f.writelines( + [ + "INSTALL_JOB EXPORT_A_SURFACE ../bin/jobs/EXPORT_A_SURFACE\n" + f"FORWARD_MODEL EXPORT_A_SURFACE(={snakeoil_path})\n" + ] + ) 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..15232893a --- /dev/null +++ b/tests/test_integration/test_simple_export_run.py @@ -0,0 +1,62 @@ +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 + +from .ert_config_utils import ( + add_create_case_workflow, + add_export_a_surface_forward_model, +) + + +@pytest.fixture +def snakeoil_export_surface( + fmu_snakeoil_project: Path, monkeypatch: Any, mocker: Any +) -> Path: + monkeypatch.chdir(fmu_snakeoil_project / "ert/model") + add_create_case_workflow("snakeoil.ert") + add_export_a_surface_forward_model(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_integration/test_wf_copy_preprocessed_data.py b/tests/test_integration/test_wf_copy_preprocessed_data.py index 5f9b48719..e1ecbf85e 100644 --- a/tests/test_integration/test_wf_copy_preprocessed_data.py +++ b/tests/test_integration/test_wf_copy_preprocessed_data.py @@ -4,6 +4,11 @@ import fmu.dataio as dataio +from .ert_config_utils import ( + add_copy_preprocessed_workflow, + add_create_case_workflow, +) + def _export_preprocessed_data(config, regsurf): """Export preprocessed surfaces""" @@ -23,26 +28,6 @@ def _export_preprocessed_data(config, regsurf): ).export(regsurf) -def _add_create_case_workflow(filepath): - with open(filepath, "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" - ] - ) - - -def _add_copy_preprocessed_workflow(filepath): - with open(filepath, "a", encoding="utf-8") as f: - f.writelines( - [ - "LOAD_WORKFLOW ../bin/workflows/xhook_copy_preprocessed_data\n" - "HOOK_WORKFLOW xhook_copy_preprocessed_data PRE_SIMULATION\n" - ] - ) - - def test_copy_preprocessed_runs_successfully( fmu_snakeoil_project, monkeypatch, mocker, globalconfig2, regsurf ): @@ -51,8 +36,8 @@ def test_copy_preprocessed_runs_successfully( _export_preprocessed_data(globalconfig2, regsurf) monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_create_case_workflow("snakeoil.ert") - _add_copy_preprocessed_workflow("snakeoil.ert") + add_create_case_workflow("snakeoil.ert") + add_copy_preprocessed_workflow("snakeoil.ert") mocker.patch( "sys.argv", @@ -92,7 +77,7 @@ def test_copy_preprocessed_no_casemeta( _export_preprocessed_data(globalconfig2, regsurf) monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_copy_preprocessed_workflow("snakeoil.ert") + add_copy_preprocessed_workflow("snakeoil.ert") mocker.patch( "sys.argv", @@ -114,8 +99,8 @@ def test_copy_preprocessed_no_preprocessed_files( """ monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_create_case_workflow("snakeoil.ert") - _add_copy_preprocessed_workflow("snakeoil.ert") + add_create_case_workflow("snakeoil.ert") + add_copy_preprocessed_workflow("snakeoil.ert") mocker.patch( "sys.argv", @@ -142,7 +127,7 @@ def test_inpath_absolute_path_raises(fmu_snakeoil_project, monkeypatch, mocker, ) monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_copy_preprocessed_workflow("snakeoil.ert") + add_copy_preprocessed_workflow("snakeoil.ert") mocker.patch( "sys.argv", @@ -166,8 +151,8 @@ def test_copy_preprocessed_no_preprocessed_meta( _export_preprocessed_data({"wrong": "config"}, regsurf) monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_create_case_workflow("snakeoil.ert") - _add_copy_preprocessed_workflow("snakeoil.ert") + add_create_case_workflow("snakeoil.ert") + add_copy_preprocessed_workflow("snakeoil.ert") mocker.patch( "sys.argv", @@ -200,7 +185,7 @@ def test_deprecation_warning_global_variables( f.write(" '--global_variables_path' dummypath") monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_copy_preprocessed_workflow("snakeoil.ert") + add_copy_preprocessed_workflow("snakeoil.ert") mocker.patch( "sys.argv", diff --git a/tests/test_integration/test_wf_create_case_metadata.py b/tests/test_integration/test_wf_create_case_metadata.py index 8689ddaf9..672636436 100644 --- a/tests/test_integration/test_wf_create_case_metadata.py +++ b/tests/test_integration/test_wf_create_case_metadata.py @@ -7,22 +7,14 @@ import pytest import yaml - -def _add_create_case_workflow(filepath): - with open(filepath, "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" - ] - ) +from .ert_config_utils import add_create_case_workflow def test_create_case_metadata_runs_successfully( fmu_snakeoil_project, monkeypatch, mocker ): monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_create_case_workflow("snakeoil.ert") + add_create_case_workflow("snakeoil.ert") mocker.patch( "sys.argv", ["ert", "test_run", "snakeoil.ert", "--disable-monitoring"] @@ -48,7 +40,7 @@ def test_create_case_metadata_warns_without_overwriting( fmu_snakeoil_project, monkeypatch, mocker ): monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_create_case_workflow("snakeoil.ert") + add_create_case_workflow("snakeoil.ert") share_metadata = fmu_snakeoil_project / "scratch/user/snakeoil/share/metadata" fmu_case_yml = share_metadata / "fmu_case.yml" @@ -93,7 +85,7 @@ def test_create_case_metadata_enable_mocked_sumo( f.write(' "--sumo" "--sumo_env" ') monkeypatch.chdir(fmu_snakeoil_project / "ert/model") - _add_create_case_workflow("snakeoil.ert") + add_create_case_workflow("snakeoil.ert") mocker.patch( "sys.argv", ["ert", "test_run", "snakeoil.ert", "--disable-monitoring"] 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 diff --git a/tests/test_units/test_fmuprovider_class.py b/tests/test_units/test_fmuprovider_class.py index ca3d46db4..c5badc54a 100644 --- a/tests/test_units/test_fmuprovider_class.py +++ b/tests/test_units/test_fmuprovider_class.py @@ -1,5 +1,6 @@ """Test the FmuProvider class applied the _metadata.py module""" +import importlib import logging import os @@ -9,7 +10,7 @@ import fmu.dataio as dataio # from conftest import pretend_ert_env_run1 -from fmu.dataio._model.enums import FMUContext +from fmu.dataio._model.enums import ErtSimulationMode, FMUContext from fmu.dataio.exceptions import InvalidMetadataError from fmu.dataio.providers._fmu import ( DEFAULT_ITER_NAME, @@ -406,3 +407,19 @@ def test_fmuprovider_workflow_reference(fmurun_w_casemetadata, globalconfig2): pydantic.ValidationError, match="Input should be a valid string" ): FmuProvider(model=GLOBAL_CONFIG_MODEL, workflow=123.4).get_metadata() + + +def test_ert_simulation_modes_one_to_one() -> None: + """Ensure dataio known modes match those defined by Ert. These are currently defined + in `ert.mode_definitions`. `MANUAL_MODE` is skipped due to seemingly being relevant + to Ert internally -- the modes are duplicated there.""" + ert_mode_definitions = importlib.import_module("ert.mode_definitions") + ert_modes = { + getattr(ert_mode_definitions, name) + for name in dir(ert_mode_definitions) + if not name.startswith("__") + and name != "MODULE_MODE" + and isinstance(getattr(ert_mode_definitions, name), str) + } + dataio_known_modes = {mode.value for mode in ErtSimulationMode} + assert ert_modes == dataio_known_modes