setiastrosuitepro 1.6.2__py3-none-any.whl → 1.6.12__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 (162) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/acv_icon.png +0 -0
  3. setiastro/images/colorwheel.svg +97 -0
  4. setiastro/images/cosmic.svg +40 -0
  5. setiastro/images/cosmicsat.svg +24 -0
  6. setiastro/images/first_quarter.png +0 -0
  7. setiastro/images/full_moon.png +0 -0
  8. setiastro/images/graxpert.svg +19 -0
  9. setiastro/images/last_quarter.png +0 -0
  10. setiastro/images/linearfit.svg +32 -0
  11. setiastro/images/new_moon.png +0 -0
  12. setiastro/images/pixelmath.svg +42 -0
  13. setiastro/images/rotatearbitrary.png +0 -0
  14. setiastro/images/waning_crescent_1.png +0 -0
  15. setiastro/images/waning_crescent_2.png +0 -0
  16. setiastro/images/waning_crescent_3.png +0 -0
  17. setiastro/images/waning_crescent_4.png +0 -0
  18. setiastro/images/waning_crescent_5.png +0 -0
  19. setiastro/images/waning_gibbous_1.png +0 -0
  20. setiastro/images/waning_gibbous_2.png +0 -0
  21. setiastro/images/waning_gibbous_3.png +0 -0
  22. setiastro/images/waning_gibbous_4.png +0 -0
  23. setiastro/images/waning_gibbous_5.png +0 -0
  24. setiastro/images/waxing_crescent_1.png +0 -0
  25. setiastro/images/waxing_crescent_2.png +0 -0
  26. setiastro/images/waxing_crescent_3.png +0 -0
  27. setiastro/images/waxing_crescent_4.png +0 -0
  28. setiastro/images/waxing_crescent_5.png +0 -0
  29. setiastro/images/waxing_gibbous_1.png +0 -0
  30. setiastro/images/waxing_gibbous_2.png +0 -0
  31. setiastro/images/waxing_gibbous_3.png +0 -0
  32. setiastro/images/waxing_gibbous_4.png +0 -0
  33. setiastro/images/waxing_gibbous_5.png +0 -0
  34. setiastro/qml/ResourceMonitor.qml +84 -82
  35. setiastro/saspro/__main__.py +20 -1
  36. setiastro/saspro/_generated/build_info.py +2 -2
  37. setiastro/saspro/abe.py +37 -4
  38. setiastro/saspro/aberration_ai.py +237 -21
  39. setiastro/saspro/acv_exporter.py +379 -0
  40. setiastro/saspro/add_stars.py +33 -6
  41. setiastro/saspro/backgroundneutral.py +114 -37
  42. setiastro/saspro/blemish_blaster.py +4 -1
  43. setiastro/saspro/blink_comparator_pro.py +548 -275
  44. setiastro/saspro/clahe.py +4 -1
  45. setiastro/saspro/continuum_subtract.py +4 -1
  46. setiastro/saspro/convo.py +13 -7
  47. setiastro/saspro/cosmicclarity.py +129 -18
  48. setiastro/saspro/crop_dialog_pro.py +134 -8
  49. setiastro/saspro/curve_editor_pro.py +109 -42
  50. setiastro/saspro/doc_manager.py +246 -16
  51. setiastro/saspro/exoplanet_detector.py +120 -28
  52. setiastro/saspro/frequency_separation.py +1158 -204
  53. setiastro/saspro/function_bundle.py +16 -16
  54. setiastro/saspro/ghs_dialog_pro.py +81 -16
  55. setiastro/saspro/graxpert.py +1 -0
  56. setiastro/saspro/gui/main_window.py +519 -289
  57. setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
  58. setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
  59. setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
  60. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  61. setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
  62. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  63. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  64. setiastro/saspro/halobgon.py +4 -0
  65. setiastro/saspro/histogram.py +5 -1
  66. setiastro/saspro/image_combine.py +4 -0
  67. setiastro/saspro/image_peeker_pro.py +4 -0
  68. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  69. setiastro/saspro/imageops/stretch.py +582 -62
  70. setiastro/saspro/isophote.py +4 -0
  71. setiastro/saspro/layers.py +13 -9
  72. setiastro/saspro/layers_dock.py +183 -3
  73. setiastro/saspro/legacy/image_manager.py +154 -20
  74. setiastro/saspro/legacy/numba_utils.py +67 -47
  75. setiastro/saspro/legacy/xisf.py +240 -98
  76. setiastro/saspro/live_stacking.py +180 -79
  77. setiastro/saspro/luminancerecombine.py +228 -27
  78. setiastro/saspro/mask_creation.py +174 -15
  79. setiastro/saspro/mfdeconv.py +113 -35
  80. setiastro/saspro/mfdeconvcudnn.py +119 -70
  81. setiastro/saspro/mfdeconvsport.py +112 -35
  82. setiastro/saspro/morphology.py +4 -0
  83. setiastro/saspro/multiscale_decomp.py +748 -255
  84. setiastro/saspro/numba_utils.py +72 -57
  85. setiastro/saspro/ops/commands.py +18 -18
  86. setiastro/saspro/ops/script_editor.py +10 -2
  87. setiastro/saspro/ops/scripts.py +122 -0
  88. setiastro/saspro/perfect_palette_picker.py +37 -3
  89. setiastro/saspro/plate_solver.py +84 -49
  90. setiastro/saspro/psf_viewer.py +119 -37
  91. setiastro/saspro/remove_stars_preset.py +55 -13
  92. setiastro/saspro/resources.py +97 -11
  93. setiastro/saspro/rgbalign.py +4 -0
  94. setiastro/saspro/selective_color.py +83 -21
  95. setiastro/saspro/sfcc.py +364 -152
  96. setiastro/saspro/shortcuts.py +253 -49
  97. setiastro/saspro/signature_insert.py +692 -33
  98. setiastro/saspro/stacking_suite.py +1610 -574
  99. setiastro/saspro/star_alignment.py +522 -453
  100. setiastro/saspro/star_spikes.py +4 -0
  101. setiastro/saspro/star_stretch.py +38 -3
  102. setiastro/saspro/stat_stretch.py +743 -128
  103. setiastro/saspro/status_log_dock.py +1 -1
  104. setiastro/saspro/subwindow.py +786 -360
  105. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  106. setiastro/saspro/swap_manager.py +77 -42
  107. setiastro/saspro/translations/all_source_strings.json +1588 -516
  108. setiastro/saspro/translations/ar_translations.py +915 -684
  109. setiastro/saspro/translations/de_translations.py +442 -463
  110. setiastro/saspro/translations/es_translations.py +277 -47
  111. setiastro/saspro/translations/fr_translations.py +279 -47
  112. setiastro/saspro/translations/hi_translations.py +253 -21
  113. setiastro/saspro/translations/integrate_translations.py +3 -2
  114. setiastro/saspro/translations/it_translations.py +1211 -161
  115. setiastro/saspro/translations/ja_translations.py +3340 -3107
  116. setiastro/saspro/translations/pt_translations.py +3315 -3337
  117. setiastro/saspro/translations/ru_translations.py +351 -117
  118. setiastro/saspro/translations/saspro_ar.qm +0 -0
  119. setiastro/saspro/translations/saspro_ar.ts +15902 -138
  120. setiastro/saspro/translations/saspro_de.qm +0 -0
  121. setiastro/saspro/translations/saspro_de.ts +14428 -133
  122. setiastro/saspro/translations/saspro_es.qm +0 -0
  123. setiastro/saspro/translations/saspro_es.ts +11503 -7821
  124. setiastro/saspro/translations/saspro_fr.qm +0 -0
  125. setiastro/saspro/translations/saspro_fr.ts +11168 -7812
  126. setiastro/saspro/translations/saspro_hi.qm +0 -0
  127. setiastro/saspro/translations/saspro_hi.ts +14733 -135
  128. setiastro/saspro/translations/saspro_it.qm +0 -0
  129. setiastro/saspro/translations/saspro_it.ts +14347 -7821
  130. setiastro/saspro/translations/saspro_ja.qm +0 -0
  131. setiastro/saspro/translations/saspro_ja.ts +14860 -137
  132. setiastro/saspro/translations/saspro_pt.qm +0 -0
  133. setiastro/saspro/translations/saspro_pt.ts +14904 -137
  134. setiastro/saspro/translations/saspro_ru.qm +0 -0
  135. setiastro/saspro/translations/saspro_ru.ts +11766 -168
  136. setiastro/saspro/translations/saspro_sw.qm +0 -0
  137. setiastro/saspro/translations/saspro_sw.ts +15115 -135
  138. setiastro/saspro/translations/saspro_uk.qm +0 -0
  139. setiastro/saspro/translations/saspro_uk.ts +11206 -6729
  140. setiastro/saspro/translations/saspro_zh.qm +0 -0
  141. setiastro/saspro/translations/saspro_zh.ts +10581 -7812
  142. setiastro/saspro/translations/sw_translations.py +282 -56
  143. setiastro/saspro/translations/uk_translations.py +264 -35
  144. setiastro/saspro/translations/zh_translations.py +282 -47
  145. setiastro/saspro/view_bundle.py +17 -17
  146. setiastro/saspro/wavescale_hdr.py +4 -1
  147. setiastro/saspro/wavescalede.py +4 -1
  148. setiastro/saspro/whitebalance.py +84 -12
  149. setiastro/saspro/widgets/common_utilities.py +28 -21
  150. setiastro/saspro/widgets/minigame/game.js +11 -6
  151. setiastro/saspro/widgets/resource_monitor.py +133 -57
  152. setiastro/saspro/widgets/spinboxes.py +28 -13
  153. setiastro/saspro/wimi.py +92 -721
  154. setiastro/saspro/wims.py +46 -36
  155. setiastro/saspro/window_shelf.py +2 -2
  156. setiastro/saspro/xisf.py +101 -11
  157. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
  158. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
  159. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
  160. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
  161. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
  162. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/wimi.py CHANGED
