libera_utils.footprint_matching.readers.nsidc.NISEReader#

class libera_utils.footprint_matching.readers.nsidc.NISEReader(file_paths: Sequence[Path])#

Bases: StaticReader

Read NISE surface-class coverage layers from an HDF-EOS4 file.

The NISE product (Near-real-time Ice and Snow Extent) distributes a uint8 Extent SDS per hemisphere whose category codes encode sea ice concentration, snow on land, permanent ice, and ice/snow-free pixels. This reader splits those codes into five independent float32 coverage layers (see module docstring), reprojects each hemisphere’s EASE-Grid pixel centers to WGS84 lat/lon with its own CRS (North EPSG:3408, South EPSG:3409), and merges both hemispheres into one point cloud of (5, n_pixels) values in VARIABLES order (see the module docstring’s geolocation note). The daily granule has no time axis, so the staged day is the time match (TIME_MATCH = DAILY).

Class Attributes#

READER_KEYstr

Registry key "NISE".

RESOLUTION_KMfloat

25 km (NISE EASE-Grid 25-km product).

VARIABLEStuple[VariableSpec, …]

Five fractional-coverage variables, all continuous float32 with weighted_mean aggregation and range 0.0–1.0: "Sea_Ice_Concentration", "Snow_Free_Land", "Permanent_Ice", "Snow_On_Land", and "Open_Ocean".

_HEMISPHEREStuple[_HemisphereGrid, …]

The EASE-Grids read from every granule, in order: the Northern (EPSG:3408) and Southern (EPSG:3409) 721 x 721 25 km grids.

param file_paths:

The one NISE HDF-EOS4 file (*.HDFEOS) for the processing day.

type file_paths:

Sequence[Path]

__init__(file_paths: Sequence[Path]) → None#

Methods

preload()

Load and index the whole source now, rather than on the first query.

product_stacks()

Return every stacked product variable this reader contributes.

product_variable_specs()

Return every variable this reader contributes to the product definition.

samples_in(bbox, time)

Return the native samples inside bbox that match time.

Attributes

ADDITIONAL_PRODUCT_VARIABLES

INSTRUMENT

MAX_TIME_OFFSET

PASS_WINDOW

PRODUCT_STACKS

READER_KEY

RESOLUTION_KM

TIMESTAMP_SOURCE

TIME_MATCH

VARIABLES

file_paths

The staged granule paths passed at construction, in the order given.

n_variables

Number of read variables (the first axis of every values array).

single_file_path

The one granule of a reader that reads exactly one per period.

