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.
- solar_radio_image_viewer/__init__.py +12 -0
- solar_radio_image_viewer/assets/add_tab_default.png +0 -0
- solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/browse.png +0 -0
- solar_radio_image_viewer/assets/browse_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
- solar_radio_image_viewer/assets/profile.png +0 -0
- solar_radio_image_viewer/assets/profile_light.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
- solar_radio_image_viewer/assets/reset.png +0 -0
- solar_radio_image_viewer/assets/reset_light.png +0 -0
- solar_radio_image_viewer/assets/ruler.png +0 -0
- solar_radio_image_viewer/assets/ruler_light.png +0 -0
- solar_radio_image_viewer/assets/search.png +0 -0
- solar_radio_image_viewer/assets/search_light.png +0 -0
- solar_radio_image_viewer/assets/settings.png +0 -0
- solar_radio_image_viewer/assets/settings_light.png +0 -0
- solar_radio_image_viewer/assets/splash.fits +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_in.png +0 -0
- solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_out.png +0 -0
- solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
- solar_radio_image_viewer/create_video.py +1345 -0
- solar_radio_image_viewer/dialogs.py +2665 -0
- solar_radio_image_viewer/from_simpl/__init__.py +184 -0
- solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
- solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
- solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
- solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
- solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
- solar_radio_image_viewer/from_simpl/utils.py +984 -0
- solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
- solar_radio_image_viewer/helioprojective.py +1916 -0
- solar_radio_image_viewer/helioprojective_viewer.py +817 -0
- solar_radio_image_viewer/helioviewer_browser.py +1514 -0
- solar_radio_image_viewer/main.py +148 -0
- solar_radio_image_viewer/move_phasecenter.py +1269 -0
- solar_radio_image_viewer/napari_viewer.py +368 -0
- solar_radio_image_viewer/noaa_events/__init__.py +32 -0
- solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
- solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
- solar_radio_image_viewer/norms.py +293 -0
- solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
- solar_radio_image_viewer/searchable_combobox.py +220 -0
- solar_radio_image_viewer/solar_context/__init__.py +41 -0
- solar_radio_image_viewer/solar_context/active_regions.py +371 -0
- solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
- solar_radio_image_viewer/solar_context/context_images.py +297 -0
- solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
- solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
- solar_radio_image_viewer/styles.py +643 -0
- solar_radio_image_viewer/utils/__init__.py +32 -0
- solar_radio_image_viewer/utils/rate_limiter.py +255 -0
- solar_radio_image_viewer/utils.py +952 -0
- solar_radio_image_viewer/video_dialog.py +2629 -0
- solar_radio_image_viewer/video_utils.py +656 -0
- solar_radio_image_viewer/viewer.py +11174 -0
- solarviewer-1.0.2.dist-info/METADATA +343 -0
- solarviewer-1.0.2.dist-info/RECORD +82 -0
- solarviewer-1.0.2.dist-info/WHEEL +5 -0
- solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
- solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
- solarviewer-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1916 @@
|
|
|
1
|
+
# requires sunpy
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
# Only set QT_QPA_PLATFORM to offscreen when running this script directly
|
|
6
|
+
# This prevents issues when importing this module from other scripts
|
|
7
|
+
if __name__ == "__main__":
|
|
8
|
+
os.environ["QT_QPA_PLATFORM"] = "offscreen"
|
|
9
|
+
|
|
10
|
+
import numpy as np, matplotlib.pyplot as plt, astropy.units as u, matplotlib.patches as patches
|
|
11
|
+
from astropy.coordinates import SkyCoord, EarthLocation
|
|
12
|
+
from astropy.time import Time
|
|
13
|
+
from astropy.io import fits
|
|
14
|
+
from datetime import timedelta
|
|
15
|
+
from matplotlib import rcParams
|
|
16
|
+
from casatools import image as IA
|
|
17
|
+
from casatasks import immath, exportfits
|
|
18
|
+
import math
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from sunpy.net import Fido, attrs as a
|
|
22
|
+
import sunpy.map
|
|
23
|
+
from sunpy.coordinates import Helioprojective
|
|
24
|
+
from sunpy.coordinates.sun import P
|
|
25
|
+
|
|
26
|
+
sunpy_imported = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
print("[ERROR] sunpy is not installed or some components are missing")
|
|
29
|
+
sunpy_imported = False
|
|
30
|
+
|
|
31
|
+
from .utils import estimate_rms_near_Sun, remove_pixels_away_from_sun
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
rcParams["axes.linewidth"] = 1.4
|
|
35
|
+
rcParams["font.size"] = 12
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_Earthlocation(fits_file="", lat=None, long=None, height=None, observatory=None):
|
|
39
|
+
hdul = fits.open(fits_file)
|
|
40
|
+
header = hdul[0].header
|
|
41
|
+
POS = None
|
|
42
|
+
if observatory is not None:
|
|
43
|
+
if observatory.upper() == "LOFAR":
|
|
44
|
+
lat = "52:55:53.90"
|
|
45
|
+
long = "06:51:56.95"
|
|
46
|
+
height = 50.16
|
|
47
|
+
elif observatory.upper() == "MWA":
|
|
48
|
+
lat = "-26:42:12.09"
|
|
49
|
+
long = "116:40:14.84"
|
|
50
|
+
height = 375.75
|
|
51
|
+
elif observatory.upper() == "MEERKAT":
|
|
52
|
+
lat = "-30:42:47.36"
|
|
53
|
+
long = "21:26:38.09"
|
|
54
|
+
height = 1050.82
|
|
55
|
+
elif observatory.upper() == "GMRT" or observatory.upper() == "UGMRT":
|
|
56
|
+
lat = "19:05:26.21"
|
|
57
|
+
long = "74:02:59.90"
|
|
58
|
+
height = 639.68
|
|
59
|
+
else:
|
|
60
|
+
print(
|
|
61
|
+
"[WARNING] Observatory parameter could not be understood. Trying from lat and long...."
|
|
62
|
+
)
|
|
63
|
+
if lat is not None and long is not None and height is not None:
|
|
64
|
+
#print(
|
|
65
|
+
# f"[DEBUG] USING observer position from arguments LAT={lat}, LON={long}, HEIGHT={height}"
|
|
66
|
+
#)
|
|
67
|
+
POS = EarthLocation.from_geodetic(lon=long, lat=lat, height=height)
|
|
68
|
+
|
|
69
|
+
if lat is None or long is None or height is None:
|
|
70
|
+
try:
|
|
71
|
+
#print(f"No lat, long input found. Trying to read from header .....")
|
|
72
|
+
x = header["OBSGEO-X"]
|
|
73
|
+
y = header["OBSGEO-Y"]
|
|
74
|
+
z = header["OBSGEO-Z"]
|
|
75
|
+
if abs(x) > 1e6 and abs(y) > 1e6 and abs(z) > 1e6:
|
|
76
|
+
POS = EarthLocation.from_geocentric(x, y, z, unit=u.m)
|
|
77
|
+
else:
|
|
78
|
+
print(
|
|
79
|
+
f"[WARNING] Invalid telescope position in header. Looking for observatory from header..."
|
|
80
|
+
)
|
|
81
|
+
ia_tool = IA()
|
|
82
|
+
ia_tool.open(fits_file)
|
|
83
|
+
metadata_dict = ia_tool.summary(list=False, verbose=True)
|
|
84
|
+
lat, long, height, observatory = extract_telescope_position(
|
|
85
|
+
metadata_dict
|
|
86
|
+
)
|
|
87
|
+
ia_tool.close()
|
|
88
|
+
POS = EarthLocation.from_geodetic(lon=long, lat=lat, height=height)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(f"[ERROR] {str(e)}")
|
|
91
|
+
return POS
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def convert_to_hpc(
|
|
95
|
+
fits_file,
|
|
96
|
+
Stokes="I",
|
|
97
|
+
thres=10,
|
|
98
|
+
lat=None,
|
|
99
|
+
long=None,
|
|
100
|
+
height=None,
|
|
101
|
+
observatory=None,
|
|
102
|
+
rms_box=(0, 200, 0, 130),
|
|
103
|
+
):
|
|
104
|
+
"""
|
|
105
|
+
Convert a FITS file to helioprojective coordinates
|
|
106
|
+
TODO: Redundantly written for ndim>2 and ndim=2, should be refactored
|
|
107
|
+
"""
|
|
108
|
+
if not sunpy_imported:
|
|
109
|
+
print("[ERROR] Cannot convert to helioprojective coordinates without sunpy")
|
|
110
|
+
return None, None, None
|
|
111
|
+
|
|
112
|
+
single_stokes_flag = False
|
|
113
|
+
try:
|
|
114
|
+
ia_tool = IA()
|
|
115
|
+
ia_tool.open(fits_file)
|
|
116
|
+
psf = ia_tool.restoringbeam()
|
|
117
|
+
csys = ia_tool.coordsys()
|
|
118
|
+
ia_tool.close()
|
|
119
|
+
# csys = "hpc"
|
|
120
|
+
except Exception as e:
|
|
121
|
+
raise RuntimeError(f"Failed to open image {fits_file}: {e}")
|
|
122
|
+
|
|
123
|
+
#print("************************")
|
|
124
|
+
#print(f"[INFO] Starting hpc conversion for image {fits_file}")
|
|
125
|
+
# Read the FITS file
|
|
126
|
+
hdu = fits.open(fits_file)
|
|
127
|
+
header = hdu[0].header
|
|
128
|
+
if header["SIMPLE"] == False:
|
|
129
|
+
print("[ERROR] FITS file is not a valid image")
|
|
130
|
+
return None, None, None
|
|
131
|
+
ndim = header["NAXIS"]
|
|
132
|
+
if ndim < 2:
|
|
133
|
+
single_stokes_flag = True
|
|
134
|
+
|
|
135
|
+
if ndim > 2:
|
|
136
|
+
stokes_map = {"I": 0, "Q": 1, "U": 2, "V": 3}
|
|
137
|
+
try:
|
|
138
|
+
# Create a mapping of axis types to their positions
|
|
139
|
+
axis_types = {f"CTYPE{i+1}": header[f"CTYPE{i+1}"] for i in range(ndim)}
|
|
140
|
+
stokes_axis = None
|
|
141
|
+
freq_axis = None
|
|
142
|
+
spatial_axes = []
|
|
143
|
+
|
|
144
|
+
# Find the axes by their types
|
|
145
|
+
for axis_num, axis_type in axis_types.items():
|
|
146
|
+
axis_index = int(axis_num[-1]) - 1 # Convert to 0-based index
|
|
147
|
+
if axis_type == "STOKES":
|
|
148
|
+
stokes_axis = axis_index
|
|
149
|
+
elif axis_type == "FREQ":
|
|
150
|
+
freq_axis = axis_index
|
|
151
|
+
elif axis_type in ["RA---SIN", "DEC--SIN", "HPLN-TAN", "HPLT-TAN"]:
|
|
152
|
+
spatial_axes.append(axis_index)
|
|
153
|
+
|
|
154
|
+
# Account for NumPy/FITS axis reversal
|
|
155
|
+
# In FITS: (RA, DEC, FREQ, STOKES) -> In NumPy: (STOKES, FREQ, DEC, RA)
|
|
156
|
+
numpy_axis_map = {}
|
|
157
|
+
if stokes_axis is not None:
|
|
158
|
+
numpy_axis_map[stokes_axis] = ndim - stokes_axis - 1
|
|
159
|
+
if freq_axis is not None:
|
|
160
|
+
numpy_axis_map[freq_axis] = ndim - freq_axis - 1
|
|
161
|
+
for spatial_axis in spatial_axes:
|
|
162
|
+
numpy_axis_map[spatial_axis] = ndim - spatial_axis - 1
|
|
163
|
+
|
|
164
|
+
#print(f"[DEBUG] FITS axes mapping to NumPy positions: {numpy_axis_map}")
|
|
165
|
+
#print(f"[DEBUG] Spatial axes in FITS (0-indexed): {spatial_axes}")
|
|
166
|
+
|
|
167
|
+
# Update the axes to their NumPy positions
|
|
168
|
+
if stokes_axis is not None:
|
|
169
|
+
original_stokes_axis = (
|
|
170
|
+
stokes_axis + 1
|
|
171
|
+
) # Save the original FITS axis number (1-indexed)
|
|
172
|
+
stokes_axis = numpy_axis_map[stokes_axis]
|
|
173
|
+
if freq_axis is not None:
|
|
174
|
+
original_freq_axis = (
|
|
175
|
+
freq_axis + 1
|
|
176
|
+
) # Save the original FITS axis number (1-indexed)
|
|
177
|
+
freq_axis = numpy_axis_map[freq_axis]
|
|
178
|
+
|
|
179
|
+
if stokes_axis is not None and header[f"NAXIS{original_stokes_axis}"] == 1:
|
|
180
|
+
single_stokes_flag = True
|
|
181
|
+
|
|
182
|
+
data_all = hdu[0].data
|
|
183
|
+
#print(f"[DEBUG] Data from FITS file: {data_all.shape}")
|
|
184
|
+
if Stokes in ["I", "Q", "U", "V"]:
|
|
185
|
+
idx = stokes_map.get(Stokes)
|
|
186
|
+
if idx is None:
|
|
187
|
+
raise ValueError(f"Unknown Stokes parameter: {Stokes}")
|
|
188
|
+
slice_list = [slice(None)] * ndim
|
|
189
|
+
if stokes_axis is not None:
|
|
190
|
+
if single_stokes_flag:
|
|
191
|
+
if Stokes != "I":
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
"The image is single stokes, but the Stokes parameter is not 'I'."
|
|
194
|
+
)
|
|
195
|
+
slice_list[stokes_axis] = idx
|
|
196
|
+
#print(
|
|
197
|
+
#f"[DEBUG] Stokes axis in FITS: {original_stokes_axis} (NumPy position: {stokes_axis})"
|
|
198
|
+
#)
|
|
199
|
+
if freq_axis is not None:
|
|
200
|
+
#print(
|
|
201
|
+
#f"[DEBUG] Frequency axis in FITS: {original_freq_axis} (NumPy position: {freq_axis})"
|
|
202
|
+
#)
|
|
203
|
+
pass
|
|
204
|
+
if freq_axis is not None:
|
|
205
|
+
slice_list[freq_axis] = 0
|
|
206
|
+
data = data_all[tuple(slice_list)]
|
|
207
|
+
elif Stokes == "L":
|
|
208
|
+
if stokes_axis is None:
|
|
209
|
+
raise RuntimeError("The image does not have a Stokes axis.")
|
|
210
|
+
elif single_stokes_flag:
|
|
211
|
+
raise RuntimeError(
|
|
212
|
+
"The image is single stokes, but the Stokes parameter is not 'I'."
|
|
213
|
+
)
|
|
214
|
+
slice_list_Q = [slice(None)] * ndim
|
|
215
|
+
slice_list_U = [slice(None)] * ndim
|
|
216
|
+
slice_list_Q[stokes_axis] = 1
|
|
217
|
+
slice_list_U[stokes_axis] = 2
|
|
218
|
+
slice_list_Q[freq_axis] = 0
|
|
219
|
+
slice_list_U[freq_axis] = 0
|
|
220
|
+
pix_Q = data_all[tuple(slice_list_Q)]
|
|
221
|
+
pix_U = data_all[tuple(slice_list_U)]
|
|
222
|
+
data = np.sqrt(pix_Q**2 + pix_U**2)
|
|
223
|
+
elif Stokes == "Lfrac":
|
|
224
|
+
if stokes_axis is None:
|
|
225
|
+
raise RuntimeError("The image does not have a Stokes axis.")
|
|
226
|
+
elif single_stokes_flag:
|
|
227
|
+
raise RuntimeError(
|
|
228
|
+
"The image is single stokes, but the Stokes parameter is not 'I'."
|
|
229
|
+
)
|
|
230
|
+
try:
|
|
231
|
+
immath(imagename=fits_file, outfile="temp_p_map.im", mode="lpoli")
|
|
232
|
+
p_rms = estimate_rms_near_Sun("temp_p_map.im", "I", rms_box)
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise RuntimeError(f"Error generating polarization map: {e}")
|
|
235
|
+
finally:
|
|
236
|
+
os.system("rm -rf temp_p_map.im")
|
|
237
|
+
slice_list_Q = [slice(None)] * ndim
|
|
238
|
+
slice_list_U = [slice(None)] * ndim
|
|
239
|
+
slice_list_I = [slice(None)] * ndim
|
|
240
|
+
slice_list_Q[stokes_axis] = 1
|
|
241
|
+
slice_list_U[stokes_axis] = 2
|
|
242
|
+
slice_list_I[stokes_axis] = 0
|
|
243
|
+
slice_list_Q[freq_axis] = 0
|
|
244
|
+
slice_list_U[freq_axis] = 0
|
|
245
|
+
slice_list_I[freq_axis] = 0
|
|
246
|
+
pix_Q = data_all[tuple(slice_list_Q)]
|
|
247
|
+
pix_U = data_all[tuple(slice_list_U)]
|
|
248
|
+
pix_I = data_all[tuple(slice_list_I)]
|
|
249
|
+
L = np.sqrt(pix_Q**2 + pix_U**2)
|
|
250
|
+
mask = L < (thres * p_rms)
|
|
251
|
+
L[mask] = 0
|
|
252
|
+
Lfrac = L / pix_I
|
|
253
|
+
data = remove_pixels_away_from_sun(Lfrac, csys, 60)
|
|
254
|
+
elif Stokes == "Vfrac":
|
|
255
|
+
if stokes_axis is None:
|
|
256
|
+
raise RuntimeError("The image does not have a Stokes axis.")
|
|
257
|
+
elif single_stokes_flag:
|
|
258
|
+
raise RuntimeError(
|
|
259
|
+
"The image is single stokes, but the Stokes parameter is not 'I'."
|
|
260
|
+
)
|
|
261
|
+
slice_list_V = [slice(None)] * ndim
|
|
262
|
+
slice_list_I = [slice(None)] * ndim
|
|
263
|
+
slice_list_V[stokes_axis] = 3
|
|
264
|
+
slice_list_I[stokes_axis] = 0
|
|
265
|
+
slice_list_V[freq_axis] = 0
|
|
266
|
+
slice_list_I[freq_axis] = 0
|
|
267
|
+
pix_V = data_all[tuple(slice_list_V)]
|
|
268
|
+
pix_I = data_all[tuple(slice_list_I)]
|
|
269
|
+
v_rms = estimate_rms_near_Sun(fits_file, "V", rms_box)
|
|
270
|
+
mask = np.abs(pix_V) < (thres * v_rms)
|
|
271
|
+
pix_V[mask] = 0
|
|
272
|
+
Vfrac = pix_V / pix_I
|
|
273
|
+
data = remove_pixels_away_from_sun(Vfrac, csys, 60)
|
|
274
|
+
elif Stokes == "Q/I":
|
|
275
|
+
if stokes_axis is None:
|
|
276
|
+
raise RuntimeError("The image does not have a Stokes axis.")
|
|
277
|
+
elif single_stokes_flag:
|
|
278
|
+
raise RuntimeError(
|
|
279
|
+
"The image is single stokes, but the Stokes parameter is not 'I'."
|
|
280
|
+
)
|
|
281
|
+
q_rms = estimate_rms_near_Sun(fits_file, "Q", rms_box)
|
|
282
|
+
slice_list_Q = [slice(None)] * ndim
|
|
283
|
+
slice_list_I = [slice(None)] * ndim
|
|
284
|
+
slice_list_Q[stokes_axis] = 1
|
|
285
|
+
slice_list_I[stokes_axis] = 0
|
|
286
|
+
slice_list_Q[freq_axis] = 0
|
|
287
|
+
slice_list_I[freq_axis] = 0
|
|
288
|
+
pix_Q = data_all[tuple(slice_list_Q)]
|
|
289
|
+
mask = np.abs(pix_Q) < (thres * q_rms)
|
|
290
|
+
pix_Q[mask] = 0
|
|
291
|
+
pix_I = data_all[tuple(slice_list_I)]
|
|
292
|
+
Q_I = pix_Q / pix_I
|
|
293
|
+
data = remove_pixels_away_from_sun(Q_I, csys, 60)
|
|
294
|
+
elif Stokes == "U/I":
|
|
295
|
+
if stokes_axis is None:
|
|
296
|
+
raise RuntimeError("The image does not have a Stokes axis.")
|
|
297
|
+
elif single_stokes_flag:
|
|
298
|
+
raise RuntimeError(
|
|
299
|
+
"The image is single stokes, but the Stokes parameter is not 'I'."
|
|
300
|
+
)
|
|
301
|
+
u_rms = estimate_rms_near_Sun(fits_file, "U", rms_box)
|
|
302
|
+
slice_list_U = [slice(None)] * ndim
|
|
303
|
+
slice_list_I = [slice(None)] * ndim
|
|
304
|
+
slice_list_U[stokes_axis] = 2
|
|
305
|
+
slice_list_I[stokes_axis] = 0
|
|
306
|
+
slice_list_U[freq_axis] = 0
|
|
307
|
+
slice_list_I[freq_axis] = 0
|
|
308
|
+
pix_U = data_all[tuple(slice_list_U)]
|
|
309
|
+
mask = np.abs(pix_U) < (thres * u_rms)
|
|
310
|
+
pix_U[mask] = 0
|
|
311
|
+
pix_I = data_all[tuple(slice_list_I)]
|
|
312
|
+
U_I = pix_U / pix_I
|
|
313
|
+
data = remove_pixels_away_from_sun(U_I, csys, 60)
|
|
314
|
+
elif Stokes == "U/V":
|
|
315
|
+
if stokes_axis is None:
|
|
316
|
+
raise RuntimeError("The image does not have a Stokes axis.")
|
|
317
|
+
elif single_stokes_flag:
|
|
318
|
+
raise RuntimeError(
|
|
319
|
+
"The image is single stokes, but the Stokes parameter is not 'I'."
|
|
320
|
+
)
|
|
321
|
+
u_rms = estimate_rms_near_Sun(fits_file, "U", rms_box)
|
|
322
|
+
slice_list_U = [slice(None)] * ndim
|
|
323
|
+
slice_list_V = [slice(None)] * ndim
|
|
324
|
+
slice_list_U[stokes_axis] = 2
|
|
325
|
+
slice_list_V[stokes_axis] = 3
|
|
326
|
+
slice_list_U[freq_axis] = 0
|
|
327
|
+
slice_list_V[freq_axis] = 0
|
|
328
|
+
pix_U = data_all[tuple(slice_list_U)]
|
|
329
|
+
pix_V = data_all[tuple(slice_list_V)]
|
|
330
|
+
mask = np.abs(pix_U) < (thres * u_rms)
|
|
331
|
+
pix_U[mask] = 0
|
|
332
|
+
U_V = pix_U / pix_V
|
|
333
|
+
data = remove_pixels_away_from_sun(U_V, csys, 60)
|
|
334
|
+
elif Stokes == "PANG":
|
|
335
|
+
if stokes_axis is None:
|
|
336
|
+
raise RuntimeError("The image does not have a Stokes axis.")
|
|
337
|
+
elif single_stokes_flag:
|
|
338
|
+
raise RuntimeError(
|
|
339
|
+
"The image is single stokes, but the Stokes parameter is not 'I'."
|
|
340
|
+
)
|
|
341
|
+
slice_list_Q = [slice(None)] * ndim
|
|
342
|
+
slice_list_U = [slice(None)] * ndim
|
|
343
|
+
slice_list_Q[stokes_axis] = 1
|
|
344
|
+
slice_list_U[stokes_axis] = 2
|
|
345
|
+
slice_list_Q[freq_axis] = 0
|
|
346
|
+
slice_list_U[freq_axis] = 0
|
|
347
|
+
pix_Q = data_all[tuple(slice_list_Q)]
|
|
348
|
+
pix_U = data_all[tuple(slice_list_U)]
|
|
349
|
+
PANG = 0.5 * np.arctan2(pix_U, pix_Q) * 180 / np.pi
|
|
350
|
+
data = PANG
|
|
351
|
+
else:
|
|
352
|
+
slice_list_I = [slice(None)] * ndim
|
|
353
|
+
slice_list_I[stokes_axis] = 0
|
|
354
|
+
slice_list_I[freq_axis] = 0
|
|
355
|
+
data = data_all[tuple(slice_list_I)]
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
#print(f"[ERROR] {str(e)}")
|
|
359
|
+
return None, None, None
|
|
360
|
+
|
|
361
|
+
# Get frequency from the appropriate header keyword based on the original FITS axis
|
|
362
|
+
if freq_axis is not None:
|
|
363
|
+
frequency = header[f"CRVAL{original_freq_axis}"] * u.Hz
|
|
364
|
+
#print(f"Using frequency from CRVAL{original_freq_axis}: {frequency}")
|
|
365
|
+
else:
|
|
366
|
+
# Try to find frequency from other header keywords if freq_axis is not identified
|
|
367
|
+
if "FREQ" in header:
|
|
368
|
+
frequency = header["FREQ"] * u.Hz
|
|
369
|
+
#print(f"Using frequency from FREQ keyword: {frequency}")
|
|
370
|
+
else:
|
|
371
|
+
raise RuntimeError("Could not determine frequency from header")
|
|
372
|
+
obstime = Time(header["date-obs"])
|
|
373
|
+
ia_tool = IA()
|
|
374
|
+
ia_tool.open(fits_file)
|
|
375
|
+
metadata_dict = ia_tool.summary(list=False, verbose=True)
|
|
376
|
+
lat, long, height, observatory = extract_telescope_position(metadata_dict)
|
|
377
|
+
ia_tool.close()
|
|
378
|
+
if (
|
|
379
|
+
observatory.upper() == "LOFAR"
|
|
380
|
+
or observatory.upper() == "MWA"
|
|
381
|
+
or observatory.upper() == "MEERKAT"
|
|
382
|
+
or observatory.upper() == "GMRT"
|
|
383
|
+
or observatory.upper() == "UGMRT"
|
|
384
|
+
):
|
|
385
|
+
#print(f"Observatory: {observatory}")
|
|
386
|
+
pass
|
|
387
|
+
else:
|
|
388
|
+
observatory = None
|
|
389
|
+
POS = get_Earthlocation(fits_file=fits_file, observatory=observatory)
|
|
390
|
+
gcrs = SkyCoord(POS.get_gcrs(obstime))
|
|
391
|
+
reference_coord = SkyCoord(
|
|
392
|
+
header["CRVAL1"] * u.Unit(header["CUNIT1"]),
|
|
393
|
+
header["CRVAL2"] * u.Unit(header["CUNIT2"]),
|
|
394
|
+
frame="gcrs",
|
|
395
|
+
obstime=obstime,
|
|
396
|
+
obsgeoloc=gcrs.cartesian,
|
|
397
|
+
obsgeovel=gcrs.velocity.to_cartesian(),
|
|
398
|
+
distance=gcrs.hcrs.distance,
|
|
399
|
+
)
|
|
400
|
+
reference_coord_arcsec = reference_coord.transform_to(
|
|
401
|
+
Helioprojective(observer=gcrs)
|
|
402
|
+
)
|
|
403
|
+
cdelta_1 = (np.abs(header["CDELT1"]) * u.deg).to(u.arcsec)
|
|
404
|
+
cdelta_2 = (np.abs(header["CDELT2"]) * u.deg).to(u.arcsec)
|
|
405
|
+
P_angle = P(obstime) * -1
|
|
406
|
+
#print(f"Rotating by {P_angle}")
|
|
407
|
+
|
|
408
|
+
# Ensure frequency unit is properly formatted for the FITS header
|
|
409
|
+
new_header = sunpy.map.make_fitswcs_header(
|
|
410
|
+
data,
|
|
411
|
+
reference_coord_arcsec,
|
|
412
|
+
reference_pixel=u.Quantity(
|
|
413
|
+
[header["CRPIX1"] - 1, header["CRPIX2"] - 1] * u.pix,
|
|
414
|
+
),
|
|
415
|
+
scale=u.Quantity([cdelta_1, cdelta_2] * u.arcsec / u.pix),
|
|
416
|
+
rotation_angle=P_angle,
|
|
417
|
+
wavelength=frequency.to(u.MHz).round(2),
|
|
418
|
+
observatory=observatory,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
# Add additional metadata to the header
|
|
422
|
+
new_header["DATE-OBS"] = header.get("DATE-OBS", obstime.isot)
|
|
423
|
+
new_header["TELESCOP"] = observatory
|
|
424
|
+
new_header["INSTRUME"] = header.get("INSTRUME", "Unknown")
|
|
425
|
+
new_header["OBJECT"] = "Sun"
|
|
426
|
+
new_header["ORIGIN"] = "Solar Radio Image Viewer"
|
|
427
|
+
|
|
428
|
+
# Add frequency information
|
|
429
|
+
new_header["FREQ"] = frequency.to(u.Hz).value
|
|
430
|
+
new_header["FREQUNIT"] = "Hz"
|
|
431
|
+
|
|
432
|
+
# Add coordinate system information
|
|
433
|
+
new_header["WCSNAME"] = "Helioprojective"
|
|
434
|
+
new_header["CTYPE1"] = (
|
|
435
|
+
"HPLN-TAN" # Helioprojective longitude with TAN projection
|
|
436
|
+
)
|
|
437
|
+
new_header["CTYPE2"] = (
|
|
438
|
+
"HPLT-TAN" # Helioprojective latitude with TAN projection
|
|
439
|
+
)
|
|
440
|
+
new_header["CUNIT1"] = "arcsec"
|
|
441
|
+
new_header["CUNIT2"] = "arcsec"
|
|
442
|
+
|
|
443
|
+
# Add original processing information if available
|
|
444
|
+
if "HISTORY" in new_header:
|
|
445
|
+
for history in header["HISTORY"]:
|
|
446
|
+
new_header.append(history)
|
|
447
|
+
|
|
448
|
+
# Add new history entry
|
|
449
|
+
new_history = f"Converted to helioprojective coordinates on {Time.now().isot}"
|
|
450
|
+
if "HISTORY" in new_header:
|
|
451
|
+
if isinstance(new_header["HISTORY"], list):
|
|
452
|
+
new_header["HISTORY"].append(new_history)
|
|
453
|
+
else:
|
|
454
|
+
new_header["HISTORY"] = [new_header["HISTORY"], new_history]
|
|
455
|
+
else:
|
|
456
|
+
new_header["HISTORY"] = new_history
|
|
457
|
+
|
|
458
|
+
# Add information about the Stokes parameter
|
|
459
|
+
new_header["STOKES"] = Stokes
|
|
460
|
+
|
|
461
|
+
# Add beam information if available (adjust BPA by P-angle rotation)
|
|
462
|
+
if psf:
|
|
463
|
+
new_header["BMAJ"] = header["BMAJ"]
|
|
464
|
+
new_header["BMIN"] = header["BMIN"]
|
|
465
|
+
# Adjust beam position angle by the P-angle rotation
|
|
466
|
+
original_bpa = header.get("BPA", 0)
|
|
467
|
+
rotated_bpa = original_bpa + P_angle.to(u.deg).value
|
|
468
|
+
new_header["BPA"] = rotated_bpa
|
|
469
|
+
|
|
470
|
+
# Ensure the wavelength unit is properly formatted
|
|
471
|
+
if "waveunit" in new_header:
|
|
472
|
+
waveunit = new_header["waveunit"].strip().lower()
|
|
473
|
+
if waveunit == "mhz":
|
|
474
|
+
new_header["waveunit"] = "MHz"
|
|
475
|
+
elif waveunit == "ghz":
|
|
476
|
+
new_header["waveunit"] = "GHz"
|
|
477
|
+
elif waveunit == "khz":
|
|
478
|
+
new_header["waveunit"] = "kHz"
|
|
479
|
+
elif waveunit == "hz":
|
|
480
|
+
new_header["waveunit"] = "Hz"
|
|
481
|
+
|
|
482
|
+
map = sunpy.map.Map(data, new_header)
|
|
483
|
+
try:
|
|
484
|
+
map_rotated = map.rotate(P_angle)
|
|
485
|
+
except Exception as e:
|
|
486
|
+
print(f"[WARNING] Error rotating map: {e}. Trying another method.")
|
|
487
|
+
try:
|
|
488
|
+
P_angle_deg = P_angle.to(u.deg).value
|
|
489
|
+
|
|
490
|
+
def deg_to_dms_str(deg):
|
|
491
|
+
# Determine the sign and work with the absolute value
|
|
492
|
+
sign = "-" if deg < 0 else ""
|
|
493
|
+
abs_deg = abs(deg)
|
|
494
|
+
|
|
495
|
+
# Extract degrees, minutes, and seconds
|
|
496
|
+
degrees = int(abs_deg)
|
|
497
|
+
minutes_full = (abs_deg - degrees) * 60
|
|
498
|
+
minutes = int(minutes_full)
|
|
499
|
+
seconds = (minutes_full - minutes) * 60
|
|
500
|
+
|
|
501
|
+
# Format string: you can adjust the precision of seconds as needed
|
|
502
|
+
#print(f"[DEBUG] Rotating by {sign}{degrees}d{minutes}m{seconds:.1f}s")
|
|
503
|
+
return f"{sign}{degrees}d{minutes}m{seconds:.1f}s"
|
|
504
|
+
|
|
505
|
+
map_rotated = map.rotate(deg_to_dms_str(P_angle_deg))
|
|
506
|
+
except Exception as e:
|
|
507
|
+
print(f"[ERROR] Error rotating map: {e}")
|
|
508
|
+
map_rotated = None
|
|
509
|
+
|
|
510
|
+
return map_rotated, csys, psf
|
|
511
|
+
elif ndim == 2:
|
|
512
|
+
data = hdu[0].data
|
|
513
|
+
try:
|
|
514
|
+
# Try CRVAL3 first, then fallback to RESTFRQ or FREQ
|
|
515
|
+
if "CRVAL3" in header:
|
|
516
|
+
freq = header["CRVAL3"] * u.Hz
|
|
517
|
+
elif "RESTFRQ" in header:
|
|
518
|
+
freq = header["RESTFRQ"] * u.Hz
|
|
519
|
+
elif "FREQ" in header:
|
|
520
|
+
freq = header["FREQ"] * u.Hz
|
|
521
|
+
else:
|
|
522
|
+
print(f"[WARNING] No frequency keyword found in header")
|
|
523
|
+
freq = None
|
|
524
|
+
except Exception as e:
|
|
525
|
+
print(f"[ERROR] Error getting frequency: {e}")
|
|
526
|
+
freq = None
|
|
527
|
+
try:
|
|
528
|
+
obstime = Time(hdu[0].header["DATE-OBS"])
|
|
529
|
+
except Exception as e:
|
|
530
|
+
print(f"[ERROR] Error getting observation time: {e}")
|
|
531
|
+
obstime = None
|
|
532
|
+
ia_tool = IA()
|
|
533
|
+
ia_tool.open(fits_file)
|
|
534
|
+
metadata_dict = ia_tool.summary(list=False, verbose=True)
|
|
535
|
+
lat, long, height, observatory = extract_telescope_position(metadata_dict)
|
|
536
|
+
ia_tool.close()
|
|
537
|
+
if (
|
|
538
|
+
observatory.upper() == "LOFAR"
|
|
539
|
+
or observatory.upper() == "MWA"
|
|
540
|
+
or observatory.upper() == "MEERKAT"
|
|
541
|
+
or observatory.upper() == "GMRT"
|
|
542
|
+
or observatory.upper() == "UGMRT"
|
|
543
|
+
):
|
|
544
|
+
#print(f"Observatory: {observatory}")
|
|
545
|
+
pass
|
|
546
|
+
else:
|
|
547
|
+
observatory = None
|
|
548
|
+
POS = get_Earthlocation(fits_file=fits_file, observatory=observatory)
|
|
549
|
+
gcrs = SkyCoord(POS.get_gcrs(obstime))
|
|
550
|
+
reference_coord = SkyCoord(
|
|
551
|
+
header["CRVAL1"] * u.Unit(header["CUNIT1"]),
|
|
552
|
+
header["CRVAL2"] * u.Unit(header["CUNIT2"]),
|
|
553
|
+
frame="gcrs",
|
|
554
|
+
obstime=obstime,
|
|
555
|
+
obsgeoloc=gcrs.cartesian,
|
|
556
|
+
obsgeovel=gcrs.velocity.to_cartesian(),
|
|
557
|
+
distance=gcrs.hcrs.distance,
|
|
558
|
+
)
|
|
559
|
+
# Transform to Helioprojective frame (same as 4D branch)
|
|
560
|
+
reference_coord_arcsec = reference_coord.transform_to(
|
|
561
|
+
Helioprojective(observer=gcrs)
|
|
562
|
+
)
|
|
563
|
+
cdelta_1 = (np.abs(header["CDELT1"]) * u.deg).to(u.arcsec)
|
|
564
|
+
cdelta_2 = (np.abs(header["CDELT2"]) * u.deg).to(u.arcsec)
|
|
565
|
+
P_angle = P(obstime) * -1
|
|
566
|
+
#print(f"Rotating by {P_angle}")
|
|
567
|
+
new_header = sunpy.map.make_fitswcs_header(
|
|
568
|
+
data,
|
|
569
|
+
reference_coord_arcsec,
|
|
570
|
+
reference_pixel=u.Quantity(
|
|
571
|
+
[header["CRPIX1"] - 1, header["CRPIX2"] - 1] * u.pix
|
|
572
|
+
),
|
|
573
|
+
scale=u.Quantity([cdelta_1, cdelta_2] * u.arcsec / u.pix),
|
|
574
|
+
rotation_angle=P_angle,
|
|
575
|
+
wavelength=freq.to(u.MHz).round(2),
|
|
576
|
+
observatory=observatory,
|
|
577
|
+
)
|
|
578
|
+
new_header["DATE-OBS"] = header.get("DATE-OBS", obstime.isot)
|
|
579
|
+
new_header["TELESCOP"] = observatory
|
|
580
|
+
new_header["INSTRUME"] = header.get("INSTRUME", "Unknown")
|
|
581
|
+
new_header["OBJECT"] = "Sun"
|
|
582
|
+
new_header["ORIGIN"] = "Solar Radio Image Viewer"
|
|
583
|
+
try:
|
|
584
|
+
new_header["FREQ"] = freq.to(u.Hz).value
|
|
585
|
+
new_header["FREQUNIT"] = "Hz"
|
|
586
|
+
except Exception as e:
|
|
587
|
+
print(f"[ERROR] Error adding frequency information: {e}")
|
|
588
|
+
new_header["WCSNAME"] = "Helioprojective"
|
|
589
|
+
new_header["CTYPE1"] = "HPLN-TAN"
|
|
590
|
+
new_header["CTYPE2"] = "HPLT-TAN"
|
|
591
|
+
new_header["CUNIT1"] = "arcsec"
|
|
592
|
+
new_header["CUNIT2"] = "arcsec"
|
|
593
|
+
new_header["STOKES"] = Stokes
|
|
594
|
+
if psf:
|
|
595
|
+
new_header["BMAJ"] = header["BMAJ"]
|
|
596
|
+
new_header["BMIN"] = header["BMIN"]
|
|
597
|
+
# Adjust beam position angle by the P-angle rotation
|
|
598
|
+
original_bpa = header.get("BPA", 0)
|
|
599
|
+
rotated_bpa = original_bpa + P_angle.to(u.deg).value
|
|
600
|
+
new_header["BPA"] = rotated_bpa
|
|
601
|
+
map = sunpy.map.Map(data, new_header)
|
|
602
|
+
try:
|
|
603
|
+
map_rotated = map.rotate(P_angle)
|
|
604
|
+
except Exception as e:
|
|
605
|
+
print(f"[ERROR] Error rotating map: {e}. Trying another method.")
|
|
606
|
+
try:
|
|
607
|
+
P_angle_deg = P_angle.to(u.deg).value
|
|
608
|
+
|
|
609
|
+
def deg_to_dms_str(deg):
|
|
610
|
+
# Determine the sign and work with the absolute value
|
|
611
|
+
sign = "-" if deg < 0 else ""
|
|
612
|
+
abs_deg = abs(deg)
|
|
613
|
+
|
|
614
|
+
map_rotated = map.rotate(deg_to_dms_str(P_angle_deg))
|
|
615
|
+
except Exception as e:
|
|
616
|
+
print(f"[ERROR] Error rotating map: {e}")
|
|
617
|
+
map_rotated = None
|
|
618
|
+
|
|
619
|
+
return map_rotated, csys, psf
|
|
620
|
+
else:
|
|
621
|
+
return None, None, None
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def convert_casaimage_to_fits(
|
|
625
|
+
imagename=None, fitsname="temp.fits", dropdeg=False, overwrite=True
|
|
626
|
+
):
|
|
627
|
+
"""
|
|
628
|
+
Convert a CASA image to a FITS file.
|
|
629
|
+
"""
|
|
630
|
+
if imagename is None:
|
|
631
|
+
raise ValueError("imagename is required")
|
|
632
|
+
from casatasks import exportfits
|
|
633
|
+
|
|
634
|
+
exportfits(imagename=imagename, fitsimage=fitsname, overwrite=True, dropdeg=dropdeg)
|
|
635
|
+
return fitsname
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def plot_helioprojective_map(
|
|
639
|
+
fits_file,
|
|
640
|
+
output_file="helioprojective_map.png",
|
|
641
|
+
cmap="viridis",
|
|
642
|
+
figsize=(14, 12),
|
|
643
|
+
dpi=300,
|
|
644
|
+
show_limb=True,
|
|
645
|
+
show_grid=True,
|
|
646
|
+
**kwargs,
|
|
647
|
+
):
|
|
648
|
+
"""
|
|
649
|
+
Plot a FITS file in helioprojective coordinates and save it to a file.
|
|
650
|
+
|
|
651
|
+
Parameters
|
|
652
|
+
----------
|
|
653
|
+
fits_file : str
|
|
654
|
+
Path to the FITS file.
|
|
655
|
+
output_file : str, optional
|
|
656
|
+
Path to save the output image. Default is "helioprojective_map.png".
|
|
657
|
+
cmap : str, optional
|
|
658
|
+
Colormap to use for the plot. Default is "viridis".
|
|
659
|
+
figsize : tuple, optional
|
|
660
|
+
Figure size in inches. Default is (14, 12).
|
|
661
|
+
dpi : int, optional
|
|
662
|
+
DPI for the saved image. Default is 300.
|
|
663
|
+
show_limb : bool, optional
|
|
664
|
+
Whether to show the solar limb. Default is True.
|
|
665
|
+
show_grid : bool, optional
|
|
666
|
+
Whether to show coordinate grid lines. Default is True.
|
|
667
|
+
**kwargs : dict
|
|
668
|
+
Additional keyword arguments to pass to convert_to_hpc.
|
|
669
|
+
|
|
670
|
+
Returns
|
|
671
|
+
-------
|
|
672
|
+
map : sunpy.map.Map or None
|
|
673
|
+
The helioprojective map if successful, None otherwise.
|
|
674
|
+
"""
|
|
675
|
+
map, cdelta_1, cdelta_2 = convert_to_hpc(fits_file, **kwargs)
|
|
676
|
+
if map is not None:
|
|
677
|
+
# Create a figure with a specific size
|
|
678
|
+
fig = plt.figure(figsize=figsize)
|
|
679
|
+
|
|
680
|
+
# Create a SunPy plot with proper helioprojective coordinates
|
|
681
|
+
ax = fig.add_subplot(111, projection=map)
|
|
682
|
+
|
|
683
|
+
# Plot the map with proper coordinate axes and customized appearance
|
|
684
|
+
im = map.plot(axes=ax, cmap=cmap, title=False)
|
|
685
|
+
|
|
686
|
+
# Add a colorbar with a label
|
|
687
|
+
cbar = plt.colorbar(im, ax=ax, label="Intensity")
|
|
688
|
+
cbar.ax.tick_params(labelsize=12)
|
|
689
|
+
|
|
690
|
+
# Add grid lines for helioprojective coordinates if requested
|
|
691
|
+
if show_grid:
|
|
692
|
+
ax.grid(color="white", linestyle="--", alpha=0.7)
|
|
693
|
+
|
|
694
|
+
# Add a limb (solar edge) overlay if requested
|
|
695
|
+
if show_limb:
|
|
696
|
+
map.draw_limb(axes=ax, color="white", alpha=0.5, linewidth=1.5)
|
|
697
|
+
|
|
698
|
+
# Set title with observation information
|
|
699
|
+
wavelength_str = f"{map.wavelength.value:.2f} {map.wavelength.unit}"
|
|
700
|
+
title = f"Helioprojective Coordinate Map\n{wavelength_str} - {map.date.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
701
|
+
ax.set_title(title, fontsize=16)
|
|
702
|
+
|
|
703
|
+
# Add coordinate labels with larger font
|
|
704
|
+
ax.set_xlabel("Helioprojective Longitude (arcsec)", fontsize=14)
|
|
705
|
+
ax.set_ylabel("Helioprojective Latitude (arcsec)", fontsize=14)
|
|
706
|
+
ax.tick_params(labelsize=12)
|
|
707
|
+
|
|
708
|
+
# Adjust layout to make room for labels
|
|
709
|
+
plt.tight_layout()
|
|
710
|
+
|
|
711
|
+
# Save the figure with high resolution
|
|
712
|
+
plt.savefig(output_file, dpi=dpi, bbox_inches="tight")
|
|
713
|
+
print(f"[INFO] Map saved to {output_file} with proper helioprojective coordinates")
|
|
714
|
+
|
|
715
|
+
return map
|
|
716
|
+
else:
|
|
717
|
+
print("[ERROR] Failed to convert to helioprojective coordinates")
|
|
718
|
+
return None
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def save_helioprojective_map(map_obj, output_file):
|
|
722
|
+
"""
|
|
723
|
+
Save a helioprojective map as a FITS file.
|
|
724
|
+
|
|
725
|
+
Parameters
|
|
726
|
+
----------
|
|
727
|
+
map_obj : sunpy.map.Map
|
|
728
|
+
The helioprojective map to save.
|
|
729
|
+
output_file : str
|
|
730
|
+
The path to save the FITS file.
|
|
731
|
+
|
|
732
|
+
Returns
|
|
733
|
+
-------
|
|
734
|
+
bool
|
|
735
|
+
True if the file was saved successfully, False otherwise.
|
|
736
|
+
"""
|
|
737
|
+
if not sunpy_imported:
|
|
738
|
+
print("[ERROR] Cannot save helioprojective map without sunpy")
|
|
739
|
+
return False
|
|
740
|
+
|
|
741
|
+
if map_obj is None:
|
|
742
|
+
print("[ERROR] No map to save")
|
|
743
|
+
return False
|
|
744
|
+
|
|
745
|
+
try:
|
|
746
|
+
# Save the map as a FITS file
|
|
747
|
+
map_obj.save(output_file, overwrite=True)
|
|
748
|
+
print(f"[INFO] Helioprojective map saved to {output_file}")
|
|
749
|
+
return True
|
|
750
|
+
except Exception as e:
|
|
751
|
+
print(f"[ERROR] Error saving helioprojective map: {str(e)}")
|
|
752
|
+
return False
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def convert_and_save_hpc(
|
|
756
|
+
input_fits_file,
|
|
757
|
+
output_fits_file,
|
|
758
|
+
Stokes="I",
|
|
759
|
+
thres=10,
|
|
760
|
+
lat=None,
|
|
761
|
+
long=None,
|
|
762
|
+
height=None,
|
|
763
|
+
observatory=None,
|
|
764
|
+
overwrite=True,
|
|
765
|
+
temp_suffix="",
|
|
766
|
+
):
|
|
767
|
+
"""
|
|
768
|
+
Convert a FITS file to helioprojective coordinates and save it as a new FITS file.
|
|
769
|
+
|
|
770
|
+
Parameters
|
|
771
|
+
----------
|
|
772
|
+
input_fits_file : str
|
|
773
|
+
Path to the input FITS file/Casa Image.
|
|
774
|
+
output_fits_file : str
|
|
775
|
+
Path to save the output FITS file.
|
|
776
|
+
Stokes : str, optional
|
|
777
|
+
Stokes parameter to use. Default is "I".
|
|
778
|
+
thres : float, optional
|
|
779
|
+
Threshold value. Default is 10.
|
|
780
|
+
lat : str, optional
|
|
781
|
+
Observer latitude. Default is "-26:42:11.95" (MWA).
|
|
782
|
+
long : str, optional
|
|
783
|
+
Observer longitude. Default is "116:40:14.93" (MWA).
|
|
784
|
+
height : float, optional
|
|
785
|
+
Observer height in meters. Default is 377.8 (MWA).
|
|
786
|
+
observatory : str, optional
|
|
787
|
+
Observatory name. Default is "MWA".
|
|
788
|
+
overwrite : bool, optional
|
|
789
|
+
Whether to overwrite the output file if it exists. Default is True.
|
|
790
|
+
temp_suffix : str, optional
|
|
791
|
+
Suffix to add to temporary files to avoid conflicts. Default is "".
|
|
792
|
+
|
|
793
|
+
Returns
|
|
794
|
+
-------
|
|
795
|
+
bool
|
|
796
|
+
True if the file was saved successfully, False otherwise.
|
|
797
|
+
"""
|
|
798
|
+
if not sunpy_imported:
|
|
799
|
+
print("[ERROR] Cannot convert to helioprojective coordinates without sunpy")
|
|
800
|
+
return False
|
|
801
|
+
|
|
802
|
+
try:
|
|
803
|
+
not_fits_flag = False
|
|
804
|
+
original_file = input_fits_file # Keep reference to original
|
|
805
|
+
|
|
806
|
+
# Fix: use AND instead of OR to check if NOT a FITS file
|
|
807
|
+
if not (input_fits_file.endswith(".fits") or input_fits_file.endswith(".fts")):
|
|
808
|
+
# Create unique temp filename with provided suffix
|
|
809
|
+
temp_filename = f"temp{temp_suffix}.fits"
|
|
810
|
+
input_fits_file = convert_casaimage_to_fits(
|
|
811
|
+
imagename=input_fits_file, fitsname=temp_filename
|
|
812
|
+
)
|
|
813
|
+
#print(f"Converted casaimage {original_file} to a temporary fits file ...")
|
|
814
|
+
not_fits_flag = True
|
|
815
|
+
|
|
816
|
+
# Get BUNIT from original file BEFORE any processing
|
|
817
|
+
original_bunit = None
|
|
818
|
+
try:
|
|
819
|
+
from astropy.io import fits as afits
|
|
820
|
+
with afits.open(input_fits_file) as orig_hdu:
|
|
821
|
+
original_bunit = orig_hdu[0].header.get('BUNIT', None)
|
|
822
|
+
#print(f"[HPC] Found BUNIT in original: {original_bunit}")
|
|
823
|
+
except Exception as e:
|
|
824
|
+
print(f"[ERROR] Could not read BUNIT: {e}")
|
|
825
|
+
|
|
826
|
+
# Convert to helioprojective coordinates
|
|
827
|
+
map_obj, cdelta_1, cdelta_2 = convert_to_hpc(
|
|
828
|
+
input_fits_file,
|
|
829
|
+
Stokes=Stokes,
|
|
830
|
+
thres=thres,
|
|
831
|
+
lat=lat,
|
|
832
|
+
long=long,
|
|
833
|
+
height=height,
|
|
834
|
+
observatory=observatory,
|
|
835
|
+
)
|
|
836
|
+
if not_fits_flag:
|
|
837
|
+
# Remove the unique temp file
|
|
838
|
+
temp_filename = f"temp{temp_suffix}.fits"
|
|
839
|
+
os.system(f"rm {temp_filename}")
|
|
840
|
+
|
|
841
|
+
if map_obj is None:
|
|
842
|
+
print("[ERROR] Failed to convert to helioprojective coordinates")
|
|
843
|
+
return False
|
|
844
|
+
|
|
845
|
+
# Save the map as a FITS file
|
|
846
|
+
map_obj.save(output_fits_file, overwrite=overwrite)
|
|
847
|
+
|
|
848
|
+
# Add BUNIT directly to the saved file
|
|
849
|
+
if original_bunit:
|
|
850
|
+
from astropy.io import fits as afits
|
|
851
|
+
with afits.open(output_fits_file, mode='update') as hdul:
|
|
852
|
+
hdul[0].header['BUNIT'] = original_bunit
|
|
853
|
+
hdul.flush()
|
|
854
|
+
#print(f"[DEBUG] Added BUNIT={original_bunit} to HPC file")
|
|
855
|
+
|
|
856
|
+
print(f"[INFO] Helioprojective map saved to {output_fits_file}")
|
|
857
|
+
|
|
858
|
+
# Return the map object for further use if needed
|
|
859
|
+
return True
|
|
860
|
+
except Exception as e:
|
|
861
|
+
print(f"[ERROR] Error converting and saving to helioprojective coordinates: {str(e)}")
|
|
862
|
+
import traceback
|
|
863
|
+
|
|
864
|
+
traceback.print_exc()
|
|
865
|
+
return False
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def convert_and_save_hpc_all_stokes(
|
|
869
|
+
input_fits_file,
|
|
870
|
+
output_fits_file,
|
|
871
|
+
thres=10,
|
|
872
|
+
lat=None,
|
|
873
|
+
long=None,
|
|
874
|
+
height=None,
|
|
875
|
+
observatory=None,
|
|
876
|
+
overwrite=True,
|
|
877
|
+
temp_suffix="",
|
|
878
|
+
):
|
|
879
|
+
"""
|
|
880
|
+
Convert a FITS file to helioprojective coordinates for all Stokes parameters,
|
|
881
|
+
and save it as a new FITS file with Stokes axis preserved.
|
|
882
|
+
|
|
883
|
+
Parameters
|
|
884
|
+
----------
|
|
885
|
+
input_fits_file : str
|
|
886
|
+
Path to the input FITS file/Casa Image.
|
|
887
|
+
output_fits_file : str
|
|
888
|
+
Path to save the output FITS file.
|
|
889
|
+
thres : float, optional
|
|
890
|
+
Threshold value. Default is 10.
|
|
891
|
+
lat : str, optional
|
|
892
|
+
Observer latitude.
|
|
893
|
+
long : str, optional
|
|
894
|
+
Observer longitude.
|
|
895
|
+
height : float, optional
|
|
896
|
+
Observer height in meters.
|
|
897
|
+
observatory : str, optional
|
|
898
|
+
Observatory name.
|
|
899
|
+
overwrite : bool, optional
|
|
900
|
+
Whether to overwrite the output file if it exists. Default is True.
|
|
901
|
+
temp_suffix : str, optional
|
|
902
|
+
Suffix to add to temporary files to avoid conflicts. Default is "".
|
|
903
|
+
|
|
904
|
+
Returns
|
|
905
|
+
-------
|
|
906
|
+
bool
|
|
907
|
+
True if the file was saved successfully, False otherwise.
|
|
908
|
+
"""
|
|
909
|
+
if not sunpy_imported:
|
|
910
|
+
print("[ERROR] Cannot convert to helioprojective coordinates without sunpy")
|
|
911
|
+
return False
|
|
912
|
+
|
|
913
|
+
try:
|
|
914
|
+
not_fits_flag = False
|
|
915
|
+
original_input = input_fits_file
|
|
916
|
+
|
|
917
|
+
if not input_fits_file.endswith(".fits") and not input_fits_file.endswith(".fts"):
|
|
918
|
+
# Create unique temp filename with provided suffix
|
|
919
|
+
temp_filename = f"temp{temp_suffix}.fits"
|
|
920
|
+
input_fits_file = convert_casaimage_to_fits(
|
|
921
|
+
imagename=input_fits_file, fitsname=temp_filename
|
|
922
|
+
)
|
|
923
|
+
#print(f"Converted casaimage {original_input} to a temporary fits file ...")
|
|
924
|
+
not_fits_flag = True
|
|
925
|
+
|
|
926
|
+
# Check how many Stokes parameters are available
|
|
927
|
+
hdu = fits.open(input_fits_file)
|
|
928
|
+
header = hdu[0].header
|
|
929
|
+
ndim = header["NAXIS"]
|
|
930
|
+
|
|
931
|
+
# Get BUNIT early before temp file is deleted
|
|
932
|
+
original_bunit = header.get('BUNIT', None)
|
|
933
|
+
if original_bunit:
|
|
934
|
+
#print(f"[DEBUG] Found BUNIT in original: {original_bunit}")
|
|
935
|
+
pass
|
|
936
|
+
|
|
937
|
+
# Find Stokes axis
|
|
938
|
+
stokes_axis_fits = None
|
|
939
|
+
num_stokes = 1
|
|
940
|
+
for i in range(1, ndim + 1):
|
|
941
|
+
if header.get(f"CTYPE{i}", "").upper() == "STOKES":
|
|
942
|
+
stokes_axis_fits = i
|
|
943
|
+
num_stokes = header[f"NAXIS{i}"]
|
|
944
|
+
break
|
|
945
|
+
hdu.close()
|
|
946
|
+
|
|
947
|
+
if stokes_axis_fits is None or num_stokes == 1:
|
|
948
|
+
# Single Stokes - just use regular conversion
|
|
949
|
+
#print("Single Stokes image, using standard conversion")
|
|
950
|
+
return convert_and_save_hpc(
|
|
951
|
+
input_fits_file=original_input,
|
|
952
|
+
output_fits_file=output_fits_file,
|
|
953
|
+
Stokes="I",
|
|
954
|
+
thres=thres,
|
|
955
|
+
lat=lat,
|
|
956
|
+
long=long,
|
|
957
|
+
height=height,
|
|
958
|
+
observatory=observatory,
|
|
959
|
+
overwrite=overwrite,
|
|
960
|
+
temp_suffix=temp_suffix,
|
|
961
|
+
)
|
|
962
|
+
|
|
963
|
+
# Convert each Stokes parameter
|
|
964
|
+
stokes_list = ["I", "Q", "U", "V"][:num_stokes]
|
|
965
|
+
stokes_maps = []
|
|
966
|
+
reference_header = None
|
|
967
|
+
|
|
968
|
+
for stokes in stokes_list:
|
|
969
|
+
#print(f"Converting Stokes {stokes} to helioprojective...")
|
|
970
|
+
try:
|
|
971
|
+
map_obj, csys, psf = convert_to_hpc(
|
|
972
|
+
input_fits_file,
|
|
973
|
+
Stokes=stokes,
|
|
974
|
+
thres=thres,
|
|
975
|
+
lat=lat,
|
|
976
|
+
long=long,
|
|
977
|
+
height=height,
|
|
978
|
+
observatory=observatory,
|
|
979
|
+
)
|
|
980
|
+
if map_obj is not None:
|
|
981
|
+
stokes_maps.append(map_obj.data)
|
|
982
|
+
if reference_header is None:
|
|
983
|
+
reference_header = dict(map_obj.meta)
|
|
984
|
+
else:
|
|
985
|
+
print(f"[ERROR] Failed to convert Stokes {stokes}, using zeros")
|
|
986
|
+
if stokes_maps:
|
|
987
|
+
stokes_maps.append(np.zeros_like(stokes_maps[0]))
|
|
988
|
+
else:
|
|
989
|
+
raise RuntimeError(f"Failed to convert first Stokes {stokes}")
|
|
990
|
+
except Exception as e:
|
|
991
|
+
print(f"[ERROR] Error converting Stokes {stokes}: {e}")
|
|
992
|
+
if stokes_maps:
|
|
993
|
+
stokes_maps.append(np.zeros_like(stokes_maps[0]))
|
|
994
|
+
else:
|
|
995
|
+
raise
|
|
996
|
+
|
|
997
|
+
if not_fits_flag:
|
|
998
|
+
# Remove the temp file
|
|
999
|
+
temp_filename = f"temp{temp_suffix}.fits"
|
|
1000
|
+
os.system(f"rm {temp_filename}")
|
|
1001
|
+
|
|
1002
|
+
if not stokes_maps:
|
|
1003
|
+
print("[ERROR] No Stokes maps were successfully converted")
|
|
1004
|
+
return False
|
|
1005
|
+
|
|
1006
|
+
# Ensure all Stokes maps have the same shape (reference is first map - Stokes I)
|
|
1007
|
+
reference_shape = stokes_maps[0].shape
|
|
1008
|
+
aligned_maps = [stokes_maps[0]]
|
|
1009
|
+
|
|
1010
|
+
for i, smap in enumerate(stokes_maps[1:], 1):
|
|
1011
|
+
if smap.shape != reference_shape:
|
|
1012
|
+
# print(f"[DEBUG] Stokes {stokes_list[i]} shape {smap.shape} differs from reference {reference_shape}, resampling...")
|
|
1013
|
+
# Resize to match reference shape
|
|
1014
|
+
from scipy.ndimage import zoom
|
|
1015
|
+
zoom_factors = (reference_shape[0] / smap.shape[0], reference_shape[1] / smap.shape[1])
|
|
1016
|
+
smap = zoom(smap, zoom_factors, order=1)
|
|
1017
|
+
# Ensure exact shape match after zoom
|
|
1018
|
+
smap = smap[:reference_shape[0], :reference_shape[1]]
|
|
1019
|
+
aligned_maps.append(smap)
|
|
1020
|
+
|
|
1021
|
+
# Stack all Stokes into a single 3D array
|
|
1022
|
+
stokes_data = np.stack(aligned_maps, axis=0)
|
|
1023
|
+
#print(f"Combined Stokes data shape: {stokes_data.shape}")
|
|
1024
|
+
|
|
1025
|
+
# Create new header with Stokes axis
|
|
1026
|
+
from astropy.io import fits as afits
|
|
1027
|
+
|
|
1028
|
+
# BUNIT was already read earlier (before temp file deletion)
|
|
1029
|
+
|
|
1030
|
+
new_header = afits.Header()
|
|
1031
|
+
for key, value in reference_header.items():
|
|
1032
|
+
if key not in ['COMMENT', 'HISTORY', '']:
|
|
1033
|
+
try:
|
|
1034
|
+
new_header[key] = value
|
|
1035
|
+
except Exception:
|
|
1036
|
+
pass
|
|
1037
|
+
|
|
1038
|
+
# Update for 3D with Stokes
|
|
1039
|
+
new_header['NAXIS'] = 3
|
|
1040
|
+
new_header['NAXIS3'] = len(stokes_maps)
|
|
1041
|
+
new_header['CTYPE3'] = 'STOKES'
|
|
1042
|
+
new_header['CRVAL3'] = 1.0
|
|
1043
|
+
new_header['CDELT3'] = 1.0
|
|
1044
|
+
new_header['CRPIX3'] = 1.0
|
|
1045
|
+
new_header['CUNIT3'] = ''
|
|
1046
|
+
|
|
1047
|
+
# Preserve BUNIT from original file
|
|
1048
|
+
if original_bunit:
|
|
1049
|
+
new_header['BUNIT'] = original_bunit
|
|
1050
|
+
#print(f"[HPC] Added BUNIT={original_bunit} to multi-Stokes HPC file")
|
|
1051
|
+
|
|
1052
|
+
# Remove single-Stokes marker if present
|
|
1053
|
+
if 'STOKES' in new_header:
|
|
1054
|
+
del new_header['STOKES']
|
|
1055
|
+
|
|
1056
|
+
# Write the combined FITS file
|
|
1057
|
+
hdu_out = afits.PrimaryHDU(stokes_data, header=new_header)
|
|
1058
|
+
hdu_out.writeto(output_fits_file, overwrite=overwrite)
|
|
1059
|
+
#print(f"Helioprojective map with {len(stokes_maps)} Stokes saved to {output_fits_file}")
|
|
1060
|
+
|
|
1061
|
+
return True
|
|
1062
|
+
|
|
1063
|
+
except Exception as e:
|
|
1064
|
+
print(f"[ERROR] Error converting and saving all Stokes to helioprojective: {str(e)}")
|
|
1065
|
+
import traceback
|
|
1066
|
+
traceback.print_exc()
|
|
1067
|
+
return False
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
def convert_hpc_to_radec(
|
|
1071
|
+
input_fits_file,
|
|
1072
|
+
output_fits_file,
|
|
1073
|
+
overwrite=True,
|
|
1074
|
+
):
|
|
1075
|
+
"""
|
|
1076
|
+
Convert a helioprojective (Solar-X/Y) FITS file back to RA/Dec coordinates.
|
|
1077
|
+
|
|
1078
|
+
This actually rotates the data by the P-angle to undo the solar rotation.
|
|
1079
|
+
|
|
1080
|
+
Parameters
|
|
1081
|
+
----------
|
|
1082
|
+
input_fits_file : str
|
|
1083
|
+
Path to the input helioprojective FITS file.
|
|
1084
|
+
output_fits_file : str
|
|
1085
|
+
Path to save the output RA/Dec FITS file.
|
|
1086
|
+
overwrite : bool, optional
|
|
1087
|
+
Whether to overwrite the output file if it exists. Default is True.
|
|
1088
|
+
|
|
1089
|
+
Returns
|
|
1090
|
+
-------
|
|
1091
|
+
bool
|
|
1092
|
+
True if the file was saved successfully, False otherwise.
|
|
1093
|
+
"""
|
|
1094
|
+
if not sunpy_imported:
|
|
1095
|
+
print("[ERROR] Cannot convert from helioprojective coordinates without sunpy")
|
|
1096
|
+
return False
|
|
1097
|
+
|
|
1098
|
+
try:
|
|
1099
|
+
from astropy.io import fits as afits
|
|
1100
|
+
from astropy.wcs import WCS
|
|
1101
|
+
from scipy.ndimage import rotate
|
|
1102
|
+
|
|
1103
|
+
#print(f"[HPC->RA/Dec] Converting {input_fits_file}")
|
|
1104
|
+
|
|
1105
|
+
# Load the HPC FITS file
|
|
1106
|
+
with afits.open(input_fits_file) as hdul:
|
|
1107
|
+
hpc_data = hdul[0].data.copy()
|
|
1108
|
+
hpc_header = hdul[0].header.copy()
|
|
1109
|
+
|
|
1110
|
+
# Check if it's actually HPC
|
|
1111
|
+
ctype1 = hpc_header.get('CTYPE1', '').upper()
|
|
1112
|
+
ctype2 = hpc_header.get('CTYPE2', '').upper()
|
|
1113
|
+
if 'HPLN' not in ctype1 and 'HPLT' not in ctype2 and 'SOLAR' not in ctype1:
|
|
1114
|
+
print(f"[ERROR] File does not appear to be in HPC coordinates (CTYPE1={ctype1}, CTYPE2={ctype2})")
|
|
1115
|
+
return False
|
|
1116
|
+
|
|
1117
|
+
# Get observation time
|
|
1118
|
+
obstime_str = hpc_header.get('DATE-OBS', None)
|
|
1119
|
+
if obstime_str is None:
|
|
1120
|
+
print("[ERROR] DATE-OBS not found in header")
|
|
1121
|
+
return False
|
|
1122
|
+
obstime = Time(obstime_str)
|
|
1123
|
+
|
|
1124
|
+
# Get P-angle - this is the rotation we need to undo
|
|
1125
|
+
p_angle = P(obstime).to(u.deg).value
|
|
1126
|
+
#print(f"[HPC->RA/Dec] P-angle: {p_angle:.2f} deg - will rotate by {-p_angle:.2f} deg")
|
|
1127
|
+
|
|
1128
|
+
# Get Sun center in RA/Dec at observation time
|
|
1129
|
+
sun_coord = SkyCoord(0*u.arcsec, 0*u.arcsec,
|
|
1130
|
+
frame=Helioprojective(obstime=obstime, observer='earth'))
|
|
1131
|
+
sun_radec = sun_coord.transform_to('gcrs')
|
|
1132
|
+
sun_ra = sun_radec.ra.deg
|
|
1133
|
+
sun_dec = sun_radec.dec.deg
|
|
1134
|
+
#print(f"[HPC->RA/Dec] Sun center at {obstime.iso}: RA={sun_ra:.4f}, Dec={sun_dec:.4f}")
|
|
1135
|
+
|
|
1136
|
+
# Get HPC coordinate parameters
|
|
1137
|
+
crpix1 = hpc_header.get('CRPIX1', hpc_data.shape[-1]/2)
|
|
1138
|
+
crpix2 = hpc_header.get('CRPIX2', hpc_data.shape[-2]/2)
|
|
1139
|
+
cdelt1 = hpc_header.get('CDELT1', 1.0)
|
|
1140
|
+
cdelt2 = hpc_header.get('CDELT2', 1.0)
|
|
1141
|
+
|
|
1142
|
+
# Rotate the data by -P_angle to undo the solar rotation
|
|
1143
|
+
# scipy.ndimage.rotate uses counter-clockwise positive angles
|
|
1144
|
+
# so we negate to rotate clockwise (undoing the original rotation)
|
|
1145
|
+
# Using reshape=False to maintain original image dimensions
|
|
1146
|
+
if hpc_data.ndim == 2:
|
|
1147
|
+
rotated_data = rotate(hpc_data, -p_angle, reshape=False, order=1, mode='constant', cval=np.nan)
|
|
1148
|
+
elif hpc_data.ndim == 3:
|
|
1149
|
+
# Rotate each Stokes plane separately
|
|
1150
|
+
rotated_data = np.zeros_like(hpc_data)
|
|
1151
|
+
for i in range(hpc_data.shape[0]):
|
|
1152
|
+
rotated_data[i] = rotate(hpc_data[i], -p_angle, reshape=False, order=1, mode='constant', cval=np.nan)
|
|
1153
|
+
else:
|
|
1154
|
+
print(f"[ERROR] Unexpected data dimensions: {hpc_data.ndim}")
|
|
1155
|
+
return False
|
|
1156
|
+
|
|
1157
|
+
# Crop to non-NaN bounding box to remove NaN borders
|
|
1158
|
+
if rotated_data.ndim == 2:
|
|
1159
|
+
valid_mask = ~np.isnan(rotated_data)
|
|
1160
|
+
else: # 3D
|
|
1161
|
+
# Use union of all planes for the mask
|
|
1162
|
+
valid_mask = ~np.isnan(rotated_data[0])
|
|
1163
|
+
for i in range(1, rotated_data.shape[0]):
|
|
1164
|
+
valid_mask |= ~np.isnan(rotated_data[i])
|
|
1165
|
+
|
|
1166
|
+
# Find bounding box of valid data
|
|
1167
|
+
rows = np.any(valid_mask, axis=1)
|
|
1168
|
+
cols = np.any(valid_mask, axis=0)
|
|
1169
|
+
if not np.any(rows) or not np.any(cols):
|
|
1170
|
+
print("[ERROR] No valid data after rotation")
|
|
1171
|
+
return False
|
|
1172
|
+
|
|
1173
|
+
rmin, rmax = np.where(rows)[0][[0, -1]]
|
|
1174
|
+
cmin, cmax = np.where(cols)[0][[0, -1]]
|
|
1175
|
+
|
|
1176
|
+
# Add small padding to avoid over-cropping due to interpolation edge effects
|
|
1177
|
+
padding = 0 # pixels
|
|
1178
|
+
if rotated_data.ndim == 2:
|
|
1179
|
+
rmin = max(0, rmin - padding)
|
|
1180
|
+
rmax = min(rotated_data.shape[0] - 1, rmax + padding)
|
|
1181
|
+
cmin = max(0, cmin - padding)
|
|
1182
|
+
cmax = min(rotated_data.shape[1] - 1, cmax + padding)
|
|
1183
|
+
else: # 3D
|
|
1184
|
+
rmin = max(0, rmin - padding)
|
|
1185
|
+
rmax = min(rotated_data.shape[1] - 1, rmax + padding)
|
|
1186
|
+
cmin = max(0, cmin - padding)
|
|
1187
|
+
cmax = min(rotated_data.shape[2] - 1, cmax + padding)
|
|
1188
|
+
|
|
1189
|
+
# Crop the data
|
|
1190
|
+
if rotated_data.ndim == 2:
|
|
1191
|
+
rotated_data = rotated_data[rmin:rmax+1, cmin:cmax+1]
|
|
1192
|
+
else:
|
|
1193
|
+
rotated_data = rotated_data[:, rmin:rmax+1, cmin:cmax+1]
|
|
1194
|
+
|
|
1195
|
+
# Adjust CRPIX for the crop
|
|
1196
|
+
crpix1 = crpix1 - cmin
|
|
1197
|
+
crpix2 = crpix2 - rmin
|
|
1198
|
+
|
|
1199
|
+
#print(f"[HPC->RA/Dec] Cropped to {rotated_data.shape[-1]}x{rotated_data.shape[-2]} (removed NaN borders)")
|
|
1200
|
+
|
|
1201
|
+
# Convert cdelt from arcsec to degrees if needed
|
|
1202
|
+
cunit1 = hpc_header.get('CUNIT1', 'arcsec').lower()
|
|
1203
|
+
cunit2 = hpc_header.get('CUNIT2', 'arcsec').lower()
|
|
1204
|
+
if 'arcsec' in cunit1:
|
|
1205
|
+
cdelt1_deg = cdelt1 / 3600.0
|
|
1206
|
+
elif 'deg' in cunit1:
|
|
1207
|
+
cdelt1_deg = cdelt1
|
|
1208
|
+
else:
|
|
1209
|
+
cdelt1_deg = cdelt1 / 3600.0
|
|
1210
|
+
|
|
1211
|
+
if 'arcsec' in cunit2:
|
|
1212
|
+
cdelt2_deg = cdelt2 / 3600.0
|
|
1213
|
+
elif 'deg' in cunit2:
|
|
1214
|
+
cdelt2_deg = cdelt2
|
|
1215
|
+
else:
|
|
1216
|
+
cdelt2_deg = cdelt2 / 3600.0
|
|
1217
|
+
|
|
1218
|
+
# Create new RA/Dec header
|
|
1219
|
+
new_header = afits.Header()
|
|
1220
|
+
|
|
1221
|
+
# Copy essential keywords
|
|
1222
|
+
for key in ['SIMPLE', 'BITPIX', 'NAXIS', 'NAXIS1', 'NAXIS2',
|
|
1223
|
+
'BMAJ', 'BMIN', 'BPA', 'BUNIT', 'TELESCOP', 'INSTRUME',
|
|
1224
|
+
'DATE-OBS', 'FREQ', 'FREQUNIT', 'OBJECT', 'ORIGIN',
|
|
1225
|
+
'CRVAL3', 'CRVAL4', 'RESTFRQ']:
|
|
1226
|
+
if key in hpc_header:
|
|
1227
|
+
new_header[key] = hpc_header[key]
|
|
1228
|
+
|
|
1229
|
+
# Handle Stokes axis
|
|
1230
|
+
if hpc_data.ndim == 3:
|
|
1231
|
+
# Save frequency before CRVAL3 is overwritten for Stokes
|
|
1232
|
+
orig_freq = hpc_header.get('CRVAL3') or hpc_header.get('RESTFRQ') or hpc_header.get('FREQ')
|
|
1233
|
+
|
|
1234
|
+
new_header['NAXIS'] = 3
|
|
1235
|
+
new_header['NAXIS3'] = hpc_data.shape[0]
|
|
1236
|
+
new_header['CTYPE3'] = 'STOKES'
|
|
1237
|
+
new_header['CRVAL3'] = 1.0
|
|
1238
|
+
new_header['CDELT3'] = 1.0
|
|
1239
|
+
new_header['CRPIX3'] = 1.0
|
|
1240
|
+
|
|
1241
|
+
# Preserve frequency in RESTFRQ if it was in CRVAL3
|
|
1242
|
+
if orig_freq and 'RESTFRQ' not in new_header:
|
|
1243
|
+
new_header['RESTFRQ'] = float(orig_freq)
|
|
1244
|
+
|
|
1245
|
+
# Set RA/Dec coordinate system (SIN projection for radio)
|
|
1246
|
+
new_header['CTYPE1'] = 'RA---SIN'
|
|
1247
|
+
new_header['CTYPE2'] = 'DEC--SIN'
|
|
1248
|
+
new_header['CUNIT1'] = 'deg'
|
|
1249
|
+
new_header['CUNIT2'] = 'deg'
|
|
1250
|
+
new_header['CRPIX1'] = crpix1
|
|
1251
|
+
new_header['CRPIX2'] = crpix2
|
|
1252
|
+
new_header['CDELT1'] = -abs(cdelt1_deg) # RA increases to the left
|
|
1253
|
+
new_header['CDELT2'] = abs(cdelt2_deg)
|
|
1254
|
+
new_header['CRVAL1'] = sun_ra
|
|
1255
|
+
new_header['CRVAL2'] = sun_dec
|
|
1256
|
+
|
|
1257
|
+
# No PC matrix needed - rotation already applied to data
|
|
1258
|
+
new_header['PC1_1'] = 1.0
|
|
1259
|
+
new_header['PC1_2'] = 0.0
|
|
1260
|
+
new_header['PC2_1'] = 0.0
|
|
1261
|
+
new_header['PC2_2'] = 1.0
|
|
1262
|
+
|
|
1263
|
+
new_header['RADESYS'] = 'ICRS'
|
|
1264
|
+
new_header['EQUINOX'] = 2000.0
|
|
1265
|
+
|
|
1266
|
+
new_header['HISTORY'] = f'Converted from Helioprojective to RA/Dec by SolarViewer (rotated {-p_angle:.2f} deg)'
|
|
1267
|
+
|
|
1268
|
+
# Write output file
|
|
1269
|
+
hdu = afits.PrimaryHDU(data=rotated_data, header=new_header)
|
|
1270
|
+
hdu.writeto(output_fits_file, overwrite=overwrite)
|
|
1271
|
+
|
|
1272
|
+
#print(f"[HPC->RA/Dec] Saved rotated RA/Dec file to: {output_fits_file}")
|
|
1273
|
+
return True
|
|
1274
|
+
|
|
1275
|
+
except Exception as e:
|
|
1276
|
+
print(f"[ERROR] Error converting HPC to RA/Dec: {str(e)}")
|
|
1277
|
+
import traceback
|
|
1278
|
+
traceback.print_exc()
|
|
1279
|
+
return False
|
|
1280
|
+
|
|
1281
|
+
|
|
1282
|
+
def itrf_to_geodetic(x, y, z, scale_factor=1.0):
|
|
1283
|
+
"""
|
|
1284
|
+
Convert ITRF (International Terrestrial Reference Frame) coordinates to geodetic coordinates.
|
|
1285
|
+
|
|
1286
|
+
ITRF coordinates are Earth-centered, Earth-fixed Cartesian coordinates.
|
|
1287
|
+
Typical ITRF coordinates are in the range of millions of meters from the Earth's center.
|
|
1288
|
+
For example, a point on the Earth's surface might have ITRF coordinates like:
|
|
1289
|
+
X = 4075539.5, Y = 931735.8, Z = 4801629.6 (in meters)
|
|
1290
|
+
|
|
1291
|
+
Parameters
|
|
1292
|
+
----------
|
|
1293
|
+
x : float
|
|
1294
|
+
X coordinate in meters.
|
|
1295
|
+
y : float
|
|
1296
|
+
Y coordinate in meters.
|
|
1297
|
+
z : float
|
|
1298
|
+
Z coordinate in meters.
|
|
1299
|
+
scale_factor : float, optional
|
|
1300
|
+
Scale factor to apply to the coordinates. Default is 1.0.
|
|
1301
|
+
This is useful if the coordinates are provided in different units.
|
|
1302
|
+
|
|
1303
|
+
Returns
|
|
1304
|
+
-------
|
|
1305
|
+
lat : str
|
|
1306
|
+
Latitude in the format "DD:MM:SS.SS".
|
|
1307
|
+
long : str
|
|
1308
|
+
Longitude in the format "DD:MM:SS.SS".
|
|
1309
|
+
height : float
|
|
1310
|
+
Height in meters above the WGS84 ellipsoid.
|
|
1311
|
+
|
|
1312
|
+
Examples
|
|
1313
|
+
--------
|
|
1314
|
+
>>> lat, long, height = itrf_to_geodetic(4075539.5, 931735.8, 4801629.6)
|
|
1315
|
+
>>> print(lat, long, height)
|
|
1316
|
+
'49:08:42.04' '12:52:38.85' 669.13
|
|
1317
|
+
"""
|
|
1318
|
+
try:
|
|
1319
|
+
# Apply scale factor if provided
|
|
1320
|
+
x = x * scale_factor
|
|
1321
|
+
y = y * scale_factor
|
|
1322
|
+
z = z * scale_factor
|
|
1323
|
+
|
|
1324
|
+
# Use astropy's EarthLocation for the conversion if available
|
|
1325
|
+
try:
|
|
1326
|
+
# Create EarthLocation object from ITRF coordinates
|
|
1327
|
+
location = EarthLocation.from_geocentric(x, y, z, unit=u.m)
|
|
1328
|
+
|
|
1329
|
+
# Get latitude and longitude in degrees
|
|
1330
|
+
lat_deg = location.lat.deg
|
|
1331
|
+
lon_deg = location.lon.deg
|
|
1332
|
+
height = location.height.value # in meters
|
|
1333
|
+
|
|
1334
|
+
except Exception as e:
|
|
1335
|
+
print(f"[ERROR] Error {str(e)}. Falling back to manual calculation.")
|
|
1336
|
+
# Fall back to manual calculation if astropy is not available
|
|
1337
|
+
# WGS84 ellipsoid parameters
|
|
1338
|
+
a = 6378137.0 # semi-major axis in meters
|
|
1339
|
+
f = 1 / 298.257223563 # flattening
|
|
1340
|
+
b = a * (1 - f) # semi-minor axis
|
|
1341
|
+
e_sq = 1 - (b / a) ** 2 # eccentricity squared
|
|
1342
|
+
|
|
1343
|
+
# Calculate longitude (easy)
|
|
1344
|
+
longitude = np.arctan2(y, x)
|
|
1345
|
+
|
|
1346
|
+
# Iterative calculation of latitude and height
|
|
1347
|
+
p = np.sqrt(x**2 + y**2)
|
|
1348
|
+
|
|
1349
|
+
# Initial guess
|
|
1350
|
+
latitude = np.arctan2(z, p * (1 - e_sq))
|
|
1351
|
+
|
|
1352
|
+
# Iterative improvement
|
|
1353
|
+
for _ in range(5): # Usually converges in a few iterations
|
|
1354
|
+
N = a / np.sqrt(1 - e_sq * np.sin(latitude) ** 2)
|
|
1355
|
+
height = p / np.cos(latitude) - N
|
|
1356
|
+
latitude = np.arctan2(z, p * (1 - e_sq * N / (N + height)))
|
|
1357
|
+
|
|
1358
|
+
# Convert to degrees
|
|
1359
|
+
lat_deg = np.degrees(latitude)
|
|
1360
|
+
lon_deg = np.degrees(longitude)
|
|
1361
|
+
|
|
1362
|
+
# Convert decimal degrees to DD:MM:SS.SS format
|
|
1363
|
+
def decimal_to_dms(decimal_degrees):
|
|
1364
|
+
is_negative = decimal_degrees < 0
|
|
1365
|
+
decimal_degrees = abs(decimal_degrees)
|
|
1366
|
+
|
|
1367
|
+
degrees = int(decimal_degrees)
|
|
1368
|
+
decimal_minutes = (decimal_degrees - degrees) * 60
|
|
1369
|
+
minutes = int(decimal_minutes)
|
|
1370
|
+
seconds = (decimal_minutes - minutes) * 60
|
|
1371
|
+
|
|
1372
|
+
# Format as DD:MM:SS.SS
|
|
1373
|
+
dms = f"{degrees:02d}:{minutes:02d}:{seconds:05.2f}"
|
|
1374
|
+
|
|
1375
|
+
# Add negative sign if needed
|
|
1376
|
+
if is_negative:
|
|
1377
|
+
dms = f"-{dms}"
|
|
1378
|
+
|
|
1379
|
+
return dms
|
|
1380
|
+
|
|
1381
|
+
lat_str = decimal_to_dms(lat_deg)
|
|
1382
|
+
lon_str = decimal_to_dms(lon_deg)
|
|
1383
|
+
|
|
1384
|
+
return lat_str, lon_str, height
|
|
1385
|
+
|
|
1386
|
+
except Exception as e:
|
|
1387
|
+
print(f"[ERROR] Error converting ITRF to geodetic: {str(e)}")
|
|
1388
|
+
import traceback
|
|
1389
|
+
|
|
1390
|
+
traceback.print_exc()
|
|
1391
|
+
return None, None, None
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def geodetic_to_itrf(lat, lon, height):
|
|
1395
|
+
"""
|
|
1396
|
+
Convert geodetic coordinates to ITRF (International Terrestrial Reference Frame) coordinates.
|
|
1397
|
+
|
|
1398
|
+
Parameters
|
|
1399
|
+
----------
|
|
1400
|
+
lat : str or float
|
|
1401
|
+
Latitude in the format "DD:MM:SS.SS" or decimal degrees.
|
|
1402
|
+
lon : str or float
|
|
1403
|
+
Longitude in the format "DD:MM:SS.SS" or decimal degrees.
|
|
1404
|
+
height : float
|
|
1405
|
+
Height in meters above the WGS84 ellipsoid.
|
|
1406
|
+
|
|
1407
|
+
Returns
|
|
1408
|
+
-------
|
|
1409
|
+
x : float
|
|
1410
|
+
X coordinate in meters.
|
|
1411
|
+
y : float
|
|
1412
|
+
Y coordinate in meters.
|
|
1413
|
+
z : float
|
|
1414
|
+
Z coordinate in meters.
|
|
1415
|
+
|
|
1416
|
+
Examples
|
|
1417
|
+
--------
|
|
1418
|
+
>>> x, y, z = geodetic_to_itrf("49:08:42.04", "12:52:38.85", 669.13)
|
|
1419
|
+
>>> print(f"{x:.1f}, {y:.1f}, {z:.1f}")
|
|
1420
|
+
'4075539.5, 931735.8, 4801629.6'
|
|
1421
|
+
"""
|
|
1422
|
+
try:
|
|
1423
|
+
# Convert lat/lon from string to decimal degrees if needed
|
|
1424
|
+
if isinstance(lat, str):
|
|
1425
|
+
lat = dms_to_decimal(lat)
|
|
1426
|
+
if isinstance(lon, str):
|
|
1427
|
+
lon = dms_to_decimal(lon)
|
|
1428
|
+
|
|
1429
|
+
# Use astropy's EarthLocation for the conversion if available
|
|
1430
|
+
try:
|
|
1431
|
+
# Create EarthLocation object from geodetic coordinates
|
|
1432
|
+
location = EarthLocation.from_geodetic(lon, lat, height, unit=u.m)
|
|
1433
|
+
|
|
1434
|
+
# Get ITRF coordinates
|
|
1435
|
+
x = location.x.value
|
|
1436
|
+
y = location.y.value
|
|
1437
|
+
z = location.z.value
|
|
1438
|
+
|
|
1439
|
+
except Exception as e:
|
|
1440
|
+
print(f"[ERROR] Error {str(e)}. Falling back to manual calculation.")
|
|
1441
|
+
# Convert to radians
|
|
1442
|
+
lat_rad = np.radians(lat)
|
|
1443
|
+
lon_rad = np.radians(lon)
|
|
1444
|
+
|
|
1445
|
+
# WGS84 ellipsoid parameters
|
|
1446
|
+
a = 6378137.0 # semi-major axis in meters
|
|
1447
|
+
f = 1 / 298.257223563 # flattening
|
|
1448
|
+
e_sq = 2 * f - f**2 # eccentricity squared
|
|
1449
|
+
|
|
1450
|
+
# Calculate N (radius of curvature in the prime vertical)
|
|
1451
|
+
N = a / np.sqrt(1 - e_sq * np.sin(lat_rad) ** 2)
|
|
1452
|
+
|
|
1453
|
+
# Calculate ECEF coordinates
|
|
1454
|
+
x = (N + height) * np.cos(lat_rad) * np.cos(lon_rad)
|
|
1455
|
+
y = (N + height) * np.cos(lat_rad) * np.sin(lon_rad)
|
|
1456
|
+
z = (N * (1 - e_sq) + height) * np.sin(lat_rad)
|
|
1457
|
+
|
|
1458
|
+
return x, y, z
|
|
1459
|
+
|
|
1460
|
+
except Exception as e:
|
|
1461
|
+
print(f"[ERROR] Error converting geodetic to ITRF: {str(e)}")
|
|
1462
|
+
import traceback
|
|
1463
|
+
|
|
1464
|
+
traceback.print_exc()
|
|
1465
|
+
return None, None, None
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def dms_to_decimal(dms_str):
|
|
1469
|
+
"""
|
|
1470
|
+
Convert a coordinate string in DD:MM:SS.SS format to decimal degrees.
|
|
1471
|
+
|
|
1472
|
+
Parameters
|
|
1473
|
+
----------
|
|
1474
|
+
dms_str : str
|
|
1475
|
+
Coordinate string in the format "DD:MM:SS.SS" or "-DD:MM:SS.SS".
|
|
1476
|
+
|
|
1477
|
+
Returns
|
|
1478
|
+
-------
|
|
1479
|
+
float
|
|
1480
|
+
Coordinate in decimal degrees.
|
|
1481
|
+
"""
|
|
1482
|
+
try:
|
|
1483
|
+
# Check if the string is negative
|
|
1484
|
+
is_negative = dms_str.startswith("-")
|
|
1485
|
+
if is_negative:
|
|
1486
|
+
dms_str = dms_str[1:] # Remove the negative sign
|
|
1487
|
+
|
|
1488
|
+
# Split the string into degrees, minutes, and seconds
|
|
1489
|
+
parts = dms_str.split(":")
|
|
1490
|
+
if len(parts) != 3:
|
|
1491
|
+
raise ValueError(
|
|
1492
|
+
f"Invalid DMS format: {dms_str}. Expected format: DD:MM:SS.SS"
|
|
1493
|
+
)
|
|
1494
|
+
|
|
1495
|
+
degrees = float(parts[0])
|
|
1496
|
+
minutes = float(parts[1])
|
|
1497
|
+
seconds = float(parts[2])
|
|
1498
|
+
|
|
1499
|
+
# Convert to decimal degrees
|
|
1500
|
+
decimal = degrees + minutes / 60 + seconds / 3600
|
|
1501
|
+
|
|
1502
|
+
# Apply negative sign if needed
|
|
1503
|
+
if is_negative:
|
|
1504
|
+
decimal = -decimal
|
|
1505
|
+
|
|
1506
|
+
return decimal
|
|
1507
|
+
|
|
1508
|
+
except Exception as e:
|
|
1509
|
+
print(f"[ERROR] Error converting DMS to decimal: {str(e)}")
|
|
1510
|
+
raise
|
|
1511
|
+
|
|
1512
|
+
|
|
1513
|
+
def extract_telescope_position(metadata_dict):
|
|
1514
|
+
"""
|
|
1515
|
+
Extract telescope position from metadata dictionary and convert to latitude, longitude, and height.
|
|
1516
|
+
|
|
1517
|
+
Parameters
|
|
1518
|
+
----------
|
|
1519
|
+
metadata_dict : dict
|
|
1520
|
+
Metadata dictionary containing telescope position information.
|
|
1521
|
+
|
|
1522
|
+
Returns
|
|
1523
|
+
-------
|
|
1524
|
+
lat : str
|
|
1525
|
+
Latitude in the format "DD:MM:SS.SS".
|
|
1526
|
+
long : str
|
|
1527
|
+
Longitude in the format "DD:MM:SS.SS".
|
|
1528
|
+
height : float
|
|
1529
|
+
Height in meters above the WGS84 ellipsoid.
|
|
1530
|
+
observatory : str
|
|
1531
|
+
Observatory name if available, otherwise None.
|
|
1532
|
+
|
|
1533
|
+
Examples
|
|
1534
|
+
--------
|
|
1535
|
+
>>> metadata = {'messages': [
|
|
1536
|
+
... 'Telescope position: [-2.55945e+06m, 5.09537e+06m, -2.84906e+06m] (ITRF)',
|
|
1537
|
+
... 'Telescope: MWA'
|
|
1538
|
+
... ]}
|
|
1539
|
+
>>> lat, lon, height, observatory = extract_telescope_position(metadata)
|
|
1540
|
+
>>> print(lat, lon, height, observatory)
|
|
1541
|
+
'-26:42:11.95' '116:40:14.93' 377.8 'MWA'
|
|
1542
|
+
"""
|
|
1543
|
+
try:
|
|
1544
|
+
# Extract telescope position from messages
|
|
1545
|
+
telescope_position = None
|
|
1546
|
+
observatory = None
|
|
1547
|
+
|
|
1548
|
+
if "messages" in metadata_dict:
|
|
1549
|
+
for message in metadata_dict["messages"]:
|
|
1550
|
+
# Look for telescope position in the message
|
|
1551
|
+
if "Telescope position:" in message:
|
|
1552
|
+
# Extract the position string
|
|
1553
|
+
pos_start = message.find("Telescope position:") + len(
|
|
1554
|
+
"Telescope position:"
|
|
1555
|
+
)
|
|
1556
|
+
pos_end = (
|
|
1557
|
+
message.find("(ITRF)") if "(ITRF)" in message else len(message)
|
|
1558
|
+
)
|
|
1559
|
+
telescope_position = message[pos_start:pos_end].strip()
|
|
1560
|
+
|
|
1561
|
+
# Look for telescope/observatory name
|
|
1562
|
+
for line in message.split("\n"):
|
|
1563
|
+
if "Telescope" in line and ":" in line and "position" not in line:
|
|
1564
|
+
parts = line.split(":")
|
|
1565
|
+
if len(parts) > 1:
|
|
1566
|
+
observatory = parts[1].strip()
|
|
1567
|
+
break
|
|
1568
|
+
|
|
1569
|
+
# If observatory is still None, check if it's directly in the metadata
|
|
1570
|
+
if observatory is None and "observatory" in metadata_dict:
|
|
1571
|
+
observatory = metadata_dict["observatory"]
|
|
1572
|
+
|
|
1573
|
+
if telescope_position is None:
|
|
1574
|
+
print("[ERROR] Telescope position not found in metadata")
|
|
1575
|
+
return None, None, None, observatory
|
|
1576
|
+
|
|
1577
|
+
# Extract the ITRF coordinates
|
|
1578
|
+
# Remove brackets and 'm' units, then split by comma
|
|
1579
|
+
telescope_position = telescope_position.replace("[", "").replace("]", "")
|
|
1580
|
+
coords = telescope_position.split(",")
|
|
1581
|
+
|
|
1582
|
+
if len(coords) != 3:
|
|
1583
|
+
print(f"[ERROR] Invalid telescope position format: {telescope_position}")
|
|
1584
|
+
return None, None, None, observatory
|
|
1585
|
+
|
|
1586
|
+
# Parse the coordinates
|
|
1587
|
+
x = float(coords[0].replace("m", "").strip())
|
|
1588
|
+
y = float(coords[1].replace("m", "").strip())
|
|
1589
|
+
z = float(coords[2].replace("m", "").strip())
|
|
1590
|
+
if abs(x) < 1e6 and abs(y) < 1e6 and abs(z) < 1e6:
|
|
1591
|
+
print(f"[WARNING] Invalid telescope position format: {telescope_position}")
|
|
1592
|
+
print(f"Checking if it is a known telescope ....")
|
|
1593
|
+
if observatory.upper() == "LOFAR":
|
|
1594
|
+
print(f"Observatory matched with LOFAR")
|
|
1595
|
+
lat = "52:55:53.90"
|
|
1596
|
+
lon = "06:51:56.95"
|
|
1597
|
+
height = 50.16
|
|
1598
|
+
elif observatory.upper() == "MWA":
|
|
1599
|
+
print(f"Observatory matched with MWA")
|
|
1600
|
+
lat = "-26:42:12.09"
|
|
1601
|
+
lon = "116:40:14.84"
|
|
1602
|
+
height = 375.75
|
|
1603
|
+
elif observatory.upper() == "MEERKAT":
|
|
1604
|
+
print(f"Observatory matched with MEERKAT")
|
|
1605
|
+
lat = "-30:42:47.36"
|
|
1606
|
+
lon = "21:26:38.09"
|
|
1607
|
+
height = 1050.82
|
|
1608
|
+
elif observatory.upper() == "GMRT" or observatory.upper() == "UGMRT":
|
|
1609
|
+
print(f"Observatory matched with uGMRT")
|
|
1610
|
+
lat = "19:05:26.21"
|
|
1611
|
+
lon = "74:02:59.90"
|
|
1612
|
+
height = 639.68
|
|
1613
|
+
else:
|
|
1614
|
+
print(f"Observatory not matched with database of known telescopes")
|
|
1615
|
+
return None, None, None, observatory
|
|
1616
|
+
else:
|
|
1617
|
+
# Convert ITRF coordinates to latitude, longitude, and height
|
|
1618
|
+
lat, lon, height = itrf_to_geodetic(x, y, z)
|
|
1619
|
+
|
|
1620
|
+
return lat, lon, height, observatory
|
|
1621
|
+
|
|
1622
|
+
except Exception as e:
|
|
1623
|
+
print(f"[ERROR] Error extracting telescope position: {str(e)}")
|
|
1624
|
+
import traceback
|
|
1625
|
+
|
|
1626
|
+
traceback.print_exc()
|
|
1627
|
+
return None, None, None, None
|
|
1628
|
+
|
|
1629
|
+
|
|
1630
|
+
def test_extract_telescope_position():
|
|
1631
|
+
"""
|
|
1632
|
+
Test the extract_telescope_position function with a sample metadata dictionary.
|
|
1633
|
+
"""
|
|
1634
|
+
# Sample metadata dictionary
|
|
1635
|
+
metadata = {
|
|
1636
|
+
"axisnames": ["Right Ascension", "Declination", "Stokes", "Frequency"],
|
|
1637
|
+
"axisunits": ["rad", "rad", "", "Hz"],
|
|
1638
|
+
"defaultmask": "",
|
|
1639
|
+
"hasmask": False,
|
|
1640
|
+
"imagetype": "Intensity",
|
|
1641
|
+
"incr": [-2.08469883e-04, 2.08469883e-04, 1.00000000e00, 1.60000000e05],
|
|
1642
|
+
"masks": [],
|
|
1643
|
+
"messages": [
|
|
1644
|
+
"\nImage name : solar_2014_11_03_06_12_50.20_125.995MHz.image\nObject name : Sun\nImage type : PagedImage\nImage quantity : Intensity\nPixel mask(s) : None\nRegion(s) : None\nImage units : Jy/beam\nRestoring Beam : 408.885 arcsec, 303.362 arcsec, -67.7405 deg",
|
|
1645
|
+
"\nDirection reference : J2000\nSpectral reference : Undefined\nVelocity type : RADIO\nTelescope : MWA\nObserver : DivyaOberoi\nDate observation : 2014/11/03/06:12:50.200000\nTelescope position: [-2.55945e+06m, 5.09537e+06m, -2.84906e+06m] (ITRF)\n\nAxis Coord Type Name Proj Shape Tile Coord value at pixel Coord incr Units\n------------------------------------------------------------------------------------------------ \n0 0 Direction Right Ascension SIN 1280 160 14:30:29.637 640.00 -4.300000e+01 arcsec\n1 0 Direction Declination SIN 1280 160 -14.58.57.711 640.00 4.300000e+01 arcsec\n2 1 Stokes Stokes 4 1 I Q U V\n3 2 Spectral Frequency 1 1 1.25995e+08 0.00 1.600000e+05 Hz",
|
|
1646
|
+
],
|
|
1647
|
+
"ndim": 4,
|
|
1648
|
+
"refpix": [640.0, 640.0, 0.0, 0.0],
|
|
1649
|
+
"refval": [-2.48493895e00, -2.61497402e-01, 1.00000000e00, 1.25995000e08],
|
|
1650
|
+
"restoringbeam": {
|
|
1651
|
+
"major": {"unit": "arcsec", "value": 408.88454881068},
|
|
1652
|
+
"minor": {"unit": "arcsec", "value": 303.36225356523596},
|
|
1653
|
+
"positionangle": {"unit": "deg", "value": -67.74046593535},
|
|
1654
|
+
},
|
|
1655
|
+
"shape": [1280, 1280, 4, 1],
|
|
1656
|
+
"tileshape": [160, 160, 1, 1],
|
|
1657
|
+
"unit": "Jy/beam",
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
# Test with the original metadata
|
|
1661
|
+
#print("\nTest 1: Original metadata")
|
|
1662
|
+
lat, lon, height, observatory = extract_telescope_position(metadata)
|
|
1663
|
+
|
|
1664
|
+
# Print the results
|
|
1665
|
+
#print("\nExtracted Telescope Position:")
|
|
1666
|
+
#print(f"Latitude: {lat}")
|
|
1667
|
+
#print(f"Longitude: {lon}")
|
|
1668
|
+
#print(f"Height: {height} meters")
|
|
1669
|
+
#print(f"Observatory: {observatory}")
|
|
1670
|
+
|
|
1671
|
+
# Print the values to use in convert_to_hpc
|
|
1672
|
+
#print("\nUse these values in convert_to_hpc as:")
|
|
1673
|
+
#print(f'lat="{lat}", long="{lon}", height={height}, observatory="{observatory}"')
|
|
1674
|
+
|
|
1675
|
+
# Test with metadata that has observatory directly in the dictionary
|
|
1676
|
+
#print("\nTest 2: Metadata with observatory directly in the dictionary")
|
|
1677
|
+
metadata2 = metadata.copy()
|
|
1678
|
+
metadata2["observatory"] = "MWA"
|
|
1679
|
+
|
|
1680
|
+
lat2, lon2, height2, observatory2 = extract_telescope_position(metadata2)
|
|
1681
|
+
|
|
1682
|
+
# Print the results
|
|
1683
|
+
#print("\nExtracted Telescope Position:")
|
|
1684
|
+
#print(f"Latitude: {lat2}")
|
|
1685
|
+
#print(f"Longitude: {lon2}")
|
|
1686
|
+
#print(f"Height: {height2} meters")
|
|
1687
|
+
#print(f"Observatory: {observatory2}")
|
|
1688
|
+
|
|
1689
|
+
# Print the values to use in convert_to_hpc
|
|
1690
|
+
#print("\nUse these values in convert_to_hpc as:")
|
|
1691
|
+
#print(
|
|
1692
|
+
# f'lat="{lat2}", long="{lon2}", height={height2}, observatory="{observatory2}"'
|
|
1693
|
+
#)
|
|
1694
|
+
|
|
1695
|
+
return lat, lon, height, observatory
|
|
1696
|
+
|
|
1697
|
+
|
|
1698
|
+
def main():
|
|
1699
|
+
"""fits_file = "/home/soham/solarviewer/test_data/mwa_solar.fits"
|
|
1700
|
+
hpc_map, c1, c2 = convert_to_hpc(
|
|
1701
|
+
fits_file,
|
|
1702
|
+
lat="52:55:53.90",
|
|
1703
|
+
long="06:51:56.95",
|
|
1704
|
+
height=50.16,
|
|
1705
|
+
observatory="LOFAR",
|
|
1706
|
+
Stokes="Lfrac",
|
|
1707
|
+
)
|
|
1708
|
+
fig = plt.figure()
|
|
1709
|
+
ax = fig.add_subplot(111, projection=hpc_map)
|
|
1710
|
+
im = hpc_map.plot(axes=ax, cmap="gray", title=False)
|
|
1711
|
+
plt.savefig("hpc_map.png")"""
|
|
1712
|
+
|
|
1713
|
+
# Example 1: Basic usage with default parameters
|
|
1714
|
+
"""plot_helioprojective_map(fits_file)
|
|
1715
|
+
|
|
1716
|
+
# Example 2: Customized plot
|
|
1717
|
+
plot_helioprojective_map(
|
|
1718
|
+
fits_file,
|
|
1719
|
+
output_file="custom_helioprojective_map.png",
|
|
1720
|
+
cmap="hot",
|
|
1721
|
+
figsize=(10, 10),
|
|
1722
|
+
dpi=150,
|
|
1723
|
+
show_limb=True,
|
|
1724
|
+
show_grid=False,
|
|
1725
|
+
observatory="LOFAR", # This is passed to convert_to_hpc
|
|
1726
|
+
)"""
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
if __name__ == "__main__":
|
|
1730
|
+
# main()
|
|
1731
|
+
"""imagename = "/home/soham/solarviewer/test_data/LOFAR_HBA_noisestorm.fits"
|
|
1732
|
+
from casatools import image as IA
|
|
1733
|
+
|
|
1734
|
+
ia = IA()
|
|
1735
|
+
ia.open(imagename)
|
|
1736
|
+
metadata = ia.summary(list=False)
|
|
1737
|
+
ia.close()
|
|
1738
|
+
print(metadata)
|
|
1739
|
+
lat, lon, height, observatory = extract_telescope_position(metadata)
|
|
1740
|
+
print(f"Latitude: {lat}")
|
|
1741
|
+
print(f"Longitude: {lon}")
|
|
1742
|
+
print(f"Height: {height} meters")
|
|
1743
|
+
print(f"Observatory: {observatory}")"""
|
|
1744
|
+
import argparse
|
|
1745
|
+
|
|
1746
|
+
# Create the argument parser
|
|
1747
|
+
parser = argparse.ArgumentParser(
|
|
1748
|
+
description="Convert a FITS file to helioprojective coordinates and save it as a FITS file"
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
# Add subparsers for different commands
|
|
1752
|
+
subparsers = parser.add_subparsers(dest="command", help="Command to execute")
|
|
1753
|
+
|
|
1754
|
+
# Subparser for the convert command
|
|
1755
|
+
convert_parser = subparsers.add_parser(
|
|
1756
|
+
"convert", help="Convert a FITS file to helioprojective coordinates"
|
|
1757
|
+
)
|
|
1758
|
+
convert_parser.add_argument("input_file", help="Path to the input FITS file")
|
|
1759
|
+
convert_parser.add_argument("output_file", help="Path to save the output FITS file")
|
|
1760
|
+
convert_parser.add_argument(
|
|
1761
|
+
"--stokes", default="I", help="Stokes parameter to use (default: I)"
|
|
1762
|
+
)
|
|
1763
|
+
convert_parser.add_argument(
|
|
1764
|
+
"--threshold", type=float, default=10, help="Threshold value (default: 10)"
|
|
1765
|
+
)
|
|
1766
|
+
convert_parser.add_argument(
|
|
1767
|
+
"--lat",
|
|
1768
|
+
default=None,
|
|
1769
|
+
help="Observer latitude (default: None (Example: -26:42:11.95, MWA))",
|
|
1770
|
+
)
|
|
1771
|
+
convert_parser.add_argument(
|
|
1772
|
+
"--long",
|
|
1773
|
+
default=None,
|
|
1774
|
+
help="Observer longitude (default: None (Example: 116:40:14.93, MWA))",
|
|
1775
|
+
)
|
|
1776
|
+
convert_parser.add_argument(
|
|
1777
|
+
"--height",
|
|
1778
|
+
type=float,
|
|
1779
|
+
default=None,
|
|
1780
|
+
help="Observer height in meters (default: None (Example: 377.8, MWA))",
|
|
1781
|
+
)
|
|
1782
|
+
convert_parser.add_argument(
|
|
1783
|
+
"--observatory", default="MWA", help="Observatory name (default: MWA)"
|
|
1784
|
+
)
|
|
1785
|
+
convert_parser.add_argument(
|
|
1786
|
+
"--no-overwrite",
|
|
1787
|
+
action="store_true",
|
|
1788
|
+
help="Do not overwrite the output file if it exists",
|
|
1789
|
+
)
|
|
1790
|
+
|
|
1791
|
+
# Subparser for the itrf command
|
|
1792
|
+
itrf_parser = subparsers.add_parser(
|
|
1793
|
+
"itrf", help="Convert ITRF coordinates to geodetic coordinates"
|
|
1794
|
+
)
|
|
1795
|
+
itrf_parser.add_argument("x", type=float, help="X coordinate in meters")
|
|
1796
|
+
itrf_parser.add_argument("y", type=float, help="Y coordinate in meters")
|
|
1797
|
+
itrf_parser.add_argument("z", type=float, help="Z coordinate in meters")
|
|
1798
|
+
itrf_parser.add_argument(
|
|
1799
|
+
"--scale",
|
|
1800
|
+
type=float,
|
|
1801
|
+
default=1.0,
|
|
1802
|
+
help="Scale factor to apply to the coordinates (default: 1.0)",
|
|
1803
|
+
)
|
|
1804
|
+
|
|
1805
|
+
# Subparser for the geodetic command
|
|
1806
|
+
geodetic_parser = subparsers.add_parser(
|
|
1807
|
+
"geodetic", help="Convert geodetic coordinates to ITRF coordinates"
|
|
1808
|
+
)
|
|
1809
|
+
geodetic_parser.add_argument(
|
|
1810
|
+
"lat", help="Latitude in the format DD:MM:SS.SS or decimal degrees"
|
|
1811
|
+
)
|
|
1812
|
+
geodetic_parser.add_argument(
|
|
1813
|
+
"lon", help="Longitude in the format DD:MM:SS.SS or decimal degrees"
|
|
1814
|
+
)
|
|
1815
|
+
geodetic_parser.add_argument(
|
|
1816
|
+
"height", type=float, help="Height in meters above the WGS84 ellipsoid"
|
|
1817
|
+
)
|
|
1818
|
+
|
|
1819
|
+
# Subparser for the test command
|
|
1820
|
+
test_parser = subparsers.add_parser(
|
|
1821
|
+
"test", help="Run tests for the helioprojective module"
|
|
1822
|
+
)
|
|
1823
|
+
test_parser.add_argument(
|
|
1824
|
+
"--extract-position",
|
|
1825
|
+
action="store_true",
|
|
1826
|
+
help="Test the extract_telescope_position function",
|
|
1827
|
+
)
|
|
1828
|
+
|
|
1829
|
+
# Parse the arguments
|
|
1830
|
+
args = parser.parse_args()
|
|
1831
|
+
|
|
1832
|
+
# Execute the appropriate command
|
|
1833
|
+
if args.command == "convert":
|
|
1834
|
+
success = convert_and_save_hpc(
|
|
1835
|
+
args.input_file,
|
|
1836
|
+
args.output_file,
|
|
1837
|
+
Stokes=args.stokes,
|
|
1838
|
+
thres=args.threshold,
|
|
1839
|
+
lat=args.lat,
|
|
1840
|
+
long=args.long,
|
|
1841
|
+
height=args.height,
|
|
1842
|
+
observatory=args.observatory,
|
|
1843
|
+
overwrite=not args.no_overwrite,
|
|
1844
|
+
)
|
|
1845
|
+
|
|
1846
|
+
if success:
|
|
1847
|
+
print("Conversion successful!")
|
|
1848
|
+
else:
|
|
1849
|
+
print("Conversion failed.")
|
|
1850
|
+
sys.exit(1)
|
|
1851
|
+
elif args.command == "itrf":
|
|
1852
|
+
lat, lon, height = itrf_to_geodetic(args.x, args.y, args.z, args.scale)
|
|
1853
|
+
if lat is not None:
|
|
1854
|
+
print(f"Latitude: {lat}")
|
|
1855
|
+
print(f"Longitude: {lon}")
|
|
1856
|
+
print(f"Height: {height} meters")
|
|
1857
|
+
print("\nUse these values in convert_to_hpc as:")
|
|
1858
|
+
print(f'lat="{lat}", long="{lon}", height={height}')
|
|
1859
|
+
else:
|
|
1860
|
+
print("ITRF conversion failed.")
|
|
1861
|
+
sys.exit(1)
|
|
1862
|
+
elif args.command == "geodetic":
|
|
1863
|
+
try:
|
|
1864
|
+
# Try to convert lat/lon to float if they are in decimal format
|
|
1865
|
+
try:
|
|
1866
|
+
lat = float(args.lat)
|
|
1867
|
+
except ValueError:
|
|
1868
|
+
lat = args.lat
|
|
1869
|
+
|
|
1870
|
+
try:
|
|
1871
|
+
lon = float(args.lon)
|
|
1872
|
+
except ValueError:
|
|
1873
|
+
lon = args.lon
|
|
1874
|
+
|
|
1875
|
+
x, y, z = geodetic_to_itrf(lat, lon, args.height)
|
|
1876
|
+
if x is not None:
|
|
1877
|
+
print(f"ITRF Coordinates:")
|
|
1878
|
+
print(f"X: {x:.3f} meters")
|
|
1879
|
+
print(f"Y: {y:.3f} meters")
|
|
1880
|
+
print(f"Z: {z:.3f} meters")
|
|
1881
|
+
else:
|
|
1882
|
+
print("Geodetic to ITRF conversion failed.")
|
|
1883
|
+
sys.exit(1)
|
|
1884
|
+
except Exception as e:
|
|
1885
|
+
print(f"Error: {str(e)}")
|
|
1886
|
+
sys.exit(1)
|
|
1887
|
+
elif args.command == "test":
|
|
1888
|
+
if args.extract_position:
|
|
1889
|
+
test_extract_telescope_position()
|
|
1890
|
+
else:
|
|
1891
|
+
print(
|
|
1892
|
+
"No test specified. Use --extract-position to test the extract_telescope_position function."
|
|
1893
|
+
)
|
|
1894
|
+
sys.exit(1)
|
|
1895
|
+
else:
|
|
1896
|
+
# Default behavior for backward compatibility
|
|
1897
|
+
if len(sys.argv) >= 3 and not sys.argv[1].startswith("-"):
|
|
1898
|
+
# Assume the first two arguments are input_file and output_file
|
|
1899
|
+
success = convert_and_save_hpc(
|
|
1900
|
+
sys.argv[1],
|
|
1901
|
+
sys.argv[2],
|
|
1902
|
+
# Parse any additional arguments
|
|
1903
|
+
**{
|
|
1904
|
+
k.lstrip("-").replace("-", "_"): v
|
|
1905
|
+
for k, v in zip(sys.argv[3::2], sys.argv[4::2])
|
|
1906
|
+
},
|
|
1907
|
+
)
|
|
1908
|
+
|
|
1909
|
+
if success:
|
|
1910
|
+
print("Conversion successful!")
|
|
1911
|
+
else:
|
|
1912
|
+
print("Conversion failed.")
|
|
1913
|
+
sys.exit(1)
|
|
1914
|
+
else:
|
|
1915
|
+
parser.print_help()
|
|
1916
|
+
sys.exit(1)
|