healpix_analyse.dino#

DINOv3 (SAT-493M) embeddings of NESTED HEALPix data for the healpix_analyse package.

The idea#

A ViT is not a “grid” architecture: after the patch embedding it only sees a set of tokens. In the NESTED HEALPix scheme every cell at parent_level contains exactly 4**(level - parent_level) cells of level, stored contiguously and ordered by a Morton (Z-order) code of their local (x, y) face coordinates. A NESTED block can therefore be reshaped exactly into a square image of side 2**(level - parent_level) pixels, fed to an unmodified DINOv3 network, and every 16x16 patch token maps back to one HEALPix cell at level - 4.

This module provides

Orientation#

Inside a HEALPix base face the local axes (x, y) are rotated by 45 degrees with respect to the local East/North directions: the “north” corner of a face is at (x, y) = (max, max). Tiles are built with row = S - 1 - y and col = x so that North points to the top-right corner of the image. The orientation is consistent for all tiles of the same base face and rotated for the polar faces, which is a mild nuisance for a satellite backbone (no gravity direction) and should be handled by rotation augmentations when fine-tuning.

Dependencies: numpy, torch, healpix-geo (the GRID4EARTH HEALPix library – healpy is deliberately not used anywhere in this package), and either the dinov3 torch-hub repo (facebookresearch/dinov3) or transformers (HF weights).

Attributes#

Classes#

Functions#

set_ellipsoid(→ str)

Choose the reference ellipsoid ("sphere", "wgs84", ...) and return the old one.

cell_centres_lonlat(→ Tuple[numpy.ndarray, numpy.ndarray])

Centres of NESTED cells at level, as (lon, lat) in degrees.

cells_at_lonlat(→ numpy.ndarray)

NESTED cell ids at level containing the given (lon, lat), in degrees.

cell_vectors(→ numpy.ndarray)

Unit vectors of the cell centres, shape [3, N].

cell_neighbours(→ numpy.ndarray)

The eight NESTED neighbours of each cell, shape [8, N]; -1 where absent.

nested_to_xy(→ Tuple[numpy.ndarray, numpy.ndarray])

Decode a relative NESTED index into local (x, y) coordinates.

xy_to_nested(→ numpy.ndarray)

Inverse of nested_to_xy().

nested_to_tiles(→ Tuple[numpy.ndarray, numpy.ndarray, ...)

Reorder a NESTED HEALPix dataset into square tiles, one per

tiles_to_nested(→ Tuple[numpy.ndarray, numpy.ndarray])

Inverse of nested_to_tiles() (full tiles, no mask).

tile_grid_cell_ids(→ numpy.ndarray)

NESTED ids (at sub_level) of the [S, S] image grid of every tile.

healpix_gsd_m(→ float)

Square root of the HEALPix cell area at level, in metres.

tangent_grid_lonlat(, radius, numpy.ndarray])

Longitude / latitude of north-up gnomonic grids centred on given points.

offsets_to_lonlat(→ Tuple[numpy.ndarray, numpy.ndarray])

Gnomonic offsets (east xi, north eta, in tangent units) to lon/lat.

lonlat_to_offsets(→ Tuple[numpy.ndarray, numpy.ndarray])

Inverse of offsets_to_lonlat(): lon/lat to gnomonic offsets.

parent_cover_px(→ Tuple[int, int])

Size (rows, columns) of the smallest tangent image covering every parent cell.

sample_healpix(→ Tuple[numpy.ndarray, numpy.ndarray])

Sample a partial-sky NESTED HEALPix map at arbitrary lon/lat.

pow2_cover_px(→ int)

Side of the smallest square power-of-two image containing every parent cell.

tangent_tiles(, interpolation, fill, duplicates, ...)

Resample the data onto north-up tangent-plane images, one per tile centre.

load_dinov3_sat(→ torch.nn.Module)

Load a DINOv3 SAT-493M backbone in evaluation mode.

GetDINOV3SAT(, mean, std, pooling, return_patches, ...)

DINOv3 SAT-493M embeddings of NESTED HEALPix data.

Module Contents#

healpix_analyse.dino.SAT493M_MEAN: Tuple[float, float, float] = (0.43, 0.411, 0.296)#
healpix_analyse.dino.SAT493M_STD: Tuple[float, float, float] = (0.213, 0.156, 0.143)#
healpix_analyse.dino.set_ellipsoid(name: str) → str[source]#

