setiastrosuitepro 1.7.5.post1__py3-none-any.whl → 1.8.0__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. setiastro/saspro/_generated/build_info.py +2 -2
  2. setiastro/saspro/accel_installer.py +21 -8
  3. setiastro/saspro/accel_workers.py +11 -12
  4. setiastro/saspro/comet_stacking.py +113 -85
  5. setiastro/saspro/cosmicclarity.py +604 -826
  6. setiastro/saspro/cosmicclarity_engines/benchmark_engine.py +715 -0
  7. setiastro/saspro/cosmicclarity_engines/darkstar_engine.py +576 -0
  8. setiastro/saspro/cosmicclarity_engines/denoise_engine.py +567 -0
  9. setiastro/saspro/cosmicclarity_engines/satellite_engine.py +620 -0
  10. setiastro/saspro/cosmicclarity_engines/sharpen_engine.py +587 -0
  11. setiastro/saspro/cosmicclarity_engines/superres_engine.py +412 -0
  12. setiastro/saspro/gui/main_window.py +14 -0
  13. setiastro/saspro/gui/mixins/menu_mixin.py +2 -0
  14. setiastro/saspro/model_manager.py +306 -0
  15. setiastro/saspro/model_workers.py +65 -0
  16. setiastro/saspro/ops/benchmark.py +320 -0
  17. setiastro/saspro/ops/settings.py +308 -9
  18. setiastro/saspro/remove_stars.py +424 -442
  19. setiastro/saspro/resources.py +73 -10
  20. setiastro/saspro/runtime_torch.py +107 -22
  21. setiastro/saspro/signature_insert.py +14 -3
  22. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.dist-info}/METADATA +2 -1
  23. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.dist-info}/RECORD +27 -18
  24. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.dist-info}/WHEEL +0 -0
  25. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.dist-info}/entry_points.txt +0 -0
  26. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.dist-info}/licenses/LICENSE +0 -0
  27. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.dist-info}/licenses/license.txt +0 -0
@@ -4,10 +4,14 @@ from PyQt6.QtWidgets import (
4
4
  QHBoxLayout, QVBoxLayout, QWidget, QCheckBox, QComboBox, QSpinBox, QDoubleSpinBox, QLabel, QColorDialog, QFontDialog, QSlider)
5
5
  from PyQt6.QtCore import QSettings, Qt
6
6
  import pytz # for timezone list
7
-
7
+ from setiastro.saspro.accel_installer import current_backend
8
+ import sys, platform
9
+ from PyQt6.QtWidgets import QToolButton, QProgressDialog
10
+ from PyQt6.QtCore import QThread
8
11
  # i18n support
9
12
  from setiastro.saspro.i18n import get_available_languages, get_saved_language, save_language
10
-
13
+ import importlib.util
14
+ import importlib.metadata
11
15
 
12
16
  class SettingsDialog(QDialog):
