Skip to content

Commit ab84ff8

Browse files
authored
swfunction Add new method for height above FFL and refactoring (#300)
Breaking change: the `compute` method now return dictionary of grid property instead of grid property. The key for dictionary are: SW, HTOP, HBOT, HCENTER
1 parent baeb4d6 commit ab84ff8

3 files changed

Lines changed: 35 additions & 49 deletions

File tree

docs/properties.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ factors are constant numbers:
6969
invert=True,
7070
method=METHOD,
7171
)
72-
sw = sw_func.compute(CALC)
72+
sw = sw_func.compute(CALC)["SW"]
7373
sw.values[poro.values < 0.05] = 1.0 # 100% water when low porosity
7474
sw.to_roxar(project, GRIDNAME, SW_RESULT)
7575

src/fmu/tools/properties/swfunction.py

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@
2525
logger = logging.getLogger(__name__)
2626

2727
ALLOWED_METHODS = [
28-
"cell_center",
2928
"cell_center_above_ffl",
3029
"cell_corners_above_ffl",
30+
"truncated_cell_corners_above_ffl",
3131
]
3232

3333

@@ -67,8 +67,6 @@ class SwFunction:
6767
is called several time for the same grid.
6868
hbot: Optional. May speed up computation if provided, in case the function
6969
is called several time for the same grid.
70-
debug: If True, several "check parameters" will we given, either as grid
71-
properties (if working in RMS) or as ROFF files (if working outside RMS).
7270
tag: Optional string to identify debug parameters.
7371
7472
@@ -92,13 +90,12 @@ class SwFunction:
9290
invert: bool = False # if the SwJ function is on "reverse" form
9391

9492
# if None they will be computed here; otherwise they can be given explicitly:
95-
hcenter: xtgeo.GridProperty = None
96-
htop: xtgeo.GridProperty = None
97-
hbot: xtgeo.GridProperty = None
93+
hcenter: xtgeo.GridProperty | None = None
94+
htop: xtgeo.GridProperty | None = None
95+
hbot: xtgeo.GridProperty | None = None
9896

9997
# debug flag for additional parameters. Given in RMS if project is given; otherwise
10098
# as files on your working folder
101-
debug: bool = False
10299
tag: str = "" # identification tag to add to debug params
103100

104101
# derived and internal
@@ -118,12 +115,6 @@ def __post_init__(self) -> None:
118115
else:
119116
self._compute_htop_hbot()
120117

121-
if self.debug:
122-
if self.project:
123-
self.grid.to_roxar(self.project, "DEBUG_" + self.gridname)
124-
else:
125-
self.grid.to_file("debug_grid.roff")
126-
127118
def _process_input(self) -> None:
128119
"""Work with a, b, x, ie. inversing and convert from float."""
129120
logger.info("Process a, b, etc")
@@ -181,20 +172,6 @@ def _compute_htop_hbot(self) -> None:
181172
self.hcenter = hmid
182173
logger.info("Use method %s", self.method)
183174

184-
if self.debug:
185-
htop.name = "TOP_SW" + self.tag
186-
hbot.name = "BOT_SW" + self.tag
187-
hmid.name = "CENTER_SW" + self.tag
188-
if self.project:
189-
logger.debug("TAG is ", self.tag)
190-
htop.to_roxar(self.project, self.gridname, "HTOP" + self.tag)
191-
hbot.to_roxar(self.project, self.gridname, "HBOT" + self.tag)
192-
hmid.to_roxar(self.project, self.gridname, "HCENTER" + self.tag)
193-
else:
194-
htop.to_file(f"debug_htop{self.tag}.roff")
195-
hbot.to_file(f"debug_hbot{self.tag}.roff")
196-
hmid.to_file(f"debug_hcenter{self.tag}.roff")
197-
198175
def _sw_function_direct(self) -> None:
199176
"""Use function on form Sw = A*(M + X*h)^B; generic function!"""
200177
assert isinstance(self.a, xtgeo.GridProperty) # mypy
@@ -203,6 +180,7 @@ def _sw_function_direct(self) -> None:
203180
assert isinstance(self.m, xtgeo.GridProperty) # mypy
204181
assert isinstance(self.swira, xtgeo.GridProperty) # mypy
205182
assert isinstance(self.swmax, xtgeo.GridProperty) # mypy
183+
assert isinstance(self.hcenter, xtgeo.GridProperty)
206184

