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,1269 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import numpy as np
|
|
3
|
+
import sys
|
|
4
|
+
import glob
|
|
5
|
+
from casatools import msmetadata, table, measures, quanta, image
|
|
6
|
+
from casatasks import *
|
|
7
|
+
from astropy.io import fits
|
|
8
|
+
from astropy.wcs import WCS
|
|
9
|
+
import scipy.ndimage as ndi
|
|
10
|
+
import argparse
|
|
11
|
+
import multiprocessing
|
|
12
|
+
from multiprocessing import Pool
|
|
13
|
+
from functools import partial
|
|
14
|
+
import shutil
|
|
15
|
+
import hashlib
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SolarPhaseCenter:
|
|
19
|
+
"""
|
|
20
|
+
Class to calculate and apply phase shifts to solar images
|
|
21
|
+
|
|
22
|
+
This class contains methods to:
|
|
23
|
+
1. Calculate the difference between solar center and phase center
|
|
24
|
+
2. Apply the phase shift to align the solar center with the phase center
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
msname : str
|
|
29
|
+
Name of the measurement set
|
|
30
|
+
cellsize : float
|
|
31
|
+
Cell size of the image in arcsec
|
|
32
|
+
imsize : int
|
|
33
|
+
Size of the image in pixels
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, msname=None, cellsize=None, imsize=None):
|
|
37
|
+
self.msname = msname
|
|
38
|
+
self.cellsize = cellsize # in arcsec
|
|
39
|
+
self.imsize = imsize
|
|
40
|
+
|
|
41
|
+
# Get working directory
|
|
42
|
+
self.cwd = os.getcwd()
|
|
43
|
+
|
|
44
|
+
# Initialize rms boxes with default values
|
|
45
|
+
self.rms_box = "50,50,100,75"
|
|
46
|
+
self.rms_box_nearsun = "40,40,80,60"
|
|
47
|
+
|
|
48
|
+
# Setup RMS box for calculations (near Sun and general)
|
|
49
|
+
if imsize is not None and cellsize is not None:
|
|
50
|
+
self.setup_rms_boxes(imsize, cellsize)
|
|
51
|
+
|
|
52
|
+
def setup_rms_boxes(self, imsize, cellsize):
|
|
53
|
+
"""
|
|
54
|
+
Set up RMS boxes for calculations
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
imsize : int
|
|
59
|
+
Size of the image in pixels
|
|
60
|
+
cellsize : float
|
|
61
|
+
Cell size in arcsec
|
|
62
|
+
"""
|
|
63
|
+
# Ensure parameters are valid
|
|
64
|
+
if imsize <= 0 or cellsize <= 0:
|
|
65
|
+
print("Warning: Invalid image size or cell size. Using default RMS boxes.")
|
|
66
|
+
self.rms_box = "50,50,100,75"
|
|
67
|
+
self.rms_box_nearsun = "40,40,80,60"
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
# General RMS box - set to a reasonable size relative to the image
|
|
71
|
+
rms_width = min(int(imsize / 4), imsize - 50)
|
|
72
|
+
self.rms_box = f"50,50,{min(imsize-10, 100)},{min(rms_width, 100)}"
|
|
73
|
+
|
|
74
|
+
try:
|
|
75
|
+
# Calculate reasonable values for boxcenter_y and ywidth
|
|
76
|
+
# Using a safer approach to avoid negative values
|
|
77
|
+
center_y = int(imsize / 2)
|
|
78
|
+
|
|
79
|
+
# Calculate offsets based on solar diameter, but ensure they're reasonable
|
|
80
|
+
y_offset = min(int(3 * 3600 / max(1, cellsize)), int(imsize / 4))
|
|
81
|
+
boxcenter_y = max(y_offset + 10, center_y - y_offset)
|
|
82
|
+
|
|
83
|
+
# Limit ywidth to prevent box from going outside image
|
|
84
|
+
ywidth = min(int(3600 / max(1, cellsize)), int(imsize / 6))
|
|
85
|
+
|
|
86
|
+
# Reference center of the image for x coordinate
|
|
87
|
+
boxcenter_x = center_y
|
|
88
|
+
|
|
89
|
+
# Calculate safe box bounds (ensure at least 10 pixels from each edge)
|
|
90
|
+
safe_margin = 10
|
|
91
|
+
x_min = safe_margin
|
|
92
|
+
y_min = safe_margin
|
|
93
|
+
x_max = imsize - safe_margin
|
|
94
|
+
y_max = imsize - safe_margin
|
|
95
|
+
|
|
96
|
+
# Ensure the box is inside the image and has reasonable size
|
|
97
|
+
box_width = min(int(imsize / 5), (x_max - x_min) / 2)
|
|
98
|
+
box_height = min(ywidth, (y_max - y_min) / 2)
|
|
99
|
+
|
|
100
|
+
# Define box coordinates ensuring they're within image bounds
|
|
101
|
+
x1 = max(x_min, boxcenter_x - box_width)
|
|
102
|
+
y1 = max(y_min, boxcenter_y - box_height)
|
|
103
|
+
x2 = min(x_max, boxcenter_x + box_width)
|
|
104
|
+
y2 = min(y_max, boxcenter_y + box_height)
|
|
105
|
+
|
|
106
|
+
# Ensure the box has minimum dimensions
|
|
107
|
+
if x2 - x1 < 20:
|
|
108
|
+
x2 = min(x_max, x1 + 20)
|
|
109
|
+
if y2 - y1 < 20:
|
|
110
|
+
y2 = min(y_max, y1 + 20)
|
|
111
|
+
|
|
112
|
+
self.rms_box_nearsun = f"{int(x1)},{int(y1)},{int(x2)},{int(y2)}"
|
|
113
|
+
print(f"RMS box near sun: {self.rms_box_nearsun}")
|
|
114
|
+
except Exception as e:
|
|
115
|
+
print(f"Error setting up RMS boxes: {e}")
|
|
116
|
+
# Fallback to a very conservative box that should work for any image
|
|
117
|
+
self.rms_box_nearsun = (
|
|
118
|
+
f"{safe_margin},{safe_margin},{imsize-safe_margin},{imsize-safe_margin}"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def get_phasecenter(self):
|
|
122
|
+
"""
|
|
123
|
+
Get the phase center of the MS
|
|
124
|
+
|
|
125
|
+
Returns
|
|
126
|
+
-------
|
|
127
|
+
tuple
|
|
128
|
+
(radec_str, radeg, decdeg) - RA/DEC as string and degrees
|
|
129
|
+
"""
|
|
130
|
+
if self.msname is None:
|
|
131
|
+
print("Error: MS name not provided")
|
|
132
|
+
return None, None, None
|
|
133
|
+
|
|
134
|
+
ms_meta = msmetadata()
|
|
135
|
+
ms_meta.open(self.msname)
|
|
136
|
+
|
|
137
|
+
# Get field ID 0 (assuming single field)
|
|
138
|
+
t = table()
|
|
139
|
+
t.open(f"{self.msname}/FIELD")
|
|
140
|
+
direction = t.getcol("PHASE_DIR")
|
|
141
|
+
t.close()
|
|
142
|
+
|
|
143
|
+
# Convert to degrees
|
|
144
|
+
radeg = np.degrees(direction[0][0][0])
|
|
145
|
+
decdeg = np.degrees(direction[0][0][1])
|
|
146
|
+
|
|
147
|
+
# Format as strings
|
|
148
|
+
ra_hms = self.deg2hms(radeg)
|
|
149
|
+
dec_dms = self.deg2dms(decdeg)
|
|
150
|
+
|
|
151
|
+
ms_meta.close()
|
|
152
|
+
return [ra_hms, dec_dms], radeg, decdeg
|
|
153
|
+
|
|
154
|
+
def deg2hms(self, ra_deg):
|
|
155
|
+
"""
|
|
156
|
+
Convert RA from degrees to HH:MM:SS.SSS format
|
|
157
|
+
|
|
158
|
+
Parameters
|
|
159
|
+
----------
|
|
160
|
+
ra_deg : float
|
|
161
|
+
RA in degrees
|
|
162
|
+
|
|
163
|
+
Returns
|
|
164
|
+
-------
|
|
165
|
+
str
|
|
166
|
+
RA in HH:MM:SS.SSS format
|
|
167
|
+
"""
|
|
168
|
+
ra_hour = ra_deg / 15.0
|
|
169
|
+
ra_h = int(ra_hour)
|
|
170
|
+
ra_m = int((ra_hour - ra_h) * 60)
|
|
171
|
+
ra_s = ((ra_hour - ra_h) * 60 - ra_m) * 60
|
|
172
|
+
return f"{ra_h:02d}h{ra_m:02d}m{ra_s:.3f}s"
|
|
173
|
+
|
|
174
|
+
def deg2dms(self, dec_deg):
|
|
175
|
+
"""
|
|
176
|
+
Convert DEC from degrees to DD:MM:SS.SSS format
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
dec_deg : float
|
|
181
|
+
DEC in degrees
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
str
|
|
186
|
+
DEC in DD:MM:SS.SSS format
|
|
187
|
+
"""
|
|
188
|
+
dec_sign = "+" if dec_deg >= 0 else "-"
|
|
189
|
+
dec_deg = abs(dec_deg)
|
|
190
|
+
dec_d = int(dec_deg)
|
|
191
|
+
dec_m = int((dec_deg - dec_d) * 60)
|
|
192
|
+
dec_s = ((dec_deg - dec_d) * 60 - dec_m) * 60
|
|
193
|
+
return f"{dec_sign}{dec_d:02d}d{dec_m:02d}m{dec_s:.3f}s"
|
|
194
|
+
|
|
195
|
+
def negative_box(self, max_pix, imsize=None, box_width=3):
|
|
196
|
+
"""
|
|
197
|
+
Create a box around the maximum pixel for searching
|
|
198
|
+
|
|
199
|
+
Parameters
|
|
200
|
+
----------
|
|
201
|
+
max_pix : list
|
|
202
|
+
Maximum pixel [xxmax, yymax]
|
|
203
|
+
imsize : int
|
|
204
|
+
Image size (if None, use self.imsize)
|
|
205
|
+
box_width : float
|
|
206
|
+
Box width in degrees (default: 3 degrees)
|
|
207
|
+
|
|
208
|
+
Returns
|
|
209
|
+
-------
|
|
210
|
+
str
|
|
211
|
+
CASA box format 'xblc,yblc,xrtc,yrtc'
|
|
212
|
+
"""
|
|
213
|
+
if imsize is None:
|
|
214
|
+
imsize = self.imsize
|
|
215
|
+
|
|
216
|
+
if self.cellsize is None:
|
|
217
|
+
print("Error: Cell size not provided")
|
|
218
|
+
return "0,0,0,0"
|
|
219
|
+
|
|
220
|
+
max_pix_xx = max_pix[0]
|
|
221
|
+
max_pix_yy = max_pix[1]
|
|
222
|
+
|
|
223
|
+
# Calculate box length in pixels (box_width in degrees, cellsize in arcsec)
|
|
224
|
+
box_length = (float(box_width) * 3600.0) / self.cellsize
|
|
225
|
+
|
|
226
|
+
xblc = max(0, int(max_pix_xx - (box_length / 2.0)))
|
|
227
|
+
yblc = max(0, int(max_pix_yy - (box_length / 2.0)))
|
|
228
|
+
xrtc = min(imsize - 1, int(max_pix_xx + (box_length / 2.0)))
|
|
229
|
+
yrtc = min(imsize - 1, int(max_pix_yy + (box_length / 2.0)))
|
|
230
|
+
|
|
231
|
+
return f"{xblc},{yblc},{xrtc},{yrtc}"
|
|
232
|
+
|
|
233
|
+
def create_circular_mask(self, h, w, center=None, radius=None):
|
|
234
|
+
"""
|
|
235
|
+
Create a circular mask for an image
|
|
236
|
+
|
|
237
|
+
Parameters
|
|
238
|
+
----------
|
|
239
|
+
h, w : int
|
|
240
|
+
Height and width of the image
|
|
241
|
+
center : tuple
|
|
242
|
+
(x, y) center of the circle
|
|
243
|
+
radius : float
|
|
244
|
+
Radius of the circle
|
|
245
|
+
|
|
246
|
+
Returns
|
|
247
|
+
-------
|
|
248
|
+
ndarray
|
|
249
|
+
Boolean mask array (True inside circle, False outside)
|
|
250
|
+
"""
|
|
251
|
+
if center is None:
|
|
252
|
+
center = (int(w / 2), int(h / 2))
|
|
253
|
+
if radius is None:
|
|
254
|
+
radius = min(center[0], center[1], w - center[0], h - center[1])
|
|
255
|
+
|
|
256
|
+
Y, X = np.ogrid[:h, :w]
|
|
257
|
+
dist_from_center = np.sqrt((X - center[0]) ** 2 + (Y - center[1]) ** 2)
|
|
258
|
+
|
|
259
|
+
mask = dist_from_center <= radius
|
|
260
|
+
return mask
|
|
261
|
+
|
|
262
|
+
def calc_sun_dia(self):
|
|
263
|
+
"""
|
|
264
|
+
Calculate the apparent diameter of the sun in arcmin
|
|
265
|
+
|
|
266
|
+
Returns
|
|
267
|
+
-------
|
|
268
|
+
float
|
|
269
|
+
Sun diameter in arcmin
|
|
270
|
+
"""
|
|
271
|
+
# Standard solar diameter in arcmin at 1 AU
|
|
272
|
+
standard_dia = 32.0
|
|
273
|
+
|
|
274
|
+
if self.msname is None:
|
|
275
|
+
return standard_dia
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
# Get the observation time
|
|
279
|
+
ms_meta = msmetadata()
|
|
280
|
+
ms_meta.open(self.msname)
|
|
281
|
+
|
|
282
|
+
time_mid = ms_meta.timesforfield(0)[int(len(ms_meta.timesforfield(0)) / 2)]
|
|
283
|
+
|
|
284
|
+
# Setup measures and quanta tools
|
|
285
|
+
me = measures()
|
|
286
|
+
qa = quanta()
|
|
287
|
+
|
|
288
|
+
# Set the reference frame
|
|
289
|
+
me.doframe(me.epoch("UTC", qa.quantity(time_mid, "s")))
|
|
290
|
+
me.doframe(me.observatory("LOFAR")) # Assuming LOFAR observations
|
|
291
|
+
|
|
292
|
+
# Get the sun position
|
|
293
|
+
sun_pos = me.direction("SUN")
|
|
294
|
+
|
|
295
|
+
# Get the distance to sun in AU
|
|
296
|
+
sun_dist = me.separation(me.direction("SUN"), me.direction("SUN_DIST"))
|
|
297
|
+
sun_dist_au = qa.convert(sun_dist, "AU")["value"]
|
|
298
|
+
|
|
299
|
+
# Scale the solar diameter
|
|
300
|
+
sun_dia = standard_dia / sun_dist_au
|
|
301
|
+
|
|
302
|
+
ms_meta.close()
|
|
303
|
+
return sun_dia
|
|
304
|
+
except Exception as e:
|
|
305
|
+
print(f"Error calculating sun diameter: {e}")
|
|
306
|
+
return standard_dia
|
|
307
|
+
|
|
308
|
+
def cal_solar_phaseshift(self, imagename, fit_gaussian=True, sigma=10):
|
|
309
|
+
"""
|
|
310
|
+
Calculate the difference between solar center and phase center of the image
|
|
311
|
+
|
|
312
|
+
Parameters
|
|
313
|
+
----------
|
|
314
|
+
imagename : str
|
|
315
|
+
Name of the image
|
|
316
|
+
fit_gaussian : bool
|
|
317
|
+
Perform Gaussian fitting to unresolved Sun to estimate solar center
|
|
318
|
+
sigma : float
|
|
319
|
+
If Gaussian fitting is not used, threshold for estimating center of mass
|
|
320
|
+
|
|
321
|
+
Returns
|
|
322
|
+
-------
|
|
323
|
+
float
|
|
324
|
+
RA of the solar center in degrees
|
|
325
|
+
float
|
|
326
|
+
DEC of the solar center in degrees
|
|
327
|
+
bool
|
|
328
|
+
Whether phase shift is required or not
|
|
329
|
+
"""
|
|
330
|
+
# Get current phase center
|
|
331
|
+
if self.msname:
|
|
332
|
+
radec_str, radeg, decdeg = self.get_phasecenter()
|
|
333
|
+
else:
|
|
334
|
+
# If no MS provided, extract from image header
|
|
335
|
+
ia = image()
|
|
336
|
+
ia.open(imagename)
|
|
337
|
+
csys = ia.coordsys()
|
|
338
|
+
radeg = csys.referencepixel()["numeric"][0]
|
|
339
|
+
decdeg = csys.referencepixel()["numeric"][1]
|
|
340
|
+
ia.close()
|
|
341
|
+
|
|
342
|
+
# Extract cell size and imsize from image if not provided
|
|
343
|
+
if self.cellsize is None or self.imsize is None:
|
|
344
|
+
try:
|
|
345
|
+
header = imhead(imagename=imagename, mode="list")
|
|
346
|
+
self.cellsize = np.abs(
|
|
347
|
+
np.rad2deg(header["cdelt1"]) * 3600.0
|
|
348
|
+
) # Convert to arcsec
|
|
349
|
+
self.imsize = header["shape"][0]
|
|
350
|
+
|
|
351
|
+
# Setup RMS boxes now that we have the required parameters
|
|
352
|
+
self.setup_rms_boxes(self.imsize, self.cellsize)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
print(f"Error extracting image properties: {e}")
|
|
355
|
+
return radeg, decdeg, False
|
|
356
|
+
|
|
357
|
+
# Method 1: Fit Gaussian
|
|
358
|
+
if fit_gaussian:
|
|
359
|
+
sun_dia = self.calc_sun_dia() # In arcmin
|
|
360
|
+
unresolved_image = f"{imagename.split('.image')[0]}_unresolved.image"
|
|
361
|
+
|
|
362
|
+
if os.path.exists(unresolved_image):
|
|
363
|
+
os.system(f"rm -rf {unresolved_image}")
|
|
364
|
+
|
|
365
|
+
# Smooth the image to sun size
|
|
366
|
+
imsmooth(
|
|
367
|
+
imagename=imagename,
|
|
368
|
+
targetres=True,
|
|
369
|
+
major=f"{sun_dia}arcmin",
|
|
370
|
+
minor=f"{sun_dia}arcmin",
|
|
371
|
+
pa="0deg",
|
|
372
|
+
outfile=unresolved_image,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
maxpos = imstat(imagename=imagename)["maxpos"]
|
|
376
|
+
fit_box = self.negative_box(maxpos, box_width=3)
|
|
377
|
+
|
|
378
|
+
# Fit gaussian to smoothed image
|
|
379
|
+
fitted_params = imfit(imagename=unresolved_image, box=fit_box)
|
|
380
|
+
|
|
381
|
+
try:
|
|
382
|
+
# Extract RA/DEC from fit
|
|
383
|
+
ra = np.rad2deg(
|
|
384
|
+
fitted_params["deconvolved"]["component0"]["shape"]["direction"][
|
|
385
|
+
"m0"
|
|
386
|
+
]["value"]
|
|
387
|
+
)
|
|
388
|
+
dec = np.rad2deg(
|
|
389
|
+
fitted_params["deconvolved"]["component0"]["shape"]["direction"][
|
|
390
|
+
"m1"
|
|
391
|
+
]["value"]
|
|
392
|
+
)
|
|
393
|
+
|
|
394
|
+
# Check if shift is significant
|
|
395
|
+
if np.sqrt((ra - radeg) ** 2 + (dec - decdeg) ** 2) < (
|
|
396
|
+
self.cellsize / 3600.0
|
|
397
|
+
):
|
|
398
|
+
os.system(f"rm -rf {unresolved_image}")
|
|
399
|
+
return radeg, decdeg, False
|
|
400
|
+
else:
|
|
401
|
+
os.system(f"rm -rf {unresolved_image}")
|
|
402
|
+
return ra, dec, True
|
|
403
|
+
except:
|
|
404
|
+
os.system(f"rm -rf {unresolved_image}")
|
|
405
|
+
print("Error in Gaussian fitting, trying alternate method")
|
|
406
|
+
# Fall through to center of mass method
|
|
407
|
+
|
|
408
|
+
# Method 2: Center of mass method
|
|
409
|
+
image_path = os.path.dirname(os.path.abspath(imagename))
|
|
410
|
+
temp_prefix = f"{image_path}/phaseshift"
|
|
411
|
+
|
|
412
|
+
os.system(f"rm -rf {temp_prefix}*")
|
|
413
|
+
|
|
414
|
+
# Setup for center of mass calculation
|
|
415
|
+
if os.path.isfile(f"{temp_prefix}.fits"):
|
|
416
|
+
os.system(f"rm -rf {temp_prefix}.fits")
|
|
417
|
+
|
|
418
|
+
# Export to FITS for easier manipulation
|
|
419
|
+
exportfits(
|
|
420
|
+
imagename=imagename,
|
|
421
|
+
fitsimage=f"{temp_prefix}.fits",
|
|
422
|
+
dropdeg=True,
|
|
423
|
+
dropstokes=True,
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Calculate RMS for thresholding
|
|
427
|
+
try:
|
|
428
|
+
rms = imstat(imagename=imagename, box=self.rms_box_nearsun)["rms"][0]
|
|
429
|
+
except Exception as e:
|
|
430
|
+
print(f"Error using rms_box_nearsun: {e}")
|
|
431
|
+
print("Trying with a safer box...")
|
|
432
|
+
# Try with the general RMS box instead
|
|
433
|
+
try:
|
|
434
|
+
rms = imstat(imagename=imagename, box=self.rms_box)["rms"][0]
|
|
435
|
+
except Exception as e2:
|
|
436
|
+
print(f"Error using rms_box: {e2}")
|
|
437
|
+
print("Using a very safe default box")
|
|
438
|
+
# Use a very safe default that should work for any image
|
|
439
|
+
imsize = self.imsize if self.imsize else 512
|
|
440
|
+
safe_box = f"10,10,{imsize-10},{imsize-10}"
|
|
441
|
+
try:
|
|
442
|
+
rms = imstat(imagename=imagename, box=safe_box)["rms"][0]
|
|
443
|
+
except Exception as e3:
|
|
444
|
+
print(f"Error using safe box: {e3}")
|
|
445
|
+
# Last resort: just calculate RMS from the entire image
|
|
446
|
+
ia = image()
|
|
447
|
+
ia.open(imagename)
|
|
448
|
+
data = ia.getchunk()
|
|
449
|
+
ia.close()
|
|
450
|
+
if data.size > 0:
|
|
451
|
+
# Mask NaN values
|
|
452
|
+
valid_data = data[~np.isnan(data)]
|
|
453
|
+
if valid_data.size > 0:
|
|
454
|
+
rms = np.sqrt(np.mean(valid_data**2))
|
|
455
|
+
else:
|
|
456
|
+
rms = 1.0 # Default if all values are NaN
|
|
457
|
+
else:
|
|
458
|
+
rms = 1.0 # Default if empty data
|
|
459
|
+
|
|
460
|
+
# Load FITS data
|
|
461
|
+
f = fits.open(f"{temp_prefix}.fits")
|
|
462
|
+
data = fits.getdata(f"{temp_prefix}.fits")
|
|
463
|
+
|
|
464
|
+
# Apply threshold
|
|
465
|
+
data[data <= sigma * rms] = 0
|
|
466
|
+
data[data > sigma * rms] = 1
|
|
467
|
+
|
|
468
|
+
# Handle different dimensionality
|
|
469
|
+
ndim = data.ndim
|
|
470
|
+
if ndim > 2:
|
|
471
|
+
if ndim == 3:
|
|
472
|
+
data = data[0, :, :]
|
|
473
|
+
elif ndim == 4:
|
|
474
|
+
data = data[0, 0, :, :]
|
|
475
|
+
|
|
476
|
+
# Create circular mask around center (5 degrees radius)
|
|
477
|
+
circular_mask = self.create_circular_mask(
|
|
478
|
+
data.shape[0],
|
|
479
|
+
data.shape[1],
|
|
480
|
+
center=(int(data.shape[0] / 2), int(data.shape[1] / 2)),
|
|
481
|
+
radius=int(5 / (self.cellsize / 3600.0)),
|
|
482
|
+
)
|
|
483
|
+
data[~circular_mask] = 0
|
|
484
|
+
|
|
485
|
+
# Calculate center of mass
|
|
486
|
+
cy, cx = ndi.center_of_mass(data)
|
|
487
|
+
|
|
488
|
+
# Convert pixel position to world coordinates
|
|
489
|
+
w = WCS(f"{temp_prefix}.fits")
|
|
490
|
+
try:
|
|
491
|
+
result = w.pixel_to_world(int(cx), int(cy))
|
|
492
|
+
ra = float(result.ra.deg)
|
|
493
|
+
dec = float(result.dec.deg)
|
|
494
|
+
except:
|
|
495
|
+
# Alternative method for older astropy versions
|
|
496
|
+
try:
|
|
497
|
+
result = w.array_index_to_world(0, int(cy), int(cx))
|
|
498
|
+
ra = result[0].ra.deg
|
|
499
|
+
dec = result[0].dec.deg
|
|
500
|
+
except:
|
|
501
|
+
result = w.array_index_to_world(int(cy), int(cx))
|
|
502
|
+
ra = result.ra.deg
|
|
503
|
+
dec = result.dec.deg
|
|
504
|
+
|
|
505
|
+
# Clean up
|
|
506
|
+
os.system(f"rm -rf {temp_prefix}*")
|
|
507
|
+
|
|
508
|
+
# Check if shift is significant
|
|
509
|
+
if np.sqrt((ra - radeg) ** 2 + (dec - decdeg) ** 2) < (self.cellsize / 3600.0):
|
|
510
|
+
return radeg, decdeg, False
|
|
511
|
+
else:
|
|
512
|
+
return ra, dec, True
|
|
513
|
+
|
|
514
|
+
def shift_phasecenter(self, imagename, ra, dec, stokes="I", process_id=None):
|
|
515
|
+
"""
|
|
516
|
+
Function to shift solar center to phase center of the measurement set
|
|
517
|
+
|
|
518
|
+
Parameters
|
|
519
|
+
----------
|
|
520
|
+
imagename : str
|
|
521
|
+
Name of the image
|
|
522
|
+
ra : float
|
|
523
|
+
Solar center RA in degrees
|
|
524
|
+
dec : float
|
|
525
|
+
Solar center DEC in degrees
|
|
526
|
+
stokes : str
|
|
527
|
+
Stokes parameter to use
|
|
528
|
+
process_id : int, optional
|
|
529
|
+
Process ID for multiprocessing (creates unique temp files)
|
|
530
|
+
|
|
531
|
+
Returns
|
|
532
|
+
-------
|
|
533
|
+
int
|
|
534
|
+
Success code 0: Successfully shifted, 1: Shifting not required, 2: Error
|
|
535
|
+
"""
|
|
536
|
+
try:
|
|
537
|
+
if stokes is None:
|
|
538
|
+
return 2
|
|
539
|
+
|
|
540
|
+
# Determine image type
|
|
541
|
+
if os.path.isdir(imagename):
|
|
542
|
+
imagetype = "casa"
|
|
543
|
+
else:
|
|
544
|
+
imagetype = "fits"
|
|
545
|
+
|
|
546
|
+
# Get target phase center
|
|
547
|
+
if self.msname:
|
|
548
|
+
radec_str, radeg, decdeg = self.get_phasecenter()
|
|
549
|
+
else:
|
|
550
|
+
radec_str = ["Unknown", "Unknown"]
|
|
551
|
+
radeg, decdeg = ra, dec # Just use the calculated center
|
|
552
|
+
|
|
553
|
+
image_path = os.path.dirname(os.path.abspath(imagename))
|
|
554
|
+
|
|
555
|
+
# Create unique temporary filenames for multiprocessing
|
|
556
|
+
if process_id is not None:
|
|
557
|
+
temp_image = f"{image_path}/I_model_{process_id}_{os.getpid()}"
|
|
558
|
+
temp_fits = f"{image_path}/wcs_model_{process_id}_{os.getpid()}.fits"
|
|
559
|
+
else:
|
|
560
|
+
temp_image = f"{image_path}/I.model"
|
|
561
|
+
temp_fits = f"{image_path}/wcs_model.fits"
|
|
562
|
+
|
|
563
|
+
# Clean up previous files
|
|
564
|
+
if os.path.isfile(temp_fits):
|
|
565
|
+
os.system(f"rm -rf {temp_fits}")
|
|
566
|
+
if os.path.isdir(temp_image):
|
|
567
|
+
os.system(f"rm -rf {temp_image}")
|
|
568
|
+
|
|
569
|
+
# Handle trailing slashes
|
|
570
|
+
if imagename.endswith("/"):
|
|
571
|
+
imagename = imagename[:-1]
|
|
572
|
+
|
|
573
|
+
# Extract stokes plane for coordinate calculation
|
|
574
|
+
imsubimage(
|
|
575
|
+
imagename=imagename, outfile=temp_image, stokes=stokes, dropdeg=False
|
|
576
|
+
)
|
|
577
|
+
exportfits(
|
|
578
|
+
imagename=temp_image, fitsimage=temp_fits, dropdeg=True, dropstokes=True
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
# Calculate pixel position for the target RA/DEC
|
|
582
|
+
w = WCS(temp_fits)
|
|
583
|
+
pix = np.nanmean(
|
|
584
|
+
w.all_world2pix(np.array([[ra, dec], [ra, dec]]), 0), axis=0
|
|
585
|
+
)
|
|
586
|
+
ra_pix = int(pix[0])
|
|
587
|
+
dec_pix = int(pix[1])
|
|
588
|
+
|
|
589
|
+
# Apply the shift
|
|
590
|
+
if imagetype == "casa":
|
|
591
|
+
# Update CRPIX values in CASA image
|
|
592
|
+
imhead(
|
|
593
|
+
imagename=imagename, mode="put", hdkey="CRPIX1", hdvalue=str(ra_pix)
|
|
594
|
+
)
|
|
595
|
+
imhead(
|
|
596
|
+
imagename=imagename,
|
|
597
|
+
mode="put",
|
|
598
|
+
hdkey="CRPIX2",
|
|
599
|
+
hdvalue=str(dec_pix),
|
|
600
|
+
)
|
|
601
|
+
elif imagetype == "fits":
|
|
602
|
+
# Update CRPIX values in FITS header
|
|
603
|
+
data = fits.getdata(imagename)
|
|
604
|
+
header = fits.getheader(imagename)
|
|
605
|
+
header["CRPIX1"] = float(ra_pix)
|
|
606
|
+
header["CRPIX2"] = float(dec_pix)
|
|
607
|
+
fits.writeto(imagename, data=data, header=header, overwrite=True)
|
|
608
|
+
else:
|
|
609
|
+
print("Image is not either fits or CASA format.")
|
|
610
|
+
return 1
|
|
611
|
+
|
|
612
|
+
print(
|
|
613
|
+
f"Image phase center shifted to, RA: {radec_str[0]}, DEC: {radec_str[1]}"
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Clean up
|
|
617
|
+
os.system(f"rm -rf {temp_image} {temp_fits}")
|
|
618
|
+
return 0
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
print(f"Error in shift_phasecenter: {e}")
|
|
622
|
+
return 2
|
|
623
|
+
|
|
624
|
+
def visually_center_image(self, imagename, output_file, crpix1, crpix2):
|
|
625
|
+
"""
|
|
626
|
+
Create a new visually centered image with the Sun in the middle
|
|
627
|
+
|
|
628
|
+
Parameters
|
|
629
|
+
----------
|
|
630
|
+
imagename : str
|
|
631
|
+
Name of the input image
|
|
632
|
+
output_file : str
|
|
633
|
+
Name of the output image
|
|
634
|
+
crpix1 : int
|
|
635
|
+
X coordinate of the reference pixel (solar center)
|
|
636
|
+
crpix2 : int
|
|
637
|
+
Y coordinate of the reference pixel (solar center)
|
|
638
|
+
|
|
639
|
+
Returns
|
|
640
|
+
-------
|
|
641
|
+
bool
|
|
642
|
+
True if successful, False if there was an error
|
|
643
|
+
"""
|
|
644
|
+
try:
|
|
645
|
+
# Load the image
|
|
646
|
+
hdul = fits.open(imagename)
|
|
647
|
+
header = hdul[0].header
|
|
648
|
+
data = hdul[0].data
|
|
649
|
+
|
|
650
|
+
# Get image dimensions
|
|
651
|
+
if len(data.shape) == 2:
|
|
652
|
+
ny, nx = data.shape
|
|
653
|
+
else:
|
|
654
|
+
ny, nx = data.shape[-2:]
|
|
655
|
+
|
|
656
|
+
# Create a new array for the centered image
|
|
657
|
+
new_data = np.zeros_like(data)
|
|
658
|
+
center_x = nx // 2
|
|
659
|
+
center_y = ny // 2
|
|
660
|
+
|
|
661
|
+
# Calculate offsets
|
|
662
|
+
offset_x = center_x - crpix1
|
|
663
|
+
offset_y = center_y - crpix2
|
|
664
|
+
|
|
665
|
+
print(f"Original image dimensions: {data.shape}")
|
|
666
|
+
print(f"Original reference pixel: CRPIX1={crpix1}, CRPIX2={crpix2}")
|
|
667
|
+
print(
|
|
668
|
+
f"Shifting data by ({offset_x}, {offset_y}) pixels to visually center"
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
# Shift the data
|
|
672
|
+
if len(data.shape) == 2:
|
|
673
|
+
# Handle 2D image
|
|
674
|
+
for y in range(ny):
|
|
675
|
+
for x in range(nx):
|
|
676
|
+
new_y = y - offset_y
|
|
677
|
+
new_x = x - offset_x
|
|
678
|
+
if 0 <= new_y < ny and 0 <= new_x < nx:
|
|
679
|
+
new_data[y, x] = data[new_y, new_x]
|
|
680
|
+
else:
|
|
681
|
+
# Handle higher dimensions
|
|
682
|
+
for y in range(ny):
|
|
683
|
+
for x in range(nx):
|
|
684
|
+
new_y = y - offset_y
|
|
685
|
+
new_x = x - offset_x
|
|
686
|
+
if 0 <= new_y < ny and 0 <= new_x < nx:
|
|
687
|
+
new_data[..., y, x] = data[..., new_y, new_x]
|
|
688
|
+
|
|
689
|
+
# Update the header
|
|
690
|
+
header["CRPIX1"] = center_x
|
|
691
|
+
header["CRPIX2"] = center_y
|
|
692
|
+
|
|
693
|
+
# Save the centered image
|
|
694
|
+
hdul[0].data = new_data
|
|
695
|
+
hdul.writeto(output_file, overwrite=True)
|
|
696
|
+
hdul.close()
|
|
697
|
+
|
|
698
|
+
print(f"Created a visually centered image: {output_file}")
|
|
699
|
+
print(f"New reference pixel: CRPIX1={center_x}, CRPIX2={center_y}")
|
|
700
|
+
return True
|
|
701
|
+
|
|
702
|
+
except Exception as e:
|
|
703
|
+
print(f"Error creating visually centered image: {e}")
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
def shift_phasecenter_ms(self, msname, ra, dec):
|
|
707
|
+
"""
|
|
708
|
+
Apply phase shift to a measurement set
|
|
709
|
+
|
|
710
|
+
Parameters
|
|
711
|
+
----------
|
|
712
|
+
msname : str
|
|
713
|
+
Name of the measurement set
|
|
714
|
+
ra : float
|
|
715
|
+
RA of the new phase center in degrees
|
|
716
|
+
dec : float
|
|
717
|
+
DEC of the new phase center in degrees
|
|
718
|
+
|
|
719
|
+
Returns
|
|
720
|
+
-------
|
|
721
|
+
int
|
|
722
|
+
Success code 0: Successfully shifted, 1: Error in shifting
|
|
723
|
+
"""
|
|
724
|
+
try:
|
|
725
|
+
# Create a table tool
|
|
726
|
+
t = table()
|
|
727
|
+
|
|
728
|
+
# Get original phase center from MS
|
|
729
|
+
t.open(f"{msname}/FIELD")
|
|
730
|
+
orig_dir = t.getcol("PHASE_DIR")
|
|
731
|
+
# Convert the new coordinates to radians
|
|
732
|
+
new_ra_rad = np.deg2rad(ra)
|
|
733
|
+
new_dec_rad = np.deg2rad(dec)
|
|
734
|
+
|
|
735
|
+
# Format for display
|
|
736
|
+
ra_hms = self.deg2hms(ra)
|
|
737
|
+
dec_dms = self.deg2dms(dec)
|
|
738
|
+
orig_ra_deg = np.degrees(orig_dir[0][0][0])
|
|
739
|
+
orig_dec_deg = np.degrees(orig_dir[0][0][1])
|
|
740
|
+
orig_ra_hms = self.deg2hms(orig_ra_deg)
|
|
741
|
+
orig_dec_dms = self.deg2dms(orig_dec_deg)
|
|
742
|
+
|
|
743
|
+
print(
|
|
744
|
+
f"Original phase center: RA = {orig_ra_hms} ({orig_ra_deg} deg), DEC = {orig_dec_dms} ({orig_dec_deg} deg)"
|
|
745
|
+
)
|
|
746
|
+
print(
|
|
747
|
+
f"New phase center: RA = {ra_hms} ({ra} deg), DEC = {dec_dms} ({dec} deg)"
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
# Update the phase center
|
|
751
|
+
for i in range(orig_dir.shape[0]):
|
|
752
|
+
orig_dir[i][0][0] = new_ra_rad
|
|
753
|
+
orig_dir[i][0][1] = new_dec_rad
|
|
754
|
+
|
|
755
|
+
# Write back to the table
|
|
756
|
+
t.putcol("PHASE_DIR", orig_dir)
|
|
757
|
+
t.close()
|
|
758
|
+
|
|
759
|
+
# Update UVW coordinates to match the new phase center
|
|
760
|
+
fixvis(
|
|
761
|
+
vis=msname,
|
|
762
|
+
outputvis="",
|
|
763
|
+
phasecenter=f"J2000 {ra_hms} {dec_dms}",
|
|
764
|
+
datacolumn="all",
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
print(f"Phase center of MS successfully updated")
|
|
768
|
+
return 0
|
|
769
|
+
except Exception as e:
|
|
770
|
+
print(f"Error shifting phase center in MS: {e}")
|
|
771
|
+
return 1
|
|
772
|
+
|
|
773
|
+
def apply_shift_to_multiple_fits(
|
|
774
|
+
self,
|
|
775
|
+
ra,
|
|
776
|
+
dec,
|
|
777
|
+
input_pattern,
|
|
778
|
+
output_pattern=None,
|
|
779
|
+
stokes="I",
|
|
780
|
+
visual_center=False,
|
|
781
|
+
use_multiprocessing=True,
|
|
782
|
+
max_processes=None,
|
|
783
|
+
):
|
|
784
|
+
"""
|
|
785
|
+
Apply the same phase shift to multiple FITS files
|
|
786
|
+
|
|
787
|
+
Parameters
|
|
788
|
+
----------
|
|
789
|
+
ra : float
|
|
790
|
+
RA of the solar center in degrees
|
|
791
|
+
dec : float
|
|
792
|
+
DEC of the solar center in degrees
|
|
793
|
+
input_pattern : str
|
|
794
|
+
Glob pattern for input files (e.g., "path/to/*.fits")
|
|
795
|
+
output_pattern : str, optional
|
|
796
|
+
Pattern for output files (if None, input files will be modified)
|
|
797
|
+
stokes : str
|
|
798
|
+
Stokes parameter to use
|
|
799
|
+
visual_center : bool
|
|
800
|
+
Whether to also create visually centered images
|
|
801
|
+
use_multiprocessing : bool
|
|
802
|
+
Whether to use multiprocessing for batch processing
|
|
803
|
+
max_processes : int, optional
|
|
804
|
+
Maximum number of processes to use (defaults to number of CPU cores)
|
|
805
|
+
|
|
806
|
+
Returns
|
|
807
|
+
-------
|
|
808
|
+
list
|
|
809
|
+
List of [success_count, total_count]
|
|
810
|
+
"""
|
|
811
|
+
try:
|
|
812
|
+
# Clean up any leftover temporary files first
|
|
813
|
+
input_dir = os.path.dirname(input_pattern)
|
|
814
|
+
if input_dir and os.path.exists(input_dir):
|
|
815
|
+
print(f"Cleaning up any leftover temporary files in {input_dir}")
|
|
816
|
+
os.system(
|
|
817
|
+
f"rm -rf {input_dir}/I_model_* {input_dir}/wcs_model_*.fits {input_dir}/I.model {input_dir}/wcs_model.fits"
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
# Get list of files matching the pattern
|
|
821
|
+
files = glob.glob(input_pattern)
|
|
822
|
+
if not files:
|
|
823
|
+
print(f"No files found matching pattern: {input_pattern}")
|
|
824
|
+
return [0, 0]
|
|
825
|
+
|
|
826
|
+
total_count = len(files)
|
|
827
|
+
print(f"Found {total_count} files matching pattern: {input_pattern}")
|
|
828
|
+
print(f"Applying phase shift: RA = {ra} deg, DEC = {dec} deg")
|
|
829
|
+
|
|
830
|
+
# If only one file or multiprocessing is disabled, use the single-processing approach
|
|
831
|
+
if total_count == 1 or not use_multiprocessing:
|
|
832
|
+
success_count = 0
|
|
833
|
+
for i, file in enumerate(files):
|
|
834
|
+
print(f"Processing file {i+1}/{total_count}: {file}")
|
|
835
|
+
|
|
836
|
+
# Determine output file
|
|
837
|
+
if output_pattern:
|
|
838
|
+
file_basename = os.path.basename(file)
|
|
839
|
+
file_name, file_ext = os.path.splitext(file_basename)
|
|
840
|
+
|
|
841
|
+
# Replace wildcards in the output pattern
|
|
842
|
+
output_file = output_pattern.replace("*", file_name)
|
|
843
|
+
if not output_file.endswith(file_ext):
|
|
844
|
+
output_file += file_ext
|
|
845
|
+
|
|
846
|
+
# Make a copy of the input file
|
|
847
|
+
if os.path.isdir(file):
|
|
848
|
+
os.system(f"rm -rf {output_file}")
|
|
849
|
+
os.system(f"cp -r {file} {output_file}")
|
|
850
|
+
target = output_file
|
|
851
|
+
else:
|
|
852
|
+
shutil.copy(file, output_file)
|
|
853
|
+
target = output_file
|
|
854
|
+
else:
|
|
855
|
+
target = file
|
|
856
|
+
|
|
857
|
+
# Apply the phase shift
|
|
858
|
+
result = self.shift_phasecenter(
|
|
859
|
+
imagename=target, ra=ra, dec=dec, stokes=stokes
|
|
860
|
+
)
|
|
861
|
+
|
|
862
|
+
if result == 0:
|
|
863
|
+
success_count += 1
|
|
864
|
+
|
|
865
|
+
# Create a visually centered image if requested
|
|
866
|
+
if visual_center:
|
|
867
|
+
try:
|
|
868
|
+
# Get the reference pixel values from the shifted image
|
|
869
|
+
header = fits.getheader(target)
|
|
870
|
+
crpix1 = int(header["CRPIX1"])
|
|
871
|
+
crpix2 = int(header["CRPIX2"])
|
|
872
|
+
|
|
873
|
+
# Generate output filename for visually centered image
|
|
874
|
+
visual_output = (
|
|
875
|
+
os.path.splitext(target)[0]
|
|
876
|
+
+ "_centered"
|
|
877
|
+
+ os.path.splitext(target)[1]
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
print(
|
|
881
|
+
f"Creating visually centered image: {visual_output}"
|
|
882
|
+
)
|
|
883
|
+
# Create the visually centered image
|
|
884
|
+
self.visually_center_image(
|
|
885
|
+
target, visual_output, crpix1, crpix2
|
|
886
|
+
)
|
|
887
|
+
print(
|
|
888
|
+
f"Visually centered image created: {visual_output}"
|
|
889
|
+
)
|
|
890
|
+
except Exception as e:
|
|
891
|
+
print(
|
|
892
|
+
f"Error creating visually centered image for {target}: {e}"
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
print(f"Successfully processed {success_count}/{total_count} files")
|
|
896
|
+
|
|
897
|
+
# Clean up any temporary files
|
|
898
|
+
if input_dir and os.path.exists(input_dir):
|
|
899
|
+
os.system(
|
|
900
|
+
f"rm -rf {input_dir}/I_model_* {input_dir}/wcs_model_*.fits {input_dir}/I.model {input_dir}/wcs_model.fits"
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
return [success_count, total_count]
|
|
904
|
+
|
|
905
|
+
# Use multiprocessing for batch processing
|
|
906
|
+
else:
|
|
907
|
+
# Determine number of processes to use
|
|
908
|
+
if max_processes is None:
|
|
909
|
+
max_processes = min(multiprocessing.cpu_count(), total_count)
|
|
910
|
+
else:
|
|
911
|
+
max_processes = min(
|
|
912
|
+
max_processes, multiprocessing.cpu_count(), total_count
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
print(f"Using multiprocessing with {max_processes} processes")
|
|
916
|
+
|
|
917
|
+
# Prepare the arguments for each file
|
|
918
|
+
file_args = [
|
|
919
|
+
(file, ra, dec, stokes, output_pattern, visual_center)
|
|
920
|
+
for file in files
|
|
921
|
+
]
|
|
922
|
+
|
|
923
|
+
# Create a process pool and process the files
|
|
924
|
+
with Pool(processes=max_processes) as pool:
|
|
925
|
+
results = pool.map(self.process_single_file, file_args)
|
|
926
|
+
|
|
927
|
+
# Count successful operations
|
|
928
|
+
success_count = sum(1 for success, _, _ in results if success)
|
|
929
|
+
|
|
930
|
+
# Print any errors or warnings
|
|
931
|
+
for success, file, message in results:
|
|
932
|
+
if message:
|
|
933
|
+
print(f"{file}: {message}")
|
|
934
|
+
|
|
935
|
+
print(f"Successfully processed {success_count}/{total_count} files")
|
|
936
|
+
|
|
937
|
+
# Final cleanup to ensure all temporary files are removed
|
|
938
|
+
if input_dir and os.path.exists(input_dir):
|
|
939
|
+
print(f"Final cleanup of temporary files in {input_dir}")
|
|
940
|
+
os.system(
|
|
941
|
+
f"rm -rf {input_dir}/I_model_* {input_dir}/wcs_model_*.fits {input_dir}/I.model {input_dir}/wcs_model.fits"
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
return [success_count, total_count]
|
|
945
|
+
|
|
946
|
+
except Exception as e:
|
|
947
|
+
print(f"Error in applying shift to multiple files: {e}")
|
|
948
|
+
|
|
949
|
+
# Cleanup even if an error occurred
|
|
950
|
+
if "input_dir" in locals() and input_dir and os.path.exists(input_dir):
|
|
951
|
+
print(f"Cleaning up temporary files after error in {input_dir}")
|
|
952
|
+
os.system(
|
|
953
|
+
f"rm -rf {input_dir}/I_model_* {input_dir}/wcs_model_*.fits {input_dir}/I.model {input_dir}/wcs_model.fits"
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
try:
|
|
957
|
+
return [0, total_count]
|
|
958
|
+
except:
|
|
959
|
+
return [0, 0]
|
|
960
|
+
|
|
961
|
+
def process_single_file(self, file_info):
|
|
962
|
+
"""
|
|
963
|
+
Process a single file for multiprocessing in batch mode
|
|
964
|
+
|
|
965
|
+
Parameters
|
|
966
|
+
----------
|
|
967
|
+
file_info : tuple
|
|
968
|
+
Tuple containing (file_path, ra, dec, stokes, output_pattern, visual_center)
|
|
969
|
+
|
|
970
|
+
Returns
|
|
971
|
+
-------
|
|
972
|
+
tuple
|
|
973
|
+
Tuple containing (success, file_path, error_message)
|
|
974
|
+
"""
|
|
975
|
+
file, ra, dec, stokes, output_pattern, visual_center = file_info
|
|
976
|
+
|
|
977
|
+
try:
|
|
978
|
+
# Use process ID and file identifier to create a unique identifier for this task
|
|
979
|
+
process_id = int(hashlib.md5(file.encode()).hexdigest(), 16) % 10000
|
|
980
|
+
|
|
981
|
+
# Determine output file
|
|
982
|
+
if output_pattern:
|
|
983
|
+
file_basename = os.path.basename(file)
|
|
984
|
+
file_name, file_ext = os.path.splitext(file_basename)
|
|
985
|
+
|
|
986
|
+
# Replace wildcards in the output pattern
|
|
987
|
+
output_file = output_pattern.replace("*", file_name)
|
|
988
|
+
if not output_file.endswith(file_ext):
|
|
989
|
+
output_file += file_ext
|
|
990
|
+
|
|
991
|
+
# Make a copy of the input file
|
|
992
|
+
if os.path.isdir(file):
|
|
993
|
+
os.system(f"rm -rf {output_file}")
|
|
994
|
+
os.system(f"cp -r {file} {output_file}")
|
|
995
|
+
target = output_file
|
|
996
|
+
else:
|
|
997
|
+
shutil.copy(file, output_file)
|
|
998
|
+
target = output_file
|
|
999
|
+
else:
|
|
1000
|
+
target = file
|
|
1001
|
+
|
|
1002
|
+
# Apply the phase shift with the process_id
|
|
1003
|
+
result = self.shift_phasecenter(
|
|
1004
|
+
imagename=target, ra=ra, dec=dec, stokes=stokes, process_id=process_id
|
|
1005
|
+
)
|
|
1006
|
+
|
|
1007
|
+
if result == 0:
|
|
1008
|
+
# Create a visually centered image if requested
|
|
1009
|
+
if visual_center:
|
|
1010
|
+
try:
|
|
1011
|
+
# Get the reference pixel values from the shifted image
|
|
1012
|
+
header = fits.getheader(target)
|
|
1013
|
+
crpix1 = int(header["CRPIX1"])
|
|
1014
|
+
crpix2 = int(header["CRPIX2"])
|
|
1015
|
+
|
|
1016
|
+
# Generate output filename for visually centered image
|
|
1017
|
+
visual_output = (
|
|
1018
|
+
os.path.splitext(target)[0]
|
|
1019
|
+
+ "_centered"
|
|
1020
|
+
+ os.path.splitext(target)[1]
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
# Create the visually centered image
|
|
1024
|
+
self.visually_center_image(
|
|
1025
|
+
target, visual_output, crpix1, crpix2
|
|
1026
|
+
)
|
|
1027
|
+
return (True, file, None)
|
|
1028
|
+
except Exception as e:
|
|
1029
|
+
return (
|
|
1030
|
+
True,
|
|
1031
|
+
file,
|
|
1032
|
+
f"Warning: Error creating visually centered image: {str(e)}",
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
return (True, file, None)
|
|
1036
|
+
else:
|
|
1037
|
+
return (False, file, f"Error applying phase shift (code: {result})")
|
|
1038
|
+
|
|
1039
|
+
except Exception as e:
|
|
1040
|
+
return (False, file, f"Error: {str(e)}")
|
|
1041
|
+
|
|
1042
|
+
|
|
1043
|
+
def main():
|
|
1044
|
+
"""
|
|
1045
|
+
Main function to run from command line
|
|
1046
|
+
"""
|
|
1047
|
+
parser = argparse.ArgumentParser(
|
|
1048
|
+
description="Calculate and apply phase shifts to solar images"
|
|
1049
|
+
)
|
|
1050
|
+
parser.add_argument(
|
|
1051
|
+
"--imagename",
|
|
1052
|
+
type=str,
|
|
1053
|
+
required=False,
|
|
1054
|
+
help="Input image name (CASA or FITS format) for calculating phase shift",
|
|
1055
|
+
)
|
|
1056
|
+
parser.add_argument(
|
|
1057
|
+
"--msname", type=str, default=None, help="Measurement set name (optional)"
|
|
1058
|
+
)
|
|
1059
|
+
parser.add_argument(
|
|
1060
|
+
"--cellsize",
|
|
1061
|
+
type=float,
|
|
1062
|
+
default=None,
|
|
1063
|
+
help="Cell size in arcsec (optional, will be read from image if not provided)",
|
|
1064
|
+
)
|
|
1065
|
+
parser.add_argument(
|
|
1066
|
+
"--imsize",
|
|
1067
|
+
type=int,
|
|
1068
|
+
default=None,
|
|
1069
|
+
help="Image size in pixels (optional, will be read from image if not provided)",
|
|
1070
|
+
)
|
|
1071
|
+
parser.add_argument(
|
|
1072
|
+
"--stokes", type=str, default="I", help="Stokes parameter to use (default: I)"
|
|
1073
|
+
)
|
|
1074
|
+
parser.add_argument(
|
|
1075
|
+
"--fit_gaussian",
|
|
1076
|
+
action="store_true",
|
|
1077
|
+
default=False,
|
|
1078
|
+
help="Use Gaussian fitting for solar center",
|
|
1079
|
+
)
|
|
1080
|
+
parser.add_argument(
|
|
1081
|
+
"--sigma",
|
|
1082
|
+
type=float,
|
|
1083
|
+
default=10,
|
|
1084
|
+
help="Sigma threshold for center-of-mass calculation (default: 10)",
|
|
1085
|
+
)
|
|
1086
|
+
parser.add_argument(
|
|
1087
|
+
"--apply_shift",
|
|
1088
|
+
action="store_true",
|
|
1089
|
+
default=True,
|
|
1090
|
+
help="Apply the calculated shift to the image",
|
|
1091
|
+
)
|
|
1092
|
+
parser.add_argument(
|
|
1093
|
+
"--output",
|
|
1094
|
+
type=str,
|
|
1095
|
+
default=None,
|
|
1096
|
+
help="Output image name (if not specified, input image will be modified)",
|
|
1097
|
+
)
|
|
1098
|
+
parser.add_argument(
|
|
1099
|
+
"--visual_center",
|
|
1100
|
+
action="store_true",
|
|
1101
|
+
default=False,
|
|
1102
|
+
help="Create a visually centered image (moves pixel data)",
|
|
1103
|
+
)
|
|
1104
|
+
|
|
1105
|
+
# New arguments for batch processing
|
|
1106
|
+
parser.add_argument(
|
|
1107
|
+
"--ra",
|
|
1108
|
+
type=float,
|
|
1109
|
+
default=None,
|
|
1110
|
+
help="RA in degrees (if provided, skips calculation)",
|
|
1111
|
+
)
|
|
1112
|
+
parser.add_argument(
|
|
1113
|
+
"--dec",
|
|
1114
|
+
type=float,
|
|
1115
|
+
default=None,
|
|
1116
|
+
help="DEC in degrees (if provided, skips calculation)",
|
|
1117
|
+
)
|
|
1118
|
+
parser.add_argument(
|
|
1119
|
+
"--apply_to_ms",
|
|
1120
|
+
action="store_true",
|
|
1121
|
+
default=False,
|
|
1122
|
+
help="Apply the calculated/provided shift to the MS file",
|
|
1123
|
+
)
|
|
1124
|
+
parser.add_argument(
|
|
1125
|
+
"--input_pattern",
|
|
1126
|
+
type=str,
|
|
1127
|
+
default=None,
|
|
1128
|
+
help="Glob pattern for batch processing multiple files",
|
|
1129
|
+
)
|
|
1130
|
+
parser.add_argument(
|
|
1131
|
+
"--output_pattern",
|
|
1132
|
+
type=str,
|
|
1133
|
+
default=None,
|
|
1134
|
+
help='Output pattern for batch processing (e.g., "/path/to/shifted_*.fits")',
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
args = parser.parse_args()
|
|
1138
|
+
|
|
1139
|
+
# Initialize the object
|
|
1140
|
+
spc = SolarPhaseCenter(
|
|
1141
|
+
msname=args.msname, cellsize=args.cellsize, imsize=args.imsize
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
# Determine phase shift coordinates
|
|
1145
|
+
if args.ra is not None and args.dec is not None:
|
|
1146
|
+
# Use provided coordinates
|
|
1147
|
+
ra = args.ra
|
|
1148
|
+
dec = args.dec
|
|
1149
|
+
needs_shift = True
|
|
1150
|
+
print(f"Using provided coordinates: RA = {ra} deg, DEC = {dec} deg")
|
|
1151
|
+
elif args.imagename:
|
|
1152
|
+
# Calculate from image
|
|
1153
|
+
ra, dec, needs_shift = spc.cal_solar_phaseshift(
|
|
1154
|
+
imagename=args.imagename, fit_gaussian=args.fit_gaussian, sigma=args.sigma
|
|
1155
|
+
)
|
|
1156
|
+
print(f"Calculated solar center: RA = {ra} deg, DEC = {dec} deg")
|
|
1157
|
+
print(f"Phase shift needed: {needs_shift}")
|
|
1158
|
+
else:
|
|
1159
|
+
print(
|
|
1160
|
+
"Error: Either provide an image for calculation or specify RA and DEC coordinates"
|
|
1161
|
+
)
|
|
1162
|
+
return
|
|
1163
|
+
|
|
1164
|
+
# Handle MS phase shift
|
|
1165
|
+
if args.apply_to_ms and args.msname:
|
|
1166
|
+
if needs_shift:
|
|
1167
|
+
result = spc.shift_phasecenter_ms(args.msname, ra, dec)
|
|
1168
|
+
if result == 0:
|
|
1169
|
+
print(f"Successfully applied phase shift to MS: {args.msname}")
|
|
1170
|
+
else:
|
|
1171
|
+
print(f"Failed to apply phase shift to MS: {args.msname}")
|
|
1172
|
+
else:
|
|
1173
|
+
print("No phase shift needed for the MS")
|
|
1174
|
+
|
|
1175
|
+
# Handle batch processing of FITS files
|
|
1176
|
+
if args.input_pattern:
|
|
1177
|
+
if needs_shift:
|
|
1178
|
+
success_count, total_count = spc.apply_shift_to_multiple_fits(
|
|
1179
|
+
ra,
|
|
1180
|
+
dec,
|
|
1181
|
+
args.input_pattern,
|
|
1182
|
+
args.output_pattern,
|
|
1183
|
+
args.stokes,
|
|
1184
|
+
args.visual_center,
|
|
1185
|
+
)
|
|
1186
|
+
if success_count == total_count:
|
|
1187
|
+
print(f"Successfully applied phase shift to all {total_count} files")
|
|
1188
|
+
else:
|
|
1189
|
+
print(
|
|
1190
|
+
f"Applied phase shift to {success_count} out of {total_count} files"
|
|
1191
|
+
)
|
|
1192
|
+
else:
|
|
1193
|
+
print("No phase shift needed for the image files")
|
|
1194
|
+
|
|
1195
|
+
# Handle single image (original functionality)
|
|
1196
|
+
elif args.imagename and args.apply_shift and needs_shift:
|
|
1197
|
+
if args.output:
|
|
1198
|
+
# Make a copy of the image
|
|
1199
|
+
if os.path.isdir(args.imagename):
|
|
1200
|
+
os.system(f"rm -rf {args.output}")
|
|
1201
|
+
os.system(f"cp -r {args.imagename} {args.output}")
|
|
1202
|
+
target = args.output
|
|
1203
|
+
else:
|
|
1204
|
+
import shutil
|
|
1205
|
+
|
|
1206
|
+
shutil.copy(args.imagename, args.output)
|
|
1207
|
+
target = args.output
|
|
1208
|
+
else:
|
|
1209
|
+
target = args.imagename
|
|
1210
|
+
|
|
1211
|
+
result = spc.shift_phasecenter(
|
|
1212
|
+
imagename=target, ra=ra, dec=dec, stokes=args.stokes
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
if result == 0:
|
|
1216
|
+
print("Phase shift successfully applied")
|
|
1217
|
+
|
|
1218
|
+
# Create a visually centered image if requested
|
|
1219
|
+
if args.visual_center and args.output:
|
|
1220
|
+
# Get the reference pixel values from the shifted image
|
|
1221
|
+
header = fits.getheader(target)
|
|
1222
|
+
crpix1 = int(header["CRPIX1"])
|
|
1223
|
+
crpix2 = int(header["CRPIX2"])
|
|
1224
|
+
|
|
1225
|
+
# Generate output filename for visually centered image
|
|
1226
|
+
visual_output = (
|
|
1227
|
+
os.path.splitext(args.output)[0]
|
|
1228
|
+
+ "_centered"
|
|
1229
|
+
+ os.path.splitext(args.output)[1]
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
# Create the visually centered image
|
|
1233
|
+
spc.visually_center_image(target, visual_output, crpix1, crpix2)
|
|
1234
|
+
|
|
1235
|
+
elif result == 1:
|
|
1236
|
+
print("Phase shift not needed")
|
|
1237
|
+
else:
|
|
1238
|
+
print("Error applying phase shift")
|
|
1239
|
+
elif args.imagename and args.output and not needs_shift:
|
|
1240
|
+
# User requested output file but no shift needed
|
|
1241
|
+
if os.path.isdir(args.imagename):
|
|
1242
|
+
os.system(f"rm -rf {args.output}")
|
|
1243
|
+
os.system(f"cp -r {args.imagename} {args.output}")
|
|
1244
|
+
else:
|
|
1245
|
+
import shutil
|
|
1246
|
+
|
|
1247
|
+
shutil.copy(args.imagename, args.output)
|
|
1248
|
+
print(f"No phase shift needed. Copied original image to {args.output}")
|
|
1249
|
+
|
|
1250
|
+
# If visual centering was requested but no shift needed, still create it
|
|
1251
|
+
if args.visual_center:
|
|
1252
|
+
# Need to get current reference pixels
|
|
1253
|
+
header = fits.getheader(args.output)
|
|
1254
|
+
crpix1 = int(header["CRPIX1"])
|
|
1255
|
+
crpix2 = int(header["CRPIX2"])
|
|
1256
|
+
|
|
1257
|
+
# Generate output filename for visually centered image
|
|
1258
|
+
visual_output = (
|
|
1259
|
+
os.path.splitext(args.output)[0]
|
|
1260
|
+
+ "_centered"
|
|
1261
|
+
+ os.path.splitext(args.output)[1]
|
|
1262
|
+
)
|
|
1263
|
+
|
|
1264
|
+
# Create the visually centered image
|
|
1265
|
+
spc.visually_center_image(args.output, visual_output, crpix1, crpix2)
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
if __name__ == "__main__":
|
|
1269
|
+
main()
|