Chimie PT / validation quantitative
Les énergies d’ionisation dérivées par la PT
Le tableau périodique donne l’ossature. L’énergie d’ionisation teste si cette ossature sait produire des nombres mesurables : combien d’énergie faut-il pour arracher le premier électron d’un atome ?
La page montre deux niveaux : le moteur géométrique, lisible et directement relié aux polygones de couche, puis le moteur atomique complet qui ajoute les corrections d’écrantage et de relaxation du modèle PT, y compris la correction spinorielle jj2, la CPR superlourde et la branche continue CPR-cont projetée sur les canaux s/p/d/f.
PT vs références, Z = 1–118
Les références du graphe sont arrondies au milli-électronvolt.
Ordre de grandeur dû au seul arrondi, de 25 eV à 5 eV.
Les barres d’incertitude seraient sub-pixel ici ; les superlourds restent des références compilées/évaluées.
MAE sur Z = 1–86, références NIST.
MAE sur Z = 1–103, références mesurées.
MAE globale sur Z = 1–118 après CPR-continuum.
MAE sur Z = 104–118, branche seuil jj2 + CPR.
La calibration vient de la chaîne PT, pas d’un fit chimique.
De la couche à la valeur mesurable
Une énergie d’ionisation n’est pas seulement une charge nucléaire. Elle dépend de la couche externe, de l’écrantage des électrons internes, des fermetures de période, des demi-remplissages, et des corrections fines qui déplacent les blocs d et f.
En PT, ces contributions sont assemblées dans une action d’écrantage $S(Z)$. Le noyau attire comme $Z$, mais l’électron externe voit une charge effective $Z_{eff}$ projetée par la géométrie de la période.
Échelle
Le Rydberg n’est pas injecté comme constante libre : il descend de $m_e$, de $\alpha_{EM}$ et de la cascade PT.
$Ry = m_e\alpha_{EM}^2/2$
Écrantage
La charge nucléaire brute est réduite par une action d’écrantage construite sur les couches, les polygones et les corrections PT.
$Z_{eff}=Ze^{-S(Z)}$
Ionisation
L’énergie d’arrachement suit alors une loi de type Rydberg, modulée par la période et par l’amplitude d’éjection du canal actif.
$IE=Ry\,(Z_{eff}/per)^2\,A_{ej}$
Résonance
La branche continue ajoute une enveloppe $(Z\alpha)^2$ projetée par les sommets géométriques s/p/d/f ; la branche seuil reste active pour les superlourds.
$\Delta S=\Delta S_{jj2}+\Delta S_{CPR}+\Delta S_{cont}$
Points de contrôle que reconnaît un chimiste
moteur complet `IE_eV`| Z | Élément | Bloc | Réf. eV | PT eV | Erreur |
|---|---|---|---|---|---|
| 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 % |
Ce que la courbe doit réussir
Une courbe d’ionisation est pleine de ruptures. Les alcalins chutent, les gaz nobles montent, les demi-remplissages créent des bosses, et les transitions d/f ne se comportent pas comme une simple loi en $Z^2$.
C’est précisément ce qui rend ce test intéressant : la PT doit suivre la forme chimique globale, pas seulement retrouver une moyenne.
Script joint : public/scripts-source/ptc/ie_geo.py
94 lignes, copie du moteur géométrique PTC `ie_geo.py`. Preuves jointes : 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,
}