Skip to content

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

\[ \text{Species} + h\nu \longrightarrow \text{Products} \]

The reaction rate per unit volume is

\[ \mathcal{R} = \prod_i {n_i}^{\alpha} \int_0^\infty \sigma(E_\nu)\, c\, \dfrac{ \partial n_\gamma}{\partial E_\nu}\, dE_{\nu} \]

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:

\[ \mathcal{R} = \prod_i {n_i}^{\alpha} \sum_i k_i \cdot \rho_i \]

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:

\[ n(E) \propto E^{\alpha - 2} \]

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}]\):

\[ \langle\sigma\rangle_i = \frac{\displaystyle\int_{E_\text{lo}}^{E_\text{hi}} \sigma(E_\nu)\, \dfrac{ \partial n_\gamma}{\partial E_\nu}\, dE_\nu}{\displaystyle\int_{E_\text{lo}}^{E_\text{hi}} \dfrac{ \partial n_\gamma}{\partial E_\nu}\, dE_\nu} \]

Rate coefficient

In photon-density mode (energy_density = false):

\[ k_i = c \cdot \langle\sigma\rangle_i \]

In energy-density mode (energy_density = true):

\[ k_i = \frac{c \cdot \langle\sigma\rangle_i}{\langle E\rangle_i} \]

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:

\[ \frac{dn_i}{dt} \supset -\prod_i {n_i}^{\alpha} \sum_i k_i \cdot \rho_i \]

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:

H -> H+ + E    []    PHOTO, 13.60

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 a photo_absorption flag, a decay_type ("ionization" / "dissociation") and a file.hdf5::<group> pointer into the Leiden or NORAD HDF5 file.
  • verner_cross_sections — the Verner analytic σ(E) expression as a SymPy-parseable string (symbol E, 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:

\[ k_i \;\longrightarrow\; k_i \cdot f_\text{sh} \]

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:

\[ f_\text{sh} = \prod_{s} f_s\big(N_s\big) \]

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

\[ f_\text{sh} = \frac{0.965}{(1 + x/b_5)^{\alpha}} + \frac{0.035}{(1 + x)^{1/2}}\, \exp\!\big(-8.5\times10^{-4}\,(1 + x)^{1/2}\big) \]

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