Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

Release PyPI Pipeline Status Coverage Status

aiida-feff

An AiiDA plugin for the FEFF real-space multiple-scattering code, enabling fully provenance-tracked EXAFS calculations — including ensemble averaging over MD snapshots via larch.


Features

Component Description
FeffCalculation CalcJob wrapping a single FEFF run; builds feff.inp from StructureData + FeffParameters
FeffParser Parses chi.dat into XasData output nodes
FeffBatchCalculation CalcJob that runs N FEFF instances in one Slurm job (one per core); for HPC ensemble runs
FeffBatchParser Parses all per-snapshot outputs from a batch job into a dynamic namespace of XasData nodes
FeffParameters Validated Dict subclass for FEFF control cards
XasData ArrayData subclass storing χ(k), plus μ(E) on experimental imports
EnsembleExafsWorkChain Fan-out over MD snapshots → one ExafsArchiveData and ensemble-averaged χ(k); single-job and batch modes
calcfunctions Larch post-processing (FT), archive projection, experimental imports
visualise Matplotlib helpers for χ(k) and χ(R) plots

Installation

pip install aiida-feff
verdi plugin list aiida.calculations   # should show feff.feff and feff.feff_batch
verdi plugin list aiida.workflows      # should show feff.ensemble

For plotting (optional):

pip install aiida-feff[plots]

Quick start

Experimental spectrum import

aiida_feff.calcfunctions.experimental.import_experimental_spectrum adapts Larch's readers to a provenance-tracked XasData node. Pass a stored SinglefileData upload and a Dict of Larch import options, such as plain text column labels or a selected Athena group. The raw file remains an input node; the plugin does not implement a competing file parser.

scale_simulated_spectrum stores a separate $S_0^2$/ΔE₀-adjusted simulated XasData node for reproducible experimental comparisons.

1. Register your FEFF code

verdi code setup \
    --label feff \
    --computer localhost \
    --input-plugin feff.feff \
    --remote-abs-path /path/to/feff

2. Run a single-site EXAFS calculation

from aiida import load_profile, orm
from aiida.engine import run
from aiida_feff.calculations.feff import FeffCalculation
from aiida_feff.data.parameters import FeffParameters

load_profile()

structure = orm.StructureData(cell=[[2.87,0,0],[0,2.87,0],[0,0,2.87]])
structure.append_atom(position=(0,0,0), symbols='Fe')
structure.append_atom(position=(1.435,1.435,1.435), symbols='Fe')

params = FeffParameters(dict={
    "edge": "K",
    "spectrum_type": "EXAFS",
    "radius": 5.5,           # FEFF RPATH / cluster radius in Å
    "s02": 1.0,
})

result = run(FeffCalculation,
    code=orm.load_code('feff@localhost'),
    structure=structure,
    parameters=params,
    metadata={"options": {
        "resources": {"num_machines": 1},
        "max_wallclock_seconds": 300,
    }},
)

xas = result['xas_data']
print(f"chi(k) shape: {xas.chi_k.shape}")

3. Ensemble EXAFS from an MD trajectory (localhost / dev)

Recommended environment: the DevContainer in .devcontainer/ gives you a full IDE against a real AiiDA profile: SQLite storage, a RabbitMQ broker, a running daemon, and both feff@localhost and python3@localhost registered. Calculations you run there produce real nodes and a real provenance graph, so verdi works as it would anywhere else.

It runs on the host's own architecture, so it is equally at home in GitHub Codespaces (Code → Codespaces → Create) and on a local Docker or Podman. Verified end to end on Apple Silicon: post-create.sh completes, FEFF8L runs, and the full test suite passes inside the container.

FEFF8L is not downloaded; it ships inside the xraylarch dependency and the container points a code at it. On arm64 those x86_64 binaries run through the runtime's qemu handler, for which post-create installs the x86_64 loader. No PostgreSQL is involved: storage is core.sqlite_dos, the same backend the tests use.

Podman users: set dockerComposeFile in .devcontainer/devcontainer.json to ["docker-compose.yml", "docker-compose.podman.yml"]. The override adds userns_mode: keep-id, which Docker Engine rejects and which therefore cannot live in the base file.

