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— ConvertGridDatato 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`"]