MoleditPy 2.2.0a0__py3-none-any.whl → 2.2.0a2__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 (27) hide show
  1. moleditpy/modules/constants.py +1 -1
  2. moleditpy/modules/main_window_main_init.py +73 -103
  3. moleditpy/modules/main_window_ui_manager.py +21 -2
  4. moleditpy/modules/plugin_manager.py +10 -0
  5. moleditpy/plugins/Analysis/ms_spectrum_neo.py +919 -0
  6. moleditpy/plugins/File/animated_xyz_giffer.py +583 -0
  7. moleditpy/plugins/File/cube_viewer.py +689 -0
  8. moleditpy/plugins/File/gaussian_fchk_freq_analyzer.py +1148 -0
  9. moleditpy/plugins/File/mapped_cube_viewer.py +552 -0
  10. moleditpy/plugins/File/orca_out_freq_analyzer.py +1226 -0
  11. moleditpy/plugins/File/paste_xyz.py +336 -0
  12. moleditpy/plugins/Input Generator/gaussian_input_generator_neo.py +930 -0
  13. moleditpy/plugins/Input Generator/orca_input_generator_neo.py +1028 -0
  14. moleditpy/plugins/Input Generator/orca_xyz2inp_gui.py +286 -0
  15. moleditpy/plugins/Optimization/all-trans_optimizer.py +65 -0
  16. moleditpy/plugins/Optimization/complex_molecule_untangler.py +268 -0
  17. moleditpy/plugins/Optimization/conf_search.py +224 -0
  18. moleditpy/plugins/Utility/atom_colorizer.py +262 -0
  19. moleditpy/plugins/Utility/console.py +163 -0
  20. moleditpy/plugins/Utility/pubchem_ressolver.py +244 -0
  21. moleditpy/plugins/Utility/vdw_radii_overlay.py +432 -0
  22. {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/METADATA +1 -1
  23. {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/RECORD +27 -10
  24. {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/WHEEL +0 -0
  25. {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/entry_points.txt +0 -0
  26. {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/licenses/LICENSE +0 -0
  27. {moleditpy-2.2.0a0.dist-info → moleditpy-2.2.0a2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,552 @@
1
+ import os
2
+ import numpy as np
3
+ import pyvista as pv
4
+ from PyQt6.QtWidgets import (QFileDialog, QDockWidget, QWidget, QVBoxLayout,
5
+ QSlider, QLabel, QHBoxLayout, QPushButton, QMessageBox, QDoubleSpinBox, QProgressBar, QComboBox, QCheckBox, QDialog, QLineEdit, QFormLayout, QDialogButtonBox)
6
+ from PyQt6.QtCore import Qt
7
+
8
+ # --- Dependency Management ---
9
+ try:
10
+ from rdkit import Chem
11
+ from rdkit import Geometry
12
+ except ImportError:
13
+ Chem = None
14
+ Geometry = None
15
+
16
+ # Periodic Table for Radii
17
+ try:
18
+ from moleditpy.modules.constants import pt
19
+ except ImportError:
20
+ try:
21
+ from modules.constants import pt
22
+ except ImportError:
23
+ try:
24
+ from rdkit import Chem
25
+ pt = Chem.GetPeriodicTable()
26
+ except:
27
+ pt = None
28
+
29
+ __version__="2025.12.25" # Fixed version
30
+ __author__="HiroYokoyama"
31
+ PLUGIN_NAME = "Mapped Cube Viewer"
32
+
33
+ # --- Core Logic: Robust Parser from cube_viewer.py ---
34
+
35
+ def parse_cube_data(filename):
36
+ """
37
+ Robust Cube file parser copied from cube_viewer.py
38
+ """
39
+ with open(filename, 'r') as f:
40
+ lines = f.readlines()
41
+
42
+ if len(lines) < 6:
43
+ raise ValueError("File too short to be a Cube file.")
44
+
45
+ # --- Header Parsing ---
46
+ tokens = lines[2].split()
47
+ n_atoms_raw = int(tokens[0])
48
+ n_atoms = abs(n_atoms_raw)
49
+ origin_raw = np.array([float(tokens[1]), float(tokens[2]), float(tokens[3])])
50
+
51
+ def parse_vec(line):
52
+ t = line.split()
53
+ return int(t[0]), np.array([float(t[1]), float(t[2]), float(t[3])])
54
+
55
+ nx, x_vec_raw = parse_vec(lines[3])
56
+ ny, y_vec_raw = parse_vec(lines[4])
57
+ nz, z_vec_raw = parse_vec(lines[5])
58
+
59
+ is_angstrom_header = (nx < 0 or ny < 0 or nz < 0)
60
+ nx, ny, nz = abs(nx), abs(ny), abs(nz)
61
+
62
+ # --- Atoms Parsing ---
63
+ atoms = []
64
+ current_line = 6
65
+ if n_atoms_raw < 0:
66
+ try:
67
+ parts = lines[current_line].split()
68
+ if len(parts) != 5:
69
+ current_line += 1
70
+ except:
71
+ current_line += 1
72
+
73
+ for _ in range(n_atoms):
74
+ line = lines[current_line].split()
75
+ current_line += 1
76
+ atomic_num = int(line[0])
77
+ try:
78
+ x, y, z = float(line[2]), float(line[3]), float(line[4])
79
+ except:
80
+ x, y, z = 0.0, 0.0, 0.0
81
+ atoms.append((atomic_num, np.array([x, y, z])))
82
+
83
+ # --- Volumetric Data Parsing (Skip Metadata) ---
84
+ while current_line < len(lines):
85
+ line_content = lines[current_line].strip()
86
+ parts = line_content.split()
87
+ if not parts:
88
+ current_line += 1
89
+ continue
90
+ if len(parts) < 6: # Heuristic: Data lines usually have 6 cols
91
+ current_line += 1
92
+ continue
93
+ try:
94
+ float(parts[0])
95
+ except ValueError:
96
+ current_line += 1
97
+ continue
98
+ break
99
+
100
+ full_str = " ".join(lines[current_line:])
101
+ try:
102
+ data_values = np.fromstring(full_str, sep=' ')
103
+ except:
104
+ data_values = np.array([])
105
+
106
+ expected_size = nx * ny * nz
107
+ actual_size = len(data_values)
108
+
109
+ if actual_size > expected_size:
110
+ excess = actual_size - expected_size
111
+ data_values = data_values[excess:] # Trim from start if header noise included
112
+ elif actual_size < expected_size:
113
+ pad = np.zeros(expected_size - actual_size)
114
+ data_values = np.concatenate((data_values, pad))
115
+
116
+ return {
117
+ "atoms": atoms,
118
+ "origin": origin_raw,
119
+ "x_vec": x_vec_raw,
120
+ "y_vec": y_vec_raw,
121
+ "z_vec": z_vec_raw,
122
+ "dims": (nx, ny, nz),
123
+ "data_flat": data_values,
124
+ "is_angstrom_header": is_angstrom_header
125
+ }
126
+
127
+ def build_grid_from_meta(meta):
128
+ """
129
+ Reconstructs the PyVista grid.
130
+ Correctly handles standard Cube (Z-fast) mapping.
131
+ """
132
+ nx, ny, nz = meta['dims']
133
+ origin = meta['origin'].copy()
134
+ x_vec = meta['x_vec'].copy()
135
+ y_vec = meta['y_vec'].copy()
136
+ z_vec = meta['z_vec'].copy()
137
+ atoms = []
138
+
139
+ BOHR_TO_ANGSTROM = 0.529177210903
140
+ convert_to_ang = True
141
+ if meta['is_angstrom_header']:
142
+ convert_to_ang = False
143
+
144
+ if convert_to_ang:
145
+ origin *= BOHR_TO_ANGSTROM
146
+ x_vec *= BOHR_TO_ANGSTROM
147
+ y_vec *= BOHR_TO_ANGSTROM
148
+ z_vec *= BOHR_TO_ANGSTROM
149
+
150
+ for anum, pos in meta['atoms']:
151
+ p = pos.copy()
152
+ if convert_to_ang:
153
+ p *= BOHR_TO_ANGSTROM
154
+ atoms.append((anum, p))
155
+
156
+ # Grid Points Generation
157
+ x_range = np.arange(nx)
158
+ y_range = np.arange(ny)
159
+ z_range = np.arange(nz)
160
+
161
+ gx, gy, gz = np.meshgrid(x_range, y_range, z_range, indexing='ij')
162
+
163
+ # Flatten using Fortran order (X-fast) for VTK Structure
164
+ # !!! IMPORTANT FIX: Must match Flatten order of data !!!
165
+ gx_f = gx.flatten(order='F')
166
+ gy_f = gy.flatten(order='F')
167
+ gz_f = gz.flatten(order='F')
168
+
169
+ points = (origin +
170
+ np.outer(gx_f, x_vec) +
171
+ np.outer(gy_f, y_vec) +
172
+ np.outer(gz_f, z_vec))
173
+
174
+ grid = pv.StructuredGrid()
175
+ grid.points = points
176
+ grid.dimensions = [nx, ny, nz]
177
+
178
+ # Data Mapping
179
+ raw_data = meta['data_flat']
180
+
181
+ # Standard Cube: Z-Fast (C-order reshape)
182
+ # Then flatten F-order to match the X-fast points we just generated
183
+ vol_3d = raw_data.reshape((nx, ny, nz), order='C')
184
+
185
+ # Key name "property_values" for Mapped Viewer
186
+ grid.point_data["property_values"] = vol_3d.flatten(order='F')
187
+
188
+ return {"atoms": atoms}, grid
189
+
190
+ def read_cube(filename):
191
+ meta = parse_cube_data(filename)
192
+ return build_grid_from_meta(meta)
193
+
194
+ # --- Dialog & Widget (Identical Logic, just ensured imports) ---
195
+
196
+ class MappedCubeSetupDialog(QDialog):
197
+ def __init__(self, parent=None):
198
+ super().__init__(parent)
199
+ self.setWindowTitle("Mapped Cube Setup")
200
+ self.surface_file = None
201
+ self.property_file = None
202
+
203
+ layout = QVBoxLayout()
204
+ self.setLayout(layout)
205
+
206
+ form = QFormLayout()
207
+
208
+ # File 1: Surface
209
+ self.le_surf = QLineEdit()
210
+ btn_surf = QPushButton("Browse")
211
+ btn_surf.clicked.connect(self.browse_surf)
212
+ h1 = QHBoxLayout()
213
+ h1.addWidget(self.le_surf)
214
+ h1.addWidget(btn_surf)
215
+ form.addRow("Surface (Density):", h1)
216
+
217
+ # File 2: Property
218
+ self.le_prop = QLineEdit()
219
+ btn_prop = QPushButton("Browse")
220
+ btn_prop.clicked.connect(self.browse_prop)
221
+ h2 = QHBoxLayout()
222
+ h2.addWidget(self.le_prop)
223
+ h2.addWidget(btn_prop)
224
+ form.addRow("Property (Color):", h2)
225
+
226
+ layout.addLayout(form)
227
+
228
+ buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
229
+ buttons.accepted.connect(self.accept)
230
+ buttons.rejected.connect(self.reject)
231
+ layout.addWidget(buttons)
232
+
233
+ def browse_surf(self):
234
+ f, _ = QFileDialog.getOpenFileName(self, "Select Surface Cube", "", "Cube (*.cube *.cub)")
235
+ if f: self.le_surf.setText(f)
236
+
237
+ def browse_prop(self):
238
+ f, _ = QFileDialog.getOpenFileName(self, "Select Property Cube", "", "Cube (*.cube *.cub)")
239
+ if f: self.le_prop.setText(f)
240
+
241
+ def accept(self):
242
+ self.surface_file = self.le_surf.text()
243
+ self.property_file = self.le_prop.text()
244
+ if not os.path.exists(self.surface_file):
245
+ QMessageBox.warning(self, "Error", "Surface file not found.")
246
+ return
247
+ if not os.path.exists(self.property_file):
248
+ QMessageBox.warning(self, "Error", "Property file not found.")
249
+ return
250
+ super().accept()
251
+
252
+ class MappedWidget(QWidget):
253
+ def __init__(self, mw, dock, grid_surf, grid_prop):
254
+ super().__init__()
255
+ self.mw = mw
256
+ self.dock = dock
257
+ self.grid_surf = grid_surf
258
+ self.grid_prop = grid_prop
259
+ self.actor = None
260
+
261
+ self.layout = QVBoxLayout()
262
+ self.setLayout(self.layout)
263
+
264
+ # Controls
265
+ self.layout.addWidget(QLabel("<b>Surface Isovalue:</b>"))
266
+ self.iso_spin = QDoubleSpinBox()
267
+ self.iso_spin.setRange(-1000, 1000)
268
+ self.iso_spin.setDecimals(5)
269
+ self.iso_spin.setSingleStep(0.001)
270
+
271
+ # Default ISO
272
+ vals = self.grid_surf.point_data.get("property_values", np.array([0]))
273
+ # Robust default calculation
274
+ if len(vals) > 0:
275
+ if vals.max() > 0.1:
276
+ default_iso = 0.00200
277
+ else:
278
+ default_iso = float(np.mean(vals))
279
+ else:
280
+ default_iso = 0.002
281
+
282
+ self.iso_spin.setValue(default_iso)
283
+ self.iso_spin.setKeyboardTracking(False)
284
+ self.iso_spin.valueChanged.connect(lambda: self.update_mesh(auto_fit=False))
285
+ self.layout.addWidget(self.iso_spin)
286
+
287
+ # Color Range
288
+ self.layout.addWidget(QLabel("<b>Property Color Range:</b>"))
289
+
290
+ cbox = QHBoxLayout()
291
+ cbox.addWidget(QLabel("Style:"))
292
+ self.cmap_combo = QComboBox()
293
+ self.cmap_combo.addItems([
294
+ 'jet_r', 'jet',
295
+ 'rainbow_r', 'rainbow',
296
+ 'bwr_r', 'bwr',
297
+ 'seismic_r', 'seismic',
298
+ 'coolwarm_r', 'coolwarm',
299
+ 'viridis_r', 'viridis',
300
+ 'plasma_r', 'plasma',
301
+ 'magma_r', 'magma',
302
+ 'inferno_r', 'inferno'
303
+ ])
304
+ self.cmap_combo.setCurrentText('jet_r')
305
+ self.cmap_combo.currentTextChanged.connect(lambda: self.update_mesh(auto_fit=False))
306
+ cbox.addWidget(self.cmap_combo)
307
+ self.layout.addLayout(cbox)
308
+
309
+ rbox = QHBoxLayout()
310
+
311
+ pvals = self.grid_prop.point_data.get("property_values", np.array([-0.1, 0.1]))
312
+ if len(pvals) > 0:
313
+ vmin, vmax = pvals.min(), pvals.max()
314
+ else:
315
+ vmin, vmax = -0.1, 0.1
316
+
317
+ self.min_spin = QDoubleSpinBox()
318
+ self.min_spin.setRange(-1e20, 1e20)
319
+ self.min_spin.setDecimals(6)
320
+ self.min_spin.setSingleStep(0.01)
321
+ self.min_spin.setKeyboardTracking(False)
322
+ self.min_spin.setValue(vmin)
323
+ self.min_spin.valueChanged.connect(lambda: self.update_mesh(auto_fit=False))
324
+
325
+ self.max_spin = QDoubleSpinBox()
326
+ self.max_spin.setRange(-1e20, 1e20)
327
+ self.max_spin.setDecimals(6)
328
+ self.max_spin.setSingleStep(0.01)
329
+ self.max_spin.setKeyboardTracking(False)
330
+ self.max_spin.setValue(vmax)
331
+ self.max_spin.valueChanged.connect(lambda: self.update_mesh(auto_fit=False))
332
+
333
+ rbox.addWidget(QLabel("Min:"))
334
+ rbox.addWidget(self.min_spin)
335
+ rbox.addWidget(QLabel("Max:"))
336
+ rbox.addWidget(self.max_spin)
337
+ self.layout.addLayout(rbox)
338
+
339
+ btn_fit = QPushButton("Fit Range to Surface")
340
+ btn_fit.clicked.connect(lambda: self.update_mesh(auto_fit=True))
341
+ self.layout.addWidget(btn_fit)
342
+
343
+ # Opacity
344
+ self.layout.addWidget(QLabel("<b>Opacity:</b>"))
345
+ self.opacity_spin = QDoubleSpinBox()
346
+ self.opacity_spin.setRange(0, 1)
347
+ self.opacity_spin.setSingleStep(0.1)
348
+ self.opacity_spin.setValue(0.4)
349
+ self.opacity_spin.valueChanged.connect(lambda: self.update_mesh(auto_fit=False))
350
+ self.layout.addWidget(self.opacity_spin)
351
+
352
+ # Exports
353
+ ebox = QHBoxLayout()
354
+ btn_view = QPushButton("Export View")
355
+ btn_view.clicked.connect(self.export_view)
356
+ ebox.addWidget(btn_view)
357
+
358
+ btn_cbar = QPushButton("Export Color Bar")
359
+ btn_cbar.clicked.connect(self.export_colorbar)
360
+ ebox.addWidget(btn_cbar)
361
+ self.layout.addLayout(ebox)
362
+
363
+ # Options
364
+ self.check_transparent = QCheckBox("Transparent Background")
365
+ self.check_transparent.setChecked(True)
366
+ self.layout.addWidget(self.check_transparent)
367
+
368
+ self.layout.addStretch()
369
+ btn = QPushButton("Close")
370
+ btn.clicked.connect(self.close_plugin)
371
+ self.layout.addWidget(btn)
372
+
373
+ self.update_mesh(auto_fit=True)
374
+
375
+ def export_view(self):
376
+ f, _ = QFileDialog.getSaveFileName(self, "Save View", "mapped_view.png", "PNG Image (*.png)")
377
+ if f:
378
+ try:
379
+ self.mw.plotter.screenshot(f, transparent_background=self.check_transparent.isChecked())
380
+ QMessageBox.information(self, "Success", f"Saved View to {f}")
381
+ except Exception as e:
382
+ QMessageBox.critical(self, "Error", f"Save failed: {e}")
383
+
384
+ def export_colorbar(self):
385
+ f, _ = QFileDialog.getSaveFileName(self, "Save Color Bar", "mapped_colorbar.png", "PNG Image (*.png)")
386
+ if f:
387
+ try:
388
+ pl = pv.Plotter(off_screen=True, window_size=(2000, 400))
389
+ if self.check_transparent.isChecked():
390
+ pl.set_background(None)
391
+ else:
392
+ pl.set_background('white')
393
+
394
+ vmin = self.min_spin.value()
395
+ vmax = self.max_spin.value()
396
+ cmap = self.cmap_combo.currentText()
397
+
398
+ mesh = pv.Box()
399
+ mesh.point_data['scalars'] = np.linspace(vmin, vmax, mesh.n_points)
400
+
401
+ pl.add_mesh(mesh,
402
+ scalars='scalars',
403
+ cmap=cmap,
404
+ clim=[vmin, vmax],
405
+ show_scalar_bar=True,
406
+ opacity=0.0,
407
+ scalar_bar_args={
408
+ "title": "",
409
+ "vertical": False,
410
+ "n_labels": 5,
411
+ "italic": False,
412
+ "bold": False,
413
+ "title_font_size": 1,
414
+ "label_font_size": 50,
415
+ "color": "black",
416
+ "height": 0.4,
417
+ "width": 0.8,
418
+ "position_x": 0.1,
419
+ "position_y": 0.3
420
+ }
421
+ )
422
+
423
+ if hasattr(pl, 'scalar_bar'):
424
+ try:
425
+ sb = pl.scalar_bar
426
+ sb.GetLabelTextProperty().SetJustificationToCentered()
427
+ except: pass
428
+
429
+ pl.screenshot(f, transparent_background=self.check_transparent.isChecked())
430
+ pl.close()
431
+ QMessageBox.information(self, "Success", f"Saved Color Bar to {f}")
432
+ except Exception as e:
433
+ import traceback
434
+ traceback.print_exc()
435
+ QMessageBox.critical(self, "Error", f"Save failed: {e}")
436
+
437
+ def update_mesh(self, auto_fit=False):
438
+ try:
439
+ iso_val = self.iso_spin.value()
440
+ opacity = self.opacity_spin.value()
441
+
442
+ iso = self.grid_surf.contour([iso_val], scalars="property_values")
443
+ if iso.n_points == 0:
444
+ return
445
+
446
+ mapped = iso.sample(self.grid_prop)
447
+
448
+ clim = [self.min_spin.value(), self.max_spin.value()]
449
+ if auto_fit:
450
+ mvals = mapped.point_data.get("property_values")
451
+ if mvals is not None and len(mvals) > 0:
452
+ vmin, vmax = mvals.min(), mvals.max()
453
+ if vmax - vmin < 1e-9: vmax += 0.001
454
+ clim = [vmin, vmax]
455
+
456
+ self.min_spin.blockSignals(True); self.min_spin.setValue(vmin); self.min_spin.blockSignals(False)
457
+ self.max_spin.blockSignals(True); self.max_spin.setValue(vmax); self.max_spin.blockSignals(False)
458
+
459
+ if self.actor:
460
+ self.mw.plotter.remove_actor(self.actor)
461
+
462
+ self.actor = self.mw.plotter.add_mesh(
463
+ mapped,
464
+ scalars="property_values",
465
+ cmap=self.cmap_combo.currentText(),
466
+ clim=clim,
467
+ smooth_shading=True,
468
+ opacity=opacity,
469
+ name="mapped_mesh_dock",
470
+ scalar_bar_args={'title': ''}
471
+ )
472
+
473
+ self.mw.plotter.render()
474
+
475
+ except Exception as e:
476
+ print(f"Update Error: {e}")
477
+
478
+ def close_plugin(self):
479
+ try:
480
+ self.mw.plotter.remove_actor(self.actor)
481
+ self.mw.plotter.render()
482
+ except: pass
483
+ if self.dock:
484
+ self.dock.close()
485
+ try:
486
+ self.mw.clear_all()
487
+ except: pass
488
+ self.close()
489
+
490
+ # --- Entry Point ---
491
+ def run(mw):
492
+ dlg = MappedCubeSetupDialog(mw)
493
+ if dlg.exec() != QDialog.DialogCode.Accepted:
494
+ return
495
+
496
+ s_file = dlg.surface_file
497
+ p_file = dlg.property_file
498
+
499
+ if not s_file or not p_file: return
500
+
501
+ try:
502
+ from PyQt6.QtWidgets import QApplication, QProgressDialog
503
+
504
+ progress = QProgressDialog("Reading Cube Files...", "Cancel", 0, 2, mw)
505
+ progress.setWindowModality(Qt.WindowModality.WindowModal)
506
+ progress.show()
507
+
508
+ # Using the robust read_cube logic
509
+ meta1, grid_surf = read_cube(s_file)
510
+ progress.setValue(1)
511
+ QApplication.processEvents()
512
+
513
+ meta2, grid_prop = read_cube(p_file)
514
+ progress.setValue(2)
515
+ QApplication.processEvents()
516
+ progress.close()
517
+
518
+ # Visualize Atoms
519
+ atoms = meta1['atoms']
520
+ if Chem:
521
+ s = f"{len(atoms)}\n\n"
522
+ for n, p in atoms:
523
+ sym = pt.GetElementSymbol(n) if pt else "C"
524
+ s += f"{sym} {p[0]} {p[1]} {p[2]}\n"
525
+ mol = Chem.MolFromXYZBlock(s)
526
+ try:
527
+ from rdkit.Chem import rdDetermineBonds
528
+ rdDetermineBonds.DetermineConnectivity(mol)
529
+ except: pass
530
+
531
+ mw.current_mol = mol
532
+ if hasattr(mw, 'draw_molecule_3d'):
533
+ mw.draw_molecule_3d(mol)
534
+
535
+ if hasattr(mw, 'main_window_ui_manager'):
536
+ try: mw.main_window_ui_manager._enter_3d_viewer_ui_mode()
537
+ except: pass
538
+
539
+ for d in mw.findChildren(QDockWidget):
540
+ if d.windowTitle() == "Mapped Viewer":
541
+ d.close()
542
+
543
+ dock = QDockWidget("Mapped Viewer", mw)
544
+ dock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea)
545
+ widget = MappedWidget(mw, dock, grid_surf, grid_prop)
546
+ dock.setWidget(widget)
547
+ mw.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, dock)
548
+
549
+ except Exception as e:
550
+ import traceback
551
+ traceback.print_exc()
552
+ QMessageBox.critical(mw, "Error", f"Failed:\n{e}")