Source code for gempy.core.data.grid_modules.topography

import dataclasses

import warnings
from pydantic import Field
from typing import Optional, Tuple, Annotated

import numpy as np

from .regular_grid import RegularGrid

from ....optional_dependencies import require_skimage, require_scipy
from dataclasses import field, dataclass
from ..encoders.converters import short_array_type


[docs] @dataclass class Topography: """ Object to include topography in the model. Notes: This always assumes that the topography we pass fits perfectly the extent. """ _regular_grid: RegularGrid values_2d: Annotated[np.ndarray, Field(exclude=True)] = field(default_factory=lambda: np.zeros((0, 0, 3))) source: Optional[str] = None # Fields managed internally values: short_array_type = field(init=False, default_factory=lambda: np.zeros((0, 3))) resolution: Tuple[int, int] = Field(init=True, default=(0, 0)) raster_shape: Tuple[int, ...] = field(init=False, default_factory=tuple) _mask_topo: Optional[np.ndarray] = field(init=False, default=None, repr=False) _x: Optional[np.ndarray] = field(init=False, default=None, repr=False) _y: Optional[np.ndarray] = field(init=False, default=None, repr=False) def __post_init__(self): # if a non-empty array was provided, initialize the flattened values if self.values_2d.size: self.set_values(self.values_2d)
[docs] @classmethod def from_subsurface_structured_data(cls, structured_data: 'subsurface.StructuredData', regular_grid: RegularGrid): """Creates a topography object from a subsurface structured data object Args: structured_data (subsurface.StructuredData): Structured data object Returns: :class:`gempy.core.grid_modules.topography.Topography` """ # Generate meshgrid for x and y coordinates ds = structured_data.data x_coordinates = ds['x'] y_coordinates = ds['y'] height_values = ds['topography'] return cls.from_arrays(regular_grid, x_coordinates, y_coordinates, height_values)
[docs] @classmethod def from_unstructured_mesh(cls, regular_grid, xyz_vertices): """Creates a topography object from an unstructured mesh of XYZ vertices. Args: regular_grid (RegularGrid): The regular grid object. xyz_vertices (numpy.ndarray): Array of XYZ vertices of the unstructured mesh. Returns: :class:`gempy.core.grid_modules.topography.Topography` """ # Perform Delaunay triangulation on the vertices # Generate the regular grid points from scipy.interpolate import griddata x_regular, y_regular = np.meshgrid( np.linspace(regular_grid.extent[0], regular_grid.extent[1], regular_grid.resolution[0]), np.linspace(regular_grid.extent[2], regular_grid.extent[3], regular_grid.resolution[1]), indexing='ij' ) # Interpolate the z-values onto the regular grid z_regular = griddata( points=xyz_vertices[:, :2], values=xyz_vertices[:, 2], xi=(x_regular, y_regular), method='nearest', fill_value=np.nan # You can choose a different fill value or method ) # Reshape the grid for compatibility with existing structure values_2d = np.stack((x_regular, y_regular, z_regular), axis=-1) return cls(_regular_grid=regular_grid, values_2d=values_2d)
@classmethod def from_arrays(cls, regular_grid, x_coordinates, y_coordinates, height_values,): x_vals, y_vals = np.meshgrid(x_coordinates, y_coordinates, indexing='ij') # Reshape arrays for stacking x_vals = x_vals[:, :, np.newaxis] # shape (73, 34, 1) y_vals = y_vals[:, :, np.newaxis] # shape (73, 34, 1) topography_vals = height_values.values[:, :, np.newaxis] # shape (73, 34, 1) # Stack along the last dimension result = np.concatenate([x_vals, y_vals, topography_vals], axis=2) # shape (73, 34, 3) return cls(_regular_grid=regular_grid, values_2d=result) @property def extent(self): return self._regular_grid.extent @property def regular_grid_resolution(self): return self._regular_grid.resolution @property def x(self): if self._x is not None: return self._x else: val = self.values[:, 0].copy() val.sort() return np.unique(val) @property def y(self): if self._y is not None: return self._y else: val = self.values[:, 1].copy() val.sort() return np.unique(val)
[docs] def set_values(self, values_2d: np.ndarray): """General method to set topography Args: values_2d (numpy.ndarray[float,float, 3]): array with the XYZ values in 2D Returns: :class:`gempy.core.grid_modules.topography.Topography` """ # Original topography data self.values_2d = values_2d self.resolution = values_2d.shape[:2] # n,3 array self.values = values_2d.reshape((-1, 3), order='C') return self
[docs] def set_values2d(self, values: np.ndarray) -> "Topography": """ Reconstruct the 2D topography (shape = resolution + [3]) from a flat Nx3 array (or from self.values if none is provided). """ # default to the already-flattened XYZ array # compute expected size nx, ny = self.resolution expected = nx * ny * 3 if values.size != expected: raise ValueError( f"Cannot reshape array of size {values.size} into shape {(nx, ny, 3)}." ) # reshape in C-order to (nx, ny, 3) self.set_values( values_2d=values.reshape(nx, ny, 3, order="C") ) # invalidate any cached mask self._mask_topo = None return self
def set_values2d_(self, values: np.ndarray): resolution = (60, 60) self.values_2d = values.reshape(*resolution, 3) @property def topography_mask(self): """This method takes a topography grid of the same extent as the regular grid and creates a mask of voxels """ # * Check if the topography is the same as the cached one and if so, return the cached mask if self._mask_topo is not None: return self._mask_topo # interpolate topography values to the regular grid skimage = require_skimage() regular_grid_topo = skimage.transform.resize( image=self.values_2d, output_shape=(self.regular_grid_resolution[0], self.regular_grid_resolution[1]), mode='constant', anti_aliasing=False, preserve_range=True ) # Adjust the topography to be lower by half a voxel height # Assumes your voxel heights are uniform and can be calculated as the total height divided by resolution voxel_height = self._regular_grid.dz * 2 regular_grid_topo = regular_grid_topo - voxel_height # Reshape the Z values of the regular grid to 3d values_3d = self._regular_grid.values[:, 2].reshape(self.regular_grid_resolution) if regular_grid_topo.ndim == 3: regular_grid_topo_z = regular_grid_topo[:, :, [2]] elif regular_grid_topo.ndim == 2: regular_grid_topo_z = regular_grid_topo else: raise ValueError() mask = np.greater(values_3d[:, :, :], regular_grid_topo_z) self._mask_topo = mask return self._mask_topo def resize_topo(self): skimage = require_skimage() regular_grid_topo = skimage.transform.resize( self.values_2d, (self.regular_grid_resolution[0], self.regular_grid_resolution[1]), mode='constant', anti_aliasing=False, preserve_range=True) return regular_grid_topo def load_random_hills(self, **kwargs): warnings.warn('This function is deprecated. Use load_from_random instead', DeprecationWarning) if 'extent' in kwargs: self.extent = kwargs.pop('extent') if 'resolution' in kwargs: self.regular_grid_resolution = kwargs.pop('resolution') dem = _LoadDEMArtificial(extent=self.extent, resolution=self.regular_grid_resolution, **kwargs) self._x, self._y = dem.x, dem.y self.set_values(dem.get_values()) def save(self, path): np.save(path, self.values_2d) def load(self, path): self.set_values(np.load(path)) self._x, self._y = None, None return self.values def load_from_saved(self, *args, **kwargs): self.load(*args, **kwargs)
class _LoadDEMArtificial: # * Cannot think of a good reason to be a class def __init__(self, grid=None, fd=2.0, extent=None, resolution=None, d_z=None): """Class to create a random topography based on a fractal grid algorithm. Args: fd: fractal dimension, defaults to 2.0 d_z: maximum height difference. If none, last 20% of the model in z direction extent: extent in xy direction. If none, geo_model.grid.extent resolution: desired resolution of the topography array. If none, geo_model.grid.resolution """ self.values_2d = np.array([]) self.resolution = grid.resolution[:2] if resolution is None else resolution assert all(np.asarray(self.resolution) >= 2), 'The regular grid needs to be at least of size 2 on all directions.' self.extent = grid.extent if extent is None else extent if d_z is None: self.d_z = np.array([self.extent[5] - (self.extent[5] - self.extent[4]) * 1 / 5, self.extent[5]]) print(self.d_z) else: self.d_z = d_z topo = self.fractalGrid(fd, n=self.resolution.max()) topo = np.interp(topo, (topo.min(), topo.max()), self.d_z) self.dem_zval = topo[:self.resolution[0], :self.resolution[1]] # crop fractal grid with resolution self.create_topo_array() @staticmethod def fractalGrid(fd, n=256): """ Modified after https://github.com/samthiele/pycompass/blob/master/examples/3_Synthetic%20Examples.ipynb Generate isotropic fractal surface image using spectral synthesis method [1, p.] References: 1. Yuval Fisher, Michael McGuire, The Science of Fractal Images, 1988 (cf. http://shortrecipes.blogspot.com.au/2008/11/python-isotropic-fractal-surface.html) **Arguments**: -fd = the fractal dimension -N = the size of the fractal surface/image """ h = 1 - (fd - 2) # X = np.zeros((N, N), complex) a = np.zeros((n, n), complex) powerr = -(h + 1.0) / 2.0 for i in range(int(n / 2) + 1): for j in range(int(n / 2) + 1): phase = 2 * np.pi * np.random.rand() if i != 0 or j != 0: rad = (i * i + j * j) ** powerr * np.random.normal() else: rad = 0.0 a[i, j] = complex(rad * np.cos(phase), rad * np.sin(phase)) if i == 0: i0 = 0 else: i0 = n - i if j == 0: j0 = 0 else: j0 = n - j a[i0, j0] = complex(rad * np.cos(phase), -rad * np.sin(phase)) a.imag[int(n / 2)][0] = 0.0 a.imag[0, int(n / 2)] = 0.0 a.imag[int(n / 2)][int(n / 2)] = 0.0 for i in range(1, int(n / 2)): for j in range(1, int(n / 2)): phase = 2 * np.pi * np.random.rand() rad = (i * i + j * j) ** powerr * np.random.normal() a[i, n - j] = complex(rad * np.cos(phase), rad * np.sin(phase)) a[n - i, j] = complex(rad * np.cos(phase), -rad * np.sin(phase)) scipy = require_scipy() itemp = scipy.fftpack.ifft2(a) itemp = itemp - itemp.min() return itemp.real / itemp.real.max() def create_topo_array(self): """for masking the lith block""" x = np.linspace(self.extent[0], self.extent[1], self.resolution[0]) y = np.linspace(self.extent[2], self.extent[3], self.resolution[1]) self.x = x self.y = y xx, yy = np.meshgrid(x, y, indexing='ij') self.values_2d = np.dstack([xx, yy, self.dem_zval]) def get_values(self): return self.values_2d