Extending rs-embed¶
This page documents the extension contract for adding a new embedder.
For repository workflow, issue expectations, and pull request requirements, see Contributing Guide.
In most cases, adding a model means:
- Create an embedder class in
src/rs_embed/embedders/. - Register it with
@register("your_model_name"). - Add it to
src/rs_embed/embedders/catalog.py(MODEL_SPECS). - Implement
describe()andget_embedding(...). - Override batch methods when the model supports true batched inference.
For on-the-fly models, also choose one fetch path:
- declarative fetch via
input_spec = ModelInputSpec(...) - custom fetch via
fetch_input(...)
Useful source-of-truth locations:
- catalog:
src/rs_embed/embedders/catalog.py - base contract:
src/rs_embed/embedders/base.py - on-the-fly examples:
src/rs_embed/embedders/onthefly_*.py - precomputed examples:
src/rs_embed/embedders/precomputed_*.py
Registration¶
Models are discovered through rs_embed.core.registry.
@register("name")registers an embedder class.get_embedder_cls("name")resolves the class.list_models()lists models that have already been loaded in the current process.rs_embed.list_models()returns the stable public model catalog fromMODEL_SPECS.
Model loading is lazy:
get_embedder_cls("name")looks upnameinMODEL_SPECS.- Then it imports the mapped module and reads the mapped class.
- The class is inserted into the runtime registry.
Tip
Put your embedder in rs_embed/embedders/ and add it to src/rs_embed/embedders/catalog.py.
If it's not in MODEL_SPECS, string-based lookup (get_embedding("...")) will not find it.
Embedder Interface¶
All models implement EmbedderBase:
from rs_embed.embedders.base import EmbedderBase
class EmbedderBase:
def describe(self) -> dict: ...
def fetch_input(self, provider, *, spatial, temporal, sensor): ...
def get_embedding(
self,
*,
spatial,
temporal,
sensor,
output,
backend,
device="auto",
input_chw=None,
model_config=None,
): ...
def get_embeddings_batch(..., model_config=None): ...
def get_embeddings_batch_from_inputs(..., model_config=None): ...
The stable extension points are:
describe()for capability metadatafetch_input(...)for model-specific provider fetch behaviorget_embedding(...)for single-item inferenceget_embeddings_batch(...)andget_embeddings_batch_from_inputs(...)for true batched inference
describe()¶
describe() should return a small JSON-serializable capability dictionary. A typical shape is:
{
"type": "on_the_fly" | "precomputed",
"backend": ["provider" | "gee" | "auto" | "tensor", ...],
"inputs": {
"collection": "COPERNICUS/S2_SR_HARMONIZED",
"bands": ["B4", "B3", "B2"],
# or, for models with a more specific provider-facing default:
"provider_default": {
"collection": "...",
"bands": [...]
}
},
"temporal": {"mode": "year" | "range"} | null,
"output": ["pooled", "grid"],
"defaults": {
"scale_m": 10,
"cloudy_pct": 30,
"composite": "median",
"image_size": 224
},
"model_config": {
"variant": {
"type": "string",
"default": "base",
"choices": ["base", "large"]
}
}
}
Note
describe() must stay fast. It should not trigger checkpoint downloads, provider setup, or heavy model loading.
Important fields currently consumed by rs-embed:
backend,output, andtemporalfor capability checksinputs,defaults,modalities, or_default_sensor()for default-sensor resolutionmodel_configfor public model-specific keyword settings such asvariant
Minimal Skeleton¶
This is the smallest useful embedder pattern. It returns a deterministic vector and supports the current public contract.
Create src/rs_embed/embedders/toy_model.py:
from __future__ import annotations
import hashlib
from dataclasses import asdict
from typing import Any, Dict, Optional
import numpy as np
from rs_embed.core.registry import register
from rs_embed.core.embedding import Embedding
from rs_embed.core.errors import ModelError
from rs_embed.core.specs import SpatialSpec, TemporalSpec, SensorSpec, OutputSpec
from rs_embed.embedders.base import EmbedderBase
@register("toy_model_v1")
class ToyModelV1(EmbedderBase):
def describe(self) -> Dict[str, Any]:
return {
"type": "precomputed",
"backend": ["auto"], # use "provider"/"gee" for on-the-fly fetchers
"output": ["pooled"],
}
def get_embedding(
self,
*,
spatial: SpatialSpec,
temporal: Optional[TemporalSpec],
sensor: Optional[SensorSpec],
output: OutputSpec,
backend: str = "auto",
device: str = "auto",
input_chw: Optional[np.ndarray] = None,
model_config: Optional[Dict[str, Any]] = None,
) -> Embedding:
# Use a stable hash so results are reproducible across processes.
seed_bytes = hashlib.blake2s(
f"{spatial!r}|{temporal!r}|{self.model_name}".encode("utf-8"),
digest_size=4,
).digest()
seed = int.from_bytes(seed_bytes, "little")
rng = np.random.default_rng(seed)
if output.mode != "pooled":
raise ModelError("toy_model_v1 only supports pooled output")
vec = rng.standard_normal(512).astype("float32")
meta = {
"model": self.model_name,
"backend": backend,
"device": device,
"model_config": model_config,
"spatial": asdict(spatial),
"temporal": asdict(temporal) if temporal else None,
}
return Embedding(data=vec, meta=meta)
Then register it in src/rs_embed/embedders/catalog.py:
MODEL_SPECS["toy_model_v1"] = ("toy_model", "ToyModelV1")
On-the-fly Models¶
Most on-the-fly embedders follow this flow:
- Use a provider (e.g., Earth Engine) to fetch an input patch (CHW numpy).
- Preprocess it (normalize/resample).
- Run inference.
- Return
Embedding(data=..., meta=...).
Recommended extension pattern:
- If generic provider fetching is enough, prefer declaring
input_spec = ModelInputSpec(...)on the embedder. That lets the basefetch_input()implementation handle provider fetch + normalization. - If your model needs custom fetch logic, fallback chains, multi-sensor routing,
or fetch-time metadata, override
fetch_input(...). - Use
SensorSpecand model defaults so API-levelfetch=...,sensor=..., andmodality=...resolution can reuse your model cleanly.
You can follow existing implementations in:
rs_embed/embedders/onthefly_*.py
Tip
Keep provider IO separate from model inference whenever possible. That makes batching, caching, and export reuse simpler.
Vendored Runtime Code¶
If a model depends on upstream runtime code that is easier to vendor than to wrap as an external package dependency, place that code under src/rs_embed/embedders/_vendor/.
Typical patterns:
- a single vendored module such as
_vendor/prithvi_mae.py - a small vendored package such as
_vendor/anysat/or_vendor/thor/
Recommended layout:
- keep the adapter itself in
onthefly_<model>.pyorprecomputed_<model>.py - keep vendored upstream runtime code in
_vendor/ - keep vendored code minimally patched and document any rs-embed-specific changes in comments or a short local note
License and dependency notes may live alongside the vendored runtime:
- add the upstream license text under
_vendor/, for exampleLICENSE.<model>or a package-localLICENSE - include
NOTICEor attribution material when the upstream project requires it - if the vendored runtime still needs third-party Python packages at import or runtime, document those requirements close to the vendored code and surface the user-facing install error from the adapter with
ModelError
_vendor/ is an implementation area, not a public API surface. Public behavior should remain defined by the adapter, describe(), and the stable rs-embed APIs.
Input Reuse¶
export_batch can prefetch the input patch once and reuse it for both:
- saving
inputs - computing
embeddings
Contract:
If
input_chwis provided, do not fetch inputs again. Useinput_chwas the model input.
Example snippet:
if input_chw is None:
# fetch from backend/provider, or via your custom fetch_input(...)
input_chw = provider.fetch_array_chw(...)
# now preprocess + infer using input_chw
Important
This is the key to avoiding “download twice” when save_inputs=True and save_embeddings=True.
Batch Methods¶
EmbedderBase.get_embeddings_batch defaults to a Python loop calling get_embedding.
EmbedderBase.get_embeddings_batch_from_inputs defaults to a Python loop calling
get_embedding(..., input_chw=...).
Override one or both when the model supports true vectorized inference:
def get_embeddings_batch(
self,
*,
spatials,
temporal=None,
sensor=None,
model_config=None,
output=OutputSpec.pooled(),
backend="gee",
device="auto",
):
# 1) fetch/preprocess inputs for all spatials
# 2) stack into a batch tensor
# 3) run a single forward pass
# 4) split outputs back into Embedding objects
If your model supports prefetched inputs, also consider:
def get_embeddings_batch_from_inputs(
self,
*,
spatials,
input_chws,
temporal=None,
sensor=None,
model_config=None,
output=OutputSpec.pooled(),
backend="auto",
device="auto",
):
# 1) preprocess/stack prefetched CHW inputs
# 2) run a single batched forward pass
# 3) split outputs back into Embedding objects
export_batch(...) prefers get_embeddings_batch_from_inputs(...) when it has prefetched
provider inputs available, so overriding this method usually gives the biggest speedup for
on-the-fly models.
General guidance:
- Batch inference (GPU-friendly).
- Parallelize IO (provider fetch) with threads if needed.
- Keep memory stable by using chunking (see
ExportConfig(chunk_size=...)inexport_batch(...)).
Output Modes¶
OutputSpec controls output shape:
OutputSpec.pooled()→(D,)OutputSpec.grid(...)→(D, H, W)
If your model does not support a mode, raise a clear error:
if output.mode == "grid" and not supported:
raise ModelError("model_x does not support grid output")
Optional Dependencies¶
Many embedders rely on optional packages (e.g., torch, ee).
Recommended pattern:
- Import heavy dependencies inside methods or within a
try/exceptat module import. - If the dependency is missing, raise a helpful error (
ModelError) explaining what to install.
Example:
from rs_embed.core.errors import ModelError
try:
import torch
except Exception as e:
torch = None
_torch_err = e
def _require_torch():
if torch is None:
raise ModelError("Torch is required. Install this repository with: pip install -e .")
Testing¶
Minimum test coverage:
Registry¶
Add a small test ensuring registration works:
from rs_embed.core.registry import get_embedder_cls
def test_toy_model_registered():
cls = get_embedder_cls("toy_model_v1")
assert cls is not None
API-level¶
from rs_embed import PointBuffer, TemporalSpec, OutputSpec, get_embedding
def test_toy_model_get_embedding():
emb = get_embedding(
"toy_model_v1",
spatial=PointBuffer(0, 0, 1000),
temporal=TemporalSpec.year(2022),
output=OutputSpec.pooled(),
backend="auto",
)
assert emb.data.shape == (512,)
Export integration¶
If your model supports input reuse and batch export, add a small export_batch test using monkeypatch to avoid real network calls.
See existing patterns in:
tests/test_export_batch.pytests/test_gee_provider.py
Run tests:
pytest -q
Documentation¶
Update docs in these places as needed:
docs/models.mdfor the overview table / entry pointdocs/models/<model>.mdfor the detailed model pagedocs/models_reference.mdif the model adds important preprocessing or comparison caveats
Use Model Detail Template for the detailed page structure.
Checklist¶
Before opening a PR:
- [ ]
@register("...")added and entry added insrc/rs_embed/embedders/catalog.py - [ ]
describe()is fast and accurate - [ ] on-the-fly fetch path is defined through
input_specorfetch_input(...) - [ ]
get_embedding()supportsinput_chwreuse (if on-the-fly) - [ ] override
get_embeddings_batch_from_inputs()if your model can batch prefetched inputs - [ ] clear errors for missing optional dependencies
- [ ] unit tests added (
pytest -qpasses) - [ ] docs updated (
models.md, model detail page, and reference pages if needed)