# HFS screened-potential atom model — manual run & verify (runbook)

Design/validation narrative: `pc/screened_potential_model.md` (and ATOMS.md §5).
Prerequisites: Python 3.10+, `numpy`, `scipy`. The `micropython/` modules need
the `micropython_shim` (auto-imported by the pc/ scripts).

---

## 1. Solver self-checks (seconds)

```bash
python pc/hfs_solver.py --coulomb-check   # exact hydrogen: E + modes ~1e-5, all OK
python pc/dirac_solver.py                 # exact Dirac hydrogenic: ~1e-9, all OK
```

## 2. Generate the radial tables (~90–150 min serial)

```bash
python pc/hfs_solver.py --zmin 1 --zmax 118 \
    --alpha 0.6666667 --relativistic --out pc/hfs_tables.npz
```

- `--alpha 0.6666667` = Dirac/Kohn–Sham exchange — the value that matches the
  NIST LDA eigenvalues to <0.7 eV (α=1.0/Slater is ~1.2–3 eV too deep).
- `--relativistic` = one-shot radial Dirac pass on the converged potential
  for Z ≥ `--rel-min` (default 55) — the s/p contraction that fixes U
  (7s: 242 pm NR → 174 pm vs literature 175).
- Progress: one flushed line per element (Z, time, iterations, valence
  mode) + **incremental npz save after every element** — interruption loses
  at most the element in flight. Resume a broken run with `--resume`
  (skips the Z already present in `--out`).
- Optional 4× compaction (513 pts/shell, <0.25% max mode deviation; the
  samplers and harness read both formats):

```bash
python pc/hfs_tables.py --compact pc/hfs_tables.npz pc/hfs_tables.npz 513
```

## 3. Validation against literature (minutes)

```bash
# 3a. Clementi-Raimondi radii + physics checks + Koopmans IP check.
#     Exit 0 = STRICT PASS + every physics check PASS.
python pc/validate_atoms.py --model hfs --strict --all --tables pc/hfs_tables.npz

# 3b. NIST dftdata cross-check (archive under examples/nis data/dftdata,
#     from math.nist.gov/DFTdata/atomdata/):
#       - configurations: 92/92 match slater.electron_configuration()
#       - LDA valence eigenvalues: within <~1 eV at alpha=2/3
#       - RLDA spin-orbit splits: ours ~1.1–1.3× NIST (central-field OK)
#       - ScRLDA (scalar-relativistic) vs our j-averaged Dirac: same
#         construction (NIST ScRLDA == their own RLDA j-average to 0.3%);
#         absolute eigenvalues ~2–10% deeper (exchange-only vs LDA+corr.)
python pc/nist_compare.py "examples/nis data/dftdata" --alpha 0.6666667 \
    --elements 18,36,54,79,92

# 3c. Hydrogenic-model regression (must stay unchanged)
python pc/validate_atoms.py --strict
```

## 4. Interactive PC viewer (Tk)

```bash
python pc/atom_main.py 26 --model hfs     # iron, new radial model
python pc/atom_main.py 92 --model hfs     # uranium (relativistic 7s)
python pc/atom_main.py                    # default: carbon
```

## Expected numbers (representative set, α=2/3, mix=0.35)

| Z  | el | valence | model/lit radius | Z  | el | valence | model/lit |
|----|----|---------|-----------------:|----|----|---------|----------:|
| 1  | H  | 1s      | 0.99             | 36 | Kr | 4p      | 0.91      |
| 3  | Li | 2s      | 0.95             | 54 | Xe | 5p      | 0.93      |
| 6  | C  | 2p      | 0.91             | 55 | Cs | 6s      | 0.93      |
| 11 | Na | 3s      | 0.94             | 79 | Au | 6s      | ~0.95     |
| 24 | Cr | 4s      | 0.95             | 79 | Au | 6s (Dirac) | ~0.70* |
| 26 | Fe | 4s      | 0.90             | 92 | U  | 7s (Dirac) | 0.99   |
| 29 | Cu | 4s      | 0.96             |    |    |            |        |

\* Z\u226555 tables are RELATIVISTIC (Dirac); Clementi-Raimondi is
nonrelativistic, so the s/p ratios carry a systematic relativistic-vs-NR
offset (~0.7 for the 5d/6s block) -- the contraction itself is validated
against the NIST RLDA eigenvalues. Pd (Z=46, 0.33) is a documented
X\u03b1/LDA d-shell limitation (4d eigenvalue matches NIST LDA).

(Old hydrogenic model: Li 1.30, Na 1.45, Fe 1.54, Kr 1.52, Cs 3.34,
Au 3.40, U 4.96.)

---

## 5. SPARC-atomSFE tables — current default (2026-08)

