Skip to content

Commit 581762c

Browse files
committed
ENH: Add to_rms()/from_rms() methods in NestedHybridGrid
1 parent e344fe9 commit 581762c

4 files changed

Lines changed: 648 additions & 536 deletions

File tree

docs/nestedhybridgrid.rst

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ Overview
1616

1717
The ``nestedhybridgrid`` module creates **nested hybrid grids** where a
1818
selected region of a coarse grid is replaced by a refined (subdivided)
19-
sub-grid. The two grids are merged into a single grid and connected
19+
sub-grid. The two grids are merged into a single grid and connected
2020
through Non-Neighbour Connections (NNCs).
2121

2222
The typical workflow is:
2323

24-
1. Define a coarse grid and a region property that marks which cells to
25-
refine.
26-
2. Call :func:`~fmu.tools.nestedhybridgrid.create_nested_hybrid_grid` to
24+
1. Define a coarse grid and a region property that marks cells to
25+
refine with value 1.
26+
2. Use the :class:`~fmu.tools.nestedhybridgrid.NestedHybridGrid` to
2727
produce the merged grid and an NNC table. You may need to export the NNC file to csv
2828
at this stage.
2929
3. Do rescaling from the original gridmodel (e.g. a finer geogrid) to the merged grid,
@@ -39,33 +39,22 @@ The example here runs within RMS, but similar workflows can be created for file
3939

