Photochemistry
JAFF provides first-class support for photodissociation and photoionisation reactions. This page explains the physical model, how JAFF reads cross-section data, and how rate coefficients are derived from a discretised radiation field.
Overview
A photochemical reaction has the form
The reaction rate per unit volume is
where \(\sigma(E_\nu)\) is the reaction cross section (\(\text{cm}^2\)), \(c\) is the speed of light (\(\text{cm s}^{-1}\)), and \(n_\gamma\) is the photon number density (\(\text{cm}^{-3}\)). The subscript \(\gamma\) signifies the local radiation field and a subscript of \(\nu\) signifies the frequency of the photon. JAFF uses high-resolution cross-section tables from the Leiden Observatory Database and the NORAD Database for photo-dissociaton and photo-ionization reactions respectively to store and calculate band averaged cross-sections.
When the radiation energy density per band is calculated, JAFF replaces the continuous integral with a sum over discrete radiation frequency bands:
where \(\rho_i\) is the radiation energy or photon density in band \(i\) and \(k_i\) is a pre-computed rate coefficient for that band.
Radiation source terms
If a reaction adds photons to the local radiation field, it can be specified using a custom deltaRad<N> function. Details are mentioned in Auxiliary functions file
Radiation Field Discretisation
The radiation field is divided into contiguous energy bands. You specify the band boundaries in jaff.toml (see Configuration File):
[radiation]
bands = [13.6, "inf"] # band edges in eV; "inf" for open upper bound
power_law_index = 0 # photon-number spectrum index α
energy_density = false # use photon density (false) or energy density (true)
rsl = 2.99792458e10 # speed of light (cm/s)
Photon spectrum
The photon number density spectrum is assumed to be a power law in photon energy:
where \(\alpha\) is power_law_index. Setting \(\alpha = 0\) gives \(n(E) \propto E^{-2}\), i.e. equal energy per logarithmic bin which is the default assumption.
Band-averaged cross section
For band \(i\) spanning \([E_\text{lo}, E_\text{hi}]\):
Rate coefficient
In photon-density mode (energy_density = false):
In energy-density mode (energy_density = true):
where \(\langle E\rangle_i\) is the photon-number–weighted average energy in the band.
Generated Rate Expressions
After band integration, each photochemical reaction contributes one term per band to the ODE right-hand side:
The generated code uses the radiation density array den[i] as the runtime variable for \(\rho_i\). The ODE for the radiation field itself can be obtained from Network.sradodes().
Marking a Reaction as Photochemical
In the PRIZMO format, append PHOTO followed by the energy threshold (eV) instead of a rate expression:
JAFF will look up the matching cross section in its bundled database and integrate it over the configured radiation bands.
Cross-Section Data
JAFF bundles cross sections from three sources. At network-load time the
serialized reaction key (Reactant1_Reactant2__Product1_Product2, the
serialized form of the reaction) is looked
up in jaff.db, and the cross-section arrays are attached to the reaction's
xsecs_dict.
| Source | Processes | Notes |
|---|---|---|
| Leiden PDR database (van Dishoeck et al.) | photoabsorption, photodissociation, photoionization | Tabulated cross sections |
| NORAD / OP (Nahar, OSU) | photoionization (ground state) | Tabulated, per ion Z = 1..26 |
| Verner et al. 1996 (ADS) | photoionization | Analytic fits σ(E) |
Storage layout
Tabulated cross sections are stored as two collapsed HDF5 files, one group per
serialized reaction, all datasets co-sorted by ascending photon energy. These
files are not bundled in the package: on first network load JAFF downloads them
(via pooch) from a remote mirror into the local data/xsecs directory, then
reuses the cached copies on subsequent runs:
src/jaff/data/xsecs/leiden.hdf5 # one group per reaction (absorption + decay)
src/jaff/data/xsecs/norad.hdf5 # one group per reaction (ionization decay)
src/jaff/data/xsecs/verner_1996.csv # analytic-fit parameters
photon_energy is stored in eV and every cross-section dataset
(photoabsorption / photodecay) in cm², where photodecay is the
reaction's single ionization-or-dissociation channel.
These assets feed two SQLite tables in jaff.db, which is what JAFF actually
queries at runtime (see Codebase Structure):
photo_reaction_cross_sections— one row per reaction, with aphoto_absorptionflag, adecay_type("ionization"/"dissociation") and afile.hdf5::<group>pointer into the Leiden or NORAD HDF5 file.verner_cross_sections— the Verner analytic σ(E) expression as a SymPy-parseable string (symbolE, photon energy in erg, σ in cm²).
What lands on the reaction
For tabulated sources, reaction.xsecs_dict is an XsecsProps dict carrying
the photon_energy grid (eV) plus photo_absorption and the single
photodecay channel (cm² arrays, or None); _equations["decay_type"]
records whether that channel is ionization or dissociation. The radiation
integrator reads these arrays directly and integrates them numerically over
each band; for photoionization it falls back to the Verner analytic fit when no
tabulated entry exists.
Threshold energy
The PHOTO, <eV> threshold in the network file selects which band edges
apply to a reaction. Reactions whose threshold lies above the upper edge of
a band are not assigned a rate coefficient for that band.
Shielding
Photo-rates computed from cross sections assume an unattenuated radiation field. In a real cloud the photons that drive a reaction are absorbed by intervening gas — including the dissociating species shielding itself ("self-shielding"). JAFF models this with a dimensionless shielding factor \(f_\text{sh} \in [0, 1]\) that multiplies the rate coefficient:
The factor is applied to every band of a reaction (it multiplies each
band's \(k_i\) before the band sum), so a single \(f_\text{sh}\) attenuates the
whole reaction rate. \(f_\text{sh}\) is a symbolic expression in runtime
quantities — column densities (ncol_<species>) and, for H2, the velocity
dispersion (vdisp) — that the host code supplies at integration time.
When a shielding function depends on several shielding species, the per-species factors are multiplied together:
Enabling shielding
Shielding is opt-in per reaction, declared in jaff.toml under
[reaction.<serialized>.shielding] (see the
configuration reference).
The reaction must be a photo-reaction; the type key selects the shielding
function (default "leiden").
# Leiden tabulated line shielding for CO photodissociation
[reaction.CO__C_O.shielding]
type = "leiden"
radiation = "ISRF" # radiation-field subgroup (default "ISRF")
shielded_by = ["self", "H2"] # shielding species; "self" = the reactant (CO)
# H2 self-shielding via the Hartwig et al. (2015) fit
[reaction.H2__H_H.shielding]
type = "hg2015"
min_ncol = 1.0e-35 # optional floors (see below)
min_vdisp = 1.0e-20
Shielding types
type |
Function | Reactions | Reference |
|---|---|---|---|
"leiden" |
Leiden tabulated tables | any photo-reaction | Leiden photodissociation database; Heays et al. 2017, A&A 602, A105 |
"db1996" |
H2 self-shielding fit (\(\alpha = 2\)) | H2__H_H |
Draine & Bertoldi 1996, ApJ 468, 269 (DOI 10.1086/177689) |
"hg2015" |
H2 self-shielding fit (\(\alpha = 1.1\)) | H2__H_H |
Hartwig et al. 2015, MNRAS 452, 1233 (DOI 10.1093/mnras/stv1368) |
Leiden tabulated shielding (type = "leiden")
The shielding factor is read from the collapsed Leiden tables
(data/shielding/leiden.hdf5, one group per reaction, downloaded on first use
alongside the cross sections). Parameters:
| Key | Required | Default | Description |
|---|---|---|---|
shielded_by |
yes | — | List of shielding species. Allowed: "self" (the reactant's own column), "H2", "H", "C", "N2", "CO". |
radiation |
no | "ISRF" |
Radiation-field subgroup in the table (e.g. "ISRF", "bb-10000", "Ly-alpha"). |
For each species in shielded_by, JAFF emits a per-reaction
shielding_<reaction>.hdf5 table next to the generated code and one
interpolation call
interp_<index>_shielding_<species>(ncol_<species>); the total factor is their
product. "self" resolves to the reaction's reactant, so it interpolates over
that species' own column density (e.g. ncol_CO for CO__C_O).
H2 self-shielding (type = "db1996" / "hg2015")
Both apply only to the H2__H_H (H2 → H + H) reaction and evaluate the
standard three-term analytic fit
with \(x = N_{\text{H2}}/5\times10^{14}\,\text{cm}^{-2}\) and
\(b_5 = b/10^5\,\text{cm s}^{-1}\), where the Doppler parameter
\(b = \sqrt{2}\,\sigma_v\). The exponent \(\alpha\) is the only difference between
the two: \(\alpha = 2\) for db1996, \(\alpha = 1.1\) for hg2015. The runtime
inputs are the H2 column density ncol_H2 and velocity dispersion vdisp.
| Key | Required | Default | Description |
|---|---|---|---|
min_ncol |
no | 1e-50 |
Lower floor applied in the fit to avoid a zero denominator (cm⁻²). |
min_vdisp |
no | 1e-50 |
Lower floor applied in the fit to avoid a zero denominator (cm s⁻¹). |
Shielding is exposed programmatically too
jaff.physics.Photochemistry.shielding(reaction, network) returns the
symbolic factor and caches it on
reaction.metadata["shielding"]["value"]; the radiation integrator reuses
that cached value across bands.
Python API
from jaff import Network
# Enable photochemistry by declaring radiation bands
net = Network(
"networks/h_photoionization/h_photo.jet",
rad_bands=[13.6, float("inf")], # band edges in eV
rad_powerlaw_index=0,
rad_energy_density=False,
)
# Inspect photochemical reactions
photo = net.reactions.photo_reactions()
for rxn in photo:
print(rxn.verbatim)
# Cross sections are attached at load time
rxn = photo[0]
rxn.xsecs_dict["photon_energy"] # eV grid
rxn.xsecs_dict["photodecay"] # cm² array (or None)
rxn.plot_xsecs() # visualise σ(E)
# Symbolic radiation ODEs
print(net.sradodes())
Cross-section lookup is also exposed directly via the
jaff.physics.Photochemistry class. Constructing it downloads the cross-section
and line-shielding data files on first use (cached thereafter), so instantiate
once and reuse:
from jaff.physics import Photochemistry
photo = Photochemistry()
photo.get_xsec(rxn) # XsecsProps from the tabulated databases
photo.get_verner_xsec(rxn) # analytic Verner σ(E) (sympy) or None