Skip to content

canvod.grids API reference

Hemispheric grid implementations and spatial analysis tools.

Package

HEALPix and hemispheric grid operations.

Provides hemisphere grid structures for GNSS signal observation analysis.

BaseGridBuilder

Bases: ABC

Abstract base for hemispherical grid builders.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees

2
cutoff_theta float

Maximum polar angle cutoff in degrees

0
phi_rotation float

Rotation angle in degrees (applied to all phi values)

0

__init__(angular_resolution=2, cutoff_theta=0, phi_rotation=0)

Initialize the grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
phi_rotation float

Rotation angle in degrees.

0

get_grid_type() abstractmethod

Get grid type identifier.

build()

Build hemisphere grid.

Returns:

Type Description
GridData

Complete grid data structure

GridData dataclass

Immutable container for hemispherical grid structure.

Parameters:

Name Type Description Default
grid DataFrame

Grid cells with phi, theta, and bounds

required
theta_lims ndarray

Theta band limits

required
phi_lims list[ndarray]

Phi limits per theta band

required
cell_ids list[ndarray]

Cell IDs per theta band

required
grid_type str

Grid type identifier

required
solid_angles ndarray

Solid angles per cell [steradians]

None
metadata dict

Additional grid metadata

None
voronoi Any

Voronoi tessellation object (for Fibonacci grids)

None
vertices ndarray

3D vertices (for triangular grids)

None
points_xyz ndarray

3D point cloud (for Fibonacci grids)

None
vertex_phi ndarray

Vertex phi coordinates

None
vertex_theta ndarray

Vertex theta coordinates

None

coords property

Get cell coordinates.

ncells property

Number of cells in grid.

get_patches()

Create matplotlib patches for polar visualization.

get_solid_angles()

Calculate solid angle for each cell [steradians].

get_grid_stats()

Get grid statistics including solid angle uniformity.

GridType

Bases: Enum

Available grid projection types for hemispherical tessellation.

EqualAreaBuilder

Bases: BaseGridBuilder

Equal solid angle tessellation using concentric theta bands.

The hemisphere is divided into annular bands of constant width in theta. Within each band the number of azimuthal (phi) sectors is chosen so that every cell subtends approximately the same solid angle. This is the only grid type that has been validated for scientific use in this codebase.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle measured from zenith (0 = straight up, π/2 = horizon)

What angular_resolution means

angular_resolution (degrees) sets the width of each theta band. All bands have this same width Δθ. The azimuthal width of cells varies by band: near the zenith cells are wide in phi; near the horizon they are narrow, so that the solid angle stays constant.

Mathematical construction

  1. Target solid angle per cell is chosen equal to the solid angle of a cap of half-angle Δθ/2::

    Ω_target = 2π (1 − cos(Δθ/2))

  2. Zenith cap – a single cell covers [0, Δθ/2] in theta and the full azimuth [0, 2π).

  3. Theta bands – edges are placed at Δθ/2, 3Δθ/2, 5Δθ/2, … and a last edge at π/2 − cutoff_theta, so the grid reaches the horizon (or the cutoff). With the zenith cap of half width the last band is usually narrower than Δθ (half as wide when the cutoff is a multiple of Δθ); its phi divisions follow from its solid angle like any other band's. For each band [θ_inner, θ_outer] the band's total solid angle is::

    Ω_band = 2π (cos θ_inner − cos θ_outer)

  4. Phi divisions – the number of sectors in the band is::

    n_phi = round(Ω_band / Ω_target)

Each sector spans Δφ = 2π / n_phi. The cell center is placed at the geometric midpoint of its (phi, theta) rectangle.

Parameters:

Name Type Description Default
angular_resolution float

Theta-band width in degrees. Controls both the radial resolution and (indirectly, via the equal-area constraint) the azimuthal resolution.

2
cutoff_theta float

Minimum elevation above the horizon in degrees. The last band ends at θ = 90° − cutoff_theta; the sky below that elevation gets no cells. In GNSS terms this is the satellite elevation mask angle.

0
phi_rotation float

Rigid rotation applied to all phi coordinates after grid construction, in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"equal_area"

EqualAngleBuilder

Bases: BaseGridBuilder

Equal angular spacing in both theta and phi (NOT equal area).

Every cell is a rectangle of the same angular size Δθ × Δφ in the (theta, phi) parameter space. A cell's solid angle is Δφ · (cos θ_min − cos θ_max) ≈ Δφ · Δθ · sin θ, so cells near the zenith subtend less solid angle than cells near the horizon. A statistic that weights every cell equally therefore gives the sky near the zenith more weight than its share of the hemisphere. Not recommended for scientific analysis – use EqualAreaBuilder instead.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith

What angular_resolution means

angular_resolution (degrees) is used as both the theta-band width and the phi-sector width. The number of phi divisions is the same in every band but the zenith cap::

n_phi = floor(2π / Δθ)

so the sectors are wider than Δθ when Δθ does not divide 360°.

Mathematical construction

  1. A zenith cap cell covers [0, Δθ/2] × [0, 2π).
  2. Theta band edges are placed at Δθ/2, 3Δθ/2, … and a last edge at π/2 − cutoff_theta, so the grid reaches the horizon (or the cutoff); the last band is usually narrower than Δθ.
  3. Within every band, the full azimuth is split into n_phi sectors of equal width Δφ = 2π / n_phi.
  4. Cell centers are at the midpoint of each (phi, theta) rectangle.

Parameters:

Name Type Description Default
angular_resolution float

Angular spacing in degrees, applied identically in both theta and phi.

2
cutoff_theta float

Elevation mask angle in degrees. The last band ends at θ = 90° − cutoff_theta; the sky below that elevation gets no cells.

0
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"equal_angle"

EquirectangularBuilder

Bases: BaseGridBuilder

Simple rectangular grid in (theta, phi) space.

The hemisphere is divided into a regular rectangular array: a constant number of theta bands, each containing the same constant number of phi sectors. Every cell is a rectangle of the same angular size in (theta, phi), except the last band and the last sector, which are narrower when Δθ does not divide their range. It differs from EqualAngleBuilder at the zenith: EqualAngleBuilder makes the innermost half band a single zenith cap and puts its band edges half a band off, while this builder starts the first band at the zenith with the same number of sectors as every other band.

A cell's solid angle is Δφ · (cos θ_min − cos θ_max) ≈ Δφ · Δθ · sin θ, so cells near the zenith subtend less solid angle than cells near the horizon. A statistic that weights every cell equally therefore gives the sky near the zenith more weight than its share of the hemisphere. Not recommended for scientific analysis – use EqualAreaBuilder instead.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith (0 = straight up, π/2 = horizon)

What angular_resolution means

angular_resolution (degrees) is used as both the theta-band width and the phi-sector width::

n_theta = ceil((π/2 − cutoff) / Δθ)
n_phi   = ceil(2π / Δθ)
total cells = n_theta × n_phi

Mathematical construction

  1. Theta edges are placed at 0, Δθ, 2Δθ, … up to π/2 − cutoff_theta (the last edge is clamped exactly to π/2 − cutoff_theta rather than overshooting it).
  2. Phi edges are placed at 0, Δθ, 2Δθ, … and a last edge at 2π; the last sector is narrower when Δθ does not divide 360°.
  3. Every (theta_band, phi_sector) combination produces one cell. The cell center is the midpoint of the rectangle.
  4. No special zenith cap is created; the band nearest the zenith has the same number of phi sectors as all other bands.

Parameters:

Name Type Description Default
angular_resolution float

Angular spacing in degrees, applied identically in both theta and phi.

2
cutoff_theta float

Elevation mask angle in degrees. The last band ends at θ = 90° − cutoff_theta; the sky below that elevation gets no cells.

0
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"equirectangular"

HEALPixBuilder

Bases: BaseGridBuilder

HEALPix tessellation (Hierarchical Equal Area isoLatitude Pixelization).

HEALPix partitions the sphere into 12 base pixels arranged at equal latitudes. Each base pixel is recursively subdivided into 4 children, producing 12 × nside² pixels on the full sphere, all with exactly the same solid angle. This strict equal-area property makes HEALPix the gold standard for pixelisations that must be unbiased under solid-angle weighting.

This builder delegates the pixel geometry entirely to the healpy library. It filters the full-sphere pixelisation down to the northern hemisphere and stores approximate bounding boxes (phi_min/max, theta_min/max) derived from the pixel resolution. The bounding boxes are not the true pixel boundaries (which are curvilinear); they are only approximations suitable for quick spatial queries. For exact pixel membership use healpy.ang2pix directly.

Coordinate convention

HEALPix natively uses colatitude theta ∈ [0, π] (0 = North Pole) and longitude phi ∈ [0, 2π). This matches the GNSS convention used elsewhere in canvodpy: theta = 0 is the zenith, theta = π/2 is the horizon. No coordinate transform is applied.

What nside (resolution) means

nside is the single resolution parameter of HEALPix. It must be a power of 2. The key derived quantities are::

n_pixels   = 12 × nside²           (full sphere)
pixel_area = 4π / n_pixels          (steradians, exact)
resolution ≈ √(pixel_area)          (approximate angular diameter)
           ≈ 58.6° / nside         (degrees)
nside Pixels (full) Approx resolution Pixel area (sr)
1 12 58.6° 1.049
2 48 29.3° 0.262
4 192 14.7° 0.065
8 768 7.3° 0.016
16 3 072 3.7° 0.004
32 12 288 1.8° 0.001

When nside is not provided, it is estimated from angular_resolution and rounded to the nearest power of 2::

nside_estimate = round_to_pow2( √(3/π) × 60 / angular_resolution )

Mathematical construction

HEALPix construction is performed entirely by healpy. At a high level:

  1. The sphere is divided into 12 congruent base pixels (a curvilinear quadrilateral arrangement at three latitude zones: polar caps and equatorial belt).
  2. Each base pixel is subdivided into nside² equal-area children using a hierarchical quadtree.
  3. Pixel centers are returned by healpy.pix2ang(nside, ipix) in RING ordering (pixels ordered by increasing colatitude).
  4. This builder keeps only pixels with theta ≤ π/2 − cutoff_theta (northern hemisphere above the elevation mask).

Parameters:

Name Type Description Default
angular_resolution float

Approximate angular resolution in degrees. Used only to derive nside when that parameter is not given explicitly.

2
cutoff_theta float

Elevation mask angle in degrees. Pixels with colatitude theta > π/2 − cutoff_theta (i.e. below the mask) are excluded.

0
nside int or None

HEALPix resolution parameter. Must be a power of 2. If None, estimated from angular_resolution.

None
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

Raises:

Type Description
ImportError

If healpy is not installed.

ValueError

If nside is not a power of 2.

__init__(angular_resolution=2, cutoff_theta=0, nside=None, phi_rotation=0)

Initialize the HEALPix grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
nside int | None

HEALPix nside parameter.

None
phi_rotation float

Rotation angle in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"healpix"

get_healpix_info()

Get HEALPix-specific information.

Returns:

Name Type Description
info dict

Keys: nside, npix_total, pixel_area_sr, pixel_area_arcmin2, resolution_arcmin, resolution_deg, max_pixel_radius_deg.

GeodesicBuilder

Bases: BaseGridBuilder

Geodesic grid based on a subdivided icosahedron.

The sphere is tessellated into triangular cells by starting with an icosahedron (20 equilateral triangles) and recursively subdividing each triangle into four smaller triangles. All vertices are projected back onto the unit sphere after each subdivision step, so the final cells are spherical triangles. The grid has no polar singularity and provides near-uniform cell areas, though strict equal-area is not guaranteed — cell areas vary by a few percent depending on how they inherit the icosahedral symmetry axes.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith (0 = straight up, π/2 = horizon)

Cell centers are computed as the 3D Cartesian mean of the three vertices, re-normalized onto the unit sphere.

What angular_resolution means

angular_resolution is not used directly as a cell size. Instead it is used only when subdivision_level is not explicitly supplied, to estimate an appropriate subdivision level. The heuristic targets an approximate triangle edge length of 2 × angular_resolution::

