KSvend Claude Happy commited on
Commit
0ca7a83
Β·
1 Parent(s): 9b79e73

docs: add design spec for CDSE openEO EO product upgrade

Browse files

Fundamental upgrade from scene-level metadata proxies to pixel-level
raster products. Replaces Open-Meteo/STAC-metadata approach with
server-side processing via CDSE openEO for Sentinel-1/2, adds SAR
change detection and built-up area monitoring, upgrades map rendering
from blank cartopy grids to raster overlays on true-color composites.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>

docs/superpowers/specs/2026-03-31-openeo-eo-upgrade-design.md ADDED
@@ -0,0 +1,457 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Aperture EO Product Upgrade β€” CDSE openEO
2
+
3
+ **Date:** 2026-03-31
4
+ **Status:** Draft
5
+ **Scope:** Fundamental upgrade from scene-level metadata proxies to pixel-level raster products via CDSE openEO
6
+
7
+ ## Problem
8
+
9
+ Aperture's current EO products are not credible to technical EO specialists:
10
+
11
+ 1. **No real spectral indices** β€” vegetation and water use Sentinel-2 scene-level metadata percentages (`s2:vegetation_percentage`, `s2:water_percentage`) instead of computing NDVI/MNDWI from pixels.
12
+ 2. **No satellite imagery in reports** β€” maps render as bounding boxes on blank grids. No true-color composites, no visual context.
13
+ 3. **Spatial analysis is too coarse** β€” rainfall, LST, and NO2 query a single centroid point via Open-Meteo. Large AOIs lose all spatial variation.
14
+ 4. **Maps and charts look amateur** β€” cartopy-based maps with no basemap (shapefiles fail in Docker), sparse 2-point charts, empty x-axes.
15
+ 5. **Some indicators aren't real observations** β€” NO2 comes from a CAMS forecast model, not satellite data. Cropland duplicates vegetation without land cover classification.
16
+
17
+ ## Solution
18
+
19
+ Replace the data processing backbone with **CDSE openEO** (Copernicus Data Space Ecosystem). Processing graphs are defined in Python and executed server-side on ESA infrastructure. Aperture receives computed raster products (GeoTIFFs) and handles post-processing, status classification, and report rendering locally.
20
+
21
+ Active fires remain on NASA FIRMS API (near-real-time, already working well).
22
+
23
+ ## Architecture
24
+
25
+ ```
26
+ User defines AOI + period
27
+ β”‚
28
+ β–Ό
29
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
30
+ β”‚ Aperture App β”‚ (HF Space, free tier)
31
+ β”‚ - Gradio frontend β”‚
32
+ β”‚ - Job orchestrator β”‚
33
+ β”‚ - Report renderer β”‚
34
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
35
+ β”‚ openEO process graphs + FIRMS HTTP
36
+ β–Ό
37
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
38
+ β”‚ CDSE openEO β”‚ β”‚ NASA FIRMS β”‚
39
+ β”‚ - Sentinel-1 ARD β”‚ β”‚ - Fire pointsβ”‚
40
+ β”‚ - Sentinel-2 NDVI β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
41
+ β”‚ - CHIRPS rainfall β”‚
42
+ β”‚ - Composites β”‚
43
+ β”‚ - LST, nightlights β”‚
44
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
45
+ β”‚ GeoTIFF / JSON results
46
+ β–Ό
47
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
48
+ β”‚ Post-processing β”‚ (on Aperture app, lightweight)
49
+ β”‚ - Clip to AOI β”‚
50
+ β”‚ - Compute zonal stats β”‚
51
+ β”‚ - Classify status β”‚
52
+ β”‚ - Render maps/PDF β”‚
53
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
54
+ ```
55
+
56
+ ### openEO Processing Model
57
+
58
+ Processing graphs are defined in Python using the `openeo` library. Nothing executes locally β€” the graph is serialized and sent to CDSE, which runs it on their infrastructure and returns a GeoTIFF.
59
+
60
+ ```python
61
+ import openeo
62
+
63
+ conn = openeo.connect("openeo.dataspace.copernicus.eu")
64
+ conn.authenticate_oidc()
65
+
66
+ cube = conn.load_collection(
67
+ "SENTINEL2_L2A",
68
+ spatial_extent={"west": 32.45, "south": 15.65, "east": 32.65, "north": 15.8},
69
+ temporal_extent=["2025-03-01", "2026-03-01"],
70
+ bands=["B04", "B08"]
71
+ )
72
+ ndvi = (cube.band("B08") - cube.band("B04")) / (cube.band("B08") + cube.band("B04"))
73
+ monthly = ndvi.aggregate_temporal_period("month", reducer="median")
74
+ result = monthly.download("ndvi_monthly.tif")
75
+ ```
76
+
77
+ ### Key Design Decisions
78
+
79
+ 1. **Synchronous for small jobs** β€” openEO supports sync (< 5 min) and async batch. For typical AOIs (< 500 kmΒ²) at 100m resolution, sync should work. Async batch with polling for larger jobs.
80
+
81
+ 2. **Results as GeoTIFF** β€” openEO returns multi-band rasters. Aperture downloads, clips, computes zonal stats, classifies status, renders maps. The existing indicator β†’ status β†’ report pipeline stays intact.
82
+
83
+ 3. **One openEO connection per job** β€” authenticate once, submit indicator processing graphs sequentially (memory constraint), collect results.
84
+
85
+ 4. **Resolution as config** β€” default 100m for free-tier HF Space (2GB RAM). Configurable to 10m when deployed on larger hardware. This is a spatial resampling parameter passed to openEO, not a code change.
86
+
87
+ 5. **Graceful fallback** β€” if CDSE is down or quota exhausted, fall back to current metadata-based approach with a "degraded data quality" flag in the report.
88
+
89
+ 6. **Caching** β€” downloaded GeoTIFFs stored in `results/{job_id}/`. Re-running a report for the same AOI/period skips openEO calls.
90
+
91
+ ## Configuration
92
+
93
+ New environment variables / config constants:
94
+
95
+ | Variable | Default | Description |
96
+ |---|---|---|
97
+ | `APERTURE_RESOLUTION_M` | `100` | Spatial resolution in meters. 100m for free tier, 10m on upgraded hardware |
98
+ | `APERTURE_MAX_AOI_KM2` | `500` | Soft limit for AOI size on free tier |
99
+ | `OPENEO_BACKEND` | `openeo.dataspace.copernicus.eu` | openEO backend URL |
100
+ | `OPENEO_CLIENT_ID` | (secret) | CDSE OAuth2 client ID |
101
+ | `OPENEO_CLIENT_SECRET` | (secret) | CDSE OAuth2 client secret |
102
+
103
+ Existing config stays: `APERTURE_CORS_ORIGINS`, `APERTURE_DEMO`.
104
+
105
+ ## Indicator Set
106
+
107
+ ### Upgraded Indicators (openEO-processed)
108
+
109
+ #### 1. Vegetation β€” NDVI (replaces vegetation + cropland)
110
+
111
+ | Field | Value |
112
+ |---|---|
113
+ | **ID** | `ndvi` |
114
+ | **Source** | Sentinel-2 L2A via CDSE |
115
+ | **Method** | Pixel-level NDVI = (B08 - B04) / (B08 + B04), monthly median composites, cloud-masked (SCL band) |
116
+ | **Baseline** | 5-year monthly NDVI medians |
117
+ | **Status** | Anomaly = current median - baseline median. GREEN: β‰₯ -0.05, AMBER: -0.05 to -0.15, RED: < -0.15 |
118
+ | **Outputs** | Monthly NDVI raster, anomaly map (current vs baseline), zonal stats |
119
+ | **Resolution** | Configurable (default 100m, native 10m) |
120
+
121
+ #### 2. Water / Flood Extent
122
+
123
+ | Field | Value |
124
+ |---|---|
125
+ | **ID** | `water` |
126
+ | **Source** | Sentinel-2 L2A (MNDWI) + Sentinel-1 GRD (flood backup) |
127
+ | **Method** | MNDWI = (B03 - B11) / (B03 + B11), threshold > 0.0 for water classification. Sentinel-1 VH backscatter drop for cloud-free flood detection |
128
+ | **Baseline** | 3-year water extent frequency map |
129
+ | **Status** | Change in water pixel fraction. GREEN: within Β±10pp, AMBER: 10-25pp change, RED: > 25pp change |
130
+ | **Outputs** | Water mask raster, flood extent change map, area statistics |
131
+ | **Resolution** | Configurable (default 100m, optical native 10m, SAR native 10m) |
132
+
133
+ #### 3. Land Surface Temperature
134
+
135
+ | Field | Value |
136
+ |---|---|
137
+ | **ID** | `lst` |
138
+ | **Source** | Sentinel-3 SLSTR LST via CDSE (preferred), or MODIS MOD11A2 via NASA AppEEARS (fallback β€” MODIS may not be on CDSE) |
139
+ | **Method** | Daytime LST (Band 31), quality-filtered, monthly mean composites |
140
+ | **Baseline** | 5-year monthly means |
141
+ | **Status** | Z-score anomaly. GREEN: |z| < 1.0, AMBER: 1.0-2.0, RED: > 2.0 |
142
+ | **Outputs** | Monthly LST raster, anomaly map, zonal stats |
143
+ | **Resolution** | 1 km (MODIS native) |
144
+
145
+ #### 4. Rainfall β€” SPI
146
+
147
+ | Field | Value |
148
+ |---|---|
149
+ | **ID** | `rainfall` |
150
+ | **Source** | CHIRPS v2.0 pentadal/monthly. Primary: direct UCSB/IRI download (CHIRPS is not a Copernicus product and may not be on CDSE). Alternative: ERA5-Land precipitation via CDSE openEO |
151
+ | **Method** | Monthly precipitation totals, Standardized Precipitation Index (SPI-3) against 30-year climatology |
152
+ | **Baseline** | CHIRPS 30-year climatology (1991-2020) |
153
+ | **Status** | SPI-3 classification. GREEN: SPI > -1.0, AMBER: -1.0 to -1.5, RED: < -1.5 (severe drought) |
154
+ | **Outputs** | Monthly precipitation raster, SPI anomaly map, zonal stats |
155
+ | **Resolution** | ~5 km (CHIRPS native) |
156
+
157
+ #### 5. SAR Change Detection
158
+
159
+ | Field | Value |
160
+ |---|---|
161
+ | **ID** | `sar_change` |
162
+ | **Source** | Sentinel-1 GRD (VV + VH polarization) via CDSE |
163
+ | **Method** | Log-ratio change detection: dB_change = 10Β·log10(current / baseline). Temporal composites (monthly median) to reduce speckle. VV for surface change, VH for vegetation structure |
164
+ | **Baseline** | 12-month pre-period median backscatter |
165
+ | **Status** | Based on area fraction with significant change (|dB_change| > 2.0). GREEN: < 5%, AMBER: 5-15%, RED: > 15% |
166
+ | **Outputs** | Backscatter change raster (dB), classified change map, zonal stats |
167
+ | **Resolution** | Configurable (default 100m, native 10m) |
168
+
169
+ #### 6. Nightlights
170
+
171
+ | Field | Value |
172
+ |---|---|
173
+ | **ID** | `nightlights` |
174
+ | **Source** | VIIRS DNB monthly composites via Colorado School of Mines EOG (direct download β€” VIIRS is not a Copernicus product). CDSE fallback if EOG adds openEO support |
175
+ | **Method** | Monthly mean radiance (nW·cm⁻²·sr⁻¹), stray-light corrected |
176
+ | **Baseline** | 3-year same-month mean radiance |
177
+ | **Status** | Percentage change from baseline. GREEN: > -15%, AMBER: -15% to -40%, RED: < -40% |
178
+ | **Outputs** | Radiance raster, change map, zonal stats |
179
+ | **Resolution** | ~500m (VIIRS native) |
180
+
181
+ #### 7. Built-up Area Change
182
+
183
+ | Field | Value |
184
+ |---|---|
185
+ | **ID** | `built_up` |
186
+ | **Source** | Sentinel-1 GRD coherence pairs via CDSE |
187
+ | **Method** | InSAR coherence between consecutive passes. Coherence drop indicates structural change (destruction). Coherence increase can indicate new construction. Monthly coherence composites |
188
+ | **Baseline** | 12-month pre-period mean coherence |
189
+ | **Status** | Area fraction with coherence loss > 0.3. GREEN: < 2%, AMBER: 2-10%, RED: > 10% |
190
+ | **Outputs** | Coherence change raster, classified damage map, zonal stats |
191
+ | **Resolution** | Configurable (default 100m, native ~20m) |
192
+
193
+ ### Kept Outside openEO
194
+
195
+ #### 8. Active Fires
196
+
197
+ | Field | Value |
198
+ |---|---|
199
+ | **ID** | `fires` |
200
+ | **Source** | NASA FIRMS API (VIIRS SNPP NRT, 375m) |
201
+ | **Method** | Unchanged β€” point-based fire detections, confidence filtering, 10-day chunked queries |
202
+ | **Outputs** | Fire point GeoJSON (upgraded: rendered on true-color composite base) |
203
+
204
+ ### Dropped
205
+
206
+ | Indicator | Reason |
207
+ |---|---|
208
+ | **NO2** (`no2.py`) | CAMS forecast model data, not satellite observation. Not credible as EO product |
209
+ | **Cropland** (`cropland.py`) | Merged into NDVI vegetation β€” redundant without land cover classification |
210
+ | **Food Security composite** (`food_security.py`) | Becomes a narrative section in the report, not a pseudo-indicator with traffic-light status |
211
+
212
+ ### Added to Every Report
213
+
214
+ | Product | Source | Purpose |
215
+ |---|---|---|
216
+ | **True-color composite** | Sentinel-2 B04/B03/B02 via openEO | Visual context β€” the first thing any EO analyst looks for |
217
+ | **False-color composite** | Sentinel-2 B08/B04/B03 via openEO | Vegetation health at a glance β€” red = healthy vegetation |
218
+
219
+ These are rendered as full-page images in the report (before/current period side by side) and used as base layers for all indicator maps.
220
+
221
+ ## Map Rendering Upgrade
222
+
223
+ ### Current β†’ New
224
+
225
+ Replace cartopy with **rasterio + matplotlib** for raster-native rendering.
226
+
227
+ | Aspect | Current | New |
228
+ |---|---|---|
229
+ | **Base layer** | Blank grid (cartopy fails in Docker) | True-color Sentinel-2 composite |
230
+ | **Indicator overlay** | Colored bounding box or point scatter | Raster overlay with transparency (alpha blend on true-color base) |
231
+ | **Colormaps** | Generic matplotlib defaults | Indicator-specific diverging colormaps (RdYlGn for NDVI, RdBu for temperature, etc.) |
232
+ | **AOI boundary** | Orange rectangle | Subtle white/black outline on satellite imagery |
233
+ | **Annotations** | Lat/lon gridlines only | Scale bar, north arrow, coordinate labels, colorbar with units |
234
+ | **Legend** | None or basic | Proper legend with units, data source, date range |
235
+ | **Output size** | 4"Γ—3" at 150 DPI | 6"Γ—5" at 200 DPI (higher quality for PDF) |
236
+
237
+ ### Map Types Produced Per Indicator
238
+
239
+ Each indicator produces up to 3 maps:
240
+
241
+ 1. **Current-period map** β€” indicator raster on true-color base (e.g., NDVI values overlaid on RGB)
242
+ 2. **Anomaly/change map** β€” diverging colormap showing deviation from baseline (e.g., NDVI loss in red, gain in green)
243
+ 3. **Classification map** β€” binary/categorical overlay (e.g., flood extent, damaged areas)
244
+
245
+ The summary map becomes a **multi-indicator change detection overview** β€” a single map showing the spatial distribution of where indicators flag RED/AMBER.
246
+
247
+ ### Rendering Implementation
248
+
249
+ ```python
250
+ import rasterio
251
+ import matplotlib.pyplot as plt
252
+ import numpy as np
253
+
254
+ def render_indicator_map(
255
+ true_color_tif: str, # Sentinel-2 RGB composite GeoTIFF
256
+ indicator_tif: str, # Indicator raster (NDVI, LST, etc.)
257
+ aoi_geojson: dict, # AOI boundary
258
+ output_path: str,
259
+ cmap: str = "RdYlGn", # Diverging colormap
260
+ vmin: float = None,
261
+ vmax: float = None,
262
+ alpha: float = 0.6, # Overlay transparency
263
+ label: str = "",
264
+ ):
265
+ fig, ax = plt.subplots(figsize=(6, 5), dpi=200)
266
+
267
+ # Render true-color base
268
+ with rasterio.open(true_color_tif) as src:
269
+ rgb = src.read([1, 2, 3])
270
+ extent = [src.bounds.left, src.bounds.right, src.bounds.bottom, src.bounds.top]
271
+ rgb_normalized = np.clip(rgb / 3000, 0, 1) # Sentinel-2 reflectance scaling
272
+ ax.imshow(rgb_normalized.transpose(1, 2, 0), extent=extent)
273
+
274
+ # Overlay indicator raster
275
+ with rasterio.open(indicator_tif) as src:
276
+ data = src.read(1)
277
+ nodata = src.nodata
278
+ masked = np.ma.masked_where(data == nodata, data)
279
+ im = ax.imshow(masked, extent=extent, cmap=cmap, alpha=alpha, vmin=vmin, vmax=vmax)
280
+
281
+ # Colorbar, AOI outline, annotations
282
+ plt.colorbar(im, ax=ax, label=label, shrink=0.8)
283
+ # ... scale bar, north arrow, title
284
+ fig.savefig(output_path, bbox_inches="tight")
285
+ plt.close(fig)
286
+ ```
287
+
288
+ ## Chart Rendering Upgrade
289
+
290
+ ### Changes
291
+
292
+ | Aspect | Current | New |
293
+ |---|---|---|
294
+ | **Data density** | 2-point charts (baseline, current) for most indicators | 12+ monthly data points from openEO temporal aggregation |
295
+ | **Baseline overlay** | None | Shaded min-max band + dashed mean line (from per-year monthly stats) |
296
+ | **Anomaly mode** | Not available | For LST, rainfall: chart shows deviation from baseline, not absolute values |
297
+ | **X-axis** | Often too wide, empty space | Tight to analysis period, monthly ticks |
298
+ | **Before/after context** | None | Vertical line marking baseline/current boundary |
299
+
300
+ The existing chart rendering code in `app/outputs/charts.py` is mostly retained β€” it already supports baseline overlays (from the Phase 1 plan). The main change is that every indicator now supplies dense monthly data instead of 2-point summaries.
301
+
302
+ ## Report Structure
303
+
304
+ ### Current Report Sections
305
+
306
+ 1. Title + metadata
307
+ 2. How to Read This Report
308
+ 3. Executive Summary (traffic-light counts)
309
+ 4. Summary map (AOI bounding box)
310
+ 5. Per-indicator: status badge, map, chart, summary, limitations
311
+ 6. Status summary table
312
+ 7. Data Sources & Methodology
313
+ 8. Disclaimer
314
+
315
+ ### New Report Sections
316
+
317
+ 1. **Title + metadata** β€” unchanged, add resolution and processing backend info
318
+ 2. **Visual Overview** (NEW) β€” full-page before/current true-color composite, side by side. This is the first thing an EO analyst wants to see.
319
+ 3. **False-color Overview** (NEW) β€” NIR-R-G composite for vegetation health context
320
+ 4. **Executive Summary** β€” traffic-light counts, unchanged
321
+ 5. **Multi-indicator Change Map** (NEW) β€” single spatial overview showing where indicators flag concern
322
+ 6. **Per-indicator sections** (UPGRADED):
323
+ - Status badge + headline
324
+ - Current-period map (indicator on true-color base)
325
+ - Anomaly/change map (diverging colormap)
326
+ - Monthly time-series chart with baseline band
327
+ - Zonal statistics table (mean, std, % area affected)
328
+ - Summary text + limitations
329
+ 7. **Food Security Narrative** (CHANGED) β€” prose section synthesizing rainfall, NDVI, LST, and fires into a food security assessment. Not a traffic-light indicator.
330
+ 8. **Status summary table** β€” unchanged
331
+ 9. **Data Sources & Methodology** (UPGRADED) β€” actual processing chain descriptions (e.g., "Sentinel-2 L2A β†’ SCL cloud mask β†’ NDVI = (B08-B04)/(B08+B04) β†’ monthly median composite β†’ 5-year anomaly")
332
+ 10. **Disclaimer** β€” unchanged
333
+
334
+ ## Files Changed
335
+
336
+ ### New Files
337
+
338
+ | File | Purpose |
339
+ |---|---|
340
+ | `app/openeo_client.py` | openEO connection management, authentication, graph builders for each indicator |
341
+ | `app/indicators/ndvi.py` | Replaces vegetation.py + cropland.py |
342
+ | `app/indicators/sar_change.py` | New: SAR backscatter change detection |
343
+ | `app/indicators/built_up.py` | New: coherence-based structural change |
344
+ | `app/config.py` | Centralized configuration (resolution, AOI limits, backend URL) |
345
+
346
+ ### Modified Files
347
+
348
+ | File | Change |
349
+ |---|---|
350
+ | `app/indicators/water.py` | Rewrite: MNDWI pixel-level water classification via openEO |
351
+ | `app/indicators/lst.py` | Rewrite: MODIS LST via openEO instead of Open-Meteo |
352
+ | `app/indicators/rainfall.py` | Rewrite: CHIRPS SPI via openEO instead of Open-Meteo |
353
+ | `app/indicators/nightlights.py` | Rewrite: VIIRS DNB via openEO/EOG instead of Planetary Computer |
354
+ | `app/indicators/fires.py` | Minor: map rendering upgrade (points on true-color base) |
355
+ | `app/indicators/base.py` | Add openEO connection parameter, raster result handling |
356
+ | `app/indicators/__init__.py` | Update registry: remove cropland/no2/food_security, add ndvi/sar_change/built_up |
357
+ | `app/outputs/maps.py` | Rewrite: rasterio-based rendering replacing cartopy |
358
+ | `app/outputs/charts.py` | Extend: all indicators now supply monthly data, anomaly chart mode |
359
+ | `app/outputs/report.py` | Extend: new sections (visual overview, change map, food security narrative) |
360
+ | `app/models.py` | Add resolution config to AOI, remove food_security from indicator list |
361
+ | `Dockerfile` | Remove cartopy + Natural Earth. Add openeo, keep rasterio/rioxarray |
362
+ | `pyproject.toml` | Add `openeo` dependency. Remove `cartopy`, `planetary-computer`, `stackstac` |
363
+
364
+ ### Deleted Files
365
+
366
+ | File | Reason |
367
+ |---|---|
368
+ | `app/indicators/vegetation.py` | Replaced by `ndvi.py` |
369
+ | `app/indicators/cropland.py` | Merged into `ndvi.py` |
370
+ | `app/indicators/no2.py` | Dropped β€” CAMS model data, not EO |
371
+ | `app/indicators/food_security.py` | Replaced by narrative section in report |
372
+
373
+ ## Deployment
374
+
375
+ ### Hardware
376
+
377
+ Free HF Space (2GB RAM). Resolution defaults to 100m to fit memory constraints.
378
+
379
+ At 100m resolution, a 500 kmΒ² AOI produces rasters of ~220Γ—220 pixels per band per month β€” roughly 0.5MB per indicator product. Comfortable on 2GB.
380
+
381
+ ### Resolution Scaling
382
+
383
+ | Deployment | `APERTURE_RESOLUTION_M` | Pixel grid (500 kmΒ² AOI) | RAM per indicator |
384
+ |---|---|---|---|
385
+ | Free HF Space (2GB) | `100` | ~220Γ—220 | ~0.5 MB |
386
+ | CPU Upgrade HF ($9/mo, 16GB) | `20` | ~1100Γ—1100 | ~12 MB |
387
+ | CPU Upgrade HF ($9/mo, 16GB) | `10` | ~2200Γ—2200 | ~48 MB |
388
+
389
+ Upgrading resolution requires only changing the environment variable. No code changes.
390
+
391
+ ### Memory Management
392
+
393
+ Process indicators sequentially (one at a time). After each indicator:
394
+ 1. Download GeoTIFF from openEO
395
+ 2. Compute zonal stats
396
+ 3. Render maps
397
+ 4. Write results to disk
398
+ 5. Release raster arrays from memory
399
+
400
+ ### CDSE Authentication
401
+
402
+ CDSE openEO supports OAuth2 client credentials:
403
+ 1. Register free CDSE account at dataspace.copernicus.eu
404
+ 2. Create client credentials in the dashboard
405
+ 3. Store as HF Space secrets: `OPENEO_CLIENT_ID`, `OPENEO_CLIENT_SECRET`
406
+ 4. `openeo` library picks them up via `conn.authenticate_oidc()`
407
+
408
+ ### Docker Image Changes
409
+
410
+ | Remove | Add |
411
+ |---|---|
412
+ | `cartopy β‰₯0.22.0` | `openeo β‰₯0.28.0` |
413
+ | `planetary-computer β‰₯1.0.0` | β€” |
414
+ | `stackstac β‰₯0.5.0` | β€” |
415
+ | `pystac-client β‰₯0.7.0` | β€” |
416
+ | Natural Earth shapefile download | β€” |
417
+ | `libproj-dev`, `proj-data` (builder) | β€” |
418
+
419
+ Net effect: Docker image gets smaller (~400MB vs current ~500MB).
420
+
421
+ ## Testing Strategy
422
+
423
+ ### Unit Tests
424
+
425
+ - **openEO graph builders** β€” mock the openEO connection, verify correct process graph structure (bands, temporal extent, spatial extent, resolution)
426
+ - **Raster post-processing** β€” test zonal stats computation, status classification from raster values
427
+ - **Map rendering** β€” test that rasterio-based renderer produces valid PNGs from sample GeoTIFFs
428
+ - **Chart rendering** β€” test monthly data with baseline bands (extends existing tests)
429
+
430
+ ### Integration Tests
431
+
432
+ - **openEO round-trip** β€” submit a minimal NDVI graph to CDSE sandbox, verify GeoTIFF returned (requires CDSE credentials, run manually or in CI with secrets)
433
+ - **End-to-end report** β€” run full job with test AOI, verify PDF contains all new sections
434
+
435
+ ### Test Data
436
+
437
+ Create sample GeoTIFFs (small, synthetic rasters) for unit tests so they run without network access or CDSE credentials.
438
+
439
+ ## Migration Path
440
+
441
+ This is a breaking change to the indicator set. Recommended phased approach:
442
+
443
+ 1. **Phase A** β€” openEO client + NDVI indicator + new map renderer. Proves the full pipeline works end-to-end.
444
+ 2. **Phase B** β€” Migrate remaining indicators (water, LST, rainfall, nightlights) from current sources to openEO.
445
+ 3. **Phase C** β€” Add new indicators (SAR change, built-up) and visual overview pages.
446
+ 4. **Phase D** β€” Remove deprecated code (cartopy, Open-Meteo, Planetary Computer, old indicators).
447
+
448
+ Each phase is independently deployable and testable.
449
+
450
+ ## Out of Scope
451
+
452
+ - Interactive web maps (Leaflet/MapLibre) for indicator results β€” keep static PNG for now
453
+ - Land cover classification (distinguishing crop from forest from grassland)
454
+ - Sub-daily temporal resolution (hourly fire tracking, etc.)
455
+ - Multi-AOI comparison in a single report
456
+ - User-uploaded vector boundaries (shapefile/GeoJSON AOI upload)
457
+ - Automated scheduled monitoring (cron-based re-runs)