setiastrosuitepro 1.6.0__py3-none-any.whl → 1.6.4.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.
Files changed (293) hide show
  1. setiastro/data/SASP_data.fits +0 -0
  2. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  3. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  4. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  5. setiastro/data/catalogs/cali2.csv +63 -0
  6. setiastro/data/catalogs/cali2color.csv +65 -0
  7. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  8. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  9. setiastro/data/catalogs/detected_stars.csv +24784 -0
  10. setiastro/data/catalogs/fits_header_data.csv +46 -0
  11. setiastro/data/catalogs/test.csv +8 -0
  12. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  13. setiastro/images/Astro_Spikes.png +0 -0
  14. setiastro/images/Background_startup.jpg +0 -0
  15. setiastro/images/HRDiagram.png +0 -0
  16. setiastro/images/LExtract.png +0 -0
  17. setiastro/images/LInsert.png +0 -0
  18. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  19. setiastro/images/RGB080604.png +0 -0
  20. setiastro/images/abeicon.png +0 -0
  21. setiastro/images/aberration.png +0 -0
  22. setiastro/images/andromedatry.png +0 -0
  23. setiastro/images/andromedatry_satellited.png +0 -0
  24. setiastro/images/annotated.png +0 -0
  25. setiastro/images/aperture.png +0 -0
  26. setiastro/images/astrosuite.ico +0 -0
  27. setiastro/images/astrosuite.png +0 -0
  28. setiastro/images/astrosuitepro.icns +0 -0
  29. setiastro/images/astrosuitepro.ico +0 -0
  30. setiastro/images/astrosuitepro.png +0 -0
  31. setiastro/images/background.png +0 -0
  32. setiastro/images/background2.png +0 -0
  33. setiastro/images/benchmark.png +0 -0
  34. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  36. setiastro/images/blaster.png +0 -0
  37. setiastro/images/blink.png +0 -0
  38. setiastro/images/clahe.png +0 -0
  39. setiastro/images/collage.png +0 -0
  40. setiastro/images/colorwheel.png +0 -0
  41. setiastro/images/contsub.png +0 -0
  42. setiastro/images/convo.png +0 -0
  43. setiastro/images/copyslot.png +0 -0
  44. setiastro/images/cosmic.png +0 -0
  45. setiastro/images/cosmicsat.png +0 -0
  46. setiastro/images/crop1.png +0 -0
  47. setiastro/images/cropicon.png +0 -0
  48. setiastro/images/curves.png +0 -0
  49. setiastro/images/cvs.png +0 -0
  50. setiastro/images/debayer.png +0 -0
  51. setiastro/images/denoise_cnn_custom.png +0 -0
  52. setiastro/images/denoise_cnn_graph.png +0 -0
  53. setiastro/images/disk.png +0 -0
  54. setiastro/images/dse.png +0 -0
  55. setiastro/images/exoicon.png +0 -0
  56. setiastro/images/eye.png +0 -0
  57. setiastro/images/fliphorizontal.png +0 -0
  58. setiastro/images/flipvertical.png +0 -0
  59. setiastro/images/font.png +0 -0
  60. setiastro/images/freqsep.png +0 -0
  61. setiastro/images/functionbundle.png +0 -0
  62. setiastro/images/graxpert.png +0 -0
  63. setiastro/images/green.png +0 -0
  64. setiastro/images/gridicon.png +0 -0
  65. setiastro/images/halo.png +0 -0
  66. setiastro/images/hdr.png +0 -0
  67. setiastro/images/histogram.png +0 -0
  68. setiastro/images/hubble.png +0 -0
  69. setiastro/images/imagecombine.png +0 -0
  70. setiastro/images/invert.png +0 -0
  71. setiastro/images/isophote.png +0 -0
  72. setiastro/images/isophote_demo_figure.png +0 -0
  73. setiastro/images/isophote_demo_image.png +0 -0
  74. setiastro/images/isophote_demo_model.png +0 -0
  75. setiastro/images/isophote_demo_residual.png +0 -0
  76. setiastro/images/jwstpupil.png +0 -0
  77. setiastro/images/linearfit.png +0 -0
  78. setiastro/images/livestacking.png +0 -0
  79. setiastro/images/mask.png +0 -0
  80. setiastro/images/maskapply.png +0 -0
  81. setiastro/images/maskcreate.png +0 -0
  82. setiastro/images/maskremove.png +0 -0
  83. setiastro/images/morpho.png +0 -0
  84. setiastro/images/mosaic.png +0 -0
  85. setiastro/images/multiscale_decomp.png +0 -0
  86. setiastro/images/nbtorgb.png +0 -0
  87. setiastro/images/neutral.png +0 -0
  88. setiastro/images/nuke.png +0 -0
  89. setiastro/images/openfile.png +0 -0
  90. setiastro/images/pedestal.png +0 -0
  91. setiastro/images/pen.png +0 -0
  92. setiastro/images/pixelmath.png +0 -0
  93. setiastro/images/platesolve.png +0 -0
  94. setiastro/images/ppp.png +0 -0
  95. setiastro/images/pro.png +0 -0
  96. setiastro/images/project.png +0 -0
  97. setiastro/images/psf.png +0 -0
  98. setiastro/images/redo.png +0 -0
  99. setiastro/images/redoicon.png +0 -0
  100. setiastro/images/rescale.png +0 -0
  101. setiastro/images/rgbalign.png +0 -0
  102. setiastro/images/rgbcombo.png +0 -0
  103. setiastro/images/rgbextract.png +0 -0
  104. setiastro/images/rotate180.png +0 -0
  105. setiastro/images/rotatearbitrary.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/__main__.py +228 -67
  146. setiastro/saspro/_generated/build_info.py +2 -1
  147. setiastro/saspro/abe.py +76 -25
  148. setiastro/saspro/aberration_ai.py +14 -14
  149. setiastro/saspro/add_stars.py +15 -12
  150. setiastro/saspro/astrobin_exporter.py +61 -58
  151. setiastro/saspro/astrospike_python.py +3 -1
  152. setiastro/saspro/autostretch.py +4 -2
  153. setiastro/saspro/backgroundneutral.py +65 -14
  154. setiastro/saspro/batch_convert.py +8 -5
  155. setiastro/saspro/batch_renamer.py +39 -36
  156. setiastro/saspro/blemish_blaster.py +15 -12
  157. setiastro/saspro/blink_comparator_pro.py +605 -379
  158. setiastro/saspro/cheat_sheet.py +62 -17
  159. setiastro/saspro/clahe.py +34 -8
  160. setiastro/saspro/comet_stacking.py +103 -38
  161. setiastro/saspro/common_tr.py +107 -0
  162. setiastro/saspro/continuum_subtract.py +7 -7
  163. setiastro/saspro/convo.py +12 -9
  164. setiastro/saspro/copyastro.py +3 -0
  165. setiastro/saspro/cosmicclarity.py +77 -52
  166. setiastro/saspro/crop_dialog_pro.py +80 -45
  167. setiastro/saspro/curve_editor_pro.py +51 -33
  168. setiastro/saspro/debayer.py +6 -3
  169. setiastro/saspro/doc_manager.py +49 -19
  170. setiastro/saspro/exoplanet_detector.py +11 -11
  171. setiastro/saspro/fitsmodifier.py +48 -44
  172. setiastro/saspro/fix_bom.py +32 -0
  173. setiastro/saspro/frequency_separation.py +18 -12
  174. setiastro/saspro/function_bundle.py +18 -16
  175. setiastro/saspro/generate_translations.py +3092 -0
  176. setiastro/saspro/ghs_dialog_pro.py +19 -16
  177. setiastro/saspro/graxpert.py +3 -0
  178. setiastro/saspro/gui/main_window.py +471 -126
  179. setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
  180. setiastro/saspro/gui/mixins/file_mixin.py +25 -20
  181. setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
  182. setiastro/saspro/gui/mixins/header_mixin.py +6 -6
  183. setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
  184. setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
  185. setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
  186. setiastro/saspro/gui/mixins/update_mixin.py +26 -26
  187. setiastro/saspro/gui/statistics_dialog.py +47 -0
  188. setiastro/saspro/halobgon.py +29 -3
  189. setiastro/saspro/header_viewer.py +21 -18
  190. setiastro/saspro/histogram.py +29 -26
  191. setiastro/saspro/history_explorer.py +2 -0
  192. setiastro/saspro/i18n.py +168 -0
  193. setiastro/saspro/image_combine.py +3 -0
  194. setiastro/saspro/image_peeker_pro.py +52 -44
  195. setiastro/saspro/imageops/stretch.py +5 -13
  196. setiastro/saspro/isophote.py +3 -0
  197. setiastro/saspro/legacy/numba_utils.py +64 -47
  198. setiastro/saspro/linear_fit.py +3 -0
  199. setiastro/saspro/live_stacking.py +13 -2
  200. setiastro/saspro/mask_creation.py +180 -22
  201. setiastro/saspro/mfdeconv.py +5 -0
  202. setiastro/saspro/morphology.py +38 -13
  203. setiastro/saspro/multiscale_decomp.py +713 -256
  204. setiastro/saspro/nbtorgb_stars.py +12 -2
  205. setiastro/saspro/numba_utils.py +149 -48
  206. setiastro/saspro/ops/scripts.py +77 -17
  207. setiastro/saspro/ops/settings.py +177 -100
  208. setiastro/saspro/perfect_palette_picker.py +25 -7
  209. setiastro/saspro/pixelmath.py +114 -110
  210. setiastro/saspro/plate_solver.py +118 -108
  211. setiastro/saspro/remove_green.py +24 -7
  212. setiastro/saspro/remove_stars.py +136 -162
  213. setiastro/saspro/remove_stars_preset.py +55 -13
  214. setiastro/saspro/resources.py +46 -15
  215. setiastro/saspro/rgb_combination.py +19 -18
  216. setiastro/saspro/rgbalign.py +11 -11
  217. setiastro/saspro/save_options.py +5 -4
  218. setiastro/saspro/selective_color.py +84 -25
  219. setiastro/saspro/sfcc.py +119 -72
  220. setiastro/saspro/shortcuts.py +345 -36
  221. setiastro/saspro/signature_insert.py +4 -1
  222. setiastro/saspro/stacking_suite.py +2066 -1119
  223. setiastro/saspro/star_alignment.py +291 -331
  224. setiastro/saspro/star_spikes.py +137 -53
  225. setiastro/saspro/star_stretch.py +47 -10
  226. setiastro/saspro/stat_stretch.py +52 -16
  227. setiastro/saspro/status_log_dock.py +1 -1
  228. setiastro/saspro/subwindow.py +97 -36
  229. setiastro/saspro/supernovaasteroidhunter.py +68 -61
  230. setiastro/saspro/swap_manager.py +77 -42
  231. setiastro/saspro/translations/all_source_strings.json +4726 -0
  232. setiastro/saspro/translations/ar_translations.py +4096 -0
  233. setiastro/saspro/translations/de_translations.py +3728 -0
  234. setiastro/saspro/translations/es_translations.py +4169 -0
  235. setiastro/saspro/translations/fr_translations.py +4090 -0
  236. setiastro/saspro/translations/hi_translations.py +3803 -0
  237. setiastro/saspro/translations/integrate_translations.py +271 -0
  238. setiastro/saspro/translations/it_translations.py +4728 -0
  239. setiastro/saspro/translations/ja_translations.py +3834 -0
  240. setiastro/saspro/translations/pt_translations.py +3847 -0
  241. setiastro/saspro/translations/ru_translations.py +3082 -0
  242. setiastro/saspro/translations/saspro_ar.qm +0 -0
  243. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  244. setiastro/saspro/translations/saspro_de.qm +0 -0
  245. setiastro/saspro/translations/saspro_de.ts +14548 -0
  246. setiastro/saspro/translations/saspro_es.qm +0 -0
  247. setiastro/saspro/translations/saspro_es.ts +16202 -0
  248. setiastro/saspro/translations/saspro_fr.qm +0 -0
  249. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  250. setiastro/saspro/translations/saspro_hi.qm +0 -0
  251. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  252. setiastro/saspro/translations/saspro_it.qm +0 -0
  253. setiastro/saspro/translations/saspro_it.ts +19046 -0
  254. setiastro/saspro/translations/saspro_ja.qm +0 -0
  255. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  256. setiastro/saspro/translations/saspro_pt.qm +0 -0
  257. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  258. setiastro/saspro/translations/saspro_ru.qm +0 -0
  259. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  260. setiastro/saspro/translations/saspro_sw.qm +0 -0
  261. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  262. setiastro/saspro/translations/saspro_uk.qm +0 -0
  263. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  264. setiastro/saspro/translations/saspro_zh.qm +0 -0
  265. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  266. setiastro/saspro/translations/sw_translations.py +3897 -0
  267. setiastro/saspro/translations/uk_translations.py +3929 -0
  268. setiastro/saspro/translations/zh_translations.py +3910 -0
  269. setiastro/saspro/versioning.py +77 -0
  270. setiastro/saspro/view_bundle.py +20 -17
  271. setiastro/saspro/wavescale_hdr.py +54 -33
  272. setiastro/saspro/wavescale_hdr_preset.py +6 -5
  273. setiastro/saspro/wavescalede.py +54 -31
  274. setiastro/saspro/wavescalede_preset.py +9 -7
  275. setiastro/saspro/whitebalance.py +58 -22
  276. setiastro/saspro/widgets/common_utilities.py +12 -11
  277. setiastro/saspro/widgets/minigame/game.js +991 -0
  278. setiastro/saspro/widgets/minigame/index.html +53 -0
  279. setiastro/saspro/widgets/minigame/style.css +241 -0
  280. setiastro/saspro/widgets/preview_dialogs.py +8 -8
  281. setiastro/saspro/widgets/resource_monitor.py +263 -0
  282. setiastro/saspro/widgets/spinboxes.py +18 -0
  283. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  284. setiastro/saspro/wimi.py +7996 -0
  285. setiastro/saspro/wims.py +578 -0
  286. setiastro/saspro/window_shelf.py +2 -2
  287. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
  288. setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
  289. setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
  290. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
  291. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
  292. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
  293. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/license.txt +0 -0
