setiastrosuitepro 1.6.2.post1__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 (367) 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/andromedatry.png +0 -0
  24. setiastro/images/andromedatry_satellited.png +0 -0
  25. setiastro/images/annotated.png +0 -0
  26. setiastro/images/aperture.png +0 -0
  27. setiastro/images/astrosuite.ico +0 -0
  28. setiastro/images/astrosuite.png +0 -0
  29. setiastro/images/astrosuitepro.icns +0 -0
  30. setiastro/images/astrosuitepro.ico +0 -0
  31. setiastro/images/astrosuitepro.png +0 -0
  32. setiastro/images/background.png +0 -0
  33. setiastro/images/background2.png +0 -0
  34. setiastro/images/benchmark.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  37. setiastro/images/blaster.png +0 -0
  38. setiastro/images/blink.png +0 -0
  39. setiastro/images/clahe.png +0 -0
  40. setiastro/images/collage.png +0 -0
  41. setiastro/images/colorwheel.png +0 -0
  42. setiastro/images/contsub.png +0 -0
  43. setiastro/images/convo.png +0 -0
  44. setiastro/images/copyslot.png +0 -0
  45. setiastro/images/cosmic.png +0 -0
  46. setiastro/images/cosmicsat.png +0 -0
  47. setiastro/images/crop1.png +0 -0
  48. setiastro/images/cropicon.png +0 -0
  49. setiastro/images/curves.png +0 -0
  50. setiastro/images/cvs.png +0 -0
  51. setiastro/images/debayer.png +0 -0
  52. setiastro/images/denoise_cnn_custom.png +0 -0
  53. setiastro/images/denoise_cnn_graph.png +0 -0
  54. setiastro/images/disk.png +0 -0
  55. setiastro/images/dse.png +0 -0
  56. setiastro/images/exoicon.png +0 -0
  57. setiastro/images/eye.png +0 -0
  58. setiastro/images/fliphorizontal.png +0 -0
  59. setiastro/images/flipvertical.png +0 -0
  60. setiastro/images/font.png +0 -0
  61. setiastro/images/freqsep.png +0 -0
  62. setiastro/images/functionbundle.png +0 -0
  63. setiastro/images/graxpert.png +0 -0
  64. setiastro/images/green.png +0 -0
  65. setiastro/images/gridicon.png +0 -0
  66. setiastro/images/halo.png +0 -0
  67. setiastro/images/hdr.png +0 -0
  68. setiastro/images/histogram.png +0 -0
  69. setiastro/images/hubble.png +0 -0
  70. setiastro/images/imagecombine.png +0 -0
  71. setiastro/images/invert.png +0 -0
  72. setiastro/images/isophote.png +0 -0
  73. setiastro/images/isophote_demo_figure.png +0 -0
  74. setiastro/images/isophote_demo_image.png +0 -0
  75. setiastro/images/isophote_demo_model.png +0 -0
  76. setiastro/images/isophote_demo_residual.png +0 -0
  77. setiastro/images/jwstpupil.png +0 -0
  78. setiastro/images/linearfit.png +0 -0
  79. setiastro/images/livestacking.png +0 -0
  80. setiastro/images/mask.png +0 -0
  81. setiastro/images/maskapply.png +0 -0
  82. setiastro/images/maskcreate.png +0 -0
  83. setiastro/images/maskremove.png +0 -0
  84. setiastro/images/morpho.png +0 -0
  85. setiastro/images/mosaic.png +0 -0
  86. setiastro/images/multiscale_decomp.png +0 -0
  87. setiastro/images/nbtorgb.png +0 -0
  88. setiastro/images/neutral.png +0 -0
  89. setiastro/images/nuke.png +0 -0
  90. setiastro/images/openfile.png +0 -0
  91. setiastro/images/pedestal.png +0 -0
  92. setiastro/images/pen.png +0 -0
  93. setiastro/images/pixelmath.png +0 -0
  94. setiastro/images/platesolve.png +0 -0
  95. setiastro/images/ppp.png +0 -0
  96. setiastro/images/pro.png +0 -0
  97. setiastro/images/project.png +0 -0
  98. setiastro/images/psf.png +0 -0
  99. setiastro/images/redo.png +0 -0
  100. setiastro/images/redoicon.png +0 -0
  101. setiastro/images/rescale.png +0 -0
  102. setiastro/images/rgbalign.png +0 -0
  103. setiastro/images/rgbcombo.png +0 -0
  104. setiastro/images/rgbextract.png +0 -0
  105. setiastro/images/rotate180.png +0 -0
  106. setiastro/images/rotateclockwise.png +0 -0
  107. setiastro/images/rotatecounterclockwise.png +0 -0
  108. setiastro/images/satellite.png +0 -0
  109. setiastro/images/script.png +0 -0
  110. setiastro/images/selectivecolor.png +0 -0
  111. setiastro/images/simbad.png +0 -0
  112. setiastro/images/slot0.png +0 -0
  113. setiastro/images/slot1.png +0 -0
  114. setiastro/images/slot2.png +0 -0
  115. setiastro/images/slot3.png +0 -0
  116. setiastro/images/slot4.png +0 -0
  117. setiastro/images/slot5.png +0 -0
  118. setiastro/images/slot6.png +0 -0
  119. setiastro/images/slot7.png +0 -0
  120. setiastro/images/slot8.png +0 -0
  121. setiastro/images/slot9.png +0 -0
  122. setiastro/images/spcc.png +0 -0
  123. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  124. setiastro/images/spinner.gif +0 -0
  125. setiastro/images/stacking.png +0 -0
  126. setiastro/images/staradd.png +0 -0
  127. setiastro/images/staralign.png +0 -0
  128. setiastro/images/starnet.png +0 -0
  129. setiastro/images/starregistration.png +0 -0
  130. setiastro/images/starspike.png +0 -0
  131. setiastro/images/starstretch.png +0 -0
  132. setiastro/images/statstretch.png +0 -0
  133. setiastro/images/supernova.png +0 -0
  134. setiastro/images/uhs.png +0 -0
  135. setiastro/images/undoicon.png +0 -0
  136. setiastro/images/upscale.png +0 -0
  137. setiastro/images/viewbundle.png +0 -0
  138. setiastro/images/whitebalance.png +0 -0
  139. setiastro/images/wimi_icon_256x256.png +0 -0
  140. setiastro/images/wimilogo.png +0 -0
  141. setiastro/images/wims.png +0 -0
  142. setiastro/images/wrench_icon.png +0 -0
  143. setiastro/images/xisfliberator.png +0 -0
  144. setiastro/qml/ResourceMonitor.qml +126 -0
  145. setiastro/saspro/__init__.py +20 -0
  146. setiastro/saspro/__main__.py +945 -0
  147. setiastro/saspro/_generated/__init__.py +7 -0
  148. setiastro/saspro/_generated/build_info.py +3 -0
  149. setiastro/saspro/abe.py +1346 -0
  150. setiastro/saspro/abe_preset.py +196 -0
  151. setiastro/saspro/aberration_ai.py +694 -0
  152. setiastro/saspro/aberration_ai_preset.py +224 -0
  153. setiastro/saspro/accel_installer.py +218 -0
  154. setiastro/saspro/accel_workers.py +30 -0
  155. setiastro/saspro/add_stars.py +624 -0
  156. setiastro/saspro/astrobin_exporter.py +1010 -0
  157. setiastro/saspro/astrospike.py +153 -0
  158. setiastro/saspro/astrospike_python.py +1841 -0
  159. setiastro/saspro/autostretch.py +198 -0
  160. setiastro/saspro/backgroundneutral.py +602 -0
  161. setiastro/saspro/batch_convert.py +328 -0
  162. setiastro/saspro/batch_renamer.py +522 -0
  163. setiastro/saspro/blemish_blaster.py +491 -0
  164. setiastro/saspro/blink_comparator_pro.py +2926 -0
  165. setiastro/saspro/bundles.py +61 -0
  166. setiastro/saspro/bundles_dock.py +114 -0
  167. setiastro/saspro/cheat_sheet.py +213 -0
  168. setiastro/saspro/clahe.py +368 -0
  169. setiastro/saspro/comet_stacking.py +1442 -0
  170. setiastro/saspro/common_tr.py +107 -0
  171. setiastro/saspro/config.py +38 -0
  172. setiastro/saspro/config_bootstrap.py +40 -0
  173. setiastro/saspro/config_manager.py +316 -0
  174. setiastro/saspro/continuum_subtract.py +1617 -0
  175. setiastro/saspro/convo.py +1400 -0
  176. setiastro/saspro/convo_preset.py +414 -0
  177. setiastro/saspro/copyastro.py +190 -0
  178. setiastro/saspro/cosmicclarity.py +1589 -0
  179. setiastro/saspro/cosmicclarity_preset.py +407 -0
  180. setiastro/saspro/crop_dialog_pro.py +973 -0
  181. setiastro/saspro/crop_preset.py +189 -0
  182. setiastro/saspro/curve_editor_pro.py +2562 -0
  183. setiastro/saspro/curves_preset.py +375 -0
  184. setiastro/saspro/debayer.py +673 -0
  185. setiastro/saspro/debug_utils.py +29 -0
  186. setiastro/saspro/dnd_mime.py +35 -0
  187. setiastro/saspro/doc_manager.py +2664 -0
  188. setiastro/saspro/exoplanet_detector.py +2166 -0
  189. setiastro/saspro/file_utils.py +284 -0
  190. setiastro/saspro/fitsmodifier.py +748 -0
  191. setiastro/saspro/fix_bom.py +32 -0
  192. setiastro/saspro/free_torch_memory.py +48 -0
  193. setiastro/saspro/frequency_separation.py +1349 -0
  194. setiastro/saspro/function_bundle.py +1596 -0
  195. setiastro/saspro/generate_translations.py +3092 -0
  196. setiastro/saspro/ghs_dialog_pro.py +663 -0
  197. setiastro/saspro/ghs_preset.py +284 -0
  198. setiastro/saspro/graxpert.py +637 -0
  199. setiastro/saspro/graxpert_preset.py +287 -0
  200. setiastro/saspro/gui/__init__.py +0 -0
  201. setiastro/saspro/gui/main_window.py +8810 -0
  202. setiastro/saspro/gui/mixins/__init__.py +33 -0
  203. setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
  204. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  205. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  206. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  207. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  208. setiastro/saspro/gui/mixins/menu_mixin.py +389 -0
  209. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  210. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  211. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  212. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  213. setiastro/saspro/gui/statistics_dialog.py +47 -0
  214. setiastro/saspro/halobgon.py +488 -0
  215. setiastro/saspro/header_viewer.py +448 -0
  216. setiastro/saspro/headless_utils.py +88 -0
  217. setiastro/saspro/histogram.py +756 -0
  218. setiastro/saspro/history_explorer.py +941 -0
  219. setiastro/saspro/i18n.py +168 -0
  220. setiastro/saspro/image_combine.py +417 -0
  221. setiastro/saspro/image_peeker_pro.py +1604 -0
  222. setiastro/saspro/imageops/__init__.py +37 -0
  223. setiastro/saspro/imageops/mdi_snap.py +292 -0
  224. setiastro/saspro/imageops/scnr.py +36 -0
  225. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  226. setiastro/saspro/imageops/stretch.py +236 -0
  227. setiastro/saspro/isophote.py +1182 -0
  228. setiastro/saspro/layers.py +208 -0
  229. setiastro/saspro/layers_dock.py +714 -0
  230. setiastro/saspro/lazy_imports.py +193 -0
  231. setiastro/saspro/legacy/__init__.py +2 -0
  232. setiastro/saspro/legacy/image_manager.py +2226 -0
  233. setiastro/saspro/legacy/numba_utils.py +3676 -0
  234. setiastro/saspro/legacy/xisf.py +1071 -0
  235. setiastro/saspro/linear_fit.py +537 -0
  236. setiastro/saspro/live_stacking.py +1841 -0
  237. setiastro/saspro/log_bus.py +5 -0
  238. setiastro/saspro/logging_config.py +460 -0
  239. setiastro/saspro/luminancerecombine.py +309 -0
  240. setiastro/saspro/main_helpers.py +201 -0
  241. setiastro/saspro/mask_creation.py +931 -0
  242. setiastro/saspro/masks_core.py +56 -0
  243. setiastro/saspro/mdi_widgets.py +353 -0
  244. setiastro/saspro/memory_utils.py +666 -0
  245. setiastro/saspro/metadata_patcher.py +75 -0
  246. setiastro/saspro/mfdeconv.py +3831 -0
  247. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  248. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  249. setiastro/saspro/mfdeconvsport.py +2382 -0
  250. setiastro/saspro/minorbodycatalog.py +567 -0
  251. setiastro/saspro/morphology.py +407 -0
  252. setiastro/saspro/multiscale_decomp.py +1293 -0
  253. setiastro/saspro/nbtorgb_stars.py +541 -0
  254. setiastro/saspro/numba_utils.py +3145 -0
  255. setiastro/saspro/numba_warmup.py +141 -0
  256. setiastro/saspro/ops/__init__.py +9 -0
  257. setiastro/saspro/ops/command_help_dialog.py +623 -0
  258. setiastro/saspro/ops/command_runner.py +217 -0
  259. setiastro/saspro/ops/commands.py +1594 -0
  260. setiastro/saspro/ops/script_editor.py +1102 -0
  261. setiastro/saspro/ops/scripts.py +1473 -0
  262. setiastro/saspro/ops/settings.py +637 -0
  263. setiastro/saspro/parallel_utils.py +554 -0
  264. setiastro/saspro/pedestal.py +121 -0
  265. setiastro/saspro/perfect_palette_picker.py +1071 -0
  266. setiastro/saspro/pipeline.py +110 -0
  267. setiastro/saspro/pixelmath.py +1604 -0
  268. setiastro/saspro/plate_solver.py +2445 -0
  269. setiastro/saspro/project_io.py +797 -0
  270. setiastro/saspro/psf_utils.py +136 -0
  271. setiastro/saspro/psf_viewer.py +549 -0
  272. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  273. setiastro/saspro/remove_green.py +331 -0
  274. setiastro/saspro/remove_stars.py +1599 -0
  275. setiastro/saspro/remove_stars_preset.py +404 -0
  276. setiastro/saspro/resources.py +501 -0
  277. setiastro/saspro/rgb_combination.py +208 -0
  278. setiastro/saspro/rgb_extract.py +19 -0
  279. setiastro/saspro/rgbalign.py +723 -0
  280. setiastro/saspro/runtime_imports.py +7 -0
  281. setiastro/saspro/runtime_torch.py +754 -0
  282. setiastro/saspro/save_options.py +73 -0
  283. setiastro/saspro/selective_color.py +1552 -0
  284. setiastro/saspro/sfcc.py +1472 -0
  285. setiastro/saspro/shortcuts.py +3043 -0
  286. setiastro/saspro/signature_insert.py +1102 -0
  287. setiastro/saspro/stacking_suite.py +18470 -0
  288. setiastro/saspro/star_alignment.py +7435 -0
  289. setiastro/saspro/star_alignment_preset.py +329 -0
  290. setiastro/saspro/star_metrics.py +49 -0
  291. setiastro/saspro/star_spikes.py +765 -0
  292. setiastro/saspro/star_stretch.py +507 -0
  293. setiastro/saspro/stat_stretch.py +538 -0
  294. setiastro/saspro/status_log_dock.py +78 -0
  295. setiastro/saspro/subwindow.py +3328 -0
  296. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  297. setiastro/saspro/swap_manager.py +99 -0
  298. setiastro/saspro/torch_backend.py +89 -0
  299. setiastro/saspro/torch_rejection.py +434 -0
  300. setiastro/saspro/translations/all_source_strings.json +3654 -0
  301. setiastro/saspro/translations/ar_translations.py +3865 -0
  302. setiastro/saspro/translations/de_translations.py +3749 -0
  303. setiastro/saspro/translations/es_translations.py +3939 -0
  304. setiastro/saspro/translations/fr_translations.py +3858 -0
  305. setiastro/saspro/translations/hi_translations.py +3571 -0
  306. setiastro/saspro/translations/integrate_translations.py +270 -0
  307. setiastro/saspro/translations/it_translations.py +3678 -0
  308. setiastro/saspro/translations/ja_translations.py +3601 -0
  309. setiastro/saspro/translations/pt_translations.py +3869 -0
  310. setiastro/saspro/translations/ru_translations.py +2848 -0
  311. setiastro/saspro/translations/saspro_ar.qm +0 -0
  312. setiastro/saspro/translations/saspro_ar.ts +255 -0
  313. setiastro/saspro/translations/saspro_de.qm +0 -0
  314. setiastro/saspro/translations/saspro_de.ts +253 -0
  315. setiastro/saspro/translations/saspro_es.qm +0 -0
  316. setiastro/saspro/translations/saspro_es.ts +12520 -0
  317. setiastro/saspro/translations/saspro_fr.qm +0 -0
  318. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  319. setiastro/saspro/translations/saspro_hi.qm +0 -0
  320. setiastro/saspro/translations/saspro_hi.ts +257 -0
  321. setiastro/saspro/translations/saspro_it.qm +0 -0
  322. setiastro/saspro/translations/saspro_it.ts +12520 -0
  323. setiastro/saspro/translations/saspro_ja.qm +0 -0
  324. setiastro/saspro/translations/saspro_ja.ts +257 -0
  325. setiastro/saspro/translations/saspro_pt.qm +0 -0
  326. setiastro/saspro/translations/saspro_pt.ts +257 -0
  327. setiastro/saspro/translations/saspro_ru.qm +0 -0
  328. setiastro/saspro/translations/saspro_ru.ts +237 -0
  329. setiastro/saspro/translations/saspro_sw.qm +0 -0
  330. setiastro/saspro/translations/saspro_sw.ts +257 -0
  331. setiastro/saspro/translations/saspro_uk.qm +0 -0
  332. setiastro/saspro/translations/saspro_uk.ts +10771 -0
  333. setiastro/saspro/translations/saspro_zh.qm +0 -0
  334. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  335. setiastro/saspro/translations/sw_translations.py +3671 -0
  336. setiastro/saspro/translations/uk_translations.py +3700 -0
  337. setiastro/saspro/translations/zh_translations.py +3675 -0
  338. setiastro/saspro/versioning.py +77 -0
  339. setiastro/saspro/view_bundle.py +1558 -0
  340. setiastro/saspro/wavescale_hdr.py +645 -0
  341. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  342. setiastro/saspro/wavescalede.py +680 -0
  343. setiastro/saspro/wavescalede_preset.py +230 -0
  344. setiastro/saspro/wcs_update.py +374 -0
  345. setiastro/saspro/whitebalance.py +492 -0
  346. setiastro/saspro/widgets/__init__.py +48 -0
  347. setiastro/saspro/widgets/common_utilities.py +306 -0
  348. setiastro/saspro/widgets/graphics_views.py +122 -0
  349. setiastro/saspro/widgets/image_utils.py +518 -0
  350. setiastro/saspro/widgets/minigame/game.js +986 -0
  351. setiastro/saspro/widgets/minigame/index.html +53 -0
  352. setiastro/saspro/widgets/minigame/style.css +241 -0
  353. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  354. setiastro/saspro/widgets/resource_monitor.py +237 -0
  355. setiastro/saspro/widgets/spinboxes.py +275 -0
  356. setiastro/saspro/widgets/themed_buttons.py +13 -0
  357. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  358. setiastro/saspro/wimi.py +7996 -0
  359. setiastro/saspro/wims.py +578 -0
  360. setiastro/saspro/window_shelf.py +185 -0
  361. setiastro/saspro/xisf.py +1123 -0
  362. setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
  363. setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
  364. setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
  365. setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
  366. setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
  367. setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,414 @@
