setiastrosuitepro 1.6.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.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (174) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/saspro/__init__.py +20 -0
  3. setiastro/saspro/__main__.py +784 -0
  4. setiastro/saspro/_generated/__init__.py +7 -0
  5. setiastro/saspro/_generated/build_info.py +2 -0
  6. setiastro/saspro/abe.py +1295 -0
  7. setiastro/saspro/abe_preset.py +196 -0
  8. setiastro/saspro/aberration_ai.py +694 -0
  9. setiastro/saspro/aberration_ai_preset.py +224 -0
  10. setiastro/saspro/accel_installer.py +218 -0
  11. setiastro/saspro/accel_workers.py +30 -0
  12. setiastro/saspro/add_stars.py +621 -0
  13. setiastro/saspro/astrobin_exporter.py +1007 -0
  14. setiastro/saspro/astrospike.py +153 -0
  15. setiastro/saspro/astrospike_python.py +1839 -0
  16. setiastro/saspro/autostretch.py +196 -0
  17. setiastro/saspro/backgroundneutral.py +560 -0
  18. setiastro/saspro/batch_convert.py +325 -0
  19. setiastro/saspro/batch_renamer.py +519 -0
  20. setiastro/saspro/blemish_blaster.py +488 -0
  21. setiastro/saspro/blink_comparator_pro.py +2923 -0
  22. setiastro/saspro/bundles.py +61 -0
  23. setiastro/saspro/bundles_dock.py +114 -0
  24. setiastro/saspro/cheat_sheet.py +168 -0
  25. setiastro/saspro/clahe.py +342 -0
  26. setiastro/saspro/comet_stacking.py +1377 -0
  27. setiastro/saspro/config.py +38 -0
  28. setiastro/saspro/config_bootstrap.py +40 -0
  29. setiastro/saspro/config_manager.py +316 -0
  30. setiastro/saspro/continuum_subtract.py +1617 -0
  31. setiastro/saspro/convo.py +1397 -0
  32. setiastro/saspro/convo_preset.py +414 -0
  33. setiastro/saspro/copyastro.py +187 -0
  34. setiastro/saspro/cosmicclarity.py +1564 -0
  35. setiastro/saspro/cosmicclarity_preset.py +407 -0
  36. setiastro/saspro/crop_dialog_pro.py +948 -0
  37. setiastro/saspro/crop_preset.py +189 -0
  38. setiastro/saspro/curve_editor_pro.py +2544 -0
  39. setiastro/saspro/curves_preset.py +375 -0
  40. setiastro/saspro/debayer.py +670 -0
  41. setiastro/saspro/debug_utils.py +29 -0
  42. setiastro/saspro/dnd_mime.py +35 -0
  43. setiastro/saspro/doc_manager.py +2634 -0
  44. setiastro/saspro/exoplanet_detector.py +2166 -0
  45. setiastro/saspro/file_utils.py +284 -0
  46. setiastro/saspro/fitsmodifier.py +744 -0
  47. setiastro/saspro/free_torch_memory.py +48 -0
  48. setiastro/saspro/frequency_separation.py +1343 -0
  49. setiastro/saspro/function_bundle.py +1594 -0
  50. setiastro/saspro/ghs_dialog_pro.py +660 -0
  51. setiastro/saspro/ghs_preset.py +284 -0
  52. setiastro/saspro/graxpert.py +634 -0
  53. setiastro/saspro/graxpert_preset.py +287 -0
  54. setiastro/saspro/gui/__init__.py +0 -0
  55. setiastro/saspro/gui/main_window.py +8494 -0
  56. setiastro/saspro/gui/mixins/__init__.py +33 -0
  57. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  58. setiastro/saspro/gui/mixins/file_mixin.py +445 -0
  59. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  60. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  61. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  62. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  63. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  64. setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
  65. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  66. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  67. setiastro/saspro/halobgon.py +462 -0
  68. setiastro/saspro/header_viewer.py +445 -0
  69. setiastro/saspro/headless_utils.py +88 -0
  70. setiastro/saspro/histogram.py +753 -0
  71. setiastro/saspro/history_explorer.py +939 -0
  72. setiastro/saspro/image_combine.py +414 -0
  73. setiastro/saspro/image_peeker_pro.py +1596 -0
  74. setiastro/saspro/imageops/__init__.py +37 -0
  75. setiastro/saspro/imageops/mdi_snap.py +292 -0
  76. setiastro/saspro/imageops/scnr.py +36 -0
  77. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  78. setiastro/saspro/imageops/stretch.py +244 -0
  79. setiastro/saspro/isophote.py +1179 -0
  80. setiastro/saspro/layers.py +208 -0
  81. setiastro/saspro/layers_dock.py +714 -0
  82. setiastro/saspro/lazy_imports.py +193 -0
  83. setiastro/saspro/legacy/__init__.py +2 -0
  84. setiastro/saspro/legacy/image_manager.py +2226 -0
  85. setiastro/saspro/legacy/numba_utils.py +3659 -0
  86. setiastro/saspro/legacy/xisf.py +1071 -0
  87. setiastro/saspro/linear_fit.py +534 -0
  88. setiastro/saspro/live_stacking.py +1830 -0
  89. setiastro/saspro/log_bus.py +5 -0
  90. setiastro/saspro/logging_config.py +460 -0
  91. setiastro/saspro/luminancerecombine.py +309 -0
  92. setiastro/saspro/main_helpers.py +201 -0
  93. setiastro/saspro/mask_creation.py +928 -0
  94. setiastro/saspro/masks_core.py +56 -0
  95. setiastro/saspro/mdi_widgets.py +353 -0
  96. setiastro/saspro/memory_utils.py +666 -0
  97. setiastro/saspro/metadata_patcher.py +75 -0
  98. setiastro/saspro/mfdeconv.py +3826 -0
  99. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  100. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  101. setiastro/saspro/mfdeconvsport.py +2382 -0
  102. setiastro/saspro/minorbodycatalog.py +567 -0
  103. setiastro/saspro/morphology.py +382 -0
  104. setiastro/saspro/multiscale_decomp.py +1290 -0
  105. setiastro/saspro/nbtorgb_stars.py +531 -0
  106. setiastro/saspro/numba_utils.py +3044 -0
  107. setiastro/saspro/numba_warmup.py +141 -0
  108. setiastro/saspro/ops/__init__.py +9 -0
  109. setiastro/saspro/ops/command_help_dialog.py +623 -0
  110. setiastro/saspro/ops/command_runner.py +217 -0
  111. setiastro/saspro/ops/commands.py +1594 -0
  112. setiastro/saspro/ops/script_editor.py +1102 -0
  113. setiastro/saspro/ops/scripts.py +1413 -0
  114. setiastro/saspro/ops/settings.py +560 -0
  115. setiastro/saspro/parallel_utils.py +554 -0
  116. setiastro/saspro/pedestal.py +121 -0
  117. setiastro/saspro/perfect_palette_picker.py +1053 -0
  118. setiastro/saspro/pipeline.py +110 -0
  119. setiastro/saspro/pixelmath.py +1600 -0
  120. setiastro/saspro/plate_solver.py +2435 -0
  121. setiastro/saspro/project_io.py +797 -0
  122. setiastro/saspro/psf_utils.py +136 -0
  123. setiastro/saspro/psf_viewer.py +549 -0
  124. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  125. setiastro/saspro/remove_green.py +314 -0
  126. setiastro/saspro/remove_stars.py +1625 -0
  127. setiastro/saspro/remove_stars_preset.py +404 -0
  128. setiastro/saspro/resources.py +472 -0
  129. setiastro/saspro/rgb_combination.py +207 -0
  130. setiastro/saspro/rgb_extract.py +19 -0
  131. setiastro/saspro/rgbalign.py +723 -0
  132. setiastro/saspro/runtime_imports.py +7 -0
  133. setiastro/saspro/runtime_torch.py +754 -0
  134. setiastro/saspro/save_options.py +72 -0
  135. setiastro/saspro/selective_color.py +1552 -0
  136. setiastro/saspro/sfcc.py +1425 -0
  137. setiastro/saspro/shortcuts.py +2807 -0
  138. setiastro/saspro/signature_insert.py +1099 -0
  139. setiastro/saspro/stacking_suite.py +17712 -0
  140. setiastro/saspro/star_alignment.py +7420 -0
  141. setiastro/saspro/star_alignment_preset.py +329 -0
  142. setiastro/saspro/star_metrics.py +49 -0
  143. setiastro/saspro/star_spikes.py +681 -0
  144. setiastro/saspro/star_stretch.py +470 -0
  145. setiastro/saspro/stat_stretch.py +502 -0
  146. setiastro/saspro/status_log_dock.py +78 -0
  147. setiastro/saspro/subwindow.py +3267 -0
  148. setiastro/saspro/supernovaasteroidhunter.py +1712 -0
  149. setiastro/saspro/swap_manager.py +99 -0
  150. setiastro/saspro/torch_backend.py +89 -0
  151. setiastro/saspro/torch_rejection.py +434 -0
  152. setiastro/saspro/view_bundle.py +1555 -0
  153. setiastro/saspro/wavescale_hdr.py +624 -0
  154. setiastro/saspro/wavescale_hdr_preset.py +100 -0
  155. setiastro/saspro/wavescalede.py +657 -0
  156. setiastro/saspro/wavescalede_preset.py +228 -0
  157. setiastro/saspro/wcs_update.py +374 -0
  158. setiastro/saspro/whitebalance.py +456 -0
  159. setiastro/saspro/widgets/__init__.py +48 -0
  160. setiastro/saspro/widgets/common_utilities.py +305 -0
  161. setiastro/saspro/widgets/graphics_views.py +122 -0
  162. setiastro/saspro/widgets/image_utils.py +518 -0
  163. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  164. setiastro/saspro/widgets/spinboxes.py +275 -0
  165. setiastro/saspro/widgets/themed_buttons.py +13 -0
  166. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  167. setiastro/saspro/window_shelf.py +185 -0
  168. setiastro/saspro/xisf.py +1123 -0
  169. setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
  170. setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
  171. setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
  172. setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
  173. setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
  174. setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,224 @@
