← the appv0.1.1

Science notes

An interactive teaching resource on how neutral hydrogen between a distant quasar and us becomes the Lyman-α forest: atoms meet light, light stretches, gas maps to velocity, optical depth adds, transmitted light multiplies, and the forest can be read backward only up to a point. These notes list, for every scientific claim the scenes make, its sources, the assumptions under which it holds, the simplifications made for teaching, and how the browser's numbers were checked.

What is modeled

What the teaching app does not model

Toy, oracles and data

The teaching toy
Every scene draws from one small synthetic data set: a 20 Mpc/h Zel'dovich volume at z = 3 (a 20 × 8 × 8 Mpc/h block of it is drawn) and one sightline through it The toy Zel'dovich volume (synthetic teaching data), decomposed for teaching into parcels and cells Parcels are a teaching decomposition of a continuous sightline. It is not a hydrodynamic simulation and contains no data from any survey or simulation suite.
Numerical oracles
Independent calculations check the browser's physics: SciPy's Faddeeva function for the Voigt profile, exact-Voigt optical-depth sums, numerical integration of distances, and the fake_spectra code for the real-space method. Their results are listed under Accuracy.
Simulation data, later
The skewer interface The skewer interface (dataset-independent) is independent of the data set, so a later release may replace the toy with simulation skewers whose conventions match fake_spectra fake_spectra conventions (the validation reference). No such data are included in this release.

The claims, in six chapters

Each claim states what the app teaches, its sources at their exact location, its assumptions and validity, the simplifications made for teaching, and how its numbers are checked. Read in order, the chapters follow the story of the app: one atom, one line, the expanding universe, many contributions, the gas itself, and the inverse problem.

1 · Lyα: one atom and the colour it absorbs

What a single hydrogen atom does to the quasar's light: the Lyα transition, resonance in the atom's own frame, and resonant scattering out of the beam.

Lyα (H I 1s → 2p) transition data

Neutral hydrogen in its ground state resonantly absorbs Lyα photons at a rest (vacuum) wavelength of 1215.67 Å, with absorption oscillator strength f ≈ 0.4164; the 2p level decays back to 1s with Einstein coefficient A21 ≈ 6.26×10⁸ s⁻¹, i.e. a lifetime ≈ 1.6 ns.

λ0=1215.6701 A˚ (vacuum),f12=0.4164,A21=6.2649×108 s−1\lambda_0 = 1215.6701\ \text{Å (vacuum)},\quad f_{12} = 0.4164,\quad A_{21} = 6.2649\times10^{8}\ {\rm s^{-1}}
Convention
λ0 = 1215.6701 Å is NIST's unresolved 1s–2 multiplet value (its upper level includes 2s); the gf-weighted 2p centre is 1215.67004 Å (0.015 km/s different). The ~1.3 km/s fine-structure splitting (1215.6736 / 1215.6682 Å) is below every width the resource shows and is not taught.
Assumptions
  • vacuum wavelengths; fine structure unresolved; ground-state population only
Valid for
all IGM conditions considered (A₂₁ far exceeds the collisional and photo-excitation rates at IGM densities, so n = 2 is essentially empty)
Sources
How it is checked

values typed independently in a separate Python check and compared in Optical depth

Optical depth — pass

used in 2 · One atom meets one colour, 4 · The atom's lifetime leaves faint, far wings, 5 · The same colour, stretched, 13 · Same shadow · status: verified · id SCI-ATOM-001

Resonance in the atom's frame (Doppler condition)

An atom moving with line-of-sight velocity v sees incoming light Doppler-shifted; it is resonant with light whose observed (lab-frame) wavelength is λ0(1 + v/c) to first order in v/c. An atom moving away from the observer (v > 0, toward the quasar) therefore absorbs a redder colour.

λres=λ0 (1+v/c)+O(v2/c2)\lambda_{\rm res} = \lambda_0\,(1 + v/c) + \mathcal{O}(v^2/c^2)
Convention
same sign as Peculiar-velocity sign convention
Assumptions
  • non-relativistic speeds (v/c ≲ 10⁻³)
Valid for
v ≪ c
Simplifications
Sources
How it is checked

Real space to velocity space checks that v_pec > 0 gives larger u and longer λ_obs

Real space to velocity space — pass

used in 2 · One atom meets one colour · status: verified · id SCI-ATOM-002

Natural broadening from the finite lifetime of 2p

Because the 2p state has a finite lifetime (1/A21 ≈ 1.6 ns), even a stationary atom's response is a Lorentzian with half-width γ = Γλ0/4π ≈ 0.0061 km/s, Γ = A21 (2p decays only to 1s). This is 1765× narrower (FWHM) than the thermal width at 10⁴ K, but falls off only as 1/Δv².

γ=Γλ04π=6.06×10−3 km s−1,a=γ/b\gamma = \frac{\Gamma\lambda_0}{4\pi} = 6.06\times10^{-3}\ {\rm km\,s^{-1}},\qquad a = \gamma/b
Convention
Γ is the total decay rate out of the upper level (the lower level is the ground state, width 0)
Assumptions
  • radiative damping only; collisional (pressure) broadening negligible at IGM densities
Valid for
IGM densities (n ≲ 10⁻² cm⁻³)
Simplifications
  • The damped oscillation (microscope only, labelled) is the classical Lorentz-oscillator analogue; the physical statement is the finite lifetime.
  • The natural line is magnified ×1200 in the inset (Natural width is magnified wherever it is drawn).
Sources
How it is checked

γ_v recomputed independently in a separate Python check; Optical depth

Optical depth — pass

How it is computed
Natural width and the Voigt profile (lya_app)

used in 4 · The atom's lifetime leaves faint, far wings · status: verified · id SCI-NAT-001

Photoionisation equilibrium and the neutral-amount control

In the highly ionised IGM, photoionisation balances recombination: n_HI Γ_HI = α(T) n_e n_p, so n_HI ≈ α(T) n_e n_H / Γ_HI ∝ Δ² T^(−0.7) / Γ_HI. Changing the UV background rescales n_HI (and τ) by 1/Γ_HI; it does not move lines or change their τ-shape.

nHI≃αA(T) ne nHΓHI,αA≃4.2×10−13 (T/104 K)−0.7 cm3 s−1n_{\rm HI} \simeq \frac{\alpha_A(T)\,n_e\,n_{\rm H}}{\Gamma_{\rm HI}},\qquad \alpha_A \simeq 4.2\times10^{-13}\,(T/10^4\,{\rm K})^{-0.7}\ {\rm cm^3\,s^{-1}}
Convention
case-A recombination (optically thin IGM); n_e = 1.163 n_H (H and He fully ionised, Y_P = 0.2454, Planck 2018)
Assumptions
  • ionisation equilibrium (recombination time ≪ Hubble time is NOT true at mean density; equilibrium is still set by photoionisation, which is fast)
  • He fully ionised (n_e/n_H = 1.081 with helium singly ionised to 1.163 fully ionised, for Y_P = 0.2454; a 7 % systematic)
Valid for
n_HI ≪ n_H (forest; not self-shielded LLS/DLAs)
Simplifications
  • The teaching control changes N_HI directly, holding density, temperature and position fixed; real UV changes also heat the gas.
Sources
  • Hui & Gnedin 1997 — Eq. (12): α ≈ 4×10⁻¹³ (T/10⁴ K)^−0.7 cm³ s⁻¹; Appendix A fit
  • McQuinn 2016 — §2.1, Eq. (2): x_HI = α_A n_e/Γ
  • Meiksin 2009 — §III.A.1, Eqs. (40)–(42) (secondary)
  • Draine 2011 — §14.2, Eq. (14.3): α_A ≈ 4.13e-13 at 10⁴ K (secondary)
How it is checked

Toy sightline: line kernels, neutral fraction, and the mean-flux calibration — pass

How it is computed
How much of the hydrogen is neutral (lya_app)

used in 1 · Same gas, different scale, 6 · Where does one parcel land? · status: verified · id SCI-ION-001

2 · The line profile: thermal core and natural wings

How motion and the excited state's short life shape one absorption line — the Gaussian census, the Lorentzian wings, their Voigt combination and how the browser computes it.

Maxwellian velocities; one line-of-sight component is Gaussian

In gas at temperature T each Cartesian velocity component of the atoms follows a Gaussian with variance kT/m. The beam is sensitive only to the component along it.

f(v∥)=m2πkT  e−mv∥2/2kTf(v_\parallel) = \sqrt{\frac{m}{2\pi kT}}\;e^{-m v_\parallel^2/2kT}
Convention
v_∥ along the sightline, positive away from the observer
Assumptions
  • thermal equilibrium of the atoms' motions; no turbulence
Valid for
IGM gas (collisional thermalisation of velocities is fast compared with dynamical times)
Simplifications
Sources
  • Rybicki & Lightman 1979 — §10.6 Line Broadening Mechanisms — Doppler broadening, p. 287
  • Draine 2011 — ch. 6, §6.5 'Doppler Broadening: The Voigt Line Profile' (p. 58); Lyα f = 0.4164 per the author's errata to p. 58 (secondary)
How it is checked

the micro view samples N atoms from the Maxwellian and overlays the analytic curve

How it is computed
Thermal Doppler width (lya_app)

used in 3 · Temperature becomes line width · status: verified · id SCI-THERM-001

Doppler parameter and the thermal (Gaussian) profile

The census of line-of-sight speeds is the line's thermal profile: a Gaussian in Δv of width b = √(2kT/m), normalised so that heating spreads the same absorption over more velocities without adding any. b(10⁴ K) = 12.85 km/s for hydrogen.

b=2kT/mH+bturb2,ϕG(Δv)=e−Δv2/b2π bb = \sqrt{2kT/m_{\rm H} + b_{\rm turb}^2},\qquad \phi_{\rm G}(\Delta v) = \frac{e^{-\Delta v^2/b^2}}{\sqrt{\pi}\,b}
Convention
m = mass of the ¹H atom (1.00782503 u = 1.6735328×10⁻²⁴ g; an earlier value, 1.6735575×10⁻²⁴ g, was corrected — a 1.5×10⁻⁵ effect); b_turb = 0 unless stated. (fake_spectra's mass convention is recorded in fake_spectra conventions (the validation reference).)
Assumptions
Valid for
all thermal widths considered
Simplifications
Sources
  • Rybicki & Lightman 1979 — §10.6 Line Broadening Mechanisms — Doppler broadening, p. 287
  • Draine 2011 — ch. 6, §6.5 (p. 58) (secondary)
  • Meiksin 2009 — §II.A, Eqs. (10)–(11): b = (2kT/m_a)^½; a kinematic component adds in quadrature (secondary)
How it is checked

Optical depth checks τ0 and ∫τ du, which fix the normalisation

Optical depth — pass

How it is computed
Thermal Doppler width (lya_app) · One parcel's line — its width and depth (lya_app)

used in 3 · Temperature becomes line width, 6 · Where does one parcel land?, 13 · Same shadow · status: verified · id SCI-THERM-002

The Voigt profile — normalisation, definition and asymptotics

The line profile is the convolution of the thermal Gaussian with the natural Lorentzian (each atom's Lorentzian at each atom's Doppler shift). In velocity units φ(Δv) = H(a, Δv/b)/(√π b), with H(a,x) = Re w(x + ia) and ∫H dx = √π. For a ≪ 1 the core is ≈ e^(−x²) and the far wings ≈ a/(√π x²): core from motion, wings from lifetime.

H(a,x)=aπ∫−∞∞e−y2 dy(x−y)2+a2,ϕ(Δv)=H(a,Δv/b)π bH(a,x) = \frac{a}{\pi}\int_{-\infty}^{\infty}\frac{e^{-y^2}\,dy}{(x-y)^2 + a^2},\qquad \phi(\Delta v) = \frac{H(a,\Delta v/b)}{\sqrt{\pi}\,b}
H(a,x)≃e−x2+aπ x2(a≪1)H(a,x) \simeq e^{-x^2} + \frac{a}{\sqrt{\pi}\,x^2}\quad (a \ll 1)
Convention
x = Δv/b, a = γ/b
Assumptions
Valid for
all a considered; the two-term asymptotic is a teaching approximation, not used in computation
Simplifications
  • The micro view exaggerates the damping parameter (labelled, ×1–1000).
Sources
How it is checked

scipy.special.wofz (Faddeeva function)

Voigt function accuracy — pass

Optical depth — pass

How it is computed
Natural width and the Voigt profile (lya_app)

used in 4 · The atom's lifetime leaves faint, far wings · status: verified · id SCI-VOIGT-001

Numerical Voigt implementation in the app

The app evaluates H(a,x) with Humlicek's (1982) W4 rational approximation at all (a, x). Against the Faddeeva function (SciPy's wofz) it is accurate to < 10⁻⁴ (relative) over a ∈ [10⁻⁶, 1], |x| ≤ 2×10⁴ (measured values in Voigt function accuracy).

Valid for
a ∈ [1e-6, 1], |x| ≤ 2e4
Sources
  • Humlíček 1982 — algorithm W4 (regions I–IV): all 29 coefficients and 4 border constants are identical, digit by digit, in five independent open-source transcriptions that cite Humlicek 1982 (RFM, ExoCross, SKIRT 9, SOCRATES, monoRTM). The original paper was not consulted; the accuracy authority is SciPy's Faddeeva function (wofz), against which Voigt function accuracy tests every region down to a = 10⁻⁶; the regime where Zaghloul (2015) reports W4 failing (a < 10⁻⁷) lies below every damping parameter used here
  • Zaghloul 2015 — Table 1 (original borders of the regions), p. 3, arXiv:1505.05596v1 — matches the app's region borders (secondary)
How it is checked

Voigt function accuracy — pass

How it is computed
Natural width and the Voigt profile (lya_app) · SciPy's Faddeeva function — the Voigt oracle (external package)

used in 4 · The atom's lifetime leaves faint, far wings · status: verified-numeric · id SCI-VOIGT-002

Saturation and the curve of growth

Once τ0 ≳ 3 the line core transmits almost nothing and the line's depth stops measuring N; its equivalent width then grows only logarithmically (∝ b√ln τ0) until the damping wings pass τ = 1, after which W ∝ √(N γ). In τ the profile shape never changes; only in F does the line change character.

W∝N (τ0≪1);W∝bln⁡τ0 (flat);W∝Nγ (damped)W \propto N\ (\tau_0 \ll 1);\quad W \propto b\sqrt{\ln\tau_0}\ (\text{flat});\quad W \propto \sqrt{N\gamma}\ (\text{damped})
Assumptions
  • single Voigt line
Valid for
isolated lines
Sources
  • Draine 2011 — ch. 9 Absorption Lines: The Curve of Growth (p. 75 ff.)
How it is checked

Optical depth — pass

How it is computed
Transmission — light multiplies (lya_app)

used in 10 · Light multiplies, 13 · Same shadow · status: verified · id SCI-SAT-001

Where damping wings become visible

From the Lorentzian asymptote, τ(Δv) ≈ Nσ_vγ/(πΔv²), so the wing reaches τ = 1 at Δv ≈ √(Nσ_vγ/π) ≈ 160 km/s (N/10¹⁹ cm⁻²)^½, independent of b (this algebra is ours, from the far-wing profile in Meiksin 2009). For forest lines (N ≲ 10¹⁵) this lies inside the Doppler core: their wings are invisible. The wings become detectable only in rare absorbers of very high column density (high N_HI).

Δvτ=1≈Nσvγ/π\Delta v_{\tau=1} \approx \sqrt{N\sigma_v\gamma/\pi}
Assumptions
  • a ≪ 1
Valid for
damping wings become detectable from N ≳ 10^18.5 (≈ 3×10¹⁸) cm⁻² and set the line's width at τ = 1 above about 10¹⁹ cm⁻² (the sources below); the asymptote assumes a ≪ 1
Sources
  • Meiksin 2009 — §II.A, Eqs. (6), (12)–(13) and the text before Eq. (26) (arXiv v3 p. 9): far from line centre φ_V ≃ a/(πx²); §II.B.1 (p. 9): forest systems are well fit by Doppler profiles, DLAs show the damping wings
  • Rauch 1998 — §3.4 (arXiv v1 p. 15): 'From N ≳ 10^18.5 on, the damping wings of Lyα become detectable'
  • Wolfe, Gawiser & Prochaska 2005 — §1.1 (arXiv v2 p. 4): unit optical depth in the damping wings at Δν_τ=1 ∝ [A21 f21 N(H I)]^1/2 when N(H I) > 10^19 cm⁻² (secondary)
How it is checked

Optical depth: 164 vs 161 km/s (10¹⁹), 720.7 vs 720.0 km/s (2×10²⁰)

Optical depth — pass

used in 4 · The atom's lifetime leaves faint, far wings, 10 · Light multiplies · status: verified · id SCI-SAT-002

3 · Cosmological coordinates: redshift, distance and velocity space

How the expanding universe stretches the light, how real-space distance and peculiar motion map to observed velocity and wavelength, and the conventions used for both.

Cosmological redshift stretches every wavelength by the same factor

Light absorbed by gas at redshift z_abs at rest-frame wavelength λ_rest (1215.67 Å for Lyα, in the gas's own frame) reaches us at λ_obs = λ_rest (1 + z_abs), where 1 + z_abs = a(t_obs)/a(t_abs). Every wavelength, and every wavelength interval, is multiplied by the same factor. The reverse operation, λ_rest = λ_obs/(1 + z_abs), re-expresses the observed spectrum in the absorber's rest frame: there the absorber's own line is at exactly 1215.67 Å. (In the quasar's rest frame, λ_obs/(1 + z_q), the same absorption lies blueward of 1215.67 Å — The quasar's frame — why the forest lies blueward of the quasar's own Lyα.)

λobs=λrest(1+z),1+z=λobsλrest=a(tobs)a(tem),λrest=λobs1+z\lambda_{\rm obs} = \lambda_{\rm rest}(1+z),\qquad 1+z = \frac{\lambda_{\rm obs}}{\lambda_{\rm rest}} = \frac{a(t_{\rm obs})}{a(t_{\rm em})},\qquad \lambda_{\rm rest} = \frac{\lambda_{\rm obs}}{1+z}
Convention
Three frames are kept distinct: the absorber's rest frame (where the atom sees 1215.67 Å), the observer's frame (us), and the quasar's rest frame (The quasar's frame — why the forest lies blueward of the quasar's own Lyα). "Measured at a point along the path" means by an observer at rest with the cosmic expansion there (comoving), for whom λ ∝ a.
Assumptions
  • FLRW expansion; comoving observers; the gas's own small motion folded into z_abs here
