Skip to main content

General approximator for strong-field ionization rates — retrieval of sub-cycle ionization dynamics from ab initio or experimental probabilities (Agarwal, Scrinzi & Yakovlev, PRA 113, L021101, 2026)

Project description

GASFIR: General Approximator for Strong-Field Ionization Rates

PyPI version Python 3.8+ License: MIT Documentation Tests Coverage DOI

GASFIR is a Python package implementing the ionization-rate retrieval framework introduced in:

Agarwal, Scrinzi & YakovlevGeneral approximator for strong-field ionization rates
Physical Review A 113, L021101 (2026) — 10.1103/vxgm-kdtt

It addresses the long-standing problem of determining accurate, time-resolved ionization rates for atoms in strong laser fields — a quantity fundamental to attosecond science. By fitting a five-parameter kernel to ionization probabilities computed for a set of few-cycle laser pulses, GASFIR retrieves sub-optical-cycle ionization dynamics within and beyond the strong-field approximation.

Three calculation methods are provided:

  1. GASFIR — semi-analytic kernel, Eqs. (2), (3), (7) of the paper; fast
  2. Exact SFA — full numerical saddle-point integration of the SFA kernel, Eq. (6)
  3. QS — quasistatic (tunneling) limit, Eq. (9); connects to ADK/PPT theory

All internal quantities are in atomic units unless stated otherwise.


Installation

Core only — ionization calculations

pip install gasfir

Installs: numba, numpy, scipy, pandas. No fitting library required.

With fitting & retrieval pipeline

pip install gasfir[retrieval]

Adds: lmfit, emcee, cma (CMA-ES), tqdm, corner.

Full development install

pip install gasfir[retrieval,dev,docs]

Conda environment (recommended)

conda env create -f environment.yml
conda activate gasfir
pip install -e ".[retrieval,dev,docs]"

To recreate environment.yml after changing dependencies:

conda env export -n gasfir --no-builds | grep -v "^prefix:" > environment.yml

Quick Start

Ionization probability

from gasfir import create_pulse, get_parameters, get_diabatic_ionization_probability

# create_pulse(wavelength_nm, intensity_Wcm2, CEP_rad, duration_optical_cycles)
laser = create_pulse(800, 1e14, 0, 6)

params = get_parameters("Hydrogen_SFA")
prob = get_diabatic_ionization_probability(pulse=laser, param_dict=params)
print(f"P = {prob:.4e}")

Time-resolved ionization rate

from gasfir import get_diabatic_ionization_rate
import matplotlib.pyplot as plt

t = laser.get_tgrid(dt=0.25)   # time grid in atomic units
rates = get_diabatic_ionization_rate(t_grid=t, pulse=laser, param_dict=params)

plt.semilogy(t, rates)
plt.xlabel("Time (a.u.)")
plt.ylabel("Ionization rate (a.u.)")
plt.tight_layout()
plt.show()

Available parameter sets

from gasfir import get_parameters

print(get_parameters())           # list all names
params = get_parameters("He_Hacc5_QS")

Unit Conversions — AtomicUnits

AtomicUnits provides named two-way conversions so you never have to reason about which direction to multiply or divide.

from gasfir import AtomicUnits

# Length
AtomicUnits.nm_to_au(800)          # 800 nm → a.u.
AtomicUnits.au_to_nm(1.0)          # Bohr radius in nm (≈ 0.0529)
AtomicUnits.angstrom_to_au(0.529)
AtomicUnits.m_to_au(5.29e-11)

# Time
AtomicUnits.fs_to_au(2.42)         # 2.42 fs → a.u.
AtomicUnits.au_to_fs(100)          # 100 a.u. → fs

# Energy
AtomicUnits.eV_to_au(13.6)         # hydrogen IP in a.u. (≈ 0.5)
AtomicUnits.au_to_eV(0.5)          # ≈ 13.6 eV

# Electric field
AtomicUnits.Vm_to_au(5.14e11)      # ≈ 1 a.u. of field
AtomicUnits.VA_to_au(51.4)         # same in V/Å

# Laser wavelength ↔ angular frequency
AtomicUnits.wavelength_nm_to_omega_au(800)     # ≈ 0.0570 a.u.
AtomicUnits.omega_au_to_wavelength_nm(0.0570)  # ≈ 800 nm

# Photon energy ↔ angular frequency (ħ = 1 in a.u.)
AtomicUnits.photon_energy_eV_to_omega_au(1.55)
AtomicUnits.omega_au_to_photon_energy_eV(0.057)

# Laser intensity ↔ field amplitude
AtomicUnits.intensity_Wcm2_to_field_au(1e14)   # E₀ in a.u. (≈ 0.0534)
AtomicUnits.field_au_to_intensity_Wcm2(0.0534) # back to W/cm²
AtomicUnits.intensity_Wcm2_to_au(1e14)         # intensity in a.u. (≠ field amplitude)

