DHDRL commited on
Commit
907a236
·
verified ·
1 Parent(s): 6b83fa5

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +84 -746
README.md CHANGED
@@ -1,196 +1,50 @@
1
- # Weather-Risk RL: Agricultural Early-Warning System (Indonesia)
2
-
3
- An RL-based agricultural weather-risk early-warning system: a multi-source
4
- data pipeline (Open-Meteo / ERA5 / satellite / synthetic) feeds a
5
- deterministic, economics-calibrated crop-risk scorer. On top of that scorer
6
- a MaskablePPO + GRU agent learns *which zones to inspect and when to alert*
7
- under a **scarce inspection budget**. Trained via curriculum learning,
8
- exportable to MNN for edge inference (C++/Vulkan) with MQTT product-alert +
9
- hidden-state sync across a node fleet.
10
-
11
- The current focus is a grounded **Indonesia rice deployment**: real zones,
12
- crop calendars, monsoon context, climatology-based anomaly scoring, basin /
13
- heliophysics context, curated **L1 impact labels**, and evaluation with
14
- **three layers**:
15
-
16
- 1. **Proxy skill** — hazard-specific operating points (`drought_*` / `flood_*`)
17
- against precip-percentile proxies (development / ranking).
18
- 2. **Product skill** — idempotent `EMITTED` alerts vs curated L1 impact
19
- labels (what a buyer should quote — **with L1 density caveats**).
20
- 3. **Agent path** — real-trajectory eval of trained policies via relative
21
- belief movement and product flags (`evaluate_checkpoint_real.py`).
22
-
23
- This is a research / portfolio codebase, not a production alerting service.
24
- See **Status**, **Verified evaluation**, **Held-out protocol**, and
25
- **Still open** before quoting.
26
-
27
  ---
28
-
29
- ## Status (what is integrated and verified)
30
-
31
- | Area | State |
32
- |------|--------|
33
- | Schema v3 (`ZoneObs`, `BasinContext`, helio fields) | **Done** |
34
- | Climatology anomalies (precip / temp / soil / RH) for real obs | **Done** — pinned no-look-ahead for backtests |
35
- | RH-anomaly fungi correction (obs + forecast) | **Done** — synthetic remains true no-op |
36
- | Basin context (RONI / DMI) + SWPC helio fetch | **Done** — strict real-only mode available |
37
- | Indonesia zone registry (14 zones) + rice calendars | **Done** |
38
- | Hazard-specific scorecards (`drought_*` / `flood_*`) | **Done** — multi-threshold harness |
39
- | Product `AlertLevel` from drought/flood channels | **Done** — WARNING/CRITICAL fire on real data |
40
- | Threshold freeze across 4 Java zones × 2 seasons (proxy) | **Done** — see proxy tables |
41
- | Continuous skill (matched separations) | **Done** — positive drought + flood ranking |
42
- | **L1 impact label store** (`impact_labels.py` + JSON) | **Done** — curated Java v1 (**7 events**) |
43
- | **Product alert service** (gate + idempotent emit) | **Done** — `product_alert_service.py` + MQTT bus |
44
- | **Product vs L1 metrics in backtest** | **Done** — primary product skill block |
45
- | Helio in env obs (`basin_context` 4→8) | **Done** — SWPC live→vector verified; scorer still no helio term |
46
- | Env `product_actionable` = product gate (not ADVISORY) | **Done** — matches `product_alert_service` |
47
- | **Real-trajectory agent/scorer eval harness** | **Done** — injection fix, PATH_CHECK, relative belief metric |
48
- | Env real-day injection (`options["context"]`) | **Done** — A=B=C scorer product on historical days |
49
- | `decide_checkpoint` path integrity | **Done** — PATH_CHECK: return loop product, rs display-only |
50
- | Relative belief metric (`belief_raised` / Δbelief) | **Done** — null control under zero_inspect is exact at n_zones=1 **and** multi-zone |
51
- | **Unlabeled exclusion from P/R/F1** | **Done** — `gt_source="unlabeled"` outside all L1 spans; not counted as TN/FP |
52
- | **Multi-zone real injection (Blocker B)** | **Done** — `EpisodeContext` per-zone obs/forecast lists; n_zones>1 real eval path |
53
- | Synthetic train calibration (`clean_episode_ratio`, `event_spatial_correlation`) | **Done** — matched train/eval CLI; correlated regional events |
54
- | **Budget modes** (`full` / `scarce` / `triage`) | **Done** — default **triage** forces leaving ≥1 zone unvisited |
55
- | **Per-episode zone-order shuffle** | **Done** — `shuffle_zone_order=True` (default); breaks fixed-slot habits |
56
- | Dynamics pairs from historical cache | **Done** — `build_dynamics_pairs.py` (892 pairs on Java zones) |
57
- | Held-out protocol | **Done** — see **Held-out protocol** section below |
58
- | Offline self-tests (zone_obs, climatology, impact, product, backtest, pytest) | **Passing** |
59
-
60
- Still open items are listed only under **Still open** at the bottom — do not
61
- treat fixed items above as missing.
62
-
63
  ---
64
 
