Source code for healpix_resample.base

from __future__ import annotations

from dataclasses import dataclass
from typing import Generic, Optional, TypeVar

import numpy as np
import torch


T_Array = TypeVar("T_Array", np.ndarray, torch.Tensor)


[docs] def estimate_pixel_area( lon_deg, lat_deg, radius: float = 6371000.0, min_points_per_ring: float = 3.0, ) -> Optional[np.ndarray]: """Estimate each sample's pixel area from shared-latitude "ring" structure. Many native lon/lat grids -- regular lat/lon grids, and reduced Gaussian grids such as ECMWF's N-grids (see the ERA5 tutorial) -- have samples that share *exact* latitude values, with the number of longitude points per ring possibly varying (e.g. shrinking towards the poles). When that structure is present, each sample's cell area can be computed exactly as a spherical zone: the latitudinal width from the midpoints with neighbouring rings, times the longitudinal width implied by how many samples share that ring. This does **not** detect grids that are regular in a different projection (e.g. a UTM pixel grid): after reprojection to lon/lat, such grids generally have no two samples sharing an exact latitude, so no ring structure is found. Parameters ---------- lon_deg, lat_deg : array-like, shape (N,) Sample coordinates in degrees. radius : float Sphere radius; the returned area is in the same squared units (default: metres, giving an area in m^2). min_points_per_ring : float Minimum *average* number of samples per unique latitude value for the ring structure to be considered genuine rather than incidental (default 3 -- comfortably above 1, which would just mean every sample has a numerically-unique latitude). Returns ------- numpy.ndarray, shape (N,), or None Per-sample area estimate, or ``None`` if no ring structure was detected -- callers should then fall back to a uniform weight. """ lat = np.asarray(lat_deg, dtype=np.float64).reshape(-1) lon = np.asarray(lon_deg, dtype=np.float64).reshape(-1) % 360.0 N = lat.size if N == 0: return None lat_u, lat_inv, lat_counts = np.unique(lat, return_inverse=True, return_counts=True) if lat_u.size == 0 or N / lat_u.size < min_points_per_ring: return None n_lon_per_point = lat_counts[lat_inv] dlon_rad = 2.0 * np.pi / n_lon_per_point J = lat_u.size bounds = np.empty(J + 1) bounds[0], bounds[-1] = -90.0, 90.0 bounds[1:-1] = (lat_u[:-1] + lat_u[1:]) / 2.0 sin_bounds = np.sin(np.radians(bounds)) dsin = (sin_bounds[1:] - sin_bounds[:-1])[lat_inv] return (radius ** 2) * dsin * dlon_rad
[docs] @dataclass(frozen=True) class ResampleResults(Generic[T_Array]): """Proxy to resampling results. Attributes ---------- cell_data : numpy.ndarray or torch.Tensor Data values resampled on HEALPix cells cell_ids : numpy.ndarray or torch.Tensor HEALPix cell ids. cg_residual_norms : numpy.ndarray or torch.Tensor or None Conjugate gradient residual norms (if any). cg_niters : numpy.ndarray or torch.Tensor or None Conjugate gradient number of iterations (if any). """ cell_data: T_Array cell_ids: T_Array cg_residual_norms: T_Array | None = None cg_niters: T_Array | None = None