@@ -220,7 +220,10 @@ class NBtoRGBStars(QWidget):
220
220
  if img.ndim == 3: img = img[...,0]
221
221
  setattr(self, which.lower(), self._as_float01(img))
222
222
  else: # OSC
223
- if img.ndim == 2: img = np.stack([img]*3, axis=-1)
223
+ # Optimization: Store mono OSC as-is (2D) to save memory
224
+ # The combine step will handle expansion.
225
+ if img.ndim == 3 and img.shape[2] == 1:
226
+ img = img[..., 0]
224
227
  setattr(self, which.lower(), self._as_float01(img))
225
228
 
226
229
  setattr(self, f"_file_{which.lower()}", path)
@@ -309,7 +312,14 @@ class NBtoRGBStars(QWidget):
309
312
  raise ValueError(f"Channel sizes differ: {set(shapes)}")
310
313
 
311
314
  if self.osc is not None:
312
- r = self.osc[...,0]; g = self.osc[...,1]; b = self.osc[...,2]
315
+ if self.osc.ndim == 2:
316
+ r = self.osc; g = self.osc; b = self.osc
317
+ elif self.osc.ndim == 3 and self.osc.shape[2] >= 3:
318
+ r = self.osc[...,0]; g = self.osc[...,1]; b = self.osc[...,2]
319
+ else:
320
+ # fallback for unexpected shapes (e.g. 3D but 1-channel)
321
+ r = self.osc.squeeze(); g = r; b = r
322
+
313
323
  sii = self.sii if self.sii is not None else r
