|
5 | 5 | "cell_type": "markdown", |
6 | 6 | "metadata": {}, |
7 | 7 | "source": [ |
8 | | - "# Spherical grids and UnitConversion\n" |
| 8 | + "# Spherical grids and unit converters\n" |
9 | 9 | ] |
10 | 10 | }, |
11 | 11 | { |
|
15 | 15 | "source": [ |
16 | 16 | "In most applications, Parcels works with `spherical` meshes, where longitude and latitude are given in degrees, while depth is given in meters. But it is also possible to use `flat` meshes, where longitude and latitude are given in meters (note that the dimensions are then still called `longitude` and `latitude` for consistency reasons).\n", |
17 | 17 | "\n", |
18 | | - "In all cases, velocities are given in m/s. So Parcels seamlessly converts between meters and degrees, under the hood. For transparency, this tutorial explain how this works.\n" |
| 18 | + "In all cases, velocities are given in m/s. Parcels seamlessly converts between meters and degrees, under the hood. For transparency, this guide explains how this works.\n" |
19 | 19 | ] |
20 | 20 | }, |
21 | 21 | { |
22 | 22 | "attachments": {}, |
23 | 23 | "cell_type": "markdown", |
24 | 24 | "metadata": {}, |
25 | 25 | "source": [ |
26 | | - "Let's first import the relevant modules, and create dictionaries for the `U`, `V` and `temp` 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 and the temperature 20C.\n" |
27 | 27 | ] |
28 | 28 | }, |
29 | 29 | { |
|
34 | 34 | "source": [ |
35 | 35 | "import matplotlib.pyplot as plt\n", |
36 | 36 | "import numpy as np\n", |
| 37 | + "import xarray as xr\n", |
37 | 38 | "\n", |
38 | 39 | "import parcels" |
39 | 40 | ] |
|
44 | 45 | "metadata": {}, |
45 | 46 | "outputs": [], |
46 | 47 | "source": [ |
47 | | - "xdim, ydim = (10, 20)\n", |
48 | | - "data = {\n", |
49 | | - " \"U\": np.ones((ydim, xdim), dtype=np.float32),\n", |
50 | | - " \"V\": np.ones((ydim, xdim), dtype=np.float32),\n", |
51 | | - " \"temp\": 20 * np.ones((ydim, xdim), dtype=np.float32),\n", |
52 | | - "}\n", |
53 | | - "dims = {\n", |
54 | | - " \"lon\": np.linspace(-15, 5, xdim, dtype=np.float32),\n", |
55 | | - " \"lat\": np.linspace(35, 60, ydim, dtype=np.float32),\n", |
56 | | - "}" |
| 48 | + "from parcels._datasets.structured.generated import simple_UV_dataset\n", |
| 49 | + "\n", |
| 50 | + "nlat = 10\n", |
| 51 | + "nlon = 18\n", |
| 52 | + "ds = simple_UV_dataset(dims=(1, 1, nlat, nlon), mesh=\"spherical\").isel(time=0, depth=0)\n", |
| 53 | + "ds[\"temperature\"] = ds[\"U\"] + 20 # add temperature field of 20 deg\n", |
| 54 | + "ds[\"U\"].data[:] = 1.0 # set U to 1 m/s\n", |
| 55 | + "ds[\"V\"].data[:] = 1.0 # set V to 1 m/s\n", |
| 56 | + "ds" |
57 | 57 | ] |
58 | 58 | }, |
59 | 59 | { |
60 | 60 | "attachments": {}, |
61 | 61 | "cell_type": "markdown", |
62 | 62 | "metadata": {}, |
63 | 63 | "source": [ |
64 | | - "We can convert these data and dims to a FieldSet object using `FieldSet.from_data`. We add the argument `mesh='spherical'` (this is the default option) to signal that all longitudes and latitudes are in degrees.\n", |
| 64 | + "To create a `parcels.FieldSet` object, we define the `parcels.Field`s and the structured grid (`parcels.XGrid`) the fields are defined on. We add the argument `mesh='spherical'` to the `parcels.XGrid` to signal that all longitudes and latitudes are in degrees.\n", |
| 65 | + "\n", |
| 66 | + "```{note}\n", |
| 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 | + "```\n", |
65 | 69 | "\n", |
66 | | - "Plotting the `U` field indeed shows a uniform 1m/s eastward flow.\n" |
| 70 | + "Plotting the `U` field indeed shows a uniform 1 m/s eastward flow.\n" |
67 | 71 | ] |
68 | 72 | }, |
69 | 73 | { |
|
72 | 76 | "metadata": {}, |
73 | 77 | "outputs": [], |
74 | 78 | "source": [ |
75 | | - "fieldset = parcels.FieldSet.from_data(data, dims, mesh=\"spherical\")\n", |
76 | | - "plt.pcolormesh(fieldset.U.lon, fieldset.U.lat, fieldset.U.data[0, :, :], vmin=0, vmax=1)\n", |
| 79 | + "grid = parcels.XGrid.from_dataset(ds, mesh=\"spherical\")\n", |
| 80 | + "U = parcels.Field(\"U\", ds[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n", |
| 81 | + "V = parcels.Field(\"V\", ds[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n", |
| 82 | + "UV = parcels.VectorField(\"UV\", U, V)\n", |
| 83 | + "temperature = parcels.Field(\n", |
| 84 | + " \"temperature\", ds[\"temperature\"], grid, interp_method=parcels.interpolators.XLinear\n", |
| 85 | + ")\n", |
| 86 | + "fieldset = parcels.FieldSet([U, V, UV, temperature])\n", |
| 87 | + "\n", |
| 88 | + "plt.pcolormesh(\n", |
| 89 | + " fieldset.U.grid.lon,\n", |
| 90 | + " fieldset.U.grid.lat,\n", |
| 91 | + " fieldset.U.data[0, 0, :, :],\n", |
| 92 | + " vmin=0,\n", |
| 93 | + " vmax=1,\n", |
| 94 | + " shading=\"gouraud\",\n", |
| 95 | + ")\n", |
| 96 | + "plt.ylabel(\"Latitude\")\n", |
| 97 | + "plt.xlabel(\"Longitude\")\n", |
77 | 98 | "plt.colorbar()\n", |
78 | 99 | "plt.show()" |
79 | 100 | ] |
|
92 | 113 | "metadata": {}, |
93 | 114 | "outputs": [], |
94 | 115 | "source": [ |
95 | | - "print(fieldset.UV[0, 0, 40, -5])\n", |
96 | | - "print(fieldset.temp[0, 0, 40, -5])" |
| 116 | + "time = np.array([0])\n", |
| 117 | + "z = np.array([0])\n", |
| 118 | + "lat = np.array([40])\n", |
| 119 | + "lon = np.array([-5])\n", |
| 120 | + "print(fieldset.UV[time, z, lat, lon])\n", |
| 121 | + "print(fieldset.temperature[time, z, lat, lon])" |
97 | 122 | ] |
98 | 123 | }, |
99 | 124 | { |
|
103 | 128 | "source": [ |
104 | 129 | "While the temperature field indeed is 20C, as we defined, these printed velocities are much smaller.\n", |
105 | 130 | "\n", |
106 | | - "This is because Parcels converts under the hood from m/s to degrees/s. This conversion is done with a `UnitConverter` object, which is stored in the `.units` attribute of each Field. Below, we print these\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" |
107 | 132 | ] |
108 | 133 | }, |
109 | 134 | { |
|
112 | 137 | "metadata": {}, |
113 | 138 | "outputs": [], |
114 | 139 | "source": [ |
115 | | - "for fld in [fieldset.U, fieldset.V, fieldset.temp]:\n", |
| 140 | + "for fld in [fieldset.U, fieldset.V, fieldset.temperature]:\n", |
116 | 141 | " print(f\"{fld.name}: {fld.units}\")" |
117 | 142 | ] |
118 | 143 | }, |
|
121 | 146 | "cell_type": "markdown", |
122 | 147 | "metadata": {}, |
123 | 148 | "source": [ |
124 | | - "So the U field has a `GeographicPolar` UnitConverter object, the V field has a `Geographic` Unitconverter and the `temp` field has a `UnitConverter` object.\n", |
| 149 | + "So the U field has a `GeographicPolar` UnitConverter object, the V field has a `Geographic` UnitConverter and the `temp` field has a `UnitConverter` object.\n", |
125 | 150 | "\n", |
126 | 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" |
127 | 152 | ] |
|
132 | 157 | "metadata": {}, |
133 | 158 | "outputs": [], |
134 | 159 | "source": [ |
135 | | - "u, v = fieldset.UV[0, 0, 40, -5]\n", |
| 160 | + "u, v = fieldset.UV[time, z, lat, lon]\n", |
136 | 161 | "print(v * 1852 * 60)" |
137 | 162 | ] |
138 | 163 | }, |
|
150 | 175 | "metadata": {}, |
151 | 176 | "outputs": [], |
152 | 177 | "source": [ |
153 | | - "print(fieldset.UV.eval(0, 0, 40, -5, applyConversion=False))" |
| 178 | + "print(\n", |
| 179 | + " fieldset.UV.eval(\n", |
| 180 | + " time,\n", |
| 181 | + " z,\n", |
| 182 | + " lat,\n", |
| 183 | + " lon,\n", |
| 184 | + " applyConversion=False,\n", |
| 185 | + " )\n", |
| 186 | + ")" |
154 | 187 | ] |
155 | 188 | }, |
156 | 189 | { |
|
166 | 199 | "cell_type": "markdown", |
167 | 200 | "metadata": {}, |
168 | 201 | "source": [ |
169 | | - "If longitudes and latitudes are given in meters, rather than degrees, simply add `mesh='flat'` when creating the FieldSet object.\n" |
| 202 | + "If longitudes and latitudes are given in meters, rather than degrees, simply add `mesh='flat'` when creating the XGrid object.\n" |
170 | 203 | ] |
171 | 204 | }, |
172 | 205 | { |
|
175 | 208 | "metadata": {}, |
176 | 209 | "outputs": [], |
177 | 210 | "source": [ |
178 | | - "fieldset_flat = parcels.FieldSet.from_data(data, dims, mesh=\"flat\")\n", |
| 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", |
179 | 226 | "\n", |
180 | 227 | "plt.pcolormesh(\n", |
181 | | - " fieldset_flat.U.lon,\n", |
182 | | - " fieldset_flat.U.lat,\n", |
183 | | - " fieldset_flat.U.data[0, :, :],\n", |
| 228 | + " fieldset_flat.U.grid.lon,\n", |
| 229 | + " fieldset_flat.U.grid.lat,\n", |
| 230 | + " fieldset_flat.U.data[0, 0, :, :],\n", |
184 | 231 | " vmin=0,\n", |
185 | 232 | " vmax=1,\n", |
| 233 | + " shading=\"gouraud\",\n", |
186 | 234 | ")\n", |
187 | 235 | "plt.colorbar()\n", |
188 | 236 | "plt.show()\n", |
189 | 237 | "\n", |
190 | | - "print(\"Velocities:\", fieldset_flat.UV[0, 0, 40, -5])\n", |
191 | | - "for fld in [fieldset_flat.U, fieldset_flat.V, fieldset_flat.temp]:\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", |
192 | 243 | " print(f\"{fld.name}: {fld.units}\")" |
193 | 244 | ] |
194 | 245 | }, |
|
225 | 276 | "kh_zonal = 100 # in m^2/s\n", |
226 | 277 | "kh_meridional = 100 # in m^2/s\n", |
227 | 278 | "\n", |
228 | | - "fieldset.add_field(\n", |
229 | | - " parcels.Field(\n", |
230 | | - " \"Kh_zonal\",\n", |
231 | | - " kh_zonal * np.ones((ydim, xdim), dtype=np.float32),\n", |
232 | | - " grid=fieldset.U.grid,\n", |
233 | | - " )\n", |
| 279 | + "ds[\"Kh_zonal\"] = xr.DataArray(\n", |
| 280 | + " data=kh_zonal * np.ones((nlat, nlon), dtype=np.float32), dims=[\"YG\", \"XG\"]\n", |
234 | 281 | ")\n", |
235 | | - "fieldset.add_field(\n", |
236 | | - " parcels.Field(\n", |
237 | | - " \"Kh_meridional\",\n", |
238 | | - " kh_meridional * np.ones((ydim, xdim), dtype=np.float32),\n", |
239 | | - " grid=fieldset.U.grid,\n", |
240 | | - " )\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", |
241 | 294 | ")\n", |
242 | 295 | "\n", |
| 296 | + "kh_meridional_field = parcels.Field(\n", |
| 297 | + " \"Kh_meridional\",\n", |
| 298 | + " ds[\"Kh_meridional\"],\n", |
| 299 | + " grid=grid,\n", |
| 300 | + " interp_method=parcels.interpolators.XLinear,\n", |
| 301 | + ")\n", |
| 302 | + "\n", |
| 303 | + "fieldset.add_field(kh_meridional_field)\n", |
| 304 | + "\n", |
243 | 305 | "for fld in [fieldset.Kh_zonal, fieldset.Kh_meridional]:\n", |
244 | | - " print(f\"{fld.name}: {fld[0, 0, 40, -5]:e} {fld.units}\")" |
| 306 | + " val = fld[time, z, lat, lon]\n", |
| 307 | + " print(f\"{fld.name}: {val} {fld.units}\")" |
245 | 308 | ] |
246 | 309 | }, |
247 | 310 | { |
|
261 | 324 | "outputs": [], |
262 | 325 | "source": [ |
263 | 326 | "deg_to_m = 1852 * 60\n", |
264 | | - "print(fieldset.Kh_meridional[0, 0, 40, -5] * deg_to_m**2)" |
| 327 | + "print(fieldset.Kh_meridional[time, z, lat, lon] * deg_to_m**2)" |
265 | 328 | ] |
266 | 329 | }, |
267 | 330 | { |
|
281 | 344 | "\n", |
282 | 345 | "| Field name | Converter object | Conversion for `mesh='spherical'` | Conversion for `mesh='flat'` |\n", |
283 | 346 | "| ---------------- | ----------------------- | --------------------------------------------------------- | ---------------------------- |\n", |
284 | | - "| 'U' | `GeographicPolar` | $1852 \\cdot 60 \\cdot \\cos(lat \\cdot \\frac{\\pi}{180})$ | 1 |\n", |
285 | | - "| 'V' | `Geographic` | $1852 \\cdot 60$ | 1 |\n", |
286 | | - "| 'Kh_zonal' | `GeographicPolarSquare` | $(1852 \\cdot 60 \\cdot \\cos(lat \\cdot \\frac{\\pi}{180}))^2$ | 1 |\n", |
287 | | - "| 'Kh_meridional' | `GeographicSquare` | $(1852 \\cdot 60)^2$ | 1 |\n", |
288 | | - "| All other fields | `UnitConverter` | 1 | 1 |\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 | `UnitConverter` | 1 | 1 |\n", |
289 | 352 | "\n", |
290 | 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", |
291 | 354 | "\n", |
|
298 | 361 | "metadata": {}, |
299 | 362 | "outputs": [], |
300 | 363 | "source": [ |
| 364 | + "ds[\"Ustokes\"] = xr.DataArray(\n", |
| 365 | + " data=np.ones((nlat, nlon), dtype=np.float32), dims=[\"YG\", \"XG\"]\n", |
| 366 | + ")\n", |
| 367 | + "\n", |
301 | 368 | "fieldset.add_field(\n", |
302 | 369 | " parcels.Field(\n", |
303 | | - " \"Ustokes\", np.ones((ydim, xdim), dtype=np.float32), grid=fieldset.U.grid\n", |
| 370 | + " \"Ustokes\",\n", |
| 371 | + " ds[\"Ustokes\"],\n", |
| 372 | + " grid=fieldset.U.grid,\n", |
| 373 | + " interp_method=parcels.interpolators.XLinear,\n", |
304 | 374 | " )\n", |
305 | 375 | ")\n", |
306 | | - "print(fieldset.Ustokes[0, 0, 40, -5])" |
| 376 | + "print(fieldset.Ustokes[time, z, lat, lon])" |
307 | 377 | ] |
308 | 378 | }, |
309 | 379 | { |
|
320 | 390 | "metadata": {}, |
321 | 391 | "outputs": [], |
322 | 392 | "source": [ |
323 | | - "from parcels.tools.converters import GeographicPolar\n", |
324 | | - "\n", |
325 | | - "fieldset.Ustokes.units = GeographicPolar()\n", |
326 | | - "print(fieldset.Ustokes[0, 0, 40, -5])\n", |
327 | | - "print(fieldset.Ustokes[0, 0, 40, -5] * 1852 * 60 * np.cos(40 * np.pi / 180))" |
| 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))" |
328 | 396 | ] |
329 | 397 | } |
330 | 398 | ], |
331 | 399 | "metadata": { |
332 | 400 | "kernelspec": { |
333 | | - "display_name": "Python 3", |
| 401 | + "display_name": "test-notebooks", |
334 | 402 | "language": "python", |
335 | 403 | "name": "python3" |
336 | 404 | }, |
|
344 | 412 | "name": "python", |
345 | 413 | "nbconvert_exporter": "python", |
346 | 414 | "pygments_lexer": "ipython3", |
347 | | - "version": "3.11.6" |
| 415 | + "version": "3.11.0" |
348 | 416 | } |
349 | 417 | }, |
350 | 418 | "nbformat": 4, |
|
0 commit comments