Upload YEL_2_0_Inference.ipynb

#3
Files changed (1) hide show
  1. YEL_2_0_Inference.ipynb +290 -0
YEL_2_0_Inference.ipynb ADDED
@@ -0,0 +1,290 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# YEL 2.0 wind-field inference\n",
8
+ "\n",
9
+ "This notebook downloads the public **SustainableUrbanSystemsLab/Yel-2.0** ONNX model and runs it using the same 8-channel, 504 × 504 feature layout as Eddy3D's `Wind Predictor` component.\n",
10
+ "\n",
11
+ "> Important: YEL 1.0 is a different, 3-channel GAN architecture. This notebook intentionally uses **YEL 2.0**.\n",
12
+ "\n",
13
+ "Your input CSV should contain one row per 2 m grid point and these columns:\n",
14
+ "\n",
15
+ "| Column | Meaning |\n",
16
+ "|---|---|\n",
17
+ "| `x`, `y` | World coordinates in metres |\n",
18
+ "| `z_relative` | Evaluation height relative to the local ground/building datum (m) |\n",
19
+ "| `sdf` | Signed-distance feature used by Eddy3D (m; typically negative distance to the nearest building) |\n",
20
+ "| `building_height` | Building height at the point (m) |\n",
21
+ "| `u_ratio` | Atmospheric-profile velocity at `z_relative`, divided by reference velocity |\n",
22
+ "\n",
23
+ "The notebook includes a synthetic open-terrain example so every cell can run before you load real data."
24
+ ]
25
+ },
26
+ {
27
+ "cell_type": "code",
28
+ "execution_count": null,
29
+ "metadata": {},
30
+ "outputs": [],
31
+ "source": [
32
+ "%pip install -q numpy pandas matplotlib onnxruntime huggingface_hub"
33
+ ]
34
+ },
35
+ {
36
+ "cell_type": "code",
37
+ "execution_count": null,
38
+ "metadata": {},
39
+ "outputs": [],
40
+ "source": [
41
+ "from pathlib import Path\n",
42
+ "\n",
43
+ "import matplotlib.pyplot as plt\n",
44
+ "import numpy as np\n",
45
+ "import onnxruntime as ort\n",
46
+ "import pandas as pd\n",
47
+ "from huggingface_hub import hf_hub_download\n",
48
+ "\n",
49
+ "MODEL_REPO = \"SustainableUrbanSystemsLab/Yel-2.0\"\n",
50
+ "MODEL_FILE = \"Yel.onnx\"\n",
51
+ "\n",
52
+ "H = W = 504\n",
53
+ "CHANNELS = 8\n",
54
+ "X_MIN, X_MAX, X_STEP = -485.0, 521.0, 2.0\n",
55
+ "Y_MIN, Y_MAX, Y_STEP = -514.5, 491.5, 2.0\n",
56
+ "REQUIRED_COLUMNS = [\"x\", \"y\", \"z_relative\", \"sdf\", \"building_height\", \"u_ratio\"]\n",
57
+ "\n",
58
+ "# User settings\n",
59
+ "INPUT_CSV = None # Example: Path(\"my_yel_features.csv\"); keep None to run the demo\n",
60
+ "WIND_DIRECTIONS_DEG = [0, 45, 90, 180, 270]\n",
61
+ "REFERENCE_WIND_SPEED = 5.0 # Uref in m/s\n",
62
+ "OUTPUT_CSV = Path(\"yel_predictions.csv\")"
63
+ ]
64
+ },
65
+ {
66
+ "cell_type": "markdown",
67
+ "metadata": {},
68
+ "source": [
69
+ "## Load features\n",
70
+ "\n",
71
+ "The demo represents open terrain at pedestrian level. It is useful for checking the inference pipeline, but meaningful building predictions require features prepared from your own geometry (or exported from Eddy3D's preprocessing workflow)."
72
+ ]
73
+ },
74
+ {
75
+ "cell_type": "code",
76
+ "execution_count": null,
77
+ "metadata": {},
78
+ "outputs": [],
79
+ "source": [
80
+ "def make_demo_features(step=2.0, extent=160.0):\n",
81
+ " xs = np.arange(-extent, extent + step, step, dtype=np.float32)\n",
82
+ " ys = np.arange(-extent, extent + step, step, dtype=np.float32)\n",
83
+ " xx, yy = np.meshgrid(xs, ys)\n",
84
+ " n = xx.size\n",
85
+ " return pd.DataFrame({\n",
86
+ " \"x\": xx.ravel(),\n",
87
+ " \"y\": yy.ravel(),\n",
88
+ " \"z_relative\": np.full(n, 1.8, dtype=np.float32),\n",
89
+ " \"sdf\": np.full(n, -100.0, dtype=np.float32),\n",
90
+ " \"building_height\": np.zeros(n, dtype=np.float32),\n",
91
+ " \"u_ratio\": np.ones(n, dtype=np.float32),\n",
92
+ " })\n",
93
+ "\n",
94
+ "if INPUT_CSV is None:\n",
95
+ " features = make_demo_features()\n",
96
+ " print(\"Using the synthetic open-terrain demo.\")\n",
97
+ "else:\n",
98
+ " features = pd.read_csv(INPUT_CSV)\n",
99
+ " print(f\"Loaded {len(features):,} rows from {INPUT_CSV}\")\n",
100
+ "\n",
101
+ "missing = sorted(set(REQUIRED_COLUMNS) - set(features.columns))\n",
102
+ "if missing:\n",
103
+ " raise ValueError(f\"Missing required CSV columns: {missing}\")\n",
104
+ "if features[REQUIRED_COLUMNS].isna().any().any():\n",
105
+ " raise ValueError(\"The required feature columns contain missing values.\")\n",
106
+ "\n",
107
+ "features.head()"
108
+ ]
109
+ },
110
+ {
111
+ "cell_type": "markdown",
112
+ "metadata": {},
113
+ "source": [
114
+ "## Download and inspect YEL 2.0"
115
+ ]
116
+ },
117
+ {
118
+ "cell_type": "code",
119
+ "execution_count": null,
120
+ "metadata": {},
121
+ "outputs": [],
122
+ "source": [
123
+ "model_path = Path(hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE))\n",
124
+ "providers = [\"CPUExecutionProvider\"]\n",
125
+ "session = ort.InferenceSession(str(model_path), providers=providers)\n",
126
+ "\n",
127
+ "input_meta = session.get_inputs()[0]\n",
128
+ "output_meta = session.get_outputs()[0]\n",
129
+ "print(f\"Model: {model_path}\")\n",
130
+ "print(f\"Input: {input_meta.name} {input_meta.shape} ({input_meta.type})\")\n",
131
+ "print(f\"Output: {output_meta.name} {output_meta.shape} ({output_meta.type})\")\n",
132
+ "\n",
133
+ "shape = input_meta.shape\n",
134
+ "known_shape = tuple(v if isinstance(v, int) else None for v in shape)\n",
135
+ "expected = (1, CHANNELS, H, W)\n",
136
+ "if any(actual is not None and actual != wanted for actual, wanted in zip(known_shape, expected)):\n",
137
+ " raise ValueError(\n",
138
+ " f\"Expected a YEL 2.0 input compatible with {expected}, but the model reports {shape}. \"\n",
139
+ " \"Make sure you did not download YEL 1.0.\"\n",
140
+ " )"
141
+ ]
142
+ },
143
+ {
144
+ "cell_type": "markdown",
145
+ "metadata": {},
146
+ "source": [
147
+ "## Assemble Eddy3D-compatible tensors\n",
148
+ "\n",
149
+ "Channels are: `x`, `y`, `z_relative`, `sdf`, `building_height`, `u_ratio`, `-sin(direction)`, and `-cos(direction)`. Feature values and direction values are scattered only at supplied grid points, matching the plugin."
150
+ ]
151
+ },
152
+ {
153
+ "cell_type": "code",
154
+ "execution_count": null,
155
+ "metadata": {},
156
+ "outputs": [],
157
+ "source": [
158
+ "def prepare_base_tensor(frame):\n",
159
+ " data = frame.copy().reset_index(drop=True)\n",
160
+ " ix = np.rint((data[\"x\"].to_numpy() - X_MIN) / X_STEP).astype(np.int64)\n",
161
+ " iy = np.rint((data[\"y\"].to_numpy() - Y_MIN) / Y_STEP).astype(np.int64)\n",
162
+ " valid = (ix >= 0) & (ix < W) & (iy >= 0) & (iy < H)\n",
163
+ " if not valid.any():\n",
164
+ " raise ValueError(\"No input points fall inside YEL's fixed world grid.\")\n",
165
+ "\n",
166
+ " tensor = np.zeros((1, CHANNELS, H, W), dtype=np.float32)\n",
167
+ " tensor[0, 0] = np.broadcast_to(np.linspace(X_MIN, X_MAX, W, dtype=np.float32), (H, W))\n",
168
+ " tensor[0, 1] = np.broadcast_to(np.linspace(Y_MIN, Y_MAX, H, dtype=np.float32)[:, None], (H, W))\n",
169
+ "\n",
170
+ " rows, cols = iy[valid], ix[valid]\n",
171
+ " for channel, column in enumerate(REQUIRED_COLUMNS[2:], start=2):\n",
172
+ " tensor[0, channel, rows, cols] = data.loc[valid, column].to_numpy(dtype=np.float32)\n",
173
+ "\n",
174
+ " return data, tensor, ix, iy, valid\n",
175
+ "\n",
176
+ "\n",
177
+ "def tensor_for_direction(base_tensor, ix, iy, valid, direction_deg):\n",
178
+ " tensor = base_tensor.copy()\n",
179
+ " radians = np.deg2rad(direction_deg)\n",
180
+ " rows, cols = iy[valid], ix[valid]\n",
181
+ " tensor[0, 6, rows, cols] = np.float32(-np.sin(radians))\n",
182
+ " tensor[0, 7, rows, cols] = np.float32(-np.cos(radians))\n",
183
+ " return tensor\n",
184
+ "\n",
185
+ "\n",
186
+ "data, base_tensor, grid_x, grid_y, valid_mask = prepare_base_tensor(features)\n",
187
+ "print(f\"{valid_mask.sum():,}/{len(data):,} points are inside the fixed YEL grid.\")\n",
188
+ "if valid_mask.sum() < len(data):\n",
189
+ " print(f\"Grid limits: X={X_MIN}…{X_MAX}, Y={Y_MIN}…{Y_MAX} metres\")"
190
+ ]
191
+ },
192
+ {
193
+ "cell_type": "markdown",
194
+ "metadata": {},
195
+ "source": [
196
+ "## Run inference\n",
197
+ "\n",
198
+ "YEL's velocity output is normalized as `U/Uref`; the code multiplies velocity channels by `REFERENCE_WIND_SPEED`. If the model has extra output channels, the Eddy3D convention is: pedestrian velocity, pedestrian TKE, roof velocity, roof TKE."
199
+ ]
200
+ },
201
+ {
202
+ "cell_type": "code",
203
+ "execution_count": null,
204
+ "metadata": {},
205
+ "outputs": [],
206
+ "source": [
207
+ "CHANNEL_NAMES = [\"wind_speed_m_s\", \"tke_m2_s2\", \"roof_wind_speed_m_s\", \"roof_tke_m2_s2\"]\n",
208
+ "\n",
209
+ "\n",
210
+ "def run_direction(direction_deg):\n",
211
+ " model_input = tensor_for_direction(base_tensor, grid_x, grid_y, valid_mask, direction_deg)\n",
212
+ " output = session.run(None, {input_meta.name: model_input})[0]\n",
213
+ " if output.ndim != 4:\n",
214
+ " raise ValueError(f\"Expected a 4D model output, received shape {output.shape}\")\n",
215
+ "\n",
216
+ " result = data[[\"x\", \"y\"]].copy()\n",
217
+ " result[\"wind_direction_deg\"] = direction_deg\n",
218
+ " for channel in range(min(output.shape[1], len(CHANNEL_NAMES))):\n",
219
+ " values = np.zeros(len(data), dtype=np.float32)\n",
220
+ " values[valid_mask] = output[0, channel, grid_y[valid_mask], grid_x[valid_mask]]\n",
221
+ " if channel in (0, 2):\n",
222
+ " values *= REFERENCE_WIND_SPEED\n",
223
+ " result[CHANNEL_NAMES[channel]] = np.maximum(values, 0.0)\n",
224
+ " return result\n",
225
+ "\n",
226
+ "\n",
227
+ "predictions = pd.concat(\n",
228
+ " [run_direction(direction) for direction in WIND_DIRECTIONS_DEG],\n",
229
+ " ignore_index=True,\n",
230
+ ")\n",
231
+ "predictions.head()"
232
+ ]
233
+ },
234
+ {
235
+ "cell_type": "markdown",
236
+ "metadata": {},
237
+ "source": [
238
+ "## Plot and export results"
239
+ ]
240
+ },
241
+ {
242
+ "cell_type": "code",
243
+ "execution_count": null,
244
+ "metadata": {},
245
+ "outputs": [],
246
+ "source": [
247
+ "directions = list(dict.fromkeys(predictions[\"wind_direction_deg\"]))\n",
248
+ "fig, axes = plt.subplots(\n",
249
+ " 1, len(directions), figsize=(5 * len(directions), 4), squeeze=False, constrained_layout=True\n",
250
+ ")\n",
251
+ "\n",
252
+ "vmax = float(predictions[\"wind_speed_m_s\"].max()) or 1.0\n",
253
+ "for axis, direction in zip(axes.ravel(), directions):\n",
254
+ " view = predictions[predictions[\"wind_direction_deg\"] == direction]\n",
255
+ " artist = axis.scatter(\n",
256
+ " view[\"x\"], view[\"y\"], c=view[\"wind_speed_m_s\"], s=8,\n",
257
+ " cmap=\"turbo\", vmin=0, vmax=vmax\n",
258
+ " )\n",
259
+ " axis.set(title=f\"Wind from {direction:g}°\", xlabel=\"X (m)\", ylabel=\"Y (m)\", aspect=\"equal\")\n",
260
+ " fig.colorbar(artist, ax=axis, label=\"Wind speed (m/s)\")\n",
261
+ "\n",
262
+ "plt.show()\n",
263
+ "predictions.to_csv(OUTPUT_CSV, index=False)\n",
264
+ "print(f\"Saved {len(predictions):,} predictions to {OUTPUT_CSV.resolve()}\")"
265
+ ]
266
+ },
267
+ {
268
+ "cell_type": "markdown",
269
+ "metadata": {},
270
+ "source": [
271
+ "## Optional: faster hardware\n",
272
+ "\n",
273
+ "The notebook defaults to CPU for portability. On Windows with a supported GPU, install `onnxruntime-directml` instead of `onnxruntime` and create the session with `providers=[\"DmlExecutionProvider\", \"CPUExecutionProvider\"]`. For NVIDIA CUDA, use the matching `onnxruntime-gpu` package and `CUDAExecutionProvider`."
274
+ ]
275
+ }
276
+ ],
277
+ "metadata": {
278
+ "kernelspec": {
279
+ "display_name": "Python 3",
280
+ "language": "python",
281
+ "name": "python3"
282
+ },
283
+ "language_info": {
284
+ "name": "python",
285
+ "version": "3.11"
286
+ }
287
+ },
288
+ "nbformat": 4,
289
+ "nbformat_minor": 5
290
+ }