target_edge ≈ 2 × angular_resolution   (degrees)
subdivision_level = ceil(log₂(63.4 / target_edge))

The number 63.4° is the edge length of a regular icosahedron inscribed in a unit sphere. Each subdivision halves the edge length, so the actual edge length at level n is approximately::

edge ≈ 63.4° / 2ⁿ   (degrees)

The total number of triangles on the full sphere is 20 × 4ⁿ. Roughly half fall in the northern hemisphere (exact count depends on the hemisphere boundary).

Mathematical construction

  1. Icosahedron – 12 vertices placed at the intersections of three mutually perpendicular golden-ratio rectangles, normalized to the unit sphere. 20 triangular faces connect them.
  2. Subdivision – each triangle is split into 4 by inserting edge midpoints. Each midpoint is projected onto the unit sphere (re-normalized) before the next subdivision. This is repeated subdivision_level times.
  3. Hemisphere filter – faces are kept if their centroid (the 3D Cartesian mean of the three vertices, renormalised to the unit sphere) satisfies theta ≤ π/2 − cutoff_theta. Filtering on centroid rather than "any vertex above" avoids keeping boundary triangles that are mostly below the horizon; kept triangles may still extend slightly across the horizon since cells are not clipped to an exact circle.
  4. Phi wrapping – for triangles that straddle the 0/2π azimuthal boundary, bounding-box limits are computed with a seam-aware min/max (see phi_bbox) rather than naive min/max.

Parameters:

Name Type Description Default
angular_resolution float

Approximate angular resolution in degrees. Used only to derive subdivision_level when that parameter is not given explicitly.

2
cutoff_theta float

Elevation mask angle in degrees. Triangles are excluded if their centroid is below this elevation.

0
subdivision_level int or None

Number of icosahedral subdivisions. If None, estimated from angular_resolution. Typical range 0–5.

None
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

Notes

The theta_lims, phi_lims, and cell_ids fields of the returned GridData are synthetic evenly-spaced arrays kept only for interface compatibility with ring-based grids. They do not describe the actual triangular cell layout. Use the geodesic_vertices column and the vertices array in GridData.vertices for the true geometry.

__init__(angular_resolution=2, cutoff_theta=0, subdivision_level=None, phi_rotation=0)

Initialize the geodesic grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
subdivision_level int | None

Subdivision level override.

None
phi_rotation float

Rotation angle in degrees.

0

get_triangles()

Return triangle vertex coordinates for visualization.

Returns:

Name Type Description
triangles ndarray or None

Array of shape (n_faces, 3, 3) where triangles[i] contains the three 3D unit-sphere vertices of triangle i. None if the grid has not been built yet.

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"geodesic"

FibonacciBuilder

Bases: BaseGridBuilder

Fibonacci sphere grid with spherical Voronoi tessellation.

Points are distributed on the sphere using the Fibonacci lattice (golden-spiral method), which provides one of the most uniform point distributions achievable on a sphere without iterative optimization. Each point then becomes the center of a spherical Voronoi cell — the region of the sphere closer to that point than to any other. The resulting tessellation has no polar singularities and near-uniform cell areas.

The tessellation is computed by scipy.spatial.SphericalVoronoi. Because Voronoi cells have curvilinear boundaries, the phi_min/max and theta_min/max columns in the grid are axis-aligned bounding boxes, not the true cell boundaries. They are unreliable for spatial queries — use the voronoi_region column (vertex indices into the SphericalVoronoi.vertices array) for exact geometry.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith (0 = straight up, π/2 = horizon)

What n_points (resolution) means

Resolution is controlled by n_points, the number of Voronoi cells in the hemisphere. When n_points is not supplied it is estimated from angular_resolution via::

cell_area  ≈ angular_resolution²   (radians²)
n_points   = max(10, round(2π / cell_area))

The approximate cell "diameter" (assuming a circular cell of equal area) is::

d ≈ 2 √(2π / n_points)   (radians)
  ≈ 2 × angular_resolution

angular_resolution therefore has no direct geometric meaning for this grid type — it is only a convenience for the n_points estimator.

Mathematical construction

  1. Full-sphere Fibonacci lattice – 2 × n_points points are generated on the unit sphere. Point i has::

    θᵢ = arccos(1 − 2(i + 0.5) / N) φᵢ = 2π (i + 0.5) / φ_golden (mod 2π)

where N = 2 × n_points and φ_golden = (1+√5)/2. The +0.5 offset avoids placing points exactly at the poles. 2. Spherical Voronoi tessellation – scipy.spatial.SphericalVoronoi computes the Voronoi diagram on the full sphere (both hemispheres). Regions are sorted so that vertices appear in counter-clockwise order around each cell. Tessellating the full sphere (rather than hemisphere-only points) is required for correctness: with no sites in the southern hemisphere, cells along the horizon would have no southern neighbor to bound them and would balloon out to cover the entire (empty) opposite hemisphere. 3. Hemisphere filter – of the resulting cells, only those whose generating point has θ ≤ π/2 − cutoff_theta are kept. 4. Bounding boxes – axis-aligned bounding boxes in (phi, theta) are computed from the Voronoi vertex coordinates, using a seam-aware min/max so cells straddling phi = 0/2π get their true (narrow) angular extent instead of a near-full-circle span. These bounding boxes are still approximations only (see caveat above).

Parameters:

Name Type Description Default
angular_resolution float

Approximate angular resolution in degrees. Used only to estimate n_points when that parameter is not given explicitly.

2
cutoff_theta float

Elevation mask angle in degrees. Points below this elevation are excluded before tessellation.

0
n_points int or None

Target number of Voronoi cells in the hemisphere. If None, estimated from angular_resolution.

None
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

Raises:

Type Description
ImportError

If scipy is not installed.

ValueError

If fewer than 4 points survive the hemisphere filter.

Notes

The theta_lims, phi_lims, and cell_ids fields of the returned GridData are synthetic evenly-spaced arrays kept only for interface compatibility with ring-based grids. They do not describe the actual Voronoi cell layout.

__init__(angular_resolution=2, cutoff_theta=0, n_points=None, phi_rotation=0)

Initialize the Fibonacci grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
n_points int | None

Number of points to generate.

None
phi_rotation float

Rotation angle in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"fibonacci"

HTMBuilder

Bases: BaseGridBuilder

Hierarchical Triangular Mesh (HTM) grid.

HTM divides the sphere into an octahedron (8 triangular faces), then recursively subdivides each face into 4 smaller triangles by inserting edge-midpoint vertices projected onto the unit sphere. The recursion depth is controlled by htm_level. This produces a strictly hierarchical triangulation: every triangle at level n is the union of exactly 4 triangles at level n + 1.

Cell areas are approximately equal but not strictly so — area uniformity improves with level because the icosahedral edge-length asymmetry averages out over many subdivisions.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith (0 = straight up, π/2 = horizon)

Cell centers are the 3D Cartesian mean of the three triangle vertices, re-normalized onto the unit sphere.

What htm_level (resolution) means

The resolution is set by htm_level, not by angular_resolution. angular_resolution is used only to estimate an appropriate level when htm_level is not supplied explicitly. The heuristic is::

target_edge ≈ 2 × angular_resolution   (degrees)
htm_level   = min(15, ceil(log₂(90 / target_edge)))

The approximate triangle edge length at level n is::

edge ≈ 90° / 2ⁿ
Level Triangles (full sphere) Approx edge
0 8 90°
1 32 45°
2 128 22.5°
3 512 11.25°
4 2 048 5.6°
n 8 × 4ⁿ 90° / 2ⁿ

Mathematical construction

  1. Octahedron – 6 vertices at ±x, ±y, ±z on the unit sphere, forming 8 triangular faces (4 northern, 4 southern).
  2. Subdivision – for each triangle [v₀, v₁, v₂], three edge midpoints are computed and projected onto the unit sphere::

    m₀ = normalize((v₀ + v₁) / 2) m₁ = normalize((v₁ + v₂) / 2) m₂ = normalize((v₂ + v₀) / 2)

The four children are [v₀, m₀, m₂], [v₁, m₁, m₀], [v₂, m₂, m₁], and [m₀, m₁, m₂]. This is repeated htm_level times. 3. Hemisphere filter – a triangle is kept if its centroid (the 3D Cartesian mean of the three vertices, renormalised to the unit sphere) satisfies theta ≤ π/2 − cutoff_theta. Filtering on centroid rather than "any vertex above" avoids keeping boundary triangles that are mostly below the horizon; kept triangles may still extend slightly across the horizon since cells are not clipped to an exact circle. 4. Each leaf triangle becomes one cell; its center, bounding box, and three vertex coordinates are stored.

Parameters:

Name Type Description Default
angular_resolution float

Approximate angular resolution in degrees. Used only to derive htm_level when that parameter is not given explicitly.

2
cutoff_theta float

Elevation mask angle in degrees. Triangles are excluded if their centroid is below this elevation.

0
htm_level int or None

HTM subdivision depth. If None, estimated from angular_resolution. Practical range 0–15.

None
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

Notes

The theta_lims, phi_lims, and cell_ids fields of the returned GridData are synthetic evenly-spaced arrays kept only for interface compatibility with ring-based grids. They do not describe the actual triangular cell layout.

HTM IDs in this implementation use a decimal-digit scheme (parent_id × 10 + child_index) which diverges from the original SDSS HTM binary-coded ID scheme. This is adequate for indexing but should not be compared with external HTM catalogs.

References

Kunszt et al. (2001): "The Hierarchical Triangular Mesh" https://www.sdss.org/dr12/algorithms/htm/

__init__(angular_resolution=2, cutoff_theta=0, htm_level=None, phi_rotation=0)

Initialize the HTM grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
htm_level int | None

HTM subdivision level.

None
phi_rotation float

Rotation angle in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"htm"

get_htm_info()

Get HTM-specific information.

Returns:

Name Type Description
info dict

Keys: htm_level, n_triangles_full_sphere, approx_edge_length_deg, approx_edge_length_arcmin.

CellAggregator

Polars-based per-cell aggregation helpers.

aggregate_by_cell(df, value_var='VOD', method='mean') staticmethod

Aggregate values by cell_id.

Parameters:

Name Type Description Default
df DataFrame

Must contain cell_id and value_var columns.

required
value_var str

Column to aggregate.

'VOD'
method ('mean', 'median', 'std', 'count')

Aggregation method.

'mean'

Returns:

Type Description
DataFrame

Two-column DataFrame: cell_id, value_var.

create_hemigrid(grid_type, angular_resolution=10.0, **kwargs)

Create hemisphere grid of specified type.

Factory function for creating various hemisphere grid types commonly used in GNSS analysis.

Parameters:

Name Type Description Default
grid_type str

Type of grid to create: - 'equal_area': Regular lat/lon grid with equal solid angle cells - 'equal_angle': Regular angular spacing (not recommended) - 'rectangular' or 'equirectangular': Simple rectangular grid - 'HTM': Hierarchical Triangular Mesh - 'geodesic': Geodesic sphere subdivision (icosahedron-based) - 'healpix': HEALPix grid (requires healpy) - 'fibonacci': Fibonacci sphere (requires scipy)

required
angular_resolution float

Angular resolution in degrees

10.0
**kwargs Any

Additional grid-specific parameters: - cutoff_theta : float - Maximum theta angle cutoff (degrees) - phi_rotation : float - Rotation angle (degrees) - subdivision_level : int - For geodesic/HTM grids - htm_level : int - For HTM grids specifically - nside : int - For HEALPix grids - n_points : int - For Fibonacci grids

{}

Returns:

Type Description
GridData

Complete hemisphere grid data structure

Examples:

>>> # Equal area grid with 10° resolution
>>> grid = create_hemigrid('equal_area', angular_resolution=10.0)
>>>
>>> # HTM grid with subdivision level 3
>>> grid = create_hemigrid('HTM', angular_resolution=5.0, htm_level=3)
>>>
>>> # Geodesic grid
>>> grid = create_hemigrid('geodesic', angular_resolution=5.0, subdivision_level=2)