207185
# the direct function is mostly used to compare with integrated approach, as QC
208186
height = self.hcenter.values
@@ -237,20 +215,16 @@ def _sw_function_integrate_w_mterm(self) -> None:
237215
assert isinstance(self.m, xtgeo.GridProperty) # mypy
238216
assert isinstance(self.swira, xtgeo.GridProperty) # mypy
239217
assert isinstance(self.swmax, xtgeo.GridProperty) # mypy
218+
assert isinstance(self.htop, xtgeo.GridProperty) # mypy
219+
assert isinstance(self.hbot, xtgeo.GridProperty) # mypy
220+
240221
ht = (
241222
((1.0 / self.a.values) ** (1.0 / self.b.values)) - self.m.values
242223
) / self.x.values # threshold height
243224

244225
h2 = self.htop.values.copy() # h_top or H2 in integration
245226
h1 = self.hbot.values.copy() # h_bot or H1 in integration
246227

247-
if self.debug:
248-
tmp = xtgeo.GridProperty(self.grid, values=ht, name="HT" + self.tag)
249-
if self.project:
250-
tmp.to_roxar(self.project, self.gridname, "THRESHOLD HEIGHT" + self.tag)
251-
else:
252-
tmp.to_file(f"debug_ht{self.tag}.roff")
253-
254228
water = h2 * 0.0
255229
water = np.ma.where(h2 < ht, 1.0, water)
256230
water = np.ma.where(h2 <= 0.0, 1.0, water) # may occur for negative ht
@@ -317,7 +291,9 @@ def _compute_direct(self) -> None:
317291
if self._sw.values.min() < 0.0:
318292
raise RuntimeError(f"SW min out of range: {self._sw.values.min()}")
319293

320-
def compute(self, compute_method: str = "integrated") -> xtgeo.GridProperty:
294+
def compute(
295+
self, compute_method: str = "integrated"
296+
) -> dict[str, xtgeo.GridProperty]:
321297
"""Common compute function for saturation, and returns the Sw property"""
322298
if compute_method == "integrated":
323299
self._compute_integrated()
@@ -331,4 +307,11 @@ def compute(self, compute_method: str = "integrated") -> xtgeo.GridProperty:
331307
"Bug: Grid mask and Sw mask are not equal, contact developer!"
332308
)
333309

334-
return self._sw
310+
output_props = [self._sw, self.htop, self.hbot, self.hcenter]
311+
output_props_name = ["SW", "HTOP", "HBOT", "HCENTER"]
312+
313+
for prop, prop_name in zip(output_props, output_props_name):
314+
assert isinstance(prop, xtgeo.GridProperty)
315+
prop.name = self.tag + prop_name
316+
317+
return dict(zip(output_props_name, output_props))

tests/properties/test_swfunction.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
(0.3, -5, 12.5, True, "cell_center_above_ffl", 0.547251),
1919
(0.3, -5, 12.5, False, "cell_center_above_ffl", 0.549613),
2020
(0.3, -5, 12.5, False, "cell_corners_above_ffl", 0.549613),
21+
(1, -2, 13, True, "truncated_cell_corners_above_ffl", 0.408742),
22+
(1, -2, 13, False, "truncated_cell_corners_above_ffl", 0.412536),
2123
],
2224
)
2325
def test_swj_simple(avalue, bvalue, ffl, direct, cellmethod, expected_mean):
@@ -38,7 +40,7 @@ def test_swj_simple(avalue, bvalue, ffl, direct, cellmethod, expected_mean):
3840
invert=True,
3941
method=cellmethod,
4042
)
41-
sw = sw_obj.compute("direct" if direct else "integrated")
43+
sw = sw_obj.compute("direct" if direct else "integrated")["SW"]
4244
assert sw.values.mean() == pytest.approx(expected_mean, rel=0.01)
4345

4446

@@ -63,7 +65,8 @@ def test_swj_simple_x_zero():
6365
invert=True,
6466
method="cell_center_above_ffl",
6567
)
66-
sw = sw_obj.compute("integrated")
68+
69+
sw = sw_obj.compute("integrated")["SW"]
6770
assert not sw.values.mask[0, 0, 0]
6871

