MoleditPy 2.2.0a2__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/plugin_interface.py +1 -10
- moleditpy/modules/plugin_manager.py +0 -3
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/METADATA +1 -1
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/RECORD +10 -27
- 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 -262
- moleditpy/plugins/Utility/console.py +0 -163
- moleditpy/plugins/Utility/pubchem_ressolver.py +0 -244
- moleditpy/plugins/Utility/vdw_radii_overlay.py +0 -432
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/WHEEL +0 -0
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/entry_points.txt +0 -0
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/licenses/LICENSE +0 -0
- {moleditpy-2.2.0a2.dist-info → moleditpy-2.2.0a3.dist-info}/top_level.txt +0 -0
|
@@ -1,930 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
import os
|
|
3
|
-
from PyQt6.QtWidgets import (QMessageBox, QDialog, QVBoxLayout, QLabel,
|
|
4
|
-
QLineEdit, QSpinBox, QPushButton, QFileDialog,
|
|
5
|
-
QFormLayout, QGroupBox, QHBoxLayout, QComboBox, QTextEdit,
|
|
6
|
-
QInputDialog, QTabWidget, QCheckBox, QRadioButton, QButtonGroup,
|
|
7
|
-
QWidget, QScrollArea)
|
|
8
|
-
from PyQt6.QtGui import QPalette, QColor, QFont
|
|
9
|
-
from PyQt6.QtCore import Qt
|
|
10
|
-
from rdkit import Chem
|
|
11
|
-
import json
|
|
12
|
-
|
|
13
|
-
__version__="2025.12.16"
|
|
14
|
-
__author__="HiroYokoyama"
|
|
15
|
-
|
|
16
|
-
PLUGIN_NAME = "Gaussian Input Generator Neo"
|
|
17
|
-
SETTINGS_FILE = os.path.join(os.path.dirname(__file__), "gaussian_input_generator_neo.json")
|
|
18
|
-
|
|
19
|
-
class RouteBuilderDialog(QDialog):
|
|
20
|
-
"""
|
|
21
|
-
Dialog to construct the Gaussian Job Route line.
|
|
22
|
-
"""
|
|
23
|
-
def __init__(self, parent=None, current_route=""):
|
|
24
|
-
super().__init__(parent)
|
|
25
|
-
self.setWindowTitle("Route Builder")
|
|
26
|
-
self.resize(600, 500)
|
|
27
|
-
self.ui_ready = False # Flag to prevent premature updates
|
|
28
|
-
self.current_route = current_route
|
|
29
|
-
self.setup_ui()
|
|
30
|
-
self.parse_route(current_route)
|
|
31
|
-
|
|
32
|
-
def setup_ui(self):
|
|
33
|
-
layout = QVBoxLayout()
|
|
34
|
-
|
|
35
|
-
self.tabs = QTabWidget()
|
|
36
|
-
|
|
37
|
-
# --- Tab 1: Method & Basis ---
|
|
38
|
-
self.tab_method = QWidget()
|
|
39
|
-
self.setup_method_tab()
|
|
40
|
-
self.tabs.addTab(self.tab_method, "Method/Basis")
|
|
41
|
-
|
|
42
|
-
# --- Tab 2: Job Type ---
|
|
43
|
-
self.tab_job = QWidget()
|
|
44
|
-
self.setup_job_tab()
|
|
45
|
-
self.tabs.addTab(self.tab_job, "Job Type")
|
|
46
|
-
|
|
47
|
-
# --- Tab 3: Solvation & Disp ---
|
|
48
|
-
self.tab_solvation = QWidget()
|
|
49
|
-
self.setup_solvation_tab()
|
|
50
|
-
self.tabs.addTab(self.tab_solvation, "Solvation/Dispersion")
|
|
51
|
-
|
|
52
|
-
# --- Tab 4: Properties ---
|
|
53
|
-
self.tab_props = QWidget()
|
|
54
|
-
self.setup_props_tab()
|
|
55
|
-
self.tabs.addTab(self.tab_props, "Properties")
|
|
56
|
-
|
|
57
|
-
layout.addWidget(self.tabs)
|
|
58
|
-
|
|
59
|
-
# --- Preview ---
|
|
60
|
-
preview_group = QGroupBox("Route Preview")
|
|
61
|
-
preview_layout = QVBoxLayout()
|
|
62
|
-
self.preview_label = QLabel()
|
|
63
|
-
self.preview_label.setWordWrap(True)
|
|
64
|
-
self.preview_label.setStyleSheet("font-weight: bold; color: blue; font-size: 14px;")
|
|
65
|
-
preview_layout.addWidget(self.preview_label)
|
|
66
|
-
preview_group.setLayout(preview_layout)
|
|
67
|
-
layout.addWidget(preview_group)
|
|
68
|
-
|
|
69
|
-
# --- Buttons ---
|
|
70
|
-
btn_layout = QHBoxLayout()
|
|
71
|
-
self.btn_ok = QPushButton("Apply to Job")
|
|
72
|
-
self.btn_ok.clicked.connect(self.accept)
|
|
73
|
-
self.btn_cancel = QPushButton("Cancel")
|
|
74
|
-
self.btn_cancel.clicked.connect(self.reject)
|
|
75
|
-
btn_layout.addStretch()
|
|
76
|
-
btn_layout.addWidget(self.btn_ok)
|
|
77
|
-
btn_layout.addWidget(self.btn_cancel)
|
|
78
|
-
layout.addLayout(btn_layout)
|
|
79
|
-
|
|
80
|
-
self.setLayout(layout)
|
|
81
|
-
|
|
82
|
-
# Connect signals to update preview
|
|
83
|
-
self.connect_signals()
|
|
84
|
-
|
|
85
|
-
self.ui_ready = True # Enable updates
|
|
86
|
-
self.update_preview()
|
|
87
|
-
|
|
88
|
-
def setup_method_tab(self):
|
|
89
|
-
layout = QFormLayout()
|
|
90
|
-
|
|
91
|
-
# Output Level
|
|
92
|
-
self.output_level = QComboBox()
|
|
93
|
-
self.output_level.addItems(["Additional Output (#P)", "Standard Output (#)", "Terse Output (#T)"])
|
|
94
|
-
# Default to # (Standard)
|
|
95
|
-
self.output_level.setCurrentIndex(1)
|
|
96
|
-
self.output_level.currentIndexChanged.connect(self.update_preview)
|
|
97
|
-
layout.addRow("Print Level:", self.output_level)
|
|
98
|
-
|
|
99
|
-
# Method Type
|
|
100
|
-
self.method_type = QComboBox()
|
|
101
|
-
self.method_type.addItems(["DFT", "Double Hybrid", "MP2", "Hartree-Fock", "Semi-Empirical", "Other"])
|
|
102
|
-
self.method_type.currentIndexChanged.connect(self.update_method_list)
|
|
103
|
-
layout.addRow("Method Type:", self.method_type)
|
|
104
|
-
|
|
105
|
-
# Method Name
|
|
106
|
-
self.method_name = QComboBox()
|
|
107
|
-
# Initial population
|
|
108
|
-
# Note: update_method_list calls update_preview, which calls currentText()
|
|
109
|
-
# We need to make sure update_preview guards against uninitialized widgets
|
|
110
|
-
self.update_method_list()
|
|
111
|
-
layout.addRow("Method:", self.method_name)
|
|
112
|
-
|
|
113
|
-
# Basis Set
|
|
114
|
-
self.basis_set = QComboBox()
|
|
115
|
-
basis_groups = [
|
|
116
|
-
"6-31G(d)", "6-31+G(d,p)", "6-311G(d,p)", "6-311+G(d,p)", # Pople
|
|
117
|
-
"cc-pVDZ", "cc-pVTZ", "aug-cc-pVDZ", "aug-cc-pVTZ", # Dunning
|
|
118
|
-
"def2SVP", "def2TZVP", "def2QZVP", # Karlsruhe
|
|
119
|
-
"LanL2DZ", "SDD", "Gen", "GenECP" # ECP/Gen
|
|
120
|
-
]
|
|
121
|
-
self.basis_set.addItems(basis_groups)
|
|
122
|
-
layout.addRow("Basis Set:", self.basis_set)
|
|
123
|
-
|
|
124
|
-
self.tab_method.setLayout(layout)
|
|
125
|
-
|
|
126
|
-
def update_method_list(self):
|
|
127
|
-
mtype = self.method_type.currentText()
|
|
128
|
-
self.method_name.blockSignals(True)
|
|
129
|
-
self.method_name.clear()
|
|
130
|
-
|
|
131
|
-
if mtype == "DFT":
|
|
132
|
-
self.method_name.addItems([
|
|
133
|
-
"B3LYP", "WB97XD", "M062X", "PBE0", "CAM-B3LYP", "APFD", "B97D3", "TPSSTPSS",
|
|
134
|
-
"MN15", "MN15L", "BHandHLYP"
|
|
135
|
-
])
|
|
136
|
-
elif mtype == "Double Hybrid":
|
|
137
|
-
self.method_name.addItems(["B2PLYP"])
|
|
138
|
-
elif mtype == "MP2":
|
|
139
|
-
self.method_name.addItems(["MP2", "MP3", "MP4", "CCSD", "CCSD(T)"])
|
|
140
|
-
elif mtype == "Hartree-Fock":
|
|
141
|
-
self.method_name.addItems(["HF", "ROHF", "UHF"])
|
|
142
|
-
elif mtype == "Semi-Empirical":
|
|
143
|
-
self.method_name.addItems(["AM1", "PM6", "PM7", "ZINDO"])
|
|
144
|
-
else:
|
|
145
|
-
self.method_name.addItem("Custom")
|
|
146
|
-
|
|
147
|
-
self.method_name.blockSignals(False)
|
|
148
|
-
self.update_preview()
|
|
149
|
-
|
|
150
|
-
def setup_job_tab(self):
|
|
151
|
-
layout = QVBoxLayout()
|
|
152
|
-
|
|
153
|
-
self.job_type = QComboBox()
|
|
154
|
-
self.job_type.addItems([
|
|
155
|
-
"Optimization + Freq (Opt Freq)",
|
|
156
|
-
"Optimization Only (Opt)",
|
|
157
|
-
"Frequency Only (Freq)",
|
|
158
|
-
"Single Point Energy (SP)",
|
|
159
|
-
"Scan (ModRedundant)",
|
|
160
|
-
"IRC",
|
|
161
|
-
"Stability Analysis (Stable)",
|
|
162
|
-
"Volume"
|
|
163
|
-
])
|
|
164
|
-
layout.addWidget(QLabel("Job Task:"))
|
|
165
|
-
layout.addWidget(self.job_type)
|
|
166
|
-
self.job_type.currentIndexChanged.connect(self.update_job_options_visibility)
|
|
167
|
-
|
|
168
|
-
# Opt Options
|
|
169
|
-
self.opt_group = QGroupBox("Optimization Options")
|
|
170
|
-
opt_layout = QHBoxLayout()
|
|
171
|
-
self.opt_tight = QCheckBox("Tight")
|
|
172
|
-
self.opt_verytight = QCheckBox("VeryTight")
|
|
173
|
-
self.opt_calcfc = QCheckBox("CalcFC")
|
|
174
|
-
self.opt_maxcycles = QCheckBox("MaxCycles=100")
|
|
175
|
-
opt_layout.addWidget(self.opt_tight)
|
|
176
|
-
opt_layout.addWidget(self.opt_verytight)
|
|
177
|
-
opt_layout.addWidget(self.opt_calcfc)
|
|
178
|
-
opt_layout.addWidget(self.opt_maxcycles)
|
|
179
|
-
self.opt_group.setLayout(opt_layout)
|
|
180
|
-
layout.addWidget(self.opt_group)
|
|
181
|
-
|
|
182
|
-
# Freq Options
|
|
183
|
-
self.freq_group = QGroupBox("Freq Options")
|
|
184
|
-
freq_layout = QHBoxLayout()
|
|
185
|
-
self.freq_raman = QCheckBox("Raman")
|
|
186
|
-
self.freq_anharm = QCheckBox("Anharmonic")
|
|
187
|
-
freq_layout.addWidget(self.freq_raman)
|
|
188
|
-
freq_layout.addWidget(self.freq_anharm)
|
|
189
|
-
self.freq_group.setLayout(freq_layout)
|
|
190
|
-
layout.addWidget(self.freq_group)
|
|
191
|
-
|
|
192
|
-
layout.addStretch()
|
|
193
|
-
self.tab_job.setLayout(layout)
|
|
194
|
-
|
|
195
|
-
# Initial visibility update
|
|
196
|
-
self.update_job_options_visibility()
|
|
197
|
-
|
|
198
|
-
def update_job_options_visibility(self):
|
|
199
|
-
job_idx = self.job_type.currentIndex()
|
|
200
|
-
txt = self.job_type.currentText()
|
|
201
|
-
|
|
202
|
-
# Opt options: Show for Opt, Opt+Freq, Scan, IRC, Stable, Volume (some imply optimization)
|
|
203
|
-
# Strictly speaking: Opt tasks.
|
|
204
|
-
# 0: Opt+Freq, 1: Opt, 4: Scan (ModRedundant), 5: IRC
|
|
205
|
-
is_opt = job_idx in [0, 1, 4, 5]
|
|
206
|
-
self.opt_group.setVisible(is_opt)
|
|
207
|
-
|
|
208
|
-
# Freq options: Show for Opt+Freq, Freq
|
|
209
|
-
# 0: Opt+Freq, 2: Freq
|
|
210
|
-
is_freq = job_idx in [0, 2]
|
|
211
|
-
self.freq_group.setVisible(is_freq)
|
|
212
|
-
|
|
213
|
-
def setup_solvation_tab(self):
|
|
214
|
-
layout = QFormLayout()
|
|
215
|
-
|
|
216
|
-
self.solv_model = QComboBox()
|
|
217
|
-
self.solv_model.addItems(["None", "PCM", "CPCM", "SMD", "IEFPCM"])
|
|
218
|
-
layout.addRow("Solvation Model:", self.solv_model)
|
|
219
|
-
|
|
220
|
-
self.solvent = QComboBox()
|
|
221
|
-
solvents = [
|
|
222
|
-
"Water", "Acetonitrile", "Methanol", "Ethanol",
|
|
223
|
-
"Chloroform", "Dichloromethane", "Toluene",
|
|
224
|
-
"Tetrahydrofuran", "DimethylSulfoxide", "Cyclohexane",
|
|
225
|
-
"Benzene", "Acetone"
|
|
226
|
-
]
|
|
227
|
-
self.solvent.addItems(solvents)
|
|
228
|
-
layout.addRow("Solvent:", self.solvent)
|
|
229
|
-
|
|
230
|
-
layout.addRow(QLabel(" ")) # Spacer
|
|
231
|
-
|
|
232
|
-
self.dispersion = QCheckBox("EmpiricalDispersion=GD3BJ")
|
|
233
|
-
layout.addRow("Dispersion:", self.dispersion)
|
|
234
|
-
|
|
235
|
-
self.tab_solvation.setLayout(layout)
|
|
236
|
-
|
|
237
|
-
def setup_props_tab(self):
|
|
238
|
-
layout = QFormLayout()
|
|
239
|
-
|
|
240
|
-
self.pop_analysis = QComboBox()
|
|
241
|
-
self.pop_analysis.addItems(["None", "NBO (Pop=NBO)", "Hirshfeld", "MK (Merz-Kollman)", "Regular (Pop=Reg)"])
|
|
242
|
-
layout.addRow("Population Analysis:", self.pop_analysis)
|
|
243
|
-
|
|
244
|
-
self.density_chk = QCheckBox("Density=Current")
|
|
245
|
-
layout.addRow(QLabel("Density:"), self.density_chk)
|
|
246
|
-
|
|
247
|
-
self.symmetry_combo = QComboBox()
|
|
248
|
-
self.symmetry_combo.addItems(["Default", "Loose", "None (NoSymm)"])
|
|
249
|
-
layout.addRow("Symmetry:", self.symmetry_combo)
|
|
250
|
-
|
|
251
|
-
self.grid_combo = QComboBox()
|
|
252
|
-
self.grid_combo.addItems(["Default", "FineGrid", "UltraFine", "SuperFine"])
|
|
253
|
-
layout.addRow("Integration Grid:", self.grid_combo)
|
|
254
|
-
|
|
255
|
-
# TD-DFT Section
|
|
256
|
-
td_group = QGroupBox("Excited States (TD-DFT)")
|
|
257
|
-
td_layout = QHBoxLayout()
|
|
258
|
-
self.td_chk = QCheckBox("Enable TD")
|
|
259
|
-
self.td_nstates = QSpinBox()
|
|
260
|
-
self.td_nstates.setValue(6)
|
|
261
|
-
self.td_nstates.setPrefix("NStates=")
|
|
262
|
-
td_layout.addWidget(self.td_chk)
|
|
263
|
-
td_layout.addWidget(self.td_nstates)
|
|
264
|
-
td_group.setLayout(td_layout)
|
|
265
|
-
layout.addRow(td_group)
|
|
266
|
-
|
|
267
|
-
self.tab_props.setLayout(layout)
|
|
268
|
-
|
|
269
|
-
def connect_signals(self):
|
|
270
|
-
# Connect all widgets to update_preview
|
|
271
|
-
widgets = [
|
|
272
|
-
self.method_type, self.method_name, self.basis_set,
|
|
273
|
-
self.job_type, self.opt_tight, self.opt_verytight, self.opt_calcfc, self.opt_maxcycles,
|
|
274
|
-
self.freq_raman, self.freq_anharm,
|
|
275
|
-
self.solv_model, self.solvent, self.dispersion,
|
|
276
|
-
self.pop_analysis, self.density_chk, self.symmetry_combo, self.grid_combo,
|
|
277
|
-
self.td_chk, self.td_nstates
|
|
278
|
-
]
|
|
279
|
-
for w in widgets:
|
|
280
|
-
if isinstance(w, QComboBox):
|
|
281
|
-
w.currentIndexChanged.connect(self.update_preview)
|
|
282
|
-
elif isinstance(w, QCheckBox):
|
|
283
|
-
w.toggled.connect(self.update_preview)
|
|
284
|
-
elif isinstance(w, QSpinBox):
|
|
285
|
-
w.valueChanged.connect(self.update_preview)
|
|
286
|
-
|
|
287
|
-
def update_preview(self):
|
|
288
|
-
if not getattr(self, 'ui_ready', False):
|
|
289
|
-
return
|
|
290
|
-
|
|
291
|
-
# Output Level
|
|
292
|
-
lvl_map = {0: "#P", 1: "#", 2: "#T"}
|
|
293
|
-
prefix = lvl_map.get(self.output_level.currentIndex(), "#P")
|
|
294
|
-
route_parts = [prefix]
|
|
295
|
-
|
|
296
|
-
# Method / Basis
|
|
297
|
-
method = self.method_name.currentText()
|
|
298
|
-
basis = self.basis_set.currentText()
|
|
299
|
-
route_parts.append(f"{method}/{basis}")
|
|
300
|
-
|
|
301
|
-
# Job Type
|
|
302
|
-
job_idx = self.job_type.currentIndex()
|
|
303
|
-
if job_idx == 0: route_parts.extend(["Opt", "Freq"])
|
|
304
|
-
elif job_idx == 1: route_parts.append("Opt")
|
|
305
|
-
elif job_idx == 2: route_parts.append("Freq")
|
|
306
|
-
elif job_idx == 3: route_parts.append("SP")
|
|
307
|
-
elif job_idx == 4: route_parts.append("Scan") # ModRedundant usually implies Opt=ModRedundant but simple Scan is obsolete
|
|
308
|
-
elif job_idx == 5: route_parts.append("IRC")
|
|
309
|
-
elif job_idx == 6: route_parts.append("Stable")
|
|
310
|
-
elif job_idx == 7: route_parts.append("Volume")
|
|
311
|
-
|
|
312
|
-
# Opt Options
|
|
313
|
-
opt_opts = []
|
|
314
|
-
if self.opt_tight.isChecked(): opt_opts.append("Tight")
|
|
315
|
-
if self.opt_verytight.isChecked(): opt_opts.append("VeryTight")
|
|
316
|
-
if self.opt_calcfc.isChecked(): opt_opts.append("CalcFC")
|
|
317
|
-
if self.opt_maxcycles.isChecked(): opt_opts.append("MaxCycles=100")
|
|
318
|
-
|
|
319
|
-
# If Opt is in route and we have options, combine them
|
|
320
|
-
# Note: In Gaussian, it's usually Opt=(Tight, CalcFC)
|
|
321
|
-
# But if we just appended "Opt", we need to replace it or append.
|
|
322
|
-
# Simplified approach: If Opt is present, modify the Opt string.
|
|
323
|
-
if "Opt" in route_parts and opt_opts:
|
|
324
|
-
# Find index of "Opt"
|
|
325
|
-
idx = route_parts.index("Opt")
|
|
326
|
-
route_parts[idx] = f"Opt=({', '.join(opt_opts)})"
|
|
327
|
-
|
|
328
|
-
# Freq Options
|
|
329
|
-
freq_opts = []
|
|
330
|
-
if self.freq_raman.isChecked(): freq_opts.append("Raman")
|
|
331
|
-
if self.freq_anharm.isChecked(): freq_opts.append("Anharmonic")
|
|
332
|
-
|
|
333
|
-
if "Freq" in route_parts and freq_opts:
|
|
334
|
-
idx = route_parts.index("Freq")
|
|
335
|
-
route_parts[idx] = f"Freq=({', '.join(freq_opts)})"
|
|
336
|
-
|
|
337
|
-
# Solvation
|
|
338
|
-
solv = self.solv_model.currentText()
|
|
339
|
-
if solv != "None":
|
|
340
|
-
solvent = self.solvent.currentText()
|
|
341
|
-
scrf_opts = [f"{solv}", f"Solvent={solvent}"]
|
|
342
|
-
route_parts.append(f"SCRF=({', '.join(scrf_opts)})")
|
|
343
|
-
|
|
344
|
-
# Dispersion
|
|
345
|
-
if self.dispersion.isChecked():
|
|
346
|
-
route_parts.append("EmpiricalDispersion=GD3BJ")
|
|
347
|
-
|
|
348
|
-
# Properties
|
|
349
|
-
pop = self.pop_analysis.currentText()
|
|
350
|
-
if "NBO" in pop: route_parts.append("Pop=NBO")
|
|
351
|
-
elif "Hirshfeld" in pop: route_parts.append("Pop=Hirshfeld")
|
|
352
|
-
elif "MK" in pop: route_parts.append("Pop=MK")
|
|
353
|
-
elif "Reg" in pop: route_parts.append("Pop=Reg")
|
|
354
|
-
|
|
355
|
-
if self.density_chk.isChecked():
|
|
356
|
-
route_parts.append("Density=Current")
|
|
357
|
-
|
|
358
|
-
sym = self.symmetry_combo.currentText()
|
|
359
|
-
if "Loose" in sym: route_parts.append("Symmetry=Loose")
|
|
360
|
-
elif "None" in sym: route_parts.append("NoSymm")
|
|
361
|
-
|
|
362
|
-
grid = self.grid_combo.currentText()
|
|
363
|
-
if grid != "Default":
|
|
364
|
-
route_parts.append(f"Integral({grid})")
|
|
365
|
-
|
|
366
|
-
# TD-DFT
|
|
367
|
-
if self.td_chk.isChecked():
|
|
368
|
-
route_parts.append(f"TD=(NStates={self.td_nstates.value()})")
|
|
369
|
-
|
|
370
|
-
self.preview_str = " ".join(route_parts)
|
|
371
|
-
self.preview_label.setText(self.preview_str)
|
|
372
|
-
|
|
373
|
-
def get_route(self):
|
|
374
|
-
return self.preview_str
|
|
375
|
-
|
|
376
|
-
def parse_route(self, route):
|
|
377
|
-
# Very basic parsing to try and populate fields from an existing string
|
|
378
|
-
# This is hard to do perfectly, so we might just best-effort match.
|
|
379
|
-
# For now, we will leave everything as default if loading,
|
|
380
|
-
# or maybe we can try to at least Find B3LYP or Opt.
|
|
381
|
-
# Since this is "Advanced", users might type weird things.
|
|
382
|
-
# Let's just try to match Method/Basis/Job.
|
|
383
|
-
|
|
384
|
-
route = route.upper()
|
|
385
|
-
|
|
386
|
-
# Check Job Type
|
|
387
|
-
if "OPT" in route and "FREQ" in route: self.job_type.setCurrentIndex(0)
|
|
388
|
-
elif "OPT" in route: self.job_type.setCurrentIndex(1)
|
|
389
|
-
elif "FREQ" in route: self.job_type.setCurrentIndex(2)
|
|
390
|
-
elif "SP" in route: self.job_type.setCurrentIndex(3)
|
|
391
|
-
elif "SCAN" in route: self.job_type.setCurrentIndex(4)
|
|
392
|
-
elif "IRC" in route: self.job_type.setCurrentIndex(5)
|
|
393
|
-
elif "STABLE" in route: self.job_type.setCurrentIndex(6)
|
|
394
|
-
elif "VOLUME" in route: self.job_type.setCurrentIndex(7)
|
|
395
|
-
|
|
396
|
-
# Check Method (Just a few examples)
|
|
397
|
-
if "WB97XD" in route:
|
|
398
|
-
self.method_type.setCurrentText("DFT")
|
|
399
|
-
self.method_name.setCurrentText("WB97XD")
|
|
400
|
-
elif "B3LYP" in route:
|
|
401
|
-
self.method_type.setCurrentText("DFT")
|
|
402
|
-
self.method_name.setCurrentText("B3LYP")
|
|
403
|
-
elif "M062X" in route:
|
|
404
|
-
self.method_type.setCurrentText("DFT")
|
|
405
|
-
self.method_name.setCurrentText("M062X")
|
|
406
|
-
elif "HF" in route:
|
|
407
|
-
self.method_type.setCurrentText("Hartree-Fock")
|
|
408
|
-
self.method_name.setCurrentText("HF")
|
|
409
|
-
|
|
410
|
-
# Check Basis
|
|
411
|
-
if "6-31G(D)" in route: self.basis_set.setCurrentText("6-31G(d)")
|
|
412
|
-
elif "CC-PVTZ" in route: self.basis_set.setCurrentText("cc-pVTZ")
|
|
413
|
-
elif "DEF2TZVP" in route: self.basis_set.setCurrentText("def2TZVP")
|
|
414
|
-
elif "GEN" in route: self.basis_set.setCurrentText("Gen")
|
|
415
|
-
|
|
416
|
-
# Solvation
|
|
417
|
-
if "SCRF" in route:
|
|
418
|
-
if "SMD" in route: self.solv_model.setCurrentText("SMD")
|
|
419
|
-
elif "CPCM" in route: self.solv_model.setCurrentText("CPCM")
|
|
420
|
-
else: self.solv_model.setCurrentText("PCM")
|
|
421
|
-
|
|
422
|
-
class GaussianSetupDialog(QDialog):
|
|
423
|
-
"""
|
|
424
|
-
Gaussianインプット作成のための高機能設定ダイアログ
|
|
425
|
-
Link 0, Route, Title, Charge/Mult, および追記データに対応
|
|
426
|
-
"""
|
|
427
|
-
def __init__(self, parent=None, mol=None):
|
|
428
|
-
super().__init__(parent)
|
|
429
|
-
self.setWindowTitle("Gaussian Input Setup")
|
|
430
|
-
self.resize(500, 600) # 少し大きめに設定
|
|
431
|
-
self.mol = mol
|
|
432
|
-
self.setup_ui()
|
|
433
|
-
|
|
434
|
-
def setup_ui(self):
|
|
435
|
-
main_layout = QVBoxLayout()
|
|
436
|
-
|
|
437
|
-
# --- 0. Preset Management ---
|
|
438
|
-
preset_group = QGroupBox("Preset Management")
|
|
439
|
-
preset_layout = QHBoxLayout()
|
|
440
|
-
|
|
441
|
-
self.preset_combo = QComboBox()
|
|
442
|
-
self.preset_combo.currentIndexChanged.connect(self.apply_selected_preset)
|
|
443
|
-
preset_layout.addWidget(QLabel("Preset:"))
|
|
444
|
-
preset_layout.addWidget(self.preset_combo, 1)
|
|
445
|
-
|
|
446
|
-
self.btn_save_preset = QPushButton("Save New...")
|
|
447
|
-
self.btn_save_preset.clicked.connect(self.save_preset_dialog)
|
|
448
|
-
preset_layout.addWidget(self.btn_save_preset)
|
|
449
|
-
|
|
450
|
-
self.btn_del_preset = QPushButton("Delete")
|
|
451
|
-
self.btn_del_preset.clicked.connect(self.delete_preset)
|
|
452
|
-
preset_layout.addWidget(self.btn_del_preset)
|
|
453
|
-
|
|
454
|
-
preset_group.setLayout(preset_layout)
|
|
455
|
-
main_layout.addWidget(preset_group)
|
|
456
|
-
|
|
457
|
-
# --- 1. Link 0 Section (System Resources) ---
|
|
458
|
-
link0_group = QGroupBox("Link 0 Commands (System Resources)")
|
|
459
|
-
link0_layout = QFormLayout()
|
|
460
|
-
|
|
461
|
-
# Memory
|
|
462
|
-
mem_layout = QHBoxLayout()
|
|
463
|
-
self.mem_spin = QSpinBox()
|
|
464
|
-
self.mem_spin.setRange(1, 999999)
|
|
465
|
-
self.mem_spin.setValue(4) # Default
|
|
466
|
-
self.mem_unit = QComboBox()
|
|
467
|
-
self.mem_unit.addItems(["GB", "MB", "MW"])
|
|
468
|
-
mem_layout.addWidget(self.mem_spin)
|
|
469
|
-
mem_layout.addWidget(self.mem_unit)
|
|
470
|
-
link0_layout.addRow("Memory (%mem):", mem_layout)
|
|
471
|
-
|
|
472
|
-
# Processors
|
|
473
|
-
self.nproc_spin = QSpinBox()
|
|
474
|
-
self.nproc_spin.setRange(1, 128)
|
|
475
|
-
self.nproc_spin.setValue(4) # Default
|
|
476
|
-
link0_layout.addRow("Processors (%nprocshared):", self.nproc_spin)
|
|
477
|
-
|
|
478
|
-
# Checkpoint (Optional override)
|
|
479
|
-
self.chk_edit = QLineEdit()
|
|
480
|
-
self.chk_edit.setPlaceholderText("Auto-generated from filename if empty")
|
|
481
|
-
link0_layout.addRow("Checkpoint (%chk):", self.chk_edit)
|
|
482
|
-
|
|
483
|
-
link0_group.setLayout(link0_layout)
|
|
484
|
-
main_layout.addWidget(link0_group)
|
|
485
|
-
|
|
486
|
-
# --- 2. Job Specification (Route & Title) ---
|
|
487
|
-
job_group = QGroupBox("Job Specification")
|
|
488
|
-
job_layout = QFormLayout()
|
|
489
|
-
|
|
490
|
-
# Route section
|
|
491
|
-
# Route section
|
|
492
|
-
route_layout = QHBoxLayout()
|
|
493
|
-
self.keywords_edit = QLineEdit("#P B3LYP/6-31G(d) Opt Freq")
|
|
494
|
-
self.btn_route_builder = QPushButton("Route Builder...")
|
|
495
|
-
self.btn_route_builder.clicked.connect(self.open_route_builder)
|
|
496
|
-
route_layout.addWidget(self.keywords_edit)
|
|
497
|
-
route_layout.addWidget(self.btn_route_builder)
|
|
498
|
-
job_layout.addRow("Route Section (#):", route_layout)
|
|
499
|
-
|
|
500
|
-
# Title
|
|
501
|
-
self.title_edit = QLineEdit("Generated by MoleditPy Plugin")
|
|
502
|
-
job_layout.addRow("Title:", self.title_edit)
|
|
503
|
-
|
|
504
|
-
job_group.setLayout(job_layout)
|
|
505
|
-
main_layout.addWidget(job_group)
|
|
506
|
-
|
|
507
|
-
# --- 3. Molecular State ---
|
|
508
|
-
mol_group = QGroupBox("Molecular Specification")
|
|
509
|
-
mol_layout = QHBoxLayout()
|
|
510
|
-
|
|
511
|
-
self.charge_spin = QSpinBox()
|
|
512
|
-
self.charge_spin.setRange(-10, 10)
|
|
513
|
-
self.charge_spin.valueChanged.connect(self.validate_charge_mult)
|
|
514
|
-
|
|
515
|
-
# Save default palette for restoration
|
|
516
|
-
self.default_palette = self.charge_spin.palette()
|
|
517
|
-
|
|
518
|
-
# Multiplicity
|
|
519
|
-
self.mult_spin = QSpinBox()
|
|
520
|
-
self.mult_spin.setRange(1, 10)
|
|
521
|
-
self.mult_spin.valueChanged.connect(self.validate_charge_mult)
|
|
522
|
-
|
|
523
|
-
mol_layout.addWidget(QLabel("Charge:"))
|
|
524
|
-
mol_layout.addWidget(self.charge_spin)
|
|
525
|
-
mol_layout.addWidget(QLabel("Multiplicity:"))
|
|
526
|
-
mol_layout.addWidget(self.mult_spin)
|
|
527
|
-
|
|
528
|
-
mol_group.setLayout(mol_layout)
|
|
529
|
-
main_layout.addWidget(mol_group)
|
|
530
|
-
|
|
531
|
-
# Calculate initial values
|
|
532
|
-
self.calc_initial_charge_mult()
|
|
533
|
-
|
|
534
|
-
# --- 4. Additional Input (The "Everything" section) ---
|
|
535
|
-
tail_group = QGroupBox("Additional Input (Post-Coordinates)")
|
|
536
|
-
tail_layout = QVBoxLayout()
|
|
537
|
-
|
|
538
|
-
help_label = QLabel("For ModRedundant, Basis Sets (Gen), or Link1.\nAppended after the molecule specification.")
|
|
539
|
-
help_label.setStyleSheet("color: gray; font-size: 10px;")
|
|
540
|
-
tail_layout.addWidget(help_label)
|
|
541
|
-
|
|
542
|
-
self.tail_edit = QTextEdit()
|
|
543
|
-
self.tail_edit.setPlaceholderText("Example:\nD 1 2 3 4 S 10 10.0\n\nOr basis set data for Gen/GenECP...")
|
|
544
|
-
tail_layout.addWidget(self.tail_edit)
|
|
545
|
-
|
|
546
|
-
tail_group.setLayout(tail_layout)
|
|
547
|
-
main_layout.addWidget(tail_group)
|
|
548
|
-
|
|
549
|
-
# --- Save Button ---
|
|
550
|
-
# --- Save/Preview Buttons ---
|
|
551
|
-
btn_box = QHBoxLayout()
|
|
552
|
-
|
|
553
|
-
self.btn_preview = QPushButton("Preview Input")
|
|
554
|
-
self.btn_preview.clicked.connect(self.preview_file)
|
|
555
|
-
btn_box.addWidget(self.btn_preview)
|
|
556
|
-
|
|
557
|
-
self.save_btn = QPushButton("Save Input File...")
|
|
558
|
-
self.save_btn.clicked.connect(self.save_file)
|
|
559
|
-
self.save_btn.setStyleSheet("font-weight: bold; padding: 5px;")
|
|
560
|
-
btn_box.addWidget(self.save_btn)
|
|
561
|
-
|
|
562
|
-
main_layout.addLayout(btn_box)
|
|
563
|
-
|
|
564
|
-
self.setLayout(main_layout)
|
|
565
|
-
|
|
566
|
-
def get_coords_string(self):
|
|
567
|
-
"""
|
|
568
|
-
分子オブジェクトから原子座標文字列を生成する
|
|
569
|
-
"""
|
|
570
|
-
if not self.mol:
|
|
571
|
-
return ""
|
|
572
|
-
|
|
573
|
-
lines = []
|
|
574
|
-
try:
|
|
575
|
-
# RDKitの場合の座標取得
|
|
576
|
-
conf = self.mol.GetConformer()
|
|
577
|
-
for i in range(self.mol.GetNumAtoms()):
|
|
578
|
-
pos = conf.GetAtomPosition(i)
|
|
579
|
-
atom = self.mol.GetAtomWithIdx(i)
|
|
580
|
-
symbol = atom.GetSymbol()
|
|
581
|
-
# Symbol X Y Z formatted
|
|
582
|
-
lines.append(f"{symbol: <4} {pos.x: >12.6f} {pos.y: >12.6f} {pos.z: >12.6f}")
|
|
583
|
-
except Exception as e:
|
|
584
|
-
return f"Error extracting coordinates: {str(e)}"
|
|
585
|
-
|
|
586
|
-
return "\n".join(lines)
|
|
587
|
-
|
|
588
|
-
def generate_input_content(self):
|
|
589
|
-
"""Generates the full content of the input file as a string."""
|
|
590
|
-
lines = []
|
|
591
|
-
|
|
592
|
-
# --- Link 0 Section ---
|
|
593
|
-
lines.append(f"%nprocshared={self.nproc_spin.value()}")
|
|
594
|
-
lines.append(f"%mem={self.mem_spin.value()}{self.mem_unit.currentText()}")
|
|
595
|
-
|
|
596
|
-
chk_name = self.chk_edit.text().strip()
|
|
597
|
-
if chk_name and not chk_name.endswith(".chk"): chk_name += ".chk"
|
|
598
|
-
if chk_name: lines.append(f"%chk={chk_name}")
|
|
599
|
-
|
|
600
|
-
# --- Route Section ---
|
|
601
|
-
route_line = self.keywords_edit.text().strip()
|
|
602
|
-
if not route_line.startswith("#"):
|
|
603
|
-
route_line = "# " + route_line
|
|
604
|
-
lines.append(f"{route_line}")
|
|
605
|
-
lines.append("") # Blank line required
|
|
606
|
-
|
|
607
|
-
# --- Title Section ---
|
|
608
|
-
title_line = self.title_edit.text().strip()
|
|
609
|
-
if not title_line:
|
|
610
|
-
title_line = "Gaussian Job"
|
|
611
|
-
lines.append(f"{title_line}")
|
|
612
|
-
lines.append("") # Blank line required
|
|
613
|
-
|
|
614
|
-
# --- Charge & Multiplicity ---
|
|
615
|
-
lines.append(f"{self.charge_spin.value()} {self.mult_spin.value()}")
|
|
616
|
-
|
|
617
|
-
# --- Molecule Specification ---
|
|
618
|
-
coords_block = self.get_coords_string()
|
|
619
|
-
if "Error" in coords_block:
|
|
620
|
-
return f"# Error generating coordinates: {coords_block}"
|
|
621
|
-
lines.append(coords_block)
|
|
622
|
-
lines.append("") # Must end coord block with newline
|
|
623
|
-
|
|
624
|
-
# --- Additional Input (Tail) ---
|
|
625
|
-
tail_content = self.tail_edit.toPlainText()
|
|
626
|
-
if tail_content.strip():
|
|
627
|
-
# Ensure blank line before tail if needed (already added above)
|
|
628
|
-
# Actually, standard Gaussian input format:
|
|
629
|
-
# Route
|
|
630
|
-
# <blank>
|
|
631
|
-
# Title
|
|
632
|
-
# <blank>
|
|
633
|
-
# Charge Mult
|
|
634
|
-
# Coords
|
|
635
|
-
# <blank>
|
|
636
|
-
# Tail
|
|
637
|
-
# <blank>
|
|
638
|
-
lines.append(tail_content)
|
|
639
|
-
# Ensure tail ends with newline (managed by join)
|
|
640
|
-
|
|
641
|
-
# Final blank line
|
|
642
|
-
lines.append("")
|
|
643
|
-
|
|
644
|
-
return "\n".join(lines)
|
|
645
|
-
|
|
646
|
-
def preview_file(self):
|
|
647
|
-
content = self.generate_input_content()
|
|
648
|
-
|
|
649
|
-
d = QDialog(self)
|
|
650
|
-
d.setWindowTitle("Preview Input - Gaussian Neo")
|
|
651
|
-
d.resize(600, 500)
|
|
652
|
-
l = QVBoxLayout()
|
|
653
|
-
t = QTextEdit()
|
|
654
|
-
t.setPlainText(content)
|
|
655
|
-
t.setReadOnly(True)
|
|
656
|
-
t.setFont(QFont("Courier New", 10))
|
|
657
|
-
l.addWidget(t)
|
|
658
|
-
|
|
659
|
-
btn = QPushButton("Close")
|
|
660
|
-
btn.clicked.connect(d.accept)
|
|
661
|
-
l.addWidget(btn)
|
|
662
|
-
d.setLayout(l)
|
|
663
|
-
d.exec()
|
|
664
|
-
|
|
665
|
-
def save_file(self):
|
|
666
|
-
# 座標データの取得 (Check for error first)
|
|
667
|
-
coords_block = self.get_coords_string()
|
|
668
|
-
if "Error" in coords_block:
|
|
669
|
-
QMessageBox.critical(self, "Error", coords_block)
|
|
670
|
-
return
|
|
671
|
-
|
|
672
|
-
# ファイル保存ダイアログ
|
|
673
|
-
file_path, _ = QFileDialog.getSaveFileName(
|
|
674
|
-
self, "Save Gaussian Input", "", "Gaussian Input (*.gjf *.com );;All Files (*)"
|
|
675
|
-
)
|
|
676
|
-
|
|
677
|
-
if file_path:
|
|
678
|
-
try:
|
|
679
|
-
# ファイル名に基づいてchk名を決定(ユーザー入力がない場合)
|
|
680
|
-
filename_base = os.path.splitext(os.path.basename(file_path))[0]
|
|
681
|
-
chk_name = self.chk_edit.text().strip()
|
|
682
|
-
if not chk_name:
|
|
683
|
-
# Set temporary chk name for generation, but we need to update the content?
|
|
684
|
-
# The generate_input_content uses self.chk_edit.
|
|
685
|
-
# If empty, it doesn't add %chk line (or we should add logic there).
|
|
686
|
-
# Wait, old code logic: if empty, used filename_base.chk.
|
|
687
|
-
# I should replicate that logic to be safe, or just auto-fill chk_edit on save?
|
|
688
|
-
# Let's auto-fill the field if empty? Or just pass it.
|
|
689
|
-
# To be consistent with preview, maybe we should update the UI field?
|
|
690
|
-
# Or just construct content with the filename.
|
|
691
|
-
pass
|
|
692
|
-
|
|
693
|
-
# Special handling for %chk auto-generation if field is empty
|
|
694
|
-
# We want the content to have the chk name based on the file unless specified.
|
|
695
|
-
# Let's temporarily set the chk text if empty, then restore?
|
|
696
|
-
# Or pass an argument to generate_input_content.
|
|
697
|
-
# Simpler: just use generate_input_content as is.
|
|
698
|
-
# BUT, original code did: `if not chk_name: chk_name = f"{filename_base}.chk"`
|
|
699
|
-
# So if I use `generate_input_content`, I miss this feature if I don't handle it.
|
|
700
|
-
|
|
701
|
-
# Let's modify generate_input_content to handle "auto" chk?
|
|
702
|
-
# No, let's just do it here properly.
|
|
703
|
-
|
|
704
|
-
# Update CHK field if empty?
|
|
705
|
-
original_chk = self.chk_edit.text()
|
|
706
|
-
if not original_chk.strip():
|
|
707
|
-
self.chk_edit.setText(f"{filename_base}.chk")
|
|
708
|
-
|
|
709
|
-
content = self.generate_input_content()
|
|
710
|
-
|
|
711
|
-
# Restore if we want? Maybe keep it.
|
|
712
|
-
# If we keep it, the user sees what was used. Good.
|
|
713
|
-
|
|
714
|
-
with open(file_path, 'w', encoding='utf-8') as f:
|
|
715
|
-
f.write(content)
|
|
716
|
-
|
|
717
|
-
QMessageBox.information(self, "Success", f"File saved:\n{file_path}")
|
|
718
|
-
self.accept()
|
|
719
|
-
except Exception as e:
|
|
720
|
-
QMessageBox.critical(self, "Error", f"Failed to save file:\n{str(e)}")
|
|
721
|
-
|
|
722
|
-
def open_route_builder(self):
|
|
723
|
-
"""Open the Route Builder dialog."""
|
|
724
|
-
dialog = RouteBuilderDialog(self, self.keywords_edit.text())
|
|
725
|
-
if dialog.exec() == QDialog.DialogCode.Accepted:
|
|
726
|
-
self.keywords_edit.setText(dialog.get_route())
|
|
727
|
-
|
|
728
|
-
def calc_initial_charge_mult(self):
|
|
729
|
-
"""
|
|
730
|
-
Calculate default charge and multiplicity from the molecule.
|
|
731
|
-
"""
|
|
732
|
-
if not self.mol:
|
|
733
|
-
return
|
|
734
|
-
|
|
735
|
-
try:
|
|
736
|
-
# Logic taken from ORCA xyz2inp GUI
|
|
737
|
-
|
|
738
|
-
# Charge
|
|
739
|
-
# Use Chem.GetFormalCharge for consistency
|
|
740
|
-
try:
|
|
741
|
-
charge = Chem.GetFormalCharge(self.mol)
|
|
742
|
-
except:
|
|
743
|
-
# Fallback if Chem.GetFormalCharge fails
|
|
744
|
-
charge = 0
|
|
745
|
-
if hasattr(self.mol, "GetFormalCharge"):
|
|
746
|
-
charge = self.mol.GetFormalCharge()
|
|
747
|
-
|
|
748
|
-
# Multiplicity
|
|
749
|
-
# RDKit keeps track of radical electrons on atoms
|
|
750
|
-
num_radical_electrons = sum(atom.GetNumRadicalElectrons() for atom in self.mol.GetAtoms())
|
|
751
|
-
mult = num_radical_electrons + 1
|
|
752
|
-
|
|
753
|
-
# Note: The ORCA plugin logic is simple (radicals + 1).
|
|
754
|
-
# It does not force even-electron systems to be doublets explicitly if no radicals are set.
|
|
755
|
-
# But the user said "orca... works well", so we trust this primary logic.
|
|
756
|
-
|
|
757
|
-
# Set values
|
|
758
|
-
self.charge_spin.setValue(int(charge))
|
|
759
|
-
self.mult_spin.setValue(int(mult))
|
|
760
|
-
self.validate_charge_mult()
|
|
761
|
-
|
|
762
|
-
except Exception as e:
|
|
763
|
-
print(f"Error calculating charge/mult: {e}")
|
|
764
|
-
|
|
765
|
-
def validate_charge_mult(self):
|
|
766
|
-
"""
|
|
767
|
-
Validate if the charge/multiplicity combination is chemically consistent.
|
|
768
|
-
Turns input red if invalid.
|
|
769
|
-
"""
|
|
770
|
-
if not self.mol:
|
|
771
|
-
return
|
|
772
|
-
|
|
773
|
-
try:
|
|
774
|
-
charge = self.charge_spin.value()
|
|
775
|
-
mult = self.mult_spin.value()
|
|
776
|
-
|
|
777
|
-
total_protons = 0
|
|
778
|
-
for i in range(self.mol.GetNumAtoms()):
|
|
779
|
-
atom = self.mol.GetAtomWithIdx(i)
|
|
780
|
-
total_protons += atom.GetAtomicNum()
|
|
781
|
-
|
|
782
|
-
total_electrons = total_protons - charge
|
|
783
|
-
|
|
784
|
-
# Check consistency
|
|
785
|
-
# If electrons are even, multiplicity must be odd (1, 3, 5)
|
|
786
|
-
# If electrons are odd, multiplicity must be even (2, 4, 6)
|
|
787
|
-
is_valid = False
|
|
788
|
-
if total_electrons % 2 == 0:
|
|
789
|
-
if mult % 2 != 0: is_valid = True
|
|
790
|
-
else:
|
|
791
|
-
if mult % 2 == 0: is_valid = True
|
|
792
|
-
|
|
793
|
-
if is_valid:
|
|
794
|
-
self.charge_spin.setPalette(self.default_palette)
|
|
795
|
-
self.mult_spin.setPalette(self.default_palette)
|
|
796
|
-
else:
|
|
797
|
-
p = self.charge_spin.palette()
|
|
798
|
-
p.setColor(QPalette.ColorRole.Base, QColor("#FFDDDD"))
|
|
799
|
-
p.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.red)
|
|
800
|
-
self.charge_spin.setPalette(p)
|
|
801
|
-
self.mult_spin.setPalette(p)
|
|
802
|
-
|
|
803
|
-
except Exception as e:
|
|
804
|
-
print(f"Validation error: {e}")
|
|
805
|
-
|
|
806
|
-
# --- Preset Management Methods ---
|
|
807
|
-
|
|
808
|
-
def load_presets_from_file(self):
|
|
809
|
-
"""Load presets from JSON file and populate combo box."""
|
|
810
|
-
self.presets_data = {}
|
|
811
|
-
if os.path.exists(SETTINGS_FILE):
|
|
812
|
-
try:
|
|
813
|
-
with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
|
|
814
|
-
self.presets_data = json.load(f)
|
|
815
|
-
except Exception as e:
|
|
816
|
-
print(f"Error loading presets: {e}")
|
|
817
|
-
|
|
818
|
-
# Ensure Default exists if empty
|
|
819
|
-
if "Default" not in self.presets_data:
|
|
820
|
-
self.presets_data["Default"] = {
|
|
821
|
-
"nproc": 4, "mem_val": 4, "mem_unit": "GB", "chk": "",
|
|
822
|
-
"route": "# B3LYP/6-31G(d) Opt Freq",
|
|
823
|
-
"tail": ""
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
self.update_preset_combo()
|
|
827
|
-
|
|
828
|
-
def update_preset_combo(self):
|
|
829
|
-
"""Update the combobox items from self.presets_data."""
|
|
830
|
-
current = self.preset_combo.currentText()
|
|
831
|
-
self.preset_combo.blockSignals(True)
|
|
832
|
-
self.preset_combo.clear()
|
|
833
|
-
self.preset_combo.addItems(sorted(self.presets_data.keys()))
|
|
834
|
-
|
|
835
|
-
# Restore selection or select Default
|
|
836
|
-
if current in self.presets_data:
|
|
837
|
-
self.preset_combo.setCurrentText(current)
|
|
838
|
-
elif "Default" in self.presets_data:
|
|
839
|
-
self.preset_combo.setCurrentText("Default")
|
|
840
|
-
self.preset_combo.blockSignals(False)
|
|
841
|
-
|
|
842
|
-
# Apply the current selection
|
|
843
|
-
self.apply_selected_preset()
|
|
844
|
-
|
|
845
|
-
def apply_selected_preset(self):
|
|
846
|
-
"""Apply the settings from the selected preset to the UI."""
|
|
847
|
-
name = self.preset_combo.currentText()
|
|
848
|
-
if name not in self.presets_data:
|
|
849
|
-
return
|
|
850
|
-
|
|
851
|
-
data = self.presets_data[name]
|
|
852
|
-
|
|
853
|
-
# Apply values
|
|
854
|
-
self.nproc_spin.setValue(data.get("nproc", 4))
|
|
855
|
-
self.mem_spin.setValue(data.get("mem_val", 4))
|
|
856
|
-
self.mem_unit.setCurrentText(data.get("mem_unit", "GB"))
|
|
857
|
-
self.chk_edit.setText(data.get("chk", ""))
|
|
858
|
-
self.keywords_edit.setText(data.get("route", ""))
|
|
859
|
-
# DO NOT set Title, Charge, Mult (Molecule specific)
|
|
860
|
-
self.tail_edit.setPlainText(data.get("tail", ""))
|
|
861
|
-
|
|
862
|
-
def get_current_ui_settings(self):
|
|
863
|
-
"""Return a dict of current UI settings."""
|
|
864
|
-
return {
|
|
865
|
-
"nproc": self.nproc_spin.value(),
|
|
866
|
-
"mem_val": self.mem_spin.value(),
|
|
867
|
-
"mem_unit": self.mem_unit.currentText(),
|
|
868
|
-
"chk": self.chk_edit.text(),
|
|
869
|
-
"route": self.keywords_edit.text(),
|
|
870
|
-
# DO NOT save Title, Charge, Mult
|
|
871
|
-
"tail": self.tail_edit.toPlainText()
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
def save_presets_to_file(self):
|
|
875
|
-
"""Save self.presets_data to JSON file."""
|
|
876
|
-
try:
|
|
877
|
-
with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
|
|
878
|
-
json.dump(self.presets_data, f, indent=4)
|
|
879
|
-
except Exception as e:
|
|
880
|
-
QMessageBox.warning(self, "Error", f"Failed to save presets:\n{e}")
|
|
881
|
-
|
|
882
|
-
def save_preset_dialog(self):
|
|
883
|
-
"""Show dialog to save current settings as a new preset."""
|
|
884
|
-
name, ok = QInputDialog.getText(self, "Save Preset", "Preset Name:", text=self.preset_combo.currentText())
|
|
885
|
-
if ok and name:
|
|
886
|
-
name = name.strip()
|
|
887
|
-
if not name:
|
|
888
|
-
return
|
|
889
|
-
|
|
890
|
-
# Confirm overwrite
|
|
891
|
-
if name in self.presets_data:
|
|
892
|
-
ret = QMessageBox.question(self, "Overwrite", f"Preset '{name}' already exists. Overwrite?",
|
|
893
|
-
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
894
|
-
if ret != QMessageBox.StandardButton.Yes:
|
|
895
|
-
return
|
|
896
|
-
|
|
897
|
-
# Save
|
|
898
|
-
self.presets_data[name] = self.get_current_ui_settings()
|
|
899
|
-
self.save_presets_to_file()
|
|
900
|
-
self.update_preset_combo()
|
|
901
|
-
self.preset_combo.setCurrentText(name) # Select the new one
|
|
902
|
-
|
|
903
|
-
def delete_preset(self):
|
|
904
|
-
"""Delete the currently selected preset."""
|
|
905
|
-
name = self.preset_combo.currentText()
|
|
906
|
-
if name == "Default":
|
|
907
|
-
QMessageBox.warning(self, "Warning", "Cannot delete 'Default' preset.")
|
|
908
|
-
return
|
|
909
|
-
|
|
910
|
-
ret = QMessageBox.question(self, "Delete Preset", f"Are you sure you want to delete '{name}'?",
|
|
911
|
-
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
|
|
912
|
-
if ret == QMessageBox.StandardButton.Yes:
|
|
913
|
-
if name in self.presets_data:
|
|
914
|
-
del self.presets_data[name]
|
|
915
|
-
self.save_presets_to_file()
|
|
916
|
-
self.update_preset_combo()
|
|
917
|
-
|
|
918
|
-
def run(mw):
|
|
919
|
-
mol = getattr(mw, 'current_mol', None)
|
|
920
|
-
|
|
921
|
-
if not mol or mol.GetNumAtoms() == 0:
|
|
922
|
-
QMessageBox.warning(mw, PLUGIN_NAME, "No molecule loaded or molecule is empty.")
|
|
923
|
-
return
|
|
924
|
-
|
|
925
|
-
dialog = GaussianSetupDialog(parent=mw, mol=mol)
|
|
926
|
-
dialog.load_presets_from_file() # Load presets after init
|
|
927
|
-
dialog.exec()
|
|
928
|
-
|
|
929
|
-
# initialize removed as it only registered the menu action
|
|
930
|
-
|