diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 68b7d66c97d66c58de883ed0c451af2b3183e6f3..0000000000000000000000000000000000000000 --- a/LICENSE +++ /dev/null @@ -1,203 +0,0 @@ -Copyright 2018- The Hugging Face team. All rights reserved. - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 477b6019b5fae4b65cbdd358283e00b3987abfb6..0000000000000000000000000000000000000000 --- a/NOTICE +++ /dev/null @@ -1,11 +0,0 @@ -naturev1 -Copyright 2026 Nathan - -Licensed under the Apache License, Version 2.0. See LICENSE. - -Builds on `ihelix` (Apache-2.0), which in turn carries kernels from `helix-lm` -and ultimately from HuggingFace Transformers (Apache-2.0). - -Data sources referenced by this package are public: NOAA GOES ABI imagery on -AWS Open Data, and NOAA HURDAT2 best-track archives. This package is not -affiliated with or endorsed by NOAA, NESDIS or the National Hurricane Center. diff --git a/NatureV1_model_card.md b/NatureV1_model_card.md deleted file mode 100644 index a915251c3d4db08d93acbeeddd388d8d73dca1ed..0000000000000000000000000000000000000000 --- a/NatureV1_model_card.md +++ /dev/null @@ -1,1331 +0,0 @@ ---- -license: apache-2.0 -language: - - en -library_name: pytorch -tags: - - weather - - weather-forecasting - - hurricane - - tropical-cyclone - - era5 - - weatherbench2 - - hurdat2 - - geodesic-attention - - physics-informed - - probabilistic-forecasting - - ihelix -datasets: - - weatherbench2/era5 - - hurdat2 -pipeline_tag: other ---- - -# NatureV1 — a probabilistic weather and hurricane model built on geometric attention - -> **Status: trained and scored — a research model, not an operational forecast.** The first full run is complete -> and every number in [section 4](#4-results) was measured on data the model never trained on, including an -> end-to-end test on the 2023 hurricane season (a strong El Niño year no part of the model ever saw). It beats -> physics-only steering and straight-line extrapolation on hurricane track at every lead, but it is several times -> less accurate than the National Hurricane Center and the leading global models, it misses rapid intensification, -> and a red-team pass (section 4.8) found failure modes you should know before using any output. **Do not use it -> for real decisions** — for those, use [the National Hurricane Center](https://www.nhc.noaa.gov). -> -> **Training took about one week** of wall-clock time on a single GPU in a hosted notebook (molab), including -> session restarts and re-staging; the rollout stage alone was 74 GPU-hours. -> -> **Version 0.11** adds stage three — storm-centred infrared satellite imagery for the hurricane heads — and fixes -> what the red team found: calibrated probabilities, an input monitor, and a forecast that can no longer state -> impossible structure (section 4.9). `training_record.json` says which stage the released weights come from. -> -> **Version 0.12 (code ready, not yet trained)** rebuilds the satellite path around the red team's findings and -> moves the storm heads from the Atlantic to every basin — see [section 4.10](#410-012-the-satellite-path-rebuilt-and-every-basin-code-ready-not-yet-trained). -> The released weights do not use it until it has been trained and has beaten them on the same test storms. -> -> **Version 0.13** adds `naturev1.cyclone`: 39 modules of tropical-cyclone physics that need **no neural network**, -> measured on 4,447 forecasts of 407 storms in every basin in seasons they were never fitted on (2018, 2020-2022). -> The physics consensus beats straight-line extrapolation at **every lead from 6 to 120 hours** (+12% to +38%, -> every interval clear of zero) and beats CLIPER at every lead. It does **not** beat GraphCast, Pangu-Weather, -> HRES, Aurora, FGN (WeatherNext 2), FuXi or GenCast past 6 hours; combined with any of them it is never worse and -> is better at 6-12 hours. Plus genesis detection (68% of formations flagged a median 24 h ahead), multi-teacher -> distillation, data assimilation and a 0.25-degree decoder — [section 4.12](#412-013-hurricane-physics-measured-on-seasons-it-never-saw). -> The **swarm sandbox** takes satellite fixes minute by minute and maps impacts parish by parish across the whole Caribbean. -> On unseen seasons its stall probabilities are skilful, its cone is not better than the fixed cone, and on Hurricane -> Melissa it kept western Jamaica at risk but far too low — [section 4.13](#413-013-the-swarm-sandbox-the-satellite-path-and-caribbean-impacts). -> **0.13.1** steers the track with a dynamical model's forecast winds (GFS live, HRES in the hindcasts) and blends -> in the model's own track. Held-out track errors fall by 12% to 65% (72 h: 512 → 184 km), slightly ahead of HRES -> alone to 72 h. On Melissa the track now curves north into Jamaica, but the intensity is still far too low — -> [section 4.14](#414-0131-the-turns-and-rapid-intensification). -> **0.14** blends the consensus with the equal-weight mean of several models (live: GFS, ECMWF IFS and AIFS, -> GraphCast-GFS, GEFS), which cuts held-out errors another 11% at 24 h and 19-23% at 48-120 h (72 h: 142 km), sizes -> the cone per forecast, and makes the swarm's worlds each follow one model — -> [section 4.15](#415-014-several-models-at-once-a-cone-sized-per-forecast-and-a-swarm-on-several-models). - -NatureV1 is an 89-million-parameter model of the atmosphere that forecasts global weather out to five days and, -for tropical cyclones, where a storm will go, how strong it will get, whether it will intensify rapidly, and -whether it will make landfall. Every number it produces comes with an uncertainty. It is built on -[iHELIX](https://pypi.org/project/ihelix/), an attention engine that works on the physical geometry of the planet -rather than on a flat grid, and trained by a single, resumable, self-verifying pipeline that lives in one notebook -cell. The whole stack — engine, model, training pipeline, verification — is open and written by one person. - -- Code and training pipeline: `pip install naturev1[all]` — [naturev1 on PyPI](https://pypi.org/project/naturev1/) -- Engine: `pip install ihelix` — [ihelix on PyPI](https://pypi.org/project/ihelix/) -- Author: Nathan. License: Apache-2.0. - ---- - -## 1. What it does - -Given six frames of the atmosphere (the last 30 hours of ERA5 reanalysis: 89 channels — 11 surface fields and 6 -variables on 13 pressure levels — on a 1.5° global grid), NatureV1 produces: - -**Global weather, at 6, 12, 18, 24, 36, 48, 72, 96 and 120 hours** -- A **full-state rollout**: the whole atmosphere — all 86 prognostic channels, surface to 50 hPa — stepped forward - 6 hours at a time on its own forecast, with mass conservation, out to 72 hours, as an ensemble. **This is the - weather output with measured skill** (section 4.2). -- 2 m temperature, mean sea-level pressure, 10 m wind and precipitation directly at each lead, as a mean and a 90% - interval. These direct surface heads were not updated after the rollout stage and have no skill in this release; - use the rollout. -- A weather-type category head exists but was never trained, so it is never shown. - -**For a tropical cyclone, given its current centre** -- **Track as scenarios, not a line**: six weighted trajectories ("38% recurves offshore, 27% tracks the coast…"), - each with a tilting uncertainty ellipse per lead, so the cone of uncertainty is drawn from the model, not - from a lookup table. -- A **multi-model consensus track**: the learned track, a physics-only steering-flow track, and straight - extrapolation, blended with per-lead weights fitted on held-out seasons — the way the National Hurricane Center's - consensus beats every single model in it, done inside one model. Every member is reported alongside the blend. -- **Intensity**: maximum wind and minimum central pressure per lead, with intervals and the Saffir–Simpson category. -- **Rapid intensification**: the probability of a 25, 30 and 35 kt gain in 24 hours, plus the expected change. - It ranks risk well but its probabilities are far too high (section 4.8). -- **Landfall**: the probability of landfall by each lead. (The landfall-timing head was never trained and is not - shown.) -- **Core structure**: peak eyewall wind, radius of maximum wind, and 34/50/64 kt wind radii by quadrant, kept - physically ordered (34 ≥ 50 ≥ 64 kt; radius of maximum wind inside the gale radius). -- **A cone drawn from the whole scenario mixture** (`track_cone`: the radius holding 50% and 90% of the mixture - per lead), which on held-out storms held the truth as often as it claims to. -- **What the satellite sees (0.11, after stage three).** Storm-centred 11-micron infrared — now and six hours - earlier — from HURSAT-B1, GridSat-B1 or a live GOES scene, read by a small branch aimed at intensity. -- **The storm's environment**, read from the model's own atmosphere: deep-layer steering, 200–850 hPa shear, - mid-level humidity, ocean temperature — the predictors operational statistical schemes (SHIPS) are built on. - -**A local forecast** for any point on Earth, in physical units, from the gridded fields. - -### What it refuses to do -This is as much a part of the model as the outputs above. -- An output whose head has **never received a training step is never shown**. It is replaced by - `{"available": false, "reason": ...}`. The training record travels inside the checkpoint. -- A forecast run from an input path the model was never trained on (today, the satellite path) is marked - **`trustworthy: false`** and the live satellite forecast is refused outright. -- An **input monitor** (0.11) compares the storm features with what training looked like; a forecast from - inputs unlike anything it learned from — corrupted data, noise, zeros — is marked `trustworthy: false`. -- `check_forecast` tests every scenario track, the consensus and the cone, intensity, eyewall and RI value - against **physical bounds**, and checks that landfall never becomes less likely with time and wind radii stay - ordered: no - 400 kt hurricane, no pressure outside 850–1100 hPa, no cone wider than an ocean, no probabilities that do not sum - to one. It reports problems; it does not block the forecast, so a caller must act on what it returns. Section - 4.8 lists what it does not catch. -- The pipeline publishes only if the model beats persistence somewhere on held-out data and demonstrably changes - its answer when the weather changes. That gate is weak (precipitation alone can pass it); this release was - published after the full scorecards and red-team below were reviewed. - ---- - -## 2. How it works - -### 2.1 The engine: iHELIX, attention over physical space -Most weather transformers flatten the globe into tokens and let attention learn geography from scratch. iHELIX -keeps the geometry: - -- **Geometry axes.** A domain is composed from spherical, periodic, linear and logarithmic axes. `Geometry.globe()` - is the Earth's surface; `Geometry.atmosphere()` is a spherical shell whose vertical axis is log-pressure, so that - 1000→500 hPa is the same distance as 500→250 hPa. Every point carries a local east/north/up frame, so "20 km - north" means the same thing everywhere. -- **FieldGrid: data, not architecture.** The points data lives on — ERA5's 240×121 grid, a 4,096-point Fibonacci - mesh, a satellite scan's pixel footprints — with true area weights. The same model reads any of them. -- **GeodesicAttention.** Every attention head has a physical radius in kilometres with compact support: heads - from 120 km (fronts, convection) to 1,600 km (synoptic scale). Attention is over great-circle distance. -- **IndexAttention.** A region tree with beam search lets a point reach distant points (teleconnections) without - paying for the whole globe. -- **TemporalStrand.** A gated delta-rule recurrence carries state across the six input frames at linear cost — - inherited from HELIX, a linear-context language model this engine grew out of. -- **Encode → process → decode.** Any source grid is read onto a shared 4,096-point mesh, processed by 17 blocks, - and decoded onto any output grid. - -### 2.2 The model: NatureV1 -| | | -|---|---| -| Parameters | 89.45 M: 17 iHELIX blocks 84.67 M, input encoders 2.42 M, decoder 0.79 M, storm heads 1.23 M, time embedding 0.13 M, other output heads (field, weather type, noise, ENSO) 0.11 M, state head 0.09 M, norms + mesh seed ~3 k | -| Hidden size / heads | 512 / 8 heads (4 key-value heads), head dim 64, MLP 2048 | -| Latent mesh | 4,096 points, 48 neighbours, cluster 64 | -| Head radii | 120 km to 1,600 km | -| History | 6 frames × 6 h | -| Inputs | ERA5 analysis (89 channels + observation masks, padded to 96); calendar features (solar angle, season) given explicitly; optional satellite and environment encoders; per-member noise for ensembles | -| Outputs | gridded fields with log-variance; full-state residual with log-variance; track mixture (6 modes × 9 leads × 5 parameters); intensity; landfall; RI; eyewall; ENSO | -| Precision | bf16 mixed precision; gradient checkpointing | - -**The storm path** pools the processed mesh around the storm centre with a 500 km Gaussian and, since 0.9, a -second 1,000 km pool (the ridge that steers a storm sits a thousand kilometres from the eye). It adds the storm's -own state (wind, pressure, 12 h motion, 12 h wind change — what a 1.5° reanalysis cannot show), the storm's -position (Coriolis and beta drift), and the **physical environment** computed from the state tensor. The track -head predicts the **residual from a steering-flow track**, which is a far easier target than the whole track: -beta drift, trough interactions, what steering alone gets wrong. - -**Everything new starts at exactly zero effect.** The state head opens as persistence, the ensemble noise as -nothing, the environment inputs as nothing. Adding a capability can never make an existing checkpoint worse, and -uncertainties open at climatological spread rather than at a confident guess. - -### 2.3 The physics inside the model -- **Channel roles.** Every input channel is prognostic (evolved), forcing (incoming solar radiation, recomputed - each step from orbital geometry) or static (land mask, orography, observation masks, padding). A static field - marked prognostic lets a model slowly move a coastline; this is what prevents it. -- **Rollout.** The whole atmosphere advances 6 h at a time on the model's own forecast, with surface-pressure mass - conservation and a GraphCast-style curriculum from one step to twelve. -- **Ensembles.** Per-point noise is spread spatially by the geodesic blocks; a fair CRPS loss grows the spread only - as far as the data justifies; spread/skill is reported. -- **Hurricane physics** (`naturev1.steering`): thickness-weighted 850–200 hPa steering over the 200–800 km ring - around the centre, 200–850 hPa shear, 700 hPa specific humidity, SST versus 26.5 °C; advection with the correct - longitude scaling and climatological beta drift; a steering track from today's analysis held steady, or from the - model's own rolled-out atmosphere step by step. The steering track needs **no training at all**. - -### 2.4 Training, in stages -The whole run took **about one week of wall-clock time** on one RTX PRO 6000 (96 GB) in a hosted notebook. The -GPU time was roughly three to four days; the rest was hosted-session overhead — sessions end after 12 hours or 90 -idle minutes and the disk is wiped, so each restart re-pulled checkpoints and staged data from this repository -before training resumed. Every stage is resumable from a one-minute checkpoint, so no restart lost more than a -minute of training. - -1. **Stage one — pretraining on reanalysis.** Every surface channel at every lead, Gaussian negative - log-likelihood, area-weighted, on years of ERA5. This is where the backbone learns what the atmosphere does, - from about 2×10¹¹ supervised values. -2. **Stage 1b — full-state rollout.** The residual state head is trained to roll the whole atmosphere forward, - curriculum to 12 steps (72 h), 2-member CRPS ensemble. -3. **Stage two — hurricane heads on HURDAT2.** The backbone is **frozen**; only ~1.2 M parameters ever see a - best-track label. The frozen backbone runs once per storm into a cached feature bank; the heads then train in - seconds per epoch with early stopping on seasons never trained on. Afterwards the consensus weights are fitted - on the held-out seasons. -4. **Stage three — the satellite (0.11).** Every storm sample gets two storm-centred infrared images (now and - six hours earlier; 256 × 256 pixels at 0.07°, about 18° across): HURSAT-B1 for 1979–2016, GridSat-B1 after - — the same ISCCP B1 record on the same grid (on Katrina at landfall they agree with correlation 0.989). - A small network (0.84 M parameters: a CNN plus the Dvorak technique's own radial profile of cloud-top - temperature) is added to the hurricane heads with its output at exactly zero, then fine-tuned with the - backbone frozen. The model it starts from is the score to beat on held-out storms; if the satellite does not - beat it, that model is kept unchanged. Then landfall and rapid-intensification probabilities are calibrated - on held-out seasons, the input monitor is fitted, and the rollout's tendency statistics are stored with the - weights. - -### 2.5 Data -- **ERA5 via WeatherBench 2**, 1.5°, 240×121, six-hourly, 13 pressure levels. Split **by time** over 1959–2021 - (train to 2015, validation 2016–2019, test 2020–2021); this run staged the last **five training years, - 2011–2015 (7,300 global states, ~38 GB)**, and validated on 2016. A random split would leak tomorrow's answer - into training. -- **HURDAT2** Atlantic best tracks, 1979 onward (the satellite era). Split **by season** (validation 2017, 2019, - 2021; test 2018, 2020). Labels are paired with the reanalysis **by timestamp**, not row index; off-synoptic - landfall specials are dropped rather than snapped; scarce labels (radius of maximum wind exists on under 5% of - points) arrive as NaN and are masked, never imputed. -- Normalisation per flat channel with log and asinh transforms for spiky fields (precipitation, vertical velocity) - and observation masks for gappy ones (sea-surface temperature). - ---- - -## 3. Verification and safety - -The model is scored the way operational centres score: against the baselines a forecaster would actually use, on -data it never saw, in physical units. - -- **WeatherBench-style scorecards**: area-weighted RMSE and anomaly correlation for t2m, mslp, u10, v10, - precipitation and for Z500 / T850 rollouts, against **persistence** and **climatology** on identical batches, - with a 1% margin before anything is called skill. -- **Storm scorecard** on held-out seasons: great-circle track error of the learned track, the physics-only - steering track, the consensus (scored leave-one-season-out), a storm that stays put, and straight extrapolation; - intensity MAE against persistence. Beating extrapolation and persistence is the bar; beating steering is what - the learned part is for. -- **Hindcast**: a held-out hurricane (Dorian 2019) forecast from its real analysis, every member against what - happened, with wind forecast versus actual. -- **Training watchdog**: raises on a non-finite loss, warns on divergence and on collapse; held-out validation - every 500 steps with an explicit over-fitting flag. -- **Preflight**: grid alignment, normalisation spikes, grid weights and device agreement are checked before step - one, and the run halts if anything fails. -- **Does it respond to the weather?** A probe confirms different inputs give different outputs. - -Resilience is part of safety for a multi-day run: atomic checkpoint writes every minute with snapshots and a -second copy elsewhere; the optimizer, schedule, scaler and random states travel with the weights; damaged -checkpoints fall back to older ones and incompatible ones are quarantined rather than force-loaded; a supervised -background process restarts the run on crash, out-of-memory or stall; every file the run reads back is written -atomically; and on a hosted notebook everything expensive is mirrored to this repository so a wiped disk costs -minutes. - ---- - -## 4. Results - -All numbers below come from the published weights. Track errors are great-circle kilometres; "steering" is the -physics-only steering-flow track (no training), "straight" is extrapolation of the last 12 hours, "stay-put" is a -storm that does not move, and wind "persistence" is "it stays as strong as it is now". The raw outputs are in -[`results/`](./results) and the scripts that produced them in [`eval/`](./eval). - -### 4.1 Training, in numbers -| Stage | What trained | Steps | Notes | -|---|---|---|---| -| One — reanalysis pretraining | backbone + surface heads | 7,275 (8 epochs) | ERA5 2011–2015, Gaussian NLL | -| 1b — full-state rollout | backbone + state head | 4,000 | curriculum to 12 steps (72 h), 2-member CRPS; **74.3 GPU-hours** (4,459 min) | -| Two — hurricane heads | storm heads only, backbone frozen | best epoch 3 of 28 | 584 storms / 16,219 samples, 1979+; feature banks ~2 h, heads minutes; early-stopped on held-out seasons | - -**About one week end to end** (three to four GPU-days plus hosted-notebook restarts), one RTX PRO 6000 (96 GB). The full per-output training record is in `training_record.json`; outputs with zero -steps (weather type, landfall timing, RI onset, ENSO, satellite and environment encoders) are never shown. - -### 4.2 Global weather (held-out data) -From the run's own WeatherBench-style scorecard, on identical batches for every baseline: -- **Full-state rollout: beats climatology by more than 1% on 84 of 84 field-lead pairs, and has skill against both - persistence and climatology out to +72 h on msl, q700, t2m, t850, u10, v10 and z500.** The ensemble - spread/skill ratio at +24 h is **1.08** (1.0 is perfectly calibrated). -- The **direct multi-lead surface heads do not have skill** (they beat persistence on 6 of 45 field-lead pairs, all - precipitation). They were trained in stage one and not updated when the rollout retrained the backbone under - them; use the full-state rollout for weather. The complete tables are in `logs/train.log`. - -### 4.3 Hurricane track and intensity — held-out seasons 2017, 2019, 2021 -59 Atlantic storms (including Harvey, Irma, Maria, Dorian, Lorenzo, Ida) that neither the backbone nor the storm -heads trained on. Consensus weights scored leave-one-season-out. - -| Lead | n | **NatureV1** consensus | learned track | steering | straight | stay-put | wind error | persistence | -|---|---|---|---|---|---|---|---|---| -| 6 h | 1589 | **39** | 42 | 54 | 53 | 140 | 6.2 kt | 3.4 kt | -| 12 h | 1530 | **75** | 84 | 107 | 121 | 275 | 7.8 kt | 6.6 kt | -| 24 h | 1412 | **163** | 180 | 233 | 283 | 527 | **11.0 kt** | 11.8 kt | -| 48 h | 1187 | **421** | 457 | 572 | 628 | 977 | **16.1 kt** | 18.8 kt | -| 72 h | 997 | **739** | 844 | 978 | 1002 | 1369 | **19.5 kt** | 24.2 kt | -| 120 h | 677 | **1373** | 1576 | 1779 | 1828 | 2081 | **22.8 kt** | 31.6 kt | - -Track beats steering and straight-line extrapolation at **every** lead; wind beats persistence from 24 h on. - -### 4.4 End to end on an unseen El Niño season — the 2023 hurricanes -2023 was a strong El Niño year and is outside everything the model saw. ERA5 for 2023 was rebuilt at WeatherBench 2's -1.5° grid from ARCO-ERA5 with area-weighted conservative regridding — checked against the WeatherBench 2 store on -an overlapping date, where it agrees to rounding precision (0.03 Pa in sea-level pressure) — and every forecast -ran through the **full** model, raw weather in. All seven hurricanes, one forecast per day at 00Z (96 forecasts), -with the shipped consensus weights and no refitting. - -| Lead | n | **NatureV1** | steering | straight | stay-put | wind error | persistence | 2017/19/21 for comparison | -|---|---|---|---|---|---|---|---|---| -| 6 h | 96 | **38** | 47 | 53 | 125 | 6.5 kt | 2.9 kt | 39 | -| 12 h | 95 | **74** | 99 | 128 | 248 | 7.5 kt | 5.8 kt | 75 | -| 24 h | 89 | **167** | 231 | 309 | 476 | **10.7 kt** | 11.9 kt | 163 | -| 48 h | 82 | **503** | 689 | 719 | 866 | **14.7 kt** | 18.7 kt | 418 | -| 72 h | 75 | **810** | 1146 | 1132 | 1211 | **15.8 kt** | 21.5 kt | 735 | -| 120 h | 61 | **1487** | 2028 | 1868 | 1807 | **16.1 kt** | 28.0 kt | 1374 | - -| Storm (peak) | 24 h | 48 h | 72 h | -|---|---|---|---| -| Lee (145 kt) | **100** (steering 120) | **240** (378) | **325** (635) | -| Margot | 84 (167) | 350 (522) | 678 (926) | -| Tammy | 125 (223) | 441 (601) | 838 (1295) | -| Don | 169 (222) | 439 (577) | 858 (1031) | -| Idalia | 165 (245) | 534 (718) | 1049 (1373) | -| Franklin | 211 (262) | 680 (944) | 827 (1263) | -| Nigel | 292 (369) | 727 (924) | 1111 (1429) | - -Out to 24 h the El Niño season scored the same as the held-out neutral/La Niña seasons; from 48 h the errors are -larger, but the margin over physics steering grew (29% better at 72 h, against 25%). It beat steering for every -storm at every one of these leads. One season, seven storms, 00Z only: encouraging, not conclusive. - -### 4.5 Landfall, rapid intensification and structure (2017/19/21) -| Output | Result | Verdict | -|---|---|---| -| Landfall probability | Brier skill over the base rate +4% (6 h) to +29% (72 h); AUC 0.81–0.86. 2023: +30 to +42% from 72 h, AUC 0.81–0.95 | skilful | -| Rapid intensification (3 thresholds) | ranks risk well (AUC 0.82–0.84) but probabilities are too high (Brier worse than the base rate) | needs calibration before use | -| Peak wind (eyewall head) | MAE 6 / 11 / 19 kt at 6 / 24 / 72 h vs 22–26 kt for the climatological mean | beats climatology | -| 34/50/64 kt wind radii | MAE 32–37 n mi vs 47–49 for the mean | about 30% better | -| Radius of maximum wind | 28 vs 29 n mi (6 h), loses by 72 h | no skill | - -### 4.6 Individual forecasts -**Maria 2017**, issued 18 Sep 00Z east of the Lesser Antilles, the full model from raw ERA5 (`results/maria_2017_end_to_end.txt`): -miss **14 / 26 / 15 / 65 / 118 / 343 km** at 6 h / 1 / 2 / 3 / 4 / 5 days, against 33 / 91 / 101 / 136 / 181 / 245 km -for steering. Wind forecast about 70 kt against an observed 145–150 kt. - -**Dorian 2019**, issued 30 Aug 18Z, from the run's hindcast: - -| Lead | 6 h | 24 h | 48 h | 72 h | 120 h | -|---|---|---|---|---|---| -| learned track (top scenario 26%) | 24 | 99 | 336 | 688 | 1034 | -| consensus | 12 | 106 | 355 | 669 | 935 | -| steering | 14 | 177 | 382 | 546 | 531 | -| straight | 17 | 161 | 397 | 651 | 1012 | - -Good to two days; Dorian then stalled over the Bahamas, which steering handled better. - -### 4.7 In context -For scale only: the National Hurricane Center's official Atlantic track error averaged about 66 km at 24 h and -168 km at 72 h over 2018–2022, and the leading global AI models (GraphCast, Pangu-Weather, FuXi) and ECMWF are in -the same league. NatureV1's 163 km and 739 km are **2.5–4.5× larger**. It is a 1.5° model trained on five years -of data with a few GPU-days of compute; those systems use 0.25°, decades of data and orders of magnitude more -compute. Intensity is the weakest output: like every 1.5° model it cannot see an eyewall, and it forecasts about -70 kt for storms that reach 145–160 kt. - -### 4.8 Red team: where it breaks -A deliberate attempt to break the published model: physically impossible inputs, calibration against what -actually happened, removing one input at a time, logical consistency, a data-leak audit, and the hardest storm we -could find. Everything here is on held-out data unless noted; scripts in [`eval/`](./eval). - -**Data leakage — none found.** Storm-head training seasons are 1979–2016; held-out 2017/2019/2021 and test -2018/2020/2022 never appear in training; the weather backbone trained on 2011–2015 only. 2023 is outside -everything. - -**Calibration — is the uncertainty honest?** (2017/19/21; 2023 in brackets) - -| Output | Claimed | Actually | -|---|---|---| -| Track, full 6-scenario mixture, 50% region | 50% | 47–53% (32–55%) — honest | -| Track, full mixture, 90% region | 90% | 87–92% (81–93%) — honest, slightly tight at 4–5 days in 2023 | -| Track, **top scenario's "95%" cone** (what a single cone shows) | 95% | **60–87%** — over-confident; the other scenarios carry the rest | -| `cone_radius_km_68` field | 68% | it is the 1-sigma ellipse, which holds **39%** in two dimensions — mislabelled | -| Wind, 90% interval | 90% | 85–93% — honest | -| Landfall by 72 h | 18% / 39% / 61% / 79% | 10% / 31% / 51% / 79% — too high below 70% | -| Rapid intensification (+30 kt/24 h) | 21% / 39% / 55% | **1% / 10% / 38%** — far too high; do not read these as probabilities | - -**Intensity is biased low, and worse for stronger storms** (forecast minus actual, held-out): - -| Current strength | 24 h | 72 h | -|---|---|---| -| Tropical storm (<64 kt) | −3 kt | −6 kt | -| Category 1–2 | −7 kt | −15 kt | -| Category 3–5 | −10 kt | −18 kt | - -**What the hurricane heads actually rely on** (remove one input, held-out error change): - -| Removed | Track 24 h | Track 72 h | Wind 24 h | -|---|---|---|---| -| Physics steering-track prior | **+240%** | +40% | 0% | -| All backbone features | +30% | +13% | +18% | -| Backbone features from a different storm | +29% | +12% | +43% | -| 1,000 km far pool | +10% | +3% | +5% | -| Physical environment (shear, steering, humidity, SST) | +14% | +4% | **+2%** | -| Current wind and pressure | 0% | 0% | **+58%** | -| Last 12 h of motion | +10% | +15% | −1% | - -The track is the physics steering track plus a learned correction worth about 30% at 24 h. The intensity forecast -leans on the storm's current wind; the shear/humidity/SST inputs built to help intensity barely move it. - -**Inputs the safety checks do not catch.** -- **Garbage in, plausible out.** Random noise in place of the backbone features produced a normal-looking forecast - (22–45 kt, a sensible track) with **zero** check failures. There is no out-of-distribution detector. -- **A corrupted steering prior passes straight through.** Shifting the prior by 30° moved the forecast 30° with - zero check failures. -- **The consensus track is not bounds-checked** — a centre near the pole gave consensus latitudes above 90°; - only the scenario tracks were flagged. -- A missing (NaN) storm centre is flagged on the track, but intensity and RI are still produced as if valid. -- Caught correctly: a 400 kt current wind (forecast 370 kt → flagged), positions off the planet, cones wider than an - ocean, and the untrained satellite path (`trustworthy: false`). The dateline and Southern Hemisphere beta drift - are handled correctly. -- Bug: `build_forecast` without a training record crashes on outputs that carry no ENSO field. - -**Logical consistency.** -- Landfall is "probability of landfall *by* this lead", which can never fall as the lead grows. It falls in **20%** - of forecasts (by up to 12 points). -- A 50 kt or 64 kt wind radius exceeds the 34/50 kt radius in **21%** of forecasts, and the radius of maximum wind - exceeds the 34 kt radius in **17%** — both physically impossible. -- Consistent: the three RI thresholds are always ordered, and the two intensity heads agree to 1.5 kt on average. - -**The hardest case: Hurricane Otis, 2023** (East Pacific — outside the Atlantic the storm heads trained on; full -model from raw ERA5, 5 forecasts). Otis went from 50 kt to a 130 kt Category 5 in 24 hours and struck Acapulco. -From 24 Oct 00Z NatureV1 forecast **45 kt** — a slight weakening — and gave rapid intensification 44%, about what -it said on the quiet days before (34–38%). Track: 104 km at 24 h (steering 156 km), but at 72 h it kept Otis at the -same latitude while it moved 5° north. **It cannot warn of explosive intensification.** - -**Reproducibility gap.** The full-state rollout scales its residuals by per-channel tendency statistics measured -from the training data; they are not shipped in this release, so reproducing the weather rollout exactly means -re-measuring them from ERA5 2011–2015 (64 evenly spaced training windows). - -**Outside the Atlantic — the Southern Hemisphere** (full model from raw ERA5; the storm heads only ever saw -Atlantic storms): - -| Storm | n | 24 h: NatureV1 / steering / straight | 72 h: NatureV1 / steering / straight | 72 h north–south: forecast vs actual | -|---|---|---|---|---| -| Seroja 2021 (South Indian) | 10 | 249 / **180** / 338 km | **682** / 788 / 962 km | −6.4° vs −6.6° | -| Niran 2021 (South Pacific) | 9 | 209 / **181** / 277 km | **976** / 1125 / 1246 km | −3.2° vs −2.6° | - -It sends Southern Hemisphere storms poleward — south — as it should, and beats steering at 72 h, but at 24 h the -learned correction, fitted only to Atlantic storms, makes the track *worse* than physics steering alone. Outside -the Atlantic, trust the steering member at short range. - -Still running at the time of writing: four more storms outside the Atlantic (West and East Pacific) and a 10-day -rollout to test stability beyond the 72 h trained horizon. They will be added here. - -### 4.9 Fixed in 0.11, measured on the same held-out storms -| Red-team finding | Fix | Measured after the fix | -|---|---|---| -| RI probabilities far too high (Brier 74% worse than the base rate) | Platt calibration fitted on held-out seasons | **+12% Brier skill** over the base rate, each season calibrated without itself; says 16% → 12% happen, 38% → 44% | -| Landfall over-forecast below 70% | same | says 20% → 21% happen, 39% → 44%; Brier skill unchanged at +29% | -| Garbage inputs gave a plausible forecast | input monitor (feature z-score and length) | random noise and all-zero inputs flagged **100%**; real held-out storms flagged **0%** | -| Landfall "by this lead" fell with lead in 20% | reported as a running maximum | 0% | -| Wind radii out of order in 21%; RMW beyond the gale radius in 17% | structure made consistent when decoded | 0% | -| Consensus track not bounds-checked; `cone_radius_km_68` mislabelled | checked; renamed `cone_radius_km_39`; `track_cone` from the full mixture | — | -| Rollout tendency statistics not shipped | stored in the weights (`state_tendency`) | — | -| No storm centre still produced intensity and RI | every storm section withheld | — | - -Not fixed, and why: a corrupted *steering prior* still passes through (it comes from the model's own analysis, -and a 30° shift is not physically impossible on its own); intensity remains biased low until the satellite stage -has been trained and scored — its results will be added here. - -### 4.10 0.12: the satellite path rebuilt, and every basin (code ready, not yet trained) - -Every red-team finding mapped to a change. **None of these has results yet** — they are trained on 8x B300 by -`b300_naturev1.py`, and this section will report what they measured on the test seasons (2018, 2020), next to the -released model on the same storms. The release is replaced only if the new model beats it. - -| What went wrong | 0.12 | -|---|---| -| Intensity 10–18 kt too weak for majors; Otis 50→130 kt called 45 kt | **SatNet-Storm** (21.8M parameters): a four-frame loop (now, −3, −6, −9 h) of storm-centred 11 µm *and* 6.7 µm imagery → intensity, central pressure, wind radii, RMW, 12–72 h intensity change, motion; trained on ~230,000 fixes in every basin since 1980. Plus a per-lead **intensity bias correction** fitted on held-out seasons | -| RI nearly invisible | **RI 2.0**: the SHIPS-RII thresholds (25/30/35/40 kt in 24 h, 45/36 h, 55/48 h, 65/72 h) as one *ordinal* family (P(≥40) ≤ P(≥35) ≤ …), fed the predictors that carry RI signal — distance below maximum potential intensity, shear, mid-level moisture, ocean temperature, persistence — and SatNet's reading of the inner core; focal loss; calibrated afterwards | -| A storm-centre position was required | SatNet is given a first guess **wrong by up to 150 km** in training and must find the centre; a **vortex tracker** proposes first guesses from the model's own sea-level pressure where there is no advisory | -| Southern hemisphere worse than physics; poleward turns missed (Otis, Rai) | heads work in a **poleward frame** (southern storms mirrored in, mirrored out — an exact mirror, tested); labels from **every basin** (IBTrACS, ~4,500 storms vs 584); two new consensus members from the **model's own rollout**: the storm steered by the *forecast* flow, and the low followed through the forecast pressure field | -| "Satellites do not see the whole atmosphere" | the **retrieval** never pretends to: it brings a 6–48 h old state up to date from a three-frame global satellite loop (window + water vapour, conservative quarter-degree regridding that keeps each cell's coldest pixel and spread, cloud-motion vectors, satellite and view angle per pixel) and starts as *exactly* the stale state; its score is the share of the change since the stale state it recovers | -| Heads trained on perfect reanalysis only | stage B trains the heads **and the backbone's last 4 blocks** with 30% of samples' newest frame replaced by the retrieval, so the model learns from what it gets in real time; the next-step state loss and a pull back to the starting weights keep it a weather model | -| GridSat read naively is 99.95% masked | decoded by hand; QC removes single-pixel glitches but keeps sharp cloud edges; files under half observed rejected and recorded | - -Engine: [iHELIX 0.6](https://github.com/Wisk-inc/transformers/tree/claude/linear-context-architecture-a35c6h/ihelix) -adds conservative regridding, seam-free spherical convolution and grid-to-mesh pooling. Training: data-parallel on -8 GPUs with a learning-rate schedule driven by the clock (it ends exactly when the budget does) and **every -checkpoint pushed to this repository** under `checkpoints//` as it is written. - -### 4.11 0.12: physics, wind and a wider consensus (code ready, not yet trained) - -No single change here is promised to help. The approach is to give the forecast many independent ways to be -right and let the **held-out seasons decide** how much each one counts: every member below enters a consensus -whose weights are fitted on 2017/2019/2021 and can go to zero, and nothing is scored until the test seasons. - -| Area | Added in 0.12 | -|---|---| -| Thermodynamics (`naturev1.thermo`) | Bister–Emanuel **potential intensity** from the model's own sounding (reversible CAPE, iterated eyewall pressure, Ck/Cd 0.9); Tang–Emanuel **ventilation index** and entropy deficit; Emanuel–Nolan **genesis potential**; Hart **cyclone phase space** (B, −VTL, −VTU) for tropical / hybrid / extratropical; 200 hPa divergence, 850 hPa vorticity, mid/low humidity, boundary-layer θe, core rain, land share — 23 storm predictors fed to every head and to RI 2.0 | -| Steering | the steering layer depends on intensity (Velden & Leslie: 850–500 hPa for weak storms up to 850–200 for majors) and beta drift scales with storm size | -| Tracking (`naturev1.tracker`) | **multi-field centre fix** (sea-level pressure, 850/700 hPa vorticity and geopotential, weighted by agreement); intensity and phase read *along the rollout*; a **rollout ensemble** member with its spread; an **analog** member (nearest storms in 45 years of best tracks, in the poleward frame); the satellite centre fix **iterates** — each pass reads a loop cut at the better centre | -| Wind (`naturev1.windfield`) | an **asymmetric Holland wind field** (B from Vickery–Wadhera or fitted to the 34 kt radius per quadrant, Willoughby RMW climatology, Phadke inflow angle, Lin–Chavas motion asymmetry, land roughness, gusts); **NHC-style wind-speed probabilities** (34/50/64 kt) and gale **arrival times** by Monte Carlo over the forecast, at points or on a 0.25° map; Kaplan–DeMaria **inland decay**; R-CLIPER **rain** totals | -| Intensity consensus (`naturev1.statistical`) | a SHIPS-style **regression**, an LGEM-style **logistic growth** model bounded by potential intensity, **climatology–persistence**, the rollout's own intensity, SatNet and the analogs — combined with the network's heads by weights *and* biases fitted per lead on held-out seasons | -| Track consensus | 8 members (two head summaries, steering, extrapolation, rollout-steered, rollout vortex, rollout ensemble, analogs), a per-lead **bias correction** in the poleward frame, and a Goerss-style **predicted consensus error** cone that widens when the members disagree | -| RI | a stacked RI consensus over RI 2.0, the satellite network, the statistical models and the rollout | -| Training | intensity loss **rebalanced by class** (square-root inverse frequency, capped: a major hurricane counts ~4× a tropical storm, the loss keeps its size); an **average of the weights** (EMA 0.999) is what is evaluated and released | -| Forecast time | a **test-time rotation ensemble**: the whole planet turned by the golden angle and run again — the storm, its latitude and its surroundings unchanged, the arithmetic different — merged as a mixture (disagreement widens the uncertainty). Used only if it beats a single pass on the held-out seasons | - -### 4.12 0.13: hurricane physics, measured on seasons it never saw - -Technical report: [Hurricane tracks without a neural network: a physics consensus scored on four unseen seasons](https://corxlabs-papers.static.hf.space/naturev1-hurricane-physics-report.html). - -`naturev1.cyclone` forecasts a storm's track from one 1.5-degree analysis and the best track so far, with no neural -network: 13 members (extrapolation, a rebuilt CLIPER, track-shape analogs, steering trajectories through the -vortex-removed analysis with beta drift that depends on the storm's outer circulation, a flow that keeps evolving -the way it was evolving, a flow that relaxes to the 1981-2010 climatology as its anomaly loses memory, the -vorticity-budget motion, a physics ensemble, a limited-area barotropic model) blended by along- and cross-track -weights per lead. Every constant was fitted on 1980-2014 best tracks or the 2016/2017/2019 hindcasts; the table -below is the four seasons **never used for anything**: 2018, 2020, 2021, 2022, every basin (NA, EP, WP, SI, SP), -forecasts from every 00/12 UTC tropical or subtropical fix, verified while the system stays tropical (NHC rules), -homogeneous samples, 90% intervals from a bootstrap over whole storms. - -| track error (km) | 6 h | 12 h | 24 h | 36 h | 48 h | 72 h | 96 h | 120 h | -|---|---|---|---|---|---|---|---|---| -| forecasts | 4,217 | 3,983 | 3,559 | 3,177 | 2,826 | 2,200 | 1,677 | 1,262 | -| strongest extrapolation (best of 6 h / 12 h / great-circle, per lead) | 33 | 74 | 179 | 303 | 442 | 751 | 1,086 | 1,417 | -| CLIPER (rebuilt, 1980-2014) | 32 | 70 | 163 | 268 | 378 | 589 | 769 | 914 | -| **NatureV1 physics consensus** | **29** | **59** | **127** | **207** | **301** | **515** | **719** | **884** | -| skill against extrapolation | +12% | +21% | +29% | +32% | +32% | +31% | +34% | +38% | - -**Against the teachers, on their own years and the same storms** (each tracked through its own WeatherBench 2 -forecast at 1.5 degrees from the same starting position; tracking at 1.5 degrees costs every teacher a little at -6-12 hours, and they ran at 0.25): - -| km | 6 h | 12 h | 24 h | 48 h | 72 h | 120 h | -|---|---|---|---|---|---|---| -| NatureV1 physics (2018, 2020) | 28 | 58 | 126 | 299 | 511 | 914 | -| GraphCast | 49 | 54 | 67 | 102 | 162 | 296 | -| physics + GraphCast, lead-wise selected (weights never see the scored season) | **26** | **47** | 67 | 102 | 162 | 296 | -| Pangu-Weather (2018-2022) | 49 | 58 | 69 | 101 | 151 | 278 | -| physics + Pangu | **27** | **44** | 69 | 101 | 151 | 278 | -| HRES (2018-2022) | 49 | 60 | 79 | 123 | 187 | 326 | -| Aurora (2022) / FGN-WeatherNext 2 (2022) | 46 / 45 | 55 / 51 | 66 / 61 | 92 / 82 | 146 / 111 | 313 / 199 | - -So: the physics alone is far better than extrapolation and climatology and far worse than a modern global model -past 6 hours; what it adds to a global model is the first 12 hours. Distilling those models into NatureV1 (below) -is the route to their long-range skill; the physics is then the short-range half of the consensus. - -**Intensity, on the same unseen seasons** (SHIFOR, a SHIPS-style regression, a logistic growth model with -Kaplan-DeMaria land and terrain decay, all fitted on 2016/2017/2019 and blended per lead; mean absolute error of the -peak wind, kt): - -| kt | 6 h | 12 h | 24 h | 36 h | 48 h | 72 h | 96 h | 120 h | -|---|---|---|---|---|---|---|---|---| -| persistence | 4 | 8 | 14 | 19 | 23 | 28 | 31 | 32 | -| **NatureV1 intensity consensus** | **4** | **6** | **11** | **14** | **17** | **20** | **21** | **22** | -| skill against persistence | +14% | +21% | +24% | +25% | +26% | +29% | +31% | +31% | - -Every interval is clear of zero and the consensus beats each of its members at every lead. That is the standard -statistical-dynamical bar; operational models do better, and rapid intensification is still missed. - -**Hurricane Dorian (2019, a tuning season; weights fitted on 2016-2017 only):** 21/49/99/164/249/480/656/810 km at -6-120 h against extrapolation's 23/52/131/237/365/641/897/1173 -- and 230 km against 341 at 48 h during the stall -over the Bahamas -- while HRES scored 64 km at 24 h and Pangu 63. - -**Synthetic hard cases** (`naturev1.cyclone.synthetic`: a Dorian-type col where the ridge fades and the trough stays -away, ridge breakdown, trough capture, a loop, a Fujiwhara pair, a centre reformation, a sharp turn, a southern -recurvature; truth integrated through the evolving flow, the forecast sees only the start): the physics beats -extrapolation at 48 and 72 h in all nine (Dorian stall 344 vs 552 km at 48 h and 471 vs 961 at 72 h, the loop 56 vs -416 at 48 h), loses at 24 h in four, and loses at 120 h in four, where the scenario's flow is unlike Earth's -climatology (CLIPER and the analogs pull toward real climatology). The stall is not flagged at the initial time when -the col only forms later. - -**Genesis** (`naturev1.cyclone.genesis`, northern hemisphere June-November, fitted on 2016/2017/2019, scored on -2018/2020-2022, 259 formations): Brier skill +0.22 over climatology, well calibrated (top third: forecast 12%, -observed 13%); at a 30% threshold it flags 68% of formations a median 24 hours ahead with 52% false alarms. - -**Also new in 0.13, code ready and tested, not yet run on a GPU:** multi-teacher distillation (`naturev1.distill`: -every teacher resampled into NatureV1's grid, levels, steps and channels before any loss; skill-weighted mixture -targets with an honest variance; rollout loss with an ERA5 anchor and extra weight near active cyclones), data -assimilation for live inputs (`naturev1.assimilation`: bias correction, gross/background/buddy checks, local -optimal interpolation with NatureV1's own predicted variance as the background error), and a 0.25-degree decoder -(`naturev1.highres`) that reads the latent mesh directly. The molab runbook (`molab_naturev1.py`) runs all of it on -one 96 GB GPU and publishes only if the distilled model beats the physics-only consensus. - -### 4.13 0.13: the swarm sandbox, the satellite path and Caribbean impacts - -`naturev1.swarm` (idea: Nathan) runs alongside the physics. It is a particle filter over possible futures. Each -world is a **4D worldline**: longitude, latitude, **steering depth** (it follows the storm's intensity and wanders) -and time. It is integrated in micro-segments of a few minutes and carries its own physics: steering-layer depth, beta -drift, how far the flow's recent trend persists, how fast its departure from climatology fades, non-divergent -steering perturbations, and a short-memory wobble. - -The **satellite path** comes from NHC's fix deck: Dvorak fixes from GOES imagery, microwave, scatterometer, SAR, -reconnaissance and radar centre fixes. Each fix has its own uncertainty. Fixes stream in at their own minute, and -each one re-weights the worlds. Worlds that disagree with the storm are pruned and the survivors are resampled -with genealogy. Fixes that arrive after the analysis re-weight the forecast itself. - -The swarm is then **painted onto the ground**. The map holds every Caribbean country and territory, split into its -parishes, provinces and districts: 309 units on a 3 km grid, with terrain. Each world contributes the -asymmetric Holland wind field, gusts, and R-CLIPER rain with orographic enhancement on windward slopes. Every unit -gets probabilities of tropical-storm, 50 kt and hurricane-force wind (and gusts), rain totals, a flash-flood signal -on steep ground, and an arrival time. A million worlds run on one GPU. On a 4-core CPU it manages about 15,000 -120-hour worlds a second, and 1,000 worlds painted over a Jamaica landfall take a minute. - -**Measured on the same unseen seasons (2018 and 2020-2022, 4,447 forecasts).** - -- **Track:** the swarm's own mean track beats extrapolation from 12 h (+2% to +22%), loses at 6 h (−9%), and is - well behind the consensus. So the consensus stays the track and the swarm is re-centred on it. -- **Cone:** calibrated on the tuning seasons, the swarm's cone covers 57-64% of positions, against 60-65% for the - fixed cone, at about the same size. It is **not** better yet: its spread barely predicts the consensus's error - (rank correlation 0.06-0.13). -- **Scenarios:** what it adds is these. The probability of a Dorian-type **stall** has Brier skill +0.08 over - climatology, and the probabilities line up with outcomes: forecast 5% → stalled 8%, 17% → 25%, 33% → 38%. - Loop probabilities are reliable (forecast 2.2%, observed 2.1%). Recurvature and steady-track probabilities are - no better than climatology. - -**Hurricane Melissa (2025), replayed with only what was known at the time.** The inputs were the GFS analysis, -the best track to then, and the 21 merged satellite, recon, microwave and scatterometer fixes of the previous -24 hours. - -- From 00 UTC on 27 October, about 41 h before the Category 5 landfall, the consensus missed the northward turn - and took the storm west-south-west of Jamaica. -- The swarm still put western Jamaica at risk: Westmoreland, St Elizabeth, Hanover, Manchester and St James - were the most threatened parishes, at 13-20% for hurricane-force wind and 55-70% for tropical-storm wind, with - the heaviest rain on St Elizabeth's and Manchester's slopes. That is where the worst damage was. -- It was **far too low**. Melissa's best track puts hurricane-force wind over 12 of the 14 parishes and - 250-480 mm of rain over them, against forecasts of 110-175 mm. The intensity consensus forecast 111-125 kt for - a storm that reached 160 kt: rapid intensification to Category 5 is beyond these statistical models. -- From 26 October, extrapolation beat the consensus from 12 to 48 h while Melissa crawled. - -Section 4.14 shows the same forecast after the 0.13.1 upgrade, which fixes the missed turn. - -The molab cell (`molab_swarm_cell.py`) runs all of this for every active NHC storm, or replays any storm since -2021, on one GPU. Research model, not an operational forecast: storm surge is not modelled. Boundaries are from -geoBoundaries (OpenStreetMap and others; attribution travels with the map) and terrain from the Terrarium tiles -on AWS Open Data. - -### 4.14 0.13.1: the turns, and rapid intensification - -A storm turns when its steering changes: a trough digs in, a ridge erodes. The analysis, even with its 12-hour -trend, cannot see a trough that is still 2,000 km and two days away. A dynamical model's forecast can. 0.13.1 -uses that forecast the way NHC's Beta and Advection Model does, and blends in the model's own track: - -- **Forecast-flow member.** The storm is carried through the model's forecast 850-400 hPa winds, every 12 hours - to 120 h, plus beta drift and the part of its present motion those winds do not explain. At each forecast hour, - the model's own copy of the storm is found in its pressure field and removed from its winds, so the storm is - steered by the environment rather than by itself. -- **Model blend.** The consensus, now with the forecast-flow member, is pulled toward the model's own track by - one weight per lead, fitted on the tuning seasons: 0.2 at 6 h, 0.7 at 24 h and 0.9 by day 3. One weight per lead - transferred cleanly between seasons. Fifteen free per-member weights did not, at 96-120 h. -- **Live**, the model is GFS: `GFSAnalysis.forecast_flow` reads its forecast winds and pressure from NOAA's - bucket, about 150 MB per storm, cached. **In the hindcasts** it is ECMWF HRES from WeatherBench 2, at - 1.5 degrees in both cases. - -**Track, held-out seasons 2018 and 2020-2022** (4,188 forecasts at 6 h, 1,103 at 120 h; every gain over the old -consensus has its whole 90% storm-bootstrap interval above zero): - -| lead (h) | 6 | 12 | 24 | 36 | 48 | 72 | 96 | 120 | -|---|---|---|---|---|---|---|---|---| -| 0.13.0 consensus (km) | 29 | 59 | 127 | 207 | 301 | 512 | 711 | 874 | -| HRES's own track (km) | 49 | 60 | 79 | 100 | 123 | 187 | 251 | 326 | -| **0.13.1 consensus (km)** | **26** | **45** | **72** | **96** | **120** | **184** | **249** | **326** | -| gain over 0.13.0 | 12% | 24% | 43% | 54% | 60% | 64% | 65% | 63% | -| gain over HRES alone | 48% | 25% | 8% | 4% | 2% | 2% | 1% (n.s.) | 0% (n.s.) | - -- **Storms that turned.** On the 561 forecasts whose true track turned by more than 45 degrees between 24 and - 72 h, errors fell from 131 / 326 / 601 / 1,048 km to 71 / 119 / 209 / 363 km at 24 / 48 / 72 / 120 h. -- **Cone.** Recalibrated for the new consensus (27 km at 6 h to 413 km at 120 h), it covers 63-70% of held-out - positions against a 67% target. -- **Swarm scenarios.** The swarm's worlds are now steered by the same forecast winds, which markedly improves - its scenario probabilities. Measured on 1,200 held-out forecasts (287 storms with a complete 120-hour truth): - - | Brier skill vs climatology | recurve | stall | steady | loop | - |---|---|---|---|---| - | swarm, analysis steering | +0.05 | +0.09 | +0.01 | +0.01 | - | swarm, forecast steering | **+0.26** | **+0.20** | **+0.14** | **−0.25** | - - Recurvature probabilities are now reliable: forecasts of 33% verified 41% of the time, and 63% verified 67%. - Loops got worse. The swarm now forecasts them three times too often (6.4% against 2.1% observed), so treat its - loop probability as unreliable. The swarm's cone still sizes uncertainty no better than the fixed one (rank - correlation with error 0.09-0.22). - -**Intensity: a rapid-intensification switch.** A logistic model gives the probability of a 30 kt rise in 24 hours -from SHIPS-RII-style predictors: recent change, intensity, headroom below potential intensity, shear, humidity, -ocean heat, latitude and land. On the held-out seasons its Brier skill over climatology is +0.10 and its -AUC 0.81, and it is reliable up to about 50%. When it reaches 15%, the intensity consensus switches to weights fitted on -such storms. Those lean on LGEM and a new **trend member**, which carries the last 24 hours' change forward -with a 24-hour memory, capped at the potential intensity and decayed over land. - -On all held-out storms the switch lowers the mean absolute error by 0.9-2.6% from 6 to 72 h, every lead -significant, and is neutral at 96-120 h. On the 72 forecasts with P(RI) ≥ 30% it lowers the error by 5-10% -from 6 to 36 h. `physics_forecast` now also returns `ri_probability` and `intensity_regime`. - -**Melissa, replayed again** with only what was known at the time: - -| track error (km), lead (h) | 6 | 12 | 24 | 36 | 48 | 72 | 96 | 120 | -|---|---|---|---|---|---|---|---|---| -| from 27 Oct 00Z, 0.13.0 | 2 | 12 | 35 | 187 | 392 | 892 | 1,834 | 3,383 | -| from 27 Oct 00Z, **0.13.1** | 5 | 17 | 91 | **55** | **79** | **246** | **369** | **1,011** | -| from 26 Oct 00Z, 0.13.0 | 14 | 29 | 69 | 149 | 236 | 588 | 992 | 1,794 | -| from 26 Oct 00Z, **0.13.1** | 17 | 28 | 69 | 140 | 215 | **294** | **394** | **375** | - -- **From 27 October**, about 41 hours before landfall, the track now curves north into Jamaica. The forecast puts - the storm at 18.0N 78.0W at 36 h, against an actual 17.5N 78.1W. -- **Parishes.** Every parish now gets a 24-47% chance of hurricane-force wind, up from 13-20%. St Elizabeth, - Manchester and Westmoreland are highest. The four parishes the best track gave only tropical-storm winds - (St Andrew, St Thomas, Kingston and Portland) are the four lowest. -- **Rain.** The forecast is 165-261 mm per parish, against a best-track swath of 146-431 mm (it was 110-175 mm in - 0.13.0). -- **From 26 October**, extrapolation still wins to 48 h while Melissa crawled, because GFS turned it north too - early that day. -- **Intensity is still missed.** From 26 October, P(RI) was 26% and the rapid weights were used, yet the - forecast was 107 kt at 24 h against 125 kt observed, and 100 kt at 48 h against 155. Taking an already intense - storm to Category 5 is still beyond these statistical models. - -Melissa 2025 over Jamaica from 27 Oct 00Z: hurricane-wind probability in 0.13.0 and 0.13.1, best-track swath, and 0.13.1 rain - -**What to keep in mind.** Much of the new track skill comes from a dynamical model: HRES in these hindcasts, GFS -live. GFS has historically tracked storms somewhat less accurately than HRES, so live errors will likely be -larger than this table, and the cone may run slightly narrow. Without GFS's forecast, for example if NOAA's -bucket is unreachable, the forecast falls back to the 0.13.0 consensus and cone, and the run says so. - -### 4.15 0.14: several models at once, a cone sized per forecast, and a swarm on several models - -**Why the long-range errors were what they were.** After 0.13.1 the consensus had become, from 72 h on, essentially -the one model it was blended with (HRES in the hindcasts, GFS live): 184 / 249 / 326 km at 72 / 96 / 120 h against -HRES's own 187 / 251 / 326. One model's error is the floor you reach that way. Two ideas that sounded right were -measured and turned out wrong, so they are not in the release: anchoring the model's track to the observed -position (NHC's "interpolated" models) made the blend worse at 6-24 h on the held-out seasons, because it makes the -model's short-range error correlated with the physics consensus's and the averaging benefit disappears; and -shrinking the model's weight when it disagrees with the forecast-flow member was worse at every lead, because when -they disagree the model is still the better of the two. - -What does work is the oldest result in track forecasting: several independent models, averaged with equal weight. - -**Held-out seasons 2018 and 2020-2022, forecasts with at least two archived models** (4,205 of 4,447; the models are -HRES, Pangu-Weather, GraphCast, FuXi, Aurora and FGN, whichever WeatherBench 2 holds for the date; no weight fitted -to them): - -| lead (h) | 6 | 12 | 24 | 36 | 48 | 72 | 96 | 120 | -|---|---|---|---|---|---|---|---|---| -| 0.13.1: consensus blended with HRES (km) | 26 | 45 | 72 | 96 | 120 | 184 | 249 | 326 | -| equal-weight model mean alone (km) | 44 | 51 | 64 | 79 | 93 | 140 | 195 | 256 | -| **0.14: consensus blended with the model mean (km)** | **25** | **42** | **64** | **81** | **96** | **142** | **200** | **263** | - -Every gain from 24 h on has its whole 90% storm-bootstrap interval above zero. The physics still earns its place -at 6-12 h, where it beats the models' mean by 18-43%; from 24 h on the forecast is mostly the models' mean. That is -the honest shape of it: the physics knows the next day, the models know the week. - -**Live, the models are** GFS, ECMWF IFS and AIFS Single (ECMWF open data, CC-BY-4.0), NOAA's GraphCast-GFS and -the GEFS ensemble mean (with its spread as a diagnostic), each read by byte range from its public bucket into the -same 1.5-degree boxes the physics was fitted on (`naturev1.swarm.models`). Any model not yet published for a -cycle is skipped. For replays older than about four days the ECMWF models come from the AWS mirror, which -throttled every request from this session's network, so the Melissa replay below uses the three NOAA models. - -**A cone sized for this forecast.** The cone is now a regression of the consensus's error on how far the physics -members spread, how far the model mean sits from the physics consensus, how far the forecast-flow member sits from -the model, the storm's speed, latitude and intensity (Goerss's GPCE idea), fitted on the tuning seasons and scaled -to 67% coverage. On the held-out seasons its radius ranks the actual errors with a correlation of 0.17-0.38 (the -fixed cone, by construction, 0; the swarm's own spread 0.09-0.22) at 64-71% coverage, about 5-10% wider on average -than the fixed cone it replaces. The fixed cone is still returned alongside it. - -**The swarm: a tuning that did not survive the held-out test.** With forecast steering the swarm's spread had -been three times the consensus's error at 72-120 h, and that excess motion looked like the reason it forecast loops -three times too often. Cutting the steering-error and residual perturbations to a quarter did bring its spread to -within 12-24% of the error on the tuning seasons and raised every scenario's skill *there*. On the held-out seasons -it did the opposite: the quieter swarm was over-confident (where it said 72% stall, 43% stalled), stall skill fell -from +0.20 to +0.02, steady from +0.14 to +0.06, and the loop over-forecast got worse. The spread that is right for -the swarm's *positions* is not the spread that is right for its *scenarios*, because the swarm is re-centred on a -consensus that is better than its own mean. So 0.14 keeps the 0.13 priors, and the 0.13.1 scenario scores stand: -recurvature +0.26, stall +0.20, steady +0.14, loops unreliable. - -What did change: each world now follows **one model's forecast winds**, drawn with equal probability (live: the -models read for that cycle), so the swarm's futures carry the disagreement between models the way an ensemble does. -Checked on the 2019 tuning season with HRES and Pangu-Weather as the two models (120 forecasts, 33 storms with a -full five-day truth): the two-model swarm's spread and scenario skill were the same as the one-model swarm's to -within noise, because at 1.5 degrees those two models' layer-mean steering is nearly identical (correlation 0.999 -at 24 h). The latent only matters when models disagree, which the live set does more often (on Melissa, below, -GraphCast-GFS and the GEFS mean parted by 300 km at 72 h). It is not yet scored on the held-out seasons. - -**Intensity: the models' own deepening.** The one thing the statistical intensity members cannot know is what -a dynamical model does with the storm. 0.14 reads each model's central pressure along its own track, takes the -change in its pressure deficit (central minus environmental pressure) from the analysis, and uses the models' mean -deepening twice: as a sixth intensity member (``V0`` plus 0.7 kt per hPa at 6 h rising to 2.4 kt per hPa at 48 h, -fitted on the tuning seasons, capped at the potential intensity and decayed over land) and as three more -predictors of the rapid-intensification probability. In the hindcasts the model is HRES at 1.5 degrees; live it is -the mean over the models read. - -Held-out seasons, mean absolute intensity error (kt), the 0.13.1 regime consensus against the same with the -model member (every lead significant): - -| lead (h) | 6 | 12 | 24 | 36 | 48 | 72 | 96 | 120 | -|---|---|---|---|---|---|---|---|---| -| 0.13.1 | 4.1 | 6.4 | 10.6 | 14.0 | 16.7 | 20.3 | 21.4 | 22.5 | -| **0.14** | **4.1** | **6.3** | **10.1** | **13.1** | **15.5** | **19.1** | **20.6** | **21.8** | -| gain | 0.9% | 1.8% | 4.8% | 6.3% | 7.2% | 5.9% | 3.5% | 3.1% | - -The rapid-intensification probability improves from Brier skill +0.096 / AUC 0.813 to **+0.123 / AUC 0.840**. Where it -says 30% or more (98 held-out forecasts), 39% intensified rapidly; above 50% (23 forecasts) only 26% did, so the top -of the scale is over-confident. On those 98 forecasts the point forecast itself is no better than 0.13.1's: the -member helps on the broad population of storms, not yet on the extreme ones. - -**Category probabilities.** `physics_forecast` now returns P(≥ 64 / 96 / 113 / 137 kt) at every lead, from the -forecast plus the empirical distribution of the consensus's out-of-sample errors on the tuning seasons (25 -quantiles per lead, separately for the normal and the rapid regime). Held-out Brier skill over climatology: - -| threshold | 24 h | 48 h | 72 h | 120 h | -|---|---|---|---|---| -| hurricane (≥ 64 kt) | +0.65 | +0.45 | +0.38 | +0.36 | -| major (≥ 96 kt) | +0.48 | +0.30 | +0.26 | +0.17 | -| Category 4 (≥ 113 kt) | +0.36 | +0.23 | +0.17 | +0.10 | -| Category 5 (≥ 137 kt) | +0.18 | +0.10 | +0.11 | −0.01 (19 cases) | - -Skilful everywhere except Category 5 at day 5, where there were nineteen cases to score on. - -**Melissa, replayed a third time** with only what was known at the time. The ECMWF models could not be fetched -for October 2025 from this session's network, so the models are GFS, GraphCast-GFS and the GEFS mean. - -| track error (km), lead (h) | 6 | 12 | 24 | 36 | 48 | 72 | 96 | 120 | -|---|---|---|---|---|---|---|---|---| -| from 27 Oct 00Z, 0.13.0 | 2 | 12 | 35 | 187 | 392 | 892 | 1,834 | 3,383 | -| from 27 Oct 00Z, 0.13.1 (GFS) | 5 | 17 | 91 | 55 | 79 | 246 | 369 | 1,011 | -| from 27 Oct 00Z, **0.14 (three models)** | 3 | 13 | **56** | **46** | **69** | **194** | **274** | **700** | -| the three models' mean alone | 18 | 13 | 66 | 60 | 100 | 238 | 264 | 590 | -| from 26 Oct 00Z, 0.13.1 (GFS) | 17 | 28 | 69 | 140 | 215 | 294 | 394 | 375 | -| from 26 Oct 00Z, **0.14 (three models)** | 14 | 23 | **44** | **85** | **150** | **261** | **362** | **271** | - -- From 27 October the models disagreed usefully: GraphCast-GFS had the track to 198 km at 96 h, the GEFS mean to - 284 km at 120 h, GFS to 971 km; their mean, blended with the physics, beat each of them to 72 h. The cone sized for - this forecast (12 / 48 / 138 / 232 / 814 km at 6 / 24 / 48 / 72 / 120 h) contained the truth at every lead but - 24 h, where the error was 56 km against a 48 km radius. -- From 26 October, GraphCast-GFS alone was remarkable (25 km at 36 h, 123 km at 72 h); GFS and the GEFS mean turned - the storm north a day too early, and the blend landed between them. -- **Parishes** from 27 October: 30-43% for hurricane-force wind over the western and central parishes the storm - crossed (St Elizabeth 43%, Manchester 41%, Westmoreland 39%), 22-25% over the eastern parishes it spared -- - about where 0.13.1 had them, because the swarm's spread, not the consensus track, sets these probabilities. - Rain 153-243 mm per parish against 146-431 mm observed. -- **Intensity is still the miss**: 114 kt at 24 h from 27 October against 155 kt observed; from 26 October the - rapid regime was used (P(RI) 22%) and the forecast reached 109 kt against 145. At 1.5 degrees the models deepened - Melissa by only 3-13 hPa, so the new deepening member had little to say here; it earns its keep on the broad - population of storms, not on this one. - -Melissa 2025 over Jamaica from 27 Oct 00Z: hurricane-wind probability in 0.13.0, 0.13.1 and 0.14, the best-track swath, and 0.14 rain - -**What to keep in mind.** The held-out gains were measured with the models WeatherBench 2 archives; live, the set -is different (GFS, IFS, AIFS, GraphCast-GFS, GEFS mean), and the blend weight was fitted for one model. The cone -regression did not see inter-model spread in training, since the tuning seasons had one model. The model-deepening -member was fitted on HRES's pressure at 1.5 degrees; other models deepen storms differently at that resolution. -Everything here degrades gracefully: with no model readable, the forecast is the 0.13.0 one and says so. - -### 4.16 0.15: the ocean's depth, storm size, and the benchmark table - -**Track: what 0.14 beats, on each reference's own held-out cases** (2018 and 2020-2022; mean error in km, -reference → NatureV1 0.14; * = the whole 90% storm-bootstrap interval of the improvement is above zero). - -| reference | n (24 h) | 12 h | 24 h | 48 h | 72 h | 120 h | -|---|---|---|---|---|---|---| -| extrapolation (6 h motion) | 3,559 | 71 → 41 (+42%*) | 179 → 66 (+63%*) | 440 → 100 (+77%*) | 747 → 149 (+80%*) | 1,417 → 301 (+79%*) | -| CLIPER (rebuilt) | 3,559 | 67 → 41 (+39%*) | 163 → 66 (+59%*) | 376 → 100 (+74%*) | 587 → 149 (+75%*) | 910 → 301 (+67%*) | -| NatureV1 0.13.0 (physics only) | 3,559 | 56 → 41 (+27%*) | 127 → 66 (+48%*) | 299 → 100 (+67%*) | 512 → 149 (+71%*) | 879 → 301 (+66%*) | -| NatureV1 0.13.1 (one model) | 3,559 | 43 → 41 (+4%*) | 73 → 66 (+10%*) | 122 → 100 (+18%*) | 190 → 149 (+22%*) | 373 → 301 (+19%*) | -| **ECMWF HRES** | 3,504 | 56 → 40 (+27%*) | 79 → 65 (+18%*) | 122 → 96 (+22%*) | 185 → 140 (+24%*) | 324 → 260 (+20%*) | -| **Pangu-Weather** | 3,456 | 52 → 40 (+24%*) | 69 → 63 (+9%*) | 101 → 94 (+6%*) | 151 → 139 (+8%*) | 278 → 254 (+8%*) | -| **GraphCast** | 1,862 | 48 → 40 (+17%*) | 67 → 65 (+4%*) | 102 → 97 (+5%*) | 162 → 143 (+11%*) | 294 → 260 (+11%*) | -| **FuXi** | 734 | 49 → 39 (+19%*) | 69 → 64 (+7%*) | 93 → 94 (−1%) | 154 → 146 (+5%) | 295 → 279 (+6%) | -| **Aurora** | 786 | 49 → 41 (+18%*) | 66 → 62 (+6%*) | 92 → 84 (+9%*) | 146 → 123 (+16%*) | 312 → 249 (+20%*) | -| **FGN (WeatherNext 2)** | 787 | 47 → 41 (+13%*) | 61 → 63 (−2%) | 81 → 86 (−7%) | 111 → 118 (−7%) | 199 → 209 (−5%) | - -Read it plainly. NatureV1 now beats every archived model it was compared with at 12 h, beats HRES, Pangu, -GraphCast and Aurora at every lead to day 5, and is level with FuXi past 24 h. It does **not** beat FGN, the -model behind WeatherNext 2, past 12 h: on FGN's 787 cases (2022) FGN alone is 5-7% better than the blend from -24 h on, which is what an equal-weight mean does when one member is clearly the best -- it is diluted by the -others. Two caveats carry over from 4.15: the blend contains these models' own tracks, so this is a product -comparison, not a physics-versus-model one; and live the model set is different (GFS, IFS, AIFS, GraphCast-GFS, -GEFS), with the blend weight fitted for one model. - -**Landfall, answered the way a parish needs it.** `naturev1.swarm.landfall` turns the swarm and the consensus into: -when the centre comes ashore (the median and the 10-90% range over the worlds, with dates, in UTC), which parish it -crosses first and with what probability, the intensity at the consensus landfall time with its calibrated 10-90% -band, the peak intensity and when it comes, and for every parish the expected peak sustained wind, peak gust, the -90th percentile of the peak over the worlds, rain, and the times tropical-storm force and hurricane force arrive -and the peak passes. `plot.landfall_figure` draws it; the molab cell shows it for `LANDFALL_COUNTRY`. The wind field -is now sized to the storm's observed 34-kt radius from the best track, and the worlds' intensities are drawn from -the forecast's own calibrated error distribution for the storm's regime instead of a fixed ±8%. - -**Observations that survive a landfall.** `naturev1.swarm.observations` holds two operators for feeds this package -does not yet read: a vortex fitted to any set of barometer readings (phones, watches, home stations -- pressure goes -through walls) gives a centre fix with its uncertainty, a central pressure and a wind, which the swarm filters like -a satellite fix; and point wind observations are blended into the painted wind map by optimal interpolation. Both -are checked on constructed storms (300 phone-grade readings place a 935 hPa centre to within 12 km and 1 hPa). -No telecom signal-loss or crowd-sourced pressure data was available to train on, so nothing here claims to infer -wind from it; the physics that would use such data is in place. A terrain boundary-layer term (windward speed-up, -lee sheltering, Jackson-Hunt scaling) is in `ImpactSettings.terrain_speedup_per_slope`, off by default, because it -has not been verified against station winds here. - -**The ocean's depth: what the measurement said.** Melissa's lesson was that a storm crawling over 120 m of -26 C water does not cool its own ocean the way one over 30 m does, so 0.15 brought in the World Ocean Atlas -climatology of the 26 C depth and the heat potential, let the wake cooling scale with that depth, let this year's -sea-surface anomaly deepen or shoal the climatological layer, and put the heat potential and the storm's observed -size into the SHIPS and rapid-intensification predictors. Each piece was then chosen on the tuning seasons by -cross-validation, and the cooling term lost: with no depth dependence at all the LGEM member's 12-72 h error was -15.32 kt; with any depth exponent (0.25-1.0) or any anomaly coefficient it was 15.43-15.56 kt, and the same held on -the 349 forecasts over water more than 1 C warmer than the climatology. A monthly 1-degree climatology does not know -the water under one storm well enough to improve on the surface temperature the analysis already has. The depth -term is in the code with its exponent set to zero by the fitted coefficients; the heat potential and the size are -scored below as predictors. -**Intensity: each release's shipped tables on the same held-out cases** (2018 and 2020-2022, 4,215 forecasts of -407 storms at 6 h; mean absolute error in kt; skill = % lower error than the row above it, * = the whole 90% -storm-bootstrap interval above zero). - -| | 6 h | 12 h | 24 h | 36 h | 48 h | 72 h | 96 h | 120 h | -|---|---|---|---|---|---|---|---|---| -| persistence | 4.4 | 8.1 | 14.3 | 18.9 | 23.0 | 28.3 | 30.9 | 32.2 | -| SHIFOR (climatology + persistence) | 4.0 | 6.9 | 11.7 | 15.5 | 18.3 | 21.2 | 22.3 | 22.8 | -| NatureV1 0.13.1 (5-member regime consensus) | 3.8 | 6.4 | 10.8 | 14.3 | 17.0 | 20.1 | 21.3 | 22.3 | -| NatureV1 0.14 (+ model deepening) | 3.7 | 6.2 | 10.1 | 13.1 | 15.5 | 18.9 | 20.6 | 21.8 | -| **NatureV1 0.15** (+ ocean heat content, size, basin bias) | **3.7** | **6.1** | **9.9** | **13.0** | **15.5** | 18.9 | 20.6 | 21.7 | -| 0.15 vs 0.14 | +0.9%* | +1.3%* | +1.3%* | +0.8% | +0.4% | −0.4% | +0.0% | +0.3% | -| 0.15 vs SHIFOR | +6.6%* | +10.7%* | +15.5%* | +16.1%* | +15.1%* | +10.9%* | +7.7%* | +4.7%* | -| 0.15 vs persistence | +16.2%* | +24.8%* | +30.7%* | +32.5%* | +32.7%* | +33.2%* | +33.4%* | +32.6%* | - -Where the ocean and the size earned their place is the rapid-intensification probability, not the mean: held-out -Brier skill 0.094 (0.13.1) → 0.122 (0.14) → **0.154** (0.15), AUC 0.813 → 0.840 → **0.864**; of the 107 forecasts -that put P(RI) at 30% or more, 46% verified (39% in 0.14). On the held-out forecasts over the warmest, deepest water -(slow storms over more than 80 kJ/cm², 54 cases) the mean error fell 4-9% at 6-36 h. The per-basin bias was fitted on -the tuning seasons and then **shrunk to zero wherever the three seasons did not agree on its sign**: what survives is -+0.6 to +3.7 kt in the Atlantic and −0.5 to −2.4 kt in the west Pacific at 6-48 h. Honest footnote: the unshrunk -Atlantic bias (up to +4.2 kt) made the held-out Atlantic *worse* (−0.2 to −3.5% at 36-72 h) -- the under-forecast of -the tuning seasons' Atlantic was situational, not a constant -- and the unshrunk table would have scored +1.7%* and -+2.7%* at 96 and 120 h overall on the strength of a south Indian Ocean term the seasons disagreed on. The shrunk -version is what ships. - -**Why it under-forecasts the wind, measured.** The complaint about Melissa -- the consensus held 103-124 kt while -the storm went to 160 -- is not a bias. Over all held-out cases the 0.15 forecast is 0 to +3 kt *high* in the -Atlantic. It is conditional (bias = forecast − truth, kt, at 12 / 24 / 36 / 48 h): - -| the storm then... | n | 12 h | 24 h | 36 h | 48 h | -|---|---|---|---|---|---| -| intensified ≥ 30 kt in 24 h (rapid) | 272 | −10.9 | −25.5 | −26.4 | −21.3 | -| intensified 20-30 kt | 332 | −5.2 | −10.9 | −13.4 | −13.5 | -| held steady (±5 kt) | 891 | +0.7 | +3.5 | +5.7 | +7.3 | -| weakened ≥ 20 kt | 328 | +7.8 | +14.2 | +17.4 | +17.9 | - -The forecast maximum over the next five days sits 30 kt under the true maximum for the 620 held-out forecasts of -storms that went on to reach 113 kt. A consensus that minimises the mean error regresses toward the middle; no -weighting fixes that, and every attempt to move the mean toward the rapid intensifiers' path in proportion to -P(RI) came back with a weight of zero on the tuning seasons (and would have cost the 4,173 held-out cases that did -not intensify rapidly 2-3 times their error). Two things do help, and both ship: - -* **The rapid-intensification path as a scenario** (`intensity_if_ri`, `coefficients.RI_PATH`): the mean change of - the 217 tuning-season storms that did intensify rapidly (+9/+19/+40/+44/+37 kt at 6/12/24/36/48 h), capped at the - potential intensity along the track and decayed over land. On the 272 held-out forecasts of storms that did go - rapid it beats the consensus by 21%, 35%, 60%, 37% and 8% at 6-48 h; on the ones that did not it is 2-3 times - worse. It is therefore issued *beside* the consensus with its probability, never blended into it. Mixing it into - the category probabilities was tried and switched off: better at ≥96 and ≥113 kt at 24 h, worse at ≥137 kt and at - every threshold from 48 h on. -* **The peak-intensity probability** (`peak_probability`, `coefficients.PEAK`): P(the storm's peak within the next - 120 h reaches 64 / 96 / 113 / 137 kt), a logistic on the analysis intensity, the consensus's rise and maximum, the - warm-ocean headroom, heat content, shear, humidity, P(RI), the model's deepening and the land ahead. Held-out, for - storms below each threshold at analysis time (Brier skill against climatology; the second number is the best a - user had before, the maximum over leads of the per-lead category probability): - - | threshold | n | base rate | peak logistic | per-lead maximum | reliability (forecast → observed) | - |---|---|---|---|---|---| - | ≥ 64 kt (hurricane) | 1,969 | 0.41 | **0.32** | 0.26 | 0.15→0.13, 0.40→0.31, 0.60→0.56, 0.84→0.84 | - | ≥ 96 kt (major) | 2,472 | 0.22 | **0.31** | 0.22 | 0.15→0.09, 0.39→0.37, 0.59→0.60, 0.82→0.80 | - | ≥ 113 kt (category 4) | 2,616 | 0.16 | **0.28** | 0.15 | 0.14→0.15, 0.25→0.25, 0.39→0.41, 0.59→0.53, 0.83→0.82 | - | ≥ 137 kt (category 5) | 2,785 | 0.05 | **0.28** | 0.16 | 0.07→0.06, 0.14→0.15, 0.24→0.34, 0.40→0.54 | - - The landfall report now opens with it ("chance the storm peaks at or above: hurricane 100%, major 62%, category 4 - 31%, category 5 8%"), which is the sentence a parish needs when the point forecast says 107 kt. - -**The track, fed its own recent error.** A model track's error persists from one cycle to the next: the previous -cycle's forecast for the time now being analysed, minus the analysis, is known at analysis time, and a fitted share -of it is subtracted from the new blended track (`apply_track_feedback`, `coefficients.TRACK_FEEDBACK`; pass the -previous cycle's result as `previous=`). Fitted on the tuning seasons' HRES blends, the share is 0.25-0.34 at -24-48 h for a 24 h gap and zero beyond 48 h, where it did harm. Held-out, on the multi-model blend: +0.2%*, +1.1%*, -+1.2%*, +0.4% and +0.2% at 6-48 h. Small and significant, and the honest size of what "learning from the storm's own -behaviour" is worth once the blend already removes the initial-position offset (on the raw HRES track the same idea -is worth 5-11% at 6-24 h, which is that offset). - -**Rain, calibrated against radar.** R-CLIPER (Tuleya et al. 2007) was run along the *observed* track of 24 US -landfalls -- so track and intensity error are removed and only the rain model is tested -- against NOAA MRMS -gauge-corrected radar storm totals on 0.05°, over land within 300 km of the track: 8 tuning storms (Hermine, Matthew, -Harvey, Irma, Nate, Barry, Imelda, Dorian; 137,177 cells) and 16 held-out (Gordon, Florence, Michael, Hanna, Isaias, -Laura, Sally, Delta, Claudette, Elsa, Fred, Henri, Ida, Nicholas, Ian, Nicole; 329,416 cells; Zeta lost a radar day). -As published, R-CLIPER delivers about **half** the observed rain: the tuning storms' observed/forecast ratio is 1.91 -(per storm 0.9-2.8), the area over 100 mm is a third of the observed, the area over 250 mm a sixth. The multiplier -is set so the tuning storms' total is unbiased, **×1.95** (`coefficients.RAIN_SCALE`, `ImpactSettings.rain_scale`). -Held-out: - -| | cell MAE (mm) | bias (mm) | area ≥ 100 mm, forecast ÷ observed | area ≥ 250 mm | 99.5th pct | CSI ≥ 100 mm | CSI ≥ 250 mm | MAE where ≥ 100 mm fell | -|---|---|---|---|---|---|---|---|---| -| as published (×1.0) | 31.3 | −15.3 | 0.39 | 0.15 | 0.58 | 0.204 | 0.115 | 97 | -| MAE-optimal on tuning (×1.2) | 31.3 | −9.4 | 0.55 | 0.36 | 0.70 | 0.239 | 0.180 | 88 | -| **shipped (×1.95)** | 38.4 | +13.1 | 1.41 | 1.87 | 1.10 | **0.285** | **0.291** | **72** | - -So the heavy rain is found far more often (the threat score for 100 mm rises from 0.20 to 0.29, for 250 mm from -0.12 to 0.29, in 13 of the 16 held-out storms) and the error where 100 mm or more actually fell drops by a quarter, -at the price of too much rain in the light-rain cells (the held-out storms were less under-forecast than the tuning -storms: ratio about 1.5 against 1.9), which is where the overall MAE and the +13 mm bias come from. A multiplier -cannot fix R-CLIPER's shape -- too concentrated near the core, nothing for the outer bands and the remnant low -- and -that is the next thing to build. One consequence was handled by hand: the US storms have no mountains, so the multiplier -was calibrated with the orographic term at 1, and stacking it under the old orographic cap of 4 doubled the wettest cells -of a perfect-track Melissa painting against Jamaica's 13 parish gauge maxima; the cap is now 2 (a 1.5-2.5x enhancement is -what the literature finds on windward slopes). That is a choice informed by Melissa, not a verification, and it is marked as such. The parish numbers also changed form: the swarm-mean rain is diluted by every world -that misses, so the landfall report now gives the rain **if the parish is hit**, its 90th percentile over the worlds, -and the parish's wettest cell if hit (the number a rain gauge reports), next to the mean. Satellite rain was checked -as a reference and rejected: CHIRPS put Melissa's 27-29 October total over Jamaica at a maximum of 158 mm against 817 mm -in the gauges. - -**The swarm, sized to the forecast.** The worlds' raw spread was never the calibrated one: `cone_scale` in the swarm -tables (0.27-0.70 for the forecast-steered swarm) corrected the *reported* cone, but the strike probabilities, the -landfall probability and timing and every parish number are made from the raw worlds, which on the held-out cases -were 1.4 to 3.2 times (median, 6 to 120 h) wider than the flow-dependent cone. 0.15 rescales every world's -displacement about the consensus so the swarm's own 67% radius equals the cone sized for this forecast -(`CONE_FLOW`: tight when the models agree, wide when they do not; `swarm_products(target_cone_km=...)`, the affine -`rescale_worlds`). Verified on 396 held-out forecasts of 2018 and 2020 (HRES-steered swarm, 4,096 worlds; the share -of the truth inside the swarm's own 50 / 67 / 90% radii about the consensus, 6 h → 120 h): - -| | 50% radius | 67% radius | 90% radius | -|---|---|---|---| -| raw worlds (0.14) | 0.69 → 0.98 | 0.81 → 0.99 | 0.93 → 1.00 | -| **sized to the cone (0.15)** | 0.50, 0.51, 0.58, 0.56, 0.53, 0.59, 0.63, 0.63 | 0.65, 0.64, 0.70, 0.71, 0.69, 0.73, 0.74, 0.75 | 0.87, 0.87, 0.86, 0.87, 0.90, 0.89, 0.89, 0.89 | - -The 67% coverage is the flow-dependent cone's own (0.64-0.72 on the same cases), the 50% and 90% radii fall where -they should, and the swarm's shape (its 50% and 90% radii are 0.79 and 1.47 times its 67% one) matches the -truth's (0.64-0.79 and 1.50-1.66). Before this the swarm was spreading a Jamaica landfall over the whole western -Caribbean; a world that misses by 300 km at 24 h no longer counts against the parish in its path. - -**Melissa, replayed cycle by cycle with 0.15.** GFS, GraphCast-GFS and the GEFS mean (the ECMWF mirror is throttled -from this network); a 20,000-world swarm; each cycle fed the previous cycle's track. What was issued, against what -happened (landfall near New Hope, Westmoreland, 18.09N 77.97W, about 17 UTC on 28 October at 160 kt; peak 165 kt at -12 UTC): - -| forecast from | before landfall | P(landfall, Jamaica) | centre ashore, median (10-90%) | consensus landfall | intensity then (10-90%) | the RI branch (its probability) | P(peak ≥ cat 4 / cat 5) | track error 24 / 48 / 72 h | -|---|---|---|---|---|---|---|---|---| -| 26 Oct 00 UTC | 65 h | 74% | 28 Oct 03 UTC (27 Oct 18 → 28 Oct 15) | 28 Oct 03 UTC, Clarendon | 104 kt (72-140) | 125 kt at landfall, peak 134 (24%) | 63% / 29% | 42 / 158 / 277 km | -| 26 Oct 12 UTC | 53 h | 80% | 28 Oct 15 UTC (06 → 21) | 28 Oct 13 UTC, St Elizabeth | 110 kt (79-146) | 152 kt, peak 155 (20%) | 100% / 32% | 19 / 57 / 161 km | -| 27 Oct 00 UTC | 41 h | 76% | 28 Oct 15 UTC (09 → 21) | 28 Oct 14 UTC, St Elizabeth, 18.12N 77.88W | 110 kt (90-133) | 153 kt, peak 162 (8%) | 100% / 23% | 43 / 74 / 215 km | - -![Melissa over Jamaica, parish by parish, from 27 October 00 UTC](melissa_landfall.png) - -Where it is now right: the landfall point (10 km from the actual at 41 h out), the landfall time (the median two hours -early with the actual inside a 12-hour band, where 0.14 had put it eight hours early inside a 30-hour band), the -parish ranking (at 27 Oct 00 UTC the ten parishes the best-track swath gave hurricane-force wind are the top ten by -P(HU): St Elizabeth 84%, Westmoreland 77%, Manchester 72%, St James 71%, Trelawny 71%, Hanover 64%, Clarendon 56%, -St Ann 53%, St Catherine 33%, St Mary 20%; the eastern parishes 9-15%; Brier 0.061 against the swath), and the -landfall probability itself (76% where 0.14 said 41% from the same cycle -- the swarm sized to the cone, 4.16 above). -Where it is still wrong, said plainly: the consensus intensity, 125 → 116 → 106 kt over the first 48 h against -135 → 155 → 165 → 105, is the under-forecast of section 4.16's table made flesh -- a storm in the top few percent of -intensification over the deepest warm water in the basin. The branch that *was* the storm -- 153 kt at landfall, -peak 162 -- was on the page from 26 October with its probability (24%, 20%, 8%), and the chance of a category-5 -peak sat at 23-32% through all three cycles; a reader of the report had the right scenario and a fair probability -for it, not a point forecast that said so. Parish timing ran 3-4 hours early (hurricane force into St Elizabeth at -09:20 UTC, the peak at 13:17, against an eyewall that arrived mid-afternoon), because the consensus moved the storm -faster than the crawl it actually made. Rain, if hit / wettest cell against the NHC gauge maximum: Manchester -563 / 747 against 817 mm, St Elizabeth 547 / 787 against 508, Clarendon 483 / 668 against 530, St Catherine -432 / 593 against 668, Kingston 424 against 567 -- the right magnitude in the centre and south, now that the -multiplier is in; too much in the far west, where the storm crossed quickest (Westmoreland 689 against 309, -Hanover 603 against a single gauge at 146), and too little in the east (Portland 397 against 536), because R-CLIPER -puts the rain under the strongest wind and Melissa's two days of crawling south of the island fed the Blue Mountains -and St Catherine instead. `melissa_jamaica.png` shows the four releases side by side from 27 October 00 UTC. - -### 4.17 0.16: the official guidance as members, the model's own rain, and the answer key - -The NHC's public a-deck carries every operational aid's track and intensity for a cycle within minutes of the synoptic -time. 0.16 reads it (`naturev1.swarm.adeck`) and blends the aids in with tables fitted on the tuning seasons -(`naturev1.cyclone.aids`, `coefficients.ADECK_TRACK` / `ADECK_INTENSITY`). Everything below is on the held-out -seasons 2018 and 2020-2022, Atlantic cases where the guidance exists, homogeneous across the rows (749 / 703 / 626 / -554 / 490 / 374 / 281 / 206 cases at 6 / 12 / 24 / 36 / 48 / 72 / 96 / 120 h; 79 storms); * marks a whole 90% -storm-bootstrap interval above zero. The 6 h column of every aid is an interpolation between its analysis and its 12 h -point, so it flatters nobody but us; read the aids from 12 h on. - -**Track (mean error, km).** The rule chosen on the tuning seasons -- with our own track cross-fitted by season so that -it was scored out of sample like the aids -- was the plain mean of our multi-model blend and HCCA; the fitted simplex -over HCCA, TVCN, the GEFS mean and the GFS came within 0.2% of it there, so the mean ships. - -| | 6 h | 12 h | 24 h | 36 h | 48 h | 72 h | 96 h | 120 h | -|---|---|---|---|---|---|---|---|---| -| NatureV1 0.14 blend | 22.1 | 40.1 | 62.2 | 82.3 | 95.5 | 168.7 | 260.5 | 365.1 | -| **NatureV1 0.16 = mean(0.14 blend, HCCA)** | **21.5** | **35.7** | **54.1** | **72.5** | **89.4** | **149.6** | **227.7** | **318.7** | -| 0.16 against 0.14 | +2.8% | +11.0%* | +13.0%* | +11.9%* | +6.4%* | +11.4%* | +12.6%* | +12.7%* | -| HCCA (HFIP corrected consensus) | 29.9 | 40.7 | 60.3 | 80.7 | 104.6 | 170.5 | 253.8 | 363.9 | -| TVCN (track consensus) | 29.8 | 41.8 | 62.7 | 84.5 | 109.5 | 170.0 | 248.8 | 346.5 | -| OFCL (NHC official forecast) | 27.6 | 42.4 | 64.1 | 84.9 | 109.2 | 169.4 | 241.6 | 334.4 | -| 0.16 against the official forecast | +22.0%* | +15.9%* | +15.5%* | +14.6%* | +18.1%* | +11.7%* | +5.8% | +4.7% | - -On the tuning seasons the same mean gained 2% -- there our blend leaned on HRES, which HCCA also contains; on the -held-out seasons the blend is a six-model AI-heavy mean whose errors are more independent of HCCA's, and the gain is -five times larger. Two things to read with it: HCCA reaches the a-deck about two and a half hours after the synoptic -time (our cycle waits for the GFS analysis anyway, which is later), and the official forecast is written by -forecasters from this same guidance, so the last row is a product comparison, not a claim about who sees the -atmosphere better. - -**Intensity (mean absolute error, kt).** Smooth-L1 simplex weights per lead over our 0.15 consensus and the -early-cycle aids; leave-one-season-out on the tuning seasons the blend gained 13-28% at 24-96 h in 2016, 2017 and 2019 -alike. HAFS-A and HAFS-B take the HWRF and HMON slots when those are absent, a stand-in by lineage that the archive -could not verify and that the record reports. - -| | 6 h | 12 h | 24 h | 36 h | 48 h | 72 h | 96 h | 120 h | -|---|---|---|---|---|---|---|---|---| -| NatureV1 0.15 consensus | 3.4 | 6.0 | 9.6 | 12.2 | 13.9 | 16.2 | 18.4 | 18.8 | -| **NatureV1 0.16 = 0.15 + aids** | **3.3** | **5.4** | **7.1** | **8.5** | **9.5** | **10.5** | **12.5** | **12.8** | -| 0.16 against 0.15 | +2.9%* | +9.9%* | +26.0%* | +30.6%* | +31.4%* | +35.1%* | +32.0%* | +32.3%* | -| IVCN (intensity consensus) | 4.3 | 5.5 | 7.2 | 8.5 | 9.4 | 10.7 | 12.7 | 13.8 | -| HCCA | 4.3 | 5.5 | 7.2 | 8.7 | 9.6 | 11.1 | 12.5 | 13.0 | -| HWRF (HWFI) | 4.6 | 6.2 | 8.0 | 9.4 | 10.6 | 13.3 | 15.3 | 16.3 | -| SHIPS with decay (DSHP) | 4.5 | 6.1 | 8.4 | 10.2 | 11.6 | 13.4 | 16.3 | 18.3 | -| LGEM | 4.5 | 6.4 | 9.0 | 11.1 | 12.2 | 13.3 | 15.8 | 17.5 | -| OFCL (NHC official forecast) | 3.6 | 5.0 | 7.4 | 8.8 | 10.1 | 11.2 | 12.4 | 13.4 | -| 0.16 against the official forecast | +6.4% | −7.9% | +4.9% | +4.5% | +5.9%* | +6.1% | −0.7% | +4.6% | - -The weights say what the archive thinks of us: our consensus carries 78% of the blend at 6 h, 54% at 12 h, 20% at 24 h -and nothing from 48 h on, where HCCA, IVCN and the hurricane models take over. The one thing the aids do not fix is -the thing this roadmap started from: on the 39 held-out cases that intensified by 30 kt or more in the next day, the -bias at 24 h moves from −23 kt (ours) to −21 kt (the blend), with the official forecast at −19 kt -- every mean -forecast under-forecasts the explosive cases, which is why the calibrated peak quantiles and the RI branch (4.16) stay -in the report beside the headline. - -**Rapid intensification, the NHC's probabilities beside ours.** The SHIPS text carries the SHIPS-RII, logistic, -Bayesian, DTOPS and consensus probabilities of +30 kt in 24 h; the season archive's e-deck carries the same after the -fact -- but only from 2020, so there is no tuning-season sample and nothing here was chosen on one. On 2020-2022 -(481 cases with the consensus, 453 with DTOPS; 25 and 24 events): ours Brier skill 0.125 (AUC 0.866), the SHIPS-RII -consensus 0.068 (0.847), the deterministic SHIPS-RII 0.020, the logistic −0.070, the Bayesian 0.074, DTOPS 0.171 -(0.866); the equal-weight mean of ours and the consensus 0.133, of ours and DTOPS 0.199. The mean with DTOPS is -reported as `ri_probability_combined` in the cycle record and the cell with that caveat; ours stays the headline and -the regime switch until a tuning-season sample exists. - -**The rest of the roadmap, briefly** (`ROADMAP_0.16.md` has the matrix): the GFS's own accumulated rain travels with -every world as half of its rain -- against gauge-corrected radar on US landfalls, Ida 23 vs 45 mm mean error, -Nicholas 17 vs 55, Fred 21 vs 36, Ian level; the calibrated peak quantiles (coverage 0.53 / 0.77 / 0.91 / 0.95 at -50 / 75 / 90 / 95%, q90 four kt above the true peak of the category-5 storms where the consensus max sits 37 kt -under) and E[peak | RI] (+0.6 kt bias on the storms that went rapid); the arrival-time window (3.6 h at 24 h overall, -7.3 h for storms under 3 m/s; no along-track bias found, so nothing is nudged); the cycle record and the previous -cycle's feedback found automatically. Rejected on the tuning seasons and listed as such: the stall-over-water penalty, -a translation-speed rain factor, a 115-kt rain saturation, nonlinear rain transforms, dynamic per-storm model weighting. - -**The answer key.** `benchmark_heldout_atlantic_2018_2020_2022.csv.gz` in this repository holds the 849 held-out -Atlantic cases with the best-track truth at every lead and every reference's forecast or error on them: NatureV1 0.14 -and 0.16 tracks, 0.15 and 0.16 intensities, the official forecast and the aids (HCCA, TVCN, IVCN, HWRF, HMON, SHIPS, -LGEM, GFS, GEFS mean), the archived models' track errors (HRES, Pangu, GraphCast, FuXi, Aurora, FGN), CLIPER and -extrapolation. `naturev1.cyclone.verify.compare` scores a new forecast against it on the same storms with the -storm-block bootstrap. One long table, one scoring function: a claim about a hurricane model can be checked on it. - -**Melissa replayed with 0.16** (the same three cycles as 4.16; the GFS rain member and the NHC guidance in, 20,000-world -swarm; "own" is our blend before the aids, "official" the NHC forecast from the same cycle): - -| forecast from | before landfall | P(landfall, Jamaica) | centre ashore, median (10-90%) | consensus landfall | intensity then (10-90%) | the RI branch (its probability) | P(peak ≥ cat 4 / cat 5) | track error 24 / 48 / 72 h: 0.16 (own; official) | -|---|---|---|---|---|---|---|---|---| -| 26 Oct 00 UTC | 65 h | 72% | 28 Oct 09 UTC (27 Oct 21 → 28 Oct 21) | 28 Oct 06 UTC, Clarendon | 96 kt (64-132) | 124 kt at landfall, peak 130 (23%) | 62% / 28% | 44 / 121 / 195 km (42 / 158 / 277; 34 / 123 / 124) | -| 26 Oct 12 UTC | 53 h | 83% | 28 Oct 12 UTC (06 → 21) | 28 Oct 11 UTC, St Elizabeth | 101 kt (71-136) | 151 kt, peak 152 (19%) | 100% / 31% | 42 / 72 / 147 km (15 / 53 / 161; 40 / 54 / 73) | -| 27 Oct 00 UTC | 41 h | 83% | 28 Oct 15 UTC (09 → 21) | 28 Oct 14 UTC, St Elizabeth, 18.06N 77.72W | 98 kt (78-121) | 149 kt, peak 160 (8%) | 100% / 23% | 54 / 71 / 212 km (38 / 69 / 215; 79 / 79 / 164) | - -![Melissa over Jamaica, parish by parish, from 27 October 00 UTC, 0.16](melissa_landfall.png) - -What moved against 4.16: the landfall probability (76 → 83% at 41 h), the far-west rain (Westmoreland 413 mm if hit -against the 309 mm gauge where 0.15 said 689; Hanover 407 against 146 where 0.15 said 603; Manchester 584 / 865 against -817, St Catherine 468 / 666 against 668, Kingston 466 against 567 -- the GFS's own rain pulls the total off the fast -western crossing and onto the parishes that got it), the landfall point and time (14 UTC against about 17, the median -15 UTC inside a 12-hour band, as before). What moved the wrong way, said plainly: Melissa was not the archive. On the -two earlier cycles the NHC guidance was worse than our standalone blend at 24-48 h -- at 26 Oct 12 UTC HCCA sat 70 km -off at 24 h where our blend had 15 km, so the mean sat at 42; the intensity aids were all low on this storm and pulled -the 48 h intensity from 110 to 100 kt (26 Oct 12 UTC) and from 106 to 85 kt (27 Oct 00 UTC) against a truth of 165 and -105 -- and the mean inherits what its members do. That is the price of a rule that lowers the error 11-13% over 626 -held-out cases; the parish Brier against the swath went from 0.061 to 0.073 at 41 h. The averaging penalty, taken to -the archive (the table below), and what ships for it, is in the next paragraph. The branch that -was the storm -- 149-151 kt at landfall, peak 152-160 -- stayed on the page from 26 October with its probability, and -the 95% peak quantile sat at 133-140 kt against the 165 kt that happened: the explosive case is still a tail event to -this engine, as to the official forecast (140 kt at 24 h against 155). Parish timing still ran about four hours early -(hurricane force into St Elizabeth at 08:20 UTC, the peak at 12:42, against an eyewall that arrived mid-afternoon), as -the arrival window in the report now says it can. `melissa_jamaica.png` shows the four releases side by side. - -**The averaging penalty.** A fixed 50/50 mean drags a member that caught a turn toward one that did not -- the 26 Oct -12 UTC cycle above. Whether that is the rule or the exception is a question for the archive, not for one storm. On the -held-out Atlantic cases, split by how far our blend and HCCA sit apart at 24 h: - -| held-out cases | n | ours alone | HCCA | TVCN | 50/50 mean | ours closer than HCCA | ours the outlier against TVCN | -|---|---|---|---|---|---|---|---| -| all with HCCA | 641 | 62 km | 60 | 63 | 54 | -- | -- | -| the 20% that disagree most (≥ 92 km apart) | 159 | 133 | 93 | 95 | 94 | 31% | 97% | -| the 10% that disagree most (≥ 128 km apart) | 80 | 220 | 101 | 106 | 138 | 16% | 100% | - -When the two disagree strongly it is our standalone that is wrong two times in three, and the mean is a hedge that -costs little when ours is right and saves a great deal when it is not. Every remedy that trusts the standalone in a -disagreement was tried on the tuning seasons and lost there (ours 2/3, a disagreement gate toward ours, the median and -the trimmed mean of ours, HCCA and TVCN). Two rules survived and ship in `ADECK_TRACK`: (1) **weights earned from the -previous cycle** -- at issue time the previous cycle's forecasts of ours and of HCCA are verified against the position -just analysed, and the split follows `sigmoid(0.5 · log(e_HCCA / e_ours))` within 25-75% at 6-24 h (chosen per lead on -the tuning seasons; nothing to earn beyond 24 h, so the split stays fixed there); (2) **the outlier gate** -- once the -two sit more than 1.5 cone radii apart, up to 10% of the pair's weight moves away from whichever of them is further -from TVCN (a tie with the plain mean on the tuning seasons, a loss at 6 h in every season, so it starts at 12 h). -Together on held-out: 1.4% / 2.5% / 1.1% / 0.7% / 1.0% / 1.9% / 2.3% lower error at 12 / 24 / 36 / 48 / 72 / 96 / 120 h, -every lead significant; in the most-disagreeing fifth, 94 → 89 km at 24 h. For the Melissa cycle that raised the -question the earned split would have given ours 54% at 24 h (its previous 12 h forecast was 22 km off, HCCA's 33) and -the gate nothing (the two were less than 1.5 cone radii apart) -- a few km back, not the 27 km lost, because the -archive says that cycle was the exception. The disagreement is now reported per lead (`aids_track["disagreement_km"]`), -the cell says when the two sit further apart than the cone, and both tracks are in the record for the forecaster who -has reason to believe the standalone this time. - -## 5. Limitations - -- **Resolution.** 1.5° (~165 km) cannot resolve an eyewall, so intensity is the weakest output. The explicit - environment inputs (shear, humidity, SST) were added to compensate; the red team shows they barely move the - intensity forecast (section 4.8). -- **Not operational.** It runs on reanalysis, which is available days after the fact. There is no real-time - analysis feed, and the satellite input path exists but is untrained. -- **Verification is internal.** Three held-out seasons plus the 2023 season, scored by this project's own - scripts; no independent party has verified it. -- **Intensity.** Biased low, by 10–18 kt for major hurricanes; misses explosive intensification entirely (Otis). - Rapid-intensification probabilities are calibrated in 0.11 but carry only modest skill; radius of maximum wind - has no skill. The satellite stage targets this; its effect is measured when it is trained. -- **Uncertainty display.** The full scenario mixture is well calibrated; a single scenario's cone is - over-confident. Use `track_cone`, drawn from the mixture. -- **Input checks are statistical.** The monitor catches inputs unlike training; a plausible but wrong input - (another storm's features, a shifted steering prior) is not detectable this way. -- **Other basins.** At 24 h the learned track correction is worse than physics steering outside the Atlantic. -- **Surface heads are stale.** The direct multi-lead surface fields were not retrained after the rollout stage; - use the full-state rollout for weather. -- **Small data.** Five years of reanalysis at 1.5°, and the storm heads start memorising after about three epochs. -- **Atlantic only** for the storm heads, 1979 onward; other basins are out of distribution. -- **Training cost.** About one week wall-clock on one GPU in a hosted notebook; most of the overhead was session - restarts. A machine that keeps its disk would cut it to three or four days. -- **Tornadoes, hail, flash floods** are far below this resolution and out of scope. - ---- - -## 6. Using it - -Install and train, or score, from one cell (marimo, Colab, Jupyter or a plain script): - -```python -pip install "naturev1[all]" # 0.16 from PyPI -pip install "naturev1[all] @ git+https://github.com/Wisk-inc/transformers@claude/linear-context-architecture-a35c6h#subdirectory=natureweather" # the development branch -``` -The complete pipeline is the cell `colab_naturev1.py` shipped with the package; it installs what it needs, stages -the data, trains in stages, scores itself, and only publishes if it earned it. Every setting is at the top. - -Load the released model (weights, config and the normalisation statistics it was trained with): - -```python -from naturev1.hub import from_pretrained - -model, config, normalizer = from_pretrained("CorxLabs/NatureV1-500") -print(model.trained_heads()) # steps each output has received; 0 = never trained, never shown -``` - -With satellite imagery (0.11, once stage three has been trained), from a live GOES ABI band 13 file: - -```python -import numpy as np -import torch -from naturev1.stormsat import encode_bt, goes_patch - -now = goes_patch("C13_now.nc", (lat, lon)) # storm-centred 0.07-degree patch, kelvin -earlier = goes_patch("C13_6h_ago.nc", (lat_6h, lon_6h)) # where the storm was six hours ago -codes = torch.from_numpy(encode_bt(np.stack([now, earlier])))[None] -outputs = model(..., storm_satellite=(codes, torch.tensor([[True, True]]))) -``` - -The weights are also provided as `model.safetensors`. To reproduce the results in section 4, see [`eval/`](./eval): -`elnino2023.py` rebuilds 1.5° ERA5 for any date from ARCO-ERA5 and runs the full model on a season's storms. - -`build_forecast(outputs, config.lead_times_hours, issued, storm_center=(lat, lon), trained=model.trained_heads())` -turns a forward pass into the forecast bundle: track scenarios with cones, consensus with members, landfall, -intensity, rapid intensification, eyewall, and a point forecast, in physical units with 90% intervals, with -every untrained section marked unavailable. - -The hurricane path (0.13-0.16; the physics, the swarm, the guidance, the parishes) is one call, live or replayed as it was known at -the time: - -```python -from naturev1.swarm.geography import CARIBBEAN, RegionMap -from naturev1.swarm.landfall import format_landfall, landfall_summary -from naturev1.swarm.live import forecast_storm -region = RegionMap.build(CARIBBEAN) # once; cached -earlier = forecast_storm("al132025", when="2025-10-26T12", archive=True, particles=100_000, region=region) -run = forecast_storm("al132025", when="2025-10-27T00", archive=True, # Melissa as it was known then - particles=100_000, region=region, - previous={"time": "2025-10-26T12", "track": earlier["result"]["track"]}) # 0.15: its error feeds back -print(run["impact"].table()) # parish-by-parish probabilities -print(format_landfall(landfall_summary(region, run["result"], run["case"], run["impact"], "Jamaica", - analysis_time="2025-10-27T00"))) # when, where, how strong; rain if hit -print(run["result"]["peak_probability"]) # P(peak within 5 days >= 64/96/113/137 kt) -print(run["result"]["intensity_if_ri"], run["result"]["ri_probability"]) # the rapid-intensification branch -# 0.16: the NHC a-deck is read by default (guidance=False switches it off) and its aids blended in; the official -# forecast, the aids used and our own forecast before the blend: -print(run["guidance"]["ofcl"], run["result"]["aids_track"], run["result"]["aids_intensity"], run["result"]["intensity_ours"]) -print(run["guidance"]["nhc_ri"], run["guidance"].get("ri_probability_combined")) # the NHC's RI guidance beside ours -``` - -### Files in this repository -``` -model.safetensors the released weights (89.45 M parameters), safetensors format -model.safetensors.pt the same weights as a PyTorch state dict (what from_pretrained loads) -config.json, normalizer.json model configuration and the training normalisation statistics -training_record.json steps per output, parameter count, stage-two state -results/ every scorecard and test in section 4, as produced -eval/ the scripts that produced them -checkpoints/stage1|rollout|stage2 full training checkpoints (with optimizer state) for resuming or fine-tuning -checkpoints/stage3 stage three: satellite branch, calibration, input monitor (0.11) -checkpoints/storm_bank_*.pt frozen-backbone features for the storm heads -checkpoints/satellite_bank_*.pt storm-centred infrared for every storm sample (one byte per pixel) -staging/ the normalised ERA5 staging, so a new session needs no re-download -logs/train.log the complete training log, including every scorecard -``` - ---- - -## 7. Why it is built this way - -Three convictions run through the whole stack. - -**Geometry is not something a weather model should have to learn.** Distances, areas, the sphere, the vertical: -these are known. iHELIX gives attention physical radii and true area weights so the parameters go on the -atmosphere, not on rediscovering the globe. - -**Scarce labels must be spent on few parameters.** There are 55,230 Atlantic best-track points in history and no -engineering makes more. The backbone learns from 10¹¹ free reanalysis values; the hurricane heads, about a million -parameters, learn from the labels on top of a frozen backbone, with the physics handed to them explicitly. - -**A forecast that cannot say "I don't know" is dangerous.** Every output carries an uncertainty, every untrained -head is hidden, the physical bounds are checked, the model is scored against persistence before anyone is -allowed to believe it, and it has been red-teamed so its failures are written down (section 4.8). The light bulb was not made in one shot; this one is instrumented so that when it fails, -it fails visibly. - ---- - -*NatureV1, iHELIX and the training pipeline are by Nathan, Apache-2.0. This card describes the first full -training run of version 0.10 of the pipeline (about one week of training, completed October 2026). ERA5 data: Copernicus Climate Change Service; HURDAT2: NOAA -National Hurricane Center.* diff --git a/README.md b/README.md index 9778e26bf8426e15140412e1e2b0eb7cc982ab74..b29b84e78623bcb51a1fe4bb0b04827ea803566c 100644 --- a/README.md +++ b/README.md @@ -5,44 +5,15 @@ tags: - weather - tropical-cyclone - hurricane -- satellite -- ihelix pipeline_tag: other --- # NatureV2-500 -The next model of the CorxLabs hurricane engine. It starts from the NatureV1-500 **stage-1 checkpoint** -(`checkpoints/stage1/latest.pt`, with its `config.json`, `normalizer.json` and the storm banks, copied here from -[CorxLabs/NatureV1-500](https://huggingface.co/CorxLabs/NatureV1-500)) and carries the complete `naturev1` 0.16 code base, -so that the fine-tuning of the satellite encoder and the new features can start from one place. +Base weights for the next CorxLabs model: the NatureV1-500 stage-1 checkpoint (`checkpoints/stage1/latest.pt`) with +its `config.json`, `normalizer.json`, `training_record.json` and the storm banks, copied from +[CorxLabs/NatureV1-500](https://huggingface.co/CorxLabs/NatureV1-500). NatureV2 is a new architecture built on this +backbone: the satellite encoder enabled and new storm features added. The code is the `naturev1` package on PyPI +(`pip install "naturev1[swarm,hub]"`); nothing else is kept here. -## What is here - -* `checkpoints/stage1/latest.pt`, `config.json`, `normalizer.json`, `training_record.json`, `checkpoints/storm_bank_*.pt` - -- the base to fine-tune from (the global atmosphere at 1.5 degrees after stage 1; the storm banks for the storm heads). -* `naturev1/`, `pyproject.toml`, `tests/` -- the 0.16 code base: the model and its training stages (`model.py`, - `train.py`, `sattrain.py`, `satstage.py`, the satellite readers `stormsat.py` / `satellite.py`), the hurricane physics - engine (`cyclone/`), the swarm, impacts, landfall, the live path and the NHC guidance layer (`swarm/`). - `pip install "naturev1[swarm,hub]"` installs the same code from PyPI (0.16.0). -* `RESULTS_0.16.md` -- the complete record of the results and of what was built, with every held-out table. -* `ROADMAP_0.16.md` -- the deficit matrix, what shipped and what was rejected. `NatureV1_model_card.md` -- the NatureV1 - card (sections 4.12-4.17 carry the verification of every release). `naturev1_README.md` -- the package README. -* `molab_swarm_cell.py` -- the operational cell (one marimo cell: the live path, the parishes, the cycle record; - `TRAIN_SATELLITE = True` runs the satellite stage on the GPU from `checkpoints/stage1`). `b300_naturev1.py`, - `molab_naturev1.py`, `colab_naturev1.py` -- the training cells of the earlier stages. - -## The plan - -1. **The satellite encoder.** Fine-tune the band-13 path (SatNet-Storm 2.0 and the global satellite retrieval of 0.12) - from the stage-1 weights on storm-centred GridSat / HURSAT / GOES imagery (`naturev1.satstage` stages it, - `naturev1.sattrain` trains it; the cell wires both). -2. **New features.** The infrared structure diagnostics and their trend (`naturev1.swarm.infrared`), lightning, and the - ocean heat feed, as inputs to the storm heads and to the rapid-intensification probability -- the explosive cases - are still tail events to every mean forecast, and the satellite is where the next predictor is. -3. **Verification before use.** Every new number is scored on the same held-out seasons (2018, 2020-2022) and the same - benchmark table (`benchmark_heldout_atlantic_2018_2020_2022.csv.gz` on the NatureV1-500 repository) before it - changes anything in the operational path. - -Research model, not an operational forecast. For decisions, follow your national meteorological service and the -National Hurricane Center. +Research model, not an operational forecast. diff --git a/RESULTS_0.16.md b/RESULTS_0.16.md deleted file mode 100644 index 5b03b4e8cf82daeaf4e70cba6ef030aa1df29cca..0000000000000000000000000000000000000000 --- a/RESULTS_0.16.md +++ /dev/null @@ -1,208 +0,0 @@ -# NatureV1 0.16 -- the results, and the record of what was built - -CorxLabs, Westmoreland, Jamaica. October 2026. This is the complete account of the hurricane engine as it stands at -0.16.0: what the architecture is, how every number was earned, what was tried and rejected, what shipped, and where it -is published. Every figure here comes from the held-out seasons unless it says otherwise; nothing was tuned on them. - -## 1. What the system is - -NatureV1 is a weather model with a hurricane engine around it. Three layers: - -1. **iHELIX**, the engine: attention over physical space on the sphere (conservative regrid, spherical convolution, - binning), the kernels the model is built from (`ihelix`, on PyPI). -2. **NatureV1-500**, the model: the global atmosphere at 1.5 degrees with storm heads (track, intensity, rapid - intensification, eyewall, landfall), every output a distribution, trained in stages (stage 1 the atmosphere, - stage 2 the storm heads on the storm bank, stage 3 the satellite path: SatNet-Storm 2.0 and the global satellite - retrieval on GridSat/HURSAT/GOES band 13), published at `CorxLabs/NatureV1-500` with its checkpoints - (`checkpoints/stage1`, `stage2`, `rollout`, the storm banks), config and normaliser. -3. **The hurricane physics engine** (`naturev1.cyclone`, `naturev1.swarm`; versions 0.13 to 0.16), which is what - this document is about. It is deterministic physics plus fitted tables, no network in the loop, and it is the part - verified against every archived reference: - * **Track.** A consensus of physics members (extrapolation, CLIPER rebuilt, beta-and-advection steering from the - analysis at several depths, an analog member, the forecast-flow member that follows the dynamical model's own - steering), simplex weights per lead; blended with the mean track of the live models (GFS, ECMWF IFS and AIFS, - GraphCast-GFS, the GEFS mean, each followed at native resolution); the previous cycle's error fed back; and - since 0.16 the NHC's operational aids blended in (section 4). - * **Cone and swarm.** A flow-dependent cone (Goerss-style regression of the blend's error on member spread, model - disagreement, speed, latitude, intensity) and a particle-filter swarm of up to a million worlds, each world - steered by one model's flow, filtered on every satellite, recon and radar centre fix, sized to the cone, - re-weighted by fixes that arrive after the analysis. It gives scenario probabilities (stall, loop, recurve, - steady), strike probabilities and the parish impacts. - * **Intensity.** SHIFOR, decay-SHIFOR, a SHIPS-style statistical-dynamical member, LGEM, a trend member and the - models' own deepening, in a simplex consensus with a rapid-intensification regime; a 17-feature logistic P(RI); - the RI path as a branch beside the mean; ocean heat content and 26 C isotherm depth from the World Ocean Atlas - under the storm; peak probabilities P(peak >= 64/96/113/137 kt), calibrated peak quantiles and E[peak | RI]; - category probabilities per lead; since 0.16 the NHC intensity aids blended in. - * **Impacts.** A parametric asymmetric wind field sized to the observed 34-kt radius, land decay, gusts, arrival - times; rain from R-CLIPER reshaped against gauge-corrected radar with an orographic term, and since 0.16 the - GFS's own precipitation travelling with every world; painted onto every parish, province and district of the - Caribbean with terrain; the landfall report (when, where first, how strong, each parish's numbers, the - arrival-time window). - * **Operations.** One call from an NHC storm id (`naturev1.swarm.live.forecast_storm`), live or replayed as it was - known; every cycle saved as JSON and a parish CSV; the previous cycle found again automatically; GOES band-13 - read quantitatively; the molab cell that runs all of it and keeps watching NHC. - -**The protocol that every number obeys.** Fitted on the tuning seasons 2016, 2017 and 2019. Scored once on the -held-out seasons 2018, 2020, 2021 and 2022 (the Atlantic where the NHC guidance exists; all basins for the physics). -Significance by a storm-block bootstrap (a star marks a whole 90% interval above zero). Shipped only if it helped -held-out, reported either way. The coefficients module records the provenance of every table. - -## 2. Where it stands against the references (held-out) - -**Track, km, Atlantic 2018 / 2020-2022, homogeneous (749 / 703 / 626 / 554 / 490 / 374 / 281 / 206 cases at -6 / 12 / 24 / 36 / 48 / 72 / 96 / 120 h, 79 storms).** - -| | 6 h | 12 h | 24 h | 36 h | 48 h | 72 h | 96 h | 120 h | -|---|---|---|---|---|---|---|---|---| -| NatureV1 0.14 blend (physics + live models) | 22.1 | 40.1 | 62.2 | 82.3 | 95.5 | 168.7 | 260.5 | 365.1 | -| NatureV1 0.16, mean with HCCA | 21.5 | 35.7 | 54.1 | 72.5 | 89.4 | 149.6 | 227.7 | 318.7 | -| **NatureV1 0.16 shipped (earned split + outlier gate)** | **22** | **36** | **53** | **72** | **89** | **149** | **225** | **317** | -| HCCA (HFIP corrected consensus) | 29.9 | 40.7 | 60.3 | 80.7 | 104.6 | 170.5 | 253.8 | 363.9 | -| TVCN (track consensus) | 29.8 | 41.8 | 62.7 | 84.5 | 109.5 | 170.0 | 248.8 | 346.5 | -| NHC official forecast (OFCL) | 27.6 | 42.4 | 64.1 | 84.9 | 109.2 | 169.4 | 241.6 | 334.4 | - -The 0.16 blend is 11-13% under 0.14 at 12-120 h (every lead significant) and 12-22% under the official forecast at -6-72 h, 5-6% at 96-120 h. The aids' 6 h values are interpolations between their analysis and their 12 h point, so -read the aids from 12 h on. - -**Track against the archived models, all basins (from 4.16 of the model card; each reference on its own cases).** -NatureV1 beats HRES (+18% at 24 h, +24% at 72 h), Pangu (+9%, +8%), GraphCast (+4%, +11%), Aurora (+6%, +16%) at -every lead to day 5, is level with FuXi past 24 h, and trails FGN (WeatherNext 2) by 5-7% from 24 h on its 787 -cases. Against the single dynamical models in the a-deck (GFS, HWRF, HMON, COAMPS-TC, the GEFS mean) it is better at -every lead by 6-39%. - -**Intensity, kt mean absolute error, Atlantic held-out, homogeneous with the official forecast.** - -| | 6 h | 12 h | 24 h | 36 h | 48 h | 72 h | 96 h | 120 h | -|---|---|---|---|---|---|---|---|---| -| persistence / SHIFOR (0.15's own references) | -- | -- | 0.15 is 16-33% better than persistence and 5-16% better than SHIFOR at every lead | | | | | | -| NatureV1 0.15 consensus | 3.4 | 6.0 | 9.6 | 12.2 | 13.9 | 16.2 | 18.4 | 18.8 | -| **NatureV1 0.16 (0.15 + the aids)** | **3.3** | **5.4** | **7.1** | **8.5** | **9.5** | **10.5** | **12.5** | **12.8** | -| IVCN (intensity consensus) | 4.3 | 5.5 | 7.2 | 8.5 | 9.4 | 10.7 | 12.7 | 13.8 | -| HCCA | 4.3 | 5.5 | 7.2 | 8.7 | 9.6 | 11.1 | 12.5 | 13.0 | -| HWRF (HWFI) | 4.6 | 6.2 | 8.0 | 9.4 | 10.6 | 13.3 | 15.3 | 16.3 | -| SHIPS with decay (DSHP) | 4.5 | 6.1 | 8.4 | 10.2 | 11.6 | 13.4 | 16.3 | 18.3 | -| LGEM | 4.5 | 6.4 | 9.0 | 11.1 | 12.2 | 13.3 | 15.8 | 17.5 | -| NHC official forecast | 3.6 | 5.0 | 7.4 | 8.8 | 10.1 | 11.2 | 12.4 | 13.4 | - -0.16 is 26-35% under 0.15 from 24 h on (every tuning season agreed in leave-one-season-out, 13-28% at 24-96 h) and -4-6% under the official forecast at 24-72 h, level at 96-120 h. The official forecast is itself written from this -guidance three hours later: read it as a product comparison. - -**Rapid intensification, P(+30 kt in 24 h).** Our logistic (17 predictors: the SHIPS-style set, the models' deepening, -ocean heat content and 26 C depth, the gale radius): held-out Brier skill 0.154, AUC 0.864 (0.14: 0.122, 0.840). On the -2020-2022 cases where the NHC e-deck carries RI rows (there are none in the archive before 2020): ours 0.125, the -SHIPS-RII consensus 0.068, the deterministic SHIPS-RII 0.020, the logistic -0.070, the Bayesian 0.074, DTOPS 0.171; -the equal-weight mean of ours and DTOPS 0.199. That mean is reported beside ours, not as the headline, because it -could not be chosen on a tuning season. - -**Peak intensity.** P(peak within 5 days >= 64 / 96 / 113 / 137 kt): held-out Brier skill 0.32 / 0.31 / 0.28 / 0.28 -against 0.26 / 0.22 / 0.15 / 0.16 for the best per-lead category probability. Calibrated peak quantiles: coverage -0.53 / 0.77 / 0.91 / 0.95 at the 50 / 75 / 90 / 95% levels; the 90% quantile sits +4 kt above the true peak of the ->= 137 kt storms where the consensus maximum sits -37 kt. E[peak | RI]: +0.6 kt bias, 13 kt MAE on the storms that went -rapid. The bias that remains is conditional: -21 kt at 24 h on the storms that intensify rapidly (0.15: -23; the -official forecast: -19), +14 kt on those that weaken fast. - -**Cone and swarm.** The flow-dependent cone replaces the climatological one; the swarm's raw worlds were 1.4-3.2 times -too wide (the truth inside the nominal 67% radius 81-99% of the time); sized to the cone, 396 held-out forecasts put -the truth inside the 50 / 67 / 90% radii 50-63 / 64-75 / 85-90% of the time. - -**Rain.** R-CLIPER along the observed track of 24 US landfalls against NOAA MRMS gauge-corrected radar delivered half -the observed rain. The reshaped profile (multiplier 1.19, radial tail x1.5, front-right asymmetry 0.3) lifts the -held-out threat score for 100 mm to 0.296 and for 250 mm from 0.12 to 0.348, and brings the strong storms' area ->= 250 mm from 3.2x observed to 1.7x. The GFS's accumulated precipitation moved with each world, at half weight: -Ida 23 vs 45 mm mean error, Nicholas 17 vs 55, Fred 21 vs 36, Ian level. - -**Timing.** No systematic along-track bias (+1 / +4 / +5 / +2 / -1 km at 6-48 h). Arrival-time error 3.6 h at 24 h, -7.3 h for storms under 3 m/s: reported as a window in the landfall report, nothing corrected. The previous cycle's -error fed back into the track: +1.1-1.2% at 12-24 h, nothing past 48 h. - -## 3. The 0.16 work, item by item - -The release started from six named deficits (the roadmap matrix) and ended with eight rows, each with a state. - -| deficit | what the archive said | what shipped / what was rejected | -|---|---|---| -| explosive peak gap (forecast peak 25-37 kt under category-5 peaks) | regression to the mean under RI uncertainty, not a loss artefact; any weight toward the RI path costs the 94% that do not explode | **shipped**: calibrated peak quantiles, E[peak \| RI]; the aids take 26-35% off the intensity error but move the RI-case bias only -23 -> -21 kt | -| track 2-7% behind FGN past 12 h | an equal-weight mean diluted by weaker members; no FGN live | **shipped**: the NHC aids (HCCA) as a member, -11-13%; **rejected**: dynamic per-storm model weighting (~0), NNLS member weights (+0.3-1.1%, noise) | -| light-rain bloat | a per-storm scale error from wetter tuning storms, not a radial-tail problem | **shipped**: the reshaped profile and the GFS rain member; **rejected**: every nonlinear transform, a 115-kt saturation, a speed factor | -| terrain and moisture | no moisture flux in a parametric profile | **partly**: east of Melissa right since the reshape; the QPF member is the moisture term; IVT orography needs a calibration set | -| stalls over water held too strong | the sign of the stall residual flips between seasons | **rejected**: the fixed rule is 45-122% worse on the stall cases; Melissa stalled over 120 m of 26 C water and intensified | -| landfall timing 3-4 h early | no along-track bias; the error is a spread, largest for slow movers | **measured, reported as a window**, nothing nudged | -| the operational guidance was not in the room | the a-deck carries every aid minutes after the cycle | **shipped**: `naturev1.swarm.adeck` + `naturev1.cyclone.aids`, two fitted tables, stand-ins by lineage (HAFS for HWRF/HMON, TVCN for HCCA) | -| the NHC RI guidance was not in the room | the SHIPS text carries five schemes live | **shown beside ours** with the equal-weight mean and the no-tuning-sample caveat | - -**The averaging penalty**, raised on the Melissa 26 Oct 12 UTC cycle (ours 15 km at 24 h, HCCA 70, the mean 42), taken -to the archive: - -| held-out cases | n | ours alone | HCCA | TVCN | 50/50 mean | ours closer than HCCA | ours the outlier against TVCN | -|---|---|---|---|---|---|---|---| -| all with HCCA | 641 | 62 km | 60 | 63 | 54 | -- | -- | -| the 20% that disagree most (>= 92 km apart at 24 h) | 159 | 133 | 93 | 95 | 94 | 31% | 97% | -| the 10% that disagree most (>= 128 km apart) | 80 | 220 | 101 | 106 | 138 | 16% | 100% | - -When the two disagree strongly, our standalone is the one that is wrong two times in three. Rejected on the tuning -seasons: ours 2/3, a gate toward ours, the median and the trimmed mean of ours/HCCA/TVCN. Shipped: (1) **weights earned -from the previous cycle** -- the previous forecasts of ours and of HCCA verified against the position just analysed, -`sigmoid(0.5 log(e_HCCA / e_ours))` within 25-75% at 6-24 h; (2) **the outlier gate** -- beyond 1.5 cone radii of -disagreement up to 10% of the pair's weight moves away from whichever member is further from TVCN, from 12 h on. -Together on held-out: 1.4 / 2.5 / 1.1 / 0.7 / 1.0 / 1.9 / 2.3% lower error at 12 / 24 / 36 / 48 / 72 / 96 / 120 h, every -lead significant. The disagreement is reported per lead and both tracks are in the record. - -**Also built in 0.16:** the GOES band-13 diagnostics with trend (`infrared.py`); the cycle record and history -(`bulletin.py`); `forecast_storm(previous="auto", guidance=True)`; the molab cell rewritten with the watch loop; audit -fixes (dateline-safe swarm rescale, caller's leads for the 24/48 h indices, silent fallbacks leave a note, NHC-only -ids, the previous analysis position as the first feedback knot); the benchmark table. - -## 4. Melissa (AL13 2025), replayed with what was known at the time - -| forecast from | before landfall | P(landfall, Jamaica) | centre ashore, median (10-90%) | consensus landfall | intensity then (10-90%) | RI branch (its probability) | P(peak >= cat 4 / cat 5) | track error 24 / 48 / 72 h: 0.16 (own; official) | -|---|---|---|---|---|---|---|---|---| -| 26 Oct 00 UTC | 65 h | 72% | 28 Oct 09 UTC (27 Oct 21 -> 28 Oct 21) | 28 Oct 06 UTC, Clarendon | 96 kt (64-132) | 124 kt, peak 130 (23%) | 62% / 28% | 44 / 121 / 195 km (42 / 158 / 277; 34 / 123 / 124) | -| 26 Oct 12 UTC | 53 h | 83% | 28 Oct 12 UTC (06 -> 21) | 28 Oct 11 UTC, St Elizabeth | 101 kt (71-136) | 151 kt, peak 152 (19%) | 100% / 31% | 42 / 72 / 147 km (15 / 53 / 161; 40 / 54 / 73) | -| 27 Oct 00 UTC | 41 h | 83% | 28 Oct 15 UTC (09 -> 21) | 28 Oct 14 UTC, St Elizabeth, 18.06N 77.72W | 98 kt (78-121) | 149 kt, peak 160 (8%) | 100% / 23% | 54 / 71 / 212 km (38 / 69 / 215; 79 / 79 / 164) | - -Actual: landfall near New Hope, St Elizabeth (18.09N 77.97W) about 17 UTC on 28 October at 160 kt; peak 165 kt. Right: -the landfall point and probability, the parish ranking (St Elizabeth 77%, Manchester 69%, Westmoreland 60%, Clarendon -54%; the east 10-15%), the far-west rain (Westmoreland 413 mm if hit against the 309 mm gauge where 0.15 said 689; -Manchester 584 / 865 against 817; St Catherine 468 / 666 against 668). Wrong, said plainly: the intensity -- the aids -were all low on this storm and pulled the 48 h intensity to 85-100 kt against 105-165; the explosive case stayed a tail -event (the 95% peak quantile 133-140 kt against 165), as it did for the official forecast (140 kt at 24 h against 155); -parish timing about four hours early. - -## 5. Published - -* **PyPI**: `naturev1` 0.16.0 (wheel and sdist; install verified from the index). `pip install "naturev1[swarm,hub]"`. -* **Hugging Face** `CorxLabs/NatureV1-500`: the model card (section 4.17 carries the 0.16 tables), `melissa_landfall.png`, - `melissa_jamaica.png`, `benchmark_heldout_atlantic_2018_2020_2022.csv.gz` (849 cases, 120,472 rows: the truth and - every reference's forecast or error at every lead; `naturev1.cyclone.verify.compare` scores a new model on it). -* **Git**, branch `claude/linear-context-architecture-a35c6h` of `Wisk-inc/transformers`, `natureweather/`: commits - `ed86e0e` (rain shape, operations layer, infrared, audit), `5eae22b` (peak quantiles, arrival window, QPF member, - cell, roadmap), `b3d5a09` (the guidance layer), `4a5917b` (Melissa), `4ac6e53` (earned weights), `45e92bd` (the - outlier gate). Tests: 403 passed, 1 skipped, plus the 0.16 additions (`tests/test_adeck.py`, `test_peak.py`, - `test_operational.py`). -* **The cell**: `molab_swarm_cell.py`, one marimo cell, live or replay. - -## 6. Limits that remain, and the next steps - -* The explosive cases are tail events to every mean forecast here, ours and the NHC's; the answer stays in the - distribution (quantiles, the RI branch) until a predictor that separates them exists. The 0.12 satellite path is the - candidate: GOES structure and lightning as RI predictors need an archive of storm-centred imagery to fit. -* The guidance tables were fitted on Atlantic storms; an eastern Pacific storm gets the same weights, unverified there. - HWRF and HMON appear in about half the 2026 cycles; the blend renormalises and the record says what it used. -* The cone is sized for the track before the aids; with the aids the error is 11-13% smaller, so coverage runs a - little above nominal. A refit on the blended track is straightforward. -* The RI blend with the NHC schemes needs a tuning sample: build 2023-2025 as a new tuning set. -* A split swarm when ours and HCCA disagree beyond the cone (half the worlds on each track) would show both scenarios - in the threat map instead of a midpoint neither predicts; it needs a swarm re-verification on the 396 cases. -* Real-time data still to bring in: NESDIS ocean heat content, ECMWF ENS tracks (IFS-ENS, AIFS-ENS), HAFS-A/B tracks, - CIMSS ADT/SATCON/AiDT, NHC wind-speed probabilities, STOFS and SLOSH for surge. - -## 7. NatureV2-500 - -`CorxLabs/NatureV2-500` starts from the NatureV1-500 stage-1 checkpoint (`checkpoints/stage1/latest.pt`, with its -`config.json` and `normalizer.json`) and carries this code base. The plan: fine-tune the satellite encoder (the -GOES/GridSat band-13 path of 0.12, SatNet-Storm 2.0 and the global retrieval) on the storm-centred imagery, add the -new features the 0.16 verification asked for (infrared structure and its trend, lightning, the ocean heat feed) as -inputs to the storm heads and to the RI probability, and verify them on the same held-out seasons with the same -benchmark table before any of it changes a number in the operational path. diff --git a/ROADMAP_0.16.md b/ROADMAP_0.16.md deleted file mode 100644 index e1c2b601e42b23a0178e63dd536fc7798dd769b2..0000000000000000000000000000000000000000 --- a/ROADMAP_0.16.md +++ /dev/null @@ -1,55 +0,0 @@ -# naturev1 0.16: the engineering roadmap, closed out - -The six deficits of 0.15, what the archive said causes each, the experiment that decided each fix, and where it ended. -Every item was held to the same rule as everything before it: fitted on the tuning seasons (2016, 2017, 2019), scored -once on the held-out seasons (2018, 2020-2022), shipped only if it helped there, and reported either way. "Held-out" -below always means those seasons. The matrix the user supplied is kept, with the mechanics corrected where the archive -said otherwise -- a roadmap built on the wrong cause fixes the wrong thing. - -| # | deficit | what the data say causes it | fix | state | -|---|---|---|---|---| -| 1 | explosive peak gap: forecast peak 25-37 kt under the true peak of category-5 storms | not a loss-function artefact (the intensity is a simplex-weighted consensus, no network); it is regression to the mean under rapid-intensification uncertainty -- any weight that moves the mean toward the RI path costs the 94% of cases that do not explode | close it in the **distribution**, not the mean: calibrated peak quantiles (50/75/90/95%) and E[peak \| RI]; then the operational guidance as members | **shipped** (`peak_quantiles`, `peak_if_ri`): held-out q90 sits +4 kt above the true peak of the >=137 kt storms where the consensus max sits -37 kt; coverage 0.53/0.77/0.91/0.95; E[peak \| RI] +0.6 kt bias on the storms that went rapid. The a-deck blend (row 7) takes 26-35% off the intensity error at 24-120 h but moves the RI-case bias at 24 h only from -23 to -21 kt (the official forecast: -19): the gap is a property of every mean forecast, which is why the distribution products stay the answer | -| 2 | track: 2-7% behind FGN past 12 h | an equal-weight mean is diluted by weaker members; the live set has no FGN at all | dynamic per-storm model weighting from the last cycles' errors (tested: ~0); NNLS member weights (+0.3-1.1%, not shipped); the NHC a-deck's consensus aids as live members | **shipped** (row 7): the plain mean of our blend and HCCA, chosen on the tuning seasons, lowers the held-out Atlantic track error 11-13% at 12-120 h and beats the official forecast by 12-22% at 6-72 h | -| 3 | light-rain bloat from the rain multiplier | the tuning storms are wetter than the held-out ones (obs/forecast 1.9 vs 1.5), so a pooled multiplier runs high in the periphery; it is a per-storm scale error, largest in the cores of slow movers, not a radial-tail problem | profile reshaped (tail x1.5, front-right asymmetry 0.3, multiplier 1.19); the model's own rain as a member | **shipped**: reshape (held-out CSI 250 mm 0.295 -> 0.348, strong storms' area >=250 mm 3.2x -> 1.7x) and the GFS accumulated precipitation travelling with every world as half of its rain (radar check on US landfalls: Ida 23 vs 45 mm MAE, Nicholas 17 vs 55, Fred 21 vs 36, Ian level). Rejected on the tuning storms: every nonlinear heavy-favouring transform, a 115-kt saturation, a translation-speed factor | -| 4 | terrain and moisture: rain under the wind, not where two days of easterlies fed the Blue Mountains | a parametric profile has no moisture flux and no outer bands; the orographic term is a slope x wind product without moisture; the multiplier was calibrated on flat ground | reshaped profile; the model-QPF member, which carries the model's moisture and its own orography | **partly**: east of Melissa's track right since the reshape (Portland 470 vs 536 mm, St Catherine 624 vs 668, Kingston 447 vs 567); the QPF member is the moisture term; an integrated-vapour-transport orographic scheme needs a calibration set that does not exist yet | -| 5 | stalls over water held too strong | the continuous depth-aware cooling lost on the tuning seasons (15.3 -> 15.4-15.6 kt); a stall rule (speed < 3 kt for > 12 h, D26 < 50 m, -5 to -15 kt) was tested rather than assumed | penalty fitted on stall cases, with the deep-water sign check | **rejected**: the sign of the stall residual flips between the tuning and the held-out seasons, and the fixed rule is 45-122% worse on the stall cases themselves; Melissa stalled over 120 m of 26 C water and intensified. Nothing shipped; the wake cooling already in the SHIPS-style members stands | -| 6 | landfall timing 3-4 h early | hypothesis was a missing coastal slowdown; measured instead as along-track bias by speed, lead and distance to land | along/cross decomposition, speed-dependent correction, arrival-time check | **measured, no correction**: no systematic along-track bias (+1/+4/+5/+2/-1 km at 6-48 h); the arrival-time error is 3.6 h at 24 h overall and 7.3 h for storms moving under 3 m/s -- so the error is reported as a window (`ARRIVAL_TIME_ERROR`, in the landfall report) and nothing is nudged | -| 6b | the averaging penalty: a fixed 50/50 mean with HCCA drags a forecast that caught the turn toward a member that did not (Melissa 26 Oct 12 UTC: ours 15 km, HCCA 70, the mean 42) | on held-out, when the two disagree most it is our standalone that is wrong two times in three (24 h: ours 133 km, HCCA 93, the mean 94; in the extreme tenth ours 220, HCCA 101), so trusting the standalone in a disagreement loses on average | weights earned from the previous cycle's verified errors; an outlier gate with TVCN as tie-breaker; rules that favour ours, the median and trimmed mean of three | **shipped** (`ADECK_TRACK["adaptive"]`, `["gate"]`): earned split at 6-24 h (b 0.5, within 25-75%) and up to 10% of the pair away from the member that stands alone beyond 1.5 cone radii from 12 h -- together 1.4-2.5% lower held-out error at 12-36 h, 1-2% at 48-120 h, every lead significant; the disagreement reported per lead with both tracks in the record. **Rejected**: ours 2/3, a gate toward ours, the median and trimmed mean of ours/HCCA/TVCN (all lost on the tuning seasons) | -| 7 | the operational guidance was not in the room | the NHC a-deck carries every aid's track and intensity minutes after each cycle; nothing read it | `naturev1.swarm.adeck` reads the public a-deck (live and archive); `naturev1.cyclone.aids` blends the aids in with tables fitted on the tuning seasons | **shipped**: intensity -- simplex weights over our consensus, IVCN, HWRF, HMON, SHIPS, LGEM, GFS and HCCA (leave-one-season-out gains 13-28% at 24-96 h in every tuning season; held-out 26-35% at 24-120 h, 4-6% ahead of the official forecast at 24-72 h); track -- the mean with HCCA (row 2). HAFS-A/B stand in for HWRF/HMON and TVCN for HCCA when those are absent (lineage stand-ins, reported as such) | -| 8 | the NHC's rapid-intensification guidance was not in the room | the SHIPS text carries the SHIPS-RII, logistic, Bayesian, DTOPS and consensus probabilities live; the e-deck carries them after the fact | read both; verify against ours on the archive | **shown beside ours, not the headline**: the archive's e-decks carry no RI rows before 2020, so nothing could be chosen on the tuning seasons; on 2020-2022 (453 cases, 24 events) ours has Brier skill 0.13, the SHIPS-RII consensus 0.07, DTOPS 0.17 and the equal-weight mean of ours and DTOPS 0.20 -- that mean is reported in the record and the cell as `ri_probability_combined`, with this caveat attached | - -## Shipped in 0.16 - -* **The NHC guidance as members** -- `naturev1.swarm.adeck` (a-deck, e-deck, SHIPS text), `naturev1.cyclone.aids`, `coefficients.ADECK_TRACK` / `ADECK_INTENSITY`; `forecast_storm(guidance=True)` by default; `track_ours` / `intensity_ours` keep our own forecast beside the blend; the official forecast and the RI guidance in `run["guidance"]` and the cycle record. -* **The averaging penalty answered** -- the ours/HCCA split earned from the previous cycle and gated away from the outlier against TVCN; the disagreement per lead in `aids_track`. -* **Rain** -- `RAIN_SCALE = {multiplier 1.19, tail 1.5, asymmetry 0.3 front-right}`; orographic cap 2; the GFS QPF member (`models.fetch_qpf`, `qpf_weight` 0.5). -* **Peak quantiles and E[peak | RI]** -- `coefficients.PEAK_QUANTILES`, `PEAK_IF_RI`; in the landfall report ("plan for the 90% figure"). -* **Arrival-time window** -- `coefficients.ARRIVAL_TIME_ERROR`; in the landfall report. -* **Operations layer** -- `naturev1.swarm.bulletin` (every cycle saved as JSON + parish CSV; the previous cycle found again and fed back automatically), `naturev1.swarm.infrared` (GOES band-13 structure diagnostics with trend), `forecast_storm(previous="auto")`, the molab cell with its watch loop. -* **Audit fixes** -- missing physics context reported, swarm rescale keeps the longitude frame across the dateline, 24/48 h indices follow the caller's leads, silent fallbacks leave `intensity_note`, NHC-only ids, previous analysis position as the first feedback knot. -* **The swarm sized to the flow-dependent cone** (0.15.0, verified on 396 held-out forecasts). - -## Tried and rejected (kept here so nobody tries them again without new evidence) - -Stall penalty (row 5); translation-speed rain factor and 115-kt saturation; nonlinear rain transforms; dynamic per-storm -model weighting; NNLS physics-member weights (+0.3-1.1%, inside the noise); the RI-path mixture into the mean; the -continuous depth-aware wake cooling; CHIRPS as rain truth (too coarse at the parish scale); the fitted simplex over -the track aids (within 0.2% of the plain mean on the tuning seasons, so the mean ships). - -## Real-time data still to bring in (each needs a reader and a verification before it changes a number) - -* NESDIS ocean heat content (26 C depth, TCHP) on CoastWatch ERDDAP, daily -- replaces the climatology under the storm. -* ECMWF open-data TC-track BUFR for IFS-ENS, AIFS-single and AIFS-ENS (51 + 51 member tracks and intensities). -* HAFS-A/B tracks (3-hourly to 126 h, with radii) on NOMADS -- the a-deck already carries their interpolated runs. -* CIMSS ADT / SATCON / AiDT / D-MINT text feeds (30-minute satellite intensity, radii) and STAR SAR winds -- fixes with intensity for the particle filter. -* GLM lightning and GOES band-13 structure as RI predictors (an archive of storm-centred imagery is needed to fit them). -* NHC wind-speed probabilities and arrival-time grids, STOFS-2D water levels and the Jamaica SLOSH MOM layer for a surge pillar. - -## The benchmark we publish: the answer key - -`benchmark_heldout_atlantic_2018_2020_2022.csv.gz` on the model repository: the 849 held-out Atlantic cases (storm, -time, position, intensity) with the best-track truth at every lead and every reference's forecast or error on it -- -NatureV1 0.14/0.16 tracks and 0.15/0.16 intensities, the NHC official forecast and its aids (HCCA, TVCN, IVCN, HWRF, -HMON, SHIPS, LGEM, GFS, GEFS mean), the archived models' track errors (HRES, Pangu, GraphCast, FuXi, Aurora, FGN), -CLIPER and extrapolation. `naturev1.cyclone.verify.compare` scores any new forecast against it on the same storms, -with the storm-block bootstrap, so that a claim about a hurricane model can be checked rather than believed. diff --git a/b300_naturev1.py b/b300_naturev1.py deleted file mode 100644 index c507d7b74f374cba56fcdde3b521bae436de9c02..0000000000000000000000000000000000000000 --- a/b300_naturev1.py +++ /dev/null @@ -1,151 +0,0 @@ -# ═══════════════════════════════════════════════════════════════════════════════════════════════════ -# NatureV1 0.12 on 8x NVIDIA B300 -- the satellite path, then every basin. -# -# Cells for JupyterLab or VS Code ("# %%"), or run top to bottom as a script. Every long step runs as -# its own detached process with a log under DATA/logs: closing the notebook or losing the connection -# never stops it, and running the same cell again reattaches to it instead of starting it twice. -# -# What happens, and roughly how long it takes on 8x B300 with a fast NVMe array and a fast network: -# -# 1 install, preflight minutes checks GPUs, disks, network, HF token; measures a step -# 2 base model + statistics from the Hub minutes the trained 0.10/0.11 release is the starting point -# 3 labels minutes IBTrACS since 1980, every basin: ~230,000 storm fixes -# 4 stage the record (two jobs at once) 1-4 h ERA5 1980-2021 (~0.33 TB) and GridSat-B1 1980-2021 -# (~4-6 TB read, ~0.8 TB of training shards written) -# 5 stage A, 3 h: both satellite networks 3 h GPUs 0-3: the storm network; GPUs 4-7: the retrieval -# 6 stage B: heads + backbone tail 2 h + ~1 every basin, satellites in the loop; then banks with -# rollout members, consensus, calibration, test scores -# 7 publish minutes only replaces the release if it beat the old one -# -# Every checkpoint of every run is pushed to HF_REPO as it is written (checkpoints//latest.pt, and -# the weights of each one under checkpoints//history/), so a dead machine resumes on a new one. -# -# The training time is the GPU budget you set (STAGE_A_HOURS, STAGE_B_HOURS); staging is separate and -# bound by network and disk, not by the GPUs. Nothing here has been run on a B300 by its author -- the -# code is tested end to end on CPU (including real two-process data parallelism); the preflight is the -# first thing that touches your hardware, and it says so if anything is wrong. -# ═══════════════════════════════════════════════════════════════════════════════════════════════════ - -# %% [0] settings ────────────────────────────────────────────────────────────────────────────────────── -HF_REPO = "CorxLabs/NatureV1-500" -HF_TOKEN = "" # paste a WRITE token here for this session, then clear it afterwards -DATA = "/data/naturev1" # the NVMe array: >= 2 TB free (staged shards + ERA5 + working space) -FIRST_YEAR = 1980 # GridSat-B1 begins 1980 -LAST_YEAR = 2021 # WeatherBench 2's 1.5-degree ERA5 ends 2021-12-31 -SAT_RESOLUTION = 0.25 # degrees of the staged global frames (0.1 writes ~4 TB of shards instead of ~0.8) -STAGE_A_HOURS = 3.0 # both satellite networks, side by side, four GPUs each -STAGE_B_HOURS = 2.0 # storm heads + backbone tail on all eight GPUs (banks and scoring come after) -TAIL_BLOCKS = 4 # backbone blocks fine-tuned in stage B (of 17) -ROTATIONS = 4 # test-time rotation ensemble kept in the banks (1: off); used only if it helps -STAGING_WORKERS = None # None: all CPUs but 16 -KEEP_RAW = False # keep the raw GridSat files (4-6 TB) after staging -BRANCH = "claude/linear-context-architecture-a35c6h" - -# %% [1] install and preflight ────────────────────────────────────────────────────────────────────────── -import os, subprocess, sys -from pathlib import Path - -REPO_URL = "git+https://github.com/Wisk-inc/transformers@" + BRANCH -subprocess.run([sys.executable, "-m", "pip", "install", "-q", "--upgrade", - f"ihelix @ {REPO_URL}#subdirectory=ihelix", - f"naturev1[all] @ {REPO_URL}#subdirectory=natureweather", - "safetensors", "netCDF4", "h5netcdf"], check=True) -if HF_TOKEN.strip(): - os.environ["HF_TOKEN"] = HF_TOKEN.strip() # inherited by every job below; never written to disk -os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "0") -DATA = Path(DATA) -LOGS, SAT, CKPT = DATA / "logs", DATA / "satellite", DATA / "runs" -for folder in (LOGS, SAT, CKPT): - folder.mkdir(parents=True, exist_ok=True) - -import naturev1 -from naturev1.hardware import hardware_preflight - -print(f"naturev1 {naturev1.__version__}") -READY = hardware_preflight(DATA, HF_REPO, need_tb=1.6 if SAT_RESOLUTION >= 0.25 else 5.0, hours=STAGE_A_HOURS) -assert READY, "fix the lines marked !! above before spending GPU hours" - -# %% [2] the starting model and its statistics, from the Hub ─────────────────────────────────────────── -import shutil -import torch -from huggingface_hub import hf_hub_download - -BASE = DATA / "base.pt" -if not BASE.exists(): - for tag in ("stage3", "stage2"): # the newest released stage - try: - shutil.copy2(hf_hub_download(HF_REPO, f"checkpoints/{tag}/latest.pt"), BASE) - print(f"starting from checkpoints/{tag}/latest.pt") - break - except Exception as error: - print(f"no {tag} on the Hub ({type(error).__name__})") -STATS = DATA / "stats.json" -if not STATS.exists(): - shutil.copy2(hf_hub_download(HF_REPO, "staging/stats.json"), STATS) -base = torch.load(BASE, map_location="cpu", weights_only=False) -print("base config:", {k: base["config"][k] for k in ("hidden_size", "num_layers", "analysis_channels", "state_channels")}) -ANALYSIS_CHANNELS = base["config"]["analysis_channels"] -TENDENCY = DATA / "tendency.pt" -recorded = base["model"].get("state_tendency") -if recorded is not None and float(recorded.abs().sum()) > 0: - torch.save(recorded.float(), TENDENCY) - print("tendency scale: recorded in the checkpoint") -del base - -# %% [3] labels: every basin since 1980 ──────────────────────────────────────────────────────────────── -import numpy as np -from naturev1.ibtracs import describe_table, fetch_ibtracs, parse_ibtracs, storm_label_table - -IBTRACS = fetch_ibtracs(DATA) -LABELS = SAT / "labels.npz" -if not LABELS.exists(): - np.savez(LABELS, **storm_label_table(parse_ibtracs(IBTRACS, since=FIRST_YEAR, until=LAST_YEAR))) -print(describe_table(dict(np.load(LABELS)))) - -# %% [4] stage the record: ERA5 and GridSat, as two detached jobs ────────────────────────────────────── -from naturev1.b300 import follow, start_job -from naturev1.satstage import staging_plan - -print(staging_plan(range(FIRST_YEAR, LAST_YEAR + 1), SAT_RESOLUTION, len(np.load(LABELS)["time"]))) -ERA5 = DATA / f"era5_{FIRST_YEAR}_{LAST_YEAR}.npy" -start_job("era5", [sys.executable, "-c", - f"from naturev1.b300 import stage_era5_record; " - f"stage_era5_record({str(DATA)!r}, {FIRST_YEAR}, {LAST_YEAR}, {str(STATS)!r}, workers=32)"], LOGS) -workers = STAGING_WORKERS or max((os.cpu_count() or 32) - 16, 4) -start_job("gridsat", [sys.executable, "-m", "naturev1.satstage", "--root", str(SAT), "--ibtracs", str(IBTRACS), - "--years", f"{FIRST_YEAR}-{LAST_YEAR}", "--workers", str(workers), - "--resolution", str(SAT_RESOLUTION)] + (["--keep-raw", "--raw-dir", str(DATA / "gridsat_raw")] - if KEEP_RAW else []), LOGS) -follow(LOGS, ["era5", "gridsat"]) - -# %% [5] stage A: the storm network on GPUs 0-3, the retrieval on GPUs 4-7, side by side ───────────────── -if not TENDENCY.exists(): # a checkpoint from before 0.11 does not record it: measure it - from naturev1.b300 import memmap_tendency - - torch.save(memmap_tendency(ERA5, ANALYSIS_CHANNELS), TENDENCY) -common = ["--checkpoint", str(BASE), "--root", str(SAT), "--table", str(LABELS), "--output", str(CKPT), - "--hours", str(STAGE_A_HOURS), "--hub-repo", HF_REPO] -start_job("stage-a-satnet", ["-m", "naturev1.sattrain", "satnet", *common, "--workers", "12"], LOGS, - gpus=[0, 1, 2, 3]) -start_job("stage-a-retrieval", ["-m", "naturev1.sattrain", "retrieval", *common, "--era5", str(ERA5), - "--tendency", str(TENDENCY), "--workers", "10"], LOGS, gpus=[4, 5, 6, 7]) -follow(LOGS, ["stage-a-satnet", "stage-a-retrieval"]) - -# %% [6] stage B: storm heads + the backbone's last blocks, every basin, satellites in the loop ─────────── -start_job("stage-b", ["-m", "naturev1.stormtrain", "--base", str(BASE), - "--satnet", str(CKPT / "stage-a-satnet" / "latest.pt"), - "--retrieval", str(CKPT / "stage-a-retrieval" / "latest.pt"), - "--era5", str(ERA5), "--tendency", str(TENDENCY), "--ibtracs", str(IBTRACS), - "--table", str(LABELS), "--root", str(SAT), "--output", str(CKPT), - "--hours", str(STAGE_B_HOURS), "--tail", str(TAIL_BLOCKS), "--rotations", str(ROTATIONS), - "--hub-repo", HF_REPO], LOGS, - gpus=list(range(8))) -follow(LOGS, ["stage-b"]) -print((CKPT / "stage-b" / "report.txt").read_text() if (CKPT / "stage-b" / "report.txt").exists() else "no report yet") - -# %% [7] publish: the release only if it beat the model it started from ────────────────────────────────── -from naturev1.b300 import publish_release - -where = publish_release(CKPT / "stage-b" / "latest.pt", STATS, HF_REPO, DATA / "release", - comparison=CKPT / "comparison.json", report=CKPT / "stage-b" / "report.txt") -print("published to", where, "-- now clear HF_TOKEN in cell [0]") diff --git a/colab_naturev1.py b/colab_naturev1.py deleted file mode 100644 index 34319c17c832eb44276fd3cafb216978211e8af7..0000000000000000000000000000000000000000 --- a/colab_naturev1.py +++ /dev/null @@ -1,766 +0,0 @@ -# ═══════════════════════════════════════════════════════════════════════════════════════════════════ -# NatureV1 0.11.0 — the whole thing in one cell. Pure Python: marimo, Colab, Jupyter or a plain script. -# -# Paste and run. It installs what it needs, stages the data, builds the 89M-parameter model, trains it -# in stages, scores itself against persistence and climatology, and only publishes if it earned it. -# -# With BACKGROUND = True (the default) the run happens in a supervised process on the machine itself: -# it keeps going if the wifi drops or the notebook closes, and it restarts itself from its last -# checkpoint if it crashes, runs out of GPU memory, or stalls. The cell then stays running as a live -# window on it -- every log line as it happens, a proof-of-life line every minute when the run is -# quiet, a status line every ten minutes -- until the run finishes (WATCH_SECONDS = None). Stopping the -# cell stops only the window; the run never depends on it. Run the cell again any time to reopen it. -# -# If it stops for any reason -- a crash, a reboot, naturev1.stop(), the supervisor giving up -- just run -# this cell again: every stage continues from its last checkpoint (saved every minute, a snapshot every -# 30 minutes, and a second copy in BACKUP_DIR). naturev1.progress() shows where it will continue from. -# -# ON A HOSTED NOTEBOOK (molab, Colab): the run survives the notebook, but not the machine. molab ends a -# session after 12 hours, or after 90 idle minutes, and wipes its disk. So, with HF_TOKEN set, everything -# expensive is mirrored to HF_REPO as soon as it exists: the staged ERA5 (once, in the background), the -# checkpoints (every 10 minutes, and once more when the session is told to stop), the storm feature -# banks, the remembered batch size. A new session pulls them back in minutes instead of rebuilding them: -# the download does not start over, training resumes where the last session stopped. On molab: set -# HF_TOKEN, keep this tab open (the cell staying active is what keeps the notebook from being idle), and -# when the 12 hours are up, open a new session and run this cell again. -# -# 0.9: the hurricane heads read the storm's physical environment -- deep-layer steering, 200-850 hPa -# shear, mid-level moisture, ocean temperature -- computed from the model's own atmosphere, plus a -# wider 1000 km pool. The track head predicts the residual from a physics-only steering track, and the -# forecast is a multi-model consensus of learned track, steering track and extrapolation, weighted per -# lead on held-out seasons. The scorecard shows every member, so you can see which one earns its place. -# -# 0.11: stage three -- what the satellite sees. Storm-centred infrared (HURSAT-B1 to 2016, GridSat-B1 -# after, GOES band 13 live) feeds a small branch into the hurricane heads, aimed at intensity, which a -# 1.5-degree reanalysis cannot see. It is fine-tuned with the backbone frozen and can never end worse on -# held-out storms than the model it started from. Then, from the red team: landfall and rapid- -# intensification probabilities are calibrated on held-out seasons, an input monitor marks forecasts -# from inputs unlike training as untrustworthy, the forecast can no longer state impossible structure, -# and the rollout's tendency statistics ship with the weights. Stages already trained are loaded, not -# redone: a finished 0.10 run goes straight to stage three. -# ═══════════════════════════════════════════════════════════════════════════════════════════════════ - -BACKGROUND = True # supervised background run: survives disconnects, restarts on crash or stall -STALL_MINUTES = 30 # background: restart a run that shows no progress for this long (12 while downloading) -WATCH_SECONDS = None # None: this cell stays running as a live window until the run finishes; or seconds -MIRROR = True # mirror staged data, checkpoints and banks to HF_REPO (needs HF_TOKEN); see above - -RUN_PRETRAIN = True # stage one: every channel at every lead, on ERA5 reanalysis -RUN_ROLLOUT = True # stage 1b: the whole atmosphere rolled forward 12 steps, CRPS ensemble -RUN_STORMS = True # stage two: hurricane heads on HURDAT2, backbone frozen -RETRAIN_STORMS = False # stage two: train the heads again even if a stage-two checkpoint exists -RUN_SATELLITE = True # stage three: storm-centred infrared satellite, then calibration and the monitor -RETRAIN_SATELLITE = False # stage three: train again even if a stage-three checkpoint exists -RUN_SCORE = True # WeatherBench-style scorecards against persistence and climatology -RUN_HINDCAST = True # forecast a held-out hurricane from its real analysis; compare with truth -RUN_PUBLISH = False # push to the Hub -- only if it beats persistence somewhere -RUN_FORECAST = False # live GOES forecast (refused while the satellite path is untrained) -RUN_WATCHER = False # then re-forecast every hour - -STAGE_YEARS = 5 # training years staged to local disk, ~7.5 GB each at 89 channels -VAL_YEARS = 1 # held-out years staged too, so validation and scoring are not network-bound -EPOCHS = 8 # passes over the staged years in stage one -BATCH = None # None: find the largest batch that fits once, then remember it; or a number -MAX_BATCH = 16 # ceiling on the batch search: host RAM, not VRAM, is the limit above this -WORKERS = 4 # data-loading processes -ROLLOUT_STEPS = 12 # longest rollout trained: 12 x 6 h = 72 h, GraphCast's curriculum -ROLLOUT_TRAIN = 4_000 # optimizer steps of rollout training (the log prints the time per step) -MEMBERS = 2 # CRPS ensemble members; 1 = deterministic Gaussian training -STORM_FIRST_SEASON = 1979 # the satellite era; earlier best-track intensities are much less reliable -HINDCAST = ("DORIAN", 2019) # a held-out storm: 2019 is never trained on -STORM = (24.6, -78.2) # live forecast: current storm centre (lat, lon) -CITY = (25.77, -80.19) # live forecast: somewhere you want a local forecast -HF_REPO = "CorxLabs/NatureV1-500" -HF_TOKEN = "" # a write token, to publish and mirror checkpoints; or set the HF_TOKEN env var -DATA_DIR = None # None: /content on Colab, ~/naturev1_data elsewhere; or any folder with room -BACKUP_DIR = None # second copy of every checkpoint. None: ~/naturev1_backup; best on another disk; - # "" turns it off - -# ═══ 0 ═══ install ═════════════════════════════════════════════════════════════════════════════════ -# Standard library only, before numpy or torch are imported: pip may upgrade numpy while satisfying -# zarr or gcsfs, and a numpy that changes under an imported torch fails far from here. -import importlib -import importlib.metadata -import importlib.util -import os -import shutil -import subprocess -import sys - -os.environ.setdefault("GRPC_VERBOSITY", "ERROR") # the cloud client logs every DataLoader fork otherwise -# Let the GPU allocator grow segments instead of fragmenting them. Training at batch 8 fills a 96 GB card -# to within ~130 MB, and without this the allocator has to flush and retry. Only takes effect if torch -# has not touched the GPU yet in this kernel -- restart the kernel for it to apply. -os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") - - -def _halt(message, kind="danger"): - """ - End the cell here, on purpose. Only kind="danger" is an error. marimo shows the message as a - coloured note instead of a traceback, Jupyter and Colab show it without one, and a plain script - exits with status 0 for "success" and 1 for anything else -- which is what the supervisor of a - background run reads, so a run stopped part-way is resumed and a finished one is not. - """ - marimo = sys.modules.get("marimo") - if marimo is not None and getattr(marimo, "running_in_notebook", lambda: False)(): - try: - note = marimo.callout(marimo.plain_text(message), kind=kind) - except Exception: - note = None - print(message) - marimo.stop(True, note) - ipython = sys.modules.get("IPython") - if kind != "danger" and ipython is not None and getattr(ipython, "get_ipython", lambda: None)() is not None: - class Halted(Exception): - def _render_traceback_(self): - return [] # the message printed above, and no traceback - print(message) - raise Halted("the cell stopped here on purpose -- the message is above") - if kind == "success": - print(message) - raise SystemExit(0) - raise SystemExit(message) - - -# 0.11 is installed from the project's GitHub branch until it is on PyPI. -_NEEDED = {"naturev1": ("naturev1[all] @ git+https://github.com/Wisk-inc/transformers" - "@claude/linear-context-architecture-a35c6h#subdirectory=natureweather", (0, 11, 0)), - "ihelix": ("ihelix>=0.5.1", (0, 5, 1))} - - -def _version(module): - try: - return tuple(int(p) for p in importlib.metadata.version(module).split(".")[:3]) - except Exception: - return None - - -_missing = [req for mod, (req, low) in _NEEDED.items() - if importlib.util.find_spec(mod) is None or (_version(mod) or (0,)) < low] -if _missing: - print(f"installing: {' '.join(_missing)}") - _loaded = {name: getattr(sys.modules.get(name), "__version__", None) for name in ("numpy", "torch")} - _tries = ([("uv", ["uv", "pip", "install", "--python", sys.executable, "--upgrade", *_missing])] - if shutil.which("uv") else []) - _pip = [sys.executable, "-m", "pip", "install", "--upgrade"] - _tries += [("pip, fresh index", [*_pip, "--no-cache-dir", "--index-url", "https://pypi.org/simple", *_missing]), - ("pip, no cache", [*_pip, "--no-cache-dir", *_missing]), - ("pip", [*_pip, *_missing])] - for _label, _command in _tries: - try: - subprocess.check_call(_command) - print(f"installed via {_label}") - break - except (subprocess.CalledProcessError, FileNotFoundError): - print(f" {_label} failed, trying the next way") - else: - _halt("could not install; add these in your package manager, then re-run: " + " ".join(_missing)) - importlib.invalidate_caches() - # A restart is needed only if an old copy is already in memory, or pip moved numpy/torch under us. - _stale = [m for m in _NEEDED if m in sys.modules] - for _name, _was in _loaded.items(): - try: - if _was and importlib.metadata.version(_name) != _was: - _stale.append(f"{_name} {_was}") - except importlib.metadata.PackageNotFoundError: - pass - if _stale: - _halt(f"installed. {', '.join(_stale)} was already loaded in this kernel: RESTART THE KERNEL, then run " - "this cell again. (A background run that is already training is not affected.)", kind="warn") - -if HF_TOKEN: - os.environ["HF_TOKEN"] = HF_TOKEN -# Every checkpoint is also copied here, so losing or damaging the data folder does not lose the run. -os.environ["NATUREV1_BACKUP_DIR"] = (os.path.expanduser("~/naturev1_backup") if BACKUP_DIR is None - else os.path.expanduser(BACKUP_DIR)) - -if BACKGROUND and not os.environ.get("NATUREV1_BACKGROUND_CHILD"): - # Hand this same pipeline, with these settings, to a supervised process on this machine, and stop - # here: the notebook only starts and watches it. The GPU is never touched from the notebook. - # Imported under a private name: marimo lets a name be defined by only one cell, and this way any - # other cell can still `import naturev1` to call naturev1.follow() or naturev1.stop(). - import naturev1 as _naturev1 - - _SETTINGS = {name: globals()[name] for name in ( - "RUN_PRETRAIN", "RUN_ROLLOUT", "RUN_STORMS", "RETRAIN_STORMS", "RUN_SATELLITE", "RETRAIN_SATELLITE", - "RUN_SCORE", "RUN_HINDCAST", "RUN_PUBLISH", - "RUN_FORECAST", "RUN_WATCHER", "STAGE_YEARS", "VAL_YEARS", "EPOCHS", "BATCH", "MAX_BATCH", "WORKERS", - "ROLLOUT_STEPS", "ROLLOUT_TRAIN", "MEMBERS", "STORM_FIRST_SEASON", "HINDCAST", "STORM", "CITY", - "HF_REPO", "HF_TOKEN", "DATA_DIR", "BACKUP_DIR", "MIRROR")} - _was_running = _naturev1.status(DATA_DIR) is not None - _naturev1.launch(directory=DATA_DIR, stall_minutes=STALL_MINUTES, **_SETTINGS) - if _was_running: - _naturev1.progress(DATA_DIR) # what the run that is already going has done so far - # A fresh run: wait for its first lines. Then show the log live for a while, so the cell ends with - # the run visibly moving rather than with a snapshot that looks frozen. - _naturev1.follow(lines=25, directory=DATA_DIR, wait=90, watch=WATCH_SECONDS) - if _naturev1.status(DATA_DIR) is None: - _naturev1.progress(DATA_DIR) - _halt("The run has ended -- the last lines above say whether it finished or stopped. If it finished, " - "the scorecards and the hindcast are above. If it stopped, run this cell again: it continues " - "from the last checkpoint, pulling data, checkpoints and banks back from the Hub mirror if this " - "machine's disk was wiped.", kind="success") - _halt("NatureV1 is running in the background; this cell was its window and has closed -- the run has " - "NOT stopped. Nothing in this notebook can stop it except naturev1.stop().\n" - "To reopen the window: run this cell again (it will not start a second run), or in any cell\n" - " import naturev1; naturev1.follow(watch=None)\n" - "If the run ever does stop, run this cell again: it continues from the last checkpoint, pulling " - "data, checkpoints and banks back from the Hub mirror if this machine's disk was wiped.\n" - "naturev1.progress() shows each stage's checkpoint, its age, and its backup.", - kind="success") - -# ═══ 1 ═══ imports, paths, hardware ════════════════════════════════════════════════════════════════ -import datetime as dt -import json - -import numpy as np -import torch -from ihelix import FieldGrid, Geometry, fibonacci_sphere -from naturev1 import (HEADLINE_STATE_FIELDS, CachedERA5, CheckpointManager, ERA5Window, HubMirror, NatureConfig, - NatureV1, RolloutSchedule, StateStepper, Trainer, TrainSettings, autotune_batch_size, - benchmark_steps, build_climatology, build_forecast, calendar_features, catalogue, - channel_loss_weights, check_forecast, check_grid_alignment, check_normalization, - check_weights, corpus_scale, describe_environment, device_report, era5_loader, - era5_source_grid, era5_splits, expand_variables, extrapolation_track, fetch_latest, - format_pairing, format_plan, format_preflight, lead_offsets, masked_gaussian_nll, - materialise, normalize_channels, open_weatherbench_levels, pair_tracks_with_reanalysis, - parse_hurdat2, preflight, push_to_hub, rapid_intensification, resolve_all, - responds_to_input, scene_from_netcdf, score_model, score_state, scoring_fields, - split_by_storm, spread_skill_ratio, state_forecast, steering_track, storm_environment, - storm_feature_bank, storm_scorecard, storm_state_vector, train_state_rollout, - train_storm_heads, training_plan, upper_air_report, watch) -from naturev1.besttrack import download -from naturev1.checkpoint import TrainingState -from naturev1.era5 import _target_index -from naturev1.storms import calibration_report, fit_feature_monitor, fit_probability_calibration -from naturev1.stormsat import (IBTRACS_NA_URL, GridSatArchive, HursatArchive, describe_patch, encode_bt, - ibtracs_atcf_map, satellite_bank) -from naturev1.safeio import atomic_write_text, read_json - -DEVICE = "cuda" if torch.cuda.is_available() else "cpu" -COLAB_DRIVE = "/content/drive/MyDrive" -DATA_DIR = DATA_DIR or ("/content" if os.path.isdir("/content") else os.path.expanduser("~/naturev1_data")) -CKPT_DIR = (f"{COLAB_DRIVE}/naturev1_ckpt" if os.path.isdir(COLAB_DRIVE) # Drive outlives the VM - else os.path.join(DATA_DIR, "naturev1_ckpt")) -os.makedirs(DATA_DIR, exist_ok=True) -os.makedirs(CKPT_DIR, exist_ok=True) -CACHE, VAL_CACHE, STATS = (os.path.join(DATA_DIR, name) for name in ("era5.npy", "era5_val.npy", "stats.json")) -# Everything expensive goes to the Hub as soon as it exists and comes back at the start of a new session, -# so a wiped disk costs minutes, not hours. Off without a repo or token; see the header. -mirror = HubMirror(HF_REPO if MIRROR else None, token=HF_TOKEN or None) - - -def banner(text): - print(f"\n{'═' * 99}\n {text}\n{'═' * 99}") - - -banner("HARDWARE") -print(device_report()) -print(f"data {DATA_DIR} | checkpoints {CKPT_DIR}\n{mirror.describe()}") - -# ═══ 2 ═══ data: 13 pressure levels + surface + static, resolved against this machine ══════════════ -banner("DATA — ERA5 (WeatherBench 2), surface and 13 pressure levels") -ERA5, SURFACE, UPPER = open_weatherbench_levels() -LEVELS = [int(x) for x in ERA5.level.values] -SRC, LAT, LON = era5_source_grid(ERA5) -plan = resolve_all(ERA5, tuple(SURFACE) + tuple(UPPER), LEVELS, STAGE_YEARS + VAL_YEARS, SRC.num_points, - layers=17, batch=MAX_BATCH, precision="bf16", path=DATA_DIR, - staged=(CACHE, VAL_CACHE)) # data already on disk counts as room, not as used -print(plan.summary()) -R = plan.resolved -CHANNELS = expand_variables(ERA5, R["variables"], R["levels"]) -WIDTH = len(CHANNELS) + 7 # room for observation-mask channels, zero-padded -print(f"\n{upper_air_report(SURFACE, UPPER, LEVELS)}") -assert not check_weights(SRC), check_weights(SRC) # poles present, not deleted by cos(latitude) - -OFFSETS, SPLITS = lead_offsets((6, 12, 18, 24, 36, 48, 72, 96, 120)), era5_splits(ERA5) -TRAIN_STEPS = max(R["years"] - VAL_YEARS, 1) * 1460 -VAL_STEPS = VAL_YEARS * 1460 -common = dict(history=6, lead_steps=OFFSETS, variables=tuple(R["variables"]), levels=R["levels"], channels=WIDTH) -for name, index in SPLITS.items(): - print(f" {name:5} {str(ERA5.time.values[index[0]])[:10]} -> {str(ERA5.time.values[index[-1]])[:10]}") -print("split by TIME: weather is autocorrelated for days, so a random split leaks the answer.") - -mirror.pull(STATS, "staging/stats.json") # the normalisation statistics, from an earlier session -stream = ERA5Window(ERA5, indices=SPLITS["train"], stats_cache=STATS, **common) -mirror.push(STATS, "staging/stats.json") -print(f"\n{stream.normalizer.report()}") -print(f"\n{corpus_scale(ERA5, tuple(R['variables']))}\n\n{catalogue()}") - -if R["streaming"]: - print("\nnot enough disk to stage: streaming from the network (~100x slower per window)") - train_ds, roll_ds = stream, ERA5Window(ERA5, indices=SPLITS["train"], stats_cache=STATS, - state_steps=ROLLOUT_STEPS, **common) - val_ds = ERA5Window(ERA5, indices=SPLITS["val"], augment=False, stats_cache=STATS, - state_steps=ROLLOUT_STEPS, **common) -else: - # Always called, and never downloads twice: it returns at once when what is on disk already covers - # the request, copies from disk what it can when the request grows, and resumes a download that died. - TRAIN_SPAN, VAL_SPAN = SPLITS["train"][-TRAIN_STEPS:], SPLITS["val"][:VAL_STEPS] - # A staging an earlier session already made comes back from the Hub in minutes; it is pushed there, - # in the background, the first time it is complete. Then materialise() reuses, grows or resumes it. - mirror.pull_staging(CACHE, "staging/era5", CHANNELS, TRAIN_SPAN) - mirror.pull_staging(VAL_CACHE, "staging/era5_val", CHANNELS, VAL_SPAN) - materialise(ERA5, TRAIN_SPAN, CACHE, variables=tuple(R["variables"]), - levels=R["levels"], normalizer=stream.normalizer, workers=8) - materialise(ERA5, VAL_SPAN, VAL_CACHE, variables=tuple(R["variables"]), - levels=R["levels"], normalizer=stream.normalizer, workers=8) - mirror.push_staging(CACHE, "staging/era5") - mirror.push_staging(VAL_CACHE, "staging/era5_val") - train_ds = CachedERA5(CACHE, history=6, lead_steps=OFFSETS, channels=WIDTH, augment=True, - restrict_to=TRAIN_SPAN) - roll_ds = CachedERA5(CACHE, history=6, lead_steps=OFFSETS, channels=WIDTH, augment=True, - state_steps=ROLLOUT_STEPS, restrict_to=TRAIN_SPAN) - val_ds = CachedERA5(VAL_CACHE, history=6, lead_steps=OFFSETS, channels=WIDTH, augment=False, - state_steps=ROLLOUT_STEPS, restrict_to=VAL_SPAN) -print(f"\ntrain {len(train_ds):,} windows | validate on {len(val_ds):,} held-out windows") - -sample = train_ds[0]["analysis"] -for problem in (check_grid_alignment(SRC, sample[-1, :, train_ds.variables.index("2m_temperature")]) - + check_normalization(sample, "analysis")): - _halt(f"DATA PROBLEM: {problem}") -print("data ok: samples land where the grid says, no spike channels") - -# ═══ 3 ═══ what each channel IS — decides what a rollout does with it ══════════════════════════════ -stepper = StateStepper.build(roll_ds, input_channels=WIDTH, grid=SRC, samples=64) -print(f"\nchannel roles:\n{stepper.roles.describe()}") -WEIGHTS = channel_loss_weights(train_ds.variables, WIDTH) - -# ═══ 4 ═══ the model ═══════════════════════════════════════════════════════════════════════════════ -banner("MODEL") -CFG = NatureConfig( - analysis_channels=WIDTH, satellite_channels=6, environment_channels=8, - hidden_size=512, num_layers=17, num_heads=8, num_kv_heads=4, head_dim=64, intermediate_size=2048, - latent_points=4096, min_radius_km=120.0, max_radius_km=1600.0, history_frames=6, - lead_times_hours=(6, 12, 18, 24, 36, 48, 72, 96, 120), track_modes=6, - state_channels=stepper.roles.num_prognostic, # predict and evolve the whole atmosphere - noise_dim=32 if MEMBERS > 1 else 0) # per-member noise for the CRPS ensemble -model = NatureV1(CFG, fibonacci_sphere(CFG.latent_points, num_neighbours=CFG.latent_neighbours, - cluster_size=CFG.latent_cluster)).to(DEVICE) -model.gradient_checkpointing_enable(True) -stepper.grid = SRC.to(DEVICE) -print(f"NatureV1 {model.num_parameters()/1e6:.2f}M parameters on {DEVICE}; state head over " - f"{CFG.state_channels} channels; {MEMBERS}-member ensemble") -print(format_preflight(preflight(model=model, dataset=train_ds, grid=SRC, strict=True))) - - -def restore(*stages): - """Load the newest checkpoint of the first stage that has one -- locally, else from the Hub.""" - for stage in stages: - manager = CheckpointManager(f"{CKPT_DIR}/{stage}", repo_id=HF_REPO) - manager.fetch_from_hub() - if manager.load(model, map_location=DEVICE) is not None: - return stage - return None - - -# ═══ 5 ═══ the largest batch that fits, and what the run costs ═════════════════════════════════════ -banner("BENCHMARK — largest batch that fits, and what the run costs") - - -def make_step(batch_size): - items = [train_ds[i] for i in range(batch_size)] - batch = {k: torch.stack([x[k] for x in items]).to(DEVICE) for k in ("analysis", "calendar", "field_target", "field_mask")} - - def step(): - with torch.autocast(DEVICE, dtype=torch.bfloat16, enabled=DEVICE == "cuda"): - out = model(analysis=batch["analysis"], analysis_grid=SRC, calendar=batch["calendar"], output_grid=SRC) - loss = masked_gaussian_nll(out["field_mean"], out["field_log_var"], - torch.where(batch["field_mask"] > 0, batch["field_target"], torch.nan)) - loss.backward() - model.zero_grad(set_to_none=True) - return step - - -# The search is done once and remembered: repeating it on every restart costs minutes, pushes the card -# to the edge of its memory each time, and could pick a different batch -- a different run -- each time. -BATCH_FILE = os.path.join(CKPT_DIR, "batch.json") -mirror.pull(BATCH_FILE, "staging/batch.json") -REMEMBERED = read_json(BATCH_FILE, default={}) if BATCH is None else {} # a damaged file is searched again -if REMEMBERED.get("batch"): - BATCH, SPEED = int(REMEMBERED["batch"]), float(REMEMBERED.get("samples_per_second", 0.0)) - print(f"batch {BATCH}, found on an earlier run (delete {BATCH_FILE} to search again)") -else: - if BATCH is None: - BATCH = min(autotune_batch_size(make_step, start=1, target_fraction=0.85), MAX_BATCH) - MARK = benchmark_steps(make_step(BATCH), BATCH, warmup=1, iterations=10, gradient_checkpointing=True) - SPEED = MARK.samples_per_second - atomic_write_text(BATCH_FILE, json.dumps({"batch": BATCH, "samples_per_second": SPEED})) - mirror.push(BATCH_FILE, "staging/batch.json") - print(f"batch {BATCH} | {MARK}") -if SPEED: - print(format_plan(training_plan(SPEED, corpus_samples=len(train_ds), epochs=EPOCHS, - watts=600.0, electricity_per_kwh=0.15, cloud_per_hour=2.50))) -if DEVICE == "cuda": - torch.cuda.empty_cache() # hand back what the search held before training starts -loader = era5_loader(train_ds, batch_size=BATCH, num_workers=WORKERS, analysis_grid=SRC, output_grid=SRC, - prefetch_factor=2) -val_loader = era5_loader(val_ds, batch_size=BATCH, num_workers=WORKERS, shuffle=False, analysis_grid=SRC, - output_grid=SRC, prefetch_factor=2) - -# ═══ 6 ═══ stage one — every channel, every lead ═══════════════════════════════════════════════════ -if RUN_PRETRAIN: - banner("STAGE ONE — pretraining on reanalysis") - trainer = Trainer(model, TrainSettings( - stage="pretrain", learning_rate=3e-4, warmup_steps=1000, - precision=R["precision"], max_steps=int(len(train_ds) * EPOCHS / BATCH), - checkpoint_dir=f"{CKPT_DIR}/stage1", checkpoint_seconds=60, hub_repo=HF_REPO, hub_push_seconds=600, - ema_decay=0.0, log_every=5, val_every=500, val_batches=32), device=DEVICE) - trainer.resume() - trainer.fit(loader, epochs=EPOCHS, val_loader=val_loader) - if trainer.interrupted: - # Stopped part-way: checkpointed, and the next stage must not start from a half-trained model. - _halt("stage one stopped and checkpointed -- run again to resume it where it left off", kind="warn") - print("held-out falling = learning; held-out rising while training falls = memorising.") - -# ═══ 7 ═══ stage 1b — the whole atmosphere, rolled forward on its own forecasts ════════════════════ -if RUN_ROLLOUT: - banner(f"ROLLOUT — {ROLLOUT_STEPS}-step full-state training, {MEMBERS}-member CRPS ensemble") - if not RUN_PRETRAIN and restore("stage1") is None: - print("!! no stage-one checkpoint: rolling out an untrained backbone. Set RUN_PRETRAIN = True.") - steps = ROLLOUT_TRAIN - roll_loader = era5_loader(roll_ds, batch_size=max(BATCH // 2, 1), num_workers=WORKERS, - analysis_grid=SRC, output_grid=SRC, prefetch_factor=2) - train_state_rollout( - model, roll_loader, stepper, max_steps=steps, - schedule=RolloutSchedule(start_step=max(steps // 10, 1), ramp_steps=max(steps // 2, 1), - max_steps=ROLLOUT_STEPS), - channel_weights=WEIGHTS, members=MEMBERS, precision=R["precision"], - checkpoint_dir=f"{CKPT_DIR}/rollout", hub_repo=HF_REPO, log_every=25) - -# ═══ 8 ═══ stage two — hurricane heads, on the frozen backbone ═════════════════════════════════════ -banner("STAGE TWO — HURDAT2 best tracks") -tracks = parse_hurdat2(download("https://www.nhc.noaa.gov/data/hurdat/hurdat2-1851-2024-040425.txt", - os.path.join(DATA_DIR, "hurdat2.txt"))) -print(f"HURDAT2: {len(tracks):,} storms, {sum(len(t) for t in tracks):,} points, " - f"{sum(int(t.landfall.sum()) for t in tracks):,} landfalls") -walk = rapid_intensification(tracks, threshold_kt=30.0) -print(f"rapid intensification: {walk['positives']:,} of {walk['eligible']:,} eligible = " - f"{100 * walk['base_rate']:.2f}% (an always-'no' classifier scores {100 - 100 * walk['base_rate']:.0f}%)") -GROUPS = split_by_storm([t for t in tracks if t.year >= STORM_FIRST_SEASON]) -STORE_TIMES = ERA5.time.values.astype("datetime64[s]").astype(np.int64) - - -def paired(which): - starts, targets, report = pair_tracks_with_reanalysis(GROUPS[which], STORE_TIMES, OFFSETS, history=6) - return starts, targets, report - - -def storm_banks(): - """The frozen backbone's features for every storm sample: built once, cached, mirrored.""" - (tr_starts, tr_targets, tr_report), (va_starts, va_targets, va_report) = paired("train"), paired("validation") - print(f"\n{format_pairing(tr_report)}") - print(f"validation: {va_report['paired']:,} samples from {va_report['storms']} unseen storms " - f"(seasons 2017/2019/2021)") - print("\nreading each storm's reanalysis once and running the frozen backbone over it " - "(cached; resumes if interrupted):") - for _name in ("storm_bank_train", "storm_bank_val"): - mirror.pull(f"{CKPT_DIR}/{_name}.pt", f"checkpoints/{_name}.pt") # rebuilt anyway if the backbone changed - train = storm_feature_bank(model, ERA5, stream, tr_starts, tr_targets, SRC, - cache=f"{CKPT_DIR}/storm_bank_train.pt", precision=R["precision"]) - val = storm_feature_bank(model, ERA5, stream, va_starts, va_targets, SRC, - cache=f"{CKPT_DIR}/storm_bank_val.pt", precision=R["precision"]) - for _name in ("storm_bank_train", "storm_bank_val"): - mirror.push(f"{CKPT_DIR}/{_name}.pt", f"checkpoints/{_name}.pt") - return train, val, tr_targets, va_targets - - -bank_train = bank_val = None -if RUN_STORMS: - # A finished stage two is loaded *before* the banks are built: its backbone is the one they were - # built with, so they are reused rather than rebuilt from whatever checkpoint the Hub has for 1b. - resumed = None if RETRAIN_STORMS else restore("stage2") - if resumed is None and not (RUN_PRETRAIN or RUN_ROLLOUT) and restore("rollout", "stage1") is None: - _halt("no pretrained backbone found. Freezing an untrained one would fine-tune the storm " - "heads on noise -- set RUN_PRETRAIN = True.") - bank_train, bank_val, tr_targets, va_targets = storm_banks() - if resumed is not None: - print("\nstage two is already trained: its checkpoint is loaded (RETRAIN_STORMS = True trains it again)") - else: - history = train_storm_heads(model, bank_train, bank_val, epochs=300, - patience=25, checkpoint_dir=f"{CKPT_DIR}/stage2", hub_repo=HF_REPO) - best = min(history, key=lambda h: h["val"]) - banner("SCOREBOARD — is it generalising, or memorising?") - print(f"best epoch {best['epoch']}: held-out {best['val']:.4f} training {best['train']:.4f} " - f"gap {best['val'] - best['train']:+.4f} (a large positive gap = memorised the training storms)") - print(f"\nheld-out storms, seasons never trained on:\n{storm_scorecard(model, bank_val, CFG.lead_times_hours)}") - -# ═══ 8b ═══ stage three — what the satellite sees ══════════════════════════════════════════════════ -if RUN_SATELLITE: - banner("STAGE THREE — storm-centred infrared (HURSAT-B1 to 2016, GridSat-B1 after)") - loaded = None if RETRAIN_SATELLITE else restore("stage3") - if loaded is None and not RUN_STORMS and restore("stage2") is None: - _halt("no trained hurricane heads to build on -- run with RUN_STORMS = True first") - if bank_train is None: - bank_train, bank_val, tr_targets, va_targets = storm_banks() - print("\nstorm-centred infrared for every sample, now and six hours earlier (cached; mirrored):") - hursat = HursatArchive(os.path.join(DATA_DIR, "hursat"), - ibtracs_atcf_map(download(IBTRACS_NA_URL, os.path.join(DATA_DIR, "ibtracs.NA.csv")))) - gridsat = GridSatArchive() - for _name in ("satellite_bank_train", "satellite_bank_val"): - mirror.pull(f"{CKPT_DIR}/{_name}.pt", f"checkpoints/{_name}.pt") - sat_train = satellite_bank(tr_targets, f"{CKPT_DIR}/satellite_bank_train.pt", hursat, gridsat) - sat_val = satellite_bank(va_targets, f"{CKPT_DIR}/satellite_bank_val.pt", hursat, gridsat) - for _name in ("satellite_bank_train", "satellite_bank_val"): - mirror.push(f"{CKPT_DIR}/{_name}.pt", f"checkpoints/{_name}.pt") - shutil.rmtree(os.path.join(DATA_DIR, "hursat"), ignore_errors=True) # the patches are kept; the archives are not - plain_val = dict(bank_val) - bank_train["satellite"], bank_val["satellite"] = sat_train, sat_val - coverage = float(sat_val["available"][:, 0].float().mean()) - # The model as it arrives -- stage two, no satellite -- scored first: that is what stage three must beat. - before = None if loaded is not None else storm_scorecard(model, plain_val, CFG.lead_times_hours) - if loaded is not None: - print("\nstage three is already trained: its checkpoint is loaded (RETRAIN_SATELLITE = True trains it again)") - elif coverage < 0.3: - print(f"\nonly {coverage:.0%} of held-out samples have an image: too few to judge the satellite branch, " - "so it is not trained. Calibration and the input monitor still are.") - else: - history = train_storm_heads(model, bank_train, bank_val, epochs=80, batch_size=128, learning_rate=2e-4, - satellite_learning_rate=1e-3, augment=True, keep_if_no_better=True, - patience=15, log_every=5) - banner("CALIBRATION, INPUT MONITOR, ROLLOUT STATISTICS") - print("probabilities on held-out seasons, each season calibrated without itself:\n" - + calibration_report(model, bank_val)) - fit_probability_calibration(model, bank_val) - print(f"\ninput monitor fitted on the training storms: limit {fit_feature_monitor(model, bank_train):.2f}") - model.state_tendency.copy_(stepper.tendency.to(model.state_tendency.device)) - _stage3 = CheckpointManager(f"{CKPT_DIR}/stage3", repo_id=HF_REPO) - _stage3.save(model, None, state=TrainingState(step=int(model.trained_heads()["storm_satellite"])), - config=model.config.to_dict(), force=True) - _stage3.push_now(TrainingState(step=int(model.trained_heads()["storm_satellite"]))) - banner("SCOREBOARD — what the satellite adds, on held-out storms") - if before is not None: - print(f"stage two, before the satellite (the score to beat):\n{before}") - else: - print(f"stage three with the images withheld:\n{storm_scorecard(model, plain_val, CFG.lead_times_hours)}") - print(f"\nstage three, with the satellite ({coverage:.0%} of held-out samples have an image):\n" - f"{storm_scorecard(model, bank_val, CFG.lead_times_hours)}") - -# ═══ 9 ═══ did it learn? scorecards against persistence and climatology ════════════════════════════ -if not (RUN_PRETRAIN or RUN_ROLLOUT or RUN_STORMS or RUN_SATELLITE): - print(f"loaded: {restore('stage3', 'stage2', 'rollout', 'stage1') or 'nothing -- untrained'}") -print(f"\ntraining record (steps per output; 0 = never trained, never shown as a forecast):\n" - f" {model.trained_heads()}") - -_probe_stream = iter(val_loader) - - -def _probe(): - batch = next(_probe_stream) - return {"analysis": batch["analysis"].to(DEVICE), "calendar": batch["calendar"].to(DEVICE)} - - -responds, spread = responds_to_input(model, _probe, SRC) -print(f"\nresponds to the weather: {responds} (spread {spread:.2e})") - -card = state_card = None -if RUN_SCORE: - banner("SCORECARDS — area-weighted, physical units, identical batches for every baseline") - climo = build_climatology(train_ds, np.arange(len(train_ds)), WIDTH, SRC.num_points, - samples=200) - batches = 32 - card = score_model(model, val_loader, SRC, LAT, len(LON), CFG.lead_times_hours, - fields=scoring_fields(train_ds.variables, _target_index(train_ds.variables)), - normalizer=train_ds.normalizer, climatology=climo, max_batches=batches, device=DEVICE) - print("surface heads, direct multi-lead:\n" + card.table() + "\n" + card.verdict()) - state_card = score_state(model, val_loader, stepper, HEADLINE_STATE_FIELDS, - steps=ROLLOUT_STEPS, climatology=climo, - max_batches=batches, precision=R["precision"]) - print("\nfull-state rollout (Z500 in m2/s2, as WeatherBench 2 reports it):\n" - + state_card.table() + "\n" + state_card.verdict()) - if MEMBERS > 1 and "geopotential@500" in train_ds.variables: - batch = {k: (v.to(DEVICE) if torch.is_tensor(v) else v) for k, v in next(iter(val_loader)).items()} - lead = min(4, batch["state_target"].shape[1]) - ensemble = state_forecast(model, batch, stepper, steps=lead, members=max(MEMBERS, 4), - precision=R["precision"]) - z500 = train_ds.variables.index("geopotential@500") - ratio = spread_skill_ratio(ensemble[:, -1, ..., z500], batch["state_target"][:, lead - 1, :, z500]) - print(f"\nZ500 +{6 * lead}h ensemble spread/skill {ratio:.2f} (1.0 calibrated, <1 overconfident)") - if ratio < 0.05: - print(" no spread yet: the noise path starts at exactly zero and CRPS training grows it.") - -# ═══ 10 ═══ hindcast — a held-out hurricane, from its real analysis ════════════════════════════════ -if RUN_HINDCAST: - banner(f"HINDCAST — {HINDCAST[0].title()} {HINDCAST[1]}, a season the model never trained on") - storm = next((t for t in tracks if t.name.strip().upper() == HINDCAST[0] and t.year == HINDCAST[1]), None) - starts, targets, _ = (pair_tracks_with_reanalysis([storm], STORE_TIMES, OFFSETS, history=6) - if storm is not None else ([], [], None)) - if not len(starts): - print("that storm is not in the reanalysis window (1959-2021); pick another in HINDCAST") - else: - peak = int(np.nanargmax(storm.max_wind_kt)) - choice = min(range(len(targets)), key=lambda i: abs(targets[i].point - max(peak - 8, 0))) # ~48 h before peak - target = targets[choice].build() - window = ERA5Window(ERA5, indices=starts[choice:choice + 1], augment=False, stats_cache=STATS, **common)[0] - issued = dt.datetime.fromtimestamp(float(window["valid_time"]), tz=dt.timezone.utc) - centre = target["storm_center"].tolist() - here = target["storm_center"][None].to(DEVICE) - # The storm's environment and the physics-only steering track, from the analysis "now". - now = window["analysis"][-1][None].to(DEVICE) - environment = storm_environment(now, train_ds.variables, train_ds.normalizer, SRC, here) - steer = steering_track(now, train_ds.variables, train_ds.normalizer, SRC, here, CFG.lead_times_hours) - print(describe_environment(environment[0])) - # The satellite view at issue time and six hours earlier, if stage three has trained that branch. - seen = None - if model.trained_heads()["storm_satellite"] > 0: - from naturev1.stormsat import _earlier_position - earlier = _earlier_position(targets[choice]) - when = np.datetime64(issued.replace(tzinfo=None), "m") - wanted = [(when, tuple(centre))] + ([earlier] if earlier is not None else []) - images = GridSatArchive().patches(wanted, progress=False) - if images[0] is not None: - codes = np.zeros((1, 2, *images[0].shape), np.uint8) - codes[0, 0] = encode_bt(images[0]) - if len(images) > 1 and images[1] is not None: - codes[0, 1] = encode_bt(images[1]) - seen = (torch.from_numpy(codes).to(DEVICE), - torch.tensor([[True, len(images) > 1 and images[1] is not None]], device=DEVICE)) - print("satellite (GridSat-B1): " + describe_patch(images[0])) - with torch.no_grad(), torch.autocast(DEVICE, dtype=torch.bfloat16, enabled=DEVICE == "cuda"): - out = model.eval()(analysis=window["analysis"][None].to(DEVICE), analysis_grid=SRC, - calendar=window["calendar"][None].to(DEVICE), output_grid=SRC, - storm_center=here, storm_state=target["storm_state"][None].to(DEVICE), - storm_environment=environment, - track_prior=steer if model.uses_track_prior() else None, - storm_satellite=seen) - out = {k: v.float().cpu() for k, v in out.items()} - out.setdefault("track_prior", steer.float().cpu()) - # A second physics member: the storm carried by the atmosphere the model *forecasts*, step by - # step, rather than by today's flow held steady. Only once the state head has been trained. - rolled = None - if model.trained_heads()["state"] > 0: - with torch.no_grad(): - future = state_forecast(model, {"analysis": window["analysis"][None].to(DEVICE), - "calendar": window["calendar"][None].to(DEVICE), - "valid_time": window["valid_time"].view(1)}, - stepper, steps=ROLLOUT_STEPS, precision=R["precision"])[0] - rolled = steering_track(torch.cat([now[None], future.to(DEVICE)]), train_ds.variables, - train_ds.normalizer, SRC, here, CFG.lead_times_hours).float().cpu() - fc = build_forecast(out, CFG.lead_times_hours, issued, storm_center=centre, - normalizer=train_ds.normalizer, trained=model.trained_heads(), inputs=("analysis",), - consensus={"weights": model.track_consensus.cpu(), "storm_state": target["storm_state"]}) - print(f"issued {issued:%Y-%m-%d %H:%M}Z at {centre[0]:.1f}N {-centre[1]:.1f}W, " - f"{float(target['current_wind_kt']):.0f} kt now") - - def km_off(d_lat, d_lon, lead): - lat = centre[0] + float(target["track_target"][lead, 0]) - lon = centre[1] + float(target["track_target"][lead, 1]) - return 111.2 * np.hypot(centre[0] + float(d_lat) - lat, - (centre[1] + float(d_lon) - lon) * np.cos(np.radians(lat))) - - members = {"steering": steer[0].cpu(), "extrap.": extrapolation_track(target["storm_state"][None], - CFG.lead_times_hours)[0]} - if rolled is not None: - members["rollout-steer"] = rolled[0] - if isinstance(fc["track_scenarios"], list): - top = fc["track_scenarios"][0] - members = {f"model ({top['probability']:.0%})": torch.tensor( - [[p["latitude"] - centre[0], p["longitude"] - centre[1]] for p in top["track"]]), **members} - if "track_consensus" in fc: - members["consensus"] = torch.tensor([[p["latitude"] - centre[0], p["longitude"] - centre[1]] - for p in fc["track_consensus"]["track"]]) - print("track error vs what happened, km, by member -- the physics members need no training:") - print(" " + f"{'lead':>6}" + "".join(f"{name:>16}" for name in members) + f"{'wind fcst/actual':>20}") - for lead, hours in enumerate(CFG.lead_times_hours): - if target["track_valid"][lead] > 0: - row = "".join(f"{km_off(*member[lead], lead):>13.0f} km" for member in members.values()) - wind = "" - if isinstance(fc["intensity"], list): - wind = f"{fc['intensity'][lead]['max_wind_ms'] * 1.94384:5.0f}" - if target["intensity_mask"][lead, 0] > 0: - wind += f" / {float(target['intensity_target'][lead, 0]) * 1.94384:3.0f} kt" - print(f" +{hours:4d}h {row}{wind:>20}") - if not isinstance(fc["track_scenarios"], list): - print("track head untrained -- run RUN_STORMS first; the physics members above still stand") - problems = check_forecast(fc) - print("forecast checks: " + ("pass" if not problems else "; ".join(problems))) - -# ═══ 11 ═══ publish only if it earned it ═══════════════════════════════════════════════════════════ -if RUN_PUBLISH: - cards = [c for c in (state_card, card) if c is not None] - beats = [s for c in cards for s in c.scores if s.beats_persistence] - if not cards: - print("\nnot publishing: nothing was scored (set RUN_SCORE = True).") - elif not responds: - print("\nnot publishing: the model gives the same answer whatever weather it is shown.") - elif not beats: - print("\nnot publishing: it loses to persistence at every field and lead.") - else: - url = push_to_hub(model, HF_REPO, normalizer=train_ds.normalizer, scorecard=cards[0], name="NatureV1", - author="Nathan", token=HF_TOKEN or None, - training_data=f"ERA5 (WeatherBench 2), {len(CHANNELS)} channels, {R['years']} years; " - f"full-state {ROLLOUT_STEPS}-step rollout, {MEMBERS}-member CRPS; " - f"HURDAT2 {STORM_FIRST_SEASON}+ for the storm heads.") - print(f"\npublished: {url} (beats persistence on {len(beats)} field-lead pairs)") - -# ═══ 12 ═══ live forecast from GOES — only once that input path is trained ═════════════════════════ -def load_scene(paths, center, half_width_deg=9.0, stride=4): - """Geolocated samples plus true pixel footprints, cropped to a real box on the planet.""" - scene = scene_from_netcdf({c: str(p) for c, p in paths.items()}, stride=stride, - bounds=(center[0] - half_width_deg, center[0] + half_width_deg, - center[1] - half_width_deg, center[1] + half_width_deg)) - coords = torch.tensor(np.stack([np.radians(scene.latitude), np.radians(scene.longitude) % (2 * np.pi)], -1), - dtype=torch.float64) - grid = FieldGrid.from_points(coords, Geometry.globe(), num_neighbours=16, cluster_size=64, - weights=torch.tensor(scene.area_km2, dtype=torch.float64)) - values = torch.tensor(normalize_channels(scene.values, scene.channels), dtype=torch.float32) - if CFG.satellite_channels > values.shape[-1]: - values = torch.cat([values, torch.zeros(values.shape[0], CFG.satellite_channels - values.shape[-1])], -1) - return grid, values, scene - - -@torch.no_grad() -def forecast_now(storm=STORM, city=CITY): - paths = fetch_latest(os.path.join(DATA_DIR, "goes"), ("C13", "C09"), satellite="east", product="conus") - grid, values, scene = load_scene(paths, storm) - print(scene) - frames = CFG.history_frames - out_grid = fibonacci_sphere(2000, num_neighbours=16, cluster_size=50) - out = model.eval()(satellite=values[None, None].expand(1, frames, -1, -1).contiguous().to(DEVICE), - satellite_grid=grid, output_grid=out_grid, neighbours=12, - calendar=calendar_features(torch.full((1, frames), scene.timestamp.timestamp())).to(DEVICE), - storm_center=torch.tensor([storm], dtype=torch.float32, device=DEVICE)) - return build_forecast({k: v.float().cpu() for k, v in out.items()}, CFG.lead_times_hours, scene.timestamp, - storm_center=storm, output_grid=out_grid, point_of_interest=city, - normalizer=train_ds.normalizer, trained=model.trained_heads(), inputs=("satellite",)) - - -def show(fc): - print(f"\nissued {fc['issued']} trustworthy: {fc['trustworthy']}") - for key in ("warning", "note"): - if key in fc: - print(f" !! {fc[key]}") - for section in ("enso", "landfall", "track_scenarios", "eyewall", "rapid_intensification"): - value = fc.get(section) - if isinstance(value, dict) and value.get("available") is False: - print(f" {section}: not available -- {value['reason']}") - if isinstance(fc.get("track_scenarios"), list): - for s in fc["track_scenarios"][:4]: - p = s["track"][5] - print(f" {s['probability']:>5.0%} -> +{p['lead_hours']}h {p['latitude']:6.2f},{p['longitude']:7.2f}" - f" 95% cone {p['cone_radius_km_95']:.0f} km") - print(" checks: " + ("pass" if not check_forecast(fc) else "; ".join(check_forecast(fc)))) - - -if RUN_FORECAST or RUN_WATCHER: - if model.trained_heads()["satellite_encoder"] == 0: - print("\nlive GOES forecast refused: no training stage has ever fed this model satellite imagery, so " - "its satellite encoder is at initialisation and every number it produced would be noise shaped " - "like a forecast. Use the HINDCAST section, which runs from the analysis the model was trained on.") - elif RUN_FORECAST: - banner("LIVE FORECAST — newest GOES scene") - show(forecast_now()) - - -def on_new_scene(paths): - result = forecast_now() - os.makedirs(os.path.join(DATA_DIR, "forecasts"), exist_ok=True) - stamp = dt.datetime.now(dt.timezone.utc).strftime("%Y%m%dT%H%M") - with open(os.path.join(DATA_DIR, "forecasts", f"forecast_{stamp}.json"), "w") as handle: - json.dump(result, handle, indent=2) - show(result) - - -if RUN_WATCHER and model.trained_heads()["satellite_encoder"] > 0: - banner("WATCHING — a new forecast every hour") - watch(on_new_scene, interval_seconds=3600, directory=os.path.join(DATA_DIR, "goes"), channels=("C13", "C09")) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md deleted file mode 100644 index 8a1ce1cf44880a070fd10c3830f1ed2f0b88b0ce..0000000000000000000000000000000000000000 --- a/docs/ARCHITECTURE.md +++ /dev/null @@ -1,282 +0,0 @@ -# How iHELIX and NatureV1 work - -This is the detailed version. The short version: **most models reach a grid by flattening it and hoping a -positional encoding puts the geometry back.** iHELIX does the opposite — you declare what your axes are, -and everything downstream works in a space where the geometry is already correct. - ---- - -## 1. The problem with flattening - -Take a 240×121 latitude/longitude grid and flatten it row-major. Three things break: - -| | what the model sees | what is true | -|---|---|---| -| **Vertical neighbours** | 240 positions apart | adjacent | -| **The date line** | 359.9° and 0.1° are 239 positions apart | adjacent | -| **The poles** | ordinary interior points | all 240 entries are one place | - -A positional encoding gives the model an index. Nothing in an index says that cell 12,240 is directly -above cell 12,000, or that the last column touches the first. So the model has to *learn spherical -topology from data* — spending capacity rediscovering something you already knew. - -And it never quite succeeds, because the errors are worst exactly where the data is sparsest: near the -poles, where meridians converge and grid cells shrink to nothing. - ---- - -## 2. Geometry: what replaces "position in a sequence" - -You declare axes. Each one says what kind of thing it is: - -```python -Geometry([Axis("linear"), Axis("linear")]) # an image -Geometry([Axis("linear")] * 3) # a volume or point cloud -Geometry([Axis("periodic", period=L)] * 2 + [Axis("linear")]) # a channel-flow box -Geometry.globe() # the Earth's surface -Geometry.atmosphere() # Earth + pressure -``` - -Each axis maps into a Euclidean space where **straight-line distance already is the distance you care -about**: - -- **spherical** — `(latitude, longitude)` → a point on a sphere in 3-D. Chordal distance there is a - monotone function of great-circle distance. The poles stop being special. The date line **stops - existing**: 359.9° and 0.1° are near each other because they *are* near each other. -- **periodic** — → a circle. Same trick, for a wind-tunnel box. -- **log** — pressure. The model thinks in *factors* of pressure, not hectopascals, so equal ratios are - equal distances. -- **linear** — passes through, scaled. - -Every axis carries a `scale` in real units (km, say). That's how you declare that a kilometre upward is -not meteorologically the same as a kilometre sideways. - -### Local frames — the part that actually kills polar distortion - -At every point, the geometry provides an orthonormal frame: **east, north, up**. Neighbour offsets are -expressed in *that point's own frame*. - -So "300 km east" produces the **same input vector** at the equator and at 80°N. The distortion isn't -corrected by a factor — it is never introduced, because the representation never had it. - -> Measured: frames orthonormal to 1.11e-16 at the poles. Date-line distance 22 km, not 12,742 km. - ---- - -## 3. The grid: data, not architecture - -A `FieldGrid` holds two structures, computed once from the sample positions: - -**Neighbour graph** — for each point, its nearest neighbours *in metric space*. A point beside the date -line gets neighbours on both sides of it. A polar point gets the ones physically close, not the ones the -array happens to list adjacently. - -**Hierarchy** — a tree of nested spatial clusters, built by recursive splitting. The split is along the -**principal axis** (`torch.linalg.eigh` of the covariance), not the widest coordinate axis — coordinate -axes are an arbitrary choice of frame, so splitting on them would make the tree depend on how the domain -happens to be oriented. - -The model takes the grid **as an argument**. Swap in a finer grid and the same weights run unchanged. -That is the entire answer to resolution: the model never sees a grid shape, a resolution, or a -dimensionality. It sees points, distances and weights. - ---- - -## 4. The three strands - -Every block braids three mixers. This is the core of the architecture. - -### Strand I — Geodesic attention (short range) - -Each sample attends to its nearest neighbours in metric space. Three things are added to the logits: - -1. **A learned geometric bias** from the displacement in the query's own frame, plus log-distance, plus - how much the neighbour's frame is rotated relative to the query's (which on a curved manifold is what - you need to parallel-transport a direction). -2. **A per-head physical window.** Each head has its own radius — a *length*, not a count. At 88M the - eight heads sit on a geometric ladder: **120, 174, 252, 364, 527, 763, 1105, 1600 km**. One layer sees - eight scales at once, and those scales mean the same thing when the sampling changes. -3. **Quadrature weights** — how much of the domain each sample stands for. - -That third term is what makes attention a discretization of a continuous operator: - -``` -out(x) = ∫ a(x,y) v(y) dμ(y) / ∫ a(x,y) dμ(y) -``` - -Refine the grid and the estimate **converges** instead of drifting. - -> Measured convergence: 0.455% → 0.174% → 0.090% → 0.034% as the grid refines. - -**The window has compact support** — exactly zero past 3σ, not merely small. This is not cosmetic. It's -what makes the invariances hold *to the last bit* rather than to a few decimals. An early version -truncated at K neighbours instead, and the permutation error was 3.0e-2; with compact support it is -**exactly 0**. - -### Strand II — Index attention (long range) - -Local attention reaches 1,600 km. A teleconnection reaches across an ocean. - -The domain is summarized bottom-up into a tree of nested regions. Each query region computes a routing -vector, descends the tree keeping a beam, and ends up attending over the samples of the `topk` regions it -selected — which may be **anywhere, at any distance**. - -This is the part a fixed mesh cannot do. Message passing on a multi-mesh (GraphCast) couples regions by -distance along fixed edges, so reaching the far side costs hops and the route is decided in advance. -Here the route is **content-addressed and learned**: a ridge over the Atlantic can read the Pacific in one -step, and nothing in the wiring had to anticipate it. - -**Making a discrete top-k differentiable.** Selection is argmax — no gradient. So the routing score is -accumulated down the descent path and fed back as an additive bias on the retrieved logits, via -`logsigmoid(path_score)`. Gradient reaches every level of the tree. (An early version didn't accumulate, -and the node pooler received no gradient at all.) - -8 of the 17 layers carry the index strand, interleaved every other layer. - -### Strand III — Temporal (the gated delta rule) - -**Space has no order, so nothing recurrent runs across it.** Time does, and this is the only place a -recurrence runs. - -State is a **matrix per head per sample** whose size does not depend on how many frames have passed. A -rollout costs the same at frame 1000 as at frame 1 — which is what makes long autoregressive integrations -affordable. Training uses a chunkwise-parallel UT transform, so it parallelizes rather than stepping. - -A per-sample, per-head gate mixes strand I and strand II. Then a feed-forward, with RMSNorm and residuals -throughout. - ---- - -## 5. Encode → process → decode - -``` -satellite (2 km) ─┐ -analysis (25 km) ─┼─► cross-attention ─► Fibonacci mesh (4,096 pts) ─► 17 blocks ─► cross-attention ─► any output grid -environment ─┘ -``` - -Each source has **its own input projection and its own read radius** — a satellite scene at 2 km and an -analysis field at 25 km are not the same kind of measurement and shouldn't pretend to be. What they share -is the mesh they're read onto, which makes fusing them a **sum** rather than a resampling. - -Each encoder carries a learned `absent` vector, so a mesh point with no coverage (the far side of the -planet from a geostationary satellite) produces a clean absence rather than attending to noise. - -The mesh is a **Fibonacci sphere** — near-equal-area, so there's no pole to distort and no seam to cross. - ---- - -## 6. Why this beats a ViT/CNN on satellite imagery - -| | ViT / CNN | iHELIX | -|---|---|---| -| Pixel size | assumed constant | **measured**: a GOES limb pixel covers 64× the ground of a nadir pixel | -| Location | `image[400, 1200]` says nothing | every sample carries lat/lon + true footprint area | -| Values | normalized to 0–1, physics erased | brightness temperature stays in kelvin | -| Two sources | resample one onto the other, lose information | read both onto one mesh, add | - -The projection is solved per-pixel from GOES-R PUG Vol 3 §5.1.2.8.1 — validated against the file's own -metadata at 2.07 km at nadir where nominal C13 resolution is 2 km. - ---- - -## 7. Parameter budget (88M) - -``` -blocks 84.67M 95.1% ← the 17 braided blocks -analysis_encoder 0.80M 0.9% -environment_encoder 0.79M 0.9% -satellite_encoder 0.79M 0.9% -decoder 0.79M 0.9% -ri_head 0.40M 0.4% -summary_pool 0.26M 0.3% -track_head 0.14M 0.2% -eyewall_head 0.13M 0.1% -field_head 0.07M 0.1% -... -``` - -95% of the model is the shared processor. The heads are almost free — which is exactly what makes freezing -the backbone and fine-tuning on 24,585 storm points viable: **0.96M trainable, 1.1%**. - ---- - -## 8. Every output is a distribution - -- **Track** — weighted scenarios, each a full trajectory with a *tilting* uncertainty ellipse (two sigmas - and a correlation, so the ellipse can align with the direction of travel). Reads as "62% recurves - offshore, 26% tracks the coast, 12% inland" rather than an average track through somewhere the storm was - never going. *Measured: collapsing to one line scores 116 under the mixture likelihood where keeping both - branches scores 8.2.* -- **Fields** — per-point mean and variance, per lead time. -- **Eyewall** — peak wind, RMW, and 34/50/64 kt radii per quadrant, each with intervals. -- **RI** — probability per threshold, plus expected 24 h change and likeliest onset. -- **Landfall** — probability per lead time. - -Heads start at **Atlantic climatology**, not zero: 65 ± 30 kt peak wind, 985 hPa, RI at the measured 4.11% -base rate, track spread growing 0.6°→12° with lead. Without that, the intensity term alone was 119,191 of -a 60,565 total loss. - ---- - -## 9. The guarantees, and their one precondition - -| Property | Status | -|---|---| -| Sample order doesn't matter | **exact** (atol=0) | -| Spinning the globe in longitude | **exact** for the local strand (atol=1e-6) | -| Refining the grid converges | 0.455% → 0.034% | -| Time is causal, space is not | enforced | -| Temporal state doesn't grow | constant | - -**The one precondition:** each head's neighbour list must reach 3× its widest radius. `coverage()` reports -whether it does, and the model warns at construction if not. At coverage 1.0 the samples beyond a head's -window contribute *exactly* nothing and the invariances are exact. - -**One honest limit:** the index strand is *not* rotation-equivariant, because carving a domain into -regions is a discretization and no finite partition of a sphere commutes with arbitrary rotation. A fixed -icosahedral mesh has the same property. Sub-regions aren't isotropic, so every split below the first is -well determined. - ---- - -## 10. What 0.8 added, and why each one helps accuracy - -**Full-state residual prediction.** A `state_head` predicts the 6-hour *change* of every prognostic -channel — all 86 of them, including all 13 pressure levels — rather than seven surface fields. Before -this, a 12-step rollout froze 84 of 89 channels: Z500 never moved. The head is zero-initialised, so an -untrained model is *exactly* persistence and training can only move it from the strongest trivial -baseline. Changes are scaled by each channel's own 6-hour tendency spread (0.067σ for Z500, 1.08σ for -ω@500), so no channel dominates the gradient by being volatile. - -**Channel roles.** Every input channel is prognostic (predicted), forcing (recomputed) or static -(carried). Top-of-atmosphere solar radiation is computed from orbital geometry at each step's valid time, -matching ERA5 at correlation 1.00000; it rotates with an augmented globe. - -**Physical constraints.** Mass conservation removes any change in the area-weighted mean surface pressure -each step. Pressure levels are weighted in proportion to pressure (GraphCast), and every loss is -area-weighted — the same weighting the WeatherBench score uses. - -**Fair-CRPS ensembles.** Per-member noise enters before the processor; a fair CRPS over members trains -sharp members with calibrated spread. A model trained on squared error learns the conditional mean, and -the mean of all the ways a hurricane could go is a weak, smeared vortex — the documented intensity low -bias of deterministic AI models. - -**Storm-centred heads.** Storm heads used to read a mean over the whole planet, so two storms at the same -hour got identical answers. They now pool the mesh around the storm (Gaussian, 500 km), and are told -where it is and what it is doing: current intensity and 12-hour motion — the predictors every operational -intensity scheme starts from, and exactly what a 1.5° reanalysis cannot show. - -**A training record.** Every output counts its optimizer steps, saved with the weights. Forecasts show a -section only if its head was trained, and refuse outright when the input path (e.g. GOES imagery) never -was. - -## 11. What is not claimed - -**No weights here have been trained.** The architecture, losses, ingest and training loop are complete and -exercised end to end on real data, but no accuracy has been measured, and nothing here should inform a -decision about a real storm. The National Hurricane Center is the authoritative source. - -The next honest step is baselines: score against **persistence** ("tomorrow = today") and **climatology** -on the same held-out batches. A model that ties climatology has learned nothing; likelihoods alone can't -tell you which. diff --git a/melissa_jamaica.png b/melissa_jamaica.png deleted file mode 100644 index b9118a9a89dcb243d2a9b13a68e08b2e644a3578..0000000000000000000000000000000000000000 --- a/melissa_jamaica.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad22d73b9e5d56165a151290b2417895718b6dc6f3996adb34071e2737a1c5d0 -size 306885 diff --git a/melissa_landfall.png b/melissa_landfall.png deleted file mode 100644 index 61e7c96021d50236b61e66e85d403894d9cbc5b7..0000000000000000000000000000000000000000 --- a/melissa_landfall.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fad18f5048584fd39ae86a41cefd113e15b047d92db1dba1a4d45a7564a4da7a -size 384068 diff --git a/molab_swarm_cell.py b/molab_swarm_cell.py deleted file mode 100644 index e9a2173a2404d92abe0dc5f4f786657bb6a82b00..0000000000000000000000000000000000000000 --- a/molab_swarm_cell.py +++ /dev/null @@ -1,311 +0,0 @@ -# NatureV1 0.16 -- the operational cell: the satellite path, the swarm, the parishes, the cycle record, in ONE marimo cell -# (molab, 1 GPU; runs on a CPU box too, smaller). -# -# Paste this whole file into one marimo cell and run it. What it does: -# 1. installs naturev1 0.16 with the swarm extras (GRIB decoder, terrain tiles, maps, the Hub client) -# 2. builds the map of the whole Caribbean -- every parish / province / district of every country and territory, -# with terrain -- and the full-consensus inputs (climatology + analog database), once, cached under DATA -# 3. for every active NHC storm (or a replay, e.g. Melissa 2025): the best track + every satellite / recon / radar -# centre fix from NHC, the GFS analysis AND the forecasts of five models -- GFS, ECMWF IFS, ECMWF AIFS, -# GraphCast-GFS, the GEFS mean -- read from their public buckets; the NatureV1 consensus blended with the models' -# mean track and fed the previous cycle's error; a cone sized for this forecast and a swarm sized to it; the -# intensity consensus with its rapid-intensification branch, the chance the storm peaks at each category and the -# calibrated peak quantiles (plan for the 90% figure); the swarm's worlds filtered minute by minute on the fixes -# and painted onto the parishes: peak wind if hit, gusts, rain if hit and the wettest cell, when tropical-storm -# and hurricane force arrive, the landfall report for LANDFALL_COUNTRIES -# 4. the satellite, quantitatively: the newest GOES band-13 scene shown, and read -- coldest tops, the cold share of -# the 50-200 km annulus, ring symmetry, an eye, and their trend since the previous cycle (diagnostics beside -# the forecast; not yet inside the probabilities) -# 5. the operations desk's record: every cycle saved under DATA/runs// as JSON plus the parish table as CSV; -# the previous cycle found again automatically; what changed since it; optionally keeps watching NHC and -# re-issues when a new 6-hourly cycle appears (WATCH_MINUTES) -# 6. optionally (TRAIN_SATELLITE) trains the satellite networks of the 0.12 satellite path on this GPU -# -# Research model, not an operational forecast: for decisions, follow your national meteorological service and the -# National Hurricane Center. -import datetime as dt -import os -import subprocess -import sys -import time - -subprocess.run([sys.executable, "-m", "pip", "install", "-q", "--upgrade", "naturev1[swarm,hub]>=0.16.0", "ihelix>=0.6.0"], - check=True) - -from pathlib import Path - -import marimo as mo -import numpy as np -import torch - -# ── settings ────────────────────────────────────────────────────────────────────────────────────────────────────── -STORM = None # None: every active NHC storm. Or an NHC id, e.g. "al132025" (Melissa 2025) -REPLAY_TIME = None # with STORM: replay what was known then, e.g. "2025-10-27T00" (fixes after it unused) -LANDFALL_COUNTRIES = ("Jamaica",) # one landfall report per country: when, where first, how strong, every parish's numbers -GPU = torch.cuda.is_available() -PARTICLES = 1_000_000 if GPU else 50_000 # worlds in the swarm -IMPACT_WORLDS = 40_000 if GPU else 2_000 # worlds painted onto the map (the map is the costly part) -FULL_CONSENSUS = True # 13-member consensus: builds a 2.5 GB climatology + analog database once (~20-30 min) -SHOW_GOES = True # the newest GOES-19 / GOES-18 infrared image around each live storm, shown and read -FORECAST_FLOW = True # the models' forecast steering + their tracks as members (~150 MB per model, cached) -MODELS = ("gfs", "ifs", "aifs", "graphcastgfs", "gefs") # any that is not yet published for the cycle is skipped -PREVIOUS_CYCLE = "auto" # the previous cycle's error feeds the new track ("auto": from the saved records; None: off) -WATCH_MINUTES = 0 # 0: issue once and stop. 20: keep watching NHC, re-issue when a new 6-hourly cycle appears -TRAIN_SATELLITE = False # the 0.12 satellite networks on this GPU (needs ~1 TB disk for the staged GridSat) -HF_REPO, HF_TOKEN = "CorxLabs/NatureV1-500", "" # only for TRAIN_SATELLITE pushes; paste a token, clear it after -DATA = Path(os.environ.get("NATUREV1_DATA", "/data/naturev1" if Path("/data").exists() else Path.home() / "naturev1")) -DATA.mkdir(parents=True, exist_ok=True) -RUNS = DATA / "runs" # the cycle records: /