solarviewer 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. solar_radio_image_viewer/__init__.py +12 -0
  2. solar_radio_image_viewer/assets/add_tab_default.png +0 -0
  3. solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
  4. solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
  5. solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
  6. solar_radio_image_viewer/assets/browse.png +0 -0
  7. solar_radio_image_viewer/assets/browse_light.png +0 -0
  8. solar_radio_image_viewer/assets/close_tab_default.png +0 -0
  9. solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
  10. solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
  11. solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
  12. solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
  13. solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
  14. solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
  15. solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
  16. solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
  17. solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
  18. solar_radio_image_viewer/assets/profile.png +0 -0
  19. solar_radio_image_viewer/assets/profile_light.png +0 -0
  20. solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
  21. solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
  22. solar_radio_image_viewer/assets/reset.png +0 -0
  23. solar_radio_image_viewer/assets/reset_light.png +0 -0
  24. solar_radio_image_viewer/assets/ruler.png +0 -0
  25. solar_radio_image_viewer/assets/ruler_light.png +0 -0
  26. solar_radio_image_viewer/assets/search.png +0 -0
  27. solar_radio_image_viewer/assets/search_light.png +0 -0
  28. solar_radio_image_viewer/assets/settings.png +0 -0
  29. solar_radio_image_viewer/assets/settings_light.png +0 -0
  30. solar_radio_image_viewer/assets/splash.fits +0 -0
  31. solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
  32. solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
  33. solar_radio_image_viewer/assets/zoom_in.png +0 -0
  34. solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
  35. solar_radio_image_viewer/assets/zoom_out.png +0 -0
  36. solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
  37. solar_radio_image_viewer/create_video.py +1345 -0
  38. solar_radio_image_viewer/dialogs.py +2665 -0
  39. solar_radio_image_viewer/from_simpl/__init__.py +184 -0
  40. solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
  41. solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
  42. solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
  43. solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
  44. solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
  45. solar_radio_image_viewer/from_simpl/utils.py +984 -0
  46. solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
  47. solar_radio_image_viewer/helioprojective.py +1916 -0
  48. solar_radio_image_viewer/helioprojective_viewer.py +817 -0
  49. solar_radio_image_viewer/helioviewer_browser.py +1514 -0
  50. solar_radio_image_viewer/main.py +148 -0
  51. solar_radio_image_viewer/move_phasecenter.py +1269 -0
  52. solar_radio_image_viewer/napari_viewer.py +368 -0
  53. solar_radio_image_viewer/noaa_events/__init__.py +32 -0
  54. solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
  55. solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
  56. solar_radio_image_viewer/norms.py +293 -0
  57. solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
  58. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
  59. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
  60. solar_radio_image_viewer/searchable_combobox.py +220 -0
  61. solar_radio_image_viewer/solar_context/__init__.py +41 -0
  62. solar_radio_image_viewer/solar_context/active_regions.py +371 -0
  63. solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
  64. solar_radio_image_viewer/solar_context/context_images.py +297 -0
  65. solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
  66. solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
  67. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
  68. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
  69. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
  70. solar_radio_image_viewer/styles.py +643 -0
  71. solar_radio_image_viewer/utils/__init__.py +32 -0
  72. solar_radio_image_viewer/utils/rate_limiter.py +255 -0
  73. solar_radio_image_viewer/utils.py +952 -0
  74. solar_radio_image_viewer/video_dialog.py +2629 -0
  75. solar_radio_image_viewer/video_utils.py +656 -0
  76. solar_radio_image_viewer/viewer.py +11174 -0
  77. solarviewer-1.0.2.dist-info/METADATA +343 -0
  78. solarviewer-1.0.2.dist-info/RECORD +82 -0
  79. solarviewer-1.0.2.dist-info/WHEEL +5 -0
  80. solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
  81. solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
  82. solarviewer-1.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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()