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.
@@ -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
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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/