314
324
  ha = self.ha if self.ha is not None else r
315
325
  oiii= self.oiii if self.oiii is not None else b
@@ -1,4 +1,4 @@
1
- import numpy as np
1
+ import numpy as np
2
2
  from numba import njit, prange
3
3
  import cv2
4
4
  import math
@@ -584,39 +584,55 @@ def kappa_sigma_clip_weighted_3d(stack, weights, kappa=2.5, iterations=3):
584
584
  pixel_weights = weights[:]
585
585
  else:
586
586
  pixel_weights = weights[:, i, j].copy()
587
- # Initialize tracking of indices
588
- current_idx = np.empty(num_frames, dtype=np.int64)
589
- for f in range(num_frames):
590
- current_idx[f] = f
591
- current_vals = pixel_values
592
- current_w = pixel_weights
593
- current_indices = current_idx
587
+
588
+ # Use boolean mask instead of tracking indices
589
+ valid_mask = pixel_values != 0
590
+
594
591
  med = 0.0
595
592
  for _ in range(iterations):
596
- if current_vals.size == 0:
593
+ # Count valid pixels
594
+ count = 0
595
+ for k in range(num_frames):
596
+ if valid_mask[k]:
597
+ count += 1
598
+
599
+ if count == 0:
597
600
  break
601
+
602
+ # Extract valid values for stats (this allocation is unavoidable but smaller/temp)
603
+ # In numba this usually lowers to efficient code if we avoid 'np.empty' overhead inside loops
604
+ # but pure mask operations are often faster.
605
+ # However, for median/std we need the compacted array.
606
+ current_vals = pixel_values[valid_mask]
607
+
598
608
  med = np.median(current_vals)