65
- ## Repository layout
66
-
67
- | File | Role |
68
- |------|------|
69
- | `zone_observation.py` | Core schema v3: `ZoneObs`, `BasinContext` (incl. helio), `ForecastResult`, `RiskScore`, `ForecastConfig` (incl. `clean_episode_ratio`, `event_spatial_correlation`, `shuffle_zone_order`). |
70
- | `crop_risk_scorer.py` | Deterministic scoring; RH-anomaly fungi fix; **hazard-specific WARNING/CRITICAL** (`drought_risk` / `flood_risk` thresholds). |
71
- | `era5_data_pipeline.py` | Real fetchers (Open-Meteo, ERA5, SWPC helio, RONI/DMI); synthetic fallback; strict real-only mode. |
72
- | `climatology.py` | Per-zone day-of-year climatology (precip, temp, soil moisture, RH). |
73
- | `indonesia_zones.py` | 14 agricultural zones, rice calendars, monsoon-onset heuristic, planting advisories. |
74
- | `impact_labels.py` | L1 impact event schema + file-backed store; day query; match helpers. |
75
- | `impact_labels_java_v1.json` | Curated Karawang / Indramayu drought & flood spans (provincial/press; **7 events**). |
76
- | `product_alert_service.py` | Product gate (WARNING+ **or** drought≥0.35 / flood≥0.25) + idempotent emit onto `NodeTransport`. |
77
- | `weather_forecast_env.py` | Gymnasium env: multi-zone inspection under budget; real EpisodeContext injection; correlated event plan; **per-episode zone-order shuffle**; `believed_p` = max over active zones. |
78
- | `gru_weather_policy.py` | GRU feature extractor for MaskablePPO. **v3:** invariant pooled path for value/terminate (weight-transfer across `n_zones`) **plus** an equivariant per-zone `zone_action_head` computed *before* pooling, so inspect logits can condition on which zone's content looks risky. Requires constructing `MaskablePPO` with `ZoneEquivariantMaskablePolicy`, not the default `MultiInputPolicy` — the default policy's `action_net` sits on the pooled (identity-erased) features and cannot learn zone selection; see module docstring. |
79
- | `train_curriculum.py` / `train_kaggle.py` | Curriculum / Kaggle training; **`--budget-mode`** (`full`/`scarce`/`triage`); CLI for clean ratio and spatial correlation. |
80
- | `hierarchical_search.py` | Country-scale bisection search (Phase 1 = deterministic gate). |
81
- | `backtest_indonesia.py` | Historical replay: proxy scorecards **and** product-vs-L1 metrics; optional `--emit-product-alerts`. |
82
- | `evaluate_checkpoint_real.py` | Agent/scorer eval on historical pkl + L1; multi-zone packs; relative belief Δ; PATH_CHECK; zero_inspect; unlabeled exclusion. |
83
- | `evaluate_checkpoint.py` | Synthetic matched-distribution eval (belief gate vs product gate; zero_inspect control). |
84
- | `build_dynamics_pairs.py` | Historical cache → DynamicsTrainer (current, next) pairs. |
85
- | `mnn_export.py` / `edge_wrapper.cpp` | ONNX/MNN export and C++/Vulkan edge runtime. |
86
- | `node_transport.py` | Product alerts + hidden-state transport (LocalTransport or MQTT). |
87
- | `Indonesia_upgrade_changelog.md` | Detailed upgrade history. |
88
-
89
- ---
90
-
91
- ## Quickstart
92
-
93
- ```bash
94
- pip install -r requirements.txt
95
-
96
- # Offline self-tests (no network)
97
- python zone_observation.py
98
- python climatology.py
99
- python indonesia_zones.py
100
- python impact_labels.py
101
- python product_alert_service.py # needs zone_observation + node_transport on PYTHONPATH
102
- python backtest_indonesia.py # offline self-test groups
103
- python -m pytest test_*.py
104
-
105
- # Country-scale search demo (synthetic)
106
- python hierarchical_search.py
107
- ```
108
-
109
- ### Reproduce scorecards (network required)
110
-
111
- ```bash
112
- ZONES=karawang_rice,indramayu_rice,central_java_rice,east_java_rice
113
-
114
- # Proxy skill — 2023 El Niño dry (drought-primary)
115
- python backtest_indonesia.py --mode live --forecast-backend baseline \
116
- --zones $ZONES --start 2023-07-01 --end 2023-11-30 \
117
- --min-drought 0.35 --scorecard scorecard_2023_4zone.json
118
-
119
- # Proxy skill — 2021–22 wet (flood-primary)
120
- python backtest_indonesia.py --mode live --forecast-backend baseline \
121
- --zones $ZONES --start 2021-12-01 --end 2022-03-31 \
122
- --min-flood 0.25 --scorecard scorecard_2021_22_4zone.json
123
-
124
- # Product skill vs L1 — dry
125
- python backtest_indonesia.py --mode live \
126
- --zones karawang_rice,indramayu_rice \
127
- --start 2023-07-01 --end 2023-11-30 \
128
- --impact-labels impact_labels_java_v1.json \
129
- --emit-product-alerts \
130
- --out full_2023_dry_l1.json
131
- ```
132
-
133
- `cdsapi not installed` log lines are **harmless** for these runs: the harness
134
- uses Open-Meteo archive + baseline forecast, not CDS/ERA5.
135
 
136
- ### Train under triage pressure (required for allocation skill)
 
 
 
 
137
 
138
- Full-tour training (`max_steps n_zones + 1`) never forces the agent to
139
- **choose which zone to skip**. The visit-once mask plus
140
- `unvisited_zone_penalty` makes "inspect everyone, then terminate" EV-optimal.
141
- Policies trained that way can collapse to a fixed slot (e.g. always the same
142
- zone under true triage eval).
143
 
144
- **Budget modes** (derived `max_steps` unless you override):
145
 
146
- | Mode | `max_steps` | Meaning |
147
- |------|-------------|---------|
148
- | `full` | `n_zones + 1` | Can visit every zone (legacy; does **not** train selection) |
149
- | `scarce` | `n_zones` | Borderline; must skip terminate to visit all |
150
- | **`triage`** (default) | `max(1, n_zones - 1)` | **Must** leave ≥1 zone unvisited |
 
 
151
 
152
- **Zone-order shuffle** (`ForecastConfig.shuffle_zone_order=True` by default)
153
- permutes action slots each episode so index `i` is not permanently bound to
154
- a fixed zone identity. Disable only for fixed-order diagnostic probes
155
- (`shuffle_zone_order=False`). Oracle-style EV probes that index into the
156
- pre-shuffle event plan **must** use `shuffle_zone_order=False` (or map
157
- actions via `info["zone_ids"]`); otherwise “oracle” inspects the wrong slot.
158
 
159
- **Reward path (unified termination + budget-aware unvisited):** Budget
160
- exhaustion and explicit early-terminate both use `_compute_termination_reward`
161
- (belief-driven gain/cost/miss). `unvisited_frac` is relative to available
162
- budget `min(n_zones, max_steps)`, not raw `n_zones` full mode unchanged;
163
- under triage the agent is not half-penalised merely for leaving zones it
164
- was never allowed to visit. Checkpoints trained **before** this change
165
- optimise a different objective; do not compare raw returns across that cut.
166
-
167
- ```bash
168
- # Recommended: 2-zone triage + shuffle (defaults)
169
- python train_kaggle.py \
170
- --out ./run_nz2_triage_shuf \
171
- --n-zones 2 \
172
- --budget-mode triage \
173
- --steps 100000 \
174
- --clean-episode-ratio 0.90 \
175
- --event-spatial-correlation 0.85 \
176
- --device auto
177
-
178
- # Expect log lines like:
179
- # budget-mode=triage → max_steps=1 (full ceiling=3)
180
- # must_skip_zones=yes
181
- ```
182
 
183
- Curriculum phases (`train_curriculum.py`) progress the same way:
184
- `normal` = full → `monsoon` = scarce → later phases = triage.
185
 
186
- **Recipe validated to produce single-dirty selection accuracy well above
187
- chance, and — at sufficient step count — accuracy at 100% across every
188
- hazard type** (see Still open #5 for exact numbers and what's independently
189
- confirmed vs. still-reported-only):
190
 
191
  ```bash
192
  python train_kaggle.py \
193
- --out ./run_nz2_triage_selection \
194
  --n-zones 2 \
195
  --budget-mode triage \
196
  --steps 2400000 \
@@ -200,586 +54,70 @@ python train_kaggle.py \
200
  --device auto
201
  ```
202
 