Notes

Grid coordinates use navigation convention: - phi: azimuth angle, 0 to 2π (0 = North, π/2 = East, clockwise) - theta: polar angle from zenith, 0 to π/2 (0 = zenith, π/2 = horizon)

Grid core

Base class for hemisphere grid builders.

BaseGridBuilder

Bases: ABC

Abstract base for hemispherical grid builders.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees

2
cutoff_theta float

Maximum polar angle cutoff in degrees

0
phi_rotation float

Rotation angle in degrees (applied to all phi values)

0

__init__(angular_resolution=2, cutoff_theta=0, phi_rotation=0)

Initialize the grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
phi_rotation float

Rotation angle in degrees.

0

get_grid_type() abstractmethod

Get grid type identifier.

build()

Build hemisphere grid.

Returns:

Type Description
GridData

Complete grid data structure

Grid data container for hemisphere grids.

GridData dataclass

Immutable container for hemispherical grid structure.

Parameters:

Name Type Description Default
grid DataFrame

Grid cells with phi, theta, and bounds

required
theta_lims ndarray

Theta band limits

required
phi_lims list[ndarray]

Phi limits per theta band

required
cell_ids list[ndarray]

Cell IDs per theta band

required
grid_type str

Grid type identifier

required
solid_angles ndarray

Solid angles per cell [steradians]

None
metadata dict

Additional grid metadata

None
voronoi Any

Voronoi tessellation object (for Fibonacci grids)

None
vertices ndarray

3D vertices (for triangular grids)

None
points_xyz ndarray

3D point cloud (for Fibonacci grids)

None
vertex_phi ndarray

Vertex phi coordinates

None
vertex_theta ndarray

Vertex theta coordinates

None

coords property

Get cell coordinates.

ncells property

Number of cells in grid.

get_patches()

Create matplotlib patches for polar visualization.

get_solid_angles()

Calculate solid angle for each cell [steradians].

get_grid_stats()

Get grid statistics including solid angle uniformity.

Grid type definitions for hemisphere tessellation.

GridType

Bases: Enum

Available grid projection types for hemispherical tessellation.

Grid builders

Equal-area grid

Equal-area grid implementation.

EqualAreaBuilder

Bases: BaseGridBuilder

Equal solid angle tessellation using concentric theta bands.

The hemisphere is divided into annular bands of constant width in theta. Within each band the number of azimuthal (phi) sectors is chosen so that every cell subtends approximately the same solid angle. This is the only grid type that has been validated for scientific use in this codebase.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle measured from zenith (0 = straight up, π/2 = horizon)

What angular_resolution means

angular_resolution (degrees) sets the width of each theta band. All bands have this same width Δθ. The azimuthal width of cells varies by band: near the zenith cells are wide in phi; near the horizon they are narrow, so that the solid angle stays constant.

Mathematical construction

  1. Target solid angle per cell is chosen equal to the solid angle of a cap of half-angle Δθ/2::

    Ω_target = 2π (1 − cos(Δθ/2))

  2. Zenith cap – a single cell covers [0, Δθ/2] in theta and the full azimuth [0, 2π).

  3. Theta bands – edges are placed at Δθ/2, 3Δθ/2, 5Δθ/2, … and a last edge at π/2 − cutoff_theta, so the grid reaches the horizon (or the cutoff). With the zenith cap of half width the last band is usually narrower than Δθ (half as wide when the cutoff is a multiple of Δθ); its phi divisions follow from its solid angle like any other band's. For each band [θ_inner, θ_outer] the band's total solid angle is::

    Ω_band = 2π (cos θ_inner − cos θ_outer)

  4. Phi divisions – the number of sectors in the band is::

    n_phi = round(Ω_band / Ω_target)

Each sector spans Δφ = 2π / n_phi. The cell center is placed at the geometric midpoint of its (phi, theta) rectangle.

Parameters:

Name Type Description Default
angular_resolution float

Theta-band width in degrees. Controls both the radial resolution and (indirectly, via the equal-area constraint) the azimuthal resolution.

2
cutoff_theta float

Minimum elevation above the horizon in degrees. The last band ends at θ = 90° − cutoff_theta; the sky below that elevation gets no cells. In GNSS terms this is the satellite elevation mask angle.

0
phi_rotation float

Rigid rotation applied to all phi coordinates after grid construction, in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"equal_area"

Equal-angle grid

Equal-angle grid implementation.

EqualAngleBuilder

Bases: BaseGridBuilder

Equal angular spacing in both theta and phi (NOT equal area).

Every cell is a rectangle of the same angular size Δθ × Δφ in the (theta, phi) parameter space. A cell's solid angle is Δφ · (cos θ_min − cos θ_max) ≈ Δφ · Δθ · sin θ, so cells near the zenith subtend less solid angle than cells near the horizon. A statistic that weights every cell equally therefore gives the sky near the zenith more weight than its share of the hemisphere. Not recommended for scientific analysis – use EqualAreaBuilder instead.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith

What angular_resolution means

angular_resolution (degrees) is used as both the theta-band width and the phi-sector width. The number of phi divisions is the same in every band but the zenith cap::

n_phi = floor(2π / Δθ)

so the sectors are wider than Δθ when Δθ does not divide 360°.

Mathematical construction

  1. A zenith cap cell covers [0, Δθ/2] × [0, 2π).
  2. Theta band edges are placed at Δθ/2, 3Δθ/2, … and a last edge at π/2 − cutoff_theta, so the grid reaches the horizon (or the cutoff); the last band is usually narrower than Δθ.
  3. Within every band, the full azimuth is split into n_phi sectors of equal width Δφ = 2π / n_phi.
  4. Cell centers are at the midpoint of each (phi, theta) rectangle.

Parameters:

Name Type Description Default
angular_resolution float

Angular spacing in degrees, applied identically in both theta and phi.

2
cutoff_theta float

Elevation mask angle in degrees. The last band ends at θ = 90° − cutoff_theta; the sky below that elevation gets no cells.

0
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"equal_angle"

Equirectangular grid

Equirectangular grid implementation.

EquirectangularBuilder

Bases: BaseGridBuilder

Simple rectangular grid in (theta, phi) space.

The hemisphere is divided into a regular rectangular array: a constant number of theta bands, each containing the same constant number of phi sectors. Every cell is a rectangle of the same angular size in (theta, phi), except the last band and the last sector, which are narrower when Δθ does not divide their range. It differs from EqualAngleBuilder at the zenith: EqualAngleBuilder makes the innermost half band a single zenith cap and puts its band edges half a band off, while this builder starts the first band at the zenith with the same number of sectors as every other band.

A cell's solid angle is Δφ · (cos θ_min − cos θ_max) ≈ Δφ · Δθ · sin θ, so cells near the zenith subtend less solid angle than cells near the horizon. A statistic that weights every cell equally therefore gives the sky near the zenith more weight than its share of the hemisphere. Not recommended for scientific analysis – use EqualAreaBuilder instead.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith (0 = straight up, π/2 = horizon)

What angular_resolution means

angular_resolution (degrees) is used as both the theta-band width and the phi-sector width::

n_theta = ceil((π/2 − cutoff) / Δθ)
n_phi   = ceil(2π / Δθ)
total cells = n_theta × n_phi

Mathematical construction

  1. Theta edges are placed at 0, Δθ, 2Δθ, … up to π/2 − cutoff_theta (the last edge is clamped exactly to π/2 − cutoff_theta rather than overshooting it).
  2. Phi edges are placed at 0, Δθ, 2Δθ, … and a last edge at 2π; the last sector is narrower when Δθ does not divide 360°.
  3. Every (theta_band, phi_sector) combination produces one cell. The cell center is the midpoint of the rectangle.
  4. No special zenith cap is created; the band nearest the zenith has the same number of phi sectors as all other bands.

Parameters:

Name Type Description Default
angular_resolution float

Angular spacing in degrees, applied identically in both theta and phi.

2
cutoff_theta float

Elevation mask angle in degrees. The last band ends at θ = 90° − cutoff_theta; the sky below that elevation gets no cells.

0
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"equirectangular"

HEALPix grid

HEALPix grid implementation.

HEALPixBuilder

Bases: BaseGridBuilder

HEALPix tessellation (Hierarchical Equal Area isoLatitude Pixelization).

HEALPix partitions the sphere into 12 base pixels arranged at equal latitudes. Each base pixel is recursively subdivided into 4 children, producing 12 × nside² pixels on the full sphere, all with exactly the same solid angle. This strict equal-area property makes HEALPix the gold standard for pixelisations that must be unbiased under solid-angle weighting.

This builder delegates the pixel geometry entirely to the healpy library. It filters the full-sphere pixelisation down to the northern hemisphere and stores approximate bounding boxes (phi_min/max, theta_min/max) derived from the pixel resolution. The bounding boxes are not the true pixel boundaries (which are curvilinear); they are only approximations suitable for quick spatial queries. For exact pixel membership use healpy.ang2pix directly.

Coordinate convention

HEALPix natively uses colatitude theta ∈ [0, π] (0 = North Pole) and longitude phi ∈ [0, 2π). This matches the GNSS convention used elsewhere in canvodpy: theta = 0 is the zenith, theta = π/2 is the horizon. No coordinate transform is applied.

What nside (resolution) means

nside is the single resolution parameter of HEALPix. It must be a power of 2. The key derived quantities are::

n_pixels   = 12 × nside²           (full sphere)
pixel_area = 4π / n_pixels          (steradians, exact)
resolution ≈ √(pixel_area)          (approximate angular diameter)
           ≈ 58.6° / nside         (degrees)
nside Pixels (full) Approx resolution Pixel area (sr)
1 12 58.6° 1.049
2 48 29.3° 0.262
4 192 14.7° 0.065
8 768 7.3° 0.016
16 3 072 3.7° 0.004
32 12 288 1.8° 0.001

When nside is not provided, it is estimated from angular_resolution and rounded to the nearest power of 2::

nside_estimate = round_to_pow2( √(3/π) × 60 / angular_resolution )

Mathematical construction

HEALPix construction is performed entirely by healpy. At a high level:

  1. The sphere is divided into 12 congruent base pixels (a curvilinear quadrilateral arrangement at three latitude zones: polar caps and equatorial belt).
  2. Each base pixel is subdivided into nside² equal-area children using a hierarchical quadtree.
  3. Pixel centers are returned by healpy.pix2ang(nside, ipix) in RING ordering (pixels ordered by increasing colatitude).
  4. This builder keeps only pixels with theta ≤ π/2 − cutoff_theta (northern hemisphere above the elevation mask).

Parameters:

Name Type Description Default
angular_resolution float

Approximate angular resolution in degrees. Used only to derive nside when that parameter is not given explicitly.

2
cutoff_theta float

Elevation mask angle in degrees. Pixels with colatitude theta > π/2 − cutoff_theta (i.e. below the mask) are excluded.

0
nside int or None

HEALPix resolution parameter. Must be a power of 2. If None, estimated from angular_resolution.

None
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

Raises:

Type Description
ImportError

If healpy is not installed.

ValueError

If nside is not a power of 2.

__init__(angular_resolution=2, cutoff_theta=0, nside=None, phi_rotation=0)

Initialize the HEALPix grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
nside int | None

HEALPix nside parameter.

None
phi_rotation float

Rotation angle in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"healpix"

get_healpix_info()

Get HEALPix-specific information.

Returns:

Name Type Description
info dict

Keys: nside, npix_total, pixel_area_sr, pixel_area_arcmin2, resolution_arcmin, resolution_deg, max_pixel_radius_deg.

Geodesic grid

Geodesic grid implementation.

GeodesicBuilder

Bases: BaseGridBuilder

Geodesic grid based on a subdivided icosahedron.

The sphere is tessellated into triangular cells by starting with an icosahedron (20 equilateral triangles) and recursively subdividing each triangle into four smaller triangles. All vertices are projected back onto the unit sphere after each subdivision step, so the final cells are spherical triangles. The grid has no polar singularity and provides near-uniform cell areas, though strict equal-area is not guaranteed — cell areas vary by a few percent depending on how they inherit the icosahedral symmetry axes.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith (0 = straight up, π/2 = horizon)

