Reinforcement Learning
stable-baselines3
deep-reinforcement-learning
agricultural-ai
weather-modelling
curriculum-learning
edge-ai
Instructions to use DHDRL/monsoon-rl with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- stable-baselines3
How to use DHDRL/monsoon-rl with stable-baselines3:
from huggingface_sb3 import load_from_hub checkpoint = load_from_hub( repo_id="DHDRL/monsoon-rl", filename="{MODEL FILENAME}.zip", ) - Notebooks
- Google Colab
- Kaggle
Update README.md
Browse files
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 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 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 |
-
#
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
| 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 |
-
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
|
|
|
|
|
|
| 151 |
|
| 152 |
-
|
| 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 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 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 |
-
|
| 184 |
-
`normal` = full → `monsoon` = scarce → later phases = triage.
|
| 185 |
|
| 186 |
-
|
| 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 ./
|
| 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 |
-
`
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
-
|
| 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 |
-
|
| 241 |
-
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 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 |
-
###
|
| 266 |
|
| 267 |
```bash
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
--
|
| 271 |
-
--
|
| 272 |
-
--zones
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
|
| 553 |
-
|
|
| 554 |
-
|
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
-
|
| 561 |
-
-
|
| 562 |
-
|
| 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 |
-
|
| 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.
|