setiastrosuitepro 1.6.4__py3-none-any.whl → 1.6.10__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 (112) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/acv_icon.png +0 -0
  3. setiastro/images/cosmic.svg +40 -0
  4. setiastro/images/cosmicsat.svg +24 -0
  5. setiastro/images/first_quarter.png +0 -0
  6. setiastro/images/full_moon.png +0 -0
  7. setiastro/images/graxpert.svg +19 -0
  8. setiastro/images/last_quarter.png +0 -0
  9. setiastro/images/linearfit.svg +32 -0
  10. setiastro/images/new_moon.png +0 -0
  11. setiastro/images/pixelmath.svg +42 -0
  12. setiastro/images/waning_crescent_1.png +0 -0
  13. setiastro/images/waning_crescent_2.png +0 -0
  14. setiastro/images/waning_crescent_3.png +0 -0
  15. setiastro/images/waning_crescent_4.png +0 -0
  16. setiastro/images/waning_crescent_5.png +0 -0
  17. setiastro/images/waning_gibbous_1.png +0 -0
  18. setiastro/images/waning_gibbous_2.png +0 -0
  19. setiastro/images/waning_gibbous_3.png +0 -0
  20. setiastro/images/waning_gibbous_4.png +0 -0
  21. setiastro/images/waning_gibbous_5.png +0 -0
  22. setiastro/images/waxing_crescent_1.png +0 -0
  23. setiastro/images/waxing_crescent_2.png +0 -0
  24. setiastro/images/waxing_crescent_3.png +0 -0
  25. setiastro/images/waxing_crescent_4.png +0 -0
  26. setiastro/images/waxing_crescent_5.png +0 -0
  27. setiastro/images/waxing_gibbous_1.png +0 -0
  28. setiastro/images/waxing_gibbous_2.png +0 -0
  29. setiastro/images/waxing_gibbous_3.png +0 -0
  30. setiastro/images/waxing_gibbous_4.png +0 -0
  31. setiastro/images/waxing_gibbous_5.png +0 -0
  32. setiastro/qml/ResourceMonitor.qml +84 -82
  33. setiastro/saspro/__main__.py +19 -0
  34. setiastro/saspro/_generated/build_info.py +2 -2
  35. setiastro/saspro/abe.py +37 -4
  36. setiastro/saspro/aberration_ai.py +237 -21
  37. setiastro/saspro/acv_exporter.py +379 -0
  38. setiastro/saspro/add_stars.py +33 -6
  39. setiastro/saspro/backgroundneutral.py +35 -7
  40. setiastro/saspro/blemish_blaster.py +4 -1
  41. setiastro/saspro/blink_comparator_pro.py +74 -24
  42. setiastro/saspro/clahe.py +4 -1
  43. setiastro/saspro/continuum_subtract.py +4 -1
  44. setiastro/saspro/convo.py +4 -1
  45. setiastro/saspro/cosmicclarity.py +129 -18
  46. setiastro/saspro/crop_dialog_pro.py +123 -7
  47. setiastro/saspro/curve_editor_pro.py +109 -42
  48. setiastro/saspro/doc_manager.py +67 -4
  49. setiastro/saspro/exoplanet_detector.py +120 -28
  50. setiastro/saspro/frequency_separation.py +1158 -204
  51. setiastro/saspro/ghs_dialog_pro.py +81 -16
  52. setiastro/saspro/graxpert.py +1 -0
  53. setiastro/saspro/gui/main_window.py +393 -204
  54. setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
  55. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  56. setiastro/saspro/gui/mixins/toolbar_mixin.py +356 -12
  57. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  58. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  59. setiastro/saspro/halobgon.py +4 -0
  60. setiastro/saspro/histogram.py +5 -1
  61. setiastro/saspro/image_combine.py +4 -0
  62. setiastro/saspro/image_peeker_pro.py +4 -0
  63. setiastro/saspro/imageops/stretch.py +531 -62
  64. setiastro/saspro/isophote.py +4 -0
  65. setiastro/saspro/layers.py +13 -9
  66. setiastro/saspro/layers_dock.py +183 -3
  67. setiastro/saspro/legacy/image_manager.py +154 -20
  68. setiastro/saspro/legacy/numba_utils.py +43 -0
  69. setiastro/saspro/legacy/xisf.py +240 -98
  70. setiastro/saspro/live_stacking.py +180 -79
  71. setiastro/saspro/luminancerecombine.py +228 -27
  72. setiastro/saspro/mask_creation.py +174 -15
  73. setiastro/saspro/mfdeconv.py +113 -35
  74. setiastro/saspro/mfdeconvcudnn.py +119 -70
  75. setiastro/saspro/mfdeconvsport.py +112 -35
  76. setiastro/saspro/morphology.py +4 -0
  77. setiastro/saspro/multiscale_decomp.py +51 -12
  78. setiastro/saspro/numba_utils.py +72 -2
  79. setiastro/saspro/ops/commands.py +18 -18
  80. setiastro/saspro/ops/script_editor.py +5 -2
  81. setiastro/saspro/ops/scripts.py +3 -0
  82. setiastro/saspro/perfect_palette_picker.py +37 -3
  83. setiastro/saspro/plate_solver.py +84 -49
  84. setiastro/saspro/psf_viewer.py +119 -37
  85. setiastro/saspro/resources.py +67 -0
  86. setiastro/saspro/rgbalign.py +4 -0
  87. setiastro/saspro/selective_color.py +4 -1
  88. setiastro/saspro/sfcc.py +60 -2
  89. setiastro/saspro/shortcuts.py +142 -23
  90. setiastro/saspro/signature_insert.py +692 -33
  91. setiastro/saspro/stacking_suite.py +1017 -400
  92. setiastro/saspro/star_alignment.py +4 -1
  93. setiastro/saspro/star_spikes.py +4 -0
  94. setiastro/saspro/star_stretch.py +38 -3
  95. setiastro/saspro/stat_stretch.py +702 -128
  96. setiastro/saspro/subwindow.py +786 -360
  97. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  98. setiastro/saspro/wavescale_hdr.py +4 -1
  99. setiastro/saspro/wavescalede.py +4 -1
  100. setiastro/saspro/whitebalance.py +60 -12
  101. setiastro/saspro/widgets/common_utilities.py +28 -21
  102. setiastro/saspro/widgets/resource_monitor.py +109 -59
  103. setiastro/saspro/widgets/spinboxes.py +10 -13
  104. setiastro/saspro/wimi.py +27 -656
  105. setiastro/saspro/wims.py +13 -3
  106. setiastro/saspro/xisf.py +101 -11
  107. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +2 -1
  108. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +112 -80
  109. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
  110. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
  111. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
  112. {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
@@ -1555,6 +1555,9 @@ def debug_dump_metadata_print(meta: dict, context: str = ""):
1555
1555
 
1556
1556
  print("===== END METADATA DUMP ({}) =====".format(context))
1557
1557
 
1558
+ import time
1559
+ _DEBUG_DND_DUP = False
1560
+
1558
1561
  class DocManager(QObject):
1559
1562
  documentAdded = pyqtSignal(object) # ImageDocument
1560
1563
  documentRemoved = pyqtSignal(object) # ImageDocument
@@ -2266,6 +2269,46 @@ class DocManager(QObject):
2266
2269
  if hasattr(doc, "changed"):
2267
2270
  doc.changed.emit()
2268
2271
 
2272
+ def _current_view_title_for_doc(self, source_doc: ImageDocument) -> str | None:
2273
+ """
2274
+ If the active MDI subwindow is showing 'source_doc' (or its parent/base),
2275
+ return the current view's title (windowTitle), otherwise None.
2276
+ """
2277
+ sw = self._active_subwindow()
2278
+ if sw is None:
2279
+ return None
2280
+
2281
+ try:
2282
+ w = sw.widget()
2283
+ except Exception:
2284
+ w = None
2285
+
2286
+ # Resolve what doc the active view corresponds to (base doc)
2287
+ try:
2288
+ base = (
2289
+ getattr(w, "base_document", None)
2290
+ or getattr(w, "_base_document", None)
2291
+ or getattr(w, "document", None)
2292
+ or getattr(sw, "document", None)
2293
+ )
2294
+ parent = getattr(base, "_parent_doc", None)
2295
+ if isinstance(parent, ImageDocument):
2296
+ base = parent
2297
+ except Exception:
2298
+ base = None
2299
+
2300
+ if base is not source_doc:
2301
+ return None
2302
+
2303
+ # Prefer the actual subwindow title (includes [View N], etc.)
2304
+ try:
2305
+ title = sw.windowTitle()
2306
+ title = title.strip() if isinstance(title, str) else ""
2307
+ return title or None
2308
+ except Exception:
2309
+ return None
2310
+
2311
+
2269
2312
  def duplicate_document(self, source_doc: ImageDocument, new_name: str | None = None) -> ImageDocument:
2270
2313
  # DEBUG: log the source doc WCS before we touch anything
2271
2314
  if _DEBUG_WCS:
@@ -2275,19 +2318,33 @@ class DocManager(QObject):
2275
2318
  name = "<src>"
2276
2319
 
2277
2320
  _debug_log_wcs_context(" source.metadata", getattr(source_doc, "metadata", {}))
2278
-
2321
+ if _DEBUG_DND_DUP:
2322
+ try:
2323
+ src_dn = source_doc.display_name() if hasattr(source_doc, "display_name") else None
2324
+ except Exception:
2325
+ src_dn = None
2326
+ print("\n[DNDDBG:DUPLICATE_DOCUMENT]")
2327
+ print(" source_doc:", source_doc, "id:", id(source_doc), "uid:", getattr(source_doc,"uid",None))
2328
+ print(" source_doc.display_name():", src_dn)
2329
+ print(" new_name arg:", new_name)
2279
2330
  # COPY-ON-WRITE: Share the source image instead of copying immediately.
2280
2331
  # The duplicate's apply_edit will copy when it first modifies the image.
2281
2332
  # This saves memory when duplicates are created but not modified.
2282
2333
  img_ref = source_doc.image # Shared reference, no copy
2283
2334
 
2284
2335
  meta = dict(source_doc.metadata or {})
2285
- base = source_doc.display_name()
2336
+
2337
+ # ✅ Use CURRENT VIEW NAME if this doc is what's active; else fall back to doc display_name()
2338
+ base = self._current_view_title_for_doc(source_doc) or source_doc.display_name()
2339
+
2286
2340
  dup_title = (new_name or f"{base}_duplicate")
2341
+
2287
2342
  # 🚫 strip any lingering emojis / link markers
2288
2343
  dup_title = dup_title.replace("🔗", "").strip()
2289
- meta["display_name"] = dup_title
2290
2344
 
2345
+ meta["display_name"] = dup_title
2346
+ if _DEBUG_DND_DUP:
2347
+ print(" dup_title computed:", dup_title)
2291
2348
  # Remove anything that makes the view look "linked/preview"
2292
2349
  imi = dict(meta.get("image_meta") or {})
2293
2350
  for k in ("readonly", "view_kind", "derived_from", "layer", "layer_index", "linked"):
@@ -2311,7 +2368,13 @@ class DocManager(QObject):
2311
2368
  # Mark this duplicate as sharing image data with source
2312
2369
  dup._cow_source = source_doc
2313
2370
  self._register_doc(dup)
2314
-
2371
+ if _DEBUG_DND_DUP:
2372
+ try:
2373
+ dn = dup.display_name() if hasattr(dup, "display_name") else None
2374
+ except Exception:
2375
+ dn = None
2376
+ print(" dup.metadata.display_name:", (dup.metadata or {}).get("display_name"))
2377
+ print(" dup.display_name():", dn)
2315
2378
  # DEBUG: log the duplicate doc WCS
2316
2379
  if _DEBUG_WCS:
2317
2380
  try:
@@ -27,6 +27,7 @@ from astropy.coordinates import SkyCoord
27
27
  import astropy.units as u
28
28
  from astropy.wcs import WCS
29
29
  from astropy.timeseries import LombScargle, BoxLeastSquares
30
+ import re
30
31
 
31
32
  from astroquery.simbad import Simbad
32
33
  from astroquery.vizier import Vizier
@@ -152,6 +153,41 @@ def _estimate_scale_arcsec_per_pix(h: fits.Header):
152
153
 
153
154
  return None
154
155
 
156
+ _TZ_RE = re.compile(r'([+-])(\d{2})(\d{2})$') # -0700 -> -07:00
157
+
158
+ def _fix_iso_tz(s: str) -> str:
159
+ s = s.strip()
160
+ m = _TZ_RE.search(s)
161
+ if m:
162
+ s = s[:m.start()] + f"{m.group(1)}{m.group(2)}:{m.group(3)}"
163
+ return s
164
+
165
+ def _parse_obs_time_from_header(hdr) -> Time | None:
166
+ # hdr can be fits.Header or your dict-ish header
167
+ def _get(key):
168
+ try:
169
+ return hdr.get(key)
170
+ except Exception:
171
+ return None
172
+
173
+ # 1) Prefer UT-OBS if present (already “UTC-ish”)
174
+ for key in ("UT-OBS", "DATE-OBS", "DATE-END"):
175
+ v = _get(key)
176
+ if isinstance(v, str) and v.strip():
177
+ try:
178
+ return Time(_fix_iso_tz(v), format="isot", scale="utc")
179
+ except Exception:
180
+ pass
181
+
182
+ # 2) MJD-OBS is super reliable
183
+ v = _get("MJD-OBS")
184
+ if v is not None:
185
+ try:
186
+ return Time(float(v), format="mjd", scale="utc")
187
+ except Exception:
188
+ pass
189
+
190
+ return None
155
191
 
156
192
  def _estimate_fov_deg(img_shape, scale_arcsec):
157
193
  """Rough FOV (deg) from image size and scale (max of X/Y)."""
@@ -646,9 +682,12 @@ class ExoPlanetWindow(QDialog):
646
682
  self.measure_btn.setVisible(is_raw)
647
683
 
648
684
  def load_and_measure_subs(self):
685
+ before = len(getattr(self, "image_paths", []))
649
686
  self.load_aligned_subs()
687
+ after = len(getattr(self, "image_paths", []))
688
+ if after == 0 or after == before and not self._cached_images:
689
+ return
650
690
  self.detect_stars()
651
-
652
691
  # --------------- I/O + Calibration ----------------
653
692
 
654
693
  def load_raw_subs(self):
@@ -682,13 +721,38 @@ class ExoPlanetWindow(QDialog):
682
721
  ds = hdr0.get('DATE-OBS')
683
722
  except:
684
723
  ds = None
724
+
725
+ # Use robust header time parsing (UT-OBS -> DATE-OBS -> MJD-OBS fallback)
685
726
  t = None
686
- if isinstance(ds, str):
687
- try:
688
- t = Time(ds, format='isot', scale='utc')
689
- except Exception as e:
690
- print(f"[DEBUG] Failed to parse DATE-OBS for {p}: {e}")
727
+ try:
728
+ if ext == ".xisf":
729
+ # Build a tiny dict-like header for the helper
730
+ hdr_like = {}
731
+ try:
732
+ xisf = XISF(p)
733
+ img_meta = xisf.get_images_metadata()[0]
734
+ kw = img_meta.get("FITSKeywords", {}) or {}
735
+ # XISF FITSKeywords layout: key -> [ {value: ...}, ... ]
736
+ for k in ("UT-OBS", "DATE-OBS", "DATE-END", "MJD-OBS"):
737
+ if k in kw and kw[k]:
738
+ hdr_like[k] = kw[k][0].get("value")
739
+ except Exception:
740
+ hdr_like = {}
741
+ t = _parse_obs_time_from_header(hdr_like)
742
+
743
+ elif ext in (".fit", ".fits", ".fz"):
744
+ hdr0, _ = get_valid_header(p)
745
+ t = _parse_obs_time_from_header(hdr0)
746
+
747
+ else:
748
+ # TIFF etc. may not have FITS-like time headers; leave None
749
+ t = None
750
+
751
+ except Exception as e:
752
+ print(f"[DEBUG] Failed to parse obs time for {p}: {e}")
753
+
691
754
  datelist.append((p, t))
755
+
692
756
  self.progress_bar.setValue(i)
693
757
  QApplication.processEvents()
694
758
 
@@ -768,24 +832,28 @@ class ExoPlanetWindow(QDialog):
768
832
  self.progress_bar.setValue(i)
769
833
  QApplication.processEvents()
770
834
 
771
- iso_strs, mask_arr = [], []
772
- for _, t in datelist:
773
- if t is not None:
774
- iso_strs.append(t.isot); mask_arr.append(False)
775
- else:
776
- iso_strs.append(''); mask_arr.append(True)
777
- ma_strs = np.ma.MaskedArray(iso_strs, mask=mask_arr)
778
- self.times = Time(ma_strs, format='isot', scale='utc', out_subfmt='date')
835
+ # Keep full timestamps (DO NOT truncate to date-only)
836
+ tlist = [t for _, t in datelist if t is not None]
837
+ if tlist:
838
+ self.times = Time(tlist) # already utc scale from helper
839
+ else:
840
+ self.times = None
779
841
 
780
842
  self.progress_bar.setVisible(False)
781
843
  loaded = sum(1 for im in self._cached_images if im is not None)
782
844
  self.status_label.setText(f"Loaded {loaded}/{len(sorted_paths)} raw frames")
783
845
 
784
- def load_aligned_subs(self):
846
+ def load_aligned_subs(self) -> bool:
785
847
  settings = QSettings()
786
848
  start_dir = settings.value("ExoPlanet/lastAlignedFolder", os.path.expanduser("~"), type=str)
787
- paths, _ = QFileDialog.getOpenFileNames(self, "Select Aligned Frames", start_dir, "FITS or TIFF (*.fit *.fits *.tif *.tiff *.xisf)")
788
- if not paths: return
849
+ paths, _ = QFileDialog.getOpenFileNames(
850
+ self, "Select Aligned Frames", start_dir,
851
+ "FITS or TIFF (*.fit *.fits *.tif *.tiff *.xisf)"
852
+ )
853
+ if not paths:
854
+ self.status_label.setText("Load canceled.")
855
+ return False
856
+
789
857
  settings.setValue("ExoPlanet/lastAlignedFolder", os.path.dirname(paths[0]))
790
858
 
791
859
  self.status_label.setText("Reading metadata from aligned frames…")
@@ -809,10 +877,35 @@ class ExoPlanetWindow(QDialog):
809
877
  hdr0, _ = get_valid_header(p)
810
878
  ds = hdr0.get('DATE-OBS')
811
879
  except: ds = None
880
+ # Use robust header time parsing (UT-OBS -> DATE-OBS -> MJD-OBS fallback)
812
881
  t = None
813
- if isinstance(ds, str):
814
- try: t = Time(ds, format='isot', scale='utc')
815
- except Exception as e: print(f"[DEBUG] Failed to parse DATE-OBS for {p}: {e}")
882
+ try:
883
+ if ext == ".xisf":
884
+ # Build a tiny dict-like header for the helper
885
+ hdr_like = {}
886
+ try:
887
+ xisf = XISF(p)
888
+ img_meta = xisf.get_images_metadata()[0]
889
+ kw = img_meta.get("FITSKeywords", {}) or {}
890
+ # XISF FITSKeywords layout: key -> [ {value: ...}, ... ]
891
+ for k in ("UT-OBS", "DATE-OBS", "DATE-END", "MJD-OBS"):
892
+ if k in kw and kw[k]:
893
+ hdr_like[k] = kw[k][0].get("value")
894
+ except Exception:
895
+ hdr_like = {}
896
+ t = _parse_obs_time_from_header(hdr_like)
897
+
898
+ elif ext in (".fit", ".fits", ".fz"):
899
+ hdr0, _ = get_valid_header(p)
900
+ t = _parse_obs_time_from_header(hdr0)
901
+
902
+ else:
903
+ # TIFF etc. may not have FITS-like time headers; leave None
904
+ t = None
905
+
906
+ except Exception as e:
907
+ print(f"[DEBUG] Failed to parse obs time for {p}: {e}")
908
+
816
909
  datelist.append((p, t))
817
910
  self.progress_bar.setValue(i)
818
911
  QApplication.processEvents()
@@ -889,18 +982,17 @@ class ExoPlanetWindow(QDialog):
889
982
  self.progress_bar.setValue(i)
890
983
  QApplication.processEvents()
891
984
 
892
- iso_strs, mask_arr = [], []
893
- for _, t in datelist:
894
- if t is not None:
895
- iso_strs.append(t.isot); mask_arr.append(False)
896
- else:
897
- iso_strs.append(''); mask_arr.append(True)
898
- ma_strs = np.ma.MaskedArray(iso_strs, mask=mask_arr)
899
- self.times = Time(ma_strs, format='isot', scale='utc', out_subfmt='date')
985
+ # Keep full timestamps (DO NOT truncate to date-only)
986
+ tlist = [t for _, t in datelist if t is not None]
987
+ if tlist:
988
+ self.times = Time(tlist) # already utc scale from helper
989
+ else:
990
+ self.times = None
900
991
 
901
992
  self.progress_bar.setVisible(False)
902
993
  loaded = sum(1 for im in self._cached_images if im is not None)
903
994
  self.status_label.setText(f"Loaded {loaded}/{len(sorted_paths)} aligned frames")
995
+ return loaded > 0
904
996
 
905
997
  def load_masters(self):
906
998
  settings = QSettings()