Cell centers are computed as the 3D Cartesian mean of the three vertices, re-normalized onto the unit sphere.

What angular_resolution means

angular_resolution is not used directly as a cell size. Instead it is used only when subdivision_level is not explicitly supplied, to estimate an appropriate subdivision level. The heuristic targets an approximate triangle edge length of 2 × angular_resolution::

target_edge ≈ 2 × angular_resolution   (degrees)
subdivision_level = ceil(log₂(63.4 / target_edge))

The number 63.4° is the edge length of a regular icosahedron inscribed in a unit sphere. Each subdivision halves the edge length, so the actual edge length at level n is approximately::

edge ≈ 63.4° / 2ⁿ   (degrees)

The total number of triangles on the full sphere is 20 × 4ⁿ. Roughly half fall in the northern hemisphere (exact count depends on the hemisphere boundary).

Mathematical construction

  1. Icosahedron – 12 vertices placed at the intersections of three mutually perpendicular golden-ratio rectangles, normalized to the unit sphere. 20 triangular faces connect them.
  2. Subdivision – each triangle is split into 4 by inserting edge midpoints. Each midpoint is projected onto the unit sphere (re-normalized) before the next subdivision. This is repeated subdivision_level times.
  3. Hemisphere filter – faces are kept if their centroid (the 3D Cartesian mean of the three vertices, renormalised to the unit sphere) satisfies theta ≤ π/2 − cutoff_theta. Filtering on centroid rather than "any vertex above" avoids keeping boundary triangles that are mostly below the horizon; kept triangles may still extend slightly across the horizon since cells are not clipped to an exact circle.
  4. Phi wrapping – for triangles that straddle the 0/2π azimuthal boundary, bounding-box limits are computed with a seam-aware min/max (see phi_bbox) rather than naive min/max.

Parameters:

Name Type Description Default
angular_resolution float

Approximate angular resolution in degrees. Used only to derive subdivision_level when that parameter is not given explicitly.

2
cutoff_theta float

Elevation mask angle in degrees. Triangles are excluded if their centroid is below this elevation.

0
subdivision_level int or None

Number of icosahedral subdivisions. If None, estimated from angular_resolution. Typical range 0–5.

None
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

Notes

The theta_lims, phi_lims, and cell_ids fields of the returned GridData are synthetic evenly-spaced arrays kept only for interface compatibility with ring-based grids. They do not describe the actual triangular cell layout. Use the geodesic_vertices column and the vertices array in GridData.vertices for the true geometry.

__init__(angular_resolution=2, cutoff_theta=0, subdivision_level=None, phi_rotation=0)

Initialize the geodesic grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
subdivision_level int | None

Subdivision level override.

None
phi_rotation float

Rotation angle in degrees.

0

get_triangles()

Return triangle vertex coordinates for visualization.

Returns:

Name Type Description
triangles ndarray or None

Array of shape (n_faces, 3, 3) where triangles[i] contains the three 3D unit-sphere vertices of triangle i. None if the grid has not been built yet.

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"geodesic"

Fibonacci grid

Fibonacci sphere grid implementation.

FibonacciBuilder

Bases: BaseGridBuilder

Fibonacci sphere grid with spherical Voronoi tessellation.

Points are distributed on the sphere using the Fibonacci lattice (golden-spiral method), which provides one of the most uniform point distributions achievable on a sphere without iterative optimization. Each point then becomes the center of a spherical Voronoi cell — the region of the sphere closer to that point than to any other. The resulting tessellation has no polar singularities and near-uniform cell areas.

The tessellation is computed by scipy.spatial.SphericalVoronoi. Because Voronoi cells have curvilinear boundaries, the phi_min/max and theta_min/max columns in the grid are axis-aligned bounding boxes, not the true cell boundaries. They are unreliable for spatial queries — use the voronoi_region column (vertex indices into the SphericalVoronoi.vertices array) for exact geometry.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith (0 = straight up, π/2 = horizon)

What n_points (resolution) means

Resolution is controlled by n_points, the number of Voronoi cells in the hemisphere. When n_points is not supplied it is estimated from angular_resolution via::

cell_area  ≈ angular_resolution²   (radians²)
n_points   = max(10, round(2π / cell_area))

The approximate cell "diameter" (assuming a circular cell of equal area) is::

d ≈ 2 √(2π / n_points)   (radians)
  ≈ 2 × angular_resolution

angular_resolution therefore has no direct geometric meaning for this grid type — it is only a convenience for the n_points estimator.

Mathematical construction

  1. Full-sphere Fibonacci lattice – 2 × n_points points are generated on the unit sphere. Point i has::

    θᵢ = arccos(1 − 2(i + 0.5) / N) φᵢ = 2π (i + 0.5) / φ_golden (mod 2π)

where N = 2 × n_points and φ_golden = (1+√5)/2. The +0.5 offset avoids placing points exactly at the poles. 2. Spherical Voronoi tessellation – scipy.spatial.SphericalVoronoi computes the Voronoi diagram on the full sphere (both hemispheres). Regions are sorted so that vertices appear in counter-clockwise order around each cell. Tessellating the full sphere (rather than hemisphere-only points) is required for correctness: with no sites in the southern hemisphere, cells along the horizon would have no southern neighbor to bound them and would balloon out to cover the entire (empty) opposite hemisphere. 3. Hemisphere filter – of the resulting cells, only those whose generating point has θ ≤ π/2 − cutoff_theta are kept. 4. Bounding boxes – axis-aligned bounding boxes in (phi, theta) are computed from the Voronoi vertex coordinates, using a seam-aware min/max so cells straddling phi = 0/2π get their true (narrow) angular extent instead of a near-full-circle span. These bounding boxes are still approximations only (see caveat above).

Parameters:

Name Type Description Default
angular_resolution float

Approximate angular resolution in degrees. Used only to estimate n_points when that parameter is not given explicitly.

2
cutoff_theta float

Elevation mask angle in degrees. Points below this elevation are excluded before tessellation.

0
n_points int or None

Target number of Voronoi cells in the hemisphere. If None, estimated from angular_resolution.

None
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

Raises:

Type Description
ImportError

If scipy is not installed.

ValueError

If fewer than 4 points survive the hemisphere filter.

Notes

The theta_lims, phi_lims, and cell_ids fields of the returned GridData are synthetic evenly-spaced arrays kept only for interface compatibility with ring-based grids. They do not describe the actual Voronoi cell layout.

__init__(angular_resolution=2, cutoff_theta=0, n_points=None, phi_rotation=0)

Initialize the Fibonacci grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
n_points int | None

Number of points to generate.

None
phi_rotation float

Rotation angle in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"fibonacci"

HTM grid

HTM (Hierarchical Triangular Mesh) grid implementation.

HTMBuilder

Bases: BaseGridBuilder

Hierarchical Triangular Mesh (HTM) grid.

HTM divides the sphere into an octahedron (8 triangular faces), then recursively subdivides each face into 4 smaller triangles by inserting edge-midpoint vertices projected onto the unit sphere. The recursion depth is controlled by htm_level. This produces a strictly hierarchical triangulation: every triangle at level n is the union of exactly 4 triangles at level n + 1.

Cell areas are approximately equal but not strictly so — area uniformity improves with level because the icosahedral edge-length asymmetry averages out over many subdivisions.

Coordinate convention (physics / GNSS)

  • phi ∈ [0, 2π) – azimuthal angle from North, clockwise (navigation convention)
  • theta ∈ [0, π/2] – polar angle from zenith (0 = straight up, π/2 = horizon)

Cell centers are the 3D Cartesian mean of the three triangle vertices, re-normalized onto the unit sphere.

What htm_level (resolution) means

The resolution is set by htm_level, not by angular_resolution. angular_resolution is used only to estimate an appropriate level when htm_level is not supplied explicitly. The heuristic is::

target_edge ≈ 2 × angular_resolution   (degrees)
htm_level   = min(15, ceil(log₂(90 / target_edge)))

The approximate triangle edge length at level n is::

edge ≈ 90° / 2ⁿ
Level Triangles (full sphere) Approx edge
0 8 90°
1 32 45°
2 128 22.5°
3 512 11.25°
4 2 048 5.6°
n 8 × 4ⁿ 90° / 2ⁿ

Mathematical construction

  1. Octahedron – 6 vertices at ±x, ±y, ±z on the unit sphere, forming 8 triangular faces (4 northern, 4 southern).
  2. Subdivision – for each triangle [v₀, v₁, v₂], three edge midpoints are computed and projected onto the unit sphere::

    m₀ = normalize((v₀ + v₁) / 2) m₁ = normalize((v₁ + v₂) / 2) m₂ = normalize((v₂ + v₀) / 2)

The four children are [v₀, m₀, m₂], [v₁, m₁, m₀], [v₂, m₂, m₁], and [m₀, m₁, m₂]. This is repeated htm_level times. 3. Hemisphere filter – a triangle is kept if its centroid (the 3D Cartesian mean of the three vertices, renormalised to the unit sphere) satisfies theta ≤ π/2 − cutoff_theta. Filtering on centroid rather than "any vertex above" avoids keeping boundary triangles that are mostly below the horizon; kept triangles may still extend slightly across the horizon since cells are not clipped to an exact circle. 4. Each leaf triangle becomes one cell; its center, bounding box, and three vertex coordinates are stored.

Parameters:

Name Type Description Default
angular_resolution float

Approximate angular resolution in degrees. Used only to derive htm_level when that parameter is not given explicitly.

2
cutoff_theta float

Elevation mask angle in degrees. Triangles are excluded if their centroid is below this elevation.

0
htm_level int or None

HTM subdivision depth. If None, estimated from angular_resolution. Practical range 0–15.

None
phi_rotation float

Rigid azimuthal rotation applied after construction, in degrees.

0

Notes

The theta_lims, phi_lims, and cell_ids fields of the returned GridData are synthetic evenly-spaced arrays kept only for interface compatibility with ring-based grids. They do not describe the actual triangular cell layout.

HTM IDs in this implementation use a decimal-digit scheme (parent_id × 10 + child_index) which diverges from the original SDSS HTM binary-coded ID scheme. This is adequate for indexing but should not be compared with external HTM catalogs.

References

Kunszt et al. (2001): "The Hierarchical Triangular Mesh" https://www.sdss.org/dr12/algorithms/htm/

__init__(angular_resolution=2, cutoff_theta=0, htm_level=None, phi_rotation=0)

Initialize the HTM grid builder.

Parameters:

Name Type Description Default
angular_resolution float

Angular resolution in degrees.

2
cutoff_theta float

Maximum polar angle cutoff in degrees.

0
htm_level int | None

HTM subdivision level.

None
phi_rotation float

Rotation angle in degrees.

0

get_grid_type()

Return the grid-type identifier string.

Returns:

Type Description
str

"htm"

get_htm_info()

Get HTM-specific information.

Returns:

Name Type Description
info dict

Keys: htm_level, n_triangles_full_sphere, approx_edge_length_deg, approx_edge_length_arcmin.

Grid operations

Cell assignment and vertex extraction for hemisphere grids.

Functions in this module operate on :class:~canvod.grids.core.GridData instances and VOD xarray Datasets.

Cell assignment

add_cell_ids_to_ds_fast – the cell that contains each observation, eager or dask-lazy

Vertex / grid conversion

extract_grid_vertices – flat (x, y, z) arrays for 3-D visualization grid_to_dataset – xarray Dataset with vertices and solid angles

add_cell_ids_to_ds_fast(ds, grid, grid_name)

Assign every observation to the grid cell that contains it.

Each grid type is looked up by its own cell boundaries: theta band and phi sector for equal_area, equal_angle and equirectangular; the spherical triangle for htm and geodesic; the pixel for healpix; the Voronoi cell for fibonacci. Bands and sectors are closed at their inner edge and open at their outer edge; the last band also includes its outer edge.

