Tables to BIN

How the PC-side toolchain turns validated orbital math and atomSFE solutions into the two binary blobs the firmware reads from flash — data/orbital_samplers.bin and data/hfs_tables.bin — and how those blobs get from the repo onto the device's SPIFFS "storage" partition.

Build side

This is the build side of Tables to Point Clouds: the two blobs produced here are exactly the flash-resident "given" tables that page's pipelines consume (the hydrogen samplers and the atomSFE radial rows). The math behind the hydrogen samplers is the subject of Coefficients to Tables; the atomSFE model's design and the full regenerate/flash runbook live in ATOMS.md (§5) and pc/RUN_HFS.md.

Hydrogen sampler blob — data/orbital_samplers.bin

tools/orbital_table_gen.py · micropython/pointcloud.py — one run per library entry (36 hydrogen presets), host-side in Python (double precision)

Given — the preset list and the math

• kOrbitalLibrary — 36 descriptors {n, ℓ, m, label, phase colors}, compiled into the firmware (src/physics/orbital_library.h): the preset order IS the record order
• cloud_common.ORBITAL_PRESETS — the same 36 (n, ℓ, m, label) tuples in the same order, index-matched to kOrbitalLibrary (keep both lists appended identically)
• micropython/orbitals.py + pointcloud.py — the validated math, imported unmodified under CPython via pc/micropython_shim.py

Same code path already cross-validated bit-identical against the C++/JS ports by tools/orbitals_host/run_crosscheck.sh — this generator is not a third reimplementation, it reuses the MicroPython port itself.

kOrbitalLibrary / ORBITAL_PRESETS / micropython_shim
36 × (n, ℓ, m)

1 · Sample each preset's inverse-CDF tables

for each (n, ℓ, m):   pointcloud.init_orbital_sampler(n, ℓ, m)
→ OrbitalSampler { n, ℓ, m, maxR, invRTable[1001], invThetaTable[1001], invPhiTable[1001] }

Exactly the three quantile tables Coefficients to Tables describes buildOrbitalSamplerConstexpr() produces in C++ — same 1001-point resolution, same math, just executed on the PC instead of inside the compiler.

init_orbital_sampler()
36 OrbitalSamplers

2 · Flatten to little-endian records

header <HH>  count = 36,  tableSize = 1001
per preset:  <iii> n, ℓ, m  +  <f> maxR  +  3 × <1001f> invR / invTheta / invPhi

Tables are fixed at float32 on disk regardless of the generator's own precision — the C++ reader static_asserts sizeof(orb_real_t) == sizeof(float) so the fread() sizes can never silently drift. A missing or stale blob is caught by the 2-value header check and degrades to a single point at the origin, never a crash (see orbital_library.cpp).

emit_binary()
flat little-endian records

Deliverable — data/orbital_samplers.bin

433,012 bytes  (~423 KB)

Regenerate with python3 tools/orbital_table_gen.py whenever kOrbitalLibrary / ORBITAL_PRESETS changes. No offset table: a preset's record index IS its index in kOrbitalLibrary, which stays compiled into the firmware.

tools/orbital_table_gen.py

AtomSFE radial rows — data/hfs_tables.bin

pc/hfs_atomsfe.py → pc/hfs_tables.py --compact → tools/hfs_table_gen.py

Given — the solved wavefunctions

