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
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 CHANGELOG.md for a full history of changes.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gasfir-1.0.1-py3-none-any.whl.
File metadata
- Download URL: gasfir-1.0.1-py3-none-any.whl
- Upload date:
- Size: 110.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
684a491b3951d0c6b7dbc71660ecb48a6493d2f85363af9409dac387c98901f4
|
|
| MD5 |
e4e84b173e3a7bdc288ad8932a510958
|
|
| BLAKE2b-256 |
8e69d5e6f50bde6e335c98771a924dfca8d6ef24199da5719879c2a5fc978553
|