Valid for
any z; the beat uses 0 ≤ z_abs < z_q = 3.2 with z = 3 as the default (the toy's redshift)
Simplifications
  • Redshift is presented as the stretching of the light's wavelength with the expansion (λ ∝ a); the Doppler-vs-expansion interpretation debate is not raised.
Sources
  • Hogg 1999 — §3 'Redshift', Eq. (8): z = λ_o/λ_e − 1; Eq. (12): 1 + z = a(t_o)/a(t_e)
How it is checked

frame conversions computed directly from 1 + z = λ_obs/λ_rest (science/oracle/redshift_oracle.py)

Redshift and frames — pass

How it is computed
Redshift, distance and observed wavelength (lya_app)

used in 5 · The same colour, stretched · status: verified · id SCI-RED-001

The quasar's frame — why the forest lies blueward of the quasar's own Lyα

The quasar at z_q emits its own Lyα at 1215.67 Å in its frame, observed at 1215.67 (1 + z_q) Å. Gas between us and the quasar (z_abs < z_q) absorbs at 1215.67 (1 + z_abs) Å observed, i.e. at 1215.67 (1 + z_abs)/(1 + z_q) < 1215.67 Å in the quasar's rest frame: the Lyα forest lies blueward of the quasar's Lyα emission.

λq=λobs1+zq=1215.67 A˚ 1+zabs1+zq<1215.67 A˚(zabs<zq)\lambda_{\rm q} = \frac{\lambda_{\rm obs}}{1+z_{\rm q}} = 1215.67\,\text{Å}\,\frac{1+z_{\rm abs}}{1+z_{\rm q}} < 1215.67\,\text{Å}\quad (z_{\rm abs} < z_{\rm q})
Convention
Cosmological redshift stretches every wavelength by the same factor
Assumptions
  • absorbers lie between us and the quasar
Valid for
z_abs < z_q
Simplifications
  • The quasar's emission line is drawn as a smooth bump on a flat continuum; its real shape and the proximity zone are not shown.
Sources
  • Meiksin 2009 — §II.A 'Resonance absorption lines', text preceding Eq. (15) (arXiv v3 p. 7); §I, p. 2 (absorption shortward of the QSO's Lyα)
  • Hogg 1999 — §3, Eq. (13): 1 + z₁₂ = (1 + z₂)/(1 + z₁)
  • Rauch 1998 — §1 Introduction, first paragraph (preprint p. 3), stated in words (secondary)
How it is checked

quasar-frame cases in science/oracle/redshift_oracle.py

Redshift and frames — pass

used in 5 · The same colour, stretched · status: verified · id SCI-RED-002

On a logarithmic ruler a stretch is a slide; small slides are velocities

Because the stretch multiplies every wavelength by (1 + z), it adds the same amount, ln(1 + z), to every ln λ. Small differences in ln λ are velocities, Δu = c Δln λ; this is the velocity coordinate u used from Beat 6 on, with λ_obs = λ0 (1 + z0) e^(u/c) (Velocity coordinate ↔ observed wavelength).

ln⁡λobs=ln⁡λrest+ln⁡(1+z),u=c ln⁡1+zabs1+z0\ln\lambda_{\rm obs} = \ln\lambda_{\rm rest} + \ln(1+z),\qquad u = c\,\ln\frac{1+z_{\rm abs}}{1+z_0}
Convention
Velocity coordinate ↔ observed wavelength (log-λ, kept once defined)
Valid for
exact (identity)
Sources
No literature source: checked numerically against an independent code.
How it is checked

log-shift cases in science/oracle/redshift_oracle.py

Redshift and frames — pass

used in 5 · The same colour, stretched · status: verified-numeric · id SCI-RED-003

Comoving distance along the light's path (Planck 2018)

The line-of-sight comoving distance to redshift z is χ(z) = (c/H0) ∫0^z dz'/E(z'), E = (Ωm(1+z)³ + ΩΛ)^½ for flat ΛCDM; with Planck 2018 parameters χ(3) ≈ 4384 Mpc/h. It places the gas and the travelling light along the drawn path in Beat 5, and gives the distance to us printed beside the volume (Beats 0, 1, 11, 12), where the beam is broken because the 20 Mpc/h sample is about 0.5 % of that distance.

χ(z)=cH0∫0zdz′E(z′)\chi(z) = \frac{c}{H_0}\int_0^z \frac{dz^{\prime}}{E(z^{\prime})}
Convention
Planck 2018 TT,TE,EE+lowE+lensing (Ωm = 0.3153, ΩΛ = 0.6847, h = 0.6736); radiation neglected (≲ 10⁻³ of E at z ≲ 4)
Assumptions
  • flat ΛCDM
Valid for
0 ≤ z ≤ 6 tested
Simplifications
  • The path is drawn to scale in comoving distance, so equal stretches do not cover equal lengths of path.
Sources
  • Hogg 1999 — §2 Eq. (4) (D_H = c/H0); §4 Eqs. (14)–(15) (E(z); D_C = D_H ∫ dz′/E)
  • Planck Collaboration 2020 — Table 2, column TT,TE,EE+lowE+lensing (arXiv v4 p. 16)
How it is checked

scipy quad + brentq (science/oracle/redshift_oracle.py)

Redshift and frames — pass

How it is computed
Redshift, distance and observed wavelength (lya_app)

used in 0 · Gas occupies real space, 1 · Same gas, different scale, 5 · The same colour, stretched, 11 · A forest emerges, 12 · Who ate this colour? · status: verified · id SCI-RED-004

The forest is written along the way

As the quasar's light travels, its wavelength measured by local comoving observers grows, λ_here = λ_emit (1 + z_q)/(1 + z_here). Each intervening hydrogen structure at z_abs absorbs the part of the spectrum that is at 1215.67 Å in its own frame — the part emitted at λ_emit = 1215.67 (1 + z_abs)/(1 + z_q) — and that shadow then stretches on with the light, reaching us at 1215.67 (1 + z_abs). Structures at different redshifts therefore cast shadows at different observed wavelengths, ordered by redshift, all blueward of the quasar's own Lyα at 1215.67 (1 + z_q): the Lyα forest.

λhere=λemit 1+zq1+zhere,λemit=1215.67 A˚ 1+zabs1+zq ⇒ λobs=1215.67 A˚ (1+zabs)\lambda_{\rm here} = \lambda_{\rm emit}\,\frac{1+z_{\rm q}}{1+z_{\rm here}},\qquad \lambda_{\rm emit} = 1215.67\,\text{Å}\,\frac{1+z_{\rm abs}}{1+z_{\rm q}}\ \Rightarrow\ \lambda_{\rm obs} = 1215.67\,\text{Å}\,(1+z_{\rm abs})
Convention
Cosmological redshift stretches every wavelength by the same factor; one redshift per structure (peculiar motion folded in, separated in Real space → velocity space map)
Assumptions
  • absorbers between us and the quasar; Lyα only
Valid for
z_abs < z_q
Simplifications
  • Lyβ and higher Lyman lines, the Lyman limit, and the atmospheric cut-off are not drawn.
Sources
  • Meiksin 2009 — §II.A 'Resonance absorption lines', text preceding Eq. (15) (arXiv v3 p. 7): absorption appears at observed wavelengths between λ_lu and λ_lu a(t)/a(t₀)
  • Hogg 1999 — §3, Eq. (13): 1 + z₁₂ = (1 + z₂)/(1 + z₁)
  • Rauch 1998 — §1 Introduction, first paragraph (preprint p. 3), stated in words (secondary)
How it is checked

forest cases in science/oracle/redshift_oracle.py: λ_emit, λ_here at z_abs = 1215.67 Å, λ_obs, ordering

Redshift and frames — pass

How it is computed
Redshift, distance and observed wavelength (lya_app)

used in 5 · The same colour, stretched · status: verified · id SCI-RED-005

Real space → velocity space map

Gas at comoving position x along the sightline, with line-of-sight peculiar velocity v_pec, absorbs at velocity coordinate u = aH(z) x_com + v_pec. Across a box of tens of Mpc/h the expansion rate is treated as constant.

u(x)=a(z)H(z) xcom+vpec(x)=100 E(z)1+z x [Mpc/h]  km s−1,E=Ωm(1+z)3+ΩΛu(x) = a(z)H(z)\,x_{\rm com} + v_{\rm pec}(x) = \frac{100\,E(z)}{1+z}\,x\,[{\rm Mpc}/h]\ \ {\rm km\,s^{-1}},\qquad E = \sqrt{\Omega_m(1+z)^3 + \Omega_\Lambda}
Convention
flat ΛCDM, radiation neglected; Planck 2018 (Ωm = 0.3153, ΩΛ = 0.6847, h = 0.6736) gives 114.19 km/s per Mpc/h at z = 3
Assumptions
Valid for
boxes ≲ 100 Mpc/h at z ≈ 2–4 (the linear map deviates from the exact distance–redshift map by 1.0% at 100 Mpc/h; Real space to velocity space)
Simplifications
  • Beat 7 shows velocities relative to the near edge of the stretch (an additive constant).
Sources
How it is checked

comoving-distance integration (science/oracle/mapping_oracle.py)

Real space to velocity space — pass

Closure against fake_spectra — pass (declared domain)

How it is computed
Real space to velocity space (lya_app)

used in 5 · The same colour, stretched, 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate, 12 · Who ate this colour? · status: verified · id SCI-MAP-001

Comoving vs physical coordinates and h-units

Positions are comoving (Mpc/h); a column density uses the proper path length Δx_prop = a·Δx_com/h (in cm); velocities are physical km/s; densities are proper cm⁻³. The velocity per comoving Mpc/h, aH(z)/h = 100E(z)/(1+z), does not depend on h.

Nj=nHI,j  Δxprop,Δxprop=a Δxcom[Mpc/h]h MpcN_j = n_{{\rm HI},j}\;\Delta x_{\rm prop},\quad \Delta x_{\rm prop} = \frac{a\,\Delta x_{\rm com}[{\rm Mpc}/h]}{h}\,{\rm Mpc}
Convention
as stated
Valid for
always
Sources
  • fake_spectra (Bird) — v2.2.4 sdist: unitsystem.py:7–17 (UnitLength_in_cm 3.085678e21 = 1 kpc/h, UnitMass_in_g 1.98892e43, UnitVelocity_in_cm_per_s 1e5); abstractsnapshot.py:102–112 (header units), 114–119 (HDF5 velocities × √a); spectra.py:182–187, 210 (rscale = UnitLength·a/h), 212–217 (velfac = rscale·H/3.085678e24); absorption.h:15–20, 40–46 (comoving kpc/h, physical km/s); absorption.cpp:234 (u = velfac·x + v_pec)
  • Springel 2005 (GADGET-2 user guide) — §6.1 items 2–3, p. 32: positions comoving h⁻¹ kpc; peculiar velocity v = u√a (§5.9 pp. 25–26: default units)
How it is checked

Real space to velocity space h-independence

Real space to velocity space — pass

Closure against fake_spectra — pass (declared domain)

How it is computed
Real space to velocity space (lya_app)

used in 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate · status: verified · id SCI-MAP-002

Peculiar-velocity sign convention

v_pec > 0 means the gas moves away from the observer (toward the quasar, +x); it lands at larger u, i.e. at a longer observed wavelength (redder). This is the same sign as the resonance condition Resonance in the atom's frame (Doppler condition).

vpec>0  ⇒  u↑  ⇒  λobs↑v_{\rm pec} > 0 \;\Rightarrow\; u \uparrow \;\Rightarrow\; \lambda_{\rm obs}\uparrow
Convention
as stated; used identically by the app, the independent checks and the toy generator
Valid for
always
Sources
How it is checked

Real space to velocity space sign test; Closure against fake_spectra case C7

Real space to velocity space — pass

Closure against fake_spectra — pass (declared domain)

How it is computed
Real space to velocity space (lya_app)

used in 2 · One atom meets one colour, 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate · status: verified · id SCI-MAP-003

Velocity coordinate ↔ observed wavelength

The velocity ruler is converted to observed wavelength with λ_obs = λ0(1+z0)e^(u/c) (the log-λ convention); to first order this equals λ0(1+z0)(1+u/c). Over 20 Mpc/h at z = 3 the two differ by 2.9e-05 in 1+z.

λobs=λ0 (1+z0) eu/c\lambda_{\rm obs} = \lambda_0\,(1+z_0)\,e^{u/c}
Convention
z0 is the redshift of the u = 0 point (the near end of the sightline)
Valid for
|u| ≪ c
Sources
  • Karaçaylı et al. 2024 — §3.2 'Quadratic estimator', Eq. (14) (arXiv v3 p. 6): v = c ln(λ/λ_Lyα), 'following the convention in cosmology' — the same convention, with this resource's zero point at λ0(1+z0)
  • Palanque-Delabrouille et al. 2013 — §4.1.1 (arXiv v2 p. 9): Δv = c Δλ/λ = c Δ[ln λ] — the differential form (secondary)
How it is checked

Real space to velocity space

Real space to velocity space — pass

How it is computed
Redshift, distance and observed wavelength (lya_app)

used in 5 · The same colour, stretched, 7 · Distance and observed colour are not the same coordinate, 11 · A forest emerges · status: verified · id SCI-MAP-004

Validity of the constant-H (linear) map

Treating aH as constant across the box deviates from the exact distance–redshift relation by 1e-04 per Mpc/h (0.2% over 20 Mpc/h, 3.9 km/s at z = 3; 1.0% at 100 Mpc/h).

Valid for
as measured in Real space to velocity space
Sources
No literature source: checked numerically against an independent code.
How it is checked

Real space to velocity space — pass

How it is computed
Real space to velocity space (lya_app)

used in 7 · Distance and observed colour are not the same coordinate · status: verified-numeric · id SCI-MAP-005

Velocity-gradient (Hubble) broadening of an extended absorber

Gas that is extended along the line of sight spreads its absorption in velocity: the Hubble flow across its depth, and any internal peculiar-velocity gradient, give its parts different line-of-sight velocities, and this bulk spread adds to the thermal motion of its atoms. For a Gaussian clump of width σ_x with a linear velocity field, the spread is a Gaussian of dispersion |aH + dv/dx|·σ_x, i.e. a Doppler parameter b_size = √2·|aH + dv/dx|·σ_x, which adds in quadrature to the thermal b; this expression is the resource's own algebra (a Gaussian velocity spread convolved with the thermal Gaussian), checked analytically and by One parcel, four separate variables — the sources give the physics, not the formula. In CDM collapse models the low-column-density forest clouds are cool but large, so this term can dominate their widths.

b2=bthermal2+bsize2,bsize=2 ∣aH+dvdx∣σxb^2 = b_{\rm thermal}^2 + b_{\rm size}^2,\qquad b_{\rm size} = \sqrt{2}\,\left|aH + \tfrac{dv}{dx}\right|\sigma_x
Convention
x comoving in Mpc/h, aH in km/s per Mpc/h (Real space → velocity space map); σ_x the clump's 1σ width along the beam
Assumptions
  • Gaussian density profile along the beam; a linear velocity field across it; one temperature
Valid for
smooth, single-stream flow across the clump (One velocity per sampling element (unresolved velocity structure is not represented)); a real absorber's flow need not be linear
Sources
  • Rauch 1998 — §3.3 (arXiv v1 p. 14): in CDM collapse models, low-column clouds are cool but large, 'so that the Hubble expansion dominates the line broadening'
  • Meiksin 2009 — §II.A, Eqs. (10)–(11): b = (2kT/m_a)^½; a kinematic component adds in quadrature
How it is checked

a Gaussian velocity spread convolved with a Gaussian thermal profile is a Gaussian whose b² is the sum; One parcel, four separate variables checks the drawn width against it

One parcel, four separate variables — pass

How it is computed
One parcel's line — its width and depth (lya_app) · The constructed "same shadow" (lya_app)

used in 6 · Where does one parcel land?, 13 · Same shadow · status: verified · id SCI-MAP-006

4 · Optical depth, transmission and observation

Why optical depths add while transmitted light multiplies, what saturation does, and what an instrument changes — the record, not the gas.

Lyα "absorption" in the IGM is resonant scattering out of the beam

After excitation to 2p the atom decays radiatively (A21 ≫ collisional and photo-excitation rates at IGM densities), re-emitting a Lyα photon in an essentially random direction. For a narrow beam from a point source this removes the photon from the line of sight; scattering of other photons into the beam is negligible. The beam's extinction therefore obeys the same e^−τ law as true absorption.

1s+γLyα→2p→1s+γ (random direction)1s + \gamma_{\rm Ly\alpha} \to 2p \to 1s + \gamma\ (\text{random direction})
Convention
Say "scattered out of our beam" in Beat 2 and again in Beat 5, where "shadow" is introduced; "removed"/"absorbed" elsewhere as shorthand.
Assumptions
  • point source; beam solid angle ≪ 4π; negligible re-scattering into the beam
Valid for
quasar sightlines through the diffuse IGM
Simplifications
  • Later beats say "absorbed" for brevity.
Sources
  • Meiksin 2009 — §I footnote 7; §II.A opening (features are scattering of resonance photons out of the line of sight)
  • Dijkstra 2014 — §4 Lyα Radiative Transfer Basics, opening paragraph; §5.2 (secondary)

used in 2 · One atom meets one colour · status: verified · id SCI-RES-001

Optical depth of one absorber

An absorber of neutral column N_HI has τ(Δv) = N σ_v φ(Δv) with σ_v = (πe²/m_e c) f λ0 = 1.3435×10⁻¹² cm² km/s, so ∫τ dv = N σ_v (independent of temperature) and the Doppler line-centre value is τ0 = 0.758 (N/10¹³)(10 km/s / b).

τ(Δv)=NHI σv ϕ(Δv),σv=πe2mecfλ0,τ0=Nσvπ b\tau(\Delta v) = N_{\rm HI}\,\sigma_v\,\phi(\Delta v),\quad \sigma_v = \frac{\pi e^2}{m_e c}f\lambda_0,\quad \tau_0 = \frac{N\sigma_v}{\sqrt\pi\,b}
Convention
velocity-space cross-section (cm² km/s); φ normalised to 1 in km/s
Assumptions
  • ground-state population only; stimulated emission negligible
Valid for
IGM
Sources
  • Rybicki & Lightman 1979 — §10.3 Einstein coefficients and oscillator strengths, p. 274
  • Meiksin 2009 — §II.A, Eqs. (14)–(19): τ = ∫nσ ds; τ0 = Nσλ/(√π b) (secondary)
How it is checked

science/oracle/tau_oracle.py

Optical depth — pass

How it is computed
One parcel's line — its width and depth (lya_app) · Optical depth — contributions add (lya_app)

used in 3 · Temperature becomes line width, 5 · The same colour, stretched, 6 · Where does one parcel land? · status: verified · id SCI-TAU-001

Line-of-sight accumulation of optical depth in velocity space

The optical depth at velocity u is the sum (integral) over all neutral hydrogen along the sightline of each element's profile centred on its own u(x): τ(u) = ∫ n_HI σ_v φ(u − u(x); b(x)) dx_prop. Discretised on cells it is Σ_j N_j σ_v φ(u − u_j; b_j).

τ(u)=∫nHI(x) σv ϕ(u−u(x); b(x)) dxprop ≈ ∑jNj σv ϕ(u−uj;bj)\tau(u) = \int n_{\rm HI}(x)\,\sigma_v\,\phi\bigl(u - u(x);\,b(x)\bigr)\,dx_{\rm prop}\ \approx\ \sum_j N_j\,\sigma_v\,\phi(u-u_j;b_j)
Convention
Real space → velocity space map, Comoving vs physical coordinates and h-units, Peculiar-velocity sign convention
Assumptions
Valid for
Bounded by Closure against fake_spectra. For single-stream gas with b ≥ 2Δu, the app's sum matches an exact reference to 2.7e-07 in F (worst case, with fake_spectra's constants) and fake_spectra 2.2.4 to max |ΔF| 4.9e-03 (dominated by fake_spectra's H-mass convention, fake_spectra conventions (the validation reference)); forest P1D within 0.33% (k ≤ 0.1 s/km). Outside this domain see One velocity per sampling element (unresolved velocity structure is not represented) (multi-stream) and Discretisation limits — pixel size, pixel averaging and the periodic box (resolution, box).
Simplifications
Sources
How it is checked

fake_spectra 2.2.4 _Particle_Interpolate on controlled skewers

Optical depth — pass

Closure against fake_spectra — pass (declared domain)

How it is computed
Optical depth — contributions add (lya_app) · Tracing a colour back to the gas (lya_app) · The toy sightline's spectrum (lya_app, with an external reference)

used in 8 · From parcels back to a continuous sightline, 9 · Ink adds, 11 · A forest emerges, 12 · Who ate this colour? · status: verified · id SCI-TAU-002

Discretisation limits — pixel size, pixel averaging and the periodic box

The cell sum is accurate only when (i) real-space pixels are finer than the narrowest line (aH·Δx ≤ b_min/2); (ii) one states whether a pixel's value is its centre τ, its mean τ or its mean F, since these differ for lines narrower than a pixel; and (iii) damping wings of strong absorbers are not cut by the periodic box.

aH Δx≤bmin⁡/2,⟨e−τ⟩pix≠e−⟨τ⟩pixaH\,\Delta x \le b_{\min}/2,\qquad \langle e^{-\tau}\rangle_{\rm pix} \ne e^{-\langle\tau\rangle_{\rm pix}}
Convention
the app's default = point sampling of τ at pixel centres on a uniform u grid; nearest periodic image per cell
Valid for
Closure against fake_spectra: lines narrower than a coarse pixel are wrong by up to 0.46 in F (EW up to +93%); oversampling to aH·Δx ≤ b/2 with exact pixel averaging brings this to 0.019. The pixel-mean flux and the flux of the pixel-mean τ differ by 7e-03 to 0.31 at 10 km/s pixels (the larger for narrow saturated lines). Both codes truncate wings at half a box: for an LLS/DLA in a 5 Mpc/h box the optical depth dropped against a true periodic sum reaches 8.8. None of these occur in the toy at its sampling.
Sources
  • fake_spectra (Bird) — singleabs.h:104-126 (centre vs trapezoid τ); absorption.cpp:250-278 (nearest image, ±½ box)
How it is checked

cases C6 (narrow lines), C2 (LLS/DLA wings) in science/validation/closure/

Optical depth — pass

Closure against fake_spectra — pass (declared domain)

How it is computed
Optical depth — contributions add (lya_app) · fake_spectra 2.2.4 — the external reference spectra (external package)

used in 8 · From parcels back to a continuous sightline, 9 · Ink adds · status: verified · id SCI-TAU-005

Why optical depths add

Optical depth is the path integral of the opacity; the opacity of independent absorbers (here: different gas elements at the same observed velocity) adds linearly because each atom's absorption is independent of the others and of the radiation field (no stimulated emission, no saturation of the level populations). Flux decrements do not add.

τ(u)=∑iτi(u)\tau(u) = \sum_i \tau_i(u)
Assumptions
  • linear (optically passive) medium; ground-state populations unaffected by the beam
Valid for
IGM / quasar beams
Sources
How it is checked

Optical depth blend case: τ of two lines = sum of individual τ

Optical depth — pass

Tracing a colour back to the gas — pass

How it is computed
Optical depth — contributions add (lya_app) · Tracing a colour back to the gas (lya_app)

used in 9 · Ink adds, 12 · Who ate this colour? · status: verified · id SCI-TAU-003

Transmitted flux (Beer–Lambert)

With no emission into the beam along the path, the transmitted fraction of the quasar's light is F = e^(−τ). F is the observed spectrum divided by the quasar's unabsorbed (continuum) spectrum.

F(u)=e−τ(u)F(u) = e^{-\tau(u)}
Convention
continuum-normalised flux, 0 ≤ F ≤ 1
Assumptions
  • no source term in the beam; continuum known
Valid for
quasar sightlines
Simplifications
  • The quasar continuum is divided out and never drawn (continuum fitting is out of scope).
Sources
How it is checked

Optical depth — pass

How it is computed
Transmission — light multiplies (lya_app)

used in 5 · The same colour, stretched, 10 · Light multiplies · status: verified · id SCI-FLUX-001

Light multiplies

The transmissions of absorbers in series multiply, because their optical depths add.

e−∑iτi=∏ie−τie^{-\sum_i \tau_i} = \prod_i e^{-\tau_i}
Assumptions
Valid for
always
Sources
No literature source: a mathematical identity, following from the claims it builds on.

used in 10 · Light multiplies, 12 · Who ate this colour? · status: verified · id SCI-FLUX-002

Instrument resolution acts on the observed flux

A spectrograph records the transmitted flux convolved with its line-spread function, approximated as a Gaussian (we quote its FWHM in km/s; P1D analyses often quote the rms width, e.g. Palanque-Delabrouille et al. 2013 Eq. 6). Resolution changes neither the gas nor its optical depth — only the recorded spectrum.

Fobs(u)=∫F(u′) LSF(u−u′) du′ (+ noise)F_{\rm obs}(u) = \int F(u^{\prime})\,{\rm LSF}(u-u^{\prime})\,du^{\prime}\ (+\ \text{noise})
Convention
Gaussian LSF; FWHM in km/s
Assumptions
  • Gaussian
  • velocity-independent LSF
Valid for
illustrative; real LSFs vary with wavelength and fibre
Simplifications
  • Gaussian LSF
Sources
How it is computed
The instrument — resolution, pixels and noise (lya_app)

used in 11 · A forest emerges · status: verified · id SCI-OBS-001

Noise model and signal-to-noise

The toy noise is white Gaussian with constant σ = 1/(S/N) per detector pixel in continuum-normalised flux; detector pixels are max(2.5 km/s, FWHM/3) wide. Any claim of "indistinguishable" is made only at a stated S/N and pixel size.

σF=(S/N)−1 per pixel\sigma_F = (S/N)^{-1}\ \text{per pixel}
Convention
S/N quoted per 2.5 km/s pixel unless stated
Assumptions
  • flux-independent noise; no sky
  • no continuum error
Valid for
illustrative
Simplifications
  • constant σ; Gaussian
Sources
No literature source: an idealised model chosen by this resource and declared here.
How it is checked

a declared, idealised noise model of this resource (white, Gaussian, flux-independent); the degeneracy check Same-shadow degeneracy uses it

Same-shadow degeneracy — pass

How it is computed
The instrument — resolution, pixels and noise (lya_app) · The constructed "same shadow" (lya_app)

used in 11 · A forest emerges, 13 · Same shadow · status: verified · id SCI-OBS-002

5 · The gas and how it is represented: a continuous sightline

The intergalactic gas itself — density, ionisation, temperature — and the teaching representations (volume, atoms, parcels, cells) that stand for it, with the toy data behind them.

One velocity per sampling element (unresolved velocity structure is not represented)

A real-space skewer gives each sampling element (pixel) one peculiar velocity and one temperature. If the gas inside one element has several velocities — in particle codes, several SPH particles or kernel contributions moving differently that overlap the same pixel — that sub-element velocity structure is not represented, and its absorption is placed at one velocity instead of being spread over several.

τ(u)=∑componentsnHI σv∣du/dx∣ (cold limit)vs one u per sampling element\tau(u) = \sum_{\rm components} \frac{n_{\rm HI}\,\sigma_v}{|du/dx|}\ \text{(cold limit)}\quad\text{vs one } u \text{ per sampling element}
Convention
Real space → velocity space map
Assumptions
  • one mass-weighted velocity and temperature per real-space sampling element
Valid for
Measured in Closure against fake_spectra on synthetic configurations (largest difference in F): gas-like 1D forest 4.9e-03 (inside the tolerance); two velocity components at one position (C3c) 0.089; a velocity fold (C4c) 0.021; a 1D Zel'dovich field evolved past shell crossing (C10, a deliberately exaggerated stress test, not a claim about the IGM) up to 0.2. Any reduced sightline representation of this kind loses the velocity and density structure inside one sampling element.
Simplifications
  • The toy (The toy Zel'dovich volume (synthetic teaching data)) stores one mass-weighted, smoothed velocity per pixel (the Zel'dovich field is just past first shell crossing in places, but the deposit averages over it), so the app and the toy data agree by construction; the limitation matters only when comparing with particle-based codes.
Sources
How it is checked

cases C3c, C4c, C10 in science/validation/closure/

Closure against fake_spectra — pass (declared domain)

How it is computed
The toy sightline's spectrum (lya_app, with an external reference)

used in 8 · From parcels back to a continuous sightline · status: verified · id SCI-TAU-004

How neutral the IGM is

At mean density at z ≈ 3 only ~10⁻⁵ of hydrogen is neutral: x_HI ≈ 0.74×10⁻⁵ at T0 = 10⁴ K and 0.62×10⁻⁵ at 1.3×10⁴ K, for Γ_HI ≈ 0.8×10⁻¹² s⁻¹ (Photoionisation equilibrium and the neutral-amount control, Mean hydrogen density of the universe at z = 3, IGM temperature at z ≈ 3, H I photoionisation rate at z ≈ 3). McQuinn (2016) §2.1 states the forest is sensitive to x_HI ~ 10⁻⁵. The forest is written by the rare neutral atom.

xHI=αAneΓHI≈5×10−6 Δ (T/1.3×104 K)−0.7 (ΓHI/10−12 s−1)−1x_{\rm HI} = \frac{\alpha_A n_e}{\Gamma_{\rm HI}} \approx 5\times10^{-6}\,\Delta\,(T/1.3\times10^4\,{\rm K})^{-0.7}\,(\Gamma_{\rm HI}/10^{-12}\,{\rm s^{-1}})^{-1}
Assumptions
Valid for
the quoted numbers hold at z ≈ 3 and mean density for those inputs; x_HI scales as Δ (1+z)³ T^−0.7 / Γ_HI (the equation), so denser or cooler gas is more neutral
Simplifications
  • The teaching copy says "about one in a hundred thousand" (computed: one in 135 000–160 000).
Sources
How it is checked

inputs and arithmetic recorded in the constants registry,

Toy sightline: line kernels, neutral fraction, and the mean-flux calibration — pass

How it is computed
How much of the hydrogen is neutral (lya_app)

used in 1 · Same gas, different scale, 6 · Where does one parcel land? · status: verified · id SCI-ION-002

The skewer interface (dataset-independent)

A skewer is delivered as real-space pixels along the line of sight: x (comoving Mpc/h, uniform Δx), n_HI (proper cm⁻³), T (K), v_pec (km/s, + away from observer), with metadata z, cosmology (Ωm, ΩΛ, h), Δx, and optionally the producing code's own τ(u) for validation. The app computes N_j = n_HI,j Δx_prop and τ by Line-of-sight accumulation of optical depth in velocity space.

Convention
Real space → velocity space map, Comoving vs physical coordinates and h-units, Peculiar-velocity sign convention
Assumptions
Valid for
Interface is dataset-independent (no field names or units from any one code). Optional metadata `mass_convention` and per-pixel `b_turb` let the generating code's own τ be reproduced (the app's import options for mass and b_turb; the validation mode of Closure against fake_spectra uses fake_spectra's H mass, 1.00794 m_p, and so its thermal widths).
Sources
No literature source: checked numerically against an independent code.
How it is checked

Closure against fake_spectra — pass (declared domain)

How it is computed
The toy sightline's spectrum (lya_app, with an external reference)

used in 8 · From parcels back to a continuous sightline, 11 · A forest emerges · status: verified · id SCI-SIM-001

fake_spectra conventions (the validation reference)

fake_spectra 2.2.4 conventions for units, velocity mapping, thermal broadening mass, profile evaluation, SPH kernel integration and line data are documented with source citations in science/notes/FAKE_SPECTRA_CONVENTIONS.md, and their effect on our real-space method is quantified in Closure against fake_spectra.

Convention
documented in the source repository (science/notes/FAKE_SPECTRA_CONVENTIONS.md)
Valid for
fake_spectra 2.2.4 (PyPI sdist sha256 4cc80e6c…eb84), MIT licence
Simplifications
  • Differences that matter for closure: thermal width uses 1.00794 m_p (b 0.37% small; up to |ΔF| 5.5e-03 in a forest); stored τ is the pixel-averaged τ when the pixel is at least b/2 wide, the pixel-centre value otherwise (Discretisation limits — pixel size, pixel averaging and the periodic box); wings truncated at half a box; tail floor τ < 10⁻⁷. Its total difference from an exact calculation is up to 1.8e-04 in F for isolated lines, almost all from its pixel quadrature (1.8e-04); its 7-node kernel quadrature alone contributes up to 2.2e-07 there, and dominates for broad cold kernels (1.5e-02 in F).
  • Lyα data in atom.dat: 1215.6701 Å, f 0.4164, Γ 6.265×10⁸ s⁻¹ — same as Lyα (H I 1s → 2p) transition data to within 6e-06 in F.
Sources
  • fake_spectra (Bird) — v2.2.4 sdist (PyPI 2025-08-11): H mass 1.00794 m_p, line_data.py:21 and absorption.h:4; pixel-centre value or pixel average (the b/2 rule), singleabs.h:104-126; wings stopped at half a box, absorption.cpp:248-278; tail floor τ < 10⁻⁷, spectra.py:135 and absorption.cpp:250-278; 7-node kernel quadrature, singleabs.h:8, 143-167; Lyα data, atom.dat:7. Full notes in the source repository (science/notes/FAKE_SPECTRA_CONVENTIONS.md)
  • Bird et al. 2015 — §2.6, Eqs. (6)–(7)
How it is checked

Closure against fake_spectra — pass (declared domain)

How it is computed
fake_spectra 2.2.4 — the external reference spectra (external package)

used in 8 · From parcels back to a continuous sightline · status: verified · id SCI-SIM-002

Exporting skewers from fake_spectra 2.2.4 — a velocity-convention compatibility note

During compatibility testing, the velocity returned through the tested export path — fake_spectra 2.2.4, Spectra.get_velocity on a Gadget-format snapshot — differed from the convention this resource requires (the proper peculiar velocity in km/s, Comoving vs physical coordinates and h-units). On a mock snapshot at a = 0.25 with known particle velocities, the returned value was 0.5 of the true peculiar velocity in the pixel of every moving particle, equal to √a — consistent with the √a unit conversion being applied once more than this convention needs. The resource therefore never takes skewer velocities from that path: it constructs the line-of-sight velocity independently from the stored quantities (the toy from its displacement field). One other observation on the same version (2.2.4), for anyone exporting skewers under NumPy 2: get_velocity fails unless its arrays, promoted to float64, are cast back to float32, and HDF5Snapshot.get_npart overflows (2³² × uint32) on headers that store particle counts as uint32, as Gadget does — the mock snapshots store them as int64.

vreturned/vpec=a on the tested path (mock snapshot);used here: vpec computed independentlyv_{\rm returned}/v_{\rm pec} = \sqrt{a}\ \text{on the tested path (mock snapshot)};\quad\text{used here: } v_{\rm pec}\ \text{computed independently}
Convention
v_pec in km/s, proper peculiar velocity (Comoving vs physical coordinates and h-units)
Valid for
fake_spectra 2.2.4 (PyPI sdist), the tested path only; the √a observation also holds on the upstream master branch as inspected on 2026-10-02. If an upstream issue is filed, this note will link it.
Sources
  • fake_spectra (Bird) — spectra.py:954 (second √a); abstractsnapshot.py:114-119 (first √a); under NumPy 2: spectra.py:952-955 (float64 promotion), abstractsnapshot.py:226-228 (get_npart)
How it is checked

mock snapshot: returned/true = 0.5000 at a = 0.25 (science/validation/closure/mocksnap.json)

Closure against fake_spectra — pass (declared domain)

How it is computed
fake_spectra 2.2.4 — the external reference spectra (external package)

not used by a scene · status: verified · id SCI-SIM-003

The toy Zel'dovich volume (synthetic teaching data)

The 3D gas, sightline and parcels in this resource come from a declared toy, not a simulation: a 256³ Zel'dovich displacement of a Gaussian field over 20 Mpc/h (P(k) ∝ k⁻² e^(−k²R²), R = 0.075 Mpc/h, ψ_rms = 0.36 Mpc/h), linear peculiar velocities v = f aHψ with f = Ω_m(z)^0.55 (no free velocity scaling), T = T0 Δ^(γ−1) with T0 = 1.3×10⁴ K, γ = 1.5, Planck 2018 cosmology, and n_HI from Photoionisation equilibrium and the neutral-amount control. Γ_HI is tuned so ⟨F⟩ = 0.68 on the chosen sightline (Mean transmitted flux at z ≈ 3); the tuned value (2.41×10⁻¹² s⁻¹) is 3.0× the measured 0.8×10⁻¹² s⁻¹, so the toy's absolute ionisation is not a physical claim. The skewer file also carries the generating code's own τ (exact Voigt) for the reference view.

Convention
the mapping conventions of Real space → velocity space map, Peculiar-velocity sign convention; LOS along the box x-axis; peculiar velocities computed from the displacement field (never from fake_spectra's get_velocity, Exporting skewers from fake_spectra 2.2.4 — a velocity-convention compatibility note)
Assumptions
  • Zel'dovich approximation; no hydrodynamics
  • no shocks
  • no pressure beyond a Gaussian filter
Valid for
teaching only
Simplifications
  • Rescaling τ to an observed mean flux is standard practice in simulation analysis (fake_spectra ships a mean-flux rescaler); here it is done through Γ_HI.
Sources
How it is checked

science/data/toy/zeldovich3d.py (seed 31415) — deterministic

Toy sightline: line kernels, neutral fraction, and the mean-flux calibration — pass

Toy spectrum against the code that generated it — pass

How it is computed
The toy sightline's spectrum (lya_app, with an external reference)

used in 0 · Gas occupies real space, 5 · The same colour, stretched, 8 · From parcels back to a continuous sightline, 11 · A forest emerges · status: verified · id SCI-SIM-TOY-001

Mean hydrogen density of the universe at z = 3

n̄_H(z) = (1−Y) Ω_b ρ_crit,0 (1+z)³ / m_H ≈ 1.21×10⁻⁵ cm⁻³ at z = 3 for Planck 2018 (Ω_b h² = 0.02237, Y_P = 0.2454) — about a dozen hydrogen nuclei per cubic metre; ~80 per m³ in a filament at 6.6× the mean.

nˉH=(1−Y) Ωb ρcrit,0 (1+z)3/mH\bar n_{\rm H} = (1-Y)\,\Omega_b\,\rho_{\rm crit,0}\,(1+z)^3/m_{\rm H}
Convention
ρ_crit,0 = 1.87834×10⁻²⁹ h² g cm⁻³
Valid for
the number holds at z = 3; it scales as (1+z)³ (the formula holds at any z)
Sources
How it is checked

science/data/toy/zeldovich3d.py computes n̄_H = 1.2126e-5 cm⁻³ (the toy's skewer.json nH_bar)

How it is computed
How much of the hydrogen is neutral (lya_app)

used in 0 · Gas occupies real space, 1 · Same gas, different scale · status: verified · id SCI-CTX-001

IGM temperature at z ≈ 3

Gas near mean density at z ≈ 3 has T0 ≈ 1–2×10⁴ K with a temperature–density slope γ ≈ 1.3–1.6.

T=T0 Δγ−1T = T_0\,\Delta^{\gamma-1}
Valid for
z ≈ 2–4
Sources
  • Becker et al. 2011 — Table 3: T0 = 11640 K (z = 2.79), 10980 K (z = 3.16), fiducial γ; 1.1–1.6×10⁴ K across assumed γ
  • McQuinn 2016 — §2.3: T0 = (10–20)×10³ K over 2 < z < 4; §2.1, Eq. (3): T = T0 Δ^(γ−1)

used in 3 · Temperature becomes line width · status: verified · id SCI-CTX-002

Mean transmitted flux at z ≈ 3

At z ≈ 3 the mean Lyα-forest transmission is ≈ 0.68 (τ_eff ≈ 0.39; Becker et al. 2013 Table 2 bins bracket z = 3 at 0.697 and 0.667).

⟨F⟩=e−τeff\langle F\rangle = e^{-\tau_{\rm eff}}
Valid for
z ≈ 3
Sources
  • Becker et al. 2013 — Table 2 bins (0.697 at z = 2.95, 0.667 at z = 3.05); Eq. (5) fit gives 0.669 at z = 3
How it is checked

Toy sightline: line kernels, neutral fraction, and the mean-flux calibration — pass

used in 11 · A forest emerges · status: verified · id SCI-CTX-003

H I photoionisation rate at z ≈ 3

Γ_HI ≈ 0.8×10⁻¹² s⁻¹ at z ≈ 3 (Becker & Bolton 2013; ±0.13 dex).

Valid for
z ≈ 2–4
Sources
  • Becker & Bolton 2013 — Table 1: log(Γ/10⁻¹² s⁻¹) = −0.066 (z = 2.80), −0.103 (z = 3.20)

used in 1 · Same gas, different scale, 11 · A forest emerges · status: verified · id SCI-CTX-004

Velocity scales along the sightline

At z = 3 one comoving Mpc/h corresponds to ≈ 114.2 km/s (Planck 2018), and peculiar velocities of tens of km/s (toy rms 40 km/s per component) shift gas by a few tenths of a Mpc/h in the spectrum — up to ~1 Mpc/h in infall regions.

Convention
Real space → velocity space map
Valid for
toy values; typical-scale statements only
Sources
No literature source: a calculation from the cited inputs.
How it is checked

Real space to velocity space for H; generator log for the toy rms

Real space to velocity space — pass

used in 0 · Gas occupies real space · status: verified-numeric · id SCI-CTX-005

Column-density classes

Lyα-forest lines have N_HI ≲ 10¹⁷ cm⁻²; Lyman-limit systems N ≳ 1.59×10¹⁷ (τ at 912 Å ≥ 1, σ_912 = 6.304×10⁻¹⁸ cm²; surveys also use 10¹⁷ or 10^17.2); damped Lyα systems N ≥ 2×10²⁰ cm⁻².

Valid for
conventional definitions
Sources

used in 10 · Light multiplies · status: verified · id SCI-CTX-006

Parcel sizes and atom counts

A ~100 kpc (proper) parcel at ~5× mean density contains ~10⁶⁶ hydrogen nuclei; a cubic metre of filament gas holds ~10² nuclei, almost all ionised.

NH≈43π(50 kpc)3×5 nˉH≈1066N_{\rm H} \approx \tfrac{4}{3}\pi (50\,{\rm kpc})^3 \times 5\,\bar n_{\rm H} \approx 10^{66}
Convention
proper volumes
Assumptions
Valid for
order of magnitude
Sources
No literature source: a calculation from the cited inputs.

used in 1 · Same gas, different scale · status: verified · id SCI-CTX-007

Representative atoms are a cartoon sample

The atoms drawn when zooming into a parcel are a few hundred representative neutral atoms, not a count and not simulation particles; their motions are straight-line cartoons slowed ~10¹³×, with velocity components drawn from the correct Maxwellian.

Valid for
always declared on screen
Simplifications
  • only neutral atoms drawn; ~10⁵ ionised nuclei per neutral atom omitted
Sources
A teaching representation chosen by this resource — described here so it is not mistaken for physics.
How it is computed
Representative atoms (teaching visual)

used in 1 · Same gas, different scale, 3 · Temperature becomes line width, 6 · Where does one parcel land? · status: verified · id SCI-REP-001

The 3D slab is a teaching volume; only gas on the beam absorbs

The slab is a 20 × 8 × 8 Mpc/h toy volume (The toy Zel'dovich volume (synthetic teaching data); 7.97 Mpc/h across the beam) drawn as stacked translucent slices; gas near the beam is darkened (focus rule) because only gas on the line of sight contributes to τ.

Valid for
always
Simplifications
  • slices are downsampled 2× from the toy grid; darkness ∝ log density
  • the drawing fades toward the sample's edges (6 % of its length, 20 % across): a drawing convention — the sample ends, the gas does not
Sources
A teaching representation chosen by this resource — described here so it is not mistaken for physics.
How it is computed
The 3D volume (teaching visual)

used in 0 · Gas occupies real space, 8 · From parcels back to a continuous sightline · status: verified · id SCI-REP-002

Parcels are a teaching decomposition of a continuous sightline

The three parcels a, b, c summarise three n_HI peak regions of the real toy sightline (HI-weighted position, width, column, temperature, velocity and gradient). They are not discrete clouds; Beat 8 dissolves them back into cells. A parcel has a size along the beam, so the velocity spread across it widens its line too (the physics is Velocity-gradient (Hubble) broadening of an extended absorber). Beat 6 isolates the declared terms by setting the parcel's own peculiar-velocity gradient to zero (dv/dx = 0 there), so its size term is the Hubble flow alone, b_size = √2·aH·σ_x; for the parcel it starts from, b_size = 23.8 km/s against a thermal 21.4 km/s, so the drawn line is 32.0 km/s wide (b) — Beat 6's figure names both parts, and "show the physics" prints each and the drawn line's own width. The same toy parcel with its own gradient (-70 km/s per Mpc/h: infall, partly cancelling the Hubble flow), as Beats 7–8 use it, has b_size = 9.1 km/s and b = 23.3 km/s.

b2≃bthermal2+bsize2,bsize=2 ∣aH+dvdx∣σxb^2 \simeq b_{\rm thermal}^2 + b_{\rm size}^2,\qquad b_{\rm size} = \sqrt{2}\,\left|aH + \tfrac{dv}{dx}\right|\sigma_x
Convention
computed once in science/data/toy/zeldovich3d.py (decompose)
Assumptions
  • Gaussian thermal and spatial profiles (the damping wings aside)
Valid for
always declared
Simplifications
  • Gaussian parcel shape
Sources
A teaching representation chosen by this resource — described here so it is not mistaken for physics.
How it is checked

Beat 8 reports the largest flux difference between the 3-parcel and full calculations; One parcel, four separate variables checks the width Beat 6 prints

One parcel, four separate variables — pass

How it is computed
One parcel's line — its width and depth (lya_app) · Parcels and clouds (teaching visual)

used in 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate, 8 · From parcels back to a continuous sightline · status: verified · id SCI-REP-003

Natural width is magnified wherever it is drawn

The natural (Lorentzian) width is 0.006 km/s; wherever it must be seen it is magnified and labelled (×500 resonance window in Beat 2, ×1200 inset in Beat 4, ×1–1000 control in the micro view).

Valid for
always labelled
Sources
A teaching representation chosen by this resource — described here so it is not mistaken for physics.
How it is computed
Magnified natural width (teaching visual)

used in 2 · One atom meets one colour, 4 · The atom's lifetime leaves faint, far wings · status: verified · id SCI-REP-004

Ink is optical depth, and the ink rendering performs Beer–Lambert

"Ink" has one meaning throughout: a contribution to optical depth τ, laid in velocity space (Beats 6–13); neutral atoms are drawn as dark dots and motions as arrows, never called ink. The ribbon is rendered by compositing ink layers of opacity 1 − e^(−τᵢ) over a backlight C₀, which yields the transmitted fraction (C − C_ink)/(C₀ − C_ink) = Π e^(−τᵢ) = e^(−Στᵢ): the picture performs the rule it teaches (checked pixel by pixel, The drawing performs Beer–Lambert).

C←C (1−αi)+Cink αi,  αi=1−e−τi ⇒ C−CinkC0−Cink=∏ie−τiC \leftarrow C\,(1-\alpha_i) + C_{\rm ink}\,\alpha_i,\ \ \alpha_i = 1 - e^{-\tau_i}\ \Rightarrow\ \dfrac{C - C_{\rm ink}}{C_0 - C_{\rm ink}} = \prod_i e^{-\tau_i}
Assumptions
  • ink colour ≈ black
Valid for
exact for black ink
Sources
A teaching representation chosen by this resource — described here so it is not mistaken for physics.
How it is checked

The drawing performs Beer–Lambert — pass

How it is computed
Transmission — light multiplies (lya_app) · Ink and its brightness (teaching visual)

used in 6 · Where does one parcel land?, 9 · Ink adds, 10 · Light multiplies · status: verified · id SCI-REP-005

Four representations of the same gas, never mixed silently

The resource draws the same gas in distinct representations, each labelled where it first appears: (1) the continuous gas field (density, Beats 0, 7–8, 11–12); (2) representative atoms, not to scale (Beats 1–4, and inside Beat 6's parcel); (3) teaching parcels — a summary of one stretch of the sightline (Beats 6–8; Beat 13's "clouds" are drawn the same way); (4) cells — the sightline sampled finely (Beats 8–12). Simulation particles are never drawn.

Valid for
representation rule
Sources
A teaching representation chosen by this resource — described here so it is not mistaken for physics.

used in 1 · Same gas, different scale · status: verified · id SCI-REP-006

The stretching ribbon and the written shadows (Beat 5 representation)

In Beat 5 the quasar's light is a ribbon carrying its own labels (the wavelength each part left with), laid against a ruler of wavelength measured where the light is now, starting at 0 Å, so the stretch by (1 + z_q)/(1 + z_here) is drawn exactly (anchored at zero, every interval scaled). Lyα of the gas being passed is a fixed mark at 1215.67 Å. The gas along the line of sight is a schematic continuous field, not a set of separate clouds: a seeded Gaussian random field in ln Δ (correlation length ~7 Mpc/h comoving, σ = 0.9; far coarser than real structure, chosen to be visible on a 4.5 Gpc/h path), quieter near the quasar, with three density peaks set exactly for the narration (the selected one, placed where the toy stretch's deepest absorption falls on the forest's own wavelength scale, so its shadow has the same wavelength in Beats 5, 11 and 12; and z = 2.90, 2.81). Every cell writes at its own Lyα with strength ∝ Δ² (photoionisation equilibrium, Photoionisation equilibrium and the neutral-amount control) × ((1+z)/4)^1.6, normalised so ⟨F⟩ = 0.74 over z = 2–3; the forest therefore thins toward low z. Density peaks (Δ > 2.2, with their surroundings Δ > 1.3) are the "structures" the narration names and the reader can select. What each cell writes is drawn ~60× wider than a real forest line (6 Å Doppler width when written, versus ~0.1 Å for b = 25 km/s); shadows combine by Beer–Lambert. The close-up of the mark is magnified ~20×. The surface spectrum is continuum-normalised; the quasar's Lyα emission (a schematic bump) is drawn only with "show the physics". The light packet's drawn wavelength is proportional to its true stretch. Path positions are comoving distances (Comoving distance along the light's path (Planck 2018)).

Convention
Cosmological redshift stretches every wavelength by the same factor, The forest is written along the way
Valid for
representation rule
Simplifications
  • No colours are painted on the ribbon; ultraviolet and visible light are not distinguished.
  • The close-up keeps a pencil trail of the drift of the quasar's own Lyα (and a cinnabar one for the selected shadow) from the Lyα mark — a record of motion, drawn only while it fits the close-up.
Sources
A teaching representation chosen by this resource — described here so it is not mistaken for physics.
How it is checked

Redshift and frames — pass

How it is computed
The stretching ribbon (teaching visual)

used in 5 · The same colour, stretched · status: verified · id SCI-REP-007

6 · Inference: reading the forest backwards

Tracing a colour back to the gas, and why different gas can leave the same shadow — what breaks the tie, and where the inverse problem stays open.

The "same shadow" configurations

A constructed degeneracy example, not a model of typical forest absorbers: (i) one unusually hot, idealised compact gas clump (T = 4.5×10⁴ K, Hubble width 2.23 km/s — about 7 kpc proper, far more compact than typical forest gas — N = 2.5×10¹⁴, at rest); (ii) two cold clouds (T = 10⁴ K, Hubble width 12.8 km/s each, 78 km/s apart in Hubble flow, 9.75×10¹³ each, approaching at ±24.9 km/s); (iii) one cold cloud stretched along the line (T = 10⁴ K, Hubble width 11.15 km/s, N = 2.5×10¹⁴) whose smooth extra velocity across 1σ (5.988 km/s) is computed from (i), not tuned: it is the value that gives (iii) the same total Doppler parameter as (i), b_tot² = 2kT/m_H + 2σ_u² = (27.431 km/s)², with the same column. At S/N = 20 per 2.5 km/s pixel with perfect resolution the three Lyα lines are not significantly distinguishable (Δχ² ≈ 2.8 for the blend over 121 pixels); the blend shows by S/N = 50 (Δχ² ≈ 17) and grows as (S/N)², while (iii) stays at Δχ² ≤ 0.002 at every S/N up to 200, in Lyα and in Lyβ. In this constructed model (i) and (iii) are degenerate in every H I Lyman-series line: for a Gaussian clump with a smooth (linear) velocity field the H I profile is a Voigt profile fixed by b_tot and N alone. These are two different kinds of degeneracy. The blend (ii) is measurement-limited: higher S/N, resolution or wavelength coverage can break it. Heat against smooth motion, (i) against (iii), is information-limited for hydrogen: every H I line depends only on the line-of-sight distribution of velocity and optical depth, in which thermal and smooth bulk broadening of one species are the same Gaussian, so another H I line gives no independent distinction — a line of a different mass does (What breaks the degeneracy). The blend hides because its two components overlap inside the saturated Lyα core, where the transmitted flux is near zero however the optical depth is split (Saturation and the curve of growth); Lyβ, with a smaller f λ, is not saturated there and separates it (What breaks the degeneracy). What remains of (iii)'s difference is numerical: the clumps are cut at ±5σ (exact-Voigt path, max |ΔF| 4e-06), and the app adds its Voigt approximation's own error (max |ΔF| 4e-05; Numerical Voigt implementation in the app).

Δχ2=∑k(Fk(n)−Fk(i))2/σF2\Delta\chi^2 = \sum_k \bigl(F^{(n)}_k - F^{(i)}_k\bigr)^2/\sigma_F^2
btot2=2kTmH+2σu2,σu=su+δv,δv(iii)=(su(i))2+12(bT(i) 2−bT(iii) 2)−su(iii)b_{\rm tot}^2 = \frac{2kT}{m_{\rm H}} + 2\sigma_u^2,\quad \sigma_u = s_u + \delta v,\qquad \delta v^{\rm (iii)} = \sqrt{\bigl(s_u^{\rm (i)}\bigr)^2 + \tfrac12\bigl(b_T^{\rm (i)\,2} - b_T^{\rm (iii)\,2}\bigr)} - s_u^{\rm (iii)}
Convention
noise-free Δχ² against configuration (i), σ_F = 1/(S/N) per 2.5 km/s pixel; configurations defined in velocity space (single source science/validation/degeneracy/configs.json, written by science/tools/same_shadow_configs.py), so they do not depend on the cosmology
Assumptions
Valid for
(ii) against (i) — only at the stated S/N and resolution; (i) against (iii) — any S/N and resolution, in H I absorption alone (every Lyman-series line), for this constructed model's Gaussian clumps with smooth (linear) velocity fields
Simplifications
  • (i) and (ii) are chosen by hand; (iii) is computed from (i) by the matching condition
  • idealised Gaussian clumps, not typical forest absorbers
Sources
  • Rauch et al. 1996 — §3, Eq. (1) (arXiv p. 5): b_i² = b_b² + 2kT/m_i — a bulk width common to all species and a thermal width scaling as m^(−½); the paper separates temperature from bulk motion with ions of different mass — with lines of one mass, Eq. (1) cannot separate T from b_b (our inference)
  • Meiksin 2009 — §II.A, Eqs. (10)–(11): b = (2kT/m_a)^½; a kinematic component adds in quadrature
How it is checked

independent recomputation with wofz (science/oracle/degeneracy_oracle.py)

Same-shadow degeneracy — pass

How it is computed
The constructed "same shadow" (lya_app)

used in 13 · Same shadow · status: verified · id SCI-DEG-001

What breaks the degeneracy

Thermal width scales as m^(−½) while bulk motion broadens all species alike, so a heavier ion's line in the same gas separates heat from motion (Si IV: m ≈ 28 m_H). Lyβ (f λ 6.24× smaller) saturates differently and can, in some cases, expose columns hidden in saturated Lyα.

b2=2kTm+bbulk2,(fλ)Lyβ(fλ)Lyα=0.079142×1025.7220.4164×1215.67=0.160b^2 = \frac{2kT}{m} + b^2_{\rm bulk},\qquad \frac{(f\lambda)_{\rm Ly\beta}}{(f\lambda)_{\rm Ly\alpha}} = \frac{0.079142\times1025.722}{0.4164\times1215.67} = 0.160
Assumptions
  • the gas holds silicon in this ion — the tie-breaker applies only if it does
  • the metal line traces the same gas (single phase)
  • a common non-thermal (bulk) width shared by both species — an assumption that need not hold everywhere (Rauch et al. 1996), and a stronger one for H I with a metal ion than for two metal ions
  • illustrative metal column
Valid for
Illustrative. Si IV separates heat from motion only where silicon is present in the same gas. Lyβ helps only in some cases: in real spectra it lies inside the Lyα forest of lower-redshift gas, so blending and contamination must be modelled; its smaller f λ makes it useful mainly where Lyα saturates; and it needs wavelength coverage down to 1026 Å in the absorber's frame. Like every H I line, Lyβ cannot separate heat from smooth motion (The "same shadow" configurations).
Simplifications
  • the Si IV column is chosen for visibility
  • Lyβ is drawn as a clean line, without the lower-redshift Lyα forest it lies in
Sources
  • Rauch et al. 1996 — §3, Eq. (1) (arXiv p. 5): b_i² = b_b² + 2kT/m_i — two ions of different mass (C IV and Si IV) separate temperature from bulk motion
  • NIST ASD (Kramida et al. 2024) — Si IV lines output 1390–1405 Å (ASD v5.12, accessed 2026-10-04): 3s–3p ²P°3/2 at 1393.76 Å (vacuum), f = 0.513
  • NIST isotopic compositions (silicon) — Silicon entry: ²⁸Si 27.9769 u, 92.2 % of silicon
  • NIST ASD (Kramida et al. 2024) — H I lines output 1025–1026 Å (ASD v5.12, accessed 2026-10-03): 1s–3p 2P°3/2 and 2P°1/2, Ritz λ 1025.72182505 / 1025.72296565 Å, f 0.052761 / 0.026380 (multiplet value 0.079142; f-weighted centre 1025.72221 Å)
  • Wiese & Fuhr 2009 — Table 6, entry No. 2 (1s–3p), p. 578: f = 0.079142
  • Rauch 1998 — §2.4 (arXiv v1 p. 7): 'For stronger Lyα lines the higher order Lyman lines can provide additional constraints when fitted simultaneously'; §3.4 (p. 15): for saturated, blended lines 'simultaneous fits to the higher order Lyman lines (which are less saturated owing to their lower oscillator strengths) can only help to some degree'
  • Griesmann & Kling 2000 — Table 1 'Resonance transitions' (arXiv p. 5): Si IV 3s ²S1/2 – 3p ²P3/2 at 1393.76018 Å (vacuum, laboratory Fourier-transform spectroscopy) (secondary)
How it is computed
The constructed "same shadow" (lya_app)

used in 13 · Same shadow · status: verified · id SCI-DEG-002

7 · Validation and numerical accuracy

Each check is an automated test in the source repository; its measured values are written by the test, never typed. Status: verified — claim, equation and source location checked; the numerical check passes where one applies; verified-numeric — the numerical check passes; it is an identity or a calculation, so no literature location applies.

Voigt function accuracy pass

The app's Voigt–Hjerting function H(a, x) is compared with SciPy's Faddeeva function (wofz) on a 42 × 321 grid covering a from 10⁻⁶ to 1 and x from 0 to 2×10⁴.

max relative error 8.5e-05 over 13482 grid points (physical a (1e-4 ≤ a ≤ 6.1e-3), core |x| ≤ 4: 5.0e-05; physical a, wings 4 < |x| ≤ 2e4: 4.9e-05; tiny a (1e-6 ≤ a < 1e-4), all x: 5.0e-05; teaching exaggeration (6.1e-3 < a ≤ 1), all x: 8.5e-05)

Criterion: max relative error < 1e-3 against wofz in every tested region of (a, x)

id VAL-VOIGT-001

Measured values: report (JSON)

Real space to velocity space pass

The map u = aH(z)·x + v_pec is checked against an independent formula, for independence of h, for its sign convention, and against the exact distance–redshift relation (how well a constant H holds across the box); the velocity-to-wavelength convention is checked too.

5/5 checks; linear map vs exact over 20 Mpc/h: 2.0e-03

Criterion: exact identities to 1e-12; linear map within 1 % over ≤ 20 Mpc/h

id VAL-MAP-001

Measured values: report (JSON)

Optical depth pass

Optical depth of single, blended, saturated, damped, under-resolved and periodic lines is compared with an independent exact-Voigt calculation; the line-centre optical depth τ₀ and the integral ∫τ du are checked against their analytic values.

8/8 checks; point sampling max rel τ 5.0e-05, max |ΔF| 1.8e-05

Criterion: point sampling: rel τ < 1e-3 and |ΔF| < 1e-4

id VAL-TAU-001

Measured values: report (JSON)

Toy sightline: line kernels, neutral fraction, and the mean-flux calibration pass

On the toy sightline, the spectrum as drawn (Gaussian thermal kernels) is compared with the same sum using full Voigt kernels. The neutral fraction at mean density is computed from the measured ultraviolet background and checked to be about one in 10⁵. The mean transmitted flux is a calibration, not a test: the toy's ultraviolet background is tuned so that its mean flux matches the observed value, which is why the toy's ultraviolet is about three times the measured one.

3/3 checks; Gauss vs Voigt max |ΔF| 5.7e-04; neutral fraction x_HI = 6.16e-06 at mean density and T0 = 13000 K, for the measured Γ_HI = 0.8e-12 s⁻¹; ⟨F⟩ = 0.680, a calibration (Γ_HI tuned to 3.0× measured to meet it)

Criterion: max |ΔF| < 1e-3 between Gaussian and Voigt kernels; x_HI at mean density between 5×10⁻⁶ and 8×10⁻⁶; ⟨F⟩ = 0.68 ± 0.01 (a calibration: the toy's Γ_HI is tuned to meet it)

id VAL-TOY-001

Measured values: report (JSON)

Python reproductions pass

Each calculation this resource implements has a short, independent Python reproduction (NumPy and SciPy only) of the equation it publishes. Every reproduction is run on its own: its printed output must equal the expected output formatted from the app's own values at the same inputs, and every full-precision value must agree with the app's within the tolerance it states.

10/10 reproductions: each snippet, run alone, prints its expected output (formatted from the app's own values); 74/74 full-precision values agree with the app within their stated tolerances (largest relative deviation 1e-04)

Criterion: every snippet's output equals its expected output; every script exits 0; every value within its stated tolerance

id VAL-PY-001

Measured values: report (JSON)

Same-shadow degeneracy pass

The third “same shadow” configuration is checked to follow from its matching condition (the same total Doppler parameter and column as the first); the Δχ² separating the configurations is recomputed independently with exact Voigt profiles, in Lyα and Lyβ, at signal-to-noise 20, 50, 100 and 200.

13/13 checks; (iii) constructed by the matching rule (dv = 5.9883 km/s, b_tot = 27.4306 km/s for both); Lyα Δχ²(ii) at S/N 20 / 50 / 100 / 200: 2.79 / 17.4 / 70 / 279, Lyβ 103 at S/N 20; Δχ²(iii) ≤ 9e-06 exact-Voigt, ≤ 2e-03 as the app draws it (S/N ≤ 200, Lyα and Lyβ); max |ΔF| vs an independent calculation 3.6e-05

Criterion: (iii) equals the matching rule to 1e-9 km/s and b_tot(iii) = b_tot(i) to 1e-12; Δχ²(ii) browser vs oracle within 5 % at every S/N in Lyα and Lyβ; Δχ²(iii) < 1e-4 on the exact-Voigt path; the app's max |F(iii) − F(i)| < 1e-4 (its Voigt approximation)

id VAL-DEG-001

Measured values: report (JSON)

Redshift and frames pass

Comoving distance χ(z) and its inverse are compared with SciPy integration (Planck 2018 parameters); the frame conversions λ_obs = λ_rest(1 + z) and their reverses, the quasar frame, and the correspondence of log-wavelength and velocity are checked.

6/6 checks; χ(z) vs scipy max rel 8e-11; z(χ) inverse 2e-09; frame conversions exact to 1e-16; Lyα at z = 3 → 4862.68 Å

Criterion: χ rel < 1e-7; |Δz| < 1e-8; identities to 1e-14

id VAL-RED-001

Measured values: report (JSON)

Toy spectrum against the code that generated it pass

The app's toy spectrum, as drawn, is compared with the exact-Voigt optical depth computed by the independent code that produced the toy data.

the app (Gaussian kernels, as drawn) vs the generating code's exact-Voigt τ: max |ΔF| 5.8e-04; the app with Voigt kernels 3.2e-05; ⟨F⟩ 0.6804 vs 0.6804

Criterion: as drawn max |ΔF| < 1e-3; Voigt kernels < 1e-4; ⟨F⟩ to 1e-3

id VAL-TOY-002

Measured values: report (JSON)

Tracing a colour back to the gas pass

In “Who ate this colour?”, the per-cell contributions traced back from a colour are checked to sum to the spectrum's own optical depth at every pixel; the share the display leaves untraced is recorded.

Σ of traced contributions vs the spectrum's τ over 2048 pixels: max relative difference 5e-15; τ in cells under the 1% display cut (kept in the ledger's “rest”): median 3.2%, max 24.8% of a pixel's τ

Criterion: max relative |Σ τ_j − τ| < 1e-12 at every pixel; thresholds read from the beat manifest

id VAL-INV-001

Measured values: report (JSON)

One parcel, four separate variables pass

In “Where does one parcel land?”, place, own motion, temperature and neutral amount are each changed alone across their ranges and checked to change only what they should — where the optical depth lands, how wide it is, or how deep — and the comparison the scene prints is checked against a fine-grid calculation. The width and peak it prints are checked to be those of the line it draws: heat alone and the parcel's size (the Hubble flow across it) add in quadrature.

91/91 checks over 18 one-variable changes (cells and τ, fine grid and the scene's grid): place and motion move the centre by aH·Δx and Δv (max error 4e-07 km/s) at fixed FWHM and area; temperature changes FWHM² by 4 ln2 Δ(b²) (max rel. error 1e-04) at fixed centre and area; amount scales τ exactly (max rel. 2e-15) at fixed centre and FWHM. The width the scene prints is the drawn line's: heat alone b = 21.4 km/s and the parcel's size 23.8 km/s add in quadrature to the drawn 32.0 km/s (max rel. error 2e-04; peak 3e-04). Flux is not linear (recorded): N_HI ×2 gives τ area ×2 but equivalent width ×1.19 (τ₀ = 4.4); T ×2 at fixed N_HI changes it ×1.13

Criterion: cells: only the intended property changes (relative 1e-12); τ on the fine grid: centre within 1e-3 km/s of aH·Δx or Δv, FWHM and area within 1e-3 (relative) when they should not change, FWHM² increment within 1e-2 of 4 ln2 Δ(b²), τ ratio = N ratio within 1e-9 where τ > 10⁻⁴ of its peak; the drawn line's b within 2e-3 of √(b_thermal² + b_size²) and its peak within 2e-3 of N σ_v/(√π b); the scene's printed comparison, width and peak correct to their printed precision

id VAL-ORTH-001

Measured values: report (JSON)

The drawing performs Beer–Lambert pass

In “Light multiplies”, each pixel of the drawn light table is read back from the canvas: its transmitted fraction equals e^−τ at that colour, within 8-bit quantisation.

Beat 10 ribbon read back from the canvas: max |T − e^(−τ)| 2.1e-03 over 1000 columns (8-bit bound 2.1e-03); mean 9.0e-04

Criterion: max |T − e^(−τ)| ≤ the 8-bit quantisation bound 0.5/(light − ink)

id VAL-REND-001

Measured values: report (JSON)

Closure against fake_spectra pass (declared domain)

The app's real-space method is compared with fake_spectra 2.2.4 on controlled skewers; differences are decomposed by source, and the domain in which the app's method applies is declared.

in domain (one velocity per sampling element, b ≥ 2Δu; 25 cases): max |ΔF| 4.9e-03, |Δ⟨F⟩| 2.9e-04, |ΔEW/EW| 2.8e-03, forest P1D 0.17 % (k ≤ 0.05 s/km), 0.32 % (k ≤ 0.1); C8b compared with the exact reference (fake_spectra's own quadrature error exceeds half the tolerance). Outside the domain, stated not tolerated (max |ΔF| per test case, by case name): several velocities inside one real-space sampling element (unresolved velocity structure; C10 is a deliberately exaggerated 1D shell-crossing stress test): C10s0 0.14, C10s1 0.059, C10s2 0.13, C10s3 0.2, C3c 0.089, C4c 0.021; lines narrower than two velocity pixels (b < 2Δu); fixable by oversampling the skewer: C6a 0.45, C6b 0.21, C6c 0.049, C6d 0.46. The app's code vs its independent Python port: 6e-15 in τ

Criterion: declared domain (one velocity per sampling element, b ≥ 2Δu): max |ΔF| ≤ 0.01, |Δ⟨F⟩| ≤ 1e-3, |ΔEW/EW| ≤ 1 %, P1D within 1 % (k ≤ 0.05 s/km) and 2 % (k ≤ 0.1); where fake_spectra's own error exceeds half the tolerance, compare with the exact reference; out-of-domain cases listed with measured values

id VAL-CLOSURE-FS

Measured values: science/validation/closure/metrics.json in the source repository

8 · How each quantity is computed

Every scientific number, curve and profile the app shows has a computation record: who computed it, from which relation, with which inputs, constants, conventions and assumptions, by which numerical method, where the code is, how it was checked independently, and — for the calculations this resource implements — a short Python reproduction you can run yourself. Four kinds of provenance are kept apart: the literature (why a physical relation is accepted: the claims above), the computation (how this app produced the displayed result: here), the validation (how the implementation was tested: section 7), and the teaching choices (what was simplified or exaggerated: the simplifications and the teaching visuals below). A record is computed by lya_app (code written for this resource), by an external package, by both (lya_app's result, compared on the same physical inputs with an independent external forward-model code — a lya_app computation whose functions are checked against a mathematical library such as SciPy stays lya_app), or is a teaching visualization that computes nothing. The same records, as a table: science/COMPUTATION_MATRIX.md in the source repository.

ComputationComputed byImplementationIndependent checkPythonScenes
Thermal Doppler widthlya_appdopplerBOptical depth, Python reproductions✓3, 6, 13
Natural width and the Voigt profilelya_appaDamp, voigtH, humlicekW4, phiVVoigt function accuracy, Python reproductions✓2, 4, 6, 7, 8, 13
Redshift, distance and observed wavelengthlya_applambdaObsOfRest, comovingDistance, zOfComoving, lambdaObsRedshift and frames, Real space to velocity space, Python reproductions✓0, 1, 5, 11, 12
Real space to velocity spacelya_apphubblePerMpch, uOfXReal space to velocity space, Python reproductions✓5, 6, 7, 8, 11, 12
One parcel's line — its width and depthlya_appparcelToCells, parcelTau, parcelWidths, tau0One parcel, four separate variables, Python reproductions✓6, 7, 8
Optical depth — contributions addlya_apptauFromCells, cellsFromSkewerOptical depth, Tracing a colour back to the gas, Python reproductions✓3, 6, 8, 9, 10, 11, 12, 13
Transmission — light multiplieslya_appfluxFromTau, tau0The drawing performs Beer–Lambert, Python reproductions✓5, 9, 10, 11, 12, 13
The instrument — resolution, pixels and noiselya_appconvolveLSF, addNoisePython reproductions✓11, 13
How much of the hydrogen is neutrallya_appalphaA, neOverNH, neutralFraction, nHIRelativeToy sightline: line kernels, neutral fraction, and the mean-flux calibration, Python reproductions✓0, 1, 6
Tracing a colour back to the gaslya_apptauContributionsTracing a colour back to the gas, Python reproductions✓12
The constructed "same shadow"lya_appsame_shadow_configs, parcelToCells, tauFromCells, b13tauSame-shadow degeneracy, Python reproductions✓13
The toy sightline's spectrumlya_app · reference fake_spectracellsFromSkewer, tauFromCells, fluxFromTauToy spectrum against the code that generated it, Closure against fake_spectra, Python reproductions✓8, 9, 10, 11, 12
fake_spectra 2.2.4 — the external reference spectrafake_spectra 2.2.4—Closure against fake_spectra—8
SciPy's Faddeeva function — the Voigt oracleSciPy 1.15.2—Voigt function accuracy—4
Representative atomsteaching visual———1, 2, 3, 4, 6
The 3D volumeteaching visual———0, 1
Parcels and cloudsteaching visual———6, 7, 8, 13
Magnified natural widthteaching visual———2, 4
Ink and its brightnessteaching visual———5, 6, 9, 10
The stretching ribbonteaching visual———5
Animation and timingteaching visual———0, 2, 3, 5, 7, 9, 12, 13
The red colourteaching visual———0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13

Thermal Doppler width

Computation: lya_app

the width of the thermal line — Beat 3's census width ±b, the thermal part of Beat 6's line, b_T of Beat 13's gases

bT=2kBTmH,ϕG(Δu)=e−Δu2/bT2π bTb_T = \sqrt{\dfrac{2k_BT}{m_{\rm H}}},\qquad \phi_{\rm G}(\Delta u) = \dfrac{e^{-\Delta u^2/b_T^2}}{\sqrt{\pi}\,b_T}

the Doppler parameter of the atoms' Maxwellian line-of-sight velocities, and the normalised thermal profile it sets (∫φ_G du = 1)

Variables
  • T — gas temperature [K]
  • b_T — thermal Doppler parameter (√2 × the line-of-sight velocity dispersion) [km/s]
  • Δu — velocity offset from the line centre [km/s]
Constants
Conventions
  • b in km/s
  • the 1/e half-width of the Gaussian; velocities along the line of sight
Assumptions
  • Maxwellian velocities at one temperature (thermal equilibrium of the atoms' motions); no turbulence
Numerical method
closed form
Validated for
the app's temperatures, 2×10³ to 10⁵ K
Validation
Optical depth · Python reproductions
the analytic Doppler parameter (Optical depth) and an independent Python implementation
tolerance: relative 1e-12
Implementation
science/js/lyaphys.js (dopplerB)
Claims (literature)
Maxwellian velocities; one line-of-sight component is Gaussian · Doppler parameter and the thermal (Gaussian) profile
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np

k_B = 1.380649e-16      # Boltzmann constant, erg/K (exact, SI 2019)
m_H = 1.6735328e-24     # mass of the 1H atom, g (1.00782503223 u, NIST)

def b_thermal(T):
    """Doppler parameter of the thermal (Gaussian) profile, km/s: b = sqrt(2 k_B T / m_H)"""
    return np.sqrt(2 * k_B * T / m_H) / 1e5

for T in (4.0e3, 1.0e4, 4.2e4):
    print(f"T = {T:7.0f} K  ->  b = {b_thermal(T):.3f} km/s")

Expected output — the app's own values, checked automatically (Python reproductions):

T =    4000 K  ->  b = 8.124 km/s
T =   10000 K  ->  b = 12.845 km/s
T =   42000 K  ->  b = 26.325 km/s

View the full reproduction script

scenes 3 · Temperature becomes line width, 6 · Where does one parcel land?, 13 · Same shadow · id COMP-THERM

Natural width and the Voigt profile

Computation: lya_app

the natural (Lorentzian) width and Beat 4's line shape; the profile of every line the app draws with Voigt kernels

γ=Γ λ04π,a=γb,x=Δub\gamma = \dfrac{\Gamma\,\lambda_0}{4\pi},\qquad a = \dfrac{\gamma}{b},\qquad x = \dfrac{\Delta u}{b}

the natural half-width in velocity, the damping parameter and the dimensionless offset (Γ = A₂₁, the 2p state decays only to 1s)

H(a,x)=Re⁡ w(x+ia)=aπ∫−∞∞e−y2 dy(x−y)2+a2,ϕV(Δu)=H(a, Δu/b)π bH(a,x) = \operatorname{Re}\,w(x + ia) = \dfrac{a}{\pi}\int_{-\infty}^{\infty}\dfrac{e^{-y^2}\,dy}{(x-y)^2 + a^2},\qquad \phi_{\rm V}(\Delta u) = \dfrac{H(a,\,\Delta u/b)}{\sqrt{\pi}\,b}

the Voigt–Hjerting function — the thermal Gaussian convolved with the natural Lorentzian — and the normalised profile per unit velocity

Variables
  • Γ — total decay rate of the upper level (A₂₁) [s⁻¹]
  • λ₀ — Lyα rest wavelength [cm]
  • γ — Lorentzian half width at half maximum in velocity [km/s]
  • a — damping parameter [dimensionless]
  • w(z) — the Faddeeva function, w(z) = e^(−z²) erfc(−iz) [dimensionless]
Constants
Conventions
Assumptions
  • natural broadening from the 2p lifetime only — collisional broadening is negligible at IGM densities
Approximations
  • w(z) is evaluated with Humlíček's (1982) W4 rational approximation, accurate to < 10⁻⁴ (relative) where tested
Numerical method
Humlíček W4: four regions chosen by |x| + a and by a against 0.195|x| − 0.176, each with its own rational or asymptotic form (region borders as in Zaghloul 2015, Table 1)
Validated for
a ∈ [10⁻⁶, 1], |x| ≤ 2×10⁴
Validation
Voigt function accuracy · Python reproductions
SciPy's Faddeeva function (wofz) on a 42 × 321 grid (Voigt function accuracy), and in Python
tolerance: relative < 10⁻⁴ for H (the W4 approximation); 1e-12 for γ and a
Implementation
science/js/lyaphys.js (aDamp, voigtH, humlicekW4, phiV)
Claims (literature)
Natural broadening from the finite lifetime of 2p · The Voigt profile — normalisation, definition and asymptotics · Numerical Voigt implementation in the app
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np
from scipy.special import wofz

k_B, m_H = 1.380649e-16, 1.6735328e-24      # erg/K; g (the 1H atom)
A21 = 6.2649e8                              # 2p -> 1s decay rate, 1/s (Wiese & Fuhr 2009)
lambda0 = 1215.6701e-8                      # Lya, cm (vacuum)

gamma = A21 * lambda0 / (4 * np.pi) / 1e5   # Lorentzian half width in velocity, km/s
b = np.sqrt(2 * k_B * 1.0e4 / m_H) / 1e5    # thermal Doppler parameter at 1e4 K, km/s
a = gamma / b                               # Voigt damping parameter

def H(a, x):
    """Voigt-Hjerting function: H(a, x) = Re w(x + i a); the line profile is H(a, du/b) / (sqrt(pi) b)"""
    return wofz(x + 1j * a).real

print(f"natural half width gamma = {gamma:.4e} km/s")
print(f"damping parameter a at 1e4 K = {a:.4e}")
print(f"thermal FWHM / natural FWHM at 1e4 K = {np.sqrt(np.log(2)) * b / gamma:.0f}")
for x in (0, 1, 2, 4, 10, 100):
    print(f"H(a, {x:3d}) = {H(a, x):.3e}")

Expected output — the app's own values, checked automatically (Python reproductions):

natural half width gamma = 6.0607e-03 km/s
damping parameter a at 1e4 K = 4.7182e-04
thermal FWHM / natural FWHM at 1e4 K = 1765
H(a,   0) = 9.995e-01
H(a,   1) = 3.679e-01
H(a,   2) = 1.842e-02
H(a,   4) = 1.863e-05
H(a,  10) = 2.703e-06
H(a, 100) = 2.662e-08

View the full reproduction script

scenes 2 · One atom meets one colour, 4 · The atom's lifetime leaves faint, far wings, 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate, 8 · From parcels back to a continuous sightline, 13 · Same shadow · id COMP-VOIGT

Redshift, distance and observed wavelength

Computation: lya_app

observed wavelengths (Beats 5, 11, 12), the distance to the toy sample (≈ 4.4 Gpc/h), the stretching ribbon's scale

λobs=λrest (1+z),χ(z)=cH0∫0zdz′E(z′),E(z)=Ωm(1+z)3+ΩΛ\lambda_{\rm obs} = \lambda_{\rm rest}\,(1 + z),\qquad \chi(z) = \dfrac{c}{H_0}\int_0^z \dfrac{dz'}{E(z')},\quad E(z) = \sqrt{\Omega_m(1+z)^3 + \Omega_\Lambda}

the stretch of every wavelength between the absorber's frame and ours, and the comoving distance along the light's path

λobs(u)=λ0 (1+z0) eu/c\lambda_{\rm obs}(u) = \lambda_0\,(1+z_0)\,e^{u/c}

the velocity coordinate on the observed-wavelength ruler (log-λ convention; z₀ is the redshift of the u = 0 point)

Variables
  • z — redshift of the absorbing gas [dimensionless]
  • χ — comoving distance [Mpc/h]
  • u — velocity coordinate along the sightline [km/s]
Constants
Conventions
  • flat ΛCDM with radiation neglected; distances in comoving Mpc/h; z₀ is the near end of the sightline
Assumptions
  • FLRW expansion; comoving observers; the gas's own small motion is folded into z for a single absorber
Approximations
  • the log-λ and the first-order forms differ by 2.9e-05 in 1 + z over 20 Mpc/h at z = 3
Numerical method
χ(z): composite Simpson's rule on 512 panels; z(χ): bisection (80 steps)
Validated for
z from 0.01 to 6 (tested)
Validation
Redshift and frames · Real space to velocity space · Python reproductions
SciPy quadrature (Redshift and frames), and in Python
tolerance: χ relative < 1e-7; identities to 1e-14
Implementation
science/js/lyaphys.js (lambdaObsOfRest, comovingDistance, zOfComoving, lambdaObs)
Claims (literature)
Cosmological redshift stretches every wavelength by the same factor · Comoving distance along the light's path (Planck 2018) · The forest is written along the way · Velocity coordinate ↔ observed wavelength
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np
from scipy.integrate import quad

c = 299792.458                   # km/s
lambda0 = 1215.6701              # Lya rest wavelength, Angstrom (vacuum)
Om, OL = 0.3153, 0.6847          # Planck 2018, flat; distances in Mpc/h do not depend on h
z = 3.0

E = lambda zz: np.sqrt(Om * (1 + zz)**3 + OL)
lambda_obs = lambda0 * (1 + z)                              # every wavelength is stretched by 1 + z
chi = c / 100 * quad(lambda zz: 1 / E(zz), 0, z)[0]          # comoving distance, Mpc/h
aH_h = 100 * E(z) / (1 + z)                                 # Hubble velocity per comoving Mpc/h at z, km/s
u = aH_h * 4.4                                              # across a 4.4 Mpc/h stretch of gas, km/s
dlambda = lambda_obs * (np.exp(u / c) - 1)                  # its width in observed wavelength (log-lambda convention)

print(f"Lya absorbed at z = 3 is observed at {lambda_obs:.2f} Angstrom")
print(f"comoving distance to z = 3: {chi:.1f} Mpc/h")
print(f"aH/h at z = 3: {aH_h:.2f} km/s per Mpc/h")
print(f"4.4 Mpc/h of Hubble flow: {u:.1f} km/s = {dlambda:.2f} Angstrom observed")

Expected output — the app's own values, checked automatically (Python reproductions):

Lya absorbed at z = 3 is observed at 4862.68 Angstrom
comoving distance to z = 3: 4383.8 Mpc/h
aH/h at z = 3: 114.19 km/s per Mpc/h
4.4 Mpc/h of Hubble flow: 502.4 km/s = 8.16 Angstrom observed

View the full reproduction script

scenes 0 · Gas occupies real space, 1 · Same gas, different scale, 5 · The same colour, stretched, 11 · A forest emerges, 12 · Who ate this colour? · id COMP-REDSHIFT

Real space to velocity space

Computation: lya_app

where gas lands in velocity — Beat 6's 363 + 87 = 450 km/s, Beat 7's three parcels, every sightline's u

u(x)=aH(z) xcom+vpec(x),aH(z)h=100 E(z)1+z  km s−1 per h−1Mpcu(x) = aH(z)\,x_{\rm com} + v_{\rm pec}(x),\qquad \dfrac{aH(z)}{h} = \dfrac{100\,E(z)}{1+z}\ \ {\rm km\,s^{-1}\ per}\ h^{-1}{\rm Mpc}

the velocity at which gas at comoving position x absorbs — the Hubble flow from the near edge plus its own peculiar velocity

Variables
  • x_com — comoving position from the near edge of the sightline or stretch (an arbitrary zero point) [Mpc/h]
  • v_pec — peculiar velocity along the line of sight; > 0 is away from us (redder) [km/s]
  • u — velocity coordinate, zero at x = 0 (an arbitrary zero point) [km/s]
  • aH/h — Hubble velocity per comoving Mpc/h [km/s per Mpc/h]
Constants
Conventions
  • the sightline runs from the observer side (x = 0) toward the quasar (+x)
  • v_pec > 0 ⇔ moving away from the observer ⇔ redder
  • u has an arbitrary zero point; observed wavelength follows from Redshift, distance and observed wavelength
Assumptions
  • aH constant across the box (a local
  • linear map); first order in v/c
Approximations
  • constant H: 0.2% from the exact distance–redshift relation over 20 Mpc/h (1.0% at 100 Mpc/h)
Numerical method
closed form
Validated for
boxes up to 100 Mpc/h at z = 2–4 (tested)
Validation
Real space to velocity space · Python reproductions
the exact log-λ relation integrated numerically (Real space to velocity space), and in Python
tolerance: relative 1e-12 (closed form)
Implementation
science/js/lyaphys.js (hubblePerMpch, uOfX)
Claims (literature)
Real space → velocity space map · Comoving vs physical coordinates and h-units · Peculiar-velocity sign convention · Validity of the constant-H (linear) map
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np

Om, OL, z = 0.3153, 0.6847, 3.0
aH_h = 100 * np.sqrt(Om * (1 + z)**3 + OL) / (1 + z)    # km/s per comoving Mpc/h (independent of h)

x = np.array([1.9303074084405747, 3.175154331970289, 3.8278357071063325])   # parcels a, b, c: comoving Mpc/h
v_pec = np.array([62.0, 87.0, 20.0])                                         # their own motion, km/s

u_hubble = aH_h * x              # expansion alone: the order in velocity follows the order in space
u = aH_h * x + v_pec             # with their own motion: b and c land close together
for name, uh, uu in zip("abc", u_hubble, u):
    print(f"parcel {name}: expansion alone {uh:.1f} km/s, with its own motion {uu:.1f} km/s")

Expected output — the app's own values, checked automatically (Python reproductions):

parcel a: expansion alone 220.4 km/s, with its own motion 282.4 km/s
parcel b: expansion alone 362.6 km/s, with its own motion 449.6 km/s
parcel c: expansion alone 437.1 km/s, with its own motion 457.1 km/s

View the full reproduction script

scenes 5 · The same colour, stretched, 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate, 8 · From parcels back to a continuous sightline, 11 · A forest emerges, 12 · Who ate this colour? · id COMP-VELMAP

One parcel's line — its width and depth

Computation: lya_app

Beat 6's widths (thermal, size, together), its peak optical depth, and the parcel lines of Beats 6–8

b2=bT2+bsize2,bsize=2 ∣aH+dvdx∣ σx,τ0=NHI σvπ bb^2 = b_T^2 + b_{\rm size}^2,\qquad b_{\rm size} = \sqrt{2}\,\Bigl|aH + \dfrac{dv}{dx}\Bigr|\,\sigma_x,\qquad \tau_0 = \dfrac{N_{\rm HI}\,\sigma_v}{\sqrt{\pi}\,b}

a Gaussian clump of width σ_x with a linear velocity field has a Gaussian spread of velocities that adds in quadrature to the thermal width; τ₀ is the line-centre optical depth of the Doppler profile

τ(u)=∑kNk σv ϕ(u−uk; bT),Nk∝e−sk2/2,  uk=aH (x+skσx)+v+dvdx skσx\tau(u) = \sum_{k} N_k\,\sigma_v\,\phi\bigl(u - u_k;\,b_T\bigr),\qquad N_k \propto e^{-s_k^2/2},\ \ u_k = aH\,(x + s_k\sigma_x) + v + \dfrac{dv}{dx}\,s_k\sigma_x

what the scene draws — the clump cut into sub-cells s_k ∈ [−5, 5], each a thermal line at its own velocity (its sum reproduces the quadrature law above)

Variables
  • σ_x — the clump's 1σ width along the beam [comoving Mpc/h]
  • dv/dx — the parcel's own peculiar-velocity gradient (Beat 6: set to zero) [km/s per Mpc/h]
  • N_HI — neutral hydrogen column [cm⁻²]
  • σ_v — integrated cross-section per unit velocity, (πe²/m_e c) f λ₀ [cm² km/s]
Constants
Conventions
  • x comoving from the near edge of the stretch; the parcel's column N is the total over the clump
Assumptions
  • Gaussian density profile along the beam; one temperature; a linear velocity field
  • Beat 6 sets the parcel's own peculiar-velocity gradient to zero, isolating heat and the Hubble flow across its size; with its own gradient the same parcel has b = 23.3 km/s
Approximations
  • sub-cell sampling: 161 sub-cells over ±5σ in Beats 6–10, 201 in Beat 13; the damping wings are kept (Voigt kernels)
Numerical method
parcelToCells (sub-cells with Gaussian weights) → tauFromCells (direct summation); parcelWidths gives the analytic widths
Validated for
Beat 6's control ranges (place, motion, temperature, amount)
Validation
One parcel, four separate variables · Python reproductions
the analytic quadrature law and τ₀ (One parcel, four separate variables: the drawn width within 2e-3 of it), and in Python
tolerance: relative 1e-12 for the analytic widths and τ₀
Implementation
science/js/lyaphys.js (parcelToCells, parcelTau, parcelWidths, tau0)
Claims (literature)
Doppler parameter and the thermal (Gaussian) profile · Velocity-gradient (Hubble) broadening of an extended absorber · Optical depth of one absorber
Teaching representation
Parcels are a teaching decomposition of a continuous sightline
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np

k_B, m_H = 1.380649e-16, 1.6735328e-24                  # erg/K; g (the 1H atom)
sigma_v = 2.6540e-2 * 0.4164 * 1215.6701e-8 / 1e5       # (pi e^2 / m_e c) f lambda0: cm^2 km/s
Om, OL, z = 0.3153, 0.6847, 3.0
aH_h = 100 * np.sqrt(Om * (1 + z)**3 + OL) / (1 + z)    # km/s per comoving Mpc/h

# the toy's parcel b, as Beat 6 starts it
T, sigma_x, N = 27744.0, 0.14721893675599934, 10**14.27  # K; comoving Mpc/h (1 sigma); cm^-2

def widths(dvdx):
    """thermal b, size b (Gaussian clump with a linear velocity field), and their quadrature sum, km/s"""
    b_th = np.sqrt(2 * k_B * T / m_H) / 1e5
    b_size = np.sqrt(2) * abs(aH_h + dvdx) * sigma_x
    return b_th, b_size, np.hypot(b_th, b_size)

b_th, b_size, b = widths(0.0)                           # this scene: the parcel's own gradient switched off
tau0 = N * sigma_v / (np.sqrt(np.pi) * b)                # peak optical depth of the Doppler profile
print(f"thermal b = {b_th:.2f} km/s, size b = {b_size:.2f} km/s, together b = {b:.2f} km/s")
print(f"peak optical depth tau0 = {tau0:.2f}")
_, b_size_own, b_own = widths(-70.41475616076316)       # the same parcel with its own (infalling) gradient
print(f"with its own gradient: size b = {b_size_own:.2f} km/s, b = {b_own:.2f} km/s")

Expected output — the app's own values, checked automatically (Python reproductions):

thermal b = 21.40 km/s, size b = 23.77 km/s, together b = 31.98 km/s
peak optical depth tau0 = 4.41
with its own gradient: size b = 9.11 km/s, b = 23.26 km/s

View the full reproduction script

scenes 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate, 8 · From parcels back to a continuous sightline · id COMP-PARCEL

Optical depth — contributions add

Computation: lya_app

every optical-depth curve (Beats 3, 6–13) and Beat 9's sum

τ(u)=∑jNj σv ϕ(u−uj; bj),σv=πe2mec f λ0,Nj=nHI,j Δxprop\tau(u) = \sum_j N_j\,\sigma_v\,\phi\bigl(u - u_j;\,b_j\bigr),\qquad \sigma_v = \dfrac{\pi e^2}{m_e c}\,f\,\lambda_0,\qquad N_j = n_{{\rm HI},j}\,\Delta x_{\rm prop}

each contribution's column times the cross-section per unit velocity times its normalised profile; along a sightline each real-space cell contributes its own column

Variables
Constants
Conventions
  • profile sampled at pixel centres (point sampling); each contribution's kernel extends to 8 b and its damping wings until τ < 10⁻⁶; periodic boxes use the nearest image
Assumptions
  • optical depths of independent absorbers add (a linear
  • optically passive medium); one velocity per sampling element
Approximations
  • the toy forest is drawn with Gaussian kernels: max |ΔF| 6e-04 against Voigt kernels
Numerical method
direct summation over contributions on a uniform velocity grid
Validated for
one velocity per sampling element and lines resolved by at least two pixels (One velocity per sampling element (unresolved velocity structure is not represented), Discretisation limits — pixel size, pixel averaging and the periodic box)
Validation
Optical depth · Tracing a colour back to the gas · Python reproductions
exact Voigt sums and ∫τ du = N σ_v (Optical depth); the traced contributions sum to τ (Tracing a colour back to the gas); and in Python
tolerance: relative 1e-4 against exact Voigt (the W4 approximation)
Implementation
science/js/lyaphys.js (tauFromCells, cellsFromSkewer)
Claims (literature)
Optical depth of one absorber · Line-of-sight accumulation of optical depth in velocity space · Why optical depths add · Discretisation limits — pixel size, pixel averaging and the periodic box
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np
from scipy.special import wofz

sigma_v = 2.6540e-2 * 0.4164 * 1215.6701e-8 / 1e5       # (pi e^2 / m_e c) f lambda0: cm^2 km/s
gamma = 6.2649e8 * 1215.6701e-8 / (4 * np.pi) / 1e5      # natural half width, km/s

def tau(u, u0, N, b):
    """one contribution: N sigma_v phi(u - u0), phi = H(a, x) / (sqrt(pi) b) with x = (u - u0)/b, a = gamma/b"""
    return N * sigma_v * wofz((u - u0) / b + 1j * gamma / b).real / (np.sqrt(np.pi) * b)

cells = [(-20.0, 5e13, 15.0), (0.0, 1e14, 20.0), (25.0, 3e13, 12.0)]   # (u0 km/s, N cm^-2, b km/s)
for u in (0.0, 20.0):
    each = [tau(u, *c) for c in cells]
    total = sum(each)
    print(f"u = {u:4.0f} km/s: tau = " + " + ".join(f"{t:.4f}" for t in each) + f" = {total:.4f};  F = exp(-tau) = {np.exp(-total):.5f} = product of exp(-tau_j) = {np.prod(np.exp(-np.array(each))):.5f}")

Expected output — the app's own values, checked automatically (Python reproductions):

u =    0 km/s: tau = 0.4273 + 3.7886 + 0.0249 = 4.2408;  F = exp(-tau) = 0.01440 = product of exp(-tau_j) = 0.01440
u =   20 km/s: tau = 0.0022 + 1.3943 + 1.5922 = 2.9887;  F = exp(-tau) = 0.05036 = product of exp(-tau_j) = 0.05036

View the full reproduction script

scenes 3 · Temperature becomes line width, 6 · Where does one parcel land?, 8 · From parcels back to a continuous sightline, 9 · Ink adds, 10 · Light multiplies, 11 · A forest emerges, 12 · Who ate this colour?, 13 · Same shadow · id COMP-TAU

Transmission — light multiplies

Computation: lya_app

every transmitted-flux curve, Beat 10's products and the ink's brightness

F(u)=e−τ(u)=∏je−τj(u)F(u) = e^{-\tau(u)} = \prod_j e^{-\tau_j(u)}

the intrinsic transmission, before any instrument

Variables
  • F — transmitted fraction of the continuum (intrinsic) [dimensionless]
Conventions
Assumptions
  • absorption only
  • no source term along the beam (resonant scattering removes light from a narrow beam)
Numerical method
closed form
Validated for
all τ ≥ 0
Validation
The drawing performs Beer–Lambert · Python reproductions
the drawn ink read back from the canvas equals e^(−τ) (The drawing performs Beer–Lambert), and in Python
tolerance: relative 1e-12
Implementation
science/js/lyaphys.js (fluxFromTau, tau0)
Claims (literature)
Transmitted flux (Beer–Lambert) · Saturation and the curve of growth
Teaching representation
Ink is optical depth, and the ink rendering performs Beer–Lambert
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np

sigma_v = 2.6540e-2 * 0.4164 * 1215.6701e-8 / 1e5       # (pi e^2 / m_e c) f lambda0: cm^2 km/s

for t in (1.73, 11.9, 0.37):
    print(f"tau = {t:5.2f}  ->  F = exp(-tau) = {np.exp(-t):.3e}")
N, b = 1e15, 25.0                                        # cm^-2, km/s
tau0 = N * sigma_v / (np.sqrt(np.pi) * b)                # line-centre optical depth of a Doppler line
print(f"N = 1e15 cm^-2, b = 25 km/s: tau0 = {tau0:.2f}, F at line centre = {np.exp(-tau0):.1e} (saturated)")

Expected output — the app's own values, checked automatically (Python reproductions):

tau =  1.73  ->  F = exp(-tau) = 1.773e-01
tau = 11.90  ->  F = exp(-tau) = 6.790e-06
tau =  0.37  ->  F = exp(-tau) = 6.907e-01
N = 1e15 cm^-2, b = 25 km/s: tau0 = 30.32, F at line centre = 6.8e-14 (saturated)

View the full reproduction script

scenes 5 · The same colour, stretched, 9 · Ink adds, 10 · Light multiplies, 11 · A forest emerges, 12 · Who ate this colour?, 13 · Same shadow · id COMP-FLUX

The instrument — resolution, pixels and noise

Computation: lya_app

Beat 11's recorded spectrum and Beat 13's noisy line

Fobs(u)=(F∗GFWHM)(u)+n,G(u)∝e−u2/2σ2,  σ=FWHM22ln⁡2,n∼N(0, (S/N)−2)F_{\rm obs}(u) = \bigl(F \ast G_{\rm FWHM}\bigr)(u) + n,\qquad G(u) \propto e^{-u^2/2\sigma^2},\ \ \sigma = \dfrac{\rm FWHM}{2\sqrt{2\ln 2}},\qquad n \sim \mathcal{N}\bigl(0,\,(S/N)^{-2}\bigr)

the spectrograph convolves the transmitted flux (never τ) with its line-spread function; the detector adds white noise per pixel

Variables
  • FWHM — the resolution [km/s]
  • S/N — signal-to-noise per pixel (continuum-normalised) [dimensionless]
Conventions
  • order: intrinsic τ → intrinsic F → line-spread function → pixels → noise; the gas and τ are untouched
  • pixels max(2.5 km/s, FWHM/3) wide
Assumptions
  • a Gaussian line-spread function; white
  • flux-independent noise; no sky
  • no continuum error
Approximations
  • the kernel is cut at 5σ
Numerical method
discrete convolution with a normalised Gaussian kernel (edges extended; the toy forest is periodic); noise from a seeded generator (Box–Muller), so a given seed draws the same noise
Validated for
FWHM 0–200 km/s and S/N 5, 20, 50 or no noise (Beat 11); S/N 20 (Beat 13)
Validation
Python reproductions
an independent Python convolution (the noise draw is not reproduced: it depends on the generator)
tolerance: relative 1e-4
Implementation
science/js/lyaphys.js (convolveLSF, addNoise)
Claims (literature)
Instrument resolution acts on the observed flux · Noise model and signal-to-noise
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np
from scipy.special import wofz

sigma_v = 2.6540e-2 * 0.4164 * 1215.6701e-8 / 1e5       # cm^2 km/s
gamma = 6.2649e8 * 1215.6701e-8 / (4 * np.pi) / 1e5      # km/s
du = 2.5
u = np.arange(-300, 300 + du / 2, du)                    # km/s, 2.5 km/s pixels
N, b, fwhm = 2e14, 25.0, 70.0                            # cm^-2, km/s, the spectrograph's resolution (FWHM, km/s)

F = np.exp(-N * sigma_v * wofz(u / b + 1j * gamma / b).real / (np.sqrt(np.pi) * b))   # intrinsic transmission
s = fwhm / (2 * np.sqrt(2 * np.log(2)))                  # Gaussian LSF: sigma from FWHM
h = int(np.ceil(5 * s / du))
w = np.exp(-0.5 * (np.arange(-h, h + 1) * du / s) ** 2); w /= w.sum()
F_obs = np.convolve(np.pad(F, h, mode='edge'), w, mode='valid')   # applied to F, never to tau
EW = lambda f: np.sum(1 - f) * du
print(f"deepest point: intrinsic F = {F.min():.4f}, observed F = {F_obs.min():.4f}")
print(f"equivalent width: intrinsic {EW(F):.2f} km/s, observed {EW(F_obs):.2f} km/s")

Expected output — the app's own values, checked automatically (Python reproductions):

deepest point: intrinsic F = 0.0023, observed F = 0.2374
equivalent width: intrinsic 74.34 km/s, observed 74.34 km/s

View the full reproduction script

scenes 11 · A forest emerges, 13 · Same shadow · id COMP-INSTR

How much of the hydrogen is neutral

Computation: lya_app

the mean density (≈ a dozen atoms per m³), x_HI ≈ 1 in 100 000 (Beats 0, 1), the ultraviolet microscope (Beat 6)

xHI=αA(T) neΓHI,αA=4.2×10−13(T104 K)−0.7 cm3 s−1,nenH=1+2Y4(1−Y)x_{\rm HI} = \dfrac{\alpha_A(T)\,n_e}{\Gamma_{\rm HI}},\quad \alpha_A = 4.2\times10^{-13}\Bigl(\dfrac{T}{10^4\,{\rm K}}\Bigr)^{-0.7}\ {\rm cm^3\,s^{-1}},\quad \dfrac{n_e}{n_{\rm H}} = 1 + \dfrac{2Y}{4(1-Y)}

photoionisation equilibrium in a highly ionised gas (case A), with hydrogen and helium fully ionised

nˉH(z)=Ωbh2 ρcrit,0h−2 (1−Y)mH (1+z)3,nH=nˉH Δ\bar n_{\rm H}(z) = \dfrac{\Omega_b h^2\,\rho_{{\rm crit},0}h^{-2}\,(1 - Y)}{m_{\rm H}}\,(1+z)^3,\qquad n_{\rm H} = \bar n_{\rm H}\,\Delta

the mean hydrogen density (proper), and at overdensity Δ

Variables
  • Γ_HI — H I photoionisation rate (measured ≈ 0.8×10⁻¹² s⁻¹ at z ≈ 3) [s⁻¹]
  • Δ — overdensity, n_H / n̄_H [dimensionless]
  • Y — helium mass fraction [dimensionless]
Constants
Conventions
  • densities proper
  • in cm⁻³
Assumptions
  • photoionisation equilibrium; the gas highly ionised (x_HI ≪ 1); case-A recombination fit
Approximations
  • α_A is a power-law fit to the case-A rate (compared with Hui & Gnedin 1997 and Draine 2011 in the claim it implements)
Numerical method
closed form (the mean density is the toy generator's, science/data/toy/zeldovich3d.py)
Validated for
the app's temperatures and overdensities, where x_HI ≪ 1
Validation
Toy sightline: line kernels, neutral fraction, and the mean-flux calibration · Python reproductions
x_HI at mean density checked against the literature range (Toy sightline: line kernels, neutral fraction, and the mean-flux calibration), and in Python from the cosmological parameters
tolerance: relative 1e-6
Implementation
science/js/lyaphys.js (alphaA, neOverNH, neutralFraction, nHIRelative)
Claims (literature)
Mean hydrogen density of the universe at z = 3 · Photoionisation equilibrium and the neutral-amount control · How neutral the IGM is
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page.

import numpy as np

# Planck 2018 (TT,TE,EE+lowE+lensing): baryon density and helium mass fraction; the redshift of the toy
Obh2, Yp, z = 0.02237, 0.2454, 3.0
G = 6.67430e-8                     # cm^3 g^-1 s^-2 (CODATA 2022)
m_H = 1.6735328e-24                # g, the 1H atom
Mpc = 3.0856775814913673e24        # cm
H100 = 100e5 / Mpc                 # 100 km/s/Mpc in 1/s

rho_crit_h2 = 3 * H100**2 / (8 * np.pi * G)            # critical density today per h^2, g/cm^3
nH_bar = Obh2 * rho_crit_h2 * (1 - Yp) / m_H * (1 + z)**3   # mean hydrogen density at z, cm^-3 (proper)
ne_over_nH = 1 + 2 * Yp / (4 * (1 - Yp))                # electrons per H nucleus, H and He fully ionised

def x_HI(T, Gamma12, Delta=1.0):
    """neutral fraction in photoionisation equilibrium (case A): alpha_A(T) n_e / Gamma_HI"""
    alpha_A = 4.2e-13 * (T / 1e4) ** -0.7               # cm^3/s
    return alpha_A * ne_over_nH * nH_bar * Delta / (Gamma12 * 1e-12)

print(f"mean hydrogen density at z = 3: {nH_bar:.4e} cm^-3")
for T in (1.0e4, 1.3e4):
    print(f"x_HI at mean density, T = {T:.1e} K, Gamma_HI = 0.8e-12 /s: {x_HI(T, 0.8):.3e}")

Expected output — the app's own values, checked automatically (Python reproductions):

mean hydrogen density at z = 3: 1.2126e-05 cm^-3
x_HI at mean density, T = 1.0e+04 K, Gamma_HI = 0.8e-12 /s: 7.401e-06
x_HI at mean density, T = 1.3e+04 K, Gamma_HI = 0.8e-12 /s: 6.159e-06

View the full reproduction script

scenes 0 · Gas occupies real space, 1 · Same gas, different scale, 6 · Where does one parcel land? · id COMP-ION

Tracing a colour back to the gas

Computation: lya_app

Beat 12's ledger — which places contribute to one colour's optical depth

τj(u)=τ0(Nj,bj) e−(u−uj)2/bj2,τ(u)=∑jτj(u),F(u)=∏je−τj(u)\tau_j(u) = \tau_0(N_j, b_j)\,e^{-(u - u_j)^2/b_j^2},\qquad \tau(u) = \sum_j \tau_j(u),\qquad F(u) = \prod_j e^{-\tau_j(u)}

each cell's own optical depth at the chosen colour (the Gaussian kernels the toy forest is drawn with); the contributions sum to the spectrum's τ

Variables
  • τ_j(u) — cell j's contribution at velocity u [dimensionless]
Conventions
  • the same kernels and cut as the drawn spectrum; places are grouped by contiguity along the line — a display choice
Assumptions
  • the forward-model truth is known: the toy's gas is traced, not inferred
Approximations
  • cells under the display cut stay in the ledger's "rest" (median 3.2% of a pixel's τ)
Numerical method
direct evaluation per cell within the kernel cut
Validated for
the toy sightline
Validation
Tracing a colour back to the gas · Python reproductions
the traced contributions sum to the spectrum's own τ at every pixel (Tracing a colour back to the gas); the same sum in Python (reproduce/beat09_optical_depth.py)
tolerance: relative 1e-12 (the sum)
Implementation
science/js/lyaphys.js (tauContributions)
Claims (literature)
Line-of-sight accumulation of optical depth in velocity space · Why optical depths add
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page. Note: this reproduces the sum the trace relies on — contributions add, and the transmission is their product — on a small example with Voigt kernels; the trace itself uses the Gaussian kernels above, and Tracing a colour back to the gas checks that its contributions sum to τ.

import numpy as np
from scipy.special import wofz

sigma_v = 2.6540e-2 * 0.4164 * 1215.6701e-8 / 1e5       # (pi e^2 / m_e c) f lambda0: cm^2 km/s
gamma = 6.2649e8 * 1215.6701e-8 / (4 * np.pi) / 1e5      # natural half width, km/s

def tau(u, u0, N, b):
    """one contribution: N sigma_v phi(u - u0), phi = H(a, x) / (sqrt(pi) b) with x = (u - u0)/b, a = gamma/b"""
    return N * sigma_v * wofz((u - u0) / b + 1j * gamma / b).real / (np.sqrt(np.pi) * b)

cells = [(-20.0, 5e13, 15.0), (0.0, 1e14, 20.0), (25.0, 3e13, 12.0)]   # (u0 km/s, N cm^-2, b km/s)
for u in (0.0, 20.0):
    each = [tau(u, *c) for c in cells]
    total = sum(each)
    print(f"u = {u:4.0f} km/s: tau = " + " + ".join(f"{t:.4f}" for t in each) + f" = {total:.4f};  F = exp(-tau) = {np.exp(-total):.5f} = product of exp(-tau_j) = {np.prod(np.exp(-np.array(each))):.5f}")

Expected output — the app's own values, checked automatically (Python reproductions):

u =    0 km/s: tau = 0.4273 + 3.7886 + 0.0249 = 4.2408;  F = exp(-tau) = 0.01440 = product of exp(-tau_j) = 0.01440
u =   20 km/s: tau = 0.0022 + 1.3943 + 1.5922 = 2.9887;  F = exp(-tau) = 0.05036 = product of exp(-tau_j) = 0.05036

View the full reproduction script

scenes 12 · Who ate this colour? · id COMP-INV

The constructed "same shadow"

Computation: lya_app

Beat 13's three gases, their Lyα, Lyβ and Si IV lines, and the Δχ² that separates them

btot2=2kTmH+2σu2,σu=su+δv,δv(iii)=(su(i))2+12(bT(i) 2−bT(iii) 2)−su(iii)b_{\rm tot}^2 = \dfrac{2kT}{m_{\rm H}} + 2\sigma_u^2,\quad \sigma_u = s_u + \delta v,\qquad \delta v^{\rm (iii)} = \sqrt{\bigl(s_u^{\rm (i)}\bigr)^2 + \tfrac12\bigl(b_T^{\rm (i)\,2} - b_T^{\rm (iii)\,2}\bigr)} - s_u^{\rm (iii)}

a clump's total Doppler parameter, and the matching condition b_tot(iii) = b_tot(i) that gives (iii) its smooth extra velocity (5.9883 km/s)

Δχ2=∑k(Fk(n)−Fk(i))2 (S/N)2,τLyβ(u)=(fλ)Lyβ(fλ)Lyα τLyα(u)\Delta\chi^2 = \sum_k \bigl(F^{(n)}_k - F^{(i)}_k\bigr)^2\,(S/N)^2,\qquad \tau_{\rm Ly\beta}(u) = \dfrac{(f\lambda)_{\rm Ly\beta}}{(f\lambda)_{\rm Ly\alpha}}\,\tau_{\rm Ly\alpha}(u)

noise-free Δχ² against (i) over 121 pixels of 2.5 km/s; the Lyβ line as the app draws it

Variables
  • s_u — Hubble-flow width of a clump (1σ) [km/s]
  • δv — extra smooth peculiar velocity across 1σ [km/s]
  • S/N — signal-to-noise per 2.5 km/s pixel [dimensionless]
Constants
Conventions
  • configurations in velocity space (so they do not depend on the cosmology), from their single source science/validation/degeneracy/configs.json
Assumptions
Approximations
  • clumps cut into 201 sub-cells over ±5σ; Lyβ keeps Lyα's damping constant (its wings are immaterial at these columns)
Numerical method
construction by the matching rule (science/tools/same_shadow_configs.py); lines by parcelToCells → tauFromCells; Δχ² by direct summation
Validated for
S/N 20 to 200
Validation
Same-shadow degeneracy · Python reproductions
an exact-Voigt sub-cell oracle (Same-shadow degeneracy) and a Python sub-cell sum with SciPy's Faddeeva function — two implementations, neither the app's
tolerance: Δχ²(ii) within 5 % (oracle) and 1e-3 (Python); Δχ²(iii) < 1e-4 on the exact-Voigt paths (the oracle and Python), < 1e-2 as the app draws it with Humlíček's W4
Implementation
science/tools/same_shadow_configs.py · science/js/lyaphys.js · app/src/scenes/b13-same-shadow.js (same_shadow_configs, parcelToCells, tauFromCells, b13tau)
Claims (literature)
The "same shadow" configurations · What breaks the degeneracy · Velocity-gradient (Hubble) broadening of an extended absorber · Noise model and signal-to-noise
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page. Note: the matching condition is closed form; the lines are summed over each clump's sub-cells, with no closed form, so that (iii) matches (i) is computed, not assumed.

import numpy as np
from scipy.special import wofz

k_B, m_H = 1.380649e-16, 1.6735328e-24
sigma_v = 2.6540e-2 * 0.4164 * 1215.6701e-8 / 1e5       # Lya: (pi e^2 / m_e c) f lambda0, cm^2 km/s
gamma = 6.2649e8 * 1215.6701e-8 / (4 * np.pi) / 1e5      # km/s
lyb = 0.079142 * 1025.72221 / (0.4164 * 1215.6701)       # Lyb optical depth relative to Lya: (f lambda) ratio
b_T = lambda T: np.sqrt(2 * k_B * T / m_H) / 1e5         # thermal Doppler parameter, km/s

# gases in velocity space: centre u, Hubble-flow width su (1 sigma), bulk velocity v, extra smooth velocity dv across 1 sigma
I = [dict(u=0.0, su=2.23, N=2.5e14, T=45000.0, v=0.0, dv=0.0)]                       # one hot, compact clump
II = [dict(u=-39.0, su=12.8, N=9.75e13, T=1e4, v=24.9, dv=0.0),                      # two cold clumps, converging
      dict(u=39.0, su=12.8, N=9.75e13, T=1e4, v=-24.9, dv=0.0)]
dv = np.sqrt(2.23**2 + (b_T(45000.0)**2 - b_T(1e4)**2) / 2) - 11.15                 # matching: b_T^2 + 2 sigma_u^2 equal
III = [dict(u=0.0, su=11.15, N=2.5e14, T=1e4, v=0.0, dv=dv)]                         # one cold clump, expanding

u = np.linspace(-150, 150, 121)                                                      # 2.5 km/s pixels
s = np.linspace(-5, 5, 401); w = np.exp(-s**2 / 2); w /= w.sum()                    # a clump in sub-cells over +-5 sigma
def tau(gas, scale=1.0):                       # no closed form here: every sub-cell is a thermal line at its own velocity
    t = np.zeros_like(u)
    for c in gas:
        b = b_T(c['T']); x = (u[:, None] - c['u'] - c['v'] - (c['su'] + c['dv']) * s) / b
        t += scale * c['N'] * sigma_v * (w * wofz(x + 1j * gamma / b).real).sum(1) / (np.sqrt(np.pi) * b)
    return t

sig3 = lambda x: f"{float(f'{x:.3g}'):g}"
print(f"(iii)'s smooth extra velocity from the matching condition: dv = {dv:.4f} km/s")
for line, scale in (("Lya", 1.0), ("Lyb", lyb)):
    F = {k: np.exp(-tau(g, scale)) for k, g in (("i", I), ("ii", II), ("iii", III))}
    for snr in (20, 50, 100, 200):
        chi2 = {k: np.sum((F[k] - F["i"]) ** 2) * snr**2 for k in ("ii", "iii")}          # noise-free, against (i)
        print(f"{line} S/N {snr:3d}: dchi2(ii) = {sig3(chi2['ii']):>6}   dchi2(iii) = {chi2['iii']:.2f}")

Expected output — the app's own values, checked automatically (Python reproductions):

(iii)'s smooth extra velocity from the matching condition: dv = 5.9883 km/s
Lya S/N  20: dchi2(ii) =   2.79   dchi2(iii) = 0.00
Lya S/N  50: dchi2(ii) =   17.4   dchi2(iii) = 0.00
Lya S/N 100: dchi2(ii) =   69.6   dchi2(iii) = 0.00
Lya S/N 200: dchi2(ii) =    279   dchi2(iii) = 0.00
Lyb S/N  20: dchi2(ii) =    103   dchi2(iii) = 0.00
Lyb S/N  50: dchi2(ii) =    645   dchi2(iii) = 0.00
Lyb S/N 100: dchi2(ii) =   2580   dchi2(iii) = 0.00
Lyb S/N 200: dchi2(ii) =  10300   dchi2(iii) = 0.00

View the full reproduction script

scenes 13 · Same shadow · id COMP-DEG

The toy sightline's spectrum

Displayed result: lya_app reduced forward model · Independent reference: fake_spectra 2.2.4

the toy forest (Beats 8–12) — the continuous sightline, its optical depth and transmission

Nj=nHI,j Δxcom ah,uj=aH xj+vpec,j,bj=2kTj/mH,τ(u)=∑jNj σv ϕ(u−uj; bj),F=e−τN_j = n_{{\rm HI},j}\,\Delta x_{\rm com}\,\dfrac{a}{h},\quad u_j = aH\,x_j + v_{{\rm pec},j},\quad b_j = \sqrt{2kT_j/m_{\rm H}},\qquad \tau(u) = \sum_j N_j\,\sigma_v\,\phi(u - u_j;\,b_j),\quad F = e^{-\tau}

the reduced forward model the app runs on the toy's real-space pixels

Variables
  • n_HI,j, T_j, v_pec,j — the toy skewer's real-space pixels (neutral density, temperature, peculiar velocity) [cm⁻³ (proper), K, km/s]
Constants
  • Γ_HI (measured) = 0.8×10⁻¹² s⁻¹ at z ≈ 3 — Becker & Bolton 2013
  • Γ_HI (toy) = tuned to 3.0× the measured rate so that ⟨F⟩ = 0.68 (the mean-flux target) — a calibration — Becker et al. 2013
Conventions
  • x comoving Mpc/h; v_pec > 0 away from the observer; periodic 20 Mpc/h box
Assumptions
  • one velocity and one temperature per real-space cell (single-stream); Zel'dovich toy density field
  • not a hydrodynamic simulation
Approximations
  • drawn with Gaussian kernels (max |ΔF| 6e-04 against Voigt kernels)
Numerical method
cellsFromSkewer → tauFromCells (direct summation) → fluxFromTau
Validated for
the toy at its own sampling, inside the declared closure domain (one velocity per sampling element; lines resolved by at least two pixels)
Independent reference
fake_spectra 2.2.4 — independent reference implementation (validation only: no scene shows a fake_spectra spectrum) (its record)
Validation
Toy spectrum against the code that generated it · Closure against fake_spectra · Python reproductions
the toy generator's own exact-Voigt τ (Toy spectrum against the code that generated it); fake_spectra 2.2.4 on synthetic test skewers (Closure against fake_spectra); the accumulation in Python
tolerance: as drawn max |ΔF| < 1e-3 against the generator; declared-domain closure criteria against fake_spectra
Implementation
science/js/lyaphys.js (cellsFromSkewer, tauFromCells, fluxFromTau)
Claims (literature)
The skewer interface (dataset-independent) · The toy Zel'dovich volume (synthetic teaching data) · Line-of-sight accumulation of optical depth in velocity space · One velocity per sampling element (unresolved velocity structure is not represented)
Reproduce in Python ▸

An independent reproduction in Python, with NumPy and SciPy only. Copy it into your own Python, a notebook or Colab; nothing runs on this page. Note: the same accumulation on a small example; the full sightline is checked against the two independent codes.

import numpy as np
from scipy.special import wofz

sigma_v = 2.6540e-2 * 0.4164 * 1215.6701e-8 / 1e5       # (pi e^2 / m_e c) f lambda0: cm^2 km/s
gamma = 6.2649e8 * 1215.6701e-8 / (4 * np.pi) / 1e5      # natural half width, km/s

def tau(u, u0, N, b):
    """one contribution: N sigma_v phi(u - u0), phi = H(a, x) / (sqrt(pi) b) with x = (u - u0)/b, a = gamma/b"""
    return N * sigma_v * wofz((u - u0) / b + 1j * gamma / b).real / (np.sqrt(np.pi) * b)

cells = [(-20.0, 5e13, 15.0), (0.0, 1e14, 20.0), (25.0, 3e13, 12.0)]   # (u0 km/s, N cm^-2, b km/s)
for u in (0.0, 20.0):
    each = [tau(u, *c) for c in cells]
    total = sum(each)
    print(f"u = {u:4.0f} km/s: tau = " + " + ".join(f"{t:.4f}" for t in each) + f" = {total:.4f};  F = exp(-tau) = {np.exp(-total):.5f} = product of exp(-tau_j) = {np.prod(np.exp(-np.array(each))):.5f}")

Expected output — the app's own values, checked automatically (Python reproductions):

u =    0 km/s: tau = 0.4273 + 3.7886 + 0.0249 = 4.2408;  F = exp(-tau) = 0.01440 = product of exp(-tau_j) = 0.01440
u =   20 km/s: tau = 0.0022 + 1.3943 + 1.5922 = 2.9887;  F = exp(-tau) = 0.05036 = product of exp(-tau_j) = 0.05036

View the full reproduction script

scenes 8 · From parcels back to a continuous sightline, 9 · Ink adds, 10 · Light multiplies, 11 · A forest emerges, 12 · Who ate this colour? · id COMP-SIGHTLINE

fake_spectra 2.2.4 — the external reference spectra

Reference: fake_spectra 2.2.4

the closure numbers in the science notes (Closure against fake_spectra); no scene shows a fake_spectra spectrum

Conventions
Package
fake_spectra 2.2.4 (MIT) — fake_spectra (Bird)
Interface
fake_spectra.spectra.Spectra on mock Gadget-format snapshots (science/oracle/fakespectra_mocksnap.py writes them; science/oracle/fakespectra_run_closure.py runs Spectra and reads τ)
Inputs
particle positions, velocities, temperatures, smoothing lengths and neutral hydrogen of synthetic test cases (science/validation/closure/cases)
Conversions by this resource
  • this project writes its synthetic skewers as mock snapshots in Gadget units (comoving kpc/h; velocities with the √a factor) and compares τ and F on the same pixel grid
  • skewer velocities are never taken from get_velocity
Validation
Closure against fake_spectra
compared with this project's real-space method and an exact reference on the same cases
tolerance: declared-domain criteria (max |ΔF| ≤ 0.01; |Δ⟨F⟩| ≤ 1e-3)
Claims (literature)
fake_spectra conventions (the validation reference) · Exporting skewers from fake_spectra 2.2.4 — a velocity-convention compatibility note · Discretisation limits — pixel size, pixel averaging and the periodic box

scenes 8 · From parcels back to a continuous sightline · id COMP-FAKESPECTRA

SciPy's Faddeeva function — the Voigt oracle

Reference: SciPy 1.15.2

nothing directly: it is the accuracy reference for the app's Voigt function and for the oracles

Conventions
  • H(a, x) = Re w(x + i a)
Package
SciPy 1.15.2 (BSD-3-Clause) — SciPy scipy.special.wofz
Interface
scipy.special.wofz (science/oracle/voigt_oracle.py, degeneracy_oracle.py, tau_oracle.py; reproduce/*.py)
Inputs
complex argument x + i a on the test grid
Validation
Voigt function accuracy
the reference itself (Faddeeva package algorithms)
tolerance: —
Claims (literature)
Numerical Voigt implementation in the app

scenes 4 · The atom's lifetime leaves faint, far wings · id COMP-WOFZ

Representative atoms

Teaching visualization

dots that stand for neutral atoms — a cartoon sample, their number proportional to N_HI; not simulation particles and not to scale

The physics
the absorption is computed from N_HI and the profiles above, never from the drawn dots
Teaching representation
Representative atoms are a cartoon sample

scenes 1 · Same gas, different scale, 2 · One atom meets one colour, 3 · Temperature becomes line width, 4 · The atom's lifetime leaves faint, far wings, 6 · Where does one parcel land? · id TV-ATOMS

The 3D volume

Teaching visualization

the toy's density field drawn as a slab that fades away from the beam; only gas on the beam absorbs

The physics
the sightline's spectrum uses the beam's own pixels (The toy sightline's spectrum)
Teaching representation
The 3D slab is a teaching volume; only gas on the beam absorbs

scenes 0 · Gas occupies real space, 1 · Same gas, different scale · id TV-VOLUME

Parcels and clouds

Teaching visualization

three Gaussian parcels summarising peak regions of the continuous sightline, and Beat 13's idealised clumps — a teaching decomposition, not separate clouds

The physics
their lines are computed (One parcel's line — its width and depth, The constructed "same shadow"); Beat 8 shows the full sightline the parcels summarise
Teaching representation
Parcels are a teaching decomposition of a continuous sightline

scenes 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate, 8 · From parcels back to a continuous sightline, 13 · Same shadow · id TV-PARCELS

Magnified natural width

Teaching visualization

the natural width (≈ 0.006 km/s) magnified wherever it must be seen, and labelled (×500 in Beat 2, ×1200 in Beat 4)

The physics
the computed profiles use the true width (Natural width and the Voigt profile)
Teaching representation
Natural width is magnified wherever it is drawn

scenes 2 · One atom meets one colour, 4 · The atom's lifetime leaves faint, far wings · id TV-NATWIDTH

Ink and its brightness

Teaching visualization

ink stands for optical depth; where it is drawn as light, its brightness is e^(−τ)

The physics
the rendering performs Beer–Lambert, checked from the canvas (The drawing performs Beer–Lambert)
Teaching representation
Ink is optical depth, and the ink rendering performs Beer–Lambert

scenes 5 · The same colour, stretched, 6 · Where does one parcel land?, 9 · Ink adds, 10 · Light multiplies · id TV-INK

The stretching ribbon

Teaching visualization

Beat 5's schematic field and ribbon — shadows drawn about 60× wider than real forest lines, the low-redshift forest exaggerated, Lyα only

The physics
wavelengths and redshifts on the ribbon are computed (Redshift, distance and observed wavelength)
Teaching representation
The stretching ribbon and the written shadows (Beat 5 representation)

scenes 5 · The same colour, stretched · id TV-RIBBON

Animation and timing

Teaching visualization

reveals, holds and replays; their speed and duration have no physical meaning (atoms' motions are slowed enormously so they can be seen)

The physics
every drawn state is a computed physical state; time is only how it is shown

scenes 0 · Gas occupies real space, 2 · One atom meets one colour, 3 · Temperature becomes line width, 5 · The same colour, stretched, 7 · Distance and observed colour are not the same coordinate, 9 · Ink adds, 12 · Who ate this colour?, 13 · Same shadow · id TV-TIMING

The red colour

Teaching visualization

cinnabar red marks the current causal focus — what the reader is acting on and its consequence — never a physical category

The physics
colour carries no physical value

scenes 0 · Gas occupies real space, 1 · Same gas, different scale, 2 · One atom meets one colour, 3 · Temperature becomes line width, 4 · The atom's lifetime leaves faint, far wings, 5 · The same colour, stretched, 6 · Where does one parcel land?, 7 · Distance and observed colour are not the same coordinate, 8 · From parcels back to a continuous sightline, 9 · Ink adds, 10 · Light multiplies, 11 · A forest emerges, 12 · Who ate this colour?, 13 · Same shadow · id TV-FOCUS

Claims by scene

References

  1. Becker & Bolton 2013. Becker, G. D. & Bolton, J. S. 2013, MNRAS 436, 1023–1039, 'New measurements of the ionizing ultraviolet background over 2 < z < 5 and implications for hydrogen reionization' DOI · arXiv
  2. Becker et al. 2011. Becker, G. D., Bolton, J. S., Haehnelt, M. G. & Sargent, W. L. W. 2011, MNRAS 410, 1096–1112, 'Detection of extended He II reionization in the temperature evolution of the intergalactic medium' DOI · ADS · arXiv
  3. Becker et al. 2013. Becker, G. D., Hewett, P. C., Worseck, G. & Prochaska, J. X. 2013, MNRAS 430, 2067–2081, 'A refined measurement of the mean transmitted flux in the Lyα forest over 2 < z < 5 using composite quasar spectra' DOI · arXiv
  4. Bird et al. 2015. Bird, S., Haehnelt, M., Neeleman, M., Genel, S., Vogelsberger, M. & Hernquist, L. 2015, MNRAS 447, 1834–1846, 'Reproducing the kinematics of damped Lyman α systems' DOI · ADS · arXiv
  5. CODATA 2022 (Mohr et al. 2025). Mohr, P. J., Newell, D. B., Taylor, B. N. & Tiesinga, E. 2025, Rev. Mod. Phys. 97, 025002, 'CODATA recommended values of the fundamental physical constants: 2022' (also J. Phys. Chem. Ref. Data 54, 033105); values as served by NIST CUU DOI · link
  6. Dijkstra 2014. Dijkstra, M. 2014, PASA 31, e040, 'Lyα Emitting Galaxies as a Probe of Reionisation' DOI · ADS · arXiv
  7. Draine 2011. Draine, B. T. 2011, Physics of the Interstellar and Intergalactic Medium (Princeton University Press)
  8. fake_spectra (Bird). fake_spectra (S. Bird), MIT license, https://github.com/sbird/fake_spectra; PyPI package 'fake_spectra' v2.2.4 link · PyPI
  9. Griesmann & Kling 2000. Griesmann, U. & Kling, R. 2000, ApJ 536, L113, 'Interferometric Measurement of Resonance Transition Wavelengths in C IV, Si IV, Al III, Al II, and Si II' DOI · arXiv
  10. Hogg 1999. Hogg, D. W. 1999, 'Distance measures in cosmology', arXiv:astro-ph/9905116 (v4, 16 Dec 2000); pedagogical note, not published in a journal DOI · arXiv
  11. Hui & Gnedin 1997. Hui, L. & Gnedin, N. Y. 1997, MNRAS 292, 27–42, 'Equation of state of the photoionized intergalactic medium' DOI · ADS · arXiv
  12. Hui, Gnedin & Zhang 1997. Hui, L., Gnedin, N. Y. & Zhang, Y. 1997, ApJ 486, 599–622, 'The Statistics of Density Peaks and the Column Density Distribution of the Lyα Forest' DOI · ADS · arXiv
  13. Humlíček 1982. Humlíček, J. 1982, J. Quant. Spectrosc. Radiat. Transfer 27, 437–444, 'Optimized computation of the Voigt and complex probability functions' DOI · ADS
  14. Karaçaylı et al. 2024. Karaçaylı, N. G. et al. 2024, MNRAS 528, 3941, 'Optimal 1D Lyα forest power spectrum estimation — III. DESI early data' DOI · ADS · arXiv
  15. McQuinn 2016. McQuinn, M. 2016, ARA&A 54, 313–362, 'The Evolution of the Intergalactic Medium' DOI · ADS · arXiv
  16. Meiksin 2009. Meiksin, A. A. 2009, Rev. Mod. Phys. 81, 1405–1469, 'The physics of the intergalactic medium' DOI · ADS · arXiv
  17. NIST ASD (Kramida et al. 2024). Kramida, A., Ralchenko, Yu., Reader, J., and NIST ASD Team (2024). NIST Atomic Spectra Database (ver. 5.12), https://physics.nist.gov/asd [accessed 2026-10-02]. National Institute of Standards and Technology, Gaithersburg, MD. link
  18. NIST isotopic compositions. NIST Atomic Weights and Isotopic Compositions with Relative Atomic Masses, https://physics.nist.gov/comp — relative atomic masses from 'The AME2012 Atomic Mass Evaluation' (Wang, M. et al. 2012, Chinese Physics C 36(12); DOI 10.1088/1674-1137/36/12/003 as given on the NIST page) link
  19. NIST isotopic compositions (silicon). NIST Atomic Weights and Isotopic Compositions with Relative Atomic Masses, https://physics.nist.gov/comp — silicon entry; relative atomic masses from 'The AME2012 Atomic Mass Evaluation' (Wang, M. et al. 2012, Chinese Physics C 36(12); DOI 10.1088/1674-1137/36/12/003 as given on the NIST page) link
  20. Palanque-Delabrouille et al. 2013. Palanque-Delabrouille, N., Yèche, Ch., Borde, A., Le Goff, J.-M., Rossi, G., et al. 2013, A&A 559, A85, 'The one-dimensional Lyα forest power spectrum from BOSS' DOI · ADS · arXiv
  21. Planck Collaboration 2020. Planck Collaboration (Aghanim, N. et al.) 2020, A&A 641, A6, 'Planck 2018 results. VI. Cosmological parameters'; Corrigendum A&A 652, C4 (2021) DOI · ADS · arXiv
  22. Rauch 1998. Rauch, M. 1998, ARA&A 36, 267–316, 'The Lyman Alpha Forest in the Spectra of Quasistellar Objects' DOI · ADS · arXiv
  23. Rauch et al. 1996. Rauch, M., Sargent, W. L. W., Womble, D. S. & Barlow, T. A. 1996, ApJ 467, L5, 'Temperature and Kinematics of C IV Absorption Systems' DOI · arXiv
  24. Rybicki & Lightman 1979. Rybicki, G. B. & Lightman, A. P. 1979, Radiative Processes in Astrophysics (New York: Wiley); Wiley-VCH reprint 2004 ISBN 978-0-471-82759-7
  25. SciPy scipy.special.wofz. SciPy scipy.special.wofz documentation (v1.15.2 docs page, the version used here; the v1.18.0 page is the same): w(z) = exp(−z²) erfc(−iz); Reference [1] Steven G. Johnson, Faddeeva W function implementation, http://ab-initio.mit.edu/Faddeeva link
  26. Springel 2005 (GADGET-2 user guide). Springel, V. 2005, 'User guide for GADGET-2' (dated May 1, 2005), https://wwwmpa.mpa-garching.mpg.de/gadget/users-guide.pdf link
  27. Wiese & Fuhr 2009. Wiese, W. L. & Fuhr, J. R. 2009, J. Phys. Chem. Ref. Data 38, 565–720, 'Accurate Atomic Transition Probabilities for Hydrogen, Helium, and Lithium'; Publisher's Note: J. Phys. Chem. Ref. Data 38, 1129 (2009) DOI · ADS
  28. Wolfe, Gawiser & Prochaska 2005. Wolfe, A. M., Gawiser, E. & Prochaska, J. X. 2005, ARA&A 43, 861–918, 'Damped Lyα Systems' DOI · ADS · arXiv
  29. Zaghloul 2015. Zaghloul, M. R. 2015, arXiv:1505.05596, 'A simple reform for treating the loss of accuracy of Humlicek's W4 algorithm near the real axis' arXiv

Cite and version

Lyα: from gas to forest — science notes, version 0.1.1. How to cite: see CITATION.cff in the source repository. Fonts and KaTeX are redistributed under their own licences: third-party licences.