Skip to content

Commit 2a4c5f5

Browse files
lahus-0Lars Hustoft (SUB CCI UKI)
andauthored
ENH: nested grid clean up (#431)
Co-authored-by: Lars Hustoft (SUB CCI UKI) <lahus@st-lintgx0472.st.statoil.no>
1 parent 5ece28f commit 2a4c5f5

4 files changed

Lines changed: 149 additions & 57 deletions

File tree

docs/nestedhybridgrid.rst

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ The example here runs within RMS, but similar workflows can be created for file
4141
# Load grid and region property
4242
grid = xtgeo.grid_from_roxar(project, "Simgrid")
4343
region = xtgeo.gridproperty_from_roxar(project, "Simgrid", "REGION")
44+
45+
# Optionally load any other parameter you wish to preserve on grid
46+
#zone = xtgeo.gridproperty_from_roxar(project, "Simgrid", "Zone")
47+
#grid.append_prop(zone)
4448
4549
# Create nested hybrid grid (e.g. refine region 2 by 2×2×1)
4650
merged, nnc_table = create_nested_hybrid_grid(
@@ -50,11 +54,12 @@ The example here runs within RMS, but similar workflows can be created for file
5054
# store merged grid in RMS (or file)
5155
merged.to_roxar(project, "NestedHybrid")
5256
53-
merged.get_prop_by_name("NEST_ID")
54-
nest_id.to_roxar(project, "NestedHybrid", nest_id.name)
57+
# Optionally extract and store any necessary parameters from grid
58+
region2 = merged.get_prop_by_name("REGION")
59+
region2.to_roxar(project, "NestedHybrid", region2.name)
5560
5661
# write the NNC pandas to disk; this will be applied for computing NNC's in the next script
57-
nnc_table.write_csv("path_to_some_csv_file.csv", index=False)
62+
nnc_table.to_csv("path_to_some_csv_file.csv", index=False)
5863
5964
6065
The next step is to do a rescaling from the original geogrid to the merged grid
@@ -64,6 +69,7 @@ Further, we need to create NNC transmissibilities and generate file for flow sim
6469

6570
.. code-block:: python
6671
72+
import pandas as pd
6773
import xtgeo
6874
from fmu.tools.nestedhybridgrid import (
6975
nnc_to_flowsimulator_input,
@@ -74,7 +80,6 @@ Further, we need to create NNC transmissibilities and generate file for flow sim
7480
7581
# Load grid and region property which may be stored in RMS
7682
nested = xtgeo.grid_from_roxar(project, GNAME)
77-
region = nested.get_prop_by_name("NEST_ID") # if needed for QC
7883
7984
# load the NNC table
8085
nnc_table = pd.read_csv("path_to_some_nnc_file.csv")
@@ -88,15 +93,15 @@ Further, we need to create NNC transmissibilities and generate file for flow sim
8893
8994
# compute transmissibilities. Note that flow simulators do this for the normal cells/faults
9095
# so strictly speaking, only nnc_hybrid is needed here.
91-
tranx, trany, tranz, nnc_fault, nnc_hybrid, rbnd = merged.get_transmissibilities(
96+
tranx, trany, tranz, nnc_fault, nnc_hybrid, rbnd = nested.get_transmissibilities(
9297
permx, permy, permz, ntg, nnc_table=nnc_table
9398
)
9499
95100
# Export NNC keyword for Eclipse / OPM Flow
96101
nnc_to_flowsimulator_input(nnc_hybrid, "some_path/NNC_HYBRID.INC")
97102
98103
# Or map NNCs onto grid properties for visualisation
99-
tx_nnc, ty_nnc, tz_nnc = nnc_to_gridproperty(merged, nnc_hybrid)
104+
tx_nnc, ty_nnc, tz_nnc = nnc_to_gridproperty(nested, nnc_hybrid)
100105
tx_nnc.to_roxar(project, GNAME, "TRANX_NNC_QC") # etc
101106
102107
Concepts
@@ -127,15 +132,6 @@ This table is passed to :meth:`xtgeo.Grid.get_transmissibilities` via the
127132
face-overlap calculations (Sutherland–Hodgman algorithm) and two-point flux
128133
approximation (TPFA).
129134

130-
NEST_ID property
131-
^^^^^^^^^^^^^^^^
132-
133-
The merged grid carries a discrete property called ``NEST_ID``:
134-
135-
- **0** — inactive hole cells (carved out to make room for the refined region)
136-
- **1** — mother (coarse) cells
137-
- **2** — refined cells
138-
139135
Eclipse / OPM Flow export
140136
^^^^^^^^^^^^^^^^^^^^^^^^^^
141137

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ dependencies = [
4444
"pyyaml>=5.3",
4545
"scipy>=1.2",
4646
"xlrd>=1.2",
47-
"xtgeo>=4.21.0",
47+
"xtgeo>=4.23.0",
4848
]
4949

5050
[project.optional-dependencies]

src/fmu/tools/nestedhybridgrid/nestedhybrid.py

Lines changed: 64 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ def _compute_nnc_table(
130130
crop_origin: tuple[int, int, int],
131131
refinement: tuple[int, int, int],
132132
coarse_ncol: int,
133+
lmap1: np.ndarray,
134+
lmap2: np.ndarray,
133135
) -> pd.DataFrame:
134136
"""Compute NNC cell-pair mapping between mother and refined cells.
135137
@@ -155,6 +157,9 @@ def _compute_nnc_table(
155157
crop_origin: 0-based ``(i0, j0, k0)`` origin of the crop box.
156158
refinement: ``(rcol, rrow, rlay)`` refinement factors.
157159
coarse_ncol: Number of columns in the coarse grid (grid1 in the merge).
160+
lmap1: Numpy array with layer_mapping (input k -> output k) for grid1
161+
lmap2: Numpy array with layer_mapping (input k -> output k) for grid2
162+
158163
159164
Returns:
160165
A DataFrame with columns ``I1, J1, K1, I2, J2, K2, DIRECTION``.
@@ -163,6 +168,7 @@ def _compute_nnc_table(
163168

164169
rcol, rrow, rlay = refinement
165170
i0, j0, k0 = crop_origin
171+
166172
# In the merged grid, grid2 (refined) starts after a 1-column gap:
167173
i_offset = coarse_ncol + 1
168174

@@ -234,10 +240,10 @@ def _compute_nnc_table(
234240
{
235241
"I1": mi + 1,
236242
"J1": mj + 1,
237-
"K1": mk + 1,
243+
"K1": lmap1[mk] + 1,
238244
"I2": ri + i_offset + 1,
239245
"J2": rj + 1,
240-
"K2": rk + 1,
246+
"K2": lmap2[rk] + 1,
241247
"DIRECTION": direction,
242248
}
243249
)
@@ -271,6 +277,38 @@ def _set_actnum_by_region(
271277
grid.set_actnum(actnum)
272278

273279

280+
def _generate_layer_mappings(
281+
coarse_nlay: int,
282+
refined_nlay: int,
283+
refinement: tuple[int, int, int],
284+
crop_origin: tuple[int, int, int],
285+
) -> tuple[np.ndarray, np.ndarray]:
286+
"""Generate mappings from old to new layer number.
287+
Args:
288+
coarse_nlay: Number of layers in the coarse grid (grid1 in the merge).
289+
refined_nlay: Number of layers in the refined grid (grid2 in the merge).
290+
crop_origin: 0-based ``(i0, j0, k0)`` origin of the crop box.
291+
refinement: ``(rcol, rrow, rlay)`` refinement factors.
292+
293+
Returns:
294+
lmap1: Numpy array with layer_mapping (input k -> output k) for grid1
295+
lmap2: Numpy array with layer_mapping (input k -> output k) for grid2
296+
"""
297+
298+
_, _, rlay = refinement
299+
_, _, k0 = crop_origin
300+
301+
lmap1 = np.arange(coarse_nlay, dtype=np.int32)
302+
lmap1 = lmap1 + np.where(
303+
lmap1 < k0,
304+
0,
305+
(rlay - 1) * np.minimum(int(refined_nlay / rlay), lmap1 - k0),
306+
)
307+
lmap2 = np.arange(refined_nlay, dtype=np.int32) + k0
308+
309+
return (lmap1, lmap2)
310+
311+
274312
# ---------------------------------------------------------------------------
275313
# Public API
276314
# ---------------------------------------------------------------------------
@@ -281,17 +319,16 @@ def create_nested_hybrid_grid(
281319
region: xtgeo.GridProperty,
282320
target_region_id: int,
283321
refinement: tuple[int, int, int],
284-
) -> tuple[xtgeo.Grid, pd.DataFrame]:
322+
) -> tuple[
323+
xtgeo.Grid,
324+
pd.DataFrame,
325+
]:
285326
"""Create a nested hybrid grid by refining one region and merging it back.
286327
287328
The cells belonging to *target_region_id* are replaced by a refined
288-
(subdivided) version of the same region. A ``NEST_ID`` discrete property
289-
is attached to the merged grid, encoding the nested hybrid structure:
290-
291-
- ``NEST_ID == 1``: coarse (mother) grid cells.
292-
- ``NEST_ID == 2``: refined grid cells.
329+
(subdivided) version of the same region.
293330
294-
In addition, a **NNC mapping table** is returned that lists every
331+
A **NNC mapping table** is returned that lists every
295332
mother ↔ refined cell pair that should be connected by a Non-Neighbour
296333
Connection (NNC). The table is derived from the topological knowledge
297334
available at merge time (which original cell was refined and how its
@@ -316,9 +353,9 @@ def create_nested_hybrid_grid(
316353
refinement: ``(ncol, nrow, nlay)`` refinement factors.
317354
318355
Returns:
319-
A tuple ``(merged_grid, nnc_table)`` where *merged_grid* is a new
320-
:class:`xtgeo.Grid` with the refined region stitched back into the
321-
coarse grid and *nnc_table* is a :class:`pandas.DataFrame` mapping
356+
A tuple ``(merged_grid, nnc_table)`` where *merged_grid*
357+
is a new :class:`xtgeo.Grid` with the refined region stitched back into
358+
the coarse grid and *nnc_table* is a :class:`pandas.DataFrame` mapping
322359
mother cells to their connected refined cells.
323360
"""
324361
if any(r < 1 for r in refinement):
@@ -343,10 +380,19 @@ def create_nested_hybrid_grid(
343380
# 2. Refine the cropped grid.
344381
refined = cropped.copy()
345382
rcol, rrow, rlay = refinement
383+
_, _, olay = crop_origin
346384
refined.refine(refine_col=rcol, refine_row=rrow, refine_layer=rlay)
347385
_logger.info("Refined cropped grid dimensions: %s", refined.dimensions)
348386

349-
# 3. Compute the NNC mapping table *before* deactivation mutates anything.
387+
# 3. Generate layer mappings
388+
lmap1, lmap2 = _generate_layer_mappings(
389+
coarse_nlay=grid.nlay,
390+
refined_nlay=refined.nlay,
391+
crop_origin=crop_origin,
392+
refinement=refinement,
393+
)
394+
395+
# 4. Compute the NNC mapping table *before* deactivation mutates anything.
350396
# This uses the original region property to find boundary faces and
351397
# maps them through the crop → refine → merge index chain.
352398
nnc_table = _compute_nnc_table(
@@ -355,37 +401,20 @@ def create_nested_hybrid_grid(
355401
crop_origin=crop_origin,
356402
refinement=refinement,
357403
coarse_ncol=grid.ncol,
404+
lmap1=lmap1,
405+
lmap2=lmap2,
358406
)
359407

360-
# 4. Deactivate the target region in the coarse grid (will be replaced).
408+
# 5. Deactivate the target region in the coarse grid (will be replaced).
361409
coarse_region = grid.get_prop_by_name(region.name)
362410
_set_actnum_by_region(grid, coarse_region, target_region_id, invert=False)
363411

364-
# 5. In the refined grid keep only target-region cells active.
412+
# 6. In the refined grid keep only target-region cells active.
365413
refined_region = refined.get_prop_by_name(region.name)
366414
_set_actnum_by_region(refined, refined_region, target_region_id, invert=True)
367415

368-
# 6. Create NEST_ID properties before merging (1=mother, 2=refined).
369-
nest_id_coarse = xtgeo.GridProperty(
370-
grid,
371-
name="NEST_ID",
372-
discrete=True,
373-
values=np.where(grid.get_actnum().values == 1, 1, 0).astype(np.int32),
374-
codes={0: "inactive", 1: "mother", 2: "refined"},
375-
)
376-
grid.append_prop(nest_id_coarse)
377-
378-
nest_id_refined = xtgeo.GridProperty(
379-
refined,
380-
name="NEST_ID",
381-
discrete=True,
382-
values=np.where(refined.get_actnum().values == 1, 2, 0).astype(np.int32),
383-
codes={0: "inactive", 1: "mother", 2: "refined"},
384-
)
385-
refined.append_prop(nest_id_refined)
386-
387416
# 7. Merge the two grids.
388-
merged = xtgeo.grid_merge(grid, refined)
417+
merged = xtgeo.grid_merge(grid, refined, lmap1, lmap2)
389418
_logger.info("Merged grid dimensions: %s", merged.dimensions)
390419

391420
return merged, nnc_table

tests/nestedhybridgrid/test_nestedhybrid.py

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
nnc_to_flowsimulator_input,
1111
nnc_to_gridproperty,
1212
)
13+
from fmu.tools.nestedhybridgrid.nestedhybrid import (
14+
_generate_layer_mappings,
15+
)
1316

1417
# ---------------------------------------------------------------------------
1518
# Helpers
@@ -77,23 +80,23 @@ def test_basic_merge_dimensions(self):
7780
assert merged.nlay >= grid.nlay
7881

7982
def test_nest_id_property_attached(self):
80-
"""The merged grid must have a NEST_ID property."""
83+
"""The merged grid must have a refinement region property."""
8184
grid, region, rid = _make_box_grid_with_region(dimension=(6, 6, 2))
8285
merged, _ = create_nested_hybrid_grid(grid, region, rid, refinement=(1, 1, 1))
8386

84-
nest_id = merged.get_prop_by_name("NEST_ID")
87+
nest_id = merged.get_prop_by_name(region.name)
8588
assert nest_id is not None
8689
unique_vals = set(np.unique(np.ma.filled(nest_id.values, fill_value=0)))
8790
# Must contain at least mother (1) and refined (2) cells
8891
assert 1 in unique_vals
8992
assert 2 in unique_vals
9093

9194
def test_nest_id_values_consistent(self):
92-
"""Active cells should only have NEST_ID in {1, 2}."""
95+
"""Active cells should only have refinement region in {1, 2}."""
9396
grid, region, rid = _make_box_grid_with_region(dimension=(6, 6, 2))
9497
merged, _ = create_nested_hybrid_grid(grid, region, rid, refinement=(1, 1, 1))
9598

96-
nest_id = merged.get_prop_by_name("NEST_ID")
99+
nest_id = merged.get_prop_by_name(region.name)
97100
actnum = merged.get_actnum()
98101

99102
active_mask = actnum.values == 1
@@ -125,20 +128,84 @@ def test_original_grid_not_mutated(self):
125128
orig_nactive = grid.nactive
126129
orig_region_sum = int(np.ma.filled(region.values, 0).sum())
127130

128-
_ = create_nested_hybrid_grid(grid, region, rid, refinement=(2, 2, 1))
131+
_, _ = create_nested_hybrid_grid(grid, region, rid, refinement=(2, 2, 1))
129132

130133
assert grid.ncol == orig_ncol
131134
assert grid.nactive == orig_nactive
132135
assert int(np.ma.filled(region.values, 0).sum()) == orig_region_sum
133136

137+
def test_lmap_via_nnc(self):
138+
"""Test correct lmaps generated indirectly via nnc_table.
139+
Cells in nnc_table for layer number completely controlled by lmap.
140+
"""
141+
grid, region, rid = _make_box_grid_with_region(dimension=(3, 3, 3))
142+
143+
region.values = np.ones(region.values.shape)
144+
region.values[1][1][1] = rid
145+
146+
_, nnc_table = create_nested_hybrid_grid(
147+
grid, region, rid, refinement=(2, 2, 2)
148+
)
149+
nnc_table1 = nnc_table[
150+
(nnc_table["I1"] == 2)
151+
& (nnc_table["J1"] == 1)
152+
& (nnc_table["I2"] == 5)
153+
& (nnc_table["J2"] == 1)
154+
]
155+
156+
assert nnc_table1[(nnc_table1["K1"] == 2) & (nnc_table1["K2"] == 2)].shape == (
157+
1,
158+
7,
159+
)
160+
assert nnc_table1[(nnc_table1["K1"] == 2) & (nnc_table1["K2"] == 3)].shape == (
161+
1,
162+
7,
163+
)
164+
assert nnc_table1[(nnc_table1["K1"] == 3) & (nnc_table1["K2"] == 2)].shape == (
165+
0,
166+
7,
167+
)
168+
169+
def test_lmap_generation_simple(self):
170+
"""Tests that the correct layer mappings are generated"""
171+
172+
lmap1, lmap2 = _generate_layer_mappings(3, 2, (2, 2, 2), (1, 1, 1))
173+
174+
assert np.array_equal(lmap1, np.array([0, 1, 3]))
175+
assert np.array_equal(lmap2, np.array([1, 2]))
176+
177+
def test_lmap_generation_no_offset(self):
178+
"""Tests that the correct layer mappings are generated"""
179+
180+
lmap1, lmap2 = _generate_layer_mappings(3, 4, (2, 2, 2), (1, 1, 0))
181+
182+
assert np.array_equal(lmap1, np.array([0, 2, 4]))
183+
assert np.array_equal(lmap2, np.array([0, 1, 2, 3]))
184+
185+
def test_lmap_generation_full_offset(self):
186+
"""Tests that the correct layer mappings are generated"""
187+
188+
lmap1, lmap2 = _generate_layer_mappings(3, 4, (2, 2, 2), (1, 1, 3))
189+
190+
assert np.array_equal(lmap1, np.array([0, 1, 2]))
191+
assert np.array_equal(lmap2, np.array([3, 4, 5, 6]))
192+
193+
def test_lmap_generation_ref10(self):
194+
"""Tests that the correct layer mappings are generated"""
195+
196+
lmap1, lmap2 = _generate_layer_mappings(3, 20, (2, 2, 10), (1, 1, 1))
197+
198+
assert np.array_equal(lmap1, np.array([0, 1, 11]))
199+
assert np.array_equal(lmap2, np.arange(20) + 1)
200+
134201

135202
# ---------------------------------------------------------------------------
136203
# Tests for get_transmissibilities with nested hybrid NNCs
137204
# ---------------------------------------------------------------------------
138205

139206

140207
class TestTransmissibilitiesOnMergedGrid:
141-
"""Test calling get_transmissibilities on the merged grid with NEST_ID."""
208+
"""Test calling get_transmissibilities on the merged grid"""
142209

143210
@staticmethod
144211
def _build_merged_with_props(dimension=(6, 6, 2), refinement=(1, 1, 1)):

0 commit comments

Comments
 (0)