Source code for cratermaker.components.target

from __future__ import annotations

from typing import Any

import numpy as np
from astropy.constants import G

from cratermaker.constants import FloatLike
from cratermaker.core.base import ComponentBase, import_components
from cratermaker.utils.general_utils import (
    _create_catalogue,
    _set_properties,
    format_large_units,
    parameter,
)


[docs] class Target(ComponentBase): """ Represents the target body in a crater simulation. This class encapsulates the properties of the target that is impacted, including its material composition, size, and other relevant physical characteristics. """ _catalogue_header = [ "name", "radius", "mass", "material", "transition_scale_type", ] # The catalogue was created with Swiftest _catalogue_values = [ ("Mercury", 2439.40e3, 3.301001e23, "Soft Rock", "silicate"), ("Venus", 6051.84e3, 4.867306e24, "Hard Rock", "silicate"), ("Earth", 6371.01e3, 5.972168e24, "Wet Soil", "silicate"), ("Moon", 1737.53e3, 7.345789e22, "Soft Rock", "silicate"), ("Mars", 3389.92e3, 6.416909e23, "Soft Rock", "silicate"), ("Phobos", 11.17e3, 1.080000e16, "Soft Rock", "silicate"), ("Deimos", 6.30e3, 1.800000e15, "Soft Rock", "silicate"), ("Ceres", 469.70e3, 9.383516e20, "Soft Rock", "ice"), ("Vesta", 262.70e3, 2.590270e20, "Soft Rock", "silicate"), ("Io", 1821.49e3, 8.929649e22, "Hard Rock", "silicate"), ("Europa", 1560.80e3, 4.798574e22, "Ice", "ice"), ("Ganymede", 2631.20e3, 1.481479e23, "Ice", "ice"), ("Callisto", 2410.30e3, 1.075661e23, "Ice", "ice"), ("Titan", 2575.50e3, 1.345181e23, "Ice", "ice"), ("Rhea", 764.50e3, 2.306459e21, "Ice", "ice"), ("Dione", 562.50e3, 1.095486e21, "Ice", "ice"), ("Tethys", 536.30e3, 6.174430e20, "Ice", "ice"), ("Enceladus", 252.30e3, 1.080318e20, "Ice", "ice"), ("Mimas", 198.80e3, 3.750939e19, "Ice", "ice"), ("Ariel", 578.90e3, 1.250019e21, "Ice", "ice"), ("Umbriel", 584.70e3, 1.279535e21, "Ice", "ice"), ("Titania", 788.90e3, 3.338178e21, "Ice", "ice"), ("Oberon", 761.40e3, 3.076577e21, "Ice", "ice"), ("Miranda", 235.70e3, 6.442623e19, "Ice", "ice"), ("Triton", 1352.60e3, 2.140292e22, "Ice", "ice"), ("Charon", 606.00e3, 1.589680e21, "Ice", "ice"), ("Pluto", 1188.30e3, 1.302498e22, "Ice", "ice"), ("Arrokoth", 9.13e3, 7.485000e14, "Ice", "ice"), ] _catalogue = _create_catalogue(_catalogue_header, _catalogue_values) _density_catalogue = { "Water": 1000.0, "Sand": 1750.0, "Dry Soil": 1500.0, "Wet Soil": 2000.0, "Soft Rock": 2250.0, "Hard Rock": 2500.0, "Ice": 900.0, } def __init__( self, name: str, radius: FloatLike | None = None, diameter: FloatLike | None = None, mass: FloatLike | None = None, transition_scale_type: str | None = None, material: str | None = None, density: FloatLike | None = None, **kwargs: Any, ): """ **Warning:** This object should not be instantiated directly. Instead, use the ``.maker()`` method. Parameters ---------- name : str or None Name of the target body. radius : FloatLike or None Radius of the target body in km. diameter : FloatLike or None Diameter of the target body in km. mass : FloatLike or None Mass of the target body in kg. transition_scale_type : str or None Simple-to-complex transition scaling to use for the surface (either "silicate" or "ice"). material : str or None Name of the material composition of the target body. density : FloatLike or None Volumetric density of the surface of the target body in kg/m³. **kwargs : Any |kwargs| Notes ----- - The `radius` and `diameter` parameters are mutually exclusive. Only one of them should be provided. - Parameters set explicitly using keyword arguments will override those drawn from the catalogue. """ object.__setattr__(self, "_updating", False) # guard against recursive updates super().__init__(**kwargs) object.__setattr__(self, "_name", None) object.__setattr__(self, "_radius", None) object.__setattr__(self, "_mass", None) object.__setattr__(self, "_transition_scale_type", None) object.__setattr__(self, "_material", None) object.__setattr__(self, "_density", None) # ensure that only either diamter of radius is passed size_values_set = sum(x is not None for x in [diameter, radius]) if size_values_set > 1: raise ValueError("Only one of diameter or radius may be set") if diameter is not None: radius = diameter / 2.0 catalogue = kwargs.pop("catalogue", self.__class__._catalogue) # Set properties for the Target object based on the arguments passed to the function _set_properties( self, name=name, radius=radius, mass=mass, material=material, catalogue=catalogue, density=density, transition_scale_type=transition_scale_type, **kwargs, ) arg_check = sum(x is None for x in [self.name, self.diameter, self.mass, self.transition_scale_type]) if arg_check > 0: raise ValueError("Invalid Target") if self._density is None and self.material in self._density_catalogue: self._density = self._density_catalogue[self.material] def __str__(self) -> str: diameter = format_large_units(self.diameter, quantity="length") escape_velocity = format_large_units(self.escape_velocity, quantity="velocity") str_repr = ( f"<Target: {self.name}>\n" f"Material: {self.material}\n" f"Diameter: {diameter}\n" f"Mass: {self.mass:.2e} kg\n" f"Surface density: {self.density:.1f} kg/m³\n" f"Transition Type: {self.transition_scale_type}\n" f"Escape Velocity: {escape_velocity}\n" f"Gravity: {self.gravity:.3f} m/s²\n" ) return str_repr
[docs] @classmethod def maker( cls: type[Target], target: Target | str = "Moon", radius: FloatLike | None = None, diameter: FloatLike | None = None, mass: FloatLike | None = None, transition_scale_type: str | None = None, material: str | None = None, density: FloatLike | None = None, **kwargs: Any, ) -> Target: """ Initialize the target object, setting properties from the provided arguments. Parameters ---------- target : str, Target, or None Name of the target body or a Target object. radius : FloatLike or None Radius of the target body in km. diameter : FloatLike or None Diameter of the target body in km. mass : FloatLike or None Mass of the target body in kg. transition_scale_type : str or None Simple-to-complex transition scaling to use for the surface (either "silicate" or "ice"). material : str or None Name of the material composition of the target body. density : FloatLike or None Volumetric density of the surface of the target body in kg/m³. **kwargs : Any |kwargs| Notes ----- - The `radius` and `diameter` parameters are mutually exclusive. Only one of them should be provided. - Parameters set explicitly using keyword arguments will override those drawn from the catalogue. """ if target is None: try: target = cls( name="Moon", radius=radius, diameter=diameter, mass=mass, transition_scale_type=transition_scale_type, material=material, density=density, **kwargs, ) except Exception as e: raise RuntimeError("Error initializing target.") from e elif isinstance(target, str): try: target = cls( name=target, radius=radius, diameter=diameter, mass=mass, transition_scale_type=transition_scale_type, material=material, density=density, **kwargs, ) except KeyError as e: raise ValueError(f"Target '{target}' not found in the catalogue. Please provide a valid target name.") from e elif not isinstance(target, Target): raise TypeError("target must be a string or a Target object") target._component_name = target.name return target
@parameter def radius(self) -> float | None: """The radius of the target body in meters.""" return self._radius @radius.setter def radius(self, value: FloatLike): if value is not None and value <= 0: raise ValueError("Radius must be positive") self._radius = float(value) @property def diameter(self) -> float | None: """The diameter of the target body in meters.""" if self._radius is not None: return 2 * self._radius @property def surface_area(self) -> float | None: """The surface area of the target body in m².""" if self.radius is not None: return 4 * np.pi * self.radius**2 @property def volume(self) -> float | None: """The volume of the target body in m³.""" if self.radius is not None: return (4 / 3) * np.pi * self.radius**3 @property def circumference(self) -> float | None: """The circumference of the target body in meters.""" if self.radius is not None: return 2 * np.pi * self.radius @property def mass(self) -> float | None: """The mass of the target body in kilograms.""" return self._mass @mass.setter def mass(self, value: FloatLike): if value is not None and value <= 0: raise ValueError("Mass must be positive") self._mass = float(value) @property def name(self): """The name of the target body.""" return self._name @name.setter def name(self, value): if not isinstance(value, str) and value is not None: raise TypeError("name must be a string or None") self._name = value @parameter def material(self): """The name of the material composition of the target body's surface, which is used for crater scaling.""" return self._material @material.setter def material(self, value): if not isinstance(value, str) and value is not None: raise TypeError("material must be a string or None") self._material = value @parameter def density(self): """The volumetric density of the surface of the target body in kg/m³.""" return self._density @density.setter def density(self, value): if value is not None: if not isinstance(value, FloatLike): raise TypeError("density must be a numeric value or None") if value <= 0: raise ValueError("density must be a positive number") self._density = float(value) else: self._density = None return @property def catalogue(self) -> str: """Catalogue of predefined target bodies.""" from cratermaker.utils.general_utils import format_large_units if self.__class__._catalogue is None: return "This Projectile component does not have a catalogue." lines = [] # Use the self.__class__._catalogue_header list to make the header header = "|".join([f"{h:<11}" for h in self.__class__._catalogue_header]) lines.append(header) lines.append(len(header) * "-") for name, entry in self.__class__._catalogue.items(): line = "" for k, v in entry.items(): if k == "radius": val = format_large_units(v, quantity="length") elif k == "mass": val = f"{v:.2e} kg" else: val = v line += f"|{val:<11}" lines.append(f"{name:<11}{line}") return "\n".join(lines) @property def catalogue_key(self): """The key used to identify the property used as the key in a catalogue.""" return "name" @property def transition_scale_type(self): """The type of simple-to-complex transition scaling used for the surface, either 'silicate' or 'ice'.""" return self._transition_scale_type @transition_scale_type.setter def transition_scale_type(self, value): valid_types = ["silicate", "ice"] if value not in valid_types and value is not None: raise ValueError(f"Invalid transition_scale_type: {value}. Must be one of {valid_types} or None") self._transition_scale_type = value @property def escape_velocity(self) -> float: """The escape velocity for the target body in m/s.""" return np.sqrt(2 * self.radius * self.gravity) @property def gravity(self) -> float | None: """The gravitational acceleration at the surface of the target body in m/s².""" return G.value * self.mass / (self.radius**2)
import_components(__name__, __path__)