13
17
  """
@@ -24,7 +28,7 @@ class SettingsDialog(QDialog):
24
28
 
25
29
  # ---- Existing fields (paths, checkboxes, etc.) ----
26
30
  self.le_graxpert = QLineEdit()
27
- self.le_cosmic = QLineEdit()
31
+
28
32
  self.le_starnet = QLineEdit()
29
33
  self.le_astap = QLineEdit()
30
34
 
@@ -75,12 +79,12 @@ class SettingsDialog(QDialog):
75
79
  self._initial_language = "en" # placeholder, set in refresh_ui
76
80
 
77
81
  btn_grax = QPushButton(self.tr("Browse…")); btn_grax.clicked.connect(lambda: self._browse_into(self.le_graxpert))
78
- btn_ccl = QPushButton(self.tr("Browse…")); btn_ccl.clicked.connect(lambda: self._browse_dir(self.le_cosmic))
82
+
79
83
  btn_star = QPushButton(self.tr("Browse…")); btn_star.clicked.connect(lambda: self._browse_into(self.le_starnet))
80
84
  btn_astap = QPushButton(self.tr("Browse…")); btn_astap.clicked.connect(lambda: self._browse_into(self.le_astap))
81
85
 
82
86
  row_grax = QHBoxLayout(); row_grax.addWidget(self.le_graxpert); row_grax.addWidget(btn_grax)
83
- row_ccl = QHBoxLayout(); row_ccl.addWidget(self.le_cosmic); row_ccl.addWidget(btn_ccl)
87
+
84
88
  row_star = QHBoxLayout(); row_star.addWidget(self.le_starnet); row_star.addWidget(btn_star)
85
89
  row_astap = QHBoxLayout(); row_astap.addWidget(self.le_astap); row_astap.addWidget(btn_astap)
86
90
 
@@ -163,7 +167,7 @@ class SettingsDialog(QDialog):
163
167
  # ---- Left column: Paths & Integrations ----
164
168
  left_col.addRow(QLabel(self.tr("<b>Paths & Integrations</b>")))
165
169
  w = QWidget(); w.setLayout(row_grax); left_col.addRow(self.tr("GraXpert executable:"), w)
166
- w = QWidget(); w.setLayout(row_ccl); left_col.addRow(self.tr("Cosmic Clarity folder:"), w)
170
+
167
171
  w = QWidget(); w.setLayout(row_star); left_col.addRow(self.tr("StarNet executable:"), w)
168
172
  w = QWidget(); w.setLayout(row_astap); left_col.addRow(self.tr("ASTAP executable:"), w)
169
173
  left_col.addRow(self.tr("Astrometry.net API key:"), self.le_astrometry)
@@ -182,6 +186,62 @@ class SettingsDialog(QDialog):
182
186
  left_col.addRow(self.tr("Background Opacity:"), w_bg_opacity)
183
187
  left_col.addRow(self.tr("Background Image:"), w_bg_image)
184
188
 
189
+ left_col.addRow(QLabel(self.tr("<b>Acceleration</b>")))
190
+
191
+ accel_row = QHBoxLayout()
192
+ self.backend_label = QLabel(self.tr("Backend: {0}").format(current_backend()))
193
+ accel_row.addWidget(self.backend_label)
194
+ # NEW: dependency status label (rich text)
195
+ self.accel_deps_label = QLabel()
196
+ self.accel_deps_label.setTextFormat(Qt.TextFormat.RichText)
197
+ self.accel_deps_label.setStyleSheet("color:#888;") # subtle
198
+ self.accel_deps_label.setToolTip(self.tr("Installed acceleration-related Python packages"))
199
+ accel_row.addSpacing(12)
200
+ accel_row.addWidget(self.accel_deps_label)
201
+
202
+ # NEW: preference combo
203
+ self.cb_accel_pref = QComboBox()
204
+ self.cb_accel_pref.addItems([
205
+ "Auto (recommended)",
206
+ "CUDA (NVIDIA)",
207
+ "Intel XPU (Arc/Xe)",
208
+ "DirectML (Windows AMD/Intel)",
209
+ "CPU only",
210
+ ])
211
+ # hide DirectML option on non-Windows if you want:
212
+ if platform.system() != "Windows":
213
+ # remove the DirectML entry (index 3)
214
+ self.cb_accel_pref.removeItem(3)
215
+
216
+ accel_row.addSpacing(8)
217
+ accel_row.addWidget(QLabel(self.tr("Preference:")))
218
+ accel_row.addWidget(self.cb_accel_pref)
219
+
220
+ self.install_accel_btn = QPushButton(self.tr("Install/Update GPU Acceleration…"))
221
+ accel_row.addWidget(self.install_accel_btn)
222
+
223
+ gpu_help_btn = QToolButton()
224
+ gpu_help_btn.setText("?")
225
+ gpu_help_btn.setToolTip(self.tr("If GPU still not being used — click for fix steps"))
226
+ gpu_help_btn.clicked.connect(self._show_gpu_accel_fix_help)
227
+ accel_row.addWidget(gpu_help_btn)
228
+
229
+ accel_row.addStretch(1)
230
+ w_accel = QWidget()
231
+ w_accel.setLayout(accel_row)
232
+ left_col.addRow(w_accel)
233
+
234
+ # ---- Models ----
235
+ right_col.addRow(QLabel(self.tr("<b>AI Models</b>")))
236
+
237
+ self.lbl_models_status = QLabel(self.tr("Status: (unknown)"))
238
+ self.lbl_models_status.setStyleSheet("color:#888;")
239
+ right_col.addRow(self.lbl_models_status)
240
+
241
+ self.btn_models_update = QPushButton(self.tr("Download/Update Models…"))
242
+ self.btn_models_update.clicked.connect(self._models_update_clicked)
243
+ right_col.addRow(self.btn_models_update)
244
+
185
245
  # ---- Right column: WIMS + RA/Dec + Updates + Display ----
186
246
  right_col.addRow(QLabel(self.tr("<b>What's In My Sky — Defaults</b>")))
187
247
  right_col.addRow(self.tr("Latitude (°):"), self.sp_lat)
@@ -227,13 +287,218 @@ class SettingsDialog(QDialog):
227
287
  btns = QDialogButtonBox(
228
288
  QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self
229
289
  )
290
+ self.cb_accel_pref.currentIndexChanged.connect(self._accel_pref_changed)
291
+
292
+
293
+
230
294
  btns.accepted.connect(self._save_and_accept)
231
295
  btns.rejected.connect(self.reject)
232
296
  root.addWidget(btns)
233
-
297
+ self.install_accel_btn.clicked.connect(self._install_or_update_accel)
234
298
  # Initial Load:
235
299
  self.refresh_ui()
236
300
 
301
+ def _models_update_clicked(self):
302
+ from PyQt6.QtWidgets import QMessageBox, QProgressDialog
303
+ from PyQt6.QtCore import Qt, QThread
304
+
305
+ # NOTE: these must be FILE links or file IDs, not folder links.
306
+ # Put your actual *zip file* share links here once you create them.
307
+ PRIMARY = "https://drive.google.com/file/d/1n4p0grtNpfllalMqtgaEmsTYaFhT5u7Y/view?usp=drive_link"
308
+ BACKUP = "https://drive.google.com/file/d/1uRGJCITlfMMN89ZkOO5ICWEKMH24KGit/view?usp=drive_link"
309
+
310
+
311
+ self.btn_models_update.setEnabled(False)
312
+
313
+ pd = QProgressDialog(self.tr("Preparing…"), self.tr("Cancel"), 0, 0, self)
314
+ pd.setWindowTitle(self.tr("Updating Models"))
315
+ pd.setWindowModality(Qt.WindowModality.ApplicationModal)
316
+ pd.setAutoClose(True)
317
+ pd.setMinimumDuration(0)
318
+ pd.show()
319
+
320
+ from setiastro.saspro.model_workers import ModelsDownloadWorker
321
+
322
+ self._models_thread = QThread(self)
323
+ self._models_worker = ModelsDownloadWorker(PRIMARY, BACKUP, expected_sha256=None)
324
+ self._models_worker.moveToThread(self._models_thread)
325
+
326
+ self._models_thread.started.connect(self._models_worker.run, Qt.ConnectionType.QueuedConnection)
327
+ self._models_worker.progress.connect(pd.setLabelText, Qt.ConnectionType.QueuedConnection)
328
+ def should_cancel():
329
+ return self._models_thread.isInterruptionRequested()
330
+ def _cancel():
331
+ if self._models_thread.isRunning():
332
+ self._models_thread.requestInterruption()
333
+ pd.canceled.connect(_cancel, Qt.ConnectionType.QueuedConnection)
334
+
335
+ def _done(ok: bool, msg: str):
336
+ pd.reset()
337
+ pd.deleteLater()
338
+
339
+ self._models_thread.quit()
340
+ self._models_thread.wait()
341
+
342
+ self.btn_models_update.setEnabled(True)
343
+ self._refresh_models_status()
344
+
345
+ if ok:
346
+ QMessageBox.information(self, self.tr("Models"), self.tr("✅ {0}").format(msg))
347
+ else:
348
+ QMessageBox.warning(self, self.tr("Models"), self.tr("❌ {0}").format(msg))
349
+
350
+ self._models_worker.finished.connect(_done, Qt.ConnectionType.QueuedConnection)
351
+ self._models_thread.finished.connect(self._models_worker.deleteLater, Qt.ConnectionType.QueuedConnection)
352
+ self._models_thread.finished.connect(self._models_thread.deleteLater, Qt.ConnectionType.QueuedConnection)
353
+
354
+ self._models_thread.start()
355
+
356
+ def _refresh_models_status(self):
357
+ from setiastro.saspro.model_manager import read_installed_manifest, models_root
358
+ m = read_installed_manifest()
359
+ if not m:
360
+ self.lbl_models_status.setText(self.tr("Status: not installed"))
361
+ return
362
+ fid = m.get("file_id", "")
363
+ self.lbl_models_status.setText(
364
+ self.tr("Status: installed\nLocation: {0}").format(models_root())
365
+ )
366
+
367
+
368
+ def _accel_pref_changed(self, idx: int):
369
+ inv = {0:"auto", 1:"cuda", 2:"xpu", 3:"directml", 4:"cpu"}
370
+ val = inv.get(idx, "auto")
371
+ self.settings.setValue("accel/preferred_backend", val)
372
+ self.settings.sync()
373
+
374
+ def _show_gpu_accel_fix_help(self):
375
+ from PyQt6.QtWidgets import QMessageBox
376
+ QMessageBox.information(
377
+ self, self.tr("GPU Acceleration Help"),
378
+ self.tr(
379
+ "If GPU is not being used:\n"
380
+ " • Click Install/Update GPU Acceleration…\n"
381
+ " • Restart SAS Pro\n"
382
+ " • On NVIDIA systems, verify drivers and that 'nvidia-smi' works.\n"
383
+ " • On Windows non-NVIDIA, DirectML may be used.\n"
384
+ )
385
+ )
386
+
387
+ def _pkg_status(self, dist_name: str, import_name: str | None = None) -> tuple[bool, str]:
388
+ """
389
+ Returns (installed?, display_text). Does NOT import the module.
390
+ dist_name = pip distribution name used for version lookup (e.g., 'torchvision')
391
+ import_name = python import name used for find_spec (e.g., 'torch_directml')
392
+ """
393
+ mod = import_name or dist_name.replace("-", "_")
394
+ present = importlib.util.find_spec(mod) is not None
395
+
396
+ ver = ""
397
+ if present:
398
+ try:
399
+ ver = importlib.metadata.version(dist_name)
400
+ except importlib.metadata.PackageNotFoundError:
401
+ # Sometimes dist name differs; fall back to "installed" without version
402
+ ver = ""
403
+ except Exception:
404
+ ver = ""
405
+
406
+ if present:
407
+ return True, (f"✅ {ver}" if ver else "✅ installed")
408
+ return False, "— not installed"
409
+
410
+
411
+ def _format_accel_deps_text(self) -> str:
412
+ # Torch + friends
413
+ torch_ok, torch_txt = self._pkg_status("torch", "torch")
414
+ dml_ok, dml_txt = self._pkg_status("torch-directml", "torch_directml")
415
+ ta_ok, ta_txt = self._pkg_status("torchaudio", "torchaudio")
416
+ tv_ok, tv_txt = self._pkg_status("torchvision", "torchvision")
417
+
418
+ # Pretty, compact, and stable in a QLabel
419
+ return (
420
+ f"Torch: <b>{torch_txt}</b><br>"
421
+ f"Torch-DirectML: <b>{dml_txt}</b><br>"
422
+ f"TorchAudio: <b>{ta_txt}</b><br>"
423
+ f"TorchVision: <b>{tv_txt}</b>"
424
+ )
425
+
426
+
427
+ def _install_or_update_accel(self):
428
+ import sys, platform
429
+ from PyQt6.QtWidgets import QMessageBox, QProgressDialog
430
+ from PyQt6.QtCore import Qt, QThread
431
+ from setiastro.saspro.accel_installer import current_backend
432
+ from setiastro.saspro.accel_workers import AccelInstallWorker # wherever yours lives
433
+
434
+ v = sys.version_info
435
+ if not (v.major == 3 and v.minor in (10, 11, 12)):
436
+ why = self.tr("This app is running on Python {0}.{1}. GPU acceleration requires Python 3.10, 3.11, or 3.12.").format(v.major, v.minor)
437
+ tip = ""
438
+ sysname = platform.system()
439
+ if sysname == "Darwin":
440
+ tip = self.tr("\n\nmacOS tip (Apple Silicon):\n • Install Python 3.12: brew install python@3.12\n • Relaunch the app.")
441
+ elif sysname == "Windows":
442
+ tip = self.tr("\n\nWindows tip:\n • Install Python 3.12/3.11/3.10 (x64) from python.org\n • Relaunch the app.")
443
+ else:
444
+ tip = self.tr("\n\nLinux tip:\n • Install python3.12 or 3.11 via your package manager\n • Relaunch the app.")
445
+
446
+ QMessageBox.warning(self, self.tr("Unsupported Python Version"), why + tip)
447
+ self.backend_label.setText(self.tr("Backend: CPU (Python version not supported for GPU install)"))
448
+ return
449
+
450
+ self.install_accel_btn.setEnabled(False)
451
+ self.backend_label.setText(self.tr("Backend: installing…"))
452
+
453
+ pref = (self.settings.value("accel/preferred_backend", "auto", type=str) or "auto").lower()
454
+
455
+ self._accel_pd = QProgressDialog(self.tr("Preparing runtime…"), self.tr("Cancel"), 0, 0, self)
456
+ self._accel_pd.setWindowTitle(self.tr("Installing GPU Acceleration"))
457
+ self._accel_pd.setWindowModality(Qt.WindowModality.ApplicationModal)
458
+ self._accel_pd.setAutoClose(True)
459
+ self._accel_pd.setMinimumDuration(0)
460
+ self._accel_pd.show()
461
+
462
+ self._accel_thread = QThread(self)
463
+ self._accel_worker = AccelInstallWorker(prefer_gpu=True, preferred_backend=pref)
464
+ self._accel_worker.moveToThread(self._accel_thread)
465
+
466
+ self._accel_thread.started.connect(self._accel_worker.run, Qt.ConnectionType.QueuedConnection)
467
+ self._accel_worker.progress.connect(self._accel_pd.setLabelText, Qt.ConnectionType.QueuedConnection)
468
+
469
+ def _cancel():
470
+ if self._accel_thread.isRunning():
471
+ self._accel_thread.requestInterruption()
472
+ self._accel_pd.canceled.connect(_cancel, Qt.ConnectionType.QueuedConnection)
473
+
474
+ def _done(ok: bool, msg: str):
475
+ if getattr(self, "_accel_pd", None):
476
+ self._accel_pd.reset()
477
+ self._accel_pd.deleteLater()
478
+ self._accel_pd = None
479
+
480
+ self._accel_thread.quit()
481
+ self._accel_thread.wait()
482
+
483
+ self.install_accel_btn.setEnabled(True)
484
+ self.backend_label.setText(self.tr("Backend: {0}").format(current_backend()))
485
+ try:
486
+ self.accel_deps_label.setText(self._format_accel_deps_text())
487
+ except Exception:
488
+ pass
489
+
490
+ if ok:
491
+ QMessageBox.information(self, self.tr("Acceleration"), self.tr("✅ {0}").format(msg))
492
+ else:
493
+ QMessageBox.warning(self, self.tr("Acceleration"), self.tr("❌ {0}").format(msg))
494
+
495
+ self._accel_worker.finished.connect(_done, Qt.ConnectionType.QueuedConnection)
496
+ self._accel_thread.finished.connect(self._accel_worker.deleteLater, Qt.ConnectionType.QueuedConnection)
497
+ self._accel_thread.finished.connect(self._accel_thread.deleteLater, Qt.ConnectionType.QueuedConnection)
498
+
499
+ self._accel_thread.start()
500
+
501
+
237
502
  def refresh_ui(self):
238
503
  """
