From 1459d4fee7921bc87dced509e09fda585b18d31a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Therese=20Natter=C3=B8y?= <61694854+tnatt@users.noreply.github.com> Date: Wed, 30 Oct 2024 15:35:08 +0100 Subject: [PATCH] ENH: Add zonation with layer range info to grid metadata (#865) --- .../definitions/0.8.0/schema/fmu_results.json | 41 ++++++++++++++++ src/fmu/dataio/_model/specification.py | 20 ++++++++ src/fmu/dataio/providers/objectdata/_xtgeo.py | 19 ++++++++ tests/test_units/test_rms_context.py | 48 +++++++++++++++++++ 4 files changed, 128 insertions(+) diff --git a/schema/definitions/0.8.0/schema/fmu_results.json b/schema/definitions/0.8.0/schema/fmu_results.json index 37bf94938..cf4aa319b 100644 --- a/schema/definitions/0.8.0/schema/fmu_results.json +++ b/schema/definitions/0.8.0/schema/fmu_results.json @@ -356,6 +356,21 @@ "title": "Yshift", "type": "number" }, + "zonation": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ZoneDefinition" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Zonation" + }, "zscale": { "title": "Zscale", "type": "number" @@ -10146,6 +10161,32 @@ ], "title": "Workflow", "type": "object" + }, + "ZoneDefinition": { + "description": "Zone name and corresponding layer index min/max", + "properties": { + "max_layer_idx": { + "minimum": 0, + "title": "Max Layer Idx", + "type": "integer" + }, + "min_layer_idx": { + "minimum": 0, + "title": "Min Layer Idx", + "type": "integer" + }, + "name": { + "title": "Name", + "type": "string" + } + }, + "required": [ + "name", + "min_layer_idx", + "max_layer_idx" + ], + "title": "ZoneDefinition", + "type": "object" } }, "$id": "fmu_results.json", diff --git a/src/fmu/dataio/_model/specification.py b/src/fmu/dataio/_model/specification.py index cdc5f60fc..1e2e1fe4f 100644 --- a/src/fmu/dataio/_model/specification.py +++ b/src/fmu/dataio/_model/specification.py @@ -96,6 +96,26 @@ class CPGridSpecification(RowColumnLayer): zscale: float = Field(allow_inf_nan=False) """Scaling factor for the z-axis.""" + zonation: Optional[List[ZoneDefinition]] = Field(default=None) + """ + Zone names and corresponding layer index ranges. The list is ordered from + shallowest to deepest zone. Note the layer indices are zero-based; add 1 to + convert to corresponding layer number. + """ + + +class ZoneDefinition(BaseModel): + """Zone name and corresponding layer index min/max""" + + name: str + """Name of zone""" + + min_layer_idx: int = Field(ge=0) + """Minimum layer index for the zone""" + + max_layer_idx: int = Field(ge=0) + """Maximum layer index for the zone""" + class CPGridPropertySpecification(RowColumnLayer): """Specifies relevant values describing a corner point grid property object.""" diff --git a/src/fmu/dataio/providers/objectdata/_xtgeo.py b/src/fmu/dataio/providers/objectdata/_xtgeo.py index 77cb75ed5..f64dc836a 100644 --- a/src/fmu/dataio/providers/objectdata/_xtgeo.py +++ b/src/fmu/dataio/providers/objectdata/_xtgeo.py @@ -18,6 +18,7 @@ PointSpecification, PolygonsSpecification, SurfaceSpecification, + ZoneDefinition, ) from fmu.dataio._utils import get_geometry_ref, npfloat_to_float @@ -383,6 +384,24 @@ def get_spec(self) -> CPGridSpecification: xscale=npfloat_to_float(required["xscale"]), yscale=npfloat_to_float(required["yscale"]), zscale=npfloat_to_float(required["zscale"]), + zonation=self._get_zonation() if self.obj.subgrids else None, + ) + + def _get_zonation(self) -> list[ZoneDefinition]: + """ + Get the zonation for the grid as a list of zone definitions. + The list will be ordered from shallowest zone to deepest. + """ + return sorted( + [ + ZoneDefinition( + name=zone, + min_layer_idx=min(layerlist) - 1, + max_layer_idx=max(layerlist) - 1, + ) + for zone, layerlist in self.obj.subgrids.items() + ], + key=lambda x: x.min_layer_idx, ) diff --git a/tests/test_units/test_rms_context.py b/tests/test_units/test_rms_context.py index cd420ea66..e0a76b5c8 100644 --- a/tests/test_units/test_rms_context.py +++ b/tests/test_units/test_rms_context.py @@ -11,6 +11,7 @@ import pandas as pd import pytest +import xtgeo import yaml import fmu.dataio.dataio as dataio @@ -497,6 +498,53 @@ def test_grid_export_file_set_name(inside_rms_setup, grid): ) +@inside_rms +def test_grid_zonation_in_metadata(inside_rms_setup): + """Export the grid to file with correct metadata and name.""" + + grid = xtgeo.create_box_grid((3, 4, 5)) + + edata = dataio.ExportData( + config=inside_rms_setup["config"], content="depth", name="MyGrid" + ) + + assert grid.subgrids is None + meta = edata.generate_metadata(grid) + assert meta["data"]["spec"].get("zonation") is None + + grid.set_subgrids({"zone1": 2, "zone2": 3}) + meta = edata.generate_metadata(grid) + assert meta["data"]["spec"]["zonation"] == [ + {"name": "zone1", "min_layer_idx": 0, "max_layer_idx": 1}, + {"name": "zone2", "min_layer_idx": 2, "max_layer_idx": 4}, + ] + + # check that we get zones in order even though subgrids are not + grid.subgrids = {"zone1": [1, 2], "zone3": [5], "zone2": [3, 4]} + meta = edata.generate_metadata(grid) + assert meta["data"]["spec"]["zonation"] == [ + {"name": "zone1", "min_layer_idx": 0, "max_layer_idx": 1}, + {"name": "zone2", "min_layer_idx": 2, "max_layer_idx": 3}, + {"name": "zone3", "min_layer_idx": 4, "max_layer_idx": 4}, + ] + + # test that various incorrect subgrid input causes error (in XTGeo) + with pytest.raises(ValueError, match="Subgrids lengths"): + grid.subgrids = {"zone1": [1, 2, 3], "zone2": [3, 4, 5]} + + with pytest.raises(ValueError, match="not valid"): + grid.subgrids = {"zone1": [1, 2, 3], "zone2": [3, 4]} + + with pytest.raises(ValueError, match="not valid"): + grid.subgrids = {"zone1": [1, 2, 3], "zone2": [5, 6]} + + with pytest.raises(ValueError, match="Subgrids lengths"): + grid.set_subgrids({"zone1": 3, "zone2": 3}) + + with pytest.raises(ValueError, match="Subgrids lengths"): + grid.set_subgrids({"zone1": 1, "zone2": 1}) + + @inside_rms def test_gridproperty_export_file_set_name(inside_rms_setup, gridproperty): """Export the gridprop to file with correct metadata and name."""