@@ -2596,12 +2596,12 @@ class WIMIDialog(QDialog):
2596
2596
  button_layout = QHBoxLayout()
2597
2597
 
2598
2598
  # Load button
2599
- self.load_button = QPushButton("Load Image File")
2599
+ self.load_button = QPushButton(self.tr("Load Image File"))
2600
2600
  self.load_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogStart))
2601
2601
  self.load_button.clicked.connect(self.open_image)
2602
2602
 
2603
2603
  self.load_from_view_btn = QToolButton()
2604
- self.load_from_view_btn.setText("Load from View")
2604
+ self.load_from_view_btn.setText(self.tr("Load from View"))
2605
2605
  self.load_from_view_btn.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
2606
2606
  self.load_from_view_btn.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DirOpenIcon))
2607
2607
  self.load_from_view_menu = QMenu(self)
@@ -2611,7 +2611,7 @@ class WIMIDialog(QDialog):
2611
2611
  button_layout.addWidget(self.load_from_view_btn)
2612
2612
 
2613
2613
  # AutoStretch button
2614
- self.auto_stretch_button = QPushButton("AutoStretch")
2614
+ self.auto_stretch_button = QPushButton(self.tr("AutoStretch"))
2615
2615
  self.auto_stretch_button.clicked.connect(self.toggle_autostretch)
2616
2616
 
2617
2617
  # Add both buttons to the horizontal layout
@@ -2622,7 +2622,7 @@ class WIMIDialog(QDialog):
2622
2622
  left_panel.addLayout(button_layout)
2623
2623
 
2624
2624
  # Create the instruction QLabel for search region
2625
- search_region_instruction_label = QLabel("Shift+Click to define a search region")
2625
+ search_region_instruction_label = QLabel(self.tr("Shift+Click to define a search region"))
2626
2626
  search_region_instruction_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
2627
2627
  search_region_instruction_label.setStyleSheet("font-size: 15px; color: gray;")
2628
2628
 
@@ -2632,12 +2632,12 @@ class WIMIDialog(QDialog):
2632
2632
 
2633
2633
 
2634
2634
  # Query Simbad button
2635
- self.query_button = QPushButton("Query Simbad")
2635
+ self.query_button = QPushButton(self.tr("Query Simbad"))
2636
2636
  self.query_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogApplyButton))
2637
2637
  left_panel.addWidget(self.query_button)
2638
2638
  self.query_button.clicked.connect(lambda: self.query_simbad(self.get_defined_radius()))
2639
2639
 
2640
- self.legend_button = QPushButton("Legend")
2640
+ self.legend_button = QPushButton(self.tr("Legend"))
2641
2641
  self.legend_button.clicked.connect(self.show_legend)
2642
2642
  left_panel.addWidget(self.legend_button)
2643
2643
 
@@ -2645,12 +2645,12 @@ class WIMIDialog(QDialog):
2645
2645
  show_clear_layout = QHBoxLayout()
2646
2646
 
2647
2647
  # Create the Show Object Names checkbox
2648
- self.show_names_checkbox = QCheckBox("Show Object Names")
2648
+ self.show_names_checkbox = QCheckBox(self.tr("Show Object Names"))
2649
2649
  self.show_names_checkbox.stateChanged.connect(self.toggle_object_names) # Connect to a function to toggle names
2650
2650
  show_clear_layout.addWidget(self.show_names_checkbox)
2651
2651
 
2652
2652
  # Create the Clear Results button
2653
- self.clear_results_button = QPushButton("Clear Results")
2653
+ self.clear_results_button = QPushButton(self.tr("Clear Results"))
2654
2654
  self.clear_results_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogCloseButton))
2655
2655
  self.clear_results_button.clicked.connect(self.clear_search_results) # Connect to a function to clear results
2656
2656
  show_clear_layout.addWidget(self.clear_results_button)
@@ -2662,7 +2662,7 @@ class WIMIDialog(QDialog):
2662
2662
  button_layout = QHBoxLayout()
2663
2663
 
2664
2664
  # Show Visible Objects Only button
2665
- self.toggle_visible_objects_button = QPushButton("Show Visible Objects Only")
2665
+ self.toggle_visible_objects_button = QPushButton(self.tr("Show Visible Objects Only"))
2666
2666
  self.toggle_visible_objects_button.setCheckable(True) # Toggle button state
2667
2667
  self.toggle_visible_objects_button.setIcon(QIcon(eye_icon_path))
2668
2668
  self.toggle_visible_objects_button.clicked.connect(self.filter_visible_objects)
@@ -2670,7 +2670,7 @@ class WIMIDialog(QDialog):
2670
2670
  button_layout.addWidget(self.toggle_visible_objects_button)
2671
2671
 
2672
2672
  # Save CSV button
2673
- self.save_csv_button = QPushButton("Save CSV")
2673
+ self.save_csv_button = QPushButton(self.tr("Save CSV"))
2674
2674
  self.save_csv_button.setIcon(QIcon(csv_icon_path))
2675
2675
  self.save_csv_button.clicked.connect(self.save_results_as_csv)
2676
2676
  button_layout.addWidget(self.save_csv_button)
@@ -2679,7 +2679,7 @@ class WIMIDialog(QDialog):
2679
2679
  left_panel.addLayout(button_layout)
2680
2680
 
2681
2681
  # Advanced Search Button
2682
- self.advanced_search_button = QPushButton("Advanced Search")
2682
+ self.advanced_search_button = QPushButton(self.tr("Advanced Search"))
2683
2683
  self.advanced_search_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogDetailedView))
2684
2684
  self.advanced_search_button.setCheckable(True)
2685
2685
  self.advanced_search_button.clicked.connect(self.toggle_advanced_search)
@@ -2693,14 +2693,14 @@ class WIMIDialog(QDialog):
2693
2693
  self.advanced_search_panel_widget.setVisible(False) # Hide initially
2694
2694
 
2695
2695
  # Status label
2696
- self.status_label = QLabel("Status: Ready")
2696
+ self.status_label = QLabel(self.tr("Status: Ready"))
2697
2697
  left_panel.addWidget(self.status_label)
2698
2698
 
2699
2699
  # Create a horizontal layout
2700
2700
  button_layout = QHBoxLayout()
2701
2701
 
2702
2702
  # Copy RA/Dec to Clipboard button
2703
- self.copy_button = QPushButton("Copy RA/Dec to Clipboard", self)
2703
+ self.copy_button = QPushButton(self.tr("Copy RA/Dec to Clipboard"), self)
2704
2704
  self.copy_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_CommandLink))
2705
2705
  self.copy_button.clicked.connect(self.copy_ra_dec_to_clipboard)
2706
2706
  button_layout.addWidget(self.copy_button)
@@ -2715,23 +2715,23 @@ class WIMIDialog(QDialog):
2715
2715
  left_panel.addLayout(button_layout)
2716
2716
 
2717
2717
  # Save Plate Solved Fits Button
2718
- self.save_plate_solved_button = QPushButton("Save Plate Solved Fits")
2718
+ self.save_plate_solved_button = QPushButton(self.tr("Save Plate Solved Fits"))
2719
2719
  self.save_plate_solved_button.setIcon(QIcon(disk_icon_path))
2720
2720
  self.save_plate_solved_button.clicked.connect(self.save_plate_solved_fits)
2721
2721
  left_panel.addWidget(self.save_plate_solved_button)
2722
2722
 
2723
2723
  # RA/Dec Labels
2724
2724
  ra_dec_layout = QHBoxLayout()
2725
- self.ra_label = QLabel("RA: N/A")
2726
- self.dec_label = QLabel("Dec: N/A")
2727
- self.orientation_label = QLabel("Orientation: N/A°")
2725
+ self.ra_label = QLabel(self.tr("RA: N/A"))
2726
+ self.dec_label = QLabel(self.tr("Dec: N/A"))
2727
+ self.orientation_label = QLabel(self.tr("Orientation: N/A°"))
2728
2728
  ra_dec_layout.addWidget(self.ra_label)
2729
2729
  ra_dec_layout.addWidget(self.dec_label)
2730
2730
  ra_dec_layout.addWidget(self.orientation_label)
2731
2731
  left_panel.addLayout(ra_dec_layout)
2732
2732
 
2733
2733
  # Mini Preview
