setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.12__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 (115) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/acv_icon.png +0 -0
  3. setiastro/images/colorwheel.svg +97 -0
  4. setiastro/images/cosmic.svg +40 -0
  5. setiastro/images/cosmicsat.svg +24 -0
  6. setiastro/images/first_quarter.png +0 -0
  7. setiastro/images/full_moon.png +0 -0
  8. setiastro/images/graxpert.svg +19 -0
  9. setiastro/images/last_quarter.png +0 -0
  10. setiastro/images/linearfit.svg +32 -0
  11. setiastro/images/new_moon.png +0 -0
  12. setiastro/images/pixelmath.svg +42 -0
  13. setiastro/images/waning_crescent_1.png +0 -0
  14. setiastro/images/waning_crescent_2.png +0 -0
  15. setiastro/images/waning_crescent_3.png +0 -0
  16. setiastro/images/waning_crescent_4.png +0 -0
  17. setiastro/images/waning_crescent_5.png +0 -0
  18. setiastro/images/waning_gibbous_1.png +0 -0
  19. setiastro/images/waning_gibbous_2.png +0 -0
  20. setiastro/images/waning_gibbous_3.png +0 -0
  21. setiastro/images/waning_gibbous_4.png +0 -0
  22. setiastro/images/waning_gibbous_5.png +0 -0
  23. setiastro/images/waxing_crescent_1.png +0 -0
  24. setiastro/images/waxing_crescent_2.png +0 -0
  25. setiastro/images/waxing_crescent_3.png +0 -0
  26. setiastro/images/waxing_crescent_4.png +0 -0
  27. setiastro/images/waxing_crescent_5.png +0 -0
  28. setiastro/images/waxing_gibbous_1.png +0 -0
  29. setiastro/images/waxing_gibbous_2.png +0 -0
  30. setiastro/images/waxing_gibbous_3.png +0 -0
  31. setiastro/images/waxing_gibbous_4.png +0 -0
  32. setiastro/images/waxing_gibbous_5.png +0 -0
  33. setiastro/qml/ResourceMonitor.qml +84 -82
  34. setiastro/saspro/__main__.py +20 -1
  35. setiastro/saspro/_generated/build_info.py +2 -2
  36. setiastro/saspro/abe.py +37 -4
  37. setiastro/saspro/aberration_ai.py +237 -21
  38. setiastro/saspro/acv_exporter.py +379 -0
  39. setiastro/saspro/add_stars.py +33 -6
  40. setiastro/saspro/backgroundneutral.py +108 -40
  41. setiastro/saspro/blemish_blaster.py +4 -1
  42. setiastro/saspro/blink_comparator_pro.py +74 -24
  43. setiastro/saspro/clahe.py +4 -1
  44. setiastro/saspro/continuum_subtract.py +4 -1
  45. setiastro/saspro/convo.py +13 -7
  46. setiastro/saspro/cosmicclarity.py +129 -18
  47. setiastro/saspro/crop_dialog_pro.py +123 -7
  48. setiastro/saspro/curve_editor_pro.py +109 -42
  49. setiastro/saspro/doc_manager.py +245 -15
  50. setiastro/saspro/exoplanet_detector.py +120 -28
  51. setiastro/saspro/frequency_separation.py +1158 -204
  52. setiastro/saspro/ghs_dialog_pro.py +81 -16
  53. setiastro/saspro/graxpert.py +1 -0
  54. setiastro/saspro/gui/main_window.py +429 -228
  55. setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
  56. setiastro/saspro/gui/mixins/menu_mixin.py +27 -1
  57. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  58. setiastro/saspro/gui/mixins/toolbar_mixin.py +384 -18
  59. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  60. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  61. setiastro/saspro/halobgon.py +4 -0
  62. setiastro/saspro/histogram.py +5 -1
  63. setiastro/saspro/image_combine.py +4 -0
  64. setiastro/saspro/image_peeker_pro.py +4 -0
  65. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  66. setiastro/saspro/imageops/stretch.py +582 -62
  67. setiastro/saspro/isophote.py +4 -0
  68. setiastro/saspro/layers.py +13 -9
  69. setiastro/saspro/layers_dock.py +183 -3
  70. setiastro/saspro/legacy/image_manager.py +154 -20
  71. setiastro/saspro/legacy/numba_utils.py +67 -47
  72. setiastro/saspro/legacy/xisf.py +240 -98
  73. setiastro/saspro/live_stacking.py +180 -79
  74. setiastro/saspro/luminancerecombine.py +228 -27
  75. setiastro/saspro/mask_creation.py +174 -15
  76. setiastro/saspro/mfdeconv.py +113 -35
  77. setiastro/saspro/mfdeconvcudnn.py +119 -70
  78. setiastro/saspro/mfdeconvsport.py +112 -35
  79. setiastro/saspro/morphology.py +4 -0
  80. setiastro/saspro/multiscale_decomp.py +51 -12
  81. setiastro/saspro/numba_utils.py +72 -57
  82. setiastro/saspro/ops/commands.py +18 -18
  83. setiastro/saspro/ops/script_editor.py +10 -2
  84. setiastro/saspro/ops/scripts.py +122 -0
  85. setiastro/saspro/perfect_palette_picker.py +37 -3
  86. setiastro/saspro/plate_solver.py +84 -49
  87. setiastro/saspro/psf_viewer.py +119 -37
  88. setiastro/saspro/resources.py +67 -0
  89. setiastro/saspro/rgbalign.py +4 -0
  90. setiastro/saspro/selective_color.py +4 -1
  91. setiastro/saspro/sfcc.py +364 -152
  92. setiastro/saspro/shortcuts.py +160 -29
  93. setiastro/saspro/signature_insert.py +692 -33
  94. setiastro/saspro/stacking_suite.py +1331 -484
  95. setiastro/saspro/star_alignment.py +247 -123
  96. setiastro/saspro/star_spikes.py +4 -0
  97. setiastro/saspro/star_stretch.py +38 -3
  98. setiastro/saspro/stat_stretch.py +743 -128
  99. setiastro/saspro/subwindow.py +786 -360
  100. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  101. setiastro/saspro/wavescale_hdr.py +4 -1
  102. setiastro/saspro/wavescalede.py +4 -1
  103. setiastro/saspro/whitebalance.py +84 -12
  104. setiastro/saspro/widgets/common_utilities.py +28 -21
  105. setiastro/saspro/widgets/resource_monitor.py +109 -59
  106. setiastro/saspro/widgets/spinboxes.py +10 -13
  107. setiastro/saspro/wimi.py +27 -656
  108. setiastro/saspro/wims.py +13 -3
  109. setiastro/saspro/xisf.py +101 -11
  110. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +2 -1
  111. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +115 -82
  112. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
  113. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
  114. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
  115. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
