ecallistolib 0.2.1__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.
- ecallistolib/__init__.py +110 -0
- ecallistolib/combine.py +110 -0
- ecallistolib/crop.py +225 -0
- ecallistolib/download.py +147 -0
- ecallistolib/exceptions.py +38 -0
- ecallistolib/io.py +130 -0
- ecallistolib/models.py +44 -0
- ecallistolib/plotting.py +406 -0
- ecallistolib/processing.py +70 -0
- ecallistolib-0.2.1.dist-info/METADATA +833 -0
- ecallistolib-0.2.1.dist-info/RECORD +14 -0
- ecallistolib-0.2.1.dist-info/WHEEL +5 -0
- ecallistolib-0.2.1.dist-info/licenses/LICENSE +21 -0
- ecallistolib-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ecallistolib
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Tools to download, read, process, and plot e-CALLISTO FITS dynamic spectra.
|
|
5
|
+
Author: Sahan S. Liyanage
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/saandev/ecallistolib
|
|
8
|
+
Project-URL: Issues, https://github.com/saandev/ecallistolib/issues
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: numpy>=1.23
|
|
13
|
+
Requires-Dist: astropy>=5.3
|
|
14
|
+
Provides-Extra: download
|
|
15
|
+
Requires-Dist: requests>=2.31; extra == "download"
|
|
16
|
+
Requires-Dist: beautifulsoup4>=4.12; extra == "download"
|
|
17
|
+
Provides-Extra: plot
|
|
18
|
+
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# ecallistolib
|
|
22
|
+
|
|
23
|
+
[](https://www.python.org/downloads/)
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
|
|
26
|
+
A Python library to **download**, **read**, **process**, and **plot** e-CALLISTO FITS dynamic spectra.
|
|
27
|
+
|
|
28
|
+
[e-CALLISTO](http://www.e-callisto.org/) (Compact Astronomical Low-frequency Low-cost Instrument for Spectroscopy and Transportable Observatory) is an international network of solar radio spectrometers that monitor solar radio emissions in the frequency range of approximately 45–870 MHz.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Table of Contents
|
|
33
|
+
|
|
34
|
+
- [Features](#features)
|
|
35
|
+
- [Installation](#installation)
|
|
36
|
+
- [Quick Start](#quick-start)
|
|
37
|
+
- [Usage Guide](#usage-guide)
|
|
38
|
+
- [Reading FITS Files](#reading-fits-files)
|
|
39
|
+
- [Downloading Data](#downloading-data)
|
|
40
|
+
- [Processing Data](#processing-data)
|
|
41
|
+
- [Cropping & Slicing](#cropping--slicing)
|
|
42
|
+
- [Combining Spectra](#combining-spectra)
|
|
43
|
+
- [Plotting](#plotting)
|
|
44
|
+
- [API Reference](#api-reference)
|
|
45
|
+
- [DynamicSpectrum](#dynamicspectrum)
|
|
46
|
+
- [I/O Functions](#io-functions)
|
|
47
|
+
- [Download Functions](#download-functions)
|
|
48
|
+
- [Processing Functions](#processing-functions)
|
|
49
|
+
- [Cropping Functions](#cropping-functions)
|
|
50
|
+
- [Combine Functions](#combine-functions)
|
|
51
|
+
- [Plotting Functions](#plotting-functions)
|
|
52
|
+
- [Exceptions](#exceptions)
|
|
53
|
+
- [Examples](#examples)
|
|
54
|
+
- [Contributing](#contributing)
|
|
55
|
+
- [License](#license)
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- 📥 **Download** – List and download FITS files directly from the e-CALLISTO data archive
|
|
62
|
+
- 📖 **Read** – Parse e-CALLISTO FITS files (`.fit`, `.fit.gz`) into structured Python objects
|
|
63
|
+
- 🔧 **Process** – Apply noise reduction techniques (mean subtraction, clipping, scaling)
|
|
64
|
+
- ✂️ **Crop** – Extract frequency and time ranges from spectra
|
|
65
|
+
- 🔗 **Combine** – Merge multiple spectra along the time or frequency axis
|
|
66
|
+
- 📊 **Plot** – Generate publication-ready dynamic spectrum visualizations
|
|
67
|
+
- ⚠️ **Error Handling** – Custom exceptions for robust error management
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Installation
|
|
72
|
+
|
|
73
|
+
### From Source (Development)
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
git clone https://github.com/saandev/ecallistolib.git
|
|
77
|
+
cd ecallistolib
|
|
78
|
+
pip install -e .
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Optional Dependencies
|
|
82
|
+
|
|
83
|
+
Install optional features as needed:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# For downloading data from the e-CALLISTO archive
|
|
87
|
+
pip install -e ".[download]"
|
|
88
|
+
|
|
89
|
+
# For plotting
|
|
90
|
+
pip install -e ".[plot]"
|
|
91
|
+
|
|
92
|
+
# Install all optional dependencies
|
|
93
|
+
pip install -e ".[download,plot]"
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Quick Start
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import ecallistolib as ecl
|
|
102
|
+
|
|
103
|
+
# Read a FITS file
|
|
104
|
+
spectrum = ecl.read_fits("ALASKA_20230101_120000_01.fit.gz")
|
|
105
|
+
|
|
106
|
+
# Apply noise reduction
|
|
107
|
+
cleaned = ecl.noise_reduce_mean_clip(spectrum)
|
|
108
|
+
|
|
109
|
+
# Plot the result
|
|
110
|
+
fig, ax, im = ecl.plot_dynamic_spectrum(cleaned, title="Solar Radio Burst")
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Usage Guide
|
|
116
|
+
|
|
117
|
+
### Reading FITS Files
|
|
118
|
+
|
|
119
|
+
The library can read standard e-CALLISTO FITS files:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import ecallistolib as ecl
|
|
123
|
+
|
|
124
|
+
# Read a single FITS file
|
|
125
|
+
spectrum = ecl.read_fits("path/to/STATION_YYYYMMDD_HHMMSS_NN.fit.gz")
|
|
126
|
+
|
|
127
|
+
# Access the data
|
|
128
|
+
print(f"Data shape: {spectrum.shape}") # (n_freq, n_time)
|
|
129
|
+
print(f"Frequencies: {spectrum.freqs_mhz}") # Frequency axis in MHz
|
|
130
|
+
print(f"Time samples: {spectrum.time_s}") # Time axis in seconds
|
|
131
|
+
print(f"Source file: {spectrum.source}") # Original file path
|
|
132
|
+
print(f"Metadata: {spectrum.meta}") # Station, date, etc.
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Parsing Filenames
|
|
136
|
+
|
|
137
|
+
Extract metadata from e-CALLISTO filenames:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
parts = ecl.parse_callisto_filename("ALASKA_20230615_143000_01.fit.gz")
|
|
141
|
+
|
|
142
|
+
print(parts.station) # "ALASKA"
|
|
143
|
+
print(parts.date_yyyymmdd) # "20230615"
|
|
144
|
+
print(parts.time_hhmmss) # "143000"
|
|
145
|
+
print(parts.focus) # "01"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### Downloading Data
|
|
151
|
+
|
|
152
|
+
Download FITS files directly from the e-CALLISTO archive:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from datetime import date
|
|
156
|
+
import ecallistolib as ecl
|
|
157
|
+
|
|
158
|
+
# List available files for a specific day, hour, and station
|
|
159
|
+
remote_files = ecl.list_remote_fits(
|
|
160
|
+
day=date(2023, 6, 15),
|
|
161
|
+
hour=14, # UTC hour (0-23)
|
|
162
|
+
station_substring="alaska" # Case-insensitive station filter
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
print(f"Found {len(remote_files)} files:")
|
|
166
|
+
for rf in remote_files:
|
|
167
|
+
print(f" - {rf.name}: {rf.url}")
|
|
168
|
+
|
|
169
|
+
# Download the files
|
|
170
|
+
saved_paths = ecl.download_files(remote_files, out_dir="./data")
|
|
171
|
+
|
|
172
|
+
for path in saved_paths:
|
|
173
|
+
print(f"Downloaded: {path}")
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
### Processing Data
|
|
179
|
+
|
|
180
|
+
#### Noise Reduction
|
|
181
|
+
|
|
182
|
+
Apply mean-subtraction and clipping to enhance signal visibility:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
import ecallistolib as ecl
|
|
186
|
+
|
|
187
|
+
spectrum = ecl.read_fits("my_spectrum.fit.gz")
|
|
188
|
+
|
|
189
|
+
# Apply noise reduction with default parameters
|
|
190
|
+
cleaned = ecl.noise_reduce_mean_clip(spectrum)
|
|
191
|
+
|
|
192
|
+
# Or customize the parameters
|
|
193
|
+
cleaned = ecl.noise_reduce_mean_clip(
|
|
194
|
+
spectrum,
|
|
195
|
+
clip_low=-5.0, # Lower clipping threshold
|
|
196
|
+
clip_high=20.0, # Upper clipping threshold
|
|
197
|
+
scale=2500.0 / 255.0 / 25.4 # Scaling factor (None to disable)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Processing metadata is recorded
|
|
201
|
+
print(cleaned.meta["noise_reduction"])
|
|
202
|
+
# {'method': 'mean_subtract_clip', 'clip_low': -5.0, 'clip_high': 20.0, 'scale': 3.88...}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Algorithm Details:**
|
|
206
|
+
1. Subtract the mean intensity over time for each frequency channel (removes baseline)
|
|
207
|
+
2. Clip values to the specified range
|
|
208
|
+
3. Apply optional scaling factor
|
|
209
|
+
|
|
210
|
+
#### Background Subtraction Only
|
|
211
|
+
|
|
212
|
+
If you want to visualize the result before clipping is applied:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
import ecallistolib as ecl
|
|
216
|
+
|
|
217
|
+
spectrum = ecl.read_fits("my_spectrum.fit.gz")
|
|
218
|
+
|
|
219
|
+
# Apply only background subtraction (no clipping)
|
|
220
|
+
bg_subtracted = ecl.background_subtract(spectrum)
|
|
221
|
+
|
|
222
|
+
# This is equivalent to the first step of noise_reduce_mean_clip
|
|
223
|
+
# Each frequency channel now has zero mean
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
### Cropping & Slicing
|
|
229
|
+
|
|
230
|
+
Extract specific frequency or time ranges from a spectrum:
|
|
231
|
+
|
|
232
|
+
#### Crop by Physical Values
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
import ecallistolib as ecl
|
|
236
|
+
|
|
237
|
+
spectrum = ecl.read_fits("my_spectrum.fit.gz")
|
|
238
|
+
|
|
239
|
+
# Crop to specific frequency range (in MHz)
|
|
240
|
+
cropped = ecl.crop_frequency(spectrum, freq_min=100, freq_max=300)
|
|
241
|
+
|
|
242
|
+
# Crop to specific time range (in seconds)
|
|
243
|
+
cropped = ecf.crop_time(spectrum, time_min=10, time_max=60)
|
|
244
|
+
|
|
245
|
+
# Crop both axes at once
|
|
246
|
+
cropped = ecl.crop(spectrum, freq_range=(100, 300), time_range=(10, 60))
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Slice by Array Index
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
# Get first 100 frequency channels
|
|
253
|
+
sliced = ecl.slice_by_index(spectrum, freq_slice=slice(0, 100))
|
|
254
|
+
|
|
255
|
+
# Get every other time sample (downsampling)
|
|
256
|
+
sliced = ecl.slice_by_index(spectrum, time_slice=slice(None, None, 2))
|
|
257
|
+
|
|
258
|
+
# Combine slices
|
|
259
|
+
sliced = ecl.slice_by_index(spectrum, freq_slice=slice(50, 150), time_slice=slice(0, 500))
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### Cropping Preserves Metadata
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
cropped = ecl.crop(spectrum, freq_range=(100, 200))
|
|
266
|
+
|
|
267
|
+
# Check what was cropped
|
|
268
|
+
print(cropped.meta["cropped"])
|
|
269
|
+
# {'frequency': {'min': 100, 'max': 200}}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### Combining Spectra
|
|
275
|
+
|
|
276
|
+
#### Combine Along Frequency (Vertical Stacking)
|
|
277
|
+
|
|
278
|
+
Combine two spectra from the same observation but different frequency bands (e.g., focus 01 and 02):
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
import ecallistolib as ecl
|
|
282
|
+
|
|
283
|
+
# Check if files can be combined
|
|
284
|
+
if ecl.can_combine_frequency("file_01.fit.gz", "file_02.fit.gz"):
|
|
285
|
+
combined = ecl.combine_frequency("file_01.fit.gz", "file_02.fit.gz")
|
|
286
|
+
print(f"Combined shape: {combined.shape}")
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Requirements for frequency combination:**
|
|
290
|
+
- Same station, date, and time
|
|
291
|
+
- Different focus numbers (01 vs 02)
|
|
292
|
+
- Matching time axes
|
|
293
|
+
|
|
294
|
+
#### Combine Along Time (Horizontal Concatenation)
|
|
295
|
+
|
|
296
|
+
Concatenate multiple spectra recorded consecutively:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
import ecallistolib as ecl
|
|
300
|
+
|
|
301
|
+
files = [
|
|
302
|
+
"ALASKA_20230615_140000_01.fit.gz",
|
|
303
|
+
"ALASKA_20230615_141500_01.fit.gz",
|
|
304
|
+
"ALASKA_20230615_143000_01.fit.gz",
|
|
305
|
+
]
|
|
306
|
+
|
|
307
|
+
# Check compatibility
|
|
308
|
+
if ecl.can_combine_time(files):
|
|
309
|
+
combined = ecl.combine_time(files)
|
|
310
|
+
print(f"Combined shape: {combined.shape}")
|
|
311
|
+
print(f"Total duration: {combined.time_s[-1] - combined.time_s[0]:.1f} seconds")
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
**Requirements for time combination:**
|
|
315
|
+
- Same station, date, and focus
|
|
316
|
+
- Matching frequency axes
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### Plotting
|
|
321
|
+
|
|
322
|
+
Create dynamic spectrum visualizations with full customization:
|
|
323
|
+
|
|
324
|
+
```python
|
|
325
|
+
import ecallistolib as ecl
|
|
326
|
+
import matplotlib.pyplot as plt
|
|
327
|
+
|
|
328
|
+
spectrum = ecf.read_fits("my_spectrum.fit.gz")
|
|
329
|
+
cleaned = ecf.noise_reduce_mean_clip(spectrum)
|
|
330
|
+
|
|
331
|
+
# Basic plot
|
|
332
|
+
fig, ax, im = ecf.plot_dynamic_spectrum(cleaned, title="Solar Radio Observation")
|
|
333
|
+
plt.show()
|
|
334
|
+
|
|
335
|
+
# Customized plot with clipping values, colormap, and figure size
|
|
336
|
+
fig, ax, im = ecf.plot_dynamic_spectrum(
|
|
337
|
+
cleaned,
|
|
338
|
+
title="Type III Solar Burst",
|
|
339
|
+
cmap="magma", # Matplotlib colormap
|
|
340
|
+
vmin=-5, # Colormap lower bound
|
|
341
|
+
vmax=20, # Colormap upper bound
|
|
342
|
+
figsize=(12, 6), # Figure size in inches
|
|
343
|
+
interpolation="bilinear" # Any matplotlib imshow kwarg
|
|
344
|
+
)
|
|
345
|
+
plt.savefig("spectrum.png", dpi=150, bbox_inches="tight")
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
#### Plotting Raw Data
|
|
349
|
+
|
|
350
|
+
```python
|
|
351
|
+
import ecallistolib as ecf
|
|
352
|
+
|
|
353
|
+
spectrum = ecf.read_fits("my_spectrum.fit.gz")
|
|
354
|
+
|
|
355
|
+
# Plot raw spectrum without any processing
|
|
356
|
+
fig, ax, im = ecf.plot_raw_spectrum(
|
|
357
|
+
spectrum,
|
|
358
|
+
title="Raw Spectrum",
|
|
359
|
+
cmap="viridis",
|
|
360
|
+
figsize=(10, 5)
|
|
361
|
+
)
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
#### Plotting Background Subtracted (Before Clipping)
|
|
365
|
+
|
|
366
|
+
```python
|
|
367
|
+
import ecallistolib as ecf
|
|
368
|
+
|
|
369
|
+
spectrum = ecf.read_fits("my_spectrum.fit.gz")
|
|
370
|
+
|
|
371
|
+
# Plot after background subtraction but before clipping
|
|
372
|
+
fig, ax, im = ecf.plot_background_subtracted(
|
|
373
|
+
spectrum,
|
|
374
|
+
vmin=-10,
|
|
375
|
+
vmax=30,
|
|
376
|
+
cmap="RdBu_r" # Diverging colormap for +/- values
|
|
377
|
+
)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
#### Time Axis Formats
|
|
381
|
+
|
|
382
|
+
Display time in seconds or Universal Time (UT):
|
|
383
|
+
|
|
384
|
+
```python
|
|
385
|
+
import ecallistolib as ecf
|
|
386
|
+
|
|
387
|
+
spectrum = ecf.read_fits("my_spectrum.fit.gz")
|
|
388
|
+
|
|
389
|
+
# Default: time in seconds
|
|
390
|
+
ecf.plot_dynamic_spectrum(spectrum, time_format="seconds")
|
|
391
|
+
|
|
392
|
+
# Time in UT format (HH:MM:SS)
|
|
393
|
+
ecf.plot_dynamic_spectrum(spectrum, time_format="ut")
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
#### Time Axis Converter
|
|
397
|
+
|
|
398
|
+
Convert between elapsed seconds and UT time programmatically:
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
import ecallistolib as ecf
|
|
402
|
+
|
|
403
|
+
spectrum = ecf.read_fits("my_spectrum.fit.gz")
|
|
404
|
+
|
|
405
|
+
# Create converter from spectrum metadata
|
|
406
|
+
converter = ecf.TimeAxisConverter.from_dynamic_spectrum(spectrum)
|
|
407
|
+
|
|
408
|
+
# Convert seconds to UT
|
|
409
|
+
print(converter.seconds_to_ut(100)) # "12:01:40"
|
|
410
|
+
print(converter.seconds_to_ut(3661)) # "13:01:01"
|
|
411
|
+
|
|
412
|
+
# Convert UT to seconds
|
|
413
|
+
print(converter.ut_to_seconds("12:01:40")) # 100.0
|
|
414
|
+
print(converter.ut_to_seconds("13:00:00")) # 3600.0
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### Using a Custom Axes
|
|
418
|
+
|
|
419
|
+
```python
|
|
420
|
+
import matplotlib.pyplot as plt
|
|
421
|
+
import ecallistolib as ecf
|
|
422
|
+
|
|
423
|
+
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
|
|
424
|
+
|
|
425
|
+
spectrum1 = ecf.read_fits("file1.fit.gz")
|
|
426
|
+
spectrum2 = ecf.read_fits("file2.fit.gz")
|
|
427
|
+
|
|
428
|
+
ecf.plot_raw_spectrum(spectrum1, ax=axes[0], title="Raw")
|
|
429
|
+
ecf.plot_dynamic_spectrum(
|
|
430
|
+
ecf.noise_reduce_mean_clip(spectrum2),
|
|
431
|
+
ax=axes[1],
|
|
432
|
+
title="Noise Reduced",
|
|
433
|
+
vmin=-5, vmax=20
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
plt.tight_layout()
|
|
437
|
+
plt.show()
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## API Reference
|
|
443
|
+
|
|
444
|
+
### DynamicSpectrum
|
|
445
|
+
|
|
446
|
+
The core data structure representing an e-CALLISTO dynamic spectrum.
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
@dataclass(frozen=True)
|
|
450
|
+
class DynamicSpectrum:
|
|
451
|
+
data: np.ndarray # Intensity data, shape (n_freq, n_time)
|
|
452
|
+
freqs_mhz: np.ndarray # Frequency axis in MHz, shape (n_freq,)
|
|
453
|
+
time_s: np.ndarray # Time axis in seconds, shape (n_time,)
|
|
454
|
+
source: Optional[Path] # Original file path
|
|
455
|
+
meta: Mapping[str, Any] # Metadata dictionary
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
#### Properties
|
|
459
|
+
|
|
460
|
+
| Property | Type | Description |
|
|
461
|
+
|----------|------|-------------|
|
|
462
|
+
| `shape` | `tuple[int, int]` | Returns `(n_freq, n_time)` |
|
|
463
|
+
|
|
464
|
+
#### Methods
|
|
465
|
+
|
|
466
|
+
| Method | Description |
|
|
467
|
+
|--------|-------------|
|
|
468
|
+
| `copy_with(**changes)` | Returns a new `DynamicSpectrum` with specified fields replaced |
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
### I/O Functions
|
|
473
|
+
|
|
474
|
+
#### `read_fits(path: str | Path) -> DynamicSpectrum`
|
|
475
|
+
|
|
476
|
+
Read an e-CALLISTO FITS file.
|
|
477
|
+
|
|
478
|
+
| Parameter | Type | Description |
|
|
479
|
+
|-----------|------|-------------|
|
|
480
|
+
| `path` | `str \| Path` | Path to the FITS file (`.fit` or `.fit.gz`) |
|
|
481
|
+
|
|
482
|
+
**Returns:** `DynamicSpectrum` object with data, frequencies, time, and metadata.
|
|
483
|
+
|
|
484
|
+
---
|
|
485
|
+
|
|
486
|
+
#### `parse_callisto_filename(path: str | Path) -> CallistoFileParts`
|
|
487
|
+
|
|
488
|
+
Parse an e-CALLISTO filename.
|
|
489
|
+
|
|
490
|
+
**Returns:** `CallistoFileParts` with attributes:
|
|
491
|
+
- `station` – Station name
|
|
492
|
+
- `date_yyyymmdd` – Date string
|
|
493
|
+
- `time_hhmmss` – Time string
|
|
494
|
+
- `focus` – Focus/channel number
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
### Download Functions
|
|
499
|
+
|
|
500
|
+
#### `list_remote_fits(day, hour, station_substring, base_url=..., timeout_s=10.0) -> List[RemoteFITS]`
|
|
501
|
+
|
|
502
|
+
List available FITS files from the e-CALLISTO archive.
|
|
503
|
+
|
|
504
|
+
| Parameter | Type | Description |
|
|
505
|
+
|-----------|------|-------------|
|
|
506
|
+
| `day` | `date` | Target date |
|
|
507
|
+
| `hour` | `int` | UTC hour (0–23) |
|
|
508
|
+
| `station_substring` | `str` | Case-insensitive station filter |
|
|
509
|
+
| `base_url` | `str` | Archive base URL (optional) |
|
|
510
|
+
| `timeout_s` | `float` | Request timeout in seconds |
|
|
511
|
+
|
|
512
|
+
**Returns:** List of `RemoteFITS` objects with `name` and `url` attributes.
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
#### `download_files(items, out_dir, timeout_s=30.0) -> List[Path]`
|
|
517
|
+
|
|
518
|
+
Download FITS files to a local directory.
|
|
519
|
+
|
|
520
|
+
| Parameter | Type | Description |
|
|
521
|
+
|-----------|------|-------------|
|
|
522
|
+
| `items` | `Iterable[RemoteFITS]` | Files to download |
|
|
523
|
+
| `out_dir` | `str \| Path` | Output directory |
|
|
524
|
+
| `timeout_s` | `float` | Request timeout per file |
|
|
525
|
+
|
|
526
|
+
**Returns:** List of saved file paths.
|
|
527
|
+
|
|
528
|
+
---
|
|
529
|
+
|
|
530
|
+
### Processing Functions
|
|
531
|
+
|
|
532
|
+
#### `noise_reduce_mean_clip(ds, clip_low=-5.0, clip_high=20.0, scale=...) -> DynamicSpectrum`
|
|
533
|
+
|
|
534
|
+
Apply noise reduction via mean subtraction and clipping.
|
|
535
|
+
|
|
536
|
+
| Parameter | Type | Default | Description |
|
|
537
|
+
|-----------|------|---------|-------------|
|
|
538
|
+
| `ds` | `DynamicSpectrum` | — | Input spectrum |
|
|
539
|
+
| `clip_low` | `float` | `-5.0` | Lower clipping threshold |
|
|
540
|
+
| `clip_high` | `float` | `20.0` | Upper clipping threshold |
|
|
541
|
+
| `scale` | `float \| None` | `~3.88` | Scaling factor (`None` to disable) |
|
|
542
|
+
|
|
543
|
+
**Returns:** New `DynamicSpectrum` with processed data and updated metadata.
|
|
544
|
+
|
|
545
|
+
---
|
|
546
|
+
|
|
547
|
+
#### `background_subtract(ds) -> DynamicSpectrum`
|
|
548
|
+
|
|
549
|
+
Subtract mean over time for each frequency channel (background subtraction only, no clipping).
|
|
550
|
+
|
|
551
|
+
| Parameter | Type | Description |
|
|
552
|
+
|-----------|------|-------------|
|
|
553
|
+
| `ds` | `DynamicSpectrum` | Input spectrum |
|
|
554
|
+
|
|
555
|
+
**Returns:** New `DynamicSpectrum` with background subtracted. Useful for visualizing data before clipping is applied.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
### Cropping Functions
|
|
560
|
+
|
|
561
|
+
#### `crop_frequency(ds, freq_min=None, freq_max=None) -> DynamicSpectrum`
|
|
562
|
+
|
|
563
|
+
Crop a spectrum to a frequency range.
|
|
564
|
+
|
|
565
|
+
| Parameter | Type | Description |
|
|
566
|
+
|-----------|------|-------------|
|
|
567
|
+
| `ds` | `DynamicSpectrum` | Input spectrum |
|
|
568
|
+
| `freq_min` | `float \| None` | Minimum frequency in MHz (inclusive) |
|
|
569
|
+
| `freq_max` | `float \| None` | Maximum frequency in MHz (inclusive) |
|
|
570
|
+
|
|
571
|
+
**Raises:** `CropError` if range is invalid or results in empty data.
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
#### `crop_time(ds, time_min=None, time_max=None) -> DynamicSpectrum`
|
|
576
|
+
|
|
577
|
+
Crop a spectrum to a time range.
|
|
578
|
+
|
|
579
|
+
| Parameter | Type | Description |
|
|
580
|
+
|-----------|------|-------------|
|
|
581
|
+
| `ds` | `DynamicSpectrum` | Input spectrum |
|
|
582
|
+
| `time_min` | `float \| None` | Minimum time in seconds |
|
|
583
|
+
| `time_max` | `float \| None` | Maximum time in seconds |
|
|
584
|
+
|
|
585
|
+
**Raises:** `CropError` if range is invalid or results in empty data.
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
589
|
+
#### `crop(ds, freq_range=None, time_range=None) -> DynamicSpectrum`
|
|
590
|
+
|
|
591
|
+
Crop a spectrum along both axes at once.
|
|
592
|
+
|
|
593
|
+
| Parameter | Type | Description |
|
|
594
|
+
|-----------|------|-------------|
|
|
595
|
+
| `ds` | `DynamicSpectrum` | Input spectrum |
|
|
596
|
+
| `freq_range` | `tuple \| None` | `(min, max)` frequency in MHz |
|
|
597
|
+
| `time_range` | `tuple \| None` | `(min, max)` time in seconds |
|
|
598
|
+
|
|
599
|
+
---
|
|
600
|
+
|
|
601
|
+
#### `slice_by_index(ds, freq_slice=None, time_slice=None) -> DynamicSpectrum`
|
|
602
|
+
|
|
603
|
+
Slice a spectrum by array indices.
|
|
604
|
+
|
|
605
|
+
| Parameter | Type | Description |
|
|
606
|
+
|-----------|------|-------------|
|
|
607
|
+
| `ds` | `DynamicSpectrum` | Input spectrum |
|
|
608
|
+
| `freq_slice` | `slice \| None` | Slice for frequency axis |
|
|
609
|
+
| `time_slice` | `slice \| None` | Slice for time axis |
|
|
610
|
+
|
|
611
|
+
---
|
|
612
|
+
|
|
613
|
+
### Combine Functions
|
|
614
|
+
|
|
615
|
+
#### `can_combine_frequency(path1, path2, time_atol=0.01) -> bool`
|
|
616
|
+
|
|
617
|
+
Check if two files can be combined along the frequency axis.
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
#### `combine_frequency(path1, path2) -> DynamicSpectrum`
|
|
622
|
+
|
|
623
|
+
Combine two spectra vertically (frequency stacking).
|
|
624
|
+
|
|
625
|
+
---
|
|
626
|
+
|
|
627
|
+
#### `can_combine_time(paths, freq_atol=0.01) -> bool`
|
|
628
|
+
|
|
629
|
+
Check if files can be combined along the time axis.
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
#### `combine_time(paths) -> DynamicSpectrum`
|
|
634
|
+
|
|
635
|
+
Concatenate spectra horizontally (time concatenation).
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
### Plotting Functions
|
|
640
|
+
|
|
641
|
+
#### `plot_dynamic_spectrum(ds, title="...", cmap="inferno", figsize=None, vmin=None, vmax=None, ax=None, show_colorbar=True, time_format="seconds", **imshow_kwargs)`
|
|
642
|
+
|
|
643
|
+
Plot a dynamic spectrum with full customization.
|
|
644
|
+
|
|
645
|
+
| Parameter | Type | Default | Description |
|
|
646
|
+
|-----------|------|---------|-------------|
|
|
647
|
+
| `ds` | `DynamicSpectrum` | — | Spectrum to plot |
|
|
648
|
+
| `title` | `str` | `"Dynamic Spectrum"` | Plot title |
|
|
649
|
+
| `cmap` | `str` | `"inferno"` | Matplotlib colormap |
|
|
650
|
+
| `figsize` | `tuple \| None` | `None` | Figure size as `(width, height)` in inches |
|
|
651
|
+
| `vmin` | `float \| None` | `None` | Colormap lower bound (clipping) |
|
|
652
|
+
| `vmax` | `float \| None` | `None` | Colormap upper bound (clipping) |
|
|
653
|
+
| `ax` | `Axes \| None` | `None` | Existing axes (creates new if `None`) |
|
|
654
|
+
| `show_colorbar` | `bool` | `True` | Whether to display colorbar |
|
|
655
|
+
| `time_format` | `str` | `"seconds"` | `"seconds"` or `"ut"` for time axis format |
|
|
656
|
+
| `**imshow_kwargs` | — | — | Additional kwargs passed to `matplotlib.imshow()` |
|
|
657
|
+
|
|
658
|
+
**Returns:** Tuple of `(fig, ax, im)`.
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
#### `plot_raw_spectrum(ds, title="Raw Spectrum", cmap="viridis", ...)`
|
|
663
|
+
|
|
664
|
+
Plot raw spectrum data without any processing. Accepts the same parameters as `plot_dynamic_spectrum`.
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
#### `plot_background_subtracted(ds, title="Background Subtracted", cmap="RdBu_r", ...)`
|
|
669
|
+
|
|
670
|
+
Plot spectrum after background subtraction (before clipping). Automatically applies `background_subtract()` and plots the result. Accepts the same parameters as `plot_dynamic_spectrum`.
|
|
671
|
+
|
|
672
|
+
---
|
|
673
|
+
|
|
674
|
+
#### `TimeAxisConverter`
|
|
675
|
+
|
|
676
|
+
Convert between elapsed seconds and UT time.
|
|
677
|
+
|
|
678
|
+
```python
|
|
679
|
+
@dataclass
|
|
680
|
+
class TimeAxisConverter:
|
|
681
|
+
ut_start_sec: float # UT observation start in seconds since midnight
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
| Method | Description |
|
|
685
|
+
|--------|-------------|
|
|
686
|
+
| `seconds_to_ut(seconds)` | Convert elapsed seconds to UT string (HH:MM:SS) |
|
|
687
|
+
| `ut_to_seconds(ut_str)` | Convert UT string to elapsed seconds |
|
|
688
|
+
| `from_dynamic_spectrum(ds)` | Create converter from spectrum metadata |
|
|
689
|
+
|
|
690
|
+
---
|
|
691
|
+
|
|
692
|
+
### Exceptions
|
|
693
|
+
|
|
694
|
+
The library provides a hierarchy of custom exceptions for robust error handling:
|
|
695
|
+
|
|
696
|
+
| Exception | Description |
|
|
697
|
+
|-----------|-------------|
|
|
698
|
+
| `ECallistoError` | Base exception for all library errors |
|
|
699
|
+
| `InvalidFITSError` | Raised when a FITS file is invalid or cannot be read |
|
|
700
|
+
| `InvalidFilenameError` | Raised when a filename doesn't match e-CALLISTO naming convention |
|
|
701
|
+
| `DownloadError` | Raised when downloading files from the archive fails |
|
|
702
|
+
| `CombineError` | Raised when spectra cannot be combined |
|
|
703
|
+
| `CropError` | Raised when cropping parameters are invalid |
|
|
704
|
+
|
|
705
|
+
#### Error Handling Example
|
|
706
|
+
|
|
707
|
+
```python
|
|
708
|
+
import ecallistolib as ecf
|
|
709
|
+
from ecallistolib import InvalidFITSError, CropError
|
|
710
|
+
|
|
711
|
+
try:
|
|
712
|
+
spectrum = ecf.read_fits("corrupted_file.fit")
|
|
713
|
+
except FileNotFoundError:
|
|
714
|
+
print("File not found")
|
|
715
|
+
except InvalidFITSError as e:
|
|
716
|
+
print(f"Invalid FITS file: {e}")
|
|
717
|
+
|
|
718
|
+
try:
|
|
719
|
+
cropped = ecf.crop(spectrum, freq_range=(1000, 2000)) # Out of range
|
|
720
|
+
except CropError as e:
|
|
721
|
+
print(f"Cropping failed: {e}")
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## Examples
|
|
727
|
+
|
|
728
|
+
### Complete Workflow
|
|
729
|
+
|
|
730
|
+
```python
|
|
731
|
+
from datetime import date
|
|
732
|
+
import ecallistolib as ecf
|
|
733
|
+
import matplotlib.pyplot as plt
|
|
734
|
+
|
|
735
|
+
# 1. Download data
|
|
736
|
+
remote = ecf.list_remote_fits(date(2023, 6, 15), hour=12, station_substring="alaska")
|
|
737
|
+
paths = ecf.download_files(remote[:2], out_dir="./data")
|
|
738
|
+
|
|
739
|
+
# 2. Read and combine
|
|
740
|
+
if ecf.can_combine_time(paths):
|
|
741
|
+
spectrum = ecf.combine_time(paths)
|
|
742
|
+
else:
|
|
743
|
+
spectrum = ecf.read_fits(paths[0])
|
|
744
|
+
|
|
745
|
+
# 3. Process
|
|
746
|
+
cleaned = ecf.noise_reduce_mean_clip(spectrum)
|
|
747
|
+
|
|
748
|
+
# 4. Plot
|
|
749
|
+
fig, ax, im = ecf.plot_dynamic_spectrum(
|
|
750
|
+
cleaned,
|
|
751
|
+
title=f"e-CALLISTO Observation - {spectrum.meta.get('station', 'Unknown')}",
|
|
752
|
+
cmap="plasma"
|
|
753
|
+
)
|
|
754
|
+
plt.savefig("observation.png", dpi=200)
|
|
755
|
+
plt.show()
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Working with Metadata
|
|
759
|
+
|
|
760
|
+
```python
|
|
761
|
+
import ecallistolib as ecf
|
|
762
|
+
|
|
763
|
+
spectrum = ecf.read_fits("my_file.fit.gz")
|
|
764
|
+
|
|
765
|
+
# Access metadata
|
|
766
|
+
print(f"Station: {spectrum.meta.get('station')}")
|
|
767
|
+
print(f"Date: {spectrum.meta.get('date')}")
|
|
768
|
+
print(f"UT Start: {spectrum.meta.get('ut_start_sec')} seconds")
|
|
769
|
+
|
|
770
|
+
# After processing, metadata is preserved and extended
|
|
771
|
+
processed = ecf.noise_reduce_mean_clip(spectrum)
|
|
772
|
+
print(f"Processing applied: {processed.meta.get('noise_reduction')}")
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
---
|
|
776
|
+
|
|
777
|
+
## Data Format
|
|
778
|
+
|
|
779
|
+
e-CALLISTO FITS files follow a standard naming convention:
|
|
780
|
+
|
|
781
|
+
```
|
|
782
|
+
STATION_YYYYMMDD_HHMMSS_NN.fit.gz
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
| Field | Description |
|
|
786
|
+
|-------|-------------|
|
|
787
|
+
| `STATION` | Observatory name (e.g., `ALASKA`, `GLASGOW`) |
|
|
788
|
+
| `YYYYMMDD` | Observation date |
|
|
789
|
+
| `HHMMSS` | Observation start time (UTC) |
|
|
790
|
+
| `NN` | Focus/channel number (typically `01` or `02`) |
|
|
791
|
+
|
|
792
|
+
The FITS files contain:
|
|
793
|
+
- **Primary HDU**: 2D array of intensity values
|
|
794
|
+
- **Extension 1**: Binary table with `frequency` and `time` axes
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## Contributing
|
|
799
|
+
|
|
800
|
+
Contributions are welcome! Please feel free to submit issues or pull requests.
|
|
801
|
+
|
|
802
|
+
1. Fork the repository
|
|
803
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
804
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
805
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
806
|
+
5. Open a Pull Request
|
|
807
|
+
|
|
808
|
+
### Running Tests
|
|
809
|
+
|
|
810
|
+
```bash
|
|
811
|
+
pip install pytest
|
|
812
|
+
pytest
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## License
|
|
818
|
+
|
|
819
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
## Acknowledgments
|
|
824
|
+
|
|
825
|
+
- [e-CALLISTO Network](http://www.e-callisto.org/) for providing open access to solar radio data
|
|
826
|
+
- [Astropy](https://www.astropy.org/) for FITS file handling
|
|
827
|
+
|
|
828
|
+
---
|
|
829
|
+
|
|
830
|
+
## Links
|
|
831
|
+
|
|
832
|
+
- **e-CALLISTO Data Archive**: http://soleil80.cs.technik.fhnw.ch/solarradio/data/2002-20yy_Callisto/
|
|
833
|
+
- **e-CALLISTO Homepage**: http://www.e-callisto.org/
|