Unified tooling for querying geospatial encoders at coordinates and building reproducible land-only embedding datasets with explicit coordinate and provenance metadata.
This repository has two main entry points:
scripts/get_embeddings.py: point-query CLI for every registered encoderscripts/generate_dataset.py: reproducible land-only dataset generator for coordinate models, raster products, and deterministic baselinesscripts/generate_report.py: offline visual audit and comparison reports for generated embedding datasets
The dataset generator currently supports:
geoclipsatcliplgnd_claycopernicus_embedtesseragoogle_satellite_embeddingrangerange_pluscspcsp_fmowcsp_fmow_unsupercsp_inatcsp_inat_unsupergtloctorchspatial_directtorchspatial_cartesian3dtorchspatial_wraptorchspatial_gridtorchspatial_theorytorchspatial_rffterranovaterranova_spatial
All dataset outputs use the same coordinate conventions and named embedding fields, even though the underlying models and products use different native coordinate orders and storage layouts.
scripts/generate_dataset.py:
- samples candidate coordinates with Fibonacci sphere sampling
- uses a seeded Fibonacci sampler, or reusable coordinates supplied by the user
- filters to land using a selected Natural Earth polygon resolution
- queries one or more encoders
- drops invalid rows for the selected encoder set
- writes
.pt,.csv, or chunked.zarrdatasets - writes location plots and ICA-to-RGB embedding plots
For temporal products, the generator can emit one output per year.
Requested encoders are initialized fail-fast: if any one cannot be created, the run stops before writing a partial dataset.
Inside this repo, the standard coordinate input format is always:
(latitude, longitude)
Saved dataset files include both explicit coordinate layouts:
coordinatesandcoordinates_latlon:(lat, lon)coordinates_lonlat:(lon, lat)- separate
latitudeandlongitudetensors/vectors
For .pt outputs, the metadata block records these conventions explicitly.
The point-query CLI uses the same layouts in both .pt and .npz outputs.
- Python 3.10+
pip- optional CUDA GPU for faster
geoclip/satclip
git clone https://github.com/crp94/geospatial_embeddings_wrapper.git
cd geospatial_embeddings_wrapper
python -m venv .venv
source .venv/bin/activate
# Full installation, including raster products, model-backed encoders, and plots.
pip install ".[all]"
# The original requirements-file workflow remains supported.
# pip install -r requirements.txtsatclip/ is vendored in this repository already. You do not need to clone it separately.
The base package installs the shared API and the Parquet engine needed for the
Google Satellite Embedding index. Install only the feature groups you need with
pip install ".[raster]", pip install ".[models]", or
pip install ".[visualization]". Zarr output is included in the full install
and requirements file; install pip install ".[storage]" when starting from
the minimal package. The installed command-line entry points are
geospatial-embeddings, geospatial-embeddings-dataset, and
geospatial-embeddings-report.
Install the reporting stack separately when you only need post-processing and visualization:
pip install ".[reports]"scripts/get_embeddings.py accepts every canonical name and alias in
wrappers/registry.py. Its lightweight defaults remain geoclip satclip.
Use --encoder-root ENCODER=PATH repeatedly for local data or checkpoint
specifications, and pass --year when querying a temporal encoder.
Example:
python scripts/get_embeddings.py \
--lat 40.7128 34.0522 \
--lon -74.0060 -118.2437 \
--encoders geoclip satclip \
--output embeddings.npzCoordinates are always (latitude, longitude), must be finite, and must be in
the inclusive ranges [-90, 90] and [-180, 180]. Supply either paired
--lat/--lon values or --input, never both. JSON input accepts coordinate
pairs or lat/lon objects; CSV and text input accept the first two numeric
columns or columns headed lat/lon (or latitude/longitude).
Temporal encoders take --year with either one value, applied to every
coordinate, or exactly one value per coordinate:
python scripts/get_embeddings.py \
--lat 41.9 -33.9 40.7 \
--lon 12.5 151.2 -74.0 \
--encoders terranova \
--year 1950 2015 2030 \
--output embeddings.npzInput files can carry the year themselves: a year field on JSON objects, a
top-level year array next to latitude/longitude, or a year column in
CSV/text input. Years must be present for every row or for none, and cannot be
combined with --year. Whenever a year is known, the output stores a per-row
year array and the metadata records year_mode (static, scalar, or
per_row) plus year_range for per-row runs. Static encoders ignore years.
scripts/generate_dataset.py supports:
geoclipsatcliplgnd_claycopernicus_embedtesseragoogle_satellite_embeddingrangerange_pluscspcsp_fmowcsp_fmow_unsupercsp_inatcsp_inat_unsupergtloctorchspatial_directtorchspatial_cartesian3dtorchspatial_wraptorchspatial_gridtorchspatial_theorytorchspatial_rffterranovaterranova_spatial
Notes:
geoclipandsatclipare model-backed encoderslgnd_clay,copernicus_embed,tessera, andgoogle_satellite_embeddingare raster-backed productscopernicus_embedis TorchGeo-backed and auto-downloads its rastertesseracan run throughgeotesserawithout a local rootgoogle_satellite_embeddingcan run against the public AEF annual index without a local rootrangeandrange_pluswrap the open-weight RANGE modelscspwraps the open-weight Contrastive Spatial Pre-Training location encodercsp_fmow,csp_fmow_unsuper,csp_inat, andcsp_inat_unsuperselect the other published CSP variantsgtlocwraps the open-weight GT-Loc GPS branchtorchspatial_*encoders are deterministic coordinate-feature baselines and do not require pretrained weightsterranovawraps the TerraNova foundation model (crp94/terranovaon the Hugging Face Hub); it returns the 256-d spatiotemporal embedding and is temporal over 1900-2035, so pass--year/--years(the point-query default is 2015).terranova_spatialreturns the year-independent 256-d spatial embedding- land-only behavior is enforced by this generator layer, not by every source dataset
The new coordinate-only wrappers do not vendor pretrained weights in this repository. Users should download upstream checkpoints and point the generator at them when needed:
range/range_plus: pass a local SatCLIP checkpoint and RANGE database withcheckpoint=...;db=...if the Hugging Face auto-download is not available or if you want a specific local copycsp*: pass a local CSP.pth.tarcheckpoint withcheckpoint=...if the Dropbox auto-download is not available or if you want a specific CSP variant/checkpointgtloc: always requires a local GT-Loc checkpoint viacheckpoint=...torchspatial_*: no checkpoint is requiredterranova/terranova_spatial: weights are downloaded automatically fromcrp94/terranovathrough theterranovapackage (pip install git+https://github.com/crp94/terranova-model, included in.[models]). Point at a local snapshot withrepo=/path/to/snapshot, or pinrevision=...,cache=...,space=...,batch_size=...
Use --encoder_root for these paths. It accepts either encoder=/plain/path or a semicolon-separated spec such as encoder=repo=/path/to/repo;checkpoint=/path/to/model.pt.
Generate a static 100k land-only dataset with the two model-backed encoders:
python scripts/generate_dataset.py \
--n_points 100000 \
--encoders geoclip satclip \
--device cuda \
--output_path outputs/example_staticMake a sampled coordinate set reproducible and reuse it across separate encoder
runs. --coordinates_in uses the supplied points exactly: rows outside an
encoder's coverage cause a clear error rather than being silently replaced.
# Sample once and retain the exact selected land points.
python scripts/generate_dataset.py \
--n_points 100000 \
--encoders geoclip \
--seed 20260820 \
--coordinates_out outputs/shared_coordinates.npz \
--output_path outputs/geoclip
# Query another encoder at precisely those locations.
python scripts/generate_dataset.py \
--encoders satclip \
--coordinates_in outputs/shared_coordinates.npz \
--output_path outputs/satclipSupplied coordinates can also carry one year per row: a year column in CSV, or
a year array in .npz/.pt input. The generator then embeds each row at its
own year, writes a single output (no year suffix) with a per-row year
array/column, validates every distinct year against the temporal encoders, and
rejects --years for that run. --coordinates_out preserves the years so the
same rows can be re-queried with another encoder:
# points.csv: latitude,longitude,year
python scripts/generate_dataset.py \
--encoders terranova \
--coordinates_in points.csv \
--coordinates_out outputs/points_with_years.npz \
--output_path outputs/terranova_per_row_yearsEncoders whose backend accepts a year vector (TerraNova) embed all rows in one call; other temporal encoders are queried once per distinct year.
For a large multi-encoder run, Zarr streams batches into chunked arrays rather than retaining the complete embedding matrix in memory:
python scripts/generate_dataset.py \
--n_points 500000 \
--encoders geoclip satclip \
--seed 20260820 \
--output_format zarr \
--no_plot \
--output_path outputs/land_embeddingsGenerate a 2024 land-only dataset for the 3 temporal/raster products:
python scripts/generate_dataset.py \
--n_points 100000 \
--encoders copernicus_embed tessera google_satellite_embedding \
--years 2024 \
--output_path outputs/example_2024Generate the full 5-product set as separate runs:
python scripts/generate_dataset.py --n_points 500000 --encoders geoclip --device cuda --output_path outputs/land_only_500k/geoclip_land_500k
python scripts/generate_dataset.py --n_points 500000 --encoders satclip --device cuda --output_path outputs/land_only_500k/satclip_land_500k
python scripts/generate_dataset.py --n_points 500000 --encoders copernicus_embed --output_path outputs/land_only_500k/copernicus_land_500k
python scripts/generate_dataset.py --n_points 500000 --encoders tessera --years 2024 --output_path outputs/land_only_500k/tessera_land_500k
python scripts/generate_dataset.py --n_points 500000 --encoders google_satellite_embedding --years 2024 --output_path outputs/land_only_500k/google_satellite_embedding_land_500kGenerate CSV output without plots:
python scripts/generate_dataset.py \
--n_points 50000 \
--encoders geoclip \
--output_format csv \
--no_plot \
--output_path outputs/geoclip_csvCSV expands every embedding dimension into a separate column, so reserve it for
small, interoperability-focused exports. Prefer .pt for ordinary PyTorch
workflows and .zarr for large datasets or incremental reads.
Generate the additional coordinate-only encoders:
python scripts/generate_dataset.py \
--n_points 500000 \
--encoders range_plus \
--encoder_root 'range_plus=checkpoint=/path/to/satclip.ckpt;db=/path/to/range_db_large.npz' \
--device cuda \
--output_path outputs/land_only_500k/range_plus_land_500k
python scripts/generate_dataset.py \
--n_points 500000 \
--encoders csp \
--encoder_root 'csp=checkpoint=/path/to/csp_model.pth.tar' \
--device cuda \
--output_path outputs/land_only_500k/csp_land_500k
python scripts/generate_dataset.py \
--n_points 500000 \
--encoders csp_inat \
--encoder_root 'csp_inat=checkpoint=/path/to/csp_inat_model.pth.tar' \
--device cuda \
--output_path outputs/land_only_500k/csp_inat_land_500k
python scripts/generate_dataset.py \
--n_points 500000 \
--encoders gtloc \
--encoder_root 'gtloc=repo=/path/to/gtloc;checkpoint=/path/to/gtloc.pt' \
--device cuda \
--output_path outputs/land_only_500k/gtloc_land_500k
python scripts/generate_dataset.py \
--n_points 500000 \
--encoders torchspatial_direct torchspatial_cartesian3d torchspatial_wrap \
torchspatial_grid torchspatial_theory torchspatial_rff \
--output_path outputs/land_only_500k/torchspatial_baselines_land_500kGenerate TerraNova embeddings. The spatiotemporal space is year-specific, so
constrain the run with --years; the spatial space is static:
python scripts/generate_dataset.py \
--n_points 500000 \
--encoders terranova \
--years 2015 \
--device cuda \
--output_path outputs/land_only_500k/terranova_land_500k
python scripts/generate_dataset.py \
--n_points 500000 \
--encoders terranova_spatial \
--device cuda \
--output_path outputs/land_only_500k/terranova_spatial_land_500kEach saved .pt dataset contains:
metadatalatitudelongitudecoordinatescoordinates_latloncoordinates_lonlatyear(per-row, when the run had a scalar or per-row year)- one
*_embeddingstensor per encoder
Example keys:
[
"metadata",
"latitude",
"longitude",
"coordinates",
"coordinates_latlon",
"coordinates_lonlat",
"geoclip_embeddings",
]The metadata block includes:
- selected encoders
- year,
year_mode(static,scalar, orper_row), andyear_rangefor per-row runs - number of points
- coordinate order declarations
- encoder-specific metadata such as embedding dimension and available years
- sampling seed and maximum sampling-attempt limit
- land-mask source, resolution, and Antarctica policy
The same metadata is saved as metadata_json in point-query .npz output and
as the metadata group attribute in Zarr output.
Zarr stores the same named arrays as .pt (latitude, longitude, all three
coordinate layouts, and <encoder>_embeddings) in independently chunked,
float32 arrays. It is the preferred format for hundreds of thousands of points
or high-dimensional/multi-encoder datasets because data can be read in chunks.
Plots are skipped for streamed Zarr sampling; create visualizations from a
deterministic subset in a separate analysis step.
Point queries retain their legacy top-level encoder keys/arrays. Both .pt and
.npz now also retain the submitted coordinates under coordinates,
coordinates_latlon, coordinates_lonlat, latitude, and longitude, so
outputs can be joined safely with generated datasets.
When plotting is enabled, the generator writes:
*_locations.png: sampled land coordinates*_<encoder>_ica.png: embeddings projected to RGB with ICA
The ICA fit is done on a capped subsample and transformed in batches, so large outputs remain tractable.
geospatial-embeddings-report analyzes generated .pt, .zarr, and wide CSV
artifacts without loading encoders, downloading models, or modifying the input
datasets. It writes an offline HTML dashboard, publication-ready PNGs, a PDF,
metric tables, and a reproducibility manifest.
geospatial-embeddings-report \
outputs/geoclip.zarr outputs/satclip.zarr \
--output-dir reports/geoclip_vs_satclip \
--analysis-sample-size 100000 \
--seed 42The report includes spatial coverage and sampling-density maps, embedding atlases (PCA/ICA/UMAP), quality diagnostics, cross-encoder similarity and neighborhood-agreement metrics, temporal displacement views when compatible years are present, and geographic transects. Inputs with the same coordinates are compared by default even if their rows are ordered differently. Use nearest matching only deliberately, and record its tolerance in the report manifest:
geospatial-embeddings-report outputs/year_2023.zarr outputs/year_2024.zarr \
--output-dir reports/temporal \
--match-mode nearest \
--match-tolerance-km 1.0Large Zarr artifacts are streamed for full-data quality summaries; expensive
reductions and pairwise analyses use a deterministic equal-area sample. Pass
--no-umap for a faster report or --probes path/to/probes.json to add custom
transects. The report command refuses to overwrite an existing output directory
unless --overwrite is supplied.
The following products are temporal in this repo:
tesseragoogle_satellite_embeddingterranova(years 1900-2035; after 2025 is extrapolation)
copernicus_embed is treated as a fixed annual product with reference year 2021.
When no --years are given, the generator enumerates every year shared by the
selected temporal encoders. It refuses to do so implicitly for more than 10
years, so long-range encoders such as terranova require explicit --years.
If you pass --years, the generator creates one output per requested year:
python scripts/generate_dataset.py \
--n_points 100000 \
--encoders tessera google_satellite_embedding \
--years 2023 2024 \
--output_path outputs/temporal_pairThis produces:
outputs/temporal_pair_2023.ptoutputs/temporal_pair_2024.pt
The date-specific output names use the selected extension (or .zarr directory).
The shared encoder contract is defined in wrappers/embedding_encoder.py.
The main implementation split is:
wrappers/geoclip_encoder.pywrappers/satclip_encoder.pywrappers/torchgeo_encoders.pywrappers/location_model_encoders.pywrappers/terranova_encoder.pywrappers/registry.py
Canonical encoder names and aliases are centralized in wrappers/registry.py.
Run the test suite with:
python -m unittest discover -s tests -vQuick syntax check:
python -m py_compile scripts/get_embeddings.py scripts/generate_dataset.py wrappers/*.py tests/test_*.py- Use
--seedto reproduce the generator's candidate sampling and valid-row selection, subject to stable encoder/data versions. The saved metadata records the seed and land-mask policy. - Use
--coordinates_outfollowed by--coordinates_inwhen comparing separate encoder runs. This is stronger than matching a seed: it guarantees the exact same submitted coordinates. - Without
--include_antarctica, the land filter excludes points at or below latitude-60before testing against Natural Earth polygons. This preserves the historical policy. Choose--land_resolution 110m,50m, or10mdeliberately: resolution affects coastlines and small islands. Polygon boundaries are not considered land (containssemantics). - Sampling has a bounded
--max_sampling_attemptslimit (default 100) and reports candidate/valid counts on failure. If coverage is sparse, reduce the requested size, select a compatible year, adjust land-mask policy, or raise the limit intentionally. geoclipandsatclipare much faster than the large raster-backed products.tesseraandgoogle_satellite_embeddingmay need substantial network and disk activity on first use.google_satellite_embeddinguses the public AEF annual index and remote GeoTIFF access. Its index is downloaded to a temporary file, validated as Parquet, and atomically promoted into the cache; a corrupt cached index is automatically replaced.
Licensing is not uniform across the supported products.
geoclippackage: MITsatclip: MITtesseraembeddings in TorchGeo: CC0-1.0copernicus_embed: CC-BY-4.0google_satellite_embedding: CC-BY-4.0terranovaweights: CC-BY-4.0 (cite arXiv:2607.29527)
For the CC-BY products, attribution is required.
geospatial_embeddings_wrapper/
├── images/
├── outputs/
├── satclip/
├── scripts/
│ ├── generate_dataset.py
│ ├── generate_report.py
│ └── get_embeddings.py
├── reporting/
├── tests/
├── wrappers/
│ ├── embedding_encoder.py
│ ├── geoclip_encoder.py
│ ├── satclip_encoder.py
│ ├── torchgeo_encoders.py
│ ├── location_model_encoders.py
│ ├── terranova_encoder.py
│ └── registry.py
├── README.md
└── requirements.txt