Note intensity_Wcm2_to_au returns intensity in atomic units (I × factor, dimensionally I in a.u.). intensity_Wcm2_to_field_au returns the electric field amplitude (√(I × factor)), which is ~18× larger and what you usually need.


Keldysh Parameter

The Keldysh parameter γ = ω√(2Iₚ) / E₀ separates the tunnel (γ ≪ 1) and multiphoton (γ ≫ 1) ionization regimes.

Compute γ for a given pulse

laser = create_pulse(800, 1e14, 0, 6)
gamma = laser.get_keldysh_parameter(Ip_au=0.5)   # hydrogen
print(f"γ = {gamma:.3f}")  # ≈ 1.07 → near the tunnel/multiphoton border

Design a pulse for a target γ

from gasfir import AtomicUnits

Ip = 0.5   # hydrogen (a.u.)

# 10-photon resonance wavelength (ω = Ip/10)
wl = AtomicUnits.omega_au_to_wavelength_nm(Ip / 10)   # ≈ 911 nm

# Intensities for the three main regimes
I_multi  = AtomicUnits.intensity_for_keldysh(4.0,  wl, Ip)  # γ=4, multiphoton
I_border = AtomicUnits.intensity_for_keldysh(1.0,  wl, Ip)  # γ=1, border
I_tunnel = AtomicUnits.intensity_for_keldysh(0.25, wl, Ip)  # γ=0.25, deep tunnel

print(f"γ=4   →  I = {I_multi:.2e} W/cm²")
print(f"γ=1   →  I = {I_border:.2e} W/cm²")
print(f"γ=0.25→  I = {I_tunnel:.2e} W/cm²")

Find the wavelength for a target γ at fixed intensity

wl = AtomicUnits.wavelength_for_keldysh(gamma=1.0, intensity_Wcm2=1e14, Ip_au=0.5)
print(f"λ for γ=1 at 1e14 W/cm²: {wl:.0f} nm")   # ≈ 749 nm

Ionization Potentials

GASFIR ships NIST first ionization potentials for all 118 elements:

from gasfir import get_ionization_potential

get_ionization_potential("Ar")      # 0.5792 a.u. (15.76 eV)
get_ionization_potential("helium")  # 0.9036 a.u. — full names work too
get_ionization_potential("Diamond") # None — not an element

Parameter Fitting & Retrieval

Requires pip install gasfir[retrieval]

Fitting functions live in gasfir.fitting and gasfir.retrievalnot in the top-level gasfir namespace.

Results are saved to <medium_name>/ by default (JSON, HDF5 chain, corner plot, trace plot, LaTeX summary).

Known medium — full pipeline

import pandas as pd
from gasfir import create_pulse
from gasfir.retrieval import RetrievalConfig, retrieve

data = pd.DataFrame({
    "pulses": [create_pulse(800, I, 0, 6) for I in intensities],
    "Y":      measured_probabilities,
})

# output saved to ./Hydrogen_SFA/
cfg = RetrievalConfig(medium_name="Hydrogen_SFA")
result = retrieve(data_NA=data, config=cfg)

for name in result.var_names:
    v = result.params[name]
    print(f"  {name} = {v.value:.4g} ± {v.stderr:.2g}")

After the LS phase the pipeline automatically compares fitted values against the stored reference and prints a Nσ table. After emcee it saves:

  • Hydrogen_SFA_publication_corner.pdf — posterior corner plot
  • Hydrogen_SFA_trace.pdf — walker trace
  • Hydrogen_SFA_fit_summary.tex — LaTeX parameter table + correlation matrix
  • Hydrogen_SFA_retrieval_result.json — full result for reload

Cold start — atom (E_g from NIST)

For elements not yet in the GASFIR parameter store, E_g is looked up automatically from the NIST database; no initial guess is required:

cfg = RetrievalConfig(
    medium_name="Kr",        # krypton — E_g auto-resolved: 0.5145 a.u.
    cma_maxiter=5000,        # broader search for an unknown system
)
result = retrieve(data_NA=data, config=cfg)

Cold start — crystal (E_g must be supplied)

For crystals, the gamma-point band gap from DFT is unreliable for ultrafast dynamics — E_g must always be provided explicitly:

cfg = RetrievalConfig(
    medium_name="MyMaterial",
    initial_params={"E_g": 0.30},   # a.u. — only required field
    is_crystal=True,
    ret_electron_density=True,
    cma_maxiter=5000,
)
result = retrieve(data_NA=data, config=cfg)

Physical defaults are used for all other parameters; the cold-start notice prints the energy scale and source so you always know what was assumed.