@@ -250,14 +250,20 @@ def _probe_hw(path: str) -> tuple[int, int, int | None]:
250
250
  raise ValueError(f"Unsupported ndim={a.ndim} for {path}")
251
251
 
252
252
  def _common_hw_from_paths(paths: list[str]) -> tuple[int, int]:
253
- """
254
- Replacement for the old FITS-only version: min(H), min(W) across files.
255
- """
256
253
  Hs, Ws = [], []
257
254
  for p in paths:
258
255
  h, w, _ = _probe_hw(p)
259
- Hs.append(int(h)); Ws.append(int(w))
260
- return int(min(Hs)), int(min(Ws))
256
+ h = int(h); w = int(w)
257
+ if h > 0 and w > 0:
258
+ Hs.append(h); Ws.append(w)
259
+
260
+ if not Hs:
261
+ raise ValueError("Could not determine any valid frame sizes.")
262
+ Ht = min(Hs); Wt = min(Ws)
263
+ if Ht < 8 or Wt < 8:
264
+ raise ValueError(f"Intersection too small: {Ht}x{Wt}")
265
+ return Ht, Wt
266
+
261
267
 
262
268
  def _to_chw_float32(img: np.ndarray, color_mode: str) -> np.ndarray:
263
269
  """
@@ -367,22 +373,36 @@ def _compute_frame_assets(i, arr, hdr, *, make_masks, make_varmaps,
367
373
  f_whm = 2.5
368
374
  k_auto = _auto_ksize_from_fwhm(f_whm)
369
375
 
370
- # --- Star-derived PSF with retries ---
371
- tried, psf = [], None
372
- for k_try in [k_auto, max(k_auto - 4, 11), 21, 17, 15, 13, 11]:
373
- if k_try in tried: continue
374
- tried.append(k_try)
375
- try:
376
- out = compute_psf_kernel_for_image(arr, ksize=k_try, det_sigma=6.0, max_stars=80)
377
- psf_try = out[0] if (isinstance(out, tuple) and len(out) >= 1) else out
378
- if psf_try is not None:
379
- psf = psf_try
380
- break
381
- except Exception:
382
- psf = None
376
+ # --- Star-derived PSF with retries (dynamic det_sigma ladder) ---
377
+ psf = None
378
+
379
+ # Your existing ksize ladder
380
+ k_ladder = [k_auto, max(k_auto - 4, 11), 21, 17, 15, 13, 11]
381
+
382
+ # New: start high to avoid detecting 10k stars; step down only if needed
383
+ sigma_ladder = [50.0, 25.0, 12.0, 6.0]
384
+
385
+ tried = set()
386
+ for det_sigma in sigma_ladder:
387
+ for k_try in k_ladder:
388
+ if (det_sigma, k_try) in tried:
389
+ continue
390
+ tried.add((det_sigma, k_try))
391
+ try:
392
+ out = compute_psf_kernel_for_image(arr, ksize=k_try, det_sigma=det_sigma, max_stars=80)
393
+ psf_try = out[0] if (isinstance(out, tuple) and len(out) >= 1) else out
394
+ if psf_try is not None:
395
+ psf = psf_try
396
+ break
397
+ except Exception:
398
+ psf = None
399
+ if psf is not None:
400
+ break
401
+
383
402
  if psf is None:
384
403
  psf = _gaussian_psf(f_whm, ksize=k_auto)
385
- psf = _soften_psf(_normalize_psf(psf.astype(np.float32, copy=False)), sigma_px=0.0)
404
+
405
+ psf = _soften_psf(_normalize_psf(psf.astype(np.float32, copy=False)), sigma_px=0.25)
386
406
 
387
407
  mask = None
388
408
  var = None
@@ -425,6 +445,7 @@ def _compute_frame_assets(i, arr, hdr, *, make_masks, make_varmaps,
425
445
 
426
446
  return i, psf, mask, var, logs
427
447
 
448
+
428
449
  def _compute_one_worker(args):
429
450
  """