599
609
  std = np.std(current_vals)
600
610
  lower_bound = med - kappa * std
601
611
  upper_bound = med + kappa * std
602
- valid = (current_vals != 0) & (current_vals >= lower_bound) & (current_vals <= upper_bound)
603
- current_vals = current_vals[valid]
604
- current_w = current_w[valid]
605
- current_indices = current_indices[valid]
606
- # Mark rejected: frames not in current_indices are rejected.
612
+
613
+ # Update mask: must be valid AND within bounds
614
+ for k in range(num_frames):
615
+ if valid_mask[k]:
616
+ val = pixel_values[k]
617
+ if val < lower_bound or val > upper_bound:
618
+ valid_mask[k] = False
619
+
620
+ # Fill rejection mask
607
621
  for f in range(num_frames):
608
- # Check if f is in current_indices
609
- found = False
610
- for k in range(current_indices.size):
611
- if current_indices[k] == f:
612
- found = True
613
- break
614
- if not found:
615
- rej_mask[f, i, j] = True
616
- else:
617
- rej_mask[f, i, j] = False
618
- if current_w.size > 0 and current_w.sum() > 0:
619
- clipped[i, j] = np.sum(current_vals * current_w) / current_w.sum()
622
+ rej_mask[f, i, j] = not valid_mask[f]
623
+
624
+ # Compute weighted mean of final valid pixels
625
+ wsum = 0.0
626
+ vsum = 0.0
627
+ for k in range(num_frames):
628
+ if valid_mask[k]:
629
+ w = pixel_weights[k]
630
+ v = pixel_values[k]
631
+ wsum += w
632
+ vsum += v * w
633
+
634
+ if wsum > 0:
635
+ clipped[i, j] = vsum / wsum
620
636
  else:
621
637
  clipped[i, j] = med
622
638
  return clipped, rej_mask
@@ -641,36 +657,46 @@ def kappa_sigma_clip_weighted_4d(stack, weights, kappa=2.5, iterations=3):
641
657
  pixel_weights = weights[:]
642
658
  else:
643
659
  pixel_weights = weights[:, i, j, c].copy()
644
- current_idx = np.empty(num_frames, dtype=np.int64)
645
- for f in range(num_frames):
646
- current_idx[f] = f
647
- current_vals = pixel_values
648
- current_w = pixel_weights
649
- current_indices = current_idx
660
+
661
+ valid_mask = pixel_values != 0
662
+
650
663
  med = 0.0
651
664
  for _ in range(iterations):
652
- if current_vals.size == 0:
665
+ count = 0
666
+ for k in range(num_frames):
667
+ if valid_mask[k]:
668
+ count += 1
669
+
670
+ if count == 0:
653
671
  break
672
+
673
+ current_vals = pixel_values[valid_mask]
674
+
654
675
  med = np.median(current_vals)
655
676
  std = np.std(current_vals)
656
677
  lower_bound = med - kappa * std
657
678
  upper_bound = med + kappa * std
658
- valid = (current_vals != 0) & (current_vals >= lower_bound) & (current_vals <= upper_bound)
659
- current_vals = current_vals[valid]
660
- current_w = current_w[valid]
661
- current_indices = current_indices[valid]
679
+
680
+ for k in range(num_frames):
681
+ if valid_mask[k]:
682
+ val = pixel_values[k]
683
+ if val < lower_bound or val > upper_bound:
684
+ valid_mask[k] = False
685
+
662
686
  for f in range(num_frames):
