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.
Files changed (28) hide show
  1. moleditpy/modules/constants.py +1 -1
  2. moleditpy/modules/main_window_main_init.py +31 -13
  3. moleditpy/modules/main_window_ui_manager.py +21 -2
  4. moleditpy/modules/plugin_interface.py +1 -10
  5. moleditpy/modules/plugin_manager.py +0 -3
  6. {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/METADATA +1 -1
  7. {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/RECORD +11 -28
  8. moleditpy/plugins/Analysis/ms_spectrum_neo.py +0 -919
  9. moleditpy/plugins/File/animated_xyz_giffer.py +0 -583
  10. moleditpy/plugins/File/cube_viewer.py +0 -689
  11. moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +0 -1148
  12. moleditpy/plugins/File/mapped_cube_viewer.py +0 -552
  13. moleditpy/plugins/File/orca_out_freq_analyzer.py +0 -1226
  14. moleditpy/plugins/File/paste_xyz.py +0 -336
  15. moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +0 -930
  16. moleditpy/plugins/Input Generator/orca_input_generator_neo.py +0 -1028
  17. moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +0 -286
  18. moleditpy/plugins/Optimization/all-trans_optimizer.py +0 -65
  19. moleditpy/plugins/Optimization/complex_molecule_untangler.py +0 -268
  20. moleditpy/plugins/Optimization/conf_search.py +0 -224
  21. moleditpy/plugins/Utility/atom_colorizer.py +0 -547
  22. moleditpy/plugins/Utility/console.py +0 -163
  23. moleditpy/plugins/Utility/pubchem_ressolver.py +0 -244
  24. moleditpy/plugins/Utility/vdw_radii_overlay.py +0 -303
  25. {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/WHEEL +0 -0
  26. {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/entry_points.txt +0 -0
  27. {moleditpy-2.2.0a1.dist-info → moleditpy-2.2.0a3.dist-info}/licenses/LICENSE +0 -0
  28. {moleditpy-2.2.0a1.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
-