ADDITIONAL_PRODUCT_VARIABLES: tuple[VariableSpec, ...] = ()#
INSTRUMENT: str = 'AMSR2'#
MAX_TIME_OFFSET: timedelta64 | None = None#
PASS_WINDOW: timedelta64 | None = None#
PRODUCT_STACKS: tuple[ProductStack, ...] = ()#
READER_KEY: str = 'NISE'#
RESOLUTION_KM: float = 25.0#
TIMESTAMP_SOURCE: str | None = None#
TIME_MATCH: TimeMatch = 'daily'#
VARIABLES: tuple[VariableSpec, ...] = (VariableSpec(name='Sea_Ice_Concentration', dtype='float32', aggregation='weighted_mean', modes=frozenset({<OperationalMode.IMAGER: 'FMATCH-IMAGER'>, <OperationalMode.CAM_CAMTIME: 'FMATCH-CAM-CAMTIME'>, <OperationalMode.CAM: 'FMATCH-CAM'>, <OperationalMode.IMAGER_CAMTIME: 'FMATCH-IMAGER-CAMTIME'>}), n_categories=None), VariableSpec(name='Snow_Free_Land', dtype='float32', aggregation='weighted_mean', modes=frozenset({<OperationalMode.IMAGER: 'FMATCH-IMAGER'>, <OperationalMode.CAM_CAMTIME: 'FMATCH-CAM-CAMTIME'>, <OperationalMode.CAM: 'FMATCH-CAM'>, <OperationalMode.IMAGER_CAMTIME: 'FMATCH-IMAGER-CAMTIME'>}), n_categories=None), VariableSpec(name='Permanent_Ice', dtype='float32', aggregation='weighted_mean', modes=frozenset({<OperationalMode.IMAGER: 'FMATCH-IMAGER'>, <OperationalMode.CAM_CAMTIME: 'FMATCH-CAM-CAMTIME'>, <OperationalMode.CAM: 'FMATCH-CAM'>, <OperationalMode.IMAGER_CAMTIME: 'FMATCH-IMAGER-CAMTIME'>}), n_categories=None), VariableSpec(name='Snow_On_Land', dtype='float32', aggregation='weighted_mean', modes=frozenset({<OperationalMode.IMAGER: 'FMATCH-IMAGER'>, <OperationalMode.CAM_CAMTIME: 'FMATCH-CAM-CAMTIME'>, <OperationalMode.CAM: 'FMATCH-CAM'>, <OperationalMode.IMAGER_CAMTIME: 'FMATCH-IMAGER-CAMTIME'>}), n_categories=None), VariableSpec(name='Open_Ocean', dtype='float32', aggregation='weighted_mean', modes=frozenset({<OperationalMode.IMAGER: 'FMATCH-IMAGER'>, <OperationalMode.CAM_CAMTIME: 'FMATCH-CAM-CAMTIME'>, <OperationalMode.CAM: 'FMATCH-CAM'>, <OperationalMode.IMAGER_CAMTIME: 'FMATCH-IMAGER-CAMTIME'>}), n_categories=None))#
_HEMISPHERES: tuple[_HemisphereGrid, ...] = (_HemisphereGrid(epsg=3408, hemisphere_label='Northern Hemisphere', grid_rows=721, grid_cols=721, resolution_m=25067.525, x_origin=-9036842.7625, y_origin=9036842.7625), _HemisphereGrid(epsg=3409, hemisphere_label='Southern Hemisphere', grid_rows=721, grid_cols=721, resolution_m=25067.525, x_origin=-9036842.7625, y_origin=9036842.7625))#
_box_samples(native: GridSamples | _PointIndex | _BoxNative, bbox: BoundingBox) → tuple[ndarray, ndarray, ndarray, ndarray | None]#

Return (lats, lons, values, times) of the native samples inside one plain box.

Raises:

TypeError – If native is none of the three native forms.

_build_transformer(epsg: int) → object#

Build an epsg → WGS84 transformer for one hemisphere’s pixel centers.

Called once per hemisphere from _load_native(), which the base class runs once per reader, so no additional caching is needed. CRS objects (not bare EPSG strings) avoid the pyproj FutureWarning about authority-based CRS construction.

Parameters:

epsg (int) – Source CRS EPSG code (3408 for EASE-Grid North, 3409 for South).

_check_grid(struct_meta: str, hemi: _HemisphereGrid) → None#

Check one hemisphere’s StructMetadata.0 grid against the EASE-Grid it is geolocated on.

Pixel centers are computed from hemi’s fixed geometry, not from the file, so a granule whose grid differs in size, corners or projection would be misplaced; this stops the load instead.

Parameters:
  • struct_meta (str) – The granule’s StructMetadata.0 text.

  • hemi (_HemisphereGrid) – The grid the hemisphere’s pixels are geolocated on.

Raises:

ValueError – If StructMetadata.0 has no grid named hemi.hemisphere_label or lacks one of its geometry fields, names a projection other than GCTP_LAMAZ, or gives dimensions or corners other than hemi’s.

classmethod _check_product_stacks() → None#

Reject a PRODUCT_STACKS declaration the product could not be built from.

Raises:

TypeError – If a stack names a spec the reader does not produce, a spec is stacked twice, a stack takes the name of a product spec, a stack’s members have standard-deviation companions for some members but not others, or a stack’s members do not all have the same modes.

_compute_latlon_grid(hemi: _HemisphereGrid) → tuple[ndarray, ndarray]#

Compute geographic lat/lon coordinates for every pixel in one hemisphere grid.

Uses pyproj to transform the EASE-Grid pixel centers (hemi.epsg: 3408 for the North grid, 3409 for the South) to WGS84 geographic coordinates (EPSG:4326).

Parameters:

hemi (_HemisphereGrid) – The hemisphere grid to reproject.

