Tables to Point Clouds

How the firmware turns a chosen quantum state into the (x, y, z) points a frame actually draws — every draw, lookup, and coordinate transform that runs per point or per element switch.

Scope

Both pipelines below start from tables that already exist — the flash-resident hydrogen samplers (built host-side in Python; see the companion diagram), the compile-time angular tables, the flash-resident atomSFE radial rows, and the Clementi–Raimondi constants. Dashed boxes mark that given data; solid boxes are operations this pipeline performs. How the hydrogen tables themselves get built is covered in Coefficients to Tables; the atomSFE rows' provenance and the hydrogenic/Z_eff fallback model live in ATOMS.md (§5) and pc/RUN_HFS.md.

Hydrogen orbital — one sampled point

src/physics/pointcloud.h · orbital_presets.cpp — repeated kOrbitalNumPoints times per preset

Given — OrbitalSampler(n, ℓ, m)

ρ(r,θ,φ)  ∝  [r·Rnℓ(r)]² · [Pm(θ)² sinθ] · Φm(φ

Three independent inverse-CDF tables — Fr−1, Fθ−1, Fφ−1 — already built so that sampling each marginal on its own reproduces this joint density exactly.

invRTable / invThetaTable / invPhiTable

1 · Draw three uniforms

ur, uθ, uφ  ~  U(0, 1)

A fixed-order PRNG draw per axis — same seed, same three numbers, in every port (C++ / MicroPython / JS).

XorShift32::uniform01()
u_r, u_θ, u_φ

2 · Invert through the table

r = Fr−1(ur)    θ = Fθ−1(uθ)    φ = Fφ−1(uφ)
F−1(u) = (1−η)·T[i] + η·T[i+1]

T is one of the 1001-entry tables above; i = ⌊u·(N−1)⌋ is the table index just below u, and η = u·(N−1) − i is the fractional remainder used to blend T[i] toward T[i+1]. One interpolated read per axis, O(1) — no rejection loop, no per-point search, so every point costs exactly the same.

getValueFromLookupTable()
r, θ, φ

3 · Spherical → Cartesian

x = r sinθ cosφ
y = r sinθ sinφ
z = r cosθ

The point's position is fixed here — everything after this only decides its color.

sampleOrbitalPoint()
same (r, θ, φ)

4 · Evaluate ψ at that point

Rnℓ(r) = (Σk bk rk) · r · er/n
Pm(θ) = (Σj cj uj) · sin|m|θ,  u=cosθ
Φm(φ) =
cos()   m ≥ 0
sin(|m|φ)   m < 0
ψ = Rnℓ · Pm · Φm

bk are the radial polynomial's coefficients (from the associated Laguerre polynomial, one fixed set per (n,)) and cj are the angular polynomial's coefficients (from the associated Legendre polynomial, one fixed set per (,m)) — the same arrays already used to build the sampler tables above (same b/c naming as Coefficients to Tables), just re-evaluated at this one point instead of at every table entry.

psiReal()
ψ, ψ²

5 · Rank → brightness, sign → phase

q = rank(ψ²) / (M−1)
level = Lmin + qγ·(Lmax−Lmin),  γ = 0.4
color = red if ψ ≥ 0, else blue

Rank, not raw magnitude — ψ² is heavily right-skewed, so ranking spreads brightness evenly instead of crushing most points into the dim end. M is the cloud's point count (kOrbitalNumPoints), not the 1001-entry table size N; Lmin = 80, Lmax = 255.

computeOrbitalLevels() / orbitalLevelToColor565()

6 · Auto-zoom from the whole cloud

rref = P90({ri})  (90th-percentile sampled radius)
scale = ptargetrref

ptarget = 100 px. Every preset — 1s or 6p — fills the same on-screen radius, so switching orbitals never means "just a dot far away".

scaleFromRadii()
Steps 1–5 also re-run continuously, one round-robin point at a time (1% of the cloud every 3 frames — kOrbitalCullFraction / kOrbitalCullRefreshFrames), to keep the cloud slowly turning over. A fresh point's rank is bisected against the frozen sorted-ψ² snapshot and can exceed the initial max, so callers clamp to 255 — resampleOneOrbitalPoint()

Multi-electron atom — one element's cloud

src/physics/atom_cloud.cpp · slater.h · hfs_radial.h — runs once per Z, not per frame

Given — tables for this Z

• OrbitalAngularTables(,m), ≤3 — compile-time, independent of Z
• atomSFE row u(r) for (Z,n,), Z≤92 — read from flash on demand
• Clementi–Raimondi Zeff table, Z≤54, and the size-calibration factor κZ (92-entry generated table)
angular_library.h / hfs_tables.bin / slater_data.h

The atomSFE rows come from the SPARC-atomSFE Kohn–Sham solver (LDA_SVWN) — see ATOMS.md §5 and pc/RUN_HFS.md.

Z

1 · Ground-state configuration

Madelung order (n+, then n), real exceptions substituted
Z electrons  →  {(n, , occ)}

Cr, Cu, Nb, the lanthanide/actinide anomalies, etc. override the naive fill rule where the real atom disagrees with it.

electronConfiguration()
subshells

2 · Expand each subshell into drawing groups

Full — occ = 2(2ℓ+1)

1 isotropic group, weight = occ

Partial — Hund's rule

1 e⁻ per orbital first (m=−ℓ…ℓ), then pair up

Full-shell density is exactly spherical (Unsöld's theorem) — only a partial shell needs per-orbital treatment. Without this split, carbon's 2p² would render spherical, which it isn't.

drawingGroups() / hundFillM()
groups {n,ℓ,m,weight}

3 · Split the point budget

Ng = ⌊N · wg / ΣW⌋, remainder by largest fractional part

N is this element's total point budget (kAtomNumPoints). Largest-remainder method afterward, so the per-group counts sum to exactly N — not "close to it".

splitCounts()
N_g per group

4 · Pick this group's radial source

Table available — Z≤92

R(r) = u(r) / r

Fallback — hydrogenic

Zeff = ZCReff or Z − Σ shield
rescaled ·n/n* for n≥4
Rnℓ(r) = RHnℓ(Zeff·r)

Clementi–Raimondi where the table has it (Z≤54); otherwise Slater's shielding rule (0.35/0.85/1.00) with the n* correction that keeps heavy elements from being systematically too big.

hfsRLookup() / slaterZEff() / zEffRadial()
R(r)

5 · Build this group's radial inverse-CDF

Isotropic group (full subshell)

trapezoid-rule inverse CDF on density ∝ u(r)², over the table's own log-uniform grid

Oriented group (Hund's rule)

re-sample R(r) = u(r)/r at 1001 evenly spaced r ∈ [0, maxR], then the standard evenly-spaced inverse CDF

Both branches invert density ∝ [r·R(r)]² (u = r·R, so u² is the same quantity); they differ in the grid. The isotropic branch integrates the table's own grid directly; the oriented branch re-samples R on an even grid first — an asymmetry deliberately kept from the Python model (hfs_radial.h). The hydrogenic fallback branch always uses the even-grid sweep.

buildHfsRadialSamplerIsotropic() / buildHfsRadialSamplerOriented() / buildRadialSamplerRuntime()
F_r⁻¹

6 · Sample this group's points

Isotropic group

r=Fr−1(ur)   cosθ=2uθ−1   φ=2πuφ
sinθ = 1−cos²θ

Oriented group (m fixed)

r=Fr−1(ur)   θ=Fθ−1(uθ)   φ=Fφ−1(uφ)
x=r sinθ cosφ    y=r sinθ sinφ    z=r cosθ

Which branch a group takes was already decided back in step 2 — isotropic groups draw a uniform direction, oriented groups draw θ and φ from that (ℓ, m)'s own angular tables.

sampleIsotropicPoint() / sampleOrientedPoint()
(x,y,z), all groups

7 · Measure each subshell's reach

rref(n,ℓ) = P90({ri ∈ subshell})
outer = argmax rref  (the valence subshell)
outerSubshellRRef()

8 · Color by shell, emphasize valence

base = shellColor(n)  (K→Q spectroscopic palette)
outer: c′ = c + (255−cfbright
inner: c′ = c·(1−fdim)

Points in every other subshell are dimmed instead — otherwise the valence subshell, often a small minority of the point budget, disappears visually. Points are emitted in contiguous per-subshell runs (AtomSubshellRange), so this is one lerp per subshell, not per point.

colorizeAtomSubshells()

9 · Calibrate and fit to screen

(x,y,z) ×= κZ  (Clementi–Raimondi valence-radius calibration)
scale = ptargetrrefouter  (ptarget = 96 px = 240/2.5)

The κZ multiply happens inside buildAtomPointCloud: it scales the whole cloud so the valence subshell's mode radius lands on the Clementi–Raimondi literature value — it mainly keeps the pm scale bar and dissection depths consistent, since scaleForAtom renormalizes on-screen size anyway. That target-pixel-radius idea (same as the orbital pipeline's step 6) keeps lithium and uranium both legible on a 240px screen, at the cost of the real size trend between elements.

scaleForAtom() / kAtomSizeCalibFactor