from gempy.core.grid_modules.create_topography import LoadDEMArtificial, LoadDEMGDAL
import numpy as np
import skimage.transform
import matplotlib.pyplot as plt
from scipy.constants import G
from scipy import interpolate
from gempy.utils.meta import _setdoc_pro
import gempy.utils.docstring as ds
import pandas as pn
[docs]class RegularGrid:
"""
Class with the methods and properties to manage 3D regular grids where the model will be interpolated.
Args:
extent (np.ndarray): [x_min, x_max, y_min, y_max, z_min, z_max]
resolution (np.ndarray): [nx, ny, nz]
Attributes:
extent (np.ndarray): [x_min, x_max, y_min, y_max, z_min, z_max]
resolution (np.ndarray): [nx, ny, nz]
values (np.ndarray): XYZ coordinates
mask_topo (np.ndarray, dtype=bool): same shape as values. Values above the topography are False
dx (float): size of the cells on x
dy (float): size of the cells on y
dz (float): size of the cells on z
"""
[docs] def __init__(self, extent=None, resolution=None, **kwargs):
self.resolution = np.ones((0, 3), dtype='int64')
self.extent = np.zeros(6, dtype='float64')
self.extent_r = np.zeros(6, dtype='float64')
self.values = np.zeros((0, 3))
self.values_r = np.zeros((0, 3))
self.mask_topo = np.zeros((0, 3), dtype=bool)
self.x = None
self.y = None
self.z = None
if extent is not None and resolution is not None:
self.set_regular_grid(extent, resolution)
self.dx, self.dy, self.dz = self.get_dx_dy_dz()
def set_coord(self, extent, resolution):
dx = (extent[1] - extent[0]) / resolution[0]
dy = (extent[3] - extent[2]) / resolution[1]
dz = (extent[5] - extent[4]) / resolution[2]
self.x = np.linspace(extent[0] + dx / 2, extent[1] - dx / 2, resolution[0],
dtype="float64")
self.y = np.linspace(extent[2] + dy / 2, extent[3] - dy / 2, resolution[1],
dtype="float64")
self.z = np.linspace(extent[4] + dz / 2, extent[5] - dz / 2, resolution[2],
dtype="float64")
return self.x, self.y, self.z
[docs] def create_regular_grid_3d(self, extent, resolution):
"""
Method to create a 3D regular grid where is interpolated
Args:
extent (list): [x_min, x_max, y_min, y_max, z_min, z_max]
resolution (list): [nx, ny, nz].
Returns:
numpy.ndarray: Unraveled 3D numpy array where every row correspond to the xyz coordinates of a regular grid
"""
coords = self.set_coord(extent, resolution)
g = np.meshgrid(*coords, indexing="ij")
values = np.vstack(tuple(map(np.ravel, g))).T.astype("float64")
return values
def get_dx_dy_dz(self, rescale=False):
if rescale is True:
dx = (self.extent_r[1] - self.extent_r[0]) / self.resolution[0]
dy = (self.extent_r[3] - self.extent_r[2]) / self.resolution[1]
dz = (self.extent_r[5] - self.extent_r[4]) / self.resolution[2]
else:
dx = (self.extent[1] - self.extent[0]) / self.resolution[0]
dy = (self.extent[3] - self.extent[2]) / self.resolution[1]
dz = (self.extent[5] - self.extent[4]) / self.resolution[2]
return dx, dy, dz
[docs] def set_regular_grid(self, extent, resolution):
"""
Set a regular grid into the values parameters for further computations
Args:
extent (list, np.ndarry): [x_min, x_max, y_min, y_max, z_min, z_max]
resolution (list, np.ndarray): [nx, ny, nz]
"""
self.extent = np.asarray(extent, dtype='float64')
self.resolution = np.asarray(resolution)
self.values = self.create_regular_grid_3d(extent, resolution)
self.length = self.values.shape[0]
self.dx, self.dy, self.dz = self.get_dx_dy_dz()
return self.values
[docs] def set_topography_mask(self, topography):
"""This method takes a topography grid of the same extent as the regular
grid and creates a mask of voxels
Args:
topography (:class:`gempy.core.grid_modules.topography.Topography`):
Returns:
"""
assert np.array_equal(topography.extent,
self.extent), 'The extent of' \
'the topography must match to the extent of the regular grid.'
# interpolate topography values to the regular grid
regular_grid_topo = skimage.transform.resize(
topography.values_2d,
(self.resolution[0], self.resolution[1]),
mode='constant',
anti_aliasing=False, preserve_range=True)
# Reshape the Z values of the regular grid to 3d
values_3d = self.values[:, 2].reshape(self.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
[docs]class Sections:
"""
Object that creates a grid of cross sections between two points.
Args:
regular_grid: Model.grid.regular_grid
section_dict: {'section name': ([p1_x, p1_y], [p2_x, p2_y], [xyres, zres])}
"""
[docs] def __init__(self, regular_grid=None, z_ext=None, section_dict=None):
if regular_grid is not None:
self.z_ext = regular_grid.extent[4:]
else:
self.z_ext = z_ext
self.section_dict = section_dict
self.names = []
self.points = []
self.resolution = []
self.length = [0]
self.dist = []
self.df = pn.DataFrame()
self.df['dist'] = self.dist
self.values = []
self.extent = None
if section_dict is not None:
self.set_sections(section_dict)
def _repr_html_(self):
return self.df.to_html()
def __repr__(self):
return self.df.to_string()
def show(self):
pass
def set_sections(self, section_dict, regular_grid=None, z_ext=None):
self.section_dict = section_dict
if regular_grid is not None:
self.z_ext = regular_grid.extent[4:]
self.names = np.array(list(self.section_dict.keys()))
self.get_section_params()
self.calculate_all_distances()
self.df = pn.DataFrame.from_dict(self.section_dict, orient='index',
columns=['start', 'stop', 'resolution'])
self.df['dist'] = self.dist
self.compute_section_coordinates()
def get_section_params(self):
self.points = []
self.resolution = []
self.length = [0]
for i, section in enumerate(self.names):
points = [self.section_dict[section][0], self.section_dict[section][1]]
assert points[0] != points[
1], 'The start and end points of the section must not be identical.'
self.points.append(points)
self.resolution.append(self.section_dict[section][2])
self.length = np.append(self.length, self.section_dict[section][2][0] *
self.section_dict[section][2][1])
self.length = np.array(self.length).cumsum()
def calculate_all_distances(self):
self.coordinates = np.array(self.points).ravel().reshape(-1,
4) # axis are x1,y1,x2,y2
self.dist = np.sqrt(np.diff(self.coordinates[:, [0, 2]]) ** 2 + np.diff(
self.coordinates[:, [1, 3]]) ** 2)
@staticmethod
def distance_2_points(p1, p2):
return np.sqrt(np.diff((p1[0], p2[0])) ** 2 + np.diff((p1[1], p2[1])) ** 2)
def compute_section_coordinates(self):
for i in range(len(self.names)):
xy = self.calculate_line_coordinates_2points(self.coordinates[i, :2],
self.coordinates[i, 2:],
self.resolution[i][0])
zaxis = np.linspace(self.z_ext[0], self.z_ext[1], self.resolution[i][1],
dtype="float64")
X, Z = np.meshgrid(xy[:, 0], zaxis, indexing='ij')
Y, _ = np.meshgrid(xy[:, 1], zaxis, indexing='ij')
xyz = np.vstack((X.flatten(), Y.flatten(), Z.flatten())).T
if i == 0:
self.values = xyz
else:
self.values = np.vstack((self.values, xyz))
def generate_axis_coord(self):
for i, name in enumerate(self.names):
xy = self.calculate_line_coordinates_2points(
self.coordinates[i, :2],
self.coordinates[i, 2:],
self.resolution[i][0]
)
yield name, xy
def calculate_line_coordinates_2points(self, p1, p2, res):
if isinstance(p1, list):
p1 = np.array(p1)
if isinstance(p2, list):
p2 = np.array(p2)
v = p2 - p1 # vector pointing from p1 to p2
u = v / np.linalg.norm(v) # normalize it
distance = self.distance_2_points(p1, p2)
steps = np.linspace(0, distance, res)
values = p1.reshape(2, 1) + u.reshape(2, 1) * steps.ravel()
return values.T
def get_section_args(self, section_name: str):
where = np.where(self.names == section_name)[0][0]
return self.length[where], self.length[where + 1]
def get_section_grid(self, section_name: str):
l0, l1 = self.get_section_args(section_name)
return self.values[l0:l1]
[docs] @staticmethod
def interpolate_zvals_at_xy(xy, topography, method='interp2d'):
"""
Interpolates DEM values on a defined section
Args:
xy: x (EW) and y (NS) coordinates of the profile
topography (:class:`gempy.core.grid_modules.topography.Topography`)
method: interpolation method, 'interp2d' for cubic scipy.interpolate.interp2d
'spline' for scipy.interpolate.RectBivariateSpline
Returns:
numpy.ndarray: z values, i.e. topography along the profile
"""
xj = topography.values_2d[:, 0, 0]
yj = topography.values_2d[0, :, 1]
zj = topography.values_2d[:, :, 2]
if method == 'interp2d':
f = interpolate.interp2d(xj, yj, zj.T, kind='cubic')
zi = f(xy[:, 0], xy[:, 1])
if xy[:, 0][0] <= xy[:, 0][-1] and xy[:, 1][0] <= xy[:, 1][-1]:
return np.diag(zi)
else:
return np.flipud(zi).diagonal()
else:
assert xy[:, 0][0] <= xy[:, 0][
-1], 'The xy values of the first point must be smaller than second.' \
'Please use interp2d as method argument. Will be fixed.'
assert xy[:, 1][0] <= xy[:, 1][
-1], 'The xy values of the first point must be smaller than second.' \
'Please use interp2d as method argument. Will be fixed.'
f = interpolate.RectBivariateSpline(xj, yj, zj)
zi = f(xy[:, 0], xy[:, 1])
return np.flipud(zi).diagonal()
[docs]class CustomGrid:
"""Object that contains arbitrary XYZ coordinates.
Args:
custom_grid (numpy.ndarray like): XYZ (in columns) of the desired coordinates
Attributes:
values (np.ndarray): XYZ coordinates
"""
[docs] def __init__(self, custom_grid: np.ndarray):
self.values = np.zeros((0, 3))
self.set_custom_grid(custom_grid)
[docs] def set_custom_grid(self, custom_grid: np.ndarray):
"""
Give the coordinates of an external generated grid
Args:
custom_grid (numpy.ndarray like): XYZ (in columns) of the desired coordinates
Returns:
numpy.ndarray: Unraveled 3D numpy array where every row correspond to the xyz coordinates of a regular
grid
"""
custom_grid = np.atleast_2d(custom_grid)
assert type(custom_grid) is np.ndarray and custom_grid.shape[1] == 3, \
'The shape of new grid must be (n,3) where n is the number of' \
' points of the grid'
self.values = custom_grid
self.length = self.values.shape[0]
return self.values
[docs]class CenteredGrid:
"""
Logarithmic spaced grid.
"""
[docs] def __init__(self, centers=None, radius=None, resolution=None):
self.grid_type = 'centered_grid'
self.values = np.empty((0, 3))
self.length = self.values.shape[0]
self.resolution = resolution
self.kernel_centers = np.empty((0, 3))
self.kernel_dxyz_left = np.empty((0, 3))
self.kernel_dxyz_right = np.empty((0, 3))
self.tz = np.empty(0)
if centers is not None and radius is not None:
if resolution is None:
resolution = [10, 10, 20]
self.set_centered_grid(centers=centers, radius=radius,
resolution=resolution)
[docs] @staticmethod
@_setdoc_pro(ds.resolution)
def create_irregular_grid_kernel(resolution, radius):
"""
Create an isometric grid kernel (centered at 0)
Args:
resolution: [s0]
radius (float): Maximum distance of the kernel
Returns:
tuple: center of the voxel, left edge of each voxel (for xyz), right edge of each voxel (for xyz).
"""
if radius is not list or radius is not np.ndarray:
radius = np.repeat(radius, 3)
g_ = []
g_2 = []
d_ = []
for xyz in [0, 1, 2]:
if xyz == 2:
# Make the grid only negative for the z axis
g_.append(np.geomspace(0.01, 1, int(resolution[xyz])))
g_2.append(
(np.concatenate(([0], g_[xyz])) + 0.05) * - radius[xyz] * 1.2)
else:
g_.append(np.geomspace(0.01, 1, int(resolution[xyz] / 2)))
g_2.append(
np.concatenate((-g_[xyz][::-1], [0], g_[xyz])) * radius[xyz])
d_.append(np.diff(np.pad(g_2[xyz], 1, 'reflect', reflect_type='odd')))
g = np.meshgrid(*g_2)
d_left = np.meshgrid(d_[0][:-1] / 2, d_[1][:-1] / 2, d_[2][:-1] / 2)
d_right = np.meshgrid(d_[0][1:] / 2, d_[1][1:] / 2, d_[2][1:] / 2)
kernel_g = np.vstack(tuple(map(np.ravel, g))).T.astype("float64")
kernel_d_left = np.vstack(tuple(map(np.ravel, d_left))).T.astype("float64")
kernel_d_right = np.vstack(tuple(map(np.ravel, d_right))).T.astype("float64")
return kernel_g, kernel_d_left, kernel_d_right
[docs] @_setdoc_pro(ds.resolution)
def set_centered_kernel(self, resolution, radius):
"""
Set a centered
Args:
resolution: [s0]
radius (float): Maximum distance of the kernel
Returns:
"""
self.kernel_centers, self.kernel_dxyz_left, self.kernel_dxyz_right = self.create_irregular_grid_kernel(
resolution, radius)
return self.kernel_centers
[docs] @_setdoc_pro(ds.resolution)
def set_centered_grid(self, centers, kernel_centers=None, **kwargs):
"""
Main method of the class, set the XYZ values around centers using a kernel.
Args:
centers (np.array): XYZ array with the centers of where we want to create a grid around
kernel_centers (Optional[np.array]): center of the voxels of a desired kernel.
**kwargs:
* resolution: [s0]
* radius (float): Maximum distance of the kernel
Returns:
"""
self.values = np.empty((0, 3))
centers = np.atleast_2d(centers)
if kernel_centers is None:
kernel_centers = self.set_centered_kernel(**kwargs)
assert centers.shape[
1] == 3, 'Centers must be a numpy array that contains the coordinates XYZ'
for i in centers:
self.values = np.vstack((self.values, i + kernel_centers))
self.length = self.values.shape[0]
def set_tz_kernel(self, **kwargs):
if self.kernel_centers.size == 0:
self.set_centered_kernel(**kwargs)
grid_values = self.kernel_centers
s_gr_x = grid_values[:, 0]
s_gr_y = grid_values[:, 1]
s_gr_z = grid_values[:, 2]
# getting the coordinates of the corners of the voxel...
x_cor = np.stack((s_gr_x - self.kernel_dxyz_left[:, 0],
s_gr_x + self.kernel_dxyz_right[:, 0]), axis=1)
y_cor = np.stack((s_gr_y - self.kernel_dxyz_left[:, 1],
s_gr_y + self.kernel_dxyz_right[:, 1]), axis=1)
z_cor = np.stack((s_gr_z - self.kernel_dxyz_left[:, 2],
s_gr_z + self.kernel_dxyz_right[:, 2]), axis=1)
# ...and prepare them for a vectorial op
x_matrix = np.repeat(x_cor, 4, axis=1)
y_matrix = np.tile(np.repeat(y_cor, 2, axis=1), (1, 2))
z_matrix = np.tile(z_cor, (1, 4))
s_r = np.sqrt(x_matrix ** 2 + y_matrix ** 2 + z_matrix ** 2)
# This is the vector that determines the sign of the corner of the voxel
mu = np.array([1, -1, -1, 1, -1, 1, 1, -1])
self.tz = (
np.sum(- 1 *
G *
mu * (
x_matrix * np.log(y_matrix + s_r) +
y_matrix * np.log(x_matrix + s_r) -
z_matrix * np.arctan(
x_matrix * y_matrix / (z_matrix * s_r))),
axis=1))
return self.tz
# class Topography:
# """
# Object to include topography in the model.
# """
# def __init__(self, regular_grid):
# self.regular_grid = regular_grid
# self.values = np.zeros((0, 3))
#
# self.topo = None
# self.values_3D = np.zeros((0, 0, 0))
# self.extent = None
# self.resolution = None
#
# self.type = None
#
# def load_from_gdal(self, filepath):
# self.topo = Load_DEM_GDAL(filepath, self.regular_grid)
# self._create_init()
# self._fit2model()
# self.type = 'real'
#
# def load_random_hills(self, **kwargs):
# self.topo = LoadDEMArtificial(self.regular_grid, **kwargs)
# self._create_init()
# self._fit2model()
# self.type = 'artificial'
#
# def load_from_saved(self, filepath):
# assert filepath[-4:] == '.npy', 'The file must end on .npy'
# topo = np.load(filepath, allow_pickle=True)
# self.values_3D = topo[0]
# self.extent = topo[1]
# self.resolution = topo[2]
# self._fit2model()
# self.type = 'real'
#
# def _create_init(self):
# self.values_3D = self.topo.values_3D
# self.extent = self.topo.extent
# self.resolution = self.topo.resolution
#
# # These two methods makes more sense in regular grid passing a topography
# # object.
# def _fit2model(self):
# self.values = np.vstack((
# self.values_3D[:, :, 0].ravel(), self.values_3D[:, :, 1].ravel(),
# self.values_3D[:, :, 2].ravel())).T.astype("float64")
#
# if np.any(self.regular_grid.extent[:4] - self.extent) != 0:
# print('obacht')
# # todo if grid extent bigger fill missing values with nans for chloe
# self._crop()
#
# if np.any(self.regular_grid.resolution[:2] - self.resolution) != 0:
# self._resize()
# else:
# self.values_3D_res = self.values_3D
#
# self.regular_grid.mask_topo = self._create_grid_mask()
#
# def _crop(self):
# pass
#
# def _resize(self):
# self.values_3D_res = skimage.transform.resize(self.values_3D,
# (self.regular_grid.resolution[0], self.regular_grid.resolution[1]),
# mode='constant',
# anti_aliasing=False, preserve_range=True)
#
# def show(self):
# from gempy.plot.helpers import add_colorbar
# if self.type == 'artificial':
# fig, ax = plt.subplots()
# CS= ax.contour(self.values_3D[:, :, 2], extent=(self.extent[:4]), colors='k', linestyles='solid')
# ax.clabel(CS, inline=1, fontsize=10, fmt='%d')
# CS2 = ax.contourf(self.values_3D[:, :, 2], extent=(self.extent[:4]), cmap='terrain')
# add_colorbar(axes=ax, label='elevation [m]', cs=CS2)
# else:
# im = plt.imshow(np.flipud(self.values_3D[:,:,2]), extent=(self.extent[:4]))
# add_colorbar(im=im, label='elevation [m]')
# plt.axis('scaled')
# plt.xlabel('X')
# plt.ylabel('Y')
# plt.title('Model topography')
#
# def save(self, filepath):
# """
# Save the topography file in a numpy array which can be loaded later, to avoid the gdal process.
# Args:
# filepath (str): path where the array should be stored.
#
# Returns:
#
# """
# np.save(filepath, np.array([self.values_3D, self.extent, self.resolution]))
# print('saved')
#
# def _create_grid_mask(self):
# ind = self._find_indices()
# gridz = self.regular_grid.values[:, 2].reshape(*self.regular_grid.resolution).copy()
# for x in range(self.regular_grid.resolution[0]):
# for y in range(self.regular_grid.resolution[1]):
# z = ind[x, y]
# gridz[x, y, z:] = 99999
# mask = (gridz == 99999)
# return mask# np.multiply(np.full(self.regular_grid.values.shape, True).T, mask.ravel()).T
#
# def _find_indices(self):
# zs = np.linspace(self.regular_grid.extent[4], self.regular_grid.extent[5], self.regular_grid.resolution[2])
# dz = (zs[-1] - zs[0]) / len(zs)
# return ((self.values_3D_res[:, :, 2] - zs[0]) / dz + 1).astype(int)
#
# def interpolate_zvals_at_xy(self, xy, method='interp2d'):
# """
# Interpolates DEM values on a defined section
#
# Args:
# :param xy: x (EW) and y (NS) coordinates of the profile
# :param method: interpolation method, 'interp2d' for cubic scipy.interpolate.interp2d
# 'spline' for scipy.interpolate.RectBivariateSpline
# Returns:
# :return: z values, i.e. topography along the profile
# """
# xj = self.values_3D[:, :, 0][0, :]
# yj = self.values_3D[:, :, 1][:, 0]
# zj = self.values_3D[:, :, 2]
#
# if method == 'interp2d':
# f = interpolate.interp2d(xj, yj, zj, kind='cubic')
# zi = f(xy[:, 0], xy[:, 1])
# if xy[:, 0][0] <= xy[:, 0][-1] and xy[:, 1][0] <= xy[:, 1][-1]:
# return np.diag(zi)
# else:
# return np.flipud(zi).diagonal()
# else:
# assert xy[:, 0][0] <= xy[:, 0][-1], 'The xy values of the first point must be smaller than second.' \
# 'Please use interp2d as method argument. Will be fixed.'
# assert xy[:, 1][0] <= xy[:, 1][-1], 'The xy values of the first point must be smaller than second.' \
# 'Please use interp2d as method argument. Will be fixed.'
# f = interpolate.RectBivariateSpline(xj, yj, zj)
# zi = f(xy[:, 0], xy[:, 1])
# return np.flipud(zi).diagonal()