MoleditPy 2.2.0a1__py3-none-any.whl → 2.2.0a3__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.
- moleditpy/modules/constants.py +1 -1
- moleditpy/modules/main_window_main_init.py +31 -13
- moleditpy/modules/main_window_ui_manager.py +21 -2
- moleditpy/modules/plugin_interface.py +1 -10
- moleditpy/modules/plugin_manager.py +0 -3
- {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/METADATA +1 -1
- {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/RECORD +11 -28
- moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -919
- moleditpy/plugins/File/animated_xyz_giffer.py +0 -583
- moleditpy/plugins/File/cube_viewer.py +0 -689
- moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -1148
- moleditpy/plugins/File/mapped_cube_viewer.py +0 -552
- moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -1226
- moleditpy/plugins/File/paste_xyz.py +0 -336
- moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -930
- moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -1028
- moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +0 -286
- moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -65
- moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -268
- moleditpy/plugins/Optimization/conf_search.py +0 -224
- moleditpy/plugins/Utility/atom_colorizer.py +0 -547
- moleditpy/plugins/Utility/console.py +0 -163
- moleditpy/plugins/Utility/pubchem_ressolver.py +0 -244
- moleditpy/plugins/Utility/vdw_radii_overlay.py +0 -303
- {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/WHEEL +0 -0
- {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/entry_points.txt +0 -0
- {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/top_level.txt +0 -0
|
@@ -1,689 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import os
|
|
3
|
-
import tempfile
|
|
4
|
-
import numpy as np
|
|
5
|
-
import pyvista as pv
|
|
6
|
-
from PyQt6.QtWidgets import (QFileDialog, QDockWidget, QWidget, QVBoxLayout,
|
|
7
|
-
QSlider, QLabel, QHBoxLayout, QPushButton, QMessageBox,
|
|
8
|
-
QDoubleSpinBox, QColorDialog, QInputDialog, QDialog,
|
|
9
|
-
QFormLayout, QDialogButtonBox, QSpinBox, QCheckBox)
|
|
10
|
-
from PyQt6.QtGui import QColor
|
|
11
|
-
from PyQt6.QtCore import Qt
|
|
12
|
-
|
|
13
|
-
# RDKit imports for molecule construction
|
|
14
|
-
try:
|
|
15
|
-
from rdkit import Chem
|
|
16
|
-
from rdkit import Geometry
|
|
17
|
-
try:
|
|
18
|
-
from rdkit.Chem import rdDetermineBonds
|
|
19
|
-
except ImportError:
|
|
20
|
-
rdDetermineBonds = None
|
|
21
|
-
except ImportError:
|
|
22
|
-
Chem = None
|
|
23
|
-
Geometry = None
|
|
24
|
-
rdDetermineBonds = None
|
|
25
|
-
|
|
26
|
-
__version__="2025.12.20"
|
|
27
|
-
__author__="HiroYokoyama"
|
|
28
|
-
PLUGIN_NAME = "Cube File Viewer"
|
|
29
|
-
|
|
30
|
-
def parse_cube_data(filename):
|
|
31
|
-
"""
|
|
32
|
-
Parses a Gaussian Cube file and returns raw data structures.
|
|
33
|
-
Adapted from test.py with robust header handling.
|
|
34
|
-
"""
|
|
35
|
-
with open(filename, 'r') as f:
|
|
36
|
-
lines = f.readlines()
|
|
37
|
-
|
|
38
|
-
if len(lines) < 6:
|
|
39
|
-
raise ValueError("File too short to be a Cube file.")
|
|
40
|
-
|
|
41
|
-
# --- Header Parsing ---
|
|
42
|
-
# Line 3: Natoms, Origin
|
|
43
|
-
tokens = lines[2].split()
|
|
44
|
-
n_atoms_raw = int(tokens[0])
|
|
45
|
-
n_atoms = abs(n_atoms_raw)
|
|
46
|
-
origin_raw = np.array([float(tokens[1]), float(tokens[2]), float(tokens[3])])
|
|
47
|
-
|
|
48
|
-
# Lines 4-6: NX, NY, NZ and vectors
|
|
49
|
-
def parse_vec(line):
|
|
50
|
-
t = line.split()
|
|
51
|
-
return int(t[0]), np.array([float(t[1]), float(t[2]), float(t[3])])
|
|
52
|
-
|
|
53
|
-
nx, x_vec_raw = parse_vec(lines[3])
|
|
54
|
-
ny, y_vec_raw = parse_vec(lines[4])
|
|
55
|
-
nz, z_vec_raw = parse_vec(lines[5])
|
|
56
|
-
|
|
57
|
-
# Auto-detect units based on sign of NX/NY/NZ (Gaussian standard)
|
|
58
|
-
is_angstrom_header = (nx < 0 or ny < 0 or nz < 0)
|
|
59
|
-
|
|
60
|
-
nx, ny, nz = abs(nx), abs(ny), abs(nz)
|
|
61
|
-
|
|
62
|
-
# --- Atoms Parsing ---
|
|
63
|
-
atoms = []
|
|
64
|
-
current_line = 6
|
|
65
|
-
if n_atoms_raw < 0:
|
|
66
|
-
# Smart check: if next line does not look like an atom line, skip it.
|
|
67
|
-
try:
|
|
68
|
-
parts = lines[current_line].split()
|
|
69
|
-
if len(parts) != 5:
|
|
70
|
-
current_line += 1
|
|
71
|
-
except:
|
|
72
|
-
current_line += 1
|
|
73
|
-
|
|
74
|
-
for _ in range(n_atoms):
|
|
75
|
-
line = lines[current_line].split()
|
|
76
|
-
current_line += 1
|
|
77
|
-
atomic_num = int(line[0])
|
|
78
|
-
try:
|
|
79
|
-
x, y, z = float(line[2]), float(line[3]), float(line[4])
|
|
80
|
-
except:
|
|
81
|
-
x, y, z = 0.0, 0.0, 0.0
|
|
82
|
-
atoms.append((atomic_num, np.array([x, y, z])))
|
|
83
|
-
|
|
84
|
-
# --- Volumetric Data Parsing ---
|
|
85
|
-
|
|
86
|
-
# Skip metadata lines (e.g. "1 150") before data starts
|
|
87
|
-
while current_line < len(lines):
|
|
88
|
-
line_content = lines[current_line].strip()
|
|
89
|
-
parts = line_content.split()
|
|
90
|
-
|
|
91
|
-
# Skip empty lines
|
|
92
|
-
if not parts:
|
|
93
|
-
current_line += 1
|
|
94
|
-
continue
|
|
95
|
-
|
|
96
|
-
# Skip short lines (metadata often has few columns, data usually has 6)
|
|
97
|
-
if len(parts) < 6:
|
|
98
|
-
current_line += 1
|
|
99
|
-
continue
|
|
100
|
-
|
|
101
|
-
# Check if start is numeric
|
|
102
|
-
try:
|
|
103
|
-
float(parts[0])
|
|
104
|
-
except ValueError:
|
|
105
|
-
current_line += 1
|
|
106
|
-
continue
|
|
107
|
-
|
|
108
|
-
# If we get here, it's likely data
|
|
109
|
-
break
|
|
110
|
-
|
|
111
|
-
# Read rest of file
|
|
112
|
-
full_str = " ".join(lines[current_line:])
|
|
113
|
-
try:
|
|
114
|
-
data_values = np.fromstring(full_str, sep=' ')
|
|
115
|
-
except:
|
|
116
|
-
data_values = np.array([])
|
|
117
|
-
|
|
118
|
-
expected_size = nx * ny * nz
|
|
119
|
-
actual_size = len(data_values)
|
|
120
|
-
|
|
121
|
-
# FIX: Trim from START if excess > 0 (The header values are at the start)
|
|
122
|
-
# This logic fixes the shift issue test.py had.
|
|
123
|
-
if actual_size > expected_size:
|
|
124
|
-
excess = actual_size - expected_size
|
|
125
|
-
data_values = data_values[excess:]
|
|
126
|
-
elif actual_size < expected_size:
|
|
127
|
-
pad = np.zeros(expected_size - actual_size)
|
|
128
|
-
data_values = np.concatenate((data_values, pad))
|
|
129
|
-
|
|
130
|
-
return {
|
|
131
|
-
"atoms": atoms,
|
|
132
|
-
"origin": origin_raw,
|
|
133
|
-
"x_vec": x_vec_raw,
|
|
134
|
-
"y_vec": y_vec_raw,
|
|
135
|
-
"z_vec": z_vec_raw,
|
|
136
|
-
"dims": (nx, ny, nz),
|
|
137
|
-
"data_flat": data_values,
|
|
138
|
-
"is_angstrom_header": is_angstrom_header
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
def build_grid_from_meta(meta):
|
|
142
|
-
"""
|
|
143
|
-
Reconstructs the PyVista grid.
|
|
144
|
-
Correctly handles standard Cube (Z-fast) mapping.
|
|
145
|
-
"""
|
|
146
|
-
nx, ny, nz = meta['dims']
|
|
147
|
-
origin = meta['origin'].copy()
|
|
148
|
-
x_vec = meta['x_vec'].copy()
|
|
149
|
-
y_vec = meta['y_vec'].copy()
|
|
150
|
-
z_vec = meta['z_vec'].copy()
|
|
151
|
-
atoms = []
|
|
152
|
-
|
|
153
|
-
# Units Handling (replicated logic)
|
|
154
|
-
BOHR_TO_ANGSTROM = 0.529177210903
|
|
155
|
-
convert_to_ang = True
|
|
156
|
-
if meta['is_angstrom_header']:
|
|
157
|
-
convert_to_ang = False
|
|
158
|
-
|
|
159
|
-
if convert_to_ang:
|
|
160
|
-
origin *= BOHR_TO_ANGSTROM
|
|
161
|
-
x_vec *= BOHR_TO_ANGSTROM
|
|
162
|
-
y_vec *= BOHR_TO_ANGSTROM
|
|
163
|
-
z_vec *= BOHR_TO_ANGSTROM
|
|
164
|
-
|
|
165
|
-
for anum, pos in meta['atoms']:
|
|
166
|
-
p = pos.copy()
|
|
167
|
-
if convert_to_ang:
|
|
168
|
-
p *= BOHR_TO_ANGSTROM
|
|
169
|
-
atoms.append((anum, p))
|
|
170
|
-
|
|
171
|
-
# Grid Points Generation (Matches test.py logic for F-order consistency)
|
|
172
|
-
x_range = np.arange(nx)
|
|
173
|
-
y_range = np.arange(ny)
|
|
174
|
-
z_range = np.arange(nz)
|
|
175
|
-
|
|
176
|
-
gx, gy, gz = np.meshgrid(x_range, y_range, z_range, indexing='ij')
|
|
177
|
-
|
|
178
|
-
# Flatten using Fortran order (X-fast) for VTK Structure
|
|
179
|
-
gx_f = gx.flatten(order='F')
|
|
180
|
-
gy_f = gy.flatten(order='F')
|
|
181
|
-
gz_f = gz.flatten(order='F')
|
|
182
|
-
|
|
183
|
-
points = (origin +
|
|
184
|
-
np.outer(gx_f, x_vec) +
|
|
185
|
-
np.outer(gy_f, y_vec) +
|
|
186
|
-
np.outer(gz_f, z_vec))
|
|
187
|
-
|
|
188
|
-
grid = pv.StructuredGrid()
|
|
189
|
-
grid.points = points
|
|
190
|
-
grid.dimensions = [nx, ny, nz]
|
|
191
|
-
|
|
192
|
-
# Data Mapping
|
|
193
|
-
raw_data = meta['data_flat']
|
|
194
|
-
|
|
195
|
-
# Standard Cube: Z-Fast (C-order reshape)
|
|
196
|
-
# Then flatten F-order to match the X-fast points we just generated
|
|
197
|
-
# This logic matches test.py's implementation which user preferred
|
|
198
|
-
vol_3d = raw_data.reshape((nx, ny, nz), order='C')
|
|
199
|
-
grid.point_data["values"] = vol_3d.flatten(order='F')
|
|
200
|
-
|
|
201
|
-
return {"atoms": atoms}, grid
|
|
202
|
-
|
|
203
|
-
def read_cube(filename):
|
|
204
|
-
meta = parse_cube_data(filename)
|
|
205
|
-
return build_grid_from_meta(meta)
|
|
206
|
-
|
|
207
|
-
class CubeViewerWidget(QWidget):
|
|
208
|
-
def __init__(self, parent_window, dock_widget, grid, data_max=1.0):
|
|
209
|
-
super().__init__(parent_window)
|
|
210
|
-
self.mw = parent_window
|
|
211
|
-
self.dock = dock_widget
|
|
212
|
-
self.grid = grid
|
|
213
|
-
# Ensure we have a reasonable positive max value
|
|
214
|
-
self.data_max = max(abs(float(data_max)), 1e-6)
|
|
215
|
-
|
|
216
|
-
self.iso_actor_p = None
|
|
217
|
-
self.iso_actor_n = None
|
|
218
|
-
|
|
219
|
-
self.plotter = self.mw.plotter
|
|
220
|
-
|
|
221
|
-
# Initial Colors
|
|
222
|
-
self.color_p = "#0000FF" # Blue
|
|
223
|
-
self.color_n = "#FF0000" # Red
|
|
224
|
-
|
|
225
|
-
self.init_ui()
|
|
226
|
-
self.update_iso()
|
|
227
|
-
|
|
228
|
-
def init_ui(self):
|
|
229
|
-
layout = QVBoxLayout()
|
|
230
|
-
|
|
231
|
-
# --- Isovalue Controls ---
|
|
232
|
-
ctrl_layout = QHBoxLayout()
|
|
233
|
-
ctrl_layout.addWidget(QLabel("Isovalue:"))
|
|
234
|
-
|
|
235
|
-
# Spinbox: Fixed max 1.0 as requested
|
|
236
|
-
# Spinbox: Dynamic max based on data
|
|
237
|
-
# We allow going a bit higher than the max found in data, e.g. 1.2x, just in case
|
|
238
|
-
self.max_val = self.data_max * 1.2
|
|
239
|
-
|
|
240
|
-
self.spin = QDoubleSpinBox()
|
|
241
|
-
self.spin.setRange(0.0001, self.max_val)
|
|
242
|
-
self.spin.setSingleStep(0.01) # User requested 0.01 step
|
|
243
|
-
self.spin.setDecimals(5)
|
|
244
|
-
|
|
245
|
-
# Default value strategy:
|
|
246
|
-
# User requested hardcoded 0.05
|
|
247
|
-
default_val = 0.05
|
|
248
|
-
|
|
249
|
-
# If default is outside the max range, adjust it
|
|
250
|
-
if default_val > self.max_val:
|
|
251
|
-
default_val = self.max_val * 0.1
|
|
252
|
-
|
|
253
|
-
self.spin.setValue(default_val)
|
|
254
|
-
|
|
255
|
-
self.spin.valueChanged.connect(self.on_spin_changed)
|
|
256
|
-
ctrl_layout.addWidget(self.spin)
|
|
257
|
-
|
|
258
|
-
# Slider
|
|
259
|
-
self.slider_max_int = 1000
|
|
260
|
-
|
|
261
|
-
self.slider = QSlider(Qt.Orientation.Horizontal)
|
|
262
|
-
self.slider.setRange(0, self.slider_max_int)
|
|
263
|
-
|
|
264
|
-
# Default 0.05
|
|
265
|
-
# Default set from spinbox value
|
|
266
|
-
self.slider.setValue(int((default_val / self.max_val) * self.slider_max_int))
|
|
267
|
-
|
|
268
|
-
self.slider.valueChanged.connect(self.on_slider_changed)
|
|
269
|
-
ctrl_layout.addWidget(self.slider)
|
|
270
|
-
|
|
271
|
-
layout.addLayout(ctrl_layout)
|
|
272
|
-
|
|
273
|
-
# --- Color Controls ---
|
|
274
|
-
# Fixed size for color buttons to align them nicely
|
|
275
|
-
btn_size = (50, 24)
|
|
276
|
-
|
|
277
|
-
# Positive
|
|
278
|
-
pos_color_layout = QHBoxLayout()
|
|
279
|
-
pos_color_layout.addWidget(QLabel("Pos Color:"))
|
|
280
|
-
self.btn_color_p = QPushButton()
|
|
281
|
-
self.btn_color_p.setFixedSize(*btn_size)
|
|
282
|
-
self.btn_color_p.setStyleSheet(f"background-color: {self.color_p}; border: 1px solid gray;")
|
|
283
|
-
self.btn_color_p.clicked.connect(self.choose_color_p)
|
|
284
|
-
pos_color_layout.addWidget(self.btn_color_p)
|
|
285
|
-
pos_color_layout.addStretch() # Align left
|
|
286
|
-
layout.addLayout(pos_color_layout)
|
|
287
|
-
|
|
288
|
-
# Negative
|
|
289
|
-
neg_color_layout = QHBoxLayout()
|
|
290
|
-
neg_color_layout.addWidget(QLabel("Neg Color:"))
|
|
291
|
-
self.btn_color_n = QPushButton()
|
|
292
|
-
self.btn_color_n.setFixedSize(*btn_size)
|
|
293
|
-
self.btn_color_n.setStyleSheet(f"background-color: {self.color_n}; border: 1px solid gray;")
|
|
294
|
-
self.btn_color_n.clicked.connect(self.choose_color_n)
|
|
295
|
-
neg_color_layout.addWidget(self.btn_color_n)
|
|
296
|
-
neg_color_layout.addStretch() # Align left
|
|
297
|
-
layout.addLayout(neg_color_layout)
|
|
298
|
-
|
|
299
|
-
# Complementary Checkbox (Next line)
|
|
300
|
-
comp_layout = QHBoxLayout()
|
|
301
|
-
self.check_comp_color = QCheckBox("Use complementary color for neg")
|
|
302
|
-
self.check_comp_color.toggled.connect(self.on_comp_color_toggled)
|
|
303
|
-
comp_layout.addWidget(self.check_comp_color)
|
|
304
|
-
comp_layout.addStretch()
|
|
305
|
-
layout.addLayout(comp_layout)
|
|
306
|
-
|
|
307
|
-
# --- Opacity Controls ---
|
|
308
|
-
opacity_layout = QHBoxLayout()
|
|
309
|
-
# Static label (no numeric value displayed)
|
|
310
|
-
self.opacity_label = QLabel("Opacity:")
|
|
311
|
-
opacity_layout.addWidget(self.opacity_label)
|
|
312
|
-
|
|
313
|
-
# Numeric opacity input: place before the slider to match isovalue controls
|
|
314
|
-
self.opacity_spin = QDoubleSpinBox()
|
|
315
|
-
self.opacity_spin.setRange(0.0, 1.0)
|
|
316
|
-
self.opacity_spin.setSingleStep(0.01)
|
|
317
|
-
self.opacity_spin.setDecimals(2)
|
|
318
|
-
self.opacity_spin.setValue(0.4)
|
|
319
|
-
self.opacity_spin.valueChanged.connect(self.on_opacity_spin_changed)
|
|
320
|
-
opacity_layout.addWidget(self.opacity_spin)
|
|
321
|
-
|
|
322
|
-
self.opacity_slider = QSlider(Qt.Orientation.Horizontal)
|
|
323
|
-
self.opacity_slider.setRange(0, 100)
|
|
324
|
-
self.opacity_slider.setValue(40) # Match spinbox
|
|
325
|
-
self.opacity_slider.valueChanged.connect(self.on_opacity_changed)
|
|
326
|
-
opacity_layout.addWidget(self.opacity_slider)
|
|
327
|
-
|
|
328
|
-
layout.addLayout(opacity_layout)
|
|
329
|
-
|
|
330
|
-
close_btn = QPushButton("Close Plugin")
|
|
331
|
-
close_btn.clicked.connect(self.close_plugin)
|
|
332
|
-
layout.addWidget(close_btn)
|
|
333
|
-
|
|
334
|
-
layout.addStretch()
|
|
335
|
-
self.setLayout(layout)
|
|
336
|
-
|
|
337
|
-
def on_comp_color_toggled(self, checked):
|
|
338
|
-
self.btn_color_n.setEnabled(not checked)
|
|
339
|
-
if checked:
|
|
340
|
-
self.update_complementary_color()
|
|
341
|
-
|
|
342
|
-
def update_complementary_color(self):
|
|
343
|
-
# Convert hex/name to QColor
|
|
344
|
-
col_p = QColor(self.color_p)
|
|
345
|
-
if not col_p.isValid():
|
|
346
|
-
return
|
|
347
|
-
|
|
348
|
-
h = col_p.hue()
|
|
349
|
-
s = col_p.saturation()
|
|
350
|
-
v = col_p.value()
|
|
351
|
-
|
|
352
|
-
# Complementary = hue + 180 degrees
|
|
353
|
-
if h != -1: # -1 means grayscale/achromatic
|
|
354
|
-
new_h = (h + 180) % 360
|
|
355
|
-
else:
|
|
356
|
-
new_h = h # Keep achromatic
|
|
357
|
-
|
|
358
|
-
col_n = QColor.fromHsv(new_h, s, v)
|
|
359
|
-
self.color_n = col_n.name()
|
|
360
|
-
self.btn_color_n.setStyleSheet(f"background-color: {self.color_n}; border: 1px solid gray;")
|
|
361
|
-
self.update_iso()
|
|
362
|
-
|
|
363
|
-
def choose_color_p(self):
|
|
364
|
-
c = QColorDialog.getColor(initial=QColor(self.color_p), title="Select Positive Lobe Color")
|
|
365
|
-
if c.isValid():
|
|
366
|
-
self.color_p = c.name()
|
|
367
|
-
self.btn_color_p.setStyleSheet(f"background-color: {self.color_p}; border: 1px solid gray;")
|
|
368
|
-
|
|
369
|
-
if self.check_comp_color.isChecked():
|
|
370
|
-
self.update_complementary_color()
|
|
371
|
-
else:
|
|
372
|
-
self.update_iso()
|
|
373
|
-
|
|
374
|
-
def choose_color_n(self):
|
|
375
|
-
c = QColorDialog.getColor(initial=QColor(self.color_n), title="Select Negative Lobe Color")
|
|
376
|
-
if c.isValid():
|
|
377
|
-
self.color_n = c.name()
|
|
378
|
-
self.btn_color_n.setStyleSheet(f"background-color: {self.color_n}; border: 1px solid gray;")
|
|
379
|
-
self.update_iso()
|
|
380
|
-
|
|
381
|
-
def update_iso(self):
|
|
382
|
-
val = self.spin.value()
|
|
383
|
-
# Prefer numeric spinbox value (kept in sync with slider)
|
|
384
|
-
opacity = self.opacity_spin.value() if hasattr(self, 'opacity_spin') else self.opacity_slider.value() / 100.0
|
|
385
|
-
|
|
386
|
-
try:
|
|
387
|
-
# Cleanup previous
|
|
388
|
-
if self.iso_actor_p: self.plotter.remove_actor(self.iso_actor_p)
|
|
389
|
-
if self.iso_actor_n: self.plotter.remove_actor(self.iso_actor_n)
|
|
390
|
-
|
|
391
|
-
# Additional safety cleanup by name
|
|
392
|
-
self.plotter.remove_actor("cube_iso_p")
|
|
393
|
-
self.plotter.remove_actor("cube_iso_n")
|
|
394
|
-
|
|
395
|
-
# Using full grid
|
|
396
|
-
using_grid = self.grid
|
|
397
|
-
|
|
398
|
-
# Positive lobe
|
|
399
|
-
iso_p = using_grid.contour(isosurfaces=[val])
|
|
400
|
-
if iso_p.n_points > 0:
|
|
401
|
-
self.iso_actor_p = self.plotter.add_mesh(iso_p, color=self.color_p, opacity=opacity, name="cube_iso_p", reset_camera=False)
|
|
402
|
-
|
|
403
|
-
# Negative lobe
|
|
404
|
-
iso_n = using_grid.contour(isosurfaces=[-val])
|
|
405
|
-
if iso_n.n_points > 0:
|
|
406
|
-
self.iso_actor_n = self.plotter.add_mesh(iso_n, color=self.color_n, opacity=opacity, name="cube_iso_n", reset_camera=False)
|
|
407
|
-
|
|
408
|
-
self.plotter.render()
|
|
409
|
-
|
|
410
|
-
except Exception as e:
|
|
411
|
-
print(f"Iso update error: {e}")
|
|
412
|
-
import traceback
|
|
413
|
-
traceback.print_exc()
|
|
414
|
-
|
|
415
|
-
def on_opacity_changed(self, val):
|
|
416
|
-
opacity = val / 100.0
|
|
417
|
-
# Sync numeric spinbox without re-triggering signals
|
|
418
|
-
if hasattr(self, 'opacity_spin'):
|
|
419
|
-
self.opacity_spin.blockSignals(True)
|
|
420
|
-
self.opacity_spin.setValue(opacity)
|
|
421
|
-
self.opacity_spin.blockSignals(False)
|
|
422
|
-
self.update_iso()
|
|
423
|
-
|
|
424
|
-
def on_opacity_spin_changed(self, val):
|
|
425
|
-
# val is between 0.0 and 1.0
|
|
426
|
-
# Sync slider (0-100)
|
|
427
|
-
if hasattr(self, 'opacity_slider'):
|
|
428
|
-
int_val = int(round(val * 100))
|
|
429
|
-
self.opacity_slider.blockSignals(True)
|
|
430
|
-
self.opacity_slider.setValue(int_val)
|
|
431
|
-
self.opacity_slider.blockSignals(False)
|
|
432
|
-
self.update_iso()
|
|
433
|
-
|
|
434
|
-
def on_slider_changed(self, val):
|
|
435
|
-
float_val = (val / self.slider_max_int) * self.max_val
|
|
436
|
-
self.spin.blockSignals(True)
|
|
437
|
-
self.spin.setValue(float_val)
|
|
438
|
-
self.spin.blockSignals(False)
|
|
439
|
-
self.update_iso()
|
|
440
|
-
|
|
441
|
-
def on_spin_changed(self, val):
|
|
442
|
-
if self.max_val > 0:
|
|
443
|
-
int_val = int((val / self.max_val) * self.slider_max_int)
|
|
444
|
-
self.slider.blockSignals(True)
|
|
445
|
-
self.slider.setValue(int_val)
|
|
446
|
-
self.slider.blockSignals(False)
|
|
447
|
-
self.update_iso()
|
|
448
|
-
|
|
449
|
-
def close_plugin(self):
|
|
450
|
-
try:
|
|
451
|
-
# Full cleanup
|
|
452
|
-
self.mw.plotter.clear()
|
|
453
|
-
self.mw.current_mol = None
|
|
454
|
-
self.mw.current_file_path = None
|
|
455
|
-
self.mw.plotter.render()
|
|
456
|
-
|
|
457
|
-
# Restore UI state
|
|
458
|
-
if hasattr(self.mw, 'restore_ui_for_editing'):
|
|
459
|
-
self.mw.restore_ui_for_editing()
|
|
460
|
-
except: pass
|
|
461
|
-
|
|
462
|
-
if self.dock:
|
|
463
|
-
self.mw.removeDockWidget(self.dock)
|
|
464
|
-
self.dock.deleteLater()
|
|
465
|
-
self.dock = None
|
|
466
|
-
|
|
467
|
-
self.deleteLater()
|
|
468
|
-
|
|
469
|
-
class ChargeDialog(QDialog):
|
|
470
|
-
def __init__(self, parent=None, current_charge=0):
|
|
471
|
-
super().__init__(parent)
|
|
472
|
-
self.setWindowTitle("Bond Connectivity Error")
|
|
473
|
-
self.result_action = "cancel" # retry, skip, cancel
|
|
474
|
-
self.charge = current_charge
|
|
475
|
-
|
|
476
|
-
self.init_ui()
|
|
477
|
-
|
|
478
|
-
def init_ui(self):
|
|
479
|
-
layout = QVBoxLayout()
|
|
480
|
-
layout.addWidget(QLabel("Could not determine connectivity with charge: " + str(self.charge)))
|
|
481
|
-
layout.addWidget(QLabel("Please specificy correct charge or skip chemistry check."))
|
|
482
|
-
|
|
483
|
-
form = QFormLayout()
|
|
484
|
-
self.spin = QSpinBox()
|
|
485
|
-
self.spin.setRange(-20, 20)
|
|
486
|
-
self.spin.setValue(self.charge)
|
|
487
|
-
form.addRow("Charge:", self.spin)
|
|
488
|
-
layout.addLayout(form)
|
|
489
|
-
|
|
490
|
-
btns = QHBoxLayout()
|
|
491
|
-
retry_btn = QPushButton("Retry")
|
|
492
|
-
retry_btn.clicked.connect(self.on_retry)
|
|
493
|
-
|
|
494
|
-
skip_btn = QPushButton("Skip Chemistry")
|
|
495
|
-
skip_btn.clicked.connect(self.on_skip)
|
|
496
|
-
|
|
497
|
-
cancel_btn = QPushButton("Cancel")
|
|
498
|
-
cancel_btn.clicked.connect(self.reject)
|
|
499
|
-
|
|
500
|
-
btns.addWidget(retry_btn)
|
|
501
|
-
btns.addWidget(skip_btn)
|
|
502
|
-
btns.addWidget(cancel_btn)
|
|
503
|
-
layout.addLayout(btns)
|
|
504
|
-
|
|
505
|
-
self.setLayout(layout)
|
|
506
|
-
|
|
507
|
-
def on_retry(self):
|
|
508
|
-
self.charge = self.spin.value()
|
|
509
|
-
self.result_action = "retry"
|
|
510
|
-
self.accept()
|
|
511
|
-
|
|
512
|
-
def on_skip(self):
|
|
513
|
-
self.result_action = "skip"
|
|
514
|
-
self.accept()
|
|
515
|
-
|
|
516
|
-
def open_cube_viewer(main_window, fname):
|
|
517
|
-
"""Core logic to open cube viewer with a specific file."""
|
|
518
|
-
if Chem is None:
|
|
519
|
-
QMessageBox.critical(main_window, "Error", "RDKit is required for this plugin.")
|
|
520
|
-
return
|
|
521
|
-
|
|
522
|
-
# Close existing docks
|
|
523
|
-
docks_to_close = []
|
|
524
|
-
for dock in main_window.findChildren(QDockWidget):
|
|
525
|
-
if dock.windowTitle() == "Cube Viewer":
|
|
526
|
-
docks_to_close.append(dock)
|
|
527
|
-
|
|
528
|
-
for dock in docks_to_close:
|
|
529
|
-
try:
|
|
530
|
-
widget = dock.widget()
|
|
531
|
-
if hasattr(widget, 'close_plugin'):
|
|
532
|
-
widget.close_plugin()
|
|
533
|
-
else:
|
|
534
|
-
main_window.removeDockWidget(dock)
|
|
535
|
-
dock.deleteLater()
|
|
536
|
-
except:
|
|
537
|
-
pass
|
|
538
|
-
|
|
539
|
-
try:
|
|
540
|
-
if not fname: # Should not happen if called correctly
|
|
541
|
-
return
|
|
542
|
-
|
|
543
|
-
try:
|
|
544
|
-
meta, grid = read_cube(fname)
|
|
545
|
-
except Exception as e:
|
|
546
|
-
import traceback
|
|
547
|
-
traceback.print_exc()
|
|
548
|
-
QMessageBox.critical(main_window, "Error", f"Failed to parse Cube file:\n{e}")
|
|
549
|
-
return
|
|
550
|
-
|
|
551
|
-
if hasattr(main_window, 'plotter'):
|
|
552
|
-
main_window.plotter.clear()
|
|
553
|
-
|
|
554
|
-
if hasattr(main_window, 'main_window_ui_manager'):
|
|
555
|
-
main_window.main_window_ui_manager._enter_3d_viewer_ui_mode()
|
|
556
|
-
|
|
557
|
-
# Create Molecule (XYZ)
|
|
558
|
-
atoms = meta['atoms']
|
|
559
|
-
xyz_lines = [f"{len(atoms)}", "Generated by Cube Plugin"]
|
|
560
|
-
pt = Chem.GetPeriodicTable()
|
|
561
|
-
for atomic_num, pos in atoms:
|
|
562
|
-
symbol = pt.GetElementSymbol(atomic_num)
|
|
563
|
-
xyz_lines.append(f"{symbol} {pos[0]:.4f} {pos[1]:.4f} {pos[2]:.4f}")
|
|
564
|
-
|
|
565
|
-
xyz_content = "\n".join(xyz_lines)
|
|
566
|
-
|
|
567
|
-
mol = Chem.MolFromXYZBlock(xyz_content)
|
|
568
|
-
|
|
569
|
-
# Use rdDetermineBonds if available and no bonds found
|
|
570
|
-
current_charge = 0
|
|
571
|
-
if mol is not None and mol.GetNumBonds() == 0 and rdDetermineBonds is not None:
|
|
572
|
-
while True:
|
|
573
|
-
# Re-create fresh molecule for this attempt to avoid dirty state on retry
|
|
574
|
-
mol = Chem.MolFromXYZBlock(xyz_content)
|
|
575
|
-
|
|
576
|
-
try:
|
|
577
|
-
rdDetermineBonds.DetermineConnectivity(mol, charge=current_charge)
|
|
578
|
-
rdDetermineBonds.DetermineBondOrders(mol, charge=current_charge)
|
|
579
|
-
break # Success
|
|
580
|
-
except Exception as e:
|
|
581
|
-
# Show Dialog
|
|
582
|
-
dlg = ChargeDialog(main_window, current_charge)
|
|
583
|
-
if dlg.exec() == QDialog.DialogCode.Accepted:
|
|
584
|
-
if dlg.result_action == "retry":
|
|
585
|
-
current_charge = dlg.charge
|
|
586
|
-
continue # Loop again with new charge
|
|
587
|
-
elif dlg.result_action == "skip":
|
|
588
|
-
# Ensure we have a clean unbonded mol
|
|
589
|
-
mol = Chem.MolFromXYZBlock(xyz_content)
|
|
590
|
-
rdDetermineBonds.DetermineConnectivity(mol, charge=0)
|
|
591
|
-
break # Break loop, accept no bonds/bad connectivity
|
|
592
|
-
else:
|
|
593
|
-
return # User Cancelled plugin load
|
|
594
|
-
|
|
595
|
-
if mol is None:
|
|
596
|
-
# Fallback
|
|
597
|
-
mol = Chem.RWMol()
|
|
598
|
-
conf = Chem.Conformer()
|
|
599
|
-
for i, (atomic_num, pos) in enumerate(atoms):
|
|
600
|
-
idx = mol.AddAtom(Chem.Atom(atomic_num))
|
|
601
|
-
conf.SetAtomPosition(idx, Geometry.Point3D(pos[0], pos[1], pos[2]))
|
|
602
|
-
mol.AddConformer(conf)
|
|
603
|
-
|
|
604
|
-
# Set current molecular data in main window for consistency
|
|
605
|
-
main_window.current_mol = mol
|
|
606
|
-
main_window.current_file_path = fname
|
|
607
|
-
|
|
608
|
-
# Draw
|
|
609
|
-
if hasattr(main_window, 'draw_molecule_3d'):
|
|
610
|
-
main_window.draw_molecule_3d(mol)
|
|
611
|
-
elif hasattr(main_window, 'main_window_view_3d'):
|
|
612
|
-
main_window.main_window_view_3d.draw_molecule_3d(mol)
|
|
613
|
-
|
|
614
|
-
# Report bonds
|
|
615
|
-
nb = mol.GetNumBonds() if mol else 0
|
|
616
|
-
if hasattr(main_window, 'statusBar'):
|
|
617
|
-
main_window.statusBar().showMessage(f"Loaded Cube. Atoms: {len(atoms)}, Bonds: {nb}")
|
|
618
|
-
|
|
619
|
-
# Setup Dock
|
|
620
|
-
dock = QDockWidget("Cube Viewer", main_window)
|
|
621
|
-
dock.setAllowedAreas(Qt.DockWidgetArea.LeftDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea)
|
|
622
|
-
|
|
623
|
-
# Calculate max absolute value in data for dynamic scaling
|
|
624
|
-
try:
|
|
625
|
-
# Data is stored in grid.point_data["values"]
|
|
626
|
-
# "make sure that the data is only in the plot data"
|
|
627
|
-
flat_data = grid.point_data["values"]
|
|
628
|
-
if len(flat_data) > 0:
|
|
629
|
-
data_max = float(np.max(np.abs(flat_data)))
|
|
630
|
-
else:
|
|
631
|
-
data_max = 1.0
|
|
632
|
-
except:
|
|
633
|
-
data_max = 1.0
|
|
634
|
-
|
|
635
|
-
viewer = CubeViewerWidget(main_window, dock, grid, data_max=data_max)
|
|
636
|
-
dock.setWidget(viewer)
|
|
637
|
-
|
|
638
|
-
main_window.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)
|
|
639
|
-
|
|
640
|
-
main_window.plotter.reset_camera()
|
|
641
|
-
|
|
642
|
-
except Exception as e:
|
|
643
|
-
import traceback
|
|
644
|
-
traceback.print_exc()
|
|
645
|
-
print(f"Plugin Error: {e}")
|
|
646
|
-
|
|
647
|
-
def run(mw):
|
|
648
|
-
if Chem is None:
|
|
649
|
-
QMessageBox.critical(mw, "Error", "RDKit is required for this plugin.")
|
|
650
|
-
return
|
|
651
|
-
|
|
652
|
-
fname, _ = QFileDialog.getOpenFileName(mw, "Open Gaussian Cube File", "", "Cube Files (*.cube *.cub);;All Files (*)")
|
|
653
|
-
if fname:
|
|
654
|
-
open_cube_viewer(mw, fname)
|
|
655
|
-
|
|
656
|
-
def initialize(context):
|
|
657
|
-
"""
|
|
658
|
-
New Plugin System Entry Point
|
|
659
|
-
"""
|
|
660
|
-
mw = context.get_main_window()
|
|
661
|
-
|
|
662
|
-
def open_cube_wrapper(fname):
|
|
663
|
-
open_cube_viewer(mw, fname)
|
|
664
|
-
|
|
665
|
-
# 1. Register File Opener (Handle File > Import)
|
|
666
|
-
context.register_file_opener('.cube', open_cube_wrapper)
|
|
667
|
-
context.register_file_opener('.cub', open_cube_wrapper)
|
|
668
|
-
|
|
669
|
-
# 2. Register Drop Handler (for robustness)
|
|
670
|
-
# The system iterates drop handlers. Return True if we handled it.
|
|
671
|
-
def drop_handler(file_path):
|
|
672
|
-
if file_path.lower().endswith(('.cube', '.cub')):
|
|
673
|
-
open_cube_viewer(mw, file_path)
|
|
674
|
-
return True
|
|
675
|
-
return False
|
|
676
|
-
|
|
677
|
-
if hasattr(context, 'register_drop_handler'):
|
|
678
|
-
context.register_drop_handler(drop_handler, priority=10)
|
|
679
|
-
|
|
680
|
-
# 4. Command Line Args (Legacy support logic moved here)
|
|
681
|
-
import sys
|
|
682
|
-
import os
|
|
683
|
-
from PyQt6.QtCore import QTimer
|
|
684
|
-
# Simple check for CLI args
|
|
685
|
-
for arg in sys.argv[1:]:
|
|
686
|
-
if arg.lower().endswith(('.cube', '.cub')) and os.path.exists(arg):
|
|
687
|
-
QTimer.singleShot(100, lambda f=arg: open_cube_viewer(mw, f))
|
|
688
|
-
break
|
|
689
|
-
|