Harley-ml commited on
Commit
90d9e38
·
verified ·
1 Parent(s): eb66990

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +1804 -3
README.md CHANGED
@@ -1,3 +1,1804 @@
1
- ---
2
- license: mit
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - forecast
5
+ - weather
6
+ - lstm
7
+ - classification
8
+ - regression
9
+ - weather-forecast
10
+ - multitask
11
+ - harley-ml
12
+ - small
13
+ ---
14
+
15
+ # Hweh-6M
16
+
17
+ ## Summary
18
+
19
+ Task: Weather Forecasting
20
+ Inputs: 72 hours time-series
21
+ Outputs: 12h multivariate forecast
22
+ Params: 446k
23
+ Framework: PyTorch
24
+ Author: Paul Courneya (Harley-ml)
25
+
26
+ ## Description
27
+
28
+ **Hweh-446k** is a **446-thousand-parameter LSTM model** distilisation of [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**.
29
+ We recommend using this model as a backup to a weather API or for fast offline forecasting when internet access is unavailable.
30
+
31
+ We would also like to give a shoutout to [**Open-Meteo**](https://open-meteo.com/) for providing a **free-to-use weather forecasting API**.
32
+
33
+ ### Why “Hweh”?
34
+
35
+ 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.
36
+
37
+ ## Architecture
38
+
39
+ The model uses a multitask LSTM setup:
40
+
41
+ | Parameter | Value |
42
+ | ----------------------- | ---------------------------------------------- |
43
+ | `input_dim` | `22` |
44
+ | `seq_len` | `72` |
45
+ | `num_predict` | `12` |
46
+ | `hidden_dim` | `128` |
47
+ | `num_layers` | `3` |
48
+ | `dropout` | `0.1` |
49
+ | `encoder_type` | `lstm` |
50
+ | `num_locations` | `82` |
51
+ | `location_emb_dim` | `32` |
52
+ | `num_weather_classes` | `7` |
53
+
54
+ ## Training
55
+
56
+ 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.
57
+
58
+ ### Input Features
59
+
60
+ 1. `temperature_2m_norm`
61
+ 2. `relative_humidity_2m_norm`
62
+ 3. `apparent_temperature_norm`
63
+ 4. `precipitation_log_norm`
64
+ 5. `sea_level_pressure_norm`
65
+ 6. `surface_pressure_norm`
66
+ 7. `cloud_cover_total_norm`
67
+ 8. `visibility_norm`
68
+ 9. `wind_speed_10m_norm`
69
+ 10. `wind_direction_10m_sin`
70
+ 11. `wind_direction_10m_cos`
71
+ 12. `hour_sin`
72
+ 13. `hour_cos`
73
+ 14. `day_of_year_sin`
74
+ 15. `day_of_year_cos`
75
+ 16. `weather_code_onehot_clear`
76
+ 17. `weather_code_onehot_cloudy`
77
+ 18. `weather_code_onehot_fog`
78
+ 19. `weather_code_onehot_drizzle`
79
+ 20. `weather_code_onehot_rain`
80
+ 21. `weather_code_onehot_snow`
81
+ 22. `weather_code_onehot_thunderstorm`
82
+
83
+ ### Output Features
84
+
85
+ 1. `y_temp_c`: continuous regression
86
+ 2. `y_humidity`: continuous regression
87
+ 3. `y_apparent_temperature`: continuous regression
88
+ 4. `y_precipitation_mm`: continuous regression
89
+ 5. `y_sea_level_pressure_hpa`: continuous regression
90
+ 6. `y_surface_pressure_hpa`: continuous regression
91
+ 7. `y_cloud_cover_total`: continuous regression
92
+ 8. `y_wind_speed_10m`: continuous regression
93
+ 9. `y_wind_direction_sin`: continuous regression
94
+ 10. `y_wind_direction_cos`: continuous regression
95
+ 11. `y_rain_prob`: binary classification
96
+ 12. `y_weather_class`: multiclass classification
97
+
98
+ ### Training Results
99
+
100
+ #### Training & Evaluation Metrics
101
+
102
+ | Step | Train Loss | Eval Loss | Weather Acc | Rain Acc | Rain Recall | Weather Recall |
103
+ | ----: | ---------: | --------: | ----------: | -------: | ----------: | -------------: |
104
+ | 1k | 8.4609 | 8.8471 | 0.6317 | 0.7451 | 0.7640 | 0.2574 |
105
+ | 5k | 5.1420 | 5.0602 | 0.6247 | 0.7531 | 0.8025 | 0.5648 |
106
+ | 10k | 4.1733 | 3.9198 | 0.6117 | 0.7876 | 0.8016 | 0.6297 |
107
+ | 15k | 3.8354 | 3.6310 | 0.6140 | 0.7920 | 0.8009 | 0.6187 |
108
+ | 20k | 3.6206 | 3.4365 | 0.6083 | 0.7881 | 0.8140 | 0.6179 |
109
+ | 25k | 3.5378 | 3.3251 | 0.6083 | 0.7859 | 0.8173 | 0.6245 |
110
+ | 30k | 3.4534 | 3.2846 | 0.6041 | 0.7812 | 0.8272 | 0.6398 |
111
+ | 35k | 3.4272 | 3.2324 | 0.6061 | 0.7860 | 0.8194 | 0.6289 |
112
+ | 40k | 3.4143 | 3.2230 | 0.6080 | 0.7862 | 0.8200 | 0.6339 |
113
+ | 42.6k | — | 3.2180 | 0.6081 | 0.7857 | 0.8212 | 0.6340 |
114
+
115
+ Note: Loss looks higher than Hweh-6M's because of KL + train/val loss.
116
+ #### Regression Error Metrics (MAE)
117
+
118
+ | Step | Apparent | Cloud | Humidity | Precip (mm) | Sea Level P | Surface P | Temp | Wind |
119
+ | ----: | -------: | ------: | -------: | ----------: | ----------: | --------: | -----: | ----: |
120
+ | 1k | 212.80 | 2179.70 | 1476.42 | 0.140 | 7571.45 | 83590.98 | 172.79 | 60.49 |
121
+ | 5k | 2.28 | 25.58 | 9.04 | 0.107 | 3.50 | 14.55 | 1.90 | 3.78 |
122
+ | 10k | 2.06 | 25.31 | 8.08 | 0.100 | 3.31 | 9.63 | 1.72 | 3.37 |
123
+ | 15k | 1.91 | 25.00 | 7.88 | 0.101 | 3.18 | 7.93 | 1.61 | 3.25 |
124
+ | 20k | 1.88 | 25.12 | 7.60 | 0.101 | 3.13 | 7.41 | 1.56 | 3.18 |
125
+ | 25k | 1.84 | 25.01 | 7.53 | 0.102 | 3.09 | 6.61 | 1.53 | 3.13 |
126
+ | 30k | 1.81 | 25.03 | 7.45 | 0.102 | 3.12 | 6.60 | 1.51 | 3.12 |
127
+ | 35k | 1.81 | 24.94 | 7.42 | 0.101 | 3.07 | 6.39 | 1.52 | 3.12 |
128
+ | 40k | 1.79 | 24.94 | 7.39 | 0.101 | 3.06 | 6.37 | 1.50 | 3.11 |
129
+ | 42.6k | 1.79 | 24.92 | 7.39 | 0.101 | 3.06 | 6.38 | 1.50 | 3.11 |
130
+
131
+ This model did better than the teacher on MAE and accuracy, but the real-world accuracy is 5-10% worse.
132
+
133
+ ## Generation Examples
134
+
135
+ | ID | Class |
136
+ | -- | ------------ |
137
+ | 0 | clear |
138
+ | 1 | cloudy |
139
+ | 2 | fog |
140
+ | 3 | drizzle |
141
+ | 4 | rain |
142
+ | 5 | snow |
143
+ | 6 | thunderstorm |
144
+
145
+ City=Seattle
146
+ ```
147
+ {
148
+ "city": "Seattle",
149
+ "location_id": "1",
150
+ "model_location_id": 0,
151
+ "data_source": "open-meteo forecast api (past-hours context only)",
152
+ "requested_at_utc": "2026-05-08T19:57:14.429521+00:00",
153
+ "context": {
154
+ "hours": 72,
155
+ "start_utc": "2026-05-05T19:00:00+00:00",
156
+ "end_utc": "2026-05-08T18:00:00+00:00",
157
+ "start_local": "2026-05-05T12:00:00-07:00",
158
+ "end_local": "2026-05-08T11:00:00-07:00"
159
+ },
160
+ "model": {
161
+ "encoder_type": "lstm",
162
+ "seq_len": 72,
163
+ "input_dim": 22,
164
+ "num_weather_classes": 7
165
+ },
166
+ "forecast": [
167
+ {
168
+ "lead_hours": 1,
169
+ "target_utc": "2026-05-08T19:00:00+00:00",
170
+ "target_local": "2026-05-08T12:00:00-07:00",
171
+ "temperature_2m_c": 12.21396255493164,
172
+ "relative_humidity_2m_pct": 72.33454895019531,
173
+ "apparent_temperature_c": 10.097986221313477,
174
+ "precipitation_mm": 0.015628309920430183,
175
+ "pressure_msl_hpa": 1022.0569458007812,
176
+ "surface_pressure_hpa": 1014.205078125,
177
+ "cloud_cover_pct": 94.34225463867188,
178
+ "wind_speed_10m_kmh": 12.568346977233887,
179
+ "rain_probability": 0.19356799125671387,
180
+ "weather_class": 1,
181
+ "weather_class_name": "class_1",
182
+ "weather_class_probabilities": {
183
+ "class_0": 0.020174680277705193,
184
+ "class_1": 0.9282320737838745,
185
+ "class_2": 0.0022441258188337088,
186
+ "class_3": 0.04022064805030823,
187
+ "class_4": 0.008552632294595242,
188
+ "class_5": 0.0005501594278030097,
189
+ "class_6": 2.556406798248645e-05
190
+ }
191
+ },
192
+ {
193
+ "lead_hours": 2,
194
+ "target_utc": "2026-05-08T20:00:00+00:00",
195
+ "target_local": "2026-05-08T13:00:00-07:00",
196
+ "temperature_2m_c": 12.8738374710083,
197
+ "relative_humidity_2m_pct": 70.51017761230469,
198
+ "apparent_temperature_c": 10.80291748046875,
199
+ "precipitation_mm": 0.011432276107370853,
200
+ "pressure_msl_hpa": 1022.0043334960938,
201
+ "surface_pressure_hpa": 1014.2881469726562,
202
+ "cloud_cover_pct": 89.5630111694336,
203
+ "wind_speed_10m_kmh": 12.822803497314453,
204
+ "rain_probability": 0.2689012587070465,
205
+ "weather_class": 1,
206
+ "weather_class_name": "class_1",
207
+ "weather_class_probabilities": {
208
+ "class_0": 0.04770936816930771,
209
+ "class_1": 0.8698592185974121,
210
+ "class_2": 0.0019157826900482178,
211
+ "class_3": 0.057819921523332596,
212
+ "class_4": 0.02183685451745987,
213
+ "class_5": 0.0008237561560235918,
214
+ "class_6": 3.5158725950168446e-05
215
+ }
216
+ },
217
+ {
218
+ "lead_hours": 3,
219
+ "target_utc": "2026-05-08T21:00:00+00:00",
220
+ "target_local": "2026-05-08T14:00:00-07:00",
221
+ "temperature_2m_c": 13.51952075958252,
222
+ "relative_humidity_2m_pct": 68.44591522216797,
223
+ "apparent_temperature_c": 11.500271797180176,
224
+ "precipitation_mm": 0.006943895947188139,
225
+ "pressure_msl_hpa": 1021.80859375,
226
+ "surface_pressure_hpa": 1014.2529296875,
227
+ "cloud_cover_pct": 84.49480438232422,
228
+ "wind_speed_10m_kmh": 12.941960334777832,
229
+ "rain_probability": 0.30342426896095276,
230
+ "weather_class": 1,
231
+ "weather_class_name": "class_1",
232
+ "weather_class_probabilities": {
233
+ "class_0": 0.07170139998197556,
234
+ "class_1": 0.8286910057067871,
235
+ "class_2": 0.0013093570014461875,
236
+ "class_3": 0.06433302909135818,
237
+ "class_4": 0.03300558775663376,
238
+ "class_5": 0.0009036734118126333,
239
+ "class_6": 5.590655928244814e-05
240
+ }
241
+ },
242
+ {
243
+ "lead_hours": 4,
244
+ "target_utc": "2026-05-08T22:00:00+00:00",
245
+ "target_local": "2026-05-08T15:00:00-07:00",
246
+ "temperature_2m_c": 13.970871925354004,
247
+ "relative_humidity_2m_pct": 66.8187026977539,
248
+ "apparent_temperature_c": 11.959537506103516,
249
+ "precipitation_mm": 0.009790810756385326,
250
+ "pressure_msl_hpa": 1021.4691162109375,
251
+ "surface_pressure_hpa": 1014.1052856445312,
252
+ "cloud_cover_pct": 80.34271240234375,
253
+ "wind_speed_10m_kmh": 13.050889015197754,
254
+ "rain_probability": 0.33110707998275757,
255
+ "weather_class": 1,
256
+ "weather_class_name": "class_1",
257
+ "weather_class_probabilities": {
258
+ "class_0": 0.10919982939958572,
259
+ "class_1": 0.7759758830070496,
260
+ "class_2": 0.0011831748997792602,
261
+ "class_3": 0.07015678286552429,
262
+ "class_4": 0.042580746114254,
263
+ "class_5": 0.000857028178870678,
264
+ "class_6": 4.6529316023224965e-05
265
+ }
266
+ },
267
+ {
268
+ "lead_hours": 5,
269
+ "target_utc": "2026-05-08T23:00:00+00:00",
270
+ "target_local": "2026-05-08T16:00:00-07:00",
271
+ "temperature_2m_c": 14.132287979125977,
272
+ "relative_humidity_2m_pct": 66.1208267211914,
273
+ "apparent_temperature_c": 12.156023025512695,
274
+ "precipitation_mm": 0.008600466884672642,
275
+ "pressure_msl_hpa": 1021.0518188476562,
276
+ "surface_pressure_hpa": 1013.7891845703125,
277
+ "cloud_cover_pct": 76.06925201416016,
278
+ "wind_speed_10m_kmh": 12.926268577575684,
279
+ "rain_probability": 0.3409281373023987,
280
+ "weather_class": 1,
281
+ "weather_class_name": "class_1",
282
+ "weather_class_probabilities": {
283
+ "class_0": 0.1305426061153412,
284
+ "class_1": 0.7395681142807007,
285
+ "class_2": 0.0007046378450468183,
286
+ "class_3": 0.07573042809963226,
287
+ "class_4": 0.052331216633319855,
288
+ "class_5": 0.0010767169296741486,
289
+ "class_6": 4.629969771485776e-05
290
+ }
291
+ },
292
+ {
293
+ "lead_hours": 6,
294
+ "target_utc": "2026-05-09T00:00:00+00:00",
295
+ "target_local": "2026-05-08T17:00:00-07:00",
296
+ "temperature_2m_c": 13.963343620300293,
297
+ "relative_humidity_2m_pct": 66.67638397216797,
298
+ "apparent_temperature_c": 11.971813201904297,
299
+ "precipitation_mm": 0.010375693440437317,
300
+ "pressure_msl_hpa": 1020.6505126953125,
301
+ "surface_pressure_hpa": 1013.4520874023438,
302
+ "cloud_cover_pct": 73.21341705322266,
303
+ "wind_speed_10m_kmh": 12.721481323242188,
304
+ "rain_probability": 0.35606324672698975,
305
+ "weather_class": 1,
306
+ "weather_class_name": "class_1",
307
+ "weather_class_probabilities": {
308
+ "class_0": 0.15235546231269836,
309
+ "class_1": 0.7133304476737976,
310
+ "class_2": 0.0007980632944963872,
311
+ "class_3": 0.07519367337226868,
312
+ "class_4": 0.05724283307790756,
313
+ "class_5": 0.0010369947412982583,
314
+ "class_6": 4.249428093316965e-05
315
+ }
316
+ },
317
+ {
318
+ "lead_hours": 7,
319
+ "target_utc": "2026-05-09T01:00:00+00:00",
320
+ "target_local": "2026-05-08T18:00:00-07:00",
321
+ "temperature_2m_c": 13.449448585510254,
322
+ "relative_humidity_2m_pct": 68.29602813720703,
323
+ "apparent_temperature_c": 11.426795959472656,
324
+ "precipitation_mm": 0.012202607467770576,
325
+ "pressure_msl_hpa": 1020.3434448242188,
326
+ "surface_pressure_hpa": 1013.139892578125,
327
+ "cloud_cover_pct": 70.92017364501953,
328
+ "wind_speed_10m_kmh": 12.359071731567383,
329
+ "rain_probability": 0.3651714026927948,
330
+ "weather_class": 1,
331
+ "weather_class_name": "class_1",
332
+ "weather_class_probabilities": {
333
+ "class_0": 0.16448773443698883,
334
+ "class_1": 0.695494532585144,
335
+ "class_2": 0.0008648976217955351,
336
+ "class_3": 0.07075871527194977,
337
+ "class_4": 0.06722015887498856,
338
+ "class_5": 0.0011408632853999734,
339
+ "class_6": 3.3135267585748807e-05
340
+ }
341
+ },
342
+ {
343
+ "lead_hours": 8,
344
+ "target_utc": "2026-05-09T02:00:00+00:00",
345
+ "target_local": "2026-05-08T19:00:00-07:00",
346
+ "temperature_2m_c": 12.755823135375977,
347
+ "relative_humidity_2m_pct": 70.44146728515625,
348
+ "apparent_temperature_c": 10.662925720214844,
349
+ "precipitation_mm": 0.014662384055554867,
350
+ "pressure_msl_hpa": 1020.1875610351562,
351
+ "surface_pressure_hpa": 1012.8895874023438,
352
+ "cloud_cover_pct": 69.15129852294922,
353
+ "wind_speed_10m_kmh": 11.787208557128906,
354
+ "rain_probability": 0.3489035665988922,
355
+ "weather_class": 1,
356
+ "weather_class_name": "class_1",
357
+ "weather_class_probabilities": {
358
+ "class_0": 0.19570188224315643,
359
+ "class_1": 0.6735588908195496,
360
+ "class_2": 0.000978420372121036,
361
+ "class_3": 0.06184739992022514,
362
+ "class_4": 0.0664038360118866,
363
+ "class_5": 0.0014616982080042362,
364
+ "class_6": 4.789666854776442e-05
365
+ }
366
+ },
367
+ {
368
+ "lead_hours": 9,
369
+ "target_utc": "2026-05-09T03:00:00+00:00",
370
+ "target_local": "2026-05-08T20:00:00-07:00",
371
+ "temperature_2m_c": 11.955390930175781,
372
+ "relative_humidity_2m_pct": 73.05667877197266,
373
+ "apparent_temperature_c": 9.77428913116455,
374
+ "precipitation_mm": 0.015376528725028038,
375
+ "pressure_msl_hpa": 1020.2242431640625,
376
+ "surface_pressure_hpa": 1012.7984619140625,
377
+ "cloud_cover_pct": 67.46344757080078,
378
+ "wind_speed_10m_kmh": 11.127586364746094,
379
+ "rain_probability": 0.36496502161026,
380
+ "weather_class": 1,
381
+ "weather_class_name": "class_1",
382
+ "weather_class_probabilities": {
383
+ "class_0": 0.20349998772144318,
384
+ "class_1": 0.6480608582496643,
385
+ "class_2": 0.0010103220120072365,
386
+ "class_3": 0.06373731791973114,
387
+ "class_4": 0.08206527680158615,
388
+ "class_5": 0.0015911461086943746,
389
+ "class_6": 3.510143869789317e-05
390
+ }
391
+ },
392
+ {
393
+ "lead_hours": 10,
394
+ "target_utc": "2026-05-09T04:00:00+00:00",
395
+ "target_local": "2026-05-08T21:00:00-07:00",
396
+ "temperature_2m_c": 11.182319641113281,
397
+ "relative_humidity_2m_pct": 75.5196304321289,
398
+ "apparent_temperature_c": 8.978246688842773,
399
+ "precipitation_mm": 0.016823438927531242,
400
+ "pressure_msl_hpa": 1020.421875,
401
+ "surface_pressure_hpa": 1012.8126831054688,
402
+ "cloud_cover_pct": 65.73115539550781,
403
+ "wind_speed_10m_kmh": 10.359850883483887,
404
+ "rain_probability": 0.35924479365348816,
405
+ "weather_class": 1,
406
+ "weather_class_name": "class_1",
407
+ "weather_class_probabilities": {
408
+ "class_0": 0.21845510601997375,
409
+ "class_1": 0.6389062404632568,
410
+ "class_2": 0.0018773162737488747,
411
+ "class_3": 0.059891991317272186,
412
+ "class_4": 0.07879848033189774,
413
+ "class_5": 0.002034474164247513,
414
+ "class_6": 3.633901724242605e-05
415
+ }
416
+ },
417
+ {
418
+ "lead_hours": 11,
419
+ "target_utc": "2026-05-09T05:00:00+00:00",
420
+ "target_local": "2026-05-08T22:00:00-07:00",
421
+ "temperature_2m_c": 10.499757766723633,
422
+ "relative_humidity_2m_pct": 77.536865234375,
423
+ "apparent_temperature_c": 8.306406021118164,
424
+ "precipitation_mm": 0.01860857754945755,
425
+ "pressure_msl_hpa": 1020.7318725585938,
426
+ "surface_pressure_hpa": 1012.9368896484375,
427
+ "cloud_cover_pct": 65.10720825195312,
428
+ "wind_speed_10m_kmh": 9.62015151977539,
429
+ "rain_probability": 0.3694237470626831,
430
+ "weather_class": 1,
431
+ "weather_class_name": "class_1",
432
+ "weather_class_probabilities": {
433
+ "class_0": 0.22774139046669006,
434
+ "class_1": 0.6242526173591614,
435
+ "class_2": 0.0028147574048489332,
436
+ "class_3": 0.06025313213467598,
437
+ "class_4": 0.0822005346417427,
438
+ "class_5": 0.0026846849359571934,
439
+ "class_6": 5.289047476253472e-05
440
+ }
441
+ },
442
+ {
443
+ "lead_hours": 12,
444
+ "target_utc": "2026-05-09T06:00:00+00:00",
445
+ "target_local": "2026-05-08T23:00:00-07:00",
446
+ "temperature_2m_c": 9.956731796264648,
447
+ "relative_humidity_2m_pct": 79.21904754638672,
448
+ "apparent_temperature_c": 7.87125301361084,
449
+ "precipitation_mm": 0.017959173768758774,
450
+ "pressure_msl_hpa": 1021.0579833984375,
451
+ "surface_pressure_hpa": 1013.093994140625,
452
+ "cloud_cover_pct": 64.14817810058594,
453
+ "wind_speed_10m_kmh": 8.923616409301758,
454
+ "rain_probability": 0.3691202700138092,
455
+ "weather_class": 1,
456
+ "weather_class_name": "class_1",
457
+ "weather_class_probabilities": {
458
+ "class_0": 0.23541118204593658,
459
+ "class_1": 0.618242621421814,
460
+ "class_2": 0.0044443667866289616,
461
+ "class_3": 0.06440076231956482,
462
+ "class_4": 0.0741729885339737,
463
+ "class_5": 0.003270090091973543,
464
+ "class_6": 5.8019890275318176e-05
465
+ }
466
+ }
467
+ ],
468
+ "sanity": {
469
+ "sequence_shape": [
470
+ 72,
471
+ 22
472
+ ],
473
+ "finite_features": true
474
+ }
475
+ }
476
+ PS C:\Users\Paulc> python3.12 weather_infer.py --model_dir "C:\Users\Paulc\weather_model\student_distilled" --city Seattle
477
+ Warning: unexpected keys while loading checkpoint: ['distill_proj.weight']
478
+ {
479
+ "city": "Seattle",
480
+ "location_id": "1",
481
+ "model_location_id": 0,
482
+ "data_source": "open-meteo forecast api (past-hours context only)",
483
+ "requested_at_utc": "2026-05-08T19:57:47.439276+00:00",
484
+ "context": {
485
+ "hours": 72,
486
+ "start_utc": "2026-05-05T19:00:00+00:00",
487
+ "end_utc": "2026-05-08T18:00:00+00:00",
488
+ "start_local": "2026-05-05T12:00:00-07:00",
489
+ "end_local": "2026-05-08T11:00:00-07:00"
490
+ },
491
+ "model": {
492
+ "encoder_type": "lstm",
493
+ "seq_len": 72,
494
+ "input_dim": 22,
495
+ "num_weather_classes": 7
496
+ },
497
+ "forecast": [
498
+ {
499
+ "lead_hours": 1,
500
+ "target_utc": "2026-05-08T19:00:00+00:00",
501
+ "target_local": "2026-05-08T12:00:00-07:00",
502
+ "temperature_2m_c": 13.681238174438477,
503
+ "relative_humidity_2m_pct": 69.90876770019531,
504
+ "apparent_temperature_c": 11.687149047851562,
505
+ "precipitation_mm": 0.0012515264097601175,
506
+ "pressure_msl_hpa": 1019.3030395507812,
507
+ "surface_pressure_hpa": 1018.5359497070312,
508
+ "cloud_cover_pct": 88.76920318603516,
509
+ "wind_speed_10m_kmh": 13.126839637756348,
510
+ "rain_probability": 0.1457637995481491,
511
+ "weather_class": 1,
512
+ "weather_class_name": "class_1",
513
+ "weather_class_probabilities": {
514
+ "class_0": 0.02097843959927559,
515
+ "class_1": 0.9304905533790588,
516
+ "class_2": 0.0025352241937071085,
517
+ "class_3": 0.03495039418339729,
518
+ "class_4": 0.01054275780916214,
519
+ "class_5": 0.0004654920194298029,
520
+ "class_6": 3.7044908822281286e-05
521
+ }
522
+ },
523
+ {
524
+ "lead_hours": 2,
525
+ "target_utc": "2026-05-08T20:00:00+00:00",
526
+ "target_local": "2026-05-08T13:00:00-07:00",
527
+ "temperature_2m_c": 14.486506462097168,
528
+ "relative_humidity_2m_pct": 67.52698516845703,
529
+ "apparent_temperature_c": 12.55270767211914,
530
+ "precipitation_mm": 0.0008608415955677629,
531
+ "pressure_msl_hpa": 1019.0198364257812,
532
+ "surface_pressure_hpa": 1018.4589233398438,
533
+ "cloud_cover_pct": 84.31889343261719,
534
+ "wind_speed_10m_kmh": 13.241435050964355,
535
+ "rain_probability": 0.19527363777160645,
536
+ "weather_class": 1,
537
+ "weather_class_name": "class_1",
538
+ "weather_class_probabilities": {
539
+ "class_0": 0.053049832582473755,
540
+ "class_1": 0.8794819712638855,
541
+ "class_2": 0.0021074640098959208,
542
+ "class_3": 0.0436088852584362,
543
+ "class_4": 0.02111213095486164,
544
+ "class_5": 0.0005800225771963596,
545
+ "class_6": 5.971627979306504e-05
546
+ }
547
+ },
548
+ {
549
+ "lead_hours": 3,
550
+ "target_utc": "2026-05-08T21:00:00+00:00",
551
+ "target_local": "2026-05-08T14:00:00-07:00",
552
+ "temperature_2m_c": 15.089279174804688,
553
+ "relative_humidity_2m_pct": 65.6773681640625,
554
+ "apparent_temperature_c": 13.181319236755371,
555
+ "precipitation_mm": 0.002190209459513426,
556
+ "pressure_msl_hpa": 1018.709716796875,
557
+ "surface_pressure_hpa": 1018.2867431640625,
558
+ "cloud_cover_pct": 80.14619445800781,
559
+ "wind_speed_10m_kmh": 13.32516860961914,
560
+ "rain_probability": 0.21867528557777405,
561
+ "weather_class": 1,
562
+ "weather_class_name": "class_1",
563
+ "weather_class_probabilities": {
564
+ "class_0": 0.08254168927669525,
565
+ "class_1": 0.8380688428878784,
566
+ "class_2": 0.0014072611229494214,
567
+ "class_3": 0.04709410294890404,
568
+ "class_4": 0.030200183391571045,
569
+ "class_5": 0.0006088378722779453,
570
+ "class_6": 7.912428554845974e-05
571
+ }
572
+ },
573
+ {
574
+ "lead_hours": 4,
575
+ "target_utc": "2026-05-08T22:00:00+00:00",
576
+ "target_local": "2026-05-08T15:00:00-07:00",
577
+ "temperature_2m_c": 15.403922080993652,
578
+ "relative_humidity_2m_pct": 64.67561340332031,
579
+ "apparent_temperature_c": 13.48654556274414,
580
+ "precipitation_mm": 0.0021571130491793156,
581
+ "pressure_msl_hpa": 1018.3944702148438,
582
+ "surface_pressure_hpa": 1018.0625,
583
+ "cloud_cover_pct": 76.4104995727539,
584
+ "wind_speed_10m_kmh": 13.275524139404297,
585
+ "rain_probability": 0.2299734503030777,
586
+ "weather_class": 1,
587
+ "weather_class_name": "class_1",
588
+ "weather_class_probabilities": {
589
+ "class_0": 0.11604393273591995,
590
+ "class_1": 0.7922014594078064,
591
+ "class_2": 0.0011422870447859168,
592
+ "class_3": 0.05158008262515068,
593
+ "class_4": 0.03833876550197601,
594
+ "class_5": 0.000621610670350492,
595
+ "class_6": 7.179530075518414e-05
596
+ }
597
+ },
598
+ {
599
+ "lead_hours": 5,
600
+ "target_utc": "2026-05-08T23:00:00+00:00",
601
+ "target_local": "2026-05-08T16:00:00-07:00",
602
+ "temperature_2m_c": 15.407997131347656,
603
+ "relative_humidity_2m_pct": 64.65668487548828,
604
+ "apparent_temperature_c": 13.48292350769043,
605
+ "precipitation_mm": 0.0026813943404704332,
606
+ "pressure_msl_hpa": 1018.1220703125,
607
+ "surface_pressure_hpa": 1017.7960205078125,
608
+ "cloud_cover_pct": 72.77561950683594,
609
+ "wind_speed_10m_kmh": 13.133893966674805,
610
+ "rain_probability": 0.23628145456314087,
611
+ "weather_class": 1,
612
+ "weather_class_name": "class_1",
613
+ "weather_class_probabilities": {
614
+ "class_0": 0.14982207119464874,
615
+ "class_1": 0.7523030638694763,
616
+ "class_2": 0.0008282885537482798,
617
+ "class_3": 0.05172016844153404,
618
+ "class_4": 0.04454538971185684,
619
+ "class_5": 0.0007084720418788493,
620
+ "class_6": 7.257604011101648e-05
621
+ }
622
+ },
623
+ {
624
+ "lead_hours": 6,
625
+ "target_utc": "2026-05-09T00:00:00+00:00",
626
+ "target_local": "2026-05-08T17:00:00-07:00",
627
+ "temperature_2m_c": 15.139252662658691,
628
+ "relative_humidity_2m_pct": 65.5518798828125,
629
+ "apparent_temperature_c": 13.192585945129395,
630
+ "precipitation_mm": 0.0026606114115566015,
631
+ "pressure_msl_hpa": 1017.9351196289062,
632
+ "surface_pressure_hpa": 1017.5604858398438,
633
+ "cloud_cover_pct": 69.60166931152344,
634
+ "wind_speed_10m_kmh": 12.789706230163574,
635
+ "rain_probability": 0.23617199063301086,
636
+ "weather_class": 1,
637
+ "weather_class_name": "class_1",
638
+ "weather_class_probabilities": {
639
+ "class_0": 0.17920561134815216,
640
+ "class_1": 0.7213184237480164,
641
+ "class_2": 0.0008871846948750317,
642
+ "class_3": 0.05144878104329109,
643
+ "class_4": 0.046307649463415146,
644
+ "class_5": 0.0007639037212356925,
645
+ "class_6": 6.843609298812225e-05
646
+ }
647
+ },
648
+ {
649
+ "lead_hours": 7,
650
+ "target_utc": "2026-05-09T01:00:00+00:00",
651
+ "target_local": "2026-05-08T18:00:00-07:00",
652
+ "temperature_2m_c": 14.66390609741211,
653
+ "relative_humidity_2m_pct": 67.1559066772461,
654
+ "apparent_temperature_c": 12.692171096801758,
655
+ "precipitation_mm": 0.002722225384786725,
656
+ "pressure_msl_hpa": 1017.8424072265625,
657
+ "surface_pressure_hpa": 1017.3685913085938,
658
+ "cloud_cover_pct": 66.97268676757812,
659
+ "wind_speed_10m_kmh": 12.329818725585938,
660
+ "rain_probability": 0.23410728573799133,
661
+ "weather_class": 1,
662
+ "weather_class_name": "class_1",
663
+ "weather_class_probabilities": {
664
+ "class_0": 0.20177967846393585,
665
+ "class_1": 0.697116494178772,
666
+ "class_2": 0.001064434414729476,
667
+ "class_3": 0.04921679198741913,
668
+ "class_4": 0.049886710941791534,
669
+ "class_5": 0.0008780939970165491,
670
+ "class_6": 5.7844863476930186e-05
671
+ }
672
+ },
673
+ {
674
+ "lead_hours": 8,
675
+ "target_utc": "2026-05-09T02:00:00+00:00",
676
+ "target_local": "2026-05-08T19:00:00-07:00",
677
+ "temperature_2m_c": 14.042488098144531,
678
+ "relative_humidity_2m_pct": 69.15681457519531,
679
+ "apparent_temperature_c": 12.045327186584473,
680
+ "precipitation_mm": 0.003981542307883501,
681
+ "pressure_msl_hpa": 1017.853759765625,
682
+ "surface_pressure_hpa": 1017.2957763671875,
683
+ "cloud_cover_pct": 64.85920715332031,
684
+ "wind_speed_10m_kmh": 11.780016899108887,
685
+ "rain_probability": 0.23837216198444366,
686
+ "weather_class": 1,
687
+ "weather_class_name": "class_1",
688
+ "weather_class_probabilities": {
689
+ "class_0": 0.23561197519302368,
690
+ "class_1": 0.6629505753517151,
691
+ "class_2": 0.0012489539803937078,
692
+ "class_3": 0.04817439988255501,
693
+ "class_4": 0.050942711532115936,
694
+ "class_5": 0.0010061608627438545,
695
+ "class_6": 6.52461385470815e-05
696
+ }
697
+ },
698
+ {
699
+ "lead_hours": 9,
700
+ "target_utc": "2026-05-09T03:00:00+00:00",
701
+ "target_local": "2026-05-08T20:00:00-07:00",
702
+ "temperature_2m_c": 13.325971603393555,
703
+ "relative_humidity_2m_pct": 71.42361450195312,
704
+ "apparent_temperature_c": 11.300540924072266,
705
+ "precipitation_mm": 0.0030152045655995607,
706
+ "pressure_msl_hpa": 1017.9505004882812,
707
+ "surface_pressure_hpa": 1017.2774047851562,
708
+ "cloud_cover_pct": 63.037109375,
709
+ "wind_speed_10m_kmh": 11.165238380432129,
710
+ "rain_probability": 0.23051044344902039,
711
+ "weather_class": 1,
712
+ "weather_class_name": "class_1",
713
+ "weather_class_probabilities": {
714
+ "class_0": 0.24986012279987335,
715
+ "class_1": 0.6375465989112854,
716
+ "class_2": 0.0016154011245816946,
717
+ "class_3": 0.04874761775135994,
718
+ "class_4": 0.06096767634153366,
719
+ "class_5": 0.0012142135528847575,
720
+ "class_6": 4.836557855014689e-05
721
+ }
722
+ },
723
+ {
724
+ "lead_hours": 10,
725
+ "target_utc": "2026-05-09T04:00:00+00:00",
726
+ "target_local": "2026-05-08T21:00:00-07:00",
727
+ "temperature_2m_c": 12.574642181396484,
728
+ "relative_humidity_2m_pct": 73.7841796875,
729
+ "apparent_temperature_c": 10.549814224243164,
730
+ "precipitation_mm": 0.004971037618815899,
731
+ "pressure_msl_hpa": 1018.10400390625,
732
+ "surface_pressure_hpa": 1017.2828979492188,
733
+ "cloud_cover_pct": 61.4162483215332,
734
+ "wind_speed_10m_kmh": 10.5538911819458,
735
+ "rain_probability": 0.23788221180438995,
736
+ "weather_class": 1,
737
+ "weather_class_name": "class_1",
738
+ "weather_class_probabilities": {
739
+ "class_0": 0.27152255177497864,
740
+ "class_1": 0.6182448863983154,
741
+ "class_2": 0.0025821358431130648,
742
+ "class_3": 0.04885515570640564,
743
+ "class_4": 0.05706281587481499,
744
+ "class_5": 0.0016854261048138142,
745
+ "class_6": 4.704758248408325e-05
746
+ }
747
+ },
748
+ {
749
+ "lead_hours": 11,
750
+ "target_utc": "2026-05-09T05:00:00+00:00",
751
+ "target_local": "2026-05-08T22:00:00-07:00",
752
+ "temperature_2m_c": 11.85836124420166,
753
+ "relative_humidity_2m_pct": 75.99488830566406,
754
+ "apparent_temperature_c": 9.845465660095215,
755
+ "precipitation_mm": 0.0059099141508340836,
756
+ "pressure_msl_hpa": 1018.2722778320312,
757
+ "surface_pressure_hpa": 1017.3274536132812,
758
+ "cloud_cover_pct": 60.944053649902344,
759
+ "wind_speed_10m_kmh": 10.019789695739746,
760
+ "rain_probability": 0.24793456494808197,
761
+ "weather_class": 1,
762
+ "weather_class_name": "class_1",
763
+ "weather_class_probabilities": {
764
+ "class_0": 0.27306604385375977,
765
+ "class_1": 0.6107510328292847,
766
+ "class_2": 0.004449337720870972,
767
+ "class_3": 0.0498417466878891,
768
+ "class_4": 0.05973493307828903,
769
+ "class_5": 0.002092213137075305,
770
+ "class_6": 6.465442857006565e-05
771
+ }
772
+ },
773
+ {
774
+ "lead_hours": 12,
775
+ "target_utc": "2026-05-09T06:00:00+00:00",
776
+ "target_local": "2026-05-08T23:00:00-07:00",
777
+ "temperature_2m_c": 11.196554183959961,
778
+ "relative_humidity_2m_pct": 78.09349060058594,
779
+ "apparent_temperature_c": 9.231935501098633,
780
+ "precipitation_mm": 0.00701399240642786,
781
+ "pressure_msl_hpa": 1018.4185791015625,
782
+ "surface_pressure_hpa": 1017.3386840820312,
783
+ "cloud_cover_pct": 60.26295471191406,
784
+ "wind_speed_10m_kmh": 9.483912467956543,
785
+ "rain_probability": 0.2565533518791199,
786
+ "weather_class": 1,
787
+ "weather_class_name": "class_1",
788
+ "weather_class_probabilities": {
789
+ "class_0": 0.27720507979393005,
790
+ "class_1": 0.6052833795547485,
791
+ "class_2": 0.007175811566412449,
792
+ "class_3": 0.04983551800251007,
793
+ "class_4": 0.05781771242618561,
794
+ "class_5": 0.0026247103232890368,
795
+ "class_6": 5.782112566521391e-05
796
+ }
797
+ }
798
+ ],
799
+ "sanity": {
800
+ "sequence_shape": [
801
+ 72,
802
+ 22
803
+ ],
804
+ "finite_features": true
805
+ }
806
+ }
807
+ ```
808
+
809
+ City=Nuuk
810
+ ```
811
+ {
812
+ "city": "Nuuk",
813
+ "location_id": "83",
814
+ "model_location_id": 0,
815
+ "data_source": "open-meteo forecast api (past-hours context only)",
816
+ "requested_at_utc": "2026-05-08T20:40:35.109779+00:00",
817
+ "context": {
818
+ "hours": 72,
819
+ "start_utc": "2026-05-05T20:00:00+00:00",
820
+ "end_utc": "2026-05-08T19:00:00+00:00",
821
+ "start_local": "2026-05-05T19:00:00-01:00",
822
+ "end_local": "2026-05-08T18:00:00-01:00"
823
+ },
824
+ "model": {
825
+ "encoder_type": "lstm",
826
+ "seq_len": 72,
827
+ "input_dim": 22,
828
+ "num_weather_classes": 7
829
+ },
830
+ "forecast": [
831
+ {
832
+ "lead_hours": 1,
833
+ "target_utc": "2026-05-08T20:00:00+00:00",
834
+ "target_local": "2026-05-08T19:00:00-01:00",
835
+ "temperature_2m_c": 5.2753753662109375,
836
+ "relative_humidity_2m_pct": 93.01068115234375,
837
+ "apparent_temperature_c": 1.6396684646606445,
838
+ "precipitation_mm": 0.3556472063064575,
839
+ "pressure_msl_hpa": 1005.5432739257812,
840
+ "surface_pressure_hpa": 973.415771484375,
841
+ "cloud_cover_pct": 98.54638671875,
842
+ "wind_speed_10m_kmh": 13.717008590698242,
843
+ "rain_probability": 0.9789170026779175,
844
+ "weather_class": 3,
845
+ "weather_class_name": "class_3",
846
+ "weather_class_probabilities": {
847
+ "class_0": 0.000619232130702585,
848
+ "class_1": 0.057769663631916046,
849
+ "class_2": 0.003395488252863288,
850
+ "class_3": 0.401492714881897,
851
+ "class_4": 0.18206973373889923,
852
+ "class_5": 0.3545896112918854,
853
+ "class_6": 6.364739965647459e-05
854
+ }
855
+ },
856
+ {
857
+ "lead_hours": 2,
858
+ "target_utc": "2026-05-08T21:00:00+00:00",
859
+ "target_local": "2026-05-08T20:00:00-01:00",
860
+ "temperature_2m_c": 4.986588478088379,
861
+ "relative_humidity_2m_pct": 93.84243774414062,
862
+ "apparent_temperature_c": 1.352757453918457,
863
+ "precipitation_mm": 0.2776345908641815,
864
+ "pressure_msl_hpa": 1005.599853515625,
865
+ "surface_pressure_hpa": 973.536376953125,
866
+ "cloud_cover_pct": 98.45586395263672,
867
+ "wind_speed_10m_kmh": 13.445389747619629,
868
+ "rain_probability": 0.9556259512901306,
869
+ "weather_class": 5,
870
+ "weather_class_name": "class_5",
871
+ "weather_class_probabilities": {
872
+ "class_0": 0.00150326790753752,
873
+ "class_1": 0.09517476707696915,
874
+ "class_2": 0.004558710381388664,
875
+ "class_3": 0.32409900426864624,
876
+ "class_4": 0.1846529245376587,
877
+ "class_5": 0.38996437191963196,
878
+ "class_6": 4.702423757407814e-05
879
+ }
880
+ },
881
+ {
882
+ "lead_hours": 3,
883
+ "target_utc": "2026-05-08T22:00:00+00:00",
884
+ "target_local": "2026-05-08T21:00:00-01:00",
885
+ "temperature_2m_c": 4.788308143615723,
886
+ "relative_humidity_2m_pct": 94.0885238647461,
887
+ "apparent_temperature_c": 1.1667909622192383,
888
+ "precipitation_mm": 0.23039932548999786,
889
+ "pressure_msl_hpa": 1005.703857421875,
890
+ "surface_pressure_hpa": 973.5965576171875,
891
+ "cloud_cover_pct": 97.65797424316406,
892
+ "wind_speed_10m_kmh": 13.209211349487305,
893
+ "rain_probability": 0.9299042820930481,
894
+ "weather_class": 5,
895
+ "weather_class_name": "class_5",
896
+ "weather_class_probabilities": {
897
+ "class_0": 0.002542331349104643,
898
+ "class_1": 0.12809209525585175,
899
+ "class_2": 0.0058285691775381565,
900
+ "class_3": 0.3043138086795807,
901
+ "class_4": 0.17225369811058044,
902
+ "class_5": 0.3869440257549286,
903
+ "class_6": 2.5515650122542866e-05
904
+ }
905
+ },
906
+ {
907
+ "lead_hours": 4,
908
+ "target_utc": "2026-05-08T23:00:00+00:00",
909
+ "target_local": "2026-05-08T22:00:00-01:00",
910
+ "temperature_2m_c": 4.660131454467773,
911
+ "relative_humidity_2m_pct": 94.03594970703125,
912
+ "apparent_temperature_c": 1.0469179153442383,
913
+ "precipitation_mm": 0.19706439971923828,
914
+ "pressure_msl_hpa": 1005.7493896484375,
915
+ "surface_pressure_hpa": 973.7083740234375,
916
+ "cloud_cover_pct": 97.17306518554688,
917
+ "wind_speed_10m_kmh": 13.017566680908203,
918
+ "rain_probability": 0.9050359129905701,
919
+ "weather_class": 5,
920
+ "weather_class_name": "class_5",
921
+ "weather_class_probabilities": {
922
+ "class_0": 0.004407276399433613,
923
+ "class_1": 0.15539932250976562,
924
+ "class_2": 0.009657401591539383,
925
+ "class_3": 0.2794102430343628,
926
+ "class_4": 0.1521354615688324,
927
+ "class_5": 0.3989632725715637,
928
+ "class_6": 2.7043566660722718e-05
929
+ }
930
+ },
931
+ {
932
+ "lead_hours": 5,
933
+ "target_utc": "2026-05-09T00:00:00+00:00",
934
+ "target_local": "2026-05-08T23:00:00-01:00",
935
+ "temperature_2m_c": 4.5112457275390625,
936
+ "relative_humidity_2m_pct": 93.88682556152344,
937
+ "apparent_temperature_c": 0.9274702072143555,
938
+ "precipitation_mm": 0.1685791015625,
939
+ "pressure_msl_hpa": 1005.7725830078125,
940
+ "surface_pressure_hpa": 973.7322387695312,
941
+ "cloud_cover_pct": 96.03288269042969,
942
+ "wind_speed_10m_kmh": 12.944330215454102,
943
+ "rain_probability": 0.8804075121879578,
944
+ "weather_class": 5,
945
+ "weather_class_name": "class_5",
946
+ "weather_class_probabilities": {
947
+ "class_0": 0.006306177470833063,
948
+ "class_1": 0.17262385785579681,
949
+ "class_2": 0.00996350683271885,
950
+ "class_3": 0.2658991515636444,
951
+ "class_4": 0.1401163786649704,
952
+ "class_5": 0.4050598442554474,
953
+ "class_6": 3.1046529329614714e-05
954
+ }
955
+ },
956
+ {
957
+ "lead_hours": 6,
958
+ "target_utc": "2026-05-09T01:00:00+00:00",
959
+ "target_local": "2026-05-09T00:00:00-01:00",
960
+ "temperature_2m_c": 4.33610725402832,
961
+ "relative_humidity_2m_pct": 94.00520324707031,
962
+ "apparent_temperature_c": 0.7778654098510742,
963
+ "precipitation_mm": 0.14649224281311035,
964
+ "pressure_msl_hpa": 1005.8167114257812,
965
+ "surface_pressure_hpa": 973.7780151367188,
966
+ "cloud_cover_pct": 95.56141662597656,
967
+ "wind_speed_10m_kmh": 12.845012664794922,
968
+ "rain_probability": 0.8599434494972229,
969
+ "weather_class": 5,
970
+ "weather_class_name": "class_5",
971
+ "weather_class_probabilities": {
972
+ "class_0": 0.007768102455884218,
973
+ "class_1": 0.18894362449645996,
974
+ "class_2": 0.011767406016588211,
975
+ "class_3": 0.2329735904932022,
976
+ "class_4": 0.12322919070720673,
977
+ "class_5": 0.43529438972473145,
978
+ "class_6": 2.3752647393848747e-05
979
+ }
980
+ },
981
+ {
982
+ "lead_hours": 7,
983
+ "target_utc": "2026-05-09T02:00:00+00:00",
984
+ "target_local": "2026-05-09T01:00:00-01:00",
985
+ "temperature_2m_c": 4.140122413635254,
986
+ "relative_humidity_2m_pct": 94.25415802001953,
987
+ "apparent_temperature_c": 0.5866508483886719,
988
+ "precipitation_mm": 0.13218756020069122,
989
+ "pressure_msl_hpa": 1005.855712890625,
990
+ "surface_pressure_hpa": 973.7844848632812,
991
+ "cloud_cover_pct": 95.55270385742188,
992
+ "wind_speed_10m_kmh": 12.77564811706543,
993
+ "rain_probability": 0.8421469330787659,
994
+ "weather_class": 5,
995
+ "weather_class_name": "class_5",
996
+ "weather_class_probabilities": {
997
+ "class_0": 0.009295819327235222,
998
+ "class_1": 0.20261473953723907,
999
+ "class_2": 0.012845687568187714,
1000
+ "class_3": 0.21367891132831573,
1001
+ "class_4": 0.11506513506174088,
1002
+ "class_5": 0.4464803636074066,
1003
+ "class_6": 1.9363311366760172e-05
1004
+ }
1005
+ },
1006
+ {
1007
+ "lead_hours": 8,
1008
+ "target_utc": "2026-05-09T03:00:00+00:00",
1009
+ "target_local": "2026-05-09T02:00:00-01:00",
1010
+ "temperature_2m_c": 3.953939437866211,
1011
+ "relative_humidity_2m_pct": 94.34648895263672,
1012
+ "apparent_temperature_c": 0.3805828094482422,
1013
+ "precipitation_mm": 0.11994519084692001,
1014
+ "pressure_msl_hpa": 1005.9537353515625,
1015
+ "surface_pressure_hpa": 973.9803466796875,
1016
+ "cloud_cover_pct": 95.32868957519531,
1017
+ "wind_speed_10m_kmh": 12.735028266906738,
1018
+ "rain_probability": 0.8208134174346924,
1019
+ "weather_class": 5,
1020
+ "weather_class_name": "class_5",
1021
+ "weather_class_probabilities": {
1022
+ "class_0": 0.011135051026940346,
1023
+ "class_1": 0.21503449976444244,
1024
+ "class_2": 0.015187171287834644,
1025
+ "class_3": 0.18286095559597015,
1026
+ "class_4": 0.1143169179558754,
1027
+ "class_5": 0.46144354343414307,
1028
+ "class_6": 2.182190291932784e-05
1029
+ }
1030
+ },
1031
+ {
1032
+ "lead_hours": 9,
1033
+ "target_utc": "2026-05-09T04:00:00+00:00",
1034
+ "target_local": "2026-05-09T03:00:00-01:00",
1035
+ "temperature_2m_c": 3.826430320739746,
1036
+ "relative_humidity_2m_pct": 94.16539001464844,
1037
+ "apparent_temperature_c": 0.16225242614746094,
1038
+ "precipitation_mm": 0.11211992800235748,
1039
+ "pressure_msl_hpa": 1006.1436767578125,
1040
+ "surface_pressure_hpa": 974.2029418945312,
1041
+ "cloud_cover_pct": 94.6148681640625,
1042
+ "wind_speed_10m_kmh": 12.761141777038574,
1043
+ "rain_probability": 0.8099679350852966,
1044
+ "weather_class": 5,
1045
+ "weather_class_name": "class_5",
1046
+ "weather_class_probabilities": {
1047
+ "class_0": 0.013121353462338448,
1048
+ "class_1": 0.23102521896362305,
1049
+ "class_2": 0.017765367403626442,
1050
+ "class_3": 0.1780213713645935,
1051
+ "class_4": 0.12005121260881424,
1052
+ "class_5": 0.4399879574775696,
1053
+ "class_6": 2.7478810807224363e-05
1054
+ }
1055
+ },
1056
+ {
1057
+ "lead_hours": 10,
1058
+ "target_utc": "2026-05-09T05:00:00+00:00",
1059
+ "target_local": "2026-05-09T04:00:00-01:00",
1060
+ "temperature_2m_c": 3.8089590072631836,
1061
+ "relative_humidity_2m_pct": 93.53528594970703,
1062
+ "apparent_temperature_c": 0.12103080749511719,
1063
+ "precipitation_mm": 0.10691206157207489,
1064
+ "pressure_msl_hpa": 1006.42529296875,
1065
+ "surface_pressure_hpa": 974.4669189453125,
1066
+ "cloud_cover_pct": 93.8226318359375,
1067
+ "wind_speed_10m_kmh": 12.700864791870117,
1068
+ "rain_probability": 0.797008216381073,
1069
+ "weather_class": 5,
1070
+ "weather_class_name": "class_5",
1071
+ "weather_class_probabilities": {
1072
+ "class_0": 0.014465805143117905,
1073
+ "class_1": 0.22260957956314087,
1074
+ "class_2": 0.018675586208701134,
1075
+ "class_3": 0.15951745212078094,
1076
+ "class_4": 0.10494350641965866,
1077
+ "class_5": 0.47975844144821167,
1078
+ "class_6": 2.9636566978297196e-05
1079
+ }
1080
+ },
1081
+ {
1082
+ "lead_hours": 11,
1083
+ "target_utc": "2026-05-09T06:00:00+00:00",
1084
+ "target_local": "2026-05-09T05:00:00-01:00",
1085
+ "temperature_2m_c": 3.9785900115966797,
1086
+ "relative_humidity_2m_pct": 92.22869110107422,
1087
+ "apparent_temperature_c": 0.24558448791503906,
1088
+ "precipitation_mm": 0.10196752846240997,
1089
+ "pressure_msl_hpa": 1006.7659301757812,
1090
+ "surface_pressure_hpa": 974.8555297851562,
1091
+ "cloud_cover_pct": 92.74380493164062,
1092
+ "wind_speed_10m_kmh": 12.71017837524414,
1093
+ "rain_probability": 0.7881969213485718,
1094
+ "weather_class": 5,
1095
+ "weather_class_name": "class_5",
1096
+ "weather_class_probabilities": {
1097
+ "class_0": 0.01727476716041565,
1098
+ "class_1": 0.232261523604393,
1099
+ "class_2": 0.019182473421096802,
1100
+ "class_3": 0.15716883540153503,
1101
+ "class_4": 0.10425136238336563,
1102
+ "class_5": 0.46981269121170044,
1103
+ "class_6": 4.833983985008672e-05
1104
+ }
1105
+ },
1106
+ {
1107
+ "lead_hours": 12,
1108
+ "target_utc": "2026-05-09T07:00:00+00:00",
1109
+ "target_local": "2026-05-09T06:00:00-01:00",
1110
+ "temperature_2m_c": 4.317435264587402,
1111
+ "relative_humidity_2m_pct": 90.45832824707031,
1112
+ "apparent_temperature_c": 0.6276102066040039,
1113
+ "precipitation_mm": 0.09439859539270401,
1114
+ "pressure_msl_hpa": 1007.0864868164062,
1115
+ "surface_pressure_hpa": 975.1940307617188,
1116
+ "cloud_cover_pct": 91.2315673828125,
1117
+ "wind_speed_10m_kmh": 12.740204811096191,
1118
+ "rain_probability": 0.7812836170196533,
1119
+ "weather_class": 5,
1120
+ "weather_class_name": "class_5",
1121
+ "weather_class_probabilities": {
1122
+ "class_0": 0.019589632749557495,
1123
+ "class_1": 0.23847728967666626,
1124
+ "class_2": 0.020689163357019424,
1125
+ "class_3": 0.159035325050354,
1126
+ "class_4": 0.08879318088293076,
1127
+ "class_5": 0.47335243225097656,
1128
+ "class_6": 6.291128374869004e-05
1129
+ }
1130
+ }
1131
+ ],
1132
+ "sanity": {
1133
+ "sequence_shape": [
1134
+ 72,
1135
+ 22
1136
+ ],
1137
+ "finite_features": true
1138
+ }
1139
+ }
1140
+ ```
1141
+
1142
+ ### Note
1143
+ In observed outputs, the model is often within **1°C** of the actual value, which is **0.7** more than Hweh-6M.
1144
+
1145
+ 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.
1146
+
1147
+ ## Use Cases
1148
+
1149
+ Intended for:
1150
+
1151
+ 1. Backup to API
1152
+ 2. Offline forecasting if you have the data
1153
+ 3. Research
1154
+ 4. Or more simply, for fun
1155
+
1156
+ Not intended for:
1157
+
1158
+ 1. Safety-critical forecasting (aviation, emergency response)
1159
+ 2. Replacing meteorological or API services
1160
+
1161
+ ## Limitations
1162
+
1163
+ 1. The model is not perfectly accurate and will produce approximate forecasts rather than exact real-world weather conditions.
1164
+ 2. Prediction accuracy decreases as the forecast horizon increases up to 12 hours.
1165
+ 3. Performance may degrade on unseen or underrepresented geographic regions and climate types.
1166
+ 4. The model does not enforce physical laws of atmospheric dynamics and may produce physically inconsistent outputs.
1167
+ 5. Forecast quality is sensitive to the quality and completeness of input weather data.
1168
+ 6. Rare or extreme weather events are underrepresented in training data and may be poorly predicted.
1169
+ 7. Weather class outputs are simplified and do not capture fine-grained meteorological distinctions.
1170
+
1171
+ # Inference
1172
+
1173
+ ```python
1174
+ #!/usr/bin/env python3
1175
+ from __future__ import annotations
1176
+
1177
+ import json
1178
+ import time
1179
+ from pathlib import Path
1180
+ from typing import Any
1181
+
1182
+ import numpy as np
1183
+ import pandas as pd
1184
+ import requests
1185
+ import torch
1186
+ from transformers import AutoConfig, AutoModel
1187
+ from zoneinfo import ZoneInfo
1188
+
1189
+ # ----------------------------
1190
+ # Change these values here
1191
+ # ----------------------------
1192
+ MODEL_ID = r"Harley-ml/Hweh-446k" # HF repo id or local path
1193
+ CITY = "New York"
1194
+ SEQUENCE_META_PATH = "Harley-ml/Hweh-446k/weather_sequences.metadata.json"
1195
+ CONTEXT_HOURS = 72
1196
+ FORECAST_HOURS = 12
1197
+ DEVICE = None # "cpu", "cuda", "cuda:0", or None for auto
1198
+
1199
+ API_BASE_URL = "https://api.open-meteo.com/v1/forecast"
1200
+ MAX_RETRIES = 6
1201
+ REQUEST_TIMEOUT_S = 60
1202
+
1203
+ HOURLY_VARS = [
1204
+ "temperature_2m",
1205
+ "relative_humidity_2m",
1206
+ "apparent_temperature",
1207
+ "precipitation",
1208
+ "weather_code",
1209
+ "pressure_msl",
1210
+ "surface_pressure",
1211
+ "cloud_cover",
1212
+ "visibility",
1213
+ "wind_speed_10m",
1214
+ "wind_direction_10m",
1215
+ ]
1216
+
1217
+ WEATHER_CODE_BUCKETS = 7
1218
+ TEMP_SCALE = 50.0
1219
+ HUMIDITY_SCALE = 100.0
1220
+ WIND_SCALE = 100.0
1221
+
1222
+ # ----------------------------
1223
+ # City metadata (82 locations)
1224
+ # ----------------------------
1225
+ CITY_SPECS: dict[str, dict[str, Any]] = {
1226
+ "Seattle": {"location_id": "1", "latitude": 47.6062, "longitude": -122.3321, "continent": "North America", "climate_tag": "temperate_oceanic", "elevation": 56},
1227
+ "Portland": {"location_id": "2", "latitude": 45.5152, "longitude": -122.6784, "continent": "North America", "climate_tag": "temperate_oceanic", "elevation": 15},
1228
+ "San Francisco": {"location_id": "3", "latitude": 37.7749, "longitude": -122.4194, "continent": "North America", "climate_tag": "foggy_mediterranean", "elevation": 16},
1229
+ "Los Angeles": {"location_id": "4", "latitude": 34.0522, "longitude": -118.2437, "continent": "North America", "climate_tag": "sunny_mediterranean", "elevation": 71},
1230
+ "Denver": {"location_id": "5", "latitude": 39.7392, "longitude": -104.9903, "continent": "North America", "climate_tag": "semi_arid_highland", "elevation": 1609},
1231
+ "Chicago": {"location_id": "6", "latitude": 41.8781, "longitude": -87.6298, "continent": "North America", "climate_tag": "humid_continental", "elevation": 181},
1232
+ "Dallas": {"location_id": "7", "latitude": 32.7767, "longitude": -96.7970, "continent": "North America", "climate_tag": "hot_subhumid", "elevation": 131},
1233
+ "Atlanta": {"location_id": "8", "latitude": 33.7490, "longitude": -84.3880, "continent": "North America", "climate_tag": "humid_subtropical", "elevation": 320},
1234
+ "New York": {"location_id": "9", "latitude": 40.7128, "longitude": -74.0060, "continent": "North America", "climate_tag": "humid_subtropical", "elevation": 10},
1235
+ "Miami": {"location_id": "10", "latitude": 25.7617, "longitude": -80.1918, "continent": "North America", "climate_tag": "tropical_humid", "elevation": 2},
1236
+ "Phoenix": {"location_id": "11", "latitude": 33.4484, "longitude": -112.0740, "continent": "North America", "climate_tag": "hot_arid", "elevation": 331},
1237
+ "Salt Lake City": {"location_id": "12", "latitude": 40.7608, "longitude": -111.8910, "continent": "North America", "climate_tag": "semi_arid", "elevation": 1288},
1238
+ "Anchorage": {"location_id": "13", "latitude": 61.2181, "longitude": -149.9003, "continent": "North America", "climate_tag": "subarctic_snowy", "elevation": 31},
1239
+ "Minneapolis": {"location_id": "14", "latitude": 44.9778, "longitude": -93.2650, "continent": "North America", "climate_tag": "cold_snowy", "elevation": 264},
1240
+ "Toronto": {"location_id": "15", "latitude": 43.6532, "longitude": -79.3832, "continent": "North America", "climate_tag": "humid_continental", "elevation": 76},
1241
+ "Montreal": {"location_id": "16", "latitude": 45.5017, "longitude": -73.5673, "continent": "North America", "climate_tag": "cold_snowy", "elevation": 233},
1242
+ "Vancouver": {"location_id": "17", "latitude": 49.2827, "longitude": -123.1207, "continent": "North America", "climate_tag": "temperate_oceanic", "elevation": 70},
1243
+ "Mexico City": {"location_id": "18", "latitude": 19.4326, "longitude": -99.1332, "continent": "North America", "climate_tag": "highland_subtropical", "elevation": 2240},
1244
+ "Havana": {"location_id": "19", "latitude": 23.1136, "longitude": -82.3666, "continent": "North America", "climate_tag": "tropical_humid", "elevation": 59},
1245
+ "San Juan": {"location_id": "20", "latitude": 18.4655, "longitude": -66.1057, "continent": "North America", "climate_tag": "tropical_humid", "elevation": 8},
1246
+
1247
+ "Lima": {"location_id": "21", "latitude": -12.0464, "longitude": -77.0428, "continent": "South America", "climate_tag": "coastal_arid", "elevation": 154},
1248
+ "Santiago": {"location_id": "22", "latitude": -33.4489, "longitude": -70.6693, "continent": "South America", "climate_tag": "mediterranean", "elevation": 520},
1249
+ "Buenos Aires": {"location_id": "23", "latitude": -34.6037, "longitude": -58.3816, "continent": "South America", "climate_tag": "humid_subtropical", "elevation": 25},
1250
+ "Bogotá": {"location_id": "24", "latitude": 4.7110, "longitude": -74.0721, "continent": "South America", "climate_tag": "highland_cool", "elevation": 2640},
1251
+ "Quito": {"location_id": "25", "latitude": -0.1807, "longitude": -78.4678, "continent": "South America", "climate_tag": "highland_equatorial", "elevation": 2850},
1252
+ "Caracas": {"location_id": "26", "latitude": 10.4806, "longitude": -66.9036, "continent": "South America", "climate_tag": "tropical_humid", "elevation": 900},
1253
+ "Rio de Janeiro": {"location_id": "27", "latitude": -22.9068, "longitude": -43.1729, "continent": "South America", "climate_tag": "tropical_humid", "elevation": 5},
1254
+ "São Paulo": {"location_id": "28", "latitude": -23.5505, "longitude": -46.6333, "continent": "South America", "climate_tag": "humid_subtropical", "elevation": 760},
1255
+ "La Paz": {"location_id": "29", "latitude": -16.4897, "longitude": -68.1193, "continent": "South America", "climate_tag": "highland_cold", "elevation": 3640},
1256
+ "Cusco": {"location_id": "30", "latitude": -13.5319, "longitude": -71.9675, "continent": "South America", "climate_tag": "highland_cool", "elevation": 3399},
1257
+ "Montevideo": {"location_id": "31", "latitude": -34.9011, "longitude": -56.1645, "continent": "South America", "climate_tag": "temperate_oceanic", "elevation": 43},
1258
+ "Asunción": {"location_id": "32", "latitude": -25.2637, "longitude": -57.5759, "continent": "South America", "climate_tag": "humid_subtropical", "elevation": 43},
1259
+ "Manaus": {"location_id": "33", "latitude": -3.1190, "longitude": -60.0217, "continent": "South America", "climate_tag": "tropical_humid", "elevation": 92},
1260
+ "Recife": {"location_id": "34", "latitude": -8.0476, "longitude": -34.8770, "continent": "South America", "climate_tag": "tropical_coastal", "elevation": 4},
1261
+ "Punta Arenas": {"location_id": "35", "latitude": -53.1638, "longitude": -70.9171, "continent": "South America", "climate_tag": "cold_windy", "elevation": 34},
1262
+
1263
+ "London": {"location_id": "36", "latitude": 51.5074, "longitude": -0.1278, "continent": "Europe", "climate_tag": "temperate_oceanic", "elevation": 11},
1264
+ "Paris": {"location_id": "37", "latitude": 48.8566, "longitude": 2.3522, "continent": "Europe", "climate_tag": "temperate_oceanic", "elevation": 35},
1265
+ "Madrid": {"location_id": "38", "latitude": 40.4168, "longitude": -3.7038, "continent": "Europe", "climate_tag": "hot_summer_mediterranean", "elevation": 667},
1266
+ "Rome": {"location_id": "39", "latitude": 41.9028, "longitude": 12.4964, "continent": "Europe", "climate_tag": "hot_summer_mediterranean", "elevation": 21},
1267
+ "Berlin": {"location_id": "40", "latitude": 52.52, "longitude": 13.4050, "continent": "Europe", "climate_tag": "temperate_continental", "elevation": 34},
1268
+ "Stockholm": {"location_id": "41", "latitude": 59.3293, "longitude": 18.0686, "continent": "Europe", "climate_tag": "cold_marine", "elevation": 28},
1269
+ "Oslo": {"location_id": "42", "latitude": 59.9139, "longitude": 10.7522, "continent": "Europe", "climate_tag": "cold_snowy", "elevation": 23},
1270
+ "Helsinki": {"location_id": "43", "latitude": 60.1699, "longitude": 24.9384, "continent": "Europe", "climate_tag": "cold_snowy", "elevation": 25},
1271
+ "Reykjavik": {"location_id": "44", "latitude": 64.1466, "longitude": -21.9426, "continent": "Europe", "climate_tag": "cold_windy", "elevation": 12},
1272
+ "Kyiv": {"location_id": "45", "latitude": 50.4501, "longitude": 30.5234, "continent": "Europe", "climate_tag": "humid_continental", "elevation": 179},
1273
+ "Lisbon": {"location_id": "46", "latitude": 38.7223, "longitude": -9.1393, "continent": "Europe", "climate_tag": "sunny_mediterranean", "elevation": 7},
1274
+ "Athens": {"location_id": "47", "latitude": 37.9838, "longitude": 23.7275, "continent": "Europe", "climate_tag": "sunny_mediterranean", "elevation": 70},
1275
+ "Zurich": {"location_id": "48", "latitude": 47.3769, "longitude": 8.5417, "continent": "Europe", "climate_tag": "temperate_continental", "elevation": 408},
1276
+ "Dublin": {"location_id": "49", "latitude": 53.3498, "longitude": -6.2603, "continent": "Europe", "climate_tag": "temperate_oceanic", "elevation": 20},
1277
+ "Vienna": {"location_id": "50", "latitude": 48.2082, "longitude": 16.3738, "continent": "Europe", "climate_tag": "temperate_continental", "elevation": 171},
1278
+
1279
+ "Dubai": {"location_id": "51", "latitude": 25.2048, "longitude": 55.2708, "continent": "Asia", "climate_tag": "hot_arid", "elevation": 16},
1280
+ "Riyadh": {"location_id": "52", "latitude": 24.7136, "longitude": 46.6753, "continent": "Asia", "climate_tag": "hot_arid", "elevation": 612},
1281
+ "Delhi": {"location_id": "53", "latitude": 28.7041, "longitude": 77.1025, "continent": "Asia", "climate_tag": "hot_semi_arid", "elevation": 216},
1282
+ "Mumbai": {"location_id": "54", "latitude": 19.0760, "longitude": 72.8777, "continent": "Asia", "climate_tag": "tropical_humid", "elevation": 14},
1283
+ "Bangkok": {"location_id": "55", "latitude": 13.7563, "longitude": 100.5018, "continent": "Asia", "climate_tag": "tropical_monsoon", "elevation": 2},
1284
+ "Singapore": {"location_id": "56", "latitude": 1.3521, "longitude": 103.8198, "continent": "Asia", "climate_tag": "tropical_humid", "elevation": 15},
1285
+ "Tokyo": {"location_id": "57", "latitude": 35.6762, "longitude": 139.6503, "continent": "Asia", "climate_tag": "humid_subtropical", "elevation": 40},
1286
+ "Seoul": {"location_id": "58", "latitude": 37.5665, "longitude": 126.9780, "continent": "Asia", "climate_tag": "humid_continental", "elevation": 38},
1287
+ "Ulaanbaatar": {"location_id": "59", "latitude": 47.8864, "longitude": 106.9057, "continent": "Asia", "climate_tag": "cold_steppe", "elevation": 1350},
1288
+ "Kathmandu": {"location_id": "60", "latitude": 27.7172, "longitude": 85.3240, "continent": "Asia", "climate_tag": "highland_subtropical", "elevation": 1400},
1289
+ "Chiang Mai": {"location_id": "61", "latitude": 18.7883, "longitude": 98.9853, "continent": "Asia", "climate_tag": "tropical_seasonal", "elevation": 300},
1290
+ "Lhasa": {"location_id": "62", "latitude": 29.6520, "longitude": 91.1721, "continent": "Asia", "climate_tag": "high_altitude_cold", "elevation": 3656},
1291
+ "Jakarta": {"location_id": "63", "latitude": -6.2088, "longitude": 106.8456, "continent": "Asia", "climate_tag": "tropical_humid", "elevation": 8},
1292
+ "Manila": {"location_id": "64", "latitude": 14.5995, "longitude": 120.9842, "continent": "Asia", "climate_tag": "tropical_humid", "elevation": 16},
1293
+ "Karachi": {"location_id": "65", "latitude": 24.8607, "longitude": 67.0011, "continent": "Asia", "climate_tag": "hot_arid", "elevation": 10},
1294
+
1295
+ "Cairo": {"location_id": "66", "latitude": 30.0444, "longitude": 31.2357, "continent": "Africa", "climate_tag": "hot_arid", "elevation": 23},
1296
+ "Alexandria": {"location_id": "67", "latitude": 31.2001, "longitude": 29.9187, "continent": "Africa", "climate_tag": "coastal_mediterranean", "elevation": 5},
1297
+ "Casablanca": {"location_id": "68", "latitude": 33.5731, "longitude": -7.5898, "continent": "Africa", "climate_tag": "coastal_mediterranean", "elevation": 56},
1298
+ "Marrakech": {"location_id": "69", "latitude": 31.6295, "longitude": -7.9811, "continent": "Africa", "climate_tag": "hot_semi_arid", "elevation": 466},
1299
+ "Lagos": {"location_id": "70", "latitude": 6.5244, "longitude": 3.3792, "continent": "Africa", "climate_tag": "tropical_humid", "elevation": 41},
1300
+ "Nairobi": {"location_id": "71", "latitude": -1.2921, "longitude": 36.8219, "continent": "Africa", "climate_tag": "temperate_highland", "elevation": 1795},
1301
+ "Addis Ababa": {"location_id": "72", "latitude": 8.9806, "longitude": 38.7578, "continent": "Africa", "climate_tag": "temperate_highland", "elevation": 2355},
1302
+ "Cape Town": {"location_id": "73", "latitude": -33.9249, "longitude": 18.4241, "continent": "Africa", "climate_tag": "mediterranean", "elevation": 25},
1303
+ "Johannesburg": {"location_id": "74", "latitude": -26.2041, "longitude": 28.0473, "continent": "Africa", "climate_tag": "subtropical_highland", "elevation": 1753},
1304
+ "Windhoek": {"location_id": "75", "latitude": -22.5609, "longitude": 17.0658, "continent": "Africa", "climate_tag": "semi_arid", "elevation": 1650},
1305
+ "Accra": {"location_id": "76", "latitude": 5.6037, "longitude": -0.1870, "continent": "Africa", "climate_tag": "tropical_humid", "elevation": 61},
1306
+ "Kigali": {"location_id": "77", "latitude": -1.9441, "longitude": 30.0619, "continent": "Africa", "climate_tag": "highland_tropical", "elevation": 1567},
1307
+ "Tunis": {"location_id": "78", "latitude": 36.8065, "longitude": 10.1815, "continent": "Africa", "climate_tag": "mediterranean", "elevation": 4},
1308
+ "Dakar": {"location_id": "79", "latitude": -14.7167, "longitude": -17.4677, "continent": "Africa", "climate_tag": "hot_coastal", "elevation": 25},
1309
+ "Mombasa": {"location_id": "80", "latitude": -4.0435, "longitude": 39.6682, "continent": "Africa", "climate_tag": "tropical_coastal", "elevation": 17},
1310
+
1311
+ "Sydney": {"location_id": "81", "latitude": -33.8688, "longitude": 151.2093, "continent": "Oceania", "climate_tag": "humid_subtropical", "elevation": 58},
1312
+ "Melbourne": {"location_id": "82", "latitude": -37.8136, "longitude": 144.9631, "continent": "Oceania", "climate_tag": "temperate_oceanic", "elevation": 31},
1313
+ }
1314
+
1315
+ CITY_TIMEZONES: dict[str, str] = {
1316
+ "Seattle": "America/Los_Angeles",
1317
+ "Portland": "America/Los_Angeles",
1318
+ "San Francisco": "America/Los_Angeles",
1319
+ "Los Angeles": "America/Los_Angeles",
1320
+ "Denver": "America/Denver",
1321
+ "Chicago": "America/Chicago",
1322
+ "Dallas": "America/Chicago",
1323
+ "Atlanta": "America/New_York",
1324
+ "New York": "America/New_York",
1325
+ "Miami": "America/New_York",
1326
+ "Phoenix": "America/Phoenix",
1327
+ "Salt Lake City": "America/Denver",
1328
+ "Anchorage": "America/Anchorage",
1329
+ "Minneapolis": "America/Chicago",
1330
+ "Toronto": "America/Toronto",
1331
+ "Montreal": "America/Toronto",
1332
+ "Vancouver": "America/Vancouver",
1333
+ "Mexico City": "America/Mexico_City",
1334
+ "Havana": "America/Havana",
1335
+ "San Juan": "America/Puerto_Rico",
1336
+ "Lima": "America/Lima",
1337
+ "Santiago": "America/Santiago",
1338
+ "Buenos Aires": "America/Argentina/Buenos_Aires",
1339
+ "Bogotá": "America/Bogota",
1340
+ "Quito": "America/Guayaquil",
1341
+ "Caracas": "America/Caracas",
1342
+ "Rio de Janeiro": "America/Sao_Paulo",
1343
+ "São Paulo": "America/Sao_Paulo",
1344
+ "La Paz": "America/La_Paz",
1345
+ "Cusco": "America/Lima",
1346
+ "Montevideo": "America/Montevideo",
1347
+ "Asunción": "America/Asuncion",
1348
+ "Manaus": "America/Manaus",
1349
+ "Recife": "America/Recife",
1350
+ "Punta Arenas": "America/Punta_Arenas",
1351
+ "London": "Europe/London",
1352
+ "Paris": "Europe/Paris",
1353
+ "Madrid": "Europe/Madrid",
1354
+ "Rome": "Europe/Rome",
1355
+ "Berlin": "Europe/Berlin",
1356
+ "Stockholm": "Europe/Stockholm",
1357
+ "Oslo": "Europe/Oslo",
1358
+ "Helsinki": "Europe/Helsinki",
1359
+ "Reykjavik": "Atlantic/Reykjavik",
1360
+ "Kyiv": "Europe/Kyiv",
1361
+ "Lisbon": "Europe/Lisbon",
1362
+ "Athens": "Europe/Athens",
1363
+ "Zurich": "Europe/Zurich",
1364
+ "Dublin": "Europe/Dublin",
1365
+ "Vienna": "Europe/Vienna",
1366
+ "Dubai": "Asia/Dubai",
1367
+ "Riyadh": "Asia/Riyadh",
1368
+ "Delhi": "Asia/Kolkata",
1369
+ "Mumbai": "Asia/Kolkata",
1370
+ "Bangkok": "Asia/Bangkok",
1371
+ "Singapore": "Asia/Singapore",
1372
+ "Tokyo": "Asia/Tokyo",
1373
+ "Seoul": "Asia/Seoul",
1374
+ "Ulaanbaatar": "Asia/Ulaanbaatar",
1375
+ "Kathmandu": "Asia/Kathmandu",
1376
+ "Chiang Mai": "Asia/Bangkok",
1377
+ "Lhasa": "Asia/Shanghai",
1378
+ "Jakarta": "Asia/Jakarta",
1379
+ "Manila": "Asia/Manila",
1380
+ "Karachi": "Asia/Karachi",
1381
+ "Cairo": "Africa/Cairo",
1382
+ "Alexandria": "Africa/Cairo",
1383
+ "Casablanca": "Africa/Casablanca",
1384
+ "Marrakech": "Africa/Casablanca",
1385
+ "Lagos": "Africa/Lagos",
1386
+ "Nairobi": "Africa/Nairobi",
1387
+ "Addis Ababa": "Africa/Addis_Ababa",
1388
+ "Cape Town": "Africa/Johannesburg",
1389
+ "Johannesburg": "Africa/Johannesburg",
1390
+ "Windhoek": "Africa/Windhoek",
1391
+ "Accra": "Africa/Accra",
1392
+ "Kigali": "Africa/Kigali",
1393
+ "Tunis": "Africa/Tunis",
1394
+ "Dakar": "Africa/Dakar",
1395
+ "Mombasa": "Africa/Nairobi",
1396
+ "Sydney": "Australia/Sydney",
1397
+ "Melbourne": "Australia/Melbourne",
1398
+ }
1399
+
1400
+ # ----------------------------
1401
+ # Helpers
1402
+ # ----------------------------
1403
+ def weather_code_to_bucket(code) -> int:
1404
+ if code is None:
1405
+ return 1
1406
+ try:
1407
+ if pd.isna(code):
1408
+ return 1
1409
+ except Exception:
1410
+ pass
1411
+
1412
+ code = int(code)
1413
+ if code == 0:
1414
+ return 0
1415
+ if code in (1, 2, 3):
1416
+ return 1
1417
+ if code in (45, 48):
1418
+ return 2
1419
+ if code in (51, 53, 55, 56, 57):
1420
+ return 3
1421
+ if code in (61, 63, 65, 66, 67, 80, 81, 82):
1422
+ return 4
1423
+ if code in (71, 73, 75, 77, 85, 86):
1424
+ return 5
1425
+ if code in (95, 96, 99):
1426
+ return 6
1427
+ return 1
1428
+
1429
+
1430
+ def cyc(x: np.ndarray, period: float) -> tuple[np.ndarray, np.ndarray]:
1431
+ angle = 2.0 * np.pi * (x / period)
1432
+ return np.sin(angle), np.cos(angle)
1433
+
1434
+
1435
+ def clamp_array(x: np.ndarray, lo: float | None = None, hi: float | None = None) -> np.ndarray:
1436
+ return np.clip(x, lo, hi)
1437
+
1438
+
1439
+ def request_with_backoff(session: requests.Session, url: str, params: dict[str, Any]) -> dict[str, Any]:
1440
+ last_exc: Exception | None = None
1441
+ for attempt in range(MAX_RETRIES):
1442
+ try:
1443
+ resp = session.get(url, params=params, timeout=REQUEST_TIMEOUT_S)
1444
+ if resp.status_code == 429:
1445
+ retry_after = resp.headers.get("Retry-After")
1446
+ sleep_s = float(retry_after) if retry_after else min(60.0, 2**attempt)
1447
+ print(f"Rate limited. Sleeping {sleep_s:.1f}s and retrying.", flush=True)
1448
+ time.sleep(sleep_s)
1449
+ continue
1450
+ resp.raise_for_status()
1451
+ return resp.json()
1452
+ except Exception as e:
1453
+ last_exc = e
1454
+ sleep_s = min(60.0, 2**attempt)
1455
+ print(f"Request failed: {e}. Sleeping {sleep_s:.1f}s and retrying.", flush=True)
1456
+ time.sleep(sleep_s)
1457
+ raise RuntimeError(f"Failed after {MAX_RETRIES} retries: {params}") from last_exc
1458
+
1459
+
1460
+ def load_sequence_meta(path: str) -> dict[str, Any]:
1461
+ p = Path(path)
1462
+ if not p.exists():
1463
+ return {"location_to_id": {}}
1464
+ with open(p, "r", encoding="utf-8") as f:
1465
+ meta = json.load(f)
1466
+ meta.setdefault("location_to_id", {})
1467
+ return meta
1468
+
1469
+
1470
+ def load_model():
1471
+ config = AutoConfig.from_pretrained(MODEL_ID, trust_remote_code=True)
1472
+ model = AutoModel.from_pretrained(MODEL_ID, config=config, trust_remote_code=True)
1473
+ model.eval()
1474
+ return model, config
1475
+
1476
+
1477
+ def fetch_recent_history(city: str, context_hours: int) -> pd.DataFrame:
1478
+ if city not in CITY_SPECS:
1479
+ raise ValueError(f"Unknown city: {city}")
1480
+
1481
+ spec = CITY_SPECS[city]
1482
+ session = requests.Session()
1483
+ session.headers.update({"User-Agent": "Mozilla/5.0"})
1484
+
1485
+ params = {
1486
+ "latitude": spec["latitude"],
1487
+ "longitude": spec["longitude"],
1488
+ "hourly": ",".join(HOURLY_VARS),
1489
+ "timezone": "UTC",
1490
+ "temperature_unit": "celsius",
1491
+ "wind_speed_unit": "kmh",
1492
+ "precipitation_unit": "mm",
1493
+ "past_hours": int(context_hours) + 2,
1494
+ "forecast_hours": 0,
1495
+ }
1496
+
1497
+ data = request_with_backoff(session, API_BASE_URL, params=params)
1498
+ hourly = data.get("hourly", {})
1499
+ if "time" not in hourly:
1500
+ raise ValueError(f"No hourly data returned for {city}: {data}")
1501
+
1502
+ df = pd.DataFrame(hourly)
1503
+ if df.empty:
1504
+ raise ValueError(f"Empty hourly response for {city}.")
1505
+
1506
+ df["time"] = pd.to_datetime(df["time"], errors="coerce", utc=True)
1507
+ df = df.dropna(subset=["time"]).sort_values("time").drop_duplicates(subset=["time"]).reset_index(drop=True)
1508
+
1509
+ needed = HOURLY_VARS
1510
+ missing = [c for c in needed if c not in df.columns]
1511
+ if missing:
1512
+ raise ValueError(f"Missing hourly columns in API response: {missing}")
1513
+
1514
+ for c in needed:
1515
+ df[c] = pd.to_numeric(df[c], errors="coerce")
1516
+
1517
+ df["weather_code"] = df["weather_code"].fillna(1)
1518
+ df["precipitation"] = df["precipitation"].fillna(0.0)
1519
+
1520
+ for c in [
1521
+ "temperature_2m",
1522
+ "relative_humidity_2m",
1523
+ "apparent_temperature",
1524
+ "precipitation",
1525
+ "pressure_msl",
1526
+ "surface_pressure",
1527
+ "cloud_cover",
1528
+ "visibility",
1529
+ "wind_speed_10m",
1530
+ "wind_direction_10m",
1531
+ ]:
1532
+ df[c] = df[c].interpolate(limit_direction="both").ffill().bfill()
1533
+
1534
+ now_utc = pd.Timestamp.now(tz="UTC")
1535
+ df = df[df["time"] <= now_utc].copy()
1536
+
1537
+ if len(df) < context_hours:
1538
+ raise ValueError(f"Not enough observed rows: got {len(df)}, need {context_hours}")
1539
+
1540
+ return df.tail(context_hours).reset_index(drop=True)
1541
+
1542
+
1543
+ def build_single_sequence(df: pd.DataFrame) -> np.ndarray:
1544
+ hour = df["time"].dt.hour.to_numpy()
1545
+ doy = df["time"].dt.dayofyear.to_numpy()
1546
+
1547
+ hour_sin, hour_cos = cyc(hour.astype(float), 24.0)
1548
+ doy_sin, doy_cos = cyc(doy.astype(float), 365.25)
1549
+
1550
+ temp = np.nan_to_num(df["temperature_2m"].astype(float).to_numpy(), nan=0.0)
1551
+ humidity = np.nan_to_num(df["relative_humidity_2m"].astype(float).to_numpy(), nan=0.0)
1552
+ apparent = np.nan_to_num(df["apparent_temperature"].astype(float).to_numpy(), nan=0.0)
1553
+ precip = np.nan_to_num(df["precipitation"].astype(float).to_numpy(), nan=0.0)
1554
+ pressure = np.nan_to_num(df["pressure_msl"].astype(float).to_numpy(), nan=0.0)
1555
+ surface_pressure = np.nan_to_num(df["surface_pressure"].astype(float).to_numpy(), nan=0.0)
1556
+ cloud_cover = np.nan_to_num(df["cloud_cover"].astype(float).to_numpy(), nan=0.0)
1557
+ visibility = np.nan_to_num(df["visibility"].astype(float).to_numpy(), nan=0.0)
1558
+ wind = np.nan_to_num(df["wind_speed_10m"].astype(float).to_numpy(), nan=0.0)
1559
+ wind_dir = np.nan_to_num(df["wind_direction_10m"].astype(float).to_numpy(), nan=0.0)
1560
+
1561
+ humidity = clamp_array(humidity, 0.0, 100.0)
1562
+ cloud_cover = clamp_array(cloud_cover, 0.0, 100.0)
1563
+ precip = clamp_array(precip, 0.0, None)
1564
+ wind = clamp_array(wind, 0.0, None)
1565
+ visibility = clamp_array(visibility, 0.0, None)
1566
+
1567
+ wind_dir_sin, wind_dir_cos = cyc(wind_dir, 360.0)
1568
+ weather_bucket = df["weather_code"].fillna(1).apply(weather_code_to_bucket).to_numpy(dtype=np.int64)
1569
+
1570
+ rows = []
1571
+ for i in range(len(df)):
1572
+ wc_oh = np.zeros(WEATHER_CODE_BUCKETS, dtype=np.float32)
1573
+ wc_oh[weather_bucket[i]] = 1.0
1574
+
1575
+ row = np.concatenate(
1576
+ [
1577
+ np.array(
1578
+ [
1579
+ temp[i] / TEMP_SCALE,
1580
+ humidity[i] / HUMIDITY_SCALE,
1581
+ apparent[i] / TEMP_SCALE,
1582
+ np.log1p(max(precip[i], 0.0)) / 3.0,
1583
+ pressure[i] / 1100.0,
1584
+ surface_pressure[i] / 1100.0,
1585
+ cloud_cover[i] / 100.0,
1586
+ visibility[i] / 50000.0,
1587
+ wind[i] / WIND_SCALE,
1588
+ wind_dir_sin[i],
1589
+ wind_dir_cos[i],
1590
+ hour_sin[i],
1591
+ hour_cos[i],
1592
+ doy_sin[i],
1593
+ doy_cos[i],
1594
+ ],
1595
+ dtype=np.float32,
1596
+ ),
1597
+ wc_oh,
1598
+ ]
1599
+ )
1600
+ rows.append(row)
1601
+
1602
+ seq = np.asarray(rows, dtype=np.float32)
1603
+
1604
+ if not np.isfinite(seq).all():
1605
+ bad = np.argwhere(~np.isfinite(seq))
1606
+ raise ValueError(f"Non-finite values remain in sequence at positions like: {bad[:10].tolist()}")
1607
+
1608
+ return seq
1609
+
1610
+
1611
+ def to_iso(ts: pd.Timestamp, tz_name: str | None = None) -> str:
1612
+ if tz_name:
1613
+ try:
1614
+ return ts.tz_convert(ZoneInfo(tz_name)).isoformat()
1615
+ except Exception:
1616
+ pass
1617
+ return ts.isoformat()
1618
+
1619
+
1620
+ def get_logits(out):
1621
+ if isinstance(out, dict) and "logits" in out:
1622
+ return out["logits"]
1623
+ if hasattr(out, "logits"):
1624
+ return out.logits
1625
+ return out
1626
+
1627
+
1628
+ def resolve_location_index(seq_meta: dict[str, Any], city_location_id: str) -> int:
1629
+ location_to_id = seq_meta.get("location_to_id", {})
1630
+
1631
+ if city_location_id in location_to_id:
1632
+ return int(location_to_id[city_location_id])
1633
+
1634
+ try:
1635
+ as_int = int(city_location_id)
1636
+ if as_int in location_to_id:
1637
+ return int(location_to_id[as_int])
1638
+ if str(as_int) in location_to_id:
1639
+ return int(location_to_id[str(as_int)])
1640
+ except Exception:
1641
+ pass
1642
+
1643
+ for unk_key in ("UNK", "<UNK>", "unknown", "UNKNOWN"):
1644
+ if unk_key in location_to_id:
1645
+ return int(location_to_id[unk_key])
1646
+
1647
+ return 0
1648
+
1649
+
1650
+ def predict():
1651
+ seq_meta = load_sequence_meta(SEQUENCE_META_PATH)
1652
+ model, config = load_model()
1653
+
1654
+ if CITY not in CITY_SPECS:
1655
+ raise ValueError(f"Unknown city: {CITY}")
1656
+
1657
+ if CONTEXT_HOURS <= 0:
1658
+ raise ValueError("CONTEXT_HOURS must be > 0")
1659
+
1660
+ if hasattr(config, "seq_len") and int(config.seq_len) != CONTEXT_HOURS:
1661
+ raise ValueError(f"Set CONTEXT_HOURS to {int(config.seq_len)} for this model.")
1662
+
1663
+ city_spec = CITY_SPECS[CITY]
1664
+ city_tz = CITY_TIMEZONES.get(CITY, "UTC")
1665
+ model_location_id = resolve_location_index(seq_meta, str(city_spec["location_id"]))
1666
+
1667
+ df = fetch_recent_history(CITY, CONTEXT_HOURS)
1668
+ seq = build_single_sequence(df)
1669
+
1670
+ X = torch.from_numpy(seq).unsqueeze(0)
1671
+ loc = torch.tensor([model_location_id], dtype=torch.long)
1672
+
1673
+ target_device = torch.device(
1674
+ DEVICE if DEVICE else ("cuda" if torch.cuda.is_available() else "cpu")
1675
+ )
1676
+ model = model.to(target_device)
1677
+ X = X.to(target_device)
1678
+ loc = loc.to(target_device)
1679
+
1680
+ weather_class_names = getattr(config, "weather_class_names", None)
1681
+ if not weather_class_names:
1682
+ weather_class_names = [f"class_{i}" for i in range(int(getattr(config, "num_weather_classes", 7)))]
1683
+
1684
+ with torch.no_grad():
1685
+ out = model(X=X, location_id=loc)
1686
+ logits = get_logits(out)
1687
+
1688
+ (
1689
+ temp_pred,
1690
+ humidity_pred,
1691
+ apparent_pred,
1692
+ precip_pred,
1693
+ sea_level_pressure_pred,
1694
+ surface_pressure_pred,
1695
+ cloud_cover_pred,
1696
+ wind_pred,
1697
+ wind_dir_sin_pred,
1698
+ wind_dir_cos_pred,
1699
+ rain_logit,
1700
+ weather_logits,
1701
+ ) = logits
1702
+
1703
+ temp_pred = temp_pred.squeeze(0).detach().cpu().numpy()
1704
+ humidity_pred = humidity_pred.squeeze(0).detach().cpu().numpy()
1705
+ apparent_pred = apparent_pred.squeeze(0).detach().cpu().numpy()
1706
+ precip_pred = precip_pred.squeeze(0).detach().cpu().numpy()
1707
+ sea_level_pressure_pred = sea_level_pressure_pred.squeeze(0).detach().cpu().numpy()
1708
+ surface_pressure_pred = surface_pressure_pred.squeeze(0).detach().cpu().numpy()
1709
+ cloud_cover_pred = cloud_cover_pred.squeeze(0).detach().cpu().numpy()
1710
+ wind_pred = wind_pred.squeeze(0).detach().cpu().numpy()
1711
+ rain_prob = torch.sigmoid(rain_logit).squeeze(0).detach().cpu().numpy()
1712
+ weather_probs = torch.softmax(weather_logits, dim=-1).squeeze(0).detach().cpu().numpy()
1713
+ weather_idx = np.argmax(weather_probs, axis=-1).astype(np.int64)
1714
+
1715
+ humidity_pred = np.clip(humidity_pred, 0.0, 100.0)
1716
+ cloud_cover_pred = np.clip(cloud_cover_pred, 0.0, 100.0)
1717
+ precip_pred = np.clip(precip_pred, 0.0, None)
1718
+ wind_pred = np.clip(wind_pred, 0.0, None)
1719
+ rain_prob = np.clip(rain_prob, 0.0, 1.0)
1720
+
1721
+ context_start = df["time"].iloc[0]
1722
+ context_end = df["time"].iloc[-1]
1723
+ requested_at_utc = pd.Timestamp.now(tz="UTC")
1724
+
1725
+ horizon = min(
1726
+ int(FORECAST_HOURS),
1727
+ int(temp_pred.shape[0]),
1728
+ int(humidity_pred.shape[0]),
1729
+ int(weather_idx.shape[0]),
1730
+ )
1731
+
1732
+ forecast = []
1733
+ for lead in range(1, horizon + 1):
1734
+ target_time = context_end + pd.Timedelta(hours=lead)
1735
+ idx = lead - 1
1736
+ w_idx = int(weather_idx[idx])
1737
+
1738
+ forecast.append(
1739
+ {
1740
+ "lead_hours": lead,
1741
+ "target_utc": target_time.isoformat(),
1742
+ "target_local": to_iso(target_time, city_tz),
1743
+ "temperature_2m_c": float(temp_pred[idx]),
1744
+ "relative_humidity_2m_pct": float(humidity_pred[idx]),
1745
+ "apparent_temperature_c": float(apparent_pred[idx]),
1746
+ "precipitation_mm": float(precip_pred[idx]),
1747
+ "pressure_msl_hpa": float(sea_level_pressure_pred[idx]),
1748
+ "surface_pressure_hpa": float(surface_pressure_pred[idx]),
1749
+ "cloud_cover_pct": float(cloud_cover_pred[idx]),
1750
+ "wind_speed_10m_kmh": float(wind_pred[idx]),
1751
+ "rain_probability": float(rain_prob[idx]),
1752
+ "weather_class": w_idx,
1753
+ "weather_class_name": weather_class_names[w_idx] if w_idx < len(weather_class_names) else f"class_{w_idx}",
1754
+ "weather_class_probabilities": {
1755
+ name: float(prob) for name, prob in zip(weather_class_names, weather_probs[idx])
1756
+ },
1757
+ }
1758
+ )
1759
+
1760
+ result = {
1761
+ "city": CITY,
1762
+ "location_id": str(city_spec["location_id"]),
1763
+ "model_location_id": int(model_location_id),
1764
+ "data_source": "open-meteo forecast api (past-hours context only)",
1765
+ "requested_at_utc": requested_at_utc.isoformat(),
1766
+ "context": {
1767
+ "hours": int(len(df)),
1768
+ "start_utc": context_start.isoformat(),
1769
+ "end_utc": context_end.isoformat(),
1770
+ "start_local": to_iso(context_start, city_tz),
1771
+ "end_local": to_iso(context_end, city_tz),
1772
+ },
1773
+ "model": {
1774
+ "model_id": MODEL_ID,
1775
+ "encoder_type": getattr(config, "encoder_type", None),
1776
+ "seq_len": int(getattr(config, "seq_len", CONTEXT_HOURS)),
1777
+ "input_dim": int(getattr(config, "input_dim", seq.shape[1])),
1778
+ "num_weather_classes": int(getattr(config, "num_weather_classes", len(weather_class_names))),
1779
+ },
1780
+ "forecast": forecast,
1781
+ "sanity": {
1782
+ "sequence_shape": list(seq.shape),
1783
+ "finite_features": bool(np.isfinite(seq).all()),
1784
+ },
1785
+ }
1786
+
1787
+ print(json.dumps(result, indent=2))
1788
+
1789
+
1790
+ if __name__ == "__main__":
1791
+ predict()
1792
+ ```
1793
+
1794
+
1795
+ ## Citation
1796
+
1797
+ ```bibtex
1798
+ @misc{Hweh-6m,
1799
+ title = {Hweh-446k: Knowledge Distillation in Short-Term Multivariate Weather Forecasting},
1800
+ author = {Paul Courneya; Harley-ml},
1801
+ year = {2026},
1802
+ url = {https://huggingface.co/Harley-ml/DistilHweh-446k}
1803
+ }
1804
+ ```