4040
.. code-block:: python
4141
42-
import xtgeo
43-
from fmu.tools.nestedhybridgrid import (
44-
create_nested_hybrid_grid,
45-
)
42+
from fmu.tools.nestedhybridgrid import NestedHybridGrid
4643
47-
# Load grid and region property
48-
grid = xtgeo.grid_from_roxar(project, "Simgrid")
49-
region = xtgeo.gridproperty_from_roxar(project, "Simgrid", "REGION")
50-
51-
# Optionally load any other parameter you wish to preserve on grid
52-
#zone = xtgeo.gridproperty_from_roxar(project, "Simgrid", "Zone")
53-
#grid.append_prop(zone)
54-
55-
# Create nested hybrid grid (e.g. refine region 2 by 2×2×1)
56-
merged, nnc_table = create_nested_hybrid_grid(
57-
grid, region, target_region_id=2, refinement=(2, 2, 1)
44+
# Create nested hybrid grid (refine region 1 by 2×2×1)
45+
nhg = NestedHybridGrid.from_rms(
46+
project,
47+
grid_name="Simgrid",
48+
region_name="Refinement_region",
49+
refinement=(2, 2, 1),
50+
properties=["Zone"], # Optional list of properties to transfer to the output grid
5851
)
5952
60-
# store merged grid in RMS (or file)
61-
merged.to_roxar(project, "NestedHybrid")
62-
63-
# Optionally extract and store any necessary parameters from grid
64-
region2 = merged.get_prop_by_name("REGION")
65-
region2.to_roxar(project, "NestedHybrid", region2.name)
53+
# store nested grid with properties in RMS
54+
nhg.to_rms(project, "NestedHybrid")
6655
6756
# write the NNC pandas to disk; this will be applied for computing NNC's in the next script
68-
nnc_table.to_csv("path_to_some_csv_file.csv", index=False)
57+
nhg.nnc_table.to_csv("path_to_some_csv_file.csv", index=False)
6958
7059
7160
The next step is to do a rescaling from the original geogrid to the merged grid
@@ -116,8 +105,10 @@ Concepts
116105
NNC table
117106
^^^^^^^^^
118107

119-
The NNC table is a :class:`~pandas.DataFrame` returned by
120-
``create_nested_hybrid_grid`` with columns:
108+
The NNC table captures which coarse (mother) cells connect to which refined cells — information that
109+
xtgeo needs to compute transmissibilities across the refinement boundary. It is accessed via the property
110+
``nnc_table`` on the :class:`~fmu.tools.nestedhybridgrid.NestedHybridGrid` instance and is of type
111+
:class:`~pandas.DataFrame` with columns:
121112

122113
.. list-table::
123114
:header-rows: 1

src/fmu/tools/nestedhybridgrid/nestedhybrid.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import logging
2020
import warnings
21-
from typing import TYPE_CHECKING, Literal, Self, TypeAlias
21+
from typing import TYPE_CHECKING, Any, Literal, Self, TypeAlias
2222

2323
import numpy as np
2424
import pandas as pd
@@ -324,6 +324,33 @@ def __init__(
324324
self._layer_map_coarse = self._generate_layer_map_coarse()
325325
self._layer_map_refined = self._generate_layer_map_refined()
326326

327+
@classmethod
328+
def from_rms(
329+
cls,
330+
project: Any,
331+
grid_name: str,
332+
region_name: str,
333+
refinement: tuple[int, int, int],
334+
properties: list[str] | None = None,
335+
) -> Self:
336+
"""Create a NestedHybridGrid instance from an RMS project."""
337+
338+
coarse_grid = xtgeo.grid_from_roxar(project, grid_name)
339+
region = xtgeo.gridproperty_from_roxar(project, grid_name, region_name)
340+
341+
for propname in properties or []:
342+
prop = xtgeo.gridproperty_from_roxar(project, grid_name, propname)
343+
coarse_grid.append_prop(prop)
344+
345+
return cls(coarse_grid, region, refinement)
346+
347+
def to_rms(self, project: Any, grid_name: str) -> None:
348+
"""Write the nested hybrid grid and its properties to an RMS project."""
349+
self.grid.to_roxar(project, grid_name)
350+
351+
for prop in self.properties:
352+
prop.to_roxar(project, grid_name, prop.name)
353+
327354
@staticmethod
328355
def _validate_inputs(
329356
coarse_grid: xtgeo.Grid,
@@ -351,11 +378,11 @@ def _validate_inputs(
351378

352379
def _build_nested_hybrid_grid(self) -> xtgeo.Grid:
353380
"""Build the nested hybrid grid."""
354-
355381
coarse_grid = self._original_grid.copy()
356-
coarse_grid.append_prop(self._original_region)
357382

358383
region_name = self._original_region.name
384+
if region_name not in coarse_grid.propnames:
385+
coarse_grid.append_prop(self._original_region)
359386

360387
# Create the refined grid, i.e. crop and refine.
361388
refined_grid = _crop_for_region(coarse_grid, self._refined_bbox)

tests/nestedhybridgrid/test_nestedhybrid.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Tests for fmu.tools.nestedhybridgrid.nestedhybrid."""
22

3+
from unittest.mock import patch
4+
35
import numpy as np
46
import pandas as pd
57
import pytest
@@ -864,3 +866,130 @@ def test_empty_dataframe_writes_skeleton(self, tmp_path):
864866
nnc_to_flowsimulator_input(df, out)
865867
lines = out.read_text().splitlines()
866868
assert lines == ["NNC", "/"]
869+
870+
871+
# ---------------------------------------------------------------------------
872+
# Tests for NestedHybridGrid.from_rms and .to_rms
873+
# ---------------------------------------------------------------------------
874+
875+
876+
class TestNestedHybridGridRmsIO:
877+
"""Tests for from_rms and to_rms."""
878+
879+
def _make_nhg(self):
880+
"""Create a minimal NestedHybridGrid for testing."""
881+
grid, region, _ = _make_box_grid_with_region(dimension=(6, 6, 2))
882+
return NestedHybridGrid(coarse_grid=grid, region=region, refinement=(1, 1, 1))
883+
884+
def test_from_rms_returns_nestedhybridgrid(self):
885+
"""from_rms should return a NestedHybridGrid instance."""
886+
grid, region, _ = _make_box_grid_with_region(dimension=(6, 6, 2))
887+
888+
with (
889+
patch("xtgeo.grid_from_roxar", return_value=grid) as mock_grid,
890+
patch("xtgeo.gridproperty_from_roxar", return_value=region) as mock_prop,
891+
):
892+
nhg = NestedHybridGrid.from_rms(
893+
project="mock_project",
894+
grid_name="Grid",
895+
region_name="REGION",
896+
refinement=(1, 1, 1),
897+
)
898+
899+
assert isinstance(nhg, NestedHybridGrid)
900+
mock_grid.assert_called_once_with("mock_project", "Grid")
901+
mock_prop.assert_called_once_with("mock_project", "Grid", "REGION")
902+
903+
def test_from_rms_no_properties_does_not_load_extras(self):
904+
"""from_rms with properties=None should only load the region property."""
905+
grid, region, _ = _make_box_grid_with_region(dimension=(6, 6, 2))
906+
907+
with (
908+
patch("xtgeo.grid_from_roxar", return_value=grid),
909+
patch("xtgeo.gridproperty_from_roxar", return_value=region) as mock_prop,
910+
):
911+
NestedHybridGrid.from_rms(
912+
project="mock_project",
913+
grid_name="Grid",
914+
region_name="REGION",
915+
refinement=(1, 1, 1),
916+
)
917+
918+
mock_prop.assert_called_once_with("mock_project", "Grid", "REGION")
919+
920+
def test_from_rms_loads_optional_properties(self):
921+
"""from_rms should load and append extra properties when provided."""
922+
grid, region, _ = _make_box_grid_with_region(dimension=(6, 6, 2))
923+
poro = _make_constant_property(grid, "PORO", 0.3)
924+
sw = _make_constant_property(grid, "SW", 0.8)
925+
926+
with (
927+
patch("xtgeo.grid_from_roxar", return_value=grid),
928+
patch(
929+
"xtgeo.gridproperty_from_roxar",
930+
side_effect=[region, poro, sw],
931+
) as mock_prop,
932+
):
933+
nhg = NestedHybridGrid.from_rms(
934+
project="mock_project",
935+
grid_name="Grid",
936+
region_name="REGION",
937+
refinement=(1, 1, 1),
938+
properties=["PORO", "SW"],
939+
)
940+
941+
assert mock_prop.call_count == 3
942+
mock_prop.assert_any_call("mock_project", "Grid", "REGION")
943+
mock_prop.assert_any_call("mock_project", "Grid", "PORO")
944+
mock_prop.assert_any_call("mock_project", "Grid", "SW")
945+
assert isinstance(nhg, NestedHybridGrid)
946+
947+
def test_from_rms_optional_properties_appear_in_merged_grid(self):
948+
"""Properties loaded via from_rms should be present on the merged grid."""
949+
grid, region, _ = _make_box_grid_with_region(dimension=(6, 6, 2))
950+
poro = _make_constant_property(grid, "PORO", 0.3)
951+
952+
with (
953+
patch("xtgeo.grid_from_roxar", return_value=grid),
954+
patch("xtgeo.gridproperty_from_roxar", side_effect=[region, poro]),
955+
):
956+
nhg = NestedHybridGrid.from_rms(
957+
project="mock_project",
958+
grid_name="Grid",
959+
region_name="REGION",
960+
refinement=(1, 1, 1),
961+
properties=["PORO"],
962+
)
963+
964+
prop_names = {p.name for p in nhg.properties}
965+
assert prop_names == {"REGION", "PORO"}
966+
967+
def test_to_rms_writes_grid_once(self):
968+
"""to_rms should call to_roxar on the grid exactly once."""
969+
nhg = self._make_nhg()
970+
971+
with (
972+
patch.object(xtgeo.Grid, "to_roxar") as mock_grid_write,
973+
patch.object(xtgeo.GridProperty, "to_roxar") as mock_prop_write,
974+
):
975+
nhg.to_rms("mock_project", "NestedGrid")
976+
977+
mock_grid_write.assert_called_once_with("mock_project", "NestedGrid")
978+
mock_prop_write.assert_called_once_with("mock_project", "NestedGrid", "REGION")
979+
980+
def test_to_rms_writes_each_property(self):
981+
"""to_rms should call to_roxar for every property with correct args."""
982+
grid, region, _ = _make_box_grid_with_region(dimension=(6, 6, 2))
983+
poro = _make_constant_property(grid, "PORO", 0.3)
984+
grid.append_prop(poro)
985+
nhg = NestedHybridGrid(coarse_grid=grid, region=region, refinement=(1, 1, 1))
986+
987+
with (
988+
patch.object(xtgeo.Grid, "to_roxar"),
989+
patch.object(xtgeo.GridProperty, "to_roxar") as mock_prop_write,
990+
):
991+
nhg.to_rms("mock_project", "NestedGrid")
992+
993+
assert mock_prop_write.call_count == len(nhg.properties)
994+
for prop in nhg.properties:
995+
mock_prop_write.assert_any_call("mock_project", "NestedGrid", prop.name)

0 commit comments

Comments
 (0)