Choose the reference ellipsoid (“sphere”, “wgs84”, …) and return the old one.

Call it once, with whatever the store’s dggs.ellipsoid.name says, before any embedding is computed.

healpix_analyse.dino.cell_centres_lonlat(level: int, cell_id: numpy.ndarray) → Tuple[numpy.ndarray, numpy.ndarray][source]#

Centres of NESTED cells at level, as (lon, lat) in degrees.

healpix_analyse.dino.cells_at_lonlat(level: int, lon: numpy.ndarray, lat: numpy.ndarray) → numpy.ndarray[source]#

NESTED cell ids at level containing the given (lon, lat), in degrees.

healpix_analyse.dino.cell_vectors(level: int, cell_id: numpy.ndarray) → numpy.ndarray[source]#

Unit vectors of the cell centres, shape [3, N].

healpix_analyse.dino.cell_neighbours(level: int, cell_id: numpy.ndarray) → numpy.ndarray[source]#

The eight NESTED neighbours of each cell, shape [8, N]; -1 where absent.

healpix_analyse.dino.nested_to_xy(rel_id: numpy.ndarray) → Tuple[numpy.ndarray, numpy.ndarray][source]#

Decode a relative NESTED index into local (x, y) coordinates.

rel_id is the index of a cell inside its ancestor block (cell_id - parent_id * 4**k); the result satisfies 0 <= x, y < 2**k. This is the HEALPix nest2xyf convention: x occupies the even bits, y the odd bits.

healpix_analyse.dino.xy_to_nested(x: numpy.ndarray, y: numpy.ndarray) → numpy.ndarray[source]#

Inverse of nested_to_xy().

healpix_analyse.dino.nested_to_tiles(data: ArrayLike, cell_id: ArrayLike, level: int, parent_level: int, *, fill: str = 'mean', duplicates: str = 'mean') → Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#

Reorder a NESTED HEALPix dataset into square tiles, one per parent_level cell present in cell_id.

Parameters:
  • data (array [N, C] or [N]) – Pixel values at HEALPix level. NaN marks a missing pixel.

  • cell_id (int array [N]) – NESTED cell ids at level. Duplicates are not allowed.

  • level (int) – Resolution of data (nside = 2**level).

  • parent_level (int) – Resolution of the tiles. k = level - parent_level >= 1; the tiles have side S = 2**k.

  • fill ({"mean", "zero", "nan"}) – Value given to the pixels of a tile that are absent from cell_id (or NaN): the per-tile, per-band mean of the available pixels, zero, or NaN.

  • duplicates ({"mean", "first", "error"}) – What to do when the same cell id appears several times, which happens when data projected from another grid puts two source pixels in the same HEALPix cell: average them (NaN-aware), keep the first one, or raise.

Returns:

  • tiles (float32 array [M, C, S, S])

  • parent_ids (int64 array [M]) – NESTED ids of the tiles at parent_level (sorted).

  • valid (bool array [M, S, S]) – Availability mask of every pixel.

  • coverage (float array [M]) – Fraction of available pixels per tile.

healpix_analyse.dino.tiles_to_nested(tiles: numpy.ndarray, parent_ids: numpy.ndarray, parent_level: int, level: int) → Tuple[numpy.ndarray, numpy.ndarray][source]#

Inverse of nested_to_tiles() (full tiles, no mask).

Parameters:
  • tiles (array [M, C, S, S])

  • parent_ids (int array [M])

  • parent_level, level (int)

Returns:

  • data (array [M * S * S, C])

  • cell_id (int64 array [M * S * S]) – NESTED ids at level, in tile-major order.

healpix_analyse.dino.tile_grid_cell_ids(parent_ids: numpy.ndarray, parent_level: int, sub_level: int) → numpy.ndarray[source]#

NESTED ids (at sub_level) of the [S, S] image grid of every tile.

Returns an int64 array [M, S, S] indexed as [tile, row, col] with the same orientation convention as nested_to_tiles().

healpix_analyse.dino.healpix_gsd_m(level: int, radius: float = EARTH_RADIUS_M) → float[source]#

Square root of the HEALPix cell area at level, in metres.

