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
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Numbers and Electron Configuration
|
|
3
|
+
|
|
4
|
+
Defines quantum number containers, subshell representations,
|
|
5
|
+
Aufbau filling order, and known configuration exceptions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from molbuilder.core.constants import HBAR
|
|
11
|
+
from molbuilder.core.elements import NOBLE_GASES
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Orbital labels
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
SUBSHELL_LETTER = {0: "s", 1: "p", 2: "d", 3: "f", 4: "g", 5: "h", 6: "i"}
|
|
18
|
+
SUBSHELL_NUMBER = {v: k for k, v in SUBSHELL_LETTER.items()}
|
|
19
|
+
|
|
20
|
+
ORBITAL_NAMES = {
|
|
21
|
+
(0, 0): "s",
|
|
22
|
+
(1, -1): "p_y", (1, 0): "p_z", (1, 1): "p_x",
|
|
23
|
+
(2, -2): "d_xy", (2, -1): "d_yz", (2, 0): "d_z2",
|
|
24
|
+
(2, 1): "d_xz", (2, 2): "d_x2-y2",
|
|
25
|
+
(3, -3): "f_y(3x2-y2)", (3, -2): "f_xyz", (3, -1): "f_yz2",
|
|
26
|
+
(3, 0): "f_z3", (3, 1): "f_xz2", (3, 2): "f_z(x2-y2)",
|
|
27
|
+
(3, 3): "f_x(x2-3y2)",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ===================================================================
|
|
32
|
+
# Quantum number container
|
|
33
|
+
# ===================================================================
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True, order=True)
|
|
36
|
+
class QuantumState:
|
|
37
|
+
"""A single-electron quantum state defined by four quantum numbers.
|
|
38
|
+
|
|
39
|
+
n : principal quantum number (1, 2, 3, ...)
|
|
40
|
+
l : orbital angular momentum number (0 .. n-1)
|
|
41
|
+
ml : magnetic quantum number (-l .. +l)
|
|
42
|
+
ms : spin magnetic quantum number (+0.5 or -0.5)
|
|
43
|
+
"""
|
|
44
|
+
n: int
|
|
45
|
+
l: int
|
|
46
|
+
ml: int
|
|
47
|
+
ms: float
|
|
48
|
+
|
|
49
|
+
def __post_init__(self):
|
|
50
|
+
if self.n < 1:
|
|
51
|
+
raise ValueError(f"n must be >= 1, got {self.n}")
|
|
52
|
+
if not (0 <= self.l < self.n):
|
|
53
|
+
raise ValueError(f"l must be 0..{self.n-1}, got {self.l}")
|
|
54
|
+
if not (-self.l <= self.ml <= self.l):
|
|
55
|
+
raise ValueError(f"ml must be {-self.l}..{self.l}, got {self.ml}")
|
|
56
|
+
if self.ms not in (0.5, -0.5):
|
|
57
|
+
raise ValueError(f"ms must be +0.5 or -0.5, got {self.ms}")
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def subshell_label(self) -> str:
|
|
61
|
+
return f"{self.n}{SUBSHELL_LETTER.get(self.l, '?')}"
|
|
62
|
+
|
|
63
|
+
def __repr__(self):
|
|
64
|
+
sign = "+" if self.ms > 0 else "-"
|
|
65
|
+
return f"({self.n},{self.l},{self.ml},{sign}1/2)"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ===================================================================
|
|
69
|
+
# Subshell
|
|
70
|
+
# ===================================================================
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Subshell:
|
|
74
|
+
"""A subshell (n, l) that can hold up to 2*(2l+1) electrons."""
|
|
75
|
+
n: int
|
|
76
|
+
l: int
|
|
77
|
+
electron_count: int = 0
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def capacity(self) -> int:
|
|
81
|
+
return 2 * (2 * self.l + 1)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def is_full(self) -> bool:
|
|
85
|
+
return self.electron_count >= self.capacity
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def is_half_filled(self) -> bool:
|
|
89
|
+
return self.electron_count == (2 * self.l + 1)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def label(self) -> str:
|
|
93
|
+
return f"{self.n}{SUBSHELL_LETTER.get(self.l, '?')}"
|
|
94
|
+
|
|
95
|
+
def quantum_states(self) -> list[QuantumState]:
|
|
96
|
+
"""Generate individual QuantumState objects following Hund's rules.
|
|
97
|
+
|
|
98
|
+
Fill order: first all m_l values with spin +1/2 (maximise spin),
|
|
99
|
+
then fill with spin -1/2 starting from most negative m_l.
|
|
100
|
+
"""
|
|
101
|
+
states = []
|
|
102
|
+
remaining = self.electron_count
|
|
103
|
+
ml_values = list(range(-self.l, self.l + 1))
|
|
104
|
+
|
|
105
|
+
# First pass: spin-up (+1/2) across all m_l
|
|
106
|
+
for ml in ml_values:
|
|
107
|
+
if remaining <= 0:
|
|
108
|
+
break
|
|
109
|
+
states.append(QuantumState(self.n, self.l, ml, +0.5))
|
|
110
|
+
remaining -= 1
|
|
111
|
+
|
|
112
|
+
# Second pass: spin-down (-1/2)
|
|
113
|
+
for ml in ml_values:
|
|
114
|
+
if remaining <= 0:
|
|
115
|
+
break
|
|
116
|
+
states.append(QuantumState(self.n, self.l, ml, -0.5))
|
|
117
|
+
remaining -= 1
|
|
118
|
+
|
|
119
|
+
return states
|
|
120
|
+
|
|
121
|
+
def __repr__(self):
|
|
122
|
+
return f"{self.label}^{self.electron_count}"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ===================================================================
|
|
126
|
+
# Aufbau filling order
|
|
127
|
+
# ===================================================================
|
|
128
|
+
|
|
129
|
+
def aufbau_order(max_n: int = 7) -> list[tuple[int, int]]:
|
|
130
|
+
"""Generate subshell filling order using the (n+l, n) rule.
|
|
131
|
+
|
|
132
|
+
Returns list of (n, l) tuples in Aufbau filling order:
|
|
133
|
+
1s, 2s, 2p, 3s, 3p, 4s, 3d, 4p, 5s, 4d, 5p, 6s, 4f, 5d, 6p, ...
|
|
134
|
+
"""
|
|
135
|
+
subshells = []
|
|
136
|
+
for n in range(1, max_n + 1):
|
|
137
|
+
for l in range(n):
|
|
138
|
+
subshells.append((n, l))
|
|
139
|
+
# Sort by (n+l, n) -- Madelung rule
|
|
140
|
+
subshells.sort(key=lambda nl: (nl[0] + nl[1], nl[0]))
|
|
141
|
+
return subshells
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ===================================================================
|
|
145
|
+
# Known electron configuration exceptions
|
|
146
|
+
# ===================================================================
|
|
147
|
+
|
|
148
|
+
# Exceptions encoded as {Z: [(n, l, count), ...]} -- full ground state config.
|
|
149
|
+
# Only elements whose config deviates from Aufbau prediction are listed.
|
|
150
|
+
AUFBAU_EXCEPTIONS: dict[int, list[tuple[int, int, int]]] = {
|
|
151
|
+
24: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,5),(4,0,1)], # Cr
|
|
152
|
+
29: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,1)], # Cu
|
|
153
|
+
41: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
154
|
+
(4,2,4),(5,0,1)], # Nb
|
|
155
|
+
42: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
156
|
+
(4,2,5),(5,0,1)], # Mo
|
|
157
|
+
44: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
158
|
+
(4,2,7),(5,0,1)], # Ru
|
|
159
|
+
45: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
160
|
+
(4,2,8),(5,0,1)], # Rh
|
|
161
|
+
46: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
162
|
+
(4,2,10)], # Pd
|
|
163
|
+
47: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
164
|
+
(4,2,10),(5,0,1)], # Ag
|
|
165
|
+
57: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
166
|
+
(4,2,10),(5,0,2),(5,1,6),(5,2,1),(6,0,2)], # La
|
|
167
|
+
58: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
168
|
+
(4,2,10),(4,3,1),(5,0,2),(5,1,6),(5,2,1),(6,0,2)], # Ce
|
|
169
|
+
64: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
170
|
+
(4,2,10),(4,3,7),(5,0,2),(5,1,6),(5,2,1),(6,0,2)], # Gd
|
|
171
|
+
78: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
172
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,9),(6,0,1)], # Pt
|
|
173
|
+
79: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
174
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,10),(6,0,1)], # Au
|
|
175
|
+
89: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
176
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,10),(6,0,2),(6,1,6),
|
|
177
|
+
(6,2,1),(7,0,2)], # Ac
|
|
178
|
+
90: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
179
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,10),(6,0,2),(6,1,6),
|
|
180
|
+
(6,2,2),(7,0,2)], # Th
|
|
181
|
+
91: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
182
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,10),(5,3,2),(6,0,2),(6,1,6),
|
|
183
|
+
(6,2,1),(7,0,2)], # Pa
|
|
184
|
+
92: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
185
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,10),(5,3,3),(6,0,2),(6,1,6),
|
|
186
|
+
(6,2,1),(7,0,2)], # U
|
|
187
|
+
93: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
188
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,10),(5,3,4),(6,0,2),(6,1,6),
|
|
189
|
+
(6,2,1),(7,0,2)], # Np
|
|
190
|
+
96: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
191
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,10),(5,3,7),(6,0,2),(6,1,6),
|
|
192
|
+
(6,2,1),(7,0,2)], # Cm
|
|
193
|
+
103: [(1,0,2),(2,0,2),(2,1,6),(3,0,2),(3,1,6),(3,2,10),(4,0,2),(4,1,6),
|
|
194
|
+
(4,2,10),(4,3,14),(5,0,2),(5,1,6),(5,2,10),(5,3,14),(6,0,2),(6,1,6),
|
|
195
|
+
(7,0,2),(7,1,1)], # Lr
|
|
196
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Quantum Mechanical Wave Functions for Hydrogen-like Atoms
|
|
3
|
+
|
|
4
|
+
Provides exact analytical solutions to the time-independent Schrodinger
|
|
5
|
+
equation for hydrogen-like (single-electron) atoms. Multi-electron atoms
|
|
6
|
+
can use these with an effective nuclear charge (Z_eff) from Slater's rules.
|
|
7
|
+
|
|
8
|
+
Key components:
|
|
9
|
+
- Radial wave function R_nl(r) (associated Laguerre polynomials)
|
|
10
|
+
- Angular wave function Y_l^m(theta, phi) (spherical harmonics)
|
|
11
|
+
- Full wave function psi_nlm = R_nl * Y_l^m
|
|
12
|
+
- Probability densities and expectation values
|
|
13
|
+
|
|
14
|
+
Physics conventions:
|
|
15
|
+
- theta : polar angle [0, pi]
|
|
16
|
+
- phi : azimuthal angle [0, 2*pi]
|
|
17
|
+
- Condon-Shortley phase is included (via scipy)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import functools
|
|
21
|
+
import math
|
|
22
|
+
import numpy as np
|
|
23
|
+
from scipy import special
|
|
24
|
+
from molbuilder.core.constants import (
|
|
25
|
+
BOHR_RADIUS_M as BOHR_RADIUS,
|
|
26
|
+
HBAR,
|
|
27
|
+
ELECTRON_MASS,
|
|
28
|
+
ELECTRON_CHARGE,
|
|
29
|
+
SPEED_OF_LIGHT,
|
|
30
|
+
EV_TO_JOULES,
|
|
31
|
+
RYDBERG_ENERGY_EV,
|
|
32
|
+
)
|
|
33
|
+
from molbuilder.atomic.quantum_numbers import SUBSHELL_LETTER, ORBITAL_NAMES
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ===================================================================
|
|
37
|
+
# Radial wave function
|
|
38
|
+
# ===================================================================
|
|
39
|
+
|
|
40
|
+
def radial_wavefunction(n: int, l: int, r, Z: int = 1):
|
|
41
|
+
"""Compute R_nl(r) for a hydrogen-like atom.
|
|
42
|
+
|
|
43
|
+
R_nl(r) = N_nl * rho^l * exp(-rho/2) * L_{n-l-1}^{2l+1}(rho)
|
|
44
|
+
|
|
45
|
+
where rho = 2*Z*r / (n * a_0) and N_nl is the normalization constant.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
n : int — principal quantum number (>= 1)
|
|
50
|
+
l : int — angular momentum quantum number (0 <= l < n)
|
|
51
|
+
r : float or ndarray — radial distance in metres
|
|
52
|
+
Z : int — nuclear charge (atomic number for hydrogen-like)
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
R : same shape as r
|
|
57
|
+
"""
|
|
58
|
+
if n < 1:
|
|
59
|
+
raise ValueError(f"n must be >= 1, got {n}")
|
|
60
|
+
if l < 0 or l >= n:
|
|
61
|
+
raise ValueError(f"l must satisfy 0 <= l < n, got l={l}, n={n}")
|
|
62
|
+
|
|
63
|
+
r = np.asarray(r, dtype=float)
|
|
64
|
+
a0 = BOHR_RADIUS
|
|
65
|
+
rho = 2.0 * Z * r / (n * a0)
|
|
66
|
+
|
|
67
|
+
# Normalization in log-space to avoid overflow for large n
|
|
68
|
+
log_norm = (
|
|
69
|
+
1.5 * math.log(2.0 * Z / (n * a0))
|
|
70
|
+
+ 0.5 * (math.lgamma(n - l) - math.log(2.0 * n) - math.lgamma(n + l + 1))
|
|
71
|
+
)
|
|
72
|
+
norm = math.exp(log_norm)
|
|
73
|
+
|
|
74
|
+
laguerre = special.genlaguerre(n - l - 1, 2 * l + 1)
|
|
75
|
+
|
|
76
|
+
R = norm * np.power(rho, l) * np.exp(-rho / 2.0) * laguerre(rho)
|
|
77
|
+
return R
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def radial_wavefunction_scaled(n: int, l: int, r_over_a0, Z: int = 1):
|
|
81
|
+
"""R_nl evaluated with r given in units of a_0 (dimensionless).
|
|
82
|
+
|
|
83
|
+
Convenient for plotting — returns R in units of a_0^{-3/2}.
|
|
84
|
+
"""
|
|
85
|
+
r_metres = np.asarray(r_over_a0, dtype=float) * BOHR_RADIUS
|
|
86
|
+
return radial_wavefunction(n, l, r_metres, Z)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ===================================================================
|
|
90
|
+
# Spherical harmonics
|
|
91
|
+
# ===================================================================
|
|
92
|
+
|
|
93
|
+
def spherical_harmonic(l: int, m: int, theta, phi):
|
|
94
|
+
"""Complex spherical harmonic Y_l^m(theta, phi).
|
|
95
|
+
|
|
96
|
+
Parameters
|
|
97
|
+
----------
|
|
98
|
+
l : int — degree (>= 0)
|
|
99
|
+
m : int — order (-l <= m <= l)
|
|
100
|
+
theta : array-like — polar angle [0, pi]
|
|
101
|
+
phi : array-like — azimuthal angle [0, 2*pi]
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
Y : complex ndarray — spherical harmonic values
|
|
106
|
+
"""
|
|
107
|
+
if abs(m) > l:
|
|
108
|
+
raise ValueError(f"|m| must be <= l, got l={l}, m={m}")
|
|
109
|
+
theta = np.asarray(theta, dtype=float)
|
|
110
|
+
phi = np.asarray(phi, dtype=float)
|
|
111
|
+
# scipy convention: sph_harm_y(n, m, theta_polar, theta_az)
|
|
112
|
+
return special.sph_harm_y(l, m, theta, phi)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def real_spherical_harmonic(l: int, m: int, theta, phi):
|
|
116
|
+
"""Real spherical harmonic S_l^m(theta, phi).
|
|
117
|
+
|
|
118
|
+
These produce the standard orbital shapes:
|
|
119
|
+
m > 0 : proportional to cos(m*phi) (e.g. p_x, d_xz, d_x2-y2)
|
|
120
|
+
m = 0 : axially symmetric (e.g. p_z, d_z2)
|
|
121
|
+
m < 0 : proportional to sin(|m|*phi) (e.g. p_y, d_yz, d_xy)
|
|
122
|
+
|
|
123
|
+
Convention:
|
|
124
|
+
m > 0 : sqrt(2) * (-1)^m * Re(Y_l^m)
|
|
125
|
+
m = 0 : Y_l^0
|
|
126
|
+
m < 0 : sqrt(2) * (-1)^|m| * Im(Y_l^|m|)
|
|
127
|
+
"""
|
|
128
|
+
if abs(m) > l:
|
|
129
|
+
raise ValueError(f"|m| must be <= l, got l={l}, m={m}")
|
|
130
|
+
|
|
131
|
+
theta = np.asarray(theta, dtype=float)
|
|
132
|
+
phi = np.asarray(phi, dtype=float)
|
|
133
|
+
|
|
134
|
+
if m > 0:
|
|
135
|
+
Y = special.sph_harm_y(l, m, theta, phi)
|
|
136
|
+
return np.sqrt(2.0) * (-1)**m * Y.real
|
|
137
|
+
elif m < 0:
|
|
138
|
+
Y = special.sph_harm_y(l, abs(m), theta, phi)
|
|
139
|
+
return np.sqrt(2.0) * (-1)**abs(m) * Y.imag
|
|
140
|
+
else:
|
|
141
|
+
return special.sph_harm_y(l, 0, theta, phi).real
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ===================================================================
|
|
145
|
+
# Full wave function
|
|
146
|
+
# ===================================================================
|
|
147
|
+
|
|
148
|
+
def wavefunction(n: int, l: int, m: int, r, theta, phi, Z: int = 1):
|
|
149
|
+
"""Full hydrogen-like wave function psi_nlm(r, theta, phi).
|
|
150
|
+
|
|
151
|
+
psi = R_nl(r) * Y_l^m(theta, phi)
|
|
152
|
+
|
|
153
|
+
Returns complex values (due to complex spherical harmonics).
|
|
154
|
+
"""
|
|
155
|
+
R = radial_wavefunction(n, l, r, Z)
|
|
156
|
+
Y = spherical_harmonic(l, m, theta, phi)
|
|
157
|
+
return R * Y
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def wavefunction_real(n: int, l: int, m: int, r, theta, phi, Z: int = 1):
|
|
161
|
+
"""Wave function using real spherical harmonics.
|
|
162
|
+
|
|
163
|
+
psi = R_nl(r) * S_l^m(theta, phi)
|
|
164
|
+
|
|
165
|
+
Returns real values. These correspond to the standard orbital shapes
|
|
166
|
+
(1s, 2px, 2py, 2pz, 3dxy, etc.).
|
|
167
|
+
"""
|
|
168
|
+
R = radial_wavefunction(n, l, r, Z)
|
|
169
|
+
S = real_spherical_harmonic(l, m, theta, phi)
|
|
170
|
+
return R * S
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ===================================================================
|
|
174
|
+
# Probability densities
|
|
175
|
+
# ===================================================================
|
|
176
|
+
|
|
177
|
+
def probability_density(n, l, m, r, theta, phi, Z=1):
|
|
178
|
+
"""Volume probability density |psi|^2 in 1/m^3."""
|
|
179
|
+
psi = wavefunction(n, l, m, r, theta, phi, Z)
|
|
180
|
+
return np.abs(psi)**2
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def probability_density_real(n, l, m, r, theta, phi, Z=1):
|
|
184
|
+
"""Volume probability density using real orbital wave functions."""
|
|
185
|
+
psi = wavefunction_real(n, l, m, r, theta, phi, Z)
|
|
186
|
+
return psi**2
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def radial_probability_density(n: int, l: int, r, Z: int = 1):
|
|
190
|
+
"""Radial probability density P(r) = r^2 * |R_nl(r)|^2.
|
|
191
|
+
|
|
192
|
+
Integral of P(r) dr from 0 to infinity equals 1.
|
|
193
|
+
The peak of P(r) gives the most probable radial distance.
|
|
194
|
+
"""
|
|
195
|
+
r = np.asarray(r, dtype=float)
|
|
196
|
+
R = radial_wavefunction(n, l, r, Z)
|
|
197
|
+
return r**2 * np.abs(R)**2
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ===================================================================
|
|
201
|
+
# Expectation values (analytical, hydrogen-like)
|
|
202
|
+
# ===================================================================
|
|
203
|
+
|
|
204
|
+
def expectation_r(n: int, l: int, Z: int = 1) -> float:
|
|
205
|
+
"""<r> — mean radial distance, in metres.
|
|
206
|
+
|
|
207
|
+
<r> = (a_0 / Z) * (1/2) * [3*n^2 - l*(l+1)]
|
|
208
|
+
"""
|
|
209
|
+
a0 = BOHR_RADIUS
|
|
210
|
+
return (a0 / Z) * 0.5 * (3 * n**2 - l * (l + 1))
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def expectation_r2(n: int, l: int, Z: int = 1) -> float:
|
|
214
|
+
"""<r^2> — mean square radial distance, in m^2.
|
|
215
|
+
|
|
216
|
+
<r^2> = (a_0/Z)^2 * (n^2/2) * [5*n^2 + 1 - 3*l*(l+1)]
|
|
217
|
+
"""
|
|
218
|
+
a0 = BOHR_RADIUS
|
|
219
|
+
return (a0 / Z)**2 * (n**2 / 2.0) * (5 * n**2 + 1 - 3 * l * (l + 1))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def expectation_1_over_r(n: int, l: int, Z: int = 1) -> float:
|
|
223
|
+
"""<1/r> in 1/metres.
|
|
224
|
+
|
|
225
|
+
<1/r> = Z / (n^2 * a_0)
|
|
226
|
+
"""
|
|
227
|
+
return Z / (n**2 * BOHR_RADIUS)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@functools.lru_cache(maxsize=256)
|
|
231
|
+
def most_probable_radius(n: int, l: int, Z: int = 1) -> float:
|
|
232
|
+
"""Most probable radius (peak of radial probability density), in metres.
|
|
233
|
+
|
|
234
|
+
For l = n-1 (circular orbits): r_mp = n^2 * a_0 / Z
|
|
235
|
+
General case found numerically.
|
|
236
|
+
"""
|
|
237
|
+
if l == n - 1:
|
|
238
|
+
return n**2 * BOHR_RADIUS / Z
|
|
239
|
+
|
|
240
|
+
# Numerical search for the peak of r^2 |R_nl|^2
|
|
241
|
+
from scipy.optimize import minimize_scalar
|
|
242
|
+
r_mean = expectation_r(n, l, Z)
|
|
243
|
+
result = minimize_scalar(
|
|
244
|
+
lambda r: -radial_probability_density(n, l, r, Z),
|
|
245
|
+
bounds=(1e-15, 5 * r_mean),
|
|
246
|
+
method="bounded",
|
|
247
|
+
)
|
|
248
|
+
return result.x
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ===================================================================
|
|
252
|
+
# Angular momentum
|
|
253
|
+
# ===================================================================
|
|
254
|
+
|
|
255
|
+
def orbital_angular_momentum(l: int) -> float:
|
|
256
|
+
"""Magnitude of orbital angular momentum L = hbar * sqrt(l*(l+1)), in J*s."""
|
|
257
|
+
return HBAR * math.sqrt(l * (l + 1))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def angular_momentum_z(m: int) -> float:
|
|
261
|
+
"""z-component of orbital angular momentum L_z = m * hbar, in J*s."""
|
|
262
|
+
return m * HBAR
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ===================================================================
|
|
266
|
+
# Energy
|
|
267
|
+
# ===================================================================
|
|
268
|
+
|
|
269
|
+
def energy_level_eV(n: int, Z: int = 1) -> float:
|
|
270
|
+
"""Energy of the n-th level: E_n = -13.6 eV * Z^2 / n^2."""
|
|
271
|
+
return -RYDBERG_ENERGY_EV * Z**2 / n**2
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def energy_level_joules(n: int, Z: int = 1) -> float:
|
|
275
|
+
"""Energy of the n-th level in joules."""
|
|
276
|
+
return energy_level_eV(n, Z) * EV_TO_JOULES
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def transition_wavelength_nm(n_i: int, n_f: int, Z: int = 1) -> float:
|
|
280
|
+
"""Wavelength (nm) of a photon from transition n_i -> n_f."""
|
|
281
|
+
dE = abs(energy_level_eV(n_i, Z) - energy_level_eV(n_f, Z)) * EV_TO_JOULES
|
|
282
|
+
if dE == 0:
|
|
283
|
+
return float("inf")
|
|
284
|
+
lam = (HBAR * 2 * math.pi * SPEED_OF_LIGHT) / dE
|
|
285
|
+
return lam * 1e9
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# ===================================================================
|
|
289
|
+
# Orbital name helper
|
|
290
|
+
# ===================================================================
|
|
291
|
+
|
|
292
|
+
def orbital_label(n: int, l: int, m: int = None) -> str:
|
|
293
|
+
"""Return a human-readable orbital label, e.g. '3d' or '2p_x'."""
|
|
294
|
+
base = f"{n}{SUBSHELL_LETTER.get(l, '?')}"
|
|
295
|
+
if m is not None and (l, m) in ORBITAL_NAMES:
|
|
296
|
+
return f"{n}{ORBITAL_NAMES[(l, m)]}"
|
|
297
|
+
return base
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"""Bonding models: Lewis structures, VSEPR, covalent bonds."""
|
|
2
|
+
from molbuilder.bonding.lewis import LewisStructure, parse_formula
|
|
3
|
+
from molbuilder.bonding.vsepr import VSEPRMolecule
|
|
4
|
+
from molbuilder.bonding.covalent import CovalentBond, MolecularBondAnalysis, single_bond, double_bond, triple_bond
|