Public repo: https://github.com/follmerlab/multitorch Version: 0.1.0 (MIT license) Tests: 477/477 passing Audits: Scientific, code review, and test builder audits all passed — deployment gate PASSED
- SCA-001: Change default
broaden_modefrom"legacy"to"correct"inpseudo_voigt— breaking API change across 6 function signatures, needs a deprecation cycle - GPU acceleration: Plan at
docs/GPU_ACCELERATION_PLAN.md - Minor: document Eg-only autograd flow in
calcXASdocstring - Minor: add assertion that shell pair is 3D for current scope
PyTorch port of the Fortran ttmult/Cowan multiplet X-ray spectroscopy suite for L-edge XAS of 3d transition metals. Must match Fortran numerically.
- Conda env:
multi(Python 3.11, PyTorch 2.5) - Python:
/opt/anaconda3/envs/multi/bin/python - Pytest:
/opt/anaconda3/envs/multi/bin/pytest tests/ -q - Working dir:
/Users/afollmer/Follmer_UCD/Follmer_Lab/Code/multiplets/multitorch/
multitorch/
multitorch/ # package source
angular/ # Wigner 3j/6j/9j, CFP, RME builder, symmetry
atomic/ # HFS SCF, Slater integrals, radial mesh
hamiltonian/ # assemble.py (main CT pipeline), diagonalize, transitions
spectrum/ # sticks, broaden (Voigt/pseudo-Voigt), rixs, background
io/ # parsers: read_rme.py, read_ban.py, read_rcf.py, read_oba.py
api/ # calc.py (calcXAS entry point), plot.py
data/
cfp/ # bundled CFP tables (rcg_cfp72, rcg_cfp73)
fixtures/ # bundled Fortran reference outputs (9 Ti-Ni fixtures)
tests/
reference_data/ # symlink/copy of fixtures for test compatibility
test_angular/ # Wigner, CFP, RME tests
test_atomic/ # HFS, Slater, mesh tests
test_hamiltonian/ # assemble, diagonalize, BAN parser tests
test_integration/ # end-to-end nid8ct XAS tests
test_spectrum/ # broadening, sticks tests
notebooks/ # 4 tutorial notebooks
docs/ # GPU_ACCELERATION_PLAN.md
Fortran source lives at ../ttmult/src/ (rcn31.f, rcn2.f, ttrcg.f, ttrac.c, ttban_exact.f).
Fortran binaries at ../ttmult/bin/.
Python wrapper at ../pyttmult/.
Legacy Python API at ../pyctm/.
All implementation progress, bugs, investigation logs, and task tracking are in .claude/orchestration/. See .claude/orchestration/INDEX.md for the full index.
Before starting any work:
- Read
.claude/orchestration/INDEX.mdto understand current state - Check if your task has an existing investigation log
- Update the relevant files when you make progress or find something new
- All tensors use
float64(DTYPEfrom_constants.py) - COWAN store sections are 0-indexed in Python; ADD entry
matrix_idxis 1-based (subtract 1) - Energy offsets (EG/EF) are NOT scaled by 1/sqrt(IDIM); Hamiltonian matrix elements ARE
.rme_rcgsections delimited by FINISHED markers map 1:1 to Fortran PAIRIN calls.rme_racoperator blocks: "GROUND" = config 1, "EXCITE" = config 2 (even in ground state manifold)- Hybridization blocks in
.rme_racare labeled TRANSI with geometry containing "HYBR"
- The Fortran PAIRIN subroutine reads .rme_rcg/.rme_rac SEQUENTIALLY. Each call consumes one COWAN section. The Python code indexes by section number instead.
- PAIRIN overwrites mateg(1)/mateg(2) on each call. The FINAL call (ground mixing, section 2) is the one that matters for make_hamiltonian.
- Empty .rme_rac blocks (no ADD entries) must be skipped — they exist as placeholders from earlier PAIRIN calls.
- The Fortran
gausslegendrewith W=0, N=1 gives point=0, weight=1 (special case, line 375-378 of ttban_exact.f). SUBANAxparsesDEF EG2 = 4.000 UNITYby finding the keyword, scanning backwards for the numeric coefficient, and multiplying by the keyword's value (UNITY=1.0, DEL=delta, UCV=Ucv, UVV=Uvv).
Where we are: C3-pre, C3a, C3b, C3c, C3d landed. Next up is C3e
(build_cowan_store_in_memory).
The decision point at C3d: build_rac_in_memory is implemented as a
loader that parses .rme_rac + .rme_rcg fixture paths and derives
the SectionPlan from the parsed COWAN store. It is not a
from-scratch generator. The reason this is the right scoping:
- The ADD entry coefficients in
.rme_racare pure CFP/Wigner-3j/6j angular products generated by Fortranttrac.c(~7000 lines). Reproducing them in PyTorch is a massive port and is orthogonal to the autograd story. - The Track C autograd path routes through the atomic-parameter
scalars (
slater_scale,soc_scale,cf,delta,lmct,mlct) that multiply into the COWAN store tensors C3e will build — not through the ADD coefficients themselves. ADD coefficients are constants regardless of physics inputs. build_rac_in_memoryexposessource_rac_path/source_rcg_pathkwargs as the explicit extension point for a future ttrac port. TheSectionPlancontract C3e consumes is identical either way, so a from-scratch generator can be slotted in later without changing C3e, C3f, C4, or any consumer.
If something goes sideways from here, come back to this point and re-read:
multitorch/hamiltonian/build_rac.py(the loader-based implementation- module docstring explaining the scoping)
tests/test_hamiltonian/test_build_rac.py::test_in_memory_builder_round_trips_through_assembler(the contract test that locks in the C3d → assembler boundary).claude/orchestration/TRACK-C-PHASE5.mdrow C3d (status table)- This section of CLAUDE.md (the "why we punted on the ttrac port" rationale)
The C3e step that follows must produce a COWAN store whose section
counts and per-slot shapes match the SectionPlan, with each tensor
built from the existing torch wrappers in
multitorch/angular/torch_blocks.py (C3a) multiplied by the scaled
atomic parameters from multitorch/atomic/scaled_params.py (C3c).
Element-wise parity against the parsed COWAN store at 1e-6 is the
success criterion. C3e is the actual angular reconstruction work; C3d
is just the contract.
- Do not modify reference data in
tests/reference_data/ - Do not add features beyond what's needed to match Fortran output
- Do not refactor code that already works and passes tests
- Do not commit to git without explicit user request