Source code for cratermaker.components.surface.arbitrary_resolution

from pathlib import Path
from typing import Any

import numpy as np
from numpy.typing import NDArray

from cratermaker.components.surface import Surface
from cratermaker.components.target import Target
from cratermaker.constants import FloatLike, PairOfFloats
from cratermaker.utils.general_utils import format_large_units, parameter


[docs] @Surface.register("arbitrary_resolution") class ArbitraryResolutionSurface(Surface): """ Create a uniform grid configuration with an arbitrary user-defined pixel size. This will not be as nice as the regular IcosphereSurface, but can be any resolution desired. Parameters ---------- pix : float The approximate face size for the mesh in meters. target : Target, optional The target body or name of a known target body for the impact simulation. reset : bool, optional Flag to indicate whether to reset the surface. Default is True. regrid : bool, optional Flag to indicate whether to regrid the surface. Default is False. simdir : str | Path |simdir| Returns ------- ArbitraryResolutionSurface An instance of the ArbitraryResolutionSurface class initialized with the given pixel size. """ def __init__( self, pix: FloatLike | None = None, target: Target | str | None = None, reset: bool = True, regrid: bool = False, simdir: str | Path | None = None, **kwargs: Any, ): super().__init__(target=target, simdir=simdir, **kwargs) self.pix = pix self._load_from_files(reset=reset, regrid=regrid, **kwargs) def __str__(self) -> str: str_repr = super().__str__() pix_mean = format_large_units(self.pix_mean, quantity="length") pix_std = format_large_units(self.pix_std, quantity="length") str_repr += f"Effective pixel size: {pix_mean} +/- {pix_std}" return str_repr @property def _hashvars(self): """ The variables used to generate the hash. """ return super()._hashvars + [self.pix] @parameter def pix(self): """ The approximate face size for a cell of the mesh. """ return self._pix @pix.setter def pix(self, value: FloatLike): if value is None: value = ( np.sqrt(4 * np.pi * self.radius**2) * 1e-3 ) # Default mesh scale that is somewhat comparable to a 1000x1000 CTEM grid elif not isinstance(value, FloatLike) or np.isnan(value) or np.isinf(value) or value <= 0: raise TypeError("pix must be a positive float") self._pix = float(value) @staticmethod def _distribute_points( distance: FloatLike, radius: FloatLike = 1.0, lon_range: PairOfFloats = (-180, 180), lat_range: PairOfFloats = (-90, 90), ) -> NDArray: """ Distributes points on a sphere using Deserno's algorithm (Deserno 2004). Parameters ---------- distance : float Approximate distance between points, used to determine the number of points, where n = 1/distance**2 when distributed over the whole sphere. radius : float, optional Radius of the sphere. Default is 1.0 lon_range : tuple, optional Range of longitudes in degrees. Default is (-180,180). lat_range : tuple, optional Range of latitudes in degrees. Default is (-90,90). Returns ------- (3,n) ndarray of np.float64 Array of cartesian points on the sphere. References ---------- - Deserno, Markus., 2004. How to generate equidistributed points on the surface of a sphere. https://www.cmu.edu/biolphys/deserno/pdf/sphere_equi.pdf """ def _sph2cart(theta, phi, r): """ Converts spherical coordinates to Cartesian coordinates. Parameters ---------- theta : float Inclination angle in radians. phi : float Azimuthal angle in radians. r : float Radius. """ x = r * np.sin(theta) * np.cos(phi) y = r * np.sin(theta) * np.sin(phi) z = r * np.cos(theta) return x, y, z phi_range = np.deg2rad(lon_range) + np.pi theta_range = np.deg2rad(lat_range) + np.pi / 2 points = [] n = int(4 * np.pi / distance**2) if n < 1: return a = 4 * np.pi / n d = np.sqrt(a) mtheta = int(np.round(np.pi / d)) dtheta = np.pi / mtheta dphi = a / dtheta thetavals = np.pi * (np.arange(mtheta) + 0.5) / mtheta thetavals = thetavals[(thetavals >= theta_range[0]) & (thetavals < theta_range[1])] for theta in thetavals: mphi = max(1, int(np.round(2 * np.pi * np.sin(theta) / dphi))) phivals = 2 * np.pi * np.arange(mphi) / mphi phivals = phivals[(phivals >= phi_range[0]) & (phivals < phi_range[1])] for phi in phivals: points.append(_sph2cart(theta, phi, radius)) if len(points) == 0: return points = np.array(points, dtype=np.float64) decimals = int(np.ceil(np.log10((2 * np.pi * radius) / distance))) points = np.unique(points.T.round(decimals=decimals), axis=0).T points = points.T return points def _generate_face_distribution(self, **kwargs: Any) -> NDArray: """ Creates the points that define the mesh centers. Returns ------- (3,n) ndarray of np.float64 Array of points on a unit sphere. """ pix_str = format_large_units(self.pix, quantity="length") print(f"Generating a mesh with uniformly distributed faces of size ~{pix_str}.") points = self._distribute_points(distance=self.pix / self.radius) points[:, 0] = np.array([0, 0, 1]) points[:, -1] = np.array([0, 0, -1]) return points