Source code for optmat.gas.two_photon

# SPDX-FileCopyrightText: 2026 John C. Travers and contributors
#
# SPDX-License-Identifier: MIT

r"""Two-photon ionisation of the noble gases: the continuum model (OEP 6, Phase 1).

The generalised two-photon ionisation cross section :math:`\sigma^{(2)}` of a gas's
ground state is defined by the rate per atom

.. math:: W = \sigma^{(2)} \Phi^2, \qquad \Phi = I / \hbar\omega,

with :math:`\Phi` the photon flux; in SI, :math:`\sigma^{(2)}` is in m^4 s (1 cm^4 s
= 1e-8 m^4 s). A quasi-monochromatic wave in a gas of number density :math:`N` then
loses intensity as :math:`dI/dz = -\beta I^2` with :math:`\beta = 2 N \sigma^{(2)} /
\hbar\omega`, two photons per event.

Each model is a curated reference curve that the author approved under data rule 8
(TC-943), interpolated linearly in :math:`\log\sigma^{(2)}` between the curve's own
drawn vertices and never fitted or extrapolated:

- helium: Mihelič and Horvat (2021), the 1S and 1D partial cross sections summed;
- neon and argon: McKenna and van der Hart (2004), R-matrix Floquet;
- xenon: Lagutin et al. (2017), linear and circular polarisation.

Krypton has none: no vector-drawn or tabulated cross section is curated (OEP 6, Q2),
and asking for it raises :class:`~optmat.exceptions.UnavailableModelError`.

A model covers the window from the lowest two-photon threshold :math:`I_p/2` to the
first one-photon resonance :math:`E_1`, and states its regions
(:attr:`~TwoPhotonIonisationModel.regions`):

- ``below_threshold``: the direct continuum cross section is zero. Bound two-photon
  excitation and (2+1) ionisation there are not modelled (OEP 6, Phase 2).
- ``supported``: the reference's values.
- ``unsupported_detail``: the reference's values, with
  :class:`~optmat.exceptions.ModelRangeWarning`. The source does not qualify them: an
  LS-coupled calculation between the ion's two fine-structure thresholds, or the wing
  of the first one-photon resonance, which the sources place differently.
- ``unavailable``: no value. The reference does not reach it (between xenon's two
  thresholds, for instance), or it is at or above :math:`E_1`.

Evaluation never raises on a frequency grid. It returns NaN where the model is
unavailable, and :meth:`~TwoPhotonIonisationModel.available` gives the mask (the
author's decision, TC-943). A propagation code decides what to do there.

The model supplies the degenerate cross section. It does not supply a time-resolved
loss for a broadband pulse, nor the mixed pairs of two bands. The pair-spectral yield
of OEP 6, section 4, is an integral identity in a stated approximation, and its time
dependence is a documented gap (TC-903). Nor does it double as a strong-field rate:
applying it and :func:`~optmat.gas.ionisation_rates.ionisation_rate_model` to the same
spectral component counts that component twice.

Examples
--------
>>> import math
>>> from optmat.constants import C
>>> argon = two_photon_ionisation_model("Ar")
>>> [round(2 * math.pi * C / t.omega * 1e9, 2) for t in argon.thresholds]
[157.34, 155.59]
>>> omega = 2 * math.pi * C / 120e-9  # 120 nm
>>> f"{float(argon.cross_section(omega)):.3e}"  # m^4 s
'1.146e-58'
>>> bool(argon.available(2 * math.pi * C / 100e-9))  # beyond E1 (106.7 nm)
False
"""

import functools
import itertools
import json
import math
import tomllib
import warnings
from collections.abc import Mapping
from dataclasses import dataclass, field, fields, is_dataclass
from typing import Any, ClassVar, Final

import numpy as np
from jaxtyping import Float
from numpy.typing import ArrayLike

from optmat.constants import HBAR
from optmat.exceptions import (
    ArgumentError,
    DataError,
    ModelRangeWarning,
    UnavailableModelError,
)
from optmat.gas import GAS_DATA_DIRECTORY
from optmat.gas.gases import Gas, as_gas
from optmat.identity import Identified, parse_status, source_ids

__all__ = [
    "DERIVATION_VERSION",
    "EVALUATION_VERSION",
    "POLARIZATIONS",
    "REGION_LABELS",
    "TwoPhotonIonisationModel",
    "TwoPhotonReference",
    "TwoPhotonRegion",
    "TwoPhotonSpread",
    "TwoPhotonThreshold",
    "TwoPhotonUncertainty",
    "two_photon_ionisation_model",
    "two_photon_ionisation_models",
]