Returns:

(lats_2d, lons_2d) each of shape (grid_rows, grid_cols) in degrees.

Return type:

tuple[np.ndarray, np.ndarray]

Notes

Pixel centers are at half-cell offsets from the grid origin:

x_center[col] = x_origin + (col + 0.5) * resolution_m y_center[row] = y_origin - (row + 0.5) * resolution_m (y decreases southward)

Pixels whose projected coordinates fall outside the EASE-Grid domain reproject to non-finite coordinates (pyproj yields inf); these are excluded by the reader base class’s point index.

_extent_to_category_masks(raw: ndarray) → ndarray#

Split the raw NISE Extent codes into five float32 coverage layers.

Each layer is a per-pixel value in [0.0, 1.0] (see the module docstring for the full encoding). Sea_Ice_Concentration keeps the fractional concentration from codes 1–100; the other four layers are 0/1 indicators. Codes that belong to no surface class (102, 105–251, and the 252/253 quality flags) are NaN in every layer, so the weigher counts them as missing.

Parameters:

raw (np.ndarray) – uint8 array of NISE Extent codes, shape (n_rows, n_cols).

Returns:

float32 array of shape (5, n_rows, n_cols); axis 0 follows _VARIABLE_ORDER (and therefore VARIABLES).

Return type:

np.ndarray

_index_points(native: PointSamples) → _PointIndex#

Validate a loaded point cloud against the reader’s contract and index it.

_load_native() → PointSamples#

Read every hemisphere’s Extent SDS and flatten to one geolocated point cloud.

Reads each configured hemisphere (Northern EPSG:3408, Southern EPSG:3409), reprojects its pixel centers to WGS84 with that hemisphere’s CRS, drops the off-Earth corner pixels (see below), and concatenates the per-hemisphere points into a single cloud. A box query downstream keeps only the points inside its box, so northern and southern footprints each draw from the correct hemisphere automatically. Every pixel is served at its own reprojected center, so the PSF weigher is the first place a pixel is weighted.

Returns:

lats/lons are (n_pixels,) float64 pixel-center coordinates (WGS84), and values is float32 shape (5, n_pixels) holding the five coverage layers in _VARIABLE_ORDER. Pixels outside the EASE-Grid domain reproject to non-finite coordinates (pyproj yields inf) and are dropped by the reader base class’s point index.

Return type:

PointSamples

Raises:

ValueError – If a hemisphere’s Extent SDS is not the shape its grid declares, or its StructMetadata.0 entry is missing or gives other dimensions, corners or projection than the EASE-Grid it is geolocated on (see _check_grid()).

property _native: GridSamples | _PointIndex | _BoxNative#

The loaded, indexed native data (loaded on first access, then held).

Raises:
_native_for(time: datetime64) → tuple[GridSamples | _PointIndex | _BoxNative, ...]#

Return the native data a query at time draws from; empty for no match.

StaticReader returns its one indexed body of data regardless of time; SteppedGridReader the step nearest time, or nothing when that step is farther than MAX_TIME_OFFSET.

_nearest_pass_mask(times: ndarray, time: datetime64, inside: ndarray) → ndarray#

Boolean mask of the samples that belong to the pass nearest time.

The nearest pass is every sample, padding included, within PASS_WINDOW of the observation time of the sample nearest time among the samples inside the footprint’s own (unpadded) box, so a pass that only clips the padding, where the PSF weight is zero, cannot displace one that covers the footprint. The window is centred on that sample’s own time rather than on its offset, so two passes on either side of the footprint time are never served together. When no sample lies inside the box (a footprint smaller than one native cell), the nearest sample is taken from every served sample; on a tie, the first in array order. When its offset exceeds MAX_TIME_OFFSET no sample is kept.

_padded(bbox: BoundingBox, resolution_km: float) → BoundingBox#

Widen bbox by half a native cell (resolution_km) on every side.

A sample whose cell overlaps the footprint’s box belongs to the footprint even when its center lies just outside: without the padding a coarse grid (ERA5 at 0.25 degree) never serves the points whose cells cover the rim of the contour, and the weigher scores the rim as missing. The PSF weight of a sample outside the contour is zero, so padding changes no aggregated value, only the coverage.