Outside the container you need a broker and daemon running, a working FEFF executable, and a Python interpreter registered as an installed code (verdi code create core.code.installed ..., e.g. python3@localhost) for path aggregation.

The synthetic example covers the whole pipeline and doubles as the smoke test CI runs:

# Serial: one scheduler job per snapshot, with per-path contributions stored
uv run python examples/example_ensemble_synthetic.py \
    --code feff@localhost --python-code python3@localhost \
    --n-snapshots 6 --store-paths --plot-file ensemble.png

# Batch: one scheduler job per chunk, the mode intended for HPC. Produces a
# consolidated ExafsArchiveData instead of per-snapshot shards.
uv run python examples/example_ensemble_synthetic.py \
    --code feff@localhost --python-code python3@localhost \
    --n-snapshots 12 --batch-size 4 --plot-file ensemble.png

# Reuse one set of scattering potentials across every snapshot
uv run python examples/example_ensemble_synthetic.py \
    --code feff@localhost --n-snapshots 6 --precompute-potentials

The ensemble runs need roughly 4 GB of RAM; below that the daemon is killed mid-run and the only symptom is exit 137.

Pass a real TrajectoryData node, or use the synthetic-trajectory helper included in examples/ to run a quick end-to-end test without any MD data:

# Quick self-contained demo (generates a synthetic BCC-Fe trajectory)
uv run python examples/example_ensemble_synthetic.py \
    --code feff@localhost \
    --n-snapshots 6 --sigma 0.06 \
    --plot-file ensemble_exafs.png

# Optional: also store and merge per-path FEFF contributions
uv run python examples/example_ensemble_synthetic.py \
    --code feff@localhost \
    --n-snapshots 6 --sigma 0.06 \
    --store-paths --python-code python3@localhost --path-cw-threshold 5 \
    --plot-file ensemble_exafs.png

For a real trajectory, pass it via Python:

from aiida_feff.workflows.ensemble import EnsembleExafsWorkChain

wc = submit(EnsembleExafsWorkChain,
    trajectory=trajectory_node,       # TrajectoryData already in DB
    sample_interval=orm.Int(5),        # use every 5th frame
    parameters=params,
    code=orm.load_code('feff@localhost'),
    options=orm.Dict({"resources": {"num_machines": 1}, "max_wallclock_seconds": 600}),
    group_label=orm.Str("md-exafs/Fe-300K"),  # optional: bundle into a named group
)

4. Ensemble EXAFS on an HPC cluster (batch mode)

For ensembles of hundreds of snapshots, submitting one Slurm job per (frame, site) pair is inefficient: most HPC centres cap queued jobs per user, and the scheduler overhead for many short serial jobs is significant.

Batch mode solves this by packing many FEFF calculations into a single Slurm job. The EnsembleExafsWorkChain accepts a batch_size input; when set, it groups all (frame, site) pairs into chunks of that size and submits one FeffBatchCalculation per chunk instead of one FeffCalculation per pair. With batch_size = num_cores_per_node, the whole ensemble runs in as few Slurm jobs as ceil(N_frames × N_sites / num_cores).

Design choices

Why one CalcJob per chunk, not a Slurm array? AiiDA maps each CalcJob 1:1 to a scheduler job. There is no native job-array fan-out in AiiDA. The batch CalcJob is the standard pattern for running many serial tasks inside one allocation.

How does FEFF's module load get applied? The feff_code input is an InstalledCode whose prepend_text typically contains module load feff/8.5 (or similar). At submission time, prepare_for_submission reads feff_code.prepend_text and feff_code.filepath_executable and writes them into batch_config.json. On the compute node, the Python driver generates a small _run_feff.sh wrapper per run directory that sources the environment and calls FEFF — so the module load is applied correctly for every instance.

Why does batch mode require python_code? The python_code input (a Python interpreter on the HPC) doubles as the runner for the batch driver script. The driver script is a pure-Python file with no AiiDA dependency that uses concurrent.futures.ProcessPoolExecutor to launch FEFF instances in parallel. If you are not storing path contributions, the aggregation step is skipped, but the Python interpreter is still needed to run the driver.