430
451
  Top-level picklable worker for ProcessPoolExecutor.
@@ -1039,25 +1060,82 @@ SOFT_SIGMA = 2.0
1039
1060
  ELLIPSE_SCALE = 1.2
1040
1061
 
1041
1062
  def _sep_background_precompute(img_2d: np.ndarray, bw: int = 64, bh: int = 64):
1042
- """One-time SEP background build; returns (sky_map, rms_map, err_scalar)."""
1063
+ """
1064
+ One-time SEP background build; returns (sky_map, rms_map, err_scalar).
1065
+
1066
+ Guarantees:
1067
+ - Always returns a 3-tuple (sky, rms, err)
1068
+ - sky/rms are float32 and same shape as img_2d
1069
+ - Robust to sep missing, sep errors, NaNs/Infs, and tiny frames
1070
+ """
1071
+ a = np.asarray(img_2d, dtype=np.float32)
1072
+ if a.ndim != 2:
1073
+ # be strict; callers expect 2D
1074
+ raise ValueError(f"_sep_background_precompute expects 2D, got shape={a.shape}")
1075
+
1076
+ H, W = int(a.shape[0]), int(a.shape[1])
1077
+ if H == 0 or W == 0:
1078
+ # should never happen, but don't return empty tuple
1079
+ sky = np.zeros((H, W), dtype=np.float32)
1080
+ rms = np.ones((H, W), dtype=np.float32)
1081
+ return sky, rms, 1.0
1082
+
1083
+ # --- robust fallback builder (works for any input) ---
1084
+ def _fallback():
1085
+ # Use finite-only stats if possible
1086
+ finite = np.isfinite(a)
1087
+ if finite.any():
1088
+ vals = a[finite]
1089
+ med = float(np.median(vals))
1090
+ mad = float(np.median(np.abs(vals - med))) + 1e-6
1091
+ else:
1092
+ med = 0.0
1093
+ mad = 1.0
1094
+ sky = np.full((H, W), med, dtype=np.float32)
1095
+ rms = np.full((H, W), 1.4826 * mad, dtype=np.float32)
1096
+ err = float(np.median(rms))
1097
+ return sky, rms, err
1098
+
1099
+ # If sep isn't available, always fallback
1043
1100
  if sep is None:
1044
- # robust fallback
1045
- med = float(np.median(img_2d))
1046
- mad = float(np.median(np.abs(img_2d - med))) + 1e-6
1047
- sky = np.full_like(img_2d, med, dtype=np.float32)
1048
- rmsm = np.full_like(img_2d, 1.4826 * mad, dtype=np.float32)
1049
- return sky, rmsm, float(np.median(rmsm))
1050
-
1051
- a = np.ascontiguousarray(img_2d.astype(np.float32))
1052
- b = sep.Background(a, bw=int(bw), bh=int(bh), fw=3, fh=3)
1053
- sky = np.asarray(b.back(), dtype=np.float32)
1101
+ return _fallback()
1102
+
1103
+ # SEP is present: sanitize input and clamp tile sizes
1104
+ # sep can choke on NaNs/Infs
1105
+ if not np.isfinite(a).all():
1106
+ # replace non-finite with median of finite values (or 0)
1107
+ finite = np.isfinite(a)
1108
+ fill = float(np.median(a[finite])) if finite.any() else 0.0
1109
+ a = np.where(finite, a, fill).astype(np.float32, copy=False)
1110
+
1111
+ a = np.ascontiguousarray(a, dtype=np.float32)
1112
+
1113
+ # Clamp bw/bh to image size; SEP doesn't like bw/bh > dims
1114
+ bw = int(max(8, min(int(bw), W)))
1115
+ bh = int(max(8, min(int(bh), H)))
1116
+
1054
1117
  try:
1055
- rmsm = np.asarray(b.rms(), dtype=np.float32)
1056
- err = float(b.globalrms)
1118
+ b = sep.Background(a, bw=bw, bh=bh, fw=3, fh=3)
1119
+
1120
+ sky = np.asarray(b.back(), dtype=np.float32)
1121
+ rms = np.asarray(b.rms(), dtype=np.float32)
1122
+
1123
+ # Ensure shape sanity (SEP should match, but be paranoid)
1124
+ if sky.shape != a.shape or rms.shape != a.shape:
1125
+ return _fallback()
1126
+
1127
+ # globalrms sometimes isn't available depending on SEP build
1128
+ err = float(getattr(b, "globalrms", np.nan))
1129
+ if not np.isfinite(err) or err <= 0:
1130
+ # robust scalar: median rms
1131
+ err = float(np.median(rms)) if rms.size else 1.0
1132
+
1133
+ return sky, rms, err
1134
+
1057
1135
  except Exception:
1058
- rmsm = np.full_like(a, float(np.median(b.rms())), dtype=np.float32)
1059
- err = float(np.median(rmsm))
1060
- return sky, rmsm, err
1136
+ # If SEP blows up for any reason, degrade gracefully
1137
+ return _fallback()
1138
+
1061
1139
 
1062
1140
 