_read_extent_sds(hemisphere_label: str) → ndarray#

Open the NISE HDF-EOS4 file and return one hemisphere’s Extent SDS.

A NISE granule holds four SDS – an Extent and an Age per hemisphere – and both hemispheres name their fields identically. hdf.select("Extent") resolves the name to the first index (always the Northern grid), and hdf.datasets() is name-keyed and hides the duplicates, so neither can address the Southern grid. This iterates the real dataset indices and matches the requested hemisphere by the SDS’s dimension name (e.g. "YDim:Northern Hemisphere").

Parameters:

hemisphere_label (str) – Substring of the grid dimension name identifying the hemisphere (see _HEMISPHERE_LABEL_NORTH / _HEMISPHERE_LABEL_SOUTH).

Returns:

Shape (grid_rows, grid_cols), dtype uint8. Each element holds a NISE Extent code (see module docstring for encoding).

Return type:

np.ndarray

Raises:
  • KeyError – If the file has no Extent SDS for the requested hemisphere.

  • pyhdf.error.HDF4Error – If the file cannot be opened (not found, not HDF4, corrupt header).

_require_valid_time(time: datetime64) → None#

Raise when a footprint time a time-selecting query needs is NaT.

Raises:

ValueError – If time is NaT.

_resolution_km(natives: tuple[GridSamples | _PointIndex | _BoxNative, ...]) → float#

The native cell size the served samples stand for, km.

The largest spacing a loaded point cloud measured on itself, else the reader’s declared RESOLUTION_KM. A measured spacing wins because a swath subsampled on delivery (e.g. every eighth pixel) must be credited with the cell it really stands for, not the instrument’s nominal one.

_unknown_native_message(native: object) → str#
property file_paths: tuple[Path, ...]#

The staged granule paths passed at construction, in the order given.

property n_variables: int#

Number of read variables (the first axis of every values array).

preload() → None#

Load and index the whole source now, rather than on the first query.

Raises:
classmethod product_stacks() → tuple[ProductStack, ...]#

Return every stacked product variable this reader contributes.

Each stack in PRODUCT_STACKS, followed by a <name>_Standard_Deviation stack of its members’ companions when its members are continuous. The stacked members leave the product as separate variables; every other spec in product_variable_specs stays a 1-D product variable.

classmethod product_variable_specs() → tuple[VariableSpec, ...]#

Return every variable this reader contributes to the product definition.

This is the full set of output variables – what appears, prefixed with <READER_KEY>_, in the FMATCH product definition YAMLs and what the product/test cross-check is built against. It is distinct from VARIABLES (the smaller set actually read from the source file) and is composed of, in order:

  1. each read variable in VARIABLES;

  2. a <name>_Standard_Deviation companion for every continuous (mean-aggregated) read variable, inserted right after its parent by with_standard_deviation_companions();

  3. any reader-specific derived outputs declared in ADDITIONAL_PRODUCT_VARIABLES (e.g. IGBP ranked scenes).

samples_in(bbox: BoundingBox, time: datetime64) → SampleSet#

Return the native samples inside bbox that match time.

Parameters:
  • bbox (BoundingBox) – The footprint’s geographic box, widened by half a native cell (RESOLUTION_KM) on every side so a sample whose cell overlaps the box is served. A box that wraps the antimeridian (given in the [0, 360) form) is split and both halves served.

  • time (np.datetime64) – The footprint’s observation time (radiometer or camera time). Ignored by a DAILY reader.

Returns:

The selected samples; zero samples when the box holds none, or when no sample lies within MAX_TIME_OFFSET of time.

Return type:

SampleSet

Raises:
  • ValueError – If time is NaT and this reader selects in time; if more than one granule is staged for a reader that reads one per period; if the loaded data carries a different number of variables than VARIABLES; if a stepped reader’s step times are empty, hold a NaT or are not strictly ascending; or if a NEAREST_PASS reader serves samples without per-sample times.

  • TypeError – If the reader’s native data is none of GridSamples, PointSamples or a box-queried native.

property single_file_path: Path#

The one granule of a reader that reads exactly one per period.

Raises:

ValueError – If more than one granule was staged for this reader.