From 84177cea25d1c22979b4961c979da8bacea07d92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Tue, 4 Jul 2023 19:48:31 +0200 Subject: [PATCH 01/18] preliminary interface --- skgstat/interfaces/gstatsim_mod.py | 326 +++++++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 skgstat/interfaces/gstatsim_mod.py diff --git a/skgstat/interfaces/gstatsim_mod.py b/skgstat/interfaces/gstatsim_mod.py new file mode 100644 index 0000000..6799fb8 --- /dev/null +++ b/skgstat/interfaces/gstatsim_mod.py @@ -0,0 +1,326 @@ +""" +Interface to GStatSim. + +You need to install GStatSim separately, as it is not a dependency of SciKit-GStat. + +```bash +pip install gstatsim +``` +""" +from typing import Any, overload, Optional, Union, Tuple, List +from typing_extensions import Literal +import warnings +import inspect +from joblib import Parallel, delayed + +import numpy as np +import pandas as pd +from skgstat.Variogram import Variogram +from skgstat.DirectionalVariogram import DirectionalVariogram + +try: + import gstatsim as gss + GSTATSIM_AVAILABLE = True + HAS_VERBOSE = 'verbose' in inspect.signature(gss.Interpolation.okrige_sgs).parameters +except ImportError: + GSTATSIM_AVAILABLE = False + HAS_VERBOSE = False + + + +# type the bounding box of a 2D grid +BBOX = Union[Tuple[int, int, int, int], Tuple[float, float, float, float]] + +class Grid: + """ + Representation of a regular grid, represented by a :class:`numpy.ndarray`, + with some additional meta-data about the grid. + """ + @overload + def __init__(self, bbox: Variogram, resolution: int) -> None: + ... + @overload + def __init__(self, bbox: Variogram, resolution=..., rows: int=..., cols: int=...) -> None: + ... + @overload + def __init__(self, bbox: BBOX, resolution: int) -> None: + ... + @overload + def __init__(self, bbox: BBOX, resolution=..., rows: int=..., cols: int=...) -> None: + ... + def __init__(self, bbox: Union[BBOX, Variogram], resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None) -> None: + # check if gstatsim is available + if not self.__check_gstatsim_available(): + raise ImportError('GStatSim is not available. Please install it with `pip install gstatsim`') + + # check the resolution and rows/cols: + if resolution is None and rows is None and cols is None: + raise AttributeError('Either resolution or rows/cols must be set') + + # get the resolution and rows/cols + if resolution is not None: + self._resolution = resolution + self._rows = None + self._cols = None + + # finally infer the bounding box from the variogram + self._infer_bbox(bbox) + + # infer the resolution from the bounding box + self._infer_resolution() + + def __check_gstatsim_available(self) -> bool: + """ + Check if GStatSim is available. + + Returns + ------- + bool + True if GStatSim is available, False otherwise. + + """ + if GSTATSIM_AVAILABLE: + return True + else: + return False + + def _infer_bbox(self, bbox: Union[BBOX, Variogram]) -> None: + """ + Infer the bounding box from the variogram. + """ + # check the type of the bbox + if isinstance(bbox, Variogram): + # get the bounding box + self._xmax = bbox.coordinates[:, 0].max() + self._xmin = bbox.coordinates[:, 0].min() + self._ymax = bbox.coordinates[:, 1].max() + self._ymin = bbox.coordinates[:, 1].min() + else: + self._xmin, self._xmax, self._ymin, self._ymax = bbox + + def _infer_resolution(self) -> None: + """ + Infer the resolution from the bounding box. + """ + # if resolution is set, infer cols and rows + if self._resolution is not None: + self._rows = int(np.rint((self._ymax - self._ymin + self._resolution) / self._resolution)) + self._cols = int(np.rint((self._xmax - self._xmin + self._resolution) / self._resolution)) + + # if rows and cols are set, infer resolution + elif self._rows is not None and self._cols is not None: + xres = (self._xmax - self._xmin) / self._cols + yres = (self._ymax - self._ymin) / self._rows + + # check if the resolution is the same in both directions + if xres == yres: + self._resolution = xres + else: + warnings.warn('The resolution is not the same in both directions. Adjusting the rows/cols setting') + self._resolution = min(xres, yres) + self._rows = None + self._cols = None + self._infer_resolution() + + @property + def resolution(self) -> Union[int, float]: + return self._resolution + + @resolution.setter + def resolution(self, resolution: Union[int, float]) -> None: + # set resolution + self._resolution = resolution + + # recalculate the rows and cols + self._rows = None + self._cols = None + self._infer_resolution() + + @property + def rows(self) -> int: + return self._rows + + @rows.setter + def rows(self, rows: int) -> None: + # set rows + self._rows = rows + + # recalculate the resolution + self._resolution = None + self._infer_resolution() + + @property + def cols(self) -> int: + return self._cols + + @cols.setter + def cols(self, cols: int) -> None: + # set cols + self._cols = cols + + # recalculate the resolution + self._resolution = None + self._infer_resolution() + + @property + def prediction_grid(self) -> np.ndarray: + grid: np.ndarray = gss.Gridding.prediction_grid(self._xmin, self._xmax, self._ymin, self._ymax, self._resolution) + + return grid + + @property + def shape(self) -> Tuple[int, int]: + if self._rows is None or self._cols is None: + self._infer_resolution() + return self._rows, self._cols + + def __call__(self, *args: Any, **kwds: Any) -> Any: + return self.prediction_grid + + def __str__(self) -> str: + return f'' + + +@overload +def prediction_grid(bbox: Variogram, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy: Literal[False] = False) -> Grid: + ... +@overload +def prediction_grid(bbox: Variogram, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy: Literal[True]) -> np.ndarray: + ... +@overload +def prediction_grid(bbox: BBOX, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy: Literal[False] = False) -> Grid: + ... +@overload +def prediction_grid(bbox: BBOX, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy: Literal[True]) -> np.ndarray: + ... +def prediction_grid(bbox: Union[BBOX, Variogram], resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None, as_numpy: bool = False) -> Union[Grid, np.ndarray]: + if resolution is not None: + grid = Grid(bbox, resolution=resolution) + elif rows is not None and cols is not None: + grid = Grid(bbox, rows=rows, cols=cols) + else: + raise AttributeError('Either resolution or rows/cols must be set') + + if as_numpy: + return grid.prediction_grid + else: + return grid + + +def simulation_params( + variogram: Variogram, + grid: Optional[Union[Grid, np.ndarray, Union[int, float], Tuple[int, int]]] = None, + minor_range: Optional[Union[int, float]] = None, +) -> Tuple[Union[Grid, np.ndarray], pd.DataFrame, list]: + # the simulation needs the condition data as pd.DataFrame + data = np.concatenate((variogram.coordinates, variogram.values.reshape(-1, 1)), axis=1) + df = pd.DataFrame(data, columns=['x', 'y', 'v']) + + # build the grid + if isinstance(grid, (int, float)): + # resolution is given + grid = Grid(variogram, resolution=grid) + elif isinstance(grid, (tuple, list)): + # rows / cols are given + grid = Grid(variogram, rows=grid[0], cols=grid[1]) + elif grid is None: + # we infer the resolution + grid = Grid(variogram, resolution=1) + new_res = min((grid._xmax - grid._xmin) / 100., (grid._ymax - grid._ymin) / 100.) + grid.resolution = new_res + + # now grid should be a Grid object or a numpy.ndarray + if not isinstance(grid, (Grid, np.ndarray)): + raise AttributeError('grid must be either a Grid object, a resolution or a tuple/list of rows and cols') + + # get the variogram parameters + major = variogram.parameters[0] + nugget = variogram.parameters[-1] + sill = variogram.parameters[1] + vtype = variogram.model.__name__ + + # extract the azimuth + if isinstance(variogram, DirectionalVariogram): + azimuth = variogram.azimuth + if minor_range is None: + raise AttributeError('minor_range must be set for directional variograms') + else: + azimuth = 0 + minor_range = major + + # build the params + params = [azimuth, nugget, major, minor_range, sill, vtype] + + return grid, df, params + + +def run_simulation( + grid: Union[Grid, np.ndarray], + cond_data: pd.DataFrame, + vario_params: list, + num_points: int = 20, + radius: Optional[Union[int, float]] = None, + method: Union[Literal['simple'], Literal['ordinary']] = 'simple', + verbose: bool = False +) -> np.ndarray: + # get the radius + if radius is None: + radius = vario_params[2] * 3 + + # get the right interpolation method + if method.lower() == 'simple': + sim_func = gss.Interpolation.skrige_sgs + elif method.lower() == 'ordinary': + sim_func = gss.Interpolation.okrige_sgs + else: + raise AttributeError('method must be either "simple" or "ordinary"') + + # get an prediction grid + if isinstance(grid, Grid): + pred_grid = grid.prediction_grid + else: + pred_grid = grid + + # run the simulation + if HAS_VERBOSE: + field: np.ndarray = sim_func(pred_grid, cond_data, 'x', 'y', 'v', num_points, vario_params, radius, verbose) + else: + field: np.ndarray = sim_func(pred_grid, cond_data, 'x', 'y', 'v', num_points, vario_params, radius) + + if isinstance(grid, Grid): + return field.reshape(grid.shape) + else: + return field + + +def simulate( + variogram: Variogram, + grid: Optional[Union[Grid, np.ndarray, Union[int, float], Tuple[int, int]]] = None, + num_points: int = 20, + radius: Optional[Union[int, float]] = None, + method: Union[Literal['simple'], Literal['ordinary']] = 'simple', + verbose: bool = False, + n_jobs: int = 1, + size: int = 1, + **kwargs, +) -> List[np.ndarray]: + # extract minor_range + minor_range = kwargs.get('minor_range', None) + + # get the simulation parameters + grid, cond_data, vario_params = simulation_params(variogram, grid, minor_range) + + # multiprocessing? + if n_jobs > 1 and size > 1: + # build th pool + pool = Parallel(n_jobs=n_jobs, verbose=0 if not verbose else 10) + + # wrapper + gen = (delayed(run_simulation)(grid, cond_data, vario_params, num_points, radius, method, verbose) for _ in range(size)) + + # run the simulation + fields = pool(gen) + return fields + else: + field = run_simulation(grid, cond_data, vario_params, num_points, radius, method, verbose) + return [field] From eecc104b074dc8295db2cdd6dff582fb675bf8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Tue, 4 Jul 2023 19:50:36 +0200 Subject: [PATCH 02/18] add typing extensions to requirements --- requirements.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d418082..699e548 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ matplotlib numba scikit-learn imageio -tqdm \ No newline at end of file +tqdm +typing_extensions \ No newline at end of file From 5b208daab7e7b7f7b5e111c59a9d8275c65ca19e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 09:08:10 +0200 Subject: [PATCH 03/18] change typing to avoid circular imports --- skgstat/interfaces/gstatsim_mod.py | 86 +++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/skgstat/interfaces/gstatsim_mod.py b/skgstat/interfaces/gstatsim_mod.py index 6799fb8..abd15bf 100644 --- a/skgstat/interfaces/gstatsim_mod.py +++ b/skgstat/interfaces/gstatsim_mod.py @@ -7,7 +7,7 @@ pip install gstatsim ``` """ -from typing import Any, overload, Optional, Union, Tuple, List +from typing import Any, overload, Optional, Union, Tuple, List, TYPE_CHECKING from typing_extensions import Literal import warnings import inspect @@ -15,8 +15,9 @@ import numpy as np import pandas as pd -from skgstat.Variogram import Variogram -from skgstat.DirectionalVariogram import DirectionalVariogram +if TYPE_CHECKING: + from skgstat.Variogram import Variogram + from skgstat.DirectionalVariogram import DirectionalVariogram try: import gstatsim as gss @@ -27,7 +28,6 @@ HAS_VERBOSE = False - # type the bounding box of a 2D grid BBOX = Union[Tuple[int, int, int, int], Tuple[float, float, float, float]] @@ -37,10 +37,10 @@ class Grid: with some additional meta-data about the grid. """ @overload - def __init__(self, bbox: Variogram, resolution: int) -> None: + def __init__(self, bbox: 'Variogram', resolution: int) -> None: ... @overload - def __init__(self, bbox: Variogram, resolution=..., rows: int=..., cols: int=...) -> None: + def __init__(self, bbox: 'Variogram', resolution=..., rows: int=..., cols: int=...) -> None: ... @overload def __init__(self, bbox: BBOX, resolution: int) -> None: @@ -48,7 +48,35 @@ def __init__(self, bbox: BBOX, resolution: int) -> None: @overload def __init__(self, bbox: BBOX, resolution=..., rows: int=..., cols: int=...) -> None: ... - def __init__(self, bbox: Union[BBOX, Variogram], resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None) -> None: + def __init__(self, bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None) -> None: + """ + Initialize a new Grid instance. + + Parameters + ---------- + bbox : Union[BBOX, Variogram] + The bounding box or variogram to use for the grid. + resolution : Optional[int], optional + The resolution of the grid, by default None. + rows : Optional[int], optional + The number of rows in the grid, by default None. + cols : Optional[int], optional + The number of columns in the grid, by default None. + + Raises + ------ + ImportError + If the `gstatsim` package is not available. + AttributeError + If neither `resolution` nor `rows`/`cols` are set. + + Notes + ----- + If `resolution` is set, it will be used as cell size and `rows` and `cols` are ignored. + If `rows` and `cols` are set, the grid will have `rows` rows and `cols` columns and + `resolution` has to be set to `None`. + + """ # check if gstatsim is available if not self.__check_gstatsim_available(): raise ImportError('GStatSim is not available. Please install it with `pip install gstatsim`') @@ -84,12 +112,27 @@ def __check_gstatsim_available(self) -> bool: else: return False - def _infer_bbox(self, bbox: Union[BBOX, Variogram]) -> None: + def _infer_bbox(self, bbox: Union[BBOX, 'Variogram']) -> None: """ - Infer the bounding box from the variogram. + Infer the bounding box from the variogram or bounding box. + + Parameters + ---------- + bbox : Union[BBOX, Variogram] + The bounding box or variogram to infer the bounding box from. + + Raises + ------ + TypeError + If `bbox` is not a `BBOX` or `Variogram` instance. + + Notes + ----- + If `bbox` is a `Variogram` instance, the bounding box is inferred from the coordinates of the variogram. + If `bbox` is a `BBOX` instance, the bounding box is set to the values of the instance. """ # check the type of the bbox - if isinstance(bbox, Variogram): + if isinstance(bbox, 'Variogram'): # get the bounding box self._xmax = bbox.coordinates[:, 0].max() self._xmin = bbox.coordinates[:, 0].min() @@ -101,6 +144,11 @@ def _infer_bbox(self, bbox: Union[BBOX, Variogram]) -> None: def _infer_resolution(self) -> None: """ Infer the resolution from the bounding box. + If `resolution` is set, the number of rows and columns are inferred from the bounding box. + If `rows` and `cols` are set, the resolution is inferred from the number of rows and columns. + If neither `resolution` nor `rows`/`cols` are set, a warning is issued and the + resolution is set to the minimum of the x and y resolutions. + """ # if resolution is set, infer cols and rows if self._resolution is not None: @@ -182,18 +230,18 @@ def __str__(self) -> str: @overload -def prediction_grid(bbox: Variogram, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy: Literal[False] = False) -> Grid: +def prediction_grid(bbox: 'Variogram', resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy = False) -> Grid: ... @overload -def prediction_grid(bbox: Variogram, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy: Literal[True]) -> np.ndarray: +def prediction_grid(bbox: 'Variogram', resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy = True) -> np.ndarray: ... @overload -def prediction_grid(bbox: BBOX, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy: Literal[False] = False) -> Grid: +def prediction_grid(bbox: BBOX, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy = False) -> Grid: ... @overload -def prediction_grid(bbox: BBOX, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy: Literal[True]) -> np.ndarray: +def prediction_grid(bbox: BBOX, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy = True) -> np.ndarray: ... -def prediction_grid(bbox: Union[BBOX, Variogram], resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None, as_numpy: bool = False) -> Union[Grid, np.ndarray]: +def prediction_grid(bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None, as_numpy: bool = False) -> Union[Grid, np.ndarray]: if resolution is not None: grid = Grid(bbox, resolution=resolution) elif rows is not None and cols is not None: @@ -208,7 +256,7 @@ def prediction_grid(bbox: Union[BBOX, Variogram], resolution: Optional[int] = No def simulation_params( - variogram: Variogram, + variogram: 'Variogram', grid: Optional[Union[Grid, np.ndarray, Union[int, float], Tuple[int, int]]] = None, minor_range: Optional[Union[int, float]] = None, ) -> Tuple[Union[Grid, np.ndarray], pd.DataFrame, list]: @@ -239,7 +287,9 @@ def simulation_params( sill = variogram.parameters[1] vtype = variogram.model.__name__ - # extract the azimuth + # due to circular imports we need to import it here + from skgstat import DirectionalVariogram + # extract the azimuth if isinstance(variogram, DirectionalVariogram): azimuth = variogram.azimuth if minor_range is None: @@ -294,7 +344,7 @@ def run_simulation( def simulate( - variogram: Variogram, + variogram: 'Variogram', grid: Optional[Union[Grid, np.ndarray, Union[int, float], Tuple[int, int]]] = None, num_points: int = 20, radius: Optional[Union[int, float]] = None, From bd5eab19f523390933216d1f1c8926d993ec667e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 09:08:26 +0200 Subject: [PATCH 04/18] draft GStatSim interface --- skgstat/Variogram.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/skgstat/Variogram.py b/skgstat/Variogram.py index 1858c70..677b321 100644 --- a/skgstat/Variogram.py +++ b/skgstat/Variogram.py @@ -2,8 +2,9 @@ Variogram class """ import copy +from typing_extensions import Literal import warnings -from typing import Iterable, Callable, Union, Tuple +from typing import Iterable, Callable, List, Optional, Union, Tuple import numpy as np from pandas import DataFrame @@ -17,6 +18,7 @@ from skgstat.util import shannon_entropy from .MetricSpace import MetricSpace, ProbabalisticMetricSpace from skgstat.interfaces.gstools import skgstat_to_gstools, skgstat_to_krige +from skgstat.interfaces import gstatsim_mod class Variogram(object): @@ -2586,6 +2588,27 @@ def to_DataFrame(self, n=100, force=False): self._model.__name__: data} ).copy() + def gstatsim_prediction_grid(self, resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None, as_numpy: bool = False) -> Union[gstatsim_mod.Grid, np.ndarray]: + """ + """ + grid = gstatsim_mod.prediction_grid(self, resolution, rows, cols, as_numpy=as_numpy) + return grid + + def simulation( + self, + grid: Optional[Union[gstatsim_mod.Grid, np.ndarray, Union[int, float], Tuple[int, int]]] = None, + num_points: int = 20, + radius: Optional[Union[int, float]] = None, + method: Union[Literal['simple'], Literal['ordinary']] = 'simple', + verbose: bool = False, + n_jobs: int = 1, + size: int = 1, + **kwargs, + ) -> List[np.ndarray]: + """""" + fields = gstatsim_mod.simulate(self, grid, num_points, radius, method, verbose, n_jobs, size, **kwargs) + return fields + def to_gstools(self, **kwargs): """ Instantiate a corresponding GSTools CovModel. From 48dfabd4a1aea585053d0977cba0daa198666eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 10:51:22 +0200 Subject: [PATCH 05/18] add some docstrings --- skgstat/interfaces/gstatsim_mod.py | 133 ++++++++++++++++++++++++++++- 1 file changed, 130 insertions(+), 3 deletions(-) diff --git a/skgstat/interfaces/gstatsim_mod.py b/skgstat/interfaces/gstatsim_mod.py index abd15bf..bb5bf05 100644 --- a/skgstat/interfaces/gstatsim_mod.py +++ b/skgstat/interfaces/gstatsim_mod.py @@ -242,6 +242,34 @@ def prediction_grid(bbox: BBOX, resolution: Optional[int], rows: Optional[int], def prediction_grid(bbox: BBOX, resolution: Optional[int], rows: Optional[int], cols: Optional[int], as_numpy = True) -> np.ndarray: ... def prediction_grid(bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None, as_numpy: bool = False) -> Union[Grid, np.ndarray]: + """ + Generate a prediction grid as used by `gstatsim.Interpolation` methods. + + Parameters + ---------- + bbox : Union[BBOX, Variogram] + The bounding box defining the spatial extent of the prediction grid. It can be either a BBOX object or a Variogram object. + resolution : Optional[int], optional + The resolution of the prediction grid. The number of cells along each axis. + Either `resolution` or `rows` and `cols` should be set. Default is None. + rows : Optional[int], optional + The number of rows in the prediction grid. Required only when `resolution` is not provided. Default is None. + cols : Optional[int], optional + The number of columns in the prediction grid. Required only when `resolution` is not provided. Default is None. + as_numpy : bool, optional + If True, return the prediction grid as a numpy array. If False, return as a Grid object. Default is False. + + Returns + ------- + Union[Grid, np.ndarray] + The prediction grid either as a Grid object or a numpy array, based on the value of `as_numpy`. + + Raises + ------ + AttributeError + If neither `resolution` nor `rows` and `cols` are set. + + """. if resolution is not None: grid = Grid(bbox, resolution=resolution) elif rows is not None and cols is not None: @@ -256,10 +284,40 @@ def prediction_grid(bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = def simulation_params( - variogram: 'Variogram', - grid: Optional[Union[Grid, np.ndarray, Union[int, float], Tuple[int, int]]] = None, - minor_range: Optional[Union[int, float]] = None, + variogram: 'Variogram', + grid: Optional[Union[Grid, np.ndarray, Union[int, float], Tuple[int, int]]] = None, + minor_range: Optional[Union[int, float]] = None ) -> Tuple[Union[Grid, np.ndarray], pd.DataFrame, list]: + """ + Generate simulation parameters for the `Interpolation.skrige_sgs` and + `Interpolation.okrige_sgs`methods of GStatSim. + + Parameters + ---------- + variogram : 'Variogram' + The variogram object used for simulation. + grid : Optional[Union[Grid, np.ndarray, Union[int, float], Tuple[int, int]]], optional + The grid object or array defining the simulation grid. + It can be either a :class:`Grid ` object, + a :class:`numpy array `, a resolution value (int or float), + or a tuple/list of rows and columns. If None, the resolution is inferred from the variogram. + minor_range : Optional[Union[int, float]], optional + The minor range for directional variograms. Required only for directional variograms. + Default is None. + + Returns + ------- + Tuple[Union[Grid, np.ndarray], pd.DataFrame, list] + A tuple containing the simulation grid, the condition data as a pandas DataFrame, and a list of simulation parameters. + + Raises + ------ + AttributeError + If grid is not a Grid object, a numpy array, a resolution value, or a tuple/list of rows and columns. + AttributeError + If minor_range is not set for directional variograms. + + """ # the simulation needs the condition data as pd.DataFrame data = np.concatenate((variogram.coordinates, variogram.values.reshape(-1, 1)), axis=1) df = pd.DataFrame(data, columns=['x', 'y', 'v']) @@ -313,6 +371,39 @@ def run_simulation( method: Union[Literal['simple'], Literal['ordinary']] = 'simple', verbose: bool = False ) -> np.ndarray: + """ + Run a sequential gaussian simulation using GStatSim. + + Parameters + ---------- + grid : Union[Grid, np.ndarray] + The grid object or array representing the simulation grid. + cond_data : pd.DataFrame + The condition data as a pandas DataFrame containing the coordinates and values. + vario_params : list + A list of variogram parameters used for simulation. + num_points : int, optional + The number of neighboring points used for interpolation. Default is 20. + radius : Optional[Union[int, float]], optional + The search radius for neighboring points. If not provided, it is calculated as + 3 times the major range from the variogram parameters. + method : Union[Literal['simple'], Literal['ordinary']], optional + The interpolation method to use. Either 'simple' for simple kriging + or 'ordinary' for ordinary kriging. Default is 'simple'. + verbose : bool, optional + If True, enable verbose output during the simulation. Default is False. + + Returns + ------- + np.ndarray + The simulated field as a numpy array. + + Raises + ------ + AttributeError + If the provided method is neither 'simple' nor 'ordinary'. + + """ # get the radius if radius is None: radius = vario_params[2] * 3 @@ -354,6 +445,42 @@ def simulate( size: int = 1, **kwargs, ) -> List[np.ndarray]: + """ + Perform spatial simulation using GStatSim. The GStatSim simulation is + can be run in parallel using joblib. Note that this will enable the + parallel execution of **multiple** simulations, it does not parallelize + the simulation itself. + + Parameters + ---------- + variogram : 'Variogram' + The variogram object used for simulation. + grid : Optional[Union[Grid, np.ndarray, Union[int, float], Tuple[int, int]]], optional + The grid object or array representing the simulation grid. + It can be either a Grid object, a numpy array, a resolution value (int or float), + or a tuple/list of rows and columns. If None, the resolution is inferred from the variogram. + num_points : int, optional + The number of neighboring points used for interpolation. Default is 20. + radius : Optional[Union[int, float]], optional + The search radius for neighboring points. If not provided, it is calculated based on the major + range from the variogram parameters. + method : Union[Literal['simple'], Literal['ordinary']], optional + The interpolation method to use. Either 'simple' for simple kriging or 'ordinary' for ordinary kriging. Default is 'simple'. + verbose : bool, optional + If True, enable verbose output during the simulation. Default is False. + n_jobs : int, optional + The number of parallel jobs to run. Default is 1 (no parallelization). + size : int, optional + The number of simulation realizations to generate. Default is 1. + **kwargs : optional keyword arguments + Additional arguments to pass to the simulation_params function. + + Returns + ------- + List[np.ndarray] + A list of simulated fields, each represented as a numpy array. + + """ # extract minor_range minor_range = kwargs.get('minor_range', None) From 7804cc314e94a79f9792de86d1a06f10258c0a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 10:59:49 +0200 Subject: [PATCH 06/18] fix typo --- skgstat/interfaces/gstatsim_mod.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skgstat/interfaces/gstatsim_mod.py b/skgstat/interfaces/gstatsim_mod.py index bb5bf05..936e212 100644 --- a/skgstat/interfaces/gstatsim_mod.py +++ b/skgstat/interfaces/gstatsim_mod.py @@ -97,7 +97,7 @@ def __init__(self, bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = N # infer the resolution from the bounding box self._infer_resolution() - def __check_gstatsim_available(self) -> bool: + def __check_gstatsim_available(self) -> bool: # pragma: no cover """ Check if GStatSim is available. @@ -131,8 +131,10 @@ def _infer_bbox(self, bbox: Union[BBOX, 'Variogram']) -> None: If `bbox` is a `Variogram` instance, the bounding box is inferred from the coordinates of the variogram. If `bbox` is a `BBOX` instance, the bounding box is set to the values of the instance. """ + # import the Variogram class only here to avoid circular imports + from skgstat import Variogram # check the type of the bbox - if isinstance(bbox, 'Variogram'): + if isinstance(bbox, Variogram): # get the bounding box self._xmax = bbox.coordinates[:, 0].max() self._xmin = bbox.coordinates[:, 0].min() @@ -269,7 +271,7 @@ def prediction_grid(bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = AttributeError If neither `resolution` nor `rows` and `cols` are set. - """. + """ if resolution is not None: grid = Grid(bbox, resolution=resolution) elif rows is not None and cols is not None: From 9fa6b290b171373c7cecc9e4b3bdf0871a32d4b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 11:45:52 +0200 Subject: [PATCH 07/18] fixed column setting bugs --- skgstat/interfaces/gstatsim_mod.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/skgstat/interfaces/gstatsim_mod.py b/skgstat/interfaces/gstatsim_mod.py index 936e212..b640fb9 100644 --- a/skgstat/interfaces/gstatsim_mod.py +++ b/skgstat/interfaces/gstatsim_mod.py @@ -19,7 +19,7 @@ from skgstat.Variogram import Variogram from skgstat.DirectionalVariogram import DirectionalVariogram -try: +try: # pragma: no cover import gstatsim as gss GSTATSIM_AVAILABLE = True HAS_VERBOSE = 'verbose' in inspect.signature(gss.Interpolation.okrige_sgs).parameters @@ -86,10 +86,9 @@ def __init__(self, bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = N raise AttributeError('Either resolution or rows/cols must be set') # get the resolution and rows/cols - if resolution is not None: - self._resolution = resolution - self._rows = None - self._cols = None + self._resolution = resolution + self._rows = rows + self._cols = cols # finally infer the bounding box from the variogram self._infer_bbox(bbox) @@ -153,12 +152,12 @@ def _infer_resolution(self) -> None: """ # if resolution is set, infer cols and rows - if self._resolution is not None: + if self._resolution is not None and self._rows is None and self._cols is None: self._rows = int(np.rint((self._ymax - self._ymin + self._resolution) / self._resolution)) self._cols = int(np.rint((self._xmax - self._xmin + self._resolution) / self._resolution)) # if rows and cols are set, infer resolution - elif self._rows is not None and self._cols is not None: + elif self._rows is not None and self._cols is not None and self._resolution is None: xres = (self._xmax - self._xmin) / self._cols yres = (self._ymax - self._ymin) / self._rows @@ -171,6 +170,21 @@ def _infer_resolution(self) -> None: self._rows = None self._cols = None self._infer_resolution() + + # only rows or only cols are set + elif self._rows is not None and self._cols is None: + if self._resolution is None: + self._resolution = (self._ymax - self._ymin) / self._rows + self._cols = int(np.rint((self._xmax - self._xmin + self._resolution) / self._resolution)) + else: + self._cols = int(np.rint((self._xmax - self._xmin + self._resolution) / self._resolution)) + + elif self._rows is None and self._cols is not None: + if self._resolution is None: + self._resolution = (self._xmax - self._xmin) / self._cols + self._rows = int(np.rint((self._ymax - self._ymin + self._resolution) / self._resolution)) + else: + self._rows = int(np.rint((self._ymax - self._ymin + self._resolution) / self._resolution)) @property def resolution(self) -> Union[int, float]: @@ -194,6 +208,7 @@ def rows(self) -> int: def rows(self, rows: int) -> None: # set rows self._rows = rows + self._cols = None # recalculate the resolution self._resolution = None @@ -207,6 +222,7 @@ def cols(self) -> int: def cols(self, cols: int) -> None: # set cols self._cols = cols + self._rows = None # recalculate the resolution self._resolution = None From 8bdfe4391942bdab1ad9c296f0932e7bcef3d635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 11:45:59 +0200 Subject: [PATCH 08/18] add some tests --- skgstat/tests/test_gstatsim_interface.py | 101 +++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 skgstat/tests/test_gstatsim_interface.py diff --git a/skgstat/tests/test_gstatsim_interface.py b/skgstat/tests/test_gstatsim_interface.py new file mode 100644 index 0000000..fa92a17 --- /dev/null +++ b/skgstat/tests/test_gstatsim_interface.py @@ -0,0 +1,101 @@ +import pytest +import numpy as np +import skgstat as skg +from skgstat.interfaces.gstatsim_mod import Grid, prediction_grid + +# Test data generation +coords, vals = skg.data.pancake(N=60).get('sample') +variogram = skg.Variogram(coords, vals, maxlag=0.6, n_lags=12) + +# Test cases +def test_grid_initialization_resolution(): + # Initialize grid with resolution + grid = Grid(variogram, resolution=0.1) + + assert grid.resolution == 0.1 + assert grid.rows == 4661 + assert grid.cols == 4731 + +def test_grid_initialization_infer_resolution(): + # Initialize grid without rows/cols + # Set resolution + grid = Grid(variogram, rows=74, cols=66) + + assert np.abs(grid.resolution - 6.3) < 0.01 + +def test_grid_initialization_no_resolution_rows_cols(): + # Initialize grid without resolution or rows/cols (expecting an error) + with pytest.raises(AttributeError): + grid = Grid(variogram, resolution=None, rows=None, cols=None) + +def test_grid_resolution_setting(): + # Set resolution + grid = Grid(variogram, rows=100, cols=100) + + assert np.abs(grid.resolution - 4.66) < 0.01 + + # Set resolution + grid.resolution = 5 + assert grid.rows == 94 + assert grid.cols == 96 + +def test_grid_rows_setting(): + grid = Grid(variogram, resolution=5) + + # Set rows + grid.rows = 50 + + assert grid.cols == 52 + assert grid.rows == 50 + assert np.abs(grid.resolution - 9.32) < 0.01 + +def test_grid_cols_setting(): + grid = Grid(variogram, resolution=5) + + # Set cols + grid.cols = 100 + + assert grid.cols == 100 + assert grid.rows == 100 + assert np.abs(grid.resolution - 4.73) < 0.01 + +def test_grid_prediction_grid(): + # Test prediction grid generation + grid = Grid(variogram, resolution=0.1) + prediction_grid = grid.prediction_grid + + assert isinstance(prediction_grid, np.ndarray) + assert prediction_grid.shape == (grid.shape[0] * grid.shape[1], 2) + +def test_grid_call_operator(): + # Test calling the Grid instance + grid = Grid(variogram, resolution=0.1) + prediction_grid = grid() + + assert isinstance(prediction_grid, np.ndarray) + assert prediction_grid.shape == (grid.shape[0] * grid.shape[1], 2) + +def test_grid_str_representation(): + # Test string representation + grid = Grid(variogram, resolution=0.1) + assert str(grid) == f'' + + +def test_prediction_grid_resolution(): + grid = prediction_grid(variogram, resolution=1, as_numpy=False) + + assert isinstance(grid, Grid) + assert grid.rows == 467 + assert grid.cols == 474 + + +def test_prediction_grid_cols_rows(): + grid = prediction_grid(variogram, cols=50, rows=50, as_numpy=True) + + assert isinstance(grid, np.ndarray) + assert grid.shape == (2652 , 2) + + +# Run the tests +if __name__ == '__main__': + pytest.main() From 0e89977ebcc986abb0d0b6b9d82856f670652cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 11:52:13 +0200 Subject: [PATCH 09/18] add gstatsim to test dependencies --- requirements.unittest.3.10.txt | 1 + requirements.unittest.3.6.txt | 3 ++- requirements.unittest.3.7.txt | 3 ++- requirements.unittest.3.8.txt | 3 ++- requirements.unittest.3.9.txt | 1 + 5 files changed, 8 insertions(+), 3 deletions(-) diff --git a/requirements.unittest.3.10.txt b/requirements.unittest.3.10.txt index 29546ab..93111bc 100644 --- a/requirements.unittest.3.10.txt +++ b/requirements.unittest.3.10.txt @@ -4,3 +4,4 @@ pytest-depends pykrige gstools>=1.3 plotly +gstatsim diff --git a/requirements.unittest.3.6.txt b/requirements.unittest.3.6.txt index 270dd01..89886b8 100644 --- a/requirements.unittest.3.6.txt +++ b/requirements.unittest.3.6.txt @@ -3,4 +3,5 @@ pytest-cov pytest-depends pykrige gstools>=1.3 -plotly \ No newline at end of file +plotly +gstatsim \ No newline at end of file diff --git a/requirements.unittest.3.7.txt b/requirements.unittest.3.7.txt index 270dd01..89886b8 100644 --- a/requirements.unittest.3.7.txt +++ b/requirements.unittest.3.7.txt @@ -3,4 +3,5 @@ pytest-cov pytest-depends pykrige gstools>=1.3 -plotly \ No newline at end of file +plotly +gstatsim \ No newline at end of file diff --git a/requirements.unittest.3.8.txt b/requirements.unittest.3.8.txt index 270dd01..89886b8 100644 --- a/requirements.unittest.3.8.txt +++ b/requirements.unittest.3.8.txt @@ -3,4 +3,5 @@ pytest-cov pytest-depends pykrige gstools>=1.3 -plotly \ No newline at end of file +plotly +gstatsim \ No newline at end of file diff --git a/requirements.unittest.3.9.txt b/requirements.unittest.3.9.txt index 9f9a97e..9e840a2 100644 --- a/requirements.unittest.3.9.txt +++ b/requirements.unittest.3.9.txt @@ -4,3 +4,4 @@ pytest-depends pykrige gstools>=1.3 plotly +gstatsim From c5040d9271f1d96e461264640e2e8a61fc5dc880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 14:31:40 +0200 Subject: [PATCH 10/18] check for python>=3.7 if using gstatsim --- skgstat/interfaces/gstatsim_mod.py | 39 +++++++++++++++++------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/skgstat/interfaces/gstatsim_mod.py b/skgstat/interfaces/gstatsim_mod.py index b640fb9..2cc9c34 100644 --- a/skgstat/interfaces/gstatsim_mod.py +++ b/skgstat/interfaces/gstatsim_mod.py @@ -11,6 +11,7 @@ from typing_extensions import Literal import warnings import inspect +import sys from joblib import Parallel, delayed import numpy as np @@ -28,6 +29,26 @@ HAS_VERBOSE = False +def __check_gstatsim_available() -> bool: # pragma: no cover + """ + Check if GStatSim is available. + + Returns + ------- + bool + True if GStatSim is available, False otherwise. + + """ + if sys.version_info.minor <= 6: + warnings.warn('GStatSim is not compatible with Python 3.6 and below. Please upgrade to Python 3.7 or higher.') + return False + + if GSTATSIM_AVAILABLE: + return True + else: + raise ImportError('GStatSim is not available. Please install it with `pip install gstatsim`') + + # type the bounding box of a 2D grid BBOX = Union[Tuple[int, int, int, int], Tuple[float, float, float, float]] @@ -78,8 +99,7 @@ def __init__(self, bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = N """ # check if gstatsim is available - if not self.__check_gstatsim_available(): - raise ImportError('GStatSim is not available. Please install it with `pip install gstatsim`') + __check_gstatsim_available() # check the resolution and rows/cols: if resolution is None and rows is None and cols is None: @@ -96,21 +116,6 @@ def __init__(self, bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = N # infer the resolution from the bounding box self._infer_resolution() - def __check_gstatsim_available(self) -> bool: # pragma: no cover - """ - Check if GStatSim is available. - - Returns - ------- - bool - True if GStatSim is available, False otherwise. - - """ - if GSTATSIM_AVAILABLE: - return True - else: - return False - def _infer_bbox(self, bbox: Union[BBOX, 'Variogram']) -> None: """ Infer the bounding box from the variogram or bounding box. From e682928665f51d09873d79e87466e601aa8153eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 14:31:50 +0200 Subject: [PATCH 11/18] emove python3.6 tests for gstatsim --- requirements.unittest.3.6.txt | 3 +-- skgstat/tests/test_gstatsim_interface.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/requirements.unittest.3.6.txt b/requirements.unittest.3.6.txt index 89886b8..270dd01 100644 --- a/requirements.unittest.3.6.txt +++ b/requirements.unittest.3.6.txt @@ -3,5 +3,4 @@ pytest-cov pytest-depends pykrige gstools>=1.3 -plotly -gstatsim \ No newline at end of file +plotly \ No newline at end of file diff --git a/skgstat/tests/test_gstatsim_interface.py b/skgstat/tests/test_gstatsim_interface.py index fa92a17..7634fb6 100644 --- a/skgstat/tests/test_gstatsim_interface.py +++ b/skgstat/tests/test_gstatsim_interface.py @@ -1,6 +1,8 @@ import pytest import numpy as np import skgstat as skg +import sys + from skgstat.interfaces.gstatsim_mod import Grid, prediction_grid # Test data generation @@ -8,6 +10,7 @@ variogram = skg.Variogram(coords, vals, maxlag=0.6, n_lags=12) # Test cases +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_initialization_resolution(): # Initialize grid with resolution grid = Grid(variogram, resolution=0.1) @@ -16,6 +19,8 @@ def test_grid_initialization_resolution(): assert grid.rows == 4661 assert grid.cols == 4731 + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_initialization_infer_resolution(): # Initialize grid without rows/cols # Set resolution @@ -23,11 +28,15 @@ def test_grid_initialization_infer_resolution(): assert np.abs(grid.resolution - 6.3) < 0.01 + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_initialization_no_resolution_rows_cols(): # Initialize grid without resolution or rows/cols (expecting an error) with pytest.raises(AttributeError): grid = Grid(variogram, resolution=None, rows=None, cols=None) + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_resolution_setting(): # Set resolution grid = Grid(variogram, rows=100, cols=100) @@ -39,6 +48,8 @@ def test_grid_resolution_setting(): assert grid.rows == 94 assert grid.cols == 96 + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_rows_setting(): grid = Grid(variogram, resolution=5) @@ -49,6 +60,8 @@ def test_grid_rows_setting(): assert grid.rows == 50 assert np.abs(grid.resolution - 9.32) < 0.01 + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_cols_setting(): grid = Grid(variogram, resolution=5) @@ -58,7 +71,9 @@ def test_grid_cols_setting(): assert grid.cols == 100 assert grid.rows == 100 assert np.abs(grid.resolution - 4.73) < 0.01 - + + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_prediction_grid(): # Test prediction grid generation grid = Grid(variogram, resolution=0.1) @@ -67,6 +82,8 @@ def test_grid_prediction_grid(): assert isinstance(prediction_grid, np.ndarray) assert prediction_grid.shape == (grid.shape[0] * grid.shape[1], 2) + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_call_operator(): # Test calling the Grid instance grid = Grid(variogram, resolution=0.1) @@ -75,12 +92,15 @@ def test_grid_call_operator(): assert isinstance(prediction_grid, np.ndarray) assert prediction_grid.shape == (grid.shape[0] * grid.shape[1], 2) + +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_grid_str_representation(): # Test string representation grid = Grid(variogram, resolution=0.1) assert str(grid) == f'' +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_prediction_grid_resolution(): grid = prediction_grid(variogram, resolution=1, as_numpy=False) @@ -89,6 +109,7 @@ def test_prediction_grid_resolution(): assert grid.cols == 474 +@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") def test_prediction_grid_cols_rows(): grid = prediction_grid(variogram, cols=50, rows=50, as_numpy=True) From 88339fc954cc2de9bf9b3db9c09555f2d8c4296b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 14:34:39 +0200 Subject: [PATCH 12/18] also exclude python 3.8 --- requirements.unittest.3.7.txt | 3 +-- skgstat/tests/test_gstatsim_interface.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/requirements.unittest.3.7.txt b/requirements.unittest.3.7.txt index 89886b8..270dd01 100644 --- a/requirements.unittest.3.7.txt +++ b/requirements.unittest.3.7.txt @@ -3,5 +3,4 @@ pytest-cov pytest-depends pykrige gstools>=1.3 -plotly -gstatsim \ No newline at end of file +plotly \ No newline at end of file diff --git a/skgstat/tests/test_gstatsim_interface.py b/skgstat/tests/test_gstatsim_interface.py index 7634fb6..f842f45 100644 --- a/skgstat/tests/test_gstatsim_interface.py +++ b/skgstat/tests/test_gstatsim_interface.py @@ -10,7 +10,7 @@ variogram = skg.Variogram(coords, vals, maxlag=0.6, n_lags=12) # Test cases -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_initialization_resolution(): # Initialize grid with resolution grid = Grid(variogram, resolution=0.1) @@ -20,7 +20,7 @@ def test_grid_initialization_resolution(): assert grid.cols == 4731 -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_initialization_infer_resolution(): # Initialize grid without rows/cols # Set resolution @@ -29,14 +29,14 @@ def test_grid_initialization_infer_resolution(): assert np.abs(grid.resolution - 6.3) < 0.01 -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_initialization_no_resolution_rows_cols(): # Initialize grid without resolution or rows/cols (expecting an error) with pytest.raises(AttributeError): grid = Grid(variogram, resolution=None, rows=None, cols=None) -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_resolution_setting(): # Set resolution grid = Grid(variogram, rows=100, cols=100) @@ -49,7 +49,7 @@ def test_grid_resolution_setting(): assert grid.cols == 96 -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_rows_setting(): grid = Grid(variogram, resolution=5) @@ -61,7 +61,7 @@ def test_grid_rows_setting(): assert np.abs(grid.resolution - 9.32) < 0.01 -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_cols_setting(): grid = Grid(variogram, resolution=5) @@ -73,7 +73,7 @@ def test_grid_cols_setting(): assert np.abs(grid.resolution - 4.73) < 0.01 -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_prediction_grid(): # Test prediction grid generation grid = Grid(variogram, resolution=0.1) @@ -83,7 +83,7 @@ def test_grid_prediction_grid(): assert prediction_grid.shape == (grid.shape[0] * grid.shape[1], 2) -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_call_operator(): # Test calling the Grid instance grid = Grid(variogram, resolution=0.1) @@ -93,14 +93,14 @@ def test_grid_call_operator(): assert prediction_grid.shape == (grid.shape[0] * grid.shape[1], 2) -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_grid_str_representation(): # Test string representation grid = Grid(variogram, resolution=0.1) assert str(grid) == f'' -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_prediction_grid_resolution(): grid = prediction_grid(variogram, resolution=1, as_numpy=False) @@ -109,7 +109,7 @@ def test_prediction_grid_resolution(): assert grid.cols == 474 -@pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher") +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") def test_prediction_grid_cols_rows(): grid = prediction_grid(variogram, cols=50, rows=50, as_numpy=True) From f70bdfdc9409cfc68b3a231da5b3cb144d358dda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Wed, 5 Jul 2023 14:40:39 +0200 Subject: [PATCH 13/18] make check function public --- skgstat/interfaces/gstatsim_mod.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skgstat/interfaces/gstatsim_mod.py b/skgstat/interfaces/gstatsim_mod.py index 2cc9c34..e896332 100644 --- a/skgstat/interfaces/gstatsim_mod.py +++ b/skgstat/interfaces/gstatsim_mod.py @@ -29,7 +29,7 @@ HAS_VERBOSE = False -def __check_gstatsim_available() -> bool: # pragma: no cover +def check_gstatsim_available() -> bool: # pragma: no cover """ Check if GStatSim is available. @@ -99,7 +99,7 @@ def __init__(self, bbox: Union[BBOX, 'Variogram'], resolution: Optional[int] = N """ # check if gstatsim is available - __check_gstatsim_available() + check_gstatsim_available() # check the resolution and rows/cols: if resolution is None and rows is None and cols is None: From 56bd6964c325793f5c357f8beeb543ded4a4808a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Tue, 11 Jul 2023 10:38:49 +0200 Subject: [PATCH 14/18] update to quiet parameter --- skgstat/interfaces/gstatsim_mod.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/skgstat/interfaces/gstatsim_mod.py b/skgstat/interfaces/gstatsim_mod.py index e896332..fd925b0 100644 --- a/skgstat/interfaces/gstatsim_mod.py +++ b/skgstat/interfaces/gstatsim_mod.py @@ -23,10 +23,10 @@ try: # pragma: no cover import gstatsim as gss GSTATSIM_AVAILABLE = True - HAS_VERBOSE = 'verbose' in inspect.signature(gss.Interpolation.okrige_sgs).parameters + HAS_QUIET = 'quiet' in inspect.signature(gss.Interpolation.okrige_sgs).parameters except ImportError: GSTATSIM_AVAILABLE = False - HAS_VERBOSE = False + HAS_QUIET = False def check_gstatsim_available() -> bool: # pragma: no cover @@ -250,6 +250,7 @@ def __call__(self, *args: Any, **kwds: Any) -> Any: def __str__(self) -> str: return f'' + @overload @@ -392,7 +393,7 @@ def run_simulation( num_points: int = 20, radius: Optional[Union[int, float]] = None, method: Union[Literal['simple'], Literal['ordinary']] = 'simple', - verbose: bool = False + quiet: bool = True ) -> np.ndarray: """ Run a sequential gaussian simulation using GStatSim. @@ -413,8 +414,8 @@ def run_simulation( method : Union[Literal['simple'], Literal['ordinary']], optional The interpolation method to use. Either 'simple' for simple kriging or 'ordinary' for ordinary kriging. Default is 'simple'. - verbose : bool, optional - If True, enable verbose output during the simulation. Default is False. + quiet : bool, optional + If True, disables progressbar output during the simulation. Default is True. Returns ------- @@ -446,8 +447,8 @@ def run_simulation( pred_grid = grid # run the simulation - if HAS_VERBOSE: - field: np.ndarray = sim_func(pred_grid, cond_data, 'x', 'y', 'v', num_points, vario_params, radius, verbose) + if HAS_QUIET: + field: np.ndarray = sim_func(pred_grid, cond_data, 'x', 'y', 'v', num_points, vario_params, radius, quiet) else: field: np.ndarray = sim_func(pred_grid, cond_data, 'x', 'y', 'v', num_points, vario_params, radius) @@ -463,7 +464,7 @@ def simulate( num_points: int = 20, radius: Optional[Union[int, float]] = None, method: Union[Literal['simple'], Literal['ordinary']] = 'simple', - verbose: bool = False, + quiet: bool = True, n_jobs: int = 1, size: int = 1, **kwargs, @@ -489,8 +490,8 @@ def simulate( range from the variogram parameters. method : Union[Literal['simple'], Literal['ordinary']], optional The interpolation method to use. Either 'simple' for simple kriging or 'ordinary' for ordinary kriging. Default is 'simple'. - verbose : bool, optional - If True, enable verbose output during the simulation. Default is False. + quiet : bool, optional + If True, disables progressbar output during the simulation. Default is True. n_jobs : int, optional The number of parallel jobs to run. Default is 1 (no parallelization). size : int, optional @@ -513,14 +514,14 @@ def simulate( # multiprocessing? if n_jobs > 1 and size > 1: # build th pool - pool = Parallel(n_jobs=n_jobs, verbose=0 if not verbose else 10) + pool = Parallel(n_jobs=n_jobs, verbose=0 if quiet else 10) # wrapper - gen = (delayed(run_simulation)(grid, cond_data, vario_params, num_points, radius, method, verbose) for _ in range(size)) + gen = (delayed(run_simulation)(grid, cond_data, vario_params, num_points, radius, method, quiet) for _ in range(size)) # run the simulation fields = pool(gen) return fields else: - field = run_simulation(grid, cond_data, vario_params, num_points, radius, method, verbose) + field = run_simulation(grid, cond_data, vario_params, num_points, radius, method, quiet) return [field] From b53e1bccbd8e6208d4252df18bb26ab566f7cc01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Tue, 11 Jul 2023 10:39:29 +0200 Subject: [PATCH 15/18] update interaface to new params --- skgstat/Variogram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skgstat/Variogram.py b/skgstat/Variogram.py index 677b321..2ed1c1c 100644 --- a/skgstat/Variogram.py +++ b/skgstat/Variogram.py @@ -2600,13 +2600,13 @@ def simulation( num_points: int = 20, radius: Optional[Union[int, float]] = None, method: Union[Literal['simple'], Literal['ordinary']] = 'simple', - verbose: bool = False, + quiet: bool = True, n_jobs: int = 1, size: int = 1, **kwargs, ) -> List[np.ndarray]: """""" - fields = gstatsim_mod.simulate(self, grid, num_points, radius, method, verbose, n_jobs, size, **kwargs) + fields = gstatsim_mod.simulate(self, grid, num_points, radius, method, quiet, n_jobs, size, **kwargs) return fields def to_gstools(self, **kwargs): From 929354a222837e95c7345aeaae5826dbdf2bd4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Sun, 16 Jul 2023 07:24:01 +0200 Subject: [PATCH 16/18] add docstring to prediction grid --- skgstat/Variogram.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/skgstat/Variogram.py b/skgstat/Variogram.py index 2ed1c1c..0b3dca8 100644 --- a/skgstat/Variogram.py +++ b/skgstat/Variogram.py @@ -2590,7 +2590,39 @@ def to_DataFrame(self, n=100, force=False): def gstatsim_prediction_grid(self, resolution: Optional[int] = None, rows: Optional[int] = None, cols: Optional[int] = None, as_numpy: bool = False) -> Union[gstatsim_mod.Grid, np.ndarray]: """ + Generate a structured gried of coordinates from this Variogram instance. + The grid has the shape (N, 2), where N is the number of grid points. + It can be created by specifiying the resolution or the number of rows and cols. + If rows and cols are used, the grid will have the same resolution in both directions, + which means, that the final grid will have a different number of rows, cols + than specified. + + Parameters + ---------- + resolution : int, optional + The resolution of the grid, by default None + rows : int, optional + The number of rows, by default None + cols : int, optional + The number of cols, by default None + as_numpy : bool, optional + If True, the grid will be returned as a numpy.ndarray, by default False + + Raises + ------ + ValueError + If the Variogram instance is not 2D + + Returns + ------- + Union[gstatsim_mod.Grid, np.ndarray] + The grid as a gstatsim_mod.Grid instance or a numpy.ndarray """ + # this does only work in 2D + if self.dim != 2: + raise ValueError('This function only works in 2D') + + # generate the grid grid = gstatsim_mod.prediction_grid(self, resolution, rows, cols, as_numpy=as_numpy) return grid From ac3c8b27ac214171591931b664b42fb6037285b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Sun, 16 Jul 2023 07:36:05 +0200 Subject: [PATCH 17/18] add interfaces to the docs --- docs/reference/interfaces.rst | 20 ++++++++++++++++++++ docs/reference/reference.rst | 1 + 2 files changed, 21 insertions(+) create mode 100644 docs/reference/interfaces.rst diff --git a/docs/reference/interfaces.rst b/docs/reference/interfaces.rst new file mode 100644 index 0000000..a0d9dc1 --- /dev/null +++ b/docs/reference/interfaces.rst @@ -0,0 +1,20 @@ +======================= +Scikit-GStat interfaces +======================= + +Scikit-GStat can interface with a number of other packages. +This sections describes the interface functions. Check the +tutorials folder for examples. + +GSTools +======= + +.. automodule:: skgstat.interfaces.gstools + :members: stable_scale, skgstat_to_gstools, skgstat_to_krige + + +GStatSim +======== + +.. automodule:: skgstat.interfaces.gstatsim_mod + :members: Grid, prediction_grid, simulation_params, run_simulation, simulate \ No newline at end of file diff --git a/docs/reference/reference.rst b/docs/reference/reference.rst index ec4c8d8..79af8c1 100644 --- a/docs/reference/reference.rst +++ b/docs/reference/reference.rst @@ -13,6 +13,7 @@ Code Reference estimator models kriging + interfaces data metric_space util \ No newline at end of file From 074e6f96d395bb36f55e5dedc97178a4d402e342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mirko=20M=C3=A4licke?= Date: Mon, 17 Jul 2023 11:25:58 +0200 Subject: [PATCH 18/18] test prediction grid interface function --- skgstat/tests/test_gstatsim_interface.py | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/skgstat/tests/test_gstatsim_interface.py b/skgstat/tests/test_gstatsim_interface.py index f842f45..ce17c29 100644 --- a/skgstat/tests/test_gstatsim_interface.py +++ b/skgstat/tests/test_gstatsim_interface.py @@ -117,6 +117,31 @@ def test_prediction_grid_cols_rows(): assert grid.shape == (2652 , 2) +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") +def test_prediction_grid_interface(): + coords, vals = skg.data.pancake(N=60, seed=42).get('sample') + vario = skg.Variogram(coords, vals, maxlag=0.6, n_lags=12) + + # create the grid + grid = vario.gstatsim_prediction_grid(resolution=5) + + assert isinstance(grid, Grid) + assert grid.rows == 94 + assert grid.cols == 96 + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="requires Python >= 3.8 or higher") +def test_prediction_grid_interface_as_numpy(): + coords, vals = skg.data.pancake(N=60).get('sample') + vario = skg.Variogram(coords, vals, maxlag=0.6, n_lags=12) + + # create the grid + grid = vario.gstatsim_prediction_grid(resolution=5, as_numpy=True) + + assert isinstance(grid, np.ndarray) + assert grid.shape == (96 * 94, 2) + + # Run the tests if __name__ == '__main__': pytest.main()