Skip to content
Draft
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
6 changes: 4 additions & 2 deletions src/subscript/fmuobs/fmuobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,10 @@ def validate_internal_dframe(obs_df: pd.DataFrame) -> bool:
failed = True
non_supported_classes = set(obs_df["CLASS"]) - set(CLASS_SHORTNAME.keys())
if non_supported_classes:
logger.error("Unsupported observation classes: %s", non_supported_classes)
failed = True
logger.warning(
"Unsupported observation classes (will be ignored): %s",
non_supported_classes,
)

index = {"CLASS", "LABEL", "OBS", "SEGMENT"}.intersection(set(obs_df.columns))
repeated_rows = obs_df[obs_df.set_index(list(index)).index.duplicated(keep=False)]
Expand Down
10 changes: 8 additions & 2 deletions src/subscript/fmuobs/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -358,15 +358,21 @@ def flatten_observation_unit(

for subunit in subunit_keys:
if len(subunit.split()) < 2:
# It must be two strings, like "OBS P1", or "SEGMENT FIRST_YEAR".
raise ValueError("Wrong observation subunit syntax: " + str(subunit))
# Single-word subunit keys (e.g. LOCALIZATION) carry no label and
# are not representable in the internal dataframe format; skip them.
logger.debug("Ignoring unlabeled subunit block: %s", subunit)
continue
obs_subunits.append(
{
subunit.split()[0]: subunit.split()[1],
**keyvalues,
**obsunit[subunit],
}
)
if not obs_subunits:
# All nested blocks were unlabeled (e.g. only LOCALIZATION); return
# the plain key-value data of this observation.
return [keyvalues]
return obs_subunits


Expand Down
63 changes: 60 additions & 3 deletions tests/test_fmuobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def fixture_readonly_testdata_dir(monkeypatch):
("ert-doc.csv", "csv"),
("fmu-ensemble-obs.yml", "yaml"),
("drogon_wbhp_rft_wct_gor_tracer_4d.obs", "ert"),
("drogon_wbhp_rft_wct_gor_tracer_plt_local.obs", "ert"),
],
)
def test_autoparse_file(filename, expected_format, readonly_testdata_dir):
Expand Down Expand Up @@ -161,6 +162,7 @@ def test_roundtrip_ertobs(filename, readonly_testdata_dir):
("ert-doc.csv"),
("fmu-ensemble-obs.yml"),
("drogon_wbhp_rft_wct_gor_tracer_4d.obs"),
("drogon_wbhp_rft_wct_gor_tracer_plt_local.obs"),
],
)
def test_roundtrip_yaml(filename, readonly_testdata_dir):
Expand Down Expand Up @@ -206,6 +208,7 @@ def test_roundtrip_yaml(filename, readonly_testdata_dir):
("ert-doc.csv"),
("fmu-ensemble-obs.yml"),
("drogon_wbhp_rft_wct_gor_tracer_4d.obs"),
("drogon_wbhp_rft_wct_gor_tracer_plt_local.obs"),
],
)
def test_roundtrip_resinsight(filename, readonly_testdata_dir):
Expand All @@ -228,19 +231,73 @@ def test_roundtrip_resinsight(filename, readonly_testdata_dir):

# LABEL is not part of the ResInsight format, and a made-up label
# is obtained through the roundtrip (when importing back). Skip it
# when comparing.
# when comparing. ERROR_MODE is also not preserved in ResInsight format.

pd.testing.assert_frame_equal(
ri_roundtrip_dframe.sort_index(axis="columns").drop(
["LABEL", "COMMENT", "SUBCOMMENT"], axis="columns", errors="ignore"
["LABEL", "COMMENT", "SUBCOMMENT", "ERROR_MODE"],
axis="columns",
errors="ignore",
),
dframe.sort_index(axis="columns").drop(
["LABEL", "COMMENT", "SUBCOMMENT"], axis="columns", errors="ignore"
["LABEL", "COMMENT", "SUBCOMMENT", "ERROR_MODE"],
axis="columns",
errors="ignore",
),
check_like=True,
)


def test_rft_observation_warning(readonly_testdata_dir, caplog):
"""Test that RFT_OBSERVATION (unsupported class) is silently ignored with
a warning instead of raising an error."""
import logging

filename = "drogon_wbhp_rft_wct_gor_tracer_plt_local.obs"
with caplog.at_level(logging.WARNING):
format_, dframe = autoparse_file(filename)

assert format_ == "ert"
assert not dframe.empty
assert "RFT_OBSERVATION" in dframe["CLASS"].values

# No error should have been logged about unsupported classes
assert (
"error" not in caplog.text.lower() or "unsupported" not in caplog.text.lower()
)
# A warning should be emitted from validate_internal_dframe via main(),
# but autoparse_file does not call validate; check the class is parseable.
assert "SUMMARY_OBSERVATION" in dframe["CLASS"].values


def test_rft_observation_warning_via_main(tmp_path, mocker, caplog, monkeypatch):
"""Test that running main() on a file with RFT_OBSERVATION emits a warning
(not an error) and produces valid output for supported classes."""
import logging

monkeypatch.chdir(tmp_path)
mocker.patch(
"sys.argv",
[
"fmuobs",
"--includedir",
str(TESTDATA_DIR),
"--csv",
"output.csv",
str(TESTDATA_DIR / "drogon_wbhp_rft_wct_gor_tracer_plt_local.obs"),
],
)
with caplog.at_level(logging.WARNING):
main()

assert Path("output.csv").exists()
# Should warn about unsupported class, not error
assert "RFT_OBSERVATION" in caplog.text
assert "Unsupported observation classes (will be ignored)" in caplog.text
# Dataframe is still valid (no error logged about invalidity)
assert "Observation dataframe is invalid" not in caplog.text


@pytest.mark.integration
def test_integration():
"""Test that the endpoint is installed"""
Expand Down
Loading
Loading