Source code for astronomix.option_classes.simulation_config

"""
Static simulation configuration.

Defines :class:`SimulationConfig` — the bundle of options that, unlike the
simulation parameters, necessitate recompilation when changed — together with
the integer-coded enumerations they reference (backends, solver/boundary/Riemann
modes, positivity modes, ...), the small geometry vector helpers, the sub-configs
for gravity and positivity, and the ``finalize_config`` pass that fills in
derived fields and validates the configuration.
"""

# general
import math
import subprocess

# typing
from types import NoneType
from typing import NamedTuple, Tuple, Union
from jaxtyping import Array, Float

# jax
import jax

# astronomix containers
from astronomix._modules._cnn_mhd_corrector._cnn_mhd_corrector_options import (
    CNNMHDconfig,
)
from astronomix._modules._cooling.cooling_options import CoolingConfig
from astronomix._modules._cosmic_rays.cosmic_ray_options import CosmicRayConfig
from astronomix._modules._neural_net_force._neural_net_force_options import (
    NeuralNetForceConfig,
)
from astronomix._modules._stellar_wind.stellar_wind_options import WindConfig
from astronomix._modules._turbulent_forcing._turbulent_forcing_options import TurbulentForcingConfig

# ===================== constant definition =====================

# backends (very limited support currently)
NATIVE_JAX = 0
PALLAS = 1
#: OPTIMAL_BACKEND is not a backend of its own: it is a request to pick the
#: fastest available one at ``finalize_config`` time.  It resolves to PALLAS on
#: GPUs new enough to run the Triton kernels (compute capability >= 8.0) and
#: falls back to NATIVE_JAX everywhere else (older GPUs, CPU, no ``nvidia-smi``).
OPTIMAL_BACKEND = 2

# positivity-enforcement modes (used by ``PositivityConfig.per_stage_mode`` /
# ``per_step_mode``).  HARD_FLOOR clamps density (and, for ideal
# gas, pressure) pointwise — cheap, non-conservative, matches the *adiabatic*
# HOW-MHD ``prot.f``.  REDISTRIBUTE neighbour-averages density+momentum (and
# energy) over the valid 3x3x3 neighbourhood of sub-threshold cells — much
# gentler at strong shocks than a hard floor (no sharp floored cell), matches
# the *isothermal* HOW-MHD ``prot.f`` (not strictly mass-conserving: like
# ``prot.f`` it copies neighbour values without debiting the donors).
POSITIVITY_NONE = 0
POSITIVITY_HARD_FLOOR = 1
POSITIVITY_REDISTRIBUTE = 2
#: CONSERVATIVE: enforce internal-energy positivity by an antisymmetric
#: face-flux diffusion that pulls internal energy into (near-)negative-pressure
#: cells from their hotter neighbours (exact total-energy conservation), plus a
#: density floor / vacuum-rest for voids and a minimal residual pressure floor
#: as the unconditional guarantee. The smooth, conservative cousin of HARD_FLOOR:
#: it keeps the energy-conserving self-gravity scheme stable on violent collapse
#: without the 100%+ energy injection a bare floor causes.
POSITIVITY_CONSERVATIVE = 3

# solver modes
FINITE_VOLUME = 0
FINITE_DIFFERENCE = 1

# differentiation modes
FORWARDS = 0
BACKWARDS = 1

# limiter types
MINMOD = 0
OSHER = 1
DOUBLE_MINMOD = 2
SUPERBEE = 3
VAN_ALBADA = 4
VAN_ALBADA_PP = 5

# splitting modes
UNSPLIT = 0
SPLIT = 1

# Riemann solvers
HLL = 0
HLLC = 1
HLLC_LM = 2
LAX_FRIEDRICHS = 3
HYBRID_HLLC = 4
AM_HLLC = 5

# time integrators
# currently only for finite volume
RK2_SSP = 0
MUSCL = 1
# currently only for finite difference
RK4_SSP = 2
RK4_LSRK = 3

# boundary conditions
OPEN_BOUNDARY = 0
REFLECTIVE_BOUNDARY = 1
PERIODIC_BOUNDARY = 2
FIXED_BOUNDARY = 3
MHD_JET_BOUNDARY = 4
FIXED_BOUNDARY_OPEN_MOMENTUM = 5