1
+ # pro/aberration_ai_preset.py
2
+ from __future__ import annotations
3
+ import os
4
+ import time
5
+ import numpy as np
6
+ from PyQt6.QtCore import QTimer
7
+ from PyQt6.QtWidgets import QDialog, QVBoxLayout, QProgressBar, QPushButton, QMessageBox, QFormLayout, QDialogButtonBox, QSpinBox, QCheckBox, QComboBox, QLabel
8
+
9
+ from PyQt6.QtCore import QSettings
10
+ # reuse everything from the UI module
11
+ from .aberration_ai import (
12
+ ort, IS_APPLE_ARM,
13
+ _ONNXWorker, pick_providers, _preserve_border
14
+ )
15
+
16
+ # ---------------------- Headless entry ----------------------
17
+
18
+ def run_aberration_ai_via_preset(main, preset: dict | None = None, doc=None):
19
+ """
20
+ Headless Aberration AI
21
+
22
+ preset keys (all optional except model):
23
+ - model: str (path to .onnx). If omitted, uses QSettings "AberrationAI/model_path".
24
+ - patch: int (default 512)
25
+ - overlap: int (default 64)
26
+ - border_px: int (default 10)
27
+ - auto_gpu: bool (default True; forced False on Apple Silicon)
28
+ - provider: str (used when auto_gpu=False), e.g. "CPUExecutionProvider",
29
+ "CUDAExecutionProvider", "DmlExecutionProvider"
30
+ """
31
+ if ort is None:
32
+ QMessageBox.critical(main, "Aberration AI", "onnxruntime not installed.")
33
+ return
34
+
35
+ # active doc
36
+ if doc is None:
37
+ d = getattr(main, "_active_doc", None)
38
+ doc = d() if callable(d) else d
39
+
40
+ if doc is None or getattr(doc, "image", None) is None:
41
+ QMessageBox.warning(main, "Aberration AI", "Load an image first.")
42
+ return
43
+
44
+ p = dict(preset or {})
45
+
46
+ # model path (preset beats QSettings)
47
+ model = p.get("model") or QSettings().value("AberrationAI/model_path", type=str)
48
+ if not model or not os.path.isfile(model):
49
+ QMessageBox.warning(main, "Aberration AI", "Model not set. Open the Aberration AI tool once and choose a model, or put 'model' into the preset.")
50
+ return
51
+
52
+ patch = int(p.get("patch", 512))
53
+ overlap = int(p.get("overlap", 64))
54
+ border_px= int(p.get("border_px", 10))
55
+
56
+ # providers
57
+ if IS_APPLE_ARM:
58
+ providers = ["CPUExecutionProvider"]
59
+ auto_gpu = False
60
+ provider_label = "CPUExecutionProvider"
61
+ else:
62
+ auto_gpu = bool(p.get("auto_gpu", True))
63
+ if auto_gpu:
64
+ providers = pick_providers(auto_gpu=True)
65
+ provider_label = "auto"
66
+ else:
67
+ sel = str(p.get("provider", "CPUExecutionProvider"))
68
+ providers = [sel]
69
+ provider_label = sel or "CPUExecutionProvider"
70
+
71
+ # Safety for CoreML if someone forces it
72
+ if "CoreMLExecutionProvider" in providers and patch > 128:
73
+ patch = 128
74
+
75
+ # Guard so interactive dialog won't pop during/after apply
76
+ setattr(main, "_aberration_ai_headless_running", True)
77
+ setattr(main, "_aberration_ai_guard", True)
78
+
79
+ # ---- minimal progress dialog ----
80
+ dlg = QDialog(main)
81
+ dlg.setWindowTitle("Aberration AI (Headless)")
82
+ lay = QVBoxLayout(dlg)
83
+ bar = QProgressBar(); bar.setRange(0, 100); lay.addWidget(bar)
84
+ btn = QPushButton("Cancel"); lay.addWidget(btn)
85
+
86
+ img = np.asarray(doc.image)
87
+ orig_for_border = img.copy()
88
+
89
+ t0 = time.perf_counter()
90
+
91
+ worker = _ONNXWorker(model, img, patch, overlap, providers)
92
+ worker.progressed.connect(bar.setValue)
93
+
94
+ def _fail(msg: str):
95
+ try:
96
+ if hasattr(main, "_log"):
97
+ main._log(f"❌ Aberration AI failed: {msg}")
98
+ except Exception:
99
+ pass
100
+ QMessageBox.critical(main, "Aberration AI", msg)
101
+ dlg.close()
102
+
103
+ def _ok(out: np.ndarray):
104
+ # preserve border and commit
105
+ try:
106
+ out2 = _preserve_border(out, orig_for_border, border_px)
107
+ except Exception:
108
+ out2 = out
109
+
110
+ meta = {
111
+ "is_mono": (out2.ndim == 2),
112
+ "processing_parameters": {
113
+ "AberrationAI": {
114
+ "model_path": model,
115
+ "patch_size": int(patch),
116
+ "overlap": int(overlap),
117
+ "provider": provider_label,
118
+ "border_px": int(border_px),
119
+ }
120
+ }
121
+ }
122
+ try:
123
+ doc.apply_edit(out2, meta, step_name="Aberration AI")
124
+ used = getattr(worker, "used_provider", provider_label)
125
+ dt = time.perf_counter() - t0
126
+ if hasattr(main, "_log"):
127
+ main._log(
128
+ f"✅ Aberration AI (headless) model={os.path.basename(model)}, "
129
+ f"provider={used}, patch={patch}, overlap={overlap}, "
130
+ f"border={border_px}px, time={dt:.2f}s"
131
+ )
132
+
133
+ # ---- Register as last_headless_command for Replay ----
134
+ try:
135
+ auto_flag = bool(auto_gpu)
136
+ replay_preset = {
137
+ "model": model,
138
+ "patch": int(patch),
139
+ "overlap": int(overlap),
140
+ "border_px": int(border_px),
141
+ "auto_gpu": auto_flag,
142
+ }
143
+ if not auto_flag:
144
+ replay_preset["provider"] = provider_label
145
+
146
+ payload = {
147
+ "command_id": "aberrationai",
148
+ "preset": replay_preset,
149
+ }
150
+ setattr(main, "_last_headless_command", payload)
151
+ except Exception:
152
+ pass
153
+ # -------------------------------------------------------
154
+ except Exception as e:
155
+ QMessageBox.critical(main, "Aberration AI", f"Failed to apply result:\n{e}")
156
+ finally:
157
+ dlg.close()
158
+
159
+ worker.failed.connect(_fail)
160
+ worker.finished_ok.connect(_ok)
161
+ worker.finished.connect(lambda: btn.setEnabled(False))
162
+ btn.clicked.connect(worker.terminate)
163
+
164
+ worker.start()
165
+ dlg.exec()
166
+
167
+ # clear the guard after a brief tick so downstream signals don’t re-open UI
168
+ def _clear():
169
+ for k in ("_aberration_ai_headless_running", "_aberration_ai_guard"):
170
+ try: delattr(main, k)
171
+ except Exception: setattr(main, k, False)
172
+ QTimer.singleShot(1000, _clear)
173
+
174
+
175
+ # ---------------------- Preset editor (for shortcut) ----------------------
176
+
177
+ class AberrationAIPresetDialog(QDialog):
178
+ def __init__(self, parent=None, initial: dict | None = None):
179
+ super().__init__(parent)
180
+ self.setWindowTitle("Aberration AI — Preset")
181
+ p = dict(initial or {})
182
+
183
+ self.spin_patch = QSpinBox(); self.spin_patch.setRange(128, 2048); self.spin_patch.setValue(int(p.get("patch", 512)))
184
+ self.spin_overlap = QSpinBox(); self.spin_overlap.setRange(16, 512); self.spin_overlap.setValue(int(p.get("overlap", 64)))
185
+ self.spin_border = QSpinBox(); self.spin_border.setRange(0, 64); self.spin_border.setValue(int(p.get("border_px", 10)))
186
+
187
+ self.chk_auto = QCheckBox("Auto GPU (prefer DML/CUDA)"); self.chk_auto.setChecked(bool(p.get("auto_gpu", True)))
188
+ self.cmb_provider = QComboBox(); self.cmb_provider.addItems([
189
+ "CPUExecutionProvider", "DmlExecutionProvider", "CUDAExecutionProvider", "CoreMLExecutionProvider"
190
+ ])
191
+ self.cmb_provider.setCurrentText(str(p.get("provider", "CPUExecutionProvider")))
192
+
193
+ # info: model is taken from QSettings unless preset provides an absolute path from code
194
+ info = QLabel("Model path is taken from the Aberration AI tool (QSettings) unless you pass 'model' in the preset programmatically.")
195
+ info.setWordWrap(True); info.setStyleSheet("color:#888; font-size:11px;")
196
+
197
+ form = QFormLayout(self)
198
+ form.addRow("Patch:", self.spin_patch)
199
+ form.addRow("Overlap:", self.spin_overlap)
200
+ form.addRow("Preserve border (px):", self.spin_border)
201
+ form.addRow(self.chk_auto)
202
+ form.addRow("Provider (if Auto off):", self.cmb_provider)
203
+ form.addRow(info)
204
+
205
+ btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
206
+ btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
207
+ form.addRow(btns)
208
+
209
+ # toggle enable
210
+ def _toggle():
211
+ en = not self.chk_auto.isChecked()
212
+ self.cmb_provider.setEnabled(en)
213
+ self.chk_auto.stateChanged.connect(lambda _: _toggle()); _toggle()
214
+
215
+ def result_dict(self) -> dict:
216
+ d = {
217
+ "patch": int(self.spin_patch.value()),
218
+ "overlap": int(self.spin_overlap.value()),
219
+ "border_px": int(self.spin_border.value()),
220
+ "auto_gpu": bool(self.chk_auto.isChecked()),
221
+ }
222
+ if not d["auto_gpu"]:
223
+ d["provider"] = self.cmb_provider.currentText()
224
+ return d
@@ -0,0 +1,218 @@
1
+ # pro/accel_installer.py
2
+ from __future__ import annotations
3
+ import platform
4
+ import subprocess
5
+ import sys
6
+ import os
7
+ from typing import Callable, Optional
8
+ from PyQt6.QtWidgets import QMessageBox
9
+ from setiastro.saspro.runtime_torch import import_torch, add_runtime_to_sys_path, _user_runtime_dir, _venv_paths
10
+
11
+ LogCB = Callable[[str], None]
12
+
13
+ def _run(cmd):
14
+ return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
15
+
16
+
17
+ def _has_intel_arc() -> bool:
18
+ """
19
+ Return True if the machine appears to have an Intel Arc / Xe (XPU-capable) adapter.
20
+ Windows: CIM/WMIC name sniff.
21
+ Linux: lspci grep.
22
+ macOS: False.
23
+ """
24
+ try:
25
+ sysname = platform.system()
26
+ if sysname == "Windows":
27
+ ps = _run(["powershell","-NoProfile","-Command",
28
+ "(Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name) -join ';'"])
29
+ out = (ps.stdout or "").lower()
30
+ # Accept 'arc' or 'iris xe' (dg/xe discrete & some laptops with XPU)
31
+ return ("intel" in out) and ("arc" in out or "iris xe" in out or "a770" in out or "a750" in out or "a580" in out or "a380" in out)
32
+ if sysname == "Linux":
33
+ r = _run(["bash","-lc","lspci -nn | grep -i 'vga\\|3d'"])
34
+ s = (r.stdout or "").lower()
35
+ return ("intel" in s) and ("arc" in s or "iris xe" in s or "xe" in s)
36
+ return False
37
+ except Exception:
38
+ return False
39
+
40
+ def _has_nvidia() -> bool:
41
+ """
42
+ Return True if the machine *appears* to have an NVIDIA adapter.
43
+ Windows: try PowerShell CIM first (wmic is deprecated), then wmic.
44
+ Linux: use nvidia-smi.
45
+ macOS: always False.
46
+ """
47
+ try:
48
+ sysname = platform.system()
49
+ if sysname == "Windows":
50
+ # Try CIM (preferred)
51
+ ps = _run(["powershell","-NoProfile","-Command",
52
+ "(Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name) -join ';'"])
53
+ out = (ps.stdout or "").lower()
54
+ if "nvidia" in out:
55
+ return True
56
+ # Fallback to wmic (older systems)
57
+ w = _run(["wmic","path","win32_VideoController","get","name"])
58
+ return "nvidia" in (w.stdout or "").lower()
59
+ if sysname == "Linux":
60
+ r = _run(["nvidia-smi","-L"])
61
+ return "GPU" in (r.stdout or "")
62
+ return False
63
+ except Exception:
64
+ return False
65
+
66
+ def _nvidia_driver_ok(log_cb: LogCB) -> bool:
67
+ try:
68
+ r = _run(["nvidia-smi", "--query-gpu=driver_version", "--format=csv,noheader"])
69
+ drv = (r.stdout or "").strip()
70
+ if not drv:
71
+ log_cb("nvidia-smi not found or driver not detected.")
72
+ return False
73
+ log_cb(f"NVIDIA driver detected: {drv}")
74
+ return True
75
+ except Exception:
76
+ log_cb("Unable to query NVIDIA driver via nvidia-smi.")
77
+ return False
78
+
79
+
80
+ def ensure_torch_installed(prefer_gpu: bool, log_cb: LogCB) -> tuple[bool, Optional[str]]:
81
+ try:
82
+ is_windows = platform.system() == "Windows"
83
+ has_nv = _has_nvidia() and platform.system() in ("Windows", "Linux")
84
+ has_intel = (not has_nv) and _has_intel_arc() and platform.system() in ("Windows", "Linux")
85
+
86
+ prefer_cuda = prefer_gpu and has_nv
87
+ prefer_xpu = prefer_gpu and (not has_nv) and has_intel
88
+
89
+ if prefer_cuda and not _nvidia_driver_ok(log_cb):
90
+ log_cb("CUDA requested but NVIDIA driver not detected/working; CUDA wheels may not initialize.")
91
+ log_cb(f"PyTorch install preference: prefer_cuda={prefer_cuda}, prefer_xpu={prefer_xpu} (OS={platform.system()})")
92
+
93
+ # Install torch (tries CUDA → XPU → CPU)
94
+ torch = import_torch(prefer_cuda=prefer_cuda, prefer_xpu=prefer_xpu, status_cb=log_cb)
95
+
96
+ cuda_ok = bool(getattr(torch, "cuda", None) and torch.cuda.is_available())
97
+ xpu_ok = bool(hasattr(torch, "xpu") and torch.xpu.is_available())
98
+
99
+ # HARD RULES about DirectML:
100
+ # • If NVIDIA exists: never use DML.
101
+ # • If XPU is active: also avoid DML to prevent confusion.
102
+ if has_nv:
103
+ _maybe_uninstall_dml = True
104
+ else:
105
+ _maybe_uninstall_dml = xpu_ok
106
+
107
+ if _maybe_uninstall_dml:
108
+ try:
109
+ from setiastro.saspro.runtime_torch import _user_runtime_dir, _venv_paths
110
+ rt = _user_runtime_dir(); vpy = _venv_paths(rt)["python"]
111
+ r = subprocess.run([str(vpy), "-m", "pip", "show", "torch-directml"],
112
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
113
+ if r.returncode == 0 and r.stdout:
114
+ log_cb("Non-DML path selected → uninstalling torch-directml.")
115
+ subprocess.run([str(vpy), "-m", "pip", "uninstall", "-y", "torch-directml"],
116
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
117
+ except Exception:
118
+ pass
119
+
120
+ if cuda_ok:
121
+ log_cb("CUDA available; using NVIDIA backend.")
122
+ return True, None
123
+
124
+ if xpu_ok:
125
+ # optional: surface device name if available
126
+ try:
127
+ name = None
128
+ if hasattr(torch.xpu, "get_device_name"):
129
+ name = torch.xpu.get_device_name(0)
130
+ log_cb(f"Intel XPU available{f' ({name})' if name else ''}.")
131
+ except Exception:
132
+ log_cb("Intel XPU available.")
133
+ return True, None
134
+
135
+ # No CUDA/XPU ⇒ evaluate DML on Windows non-NVIDIA as before
136
+ dml_enabled = False
137
+ if is_windows and (not has_nv):
138
+ try:
139
+ import importlib; importlib.invalidate_caches()
140
+ import torch_directml # noqa
141
+ dml_enabled = True
142
+ log_cb("DirectML detected (already installed).")
143
+ except Exception:
144
+ from setiastro.saspro.runtime_torch import _user_runtime_dir, _venv_paths
145
+ rt = _user_runtime_dir(); vpy = _venv_paths(rt)["python"]
146
+ log_cb("Installing torch-directml (Windows fallback)…")
147
+ r = subprocess.run([str(vpy), "-m", "pip", "install", "--prefer-binary", "torch-directml"],
148
+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
149
+ if r.returncode == 0:
150
+ try:
151
+ import importlib; importlib.invalidate_caches()
152
+ import torch_directml # noqa
153
+ dml_enabled = True
154
+ log_cb("DirectML backend available.")
155
+ except Exception:
156
+ dml_enabled = False
157
+ log_cb("DirectML import failed after install; staying on CPU.")
158
+ else:
159
+ log_cb("DirectML install failed; staying on CPU.")
160
+
161
+ return True, None
162
+ except Exception as e:
163
+ msg = str(e)
164
+ if "PyTorch C-extension check failed" in msg or "Failed to load PyTorch C extensions" in msg:
165
+ msg += (
166
+ "\n\nHints:\n"
167
+ " • Make sure you are not launching SAS Pro from a folder that contains a 'torch' directory.\n"
168
+ " • If you previously ran a local PyTorch checkout, remove it from PYTHONPATH.\n"
169
+ f" • To force a clean reinstall, delete: {os.path.join(str(_user_runtime_dir()), 'venv')} and click Install/Update again."
170
+ )
171
+ if "macOS arm64 on Python 3.13" in msg:
172
+ msg += (
173
+ "\n\nmacOS tip:\n"
174
+ " • Install Python 3.12: `brew install python@3.12`\n"
175
+ " • Ensure `/opt/homebrew/bin/python3.12` exists, then relaunch SAS Pro.\n"
176
+ )
177
+ return False, msg
178
+
179
+ def current_backend() -> str:
180
+ try:
181
+ add_runtime_to_sys_path(status_cb=lambda *_: None)
182
+ import importlib
183
+ import platform as _plat
184
+ torch = importlib.import_module("torch")
185
+
186
+ if getattr(torch, "cuda", None) and torch.cuda.is_available():
187
+ try: name = torch.cuda.get_device_name(0)
188
+ except Exception: name = "CUDA"
189
+ return f"CUDA ({name})"
190
+
191
+ # Intel XPU (Arc / Xe)
192
+ if hasattr(torch, "xpu") and torch.xpu.is_available():
193
+ try:
194
+ name = None
195
+ if hasattr(torch.xpu, "get_device_name"):
196
+ name = torch.xpu.get_device_name(0)
197
+ except Exception:
198
+ name = None
199
+ return f"Intel XPU{f' ({name})' if name else ''}"
200
+
201
+ cuda_tag = getattr(getattr(torch, "version", None), "cuda", None)
202
+ has_nv = _has_nvidia() and _plat.system() in ("Windows","Linux")
203
+ if cuda_tag and has_nv:
204
+ return f"CPU (CUDA {cuda_tag} not available — check NVIDIA driver/CUDA runtime)"
205
+
206
+ if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
207
+ return "Apple MPS"
208
+
209
+ if _plat.system() == "Windows" and not has_nv:
210
+ try:
211
+ import torch_directml # noqa
212
+ return "DirectML"
213
+ except Exception:
214
+ pass
215
+
216
+ return "CPU"
217
+ except Exception:
218
+ return "Not installed"
@@ -0,0 +1,30 @@
1
+ # pro/accel_workers.py
2
+ from __future__ import annotations
3
+ from PyQt6.QtCore import QObject, pyqtSignal, QThread
4
+ from setiastro.saspro.accel_installer import ensure_torch_installed
5
+
6
+ class AccelInstallWorker(QObject):
7
+ progress = pyqtSignal(str) # emitted from worker thread; GUI updates must connect with QueuedConnection
8
+ finished = pyqtSignal(bool, str) # (ok, message)
9
+
10
+ def __init__(self, prefer_gpu: bool = True):
11
+ super().__init__()
12
+ self.prefer_gpu = prefer_gpu
13
+
14
+ def _log(self, s: str):
15
+ # Never touch widgets here; just emit text
16
+ self.progress.emit(s)
17
+
18
+ def run(self):
19
+ # pure backend work; no QWidget/QMessageBox etc. in this method
20
+ ok, msg = ensure_torch_installed(self.prefer_gpu, self._log)
21
+
22
+ # honor cancellation if requested
23
+ if QThread.currentThread().isInterruptionRequested():
24
+ self.finished.emit(False, "Canceled.")
25
+ return
26
+
27
+ if ok:
28
+ self.finished.emit(True, "PyTorch installed and ready.")
29
+ else:
30
+ self.finished.emit(False, msg or "Installation failed.")