`pc/hfs_tables.npz` is now generated by **SPARC-atomSFE** (all-electron
LDA_SVWN, spectral finite elements) instead of the hand-rolled HFS/Dirac
solver above — see `pc/hfs_atomsfe.py` (generator), `pc/nist_compare_atomsfe.py`
(validation). The old solver remains for comparison/regeneration.

### Why and what changed

- **Eigenvalues**: atomSFE LDA_SVWN reproduces the NIST dftdata LDA
  eigenvalues to ≤7e-6 Ha across all 915 subshells of Z=1..92 (the old
  model tracked NIST to <0.7 eV). `pc/hfs_tables.npz` also fixes two data
  problems of the on-disk old tables: the old default only covered Z=1..26,
  and the old full copy (`hfs_tables - Copia.npz`) carries the pre-fix
  SCF-collapse artifacts (Cu 4s = −4.36 Ha, Cr 4s = −0.74 Ha — the
  ns-into-d metastable the sigma fix addressed). **Do not use
  `hfs_tables - Copia.npz` as a data source.**
- **Radii**: LDA valence orbitals are more diffuse than the HF-based
  Clementi-Raimondi reference (self-interaction error; H ~2.2×, period 2
  ~1.7×, Fe ~1.1×, U ~1.5×). The old Xα+Latter model matched CR (0.90–0.99)
  via its Latter cutoff, which the library lacks (its HF/EXX paths are
  spin-restricted and under-cancel open-shell self-interaction — H comes
  out at −0.358 Ha instead of −0.5; the spin-polarized UHF path raises
  NotImplementedError). The display therefore applies a **per-element
  Clementi-Raimondi size calibration** (`atom_view_pc.clementi_size_factor`):
  the rendered cloud is rescaled so the valence mode lands on the CR
  literature radius, while internal structure stays NIST-exact LDA.
- **Relativistic**: the library is nonrelativistic; the old one-shot Dirac
  pass (Z≥55 s/p contraction, U 7s 242→174 pm) is NOT reproduced. Z≥55
  tables are plain LDA. The CR size calibration hides most of the size
  impact; the internal NR shell structure remains.

### Regenerate

The generator imports the `atom` package from a workspace-local install
(`pc/_atomsfe_vendor`, gitignored). The upstream repo is likewise gitignored
(`examples/SPARC-atomSFE`, clone it yourself — it is not committed):

```bash
git clone https://github.com/SPARC-X/SPARC-atomSFE examples/SPARC-atomSFE
python -m pip install --no-deps --no-build-isolation \
    --target pc\_atomsfe_vendor examples\SPARC-atomSFE

python pc/hfs_atomsfe.py --zmin 1 --zmax 92 --out pc/hfs_tables_atomsfe.npz
copy pc\hfs_tables_atomsfe.npz pc\hfs_tables.npz   # make it the default
python pc/nist_compare_atomsfe.py "examples/nis data/dftdata"   # ≤7e-6 Ha
python pc/validate_atoms.py --model hfs --tables pc/hfs_tables.npz --all
```