An observation outside the grid gets NaN: below the outer edge of the last band of a ring grid (the horizon, or 90° - cutoff_theta), or, for the triangle, pixel and Voronoi grids, in a cell the hemisphere filter left out (these grids keep a cell when its center lies above 90° - cutoff_theta, so their edge near the horizon is not a circle).

phi and theta may be data variables (as the runs write them) or coordinates, with (epoch, sid) dimensions in either order. If they are dask arrays, the cell IDs are computed lazily, block by block.

Parameters:

Name Type Description Default
ds Dataset

Dataset with phi(epoch, sid) and theta(epoch, sid) in radians.

required
grid GridData

Hemisphere grid built with create_hemigrid(). A grid loaded with canvod.store.load_grid() works for the ring grids only; the others lose the geometry the lookup needs (ValueError).

required
grid_name str

Grid identifier; the output variable is cell_id_<grid_name>.

required

Returns:

Type Description
Dataset

ds with a cell_id_<grid_name>(epoch, sid) float64 data variable; NaN where phi or theta is not finite or the observation lies outside the grid.

extract_grid_vertices(grid)

Extract 3D vertices from hemisphere grid cells.

Dispatches to a grid-type–specific extractor. The returned arrays are flat (not per-cell); use them directly for 3-D scatter plots.

Parameters:

Name Type Description Default
grid GridData

Hemisphere grid instance.

required

Returns:

Type Description
x_vertices, y_vertices, z_vertices : np.ndarray

Cartesian vertex coordinates on the unit sphere.

grid_to_dataset(grid)

Convert a HemiGrid to a unified xarray Dataset with vertices.

The returned Dataset carries cell centers, NaN-padded vertex arrays, vertex counts, and solid angles, all indexed by cell_id.

Parameters:

Name Type Description Default
grid GridData

Hemisphere grid instance.

required

Returns:

Type Description
Dataset

Dataset with dimensions (cell_id, vertex) and variables cell_phi, cell_theta, vertices_phi, vertices_theta, n_vertices, solid_angle.

Aggregation

Per-cell aggregation of VOD observations onto hemisphere grids.

Top-level entry points

aggregate_data_to_grid – single-statistic spatial aggregation compute_percell_timeseries – chunked (cell × time) time-series

Analysis helpers

compute_global_average – observation-count–weighted global mean compute_regional_average – same, restricted to a cell subset analyze_diurnal_patterns – hourly groupby analyze_spatial_patterns – time-averaged spatial field

Convenience wrappers

compute_hemisphere_percell – daily, full hemisphere compute_zenith_percell – daily, θ ≤ 30°

CellAggregator

Polars-based per-cell aggregation helpers.

aggregate_by_cell(df, value_var='VOD', method='mean') staticmethod

Aggregate values by cell_id.

Parameters:

Name Type Description Default
df DataFrame

Must contain cell_id and value_var columns.

required
value_var str

Column to aggregate.

'VOD'
method ('mean', 'median', 'std', 'count')

Aggregation method.

'mean'

Returns:

Type Description
DataFrame

Two-column DataFrame: cell_id, value_var.

aggregate_data_to_grid(data_ds, grid, value_var='VOD', cell_var='cell_id_equal_area_2deg', sid=None, time_range=None, stat='median')

Aggregate VOD data across all timestamps and SIDs to per-cell statistics.

Parameters:

Name Type Description Default
data_ds Dataset

Full VOD dataset from the Icechunk store.

required
grid GridData

Grid definition.

required
value_var str

Name of the VOD variable.

'VOD'
cell_var str

Name of the cell-ID variable in data_ds.

'cell_id_equal_area_2deg'
sid list[str]

Satellite IDs to include. None → all.

None
time_range tuple

(start, end) datetimes for epoch filtering.

None
stat ('mean', 'median', 'std')

Statistic to compute per cell.

'mean'

Returns:

Type Description
ndarray

Array of length grid.ncells with per-cell aggregated values (NaN where no observations exist).

compute_percell_timeseries(data_ds, grid, value_var='VOD', cell_var='cell_id_equal_area_2deg', theta_range=None, phi_range=None, selected_sids=None, time_range=None, temporal_resolution='1D', chunk_days=21, min_obs_per_cell_time=1, stat='mean')

Compute time series per cell with SID aggregation.

Processing is chunked over time to bound memory usage.

Parameters:

Name Type Description Default
data_ds Dataset

Full VOD dataset.

required
grid GridData

Grid definition.

required
value_var str

VOD variable name.

'VOD'
cell_var str

Cell-ID variable name.

'cell_id_equal_area_2deg'
theta_range tuple

(min_deg, max_deg) elevation filter.

None
phi_range tuple

(min_deg, max_deg) azimuth filter (wraps at 360°).

None
selected_sids list[str]

Satellite IDs to include.

None
time_range tuple

(start, end) epoch slice.

None
temporal_resolution str

Pandas/polars frequency string (e.g. "1D", "30min").

'1D'
chunk_days int

Days per processing chunk.

21
min_obs_per_cell_time int

Minimum SID observations per (cell, time-bin) to retain.

1
stat ('mean', 'median')

Statistic to compute per (cell, time-bin), mirroring :func:aggregate_data_to_grid's stat parameter.

'mean'

Returns:

Type Description
Dataset

Dataset with dimensions (cell, time) and variables cell_timeseries, cell_weights, cell_counts, cell_theta, cell_phi.

compute_global_average(percell_ds)

Compute observation-count–weighted global average from per-cell data.

Parameters:

Name Type Description Default
percell_ds Dataset

Output of :func:compute_percell_timeseries.

required

Returns:

Type Description
Dataset

Variables: global_timeseries, spatial_std, total_weights, active_cells.

compute_regional_average(percell_ds, region_cells)

Compute observation-count–weighted average for a cell subset.

Parameters:

Name Type Description Default
percell_ds Dataset

Output of :func:compute_percell_timeseries.

required
region_cells array - like

Cell IDs defining the region.

required

Returns:

Type Description
DataArray

Weighted regional mean time series.

analyze_diurnal_patterns(percell_ds)

Compute hourly means from per-cell time series.

Parameters:

Name Type Description Default
percell_ds Dataset

Output of :func:compute_percell_timeseries.

required

Returns:

Type Description
Dataset

Grouped by time.hour.

analyze_spatial_patterns(percell_ds)

Compute time-averaged spatial field.

Parameters:

Name Type Description Default
percell_ds Dataset

Output of :func:compute_percell_timeseries.

required

Returns:

Type Description
Dataset

Time-averaged dataset.

compute_hemisphere_percell(data_ds, grid, **kwargs)

Daily per-cell time series for the full hemisphere.

compute_zenith_percell(data_ds, grid, **kwargs)

Daily per-cell time series restricted to θ ≤ 30°.

Analysis

Filtering

Global (dataset-wide) outlier filters for gridded VOD data.

Classes:

Name Description
Filter – abstract base; ``compute_mask`` / ``apply`` contract.
ZScoreFilter – mean ± k·σ rejection.
IQRFilter – Q1 – f·IQR / Q3 + f·IQR rejection.
RangeFilter – hard min/max bounds.
PercentileFilter – lower/upper percentile bounds.
CustomFilter – user-supplied callable mask.
FilterPipeline – sequential or combined multi-filter application.

Convenience functions

create_zscore_filter – one-liner z-score filter. create_range_filter – one-liner range filter.

Notes

  • Filters never modify original data. apply returns a new xr.Dataset with <var>_filtered_<n> and mask_<n> variables appended.
  • Both numpy and dask-backed arrays are supported; dask paths compute only the scalar statistics eagerly while the mask itself stays lazy.

Filter

Bases: ABC

Base class for all filters. Filters NEVER modify original data.

__init__(name)

Initialize the filter.

Parameters:

Name Type Description Default
name str

Filter name.

required

compute_mask(data, **kwargs) abstractmethod

Compute boolean mask (True = keep, False = remove).

apply(ds, var_name, output_suffix=None, **kwargs)

Apply filter to ds, returning a copy with filtered variable added.

New variables

<var_name>_filtered_<suffix> : filtered data (NaN where masked). mask_<suffix> : boolean keep-mask.

ZScoreFilter

Bases: Filter

Remove statistical outliers using z-score method.

__init__()

Initialize the filter.

compute_mask(data, threshold=3.0, **kwargs)

Compute z-score mask.

Parameters:

Name Type Description Default
data DataArray

Input data.

required
threshold float

Z-score threshold (default: 3.0).

3.0

Returns:

Type Description
DataArray

Boolean mask (True = keep).

IQRFilter

Bases: Filter

Remove outliers using Interquartile Range method.

__init__()

Initialize the filter.

compute_mask(data, factor=1.5, **kwargs)

Compute IQR mask.

Parameters:

Name Type Description Default
data DataArray

Input data.

required
factor float

IQR factor (default: 1.5).

1.5

Returns:

Type Description
DataArray

Boolean mask (True = keep).

RangeFilter

Bases: Filter

Filter values outside specified range.

__init__()

Initialize the filter.

compute_mask(data, min_value=None, max_value=None, **kwargs)

Compute range mask.

Parameters:

Name Type Description Default
data DataArray

Input data.

required
min_value float

Minimum allowed value.

None
max_value float

Maximum allowed value.

None

Returns:

Type Description
DataArray

Boolean mask (True = keep).

PercentileFilter

Bases: Filter

Filter values outside percentile range.

__init__()

Initialize the filter.

compute_mask(data, lower=5.0, upper=95.0, **kwargs)

Compute percentile mask.

Parameters:

Name Type Description Default
data DataArray

Input data.

required
lower float

Lower percentile (0–100).

5.0
upper float

Upper percentile (0–100).

95.0

Returns:

Type Description
DataArray

Boolean mask (True = keep).

CustomFilter

Bases: Filter

Apply a user-supplied callable as filter.

Parameters:

Name Type Description Default
name str

Filter identifier.

required
func callable

(xr.DataArray, **kwargs) -> xr.DataArray returning a boolean mask.

required

__init__(name, func)

Initialize the custom filter.

Parameters:

Name Type Description Default
name str

Filter identifier.

required
func Callable[..., DataArray]

Callable returning a boolean mask.

required

compute_mask(data, **kwargs)

Apply custom function.

SIDPatternFilter

Bases: Filter

Filter dataset by GNSS system, frequency band, and tracking code.

Operates on the sid dimension of a dataset, where SIDs have the format SV|Band|Code (e.g. G01|L1|C).

Parameters:

Name Type Description Default
system str or None

GNSS system prefix to keep (e.g. 'G', 'E', 'R', 'C'). None keeps all systems.

None
band str or None

Frequency band to keep (e.g. 'L1', 'E1', 'L5'). None keeps all bands.

None
code str or None

Tracking code to keep (e.g. 'C', 'L', 'W'). None keeps all codes.

None

Examples:

>>> filt = SIDPatternFilter(system="G", band="L1", code="C")
>>> ds_gps_l1c = filt.filter_dataset(ds)

compute_mask(data, **kwargs)

Compute boolean mask along the sid dimension.

Parameters:

Name Type Description Default
data DataArray

Input data with a sid dimension.

required

Returns:

Type Description
DataArray

Boolean mask (True = keep) broadcast to data shape.

filter_dataset(ds)

Filter dataset to matching SIDs by slicing the sid dimension.

Unlike apply (which masks values with NaN), this method drops non-matching SIDs entirely — reducing the dataset size.

Parameters:

Name Type Description Default
ds Dataset

Dataset with a sid dimension.

required

Returns:

Type Description
Dataset or None

Filtered dataset, or None if no SIDs match.

FilterPipeline

Manage multiple filters applied sequentially or combined.

Non-destructive: creates new DataArrays, never modifies originals.

Parameters:

Name Type Description Default
ds Dataset

Input dataset.

required
var_name str

Variable to filter (default: 'VOD').

'VOD'

__init__(ds, var_name='VOD')

Initialize the filter pipeline.

Parameters:

Name Type Description Default
ds Dataset

Input dataset.

required
var_name str

Variable to filter.

"VOD"

add_filter(filter_obj, **kwargs)

Add filter to pipeline.

Parameters:

Name Type Description Default
filter_obj Filter or str

Filter instance or short name ('zscore', 'iqr', 'range', 'percentile').

required
**kwargs Any

Parameters forwarded to compute_mask.

{}

Returns:

Type Description
FilterPipeline

Self (for chaining).

apply(mode='sequential', output_name=None)

Apply all filters in the pipeline.

Parameters:

Name Type Description Default
mode ('sequential', 'combined')

'sequential' – masks accumulate (AND) after each filter; intermediate filtered variables are written. 'combined' – all masks computed independently on the original data, then AND-ed once.

'sequential'
output_name str

Alias for the final filtered variable.

None

Returns:

Type Description
Dataset

Dataset with filtered variables appended.

summary()

Return a human-readable summary of the pipeline.

create_zscore_filter(ds, var_name='VOD', threshold=3.0, suffix='zscore')

One-liner z-score filter.

create_range_filter(ds, var_name='VOD', min_value=None, max_value=None, suffix='range')

One-liner range filter.

Per-cell filtering

Per-cell outlier filters for gridded VOD data.

Unlike the global filters in :mod:~canvod.grids.analysis.filtering, these operate independently on each grid cell, preserving spatial structure while removing temporal outliers within cells.

Classes:

Name Description
PerCellFilter – abstract base with auto cell-id detection.
PerCellIQRFilter – per-cell IQR rejection.
PerCellZScoreFilter – per-cell z-score rejection.
PerCellRangeFilter – per-cell hard bounds.
PerCellPercentileFilter – per-cell percentile bounds.
PerCellFilterPipeline – sequential or combined multi-filter application.

Convenience functions

create_per_cell_iqr_filter – one-liner per-cell IQR. create_per_cell_zscore_filter – one-liner per-cell z-score.

PerCellFilter

Bases: ABC

Base class for per-cell filtering operations.

Sub-classes implement :meth:compute_cell_mask for a single cell's 1-D data array; the base class handles iteration over cells, auto-detection of the cell_id_* variable, and output assembly.

__init__(filter_name)

Initialize the per-cell filter.

Parameters:

Name Type Description Default
filter_name str

Filter name.

required

compute_cell_mask(cell_data, **kwargs) abstractmethod

Return a boolean keep-mask for one cell's data.

Parameters:

Name Type Description Default
cell_data ndarray

1-D array of values for one cell across time.

required

Returns:

Type Description
ndarray

Boolean mask (True = keep).

apply(ds, var_name='VOD', cell_id_var=None, output_suffix=None, min_observations=5, **kwargs)

Apply per-cell filtering to ds.

Parameters:

Name Type Description Default
ds Dataset

Input dataset (must contain a cell_id_* variable).

required
var_name str

Variable to filter.

'VOD'
cell_id_var str

Cell-ID variable name. Auto-detected from cell_id_* if None.

None
output_suffix str

Suffix for output variables (default: filter_name).

None
min_observations int

Minimum observations per cell required for filtering.

5
**kwargs Any

Forwarded to :meth:compute_cell_mask.

{}

Returns:

Type Description
Dataset

Copy of ds with <var>_filtered_<suffix> and mask_<suffix> appended.

PerCellIQRFilter

Bases: PerCellFilter

Per-cell IQR outlier rejection.

Values outside [Q1 − factor·IQR, Q3 + factor·IQR] are removed independently within each cell.

__init__()

Initialize the filter.

compute_cell_mask(cell_data, factor=1.5, **kwargs)

IQR mask for a single cell.

Parameters:

Name Type Description Default
cell_data ndarray

1-D cell data.

required
factor float

IQR multiplier (default 1.5).

1.5

Returns:

Type Description
ndarray

Boolean mask.

PerCellZScoreFilter

Bases: PerCellFilter

Per-cell z-score outlier rejection.

Values with |z| > threshold are removed independently within each cell.

__init__()

Initialize the filter.

compute_cell_mask(cell_data, threshold=3.0, **kwargs)

Z-score mask for a single cell.

Parameters:

Name Type Description Default
cell_data ndarray

1-D cell data.

required
threshold float

Z-score threshold (default 3.0).

3.0

Returns:

Type Description
ndarray

Boolean mask.

PerCellRangeFilter

Bases: PerCellFilter

Per-cell hard-bound range filter.

Identical bounds applied to every cell.

__init__()

Initialize the filter.

compute_cell_mask(cell_data, min_value=None, max_value=None, **kwargs)

Range mask for a single cell.

Parameters:

Name Type Description Default
cell_data ndarray

1-D cell data.

required
min_value float

Minimum allowed value.

None
max_value float

Maximum allowed value.

None

Returns:

Type Description
ndarray

Boolean mask.

PerCellPercentileFilter

Bases: PerCellFilter

Per-cell percentile-bound filter.

Bounds are computed independently within each cell.

__init__()

Initialize the filter.

compute_cell_mask(cell_data, lower=5.0, upper=95.0, **kwargs)

Percentile mask for a single cell.

Parameters:

Name Type Description Default
cell_data ndarray

1-D cell data.

required
lower float

Lower percentile (0–100).

5.0
upper float

Upper percentile (0–100).

95.0

Returns:

Type Description
ndarray

Boolean mask.

PerCellFilterPipeline

Sequential or combined multi-filter pipeline for per-cell filtering.

Parameters:

Name Type Description Default
ds Dataset

Input dataset.

required
var_name str

Variable to filter (default: 'VOD').

'VOD'

__init__(ds, var_name='VOD')

Initialize the filter pipeline.

Parameters:

Name Type Description Default
ds Dataset

Input dataset.

required
var_name str

Variable to filter.

"VOD"

add_filter(filter_obj, **kwargs)

Add a filter.

Parameters:

Name Type Description Default
filter_obj PerCellFilter or str

Filter instance or short name ('iqr', 'zscore', 'range', 'percentile').

required
**kwargs Any

Parameters forwarded to the filter.

{}

Returns:

Type Description
PerCellFilterPipeline

Self (for chaining).

apply(mode='sequential', output_name=None)

Apply all filters.

Parameters:

Name Type Description Default
mode ('sequential', 'combined')

'sequential' – each filter operates on the previous output. 'combined' – all masks computed on the original, then AND-ed.

'sequential'
output_name str

Alias for the final filtered variable.

None

Returns:

Type Description
Dataset

Dataset with filtered results.

create_per_cell_iqr_filter(ds, var_name='VOD', factor=1.5, cell_id_var=None, min_observations=5)

One-liner per-cell IQR filter.

create_per_cell_zscore_filter(ds, var_name='VOD', threshold=3.0, cell_id_var=None, min_observations=5)

One-liner per-cell z-score filter.

Masking

Spatial masking for hemispherical grid cells.

Provides tools to create boolean masks for selecting subsets of grid cells based on geometric constraints, data quality, or custom criteria.

Classes:

Name Description
SpatialMask – builder for boolean cell-selection masks.

Convenience functions

create_hemisphere_mask – north / south / east / west mask. create_elevation_mask – elevation-angle-based mask.

SpatialMask

Create spatial masks for grid cells.

Masks are boolean arrays where True = include cell, False = exclude cell. Multiple constraints can be combined with AND or OR logic.

Parameters:

Name Type Description Default
grid GridData

Grid instance.

required

Examples:

>>> mask = SpatialMask(grid)
>>> mask.add_phi_range(0, np.pi)          # Northern hemisphere
>>> mask.add_theta_range(0, np.pi / 3)    # Exclude low elevations
>>> mask.add_quality_threshold('mean_snr', min_value=40)
>>> spatial_mask = mask.compute()          # Returns boolean array

__init__(grid)

Initialize the spatial mask builder.

Parameters:

Name Type Description Default
grid GridData

Grid instance.

required

add_phi_range(phi_min, phi_max, degrees=False)

Add azimuth angle constraint.

Parameters:

Name Type Description Default
phi_min float

Minimum azimuth angle.

required
phi_max float

Maximum azimuth angle.

required
degrees bool

If True, angles are in degrees; otherwise radians.

False

Returns:

Type Description
SpatialMask

Self for chaining.

Notes

Handles wraparound at 0°/360° correctly. Example: phi_range(350, 10) includes both 350°–360° and 0°–10°.

add_theta_range(theta_min, theta_max, degrees=False)

Add polar angle constraint.

Parameters:

Name Type Description Default
theta_min float

Minimum polar angle (0 = zenith).

required
theta_max float

Maximum polar angle (π/2 = horizon).

required
degrees bool

If True, angles are in degrees; otherwise radians.

False

Returns:

Type Description
SpatialMask

Self for chaining.

Notes

theta = 0° is zenith (straight up), theta = 90° is horizon. To exclude low elevations, use theta_max < 90°.

add_elevation_range(elev_min, elev_max, degrees=True)

Add elevation angle constraint (complementary to theta).

Parameters:

Name Type Description Default
elev_min float

Minimum elevation angle (0 = horizon, 90 = zenith).

required
elev_max float

Maximum elevation angle.

required
degrees bool

If True, angles are in degrees; otherwise radians. Default: True.

True

Returns:

Type Description
SpatialMask

Self for chaining.

Notes

elevation = 90° − theta. This is more intuitive for users who think in elevation angles.

add_cell_ids(cell_ids)

Include specific cell IDs.

Parameters:

Name Type Description Default
cell_ids list[int] or ndarray

Cell IDs to include.

required

Returns:

Type Description
SpatialMask

Self for chaining.

add_exclude_cell_ids(cell_ids)

Exclude specific cell IDs.

Parameters:

Name Type Description Default
cell_ids list[int] or ndarray

Cell IDs to exclude.

required

Returns:

Type Description
SpatialMask

Self for chaining.

add_quality_threshold(var_name, min_value=None, max_value=None)

Add data quality threshold based on grid cell properties.

Parameters:

Name Type Description Default
var_name str

Variable name in grid (e.g. 'mean_snr', 'n_observations').

required
min_value float

Minimum allowed value.

None
max_value float

Maximum allowed value.

None

Returns:

Type Description
SpatialMask

Self for chaining.

Raises:

Type Description
ValueError

If var_name doesn't exist in the grid DataFrame.

add_boundary_cells(exclude=True)

Include or exclude boundary cells.

Parameters:

Name Type Description Default
exclude bool

If True, exclude boundary cells; if False, include only boundary cells.

True

Returns:

Type Description
SpatialMask

Self for chaining.

Raises:

Type Description
ValueError

If the grid does not have an 'is_boundary' column.

Notes

Only works if the grid DataFrame contains an 'is_boundary' column.

add_custom_mask(mask, name='custom')

Add custom mask or mask-generating callable.

Parameters:

Name Type Description Default
mask ndarray or callable
  • If array: boolean mask of shape (ncells,).
  • If callable: function (grid: GridData) -> np.ndarray.
required
name str

Label for this mask (used in summary).

'custom'

Returns:

Type Description
SpatialMask

Self for chaining.

Raises:

Type Description
ValueError

If the resulting array has wrong shape or dtype.

Examples:

>>> custom = np.array([True, False, True, ...])
>>> mask.add_custom_mask(custom)
>>> def high_snr_north(grid):
...     snr   = grid.grid['mean_snr'].to_numpy()
...     phi   = grid.grid['phi'].to_numpy()
...     return (snr > 40) & (phi < np.pi)
>>> mask.add_custom_mask(high_snr_north)

add_radial_sector(center_phi, sector_width, theta_min=0.0, theta_max=np.pi / 2, degrees=True)

Add radial sector (wedge) mask.

Parameters:

Name Type Description Default
center_phi float

Center azimuth of sector.

required
sector_width float

Full angular width of sector.

required
theta_min float

Minimum polar angle (radial inner bound).

0.0
theta_max float

Maximum polar angle (radial outer bound).

pi / 2
degrees bool

If True, all angles in degrees; otherwise radians.

True

Returns:

Type Description
SpatialMask

Self for chaining.

Examples:

>>> # Northern sector, 30° wide, excluding low elevations
>>> mask.add_radial_sector(center_phi=0, sector_width=30,
...                        theta_max=60, degrees=True)