PRIMITIVE_GAS_STATE = 0
CONSERVATIVE_GAS_STATE = 1
VELOCITY_ONLY = 2
MAGNETIC_FIELD_ONLY = 3

# geometry types
CARTESIAN = 0
CYLINDRICAL = 1
SPHERICAL = 2

# axes
VARAXIS = 0
XAXIS = 1
YAXIS = 2
ZAXIS = 3

# boundary handling modes
GHOST_CELLS = 0
PERIODIC_ROLL = 1
# OPEN_SHIFT = 2

# self-gravity coupling schemes (FD):
#   SIMPLE_SOURCE              - rho * v * a energy source (non-conservative)
#   SECOND_ORDER_CONSERVATIVE  - flux-based energy source (2nd-order accurate)
#   FOURTH_ORDER_CONSERVATIVE  - corrected flux-based energy source (4th-order,
#                                the energy-conserving high-order scheme)
SIMPLE_SOURCE = 0
SECOND_ORDER_CONSERVATIVE = 1
FOURTH_ORDER_CONSERVATIVE = 2

# Magnetic part integrators for split MHD
IMPLICIT_MIDPOINT = 0
IMPLICIT_EULER = 1

# Numerical precision
SINGLE_PRECISION = 0
DOUBLE_PRECISION = 1

# Viscosity types
KINEMATIC_VISCOSITY = 0
DYNAMIC_VISCOSITY = 1

# Equation of state
IDEAL_GAS = 0
ISOTHERMAL = 1

# Snapshot storage modes
ON_DEVICE = 0
TO_DISK = 1

# ============================================================

# ===================== type definitions =====================

