Source code for gempy_engine.core.data.transforms

import pprint
import warnings
from enum import Enum, auto
from typing import Optional, Union

import numpy as np
from dataclasses import dataclass


class TransformOpsOrder(Enum):
    TRS = auto()  # * The order of the transformations is: scale, rotation, translation
    SRT = auto()  # * The order of the transformations is: translation, rotation, scale


[docs] class GlobalAnisotropy(Enum): CUBE = auto() # * Transform data to be as close as possible to a cube NONE = auto() # * Do not transform data MANUAL = auto() # * Use the user defined transform
[docs] @dataclass class Transform: position: np.ndarray rotation: np.ndarray scale: np.ndarray _is_default_transform: bool = False _cached_pivot: Optional[np.ndarray] = None def __repr__(self): return pprint.pformat(self.__dict__) def __post_init__(self): assert self.position.shape == (3,) assert self.rotation.shape == (3,) assert self.scale.shape == (3,) def __add__(self, other): if not isinstance(other, Transform): raise ValueError("Both objects must be an instance of Transform") new_position = self.position + other.position new_rotation = self.rotation + other.rotation new_scale = self.scale * other.scale return Transform(new_position, new_rotation, new_scale) @classmethod def init_neutral(cls): t = cls(position=np.zeros(3), rotation=np.zeros(3), scale=np.ones(3)) t._cached_pivot = np.zeros(3) return t @classmethod def from_matrix(cls, matrix: np.ndarray): assert matrix.shape == (4, 4) position = matrix[:3, 3] rotation_radians = np.array([ np.arctan2(matrix[2, 1], matrix[2, 2]), np.arctan2(-matrix[2, 0], np.sqrt(matrix[2, 1] ** 2 + matrix[2, 2] ** 2)), np.arctan2(matrix[1, 0], matrix[0, 0]) ]) rotation_degrees = np.degrees(rotation_radians) scale = np.array([ np.linalg.norm(matrix[0, :3]), np.linalg.norm(matrix[1, :3]), np.linalg.norm(matrix[2, :3]) ]) return cls(position, rotation_degrees, scale) @property def cached_pivot(self): return self._cached_pivot @cached_pivot.setter def cached_pivot(self, pivot: np.ndarray): self._cached_pivot = pivot @classmethod def from_input_points(cls, surface_points: 'gempy.data.SurfacePointsTable', orientations: 'gempy.data.OrientationsTable') -> 'Transform': input_points_xyz = np.concatenate([surface_points.xyz, orientations.xyz], axis=0) if input_points_xyz.shape[0] == 0: transform = cls(position=np.zeros(3), rotation=np.zeros(3), scale=np.ones(3)) transform._is_default_transform = True return transform max_coord = np.max(input_points_xyz, axis=0) min_coord = np.min(input_points_xyz, axis=0) # Compute the range for each dimension range_coord = 2 * (max_coord - min_coord) # Avoid division by zero; replace zero with a small number range_coord = np.where(range_coord == 0, 1e-10, range_coord) # The scaling factor for each dimension is the inverse of its range scaling_factors = 1 / range_coord # ! Be careful with toy models center: np.ndarray = (max_coord + min_coord) / 2 return cls( position=-center, rotation=np.zeros(3), scale=cls._adjust_scale_to_limit_ratio( s=scaling_factors, anisotropic_limit=np.array([1, 1, 1]) # ! Increase this number to auto anisotropy ) ) def apply_anisotropy(self, anisotropy_type: GlobalAnisotropy, anisotropy_limit: Optional[np.ndarray] = None): if anisotropy_type == GlobalAnisotropy.CUBE: warnings.warn( message="Interpolation is being done with the default transform. " "If you do not know what you are doing you should probably call GeoModel.update_transform() first.", category=RuntimeWarning ) elif anisotropy_type == GlobalAnisotropy.NONE: self.scale = self._adjust_scale_to_limit_ratio( s=self.scale, anisotropic_limit=np.array([1, 1, 1]) # ! Increase this number to auto anisotropy ) elif anisotropy_type == GlobalAnisotropy.MANUAL and anisotropy_limit is not None: self.scale = self._adjust_scale_to_limit_ratio( s=self.scale, anisotropic_limit=anisotropy_limit ) else: raise NotImplementedError @staticmethod def _adjust_scale_to_limit_ratio(s, anisotropic_limit=np.array([10, 10, 10])): # Calculate the ratios ratios = [ s[0] / s[1], s[0] / s[2], s[1] / s[0], s[1] / s[2], s[2] / s[0], s[2] / s[1] ] # Adjust the scales based on the index of the max ratio if ratios[0] > anisotropic_limit[0]: s[0] = s[1] * anisotropic_limit[0] if ratios[1] > anisotropic_limit[0]: s[0] = s[2] * anisotropic_limit[0] if ratios[2] > anisotropic_limit[1]: s[1] = s[0] * anisotropic_limit[1] if ratios[3] > anisotropic_limit[1]: s[1] = s[2] * anisotropic_limit[1] if ratios[4] > anisotropic_limit[2]: s[2] = s[0] * anisotropic_limit[2] if ratios[5] > anisotropic_limit[2]: s[2] = s[1] * anisotropic_limit[2] return s @staticmethod def _max_scale_ratio(s): ratios = [ s[0] / s[1], s[0] / s[2], s[1] / s[0], s[1] / s[2], s[2] / s[0], s[2] / s[1] ] return max(ratios) @property def isometric_scale(self): # TODO: double check how was done in old gempy return 1 / np.mean(self.scale) def get_transform_matrix(self, transform_type: TransformOpsOrder = TransformOpsOrder.SRT) -> np.ndarray: T = np.eye(4) R = np.eye(4) S = np.eye(4) T[:3, 3] = self.position rx, ry, rz = np.radians(self.rotation) Rx = np.array( [[1, 0, 0], [0, np.cos(rx), -np.sin(rx)], [0, np.sin(rx), np.cos(rx)]] ) Ry = np.array( [[np.cos(ry), 0, np.sin(ry)], [0, 1, 0], [-np.sin(ry), 0, np.cos(ry)]] ) Rz = np.array( [[np.cos(rz), -np.sin(rz), 0], [np.sin(rz), np.cos(rz), 0], [0, 0, 1]] ) R[:3, :3] = Rx @ Ry @ Rz S[:3, :3] = np.diag(self.scale) match transform_type: case TransformOpsOrder.TRS: return T @ R @ S case TransformOpsOrder.SRT: return S @ R @ T case _: raise NotImplementedError(f"Transform type {transform_type} not implemented") def apply(self, points: np.ndarray, transform_op_order: TransformOpsOrder = TransformOpsOrder.SRT): # * NOTE: to compare with legacy we would have to add 0.5 to the coords assert points.shape[1] == 3 if self._is_default_transform: warnings.warn( message="Interpolation is being done with the default transform. " "If you do not know what you are doing you should probably call GeoModel.update_transform() first.", category=RuntimeWarning ) homogeneous_points = np.concatenate([points, np.ones((points.shape[0], 1))], axis=1) matrix = self.get_transform_matrix(transform_op_order) transformed_points = (matrix @ homogeneous_points.T).T return transformed_points[:, :3] def scale_points(self, points: np.ndarray): return points * self.scale def apply_inverse(self, points: np.ndarray, transform_op_order: TransformOpsOrder = TransformOpsOrder.SRT): # * NOTE: to compare with legacy we would have to add 0.5 to the coords assert points.shape[1] == 3 homogeneous_points = np.concatenate([points, np.ones((points.shape[0], 1))], axis=1) matrix = self.get_transform_matrix(transform_op_order) inv = np.linalg.inv(matrix) transformed_points = (inv @ homogeneous_points.T).T return transformed_points[:, :3] def apply_with_cached_pivot(self, points: np.ndarray, transform_op_order: TransformOpsOrder = TransformOpsOrder.SRT): if self._cached_pivot is None: raise ValueError("A pivot must be set before calling this method") return self.apply_with_pivot(points, self._cached_pivot, transform_op_order) def apply_inverse_with_cached_pivot(self, points: np.ndarray, transform_op_order: TransformOpsOrder = TransformOpsOrder.SRT): if self._cached_pivot is None: raise ValueError("A pivot must be set before calling this method") return self.apply_inverse_with_pivot(points, self._cached_pivot, transform_op_order)
[docs] def apply_with_pivot(self, points: np.ndarray, pivot: np.ndarray, transform_op_order: TransformOpsOrder = TransformOpsOrder.SRT): """These are used for ellipsoids for finite faults""" assert points.shape[1] == 3 if self._is_default_transform: warnings.warn( message="Interpolation is being done with the default transform. " "If you do not know what you are doing you should probably call GeoModel.update_transform() first.", category=RuntimeWarning ) # Translation matrices to and from the pivot T_to_origin = self._translation_matrix(-pivot[0], -pivot[1], -pivot[2]) T_back = self._translation_matrix(*pivot) # Construct the transformation matrix with the pivot M = T_back @ self.get_transform_matrix(transform_op_order) @ T_to_origin homogeneous_points = np.concatenate([points, np.ones((points.shape[0], 1))], axis=1) transformed_points = (M @ homogeneous_points.T).T return transformed_points[:, :3]
def apply_inverse_with_pivot(self, points: np.ndarray, pivot: np.ndarray, transform_op_order: TransformOpsOrder = TransformOpsOrder.SRT): assert points.shape[1] == 3 # Translation matrices to and from the pivot T_to_origin = self._translation_matrix(-pivot[0], -pivot[1], -pivot[2]) T_back = self._translation_matrix(*pivot) # Construct the inverse transformation matrix with the pivot M_inv = np.linalg.inv(T_back @ self.get_transform_matrix(transform_op_order) @ T_to_origin) homogeneous_points = np.concatenate([points, np.ones((points.shape[0], 1))], axis=1) transformed_points = (M_inv @ homogeneous_points.T).T return transformed_points[:, :3] @staticmethod def _translation_matrix(tx, ty, tz): return np.array([ [1, 0, 0, tx], [0, 1, 0, ty], [0, 0, 1, tz], [0, 0, 0, 1] ]) def transform_gradient(self, gradients: np.ndarray, transform_op_order: TransformOpsOrder = TransformOpsOrder.SRT, preserve_magnitude: bool = True) -> np.ndarray: assert gradients.shape[1] == 3 # Extract the 3x3 upper-left section of the transformation matrix transformation_3x3 = self.get_transform_matrix(transform_op_order)[:3, :3] # Compute the inverse transpose of this 3x3 matrix inv_trans_3x3 = np.linalg.inv(transformation_3x3).T # Multiply the gradients by this inverse transpose matrix transformed_gradients = (inv_trans_3x3 @ gradients.T).T if preserve_magnitude: # Compute the magnitude of the original gradients gradient_magnitudes = np.linalg.norm(gradients, axis=1) # Compute the magnitude of the transformed gradients transformed_gradient_magnitudes = np.linalg.norm(transformed_gradients, axis=1) # Compute the ratio between the two magnitudes magnitude_ratio = transformed_gradient_magnitudes / gradient_magnitudes # Multiply the transformed gradients by this ratio transformed_gradients /= magnitude_ratio[:, None] return transformed_gradients