How are precomputed potentials distributed? precompute_potentials=True triggers the existing potentials-only step — one FeffCalculation per absorber site (same as non-batch mode). Their RemoteData outputs are then passed to the batch CalcJob as a dynamic input namespace (remote_potentials.site_0000, remote_potentials.site_0001, …). prepare_for_submission populates remote_copy_list to copy each site's potential files (pot.pad, phase.pad, etc.) to potentials/site_XXXX/ in the working directory. The driver copies from there into each run directory before calling FEFF, so the SCF step is skipped for every snapshot.

Partial failures are isolated. If an individual FEFF run crashes, the driver logs the error and continues with the remaining runs. The batch job exits 0. The parser detects missing chi.dat files and skips those pairs; the workchain counts them as failures and produces a partial average (exit code 301) rather than aborting entirely.

Setup: register codes on the HPC

Register the FEFF executable. The --prepend-text is the environment setup that must run before FEFF can be called:

verdi code create core.code.installed \
    --label feff \
    --computer hpc \
    --filepath-executable /path/to/feff8l \
    --prepend-text "module load feff/8.5" \
    --default-calc-job-plugin feff.feff

Register the Python interpreter. This is used both as the batch runner and (when path_cw_threshold >= 0) for path aggregation. It must have larch, numpy, and h5py installed:

verdi code create core.code.installed \
    --label python3 \
    --computer hpc \
    --filepath-executable /path/to/venv/bin/python3 \
    --default-calc-job-plugin feff.feff_batch

Verify:

verdi code list
# feff@hpc (feff.feff)
# python3@hpc (feff.feff_batch)

Running a batched ensemble

from aiida import orm
from aiida.engine import submit
from aiida_feff.workflows.ensemble import EnsembleExafsWorkChain
from aiida_feff.data.parameters import FeffParameters

load_profile()

params = FeffParameters(dict={
    "edge": "K",
    "spectrum_type": "EXAFS",
    "radius": 6.0,
    "absorbing_atoms": "Cu",   # all Cu sites; or e.g. "Cu:0,1" for a subset
})

# One Slurm job per node; each job runs 64 FEFF instances in parallel.
# Set batch_size = number of cores you want to allocate per job.
CORES_PER_NODE = 64

wc = submit(
    EnsembleExafsWorkChain,
    trajectory=trajectory_node,          # TrajectoryData in DB
    sample_interval=orm.Int(1),
    parameters=params,
    code=orm.load_code("feff@hpc"),
    python_code=orm.load_code("python3@hpc"),
    precompute_potentials=orm.Bool(True),
    batch_size=orm.Int(CORES_PER_NODE),
    n_workers=orm.Int(CORES_PER_NODE),   # omit to fall back to $SLURM_CPUS_ON_NODE
    options=orm.Dict({
        "resources": {
            "num_machines": 1,
            "num_mpiprocs_per_machine": CORES_PER_NODE,
        },
        "max_wallclock_seconds": 3600,
        "queue_name": "regular",
    }),
    group_label=orm.Str("md-exafs/Cu-300K"),
)
print(f"Submitted workchain pk={wc.pk}")

With path contributions (stores per-path FEFF scattering factors, enables later σ² fitting):

wc = submit(
    EnsembleExafsWorkChain,
    trajectory=trajectory_node,
    sample_interval=orm.Int(1),
    parameters=params,
    code=orm.load_code("feff@hpc"),
    python_code=orm.load_code("python3@hpc"),
    precompute_potentials=orm.Bool(True),
    path_cw_threshold=orm.Float(5.0),  # keep paths with ≥5% peak amplitude
    path_r_bin=orm.Float(0.15),
    batch_size=orm.Int(CORES_PER_NODE),
    n_workers=orm.Int(CORES_PER_NODE),
    options=orm.Dict({
        "resources": {
            "num_machines": 1,
            "num_mpiprocs_per_machine": CORES_PER_NODE,
        },
        "max_wallclock_seconds": 7200,
        "queue_name": "regular",
    }),
)

Sizing guide

N_frames × N_sites Recommended batch_size Slurm jobs
≤ 64 = total pairs 1
65 – 512 = cores per node (e.g. 64 or 128) 2 – 8
> 512 = cores per node ceil(N / cores)

