Skip to content

Commit

Permalink
FIX: Handle surfaces with only undefined values (#656)
Browse files Browse the repository at this point in the history
  • Loading branch information
tnatt authored May 23, 2024
1 parent f748188 commit fe33877
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 13 deletions.
4 changes: 2 additions & 2 deletions schema/definitions/0.8.0/schema/fmu_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -291,12 +291,12 @@
"type": "number"
},
"zmax": {
"description": "Maximum z-coordinate",
"description": "Maximum z-coordinate. For regular surfaces this field represents the maximum surface value and it will be absent if all values are undefined.",
"title": "Zmax",
"type": "number"
},
"zmin": {
"description": "Minimum z-coordinate",
"description": "Minimum z-coordinate. For regular surfaces this field represents the minimum surface value and it will be absent if all values are undefined.",
"title": "Zmin",
"type": "number"
}
Expand Down
10 changes: 8 additions & 2 deletions src/fmu/dataio/datastructure/meta/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,11 +142,17 @@ class BoundingBox2D(BaseModel):

class BoundingBox3D(BoundingBox2D):
zmin: float = Field(
description="Minimum z-coordinate",
description=(
"Minimum z-coordinate. For regular surfaces this field represents the "
"minimum surface value and it will be absent if all values are undefined."
),
allow_inf_nan=False,
)
zmax: float = Field(
description="Maximum z-coordinate",
description=(
"Maximum z-coordinate. For regular surfaces this field represents the "
"maximum surface value and it will be absent if all values are undefined."
),
allow_inf_nan=False,
)

Expand Down
25 changes: 17 additions & 8 deletions src/fmu/dataio/providers/objectdata/_xtgeo.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,20 +48,29 @@ def get_spec(self) -> dict[str, Any]:
)

def get_bbox(self) -> dict[str, Any]:
"""Derive data.bbox for xtgeo.RegularSurface."""
"""
Derive data.bbox for xtgeo.RegularSurface. The zmin/zmax fields represents
the minimum/maximum surface values and should be absent in the metadata if the
surface only has undefined values.
"""
logger.info("Get bbox for RegularSurface")

return meta.content.BoundingBox3D(
if np.isfinite(self.obj.values).any():
return meta.content.BoundingBox3D(
xmin=float(self.obj.xmin),
xmax=float(self.obj.xmax),
ymin=float(self.obj.ymin),
ymax=float(self.obj.ymax),
zmin=float(self.obj.values.min()),
zmax=float(self.obj.values.max()),
).model_dump(mode="json", exclude_none=True)

return meta.content.BoundingBox2D(
xmin=float(self.obj.xmin),
xmax=float(self.obj.xmax),
ymin=float(self.obj.ymin),
ymax=float(self.obj.ymax),
zmin=float(self.obj.values.min()),
zmax=float(self.obj.values.max()),
).model_dump(
mode="json",
exclude_none=True,
)
).model_dump(mode="json", exclude_none=True)

def get_objectdata(self) -> DerivedObjectDescriptor:
"""Derive object data for xtgeo.RegularSurface."""
Expand Down
17 changes: 17 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from pathlib import Path

import fmu.dataio as dio
import numpy as np
import pandas as pd
import pytest
import xtgeo
Expand Down Expand Up @@ -401,6 +402,22 @@ def metadata_examples():
return _metadata_examples()


@pytest.fixture(name="regsurf_nan_only", scope="module")
def fixture_regsurf_nan_only():
"""Create an xtgeo surface with only NaNs."""
logger.debug("Ran %s", _current_function_name())
return xtgeo.RegularSurface(ncol=12, nrow=10, xinc=20, yinc=20, values=np.nan)


@pytest.fixture(name="regsurf_masked_only", scope="module")
def fixture_regsurf_masked_only():
"""Create an xtgeo surface with only masked values."""
logger.debug("Ran %s", _current_function_name())
regsurf = xtgeo.RegularSurface(ncol=12, nrow=10, xinc=20, yinc=20, values=1000)
regsurf.values = np.ma.masked_array(regsurf.values, mask=True)
return regsurf


# ======================================================================================
# Various objects
# ======================================================================================
Expand Down
28 changes: 27 additions & 1 deletion tests/test_schema/test_pydantic_logic.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from copy import deepcopy

import pytest
from fmu.dataio.datastructure.meta import Root
from fmu.dataio.datastructure.meta import Root, content
from pydantic import ValidationError

from ..utils import _metadata_examples
Expand Down Expand Up @@ -377,3 +377,29 @@ def test_content_whitelist(metadata_examples):
example_surface["data"]["content"] = "not_valid_content"
with pytest.raises(ValidationError):
Root.model_validate(example_surface)


def test_zmin_zmax_not_present_for_surfaces(metadata_examples):
"""
Test that the validation works for surface metadata without
zmin/zmax info or with zmin/zmax = None.
"""

# fetch surface example
example_surface = deepcopy(metadata_examples["surface_depth.yml"])

# assert validation with no changes and check that bbox is 3D
model = Root.model_validate(example_surface)
assert isinstance(model.root.data.root.bbox, content.BoundingBox3D)

# assert validation works with zmin/zmax = None, bbox should be 2D
example_surface["data"]["bbox"]["zmin"] = None
example_surface["data"]["bbox"]["zmax"] = None
model = Root.model_validate(example_surface)
assert isinstance(model.root.data.root.bbox, content.BoundingBox2D)

# assert validation works without zmin/zmax, bbox should be 2D
del example_surface["data"]["bbox"]["zmin"]
del example_surface["data"]["bbox"]["zmax"]
model = Root.model_validate(example_surface)
assert isinstance(model.root.data.root.bbox, content.BoundingBox2D)
26 changes: 26 additions & 0 deletions tests/test_units/test_dataio.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,32 @@ def test_content_deprecated_seismic_offset(regsurf, globalconfig2):
}


def test_surfaces_with_non_finite_values(
globalconfig1, regsurf_masked_only, regsurf_nan_only, regsurf
):
"""
When a surface has no finite values the zmin/zmax should not be present
in the metadata.
"""

eobj = ExportData(config=globalconfig1, content="time")

# test surface with only masked values
mymeta = eobj.generate_metadata(regsurf_masked_only)
assert "zmin" not in mymeta["data"]["bbox"]
assert "zmax" not in mymeta["data"]["bbox"]

# test surface with only nan values
mymeta = eobj.generate_metadata(regsurf_nan_only)
assert "zmin" not in mymeta["data"]["bbox"]
assert "zmax" not in mymeta["data"]["bbox"]

# test surface with finite values has zmin/zmax
mymeta = eobj.generate_metadata(regsurf)
assert "zmin" in mymeta["data"]["bbox"]
assert "zmax" in mymeta["data"]["bbox"]


def test_workflow_as_string(fmurun_w_casemetadata, monkeypatch, globalconfig1, regsurf):
"""
Check that having workflow as string works both in ExportData and on export.
Expand Down

0 comments on commit fe33877

Please sign in to comment.