aiida-feff 0.1.0a1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. aiida_feff-0.1.0a1/LICENSE +28 -0
  2. aiida_feff-0.1.0a1/PKG-INFO +587 -0
  3. aiida_feff-0.1.0a1/README.md +543 -0
  4. aiida_feff-0.1.0a1/pyproject.toml +193 -0
  5. aiida_feff-0.1.0a1/setup.cfg +4 -0
  6. aiida_feff-0.1.0a1/src/aiida_feff/__init__.py +14 -0
  7. aiida_feff-0.1.0a1/src/aiida_feff/calcfunctions/__init__.py +0 -0
  8. aiida_feff-0.1.0a1/src/aiida_feff/calcfunctions/archive.py +129 -0
  9. aiida_feff-0.1.0a1/src/aiida_feff/calcfunctions/exafs.py +111 -0
  10. aiida_feff-0.1.0a1/src/aiida_feff/calcfunctions/experimental.py +200 -0
  11. aiida_feff-0.1.0a1/src/aiida_feff/calcfunctions/larch.py +102 -0
  12. aiida_feff-0.1.0a1/src/aiida_feff/calcfunctions/path_contributions.py +192 -0
  13. aiida_feff-0.1.0a1/src/aiida_feff/calculations/__init__.py +0 -0
  14. aiida_feff-0.1.0a1/src/aiida_feff/calculations/_aggregate_paths.py +424 -0
  15. aiida_feff-0.1.0a1/src/aiida_feff/calculations/_run_batch.py +465 -0
  16. aiida_feff-0.1.0a1/src/aiida_feff/calculations/feff.py +506 -0
  17. aiida_feff-0.1.0a1/src/aiida_feff/calculations/feff_batch.py +482 -0
  18. aiida_feff-0.1.0a1/src/aiida_feff/cli.py +130 -0
  19. aiida_feff-0.1.0a1/src/aiida_feff/constants.py +19 -0
  20. aiida_feff-0.1.0a1/src/aiida_feff/data/__init__.py +0 -0
  21. aiida_feff-0.1.0a1/src/aiida_feff/data/archive.py +163 -0
  22. aiida_feff-0.1.0a1/src/aiida_feff/data/parameters.py +250 -0
  23. aiida_feff-0.1.0a1/src/aiida_feff/data/pathcontributions.py +288 -0
  24. aiida_feff-0.1.0a1/src/aiida_feff/data/xasdata.py +129 -0
  25. aiida_feff-0.1.0a1/src/aiida_feff/parsers/__init__.py +0 -0
  26. aiida_feff-0.1.0a1/src/aiida_feff/parsers/feff.py +241 -0
  27. aiida_feff-0.1.0a1/src/aiida_feff/parsers/feff_batch.py +165 -0
  28. aiida_feff-0.1.0a1/src/aiida_feff/utils.py +139 -0
  29. aiida_feff-0.1.0a1/src/aiida_feff/versions.py +67 -0
  30. aiida_feff-0.1.0a1/src/aiida_feff/visualise.py +225 -0
  31. aiida_feff-0.1.0a1/src/aiida_feff/workflows/__init__.py +0 -0
  32. aiida_feff-0.1.0a1/src/aiida_feff/workflows/ensemble.py +993 -0
  33. aiida_feff-0.1.0a1/src/aiida_feff.egg-info/PKG-INFO +587 -0
  34. aiida_feff-0.1.0a1/src/aiida_feff.egg-info/SOURCES.txt +51 -0
  35. aiida_feff-0.1.0a1/src/aiida_feff.egg-info/dependency_links.txt +1 -0
  36. aiida_feff-0.1.0a1/src/aiida_feff.egg-info/entry_points.txt +19 -0
  37. aiida_feff-0.1.0a1/src/aiida_feff.egg-info/requires.txt +24 -0
  38. aiida_feff-0.1.0a1/src/aiida_feff.egg-info/top_level.txt +1 -0
  39. aiida_feff-0.1.0a1/tests/test_aggregate_paths.py +287 -0
  40. aiida_feff-0.1.0a1/tests/test_batch.py +557 -0
  41. aiida_feff-0.1.0a1/tests/test_calculations.py +350 -0
  42. aiida_feff-0.1.0a1/tests/test_cli.py +72 -0
  43. aiida_feff-0.1.0a1/tests/test_data.py +411 -0
  44. aiida_feff-0.1.0a1/tests/test_ensemble.py +123 -0
  45. aiida_feff-0.1.0a1/tests/test_ensemble_run.py +763 -0
  46. aiida_feff-0.1.0a1/tests/test_exafs.py +227 -0
  47. aiida_feff-0.1.0a1/tests/test_experimental.py +188 -0
  48. aiida_feff-0.1.0a1/tests/test_larch_ft.py +180 -0
  49. aiida_feff-0.1.0a1/tests/test_parsers.py +260 -0
  50. aiida_feff-0.1.0a1/tests/test_path_contributions.py +554 -0
  51. aiida_feff-0.1.0a1/tests/test_utils.py +103 -0
  52. aiida_feff-0.1.0a1/tests/test_versions.py +67 -0
  53. aiida_feff-0.1.0a1/tests/test_visualise.py +137 -0
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, UKRI Science and Technology Facilities Council
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,587 @@
1
+ Metadata-Version: 2.4
2
+ Name: aiida-feff
3
+ Version: 0.1.0a1
4
+ Summary: AiiDA plugin for FEFF: provenance-tracked EXAFS calculations
5
+ Author-email: "J. Kane Shenton" <kane.shenton@stfc.ac.uk>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Home, https://github.com/stfc/aiida-feff
8
+ Project-URL: Documentation, https://github.com/stfc/aiida-feff
9
+ Project-URL: Source, https://github.com/stfc/aiida-feff
10
+ Project-URL: Tracker, https://github.com/stfc/aiida-feff/issues
11
+ Keywords: aiida,feff,exafs,xas,md-exafs,larch,xraylarch,ase,pymatgen
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: AiiDA
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
19
+ Classifier: Topic :: Scientific/Engineering :: Physics
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: aiida-core<3,>=2.3
24
+ Requires-Dist: numpy>=1.21
25
+ Requires-Dist: pymatgen<2026,>=2024.1
26
+ Requires-Dist: ase>=3.22
27
+ Requires-Dist: xraylarch>=0.9.80
28
+ Requires-Dist: h5py>=3.0
29
+ Requires-Dist: md-exafs<0.4,>=0.3.0
30
+ Provides-Extra: plots
31
+ Requires-Dist: matplotlib>=3.6; extra == "plots"
32
+ Provides-Extra: testing
33
+ Requires-Dist: pytest>=7.0; extra == "testing"
34
+ Requires-Dist: pytest-cov; extra == "testing"
35
+ Requires-Dist: coveralls; extra == "testing"
36
+ Provides-Extra: pre-commit
37
+ Requires-Dist: pre-commit>=3.0; extra == "pre-commit"
38
+ Requires-Dist: ruff>=0.6.0; extra == "pre-commit"
39
+ Requires-Dist: mypy>=1.10; extra == "pre-commit"
40
+ Provides-Extra: docs
41
+ Requires-Dist: sphinx>=5; extra == "docs"
42
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
43
+ Dynamic: license-file
44
+
45
+ [![Release](https://img.shields.io/github/v/release/stfc/aiida-feff)](https://github.com/stfc/aiida-feff/releases)
46
+ [![PyPI](https://img.shields.io/pypi/v/aiida-feff)](https://pypi.org/project/aiida-feff/)
47
+ [![Pipeline Status](https://github.com/stfc/aiida-feff/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/stfc/aiida-feff/actions)
48
+ [![Coverage Status](https://coveralls.io/repos/github/stfc/aiida-feff/badge.svg?branch=main)](https://coveralls.io/github/stfc/aiida-feff?branch=main)
49
+
50
+ # aiida-feff
51
+
52
+ An [AiiDA](https://www.aiida.net) plugin for the
53
+ [FEFF](http://feffproject.org) real-space multiple-scattering code,
54
+ enabling fully provenance-tracked EXAFS calculations —
55
+ including ensemble averaging over MD snapshots via
56
+ [larch](https://xraypy.github.io/xraylarch/).
57
+
58
+ ---
59
+
60
+ ## Features
61
+
62
+ | Component | Description |
63
+ |-----------|-------------|
64
+ | `FeffCalculation` | CalcJob wrapping a single FEFF run; builds `feff.inp` from `StructureData` + `FeffParameters` |
65
+ | `FeffParser` | Parses `chi.dat` into `XasData` output nodes |
66
+ | `FeffBatchCalculation` | CalcJob that runs *N* FEFF instances in one Slurm job (one per core); for HPC ensemble runs |
67
+ | `FeffBatchParser` | Parses all per-snapshot outputs from a batch job into a dynamic namespace of `XasData` nodes |
68
+ | `FeffParameters` | Validated `Dict` subclass for FEFF control cards |
69
+ | `XasData` | `ArrayData` subclass storing χ(k), plus μ(E) on experimental imports |
70
+ | `EnsembleExafsWorkChain` | Fan-out over MD snapshots → one `ExafsArchiveData` and ensemble-averaged χ(k); single-job and batch modes |
71
+ | `calcfunctions` | Larch post-processing (FT), archive projection, experimental imports |
72
+ | `visualise` | Matplotlib helpers for χ(k) and χ(R) plots |
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ pip install aiida-feff
78
+ verdi plugin list aiida.calculations # should show feff.feff and feff.feff_batch
79
+ verdi plugin list aiida.workflows # should show feff.ensemble
80
+ ```
81
+
82
+ For plotting (optional):
83
+
84
+ ```bash
85
+ pip install aiida-feff[plots]
86
+ ```
87
+
88
+ ## Quick start
89
+
90
+ ### Experimental spectrum import
91
+
92
+ `aiida_feff.calcfunctions.experimental.import_experimental_spectrum` adapts
93
+ Larch's readers to a provenance-tracked `XasData` node. Pass a stored
94
+ `SinglefileData` upload and a `Dict` of Larch import options, such as plain
95
+ text column labels or a selected Athena group. The raw file remains an input
96
+ node; the plugin does not implement a competing file parser.
97
+
98
+ `scale_simulated_spectrum` stores a separate $S_0^2$/ΔE₀-adjusted simulated
99
+ `XasData` node for reproducible experimental comparisons.
100
+
101
+ ### 1. Register your FEFF code
102
+
103
+ ```bash
104
+ verdi code setup \
105
+ --label feff \
106
+ --computer localhost \
107
+ --input-plugin feff.feff \
108
+ --remote-abs-path /path/to/feff
109
+ ```
110
+
111
+ ### 2. Run a single-site EXAFS calculation
112
+
113
+ ```python
114
+ from aiida import load_profile, orm
115
+ from aiida.engine import run
116
+ from aiida_feff.calculations.feff import FeffCalculation
117
+ from aiida_feff.data.parameters import FeffParameters
118
+
119
+ load_profile()
120
+
121
+ structure = orm.StructureData(cell=[[2.87,0,0],[0,2.87,0],[0,0,2.87]])
122
+ structure.append_atom(position=(0,0,0), symbols='Fe')
123
+ structure.append_atom(position=(1.435,1.435,1.435), symbols='Fe')
124
+
125
+ params = FeffParameters(dict={
126
+ "edge": "K",
127
+ "spectrum_type": "EXAFS",
128
+ "radius": 5.5, # FEFF RPATH / cluster radius in Å
129
+ "s02": 1.0,
130
+ })
131
+
132
+ result = run(FeffCalculation,
133
+ code=orm.load_code('feff@localhost'),
134
+ structure=structure,
135
+ parameters=params,
136
+ metadata={"options": {
137
+ "resources": {"num_machines": 1},
138
+ "max_wallclock_seconds": 300,
139
+ }},
140
+ )
141
+
142
+ xas = result['xas_data']
143
+ print(f"chi(k) shape: {xas.chi_k.shape}")
144
+ ```
145
+
146
+ ### 3. Ensemble EXAFS from an MD trajectory (localhost / dev)
147
+
148
+ > **Recommended environment:** the DevContainer in `.devcontainer/` gives you
149
+ > a full IDE against a *real* AiiDA profile: SQLite storage, a RabbitMQ broker,
150
+ > a running daemon, and both `feff@localhost` and `python3@localhost`
151
+ > registered. Calculations you run there produce real nodes and a real
152
+ > provenance graph, so `verdi` works as it would anywhere else.
153
+ >
154
+ > It runs on the host's own architecture, so it is equally at home in
155
+ > **GitHub Codespaces** (Code → Codespaces → Create) and on a local Docker or
156
+ > Podman. Verified end to end on Apple Silicon: `post-create.sh` completes,
157
+ > FEFF8L runs, and the full test suite passes inside the container.
158
+ >
159
+ > FEFF8L is not downloaded; it ships inside the `xraylarch` dependency and the
160
+ > container points a code at it. On arm64 those x86_64 binaries run through
161
+ > the runtime's qemu handler, for which post-create installs the x86_64
162
+ > loader. No PostgreSQL is involved: storage is `core.sqlite_dos`, the same
163
+ > backend the tests use.
164
+ >
165
+ > **Podman users:** set `dockerComposeFile` in
166
+ > `.devcontainer/devcontainer.json` to
167
+ > `["docker-compose.yml", "docker-compose.podman.yml"]`. The override adds
168
+ > `userns_mode: keep-id`, which Docker Engine rejects and which therefore
169
+ > cannot live in the base file.
170
+ >
171
+ > *Outside the container you need a broker and daemon running, a working FEFF
172
+ > executable, and a Python interpreter registered as an installed code
173
+ > (`verdi code create core.code.installed ...`, e.g. `python3@localhost`) for
174
+ > path aggregation.*
175
+
176
+ The synthetic example covers the whole pipeline and doubles as the smoke test
177
+ CI runs:
178
+
179
+ ```bash
180
+ # Serial: one scheduler job per snapshot, with per-path contributions stored
181
+ uv run python examples/example_ensemble_synthetic.py \
182
+ --code feff@localhost --python-code python3@localhost \
183
+ --n-snapshots 6 --store-paths --plot-file ensemble.png
184
+
185
+ # Batch: one scheduler job per chunk, the mode intended for HPC. Produces a
186
+ # consolidated ExafsArchiveData instead of per-snapshot shards.
187
+ uv run python examples/example_ensemble_synthetic.py \
188
+ --code feff@localhost --python-code python3@localhost \
189
+ --n-snapshots 12 --batch-size 4 --plot-file ensemble.png
190
+
191
+ # Reuse one set of scattering potentials across every snapshot
192
+ uv run python examples/example_ensemble_synthetic.py \
193
+ --code feff@localhost --n-snapshots 6 --precompute-potentials
194
+ ```
195
+
196
+ The ensemble runs need roughly 4 GB of RAM; below that the daemon is killed
197
+ mid-run and the only symptom is exit 137.
198
+
199
+ Pass a real `TrajectoryData` node, or use the synthetic-trajectory helper
200
+ included in `examples/` to run a quick end-to-end test without any MD data:
201
+
202
+ ```bash
203
+ # Quick self-contained demo (generates a synthetic BCC-Fe trajectory)
204
+ uv run python examples/example_ensemble_synthetic.py \
205
+ --code feff@localhost \
206
+ --n-snapshots 6 --sigma 0.06 \
207
+ --plot-file ensemble_exafs.png
208
+
209
+ # Optional: also store and merge per-path FEFF contributions
210
+ uv run python examples/example_ensemble_synthetic.py \
211
+ --code feff@localhost \
212
+ --n-snapshots 6 --sigma 0.06 \
213
+ --store-paths --python-code python3@localhost --path-cw-threshold 5 \
214
+ --plot-file ensemble_exafs.png
215
+ ```
216
+
217
+ For a real trajectory, pass it via Python:
218
+
219
+ ```python
220
+ from aiida_feff.workflows.ensemble import EnsembleExafsWorkChain
221
+
222
+ wc = submit(EnsembleExafsWorkChain,
223
+ trajectory=trajectory_node, # TrajectoryData already in DB
224
+ sample_interval=orm.Int(5), # use every 5th frame
225
+ parameters=params,
226
+ code=orm.load_code('feff@localhost'),
227
+ options=orm.Dict({"resources": {"num_machines": 1}, "max_wallclock_seconds": 600}),
228
+ group_label=orm.Str("md-exafs/Fe-300K"), # optional: bundle into a named group
229
+ )
230
+ ```
231
+
232
+ ### 4. Ensemble EXAFS on an HPC cluster (batch mode)
233
+
234
+ For ensembles of hundreds of snapshots, submitting one Slurm job per
235
+ `(frame, site)` pair is inefficient: most HPC centres cap queued jobs per user,
236
+ and the scheduler overhead for many short serial jobs is significant.
237
+
238
+ **Batch mode** solves this by packing many FEFF calculations into a single Slurm
239
+ job. The `EnsembleExafsWorkChain` accepts a `batch_size` input; when set, it
240
+ groups all `(frame, site)` pairs into chunks of that size and submits one
241
+ `FeffBatchCalculation` per chunk instead of one `FeffCalculation` per pair.
242
+ With `batch_size = num_cores_per_node`, the whole ensemble runs in as few Slurm
243
+ jobs as `ceil(N_frames × N_sites / num_cores)`.
244
+
245
+ #### Design choices
246
+
247
+ **Why one CalcJob per chunk, not a Slurm array?**
248
+ AiiDA maps each `CalcJob` 1:1 to a scheduler job. There is no native
249
+ job-array fan-out in AiiDA. The batch CalcJob is the standard pattern for
250
+ running many serial tasks inside one allocation.
251
+
252
+ **How does FEFF's module load get applied?**
253
+ The `feff_code` input is an `InstalledCode` whose `prepend_text` typically
254
+ contains `module load feff/8.5` (or similar). At submission time,
255
+ `prepare_for_submission` reads `feff_code.prepend_text` and
256
+ `feff_code.filepath_executable` and writes them into `batch_config.json`.
257
+ On the compute node, the Python driver generates a small `_run_feff.sh`
258
+ wrapper per run directory that sources the environment and calls FEFF —
259
+ so the module load is applied correctly for every instance.
260
+
261
+ **Why does batch mode require `python_code`?**
262
+ The `python_code` input (a Python interpreter on the HPC) doubles as
263
+ the **runner** for the batch driver script. The driver script is a pure-Python
264
+ file with no AiiDA dependency that uses `concurrent.futures.ProcessPoolExecutor`
265
+ to launch FEFF instances in parallel. If you are not storing path
266
+ contributions, the aggregation step is skipped, but the Python interpreter is
267
+ still needed to run the driver.
268
+
269
+ **How are precomputed potentials distributed?**
270
+ `precompute_potentials=True` triggers the existing potentials-only step —
271
+ one `FeffCalculation` per absorber site (same as non-batch mode). Their
272
+ `RemoteData` outputs are then passed to the batch CalcJob as a dynamic input
273
+ namespace (`remote_potentials.site_0000`, `remote_potentials.site_0001`, …).
274
+ `prepare_for_submission` populates `remote_copy_list` to copy each site's
275
+ potential files (`pot.pad`, `phase.pad`, etc.) to `potentials/site_XXXX/` in
276
+ the working directory. The driver copies from there into each run directory
277
+ before calling FEFF, so the SCF step is skipped for every snapshot.
278
+
279
+ **Partial failures are isolated.**
280
+ If an individual FEFF run crashes, the driver logs the error and continues with
281
+ the remaining runs. The batch job exits 0. The parser detects missing
282
+ `chi.dat` files and skips those pairs; the workchain counts them as failures
283
+ and produces a partial average (exit code 301) rather than aborting entirely.
284
+
285
+ #### Setup: register codes on the HPC
286
+
287
+ Register the FEFF executable. The `--prepend-text` is the environment setup
288
+ that must run before FEFF can be called:
289
+
290
+ ```bash
291
+ verdi code create core.code.installed \
292
+ --label feff \
293
+ --computer hpc \
294
+ --filepath-executable /path/to/feff8l \
295
+ --prepend-text "module load feff/8.5" \
296
+ --default-calc-job-plugin feff.feff
297
+ ```
298
+
299
+ Register the Python interpreter. This is used both as the batch runner and
300
+ (when `path_cw_threshold >= 0`) for path aggregation. It must have `larch`,
301
+ `numpy`, and `h5py` installed:
302
+
303
+ ```bash
304
+ verdi code create core.code.installed \
305
+ --label python3 \
306
+ --computer hpc \
307
+ --filepath-executable /path/to/venv/bin/python3 \
308
+ --default-calc-job-plugin feff.feff_batch
309
+ ```
310
+
311
+ Verify:
312
+
313
+ ```bash
314
+ verdi code list
315
+ # feff@hpc (feff.feff)
316
+ # python3@hpc (feff.feff_batch)
317
+ ```
318
+
319
+ #### Running a batched ensemble
320
+
321
+ ```python
322
+ from aiida import orm
323
+ from aiida.engine import submit
324
+ from aiida_feff.workflows.ensemble import EnsembleExafsWorkChain
325
+ from aiida_feff.data.parameters import FeffParameters
326
+
327
+ load_profile()
328
+
329
+ params = FeffParameters(dict={
330
+ "edge": "K",
331
+ "spectrum_type": "EXAFS",
332
+ "radius": 6.0,
333
+ "absorbing_atoms": "Cu", # all Cu sites; or e.g. "Cu:0,1" for a subset
334
+ })
335
+
336
+ # One Slurm job per node; each job runs 64 FEFF instances in parallel.
337
+ # Set batch_size = number of cores you want to allocate per job.
338
+ CORES_PER_NODE = 64
339
+
340
+ wc = submit(
341
+ EnsembleExafsWorkChain,
342
+ trajectory=trajectory_node, # TrajectoryData in DB
343
+ sample_interval=orm.Int(1),
344
+ parameters=params,
345
+ code=orm.load_code("feff@hpc"),
346
+ python_code=orm.load_code("python3@hpc"),
347
+ precompute_potentials=orm.Bool(True),
348
+ batch_size=orm.Int(CORES_PER_NODE),
349
+ n_workers=orm.Int(CORES_PER_NODE), # omit to fall back to $SLURM_CPUS_ON_NODE
350
+ options=orm.Dict({
351
+ "resources": {
352
+ "num_machines": 1,
353
+ "num_mpiprocs_per_machine": CORES_PER_NODE,
354
+ },
355
+ "max_wallclock_seconds": 3600,
356
+ "queue_name": "regular",
357
+ }),
358
+ group_label=orm.Str("md-exafs/Cu-300K"),
359
+ )
360
+ print(f"Submitted workchain pk={wc.pk}")
361
+ ```
362
+
363
+ **With path contributions** (stores per-path FEFF scattering factors, enables
364
+ later σ² fitting):
365
+
366
+ ```python
367
+ wc = submit(
368
+ EnsembleExafsWorkChain,
369
+ trajectory=trajectory_node,
370
+ sample_interval=orm.Int(1),
371
+ parameters=params,
372
+ code=orm.load_code("feff@hpc"),
373
+ python_code=orm.load_code("python3@hpc"),
374
+ precompute_potentials=orm.Bool(True),
375
+ path_cw_threshold=orm.Float(5.0), # keep paths with ≥5% peak amplitude
376
+ path_r_bin=orm.Float(0.15),
377
+ batch_size=orm.Int(CORES_PER_NODE),
378
+ n_workers=orm.Int(CORES_PER_NODE),
379
+ options=orm.Dict({
380
+ "resources": {
381
+ "num_machines": 1,
382
+ "num_mpiprocs_per_machine": CORES_PER_NODE,
383
+ },
384
+ "max_wallclock_seconds": 7200,
385
+ "queue_name": "regular",
386
+ }),
387
+ )
388
+ ```
389
+
390
+ #### Sizing guide
391
+
392
+ | N\_frames × N\_sites | Recommended `batch_size` | Slurm jobs |
393
+ |---|---|---|
394
+ | ≤ 64 | = total pairs | 1 |
395
+ | 65 – 512 | = cores per node (e.g. 64 or 128) | 2 – 8 |
396
+ | > 512 | = cores per node | `ceil(N / cores)` |
397
+
398
+ If `n_workers` is omitted, the driver reads `$SLURM_CPUS_ON_NODE` at runtime,
399
+ then falls back to `$SLURM_NTASKS`, then 1. Set it explicitly if your HPC
400
+ uses `--cpus-per-task` instead of `--ntasks`.
401
+
402
+ #### Checking status
403
+
404
+ ```bash
405
+ verdi process status <pk>
406
+ # EnsembleExafsWorkChain (pk=<pk>) [ProcessState.RUNNING] [3:submit_batch_calculations]
407
+ # ├── FeffCalculation (pk=...) [FINISHED] ← potentials-only, 1 per site
408
+ # ├── FeffBatchCalculation (pk=...) [RUNNING] ← batch 0, 64 pairs
409
+ # └── FeffBatchCalculation (pk=...) [CREATED] ← batch 1, 64 pairs
410
+
411
+ verdi process report <pk> # human-readable log
412
+ verdi calcjob outputcat <pk> batch.log # driver stdout for a batch CalcJob
413
+ verdi calcjob outputcat <pk> batch_err.log # per-run FEFF pass/fail summary
414
+ ```
415
+
416
+ #### Retrieving results (same as non-batch mode)
417
+
418
+ ```python
419
+ from aiida.orm import load_node
420
+
421
+ wc = load_node(<pk>)
422
+ grand_average = wc.outputs.averaged_xas.all # XasData
423
+ per_site_avg = wc.outputs.averaged_xas.site_0000 # XasData, one per absorber
424
+ n_failed = wc.outputs.n_failed.value # int
425
+ path_contrib = wc.outputs.path_contributions # PathContributionsData (if stored)
426
+ ```
427
+
428
+ ### 5. Post-processing with larch (optional)
429
+
430
+ ```python
431
+ from aiida_feff.calcfunctions.larch import chi_k_to_r
432
+ from aiida.orm import Dict
433
+
434
+ # Fourier transform (provenance-tracked)
435
+ chir = chi_k_to_r(xas_data=xas, ft_params=Dict({"kmin":3, "kmax":14, "kweight":2}))
436
+ # Defaults, including window="kaiser", are in aiida_feff.calcfunctions.larch.FT_DEFAULTS.
437
+ ```
438
+
439
+ ### 6. Plotting (optional)
440
+
441
+ ```python
442
+ from aiida_feff.visualise import plot_chi_k, plot_chi_r
443
+
444
+ # k²χ(k)
445
+ fig = plot_chi_k(xas, kweight=2)
446
+
447
+ # χ(R) — pass the output of chi_k_to_r directly (FT already tracked)
448
+ fig = plot_chi_r(chir)
449
+
450
+ # Overlay multiple spectra on one axes
451
+ import matplotlib.pyplot as plt
452
+ fig, ax = plt.subplots()
453
+ for node in ensemble_xas_nodes:
454
+ plot_chi_k(node, ax=ax, label=node.label, plot_envelope=True)
455
+ plt.show()
456
+ ```
457
+
458
+ ### 7. Debye-Waller σ² from an MD trajectory (optional)
459
+
460
+ Per-path MSRD (σ²) is computed directly from the trajectory by
461
+ [md-exafs](https://pypi.org/project/md-exafs/), which this plugin depends on.
462
+
463
+ **This step is deliberately not provenance-tracked.** σ² is cheap to recompute
464
+ and the trajectory it derives from is already a stored node, so recording the
465
+ result would add graph weight without adding recoverable information. The
466
+ `store_msrd` / `store_adp` calcfunction wrappers that earlier versions shipped
467
+ have been removed for that reason; call md-exafs directly.
468
+
469
+ ```python
470
+ from md_exafs.debye_waller import calculate_grouped_msrd
471
+
472
+ from aiida_feff.utils import trajectory_to_structures
473
+
474
+ structures = [s.get_ase() for s in trajectory_to_structures(traj_node)]
475
+
476
+ # `cutoff` must stay below the inscribed-sphere radius of the cell, or the
477
+ # minimum-image convention picks the wrong neighbour and biases σ² low.
478
+ # Build a supercell rather than raising the cutoff.
479
+ two_body, three_body = calculate_grouped_msrd(
480
+ structures,
481
+ central_indices=[0], # zero-based absorber indices
482
+ central_label="Fe",
483
+ cutoff=3.5, # neighbour search radius in Å
484
+ cutoff_3body=3.0, # include 3-body paths (omit to skip)
485
+ )
486
+
487
+ for group in sorted(two_body, key=lambda g: g["reff"]):
488
+ print(f"{group['scatterer']}: reff={group['reff']:.3f} Å σ²={group['sigma2']:.5f} Ų")
489
+ # Fe: reff=2.481 Å σ²=0.00612 Ų
490
+ # Fe: reff=4.052 Å σ²=0.00891 Ų
491
+ ```
492
+
493
+ The resulting σ² values can be passed straight to
494
+ `aiida_feff.calcfunctions.exafs.total_chi` as a `scatterer -> σ²` mapping.
495
+
496
+ Per-atom B-factors and full U tensors come from the same module via
497
+ `md_exafs.debye_waller.compute_adp_results`.
498
+
499
+ ## CLI
500
+
501
+ ```bash
502
+ verdi data feff list # list all FeffParameters / XasData nodes
503
+ verdi data feff export <PK> # preview feff.inp from a FeffParameters node
504
+ verdi data feff show <PK> # inspect arrays in an XasData node
505
+ ```
506
+
507
+ ## Architecture overview
508
+
509
+ ### Single-job mode (localhost / small ensembles)
510
+
511
+ ```
512
+ TrajectoryData + FeffParameters
513
+ │
514
+ ▼
515
+ EnsembleExafsWorkChain
516
+ ├─ [optional] FeffCalculation × N_sites ← potentials-only (CONTROL 1 1 1 0 0 0)
517
+ │
518
+ ├─ FeffCalculation × (N_frames × N_sites) ← one Slurm job each
519
+ │ └─ FeffParser → XasData
520
+ │
521
+ ├─ create_serial_shard (calcfunction) → one shard, as the batch driver writes
522
+ │
523
+ └─ merge_exafs_shards (calcfunction) → ExafsArchiveData (archive)
524
+ └─ archive_to_averaged_xas (calcfunction) → averaged XasData per site + grand average
525
+ ```
526
+
527
+ ### Batch mode (HPC, hundreds of calculations)
528
+
529
+ ```
530
+ TrajectoryData + FeffParameters
531
+ │
532
+ ▼
533
+ EnsembleExafsWorkChain (batch_size=64)
534
+ ├─ FeffCalculation × N_sites ← potentials-only, one Slurm job each
535
+ │ └─ RemoteData (pot.pad, phase.pad, …)
536
+ │
537
+ ├─ FeffBatchCalculation ← ONE Slurm job, 64 FEFF instances in parallel
538
+ │ ├─ snap_0000_site_0000/feff.inp
539
+ │ ├─ snap_0001_site_0000/feff.inp
540
+ │ │ … (up to batch_size pairs)
541
+ │ └─ _run_batch.py (driver, concurrent.futures, 1 worker/core)
542
+ │ └─ FeffBatchParser → xas_data.snap_FFFF_site_SSSS (dynamic namespace)
543
+ │
544
+ ├─ FeffBatchCalculation ← next chunk, another Slurm job
545
+ │ └─ …
546
+ │
547
+ └─ merge_exafs_shards (calcfunction) → ExafsArchiveData (archive)
548
+ └─ archive_to_averaged_xas (calcfunction) → averaged XasData per site + grand average
549
+ ```
550
+
551
+ Both routes end at the same merge, so the ensemble average has one
552
+ implementation and `archive` is always produced.
553
+
554
+ The FEFF environment (module loads, etc.) is read from `feff_code.prepend_text`
555
+ at AiiDA submission time and embedded in `batch_config.json`. On the compute
556
+ node, the driver generates a small `_run_feff.sh` bash wrapper per run
557
+ directory, so the environment is correctly applied for every FEFF instance
558
+ without requiring any extra code to be installed on the HPC.
559
+
560
+ ## Development
561
+
562
+ ```bash
563
+ git clone https://github.com/stfc/aiida-feff
564
+ cd aiida-feff
565
+ uv sync --locked --extra testing --extra plots
566
+ uv run pytest tests/
567
+
568
+ # Lint exactly as CI does
569
+ uv run pre-commit run --all-files
570
+ ```
571
+
572
+ ## Relationship to [larch-cli](https://github.com/stfc/alc-dls-exafs/)
573
+
574
+ This plugin is designed to supersede a CLI tool built around larch + FEFF.
575
+ The key differences:
576
+
577
+ | larch-cli | aiida-feff |
578
+ |-----------|------------|
579
+ | Linear script execution | DAG of provenance-tracked nodes |
580
+ | Manual file management | AiiDA handles staging to/from HPC |
581
+ | Results as files on disk | All inputs/outputs stored in the database |
582
+ | Manual batching (N workers per node) | `EnsembleExafsWorkChain(batch_size=N)` |
583
+
584
+
585
+ ## License
586
+
587
+ BSD 3-Clause License. See [LICENSE](LICENSE) for details.