203
- `--precip-scale` is now a real `train_kaggle.py` flag (default `40.0`,
204
- matching the validated recipe) earlier revisions of this doc noted it
205
- wasn't yet exposed on the CLI; that gap is closed.
 
 
 
206
 
207
- **Step count matters more than anything else tried.** A checkpoint at
208
- 150k steps (`--seed 42`, same `clean`/`rho`) reached ~86% pooled accuracy
209
- with drought/flood already solved but fungi lagging at ~57%. A checkpoint
210
- from a separate, longer run reached 100% on drought and flood by 250k
211
- steps but fungi still only 60–67% at that point — then **100% on all
212
- three hazard types, independently re-verified against the real
213
- `weather_forecast_env.py` + `zone_observation.py` + `crop_risk_scorer.py`
214
- (not a stub), by 2,400,000 steps.** `target_kl` and the belief prior/signal
215
- blend were both floated earlier as candidate fixes for the fungi gap;
216
- neither was needed — it resolved with more steps on the unchanged recipe.
217
- **What's independently confirmed from the 2.4M checkpoint file itself:**
218
- `policy_class=ZoneEquivariantMaskablePolicy`, `precip_scale=40.0`,
219
- `_total_timesteps=5,000,000` (i.e. this was a genuine 5M-step target run,
220
- not a truncated one — a checkpoint saved partway through should not be
221
- read as "the run stopped here"). **Not independently confirmed:** the
222
- exact `clean_episode_ratio`/`event_spatial_correlation` used during this
223
- specific run (SB3 checkpoints don't store env-config kwargs, only
224
- `policy_kwargs`) — the `--clean-episode-ratio 0.80
225
- --event-spatial-correlation 0.50` above is the reported training config,
226
- consistent with the recipe that produced it, but taken on trust rather
227
- than read out of the artifact the way `precip_scale` was. Same for the
228
- often-cited "seed 7": confirmed via the actual training command
229
- (`--seed 7`), which controls `ForecastConfig(seed=args.seed)` for the
230
- train/eval env — so environment-side determinism (event plans, synthetic
231
- obs noise) really was seed 7. The checkpoint's own saved SB3 state still
232
- shows `seed: None`, and that's expected, not a discrepancy:
233
- `build_model()` never passes `seed=` to `MaskablePPO(...)` itself, only
234
- into the env config, so PyTorch's own weight-initialization RNG was never
235
- pinned by `--seed`. Full run reproducibility (including weight init) is
236
- therefore not actually guaranteed by `--seed 7` alone — worth adding
237
- `set_random_seed(args.seed)` or `MaskablePPO(..., seed=args.seed)` to
238
- `train_kaggle.py` if exact reproduction ever matters.
239
 
240
- This differs from the "recommended defaults" block above in the ways that
241
- mattered empirically, not just cosmetically:
242
 
243
- 1. `--event-spatial-correlation 0.50` (not the default 0.85) — raises the
244
- *rate* of single-dirty episodes (the only episodes with a well-defined
245
- correct zone) from ~5% to ~15% of training data. Density alone was
246
- **not** sufficient on its own (see below).
247
- 2. The `MaskablePPO` construction must use `ZoneEquivariantMaskablePolicy`
248
- from `gru_weather_policy.py`, and `GRUWeatherFeaturesExtractor` must be
249
- constructed with `precip_scale=40.0`. Precip lives in roughly `[0, 80]`
250
- while belief lives in roughly `[0, 0.3]`; with no normalization layer,
251
- precip's raw scale can suppress the much smaller but far more reliable
252
- belief signal during optimization, independent of which feature is
253
- actually more informative.
254
- 3. Enough steps. 150k is sufficient for drought/flood but not fungi; 2.4M
255
- is sufficient for all three. The step count between those two points
256
- where fungi specifically crosses over has not been narrowed down.
257
-
258
- Density (`event_spatial_correlation`) and scale-normalization are
259
- independent, both-necessary levers, not interchangeable ones: correlation
260
- alone (ρ=0.50, no normalization, no equivariant policy change) held
261
- selection accuracy at chance across three separate runs before either the
262
- equivariant architecture or the normalization patch existed. Do not assume
263
- lowering ρ alone reproduces this result without the other two changes.
264
 
265
- ### Agent eval on real trajectories
266
 
267
  ```bash
268
- # zero_inspect control (belief Δ must be exactly 0)
269
- python evaluate_checkpoint_real.py \
270
- --pkl historical_continuous_indonesia_v1.pkl \
271
- --impact-labels impact_labels_java_v1.json \
272
- --zones karawang_rice,indramayu_rice \
273
- --start 2023-07-01 --end 2023-11-30 \
274
- --mode zero_inspect --n-zones 2 --max-steps 1 --horizon-days 30
275
-
276
- # Trained triage checkpoint (n_zones and max_steps must match training)
277
- python evaluate_checkpoint_real.py \
278
- --pkl historical_continuous_indonesia_v1.pkl \
279
- --impact-labels impact_labels_java_v1.json \
280
- --zones karawang_rice,indramayu_rice \
281
- --start 2023-07-01 --end 2023-11-30 \
282
- --mode checkpoint --checkpoint ./run_nz2_triage_shuf/final_model.zip \
283
- --n-zones 2 --max-steps 1 --horizon-days 30
284
  ```
285
 