compute(mode='AND')

Compute final combined boolean mask.

Parameters:

Name Type Description Default
mode str

Combination mode:

  • 'AND' – all constraints must be satisfied (intersection).
  • 'OR' – at least one constraint must be satisfied (union).
'AND'

Returns:

Type Description
ndarray

Boolean array of shape (ncells,) where True = include.

Raises:

Type Description
ValueError

If no masks have been added or mode is unknown.

get_mask_summary()

Get summary of all added masks.

Returns:

Type Description
dict

Dictionary with mask names and per-mask / combined cell counts.

clear()

Clear all masks.

Returns:

Type Description
SpatialMask

Self for chaining.

__repr__()

Return the developer-facing representation.

Returns:

Type Description
str

Representation string.

create_hemisphere_mask(grid, hemisphere='north')

Create mask for a cardinal hemisphere.

Parameters:

Name Type Description Default
grid GridData

Grid instance.

required
hemisphere str

One of 'north', 'south', 'east', 'west'.

'north'

Returns:

Type Description
ndarray

Boolean mask.

Raises:

Type Description
ValueError

If hemisphere is not recognized.

create_elevation_mask(grid, min_elevation=30.0, max_elevation=90.0)

Create mask based on elevation angle.

Parameters:

Name Type Description Default
grid GridData

Grid instance.

required
min_elevation float

Minimum elevation angle in degrees (default: 30°).

30.0
max_elevation float

Maximum elevation angle in degrees (default: 90°).

90.0

Returns:

Type Description
ndarray

Boolean mask.

Weighting

Weighting strategies for spatial aggregation of hemispherical grid data.

Provides tools to calculate and combine different weighting schemes for computing weighted means across grid cells. Critical for unbiased spatial statistics when cells have different sizes or data quality.

Classes:

Name Description
WeightCalculator – builder for combined spatial weights.

Convenience functions

compute_uniform_weights – equal weight per cell. compute_area_weights – solid-angle-only weights.

Notes

  • Supported weight types: solid_angle, observation_count, snr, sin_elevation, inverse_variance, custom.
  • Multiple weights are combined element-wise (multiply or add) and optionally normalized to sum to 1.
  • Dask-backed datasets are handled efficiently: only scalar statistics are computed eagerly; masks stay lazy.

WeightCalculator

Calculate and combine weights for spatial aggregation.

Parameters:

Name Type Description Default
grid GridData

Grid instance.

required
ds Dataset or None

Dataset with data variables (required for data-dependent weights such as observation_count, snr, inverse_variance).

None

Examples:

>>> weights = WeightCalculator(grid, vod_ds)
>>> weights.add_weight('solid_angle')
>>> weights.add_weight('observation_count', normalize=True)
>>> total_weights = weights.compute()

__init__(grid, ds=None)

Initialize the weight calculator.

Parameters:

Name Type Description Default
grid GridData

Grid instance.

required
ds Dataset | None

Dataset with data variables for data-dependent weights.

None

add_weight(weight_type, normalize=True, **kwargs)

Add a weight component.

Parameters:

Name Type Description Default
weight_type str

One of 'solid_angle', 'observation_count', 'snr', 'sin_elevation', 'inverse_variance', 'custom'.

required
normalize bool

If True, normalize this component to sum to 1.0 before combination.

True
**kwargs Any

Weight-specific parameters (see individual _compute_* methods).

{}

Returns:

Type Description
WeightCalculator

Self for chaining.

Examples:

>>> calc.add_weight('solid_angle')
>>> calc.add_weight('observation_count', var_name='VOD', normalize=True)
>>> calc.add_weight('custom', values=my_weights, normalize=False)

compute(combination='multiply', normalize_final=True)

Compute final combined weights.

Parameters:

Name Type Description Default
combination str

'multiply' – element-wise product (default). 'add' – element-wise sum.

'multiply'
normalize_final bool

If True, normalize the final array to sum to 1.0.

True

Returns:

Type Description
ndarray

Weight array of shape (ncells,).

Raises:

Type Description
ValueError

If no weights have been added or combination is unknown.

get_weight_summary()

Summary statistics for each weight component.

Returns:

Type Description
dict

Nested dict keyed by weight type.

remove_weight(weight_type)

Remove a weight component.

Parameters:

Name Type Description Default
weight_type str

Weight type to remove.

required

Returns:

Type Description
WeightCalculator

Self for chaining.

clear()

Clear all weights.

Returns:

Type Description
WeightCalculator

Self for chaining.

__repr__()

Return the developer-facing representation.

Returns:

Type Description
str

Representation string.

compute_uniform_weights(grid)

Uniform weights (all cells equal).

Parameters:

Name Type Description Default
grid GridData

Grid instance.

required

Returns:

Type Description
ndarray

Array of 1 / ncells for each cell.

compute_area_weights(grid, normalize=True)

Weights based on cell solid angles only.

Parameters:

Name Type Description Default
grid GridData

Grid instance.

required
normalize bool

If True, normalize to sum to 1.0.

True

Returns:

Type Description
ndarray

Area-based weights.

Solar geometry

Solar position calculations and corrections for VOD data.

Provides tools to compute solar positions and apply corrections to account for solar radiation effects on vegetation optical depth measurements.

Classes:

Name Description
SolarPositionCalculator – solar zenith / azimuth and VOD correction.

Convenience functions

compute_solar_zenith – quick zenith-only computation. filter_daytime_data – mask nighttime observations.

Notes

  • When pvlib is installed it is preferred for high-accuracy calculations. The built-in fallback uses NOAA algorithms (accuracy ~0.01° for 1800–2100).
  • All public methods accept either np.ndarray of datetime64 or pd.DatetimeIndex.

SolarPositionCalculator

Calculate solar positions for VOD corrections.

Parameters:

Name Type Description Default
lat float

Observer latitude in degrees (positive = North).

required
lon float

Observer longitude in degrees (positive = East).

required
elevation float

Elevation above sea level in meters (default: 0).

0.0
use_pvlib bool

If True, use pvlib when available (more accurate). Falls back to built-in formulas automatically.

True

Examples:

>>> calc = SolarPositionCalculator(lat=40.0, lon=-105.0, elevation=1655)
>>> times = pd.date_range('2025-01-01', periods=24, freq='1H')
>>> zenith, azimuth = calc.compute_solar_position(times)
>>> corrected = calc.apply_solar_correction(vod_data, times)

__init__(lat, lon, elevation=0.0, use_pvlib=True)

Initialize the solar position calculator.

Parameters:

Name Type Description Default
lat float

Observer latitude in degrees.

required
lon float

Observer longitude in degrees.

required
elevation float

Elevation above sea level in meters.

0.0
use_pvlib bool

Whether to use pvlib if available.

True

compute_solar_position(times)

Compute solar zenith and azimuth angles.

Parameters:

Name Type Description Default
times ndarray or DatetimeIndex

Array of datetime64 or DatetimeIndex.

required

Returns:

Name Type Description
solar_zenith ndarray

Solar polar angles in degrees (0° = directly overhead).

solar_azimuth ndarray

Solar azimuth angles in degrees (0° = North, 90° = East).

compute_solar_elevation(times)

Compute solar elevation angle (complementary to zenith).

Parameters:

Name Type Description Default
times ndarray or DatetimeIndex

Array of times.

required

Returns:

Type Description
ndarray

Solar elevation angles in degrees (0° = horizon, 90° = overhead).

is_daytime(times, twilight_angle=-6.0)

Determine if times are during daytime.

Parameters:

Name Type Description Default
times ndarray or DatetimeIndex

Array of times.

required
twilight_angle float

Solar elevation threshold in degrees. Common values: 0 (geometric), -6 (civil), -12 (nautical), -18 (astronomical).

-6.0

Returns:

Type Description
ndarray

Boolean array (True = daytime).

apply_solar_correction(data, method='normalize', reference_zenith=45.0)

Apply solar correction to data.

Parameters:

Name Type Description Default
data DataArray

Input data with a time dimension ('epoch' or 'time').

required
method str

Correction method:

  • 'normalize' – normalize by cos(zenith) relative to reference_zenith.
  • 'residual' – subtract a 4th-order polynomial fitted to the diurnal pattern (1-D data only; falls back to 'normalize' for multi-dimensional data).
  • 'cos_correction' – simple cosine correction.
'normalize'
reference_zenith float

Reference polar angle for normalization (degrees).

45.0

Returns:

Type Description
DataArray

Solar-corrected data with correction metadata in attrs.

compute_solar_bins(times, n_bins=12)

Bin times by solar elevation angle.

Useful for solar-elevation-based composites instead of hour-of-day composites.

Parameters:

Name Type Description Default
times ndarray or DatetimeIndex

Array of times.

required
n_bins int

Number of solar elevation bins (range −20° to 90°).

12

Returns:

Type Description
ndarray

Bin indices (0-based) for each time.

get_sunrise_sunset(date)

Compute sunrise and sunset times for a given date.

Parameters:

Name Type Description Default
date datetime

Date to compute sunrise/sunset for.

required

Returns:

Name Type Description
sunrise Timestamp or None

Sunrise time in UTC, or None if the sun never rises.

sunset Timestamp or None

Sunset time in UTC, or None if the sun never sets.

__repr__()

Return the developer-facing representation.

Returns:

Type Description
str

Representation string.

compute_solar_zenith(lat, lon, times)

Quick computation of solar polar angles.

Parameters:

Name Type Description Default
lat float

Observer latitude in degrees.

required
lon float

Observer longitude in degrees.

required
times ndarray or DatetimeIndex

Times to compute for.

required

Returns:

Type Description
ndarray

Solar polar angles in degrees.

filter_daytime_data(data, lat, lon, twilight_angle=-6.0)

Filter data to include only daytime observations.

Nighttime values are set to NaN.

Parameters:

Name Type Description Default
data DataArray

Data with a time dimension ('epoch' or 'time').

required
lat float

Observer latitude in degrees.

required
lon float

Observer longitude in degrees.

required
twilight_angle float

Elevation threshold for daytime (degrees below horizon).

-6.0

Returns:

Type Description
DataArray

Filtered data.

Temporal analysis

Temporal analysis of gridded VOD data.

Weighted time-series computation, diurnal cycle analysis, and temporal statistics with optional solar-position correction.

Classes:

Name Description
``TemporalAnalysis``

Main analysis class; binds a VOD dataset to a grid and exposes methods for aggregation, solar correction, diurnal binning, and basic plotting.

Notes

  • All spatial masks and weight arrays must be 1-D with length grid.ncells.
  • When a SolarPositionCalculator is attached (via site_lat / site_lon), additional solar-corrected and solar-binned methods become available.
  • Plotting helpers are thin wrappers around matplotlib; they return (fig, ax) so callers can continue customising the figure.

TemporalAnalysis

Temporal analysis of gridded VOD data.

Binds a VOD dataset (with pre-assigned cell IDs) to a grid and exposes weighted aggregation, diurnal analysis, and plotting.

Parameters:

Name Type Description Default
vod_ds Dataset

Dataset containing VOD data and a cell_id_<grid_name> variable.

required
grid GridData

Grid instance (must expose .ncells).

required
grid_name str

Suffix for the cell-ID variable (e.g. 'htm_10deg').

required
site_lat float or None

Site latitude in degrees. Required for solar methods.

None
site_lon float or None

Site longitude in degrees. Required for solar methods.

None
site_elevation float

Site elevation in meters (default 0).

0.0

Raises:

Type Description
ValueError

If cell_id_<grid_name> is not present in vod_ds.

Examples:

>>> analysis = TemporalAnalysis(vod_ds, grid, "htm_10deg")
>>> ts = analysis.compute_timeseries(aggregate="1D")

__init__(vod_ds, grid, grid_name, site_lat=None, site_lon=None, site_elevation=0.0)

Initialize the temporal analysis helper.

Parameters:

Name Type Description Default
vod_ds Dataset

VOD dataset containing cell IDs.

required
grid GridData

Grid instance.

required
grid_name str

Grid name suffix for cell IDs.

required
site_lat float | None

