Source code for astronomix.initial_condition_generation.magnetic_field_from_vector_potential

"""
Construct divergence-free magnetic fields from a vector potential.

Initialising the magnetic field from a vector potential ``A`` and taking a
discrete curl guarantees ``div(B) = 0`` to machine precision under the matching
discrete divergence operator. The correct discretisation of that curl depends
on the solver topology, so this module branches on the solver mode: staggered
edge-centered differences for the constrained-transport finite-difference
scheme, and a central-difference curl for the cell-centered finite-volume
scheme.
"""

# typing
from typing import Callable, Tuple

# jax
import jax.numpy as jnp

# astronomix constants
from astronomix.option_classes.simulation_config import (
    FINITE_DIFFERENCE,
    FINITE_VOLUME,
)

# astronomix containers
from astronomix.option_classes.simulation_config import SimulationConfig

# astronomix functions
from astronomix._spatial_operators._differencing import finite_difference_int6
from astronomix._spatial_operators._interpolate import interp_face_to_center


[docs] def setup_magnetic_fields_from_vector_potential( config: SimulationConfig, vector_potential_func: Callable, *args, **kwargs ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: """ Calculate the cell-centered and face-centered magnetic fields from a given vector potential function, correctly applying discrete curl operations depending on the solver mode (FINITE_DIFFERENCE or FINITE_VOLUME) to ensure a divergence-free magnetic field. Note: Uniform background fields cannot be correctly generated from a periodic vector potential. Add background fields to the returned arrays after calling. Args: config: The simulation configuration object. vector_potential_func: A callable ``f(X, Y, Z, *args, **kwargs)`` that returns a tuple ``(A_x, A_y, A_z)`` evaluated at the given coordinates. *args: Additional positional arguments passed to ``vector_potential_func`` (e.g. a time ``t``). **kwargs: Additional keyword arguments passed to ``vector_potential_func``. Returns: B_x, B_y, B_z: Cell-centered magnetic field components. bxb, byb, bzb: Face-centered (interface) magnetic field components. """ _XAXIS, _YAXIS, _ZAXIS = 0, 1, 2 # Extract the box size and resolution, accepting either a uniform scalar # config (1D) or a per-axis tuple config (3D). try: Lx, Ly, Lz = config.box_size except TypeError: Lx = Ly = Lz = config.box_size try: Nx, Ny, Nz = config.num_cells except TypeError: Nx = Ny = Nz = config.num_cells dx, dy, dz = Lx / Nx, Ly / Ny, Lz / Nz if config.solver_mode == FINITE_DIFFERENCE: # Each vector-potential component lives on a different staggered edge of # the cell, so build the matching shifted (``_l``) and centered (``_c``) # coordinate grids per axis. x_l = jnp.linspace(dx, Lx, Nx, endpoint=True) y_l = jnp.linspace(dy, Ly, Ny, endpoint=True) z_l = jnp.linspace(dz, Lz, Nz, endpoint=True) x_c = jnp.linspace(dx/2, Lx + dx/2, Nx, endpoint=False) y_c = jnp.linspace(dy/2, Ly + dy/2, Ny, endpoint=False) z_c = jnp.linspace(dz/2, Lz + dz/2, Nz, endpoint=False) # A_x lives on yz-edges: (i, j+1/2, k+1/2) -> (x_c, y_l, z_l) Xax, Yax, Zax = jnp.meshgrid(x_c, y_l, z_l, indexing="ij") # A_y lives on zx-edges: (i+1/2, j, k+1/2) -> (x_l, y_c, z_l) Xay, Yay, Zay = jnp.meshgrid(x_l, y_c, z_l, indexing="ij") # A_z lives on xy-edges: (i+1/2, j+1/2, k) -> (x_l, y_l, z_c) Xaz, Yaz, Zaz = jnp.meshgrid(x_l, y_l, z_c, indexing="ij") # Evaluate each vector-potential component on its own edge grid. A_x, _, _ = vector_potential_func(Xax, Yax, Zax, *args, **kwargs) _, A_y, _ = vector_potential_func(Xay, Yay, Zay, *args, **kwargs) _, _, A_z = vector_potential_func(Xaz, Yaz, Zaz, *args, **kwargs) # Take the discrete curl of A on the staggered grid to obtain the # face-centered (divergence-free) magnetic field. bxb = (1.0 / dy) * finite_difference_int6(A_z, _YAXIS) \ - (1.0 / dz) * finite_difference_int6(A_y, _ZAXIS) byb = (1.0 / dz) * finite_difference_int6(A_x, _ZAXIS) \ - (1.0 / dx) * finite_difference_int6(A_z, _XAXIS) bzb = (1.0 / dx) * finite_difference_int6(A_y, _XAXIS) \ - (1.0 / dy) * finite_difference_int6(A_x, _YAXIS) # Interpolate the face fields back to cell centers for the # cell-centered magnetic field. B_x = interp_face_to_center(bxb, _XAXIS) B_y = interp_face_to_center(byb, _YAXIS) B_z = interp_face_to_center(bzb, _ZAXIS) elif config.solver_mode == FINITE_VOLUME: # The finite-volume scheme stores B at cell centers, so evaluate the # vector potential directly on the cell-centered grid. x_c = jnp.linspace(dx/2, Lx + dx/2, Nx, endpoint=False) y_c = jnp.linspace(dy/2, Ly + dy/2, Ny, endpoint=False) z_c = jnp.linspace(dz/2, Lz + dz/2, Nz, endpoint=False) Xc, Yc, Zc = jnp.meshgrid(x_c, y_c, z_c, indexing="ij") A_x_c, A_y_c, A_z_c = vector_potential_func(Xc, Yc, Zc, *args, **kwargs) # Use the same central-difference stencil the FV solver uses for div(B), # so the resulting field is divergence-free under that operator. def central_diff(f, axis, d): return (jnp.roll(f, -1, axis=axis) - jnp.roll(f, 1, axis=axis)) / (2 * d) # Central-difference curl of A -> cell-centered magnetic field. B_x = central_diff(A_z_c, _YAXIS, dy) - central_diff(A_y_c, _ZAXIS, dz) B_y = central_diff(A_x_c, _ZAXIS, dz) - central_diff(A_z_c, _XAXIS, dx) B_z = central_diff(A_y_c, _XAXIS, dx) - central_diff(A_x_c, _YAXIS, dy) # In FV there are no separate interface fields, so the "face" fields # simply mirror the cell-centered ones for a uniform return signature. bxb, byb, bzb = B_x, B_y, B_z else: raise ValueError(f"Unsupported solver_mode: {config.solver_mode}") return B_x, B_y, B_z, bxb, byb, bzb