2734
- self.mini_preview = QLabel("Mini Preview")
2734
+ self.mini_preview = QLabel(self.tr("Mini Preview"))
2735
2735
  self.mini_preview.setMaximumSize(300, 300)
2736
2736
  self.mini_preview.mousePressEvent = self.on_mini_preview_press
2737
2737
  self.mini_preview.mouseMoveEvent = self.on_mini_preview_drag
@@ -2746,9 +2746,9 @@ class WIMIDialog(QDialog):
2746
2746
 
2747
2747
  # Zoom buttons above the main preview
2748
2748
  zoom_controls_layout = QHBoxLayout()
2749
- self.zoom_in_button = QPushButton("Zoom In")
2749
+ self.zoom_in_button = QPushButton(self.tr("Zoom In"))
2750
2750
  self.zoom_in_button.clicked.connect(self.zoom_in)
2751
- self.zoom_out_button = QPushButton("Zoom Out")
2751
+ self.zoom_out_button = QPushButton(self.tr("Zoom Out"))
2752
2752
  self.zoom_out_button.clicked.connect(self.zoom_out)
2753
2753
  zoom_controls_layout.addWidget(self.zoom_in_button)
2754
2754
  zoom_controls_layout.addWidget(self.zoom_out_button)
@@ -2783,28 +2783,28 @@ class WIMIDialog(QDialog):
2783
2783
  save_buttons_layout = QHBoxLayout()
2784
2784
 
2785
2785
  # Button to toggle annotation tools section
2786
- self.show_annotations_button = QPushButton("Show Annotation Tools")
2786
+ self.show_annotations_button = QPushButton(self.tr("Show Annotation Tools"))
2787
2787
  self.show_annotations_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogResetButton))
2788
2788
  self.show_annotations_button.clicked.connect(self.toggle_annotation_tools)
2789
2789
  save_buttons_layout.addWidget(self.show_annotations_button)
2790
2790
 
2791
- self.save_annotated_button = QPushButton("Save Annotated Image")
2791
+ self.save_annotated_button = QPushButton(self.tr("Save Annotated Image"))
2792
2792
  self.save_annotated_button.setIcon(QIcon(annotated_path))
2793
2793
  self.save_annotated_button.clicked.connect(self.save_annotated_image)
2794
2794
  save_buttons_layout.addWidget(self.save_annotated_button)
2795
2795
 
2796
- self.save_collage_button = QPushButton("Save Collage of Objects")
2796
+ self.save_collage_button = QPushButton(self.tr("Save Collage of Objects"))
2797
2797
  self.save_collage_button.setIcon(QIcon(collage_path))
2798
2798
  self.save_collage_button.clicked.connect(self.save_collage_of_objects)
2799
2799
  save_buttons_layout.addWidget(self.save_collage_button)
2800
2800
 
2801
2801
  # New 3D View Button
2802
- self.show_3d_view_button = QPushButton("3D Distance Model")
2802
+ self.show_3d_view_button = QPushButton(self.tr("3D Distance Model"))
2803
2803
  self.show_3d_view_button.clicked.connect(self.show_3d_model_view)
2804
2804
  self.show_3d_view_button.setIcon( QApplication.style().standardIcon(QStyle.StandardPixmap.SP_TitleBarNormalButton))
2805
2805
  save_buttons_layout.addWidget(self.show_3d_view_button)
2806
2806
 
2807
- self.show_hr_button = QPushButton("H-R Diagram")
2807
+ self.show_hr_button = QPushButton(self.tr("H-R Diagram"))
2808
2808
  # Optionally give it an icon:
2809
2809
  self.show_hr_button.setIcon(QApplication.style().standardIcon(
2810
2810
  QStyle.StandardPixmap.SP_DesktopIcon))
@@ -2821,10 +2821,10 @@ class WIMIDialog(QDialog):
2821
2821
  label_layout = QHBoxLayout()
2822
2822
 
2823
2823
  # Create the label to display the count of objects
2824
- self.object_count_label = QLabel("Objects Found: 0")
2824
+ self.object_count_label = QLabel(self.tr("Objects Found: 0"))
2825
2825
 
2826
2826
  # Create the label with instructions
2827
- self.instructions_label = QLabel("Right Click a Row for More Options")
2827
+ self.instructions_label = QLabel(self.tr("Right Click a Row for More Options"))
2828
2828
 
2829
2829
  # Add both labels to the horizontal layout
2830
2830
  label_layout.addWidget(self.object_count_label)
@@ -2850,61 +2850,61 @@ class WIMIDialog(QDialog):
2850
2850
  self.annotation_tools_section = QWidget()
2851
2851
  annotation_tools_layout = QGridLayout(self.annotation_tools_section)
2852
2852
 
2853
- annotation_instruction_label = QLabel("Ctrl+Click to add items, Alt+Click to measure distance")
2853
+ annotation_instruction_label = QLabel(self.tr("Ctrl+Click to add items, Alt+Click to measure distance"))
2854
2854
  annotation_instruction_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
2855
2855
  annotation_instruction_label.setStyleSheet("font-size: 10px; color: gray;")
2856
2856
 
2857
- self.draw_ellipse_button = QPushButton("Draw Ellipse")
2857
+ self.draw_ellipse_button = QPushButton(self.tr("Draw Ellipse"))
2858
2858
  self.draw_ellipse_button.tool_name = "Ellipse"
2859
2859
  self.draw_ellipse_button.clicked.connect(lambda: self.set_tool("Ellipse"))
2860
2860
  self.annotation_buttons.append(self.draw_ellipse_button)
2861
2861
 
2862
- self.freehand_button = QPushButton("Freehand (Lasso)")
2862
+ self.freehand_button = QPushButton(self.tr("Freehand (Lasso)"))
2863
2863
  self.freehand_button.tool_name = "Freehand"
2864
2864
  self.freehand_button.clicked.connect(lambda: self.set_tool("Freehand"))
2865
2865
  self.annotation_buttons.append(self.freehand_button)
2866
2866
 
2867
- self.draw_rectangle_button = QPushButton("Draw Rectangle")
2867
+ self.draw_rectangle_button = QPushButton(self.tr("Draw Rectangle"))
2868
2868
  self.draw_rectangle_button.tool_name = "Rectangle"
2869
2869
  self.draw_rectangle_button.clicked.connect(lambda: self.set_tool("Rectangle"))
2870
2870
  self.annotation_buttons.append(self.draw_rectangle_button)
2871
2871
 
2872
- self.draw_arrow_button = QPushButton("Draw Arrow")
2872
+ self.draw_arrow_button = QPushButton(self.tr("Draw Arrow"))
2873
2873
  self.draw_arrow_button.tool_name = "Arrow"
2874
2874
  self.draw_arrow_button.clicked.connect(lambda: self.set_tool("Arrow"))
2875
2875
  self.annotation_buttons.append(self.draw_arrow_button)
2876
2876
 
2877
- self.place_compass_button = QPushButton("Place Celestial Compass")
2877
+ self.place_compass_button = QPushButton(self.tr("Place Celestial Compass"))
2878
2878
  self.place_compass_button.tool_name = "Compass"
2879
2879
  self.place_compass_button.clicked.connect(lambda: self.set_tool("Compass"))
2880
2880
  self.annotation_buttons.append(self.place_compass_button)
2881
2881
 
2882
- self.add_text_button = QPushButton("Add Text")
2882
+ self.add_text_button = QPushButton(self.tr("Add Text"))
2883
2883
  self.add_text_button.tool_name = "Text"
2884
2884
  self.add_text_button.clicked.connect(lambda: self.set_tool("Text"))
2885
2885
  self.annotation_buttons.append(self.add_text_button)
2886
2886
 
2887
2887
  # Add Color and Font buttons
2888
- self.color_button = QPushButton("Select Color")
2888
+ self.color_button = QPushButton(self.tr("Select Color"))
2889
2889
  self.color_button.setIcon(QIcon(colorwheel_path))
2890
2890
  self.color_button.clicked.connect(self.select_color)
2891
2891
 
2892
- self.font_button = QPushButton("Select Font")
2892
+ self.font_button = QPushButton(self.tr("Select Font"))
2893
2893
  self.font_button.setIcon(QIcon(font_path))
2894
2894
  self.font_button.clicked.connect(self.select_font)
2895
2895
 
2896
2896
  # Undo button
2897
- self.undo_button = QPushButton("Undo")
2897
+ self.undo_button = QPushButton(self.tr("Undo"))
2898
2898
  self.undo_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_ArrowLeft)) # Left arrow icon for undo
2899
2899
  self.undo_button.clicked.connect(self.main_preview.undo_annotation) # Connect to undo_annotation in CustomGraphicsView
2900
2900
 
2901
2901
  # Clear Annotations button
2902
- self.clear_annotations_button = QPushButton("Clear Annotations")
2902
+ self.clear_annotations_button = QPushButton(self.tr("Clear Annotations"))
2903
2903
  self.clear_annotations_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_TrashIcon)) # Trash icon
2904
2904
  self.clear_annotations_button.clicked.connect(self.main_preview.clear_annotations) # Connect to clear_annotations in CustomGraphicsView
2905
2905
 
2906
2906
  # Delete Selected Object button
