molbuilder 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- molbuilder/__init__.py +8 -0
- molbuilder/__main__.py +6 -0
- molbuilder/atomic/__init__.py +4 -0
- molbuilder/atomic/bohr.py +235 -0
- molbuilder/atomic/quantum_atom.py +334 -0
- molbuilder/atomic/quantum_numbers.py +196 -0
- molbuilder/atomic/wavefunctions.py +297 -0
- molbuilder/bonding/__init__.py +4 -0
- molbuilder/bonding/covalent.py +442 -0
- molbuilder/bonding/lewis.py +347 -0
- molbuilder/bonding/vsepr.py +433 -0
- molbuilder/cli/__init__.py +1 -0
- molbuilder/cli/demos.py +516 -0
- molbuilder/cli/menu.py +127 -0
- molbuilder/cli/wizard.py +831 -0
- molbuilder/core/__init__.py +6 -0
- molbuilder/core/bond_data.py +170 -0
- molbuilder/core/constants.py +51 -0
- molbuilder/core/element_properties.py +183 -0
- molbuilder/core/elements.py +181 -0
- molbuilder/core/geometry.py +232 -0
- molbuilder/gui/__init__.py +2 -0
- molbuilder/gui/app.py +286 -0
- molbuilder/gui/canvas3d.py +115 -0
- molbuilder/gui/dialogs.py +117 -0
- molbuilder/gui/event_handler.py +118 -0
- molbuilder/gui/sidebar.py +105 -0
- molbuilder/gui/toolbar.py +71 -0
- molbuilder/io/__init__.py +1 -0
- molbuilder/io/json_io.py +146 -0
- molbuilder/io/mol_sdf.py +169 -0
- molbuilder/io/pdb.py +184 -0
- molbuilder/io/smiles_io.py +47 -0
- molbuilder/io/xyz.py +103 -0
- molbuilder/molecule/__init__.py +2 -0
- molbuilder/molecule/amino_acids.py +919 -0
- molbuilder/molecule/builders.py +257 -0
- molbuilder/molecule/conformations.py +70 -0
- molbuilder/molecule/functional_groups.py +484 -0
- molbuilder/molecule/graph.py +712 -0
- molbuilder/molecule/peptides.py +13 -0
- molbuilder/molecule/stereochemistry.py +6 -0
- molbuilder/process/__init__.py +3 -0
- molbuilder/process/conditions.py +260 -0
- molbuilder/process/costing.py +316 -0
- molbuilder/process/purification.py +285 -0
- molbuilder/process/reactor.py +297 -0
- molbuilder/process/safety.py +476 -0
- molbuilder/process/scale_up.py +427 -0
- molbuilder/process/solvent_systems.py +204 -0
- molbuilder/reactions/__init__.py +3 -0
- molbuilder/reactions/functional_group_detect.py +728 -0
- molbuilder/reactions/knowledge_base.py +1716 -0
- molbuilder/reactions/reaction_types.py +102 -0
- molbuilder/reactions/reagent_data.py +1248 -0
- molbuilder/reactions/retrosynthesis.py +1430 -0
- molbuilder/reactions/synthesis_route.py +377 -0
- molbuilder/reports/__init__.py +158 -0
- molbuilder/reports/cost_report.py +206 -0
- molbuilder/reports/molecule_report.py +279 -0
- molbuilder/reports/safety_report.py +296 -0
- molbuilder/reports/synthesis_report.py +283 -0
- molbuilder/reports/text_formatter.py +170 -0
- molbuilder/smiles/__init__.py +4 -0
- molbuilder/smiles/parser.py +487 -0
- molbuilder/smiles/tokenizer.py +291 -0
- molbuilder/smiles/writer.py +375 -0
- molbuilder/visualization/__init__.py +1 -0
- molbuilder/visualization/bohr_viz.py +166 -0
- molbuilder/visualization/molecule_viz.py +368 -0
- molbuilder/visualization/quantum_viz.py +434 -0
- molbuilder/visualization/theme.py +12 -0
- molbuilder-1.0.0.dist-info/METADATA +360 -0
- molbuilder-1.0.0.dist-info/RECORD +78 -0
- molbuilder-1.0.0.dist-info/WHEEL +5 -0
- molbuilder-1.0.0.dist-info/entry_points.txt +2 -0
- molbuilder-1.0.0.dist-info/licenses/LICENSE +21 -0
- molbuilder-1.0.0.dist-info/top_level.txt +1 -0
molbuilder/__init__.py
ADDED
molbuilder/__main__.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Bohr Model of the Atom
|
|
3
|
+
|
|
4
|
+
Represents atoms using the Bohr model with quantized energy levels,
|
|
5
|
+
electron shell configurations, and orbital mechanics. Provides both
|
|
6
|
+
computational physics and animated visualization.
|
|
7
|
+
|
|
8
|
+
Physics reference:
|
|
9
|
+
- Orbital radius: r_n = n^2 * a_0 / Z
|
|
10
|
+
- Energy level: E_n = -13.6 eV * Z^2 / n^2
|
|
11
|
+
- Photon energy: delta_E = E_final - E_initial
|
|
12
|
+
- Wavelength: lambda = h*c / |delta_E|
|
|
13
|
+
- Orbital velocity: v_n = (Z * e^2) / (n * hbar)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
import warnings
|
|
18
|
+
from molbuilder.core.constants import (
|
|
19
|
+
BOHR_RADIUS_M,
|
|
20
|
+
BOHR_RADIUS_PM,
|
|
21
|
+
PLANCK_CONSTANT,
|
|
22
|
+
HBAR,
|
|
23
|
+
SPEED_OF_LIGHT,
|
|
24
|
+
ELECTRON_CHARGE,
|
|
25
|
+
ELECTRON_MASS,
|
|
26
|
+
COULOMB_CONSTANT,
|
|
27
|
+
EV_TO_JOULES,
|
|
28
|
+
RYDBERG_ENERGY_EV,
|
|
29
|
+
MAX_ELECTRONS_PER_SHELL,
|
|
30
|
+
VACUUM_PERMITTIVITY,
|
|
31
|
+
)
|
|
32
|
+
from molbuilder.core.elements import ELEMENTS, SYMBOL_TO_Z
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Core Bohr model class
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
class BohrAtom:
|
|
39
|
+
"""Represents an atom using the Bohr model.
|
|
40
|
+
|
|
41
|
+
The Bohr model is only physically valid for hydrogen-like (one-electron)
|
|
42
|
+
atoms and ions (H, He+, Li2+, etc.). For multi-electron atoms, results
|
|
43
|
+
are qualitative approximations. Use QuantumAtom from
|
|
44
|
+
molbuilder.atomic.quantum_atom for multi-electron systems.
|
|
45
|
+
|
|
46
|
+
Parameters
|
|
47
|
+
----------
|
|
48
|
+
atomic_number : int
|
|
49
|
+
Number of protons (Z). Determines the element.
|
|
50
|
+
mass_number : int or None
|
|
51
|
+
Protons + neutrons. Defaults to rounded standard atomic weight.
|
|
52
|
+
charge : int
|
|
53
|
+
Net ionic charge (0 = neutral, +1 = lost one electron, etc.).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
def __init__(self, atomic_number: int, mass_number: int = None, charge: int = 0):
|
|
57
|
+
if atomic_number < 1 or atomic_number > 118:
|
|
58
|
+
raise ValueError(f"Atomic number must be 1-118, got {atomic_number}")
|
|
59
|
+
|
|
60
|
+
self.atomic_number = atomic_number # Z
|
|
61
|
+
self.protons = atomic_number
|
|
62
|
+
symbol, name, weight = ELEMENTS[atomic_number]
|
|
63
|
+
self.symbol = symbol
|
|
64
|
+
self.name = name
|
|
65
|
+
self.standard_atomic_weight = weight
|
|
66
|
+
|
|
67
|
+
if mass_number is None:
|
|
68
|
+
self.mass_number = round(weight)
|
|
69
|
+
else:
|
|
70
|
+
self.mass_number = mass_number
|
|
71
|
+
|
|
72
|
+
self.neutrons = self.mass_number - self.protons
|
|
73
|
+
self.charge = charge
|
|
74
|
+
self.num_electrons = self.protons - charge
|
|
75
|
+
|
|
76
|
+
if self.num_electrons < 0:
|
|
77
|
+
raise ValueError("Charge exceeds number of protons (no electrons left)")
|
|
78
|
+
|
|
79
|
+
if self.num_electrons > 1:
|
|
80
|
+
warnings.warn(
|
|
81
|
+
f"BohrAtom(Z={atomic_number}): Bohr model is only exact for hydrogen-like "
|
|
82
|
+
f"(one-electron) systems. Results for Z={atomic_number} are qualitative. "
|
|
83
|
+
f"Consider using QuantumAtom for multi-electron atoms.",
|
|
84
|
+
stacklevel=2,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
self.shell_config = self._compute_shell_config()
|
|
88
|
+
self.num_shells = len(self.shell_config)
|
|
89
|
+
|
|
90
|
+
# ----- electron configuration -----
|
|
91
|
+
|
|
92
|
+
def _compute_shell_config(self) -> list[int]:
|
|
93
|
+
"""Fill electron shells following the 2n^2 rule."""
|
|
94
|
+
remaining = self.num_electrons
|
|
95
|
+
shells = []
|
|
96
|
+
n = 1
|
|
97
|
+
while remaining > 0 and n <= 7:
|
|
98
|
+
capacity = 2 * n**2
|
|
99
|
+
electrons_in_shell = min(remaining, capacity)
|
|
100
|
+
shells.append(electrons_in_shell)
|
|
101
|
+
remaining -= electrons_in_shell
|
|
102
|
+
n += 1
|
|
103
|
+
return shells
|
|
104
|
+
|
|
105
|
+
# ----- Bohr radius & energy -----
|
|
106
|
+
|
|
107
|
+
def orbital_radius(self, n: int) -> float:
|
|
108
|
+
"""Radius of the n-th Bohr orbit in metres.
|
|
109
|
+
|
|
110
|
+
r_n = n^2 * a_0 / Z
|
|
111
|
+
"""
|
|
112
|
+
if n < 1:
|
|
113
|
+
raise ValueError("Principal quantum number n must be >= 1")
|
|
114
|
+
return (n**2 * BOHR_RADIUS_M) / self.atomic_number
|
|
115
|
+
|
|
116
|
+
def orbital_radius_pm(self, n: int) -> float:
|
|
117
|
+
"""Radius of the n-th Bohr orbit in picometres."""
|
|
118
|
+
return self.orbital_radius(n) * 1e12
|
|
119
|
+
|
|
120
|
+
def energy_level(self, n: int) -> float:
|
|
121
|
+
"""Energy of the n-th level in eV.
|
|
122
|
+
|
|
123
|
+
E_n = -13.6 eV * Z^2 / n^2
|
|
124
|
+
"""
|
|
125
|
+
if n < 1:
|
|
126
|
+
raise ValueError("Principal quantum number n must be >= 1")
|
|
127
|
+
return -RYDBERG_ENERGY_EV * (self.atomic_number**2) / (n**2)
|
|
128
|
+
|
|
129
|
+
def energy_level_joules(self, n: int) -> float:
|
|
130
|
+
"""Energy of the n-th level in joules."""
|
|
131
|
+
return self.energy_level(n) * EV_TO_JOULES
|
|
132
|
+
|
|
133
|
+
def orbital_velocity(self, n: int) -> float:
|
|
134
|
+
"""Velocity of the electron in the n-th orbit in m/s.
|
|
135
|
+
|
|
136
|
+
v_n = Z * e^2 / (n * 4*pi*eps_0 * hbar)
|
|
137
|
+
= Z * alpha * c / n
|
|
138
|
+
where alpha ~ 1/137 is the fine-structure constant.
|
|
139
|
+
"""
|
|
140
|
+
alpha = ELECTRON_CHARGE**2 / (4 * math.pi * VACUUM_PERMITTIVITY * HBAR * SPEED_OF_LIGHT)
|
|
141
|
+
return self.atomic_number * alpha * SPEED_OF_LIGHT / n
|
|
142
|
+
|
|
143
|
+
def orbital_period(self, n: int) -> float:
|
|
144
|
+
"""Orbital period of the electron in shell n, in seconds."""
|
|
145
|
+
r = self.orbital_radius(n)
|
|
146
|
+
v = self.orbital_velocity(n)
|
|
147
|
+
return 2 * math.pi * r / v
|
|
148
|
+
|
|
149
|
+
# ----- transitions -----
|
|
150
|
+
|
|
151
|
+
def transition_energy(self, n_initial: int, n_final: int) -> float:
|
|
152
|
+
"""Energy of a photon emitted (positive) or absorbed (negative)
|
|
153
|
+
during a transition, in eV."""
|
|
154
|
+
return self.energy_level(n_initial) - self.energy_level(n_final)
|
|
155
|
+
|
|
156
|
+
def transition_wavelength(self, n_initial: int, n_final: int) -> float:
|
|
157
|
+
"""Wavelength in metres of a photon from a transition between levels."""
|
|
158
|
+
delta_e = abs(self.transition_energy(n_initial, n_final)) * EV_TO_JOULES
|
|
159
|
+
if delta_e == 0:
|
|
160
|
+
return float('inf')
|
|
161
|
+
return PLANCK_CONSTANT * SPEED_OF_LIGHT / delta_e
|
|
162
|
+
|
|
163
|
+
def transition_wavelength_nm(self, n_initial: int, n_final: int) -> float:
|
|
164
|
+
"""Wavelength in nanometres."""
|
|
165
|
+
return self.transition_wavelength(n_initial, n_final) * 1e9
|
|
166
|
+
|
|
167
|
+
def ionization_energy(self) -> float:
|
|
168
|
+
"""Minimum energy (eV) to remove the outermost electron (Bohr approx)."""
|
|
169
|
+
if self.num_shells == 0:
|
|
170
|
+
return 0.0
|
|
171
|
+
return -self.energy_level(self.num_shells)
|
|
172
|
+
|
|
173
|
+
# ----- electrostatic force on electron -----
|
|
174
|
+
|
|
175
|
+
def coulomb_force_on_electron(self, n: int) -> float:
|
|
176
|
+
"""Attractive Coulomb force on an electron in shell n, in newtons."""
|
|
177
|
+
r = self.orbital_radius(n)
|
|
178
|
+
return COULOMB_CONSTANT * (self.protons * ELECTRON_CHARGE**2) / r**2
|
|
179
|
+
|
|
180
|
+
# ----- representation -----
|
|
181
|
+
|
|
182
|
+
def __repr__(self):
|
|
183
|
+
iso = f"-{self.mass_number}" if self.mass_number != round(self.standard_atomic_weight) else ""
|
|
184
|
+
charge_str = ""
|
|
185
|
+
if self.charge > 0:
|
|
186
|
+
charge_str = f" (+{self.charge})"
|
|
187
|
+
elif self.charge < 0:
|
|
188
|
+
charge_str = f" ({self.charge})"
|
|
189
|
+
return (f"BohrAtom({self.symbol}{iso}, Z={self.atomic_number}, "
|
|
190
|
+
f"e={self.num_electrons}{charge_str}, "
|
|
191
|
+
f"shells={self.shell_config})")
|
|
192
|
+
|
|
193
|
+
def summary(self) -> str:
|
|
194
|
+
"""Return a human-readable summary of the atom."""
|
|
195
|
+
lines = [
|
|
196
|
+
f"{'='*50}",
|
|
197
|
+
f" {self.name} ({self.symbol})",
|
|
198
|
+
f" Atomic number (Z): {self.atomic_number}",
|
|
199
|
+
f" Mass number (A): {self.mass_number}",
|
|
200
|
+
f" Protons: {self.protons} Neutrons: {self.neutrons}",
|
|
201
|
+
f" Electrons: {self.num_electrons} Charge: {self.charge:+d}",
|
|
202
|
+
f" Shell configuration: {self.shell_config}",
|
|
203
|
+
f"{'='*50}",
|
|
204
|
+
f" Shell Electrons Radius (pm) Energy (eV)",
|
|
205
|
+
f" {'-'*46}",
|
|
206
|
+
]
|
|
207
|
+
for i, count in enumerate(self.shell_config, start=1):
|
|
208
|
+
r = self.orbital_radius_pm(i)
|
|
209
|
+
e = self.energy_level(i)
|
|
210
|
+
lines.append(f" n={i:<4} {count:<10} {r:>10.2f} {e:>10.4f}")
|
|
211
|
+
lines.append(f" {'-'*46}")
|
|
212
|
+
lines.append(f" Ionization energy (Bohr): {self.ionization_energy():.4f} eV")
|
|
213
|
+
return "\n".join(lines)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Convenience constructors
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
def from_symbol(symbol: str, mass_number: int = None, charge: int = 0) -> BohrAtom:
|
|
220
|
+
"""Create a BohrAtom from an element symbol string."""
|
|
221
|
+
symbol = symbol.capitalize()
|
|
222
|
+
if len(symbol) > 1:
|
|
223
|
+
symbol = symbol[0].upper() + symbol[1:].lower()
|
|
224
|
+
if symbol not in SYMBOL_TO_Z:
|
|
225
|
+
raise ValueError(f"Unknown element symbol: {symbol}")
|
|
226
|
+
return BohrAtom(SYMBOL_TO_Z[symbol], mass_number, charge)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def from_name(name: str, mass_number: int = None, charge: int = 0) -> BohrAtom:
|
|
230
|
+
"""Create a BohrAtom from an element name string."""
|
|
231
|
+
name_lower = name.strip().lower()
|
|
232
|
+
for z, (sym, elem_name, _) in ELEMENTS.items():
|
|
233
|
+
if elem_name.lower() == name_lower:
|
|
234
|
+
return BohrAtom(z, mass_number, charge)
|
|
235
|
+
raise ValueError(f"Unknown element name: {name}")
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Mechanical Model of the Atom
|
|
3
|
+
|
|
4
|
+
Represents atoms using the quantum mechanical framework:
|
|
5
|
+
- Four quantum numbers (n, l, m_l, m_s) per electron
|
|
6
|
+
- Electron configuration via Aufbau principle, Hund's rule, Pauli exclusion
|
|
7
|
+
- Slater's rules for effective nuclear charge (Z_eff)
|
|
8
|
+
- Approximate orbital energies for multi-electron atoms
|
|
9
|
+
- Known configuration exceptions for d- and f-block elements
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
from molbuilder.core.constants import RYDBERG_ENERGY_EV, EV_TO_JOULES, BOHR_RADIUS_M, HBAR
|
|
14
|
+
from molbuilder.core.elements import ELEMENTS, SYMBOL_TO_Z, NOBLE_GASES
|
|
15
|
+
from molbuilder.atomic.quantum_numbers import (
|
|
16
|
+
QuantumState,
|
|
17
|
+
Subshell,
|
|
18
|
+
aufbau_order,
|
|
19
|
+
AUFBAU_EXCEPTIONS,
|
|
20
|
+
SUBSHELL_LETTER,
|
|
21
|
+
)
|
|
22
|
+
from molbuilder.atomic.wavefunctions import orbital_angular_momentum, angular_momentum_z
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# ===================================================================
|
|
26
|
+
# Slater's rules for effective nuclear charge
|
|
27
|
+
# ===================================================================
|
|
28
|
+
|
|
29
|
+
def slater_zeff(Z: int, n: int, l: int,
|
|
30
|
+
config: list[tuple[int, int, int]]) -> float:
|
|
31
|
+
"""Compute Slater's effective nuclear charge Z_eff for an electron
|
|
32
|
+
in subshell (n, l) of an atom with atomic number Z.
|
|
33
|
+
|
|
34
|
+
Slater grouping: (1s)(2s,2p)(3s,3p)(3d)(4s,4p)(4d)(4f)(5s,5p)...
|
|
35
|
+
|
|
36
|
+
Shielding rules:
|
|
37
|
+
Same group: 0.35 each (0.30 for 1s)
|
|
38
|
+
(n-1) group: 0.85 each [for s,p electrons]
|
|
39
|
+
All lower: 1.00 each [for s,p electrons]
|
|
40
|
+
For d,f: same group 0.35, all lower groups 1.00
|
|
41
|
+
"""
|
|
42
|
+
def slater_group(ni, li):
|
|
43
|
+
"""Assign Slater group index for sorting."""
|
|
44
|
+
if li <= 1:
|
|
45
|
+
return (ni, 0) # s and p are in the same group
|
|
46
|
+
else:
|
|
47
|
+
return (ni, li) # d, f each in their own group
|
|
48
|
+
|
|
49
|
+
target_group = slater_group(n, l)
|
|
50
|
+
is_sp = (l <= 1) # s or p electron
|
|
51
|
+
|
|
52
|
+
# Build list of all occupied groups with their electron counts
|
|
53
|
+
groups = {}
|
|
54
|
+
for (ni, li, count) in config:
|
|
55
|
+
g = slater_group(ni, li)
|
|
56
|
+
groups[g] = groups.get(g, 0) + count
|
|
57
|
+
|
|
58
|
+
sigma = 0.0
|
|
59
|
+
|
|
60
|
+
for g, count in groups.items():
|
|
61
|
+
if g == target_group:
|
|
62
|
+
# Electrons in the same group (exclude self)
|
|
63
|
+
same_count = count - 1
|
|
64
|
+
if same_count > 0:
|
|
65
|
+
s_each = 0.30 if (n == 1 and l == 0) else 0.35
|
|
66
|
+
sigma += same_count * s_each
|
|
67
|
+
elif g < target_group:
|
|
68
|
+
if is_sp:
|
|
69
|
+
# For s/p electron: (n-1) shell shields 0.85, lower shields 1.0
|
|
70
|
+
# The (n-1) shell includes ALL groups whose principal quantum
|
|
71
|
+
# number equals n-1, regardless of angular momentum (s/p/d/f).
|
|
72
|
+
g_n = g[0] # principal quantum number of the inner group
|
|
73
|
+
if g_n == n - 1:
|
|
74
|
+
sigma += count * 0.85
|
|
75
|
+
else:
|
|
76
|
+
sigma += count * 1.00
|
|
77
|
+
else:
|
|
78
|
+
# For d/f electron: all lower groups shield 1.0
|
|
79
|
+
sigma += count * 1.00
|
|
80
|
+
|
|
81
|
+
return Z - sigma
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ===================================================================
|
|
85
|
+
# The Quantum Atom
|
|
86
|
+
# ===================================================================
|
|
87
|
+
|
|
88
|
+
class QuantumAtom:
|
|
89
|
+
"""Represents an atom using the quantum mechanical model.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
atomic_number : int
|
|
94
|
+
Number of protons (Z). Determines the element.
|
|
95
|
+
mass_number : int or None
|
|
96
|
+
Protons + neutrons. Defaults to rounded standard atomic weight.
|
|
97
|
+
charge : int
|
|
98
|
+
Net ionic charge (0 = neutral).
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def __init__(self, atomic_number: int, mass_number: int = None, charge: int = 0):
|
|
102
|
+
if atomic_number < 1 or atomic_number > 118:
|
|
103
|
+
raise ValueError(f"Atomic number must be 1-118, got {atomic_number}")
|
|
104
|
+
|
|
105
|
+
self.atomic_number = atomic_number
|
|
106
|
+
self.protons = atomic_number
|
|
107
|
+
symbol, name, weight = ELEMENTS[atomic_number]
|
|
108
|
+
self.symbol = symbol
|
|
109
|
+
self.name = name
|
|
110
|
+
self.standard_atomic_weight = weight
|
|
111
|
+
|
|
112
|
+
self.mass_number = mass_number if mass_number is not None else round(weight)
|
|
113
|
+
self.neutrons = self.mass_number - self.protons
|
|
114
|
+
self.charge = charge
|
|
115
|
+
self.num_electrons = self.protons - charge
|
|
116
|
+
|
|
117
|
+
if self.num_electrons < 0:
|
|
118
|
+
raise ValueError("Charge exceeds proton count")
|
|
119
|
+
|
|
120
|
+
# Build ground-state electron configuration
|
|
121
|
+
self.subshells = self._build_configuration()
|
|
122
|
+
self.quantum_states = self._enumerate_quantum_states()
|
|
123
|
+
|
|
124
|
+
# ------ configuration ------
|
|
125
|
+
|
|
126
|
+
def _build_configuration(self) -> list[Subshell]:
|
|
127
|
+
"""Build the ground-state electron configuration."""
|
|
128
|
+
# Check for known exceptions (neutral atoms only)
|
|
129
|
+
if self.charge == 0 and self.atomic_number in AUFBAU_EXCEPTIONS:
|
|
130
|
+
return [
|
|
131
|
+
Subshell(n, l, count)
|
|
132
|
+
for n, l, count in AUFBAU_EXCEPTIONS[self.atomic_number]
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
# Standard Aufbau filling
|
|
136
|
+
order = aufbau_order()
|
|
137
|
+
remaining = self.num_electrons
|
|
138
|
+
subshells = []
|
|
139
|
+
|
|
140
|
+
for n, l in order:
|
|
141
|
+
if remaining <= 0:
|
|
142
|
+
break
|
|
143
|
+
capacity = 2 * (2 * l + 1)
|
|
144
|
+
count = min(remaining, capacity)
|
|
145
|
+
subshells.append(Subshell(n, l, count))
|
|
146
|
+
remaining -= count
|
|
147
|
+
|
|
148
|
+
return subshells
|
|
149
|
+
|
|
150
|
+
def _enumerate_quantum_states(self) -> list[QuantumState]:
|
|
151
|
+
"""List all individual quantum states following Hund's rules."""
|
|
152
|
+
states = []
|
|
153
|
+
for ss in self.subshells:
|
|
154
|
+
states.extend(ss.quantum_states())
|
|
155
|
+
return states
|
|
156
|
+
|
|
157
|
+
# ------ configuration as tuples (for Slater's rules) ------
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def config_tuples(self) -> list[tuple[int, int, int]]:
|
|
161
|
+
"""Configuration as [(n, l, count), ...]."""
|
|
162
|
+
return [(ss.n, ss.l, ss.electron_count) for ss in self.subshells]
|
|
163
|
+
|
|
164
|
+
# ------ string representations ------
|
|
165
|
+
|
|
166
|
+
def electron_configuration_string(self) -> str:
|
|
167
|
+
"""Full configuration string, e.g. '1s2 2s2 2p6 3s2 3p2'."""
|
|
168
|
+
return " ".join(
|
|
169
|
+
f"{ss.label}{ss.electron_count}" for ss in self.subshells
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def noble_gas_notation(self) -> str:
|
|
173
|
+
"""Shorthand using noble gas core, e.g. '[Ne] 3s2 3p2'."""
|
|
174
|
+
core_z = 0
|
|
175
|
+
core_symbol = ""
|
|
176
|
+
electrons_accounted = 0
|
|
177
|
+
|
|
178
|
+
for z, sym in sorted(NOBLE_GASES.items()):
|
|
179
|
+
if z < self.atomic_number and z <= self.num_electrons:
|
|
180
|
+
core_z = z
|
|
181
|
+
core_symbol = sym
|
|
182
|
+
|
|
183
|
+
if core_z == 0:
|
|
184
|
+
return self.electron_configuration_string()
|
|
185
|
+
|
|
186
|
+
# Find how many subshells form the core
|
|
187
|
+
electrons_accounted = 0
|
|
188
|
+
core_end_idx = 0
|
|
189
|
+
for i, ss in enumerate(self.subshells):
|
|
190
|
+
electrons_accounted += ss.electron_count
|
|
191
|
+
core_end_idx = i + 1
|
|
192
|
+
if electrons_accounted == core_z:
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
valence_str = " ".join(
|
|
196
|
+
f"{ss.label}{ss.electron_count}"
|
|
197
|
+
for ss in self.subshells[core_end_idx:]
|
|
198
|
+
)
|
|
199
|
+
return f"[{core_symbol}] {valence_str}".strip()
|
|
200
|
+
|
|
201
|
+
# ------ valence / core ------
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def valence_shell(self) -> int:
|
|
205
|
+
"""Highest principal quantum number with electrons."""
|
|
206
|
+
if not self.subshells:
|
|
207
|
+
return 0
|
|
208
|
+
return max(ss.n for ss in self.subshells)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def valence_electrons(self) -> int:
|
|
212
|
+
"""Number of electrons in the valence shell."""
|
|
213
|
+
vs = self.valence_shell
|
|
214
|
+
return sum(ss.electron_count for ss in self.subshells if ss.n == vs)
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def core_electrons(self) -> int:
|
|
218
|
+
"""Number of core (non-valence) electrons."""
|
|
219
|
+
return self.num_electrons - self.valence_electrons
|
|
220
|
+
|
|
221
|
+
# ------ effective nuclear charge ------
|
|
222
|
+
|
|
223
|
+
def effective_nuclear_charge(self, n: int, l: int) -> float:
|
|
224
|
+
"""Slater's Z_eff for an electron in subshell (n, l)."""
|
|
225
|
+
return slater_zeff(self.atomic_number, n, l, self.config_tuples)
|
|
226
|
+
|
|
227
|
+
# ------ energy approximations ------
|
|
228
|
+
|
|
229
|
+
def orbital_energy_eV(self, n: int, l: int) -> float:
|
|
230
|
+
"""Approximate orbital energy using Slater's Z_eff.
|
|
231
|
+
|
|
232
|
+
E_{n,l} ~ -13.6 eV * Z_eff^2 / n^2
|
|
233
|
+
"""
|
|
234
|
+
z_eff = self.effective_nuclear_charge(n, l)
|
|
235
|
+
return -RYDBERG_ENERGY_EV * z_eff**2 / n**2
|
|
236
|
+
|
|
237
|
+
def ionization_energy_eV(self) -> float:
|
|
238
|
+
"""Approximate first ionization energy (Slater model), in eV.
|
|
239
|
+
|
|
240
|
+
Energy required to remove the outermost electron.
|
|
241
|
+
"""
|
|
242
|
+
if not self.subshells:
|
|
243
|
+
return 0.0
|
|
244
|
+
outermost = self.subshells[-1]
|
|
245
|
+
return -self.orbital_energy_eV(outermost.n, outermost.l)
|
|
246
|
+
|
|
247
|
+
# ------ angular momentum ------
|
|
248
|
+
|
|
249
|
+
def total_spin_quantum_number(self) -> float:
|
|
250
|
+
"""Total spin S = sum of m_s values (maximised by Hund's rule)."""
|
|
251
|
+
return abs(sum(qs.ms for qs in self.quantum_states))
|
|
252
|
+
|
|
253
|
+
def spin_multiplicity(self) -> int:
|
|
254
|
+
"""2S + 1 -- spin multiplicity of the ground state."""
|
|
255
|
+
S = self.total_spin_quantum_number()
|
|
256
|
+
return int(2 * S + 1)
|
|
257
|
+
|
|
258
|
+
# ------ display ------
|
|
259
|
+
|
|
260
|
+
def __repr__(self):
|
|
261
|
+
charge_str = ""
|
|
262
|
+
if self.charge > 0:
|
|
263
|
+
charge_str = f"(+{self.charge})"
|
|
264
|
+
elif self.charge < 0:
|
|
265
|
+
charge_str = f"({self.charge})"
|
|
266
|
+
return (f"QuantumAtom({self.symbol}, Z={self.atomic_number}, "
|
|
267
|
+
f"e={self.num_electrons}{charge_str})")
|
|
268
|
+
|
|
269
|
+
def summary(self) -> str:
|
|
270
|
+
"""Print a detailed summary of the atom's quantum description."""
|
|
271
|
+
lines = [
|
|
272
|
+
f"{'='*60}",
|
|
273
|
+
f" {self.name} ({self.symbol})",
|
|
274
|
+
f" Atomic number: {self.atomic_number}",
|
|
275
|
+
f" Mass number: {self.mass_number}",
|
|
276
|
+
f" Protons: {self.protons} Neutrons: {self.neutrons} "
|
|
277
|
+
f"Electrons: {self.num_electrons}",
|
|
278
|
+
f" Charge: {self.charge:+d}",
|
|
279
|
+
f"{'='*60}",
|
|
280
|
+
f" Electron configuration:",
|
|
281
|
+
f" {self.electron_configuration_string()}",
|
|
282
|
+
f" {self.noble_gas_notation()}",
|
|
283
|
+
f" Valence electrons: {self.valence_electrons}",
|
|
284
|
+
f" Spin multiplicity: {self.spin_multiplicity()}",
|
|
285
|
+
f"{'='*60}",
|
|
286
|
+
f" Subshell e- Z_eff Energy (eV)",
|
|
287
|
+
f" {'-'*48}",
|
|
288
|
+
]
|
|
289
|
+
for ss in self.subshells:
|
|
290
|
+
z_eff = self.effective_nuclear_charge(ss.n, ss.l)
|
|
291
|
+
e_eV = self.orbital_energy_eV(ss.n, ss.l)
|
|
292
|
+
lines.append(
|
|
293
|
+
f" {ss.label:<6} {ss.electron_count:>4} "
|
|
294
|
+
f"{z_eff:>7.3f} {e_eV:>10.4f}"
|
|
295
|
+
)
|
|
296
|
+
lines.append(f" {'-'*48}")
|
|
297
|
+
lines.append(
|
|
298
|
+
f" Ionization energy (Slater): "
|
|
299
|
+
f"{self.ionization_energy_eV():.4f} eV"
|
|
300
|
+
)
|
|
301
|
+
lines.append("")
|
|
302
|
+
|
|
303
|
+
# Show all quantum states for small atoms
|
|
304
|
+
if self.num_electrons <= 18:
|
|
305
|
+
lines.append(f" All quantum states (n, l, ml, ms):")
|
|
306
|
+
for qs in self.quantum_states:
|
|
307
|
+
lines.append(f" {qs}")
|
|
308
|
+
|
|
309
|
+
return "\n".join(lines)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ===================================================================
|
|
313
|
+
# Convenience constructors
|
|
314
|
+
# ===================================================================
|
|
315
|
+
|
|
316
|
+
def from_symbol(symbol: str, mass_number: int = None, charge: int = 0) -> QuantumAtom:
|
|
317
|
+
"""Create a QuantumAtom from an element symbol."""
|
|
318
|
+
sym = symbol.strip()
|
|
319
|
+
if len(sym) > 1:
|
|
320
|
+
sym = sym[0].upper() + sym[1:].lower()
|
|
321
|
+
else:
|
|
322
|
+
sym = sym.upper()
|
|
323
|
+
if sym not in SYMBOL_TO_Z:
|
|
324
|
+
raise ValueError(f"Unknown element symbol: {symbol}")
|
|
325
|
+
return QuantumAtom(SYMBOL_TO_Z[sym], mass_number, charge)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def from_name(name: str, mass_number: int = None, charge: int = 0) -> QuantumAtom:
|
|
329
|
+
"""Create a QuantumAtom from an element name."""
|
|
330
|
+
name_lower = name.strip().lower()
|
|
331
|
+
for z, (sym, elem_name, _) in ELEMENTS.items():
|
|
332
|
+
if elem_name.lower() == name_lower:
|
|
333
|
+
return QuantumAtom(z, mass_number, charge)
|
|
334
|
+
raise ValueError(f"Unknown element name: {name}")
|