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.
Files changed (78) hide show
  1. molbuilder/__init__.py +8 -0
  2. molbuilder/__main__.py +6 -0
  3. molbuilder/atomic/__init__.py +4 -0
  4. molbuilder/atomic/bohr.py +235 -0
  5. molbuilder/atomic/quantum_atom.py +334 -0
  6. molbuilder/atomic/quantum_numbers.py +196 -0
  7. molbuilder/atomic/wavefunctions.py +297 -0
  8. molbuilder/bonding/__init__.py +4 -0
  9. molbuilder/bonding/covalent.py +442 -0
  10. molbuilder/bonding/lewis.py +347 -0
  11. molbuilder/bonding/vsepr.py +433 -0
  12. molbuilder/cli/__init__.py +1 -0
  13. molbuilder/cli/demos.py +516 -0
  14. molbuilder/cli/menu.py +127 -0
  15. molbuilder/cli/wizard.py +831 -0
  16. molbuilder/core/__init__.py +6 -0
  17. molbuilder/core/bond_data.py +170 -0
  18. molbuilder/core/constants.py +51 -0
  19. molbuilder/core/element_properties.py +183 -0
  20. molbuilder/core/elements.py +181 -0
  21. molbuilder/core/geometry.py +232 -0
  22. molbuilder/gui/__init__.py +2 -0
  23. molbuilder/gui/app.py +286 -0
  24. molbuilder/gui/canvas3d.py +115 -0
  25. molbuilder/gui/dialogs.py +117 -0
  26. molbuilder/gui/event_handler.py +118 -0
  27. molbuilder/gui/sidebar.py +105 -0
  28. molbuilder/gui/toolbar.py +71 -0
  29. molbuilder/io/__init__.py +1 -0
  30. molbuilder/io/json_io.py +146 -0
  31. molbuilder/io/mol_sdf.py +169 -0
  32. molbuilder/io/pdb.py +184 -0
  33. molbuilder/io/smiles_io.py +47 -0
  34. molbuilder/io/xyz.py +103 -0
  35. molbuilder/molecule/__init__.py +2 -0
  36. molbuilder/molecule/amino_acids.py +919 -0
  37. molbuilder/molecule/builders.py +257 -0
  38. molbuilder/molecule/conformations.py +70 -0
  39. molbuilder/molecule/functional_groups.py +484 -0
  40. molbuilder/molecule/graph.py +712 -0
  41. molbuilder/molecule/peptides.py +13 -0
  42. molbuilder/molecule/stereochemistry.py +6 -0
  43. molbuilder/process/__init__.py +3 -0
  44. molbuilder/process/conditions.py +260 -0
  45. molbuilder/process/costing.py +316 -0
  46. molbuilder/process/purification.py +285 -0
  47. molbuilder/process/reactor.py +297 -0
  48. molbuilder/process/safety.py +476 -0
  49. molbuilder/process/scale_up.py +427 -0
  50. molbuilder/process/solvent_systems.py +204 -0
  51. molbuilder/reactions/__init__.py +3 -0
  52. molbuilder/reactions/functional_group_detect.py +728 -0
  53. molbuilder/reactions/knowledge_base.py +1716 -0
  54. molbuilder/reactions/reaction_types.py +102 -0
  55. molbuilder/reactions/reagent_data.py +1248 -0
  56. molbuilder/reactions/retrosynthesis.py +1430 -0
  57. molbuilder/reactions/synthesis_route.py +377 -0
  58. molbuilder/reports/__init__.py +158 -0
  59. molbuilder/reports/cost_report.py +206 -0
  60. molbuilder/reports/molecule_report.py +279 -0
  61. molbuilder/reports/safety_report.py +296 -0
  62. molbuilder/reports/synthesis_report.py +283 -0
  63. molbuilder/reports/text_formatter.py +170 -0
  64. molbuilder/smiles/__init__.py +4 -0
  65. molbuilder/smiles/parser.py +487 -0
  66. molbuilder/smiles/tokenizer.py +291 -0
  67. molbuilder/smiles/writer.py +375 -0
  68. molbuilder/visualization/__init__.py +1 -0
  69. molbuilder/visualization/bohr_viz.py +166 -0
  70. molbuilder/visualization/molecule_viz.py +368 -0
  71. molbuilder/visualization/quantum_viz.py +434 -0
  72. molbuilder/visualization/theme.py +12 -0
  73. molbuilder-1.0.0.dist-info/METADATA +360 -0
  74. molbuilder-1.0.0.dist-info/RECORD +78 -0
  75. molbuilder-1.0.0.dist-info/WHEEL +5 -0
  76. molbuilder-1.0.0.dist-info/entry_points.txt +2 -0
  77. molbuilder-1.0.0.dist-info/licenses/LICENSE +21 -0
  78. 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