If n_workers is omitted, the driver reads $SLURM_CPUS_ON_NODE at runtime, then falls back to $SLURM_NTASKS, then 1. Set it explicitly if your HPC uses --cpus-per-task instead of --ntasks.

Checking status

verdi process status <pk>
# EnsembleExafsWorkChain (pk=<pk>) [ProcessState.RUNNING] [3:submit_batch_calculations]
#   ├── FeffCalculation (pk=...) [FINISHED]  ← potentials-only, 1 per site
#   ├── FeffBatchCalculation (pk=...) [RUNNING]  ← batch 0, 64 pairs
#   └── FeffBatchCalculation (pk=...) [CREATED]  ← batch 1, 64 pairs

verdi process report <pk>           # human-readable log
verdi calcjob outputcat <pk> batch.log   # driver stdout for a batch CalcJob
verdi calcjob outputcat <pk> batch_err.log  # per-run FEFF pass/fail summary

Retrieving results (same as non-batch mode)

from aiida.orm import load_node

wc = load_node(<pk>)
grand_average  = wc.outputs.averaged_xas.all          # XasData
per_site_avg   = wc.outputs.averaged_xas.site_0000    # XasData, one per absorber
n_failed       = wc.outputs.n_failed.value            # int
path_contrib   = wc.outputs.path_contributions        # PathContributionsData (if stored)

5. Post-processing with larch (optional)

from aiida_feff.calcfunctions.larch import chi_k_to_r
from aiida.orm import Dict

# Fourier transform (provenance-tracked)
chir = chi_k_to_r(xas_data=xas, ft_params=Dict({"kmin":3, "kmax":14, "kweight":2}))
# Defaults, including window="kaiser", are in aiida_feff.calcfunctions.larch.FT_DEFAULTS.

6. Plotting (optional)

from aiida_feff.visualise import plot_chi_k, plot_chi_r

# k²χ(k)
fig = plot_chi_k(xas, kweight=2)

# χ(R) — pass the output of chi_k_to_r directly (FT already tracked)
fig = plot_chi_r(chir)

# Overlay multiple spectra on one axes
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
for node in ensemble_xas_nodes:
    plot_chi_k(node, ax=ax, label=node.label, plot_envelope=True)
plt.show()

7. Debye-Waller σ² from an MD trajectory (optional)

Per-path MSRD (σ²) is computed directly from the trajectory by md-exafs, which this plugin depends on.

This step is deliberately not provenance-tracked. σ² is cheap to recompute and the trajectory it derives from is already a stored node, so recording the result would add graph weight without adding recoverable information. The store_msrd / store_adp calcfunction wrappers that earlier versions shipped have been removed for that reason; call md-exafs directly.

from md_exafs.debye_waller import calculate_grouped_msrd

from aiida_feff.utils import trajectory_to_structures

structures = [s.get_ase() for s in trajectory_to_structures(traj_node)]

# `cutoff` must stay below the inscribed-sphere radius of the cell, or the
# minimum-image convention picks the wrong neighbour and biases σ² low.
# Build a supercell rather than raising the cutoff.
two_body, three_body = calculate_grouped_msrd(
    structures,
    central_indices=[0],     # zero-based absorber indices
    central_label="Fe",
    cutoff=3.5,              # neighbour search radius in Å
    cutoff_3body=3.0,        # include 3-body paths (omit to skip)
)

for group in sorted(two_body, key=lambda g: g["reff"]):
    print(f"{group['scatterer']}: reff={group['reff']:.3f} Å  σ²={group['sigma2']:.5f} Ų")
# Fe: reff=2.481 Å  σ²=0.00612 Ų
# Fe: reff=4.052 Å  σ²=0.00891 Ų

The resulting σ² values can be passed straight to aiida_feff.calcfunctions.exafs.total_chi as a scatterer -> σ² mapping.

Per-atom B-factors and full U tensors come from the same module via md_exafs.debye_waller.compute_adp_results.

CLI

verdi data feff list                  # list all FeffParameters / XasData nodes
verdi data feff export <PK>           # preview feff.inp from a FeffParameters node
verdi data feff show <PK>             # inspect arrays in an XasData node

