The Theory of Persistence

PT chemistry / quantitative validation

Ionization energies derived by PT

The periodic table gives the skeleton. Ionization energy tests whether that skeleton can produce measurable numbers: how much energy does it take to remove the first electron from an atom?

This page shows two levels: the geometric engine, readable and directly tied to shell polygons, then the full atomic engine adding PT screening and relaxation corrections, including the jj2 spinor correction, superheavy CPR, and the continuous CPR branch projected on the s/p/d/f channels.

PT vs references, Z = 1–118

PT ref. error
0 5 10 15 20 25 H He Ne Ar Kr Xe Rn Og atomic number Z eV residual PT - reference (%) -5 0 +5
Displayed uncertainty
±0.0005 eV

The plotted references are rounded to the millielectronvolt.

Relative scale
≈ 0.002–0.010%

Order of magnitude from rounding alone, from 25 eV to 5 eV.

Residual reading
PT MAE ≈ 2.9 meV

Error bars would be sub-pixel here; superheavy references remain compiled/evaluated values.

Full engine
0.042%

MAE for Z = 1–86, NIST references.

Full engine
0.041%

MAE for Z = 1–103, measured references.

Full engine
0.046%

Global MAE for Z = 1–118 after CPR-continuum.

Superheavy
0.077%

MAE for Z = 104–118, threshold branch jj2 + CPR.

Fitted parameters
0

Calibration comes from the PT chain, not from a chemistry fit.

From shell structure to measured value

Ionization energy is not just nuclear charge. It depends on the outer shell, inner-electron screening, period closures, half-fillings, and fine corrections moving the d and f blocks.

In PT, these contributions assemble into a screening action $S(Z)$. The nucleus attracts as $Z$, but the external electron sees an effective charge $Z_{eff}$ projected by period geometry.

Ry

Scale

The Rydberg is not injected as a free constant: it descends from $m_e$, $\alpha_{EM}$, and the PT cascade.

$Ry = m_e\alpha_{EM}^2/2$

S(Z)

Screening

The bare nuclear charge is reduced by a screening action built from shells, polygons, and PT corrections.

$Z_{eff}=Ze^{-S(Z)}$

IE

Ionization

The removal energy then follows a Rydberg-like law, modulated by the period and by the ejection amplitude of the active channel.

$IE=Ry\,(Z_{eff}/per)^2\,A_{ej}$

CPR-cont

Resonance

The continuous branch adds a $(Z\alpha)^2$ envelope projected by the s/p/d/f geometric vertices; the threshold branch remains active for superheavy elements.

$\Delta S=\Delta S_{jj2}+\Delta S_{CPR}+\Delta S_{cont}$

Checkpoints a chemist recognizes

full engine `IE_eV`
Z Element Block Ref. eV PT eV Error
1 H s 13.598 13.598 -0.003%
2 He s 24.587 24.593 +0.024%
3 Li s 5.392 5.393 +0.024%
7 N p 14.534 14.533 -0.005%
8 O p 13.618 13.619 +0.005%
10 Ne p 21.565 21.564 -0.003%
11 Na s 5.139 5.137 -0.040%
18 Ar p 15.760 15.760 -0.002%
24 Cr d 6.767 6.764 -0.046%
29 Cu d 7.726 7.727 +0.014%
36 Kr p 14.000 14.006 +0.044%
55 Cs s 3.894 3.892 -0.048%
61 Pm f 5.582 5.568 -0.247%
71 Lu d 5.426 5.433 +0.131%
72 Hf d 6.825 6.814 -0.154%
83 Bi p 7.286 7.290 +0.057%
86 Rn p 10.749 10.753 +0.041%
103 Lr d 5.510 5.517 +0.127%
106 Sg d 7.850 7.843 -0.085%
111 Rg d 10.560 10.536 -0.230%
116 Lv p 6.878 6.876 -0.029%
118 Og p 8.888 8.879 -0.106%

What the curve has to get right

An ionization curve is full of discontinuities. Alkali metals drop, noble gases rise, half-fillings create bumps, and d/f transitions do not behave like a simple $Z^2$ law.

That is why this test matters: PT must follow the chemical shape, not merely reproduce an average.

H
hydrogenic Rydberg baseline
13.598 eV
He > H
closure of the first shell
24.593 eV
N > O
half-filled p³ stability
14.533 > 13.619
Ne > F
period closure
21.564 > 17.421
Cr/Cu
informational promotions of the d pentagon
d⁵ / d¹⁰

Attached script: public/scripts-source/ptc/ie_geo.py

94 lines, copied from the geometric PTC engine `ie_geo.py`. Attached proofs: public/scripts-source/ptc/PT_IE_SUPERHEAVY_JJ2_DERIVATION.md; public/scripts-source/ptc/PT_IE_CONTRACTION_PENETRATION_RESONANCE.md; public/scripts-source/ptc/PT_IE_CPR_CONTINUUM.md.

"""ie_geo.py — Ionization Energy from polygon geometry.

Derives IE from the geometric shell operator (ShellPolygon / AtomicShell)
without calling the full atom.py engine.  Uses PT screening_action for
the effective charge, then modulates by the ejection amplitude of the
active polygon.

Zero adjustable parameters.

March 2026 — Persistence Theory
"""
from __future__ import annotations

import math

from ptc.constants import RY
from ptc.periodic import period
from ptc.shell_polygon import build_atomic_shell


def IE_geo_eV(Z: int) -> float:
    """Ionization energy (eV) from polygon geometry.

    IE = Ry × (Z_eff / per)² × ejection

    The ejection_amplitude now contains the unified PT formula
    (insight #30: I_Fisher - I_GFT), absorbing the former 2-loop
    self-energy correction.  No separate 2-loop term needed.

    Parameters
    ----------
    Z:
        Atomic number (1–118).

    Returns
    -------
    float
        Estimated first ionization energy in eV.
    """
    from ptc.atom import screening_action  # lazy import for Python 3.9 compat

    shell = build_atomic_shell(Z)
    per = period(Z)
    S = screening_action(Z)
    Z_eff = Z * math.exp(-S)
    ie_base = RY * (Z_eff / per) ** 2

    # Ejection from active polygon — unified (DC + pairing + I_Fisher - I_GFT).
    ej = shell.active_polygon.ejection_amplitude()

    return ie_base * ej


def benchmark_ie_geo() -> dict:
    """Benchmark IE_geo_eV against NIST first ionization energies.

    Returns
    -------
    dict with keys:
        count       : int — number of elements benchmarked
        mae_percent : float — mean absolute error in percent
        by_block    : dict[str, float] — MAE per block (s, p, d, f)
        rows        : list[dict] — per-element details
    """
    from ptc.data.experimental import IE_NIST
    from ptc.periodic import block_of

    rows = []
    block_errors: dict[str, list[float]] = {}

    for Z, ie_ref in sorted(IE_NIST.items()):
        if ie_ref <= 0:
            continue
        ie_calc = IE_geo_eV(Z)
        err_pct = abs(ie_calc - ie_ref) / ie_ref * 100.0
        blk = block_of(Z)
        block_errors.setdefault(blk, []).append(err_pct)
        rows.append({
            "Z": Z,
            "block": blk,
            "ie_ref": ie_ref,
            "ie_calc": ie_calc,
            "err_pct": err_pct,
        })

    mae = sum(r["err_pct"] for r in rows) / len(rows) if rows else 0.0
    by_block = {blk: sum(errs) / len(errs) for blk, errs in block_errors.items()}

    return {
        "count": len(rows),
        "mae_percent": mae,
        "by_block": by_block,
        "rows": rows,
    }