Diag output from the previous deploy:
warm/prithvi: AttributeError: 'list' object has no attribute 'view'
The yaml-instantiated GenericNonGeoSegmentationDataModule stores
`means` and `stds` as Python lists (verified against the v2
config). The previous patch chained `_old.means.view(-1)
.detach().clone()` BEFORE handing them to _DictNormalize, which
crashed on the .view() call before reaching the conversion logic
inside the class.
Fix: pass `_old.means` and `_old.stds` to _DictNormalize raw.
The class's `_torch.as_tensor(mean).view(-1, 1, 1).float()` handles
list-to-tensor conversion internally β same end state, no
precondition that assumes the inputs are already tensors.
Also: lifespan warmup now stores a 5-line traceback tail in
_LAST_ERR so the next regression surfaces with a real stack
location through /healthz and /v1/diag.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-ups to the previous EO-fix commit. The TerraMind LoRA
fix (drop tiled_inference at 224Γ224) shipped clean and is now
firing on both Stones; Prithvi-NYC-Pluvial v2 was still erroring
with the same `'list' object has no attribute 'view'` despite
the tensor-typed Normalize.
Real root cause: IBM's run_model in inference.py calls
`datamodule.aug({'image': tensor})['image']` β passing a dict
and indexing the result. The previous patch wrapped a kornia
AugmentationSequential there, which in 0.7+ doesn't accept dict
input cleanly and trips the AttributeError deep inside its
internal storage on first augmentation apply.
Fix: drop kornia entirely from the v2 patch path. Replace it
with a 12-line hand-rolled `_DictNormalize` that does the same
arithmetic β `(img - mean) / std` β and explicitly handles both
dict and tensor input shapes. Identical math, fewer moving
parts, no kornia version skew. Applied symmetrically in:
services/riprap-models/main.py
app/flood_layers/prithvi_liveβ¦
fix(eo): unblock TerraMind LoRA + Prithvi v2 inference on the L4 Spaceβ
The diag wrapper from the previous commit surfaced two real
upstream bugs that the legacy "deps unavailable" / generic 500
masking had been hiding:
1. TerraMind LULC + Buildings: "Expected size 12 but got size 2
for tensor nu" inside terratorch's tiled_inference.
Root cause: terratorch.tasks.tiled_inference doesn't handle
the 5-D (B, C, T, H, W) modality tensor shape that
backbone_use_temporal=True / backbone_temporal_n_timestamps=4
produces, so it slices/concats incorrectly when fusing the
per-modality patches and trips on the 12 (S2 bands) vs 2 (S1
bands) channel mismatch.
Fix: the canonical chip from app/context/eo_chip_cache.py is
already exactly 224Γ224 β the model's native input
resolution. Tiling is unnecessary at that size. Mirror the
training-time inference at experiments/18_terramind_nyc_lora/
shared/inference_ensemble.py:155 (`task.model(x)` direct on
the modality dict). tiled_inference is preserved as a
fallback for chips larger β¦
fix: thread Mellea attempt index + diagnose riprap-models 500sβ
Two regressions surfaced on the May 9 demo. Fix both.
1. Mellea reroll text concatenated in the briefing prose
============================================================
The strict-streaming reconcile path on a Mellea reroll fired
token events for the new attempt without telling the frontend
the attempt index changed. Result: the SvelteKit briefing
buffer never reset between attempts, so attempt 2 + attempt 3
text appended onto the already-rendered attempt 1 text.
Root cause: app/fsm.py:step_reconcile installed a token
forwarder that explicitly dropped the attempt_idx
(`lambda d, _ai: token_cb(d)`), and
app/intents/single_address.py:_on_token took only `delta`. So
the SSE `token` events carried no `attempt` field, and
web/sveltekit/src/lib/client/agentStream.ts:onAttemptStart
never fired because `d.attempt !== currentAttempt` stayed
undefined === undefined.
Fix:
- fsm.py: forward (delta, attempt_idx) to token_cb. Probe-fall
backβ¦
fix(specialists): never fall through to broken local terratorchβ
The Sat May 9 trace surfaced two regressions on the lablab UI Space:
prithvi_nyc_pluvial: RuntimeError: operator torchvision::nms does
not exist
terramind.lulc: deps unavailable on this deployment:
terratorch (RuntimeError), peft
terramind.buildings: deps unavailable on this deployment:
terratorch (RuntimeError), peft
Root cause in both: the specialist tries the configured remote
inference service, the call fails (RemoteUnreachable, ok=False, or
generic exception), and the code silently falls through to the local
terratorch path. Local on the cpu-basic UI Space has a torch /
torchvision binary mismatch (torchvision::nms not registered), so
either we crash with that RuntimeError or β for terramind_nyc, where
_DEPS_OK is False at import time β we surface a misleading "deps
unavailable" message that hides the real (remote) failure.
Fix: when remote was attempted, never fall through to local. Return
a β¦
feat(emissions): real GPU power from NVML on the L4 inference Spaceβ
Drops the cloud-comparison chip and replaces the data-sheet
energy estimate with a real per-call measurement off the L4 GPU.
inference-vllm/proxy.py
- Background sampler reads nvmlDeviceGetPowerUsage every 100 ms into
a 60 s ring buffer. Each forwarded POST records the upstream
wall-clock window and computes the mean power across the matching
samples; results land on the response as X-GPU-Power-W and
X-GPU-Energy-J (joules).
- New GET /v1/power exposes instantaneous + 1 s avg + 5 s avg for
bracket-sampling clients (used by the LLM path, where LiteLLM
hides response headers).
- pynvml init is best-effort: on failure the proxy still serves
traffic, just without power data.
inference-vllm/Dockerfile
- Adds nvidia-ml-py>=12.560 to the proxy's pip layer.
app/emissions.py
- Tracker.record_{llm,ml} accept optional joules_real / power_w_real.
When present, those values land on the row directly and the row is
flagged measured=True. Otherwise we fall back to the data-sheet
β¦
Stale string from the droplet days β _default_hardware_label() still
returned "AMD MI300X" when remote vLLM was configured, so the UI
badge and /api/backend disagreed with the emissions ledger (which
already maps to nvidia_l4 correctly via _hardware_for).
Both Riprap inference Spaces run on L4. Operator override via
RIPRAP_HARDWARE_LABEL still works for redeploys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MI300X droplet was retired 2026-05-06; both inference Spaces
(msradam/riprap-vllm for Granite 4.1 8B FP8 + msradam/riprap-inference
for Prithvi / TerraMind / TTM / GLiNER / Embedding) now run on NVIDIA
L4 (24 GB, Ada Lovelace, 72 W TGP). Updates:
- app/emissions.py: HARDWARE adds nvidia_l4 (~60 W sustained per the
L4 data sheet) and reorders so it's the first/canonical entry. The
MI300X entry stays for operators who redeploy to that hardware and
set RIPRAP_HARDWARE_LABEL=AMD MI300X explicitly.
- app/llm.py:_hardware_for: when RIPRAP_LLM_BASE_URL is set (any
remote vLLM/Ollama backend), default to nvidia_l4 instead of MI300X.
RIPRAP_HARDWARE_LABEL override matrix expanded for l4 / t4 / mi300x.
- app/inference.py:_post: record nvidia_l4 by default; honor MI300X /
T4 overrides via RIPRAP_HARDWARE_LABEL.
Net effect on the briefing: a typical query (~700 LLM tokens + a few
ML calls on L4) now reports ~50-80 mWh instead of the inflated
600 mWh that the MI300X profile producβ¦
feat(emissions): per-call inference energy + token trackerβ
Adds an additive emissions ledger that records every llm.chat call
and every remote app.inference._post call, surfacing the per-query
total Wh / J + token counts on the briefing.
Backend
- app/emissions.py: thread-local Tracker with hardware power profiles
(MI300X 600 W, T4 50 W, Apple M 20 W, x86 30 W) and a summarize()
rollup with by-kind, by-hardware, and frontier-cloud comparison.
- app/llm.py: chat() times each call, picks hardware via engine
detection, reads prompt/completion tokens from LiteLLM usage for
non-stream calls, estimates from response text for streams.
- app/inference.py: _post() records duration vs the MI300X profile.
- app/fsm.py: captures the parent-thread tracker and re-installs it on
the iter_steps runner thread (mirrors the token_callback pattern);
attaches `emissions` alongside the existing `energy` block in both
run() and the iter_steps final event.
- app/context/eo_chip_cache.py + app/flood_layers/prithvi_live.py:
ThreadPoolExecutor initializβ¦
fix(synthesis): handle 3-D DEM from _read_dem_patch (it returns (1,H,W))β
The 400 from /v1/terramind on synthesis came from the wire shape being
(1,1,1,224,224) 5-D instead of (1,1,224,224) 4-D. Root cause: HF's
_read_dem_patch returns a 3-D (1, H, W) array (it interpolates via
torch.functional.interpolate then .squeeze(0).numpy()), and the
synthesis remote-call site assumed it was 2-D and added two leading
dims via dem[None, None, :, :].
Branch on dem_arr.ndim and add exactly the dim(s) we need to reach
4-D (B, C, H, W). Also leave the debug log on the droplet so any
future dim-shape mismatch surfaces in container logs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: every EO specialist's prediction renders as a map layerβ
Round-tripped the per-pixel argmax raster from the droplet so the HF
Space can vectorise it onto WGS84 polygons and the MapLibre layer
actually paints the live model output. Was: only terramind_synthesis's
local fallback path generated polygons; everything else returned
class fractions only and the map showed nothing.
Wire format: droplet's /v1/prithvi-pluvial and /v1/terramind responses
gain `pred_b64` (uint8 base64) + `pred_shape` + (terramind only)
`class_labels`. Small payload β 224x224 uint8 round-trips in ~50 KB.
HF side:
- new app/context/_polygonize.py: polygonize_class_raster() and
polygonize_binary_mask() helpers
- app/context/eo_chip_cache.py adds `bounds_4326` to chip output
- app/context/terramind_nyc lulc/buildings accept bounds_4326,
_try_remote polygonises the response and attaches polygons_geojson
- app/context/terramind_synthesis.py polygonises the remote response
- app/flood_layers/prithvi_live.py polygonises the remote water mask
- app/fsm.py threads chip.bounβ¦
fix(synthesis): DEM shape is (1, 1, H, W) per encoder probeβ
Empirically verified against terratorch_terramind_v1_base_generate by
calling model({'DEM': torch.zeros(...)}) with each shape and reading
the encoder error:
(1, 224, 224) -> not enough values to unpack (got 3)
(1, 1, 224, 224) -> OK, LULC out (1, 10, 224, 224)
(1, 1, 1, 224, 224) -> too many values to unpack (got 5)
The encoder unpacks 'B, C, H, W = x.shape' so 4-D is required; DEM
has 1 channel, hence (1, 1, H, W). Local code's comment said this
but the implementation was off by one (.unsqueeze(0) only adds one
dim). Fix the local path too so it doesn't break if anyone runs
synthesis off the AMD path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: synthesis DEM shape matches local 3-D contractβ
The terramind v1 base generative encoder adds its batch dim internally;
sending an extra leading dim makes the embedding layer try to unpack
5-D into B, C, H, W and raise ValueError. Match the local code: HF
sends (1, H, W); droplet treats incoming (1,1,H,W) as a legacy shape
and squeezes; and rejects anything else with a clean 400 instead of
a 500 with a noisy traceback.
Also add n_building_components (scipy.ndimage.label connected-component
count) to the droplet's buildings response so the TerraMind Buildings
card renders the real number instead of '0 distinct components'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: terramind_synthesis now routes through droplet remote inferenceβ
Synthesis (DEM -> generative LULC via TerraMind v1 base) was the only
EO specialist still local-only. On the HF Space, terratorch's import
chain crashes with `RuntimeError: operator torchvision::nms does not
exist` because torchvision's C extension can't load against our CPU
torch wheel β so synthesis returned a clean skip rather than running.
Add a remote dispatch path:
- services/riprap-models/main.py: extend `_TERRAMIND_SPECS` with the
`synthesis` entry (10 ESRI LULC labels), add `_load_terramind_synthesis`
that pulls the v1 base generate model from terratorch's
FULL_MODEL_REGISTRY, and `_terramind_synthesis_inference` that
takes a 4-D (B, 1, H, W) DEM and emits class fractions matching the
local code's response shape. Same /v1/terramind dispatch handles
all three adapter names now (lulc / buildings / synthesis).
- TerramindIn schema: make all modality fields optional. lulc/buildings
still require s2; synthesis requires dem; the dispatch enforces.
- app/inference.pβ¦
fix: catch RuntimeError in EO deps probes, add demo playbookβ
The deps-probe pattern in terramind_nyc / terramind_synthesis /
prithvi_live only caught ImportError. But terratorch's import chain
on the HF Space raises RuntimeError("operator torchvision::nms does
not exist") because torchvision's C extension can't load against
our CPU torch wheel. The exception propagates past the probe, the
module fails to load, and the FSM step's outer except surfaces the
raw RuntimeError as 'err' in the trace instead of 'skipped'.
Catch any Exception during the probe and treat as unavailable. The
specialist returns a clean 'skipped' entry, the trace UI renders it
gray (silent) instead of red (errored), and the demo reads as
honest engineering instead of broken plumbing.
DEMO-PLAYBOOK.md: top-level handoff doc with the 3 demo queries,
what each one shows, the trace skip messages to expect, and the
end-to-end smoke test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: clean skip for terramind_nyc on torchvision::nmsβ
Same pattern as terramind_synthesis: when remote inference returns
non-ok or is unreachable AND local _ensure_adapter() raises the
torchvision binary-extension error, surface a clean 'skipped'
result with both root causes named instead of a noisy err.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix: surface torchvision::nms as honest skip in terramind_synthesisβ
Was bubbling up as a noisy 'operator torchvision::nms does not exist'
RuntimeError in the trace. Detect it explicitly and return a clean
skip explaining that local inference is unavailable due to the
torchvision binary extension not loading on this deployment.
terramind_synthesis has no remote-inference path, so this is the
honest engineering outcome.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: enable EO specialists via remote ML on HF Spaceβ
Prithvi-NYC-Pluvial, TerraMind LULC, TerraMind Buildings, and the
eo_chip fetch were all returning 'deps unavailable on this
deployment' on the HF Space because their deps gates required
terratorch (which doesn't fit the HF build sandbox per the four
prior failed attempts documented in the Dockerfile). But these
specialists already have remote-inference paths to the AMD MI300X
via app/inference.py β terratorch is only needed for *local*
inference, not for the chip-fetch + remote-call path that the HF
Space actually uses.
Refactor:
- Add planetary-computer, pystac-client, rioxarray, xarray, einops
to requirements.txt. These are pure-Python or thin numpy/rasterio
wrappers (~10 MB combined) β small enough to fit the HF build
budget that terratorch's 250 MB transitive dep cone overflows.
- prithvi_live._has_required_deps splits into two tiers: chip-fetch
deps (Tier 1, always required) and local-inference deps (Tier 2,
only required when remote inference is unavailable). When rβ¦
step_nycha hung the FSM for 8+ minutes per query because
summary_for_point did 20 polygonΓpolygon intersections (5 nearest
developments Γ 4 layers) every time. Refactor to read pre-computed
exposure flags from data/registers/nycha.json β the bake script
already runs that math citywide once. Per-query work is haversine
+ dict lookup, sub-millisecond.
Same pattern for step_doe_schools (reads data/registers/schools.json,
~1ms). Hospitals have no pre-built catalog (~150 entries; would have
required a new build script + LLM-budget bake) so they read the
30 KB hospitals.geojson directly and sample the Cornerstone baked
rasters per hit (~10ms first call, sub-ms after).
Schema change for NYCHA per-query view: drop pct_inside_sandy_2012
and pct_in_dep_extreme_2080 floats (polygon-overlap percentages
weren't in the prebuilt catalog), replace with binary
inside_sandy_2012 + dep_*_class/dep_*_label to match the schools
and hospitals shape. NYCHA developments are now reported with
"centroid is inβ¦
Cornerstone (DEP stormwater + Sandy 2012) bottleneck on HF Space CPU
is GDB cold-load, not the join itself: dep_extreme_2080 alone is 30s
to read on first call, and worker memory pressure evicts the
lru_cache between queries. Bake DEP scenarios + Sandy extent to
DEFLATE uint8 GeoTIFFs at 10 ft/px (data/baked/, 7 MB total) and
sample with rasterio. ~500x faster cold-load, ~5x faster per-query,
full parity on 4 canonical addresses across 3 DEP scenarios + Sandy.
- app/flood_layers/dep_stormwater.py: add join_raster() with
thread-local DatasetReader cache; legacy GDB join() retained as
fallback when baked rasters absent (local dev) and as the
polygon-overlap path for coverage_for_polygon (neighborhood mode)
- app/flood_layers/sandy_inundation.py: same pattern, inside_raster()
- app/fsm.py: step_dep / step_sandy use the raster path
- scripts/bake_cornerstone_rasters.py: one-shot bake (~50s)
- experiments/22_cornerstone_optim/: bench harness + RESULTS.md
UI honesty: stoneRegistry.tsβ¦
Squash-deploy commit for HF Space (XET-compatible β no binary blobs).
Applies the same 4 bugfixes as origin/main db5743c:
- app/fsm.py: yield error final event when FSM runner dies before any
action completes
- web/main.py: accept "nta" target type in compare; dispatch nta targets
to i_nbhd.run with neighborhood sub-plan
- web/main.py: fix mellea requirements_passed merge to use intersection
- web/sveltekit: fix mellea.attempts β mellea.n_attempts field name;
rebuild adapter-static bundle
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
prithvi_nyc_phase14.yaml uses GenericNonGeoSegmentationDataModule which
omits test_transform (leaves it None) and sets aug to terratorch Normalize
(only handles 4D/5D tensors). IBM inference.py:run_model() calls both on a
3D image dict, producing TypeError then ValueError for all neighborhood
queries locally.
Patch inside _ensure_model() when test_transform is None:
- test_transform β albumentations.Compose([ToTensorV2()]) matching IBM base
- aug β kornia.AugmentationSequential with v2-specific means/stds read from
the existing terratorch Normalize, preserving NYC Pluvial normalization
Full transform chain smoke-tested: (H,W,C)βCHWβBCHW dtype=float32.
Blast radius was all neighborhood intent queries, not Hollis specifically.
See /tmp/PRITHVI-HOLLIS-DIAGNOSIS.md for full analysis.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
fix(compare): wire compare intent into SSE handlerβ
The compare intent was classified by the planner but silently fell
through to single_address routing in api_agent_stream because (a)
"compare" was not in INTENTS so _validate() defaulted to
single_address, and (b) there was no routing case in the SSE handler.
Changes:
- app/planner.py: add "compare" to INTENTS, SPECIALISTS (compare is
applicable to all single_address specialists), PLAN_SCHEMA_DESC
hard-rules, _required_specialists, _default_specialists, and
_validate fallback. Planner now emits intent=compare with two
address targets for "compare X vs Y" queries.
- web/main.py: add _run_compare() helper that runs the full
single_address specialist suite sequentially for each target via
i_addr.run() and merges the two paragraphs into one Markdown
document (## PLACE A / ## PLACE B sections). Add compare routing
case to both api_agent_stream and api_agent endpoints.
Verified: "Compare 80 Pioneer Street, Brooklyn to 100 Gold Street,
Manhattan" produces a two-target briefiβ¦
Squashed from 5438cc8..6904684. The slides/asce/deck.pptx is tracked
via git-lfs (added *.pptx to .gitattributes), so this commit carries
only the LFS pointer β no binary blob in history.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Harden remote-ML routing + add address test suiteβ
Wires up the regressions surfaced by a real PS 188 / Lower East Side
run on the AMD MI300X stack. Five fixes + one new probe:
1. Lift the `RIPRAP_HEAVY_SPECIALISTS` gate when remote ML is
reachable. The heavy specialists' GPU work (TerraMind LULC +
Buildings, Prithvi-live, register joins) runs on the droplet, so
the local wall-clock cost drops from ~60 s to ~5 s β gate the
default to ON so the public demo never silently disables them.
2. TTM specialists never crash with the
`ModuleNotFoundError("Could not import module 'PreTrainedModel'")`
that surfaced on local-fallback paths. Force-import
`PreTrainedModel` + `TinyTimeMixerForPrediction` before
`get_model()` so the transformers lazy-registry resolves the
class strings under FSM worker threads.
3. When remote ML is configured but returns non-ok, surface the
remote error rather than fall through to a brittle local model
load (`ttm_forecast`, `ttm_battery_surge`, `prithvi_live`). Local
fallback is rβ¦
feat: route all GPU-accelerable inference through MI300X (Phase 1+2 of full GPU)β
The user's directive: "I want anything that can be GPU accelerated to
run on there. Otherwise, keep it on the CPU of wherever it's running."
Lands seven pieces in one commit; each can stand alone but they're
interlocked.
app/inference.py (new, ~250 lines)
Router shim that mirrors app/llm.py's shape but for non-LLM models.
Exports: prithvi_pluvial(), terramind(), ttm_forecast(),
granite_embed(), gliner_extract(), healthcheck(), backend_info(),
plus the typed RemoteUnreachable exception caller modules catch
to fall back to local. Env-driven via RIPRAP_ML_BACKEND
(auto|remote|local) / RIPRAP_ML_BASE_URL / RIPRAP_ML_API_KEY,
same shape as RIPRAP_LLM_*.
services/riprap-models/ (new microservice)
FastAPI service that runs alongside vLLM on the AMD MI300X
droplet. One endpoint per model class:
/v1/prithvi-pluvial Prithvi-NYC-Pluvial v2 segmentation
/v1/terramind LULC / Buildings / Synthesis (LoRA)
/v1/ttm-forecast β¦
fix(prithvi_live): build v2 model from v2 yaml, not base configβ
C5 originally tried to load v2 ckpt weights into a model built from
the IBM-NASA base config.yaml. They're architecturally different β
v2 ships UNetDecoder + 2-class head; the base ships UperNet (PSP /
FPN). Loading produced a giant size-mismatch RuntimeError on
head.head.2 and dozens of missing/unexpected keys in decoder.fpn1 /
psp_modules / lateral_convs.
Fix: when the active REPO is not BASE_REPO, download the v2 yaml +
v2 ckpt directly from the published HF artefact and let
LightningInferenceModel.from_config build the architecture from the
v2 yaml itself. The yaml's data: section points at training-droplet
paths that don't exist locally, but the
GenericNonGeoSegmentationDataModule constructor only records paths;
splits aren't read until setup(), which we never call.
Falls back to the proven base path on any v2 failure (yaml not in
repo, datamodule constructor strict, etc.) so the specialist degrades
to v1 behaviour rather than no-opping.
Co-Authored-By: Claude Opus 4.7 (1M contβ¦
Wraps msradam/Granite-TTM-r2-Battery-Surge β Apache-2.0 fine-tune of
ibm-granite/granite-timeseries-ttm-r2 trained on AMD MI300X via AMD
Developer Cloud. Test MAE 0.1091 m on held-out 2023-2024 windows
(-41% vs persistence, -25% vs zero-shot TTM r2).
app/live/ttm_battery_surge.py
fetch() pulls the trailing 1024 h of hourly verified water level +
harmonic tide predictions from NOAA station 8518750 (The Battery),
computes surge residual, runs the fine-tuned TTM, and returns the
96 h forecast plus peak-residual / peak-time scalars the reconciler
can paraphrase. Gated by RIPRAP_TTM_BATTERY_SURGE_ENABLE.
app/fsm.py
New @action step_ttm_battery_surge wired into build_app's actions
dict next to the existing TTM specialists. Plumbed through
step_reconcile reads, the snap dict, run() output, and
iter_steps' final-state dict so the SSE stream surfaces the new
state key end-to-end.
app/reconcile.py
build_documents() emits a `ttm_battery` doc inβ¦
Stones C5: upgrade prithvi_live to NYC-Pluvial v2 fine-tuneβ
Switches the live Sentinel-2 segmentation specialist from the
Sen1Floods11 base (`ibm-nasa-geospatial/Prithvi-EO-2.0-300M-TL-
Sen1Floods11`, test flood IoU ~0.10 on NYC chips) to the NYC-specialized
fine-tune (`msradam/Prithvi-EO-2.0-NYC-Pluvial`, test flood IoU
0.5979 β 6x lift). Same input/output contract; doc_id `prithvi_live`
unchanged so reconciler citations and frontend chips stay stable.
Loader is now flexible across two artifact shapes:
v2 (default): Lightning ckpt restored via
SemanticSegmentationTask.load_from_checkpoint. Probes
best_val_loss.ckpt / model.ckpt / last.ckpt then walks for any
*.ckpt in the snapshot.
v1 (fallback): raw .pt + config.yaml via
LightningInferenceModel.from_config β the original code path,
kept so RIPRAP_PRITHVI_LIVE_REPO=ibm-nasa-geospatial/... still
works for A/B comparisons.
Either path resolves the inference helper from BASE_REPO (only the
IBM-NASA repo ships inference.py).
Reconciler doc body now credits the v2 liβ¦
Stones C4: wire TerraMind-NYC LULC + Buildings into FSMβ
Adds three new Burr actions in app/fsm.py (gated behind
RIPRAP_HEAVY_SPECIALISTS):
step_eo_chip -> writes eo_chip (S2L2A + S1RTC + DEM)
step_terramind_lulc -> writes terramind_lulc
step_terramind_buildings-> writes terramind_buildings
eo_chip is a per-query cache: one ~150 MB Sentinel-2/1/DEM fetch from
Microsoft Planetary Computer, shared by both TerraMind specialists so
they don't each refetch. The cache lives in app/context/eo_chip_cache.py
and gracefully no-ops when terratorch / planetary_computer / pystac_client
/ rioxarray aren't installed.
Both TerraMind specialists call into app/context/terramind_nyc.py (added
in C3), which routes inference through terratorch tiled_inference at
224x224 and ships the per-class fraction summary the reconciler quotes.
Reconciler:
- app/reconcile.py:build_documents() now emits tm_lulc (Touchstone,
after prithvi_live) and tm_buildings (Keystone, after the legacy
terramind_synthetic prior).
- trim_docs_to_plβ¦
Refactors app/reconcile.py:build_documents() so document messages
emit in canonical Stone order: preamble (scope_note + geocode) β
Cornerstone (sandy / dep / ida_hwm / prithvi_water / microtopo) β
Keystone (mta / nycha / doe / doh / terramind synthetic) β
Touchstone (floodnet / nyc311 / nws_obs / noaa_tides / prithvi_live)
β Lodestone (nws_alerts / ttm_forecast / ttm_311_forecast /
floodnet_forecast) β policy context (rag / gliner).
Body content of every emit block is preserved character-for-character;
only their order in the function body changed. Section-header
comments were added to delimit each Stone group. Verified with a
synthetic full-state snapshot diff:
before: 24 doc_ids in old (functional-grouping-by-accident) order
after: 24 doc_ids, identical set, regrouped per Stone
The doc-ordering change does flow through to the reconciler prompt
(Granite sees Stone-grouped documents in the <documents> block).
This is the load-bearing migration step for C7's stone_start /
stone_β¦
Stones C1: add taxonomy modules without changing behaviourβ
First atomic step of the Riprap β Stones migration described in
UPDATE_STONES.md. Adds app/stones/{cornerstone,keystone,touchstone,
lodestone,capstone}.py β each exposes NAME / TAGLINE / DESCRIPTION /
SOURCES / collect(state). The FSM is unchanged; nothing imports the
new modules yet. Capstone is a thin alias around app/reconcile.py.
Forward-looking SOURCES entries (terramind_buildings, terramind_lulc,
ttm_battery_surge) are included now and gated in the unit test against
a FUTURE_STATE_KEYS allowlist so commits 4 and 6 can land without
churn here.
tests/test_stones.py is pure-import (no server required) and covers:
- required attrs on every Stone
- SOURCES keys are valid FSM state writes (modulo future keys)
- SOURCES are disjoint across data-Stones
- collect() drops silent specialists
- canonical Stone iteration order
- uniform collect() arity
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two more user-facing modes on top of the address endpoint:
/compare β side-by-side briefings for two queries, useful
when picking between candidate parcels or
comparing two neighborhoods.
/register/{class} β pre-computed exposure for an entire asset
register: NYC DOE schools, NYCHA developments,
MTA subway entrances. The JSON ships baked
(built offline by register_builder), so the
UI is just paginated rendering β every row
shows the same paragraph + tier the address
mode would produce.
Asset loaders normalise each register's source GeoJSON into the
common (name, lat, lon, props) shape register_builder expects.
Burr FSM coordinating nine specialists + agent.py CLIβ
Single sequential pass per query β geocode, then nine specialists
in dependency order (flood layers first because they're the cheap
spatial joins; live API calls grouped after; RAG last because it's
the slowest). State accumulates into a flat dict the reconciler
then turns into doc-role messages.
agent.py is the CLI entrypoint that runs the FSM end-to-end and
prints the reconciled paragraph + per-step trace, used as the
quickest end-to-end smoke test before bringing up the web layer.
Reconciler skeleton + composite score + energy stubβ
The capstone of v0.1's nine-specialist pipeline:
reconcile.py β the prompt scaffold for Granite 4.1:3b. Specialist
outputs become role='document <doc_id>' messages so
the Ollama Modelfile bundles them into a <documents>
block; system prompt enforces a four-section
structure and the citation discipline. Includes a
numeric-claim guardrail that rejects outputs whose
numbers don't appear verbatim in the doc messages.
score.py β composite weighted exposure score across the layers
with a max-empirical floor (Sandy/Ida-observed
always pulls the score up).
energy.py β per-query Wh estimate for the local-Granite path
vs. a cloud LLM baseline. Honesty-mark for the
'no vendor LLM' claim.
This unblocks the FSM (next slot) β every specialist now has a
consumer that turns its dataclass back into prose.
Granite Embedding 278M RAG over five NYC resilience plansβ
Five corpus PDFs constitute the textual evidence base:
comptroller_rain_2024 β NYC Comptroller cloudburst report
coned_22_e_0222 β ConEd Sandy storm-hardening rate case
dep_wastewater_2013 β DEP post-Sandy wastewater facilities plan
mta_resilience_2025 β MTA Climate Resilience Roadmap
nycha_lessons β NYCHA Sandy lessons-learned
Granite Embedding 278M (sentence-transformers, CPU-only) chunks every
PDF on first start, caches the embeddings, and answers retrieve(query)
with cosine top-k passages + page numbers. The reconciler treats each
chunk as a doc-role message so citations resolve back to a specific
PDF and page.
.gitattributes routes *.pdf through LFS.
Two ground-truth-from-the-public layers that complement the modelled
flood maps:
311 β flood-descriptor complaints in a buffered radius around the
query point. Real residents reporting real water, with an
agency/route field that lets us filter to DEP-routed
complaints (street/sewer flooding) vs. the noisy 'damp
basement' general complaints.
FloodNet β NYU/CUSP/Mayor's Office ultrasonic-depth sensor network.
Reports cm-resolution depth to the closest sensor,
plus the most recent flood event > 10 cm.
Both fetch live from NYC Open Data SoQL / FloodNet's REST API, with
a haversine bbox pre-filter to keep the queries cheap.
USGS 3DEP 1/3 arcsec DEM (resampled to 30 m for runtime), with two
hydrology indices computed offline by whitebox-workflows:
TWI β topographic wetness, ln(a/tan Ξ²), high where flow accumulates
HAND β height above nearest drainage, low where overbank flooding
pools first
These three rasters give the briefing terrain context that the
empirical/forward-modelled flood layers can't: even a parcel outside
every published zone can be in a topographic sink that any heavy rain
will fill.
.gitattributes routes *.tif through LFS.
Prithvi-EO 2.0 baked Ida 2021 satellite segmentationβ
NASA/IBM Prithvi-EO 2.0 600M flood-segmentation head run offline over
HLS scenes pre/post Ida 2021, then diffed and shipped as polygons.
This is the 'satellite saw water' layer β independent of the Sandy
extent and the USGS HWM points, comes from a foundation model that
was fine-tuned on labelled flood imagery.
Pre-computed (the model itself isn't in the runtime image): the
specialist just answers 'is this point inside any polygon, what's
the nearest, how confident'. The full live-segmentation pipeline
lands in a later phase as an experiment.
Field-surveyed peak water elevations from the USGS Short-Term Network
flood-response deployment after Ida (Sep 1, 2021). Where Sandy is
the storm-tide reference for coastal NYC, Ida is the cloudburst-pluvial
reference for inland and basement flooding β different physics, same
'what actually happened' grounding role.
Specialist returns the nearest mark by haversine distance plus depth
in feet; the reconciler treats it as point evidence, not extent.
DEP stormwater specialist + 3 forward-modelled rainfall scenariosβ
NYC DEP Stormwater Flood Maps as Esri FileGDBs (current, moderate
2050, extreme 2080-with-SLR). These are forward-modelled β what
DEP's hydrology team thinks 1%/0.2% AEP rain events will pool, given
present and projected sea levels β and they pair with Sandy's
empirical extent to give the briefing both 'what happened' and
'what is modelled to happen'.
.gitattributes routes the eight Esri internal binary types through
LFS.
Sandy 2012 inundation specialist + .gitattributes LFS for *.geojsonβ
Empirical extent of Hurricane Sandy storm-tide inundation, published
by NYC Open Data from FEMA MOTF surge modelling. This is the
historical-observation layer β what actually went under in October
2012 β and serves as the empirical floor when the forward-modelled
DEP scenarios disagree with lived experience.
The 91 MB GeoJSON ships pre-baked; *.geojson now routes through LFS.
NYC's official address resolver. Returns (lat, lon, BBL, borough,
match-quality) for free-form input. Used as the entry point of every
single-address query β every downstream specialist takes lat/lon, so
geocoding has to land first or the FSM has nothing to work with.
Geometry primitives in NYC State Plane (EPSG:2263)β
NYC's empirical flood layers (Sandy, DEP, Ida HWMs) are published in
EPSG:2263 (US survey feet). Reprojecting on every spatial query is
wasteful, so the convention here is: load once, reproject to 2263 at
ingest, do all distance/buffer math in feet. WGS84 is reserved for
inputs (lat/lon from the geocoder) and outputs (GeoJSON for the map).