Architecture overview

Single-job mode (localhost / small ensembles)

TrajectoryData + FeffParameters
        │
        ▼
EnsembleExafsWorkChain
  ├─ [optional] FeffCalculation × N_sites   ← potentials-only (CONTROL 1 1 1 0 0 0)
  │
  ├─ FeffCalculation × (N_frames × N_sites) ← one Slurm job each
  │    └─ FeffParser → XasData
  │
  ├─ create_serial_shard (calcfunction) → one shard, as the batch driver writes
  │
  └─ merge_exafs_shards (calcfunction) → ExafsArchiveData (archive)
       └─ archive_to_averaged_xas (calcfunction) → averaged XasData per site + grand average

Batch mode (HPC, hundreds of calculations)

TrajectoryData + FeffParameters
        │
        ▼
EnsembleExafsWorkChain (batch_size=64)
  ├─ FeffCalculation × N_sites        ← potentials-only, one Slurm job each
  │    └─ RemoteData (pot.pad, phase.pad, …)
  │
  ├─ FeffBatchCalculation             ← ONE Slurm job, 64 FEFF instances in parallel
  │    ├─ snap_0000_site_0000/feff.inp
  │    ├─ snap_0001_site_0000/feff.inp
  │    │   … (up to batch_size pairs)
  │    └─ _run_batch.py (driver, concurrent.futures, 1 worker/core)
  │         └─ FeffBatchParser → xas_data.snap_FFFF_site_SSSS (dynamic namespace)
  │
  ├─ FeffBatchCalculation             ← next chunk, another Slurm job
  │    └─ …
  │
  └─ merge_exafs_shards (calcfunction) → ExafsArchiveData (archive)
       └─ archive_to_averaged_xas (calcfunction) → averaged XasData per site + grand average

Both routes end at the same merge, so the ensemble average has one implementation and archive is always produced.

The FEFF environment (module loads, etc.) is read from feff_code.prepend_text at AiiDA submission time and embedded in batch_config.json. On the compute node, the driver generates a small _run_feff.sh bash wrapper per run directory, so the environment is correctly applied for every FEFF instance without requiring any extra code to be installed on the HPC.

Development

git clone https://github.com/stfc/aiida-feff
cd aiida-feff
uv sync --locked --extra testing --extra plots
uv run pytest tests/

# Lint exactly as CI does
uv run pre-commit run --all-files

Relationship to larch-cli

This plugin is designed to supersede a CLI tool built around larch + FEFF. The key differences:

larch-cli aiida-feff
Linear script execution DAG of provenance-tracked nodes
Manual file management AiiDA handles staging to/from HPC
Results as files on disk All inputs/outputs stored in the database
Manual batching (N workers per node) EnsembleExafsWorkChain(batch_size=N)

License

BSD 3-Clause License. See LICENSE for details.

Metadata

Release files for aiida-feff 0.1.0a1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aiida-feff 0.1.0a1
File Size Uploaded
aiida_feff-0.1.0a1.tar.gz 123.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aiida-feff 0.1.0a1
File Interpreter ABI Platform
aiida_feff-0.1.0a1-py3-none-any.whl Python 3 none any Details

Total release size: 209.2 kB

Release files / aiida_feff-0.1.0a1.tar.gz

Download URL aiida_feff-0.1.0a1.tar.gz
Size 123.9 kB
Tags Source
SHA-256 checksum
How to use checksums
54b585808857ea2558014faac4346c6e33bc5507e4e34060fcff97a26d0d5277
BLAKE2b-256 checksum
How to use checksums
8f2eb3445a6e3a666cf7dc4c1b3be3f6d58ae9a3388258621799123c70985b4b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / aiida_feff-0.1.0a1-py3-none-any.whl

Download URL aiida_feff-0.1.0a1-py3-none-any.whl
Size 85.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e63dc9c4e9fc748a1e5cb71d3dbefb3db497549ebc73b2941a6fd8249ff50a2c
BLAKE2b-256 checksum
How to use checksums
0db2eaca5ae1222f36db211ec39f3f655d7b8e67fc9e904d034118652fe5556e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0a1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page