healpix_analyse.dino.tangent_grid_lonlat(centre_lon: numpy.ndarray, centre_lat: numpy.ndarray, size: int, gsd_rad: float, *, shift: Tuple[float, float] = (0.0, 0.0), radius: float = EARTH_RADIUS_M) → Tuple[numpy.ndarray, numpy.ndarray][source]#

Longitude / latitude of north-up gnomonic grids centred on given points.

Parameters:
  • centre_lon, centre_lat (array [M]) – Tangent points, in degrees.

  • size (int or (int, int)) – Side of the grid in pixels; a pair gives (rows, columns).

  • gsd_rad (float) – Pixel spacing in tangent units (ground sampling / Earth radius).

  • shift ((float, float)) – Extra offset of the grid origin, in pixels (row, column). Used by the sliding-window oversampling.

Returns:

lon, lat (array [M, rows, cols]) – Row 0 is the northernmost, column 0 the westernmost: the images are north-up and east-right, whatever the HEALPix face.

healpix_analyse.dino.offsets_to_lonlat(centre_lon: numpy.ndarray, centre_lat: numpy.ndarray, xi: numpy.ndarray, eta: numpy.ndarray) → Tuple[numpy.ndarray, numpy.ndarray][source]#

Gnomonic offsets (east xi, north eta, in tangent units) to lon/lat.

xi and eta are broadcast against the [M] tangent points, giving [M, *xi.shape] outputs.

healpix_analyse.dino.lonlat_to_offsets(centre_lon: numpy.ndarray, centre_lat: numpy.ndarray, lon: numpy.ndarray, lat: numpy.ndarray) → Tuple[numpy.ndarray, numpy.ndarray][source]#

Inverse of offsets_to_lonlat(): lon/lat to gnomonic offsets.

centre_* has shape [M] and lon / lat shape [M, ...]; the returned xi (east) and eta (north) have the shape of lon.

healpix_analyse.dino.parent_cover_px(centre_ids: numpy.ndarray, parent_level: int, gsd_m: float, *, margin_px: int = 8, radius: float = EARTH_RADIUS_M) → Tuple[int, int][source]#

Size (rows, columns) of the smallest tangent image covering every parent cell.

A HEALPix cell is a parallelogram, not a square, so a north-up image that must contain it entirely is larger than the cell – and rectangular. Both sides are rounded up to a multiple of the DINOv3 patch.

healpix_analyse.dino.sample_healpix(data: numpy.ndarray, cell_id: numpy.ndarray, level: int, lon: numpy.ndarray, lat: numpy.ndarray, *, interpolation: str = 'bilinear') → Tuple[numpy.ndarray, numpy.ndarray][source]#

Sample a partial-sky NESTED HEALPix map at arbitrary lon/lat.

Cells absent from cell_id and non-finite values are treated as missing: the interpolation weights are renormalised over the available neighbours, and the returned mask is False where nothing was available.

Returns:

  • values (float32 array [..., C])

  • valid (bool array [...])

healpix_analyse.dino.pow2_cover_px(centre_ids: numpy.ndarray, parent_level: int, gsd_m: float, *, margin_px: int = 8, radius: float = EARTH_RADIUS_M) → int[source]#

Side of the smallest square power-of-two image containing every parent cell.

A HEALPix cell and a square of the same area cannot contain one another: a square of side 2**k px has exactly the cell’s area, so roughly a fifth of the cell always falls outside it. Those cells then have no token of their own. Doubling the side fixes it and keeps the shape a ViT expects.

healpix_analyse.dino.tangent_tiles(data: ArrayLike, cell_id: ArrayLike, level: int, parent_level: int, *, tile_px: int | str | Tuple[int, int] | None = None, gsd_m: float | None = None, shift: Tuple[float, float] = (0.0, 0.0), interpolation: str = 'bilinear', fill: str = 'mean', duplicates: str = 'mean', radius: float = EARTH_RADIUS_M) → Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray][source]#

Resample the data onto north-up tangent-plane images, one per tile centre.

Tile centres are the HEALPix cells of parent_level present in the data; each image is a square of tile_px pixels at a constant ground sampling, so its shape is right whatever the local HEALPix distortion. Unlike nested_to_tiles() the images may overlap and are not restricted to the content of one NESTED block.