2907
- self.delete_selected_object_button = QPushButton("Delete Selected Object(s)")
2907
+ self.delete_selected_object_button = QPushButton(self.tr("Delete Selected Items"))
2908
2908
  self.delete_selected_object_button.setIcon(QApplication.style().standardIcon(QStyle.StandardPixmap.SP_DialogCloseButton)) # Trash icon
2909
2909
  self.delete_selected_object_button.clicked.connect(self.main_preview.delete_selected_objects)
2910
2910
 
@@ -2928,12 +2928,12 @@ class WIMIDialog(QDialog):
2928
2928
  right_panel.addWidget(self.annotation_tools_section)
2929
2929
 
2930
2930
  # Advanced Search Panel
2931
- self.advanced_param_label = QLabel("Advanced Search Parameters")
2931
+ self.advanced_param_label = QLabel(self.tr("Advanced Search Parameters"))
2932
2932
  self.advanced_search_panel.addWidget(self.advanced_param_label)
2933
2933
 
2934
2934
  # TreeWidget for object types
2935
2935
  self.object_tree = QTreeWidget()
2936
- self.object_tree.setHeaderLabels(["Object Type", "Description"])
2936
+ self.object_tree.setHeaderLabels([self.tr("Object Type"), self.tr("Description")])
2937
2937
  self.object_tree.setColumnWidth(0, 150)
2938
2938
  self.object_tree.setSortingEnabled(True)
2939
2939
 
@@ -2949,17 +2949,17 @@ class WIMIDialog(QDialog):
2949
2949
  toggle_buttons_layout = QHBoxLayout()
2950
2950
 
2951
2951
  # Toggle All
2952
- self.toggle_all_button = QPushButton("Toggle All")
2952
+ self.toggle_all_button = QPushButton(self.tr("Toggle All"))
2953
2953
  self.toggle_all_button.clicked.connect(self.toggle_all_items)
2954
2954
  toggle_buttons_layout.addWidget(self.toggle_all_button)
2955
2955
 
2956
2956
  # Save Custom List
2957
- self.save_list_button = QPushButton("Save List…")
2957
+ self.save_list_button = QPushButton(self.tr("Save List…"))
2958
2958
  self.save_list_button.clicked.connect(self.save_custom_list)
2959
2959
  toggle_buttons_layout.addWidget(self.save_list_button)
2960
2960
 
2961
2961
  # Load Custom List
2962
- self.load_list_button = QPushButton("Load List…")
2962
+ self.load_list_button = QPushButton(self.tr("Load List…"))
2963
2963
  self.load_list_button.clicked.connect(self.load_custom_list)
2964
2964
  toggle_buttons_layout.addWidget(self.load_list_button)
2965
2965
 
@@ -2968,11 +2968,11 @@ class WIMIDialog(QDialog):
2968
2968
  # Add Simbad Search buttons below the toggle buttons
2969
2969
  search_button_layout = QHBoxLayout()
2970
2970
 
2971
- self.simbad_defined_region_button = QPushButton("Search Defined Region")
2971
+ self.simbad_defined_region_button = QPushButton(self.tr("Search Defined Region"))
2972
2972
  self.simbad_defined_region_button.clicked.connect(self.search_defined_region)
2973
2973
  search_button_layout.addWidget(self.simbad_defined_region_button)
2974
2974
 
2975
- self.simbad_entire_image_button = QPushButton("Search Entire Image")
2975
+ self.simbad_entire_image_button = QPushButton(self.tr("Search Entire Image"))
2976
2976
  self.simbad_entire_image_button.clicked.connect(self.search_entire_image)
2977
2977
  search_button_layout.addWidget(self.simbad_entire_image_button)
2978
2978
 
@@ -2981,17 +2981,17 @@ class WIMIDialog(QDialog):
2981
2981
  # ─────────────────────────────
2982
2982
  # Minor Planets / Comets block
2983
2983
  # ─────────────────────────────
2984
- self.minor_group = QGroupBox("Minor Planets / Comets")
2984
+ self.minor_group = QGroupBox(self.tr("Minor Planets / Comets"))
2985
2985
  minor_layout = QGridLayout(self.minor_group)
2986
2986
 
2987
2987
  # --- DB info + buttons ---
2988
- self.minor_db_label = QLabel("Database: not downloaded")
2988
+ self.minor_db_label = QLabel(self.tr("Database: not downloaded"))
2989
2989
  self.minor_db_label.setStyleSheet("font-size: 10px; color: gray;")
2990
2990
 
2991
- self.btn_minor_download = QPushButton("Download Catalog")
2991
+ self.btn_minor_download = QPushButton(self.tr("Download Catalog"))
2992
2992
  self.btn_minor_download.clicked.connect(self.download_minor_body_catalog)
2993
2993
 
2994
- self.btn_minor_search = QPushButton("Search Minor Bodies")
2994
+ self.btn_minor_search = QPushButton(self.tr("Search Minor Bodies"))
2995
2995
  self.btn_minor_search.clicked.connect(self.perform_minor_body_search)
2996
2996
 
2997
2997
  # Row 0: status label across full width
@@ -3002,18 +3002,18 @@ class WIMIDialog(QDialog):
3002
3002
  minor_layout.addWidget(self.btn_minor_search, 1, 2, 1, 2)
3003
3003
 
3004
3004
  # --- Search scope (Defined Circle vs Entire Image) ---
3005
- scope_label = QLabel("Search scope:")
3005
+ scope_label = QLabel(self.tr("Search scope:"))
3006
3006
  self.minor_scope_combo = QComboBox()
3007
3007
  self.minor_scope_combo.addItems([
3008
- "Defined Region",
3009
- "Entire Image",
3008
+ self.tr("Defined Region"),
3009
+ self.tr("Entire Image"),
3010
3010
  ])
3011
3011
 
3012
3012
  minor_layout.addWidget(scope_label, 2, 0)
3013
3013
  minor_layout.addWidget(self.minor_scope_combo, 2, 1, 1, 3)
3014
3014
 
3015
3015
  # --- Limits row 1: asteroid H_max + max count ---
3016
- ast_H_label = QLabel("Asteroid H \u2264")
3016
+ ast_H_label = QLabel(self.tr("Asteroid H \u2264"))
3017
3017
  self.minor_ast_H_spin = QDoubleSpinBox()
3018
3018
  self.minor_ast_H_spin.setRange(0.0, 40.0)
3019
3019
  self.minor_ast_H_spin.setDecimals(1)
@@ -3022,7 +3022,7 @@ class WIMIDialog(QDialog):
3022
3022
  float(self.settings.value("wimi/minor/asteroid_H_max", 20.0))
3023
3023
  )
3024
3024
 
3025
- ast_max_label = QLabel("Max asteroids:")
3025
+ ast_max_label = QLabel(self.tr("Max asteroids:"))
3026
3026
  self.minor_ast_max_spin = QSpinBox()
3027
3027
  self.minor_ast_max_spin.setRange(100, 2000000)
3028
3028
  self.minor_ast_max_spin.setSingleStep(1000)
@@ -3036,7 +3036,7 @@ class WIMIDialog(QDialog):
3036
3036
  minor_layout.addWidget(self.minor_ast_max_spin, 3, 3)
3037
3037
 
3038
3038
  # --- Limits row 2: comet H_max + max count ---
3039
- com_H_label = QLabel("Comet H \u2264")
3039
+ com_H_label = QLabel(self.tr("Comet H \u2264"))
3040
3040
  self.minor_com_H_spin = QDoubleSpinBox()
3041
3041
  self.minor_com_H_spin.setRange(0.0, 40.0)
3042
3042
  self.minor_com_H_spin.setDecimals(1)
@@ -3045,7 +3045,7 @@ class WIMIDialog(QDialog):
3045
3045
  float(self.settings.value("wimi/minor/comet_H_max", 15.0))
3046
3046
  )
3047
3047
 
3048
- com_max_label = QLabel("Max comets:")
3048
+ com_max_label = QLabel(self.tr("Max comets:"))
3049
3049
  self.minor_com_max_spin = QSpinBox()
3050
3050
  self.minor_com_max_spin.setRange(100, 100000)
3051
3051
  self.minor_com_max_spin.setSingleStep(500)
@@ -3059,14 +3059,14 @@ class WIMIDialog(QDialog):
3059
3059
  minor_layout.addWidget(self.minor_com_max_spin, 4, 3)
3060
3060
 
3061
3061
  # --- Optional specific target (designation / name) ---
3062
- target_label = QLabel("Target (optional):")
3062
+ target_label = QLabel(self.tr("Target (optional):"))
3063
3063
  self.minor_target_edit = QLineEdit()
3064
3064
  self.minor_target_edit.setPlaceholderText("e.g. 584, Semiramis, C/2023 A3...")
3065
3065
 
3066
3066
  minor_layout.addWidget(target_label, 5, 0)
3067
3067
  minor_layout.addWidget(self.minor_target_edit, 5, 1, 1, 3)
3068
3068
 
3069
- self.minor_count_button = QPushButton("Count Objects Brighter Than Limits")
3069
+ self.minor_count_button = QPushButton(self.tr("Count Objects Brighter Than Limits"))
3070
3070
  self.minor_count_button.setToolTip(
3071
3071
  "Show how many catalog objects are brighter than the selected H limits."
3072
3072
  )
