Simulation API

The Simulation module reads external simulation data: AthenaK GRMHD snapshots (Stone et al., 2026) in .athdf and .bin form, Z4c numerical-relativity dumps (Zhu et al., 2025), and time series of snapshots for slow-light rendering.

BlackLightPlus.SimulationModule
Simulation

AthenaK snapshot reading, meshblock sampling, and the normal-frame primitive → four-vector transform (AthenaK: Stone et al. 2024).

source

AthenaK GRMHD input

BlackLightPlus.Simulation.AthenaKSnapshotType
AthenaKSnapshot(filename)

Load an AthenaK .athdf snapshot: reads uov/B primitives, cell centres (x1v,x2v,x3v), cell faces (x1f,x2f,x3f), Levels, and LogicalLocations via HDF5.jl. Stores the (ni,nj,nk,nmb,8) primitive block, per-meshblock face-extent bounding boxes, and per-block first cell centre + spacing for the clamped-trilinear sampler.

source
BlackLightPlus.Simulation.athenak_bin_snapshotFunction
athenak_bin_snapshot(binfile; gamma) -> AthenaKSnapshot

Adapt an AthenaK GRMHD fluid .bin dump (mhd_w_bcc output group) into an AthenaKSnapshot. dens, velx, vely, velz, bcc1..3 pass through bit-identically; press → eint = press/(gamma-1) in Float64, rounded once to Float32; a temperature column is dropped. Non-finite cells (isolated c2p failures) are zeroed so validity guards skip them. Logical locations are derived from block bounds (lx = (x_min - domain_min)/width, exact for AthenaK's power-of-two layout, checked integral per block).

source
BlackLightPlus.Simulation.read_athenak_binFunction
read_athenak_bin(filename) -> AthenaKBin

Read an "Athena binary output version=1.1" .bin dump: ASCII header ending in "header offset=<H>", an <H>-byte parameter dump, then per meshblock Int32[10] = (ois,oie,ojs,oje,oks,oke, lx1,lx2,lx3, level), Real[6] block bounds (Real = the header's "size of location": 8, or 4 for a single-precision build), and Float32[n_vars·nx3·nx2·nx1] cell data (i fastest, then j, k, var).

source

Sampling and unit conversion

These are exported by Simulation but not re-exported at the top level; reach them as BlackLightPlus.Simulation.<name>.

BlackLightPlus.Simulation.sample_primitivesFunction
sample_primitives(snap, pos::SVector{4}) -> NamedTuple

Clamped trilinear sample of the 8 primitives at CKS position pos = (t,x,y,z). Returns (rho, eint, uu::SVector{3}, bb::SVector{3}, inside::Bool); outside every meshblock, inside = false and all fields zero. Cell indices are clamped so the i0+1 upper corner stays in range (ghost-free edge behaviour); clamp-shell and refinement-seam errors are an order below the ray step-quadrature error.

source
BlackLightPlus.Simulation.primitive_four_vectorsFunction
primitive_four_vectors(uu1, uu2, uu3, bb1, bb2, bb3, gcov, gcon) -> NamedTuple

Given the AthenaK normal-frame velocity (uu1, uu2, uu3), the cell-centred lab 3-field (bb1, bb2, bb3), and the covariant / contravariant metric gcov, gcon :: SMatrix{4,4} at the sample point (index 1 = time), return (ucon, ucov, bcon, bcov, b_sq): u^μ, uμ, b^μ, bμ as SVector{4} and b_sq = b^μ b_μ ≥ 0. Matches blacklight's PrimitiveToFourVectorsG.

source
BlackLightPlus.Simulation.unitsFunction
units(m_bh_cgs, mass_scale_cgs) -> NamedTuple

GRMHD code → CGS unit conversions: m_bh_cgs is the black-hole mass [g] and mass_scale_cgs the density mass scale [g]. Returns (L_unit, T_unit, dens_unit, Ne_unit, B_unit) with L = GM/c², T = L/c, dens = massscale / L³, Ne = dens / (mp + m_e), B = c·sqrt(4π dens).

source

AMR meshblock indexing

A dense finest-level leaf-index grid that turns block location into an $O(1)$ lookup. The same structure is uploaded to the device for native GPU AMR sampling.

BlackLightPlus.Simulation.LeafTableType
LeafTable

Dense finest-block index grid for O(1) meshblock location: leaf[i,j,k] is the owning meshblock (1-based; 0 = vacuum) of finest-block cell (i,j,k). Carries the mesh bounds and inverse finest spacing used by the locate.

source
BlackLightPlus.Simulation.leaf_tableFunction
leaf_table(snap) -> Union{Nothing,LeafTable}

The snapshot's O(1) leaf-block index grid, built lazily and cached (a lock guards the one-time build; an atomic flag publishes it lock-free afterwards). Returns nothing for a mesh too large / not finest-aligned (callers then use the scan).

source
BlackLightPlus.Simulation.meshblock_indexFunction
meshblock_index(snap, x, y, z) -> Int

Finest owning meshblock (1-based; 0 = vacuum) of CKS position (x,y,z), via the O(1) leaf table when available and the linear scan otherwise. The single locate entry point for sampling.

source

Z4c numerical relativity

Maps the conformal Z4c variables to physical ADM fields ($\gamma_{ij} = \tilde g_{ij}/\chi$, with $\alpha$ and $\beta^i$ passed through) and builds an ADMGridSpacetime.

BlackLightPlus.Simulation.resample_z4c_admFunction
resample_z4c_adm(snap, half, n) -> (axis, alpha, beta, gamma)

Resample the physical ADM fields from a Z4c .bin snapshot onto a uniform n×n×n box [-half, half]³: returns the axis range, alpha[n,n,n], beta[3,n,n,n] (β^i), and gamma[6,n,n,n] (gxx,gxy,gxz,gyy,gyz,gzz), with gamma_ij = g̃_ij / chi. Off-mesh points get Minkowski (alpha=1, beta=0, gamma=δ). Threaded over the grid.

source
BlackLightPlus.Simulation.z4c_grid_spacetimeFunction
z4c_grid_spacetime(binfile, half, n; alpha_min=0.15) -> ADMGridSpacetime

Read an AthenaK Z4c .bin dump, resample the ADM fields onto a uniform box [-half, half]³, and return an ADMGridSpacetime for geodesic tracing. alpha_min is the lapse-collapse inner boundary (0.15 sits outside the horizons). Frozen-slice approximation: one time slice is traced with ∂ₜg_μν = 0 (the ADM time-derivative terms are dropped), valid only while the light-crossing time ≈ 2·half is ≪ the spacetime's evolution timescale (BBH orbital period).

source

Snapshot series and slow light

BlackLightPlus.Simulation.SnapshotSeriesType
SnapshotSeries

Time-ordered GRMHD snapshot sequence for slow light: ascending frame coordinate times plus either on-disk .athdf paths (lazily loaded through a maxslots-slot LRU cache) or pre-materialised in-memory frames (from synthetic_series). t_obs is the observation epoch at the camera (X⁰ = 0): a ray sample at X⁰ was emitted at t_obs - X⁰. bracketing_pair(series, t) maps a coordinate time to (ia, w); sample_primitives_t(series, x) blends the bracketing dumps; lk serialises load/LRU/evict on a path-backed series.

source
BlackLightPlus.Simulation.synthetic_seriesFunction
synthetic_series(snap; times, evolve=(s,t)->s) -> SnapshotSeries

Build a controlled test series from a single snapshot. evolve(snap, t) returns the frame at time t; the default identity gives object-identical frames and hence the fast-light limit. E.g. a global density modulation is evolve = (s,t) -> scale_density(s, 1 + eps*sin(omega*t)).

source
BlackLightPlus.Simulation.bracketing_pairFunction
bracketing_pair(series, t) -> (ia, w)

Locate the dump bracket for coordinate time t: returns the lower frame index ia ∈ 1:N-1 and the blend weight w = (t - t_a)/(t_b - t_a) ∈ [0,1] on the UPPER frame ia+1 (so the blend is (1-w)*frame[ia] + w*frame[ia+1]). Clamps at the ends — t <= times[1] -> (1, 0), t >= times[end] -> (N-1, 1) — matching ipole's window clamp. A single-frame series returns (1, 0.0).

source
BlackLightPlus.Simulation.get_frameFunction
get_frame(series, ia) -> AthenaKSnapshot

Return frame ia, loading it from series.paths[ia] (and caching it, evicting the least-recently-used loadable frame past maxslots) if it is not already resident. Materialised (synthetic) frames are always resident and never evicted, so that hot path is read-only (thread-safe, allocation-free); loads/LRU touches/evictions on a path-backed series are serialised by series.lk.

source
BlackLightPlus.Simulation.sample_primitives_tFunction
sample_primitives_t(series, x::SVector{4}) -> NamedTuple

Slow-light primitive sample at CKS position+time x = (t, X, Y, Z): pick the dump bracket at the emission time series.t_obs - x[1] (X⁰ = x[1] is the light-travel time from the camera), spatially sample both dumps with sample_primitives, and blend the primitives linearly in time. Returns the same NamedTuple shape as sample_primitives. w == 0 / w == 1 (the window-end clamps) and object-identical dumps short-circuit to a single bit-identical spatial sample.

source
BlackLightPlus.Simulation.scale_densityFunction
scale_density(snap::AthenaKSnapshot, factor) -> AthenaKSnapshot

Return a snapshot sharing snap's geometry but with the density primitive (axis 1) scaled by factor (a global density modulation; factor == 1 returns snap unchanged so an identity evolve is object-identical). Only the primitive block is copied; the per-block geometry vectors are shared.

source