239
504
  Reloads all settings from self.settings and updates the UI widgets.
@@ -279,7 +544,7 @@ class SettingsDialog(QDialog):
279
544
 
280
545
  # Path fields
281
546
  self.le_graxpert.setText(self.settings.value("paths/graxpert", "", type=str))
282
- self.le_cosmic.setText(self.settings.value("paths/cosmic_clarity", "", type=str))
547
+
283
548
  self.le_starnet.setText(self.settings.value("paths/starnet", "", type=str))
284
549
  self.le_astap.setText(self.settings.value("paths/astap", "", type=str))
285
550
  self.le_astrometry.setText(self.settings.value("api/astrometry_key", "", type=str))
@@ -330,6 +595,29 @@ class SettingsDialog(QDialog):
330
595
  self.sp_wcs_step.setEnabled(self.cb_wcs_mode.currentIndex() == 1)
331
596
  self.sp_wcs_step.setSuffix(" °" if self.cb_wcs_unit.currentIndex() == 0 else " arcmin")
332
597
 
598
+ pref = (self.settings.value("accel/preferred_backend", "auto", type=str) or "auto").lower()
599
+
600
+ # map stored -> combobox index (adjust if you removed DirectML on non-Windows)
601
+ idx_map = {"auto": 0, "cuda": 1, "xpu": 2, "directml": 3, "cpu": 4}
602
+
603
+ idx = idx_map.get(pref, 0)
604
+ # if non-Windows and directml was saved earlier, clamp to Auto
605
+ if platform.system() != "Windows" and pref == "directml":
606
+ idx = 0
607
+
608
+ self.cb_accel_pref.setCurrentIndex(idx)
609
+
610
+ from setiastro.saspro.accel_installer import current_backend
611
+ self.backend_label.setText(self.tr("Backend: {0}").format(current_backend()))
612
+ try:
613
+ self.accel_deps_label.setText(self._format_accel_deps_text())
614
+ except Exception:
615
+ self.accel_deps_label.setText(self.tr("Torch: — unknown"))
616
+ try:
617
+ self._refresh_models_status()
618
+ except Exception:
619
+ pass
620
+
333
621
  def reject(self):