@@ -3075,7 +3075,7 @@ class WIMIDialog(QDialog):
3075
3075
  minor_layout.addWidget(self.minor_count_button, 6, 0, 1, 4)
3076
3076
 
3077
3077
  # --- Time offset (hours) for ephemerides ---
3078
- time_offset_label = QLabel("Time offset (hours):")
3078
+ time_offset_label = QLabel(self.tr("Time offset (hours):"))
3079
3079
  self.minor_time_offset_spin = QDoubleSpinBox()
3080
3080
  self.minor_time_offset_spin.setRange(-24.0, 24.0)
3081
3081
  self.minor_time_offset_spin.setDecimals(2)
@@ -3099,7 +3099,7 @@ class WIMIDialog(QDialog):
3099
3099
  self._load_minor_db_path()
3100
3100
 
3101
3101
  # Adding the "Deep Vizier Search" button below the other search buttons
3102
- self.deep_vizier_button = QPushButton("Caution - Deep Vizier Search")
3102
+ self.deep_vizier_button = QPushButton(self.tr("Caution - Deep Vizier Search"))
3103
3103
  self.deep_vizier_button.setIcon(QIcon(nuke_path)) # Assuming `nuke_path` is the correct path for the icon
3104
3104
  self.deep_vizier_button.setToolTip("Perform a deep search with Vizier. Caution: May return large datasets.")
3105
3105
 
@@ -3109,7 +3109,7 @@ class WIMIDialog(QDialog):
3109
3109
  # Add the Deep Vizier button to the advanced search layout
3110
3110
  self.advanced_search_panel.addWidget(self.deep_vizier_button)
3111
3111
 
3112
- self.mast_search_button = QPushButton("Search M.A.S.T Database")
3112
+ self.mast_search_button = QPushButton(self.tr("Search M.A.S.T Database"))
3113
3113
  self.mast_search_button.setIcon(QIcon(hubble_path))
3114
3114
  self.mast_search_button.clicked.connect(self.perform_mast_search)
3115
3115
  self.mast_search_button.setToolTip("Search Hubble, JWST, Spitzer, TESS and More.")
@@ -3604,15 +3604,15 @@ class WIMIDialog(QDialog):
3604
3604
  menu = QMenu(self)
3605
3605
 
3606
3606
  # Define actions
3607
- open_website_action = QAction("Open Website", self)
3607
+ open_website_action = QAction(self.tr("Open Website"), self)
3608
3608
  open_website_action.triggered.connect(lambda: self.results_tree.itemDoubleClicked.emit(item, 0))
3609
3609
  menu.addAction(open_website_action)
3610
3610
 
3611
- zoom_to_object_action = QAction("Zoom to Object", self)
3611
+ zoom_to_object_action = QAction(self.tr("Zoom to Object"), self)
3612
3612
  zoom_to_object_action.triggered.connect(lambda: self.zoom_to_object(item))
3613
3613
  menu.addAction(zoom_to_object_action)
3614
3614
 
3615
- copy_info_action = QAction("Copy Object Information", self)
3615
+ copy_info_action = QAction(self.tr("Copy Object Information"), self)
3616
3616
  copy_info_action.triggered.connect(lambda: self.copy_object_information(item))
3617
3617
  menu.addAction(copy_info_action)
3618
3618
 
@@ -6524,509 +6524,6 @@ class WIMIDialog(QDialog):
6524
6524
  print(f"[MinorBodies] objects inside cone: {kept}")
6525
6525
  return results
6526
6526
 
