GASFIR: General Approximator for Strong-Field Ionization Rates
GASFIR is a Python package implementing the ionization-rate retrieval framework introduced in:
Agarwal, Scrinzi & Yakovlev — General 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:
- GASFIR — semi-analytic kernel, Eqs. (2), (3), (7) of the paper; fast
- Exact SFA — full numerical saddle-point integration of the SFA kernel,
Eq. (6). Selected via
kernel_type="exact_SFA"and currently implemented for the hydrogen ground state only (use it with theH_SFAparameters) - QS — quasistatic (tunneling) limit, Eq. (9); connects to ADK/PPT theory
All internal quantities are in atomic units unless stated otherwise.
📖 Documentation
Full documentation — installation, user guide, worked examples, and the complete API reference — is hosted on Read the Docs:
→ https://gasfir.readthedocs.io
The docs are versioned: latest tracks the main branch and each release tag
(e.g. v1.0.0) has its own frozen version, selectable from the flyout menu.
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()
Pulse types
Any pulse object can be passed to every calculation function (pulse=...).
from gasfir import create_pulse
from gasfir.pulse import create_pulse_ellip, SumOfPulses, TIPTOE, DataPulse
import numpy as np
# 1. Linearly polarized cos^N pulse (N=8 default) — the usual workhorse
laser = create_pulse(800, 1e14, 0, 6) # 800 nm, 1e14 W/cm², CEP 0, 6 cycles
# 2. Elliptical / circular polarization (ellip_frac=1 → circular)
ell = create_pulse_ellip(800, 1e14, 0, 6, ellip_frac=2)
# 3. Two-colour (or arbitrary) superposition
two_colour = SumOfPulses([create_pulse(800, 1e14, 0, 6),
create_pulse(400, 2e13, 0, 6)])
# 4. TIPTOE pump–probe: strong pump fixed, weak probe scanned in delay
pump = create_pulse(800, 3e14, 0, 3)
probe = create_pulse(800, 1e13, 0, 1)
scanner = TIPTOE(pump, probe)
delays = scanner.return_delay_array() # Nyquist-sampled delay grid
field_at_tau = scanner.at_delay(delays[0]) # a SumOfPulses at that delay
# 5. From your own data — supply E(t) OR A(t) on a time grid (atomic units;
# A(t) must vanish at both ends). Use it like any other pulse.
t = np.linspace(-400, 400, 8001)
A = np.exp(-(t/150)**2) * np.sin(0.057 * t) / 0.057
my_pulse = DataPulse(t=t, A=A) # E(t) derived as -dA/dt
See the Laser Fields guide for full details (pump–probe scanning, boundary conditions, combining pulses).
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_aureturns intensity in atomic units (I × factor, dimensionally I in a.u.).intensity_Wcm2_to_field_aureturns 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.fittingandgasfir.retrieval— not in the top-levelgasfirnamespace.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 plotHydrogen_SFA_trace.pdf— walker traceHydrogen_SFA_fit_summary.tex— LaTeX parameter table + correlation matrixHydrogen_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
First-time contributor setup
Run once after cloning — this gets you a working environment and the git hooks (auto-format on commit, test reminder on push):
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]"
pre-commit install --hook-type pre-commit --hook-type pre-push
(Minimal variant if you only need the dev tools: pip install -e ".[dev]"
then the same pre-commit install line.)
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
Linter versions are pinned in the [dev] extras so local and CI formatting
always agree.
Pre-commit hooks (recommended)
Install the git hooks once per clone so formatting is fixed automatically and you're reminded to run tests before pushing:
pre-commit install --hook-type pre-commit --hook-type pre-push
- on
git commit— black + isort auto-format changed files and flake8 lints them (a clean commit is guaranteed to pass the CI lint stage); - on
git push— a reminder to run the local checks before CI does.
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.10–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
- Fork and create a branch:
git checkout -b feature/my-feature - Follow the code style (Black + isort + flake8 + mypy, Google-style docstrings)
- Add tests; all new public functions need type annotations and a docstring
- 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
- Documentation: https://gasfir.readthedocs.io
- Issues: https://gitlab.mpcdf.mpg.de/gaf/gasfir/issues
- Email: manoram.agarwal@mpq.mpg.de
Acknowledgments
Developed at the Max Planck Institute for Quantum Optics (MPQ).
See the CHANGELOG for a full history of changes.
Metadata
Release files for gasfir 1.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| gasfir-1.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Release files / gasfir-1.1.1-py3-none-any.whl
| Download URL | gasfir-1.1.1-py3-none-any.whl |
|---|---|
| Size | 116.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
fc751c6747dd3d31ba7dab0a31be7d3bf85751fcc9b73d391a6af57ba9ccf27f
|
|
BLAKE2b-256 checksum How to use checksums |
6198f401580a3b775c61fd33765364724dba2d2462827e4653606aa0d5e0eda1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.15
|