setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__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/TextureClarity.svg +56 -0
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +364 -33
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +150 -55
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +181 -64
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +245 -15
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +706 -264
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +184 -8
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +68 -48
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +203 -82
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +81 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +73 -0
- setiastro/saspro/rgbalign.py +460 -12
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -0
- setiastro/saspro/sfcc.py +662 -216
- setiastro/saspro/shortcuts.py +171 -33
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1347 -485
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +892 -129
- setiastro/saspro/subwindow.py +787 -363
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +209 -111
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +4 -2
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/doc_manager.py
CHANGED
|
@@ -310,18 +310,133 @@ class ImageDocument(QObject):
|
|
|
310
310
|
|
|
311
311
|
def close(self):
|
|
312
312
|
"""
|
|
313
|
-
|
|
313
|
+
Free all resources held by this document:
|
|
314
|
+
- delete swap states for undo/redo
|
|
315
|
+
- clear undo/redo stacks
|
|
316
|
+
- drop in-memory image array and any in-memory history
|
|
317
|
+
- clear heavy metadata (headers/WCS)
|
|
318
|
+
- clear any cached previews/pixmaps
|
|
319
|
+
- disconnect signals to help break reference cycles
|
|
314
320
|
"""
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
#
|
|
322
|
-
|
|
323
|
-
sm
|
|
324
|
-
|
|
321
|
+
# --- 0) Stop emitting while we tear down (best-effort) --------------
|
|
322
|
+
try:
|
|
323
|
+
self.blockSignals(True)
|
|
324
|
+
except Exception:
|
|
325
|
+
pass
|
|
326
|
+
|
|
327
|
+
# --- 1) Swap cleanup (your existing logic) --------------------------
|
|
328
|
+
try:
|
|
329
|
+
sm = get_swap_manager()
|
|
330
|
+
except Exception:
|
|
331
|
+
sm = None
|
|
332
|
+
|
|
333
|
+
# Undo stack
|
|
334
|
+
try:
|
|
335
|
+
for item in list(getattr(self, "_undo", [])):
|
|
336
|
+
try:
|
|
337
|
+
swap_id = item[0] # (swap_id, ..., ...)
|
|
338
|
+
except Exception:
|
|
339
|
+
swap_id = None
|
|
340
|
+
if sm is not None and swap_id is not None:
|
|
341
|
+
try:
|
|
342
|
+
sm.delete_state(swap_id)
|
|
343
|
+
except Exception:
|
|
344
|
+
pass
|
|
345
|
+
getattr(self, "_undo", []).clear()
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
# Redo stack
|
|
350
|
+
try:
|
|
351
|
+
for item in list(getattr(self, "_redo", [])):
|
|
352
|
+
try:
|
|
353
|
+
swap_id = item[0]
|
|
354
|
+
except Exception:
|
|
355
|
+
swap_id = None
|
|
356
|
+
if sm is not None and swap_id is not None:
|
|
357
|
+
try:
|
|
358
|
+
sm.delete_state(swap_id)
|
|
359
|
+
except Exception:
|
|
360
|
+
pass
|
|
361
|
+
getattr(self, "_redo", []).clear()
|
|
362
|
+
except Exception:
|
|
363
|
+
pass
|
|
364
|
+
|
|
365
|
+
# ROI preview stacks if you have them (your code uses _pundo/_predo on ROI docs)
|
|
366
|
+
for attr in ("_pundo", "_predo"):
|
|
367
|
+
try:
|
|
368
|
+
lst = getattr(self, attr, None)
|
|
369
|
+
if isinstance(lst, list):
|
|
370
|
+
# If these also store swap states, delete them too (safe even if not)
|
|
371
|
+
if sm is not None:
|
|
372
|
+
for item in list(lst):
|
|
373
|
+
try:
|
|
374
|
+
swap_id = item[0]
|
|
375
|
+
except Exception:
|
|
376
|
+
swap_id = None
|
|
377
|
+
if swap_id is not None:
|
|
378
|
+
try:
|
|
379
|
+
sm.delete_state(swap_id)
|
|
380
|
+
except Exception:
|
|
381
|
+
pass
|
|
382
|
+
lst.clear()
|
|
383
|
+
except Exception:
|
|
384
|
+
pass
|
|
385
|
+
|
|
386
|
+
# --- 2) Drop the big in-memory image --------------------------------
|
|
387
|
+
# This is what actually frees the 2–10GB allocations (assuming no other refs).
|
|
388
|
+
try:
|
|
389
|
+
self.image = None
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
|
|
393
|
+
# --- 3) Clear metadata that can keep large objects alive -------------
|
|
394
|
+
# fits.Header/WCS objects aren't huge like the image, but they can keep references
|
|
395
|
+
# and add up; also helps break cycles.
|
|
396
|
+
try:
|
|
397
|
+
md = getattr(self, "metadata", None)
|
|
398
|
+
if isinstance(md, dict):
|
|
399
|
+
for k in ("wcs", "original_header", "fits_header", "wcs_header", "header"):
|
|
400
|
+
md.pop(k, None)
|
|
401
|
+
# If you keep derived/cached headers anywhere:
|
|
402
|
+
for k in ("_header_snapshot", "_wcs_snapshot", "roi_wcs_header"):
|
|
403
|
+
md.pop(k, None)
|
|
404
|
+
except Exception:
|
|
405
|
+
pass
|
|
406
|
+
|
|
407
|
+
# --- 4) Clear any preview/pixmap/qimage caches -----------------------
|
|
408
|
+
# Adjust these attr names to match what your view/doc uses.
|
|
409
|
+
for attr in ("_qimage_cache", "_pixmap_cache", "_preview_cache", "_render_cache"):
|
|
410
|
+
try:
|
|
411
|
+
v = getattr(self, attr, None)
|
|
412
|
+
if isinstance(v, dict):
|
|
413
|
+
v.clear()
|
|
414
|
+
setattr(self, attr, None)
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
417
|
+
|
|
418
|
+
# --- 5) Disconnect signals (helps Qt reference cycles) ---------------
|
|
419
|
+
# If you connect doc.changed to closures (like ROI docs do), this helps.
|
|
420
|
+
try:
|
|
421
|
+
self.changed.disconnect()
|
|
422
|
+
except Exception:
|
|
423
|
+
pass
|
|
424
|
+
|
|
425
|
+
# If you have other signals, disconnect them similarly:
|
|
426
|
+
for sig_name in ("imageChanged", "metadataChanged"):
|
|
427
|
+
try:
|
|
428
|
+
sig = getattr(self, sig_name, None)
|
|
429
|
+
if sig is not None:
|
|
430
|
+
sig.disconnect()
|
|
431
|
+
except Exception:
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
# --- 6) Allow signals again (optional) -------------------------------
|
|
435
|
+
try:
|
|
436
|
+
self.blockSignals(False)
|
|
437
|
+
except Exception:
|
|
438
|
+
pass
|
|
439
|
+
|
|
325
440
|
|
|
326
441
|
def __del__(self):
|
|
327
442
|
# Fallback cleanup if close() wasn't called (though explicit close is better)
|
|
@@ -1555,6 +1670,9 @@ def debug_dump_metadata_print(meta: dict, context: str = ""):
|
|
|
1555
1670
|
|
|
1556
1671
|
print("===== END METADATA DUMP ({}) =====".format(context))
|
|
1557
1672
|
|
|
1673
|
+
import time
|
|
1674
|
+
_DEBUG_DND_DUP = False
|
|
1675
|
+
|
|
1558
1676
|
class DocManager(QObject):
|
|
1559
1677
|
documentAdded = pyqtSignal(object) # ImageDocument
|
|
1560
1678
|
documentRemoved = pyqtSignal(object) # ImageDocument
|
|
@@ -2266,6 +2384,46 @@ class DocManager(QObject):
|
|
|
2266
2384
|
if hasattr(doc, "changed"):
|
|
2267
2385
|
doc.changed.emit()
|
|
2268
2386
|
|
|
2387
|
+
def _current_view_title_for_doc(self, source_doc: ImageDocument) -> str | None:
|
|
2388
|
+
"""
|
|
2389
|
+
If the active MDI subwindow is showing 'source_doc' (or its parent/base),
|
|
2390
|
+
return the current view's title (windowTitle), otherwise None.
|
|
2391
|
+
"""
|
|
2392
|
+
sw = self._active_subwindow()
|
|
2393
|
+
if sw is None:
|
|
2394
|
+
return None
|
|
2395
|
+
|
|
2396
|
+
try:
|
|
2397
|
+
w = sw.widget()
|
|
2398
|
+
except Exception:
|
|
2399
|
+
w = None
|
|
2400
|
+
|
|
2401
|
+
# Resolve what doc the active view corresponds to (base doc)
|
|
2402
|
+
try:
|
|
2403
|
+
base = (
|
|
2404
|
+
getattr(w, "base_document", None)
|
|
2405
|
+
or getattr(w, "_base_document", None)
|
|
2406
|
+
or getattr(w, "document", None)
|
|
2407
|
+
or getattr(sw, "document", None)
|
|
2408
|
+
)
|
|
2409
|
+
parent = getattr(base, "_parent_doc", None)
|
|
2410
|
+
if isinstance(parent, ImageDocument):
|
|
2411
|
+
base = parent
|
|
2412
|
+
except Exception:
|
|
2413
|
+
base = None
|
|
2414
|
+
|
|
2415
|
+
if base is not source_doc:
|
|
2416
|
+
return None
|
|
2417
|
+
|
|
2418
|
+
# Prefer the actual subwindow title (includes [View N], etc.)
|
|
2419
|
+
try:
|
|
2420
|
+
title = sw.windowTitle()
|
|
2421
|
+
title = title.strip() if isinstance(title, str) else ""
|
|
2422
|
+
return title or None
|
|
2423
|
+
except Exception:
|
|
2424
|
+
return None
|
|
2425
|
+
|
|
2426
|
+
|
|
2269
2427
|
def duplicate_document(self, source_doc: ImageDocument, new_name: str | None = None) -> ImageDocument:
|
|
2270
2428
|
# DEBUG: log the source doc WCS before we touch anything
|
|
2271
2429
|
if _DEBUG_WCS:
|
|
@@ -2275,19 +2433,33 @@ class DocManager(QObject):
|
|
|
2275
2433
|
name = "<src>"
|
|
2276
2434
|
|
|
2277
2435
|
_debug_log_wcs_context(" source.metadata", getattr(source_doc, "metadata", {}))
|
|
2278
|
-
|
|
2436
|
+
if _DEBUG_DND_DUP:
|
|
2437
|
+
try:
|
|
2438
|
+
src_dn = source_doc.display_name() if hasattr(source_doc, "display_name") else None
|
|
2439
|
+
except Exception:
|
|
2440
|
+
src_dn = None
|
|
2441
|
+
print("\n[DNDDBG:DUPLICATE_DOCUMENT]")
|
|
2442
|
+
print(" source_doc:", source_doc, "id:", id(source_doc), "uid:", getattr(source_doc,"uid",None))
|
|
2443
|
+
print(" source_doc.display_name():", src_dn)
|
|
2444
|
+
print(" new_name arg:", new_name)
|
|
2279
2445
|
# COPY-ON-WRITE: Share the source image instead of copying immediately.
|
|
2280
2446
|
# The duplicate's apply_edit will copy when it first modifies the image.
|
|
2281
2447
|
# This saves memory when duplicates are created but not modified.
|
|
2282
2448
|
img_ref = source_doc.image # Shared reference, no copy
|
|
2283
2449
|
|
|
2284
2450
|
meta = dict(source_doc.metadata or {})
|
|
2285
|
-
|
|
2451
|
+
|
|
2452
|
+
# ✅ Use CURRENT VIEW NAME if this doc is what's active; else fall back to doc display_name()
|
|
2453
|
+
base = self._current_view_title_for_doc(source_doc) or source_doc.display_name()
|
|
2454
|
+
|
|
2286
2455
|
dup_title = (new_name or f"{base}_duplicate")
|
|
2456
|
+
|
|
2287
2457
|
# 🚫 strip any lingering emojis / link markers
|
|
2288
2458
|
dup_title = dup_title.replace("🔗", "").strip()
|
|
2289
|
-
meta["display_name"] = dup_title
|
|
2290
2459
|
|
|
2460
|
+
meta["display_name"] = dup_title
|
|
2461
|
+
if _DEBUG_DND_DUP:
|
|
2462
|
+
print(" dup_title computed:", dup_title)
|
|
2291
2463
|
# Remove anything that makes the view look "linked/preview"
|
|
2292
2464
|
imi = dict(meta.get("image_meta") or {})
|
|
2293
2465
|
for k in ("readonly", "view_kind", "derived_from", "layer", "layer_index", "linked"):
|
|
@@ -2311,7 +2483,13 @@ class DocManager(QObject):
|
|
|
2311
2483
|
# Mark this duplicate as sharing image data with source
|
|
2312
2484
|
dup._cow_source = source_doc
|
|
2313
2485
|
self._register_doc(dup)
|
|
2314
|
-
|
|
2486
|
+
if _DEBUG_DND_DUP:
|
|
2487
|
+
try:
|
|
2488
|
+
dn = dup.display_name() if hasattr(dup, "display_name") else None
|
|
2489
|
+
except Exception:
|
|
2490
|
+
dn = None
|
|
2491
|
+
print(" dup.metadata.display_name:", (dup.metadata or {}).get("display_name"))
|
|
2492
|
+
print(" dup.display_name():", dn)
|
|
2315
2493
|
# DEBUG: log the duplicate doc WCS
|
|
2316
2494
|
if _DEBUG_WCS:
|
|
2317
2495
|
try:
|
|
@@ -2370,7 +2548,58 @@ class DocManager(QObject):
|
|
|
2370
2548
|
def create_document(self, image, metadata: dict | None = None, name: str | None = None) -> ImageDocument:
|
|
2371
2549
|
return self.open_array(image, metadata=metadata, title=name)
|
|
2372
2550
|
|
|
2551
|
+
def _drop_all_roi_for_parent(self, parent_doc):
|
|
2552
|
+
dead = [k for k in list(self._roi_doc_cache.keys()) if k[0] == id(parent_doc)]
|
|
2553
|
+
for k in dead:
|
|
2554
|
+
roi_doc = self._roi_doc_cache.pop(k, None)
|
|
2555
|
+
if roi_doc is not None:
|
|
2556
|
+
try:
|
|
2557
|
+
roi_doc.close() # you’ll implement doc.close() to release arrays
|
|
2558
|
+
except Exception:
|
|
2559
|
+
pass
|
|
2560
|
+
|
|
2561
|
+
def _hard_memory_cleanup(self):
|
|
2562
|
+
# 1) Drop Qt pixmap cache (can hold big chunks)
|
|
2563
|
+
try:
|
|
2564
|
+
from PyQt6.QtGui import QPixmapCache
|
|
2565
|
+
QPixmapCache.clear()
|
|
2566
|
+
except Exception:
|
|
2567
|
+
pass
|
|
2568
|
+
|
|
2569
|
+
# 2) Let pending deleteLater() actually execute
|
|
2570
|
+
try:
|
|
2571
|
+
from PyQt6.QtWidgets import QApplication
|
|
2572
|
+
QApplication.processEvents()
|
|
2573
|
+
except Exception:
|
|
2574
|
+
pass
|
|
2575
|
+
|
|
2576
|
+
# 3) Force Python GC to collect cycles (common with Qt signal/closure cycles)
|
|
2577
|
+
try:
|
|
2578
|
+
import gc
|
|
2579
|
+
gc.collect()
|
|
2580
|
+
except Exception:
|
|
2581
|
+
pass
|
|
2582
|
+
|
|
2583
|
+
# 4) Optional: Linux heap trim (only helps on Linux/glibc)
|
|
2584
|
+
try:
|
|
2585
|
+
import sys
|
|
2586
|
+
if sys.platform.startswith("linux"):
|
|
2587
|
+
import ctypes
|
|
2588
|
+
libc = ctypes.CDLL("libc.so.6")
|
|
2589
|
+
libc.malloc_trim(0)
|
|
2590
|
+
except Exception:
|
|
2591
|
+
pass
|
|
2592
|
+
|
|
2373
2593
|
def close_document(self, doc):
|
|
2594
|
+
# If ROI wrapper, close parent; if parent, purge ROI cache
|
|
2595
|
+
try:
|
|
2596
|
+
parent = getattr(doc, "_parent_doc", None)
|
|
2597
|
+
if parent is not None:
|
|
2598
|
+
doc = parent
|
|
2599
|
+
except Exception:
|
|
2600
|
+
pass
|
|
2601
|
+
|
|
2602
|
+
self._drop_all_roi_for_parent(doc)
|
|
2374
2603
|
if doc in self._docs:
|
|
2375
2604
|
self._docs.remove(doc)
|
|
2376
2605
|
try:
|
|
@@ -2387,6 +2616,7 @@ class DocManager(QObject):
|
|
|
2387
2616
|
print(f"[DocManager] Failed to close document {doc}: {e}")
|
|
2388
2617
|
|
|
2389
2618
|
self.documentRemoved.emit(doc)
|
|
2619
|
+
self._hard_memory_cleanup()
|
|
2390
2620
|
|
|
2391
2621
|
# --- Active-document helpers (NEW) ---------------------------------
|
|
2392
2622
|
def all_documents(self):
|
|
@@ -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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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
|
-
|
|
772
|
-
for _, t in datelist
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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(
|
|
788
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
-
|
|
893
|
-
for _, t in datelist
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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()
|