• SPARC-atomSFE — all-electron Kohn–Sham solver (LDA_SVWN, spectral finite elements), vendored in pc/_atomsfe_vendor; solves every occupied (n, ℓ) subshell of Z = 1..92 (the library's hard cap)
• pc/hfs_tables_atomsfe.npz — the solver's output on a 2001-point log-uniform grid (r = 1e-6..100 Bohr), committed as pc/hfs_tables.npz (8.1 MB)

Eigenvalues reproduce the NIST dftdata LDA reference to ≤7×10⁻⁶ Ha across all 915 subshells, ground-state configurations 92/92 (pc/nist_compare_atomsfe.py, tolerance 2e-5 Ha/subshell). Non-relativistic — no Dirac pass for Z ≥ 55 (see ATOMS.md §5.2).

pc/hfs_atomsfe.py
npz — 2001 pts/subshell

1 · Reduce to device resolution

python pc/hfs_tables.py --compact hfs_tables.npz hfs_tables_reduced.npz 128
→ 128 points per subshell on the log-uniform grid (r = 1e-6..100 Bohr) — the committed pc/hfs_tables_reduced.npz (1.2 MB)

The radial density is smooth in log-r, so downsampling keeps the shape: hfs_tables.compact()'s docstring measures the mode radius accurate to ~0.1% at its 513-point setting; 128 is the device reference. E/occ values ride along in the npz but are dropped in the blob.

hfs_tables.compact()
reduced npz — 128 pts/subshell

2 · Pack into one flat blob

header <HHH>  gridSize = 128,  elementCount = 92,  subshellCount = 915
r grid — 128 × f32  (shared log-uniform Bohr grid)
element index — 92 × <HB (offset, count)>  per Z = 1..92
subshell index — 915 × <BB (n, ℓ)>
u(r) rows — 915 × 128 f32, back to back

u = r·R, the same quantity the atom pipeline consumes as R(r) = u(r)/r. E/occ are dropped — occupancy already comes from each port's own configuration source (slater.h / slater.py), which the NIST cross-check matches 92/92 anyway.

build_flat() / emit_binary()
471,104-byte blob

Deliverable — data/hfs_tables.bin

471,104 bytes  (~460 KB)
+ byte-identical micropython/hfs_tables.bin
+ generated src/physics/hfs_tables.h (the three size constants only)

Readers (src/physics/hfs_radial.cpp, micropython/hfs_radial_tables.py) sanity-check the file's own header against those constants at load. hfs_radial.cpp keeps its FILE* open across lookups — re-opening SPIFFS once per subshell measured ~800–980 ms for Fe's 7 subshells vs ~30–60 ms hydrogenic, so the open cost is paid once, not per row.

tools/hfs_table_gen.py
How the blobs reach the device

ESP32 C++ (S3 / CYD)

data/ is staged into the PlatformIO SPIFFS image; `pio run -t uploadfs` writes the "storage" partition

partitions_16M.csv: spiffs, 7M at 0x810000. Chained onto every `pio run -t upload` by tools/extra_script_uploadfs.py (the CYD env uses tools/extra_script_uploadfs_cyd.py's pure-Python spiffsgen target instead). Both blobs are read on demand — once per preset/element switch, never per frame. Note: uploadfs reformats the whole partition, so it also wipes on-device screenshots.

MicroPython

only the HFS blob travels: micropython/hfs_tables.bin, deployed with `mpremote ... fs cp -r micropython/. :`

Read by micropython/hfs_radial_tables.py (header/index resident in RAM, rows via open/seek). The hydrogen samplers are NOT a blob here — cloud_common.build_point_cloud() builds them at runtime with init_orbital_sampler(), memoized in a per-(n,ℓ,m) cache.

Missing or stale blob never crashes: the C++ readers return nullptr and the pipelines fall back (hydrogenic radii / single point at the origin) — a board that never ran `uploadfs` still boots and renders with the old math.

extra_script_uploadfs.py / mpremote fs cp
Sibling artifacts (not blobs)

tools/atom_size_calib_gen.py — the κ_Z size-calibration tables

• src/physics/atom_size_calib.h — C++, table-based (CR literature valence radius ÷ HFS-table valence mode, the same tables this page packs), 92 entries — consumed by buildAtomPointCloud()'s per-element multiply (Tables to Point Clouds, atom step 9)
• micropython/hfs_atom_size_calib.py — table-based, same factors, used only by micropython/atom_view.py
• micropython/atom_size_calib.py — hydrogenic variant, kept for the PC/web hydrogenic paths (pc/atom_view_pc.py --model hydrogenic)

Elements without a CR literature value (Fr, Ra) get 1.0. Unlike the two blobs, these are generated source (headers/modules) compiled or copied into each port, not read from flash.

tools/atom_size_calib_gen.py