Per-star ionizing spectra
ionizing_rt.source_spectra supplies full stellar wavelength spectra, joined to
retained catalogue emitters by their native unsigned 64-bit ParticleIDs:
The catalogue continues to supply each star's photon budget. THOR derives band fractions, photon-weighted HI/HeI/HeII cross sections, absorption-weighted excess energies, and photon-weighted mean energies from the supplied shape. Spectra are piecewise linear in L_lambda. Integration splits at wavelength nodes, band edges and atomic thresholds and uses 16-point Gauss quadrature on each segment. No atmosphere library or precomputed atomic cross sections are required in THOR.
Input contract
HDF5 root attributes: integer schema_version=1, string outside_grid="zero".
Datasets:
| Dataset | Shape / meaning |
|---|---|
particle_ids |
(sources,), unique uint64 IDs; never represented as floating point |
wavelength_angstrom |
(wavelengths,), finite, positive, strictly increasing |
luminosity_lambda_erg_s_angstrom |
(sources, wavelengths), finite nonnegative L_lambda |
Every retained emitter must match one row. Extra rows are allowed. Each SED must have positive photon content within the configured bands. Zero outside the grid is explicit in both configuration and file; no tail is extrapolated. Missing IDs, duplicates, malformed shapes, and unrepresentable coefficients fail on the host. An explicit spectrum is required for any sampled UVB when per-star spectra are active. Analytic UVB chemistry remains separate.
Sampling and transport
A common proposal is constructed from the retained emitters' SEDs, each weighted
by that emitter's catalogue photon rate (the generator luminosity that sets its
packet share). Its band fractions are therefore the physical aggregate stellar
fractions; SED rows not joined to a retained emitter do not influence it, and
accumulation in emitter order makes it independent of table row order.
Existing band boosts, schedules and adaptive allocation apply to that proposal.
An equal-star mixture let many faint cool stars dominate the proposal: for
snapshot 397 it drew about 21 times fewer packets above 24.59 eV than the
photon-rate weighting and raised the emission-weight second moment from about
1.01 to 3.8 (four bands). Both proposals are unbiased through p_star(b)/q(b).
Sampler bounds are validated once per emission; each packet draw is a binary
search over the integer-lattice bounds.
A stellar packet emitted in band b receives the factor p_star(b)/q(b) using its
own spectrum's photon fraction and the actual integer-lattice sampling
probability. This preserves each source's spectrum and the planned exposure
N_population*q(b) recorded in BandSamplingHistory. A draw in a band where that star
has zero emission carries zero weight and never enters transport. These draws
are retained in planned exposure and reported separately in
SourceBudget/aggregate_discrete_packets_zero_spectrum.
The shared immutable device table has six coefficients per spectrum/band. Each
packet carries a uint32 row index, independent of its stellar/UVB population ID.
All indices are validated before launch. At kernel entry, begin_packet copies
the row into the private interactor; it has no invalid-index return path.
Resuming a packet reloads the same row. The current ionizing path has no frequency
changes, so coefficients remain valid throughout the walk.
Estimators and outputs
Per-star runs retain seven moments per population and cell: photon flux, three
ionization-rate moments, and three heating-per-absorber moments. Each packet is
multiplied by its own coefficients before summation, so the moments are summed
over bands as they are deposited. Storage is therefore independent of both the
number of stars and the number of bands. The optional rate_smoothing applies
one iteration-weighted EMA to all seven moments; gas densities are applied to
heating only after smoothing, using the current state.
With packet_weighted_moments=1, RadiationBands has band_summed=1 and one
channel per transported population: photon_flux_cm2_s is (channel,cell),
rates come from photoionization_rate_s (species,channel,cell), and heating
from photoheating_eV_s_per_absorber with the same shape. Per-band
bin_index and cross_sections_cm2 are not written for these runs; the
per-band sampling proposal is in FrequencyGroups.
SourceSpectra records input IDs, emitter-row mapping, wavelength grid, all
coefficient rows and their mean energies. Rows are proposal bands, UVB bands,
then input SED bands. Frequency-bin summary coefficients are proposal diagnostics.
Escape luminosities sum each packet's source/band energy independently for
emitted and escaping weights, including same-state batches. They still do not
resolve hardening within one packet's band.
Verification
Unit tests cover exact band boundaries, zero tails, blackbody recovery, exact uint64 joins, missing/duplicate IDs, cache reuse/resumption, mixed-cell rate and heating deposition, and escaped-energy accounting.
regression_tests/CI/ionizing_stellar_spectra/check.py compares a tabulated
blackbody with the analytic control, mixed spectra with reordered rows, zero
spectral support, missing IDs, kernel resumption, and band-summed moment storage.
A two-stage sampling fixture checks history dimensions when the proposal changes. Its optional
--reference-seds checks all coefficients against independently integrated
reference moments from the VESTRAL export. The runner uses Apptainer and accepts
--thor, --image, and a new --output directory.