|
42 | 42 | "source": [ |
43 | 43 | "## Setting up the individual Grids and Fields\n", |
44 | 44 | "\n", |
45 | | - "Before we start, define a helper function to quickly set up a nested dataset" |
| 45 | + "First we define a helper function to quickly set up a nested dataset" |
46 | 46 | ] |
47 | 47 | }, |
48 | 48 | { |
|
69 | 69 | "id": "4", |
70 | 70 | "metadata": {}, |
71 | 71 | "source": [ |
72 | | - "First define a zonal and meridional velocity field defined on a high resolution mesh. Both the zonal and meridional velocity are uniform, with velocities of 1 m/s and 0 m/s respectively." |
| 72 | + "Now we create a zonal and meridional velocity field defined on a small grid. Both the zonal (1m/s) and meridional (0 m/s) velocity are uniform." |
73 | 73 | ] |
74 | 74 | }, |
75 | 75 | { |
|
92 | 92 | "id": "6", |
93 | 93 | "metadata": {}, |
94 | 94 | "source": [ |
95 | | - "Then define another set of zonal and meridional velocities on a slightly coarser grid. In this case, the meridional velocity is also 1 m/s." |
| 95 | + "Then, we define another set of zonal and meridional velocities on a slightly larger grid. In this case, the meridional velocity is also 1 m/s." |
96 | 96 | ] |
97 | 97 | }, |
98 | 98 | { |
|
115 | 115 | "id": "8", |
116 | 116 | "metadata": {}, |
117 | 117 | "source": [ |
118 | | - "Now define another velocity field on a coarser resolution grid. The zonal velocity is the same as for the finer resolution grid, but the meridional velocity is now a cosine as a function of longitude." |
| 118 | + "Finally, we define another set of velocities on an even larger grid. The zonal velocity is the same as for the smaller grids, but the meridional velocity is now a cosine as a function of longitude." |
119 | 119 | ] |
120 | 120 | }, |
121 | 121 | { |
|
139 | 139 | "id": "10", |
140 | 140 | "metadata": {}, |
141 | 141 | "source": [ |
142 | | - "Plot the velocity fields on top of each other, indicating the grid boundaries in red." |
| 142 | + "We plot the velocity fields on top of each other, indicating the grid boundaries in red." |
143 | 143 | ] |
144 | 144 | }, |
145 | 145 | { |
|
174 | 174 | "id": "12", |
175 | 175 | "metadata": {}, |
176 | 176 | "source": [ |
177 | | - "Note in the plot above that the dataset domains are rectangular, but the polygons that will later define the Nested grid boundaries aren't necessarily. So we can even use this method to subset parts of a Grid." |
| 177 | + "Note, as seen in the plot above, that the dataset domains in this case are rectangular, but the polygons that will later define the nested Grid boundaries don't have to be. So we can even use this method to subset parts of a Grid." |
178 | 178 | ] |
179 | 179 | }, |
180 | 180 | { |
|
186 | 186 | "\n", |
187 | 187 | "Now comes the important part: we need to create a Delaunay triangulation of the nested Grids, so that we can efficiently determine in which Grid a particle is located at any given time. We use the `triangle` package to perform the triangulation, and `shapely` to handle the geometric operations. \n", |
188 | 188 | "\n", |
189 | | - "Note that we need to keep the edges of the polygons in the triangulation, so we need a [constrained (PSLG) Delaunay triangulation](https://en.wikipedia.org/wiki/Constrained_Delaunay_triangulation).\n", |
| 189 | + "Note that we need to make sure that all the edges of the polygons are also edges of the triangulation. We do this by using a [constrained (PSLG) Delaunay triangulation](https://en.wikipedia.org/wiki/Constrained_Delaunay_triangulation).\n", |
190 | 190 | "\n", |
191 | 191 | "The result is a set of triangles covering the nested Grids, which we can use to determine in which Grid a particle is located at any given time. It is important that the list of polygons is ordered from smallest to largest Grid, so that triangles in overlapping areas are assigned to the correct Grid." |
192 | 192 | ] |
|
242 | 242 | " face_poly[ti] = ip\n", |
243 | 243 | " break\n", |
244 | 244 | "\n", |
245 | | - " return pts, tris, face_poly" |
| 245 | + " return pts, tris, face_poly\n", |
| 246 | + "\n", |
| 247 | + "\n", |
| 248 | + "points, face_tris, face_poly = constrained_triangulate_keep_edges(grid_polygons)" |
246 | 249 | ] |
247 | 250 | }, |
248 | 251 | { |
249 | 252 | "cell_type": "markdown", |
250 | 253 | "id": "15", |
251 | 254 | "metadata": {}, |
252 | 255 | "source": [ |
253 | | - "We can then run the triangulation and plot the resulting triangles to verify that they correctly cover the nested Grids." |
| 256 | + "We can then plot the resulting triangles to verify that they correctly cover the nested Grids." |
254 | 257 | ] |
255 | 258 | }, |
256 | 259 | { |
|
260 | 263 | "metadata": {}, |
261 | 264 | "outputs": [], |
262 | 265 | "source": [ |
263 | | - "points, face_tris, face_poly = constrained_triangulate_keep_edges(grid_polygons)\n", |
264 | | - "\n", |
265 | 266 | "fig, ax = plt.subplots()\n", |
266 | 267 | "for i in range(n_grids)[::-1]:\n", |
267 | 268 | " tris = face_tris[face_poly == i]\n", |
268 | | - " ax.triplot(points[:, 0], points[:, 1], tris, label=f\"Nest {i}\", color=f\"C{i}\")\n", |
| 269 | + " ax.triplot(points[:, 0], points[:, 1], tris, label=f\"Grid {i}\", color=f\"C{i}\")\n", |
269 | 270 | "ax.scatter(points[:, 0], points[:, 1], s=10, c=\"k\")\n", |
270 | 271 | "ax.set_aspect(\"equal\")\n", |
271 | 272 | "ax.legend()\n", |
|
281 | 282 | "id": "17", |
282 | 283 | "metadata": {}, |
283 | 284 | "source": [ |
284 | | - "Then, we convert the triangulation into a Parcels FieldSet using `Parcels.UxGrid()`." |
| 285 | + "Then, we convert the triangulation into an (unstructured) `parcels.FieldSet`." |
285 | 286 | ] |
286 | 287 | }, |
287 | 288 | { |
|
309 | 310 | " ),\n", |
310 | 311 | " face_poly[np.newaxis, np.newaxis, :],\n", |
311 | 312 | " {\n", |
312 | | - " \"long_name\": \"Nested grid ID\",\n", |
| 313 | + " \"long_name\": \"Grid ID\",\n", |
313 | 314 | " \"location\": \"face\",\n", |
314 | 315 | " \"mesh\": \"delaunay\",\n", |
315 | 316 | " },\n", |
|
326 | 327 | "\n", |
327 | 328 | "uxda = ux.UxDataArray(ds_tri[\"face_polygon\"], uxgrid=ux.Grid(ds_tri))\n", |
328 | 329 | "\n", |
329 | | - "NestID = parcels.Field(\n", |
330 | | - " \"NestID\",\n", |
| 330 | + "GridID = parcels.Field(\n", |
| 331 | + " \"GridID\",\n", |
331 | 332 | " uxda,\n", |
332 | 333 | " # TODO note that here we need to use mesh=\"flat\" otherwise the hashing doesn't work. See https://github.com/Parcels-code/Parcels/pull/2439#discussion_r2627664010\n", |
333 | 334 | " parcels.UxGrid(uxda.uxgrid, z=uxda[\"nz\"], mesh=\"flat\"),\n", |
334 | 335 | " interp_method=parcels.interpolators.UxPiecewiseConstantFace,\n", |
335 | 336 | ")\n", |
336 | | - "fieldset = parcels.FieldSet([NestID])" |
| 337 | + "fieldset = parcels.FieldSet([GridID])" |
337 | 338 | ] |
338 | 339 | }, |
339 | 340 | { |
340 | 341 | "cell_type": "markdown", |
341 | 342 | "id": "19", |
342 | 343 | "metadata": {}, |
343 | 344 | "source": [ |
344 | | - "We can confirm that the FieldSet has been created correctly by running a Parcels simulation where particles sample the `NestID` field, which indicates in which nest each particle is located at any given time." |
| 345 | + "We can confirm that the FieldSet has been created correctly by running a Parcels simulation where particles sample the `GridID` field, which indicates in which Grid each particle is located at any given time." |
345 | 346 | ] |
346 | 347 | }, |
347 | 348 | { |
|
356 | 357 | " np.linspace(-18, 38, 20),\n", |
357 | 358 | ")\n", |
358 | 359 | "\n", |
359 | | - "NestParticle = parcels.Particle.add_variable(parcels.Variable(\"nestID\", dtype=np.int32))\n", |
| 360 | + "NestedGridParticle = parcels.Particle.add_variable(\n", |
| 361 | + " parcels.Variable(\"gridID\", dtype=np.int32)\n", |
| 362 | + ")\n", |
360 | 363 | "pset = parcels.ParticleSet(\n", |
361 | | - " fieldset, pclass=NestParticle, lon=X.flatten(), lat=Y.flatten()\n", |
| 364 | + " fieldset, pclass=NestedGridParticle, lon=X.flatten(), lat=Y.flatten()\n", |
362 | 365 | ")\n", |
363 | 366 | "\n", |
364 | 367 | "\n", |
365 | | - "def SampleNestID(particles, fieldset):\n", |
366 | | - " particles.nestID = fieldset.NestID[particles]\n", |
| 368 | + "def SampleGridID(particles, fieldset):\n", |
| 369 | + " particles.gridID = fieldset.GridID[particles]\n", |
367 | 370 | "\n", |
368 | 371 | "\n", |
369 | | - "pset.execute(SampleNestID, runtime=np.timedelta64(1, \"s\"), dt=np.timedelta64(1, \"s\"))" |
| 372 | + "pset.execute(\n", |
| 373 | + " SampleGridID,\n", |
| 374 | + " runtime=np.timedelta64(1, \"s\"),\n", |
| 375 | + " dt=np.timedelta64(1, \"s\"),\n", |
| 376 | + " verbose_progress=False,\n", |
| 377 | + ")" |
370 | 378 | ] |
371 | 379 | }, |
372 | 380 | { |
373 | 381 | "cell_type": "markdown", |
374 | 382 | "id": "21", |
375 | 383 | "metadata": {}, |
376 | 384 | "source": [ |
377 | | - "Indeed, the visualisation below shows that particles correctly identify the nest they are in based on their location." |
| 385 | + "Indeed, the visualisation below shows that particles correctly identify the grid they are in based on their location." |
378 | 386 | ] |
379 | 387 | }, |
380 | 388 | { |
|
385 | 393 | "outputs": [], |
386 | 394 | "source": [ |
387 | 395 | "fig, ax = plt.subplots(1, 1, figsize=(10, 5))\n", |
| 396 | + "plot_args = {\"cmap\": cmap, \"edgecolors\": \"k\", \"linewidth\": 0.5}\n", |
388 | 397 | "\n", |
389 | 398 | "triang = mtri.Triangulation(\n", |
390 | 399 | " uxda.uxgrid.node_lon.values,\n", |
391 | 400 | " uxda.uxgrid.node_lat.values,\n", |
392 | 401 | " triangles=uxda.uxgrid.face_node_connectivity.values,\n", |
393 | 402 | ")\n", |
| 403 | + "facecolors = np.squeeze(uxda[0, :].values)\n", |
394 | 404 | "\n", |
395 | | - "plot_args = {\n", |
396 | | - " \"cmap\": cmap,\n", |
397 | | - " \"edgecolors\": \"k\",\n", |
398 | | - " \"linewidth\": 0.5,\n", |
399 | | - " \"vmin\": 0,\n", |
400 | | - " \"vmax\": 2.0,\n", |
401 | | - "}\n", |
402 | | - "ax.tripcolor(\n", |
403 | | - " triang, facecolors=np.squeeze(uxda[0, :].values), shading=\"flat\", **plot_args\n", |
404 | | - ")\n", |
405 | | - "ax.scatter(pset.lon, pset.lat, c=pset.nestID, **plot_args)\n", |
| 405 | + "ax.tripcolor(triang, facecolors=facecolors, shading=\"flat\", **plot_args)\n", |
| 406 | + "ax.scatter(pset.lon, pset.lat, c=pset.gridID, **plot_args)\n", |
406 | 407 | "ax.set_aspect(\"equal\")\n", |
407 | | - "ax.set_title(\"Nesting visualisation (triangulation and interpolated particle values)\")\n", |
| 408 | + "ax.set_title(\n", |
| 409 | + " \"Nested Grids visualisation (triangulation and interpolated particle values)\"\n", |
| 410 | + ")\n", |
408 | 411 | "plt.tight_layout()\n", |
409 | 412 | "plt.show()" |
410 | 413 | ] |
|
414 | 417 | "id": "23", |
415 | 418 | "metadata": {}, |
416 | 419 | "source": [ |
417 | | - "## Advecting particles with nest transitions\n", |
| 420 | + "## Advecting particles with nested Grid transitions\n", |
418 | 421 | "\n", |
419 | | - "We can now set up a particle advection simulation using the nested grids. We first combine all the Fields into a single FieldSet. \n", |
| 422 | + "We can now set up a particle advection simulation using the nested Grids. We first combine all the Fields into a single FieldSet. \n", |
420 | 423 | "\n", |
421 | | - "We rename the individual Fields by appending the nest index to their names, so that we can easily identify which Field belongs to which nest. We also add the `NestID` Field to the FieldSet (note that Parcels v4 supports combining structured and unstructured Fields into one FieldSet, which is very convenient for this usecase)." |
| 424 | + "We rename the individual Fields by appending the Grid index to their names, so that we can easily identify which Field belongs to which Grid. We also add the `GridID` Field to the FieldSet (note that Parcels v4 supports combining structured and unstructured Fields into one FieldSet, which is very convenient for this usecase)." |
422 | 425 | ] |
423 | 426 | }, |
424 | 427 | { |
|
428 | 431 | "metadata": {}, |
429 | 432 | "outputs": [], |
430 | 433 | "source": [ |
431 | | - "fields = [NestID]\n", |
| 434 | + "fields = [GridID]\n", |
432 | 435 | "for i, ds in enumerate(ds_in):\n", |
433 | 436 | " # TODO : use FieldSet.from_sgrid_convetion here once #2437 is merged\n", |
434 | 437 | " grid = parcels.XGrid.from_dataset(ds, mesh=\"spherical\")\n", |
435 | 438 | " U = parcels.Field(\"U\", ds[\"U\"], grid, interp_method=parcels.interpolators.XLinear)\n", |
436 | 439 | " V = parcels.Field(\"V\", ds[\"V\"], grid, interp_method=parcels.interpolators.XLinear)\n", |
437 | 440 | " UV = parcels.VectorField(\"UV\", U, V)\n", |
438 | | - "\n", |
439 | 441 | " fset = parcels.FieldSet([U, V, UV])\n", |
440 | 442 | "\n", |
441 | 443 | " for fld in fset.fields.values():\n", |
|
449 | 451 | "id": "25", |
450 | 452 | "metadata": {}, |
451 | 453 | "source": [ |
452 | | - "We then define a custom Advection kernel that advects particles using the appropriate velocity Field based on the `NestID` at the particle's location. Note that for simplicity, we use Eulerian advection here, but in a real-world application you would typically use a higher-order scheme." |
| 454 | + "We then define a custom Advection kernel that advects particles using the appropriate velocity Field based on the `GridID` at the particle's location. Note that for simplicity, we use Eulerian advection here, but in a real-world application you would typically use a higher-order scheme." |
453 | 455 | ] |
454 | 456 | }, |
455 | 457 | { |
|
459 | 461 | "metadata": {}, |
460 | 462 | "outputs": [], |
461 | 463 | "source": [ |
462 | | - "def AdvectEE_Nests(particles, fieldset):\n", |
463 | | - " particles.nestID = fieldset.NestID[particles]\n", |
| 464 | + "def AdvectEE_NestedGrids(particles, fieldset):\n", |
| 465 | + " particles.gridID = fieldset.GridID[particles]\n", |
464 | 466 | "\n", |
465 | 467 | " # TODO because of KernelParticle bug (GH #2143), we need to copy lon/lat/time to local variables\n", |
466 | 468 | " time = particles.time\n", |
|
470 | 472 | " u = np.zeros_like(particles.lon)\n", |
471 | 473 | " v = np.zeros_like(particles.lat)\n", |
472 | 474 | "\n", |
473 | | - " unique_ids = np.unique(particles.nestID)\n", |
474 | | - " for nid in unique_ids:\n", |
475 | | - " mask = particles.nestID == nid\n", |
476 | | - " UVField = getattr(fieldset, f\"UV{nid}\")\n", |
| 475 | + " unique_ids = np.unique(particles.gridID)\n", |
| 476 | + " for gid in unique_ids:\n", |
| 477 | + " mask = particles.gridID == gid\n", |
| 478 | + " UVField = getattr(fieldset, f\"UV{gid}\")\n", |
477 | 479 | " (u[mask], v[mask]) = UVField[time[mask], z[mask], lat[mask], lon[mask]]\n", |
478 | 480 | "\n", |
479 | 481 | " particles.dlon += u * particles.dt\n", |
|
487 | 489 | " )\n", |
488 | 490 | "\n", |
489 | 491 | "\n", |
490 | | - "def DeleteErrorParticles(particles, fieldset):\n", |
491 | | - " any_error = particles.state >= 50 # This captures all Errors\n", |
492 | | - " particles[any_error].state = parcels.StatusCode.Delete\n", |
493 | | - "\n", |
494 | | - "\n", |
495 | 492 | "lat = np.linspace(-17, 35, 10)\n", |
496 | 493 | "lon = np.full(len(lat), -5)\n", |
497 | 494 | "\n", |
498 | | - "pset = parcels.ParticleSet(fieldset, pclass=NestParticle, lon=lon, lat=lat)\n", |
499 | | - "ofile = parcels.ParticleFile(\"nest_particles.zarr\", outputdt=np.timedelta64(1, \"D\"))\n", |
| 495 | + "pset = parcels.ParticleSet(fieldset, pclass=NestedGridParticle, lon=lon, lat=lat)\n", |
| 496 | + "ofile = parcels.ParticleFile(\n", |
| 497 | + " \"nestedgrid_particles.zarr\", outputdt=np.timedelta64(1, \"D\")\n", |
| 498 | + ")\n", |
500 | 499 | "pset.execute(\n", |
501 | | - " [AdvectEE_Nests, DeleteErrorParticles],\n", |
| 500 | + " AdvectEE_NestedGrids,\n", |
502 | 501 | " runtime=np.timedelta64(60, \"D\"),\n", |
503 | 502 | " dt=np.timedelta64(1, \"D\"),\n", |
504 | 503 | " output_file=ofile,\n", |
|
513 | 512 | "metadata": {}, |
514 | 513 | "outputs": [], |
515 | 514 | "source": [ |
516 | | - "fig, ax = plt.subplots(1, 1, figsize=(10, 3))\n", |
| 515 | + "fig, ax = plt.subplots(1, 1, figsize=(10, 4))\n", |
517 | 516 | "\n", |
518 | | - "ds_out = xr.open_zarr(\"nest_particles.zarr\")\n", |
| 517 | + "ds_out = xr.open_zarr(\"nestedgrid_particles.zarr\")\n", |
519 | 518 | "\n", |
520 | 519 | "plt.plot(ds_out.lon.T, ds_out.lat.T, \"k\", linewidth=0.5)\n", |
521 | | - "sc = ax.scatter(ds_out.lon, ds_out.lat, c=ds_out.nestID, s=4, cmap=cmap, vmin=0, vmax=2)\n", |
| 520 | + "sc = ax.scatter(ds_out.lon, ds_out.lat, c=ds_out.gridID, s=4, cmap=cmap, vmin=0, vmax=2)\n", |
522 | 521 | "xl, yl = ax.get_xlim(), ax.get_ylim()\n", |
523 | 522 | "\n", |
524 | 523 | "for i in range(n_grids - 1):\n", |
|
534 | 533 | "ax.set_aspect(\"equal\")\n", |
535 | 534 | "\n", |
536 | 535 | "cbar = plt.colorbar(sc, ticks=[0, 1, 2], ax=ax)\n", |
537 | | - "cbar.set_label(\"Nest ID\")\n", |
538 | | - "ax.set_title(\"Particle advection through nests\")\n", |
| 536 | + "cbar.set_label(\"Grid ID\")\n", |
| 537 | + "ax.set_title(\"Particle advection through nested Grids\")\n", |
539 | 538 | "plt.tight_layout\n", |
540 | 539 | "plt.show()" |
541 | 540 | ] |
|
545 | 544 | "id": "28", |
546 | 545 | "metadata": {}, |
547 | 546 | "source": [ |
548 | | - "Indeed, we can see that the particles follow the cosine oscillation pattern in the coarser grid, and move purely zonally in the finer grid.\n", |
| 547 | + "Indeed, we can see that the particles follow the cosine oscillation pattern in the coarsest grid, move northeast in the finer grid, and move purely zonally in the finest grid.\n", |
549 | 548 | "\n", |
550 | | - "Note that in the plot above the particles at higher latitudes move farther in longitude-space (at a constant zonal velocity) because of the curvature of the earth, so that is expected." |
| 549 | + "Note that in the plot above the particles at higher latitudes move farther eastward because of the curvature of the earth, so that is expected." |
551 | 550 | ] |
552 | 551 | } |
553 | 552 | ], |
|
0 commit comments