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,434 @@
|
|
|
1
|
+
"""Quantum Mechanical Atom Visualization
|
|
2
|
+
|
|
3
|
+
Provides plotting functions for:
|
|
4
|
+
- 3D orbital probability clouds (electron density scatter)
|
|
5
|
+
- Radial wave functions R_nl(r)
|
|
6
|
+
- Radial probability distributions r^2 |R_nl|^2
|
|
7
|
+
- Angular probability distributions |Y_l^m|^2
|
|
8
|
+
- Orbital energy level diagrams
|
|
9
|
+
- Electron configuration box diagrams (with arrows)
|
|
10
|
+
|
|
11
|
+
Migrated from legacy/quantum_visualization.py.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import math
|
|
15
|
+
import numpy as np
|
|
16
|
+
import matplotlib.pyplot as plt
|
|
17
|
+
import matplotlib.gridspec as gridspec
|
|
18
|
+
from matplotlib.patches import FancyArrowPatch
|
|
19
|
+
from mpl_toolkits.mplot3d import Axes3D
|
|
20
|
+
|
|
21
|
+
from molbuilder.core.constants import BOHR_RADIUS_M as BOHR_RADIUS
|
|
22
|
+
from molbuilder.atomic.quantum_numbers import SUBSHELL_LETTER, ORBITAL_NAMES
|
|
23
|
+
from molbuilder.atomic.wavefunctions import (
|
|
24
|
+
radial_wavefunction,
|
|
25
|
+
real_spherical_harmonic,
|
|
26
|
+
wavefunction_real,
|
|
27
|
+
radial_probability_density,
|
|
28
|
+
expectation_r,
|
|
29
|
+
orbital_label,
|
|
30
|
+
)
|
|
31
|
+
from molbuilder.core.geometry import cartesian_to_spherical, spherical_to_cartesian
|
|
32
|
+
from molbuilder.visualization.theme import (
|
|
33
|
+
BG_COLOR, TEXT_COLOR, GRID_COLOR,
|
|
34
|
+
POSITIVE_COLOR, NEGATIVE_COLOR, NEUTRAL_COLOR, ENERGY_COLOR,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ===================================================================
|
|
39
|
+
# 3D Orbital probability cloud
|
|
40
|
+
# ===================================================================
|
|
41
|
+
|
|
42
|
+
def plot_orbital_3d(n: int, l: int, m: int, Z: int = 1,
|
|
43
|
+
num_points: int = 30000, threshold: float = 0.3,
|
|
44
|
+
figsize: tuple = (8, 8)):
|
|
45
|
+
"""Render a 3D probability cloud for orbital (n, l, m).
|
|
46
|
+
|
|
47
|
+
Uses Monte Carlo rejection sampling to generate points distributed
|
|
48
|
+
according to |psi|^2, then plots as a 3D scatter with positive/negative
|
|
49
|
+
lobes coloured differently.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
n, l, m : quantum numbers
|
|
54
|
+
Z : nuclear charge
|
|
55
|
+
num_points: number of candidate random points
|
|
56
|
+
threshold : fraction of max density below which points are culled
|
|
57
|
+
figsize : figure size
|
|
58
|
+
"""
|
|
59
|
+
# Determine radial extent: ~5x the expectation value of r
|
|
60
|
+
r_extent = 5.0 * expectation_r(n, l, Z)
|
|
61
|
+
r_extent_a0 = r_extent / BOHR_RADIUS # in units of a_0 for display
|
|
62
|
+
|
|
63
|
+
# Generate random points in a cube, then convert to spherical
|
|
64
|
+
side = r_extent
|
|
65
|
+
rng = np.random.default_rng(42)
|
|
66
|
+
x = rng.uniform(-side, side, num_points)
|
|
67
|
+
y = rng.uniform(-side, side, num_points)
|
|
68
|
+
z_coord = rng.uniform(-side, side, num_points)
|
|
69
|
+
|
|
70
|
+
r, theta, phi = cartesian_to_spherical(x, y, z_coord)
|
|
71
|
+
|
|
72
|
+
# Compute real wave function and density
|
|
73
|
+
psi = wavefunction_real(n, l, m, r, theta, phi, Z)
|
|
74
|
+
density = psi**2
|
|
75
|
+
|
|
76
|
+
# Rejection sampling: keep points with probability proportional to density
|
|
77
|
+
max_density = np.max(density)
|
|
78
|
+
if max_density == 0:
|
|
79
|
+
print("Wave function is zero everywhere in sampled region.")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
accept_prob = density / max_density
|
|
83
|
+
randoms = rng.uniform(0, 1, num_points)
|
|
84
|
+
mask = randoms < accept_prob
|
|
85
|
+
|
|
86
|
+
# Also cull very low-density points for visual clarity
|
|
87
|
+
mask &= density > threshold * max_density
|
|
88
|
+
|
|
89
|
+
x_keep = x[mask] / BOHR_RADIUS # convert to a_0 units for display
|
|
90
|
+
y_keep = y[mask] / BOHR_RADIUS
|
|
91
|
+
z_keep = z_coord[mask] / BOHR_RADIUS
|
|
92
|
+
psi_keep = psi[mask]
|
|
93
|
+
|
|
94
|
+
# Color by sign of wave function
|
|
95
|
+
colors = np.where(psi_keep >= 0, POSITIVE_COLOR, NEGATIVE_COLOR)
|
|
96
|
+
|
|
97
|
+
# Plot
|
|
98
|
+
fig = plt.figure(figsize=figsize, facecolor=BG_COLOR)
|
|
99
|
+
ax = fig.add_subplot(111, projection="3d", facecolor=BG_COLOR)
|
|
100
|
+
|
|
101
|
+
ax.scatter(x_keep, y_keep, z_keep, c=colors, s=1.0, alpha=0.5,
|
|
102
|
+
depthshade=True)
|
|
103
|
+
|
|
104
|
+
lim = r_extent_a0 * 0.8
|
|
105
|
+
ax.set_xlim(-lim, lim)
|
|
106
|
+
ax.set_ylim(-lim, lim)
|
|
107
|
+
ax.set_zlim(-lim, lim)
|
|
108
|
+
|
|
109
|
+
label = orbital_label(n, l, m)
|
|
110
|
+
ax.set_title(f"Orbital {label} (Z={Z})", color=TEXT_COLOR, fontsize=14)
|
|
111
|
+
ax.set_xlabel("x / a0", color=TEXT_COLOR, fontsize=9)
|
|
112
|
+
ax.set_ylabel("y / a0", color=TEXT_COLOR, fontsize=9)
|
|
113
|
+
ax.set_zlabel("z / a0", color=TEXT_COLOR, fontsize=9)
|
|
114
|
+
ax.tick_params(colors=TEXT_COLOR, labelsize=7)
|
|
115
|
+
|
|
116
|
+
# Style pane colors
|
|
117
|
+
for pane in [ax.xaxis.pane, ax.yaxis.pane, ax.zaxis.pane]:
|
|
118
|
+
pane.set_facecolor(BG_COLOR)
|
|
119
|
+
pane.set_edgecolor(GRID_COLOR)
|
|
120
|
+
|
|
121
|
+
plt.tight_layout()
|
|
122
|
+
plt.show()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ===================================================================
|
|
126
|
+
# Radial wave function plot
|
|
127
|
+
# ===================================================================
|
|
128
|
+
|
|
129
|
+
def plot_radial_wavefunction(n_l_pairs: list[tuple[int, int]],
|
|
130
|
+
Z: int = 1, r_max_a0: float = None,
|
|
131
|
+
figsize: tuple = (9, 5)):
|
|
132
|
+
"""Plot R_nl(r) for one or more (n, l) pairs.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
n_l_pairs : list of (n, l) tuples
|
|
137
|
+
Z : nuclear charge
|
|
138
|
+
r_max_a0 : maximum r in units of a_0 (auto-scaled if None)
|
|
139
|
+
"""
|
|
140
|
+
if r_max_a0 is None:
|
|
141
|
+
r_max_a0 = max(
|
|
142
|
+
5 * expectation_r(n, l, Z) / BOHR_RADIUS for n, l in n_l_pairs
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
r_a0 = np.linspace(1e-6, r_max_a0, 2000)
|
|
146
|
+
r_m = r_a0 * BOHR_RADIUS
|
|
147
|
+
|
|
148
|
+
fig, ax = plt.subplots(figsize=figsize, facecolor=BG_COLOR)
|
|
149
|
+
ax.set_facecolor(BG_COLOR)
|
|
150
|
+
|
|
151
|
+
for n, l in n_l_pairs:
|
|
152
|
+
R = radial_wavefunction(n, l, r_m, Z)
|
|
153
|
+
# Scale to a_0 units for display: R has units of m^{-3/2}
|
|
154
|
+
R_scaled = R * BOHR_RADIUS**1.5
|
|
155
|
+
label_str = f"R({n},{SUBSHELL_LETTER.get(l,'?')})"
|
|
156
|
+
ax.plot(r_a0, R_scaled, linewidth=1.5, label=label_str)
|
|
157
|
+
|
|
158
|
+
ax.axhline(0, color=GRID_COLOR, linewidth=0.5)
|
|
159
|
+
ax.set_xlabel("r / a0", color=TEXT_COLOR, fontsize=11)
|
|
160
|
+
ax.set_ylabel("R(r) * a0^(3/2)", color=TEXT_COLOR, fontsize=11)
|
|
161
|
+
ax.set_title(f"Radial Wave Functions (Z={Z})", color=TEXT_COLOR, fontsize=13)
|
|
162
|
+
ax.legend(facecolor="#111122", edgecolor=GRID_COLOR, labelcolor=TEXT_COLOR)
|
|
163
|
+
ax.tick_params(colors=TEXT_COLOR)
|
|
164
|
+
for spine in ax.spines.values():
|
|
165
|
+
spine.set_color(GRID_COLOR)
|
|
166
|
+
ax.grid(True, color=GRID_COLOR, alpha=0.3)
|
|
167
|
+
|
|
168
|
+
plt.tight_layout()
|
|
169
|
+
plt.show()
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ===================================================================
|
|
173
|
+
# Radial probability distribution plot
|
|
174
|
+
# ===================================================================
|
|
175
|
+
|
|
176
|
+
def plot_radial_probability(n_l_pairs: list[tuple[int, int]],
|
|
177
|
+
Z: int = 1, r_max_a0: float = None,
|
|
178
|
+
figsize: tuple = (9, 5)):
|
|
179
|
+
"""Plot r^2 |R_nl(r)|^2 for one or more (n, l) pairs."""
|
|
180
|
+
if r_max_a0 is None:
|
|
181
|
+
r_max_a0 = max(
|
|
182
|
+
5 * expectation_r(n, l, Z) / BOHR_RADIUS for n, l in n_l_pairs
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
r_a0 = np.linspace(1e-6, r_max_a0, 2000)
|
|
186
|
+
r_m = r_a0 * BOHR_RADIUS
|
|
187
|
+
|
|
188
|
+
fig, ax = plt.subplots(figsize=figsize, facecolor=BG_COLOR)
|
|
189
|
+
ax.set_facecolor(BG_COLOR)
|
|
190
|
+
|
|
191
|
+
for n, l in n_l_pairs:
|
|
192
|
+
P = radial_probability_density(n, l, r_m, Z)
|
|
193
|
+
# Scale: P has units of m^{-1}, multiply by a_0 to get dimensionless
|
|
194
|
+
P_scaled = P * BOHR_RADIUS
|
|
195
|
+
label_str = f"P({n},{SUBSHELL_LETTER.get(l,'?')})"
|
|
196
|
+
ax.plot(r_a0, P_scaled, linewidth=1.5, label=label_str)
|
|
197
|
+
ax.fill_between(r_a0, P_scaled, alpha=0.15)
|
|
198
|
+
|
|
199
|
+
ax.set_xlabel("r / a0", color=TEXT_COLOR, fontsize=11)
|
|
200
|
+
ax.set_ylabel("P(r) * a0", color=TEXT_COLOR, fontsize=11)
|
|
201
|
+
ax.set_title(
|
|
202
|
+
f"Radial Probability Distribution (Z={Z})",
|
|
203
|
+
color=TEXT_COLOR, fontsize=13,
|
|
204
|
+
)
|
|
205
|
+
ax.legend(facecolor="#111122", edgecolor=GRID_COLOR, labelcolor=TEXT_COLOR)
|
|
206
|
+
ax.tick_params(colors=TEXT_COLOR)
|
|
207
|
+
for spine in ax.spines.values():
|
|
208
|
+
spine.set_color(GRID_COLOR)
|
|
209
|
+
ax.grid(True, color=GRID_COLOR, alpha=0.3)
|
|
210
|
+
|
|
211
|
+
plt.tight_layout()
|
|
212
|
+
plt.show()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ===================================================================
|
|
216
|
+
# Angular distribution cross-section
|
|
217
|
+
# ===================================================================
|
|
218
|
+
|
|
219
|
+
def plot_angular_distribution(l: int, m: int, figsize: tuple = (6, 6)):
|
|
220
|
+
"""Polar plot of |Y_l^m(theta, phi=0)|^2 in the xz-plane."""
|
|
221
|
+
theta = np.linspace(0, 2 * np.pi, 500)
|
|
222
|
+
phi_fixed = np.zeros_like(theta)
|
|
223
|
+
|
|
224
|
+
Y = real_spherical_harmonic(l, m, theta, phi_fixed)
|
|
225
|
+
mag = np.abs(Y)
|
|
226
|
+
|
|
227
|
+
fig, ax = plt.subplots(subplot_kw={"projection": "polar"},
|
|
228
|
+
figsize=figsize, facecolor=BG_COLOR)
|
|
229
|
+
ax.set_facecolor(BG_COLOR)
|
|
230
|
+
|
|
231
|
+
# Color by sign
|
|
232
|
+
pos_mask = Y >= 0
|
|
233
|
+
neg_mask = ~pos_mask
|
|
234
|
+
|
|
235
|
+
ax.plot(theta[pos_mask], mag[pos_mask], ".", color=POSITIVE_COLOR,
|
|
236
|
+
markersize=1.5)
|
|
237
|
+
ax.plot(theta[neg_mask], mag[neg_mask], ".", color=NEGATIVE_COLOR,
|
|
238
|
+
markersize=1.5)
|
|
239
|
+
ax.fill_between(theta, mag, alpha=0.15, color=NEUTRAL_COLOR)
|
|
240
|
+
|
|
241
|
+
name = ORBITAL_NAMES.get((l, m), f"l={l},m={m}")
|
|
242
|
+
ax.set_title(f"|Y({name})| (xz plane)", color=TEXT_COLOR,
|
|
243
|
+
fontsize=12, pad=15)
|
|
244
|
+
ax.tick_params(colors=TEXT_COLOR, labelsize=7)
|
|
245
|
+
ax.grid(True, color=GRID_COLOR, alpha=0.3)
|
|
246
|
+
|
|
247
|
+
plt.tight_layout()
|
|
248
|
+
plt.show()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ===================================================================
|
|
252
|
+
# Electron configuration box diagram
|
|
253
|
+
# ===================================================================
|
|
254
|
+
|
|
255
|
+
def plot_electron_configuration(atom, figsize: tuple = None):
|
|
256
|
+
"""Draw an orbital box (arrow) diagram for an atom's electron config.
|
|
257
|
+
|
|
258
|
+
Each orbital is a box. Spin-up electrons are shown as up-arrows,
|
|
259
|
+
spin-down as down-arrows.
|
|
260
|
+
"""
|
|
261
|
+
from molbuilder.atomic.quantum_atom import QuantumAtom
|
|
262
|
+
|
|
263
|
+
subshells = atom.subshells
|
|
264
|
+
if not subshells:
|
|
265
|
+
print("No electrons to display.")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
# Layout: one row per subshell, boxes for each m_l
|
|
269
|
+
total_orbitals = sum(2 * ss.l + 1 for ss in subshells)
|
|
270
|
+
if figsize is None:
|
|
271
|
+
max_boxes = max(2 * ss.l + 1 for ss in subshells)
|
|
272
|
+
figsize = (max(6, max_boxes * 1.2 + 3), len(subshells) * 0.9 + 1)
|
|
273
|
+
|
|
274
|
+
fig, ax = plt.subplots(figsize=figsize, facecolor=BG_COLOR)
|
|
275
|
+
ax.set_facecolor(BG_COLOR)
|
|
276
|
+
ax.set_xlim(-1, max(2 * ss.l + 1 for ss in subshells) + 2)
|
|
277
|
+
ax.set_ylim(-0.5, len(subshells) * 1.0 + 0.5)
|
|
278
|
+
ax.axis("off")
|
|
279
|
+
|
|
280
|
+
charge_label = ""
|
|
281
|
+
if atom.charge > 0:
|
|
282
|
+
charge_label = f" (+{atom.charge})"
|
|
283
|
+
elif atom.charge < 0:
|
|
284
|
+
charge_label = f" ({atom.charge})"
|
|
285
|
+
ax.set_title(
|
|
286
|
+
f"Electron Configuration -- {atom.name} ({atom.symbol}{charge_label})",
|
|
287
|
+
color=TEXT_COLOR, fontsize=13, pad=10,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
box_w, box_h = 0.8, 0.6
|
|
291
|
+
|
|
292
|
+
for row_idx, ss in enumerate(reversed(subshells)):
|
|
293
|
+
y = row_idx * 1.0 + 0.3
|
|
294
|
+
num_orbitals = 2 * ss.l + 1
|
|
295
|
+
ml_values = list(range(-ss.l, ss.l + 1))
|
|
296
|
+
|
|
297
|
+
# Determine which orbitals have spin-up, spin-down
|
|
298
|
+
states = ss.quantum_states()
|
|
299
|
+
occupied = {}
|
|
300
|
+
for st in states:
|
|
301
|
+
occupied.setdefault(st.ml, []).append(st.ms)
|
|
302
|
+
|
|
303
|
+
# Label
|
|
304
|
+
ax.text(-0.8, y, ss.label, fontsize=11, color=TEXT_COLOR,
|
|
305
|
+
ha="right", va="center", fontweight="bold")
|
|
306
|
+
|
|
307
|
+
for j, ml in enumerate(ml_values):
|
|
308
|
+
bx = j * 1.0 + 0.2
|
|
309
|
+
|
|
310
|
+
# Draw box
|
|
311
|
+
rect = plt.Rectangle((bx, y - box_h / 2), box_w, box_h,
|
|
312
|
+
fill=False, edgecolor="#4466aa",
|
|
313
|
+
linewidth=1.2)
|
|
314
|
+
ax.add_patch(rect)
|
|
315
|
+
|
|
316
|
+
# Draw arrows for electrons
|
|
317
|
+
spins = occupied.get(ml, [])
|
|
318
|
+
for k, ms in enumerate(sorted(spins, reverse=True)):
|
|
319
|
+
arrow_x = bx + box_w * (0.3 + 0.4 * k)
|
|
320
|
+
if ms > 0:
|
|
321
|
+
ax.annotate("", xy=(arrow_x, y + 0.2),
|
|
322
|
+
xytext=(arrow_x, y - 0.15),
|
|
323
|
+
arrowprops=dict(arrowstyle="->",
|
|
324
|
+
color=POSITIVE_COLOR, lw=1.8))
|
|
325
|
+
else:
|
|
326
|
+
ax.annotate("", xy=(arrow_x, y - 0.15),
|
|
327
|
+
xytext=(arrow_x, y + 0.2),
|
|
328
|
+
arrowprops=dict(arrowstyle="->",
|
|
329
|
+
color=NEGATIVE_COLOR, lw=1.8))
|
|
330
|
+
|
|
331
|
+
plt.tight_layout()
|
|
332
|
+
plt.show()
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
# ===================================================================
|
|
336
|
+
# Energy level diagram
|
|
337
|
+
# ===================================================================
|
|
338
|
+
|
|
339
|
+
def plot_energy_levels(atom, figsize: tuple = (7, 8)):
|
|
340
|
+
"""Plot an energy level diagram for the atom's subshells."""
|
|
341
|
+
from molbuilder.atomic.quantum_atom import QuantumAtom
|
|
342
|
+
|
|
343
|
+
subshells = atom.subshells
|
|
344
|
+
if not subshells:
|
|
345
|
+
print("No subshells to display.")
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
fig, ax = plt.subplots(figsize=figsize, facecolor=BG_COLOR)
|
|
349
|
+
ax.set_facecolor(BG_COLOR)
|
|
350
|
+
|
|
351
|
+
# Compute energies
|
|
352
|
+
energies = []
|
|
353
|
+
labels = []
|
|
354
|
+
counts = []
|
|
355
|
+
for ss in subshells:
|
|
356
|
+
e = atom.orbital_energy_eV(ss.n, ss.l)
|
|
357
|
+
energies.append(e)
|
|
358
|
+
labels.append(ss.label)
|
|
359
|
+
counts.append(ss.electron_count)
|
|
360
|
+
|
|
361
|
+
# Group by n for horizontal positioning
|
|
362
|
+
n_values = sorted(set(ss.n for ss in subshells))
|
|
363
|
+
n_to_x = {n: i * 2.0 for i, n in enumerate(n_values)}
|
|
364
|
+
|
|
365
|
+
for i, ss in enumerate(subshells):
|
|
366
|
+
e = energies[i]
|
|
367
|
+
x_center = n_to_x[ss.n] + ss.l * 0.5
|
|
368
|
+
x_left = x_center - 0.3
|
|
369
|
+
x_right = x_center + 0.3
|
|
370
|
+
|
|
371
|
+
ax.plot([x_left, x_right], [e, e], color=ENERGY_COLOR,
|
|
372
|
+
linewidth=2.5, solid_capstyle="round")
|
|
373
|
+
ax.text(x_center, e + 0.3, f"{labels[i]}",
|
|
374
|
+
ha="center", va="bottom", fontsize=9, color=TEXT_COLOR)
|
|
375
|
+
ax.text(x_center, e - 0.5, f"({counts[i]}e-)",
|
|
376
|
+
ha="center", va="top", fontsize=7, color="#7799bb")
|
|
377
|
+
|
|
378
|
+
ax.set_ylabel("Energy (eV)", color=TEXT_COLOR, fontsize=12)
|
|
379
|
+
ax.set_title(
|
|
380
|
+
f"Orbital Energy Levels -- {atom.name} ({atom.symbol})",
|
|
381
|
+
color=TEXT_COLOR, fontsize=13, pad=12,
|
|
382
|
+
)
|
|
383
|
+
ax.tick_params(axis="y", colors=TEXT_COLOR)
|
|
384
|
+
ax.tick_params(axis="x", bottom=False, labelbottom=False)
|
|
385
|
+
for spine in ax.spines.values():
|
|
386
|
+
spine.set_color(GRID_COLOR)
|
|
387
|
+
ax.grid(True, axis="y", color=GRID_COLOR, alpha=0.3)
|
|
388
|
+
|
|
389
|
+
plt.tight_layout()
|
|
390
|
+
plt.show()
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# ===================================================================
|
|
394
|
+
# Combined atom overview
|
|
395
|
+
# ===================================================================
|
|
396
|
+
|
|
397
|
+
def visualize_atom(atom, orbital_nlm: tuple = None):
|
|
398
|
+
"""Show a multi-panel overview of an atom.
|
|
399
|
+
|
|
400
|
+
Panels:
|
|
401
|
+
1. Radial probability distributions for all occupied subshells
|
|
402
|
+
2. Energy level diagram
|
|
403
|
+
3. Electron configuration box diagram
|
|
404
|
+
4. 3D orbital for the specified (n, l, m) or outermost subshell
|
|
405
|
+
|
|
406
|
+
Parameters
|
|
407
|
+
----------
|
|
408
|
+
atom : QuantumAtom instance
|
|
409
|
+
orbital_nlm : (n, l, m) to visualize in 3D; defaults to outermost subshell m=0
|
|
410
|
+
"""
|
|
411
|
+
from molbuilder.atomic.quantum_atom import QuantumAtom
|
|
412
|
+
|
|
413
|
+
print(atom.summary())
|
|
414
|
+
|
|
415
|
+
# Determine orbital to show in 3D
|
|
416
|
+
if orbital_nlm is None:
|
|
417
|
+
ss = atom.subshells[-1]
|
|
418
|
+
orbital_nlm = (ss.n, ss.l, 0)
|
|
419
|
+
|
|
420
|
+
n, l, m = orbital_nlm
|
|
421
|
+
|
|
422
|
+
# 1) Radial probability
|
|
423
|
+
n_l_pairs = list(set((ss.n, ss.l) for ss in atom.subshells))
|
|
424
|
+
n_l_pairs.sort()
|
|
425
|
+
plot_radial_probability(n_l_pairs, Z=atom.atomic_number)
|
|
426
|
+
|
|
427
|
+
# 2) Energy levels
|
|
428
|
+
plot_energy_levels(atom)
|
|
429
|
+
|
|
430
|
+
# 3) Configuration diagram
|
|
431
|
+
plot_electron_configuration(atom)
|
|
432
|
+
|
|
433
|
+
# 4) 3D orbital
|
|
434
|
+
plot_orbital_3d(n, l, m, Z=atom.atomic_number)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Shared visualization theme: colors, palettes, style constants."""
|
|
2
|
+
|
|
3
|
+
BG_COLOR = "#0a0a1a"
|
|
4
|
+
TEXT_COLOR = "#ccddee"
|
|
5
|
+
GRID_COLOR = "#1a2a3a"
|
|
6
|
+
LONE_PAIR_COLOR = "#ffaa33"
|
|
7
|
+
BOND_COLOR = "#aabbcc"
|
|
8
|
+
ANGLE_ARC_COLOR = "#44ccff"
|
|
9
|
+
POSITIVE_COLOR = "#3399ff"
|
|
10
|
+
NEGATIVE_COLOR = "#ff5533"
|
|
11
|
+
NEUTRAL_COLOR = "#44ccff"
|
|
12
|
+
ENERGY_COLOR = "#ffaa33"
|