6972
# now make a grid with masked cells
@@ -86,7 +89,7 @@ def test_swj_simple_x_zero():
8689
invert=True,
8790
method="cell_center_above_ffl",
8891
)
89-
sw = sw_obj.compute("integrated")
92+
sw = sw_obj.compute("integrated")["SW"]
9093
assert sw.values.mask[0, 0, 0]
9194

9295

@@ -119,7 +122,7 @@ def test_swj_simple_threshold_2grids():
119122
invert=True,
120123
method=cellmethod,
121124
)
122-
sw = sw_obj.compute("integrated")
125+
sw = sw_obj.compute("integrated")["SW"]
123126
assert sw.values.mean() == pytest.approx(0.9689, rel=0.01)
124127

125128
sw_obj = SwFunction(
@@ -131,7 +134,7 @@ def test_swj_simple_threshold_2grids():
131134
invert=True,
132135
method=cellmethod,
133136
)
134-
sw = sw_obj.compute("integrated")
137+
sw = sw_obj.compute("integrated")["SW"]
135138

136139
assert sw.values.mean() == pytest.approx(0.9689, rel=0.01)
137140
assert float(sw.values[0, 0, 70]) == pytest.approx(1, rel=0.0001)
@@ -143,6 +146,8 @@ def test_swj_simple_threshold_2grids():
143146
(True, "cell_center_above_ffl", 0.7057, 0.046719), # n/a vs RMS
144147
(False, "cell_center_above_ffl", 0.70736, 0.046724), # n/a vs RMS
145148
(False, "cell_corners_above_ffl", 0.674485, 0.046791), # n/a vs RMS
149+
(True, "truncated_cell_corners_above_ffl", 0.67737, 0.046719), # n/a vs RMS
150+
(False, "truncated_cell_corners_above_ffl", 0.67931, 0.046724), # n/a vs RMS
146151
],
147152
)
148153
def test_swj_simple_reek(direct, cellmethod, expected_mean, exp_cell1, testdata_path):
@@ -174,7 +179,7 @@ def test_swj_simple_reek(direct, cellmethod, expected_mean, exp_cell1, testdata_
174179
method=cellmethod,
175180
)
176181

177-
sw = sw_obj.compute("direct" if direct else "integrated")
182+
sw = sw_obj.compute("direct" if direct else "integrated")["SW"]
178183

179184
assert sw.values.mean() == pytest.approx(expected_mean, rel=0.01)
180185

@@ -252,17 +257,16 @@ def test_sw_bvw():
252257
x=xvalue,
253258
ffl=ffl,
254259
method="cell_center_above_ffl",
255-
debug=False,
256260
)
257-
sw = sw_obj.compute("direct")
261+
sw = sw_obj.compute("direct")["SW"]
258262

259263
sw10 = float(sw.values[:, :, 20]) # 10 meter above contact
260264
assert sw10 == pytest.approx(manual10)
261265

262266
sw20 = float(sw.values[:, :, 10]) # 20 meter above contact
263267
assert sw20 == pytest.approx(0.13755086)
264268

265-
sw = sw_obj.compute("integrated")
269+
sw = sw_obj.compute("integrated")["SW"]
266270
sw10_i = float(sw.values[:, :, 20])
267271
assert sw10_i == pytest.approx(sw10, abs=0.0001)
268272

@@ -332,9 +336,8 @@ def test_sw_brooks_corey():
332336
x=xvalue,
333337
ffl=ffl,
334338
method="cell_center_above_ffl",
335-
debug=False,
336339
)
337-
sw = sw_obj.compute("direct")
340+
sw = sw_obj.compute("direct")["SW"]
338341

339342
sw10 = float(sw.values[:, :, 20]) # 10 meter above contact
340343
assert sw10 == pytest.approx(0.40303321)
@@ -344,6 +347,6 @@ def test_sw_brooks_corey():
344347
sw20 = float(sw.values[:, :, 10]) # 20 meter above contact
345348
assert sw20 == pytest.approx(0.28731234)
346349

347-
sw = sw_obj.compute("integrated")
350+
sw = sw_obj.compute("integrated")["SW"]
348351
sw10_i = float(sw.values[:, :, 20])
349352
assert sw10_i == pytest.approx(sw10, abs=0.001)

0 commit comments

Comments
 (0)