Step 1 · The question
Will this river cross its flood line in the next two days?
For a river gauge on a given evening, AERIS gives one number: the chance that the river, now below its flood line, rises above it tomorrow or the day after. It has been run on past days only, for up to 190 gauges.
Why this way
- The flood line is the 2-year flood flow: the middle value of the yearly peaks at that gauge, worked out from the training years only. Each river gets its own line, and it is crossed about once every two years, often enough to learn from.
- We forecast the crossing, not "is it in flood". A river already in flood is easy to call. The useful warning comes before the rise.
- Two days is the main target fixed in the protocol. One and three days were built too; 3 to 7 days is on the road map.
- A probability, not yes or no, so whoever acts on it can weigh the chance against the cost of acting.
From the project
- Gauges
- 190
- Forecast window
- day 1–2
- Flood line
- 2-yr flow
Source: src/aeris/labels/exceedance.py · reports/v2_report_draft.md §1 · runs/fresh_wris/
What to say
"Our system answers one question for each river gauge, every evening: will the river go above its flood line in the next two days? Here is a real example from September 2024, a year the model never saw. The river was low, it rained 127 millimetres, the model said 60 percent, and the next day the river was at more than three times its flood flow."
Step 2 · Where
190 river gauges in 15 basins of Peninsular India
Each dot is a Central Water Commission gauge with at least ten years of usable daily flow records in the training period. Thirteen basins were used to build the model. Two whole basins, the Krishna and the east-flowing rivers south of the Pennar, were kept out to test it on rivers it had never seen.
Why this way
- Open, long, checked records. CAMELS-IND gives daily flow and catchment data from 1980 to 2020 for these rivers. Flow records for the Ganga and Brahmaputra systems are classified, so northern India cannot be tested with open data.
- Held-out basins answer the question an agency will ask first: does it work on a river it has not learned?
- One model for all gauges. It learns what rivers have in common, and uses each catchment's description for what differs.
From the project
- Development gauges
- 161
- Held-out gauges
- 29
- Catchment sizes
- km²
Source: models/gauges.csv (team repo) · docs/B_data_catalogue.md · reports/A_executive_feasibility.md
What to say
"We cover Peninsular India because that is where the river data are open and long enough to test properly. We hid two entire river basins from the model, so we can check it on rivers it has never seen."
Step 3 · The data
Five kinds of information, each dated to when it was known
A forecast issued on day t may use only what existed by then. Every input carries a date stamp, and tests in the code fail if any input comes from later.
| Input | Source | Used as |
|---|---|---|
| Rain that fell | IMD 0.25° daily grid, 1980–2024 | Daily average over each catchment |
| Weather and soil wetness | ERA5-Land (reanalysis) | Temperature, humidity, wind, sunshine, evaporation, soil moisture in 4 layers |
| Forecast rain | NOAA GEFS, run of day t−1 | Rain for days t+1, +2, +3 |
| River reading | CAMELS-IND to 2020; GUARDIAN 2021–24; India-WRIS (CWC) for the newest test | Flow as a share of the flood flow |
| Catchment description | CAMELS-IND attributes | 46 fixed facts: size, slope, soils, geology, land cover, dams, population |
Why this way
- Leakage is the most common way flood models look better than they are. Date stamps make it testable instead of a promise.
- Yesterday's forecast run, not today's. At the moment of issue, today's run is not yet published.
From the project
- Training rows (forecast models)
- 782,958
- Of which flood onsets
- 3,785
- Share
- 0.48%
Source: reports/v2_report_draft.md §3 · src/aeris/features/forecast_v2.py · runs/track_a/20261019T153217Z_dc94ca7389/run.json
What to say
"The model gets five kinds of information. The key rule: it can only use what was really known on the evening of the forecast. For the rain forecast we even use yesterday's run, because today's would not be out yet. Floods are rare: only about one day in two hundred is a flood start."
Step 4 · Choosing inputs
Inputs were chosen by testing them, not by guessing
We added one group of inputs at a time and measured the gain on years the model did not train on. Each group had to earn its place.
Why this way
- The order is also a check. Rain that really fell beats the river reading, which beats neither. If a model did better without the river reading, something would be wrong.
- Some attributes were banned. A few catchment numbers shipped with the dataset are computed from the whole flow record, test years included, and would leak the answer.
- The live version drops ERA5-Land. It arrives 5 days to 3 months late. A GRU without it (G2-nolate: rain, river reading, forecast rain) was level with G2 on validation (−0.006).
- The gap to 0.241 says better rain forecasts are worth more than any change of model.
The score
Brier score = average of (forecast − outcome)², where the outcome is 1 for a flood and 0 for none. Skill = 1 − (model's Brier ÷ Brier of the usual chance for that day of the year).
Source: reports/v2_report_draft.md §5 · reports/v2_late_inputs.md · src/aeris/features/track_a.py (FORBIDDEN_ATTRIBUTE_CLASSES)
What to say
"We didn't just throw every number in. We added groups one at a time and kept what helped. Adding today's river reading helped, adding the rain forecast helped more, and the neural network got the most out of them. The last bar shows what we could get with a perfect rain forecast, so the rain forecast is the biggest thing to improve."
| Input | What we do to it |
|---|---|
| Rain | log(1 + mm); a second channel gives mm ÷ the catchment's own 99th-percentile day |
| River flow | flow ÷ the gauge's 2-year flood flow, then log(1 + x); a missing reading becomes 0 plus a "missing" flag |
| Weather, soil | subtract the mean, divide by the spread, both from training days only |
| Day of year | sine and cosine, so 31 December sits next to 1 January |
| Catchment facts | gaps filled with the median, scaled the same way, clipped to ±6 |
Step 5 · Preparing the inputs
Putting every river and every storm on the same scale
A neural network learns best when its inputs are of similar size. Daily catchment rain runs from 0 to 432 mm, and the flood flow of a gauge ranges from 39 to 31,807 cubic metres a second, so each input is reshaped first.
Why this way
- Log scale: on it, 10 mm and 100 mm are one step apart instead of 100 mm dwarfing everything.
- Dividing by the gauge's own flood flow lets one model serve a stream and a large river: 1.0 always means "at the flood line".
- Training days only. Scaling with validation or test days would leak information about them.
- A missing-reading flag lets the network tell "river was low" from "no one read the gauge".
- Dry catchment-days
- 99th-percentile day
- 48 mm
- Wettest day
- 432 mm
Source: src/aeris/models/sequence.py (_series, _vector, _windows) · data/processed/track_a_v2/train.parquet
What to say
"Before training, every input is put on a similar scale. Rain is put on a log scale, river flow is measured against that river's own flood line, and all scaling uses training years only, so nothing leaks from the test."
Step 6 · Splitting time
Trained on the past, tested on the future
Rivers remember, so neighbouring days are alike. Splitting days at random would put near-copies of test days into training. We split by whole years, left gaps, and sealed the test years until the very end.
Why this way
- Validation years chose the settings, the calibration and the alert level. So they cannot also be the evidence.
- The test years were sealed. Every read was logged, and the code refused to read them before the protocol was frozen.
- A fresh test came after: 17 months the model never saw, with river data from a different source (CWC via India-WRIS).
From the project
- Validation rows
- 113,941
- Test, same basins
- 89,289
- Test, unseen basins
- 26,280
Source: catalog/track_a_v2_dataset_card.json · docs/G2_protocol_v2.md · docs/G2_fresh_test_wris.md
What to say
"We trained on 1980 to 2014, tuned on 2016 to 2018, and kept 2021 to 2024 locked away. We scored the locked years exactly once. Then we tested again on August 2024 to December 2025."
Step 7 · Choosing the network
Why a recurrent neural network, and why a GRU
Whether today's rain becomes a flood depends on the weeks before: wet soil and full channels send rain straight to the river. A recurrent network reads the days in order and carries a memory forward, much as a catchment stores water.
Why this way
- LSTM is the established network for rain and river flow. A GRU does the same job with two gates instead of three, so its memory layer has a quarter fewer weights and trains faster (59 s against 83 s per run in v1).
- In v1 the GRU scored highest of 13 model types, so it was carried into v2.
- Convolution (TCN) and attention (Transformer) did worse here, with far more weights. They were given a smaller search (4 settings each, against 12) to fit the laptop GPU, so their scores are a lower bound.
Fair to say: the GRU's lead over the best tree model was not proven in v1 (+0.015, interval −0.005 to +0.040). On the v2 test the tree model L2 scored the same as the GRU (+0.209 against +0.210). The GRU was named the headline model before the test because it had the best validation score.
Source: reports/model_comparison.md §1.3, §1.5 · docs/F_model_comparison_plan.md §2.3
What to say
"Rivers have memory. If it rained all week, the same storm floods more. A recurrent network reads 180 days in order and keeps a memory, a bit like the soil storing water. We compared 13 kinds of model and the GRU came out on top on the tuning years."
Step 8 · The architecture
The network, layer by layer: 27,185 learned numbers
Two kinds of input go in: 180 days of daily readings, and one list of facts about the catchment and the coming rain. Out comes one probability.
Why this way
- The catchment facts go in twice: on every day, so the memory knows what kind of river it is reading, and again at the end, so the final judgement can use them directly.
- Only the last memory state is used. By day 180 it holds whatever the network learned to keep.
- Small on purpose: 27 thousand weights against 783 thousand training rows with only 3,785 floods.
The 15 daily channels (G2)
Rain (log), rain ÷ the catchment's 99th-percentile day, maximum and minimum temperature, humidity, wind, shortwave and longwave sunshine, potential evaporation, soil moisture in 4 layers, river flow ÷ flood flow (log), and a missing-reading flag. The live version, G2-nolate, keeps only rain, rain ÷ 99th percentile, river flow and the flag: 4 channels, 25,073 weights.
Source: src/aeris/models/sequence.py (_build_net) · run.json model_complexity
What to say
"Here is the whole network. 180 days of readings go into the GRU, which keeps a memory of 64 numbers. The catchment description is added on every day and again at the end. Two small layers turn that into one probability. In total it learns about 27 thousand numbers."
zt = σ(Wzxt + Uzht−1 + bz) update (keep) gate
nt = tanh(Wnxt + bn + rt ⊙ (Unht−1 + cn)) candidate memory
ht = (1 − zt) ⊙ nt + zt ⊙ ht−1 new memory
Step 9 · Inside one step
Inside a GRU step: keep, reset, write
Each day the GRU takes that day's inputs x and yesterday's memory h. Two gates, each a number between 0 and 1, decide how much memory to keep and what new content to write.
Why this way
- The last line is the trick. When the keep gate z is near 1, memory passes through almost unchanged, and so does the training signal, across all 180 days.
- A plain recurrent network multiplies its memory by the same weights every day. Over 180 days the signal shrinks to nothing or blows up. Gates fix that.
- An LSTM has a third gate and a separate cell memory. The GRU merges them, and here that cost no skill.
- Memory units
- 64
- Days read
- 180
- GRU weights
- 18,624
18,624 = 3 blocks × 64 units × (31 inputs + 64 memory + 2 biases). Form as in PyTorch nn.GRU. Source: src/aeris/models/sequence.py
What to say
"Each day, the GRU looks at the new readings and its memory, and two gates decide: how much of the old memory to keep, and how much new information to write. That is how it can remember a wet month and still react to today's storm."
Step 10 · Activation functions
Activation functions, and why each one sits where it does
An activation function bends a layer's output, so the network can learn curved relationships such as "a little rain does nothing, a lot does a great deal". AERIS uses three.
| Function | Output | Where, and why |
|---|---|---|
| Sigmoid 1 ÷ (1 + e−x) | 0 to 1 | The GRU gates, because a gate is a fraction: how much to let through. The final output, because a chance must lie between 0 and 1. |
| tanh | −1 to 1 | The GRU's candidate memory: centred on zero, so memory can move up or down, and bounded, so it cannot run away over 180 days. Also the catchment embedding, to keep it on the same scale as the scaled daily inputs. |
| ReLU max(0, x) | 0 or more | The 64-unit layer of the head: simple and fast, and its slope is 1 for any positive input, so the training signal passes through without shrinking. |
Why this way
- Without activations, stacked layers collapse into one straight-line model, no better than logistic regression.
- Slope matters for learning. Sigmoid's slope is at most 0.25, tanh's at most 1. That is one reason tanh, not sigmoid, carries the memory.
Fair to say: the gate and memory functions come with the GRU's design, and ReLU and tanh in the other layers are the standard choices. The activations were not tuned. The search tuned window length, memory size, dropout and learning rate instead (step 13).
Source: src/aeris/models/sequence.py (_build_net)
What to say
"Activation functions make the network non-linear. We use sigmoid where we need a fraction or a probability, tanh for the memory because it stays balanced and bounded, and ReLU in the final layer because it is simple and trains well."
Step 11 · The loss function
How the network is marked: log loss
Training needs one number that says how wrong the forecasts are. AERIS uses log loss, also called binary cross-entropy. A confident forecast that turns out wrong is punished very hard.
Why this way
- A proper scoring rule. The forecast that minimises log loss is the true probability, so the network is rewarded for honest chances, not bold ones.
- Floods were not up-weighted. They are 0.48% of training days. Weighting them up would push every forecast too high, and "20%" would stop meaning 20%.
- It starts at the base rate. The output bias is set to ln(0.0048 ÷ 0.9952) ≈ −5.3, so on step 1 the network already says 0.48%, not 50%.
- Settings and stopping are judged by the Brier score, another proper score and the headline measure.
Source: src/aeris/models/sequence.py fit(): BCEWithLogitsLoss, bias set from the base rate
What to say
"To learn, the network needs a mark for every forecast. Log loss punishes being confidently wrong. We did not boost the rare flood days, because we want a 20 percent forecast to really mean 20 percent."
- Draw 1,024 random gauge-days from 782,958
- Cut each one's 180-day window
- Forward pass: 1,024 probabilities
- Log loss against what happened
- Backpropagation through all 180 days
- Clip the gradient, update with AdamW
Step 12 · Training
The training loop: up to 12,000 steps on a laptop GPU
Each step shows the network 1,024 random gauge-days, measures its log loss, and nudges all 27,185 weights to lower it. Every 1,000 steps it is checked on the validation years and the best version is kept.
Why this way
- AdamW adapts the step size for each weight (learning rate 0.0003), and weight decay (0.00001) pulls weights gently toward zero to limit overfitting.
- Gradient clipping to length 1 stops the occasional huge gradient that recurrent networks produce from wrecking the weights.
- Early stopping is the main guard against memorising. The kept versions came from steps 9,000, 6,000 and 5,000.
- 16-bit arithmetic on the GPU saves memory and time on the 8 GB laptop card.
- Time per run
- 81–98 s
- Batch
- 1,024
- Learning rate
- 0.0003
Source: src/aeris/models/sequence.py fit() · runs/track_a/20261019T153217Z_dc94ca7389 (history of each seed)
What to say
"Training is a loop: show a batch, mark it, adjust the weights, repeat. Every thousand steps we check it on years it is not trained on. When that check stops improving, we stop, so the network does not just memorise the training years."
Step 13 · Tuning
Twelve settings tried, one kept
Some choices are not learned in training: how many days to read, how big the memory is, how much dropout, how fast to learn. We fixed a grid of 81 combinations, tried 12, and kept the one with the lowest validation Brier score.
| Setting | Choices | Kept |
|---|---|---|
| Days read (window) | 90 · 180 · 365 | 180 |
| Memory units | 64 · 128 · 256 | 64 |
| Dropout | 0 · 0.2 · 0.4 | 0 |
| Learning rate | 0.0003 · 0.001 · 0.003 | 0.0003 |
Why this way
- The smallest memory won, with the slowest learning rate. A likely reason: with few floods to learn from, a small network generalises better than a big one.
- The two best settings read 180 and 90 days and were nearly tied (2.859 and 2.863 × 10⁻³). The best 365-day setting scored 2.924. The differences between the top settings are small.
- The test years played no part. The search used validation years and one seed.
- The same 12 settings were tried for G2-nolate, and the same one won.
Source: configs/track_a_v2/v2_G2_gru_I2_from2000__onset_rp2_h2.yaml · run.json search.trials · src/aeris/run.py sample_candidates
What to say
"Some settings we had to choose ourselves, like how many past days the network reads. We tried 12 combinations and let the tuning years decide. The smallest network won, which makes sense when floods are rare."
| Calibration tried | Validation Brier (cross-fitted) | |
|---|---|---|
| None | 0.002878 | kept |
| Platt scaling | 0.002895 | |
| Isotonic regression | 0.002950 |
Step 14 · Finishing
Three seeds, a calibration check and an alert level
The chosen setting was trained three times from different random starts, and the three forecasts are averaged. Then two last questions: are the probabilities honest, and where should an alert begin?
Why this way
- Averaging seeds smooths out the luck of one run. Their forecasts differed by a spread of 0.0009 in probability on average.
- Calibration was tested, not assumed. Two standard corrections were tried against none, and none won: it had the best validation Brier score and also the smallest calibration error (ECE 0.00017, against 0.00023 and 0.00029).
- The alert level is 6.3%: the probability above which only 1% of validation days without a flood were alerted. It was fixed before the test and never changed.
- Alert level (G2)
- 6.3%
- G2-nolate
- 6.0%
- Seeds averaged
- 3
Source: runs/track_a/20261019T153217Z_dc94ca7389/metrics.json (reliability, seed_spread) · run.json (calibration_scores_crossfit, alert_thresholds)
What to say
"We trained it three times and averaged, so one lucky run doesn't fool us. We checked the probabilities are honest: when it says 20 to 30 percent, floods followed about a quarter of the time. And we fixed the alert level in advance."
"Better" had to mean all three
Its gain in Brier skill has a 95% interval entirely above zero.
The gain is at least 0.02.
It catches no fewer floods at the same false-alarm rate.
The order of events, 2026
- Protocol v2 accepted: models, gauges, years and the rule above fixed in writing.
- Frozen: every script, setting and data file recorded by its hash.
- Sealed test 2021–Jul 2024 scored once.
- Fresh test frozen: Aug 2024–Dec 2025, river data from CWC.
- Fresh test scored once.
Step 15 · The rules
A test the model was allowed to fail
Before anyone looked at the test years, the rules were written down and frozen: which models, which gauges, which years, and what would count as better.
Why this way
- It stops the most common error in forecasting studies: trying things on the test until something wins.
- Intervals come from resampling whole sub-basins and whole years, 500 times, because the days of one flood are not independent.
- Sanity checks passed. With shuffled labels the model scores 0.0005 against a limit of 0.005, as it should. 159 automated tests pass.
Two departures, both recorded: the freeze took place on 26 October instead of the evening of 24 October (the laptop went into standby and missed the deadline; no code, setting or plan changed in between, and no test value was read), and a one-word naming error in the report script was fixed after the freeze with the owner's approval, before any test value had been read.
Source: docs/G2_protocol_v2.md · catalog/protocol_v2_freeze.json · catalog/fresh_test_wris_freeze.json · reports/v2_report_draft.md §8
What to say
"We wrote the rules of the exam before the exam. The test years were locked, and we opened them once. If the model had failed, we would have had to report that."
Step 16 · Results
What the sealed test showed
At the 90 gauges of the development basins, 2021 to July 2024, G2 forecast flood onsets two days ahead with clear skill, beat the best simple method, and far outperformed GloFAS as agencies receive it.
- Flood events warned
- 237 of 281
- Warning ahead
- 1.7 days
- False alerts
- ~3 / gauge-yr
About 3 in 4 alerts were false alarms. The fresh test, on 17 newer months with CWC river data, gave the live-capable G2-nolate +0.139 (+0.074 to +0.188) in the same basins.
Source: reports/final_test_v2.md · reports/fresh_test_wris.md · reports/v2_report_draft.md §9–10
What to say
"On the locked years the GRU had clear skill, beat our best simple method, and caught 72 percent of flood days where the global system GloFAS caught 29. It warned of 84 percent of flood events, almost two days ahead. The honest part: three out of four alerts were false alarms, and on rivers it never saw, the result is not yet proven."
From this result to a forecast an agency could use
Step 17 · Limits and next steps
What it cannot do yet
AERIS is a research forecast, tested on past years with archived data. It issues no warnings.
Limits we measured
- False alarms are frequent: about 3 in 4 alert episodes had no flood, and in wet years the alert level raises more.
- Weaker on rivers it never saw: positive but not proven.
- Short warning: 1–2 days, where evacuations often need 3–7.
- River records disagree: in 2020, GUARDIAN and CWC flows agreed within 10% on only 19% of days.
- No live river feed: the open CWC flow record runs months behind.
Source: reports/v2_report_draft.md §11–12 · docs/J_roadmap.md §5
What to say
"We are clear about the limits. It is a research forecast. To be used by an agency it needs a full live season, longer warnings, fewer false alarms and an independent review. Condition one is partly met; the rest is our road map."
Replay of real past days
Try the forecast
Type a river, place or basin, and pick a day. You see what AERIS said that evening, then what the river did. Choose 2016–2018 (the tuning years) or August 2024 to December 2025, months the model never saw.
Loading the forecasts…
Why there is no forecast for today here
A forecast for tomorrow needs today's inputs. On 29 October 2026 they stand like this:
| Input | Available today? |
|---|---|
| Forecast rain (NOAA GEFS) | Yes A few hours after each run, free. |
| Rain that fell (IMD grid) | Substitute IMD's gridded rain is downloaded as one file per year, and we have no 2026 file. NASA's IMERG Early rain arrives about 4 hours after it falls, but the model was not trained on it. |
| River reading (CWC flow) | No Our download from India-WRIS on 26 October 2026 asked for flow up to that day; the open record it returned ends on 4 June 2026. CWC's flood-forecast site publishes mainly water levels, not the flow the model reads. |
| Catchment facts | Yes Fixed. |
A forecast for tomorrow could be run only with the river reading missing and a different rain source. That version has never been tested, so this page does not show it as a forecast.
For real flood warnings in India: India Meteorological Department · Central Water Commission flood forecasts · National Disaster Management Authority.
Data: river flow from CAMELS-IND and the Central Water Commission via India-WRIS; rain from IMD's 0.25° gridded data (Pai et al. 2014, MAUSAM 65); forecast rain from NOAA GEFS; weather and soil from ERA5-Land (Copernicus); state outlines from geoBoundaries (CC BY 4.0). India-WRIS, CWC, MoWR and IMD take no responsibility for the inferences shown here.