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
nested_to_tiles()NESTED block -> square tiles[M, C, S, S]tiles_to_nested()the exact inverse (for debugging / round trips)load_dinov3_sat()load a DINOv3 SAT-493M backbone (hub or HF)GetDINOV3SAT()the user-facing function
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#
Result of |
Functions#
|
Choose the reference ellipsoid ("sphere", "wgs84", ...) and return the old one. |
|
Centres of NESTED cells at |
|
NESTED cell ids at |
|
Unit vectors of the cell centres, shape |
|
The eight NESTED neighbours of each cell, shape |
|
Decode a relative NESTED index into local |
|
Inverse of |
|
Reorder a NESTED HEALPix dataset into square tiles, one per |
|
Inverse of |
|
NESTED ids (at |
|
Square root of the HEALPix cell area at |
|
Longitude / latitude of north-up gnomonic grids centred on given points. |
|
Gnomonic offsets (east |
|
Inverse of |
|
Size (rows, columns) of the smallest tangent image covering every parent cell. |
|
Sample a partial-sky NESTED HEALPix map at arbitrary lon/lat. |
|
Side of the smallest square power-of-two image containing every parent cell. |
|
Resample the data onto north-up tangent-plane images, one per tile centre. |
|
Load a DINOv3 SAT-493M backbone in evaluation mode. |
|
DINOv3 SAT-493M embeddings of NESTED HEALPix data. |
Module Contents#
- 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.namesays, 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
levelcontaining 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_idis the index of a cell inside its ancestor block (cell_id - parent_id * 4**k); the result satisfies0 <= x, y < 2**k. This is the HEALPixnest2xyfconvention:xoccupies the even bits,ythe 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_levelcell present incell_id.- Parameters:
data (
array [N,C]or[N]) – Pixel values at HEALPixlevel. NaN marks a missing pixel.cell_id (
int array [N]) – NESTED cell ids atlevel. Duplicates are not allowed.level (
int) – Resolution ofdata(nside = 2**level).parent_level (
int) – Resolution of the tiles.k = level - parent_level >= 1; the tiles have sideS = 2**k.fill (
{"mean", "zero", "nan"}) – Value given to the pixels of a tile that are absent fromcell_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 atparent_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 atlevel, 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 asnested_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 (
intor(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, northeta, in tangent units) to lon/lat.xiandetaare 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]andlon/latshape[M, ...]; the returnedxi(east) andeta(north) have the shape oflon.
- 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_idand 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**kpx 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_levelpresent in the data; each image is a square oftile_pxpixels at a constant ground sampling, so its shape is right whatever the local HEALPix distortion. Unlikenested_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 idsofthe tile centres)valid (
bool [M,S,S])coverage (
float [M])lon, lat (
float64 [M,S,S] geographic positionofevery 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.pthcheckpoint (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” usestorch.hub.load(repo, model_name, weights=weights); “hf” usestransformers.AutoModel; “auto” tries hub then hf.repo (
str) – Torch-hub repo or a local clone offacebookresearch/dinov3.device (
strortorch.device, optional)
- class healpix_analyse.dino.DINOEmbedding[source]#
Result of
GetDINOV3SAT().- Variables:
embedding (
array [M,Ndino]) – One vector per tile ofparent_level(CLS token by default).cell_id (
int64 array [M]) – NESTED ids of the tiles atparent_level.parent_level (
int)coverage (
float array [M]) – Fraction oflevelpixels available in each tile.patch_embedding (
array [M * P,Ndino]orNone) – Patch tokens (return_patches=True); one per HEALPix cell atpatch_level = level - 4.patch_cell_id (
int64 array [M * P]orNone)
- embedding: numpy.ndarray#
- cell_id: numpy.ndarray#
- coverage: numpy.ndarray#
- patch_embedding: numpy.ndarray | None = None#
- patch_cell_id: numpy.ndarray | None = None#
- patch_lon: numpy.ndarray | None = None#
- patch_lat: numpy.ndarray | 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_levelpresent incell_idgives 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
nestedmode 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_latare then the cell centres. The price is a resampling, twice.projection="percell"One tangent plane per output cell: for every cell of
parent_levela north-up image ofcontext_pxpixels is built, tangent at that cell’s centre, and the embedding is the patch token holding the centre (orpooling="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, andlevel - parent_level >= 4no 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 windowmpatches wide costsm**2tokens 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 atlevel, expected in[0, 1](Sentinel-2DN / 10000for instance). NaN marks a missing pixel.cell_id (
int array [N]) – NESTED cell ids atlevel.level (
int) – Resolution ofdata.parent_level (
int) – Resolution of the tile centres;level - parent_level >= 4is 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 inpercellmode, 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) – Restrictpercellmode to these cells instead of every cell ofparent_levelpresent 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.nruns the networkn**2times on windows shifted by16 / npixels and interleaves the tokens, giving a token fieldntimes denser in each direction, atlevel - 4 + log2(n). Cost grows asn**2.gsd_m (
float, optional) – Ground sampling of the tangent grid, in metres. Defaults to the HEALPix native valuesqrt(cell area)atlevel.tile_px (
intor(int,int), optional) – Size of the images,(rows, columns). In tangent mode the default is the smallest multiple of 16 containing every parent cell (seeparent_cover_px()); in nested mode it is2**(level - parent_level).interpolation (
{"bilinear", "nearest"}) – Resampling onto the tangent grid.model (
nn.Module, optional) – A backbone already loaded withload_dinov3_sat(). WhenNonethe model is loaded fromweights/model_name.weights, model_name – Passed to
load_dinov3_sat()whenmodelisNone.bands (
sequenceof3 int) – Indices of the (R, G, B) bands indata– Sentinel-2 B04, B03, B02.mean, std (
sequenceof3 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). Inpercellmode 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 (seenested_to_tiles()).batch_size (
int)device (
strortorch.device, optional)autocast (
bool) – Use bfloat16 autocast on CUDA.
- Returns: