setiastrosuitepro 1.6.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (394) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/data/SASP_data.fits +0 -0
  3. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  4. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  5. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  6. setiastro/data/catalogs/cali2.csv +63 -0
  7. setiastro/data/catalogs/cali2color.csv +65 -0
  8. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  9. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  10. setiastro/data/catalogs/detected_stars.csv +24784 -0
  11. setiastro/data/catalogs/fits_header_data.csv +46 -0
  12. setiastro/data/catalogs/test.csv +8 -0
  13. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  14. setiastro/images/Astro_Spikes.png +0 -0
  15. setiastro/images/Background_startup.jpg +0 -0
  16. setiastro/images/HRDiagram.png +0 -0
  17. setiastro/images/LExtract.png +0 -0
  18. setiastro/images/LInsert.png +0 -0
  19. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  20. setiastro/images/RGB080604.png +0 -0
  21. setiastro/images/abeicon.png +0 -0
  22. setiastro/images/aberration.png +0 -0
  23. setiastro/images/acv_icon.png +0 -0
  24. setiastro/images/andromedatry.png +0 -0
  25. setiastro/images/andromedatry_satellited.png +0 -0
  26. setiastro/images/annotated.png +0 -0
  27. setiastro/images/aperture.png +0 -0
  28. setiastro/images/astrosuite.ico +0 -0
  29. setiastro/images/astrosuite.png +0 -0
  30. setiastro/images/astrosuitepro.icns +0 -0
  31. setiastro/images/astrosuitepro.ico +0 -0
  32. setiastro/images/astrosuitepro.png +0 -0
  33. setiastro/images/background.png +0 -0
  34. setiastro/images/background2.png +0 -0
  35. setiastro/images/benchmark.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  37. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  38. setiastro/images/blaster.png +0 -0
  39. setiastro/images/blink.png +0 -0
  40. setiastro/images/clahe.png +0 -0
  41. setiastro/images/collage.png +0 -0
  42. setiastro/images/colorwheel.png +0 -0
  43. setiastro/images/contsub.png +0 -0
  44. setiastro/images/convo.png +0 -0
  45. setiastro/images/copyslot.png +0 -0
  46. setiastro/images/cosmic.png +0 -0
  47. setiastro/images/cosmicsat.png +0 -0
  48. setiastro/images/crop1.png +0 -0
  49. setiastro/images/cropicon.png +0 -0
  50. setiastro/images/curves.png +0 -0
  51. setiastro/images/cvs.png +0 -0
  52. setiastro/images/debayer.png +0 -0
  53. setiastro/images/denoise_cnn_custom.png +0 -0
  54. setiastro/images/denoise_cnn_graph.png +0 -0
  55. setiastro/images/disk.png +0 -0
  56. setiastro/images/dse.png +0 -0
  57. setiastro/images/exoicon.png +0 -0
  58. setiastro/images/eye.png +0 -0
  59. setiastro/images/first_quarter.png +0 -0
  60. setiastro/images/fliphorizontal.png +0 -0
  61. setiastro/images/flipvertical.png +0 -0
  62. setiastro/images/font.png +0 -0
  63. setiastro/images/freqsep.png +0 -0
  64. setiastro/images/full_moon.png +0 -0
  65. setiastro/images/functionbundle.png +0 -0
  66. setiastro/images/graxpert.png +0 -0
  67. setiastro/images/green.png +0 -0
  68. setiastro/images/gridicon.png +0 -0
  69. setiastro/images/halo.png +0 -0
  70. setiastro/images/hdr.png +0 -0
  71. setiastro/images/histogram.png +0 -0
  72. setiastro/images/hubble.png +0 -0
  73. setiastro/images/imagecombine.png +0 -0
  74. setiastro/images/invert.png +0 -0
  75. setiastro/images/isophote.png +0 -0
  76. setiastro/images/isophote_demo_figure.png +0 -0
  77. setiastro/images/isophote_demo_image.png +0 -0
  78. setiastro/images/isophote_demo_model.png +0 -0
  79. setiastro/images/isophote_demo_residual.png +0 -0
  80. setiastro/images/jwstpupil.png +0 -0
  81. setiastro/images/last_quarter.png +0 -0
  82. setiastro/images/linearfit.png +0 -0
  83. setiastro/images/livestacking.png +0 -0
  84. setiastro/images/mask.png +0 -0
  85. setiastro/images/maskapply.png +0 -0
  86. setiastro/images/maskcreate.png +0 -0
  87. setiastro/images/maskremove.png +0 -0
  88. setiastro/images/morpho.png +0 -0
  89. setiastro/images/mosaic.png +0 -0
  90. setiastro/images/multiscale_decomp.png +0 -0
  91. setiastro/images/nbtorgb.png +0 -0
  92. setiastro/images/neutral.png +0 -0
  93. setiastro/images/new_moon.png +0 -0
  94. setiastro/images/nuke.png +0 -0
  95. setiastro/images/openfile.png +0 -0
  96. setiastro/images/pedestal.png +0 -0
  97. setiastro/images/pen.png +0 -0
  98. setiastro/images/pixelmath.png +0 -0
  99. setiastro/images/platesolve.png +0 -0
  100. setiastro/images/ppp.png +0 -0
  101. setiastro/images/pro.png +0 -0
  102. setiastro/images/project.png +0 -0
  103. setiastro/images/psf.png +0 -0
  104. setiastro/images/redo.png +0 -0
  105. setiastro/images/redoicon.png +0 -0
  106. setiastro/images/rescale.png +0 -0
  107. setiastro/images/rgbalign.png +0 -0
  108. setiastro/images/rgbcombo.png +0 -0
  109. setiastro/images/rgbextract.png +0 -0
  110. setiastro/images/rotate180.png +0 -0
  111. setiastro/images/rotatearbitrary.png +0 -0
  112. setiastro/images/rotateclockwise.png +0 -0
  113. setiastro/images/rotatecounterclockwise.png +0 -0
  114. setiastro/images/satellite.png +0 -0
  115. setiastro/images/script.png +0 -0
  116. setiastro/images/selectivecolor.png +0 -0
  117. setiastro/images/simbad.png +0 -0
  118. setiastro/images/slot0.png +0 -0
  119. setiastro/images/slot1.png +0 -0
  120. setiastro/images/slot2.png +0 -0
  121. setiastro/images/slot3.png +0 -0
  122. setiastro/images/slot4.png +0 -0
  123. setiastro/images/slot5.png +0 -0
  124. setiastro/images/slot6.png +0 -0
  125. setiastro/images/slot7.png +0 -0
  126. setiastro/images/slot8.png +0 -0
  127. setiastro/images/slot9.png +0 -0
  128. setiastro/images/spcc.png +0 -0
  129. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  130. setiastro/images/spinner.gif +0 -0
  131. setiastro/images/stacking.png +0 -0
  132. setiastro/images/staradd.png +0 -0
  133. setiastro/images/staralign.png +0 -0
  134. setiastro/images/starnet.png +0 -0
  135. setiastro/images/starregistration.png +0 -0
  136. setiastro/images/starspike.png +0 -0
  137. setiastro/images/starstretch.png +0 -0
  138. setiastro/images/statstretch.png +0 -0
  139. setiastro/images/supernova.png +0 -0
  140. setiastro/images/uhs.png +0 -0
  141. setiastro/images/undoicon.png +0 -0
  142. setiastro/images/upscale.png +0 -0
  143. setiastro/images/viewbundle.png +0 -0
  144. setiastro/images/waning_crescent_1.png +0 -0
  145. setiastro/images/waning_crescent_2.png +0 -0
  146. setiastro/images/waning_crescent_3.png +0 -0
  147. setiastro/images/waning_crescent_4.png +0 -0
  148. setiastro/images/waning_crescent_5.png +0 -0
  149. setiastro/images/waning_gibbous_1.png +0 -0
  150. setiastro/images/waning_gibbous_2.png +0 -0
  151. setiastro/images/waning_gibbous_3.png +0 -0
  152. setiastro/images/waning_gibbous_4.png +0 -0
  153. setiastro/images/waning_gibbous_5.png +0 -0
  154. setiastro/images/waxing_crescent_1.png +0 -0
  155. setiastro/images/waxing_crescent_2.png +0 -0
  156. setiastro/images/waxing_crescent_3.png +0 -0
  157. setiastro/images/waxing_crescent_4.png +0 -0
  158. setiastro/images/waxing_crescent_5.png +0 -0
  159. setiastro/images/waxing_gibbous_1.png +0 -0
  160. setiastro/images/waxing_gibbous_2.png +0 -0
  161. setiastro/images/waxing_gibbous_3.png +0 -0
  162. setiastro/images/waxing_gibbous_4.png +0 -0
  163. setiastro/images/waxing_gibbous_5.png +0 -0
  164. setiastro/images/whitebalance.png +0 -0
  165. setiastro/images/wimi_icon_256x256.png +0 -0
  166. setiastro/images/wimilogo.png +0 -0
  167. setiastro/images/wims.png +0 -0
  168. setiastro/images/wrench_icon.png +0 -0
  169. setiastro/images/xisfliberator.png +0 -0
  170. setiastro/qml/ResourceMonitor.qml +128 -0
  171. setiastro/saspro/__init__.py +20 -0
  172. setiastro/saspro/__main__.py +964 -0
  173. setiastro/saspro/_generated/__init__.py +7 -0
  174. setiastro/saspro/_generated/build_info.py +3 -0
  175. setiastro/saspro/abe.py +1379 -0
  176. setiastro/saspro/abe_preset.py +196 -0
  177. setiastro/saspro/aberration_ai.py +910 -0
  178. setiastro/saspro/aberration_ai_preset.py +224 -0
  179. setiastro/saspro/accel_installer.py +218 -0
  180. setiastro/saspro/accel_workers.py +30 -0
  181. setiastro/saspro/acv_exporter.py +379 -0
  182. setiastro/saspro/add_stars.py +627 -0
  183. setiastro/saspro/astrobin_exporter.py +1010 -0
  184. setiastro/saspro/astrospike.py +153 -0
  185. setiastro/saspro/astrospike_python.py +1841 -0
  186. setiastro/saspro/autostretch.py +198 -0
  187. setiastro/saspro/backgroundneutral.py +639 -0
  188. setiastro/saspro/batch_convert.py +328 -0
  189. setiastro/saspro/batch_renamer.py +522 -0
  190. setiastro/saspro/blemish_blaster.py +494 -0
  191. setiastro/saspro/blink_comparator_pro.py +3149 -0
  192. setiastro/saspro/bundles.py +61 -0
  193. setiastro/saspro/bundles_dock.py +114 -0
  194. setiastro/saspro/cheat_sheet.py +213 -0
  195. setiastro/saspro/clahe.py +371 -0
  196. setiastro/saspro/comet_stacking.py +1442 -0
  197. setiastro/saspro/common_tr.py +107 -0
  198. setiastro/saspro/config.py +38 -0
  199. setiastro/saspro/config_bootstrap.py +40 -0
  200. setiastro/saspro/config_manager.py +316 -0
  201. setiastro/saspro/continuum_subtract.py +1620 -0
  202. setiastro/saspro/convo.py +1403 -0
  203. setiastro/saspro/convo_preset.py +414 -0
  204. setiastro/saspro/copyastro.py +190 -0
  205. setiastro/saspro/cosmicclarity.py +1593 -0
  206. setiastro/saspro/cosmicclarity_preset.py +407 -0
  207. setiastro/saspro/crop_dialog_pro.py +1005 -0
  208. setiastro/saspro/crop_preset.py +189 -0
  209. setiastro/saspro/curve_editor_pro.py +2608 -0
  210. setiastro/saspro/curves_preset.py +375 -0
  211. setiastro/saspro/debayer.py +673 -0
  212. setiastro/saspro/debug_utils.py +29 -0
  213. setiastro/saspro/dnd_mime.py +35 -0
  214. setiastro/saspro/doc_manager.py +2727 -0
  215. setiastro/saspro/exoplanet_detector.py +2258 -0
  216. setiastro/saspro/file_utils.py +284 -0
  217. setiastro/saspro/fitsmodifier.py +748 -0
  218. setiastro/saspro/fix_bom.py +32 -0
  219. setiastro/saspro/free_torch_memory.py +48 -0
  220. setiastro/saspro/frequency_separation.py +1352 -0
  221. setiastro/saspro/function_bundle.py +1596 -0
  222. setiastro/saspro/generate_translations.py +3092 -0
  223. setiastro/saspro/ghs_dialog_pro.py +728 -0
  224. setiastro/saspro/ghs_preset.py +284 -0
  225. setiastro/saspro/graxpert.py +638 -0
  226. setiastro/saspro/graxpert_preset.py +287 -0
  227. setiastro/saspro/gui/__init__.py +0 -0
  228. setiastro/saspro/gui/main_window.py +8928 -0
  229. setiastro/saspro/gui/mixins/__init__.py +33 -0
  230. setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
  231. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  232. setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
  233. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  234. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  235. setiastro/saspro/gui/mixins/menu_mixin.py +391 -0
  236. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  237. setiastro/saspro/gui/mixins/toolbar_mixin.py +1824 -0
  238. setiastro/saspro/gui/mixins/update_mixin.py +323 -0
  239. setiastro/saspro/gui/mixins/view_mixin.py +477 -0
  240. setiastro/saspro/gui/statistics_dialog.py +47 -0
  241. setiastro/saspro/halobgon.py +492 -0
  242. setiastro/saspro/header_viewer.py +448 -0
  243. setiastro/saspro/headless_utils.py +88 -0
  244. setiastro/saspro/histogram.py +760 -0
  245. setiastro/saspro/history_explorer.py +941 -0
  246. setiastro/saspro/i18n.py +168 -0
  247. setiastro/saspro/image_combine.py +421 -0
  248. setiastro/saspro/image_peeker_pro.py +1608 -0
  249. setiastro/saspro/imageops/__init__.py +37 -0
  250. setiastro/saspro/imageops/mdi_snap.py +292 -0
  251. setiastro/saspro/imageops/scnr.py +36 -0
  252. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  253. setiastro/saspro/imageops/stretch.py +236 -0
  254. setiastro/saspro/isophote.py +1186 -0
  255. setiastro/saspro/layers.py +208 -0
  256. setiastro/saspro/layers_dock.py +714 -0
  257. setiastro/saspro/lazy_imports.py +193 -0
  258. setiastro/saspro/legacy/__init__.py +2 -0
  259. setiastro/saspro/legacy/image_manager.py +2360 -0
  260. setiastro/saspro/legacy/numba_utils.py +3676 -0
  261. setiastro/saspro/legacy/xisf.py +1213 -0
  262. setiastro/saspro/linear_fit.py +537 -0
  263. setiastro/saspro/live_stacking.py +1854 -0
  264. setiastro/saspro/log_bus.py +5 -0
  265. setiastro/saspro/logging_config.py +460 -0
  266. setiastro/saspro/luminancerecombine.py +510 -0
  267. setiastro/saspro/main_helpers.py +201 -0
  268. setiastro/saspro/mask_creation.py +1090 -0
  269. setiastro/saspro/masks_core.py +56 -0
  270. setiastro/saspro/mdi_widgets.py +353 -0
  271. setiastro/saspro/memory_utils.py +666 -0
  272. setiastro/saspro/metadata_patcher.py +75 -0
  273. setiastro/saspro/mfdeconv.py +3909 -0
  274. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  275. setiastro/saspro/mfdeconvcudnn.py +3312 -0
  276. setiastro/saspro/mfdeconvsport.py +2459 -0
  277. setiastro/saspro/minorbodycatalog.py +567 -0
  278. setiastro/saspro/morphology.py +411 -0
  279. setiastro/saspro/multiscale_decomp.py +1751 -0
  280. setiastro/saspro/nbtorgb_stars.py +541 -0
  281. setiastro/saspro/numba_utils.py +3145 -0
  282. setiastro/saspro/numba_warmup.py +141 -0
  283. setiastro/saspro/ops/__init__.py +9 -0
  284. setiastro/saspro/ops/command_help_dialog.py +623 -0
  285. setiastro/saspro/ops/command_runner.py +217 -0
  286. setiastro/saspro/ops/commands.py +1594 -0
  287. setiastro/saspro/ops/script_editor.py +1105 -0
  288. setiastro/saspro/ops/scripts.py +1476 -0
  289. setiastro/saspro/ops/settings.py +637 -0
  290. setiastro/saspro/parallel_utils.py +554 -0
  291. setiastro/saspro/pedestal.py +121 -0
  292. setiastro/saspro/perfect_palette_picker.py +1105 -0
  293. setiastro/saspro/pipeline.py +110 -0
  294. setiastro/saspro/pixelmath.py +1604 -0
  295. setiastro/saspro/plate_solver.py +2480 -0
  296. setiastro/saspro/project_io.py +797 -0
  297. setiastro/saspro/psf_utils.py +136 -0
  298. setiastro/saspro/psf_viewer.py +631 -0
  299. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  300. setiastro/saspro/remove_green.py +331 -0
  301. setiastro/saspro/remove_stars.py +1599 -0
  302. setiastro/saspro/remove_stars_preset.py +446 -0
  303. setiastro/saspro/resources.py +570 -0
  304. setiastro/saspro/rgb_combination.py +208 -0
  305. setiastro/saspro/rgb_extract.py +19 -0
  306. setiastro/saspro/rgbalign.py +727 -0
  307. setiastro/saspro/runtime_imports.py +7 -0
  308. setiastro/saspro/runtime_torch.py +754 -0
  309. setiastro/saspro/save_options.py +73 -0
  310. setiastro/saspro/selective_color.py +1614 -0
  311. setiastro/saspro/sfcc.py +1530 -0
  312. setiastro/saspro/shortcuts.py +3125 -0
  313. setiastro/saspro/signature_insert.py +1106 -0
  314. setiastro/saspro/stacking_suite.py +19069 -0
  315. setiastro/saspro/star_alignment.py +7383 -0
  316. setiastro/saspro/star_alignment_preset.py +329 -0
  317. setiastro/saspro/star_metrics.py +49 -0
  318. setiastro/saspro/star_spikes.py +769 -0
  319. setiastro/saspro/star_stretch.py +542 -0
  320. setiastro/saspro/stat_stretch.py +554 -0
  321. setiastro/saspro/status_log_dock.py +78 -0
  322. setiastro/saspro/subwindow.py +3523 -0
  323. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  324. setiastro/saspro/swap_manager.py +134 -0
  325. setiastro/saspro/torch_backend.py +89 -0
  326. setiastro/saspro/torch_rejection.py +434 -0
  327. setiastro/saspro/translations/all_source_strings.json +4726 -0
  328. setiastro/saspro/translations/ar_translations.py +4096 -0
  329. setiastro/saspro/translations/de_translations.py +3728 -0
  330. setiastro/saspro/translations/es_translations.py +4169 -0
  331. setiastro/saspro/translations/fr_translations.py +4090 -0
  332. setiastro/saspro/translations/hi_translations.py +3803 -0
  333. setiastro/saspro/translations/integrate_translations.py +271 -0
  334. setiastro/saspro/translations/it_translations.py +4728 -0
  335. setiastro/saspro/translations/ja_translations.py +3834 -0
  336. setiastro/saspro/translations/pt_translations.py +3847 -0
  337. setiastro/saspro/translations/ru_translations.py +3082 -0
  338. setiastro/saspro/translations/saspro_ar.qm +0 -0
  339. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  340. setiastro/saspro/translations/saspro_de.qm +0 -0
  341. setiastro/saspro/translations/saspro_de.ts +14548 -0
  342. setiastro/saspro/translations/saspro_es.qm +0 -0
  343. setiastro/saspro/translations/saspro_es.ts +16202 -0
  344. setiastro/saspro/translations/saspro_fr.qm +0 -0
  345. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  346. setiastro/saspro/translations/saspro_hi.qm +0 -0
  347. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  348. setiastro/saspro/translations/saspro_it.qm +0 -0
  349. setiastro/saspro/translations/saspro_it.ts +19046 -0
  350. setiastro/saspro/translations/saspro_ja.qm +0 -0
  351. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  352. setiastro/saspro/translations/saspro_pt.qm +0 -0
  353. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  354. setiastro/saspro/translations/saspro_ru.qm +0 -0
  355. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  356. setiastro/saspro/translations/saspro_sw.qm +0 -0
  357. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  358. setiastro/saspro/translations/saspro_uk.qm +0 -0
  359. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  360. setiastro/saspro/translations/saspro_zh.qm +0 -0
  361. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  362. setiastro/saspro/translations/sw_translations.py +3897 -0
  363. setiastro/saspro/translations/uk_translations.py +3929 -0
  364. setiastro/saspro/translations/zh_translations.py +3910 -0
  365. setiastro/saspro/versioning.py +77 -0
  366. setiastro/saspro/view_bundle.py +1558 -0
  367. setiastro/saspro/wavescale_hdr.py +648 -0
  368. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  369. setiastro/saspro/wavescalede.py +683 -0
  370. setiastro/saspro/wavescalede_preset.py +230 -0
  371. setiastro/saspro/wcs_update.py +374 -0
  372. setiastro/saspro/whitebalance.py +540 -0
  373. setiastro/saspro/widgets/__init__.py +48 -0
  374. setiastro/saspro/widgets/common_utilities.py +306 -0
  375. setiastro/saspro/widgets/graphics_views.py +122 -0
  376. setiastro/saspro/widgets/image_utils.py +518 -0
  377. setiastro/saspro/widgets/minigame/game.js +991 -0
  378. setiastro/saspro/widgets/minigame/index.html +53 -0
  379. setiastro/saspro/widgets/minigame/style.css +241 -0
  380. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  381. setiastro/saspro/widgets/resource_monitor.py +313 -0
  382. setiastro/saspro/widgets/spinboxes.py +290 -0
  383. setiastro/saspro/widgets/themed_buttons.py +13 -0
  384. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  385. setiastro/saspro/wimi.py +7367 -0
  386. setiastro/saspro/wims.py +588 -0
  387. setiastro/saspro/window_shelf.py +185 -0
  388. setiastro/saspro/xisf.py +1213 -0
  389. setiastrosuitepro-1.6.7.dist-info/METADATA +279 -0
  390. setiastrosuitepro-1.6.7.dist-info/RECORD +394 -0
  391. setiastrosuitepro-1.6.7.dist-info/WHEEL +4 -0
  392. setiastrosuitepro-1.6.7.dist-info/entry_points.txt +6 -0
  393. setiastrosuitepro-1.6.7.dist-info/licenses/LICENSE +674 -0
  394. setiastrosuitepro-1.6.7.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,631 @@
1
+ # pro/psf_viewer.py
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+ import sep
6
+ from astropy.table import Table
7
+
8
+ from PyQt6.QtCore import Qt, QTimer
9
+ from PyQt6.QtGui import QPainter, QPen, QFont, QPixmap
10
+ from PyQt6.QtWidgets import (
11
+ QDialog, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QScrollArea,
12
+ QSlider, QTableWidget, QTableWidgetItem, QApplication, QMessageBox
13
+ )
14
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
15
+
16
+ from PyQt6.QtCore import QThread, pyqtSignal, QObject
17
+ from PyQt6.QtWidgets import QWidget
18
+
19
+ class _ProcessingOverlay(QWidget):
20
+ def __init__(self, parent):
21
+ super().__init__(parent)
22
+ self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
23
+ self.setStyleSheet("""
24
+ QWidget {
25
+ background: rgba(0,0,0,140);
26
+ border-radius: 10px;
27
+ }
28
+ QLabel {
29
+ color: white;
30
+ font-size: 14px;
31
+ font-weight: 600;
32
+ }
33
+ """)
34
+ lay = QVBoxLayout(self)
35
+ lay.setContentsMargins(18, 14, 18, 14)
36
+ self.lbl = QLabel("Processing…", self)
37
+ self.lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
38
+ lay.addWidget(self.lbl)
39
+
40
+ def setText(self, s: str):
41
+ self.lbl.setText(s)
42
+
43
+ class _PSFWorker(QObject):
44
+ finished = pyqtSignal(object, str) # (Table or None, status_text)
45
+ failed = pyqtSignal(str)
46
+
47
+ def __init__(self, image: np.ndarray, threshold_sigma: float):
48
+ super().__init__()
49
+ self.image = image
50
+ self.threshold_sigma = float(threshold_sigma)
51
+
52
+ def run(self):
53
+ try:
54
+ if self.image is None:
55
+ self.finished.emit(None, "Status: No image.")
56
+ return
57
+
58
+ # grayscale
59
+ if self.image.ndim == 3:
60
+ image_gray = np.mean(self.image, axis=2)
61
+ else:
62
+ image_gray = self.image
63
+ data = image_gray.astype(np.float32, copy=False)
64
+
65
+ # background
66
+ bkg = sep.Background(data)
67
+ data_sub = data - bkg.back()
68
+ try:
69
+ err_val = bkg.globalrms
70
+ except Exception:
71
+ err_val = float(np.median(bkg.rms()))
72
+
73
+ sources = sep.extract(data_sub, self.threshold_sigma, err=err_val)
74
+ if sources is None or len(sources) == 0:
75
+ self.finished.emit(None, "Status: Extraction completed — 0 sources.")
76
+ return
77
+
78
+ # HFR proxy
79
+ try:
80
+ r = 2.0 * sources["a"]
81
+ except Exception:
82
+ r = np.zeros(len(sources), dtype=np.float32)
83
+
84
+ tbl = Table()
85
+ tbl["xcentroid"] = sources["x"]
86
+ tbl["ycentroid"] = sources["y"]
87
+ tbl["flux"] = sources["flux"]
88
+ tbl["HFR"] = r
89
+ tbl["a"] = sources["a"]
90
+ tbl["b"] = sources["b"]
91
+ tbl["theta"] = sources["theta"]
92
+
93
+ self.finished.emit(tbl, f"Status: Extraction completed — {len(sources)} sources.")
94
+ except Exception as e:
95
+ self.failed.emit(f"Extraction failed: {e}")
96
+
97
+
98
+ class PSFViewer(QDialog):
99
+ """
100
+ A lightweight PSF/Flux histogram viewer.
101
+ Pass an ImageSubWindow instance *or* a document (with .image and .changed).
102
+ Listens to doc.changed to keep results fresh.
103
+ """
104
+ def __init__(self, view_or_doc, parent=None):
105
+ super().__init__(parent)
106
+ self.setWindowTitle("PSF Viewer")
107
+
108
+ # Accept either a view (with .document) or a doc directly
109
+ doc = getattr(view_or_doc, "document", None)
110
+ self.doc = doc if doc is not None else view_or_doc
111
+ try:
112
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
113
+ except Exception:
114
+ pass # older PyQt6 versions
115
+ # Image + state
116
+ self.image = self._grab_image()
117
+ self.zoom_factor = 1.0
118
+ self.log_scale = False
119
+ self.star_list = None
120
+ self.histogram_mode = "PSF" # or "Flux"
121
+ self.detection_threshold = 5 # sigma
122
+
123
+ # Debounce timer for threshold slider
124
+ self.threshold_timer = QTimer(self)
125
+ self.threshold_timer.setSingleShot(True)
126
+ self.threshold_timer.setInterval(500)
127
+ self.threshold_timer.timeout.connect(self._applyThreshold)
128
+
129
+ # Auto-update when the document changes
130
+
131
+ self._psf_thread = None
132
+ self._psf_worker = None
133
+ self._doc_conn = False
134
+ if hasattr(self.doc, "changed"):
135
+ try:
136
+ self.doc.changed.connect(self._on_doc_changed)
137
+ self._doc_conn = True
138
+ except Exception:
139
+ self._doc_conn = False
140
+
141
+ # cleanup no matter how the dialog is dismissed (accept/reject/done)
142
+ self.finished.connect(self._cleanup)
143
+ self._build_ui()
144
+ # Defer first compute until after the dialog is shown/layouted
145
+ QTimer.singleShot(0, self._applyThreshold)
146
+
147
+ # ---------- internals ----------
148
+ def _grab_image(self):
149
+ img = getattr(self.doc, "image", None)
150
+ if img is None:
151
+ return None
152
+ # Ensure ndarray
153
+ try:
154
+ return np.asarray(img)
155
+ except Exception:
156
+ return None
157
+
158
+ def _on_doc_changed(self, *_):
159
+ self.image = self._grab_image()
160
+ # reuse the existing debounce timer instead of immediate recompute
161
+ if self.threshold_timer.isActive():
162
+ self.threshold_timer.stop()
163
+ self.threshold_timer.start()
164
+
165
+ # ---------- UI ----------
166
+ def _build_ui(self):
167
+ main_layout = QVBoxLayout(self)
168
+
169
+ # Top: histogram + stats
170
+ top_layout = QHBoxLayout()
171
+ self.scroll_area = QScrollArea(self)
172
+ self.scroll_area.setFixedSize(520, 310)
173
+ self.scroll_area.setWidgetResizable(False)
174
+ self.hist_label = QLabel(self)
175
+ self.hist_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
176
+ self.scroll_area.setWidget(self.hist_label)
177
+ top_layout.addWidget(self.scroll_area)
178
+
179
+ self.stats_table = QTableWidget(self)
180
+ self.stats_table.setRowCount(4)
181
+ self.stats_table.setColumnCount(0)
182
+ self.stats_table.setVerticalHeaderLabels(["Min", "Max", "Median", "StdDev"])
183
+ self.stats_table.setFixedWidth(360)
184
+ top_layout.addWidget(self.stats_table)
185
+ main_layout.addLayout(top_layout)
186
+
187
+ self.status_label = QLabel("Status: Ready", self)
188
+ self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
189
+ main_layout.addWidget(self.status_label)
190
+
191
+ # Controls
192
+ controls_layout = QHBoxLayout()
193
+
194
+ controls_layout.addWidget(QLabel("Zoom:"))
195
+
196
+ # themed zoom buttons
197
+ btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
198
+ btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
199
+ btn_fit = themed_toolbtn("zoom-fit-best", "Fit")
200
+
201
+ btn_zoom_out.clicked.connect(lambda: self._step_zoom(1/1.25))
202
+ btn_zoom_in.clicked.connect(lambda: self._step_zoom(1.25))
203
+ btn_fit.clicked.connect(self._fit_histogram)
204
+
205
+ controls_layout.addWidget(btn_zoom_out)
206
+ controls_layout.addWidget(btn_zoom_in)
207
+ controls_layout.addWidget(btn_fit)
208
+
209
+ # keep the slider (nice for big jumps)
210
+ self.zoom_slider = QSlider(Qt.Orientation.Horizontal, self)
211
+ self.zoom_slider.setRange(50, 1000)
212
+ self.zoom_slider.setValue(100)
213
+ self.zoom_slider.setTickInterval(10)
214
+ self.zoom_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
215
+ self.zoom_slider.valueChanged.connect(self.updateZoom)
216
+ controls_layout.addWidget(self.zoom_slider, 1)
217
+
218
+ self.log_toggle_button = QPushButton("Toggle Log X-Axis", self)
219
+ self.log_toggle_button.setCheckable(True)
220
+ self.log_toggle_button.setToolTip("Toggle between linear and logarithmic x-axis.")
221
+ self.log_toggle_button.toggled.connect(self.toggleLogScale)
222
+ controls_layout.addWidget(self.log_toggle_button)
223
+
224
+ self.mode_toggle_button = QPushButton("Show Flux Histogram", self)
225
+ self.mode_toggle_button.setToolTip("Switch between PSF (HFR) and Flux histograms.")
226
+ self.mode_toggle_button.clicked.connect(self.toggleHistogramMode)
227
+ controls_layout.addWidget(self.mode_toggle_button)
228
+
229
+ main_layout.addLayout(controls_layout)
230
+
231
+ # Threshold
232
+ thresh_layout = QHBoxLayout()
233
+ thresh_layout.addWidget(QLabel("Detection Threshold (σ):", self))
234
+ self.threshold_slider = QSlider(Qt.Orientation.Horizontal, self)
235
+ self.threshold_slider.setRange(1, 20)
236
+ self.threshold_slider.setValue(self.detection_threshold)
237
+ self.threshold_slider.setTickInterval(1)
238
+ self.threshold_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
239
+ self.threshold_slider.valueChanged.connect(self.onThresholdChange)
240
+ thresh_layout.addWidget(self.threshold_slider)
241
+
242
+ self.threshold_value_label = QLabel(str(self.detection_threshold), self)
243
+ thresh_layout.addWidget(self.threshold_value_label)
244
+ main_layout.addLayout(thresh_layout)
245
+
246
+ # Close
247
+ close_btn = QPushButton("Close", self)
248
+ close_btn.clicked.connect(self.close)
249
+ main_layout.addWidget(close_btn)
250
+
251
+ self.setLayout(main_layout)
252
+ self.drawHistogram()
253
+
254
+ # ---------- interactions ----------
255
+ def onThresholdChange(self, value: int):
256
+ self.detection_threshold = int(value)
257
+ self.threshold_value_label.setText(str(value))
258
+ if self.threshold_timer.isActive():
259
+ self.threshold_timer.stop()
260
+ self.threshold_timer.start()
261
+
262
+ def _step_zoom(self, factor: float):
263
+ v = int(round(self.zoom_slider.value() * factor))
264
+ v = max(self.zoom_slider.minimum(), min(self.zoom_slider.maximum(), v))
265
+ self.zoom_slider.setValue(v) # drives updateZoom()
266
+
267
+ def _fit_histogram(self):
268
+ # Fit the histogram pixmap to the scroll viewport width.
269
+ # Keeps behavior consistent with your other preview dialogs.
270
+ if not hasattr(self, "_base_hist_pm") or self._base_hist_pm is None:
271
+ return
272
+ vp_w = self.scroll_area.viewport().width()
273
+ base_w = max(1, self._base_hist_pm.width())
274
+ z = vp_w / base_w
275
+ self.zoom_slider.setValue(int(round(z * 100)))
276
+
277
+ def _apply_hist_zoom(self):
278
+ if not hasattr(self, "_base_hist_pm") or self._base_hist_pm is None:
279
+ return
280
+ z = self.zoom_slider.value() / 100.0
281
+ w = max(1, int(self._base_hist_pm.width() * z))
282
+ h = max(1, int(self._base_hist_pm.height() * z))
283
+ scaled = self._base_hist_pm.scaled(
284
+ w, h,
285
+ Qt.AspectRatioMode.KeepAspectRatio,
286
+ Qt.TransformationMode.SmoothTransformation
287
+ )
288
+ self.hist_label.setPixmap(scaled)
289
+ self.hist_label.resize(scaled.size())
290
+
291
+ def _applyThreshold(self):
292
+ if self.image is None:
293
+ self.star_list = None
294
+ self.status_label.setText("Status: No image.")
295
+ self.drawHistogram()
296
+ return
297
+
298
+ self._show_processing("Processing… extracting stars / PSFs")
299
+
300
+ # stop any previous run cleanly
301
+ self._stop_psf_worker()
302
+
303
+ self._psf_thread = QThread(self)
304
+ self._psf_worker = _PSFWorker(self.image, self.detection_threshold)
305
+ self._psf_worker.moveToThread(self._psf_thread)
306
+
307
+ self._psf_thread.started.connect(self._psf_worker.run)
308
+ self._psf_worker.finished.connect(self._on_psf_done)
309
+ self._psf_worker.failed.connect(self._on_psf_fail)
310
+
311
+ # ensure thread quits once worker reports anything
312
+ self._psf_worker.finished.connect(lambda *_: self._stop_psf_worker(quit_only=False))
313
+ self._psf_worker.failed.connect(lambda *_: self._stop_psf_worker(quit_only=False))
314
+
315
+
316
+ self._psf_thread.start()
317
+
318
+ def _stop_psf_worker(self, quit_only: bool = False):
319
+ thr = getattr(self, "_psf_thread", None)
320
+ wkr = getattr(self, "_psf_worker", None)
321
+
322
+ if thr is None:
323
+ return
324
+
325
+ try:
326
+ thr.quit()
327
+ except Exception:
328
+ pass
329
+ try:
330
+ thr.wait(250)
331
+ except Exception:
332
+ pass
333
+
334
+ if not quit_only:
335
+ try:
336
+ if wkr is not None:
337
+ wkr.deleteLater()
338
+ except Exception:
339
+ pass
340
+ try:
341
+ thr.deleteLater()
342
+ except Exception:
343
+ pass
344
+ self._psf_worker = None
345
+ self._psf_thread = None
346
+
347
+ def _on_psf_done(self, tbl, status: str):
348
+ # tbl is an astropy Table or None
349
+ self.star_list = tbl
350
+ self.status_label.setText(status)
351
+ self._hide_processing()
352
+ self.drawHistogram()
353
+
354
+ def _on_psf_fail(self, msg: str):
355
+ self.star_list = None
356
+ self.status_label.setText(f"Status: {msg}")
357
+ self._hide_processing()
358
+ self.drawHistogram()
359
+
360
+
361
+ def updateImage(self, new_image):
362
+ self.image = np.asarray(new_image) if new_image is not None else None
363
+ if self.threshold_timer.isActive():
364
+ self.threshold_timer.stop()
365
+ self.threshold_timer.start()
366
+
367
+ def updateZoom(self, _=None):
368
+ self._apply_hist_zoom()
369
+
370
+
371
+ def toggleLogScale(self, checked: bool):
372
+ self.log_scale = bool(checked)
373
+ self.drawHistogram()
374
+
375
+ def toggleHistogramMode(self):
376
+ if self.histogram_mode == "PSF":
377
+ self.histogram_mode = "Flux"
378
+ self.mode_toggle_button.setText("Show PSF Histogram")
379
+ else:
380
+ self.histogram_mode = "PSF"
381
+ self.mode_toggle_button.setText("Show Flux Histogram")
382
+ self.drawHistogram()
383
+
384
+ def _show_processing(self, msg="Processing…"):
385
+ if not hasattr(self, "_overlay") or self._overlay is None:
386
+ self._overlay = _ProcessingOverlay(self.scroll_area)
387
+ self._overlay.hide()
388
+ self._overlay.setText(msg)
389
+ self._overlay.resize(self.scroll_area.viewport().size())
390
+ self._overlay.move(0, 0)
391
+ self._overlay.show()
392
+ self._overlay.raise_()
393
+
394
+ def _hide_processing(self):
395
+ if hasattr(self, "_overlay") and self._overlay is not None:
396
+ self._overlay.hide()
397
+
398
+ def resizeEvent(self, e):
399
+ super().resizeEvent(e)
400
+ if hasattr(self, "_overlay") and self._overlay is not None and self._overlay.isVisible():
401
+ self._overlay.resize(self.scroll_area.viewport().size())
402
+
403
+
404
+ # ---------- compute ----------
405
+ def compute_star_list(self):
406
+ if self.image is None:
407
+ self.status_label.setText("Status: No image.")
408
+ self.star_list = None
409
+ return
410
+
411
+ # Convert to grayscale
412
+ if self.image.ndim == 3:
413
+ image_gray = np.mean(self.image, axis=2)
414
+ else:
415
+ image_gray = self.image
416
+ data = image_gray.astype(np.float32, copy=False)
417
+
418
+ # Background
419
+ try:
420
+ bkg = sep.Background(data)
421
+ data_sub = data - bkg.back()
422
+ try:
423
+ err_val = bkg.globalrms
424
+ except Exception:
425
+ err_val = float(np.median(bkg.rms()))
426
+ except Exception as e:
427
+ self.status_label.setText(f"Status: Background failed: {e}")
428
+ self.star_list = None
429
+ return
430
+
431
+ threshold = float(self.detection_threshold)
432
+
433
+ self.status_label.setText("Status: Starting star extraction...")
434
+ QApplication.processEvents()
435
+
436
+ try:
437
+ sources = sep.extract(data_sub, threshold, err=err_val)
438
+ n = len(sources) if sources is not None else 0
439
+ self.status_label.setText(f"Status: Extraction completed — {n} sources.")
440
+ except Exception as e:
441
+ self.status_label.setText(f"Status: Extraction failed: {e}")
442
+ sources = None
443
+
444
+ QApplication.processEvents()
445
+
446
+ if sources is None or len(sources) == 0:
447
+ self.star_list = None
448
+ return
449
+
450
+ # HFR (quick proxy): 2 * a (a ≈ semi-major Gaussian sigma in pixels for SEP)
451
+ try:
452
+ a = sources["a"]
453
+ r = 2 * a
454
+ except Exception:
455
+ r = np.zeros(len(sources), dtype=np.float32)
456
+
457
+ tbl = Table()
458
+ tbl["xcentroid"] = sources["x"]
459
+ tbl["ycentroid"] = sources["y"]
460
+ tbl["flux"] = sources["flux"]
461
+ tbl["HFR"] = r
462
+ tbl["a"] = sources["a"]
463
+ tbl["b"] = sources["b"]
464
+ tbl["theta"] = sources["theta"]
465
+ self.star_list = tbl
466
+
467
+ # ---------- drawing ----------
468
+ def drawHistogram(self):
469
+ base_w, h = 512, 300
470
+
471
+ # Render at fixed base resolution (no zoom here)
472
+ pix = QPixmap(base_w, h)
473
+ pix.fill(Qt.GlobalColor.white)
474
+
475
+ painter = QPainter(pix)
476
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
477
+
478
+ # Prepare data
479
+ if self.star_list is None or len(self.star_list) == 0:
480
+ data = np.array([], dtype=float)
481
+ edges = np.linspace(0, 1, 51)
482
+ low, high = float(edges[0]), float(edges[-1])
483
+ else:
484
+ if self.histogram_mode == "PSF":
485
+ data = np.array(self.star_list["HFR"], dtype=float)
486
+ edges = np.linspace(0, 7.5, 51)
487
+ else:
488
+ data = np.array(self.star_list["flux"], dtype=float)
489
+ edges = np.linspace(data.min(), data.max(), 51) if data.size else np.linspace(0, 1, 51)
490
+ low, high = float(edges[0]), float(edges[-1])
491
+
492
+ # Axis scale helpers (map value -> x in [0..base_w])
493
+ if self.log_scale and high > max(low, 1e-9):
494
+ low = max(low, 1e-4)
495
+ edges = np.logspace(np.log10(low), np.log10(high if high > low else low * 10), 51)
496
+
497
+ lo_l = np.log10(low)
498
+ hi_l = np.log10(high) if high > low else lo_l + 1.0
499
+
500
+ def xfun(v: float) -> int:
501
+ lv = np.log10(max(v, low))
502
+ return int((lv - lo_l) / (hi_l - lo_l) * base_w) if hi_l > lo_l else 0
503
+ else:
504
+ def xfun(v: float) -> int:
505
+ return int((v - low) / (high - low) * base_w) if high > low else 0
506
+
507
+ # Histogram
508
+ hist = np.histogram(data, bins=edges)[0].astype(float)
509
+ if hist.size and hist.max() > 0:
510
+ hist /= hist.max()
511
+
512
+ # Bars
513
+ painter.setPen(QPen(Qt.GlobalColor.black))
514
+ for i in range(len(hist)):
515
+ x0 = xfun(float(edges[i]))
516
+ x1 = xfun(float(edges[i + 1]))
517
+ bw = max(x1 - x0, 1)
518
+ bh = float(hist[i]) * h
519
+ painter.drawRect(x0, int(h - bh), bw, int(bh))
520
+
521
+ # X axis
522
+ painter.setPen(QPen(Qt.GlobalColor.black, 2))
523
+ painter.drawLine(0, h - 1, base_w, h - 1)
524
+ painter.setFont(QFont("Arial", 10))
525
+
526
+ ticks = (
527
+ np.logspace(np.log10(max(low, 1e-4)), np.log10(max(high, low * 10)), 6)
528
+ if self.log_scale and high > low
529
+ else np.linspace(low, high, 6)
530
+ )
531
+ for t in ticks:
532
+ x = xfun(float(t))
533
+ painter.drawLine(x, h - 1, x, h - 6)
534
+ painter.drawText(x - 28, h - 10, f"{t:.3f}" if self.log_scale else f"{t:.2f}")
535
+
536
+ painter.end()
537
+
538
+ # Store base pixmap for zooming
539
+ self._base_hist_pm = pix
540
+ self._apply_hist_zoom() # scales into hist_label
541
+ self.updateStatistics()
542
+
543
+ def updateStatistics(self):
544
+ data_map = {}
545
+ if self.star_list is not None and len(self.star_list) > 0:
546
+ cols = ["HFR", "eccentricity", "a", "b", "theta", "flux"]
547
+ a = np.array(self.star_list["a"], float)
548
+ b = np.array(self.star_list["b"], float)
549
+ ecc = np.nan_to_num(np.sqrt(1 - (b / np.maximum(a, 1e-9)) ** 2))
550
+ data_map["eccentricity"] = ecc
551
+ for c in self.star_list.colnames:
552
+ try:
553
+ data_map[c] = np.array(self.star_list[c], float)
554
+ except Exception:
555
+ pass
556
+ cols = [c for c in cols if c in data_map]
557
+ else:
558
+ cols = []
559
+
560
+ self.stats_table.setColumnCount(len(cols))
561
+ self.stats_table.setHorizontalHeaderLabels(cols)
562
+ self.stats_table.setRowCount(4)
563
+ self.stats_table.setVerticalHeaderLabels(["Min", "Max", "Median", "StdDev"])
564
+
565
+ for ci, col in enumerate(cols):
566
+ arr = data_map.get(col, np.zeros(0, dtype=float))
567
+ if arr.size:
568
+ vals = [np.min(arr), np.max(arr), np.median(arr), np.std(arr)]
569
+ else:
570
+ vals = [0.0, 0.0, 0.0, 0.0]
571
+ for ri, v in enumerate(vals):
572
+ it = QTableWidgetItem(f"{v:.3f}")
573
+ it.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
574
+ self.stats_table.setItem(ri, ci, it)
575
+
576
+
577
+ def _cleanup(self):
578
+ # stop debounce timer
579
+ try:
580
+ if getattr(self, "threshold_timer", None) is not None:
581
+ self.threshold_timer.stop()
582
+ except Exception:
583
+ pass
584
+
585
+ # disconnect doc listener
586
+ try:
587
+ if self._doc_conn and hasattr(self.doc, "changed"):
588
+ self.doc.changed.disconnect(self._on_doc_changed)
589
+ except Exception:
590
+ pass
591
+ self._doc_conn = False
592
+
593
+ # stop worker/thread
594
+ try:
595
+ thr = getattr(self, "_psf_thread", None)
596
+ wkr = getattr(self, "_psf_worker", None)
597
+
598
+ if wkr is not None:
599
+ try:
600
+ wkr.deleteLater()
601
+ except Exception:
602
+ pass
603
+
604
+ if thr is not None:
605
+ try:
606
+ thr.requestInterruption()
607
+ except Exception:
608
+ pass
609
+ try:
610
+ thr.quit()
611
+ except Exception:
612
+ pass
613
+ try:
614
+ thr.wait(250)
615
+ except Exception:
616
+ pass
617
+ try:
618
+ thr.deleteLater()
619
+ except Exception:
620
+ pass
621
+ except Exception:
622
+ pass
623
+
624
+ self._psf_worker = None
625
+ self._psf_thread = None
626
+
627
+ # ---------- lifecycle ----------
628
+ def closeEvent(self, e):
629
+ self._cleanup()
630
+ super().closeEvent(e)
631
+