setiastrosuitepro 1.7.5.post1__py3-none-any.whl → 1.8.0.post3__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 (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 +732 -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 +324 -0
  15. setiastro/saspro/model_workers.py +102 -0
  16. setiastro/saspro/ops/benchmark.py +320 -0
  17. setiastro/saspro/ops/settings.py +407 -10
  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.post3.dist-info}/METADATA +2 -1
  23. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/RECORD +27 -18
  24. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/WHEEL +0 -0
  25. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/entry_points.txt +0 -0
  26. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/licenses/LICENSE +0 -0
  27. {setiastrosuitepro-1.7.5.post1.dist-info → setiastrosuitepro-1.8.0.post3.dist-info}/licenses/license.txt +0 -0
@@ -1,3 +1,3 @@
1
1
  # Auto-generated at build time. Do not edit.
2
- BUILD_TIMESTAMP = "2026-01-24T17:02:32Z"
3
- APP_VERSION = "1.7.5.post1"
2
+ BUILD_TIMESTAMP = "2026-01-26T19:14:34Z"
3
+ APP_VERSION = "1.8.0.post3"
@@ -1,4 +1,4 @@
1
- # pro/accel_installer.py
1
+ # saspro/accel_installer.py
2
2
  from __future__ import annotations
3
3
  import platform
4
4
  import subprocess
@@ -77,21 +77,34 @@ def _nvidia_driver_ok(log_cb: LogCB) -> bool:
77
77
  return False
78
78
 
79
79
 
80
- def ensure_torch_installed(prefer_gpu: bool, log_cb: LogCB) -> tuple[bool, Optional[str]]:
80
+ def ensure_torch_installed(prefer_gpu: bool, log_cb: LogCB, preferred_backend: str = "auto") -> tuple[bool, Optional[str]]:
81
+
81
82
  try:
83
+ preferred_backend = (preferred_backend or "auto").lower()
82
84
  is_windows = platform.system() == "Windows"
85
+
83
86
  has_nv = _has_nvidia() and platform.system() in ("Windows", "Linux")
84
87
  has_intel = (not has_nv) and _has_intel_arc() and platform.system() in ("Windows", "Linux")
85
88
 
86
- prefer_cuda = prefer_gpu and has_nv
87
- prefer_xpu = prefer_gpu and (not has_nv) and has_intel
89
+ prefer_cuda = prefer_gpu and (preferred_backend in ("auto", "cuda")) and has_nv
90
+ prefer_xpu = prefer_gpu and (preferred_backend in ("auto", "xpu")) and (not has_nv) and has_intel
88
91
 
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
+ # NEW: DirectML preference is ONLY meaningful on Windows with no NVIDIA
93
+ prefer_dml = (
94
+ is_windows
95
+ and (not has_nv)
96
+ and preferred_backend in ("directml", "auto")
97
+ and prefer_gpu
98
+ and (not prefer_xpu) # if Intel XPU actually works, prefer that over DML
99
+ )
100
+
101
+ # If user forced CPU, disable everything
102
+ if preferred_backend == "cpu":
103
+ prefer_cuda = prefer_xpu = prefer_dml = False
92
104
 
93
105
  # Install torch (tries CUDA → XPU → CPU)
94
- torch = import_torch(prefer_cuda=prefer_cuda, prefer_xpu=prefer_xpu, status_cb=log_cb)
106
+ torch = import_torch(prefer_cuda=prefer_cuda, prefer_xpu=prefer_xpu, prefer_dml=prefer_dml, status_cb=log_cb)
107
+
95
108
 
96
109
  cuda_ok = bool(getattr(torch, "cuda", None) and torch.cuda.is_available())
97
110
  xpu_ok = bool(hasattr(torch, "xpu") and torch.xpu.is_available())
@@ -1,25 +1,24 @@
1
- # pro/accel_workers.py
1
+ # saspro/accel_workers.py
2
2
  from __future__ import annotations
3
3
  from PyQt6.QtCore import QObject, pyqtSignal, QThread
4
4
  from setiastro.saspro.accel_installer import ensure_torch_installed
5
5
 
6
6
  class AccelInstallWorker(QObject):
