Wyvern for AI Agents
This page is the condensed, factual entry point for AI agents, LLMs, and automated
tools working with Wyvern hyperspectral data. Everything here is designed to be
ingested programmatically. A plain-markdown mirror of this page is served at
/AGENTS.md.
Machine-readable endpoints
| Resource | URL |
|---|---|
| Curated site map for LLMs | https://knowledge.wyvern.space/llms.txt |
| Full docs text (single file) | https://knowledge.wyvern.space/llms-full.txt |
| This page as plain markdown | https://knowledge.wyvern.space/AGENTS.md |
| Index library (JSON) | https://raw.githubusercontent.com/Nrevyw/wyvern-public-resources/refs/heads/main/index-library/wyvern_index_library.json |
| STAC catalog root (Open Data) | https://wyvern-odp.com/catalog.json |
| Open Data browser (human UI) | https://opendata.wyvern.space/ |
| Code, notebooks & agent skill | https://github.com/Nrevyw/wyvern-public-resources |
| Sensor spectral response curves | https://github.com/Nrevyw/wyvern-public-resources/tree/main/relative-spectral-responses |
Agent skill (recommended)
A portable, tested skill teaching coding agents the full workflow — discovery,
loading, band resolution, index computation, verification — lives at
agent-skills/working-with-wyvern-data
in the public resources repo. Claude Code users can copy it into .claude/skills/;
its SKILL.md also works as plain context for any other framework, and its bundled
STAC helper script is standard-library Python.
Data format facts
Both L2A and L1B are purchasable. Open Data publishes L2A; an L1B collection exists but returned zero items as of 2026-08 — re-check rather than assuming it is still empty.
L2A surface reflectance (Open Data today), license CC-BY-4.0:
- Cloud-Optimized GeoTIFF,
uint16, multiply byscale= 0.0001 to get reflectance (0–1); NoData = 65535 — mask before scaling - Projected to a per-scene UTM zone (
proj:epsg, e.g. 32639); GSD ≈ 5.2 m - 23 bands (Standard VNIR, Dragonette-1, ~503–799 nm) or 31 bands (Extended VNIR, Dragonette-2/3/4, ~445–870 nm)
L1B top-of-atmosphere radiance:
float32, W·m⁻²·sr⁻¹·µm⁻¹, no scale factor; NoData = −9999- EPSG:4326 with non-square pixels sized to 5 m at scene-centre latitude
- Convert to ToA reflectance with top-of-atmosphere-processing, or atmospherically correct to surface reflectance for quantitative work. Prefer L2A for absorption-feature analysis — L1B radiance carries the solar spectrum and atmospheric features (e.g. the O₂ band near 760 nm) that swamp subtle targets.
Where band metadata lives (two authoritative per-scene sources):
# 1. The GeoTIFF's own tags — already in nm, works offline
with rasterio.open(path) as src:
cwl = [float(src.tags(b)["wavelength"]) for b in range(1, src.count + 1)]
fwhm = [float(src.tags(b)["FWHM"]) for b in range(1, src.count + 1)]
nodata = src.nodata # 65535 — read it, don't hardcode
- The STAC item's COG asset
eo:bands(center_wavelength/full_width_half_maxin µm) andraster:bands(dtype / nodata / scale).
⚠️ src.scales is 1.0 — the GeoTIFF does not carry the 0.0001 reflectance scale.
Take it from STAC raster:bands; trusting src.scales silently leaves data unscaled.
STAC catalog structure
Root https://wyvern-odp.com/catalog.json (STAC 1.0.0) has child catalogs grouping
the same scenes three ways: year/, application/ (agriculture, mining, coastal,
forestry, …), and product-type/{standard,extended} — the collection files' rel: item links are the scenes.
Item assets (keys contain spaces — quote them):
| Asset key | Content |
|---|---|
Cloud optimized GeoTiff | The hyperspectral imagery (with eo:bands, raster:bands) |
Data Mask | Valid-data mask COG |
Pixel Quality Mask | Quality/cloud mask COG |
Overview image, Thumbnail image | PNG previews |
stac_metadata | This item JSON |
zip_file | Everything bundled |
Filterable item properties: eo:cloud_cover, datetime, platform,
view:sun_elevation, proj:epsg, plus the item bbox. Note product_type is the
literal string "hyperspectral" on every item and does not identify the band
configuration — use the band count (23 = Standard, 31 = Extended) or the
product-type/{standard,extended} catalog path.
Code quickstart
import requests, rasterio
import numpy as np
# 1. Discover: walk product-type collections; each rel=item link is a scene.
# (Send a User-Agent header: the CDN 403s Python-urllib's default.)
col = requests.get("https://wyvern-odp.com/product-type/extended/collection.json").json()
item_url = next(l["href"] for l in col["links"] if l["rel"] == "item")
item = requests.get(item_url).json()
# 2. Resolve wavelengths -> 1-based band numbers from THIS scene's metadata
cog = item["assets"]["Cloud optimized GeoTiff"]
bands = cog["eo:bands"] # wavelengths in µm
def band_for(nm):
return min(range(len(bands)),
key=lambda i: abs(bands[i]["center_wavelength"] * 1000 - nm)) + 1
# 3. Load (use a windowed read for an AOI), mask, THEN scale
with rasterio.open(cog["href"]) as src:
red = src.read(band_for(660)).astype("float64")
nir = src.read(band_for(800)).astype("float64")
nodata = src.nodata # 65535, from the file
scale = 0.0001 # from cog["raster:bands"]; src.scales is 1.0
red = np.where(red == nodata, np.nan, red) * scale
nir = np.where(nir == nodata, np.nan, nir) * scale
# 4. Compute + verify: reflectance in [0, ~1], NDVI in [-1, 1]
ndvi = (nir - red) / (nir + red)
Index definitions with per-product-type band mappings (JSON keyed by
"Standard VNIR" / "Extended VNIR", 1-based band_index):
wyvern_index_library.json
— this is the machine-readable source of truth. The browsable
Hyperspectral Index Library is generated from it.
Recommended Python packages
pip install rasterio numpy pystac requests pyproj shapely spectral matplotlib
| Package | Use |
|---|---|
rasterio | Read/write COGs, windowed reads, reprojection — the default loader |
numpy | Band math, index calculation, masking |
pystac | Parse STAC items (pystac.Item.from_file(url) works against the catalog) |
requests | HTTP fetches (its default User-Agent avoids the CDN 403) |
pyproj, shapely | AOI geometry and CRS transforms (each scene has its own UTM zone) |
spectral (SPy) | Target detection (ACE, matched filter), anomaly detection (RX), SAM, MNF, unmixing |
scikit-learn | Classification and clustering |
xarray + rioxarray, dask | Labeled dimensions, time series, out-of-core processing |
matplotlib | Spectral plots and index maps |
pystac-client is only useful against a STAC API; the Open Data catalog is static
JSON, so walk rel: item links instead. Avoid pysptools — it fails to import on
current Python versions; spectral covers the same algorithms and is maintained.
Spectral libraries
OpenSpecLib (third-party, not Wyvern-maintained) amalgamates USGS Spectral Library 7, ECOSTRESS, and EcoSIS into one schema-validated structure. Pin a release — counts, sizes and grid layout shift between them. v0.0.6, which the REE notebook also pins, holds 32,940 spectra: 26,780 vegetation, 2,885 mineral, 1,410 water, 470 rock, 440 man-made. Download release assets directly — no install needed:
BASE=https://github.com/null-jones/openspeclib/releases/download/v0.0.6
curl -sLO $BASE/usgs_splib07.parquet # 38 MB — minerals/rocks
curl -sLO $BASE/wavelengths.parquet # 0.3 MB — REQUIRED for wavelengths
curl -sLO $BASE/ecosis.parquet # 307 MB — optional; vegetation, large
Two schema details that trip up first attempts: spectra store
spectral_data.values but not their wavelengths — those live in
wavelengths.parquet, joined on spectral_data.wavelength_grid_id; and wavelengths
are not uniformly µm — usgs_splib07 and ecostress grids are µm but all 43 ecosis
grids (which hold the 26,780 vegetation spectra) are nm, so read wavelength_unit
rather than hardcoding a conversion. Bad-band fill values are large negatives
(e.g. -1.23e34) and must be masked. There is also a no-install
browser viewer that can search, plot,
simulate Wyvern-band downsampling, and export CSV/ENVI .sli.
Spectral analysis approaches
Run these on masked, scaled reflectance shaped (rows, cols, bands) using spectral:
| Goal | Method |
|---|---|
| "Is material X here?" (have a reference spectrum) | ACE (sp.ace) — scale-invariant, responds to spectral shape rather than brightness |
| "What's unusual here?" (no reference spectrum) | RX (sp.rx) |
| Denoise / reduce bands | MNF (sp.mnf) — preferred over PCA, orders by SNR rather than variance |
| Fractional abundance | endmembers (sp.smacc/sp.ppi) → sp.unmix |
| Sharpen narrow absorptions | sp.remove_continuum |
⚠️ SPy cannot consume a masked cube directly. Wyvern swaths are rotated, so a NoData
fringe is always present. ace, rx, mnf and calc_stats raise NaNValueError, and
spectral_angles and smacc are worse — they return corrupted or all-NaN output
without raising. Pass only the valid pixels as a degenerate (N, 1, bands) array and
scatter the results back; the agent skill ships tested valid_pixels / scatter_scores
helpers for exactly this.
SPy has no mtmf() — don't report plain matched_filter as MTMF. For working code,
thresholding, and the failure modes of each method, use the skill's
spectral-analysis reference
rather than reimplementing from this summary.
The core pipeline is resample reference spectrum → ACE → verify hits resemble the reference, worked end to end in the rare earth elements notebook (neodymium at Mountain Pass) — which runs ACE on plain scaled reflectance and uses continuum removal only to inspect features in plots, not as a detection step. Read it before building a new detection workflow.
Resample reference spectra before comparing. Library spectra are measured at
1–10 nm; Wyvern bands are 16–32 nm wide, so a reference must be convolved onto each
band's response (FWHM-weighted). Naive interpolation over-weights narrow features and
produces wrong scores. The agent skill ships scripts/resample_spectra.py for this.
Wyvern is VNIR-only, and the range depends on the product type — Standard VNIR is 503–799 nm, Extended VNIR 445–869 nm. Check the band count before applying this table; a target at 460 nm or 860 nm is undetectable on a Standard scene:
| Detectable in VNIR | Needs SWIR (not detectable) |
|---|---|
| Rare earth elements — Nd³⁺ features at 585, 745, 810, 870 nm; Standard reaches only the first two | Clays / phyllosilicates |
| Ferric iron (Fe³⁺ band ~630–715 nm, both product types) | Carbonates |
| Vegetation pigments, chlorophyll, stress | Hydrocarbons, most alteration minerals |
| Water constituents, chlorophyll-a, turbidity | Evaporites, sulfates |
Detecting ferric iron is not the same as naming the oxide. Hematite's discriminating minimum is ~860 nm (just inside Extended, outside Standard) and goethite's is ~900–920 nm — beyond both. Say "ferric material is present"; only Extended supports "hematite-like", and goethite cannot be located at all.
Don't over-generalize to "minerals need SWIR" — REE detection in VNIR is a proven Wyvern workflow. But when a target's diagnostic features fall outside the scene's range, say so rather than presenting a weak score as a detection.
Interpreting detection results: scores are relative to the scene, not absolute. Threshold statistically (e.g. 99.9th percentile) and state the threshold; confirm hits aren't clouds, shadows, glint, or scene edges; check that the detected pixels' mean spectrum actually resembles the target; and corroborate with a second method. "Not detected" is a valid finding.
Common wavelength → band numbers
| Use | nm | Standard VNIR | Extended VNIR |
|---|---|---|---|
| Blue (coastal) | 445–490 | — | 1–4 |
| Green | 550 | 5 | 9 |
| Red | 660 | 12 | 16 |
| Red edge | 712 | 17 | 21 |
| Red edge | 750 | 20 | 24 |
| NIR | 800 | 23 | 27 |
| NIR (upper) | 870 | — | 31 |
Pitfalls checklist
- Mask NoData (65535) before applying the 0.0001 scale — scaling first turns NoData into 6.5535, which silently poisons statistics.
- Never hardcode band numbers across scenes — Standard and Extended VNIR differ (band 20 is ~750 nm on Standard but ~700 nm on Extended); resolve per scene.
eo:bandswavelengths are µm, not nm — but the GeoTIFF'swavelengthtags are already nm. Also:src.scalesis 1.0, so the 0.0001 scale must come from STAC.- Asset keys contain spaces (
item["assets"]["Cloud optimized GeoTiff"]). - Each scene has its own UTM zone — reproject to a common CRS before mosaicking or cross-scene comparison; transform lon/lat AOIs into the scene CRS before windowed reads.
- Python-urllib's default User-Agent gets HTTP 403 from the catalog CDN — set
any custom User-Agent (the
requestsdefault works). - The imagery host rate-limits. Metadata/previews come from
wyvern-odp.com, but COGs are onwyvern-data.com, which returns HTTP 429 under repeated access. GDAL often reports this asRange downloading not supported by this server!rather than a 429 — treat it as "back off and retry" (GDAL_HTTP_MAX_RETRY,GDAL_HTTP_RETRY_DELAY), and fall back to downloading the asset if streaming keeps failing. - A wavelength farther than one FWHM from the nearest band center isn't covered by the sensor — report that rather than substituting silently.
- Diagnostic features outside the scene's range are undetectable — Standard is 503–799 nm, Extended 445–869 nm. Clay, carbonate, sulfate and hydrocarbon absorptions are in SWIR; say so instead of reporting a weak match. REE and ferric iron are VNIR-detectable, so don't refuse those either.