solarviewer 1.0.2__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 (82) hide show
  1. solar_radio_image_viewer/__init__.py +12 -0
  2. solar_radio_image_viewer/assets/add_tab_default.png +0 -0
  3. solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
  4. solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
  5. solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
  6. solar_radio_image_viewer/assets/browse.png +0 -0
  7. solar_radio_image_viewer/assets/browse_light.png +0 -0
  8. solar_radio_image_viewer/assets/close_tab_default.png +0 -0
  9. solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
  10. solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
  11. solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
  12. solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
  13. solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
  14. solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
  15. solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
  16. solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
  17. solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
  18. solar_radio_image_viewer/assets/profile.png +0 -0
  19. solar_radio_image_viewer/assets/profile_light.png +0 -0
  20. solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
  21. solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
  22. solar_radio_image_viewer/assets/reset.png +0 -0
  23. solar_radio_image_viewer/assets/reset_light.png +0 -0
  24. solar_radio_image_viewer/assets/ruler.png +0 -0
  25. solar_radio_image_viewer/assets/ruler_light.png +0 -0
  26. solar_radio_image_viewer/assets/search.png +0 -0
  27. solar_radio_image_viewer/assets/search_light.png +0 -0
  28. solar_radio_image_viewer/assets/settings.png +0 -0
  29. solar_radio_image_viewer/assets/settings_light.png +0 -0
  30. solar_radio_image_viewer/assets/splash.fits +0 -0
  31. solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
  32. solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
  33. solar_radio_image_viewer/assets/zoom_in.png +0 -0
  34. solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
  35. solar_radio_image_viewer/assets/zoom_out.png +0 -0
  36. solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
  37. solar_radio_image_viewer/create_video.py +1345 -0
  38. solar_radio_image_viewer/dialogs.py +2665 -0
  39. solar_radio_image_viewer/from_simpl/__init__.py +184 -0
  40. solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
  41. solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
  42. solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
  43. solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
  44. solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
  45. solar_radio_image_viewer/from_simpl/utils.py +984 -0
  46. solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
  47. solar_radio_image_viewer/helioprojective.py +1916 -0
  48. solar_radio_image_viewer/helioprojective_viewer.py +817 -0
  49. solar_radio_image_viewer/helioviewer_browser.py +1514 -0
  50. solar_radio_image_viewer/main.py +148 -0
  51. solar_radio_image_viewer/move_phasecenter.py +1269 -0
  52. solar_radio_image_viewer/napari_viewer.py +368 -0
  53. solar_radio_image_viewer/noaa_events/__init__.py +32 -0
  54. solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
  55. solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
  56. solar_radio_image_viewer/norms.py +293 -0
  57. solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
  58. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
  59. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
  60. solar_radio_image_viewer/searchable_combobox.py +220 -0
  61. solar_radio_image_viewer/solar_context/__init__.py +41 -0
  62. solar_radio_image_viewer/solar_context/active_regions.py +371 -0
  63. solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
  64. solar_radio_image_viewer/solar_context/context_images.py +297 -0
  65. solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
  66. solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
  67. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
  68. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
  69. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
  70. solar_radio_image_viewer/styles.py +643 -0
  71. solar_radio_image_viewer/utils/__init__.py +32 -0
  72. solar_radio_image_viewer/utils/rate_limiter.py +255 -0
  73. solar_radio_image_viewer/utils.py +952 -0
  74. solar_radio_image_viewer/video_dialog.py +2629 -0
  75. solar_radio_image_viewer/video_utils.py +656 -0
  76. solar_radio_image_viewer/viewer.py +11174 -0
  77. solarviewer-1.0.2.dist-info/METADATA +343 -0
  78. solarviewer-1.0.2.dist-info/RECORD +82 -0
  79. solarviewer-1.0.2.dist-info/WHEEL +5 -0
  80. solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
  81. solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
  82. solarviewer-1.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,817 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import sys