~2–3 min for the batch (the sandbox blocks multiprocessing: named pipes).
**Scope is deliberately Z=1..92** (the library's hard cap) and it is applied
to every port: the device (`src/views/atom_view.cpp` snake walk + idle jump, capped
via `periodic_grid.h`'s `kMaxDisplayZ`), the micropython viewer and chooser,
the PC viewer, and the web viewer all stop their navigation at 92
(`slater.MAX_DISPLAY_Z`); `validate_atoms --model hfs` caps at the tables'
own coverage. The Z=93..118 data (configs/symbols/names) stays available to
physics/validation code.

### Size calibration across ports

Every display port renders CR-correct atom sizes (valence mode radius =
Clementi-Raimondi literature) on top of its own radial model:

- PC viewer with tables: `atom_view_pc.clementi_size_factor()` =
  CR / LDA-table valence mode (the LDA SIE correction).
- PC viewer hydrogenic path, micropython viewer, web viewer, and the
  device (`src/physics/atom_cloud.cpp`): the generated hydrogenic factor table
  `src/physics/atom_size_calib.h` / `micropython/atom_size_calib.py`
  (f = CR / hydrogenic valence mode; the inverse of the old hydrogenic
  model's CR ratios, ~1.0 for H/He/B..Ne, ~0.65 for alkali/transition
  metals, ~0.2-0.45 for the Z>54 Slater fallback). Regenerate with
  `tools/atom_size_calib_gen.py`. The device zoom-to-fits every atom
  (`kAtomTargetPx`/rRef), so there the calibration mainly makes the pm
  scale bar and dissection depths CR-consistent.

### Reduced reference table (committed, for embedding in the ports)

`pc/hfs_tables_reduced.npz` (COMMITTED, unlike the full tables) is the
reduced orbital-data reference the ports embed (device PROGMEM /
micropython / web codegen): all 92 elements, every subshell's u(r) on a
**128-point log grid** (r = 1e-6..100 Bohr) plus eigenvalue and occupancy --
same npz schema as the full tables (`pc/hfs_tables.py` loader reads both).
Accuracy vs the full 2001-pt tables: valence-subshell modes within **1.5%**
(worst: Pd 4d 1.4%; the coarser core-orbital deviations -- up to ~5% on the
1s of heavy elements -- are sub-pixel at 240x240 and invisible). Regenerate:

```bash
python pc/hfs_tables.py --compact pc/hfs_tables.npz pc/hfs_tables_reduced.npz 128
```

The documented STO-fit / 64-pt-Hermite device form and the per-port codegen
are still the standing follow-ups (pc/screened_potential_model.md §7); this
file is the committed source of truth for that work.

### Device note

Done (2026-08), both C++ and MicroPython, both reading the tables ON DEMAND
from flash/filesystem rather than compiling them into either firmware image.
`tools/hfs_table_gen.py` packs `hfs_tables_reduced.npz` into ONE binary blob
(little-endian `struct`, same flat layout on both ports: shared `r` grid +
per-Z element index + per-subshell `(n, ell)` index + `u(r)` rows), written
to two identical copies:

- `data/hfs_tables.bin` -- staged into the PlatformIO filesystem image and
  flashed to the `storage` SPIFFS partition (`partitions_16M.csv`) via
  `pio run -t uploadfs` (a separate step from the normal firmware flash --
  see this section's build note below). `src/physics/hfs_radial.cpp` (hand-written)
  mounts `/storage` (idempotent, shared with `screenshot.cpp`'s existing use
  of that same partition), reads the small header/index into RAM once, and
  reads each subshell's own `u(r)` row from the file on demand (once per
  element switch, not once per frame). `src/physics/hfs_tables.h` (generated) is now
  just three size constants, not the ~470 KB of `.rodata` array data an
  earlier version of this work compiled in directly -- moving to on-demand
  reads was a deliberate follow-up decision to keep that data out of the
  firmware image/OTA payload.
- `micropython/hfs_tables.bin` -- deployed to the device root the same way
  every other `micropython/` file is (`mpremote ... fs cp -r micropython/. :`).
  `micropython/hfs_radial_tables.py` (hand-written) reads it the same way:
  small header/index resident, `u(r)` rows read on demand via `open()`/
  `seek()`/`read()`. `micropython/atom_view.py` now loads this once at
  import time and passes it as `atom_cloud.build_atom_point_cloud()`'s
  `radial_tables` argument, so the MicroPython device path renders through
  the tables too, not just C++.

Both ports build the same two sampler paths `micropython/atom_cloud.py`'s
`radial_tables` branch already defined -- the direct log-grid inverse-CDF for
full (isotropic) subshells via `src/physics/pointcloud.h`'s `buildInverseCdfFromGrid()`
(C++) / `micropython/pointcloud.py`'s existing `_build_inverse_cdf_from_grid()`
(MicroPython, unchanged), and the evenly-sampled-via-`R_lookup` path for
Hund's-rule (anisotropic) groups. The hydrogenic model
(`zEffRadial`/`buildRadialSamplerRuntime` in C++; the `radial_tables is None`
branch in MicroPython) stays as the fallback for a (z, n, ell) the tables
don't cover (shouldn't happen for z<=92) AND, on the C++ side, for a board
that hasn't run `pio run -t uploadfs` yet -- `hfsFindU()` returns `nullptr`
in both cases and `atom_cloud.cpp` degrades to the old radii rather than
crashing.

Size calibration: `tools/atom_size_calib_gen.py` now emits THREE files, not
two -- `src/physics/atom_size_calib.h` (C++, table-based) and
`micropython/hfs_atom_size_calib.py` (MicroPython, table-based, used only by
`atom_view.py`) both carry CR / HFS-table valence-mode factors, computed
identically. `micropython/atom_size_calib.py` is left HYDROGENIC and
otherwise untouched: it is a genuinely shared module also consumed by
`pc/atom_view_pc.py`'s default (`--model hydrogenic`) path and
`pc/atom_dissection_common.py`'s fallback (itself shared with the web
viewer) -- repurposing it to table-based factors would have silently
miscalibrated all of those.

The STO-fit / 64-point-Hermite compaction `screened_potential_model.md`
section 7 once planned turned out unnecessary: the board's flash/PSRAM
headroom made embedding the already-committed 128-point reduced table
directly (rather than compressing it further) the simpler choice, and
reading it on demand from flash rather than compiling it in keeps the
firmware image itself small regardless.

**Build note**: deploying/updating the C++ table now requires
`pio run -t uploadfs` (PlatformIO's filesystem-image upload target) IN
ADDITION TO the normal firmware flash -- `pio run -t upload` /
`pio run` alone does not touch the `storage` partition's contents. This is a
new step; existing build instructions that only ran the normal
upload/monitor cycle need it added once (or again whenever
`data/hfs_tables.bin` changes).
