molbuilder 1.0.0__py3-none-any.whl → 1.1.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 +1 -1
- molbuilder/cli/demos.py +73 -1
- molbuilder/cli/menu.py +2 -0
- molbuilder/dynamics/__init__.py +49 -0
- molbuilder/dynamics/forcefield.py +607 -0
- molbuilder/dynamics/integrator.py +275 -0
- molbuilder/dynamics/mechanism_choreography.py +216 -0
- molbuilder/dynamics/mechanisms.py +552 -0
- molbuilder/dynamics/simulation.py +209 -0
- molbuilder/dynamics/trajectory.py +215 -0
- molbuilder/gui/app.py +114 -0
- molbuilder/visualization/__init__.py +2 -1
- molbuilder/visualization/electron_density_viz.py +246 -0
- molbuilder/visualization/interaction_controls.py +211 -0
- molbuilder/visualization/interaction_viz.py +615 -0
- molbuilder/visualization/theme.py +7 -0
- {molbuilder-1.0.0.dist-info → molbuilder-1.1.0.dist-info}/METADATA +1 -1
- {molbuilder-1.0.0.dist-info → molbuilder-1.1.0.dist-info}/RECORD +22 -12
- {molbuilder-1.0.0.dist-info → molbuilder-1.1.0.dist-info}/WHEEL +0 -0
- {molbuilder-1.0.0.dist-info → molbuilder-1.1.0.dist-info}/entry_points.txt +0 -0
- {molbuilder-1.0.0.dist-info → molbuilder-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {molbuilder-1.0.0.dist-info → molbuilder-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
"""Main animation renderer for extreme slow-motion atomic interactions.
|
|
2
|
+
|
|
3
|
+
Provides ``InteractionVisualizer`` which builds matplotlib FuncAnimation
|
|
4
|
+
pipelines that render MD trajectories at sub-femtosecond resolution,
|
|
5
|
+
with optional overlays for electron density, curly arrows, energy bars,
|
|
6
|
+
and time labels.
|
|
7
|
+
|
|
8
|
+
Convenience functions:
|
|
9
|
+
- ``visualize_md_trajectory(mol, n_steps, ...)``
|
|
10
|
+
- ``visualize_reaction(mol, mechanism, ...)``
|
|
11
|
+
- ``visualize_bond_formation(mol, atom_i, atom_j, ...)``
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import math
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
import matplotlib
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
from matplotlib.animation import FuncAnimation
|
|
26
|
+
from matplotlib.patches import FancyArrowPatch
|
|
27
|
+
HAS_MATPLOTLIB = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
HAS_MATPLOTLIB = False
|
|
30
|
+
|
|
31
|
+
from molbuilder.core.element_properties import cpk_color, covalent_radius_pm
|
|
32
|
+
from molbuilder.core.elements import SYMBOL_TO_Z
|
|
33
|
+
from molbuilder.visualization.theme import (
|
|
34
|
+
BG_COLOR, TEXT_COLOR, BOND_COLOR,
|
|
35
|
+
FORMING_BOND_COLOR, BREAKING_BOND_COLOR,
|
|
36
|
+
ELECTRON_CLOUD_COLOR, ARROW_COLOR, TRANSITION_STATE_COLOR,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
if TYPE_CHECKING:
|
|
40
|
+
from molbuilder.molecule.graph import Molecule
|
|
41
|
+
from molbuilder.dynamics.trajectory import Trajectory
|
|
42
|
+
from molbuilder.dynamics.mechanisms import ReactionMechanism, ElectronFlow
|
|
43
|
+
from molbuilder.dynamics.mechanism_choreography import MechanismChoreographer
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ===================================================================
|
|
47
|
+
# Configuration
|
|
48
|
+
# ===================================================================
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class PlaybackConfig:
|
|
52
|
+
"""Configuration for the interaction visualizer.
|
|
53
|
+
|
|
54
|
+
Attributes
|
|
55
|
+
----------
|
|
56
|
+
slowmo_factor : float
|
|
57
|
+
Slowdown factor. Default 1e15 means 1 fs of simulation time
|
|
58
|
+
maps to 1 second of animation (extreme slow motion).
|
|
59
|
+
fps : int
|
|
60
|
+
Animation frame rate.
|
|
61
|
+
show_electron_density : bool
|
|
62
|
+
Render electron density clouds during bond events.
|
|
63
|
+
show_electron_flows : bool
|
|
64
|
+
Render curly/fishhook arrows for electron flow.
|
|
65
|
+
show_energy_bar : bool
|
|
66
|
+
Show an energy bar overlay.
|
|
67
|
+
show_time_label : bool
|
|
68
|
+
Show a time label with SI prefix.
|
|
69
|
+
show_bond_orders : bool
|
|
70
|
+
Show fractional bond orders as annotations.
|
|
71
|
+
export_path : str or None
|
|
72
|
+
If set, export animation to this file path (.mp4 or .gif).
|
|
73
|
+
camera_follow : bool
|
|
74
|
+
Auto-track the reaction center.
|
|
75
|
+
dpi : int
|
|
76
|
+
Resolution for export.
|
|
77
|
+
figsize : tuple[float, float]
|
|
78
|
+
Figure size in inches.
|
|
79
|
+
"""
|
|
80
|
+
slowmo_factor: float = 1e15
|
|
81
|
+
fps: int = 30
|
|
82
|
+
show_electron_density: bool = True
|
|
83
|
+
show_electron_flows: bool = True
|
|
84
|
+
show_energy_bar: bool = True
|
|
85
|
+
show_time_label: bool = True
|
|
86
|
+
show_bond_orders: bool = False
|
|
87
|
+
export_path: str | None = None
|
|
88
|
+
camera_follow: bool = False
|
|
89
|
+
dpi: int = 150
|
|
90
|
+
figsize: tuple[float, float] = (10, 8)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _time_label(t_fs: float) -> str:
|
|
94
|
+
"""Format time with appropriate SI prefix."""
|
|
95
|
+
if abs(t_fs) < 1e-3:
|
|
96
|
+
return f"{t_fs * 1e3:.2f} as"
|
|
97
|
+
if abs(t_fs) < 1.0:
|
|
98
|
+
return f"{t_fs:.3f} fs"
|
|
99
|
+
if abs(t_fs) < 1000.0:
|
|
100
|
+
return f"{t_fs:.1f} fs"
|
|
101
|
+
return f"{t_fs / 1000.0:.2f} ps"
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _bond_color_from_order(order: float) -> str:
|
|
105
|
+
"""Choose bond color based on fractional bond order."""
|
|
106
|
+
if order < 0.3:
|
|
107
|
+
return BREAKING_BOND_COLOR
|
|
108
|
+
if order > 0.7 and order < 1.3:
|
|
109
|
+
return BOND_COLOR
|
|
110
|
+
if order >= 0.3 and order < 0.7:
|
|
111
|
+
return TRANSITION_STATE_COLOR
|
|
112
|
+
return FORMING_BOND_COLOR
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ===================================================================
|
|
116
|
+
# InteractionVisualizer
|
|
117
|
+
# ===================================================================
|
|
118
|
+
|
|
119
|
+
class InteractionVisualizer:
|
|
120
|
+
"""Renders MD trajectories as slow-motion matplotlib animations.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
trajectory : Trajectory
|
|
125
|
+
The MD trajectory to visualize.
|
|
126
|
+
molecule : Molecule
|
|
127
|
+
Original molecule (for bond connectivity and atom symbols).
|
|
128
|
+
mechanism : ReactionMechanism or None
|
|
129
|
+
Optional mechanism for electron flow overlays.
|
|
130
|
+
choreographer : MechanismChoreographer or None
|
|
131
|
+
Optional choreographer for mechanism annotations.
|
|
132
|
+
config : PlaybackConfig or None
|
|
133
|
+
Playback configuration.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
def __init__(self, trajectory, molecule,
|
|
137
|
+
mechanism=None, choreographer=None,
|
|
138
|
+
config: PlaybackConfig | None = None):
|
|
139
|
+
if not HAS_MATPLOTLIB:
|
|
140
|
+
raise ImportError("matplotlib is required for visualization")
|
|
141
|
+
|
|
142
|
+
self.trajectory = trajectory
|
|
143
|
+
self.molecule = molecule
|
|
144
|
+
self.mechanism = mechanism
|
|
145
|
+
self.choreographer = choreographer
|
|
146
|
+
self.config = config or PlaybackConfig()
|
|
147
|
+
|
|
148
|
+
# Precompute animation frames
|
|
149
|
+
duration_fs = trajectory.duration
|
|
150
|
+
duration_anim_s = duration_fs * self.config.slowmo_factor * 1e-15
|
|
151
|
+
# Minimum 2 seconds of animation
|
|
152
|
+
duration_anim_s = max(duration_anim_s, 2.0)
|
|
153
|
+
self.n_frames = max(int(duration_anim_s * self.config.fps), 2)
|
|
154
|
+
|
|
155
|
+
# Time array for each animation frame
|
|
156
|
+
self.frame_times = np.linspace(
|
|
157
|
+
trajectory.t_start, trajectory.t_end, self.n_frames)
|
|
158
|
+
|
|
159
|
+
# Electron density renderer
|
|
160
|
+
self._edensity = None
|
|
161
|
+
if self.config.show_electron_density:
|
|
162
|
+
from molbuilder.visualization.electron_density_viz import (
|
|
163
|
+
ElectronDensityRenderer,
|
|
164
|
+
)
|
|
165
|
+
self._edensity = ElectronDensityRenderer(n_points=2000)
|
|
166
|
+
|
|
167
|
+
self._fig = None
|
|
168
|
+
self._ax = None
|
|
169
|
+
self._anim = None
|
|
170
|
+
self._paused = False
|
|
171
|
+
|
|
172
|
+
def build_animation(self) -> FuncAnimation:
|
|
173
|
+
"""Build and return the matplotlib FuncAnimation.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
-------
|
|
177
|
+
FuncAnimation
|
|
178
|
+
The animation object. Call ``plt.show()`` to display, or
|
|
179
|
+
use ``export()`` to save.
|
|
180
|
+
"""
|
|
181
|
+
self._fig, self._ax = plt.subplots(
|
|
182
|
+
figsize=self.config.figsize,
|
|
183
|
+
facecolor=BG_COLOR,
|
|
184
|
+
)
|
|
185
|
+
self._ax.set_facecolor(BG_COLOR)
|
|
186
|
+
self._ax.set_aspect("equal")
|
|
187
|
+
self._ax.tick_params(colors=TEXT_COLOR)
|
|
188
|
+
for spine in self._ax.spines.values():
|
|
189
|
+
spine.set_color(TEXT_COLOR)
|
|
190
|
+
|
|
191
|
+
self._anim = FuncAnimation(
|
|
192
|
+
self._fig,
|
|
193
|
+
self._render_frame,
|
|
194
|
+
frames=self.n_frames,
|
|
195
|
+
interval=1000 // self.config.fps,
|
|
196
|
+
blit=False,
|
|
197
|
+
repeat=True,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return self._anim
|
|
201
|
+
|
|
202
|
+
def _render_frame(self, frame_idx: int):
|
|
203
|
+
"""Render a single animation frame."""
|
|
204
|
+
ax = self._ax
|
|
205
|
+
ax.clear()
|
|
206
|
+
ax.set_facecolor(BG_COLOR)
|
|
207
|
+
|
|
208
|
+
t_fs = self.frame_times[frame_idx]
|
|
209
|
+
positions = self.trajectory.at_time(t_fs)
|
|
210
|
+
|
|
211
|
+
mol = self.molecule
|
|
212
|
+
symbols = [a.symbol for a in mol.atoms]
|
|
213
|
+
|
|
214
|
+
# Compute bounds for axis
|
|
215
|
+
margin = 2.0
|
|
216
|
+
x_min = positions[:, 0].min() - margin
|
|
217
|
+
x_max = positions[:, 0].max() + margin
|
|
218
|
+
y_min = positions[:, 1].min() - margin
|
|
219
|
+
y_max = positions[:, 1].max() + margin
|
|
220
|
+
ax.set_xlim(x_min, x_max)
|
|
221
|
+
ax.set_ylim(y_min, y_max)
|
|
222
|
+
ax.set_aspect("equal")
|
|
223
|
+
|
|
224
|
+
# Draw bonds
|
|
225
|
+
for bond in mol.bonds:
|
|
226
|
+
i, j = bond.atom_i, bond.atom_j
|
|
227
|
+
key = (min(i, j), max(i, j))
|
|
228
|
+
order = float(bond.order)
|
|
229
|
+
|
|
230
|
+
# Check for fractional bond order from trajectory
|
|
231
|
+
frac_order = self.trajectory.bond_order_at_time(t_fs, i, j)
|
|
232
|
+
if frac_order > 0.01:
|
|
233
|
+
order = frac_order
|
|
234
|
+
|
|
235
|
+
# Bond styling
|
|
236
|
+
color = BOND_COLOR
|
|
237
|
+
linestyle = "-"
|
|
238
|
+
linewidth = 1.5 * max(order, 0.2)
|
|
239
|
+
|
|
240
|
+
if frac_order > 0.01:
|
|
241
|
+
color = _bond_color_from_order(frac_order)
|
|
242
|
+
if frac_order < 0.5:
|
|
243
|
+
linestyle = ":"
|
|
244
|
+
elif frac_order < 0.8:
|
|
245
|
+
linestyle = "--"
|
|
246
|
+
|
|
247
|
+
pi = positions[i]
|
|
248
|
+
pj = positions[j]
|
|
249
|
+
ax.plot(
|
|
250
|
+
[pi[0], pj[0]], [pi[1], pj[1]],
|
|
251
|
+
color=color, linewidth=linewidth,
|
|
252
|
+
linestyle=linestyle, zorder=1,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Bond order annotation
|
|
256
|
+
if self.config.show_bond_orders and frac_order > 0.05:
|
|
257
|
+
mid = 0.5 * (pi + pj)
|
|
258
|
+
ax.text(mid[0], mid[1] + 0.15, f"{frac_order:.2f}",
|
|
259
|
+
color=TEXT_COLOR, fontsize=7, ha="center",
|
|
260
|
+
va="bottom", zorder=5)
|
|
261
|
+
|
|
262
|
+
# Draw electron density
|
|
263
|
+
if self._edensity is not None and self.config.show_electron_density:
|
|
264
|
+
for bond in mol.bonds:
|
|
265
|
+
i, j = bond.atom_i, bond.atom_j
|
|
266
|
+
frac_order = self.trajectory.bond_order_at_time(t_fs, i, j)
|
|
267
|
+
if 0.1 < frac_order < 0.9:
|
|
268
|
+
z_a = SYMBOL_TO_Z.get(symbols[i], 1)
|
|
269
|
+
z_b = SYMBOL_TO_Z.get(symbols[j], 1)
|
|
270
|
+
self._edensity.render_on_axis_2d(
|
|
271
|
+
ax, positions[i], positions[j],
|
|
272
|
+
z_a, z_b, frac_order, point_size=1.5)
|
|
273
|
+
|
|
274
|
+
# Draw electron flow arrows
|
|
275
|
+
if (self.config.show_electron_flows
|
|
276
|
+
and self.mechanism is not None
|
|
277
|
+
and self.choreographer is not None):
|
|
278
|
+
self._draw_electron_flows(ax, positions, t_fs, frame_idx)
|
|
279
|
+
|
|
280
|
+
# Draw atoms (on top of bonds)
|
|
281
|
+
for idx, atom in enumerate(mol.atoms):
|
|
282
|
+
x, y = positions[idx, 0], positions[idx, 1]
|
|
283
|
+
color = cpk_color(atom.symbol)
|
|
284
|
+
radius = covalent_radius_pm(atom.symbol) / 100.0 * 0.3
|
|
285
|
+
radius = max(radius, 0.15)
|
|
286
|
+
|
|
287
|
+
circle = plt.Circle(
|
|
288
|
+
(x, y), radius, color=color,
|
|
289
|
+
zorder=3, ec="#222222", linewidth=0.5)
|
|
290
|
+
ax.add_patch(circle)
|
|
291
|
+
|
|
292
|
+
# Atom label for non-hydrogen atoms
|
|
293
|
+
if atom.symbol != "H":
|
|
294
|
+
ax.text(x, y, atom.symbol,
|
|
295
|
+
color="#000000", fontsize=7, fontweight="bold",
|
|
296
|
+
ha="center", va="center", zorder=4)
|
|
297
|
+
|
|
298
|
+
# Overlays
|
|
299
|
+
if self.config.show_time_label:
|
|
300
|
+
ax.text(0.02, 0.98, _time_label(t_fs),
|
|
301
|
+
transform=ax.transAxes,
|
|
302
|
+
color=TEXT_COLOR, fontsize=12, fontweight="bold",
|
|
303
|
+
va="top", ha="left",
|
|
304
|
+
bbox=dict(boxstyle="round,pad=0.3",
|
|
305
|
+
facecolor=BG_COLOR, alpha=0.8))
|
|
306
|
+
|
|
307
|
+
if self.config.show_energy_bar:
|
|
308
|
+
self._draw_energy_bar(ax, t_fs)
|
|
309
|
+
|
|
310
|
+
# Mechanism annotation
|
|
311
|
+
if self.choreographer is not None:
|
|
312
|
+
stage_idx = self._stage_for_time(t_fs)
|
|
313
|
+
if stage_idx is not None:
|
|
314
|
+
annotation = self.choreographer.stage_annotation(stage_idx)
|
|
315
|
+
if annotation:
|
|
316
|
+
ax.text(0.5, 0.02, annotation,
|
|
317
|
+
transform=ax.transAxes,
|
|
318
|
+
color=TRANSITION_STATE_COLOR,
|
|
319
|
+
fontsize=10, ha="center", va="bottom",
|
|
320
|
+
bbox=dict(boxstyle="round,pad=0.3",
|
|
321
|
+
facecolor=BG_COLOR, alpha=0.8))
|
|
322
|
+
|
|
323
|
+
ax.set_xlabel("x (A)", color=TEXT_COLOR, fontsize=8)
|
|
324
|
+
ax.set_ylabel("y (A)", color=TEXT_COLOR, fontsize=8)
|
|
325
|
+
|
|
326
|
+
def _draw_electron_flows(self, ax, positions, t_fs, frame_idx):
|
|
327
|
+
"""Draw curly/fishhook arrows for electron flows."""
|
|
328
|
+
from molbuilder.dynamics.mechanisms import FlowType
|
|
329
|
+
|
|
330
|
+
stage_idx = self._stage_for_time(t_fs)
|
|
331
|
+
if stage_idx is None:
|
|
332
|
+
return
|
|
333
|
+
|
|
334
|
+
total_steps = len(self.mechanism.stages)
|
|
335
|
+
progress = (frame_idx / max(1, self.n_frames - 1))
|
|
336
|
+
stage_progress = (progress * total_steps) - stage_idx
|
|
337
|
+
|
|
338
|
+
flows = self.choreographer.electron_flows_at(
|
|
339
|
+
stage_idx, stage_progress)
|
|
340
|
+
|
|
341
|
+
for flow in flows:
|
|
342
|
+
start = positions[flow.from_atom][:2]
|
|
343
|
+
mid_bond = 0.5 * (
|
|
344
|
+
positions[flow.to_bond[0]][:2]
|
|
345
|
+
+ positions[flow.to_bond[1]][:2])
|
|
346
|
+
|
|
347
|
+
color = ARROW_COLOR
|
|
348
|
+
style = "Simple,tail_width=2,head_width=8,head_length=6"
|
|
349
|
+
if flow.flow_type == FlowType.FISHHOOK_ARROW:
|
|
350
|
+
style = "Simple,tail_width=1,head_width=5,head_length=4"
|
|
351
|
+
|
|
352
|
+
arrow = FancyArrowPatch(
|
|
353
|
+
posA=tuple(start), posB=tuple(mid_bond),
|
|
354
|
+
arrowstyle=style,
|
|
355
|
+
color=color, alpha=0.8,
|
|
356
|
+
connectionstyle="arc3,rad=0.3",
|
|
357
|
+
zorder=6,
|
|
358
|
+
)
|
|
359
|
+
ax.add_patch(arrow)
|
|
360
|
+
|
|
361
|
+
if flow.label:
|
|
362
|
+
label_pos = 0.5 * (start + mid_bond)
|
|
363
|
+
ax.text(label_pos[0], label_pos[1] + 0.3,
|
|
364
|
+
flow.label, color=ARROW_COLOR,
|
|
365
|
+
fontsize=6, ha="center", va="bottom",
|
|
366
|
+
zorder=7)
|
|
367
|
+
|
|
368
|
+
def _draw_energy_bar(self, ax, t_fs):
|
|
369
|
+
"""Draw a small energy bar overlay."""
|
|
370
|
+
times, ke, pe = self.trajectory.energies()
|
|
371
|
+
if len(times) < 2:
|
|
372
|
+
return
|
|
373
|
+
|
|
374
|
+
# Find closest frame
|
|
375
|
+
idx = np.searchsorted(times, t_fs)
|
|
376
|
+
idx = min(idx, len(times) - 1)
|
|
377
|
+
ke_val = ke[idx]
|
|
378
|
+
pe_val = pe[idx]
|
|
379
|
+
total = ke_val + pe_val
|
|
380
|
+
|
|
381
|
+
bar_text = f"KE: {ke_val:.1f} PE: {pe_val:.1f} Total: {total:.1f} kJ/mol"
|
|
382
|
+
ax.text(0.98, 0.98, bar_text,
|
|
383
|
+
transform=ax.transAxes,
|
|
384
|
+
color=TEXT_COLOR, fontsize=8,
|
|
385
|
+
va="top", ha="right",
|
|
386
|
+
bbox=dict(boxstyle="round,pad=0.3",
|
|
387
|
+
facecolor=BG_COLOR, alpha=0.8))
|
|
388
|
+
|
|
389
|
+
def _stage_for_time(self, t_fs):
|
|
390
|
+
"""Determine which mechanism stage corresponds to a time."""
|
|
391
|
+
if self.mechanism is None:
|
|
392
|
+
return None
|
|
393
|
+
n_stages = len(self.mechanism.stages)
|
|
394
|
+
if n_stages == 0:
|
|
395
|
+
return None
|
|
396
|
+
frac = (t_fs - self.trajectory.t_start) / max(
|
|
397
|
+
self.trajectory.duration, 1e-10)
|
|
398
|
+
stage = int(frac * n_stages)
|
|
399
|
+
return min(stage, n_stages - 1)
|
|
400
|
+
|
|
401
|
+
def show(self):
|
|
402
|
+
"""Build and display the animation interactively."""
|
|
403
|
+
self.build_animation()
|
|
404
|
+
plt.show()
|
|
405
|
+
|
|
406
|
+
def export(self, path: str | None = None):
|
|
407
|
+
"""Export the animation to a file.
|
|
408
|
+
|
|
409
|
+
Parameters
|
|
410
|
+
----------
|
|
411
|
+
path : str or None
|
|
412
|
+
Output path. If None, uses config.export_path.
|
|
413
|
+
Supports .mp4 (requires ffmpeg) and .gif (Pillow).
|
|
414
|
+
"""
|
|
415
|
+
path = path or self.config.export_path
|
|
416
|
+
if path is None:
|
|
417
|
+
raise ValueError("No export path specified")
|
|
418
|
+
|
|
419
|
+
if self._anim is None:
|
|
420
|
+
self.build_animation()
|
|
421
|
+
|
|
422
|
+
if path.endswith(".mp4"):
|
|
423
|
+
try:
|
|
424
|
+
from matplotlib.animation import FFMpegWriter
|
|
425
|
+
writer = FFMpegWriter(fps=self.config.fps)
|
|
426
|
+
self._anim.save(path, writer=writer, dpi=self.config.dpi)
|
|
427
|
+
except Exception:
|
|
428
|
+
# Fallback to gif
|
|
429
|
+
path = path.replace(".mp4", ".gif")
|
|
430
|
+
from matplotlib.animation import PillowWriter
|
|
431
|
+
writer = PillowWriter(fps=self.config.fps)
|
|
432
|
+
self._anim.save(path, writer=writer, dpi=self.config.dpi)
|
|
433
|
+
elif path.endswith(".gif"):
|
|
434
|
+
from matplotlib.animation import PillowWriter
|
|
435
|
+
writer = PillowWriter(fps=self.config.fps)
|
|
436
|
+
self._anim.save(path, writer=writer, dpi=self.config.dpi)
|
|
437
|
+
else:
|
|
438
|
+
self._anim.save(path, dpi=self.config.dpi)
|
|
439
|
+
|
|
440
|
+
plt.close(self._fig)
|
|
441
|
+
return path
|
|
442
|
+
|
|
443
|
+
@property
|
|
444
|
+
def fig(self):
|
|
445
|
+
return self._fig
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# ===================================================================
|
|
449
|
+
# Convenience functions
|
|
450
|
+
# ===================================================================
|
|
451
|
+
|
|
452
|
+
def visualize_md_trajectory(molecule,
|
|
453
|
+
n_steps: int = 500,
|
|
454
|
+
dt_fs: float = 0.5,
|
|
455
|
+
temperature_K: float = 300.0,
|
|
456
|
+
config: PlaybackConfig | None = None,
|
|
457
|
+
show: bool = True):
|
|
458
|
+
"""Quick MD simulation + visualization.
|
|
459
|
+
|
|
460
|
+
Parameters
|
|
461
|
+
----------
|
|
462
|
+
molecule : Molecule
|
|
463
|
+
Molecule to simulate.
|
|
464
|
+
n_steps : int
|
|
465
|
+
Number of MD steps.
|
|
466
|
+
dt_fs : float
|
|
467
|
+
Timestep in fs.
|
|
468
|
+
temperature_K : float
|
|
469
|
+
Target temperature.
|
|
470
|
+
config : PlaybackConfig or None
|
|
471
|
+
Visualization config.
|
|
472
|
+
show : bool
|
|
473
|
+
If True, display interactively.
|
|
474
|
+
|
|
475
|
+
Returns
|
|
476
|
+
-------
|
|
477
|
+
InteractionVisualizer
|
|
478
|
+
The visualizer (can be used for export).
|
|
479
|
+
"""
|
|
480
|
+
from molbuilder.dynamics.simulation import MDSimulation
|
|
481
|
+
|
|
482
|
+
sim = MDSimulation(molecule, dt_fs=dt_fs,
|
|
483
|
+
temperature_K=temperature_K)
|
|
484
|
+
traj = sim.run(n_steps)
|
|
485
|
+
|
|
486
|
+
viz = InteractionVisualizer(traj, molecule, config=config)
|
|
487
|
+
if show:
|
|
488
|
+
viz.show()
|
|
489
|
+
return viz
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def visualize_reaction(molecule,
|
|
493
|
+
mechanism,
|
|
494
|
+
n_steps_per_stage: int = 200,
|
|
495
|
+
dt_fs: float = 0.5,
|
|
496
|
+
temperature_K: float = 50.0,
|
|
497
|
+
config: PlaybackConfig | None = None,
|
|
498
|
+
show: bool = True):
|
|
499
|
+
"""Mechanism-steered MD + visualization.
|
|
500
|
+
|
|
501
|
+
Parameters
|
|
502
|
+
----------
|
|
503
|
+
molecule : Molecule
|
|
504
|
+
Starting molecule.
|
|
505
|
+
mechanism : ReactionMechanism
|
|
506
|
+
Reaction mechanism template.
|
|
507
|
+
n_steps_per_stage : int
|
|
508
|
+
MD steps per mechanism stage.
|
|
509
|
+
dt_fs : float
|
|
510
|
+
Timestep.
|
|
511
|
+
temperature_K : float
|
|
512
|
+
Temperature (low for cleaner mechanism).
|
|
513
|
+
config : PlaybackConfig or None
|
|
514
|
+
Visualization config.
|
|
515
|
+
show : bool
|
|
516
|
+
Display interactively.
|
|
517
|
+
|
|
518
|
+
Returns
|
|
519
|
+
-------
|
|
520
|
+
InteractionVisualizer
|
|
521
|
+
"""
|
|
522
|
+
from molbuilder.dynamics.simulation import MDSimulation
|
|
523
|
+
from molbuilder.dynamics.mechanism_choreography import MechanismChoreographer
|
|
524
|
+
from molbuilder.dynamics.forcefield import ForceField
|
|
525
|
+
|
|
526
|
+
sim = MDSimulation(molecule, dt_fs=dt_fs,
|
|
527
|
+
temperature_K=temperature_K,
|
|
528
|
+
thermostat=True,
|
|
529
|
+
thermostat_tau_fs=50.0)
|
|
530
|
+
traj = sim.run_mechanism(mechanism, n_steps_per_stage)
|
|
531
|
+
|
|
532
|
+
choreographer = MechanismChoreographer(
|
|
533
|
+
mechanism, sim.ff, n_steps_per_stage)
|
|
534
|
+
viz = InteractionVisualizer(
|
|
535
|
+
traj, molecule,
|
|
536
|
+
mechanism=mechanism,
|
|
537
|
+
choreographer=choreographer,
|
|
538
|
+
config=config)
|
|
539
|
+
if show:
|
|
540
|
+
viz.show()
|
|
541
|
+
return viz
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def visualize_bond_formation(molecule,
|
|
545
|
+
atom_i: int, atom_j: int,
|
|
546
|
+
n_steps: int = 400,
|
|
547
|
+
config: PlaybackConfig | None = None,
|
|
548
|
+
show: bool = True):
|
|
549
|
+
"""Visualize a single bond formation event.
|
|
550
|
+
|
|
551
|
+
Creates a simple mechanism that brings two atoms together and
|
|
552
|
+
forms a bond.
|
|
553
|
+
|
|
554
|
+
Parameters
|
|
555
|
+
----------
|
|
556
|
+
molecule : Molecule
|
|
557
|
+
Molecule containing both atoms.
|
|
558
|
+
atom_i, atom_j : int
|
|
559
|
+
Atom indices for the bond to form.
|
|
560
|
+
n_steps : int
|
|
561
|
+
Total MD steps.
|
|
562
|
+
config : PlaybackConfig or None
|
|
563
|
+
Visualization config.
|
|
564
|
+
show : bool
|
|
565
|
+
Display interactively.
|
|
566
|
+
|
|
567
|
+
Returns
|
|
568
|
+
-------
|
|
569
|
+
InteractionVisualizer
|
|
570
|
+
"""
|
|
571
|
+
from molbuilder.dynamics.mechanisms import (
|
|
572
|
+
ReactionMechanism, MechanismStage, MechanismType,
|
|
573
|
+
ElectronFlow, FlowType,
|
|
574
|
+
)
|
|
575
|
+
from molbuilder.core.bond_data import bond_length
|
|
576
|
+
|
|
577
|
+
sym_i = molecule.atoms[atom_i].symbol
|
|
578
|
+
sym_j = molecule.atoms[atom_j].symbol
|
|
579
|
+
target_bl = bond_length(sym_i, sym_j, 1)
|
|
580
|
+
|
|
581
|
+
key = (min(atom_i, atom_j), max(atom_i, atom_j))
|
|
582
|
+
mechanism = ReactionMechanism(
|
|
583
|
+
name=f"Bond formation {sym_i}-{sym_j}",
|
|
584
|
+
mechanism_type=MechanismType.SN2,
|
|
585
|
+
stages=[
|
|
586
|
+
MechanismStage(
|
|
587
|
+
name="Approach",
|
|
588
|
+
distance_targets={(atom_i, atom_j): target_bl * 1.5},
|
|
589
|
+
bond_order_changes={key: 0.3},
|
|
590
|
+
electron_flows=[
|
|
591
|
+
ElectronFlow(atom_i, (atom_i, atom_j),
|
|
592
|
+
FlowType.CURLY_ARROW,
|
|
593
|
+
"Bond forming"),
|
|
594
|
+
],
|
|
595
|
+
duration_weight=1.0,
|
|
596
|
+
annotation=f"Atoms approaching: {sym_i}...{sym_j}",
|
|
597
|
+
),
|
|
598
|
+
MechanismStage(
|
|
599
|
+
name="Bond formation",
|
|
600
|
+
distance_targets={(atom_i, atom_j): target_bl},
|
|
601
|
+
bond_order_changes={key: 1.0},
|
|
602
|
+
electron_flows=[],
|
|
603
|
+
duration_weight=1.5,
|
|
604
|
+
annotation=f"Bond formed: {sym_i}-{sym_j}",
|
|
605
|
+
),
|
|
606
|
+
],
|
|
607
|
+
atom_roles={"atom_i": atom_i, "atom_j": atom_j},
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
return visualize_reaction(
|
|
611
|
+
molecule, mechanism,
|
|
612
|
+
n_steps_per_stage=n_steps // 2,
|
|
613
|
+
temperature_K=50.0,
|
|
614
|
+
config=config,
|
|
615
|
+
show=show)
|
|
@@ -10,3 +10,10 @@ POSITIVE_COLOR = "#3399ff"
|
|
|
10
10
|
NEGATIVE_COLOR = "#ff5533"
|
|
11
11
|
NEUTRAL_COLOR = "#44ccff"
|
|
12
12
|
ENERGY_COLOR = "#ffaa33"
|
|
13
|
+
|
|
14
|
+
# Interaction visualization colors
|
|
15
|
+
FORMING_BOND_COLOR = "#44ff88"
|
|
16
|
+
BREAKING_BOND_COLOR = "#ff4444"
|
|
17
|
+
ELECTRON_CLOUD_COLOR = "#6699ff"
|
|
18
|
+
ARROW_COLOR = "#ff8800"
|
|
19
|
+
TRANSITION_STATE_COLOR = "#ffff44"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
molbuilder/__init__.py,sha256=
|
|
1
|
+
molbuilder/__init__.py,sha256=XV1Q1NePEaugMbuY_n_ALj4OerKAIAZwdEtQh__YP7I,205
|
|
2
2
|
molbuilder/__main__.py,sha256=F64UnMCnwJXEF59k6jtSoLga7al9rRvGxAOK2FCpA9U,123
|
|
3
3
|
molbuilder/atomic/__init__.py,sha256=dUtMg4Wr76cszBJ061BmTZN3akN9gK2AnjpRIvaExyM,217
|
|
4
4
|
molbuilder/atomic/bohr.py,sha256=IPZjQqW4BSl_f96JZ45gbQVykIh17gn5eYhZtlJDfWo,8941
|
|
@@ -10,8 +10,8 @@ molbuilder/bonding/covalent.py,sha256=ewvRvYzbsmuRaVLiXnRyUCU8F_CdDz4Reo5o7b2qbm
|
|
|
10
10
|
molbuilder/bonding/lewis.py,sha256=q4uXjvGKn3NO-TaZzFn8P7WaRgz1QarKF_JbznnCrLU,11967
|
|
11
11
|
molbuilder/bonding/vsepr.py,sha256=YU8h4dOdSfCwDSIfqwkFeAkHr1LS7_Kws6DFeseMB6g,14946
|
|
12
12
|
molbuilder/cli/__init__.py,sha256=eo7LYBWtUUUf3HLI564gX1qFQpQczPclSVurANG-b_I,64
|
|
13
|
-
molbuilder/cli/demos.py,sha256=
|
|
14
|
-
molbuilder/cli/menu.py,sha256=
|
|
13
|
+
molbuilder/cli/demos.py,sha256=GuxeRXRiO1aZblvbj54Z2Ghm871ZfZanLt9oHUVU45s,20234
|
|
14
|
+
molbuilder/cli/menu.py,sha256=vUGT56ueRp-Rhf748bEXqtFgneNxGlpe9_maHvgyxvY,3568
|
|
15
15
|
molbuilder/cli/wizard.py,sha256=NVQ9mGLsdP_FmXX5ubpdfZImmPR4f04tNReL-bKRvHc,27335
|
|
16
16
|
molbuilder/core/__init__.py,sha256=QtbG7DjHWuvu88AP3uRukb_Ax1tMKKSTFfbNoP5PjYA,428
|
|
17
17
|
molbuilder/core/bond_data.py,sha256=S5TcK6nWVG5--cIkG3Rw6-_trHVkP7quVmwglS7LSSE,5675
|
|
@@ -19,8 +19,15 @@ molbuilder/core/constants.py,sha256=Asehj4DftFKXNteveljh9bTFQaYugGY-ErIHdXRxMuA,
|
|
|
19
19
|
molbuilder/core/element_properties.py,sha256=jiiE527bRgFLQcDepOfZWIK-hsTCMDMFEQIoUg2RdwU,7575
|
|
20
20
|
molbuilder/core/elements.py,sha256=vTx0A6j53E0fl-8uOYvJXirBW-xQKc8PTp4JhO9us44,6870
|
|
21
21
|
molbuilder/core/geometry.py,sha256=ktvmZHhnm1dmhHpW-9iY-ugVPJlLml8n2TvcptmFjiI,7440
|
|
22
|
+
molbuilder/dynamics/__init__.py,sha256=BK-b7XcXJ64H77Xgc4r8IbRZ4LUQFfPlUDl6m-qMVkA,1661
|
|
23
|
+
molbuilder/dynamics/forcefield.py,sha256=rvX44zj3O2e_CVuEhZ80vK2ebL-CiD5Y2s1kv7xYxnU,20370
|
|
24
|
+
molbuilder/dynamics/integrator.py,sha256=A6JKaS-8Mh6eysLa8asbZf559iVru_xOk-LfgELaETI,9130
|
|
25
|
+
molbuilder/dynamics/mechanism_choreography.py,sha256=0JLfpQw-6w-XT7-mKzZjmuMH3_tOa3sGtTyOu582sc4,7113
|
|
26
|
+
molbuilder/dynamics/mechanisms.py,sha256=PvmO3VqRyeeUpfXk6H8AkSIqo4fRbU3w-Fok5__NHVw,17773
|
|
27
|
+
molbuilder/dynamics/simulation.py,sha256=yosysL4iHuHa_FwmZJbj65QU8LSFqmDkBexNSiD4li4,7200
|
|
28
|
+
molbuilder/dynamics/trajectory.py,sha256=S7rofRDQucgXHt0EGTKXA2LyrB3gGJBD8BNuETzXYxk,6406
|
|
22
29
|
molbuilder/gui/__init__.py,sha256=KqjMkDsQ5qIbSrK4C_YP45vHaomggTutwDg-dxmEmW4,99
|
|
23
|
-
molbuilder/gui/app.py,sha256=
|
|
30
|
+
molbuilder/gui/app.py,sha256=EffmhcivoDLSbkPVJg-x9Ob2lXVC72FjXvktE4FklR4,16254
|
|
24
31
|
molbuilder/gui/canvas3d.py,sha256=LHVbgGbNhmyN24oCQ8z8XhwWhP9ehapTcbaP7at_ivY,4735
|
|
25
32
|
molbuilder/gui/dialogs.py,sha256=ULUX3YiUHSjqWi7fCnMsi-e0pbYxWINJmYcKX8eiKhU,4037
|
|
26
33
|
molbuilder/gui/event_handler.py,sha256=p9FF75csq7pf5u2we3uVfebri-tFWbJtRPVW5jtQX3o,4341
|
|
@@ -65,14 +72,17 @@ molbuilder/smiles/__init__.py,sha256=KX9neiR2temLI71KaZS_apcbrWZpPAAgRt4Q5FIJ9G8
|
|
|
65
72
|
molbuilder/smiles/parser.py,sha256=rFoUlXH2t2skbXQyP9Xwn4JRjvWg0JcRsmq7gpCOAyw,15963
|
|
66
73
|
molbuilder/smiles/tokenizer.py,sha256=HAaA8Nok0dIRB2jXfNzAO2Yuv7KgAlL0zMyFXWIos2w,8709
|
|
67
74
|
molbuilder/smiles/writer.py,sha256=x7s-Y6HsF5yEhijvtQIPRBTkxTFccFtzsL-pKKcTHAQ,12025
|
|
68
|
-
molbuilder/visualization/__init__.py,sha256=
|
|
75
|
+
molbuilder/visualization/__init__.py,sha256=EmYmHNXJs0veqnUeEX5tnQYP8q7zS_l1PWDgRJyVU9c,125
|
|
69
76
|
molbuilder/visualization/bohr_viz.py,sha256=6lWZUOy4R1M6DdxJxHfBSY2DeW1KPDvbYKt5tXRx2t0,5624
|
|
77
|
+
molbuilder/visualization/electron_density_viz.py,sha256=4bKR-qaa1b9dLqE38qBbPA5ImKPtT4nB5RVfVKyTj70,7809
|
|
78
|
+
molbuilder/visualization/interaction_controls.py,sha256=YwHTU-QESi7hF0DWAQbEZlRdmxUhm9r9UrHxUyHcYJE,6363
|
|
79
|
+
molbuilder/visualization/interaction_viz.py,sha256=vTCnPeOQW9csQtD4TVmr5u_W5o4U3zJhru_nY9yePpA,20954
|
|
70
80
|
molbuilder/visualization/molecule_viz.py,sha256=SvDIFvGviCGs9g0W7wQQj9frGdPydoBc4v5EsacNkgM,14002
|
|
71
81
|
molbuilder/visualization/quantum_viz.py,sha256=7vvr1KaUa_WZ4FIb2r8ih6nKjUjP4piJeJosUBtsdrQ,15187
|
|
72
|
-
molbuilder/visualization/theme.py,sha256=
|
|
73
|
-
molbuilder-1.
|
|
74
|
-
molbuilder-1.
|
|
75
|
-
molbuilder-1.
|
|
76
|
-
molbuilder-1.
|
|
77
|
-
molbuilder-1.
|
|
78
|
-
molbuilder-1.
|
|
82
|
+
molbuilder/visualization/theme.py,sha256=j_jZ4lqF8v1GdHbyMxrOW1BWtN4AwwWEaB2LBPiDUeI,512
|
|
83
|
+
molbuilder-1.1.0.dist-info/licenses/LICENSE,sha256=WEWbPnVFDQwVEHeX_mpHol-ByLsQWgQZDMjSNjdFq8w,1094
|
|
84
|
+
molbuilder-1.1.0.dist-info/METADATA,sha256=kkOi_8G6HZTxCQXdFDjGQw0ApxZInBLbvQcH2eDnIx0,14828
|
|
85
|
+
molbuilder-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
86
|
+
molbuilder-1.1.0.dist-info/entry_points.txt,sha256=WkIjoyvOBBe_0KeT6ixldelD_7Hfj0mPADjamMmVgck,56
|
|
87
|
+
molbuilder-1.1.0.dist-info/top_level.txt,sha256=zqS1kyhgo71hB_QcsI2eq5lO4w7VGiMaXH1f2Hn-kaA,11
|
|
88
|
+
molbuilder-1.1.0.dist-info/RECORD,,
|
|
File without changes
|