|
5 | 5 | "cell_type": "markdown", |
6 | 6 | "metadata": {}, |
7 | 7 | "source": [ |
8 | | - "# 🖥️ Spherical grids and unit converters" |
| 8 | + "# 🖥️ Spherical grids and velocity conversion" |
9 | 9 | ] |
10 | 10 | }, |
11 | 11 | { |
|
23 | 23 | "cell_type": "markdown", |
24 | 24 | "metadata": {}, |
25 | 25 | "source": [ |
26 | | - "Let's first import the relevant modules, and generate a simple dataset on a 2D spherical mesh, with `U`, `V` and `temperature` data arrays, with the velocities 1 m/s and the temperature 20C.\n" |
| 26 | + "Let's first import the relevant modules, and generate a simple dataset on a 2D spherical mesh, with `U`, `V` and `temperature` data arrays, with the velocities 1 m s<sup>-1</sup> and the temperature 20°C.\n" |
27 | 27 | ] |
28 | 28 | }, |
29 | 29 | { |
|
67 | 67 | "When using a `FieldSet` method for a specific dataset, such as `from_copernicusmarine()`, the grid information is known and parsed by Parcels, so we do not have to add the `mesh` argument.\n", |
68 | 68 | "```\n", |
69 | 69 | "\n", |
70 | | - "Plotting the `U` field indeed shows a uniform 1 m/s eastward flow.\n" |
| 70 | + "Plotting the `U` field indeed shows a uniform 1 m s<sup>-1</sup> eastward flow.\n" |
71 | 71 | ] |
72 | 72 | }, |
73 | 73 | { |
|
104 | 104 | "cell_type": "markdown", |
105 | 105 | "metadata": {}, |
106 | 106 | "source": [ |
107 | | - "However, printing the velocites directly shows something perhaps surprising. Here, we use the square-bracket field-interpolation notation to print the field value at (5W, 40N, 0m depth) at time 0. _Note that sampling a velocity in Parcels is done by calling the `fieldset.UV` VectorField; see the [Field Sampling tutorial](https://docs.oceanparcels.org/en/latest/examples/tutorial_sampling.html#Sampling-velocity-fields) for more information._\n" |
| 107 | + "However, printing the velocites directly shows something perhaps surprising. Here, we use the square-bracket field-interpolation notation to print the field value at (5W, 40N, 0m depth) at time 0. \n", |
| 108 | + "\n", |
| 109 | + "```{note}\n", |
| 110 | + "Sampling a velocity in Parcels is done by calling the `fieldset.UV` VectorField; see also the section \"Sampling U and V separately\" below.\n", |
| 111 | + "```\n" |
108 | 112 | ] |
109 | 113 | }, |
110 | 114 | { |
|
117 | 121 | "z = np.array([0])\n", |
118 | 122 | "lat = np.array([40])\n", |
119 | 123 | "lon = np.array([-5])\n", |
120 | | - "print(fieldset.UV[time, z, lat, lon])\n", |
121 | | - "print(fieldset.temperature[time, z, lat, lon])" |
| 124 | + "u, v = fieldset.UV[time, z, lat, lon]\n", |
| 125 | + "temp = fieldset.temperature[time, z, lat, lon]\n", |
| 126 | + "\n", |
| 127 | + "print(f\"(u, v) = ({u}, {v})\")\n", |
| 128 | + "print(f\"temperature = {temp}\")" |
122 | 129 | ] |
123 | 130 | }, |
124 | 131 | { |
125 | 132 | "attachments": {}, |
126 | 133 | "cell_type": "markdown", |
127 | 134 | "metadata": {}, |
128 | 135 | "source": [ |
129 | | - "While the temperature field indeed is 20C, as we defined, these printed velocities are much smaller.\n", |
| 136 | + "While the temperature field indeed is 20°C, as we defined, these printed velocities are much smaller.\n", |
130 | 137 | "\n", |
131 | | - "This is because Parcels converts under the hood from m/s to degrees/s. This conversion is done with a `parcels.converters` object, which is stored in the `.units` attribute of each Field. Below, we print these\n" |
132 | | - ] |
133 | | - }, |
134 | | - { |
135 | | - "cell_type": "code", |
136 | | - "execution_count": null, |
137 | | - "metadata": {}, |
138 | | - "outputs": [], |
139 | | - "source": [ |
140 | | - "for fld in [fieldset.U, fieldset.V, fieldset.temperature]:\n", |
141 | | - " print(f\"{fld.name}: {fld.units}\")" |
| 138 | + "This is because Parcels converts under the hood from m s<sup>-1</sup> to degrees s<sup>-1</sup>." |
142 | 139 | ] |
143 | 140 | }, |
144 | 141 | { |
145 | 142 | "attachments": {}, |
146 | 143 | "cell_type": "markdown", |
147 | 144 | "metadata": {}, |
148 | 145 | "source": [ |
149 | | - "So the U field has a `GeographicPolar` UnitConverter object, the V field has a `Geographic` UnitConverter and the `temp` field has a `Unity` object.\n", |
150 | | - "\n", |
151 | | - "Indeed, if we multiply the value of the V field with 1852 \\* 60 (the number of meters in 1 degree of latitude), we get the expected 1 m/s.\n" |
| 146 | + "Indeed, if we multiply the value of the U field with 1852 \\* 60 \\* cos(lat) (the number of meters in 1 degree of longitude), we get the expected 1 m s<sup>-1</sup>.\n" |
152 | 147 | ] |
153 | 148 | }, |
154 | 149 | { |
|
158 | 153 | "outputs": [], |
159 | 154 | "source": [ |
160 | 155 | "u, v = fieldset.UV[time, z, lat, lon]\n", |
161 | | - "print(v * 1852 * 60)" |
| 156 | + "\n", |
| 157 | + "u = u * 1852 * 60 * np.cos(np.deg2rad(lat))\n", |
| 158 | + "v = v * 1852 * 60 * np.cos(np.deg2rad(lat))\n", |
| 159 | + "print(f\"(u, v) = ({u}, {v})\")\n", |
| 160 | + "\n", |
| 161 | + "assert np.isclose(u, 1.0)\n", |
| 162 | + "assert np.isclose(v, 1.0)" |
| 163 | + ] |
| 164 | + }, |
| 165 | + { |
| 166 | + "cell_type": "markdown", |
| 167 | + "metadata": {}, |
| 168 | + "source": [ |
| 169 | + "```{note}\n", |
| 170 | + "It may be surprising that the conversion factor depends on latitude also for the V velocity component. This is because Parcels assumes a flux-based interpolation, so that the velocity components are defined on the faces of the grid cells, which vary in size with latitude.\n", |
| 171 | + "```" |
162 | 172 | ] |
163 | 173 | }, |
164 | 174 | { |
165 | 175 | "attachments": {}, |
166 | 176 | "cell_type": "markdown", |
167 | 177 | "metadata": {}, |
168 | 178 | "source": [ |
169 | | - "Note that you can also interpolate the Field without a unit conversion, by using the `eval()` method and setting `applyConversion=False`, as below\n" |
| 179 | + "You can also interpolate the Field to these values directly by using the `eval()` method and setting `applyConversion=False`, as below\n" |
170 | 180 | ] |
171 | 181 | }, |
172 | 182 | { |
|
187 | 197 | ] |
188 | 198 | }, |
189 | 199 | { |
190 | | - "attachments": {}, |
191 | | - "cell_type": "markdown", |
192 | | - "metadata": {}, |
193 | | - "source": [ |
194 | | - "## UnitConverters for `mesh='flat'`\n" |
195 | | - ] |
196 | | - }, |
197 | | - { |
198 | | - "attachments": {}, |
199 | | - "cell_type": "markdown", |
200 | | - "metadata": {}, |
201 | | - "source": [ |
202 | | - "If longitudes and latitudes are given in meters, rather than degrees, simply add `mesh='flat'` when creating the XGrid object.\n" |
203 | | - ] |
204 | | - }, |
205 | | - { |
206 | | - "cell_type": "code", |
207 | | - "execution_count": null, |
208 | | - "metadata": {}, |
209 | | - "outputs": [], |
210 | | - "source": [ |
211 | | - "ds_flat = simple_UV_dataset(dims=(1, 1, nlat, nlon), mesh=\"flat\").isel(time=0, depth=0)\n", |
212 | | - "ds_flat[\"temperature\"] = ds_flat[\"U\"] + 20 # add temperature field of 20 deg\n", |
213 | | - "ds_flat[\"U\"].data[:] = 1.0 # set U to 1 m/s\n", |
214 | | - "ds_flat[\"V\"].data[:] = 1.0 # set V to 1 m/s\n", |
215 | | - "grid = parcels.XGrid.from_dataset(ds_flat, mesh=\"flat\")\n", |
216 | | - "U = parcels.Field(\"U\", ds_flat[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n", |
217 | | - "V = parcels.Field(\"V\", ds_flat[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n", |
218 | | - "UV = parcels.VectorField(\"UV\", U, V)\n", |
219 | | - "temperature = parcels.Field(\n", |
220 | | - " \"temperature\",\n", |
221 | | - " ds_flat[\"temperature\"],\n", |
222 | | - " grid,\n", |
223 | | - " interp_method=parcels.interpolators.XLinear,\n", |
224 | | - ")\n", |
225 | | - "fieldset_flat = parcels.FieldSet([U, V, UV, temperature])\n", |
226 | | - "\n", |
227 | | - "plt.pcolormesh(\n", |
228 | | - " fieldset_flat.U.grid.lon,\n", |
229 | | - " fieldset_flat.U.grid.lat,\n", |
230 | | - " fieldset_flat.U.data[0, 0, :, :],\n", |
231 | | - " vmin=0,\n", |
232 | | - " vmax=1,\n", |
233 | | - " shading=\"gouraud\",\n", |
234 | | - ")\n", |
235 | | - "plt.colorbar()\n", |
236 | | - "plt.show()\n", |
237 | | - "\n", |
238 | | - "print(\n", |
239 | | - " \"Velocities:\",\n", |
240 | | - " fieldset_flat.UV[time, z, lat, lon],\n", |
241 | | - ")\n", |
242 | | - "for fld in [fieldset_flat.U, fieldset_flat.V, fieldset_flat.temperature]:\n", |
243 | | - " print(f\"{fld.name}: {fld.units}\")" |
244 | | - ] |
245 | | - }, |
246 | | - { |
247 | | - "attachments": {}, |
248 | | - "cell_type": "markdown", |
249 | | - "metadata": {}, |
250 | | - "source": [ |
251 | | - "Indeed, in this case all Fields have the same default `Unity` object.\n" |
252 | | - ] |
253 | | - }, |
254 | | - { |
255 | | - "attachments": {}, |
256 | | - "cell_type": "markdown", |
257 | | - "metadata": {}, |
258 | | - "source": [ |
259 | | - "## UnitConverters for Diffusion fields\n" |
260 | | - ] |
261 | | - }, |
262 | | - { |
263 | | - "attachments": {}, |
264 | 200 | "cell_type": "markdown", |
265 | 201 | "metadata": {}, |
266 | 202 | "source": [ |
267 | | - "The units for Brownian diffusion are in $m^2/s$. If (and only if!) the diffusion fields are called \"Kh_zonal\" and \"Kh_meridional\", Parcels will automatically assign the correct Unitconverter objects to these fields.\n" |
268 | | - ] |
269 | | - }, |
270 | | - { |
271 | | - "cell_type": "code", |
272 | | - "execution_count": null, |
273 | | - "metadata": {}, |
274 | | - "outputs": [], |
275 | | - "source": [ |
276 | | - "kh_zonal = 100 # in m^2/s\n", |
277 | | - "kh_meridional = 100 # in m^2/s\n", |
278 | | - "\n", |
279 | | - "ds[\"Kh_zonal\"] = xr.DataArray(\n", |
280 | | - " data=kh_zonal * np.ones((nlat, nlon), dtype=np.float32), dims=[\"YG\", \"XG\"]\n", |
281 | | - ")\n", |
282 | | - "\n", |
283 | | - "kh_zonal_field = parcels.Field(\n", |
284 | | - " \"Kh_zonal\",\n", |
285 | | - " ds[\"Kh_zonal\"],\n", |
286 | | - " grid=fieldset.U.grid,\n", |
287 | | - " interp_method=parcels.interpolators.XLinear,\n", |
288 | | - ")\n", |
289 | | - "\n", |
290 | | - "fieldset.add_field(kh_zonal_field)\n", |
291 | | - "\n", |
292 | | - "ds[\"Kh_meridional\"] = xr.DataArray(\n", |
293 | | - " data=kh_meridional * np.ones((nlat, nlon), dtype=np.float32), dims=[\"YG\", \"XG\"]\n", |
294 | | - ")\n", |
295 | | - "\n", |
296 | | - "kh_meridional_field = parcels.Field(\n", |
297 | | - " \"Kh_meridional\",\n", |
298 | | - " ds[\"Kh_meridional\"],\n", |
299 | | - " grid=fieldset.U.grid,\n", |
300 | | - " interp_method=parcels.interpolators.XLinear,\n", |
301 | | - ")\n", |
302 | | - "\n", |
303 | | - "fieldset.add_field(kh_meridional_field)\n", |
304 | | - "\n", |
305 | | - "for fld in [fieldset.Kh_zonal, fieldset.Kh_meridional]:\n", |
306 | | - " val = fld[time, z, lat, lon]\n", |
307 | | - " print(f\"{fld.name}: {val} {fld.units}\")" |
| 203 | + "## Don't sample U and V separately" |
308 | 204 | ] |
309 | 205 | }, |
310 | 206 | { |
311 | | - "attachments": {}, |
312 | 207 | "cell_type": "markdown", |
313 | 208 | "metadata": {}, |
314 | 209 | "source": [ |
315 | | - "Here, the unitconverters are `GeographicPolarSquare` and `GeographicSquare`, respectively.\n", |
316 | | - "\n", |
317 | | - "Indeed, multiplying with $(1852\\cdot60)^2$ returns the original value\n" |
| 210 | + "Sampling `U` and `V` separately will _not_ convert to degrees s<sup>-1</sup>, so these velocities cannot be used directly for advection on spherical coordinates. This is one of the main reasons to always use the `UV` VectorField for velocity sampling in Parcels.\n" |
318 | 211 | ] |
319 | 212 | }, |
320 | 213 | { |
|
323 | 216 | "metadata": {}, |
324 | 217 | "outputs": [], |
325 | 218 | "source": [ |
326 | | - "deg_to_m = 1852 * 60\n", |
327 | | - "print(fieldset.Kh_meridional[time, z, lat, lon] * deg_to_m**2)" |
328 | | - ] |
329 | | - }, |
330 | | - { |
331 | | - "attachments": {}, |
332 | | - "cell_type": "markdown", |
333 | | - "metadata": {}, |
334 | | - "source": [ |
335 | | - "## Adding a UnitConverter object to a Field\n" |
| 219 | + "for fld in [fieldset.U, fieldset.V]:\n", |
| 220 | + " print(f\"{fld.name}: {fld.eval(time, z, lat, lon)}\")" |
336 | 221 | ] |
337 | 222 | }, |
338 | 223 | { |
339 | | - "attachments": {}, |
340 | 224 | "cell_type": "markdown", |
341 | 225 | "metadata": {}, |
342 | 226 | "source": [ |
343 | | - "So, to summarise, here is a table with all the conversions\n", |
344 | | - "\n", |
345 | | - "| Field name | Converter object | Conversion for `mesh='spherical'` | Conversion for `mesh='flat'` |\n", |
346 | | - "| ---------------- | ----------------------- | --------------------------------------------------------- | ---------------------------- |\n", |
347 | | - "| `\"U\"` | `GeographicPolar` | $1852 \\cdot 60 \\cdot \\cos(lat \\cdot \\frac{\\pi}{180})$ | 1 |\n", |
348 | | - "| `\"V\"` | `Geographic` | $1852 \\cdot 60$ | 1 |\n", |
349 | | - "| `\"Kh_zonal\"` | `GeographicPolarSquare` | $(1852 \\cdot 60 \\cdot \\cos(lat \\cdot \\frac{\\pi}{180}))^2$ | 1 |\n", |
350 | | - "| `\"Kh_meridional\"` | `GeographicSquare` | $(1852 \\cdot 60)^2$ | 1 |\n", |
351 | | - "| All other fields | `Unity` | 1 | 1 |\n", |
| 227 | + "## Unit conversion for other fields such as diffusivity\n", |
352 | 228 | "\n", |
353 | | - "Only four Field names are recognised and assigned an automatic UnitConverter object. This means that things might go very wrong when e.g. a velocity field is not called `U` or `V`.\n", |
| 229 | + "For other fields such as diffusivity, Parcels does not apply similar unit conversions when using a `spherical` mesh. For example, if we define a diffusivity field with value 10 m<sup>2</sup> s<sup>-1</sup>, Parcels will not convert this to degrees<sup>2</sup> s<sup>-1</sup> under the hood. \n", |
354 | 230 | "\n", |
355 | | - "Fortunately, you can always add a UnitConverter later, as explained below:\n" |
356 | | - ] |
357 | | - }, |
358 | | - { |
359 | | - "cell_type": "code", |
360 | | - "execution_count": null, |
361 | | - "metadata": {}, |
362 | | - "outputs": [], |
363 | | - "source": [ |
364 | | - "ds[\"Ustokes\"] = xr.DataArray(\n", |
365 | | - " data=np.ones((nlat, nlon), dtype=np.float32), dims=[\"YG\", \"XG\"]\n", |
366 | | - ")\n", |
| 231 | + "If you want to work with diffusivity in degrees<sup>2</sup> s<sup>-1</sup> (for example to move particles using a random walk), you will have to convert this yourself in your kernel. \n", |
367 | 232 | "\n", |
368 | | - "fieldset.add_field(\n", |
369 | | - " parcels.Field(\n", |
370 | | - " \"Ustokes\",\n", |
371 | | - " ds[\"Ustokes\"],\n", |
372 | | - " grid=fieldset.U.grid,\n", |
373 | | - " interp_method=parcels.interpolators.XLinear,\n", |
374 | | - " )\n", |
375 | | - ")\n", |
376 | | - "print(fieldset.Ustokes[time, z, lat, lon])" |
377 | | - ] |
378 | | - }, |
379 | | - { |
380 | | - "attachments": {}, |
381 | | - "cell_type": "markdown", |
382 | | - "metadata": {}, |
383 | | - "source": [ |
384 | | - "This value for `Ustokes` of course is not as expected, since the mesh is spherical and hence this would mean 1 degree/s velocity. Assigning the correct `GeographicPolar` Unitconverter gives\n" |
385 | | - ] |
386 | | - }, |
387 | | - { |
388 | | - "cell_type": "code", |
389 | | - "execution_count": null, |
390 | | - "metadata": {}, |
391 | | - "outputs": [], |
392 | | - "source": [ |
393 | | - "fieldset.Ustokes.units = parcels.GeographicPolar()\n", |
394 | | - "print(fieldset.Ustokes[time, z, lat, lon])\n", |
395 | | - "print(fieldset.Ustokes[time, z, lat, lon] * 1852 * 60 * np.cos(40 * np.pi / 180))" |
| 233 | + "Note that for the built-in `DiffusionUniformKh`, `AdvectionDiffusionM1` and `AdvectionDiffusionEM`, the conversion is done automatically." |
396 | 234 | ] |
397 | 235 | } |
398 | 236 | ], |
|
0 commit comments