1063
1141
  def _auto_star_mask_sep(
@@ -311,14 +311,20 @@ def _probe_hw(path: str) -> tuple[int, int, int | None]:
311
311
  raise ValueError(f"Unsupported ndim={a.ndim} for {path}")
312
312
 
313
313
  def _common_hw_from_paths(paths: list[str]) -> tuple[int, int]:
314
- """
315
- Replacement for the old FITS-only version: min(H), min(W) across files.
316
- """
317
314
  Hs, Ws = [], []
318
315
  for p in paths:
319
316
  h, w, _ = _probe_hw(p)
320
- Hs.append(int(h)); Ws.append(int(w))
321
- return int(min(Hs)), int(min(Ws))
317
+ h = int(h); w = int(w)
318
+ if h > 0 and w > 0:
319
+ Hs.append(h); Ws.append(w)
320
+
321
+ if not Hs:
322
+ raise ValueError("Could not determine any valid frame sizes.")
323
+ Ht = min(Hs); Wt = min(Ws)
324
+ if Ht < 8 or Wt < 8:
325
+ raise ValueError(f"Intersection too small: {Ht}x{Wt}")
326
+ return Ht, Wt
327
+
322
328
 
323
329
  def _to_chw_float32(img: np.ndarray, color_mode: str) -> np.ndarray:
324
330
  """
@@ -395,15 +401,12 @@ def _safe_primary_header(path: str) -> fits.Header:
395
401
  except Exception:
396
402
  return fits.Header()
397
403
 
398
-
399
404
  def _compute_frame_assets(i, arr, hdr, *, make_masks, make_varmaps,
400
405
  star_mask_cfg, varmap_cfg, status_sink=lambda s: None):
401
406
  """
402
407
  Worker function: compute PSF and optional star mask / varmap for one frame.
403
- Returns (index, psf, mask_or_None, var_or_None, var_path_or_None, log_lines)
408
+ Returns (index, psf, mask_or_None, var_or_None, log_lines)
404
409
  """
405
-
406
-
407
410
  logs = []
408
411
  def log(s): logs.append(s)
409
412
 
@@ -415,36 +418,48 @@ def _compute_frame_assets(i, arr, hdr, *, make_masks, make_varmaps,
415
418
  f_whm = 2.5
416
419
  k_auto = _auto_ksize_from_fwhm(f_whm)
417
420
 
418
- # --- Star-derived PSF with retries ---
419
- tried, psf = [], None
420
- for k_try in [k_auto, max(k_auto - 4, 11), 21, 17, 15, 13, 11]:
421
- if k_try in tried:
422
- continue
423
- tried.append(k_try)
424
- try:
425
- out = compute_psf_kernel_for_image(arr, ksize=k_try, det_sigma=6.0, max_stars=80)
426
- psf_try = out[0] if (isinstance(out, tuple) and len(out) >= 1) else out
427
- if psf_try is not None:
428
- psf = psf_try
429
- break
430
- except Exception:
431
- psf = None
421
+ # --- Star-derived PSF with retries (dynamic det_sigma ladder) ---
422
+ psf = None
423
+
424
+ # Your existing ksize ladder
425
+ k_ladder = [k_auto, max(k_auto - 4, 11), 21, 17, 15, 13, 11]
426
+
427
+ # New: start high to avoid detecting 10k stars; step down only if needed
428
+ sigma_ladder = [50.0, 25.0, 12.0, 6.0]
429
+
430
+ tried = set()
431
+ for det_sigma in sigma_ladder:
432
+ for k_try in k_ladder:
433
+ if (det_sigma, k_try) in tried:
434
+ continue
435
+ tried.add((det_sigma, k_try))
436
+ try:
437
+ out = compute_psf_kernel_for_image(arr, ksize=k_try, det_sigma=det_sigma, max_stars=80)
438
+ psf_try = out[0] if (isinstance(out, tuple) and len(out) >= 1) else out
439
+ if psf_try is not None:
440
+ psf = psf_try
441
+ break
442
+ except Exception:
443
+ psf = None
444
+ if psf is not None:
445
+ break
446
+
432
447
  if psf is None:
433
448
  psf = _gaussian_psf(f_whm, ksize=k_auto)
449
+
434
450
  psf = _soften_psf(_normalize_psf(psf.astype(np.float32, copy=False)), sigma_px=0.25)
435
451
 
436
452
  mask = None
437
453
  var = None
438
- var_path = None
439
454
 
440
455
  if make_masks or make_varmaps:
456
+ # one background per frame (reused by both)
441
457
  luma = _to_luma_local(arr)
442
458
  vmc = (varmap_cfg or {})
443
459
  sky_map, rms_map, err_scalar = _sep_background_precompute(
444
460
  luma, bw=int(vmc.get("bw", 64)), bh=int(vmc.get("bh", 64))
445
461
  )
446
462
 
447
- # ---------- Star mask ----------
448
463
  if make_masks:
449
464
  smc = star_mask_cfg or {}
450
465
  mask = _star_mask_from_precomputed(
@@ -459,44 +474,22 @@ def _compute_frame_assets(i, arr, hdr, *, make_masks, make_varmaps,
459
474
  max_side = smc.get("max_side", STAR_MASK_MAXSIDE),
460
475
  status_cb = log,
461
476
  )
462
- # keep masks compact
463
- if mask is not None and mask.dtype != np.uint8:
464
- mask = (mask > 0.5).astype(np.uint8, copy=False)
465
477
 
466
- # ---------- Variance map (memmap path; Option B) ----------
467
478
  if make_varmaps:
468
479
  vmc = varmap_cfg or {}
469
- def _vprog(frac: float, msg: str = ""):
470
- try: log(f"__PROGRESS__ {0.16 + 0.02*float(frac):.4f} {msg}")
471
- except Exception as e:
472
- import logging
473
- logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
474
-
475
- var_path = _variance_map_from_precomputed_memmap(
480
+ var = _variance_map_from_precomputed(
476
481
  luma, sky_map, rms_map, hdr,
477
- smooth_sigma = float(vmc.get("smooth_sigma", 1.0)),
478
- floor = float(vmc.get("floor", 1e-8)),
479
- tile_hw = tuple(vmc.get("tile_hw", (512, 512))),
480
- scratch_dir = vmc.get("scratch_dir", None),
481
- tag = f"varmap_{i:04d}",
482
+ smooth_sigma = vmc.get("smooth_sigma", 1.0),
483
+ floor = vmc.get("floor", 1e-8),
482
484
  status_cb = log,
483
- progress_cb = _vprog,
484
485
  )
485
- var = None # Option B: don't keep an open memmap handle
486
-
487
- # 🔻 free heavy temporaries immediately
488
- try:
489
- del luma
490
- del sky_map
491
- del rms_map
492
- except Exception:
493
- pass
494
- gc.collect()
495
486
 
496
- # per-frame summary
487
+ # small per-frame summary
497
488
  fwhm_est = _psf_fwhm_px(psf)
498
489
  logs.insert(0, f"MFDeconv: PSF{i}: ksize={psf.shape[0]} | FWHM≈{fwhm_est:.2f}px")
499
- return i, psf, mask, var, var_path, logs
490
+
491
+ return i, psf, mask, var, logs
492
+
500
493
 
501
494
  def _compute_one_worker(args):
502
495
  """
@@ -1186,25 +1179,81 @@ SOFT_SIGMA = 2.0
1186
1179
  ELLIPSE_SCALE = 1.2
1187
1180
 
1188
1181
  def _sep_background_precompute(img_2d: np.ndarray, bw: int = 64, bh: int = 64):
1189
- """One-time SEP background build; returns (sky_map, rms_map, err_scalar)."""
1182
+ """
1183
+ One-time SEP background build; returns (sky_map, rms_map, err_scalar).
1184
+
1185
+ Guarantees:
1186
+ - Always returns a 3-tuple (sky, rms, err)
1187
+ - sky/rms are float32 and same shape as img_2d
1188
+ - Robust to sep missing, sep errors, NaNs/Infs, and tiny frames
1189
+ """
1190
+ a = np.asarray(img_2d, dtype=np.float32)
1191
+ if a.ndim != 2:
1192
+ # be strict; callers expect 2D
1193
+ raise ValueError(f"_sep_background_precompute expects 2D, got shape={a.shape}")
1194
+
1195
+ H, W = int(a.shape[0]), int(a.shape[1])
1196
+ if H == 0 or W == 0:
1197
+ # should never happen, but don't return empty tuple
1198
+ sky = np.zeros((H, W), dtype=np.float32)
1199
+ rms = np.ones((H, W), dtype=np.float32)
1200
+ return sky, rms, 1.0
1201
+
1202
+ # --- robust fallback builder (works for any input) ---
1203
+ def _fallback():
1204
+ # Use finite-only stats if possible
1205
+ finite = np.isfinite(a)
1206
+ if finite.any():
1207
+ vals = a[finite]
1208
+ med = float(np.median(vals))
1209
+ mad = float(np.median(np.abs(vals - med))) + 1e-6
1210
+ else:
1211
+ med = 0.0
1212
+ mad = 1.0
1213
+ sky = np.full((H, W), med, dtype=np.float32)
1214
+ rms = np.full((H, W), 1.4826 * mad, dtype=np.float32)
1215
+ err = float(np.median(rms))
1216
+ return sky, rms, err
1217
+
1218
+ # If sep isn't available, always fallback
1190
1219
  if sep is None:
1191
- # robust fallback
1192
- med = float(np.median(img_2d))
1193
- mad = float(np.median(np.abs(img_2d - med))) + 1e-6
1194
- sky = np.full_like(img_2d, med, dtype=np.float32)
1195
- rmsm = np.full_like(img_2d, 1.4826 * mad, dtype=np.float32)
1196
- return sky, rmsm, float(np.median(rmsm))
1197
-
1198
- a = np.ascontiguousarray(img_2d.astype(np.float32))
1199
- b = sep.Background(a, bw=int(bw), bh=int(bh), fw=3, fh=3)
1200
- sky = np.asarray(b.back(), dtype=np.float32)
1220
+ return _fallback()
1221
+
1222
+ # SEP is present: sanitize input and clamp tile sizes
1223
+ # sep can choke on NaNs/Infs
1224
+ if not np.isfinite(a).all():
1225
+ # replace non-finite with median of finite values (or 0)
1226
+ finite = np.isfinite(a)
1227
+ fill = float(np.median(a[finite])) if finite.any() else 0.0
1228
+ a = np.where(finite, a, fill).astype(np.float32, copy=False)
1229
+
1230
+ a = np.ascontiguousarray(a, dtype=np.float32)
1231
+
1232
+ # Clamp bw/bh to image size; SEP doesn't like bw/bh > dims
1233
+ bw = int(max(8, min(int(bw), W)))
1234
+ bh = int(max(8, min(int(bh), H)))
1235
+
1201
1236
  try:
1202
- rmsm = np.asarray(b.rms(), dtype=np.float32)
1203
- err = float(b.globalrms)
1237
+ b = sep.Background(a, bw=bw, bh=bh, fw=3, fh=3)
1238
+
1239
+ sky = np.asarray(b.back(), dtype=np.float32)
1240
+ rms = np.asarray(b.rms(), dtype=np.float32)
1241
+
1242
+ # Ensure shape sanity (SEP should match, but be paranoid)
1243
+ if sky.shape != a.shape or rms.shape != a.shape:
1244
+ return _fallback()
1245
+
1246
+ # globalrms sometimes isn't available depending on SEP build
1247
+ err = float(getattr(b, "globalrms", np.nan))
1248
+ if not np.isfinite(err) or err <= 0:
1249
+ # robust scalar: median rms
1250
+ err = float(np.median(rms)) if rms.size else 1.0
1251
+
1252
+ return sky, rms, err
1253
+
1204
1254
  except Exception:
1205
- rmsm = np.full_like(a, float(np.median(b.rms())), dtype=np.float32)
1206
- err = float(np.median(rmsm))
1207
- return sky, rmsm, err
1255
+ # If SEP blows up for any reason, degrade gracefully
1256
+ return _fallback()
1208
1257
 
1209
1258
 
1210
1259
  def _star_mask_from_precomputed(
@@ -278,14 +278,20 @@ def _probe_hw(path: str) -> tuple[int, int, int | None]:
278
278
  raise ValueError(f"Unsupported ndim={a.ndim} for {path}")
279
279
 
280
280
  def _common_hw_from_paths(paths: list[str]) -> tuple[int, int]:
281
- """
282
- Replacement for the old FITS-only version: min(H), min(W) across files.
283
- """
284
281
  Hs, Ws = [], []
285
282
  for p in paths:
286
283
  h, w, _ = _probe_hw(p)
287
- Hs.append(int(h)); Ws.append(int(w))
288
- return int(min(Hs)), int(min(Ws))
284
+ h = int(h); w = int(w)
285
+ if h > 0 and w > 0:
286
+ Hs.append(h); Ws.append(w)
287
+
288
+ if not Hs:
289
+ raise ValueError("Could not determine any valid frame sizes.")
290
+ Ht = min(Hs); Wt = min(Ws)
291
+ if Ht < 8 or Wt < 8:
292
+ raise ValueError(f"Intersection too small: {Ht}x{Wt}")
293
+ return Ht, Wt
294
+
289
295
 
290
296
  def _to_chw_float32(img: np.ndarray, color_mode: str) -> np.ndarray:
291
297
  """
@@ -362,7 +368,6 @@ def _safe_primary_header(path: str) -> fits.Header:
362
368
  except Exception:
363
369
  return fits.Header()
364
370
 
365
-
366
371
  def _compute_frame_assets(i, arr, hdr, *, make_masks, make_varmaps,
367
372
  star_mask_cfg, varmap_cfg, status_sink=lambda s: None):
368
373
  """
@@ -380,21 +385,35 @@ def _compute_frame_assets(i, arr, hdr, *, make_masks, make_varmaps,
380
385
  f_whm = 2.5
381
386
  k_auto = _auto_ksize_from_fwhm(f_whm)
382
387
 
383
- # --- Star-derived PSF with retries ---
384
- tried, psf = [], None
385
- for k_try in [k_auto, max(k_auto - 4, 11), 21, 17, 15, 13, 11]:
386
- if k_try in tried: continue
387
- tried.append(k_try)
388
- try:
389
- out = compute_psf_kernel_for_image(arr, ksize=k_try, det_sigma=6.0, max_stars=80)
390
- psf_try = out[0] if (isinstance(out, tuple) and len(out) >= 1) else out
391
- if psf_try is not None:
392
- psf = psf_try
393
- break
394
- except Exception:
395
- psf = None
388
+ # --- Star-derived PSF with retries (dynamic det_sigma ladder) ---
389
+ psf = None
390
+
391
+ # Your existing ksize ladder
392
+ k_ladder = [k_auto, max(k_auto - 4, 11), 21, 17, 15, 13, 11]
393
+
394
+ # New: start high to avoid detecting 10k stars; step down only if needed
395
+ sigma_ladder = [50.0, 25.0, 12.0, 6.0]
396
+
397
+ tried = set()
398
+ for det_sigma in sigma_ladder:
399
+ for k_try in k_ladder:
400
+ if (det_sigma, k_try) in tried:
401
+ continue
402
+ tried.add((det_sigma, k_try))
403
+ try:
404
+ out = compute_psf_kernel_for_image(arr, ksize=k_try, det_sigma=det_sigma, max_stars=80)
405
+ psf_try = out[0] if (isinstance(out, tuple) and len(out) >= 1) else out
406
+ if psf_try is not None:
407
+ psf = psf_try
408
+ break
409
+ except Exception:
410
+ psf = None
411
+ if psf is not None:
412
+ break
413
+
396
414
  if psf is None:
397
415
  psf = _gaussian_psf(f_whm, ksize=k_auto)
416
+
398
417
  psf = _soften_psf(_normalize_psf(psf.astype(np.float32, copy=False)), sigma_px=0.25)
399
418
 
400
419
  mask = None
@@ -438,6 +457,7 @@ def _compute_frame_assets(i, arr, hdr, *, make_masks, make_varmaps,
438
457
 
439
458
  return i, psf, mask, var, logs
440
459
 
460
+
441
461
  def _compute_one_worker(args):
442
462
  """
443
463
  Top-level picklable worker for ProcessPoolExecutor.
@@ -1026,25 +1046,82 @@ SOFT_SIGMA = 2.0
1026
1046
  ELLIPSE_SCALE = 1.2
1027
1047
 
1028
1048
  def _sep_background_precompute(img_2d: np.ndarray, bw: int = 64, bh: int = 64):
1029
- """One-time SEP background build; returns (sky_map, rms_map, err_scalar)."""
1049
+ """
1050
+ One-time SEP background build; returns (sky_map, rms_map, err_scalar).
1051
+
1052
+ Guarantees:
1053
+ - Always returns a 3-tuple (sky, rms, err)
1054
+ - sky/rms are float32 and same shape as img_2d
1055
+ - Robust to sep missing, sep errors, NaNs/Infs, and tiny frames
1056
+ """
1057
+ a = np.asarray(img_2d, dtype=np.float32)
1058
+ if a.ndim != 2:
1059
+ # be strict; callers expect 2D
1060
+ raise ValueError(f"_sep_background_precompute expects 2D, got shape={a.shape}")
1061
+
1062
+ H, W = int(a.shape[0]), int(a.shape[1])
1063
+ if H == 0 or W == 0:
1064
+ # should never happen, but don't return empty tuple
1065
+ sky = np.zeros((H, W), dtype=np.float32)
1066
+ rms = np.ones((H, W), dtype=np.float32)
1067
+ return sky, rms, 1.0
1068
+
1069
+ # --- robust fallback builder (works for any input) ---
1070
+ def _fallback():
1071
+ # Use finite-only stats if possible
1072
+ finite = np.isfinite(a)
1073
+ if finite.any():
1074
+ vals = a[finite]
1075
+ med = float(np.median(vals))
1076
+ mad = float(np.median(np.abs(vals - med))) + 1e-6
1077
+ else:
1078
+ med = 0.0
1079
+ mad = 1.0
1080
+ sky = np.full((H, W), med, dtype=np.float32)
1081
+ rms = np.full((H, W), 1.4826 * mad, dtype=np.float32)
1082
+ err = float(np.median(rms))
1083
+ return sky, rms, err
1084
+
1085
+ # If sep isn't available, always fallback
1030
1086
  if sep is None:
1031
- # robust fallback
1032
- med = float(np.median(img_2d))
1033
- mad = float(np.median(np.abs(img_2d - med))) + 1e-6
1034
- sky = np.full_like(img_2d, med, dtype=np.float32)
1035
- rmsm = np.full_like(img_2d, 1.4826 * mad, dtype=np.float32)
1036
- return sky, rmsm, float(np.median(rmsm))
1037
-
1038
- a = np.ascontiguousarray(img_2d.astype(np.float32))
1039
- b = sep.Background(a, bw=int(bw), bh=int(bh), fw=3, fh=3)
1040
- sky = np.asarray(b.back(), dtype=np.float32)
1087
+ return _fallback()
1088
+
1089
+ # SEP is present: sanitize input and clamp tile sizes
1090
+ # sep can choke on NaNs/Infs
1091
+ if not np.isfinite(a).all():
1092
+ # replace non-finite with median of finite values (or 0)
1093
+ finite = np.isfinite(a)
1094
+ fill = float(np.median(a[finite])) if finite.any() else 0.0
1095
+ a = np.where(finite, a, fill).astype(np.float32, copy=False)
1096
+
1097
+ a = np.ascontiguousarray(a, dtype=np.float32)
1098
+
1099
+ # Clamp bw/bh to image size; SEP doesn't like bw/bh > dims
1100
+ bw = int(max(8, min(int(bw), W)))
1101
+ bh = int(max(8, min(int(bh), H)))
1102
+
1041
1103
  try:
1042
- rmsm = np.asarray(b.rms(), dtype=np.float32)
1043
- err = float(b.globalrms)
1104
+ b = sep.Background(a, bw=bw, bh=bh, fw=3, fh=3)
1105
+
1106
+ sky = np.asarray(b.back(), dtype=np.float32)
1107
+ rms = np.asarray(b.rms(), dtype=np.float32)
1108
+
1109
+ # Ensure shape sanity (SEP should match, but be paranoid)
1110
+ if sky.shape != a.shape or rms.shape != a.shape:
1111
+ return _fallback()
1112
+
1113
+ # globalrms sometimes isn't available depending on SEP build
1114
+ err = float(getattr(b, "globalrms", np.nan))
1115
+ if not np.isfinite(err) or err <= 0:
1116
+ # robust scalar: median rms
1117
+ err = float(np.median(rms)) if rms.size else 1.0
1118
+
1119
+ return sky, rms, err
1120
+
1044
1121
  except Exception:
1045
- rmsm = np.full_like(a, float(np.median(b.rms())), dtype=np.float32)
1046
- err = float(np.median(rmsm))
1047
- return sky, rmsm, err
1122
+ # If SEP blows up for any reason, degrade gracefully
1123
+ return _fallback()
1124
+
1048
1125
 
1049
1126
 
1050
1127
  def _star_mask_from_precomputed(
@@ -94,6 +94,10 @@ class MorphologyDialogPro(QDialog):
94
94
  self.setWindowFlag(Qt.WindowType.Window, True)
95
95
  self.setWindowModality(Qt.WindowModality.NonModal)
96
96
  self.setModal(False)
97
+ try:
98
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
99
+ except Exception:
100
+ pass # older PyQt6 versions
97
101
  if icon:
98
102
  try: self.setWindowIcon(icon)
99
103
  except Exception as e: