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,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"