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,332 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic Spectra Dialog
|
|
3
|
+
|
|
4
|
+
GUI dialog for creating dynamic spectra from MS files.
|
|
5
|
+
Uses the make_dynamic_spectra.py processing functions.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from PyQt5.QtWidgets import (
|
|
11
|
+
QDialog, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QLineEdit,
|
|
12
|
+
QPushButton, QFileDialog, QSpinBox, QDoubleSpinBox, QGroupBox,
|
|
13
|
+
QProgressBar, QTextEdit, QCheckBox, QMessageBox, QApplication
|
|
14
|
+
)
|
|
15
|
+
from PyQt5.QtCore import Qt, QThread, pyqtSignal
|
|
16
|
+
from PyQt5.QtGui import QFont
|
|
17
|
+
|
|
18
|
+
from .simpl_theme import apply_theme, get_theme_from_args
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProcessingThread(QThread):
|
|
22
|
+
"""Thread for running dynamic spectra processing in isolated subprocess."""
|
|
23
|
+
progress = pyqtSignal(str)
|
|
24
|
+
finished = pyqtSignal(bool, str)
|
|
25
|
+
|
|
26
|
+
def __init__(self, params):
|
|
27
|
+
super().__init__()
|
|
28
|
+
self.params = params
|
|
29
|
+
|
|
30
|
+
def run(self):
|
|
31
|
+
"""Run make_dynamic_spectra in a separate subprocess to avoid casacore/Qt conflicts."""
|
|
32
|
+
import subprocess
|
|
33
|
+
import json
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
# Build command line arguments for make_dynamic_spectra.py
|
|
37
|
+
script_path = os.path.join(os.path.dirname(__file__), 'make_dynamic_spectra.py')
|
|
38
|
+
|
|
39
|
+
cmd = [
|
|
40
|
+
sys.executable, script_path,
|
|
41
|
+
'--indir', self.params['indir'],
|
|
42
|
+
'--outfits', self.params['outfits'],
|
|
43
|
+
'--binwidth', str(self.params['binwidth']),
|
|
44
|
+
'--ncpu', str(self.params['ncpu']),
|
|
45
|
+
'--uvmin', str(self.params['uvmin']),
|
|
46
|
+
'--uvmax', str(self.params['uvmax']),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Add optional frequency range
|
|
50
|
+
if self.params.get('startfreq') and self.params.get('endfreq'):
|
|
51
|
+
cmd.extend(['--startfreq', str(self.params['startfreq'])])
|
|
52
|
+
cmd.extend(['--endfreq', str(self.params['endfreq'])])
|
|
53
|
+
|
|
54
|
+
# Add plot options
|
|
55
|
+
if self.params.get('saveplot'):
|
|
56
|
+
cmd.append('--saveplot')
|
|
57
|
+
if self.params.get('plot_filename'):
|
|
58
|
+
cmd.extend(['--plotfile', self.params['plot_filename']])
|
|
59
|
+
|
|
60
|
+
self.progress.emit(f"Running: {os.path.basename(script_path)}")
|
|
61
|
+
self.progress.emit(f"Input: {self.params['indir']}")
|
|
62
|
+
self.progress.emit(f"Output: {self.params['outfits']}")
|
|
63
|
+
self.progress.emit("")
|
|
64
|
+
|
|
65
|
+
# Run in subprocess (completely isolates casacore from Qt)
|
|
66
|
+
process = subprocess.Popen(
|
|
67
|
+
cmd,
|
|
68
|
+
stdout=subprocess.PIPE,
|
|
69
|
+
stderr=subprocess.STDOUT,
|
|
70
|
+
text=True,
|
|
71
|
+
bufsize=1
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Stream output in real-time
|
|
75
|
+
for line in iter(process.stdout.readline, ''):
|
|
76
|
+
if line:
|
|
77
|
+
self.progress.emit(line.rstrip())
|
|
78
|
+
|
|
79
|
+
process.wait()
|
|
80
|
+
|
|
81
|
+
if process.returncode == 0:
|
|
82
|
+
self.finished.emit(True, f"Success! Created: {self.params['outfits']}")
|
|
83
|
+
else:
|
|
84
|
+
self.finished.emit(False, f"Processing failed with exit code {process.returncode}")
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
self.finished.emit(False, f"Error: {str(e)}")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class DynamicSpectraDialog(QDialog):
|
|
91
|
+
"""Dialog for creating dynamic spectra from MS files."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, parent=None, theme="dark"):
|
|
94
|
+
super().__init__(parent)
|
|
95
|
+
self.theme = theme
|
|
96
|
+
self.processing_thread = None
|
|
97
|
+
self._setup_ui()
|
|
98
|
+
|
|
99
|
+
def _setup_ui(self):
|
|
100
|
+
self.setWindowTitle("Create Dynamic Spectra")
|
|
101
|
+
self.setMinimumSize(600, 500)
|
|
102
|
+
|
|
103
|
+
layout = QVBoxLayout(self)
|
|
104
|
+
layout.setSpacing(12)
|
|
105
|
+
|
|
106
|
+
# Input/Output Group
|
|
107
|
+
io_group = QGroupBox("Input / Output")
|
|
108
|
+
io_layout = QGridLayout(io_group)
|
|
109
|
+
|
|
110
|
+
# Input directory
|
|
111
|
+
io_layout.addWidget(QLabel("MS Directory:"), 0, 0)
|
|
112
|
+
self.input_edit = QLineEdit()
|
|
113
|
+
self.input_edit.setPlaceholderText("Directory containing MS files...")
|
|
114
|
+
io_layout.addWidget(self.input_edit, 0, 1)
|
|
115
|
+
self.input_browse_btn = QPushButton("Browse...")
|
|
116
|
+
self.input_browse_btn.clicked.connect(self._browse_input)
|
|
117
|
+
io_layout.addWidget(self.input_browse_btn, 0, 2)
|
|
118
|
+
|
|
119
|
+
# Output FITS file
|
|
120
|
+
io_layout.addWidget(QLabel("Output FITS:"), 1, 0)
|
|
121
|
+
self.output_edit = QLineEdit()
|
|
122
|
+
self.output_edit.setPlaceholderText("Output FITS file path...")
|
|
123
|
+
self.output_edit.setText("dynamic_spectrum.fits")
|
|
124
|
+
io_layout.addWidget(self.output_edit, 1, 1)
|
|
125
|
+
self.output_browse_btn = QPushButton("Browse...")
|
|
126
|
+
self.output_browse_btn.clicked.connect(self._browse_output)
|
|
127
|
+
io_layout.addWidget(self.output_browse_btn, 1, 2)
|
|
128
|
+
|
|
129
|
+
layout.addWidget(io_group)
|
|
130
|
+
|
|
131
|
+
# Parameters Group
|
|
132
|
+
params_group = QGroupBox("Processing Parameters")
|
|
133
|
+
params_layout = QGridLayout(params_group)
|
|
134
|
+
|
|
135
|
+
# Time binwidth
|
|
136
|
+
params_layout.addWidget(QLabel("Time Bin Width (s):"), 0, 0)
|
|
137
|
+
self.binwidth_spin = QDoubleSpinBox()
|
|
138
|
+
self.binwidth_spin.setRange(0.1, 60.0)
|
|
139
|
+
self.binwidth_spin.setValue(1.0)
|
|
140
|
+
self.binwidth_spin.setDecimals(1)
|
|
141
|
+
params_layout.addWidget(self.binwidth_spin, 0, 1)
|
|
142
|
+
|
|
143
|
+
# Number of CPUs
|
|
144
|
+
params_layout.addWidget(QLabel("CPU Cores:"), 0, 2)
|
|
145
|
+
self.ncpu_spin = QSpinBox()
|
|
146
|
+
self.ncpu_spin.setRange(1, os.cpu_count() or 8)
|
|
147
|
+
self.ncpu_spin.setValue(min(10, os.cpu_count() or 8))
|
|
148
|
+
params_layout.addWidget(self.ncpu_spin, 0, 3)
|
|
149
|
+
|
|
150
|
+
# UV range
|
|
151
|
+
params_layout.addWidget(QLabel("UV Min (λ):"), 1, 0)
|
|
152
|
+
self.uvmin_spin = QDoubleSpinBox()
|
|
153
|
+
self.uvmin_spin.setRange(0, 10000)
|
|
154
|
+
self.uvmin_spin.setValue(130.0)
|
|
155
|
+
params_layout.addWidget(self.uvmin_spin, 1, 1)
|
|
156
|
+
|
|
157
|
+
params_layout.addWidget(QLabel("UV Max (λ):"), 1, 2)
|
|
158
|
+
self.uvmax_spin = QDoubleSpinBox()
|
|
159
|
+
self.uvmax_spin.setRange(0, 10000)
|
|
160
|
+
self.uvmax_spin.setValue(500.0)
|
|
161
|
+
params_layout.addWidget(self.uvmax_spin, 1, 3)
|
|
162
|
+
|
|
163
|
+
# Frequency range (optional)
|
|
164
|
+
params_layout.addWidget(QLabel("Start Freq (MHz):"), 2, 0)
|
|
165
|
+
self.startfreq_spin = QDoubleSpinBox()
|
|
166
|
+
self.startfreq_spin.setRange(0, 1000)
|
|
167
|
+
self.startfreq_spin.setValue(0)
|
|
168
|
+
self.startfreq_spin.setSpecialValueText("Auto")
|
|
169
|
+
params_layout.addWidget(self.startfreq_spin, 2, 1)
|
|
170
|
+
|
|
171
|
+
params_layout.addWidget(QLabel("End Freq (MHz):"), 2, 2)
|
|
172
|
+
self.endfreq_spin = QDoubleSpinBox()
|
|
173
|
+
self.endfreq_spin.setRange(0, 1000)
|
|
174
|
+
self.endfreq_spin.setValue(0)
|
|
175
|
+
self.endfreq_spin.setSpecialValueText("Auto")
|
|
176
|
+
params_layout.addWidget(self.endfreq_spin, 2, 3)
|
|
177
|
+
|
|
178
|
+
layout.addWidget(params_group)
|
|
179
|
+
|
|
180
|
+
# Options Group
|
|
181
|
+
options_group = QGroupBox("Options")
|
|
182
|
+
options_layout = QHBoxLayout(options_group)
|
|
183
|
+
|
|
184
|
+
self.save_plot_check = QCheckBox("Save Plot")
|
|
185
|
+
self.save_plot_check.setChecked(True)
|
|
186
|
+
options_layout.addWidget(self.save_plot_check)
|
|
187
|
+
|
|
188
|
+
options_layout.addStretch()
|
|
189
|
+
|
|
190
|
+
layout.addWidget(options_group)
|
|
191
|
+
|
|
192
|
+
# Progress section
|
|
193
|
+
progress_group = QGroupBox("Progress")
|
|
194
|
+
progress_layout = QVBoxLayout(progress_group)
|
|
195
|
+
|
|
196
|
+
self.progress_bar = QProgressBar()
|
|
197
|
+
self.progress_bar.setRange(0, 0) # Indeterminate
|
|
198
|
+
self.progress_bar.setVisible(False)
|
|
199
|
+
progress_layout.addWidget(self.progress_bar)
|
|
200
|
+
|
|
201
|
+
self.log_text = QTextEdit()
|
|
202
|
+
self.log_text.setReadOnly(True)
|
|
203
|
+
self.log_text.setMaximumHeight(120)
|
|
204
|
+
self.log_text.setFont(QFont("Consolas", 9))
|
|
205
|
+
progress_layout.addWidget(self.log_text)
|
|
206
|
+
|
|
207
|
+
layout.addWidget(progress_group)
|
|
208
|
+
|
|
209
|
+
# Buttons
|
|
210
|
+
button_layout = QHBoxLayout()
|
|
211
|
+
button_layout.addStretch()
|
|
212
|
+
|
|
213
|
+
self.run_btn = QPushButton("Create Dynamic Spectra")
|
|
214
|
+
self.run_btn.setMinimumWidth(180)
|
|
215
|
+
self.run_btn.clicked.connect(self._run_processing)
|
|
216
|
+
button_layout.addWidget(self.run_btn)
|
|
217
|
+
|
|
218
|
+
self.close_btn = QPushButton("Close")
|
|
219
|
+
self.close_btn.clicked.connect(self.close)
|
|
220
|
+
button_layout.addWidget(self.close_btn)
|
|
221
|
+
|
|
222
|
+
layout.addLayout(button_layout)
|
|
223
|
+
|
|
224
|
+
def _browse_input(self):
|
|
225
|
+
dir_path = QFileDialog.getExistingDirectory(
|
|
226
|
+
self, "Select MS Directory",
|
|
227
|
+
self.input_edit.text() or os.path.expanduser("~")
|
|
228
|
+
)
|
|
229
|
+
if dir_path:
|
|
230
|
+
self.input_edit.setText(dir_path)
|
|
231
|
+
# Auto-set output path
|
|
232
|
+
if not self.output_edit.text() or self.output_edit.text() == "dynamic_spectrum.fits":
|
|
233
|
+
self.output_edit.setText(os.path.join(dir_path, "dynamic_spectrum.fits"))
|
|
234
|
+
|
|
235
|
+
def _browse_output(self):
|
|
236
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
237
|
+
self, "Save FITS File",
|
|
238
|
+
self.output_edit.text() or "dynamic_spectrum.fits",
|
|
239
|
+
"FITS Files (*.fits);;All Files (*)"
|
|
240
|
+
)
|
|
241
|
+
if file_path:
|
|
242
|
+
self.output_edit.setText(file_path)
|
|
243
|
+
|
|
244
|
+
def _run_processing(self):
|
|
245
|
+
# Validate input
|
|
246
|
+
if not self.input_edit.text():
|
|
247
|
+
QMessageBox.warning(self, "Missing Input", "Please select an MS directory.")
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
if not os.path.isdir(self.input_edit.text()):
|
|
251
|
+
QMessageBox.warning(self, "Invalid Input", "The specified directory does not exist.")
|
|
252
|
+
return
|
|
253
|
+
|
|
254
|
+
if not self.output_edit.text():
|
|
255
|
+
QMessageBox.warning(self, "Missing Output", "Please specify an output FITS file path.")
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
# Check for MS files
|
|
259
|
+
import glob
|
|
260
|
+
ms_files = glob.glob(os.path.join(self.input_edit.text(), "*.MS"))
|
|
261
|
+
if not ms_files:
|
|
262
|
+
QMessageBox.warning(self, "No MS Files", "No .MS files found in the specified directory.")
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Prepare parameters
|
|
266
|
+
params = {
|
|
267
|
+
'indir': self.input_edit.text(),
|
|
268
|
+
'outfits': self.output_edit.text(),
|
|
269
|
+
'binwidth': self.binwidth_spin.value(),
|
|
270
|
+
'ncpu': self.ncpu_spin.value(),
|
|
271
|
+
'uvmin': self.uvmin_spin.value(),
|
|
272
|
+
'uvmax': self.uvmax_spin.value(),
|
|
273
|
+
'startfreq': self.startfreq_spin.value() if self.startfreq_spin.value() > 0 else None,
|
|
274
|
+
'endfreq': self.endfreq_spin.value() if self.endfreq_spin.value() > 0 else None,
|
|
275
|
+
'saveplot': self.save_plot_check.isChecked(),
|
|
276
|
+
'plot_filename': self.output_edit.text().replace('.fits', '.png') if self.save_plot_check.isChecked() else None
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# Start processing
|
|
280
|
+
self.log_text.clear()
|
|
281
|
+
self.log_text.append(f"Found {len(ms_files)} MS files")
|
|
282
|
+
self.log_text.append(f"Output: {params['outfits']}")
|
|
283
|
+
self.log_text.append("Starting processing...")
|
|
284
|
+
|
|
285
|
+
self.progress_bar.setVisible(True)
|
|
286
|
+
self.run_btn.setEnabled(False)
|
|
287
|
+
|
|
288
|
+
self.processing_thread = ProcessingThread(params)
|
|
289
|
+
self.processing_thread.progress.connect(self._on_progress)
|
|
290
|
+
self.processing_thread.finished.connect(self._on_finished)
|
|
291
|
+
self.processing_thread.start()
|
|
292
|
+
|
|
293
|
+
def _on_progress(self, message):
|
|
294
|
+
self.log_text.append(message)
|
|
295
|
+
# Auto-scroll to bottom
|
|
296
|
+
scrollbar = self.log_text.verticalScrollBar()
|
|
297
|
+
scrollbar.setValue(scrollbar.maximum())
|
|
298
|
+
|
|
299
|
+
def _on_finished(self, success, message):
|
|
300
|
+
self.progress_bar.setVisible(False)
|
|
301
|
+
self.run_btn.setEnabled(True)
|
|
302
|
+
|
|
303
|
+
self.log_text.append("")
|
|
304
|
+
self.log_text.append(f"{'✓' if success else '✗'} {message}")
|
|
305
|
+
|
|
306
|
+
if success:
|
|
307
|
+
QMessageBox.information(self, "Success", message)
|
|
308
|
+
else:
|
|
309
|
+
QMessageBox.warning(self, "Processing Failed", message)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def main():
|
|
313
|
+
"""Main entry point for standalone dialog."""
|
|
314
|
+
# Clear Qt environment variables to avoid opencv conflicts
|
|
315
|
+
for key in ['QT_PLUGIN_PATH', 'QT_QPA_PLATFORM_PLUGIN_PATH']:
|
|
316
|
+
if key in os.environ:
|
|
317
|
+
del os.environ[key]
|
|
318
|
+
|
|
319
|
+
app = QApplication(sys.argv)
|
|
320
|
+
|
|
321
|
+
# Get theme from command line
|
|
322
|
+
theme = get_theme_from_args()
|
|
323
|
+
apply_theme(app, theme)
|
|
324
|
+
|
|
325
|
+
dialog = DynamicSpectraDialog(theme=theme)
|
|
326
|
+
dialog.show()
|
|
327
|
+
|
|
328
|
+
sys.exit(app.exec_())
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
if __name__ == "__main__":
|
|
332
|
+
main()
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Create Dynamic Spectra from MS Files
|
|
3
|
+
|
|
4
|
+
This script:
|
|
5
|
+
1. Accepts a directory (--indir) containing MS files (each representing one subband).
|
|
6
|
+
2. Processes each MS file in parallel (using --ncpu processes) to:
|
|
7
|
+
- Read TIME, DATA, FLAG, and UVW columns using casacore.
|
|
8
|
+
- Filter rows with uv distance (in wavelengths) outside the range [130, 500].
|
|
9
|
+
- Bin data in time with a specified bin width (in seconds) and compute the mean amplitude.
|
|
10
|
+
3. Combines the results into a dynamic spectrum (2D array: time x subband).
|
|
11
|
+
4. Reads each MS file's frequency (median from the SPECTRAL_WINDOW table) and converts it to MHz.
|
|
12
|
+
5. Saves the dynamic spectrum to a FITS file. In the FITS file:
|
|
13
|
+
- The primary HDU contains the dynamic spectrum.
|
|
14
|
+
- A BinTableHDU contains the time axis in MJD and a second column with UTC strings.
|
|
15
|
+
- A second BinTableHDU contains the subband frequencies in MHz.
|
|
16
|
+
6. Optionally saves and/or displays a plot. The plot's x-axis is in UTC.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python create_dynamic_spectra.py --indir /path/to/msfiles --outfits dynamic_spectra.fits --binwidth 1.0 --ncpu 10 --saveplot False --showplot False
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import glob
|
|
25
|
+
import argparse
|
|
26
|
+
import re
|
|
27
|
+
import numpy as np
|
|
28
|
+
import matplotlib.pyplot as plt
|
|
29
|
+
from casacore.tables import table
|
|
30
|
+
from astropy.io import fits
|
|
31
|
+
from astropy.time import Time
|
|
32
|
+
import concurrent.futures
|
|
33
|
+
from matplotlib.dates import DateFormatter, date2num
|
|
34
|
+
import logging
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from pipeline.tasks.basic_functions import MS_inquiry
|
|
38
|
+
pipeline_run = True
|
|
39
|
+
except ImportError:
|
|
40
|
+
pipeline_run = False
|
|
41
|
+
|
|
42
|
+
# ------------------ Helper Functions ------------------
|
|
43
|
+
def extract_sb_number(filename):
|
|
44
|
+
"""Extract integer following '_SB' from the filename for sorting."""
|
|
45
|
+
m = re.search(r'_SB(\d+)_', filename)
|
|
46
|
+
return int(m.group(1)) if m else -1
|
|
47
|
+
|
|
48
|
+
def read_frequency(ms_file):
|
|
49
|
+
"""Read the median channel frequency (Hz) from the SPECTRAL_WINDOW subtable."""
|
|
50
|
+
sp_tab = table(os.path.join(ms_file, "SPECTRAL_WINDOW"), ack=False, readonly=True)
|
|
51
|
+
chan_freq = sp_tab.getcol("CHAN_FREQ")
|
|
52
|
+
median_freq = np.median(chan_freq)
|
|
53
|
+
sp_tab.close()
|
|
54
|
+
return median_freq
|
|
55
|
+
|
|
56
|
+
def read_ms(ms_file, binwidth=1.0, uv_min=130.0, uv_max=500.0):
|
|
57
|
+
"""
|
|
58
|
+
Read one MS file and produce binned mean amplitude values.
|
|
59
|
+
|
|
60
|
+
- Reads TIME, DATA, FLAG, and UVW columns.
|
|
61
|
+
- Computes uv distance (in wavelengths) using wavelength = 299792458/freq.
|
|
62
|
+
- Filters rows with uv distance outside [uv_min, uv_max] lambda.
|
|
63
|
+
- Bins the data in time bins of width binwidth (seconds) and computes the mean amplitude.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
bin_centers (np.array): Array of bin center times (in seconds).
|
|
67
|
+
mean_amp (np.array): Mean amplitude for each bin.
|
|
68
|
+
"""
|
|
69
|
+
try:
|
|
70
|
+
sp_tab = table(os.path.join(ms_file, "SPECTRAL_WINDOW"), ack=False, readonly=True)
|
|
71
|
+
chan_freq = sp_tab.getcol("CHAN_FREQ")
|
|
72
|
+
freq = np.median(chan_freq)
|
|
73
|
+
sp_tab.close()
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"Error reading frequency from {ms_file}: {e}")
|
|
76
|
+
raise
|
|
77
|
+
|
|
78
|
+
wavelength = 299792458.0 / freq # in meters
|
|
79
|
+
print(f"Processing {os.path.basename(ms_file)}: freq = {freq/1e6:.2f} MHz, λ = {wavelength*1e2:.2f} cm")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
tb = table(ms_file, ack=False, readonly=True)
|
|
83
|
+
times = tb.getcol("TIME") # seconds (assumed absolute)
|
|
84
|
+
# For simplicity, take the first correlation from DATA.
|
|
85
|
+
data = tb.getcol("DATA")[:,:,0] # shape: (nrow, nchan)
|
|
86
|
+
flags = tb.getcol("FLAG")
|
|
87
|
+
uvw = tb.getcol("UVW") # shape: (nrow, 3)
|
|
88
|
+
tb.close()
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(f"Error reading MS columns from {ms_file}: {e}")
|
|
91
|
+
raise
|
|
92
|
+
|
|
93
|
+
uv_dist = np.sqrt(uvw[:,0]**2 + uvw[:,1]**2)
|
|
94
|
+
uv_lambda = uv_dist / wavelength
|
|
95
|
+
valid_mask = (uv_lambda >= uv_min) & (uv_lambda <= uv_max)
|
|
96
|
+
if np.sum(valid_mask) == 0:
|
|
97
|
+
print(f"Warning: No valid data in uv-lambda range for {ms_file}.")
|
|
98
|
+
return np.array([]), np.array([])
|
|
99
|
+
|
|
100
|
+
times = times[valid_mask]
|
|
101
|
+
data = data[valid_mask]
|
|
102
|
+
flags = flags[valid_mask]
|
|
103
|
+
#times = np.unique(times)
|
|
104
|
+
|
|
105
|
+
min_time = np.min(times)
|
|
106
|
+
max_time = np.max(times)
|
|
107
|
+
edges = np.arange(min_time, max_time + binwidth, binwidth)
|
|
108
|
+
|
|
109
|
+
# Use vectorized binning.
|
|
110
|
+
# Compute mean amplitude per row (averaged over channels).
|
|
111
|
+
row_mean = np.mean(np.abs(data), axis=1)
|
|
112
|
+
# Digitize times into bins.
|
|
113
|
+
bin_idx = np.digitize(times, edges) - 1 # 0-indexed
|
|
114
|
+
nbins = len(edges) - 1
|
|
115
|
+
sum_per_bin = np.bincount(bin_idx, weights=row_mean, minlength=nbins)
|
|
116
|
+
count_per_bin = np.bincount(bin_idx, minlength=nbins)
|
|
117
|
+
mean_amp = np.full(nbins, np.nan)
|
|
118
|
+
valid = count_per_bin > 0
|
|
119
|
+
mean_amp[valid] = sum_per_bin[valid] / count_per_bin[valid]
|
|
120
|
+
bin_centers = (edges[:-1] + edges[1:]) / 2.0
|
|
121
|
+
|
|
122
|
+
return bin_centers, mean_amp
|
|
123
|
+
|
|
124
|
+
def create_dynamic_spectra(indir, binwidth=1.0, ncpu=10, uv_min=130.0, uv_max=500.0, startfreq=None, endfreq=None):
|
|
125
|
+
"""
|
|
126
|
+
Process all MS files in 'indir' in parallel to create a dynamic spectrum.
|
|
127
|
+
|
|
128
|
+
Each MS file (each representing one subband) is processed to extract its
|
|
129
|
+
time bin centers and mean amplitude (filtered for uv-lambda between uv_min and uv_max).
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
all_times (np.array): Sorted array of unique time bin centers (in seconds).
|
|
133
|
+
dynamic_spectra (2D np.array): Array of shape (Ntime, Nsub) with mean amplitudes.
|
|
134
|
+
Missing values are filled with NaN.
|
|
135
|
+
subband_indices (list): List of subband numbers corresponding to each column.
|
|
136
|
+
subband_freqs (list): List of frequencies (in MHz) for each subband.
|
|
137
|
+
"""
|
|
138
|
+
if (startfreq is None and endfreq is None) or (startfreq == 0.0 and endfreq == 0.0):
|
|
139
|
+
msfiles = sorted(glob.glob(os.path.join(indir, "*.MS")), key=lambda x: extract_sb_number(os.path.basename(x)))
|
|
140
|
+
elif startfreq<0.0 or endfreq<0.0:
|
|
141
|
+
raise RuntimeError(f"Invalid frequency range: {startfreq} to {endfreq}. Exiting...")
|
|
142
|
+
else:
|
|
143
|
+
from pipeline.tasks.basic_functions import get_filtered_MSs_for_given_freq_range
|
|
144
|
+
msfiles = get_filtered_MSs_for_given_freq_range(indir, startfreq, endfreq)
|
|
145
|
+
if len(msfiles) == 0:
|
|
146
|
+
raise RuntimeError(f"No MS files found in {indir}")
|
|
147
|
+
|
|
148
|
+
results = {}
|
|
149
|
+
subband_freqs = []
|
|
150
|
+
with concurrent.futures.ProcessPoolExecutor(max_workers=ncpu) as executor:
|
|
151
|
+
futures = {executor.submit(read_ms, msfile, binwidth, uv_min=uv_min, uv_max=uv_max): msfile for msfile in msfiles}
|
|
152
|
+
for future in concurrent.futures.as_completed(futures):
|
|
153
|
+
msfile = futures[future]
|
|
154
|
+
try:
|
|
155
|
+
t, amp = future.result()
|
|
156
|
+
results[msfile] = (t, amp)
|
|
157
|
+
except Exception as e:
|
|
158
|
+
print(f"Error processing {msfile}: {e}")
|
|
159
|
+
for msfile in msfiles:
|
|
160
|
+
try:
|
|
161
|
+
freq = read_frequency(msfile)
|
|
162
|
+
subband_freqs.append(freq/1e6)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
print(f"Error reading frequency from {msfile}: {e}")
|
|
165
|
+
subband_freqs.append(np.nan)
|
|
166
|
+
|
|
167
|
+
all_times_set = set()
|
|
168
|
+
subband_indices = []
|
|
169
|
+
for msfile in msfiles:
|
|
170
|
+
if msfile in results:
|
|
171
|
+
t, amp = results[msfile]
|
|
172
|
+
all_times_set.update(t)
|
|
173
|
+
subband_indices.append(extract_sb_number(os.path.basename(msfile)))
|
|
174
|
+
else:
|
|
175
|
+
subband_indices.append(extract_sb_number(os.path.basename(msfile)))
|
|
176
|
+
all_times = np.array(sorted(list(all_times_set)))
|
|
177
|
+
Ntime = len(all_times)
|
|
178
|
+
Nsub = len(msfiles)
|
|
179
|
+
dynamic_spectra = np.full((Ntime, Nsub), np.nan, dtype=np.float32)
|
|
180
|
+
for s, msfile in enumerate(msfiles):
|
|
181
|
+
if msfile not in results:
|
|
182
|
+
continue
|
|
183
|
+
t, amp = results[msfile]
|
|
184
|
+
time_amp = {t[i]: amp[i] for i in range(len(t))}
|
|
185
|
+
for i, gt in enumerate(all_times):
|
|
186
|
+
if gt in time_amp:
|
|
187
|
+
dynamic_spectra[i, s] = time_amp[gt]
|
|
188
|
+
return all_times, dynamic_spectra, subband_indices, subband_freqs
|
|
189
|
+
|
|
190
|
+
def write_fits(all_times, dynamic_spectra, subband_freqs, outfits):
|
|
191
|
+
"""
|
|
192
|
+
Write the dynamic spectrum to a FITS file.
|
|
193
|
+
|
|
194
|
+
The primary HDU holds the dynamic spectrum (axes: TIME x Subband).
|
|
195
|
+
Two BinTable HDUs are added:
|
|
196
|
+
- One for the time axis (TIME_MJD and UTC columns).
|
|
197
|
+
- One for the subband frequencies (FREQ_MHz column).
|
|
198
|
+
"""
|
|
199
|
+
# Convert global time (in seconds) to MJD and UTC.
|
|
200
|
+
time_mjd = all_times / 86400.0
|
|
201
|
+
utc_times = Time(time_mjd, format='mjd', scale='utc').iso
|
|
202
|
+
|
|
203
|
+
# Primary HDU with dynamic spectrum.
|
|
204
|
+
hdu = fits.PrimaryHDU(dynamic_spectra)
|
|
205
|
+
hdr = hdu.header
|
|
206
|
+
hdr["EXTNAME"] = "DYNAMIC_SPECTRUM"
|
|
207
|
+
hdr["BUNIT"] = "Amplitude"
|
|
208
|
+
hdr["COMMENT"] = "Axis0 = time (s), Axis1 = subband (sorted by _SBxxx_ in filename)"
|
|
209
|
+
|
|
210
|
+
# BinTable HDU for time axis.
|
|
211
|
+
col_time = fits.Column(name="TIME_MJD", format="E", array=time_mjd)
|
|
212
|
+
col_utc = fits.Column(name="UTC", format="20A", array=np.array(utc_times))
|
|
213
|
+
time_hdu = fits.BinTableHDU.from_columns([col_time, col_utc])
|
|
214
|
+
time_hdu.name = "TIME_AXIS"
|
|
215
|
+
|
|
216
|
+
# BinTable HDU for frequency axis.
|
|
217
|
+
col_freq = fits.Column(name="FREQ_MHz", format="E", array=np.array(subband_freqs))
|
|
218
|
+
freq_hdu = fits.BinTableHDU.from_columns([col_freq])
|
|
219
|
+
freq_hdu.name = "FREQ_AXIS"
|
|
220
|
+
|
|
221
|
+
hdul = fits.HDUList([hdu, time_hdu, freq_hdu])
|
|
222
|
+
hdul.writeto(outfits, overwrite=True)
|
|
223
|
+
print(f"Dynamic spectra saved to {outfits}")
|
|
224
|
+
|
|
225
|
+
def run_dynamic_spectra(indir, outfits, binwidth=1.0, ncpu=10, uvmin=130.0, uvmax=500.0, startfreq=None, endfreq=None, saveplot=False, showplot=False, logger=None, plot_filename=None):
|
|
226
|
+
"""
|
|
227
|
+
Process all MS files in 'indir' to create a dynamic spectrum and save to a FITS file.
|
|
228
|
+
|
|
229
|
+
Parameters
|
|
230
|
+
----------
|
|
231
|
+
indir : str
|
|
232
|
+
Directory containing MS files.
|
|
233
|
+
outfits : str
|
|
234
|
+
Output FITS file path.
|
|
235
|
+
binwidth : float, optional
|
|
236
|
+
Time bin width in seconds, by default 1.0
|
|
237
|
+
ncpu : int, optional
|
|
238
|
+
Number of CPU cores to use, by default 10
|
|
239
|
+
uvmin : float, optional
|
|
240
|
+
Minimum UV baseline in lambda, by default 130.0
|
|
241
|
+
uvmax : float, optional
|
|
242
|
+
Maximum UV baseline in lambda, by default 500.0
|
|
243
|
+
startfreq : float, optional
|
|
244
|
+
Start frequency in MHz, by default None
|
|
245
|
+
endfreq : float, optional
|
|
246
|
+
End frequency in MHz, by default None
|
|
247
|
+
saveplot : bool, optional
|
|
248
|
+
Whether to save the plot, by default False
|
|
249
|
+
showplot : bool, optional
|
|
250
|
+
Whether to show the plot, by default False
|
|
251
|
+
logger : logging.Logger, optional
|
|
252
|
+
Logger instance, by default None
|
|
253
|
+
plot_filename : str, optional
|
|
254
|
+
Path where the plot should be saved, by default None
|
|
255
|
+
If None but saveplot is True, "dynamic_spectrum.png" will be used
|
|
256
|
+
|
|
257
|
+
Returns
|
|
258
|
+
-------
|
|
259
|
+
str
|
|
260
|
+
Path to the output FITS file.
|
|
261
|
+
"""
|
|
262
|
+
if logger is None:
|
|
263
|
+
logger = logging.getLogger("create_dynamic_spectra")
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
all_times, dynamic_spectra, subband_indices, subband_freqs = create_dynamic_spectra(
|
|
267
|
+
indir, binwidth=binwidth, ncpu=ncpu, uv_min=uvmin, uv_max=uvmax, startfreq=startfreq, endfreq=endfreq)
|
|
268
|
+
except Exception as e:
|
|
269
|
+
if logger:
|
|
270
|
+
logger.exception(f"Error creating dynamic spectra: {e}")
|
|
271
|
+
else:
|
|
272
|
+
print(f"Error creating dynamic spectra: {e}")
|
|
273
|
+
return None
|
|
274
|
+
|
|
275
|
+
# For plotting, convert time (in seconds) to UTC datetime numbers.
|
|
276
|
+
time_mjd = all_times / 86400.0
|
|
277
|
+
utc_dt = Time(time_mjd, format='mjd', scale='utc').to_datetime()
|
|
278
|
+
utc_num = [date2num(dt) for dt in utc_dt]
|
|
279
|
+
|
|
280
|
+
# Plot dynamic spectrum with x-axis in UTC.
|
|
281
|
+
if saveplot or showplot:
|
|
282
|
+
from matplotlib.dates import DateFormatter
|
|
283
|
+
plt.figure(figsize=(10, 6))
|
|
284
|
+
extent = [utc_num[0], utc_num[-1], min(subband_freqs), max(subband_freqs)]
|
|
285
|
+
plt.imshow(dynamic_spectra.T, aspect='auto', origin='lower', extent=extent, cmap='viridis')
|
|
286
|
+
plt.xlabel("Time (UTC)")
|
|
287
|
+
plt.ylabel("Frequency (MHz)")
|
|
288
|
+
plt.title("Dynamic Spectrum")
|
|
289
|
+
plt.colorbar(label="Amplitude")
|
|
290
|
+
ax = plt.gca()
|
|
291
|
+
date_formatter = DateFormatter('%Y-%m-%d\n%H:%M:%S')
|
|
292
|
+
ax.xaxis.set_major_formatter(date_formatter)
|
|
293
|
+
plt.gcf().autofmt_xdate()
|
|
294
|
+
|
|
295
|
+
if saveplot:
|
|
296
|
+
# Use the provided plot_filename or default to "dynamic_spectrum.png"
|
|
297
|
+
save_path = plot_filename if plot_filename else "dynamic_spectrum.png"
|
|
298
|
+
plt.savefig(save_path, dpi=150, bbox_inches='tight')
|
|
299
|
+
if logger:
|
|
300
|
+
logger.info(f"Dynamic spectrum plot saved as {save_path}")
|
|
301
|
+
else:
|
|
302
|
+
print(f"Dynamic spectrum plot saved as {save_path}")
|
|
303
|
+
|
|
304
|
+
if showplot:
|
|
305
|
+
plt.show()
|
|
306
|
+
else:
|
|
307
|
+
plt.close()
|
|
308
|
+
|
|
309
|
+
write_fits(all_times, dynamic_spectra, subband_freqs, outfits)
|
|
310
|
+
return outfits
|
|
311
|
+
|
|
312
|
+
def main():
|
|
313
|
+
parser = argparse.ArgumentParser(description="Create Dynamic Spectra from MS files in a directory.")
|
|
314
|
+
parser.add_argument("--indir", type=str, required=True, help="Directory containing MS files")
|
|
315
|
+
parser.add_argument("--outfits", type=str, default="dynamic_spectra.fits", help="Output FITS file name")
|
|
316
|
+
parser.add_argument("--binwidth", type=float, default=1.0, help="Time bin width in seconds (default 1 s)")
|
|
317
|
+
parser.add_argument("--ncpu", type=int, default=10, help="Number of CPU threads to use (default 10)")
|
|
318
|
+
parser.add_argument("--uvmin", type=float, default=130.0, help="Minimum UV baseline (lambda)")
|
|
319
|
+
parser.add_argument("--uvmax", type=float, default=500.0, help="Maximum UV baseline (lambda)")
|
|
320
|
+
parser.add_argument("--startfreq", type=float, default=None, help="Start frequency in MHz (optional)")
|
|
321
|
+
parser.add_argument("--endfreq", type=float, default=None, help="End frequency in MHz (optional)")
|
|
322
|
+
parser.add_argument("--saveplot", action="store_true", help="Save plot to file")
|
|
323
|
+
parser.add_argument("--showplot", action="store_true", help="Show plot in window")
|
|
324
|
+
parser.add_argument("--plotfile", type=str, default=None, help="Filename to save the plot")
|
|
325
|
+
args = parser.parse_args()
|
|
326
|
+
|
|
327
|
+
# Setup a logger
|
|
328
|
+
logger = logging.getLogger("create_dynamic_spectra")
|
|
329
|
+
logger.setLevel(logging.INFO)
|
|
330
|
+
handler = logging.StreamHandler()
|
|
331
|
+
handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
|
|
332
|
+
logger.addHandler(handler)
|
|
333
|
+
|
|
334
|
+
run_dynamic_spectra(
|
|
335
|
+
args.indir,
|
|
336
|
+
outfits=args.outfits,
|
|
337
|
+
binwidth=args.binwidth,
|
|
338
|
+
ncpu=args.ncpu,
|
|
339
|
+
uvmin=args.uvmin,
|
|
340
|
+
uvmax=args.uvmax,
|
|
341
|
+
startfreq=args.startfreq,
|
|
342
|
+
endfreq=args.endfreq,
|
|
343
|
+
saveplot=args.saveplot,
|
|
344
|
+
showplot=args.showplot,
|
|
345
|
+
logger=logger,
|
|
346
|
+
plot_filename=args.plotfile
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
from matplotlib.dates import date2num # imported here for plotting conversion
|
|
351
|
+
main()
|