334
622
  """User cancelled: restore the original background opacity (revert live changes)."""
335
623
  try:
@@ -439,7 +727,7 @@ class SettingsDialog(QDialog):
439
727
  def _save_and_accept(self):
440
728
  # Paths / Integrations
441
729
  self.settings.setValue("paths/graxpert", self.le_graxpert.text().strip())
442
- self.settings.setValue("paths/cosmic_clarity", self.le_cosmic.text().strip())
730
+
443
731
  self.settings.setValue("paths/starnet", self.le_starnet.text().strip())
444
732
  self.settings.setValue("paths/astap", self.le_astap.text().strip())
445
733
  self.settings.setValue("shortcuts/save_on_exit", self.chk_save_shortcuts.isChecked())
@@ -466,6 +754,12 @@ class SettingsDialog(QDialog):
466
754
  self.settings.setValue("updates/url", self.le_updates_url.text().strip())
467
755
  self.settings.setValue("display/autostretch_24bit", self.chk_autostretch_24bit.isChecked())
468
756
 
757
+ # accel preference
758
+ pref_idx = self.cb_accel_pref.currentIndex()
759
+ # map index -> stored string (again, adjust if DirectML removed on non-Windows)
760
+ inv = {0:"auto", 1:"cuda", 2:"xpu", 3:"directml", 4:"cpu"}
761
+ self.settings.setValue("accel/preferred_backend", inv.get(pref_idx, "auto"))
762
+
469
763
  # Custom background: persist the chosen path (empty -> remove)
470
764
  bg_path = (self.le_bg_path.text() or "").strip()
471
765
  if bg_path:
@@ -520,6 +814,11 @@ class SettingsDialog(QDialog):
520
814
  if hasattr(p, "mdi") and hasattr(p.mdi, "viewport"):
521
815
  p.mdi.viewport().update()
522
816
 
817
+ try:
818
+ self.settings.remove("paths/cosmic_clarity")
819
+ except Exception:
820
+ pass
821
+
523
822
  self.accept()
524
823
 
525
824
  from PyQt6.QtGui import QColor, QFont