# SPDX-FileCopyrightText: 2026 John C. Travers and contributors
#
# SPDX-License-Identifier: MIT
r"""The parameters of the strong-field ionisation rates of the gases (TC-439).
Tunnelling and multiphoton ionisation rates (ADK, PPT, molecular ADK) are evaluated
from a handful of properties of the atom or molecule. This module supplies them,
curated, with their sources and uncertainties. The rates of the noble gases are
:mod:`optmat.gas.ionisation_rates` (TC-652); the molecules' stay with the propagation
code:
- the first **ionisation energy** :math:`I_p` (adiabatic or vertical, as chosen and
stated per gas);
- the **ionising orbital**: its label, orbital angular momentum :math:`l` (an atom's
valence shell) and magnetic quantum number :math:`m` along the molecular axis;
- its **structure coefficients** :math:`C_l`, the asymptotic expansion of the orbital
in partial waves (Tong, Zhao & Lin 2002, eqs. 1-2), from each curated tabulation;
- the **static polarizabilities** of the neutral and of its cation, for the Stark shift
of the ionisation energy and the dipole correction of the PPT rate. The neutral's is
the static limit of the electronic part of the fitted dispersion model here (the
infrared, nuclear part does not follow an optical field), with the fit's
uncertainty;
- the **barrier-suppression constant** of Tong & Lin (2005) where tabulated.
From :math:`I_p` follow the effective principal quantum number
:math:`n^* = Z / \kappa`, :math:`\kappa = \sqrt{2 I_p}` (atomic units, :math:`Z` the
charge of the ion), and the asymptotic coefficient of the ADK and PPT rates in the
Hartree approximation,
.. math::
|C_{n^* l^*}|^2
= \frac{2^{2 n^*}}{n^* \, \Gamma(n^* + l^* + 1) \, \Gamma(n^* - l^*)},
\qquad l^* = n^* - 1,
(Ammosov, Delone & Krainov 1986, eqs. 17 and 19, :math:`l^* = n_0^* - 1` with
:math:`n_0^*` the ground state's), and its Stirling form (their eq. 20, which is
misprinted; see :func:`adk_coefficient_squared`).
Every choice (which ionisation energy, which tabulation) is made in
``data/gas/ionisation.toml`` with its reason, and every value points to the curated
dataset it comes from.
"""
import functools
import math
import tomllib
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field, replace
import numpy as np
from optmat import _catalogue
from optmat.constants import AU_ENERGY, AU_POLARIZABILITY, E_CHARGE, HBAR, C
from optmat.datasets import Dataset
from optmat.exceptions import DataError
from optmat.gas import GAS_DATA_DIRECTORY
from optmat.gas.gases import Gas, as_gas
from optmat.gas.reduction import polarizability as dataset_polarizability
__all__ = [
"IONISATION_FILE",
"IonisationParameters",
"StructureCoefficients",
"adk_coefficient_squared",
"combined_structure_coefficients",
"electronic_static_polarizability",
"hartree_coefficient_squared",
"ionisation_parameters",
"structure_coefficients",
]
IONISATION_FILE = GAS_DATA_DIRECTORY / "ionisation.toml"
"""The per-gas choices of the ionisation parameters, each with its reason."""
_ENERGY_UNITS: dict[str, float] = {
"eV": E_CHARGE,
"hartree": AU_ENERGY,
# A wavenumber E / (h c) in cm^-1: h c in J cm.
"cm-1": 2 * math.pi * HBAR * C * 100,
}
[docs]
def hartree_coefficient_squared(n_star: float, l_star: float) -> float:
"""Return the asymptotic coefficient ``|C_{n* l*}|^2`` of the ADK and PPT rates.
Ammosov, Delone & Krainov (1986), eq. (19): the Coulomb asymptotic wave function
with the effective quantum numbers in place of ``n`` and ``l``.
Parameters
----------
n_star : float
Effective principal quantum number ``Z / sqrt(2 I_p)``, > 0.
l_star : float
Effective orbital quantum number, ``n* - 1`` in ADK's choice.
Returns
-------
float
``2^(2 n*) / (n* Gamma(n* + l* + 1) Gamma(n* - l*))``.
Examples
--------
Hydrogen (``n* = 1``, ``l* = 0``) gives 4, the square of ``C_0 = 2``:
>>> hartree_coefficient_squared(1.0, 0.0)
4.0
"""
return 2 ** (2 * n_star) / (
n_star * math.gamma(n_star + l_star + 1) * math.gamma(n_star - l_star)
)
[docs]
def adk_coefficient_squared(n_star: float, l_star: float) -> float:
"""Return the Stirling form of ``|C_{n* l*}|^2`` (Ammosov's eq. 20, corrected).
Stirling's formula for the two Gamma functions of eq. (19) gives
``(4 e^2 / (n*^2 - l*^2))^n* ((n* - l*) / (n* + l*))^(l* + 1/2) / (2 pi n*)``.
Ammosov, Delone & Krainov print eq. (20) with ``4 e`` for ``4 e^2`` and the ratio
inverted, ``(n* + l*) / (n* - l*)``: as printed it contradicts both their eq. (19)
and, at ``l* = 0``, their eq. (11) (TC-441).
Parameters
----------
n_star : float
Effective principal quantum number, > 0.
l_star : float
Effective orbital quantum number, ``|l*| < n*``.
Returns
-------
float
The corrected eq. (20).
Examples
--------
At ``l* = 0`` it is their eq. (11) squared, ``(2 e / n*)^(2 n*) / (2 pi n*)``:
>>> f"{adk_coefficient_squared(1.0, 0.0):.4f}"
'4.7040'
"""
return (
(4 * math.e**2 / (n_star**2 - l_star**2)) ** n_star
* ((n_star - l_star) / (n_star + l_star)) ** (l_star + 0.5)
/ (2 * math.pi * n_star)
)
[docs]
@dataclass(frozen=True)
class StructureCoefficients:
"""The asymptotic coefficients of an ionising orbital, from one tabulation.
Attributes
----------
source : str
The curated dataset's key.
model : str
The theory the coefficients belong to (``moadk``, ``adk``, ``wfat``).
orbital : str
The orbital, as the source names it.
m : int
The magnetic quantum number along the molecular axis (an atom's ``m``); 0
for a combination of several (a non-linear molecule).
coefficients : dict of int to float
``C_l`` by partial wave ``l``, as printed (signs kept). For a combination of
datasets (a non-linear molecule), the root of :attr:`partial_wave_squares`:
what the average over orientations needs, not the coefficients of an aligned
molecule.
normalisation : str
The source's definition of the coefficients.
projections : dict of (int, int) to complex
``C_lm`` by ``(l, m)`` and orbital index, as printed: the real part the value,
the imaginary part the dataset's ``imaginary`` column where it has one. Keys
are ``(orbital, l, m)``, the orbitals numbered in the order given.
"""
source: str
model: str
orbital: str
m: int
coefficients: Mapping[int, float]
normalisation: str
projections: Mapping[tuple[int, int, int], complex] = field(default_factory=dict)
@property
def partial_wave_squares(self) -> dict[int, float]:
r"""The sum over ``m`` of :math:`|C_{lm}|^2` per ``l``, averaged over orbitals.
By the orthogonality of the rotation matrices, the average over orientations
of the structure factor depends on the coefficients only through these sums
(:func:`optmat.gas.molecular_ionisation_rates.isotropic_structure_factors`).
The orbitals of a degenerate shell are rotations of one another and have the
same sums; they are averaged, and their number is part of the count of
electrons.
Returns
-------
dict of int to float
:math:`\langle \sum_m |C_{lm}|^2 \rangle` by ``l``.
"""
orbitals = {key[0] for key in self.projections}
squares: dict[int, float] = {}
for (_, l, _), value in self.projections.items(): # noqa: E741 - l is the partial wave
squares[l] = squares.get(l, 0.0) + abs(value) ** 2 / len(orbitals)
return dict(sorted(squares.items()))
[docs]
def structure_coefficients(dataset: Dataset) -> StructureCoefficients:
"""Read one tabulation of structure coefficients.
Parameters
----------
dataset : Dataset
A ``tunnelling_structure_coefficient`` dataset, one row per partial wave.
Returns
-------
StructureCoefficients
The coefficients by ``l``.
Raises
------
DataError
If the dataset holds another quantity or a row is not an integer ``l``.
"""
if dataset.quantity != "tunnelling_structure_coefficient":
msg = f"{dataset.key} is a {dataset.quantity}, not structure coefficients"
raise DataError(msg)
try:
partial_waves = [int(label) for label in dataset.abscissae]
except ValueError as error:
msg = f"{dataset.key}: every row label must be a partial wave l"
raise DataError(msg) from error
values = dataset.scaled_values()
imaginary = (
dataset.column_values("imaginary")
if "imaginary" in dataset.columns
else np.zeros(len(values))
)
parameters = dataset.parameters
m = int(str(parameters["m"]))
return StructureCoefficients(
source=dataset.key,
model=str(parameters["model"]),
orbital=str(parameters["orbital"]),
m=m,
coefficients=dict(zip(partial_waves, map(float, values), strict=True)),
normalisation=str(parameters["normalisation"]),
projections={
(0, l, m): complex(float(re), float(im))
for l, re, im in zip(partial_waves, values, imaginary, strict=True) # noqa: E741 - l is the partial wave
},
)
[docs]
def combined_structure_coefficients(
tabulation: Sequence[Dataset],
) -> StructureCoefficients:
"""Read the structure coefficients of an orbital given in several datasets.
A non-linear molecule's orbital has coefficients at several ``m`` for each ``l``,
and a degenerate shell several orbitals: one dataset per orbital and ``m``
(the parameter ``orbital`` naming the orbital). The result keeps every
coefficient in :attr:`StructureCoefficients.projections`; its ``coefficients``
are the roots of :attr:`StructureCoefficients.partial_wave_squares`.
Parameters
----------
tabulation : sequence of Dataset
``tunnelling_structure_coefficient`` datasets of one source.
Returns
-------
StructureCoefficients
The combination.
Raises
------
DataError
If a dataset holds another quantity, or two hold the same orbital and ``m``.
"""
parts = [structure_coefficients(dataset) for dataset in tabulation]
orbitals = list(dict.fromkeys(part.orbital for part in parts))
projections: dict[tuple[int, int, int], complex] = {}
for part in parts:
index = orbitals.index(part.orbital)
for (_, l, m), value in part.projections.items(): # noqa: E741 - l is the partial wave
if (index, l, m) in projections:
msg = f"{part.source}: (l, m) = ({l}, {m}) of {part.orbital} twice"
raise DataError(msg)
projections[index, l, m] = value
combined = StructureCoefficients(
source=", ".join(part.source for part in parts),
model=parts[0].model,
orbital=" / ".join(orbitals),
m=0,
coefficients={},
normalisation=parts[0].normalisation,
projections=projections,
)
return replace(
combined,
coefficients={
l: math.sqrt(square)
for l, square in combined.partial_wave_squares.items() # noqa: E741 - l is the partial wave
},
)
[docs]
def electronic_static_polarizability(gas: Gas | str) -> tuple[float, float, str]:
"""Return the static electronic polarizability of a gas from its fitted model.
The fitted model's terms are the electronic oscillator strengths, then the fixed
infrared (vibrational) bands and the magnetic term. The electronic static limit is
the sum of the fitted terms' ``f_i / E_i^2`` alone: in an optical field the nuclei
do not follow, so a Stark shift sees the electronic polarizability (TC-441).
Parameters
----------
gas : Gas or str
The gas.
Returns
-------
tuple of (float, float, str)
The polarizability and its one-sigma uncertainty (Birge-scaled), C m^2/V, and
the model's name.
"""
# Loaded here: the fitted models bring the whole dispersion chain.
from optmat.gas.models.fitted import FittedModel, fitted_models # noqa: PLC0415
model: FittedModel = fitted_models()[f"fit/{as_gas(gas).value}"]
n_fitted = len(model.strengths) - model.n_fixed
strengths = np.asarray(model.strengths[:n_fitted])
energies = np.asarray(model.energies[:n_fitted])
alpha_au = float(np.sum(strengths / energies**2))
sigma_au = float(model.polarizability_sigma_au(np.zeros(1))[0])
return alpha_au * AU_POLARIZABILITY, sigma_au * AU_POLARIZABILITY, model.name
[docs]
@dataclass(frozen=True)
class IonisationParameters:
"""The curated parameters of a gas's strong-field ionisation rates.
Attributes
----------
gas : Gas
The gas.
ionization_energy : float
First ionisation energy, J.
ionization_energy_sigma : float
Its one-sigma uncertainty, J; NaN where the source states none.
ionization_energy_kind : str
``adiabatic`` or ``vertical`` (the two coincide for an atom).
ionization_energy_source : str
The dataset key.
orbital : str
The ionising orbital.
orbital_l : int or None
The orbital angular momentum of an atom's valence shell (the ``l`` of the ADK
and PPT angular factor); None for a molecule.
orbital_m : int
The magnetic quantum number of the orbital (along a molecule's axis).
structure : dict of str to StructureCoefficients
The curated tabulations of the orbital's coefficients, by name.
default_structure : str or None
The name of the recommended tabulation, or None where none is published.
polarizability : float
Static electronic polarizability of the neutral, C m^2/V.
polarizability_sigma : float
Its one-sigma uncertainty, C m^2/V.
polarizability_source : str
The model it is the static limit of.
vertical_ionization_energy : float or None
The vertical ionisation energy of a molecule, J, where it differs and is
curated: the energy at the neutral's geometry, which a tunnelling rate in the
fixed-nuclei approximation sees (TC-444).
vertical_ionization_energy_source : str
The dataset key, or an empty string.
ion_polarizability : float or None
Static polarizability of the cation, C m^2/V, where curated.
ion_polarizability_source : str
The dataset key, or an empty string.
barrier_suppression_alpha : float or None
Tong & Lin's (2005) alpha, where tabulated.
barrier_suppression_source : str
The dataset key, or an empty string.
barrier_suppression_zhang : tuple of float or None
Zhang, Lan & Lu's (2014) a1, a2, a3: the rate is ``W_ADK exp(a1 x^2 + a2 x +
a3)`` with ``x = F / F_b`` (the sign of their eq. 8 corrected, TC-542).
barrier_suppression_zhang_source : str
The dataset key, or an empty string.
effective_charge : float or None
Talebpour, Yang & Chin's (1999) effective charge of N2 or O2: a constant fitted
within their PPT rate with l = m = 0, not a property of the molecule (for O2
fitted with I_p = 12.55 eV, TC-542).
effective_charge_source : str
The dataset key, or an empty string.
reasons : dict of str to str
Why each choice was made (from ``ionisation.toml``).
"""
gas: Gas
ionization_energy: float
ionization_energy_sigma: float
ionization_energy_kind: str
ionization_energy_source: str
orbital: str
orbital_l: int | None
orbital_m: int
structure: Mapping[str, StructureCoefficients]
default_structure: str | None
polarizability: float
polarizability_sigma: float
polarizability_source: str
vertical_ionization_energy: float | None = None
vertical_ionization_energy_source: str = ""
ion_polarizability: float | None = None
ion_polarizability_source: str = ""
barrier_suppression_alpha: float | None = None
barrier_suppression_source: str = ""
barrier_suppression_zhang: tuple[float, float, float] | None = None
barrier_suppression_zhang_source: str = ""
effective_charge: float | None = None
effective_charge_source: str = ""
reasons: Mapping[str, str] = field(default_factory=dict)
@property
def ionization_energy_au(self) -> float:
"""The ionisation energy in hartree.
Returns
-------
float
``I_p / E_h``.
"""
return self.ionization_energy / AU_ENERGY
@property
def kappa(self) -> float:
"""``kappa = sqrt(2 I_p)`` in atomic units.
Returns
-------
float
The decay constant of the orbital's asymptotic tail, 1/a0.
"""
return math.sqrt(2 * self.ionization_energy_au)
@property
def n_star(self) -> float:
"""The effective principal quantum number ``Z / kappa`` of a singly charged ion.
Returns
-------
float
``1 / sqrt(2 I_p)`` with ``I_p`` in hartree.
"""
return 1.0 / self.kappa
@property
def l_star(self) -> float:
"""The effective orbital quantum number of ADK, ``n* - 1`` (Ammosov, eq. 17).
Returns
-------
float
``n* - 1``.
"""
return self.n_star - 1.0
@property
def hartree_coefficient_squared(self) -> float:
"""The PPT and ADK ``|C_{n* l*}|^2`` in the Hartree approximation.
Returns
-------
float
:func:`hartree_coefficient_squared` at ``n*`` and ``l* = n* - 1``.
"""
return hartree_coefficient_squared(self.n_star, self.l_star)
@property
def polarizability_difference(self) -> float | None:
"""The neutral's polarizability less the cation's, C m^2/V.
In a static field ``F`` each state's energy falls by ``alpha F^2 / 2``, so the
ionisation energy rises by ``(alpha_0 - alpha_+) F^2 / 2``: the Stark shift of
the PPT rate.
Returns
-------
float or None
``alpha_0 - alpha_+``, or None where the cation's is not curated.
"""
if self.ion_polarizability is None:
return None
return self.polarizability - self.ion_polarizability
def _dataset(by_key: Mapping[str, Dataset], key: str) -> Dataset:
"""Return a whole dataset named in ``ionisation.toml``.
Parameters
----------
by_key : mapping of str to Dataset
The curated datasets.
key : str
``source/dataset``.
Returns
-------
Dataset
The dataset.
Raises
------
DataError
If it does not exist.
"""
if key not in by_key:
msg = f"ionisation.toml names an unknown dataset {key!r}"
raise DataError(msg)
return by_key[key]
def _row(by_key: Mapping[str, Dataset], reference: str) -> tuple[Dataset, int]:
"""Return a dataset and a row index from ``key`` or ``key#row``.
Parameters
----------
by_key : mapping of str to Dataset
The curated datasets.
reference : str
``source/dataset`` or ``source/dataset#row``.
Returns
-------
tuple of (Dataset, int)
The dataset and the index of the row (0 when it has one row).
Raises
------
DataError
If the dataset or the row does not exist, or a row is needed and not given.
"""
key, _, row = reference.partition("#")
if key not in by_key:
msg = f"ionisation.toml names an unknown dataset {key!r}"
raise DataError(msg)
dataset = by_key[key]
if not row:
if len(dataset) != 1:
msg = f"{key} has {len(dataset)} rows: name one as {key}#<row>"
raise DataError(msg)
return dataset, 0
if row not in dataset.rows:
msg = f"{key} has no row {row!r}"
raise DataError(msg)
return dataset, dataset.rows.index(row)
def _energy(dataset: Dataset, index: int) -> tuple[float, float]:
"""Return an ionisation energy and its uncertainty in J.
Parameters
----------
dataset : Dataset
An ``ionization_energy`` dataset.
index : int
The row.
Returns
-------
tuple of (float, float)
The energy and its one-sigma uncertainty (NaN when unstated), J.
Raises
------
DataError
If the dataset is not an ionisation energy.
"""
if dataset.quantity != "ionization_energy":
msg = f"{dataset.key} is a {dataset.quantity}, not an ionization_energy"
raise DataError(msg)
factor = _ENERGY_UNITS[dataset.unit]
value = float(dataset.scaled_values()[index]) * factor
sigma = float(dataset.standard_uncertainty()[index]) * factor
return value, sigma
def _strings(entry: Mapping[str, object], name: str) -> dict[str, str]:
"""Return a sub-table of an entry as strings.
Parameters
----------
entry : mapping
A gas's table in ``ionisation.toml``.
name : str
The sub-table.
Returns
-------
dict of str to str
Its keys and values, or an empty dict when absent.
Raises
------
DataError
If it is not a table.
"""
table = entry.get(name, {})
if not isinstance(table, Mapping):
msg = f"ionisation.toml: {name} must be a table"
raise DataError(msg)
return {str(k): str(v) for k, v in table.items()} # pyright: ignore[reportUnknownVariableType]
def _structures(
entry: Mapping[str, object], by_key: Mapping[str, Dataset]
) -> dict[str, StructureCoefficients]:
"""Read a gas's tabulations of structure coefficients.
Parameters
----------
entry : mapping
Its table in ``ionisation.toml``.
by_key : mapping of str to Dataset
The curated datasets.
Returns
-------
dict of str to StructureCoefficients
The tabulations by name: one dataset, or a list of them combined
(:func:`combined_structure_coefficients`).
Raises
------
DataError
If ``structure`` is not a table, or a named dataset does not exist.
"""
tables = entry.get("structure", {})
if not isinstance(tables, Mapping):
msg = "ionisation.toml: structure must be a table"
raise DataError(msg)
structure: dict[str, StructureCoefficients] = {}
for name, key in tables.items(): # pyright: ignore[reportUnknownVariableType]
if isinstance(key, list):
structure[str(name)] = combined_structure_coefficients(
[_dataset(by_key, str(k)) for k in key] # pyright: ignore[reportUnknownVariableType, reportUnknownArgumentType]
)
else:
structure[str(name)] = structure_coefficients(_dataset(by_key, str(key)))
return structure
def _parameters(
gas: Gas, entry: Mapping[str, object], by_key: Mapping[str, Dataset]
) -> IonisationParameters:
"""Build one gas's parameters from its entry in ``ionisation.toml``.
Parameters
----------
gas : Gas
The gas.
entry : mapping
Its table in ``ionisation.toml``.
by_key : mapping of str to Dataset
The curated datasets.
Returns
-------
IonisationParameters
The parameters.
Raises
------
DataError
If a named dataset does not exist, holds another material or quantity, or the
default tabulation is not among the tabulations.
"""
reasons = _strings(entry, "reasons")
energy_dataset, energy_row = _row(by_key, str(entry["ionization_energy"]))
if energy_dataset.material != gas.value:
msg = f"{energy_dataset.key} is {energy_dataset.material}'s, not {gas.value}'s"
raise DataError(msg)
energy, energy_sigma = _energy(energy_dataset, energy_row)
vertical = None
vertical_source = ""
if "vertical_ionization_energy" in entry:
vertical_source = str(entry["vertical_ionization_energy"])
vertical_dataset, vertical_row = _row(by_key, vertical_source)
if (
vertical_dataset.material != gas.value
or vertical_dataset.parameters.get("kind") != "vertical"
):
msg = f"{vertical_dataset.key} is not a vertical energy of {gas.value}"
raise DataError(msg)
vertical = _energy(vertical_dataset, vertical_row)[0]
structure = _structures(entry, by_key)
default = entry.get("default_structure")
if default is not None and default not in structure:
msg = f"{gas.value}: default_structure {default!r} is not a tabulation"
raise DataError(msg)
alpha, alpha_sigma, alpha_source = electronic_static_polarizability(gas)
ion_alpha = None
ion_source = ""
if "ion_polarizability" in entry:
ion_source = str(entry["ion_polarizability"])
ion_dataset, ion_row = _row(by_key, ion_source)
if ion_dataset.material != f"{gas.value}+":
msg = f"{ion_dataset.key} is not {gas.value}+'s polarizability"
raise DataError(msg)
ion_alpha = float(dataset_polarizability(ion_dataset)[ion_row])
bsi = None
bsi_source = ""
if "barrier_suppression_alpha" in entry:
bsi_source = str(entry["barrier_suppression_alpha"])
bsi_dataset, bsi_row = _row(by_key, bsi_source)
bsi = float(bsi_dataset.scaled_values()[bsi_row])
zhang = None
zhang_source = ""
if "barrier_suppression_zhang" in entry:
zhang_source = str(entry["barrier_suppression_zhang"])
values = _dataset(by_key, zhang_source).scaled_values()
zhang = (float(values[0]), float(values[1]), float(values[2]))
charge = None
charge_source = ""
if "effective_charge" in entry:
charge_source = str(entry["effective_charge"])
charge_dataset, charge_row = _row(by_key, charge_source)
charge = float(charge_dataset.scaled_values()[charge_row])
orbital_l = entry.get("orbital_l")
return IonisationParameters(
gas=gas,
ionization_energy=energy,
ionization_energy_sigma=energy_sigma,
ionization_energy_kind=str(energy_dataset.parameters["kind"]),
ionization_energy_source=str(entry["ionization_energy"]),
orbital=str(entry["orbital"]),
orbital_l=None if orbital_l is None else int(str(orbital_l)),
orbital_m=int(str(entry.get("orbital_m", 0))),
structure=structure,
default_structure=None if default is None else str(default),
polarizability=alpha,
polarizability_sigma=alpha_sigma,
polarizability_source=alpha_source,
vertical_ionization_energy=vertical,
vertical_ionization_energy_source=vertical_source,
ion_polarizability=ion_alpha,
ion_polarizability_source=ion_source,
barrier_suppression_alpha=bsi,
barrier_suppression_source=bsi_source,
barrier_suppression_zhang=zhang,
barrier_suppression_zhang_source=zhang_source,
effective_charge=charge,
effective_charge_source=charge_source,
reasons=reasons,
)
@functools.cache
def _entries() -> dict[Gas, Mapping[str, object]]:
"""Read ``ionisation.toml``.
Returns
-------
dict of Gas to mapping
Each gas's entry.
"""
with IONISATION_FILE.open("rb") as handle:
table = tomllib.load(handle)
return {as_gas(name): entry for name, entry in table.items()}
@functools.cache
def _gas_parameters(species: Gas) -> IonisationParameters:
"""Build one gas's parameters, reading only the datasets it cites.
Parameters
----------
species : Gas
A gas of ``ionisation.toml``.
Returns
-------
IonisationParameters
Its parameters.
"""
return _parameters(species, _entries()[species], _catalogue.by_key())
[docs]
def ionisation_parameters(gas: Gas | str) -> IonisationParameters:
"""Return the curated ionisation parameters of a gas.
Parameters
----------
gas : Gas or str
The gas.
Returns
-------
IonisationParameters
Its parameters, each with its source.
Raises
------
DataError
If the gas has no ionisation parameters.
"""
species = as_gas(gas)
if species not in _entries():
msg = f"no ionisation parameters for {species.value}"
raise DataError(msg)
return _gas_parameters(species)