6527
- def _get_astap_exe(self) -> str:
6528
- s = self._settings()
6529
- # preferred key (what SettingsDialog writes)
6530
- p = s.value("paths/astap", "", type=str)
6531
- if p:
6532
- return p
6533
- # migrate legacy key if present
6534
- legacy = s.value("astap/exe_path", "", type=str)
6535
- if legacy:
6536
- s.setValue("paths/astap", legacy)
6537
- s.remove("astap/exe_path")
6538
- s.sync()
6539
- return legacy
6540
- return ""
6541
-
6542
- def _set_astap_exe(self, path: str) -> None:
6543
- s = self._settings()
6544
- s.setValue("paths/astap", path)
6545
- s.sync()
6546
-
6547
- def plate_solve_image(self):
6548
- """
6549
- Attempts to plate-solve the loaded image using ASTAP,
6550
- first trying a seeded solve (RA, SPD, scale, binning),
6551
- then falling back to a blind solve if anything is missing.
6552
- On success, updates self.header and self.wcs.
6553
- """
6554
- if not hasattr(self, 'image_path') or not self.image_path:
6555
- return
6556
-
6557
- # 1) Ensure ASTAP path
6558
- astap_exe = self._get_astap_exe()
6559
- if not astap_exe or not os.path.exists(astap_exe):
6560
- # last-resort browse if nothing in settings (keeps existing behavior)
6561
- filt = "Executables (*.exe);;All Files (*)" if sys.platform.startswith("win") else "Executables (*)"
6562
- new_path, _ = QFileDialog.getOpenFileName(self, "Select ASTAP Executable", "", filt)
6563
- if not new_path:
6564
- return
6565
- astap_exe = new_path
6566
- self._set_astap_exe(astap_exe)
6567
-
6568
- # 2) Write out the normalized FITS for ASTAP
6569
- normalized = self.stretch_image(self.image_data.astype(np.float32))
6570
- try:
6571
- tmp_path = self.save_temp_fits_image(normalized, self.image_path)
6572
- except Exception as e:
6573
- QMessageBox.critical(self, "Plate Solve", f"Error saving temp FITS: {e}")
6574
- return
6575
-
6576
- # 3) Seed arguments from header
6577
- raw_hdr = None
6578
- if isinstance(self.original_header, fits.Header):
6579
- raw_hdr = self.original_header
6580
- elif self.image_path.lower().endswith(('.fits','.fit')):
6581
- with fits.open(self.image_path, memmap=False) as hdul:
6582
- raw_hdr = hdul[0].header
6583
-
6584
- seed_args = []
6585
- if isinstance(raw_hdr, fits.Header):
6586
- # debug-dump
6587
- print("🔍 Raw header contents:")
6588
- for k,v in raw_hdr.items():
6589
- print(f" {k} = {v}")
6590
-
6591
- try:
6592
- # RA→hours, SPD
6593
- ra_deg = float(raw_hdr["CRVAL1"])
6594
- dec_deg= float(raw_hdr["CRVAL2"])
6595
- ra_h = ra_deg / 15.0
6596
- spd = dec_deg + 90.0
6597
-
6598
- # plate scale from CD matrix (°/px→″/px)
6599
- cd1 = float(raw_hdr.get("CD1_1", raw_hdr.get("CDELT1",0)))
6600
- cd2 = float(raw_hdr.get("CD2_1", raw_hdr.get("CDELT2",0)))
6601
- scale = np.hypot(cd1, cd2) * 3600.0
6602
-
6603
- # apply XBINNING/YBINNING
6604
- bx = int(raw_hdr.get("XBINNING", 1))
6605
- by = int(raw_hdr.get("YBINNING", bx))
6606
- if bx != by:
6607
- print(f"⚠️ Unequal binning: {bx}×{by}, averaging.")
6608
- binf = (bx+by)/2.0
6609
- scale *= binf
6610
-
6611
- seed_args = [
6612
- "-ra", f"{ra_h:.6f}",
6613
- "-spd", f"{spd:.6f}",
6614
- "-scale", f"{scale:.3f}"
6615
- ]
6616
- print(f"🔸 Seeding ASTAP: RA={ra_h:.6f}h, SPD={spd:.6f}°, scale={scale:.3f}\"/px (×{binf} bin)")
6617
- except Exception as e:
6618
- print("⚠️ Failed to build seed args, will do blind solve:", e)
6619
-
6620
- # 4) Build ASTAP args
6621
- if seed_args:
6622
- args = ["-f", tmp_path] + seed_args + ["-wcs", "-sip"]
6623
- else:
6624
- args = ["-f", tmp_path, "-r", "179", "-fov", "0", "-z", "0", "-wcs", "-sip"]
6625
-
6626
- print("▶️ Running ASTAP with arguments:", args)
6627
-
6628
- # create and launch the process
6629
- process = QProcess(self)
6630
- process.start(astap_exe, args)
6631
- if not process.waitForStarted(5000):
6632
- #QMessageBox.critical(self, "Plate Solve", "Failed to start ASTAP process.")
6633
- os.remove(tmp_path)
6634
-
6635
- return None
6636
- if not process.waitForFinished(300000):
6637
- #QMessageBox.critical(self, "Plate Solve", "ASTAP process timed out.")
6638
- os.remove(tmp_path)
6639
- return None
6640
-
6641
- exit_code = process.exitCode()
6642
- stdout = process.readAllStandardOutput().data().decode()
6643
- stderr = process.readAllStandardError().data().decode()
6644
- print("ASTAP exit code:", exit_code)
6645
- print("ASTAP STDOUT:\n", stdout)
6646
- print("ASTAP STDERR:\n", stderr)
6647
-
6648
- if exit_code != 0:
6649
- os.remove(tmp_path)
6650
- #QMessageBox.warning(self, "Plate Solve", "ASTAP failed. Falling back to blind solve.")
6651
-
6652
- return None
6653
-
6654
- # --- Retrieve the initial solved header from the temporary FITS file ---
6655
- try:
6656
- with fits.open(tmp_path, memmap=False) as hdul:
6657
- solved_header = dict(hdul[0].header)
6658
- for key in ["COMMENT", "HISTORY", "END"]:
6659
- solved_header.pop(key, None)
6660
- print("Initial solved header retrieved from temporary FITS file:")
6661
- for key, value in solved_header.items():
6662
- print(f"{key} = {value}")
6663
- except Exception as e:
6664
- QMessageBox.critical(self, "Plate Solve", f"Error reading solved header: {e}")
6665
- os.remove(tmp_path)
6666
-
6667
- return None
6668
-
6669
- # --- Check for a .wcs file and merge its header if present ---
6670
- wcs_path = os.path.splitext(tmp_path)[0] + ".wcs"
6671
- if os.path.exists(wcs_path):
6672
- try:
6673
- wcs_header = {}
6674
- with open(wcs_path, "r") as f:
6675
- text = f.read()
6676
- # Matches a FITS header keyword and its value (with an optional comment).
6677
- pattern = r"(\w+)\s*=\s*('?[^/']*'?)[\s/]"
6678
- for match in re.finditer(pattern, text):
6679
- key = match.group(1).strip().upper()
6680
- val = match.group(2).strip()
6681
- if val.startswith("'") and val.endswith("'"):
6682
- val = val[1:-1].strip()
6683
- wcs_header[key] = val
6684
- wcs_header.pop("END", None)
6685
- print("WCS header retrieved from .wcs file:")
6686
- for key, value in wcs_header.items():
6687
- print(f"{key} = {value}")
6688
- # Merge the parsed WCS header into the solved header.
6689
- solved_header.update(wcs_header)
6690
- except Exception as e:
6691
- print("Error reading .wcs file:", e)
6692
- else:
6693
- print("No .wcs file found; using header from temporary FITS.")
6694
-
6695
- # --- If loaded from a slot, merge the original file path from slot metadata ---
6696
- if getattr(self, "_from_slot", False) and hasattr(self, "_slot_meta"):
6697
- if "file_path" not in solved_header and "file_path" in self._slot_meta:
6698
- solved_header["file_path"] = self._slot_meta["file_path"]
6699
- print("Merged file_path from slot metadata into solved header.")
6700
-
6701
- # --- Add any missing required WCS keywords ---
6702
- required_keys = {
6703
- "CTYPE1": "RA---TAN",
6704
- "CTYPE2": "DEC--TAN",
6705
- "RADECSYS": "ICRS",
6706
- "WCSAXES": 2,
6707
- # CRVAL1, CRVAL2, CRPIX1, CRPIX2 are ideally provided by ASTAP.
6708
- }
6709
- for key, default in required_keys.items():
6710
- if key not in solved_header:
6711
- solved_header[key] = default
6712
- print(f"Added missing key {key} with default value {default}.")
6713
-
6714
- # --- Convert keys that are expected to be numeric from strings to numbers ---
6715
- expected_numeric_keys = {
6716
- "CRPIX1", "CRPIX2", "CRVAL1", "CRVAL2", "CROTA1", "CROTA2",
6717
- "CDELT1", "CDELT2", "CD1_1", "CD1_2", "CD2_1", "CD2_2", "WCSAXES"
6718
- }
6719
- for key in expected_numeric_keys:
6720
- if key in solved_header:
6721
- try:
6722
- # For keys that should be integers, you can use int(float(...)) if necessary.
6723
- solved_header[key] = float(solved_header[key])
6724
- except ValueError:
6725
- print(f"Warning: Could not convert {key} value '{solved_header[key]}' to float.")
6726
-
6727
- # --- Ensure integer keywords are stored as integers ---
6728
- for key in ["WCSAXES", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3"]:
6729
- if key in solved_header:
6730
- try:
6731
- solved_header[key] = int(float(solved_header[key]))
6732
- except ValueError:
6733
- print(f"Warning: Could not convert {key} value '{solved_header[key]}' to int.")
6734
-
6735
-
6736
- os.remove(tmp_path)
6737
- print("ASTAP plate solving successful. Final solved header:")
6738
- for key, value in solved_header.items():
6739
- print(f"{key} = {value}")
6740
-
6741
- # --------------------------------------------------------------------
6742
- # 1) Make sure A_ORDER/B_ORDER exist in pairs:
6743
- if "B_ORDER" in solved_header and "A_ORDER" not in solved_header:
6744
- solved_header["A_ORDER"] = solved_header["B_ORDER"]
6745
- if "A_ORDER" in solved_header and "B_ORDER" not in solved_header:
6746
- solved_header["B_ORDER"] = solved_header["A_ORDER"]
6747
-
6748
- # 2) Convert SIP‐order keywords to ints:
6749
- for key in ("A_ORDER","B_ORDER","AP_ORDER","BP_ORDER"):
6750
- if key in solved_header:
6751
- solved_header[key] = int(float(solved_header[key]))
6752
-
6753
- # 3) Convert every SIP coefficient to float:
6754
- for k in list(solved_header):
6755
- if re.match(r"^(?:A|B|AP|BP)_[0-9]+_[0-9]+$", k):
6756
- solved_header[k] = float(solved_header[k])
6757
-
6758
- # --------------------------------------------------------------------
6759
- # 4) Now rebuild your FITS header from the dict, preserving ordering:
6760
- new_hdr = fits.Header()
6761
- for key, val in solved_header.items():
6762
- # skip any stray non‑FITS metadata
6763
- if key == "file_path":
6764
- continue
6765
- new_hdr[key] = val
6766
-
6767
- # 5) Finally swap in the new header and re-init WCS (with SIP!)
6768
- self.header = new_hdr
6769
- try:
6770
- self.apply_wcs_header(self.header)
6771
- self.status_label.setText("Status: ASTAP solve succeeded.")
6772
- except Exception as e:
6773
- QMessageBox.critical(self, "Plate Solve", f"Error initializing WCS from solved header:\n{e}")
6774
- return
6775
-
6776
- return solved_header
6777
-
6778
-
6779
- def save_temp_fits_image(self, normalized_image, image_path: str):
6780
- """
6781
- Save the normalized_image as a FITS file to a temporary file.
6782
-
6783
- If the original image is FITS, this method retrieves the stored metadata
6784
- from the ImageManager and passes it directly to save_image().
6785
- If not, it generates a minimal header.
6786
-
6787
- Returns the path to the temporary FITS file.
6788
- """
6789
- # Always save as FITS.
6790
- selected_format = "fits"
6791
- bit_depth = "32-bit floating point"
6792
- is_mono = (normalized_image.ndim == 2 or
6793
- (normalized_image.ndim == 3 and normalized_image.shape[2] == 1))
6794
-
6795
- # If the original image is FITS, try to get its stored metadata.
6796
- original_header = None
6797
- if image_path.lower().endswith((".fits", ".fit")):
6798
- if self.parent() and hasattr(self.parent(), "image_manager"):
6799
- # Use the metadata from the current slot.
6800
- _, meta = self.parent().image_manager.get_current_image_and_metadata()
6801
- # Assume that meta already contains a proper 'original_header'
6802
- # (or the entire meta is the header).
6803
- original_header = meta.get("original_header", None)
6804
- # If nothing is stored, fall back to creating a minimal header.
6805
- if original_header is None:
6806
- print("No stored FITS header found; creating a minimal header.")
6807
- original_header = self.create_minimal_fits_header(normalized_image, is_mono)
6808
- else:
6809
- # For non-FITS images, generate a minimal header.
6810
- original_header = self.create_minimal_fits_header(normalized_image, is_mono)
6811
-
6812
- # Create a temporary filename.
6813
- tmp_file = tempfile.NamedTemporaryFile(suffix=".fits", delete=False)
6814
- tmp_path = tmp_file.name
6815
- tmp_file.close()
6816
-
6817
- try:
6818
- # Call your global save_image() exactly as in AstroEditingSuite.
6819
- save_image(
6820
- img_array=normalized_image,
6821
- filename=tmp_path,
6822
- original_format=selected_format,
6823
- bit_depth=bit_depth,
6824
- original_header=original_header,
6825
- is_mono=is_mono
6826
- # (image_meta and file_meta can be omitted if not needed)
6827
- )
6828
- print(f"Temporary normalized FITS saved to: {tmp_path}")
6829
- except Exception as e:
6830
- print("Error saving temporary FITS file using save_image():", e)
6831
- raise e
6832
- return tmp_path
6833
-
6834
- def create_minimal_fits_header(self, img_array, is_mono=False):
6835
- """
6836
- Creates a minimal FITS header when the original header is missing.
6837
- """
6838
-
6839
- header = Header()
6840
- header['SIMPLE'] = (True, 'Standard FITS file')
6841
- header['BITPIX'] = -32 # 32-bit floating-point data
6842
- header['NAXIS'] = 2 if is_mono else 3
6843
- header['NAXIS1'] = img_array.shape[2] if img_array.ndim == 3 and not is_mono else img_array.shape[1] # Image width
6844
- header['NAXIS2'] = img_array.shape[1] if img_array.ndim == 3 and not is_mono else img_array.shape[0] # Image height
6845
- if not is_mono:
6846
- header['NAXIS3'] = img_array.shape[0] if img_array.ndim == 3 else 1 # Number of color channels
6847
- header['BZERO'] = 0.0 # No offset
6848
- header['BSCALE'] = 1.0 # No scaling
6849
- header.add_comment("Minimal FITS header generated by AstroEditingSuite.")
6850
-
6851
- return header
6852
-
6853
- def stretch_image(self, image):
6854
- """
6855
- Perform an unlinked linear stretch on the image.
6856
- Each channel is stretched independently by subtracting its own minimum,
6857
- recording its own median, and applying the stretch formula.
6858
- Returns the stretched image in [0,1].
6859
- """
6860
- was_single_channel = False # Flag to check if image was single-channel
6861
-
6862
- # If the image is 2D or has one channel, convert to 3-channel
6863
- if image.ndim == 2 or (image.ndim == 3 and image.shape[2] == 1):
6864
- was_single_channel = True
6865
- image = np.stack([image] * 3, axis=-1)
6866
-
6867
- image = np.asarray(image, dtype=np.float32)
6868
- stretched_image = image.copy() # Need copy for in-place modification
6869
- self.stretch_original_mins = []
6870
- self.stretch_original_medians = []
6871
- target_median = 0.02
6872
-
6873
- for c in range(3):
6874
- channel_min = np.min(stretched_image[..., c])
6875
- self.stretch_original_mins.append(channel_min)
6876
- stretched_image[..., c] -= channel_min
6877
- channel_median = np.median(stretched_image[..., c])
6878
- self.stretch_original_medians.append(channel_median)
6879
- if channel_median != 0:
6880
- numerator = (channel_median - 1) * target_median * stretched_image[..., c]
6881
- denominator = (
6882
- channel_median * (target_median + stretched_image[..., c] - 1)
6883
- - target_median * stretched_image[..., c]
6884
- )
6885
- denominator = np.where(denominator == 0, 1e-6, denominator)
6886
- stretched_image[..., c] = numerator / denominator
6887
- else:
6888
- print(f"Channel {c} - Median is zero. Skipping stretch.")
6889
-
6890
- stretched_image = np.clip(stretched_image, 0.0, 1.0)
6891
- self.was_single_channel = was_single_channel
6892
- return stretched_image
6893
-
6894
- def unstretch_image(self, image):
6895
- """
6896
- Undo the unlinked linear stretch using stored parameters.
6897
- Returns the unstretched image.
6898
- """
6899
- original_mins = self.stretch_original_mins
6900
- original_medians = self.stretch_original_medians
6901
- was_single_channel = self.was_single_channel
6902
-
6903
- image = np.asarray(image, dtype=np.float32)
6904
-
6905
- if image.ndim == 2:
6906
- channel_median = np.median(image)
6907
- original_median = original_medians[0]
6908
- original_min = original_mins[0]
6909
- if channel_median != 0 and original_median != 0:
6910
- numerator = (channel_median - 1) * original_median * image
6911
- denominator = channel_median * (original_median + image - 1) - original_median * image
6912
- denominator = np.where(denominator == 0, 1e-6, denominator)
6913
- image = numerator / denominator
6914
- else:
6915
- print("Channel median or original median is zero. Skipping unstretch.")
6916
- image += original_min
6917
- image = np.clip(image, 0, 1)
6918
- return image
6919
-
6920
- for c in range(3):
6921
- channel_median = np.median(image[..., c])
6922
- original_median = original_medians[c]
6923
- original_min = original_mins[c]
6924
- if channel_median != 0 and original_median != 0:
6925
- numerator = (channel_median - 1) * original_median * image[..., c]
6926
- denominator = (
6927
- channel_median * (original_median + image[..., c] - 1)
6928
- - original_median * image[..., c]
6929
- )
6930
- denominator = np.where(denominator == 0, 1e-6, denominator)
6931
- image[..., c] = numerator / denominator
6932
- else:
6933
- print(f"Channel {c} - Median or original median is zero. Skipping unstretch.")
6934
- image[..., c] += original_min
6935
-
6936
- image = np.clip(image, 0, 1)
6937
- if was_single_channel and image.ndim == 3:
6938
- image = np.mean(image, axis=2, keepdims=True)
6939
- return image
6940
-
6941
- def retrieve_and_apply_wcs(self, job_id):
6942
- """Download the wcs.fits file from Astrometry.net, extract WCS header data, and apply it."""
6943
- try:
6944
- wcs_url = f"https://nova.astrometry.net/wcs_file/{job_id}"
6945
- wcs_filepath = "wcs.fits"
6946
- max_retries = 10
6947
- delay = 10 # seconds
6948
-
6949
- for attempt in range(max_retries):
6950
- response = requests.get(wcs_url, stream=True)
6951
- response.raise_for_status()
6952
-
6953
- with open(wcs_filepath, 'wb') as f:
6954
- for chunk in response.iter_content(chunk_size=8192):
6955
- f.write(chunk)
6956
-
6957
- try:
6958
- with fits.open(wcs_filepath, ignore_missing_simple=True, ignore_missing_end=True) as hdul:
6959
- wcs_header = hdul[0].header
6960
- print("WCS header successfully retrieved.")
6961
- # 🔁 use common path
6962
- self.apply_wcs_header(wcs_header)
6963
- return wcs_header
6964
- except Exception as e:
6965
- print(f"Attempt {attempt + 1}: Failed to process WCS file - possibly HTML instead of FITS. Retrying in {delay} seconds...")
6966
- print(f"Error: {e}")
6967
- time.sleep(delay)
6968
-
6969
- print("Failed to download a valid WCS FITS file after multiple attempts.")
6970
- return None
6971
-
6972
- except requests.exceptions.RequestException as e:
6973
- print(f"Error downloading WCS file: {e}")
6974
- except Exception as e:
6975
- print(f"Error processing WCS file: {e}")
6976
-
6977
- return None
6978
-
6979
-
6980
-
6981
- def apply_wcs_header(self, wcs_header):
6982
- """
6983
- Apply a solved WCS header. Sets self.wcs, self.pixscale (arcsec/pix),
6984
- self.orientation, and updates the orientation label.
6985
- """
6986
- # 1) Initialize the WCS object
6987
- self.wcs = WCS(wcs_header, naxis=2, relax=True)
6988
-
6989
- # 2) Derive pixel scale (arcsec/pixel)
6990
- if 'CDELT1' in wcs_header:
6991
- # CDELT1 is degrees/pixel
6992
- self.pixscale = abs(float(wcs_header['CDELT1'])) * 3600.0
6993
- elif 'CD1_1' in wcs_header and 'CD2_2' in wcs_header:
6994
- # approximate from CD matrix determinant
6995
- det = (wcs_header['CD1_1'] * wcs_header['CD2_2']
6996
- - wcs_header['CD1_2'] * wcs_header['CD2_1'])
6997
- pixscale_deg = math.sqrt(abs(det))
6998
- self.pixscale = pixscale_deg * 3600.0
6999
- else:
7000
- self.pixscale = None
7001
- print("Warning: could not derive pixscale from header.")
7002
-
7003
- # 3) Extract orientation (CROTA2 if present)
7004
- if 'CROTA2' in wcs_header:
7005
- self.orientation = float(wcs_header['CROTA2'])
7006
- else:
7007
- # fallback to your custom function
7008
- self.orientation = calculate_orientation(wcs_header)
7009
-
7010
- # 4) Update the GUI label
7011
- if self.orientation is not None:
7012
- self.orientation_label.setText(f"Orientation: {self.orientation:.2f}°")
7013
- else:
7014
- self.orientation_label.setText("Orientation: N/A")
7015
-
7016
- print(f" -> pixscale = {self.pixscale} arcsec/pixel")
7017
- print(f" -> orientation = {self.orientation}°")
7018
- try:
7019
- cr1 = wcs_header.get('CRVAL1')
7020
- cr2 = wcs_header.get('CRVAL2')
7021
- if cr1 is not None and cr2 is not None:
7022
- self.center_ra = float(cr1)
7023
- self.center_dec = float(cr2)
7024
- print(f" -> center RA/Dec = {self.center_ra:.6f}, {self.center_dec:.6f}")
7025
- except Exception:
7026
- print("Warning: could not extract CRVAL1/CRVAL2")
7027
-
7028
-
7029
-
7030
6527
  def calculate_pixel_from_ra_dec(self, ra, dec):
7031
6528
  """Convert RA/Dec to pixel coordinates using the WCS data."""
7032
6529
  if not hasattr(self, "wcs") or self.wcs is None:
@@ -7059,117 +6556,6 @@ class WIMIDialog(QDialog):
7059
6556
 
7060
6557
  return int(round(x_val)), int(round(y_val))
7061
6558
 
7062
- def login_to_astrometry(self, api_key):
7063
- try:
7064
- response = requests.post(
7065
- ASTROMETRY_API_URL + "login",
7066
- data={'request-json': json.dumps({"apikey": api_key})}
7067
- )
7068
- response_data = response.json()
7069
- if response_data.get("status") == "success":
7070
- return response_data["session"]
7071
- else:
7072
- raise ValueError("Login failed: " + response_data.get("error", "Unknown error"))
7073
- except Exception as e:
7074
- raise Exception("Login to Astrometry.net failed: " + str(e))
7075
-
7076
-
7077
- def upload_image_to_astrometry(self, image_path, session_key):
7078
- try:
7079
- # Check if the file is XISF format
7080
- file_extension = os.path.splitext(image_path)[-1].lower()
7081
- if file_extension == ".xisf":
7082
- # Load the XISF image
7083
- xisf = XISF(image_path)
7084
- im_data = xisf.read_image(0)
7085
-
7086
- # Convert to a temporary TIFF file for upload
7087
- temp_image_path = os.path.splitext(image_path)[0] + "_converted.tif"
7088
- if im_data.dtype == np.float32 or im_data.dtype == np.float64:
7089
- im_data = np.clip(im_data, 0, 1) * 65535
7090
- im_data = im_data.astype(np.uint16)
7091
-
7092
- # Save as TIFF
7093
- if im_data.shape[-1] == 1: # Grayscale
7094
- tiff.imwrite(temp_image_path, np.squeeze(im_data, axis=-1))
7095
- else: # RGB
7096
- tiff.imwrite(temp_image_path, im_data)
7097
-
7098
- print(f"Converted XISF file to TIFF at {temp_image_path} for upload.")
7099
- image_path = temp_image_path # Use the converted file for upload
7100
-
7101
- # Upload the image file
7102
- with open(image_path, 'rb') as image_file:
7103
- files = {'file': image_file}
7104
- data = {
7105
- 'request-json': json.dumps({
7106
- "publicly_visible": "y",
7107
- "allow_modifications": "d",
7108
- "session": session_key,
7109
- "allow_commercial_use": "d"
7110
- })
7111
- }
7112
- response = requests.post(ASTROMETRY_API_URL + "upload", files=files, data=data)
7113
- response_data = response.json()
7114
- if response_data.get("status") == "success":
7115
- return response_data["subid"]
7116
- else:
7117
- raise ValueError("Image upload failed: " + response_data.get("error", "Unknown error"))
7118
-
7119
- except Exception as e:
7120
- raise Exception("Image upload to Astrometry.net failed: " + str(e))
7121
-
7122
- finally:
7123
- # Clean up temporary file if created
7124
- if file_extension == ".xisf" and os.path.exists(temp_image_path):
7125
- os.remove(temp_image_path)
7126
- print(f"Temporary TIFF file {temp_image_path} deleted after upload.")
7127
-
7128
-
7129
-
7130
- def poll_submission_status(self, subid):
7131
- """Poll Astrometry.net to retrieve the job ID once the submission is processed."""
7132
- max_retries = 90 # Adjust as necessary
7133
- retries = 0
7134
- while retries < max_retries:
7135
- try:
7136
- response = requests.get(ASTROMETRY_API_URL + f"submissions/{subid}")
7137
- response_data = response.json()
7138
- jobs = response_data.get("jobs", [])
7139
- if jobs and jobs[0] is not None:
7140
- return jobs[0]
7141
- else:
7142
- print(f"Polling attempt {retries + 1}: Job not ready yet.")
7143
- except Exception as e:
7144
- print(f"Error while polling submission status: {e}")
7145
-
7146
- retries += 1
7147
- time.sleep(10) # Wait 10 seconds between retries
7148
-
7149
- return None
7150
-
7151
- def poll_calibration_data(self, job_id):
7152
- """Poll Astrometry.net to retrieve the calibration data once it's available."""
7153
- max_retries = 90 # Retry for up to 15 minutes (90 * 10 seconds)
7154
- retries = 0
7155
- while retries < max_retries:
7156
- try:
7157
- response = requests.get(ASTROMETRY_API_URL + f"jobs/{job_id}/calibration/")
7158
- response_data = response.json()
7159
- if response_data and 'ra' in response_data and 'dec' in response_data:
7160
- print("Calibration data retrieved:", response_data)
7161
- return response_data # Calibration data is complete
7162
- else:
7163
- print(f"Calibration data not available yet (Attempt {retries + 1})")
7164
- except Exception as e:
7165
- print(f"Error retrieving calibration data: {e}")
7166
-
7167
- retries += 1
7168
- time.sleep(10) # Wait 10 seconds between retries
7169
-
7170
- return None
7171
-
7172
-
7173
6559
  #If originally a fits file update the header
7174
6560
  def update_fits_with_wcs(self, filepath, calibration_data):
7175
6561
  if not filepath.lower().endswith(('.fits', '.fit')):
@@ -7391,23 +6777,43 @@ class WIMIDialog(QDialog):
7391
6777
  CIRCLE('ICRS', {ra_center}, {dec_center}, {radius_deg})
7392
6778
  ) = 1
