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.Simulation — Module
SimulationAthenaK snapshot reading, meshblock sampling, and the normal-frame primitive → four-vector transform (AthenaK: Stone et al. 2024).
AthenaK GRMHD input
BlackLightPlus.Simulation.AthenaKSnapshot — Type
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.
BlackLightPlus.Simulation.athenak_bin_snapshot — Function
athenak_bin_snapshot(binfile; gamma) -> AthenaKSnapshotAdapt 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).
BlackLightPlus.Simulation.read_athenak_bin — Function
read_athenak_bin(filename) -> AthenaKBinRead 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).
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_primitives — Function
sample_primitives(snap, pos::SVector{4}) -> NamedTupleClamped 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.
BlackLightPlus.Simulation.sample_var — Function
Trilinear-sample variable v (1-based col) at (x,y,z); NaN if off-mesh.
BlackLightPlus.Simulation.primitive_four_vectors — Function
primitive_four_vectors(uu1, uu2, uu3, bb1, bb2, bb3, gcov, gcon) -> NamedTupleGiven 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.
BlackLightPlus.Simulation.pgas_from_eint — Function
Ideal-gas pressure from internal-energy density: p_gas = (gamma - 1) * eint.
BlackLightPlus.Simulation.units — Function
units(m_bh_cgs, mass_scale_cgs) -> NamedTupleGRMHD 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).
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.LeafTable — Type
LeafTableDense 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.
BlackLightPlus.Simulation.leaf_table — Function
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).
BlackLightPlus.Simulation.meshblock_index — Function
meshblock_index(snap, x, y, z) -> IntFinest 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.
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_adm — Function
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.
BlackLightPlus.Simulation.z4c_grid_spacetime — Function
z4c_grid_spacetime(binfile, half, n; alpha_min=0.15) -> ADMGridSpacetimeRead an AthenaK Z4c .bin dump, resample the ADM fields onto a uniform n³ 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).
Snapshot series and slow light
BlackLightPlus.Simulation.SnapshotSeries — Type
SnapshotSeriesTime-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.
BlackLightPlus.Simulation.synthetic_series — Function
synthetic_series(snap; times, evolve=(s,t)->s) -> SnapshotSeriesBuild 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)).
BlackLightPlus.Simulation.bracketing_pair — Function
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).
BlackLightPlus.Simulation.get_frame — Function
get_frame(series, ia) -> AthenaKSnapshotReturn 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.
BlackLightPlus.Simulation.sample_primitives_t — Function
sample_primitives_t(series, x::SVector{4}) -> NamedTupleSlow-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.
BlackLightPlus.Simulation.scale_density — Function
scale_density(snap::AthenaKSnapshot, factor) -> AthenaKSnapshotReturn 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.