286
- Expect: `PATH_CHECK summary: …`; zero_inspect belief_delta mean = **0.0000**;
287
- checkpoint belief_delta distribution non-zero. Checkpoint `n_zones` must equal
288
- eval `n_zones` or the harness refuses with SHAPE_MISMATCH.
289
-
290
- **Success criteria for triage allocation skill (not full-tour numbers):**
291
-
292
- 1. Zero-inspect: belief_R ≈ 0, Δbelief mean ≈ 0
293
- 2. Checkpoint: belief_R on L1 clearly above zero-inspect **— still open,
294
- see Still open #5; nothing below has been checked against real L1
295
- trajectories, only synthetic single-dirty episodes**
296
- 3. Inspected-zone probe: **not** 100% one fixed zone — met (see Still open
297
- #5); confirmed via raw per-slot action counts staying near 50/50 across
298
- every checkpoint of a full 150k-step trajectory, and independently
299
- reconfirmed at 250k and 2.4M steps on a separate longer run
300
- 4. **Selection accuracy, not just distribution balance.** Criterion 3 above
301
- only rules out a fixed-slot collapse; it does not show the policy picks
302
- the zone that is *actually* dirty. Measure that directly: for
303
- single-dirty episodes (`n_d == 1`, the only case with a well-defined
304
- correct answer), compare the *chosen* zone's own physical dirty flag —
305
- not the marginal frequency of each physical zone_id across "any zone
306
- dirty" episodes. An earlier probe design computed the latter and
307
- reported ~50–55% "top share on dirty" as if it measured selection skill;
308
- under `shuffle_zone_order=True` that quantity is expected to sit near
309
- 50% for a policy with **perfect** selection skill and for one with
310
- **zero** skill alike, since it never checks whether the chosen zone
311
- matches its own dirty flag. Do not reuse that metric shape. See Still
312
- open #5 for the corrected probe and current numbers.
313
- 5. Prefer dirty-day / hazard-conditional metrics over overall return —
314
- overall return still blends clean-day always-inspect economics with
315
- whatever selection skill exists; report criterion 4's number
316
- hazard-by-hazard (see Still open #5), not as a single blended figure.
317
-
318
- ---
319
-
320
- ## Verified evaluation
321
-
322
- Three layers. **Quote product-vs-L1 only with L1 density caveats.** Quote
323
- hazard-specific proxy bars for ranking / threshold development. Quote agent
324
- belief results only with the relative metric, null control, and the correct
325
- **budget regime**. Do **not** cite ADVISORY / raw max_risk rates as product
326
- skill. Do **not** cite multi-zone product R without the 2-zone ablation
327
- below. Do **not** cite full-tour belief_R as evidence of zone-selection skill.
328
-
329
- ### A. Product skill vs L1 impact labels
330
-
331
- **Method:** Curated L1 events in `impact_labels_java_v1.json` (provincial
332
- agriculture reports + national press; confidence < 1.0). Product positive =
333
- gate: `AlertLevel ≥ WARNING` **or** `drought_risk ≥ 0.35` **or**
334
- `flood_risk ≥ 0.25`. Days outside every L1 span are `gt_source="unlabeled"`
335
- and are **excluded from P/R/F1** (reported only as `unlabeled_alert_rate`).
336
-
337
- **L1 catalog (v1, 7 events):**
338
-
339
- | Zone | Hazard | Span | Conf | Notes |
340
- |------|--------|------|------|--------|
341
- | karawang_rice | flood | 2023-02-25 → 03-08 | 0.90 | ~8.9k ha puso |
342
- | karawang_rice | drought | 2023-08-01 → 10-22 | 0.85 | El Niño dry — **only Karawang drought L1 in 2023** |
343
- | indramayu_rice | flood | 2021-02-10 → 02-28 | 0.85 | Cipanas / multi-kecamatan |
344
- | indramayu_rice | drought (peak) | 2023-06-15 → 08-31 | 0.90 | Primary dry L1 |
345
- | indramayu_rice | drought (secondary) | 2023-09-01 → 10-15 | 0.80 | Post-peak stress |
346
- | + 2 seasonal flood rows | flood | 2021-12 → 2022-01 | 0.50–0.55 | Exploratory only |
347
-
348
- **Important:** "L1 event days" in evals are **range expansions** of these
349
- rows, not independent confirmations. Example: 2023-07–11 dry window reports
350
- ~37–63 L1 days from a handful of spans.
351
-
352
- #### Dry product freeze (live backtest)
353
-
354
- **2023-07-01 → 2023-11-30**, zones `karawang_rice,indramayu_rice`, step 3d
355
- (n=102):
356
-
357
- | Metric | Value |
358
- |--------|--------|
359
- | L1 event days (expanded) | 63 (62% coverage) |
360
- | Emit rate | 56% |
361
- | **TP / FP / FN / TN** | **53 / 4 / 10 / 35** |
362
- | **Precision** | **0.930** |
363
- | **Recall** | **0.841** |
364
- | **F1** | **0.883** |
365
-
366
- #### L1 sparsity caveat (pkl scorer_oracle, same window)
367
-
368
- Scorer product vs L1 on the continuous historical cache (per-zone days,
369
- before unlabeled exclusion was the default reporting mode):
370
-
371
- | Metric | Value |
372
- |--------|--------|
373
- | P / R / F1 (legacy framing that counted boundary days as negatives) | 0.750 / 0.730 / 0.740 |
374
- | Indramayu | P=1.00 R=0.81 |
375
- | **Karawang** | **P=0.53 R=0.63** (FP-heavy under legacy framing) |
376
-
377
- **Karawang high-risk weeks outside L1 (do not treat as proven FPs):**
378
-
379
- - **Jul 15–30** — drought ≈ 0.43–0.44, **before** L1 onset (Aug 1)
380
- - **Oct 28 – Nov 17** — flood ≈ 0.29–0.43, **after** L1 end; **no Karawang flood L1 in 2023**
381
-
382
- These are **unlabeled / boundary days**. With unlabeled exclusion they no
383
- longer enter FP/TN counts. Do not quote Karawang P≈0.53 as settled
384
- false-alarm rate until those weeks are labeled or confirmed non-events.
385
-
386
- #### Wet product skill (limited by L1 density)
387
-
388
- **2021-12-01 → 2022-03-31** (n=82) — sparse lower-confidence flood L1:
389
-
390
- | P / R / F1 | 0.417 / 0.476 / 0.444 |
391
-
392
- **2021-02-01 → 2021-03-15** (n=30) — high-conf Indramayu flood, thin L1:
393
-
394
- | P / R / F1 | 0.250 / 1.000 / 0.400 |
395
-
396
- Flood product recall on labeled days can be high while precision is poor
397
- when L1 is thin. Proxy flood skill remains the better development signal
398
- until denser flood impact labels exist.
399
-
400
- ### B. Proxy skill (hazard-specific, precip percentiles)
401
-
402
- **Method:** pinned Open-Meteo climatology (no look-ahead); event days from
403
- precip percentiles (drought ≈ 30d below ~20th pct; flood ≈ 7d above ~90th
404
- pct). **Not** disaster records.
405
-
406
- #### Rejected as product bars
407
-
408
- | Rule | Result |
409
- |------|--------|
410
- | `AlertLevel.ADVISORY` (~max_risk ≥ 0.0625) | Saturates (~100% of days); TN → 0 |
411
- | Legacy WARNING via max_risk ≥ 0.65 | Never fired under confidence damping — **superseded by hazard path** |
412
- | Single max_risk mid-cut (e.g. 0.35) | OK in drought seasons; **collapses in wet** when fungi saturates max_risk |
413
 
414
- #### Hazard-specific skill (frozen thresholds)
415
-
416
- **Dry: 2023-07-01 → 2023-11-30**
417
-
418
- | Scope | Policy | P | R | F1 | Matched drought sep |
419
- |-------|--------|------|------|------|---------------------|
420
- | 2-zone | drought_0.35 | 1.00 | 0.86 | 0.93 | +0.33 |
421
- | **4-zone** | **drought_0.35** | **0.94** | **0.89** | **0.92** | **+0.30** |
422
-
423
- 4-zone dry alert mix (n=204): advisory 64 / **warning 90** / **critical 50**.
424
-
425
- **Wet: 2021-12-01 → 2022-03-31**
426
-
427
- | Scope | Policy | P | R | F1 | EV lift vs always | Matched flood sep |
428
- |-------|--------|------|------|------|-------------------|-------------------|
429
- | 2-zone | flood_0.25 | 0.61 | 0.93 | 0.74 | **+860** | +0.18 |
430
- | **4-zone** | **flood_0.25** | **0.52** | **0.91** | **0.66** | **+1160** | **+0.17** |
431
-
432
- Flood precision is **zone-dependent** (Indramayu strong; Karawang / East Java
433
- more FPs — also seen on live proxy backtests). Ranking skill and EV lift
434
- hold; do not re-tune the global cut from pooled P alone.
435
-
436
- ### C. Agent path on real trajectories
437
-
438
- **Harness integrity (validated):**
439
-
440
- | Check | Result |
441
- |--------|--------|
442
- | Env injection (real days, not synthetic redraws) | Scorer product identical for scorer_oracle / zero_inspect / checkpoint on the same days |
443
- | `decide_checkpoint` overwrite | **PATH_CHECK**: return loop product; rs is display-only |
444
- | Relative belief null control | zero_inspect: Δbelief mean=**0.0000**, std=**0** at n_zones=1 **and** multi-zone |
445
- | SHAPE_MISMATCH guard | Checkpoint `n_zones` must match env; wrong shapes refused |
446
- | Multi-zone joint metrics | Order-invariant under zone shuffle (max / OR aggregations) |
447
-
448
- **Policy-sensitive metric:** `belief_raised = (terminal − initial) > 0.02`
449
- relative to each episode's post-reset belief. Fixed bars against `prior` /
450
- `rational` saturate and must not be used for agent skill.
451
-
452
- **Product is policy-insensitive by design** on real multi-zone packs: agent
453
- and zero_inspect share the same product row (scorer on episode obs). Skill
454
- signal is belief movement **and**, under triage, which zone was inspected.
455
-
456
- #### Full-tour vs triage (read this before quoting agent skill)
457
-
458
- | Regime | `max_steps` (n_zones=2) | What it measures |
459
- |--------|-------------------------|------------------|
460
- | **Full tour** | ≥ 3 | Can visit every zone; does **not** force selection |
461
- | **Triage** | 1 | Must pick **one** zone (or terminate with none) |
462
-
463
- Older matched trains (`run_nz2_c090`, `run_nz3_c090`) used full-tour budgets.
464
- They remain valid **harness and belief-movement** baselines. They are **not**
465
- evidence that the policy learned risk-conditioned zone allocation. Under true
466
- triage (`max_steps=1`), a full-tour checkpoint was observed to inspect a
467
- **fixed** zone on every day (slot/index bias). New allocation claims require a
468
- **triage-trained** checkpoint under `shuffle_zone_order=True`.
469
-
470
- #### Multi-zone product controls (2023-07-01 → 2023-11-30, null-fixed harness)
471
-
472
- Joint packs group one calendar day across selected zones. Product uses
473
- worst-case across zones.
474
-
475
- | Setting | product R | unlabeled_alert_rate | Notes |
476
- |---------|-----------|----------------------|--------|
477
- | **3-zone** (Karawang+Indramayu+Central Java) | 1.000 | 0.875 | Central Java has **zero L1** in catalog |
478
- | **2-zone** (Karawang+Indramayu only) | **0.773** | **0.625** | Ablation drops Central Java |
479
-
480
- **Central Java ablation (confirmed):** Removing Central Java cuts joint
481
- product R 1.000 → 0.773 and unlabeled alert rate 0.875 → 0.625. Central Java
482
- has no L1 coverage and independently elevates on a large fraction of days;
483
- under `max` aggregation it carried the joint gate. Do **not** quote
484
- multi-zone product R=1.0 as architecture skill.
485
-
486
- #### 2-zone full-tour belief baseline (matched train: clean=0.90, ρ=0.85)
487
-
488
- These numbers are from **full-tour** eval (`max_steps ≈ n_zones+1`). Useful
489
- as harness + belief-movement evidence; **not** triage allocation skill.
490
-
491
- | Metric | zero_inspect | full-tour checkpoint |
492
- |--------|--------------|----------------------|
493
- | mean Δbelief | **0.0000** | +0.075 |
494
- | belief_recall (L1 days, Δ>0.02) | 0.000 | **0.818** (18/22) |
495
- | belief_unlabeled_raise_rate | 0.000 | **1.000** (8/8) |
496
- | mean Δ \| L1 | 0 | +0.067 |
497
- | mean Δ \| unlabeled | 0 | +0.097 |
498
-
499
- **Day-level pattern (locked):** L1 misses = late drought-span low contemporaneous
500
- risk (product also off). Unlabeled raises = post–Oct 22 flood-looking signal
501
- with no Karawang flood L1 — **catalog geometry**, not pure over-raise habit.
502
- Do not silently promote Oct 23–Nov 27 into L1 without official/satellite
503
- confirmation.
504
-
505
- #### Earlier n_zones=1 smoke (harness history only)
506
-
507
- First path validation used a 50k-step synthetic-only smoke checkpoint.
508
- Useful only for harness proofs. Do **not** quote smoke belief F1 as skill.
509
-
510
- ---
511
-
512
- ## Held-out protocol
513
-
514
- **Status:** required before external quotation of product P/R/F1.
515
- Thresholds in `crop_risk_scorer.py` / `ProductGateConfig` stay frozen at the
516
- documented defaults (drought 0.35 / flood 0.25 WARNING bars).
517
-
518
- ### Problem
519
-
520
- Dry (2023-07→11) and wet (2021-12→2022-03) windows were used both to
521
- **design** hazard thresholds and to **report** scorecards. Those numbers are
522
- in-sample relative to threshold selection.
523
-
524
- ### Freeze rule
525
-
526
- 1. Do not change product WARNING/CRITICAL bars from held-out alone.
527
- 2. A held-out season/zone combination must not have been used for threshold
528
- search or fungi reweighting experiments.
529
- 3. The window must **exist in the evaluation cache**
530
- (`historical_continuous_indonesia_v1.pkl`). Do not recommend years with
531
- zero trajectories.
532
-
533
- ### Cache coverage (Karawang / Indramayu)
534
-
535
- | Span | Regime |
536
- |------|--------|
537
- | 2015-05 → 2016-04 | el_nino_very_strong |
538
- | 2017-05 → 2018-04 | **neutral** |
539
- | 2018-06 → 2019-05 | el_nino_moderate (ends May 2019) |
540
- | 2020-09 → 2023-04 | La Niña series |
541
- | 2023-05 → 2024-04 | el_nino_strong (**design window**) |
542
-
543
- There is **no** full 2019-06→11 trajectory. That window is **invalid** for
544
- this cache.
545
-
546
- ### Canonical held-out windows
547
-
548
- **A — Neutral (primary)**
549
-
550
- | Field | Value |
551
  |-------|--------|
552
- | Window | **2017-06-01 2017-11-30** |
553
- | Zones | `karawang_rice,indramayu_rice` |
554
- | Why | Outside design windows; present in cache; quiet/neutral ENSO |
555
- | L1 v1 | **No 2017 events** |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
 
557
- **Metric rule when L1 is empty for the window:**
558
-
559
- - Do **not** report F1 / recall (undefined with zero event days).
560
- - Report **emit rate** = product_alerts / n_days.
561
- - Report **specificity** only with the explicit caveat
562
- "no L1 row ⇒ treated as non-event."
563
- - Example (scorer_oracle, 2017-06→11, n=72): emit rate **~65%** — gate stays
564
- hot in a neutral unlabeled season.
565
-
566
- **B — Moderate El Niño (secondary)**
567
-
568
- | Field | Value |
569
- |-------|--------|
570
- | Window | **2018-07-01 → 2018-11-30** |
571
- | Notes | In-cache moderate EN; still outside 2023 design dry window |
572
-
573
- ```bash
574
- # Quiet-season emit-rate check (empty L1 — not F1)
575
- python evaluate_checkpoint_real.py \
576
- --pkl historical_continuous_indonesia_v1.pkl \
577
- --impact-labels impact_labels_java_v1.json \
578
- --zones karawang_rice,indramayu_rice \
579
- --start 2017-06-01 --end 2017-11-30 \
580
- --mode scorer_oracle \
581
- --out eval_heldout_2017_neutral.json
582
  ```
583
 
584
- Do **not** retune thresholds from held-out alone. If emit rate is
585
- unacceptable, document it; any new freeze needs design-window recall
586
- preserved.
587
-
588
- Agent held-out runs must use the same `n_zones` and budget (`max_steps` /
589
- `--budget-mode`) as the checkpoint under test.
590
-
591
- ### What remains in-sample (documented)
592
-
593
- - 2023-07→11 dry product freeze — design window
594
- - 2021-12→2022-03 wet product — design window
595
- - 4-zone proxy scorecards on those same seasons
596
-
597
- Quote held-out **emit rate / specificity** for quiet seasons without labels.
598
- Quote product-vs-L1 P/R only on windows with curated L1 events.
599
-
600
- ---
601
-
602
- ## Design notes (integrated behaviour)
603
-
604
- - **RH / fungi fix:** Obs + forecast fungi terms get a bounded additive
605
- correction from `rh_anomaly_idx`. Synthetic data is a true no-op.
606
- - **Basin / helio:** `BasinContext` carries ENSO (RONI), IOD (DMI), and
607
- SWPC solar wind / Kp / GOES X-ray / regime when feeds are up.
608
- `ForecastConfig.require_real_basin_context` enables fail-closed mode.
609
- Scorer does **not** consume helio; benefit path is RL observation → policy.
610
- - **Confidence damping:** Components scaled by `0.5 + 0.5 * confidence`
611
- (~0.63 on many Open-Meteo archive days → scores in ~0.06–0.55). Hazard
612
- path is the product bar, not max_risk ≥ 0.65.
613
- - **L1 vs proxy:** Proxy for ranking and threshold design. Product claims
614
- prefer product-vs-L1 **only where L1 density is honest**. Days outside
615
- every span are `unlabeled` and excluded from confusion-matrix P/R/F1.
616
- - **Real-eval injection:** `EpisodeContext` carries per-zone obs/forecast
617
- lists for multi-zone packs. Checkpoint `n_zones` must match env shapes
618
- (SHAPE_MISMATCH otherwise). Belief aggregates as **max** over active zones.
619
- - **Budget modes:** Default **triage** so training cannot pass without
620
- learning to leave at least one zone unvisited. Full tour remains available
621
- for curriculum phase 1 (`normal`) only.
622
- - **Zone-order shuffle:** Default on. Action index is not a stable identity.
623
- Multi-zone product/belief metrics use order-invariant aggregations, so
624
- shuffle does not corrupt joint P/R. Per-zone argmax logging (if added later)
625
- must read `info["zone_ids"]` after shuffle, not assume CLI order.
626
- - **Synthetic calibration:** `clean_episode_ratio` (default 0.90) and
627
- `event_spatial_correlation` (default 0.85) produce regional co-occurrence
628
- of dirty zones. Train and synthetic eval must pass the same flags. Note:
629
- these are the CLI/config *defaults*, not the configuration that produced
630
- the triage-selection result in Still open #5 — that result used
631
- `clean_episode_ratio=0.80`, `event_spatial_correlation=0.50`. Lower
632
- `event_spatial_correlation` trades regional co-occurrence realism for a
633
- higher rate of single-dirty episodes (the only episodes with a
634
- well-defined correct zone for training signal); it was necessary but not
635
- sufficient on its own — see the triage-selection recipe block under
636
- Quickstart for what else was required.
637
- - **Feature scale imbalance:** `forecast_precip` (~[0, 80]) and
638
- `zone_belief` (~[0, 0.3]) enter `GRUWeatherFeaturesExtractor` with no
639
- normalization layer between them and the per-zone encoder. Left
640
- unnormalized, precip's much larger raw magnitude can dominate the
641
- smaller but more reliable belief signal during optimization, independent
642
- of information content. `precip_scale` divides precip before the encoder
643
- sees it (constructor kwarg and --precip-scale on traing_kaggle.py; default
644
- `40.0`). Confirmed working, not confirmed optimal.
645
- - **Economics:** Default `miss_penalty` (200) is 10× `false_alert_penalty`
646
- (20). In high base-rate drought seasons, "always" is hard to beat on pure
647
- EV; report ranking separation, cost-loss under stated weights, and
648
- product-vs-L1 with density caveats.
649
- - **Backtest data path:** Open-Meteo archive + baseline forecast. Missing
650
- `cdsapi` only affects optional ERA5 pulls.
651
-
652
- ---
653
-
654
- ## Still open (honest residual gaps)
655
-
656
- Only items that are **not** done. Order matches recommended work sequence:
657
-
658
- 1. **L1 density / boundary confirmation (highest leverage for product claims)**
659
- Unlabeled exclusion is implemented. Still needed: official or satellite
660
- confirmation for Karawang **Jul onset** (pre Aug 1 drought scores) and
661
- **post–Oct 22 flood-looking weeks** (no Karawang flood L1 in 2023). Do
662
- not silently promote those days into L1.
663
- 2. **Official impact catalogs** — L1 v1 is curated provincial/press
664
- (**7 events**), not bulk BNPB / BMKG / EM-DAT / AUTP claims. Zones with
665
- zero L1 coverage (e.g. Central Java in current catalog) should not be
666
- blended into the same unlabeled bucket as boundary days without a distinct
667
- `no_catalog` (or similar) tag when reporting.
668
- 3. **Flood product precision** — sparse L1 + high wet emit rate; denser
669
- flood labels and/or wet-season gate calibration.
670
- 4. **Karawang scorer/climatology** — cross-pipeline FP pattern on proxy
671
- backtests remains open as a possible zone-specific miscalibration after
672
- label-boundary effects are accounted for.
673
- 5. **Agent triage / allocation skill** — harness and full-tour belief path
674
- validated; budget modes + shuffle + unified terminal reward are
675
- integrated. Split by sub-claim:
676
-
677
- **Resolved (synthetic, corrected metric, independently re-verified):**
678
- A checkpoint at 2,400,000 training steps (`checkpoint_2400000.zip`,
679
- `_total_timesteps=5,000,000` recorded in the checkpoint's own saved
680
- state — the run's target was genuinely 5M, not a truncated run;
681
- `policy_class=ZoneEquivariantMaskablePolicy`,
682
- `features_extractor_kwargs.precip_scale=40.0`, both confirmed from the
683
- checkpoint file itself, not just claimed) reaches **100% single-dirty
684
- selection accuracy across all three hazard types** — drought, flood,
685
- *and* fungi — replicated across two independent seed blocks at
686
- `event_spatial_correlation=0.50` (n=116 and n=107) and confirmed again
687
- at `event_spatial_correlation=0.85` (n=45), for n=268 combined
688
- single-dirty episodes with zero errors. This was re-run end-to-end
689
- against the real `weather_forecast_env.py` + `zone_observation.py` +
690
- the real `crop_risk_scorer.py` (not a stub), using the corrected probe
691
- (Success criteria #4) — not inferred from a pasted summary.
692
-
693
- **Fungi needs more steps, not a different approach.** An earlier
694
- checkpoint from the same longer run (`best_model.zip`, 250,000 steps)
695
- shows drought/flood already at 100% but fungi lagging at 60–67% across
696
- the same seed blocks — matching the ~57% fungi result seen on a
697
- separate, shorter (150k-step) run. Fungi selection was never a capped
698
- or fundamentally harder capability; it simply converges later than
699
- drought/flood (roughly 250k insufficient, 2.4M sufficient). Do not
700
- quote a single blended accuracy figure without noting this — but as of
701
- the 2.4M checkpoint, the blended figure and the per-hazard figures are
702
- the same number (100%), so this caveat is now about interpreting
703
- *earlier* checkpoints correctly, not about the recommended artifact.
704
-
705
- **Still open:**
706
- - Real-trajectory evaluation: **no** checkpoint from this line of work
707
- has been run through `evaluate_checkpoint_real.py` against real L1
708
- labels. Everything above, including the 2.4M result, is synthetic
709
- (`_episode_event_plan` / `zone_observation` synthetic configs).
710
- Criterion 2 in Success criteria above remains unmet.
711
- - Training seed: confirmed as `--seed 7` via the actual training
712
- command, which controls the environment's episode generation
713
- (`ForecastConfig(seed=args.seed)`). The checkpoint's own saved SB3
714
- state shows `seed: None`, which is expected rather than
715
- contradictory: `train_kaggle.py`'s `build_model()` never passes
716
- `seed=` to `MaskablePPO(...)`, so PyTorch's weight-init RNG was never
717
- pinned by `--seed`. Env-side determinism for this run is confirmed;
718
- full bit-for-bit run reproducibility (including weight init) is not.
719
- Multi-seed replication (does a *different* `--seed` value reach the
720
- same 100%/100%/100% result on a similar step-count schedule) is
721
- still open regardless.
722
- - The full 5M-step run's final checkpoint (if one exists beyond
723
- 2,400,000) has not been checked — 2.4M already hits 100% on every
724
- measured axis, so this is a lower-priority completeness check, not a
725
- blocker.
726
- - `target_kl` and the `zone_belief` prior/signal blend (currently
727
- `0.7*prior + 0.3*signal`) were identified earlier as candidate levers
728
- for improving past a lower ceiling — that ceiling turned out to be a
729
- step-count issue, not a levers issue, for this recipe. Neither was
730
- needed and neither has been tried; do not assume either is required.
731
-
732
- Interpret overall return cautiously across the reward-path change
733
- (unified budget-exhaustion terminal reward, see Design notes).
734
- 6. **Held-out product / emit numbers** — protocol above; never-touched
735
- window not yet scored under updated L1 + unlabeled rules for a triage
736
- agent.
737
- 7. **Dyna on real pairs** — pairs buildable (`build_dynamics_pairs.py`);
738
- full DynamicsTrainer + surprise vs task-reward logging still needs a
739
- curriculum run with dynamics on.
740
- 8. **`hierarchical_search` Phase 2** — RL-driven adaptive recursion still a stub.
741
- 9. **MNN / C++ edge** — not compiled against a real MNN build here.
742
- 10. **Not a production SLA system** — no on-call, no guaranteed feed uptime,
743
- no regulatory certification.
744
-
745
- **Closed since earlier README revisions (do not re-list as open):**
746
-
747
- - Multi-zone real injection (Blocker B) — per-zone lists + real eval path.
748
- - Belief null control at multi-zone — exact zero under zero_inspect after
749
- initial/terminal aggregation fix (`believed_p` = max over active).
750
- - Unlabeled exclusion from confusion-matrix P/R/F1.
751
- - Matched synthetic train/eval via `clean_episode_ratio` /
752
- `event_spatial_correlation` CLI.
753
- - Central Java product-gate carry confirmed by 3-zone vs 2-zone ablation.
754
- - 2-zone day-level belief pattern: L1 misses = late drought-span low risk;
755
- unlabeled raises = post-span flood signal (catalog geometry).
756
- - Budget modes (`full` / `scarce` / `triage`) and default triage pressure.
757
- - Per-episode zone-order shuffle (anti–index-bias).
758
- - Unified terminal reward: budget exhaustion uses the same
759
- `_compute_termination_reward` as early-terminate (belief-driven).
760
- - Budget-aware `unvisited_frac` (`min(n_zones, max_steps)`), so triage is
761
- not half-penalised for zones outside the available budget.
762
- - Zone-equivariant policy architecture (`ZoneEquivariantMaskablePolicy`):
763
- fixes a real bug where the previous pooled/invariant-only extractor made
764
- the action head structurally unable to condition on which zone's content
765
- looked risky (confirmed via a swap test: two independently trained
766
- checkpoints produced bit-identical per-seed zone assignments, which is
767
- only possible if physical zone choice depends on the env's own shuffle
768
- and not on the policy reading content). Do **not** re-report "always
769
- picks one action index regardless of shuffle" as a live bug — it was a
770
- real bug, and it's fixed, though selection *accuracy* is a separate,
771
- still-partially-open claim (see Still open #5).
772
- - Selection-accuracy probe methodology: the original "top share on dirty"
773
- metric (marginal frequency of each physical zone_id, restricted to
774
- episodes where *any* zone was dirty) cannot distinguish a policy with
775
- real selection skill from one with none, under `shuffle_zone_order=True`
776
- — it never compares the *chosen* zone's own dirty flag against ground
777
- truth. Corrected probe: restrict to single-dirty episodes and check
778
- whether the chosen zone matches its own dirty flag (see Success criteria
779
- #4). Do not reintroduce the old metric shape in future evaluation code.
780
-
781
- ---
782
 
783
  ## License
784
 
785
- AGPL-3.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ license: agpl-3.0
3
+ library_name: stable-baselines3
4
+ tags:
5
+ - deep-reinforcement-learning
6
+ - agricultural-ai
7
+ - weather-modelling
8
+ - curriculum-learning
9
+ - edge-ai
10
+ pipeline_tag: reinforcement-learning
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  ---
12
 
13
+ # Agricultural Weather-Risk RL — MaskablePPO + Zone-Equivariant GRU
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ Budget-constrained multi-zone weather-risk inspection for Indonesian rice
16
+ zones. A Gymnasium environment tracks per-zone beliefs under a scarce
17
+ inspection budget. A zone-equivariant GRU policy supports curriculum phases
18
+ with changing `n_zones`. Deterministic crop-risk scoring grounds rewards
19
+ and product alerts. An ONNX → MNN path is provided for edge inference.
20
 
21
+ Research codebase, not a production alerting service.
 
 
 
 
22
 
23
+ ## What is included
24
 
25
+ - `weather_forecast_env.py` Gymnasium env (belief map, action masks, triage/scarce/full budget modes, zone-order shuffle, real EpisodeContext injection)
26
+ - `gru_weather_policy.py` — Zone-equivariant GRU extractor (`ZoneEquivariantMaskablePolicy`; shared per-zone weights; precip input scaling, `precip_scale=40.0`)
27
+ - `train_curriculum.py` / `train_kaggle.py` Curriculum and standalone training (`--budget-mode triage|scarce|full`, `--precip-scale`)
28
+ - `crop_risk_scorer.py` Deterministic, economics-calibrated risk scoring
29
+ - `climatology.py` + `indonesia_zones.py` Per-zone climatology and Indonesian agricultural zones
30
+ - `evaluate_checkpoint_real.py` — Real-trajectory eval (L1 labels, belief Δ, zero_inspect control)
31
+ - `mnn_export.py` + `edge_wrapper.cpp` — ONNX/MNN export and C++ edge runtime with external GRU hidden state
32
 
33
+ ## Architecture note
 
 
 
 
 
34
 
35
+ Inspect logits are scored **per zone before pooling** (`ZoneEquivariantMaskablePolicy`).
36
+ The value path stays permutation-invariant so most weights transfer across
37
+ `n_zones`. Full-tour episode length is not zone-selection skill; allocation
38
+ under triage is evaluated with `max_steps < n_zones` and per-episode zone-order
39
+ shuffle.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ ## How to use
 
42
 
43
+ ### Train (triage, recommended recipe)
 
 
 
44
 
45
  ```bash
46
  python train_kaggle.py \
47
+ --out ./run_nz2_triage \
48
  --n-zones 2 \
49
  --budget-mode triage \
50
  --steps 2400000 \
 
54
  --device auto
55
  ```
56
 
57
+ Requires `ZoneEquivariantMaskablePolicy` from `gru_weather_policy.py` (not
58
+ the string `"MultiInputPolicy"`). `--precip-scale` is a real CLI flag
59
+ (default `40.0`, matching the validated recipe). **Step count matters**:
60
+ 150k steps is enough for drought/flood selection but not fungi; fungi
61
+ selection was confirmed solved by 2.4M steps on the same recipe — see
62
+ Validation status.
63
 
64
+ ### Load
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
66
+ ```python
67
+ from sb3_contrib import MaskablePPO
68
 
69
+ model = MaskablePPO.load("run_nz2_triage/final_model.zip")
70
+ action, _ = model.predict(obs, action_masks=masks, deterministic=True)
71
+ ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ ### Edge export
74
 
75
  ```bash
76
+ python mnn_export.py \
77
+ --checkpoint run_nz2_triage/final_model.zip \
78
+ --output weather_rl_model.mnn \
79
+ --quantize int8 \
80
+ --n-zones 2
 
 
 
 
 
 
 
 
 
 
 
81
  ```
82
 
83
+ ## Validation status
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
+ | Claim | Status |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  |-------|--------|
87
+ | Weight transfer across `n_zones` changes | Supported by design |
88
+ | Zone-order shuffle avoids fixed-slot lock-in | Observed, confirmed at 250k and 2.4M steps (raw per-slot action counts stay ~50/50) |
89
+ | Risk-conditioned zone choice — drought (synthetic, single-dirty) | **Confirmed 100%** at 2.4M steps (n=76 combined, two seed blocks + ρ=0.85 transfer) |
90
+ | Risk-conditioned zone choice — flood (synthetic, single-dirty) | **Confirmed 100%** at 2.4M steps (n=97 combined, two seed blocks + ρ=0.85 transfer) |
91
+ | Risk-conditioned zone choice — fungi/disease pressure (synthetic, single-dirty) | **Confirmed 100%** at 2.4M steps (n=95 combined, two seed blocks + ρ=0.85 transfer) — lagged at ~57–67% through 250k on the same run; needed more steps, not a different mechanism |
92
+ | Triage agent evaluated against real L1 trajectories | Open — all accuracy figures above are synthetic; `evaluate_checkpoint_real.py` has not yet been run on a triage-trained checkpoint |
93
+ | Product skill vs curated L1 labels | Scorer/product path available; L1 density limits apply |
94
+ | Multi-seed replication of the triage recipe | Open — the 2.4M result is one training run (`--seed 7`, confirmed via the training command; this pins the environment's episode generation but not PyTorch's weight-init RNG, which `train_kaggle.py` doesn't currently seed separately). A second seed reaching the same result has not been tried. |
95
+ | Held-out real product / emit metrics | Open |
96
+
97
+ ## Limitations
98
+
99
+ - Research stack only: no on-call, feed SLA, or regulatory certification.
100
+ - Training is largely synthetic for the moment.
101
+ - Full-tour belief or episode-length metrics are not zone-selection skill.
102
+ - All zone-selection accuracy figures to date are from synthetic episodes;
103
+ the triage-trained policy has not been evaluated against real L1
104
+ trajectories (see Validation status).
105
+ - The 2.4M-step selection-accuracy result comes from a single training run;
106
+ not yet replicated on a second seed.
107
+ - MNN/C++ path is export-oriented, not a validated production edge deployment.
108
+
109
+ ## Dependencies
110
 
111
+ ```
112
+ torch>=2.0
113
+ gymnasium>=0.29
114
+ stable-baselines3>=2.0
115
+ sb3-contrib>=2.0
116
+ numpy>=1.24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
117
  ```
118
 
119
+ While GPU is recommended for training, CPUs can be used successfully.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
 
121
  ## License
122
 
123
+ AGPL-3.0. See repository for full terms.