7393
6779
  """
6780
+
6781
+ result = None
6782
+ last_err = None
6783
+
7394
6784
  for attempt in range(5):
7395
6785
  try:
7396
6786
  result = Simbad.query_tap(query)
6787
+ last_err = None
7397
6788
  break
7398
6789
  except Exception as e:
6790
+ last_err = e
7399
6791
  if attempt < 4:
7400
- time.sleep(1)
6792
+ time.sleep(1) # or QThread.msleep(1000) if you want less UI freeze
7401
6793
  else:
7402
- QMessageBox.critical(
6794
+ # After 5 attempts total, stop with a helpful message
6795
+ err_txt = str(last_err) if last_err is not None else "Unknown error"
6796
+ QMessageBox.warning(
7403
6797
  self,
7404
- "Query Failed",
7405
- f"Try again later:\n{e}"
7406
- )
6798
+ "SIMBAD network error",
6799
+ (
6800
+ "We couldn't reach SIMBAD due to a network or service error.\n\n"
6801
+ "We tried 5 times and then stopped.\n\n"
6802
+ "What to try:\n"
6803
+ " • Check your internet connection / VPN / firewall\n"
6804
+ " • Verify SIMBAD is reachable: https://simbad.cds.unistra.fr/simbad/\n"
6805
+ " • Then try again\n\n"
6806
+ f"Last error:\n{err_txt}"
6807
+ )
6808
+ )
6809
+ return
7407
6810
 
7408
6811
  if result is None or len(result) == 0:
7409
- QMessageBox.information(self, "No Results",
7410
- "No objects found in the specified area.")
6812
+ QMessageBox.information(
6813
+ self,
6814
+ "No Results",
6815
+ "No objects found in the specified area."
6816
+ )
7411
6817
  return
7412
6818
 
7413
6819
  # ——— 3a) list of all “star” & binary/variable OTYPE codes ———
@@ -7912,41 +7318,6 @@ class WIMIDialog(QDialog):
7912
7318
  self.status_label.setText(f"Status: Blind solve failed — {res}")
7913
7319
 
7914
7320
 
7915
- def extract_wcs_data(file_path):
7916
- try:
7917
- # Open the FITS file with minimal validation to ignore potential errors in non-essential parts
7918
- with fits.open(file_path, ignore_missing_simple=True, ignore_missing_end=True) as hdul:
7919
- header = hdul[0].header
7920
-
7921
- # Extract essential WCS parameters
7922
- wcs_params = {}
7923
- keys_to_extract = [
7924
- 'WCSAXES', 'CTYPE1', 'CTYPE2', 'EQUINOX', 'LONPOLE', 'LATPOLE',
7925
- 'CRVAL1', 'CRVAL2', 'CRPIX1', 'CRPIX2', 'CUNIT1', 'CUNIT2',
7926
- 'CD1_1', 'CD1_2', 'CD2_1', 'CD2_2', 'A_ORDER', 'A_0_0', 'A_0_1',
7927
- 'A_0_2', 'A_1_0', 'A_1_1', 'A_2_0', 'B_ORDER', 'B_0_0', 'B_0_1',
7928
- 'B_0_2', 'B_1_0', 'B_1_1', 'B_2_0', 'AP_ORDER', 'AP_0_0', 'AP_0_1',
7929
- 'AP_0_2', 'AP_1_0', 'AP_1_1', 'AP_2_0', 'BP_ORDER', 'BP_0_0',
7930
- 'BP_0_1', 'BP_0_2', 'BP_1_0', 'BP_1_1', 'BP_2_0'
7931
- ]
7932
- for key in keys_to_extract:
7933
- if key in header:
7934
- wcs_params[key] = header[key]
7935
-
7936
- # Manually create a minimal header with WCS information
7937
- wcs_header = fits.Header()
7938
- for key, value in wcs_params.items():
7939
- wcs_header[key] = value
7940
-
7941
- # Initialize WCS with this custom header
7942
- wcs = WCS(wcs_header)
7943
- print("WCS successfully initialized with minimal header.")
7944
- return wcs
7945
-
7946
- except Exception as e:
7947
- print(f"Error processing WCS file: {e}")
7948
- return None
7949
-
7950
7321
  # Function to calculate comoving radial distance (in Gly)
7951
7322
  def calculate_comoving_distance(z):
7952
7323
  z = abs(z)