EVALUATION_VERSION: Final = 1
"""The version of the evaluation: interpolation, regions and conversions."""
DERIVATION_VERSION: Final = 1
"""The version of the stored models' derivation, raised whenever it writes otherwise."""
POLARIZATIONS: Final = ("linear", "circular")
REGION_LABELS: Final = (
    "below_threshold",
    "supported",
    "unsupported_detail",
    "unavailable",
)
"""The kinds of region a model's frequency axis is divided into."""

_SELECTION_FILE: Final = GAS_DATA_DIRECTORY / "two_photon.toml"
"""The approved references and their choices (TC-943)."""
_DIRECTORY: Final = GAS_DATA_DIRECTORY / "models" / "two_photon"
_DEFINED: Final = frozenset({"below_threshold", "supported", "unsupported_detail"})
_STANDING: Final = frozenset({"status", "status_reason", "modified", "base"})


[docs] @dataclass(frozen=True) class TwoPhotonThreshold: """A threshold or resonance as a photon's angular frequency, with its source. Attributes ---------- name : str What it is, e.g. ``"Ar+ 2P3/2"`` or ``"E1"``. omega : float The photon's angular frequency, rad/s: the level's energy over the number of photons that reach it, over hbar. photons : int 2 for an ionisation threshold, 1 for :math:`E_1`. energy : float The energy above the ground state, J. datasets : tuple of str The curated datasets it is computed from. """ name: str omega: float photons: int energy: float datasets: tuple[str, ...]
[docs] @dataclass(frozen=True) class TwoPhotonRegion: """A half-open band of photon angular frequency, ``[lower, upper)``, and its label. Attributes ---------- label : str One of :data:`REGION_LABELS`. lower : float The lower edge, rad/s. upper : float The upper edge, rad/s; inf for the last region. reason : str Why the band is so labelled. """ label: str lower: float upper: float reason: str
[docs] @dataclass(frozen=True) class TwoPhotonReference: """The approved reference curve of a model, and why it was chosen. Attributes ---------- datasets : tuple of str Its datasets, summed where they are partial cross sections. gauge : str The gauge, as the record states it. method : str The calculation. intensity_w_cm2 : float or None The intensity of a non-perturbative calculation, W/cm^2; None for a lowest-order one. reason : str Why it is the reference (the author's approval, TC-943). registers : tuple of str The register entries. """ datasets: tuple[str, ...] gauge: str method: str intensity_w_cm2: float | None reason: str registers: tuple[str, ...]
[docs] @dataclass(frozen=True) class TwoPhotonSpread: """Another calculation against the reference over their supported overlap. Attributes ---------- label : str The calculation. datasets : tuple of str Its datasets, summed where they are partial cross sections. form : str ``"vector"`` or ``"raster"``: how its values were read. role : str ``"independent"``, another calculation, which enters the envelope; or ``"gauge"``, the reference's other gauge, a diagnostic only. lower : float The lowest photon angular frequency compared, rad/s. upper : float The highest, rad/s. points : int The number of reference vertices compared. lower_ratio : float The smallest ratio of it to the reference. upper_ratio : float The largest. """ label: str datasets: tuple[str, ...] form: str role: str lower: float upper: float points: int lower_ratio: float upper_ratio: float
[docs] @dataclass(frozen=True) class TwoPhotonUncertainty: """What is known of a model's uncertainty, kept as three separate kinds (Q6). Attributes ---------- source : float or None The source's own relative accuracy, None where it states none (never zero). source_level : str Its level, as stated, or ``"not stated"``. source_provenance : str Where it is stated. digitising : float The relative digitising uncertainty at a drawn vertex. digitising_provenance : str Its record and register entry. spreads : tuple of TwoPhotonSpread The other calculations against the reference, over the supported region. """ source: float | None source_level: str source_provenance: str digitising: float digitising_provenance: str spreads: tuple[TwoPhotonSpread, ...] @property def envelope(self) -> tuple[float, float] | None: """The smallest and largest ratio of the independent calculations. It is an empirical envelope of other calculations, not a confidence interval or a bound on the true cross section. Returns ------- tuple of float or None ``(lower, upper)`` ratios to the reference; None with no independent calculation. """ independent = [s for s in self.spreads if s.role == "independent"] if not independent: return None return ( min(s.lower_ratio for s in independent), max(s.upper_ratio for s in independent), )
[docs] @dataclass(frozen=True, eq=False) class TwoPhotonIonisationModel(Identified): r"""The two-photon ionisation cross section of a noble gas's ground state. Built by :func:`two_photon_ionisation_model` from its stored derivation. SI throughout: photon angular frequency in rad/s, cross section in m^4 s. Attributes ---------- gas : Gas The species. polarization : str ``"linear"`` or ``"circular"``. derivation_version : int :data:`DERIVATION_VERSION` when it was derived. omega : numpy.ndarray The reference's vertices, photon angular frequency, rad/s, increasing. sigma : numpy.ndarray The reference's cross section at them, m^4 s. thresholds : tuple of TwoPhotonThreshold The lowest ionic channel's two-photon threshold first; the ion's :math:`{}^2P_{1/2}` channel's second, for Ne to Xe. e1 : TwoPhotonThreshold The first one-photon resonance, where the window ends. regions : tuple of TwoPhotonRegion Bands covering every positive frequency, in order. reference : TwoPhotonReference The approved reference. uncertainty : TwoPhotonUncertainty Its uncertainty, by kind. input_digests : Mapping of str to str SHA-256 digests of what it was derived from: the selection and each dataset. """ gas: Gas polarization: str derivation_version: int omega: Float[np.ndarray, " n"] sigma: Float[np.ndarray, " n"] thresholds: tuple[TwoPhotonThreshold, ...] e1: TwoPhotonThreshold regions: tuple[TwoPhotonRegion, ...] reference: TwoPhotonReference uncertainty: TwoPhotonUncertainty input_digests: Mapping[str, str] = field(default_factory=dict, repr=False) IDENTITY_KIND: ClassVar[str] = "two_photon_ionisation" EVALUATION_VERSION: ClassVar[int] = EVALUATION_VERSION def __post_init__(self) -> None: """Take ownership, and check the grid and the regions. Raises ------ DataError If the grid is not increasing and positive, or the regions do not tile the positive axis with known labels. """ super().__post_init__() where = f"two-photon model {self.gas.value} ({self.polarization})" if ( self.omega.shape != self.sigma.shape or len(self.omega) < 2 or np.any(np.diff(self.omega) <= 0) or np.any(self.sigma <= 0) ): msg = f"{where}: its grid must be increasing with positive cross sections" raise DataError(msg) edges = [(r.lower, r.upper) for r in self.regions] tiled = bool(edges) and edges[0][0] == 0 and math.isinf(edges[-1][1]) tiled = tiled and all(a[1] == b[0] for a, b in itertools.pairwise(edges)) if not tiled or any(r.label not in REGION_LABELS for r in self.regions): msg = f"{where}: its regions must tile [0, inf) with known labels" raise DataError(msg) def _identity_label(self) -> tuple[str, str, tuple[str, ...]]: """Return the model's name, gas and source records. Returns ------- tuple ``("two_photon/<gas>/<polarization>", gas, sources)``. """ keys = [*self.reference.datasets, *self.e1.datasets] keys += [k for t in self.thresholds for k in t.datasets] keys += [k for s in self.uncertainty.spreads for k in s.datasets] return ( f"two_photon/{self.gas.value}/{self.polarization}", self.gas.value, source_ids(*(f"{key}/" for key in keys)), ) @property def digest(self) -> str: """The model's digest, ``identity.digest``. Returns ------- str 64 hexadecimal digits. """ return self.identity.digest def _region_indices(self, omega: np.ndarray) -> np.ndarray: """Return the index of each frequency's region. Parameters ---------- omega : numpy.ndarray Photon angular frequencies, rad/s, positive. Returns ------- numpy.ndarray Indices into :attr:`regions`. """ lowers = np.array([r.lower for r in self.regions]) return np.asarray(np.searchsorted(lowers, omega, side="right") - 1)
[docs] def region(self, omega: ArrayLike) -> np.ndarray: """Return the label of the region each frequency lies in. Parameters ---------- omega : array_like Photon angular frequency, rad/s, finite and positive. Returns ------- numpy.ndarray Labels (:data:`REGION_LABELS`), with the shape of ``omega``. Raises ------ ArgumentError If a frequency is not finite and positive. """ w = _frequencies(omega) labels = np.array([r.label for r in self.regions], dtype=object) return np.asarray(labels[self._region_indices(w)])
[docs] def available(self, omega: ArrayLike) -> np.ndarray: """Return where the model has a value: below threshold, or on its reference. Parameters ---------- omega : array_like Photon angular frequency, rad/s, finite and positive. Returns ------- numpy.ndarray Booleans with the shape of ``omega``: False where :meth:`cross_section` is NaN. Raises ------ ArgumentError If a frequency is not finite and positive. """ labels = self.region(omega) return np.isin(labels, list(_DEFINED))
[docs] def cross_section(self, omega: ArrayLike) -> Float[np.ndarray, " *shape"]: r"""Return the two-photon ionisation cross section at photon frequencies. Parameters ---------- omega : array_like Photon angular frequency, rad/s, finite and positive. Returns ------- numpy.ndarray :math:`\sigma^{(2)}`, m^4 s, with the shape of ``omega``: exactly 0 below the lowest threshold, NaN where :meth:`available` is False. Raises ------ ArgumentError If a frequency is not finite and positive. Warns ----- ModelRangeWarning If a frequency lies in an ``unsupported_detail`` region. """ w = _frequencies(omega) labels = np.array([r.label for r in self.regions], dtype=object) where = labels[self._region_indices(w)] out = np.full(w.shape, np.nan) out[where == "below_threshold"] = 0.0 on_curve = (where == "supported") | (where == "unsupported_detail") out[on_curve] = np.exp(np.interp(w[on_curve], self.omega, np.log(self.sigma))) if np.any(where == "unsupported_detail"): reasons = { r.reason for r in self.regions if r.label == "unsupported_detail" } warnings.warn( f"{self.identity.name}: values in an unsupported-detail region: " + " ".join(sorted(reasons)), ModelRangeWarning, stacklevel=2, ) return out
[docs] def pair_cross_section(self, pair_omega: ArrayLike) -> Float[np.ndarray, " *shape"]: r"""Return the cross section indexed by a photon pair's summed frequency. Parameters ---------- pair_omega : array_like The sum of the two photons' angular frequencies, rad/s, finite and positive. Returns ------- numpy.ndarray :math:`\sigma^{(2)}(\Omega/2)`, m^4 s, as :meth:`cross_section`. Raises ------ ArgumentError If a frequency is not finite and positive. Warns ----- ModelRangeWarning If a frequency lies in an ``unsupported_detail`` region. """ return self.cross_section(_frequencies(pair_omega) / 2)
[docs] def rate( self, omega: ArrayLike, intensity: ArrayLike ) -> Float[np.ndarray, " *shape"]: r"""Return the two-photon ionisation rate per atom of a monochromatic wave. Parameters ---------- omega : array_like Photon angular frequency, rad/s, finite and positive. intensity : array_like Intensity, W/m^2, finite and non-negative; broadcast with ``omega``. Returns ------- numpy.ndarray :math:`W = \sigma^{(2)} (I / \hbar\omega)^2`, 1/s; NaN where the model is unavailable. Raises ------ ArgumentError If a frequency is not finite and positive, or an intensity is not finite and non-negative. Warns ----- ModelRangeWarning If a frequency lies in an ``unsupported_detail`` region. """ w = _frequencies(omega) i = _non_negative(intensity, "intensity") return np.asarray(self.cross_section(w) * (i / (HBAR * w)) ** 2)
[docs] def loss_coefficient( self, omega: ArrayLike, number_density: ArrayLike ) -> Float[np.ndarray, " *shape"]: r"""Return the two-photon loss coefficient beta, dI/dz = -beta I^2. Parameters ---------- omega : array_like Photon angular frequency, rad/s, finite and positive. number_density : array_like The density of ground-state atoms, m^-3, finite and non-negative; broadcast with ``omega``. Returns ------- numpy.ndarray :math:`\beta = 2 N \sigma^{(2)} / \hbar\omega`, m/W: two photons per event; NaN where the model is unavailable. Raises ------ ArgumentError If a frequency is not finite and positive, or a density is not finite and non-negative. Warns ----- ModelRangeWarning If a frequency lies in an ``unsupported_detail`` region. """ w = _frequencies(omega) n = _non_negative(number_density, "number_density") return np.asarray(2 * n * self.cross_section(w) / (HBAR * w))
def _frequencies(value: ArrayLike) -> np.ndarray: """Return frequencies as a float array, checked finite and positive. Parameters ---------- value : array_like Frequencies, rad/s. Returns ------- numpy.ndarray The frequencies. Raises ------ ArgumentError If one is not finite and positive. """ w = np.asarray(value, dtype=float) if not np.all(np.isfinite(w) & (w > 0)): msg = "frequencies must be finite and positive, rad/s" raise ArgumentError(msg) return w def _non_negative(value: ArrayLike, name: str) -> np.ndarray: """Return a quantity as a float array, checked finite and non-negative. Parameters ---------- value : array_like The quantity. name : str Its name, for the error. Returns ------- numpy.ndarray The quantity. Raises ------ ArgumentError If a value is not finite and non-negative. """ x = np.asarray(value, dtype=float) if not np.all(np.isfinite(x) & (x >= 0)): msg = f"{name} must be finite and non-negative" raise ArgumentError(msg) return x @functools.cache def _selection() -> dict[str, Any]: """Return the selection file, parsed. Returns ------- dict Its tables, by gas. """ with _SELECTION_FILE.open("rb") as f: return tomllib.load(f) def _plain(value: object) -> Any: """Return a value as plain JSON values (an infinite edge as null). Parameters ---------- value : object A field's value. Returns ------- object Dicts, lists, strings, numbers, booleans or None. """ if is_dataclass(value) and not isinstance(value, type): value = { f.name: getattr(value, f.name) for f in fields(value) if f.name not in _STANDING } if isinstance(value, Mapping): return {str(k): _plain(v) for k, v in value.items()} if isinstance(value, (tuple, list, np.ndarray)): return [_plain(v) for v in value] if isinstance(value, Gas): return value.value if isinstance(value, (float, np.floating)): return None if math.isinf(value) else float(value) return value def _optional(value: Any) -> float | None: """Return a stored optional number. Parameters ---------- value : object A number or None. Returns ------- float or None The number. """ return None if value is None else float(value) def _dumps(model: TwoPhotonIonisationModel) -> str: """Serialise a model as JSON text, as stored. Parameters ---------- model : TwoPhotonIonisationModel The model. Returns ------- str Indented JSON. """ return json.dumps(_plain(model), indent=1, allow_nan=False) + "\n" def _object(cls: type, raw: Any, where: str) -> Mapping[str, Any]: """Return a stored object's fields, checking they are exactly the class's. Parameters ---------- cls : type The dataclass. raw : object The JSON object. where : str For error messages. Returns ------- Mapping The fields. Raises ------ DataError If it is not an object with exactly the class's fields. """ names = {f.name for f in fields(cls)} - _STANDING if not isinstance(raw, Mapping) or set(raw) != names: msg = f"{where}: a {cls.__name__} needs exactly the fields {sorted(names)}" raise DataError(msg) return raw def _threshold(raw: Any, where: str) -> TwoPhotonThreshold: """Read a stored threshold. Parameters ---------- raw : object The JSON object. where : str For error messages. Returns ------- TwoPhotonThreshold The threshold. """ d = _object(TwoPhotonThreshold, raw, where) return TwoPhotonThreshold( name=str(d["name"]), omega=float(d["omega"]), photons=int(d["photons"]), energy=float(d["energy"]), datasets=tuple(str(k) for k in d["datasets"]), ) def _loads(text: str, where: str) -> TwoPhotonIonisationModel: """Read a stored model from its JSON text. Parameters ---------- text : str The JSON, as :func:`_dumps` writes it. where : str For error messages. Returns ------- TwoPhotonIonisationModel The model, with the status its selection declares. Raises ------ DataError If the text is not a model of the current derivation version. """ d = _object(TwoPhotonIonisationModel, json.loads(text), where) if d["derivation_version"] != DERIVATION_VERSION: msg = ( f"{where}: derivation version {d['derivation_version']}, not " f"{DERIVATION_VERSION}; re-derive it" ) raise DataError(msg) ref = _object(TwoPhotonReference, d["reference"], where) unc = _object(TwoPhotonUncertainty, d["uncertainty"], where) gas = as_gas(d["gas"]) polarization = str(d["polarization"]) status, reason = parse_status( _selection()[gas.value][polarization], where, default="validated" ) return TwoPhotonIonisationModel( gas=gas, polarization=polarization, derivation_version=int(d["derivation_version"]), omega=np.array(d["omega"], dtype=float), sigma=np.array(d["sigma"], dtype=float), thresholds=tuple(_threshold(t, where) for t in d["thresholds"]), e1=_threshold(d["e1"], where), regions=tuple( TwoPhotonRegion( label=str(r["label"]), lower=float(r["lower"]), upper=math.inf if r["upper"] is None else float(r["upper"]), reason=str(r["reason"]), ) for r in (_object(TwoPhotonRegion, r, where) for r in d["regions"]) ), reference=TwoPhotonReference( datasets=tuple(str(k) for k in ref["datasets"]), gauge=str(ref["gauge"]), method=str(ref["method"]), intensity_w_cm2=( None if ref["intensity_w_cm2"] is None else float(ref["intensity_w_cm2"]) ), reason=str(ref["reason"]), registers=tuple(str(r) for r in ref["registers"]), ), uncertainty=TwoPhotonUncertainty( source=_optional(unc["source"]), source_level=str(unc["source_level"]), source_provenance=str(unc["source_provenance"]), digitising=float(unc["digitising"]), digitising_provenance=str(unc["digitising_provenance"]), spreads=tuple( TwoPhotonSpread( label=str(s["label"]), datasets=tuple(str(k) for k in s["datasets"]), form=str(s["form"]), role=str(s["role"]), lower=float(s["lower"]), upper=float(s["upper"]), points=int(s["points"]), lower_ratio=float(s["lower_ratio"]), upper_ratio=float(s["upper_ratio"]), ) for s in (_object(TwoPhotonSpread, s, where) for s in unc["spreads"]) ), ), input_digests={str(k): str(v) for k, v in d["input_digests"].items()}, status=status, status_reason=reason, ) @functools.cache def _stored(gas: Gas, polarization: str) -> TwoPhotonIonisationModel: """Return a stored model. Parameters ---------- gas : Gas The species. polarization : str ``"linear"`` or ``"circular"``. Returns ------- TwoPhotonIonisationModel The model of ``data/gas/models/two_photon/<gas>_<polarization>.json``. Raises ------ DataError If the file is missing, malformed, or holds another model. """ path = _DIRECTORY / f"{gas.value}_{polarization}.json" if not path.is_file(): msg = f"{path.name}: the stored two-photon model is missing; derive it" raise DataError(msg) model = _loads(path.read_text(encoding="utf-8"), path.name) if (model.gas, model.polarization) != (gas, polarization): msg = f"{path.name} holds the model of {model.gas.value} ({model.polarization})" raise DataError(msg) return model
[docs] def two_photon_ionisation_model( gas: Gas | str, polarization: str = "linear" ) -> TwoPhotonIonisationModel: """Return a noble gas's two-photon ionisation model (OEP 6, Phase 1). Parameters ---------- gas : Gas or str The gas: He, Ne, Ar or Xe (Kr has none). polarization : str, optional ``"linear"`` (default), or ``"circular"`` where an approved reference gives it (Xe). Returns ------- TwoPhotonIonisationModel The model. Raises ------ UnavailableModelError For a gas or polarisation with no approved reference, with the reason. ArgumentError For a polarisation other than linear or circular. Examples -------- >>> xenon = two_photon_ionisation_model("Xe", "circular") >>> xenon.reference.datasets ('lagutin_2017/fig8_cipfcp_circular',) """ if polarization not in POLARIZATIONS: msg = f"polarization must be one of {POLARIZATIONS}, not {polarization!r}" raise ArgumentError(msg) species = as_gas(gas) entry = _selection().get(species.value) if entry is None: msg = f"{species.value}: two-photon ionisation is curated for the noble gases" raise UnavailableModelError(msg) if "missing" in entry: raise UnavailableModelError(f"{species.value}: {entry['missing']}") if polarization not in entry: msg = ( f"{species.value}: no approved {polarization} reference (OEP 6, Q4); a " "factor read from a raster figure is not a model" ) raise UnavailableModelError(msg) return _stored(species, polarization)
[docs] def two_photon_ionisation_models() -> dict[tuple[Gas, str], TwoPhotonIonisationModel]: """Return every two-photon ionisation model, by gas and polarisation. Returns ------- dict ``{(gas, polarization): model}`` for every approved reference. Examples -------- >>> sorted(f"{g.value}/{p}" for g, p in two_photon_ionisation_models()) ['Ar/linear', 'He/linear', 'Ne/linear', 'Xe/circular', 'Xe/linear'] """ return { (as_gas(gas), polarization): _stored(as_gas(gas), polarization) for gas, entry in _selection().items() for polarization in POLARIZATIONS if polarization in entry }