Skip to content

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:

source_spectra:
  file: /absolute/path/stellar_seds.h5
  outside_grid: zero

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.