1
+ # pro/convo_preset.py
2
+ from __future__ import annotations
3
+ from typing import Optional, Tuple
4
+ import numpy as np
5
+
6
+ from PyQt6.QtWidgets import (
7
+ QDialog, QFormLayout, QDialogButtonBox, QVBoxLayout, QHBoxLayout,
8
+ QLabel, QComboBox, QCheckBox
9
+ )
10
+ from PyQt6.QtCore import Qt
11
+
12
+ # Reuse widgets/utilities from convo.py
13
+ from .convo import (
14
+ ConvoDeconvoDialog, FloatSliderWithEdit,
15
+ make_elliptical_gaussian_psf, van_cittert_deconv, larson_sekanina
16
+ )
17
+
18
+ # ---------------------------- Preset Editor Dialog ----------------------------
19
+ class ConvoPresetDialog(QDialog):
20
+ """
21
+ One dialog for all Convo/Deconvo presets (including TV).
22
+ Produces a JSON-safe dict you can stash on a shortcut.
23
+ """
24
+ def __init__(self, parent=None, initial: dict | None = None):
25
+ super().__init__(parent)
26
+ self.setWindowTitle("Convolution / Deconvolution — Preset")
27
+ p = dict(initial or {})
28
+ op = p.get("op", "convolution")
29
+
30
+ root = QVBoxLayout(self)
31
+
32
+ # --- top: operation selector ---
33
+ op_row = QHBoxLayout()
34
+ op_row.addWidget(QLabel("Operation:"))
35
+ self.op_combo = QComboBox()
36
+ self.op_combo.addItems(["convolution", "deconvolution", "tv"])
37
+ self.op_combo.setCurrentText(op if op in ("convolution", "deconvolution", "tv") else "convolution")
38
+ op_row.addWidget(self.op_combo); op_row.addStretch()
39
+ root.addLayout(op_row)
40
+
41
+ # --- stacked parameter forms (we'll toggle visibility) ---
42
+ self.form_conv = QFormLayout()
43
+ self.conv_radius = FloatSliderWithEdit(minimum=0.1, maximum=200.0, step=0.1, initial=float(p.get("radius", 5.0)), suffix=" px")
44
+ self.conv_kurtosis = FloatSliderWithEdit(minimum=0.1, maximum=10.0, step=0.1, initial=float(p.get("kurtosis", 2.0)), suffix="σ")
45
+ self.conv_aspect = FloatSliderWithEdit(minimum=0.1, maximum=10.0, step=0.1, initial=float(p.get("aspect", 1.0)))
46
+ self.conv_rotation = FloatSliderWithEdit(minimum=0.0, maximum=360.0, step=1.0, initial=float(p.get("rotation", 0.0)), suffix="°")
47
+ self.conv_strength = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("strength", 1.0)))
48
+ self.form_conv.addRow("Radius:", self.conv_radius)
49
+ self.form_conv.addRow("Kurtosis (σ):", self.conv_kurtosis)
50
+ self.form_conv.addRow("Aspect Ratio:", self.conv_aspect)
51
+ self.form_conv.addRow("Rotation:", self.conv_rotation)
52
+ self.form_conv.addRow("Strength:", self.conv_strength)
53
+
54
+ self.form_deconv = QFormLayout()
55
+ self.deconv_algo = QComboBox()
56
+ self.deconv_algo.addItems(["Richardson-Lucy", "Wiener", "Larson-Sekanina", "Van Cittert"])
57
+ self.deconv_algo.setCurrentText(p.get("algo", "Richardson-Lucy"))
58
+ self.form_deconv.addRow("Algorithm:", self.deconv_algo)
59
+
60
+ # RL/Wiener PSF params
61
+ self.psf_radius = FloatSliderWithEdit(minimum=0.1, maximum=100.0, step=0.1, initial=float(p.get("psf_radius", 3.0)), suffix=" px")
62
+ self.psf_kurtosis = FloatSliderWithEdit(minimum=0.1, maximum=10.0, step=0.1, initial=float(p.get("psf_kurtosis", 2.0)), suffix="σ")
63
+ self.psf_aspect = FloatSliderWithEdit(minimum=0.1, maximum=10.0, step=0.1, initial=float(p.get("psf_aspect", 1.0)))
64
+ self.psf_rot = FloatSliderWithEdit(minimum=0.0, maximum=360.0, step=1.0, initial=float(p.get("psf_rotation", 0.0)), suffix="°")
65
+ self.form_deconv.addRow("PSF Radius:", self.psf_radius)
66
+ self.form_deconv.addRow("PSF Kurtosis:", self.psf_kurtosis)
67
+ self.form_deconv.addRow("PSF Aspect:", self.psf_aspect)
68
+ self.form_deconv.addRow("PSF Rotation:", self.psf_rot)
69
+
70
+ # RL options
71
+ self.rl_iter = FloatSliderWithEdit(minimum=1, maximum=200, step=1, initial=float(p.get("rl_iter", 30)))
72
+ self.rl_reg = QComboBox(); self.rl_reg.addItems(["None (Plain R–L)", "Tikhonov (L2)", "Total Variation (TV)"])
73
+ self.rl_reg.setCurrentText(p.get("rl_reg", "None (Plain R–L)"))
74
+ self.rl_clip = QCheckBox("De-ring (bilateral)"); self.rl_clip.setChecked(bool(p.get("rl_dering", True)))
75
+ self.rl_l_only = QCheckBox("L* only"); self.rl_l_only.setChecked(bool(p.get("luminance_only", True)))
76
+ self.form_deconv.addRow("RL Iterations:", self.rl_iter)
77
+ self.form_deconv.addRow("RL Regularization:", self.rl_reg)
78
+ self.form_deconv.addRow("", self.rl_clip)
79
+ self.form_deconv.addRow("", self.rl_l_only)
80
+
81
+ # Wiener options
82
+ self.wiener_nsr = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.001, initial=float(p.get("wiener_nsr", 0.01)))
83
+ self.wiener_reg = QComboBox(); self.wiener_reg.addItems(["None (Classical Wiener)", "Tikhonov (L2)"])
84
+ self.wiener_reg.setCurrentText(p.get("wiener_reg", "None (Classical Wiener)"))
85
+ self.wiener_dering= QCheckBox("De-ring pass"); self.wiener_dering.setChecked(bool(p.get("wiener_dering", True)))
86
+ self.form_deconv.addRow("Wiener NSR:", self.wiener_nsr)
87
+ self.form_deconv.addRow("Wiener Regularization:", self.wiener_reg)
88
+ self.form_deconv.addRow("", self.wiener_dering)
89
+
90
+ # Larson–Sekanina
91
+ self.ls_rstep = FloatSliderWithEdit(minimum=0.0, maximum=50.0, step=0.1, initial=float(p.get("ls_rstep", 0.0)), suffix=" px")
92
+ self.ls_astep = FloatSliderWithEdit(minimum=0.1, maximum=360.0, step=0.1, initial=float(p.get("ls_astep", 1.0)), suffix="°")
93
+ self.ls_operator = QComboBox(); self.ls_operator.addItems(["Divide", "Subtract"]); self.ls_operator.setCurrentText(p.get("ls_operator", "Divide"))
94
+ self.ls_blend = QComboBox(); self.ls_blend.addItems(["SoftLight", "Screen"]); self.ls_blend.setCurrentText(p.get("ls_blend", "SoftLight"))
95
+ self.form_deconv.addRow("LS Radial Step:", self.ls_rstep)
96
+ self.form_deconv.addRow("LS Angular Step:", self.ls_astep)
97
+ self.form_deconv.addRow("LS Operator:", self.ls_operator)
98
+ self.form_deconv.addRow("Blend:", self.ls_blend)
99
+
100
+ # Van Cittert
101
+ self.vc_iter = FloatSliderWithEdit(minimum=1, maximum=1000, step=1, initial=float(p.get("vc_iter", 10)))
102
+ self.vc_relax = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("vc_relax", 0.0)))
103
+ self.form_deconv.addRow("VC Iterations:", self.vc_iter)
104
+ self.form_deconv.addRow("VC Relaxation:", self.vc_relax)
105
+
106
+ # Strength (applies to all ops)
107
+ self.deconv_strength = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("strength", 1.0)))
108
+ self.form_deconv.addRow("Strength:", self.deconv_strength)
109
+
110
+ # TV Denoise
111
+ self.form_tv = QFormLayout()
112
+ self.tv_weight = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("tv_weight", 0.10)))
113
+ self.tv_iter = FloatSliderWithEdit(minimum=1, maximum=100, step=1, initial=float(p.get("tv_iter", 10)))
114
+ self.tv_multi = QCheckBox("Multi-channel"); self.tv_multi.setChecked(bool(p.get("tv_multichannel", True)))
115
+ self.tv_strength = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("strength", 1.0)))
116
+ self.form_tv.addRow("TV Weight:", self.tv_weight)
117
+ self.form_tv.addRow("TV Iterations:", self.tv_iter)
118
+ self.form_tv.addRow("", self.tv_multi)
119
+ self.form_tv.addRow("Strength:", self.tv_strength)
120
+
121
+ # containers to show/hide
122
+ self.box_conv = _wrap_form(self.form_conv)
123
+ self.box_decv = _wrap_form(self.form_deconv)
124
+ self.box_tv = _wrap_form(self.form_tv)
125
+ root.addWidget(self.box_conv)
126
+ root.addWidget(self.box_decv)
127
+ root.addWidget(self.box_tv)
128
+
129
+ def _toggle():
130
+ v = self.op_combo.currentText()
131
+ self.box_conv.setVisible(v == "convolution")
132
+ self.box_decv.setVisible(v == "deconvolution")
133
+ self.box_tv.setVisible(v == "tv")
134
+ self.op_combo.currentTextChanged.connect(lambda _: _toggle())
135
+ _toggle()
136
+
137
+ # buttons
138
+ btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
139
+ btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
140
+ root.addWidget(btns)
141
+
142
+ def result_dict(self) -> dict:
143
+ op = self.op_combo.currentText()
144
+ if op == "convolution":
145
+ return {
146
+ "op": "convolution",
147
+ "radius": self.conv_radius.value(),
148
+ "kurtosis": self.conv_kurtosis.value(),
149
+ "aspect": self.conv_aspect.value(),
150
+ "rotation": self.conv_rotation.value(),
151
+ "strength": self.conv_strength.value(),
152
+ }
153
+ if op == "deconvolution":
154
+ return {
155
+ "op": "deconvolution",
156
+ "algo": self.deconv_algo.currentText(),
157
+ "psf_radius": self.psf_radius.value(),
158
+ "psf_kurtosis": self.psf_kurtosis.value(),
159
+ "psf_aspect": self.psf_aspect.value(),
160
+ "psf_rotation": self.psf_rot.value(),
161
+ "rl_iter": self.rl_iter.value(),
162
+ "rl_reg": self.rl_reg.currentText(),
163
+ "rl_dering": bool(self.rl_clip.isChecked()),
164
+ "luminance_only": bool(self.rl_l_only.isChecked()),
165
+ "wiener_nsr": self.wiener_nsr.value(),
166
+ "wiener_reg": self.wiener_reg.currentText(),
167
+ "wiener_dering": bool(self.wiener_dering.isChecked()),
168
+ "ls_rstep": self.ls_rstep.value(),
169
+ "ls_astep": self.ls_astep.value(),
170
+ "ls_operator": self.ls_operator.currentText(),
171
+ "ls_blend": self.ls_blend.currentText(),
172
+ "vc_iter": self.vc_iter.value(),
173
+ "vc_relax": self.vc_relax.value(),
174
+ "strength": self.deconv_strength.value(),
175
+ # optional center for LS (x,y) — if omitted we’ll use image center
176
+ # "center": [x, y],
177
+ }
178
+ # tv
179
+ return {
180
+ "op": "tv",
181
+ "tv_weight": self.tv_weight.value(),
182
+ "tv_iter": int(self.tv_iter.value()),
183
+ "tv_multichannel": bool(self.tv_multi.isChecked()),
184
+ "strength": self.tv_strength.value(),
185
+ }
186
+
187
+
188
+ def _wrap_form(form: QFormLayout):
189
+ from PyQt6.QtWidgets import QWidget, QVBoxLayout
190
+ w = QWidget(); l = QVBoxLayout(w); l.setContentsMargins(0,0,0,0); l.addLayout(form)
191
+ return w
192
+
193
+
194
+ # ---------------------------- Headless Apply ----------------------------
195
+ def apply_convo_via_preset(main_window, doc, preset: dict):
196
+ """
197
+ Headless executor for Convolution/Deconvolution/TV using the same kernels/flows
198
+ as the dialog. Applies result to `doc` via doc_manager.
199
+ """
200
+ import numpy as np
201
+ from skimage.color import rgb2lab, lab2rgb
202
+ from skimage.restoration import denoise_tv_chambolle
203
+
204
+ dm = getattr(main_window, "doc_manager", None) or getattr(main_window, "dm", None)
205
+ if dm is None or doc is None or getattr(doc, "image", None) is None:
206
+ return
207
+
208
+ # ⚠️ You can keep or drop this; it no longer matters for the apply step.
209
+ try:
210
+ if hasattr(dm, "set_active_document"):
211
+ dm.set_active_document(doc)
212
+ except Exception:
213
+ pass
214
+
215
+ img = np.asarray(doc.image).astype(np.float32, copy=False)
216
+ p = dict(preset or {})
217
+ op = p.get("op", "convolution")
218
+
219
+ # Create a dialog instance to reuse its helpers (no UI shown)
220
+ d = ConvoDeconvoDialog(doc_manager=dm, parent=main_window)
221
+
222
+ def _blend(a, b, s):
223
+ s = float(max(0.0, min(1.0, s)))
224
+ return np.clip(b * s + a * (1.0 - s), 0.0, 1.0).astype(np.float32)
225
+
226
+ if op == "convolution":
227
+ psf = make_elliptical_gaussian_psf(
228
+ float(p.get("radius", 5.0)),
229
+ float(p.get("kurtosis", 2.0)),
230
+ float(p.get("aspect", 1.0)),
231
+ float(p.get("rotation", 0.0)),
232
+ ).astype(np.float32)
233
+ out = d._convolve_color(img, psf)
234
+ out = _blend(img, out, float(p.get("strength", 1.0)))
235
+
236
+ elif op == "deconvolution":
237
+ algo = p.get("algo", "Richardson-Lucy")
238
+ if algo in ("Richardson-Lucy", "Wiener"):
239
+ psf = make_elliptical_gaussian_psf(
240
+ float(p.get("psf_radius", 3.0)),
241
+ float(p.get("psf_kurtosis", 2.0)),
242
+ float(p.get("psf_aspect", 1.0)),
243
+ float(p.get("psf_rotation", 0.0)),
244
+ ).astype(np.float32)
245
+
246
+ if algo == "Richardson-Lucy":
247
+ iters = int(round(float(p.get("rl_iter", 30))))
248
+ reg = p.get("rl_reg", "None (Plain R–L)")
249
+ clipf = bool(p.get("rl_dering", True))
250
+ lum_only = bool(p.get("luminance_only", True))
251
+ if lum_only and img.ndim == 3 and img.shape[2] == 3:
252
+ lab = rgb2lab(img); L = (lab[...,0] / 100.0).astype(np.float32)
253
+ Ld = d._richardson_lucy_color(L, psf, iterations=iters, reg_type=reg, clip_flag=clipf)
254
+ lab[...,0] = np.clip(Ld * 100.0, 0.0, 100.0)
255
+ tmp = lab2rgb(lab.astype(np.float32)).astype(np.float32)
256
+ out = np.clip(tmp, 0.0, 1.0)
257
+ else:
258
+ out = d._richardson_lucy_color(img, psf, iterations=iters, reg_type=reg, clip_flag=clipf)
259
+ out = _blend(img, out, float(p.get("strength", 1.0)))
260
+
261
+ elif algo == "Wiener":
262
+ nsr = float(p.get("wiener_nsr", 0.01))
263
+ reg = p.get("wiener_reg", "None (Classical Wiener)")
264
+ dering= bool(p.get("wiener_dering", True))
265
+ lum_only = bool(p.get("luminance_only", True))
266
+ if lum_only and img.ndim == 3 and img.shape[2] == 3:
267
+ lab = rgb2lab(img); L = (lab[...,0] / 100.0).astype(np.float32)
268
+ Ld = d._wiener_deconv_with_kernel(L, psf, nsr, reg, dering)
269
+ lab[...,0] = np.clip(Ld * 100.0, 0.0, 100.0)
270
+ tmp = lab2rgb(lab.astype(np.float32)).astype(np.float32)
271
+ out = np.clip(tmp, 0.0, 1.0)
272
+ else:
273
+ out = d._wiener_deconv_with_kernel(img, psf, nsr, reg, dering)
274
+ out = np.clip(out, 0.0, 1.0)
275
+ out = _blend(img, out, float(p.get("strength", 1.0)))
276
+
277
+
278
+ elif algo == "Larson-Sekanina":
279
+ H, W = img.shape[:2]
280
+ cxy = p.get("center", [W/2, H/2])
281
+ cx = float(cxy[0]); cy = float(cxy[1])
282
+
283
+ B = larson_sekanina(
284
+ image=img,
285
+ center=(cy, cx), # (y,x)
286
+ radial_step=float(p.get("ls_rstep", 0.0)),
287
+ angular_step_deg=float(p.get("ls_astep", 1.0)),
288
+ operator=p.get("ls_operator", "Divide")
289
+ )
290
+
291
+ A = img
292
+ if A.ndim == 3 and A.shape[2] == 3:
293
+ # ✅ FIX: repeat into channel axis
294
+ B_rgb = np.repeat(B[..., None], 3, axis=2)
295
+ A_rgb = A
296
+ else:
297
+ B_rgb = B[..., None]
298
+ A_rgb = A[..., None]
299
+
300
+ blend_mode = p.get("ls_blend", "SoftLight")
301
+ if blend_mode == "Screen":
302
+ C = (A_rgb + B_rgb - (A_rgb * B_rgb))
303
+ else: # SoftLight
304
+ C = (1 - 2 * B_rgb) * (A_rgb ** 2) + 2 * B_rgb * A_rgb
305
+
306
+ out = np.clip(C, 0.0, 1.0)
307
+ out = out[..., 0] if img.ndim == 2 else out
308
+ out = _blend(img, out, float(p.get("strength", 1.0)))
309
+
310
+ elif algo == "Van Cittert":
311
+ iters = int(round(float(p.get("vc_iter", 10))))
312
+ relax = float(p.get("vc_relax", 0.0))
313
+ if img.ndim == 3 and img.shape[2] == 3:
314
+ out = np.stack([van_cittert_deconv(img[...,c], iters, relax) for c in range(3)], axis=2).astype(np.float32)
315
+ else:
316
+ out = van_cittert_deconv(img, iters, relax).astype(np.float32)
317
+ out = np.clip(out, 0.0, 1.0)
318
+ out = _blend(img, out, float(p.get("strength", 1.0)))
319
+ else:
320
+ return # unknown algo
321
+
322
+ elif op == "tv":
323
+ from skimage.restoration import denoise_tv_chambolle
324
+ weight = float(p.get("tv_weight", 0.10))
325
+ max_iter = int(p.get("tv_iter", 10))
326
+ multich = bool(p.get("tv_multichannel", True))
327
+ if img.ndim == 3 and multich:
328
+ out = denoise_tv_chambolle(img.astype(np.float32), weight=weight, max_num_iter=max_iter, channel_axis=-1).astype(np.float32)
329
+ elif img.ndim == 3 and img.shape[2] == 3:
330
+ chans = [denoise_tv_chambolle(img[...,c].astype(np.float32), weight=weight, max_num_iter=max_iter, channel_axis=None) for c in range(3)]
331
+ out = np.stack(chans, axis=2).astype(np.float32)
332
+ else:
333
+ out = denoise_tv_chambolle(img.astype(np.float32), weight=weight, max_num_iter=max_iter, channel_axis=None).astype(np.float32)
334
+ out = _blend(img, np.clip(out, 0.0, 1.0), float(p.get("strength", 1.0)))
335
+
336
+ else:
337
+ return
338
+
339
+ meta = dict(getattr(doc, "metadata", {}) or {})
340
+ meta["source"] = "ConvoDeconvo"
341
+
342
+ try:
343
+ if hasattr(doc, "apply_edit"):
344
+ # Let Document handle full vs ROI, history, etc.
345
+ doc.apply_edit(
346
+ out.astype(np.float32, copy=False),
347
+ metadata=meta,
348
+ step_name="Convo/Deconvo (preset)",
349
+ )
350
+ else:
351
+ # Fallback for legacy paths
352
+ if hasattr(dm, "set_active_document"):
353
+ dm.set_active_document(doc)
354
+ dm.update_active_document(
355
+ out.astype(np.float32, copy=False),
356
+ metadata=meta,
357
+ step_name="Convo/Deconvo (preset)",
358
+ )
359
+ except Exception:
360
+ # Re-raise so replay_last_action_on_base can show the warning
361
+ raise
362
+
363
+ def run_convo_via_preset(main, doc_or_preset=None, preset: dict | None = None, *, target_doc=None):
364
+ """
365
+ Headless Convo/Deconvo/TV entrypoint for CommandSpec + Replay.
366
+
367
+ Supports BOTH call shapes:
368
+ 1) New CommandRunner shape:
369
+ run_convo_via_preset(main, target_doc, preset)
370
+ 2) Legacy shape:
371
+ run_convo_via_preset(main, preset_dict, target_doc=doc)
372
+ run_convo_via_preset(main, preset_dict)
373
+ """
374
+
375
+ from PyQt6.QtWidgets import QMessageBox
376
+
377
+ # ---- Interpret arguments for backward compat / new executor ----
378
+ if preset is None and isinstance(doc_or_preset, dict):
379
+ # Legacy: (main, preset_dict, target_doc=?)
380
+ p = dict(doc_or_preset or {})
381
+ doc = target_doc
382
+ else:
383
+ # New executor: (main, doc, preset_dict)
384
+ p = dict(preset or {})
385
+ doc = target_doc if target_doc is not None else doc_or_preset
386
+
387
+ # Resolve active doc if still None
388
+ if doc is None:
389
+ d = getattr(main, "_active_doc", None)
390
+ doc = d() if callable(d) else d
391
+
392
+ if doc is None or getattr(doc, "image", None) is None:
393
+ QMessageBox.warning(main, "Convolution / Deconvolution", "Load an image first.")
394
+ return
395
+
396
+ # ---- Record for Replay ----
397
+ try:
398
+ remember = getattr(main, "remember_last_headless_command", None)
399
+ if remember is None:
400
+ remember = getattr(main, "_remember_last_headless_command", None)
401
+
402
+ if callable(remember):
403
+ # IMPORTANT: store canonical id that exists in registry
404
+ remember("convo", p, description="Convolution / Deconvolution")
405
+ else:
406
+ setattr(main, "_last_headless_command", {
407
+ "command_id": "convo",
408
+ "preset": dict(p),
409
+ })
410
+ except Exception:
411
+ pass
412
+
413
+ apply_convo_via_preset(main, doc, p)
414
+
@@ -0,0 +1,190 @@
1
+ # pro/copyastro.py
2
+ # pro/m_header.py
3
+ from __future__ import annotations
4
+ from PyQt6.QtCore import Qt
5
+ from PyQt6.QtWidgets import (
6
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
7
+ QPushButton, QMessageBox, QCheckBox, QMdiSubWindow
8
+ )
9
+
10
+ class CopyAstrometryDialog(QDialog):
11
+ """
12
+ Modeless picker that copies the WCS/SIP solution from a source doc
13
+ into the target doc (explicitly passed active view).
14
+ """
15
+ def __init__(self, parent=None, target=None):
16
+ super().__init__(parent)
17
+ self.setWindowTitle("Copy Astrometric Solution")
18
+ self.setWindowFlag(Qt.WindowType.Window, True)
19
+ self.setWindowModality(Qt.WindowModality.NonModal)
20
+ self.setModal(False)
21
+ self.setMinimumWidth(420)
22
+
23
+ self._mw = parent
24
+ self._dm = getattr(parent, "doc_manager", None) or getattr(parent, "docman", None)
25
+
26
+ # --- resolve target doc from the passed-in active subwindow/view/doc
27
+ self._tgt = self._doc_from_target(target)
28
+ if self._tgt is None:
29
+ # fallback to active doc helpers, just in case
30
+ try:
31
+ self._tgt = self._dm.get_active_document() if self._dm else None
32
+ except Exception:
33
+ self._tgt = None
34
+ if self._tgt is None and hasattr(parent, "_active_doc"):
35
+ try:
36
+ self._tgt = parent._active_doc()
37
+ except Exception:
38
+ pass
39
+
40
+ lay = QVBoxLayout(self)
41
+
42
+ tgt_name = getattr(self._tgt, "display_name", lambda: None)() or "Active View"
43
+ lay.addWidget(QLabel(f"Target: <b>{tgt_name}</b>"))
44
+
45
+ lay.addWidget(QLabel("Choose a source image that already has a WCS/SIP solution:"))
46
+ self.combo = QComboBox(self)
47
+ lay.addWidget(self.combo)
48
+
49
+ self.chk_ignore_sip = QCheckBox("Ignore SIP terms (copy TAN only)")
50
+ self.chk_ignore_sip.setChecked(False)
51
+ lay.addWidget(self.chk_ignore_sip)
52
+
53
+ row = QHBoxLayout(); row.addStretch(1)
54
+ self.btn_copy = QPushButton("Copy")
55
+ self.btn_close = QPushButton("Close")
56
+ row.addWidget(self.btn_copy); row.addWidget(self.btn_close)
57
+ lay.addLayout(row)
58
+
59
+ self.btn_copy.clicked.connect(self._do_copy)
60
+ self.btn_close.clicked.connect(self.close)
61
+
62
+ self._candidates = [] # list[(doc, name, wcs_dict)]
63
+ self._load_sources()
64
+
65
+ # --- helpers --------------------------------------------------------
66
+ def _doc_from_target(self, target):
67
+ """Accept QMdiSubWindow, ImageSubWindow, or ImageDocument."""
68
+ try:
69
+ if target is None:
70
+ return None
71
+ # QMdiSubWindow → widget() → .document
72
+ if isinstance(target, QMdiSubWindow):
73
+ w = target.widget()
74
+ return getattr(w, "document", None)
75
+ # ImageSubWindow-like
76
+ if hasattr(target, "document"):
77
+ return getattr(target, "document", None)
78
+ # Already a document
79
+ if hasattr(target, "image") and hasattr(target, "metadata"):
80
+ return target
81
+ except Exception:
82
+ pass
83
+ return None
84
+
85
+ def _extract_wcs_dict_for(self, doc):
86
+ # Prefer the MW helper you already have (returns a flat dict of FITS cards)
87
+ if hasattr(self._mw, "_extract_wcs_dict"):
88
+ try:
89
+ d = self._mw._extract_wcs_dict(doc)
90
+ if d: return dict(d)
91
+ except Exception:
92
+ pass
93
+
94
+ # Fallback: read from original_header
95
+ meta = getattr(doc, "metadata", {}) or {}
96
+ hdr = meta.get("original_header") or {}
97
+ try:
98
+ keys = [str(k).upper() for k in getattr(hdr, "keys", lambda: hdr.keys())()]
99
+ if "CRVAL1" in keys and "CRVAL2" in keys:
100
+ return {k: hdr[k] for k in getattr(hdr, "keys", lambda: hdr.keys())()}
101
+ except Exception:
102
+ pass
103
+ return {}
104
+
105
+ def _load_sources(self):
106
+ self.combo.clear()
107
+ self._candidates.clear()
108
+
109
+ if not self._dm or not self._tgt:
110
+ self.combo.addItem("No target image.")
111
+ self.btn_copy.setEnabled(False)
112
+ return
113
+
114
+ try:
115
+ docs = self._dm.all_documents()
116
+ except Exception:
117
+ docs = []
118
+
119
+ found_any = False
120
+ for d in docs:
121
+ if d is self._tgt:
122
+ continue
123
+ w = self._extract_wcs_dict_for(d)
124
+ if not w:
125
+ continue
126
+ name = getattr(d, "display_name", lambda: None)() or (getattr(d, "metadata", {}).get("file_path") or "Untitled")
127
+ # hint text (RA/Dec)
128
+ ra, dec = w.get("CRVAL1"), w.get("CRVAL2")
129
+ hint = f" (RA={ra:.5f}, Dec={dec:.5f})" if isinstance(ra, (int, float)) and isinstance(dec, (int, float)) else ""
130
+ self.combo.addItem(name + hint)
131
+ self._candidates.append((d, name, w))
132
+ found_any = True
133
+
134
+ if not found_any:
135
+ self.combo.addItem("No other images with WCS found")
136
+ self.btn_copy.setEnabled(False)
137
+
138
+ # --- action ---------------------------------------------------------
139
+ def _do_copy(self):
140
+ if self._tgt is None:
141
+ QMessageBox.information(self, "Copy Astrometry", "No target image.")
142
+ return
143
+
144
+ idx = self.combo.currentIndex()
145
+ if idx < 0 or idx >= len(self._candidates):
146
+ return
147
+
148
+ _, src_name, wcs = self._candidates[idx]
149
+
150
+ # Optionally strip SIP → TAN only
151
+ if self.chk_ignore_sip.isChecked():
152
+ wcs = {
153
+ k: v for k, v in wcs.items()
154
+ if not str(k).upper().startswith(("A_", "B_", "AP_", "BP_"))
155
+ and str(k).upper() not in {"A_ORDER", "B_ORDER", "AP_ORDER", "BP_ORDER"}
156
+ }
157
+ # enforce TAN
158
+ c1 = str(wcs.get("CTYPE1", "RA---TAN"))
159
+ c2 = str(wcs.get("CTYPE2", "DEC--TAN"))
160
+ if c1.endswith("-SIP"): wcs["CTYPE1"] = "RA---TAN"
161
+ if c2.endswith("-SIP"): wcs["CTYPE2"] = "DEC--TAN"
162
+
163
+ ok = False
164
+ if hasattr(self._mw, "_apply_wcs_dict_to_doc"):
165
+ try:
166
+ ok = bool(self._mw._apply_wcs_dict_to_doc(self._tgt, dict(wcs)))
167
+ except Exception:
168
+ ok = False
169
+
170
+ if not ok:
171
+ QMessageBox.warning(self, "Copy Astrometry", "Failed to apply astrometric solution.")
172
+ return
173
+
174
+ # refresh header dock + listeners immediately
175
+ try:
176
+ if hasattr(self._mw, "_refresh_header_viewer"):
177
+ self._mw._refresh_header_viewer(self._tgt)
178
+ if hasattr(self._mw, "currentDocumentChanged"):
179
+ self._mw.currentDocumentChanged.emit(self._tgt)
180
+ except Exception:
181
+ pass
182
+
183
+ try:
184
+ tgt_name = getattr(self._tgt, "display_name", lambda: None)() or "Target"
185
+ QMessageBox.information(self, "Copy Astrometry",
186
+ f"Copied solution from “{src_name}” to “{tgt_name}”.")
187
+ except Exception:
188
+ pass
189
+
190
+ self.close()