Skip to content

canvod-grids

Purpose

The canvod-grids package provides spatial grid implementations for hemispheric GNSS signal analysis. It discretizes the hemisphere visible from a ground-based receiver into cells — a prerequisite for spatially resolved VOD estimation.


Grid Types

Seven implementations are available, all inheriting from BaseGridBuilder:

  •   EqualAreaBuilder (recommended)


    Cells of approximately equal solid angle. Ensures uniform spatial sampling across the hemisphere, avoiding zenith over-weighting.

  •   EqualAngleBuilder


    Regular angular spacing in θ and φ. Cells near zenith are smaller — appropriate when you want uniform angular resolution.

  •   EquirectangularBuilder


    Simple rectangular latitude/longitude grid. Fast to compute; strong area distortion at low elevations.

  •   HEALPixBuilder


    Hierarchical Equal Area isoLatitude Pixelization. All cells have identical areas; supports multi-resolution refinement. Requires healpy.

  •   GeodesicBuilder


    Icosahedron subdivision. Near-uniform cell area with triangular cell boundaries.

  •   FibonacciBuilder


    Fibonacci-sphere sampling. Aesthetically uniform point distribution; ideal for scatter-plot analyses. Requires scipy.

  •   HTMBuilder


    Hierarchical Triangular Mesh. Recursive triangular decomposition of the sphere; supports efficient spatial indexing.

All builders accept angular_resolution (degrees) and cutoff_theta (maximum polar angle) and return a GridData object.


Usage

from canvod.grids import create_hemigrid

grid = create_hemigrid("equal_area", angular_resolution=5.0)
print(grid.ncells)   # 216
print(grid.nbands)   # 18
from canvod.grids import EqualAreaBuilder

builder = EqualAreaBuilder(angular_resolution=5.0, cutoff_theta=10.0)
grid = builder.build()
from canvodpy import GridFactory

builder = GridFactory.create("equal_area", angular_resolution=5.0)
grid = builder.build()
print(grid.grid.head())  # Polars DataFrame with cell geometry

GridData

The GridData object returned by all builders provides:

Attribute Type Description
grid polars.DataFrame Cell geometry (boundaries, centres, solid angles)
ncells int Total number of grid cells
nbands int Number of elevation bands
definition str Human-readable grid description

Grid Operations

canvod.grids.operations handles the interface between grids and xarray Datasets:

  •   Cell Assignment


    add_cell_ids_to_ds_fast — KDTree-accelerated assignment of each observation to a grid cell. O(n log m) for n observations, m cells.

    add_cell_ids_to_vod_fast — Same for VOD datasets.

  •   Grid Persistence


    store_grid / load_grid — Save and reload grid definitions as Zarr/NetCDF for reproducible workflows.

    grid_to_dataset — Convert GridData to xarray Dataset.

  •   Aggregation


    aggregate_data_to_grid — Assign and aggregate per cell.

    compute_hemisphere_percell — Per-cell statistics.

    compute_zenith_percell — Zenith-weighted statistics.

    compute_percell_timeseries — Per-cell time series.


Analysis Subpackage

canvod.grids.analysis provides filtering, weighting, and pattern analysis:

Module Purpose
filtering Global IQR, Z-score, SID pattern filters
per_cell_filtering Per-cell variants of the above
hampel_filtering Hampel (median-MAD) outlier detection
sigma_clip_filter Numba-accelerated sigma-clipping
masking Spatial and temporal mask construction
weighting Per-cell weight calculators
solar Solar geometry (elevation, azimuth)
temporal Diurnal aggregation and analysis
spatial Per-cell spatial statistics
analysis_storage Persistent Icechunk storage for weights, masks, statistics, and per-cell timeseries

SIDPatternFilter

SIDPatternFilter selects observations by GNSS system, frequency band, and tracking code. It operates on the sid dimension where SIDs have the format SV|Band|Code (e.g. G01|L1|C).

from canvod.grids.analysis import SIDPatternFilter

filt = SIDPatternFilter(system="G", band="L1", code="C")
ds_gps_l1c = filt.filter_dataset(ds)  # returns smaller dataset or None
filt = SIDPatternFilter(system="E")
ds_masked = filt.apply(ds, "SNR", output_suffix="galileo")

Per-Cell Timeseries Storage

AnalysisStorage manages persistent Icechunk storage of analysis results. Per-cell timeseries are stored on a dedicated branch, keyed by system_band_code:

from canvod.grids.analysis import AnalysisStorage

storage = AnalysisStorage("/path/to/store")

# Store
storage.store_percell_timeseries(percell_ds, system="G", band="L1", code="C")

# List and load
storage.list_percell_datasets()          # ['C_B2b_I', 'E_E1_C', 'G_L1_C', ...]
ds = storage.load_percell_timeseries(system="G", band="L1", code="C")

Coordinate Convention

Angles

All grids use standard spherical coordinates:

  • phi (φ): azimuth 0 → 2π · 0 = North · π/2 = East · clockwise
  • theta (θ): polar angle from zenith · 0 = zenith · π/2 = horizon

This matches the canvod-auxiliary coordinate output — no conversion needed.


Role in the VOD Pipeline

flowchart TD
    A["`**Augmented Dataset**
    epoch x sid, with theta, phi`"]
    A --> B["`**Grid Assignment**
    add_cell_ids_to_ds_fast`"]
    B --> C["`**Gridded Dataset**
    + cell_id coordinate`"]
    C --> D["`**VOD per cell**
    Tau-Omega inversion`"]
    D --> E["`**Hemispherical Map**
    canvod-viz`"]