Site latitude in degrees.

None
site_lon float | None

Site longitude in degrees.

None
site_elevation float

Site elevation in meters.

0.0

compute_timeseries(var_name='VOD', spatial_mask=None, weights=None, aggregate='1D', min_cells=1)

Compute a weighted time-series aggregated over space.

Parameters:

Name Type Description Default
var_name str

Data variable to aggregate.

'VOD'
spatial_mask ndarray or None

Boolean mask of shape (grid.ncells,); True = include.

None
weights ndarray or None

Cell weights of shape (grid.ncells,); normalized internally. None → uniform weights.

None
aggregate str

Pandas-compatible frequency string for temporal resampling.

'1D'
min_cells int

Minimum unique cells required per time bin.

1

Returns:

Type Description
Dataset

Variables: mean, std, n_cells, n_observations, sum_weights.

Raises:

Type Description
ValueError

If var_name is missing or mask/weight shapes are wrong.

compute_timeseries_solar_corrected(var_name='VOD', spatial_mask=None, weights=None, aggregate='1D', min_cells=1, solar_correction='normalize', reference_zenith=45.0, daytime_only=False, twilight_angle=-6.0)

Compute a time-series after applying a solar correction.

Parameters:

Name Type Description Default
var_name str

Data variable to correct and aggregate.

'VOD'
spatial_mask ndarray or None

Cell selection mask.

None
weights ndarray or None

Cell weights.

None
aggregate str

Temporal resampling frequency.

'1D'
min_cells int

Minimum cells per time bin.

1
solar_correction ('normalize', 'residual', 'cos_correction')

Correction method passed to :meth:SolarPositionCalculator.apply_solar_correction.

'normalize'
reference_zenith float

Reference zenith for normalization (degrees).

45.0
daytime_only bool

If True, mask out nighttime epochs.

False
twilight_angle float

Solar-elevation threshold for daytime (degrees).

-6.0

Returns:

Type Description
Dataset

Solar-corrected time-series with additional metadata attrs.

Raises:

Type Description
ValueError

If no solar calculator is configured.

compute_diurnal_cycle(var_name='VOD', spatial_mask=None, weights=None, hour_bins=24, min_observations=10)

Compute clock-time diurnal cycle (hour-of-day statistics).

Parameters:

Name Type Description Default
var_name str

Data variable to bin.

'VOD'
spatial_mask ndarray or None

Cell selection mask.

None
weights ndarray or None

Cell weights.

None
hour_bins int

Number of equal-width hour bins over [0, 24).

24
min_observations int

Minimum observations required per bin.

10

Returns:

Type Description
Dataset

mean, std, n_observations on the hour coordinate.

compute_diurnal_cycle_solar(var_name='VOD', spatial_mask=None, weights=None, n_solar_bins=12, min_observations=10)

Diurnal cycle binned by solar elevation instead of clock time.

Accounts for seasonal variation in solar position, producing a more physically meaningful diurnal pattern.

Parameters:

Name Type Description Default
var_name str

Data variable to bin.

'VOD'
spatial_mask ndarray or None

Cell selection mask.

None
weights ndarray or None

Cell weights.

None
n_solar_bins int

Number of equal-width bins over [-20°, 90°].

12
min_observations int

Minimum observations per bin.

10

Returns:

Type Description
Dataset

mean, std, n_observations on the solar_elevation coordinate.

Raises:

Type Description
ValueError

If no solar calculator is configured.

add_solar_metadata_to_timeseries(timeseries)

Attach solar zenith, azimuth and elevation to a time-series.

Parameters:

Name Type Description Default
timeseries Dataset

Time-series dataset with an epoch coordinate.

required

Returns:

Type Description
Dataset

Copy with solar_zenith, solar_azimuth, solar_elevation added.

Raises:

Type Description
ValueError

If no solar calculator is configured.

plot_timeseries(timeseries, smooth_window=0, show_uncertainty=True, show_n_cells=False, ax=None, **style_kwargs)

Plot a time-series with optional Savitzky-Golay smoothing.

Parameters:

Name Type Description Default
timeseries Dataset

Output of :meth:compute_timeseries.

required
smooth_window int

Savitzky-Golay window length (0 = off; forced odd internally).

0
show_uncertainty bool

Draw ±1 std band.

True
show_n_cells bool

Secondary y-axis showing cell count.

False
ax Axes or None

Axes to draw on; created if None.

None
**style_kwargs Any

ylabel, title, figsize forwarded to matplotlib.

{}

Returns:

Type Description
fig, ax : plt.Figure, plt.Axes

plot_diurnal_cycle(diurnal, show_confidence=True, ax=None, **style_kwargs)

Plot a clock-time diurnal cycle.

Parameters:

Name Type Description Default
diurnal Dataset

Output of :meth:compute_diurnal_cycle.

required
show_confidence bool

Draw ±1 std band.

True
ax Axes or None

Axes to draw on; created if None.

None
**style_kwargs Any

ylabel, title, figsize.

{}

Returns:

Type Description
fig, ax : plt.Figure, plt.Axes

plot_diurnal_cycle_comparison(diurnal_clock, diurnal_solar, figsize=(14, 6), **style_kwargs)

Side-by-side clock-time vs solar-time diurnal cycle plots.

Parameters:

Name Type Description Default
diurnal_clock Dataset

Output of :meth:compute_diurnal_cycle.

required
diurnal_solar Dataset

Output of :meth:compute_diurnal_cycle_solar.

required
figsize tuple

Figure size.

(14, 6)
**style_kwargs Any

ylabel, title.

{}

Returns:

Type Description
fig, axes : plt.Figure, np.ndarray of plt.Axes

Spatial analysis

Spatial analysis of gridded VOD data.

Per-cell statistical aggregation and basic comparative plotting for VOD datasets with pre-assigned cell IDs.

Classes:

Name Description
``VODSpatialAnalyzer``

Computes per-cell temporal statistics and provides simple histogram-based comparisons between filtering variants.

Notes

  • Spatial visualization (hemisphere maps, 3-D projections) is handled by the canvod-viz package. This module is limited to the statistical aggregation and lightweight comparison plots that do not require a hemisphere renderer.
  • compute_spatial_statistics returns both a grid-aligned array (length grid.ncells, NaN for empty cells) and a compact patch-aligned array containing only cells with observations. Use the grid-aligned form for masking / weighting; use the patch-aligned form when passing data to canvod-viz renderers.

VODSpatialAnalyzer

Per-cell spatial analysis of a VOD dataset.

Parameters:

Name Type Description Default
vod_data Dataset

Dataset with a cell_id_<grid_name> variable already assigned.

required
grid GridData

Grid instance (must expose .ncells).

required
grid_name str

Suffix for the cell-ID variable.

'equal_area_2deg'

Raises:

Type Description
ValueError

If the expected cell-ID variable is missing.

__init__(vod_data, grid, grid_name='equal_area_2deg')

Initialize the spatial analyzer.

Parameters:

Name Type Description Default
vod_data Dataset

Dataset with cell IDs.

required
grid GridData

Grid instance.

required
grid_name str

Grid name suffix for cell IDs.

"equal_area_2deg"

compute_spatial_statistics(var_name='VOD', time_agg='mean')

Compute per-cell temporal statistics.

Parameters:

Name Type Description Default
var_name str

Data variable to aggregate over time.

'VOD'
time_agg ('mean', 'std', 'count', 'median')

Aggregation function applied per cell.

'mean'

Returns:

Type Description
dict

Keys:

grid_aligned : np.ndarray Shape (grid.ncells,). NaN for cells without data. Use for masking, weighting, or any grid-indexed operation. patch_aligned : np.ndarray Compact array containing only cells with observations. Use when passing data to canvod-viz renderers. cell_ids_with_data : np.ndarray Integer cell IDs corresponding to patch_aligned. metadata : dict valid_cells, total_cells, coverage_percent, variable, aggregation.

Raises:

Type Description
ValueError

If var_name is not in the dataset or time_agg is unrecognised.

compare_filtering_methods(original_var='VOD', filtered_var='VOD_filtered', figsize=(16, 6), ax=None)

Histogram comparison of original vs filtered VOD distributions.

Parameters:

Name Type Description Default
original_var str

Unfiltered variable name.

'VOD'
filtered_var str

Filtered variable name.

'VOD_filtered'
figsize tuple

Figure size when ax is None.

(16, 6)
ax tuple of two plt.Axes or None

Pre-existing axes pair. Created if None.

None

Returns:

Type Description
fig, axes : plt.Figure, np.ndarray of plt.Axes

Per-cell VOD analysis

Per-cell VOD analysis and plotting.

Statistical aggregation, diurnal dynamics, radial distributions, and theta-time heatmaps for per-cell VOD datasets. Handles single datasets or lists of datasets with configurable multi-dataset modes (separate vs averaged).

Classes:

Name Description
``PerCellVODAnalyzer``

Main analysis class. Accepts one or more per-cell datasets (each must expose cell_timeseries, cell_theta, cell_phi).

Utility functions

extract_percell_stats – temporal statistic per cell. percell_to_grid_counts – total observation counts per cell. extract_percell_temporal_stats – range / trend / CV per cell. extract_percell_coverage – data-coverage percentage per cell. percell_to_grid_data – thin wrapper around :func:extract_percell_stats.

Notes

  • Per-cell datasets are expected to have dimensions (cell, time) and variables cell_timeseries, cell_theta, cell_phi, and optionally cell_weights and cell_counts.
  • Spatial visualization (hemisphere maps) lives in canvod-viz.

PerCellVODAnalyzer

Multi-dataset per-cell VOD analyzer.

Parameters:

Name Type Description Default
datasets xr.Dataset or list of xr.Dataset

Per-cell dataset(s). Each must contain cell_timeseries, cell_theta, and cell_phi.

required
labels list of str or None

Human-readable labels for each dataset.

None

Raises:

Type Description
ValueError

If any dataset is missing required variables.

__init__(datasets, labels=None)

Initialize the analyzer.

Parameters:

Name Type Description Default
datasets Dataset | list[Dataset]

Per-cell dataset(s).

required
labels list[str] | None

Labels for each dataset.

None

extract_percell_stats(percell_ds, stat='median')

Compute a single temporal statistic for every cell.

Parameters:

Name Type Description Default
percell_ds Dataset

Per-cell dataset with a cell_timeseries variable of shape (cell, time).

required
stat ('mean', 'median', 'std')

Statistic to reduce across the time dimension.

"mean"

Returns:

Type Description
ndarray

1-D array of length n_cells.

percell_to_grid_counts(percell_ds)

Sum observation counts across time for each cell.

Parameters:

Name Type Description Default
percell_ds Dataset

Per-cell dataset with a cell_counts variable of shape (cell, time).

required

Returns:

Type Description
ndarray

1-D array of total counts per cell.

extract_percell_temporal_stats(percell_ds, stat='range')

Compute a temporal-characteristic statistic for every cell.

Parameters:

Name Type Description Default
percell_ds Dataset

Per-cell dataset with cell_timeseries.

required
stat ('range', 'trend', 'cv')

Statistic:

  • "range" – max − min over time.
  • "trend" – linear-regression slope (requires ≥ 4 valid points; otherwise NaN). Uses :mod:scipy.stats.
  • "cv" – coefficient of variation (std / mean).
"range"

Returns:

Type Description
ndarray

1-D array of length n_cells.

extract_percell_coverage(percell_ds)

Compute the fraction of valid (non-NaN) observations per cell.

Parameters:

Name Type Description Default
percell_ds Dataset

Per-cell dataset with cell_timeseries.

required

Returns:

Type Description
ndarray

1-D array of coverage percentages (0–100) per cell.

percell_to_grid_data(percell_ds, stat='median')

Thin wrapper around :func:extract_percell_stats.

Provided for symmetry with aggregate_data_to_grid workflows.

Parameters:

Name Type Description Default
percell_ds Dataset

Per-cell dataset.

required
stat ('mean', 'median', 'std')

Statistic to compute.

"mean"

Returns:

Type Description
ndarray

1-D array compatible with grid visualization.