Post-processing an existing MCMC chain

from gasfir.retrieval import post_process_mcmc

# Drop a parameter, add a derived one, re-snap and re-plot
post_process_mcmc(
    medium_name="Diamond_TBmBJ",
    output_dir="Diamond_TBmBJ",
    original_stored_params={"E_g": 0.235, "a1": 14, ...},
    drop_vars=["a0"],
    derived_exprs={"a0_per_au3": f"a0 / {6.74**3}"},
    latex_mapping={"E_g": r"$E_g$ [a.u.]"},
    is_crystal=True,
)

Low-level: residual function + manual lmfit

import lmfit
from gasfir.fitting import ret_residual_function

residual = ret_residual_function(data, uncertainty_Nadiabatic=0.05)

p = lmfit.Parameters()
p.add("E_g", value=0.5, vary=False)
p.add("a0",  value=5.0, min=0.01)
p.add("a1",  value=3.5, min=0.1)
p.add("a2",  value=2.0, min=0.0)
p.add("a3",  value=1.0)
p.add("a4",  value=0.0, vary=False)

result = lmfit.minimize(residual, p, method="least_squares")
lmfit.report_fit(result)

Development

Environment setup

git clone https://gitlab.mpcdf.mpg.de/gaf/gasfir.git
cd gasfir
conda env create -f environment.yml
conda activate gasfir
pip install -e ".[retrieval,dev,docs]"

Running tests

pytest tests/                                            # all tests
pytest tests/ --cov=src/gasfir --cov-report=html        # with coverage
pytest tests/test_gasfir.py -v                          # one file, verbose
pytest tests/test_integration.py::TestPerformanceIntegration  # performance only

Test pulses use physically motivated parameters defined in tests/physics.py (hydrogen Ip = 0.5 a.u., 10-photon wavelength ≈ 911 nm, intensities at γ = 0.25 / 1.0 / 4.0, 1 optical cycle).

Code quality

black src/ tests/         # format
isort src/ tests/         # sort imports
flake8 src/ tests/        # lint
mypy src/                 # type-check

Building documentation

cd docs && make html
cd docs && make livehtml   # live reload

Versioning and Releases

The version is defined only in pyproject.toml; __init__.__version__ reads it at runtime via importlib.metadata.

Tagging a release

# 1. Bump version in pyproject.toml
# 2. Update CHANGELOG.md
# 3. Commit: git commit -m "chore: release vX.Y.Z"
# 4. Tag and push:
git tag vX.Y.Z
git push origin vX.Y.Z

CI/CD pipeline (GitLab)

Trigger Stage Action
Every branch / MR test lint, type-check, pytest (Python 3.8–3.12)
main push build wheel + sdist, Sphinx docs
main push publish TestPyPI (automatic)
vX.Y.Z-alpha/beta/rc tag publish TestPyPI (automatic)
vX.Y.Z stable tag publish PyPI (manual approval)

Install from TestPyPI to verify before the production release:

pip install --index-url https://test.pypi.org/simple/ \
            --extra-index-url https://pypi.org/simple/ \
            gasfir==X.Y.Z

Contributing

  1. Fork and create a branch: git checkout -b feature/my-feature
  2. Follow the code style (Black + isort + flake8 + mypy, Google-style docstrings)
  3. Add tests; all new public functions need type annotations and a docstring
  4. Open a merge request against main

Commit messages follow Conventional Commits: feat:, fix:, docs:, refactor:, test:, chore:


License

MIT — see LICENSE.

Citation

If you use GASFIR in your research, please cite the paper:

@article{Agarwal2026gasfir,
  title     = {General approximator for strong-field ionization rates},
  author    = {Agarwal, Manoram and Scrinzi, Armin and Yakovlev, Vladislav S.},
  journal   = {Physical Review A},
  volume    = {113},
  pages     = {L021101},
  year      = {2026},
  publisher = {American Physical Society},
  doi       = {10.1103/vxgm-kdtt},
  url       = {https://doi.org/10.1103/vxgm-kdtt}
}

Support

Acknowledgments

Developed at the Max Planck Institute for Quantum Optics (MPQ).

See CHANGELOG.md for a full history of changes.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gasfir-1.0.0-py3-none-any.whl (109.5 kB view details)

Uploaded Python 3

File details

Details for the file gasfir-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: gasfir-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 109.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.15

File hashes

Hashes for gasfir-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1aa700bf49e0555b425951024887e8c1f4d522f99dfadcfb454ee60b34a4959b
MD5 3f9dd4d45cbb47b68056f9f732e563bb
BLAKE2b-256 29a48e7265519ef9bc53402c7cead885e198141341f148929984e0bde535692d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page