7
- progress = pyqtSignal(str) # emitted from worker thread; GUI updates must connect with QueuedConnection
7
+ progress = pyqtSignal(str) # emitted from worker thread
8
8
  finished = pyqtSignal(bool, str) # (ok, message)
9
9
 
10
- def __init__(self, prefer_gpu: bool = True):
10
+ def __init__(self, prefer_gpu: bool = True, preferred_backend: str = "auto"):
11
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)
12
+ self.prefer_gpu = bool(prefer_gpu)
13
+ self.preferred_backend = (preferred_backend or "auto").lower()
17
14
 
18
15
  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)
16
+ ok, msg = ensure_torch_installed(
17
+ prefer_gpu=self.prefer_gpu,
18
+ preferred_backend=self.preferred_backend,
19
+ log_cb=self.progress.emit
20
+ )
21
21
 
22
- # honor cancellation if requested
23
22
  if QThread.currentThread().isInterruptionRequested():
24
23
  self.finished.emit(False, "Canceled.")
25
24
  return
@@ -27,4 +26,4 @@ class AccelInstallWorker(QObject):
27
26
  if ok:
28
27
  self.finished.emit(True, "PyTorch installed and ready.")
29
28
  else:
30
- self.finished.emit(False, msg or "Installation failed.")
29
+ self.finished.emit(False, msg or "Installation failed.")
@@ -13,15 +13,52 @@ from functools import lru_cache
13
13
  from astropy.io import fits
14
14
  from astropy.stats import sigma_clipped_stats
15
15
  import sep
