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 src/ert/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ def get_ert_parser(parser: ArgumentParser | None = None) -> ArgumentParser:
"observation format to summary, but this can be "
"configured using the --format flag to specify "
"which format to convert to."
"Valid formats are: bulk, summary"
"Valid formats are: bulk, summary, yaml"
)
convert_obs_parser = subparsers.add_parser(
"convert_observations",
Expand Down
3 changes: 3 additions & 0 deletions src/ert/observation_converters/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,21 @@
from .summary_to_bulk import (
convert_summary_to_bulk,
)
from .summary_to_yaml import convert_summary_to_yaml


class SupportedFormat(StrEnum):
SUMMARY = "summary"
BULK = "bulk"
YAML = "yaml"
Comment thread
ajaust marked this conversation as resolved.


ConverterFunction = Callable[[str, ErtRuntimePlugins], None]

_SUPPORTED_CONVERSIONS: dict[SupportedFormat, ConverterFunction] = {
SupportedFormat.BULK: convert_summary_to_bulk,
SupportedFormat.SUMMARY: convert_history_to_summary,
SupportedFormat.YAML: convert_summary_to_yaml,
}


Expand Down
93 changes: 93 additions & 0 deletions src/ert/observation_converters/summary_to_yaml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import warnings
from pathlib import Path
from typing import Literal, TypedDict

from natsort import natsorted
from ruamel.yaml import YAML

from ert.cli.main import ErtCliError
from ert.config import ConfigValidationError, ErtConfig, Observation
from ert.plugins import ErtRuntimePlugins


class YamlObservation(TypedDict):
date: str
value: float
error: float


class SummaryDict(TypedDict):
key: str
observations: list[YamlObservation]


YamlDict = dict[Literal["smry"], list[SummaryDict]]


class YamlConverter:
TARGET_FILE = "summary_observations.yaml"

def __init__(self, observations: list[Observation]) -> None:
summary_observations = [
o for o in observations if o.type == "summary_observation"
]
if not summary_observations:
raise ErtCliError("No summary observations in configuration.\nExiting ...")

self.summary_observations = summary_observations

def _summary_to_yaml_dict(self) -> YamlDict:
summary_keys: set[str] = {o.key for o in self.summary_observations}
summary_list: list[SummaryDict] = []
for key in natsorted(summary_keys):
observations_with_key = [
o for o in self.summary_observations if o.key == key
]
chronological_observations = sorted(
observations_with_key, key=lambda o: o.date
)
# Round dates without HH/MM/SS to just date
for o in chronological_observations:
o.date = o.date.removesuffix("T00:00:00")
obs_dicts: list[YamlObservation] = [
{
"date": o.date,
"value": o.value,
"error": o.error,
}
for o in chronological_observations
]
Comment thread
ajaust marked this conversation as resolved.
summary_dict: SummaryDict = {"key": key, "observations": obs_dicts}
summary_list.append(summary_dict)
return {"smry": summary_list}

def export_yaml(self) -> None:
yaml = YAML()
yaml_dict = self._summary_to_yaml_dict()
try:
with Path(self.TARGET_FILE).open("x", encoding="utf-8") as f:
yaml.dump(yaml_dict, f)
except FileExistsError as error:
raise ErtCliError(
f"A file with name '{self.TARGET_FILE}' already exists. "
"Will not overwrite it and exit instead."
) from error
print(f"Successfully wrote summary observations to '{self.TARGET_FILE}'.")


def convert_summary_to_yaml(config: str, site_plugins: ErtRuntimePlugins) -> None:
with warnings.catch_warnings():
warnings.filterwarnings(action="ignore")
try:
ert_config = ErtConfig.with_plugins(site_plugins).from_file(config)
except ConfigValidationError as e:
raise ErtCliError(
f"Failed to internalize the ert config '{config}' with error:\n {e}"
) from e

observations = ert_config.observation_declarations

yaml_exporter = YamlConverter(
observations=observations,
)
yaml_exporter.export_yaml()
45 changes: 45 additions & 0 deletions tests/ert/unit_tests/cli/test_converters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from pathlib import Path
from unittest.mock import MagicMock

from ert.observation_converters import convert_observations
from ert.plugins import get_site_plugins


def test_that_convert_observations_does_not_fail_when_config_has_hooked_workflows(
use_tmpdir,
):
"""This reproduces the case where ErtConfig.from_file() is called without
plugins while hooked workflows reference plugin-provided jobs.
"""
site_plugins = get_site_plugins()

arbitrary_existing_job = next(iter(site_plugins.installed_workflow_jobs))

workflow_file = Path("my_hook_workflow")
workflow_file.write_text(f"{arbitrary_existing_job}\n", encoding="utf-8")

obs_config = "foo.txt"
summary_obs = (
"SUMMARY_OBSERVATION { KEY = FOPR; VALUE = 10; ERROR = 5; DATE = 2000-01-01; };"
)
Path(obs_config).write_text(
summary_obs,
encoding="utf-8",
)

ert_config = "config.ert"
minimal_workflow_config = f"""\
NUM_REALIZATIONS 10
ECLBASE foo
OBS_CONFIG {obs_config}
LOAD_WORKFLOW {workflow_file} MY_HOOK
HOOK_WORKFLOW MY_HOOK PRE_SIMULATION
"""
Path(ert_config).write_text(
minimal_workflow_config,
encoding="utf-8",
)

for format_ in ["summary", "bulk", "yaml"]:
args = MagicMock(format=format_, config=ert_config)
convert_observations(args, site_plugins)
2 changes: 1 addition & 1 deletion tests/ert/unit_tests/cli/test_summary_to_bulk.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ def _make_summary_obs(
key += f":{well}" if well else ""
return SummaryObservation(
name="foo",
key=f"{key}:{well}",
key=key,
value=0.5,
error=0.02,
date=date,
Expand Down
Loading
Loading