[docs] class StaticIntVector(NamedTuple): """A static (compile-time) per-axis integer triple (e.g. cells per axis).""" x: int = -1 y: int = -1 z: int = -1
[docs] class StaticFloatVector(NamedTuple): """A static (compile-time) per-axis float triple (e.g. box size per axis).""" x: float = -1.0 y: float = -1.0 z: float = -1.0
[docs] def __truediv__(self, other: StaticIntVector) -> "StaticFloatVector": """Divide component-wise by a :class:`StaticIntVector` (e.g. box / cells).""" if not isinstance(other, StaticIntVector): return NotImplemented return StaticFloatVector( x=self.x / other.x, y=self.y / other.y, z=self.z / other.z, )
STATE_TYPE = Union[ Float[Array, "num_vars num_cells_x"], Float[Array, "num_vars num_cells_x num_cells_y"], Float[Array, "num_vars num_cells_x num_cells_y num_cells_z"], ] STATE_TYPE_ALTERED = Union[ Float[Array, "num_vars num_cells_a"], Float[Array, "num_vars num_cells_a num_cells_b"], Float[Array, "num_vars num_cells_a num_cells_b num_cells_c"], ] FIELD_TYPE = Union[ Float[Array, "num_cells_x"], Float[Array, "num_cells_x num_cells_y"], Float[Array, "num_cells_x num_cells_y num_cells_z"], ]
[docs] class SnapshotSettings(NamedTuple): """Settings for the snapshot output of the simulation.""" #: Whether to record the full primitive state at every checkpoint. #: This is the single biggest snapshot allocation #: (``num_snapshots × num_vars × num_cells^d``); it is **opt-in**. #: Set to ``True`` if you actually need the per-snapshot states; for #: the common case of only wanting a final state plus integrated #: diagnostics (energies, total mass, runtime, num_iterations), the #: default ``False`` skips the per-snapshot state allocation entirely. return_states: bool = False #: Whether to return the final state of the simulation. return_final_state: bool = False #: Whether to return the total mass at the times the snapshots were taken. return_total_mass: bool = False #: Whether to return the total energy at the times the snapshots were taken. return_total_energy: bool = False #: Whether to return internal energy return_internal_energy: bool = False #: Whether to return kinetic energy return_kinetic_energy: bool = False #: Whether to return gravitational energy return_gravitational_energy: bool = False #: Whether to return radial momentum return_radial_momentum: bool = False #: Whether to return the kinetic energy spectrum return_kinetic_energy_spectrum: bool = False #: Whether to return the magnetic energy spectrum return_magnetic_energy_spectrum: bool = False #: Whether to return the helicity spectrum return_helicity_spectrum: bool = False #: Whether to return the magnetic field divergence #: NOTE: currently only implemented for finite difference MHD return_magnetic_divergence: bool = False #: Whether to return the temperature PDF (dV/dlogT) return_temperature_pdf: bool = False num_temperature_bins: int = 100 temperature_pdf_min: float = 1e-10 temperature_pdf_max: float = 1e10
[docs] class BoundarySettings1D(NamedTuple): """The boundary-condition type at the left and right end of a single axis.""" left_boundary: int = OPEN_BOUNDARY right_boundary: int = OPEN_BOUNDARY
[docs] class BoundarySettings(NamedTuple): """Per-axis boundary settings for the simulation.""" x: BoundarySettings1D = BoundarySettings1D() y: BoundarySettings1D = BoundarySettings1D() z: BoundarySettings1D = BoundarySettings1D()
[docs] class GravityConfig(NamedTuple): """Self-gravity and external-potential configuration.""" #: Self-gravity switch (currently only for periodic / manual-open boundaries). self_gravity: bool = False #: Coupling of the self-gravity source to the hydrodynamics. One of #: ``SIMPLE_SOURCE`` / ``SECOND_ORDER_CONSERVATIVE`` / #: ``FOURTH_ORDER_CONSERVATIVE``. self_gravity_version: int = FOURTH_ORDER_CONSERVATIVE #: Enable an external, static gravitational potential provided via #: ``params.gravitational_potential``. It is added to the self-gravity #: potential (if any) in ``_compute_total_potential``. external_potential: bool = False #: Manual open boundary conditions in the Poisson solver. poisson_manual_open_boundaries: bool = False #: Master gravity switch. Set automatically in ``finalize_config`` to #: ``self_gravity or external_potential``; gates the gravity source-term #: machinery so an external potential works without self-gravity. Not set #: by the user directly. gravity: bool = False
[docs] class PositivityConfig(NamedTuple): """ Density/pressure positivity-enforcement configuration. """ #: Casual on/off switch for the per-stage / per-step STATE floors. Default #: False (no flooring). When True, finalize_config sets per_stage_mode and #: per_step_mode to HARD_FLOOR unless explicitly overridden. Does NOT affect #: the read-only ``clamp_in_estimates`` (always respected). default_positivity_protection: bool = False #: Positivity enforcement applied inside every SSPRK/LSRK stage (on the #: conserved state — the CFL lever for strong shocks). One of #: ``POSITIVITY_{NONE,HARD_FLOOR,REDISTRIBUTE,CONSERVATIVE}``. Default NONE; #: set to HARD_FLOOR by finalize when ``default_positivity_protection``. per_stage_mode: int = POSITIVITY_NONE #: Positivity enforcement applied once per step before the evolve (on the #: primitive state). With turbulent forcing + ``vacuum_protection`` the #: conservative ``prot`` redistribution already runs once per step, so a #: per-step REDISTRIBUTE here is redundant and is auto-skipped. per_step_mode: int = POSITIVITY_NONE #: Read-only density/pressure clamp in the flux / eigenvalue / timestep #: estimates (NaN-safety; does NOT modify the evolved state). This is the #: role the old ``enforce_positivity`` bool played in those estimators. #: DECOUPLED from ``default_positivity_protection`` and ON by default -- #: cheap insurance that never touches the conserved solution. clamp_in_estimates: bool = True #: Vacuum-rest velocity recovery: zero the momentum in below-floor (vacuum) #: cells so the recovered velocity is 0 rather than ``momentum/rho_floored`` #: (which spikes and drives high-Mach blow-up); lets ``minimum_density`` be #: lowered by orders of magnitude without instability. vacuum_rest: bool = False #: NaN/inf backstop: reset non-finite conserved entries to zero before the #: density/pressure floors so they become a valid floored state. nan_safe: bool = False #: POSITIVITY_CONSERVATIVE-mode parameters (conservative internal-energy #: redistribution): per-axis diffusion coefficient (stability needs #: < 1/(2*dim)), number of Jacobi passes, and the activation margin in units #: of the internal-energy floor (keep ~1 -- genuine near-violations only). cons_coeff: float = 0.15 cons_passes: int = 16 cons_activate: float = 1.0 #: Deep-void first-order flux blending (FOFC-style): blend the WENO interface #: flux toward LLF in cells near the density floor; the weight ramps from 1 #: at the floor to 0 at ``deepvoid_blend_factor * minimum_density``. deepvoid_blend: bool = False deepvoid_blend_factor: float = 8.0 #: Positivity-preserving (Hu-Adams-Shu / Zalesak FCT) flux limiter: blend the #: WENO flux toward LLF by the largest weight keeping the LF-updated density #: AND pressure above their floors. Shares the unified flux-blending #: infrastructure with ``deepvoid_blend`` (different activation path; both may #: be on, the stronger blend wins). Forces the non-fused WENO+divergence path. preserving_flux: bool = False
[docs] class SimulationConfig(NamedTuple): """ Configuration object for the simulation. The simulation configuration are parameters defining the simulation where changes necessitate recompilation. """ # Static simulation parameters #: Backend. Defaults to OPTIMAL_BACKEND, which ``finalize_config`` resolves #: to PALLAS on compute-capability >= 8.0 GPUs and NATIVE_JAX otherwise. backend: int = OPTIMAL_BACKEND pallas_block_shape: Tuple[int, int, int] = (4, 4, 8) pallas_use_triton: bool = True pallas_interpret: bool = False pallas_num_warps: int = 4 #: Toggle for the Pallas constrained-transport helpers #: (``update_cell_center_fields``, ``constrained_transport_rhs``). #: Disabled by default: the staged Pallas-CT pipeline gives a clear #: memory win at small grids (~65% temp at N=16 on alfven_wave3D) #: but only marginal savings at production scale (~2% temp at N=64) #: while adding ~25s of one-time compile cost. Flip to True if the #: small-N memory profile matters; the rest of the Pallas backend #: stays on regardless. pallas_ct: bool = False #: Basic solver mode, either finite volume or finite difference. #: Defaults to the finite-difference HOW-MHD scheme (Jeongbhin Seo, #: Dongsu Ryu, 2023), which is the recommended solver. solver_mode: int = FINITE_DIFFERENCE #: Precision mode. numerical_precision: int = SINGLE_PRECISION #: Debug runtime errors, throws exceptions #: on e.g. negative pressure or density. #: Significantly reduces performance. runtime_debugging: bool = False #: Donate the state arrays to the time integration function #: to reduce memory allocations. If activated, the #: initial state arrays will be invalid after #: the simulation. donate_state: bool = False #: Memory analysis of the main time integration #: function memory_analysis: bool = False #: Build the simulation helper data on the host (CPU) and #: only move the fields that are actually needed by the #: enabled subsystems onto the accelerator. Useful in #: production runs where a large meshgrid like #: ``geometric_centers`` is not required on device and the #: per-field memory footprint matters. host_helper_data: bool = False #: Print the elapsed time of the simulation print_elapsed_time: bool = False #: Activate progress bar progress_bar: bool = False #: The number of dimensions of the simulation. dimensionality: int = 1 #: Use a struct for the state. state_struct: bool = False #: The geometry of the simulation. geometry: int = CARTESIAN #: The random seed for any stochastic processes #: in the simulation, e.g. turbulent forcing. random_seed: int = 42 #: The equation of state for the simulation. #: NOTE: CURRENTLY ONLY IMPLEMENTED FOR #: FINITE DIFFERENCE MODE. equation_of_state: int = IDEAL_GAS #: Magnetohydrodynamics switch. mhd: bool = False #: Integrator used for the magnetic part in the FV MHD scheme. fv_magnetic_integrator: int = IMPLICIT_MIDPOINT #: Density/pressure positivity-enforcement configuration (see PositivityConfig). positivity_config: PositivityConfig = PositivityConfig() #: Self-gravity / external-potential configuration (see GravityConfig). gravity_config: GravityConfig = GravityConfig() #: Explicit diffusion term #: (currently only for finite difference mode) diffusion: bool = False #: Viscosity type - either kinematic or dynamic viscosity. viscosity_type: int = DYNAMIC_VISCOSITY #: Explicit thermal conduction term div(kappa grad T) in the energy #: equation (constant conductivity params.thermal_conductivity, #: explicit integration). Currently only for finite difference mode. thermal_conduction: bool = False #: The size of the simulation box. box_size: Union[float, StaticFloatVector] = 1.0 #: The number of cells in the simulation. num_cells: Union[int, StaticIntVector] = 400 #: The reconstruction order is the number of #: cells on each side of the cell of interest #: used to calculate the gradients for the #: reconstruction at the interfaces. reconstruction_order: int = 1 #: The limiter for the reconstruction. #: Only affects finite volume mode. limiter: int = MINMOD #: The Riemann solver used #: Only for finite volume mode. riemann_solver: int = HLL #: Dimensional splitting / unsplit mode. #: Note that the UNSPLIT scheme currently #: interferes with energy conservation in settings #: with self-gravity. split: int = UNSPLIT #: Time integration method. time_integrator: int = RK2_SSP # Explanation of the ghost cells # |---------| # |---------| # stencil |---------| # cells || 1g | 2g | 3c | 4g | 5g || # reconstructions |L R|L R|L R| | # fluxes --> --> # update | 3c'| # --> all others are ghost cells #: The number of ghost cells. num_ghost_cells: int = reconstruction_order + 1 #: Grid spacing. grid_spacing: float = box_size / num_cells #: Explicit boundary handling mode. boundary_handling: int = GHOST_CELLS #: Boundary settings for the simulation. boundary_settings: Union[NoneType, BoundarySettings1D, BoundarySettings] = None #: Enables a fixed timestep for the simulation #: based on the specified number of timesteps. fixed_timestep: bool = False #: Exactly reach the end time. In adaptive timestepping, #: one might otherwise overshoot. exact_end_time: bool = True #: Adds the sources with the current timestep to #: a hypothetical state to estimate the actual timestep. #: Useful for time-dependent sources, but additional #: computational overhead. source_term_aware_timestep: bool = False #: The number of timesteps for the fixed timestep mode. num_timesteps: int = 1000 #: Use a maximum timestep in adaptive timestep mode. use_max_adaptive_timestep: bool = True #: The differentiation mode one whats to use #: the solver in (forwards or backwards). differentiation_mode: int = FORWARDS #: The number of checkpoints used in the setup #: with backwards differetiability and adaptive #: time stepping. num_checkpoints: int = 100 #: Return intermediate snapshots of the time evolution #: instead of only the final fluid state. return_snapshots: bool = False #: Snapshot settings snapshot_settings: SnapshotSettings = SnapshotSettings() #: Where the snapshots are stored. ``ON_DEVICE`` (default) keeps the #: snapshot diagnostics in preallocated device buffers and returns them #: at the end (the classic behaviour). ``TO_DISK`` instead streams each #: snapshot to disk via Orbax: the run is split into segments between the #: snapshot times, and the loop carry (primitive state, PRNG key, OU #: forcing field) plus the time is written to ``snapshot_storage_path`` #: after each segment. Each device writes its own shard, so this scales #: to multiple devices / nodes. TO_DISK is forward-mode only. snapshot_storage_mode: int = ON_DEVICE #: Directory the Orbax checkpoints are written to / read from when #: ``snapshot_storage_mode == TO_DISK``. Required in that mode. snapshot_storage_path: Union[str, NoneType] = None #: Call a user given function on the snapshot data, #: e.g. for saving or plotting. Must have signature #: callback(time, state, registered_variables). activate_snapshot_callback: bool = False #: Return snapshots at specific time points. use_specific_snapshot_timepoints: bool = False #: The number of snapshots to return. num_snapshots: int = 10 #: Fallback to the first order Godunov scheme. first_order_fallback: bool = False # physical modules #: Turbulent forcing configuration. turbulent_forcing_config: TurbulentForcingConfig = TurbulentForcingConfig() #: The configuration for the stellar wind module. wind_config: WindConfig = WindConfig() #: Cosmic rays cosmic_ray_config: CosmicRayConfig = CosmicRayConfig() #: The configuration for the cooling module. cooling_config: CoolingConfig = CoolingConfig() #: Frame tracking in z-direction #: shifting the frame to follow a #: turbulent radiative mixing layer frame_tracking: bool = False #: Configuration of the neural network force module. neural_net_force_config: NeuralNetForceConfig = NeuralNetForceConfig() #: Configuration of the CNN MHD corrector module. cnn_mhd_corrector_config: CNNMHDconfig = CNNMHDconfig()
[docs] def gpu_compute_capability_at_least_80() -> bool: """Return whether every visible NVIDIA GPU has compute capability >= 8.0. Compute capability 8.0 (Ampere) is the floor for the Triton kernels the Pallas backend compiles to, so this is the predicate that decides whether OPTIMAL_BACKEND resolves to PALLAS. Any failure to query the GPUs — no ``nvidia-smi`` on the PATH (e.g. a CPU-only host) or the call erroring out — is treated as "not capable" so the safe NATIVE_JAX fallback is chosen. Returns: True if all visible NVIDIA GPUs report compute capability >= 8.0, False otherwise (including when no GPU could be queried). """ try: output = subprocess.check_output( ["nvidia-smi", "--query-gpu=compute_cap", "--format=csv,noheader"], text=True, ) except (subprocess.CalledProcessError, FileNotFoundError): return False compute_caps = [] for line in output.strip().splitlines(): major, minor = map(int, line.strip().split(".")) compute_caps.append((major, minor)) if not compute_caps: return False return all(compute_cap >= (8, 0) for compute_cap in compute_caps)
[docs] def finalize_config(config: SimulationConfig, state_shape) -> SimulationConfig: """Fill in derived configuration fields and validate the configuration. Resolves the values that depend on the actual state shape or on cross-field consistency: the positivity-protection defaults, the number of cells per axis, the grid spacing, the geometry- and solver-specific overrides, the master gravity switch, the boundary defaults, and the disk-snapshot requirements. Args: config: The user-supplied simulation configuration. state_shape: The shape of the (unpadded) primitive state array, used to derive ``num_cells`` per axis. Returns: The finalized simulation configuration. """ # Resolve the OPTIMAL_BACKEND request into a concrete backend before any # downstream code inspects ``config.backend``. PALLAS needs an Ampere-class # (compute capability >= 8.0) GPU for its Triton kernels; anywhere else we # fall back to the portable NATIVE_JAX backend. if config.backend == OPTIMAL_BACKEND: if gpu_compute_capability_at_least_80(): print("OPTIMAL_BACKEND: using the PALLAS backend (GPU compute capability >= 8.0).") config = config._replace(backend=PALLAS) else: print("OPTIMAL_BACKEND: using the NATIVE_JAX backend (no compute capability >= 8.0 GPU found).") config = config._replace(backend=NATIVE_JAX) # ``default_positivity_protection`` is a casual on/off switch for the STATE # floors only: the default ``False`` is a clean slate (no per-stage / # per-step flooring). When set, turn the floors on (HARD_FLOOR) unless the # user explicitly chose a mode. The read-only clamps (clamp_in_estimates) # are decoupled and left untouched (default on), as are the feature toggles # (deepvoid_blend, preserving_flux, conservative redistribution, # vacuum_rest, nan_safe). positivity_config = config.positivity_config if positivity_config.default_positivity_protection: config = config._replace(positivity_config=positivity_config._replace( per_stage_mode=(POSITIVITY_HARD_FLOOR if positivity_config.per_stage_mode == POSITIVITY_NONE else positivity_config.per_stage_mode), per_step_mode=(POSITIVITY_HARD_FLOOR if positivity_config.per_step_mode == POSITIVITY_NONE else positivity_config.per_step_mode), )) if jax.config.jax_enable_x64: config._replace(numerical_precision=DOUBLE_PRECISION) else: config._replace(numerical_precision=SINGLE_PRECISION) # set the number of cells if config.dimensionality == 1: num_cells_x = state_shape[-1] config = config._replace(num_cells=StaticIntVector(num_cells_x, -1, -1)) if config.dimensionality == 2: num_cells_x, num_cells_y = state_shape[-2:] config = config._replace(num_cells=StaticIntVector(num_cells_x, num_cells_y, -1)) elif config.dimensionality == 3: num_cells_x, num_cells_y, num_cells_z = state_shape[-3:] config = config._replace(num_cells=StaticIntVector(num_cells_x, num_cells_y, num_cells_z)) if isinstance(config.box_size, float): config = config._replace( box_size=StaticFloatVector( config.box_size, config.box_size, config.box_size ) ) # For now we assume the grid spacing is the same in all dimensions, so the # scalar ``grid_spacing`` is taken from the x-axis and the other axes are # only checked for consistency below. This restriction can be lifted once # the solver accepts a per-axis grid-spacing vector. grid_spacing_vec = config.box_size / config.num_cells if config.dimensionality == 1: config = config._replace(grid_spacing=grid_spacing_vec.x) elif config.dimensionality == 2: config = config._replace(grid_spacing=grid_spacing_vec.x) if not math.isclose(grid_spacing_vec.x, grid_spacing_vec.y): raise ValueError( "For now, we assume the grid spacing is the same in all dimensions. " f"Got grid spacing {grid_spacing_vec}." ) elif config.dimensionality == 3: config = config._replace(grid_spacing=grid_spacing_vec.x) if not (math.isclose(grid_spacing_vec.x, grid_spacing_vec.y) and math.isclose(grid_spacing_vec.x, grid_spacing_vec.z)): raise ValueError( "For now, we assume the grid spacing is the same in all dimensions. " f"Got grid spacing {grid_spacing_vec}." ) if config.geometry == SPHERICAL: print( "For spherical geometry, only HLL is currently supported. Also, only the unsplit mode has been tested." ) # SPHERICAL is intrinsically 1D in this code; pick the x component # so grid_spacing stays a scalar (otherwise CFL divisions blow up # because StaticFloatVector can't be divided by a scalar wave speed). config = config._replace(grid_spacing=(config.box_size / config.num_cells).x) if config.riemann_solver != HLL: print("Setting HLL Riemann solver for spherical geometry.") config = config._replace(riemann_solver=HLL) if config.split != SPLIT: print("Setting unsplit mode for spherical geometry") config = config._replace(split=SPLIT) if config.limiter == VAN_ALBADA or config.limiter == VAN_ALBADA_PP: print("Setting minmod limiter for spherical geometry") config = config._replace(limiter=MINMOD) if config.time_integrator != MUSCL: print("Setting MUSCL time integrator for spherical geometry") config = config._replace(time_integrator=MUSCL) # master gravity switch: active if self-gravity and/or an external # potential is used. This gates the (shared) gravity source-term machinery. config = config._replace(gravity_config=config.gravity_config._replace( gravity=config.gravity_config.self_gravity or config.gravity_config.external_potential )) if config.gravity_config.gravity and (config.limiter != MINMOD): print( "Curiously, in self-gravitating systems, the VAN_ALBADA limiters seem to cause crashes." ) print("Setting MINMOD limiter for gravity.") config = config._replace(limiter=MINMOD) # Finite-difference-specific checks. if config.solver_mode == FINITE_DIFFERENCE: if config.dimensionality == 3 and config.boundary_settings == BoundarySettings( BoundarySettings1D( left_boundary=PERIODIC_BOUNDARY, right_boundary=PERIODIC_BOUNDARY ), BoundarySettings1D( left_boundary=PERIODIC_BOUNDARY, right_boundary=PERIODIC_BOUNDARY ), BoundarySettings1D( left_boundary=PERIODIC_BOUNDARY, right_boundary=PERIODIC_BOUNDARY ), ): # Fully periodic boundaries are enforced more cheaply by rolling the # arrays (PERIODIC_ROLL) than by maintaining explicit ghost cells. print( "For 3D simulations with periodic boundaries, setting boundary handling to " \ "PERIODIC_ROLL and num_ghost_cells to 0 for better performance." ) config = config._replace(boundary_handling=PERIODIC_ROLL, num_ghost_cells=0) else: if config.dimensionality == 3: config = config._replace(boundary_handling=GHOST_CELLS, num_ghost_cells=4) if config.dimensionality == 2 and config.boundary_settings == BoundarySettings( BoundarySettings1D( left_boundary=PERIODIC_BOUNDARY, right_boundary=PERIODIC_BOUNDARY ), BoundarySettings1D( left_boundary=PERIODIC_BOUNDARY, right_boundary=PERIODIC_BOUNDARY ), ): # Fully periodic boundaries are enforced more cheaply by rolling the # arrays (PERIODIC_ROLL) than by maintaining explicit ghost cells. print( "For 2D simulations with periodic boundaries, setting boundary handling to " \ "PERIODIC_ROLL and num_ghost_cells to 0 for better performance." ) config = config._replace(boundary_handling=PERIODIC_ROLL, num_ghost_cells=0) else: if config.dimensionality == 2: config = config._replace(boundary_handling=GHOST_CELLS, num_ghost_cells=4) # The FD scheme has two supported time integrators: the SSPRK4 # Spiteri-Ruuth 3-register scheme (default) and the Carpenter-Kennedy # 2N-storage LSRK4 ("RK4_LSRK") which trades CFL margin for one fewer # full-state buffer. Anything else falls back to SSPRK4. if config.time_integrator not in (RK4_SSP, RK4_LSRK): print( "Setting time integrator to RK4_SSP for finite difference solver mode." ) config = config._replace(time_integrator=RK4_SSP) if config.boundary_handling == PERIODIC_ROLL: config = config._replace(num_ghost_cells=0) if config.boundary_handling == GHOST_CELLS and (config.diffusion or config.thermal_conduction): config = config._replace(num_ghost_cells=max(config.num_ghost_cells, 6)) # Pick sensible default boundary conditions when the user left them unset. if config.boundary_settings is None: if config.geometry == CARTESIAN: print("Automatically setting open boundaries for Cartesian geometry.") if config.dimensionality == 1: config = config._replace( boundary_settings=BoundarySettings1D( left_boundary=OPEN_BOUNDARY, right_boundary=OPEN_BOUNDARY ) ) else: config = config._replace(boundary_settings=BoundarySettings()) elif config.geometry == SPHERICAL and config.dimensionality == 1: print( "Automatically setting reflective left and open right boundary for spherical geometry." ) config = config._replace( boundary_settings=BoundarySettings1D( left_boundary=REFLECTIVE_BOUNDARY, right_boundary=OPEN_BOUNDARY ) ) if config.wind_config.stellar_wind: print( "For stellar wind simulations, we need source term aware timesteps, turning on." ) config = config._replace(source_term_aware_timestep=True) # Disk-snapshot (Orbax) mode requirements. if config.snapshot_storage_mode == TO_DISK: if not config.snapshot_storage_path: raise ValueError( "snapshot_storage_mode == TO_DISK requires a non-empty " "snapshot_storage_path (the directory the Orbax checkpoints " "are written to)." ) if config.differentiation_mode != FORWARDS: raise ValueError( "snapshot_storage_mode == TO_DISK is forward-mode only; " "set differentiation_mode = FORWARDS." ) return config
[docs] def riemann_solver_to_string(riemann_solver: int) -> str: """Return the human-readable name of a Riemann-solver constant.""" if riemann_solver == HLL: return "HLL" elif riemann_solver == HLLC: return "HLLC" elif riemann_solver == HLLC_LM: return "HLLC_LM" elif riemann_solver == LAX_FRIEDRICHS: return "Lax-Friedrichs" elif riemann_solver == HYBRID_HLLC: return "Hybrid HLLC" elif riemann_solver == AM_HLLC: return "AM HLLC"
[docs] def limiter_to_string(limiter: int) -> str: """Return the human-readable name of a slope-limiter constant.""" if limiter == MINMOD: return "Minmod" elif limiter == SUPERBEE: return "Superbee" elif limiter == OSHER: return "Osher" elif limiter == DOUBLE_MINMOD: return "Double Minmod" elif limiter == VAN_ALBADA: return "Van Albada" elif limiter == VAN_ALBADA_PP: return "Van Albada PP"
[docs] def solver_mode_to_string(solver_mode: int) -> str: """Return the short label (``"FV"`` / ``"FD"``) of a solver-mode constant.""" if solver_mode == FINITE_VOLUME: return "FV" elif solver_mode == FINITE_DIFFERENCE: return "FD"
[docs] def config_to_string(config: SimulationConfig) -> str: """Return a compact one-line description of the solver configuration.""" if config.solver_mode == FINITE_VOLUME: return f"FV, {riemann_solver_to_string(config.riemann_solver)}, {limiter_to_string(config.limiter)}, {config.num_cells.x} cells" elif config.solver_mode == FINITE_DIFFERENCE: return f"FD, {config.num_cells.x} cells"