6
+ import numpy as np
7
+ import matplotlib.pyplot as plt
8
+ from matplotlib.figure import Figure
9
+ from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
10
+ from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
11
+ from matplotlib import rcParams
12
+ from matplotlib.colors import Normalize, LogNorm, PowerNorm
13
+ import matplotlib.patches as patches
14
+
15
+ from PyQt5.QtWidgets import (
16
+ QMainWindow,
17
+ QWidget,
18
+ QVBoxLayout,
19
+ QHBoxLayout,
20
+ QLabel,
21
+ QLineEdit,
22
+ QPushButton,
23
+ QComboBox,
24
+ QSlider,
25
+ QCheckBox,
26
+ QGroupBox,
27
+ QFormLayout,
28
+ QMessageBox,
29
+ QSizePolicy,
30
+ QStatusBar,
31
+ QToolBar,
32
+ QAction,
33
+ QToolButton,
34
+ QSpinBox,
35
+ QDoubleSpinBox,
36
+ )
37
+ from PyQt5.QtCore import Qt, QTimer
38
+ from PyQt5.QtGui import QIcon
39
+
40
+ # Try to import sunpy
41
+ try:
42
+ import sunpy
43
+ import sunpy.map
44
+ from sunpy.coordinates import frames
45
+ import astropy.units as u
46
+ from astropy.coordinates import SkyCoord
47
+
48
+ SUNPY_AVAILABLE = True
49
+ except ImportError:
50
+ SUNPY_AVAILABLE = False
51
+ print(
52
+ "Warning: sunpy is not available. Please install sunpy for helioprojective coordinates."
53
+ )
54
+
55
+ # Try to import CASA tools
56
+ try:
57
+ from casatools import image as IA
58
+ from casatasks import exportfits
59
+
60
+ CASA_AVAILABLE = True
61
+ except ImportError:
62
+ CASA_AVAILABLE = False
63
+ print(
64
+ "Warning: CASA tools are not available. Please ensure CASA is properly installed."
65
+ )
66
+
67
+
68
+ def convert_casaimage_to_fits(
69
+ imagename=None, fitsname=None, dropdeg=False, overwrite=True
70
+ ):
71
+ """Convert a CASA image to a FITS file."""
72
+ if not CASA_AVAILABLE:
73
+ print("Error: CASA tools are not available")
74
+ return None
75
+
76
+ if imagename is None:
77
+ print("Error: No input image specified")
78
+ return None
79
+
80
+ try:
81
+ if fitsname is None:
82
+ fitsname = "temp_" + os.path.basename(imagename) + ".fits"
83
+
84
+ # Use exportfits task
85
+ exportfits(
86
+ imagename=imagename,
87
+ fitsimage=fitsname,
88
+ dropdeg=dropdeg,
89
+ overwrite=overwrite,
90
+ )
91
+
92
+ if os.path.exists(fitsname):
93
+ return fitsname
94
+ else:
95
+ print(f"Error: Failed to create FITS file {fitsname}")
96
+ return None
97
+ except Exception as e:
98
+ print(f"Error in convert_casaimage_to_fits: {str(e)}")
99
+ return None
100
+
101
+
102
+ # Import the helioprojective conversion functions
103
+ try:
104
+ from .helioprojective import convert_to_hpc
105
+ from .styles import theme_manager, get_stylesheet
106
+ except ImportError:
107
+ try:
108
+ # For direct script execution
109
+ from helioprojective import convert_to_hpc
110
+ from styles import theme_manager, get_stylesheet
111
+ except ImportError:
112
+ print("Error: Could not import helioprojective conversion functions")
113
+ sys.exit(1)
114
+
115
+
116
+ def update_hpc_matplotlib_theme():
117
+ """Update matplotlib rcParams based on current theme."""
118
+ rcParams.update(theme_manager.matplotlib_params)
119
+
120
+
121
+ rcParams["axes.linewidth"] = 1.4
122
+ rcParams["font.size"] = 12
123
+ update_hpc_matplotlib_theme()
124
+
125
+
126
+ class HelioProjectiveViewer(QMainWindow):
127
+ """
128
+ A separate window for displaying images in helioprojective coordinates.
129
+ """
130
+
131
+ def __init__(
132
+ self,
133
+ imagename=None,
134
+ stokes="I",
135
+ threshold=10,
136
+ rms_box=(0, 200, 0, 130),
137
+ parent=None,
138
+ ):
139
+ super().__init__(parent)
140
+ self.setWindowTitle("Helioprojective Viewer")
141
+ self.resize(1280, 720)
142
+
143
+ # Store parameters
144
+ self.imagename = imagename
145
+ self.stokes = stokes
146
+ self.threshold = threshold
147
+ self.rms_box = rms_box
148
+ self.parent = parent
149
+
150
+ # Initialize variables
151
+ self.helioprojective_map = None
152
+ self.psf = None
153
+ self.temp_fits_file = None
154
+ self.colormap = "viridis"
155
+ self.stretch = "linear"
156
+ self.gamma = 1.0
157
+ self.vmin = None
158
+ self.vmax = None
159
+ self.show_grid = True
160
+ self.show_limb = True
161
+ self.show_beam = True
162
+ self.show_colorbar = True
163
+
164
+ # Apply current theme stylesheet
165
+ self.setStyleSheet(get_stylesheet(theme_manager.palette, theme_manager.is_dark))
166
+
167
+ # Register for theme changes
168
+ theme_manager.register_callback(self._on_theme_changed)
169
+
170
+ # Set up the UI
171
+ self.setup_ui()
172
+
173
+ # Load and display the image if provided
174
+ if imagename:
175
+ # Use QTimer to load image after UI is set up
176
+ QTimer.singleShot(100, self.load_image)
177
+
178
+ def setup_ui(self):
179
+ """Set up the user interface"""
180
+ # Create central widget and layout
181
+ self.central_widget = QWidget()
182
+ self.setCentralWidget(self.central_widget)
183
+ self.main_layout = QHBoxLayout(self.central_widget)
184
+
185
+ # Create left panel for controls
186
+ self.left_panel = QWidget()
187
+ self.left_layout = QVBoxLayout(self.left_panel)
188
+ self.left_panel.setMaximumWidth(400)
189
+
190
+ # Create right panel for figure
191
+ self.right_panel = QWidget()
192
+ self.right_layout = QVBoxLayout(self.right_panel)
193
+
194
+ # Add panels to main layout
195
+ self.main_layout.addWidget(self.left_panel)
196
+ self.main_layout.addWidget(self.right_panel, 1) # Right panel should expand
197
+
198
+ # Create controls
199
+ self.create_display_controls()
200
+ self.create_overlay_controls()
201
+
202
+ # Add a spacer to push controls to the top
203
+ self.left_layout.addStretch(1)
204
+
205
+ # Create figure and canvas
206
+ self.figure = Figure(figsize=(8, 6), dpi=100)
207
+ self.canvas = FigureCanvas(self.figure)
208
+ self.canvas.setMinimumHeight(400)
209
+ self.canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
210
+
211
+ # Create navigation toolbar
212
+ self.toolbar = NavigationToolbar(self.canvas, self)
213
+
214
+ # Add figure and toolbar to right panel
215
+ self.right_layout.addWidget(self.toolbar)
216
+ self.right_layout.addWidget(self.canvas, 1) # Canvas should expand vertically
217
+
218
+ # Create status bar
219
+ self.statusbar = QStatusBar(self)
220
+ self.setStatusBar(self.statusbar)
221
+ self.statusbar.showMessage("Ready")
222
+
223
+ def create_display_controls(self):
224
+ """Create controls for display settings"""
225
+ # Create group box for display controls
226
+ display_group = QGroupBox("Display Settings")
227
+ display_layout = QFormLayout(display_group)
228
+
229
+ # Colormap selection
230
+ self.cmap_combo = QComboBox()
231
+ for cmap in sorted(plt.colormaps()):
232
+ self.cmap_combo.addItem(cmap)
233
+ self.cmap_combo.setCurrentText(self.colormap)
234
+ self.cmap_combo.currentTextChanged.connect(self.on_display_changed)
235
+ display_layout.addRow("Colormap:", self.cmap_combo)
236
+
237
+ # Stretch function
238
+ self.stretch_combo = QComboBox()
239
+ self.stretch_combo.addItems(["linear", "sqrt", "log", "power"])
240
+ self.stretch_combo.setCurrentText(self.stretch)
241
+ self.stretch_combo.currentTextChanged.connect(self.on_display_changed)
242
+ display_layout.addRow("Stretch:", self.stretch_combo)
243
+
244
+ # Gamma (for power stretch)
245
+ gamma_layout = QHBoxLayout()
246
+ self.gamma_slider = QSlider(Qt.Horizontal)
247
+ self.gamma_slider.setRange(1, 100)
248
+ self.gamma_slider.setValue(int(self.gamma * 10))
249
+ self.gamma_slider.valueChanged.connect(self.on_gamma_changed)
250
+
251
+ self.gamma_entry = QLineEdit(str(self.gamma))
252
+ self.gamma_entry.setMaximumWidth(50)
253
+ self.gamma_entry.returnPressed.connect(self.on_gamma_entry_changed)
254
+
255
+ gamma_layout.addWidget(self.gamma_slider)
256
+ gamma_layout.addWidget(self.gamma_entry)
257
+ display_layout.addRow("Gamma:", gamma_layout)
258
+
259
+ # Min/Max values
260
+ range_layout = QHBoxLayout()
261
+ self.vmin_entry = QLineEdit()
262
+ self.vmin_entry.setMaximumWidth(80)
263
+ self.vmax_entry = QLineEdit()
264
+ self.vmax_entry.setMaximumWidth(80)
265
+
266
+ range_layout.addWidget(QLabel("Min:"))
267
+ range_layout.addWidget(self.vmin_entry)
268
+ range_layout.addWidget(QLabel("Max:"))
269
+ range_layout.addWidget(self.vmax_entry)
270
+ display_layout.addRow("Range:", range_layout)
271
+
272
+ # Auto-scale buttons
273
+ scale_layout = QHBoxLayout()
274
+ self.auto_minmax_btn = QPushButton("Min/Max")
275
+ self.auto_minmax_btn.clicked.connect(self.auto_minmax)
276
+ self.auto_percentile_btn = QPushButton("99.5%")
277
+ self.auto_percentile_btn.clicked.connect(self.auto_percentile)
278
+ self.auto_median_btn = QPushButton("Median±5σ")
279
+ self.auto_median_btn.clicked.connect(self.auto_median_rms)
280
+
281
+ scale_layout.addWidget(self.auto_minmax_btn)
282
+ scale_layout.addWidget(self.auto_percentile_btn)
283
+ scale_layout.addWidget(self.auto_median_btn)
284
+ display_layout.addRow("Auto-scale:", scale_layout)
285
+
286
+ # Add the group to the left panel
287
+ self.left_layout.addWidget(display_group)
288
+
289
+ def create_overlay_controls(self):
290
+ """Create controls for overlay settings"""
291
+ # Create group box for overlay controls
292
+ overlay_group = QGroupBox("Overlay Settings")
293
+ overlay_layout = QFormLayout(overlay_group)
294
+
295
+ # Grid lines
296
+ self.show_grid_checkbox = QCheckBox("Show Grid")
297
+ self.show_grid_checkbox.setChecked(self.show_grid)
298
+ self.show_grid_checkbox.stateChanged.connect(self.on_overlay_changed)
299
+ overlay_layout.addRow(self.show_grid_checkbox)
300
+
301
+ # Solar limb
302
+ self.show_limb_checkbox = QCheckBox("Show Solar Limb")
303
+ self.show_limb_checkbox.setChecked(self.show_limb)
304
+ self.show_limb_checkbox.stateChanged.connect(self.on_overlay_changed)
305
+ overlay_layout.addRow(self.show_limb_checkbox)
306
+
307
+ # Beam
308
+ self.show_beam_checkbox = QCheckBox("Show Beam")
309
+ self.show_beam_checkbox.setChecked(self.show_beam)
310
+ self.show_beam_checkbox.stateChanged.connect(self.on_overlay_changed)
311
+ overlay_layout.addRow(self.show_beam_checkbox)
312
+
313
+ # Colorbar
314
+ self.show_colorbar_checkbox = QCheckBox("Show Colorbar")
315
+ self.show_colorbar_checkbox.setChecked(self.show_colorbar)
316
+ self.show_colorbar_checkbox.stateChanged.connect(self.on_overlay_changed)
317
+ overlay_layout.addRow(self.show_colorbar_checkbox)
318
+
319
+ # Add the group to the left panel
320
+ self.left_layout.addWidget(overlay_group)
321
+
322
+ def load_image(self):
323
+ """Load and convert the image to helioprojective coordinates"""
324
+ if not SUNPY_AVAILABLE:
325
+ self.show_status_message("Sunpy is not available. Please install sunpy.")
326
+ return
327
+
328
+ if not self.imagename:
329
+ self.show_status_message("No image specified.")
330
+ return
331
+
332
+ try:
333
+ self.show_status_message(f"Loading image: {self.imagename}")
334
+
335
+ # If it's a CASA image, convert it to FITS first
336
+ if os.path.isdir(self.imagename):
337
+ try:
338
+ self.show_status_message(
339
+ f"Converting CASA image to FITS: {self.imagename}"
340
+ )
341
+ temp_fits = convert_casaimage_to_fits(imagename=self.imagename)
342
+ if temp_fits is None:
343
+ raise RuntimeError("Failed to convert CASA image to FITS")
344
+ self.temp_fits_file = temp_fits
345
+ fits_file = temp_fits
346
+ self.show_status_message(
347
+ f"CASA image converted to FITS: {temp_fits}"
348
+ )
349
+ except Exception as e:
350
+ self.show_status_message(
351
+ f"Error converting CASA image to FITS: {str(e)}"
352
+ )
353
+ print(f"Error converting CASA image to FITS: {str(e)}")
354
+ return
355
+ else:
356
+ fits_file = self.imagename
357
+
358
+ # Convert to helioprojective coordinates
359
+ self.show_status_message(f"Converting to helioprojective coordinates...")
360
+
361
+ # Make sure the rms_box is a tuple of integers
362
+ if isinstance(self.rms_box, list):
363
+ self.rms_box = tuple(self.rms_box)
364
+
365
+ # Convert to helioprojective coordinates
366
+ self.helioprojective_map, csys, self.psf = convert_to_hpc(
367
+ fits_file=fits_file,
368
+ Stokes=self.stokes,
369
+ thres=self.threshold,
370
+ rms_box=self.rms_box,
371
+ )
372
+
373
+ if self.helioprojective_map is None:
374
+ self.show_status_message(
375
+ "Failed to convert to helioprojective coordinates."
376
+ )
377
+ return
378
+
379
+ # Ensure required metadata is present and valid
380
+ """if not hasattr(self.helioprojective_map, "meta"):
381
+ self.helioprojective_map.meta = {}
382
+
383
+ # Fix any 'None' string values in metadata
384
+ for key in ["telescop", "instrume", "detector"]:
385
+ if self.helioprojective_map.meta.get(key) in ["None", "none", None]:
386
+ self.helioprojective_map.meta[key] = "Unknown"
387
+
388
+ # Auto-scale the image
389
+ self.auto_percentile()"""
390
+
391
+ # Plot the image
392
+ self.plot_image()
393
+
394
+ self.show_status_message(
395
+ "Image loaded and converted to helioprojective coordinates."
396
+ )
397
+ except Exception as e:
398
+ import traceback
399
+
400
+ traceback.print_exc()
401
+ error_msg = f"Error loading image: {str(e)}"
402
+ self.show_status_message(error_msg)
403
+ QMessageBox.critical(self, "Error", error_msg)
404
+
405
+ def plot_image(self):
406
+ """Plot the helioprojective map"""
407
+ if self.helioprojective_map is None:
408
+ self.show_status_message("No helioprojective map available to plot.")
409
+ return
410
+
411
+ try:
412
+ # Clear the figure
413
+ self.figure.clear()
414
+
415
+ # Create a subplot with the helioprojective map projection
416
+ try:
417
+ ax = self.figure.add_subplot(111, projection=self.helioprojective_map)
418
+ except Exception as e:
419
+ print(f"Error creating subplot with projection: {str(e)}")
420
+ ax = self.figure.add_subplot(111)
421
+
422
+ # Apply stretch function
423
+ data = self.helioprojective_map.data.copy()
424
+
425
+ # Handle NaN values
426
+ if np.isnan(data).any():
427
+ data = np.nan_to_num(data, nan=0.0)
428
+
429
+ # Apply stretch
430
+ if self.stretch == "sqrt":
431
+ norm = PowerNorm(0.5, vmin=self.vmin, vmax=self.vmax)
432
+ elif self.stretch == "log":
433
+ norm = LogNorm(
434
+ vmin=max(1e-10, self.vmin) if self.vmin else 1e-10, vmax=self.vmax
435
+ )
436
+ elif self.stretch == "power":
437
+ norm = PowerNorm(self.gamma, vmin=self.vmin, vmax=self.vmax)
438
+ else: # linear
439
+ norm = Normalize(vmin=self.vmin, vmax=self.vmax)
440
+
441
+ # Plot the map
442
+ try:
443
+ im = self.helioprojective_map.plot(
444
+ axes=ax,
445
+ cmap=self.colormap,
446
+ norm=norm,
447
+ title=False,
448
+ )
449
+ except Exception as e:
450
+ print(f"Error using sunpy plot: {str(e)}, falling back to imshow")
451
+ im = ax.imshow(
452
+ data,
453
+ cmap=self.colormap,
454
+ norm=norm,
455
+ origin="lower",
456
+ aspect="equal",
457
+ )
458
+
459
+ # Set axis labels
460
+ ax.set_xlabel("Helioprojective Longitude (arcsec)")
461
+ ax.set_ylabel("Helioprojective Latitude (arcsec)")
462
+
463
+ # Set title with observation information
464
+ try:
465
+ if hasattr(self.helioprojective_map, "wavelength") and hasattr(
466
+ self.helioprojective_map, "date"
467
+ ):
468
+ wavelength_str = f"{self.helioprojective_map.wavelength.value:.2f} {self.helioprojective_map.wavelength.unit}"
469
+ title = f"Helioprojective Coordinate Map\n{wavelength_str} - {self.helioprojective_map.date.strftime('%Y-%m-%d %H:%M:%S')}"
470
+ ax.set_title(title, fontsize=12)
471
+ else:
472
+ ax.set_title("Helioprojective Coordinate Map", fontsize=12)
473
+ except Exception as e:
474
+ print(f"Error setting title: {str(e)}")
475
+ ax.set_title("Helioprojective Coordinate Map", fontsize=12)
476
+
477
+ # Draw grid if enabled
478
+ if self.show_grid:
479
+ ax.grid(True, color="white", linestyle="--", alpha=0.5)
480
+
481
+ # Draw solar limb if enabled
482
+ if self.show_limb:
483
+ try:
484
+ self.helioprojective_map.draw_limb(
485
+ axes=ax, color="white", alpha=0.5, linewidth=1
486
+ )
487
+ except Exception as e:
488
+ print(f"Error drawing limb: {str(e)}")
489
+
490
+ # Draw PSF beam if enabled
491
+ if self.show_beam and self.psf:
492
+ try:
493
+ self._draw_beam(ax)
494
+ except Exception as e:
495
+ print(f"Error drawing beam: {str(e)}")
496
+
497
+ # Draw colorbar if enabled
498
+ if self.show_colorbar:
499
+ try:
500
+ self.figure.colorbar(im, ax=ax, label="Intensity")
501
+ except Exception as e:
502
+ print(f"Error drawing colorbar: {str(e)}")
503
+
504
+ # Update the canvas
505
+ self.canvas.draw()
506
+
507
+ self.show_status_message("Helioprojective map plotted successfully.")
508
+ except Exception as e:
509
+ import traceback
510
+
511
+ traceback.print_exc()
512
+ self.show_status_message(f"Error plotting image: {str(e)}")
513
+
514
+ def _draw_beam(self, ax):
515
+ """Draw the PSF beam on the plot"""
516
+ if not self.psf:
517
+ return
518
+
519
+ try:
520
+ # Get beam properties
521
+ if isinstance(self.psf["major"]["value"], list):
522
+ major_deg = float(self.psf["major"]["value"][0])
523
+ else:
524
+ major_deg = float(self.psf["major"]["value"])
525
+
526
+ if isinstance(self.psf["minor"]["value"], list):
527
+ minor_deg = float(self.psf["minor"]["value"][0])
528
+ else:
529
+ minor_deg = float(self.psf["minor"]["value"])
530
+
531
+ if isinstance(self.psf["positionangle"]["value"], list):
532
+ pa_deg = float(self.psf["positionangle"]["value"][0]) - 90
533
+ else:
534
+ pa_deg = float(self.psf["positionangle"]["value"]) - 90
535
+
536
+ # Convert beam size to arcseconds
537
+ major_arcsec = major_deg * 3600
538
+ minor_arcsec = minor_deg * 3600
539
+
540
+ # Get the current axis limits in arcseconds
541
+ xlim = ax.get_xlim()
542
+ ylim = ax.get_ylim()
543
+ view_width = xlim[1] - xlim[0]
544
+ view_height = ylim[1] - ylim[0]
545
+ margin_x = view_width * 0.05
546
+ margin_y = view_height * 0.05
547
+
548
+ # Position the beam in the bottom-left corner
549
+ beam_x = xlim[0] + margin_x + major_arcsec / 2
550
+ beam_y = ylim[0] + margin_y + minor_arcsec / 2
551
+
552
+ # Create the beam ellipse
553
+ beam = patches.Ellipse(
554
+ (beam_x, beam_y),
555
+ major_arcsec,
556
+ minor_arcsec,
557
+ angle=pa_deg,
558
+ fc="white",
559
+ ec="black",
560
+ alpha=0.7,
561
+ )
562
+ ax.add_patch(beam)
563
+
564
+ # Add text with beam size
565
+ ax.text(
566
+ beam_x,
567
+ beam_y + minor_arcsec,
568
+ f"{major_arcsec:.1f}″×{minor_arcsec:.1f}″",
569
+ ha="center",
570
+ va="bottom",
571
+ color="white",
572
+ fontsize=8,
573
+ )
574
+ except Exception as e:
575
+ print(f"Error drawing beam: {str(e)}")
576
+
577
+ def on_display_changed(self):
578
+ """Handle changes to display settings"""
579
+ self.colormap = self.cmap_combo.currentText()
580
+ self.stretch = self.stretch_combo.currentText()
581
+
582
+ # Update gamma controls visibility
583
+ self.gamma_slider.setEnabled(self.stretch == "power")
584
+ self.gamma_entry.setEnabled(self.stretch == "power")
585
+
586
+ # Update the plot
587
+ self.plot_image()
588
+
589
+ def on_overlay_changed(self):
590
+ """Handle changes to overlay settings"""
591
+ self.show_grid = self.show_grid_checkbox.isChecked()
592
+ self.show_limb = self.show_limb_checkbox.isChecked()
593
+ self.show_beam = self.show_beam_checkbox.isChecked()
594
+ self.show_colorbar = self.show_colorbar_checkbox.isChecked()
595
+
596
+ # Update the plot
597
+ self.plot_image()
598
+
599
+ def on_gamma_changed(self):
600
+ """Handle changes to the gamma slider"""
601
+ self.gamma = self.gamma_slider.value() / 10.0
602
+ self.gamma_entry.setText(f"{self.gamma:.1f}")
603
+
604
+ if self.stretch == "power":
605
+ self.plot_image()
606
+
607
+ def on_gamma_entry_changed(self):
608
+ """Handle changes to the gamma entry field"""
609
+ try:
610
+ gamma = float(self.gamma_entry.text())
611
+ if gamma > 0:
612
+ self.gamma = gamma
613
+ self.gamma_slider.setValue(int(gamma * 10))
614
+
615
+ if self.stretch == "power":
616
+ self.plot_image()
617
+ except ValueError:
618
+ # Restore the previous value
619
+ self.gamma_entry.setText(f"{self.gamma:.1f}")
620
+
621
+ def auto_minmax(self):
622
+ """Auto-scale using min/max values"""
623
+ if self.helioprojective_map is None:
624
+ return
625
+
626
+ data = self.helioprojective_map.data
627
+ self.vmin = float(np.nanmin(data))
628
+ self.vmax = float(np.nanmax(data))
629
+
630
+ self.vmin_entry.setText(f"{self.vmin:.6g}")
631
+ self.vmax_entry.setText(f"{self.vmax:.6g}")
632
+
633
+ self.plot_image()
634
+ self.show_status_message(
635
+ f"Auto-scaled to min/max: {self.vmin:.6g} - {self.vmax:.6g}"
636
+ )
637
+
638
+ def auto_percentile(self):
639
+ """Auto-scale using percentile values"""
640
+ if self.helioprojective_map is None:
641
+ return
642
+
643
+ data = self.helioprojective_map.data
644
+ self.vmin = float(np.nanpercentile(data, 0.5))
645
+ self.vmax = float(np.nanpercentile(data, 99.5))
646
+
647
+ self.vmin_entry.setText(f"{self.vmin:.6g}")
648
+ self.vmax_entry.setText(f"{self.vmax:.6g}")
649
+
650
+ self.plot_image()
651
+ self.show_status_message(
652
+ f"Auto-scaled to 0.5-99.5 percentile: {self.vmin:.6g} - {self.vmax:.6g}"
653
+ )
654
+
655
+ def auto_median_rms(self):
656
+ """Auto-scale using median ± 5σ"""
657
+ if self.helioprojective_map is None:
658
+ return
659
+
660
+ data = self.helioprojective_map.data
661
+ median = float(np.nanmedian(data))
662
+ rms = float(np.nanstd(data))
663
+
664
+ self.vmin = median - 5 * rms
665
+ self.vmax = median + 5 * rms
666
+
667
+ self.vmin_entry.setText(f"{self.vmin:.6g}")
668
+ self.vmax_entry.setText(f"{self.vmax:.6g}")
669
+
670
+ self.plot_image()
671
+ self.show_status_message(
672
+ f"Auto-scaled to median±5σ: {self.vmin:.6g} - {self.vmax:.6g}"
673
+ )
674
+
675
+ def show_status_message(self, message):
676
+ """Show a message in the status bar"""
677
+ self.statusbar.showMessage(message)
678
+ print(message)
679
+
680
+ def _on_theme_changed(self, new_theme):
681
+ """Handle theme change events."""
682
+ # Update matplotlib rcParams
683
+ update_hpc_matplotlib_theme()
684
+
685
+ # Update window stylesheet
686
+ self.setStyleSheet(get_stylesheet(theme_manager.palette, theme_manager.is_dark))
687
+
688
+ # Refresh the plot with new theme colors
689
+ if hasattr(self, 'figure') and self.figure and self.helioprojective_map:
690
+ palette = theme_manager.palette
691
+ is_dark = theme_manager.is_dark
692
+
693
+ # Use plot-specific colors for light mode
694
+ if is_dark:
695
+ fig_bg = palette["window"]
696
+ axes_bg = palette["base"]
697
+ text_color = palette["text"]
698
+ else:
699
+ fig_bg = palette.get("plot_bg", "#ffffff")
700
+ axes_bg = palette.get("plot_bg", "#ffffff")
701
+ text_color = palette.get("plot_text", "#1a1a1a")
702
+
703
+ self.figure.set_facecolor(fig_bg)
704
+ for ax in self.figure.get_axes():
705
+ ax.set_facecolor(axes_bg)
706
+ ax.tick_params(colors=text_color)
707
+ ax.xaxis.label.set_color(text_color)
708
+ ax.yaxis.label.set_color(text_color)
709
+ ax.title.set_color(text_color)
710
+ for spine in ax.spines.values():
711
+ spine.set_color(text_color)
712
+ self.canvas.draw_idle()
713
+
714
+ def closeEvent(self, event):
715
+ """Handle window close event"""
716
+ # Unregister theme callback
717
+ theme_manager.unregister_callback(self._on_theme_changed)
718
+
719
+ # Clean up temporary files
720
+ if self.temp_fits_file and os.path.exists(self.temp_fits_file):
721
+ try:
722
+ os.remove(self.temp_fits_file)
723
+ print(f"Removed temporary file: {self.temp_fits_file}")
724
+ except Exception as e:
725
+ print(f"Error removing temporary file: {str(e)}")
726
+
727
+ # Accept the close event
728
+ event.accept()
729
+
730
+
731
+ def main():
732
+ """Main function for standalone execution"""
733
+ import argparse
734
+ from PyQt5.QtWidgets import QApplication
735
+
736
+ # Create argument parser with detailed help
737
+ parser = argparse.ArgumentParser(
738
+ description="""
739
+ Solar Radio Image Helioprojective Viewer
740
+
741
+ A standalone viewer for displaying solar radio images in helioprojective coordinates.
742
+ This tool can handle both FITS files and CASA images, and provides interactive
743
+ visualization with various display options.
744
+
745
+ Examples:
746
+ heliosv myimage.fits
747
+ heliosv myimage.image --stokes I
748
+ heliosv myimage.fits --threshold 5 --rms-box 0 200 0 130
749
+ """,
750
+ formatter_class=argparse.RawDescriptionHelpFormatter,
751
+ )
752
+
753
+ parser.add_argument(
754
+ "imagename", help="Path to the input image (FITS file or CASA image directory)"
755
+ )
756
+
757
+ parser.add_argument(
758
+ "--stokes",
759
+ default="I",
760
+ choices=[
761
+ "I",
762
+ "Q",
763
+ "U",
764
+ "V",
765
+ "L",
766
+ "Lfrac",
767
+ "Vfrac",
768
+ "Q/I",
769
+ "U/I",
770
+ "U/V",
771
+ "PANG",
772
+ ],
773
+ help="Stokes parameter to display (default: I)",
774
+ )
775
+
776
+ parser.add_argument(
777
+ "--threshold",
778
+ type=float,
779
+ default=10,
780
+ help="Threshold value for polarization calculations (default: 10)",
781
+ )
782
+
783
+ parser.add_argument(
784
+ "--rms-box",
785
+ type=int,
786
+ nargs=4,
787
+ default=[0, 200, 0, 130],
788
+ metavar=("X1", "X2", "Y1", "Y2"),
789
+ help="RMS box coordinates as X1 X2 Y1 Y2 (default: 0 200 0 130)",
790
+ )
791
+
792
+ # Parse arguments
793
+ args = parser.parse_args()
794
+
795
+ # Check if image exists
796
+ if not os.path.exists(args.imagename):
797
+ print(f"Error: Image not found: {args.imagename}")
798
+ sys.exit(1)
799
+
800
+ # Create Qt application
801
+ app = QApplication(sys.argv)
802
+
803
+ # Create and show the viewer
804
+ viewer = HelioProjectiveViewer(
805
+ imagename=args.imagename,
806
+ stokes=args.stokes,
807
+ threshold=args.threshold,
808
+ rms_box=args.rms_box,
809
+ )
810
+ viewer.show()
811
+
812
+ # Start the event loop
813
+ sys.exit(app.exec_())
814
+
815
+
816
+ if __name__ == "__main__":
817
+ main()