| --- |
| license: mit |
| tags: |
| - forecast |
| - weather |
| - lstm |
| - classification |
| - regression |
| - weather-forecast |
| - multitask |
| - harley-ml |
| - small |
| --- |
| |
| # Hweh-446k |
|
|
| ## Summary |
|
|
| Task: Weather Forecasting |
| Inputs: 72 hours time-series |
| Outputs: 12h multivariate forecast |
| Params: 446k |
| Framework: PyTorch |
|
|
| Author: Paul Courneya (Harley-ml) |
|
|
| ## Description |
|
|
| **Hweh-446k** is a **446-thousand-parameter LSTM model** distillation of [Hweh-6M](https://huggingface.co/Harley-ml/Hweh-6M) (a ~92% reduction in params!!), trained to predict the next **12 hours of weather**, including temperature, humidity, pressure, precipitation, and more, using the previous **72 hours of weather context**. |
| We recommend using this model as a backup to a weather API or for fast offline forecasting when internet access is unavailable. |
|
|
| We would also like to give a shoutout to [**Open-Meteo**](https://open-meteo.com/) for providing a **free-to-use weather forecasting API**. |
|
|
| ### Why “Hweh”? |
|
|
| In Proto-Indo-European, the root ***h₂weh₁-** means “to blow.” We chose it as the name for a weather forecasting model because of its connection to wind and air. |
| |
| ## Architecture |
| |
| The model uses a multitask LSTM setup: |
| |
| | Parameter | Value | |
| | ----------------------- | ---------------------------------------------- | |
| | `input_dim` | `22` | |
| | `seq_len` | `72` | |
| | `num_predict` | `12` | |
| | `hidden_dim` | `128` | |
| | `num_layers` | `3` | |
| | `dropout` | `0.1` | |
| | `encoder_type` | `lstm` | |
| | `num_locations` | `82` | |
| | `location_emb_dim` | `32` | |
| | `num_weather_classes` | `7` | |
| |
| ## Training |
| |
| We trained Hweh-446k on 4.06 million rows of weather data from 82 locations with the supervision of Hweh-6M for one epoch, using a batch size of 16 and gradient accumulation of 5. Training ran for 4.3 hours on an RTX 2060 6GB GPU. |
| |
| ### Input Features |
| |
| 1. `temperature_2m_norm` |
| 2. `relative_humidity_2m_norm` |
| 3. `apparent_temperature_norm` |
| 4. `precipitation_log_norm` |
| 5. `sea_level_pressure_norm` |
| 6. `surface_pressure_norm` |
| 7. `cloud_cover_total_norm` |
| 8. `visibility_norm` |
| 9. `wind_speed_10m_norm` |
| 10. `wind_direction_10m_sin` |
| 11. `wind_direction_10m_cos` |
| 12. `hour_sin` |
| 13. `hour_cos` |
| 14. `day_of_year_sin` |
| 15. `day_of_year_cos` |
| 16. `weather_code_onehot_clear` |
| 17. `weather_code_onehot_cloudy` |
| 18. `weather_code_onehot_fog` |
| 19. `weather_code_onehot_drizzle` |
| 20. `weather_code_onehot_rain` |
| 21. `weather_code_onehot_snow` |
| 22. `weather_code_onehot_thunderstorm` |
| |
| ### Output Features |
| |
| 1. `y_temp_c`: continuous regression |
| 2. `y_humidity`: continuous regression |
| 3. `y_apparent_temperature`: continuous regression |
| 4. `y_precipitation_mm`: continuous regression |
| 5. `y_sea_level_pressure_hpa`: continuous regression |
| 6. `y_surface_pressure_hpa`: continuous regression |
| 7. `y_cloud_cover_total`: continuous regression |
| 8. `y_wind_speed_10m`: continuous regression |
| 9. `y_wind_direction_sin`: continuous regression |
| 10. `y_wind_direction_cos`: continuous regression |
| 11. `y_rain_prob`: binary classification |
| 12. `y_weather_class`: multiclass classification |
| |
| ### Training Results |
| |
| #### Training & Evaluation Metrics |
| |
| | Step | Train Loss | Eval Loss | Weather Acc | Rain Acc | Rain Recall | Weather Recall | |
| | ----: | ---------: | --------: | ----------: | -------: | ----------: | -------------: | |
| | 1k | 8.4609 | 8.8471 | 0.6317 | 0.7451 | 0.7640 | 0.2574 | |
| | 5k | 5.1420 | 5.0602 | 0.6247 | 0.7531 | 0.8025 | 0.5648 | |
| | 10k | 4.1733 | 3.9198 | 0.6117 | 0.7876 | 0.8016 | 0.6297 | |
| | 15k | 3.8354 | 3.6310 | 0.6140 | 0.7920 | 0.8009 | 0.6187 | |
| | 20k | 3.6206 | 3.4365 | 0.6083 | 0.7881 | 0.8140 | 0.6179 | |
| | 25k | 3.5378 | 3.3251 | 0.6083 | 0.7859 | 0.8173 | 0.6245 | |
| | 30k | 3.4534 | 3.2846 | 0.6041 | 0.7812 | 0.8272 | 0.6398 | |
| | 35k | 3.4272 | 3.2324 | 0.6061 | 0.7860 | 0.8194 | 0.6289 | |
| | 40k | 3.4143 | 3.2230 | 0.6080 | 0.7862 | 0.8200 | 0.6339 | |
| | 42.6k | — | 3.2180 | 0.6081 | 0.7857 | 0.8212 | 0.6340 | |
| |
| Note: Loss looks higher than Hweh-6M's because of KL + train/val loss. |
| #### Regression Error Metrics (MAE) |
| |
| | Step | Apparent | Cloud | Humidity | Precip (mm) | Sea Level P | Surface P | Temp | Wind | |
| | ----: | -------: | ------: | -------: | ----------: | ----------: | --------: | -----: | ----: | |
| | 1k | 212.80 | 2179.70 | 1476.42 | 0.140 | 7571.45 | 83590.98 | 172.79 | 60.49 | |
| | 5k | 2.28 | 25.58 | 9.04 | 0.107 | 3.50 | 14.55 | 1.90 | 3.78 | |
| | 10k | 2.06 | 25.31 | 8.08 | 0.100 | 3.31 | 9.63 | 1.72 | 3.37 | |
| | 15k | 1.91 | 25.00 | 7.88 | 0.101 | 3.18 | 7.93 | 1.61 | 3.25 | |
| | 20k | 1.88 | 25.12 | 7.60 | 0.101 | 3.13 | 7.41 | 1.56 | 3.18 | |
| | 25k | 1.84 | 25.01 | 7.53 | 0.102 | 3.09 | 6.61 | 1.53 | 3.13 | |
| | 30k | 1.81 | 25.03 | 7.45 | 0.102 | 3.12 | 6.60 | 1.51 | 3.12 | |
| | 35k | 1.81 | 24.94 | 7.42 | 0.101 | 3.07 | 6.39 | 1.52 | 3.12 | |
| | 40k | 1.79 | 24.94 | 7.39 | 0.101 | 3.06 | 6.37 | 1.50 | 3.11 | |
| | 42.6k | 1.79 | 24.92 | 7.39 | 0.101 | 3.06 | 6.38 | 1.50 | 3.11 | |
| |
| This model did better than the teacher on MAE and accuracy, but the real-world accuracy is 5-10% worse. |
| |
| ## Generation Examples |
| |
| | ID | Class | |
| | -- | ------------ | |
| | 0 | clear | |
| | 1 | cloudy | |
| | 2 | fog | |
| | 3 | drizzle | |
| | 4 | rain | |
| | 5 | snow | |
| | 6 | thunderstorm | |
| |
| City=Seattle |
| ``` |
| { |
| "city": "Seattle", |
| "location_id": "1", |
| "model_location_id": 0, |
| "data_source": "open-meteo forecast api (past-hours context only)", |
| "requested_at_utc": "2026-05-08T19:57:14.429521+00:00", |
| "context": { |
| "hours": 72, |
| "start_utc": "2026-05-05T19:00:00+00:00", |
| "end_utc": "2026-05-08T18:00:00+00:00", |
| "start_local": "2026-05-05T12:00:00-07:00", |
| "end_local": "2026-05-08T11:00:00-07:00" |
| }, |
| "model": { |
| "encoder_type": "lstm", |
| "seq_len": 72, |
| "input_dim": 22, |
| "num_weather_classes": 7 |
| }, |
| "forecast": [ |
| { |
| "lead_hours": 1, |
| "target_utc": "2026-05-08T19:00:00+00:00", |
| "target_local": "2026-05-08T12:00:00-07:00", |
| "temperature_2m_c": 12.21396255493164, |
| "relative_humidity_2m_pct": 72.33454895019531, |
| "apparent_temperature_c": 10.097986221313477, |
| "precipitation_mm": 0.015628309920430183, |
| "pressure_msl_hpa": 1022.0569458007812, |
| "surface_pressure_hpa": 1014.205078125, |
| "cloud_cover_pct": 94.34225463867188, |
| "wind_speed_10m_kmh": 12.568346977233887, |
| "rain_probability": 0.19356799125671387, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.020174680277705193, |
| "class_1": 0.9282320737838745, |
| "class_2": 0.0022441258188337088, |
| "class_3": 0.04022064805030823, |
| "class_4": 0.008552632294595242, |
| "class_5": 0.0005501594278030097, |
| "class_6": 2.556406798248645e-05 |
| } |
| }, |
| { |
| "lead_hours": 2, |
| "target_utc": "2026-05-08T20:00:00+00:00", |
| "target_local": "2026-05-08T13:00:00-07:00", |
| "temperature_2m_c": 12.8738374710083, |
| "relative_humidity_2m_pct": 70.51017761230469, |
| "apparent_temperature_c": 10.80291748046875, |
| "precipitation_mm": 0.011432276107370853, |
| "pressure_msl_hpa": 1022.0043334960938, |
| "surface_pressure_hpa": 1014.2881469726562, |
| "cloud_cover_pct": 89.5630111694336, |
| "wind_speed_10m_kmh": 12.822803497314453, |
| "rain_probability": 0.2689012587070465, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.04770936816930771, |
| "class_1": 0.8698592185974121, |
| "class_2": 0.0019157826900482178, |
| "class_3": 0.057819921523332596, |
| "class_4": 0.02183685451745987, |
| "class_5": 0.0008237561560235918, |
| "class_6": 3.5158725950168446e-05 |
| } |
| }, |
| { |
| "lead_hours": 3, |
| "target_utc": "2026-05-08T21:00:00+00:00", |
| "target_local": "2026-05-08T14:00:00-07:00", |
| "temperature_2m_c": 13.51952075958252, |
| "relative_humidity_2m_pct": 68.44591522216797, |
| "apparent_temperature_c": 11.500271797180176, |
| "precipitation_mm": 0.006943895947188139, |
| "pressure_msl_hpa": 1021.80859375, |
| "surface_pressure_hpa": 1014.2529296875, |
| "cloud_cover_pct": 84.49480438232422, |
| "wind_speed_10m_kmh": 12.941960334777832, |
| "rain_probability": 0.30342426896095276, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.07170139998197556, |
| "class_1": 0.8286910057067871, |
| "class_2": 0.0013093570014461875, |
| "class_3": 0.06433302909135818, |
| "class_4": 0.03300558775663376, |
| "class_5": 0.0009036734118126333, |
| "class_6": 5.590655928244814e-05 |
| } |
| }, |
| { |
| "lead_hours": 4, |
| "target_utc": "2026-05-08T22:00:00+00:00", |
| "target_local": "2026-05-08T15:00:00-07:00", |
| "temperature_2m_c": 13.970871925354004, |
| "relative_humidity_2m_pct": 66.8187026977539, |
| "apparent_temperature_c": 11.959537506103516, |
| "precipitation_mm": 0.009790810756385326, |
| "pressure_msl_hpa": 1021.4691162109375, |
| "surface_pressure_hpa": 1014.1052856445312, |
| "cloud_cover_pct": 80.34271240234375, |
| "wind_speed_10m_kmh": 13.050889015197754, |
| "rain_probability": 0.33110707998275757, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.10919982939958572, |
| "class_1": 0.7759758830070496, |
| "class_2": 0.0011831748997792602, |
| "class_3": 0.07015678286552429, |
| "class_4": 0.042580746114254, |
| "class_5": 0.000857028178870678, |
| "class_6": 4.6529316023224965e-05 |
| } |
| }, |
| { |
| "lead_hours": 5, |
| "target_utc": "2026-05-08T23:00:00+00:00", |
| "target_local": "2026-05-08T16:00:00-07:00", |
| "temperature_2m_c": 14.132287979125977, |
| "relative_humidity_2m_pct": 66.1208267211914, |
| "apparent_temperature_c": 12.156023025512695, |
| "precipitation_mm": 0.008600466884672642, |
| "pressure_msl_hpa": 1021.0518188476562, |
| "surface_pressure_hpa": 1013.7891845703125, |
| "cloud_cover_pct": 76.06925201416016, |
| "wind_speed_10m_kmh": 12.926268577575684, |
| "rain_probability": 0.3409281373023987, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.1305426061153412, |
| "class_1": 0.7395681142807007, |
| "class_2": 0.0007046378450468183, |
| "class_3": 0.07573042809963226, |
| "class_4": 0.052331216633319855, |
| "class_5": 0.0010767169296741486, |
| "class_6": 4.629969771485776e-05 |
| } |
| }, |
| { |
| "lead_hours": 6, |
| "target_utc": "2026-05-09T00:00:00+00:00", |
| "target_local": "2026-05-08T17:00:00-07:00", |
| "temperature_2m_c": 13.963343620300293, |
| "relative_humidity_2m_pct": 66.67638397216797, |
| "apparent_temperature_c": 11.971813201904297, |
| "precipitation_mm": 0.010375693440437317, |
| "pressure_msl_hpa": 1020.6505126953125, |
| "surface_pressure_hpa": 1013.4520874023438, |
| "cloud_cover_pct": 73.21341705322266, |
| "wind_speed_10m_kmh": 12.721481323242188, |
| "rain_probability": 0.35606324672698975, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.15235546231269836, |
| "class_1": 0.7133304476737976, |
| "class_2": 0.0007980632944963872, |
| "class_3": 0.07519367337226868, |
| "class_4": 0.05724283307790756, |
| "class_5": 0.0010369947412982583, |
| "class_6": 4.249428093316965e-05 |
| } |
| }, |
| { |
| "lead_hours": 7, |
| "target_utc": "2026-05-09T01:00:00+00:00", |
| "target_local": "2026-05-08T18:00:00-07:00", |
| "temperature_2m_c": 13.449448585510254, |
| "relative_humidity_2m_pct": 68.29602813720703, |
| "apparent_temperature_c": 11.426795959472656, |
| "precipitation_mm": 0.012202607467770576, |
| "pressure_msl_hpa": 1020.3434448242188, |
| "surface_pressure_hpa": 1013.139892578125, |
| "cloud_cover_pct": 70.92017364501953, |
| "wind_speed_10m_kmh": 12.359071731567383, |
| "rain_probability": 0.3651714026927948, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.16448773443698883, |
| "class_1": 0.695494532585144, |
| "class_2": 0.0008648976217955351, |
| "class_3": 0.07075871527194977, |
| "class_4": 0.06722015887498856, |
| "class_5": 0.0011408632853999734, |
| "class_6": 3.3135267585748807e-05 |
| } |
| }, |
| { |
| "lead_hours": 8, |
| "target_utc": "2026-05-09T02:00:00+00:00", |
| "target_local": "2026-05-08T19:00:00-07:00", |
| "temperature_2m_c": 12.755823135375977, |
| "relative_humidity_2m_pct": 70.44146728515625, |
| "apparent_temperature_c": 10.662925720214844, |
| "precipitation_mm": 0.014662384055554867, |
| "pressure_msl_hpa": 1020.1875610351562, |
| "surface_pressure_hpa": 1012.8895874023438, |
| "cloud_cover_pct": 69.15129852294922, |
| "wind_speed_10m_kmh": 11.787208557128906, |
| "rain_probability": 0.3489035665988922, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.19570188224315643, |
| "class_1": 0.6735588908195496, |
| "class_2": 0.000978420372121036, |
| "class_3": 0.06184739992022514, |
| "class_4": 0.0664038360118866, |
| "class_5": 0.0014616982080042362, |
| "class_6": 4.789666854776442e-05 |
| } |
| }, |
| { |
| "lead_hours": 9, |
| "target_utc": "2026-05-09T03:00:00+00:00", |
| "target_local": "2026-05-08T20:00:00-07:00", |
| "temperature_2m_c": 11.955390930175781, |
| "relative_humidity_2m_pct": 73.05667877197266, |
| "apparent_temperature_c": 9.77428913116455, |
| "precipitation_mm": 0.015376528725028038, |
| "pressure_msl_hpa": 1020.2242431640625, |
| "surface_pressure_hpa": 1012.7984619140625, |
| "cloud_cover_pct": 67.46344757080078, |
| "wind_speed_10m_kmh": 11.127586364746094, |
| "rain_probability": 0.36496502161026, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.20349998772144318, |
| "class_1": 0.6480608582496643, |
| "class_2": 0.0010103220120072365, |
| "class_3": 0.06373731791973114, |
| "class_4": 0.08206527680158615, |
| "class_5": 0.0015911461086943746, |
| "class_6": 3.510143869789317e-05 |
| } |
| }, |
| { |
| "lead_hours": 10, |
| "target_utc": "2026-05-09T04:00:00+00:00", |
| "target_local": "2026-05-08T21:00:00-07:00", |
| "temperature_2m_c": 11.182319641113281, |
| "relative_humidity_2m_pct": 75.5196304321289, |
| "apparent_temperature_c": 8.978246688842773, |
| "precipitation_mm": 0.016823438927531242, |
| "pressure_msl_hpa": 1020.421875, |
| "surface_pressure_hpa": 1012.8126831054688, |
| "cloud_cover_pct": 65.73115539550781, |
| "wind_speed_10m_kmh": 10.359850883483887, |
| "rain_probability": 0.35924479365348816, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.21845510601997375, |
| "class_1": 0.6389062404632568, |
| "class_2": 0.0018773162737488747, |
| "class_3": 0.059891991317272186, |
| "class_4": 0.07879848033189774, |
| "class_5": 0.002034474164247513, |
| "class_6": 3.633901724242605e-05 |
| } |
| }, |
| { |
| "lead_hours": 11, |
| "target_utc": "2026-05-09T05:00:00+00:00", |
| "target_local": "2026-05-08T22:00:00-07:00", |
| "temperature_2m_c": 10.499757766723633, |
| "relative_humidity_2m_pct": 77.536865234375, |
| "apparent_temperature_c": 8.306406021118164, |
| "precipitation_mm": 0.01860857754945755, |
| "pressure_msl_hpa": 1020.7318725585938, |
| "surface_pressure_hpa": 1012.9368896484375, |
| "cloud_cover_pct": 65.10720825195312, |
| "wind_speed_10m_kmh": 9.62015151977539, |
| "rain_probability": 0.3694237470626831, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.22774139046669006, |
| "class_1": 0.6242526173591614, |
| "class_2": 0.0028147574048489332, |
| "class_3": 0.06025313213467598, |
| "class_4": 0.0822005346417427, |
| "class_5": 0.0026846849359571934, |
| "class_6": 5.289047476253472e-05 |
| } |
| }, |
| { |
| "lead_hours": 12, |
| "target_utc": "2026-05-09T06:00:00+00:00", |
| "target_local": "2026-05-08T23:00:00-07:00", |
| "temperature_2m_c": 9.956731796264648, |
| "relative_humidity_2m_pct": 79.21904754638672, |
| "apparent_temperature_c": 7.87125301361084, |
| "precipitation_mm": 0.017959173768758774, |
| "pressure_msl_hpa": 1021.0579833984375, |
| "surface_pressure_hpa": 1013.093994140625, |
| "cloud_cover_pct": 64.14817810058594, |
| "wind_speed_10m_kmh": 8.923616409301758, |
| "rain_probability": 0.3691202700138092, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.23541118204593658, |
| "class_1": 0.618242621421814, |
| "class_2": 0.0044443667866289616, |
| "class_3": 0.06440076231956482, |
| "class_4": 0.0741729885339737, |
| "class_5": 0.003270090091973543, |
| "class_6": 5.8019890275318176e-05 |
| } |
| } |
| ], |
| "sanity": { |
| "sequence_shape": [ |
| 72, |
| 22 |
| ], |
| "finite_features": true |
| } |
| } |
| PS C:\Users\Paulc> python3.12 weather_infer.py --model_dir "C:\Users\Paulc\weather_model\student_distilled" --city Seattle |
| Warning: unexpected keys while loading checkpoint: ['distill_proj.weight'] |
| { |
| "city": "Seattle", |
| "location_id": "1", |
| "model_location_id": 0, |
| "data_source": "open-meteo forecast api (past-hours context only)", |
| "requested_at_utc": "2026-05-08T19:57:47.439276+00:00", |
| "context": { |
| "hours": 72, |
| "start_utc": "2026-05-05T19:00:00+00:00", |
| "end_utc": "2026-05-08T18:00:00+00:00", |
| "start_local": "2026-05-05T12:00:00-07:00", |
| "end_local": "2026-05-08T11:00:00-07:00" |
| }, |
| "model": { |
| "encoder_type": "lstm", |
| "seq_len": 72, |
| "input_dim": 22, |
| "num_weather_classes": 7 |
| }, |
| "forecast": [ |
| { |
| "lead_hours": 1, |
| "target_utc": "2026-05-08T19:00:00+00:00", |
| "target_local": "2026-05-08T12:00:00-07:00", |
| "temperature_2m_c": 13.681238174438477, |
| "relative_humidity_2m_pct": 69.90876770019531, |
| "apparent_temperature_c": 11.687149047851562, |
| "precipitation_mm": 0.0012515264097601175, |
| "pressure_msl_hpa": 1019.3030395507812, |
| "surface_pressure_hpa": 1018.5359497070312, |
| "cloud_cover_pct": 88.76920318603516, |
| "wind_speed_10m_kmh": 13.126839637756348, |
| "rain_probability": 0.1457637995481491, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.02097843959927559, |
| "class_1": 0.9304905533790588, |
| "class_2": 0.0025352241937071085, |
| "class_3": 0.03495039418339729, |
| "class_4": 0.01054275780916214, |
| "class_5": 0.0004654920194298029, |
| "class_6": 3.7044908822281286e-05 |
| } |
| }, |
| { |
| "lead_hours": 2, |
| "target_utc": "2026-05-08T20:00:00+00:00", |
| "target_local": "2026-05-08T13:00:00-07:00", |
| "temperature_2m_c": 14.486506462097168, |
| "relative_humidity_2m_pct": 67.52698516845703, |
| "apparent_temperature_c": 12.55270767211914, |
| "precipitation_mm": 0.0008608415955677629, |
| "pressure_msl_hpa": 1019.0198364257812, |
| "surface_pressure_hpa": 1018.4589233398438, |
| "cloud_cover_pct": 84.31889343261719, |
| "wind_speed_10m_kmh": 13.241435050964355, |
| "rain_probability": 0.19527363777160645, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.053049832582473755, |
| "class_1": 0.8794819712638855, |
| "class_2": 0.0021074640098959208, |
| "class_3": 0.0436088852584362, |
| "class_4": 0.02111213095486164, |
| "class_5": 0.0005800225771963596, |
| "class_6": 5.971627979306504e-05 |
| } |
| }, |
| { |
| "lead_hours": 3, |
| "target_utc": "2026-05-08T21:00:00+00:00", |
| "target_local": "2026-05-08T14:00:00-07:00", |
| "temperature_2m_c": 15.089279174804688, |
| "relative_humidity_2m_pct": 65.6773681640625, |
| "apparent_temperature_c": 13.181319236755371, |
| "precipitation_mm": 0.002190209459513426, |
| "pressure_msl_hpa": 1018.709716796875, |
| "surface_pressure_hpa": 1018.2867431640625, |
| "cloud_cover_pct": 80.14619445800781, |
| "wind_speed_10m_kmh": 13.32516860961914, |
| "rain_probability": 0.21867528557777405, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.08254168927669525, |
| "class_1": 0.8380688428878784, |
| "class_2": 0.0014072611229494214, |
| "class_3": 0.04709410294890404, |
| "class_4": 0.030200183391571045, |
| "class_5": 0.0006088378722779453, |
| "class_6": 7.912428554845974e-05 |
| } |
| }, |
| { |
| "lead_hours": 4, |
| "target_utc": "2026-05-08T22:00:00+00:00", |
| "target_local": "2026-05-08T15:00:00-07:00", |
| "temperature_2m_c": 15.403922080993652, |
| "relative_humidity_2m_pct": 64.67561340332031, |
| "apparent_temperature_c": 13.48654556274414, |
| "precipitation_mm": 0.0021571130491793156, |
| "pressure_msl_hpa": 1018.3944702148438, |
| "surface_pressure_hpa": 1018.0625, |
| "cloud_cover_pct": 76.4104995727539, |
| "wind_speed_10m_kmh": 13.275524139404297, |
| "rain_probability": 0.2299734503030777, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.11604393273591995, |
| "class_1": 0.7922014594078064, |
| "class_2": 0.0011422870447859168, |
| "class_3": 0.05158008262515068, |
| "class_4": 0.03833876550197601, |
| "class_5": 0.000621610670350492, |
| "class_6": 7.179530075518414e-05 |
| } |
| }, |
| { |
| "lead_hours": 5, |
| "target_utc": "2026-05-08T23:00:00+00:00", |
| "target_local": "2026-05-08T16:00:00-07:00", |
| "temperature_2m_c": 15.407997131347656, |
| "relative_humidity_2m_pct": 64.65668487548828, |
| "apparent_temperature_c": 13.48292350769043, |
| "precipitation_mm": 0.0026813943404704332, |
| "pressure_msl_hpa": 1018.1220703125, |
| "surface_pressure_hpa": 1017.7960205078125, |
| "cloud_cover_pct": 72.77561950683594, |
| "wind_speed_10m_kmh": 13.133893966674805, |
| "rain_probability": 0.23628145456314087, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.14982207119464874, |
| "class_1": 0.7523030638694763, |
| "class_2": 0.0008282885537482798, |
| "class_3": 0.05172016844153404, |
| "class_4": 0.04454538971185684, |
| "class_5": 0.0007084720418788493, |
| "class_6": 7.257604011101648e-05 |
| } |
| }, |
| { |
| "lead_hours": 6, |
| "target_utc": "2026-05-09T00:00:00+00:00", |
| "target_local": "2026-05-08T17:00:00-07:00", |
| "temperature_2m_c": 15.139252662658691, |
| "relative_humidity_2m_pct": 65.5518798828125, |
| "apparent_temperature_c": 13.192585945129395, |
| "precipitation_mm": 0.0026606114115566015, |
| "pressure_msl_hpa": 1017.9351196289062, |
| "surface_pressure_hpa": 1017.5604858398438, |
| "cloud_cover_pct": 69.60166931152344, |
| "wind_speed_10m_kmh": 12.789706230163574, |
| "rain_probability": 0.23617199063301086, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.17920561134815216, |
| "class_1": 0.7213184237480164, |
| "class_2": 0.0008871846948750317, |
| "class_3": 0.05144878104329109, |
| "class_4": 0.046307649463415146, |
| "class_5": 0.0007639037212356925, |
| "class_6": 6.843609298812225e-05 |
| } |
| }, |
| { |
| "lead_hours": 7, |
| "target_utc": "2026-05-09T01:00:00+00:00", |
| "target_local": "2026-05-08T18:00:00-07:00", |
| "temperature_2m_c": 14.66390609741211, |
| "relative_humidity_2m_pct": 67.1559066772461, |
| "apparent_temperature_c": 12.692171096801758, |
| "precipitation_mm": 0.002722225384786725, |
| "pressure_msl_hpa": 1017.8424072265625, |
| "surface_pressure_hpa": 1017.3685913085938, |
| "cloud_cover_pct": 66.97268676757812, |
| "wind_speed_10m_kmh": 12.329818725585938, |
| "rain_probability": 0.23410728573799133, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.20177967846393585, |
| "class_1": 0.697116494178772, |
| "class_2": 0.001064434414729476, |
| "class_3": 0.04921679198741913, |
| "class_4": 0.049886710941791534, |
| "class_5": 0.0008780939970165491, |
| "class_6": 5.7844863476930186e-05 |
| } |
| }, |
| { |
| "lead_hours": 8, |
| "target_utc": "2026-05-09T02:00:00+00:00", |
| "target_local": "2026-05-08T19:00:00-07:00", |
| "temperature_2m_c": 14.042488098144531, |
| "relative_humidity_2m_pct": 69.15681457519531, |
| "apparent_temperature_c": 12.045327186584473, |
| "precipitation_mm": 0.003981542307883501, |
| "pressure_msl_hpa": 1017.853759765625, |
| "surface_pressure_hpa": 1017.2957763671875, |
| "cloud_cover_pct": 64.85920715332031, |
| "wind_speed_10m_kmh": 11.780016899108887, |
| "rain_probability": 0.23837216198444366, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.23561197519302368, |
| "class_1": 0.6629505753517151, |
| "class_2": 0.0012489539803937078, |
| "class_3": 0.04817439988255501, |
| "class_4": 0.050942711532115936, |
| "class_5": 0.0010061608627438545, |
| "class_6": 6.52461385470815e-05 |
| } |
| }, |
| { |
| "lead_hours": 9, |
| "target_utc": "2026-05-09T03:00:00+00:00", |
| "target_local": "2026-05-08T20:00:00-07:00", |
| "temperature_2m_c": 13.325971603393555, |
| "relative_humidity_2m_pct": 71.42361450195312, |
| "apparent_temperature_c": 11.300540924072266, |
| "precipitation_mm": 0.0030152045655995607, |
| "pressure_msl_hpa": 1017.9505004882812, |
| "surface_pressure_hpa": 1017.2774047851562, |
| "cloud_cover_pct": 63.037109375, |
| "wind_speed_10m_kmh": 11.165238380432129, |
| "rain_probability": 0.23051044344902039, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.24986012279987335, |
| "class_1": 0.6375465989112854, |
| "class_2": 0.0016154011245816946, |
| "class_3": 0.04874761775135994, |
| "class_4": 0.06096767634153366, |
| "class_5": 0.0012142135528847575, |
| "class_6": 4.836557855014689e-05 |
| } |
| }, |
| { |
| "lead_hours": 10, |
| "target_utc": "2026-05-09T04:00:00+00:00", |
| "target_local": "2026-05-08T21:00:00-07:00", |
| "temperature_2m_c": 12.574642181396484, |
| "relative_humidity_2m_pct": 73.7841796875, |
| "apparent_temperature_c": 10.549814224243164, |
| "precipitation_mm": 0.004971037618815899, |
| "pressure_msl_hpa": 1018.10400390625, |
| "surface_pressure_hpa": 1017.2828979492188, |
| "cloud_cover_pct": 61.4162483215332, |
| "wind_speed_10m_kmh": 10.5538911819458, |
| "rain_probability": 0.23788221180438995, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.27152255177497864, |
| "class_1": 0.6182448863983154, |
| "class_2": 0.0025821358431130648, |
| "class_3": 0.04885515570640564, |
| "class_4": 0.05706281587481499, |
| "class_5": 0.0016854261048138142, |
| "class_6": 4.704758248408325e-05 |
| } |
| }, |
| { |
| "lead_hours": 11, |
| "target_utc": "2026-05-09T05:00:00+00:00", |
| "target_local": "2026-05-08T22:00:00-07:00", |
| "temperature_2m_c": 11.85836124420166, |
| "relative_humidity_2m_pct": 75.99488830566406, |
| "apparent_temperature_c": 9.845465660095215, |
| "precipitation_mm": 0.0059099141508340836, |
| "pressure_msl_hpa": 1018.2722778320312, |
| "surface_pressure_hpa": 1017.3274536132812, |
| "cloud_cover_pct": 60.944053649902344, |
| "wind_speed_10m_kmh": 10.019789695739746, |
| "rain_probability": 0.24793456494808197, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.27306604385375977, |
| "class_1": 0.6107510328292847, |
| "class_2": 0.004449337720870972, |
| "class_3": 0.0498417466878891, |
| "class_4": 0.05973493307828903, |
| "class_5": 0.002092213137075305, |
| "class_6": 6.465442857006565e-05 |
| } |
| }, |
| { |
| "lead_hours": 12, |
| "target_utc": "2026-05-09T06:00:00+00:00", |
| "target_local": "2026-05-08T23:00:00-07:00", |
| "temperature_2m_c": 11.196554183959961, |
| "relative_humidity_2m_pct": 78.09349060058594, |
| "apparent_temperature_c": 9.231935501098633, |
| "precipitation_mm": 0.00701399240642786, |
| "pressure_msl_hpa": 1018.4185791015625, |
| "surface_pressure_hpa": 1017.3386840820312, |
| "cloud_cover_pct": 60.26295471191406, |
| "wind_speed_10m_kmh": 9.483912467956543, |
| "rain_probability": 0.2565533518791199, |
| "weather_class": 1, |
| "weather_class_name": "class_1", |
| "weather_class_probabilities": { |
| "class_0": 0.27720507979393005, |
| "class_1": 0.6052833795547485, |
| "class_2": 0.007175811566412449, |
| "class_3": 0.04983551800251007, |
| "class_4": 0.05781771242618561, |
| "class_5": 0.0026247103232890368, |
| "class_6": 5.782112566521391e-05 |
| } |
| } |
| ], |
| "sanity": { |
| "sequence_shape": [ |
| 72, |
| 22 |
| ], |
| "finite_features": true |
| } |
| } |
| ``` |
| |
| City=Nuuk |
| ``` |
| { |
| "city": "Nuuk", |
| "location_id": "83", |
| "model_location_id": 0, |
| "data_source": "open-meteo forecast api (past-hours context only)", |
| "requested_at_utc": "2026-05-08T20:40:35.109779+00:00", |
| "context": { |
| "hours": 72, |
| "start_utc": "2026-05-05T20:00:00+00:00", |
| "end_utc": "2026-05-08T19:00:00+00:00", |
| "start_local": "2026-05-05T19:00:00-01:00", |
| "end_local": "2026-05-08T18:00:00-01:00" |
| }, |
| "model": { |
| "encoder_type": "lstm", |
| "seq_len": 72, |
| "input_dim": 22, |
| "num_weather_classes": 7 |
| }, |
| "forecast": [ |
| { |
| "lead_hours": 1, |
| "target_utc": "2026-05-08T20:00:00+00:00", |
| "target_local": "2026-05-08T19:00:00-01:00", |
| "temperature_2m_c": 5.2753753662109375, |
| "relative_humidity_2m_pct": 93.01068115234375, |
| "apparent_temperature_c": 1.6396684646606445, |
| "precipitation_mm": 0.3556472063064575, |
| "pressure_msl_hpa": 1005.5432739257812, |
| "surface_pressure_hpa": 973.415771484375, |
| "cloud_cover_pct": 98.54638671875, |
| "wind_speed_10m_kmh": 13.717008590698242, |
| "rain_probability": 0.9789170026779175, |
| "weather_class": 3, |
| "weather_class_name": "class_3", |
| "weather_class_probabilities": { |
| "class_0": 0.000619232130702585, |
| "class_1": 0.057769663631916046, |
| "class_2": 0.003395488252863288, |
| "class_3": 0.401492714881897, |
| "class_4": 0.18206973373889923, |
| "class_5": 0.3545896112918854, |
| "class_6": 6.364739965647459e-05 |
| } |
| }, |
| { |
| "lead_hours": 2, |
| "target_utc": "2026-05-08T21:00:00+00:00", |
| "target_local": "2026-05-08T20:00:00-01:00", |
| "temperature_2m_c": 4.986588478088379, |
| "relative_humidity_2m_pct": 93.84243774414062, |
| "apparent_temperature_c": 1.352757453918457, |
| "precipitation_mm": 0.2776345908641815, |
| "pressure_msl_hpa": 1005.599853515625, |
| "surface_pressure_hpa": 973.536376953125, |
| "cloud_cover_pct": 98.45586395263672, |
| "wind_speed_10m_kmh": 13.445389747619629, |
| "rain_probability": 0.9556259512901306, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.00150326790753752, |
| "class_1": 0.09517476707696915, |
| "class_2": 0.004558710381388664, |
| "class_3": 0.32409900426864624, |
| "class_4": 0.1846529245376587, |
| "class_5": 0.38996437191963196, |
| "class_6": 4.702423757407814e-05 |
| } |
| }, |
| { |
| "lead_hours": 3, |
| "target_utc": "2026-05-08T22:00:00+00:00", |
| "target_local": "2026-05-08T21:00:00-01:00", |
| "temperature_2m_c": 4.788308143615723, |
| "relative_humidity_2m_pct": 94.0885238647461, |
| "apparent_temperature_c": 1.1667909622192383, |
| "precipitation_mm": 0.23039932548999786, |
| "pressure_msl_hpa": 1005.703857421875, |
| "surface_pressure_hpa": 973.5965576171875, |
| "cloud_cover_pct": 97.65797424316406, |
| "wind_speed_10m_kmh": 13.209211349487305, |
| "rain_probability": 0.9299042820930481, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.002542331349104643, |
| "class_1": 0.12809209525585175, |
| "class_2": 0.0058285691775381565, |
| "class_3": 0.3043138086795807, |
| "class_4": 0.17225369811058044, |
| "class_5": 0.3869440257549286, |
| "class_6": 2.5515650122542866e-05 |
| } |
| }, |
| { |
| "lead_hours": 4, |
| "target_utc": "2026-05-08T23:00:00+00:00", |
| "target_local": "2026-05-08T22:00:00-01:00", |
| "temperature_2m_c": 4.660131454467773, |
| "relative_humidity_2m_pct": 94.03594970703125, |
| "apparent_temperature_c": 1.0469179153442383, |
| "precipitation_mm": 0.19706439971923828, |
| "pressure_msl_hpa": 1005.7493896484375, |
| "surface_pressure_hpa": 973.7083740234375, |
| "cloud_cover_pct": 97.17306518554688, |
| "wind_speed_10m_kmh": 13.017566680908203, |
| "rain_probability": 0.9050359129905701, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.004407276399433613, |
| "class_1": 0.15539932250976562, |
| "class_2": 0.009657401591539383, |
| "class_3": 0.2794102430343628, |
| "class_4": 0.1521354615688324, |
| "class_5": 0.3989632725715637, |
| "class_6": 2.7043566660722718e-05 |
| } |
| }, |
| { |
| "lead_hours": 5, |
| "target_utc": "2026-05-09T00:00:00+00:00", |
| "target_local": "2026-05-08T23:00:00-01:00", |
| "temperature_2m_c": 4.5112457275390625, |
| "relative_humidity_2m_pct": 93.88682556152344, |
| "apparent_temperature_c": 0.9274702072143555, |
| "precipitation_mm": 0.1685791015625, |
| "pressure_msl_hpa": 1005.7725830078125, |
| "surface_pressure_hpa": 973.7322387695312, |
| "cloud_cover_pct": 96.03288269042969, |
| "wind_speed_10m_kmh": 12.944330215454102, |
| "rain_probability": 0.8804075121879578, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.006306177470833063, |
| "class_1": 0.17262385785579681, |
| "class_2": 0.00996350683271885, |
| "class_3": 0.2658991515636444, |
| "class_4": 0.1401163786649704, |
| "class_5": 0.4050598442554474, |
| "class_6": 3.1046529329614714e-05 |
| } |
| }, |
| { |
| "lead_hours": 6, |
| "target_utc": "2026-05-09T01:00:00+00:00", |
| "target_local": "2026-05-09T00:00:00-01:00", |
| "temperature_2m_c": 4.33610725402832, |
| "relative_humidity_2m_pct": 94.00520324707031, |
| "apparent_temperature_c": 0.7778654098510742, |
| "precipitation_mm": 0.14649224281311035, |
| "pressure_msl_hpa": 1005.8167114257812, |
| "surface_pressure_hpa": 973.7780151367188, |
| "cloud_cover_pct": 95.56141662597656, |
| "wind_speed_10m_kmh": 12.845012664794922, |
| "rain_probability": 0.8599434494972229, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.007768102455884218, |
| "class_1": 0.18894362449645996, |
| "class_2": 0.011767406016588211, |
| "class_3": 0.2329735904932022, |
| "class_4": 0.12322919070720673, |
| "class_5": 0.43529438972473145, |
| "class_6": 2.3752647393848747e-05 |
| } |
| }, |
| { |
| "lead_hours": 7, |
| "target_utc": "2026-05-09T02:00:00+00:00", |
| "target_local": "2026-05-09T01:00:00-01:00", |
| "temperature_2m_c": 4.140122413635254, |
| "relative_humidity_2m_pct": 94.25415802001953, |
| "apparent_temperature_c": 0.5866508483886719, |
| "precipitation_mm": 0.13218756020069122, |
| "pressure_msl_hpa": 1005.855712890625, |
| "surface_pressure_hpa": 973.7844848632812, |
| "cloud_cover_pct": 95.55270385742188, |
| "wind_speed_10m_kmh": 12.77564811706543, |
| "rain_probability": 0.8421469330787659, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.009295819327235222, |
| "class_1": 0.20261473953723907, |
| "class_2": 0.012845687568187714, |
| "class_3": 0.21367891132831573, |
| "class_4": 0.11506513506174088, |
| "class_5": 0.4464803636074066, |
| "class_6": 1.9363311366760172e-05 |
| } |
| }, |
| { |
| "lead_hours": 8, |
| "target_utc": "2026-05-09T03:00:00+00:00", |
| "target_local": "2026-05-09T02:00:00-01:00", |
| "temperature_2m_c": 3.953939437866211, |
| "relative_humidity_2m_pct": 94.34648895263672, |
| "apparent_temperature_c": 0.3805828094482422, |
| "precipitation_mm": 0.11994519084692001, |
| "pressure_msl_hpa": 1005.9537353515625, |
| "surface_pressure_hpa": 973.9803466796875, |
| "cloud_cover_pct": 95.32868957519531, |
| "wind_speed_10m_kmh": 12.735028266906738, |
| "rain_probability": 0.8208134174346924, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.011135051026940346, |
| "class_1": 0.21503449976444244, |
| "class_2": 0.015187171287834644, |
| "class_3": 0.18286095559597015, |
| "class_4": 0.1143169179558754, |
| "class_5": 0.46144354343414307, |
| "class_6": 2.182190291932784e-05 |
| } |
| }, |
| { |
| "lead_hours": 9, |
| "target_utc": "2026-05-09T04:00:00+00:00", |
| "target_local": "2026-05-09T03:00:00-01:00", |
| "temperature_2m_c": 3.826430320739746, |
| "relative_humidity_2m_pct": 94.16539001464844, |
| "apparent_temperature_c": 0.16225242614746094, |
| "precipitation_mm": 0.11211992800235748, |
| "pressure_msl_hpa": 1006.1436767578125, |
| "surface_pressure_hpa": 974.2029418945312, |
| "cloud_cover_pct": 94.6148681640625, |
| "wind_speed_10m_kmh": 12.761141777038574, |
| "rain_probability": 0.8099679350852966, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.013121353462338448, |
| "class_1": 0.23102521896362305, |
| "class_2": 0.017765367403626442, |
| "class_3": 0.1780213713645935, |
| "class_4": 0.12005121260881424, |
| "class_5": 0.4399879574775696, |
| "class_6": 2.7478810807224363e-05 |
| } |
| }, |
| { |
| "lead_hours": 10, |
| "target_utc": "2026-05-09T05:00:00+00:00", |
| "target_local": "2026-05-09T04:00:00-01:00", |
| "temperature_2m_c": 3.8089590072631836, |
| "relative_humidity_2m_pct": 93.53528594970703, |
| "apparent_temperature_c": 0.12103080749511719, |
| "precipitation_mm": 0.10691206157207489, |
| "pressure_msl_hpa": 1006.42529296875, |
| "surface_pressure_hpa": 974.4669189453125, |
| "cloud_cover_pct": 93.8226318359375, |
| "wind_speed_10m_kmh": 12.700864791870117, |
| "rain_probability": 0.797008216381073, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.014465805143117905, |
| "class_1": 0.22260957956314087, |
| "class_2": 0.018675586208701134, |
| "class_3": 0.15951745212078094, |
| "class_4": 0.10494350641965866, |
| "class_5": 0.47975844144821167, |
| "class_6": 2.9636566978297196e-05 |
| } |
| }, |
| { |
| "lead_hours": 11, |
| "target_utc": "2026-05-09T06:00:00+00:00", |
| "target_local": "2026-05-09T05:00:00-01:00", |
| "temperature_2m_c": 3.9785900115966797, |
| "relative_humidity_2m_pct": 92.22869110107422, |
| "apparent_temperature_c": 0.24558448791503906, |
| "precipitation_mm": 0.10196752846240997, |
| "pressure_msl_hpa": 1006.7659301757812, |
| "surface_pressure_hpa": 974.8555297851562, |
| "cloud_cover_pct": 92.74380493164062, |
| "wind_speed_10m_kmh": 12.71017837524414, |
| "rain_probability": 0.7881969213485718, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.01727476716041565, |
| "class_1": 0.232261523604393, |
| "class_2": 0.019182473421096802, |
| "class_3": 0.15716883540153503, |
| "class_4": 0.10425136238336563, |
| "class_5": 0.46981269121170044, |
| "class_6": 4.833983985008672e-05 |
| } |
| }, |
| { |
| "lead_hours": 12, |
| "target_utc": "2026-05-09T07:00:00+00:00", |
| "target_local": "2026-05-09T06:00:00-01:00", |
| "temperature_2m_c": 4.317435264587402, |
| "relative_humidity_2m_pct": 90.45832824707031, |
| "apparent_temperature_c": 0.6276102066040039, |
| "precipitation_mm": 0.09439859539270401, |
| "pressure_msl_hpa": 1007.0864868164062, |
| "surface_pressure_hpa": 975.1940307617188, |
| "cloud_cover_pct": 91.2315673828125, |
| "wind_speed_10m_kmh": 12.740204811096191, |
| "rain_probability": 0.7812836170196533, |
| "weather_class": 5, |
| "weather_class_name": "class_5", |
| "weather_class_probabilities": { |
| "class_0": 0.019589632749557495, |
| "class_1": 0.23847728967666626, |
| "class_2": 0.020689163357019424, |
| "class_3": 0.159035325050354, |
| "class_4": 0.08879318088293076, |
| "class_5": 0.47335243225097656, |
| "class_6": 6.291128374869004e-05 |
| } |
| } |
| ], |
| "sanity": { |
| "sequence_shape": [ |
| 72, |
| 22 |
| ], |
| "finite_features": true |
| } |
| } |
| ``` |
| |
| ### Note |
| In observed outputs, the model is often within **1°C** of the actual value, which is **0.7** more than Hweh-6M. |
| |
| Furthermore, you can pass locations that are not present in the model’s location embedding table. We’ve observed that the model can generalize to out-of-distribution (OOD) cities, with an estimated accuracy drop of only about 2–5%. However, this figure is an estimate and does not reflect a true ground-truth measurement. |
| |
| ## Use Cases |
| |
| Intended for: |
| |
| 1. Backup to API |
| 2. Offline forecasting if you have the data |
| 3. Research |
| 4. Or more simply, for fun |
| |
| Not intended for: |
| |
| 1. Safety-critical forecasting (aviation, emergency response) |
| 2. Replacing meteorological or API services |
| |
| ## Limitations |
| |
| 1. The model is not perfectly accurate and will produce approximate forecasts rather than exact real-world weather conditions. |
| 2. Prediction accuracy decreases as the forecast horizon increases up to 12 hours. |
| 3. Performance may degrade on unseen or underrepresented geographic regions and climate types. |
| 4. The model does not enforce physical laws of atmospheric dynamics and may produce physically inconsistent outputs. |
| 5. Forecast quality is sensitive to the quality and completeness of input weather data. |
| 6. Rare or extreme weather events are underrepresented in training data and may be poorly predicted. |
| 7. Weather class outputs are simplified and do not capture fine-grained meteorological distinctions. |
| |
| # Inference |
| |
| ```python |
| #!/usr/bin/env python3 |
| from __future__ import annotations |
| |
| import json |
| import time |
| from pathlib import Path |
| from typing import Any |
| |
| import numpy as np |
| import pandas as pd |
| import requests |
| import torch |
| from transformers import AutoConfig, AutoModel |
| from zoneinfo import ZoneInfo |
| |
| # ---------------------------- |
| # Change these values here |
| # ---------------------------- |
| MODEL_ID = r"Harley-ml/Hweh-446k" # HF repo id or local path |
| CITY = "New York" |
| SEQUENCE_META_PATH = "Harley-ml/Hweh-446k/weather_sequences.metadata.json" |
| CONTEXT_HOURS = 72 |
| FORECAST_HOURS = 12 |
| DEVICE = None # "cpu", "cuda", "cuda:0", or None for auto |
| |
| API_BASE_URL = "https://api.open-meteo.com/v1/forecast" |
| MAX_RETRIES = 6 |
| REQUEST_TIMEOUT_S = 60 |
| |
| HOURLY_VARS = [ |
| "temperature_2m", |
| "relative_humidity_2m", |
| "apparent_temperature", |
| "precipitation", |
| "weather_code", |
| "pressure_msl", |
| "surface_pressure", |
| "cloud_cover", |
| "visibility", |
| "wind_speed_10m", |
| "wind_direction_10m", |
| ] |
| |
| WEATHER_CODE_BUCKETS = 7 |
| TEMP_SCALE = 50.0 |
| HUMIDITY_SCALE = 100.0 |
| WIND_SCALE = 100.0 |
| |
| # ---------------------------- |
| # City metadata (82 locations) |
| # ---------------------------- |
| CITY_SPECS: dict[str, dict[str, Any]] = { |
| "Seattle": {"location_id": "1", "latitude": 47.6062, "longitude": -122.3321, "continent": "North America", "climate_tag": "temperate_oceanic", "elevation": 56}, |
| "Portland": {"location_id": "2", "latitude": 45.5152, "longitude": -122.6784, "continent": "North America", "climate_tag": "temperate_oceanic", "elevation": 15}, |
| "San Francisco": {"location_id": "3", "latitude": 37.7749, "longitude": -122.4194, "continent": "North America", "climate_tag": "foggy_mediterranean", "elevation": 16}, |
| "Los Angeles": {"location_id": "4", "latitude": 34.0522, "longitude": -118.2437, "continent": "North America", "climate_tag": "sunny_mediterranean", "elevation": 71}, |
| "Denver": {"location_id": "5", "latitude": 39.7392, "longitude": -104.9903, "continent": "North America", "climate_tag": "semi_arid_highland", "elevation": 1609}, |
| "Chicago": {"location_id": "6", "latitude": 41.8781, "longitude": -87.6298, "continent": "North America", "climate_tag": "humid_continental", "elevation": 181}, |
| "Dallas": {"location_id": "7", "latitude": 32.7767, "longitude": -96.7970, "continent": "North America", "climate_tag": "hot_subhumid", "elevation": 131}, |
| "Atlanta": {"location_id": "8", "latitude": 33.7490, "longitude": -84.3880, "continent": "North America", "climate_tag": "humid_subtropical", "elevation": 320}, |
| "New York": {"location_id": "9", "latitude": 40.7128, "longitude": -74.0060, "continent": "North America", "climate_tag": "humid_subtropical", "elevation": 10}, |
| "Miami": {"location_id": "10", "latitude": 25.7617, "longitude": -80.1918, "continent": "North America", "climate_tag": "tropical_humid", "elevation": 2}, |
| "Phoenix": {"location_id": "11", "latitude": 33.4484, "longitude": -112.0740, "continent": "North America", "climate_tag": "hot_arid", "elevation": 331}, |
| "Salt Lake City": {"location_id": "12", "latitude": 40.7608, "longitude": -111.8910, "continent": "North America", "climate_tag": "semi_arid", "elevation": 1288}, |
| "Anchorage": {"location_id": "13", "latitude": 61.2181, "longitude": -149.9003, "continent": "North America", "climate_tag": "subarctic_snowy", "elevation": 31}, |
| "Minneapolis": {"location_id": "14", "latitude": 44.9778, "longitude": -93.2650, "continent": "North America", "climate_tag": "cold_snowy", "elevation": 264}, |
| "Toronto": {"location_id": "15", "latitude": 43.6532, "longitude": -79.3832, "continent": "North America", "climate_tag": "humid_continental", "elevation": 76}, |
| "Montreal": {"location_id": "16", "latitude": 45.5017, "longitude": -73.5673, "continent": "North America", "climate_tag": "cold_snowy", "elevation": 233}, |
| "Vancouver": {"location_id": "17", "latitude": 49.2827, "longitude": -123.1207, "continent": "North America", "climate_tag": "temperate_oceanic", "elevation": 70}, |
| "Mexico City": {"location_id": "18", "latitude": 19.4326, "longitude": -99.1332, "continent": "North America", "climate_tag": "highland_subtropical", "elevation": 2240}, |
| "Havana": {"location_id": "19", "latitude": 23.1136, "longitude": -82.3666, "continent": "North America", "climate_tag": "tropical_humid", "elevation": 59}, |
| "San Juan": {"location_id": "20", "latitude": 18.4655, "longitude": -66.1057, "continent": "North America", "climate_tag": "tropical_humid", "elevation": 8}, |
| |
| "Lima": {"location_id": "21", "latitude": -12.0464, "longitude": -77.0428, "continent": "South America", "climate_tag": "coastal_arid", "elevation": 154}, |
| "Santiago": {"location_id": "22", "latitude": -33.4489, "longitude": -70.6693, "continent": "South America", "climate_tag": "mediterranean", "elevation": 520}, |
| "Buenos Aires": {"location_id": "23", "latitude": -34.6037, "longitude": -58.3816, "continent": "South America", "climate_tag": "humid_subtropical", "elevation": 25}, |
| "Bogotá": {"location_id": "24", "latitude": 4.7110, "longitude": -74.0721, "continent": "South America", "climate_tag": "highland_cool", "elevation": 2640}, |
| "Quito": {"location_id": "25", "latitude": -0.1807, "longitude": -78.4678, "continent": "South America", "climate_tag": "highland_equatorial", "elevation": 2850}, |
| "Caracas": {"location_id": "26", "latitude": 10.4806, "longitude": -66.9036, "continent": "South America", "climate_tag": "tropical_humid", "elevation": 900}, |
| "Rio de Janeiro": {"location_id": "27", "latitude": -22.9068, "longitude": -43.1729, "continent": "South America", "climate_tag": "tropical_humid", "elevation": 5}, |
| "São Paulo": {"location_id": "28", "latitude": -23.5505, "longitude": -46.6333, "continent": "South America", "climate_tag": "humid_subtropical", "elevation": 760}, |
| "La Paz": {"location_id": "29", "latitude": -16.4897, "longitude": -68.1193, "continent": "South America", "climate_tag": "highland_cold", "elevation": 3640}, |
| "Cusco": {"location_id": "30", "latitude": -13.5319, "longitude": -71.9675, "continent": "South America", "climate_tag": "highland_cool", "elevation": 3399}, |
| "Montevideo": {"location_id": "31", "latitude": -34.9011, "longitude": -56.1645, "continent": "South America", "climate_tag": "temperate_oceanic", "elevation": 43}, |
| "Asunción": {"location_id": "32", "latitude": -25.2637, "longitude": -57.5759, "continent": "South America", "climate_tag": "humid_subtropical", "elevation": 43}, |
| "Manaus": {"location_id": "33", "latitude": -3.1190, "longitude": -60.0217, "continent": "South America", "climate_tag": "tropical_humid", "elevation": 92}, |
| "Recife": {"location_id": "34", "latitude": -8.0476, "longitude": -34.8770, "continent": "South America", "climate_tag": "tropical_coastal", "elevation": 4}, |
| "Punta Arenas": {"location_id": "35", "latitude": -53.1638, "longitude": -70.9171, "continent": "South America", "climate_tag": "cold_windy", "elevation": 34}, |
| |
| "London": {"location_id": "36", "latitude": 51.5074, "longitude": -0.1278, "continent": "Europe", "climate_tag": "temperate_oceanic", "elevation": 11}, |
| "Paris": {"location_id": "37", "latitude": 48.8566, "longitude": 2.3522, "continent": "Europe", "climate_tag": "temperate_oceanic", "elevation": 35}, |
| "Madrid": {"location_id": "38", "latitude": 40.4168, "longitude": -3.7038, "continent": "Europe", "climate_tag": "hot_summer_mediterranean", "elevation": 667}, |
| "Rome": {"location_id": "39", "latitude": 41.9028, "longitude": 12.4964, "continent": "Europe", "climate_tag": "hot_summer_mediterranean", "elevation": 21}, |
| "Berlin": {"location_id": "40", "latitude": 52.52, "longitude": 13.4050, "continent": "Europe", "climate_tag": "temperate_continental", "elevation": 34}, |
| "Stockholm": {"location_id": "41", "latitude": 59.3293, "longitude": 18.0686, "continent": "Europe", "climate_tag": "cold_marine", "elevation": 28}, |
| "Oslo": {"location_id": "42", "latitude": 59.9139, "longitude": 10.7522, "continent": "Europe", "climate_tag": "cold_snowy", "elevation": 23}, |
| "Helsinki": {"location_id": "43", "latitude": 60.1699, "longitude": 24.9384, "continent": "Europe", "climate_tag": "cold_snowy", "elevation": 25}, |
| "Reykjavik": {"location_id": "44", "latitude": 64.1466, "longitude": -21.9426, "continent": "Europe", "climate_tag": "cold_windy", "elevation": 12}, |
| "Kyiv": {"location_id": "45", "latitude": 50.4501, "longitude": 30.5234, "continent": "Europe", "climate_tag": "humid_continental", "elevation": 179}, |
| "Lisbon": {"location_id": "46", "latitude": 38.7223, "longitude": -9.1393, "continent": "Europe", "climate_tag": "sunny_mediterranean", "elevation": 7}, |
| "Athens": {"location_id": "47", "latitude": 37.9838, "longitude": 23.7275, "continent": "Europe", "climate_tag": "sunny_mediterranean", "elevation": 70}, |
| "Zurich": {"location_id": "48", "latitude": 47.3769, "longitude": 8.5417, "continent": "Europe", "climate_tag": "temperate_continental", "elevation": 408}, |
| "Dublin": {"location_id": "49", "latitude": 53.3498, "longitude": -6.2603, "continent": "Europe", "climate_tag": "temperate_oceanic", "elevation": 20}, |
| "Vienna": {"location_id": "50", "latitude": 48.2082, "longitude": 16.3738, "continent": "Europe", "climate_tag": "temperate_continental", "elevation": 171}, |
| |
| "Dubai": {"location_id": "51", "latitude": 25.2048, "longitude": 55.2708, "continent": "Asia", "climate_tag": "hot_arid", "elevation": 16}, |
| "Riyadh": {"location_id": "52", "latitude": 24.7136, "longitude": 46.6753, "continent": "Asia", "climate_tag": "hot_arid", "elevation": 612}, |
| "Delhi": {"location_id": "53", "latitude": 28.7041, "longitude": 77.1025, "continent": "Asia", "climate_tag": "hot_semi_arid", "elevation": 216}, |
| "Mumbai": {"location_id": "54", "latitude": 19.0760, "longitude": 72.8777, "continent": "Asia", "climate_tag": "tropical_humid", "elevation": 14}, |
| "Bangkok": {"location_id": "55", "latitude": 13.7563, "longitude": 100.5018, "continent": "Asia", "climate_tag": "tropical_monsoon", "elevation": 2}, |
| "Singapore": {"location_id": "56", "latitude": 1.3521, "longitude": 103.8198, "continent": "Asia", "climate_tag": "tropical_humid", "elevation": 15}, |
| "Tokyo": {"location_id": "57", "latitude": 35.6762, "longitude": 139.6503, "continent": "Asia", "climate_tag": "humid_subtropical", "elevation": 40}, |
| "Seoul": {"location_id": "58", "latitude": 37.5665, "longitude": 126.9780, "continent": "Asia", "climate_tag": "humid_continental", "elevation": 38}, |
| "Ulaanbaatar": {"location_id": "59", "latitude": 47.8864, "longitude": 106.9057, "continent": "Asia", "climate_tag": "cold_steppe", "elevation": 1350}, |
| "Kathmandu": {"location_id": "60", "latitude": 27.7172, "longitude": 85.3240, "continent": "Asia", "climate_tag": "highland_subtropical", "elevation": 1400}, |
| "Chiang Mai": {"location_id": "61", "latitude": 18.7883, "longitude": 98.9853, "continent": "Asia", "climate_tag": "tropical_seasonal", "elevation": 300}, |
| "Lhasa": {"location_id": "62", "latitude": 29.6520, "longitude": 91.1721, "continent": "Asia", "climate_tag": "high_altitude_cold", "elevation": 3656}, |
| "Jakarta": {"location_id": "63", "latitude": -6.2088, "longitude": 106.8456, "continent": "Asia", "climate_tag": "tropical_humid", "elevation": 8}, |
| "Manila": {"location_id": "64", "latitude": 14.5995, "longitude": 120.9842, "continent": "Asia", "climate_tag": "tropical_humid", "elevation": 16}, |
| "Karachi": {"location_id": "65", "latitude": 24.8607, "longitude": 67.0011, "continent": "Asia", "climate_tag": "hot_arid", "elevation": 10}, |
| |
| "Cairo": {"location_id": "66", "latitude": 30.0444, "longitude": 31.2357, "continent": "Africa", "climate_tag": "hot_arid", "elevation": 23}, |
| "Alexandria": {"location_id": "67", "latitude": 31.2001, "longitude": 29.9187, "continent": "Africa", "climate_tag": "coastal_mediterranean", "elevation": 5}, |
| "Casablanca": {"location_id": "68", "latitude": 33.5731, "longitude": -7.5898, "continent": "Africa", "climate_tag": "coastal_mediterranean", "elevation": 56}, |
| "Marrakech": {"location_id": "69", "latitude": 31.6295, "longitude": -7.9811, "continent": "Africa", "climate_tag": "hot_semi_arid", "elevation": 466}, |
| "Lagos": {"location_id": "70", "latitude": 6.5244, "longitude": 3.3792, "continent": "Africa", "climate_tag": "tropical_humid", "elevation": 41}, |
| "Nairobi": {"location_id": "71", "latitude": -1.2921, "longitude": 36.8219, "continent": "Africa", "climate_tag": "temperate_highland", "elevation": 1795}, |
| "Addis Ababa": {"location_id": "72", "latitude": 8.9806, "longitude": 38.7578, "continent": "Africa", "climate_tag": "temperate_highland", "elevation": 2355}, |
| "Cape Town": {"location_id": "73", "latitude": -33.9249, "longitude": 18.4241, "continent": "Africa", "climate_tag": "mediterranean", "elevation": 25}, |
| "Johannesburg": {"location_id": "74", "latitude": -26.2041, "longitude": 28.0473, "continent": "Africa", "climate_tag": "subtropical_highland", "elevation": 1753}, |
| "Windhoek": {"location_id": "75", "latitude": -22.5609, "longitude": 17.0658, "continent": "Africa", "climate_tag": "semi_arid", "elevation": 1650}, |
| "Accra": {"location_id": "76", "latitude": 5.6037, "longitude": -0.1870, "continent": "Africa", "climate_tag": "tropical_humid", "elevation": 61}, |
| "Kigali": {"location_id": "77", "latitude": -1.9441, "longitude": 30.0619, "continent": "Africa", "climate_tag": "highland_tropical", "elevation": 1567}, |
| "Tunis": {"location_id": "78", "latitude": 36.8065, "longitude": 10.1815, "continent": "Africa", "climate_tag": "mediterranean", "elevation": 4}, |
| "Dakar": {"location_id": "79", "latitude": -14.7167, "longitude": -17.4677, "continent": "Africa", "climate_tag": "hot_coastal", "elevation": 25}, |
| "Mombasa": {"location_id": "80", "latitude": -4.0435, "longitude": 39.6682, "continent": "Africa", "climate_tag": "tropical_coastal", "elevation": 17}, |
| |
| "Sydney": {"location_id": "81", "latitude": -33.8688, "longitude": 151.2093, "continent": "Oceania", "climate_tag": "humid_subtropical", "elevation": 58}, |
| "Melbourne": {"location_id": "82", "latitude": -37.8136, "longitude": 144.9631, "continent": "Oceania", "climate_tag": "temperate_oceanic", "elevation": 31}, |
| } |
| |
| CITY_TIMEZONES: dict[str, str] = { |
| "Seattle": "America/Los_Angeles", |
| "Portland": "America/Los_Angeles", |
| "San Francisco": "America/Los_Angeles", |
| "Los Angeles": "America/Los_Angeles", |
| "Denver": "America/Denver", |
| "Chicago": "America/Chicago", |
| "Dallas": "America/Chicago", |
| "Atlanta": "America/New_York", |
| "New York": "America/New_York", |
| "Miami": "America/New_York", |
| "Phoenix": "America/Phoenix", |
| "Salt Lake City": "America/Denver", |
| "Anchorage": "America/Anchorage", |
| "Minneapolis": "America/Chicago", |
| "Toronto": "America/Toronto", |
| "Montreal": "America/Toronto", |
| "Vancouver": "America/Vancouver", |
| "Mexico City": "America/Mexico_City", |
| "Havana": "America/Havana", |
| "San Juan": "America/Puerto_Rico", |
| "Lima": "America/Lima", |
| "Santiago": "America/Santiago", |
| "Buenos Aires": "America/Argentina/Buenos_Aires", |
| "Bogotá": "America/Bogota", |
| "Quito": "America/Guayaquil", |
| "Caracas": "America/Caracas", |
| "Rio de Janeiro": "America/Sao_Paulo", |
| "São Paulo": "America/Sao_Paulo", |
| "La Paz": "America/La_Paz", |
| "Cusco": "America/Lima", |
| "Montevideo": "America/Montevideo", |
| "Asunción": "America/Asuncion", |
| "Manaus": "America/Manaus", |
| "Recife": "America/Recife", |
| "Punta Arenas": "America/Punta_Arenas", |
| "London": "Europe/London", |
| "Paris": "Europe/Paris", |
| "Madrid": "Europe/Madrid", |
| "Rome": "Europe/Rome", |
| "Berlin": "Europe/Berlin", |
| "Stockholm": "Europe/Stockholm", |
| "Oslo": "Europe/Oslo", |
| "Helsinki": "Europe/Helsinki", |
| "Reykjavik": "Atlantic/Reykjavik", |
| "Kyiv": "Europe/Kyiv", |
| "Lisbon": "Europe/Lisbon", |
| "Athens": "Europe/Athens", |
| "Zurich": "Europe/Zurich", |
| "Dublin": "Europe/Dublin", |
| "Vienna": "Europe/Vienna", |
| "Dubai": "Asia/Dubai", |
| "Riyadh": "Asia/Riyadh", |
| "Delhi": "Asia/Kolkata", |
| "Mumbai": "Asia/Kolkata", |
| "Bangkok": "Asia/Bangkok", |
| "Singapore": "Asia/Singapore", |
| "Tokyo": "Asia/Tokyo", |
| "Seoul": "Asia/Seoul", |
| "Ulaanbaatar": "Asia/Ulaanbaatar", |
| "Kathmandu": "Asia/Kathmandu", |
| "Chiang Mai": "Asia/Bangkok", |
| "Lhasa": "Asia/Shanghai", |
| "Jakarta": "Asia/Jakarta", |
| "Manila": "Asia/Manila", |
| "Karachi": "Asia/Karachi", |
| "Cairo": "Africa/Cairo", |
| "Alexandria": "Africa/Cairo", |
| "Casablanca": "Africa/Casablanca", |
| "Marrakech": "Africa/Casablanca", |
| "Lagos": "Africa/Lagos", |
| "Nairobi": "Africa/Nairobi", |
| "Addis Ababa": "Africa/Addis_Ababa", |
| "Cape Town": "Africa/Johannesburg", |
| "Johannesburg": "Africa/Johannesburg", |
| "Windhoek": "Africa/Windhoek", |
| "Accra": "Africa/Accra", |
| "Kigali": "Africa/Kigali", |
| "Tunis": "Africa/Tunis", |
| "Dakar": "Africa/Dakar", |
| "Mombasa": "Africa/Nairobi", |
| "Sydney": "Australia/Sydney", |
| "Melbourne": "Australia/Melbourne", |
| } |
| |
| # ---------------------------- |
| # Helpers |
| # ---------------------------- |
| def weather_code_to_bucket(code) -> int: |
| if code is None: |
| return 1 |
| try: |
| if pd.isna(code): |
| return 1 |
| except Exception: |
| pass |
| |
| code = int(code) |
| if code == 0: |
| return 0 |
| if code in (1, 2, 3): |
| return 1 |
| if code in (45, 48): |
| return 2 |
| if code in (51, 53, 55, 56, 57): |
| return 3 |
| if code in (61, 63, 65, 66, 67, 80, 81, 82): |
| return 4 |
| if code in (71, 73, 75, 77, 85, 86): |
| return 5 |
| if code in (95, 96, 99): |
| return 6 |
| return 1 |
| |
| |
| def cyc(x: np.ndarray, period: float) -> tuple[np.ndarray, np.ndarray]: |
| angle = 2.0 * np.pi * (x / period) |
| return np.sin(angle), np.cos(angle) |
| |
| |
| def clamp_array(x: np.ndarray, lo: float | None = None, hi: float | None = None) -> np.ndarray: |
| return np.clip(x, lo, hi) |
| |
| |
| def request_with_backoff(session: requests.Session, url: str, params: dict[str, Any]) -> dict[str, Any]: |
| last_exc: Exception | None = None |
| for attempt in range(MAX_RETRIES): |
| try: |
| resp = session.get(url, params=params, timeout=REQUEST_TIMEOUT_S) |
| if resp.status_code == 429: |
| retry_after = resp.headers.get("Retry-After") |
| sleep_s = float(retry_after) if retry_after else min(60.0, 2**attempt) |
| print(f"Rate limited. Sleeping {sleep_s:.1f}s and retrying.", flush=True) |
| time.sleep(sleep_s) |
| continue |
| resp.raise_for_status() |
| return resp.json() |
| except Exception as e: |
| last_exc = e |
| sleep_s = min(60.0, 2**attempt) |
| print(f"Request failed: {e}. Sleeping {sleep_s:.1f}s and retrying.", flush=True) |
| time.sleep(sleep_s) |
| raise RuntimeError(f"Failed after {MAX_RETRIES} retries: {params}") from last_exc |
| |
|
|
| def load_sequence_meta(path: str) -> dict[str, Any]: |
| p = Path(path) |
| if not p.exists(): |
| return {"location_to_id": {}} |
| with open(p, "r", encoding="utf-8") as f: |
| meta = json.load(f) |
| meta.setdefault("location_to_id", {}) |
| return meta |
| |
|
|
| def load_model(): |
| config = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True) |
| model = AutoModel.from_pretrained(MODEL_ID, config=config, trust_remote_code=True) |
| model.eval() |
| return model, config |
| |
| |
| def fetch_recent_history(city: str, context_hours: int) -> pd.DataFrame: |
| if city not in CITY_SPECS: |
| raise ValueError(f"Unknown city: {city}") |
| |
| spec = CITY_SPECS[city] |
| session = requests.Session() |
| session.headers.update({"User-Agent": "Mozilla/5.0"}) |
| |
| params = { |
| "latitude": spec["latitude"], |
| "longitude": spec["longitude"], |
| "hourly": ",".join(HOURLY_VARS), |
| "timezone": "UTC", |
| "temperature_unit": "celsius", |
| "wind_speed_unit": "kmh", |
| "precipitation_unit": "mm", |
| "past_hours": int(context_hours) + 2, |
| "forecast_hours": 0, |
| } |
| |
| data = request_with_backoff(session, API_BASE_URL, params=params) |
| hourly = data.get("hourly", {}) |
| if "time" not in hourly: |
| raise ValueError(f"No hourly data returned for {city}: {data}") |
| |
| df = pd.DataFrame(hourly) |
| if df.empty: |
| raise ValueError(f"Empty hourly response for {city}.") |
| |
| df["time"] = pd.to_datetime(df["time"], errors="coerce", utc=True) |
| df = df.dropna(subset=["time"]).sort_values("time").drop_duplicates(subset=["time"]).reset_index(drop=True) |
| |
| needed = HOURLY_VARS |
| missing = [c for c in needed if c not in df.columns] |
| if missing: |
| raise ValueError(f"Missing hourly columns in API response: {missing}") |
| |
| for c in needed: |
| df[c] = pd.to_numeric(df[c], errors="coerce") |
| |
| df["weather_code"] = df["weather_code"].fillna(1) |
| df["precipitation"] = df["precipitation"].fillna(0.0) |
| |
| for c in [ |
| "temperature_2m", |
| "relative_humidity_2m", |
| "apparent_temperature", |
| "precipitation", |
| "pressure_msl", |
| "surface_pressure", |
| "cloud_cover", |
| "visibility", |
| "wind_speed_10m", |
| "wind_direction_10m", |
| ]: |
| df[c] = df[c].interpolate(limit_direction="both").ffill().bfill() |
| |
| now_utc = pd.Timestamp.now(tz="UTC") |
| df = df[df["time"] <= now_utc].copy() |
| |
| if len(df) < context_hours: |
| raise ValueError(f"Not enough observed rows: got {len(df)}, need {context_hours}") |
| |
| return df.tail(context_hours).reset_index(drop=True) |
| |
|
|
| def build_single_sequence(df: pd.DataFrame) -> np.ndarray: |
| hour = df["time"].dt.hour.to_numpy() |
| doy = df["time"].dt.dayofyear.to_numpy() |
| |
| hour_sin, hour_cos = cyc(hour.astype(float), 24.0) |
| doy_sin, doy_cos = cyc(doy.astype(float), 365.25) |
| |
| temp = np.nan_to_num(df["temperature_2m"].astype(float).to_numpy(), nan=0.0) |
| humidity = np.nan_to_num(df["relative_humidity_2m"].astype(float).to_numpy(), nan=0.0) |
| apparent = np.nan_to_num(df["apparent_temperature"].astype(float).to_numpy(), nan=0.0) |
| precip = np.nan_to_num(df["precipitation"].astype(float).to_numpy(), nan=0.0) |
| pressure = np.nan_to_num(df["pressure_msl"].astype(float).to_numpy(), nan=0.0) |
| surface_pressure = np.nan_to_num(df["surface_pressure"].astype(float).to_numpy(), nan=0.0) |
| cloud_cover = np.nan_to_num(df["cloud_cover"].astype(float).to_numpy(), nan=0.0) |
| visibility = np.nan_to_num(df["visibility"].astype(float).to_numpy(), nan=0.0) |
| wind = np.nan_to_num(df["wind_speed_10m"].astype(float).to_numpy(), nan=0.0) |
| wind_dir = np.nan_to_num(df["wind_direction_10m"].astype(float).to_numpy(), nan=0.0) |
| |
| humidity = clamp_array(humidity, 0.0, 100.0) |
| cloud_cover = clamp_array(cloud_cover, 0.0, 100.0) |
| precip = clamp_array(precip, 0.0, None) |
| wind = clamp_array(wind, 0.0, None) |
| visibility = clamp_array(visibility, 0.0, None) |
| |
| wind_dir_sin, wind_dir_cos = cyc(wind_dir, 360.0) |
| weather_bucket = df["weather_code"].fillna(1).apply(weather_code_to_bucket).to_numpy(dtype=np.int64) |
| |
| rows = [] |
| for i in range(len(df)): |
| wc_oh = np.zeros(WEATHER_CODE_BUCKETS, dtype=np.float32) |
| wc_oh[weather_bucket[i]] = 1.0 |
| |
| row = np.concatenate( |
| [ |
| np.array( |
| [ |
| temp[i] / TEMP_SCALE, |
| humidity[i] / HUMIDITY_SCALE, |
| apparent[i] / TEMP_SCALE, |
| np.log1p(max(precip[i], 0.0)) / 3.0, |
| pressure[i] / 1100.0, |
| surface_pressure[i] / 1100.0, |
| cloud_cover[i] / 100.0, |
| visibility[i] / 50000.0, |
| wind[i] / WIND_SCALE, |
| wind_dir_sin[i], |
| wind_dir_cos[i], |
| hour_sin[i], |
| hour_cos[i], |
| doy_sin[i], |
| doy_cos[i], |
| ], |
| dtype=np.float32, |
| ), |
| wc_oh, |
| ] |
| ) |
| rows.append(row) |
| |
| seq = np.asarray(rows, dtype=np.float32) |
| |
| if not np.isfinite(seq).all(): |
| bad = np.argwhere(~np.isfinite(seq)) |
| raise ValueError(f"Non-finite values remain in sequence at positions like: {bad[:10].tolist()}") |
| |
| return seq |
| |
|
|
| def to_iso(ts: pd.Timestamp, tz_name: str | None = None) -> str: |
| if tz_name: |
| try: |
| return ts.tz_convert(ZoneInfo(tz_name)).isoformat() |
| except Exception: |
| pass |
| return ts.isoformat() |
| |
|
|
| def get_logits(out): |
| if isinstance(out, dict) and "logits" in out: |
| return out["logits"] |
| if hasattr(out, "logits"): |
| return out.logits |
| return out |
| |
| |
| def resolve_location_index(seq_meta: dict[str, Any], city_location_id: str) -> int: |
| location_to_id = seq_meta.get("location_to_id", {}) |
| |
| if city_location_id in location_to_id: |
| return int(location_to_id[city_location_id]) |
| |
| try: |
| as_int = int(city_location_id) |
| if as_int in location_to_id: |
| return int(location_to_id[as_int]) |
| if str(as_int) in location_to_id: |
| return int(location_to_id[str(as_int)]) |
| except Exception: |
| pass |
| |
| for unk_key in ("UNK", "<UNK>", "unknown", "UNKNOWN"): |
| if unk_key in location_to_id: |
| return int(location_to_id[unk_key]) |
| |
| return 0 |
| |
|
|
| def predict(): |
| seq_meta = load_sequence_meta(SEQUENCE_META_PATH) |
| model, config = load_model() |
| |
| if CITY not in CITY_SPECS: |
| raise ValueError(f"Unknown city: {CITY}") |
| |
| if CONTEXT_HOURS <= 0: |
| raise ValueError("CONTEXT_HOURS must be > 0") |
| |
| if hasattr(config, "seq_len") and int(config.seq_len) != CONTEXT_HOURS: |
| raise ValueError(f"Set CONTEXT_HOURS to {int(config.seq_len)} for this model.") |
| |
| city_spec = CITY_SPECS[CITY] |
| city_tz = CITY_TIMEZONES.get(CITY, "UTC") |
| model_location_id = resolve_location_index(seq_meta, str(city_spec["location_id"])) |
| |
| df = fetch_recent_history(CITY, CONTEXT_HOURS) |
| seq = build_single_sequence(df) |
| |
| X = torch.from_numpy(seq).unsqueeze(0) |
| loc = torch.tensor([model_location_id], dtype=torch.long) |
| |
| target_device = torch.device( |
| DEVICE if DEVICE else ("cuda" if torch.cuda.is_available() else "cpu") |
| ) |
| model = model.to(target_device) |
| X = X.to(target_device) |
| loc = loc.to(target_device) |
| |
| weather_class_names = getattr(config, "weather_class_names", None) |
| if not weather_class_names: |
| weather_class_names = [f"class_{i}" for i in range(int(getattr(config, "num_weather_classes", 7)))] |
| |
| with torch.no_grad(): |
| out = model(X=X, location_id=loc) |
| logits = get_logits(out) |
| |
| ( |
| temp_pred, |
| humidity_pred, |
| apparent_pred, |
| precip_pred, |
| sea_level_pressure_pred, |
| surface_pressure_pred, |
| cloud_cover_pred, |
| wind_pred, |
| wind_dir_sin_pred, |
| wind_dir_cos_pred, |
| rain_logit, |
| weather_logits, |
| ) = logits |
| |
| temp_pred = temp_pred.squeeze(0).detach().cpu().numpy() |
| humidity_pred = humidity_pred.squeeze(0).detach().cpu().numpy() |
| apparent_pred = apparent_pred.squeeze(0).detach().cpu().numpy() |
| precip_pred = precip_pred.squeeze(0).detach().cpu().numpy() |
| sea_level_pressure_pred = sea_level_pressure_pred.squeeze(0).detach().cpu().numpy() |
| surface_pressure_pred = surface_pressure_pred.squeeze(0).detach().cpu().numpy() |
| cloud_cover_pred = cloud_cover_pred.squeeze(0).detach().cpu().numpy() |
| wind_pred = wind_pred.squeeze(0).detach().cpu().numpy() |
| rain_prob = torch.sigmoid(rain_logit).squeeze(0).detach().cpu().numpy() |
| weather_probs = torch.softmax(weather_logits, dim=-1).squeeze(0).detach().cpu().numpy() |
| weather_idx = np.argmax(weather_probs, axis=-1).astype(np.int64) |
| |
| humidity_pred = np.clip(humidity_pred, 0.0, 100.0) |
| cloud_cover_pred = np.clip(cloud_cover_pred, 0.0, 100.0) |
| precip_pred = np.clip(precip_pred, 0.0, None) |
| wind_pred = np.clip(wind_pred, 0.0, None) |
| rain_prob = np.clip(rain_prob, 0.0, 1.0) |
| |
| context_start = df["time"].iloc[0] |
| context_end = df["time"].iloc[-1] |
| requested_at_utc = pd.Timestamp.now(tz="UTC") |
| |
| horizon = min( |
| int(FORECAST_HOURS), |
| int(temp_pred.shape[0]), |
| int(humidity_pred.shape[0]), |
| int(weather_idx.shape[0]), |
| ) |
| |
| forecast = [] |
| for lead in range(1, horizon + 1): |
| target_time = context_end + pd.Timedelta(hours=lead) |
| idx = lead - 1 |
| w_idx = int(weather_idx[idx]) |
| |
| forecast.append( |
| { |
| "lead_hours": lead, |
| "target_utc": target_time.isoformat(), |
| "target_local": to_iso(target_time, city_tz), |
| "temperature_2m_c": float(temp_pred[idx]), |
| "relative_humidity_2m_pct": float(humidity_pred[idx]), |
| "apparent_temperature_c": float(apparent_pred[idx]), |
| "precipitation_mm": float(precip_pred[idx]), |
| "pressure_msl_hpa": float(sea_level_pressure_pred[idx]), |
| "surface_pressure_hpa": float(surface_pressure_pred[idx]), |
| "cloud_cover_pct": float(cloud_cover_pred[idx]), |
| "wind_speed_10m_kmh": float(wind_pred[idx]), |
| "rain_probability": float(rain_prob[idx]), |
| "weather_class": w_idx, |
| "weather_class_name": weather_class_names[w_idx] if w_idx < len(weather_class_names) else f"class_{w_idx}", |
| "weather_class_probabilities": { |
| name: float(prob) for name, prob in zip(weather_class_names, weather_probs[idx]) |
| }, |
| } |
| ) |
| |
| result = { |
| "city": CITY, |
| "location_id": str(city_spec["location_id"]), |
| "model_location_id": int(model_location_id), |
| "data_source": "open-meteo forecast api (past-hours context only)", |
| "requested_at_utc": requested_at_utc.isoformat(), |
| "context": { |
| "hours": int(len(df)), |
| "start_utc": context_start.isoformat(), |
| "end_utc": context_end.isoformat(), |
| "start_local": to_iso(context_start, city_tz), |
| "end_local": to_iso(context_end, city_tz), |
| }, |
| "model": { |
| "model_id": MODEL_ID, |
| "encoder_type": getattr(config, "encoder_type", None), |
| "seq_len": int(getattr(config, "seq_len", CONTEXT_HOURS)), |
| "input_dim": int(getattr(config, "input_dim", seq.shape[1])), |
| "num_weather_classes": int(getattr(config, "num_weather_classes", len(weather_class_names))), |
| }, |
| "forecast": forecast, |
| "sanity": { |
| "sequence_shape": list(seq.shape), |
| "finite_features": bool(np.isfinite(seq).all()), |
| }, |
| } |
| |
| print(json.dumps(result, indent=2)) |
| |
|
|
| if __name__ == "__main__": |
| predict() |
| ``` |
| |
| ### Related Models |
|
|
| 1. [Hweh-6M](https://huggingface.co/Harley-ml/Hweh-6M) |
|
|
| ## Citation |
|
|
| ```bibtex |
| @misc{distilhweh-446k, |
| title = {DistilHweh-446k: Knowledge Distillation in Short-Term Multivariate Weather Forecasting}, |
| author = {Paul Courneya; Harley-ml}, |
| year = {2026}, |
| url = {https://huggingface.co/Harley-ml/DistilHweh-446k} |
| } |
| ``` |