MoleditPy 2.2.0__py3-none-any.whl → 2.2.0a1__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 +13 -31
  3. moleditpy/modules/main_window_ui_manager.py +2 -21
  4. moleditpy/modules/plugin_interface.py +10 -1
  5. moleditpy/modules/plugin_manager.py +3 -0
  6. moleditpy/plugins/Analysis/ms_spectrum_neo.py +919 -0
  7. moleditpy/plugins/File/animated_xyz_giffer.py +583 -0
  8. moleditpy/plugins/File/cube_viewer.py +689 -0
  9. moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +1148 -0
  10. moleditpy/plugins/File/mapped_cube_viewer.py +552 -0
  11. moleditpy/plugins/File/orca_out_freq_analyzer.py +1226 -0
  12. moleditpy/plugins/File/paste_xyz.py +336 -0
  13. moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +930 -0
  14. moleditpy/plugins/Input Generator/orca_input_generator_neo.py +1028 -0
  15. moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +286 -0
  16. moleditpy/plugins/Optimization/all-trans_optimizer.py +65 -0
  17. moleditpy/plugins/Optimization/complex_molecule_untangler.py +268 -0
  18. moleditpy/plugins/Optimization/conf_search.py +224 -0
  19. moleditpy/plugins/Utility/atom_colorizer.py +547 -0
  20. moleditpy/plugins/Utility/console.py +163 -0
  21. moleditpy/plugins/Utility/pubchem_ressolver.py +244 -0
  22. moleditpy/plugins/Utility/vdw_radii_overlay.py +303 -0
  23. {moleditpy-2.2.0.dist-info → moleditpy-2.2.0a1.dist-info}/METADATA +1 -1
  24. {moleditpy-2.2.0.dist-info → moleditpy-2.2.0a1.dist-info}/RECORD +28 -11
  25. {moleditpy-2.2.0.dist-info → moleditpy-2.2.0a1.dist-info}/WHEEL +0 -0
  26. {moleditpy-2.2.0.dist-info → moleditpy-2.2.0a1.dist-info}/entry_points.txt +0 -0
  27. {moleditpy-2.2.0.dist-info → moleditpy-2.2.0a1.dist-info}/licenses/LICENSE +0 -0
  28. {moleditpy-2.2.0.dist-info → moleditpy-2.2.0a1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1028 @@
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
+ QTabWidget, QCheckBox, QWidget, QScrollArea, QMenu)
7
+ from PyQt6.QtGui import QPalette, QColor, QAction, QFont
8
+ from PyQt6.QtCore import Qt
9
+ from rdkit import Chem
10
+ from rdkit.Chem import rdMolTransforms
11
+ import json
12
+
13
+ __version__="2025.12.18"
14
+ __author__="HiroYokoyama"
15
+ PLUGIN_NAME = "ORCA Input Generator Neo"
16
+ SETTINGS_FILE = os.path.join(os.path.dirname(__file__), "orca_input_generator_neo.json")
17
+
18
+ class OrcaKeywordBuilderDialog(QDialog):
19
+ """
20
+ Dialog to construct the ORCA Job Route line.
21
+ """
22
+ def __init__(self, parent=None, current_route=""):
23
+ super().__init__(parent)
24
+ self.setWindowTitle("ORCA Keyword Builder")
25
+ self.resize(700, 600)
26
+ self.ui_ready = False
27
+ self.current_route = current_route
28
+ self.setup_ui()
29
+ self.parse_route(current_route)
30
+
31
+ def setup_ui(self):
32
+ layout = QVBoxLayout()
33
+
34
+ self.tabs = QTabWidget()
35
+
36
+ # --- Tab 1: Method & Basis ---
37
+ self.tab_method = QWidget()
38
+ self.setup_method_tab()
39
+ self.tabs.addTab(self.tab_method, "Method/Basis")
40
+
41
+ # --- Tab 2: Job Type ---
42
+ self.tab_job = QWidget()
43
+ self.setup_job_tab()
44
+ self.tabs.addTab(self.tab_job, "Job Type")
45
+
46
+ # --- Tab 3: Solvation & Disp ---
47
+ self.tab_solvation = QWidget()
48
+ self.setup_solvation_tab()
49
+ self.tabs.addTab(self.tab_solvation, "Solvation/Dispersion")
50
+
51
+ # --- Tab 4: Properties ---
52
+ self.tab_props = QWidget()
53
+ self.setup_props_tab()
54
+ self.tabs.addTab(self.tab_props, "Properties")
55
+
56
+ layout.addWidget(self.tabs)
57
+
58
+ # --- Preview ---
59
+ preview_group = QGroupBox("Keyword Preview")
60
+ preview_layout = QVBoxLayout()
61
+ self.preview_label = QLabel()
62
+ self.preview_label.setWordWrap(True)
63
+ self.preview_label.setStyleSheet("font-weight: bold; color: blue; font-size: 14px;")
64
+ preview_layout.addWidget(self.preview_label)
65
+ preview_group.setLayout(preview_layout)
66
+ layout.addWidget(preview_group)
67
+
68
+ # --- Buttons ---
69
+ btn_layout = QHBoxLayout()
70
+ self.btn_ok = QPushButton("Apply to Job")
71
+ self.btn_ok.clicked.connect(self.accept)
72
+ self.btn_cancel = QPushButton("Cancel")
73
+ self.btn_cancel.clicked.connect(self.reject)
74
+ btn_layout.addStretch()
75
+ btn_layout.addWidget(self.btn_ok)
76
+ btn_layout.addWidget(self.btn_cancel)
77
+ layout.addLayout(btn_layout)
78
+
79
+ self.setLayout(layout)
80
+
81
+ self.connect_signals()
82
+ self.ui_ready = True
83
+ self.update_ui_state() # Initial UI state update
84
+ self.update_preview()
85
+
86
+ def setup_method_tab(self):
87
+ layout = QFormLayout()
88
+
89
+ # Method Type
90
+ self.method_type = QComboBox()
91
+ self.method_type.addItems([
92
+ "DFT (GGA/Hybrid/Meta)",
93
+ "DFT (Range-Separated)",
94
+ "DFT (Double Hybrid)",
95
+ "Wavefunction (HF/MP2)",
96
+ "Wavefunction (Coupled Cluster)",
97
+ "Wavefunction (Multireference)",
98
+ "Semi-Empirical"
99
+ ])
100
+ self.method_type.currentIndexChanged.connect(self.update_method_list)
101
+ layout.addRow("Method Type:", self.method_type)
102
+
103
+ # Method Name
104
+ self.method_name = QComboBox()
105
+ self.update_method_list()
106
+ layout.addRow("Method:", self.method_name)
107
+
108
+ # Basis Set
109
+ self.basis_set = QComboBox()
110
+ basis_groups = [
111
+ "--- Karlsruhe (Def2) ---",
112
+ "def2-SV(P)", "def2-SVP", "def2-TZVP", "def2-QZVP",
113
+ "ma-def2-SVP", "ma-def2-TZVP", "ma-def2-QZVP",
114
+ "def2-TZVPP", "def2-QZVPP",
115
+ "--- Dunning (cc-pV) ---",
116
+ "cc-pVDZ", "cc-pVTZ", "cc-pVQZ", "cc-pV5Z",
117
+ "aug-cc-pVDZ", "aug-cc-pVTZ", "aug-cc-pVQZ", "aug-cc-pV5Z",
118
+ "--- Pople ---",
119
+ "6-31G", "6-31G*", "6-311G", "6-311G*", "6-311G**",
120
+ "6-31+G*", "6-311+G**", "6-31++G**",
121
+ "--- Jensen (pc) ---",
122
+ "pc-0", "pc-1", "pc-2", "pc-3", "aug-pc-1", "aug-pc-2",
123
+ "--- Other ---",
124
+ "EPR-II", "EPR-III", "IGLO-II", "IGLO-III"
125
+ ]
126
+ self.basis_set.addItems(basis_groups)
127
+ self.basis_set.setCurrentText("def2-SVP")
128
+ layout.addRow("Basis Set:", self.basis_set)
129
+
130
+ # Auxiliary Basis (RI/RIJCOSX)
131
+ self.aux_basis = QComboBox()
132
+ self.aux_basis.addItems([
133
+ "Auto (Def2/J, etc)", "None", "Def2/J", "Def2/JK",
134
+ "AutoAux", "NoAux"
135
+ ])
136
+ layout.addRow("Aux Basis (RI):", self.aux_basis)
137
+
138
+ self.tab_method.setLayout(layout)
139
+
140
+ def update_method_list(self):
141
+ mtype = self.method_type.currentText()
142
+ self.method_name.blockSignals(True)
143
+ self.method_name.clear()
144
+
145
+ if "GGA/Hybrid" in mtype:
146
+ self.method_name.addItems([
147
+ "B3LYP", "PBE0", "PBE", "BP86", "BLYP", "PW91",
148
+ "TPSSh", "TPSS", "SCAN", "r2SCAN-3c", "B97-3c", "PBEh-3c", # 3c系を追加
149
+ "M06", "M06-2X", "M06-HF", "M06-L",
150
+ "X3LYP", "O3LYP", "B3PW91", "BHandHLYP" # BH&HLYPをBHandHLYPに修正
151
+ ])
152
+ elif "Range-Separated" in mtype:
153
+ self.method_name.addItems([
154
+ "wB97X-D3", "wB97X-V", "wB97M-V", "wB97X", "wB97",
155
+ "CAM-B3LYP", "LC-wPBE", "wPBE", "wPBEh"
156
+ ])
157
+ elif "Double Hybrid" in mtype:
158
+ self.method_name.addItems([
159
+ "B2PLYP", "mPW2PLYP", "B2GP-PLYP", "DSD-PBEP86", "DSD-BLYP",
160
+ "PTPSS-D3", "PWPB95"
161
+ ])
162
+ elif "Wavefunction (HF/MP2)" in mtype:
163
+ self.method_name.addItems([
164
+ "HF", "HF-3c", "UHF", "ROHF", # HF-3cを追加
165
+ "MP2", "RI-MP2", "SCS-MP2", "OO-RI-MP2"
166
+ ])
167
+ elif "Coupled Cluster" in mtype:
168
+ self.method_name.addItems([
169
+ "DLPNO-CCSD(T)", "DLPNO-CCSD(T1)", "DLPNO-CCSD",
170
+ "CCSD(T)", "CCSD", "QCISD(T)", "CEPA/1"
171
+ ])
172
+ elif "Multireference" in mtype:
173
+ self.method_name.addItems([
174
+ "CASSCF", "NEVPT2", "DLPNO-NEVPT2", "MRCI"
175
+ ])
176
+ elif "Semi-Empirical" in mtype:
177
+ self.method_name.addItems(["XT", "GFN1-xTB", "GFN2-xTB", "PM3", "AM1", "ZINDO/S", "PM6", "MNDO"])
178
+ else:
179
+ self.method_name.addItem("Custom")
180
+
181
+ self.method_name.blockSignals(False)
182
+ self.update_ui_state() # Update visibility when method type changes
183
+ self.update_preview()
184
+
185
+ def setup_job_tab(self):
186
+ layout = QVBoxLayout()
187
+
188
+ self.job_type = QComboBox()
189
+ self.job_type.addItems([
190
+ "Optimization + Freq (Opt Freq)",
191
+ "Optimization Only (Opt)",
192
+ "Frequency Only (Freq)",
193
+ "Single Point Energy (SP)",
194
+ "Scan (Relaxed Surface)",
195
+ "Transition State Opt (OptTS)",
196
+ "Gradient",
197
+ "Hessian"
198
+ ])
199
+ layout.addWidget(QLabel("Job Task:"))
200
+ layout.addWidget(self.job_type)
201
+ self.job_type.currentIndexChanged.connect(self.update_ui_state)
202
+
203
+ # Opt Options
204
+ self.opt_group = QGroupBox("Optimization Options")
205
+ opt_layout = QHBoxLayout()
206
+ self.opt_tight = QCheckBox("TightOpt")
207
+ self.opt_loose = QCheckBox("LooseOpt")
208
+ self.opt_calcfc = QCheckBox("CalcFC")
209
+ self.opt_ts_mode = QCheckBox("CalcHess (for TS)")
210
+ opt_layout.addWidget(self.opt_tight)
211
+ opt_layout.addWidget(self.opt_loose)
212
+ opt_layout.addWidget(self.opt_calcfc)
213
+ opt_layout.addWidget(self.opt_ts_mode)
214
+ self.opt_group.setLayout(opt_layout)
215
+ layout.addWidget(self.opt_group)
216
+
217
+ # Freq Options
218
+ self.freq_group = QGroupBox("Freq Options")
219
+ freq_layout = QHBoxLayout()
220
+ self.freq_num = QCheckBox("NumFreq")
221
+ self.freq_raman = QCheckBox("Raman")
222
+ freq_layout.addWidget(self.freq_num)
223
+ freq_layout.addWidget(self.freq_raman)
224
+ self.freq_group.setLayout(freq_layout)
225
+ layout.addWidget(self.freq_group)
226
+
227
+ layout.addStretch()
228
+ self.tab_job.setLayout(layout)
229
+
230
+ def setup_solvation_tab(self):
231
+ layout = QFormLayout()
232
+
233
+ self.solv_model = QComboBox()
234
+ self.solv_model.addItems(["None", "CPCM", "SMD", "IEFPCM", "CPC(Water) (Short)"])
235
+ self.solv_model.currentIndexChanged.connect(self.update_ui_state)
236
+ layout.addRow("Solvation Model:", self.solv_model)
237
+
238
+ self.solvent = QComboBox()
239
+ solvents = [
240
+ "Water", "Acetonitrile", "Methanol", "Ethanol",
241
+ "Chloroform", "Dichloromethane", "Toluene",
242
+ "THF", "DMSO", "Cyclohexane", "Benzene", "Acetone",
243
+ "CCl4", "DMF", "HMPA", "Pyridine"
244
+ ]
245
+ self.solvent.addItems(solvents)
246
+ layout.addRow("Solvent:", self.solvent)
247
+
248
+ layout.addRow(QLabel(" "))
249
+
250
+ self.dispersion = QComboBox()
251
+ self.dispersion.addItems(["None", "D3BJ", "D3Zero", "D4", "D2", "NL"])
252
+ layout.addRow("Dispersion Correction:", self.dispersion)
253
+
254
+ self.tab_solvation.setLayout(layout)
255
+
256
+ def setup_props_tab(self):
257
+ layout = QFormLayout()
258
+
259
+ self.rijcosx = QCheckBox("RIJCOSX / RI approximation")
260
+ self.rijcosx.setChecked(True)
261
+ layout.addRow(self.rijcosx)
262
+
263
+ self.grid_combo = QComboBox()
264
+ self.grid_combo.addItems(["Default", "DefGrid2", "DefGrid3", "Grid4", "Grid5", "Grid6", "NoGrid"])
265
+ layout.addRow("Grid:", self.grid_combo)
266
+
267
+ self.scf_conv = QComboBox()
268
+ self.scf_conv.addItems(["Default", "LooseSCF", "TightSCF", "VeryTightSCF"])
269
+ layout.addRow("SCF Convergence:", self.scf_conv)
270
+
271
+ # NBO
272
+ self.pop_nbo = QCheckBox("NBO Analysis (! NBO)")
273
+ layout.addRow(self.pop_nbo)
274
+
275
+ self.tab_props.setLayout(layout)
276
+
277
+ def connect_signals(self):
278
+ widgets = [
279
+ self.method_type, self.method_name, self.basis_set, self.aux_basis,
280
+ self.job_type, self.opt_tight, self.opt_loose, self.opt_calcfc, self.opt_ts_mode,
281
+ self.freq_num, self.freq_raman,
282
+ self.solv_model, self.solvent, self.dispersion,
283
+ self.rijcosx, self.grid_combo, self.scf_conv, self.pop_nbo
284
+ ]
285
+ for w in widgets:
286
+ if isinstance(w, QComboBox):
287
+ w.currentIndexChanged.connect(self.update_preview)
288
+ elif isinstance(w, QCheckBox):
289
+ w.toggled.connect(self.update_preview)
290
+ elif isinstance(w, QSpinBox):
291
+ w.valueChanged.connect(self.update_preview)
292
+
293
+ def update_ui_state(self):
294
+ """Update usability of widgets based on current selection."""
295
+ if not getattr(self, 'ui_ready', False): return
296
+
297
+ # 1. Method Dependent
298
+ mtype = self.method_type.currentText()
299
+ is_semi = "Semi-Empirical" in mtype
300
+
301
+ # Disable Basis Set & Aux Basis for Semi-Empirical
302
+ self.basis_set.setEnabled(not is_semi)
303
+ self.aux_basis.setEnabled(not is_semi)
304
+
305
+ # Handling RI / RIJCOSX
306
+ if is_semi:
307
+ self.rijcosx.setEnabled(False)
308
+ self.rijcosx.setChecked(False)
309
+ self.rijcosx.setText("RIJCOSX (N/A)")
310
+ else:
311
+ self.rijcosx.setEnabled(True)
312
+ if "Wavefunction" in mtype:
313
+ self.rijcosx.setText("RI Approximation (! RI ...)")
314
+ else:
315
+ self.rijcosx.setText("RIJCOSX (Speed up Hybrid DFT)")
316
+
317
+ # 2. Solvation
318
+ solv = self.solv_model.currentText()
319
+ is_solvated = solv != "None"
320
+ self.solvent.setEnabled(is_solvated)
321
+ if "CPC(Water)" in solv:
322
+ self.solvent.setEnabled(False) # Water is implied
323
+
324
+ # 3. Job Type
325
+ job_txt = self.job_type.currentText()
326
+ is_opt = "Opt" in job_txt or "Scan" in job_txt
327
+ is_freq = "Freq" in job_txt
328
+
329
+ self.opt_group.setVisible(is_opt)
330
+ self.freq_group.setVisible(is_freq)
331
+
332
+ # 4. TD-DFT (Removed from Route Builder, handled via blocks)
333
+
334
+ def update_preview(self):
335
+ if not getattr(self, 'ui_ready', False):
336
+ return
337
+
338
+ route_parts = ["!"]
339
+
340
+ # Method / Basis
341
+ method = self.method_name.currentText()
342
+ basis = self.basis_set.currentText()
343
+
344
+ # 3c methods usually don't need basis set
345
+ mtype = self.method_type.currentText()
346
+ if "Semi-Empirical" in mtype:
347
+ route_parts.append(method)
348
+ elif "3c" in method:
349
+ route_parts.append(method)
350
+ else:
351
+ route_parts.append(method)
352
+ route_parts.append(basis)
353
+
354
+ # RIJCOSX / RI
355
+ if self.rijcosx.isEnabled() and self.rijcosx.isChecked():
356
+ if "Wavefunction" in mtype:
357
+ route_parts.append("RI")
358
+ else:
359
+ route_parts.append("RIJCOSX")
360
+
361
+ aux = self.aux_basis.currentText()
362
+ if "Def2/J" in aux: route_parts.append("Def2/J")
363
+ elif "Def2/JK" in aux: route_parts.append("Def2/JK")
364
+
365
+ # Job Type
366
+ job_txt = self.job_type.currentText()
367
+ if "Opt Freq" in job_txt: route_parts.extend(["Opt", "Freq"])
368
+ elif "Opt Only" in job_txt: route_parts.append("Opt")
369
+ elif "OptTS" in job_txt: route_parts.append("OptTS")
370
+ elif "Freq Only" in job_txt: route_parts.append("Freq")
371
+ elif "Scan" in job_txt: route_parts.append("Scan")
372
+ elif "Gradient" in job_txt: route_parts.append("Gradient")
373
+ elif "Hessian" in job_txt: route_parts.append("Hessian")
374
+ elif "SP" in job_txt: pass # No keyword
375
+
376
+ # Opt Options
377
+ if self.opt_group.isVisible():
378
+ if self.opt_tight.isChecked(): route_parts.append("TightOpt")
379
+ if self.opt_loose.isChecked(): route_parts.append("LooseOpt")
380
+ if self.opt_calcfc.isChecked(): route_parts.append("CalcFC")
381
+ if self.opt_ts_mode.isChecked(): route_parts.append("CalcHess")
382
+
383
+ # Freq Options
384
+ if self.freq_group.isVisible():
385
+ if self.freq_num.isChecked(): route_parts.append("NumFreq")
386
+
387
+ # Solvation
388
+ solv = self.solv_model.currentText()
389
+ if solv != "None":
390
+ if "CPC(Water)" in solv:
391
+ route_parts.append("CPC(Water)")
392
+ else:
393
+ solvent = self.solvent.currentText()
394
+ if "CPCM" == solv:
395
+ route_parts.append(f"CPCM({solvent})")
396
+ elif "SMD" == solv:
397
+ route_parts.append(f"CPCM({solvent})")
398
+ route_parts.append("SMD")
399
+ elif "IEFPCM" == solv:
400
+ route_parts.append(f"CPCM({solvent})")
401
+
402
+ # Dispersion
403
+ disp = self.dispersion.currentText()
404
+ if disp != "None":
405
+ route_parts.append(disp)
406
+
407
+ # SCF / Grid
408
+ scf = self.scf_conv.currentText()
409
+ if scf != "Default": route_parts.append(scf)
410
+
411
+ grid = self.grid_combo.currentText()
412
+ if grid != "Default": route_parts.append(grid)
413
+
414
+ # NBO
415
+ if self.pop_nbo.isChecked(): route_parts.append("NBO")
416
+
417
+ self.preview_str = " ".join(route_parts)
418
+ self.preview_label.setText(self.preview_str)
419
+
420
+ def get_route(self):
421
+ return self.preview_str
422
+
423
+ def parse_route(self, route):
424
+ pass
425
+
426
+ class OrcaSetupDialogNeo(QDialog):
427
+ """
428
+ ORCA Input Generator Neo
429
+ """
430
+ def __init__(self, parent=None, mol=None, filename=None):
431
+ super().__init__(parent)
432
+ self.setWindowTitle(PLUGIN_NAME)
433
+ self.resize(600, 700)
434
+ self.mol = mol
435
+ self.filename = filename
436
+ self.setup_ui()
437
+ self.load_presets_from_file()
438
+ self.calc_initial_charge_mult()
439
+
440
+ def setup_ui(self):
441
+ main_layout = QVBoxLayout()
442
+
443
+ # --- 0. Preset Management ---
444
+ preset_group = QGroupBox("Preset Management")
445
+ preset_layout = QHBoxLayout()
446
+
447
+ self.preset_combo = QComboBox()
448
+ self.preset_combo.currentIndexChanged.connect(self.apply_selected_preset)
449
+ preset_layout.addWidget(QLabel("Preset:"))
450
+ preset_layout.addWidget(self.preset_combo, 1)
451
+
452
+ self.btn_save_preset = QPushButton("Save New...")
453
+ self.btn_save_preset.clicked.connect(self.save_preset_dialog)
454
+ preset_layout.addWidget(self.btn_save_preset)
455
+
456
+ self.btn_del_preset = QPushButton("Delete")
457
+ self.btn_del_preset.clicked.connect(self.delete_preset)
458
+ preset_layout.addWidget(self.btn_del_preset)
459
+
460
+ preset_group.setLayout(preset_layout)
461
+ main_layout.addWidget(preset_group)
462
+
463
+ # --- 1. Resource Configuration ---
464
+ res_group = QGroupBox("Resources (%pal, %maxcore)")
465
+ res_layout = QFormLayout()
466
+
467
+ # NProcs
468
+ self.nproc_spin = QSpinBox()
469
+ self.nproc_spin.setRange(1, 128)
470
+ self.nproc_spin.setValue(4)
471
+ res_layout.addRow("Number of Processors (nprocs):", self.nproc_spin)
472
+
473
+ # Memory
474
+ self.mem_spin = QSpinBox()
475
+ self.mem_spin.setRange(100, 999999)
476
+ self.mem_spin.setValue(2000)
477
+ self.mem_spin.setSuffix(" MB")
478
+ res_layout.addRow("Memory per Core (MaxCore):", self.mem_spin)
479
+
480
+ res_group.setLayout(res_layout)
481
+ main_layout.addWidget(res_group)
482
+
483
+ # --- 2. Simple Input Line ---
484
+ kw_group = QGroupBox("Simple Input Line (!)")
485
+ kw_layout = QVBoxLayout()
486
+
487
+ kw_h_layout = QHBoxLayout()
488
+ self.keywords_edit = QLineEdit("! B3LYP def2-SVP Opt Freq")
489
+ self.btn_route = QPushButton("Keyword Builder...")
490
+ self.btn_route.clicked.connect(self.open_keyword_builder)
491
+ kw_h_layout.addWidget(self.keywords_edit)
492
+ kw_h_layout.addWidget(self.btn_route)
493
+
494
+ kw_layout.addWidget(QLabel("Keywords (starts with !):"))
495
+ kw_layout.addLayout(kw_h_layout)
496
+
497
+ # Comment
498
+ self.comment_edit = QLineEdit("Generated by MoleditPy ORCA Neo")
499
+ kw_layout.addWidget(QLabel("Comment (# ...):"))
500
+ kw_layout.addWidget(self.comment_edit)
501
+
502
+ kw_group.setLayout(kw_layout)
503
+ main_layout.addWidget(kw_group)
504
+
505
+ # --- 3. Molecular State ---
506
+ mol_group = QGroupBox("Molecular Specification")
507
+ mol_layout = QHBoxLayout()
508
+
509
+ self.charge_spin = QSpinBox()
510
+ self.charge_spin.setRange(-10, 10)
511
+ self.charge_spin.valueChanged.connect(self.validate_charge_mult)
512
+
513
+ self.mult_spin = QSpinBox()
514
+ self.mult_spin.setRange(1, 10)
515
+ self.mult_spin.valueChanged.connect(self.validate_charge_mult)
516
+
517
+ mol_layout.addWidget(QLabel("Charge:"))
518
+ mol_layout.addWidget(self.charge_spin)
519
+ mol_layout.addWidget(QLabel("Multiplicity:"))
520
+ mol_layout.addWidget(self.mult_spin)
521
+
522
+ self.default_palette = self.charge_spin.palette()
523
+
524
+ mol_group.setLayout(mol_layout)
525
+ main_layout.addWidget(mol_group)
526
+
527
+ # --- 3b. Coordinate Format ---
528
+ coord_group = QGroupBox("Coordinate Format")
529
+ coord_layout = QHBoxLayout()
530
+ self.coord_format_combo = QComboBox()
531
+ self.coord_format_combo.addItems(["Cartesian (XYZ)", "Internal (Z-Matrix / * int)", "Internal (Z-Matrix / * gzmt)"])
532
+ self.coord_format_combo.setCurrentIndex(0)
533
+ self.coord_format_combo.setEnabled(True)
534
+ coord_layout.addWidget(QLabel("Format:"))
535
+ coord_layout.addWidget(self.coord_format_combo)
536
+ coord_group.setLayout(coord_layout)
537
+ main_layout.addWidget(coord_group)
538
+
539
+ # --- 4. Advanced/Blocks ---
540
+ adv_group = QGroupBox("Advanced Blocks (Pre/Post Coordinates)")
541
+ adv_layout = QVBoxLayout()
542
+
543
+ # Block Helper
544
+ blk_h_layout = QHBoxLayout()
545
+ blk_h_layout.addWidget(QLabel("Block Helper:"))
546
+
547
+ self.block_combo = QComboBox()
548
+ self.block_combo.addItems([
549
+ "Select Block to Insert...",
550
+ "%scf ... end",
551
+ "%output ... end",
552
+ "%geom ... end",
553
+ "%elprop ... end",
554
+ "%plots ... end",
555
+ "%tddft ... end",
556
+ "%cis ... end",
557
+ "%mrci ... end",
558
+ "%casscf ... end",
559
+ "%eprnmr ... end (Post)",
560
+ ])
561
+ blk_h_layout.addWidget(self.block_combo, 1)
562
+
563
+ self.btn_insert_block = QPushButton("Insert")
564
+ self.btn_insert_block.clicked.connect(self.insert_block_template)
565
+ blk_h_layout.addWidget(self.btn_insert_block)
566
+
567
+ adv_layout.addLayout(blk_h_layout)
568
+
569
+ # Tabs for Pre/Post
570
+ self.adv_tabs = QTabWidget()
571
+
572
+ self.adv_edit = QTextEdit()
573
+ self.adv_edit.setPlaceholderText("Pre-Coordinate Blocks\nExample:\n%scf\n MaxIter 100\nend")
574
+ self.adv_tabs.addTab(self.adv_edit, "Pre-Coordinate")
575
+
576
+ self.post_adv_edit = QTextEdit()
577
+ self.post_adv_edit.setPlaceholderText("Post-Coordinate Blocks")
578
+ self.adv_tabs.addTab(self.post_adv_edit, "Post-Coordinate")
579
+
580
+ adv_layout.addWidget(self.adv_tabs)
581
+
582
+ adv_group.setLayout(adv_layout)
583
+ main_layout.addWidget(adv_group)
584
+
585
+ # --- Save/Preview Buttons ---
586
+ btn_box = QHBoxLayout()
587
+
588
+ self.btn_preview = QPushButton("Preview Input")
589
+ self.btn_preview.clicked.connect(self.preview_file)
590
+ btn_box.addWidget(self.btn_preview)
591
+
592
+ self.save_btn = QPushButton("Save Input File...")
593
+ self.save_btn.clicked.connect(self.save_file)
594
+ self.save_btn.setStyleSheet("font-weight: bold; padding: 5px;")
595
+ btn_box.addWidget(self.save_btn)
596
+
597
+ main_layout.addLayout(btn_box)
598
+
599
+ self.setLayout(main_layout)
600
+
601
+ def insert_block_template(self):
602
+ txt = self.block_combo.currentText()
603
+ if "Select" in txt: return
604
+
605
+ template = ""
606
+ if "%scf" in txt:
607
+ template = "%scf\n MaxIter 125\nend\n"
608
+ elif "%output" in txt:
609
+ template = "%output\n PrintLevel Normal\nend\n"
610
+ elif "%geom" in txt:
611
+ template = "%geom\n MaxIter 100\nend\n"
612
+ elif "%elprop" in txt:
613
+ template = "%elprop\n Dipole True\n Quadrupole True\nend\n"
614
+ elif "%plots" in txt:
615
+ template = "%plots\n Format Gaussian_Cube\nend\n"
616
+ elif "%tddft" in txt:
617
+ template = (
618
+ "%tddft\n"
619
+ " NRoots 10 # Number of excited states\n"
620
+ " MaxDim 10 # Max dimension of expansion space\n"
621
+ " TDA true # Tamm-Dancoff Approximation (true/false)\n"
622
+ " IRoot 1 # State of interest for gradient properties\n"
623
+ " Triplets true # Calculate triplet states\n"
624
+ " Singlets true # Calculate singlet states\n"
625
+ " DoQuad true # Compute quadrupole intensities\n"
626
+ "end\n"
627
+ )
628
+ elif "%cis" in txt:
629
+ template = "%cis\n NRoots 10\nend\n"
630
+ elif "%mrci" in txt:
631
+ template = "%mrci\n NewBlocks 1 1\nend\n"
632
+ elif "%casscf" in txt:
633
+ template = "%casscf\n Nel 2\n Norb 2\n Mult 1\nend\n"
634
+ elif "%eprnmr" in txt:
635
+ template = "%eprnmr\n nuclei = all h {shift, ssall}\nend\n"
636
+ # Switch to Post-Coordinate tab automatically
637
+ self.adv_tabs.setCurrentWidget(self.post_adv_edit)
638
+
639
+ current_widget = self.adv_tabs.currentWidget()
640
+ if isinstance(current_widget, QTextEdit):
641
+ cursor = current_widget.textCursor()
642
+ cursor.insertText(template)
643
+
644
+ def open_keyword_builder(self):
645
+ dialog = OrcaKeywordBuilderDialog(self, self.keywords_edit.text())
646
+ if dialog.exec() == QDialog.DialogCode.Accepted:
647
+ self.keywords_edit.setText(dialog.get_route())
648
+
649
+ def get_coords_lines(self):
650
+ if not self.mol: return []
651
+ lines = []
652
+ try:
653
+ conf = self.mol.GetConformer()
654
+ for i in range(self.mol.GetNumAtoms()):
655
+ pos = conf.GetAtomPosition(i)
656
+ atom = self.mol.GetAtomWithIdx(i)
657
+ lines.append(f" {atom.GetSymbol(): <4} {pos.x: >12.6f} {pos.y: >12.6f} {pos.z: >12.6f}")
658
+ except Exception as e:
659
+ return [f"# Error: {e}"]
660
+ return lines
661
+
662
+ def _build_zmatrix_data(self):
663
+ """Helper to build Z-Matrix connectivity and values."""
664
+ if not self.mol: return None
665
+ try:
666
+ atoms = list(self.mol.GetAtoms())
667
+ conf = self.mol.GetConformer()
668
+
669
+ def get_dist(i, j): return rdMolTransforms.GetBondLength(conf, i, j)
670
+ def get_angle(i, j, k): return rdMolTransforms.GetAngleDeg(conf, i, j, k)
671
+ def get_dihedral(i, j, k, l): return rdMolTransforms.GetDihedralDeg(conf, i, j, k, l)
672
+
673
+ defined = []
674
+ z_data = [] # List of dicts for each atom
675
+
676
+ for i, atom in enumerate(atoms):
677
+ symbol = atom.GetSymbol()
678
+
679
+ # Atom 0
680
+ if i == 0:
681
+ z_data.append({"symbol": symbol, "refs": []})
682
+ defined.append(i)
683
+ continue
684
+
685
+ # Find neighbors in defined set
686
+ current_idx = atom.GetIdx()
687
+ neighbors = [n.GetIdx() for n in atom.GetNeighbors()]
688
+ candidates = [n for n in neighbors if n in defined]
689
+ if not candidates: candidates = defined[:] # Fallback
690
+
691
+ refs = []
692
+ # Ref 1 (Distance)
693
+ if candidates: refs.append(candidates[-1])
694
+ else: refs.append(0)
695
+
696
+ # Ref 2 (Angle)
697
+ candidates_2 = [x for x in defined if x != refs[0]]
698
+ if candidates_2: refs.append(candidates_2[-1])
699
+
700
+ # Ref 3 (Dihedral)
701
+ candidates_3 = [x for x in defined if x not in refs]
702
+ if candidates_3: refs.append(candidates_3[-1])
703
+
704
+ # Calculate Values
705
+ row = {"symbol": symbol, "refs": [], "values": []}
706
+
707
+ if len(refs) >= 1:
708
+ row["refs"].append(refs[0]) # 0-based index for calculation
709
+ row["values"].append(get_dist(i, refs[0]))
710
+
711
+ if len(refs) >= 2:
712
+ row["refs"].append(refs[1])
713
+ row["values"].append(get_angle(i, refs[0], refs[1]))
714
+
715
+ if len(refs) >= 3:
716
+ row["refs"].append(refs[2])
717
+ row["values"].append(get_dihedral(i, refs[0], refs[1], refs[2]))
718
+
719
+ z_data.append(row)
720
+ defined.append(i)
721
+
722
+ return z_data
723
+ except Exception as e:
724
+ raise e
725
+
726
+ def get_zmatrix_standard_lines(self):
727
+ """
728
+ Generates * int style lines.
729
+ Format: Symbol Ref1 Ref2 Ref3 R Angle Dihed
730
+ Refs are 1-based integers. Missing refs/values are 0/0.0.
731
+ """
732
+ try:
733
+ data = self._build_zmatrix_data()
734
+ if not data: return []
735
+
736
+ lines = []
737
+ for i, row in enumerate(data):
738
+ symbol = row["symbol"]
739
+
740
+ # Prepare 3 refs (1-based) and 3 values
741
+ refs_out = [0, 0, 0]
742
+ vals_out = [0.0, 0.0, 0.0]
743
+
744
+ current_refs = row["refs"]
745
+ current_vals = row.get("values", [])
746
+
747
+ for k in range(min(3, len(current_refs))):
748
+ refs_out[k] = current_refs[k] + 1 # Convert to 1-based
749
+ vals_out[k] = current_vals[k]
750
+
751
+ line = f" {symbol: <3} {refs_out[0]: >3} {refs_out[1]: >3} {refs_out[2]: >3} " \
752
+ f"{vals_out[0]: >10.6f} {vals_out[1]: >10.6f} {vals_out[2]: >10.6f}"
753
+ lines.append(line)
754
+ return lines
755
+ except Exception as e:
756
+ return [f"# Error generating Standard Z-Matrix: {e}"]
757
+
758
+ def get_zmatrix_gzmt_lines(self):
759
+ """
760
+ Generates * gzmt style lines (Compact).
761
+ Format: Symbol Ref1 R Ref2 Angle Ref3 Dihed
762
+ Refs are 1-based.
763
+ """
764
+ try:
765
+ data = self._build_zmatrix_data()
766
+ if not data: return []
767
+
768
+ lines = []
769
+ for i, row in enumerate(data):
770
+ symbol = row["symbol"]
771
+ line = f" {symbol: <3}"
772
+
773
+ current_refs = row["refs"]
774
+ current_vals = row.get("values", [])
775
+
776
+ # Z-Matrix logic:
777
+ # Atom 1: Symbol
778
+ # Atom 2: Symbol Ref1 R
779
+ # Atom 3: Symbol Ref1 R Ref2 A
780
+ # Atom 4: Symbol Ref1 R Ref2 A Ref3 D
781
+
782
+ if i == 0:
783
+ pass
784
+ else:
785
+ count = len(current_refs)
786
+ if count >= 1:
787
+ line += f" {current_refs[0] + 1: >3} {current_vals[0]: .6f}"
788
+ if count >= 2:
789
+ line += f" {current_refs[1] + 1: >3} {current_vals[1]: .6f}"
790
+ if count >= 3:
791
+ line += f" {current_refs[2] + 1: >3} {current_vals[2]: .6f}"
792
+
793
+ lines.append(line)
794
+ return lines
795
+ except Exception as e:
796
+ return [f"# Error generating GZMT Z-Matrix: {e}"]
797
+
798
+
799
+ def generate_input_content(self):
800
+ """Generates the full content of the input file as a string."""
801
+ content = []
802
+
803
+ comment = self.comment_edit.text().strip()
804
+ if comment:
805
+ content.append(f"# {comment}")
806
+
807
+ # Resources
808
+ nprocs = self.nproc_spin.value()
809
+ if nprocs > 1:
810
+ content.append(f"%pal nprocs {nprocs} end")
811
+ content.append(f"%maxcore {self.mem_spin.value()}\n")
812
+
813
+ # Keywords
814
+ kw = self.keywords_edit.text().strip()
815
+ if not kw.startswith("!"): kw = "! " + kw
816
+ content.append(f"{kw}\n")
817
+
818
+ # Advanced Blocks
819
+ adv = self.adv_edit.toPlainText().strip()
820
+ if adv:
821
+ content.append(f"{adv}\n")
822
+
823
+ # Coordinates
824
+ is_cartesian = "Cartesian" in self.coord_format_combo.currentText()
825
+ coord_lines = self.get_coords_lines()
826
+
827
+ if is_cartesian:
828
+ content.append(f"* xyz {self.charge_spin.value()} {self.mult_spin.value()}")
829
+ content.extend(coord_lines)
830
+ content.append("*")
831
+ else:
832
+ # Z-Matrix
833
+ is_gzmt = "gzmt" in self.coord_format_combo.currentText()
834
+
835
+ if is_gzmt:
836
+ zmat_lines = self.get_zmatrix_gzmt_lines()
837
+ header = "* gzmt"
838
+ else:
839
+ zmat_lines = self.get_zmatrix_standard_lines()
840
+ header = "* int"
841
+
842
+ if any("Error" in line for line in zmat_lines):
843
+ content.append(f"# ERROR: Z-Matrix generation failed.")
844
+ content.append(f"* xyz {self.charge_spin.value()} {self.mult_spin.value()}")
845
+ content.extend(self.get_coords_lines())
846
+ content.append("*")
847
+ else:
848
+ content.append(f"{header} {self.charge_spin.value()} {self.mult_spin.value()}")
849
+ content.extend(zmat_lines)
850
+ content.append("*")
851
+
852
+ # Post-Coordinate Blocks
853
+ adv_post = self.post_adv_edit.toPlainText().strip()
854
+ if adv_post:
855
+ content.append(f"\n{adv_post}")
856
+
857
+ return "\n".join(content)
858
+
859
+ def preview_file(self):
860
+ content = self.generate_input_content()
861
+
862
+ d = QDialog(self)
863
+ d.setWindowTitle("Preview Input - ORCA Neo")
864
+ d.resize(600, 500)
865
+ l = QVBoxLayout()
866
+ t = QTextEdit()
867
+ t.setPlainText(content)
868
+ t.setReadOnly(True)
869
+ t.setFont(QFont("Courier New", 10))
870
+ l.addWidget(t)
871
+
872
+ btn = QPushButton("Close")
873
+ btn.clicked.connect(d.accept)
874
+ l.addWidget(btn)
875
+ d.setLayout(l)
876
+ d.exec()
877
+
878
+ def save_file(self):
879
+ # Validation Check (e.g. Z-Matrix Error)
880
+ # We can re-check here or just trust generate_input_content
881
+
882
+ default_name = ""
883
+ if self.filename:
884
+ base, _ = os.path.splitext(self.filename)
885
+ default_name = base + ".inp"
886
+
887
+ file_path, _ = QFileDialog.getSaveFileName(
888
+ self, "Save ORCA Input", default_name, "ORCA Input (*.inp);;All Files (*)"
889
+ )
890
+
891
+ if file_path:
892
+ try:
893
+ content = self.generate_input_content()
894
+ with open(file_path, 'w', encoding='utf-8') as f:
895
+ f.write(content)
896
+
897
+ QMessageBox.information(self, "Success", f"File saved:\n{file_path}")
898
+ self.accept()
899
+ except Exception as e:
900
+ QMessageBox.critical(self, "Error", f"Failed to save file:\n{e}")
901
+
902
+ # --- Preset Management (Similar to Gaussian Neo) ---
903
+ def load_presets_from_file(self):
904
+ self.presets_data = {}
905
+ if os.path.exists(SETTINGS_FILE):
906
+ try:
907
+ with open(SETTINGS_FILE, 'r', encoding='utf-8') as f:
908
+ self.presets_data = json.load(f)
909
+ except Exception as e:
910
+ print(f"Error loading presets: {e}")
911
+
912
+ if "Default" not in self.presets_data:
913
+ self.presets_data["Default"] = {
914
+ "nproc": 4, "maxcore": 2000,
915
+ "route": "! B3LYP def2-SVP Opt Freq", "adv": "", "adv_post": ""
916
+ }
917
+
918
+ self.update_preset_combo()
919
+
920
+ def update_preset_combo(self):
921
+ current = self.preset_combo.currentText()
922
+ self.preset_combo.blockSignals(True)
923
+ self.preset_combo.clear()
924
+ self.preset_combo.addItems(sorted(self.presets_data.keys()))
925
+
926
+ if current in self.presets_data:
927
+ self.preset_combo.setCurrentText(current)
928
+ elif "Default" in self.presets_data:
929
+ self.preset_combo.setCurrentText("Default")
930
+
931
+ self.preset_combo.blockSignals(False)
932
+ self.apply_selected_preset()
933
+
934
+ def apply_selected_preset(self):
935
+ name = self.preset_combo.currentText()
936
+ if name not in self.presets_data: return
937
+ data = self.presets_data[name]
938
+
939
+ self.nproc_spin.setValue(data.get("nproc", 4))
940
+ self.mem_spin.setValue(data.get("maxcore", 2000))
941
+ self.keywords_edit.setText(data.get("route", "! B3LYP def2-SVP Opt Freq"))
942
+ self.adv_edit.setPlainText(data.get("adv", ""))
943
+ self.post_adv_edit.setPlainText(data.get("adv_post", ""))
944
+
945
+ def save_preset_dialog(self):
946
+ name, ok = QInputDialog.getText(self, "Save Preset", "Preset Name:")
947
+ if ok and name:
948
+ self.presets_data[name] = {
949
+ "nproc": self.nproc_spin.value(),
950
+ "maxcore": self.mem_spin.value(),
951
+ "route": self.keywords_edit.text(),
952
+ "adv": self.adv_edit.toPlainText(),
953
+ "adv_post": self.post_adv_edit.toPlainText()
954
+ }
955
+ self.save_presets_to_file()
956
+ self.update_preset_combo()
957
+ self.preset_combo.setCurrentText(name)
958
+
959
+ def delete_preset(self):
960
+ name = self.preset_combo.currentText()
961
+ if name == "Default":
962
+ QMessageBox.warning(self, "Warning", "Cannot delete Default preset.")
963
+ return
964
+
965
+ confirm = QMessageBox.question(self, "Confirm", f"Delete preset '{name}'?")
966
+ if confirm == QMessageBox.StandardButton.Yes:
967
+ del self.presets_data[name]
968
+ self.save_presets_to_file()
969
+ self.update_preset_combo()
970
+
971
+ def save_presets_to_file(self):
972
+ try:
973
+ with open(SETTINGS_FILE, 'w', encoding='utf-8') as f:
974
+ json.dump(self.presets_data, f, indent=4)
975
+ except Exception as e:
976
+ QMessageBox.warning(self, "Error", f"Failed to save presets: {e}")
977
+
978
+ # --- Charge/Mult Logic ---
979
+ def calc_initial_charge_mult(self):
980
+ if not self.mol: return
981
+ try:
982
+ try: charge = Chem.GetFormalCharge(self.mol)
983
+ except: charge = 0
984
+
985
+ num_radical = sum(atom.GetNumRadicalElectrons() for atom in self.mol.GetAtoms())
986
+ mult = num_radical + 1
987
+
988
+ self.charge_spin.setValue(int(charge))
989
+ self.mult_spin.setValue(int(mult))
990
+ self.validate_charge_mult()
991
+ except Exception: pass
992
+
993
+ def validate_charge_mult(self):
994
+ if not self.mol: return
995
+ try:
996
+ charge = self.charge_spin.value()
997
+ mult = self.mult_spin.value()
998
+
999
+ total_protons = sum(atom.GetAtomicNum() for atom in self.mol.GetAtoms())
1000
+ total_electrons = total_protons - charge
1001
+
1002
+ is_valid = (total_electrons % 2 == 0 and mult % 2 != 0) or \
1003
+ (total_electrons % 2 != 0 and mult % 2 == 0)
1004
+
1005
+ if is_valid:
1006
+ self.charge_spin.setPalette(self.default_palette)
1007
+ self.mult_spin.setPalette(self.default_palette)
1008
+ else:
1009
+ p = self.charge_spin.palette()
1010
+ p.setColor(QPalette.ColorRole.Base, QColor("#FFDDDD"))
1011
+ p.setColor(QPalette.ColorRole.Text, Qt.GlobalColor.red)
1012
+ self.charge_spin.setPalette(p)
1013
+ self.mult_spin.setPalette(p)
1014
+ except: pass
1015
+
1016
+ from PyQt6.QtWidgets import QInputDialog
1017
+
1018
+ def run(mw):
1019
+ mol = getattr(mw, 'current_mol', None)
1020
+ if not mol:
1021
+ QMessageBox.warning(mw, PLUGIN_NAME, "No molecule loaded.")
1022
+ return
1023
+
1024
+ filename = getattr(mw, 'current_file_path', None)
1025
+ dialog = OrcaSetupDialogNeo(parent=mw, mol=mol, filename=filename)
1026
+ dialog.exec()
1027
+
1028
+ # initialize removed as it only registered the menu action