setiastrosuitepro 1.6.7__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.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/add_stars.py +29 -5
- setiastro/saspro/blink_comparator_pro.py +74 -24
- setiastro/saspro/cosmicclarity.py +125 -18
- setiastro/saspro/crop_dialog_pro.py +96 -2
- setiastro/saspro/curve_editor_pro.py +60 -39
- setiastro/saspro/frequency_separation.py +1159 -208
- setiastro/saspro/gui/main_window.py +131 -31
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/update_mixin.py +121 -33
- setiastro/saspro/imageops/stretch.py +531 -62
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/numba_utils.py +43 -0
- setiastro/saspro/live_stacking.py +158 -70
- setiastro/saspro/multiscale_decomp.py +47 -12
- setiastro/saspro/numba_utils.py +72 -2
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/shortcuts.py +122 -12
- setiastro/saspro/signature_insert.py +688 -33
- setiastro/saspro/stacking_suite.py +523 -316
- setiastro/saspro/stat_stretch.py +688 -130
- setiastro/saspro/subwindow.py +302 -71
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +7 -7
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/RECORD +37 -31
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.7.dist-info → setiastrosuitepro-1.6.10.dist-info}/licenses/license.txt +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg width="300" height="300" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Sfondo sfumato -->
|
|
3
|
+
<defs>
|
|
4
|
+
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
5
|
+
<stop offset="0%" style="stop-color:white;stop-opacity:1" />
|
|
6
|
+
<stop offset="40%" style="stop-color:#cccccc;stop-opacity:1" />
|
|
7
|
+
<stop offset="100%" style="stop-color:#4a0000;stop-opacity:1" />
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<rect width="300" height="300" fill="url(#bgGradient)" />
|
|
11
|
+
|
|
12
|
+
<!-- Lettere -->
|
|
13
|
+
<text x="20" y="110" font-family="Arial, sans-serif" font-weight="bold" font-size="140" fill="#ff0000">A</text>
|
|
14
|
+
<text x="100" y="220" font-family="Arial, sans-serif" font-weight="bold" font-size="140" fill="#00ff00">B</text>
|
|
15
|
+
<text x="185" y="295" font-family="Arial, sans-serif" font-weight="bold" font-size="140" fill="#0000ff">E</text>
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<svg width="400" height="400" viewBox="0 0 400 400" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Sfondo Tondo Blu Scuro -->
|
|
3
|
+
<circle cx="200" cy="200" r="190" fill="#2C3278" />
|
|
4
|
+
|
|
5
|
+
<!-- Scia/Onda inferiore (Swoosh) -->
|
|
6
|
+
<!-- Swoosh grande viola scuro -->
|
|
7
|
+
<path d="M80,260
|
|
8
|
+
Q110,360 200,340
|
|
9
|
+
Q280,320 320,220
|
|
10
|
+
L335,230
|
|
11
|
+
Q300,340 200,360
|
|
12
|
+
Q100,380 70,280 Z"
|
|
13
|
+
fill="#534FB1" opacity="0.9" />
|
|
14
|
+
|
|
15
|
+
<!-- Swoosh piccola interna (più chiara) -->
|
|
16
|
+
<path d="M120,290 Q160,330 220,310"
|
|
17
|
+
fill="none" stroke="#686CCF" stroke-width="12" stroke-linecap="round" />
|
|
18
|
+
|
|
19
|
+
<!-- Stella Principale Bianca -->
|
|
20
|
+
<!-- Uso curve di Bezier per ottenere l'effetto "pizzicato" esatto -->
|
|
21
|
+
<path d="M200,50
|
|
22
|
+
C215,160 215,160 350,200
|
|
23
|
+
C215,240 215,240 200,350
|
|
24
|
+
C185,240 185,240 50,200
|
|
25
|
+
C185,160 185,160 200,50 Z"
|
|
26
|
+
fill="#FFFFFF" />
|
|
27
|
+
|
|
28
|
+
<!-- Elementi decorativi -->
|
|
29
|
+
|
|
30
|
+
<!-- Stella piccola a destra (inclinata) -->
|
|
31
|
+
<path d="M280,110 C290,105 290,105 300,100 C290,95 290,95 280,90 C270,95 270,95 260,100 C270,105 270,105 280,110 Z"
|
|
32
|
+
fill="#FFFFFF" transform="rotate(-15, 280, 100)" />
|
|
33
|
+
|
|
34
|
+
<!-- Pallini a sinistra -->
|
|
35
|
+
<!-- Pallino bianco in alto -->
|
|
36
|
+
<circle cx="140" cy="85" r="5" fill="#FFFFFF" />
|
|
37
|
+
<!-- Pallino blu scuro in basso -->
|
|
38
|
+
<circle cx="90" cy="140" r="7" fill="#4C54A0" />
|
|
39
|
+
|
|
40
|
+
</svg>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<svg width="300" height="300" viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Sfondo quadrato arrotondato -->
|
|
3
|
+
<rect x="20" y="20" width="260" height="260" rx="40" fill="#293375" />
|
|
4
|
+
|
|
5
|
+
<!-- Silhouette Satellite -->
|
|
6
|
+
<g transform="translate(150,150) rotate(-45)">
|
|
7
|
+
<!-- Pannelli solari -->
|
|
8
|
+
<rect x="-90" y="-30" width="60" height="60" fill="#0c1236" />
|
|
9
|
+
<rect x="30" y="-30" width="60" height="60" fill="#0c1236" />
|
|
10
|
+
<!-- Corpo centrale -->
|
|
11
|
+
<rect x="-30" y="-50" width="60" height="100" rx="10" fill="#0c1236" />
|
|
12
|
+
<!-- Antenna -->
|
|
13
|
+
<line x1="0" y1="-50" x2="0" y2="-100" stroke="#f0f0f0" stroke-width="10" stroke-linecap="round"/>
|
|
14
|
+
<!-- Raggio inferiore -->
|
|
15
|
+
<path d="M-10,50 L-20,150 L20,150 L10,50 Z" fill="#f0f0f0" opacity="0.9"/>
|
|
16
|
+
</g>
|
|
17
|
+
|
|
18
|
+
<!-- Segnale di divieto (X rossa) -->
|
|
19
|
+
<circle cx="230" cy="230" r="60" fill="#e84545" />
|
|
20
|
+
<g stroke="white" stroke-width="15" stroke-linecap="round">
|
|
21
|
+
<line x1="210" y1="210" x2="250" y2="250" />
|
|
22
|
+
<line x1="250" y1="210" x2="210" y2="250" />
|
|
23
|
+
</g>
|
|
24
|
+
</svg>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<svg width="300" height="240" viewBox="0 0 300 240" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Sfondo gradiente radiale -->
|
|
3
|
+
<defs>
|
|
4
|
+
<radialGradient id="gradGX" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
|
|
5
|
+
<stop offset="0%" style="stop-color:#f0f0f0;stop-opacity:1" />
|
|
6
|
+
<stop offset="70%" style="stop-color:#d0d0d0;stop-opacity:1" />
|
|
7
|
+
<stop offset="100%" style="stop-color:#ffffff;stop-opacity:0" />
|
|
8
|
+
</radialGradient>
|
|
9
|
+
<filter id="blurEffect">
|
|
10
|
+
<feGaussianBlur in="SourceGraphic" stdDeviation="5" />
|
|
11
|
+
</filter>
|
|
12
|
+
</defs>
|
|
13
|
+
|
|
14
|
+
<!-- Ellisse di sfondo sfocata -->
|
|
15
|
+
<ellipse cx="150" cy="120" rx="130" ry="100" fill="url(#gradGX)" filter="url(#blurEffect)" />
|
|
16
|
+
|
|
17
|
+
<!-- Testo GX -->
|
|
18
|
+
<text x="150" y="180" font-family="Arial, sans-serif" font-weight="bold" font-size="160" text-anchor="middle" fill="black">GX</text>
|
|
19
|
+
</svg>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<svg width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<!-- Bordo quadrato arrotondato -->
|
|
3
|
+
<rect x="20" y="20" width="460" height="460" rx="80" fill="white" stroke="black" stroke-width="25" />
|
|
4
|
+
|
|
5
|
+
<!-- Assi Cartesiani -->
|
|
6
|
+
<g stroke="black" stroke-width="25" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
|
7
|
+
<!-- Asse Y e X -->
|
|
8
|
+
<path d="M80,100 L80,420 L420,420" />
|
|
9
|
+
<!-- Frecce -->
|
|
10
|
+
<path d="M50,130 L80,90 L110,130" /> <!-- Freccia Y -->
|
|
11
|
+
<path d="M390,390 L430,420 L390,450" /> <!-- Freccia X -->
|
|
12
|
+
</g>
|
|
13
|
+
|
|
14
|
+
<!-- Punti Dati (Scatter Plot) -->
|
|
15
|
+
<g fill="#ff3333">
|
|
16
|
+
<circle cx="160" cy="220" r="20" />
|
|
17
|
+
<circle cx="200" cy="230" r="20" />
|
|
18
|
+
<circle cx="160" cy="260" r="20" />
|
|
19
|
+
<circle cx="220" cy="230" r="20" />
|
|
20
|
+
<circle cx="270" cy="300" r="20" />
|
|
21
|
+
<circle cx="320" cy="300" r="20" />
|
|
22
|
+
<circle cx="240" cy="350" r="20" />
|
|
23
|
+
<circle cx="330" cy="260" r="20" />
|
|
24
|
+
<circle cx="360" cy="220" r="20" />
|
|
25
|
+
<circle cx="300" cy="150" r="20" />
|
|
26
|
+
<circle cx="370" cy="80" r="20" />
|
|
27
|
+
<circle cx="450" cy="120" r="20" />
|
|
28
|
+
</g>
|
|
29
|
+
|
|
30
|
+
<!-- Linea di Regressione (Best Fit) -->
|
|
31
|
+
<line x1="130" y1="370" x2="420" y2="100" stroke="#2d72b5" stroke-width="30" stroke-linecap="round" />
|
|
32
|
+
</svg>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<svg width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
|
|
3
|
+
<!-- Quadrato Alto Sinistra (Giallo / Più) -->
|
|
4
|
+
<g transform="translate(10, 10)">
|
|
5
|
+
<rect width="230" height="230" fill="#fec035" />
|
|
6
|
+
<!-- Effetto ombra diagonale -->
|
|
7
|
+
<path d="M230,0 L230,230 L115,230 Z" fill="#fab005" opacity="0.4" />
|
|
8
|
+
<!-- Simbolo + -->
|
|
9
|
+
<rect x="95" y="45" width="40" height="140" rx="10" fill="white" />
|
|
10
|
+
<rect x="45" y="95" width="140" height="40" rx="10" fill="white" />
|
|
11
|
+
</g>
|
|
12
|
+
|
|
13
|
+
<!-- Quadrato Alto Destra (Rosso / Meno) -->
|
|
14
|
+
<g transform="translate(260, 10)">
|
|
15
|
+
<rect width="230" height="230" fill="#f15654" />
|
|
16
|
+
<path d="M230,0 L230,230 L115,230 Z" fill="#d04442" opacity="0.4" />
|
|
17
|
+
<!-- Simbolo - -->
|
|
18
|
+
<rect x="45" y="95" width="140" height="30" rx="10" fill="white" />
|
|
19
|
+
</g>
|
|
20
|
+
|
|
21
|
+
<!-- Quadrato Basso Sinistra (Verde / Per) -->
|
|
22
|
+
<g transform="translate(10, 260)">
|
|
23
|
+
<rect width="230" height="230" fill="#5cba63" />
|
|
24
|
+
<path d="M230,0 L230,230 L115,230 Z" fill="#4fa555" opacity="0.4" />
|
|
25
|
+
<!-- Simbolo X (Ruotato +) -->
|
|
26
|
+
<g transform="translate(115, 115) rotate(45)">
|
|
27
|
+
<rect x="-20" y="-70" width="40" height="140" rx="10" fill="white" />
|
|
28
|
+
<rect x="-70" y="-20" width="140" height="40" rx="10" fill="white" />
|
|
29
|
+
</g>
|
|
30
|
+
</g>
|
|
31
|
+
|
|
32
|
+
<!-- Quadrato Basso Destra (Blu / Diviso) -->
|
|
33
|
+
<g transform="translate(260, 260)">
|
|
34
|
+
<rect width="230" height="230" fill="#427bc9" />
|
|
35
|
+
<path d="M230,0 L230,230 L115,230 Z" fill="#3664a3" opacity="0.4" />
|
|
36
|
+
<!-- Simbolo Diviso -->
|
|
37
|
+
<rect x="45" y="95" width="140" height="30" rx="10" fill="white" />
|
|
38
|
+
<circle cx="115" cy="55" r="15" fill="white" />
|
|
39
|
+
<circle cx="115" cy="175" r="15" fill="white" />
|
|
40
|
+
</g>
|
|
41
|
+
|
|
42
|
+
</svg>
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Auto-generated at build time. Do not edit.
|
|
2
|
-
BUILD_TIMESTAMP = "
|
|
3
|
-
APP_VERSION = "1.6.
|
|
2
|
+
BUILD_TIMESTAMP = "2026-01-04T16:49:01Z"
|
|
3
|
+
APP_VERSION = "1.6.10"
|
setiastro/saspro/add_stars.py
CHANGED
|
@@ -578,10 +578,9 @@ class AddStarsDialog(QDialog):
|
|
|
578
578
|
|
|
579
579
|
# Emit (target_doc, blended_image)
|
|
580
580
|
self.stars_added.emit(target_doc, self.blended_image.astype(np.float32, copy=False))
|
|
581
|
-
#
|
|
582
|
-
#
|
|
583
|
-
|
|
584
|
-
|
|
581
|
+
# Close UI after apply
|
|
582
|
+
self.accept() # or: self.close()
|
|
583
|
+
return
|
|
585
584
|
|
|
586
585
|
# Ensure initial fit once shown
|
|
587
586
|
def showEvent(self, ev):
|
|
@@ -606,7 +605,32 @@ def add_stars(main):
|
|
|
606
605
|
|
|
607
606
|
dlg = AddStarsDialog(main, parent=main)
|
|
608
607
|
dlg.stars_added.connect(lambda target, arr: _apply_to_doc(main, target, arr))
|
|
609
|
-
|
|
608
|
+
|
|
609
|
+
# IMPORTANT: keep a strong reference (non-modal show)
|
|
610
|
+
if not hasattr(main, "_tool_dialogs"):
|
|
611
|
+
main._tool_dialogs = []
|
|
612
|
+
main._tool_dialogs.append(dlg)
|
|
613
|
+
|
|
614
|
+
# When the dialog closes, drop the reference
|
|
615
|
+
def _cleanup(_=None, d=dlg):
|
|
616
|
+
try:
|
|
617
|
+
if hasattr(main, "_tool_dialogs") and d in main._tool_dialogs:
|
|
618
|
+
main._tool_dialogs.remove(d)
|
|
619
|
+
except Exception:
|
|
620
|
+
pass
|
|
621
|
+
|
|
622
|
+
try:
|
|
623
|
+
dlg.finished.connect(_cleanup) # QDialog signal
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
try:
|
|
627
|
+
dlg.destroyed.connect(_cleanup) # QObject signal (extra safety)
|
|
628
|
+
except Exception:
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
dlg.show()
|
|
632
|
+
dlg.raise_()
|
|
633
|
+
dlg.activateWindow()
|
|
610
634
|
|
|
611
635
|
|
|
612
636
|
def _apply_to_doc(main, doc, arr: np.ndarray):
|
|
@@ -150,45 +150,75 @@ class MetricsPanel(QWidget):
|
|
|
150
150
|
orig_back = entry.get('orig_background', np.nan)
|
|
151
151
|
return idx, fwhm, ecc, orig_back, star_cnt
|
|
152
152
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
153
|
+
def compute_all_metrics(self, loaded_images) -> bool:
|
|
154
|
+
"""Run SEP over the full list in parallel using threads and cache results.
|
|
155
|
+
Returns True if metrics were computed, False if user declined/canceled.
|
|
156
|
+
"""
|
|
156
157
|
n = len(loaded_images)
|
|
157
158
|
if n == 0:
|
|
158
|
-
# Clear any previous state and bail
|
|
159
159
|
self._orig_images = []
|
|
160
|
-
self.metrics_data = [np.array([])]*4
|
|
160
|
+
self.metrics_data = [np.array([])] * 4
|
|
161
161
|
self.flags = []
|
|
162
|
-
self._threshold_initialized = [False]*4
|
|
163
|
-
return
|
|
162
|
+
self._threshold_initialized = [False] * 4
|
|
163
|
+
return True
|
|
164
|
+
|
|
165
|
+
def _has_metrics(md):
|
|
166
|
+
try:
|
|
167
|
+
return md is not None and len(md) == 4 and md[0] is not None and len(md[0]) > 0
|
|
168
|
+
except Exception:
|
|
169
|
+
return False
|
|
164
170
|
|
|
165
|
-
# Heads-up dialog (as you already had)
|
|
166
171
|
settings = QSettings()
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
show_warning = settings.value("metrics/showWarning", True, type=bool)
|
|
173
|
+
|
|
174
|
+
if (not show_warning) and (not _has_metrics(getattr(self, "metrics_data", None))):
|
|
175
|
+
settings.setValue("metrics/showWarning", True)
|
|
176
|
+
show_warning = True
|
|
177
|
+
|
|
178
|
+
# ----------------------------
|
|
179
|
+
# 1) Optional warning gate
|
|
180
|
+
# ----------------------------
|
|
181
|
+
if show_warning:
|
|
169
182
|
msg = QMessageBox(self)
|
|
170
183
|
msg.setWindowTitle(self.tr("Heads-up"))
|
|
171
184
|
msg.setText(self.tr(
|
|
172
185
|
"This is going to use ALL your CPU cores and the UI may lock up until it finishes.\n\n"
|
|
173
186
|
"Continue?"
|
|
174
187
|
))
|
|
175
|
-
msg.setStandardButtons(
|
|
176
|
-
|
|
188
|
+
msg.setStandardButtons(
|
|
189
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
190
|
+
)
|
|
177
191
|
cb = QCheckBox(self.tr("Don't show again"), msg)
|
|
178
192
|
msg.setCheckBox(cb)
|
|
179
|
-
|
|
180
|
-
|
|
193
|
+
|
|
194
|
+
clicked = msg.exec()
|
|
195
|
+
clicked_yes = (clicked == QMessageBox.StandardButton.Yes)
|
|
196
|
+
|
|
197
|
+
if not clicked_yes:
|
|
198
|
+
# If they said NO, never allow "Don't show again" to lock them out.
|
|
199
|
+
# Keep the warning enabled so they can opt-in later.
|
|
200
|
+
if cb.isChecked():
|
|
201
|
+
settings.setValue("metrics/showWarning", True)
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
# They said YES: now it's safe to honor "Don't show again"
|
|
181
205
|
if cb.isChecked():
|
|
182
206
|
settings.setValue("metrics/showWarning", False)
|
|
183
207
|
|
|
184
|
-
#
|
|
208
|
+
# If show_warning is False, we compute with no prompt.
|
|
209
|
+
|
|
210
|
+
# ----------------------------
|
|
211
|
+
# 2) Allocate result arrays
|
|
212
|
+
# ----------------------------
|
|
185
213
|
m0 = np.full(n, np.nan, dtype=np.float32) # FWHM
|
|
186
214
|
m1 = np.full(n, np.nan, dtype=np.float32) # Eccentricity
|
|
187
215
|
m2 = np.full(n, np.nan, dtype=np.float32) # Background (cached)
|
|
188
216
|
m3 = np.full(n, np.nan, dtype=np.float32) # Star count
|
|
189
217
|
flags = [e.get('flagged', False) for e in loaded_images]
|
|
190
218
|
|
|
191
|
-
#
|
|
219
|
+
# ----------------------------
|
|
220
|
+
# 3) Progress dialog
|
|
221
|
+
# ----------------------------
|
|
192
222
|
prog = QProgressDialog(self.tr("Computing frame metrics…"), self.tr("Cancel"), 0, n, self)
|
|
193
223
|
prog.setWindowModality(Qt.WindowModality.WindowModal)
|
|
194
224
|
prog.setMinimumDuration(0)
|
|
@@ -198,32 +228,43 @@ class MetricsPanel(QWidget):
|
|
|
198
228
|
|
|
199
229
|
workers = min(os.cpu_count() or 1, 60)
|
|
200
230
|
tasks = [(i, loaded_images[i]) for i in range(n)]
|
|
201
|
-
done = 0
|
|
231
|
+
done = 0
|
|
232
|
+
canceled = False
|
|
202
233
|
|
|
203
234
|
try:
|
|
204
235
|
with ThreadPoolExecutor(max_workers=workers) as exe:
|
|
205
236
|
futures = {exe.submit(self._compute_one, t): t[0] for t in tasks}
|
|
206
237
|
for fut in as_completed(futures):
|
|
207
238
|
if prog.wasCanceled():
|
|
239
|
+
canceled = True
|
|
208
240
|
break
|
|
209
241
|
try:
|
|
210
242
|
idx, fwhm, ecc, orig_back, star_cnt = fut.result()
|
|
211
243
|
except Exception:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
244
|
+
idx = futures.get(fut, 0)
|
|
245
|
+
fwhm, ecc, orig_back, star_cnt = np.nan, np.nan, np.nan, 0
|
|
246
|
+
|
|
247
|
+
if 0 <= idx < n:
|
|
248
|
+
m0[idx], m1[idx], m2[idx], m3[idx] = fwhm, ecc, orig_back, float(star_cnt)
|
|
249
|
+
|
|
215
250
|
done += 1
|
|
216
251
|
prog.setValue(done)
|
|
217
252
|
QApplication.processEvents()
|
|
218
253
|
finally:
|
|
219
254
|
prog.close()
|
|
220
255
|
|
|
221
|
-
|
|
256
|
+
if canceled:
|
|
257
|
+
# IMPORTANT: leave caches alone; caller will clear/return
|
|
258
|
+
return False
|
|
259
|
+
|
|
260
|
+
# ----------------------------
|
|
261
|
+
# 4) Stash results
|
|
262
|
+
# ----------------------------
|
|
222
263
|
self._orig_images = loaded_images
|
|
223
264
|
self.metrics_data = [m0, m1, m2, m3]
|
|
224
265
|
self.flags = flags
|
|
225
|
-
self._threshold_initialized = [False]*4
|
|
226
|
-
|
|
266
|
+
self._threshold_initialized = [False] * 4
|
|
267
|
+
return True
|
|
227
268
|
|
|
228
269
|
def plot(self, loaded_images, indices=None):
|
|
229
270
|
"""
|
|
@@ -242,7 +283,16 @@ class MetricsPanel(QWidget):
|
|
|
242
283
|
|
|
243
284
|
# compute & cache on first call or new image list
|
|
244
285
|
if self._orig_images is not loaded_images or self.metrics_data is None:
|
|
245
|
-
self.compute_all_metrics(loaded_images)
|
|
286
|
+
ok = self.compute_all_metrics(loaded_images)
|
|
287
|
+
if not ok or self.metrics_data is None:
|
|
288
|
+
# user declined/canceled -> clear plots and exit cleanly
|
|
289
|
+
for pw, scat, line in zip(self.plots, self.scats, self.lines):
|
|
290
|
+
scat.setData(x=[], y=[])
|
|
291
|
+
line.setPos(0)
|
|
292
|
+
pw.getPlotItem().getViewBox().update()
|
|
293
|
+
pw.repaint()
|
|
294
|
+
return
|
|
295
|
+
|
|
246
296
|
|
|
247
297
|
# default to all indices
|
|
248
298
|
if indices is None:
|
|
@@ -228,21 +228,116 @@ class WaitForFileWorker(QThread):
|
|
|
228
228
|
fileFound = pyqtSignal(str)
|
|
229
229
|
cancelled = pyqtSignal()
|
|
230
230
|
error = pyqtSignal(str)
|
|
231
|
-
|
|
231
|
+
|
|
232
|
+
def __init__(
|
|
233
|
+
self,
|
|
234
|
+
glob_pat: str,
|
|
235
|
+
timeout_sec: int = 1800,
|
|
236
|
+
parent=None,
|
|
237
|
+
*,
|
|
238
|
+
poll_ms: int = 200,
|
|
239
|
+
stable_polls: int = 6, # 6 * 200ms = ~1.2s of stability
|
|
240
|
+
stable_timeout_sec: int = 120, # extra time after first detection
|
|
241
|
+
):
|
|
232
242
|
super().__init__(parent)
|
|
233
243
|
self._glob = glob_pat
|
|
234
|
-
self._timeout = timeout_sec
|
|
244
|
+
self._timeout = int(timeout_sec)
|
|
245
|
+
self._poll_ms = int(poll_ms)
|
|
246
|
+
self._stable_polls = int(stable_polls)
|
|
247
|
+
self._stable_timeout = int(stable_timeout_sec)
|
|
235
248
|
self._running = True
|
|
249
|
+
|
|
250
|
+
def stop(self):
|
|
251
|
+
self._running = False
|
|
252
|
+
|
|
253
|
+
def _best_candidate(self, paths: list[str]) -> str | None:
|
|
254
|
+
if not paths:
|
|
255
|
+
return None
|
|
256
|
+
# prefer biggest file; tie-break by newest mtime
|
|
257
|
+
def key(p):
|
|
258
|
+
try:
|
|
259
|
+
st = os.stat(p)
|
|
260
|
+
return (st.st_size, st.st_mtime)
|
|
261
|
+
except Exception:
|
|
262
|
+
return (-1, -1)
|
|
263
|
+
paths.sort(key=key, reverse=True)
|
|
264
|
+
return paths[0]
|
|
265
|
+
|
|
266
|
+
def _is_stable_and_readable(self, path: str) -> bool:
|
|
267
|
+
"""
|
|
268
|
+
Consider stable when size+mtime unchanged for N polls in a row AND file is readable.
|
|
269
|
+
Handles slow writers + Windows "file still locked" issues.
|
|
270
|
+
"""
|
|
271
|
+
stable = 0
|
|
272
|
+
last = None
|
|
273
|
+
|
|
274
|
+
t0 = time.monotonic()
|
|
275
|
+
while self._running and (time.monotonic() - t0) < self._stable_timeout:
|
|
276
|
+
try:
|
|
277
|
+
st = os.stat(path)
|
|
278
|
+
cur = (st.st_size, st.st_mtime)
|
|
279
|
+
if st.st_size <= 0:
|
|
280
|
+
stable = 0
|
|
281
|
+
last = cur
|
|
282
|
+
elif cur == last:
|
|
283
|
+
stable += 1
|
|
284
|
+
else:
|
|
285
|
+
stable = 0
|
|
286
|
+
last = cur
|
|
287
|
+
|
|
288
|
+
if stable >= self._stable_polls:
|
|
289
|
+
# extra “is it readable?” check (important on Windows)
|
|
290
|
+
try:
|
|
291
|
+
with open(path, "rb") as f:
|
|
292
|
+
f.read(64)
|
|
293
|
+
return True
|
|
294
|
+
except PermissionError:
|
|
295
|
+
# still locked by writer, keep waiting
|
|
296
|
+
stable = 0
|
|
297
|
+
except Exception:
|
|
298
|
+
# transient weirdness: keep waiting, don’t declare failure yet
|
|
299
|
+
stable = 0
|
|
300
|
+
|
|
301
|
+
except FileNotFoundError:
|
|
302
|
+
stable = 0
|
|
303
|
+
last = None
|
|
304
|
+
except Exception:
|
|
305
|
+
# don't crash the worker for stat weirdness
|
|
306
|
+
stable = 0
|
|
307
|
+
|
|
308
|
+
time.sleep(self._poll_ms / 1000.0)
|
|
309
|
+
|
|
310
|
+
return False
|
|
311
|
+
|
|
236
312
|
def run(self):
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
313
|
+
t_start = time.monotonic()
|
|
314
|
+
seen_first_candidate_at = None
|
|
315
|
+
|
|
316
|
+
while self._running and (time.monotonic() - t_start) < self._timeout:
|
|
317
|
+
matches = glob.glob(self._glob)
|
|
318
|
+
cand = self._best_candidate(matches)
|
|
319
|
+
|
|
320
|
+
if cand:
|
|
321
|
+
if seen_first_candidate_at is None:
|
|
322
|
+
seen_first_candidate_at = time.monotonic()
|
|
323
|
+
|
|
324
|
+
if self._is_stable_and_readable(cand):
|
|
325
|
+
self.fileFound.emit(cand)
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
# If we've been seeing candidates for a while but none stabilize,
|
|
329
|
+
# keep looping until global timeout. (This is common on slow disks.)
|
|
330
|
+
|
|
331
|
+
time.sleep(self._poll_ms / 1000.0)
|
|
332
|
+
|
|
333
|
+
if not self._running:
|
|
334
|
+
self.cancelled.emit()
|
|
335
|
+
else:
|
|
336
|
+
extra = ""
|
|
337
|
+
if seen_first_candidate_at is not None:
|
|
338
|
+
extra = " (output appeared but never stabilized)"
|
|
339
|
+
self.error.emit("Output file not found within timeout." + extra)
|
|
340
|
+
|
|
246
341
|
|
|
247
342
|
|
|
248
343
|
# =============================================================================
|
|
@@ -556,6 +651,12 @@ class CosmicClarityDialogPro(QDialog):
|
|
|
556
651
|
QMessageBox.critical(self, "Cosmic Clarity", f"Executable not found:\n{exe_path}")
|
|
557
652
|
return
|
|
558
653
|
|
|
654
|
+
# ✅ compute base early (we need it for purge + glob)
|
|
655
|
+
base = self._base_name()
|
|
656
|
+
|
|
657
|
+
# ✅ purge any stale outputs for THIS base name (avoids matching old files)
|
|
658
|
+
_purge_cc_io(self.cosmic_root, clear_input=False, clear_output=True, prefix=base)
|
|
659
|
+
|
|
559
660
|
# Build args (SASv2 flags mirrored)
|
|
560
661
|
args = []
|
|
561
662
|
if mode == "sharpen":
|
|
@@ -599,7 +700,7 @@ class CosmicClarityDialogPro(QDialog):
|
|
|
599
700
|
|
|
600
701
|
# Wait for output file
|
|
601
702
|
base = self._base_name()
|
|
602
|
-
out_glob = os.path.join(self.cosmic_root, "output", f"{base}{suffix}
|
|
703
|
+
out_glob = os.path.join(self.cosmic_root, "output", f"{base}*{suffix}*.*")
|
|
603
704
|
self._wait = WaitDialog(f"Cosmic Clarity – {mode.title()}", self)
|
|
604
705
|
self._wait.cancelled.connect(self._cancel_all)
|
|
605
706
|
self._wait.show()
|
|
@@ -615,19 +716,25 @@ class CosmicClarityDialogPro(QDialog):
|
|
|
615
716
|
|
|
616
717
|
def _read_proc_output(self, proc: QProcess, which="main"):
|
|
617
718
|
out = proc.readAllStandardOutput().data().decode("utf-8", errors="replace")
|
|
618
|
-
if not self._wait:
|
|
719
|
+
if not self._wait:
|
|
720
|
+
return
|
|
721
|
+
|
|
619
722
|
for line in out.splitlines():
|
|
620
723
|
line = line.strip()
|
|
621
|
-
if not line:
|
|
724
|
+
if not line:
|
|
725
|
+
continue
|
|
726
|
+
|
|
622
727
|
if line.startswith("Progress:"):
|
|
623
728
|
try:
|
|
624
|
-
pct = float(line.split()[1].replace("%",""))
|
|
729
|
+
pct = float(line.split()[1].replace("%", ""))
|
|
625
730
|
self._wait.set_progress(int(pct))
|
|
626
731
|
except Exception:
|
|
627
732
|
pass
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
733
|
+
continue # <- skip echo
|
|
734
|
+
|
|
735
|
+
# non-progress lines: keep showing + printing
|
|
736
|
+
self._wait.append_output(line)
|
|
737
|
+
print(f"[CC] {line}")
|
|
631
738
|
|
|
632
739
|
def _on_proc_finished(self, mode, suffix, code, status):
|
|
633
740
|
if code != 0:
|