16
- from setiastro.saspro.remove_stars import (
17
- _get_setting_any,
18
- _mtf_params_linked, _apply_mtf_linked_rgb, _invert_mtf_linked_rgb,
19
- _resolve_darkstar_exe, _ensure_exec_bit, _purge_darkstar_io
20
-
16
+ from setiastro.saspro.cosmicclarity_engines.darkstar_engine import (
17
+ darkstar_starremoval_rgb01,
18
+ DarkStarParams,
21
19
  )
20
+
21
+
22
22
  from setiastro.saspro.legacy.image_manager import (load_image, save_image)
23
23
  from setiastro.saspro.imageops.stretch import stretch_color_image, stretch_mono_image
24
24
 
25
+ def _get_setting_any(settings, keys, default=None):
26
+ """
27
+ Try multiple keys against QSettings-like or dict-like settings.
28
+ keys: iterable of strings
29
+ """
30
+ if settings is None:
31
+ return default
32
+
33
+ # QSettings / object with .value()
34
+ if hasattr(settings, "value") and callable(getattr(settings, "value")):
35
+ for k in keys:
36
+ try:
37
+ v = settings.value(k, None)
38
+ except Exception:
39
+ v = None
40
+ if v not in (None, "", "None"):
41
+ return v
42
+ return default
43
+
44
+ # dict-like
45
+ if isinstance(settings, dict):
46
+ for k in keys:
47
+ if k in settings and settings[k] not in (None, "", "None"):
48
+ return settings[k]
49
+ return default
50
+
51
+ # fallback: attribute lookup
52
+ for k in keys:
53
+ try:
54
+ v = getattr(settings, k)
55
+ except Exception:
56
+ v = None
57
+ if v not in (None, "", "None"):
58
+ return v
59
+ return default
60
+
61
+
25
62
  def _blackpoint_nonzero(img_norm: np.ndarray, p: float = 0.1) -> float:
26
63
  """Scalar blackpoint from non-zero pixels across all channels (linked).
27
64
  p in [0..100]: small percentile to resist outliers; use 0 for strict min."""
@@ -66,6 +103,18 @@ def _u16_to_float01(x: np.ndarray) -> np.ndarray:
66
103
 
67
104
  # comet_stacking.py (or wherever this lives)
68
105
 
106
+ def _ensure_exec_bit(path: str) -> None:
107
+ try:
108
+ if os.name == "nt":
109
+ return
110
+ p = os.path.realpath(path)
111
+ st = os.stat(p)
112
+ if not (st.st_mode & 0o111):
113
+ os.chmod(p, st.st_mode | 0o111)
114
+ except Exception:
115
+ return
116
+
117
+
69
118
  def starnet_starless_pair_from_array(
70
119
  src_rgb01,
71
120
  settings,
@@ -211,78 +260,42 @@ def starnet_starless_pair_from_array(
211
260
  return np.clip(protected_unstretch, 0.0, 1.0), np.clip(protected_unstretch, 0.0, 1.0)
212
261
 
213
262
 
214
-
215
- def darkstar_starless_from_array(src_rgb01: np.ndarray, settings, **_ignored) -> np.ndarray:
263
+ def darkstar_starless_from_array(
264
+ src_rgb01: np.ndarray,
265
+ settings,
266
+ *,
267
+ use_gpu: bool = True,
268
+ chunk_size: int = 512,
269
+ overlap_frac: float = 0.125,
270
+ mode: str = "unscreen",
271
+ progress_cb=None,
272
+ status_cb=None,
273
+ ) -> np.ndarray:
216
274
  """
217
- Headless CosmicClarity DarkStar run for a single RGB frame.
218
- Returns starless RGB in [0..1]. Uses CC’s input/output folders.
275
+ In-process Dark Star removal via darkstar_engine.py
276
+ Input: float32 [0..1], HxW or HxWx1 or HxWx3
277
+ Output: float32 [0..1], same "mono-ness" behavior as engine (HxWx1 if mono input)
219
278
  """
220
- # normalize channels
221
- img = src_rgb01.astype(np.float32, copy=False)
222
- # Delay expansion: if it's 2D/Mono, send it as-is if DarkStar supports it,
223
- # but DarkStar expects 3-channel TIF usually.
224
- # We'll just expand for the save call, not "in place" if possible.
225
- # Actually DarkStar runner saves `img` directly.
226
- # So we'll expand just for that save to avoid holding 2 copies in memory.
227
- if img.ndim == 2:
228
- img_to_save = np.stack([img]*3, axis=-1)
229
- elif img.ndim == 3 and img.shape[2] == 1:
230
- img_to_save = np.repeat(img, 3, axis=2)
231
- else:
232
- img_to_save = img
233
-
234
- # resolve exe and base folder
235
- exe, base = _resolve_darkstar_exe(type("Dummy", (), {"settings": settings})())
236
- if not exe or not base:
237
- raise RuntimeError("Cosmic Clarity DarkStar executable path is not set.")
238
-
239
- _ensure_exec_bit(exe)
240
-
241
- input_dir = os.path.join(base, "input")
242
- output_dir = os.path.join(base, "output")
243
- os.makedirs(input_dir, exist_ok=True)
244
- os.makedirs(output_dir, exist_ok=True)
245
-
246
- # purge any prior files (safe; scoped to imagetoremovestars*)
247
- _purge_darkstar_io(base, prefix="imagetoremovestars", clear_input=True, clear_output=True)
248
-
249
- in_path = os.path.join(input_dir, "imagetoremovestars.tif")
250
- out_path = os.path.join(output_dir, "imagetoremovestars_starless.tif")
251
-
252
- # save input as float32 TIFF
253
- # save input as float32 TIFF
254
- save_image(img_to_save, in_path, original_format="tif", bit_depth="32-bit floating point",
255
- original_header=None, is_mono=False, image_meta=None, file_meta=None)
256
-
257
- # build command (SASv2 parity): default unscreen, show extracted stars off, stride 512
258
- cmd = [exe, "--star_removal_mode", "unscreen", "--chunk_size", "512"]
259
-
260
- rc = subprocess.call(cmd, cwd=output_dir)
261
- if rc != 0 or not os.path.exists(out_path):
262
- try: os.remove(in_path)
263
- except Exception as e:
264
- import logging
265
- logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
266
- raise RuntimeError(f"DarkStar failed (rc={rc}).")
267
-
268
- starless, _, _, _ = load_image(out_path)
269
- # cleanup
270
- try:
271
- os.remove(in_path)
272
- os.remove(out_path)
273
- _purge_darkstar_io(base, prefix="imagetoremovestars", clear_input=True, clear_output=True)
274
- except Exception:
275
- pass
276
-
277
- if starless is None:
278
- raise RuntimeError("DarkStar produced no output.")
279
+ if progress_cb is None:
280
+ progress_cb = lambda done, total, stage: None
281
+ if status_cb is None:
282
+ status_cb = lambda msg: None
283
+
284
+ img = np.asarray(src_rgb01, np.float32)
285
+ params = DarkStarParams(
286
+ use_gpu=bool(use_gpu),
287
+ chunk_size=int(chunk_size),
288
+ overlap_frac=float(overlap_frac),
289
+ mode=str(mode),
290
+ output_stars_only=False,
291
+ )
279
292
 
280
- # Delayed expansion
281
- if starless.ndim == 2:
282
- starless = np.stack([starless]*3, axis=-1)
283
- elif starless.ndim == 3 and starless.shape[2] == 1:
284
- starless = np.repeat(starless, 3, axis=2)
285
-
293
+ starless, _stars_only, _was_mono = darkstar_starremoval_rgb01(
294
+ img,
295
+ params=params,
296
+ progress_cb=progress_cb,
297
+ status_cb=status_cb,
298
+ )
286
299
  return np.clip(starless.astype(np.float32, copy=False), 0.0, 1.0)
287
300
 
288
301
  # ---------- small helpers ----------
@@ -1106,16 +1119,37 @@ def _starless_frame_for_comet(img: np.ndarray,
1106
1119
 
1107
1120
  # run
1108
1121
  if tool == "CosmicClarityDarkStar":
1109
- # DarkStar returns in the same domain we fed in.
1110
- base_for_mask = src
1111
- starless = darkstar_starless_from_array(src, settings)
1122
+ base_for_mask = src # RGB [0..1]
1123
+
1124
+ # optional: forward progress from comet stacking if you want
1125
+ def _ds_progress(done, total, stage):
1126
+ # stage is like "Dark Star removal"
1127
+ # you can route to log or status_cb; keep lightweight
1128
+ pass
1129
+
1130
+ starless = darkstar_starless_from_array(
1131
+ src,
1132
+ settings,
1133
+ use_gpu=True,
1134
+ chunk_size=512,
1135
+ overlap_frac=0.125,
1136
+ mode="unscreen",
1137
+ progress_cb=_ds_progress,
1138
+ status_cb=None, # or log
1139
+ )
1140
+
1141
+ # DarkStar may return HxWx1 for mono-ish inputs — expand to RGB for nucleus protection
1142
+ if starless.ndim == 2:
1143
+ starless = np.repeat(starless[..., None], 3, axis=2)
1144
+ elif starless.ndim == 3 and starless.shape[2] == 1:
1145
+ starless = np.repeat(starless, 3, axis=2)
1112
1146
 
1113
- # protect nucleus (blend original back where mask=1), in *current* domain
1114
1147
  m = core_mask.astype(np.float32)
1115
1148
  m3 = np.repeat(m[..., None], 3, axis=2)
1116
1149
  protected = starless * (1.0 - m3) + base_for_mask * m3
1117
1150
  return np.clip(protected, 0.0, 1.0)
1118
1151
 
1152
+
1119
1153
  else:
1120
1154
  # StarNet path: do mask-blend inside the function (in its stretched domain)
1121
1155
  protected, _ = starnet_starless_pair_from_array(
@@ -1302,13 +1336,7 @@ class CometCentroidPreview(QDialog):
1302
1336
  def _prep_slider(self, s, lo, hi, val):
1303
1337
  s.setRange(lo, hi); s.setValue(val); s.setSingleStep(1); s.setPageStep(5)
1304
1338
 
1305
- def eventFilter(self, obj, ev):
1306
- if obj is self.view.viewport() and ev.type() == QEvent.Type.MouseButtonPress:
1307
- if ev.button() == Qt.MouseButton.LeftButton:
1308
- pos = self.view.mapToScene(ev.position().toPoint())
1309
- self._set_xy_current(pos.x(), pos.y())
1310
- return True
1311
- return super().eventFilter(obj, ev)
1339
+
1312
1340
 
1313
1341
  def keyPressEvent(self, ev):
1314
1342
  k = ev.key()