Skip to content

Commit

Permalink
ENH: add gaussian smoothing and improve undef handling (equinor#1002)
Browse files Browse the repository at this point in the history
  • Loading branch information
tnatt authored Nov 23, 2023
1 parent 540738e commit ab0a2f6
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 21 deletions.
33 changes: 21 additions & 12 deletions src/xtgeo/surface/_regsurf_gridding.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
"""Do gridding from 3D parameters"""

from __future__ import annotations

import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING

import numpy as np
import numpy.ma as ma
Expand All @@ -12,6 +14,9 @@
import xtgeo
from xtgeo.common import null_logger

if TYPE_CHECKING:
from xtgeo.surface import RegularSurface

logger = null_logger(__name__)

# Note: 'self' is an instance of RegularSurface
Expand Down Expand Up @@ -322,21 +327,25 @@ def surf_fill(self, fill_value=None):
logger.info("Do fill... DONE")


def smooth_median(self, iterations=1, width=1):
"""Smooth a surface using a median filter.
def _smooth(
self: RegularSurface,
window_function: Callable[[np.ndarray], np.ndarray],
iterations: int = 1,
) -> None:
"""
Smooth a RegularSurface using a window function.
.. versionadded:: 2.1
Original mask (undefined values) is stored before applying
smoothing on a filled array. Subsequently the original mask
is used to restore the undefined values in the output.
"""

mask = ma.getmaskarray(self.values)
tmpv = ma.filled(self.values, fill_value=np.nan)

for _itr in range(iterations):
tmpv = scipy.ndimage.median_filter(tmpv, width)
self.fill()

tmpv = ma.masked_invalid(tmpv)
smoothed_values = self.values
for _ in range(iterations):
smoothed_values = window_function(smoothed_values, mode="nearest")

# seems that false areas of invalids (masked) may be made; combat that:
self.values = tmpv
self.fill()
self.values = ma.array(self.values, mask=mask)
self.values = ma.array(smoothed_values, mask=mask)
32 changes: 28 additions & 4 deletions src/xtgeo/surface/regular_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
import numpy as np
import numpy.ma as ma
import pandas as pd
import scipy.ndimage

import xtgeo
import xtgeo.common.sys as xtgeosys
Expand Down Expand Up @@ -2065,18 +2066,41 @@ def fill(self, fill_value=None):
"""
_regsurf_gridding.surf_fill(self, fill_value=fill_value)

def smooth(self, method="median", iterations=1, width=1):
def smooth(
self,
method: Literal["median", "gaussian"] = "median",
iterations: int = 1,
width: float = 1,
) -> None:
"""Various smoothing methods for surfaces.
Args:
method: Smoothing method (median)
method: Smoothing method (median or gaussian)
iterations: Number of iterations
width: Range of influence (in nodes)
width:
- If method is 'median' range of influence is in nodes.
- If method is 'gaussian' range of influence is standard
deviation of the Gaussian kernel.
.. versionadded:: 2.1
"""

if method == "median":
_regsurf_gridding.smooth_median(self, iterations=iterations, width=width)
_regsurf_gridding._smooth(
self,
window_function=functools.partial(
scipy.ndimage.median_filter, size=int(width)
),
iterations=iterations,
)
elif method == "gaussian":
_regsurf_gridding._smooth(
self,
window_function=functools.partial(
scipy.ndimage.gaussian_filter, sigma=width
),
iterations=iterations,
)
else:
raise ValueError("Unsupported method for smoothing")

Expand Down
25 changes: 20 additions & 5 deletions tests/test_surface/test_regular_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -1148,20 +1148,35 @@ def test_fill():


def test_smoothing():
"""Smooth the the surface"""
"""Smooth the surface using median/gaussian filter"""

srf = xtgeo.surface_from_file(TESTSET1, fformat="irap_binary")

mean1 = srf.values.mean()
assert mean1 == pytest.approx(1698.65, abs=0.1)

srf.smooth(iterations=1, width=5)

mean2 = srf.values.mean()
# test median smoothing methoid
srf2 = srf.copy()
srf2.smooth(method="median", iterations=1, width=5)
mean2 = srf2.values.mean()
assert mean2 == pytest.approx(1698.65, abs=0.3) # smoothed ~same mean

assert mean1 != mean2 # but not exacly same

# test gaussian smoothing methoid
srf3 = srf.copy()
srf3.smooth(method="gaussian", iterations=1, width=5)
mean3 = srf3.values.mean()
assert mean3 == pytest.approx(1698.65, abs=0.3) # smoothed ~same mean
assert mean1 != mean3 # but not exacly same

# check that the three surfaces have different min values
min1 = srf.values.min()
min2 = srf2.values.min()
min3 = srf3.values.min()
assert min1 == pytest.approx(1547.20, abs=0.1)
assert min2 == pytest.approx(1553.62, abs=0.1)
assert min3 == pytest.approx(1566.91, abs=0.1)


def test_loadvalues_before_remove_deprecated(default_surface):
"""Test that load_values() has the claimed effect before deprecating __init__"""
Expand Down

0 comments on commit ab0a2f6

Please sign in to comment.