Skip to main content

Tools to download, read, process, and plot e-CALLISTO FITS dynamic spectra.

Project description

ecallistolib

Python 3.10-3.14 License: MIT

A Python library to download, read, process, and plot e-CALLISTO FITS dynamic spectra.

e-CALLISTO (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.


🆕 What's New in v1.2.0

  • Timestamp-aware time combinationcombine_time(..., timeline="actual") preserves real offsets between observation segments.
  • Observation datetime metadataread_fits() now records observation_start / observation_end, with DynamicSpectrum.start_datetime and .end_datetime convenience properties.
  • Richer time-combine metadata — merged spectra now record meta["combined"]["timeline"] and segment_offsets_s.
  • Download reliability + ergonomicsdownload_files(..., workers=..., retries=..., retry_backoff_s=..., overwrite=...).
  • Plotting UX upgradesclip_percentiles=(low, high) and direct export via save_path + savefig_kwargs.
  • Runtime support expansion — officially supported Python versions are now 3.10-3.14.
  • Stability-first defaults preservedcombine_time() still defaults to contiguous legacy-compatible behavior unless you opt into timeline="actual".

Table of Contents


Features

  • 📥 Download – List and download FITS files directly from the e-CALLISTO data archive
  • 📖 Read – Parse e-CALLISTO FITS files (.fit, .fit.gz) into structured Python objects
  • 🕓 Observation Datetimes – Preserve absolute start/end timestamps when FITS headers or filenames provide them
  • 🔧 Process – Apply noise reduction techniques (mean subtraction, clipping, scaling)
  • ✂️ Crop – Extract frequency and time ranges from spectra
  • 🔗 Combine – Merge multiple spectra along the time or frequency axis, including timestamp-aware time merges
  • 📊 Plot – Generate publication-ready dynamic spectrum visualizations
  • 🕒 Time Precision Control – Convert seconds to UT in HH:MM or HH:MM:SS
  • Efficient I/O – Stream downloads and optimize multi-day remote listing queries
  • 🛡️ Reliability Enhancements – Stricter parsing, typed combine failures, and safer metadata copying
  • ⚠️ Error Handling – Custom exceptions for robust error management

Installation

Supported Python versions: 3.10-3.14.

From PyPI (Stable)

pip install ecallistolib

Optional Dependencies

Install optional features as needed:

pip install ecallistolib"[download,plot]"

From Source (Development)

git clone https://github.com/saandev/ecallistolib.git
cd ecallistolib
pip install -e .

Optional Dependencies

Install optional features as needed:

# For downloading data from the e-CALLISTO archive
pip install -e ".[download]"

# For plotting
pip install -e ".[plot]"

# Install all optional dependencies
pip install -e ".[download,plot]"

Quick Start

import ecallistolib as ecl

# Read a FITS file
spectrum = ecl.read_fits("ALASKA_20230101_120000_01.fit.gz")

# Plot with different processing modes
fig, ax, im = ecl.plot_dynamic_spectrum(
    spectrum, 
    process="noise_reduced",
    clip_low=-5, 
    clip_high=20,
    title="Solar Radio Burst"
)

Usage Guide

Reading FITS Files

The library can read standard e-CALLISTO FITS files:

import ecallistolib as ecl

# Read a single FITS file
spectrum = ecl.read_fits("path/to/STATION_YYYYMMDD_HHMMSS_NN.fit.gz")

# Access the data
print(f"Data shape: {spectrum.shape}")          # (n_freq, n_time)
print(f"Frequencies: {spectrum.freqs_mhz}")     # Frequency axis in MHz
print(f"Time samples: {spectrum.time_s}")       # Time axis in seconds
print(f"Source file: {spectrum.source}")        # Original file path
print(f"Metadata: {spectrum.meta}")             # Station, date, etc.

# New in v1.0.0: Convenience properties
print(f"Num frequencies: {spectrum.n_freq}")    # Number of frequency channels
print(f"Num time samples: {spectrum.n_time}")   # Number of time samples
print(f"Duration: {spectrum.duration_s} s")     # Total observation duration
print(f"Freq range: {spectrum.freq_range_mhz}") # (min, max) frequency in MHz

# New in v1.2.0: Absolute observation datetimes when available
print(f"Observation start: {spectrum.start_datetime}")
print(f"Observation end: {spectrum.end_datetime}")

Parsing Filenames

Extract metadata from e-CALLISTO filenames:

parts = ecl.parse_callisto_filename("ALASKA_20230615_143000_01.fit.gz")

print(parts.station)        # "ALASKA"
print(parts.date_yyyymmdd)  # "20230615"
print(parts.time_hhmmss)    # "143000"
print(parts.focus)          # "01"

Downloading Data

Download FITS files directly from the e-CALLISTO archive:

from datetime import date
import ecallistolib as ecl

# List available files for a specific day, hour, and station
remote_files = ecl.list_remote_fits(
    day=date(2023, 6, 15),
    hour=14,                    # UTC hour (0-23)
    station_substring="alaska"  # Case-insensitive station filter
)

print(f"Found {len(remote_files)} files:")
for rf in remote_files:
    print(f"  - {rf.name}: {rf.url}")

# Download the files
saved_paths = ecl.download_files(remote_files, out_dir="./data")

for path in saved_paths:
    print(f"Downloaded: {path}")

You can also enable retries, parallel workers, and overwrite behavior:

saved_paths = ecl.download_files(
    remote_files,
    out_dir="./data",
    workers=4,               # Parallel downloads
    retries=2,               # Retry transient failures
    retry_backoff_s=0.5,     # Exponential backoff base
    overwrite="skip",        # "replace" (default), "skip", or "error"
)

Querying Multiple Days

List files over a date range with list_remote_fits_range (new in v1.0.0):

from datetime import date
import ecallistolib as ecl

# List files from June 1-3, 2023, during hours 12-14 UTC
remote_files = ecl.list_remote_fits_range(
    start_date=date(2023, 6, 1),
    end_date=date(2023, 6, 3),
    hours=[12, 13, 14],          # Optional: specific UTC hours
    station_substring="alaska",
    error_policy="skip"          # "skip" (default) or "raise"
)

print(f"Found {len(remote_files)} files across 3 days")

Error handling behavior can be configured:

# Raise immediately if any day listing fails:
remote_files = ecl.list_remote_fits_range(
    start_date=date(2023, 6, 1),
    end_date=date(2023, 6, 3),
    error_policy="raise",
)

Processing Data

Noise Reduction

Apply mean-subtraction and clipping to enhance signal visibility:

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Apply noise reduction with required clipping values
cleaned = ecl.noise_reduce_mean_clip(
    spectrum,
    clip_low=-5.0,              # Lower clipping threshold (required)
    clip_high=20.0,             # Upper clipping threshold (required)
    scale=2500.0 / 255.0 / 25.4 # Scaling factor (None to disable)
)

# Processing metadata is recorded
print(cleaned.meta["noise_reduction"])
# {'method': 'mean_subtract_clip', 'clip_low': -5.0, 'clip_high': 20.0, 'scale': 3.88...}

Algorithm Details:

  1. Subtract the mean intensity over time for each frequency channel (removes baseline)
  2. Clip values to the specified range
  3. Apply optional scaling factor

Background Subtraction Only

If you want to visualize the result before clipping is applied:

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Apply only background subtraction (no clipping)
bg_subtracted = ecl.background_subtract(spectrum)

# This is equivalent to the first step of noise_reduce_mean_clip
# Each frequency channel now has zero mean

Median-Based Noise Reduction (v1.0.0)

For data with outliers, use median-based subtraction which is more robust:

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Use median instead of mean (more robust to outliers)
cleaned = ecl.noise_reduce_median_clip(
    spectrum,
    clip_low=-5.0,
    clip_high=20.0
)

# Metadata shows the method used
print(cleaned.meta["noise_reduction"]["method"])  # 'median_subtract_clip'

Cropping & Slicing

Extract specific frequency or time ranges from a spectrum:

Crop by Physical Values

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Crop to specific frequency range (in MHz)
cropped = ecl.crop_frequency(spectrum, freq_min=100, freq_max=300)

# Crop to specific time range (in seconds)
cropped = ecl.crop_time(spectrum, time_min=10, time_max=60)

# Crop both axes at once
cropped = ecl.crop(spectrum, freq_range=(100, 300), time_range=(10, 60))

Slice by Array Index

# Get first 100 frequency channels
sliced = ecl.slice_by_index(spectrum, freq_slice=slice(0, 100))

# Get every other time sample (downsampling)
sliced = ecl.slice_by_index(spectrum, time_slice=slice(None, None, 2))

# Combine slices
sliced = ecl.slice_by_index(spectrum, freq_slice=slice(50, 150), time_slice=slice(0, 500))

Cropping Preserves Metadata

cropped = ecl.crop(spectrum, freq_range=(100, 200))

# Check what was cropped
print(cropped.meta["cropped"])
# {'frequency': {'min': 100, 'max': 200}}

Combining Spectra

Combine Along Frequency (Vertical Stacking)

Combine two spectra from the same observation but different frequency bands (e.g., focus 01 and 02):

import ecallistolib as ecl

# Check if files can be combined
if ecl.can_combine_frequency("file_01.fit.gz", "file_02.fit.gz"):
    combined = ecl.combine_frequency("file_01.fit.gz", "file_02.fit.gz")
    print(f"Combined shape: {combined.shape}")

Requirements for frequency combination:

  • Same station, date, and time
  • Different focus numbers (01 vs 02)
  • Matching time axes

Combine Along Time (Horizontal Concatenation)

Concatenate multiple spectra recorded consecutively:

import ecallistolib as ecl

files = [
    "ALASKA_20230615_140000_01.fit.gz",
    "ALASKA_20230615_141500_01.fit.gz",
    "ALASKA_20230615_143000_01.fit.gz",
]

# Check compatibility
if ecl.can_combine_time(files):
    combined = ecl.combine_time(files, timeline="actual")
    print(f"Combined shape: {combined.shape}")
    print(f"Total duration: {combined.time_s[-1] - combined.time_s[0]:.1f} seconds")
    print(f"Timeline mode: {combined.meta['combined']['timeline']}")

Use timeline="actual" to preserve the real offsets between segment start times from FITS headers or e-CALLISTO filenames:

combined = ecl.combine_time(files, timeline="actual")
print(combined.meta["combined"]["segment_offsets_s"])
# [0.0, 900.0, 1800.0] for 15-minute segment spacing

If you prefer a gap-free synthetic timeline, combine_time() still defaults to contiguous behavior. For edge cases where segment-local time axes do not start at zero, use normalized alignment to avoid over-shifting:

combined = ecl.combine_time(
    files,
    timeline="contiguous",
    normalize_segment_time=True,  # Opt-in corrected segment alignment
    freq_atol=0.02,               # Frequency compatibility tolerance
)

Requirements for time combination:

  • Same station, date, and focus
  • Matching frequency axes

Plotting

Create dynamic spectrum visualizations with selectable processing modes:

import ecallistolib as ecl
import matplotlib.pyplot as plt

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Plot raw spectrum
fig, ax, im = ecl.plot_dynamic_spectrum(spectrum, process="raw")
plt.show()

# Plot noise-reduced spectrum with required clipping values
fig, ax, im = ecl.plot_dynamic_spectrum(
    spectrum,
    process="noise_reduced",     # Apply noise reduction
    clip_low=-5,                  # Lower clipping bound (required)
    clip_high=20,                 # Upper clipping bound (required)
    title="Type III Solar Burst",
    cmap="magma",
    figsize=(12, 6),
    interpolation="bilinear"
)
plt.savefig("spectrum.png", dpi=150, bbox_inches="tight")

You can also derive clip bounds from percentiles and save directly:

fig, ax, im = ecl.plot_dynamic_spectrum(
    spectrum,
    process="noise_reduced",
    clip_percentiles=(5, 99),     # Used when clip_low/high are not provided
    save_path="plots/spectrum.png",
    savefig_kwargs={"dpi": 180, "bbox_inches": "tight"},
)

Plotting Raw Data

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Plot raw spectrum without any processing
fig, ax, im = ecl.plot_raw_spectrum(
    spectrum,
    title="Raw Spectrum",
    cmap="viridis",
    figsize=(10, 5)
)

Plotting Background Subtracted (Before Clipping)

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Plot after background subtraction but before clipping
fig, ax, im = ecl.plot_background_subtracted(
    spectrum,
    clip_low=-10,
    clip_high=30,
    cmap="RdBu_r"  # Diverging colormap for +/- values
)

Time Axis Formats

Display time in seconds or Universal Time (UT):

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Default: time in seconds
ecl.plot_dynamic_spectrum(spectrum, time_format="seconds")

# Time in UT format (HH:MM)
ecl.plot_dynamic_spectrum(spectrum, time_format="ut")

Intensity Units

Choose between raw digital values (Digits/ADU) or pseudo-calibrated dB:

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Default: intensity in Digits (raw ADU values)
ecl.plot_dynamic_spectrum(spectrum, intensity_units="digits")

# Convert to dB using: dB = Digits * 0.384 (pseudo-calibration)
ecl.plot_dynamic_spectrum(spectrum, intensity_units="dB")

Note: The dB conversion uses the formula: dB = Digits × 2500 / 256 / 25.4 ≈ Digits × 0.384

Time Axis Converter

Convert between elapsed seconds and UT time programmatically:

import ecallistolib as ecl

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Create converter from spectrum metadata
converter = ecl.TimeAxisConverter.from_dynamic_spectrum(spectrum)

# Convert seconds to UT (default minute precision)
print(converter.seconds_to_ut(100))    # "12:01"
print(converter.seconds_to_ut(3661))   # "13:01"

# Request second precision when needed
print(converter.seconds_to_ut(100, precision="second"))   # "12:01:40"
print(converter.seconds_to_ut(3661, precision="second"))  # "13:01:01"

# Convert UT to seconds
print(converter.ut_to_seconds("12:01:40"))  # 100.0
print(converter.ut_to_seconds("13:00:00"))  # 3600.0

Using a Custom Axes

import matplotlib.pyplot as plt
import ecallistolib as ecl

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

spectrum1 = ecl.read_fits("file1.fit.gz")
spectrum2 = ecl.read_fits("file2.fit.gz")

ecl.plot_dynamic_spectrum(spectrum1, process="raw", ax=axes[0], title="Raw")
ecl.plot_dynamic_spectrum(
    spectrum2, 
    process="noise_reduced",
    ax=axes[1], 
    title="Noise Reduced",
    clip_low=-5, clip_high=20
)

plt.tight_layout()
plt.show()

Light Curve Plotting

Plot intensity vs time at a specific frequency:

import ecallistolib as ecl
import matplotlib.pyplot as plt

spectrum = ecl.read_fits("my_spectrum.fit.gz")

# Plot raw light curve at 60 MHz
fig, ax, line = ecl.plot_light_curve(spectrum, frequency_mhz=60, process="raw")
plt.show()

# Plot background-subtracted light curve
fig, ax, line = ecl.plot_light_curve(
    spectrum, frequency_mhz=60, process="background_subtracted"
)
plt.show()

# Plot noise-reduced light curve (must provide clip values)
fig, ax, line = ecl.plot_light_curve(
    spectrum,
    frequency_mhz=60,
    process="noise_reduced",
    clip_low=-5,
    clip_high=20
)
plt.show()

Compare all three processing modes:

import ecallistolib as ecl
import matplotlib.pyplot as plt

spectrum = ecl.read_fits("my_spectrum.fit.gz")

fig, axes = plt.subplots(3, 1, figsize=(12, 10))

ecl.plot_light_curve(spectrum, 60, process="raw", ax=axes[0], title="Raw")
ecl.plot_light_curve(spectrum, 60, process="background_subtracted", ax=axes[1], title="BG Sub")
ecl.plot_light_curve(
    spectrum, 60, process="noise_reduced", ax=axes[2], title="Noise Reduced",
    clip_low=-5, clip_high=20
)

plt.tight_layout()
plt.show()

API Reference

DynamicSpectrum

The core data structure representing an e-CALLISTO dynamic spectrum.

@dataclass(frozen=True)
class DynamicSpectrum:
    data: np.ndarray           # Intensity data, shape (n_freq, n_time)
    freqs_mhz: np.ndarray      # Frequency axis in MHz, shape (n_freq,)
    time_s: np.ndarray         # Time axis in seconds, shape (n_time,)
    source: Optional[Path]     # Original file path
    meta: Mapping[str, Any]    # Metadata dictionary

Properties

Property Type Description
shape tuple[int, int] Returns (n_freq, n_time)
n_freq int Number of frequency channels
n_time int Number of time samples
duration_s float Total observation duration in seconds
start_datetime datetime | None Absolute observation start time in UTC when available
end_datetime datetime | None Absolute observation end time in UTC when available
freq_range_mhz tuple[float, float] Frequency range as (min, max) in MHz

Methods

Method Description
copy_with(**changes) Returns a new DynamicSpectrum with specified fields replaced

I/O Functions

read_fits(path: str | Path) -> DynamicSpectrum

Read an e-CALLISTO FITS file.

Parameter Type Description
path str | Path Path to the FITS file (.fit or .fit.gz)

Returns: DynamicSpectrum object with data, frequencies, time, and metadata.

Metadata may include:

  • station, date, time, focus
  • ut_start_sec
  • observation_start
  • observation_end

parse_callisto_filename(path: str | Path) -> CallistoFileParts

Parse an e-CALLISTO filename.

Returns: CallistoFileParts with attributes:

  • station – Station name
  • date_yyyymmdd – Date string
  • time_hhmmss – Time string
  • focus – Focus/channel number

Download Functions

list_remote_fits(day, hour, station_substring, base_url=..., timeout_s=10.0) -> List[RemoteFITS]

List available FITS files from the e-CALLISTO archive.

Parameter Type Description
day date Target date
hour int UTC hour (0–23)
station_substring str Case-insensitive station filter
base_url str Archive base URL (optional)
timeout_s float Request timeout in seconds

Returns: List of RemoteFITS objects with name and url attributes.


download_files(items, out_dir, timeout_s=30.0, chunk_size=1048576, workers=1, retries=0, retry_backoff_s=0.5, overwrite="replace") -> list[Path]

Download FITS files to a local directory.

Parameter Type Description
items Iterable[RemoteFITS] Files to download
out_dir str | Path Output directory
timeout_s float Request timeout per file
chunk_size int Streaming chunk size in bytes
workers int Parallel workers (1 keeps sequential behavior)
retries int Retry count for transient download failures
retry_backoff_s float Exponential backoff base in seconds
overwrite str "replace" (default), "skip", or "error" when file exists

Returns: List of saved file paths.


list_remote_fits_range(start_date, end_date, hours=None, station_substring="", error_policy="skip", ...) -> List[RemoteFITS]

List available FITS files over a date range (new in v1.0.0).

Parameter Type Description
start_date date Start date (inclusive)
end_date date End date (inclusive)
hours Iterable[int] | None UTC hours to include (0–23), or None for all
station_substring str Case-insensitive station filter
error_policy str "skip" to continue on failed days, "raise" to fail fast

Returns: List of RemoteFITS objects across the date range.


Processing Functions

noise_reduce_mean_clip(ds, clip_low=-5.0, clip_high=20.0, scale=...) -> DynamicSpectrum

Apply noise reduction via mean subtraction and clipping.

Parameter Type Default Description
ds DynamicSpectrum Input spectrum
clip_low float -5.0 Lower clipping threshold
clip_high float 20.0 Upper clipping threshold
scale float | None ~3.88 Scaling factor (None to disable)

Returns: New DynamicSpectrum with processed data and updated metadata.


background_subtract(ds) -> DynamicSpectrum

Subtract mean over time for each frequency channel (background subtraction only, no clipping).

Parameter Type Description
ds DynamicSpectrum Input spectrum

Returns: New DynamicSpectrum with background subtracted. Useful for visualizing data before clipping is applied.


noise_reduce_median_clip(ds, clip_low, clip_high, scale=...) -> DynamicSpectrum

Apply noise reduction via median subtraction and clipping (new in v1.0.0). More robust to outliers than mean-based method.

Parameter Type Default Description
ds DynamicSpectrum Input spectrum
clip_low float Lower clipping threshold
clip_high float Upper clipping threshold
scale float | None ~3.88 Scaling factor (None to disable)

Returns: New DynamicSpectrum with processed data and updated metadata.


Cropping Functions

crop_frequency(ds, freq_min=None, freq_max=None) -> DynamicSpectrum

Crop a spectrum to a frequency range.

Parameter Type Description
ds DynamicSpectrum Input spectrum
freq_min float | None Minimum frequency in MHz (inclusive)
freq_max float | None Maximum frequency in MHz (inclusive)

Raises: CropError if range is invalid or results in empty data.


crop_time(ds, time_min=None, time_max=None) -> DynamicSpectrum

Crop a spectrum to a time range.

Parameter Type Description
ds DynamicSpectrum Input spectrum
time_min float | None Minimum time in seconds
time_max float | None Maximum time in seconds

Raises: CropError if range is invalid or results in empty data.


crop(ds, freq_range=None, time_range=None) -> DynamicSpectrum

Crop a spectrum along both axes at once.

Parameter Type Description
ds DynamicSpectrum Input spectrum
freq_range tuple | None (min, max) frequency in MHz
time_range tuple | None (min, max) time in seconds

slice_by_index(ds, freq_slice=None, time_slice=None) -> DynamicSpectrum

Slice a spectrum by array indices.

Parameter Type Description
ds DynamicSpectrum Input spectrum
freq_slice slice | None Slice for frequency axis
time_slice slice | None Slice for time axis

Combine Functions

can_combine_frequency(path1, path2, time_atol=0.01) -> bool

Check if two files can be combined along the frequency axis.


combine_frequency(path1, path2) -> DynamicSpectrum

Combine two spectra vertically (frequency stacking).


can_combine_time(paths, freq_atol=0.01) -> bool

Check if files can be combined along the time axis.


combine_time(paths, timeline="contiguous", normalize_segment_time=False, freq_atol=0.01) -> DynamicSpectrum

Concatenate spectra horizontally (time concatenation).

Parameter Type Description
paths Iterable[str | Path] Input FITS files
timeline str "contiguous" (default) or "actual" to preserve real segment offsets
normalize_segment_time bool Applies only to timeline="contiguous"
freq_atol float Absolute tolerance for frequency-axis compatibility

Plotting Functions

plot_dynamic_spectrum(ds, process="raw", clip_low=None, clip_high=None, clip_percentiles=None, title=None, cmap="inferno", figsize=None, ax=None, show_colorbar=True, time_format="seconds", intensity_units="digits", save_path=None, savefig_kwargs=None, **imshow_kwargs)

Plot a dynamic spectrum with selectable processing mode.

Parameter Type Default Description
ds DynamicSpectrum Spectrum to plot
process str "raw" Processing mode: "raw", "background_subtracted", or "noise_reduced"
clip_low float | None None Lower clipping bound (must be paired with clip_high)
clip_high float | None None Upper clipping bound (must be paired with clip_low)
clip_percentiles tuple[float, float] | None None Percentile-based clipping when explicit clip bounds are absent
title str | None None Plot title (auto-generated if None)
cmap str "inferno" Matplotlib colormap
figsize tuple | None None Figure size as (width, height) in inches
ax Axes | None None Existing axes (creates new if None)
show_colorbar bool True Whether to display colorbar
time_format str "seconds" "seconds" or "ut" for time axis format
intensity_units str "digits" "digits" (raw ADU) or "dB" (pseudo-calibrated)
save_path str | Path | None None Optional output path to save the figure
savefig_kwargs dict | None None Optional kwargs passed to Figure.savefig
**imshow_kwargs Additional kwargs passed to matplotlib.imshow()

Returns: Tuple of (fig, ax, im).

Raises: ValueError for invalid clipping inputs or missing clipping source in process="noise_reduced".


plot_raw_spectrum(ds, clip_low=None, clip_high=None, clip_percentiles=None, title=None, cmap="viridis", save_path=None, savefig_kwargs=None, ...)

Convenience function that calls plot_dynamic_spectrum with process="raw".


plot_background_subtracted(ds, clip_low=None, clip_high=None, clip_percentiles=None, title=None, cmap="jet", save_path=None, savefig_kwargs=None, ...)

Convenience function that calls plot_dynamic_spectrum with process="background_subtracted".


plot_light_curve(ds, frequency_mhz, process="raw", title=None, figsize=None, ax=None, time_format="seconds", clip_low=None, clip_high=None, **plot_kwargs)

Plot a light curve (intensity vs time) at a specific frequency.

Parameter Type Default Description
ds DynamicSpectrum Spectrum to extract light curve from
frequency_mhz float Target frequency in MHz
process str "raw" Processing mode: "raw", "background_subtracted", or "noise_reduced"
title str | None None Plot title (auto-generated if None)
figsize tuple | None None Figure size as (width, height) in inches
ax Axes | None None Existing axes (creates new if None)
time_format str "seconds" "seconds" or "ut" for time axis format
clip_low float | None None Lower clip threshold (required for "noise_reduced")
clip_high float | None None Upper clip threshold (required for "noise_reduced")
**plot_kwargs Additional kwargs passed to matplotlib.plot()

Returns: Tuple of (fig, ax, line).

Raises:

  • FrequencyOutOfRangeError if frequency is outside spectrum's range.
  • ValueError if process="noise_reduced" without clip_low and clip_high.

TimeAxisConverter

Convert between elapsed seconds and UT time.

@dataclass
class TimeAxisConverter:
    ut_start_sec: float  # UT observation start in seconds since midnight
Method Description
seconds_to_ut(seconds, precision="minute") Convert elapsed seconds to UT string ("minute" -> HH:MM, "second" -> HH:MM:SS)
ut_to_seconds(ut_str) Convert UT string to elapsed seconds
from_dynamic_spectrum(ds) Create converter from spectrum metadata

Exceptions

The library provides a hierarchy of custom exceptions for robust error handling:

Exception Description
ECallistoError Base exception for all library errors
InvalidFITSError Raised when a FITS file is invalid or cannot be read
InvalidFilenameError Raised when a filename doesn't match e-CALLISTO naming convention
DownloadError Raised when downloading files from the archive fails
CombineError Raised when spectra cannot be combined
CropError Raised when cropping parameters are invalid
FrequencyOutOfRangeError Raised when the requested frequency is outside the spectrum's range

Error Handling Example

import ecallistolib as ecl
from ecallistolib import InvalidFITSError, CropError

try:
    spectrum = ecl.read_fits("corrupted_file.fit")
except FileNotFoundError:
    print("File not found")
except InvalidFITSError as e:
    print(f"Invalid FITS file: {e}")

try:
    cropped = ecl.crop(spectrum, freq_range=(1000, 2000))  # Out of range
except CropError as e:
    print(f"Cropping failed: {e}")

Examples

Complete Workflow

from datetime import date
import ecallistolib as ecl
import matplotlib.pyplot as plt

# 1. Download data
remote = ecl.list_remote_fits(date(2023, 6, 15), hour=12, station_substring="alaska")
paths = ecl.download_files(remote[:2], out_dir="./data")

# 2. Read and combine
if ecl.can_combine_time(paths):
    spectrum = ecl.combine_time(paths, timeline="actual")
else:
    spectrum = ecl.read_fits(paths[0])

# 3. Process
cleaned = ecl.noise_reduce_mean_clip(spectrum, clip_low=-5.0, clip_high=20.0)

# 4. Plot
fig, ax, im = ecl.plot_dynamic_spectrum(
    cleaned,
    title=f"e-CALLISTO Observation - {spectrum.meta.get('station', 'Unknown')}",
    cmap="plasma"
)
plt.savefig("observation.png", dpi=200)
plt.show()

Working with Metadata

import ecallistolib as ecl

spectrum = ecl.read_fits("my_file.fit.gz")

# Access metadata
print(f"Station: {spectrum.meta.get('station')}")
print(f"Date: {spectrum.meta.get('date')}")
print(f"UT Start: {spectrum.meta.get('ut_start_sec')} seconds")
print(f"Observation start: {spectrum.start_datetime}")
print(f"Observation end: {spectrum.end_datetime}")

# After processing, metadata is preserved and extended
processed = ecl.noise_reduce_mean_clip(spectrum, clip_low=-5.0, clip_high=20.0)
print(f"Processing applied: {processed.meta.get('noise_reduction')}")

Data Format

e-CALLISTO FITS files follow a standard naming convention:

STATION_YYYYMMDD_HHMMSS_NN.fit.gz
Field Description
STATION Observatory name (e.g., ALASKA, GLASGOW)
YYYYMMDD Observation date
HHMMSS Observation start time (UTC)
NN Focus/channel number (typically 01 or 02)

The FITS files contain:

  • Primary HDU: 2D array of intensity values
  • Extension 1: Binary table with frequency and time axes

Contributing

Contributions are welcome! Please feel free to submit issues or pull requests.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Running Tests

pip install pytest
pytest

License

This project is licensed under the MIT License. See the LICENSE file for details.


Acknowledgments


Links

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ecallistolib-1.2.0.tar.gz (57.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ecallistolib-1.2.0-py3-none-any.whl (32.0 kB view details)

Uploaded Python 3

File details

Details for the file ecallistolib-1.2.0.tar.gz.

File metadata

  • Download URL: ecallistolib-1.2.0.tar.gz
  • Upload date:
  • Size: 57.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for ecallistolib-1.2.0.tar.gz
Algorithm Hash digest
SHA256 f9bf4775fbcae1cdfe3aa27e741d2cf36750bbe404b30b0425b7e504c0c27a45
MD5 178f44a29a3a68c4cb6269be68859ef2
BLAKE2b-256 74500399dc723a8a9d90c9d39da7a5ab7442817d803e040af53b80a69ee548ba

See more details on using hashes here.

File details

Details for the file ecallistolib-1.2.0-py3-none-any.whl.

File metadata

  • Download URL: ecallistolib-1.2.0-py3-none-any.whl
  • Upload date:
  • Size: 32.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for ecallistolib-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e71dbbd4ec432ecfa9db69d82e7eb3ca8aac87a66c5e9d6f440b13da9eee8c07
MD5 ad3d4214e039f8961dabb782ef74f3d7
BLAKE2b-256 d3c7790f02adaa06cd69984d0cda1e78dc2fd5c60d37d6e014f03e75a5915b64

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page