From 32fb9016eda1e081be98c36b811005b1c9ba88cc Mon Sep 17 00:00:00 2001 From: Alif Be Date: Thu, 18 Sep 2025 12:47:51 +0200 Subject: [PATCH 1/5] `swfunction` Add new method for height calculation --- src/fmu/tools/properties/swfunction.py | 2 +- tests/properties/test_swfunction.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/fmu/tools/properties/swfunction.py b/src/fmu/tools/properties/swfunction.py index 731a83aff..a1020158c 100644 --- a/src/fmu/tools/properties/swfunction.py +++ b/src/fmu/tools/properties/swfunction.py @@ -25,9 +25,9 @@ logger = logging.getLogger(__name__) ALLOWED_METHODS = [ - "cell_center", "cell_center_above_ffl", "cell_corners_above_ffl", + "truncated_cell_corners_above_ffl", ] diff --git a/tests/properties/test_swfunction.py b/tests/properties/test_swfunction.py index 423cc9d5f..ca85884f9 100644 --- a/tests/properties/test_swfunction.py +++ b/tests/properties/test_swfunction.py @@ -17,7 +17,8 @@ (1, -2, 13, False, "cell_center_above_ffl", 0.412536), (0.3, -5, 12.5, True, "cell_center_above_ffl", 0.547251), (0.3, -5, 12.5, False, "cell_center_above_ffl", 0.549613), - (0.3, -5, 12.5, False, "cell_corners_above_ffl", 0.549613), + (1, -2, 13, True, "truncated_cell_corners_above_ffl", 0.408742), + (1, -2, 13, False, "truncated_cell_corners_above_ffl", 0.412536), ], ) def test_swj_simple(avalue, bvalue, ffl, direct, cellmethod, expected_mean): From f3952a2cec3cce40ab54bbd8087d268cfa396b0c Mon Sep 17 00:00:00 2001 From: Alif Be Date: Thu, 18 Sep 2025 12:48:09 +0200 Subject: [PATCH 2/5] `swfunction` Refactor debug mode --- docs/properties.rst | 2 +- src/fmu/tools/properties/swfunction.py | 55 ++++++++++---------------- tests/properties/test_swfunction.py | 25 ++++++------ 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/docs/properties.rst b/docs/properties.rst index bf2ca3dfb..f498447c2 100644 --- a/docs/properties.rst +++ b/docs/properties.rst @@ -69,7 +69,7 @@ factors are constant numbers: invert=True, method=METHOD, ) - sw = sw_func.compute(CALC) + sw = sw_func.compute(CALC)[0] sw.values[poro.values < 0.05] = 1.0 # 100% water when low porosity sw.to_roxar(project, GRIDNAME, SW_RESULT) diff --git a/src/fmu/tools/properties/swfunction.py b/src/fmu/tools/properties/swfunction.py index a1020158c..b8909454a 100644 --- a/src/fmu/tools/properties/swfunction.py +++ b/src/fmu/tools/properties/swfunction.py @@ -67,8 +67,6 @@ class SwFunction: is called several time for the same grid. hbot: Optional. May speed up computation if provided, in case the function is called several time for the same grid. - debug: If True, several "check parameters" will we given, either as grid - properties (if working in RMS) or as ROFF files (if working outside RMS). tag: Optional string to identify debug parameters. @@ -92,13 +90,12 @@ class SwFunction: invert: bool = False # if the SwJ function is on "reverse" form # if None they will be computed here; otherwise they can be given explicitly: - hcenter: xtgeo.GridProperty = None - htop: xtgeo.GridProperty = None - hbot: xtgeo.GridProperty = None + hcenter: xtgeo.GridProperty | None = None + htop: xtgeo.GridProperty | None = None + hbot: xtgeo.GridProperty | None = None # debug flag for additional parameters. Given in RMS if project is given; otherwise # as files on your working folder - debug: bool = False tag: str = "" # identification tag to add to debug params # derived and internal @@ -118,12 +115,6 @@ def __post_init__(self) -> None: else: self._compute_htop_hbot() - if self.debug: - if self.project: - self.grid.to_roxar(self.project, "DEBUG_" + self.gridname) - else: - self.grid.to_file("debug_grid.roff") - def _process_input(self) -> None: """Work with a, b, x, ie. inversing and convert from float.""" logger.info("Process a, b, etc") @@ -181,20 +172,6 @@ def _compute_htop_hbot(self) -> None: self.hcenter = hmid logger.info("Use method %s", self.method) - if self.debug: - htop.name = "TOP_SW" + self.tag - hbot.name = "BOT_SW" + self.tag - hmid.name = "CENTER_SW" + self.tag - if self.project: - logger.debug("TAG is ", self.tag) - htop.to_roxar(self.project, self.gridname, "HTOP" + self.tag) - hbot.to_roxar(self.project, self.gridname, "HBOT" + self.tag) - hmid.to_roxar(self.project, self.gridname, "HCENTER" + self.tag) - else: - htop.to_file(f"debug_htop{self.tag}.roff") - hbot.to_file(f"debug_hbot{self.tag}.roff") - hmid.to_file(f"debug_hcenter{self.tag}.roff") - def _sw_function_direct(self) -> None: """Use function on form Sw = A*(M + X*h)^B; generic function!""" assert isinstance(self.a, xtgeo.GridProperty) # mypy @@ -203,6 +180,7 @@ def _sw_function_direct(self) -> None: assert isinstance(self.m, xtgeo.GridProperty) # mypy assert isinstance(self.swira, xtgeo.GridProperty) # mypy assert isinstance(self.swmax, xtgeo.GridProperty) # mypy + assert isinstance(self.hcenter, xtgeo.GridProperty) # the direct function is mostly used to compare with integrated approach, as QC height = self.hcenter.values @@ -237,6 +215,9 @@ def _sw_function_integrate_w_mterm(self) -> None: assert isinstance(self.m, xtgeo.GridProperty) # mypy assert isinstance(self.swira, xtgeo.GridProperty) # mypy assert isinstance(self.swmax, xtgeo.GridProperty) # mypy + assert isinstance(self.htop, xtgeo.GridProperty) # mypy + assert isinstance(self.hbot, xtgeo.GridProperty) # mypy + ht = ( ((1.0 / self.a.values) ** (1.0 / self.b.values)) - self.m.values ) / self.x.values # threshold height @@ -244,13 +225,6 @@ def _sw_function_integrate_w_mterm(self) -> None: h2 = self.htop.values.copy() # h_top or H2 in integration h1 = self.hbot.values.copy() # h_bot or H1 in integration - if self.debug: - tmp = xtgeo.GridProperty(self.grid, values=ht, name="HT" + self.tag) - if self.project: - tmp.to_roxar(self.project, self.gridname, "THRESHOLD HEIGHT" + self.tag) - else: - tmp.to_file(f"debug_ht{self.tag}.roff") - water = h2 * 0.0 water = np.ma.where(h2 < ht, 1.0, water) water = np.ma.where(h2 <= 0.0, 1.0, water) # may occur for negative ht @@ -317,7 +291,11 @@ def _compute_direct(self) -> None: if self._sw.values.min() < 0.0: raise RuntimeError(f"SW min out of range: {self._sw.values.min()}") - def compute(self, compute_method: str = "integrated") -> xtgeo.GridProperty: + def compute( + self, compute_method: str = "integrated" + ) -> tuple[ + xtgeo.GridProperty, xtgeo.GridProperty, xtgeo.GridProperty, xtgeo.GridProperty + ]: """Common compute function for saturation, and returns the Sw property""" if compute_method == "integrated": self._compute_integrated() @@ -331,4 +309,11 @@ def compute(self, compute_method: str = "integrated") -> xtgeo.GridProperty: "Bug: Grid mask and Sw mask are not equal, contact developer!" ) - return self._sw + output_props = [self._sw, self.htop, self.hbot, self.hcenter] + output_props_name = ["SW", "HTOP", "HBOT", "HCENTER"] + + for prop, prop_name in zip(output_props, output_props_name): + assert isinstance(prop, xtgeo.GridProperty) + prop.name = self.tag + prop_name + + return tuple(output_props) diff --git a/tests/properties/test_swfunction.py b/tests/properties/test_swfunction.py index ca85884f9..8a63a4492 100644 --- a/tests/properties/test_swfunction.py +++ b/tests/properties/test_swfunction.py @@ -39,7 +39,7 @@ def test_swj_simple(avalue, bvalue, ffl, direct, cellmethod, expected_mean): invert=True, method=cellmethod, ) - sw = sw_obj.compute("direct" if direct else "integrated") + sw = sw_obj.compute("direct" if direct else "integrated")[0] assert sw.values.mean() == pytest.approx(expected_mean, rel=0.01) @@ -64,7 +64,8 @@ def test_swj_simple_x_zero(): invert=True, method="cell_center_above_ffl", ) - sw = sw_obj.compute("integrated") + + sw = sw_obj.compute("integrated")[0] assert not sw.values.mask[0, 0, 0] # now make a grid with masked cells @@ -87,7 +88,7 @@ def test_swj_simple_x_zero(): invert=True, method="cell_center_above_ffl", ) - sw = sw_obj.compute("integrated") + sw = sw_obj.compute("integrated")[0] assert sw.values.mask[0, 0, 0] @@ -120,7 +121,7 @@ def test_swj_simple_threshold_2grids(): invert=True, method=cellmethod, ) - sw = sw_obj.compute("integrated") + sw = sw_obj.compute("integrated")[0] assert sw.values.mean() == pytest.approx(0.9689, rel=0.01) sw_obj = SwFunction( @@ -132,7 +133,7 @@ def test_swj_simple_threshold_2grids(): invert=True, method=cellmethod, ) - sw = sw_obj.compute("integrated") + sw = sw_obj.compute("integrated")[0] assert sw.values.mean() == pytest.approx(0.9689, rel=0.01) assert float(sw.values[0, 0, 70]) == pytest.approx(1, rel=0.0001) @@ -144,6 +145,8 @@ def test_swj_simple_threshold_2grids(): (True, "cell_center_above_ffl", 0.7057, 0.046719), # n/a vs RMS (False, "cell_center_above_ffl", 0.70736, 0.046724), # n/a vs RMS (False, "cell_corners_above_ffl", 0.674485, 0.046791), # n/a vs RMS + (True, "truncated_cell_corners_above_ffl", 0.67737, 0.046719), # n/a vs RMS + (False, "truncated_cell_corners_above_ffl", 0.67931, 0.046724), # n/a vs RMS ], ) def test_swj_simple_reek(direct, cellmethod, expected_mean, exp_cell1, testdata_path): @@ -175,7 +178,7 @@ def test_swj_simple_reek(direct, cellmethod, expected_mean, exp_cell1, testdata_ method=cellmethod, ) - sw = sw_obj.compute("direct" if direct else "integrated") + sw = sw_obj.compute("direct" if direct else "integrated")[0] assert sw.values.mean() == pytest.approx(expected_mean, rel=0.01) @@ -253,9 +256,8 @@ def test_sw_bvw(): x=xvalue, ffl=ffl, method="cell_center_above_ffl", - debug=False, ) - sw = sw_obj.compute("direct") + sw = sw_obj.compute("direct")[0] sw10 = float(sw.values[:, :, 20]) # 10 meter above contact assert sw10 == pytest.approx(manual10) @@ -263,7 +265,7 @@ def test_sw_bvw(): sw20 = float(sw.values[:, :, 10]) # 20 meter above contact assert sw20 == pytest.approx(0.13755086) - sw = sw_obj.compute("integrated") + sw = sw_obj.compute("integrated")[0] sw10_i = float(sw.values[:, :, 20]) assert sw10_i == pytest.approx(sw10, abs=0.0001) @@ -333,9 +335,8 @@ def test_sw_brooks_corey(): x=xvalue, ffl=ffl, method="cell_center_above_ffl", - debug=False, ) - sw = sw_obj.compute("direct") + sw = sw_obj.compute("direct")[0] sw10 = float(sw.values[:, :, 20]) # 10 meter above contact assert sw10 == pytest.approx(0.40303321) @@ -345,6 +346,6 @@ def test_sw_brooks_corey(): sw20 = float(sw.values[:, :, 10]) # 20 meter above contact assert sw20 == pytest.approx(0.28731234) - sw = sw_obj.compute("integrated") + sw = sw_obj.compute("integrated")[0] sw10_i = float(sw.values[:, :, 20]) assert sw10_i == pytest.approx(sw10, abs=0.001) From c11bab1c9ef20e3b30fdbee56780eef397296509 Mon Sep 17 00:00:00 2001 From: Alif Be Date: Mon, 22 Sep 2025 09:22:44 +0200 Subject: [PATCH 3/5] Return dict instead of tuple --- src/fmu/tools/properties/swfunction.py | 6 ++---- tests/properties/test_swfunction.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/fmu/tools/properties/swfunction.py b/src/fmu/tools/properties/swfunction.py index b8909454a..34cf8ffed 100644 --- a/src/fmu/tools/properties/swfunction.py +++ b/src/fmu/tools/properties/swfunction.py @@ -293,9 +293,7 @@ def _compute_direct(self) -> None: def compute( self, compute_method: str = "integrated" - ) -> tuple[ - xtgeo.GridProperty, xtgeo.GridProperty, xtgeo.GridProperty, xtgeo.GridProperty - ]: + ) -> dict[str, xtgeo.GridProperty]: """Common compute function for saturation, and returns the Sw property""" if compute_method == "integrated": self._compute_integrated() @@ -316,4 +314,4 @@ def compute( assert isinstance(prop, xtgeo.GridProperty) prop.name = self.tag + prop_name - return tuple(output_props) + return dict(zip(output_props_name, output_props)) diff --git a/tests/properties/test_swfunction.py b/tests/properties/test_swfunction.py index 8a63a4492..7cfc316fc 100644 --- a/tests/properties/test_swfunction.py +++ b/tests/properties/test_swfunction.py @@ -39,7 +39,7 @@ def test_swj_simple(avalue, bvalue, ffl, direct, cellmethod, expected_mean): invert=True, method=cellmethod, ) - sw = sw_obj.compute("direct" if direct else "integrated")[0] + sw = sw_obj.compute("direct" if direct else "integrated")["SW"] assert sw.values.mean() == pytest.approx(expected_mean, rel=0.01) @@ -65,7 +65,7 @@ def test_swj_simple_x_zero(): method="cell_center_above_ffl", ) - sw = sw_obj.compute("integrated")[0] + sw = sw_obj.compute("integrated")["SW"] assert not sw.values.mask[0, 0, 0] # now make a grid with masked cells @@ -88,7 +88,7 @@ def test_swj_simple_x_zero(): invert=True, method="cell_center_above_ffl", ) - sw = sw_obj.compute("integrated")[0] + sw = sw_obj.compute("integrated")["SW"] assert sw.values.mask[0, 0, 0] @@ -121,7 +121,7 @@ def test_swj_simple_threshold_2grids(): invert=True, method=cellmethod, ) - sw = sw_obj.compute("integrated")[0] + sw = sw_obj.compute("integrated")["SW"] assert sw.values.mean() == pytest.approx(0.9689, rel=0.01) sw_obj = SwFunction( @@ -133,7 +133,7 @@ def test_swj_simple_threshold_2grids(): invert=True, method=cellmethod, ) - sw = sw_obj.compute("integrated")[0] + sw = sw_obj.compute("integrated")["SW"] assert sw.values.mean() == pytest.approx(0.9689, rel=0.01) assert float(sw.values[0, 0, 70]) == pytest.approx(1, rel=0.0001) @@ -178,7 +178,7 @@ def test_swj_simple_reek(direct, cellmethod, expected_mean, exp_cell1, testdata_ method=cellmethod, ) - sw = sw_obj.compute("direct" if direct else "integrated")[0] + sw = sw_obj.compute("direct" if direct else "integrated")["SW"] assert sw.values.mean() == pytest.approx(expected_mean, rel=0.01) @@ -257,7 +257,7 @@ def test_sw_bvw(): ffl=ffl, method="cell_center_above_ffl", ) - sw = sw_obj.compute("direct")[0] + sw = sw_obj.compute("direct")["SW"] sw10 = float(sw.values[:, :, 20]) # 10 meter above contact assert sw10 == pytest.approx(manual10) @@ -265,7 +265,7 @@ def test_sw_bvw(): sw20 = float(sw.values[:, :, 10]) # 20 meter above contact assert sw20 == pytest.approx(0.13755086) - sw = sw_obj.compute("integrated")[0] + sw = sw_obj.compute("integrated")["SW"] sw10_i = float(sw.values[:, :, 20]) assert sw10_i == pytest.approx(sw10, abs=0.0001) @@ -336,7 +336,7 @@ def test_sw_brooks_corey(): ffl=ffl, method="cell_center_above_ffl", ) - sw = sw_obj.compute("direct")[0] + sw = sw_obj.compute("direct")["SW"] sw10 = float(sw.values[:, :, 20]) # 10 meter above contact assert sw10 == pytest.approx(0.40303321) @@ -346,6 +346,6 @@ def test_sw_brooks_corey(): sw20 = float(sw.values[:, :, 10]) # 20 meter above contact assert sw20 == pytest.approx(0.28731234) - sw = sw_obj.compute("integrated")[0] + sw = sw_obj.compute("integrated")["SW"] sw10_i = float(sw.values[:, :, 20]) assert sw10_i == pytest.approx(sw10, abs=0.001) From 267959f359be2054ec70bddec1019bed1667594c Mon Sep 17 00:00:00 2001 From: Alif Be Date: Mon, 22 Sep 2025 09:30:45 +0200 Subject: [PATCH 4/5] Update documentation --- docs/properties.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/properties.rst b/docs/properties.rst index f498447c2..7fcb47ac5 100644 --- a/docs/properties.rst +++ b/docs/properties.rst @@ -69,7 +69,7 @@ factors are constant numbers: invert=True, method=METHOD, ) - sw = sw_func.compute(CALC)[0] + sw = sw_func.compute(CALC)["SW"] sw.values[poro.values < 0.05] = 1.0 # 100% water when low porosity sw.to_roxar(project, GRIDNAME, SW_RESULT) From 522dbf1efcc1bed8e79115c00dd28c8bc834e4cc Mon Sep 17 00:00:00 2001 From: Alif Be Date: Mon, 22 Sep 2025 09:41:27 +0200 Subject: [PATCH 5/5] Reinstate removed test --- tests/properties/test_swfunction.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/properties/test_swfunction.py b/tests/properties/test_swfunction.py index 7cfc316fc..7e8a5a7f8 100644 --- a/tests/properties/test_swfunction.py +++ b/tests/properties/test_swfunction.py @@ -17,6 +17,7 @@ (1, -2, 13, False, "cell_center_above_ffl", 0.412536), (0.3, -5, 12.5, True, "cell_center_above_ffl", 0.547251), (0.3, -5, 12.5, False, "cell_center_above_ffl", 0.549613), + (0.3, -5, 12.5, False, "cell_corners_above_ffl", 0.549613), (1, -2, 13, True, "truncated_cell_corners_above_ffl", 0.408742), (1, -2, 13, False, "truncated_cell_corners_above_ffl", 0.412536), ],