663
- found = False
664
- for k in range(current_indices.size):
665
- if current_indices[k] == f:
666
- found = True
667
- break
668
- if not found:
669
- rej_mask[f, i, j, c] = True
670
- else:
671
- rej_mask[f, i, j, c] = False
672
- if current_w.size > 0 and current_w.sum() > 0:
673
- clipped[i, j, c] = np.sum(current_vals * current_w) / current_w.sum()
687
+ rej_mask[f, i, j, c] = not valid_mask[f]
688
+
689
+ wsum = 0.0
690
+ vsum = 0.0
691
+ for k in range(num_frames):
692
+ if valid_mask[k]:
693
+ w = pixel_weights[k]
694
+ v = pixel_values[k]
695
+ wsum += w
696
+ vsum += v * w
697
+
698
+ if wsum > 0:
699
+ clipped[i, j, c] = vsum / wsum
674
700
  else:
675
701
  clipped[i, j, c] = med
676
702
  return clipped, rej_mask
@@ -3042,3 +3068,78 @@ def fast_star_detect(image,
3042
3068
  return np.empty((0,2), dtype=np.float32)
3043
3069
  else:
3044
3070
  return np.array(star_positions, dtype=np.float32)
3071
+
3072
+
3073
+ @njit(fastmath=True, cache=True)
3074
+ def gradient_descent_to_dim_spot_numba(gray_small, start_x, start_y, patch_size):
3075
+ """
3076
+ Numba implementation of _gradient_descent_to_dim_spot.
3077
+ Walks to the local minimum (median-of-patch) around (start_x, start_y).
3078
+ gray_small: 2D float32 array
3079
+ """
3080
+ H, W = gray_small.shape
3081
+ half = patch_size // 2
3082
+
3083
+ cx = int(min(max(start_x, 0), W - 1))
3084
+ cy = int(min(max(start_y, 0), H - 1))
3085
+
3086
+ # Helper to compute patch median manually or efficiently
3087
+ # Numba supports np.median on arrays, but slicing inside a loop can be costly.
3088
+ # However, for small patches (e.g. 15x15), it should be okay.
3089
+
3090
+ for _ in range(60):
3091
+ # Current value
3092
+ x0 = max(0, cx - half)
3093
+ x1 = min(W, cx + half + 1)
3094
+ y0 = max(0, cy - half)
3095
+ y1 = min(H, cy + half + 1)
3096
+ sub = gray_small[y0:y1, x0:x1].flatten()
3097
+ if sub.size == 0:
3098
+ cur_val = 1e9 # Should not happen
3099
+ else:
3100
+ cur_val = np.median(sub)
3101
+
3102
+ best_x, best_y = cx, cy
3103
+ best_val = cur_val
3104
+
3105
+ # 3x3 search
3106
+ changed = False
3107
+
3108
+ # Unroll for strict 3x3 neighborhood
3109
+ for dy in range(-1, 2):
3110
+ for dx in range(-1, 2):
3111
+ if dx == 0 and dy == 0:
3112
+ continue
3113
+
3114
+ nx = cx + dx
3115
+ ny = cy + dy
3116
+
3117
+ if nx < 0 or ny < 0 or nx >= W or ny >= H:
3118
+ continue
3119
+
3120
+ # Compute median for neighbor
3121
+ nx0 = max(0, nx - half)
3122
+ nx1 = min(W, nx + half + 1)
3123
+ ny0 = max(0, ny - half)
3124
+ ny1 = min(H, ny + half + 1)
3125
+
3126
+ # In Numba, median on a slice creates a copy.
3127
+ # For small patches this is acceptable given the huge speedup vs Python interpreter overhead.
3128
+ n_sub = gray_small[ny0:ny1, nx0:nx1].flatten()
3129
+ if n_sub.size == 0:
3130
+ val = 1e9
3131
+ else:
3132
+ val = np.median(n_sub)
3133
+
3134
+ if val < best_val:
3135
+ best_val = val
3136
+ best_x = nx
3137
+ best_y = ny
3138
+ changed = True
3139
+
3140
+ if not changed:
3141
+ break
3142
+
3143
+ cx, cy = best_x, best_y
3144
+
3145
+ return cx, cy
@@ -644,13 +644,33 @@ class ScriptManager(QObject):
644
644
 
645
645
  # ---- loading ----
646
646
  def load_registry(self):
647
- migrate_old_scripts_if_needed()
647
+ """
648
+ Discover scripts recursively under SASpro/scripts, load them, and build registry.
649
+ Skips __pycache__, hidden/underscore-prefixed files, and __init__.py.
650
+ """
651
+ migrate_old_scripts_if_needed()
648
652
  scripts_dir = get_scripts_dir()
649
653
  self.registry = []
650
654
 
651
- for path in sorted(scripts_dir.glob("*.py")):
655
+ try:
656
+ candidates = sorted(scripts_dir.rglob("*.py"))
657
+ except Exception:
658
+ candidates = []
659
+
660
+ for path in candidates:
661
+ # Skip pycache anywhere in path
662
+ parts_l = {p.lower() for p in path.parts}
663
+ if "__pycache__" in parts_l:
664
+ continue
665
+
666
+ # Skip hidden/private python files and package init
667
+ if path.name == "__init__.py":
668
+ continue
669
+ if path.name.startswith((".", "_")):
670
+ continue
671
+
652
672
  try:
653
- entry = self._load_one_script(path)
673
+ entry = self._load_one_script(path, scripts_dir)
654
674
  if entry:
655
675
  self.registry.append(entry)
656
676
  except Exception:
@@ -658,8 +678,18 @@ class ScriptManager(QObject):
658
678
 
659
679
  self._log(f"[Scripts] Loaded {len(self.registry)} script(s) from {scripts_dir}")
660
680
 
661
- def _load_one_script(self, path: Path) -> ScriptEntry | None:
662
- # Make a unique module name so reload actually reloads
681
+
682
+ def _load_one_script(self, path: Path, scripts_root: Path) -> ScriptEntry | None:
683
+ """
684
+ Load a single user script from disk.
685
+
686
+ - Creates a unique module name based on mtime so reload picks up changes.
687
+ - Imports module.
688
+ - Determines stable script_id (prefer SCRIPT_ID in module, else persisted id).
689
+ - Pulls metadata: SCRIPT_NAME/GROUP/SHORTCUT.
690
+ Group defaults to relative folder under scripts_root.
691
+ """
692
+ # Unique module name so reloading actually re-imports
663
693
  try:
664
694
  mtime_ns = path.stat().st_mtime_ns
665
695
  except Exception:
@@ -671,7 +701,8 @@ class ScriptManager(QObject):
671
701
  return None
672
702
 
673
703
  mod = importlib.util.module_from_spec(spec)
674
- script_id = self._script_id_for_path(path, mod)
704
+
705
+ # Import module first (so SCRIPT_ID / metadata exists)
675
706
  try:
676
707
  spec.loader.exec_module(mod) # type: ignore
677
708
  except Exception:
@@ -687,7 +718,7 @@ class ScriptManager(QObject):
687
718
  self._log(f"[Scripts] {path.name} has no run(ctx) or main(ctx); skipping.")
688
719
  return None
689
720
 
690
- # ---- metadata: allow CAPS or lowercase ----
721
+ # ---- helper: allow CAPS or lowercase ----
691
722
  def _pick(*names, default=None):
692
723
  for n in names:
693
724
  if hasattr(mod, n):
@@ -695,11 +726,23 @@ class ScriptManager(QObject):
695
726
  return default
696
727
 
697
728
  name = _pick("SCRIPT_NAME", "script_name", default=path.stem)
698
- group = _pick("SCRIPT_GROUP", "script_group", default="")
729
+
730
+ # Prefer explicit group; else derive group from relative folder
731
+ group = _pick("SCRIPT_GROUP", "script_group", default=None)
732
+ if group is None or not str(group).strip():
733
+ try:
734
+ rel_parent = path.parent.relative_to(scripts_root)
735
+ group = "" if str(rel_parent) in ("", ".") else rel_parent.as_posix()
736
+ except Exception:
737
+ group = ""
738
+
699
739
  shortcut = _pick("SCRIPT_SHORTCUT", "script_shortcut", default=None)
700
740
 
741
+ # Stable script id (prefer explicit SCRIPT_ID; else persisted by rel-path)
742
+ script_id = self._script_id_for_path(path, scripts_root, mod)
743
+
701
744
  entry = ScriptEntry(
702
- script_id=script_id,
745
+ script_id=str(script_id),
703
746
  path=path,
704
747
  name=str(name),
705
748
  group=str(group or ""),
@@ -710,6 +753,7 @@ class ScriptManager(QObject):
710
753
  return entry
711
754
 
712
755
 
756
+
713
757
  # ---- menu wiring ----
714
758
  def rebuild_menu(self, menu_scripts):
715
759
  """
@@ -1394,15 +1438,31 @@ def run(ctx):
1394
1438
  self._log(f"[Scripts] Failed to write sample script:\n{traceback.format_exc()}")
1395
1439
 
1396
1440
 
1397
- def _script_id_for_path(self, path: Path, mod) -> str:
1398
- # 1) Prefer explicit SCRIPT_ID in the script file (best, survives renames)
1399
- sid = getattr(mod, "SCRIPT_ID", None) or getattr(mod, "script_id", None)
1400
- if isinstance(sid, str) and sid.strip():
1401
- return sid.strip()
1441
+ def _script_id_for_path(self, path: Path, scripts_root: Path, mod=None) -> str:
1442
+ """
1443
+ Determine a stable script_id.
1444
+
1445
+ Priority:
1446
+ 1) SCRIPT_ID / script_id defined in the script (best; survives renames/moves)
1447
+ 2) Persisted id in QSettings keyed by *relative path inside scripts_root*
1448
+ (stable across machines if folder structure is same)
1449
+
1450
+ NOTE: We intentionally DO NOT key by absolute path.
1451
+ """
1452
+ # 1) Prefer explicit SCRIPT_ID in the script file (best)
1453
+ if mod is not None:
1454
+ sid = getattr(mod, "SCRIPT_ID", None) or getattr(mod, "script_id", None)
1455
+ if isinstance(sid, str) and sid.strip():
1456
+ return sid.strip()
1457
+
1458
+ # 2) Persist per-relative-path (not absolute)
1459
+ try:
1460
+ rel = path.relative_to(scripts_root).as_posix()
1461
+ except Exception:
1462
+ rel = path.as_posix()
1402
1463
 
1403
- # 2) Fallback: persist per-path in QSettings (ok for existing scripts)
1404
1464
  s = QSettings()
1405
- key = f"Scripts/ids/{str(path)}"
1465
+ key = f"Scripts/ids_rel/{rel}"
1406
1466
  sid = s.value(key, "", type=str) or ""
1407
1467
  if sid:
1408
1468
  return sid
@@ -1410,4 +1470,4 @@ def run(ctx):
1410
1470
  sid = uuid.uuid4().hex
1411
1471
  s.setValue(key, sid)
1412
1472
  s.sync()
1413
- return sid
1473
+ return sid