Skip to content

Raw IQ Pipeline — raw_parse

P pynasonde.digisonde.raw.raw_parse — full sounding pipeline (IQ → correlation → ionogram) and the IonogramResult container.


IonogramResult

C Dataclass that holds all arrays produced by a single Digisonde sounding.

pynasonde.digisonde.raw.raw_parse.IonogramResult dataclass

Container for a single processed Digisonde sounding.

All arrays are in physical units. Use :meth:to_netcdf to write the standard compressed NetCDF product, or :meth:to_xarray to get an in-memory xarray.Dataset for interactive analysis.

Attributes

frequency_hz

RF frequencies at which the sounding was taken, shape (n_freqs,), in Hz.

range_km

Virtual heights (group range) corresponding to each range gate, shape (n_ranges,), in km. The 220 µs Digisonde hardware delay has already been subtracted.

time_unix

Unix timestamp of the first pulse for each sounding frequency, shape (n_freqs,).

power_o

Ordinary-mode receive power (linear, arbitrary units), shape (n_freqs, n_ranges).

power_x

Extraordinary-mode receive power (linear, arbitrary units), same shape. All zeros when Polarization != "O and X".

phase_o

Ordinary-mode phase at the Doppler peak (radians), or None when Save Phase was not requested.

phase_x

Extraordinary-mode phase at the Doppler peak (radians), or None.

program_id

Identifier string from the program dictionary (e.g. "DPS4D_Kirtland0").

epoch

UTC epoch of the sounding.

frequency_mhz: np.ndarray property

Frequency axis in MHz.

power_total: np.ndarray property

Sum of O- and X-mode power (linear).

power_db(mode='total')

Return SNR in dB, normalised so the median equals 0 dB.

Parameters
mode

"total" (default), "O", or "X".

Returns

np.ndarray Array clipped to [0, 255] dB, shape (n_freqs, n_ranges).

to_xarray()

Return the ionogram as an xarray.Dataset.

The dataset has dimensions (frequency, range) with frequency in MHz and range in km. Both linear power arrays (O- and X-mode) and the normalised dB power are included as data variables.

Returns

xarray.Dataset

to_netcdf(path)

Write the ionogram to a compressed NetCDF4 file.

Parameters
path

Destination file path (parent directories must exist).


Functions

M process — run the full sounding pipeline and return an IonogramResult.

pynasonde.digisonde.raw.raw_parse.process(program, dir_iq='/mnt/Data/', out_dir='out/', min_range=-math.inf, max_range=math.inf, nc_flag=True, verbose=True)

Run a single Digisonde sounding program and return an :class:IonogramResult.

The pipeline mirrors the Julia Digisonde.process function:

  1. Open the IQ stream via :class:~pynasonde.digisonde.raw.iq_reader.IQStream.
  2. For every (frequency, polarisation, repeat, complementary-code) tuple:

a. Read n_samples raw IQ samples for the pulse's sub-time. b. FFT → frequency-shift to baseband + decimate to chip bandwidth. c. IFFT → optional fine-frequency residual mix. d. Interpolate onto the 30 kHz chip grid. e. Correlate with the reversed phase code via FFT convolution. f. Accumulate into the CIT voltage array.

  1. Vectorised Doppler FFT across all range gates → peak power and phase.
  2. Optionally write a compressed NetCDF product.

Parameters

program

Dictionary of sounding-program parameters. Required keys:

  • "Epoch" – :class:datetime.datetime or Unix timestamp
  • "ID" – string identifier written into the NetCDF
  • "Freq Stepping Law" – must be "linear"
  • "Wave Form" – must be "16-chip complementary"
  • "Lower Freq Limit" / "Upper Freq Limit" – Hz
  • "Coarse Freq Step" / "Fine Freq step" – Hz
  • "Number of Fine Steps" – int
  • "Fine Multiplexing" – bool
  • "Inter-Pulse Period" – seconds (IPP)
  • "Number of Integrated Repeats" – int (nRep)
  • "Interpulse Phase Switching" – bool
  • "Polarization""O and X" or "O"