Returns:

  • tiles (float32 [M, C, S, S])

  • centre_ids (int64 [M]        NESTED ids of the tile centres)

  • valid (bool [M, S, S])

  • coverage (float [M])

  • lon, lat (float64 [M, S, S]  geographic position of every pixel)

healpix_analyse.dino.load_dinov3_sat(model_name: str = 'dinov3_vitl16', weights: str | None = None, *, source: str = 'auto', repo: str = 'facebookresearch/dinov3', device: str | torch.device | None = None) → torch.nn.Module[source]#

Load a DINOv3 SAT-493M backbone in evaluation mode.

Parameters:
  • model_name ({"dinov3_vitl16", "dinov3_vit7b16"}) – SAT-493M is released for ViT-L/16 and ViT-7B/16 only.

  • weights (str, optional) – Path to the .pth checkpoint (torch-hub route; DINOv3 weights are gated and must be downloaded after accepting the licence) or a Hugging Face model id / local directory (HF route).

  • source ({"auto", "hub", "hf"}) – “hub” uses torch.hub.load(repo, model_name, weights=weights); “hf” uses transformers.AutoModel; “auto” tries hub then hf.

  • repo (str) – Torch-hub repo or a local clone of facebookresearch/dinov3.

  • device (str or torch.device, optional)

class healpix_analyse.dino.DINOEmbedding[source]#

Result of GetDINOV3SAT().

Variables:
  • embedding (array [M, Ndino]) – One vector per tile of parent_level (CLS token by default).

  • cell_id (int64 array [M]) – NESTED ids of the tiles at parent_level.

  • parent_level (int)

  • coverage (float array [M]) – Fraction of level pixels available in each tile.

  • patch_embedding (array [M * P, Ndino] or None) – Patch tokens (return_patches=True); one per HEALPix cell at patch_level = level - 4.

  • patch_cell_id (int64 array [M * P] or None)

  • patch_level (int or None)

embedding: numpy.ndarray#
cell_id: numpy.ndarray#
parent_level: int#
coverage: numpy.ndarray#
patch_embedding: numpy.ndarray | None = None#
patch_cell_id: numpy.ndarray | None = None#
patch_level: int | None = None#
patch_lon: numpy.ndarray | None = None#
patch_lat: numpy.ndarray | None = None#
projection: str = 'tangent'#
over_sample: int = 1#
gsd_m: float | None = None#
tile_px: int | None = None#
healpix_analyse.dino.GetDINOV3SAT(data: ArrayLike, cell_id: ArrayLike, level: int, parent_level: int, *, projection: str = 'tangent', over_sample: int = 1, context_px: int | None = None, out_cells: ArrayLike | None = None, gsd_m: float | None = None, tile_px: int | None = None, interpolation: str = 'bilinear', model: torch.nn.Module | None = None, weights: str | None = None, model_name: str = 'dinov3_vitl16', bands: Sequence[int] = (0, 1, 2), mean: Sequence[float] = SAT493M_MEAN, std: Sequence[float] = SAT493M_STD, pooling: str = 'cls', return_patches: bool = False, min_coverage: float = 0.0, fill: str = 'mean', duplicates: str = 'mean', batch_size: int = 16, device: str | torch.device | None = None, autocast: bool = True) → DINOEmbedding[source]#

DINOv3 SAT-493M embeddings of NESTED HEALPix data.

Every HEALPix cell of parent_level present in cell_id gives one square image, which is passed to an unmodified DINOv3 backbone. How that image is built is the important choice:

projection="tangent" (default)

The image is resampled onto a north-up gnomonic plane tangent at the cell centre, with a constant ground sampling. Shapes are then correct: HEALPix cells have equal area but not equal shape, and at 52 deg N the two NESTED axes measure 10.6 m and 16.7 m with a 60.5 deg angle between them, so the “square” of the nested mode is really a parallelogram stretched by a factor 2. A network trained on map-projected imagery cannot be expected to see through that. The image is sized to contain the whole (parallelogram-shaped) cell, so it is rectangular and a little larger than the cell.

