Skip to main content

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

ResourceURL
Curated site map for LLMshttps://knowledge.wyvern.space/llms.txt
Full docs text (single file)https://knowledge.wyvern.space/llms-full.txt
This page as plain markdownhttps://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 skillhttps://github.com/Nrevyw/wyvern-public-resources
Sensor spectral response curveshttps://github.com/Nrevyw/wyvern-public-resources/tree/main/relative-spectral-responses

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 by scale = 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
  1. The STAC item's COG asset eo:bands (center_wavelength / full_width_half_max in µm) and raster: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 keyContent
Cloud optimized GeoTiffThe hyperspectral imagery (with eo:bands, raster:bands)
Data MaskValid-data mask COG
Pixel Quality MaskQuality/cloud mask COG
Overview image, Thumbnail imagePNG previews
stac_metadataThis item JSON
zip_fileEverything 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.

pip install rasterio numpy pystac requests pyproj shapely spectral matplotlib
PackageUse
rasterioRead/write COGs, windowed reads, reprojection — the default loader
numpyBand math, index calculation, masking
pystacParse STAC items (pystac.Item.from_file(url) works against the catalog)
requestsHTTP fetches (its default User-Agent avoids the CDN 403)
pyproj, shapelyAOI 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-learnClassification and clustering
xarray + rioxarray, daskLabeled dimensions, time series, out-of-core processing
matplotlibSpectral 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:

GoalMethod
"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 bandsMNF (sp.mnf) — preferred over PCA, orders by SNR rather than variance
Fractional abundanceendmembers (sp.smacc/sp.ppi) → sp.unmix
Sharpen narrow absorptionssp.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 VNIRNeeds SWIR (not detectable)
Rare earth elements — Nd³⁺ features at 585, 745, 810, 870 nm; Standard reaches only the first twoClays / phyllosilicates
Ferric iron (Fe³⁺ band ~630–715 nm, both product types)Carbonates
Vegetation pigments, chlorophyll, stressHydrocarbons, most alteration minerals
Water constituents, chlorophyll-a, turbidityEvaporites, 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

UsenmStandard VNIRExtended VNIR
Blue (coastal)445–4901–4
Green55059
Red6601216
Red edge7121721
Red edge7502024
NIR8002327
NIR (upper)87031

Pitfalls checklist

  1. Mask NoData (65535) before applying the 0.0001 scale — scaling first turns NoData into 6.5535, which silently poisons statistics.
  2. 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.
  3. eo:bands wavelengths are µm, not nm — but the GeoTIFF's wavelength tags are already nm. Also: src.scales is 1.0, so the 0.0001 scale must come from STAC.
  4. Asset keys contain spaces (item["assets"]["Cloud optimized GeoTiff"]).
  5. 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.
  6. Python-urllib's default User-Agent gets HTTP 403 from the catalog CDN — set any custom User-Agent (the requests default works).
  7. The imagery host rate-limits. Metadata/previews come from wyvern-odp.com, but COGs are on wyvern-data.com, which returns HTTP 429 under repeated access. GDAL often reports this as Range 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.
  8. A wavelength farther than one FWHM from the nearest band center isn't covered by the sensor — report that rather than substituting silently.
  9. 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.