Optional keys: "rxTag" (default "ch0"), "Save Phase" (default False), "FFTMode" (default False).

dir_iq

Root directory containing the time-partitioned IQ recordings.

out_dir

Root directory for NetCDF output. A sub-path <hostname>/<ID>/<YYYY-mm-dd>/ is created automatically.

min_range, max_range: Reserved for future range-gate masking (not yet applied).

nc_flag

Write the NetCDF product when True (default).

verbose

Emit progress messages via loguru.

Returns

IonogramResult or None None only if the output file already existed and was skipped.

Notes

Integration depth and file count — see module docstring for the full table. For the default Kirtland schedule (IPP = 10 ms, nRep = 8, O+X, 2–15 MHz at 30 kHz steps) each call reads ≈139 one-second .bin files and takes about 139 s of wall-clock IQ data. The 12-minute cadence in :func:main leaves margin before the next sounding. Reducing nRep proportionally shortens the sounding and trades SNR (scales as √nRep).

M read_pulse — read raw IQ for a single pulse without running the full pipeline.

pynasonde.digisonde.raw.raw_parse.read_pulse(program, dir_iq, coarse_index, fine_index=0, pol_index=0, rep_index=0, comp_index=0)

Read the raw complex IQ samples for one specific pulse.

This is the entry point for inspecting individual pulses without running the full sounding pipeline. The returned samples are in the ADC's native baseband (full receiver bandwidth); use the FFT-domain mixing logic in :func:process to tune to a narrower band around a specific frequency.

Parameters

program

Same program dictionary passed to :func:process.

dir_iq

Root directory of the IQ recording tree.

coarse_index

Which coarse-frequency step to read (0-based).

fine_index

Fine-frequency sub-step within the coarse step (0 for most programs).

pol_index

Polarisation index: 0 = O-mode, 1 = X-mode.

rep_index

Repeat (integration) index within the CIT, 0 … nRep-1.

comp_index

Complementary-code index: 0 = code A, 1 = code B.

Returns

np.ndarray Complex64 array of length n_samples (next power-of-two of IPP × f_sample).

Examples

samples = read_pulse(program, "/media/data", coarse_index=0) import matplotlib.pyplot as plt plt.plot(samples.real[:500])


Quick start

import datetime as dt
from pynasonde.digisonde.raw.raw_parse import process

program = {
    "Epoch": dt.datetime(2023, 10, 14, 16, 0, tzinfo=dt.timezone.utc),
    "ID": "DPS4D_Kirtland0",
    "FFTMode": False,
    "rxTag": "ch0",
    "Save Phase": False,
    "Freq Stepping Law": "linear",
    "Lower Freq Limit": 2e6,
    "Upper Freq Limit": 15e6,
    "Coarse Freq Step": 30e3,
    "Number of Fine Steps": 1,
    "Fine Freq step": 5e3,
    "Fine Multiplexing": False,
    "Inter-Pulse Period": 10e-3,
    "Number of Integrated Repeats": 8,
    "Interpulse Phase Switching": False,
    "Wave Form": "16-chip complementary",
    "Polarization": "O and X",
}

result = process(program, dir_iq="/media/chakras4/69F9D939661D263B", out_dir="out/")
if result is not None:
    print(result.frequency_mhz)          # (n_freqs,) array in MHz
    ds = result.to_xarray()              # xarray.Dataset
    result.to_netcdf("out/ionogram.nc")  # write to disk

Accessing raw IQ for a single pulse

from pynasonde.digisonde.raw.raw_parse import read_pulse

iq = read_pulse(
    program,
    dir_iq="/media/chakras4/69F9D939661D263B",
    coarse_index=10,   # 10th coarse frequency step
    pol_index=0,       # O-mode
    rep_index=0,       # first repeat
    comp_index=0,      # complementary code A
)
# iq is a np.ndarray of np.complex64, length = file_size (≈ sample rate)