The tokens sit on a square metric grid while the cells are parallelograms, so the output is not built by assigning each token to the cell it falls in – that would leave some cells with two tokens and others with none, a moire of holes on a map. Instead the cells of the token level inside each tile are enumerated and the token field is read at their positions: every cell carries exactly one embedding, and the output tiles the sphere without holes. patch_lon / patch_lat are then the cell centres. The price is a resampling, twice.

projection="percell"

One tangent plane per output cell: for every cell of parent_level a north-up image of context_px pixels is built, tangent at that cell’s centre, and the embedding is the patch token holding the centre (or pooling="cls" for the whole window). The window is centred exactly on the cell, in the cell’s own frame, so nothing is interleaved and nothing is resampled back – this is the exact sliding window, and level - parent_level >= 4 no longer applies since the cell may be finer than a patch.

It costs one forward pass per cell, but each window is small: with the default context_px – the cell’s own footprint – the total number of tokens equals the number of output cells, exactly what the tiled modes produce, so this is affordable. What is expensive is context: a window m patches wide costs m**2 tokens per cell.

projection="nested"

The historical mode: the NESTED block is reshaped into an image, which is exact in index space and free of resampling, and every token maps onto exactly one cell of level - 4. Geometrically wrong away from the equator; kept for comparison.

Parameters:
  • data (array [N, C]) – Reflectances at level, expected in [0, 1] (Sentinel-2 DN / 10000 for instance). NaN marks a missing pixel.

  • cell_id (int array [N]) – NESTED cell ids at level.

  • level (int) – Resolution of data.

  • parent_level (int) – Resolution of the tile centres; level - parent_level >= 4 is required (one DINO patch is 16 px = 4 HEALPix levels). level - 8 (256 px tiles, 16x16 patch tokens) is the natural choice.

  • projection ({"tangent", "nested"}) – See above.

  • context_px (int, optional) – Side of the window in percell mode, a multiple of 16. The default is the cell’s own footprint, so the total number of tokens equals the number of output cells – the same count as the tiled modes. A larger window gives the network context, at a cost growing as (context_px / cell)**2: for cells of 16 px, 48 px costs 9 times more and 224 px 196 times. With a centred pooling an even number of patches per axis leaves no token on the centre, so the value is raised by one patch.

  • out_cells (int array, optional) – Restrict percell mode to these cells instead of every cell of parent_level present in the data. The way to try it on a small area before paying for the whole scene.

  • over_sample (int) – Sliding-window factor, a power of two. 1 (default) runs the patches side by side, exactly as the network does. n runs the network n**2 times on windows shifted by 16 / n pixels and interleaves the tokens, giving a token field n times denser in each direction, at level - 4 + log2(n). Cost grows as n**2.

  • gsd_m (float, optional) – Ground sampling of the tangent grid, in metres. Defaults to the HEALPix native value sqrt(cell area) at level.

  • tile_px (int or (int, int), optional) – Size of the images, (rows, columns). In tangent mode the default is the smallest multiple of 16 containing every parent cell (see parent_cover_px()); in nested mode it is 2**(level - parent_level).

  • interpolation ({"bilinear", "nearest"}) – Resampling onto the tangent grid.

  • model (nn.Module, optional) – A backbone already loaded with load_dinov3_sat(). When None the model is loaded from weights / model_name.

  • weights, model_name – Passed to load_dinov3_sat() when model is None.

  • bands (sequence of 3 int) – Indices of the (R, G, B) bands in data – Sentinel-2 B04, B03, B02.

  • mean, std (sequence of 3 float) – Normalisation constants (SAT-493M defaults).

  • pooling ({"cls", "mean", "cls+mean", "centre"}) – Tile vector: CLS token, mean of the patch tokens, or their concatenation (2 * Ndino). In percell mode the default is the patch token holding the cell centre ("centre"), which is what describes the cell itself rather than its whole window.

  • return_patches (bool) – Also return the patch tokens.

  • min_coverage (float) – Drop tiles whose fraction of usable pixels is below this value; 1.0 keeps only the complete ones.

  • fill ({"mean", "zero"}) – How missing pixels are filled before normalisation.

  • duplicates ({"mean", "first", "error"}) – How repeated cell ids are aggregated (see nested_to_tiles()).

  • batch_size (int)

  • device (str or torch.device, optional)

  • autocast (bool) – Use bfloat16 autocast on CUDA.

Returns:

DINOEmbedding