aggregator.py — RiqAggregator¶
Multi-File Es Layer Imager
Loads multiple VIPIR RIQ files at a target frequency and produces a high-resolution Capon pseudospectrum RTI using two output modes: per-file (one column per file) and moving-average (sliding window of N consecutive files averaged together).
Source¶
pynasonde/vipir/analysis/es_imaging/aggregator.py
API reference¶
pynasonde.vipir.analysis.es_imaging.aggregator
¶
aggregator.py — Multi-file Es layer imager with per-file and moving-average modes.
Each RIQ file contains 4 pulses × 8 Rx channels at each sounding frequency. This module reduces that 3-D IQ cube to a single high-resolution Capon pseudospectrum per file, then optionally stacks or window-averages across files.
Processing strategy — multi-snapshot Capon¶
Instead of beamforming and then running Capon independently on each pulse,
every (pulse, Rx) pair is treated as an independent range-profile snapshot
and all L snapshots contribute to a single averaged covariance matrix R_f
before inversion
R_f = (1 / L·cols) Σ_{l=1}^{L} G_l · G_l^H
where G_l is the Hankel subband matrix of profile l and cols = V − Z + 1.
For "per_file" with n_pulse=4 and n_rx=8: L = 32 snapshots per column.
For "moving_avg" with window=8: L = 8 × 4 × 8 = 256 snapshots per column.
A better-conditioned R_f produces a dramatically cleaner Capon pseudospectrum, making weak Es echoes (typically 40–60 dB below the direct-wave clutter) visible in the normalised spectrum.
Output modes¶
"per_file"
One spectrum column per file. For 60 files → 60 columns in the RTI.
No cross-file averaging. Each column reflects only the 4 pulses and
8 Rx channels from that single 60-second sounding.
"moving_avg"
A sliding window of window consecutive file spectra is averaged
incoherently at each step of step files. For 60 files, window=8,
step=1 → 53 output columns, each averaging 8 × 4 = 32 effective
pulse-equivalents. This is the preferred mode for cleaner Es imaging
when a time resolution of step × 60 s is acceptable.
References¶
Liu, T., Yang, G., & Jiang, C. (2023). High-resolution sporadic E layer observation based on ionosonde using a cross-spectrum analysis imaging technique. Space Weather, 21, e2022SW003195.
RiqAggregator
¶
Multi-file Es layer imager.
Parameters¶
n_subbands
Capon Z parameter. Default 100.
resolution_factor
Capon K parameter (output grid = K × V bins). Default 10.
rx_weights
Reserved for future use. Currently all Rx channels are stacked as independent snapshots for the multi-snapshot covariance estimator rather than combined by beamforming.
gate_start_km
Height of the first range gate (km). Overridden from the RIQ
header when :meth:load is called. Default 0.0.
gate_spacing_km
Gate spacing r₀ (km). Overridden from the RIQ header when
:meth:load is called. Default 1.499 (VIPIR standard).
diagonal_loading
Capon covariance diagonal loading fraction ε. Default 1e-3.
output_mode
"per_file" — one spectrum column per file (slow RTI).
"moving_avg" — sliding-window average of window files.
window
Number of consecutive files to average per output column.
Only used when output_mode="moving_avg". Default 8.
step
Sliding-window step in files. step=1 → maximum overlap
(one new column per new file). Only used when
output_mode="moving_avg". Default 1.
blank_min_km
Heights below this value (km) are zeroed in each range profile
before the Capon covariance is computed. This suppresses the
direct-wave / ground-clutter spike (typically at the first 1–3
gates) so the ionospheric signal dominates the covariance matrix.
Set to 0.0 to disable. Default 60.0 km.
Examples¶
Per-file RTI from synthetic cubes (L = 4×8 = 32 profiles/column):
agg = RiqAggregator(n_subbands=50, resolution_factor=4, ... output_mode="per_file") cubes = [np.random.randn(4, 200, 8) + 1j*np.random.randn(4, 200, 8) ... for _ in range(10)] result = agg.combine(cubes) print(result.summary()) # n_snapshots=10
Moving-average RTI (window=8, step=1):
agg = RiqAggregator(n_subbands=50, resolution_factor=4, ... output_mode="moving_avg", window=8, step=1) result = agg.combine(cubes) print(result.summary()) # n_snapshots=3 (10-8+1=3)
Source code in pynasonde/vipir/analysis/es_imaging/aggregator.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 | |
combine(cubes)
¶
Produce an Es imaging result from a list of pre-loaded IQ cubes.
Parameters¶
list of complex ndarray
Each element has shape (n_pulse, n_gate) or
(n_pulse, n_gate, n_rx). All cubes must share the same
n_gate.
Returns¶
EsImagingResult
output_mode="per_file" → n_snapshots = len(cubes)
Each column uses L = n_pulse × n_rx profiles for covariance.
output_mode="moving_avg" → n_snapshots = (N - window)//step + 1
Each column uses L = window × n_pulse × n_rx profiles (e.g. 256)
for a single averaged covariance before Capon inversion.
Source code in pynasonde/vipir/analysis/es_imaging/aggregator.py
203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 | |
load(file_list, freq_target_khz, freq_tol_khz=50.0, vipir_version_idx=1)
¶
Load IQ cubes from RIQ files at a target sounding frequency.
Iterates over file_list, finds the pulset closest to
freq_target_khz in each file, and assembles the full
(pulse_count, gate_count, rx_count) complex IQ cube.
Also updates self.gate_start_km and self.gate_spacing_km from
the first file's RIQ header.
Parameters¶
file_list
Paths to .RIQ files.
freq_target_khz
Target sounding frequency in kHz.
freq_tol_khz
Maximum allowed frequency offset. Files whose closest pulset differs by more than this are skipped with a warning.
vipir_version_idx
Index into VIPIR_VERSION_MAP.configs. Default 1.
Returns¶
list of complex ndarray, shape (n_pulse, n_gate, n_rx) each.
Source code in pynasonde/vipir/analysis/es_imaging/aggregator.py
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 | |
fit(file_list, freq_target_khz, freq_tol_khz=50.0, vipir_version_idx=1)
¶
Load RIQ files and produce a combined Es imaging result.
Convenience wrapper around :meth:load + :meth:combine.
Source code in pynasonde/vipir/analysis/es_imaging/aggregator.py
RiqAggregator¶
Constructor parameters¶
| Parameter | Type | Default | Description |
|---|---|---|---|
n_subbands |
int |
100 |
Capon Z — passed to internal EsCaponImager |
resolution_factor |
int |
10 |
Capon K — output grid = K·V high-res bins |
rx_weights |
ndarray \| None |
None |
Reserved for future use. All Rx channels are currently stacked as independent snapshots for the multi-snapshot covariance estimator |
gate_start_km |
float |
0.0 |
Height of first gate (km). Overridden from RIQ header inside load() |
gate_spacing_km |
float |
1.499 |
Native gate spacing r₀ (km). Overridden from RIQ header inside load() |
diagonal_loading |
float |
1e-3 |
Capon ε regularisation |
output_mode |
str |
"per_file" |
"per_file" → one column per file; "moving_avg" → sliding-window average |
window |
int |
8 |
Number of files per averaged column (moving_avg only) |
step |
int |
1 |
Sliding step in files (moving_avg only) |
Methods¶
| Method | Signature | Description |
|---|---|---|
load |
load(file_list, freq_target_khz, freq_tol_khz=50.0, vipir_version_idx=1) → list[ndarray] |
Load IQ cubes from RIQ files; update gate geometry from first file's header |
combine |
combine(cubes) → EsImagingResult |
Produce an imaging result from pre-loaded cubes |
fit |
fit(file_list, freq_target_khz, freq_tol_khz=50.0, vipir_version_idx=1) → EsImagingResult |
Convenience: load() + combine() in one call |
Multi-snapshot Capon covariance¶
Instead of beamforming Rx channels and running Capon per pulse, the aggregator treats every (pulse, Rx) pair as an independent range-profile snapshot and averages all L covariance matrices before inverting once:
where G_l is the Hankel subband matrix (shape Z × cols, cols = V − Z + 1)
of profile l. This is the true multi-snapshot Capon estimator.
| Mode | L (snapshots per output column) |
|---|---|
"per_file" |
n_pulse × n_rx (e.g. 4 × 8 = 32) |
"moving_avg" |
window × n_pulse × n_rx (e.g. 8 × 4 × 8 = 256) |
A better-conditioned R_f produces a cleaner Capon pseudospectrum, making weak Es echoes (typically 40–60 dB below the direct-wave clutter) visible in the normalised output.
Output modes¶
output_mode |
pseudospectrum_db shape |
n_snapshots |
Use case |
|---|---|---|---|
"per_file" |
(n_files, K·V) |
n_files |
One column per file, ~1 min cadence RTI |
"moving_avg" |
((N-W)//S+1, K·V) |
(N-W)//S+1 |
Smoothed RTI; W files averaged per column |
Where N = number of files, W = window, S = step.
Usage¶
Per-file RTI¶
from pynasonde.vipir.analysis import RiqAggregator
import glob
file_list = sorted(glob.glob("data/20230601_01??.RIQ"))
agg = RiqAggregator(
n_subbands=100,
resolution_factor=10,
output_mode="per_file",
)
result = agg.fit(file_list, freq_target_khz=5000.0, vipir_version_idx=1)
print(result.summary())
# EsImagingResult: snapshots=60 Z=100 K=10 r₀=1.499 km → Δr=0.150 km
result.plot() # 60-column RTI
Moving-average RTI¶
agg = RiqAggregator(
n_subbands=100,
resolution_factor=10,
output_mode="moving_avg",
window=8, # average 8 consecutive files per column
step=1, # slide by 1 file → maximum temporal overlap
)
result = agg.fit(file_list, freq_target_khz=5000.0, vipir_version_idx=1)
# For 60 files, window=8, step=1 → 53 columns
print(result.n_snapshots) # 53
result.plot()
Separate load + combine¶
# Load cubes (gate geometry auto-read from first file header)
cubes = agg.load(file_list, freq_target_khz=5000.0)
print(f"r₀ = {agg.gate_spacing_km:.3f} km start = {agg.gate_start_km:.2f} km")
# Combine with chosen mode
result = agg.combine(cubes)
Choosing the target frequency for Es¶
Es echoes appear below foEs (the Es critical frequency). For summer daytime Es at Wallops Island (foEs typically 2–8 MHz), use 3000–4000 kHz:
# 3500 kHz is safely below typical summer foEs → Es is reflected, not transparent
agg = RiqAggregator(n_subbands=100, resolution_factor=10,
output_mode="moving_avg", window=8, step=1)
result = agg.fit(file_list, freq_target_khz=3500.0)
load() details¶
load() iterates over file_list, opens each RIQ file via RiqDataset, finds the
pulset whose frequency is closest to freq_target_khz, assembles the full
(n_pulse, n_gate, n_rx) complex IQ cube, and returns the list.
- Gate geometry (
gate_spacing_km,gate_start_km) is updated from the first successfully loaded file's SCT header (timing.gate_step,timing.gate_start). - Files whose closest pulset differs by more than
freq_tol_khzare skipped with a warning. - Files that cannot be read (IO error, wrong format) are skipped with a warning.
- Raises
RuntimeErrorif no valid cubes are loaded from any file.
See Also¶
References¶
Liu, T., Yang, G., & Jiang, C. (2023). Space Weather, 21, e2022SW003195. https://doi.org/10.1029/2022SW003195