setiastrosuitepro 1.6.12__py3-none-any.whl → 1.7.3__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.
- setiastro/images/3dplanet.png +0 -0
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__init__.py +9 -8
- setiastro/saspro/__main__.py +326 -285
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +128 -13
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +4 -1
- setiastro/saspro/gui/main_window.py +326 -46
- setiastro/saspro/gui/mixins/file_mixin.py +41 -18
- setiastro/saspro/gui/mixins/menu_mixin.py +9 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +123 -7
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1429 -0
- setiastro/saspro/layers.py +186 -10
- setiastro/saspro/layers_dock.py +198 -5
- setiastro/saspro/legacy/image_manager.py +10 -4
- setiastro/saspro/legacy/numba_utils.py +1 -1
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/planetprojection.py +3854 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +8 -0
- setiastro/saspro/rgbalign.py +456 -12
- setiastro/saspro/save_options.py +45 -13
- setiastro/saspro/ser_stack_config.py +102 -0
- setiastro/saspro/ser_stacker.py +2327 -0
- setiastro/saspro/ser_stacker_dialog.py +1865 -0
- setiastro/saspro/ser_tracking.py +228 -0
- setiastro/saspro/serviewer.py +1773 -0
- setiastro/saspro/sfcc.py +298 -64
- setiastro/saspro/shortcuts.py +14 -7
- setiastro/saspro/stacking_suite.py +21 -6
- setiastro/saspro/stat_stretch.py +179 -31
- setiastro/saspro/subwindow.py +38 -5
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/METADATA +3 -2
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/RECORD +51 -37
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/sfcc.py
CHANGED
|
@@ -20,6 +20,7 @@ from datetime import datetime
|
|
|
20
20
|
from typing import List, Tuple, Optional
|
|
21
21
|
|
|
22
22
|
import numpy as np
|
|
23
|
+
import numpy.ma as ma
|
|
23
24
|
import pandas as pd
|
|
24
25
|
|
|
25
26
|
# ── SciPy bits
|
|
@@ -32,6 +33,7 @@ from astropy.wcs import WCS
|
|
|
32
33
|
import astropy.units as u
|
|
33
34
|
from astropy.coordinates import SkyCoord
|
|
34
35
|
from astroquery.simbad import Simbad
|
|
36
|
+
from astropy.wcs.wcs import NoConvergence
|
|
35
37
|
|
|
36
38
|
# ── SEP (Source Extractor)
|
|
37
39
|
import sep
|
|
@@ -969,8 +971,105 @@ class SFCCDialog(QDialog):
|
|
|
969
971
|
|
|
970
972
|
|
|
971
973
|
# ── SIMBAD/Star fetch ──────────────────────────────────────────────
|
|
974
|
+
def initialize_wcs_from_header(self, header):
|
|
975
|
+
"""
|
|
976
|
+
Build a robust 2D celestial WCS from the provided header.
|
|
977
|
+
|
|
978
|
+
- Normalizes deprecated RADECSYS/EPOCH keywords.
|
|
979
|
+
- Uses relax=True.
|
|
980
|
+
- Stores:
|
|
981
|
+
self.wcs (WCS)
|
|
982
|
+
self.wcs_header (fits.Header)
|
|
983
|
+
self.pixscale (arcsec/pixel approx)
|
|
984
|
+
self.center_ra, self.center_dec (deg)
|
|
985
|
+
self.orientation (deg, if derivable)
|
|
986
|
+
"""
|
|
987
|
+
if header is None:
|
|
988
|
+
print("[SFCC] No FITS header available; cannot build WCS.")
|
|
989
|
+
self.wcs = None
|
|
990
|
+
return
|
|
991
|
+
|
|
992
|
+
try:
|
|
993
|
+
hdr = header.copy()
|
|
994
|
+
|
|
995
|
+
# --- normalize deprecated keywords ---
|
|
996
|
+
if "RADECSYS" in hdr and "RADESYS" not in hdr:
|
|
997
|
+
radesys_val = str(hdr["RADECSYS"]).strip()
|
|
998
|
+
hdr["RADESYS"] = radesys_val
|
|
999
|
+
try:
|
|
1000
|
+
del hdr["RADECSYS"]
|
|
1001
|
+
except Exception:
|
|
1002
|
+
pass
|
|
1003
|
+
|
|
1004
|
+
# Carry to alternate WCS letters if present (CTYPE1A, CTYPE2A, etc.)
|
|
1005
|
+
alt_letters = {
|
|
1006
|
+
k[-1]
|
|
1007
|
+
for k in hdr.keys()
|
|
1008
|
+
if re.match(r"^CTYPE[12][A-Z]$", k)
|
|
1009
|
+
}
|
|
1010
|
+
for a in alt_letters:
|
|
1011
|
+
key = f"RADESYS{a}"
|
|
1012
|
+
if key not in hdr:
|
|
1013
|
+
hdr[key] = radesys_val
|
|
1014
|
+
|
|
1015
|
+
if "EPOCH" in hdr and "EQUINOX" not in hdr:
|
|
1016
|
+
hdr["EQUINOX"] = hdr["EPOCH"]
|
|
1017
|
+
try:
|
|
1018
|
+
del hdr["EPOCH"]
|
|
1019
|
+
except Exception:
|
|
1020
|
+
pass
|
|
1021
|
+
|
|
1022
|
+
# Build WCS
|
|
1023
|
+
self.wcs = WCS(hdr, naxis=2, relax=True)
|
|
1024
|
+
|
|
1025
|
+
# Pixel scale estimate (arcsec/px) from pixel_scale_matrix if available
|
|
1026
|
+
try:
|
|
1027
|
+
psm = self.wcs.pixel_scale_matrix
|
|
1028
|
+
self.pixscale = float(np.hypot(psm[0, 0], psm[1, 0]) * 3600.0)
|
|
1029
|
+
except Exception:
|
|
1030
|
+
self.pixscale = None
|
|
1031
|
+
|
|
1032
|
+
# CRVAL center
|
|
1033
|
+
try:
|
|
1034
|
+
self.center_ra, self.center_dec = [float(x) for x in self.wcs.wcs.crval]
|
|
1035
|
+
except Exception:
|
|
1036
|
+
self.center_ra, self.center_dec = None, None
|
|
1037
|
+
|
|
1038
|
+
# Save normalized header form
|
|
1039
|
+
try:
|
|
1040
|
+
self.wcs_header = self.wcs.to_header(relax=True)
|
|
1041
|
+
except Exception:
|
|
1042
|
+
self.wcs_header = None
|
|
1043
|
+
|
|
1044
|
+
# Orientation (optional)
|
|
1045
|
+
if "CROTA2" in hdr:
|
|
1046
|
+
try:
|
|
1047
|
+
self.orientation = float(hdr["CROTA2"])
|
|
1048
|
+
except Exception:
|
|
1049
|
+
self.orientation = None
|
|
1050
|
+
else:
|
|
1051
|
+
self.orientation = self.calculate_orientation(hdr)
|
|
1052
|
+
|
|
1053
|
+
if getattr(self, "orientation_label", None) is not None:
|
|
1054
|
+
if self.orientation is not None:
|
|
1055
|
+
self.orientation_label.setText(f"Orientation: {self.orientation:.2f}°")
|
|
1056
|
+
else:
|
|
1057
|
+
self.orientation_label.setText("Orientation: N/A")
|
|
1058
|
+
|
|
1059
|
+
except Exception as e:
|
|
1060
|
+
print("[SFCC] WCS initialization error:\n", e)
|
|
1061
|
+
self.wcs = None
|
|
1062
|
+
|
|
972
1063
|
|
|
973
1064
|
def fetch_stars(self):
|
|
1065
|
+
import time
|
|
1066
|
+
from astropy.coordinates import SkyCoord
|
|
1067
|
+
import astropy.units as u
|
|
1068
|
+
from astropy.wcs.wcs import NoConvergence
|
|
1069
|
+
from astroquery.simbad import Simbad
|
|
1070
|
+
from astropy.io import fits
|
|
1071
|
+
from PyQt6.QtWidgets import QMessageBox, QApplication
|
|
1072
|
+
|
|
974
1073
|
# 0) Grab current image + header from the active document
|
|
975
1074
|
img, hdr, _meta = self._get_active_image_and_header()
|
|
976
1075
|
self.current_image = img
|
|
@@ -986,7 +1085,7 @@ class SFCCDialog(QDialog):
|
|
|
986
1085
|
self.pickles_templates = []
|
|
987
1086
|
for p in (self.user_custom_path, self.sasp_data_path):
|
|
988
1087
|
try:
|
|
989
|
-
with fits.open(p) as hd:
|
|
1088
|
+
with fits.open(p, memmap=False) as hd:
|
|
990
1089
|
for hdu in hd:
|
|
991
1090
|
if (isinstance(hdu, fits.BinTableHDU)
|
|
992
1091
|
and hdu.header.get("CTYPE", "").upper() == "SED"):
|
|
@@ -994,115 +1093,250 @@ class SFCCDialog(QDialog):
|
|
|
994
1093
|
if extname and extname not in self.pickles_templates:
|
|
995
1094
|
self.pickles_templates.append(extname)
|
|
996
1095
|
except Exception as e:
|
|
997
|
-
print(f"[fetch_stars] Could not load Pickles templates from {p}: {e}")
|
|
1096
|
+
print(f"[SFCC] [fetch_stars] Could not load Pickles templates from {p}: {e}")
|
|
1097
|
+
self.pickles_templates.sort()
|
|
998
1098
|
|
|
999
1099
|
# Build WCS
|
|
1000
1100
|
try:
|
|
1001
1101
|
self.initialize_wcs_from_header(self.current_header)
|
|
1002
1102
|
except Exception:
|
|
1003
|
-
QMessageBox.critical(self, "WCS Error", "Could not build a 2D WCS from header.")
|
|
1103
|
+
QMessageBox.critical(self, "WCS Error", "Could not build a 2D WCS from header.")
|
|
1104
|
+
return
|
|
1105
|
+
|
|
1106
|
+
if not getattr(self, "wcs", None):
|
|
1107
|
+
QMessageBox.critical(self, "WCS Error", "Could not build a 2D WCS from header.")
|
|
1108
|
+
return
|
|
1109
|
+
|
|
1110
|
+
# Use celestial WCS if possible (safe when WCS has extra axes)
|
|
1111
|
+
wcs2 = self.wcs.celestial if hasattr(self.wcs, "celestial") else self.wcs
|
|
1004
1112
|
|
|
1005
1113
|
H, W = self.current_image.shape[:2]
|
|
1006
|
-
|
|
1114
|
+
|
|
1115
|
+
# --- original radius method (center + 4 corners) ---
|
|
1116
|
+
pix = np.array([[W / 2, H / 2], [0, 0], [W, 0], [0, H], [W, H]], dtype=float)
|
|
1007
1117
|
try:
|
|
1008
|
-
sky =
|
|
1118
|
+
sky = wcs2.all_pix2world(pix, 0)
|
|
1009
1119
|
except Exception as e:
|
|
1010
|
-
QMessageBox.critical(self, "WCS Conversion Error", str(e))
|
|
1011
|
-
|
|
1012
|
-
corners_sky = SkyCoord(ra=sky[1:,0]*u.deg, dec=sky[1:,1]*u.deg, frame="icrs")
|
|
1013
|
-
radius_deg = center_sky.separation(corners_sky).max().deg
|
|
1120
|
+
QMessageBox.critical(self, "WCS Conversion Error", str(e))
|
|
1121
|
+
return
|
|
1014
1122
|
|
|
1015
|
-
|
|
1123
|
+
center_sky = SkyCoord(ra=float(sky[0, 0]) * u.deg, dec=float(sky[0, 1]) * u.deg, frame="icrs")
|
|
1124
|
+
corners_sky = SkyCoord(ra=sky[1:, 0] * u.deg, dec=sky[1:, 1] * u.deg, frame="icrs")
|
|
1125
|
+
radius = center_sky.separation(corners_sky).max() * 1.05 # small margin
|
|
1126
|
+
|
|
1127
|
+
# --- SIMBAD fields (NEW first, fallback to legacy) ---
|
|
1016
1128
|
Simbad.reset_votable_fields()
|
|
1017
|
-
|
|
1129
|
+
|
|
1130
|
+
def _try_new_fields():
|
|
1131
|
+
# new names: B,V,R + ra,dec
|
|
1132
|
+
Simbad.add_votable_fields("sp", "B", "V", "R", "ra", "dec")
|
|
1133
|
+
|
|
1134
|
+
def _try_legacy_fields():
|
|
1135
|
+
# legacy names
|
|
1136
|
+
Simbad.add_votable_fields("sp", "flux(B)", "flux(V)", "flux(R)", "ra(d)", "dec(d)")
|
|
1137
|
+
|
|
1138
|
+
ok = False
|
|
1139
|
+
for _ in range(5):
|
|
1018
1140
|
try:
|
|
1019
|
-
|
|
1141
|
+
_try_new_fields()
|
|
1142
|
+
ok = True
|
|
1020
1143
|
break
|
|
1021
1144
|
except Exception:
|
|
1022
1145
|
QApplication.processEvents()
|
|
1023
|
-
time.sleep(
|
|
1146
|
+
time.sleep(0.8)
|
|
1147
|
+
|
|
1148
|
+
if not ok:
|
|
1149
|
+
for _ in range(5):
|
|
1150
|
+
try:
|
|
1151
|
+
_try_legacy_fields()
|
|
1152
|
+
ok = True
|
|
1153
|
+
break
|
|
1154
|
+
except Exception:
|
|
1155
|
+
QApplication.processEvents()
|
|
1156
|
+
time.sleep(0.8)
|
|
1157
|
+
|
|
1158
|
+
if not ok:
|
|
1159
|
+
QMessageBox.critical(self, "SIMBAD Error", "Could not configure SIMBAD votable fields.")
|
|
1160
|
+
return
|
|
1161
|
+
|
|
1024
1162
|
Simbad.ROW_LIMIT = 10000
|
|
1025
1163
|
|
|
1164
|
+
# --- Query SIMBAD ---
|
|
1165
|
+
result = None
|
|
1026
1166
|
for attempt in range(1, 6):
|
|
1027
1167
|
try:
|
|
1028
|
-
|
|
1168
|
+
if getattr(self, "count_label", None) is not None:
|
|
1169
|
+
self.count_label.setText(f"Attempt {attempt}/5 to query SIMBAD…")
|
|
1170
|
+
QApplication.processEvents()
|
|
1171
|
+
result = Simbad.query_region(center_sky, radius=radius)
|
|
1029
1172
|
break
|
|
1030
|
-
except Exception
|
|
1031
|
-
|
|
1032
|
-
|
|
1173
|
+
except Exception:
|
|
1174
|
+
QApplication.processEvents()
|
|
1175
|
+
time.sleep(1.2)
|
|
1033
1176
|
result = None
|
|
1177
|
+
|
|
1034
1178
|
if result is None or len(result) == 0:
|
|
1035
1179
|
QMessageBox.information(self, "No Stars", "SIMBAD returned zero objects in that region.")
|
|
1036
|
-
self.star_list = []
|
|
1180
|
+
self.star_list = []
|
|
1181
|
+
if getattr(self, "star_combo", None) is not None:
|
|
1182
|
+
self.star_combo.clear()
|
|
1183
|
+
self.star_combo.addItem("Vega (A0V)", userData="A0V")
|
|
1184
|
+
return
|
|
1037
1185
|
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
if bv < 0.00: return "B"
|
|
1041
|
-
elif bv < 0.30: return "A"
|
|
1042
|
-
elif bv < 0.58: return "F"
|
|
1043
|
-
elif bv < 0.81: return "G"
|
|
1044
|
-
elif bv < 1.40: return "K"
|
|
1045
|
-
elif bv > 1.40: return "M"
|
|
1046
|
-
else: return "U"
|
|
1047
|
-
|
|
1048
|
-
self.star_list = []; templates_for_hist = []
|
|
1049
|
-
for row in result:
|
|
1050
|
-
raw_sp = row['sp_type']
|
|
1051
|
-
bmag, vmag, rmag = row['B'], row['V'], row['R']
|
|
1052
|
-
ra_deg, dec_deg = float(row['ra']), float(row['dec'])
|
|
1186
|
+
# --- helpers ---
|
|
1187
|
+
def _unmask_num(x):
|
|
1053
1188
|
try:
|
|
1054
|
-
|
|
1189
|
+
if x is None:
|
|
1190
|
+
return None
|
|
1191
|
+
if ma.isMaskedArray(x) and ma.is_masked(x):
|
|
1192
|
+
return None
|
|
1193
|
+
return float(x)
|
|
1055
1194
|
except Exception:
|
|
1056
|
-
|
|
1195
|
+
return None
|
|
1196
|
+
|
|
1197
|
+
def infer_letter(bv):
|
|
1198
|
+
if bv is None or (isinstance(bv, float) and np.isnan(bv)):
|
|
1199
|
+
return None
|
|
1200
|
+
if bv < 0.00:
|
|
1201
|
+
return "B"
|
|
1202
|
+
elif bv < 0.30:
|
|
1203
|
+
return "A"
|
|
1204
|
+
elif bv < 0.58:
|
|
1205
|
+
return "F"
|
|
1206
|
+
elif bv < 0.81:
|
|
1207
|
+
return "G"
|
|
1208
|
+
elif bv < 1.40:
|
|
1209
|
+
return "K"
|
|
1210
|
+
elif bv > 1.40:
|
|
1211
|
+
return "M"
|
|
1212
|
+
return None
|
|
1057
1213
|
|
|
1058
|
-
|
|
1214
|
+
def safe_world2pix(ra_deg, dec_deg):
|
|
1215
|
+
try:
|
|
1216
|
+
xpix, ypix = wcs2.all_world2pix(ra_deg, dec_deg, 0)
|
|
1217
|
+
xpix, ypix = float(xpix), float(ypix)
|
|
1218
|
+
if np.isfinite(xpix) and np.isfinite(ypix):
|
|
1219
|
+
return xpix, ypix
|
|
1220
|
+
return None
|
|
1221
|
+
except NoConvergence as e:
|
|
1059
1222
|
try:
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1223
|
+
xpix, ypix = e.best_solution
|
|
1224
|
+
xpix, ypix = float(xpix), float(ypix)
|
|
1225
|
+
if np.isfinite(xpix) and np.isfinite(ypix):
|
|
1226
|
+
return xpix, ypix
|
|
1063
1227
|
except Exception:
|
|
1064
|
-
|
|
1228
|
+
pass
|
|
1229
|
+
return None
|
|
1230
|
+
except Exception:
|
|
1231
|
+
return None
|
|
1232
|
+
|
|
1233
|
+
# Column names (astroquery changed these)
|
|
1234
|
+
cols_lower = {c.lower(): c for c in result.colnames}
|
|
1235
|
+
|
|
1236
|
+
# RA/Dec in degrees:
|
|
1237
|
+
ra_col = cols_lower.get("ra", None) or cols_lower.get("ra(d)", None) or cols_lower.get("ra_d", None)
|
|
1238
|
+
dec_col = cols_lower.get("dec", None) or cols_lower.get("dec(d)", None) or cols_lower.get("dec_d", None)
|
|
1239
|
+
|
|
1240
|
+
# Mag columns:
|
|
1241
|
+
b_col = cols_lower.get("b", None) or cols_lower.get("flux_b", None)
|
|
1242
|
+
v_col = cols_lower.get("v", None) or cols_lower.get("flux_v", None)
|
|
1243
|
+
r_col = cols_lower.get("r", None) or cols_lower.get("flux_r", None)
|
|
1244
|
+
|
|
1245
|
+
if ra_col is None or dec_col is None:
|
|
1246
|
+
QMessageBox.critical(
|
|
1247
|
+
self,
|
|
1248
|
+
"SIMBAD Columns",
|
|
1249
|
+
"SIMBAD result did not include degree RA/Dec columns (ra/dec).\n"
|
|
1250
|
+
"Print result.colnames to see what's returned."
|
|
1251
|
+
)
|
|
1252
|
+
return
|
|
1253
|
+
|
|
1254
|
+
# --- main loop ---
|
|
1255
|
+
self.star_list = []
|
|
1256
|
+
templates_for_hist = []
|
|
1065
1257
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1258
|
+
for row in result:
|
|
1259
|
+
# spectral type column name in table
|
|
1260
|
+
raw_sp = None
|
|
1261
|
+
if "SP_TYPE" in result.colnames:
|
|
1262
|
+
raw_sp = row["SP_TYPE"]
|
|
1263
|
+
elif "sp_type" in result.colnames:
|
|
1264
|
+
raw_sp = row["sp_type"]
|
|
1265
|
+
|
|
1266
|
+
bmag = _unmask_num(row[b_col]) if b_col is not None else None
|
|
1267
|
+
vmag = _unmask_num(row[v_col]) if v_col is not None else None
|
|
1268
|
+
rmag = _unmask_num(row[r_col]) if r_col is not None else None
|
|
1269
|
+
|
|
1270
|
+
# ra/dec degrees
|
|
1271
|
+
ra_deg = _unmask_num(row[ra_col])
|
|
1272
|
+
dec_deg = _unmask_num(row[dec_col])
|
|
1273
|
+
if ra_deg is None or dec_deg is None:
|
|
1274
|
+
continue
|
|
1275
|
+
|
|
1276
|
+
try:
|
|
1277
|
+
sc = SkyCoord(ra=ra_deg * u.deg, dec=dec_deg * u.deg, frame="icrs")
|
|
1278
|
+
except Exception:
|
|
1279
|
+
continue
|
|
1069
1280
|
|
|
1070
1281
|
sp_clean = None
|
|
1071
1282
|
if raw_sp and str(raw_sp).strip():
|
|
1072
1283
|
sp = str(raw_sp).strip().upper()
|
|
1073
1284
|
if not (sp.startswith("SN") or sp.startswith("KA")):
|
|
1074
1285
|
sp_clean = sp
|
|
1075
|
-
elif bmag is not None and vmag is not None:
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
if not sp_clean:
|
|
1286
|
+
elif (bmag is not None) and (vmag is not None):
|
|
1287
|
+
sp_clean = infer_letter(bmag - vmag)
|
|
1288
|
+
|
|
1289
|
+
if not sp_clean:
|
|
1290
|
+
continue
|
|
1079
1291
|
|
|
1080
1292
|
match_list = pickles_match_for_simbad(sp_clean, self.pickles_templates)
|
|
1081
1293
|
best_template = match_list[0] if match_list else None
|
|
1082
|
-
|
|
1294
|
+
|
|
1295
|
+
xy = safe_world2pix(sc.ra.deg, sc.dec.deg)
|
|
1296
|
+
if xy is None:
|
|
1297
|
+
continue
|
|
1298
|
+
|
|
1299
|
+
xpix, ypix = xy
|
|
1083
1300
|
if 0 <= xpix < W and 0 <= ypix < H:
|
|
1084
1301
|
self.star_list.append({
|
|
1085
|
-
"ra": sc.ra.deg, "dec": sc.dec.deg,
|
|
1086
|
-
"
|
|
1087
|
-
"
|
|
1088
|
-
"
|
|
1089
|
-
|
|
1302
|
+
"ra": sc.ra.deg, "dec": sc.dec.deg,
|
|
1303
|
+
"sp_clean": sp_clean,
|
|
1304
|
+
"pickles_match": best_template,
|
|
1305
|
+
"x": xpix, "y": ypix,
|
|
1306
|
+
# IMPORTANT: do not use "if bmag" (0.0 becomes None)
|
|
1307
|
+
"Bmag": float(bmag) if bmag is not None else None,
|
|
1308
|
+
"Vmag": float(vmag) if vmag is not None else None,
|
|
1309
|
+
"Rmag": float(rmag) if rmag is not None else None,
|
|
1090
1310
|
})
|
|
1091
|
-
if best_template is not None:
|
|
1311
|
+
if best_template is not None:
|
|
1312
|
+
templates_for_hist.append(best_template)
|
|
1313
|
+
|
|
1314
|
+
# --- plot / UI feedback (unchanged) ---
|
|
1315
|
+
if getattr(self, "figure", None) is not None:
|
|
1316
|
+
self.figure.clf()
|
|
1092
1317
|
|
|
1093
|
-
self.figure.clf()
|
|
1094
1318
|
if templates_for_hist:
|
|
1095
1319
|
uniq, cnt = np.unique(templates_for_hist, return_counts=True)
|
|
1096
|
-
types_str = ", ".join(uniq)
|
|
1097
|
-
self
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1320
|
+
types_str = ", ".join([str(u) for u in uniq])
|
|
1321
|
+
if getattr(self, "count_label", None) is not None:
|
|
1322
|
+
self.count_label.setText(f"Found {len(self.star_list)} stars; templates: {types_str}")
|
|
1323
|
+
|
|
1324
|
+
if getattr(self, "figure", None) is not None and getattr(self, "canvas", None) is not None:
|
|
1325
|
+
ax = self.figure.add_subplot(111)
|
|
1326
|
+
ax.bar(uniq, cnt, edgecolor="black")
|
|
1327
|
+
ax.set_xlabel("Spectral Type")
|
|
1328
|
+
ax.set_ylabel("Count")
|
|
1329
|
+
ax.set_title("Spectral Distribution")
|
|
1330
|
+
ax.tick_params(axis='x', rotation=90)
|
|
1331
|
+
ax.grid(axis="y", linestyle="--", alpha=0.3)
|
|
1332
|
+
self.canvas.setVisible(True)
|
|
1333
|
+
self.canvas.draw()
|
|
1103
1334
|
else:
|
|
1104
|
-
self
|
|
1105
|
-
|
|
1335
|
+
if getattr(self, "count_label", None) is not None:
|
|
1336
|
+
self.count_label.setText(f"Found {len(self.star_list)} in-frame SIMBAD stars (0 with Pickles matches).")
|
|
1337
|
+
if getattr(self, "canvas", None) is not None:
|
|
1338
|
+
self.canvas.setVisible(False)
|
|
1339
|
+
self.canvas.draw()
|
|
1106
1340
|
|
|
1107
1341
|
# ── Core SFCC ───────────────────────────────────────────────────────
|
|
1108
1342
|
def run_spcc(self):
|
setiastro/saspro/shortcuts.py
CHANGED
|
@@ -503,7 +503,6 @@ class DraggableToolBar(QToolBar):
|
|
|
503
503
|
|
|
504
504
|
m.exec(gpos)
|
|
505
505
|
|
|
506
|
-
|
|
507
506
|
def contextMenuEvent(self, ev):
|
|
508
507
|
# Right-click on empty toolbar area
|
|
509
508
|
m = QMenu(self)
|
|
@@ -513,12 +512,12 @@ class DraggableToolBar(QToolBar):
|
|
|
513
512
|
act_lock = m.addAction(self.tr("Lock Toolbar Icons"))
|
|
514
513
|
act_lock.setCheckable(True)
|
|
515
514
|
act_lock.setChecked(is_locked)
|
|
516
|
-
|
|
515
|
+
|
|
517
516
|
def _toggle_lock(checked):
|
|
518
517
|
self._set_locked(checked)
|
|
519
|
-
|
|
518
|
+
|
|
520
519
|
act_lock.triggered.connect(_toggle_lock)
|
|
521
|
-
|
|
520
|
+
|
|
522
521
|
m.addSeparator()
|
|
523
522
|
|
|
524
523
|
# Submenu listing hidden actions for this toolbar
|
|
@@ -529,12 +528,13 @@ class DraggableToolBar(QToolBar):
|
|
|
529
528
|
any_hidden = False
|
|
530
529
|
if tb_hidden:
|
|
531
530
|
for act in tb_hidden.actions():
|
|
532
|
-
# Skip separators
|
|
533
531
|
if act.isSeparator():
|
|
534
532
|
continue
|
|
535
533
|
any_hidden = True
|
|
536
|
-
sub.addAction(
|
|
537
|
-
|
|
534
|
+
sub.addAction(
|
|
535
|
+
act.text() or (act.property("command_id") or act.objectName() or "item"),
|
|
536
|
+
lambda a=act: mw._unhide_action_from_hidden_toolbar(a)
|
|
537
|
+
)
|
|
538
538
|
|
|
539
539
|
if not any_hidden:
|
|
540
540
|
sub.setEnabled(False)
|
|
@@ -542,8 +542,15 @@ class DraggableToolBar(QToolBar):
|
|
|
542
542
|
m.addSeparator()
|
|
543
543
|
m.addAction(self.tr("Reset hidden icons"), self._reset_hidden_icons)
|
|
544
544
|
|
|
545
|
+
# ✅ NEW: Factory reset for all toolbars
|
|
546
|
+
m.addSeparator()
|
|
547
|
+
reset_all = m.addAction(self.tr("Reset ALL Toolbars (Factory Layout)"))
|
|
548
|
+
reset_all.setStatusTip(self.tr("Clears saved toolbar positions/orders/hidden state and restores defaults"))
|
|
549
|
+
reset_all.triggered.connect(lambda: getattr(self.window(), "_reset_all_toolbars_to_factory", lambda: None)())
|
|
550
|
+
|
|
545
551
|
m.exec(ev.globalPos())
|
|
546
552
|
|
|
553
|
+
|
|
547
554
|
def _reset_hidden_icons(self):
|
|
548
555
|
mw = self.window()
|
|
549
556
|
tb_hidden = getattr(mw, "_hidden_toolbar", lambda: None)()
|
|
@@ -4324,8 +4324,6 @@ def _arr_stats(a: np.ndarray):
|
|
|
4324
4324
|
max=float(v.max()),
|
|
4325
4325
|
p01=float(np.percentile(v, 1)),
|
|
4326
4326
|
p50=float(np.percentile(v, 50)),
|
|
4327
|
-
p99=float(np.percentile(v, 99)),
|
|
4328
|
-
mean=float(v.mean()),
|
|
4329
4327
|
)
|
|
4330
4328
|
return dict(dtype=str(a.dtype), shape=tuple(a.shape), finite=0, nan=int(np.isnan(a).sum()), inf=int(np.isinf(a).sum()))
|
|
4331
4329
|
|
|
@@ -4334,7 +4332,7 @@ def _print_stats(tag: str, a: np.ndarray, *, bit_depth=None, hdr=None):
|
|
|
4334
4332
|
bd = f", bit_depth={bit_depth}" if bit_depth is not None else ""
|
|
4335
4333
|
print(f"🧪 {tag}{bd} dtype={s['dtype']} shape={s['shape']} finite={s['finite']} nan={s['nan']} inf={s['inf']}")
|
|
4336
4334
|
if s["finite"] > 0:
|
|
4337
|
-
print(f" min={s['min']:.6f} p01={s['p01']:.6f} p50={s['p50']:.6f}
|
|
4335
|
+
print(f" min={s['min']:.6f} p01={s['p01']:.6f} p50={s['p50']:.6f} max={s['max']:.6f}")
|
|
4338
4336
|
# Header hints (best-effort)
|
|
4339
4337
|
if hdr is not None:
|
|
4340
4338
|
try:
|
|
@@ -5583,6 +5581,20 @@ class StackingSuiteDialog(QDialog):
|
|
|
5583
5581
|
|
|
5584
5582
|
left_col.addWidget(gb_general)
|
|
5585
5583
|
|
|
5584
|
+
self.temp_group_step_spin = QDoubleSpinBox()
|
|
5585
|
+
self.temp_group_step_spin.setRange(0.0, 20.0) # 0 disables grouping-by-temp (optional behavior)
|
|
5586
|
+
self.temp_group_step_spin.setDecimals(2)
|
|
5587
|
+
self.temp_group_step_spin.setSingleStep(0.1)
|
|
5588
|
+
self.temp_group_step_spin.setValue(
|
|
5589
|
+
self.settings.value("stacking/temp_group_step", 1.0, type=float)
|
|
5590
|
+
)
|
|
5591
|
+
self.temp_group_step_spin.setToolTip(
|
|
5592
|
+
self.tr("Temperature grouping tolerance in °C.\n"
|
|
5593
|
+
"Frames within ±step are grouped together.\n"
|
|
5594
|
+
"Set 0 to disable temperature-based grouping.")
|
|
5595
|
+
)
|
|
5596
|
+
fl_general.addRow(self.tr("Temp grouping step (°C):"), self.temp_group_step_spin)
|
|
5597
|
+
|
|
5586
5598
|
# --- Distortion / Transform model ---
|
|
5587
5599
|
# --- Distortion / Transform model ---
|
|
5588
5600
|
disto_box = QGroupBox(self.tr("Distortion / Transform"))
|
|
@@ -6360,7 +6372,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
6360
6372
|
self.settings.setValue("stacking/chunk_width", self.chunk_width)
|
|
6361
6373
|
self.settings.setValue("stacking/autocrop_enabled", self.autocrop_cb.isChecked())
|
|
6362
6374
|
self.settings.setValue("stacking/autocrop_pct", float(self.autocrop_pct.value()))
|
|
6363
|
-
|
|
6375
|
+
self.temp_group_step = float(self.temp_group_step_spin.value())
|
|
6376
|
+
self.settings.setValue("stacking/temp_group_step", self.temp_group_step)
|
|
6364
6377
|
# ----- alignment model (affine | homography | poly3 | poly4) -----
|
|
6365
6378
|
model_idx = self.align_model_combo.currentIndex()
|
|
6366
6379
|
if model_idx == 0: model_name = "affine"
|
|
@@ -10828,7 +10841,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
10828
10841
|
set_t = _get_key_float(header, "SET-TEMP")
|
|
10829
10842
|
chosen_t = ccd_t if ccd_t is not None else set_t
|
|
10830
10843
|
|
|
10831
|
-
temp_step = self.settings.value("stacking/temp_group_step", 1.0, type=float)
|
|
10844
|
+
temp_step = float(self.settings.value("stacking/temp_group_step", 1.0, type=float) or 1.0)
|
|
10845
|
+
temp_step = max(0.0, temp_step)
|
|
10832
10846
|
temp_bucket = self._bucket_temp(chosen_t, step=temp_step)
|
|
10833
10847
|
temp_label = self._temp_label(temp_bucket, step=temp_step)
|
|
10834
10848
|
|
|
@@ -11057,7 +11071,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
11057
11071
|
# Group darks by (exposure +/- tolerance, image size, session, temp_bucket)
|
|
11058
11072
|
# TEMP_STEP is the rounding bucket (1.0C default)
|
|
11059
11073
|
# -------------------------------------------------------------------------
|
|
11060
|
-
TEMP_STEP = self.settings.value("stacking/temp_group_step", 1.0, type=float)
|
|
11074
|
+
TEMP_STEP = float(self.settings.value("stacking/temp_group_step", 1.0, type=float) or 1.0)
|
|
11075
|
+
TEMP_STEP = max(0.0, TEMP_STEP)
|
|
11061
11076
|
|
|
11062
11077
|
dark_files_by_group: dict[tuple[float, str, str, float | None], list[str]] = {} # (exp,size,session,temp)->list
|
|
11063
11078
|
|