|
29 | 29 | "id": "2", |
30 | 30 | "metadata": {}, |
31 | 31 | "source": [ |
32 | | - "### A priori estimation\n", |
| 32 | + "### A priori estimation of `dt`\n", |
33 | 33 | "\n", |
34 | 34 | "In this example, we will estimate the appropriate timestep to compute advection of a virtual particle through an ocean current field. \n", |
35 | 35 | "\n", |
|
43 | 43 | "\n", |
44 | 44 | "where $\\mathbf{v}(\\mathbf{x},t) = (u(\\mathbf{x},t), v(\\mathbf{x},t))$ describes the ocean velocity field at position $\\mathbf{x}$ at time $t$.\n", |
45 | 45 | "\n", |
46 | | - "To estimate the timescale that we want to resolve, we can think about the scales at which advection varies. Here we use the daily velocity fields at 1/12th degree horizontal resolution from the Copernicus Marine Service. This means that the velocity will vary in time at scales >= 24 hours, and in space at scales >= 1/12th degree." |
| 46 | + "To estimate the timescale that we want to resolve, we can think about the scales at which advection varies. Here we use the daily velocity fields at 1/12th degree horizontal resolution from the Copernicus Marine Service. This means that the velocity will vary in time at scales >= 24 hours, and in space at scales >= 1/12th degree.\n", |
| 47 | + "\n", |
| 48 | + "```{note}\n", |
| 49 | + "Our displacement occurs in units of longitude and latitde, but our velocity field is in m/s. Read [this guide](./tutorial_unitconverters.ipynb) to understand how Parcels converts these units under the hood.\n", |
| 50 | + "```" |
47 | 51 | ] |
48 | 52 | }, |
49 | 53 | { |
|
55 | 59 | "source": [ |
56 | 60 | "import matplotlib.pyplot as plt\n", |
57 | 61 | "import numpy as np\n", |
| 62 | + "import pandas as pd\n", |
58 | 63 | "import xarray as xr\n", |
59 | 64 | "\n", |
60 | 65 | "import parcels\n", |
|
143 | 148 | "id": "7", |
144 | 149 | "metadata": {}, |
145 | 150 | "source": [ |
| 151 | + "$$\n", |
| 152 | + "\\begin{aligned}\n", |
| 153 | + "\\text{d}t < \\frac{1}{12 U_{max}}\n", |
| 154 | + "\\end{aligned}\n", |
| 155 | + "$$\n", |
| 156 | + "\n", |
146 | 157 | "Using `U_max_surface_deg`, we find a second estimated limit of an appropriate `dt`:\n", |
147 | 158 | "\n", |
148 | 159 | "$$\n", |
149 | 160 | "\\begin{aligned}\n", |
| 161 | + "\\text{d}t < \\frac{1}{12 * 1.71e-5} = 4.9e3 \\text{ seconds}\n", |
| 162 | + "\\end{aligned}\n", |
| 163 | + "$$\n", |
| 164 | + "\n", |
| 165 | + "\n", |
| 166 | + "$$\n", |
| 167 | + "\\begin{aligned}\n", |
150 | 168 | "\\text{d}t < 2 \\text{ hours}\n", |
151 | 169 | "\\end{aligned}\n", |
152 | 170 | "$$" |
|
184 | 202 | "id": "10", |
185 | 203 | "metadata": {}, |
186 | 204 | "source": [ |
187 | | - "We simulate particles using a range of timesteps that differ by a factor 2-10, starting at (dt < 24 hours). We also keep track of the time it takes to run each simulation:" |
| 205 | + "We simulate particles using a range of timesteps that differ by a factor 2-10, starting at (dt < 24 hours). We also keep track of the time it takes to run each simulation:\n", |
| 206 | + "\n", |
| 207 | + "```{warning}\n", |
| 208 | + "`dt` must be chosen such that $k`dt`$ fits exactly in the FieldSet dt (24 hours), where $k$ is an integer. \n", |
| 209 | + "```" |
188 | 210 | ] |
189 | 211 | }, |
190 | 212 | { |
191 | 213 | "cell_type": "code", |
192 | 214 | "execution_count": null, |
193 | 215 | "id": "11", |
194 | | - "metadata": { |
195 | | - "tags": [ |
196 | | - "hide-output" |
197 | | - ] |
198 | | - }, |
| 216 | + "metadata": {}, |
199 | 217 | "outputs": [], |
200 | 218 | "source": [ |
201 | 219 | "import time\n", |
|
235 | 253 | " # time and run simulation\n", |
236 | 254 | " start_time = time.time()\n", |
237 | 255 | " pset.execute(\n", |
238 | | - " parcels.kernels.AdvectionRK2, runtime=runtime, dt=dt, output_file=pfile\n", |
| 256 | + " parcels.kernels.AdvectionRK2,\n", |
| 257 | + " runtime=runtime,\n", |
| 258 | + " dt=dt,\n", |
| 259 | + " output_file=pfile,\n", |
| 260 | + " verbose_progress=False,\n", |
239 | 261 | " )\n", |
240 | 262 | " sim_duration_i = time.time() - start_time\n", |
241 | | - " sim_duration[i] = sim_duration_i" |
| 263 | + " sim_duration[i] = sim_duration_i\n", |
| 264 | + " print(f\"Simulation duration = {np.round(sim_duration_i, 2)} seconds\")" |
242 | 265 | ] |
243 | 266 | }, |
244 | 267 | { |
|
443 | 466 | "ax[1].set_xlabel(\"dt (minutes)\")\n", |
444 | 467 | "ax[1].grid()\n", |
445 | 468 | "ax[1].legend()\n", |
446 | | - "plt.show()\n", |
447 | | - "print(f\"dt choices = {(dt_choices / np.timedelta64(1, 'm')).astype(int)} minutes\")\n", |
448 | | - "print(f\"precision = {np.round(np.mean(dist_end, axis=1), 2)} km\")\n", |
449 | | - "print(f\"sim duration = {np.round(sim_duration, 2)} s\")" |
| 469 | + "plt.show()" |
450 | 470 | ] |
451 | 471 | }, |
452 | 472 | { |
453 | 473 | "cell_type": "markdown", |
454 | 474 | "id": "20", |
455 | 475 | "metadata": {}, |
456 | 476 | "source": [ |
457 | | - "We can see that in our simulation advecting particles for 7 days, the effect of `dt` on the precision of our simulation is approximately linear. The precision for a simulation with a timestep of 20 minutes is order of magnitude ~100 m. The effect on the time it takes to run a simulation is not linear in our case however; it increases sharply as we decrease our timestep. This may be optimized using more efficient chunking.\n", |
458 | | - "\n", |
459 | | - "| `dt` | Mean separation distance after 7 days (km) | Simulation duration (s) | \n", |
460 | | - "| ---------- | ------------------------------------------ | ------------------------ |\n", |
461 | | - "| 12 hours | 19.52 | 0.11 |\n", |
462 | | - "| 6 hours | 9.64 | 0.37 | \n", |
463 | | - "| 1 hour | 1.4 | 0.94 | \n", |
464 | | - "| 20 minutes | 0.38 | 3.19 | \n", |
465 | | - "| 5 minutes | x | 12.34 | \n" |
| 477 | + "We can see that in our simulation advecting particles for 7 days, the effect of `dt` on the precision of our simulation is approximately linear. The precision for a simulation with a timestep of 20 minutes is order of magnitude ~100 m. The effect on the time it takes to run a simulation is not linear in our case however; it increases sharply as we decrease our timestep. This may be optimized using more efficient chunking." |
466 | 478 | ] |
467 | 479 | }, |
468 | 480 | { |
469 | | - "cell_type": "markdown", |
| 481 | + "cell_type": "code", |
| 482 | + "execution_count": null, |
470 | 483 | "id": "21", |
471 | 484 | "metadata": {}, |
| 485 | + "outputs": [], |
| 486 | + "source": [ |
| 487 | + "df = pd.DataFrame(\n", |
| 488 | + " {\n", |
| 489 | + " \"dt\": dt_choices,\n", |
| 490 | + " \"Mean separation distance (km)\": np.append(\n", |
| 491 | + " np.round(np.mean(dist_end, axis=1), 2), [None]\n", |
| 492 | + " ),\n", |
| 493 | + " \"Simulation duration (s)\": np.round(sim_duration, 2),\n", |
| 494 | + " }\n", |
| 495 | + ")\n", |
| 496 | + "df" |
| 497 | + ] |
| 498 | + }, |
| 499 | + { |
| 500 | + "cell_type": "markdown", |
| 501 | + "id": "22", |
| 502 | + "metadata": {}, |
472 | 503 | "source": [ |
473 | 504 | "```{note}\n", |
474 | 505 | "The desired precision is not always best measured by the separation distance of individual trajectories. Depending on the application of your Parcels simulation and the process you are computing, other metrics may be more suitable.\n", |
|
477 | 508 | }, |
478 | 509 | { |
479 | 510 | "cell_type": "markdown", |
480 | | - "id": "22", |
| 511 | + "id": "23", |
481 | 512 | "metadata": {}, |
482 | 513 | "source": [ |
483 | 514 | "## Integration schemes\n", |
|
488 | 519 | { |
489 | 520 | "cell_type": "code", |
490 | 521 | "execution_count": null, |
491 | | - "id": "23", |
| 522 | + "id": "24", |
492 | 523 | "metadata": {}, |
493 | 524 | "outputs": [], |
494 | 525 | "source": [ |
|
501 | 532 | { |
502 | 533 | "cell_type": "code", |
503 | 534 | "execution_count": null, |
504 | | - "id": "24", |
| 535 | + "id": "25", |
505 | 536 | "metadata": {}, |
506 | 537 | "outputs": [], |
507 | 538 | "source": [ |
|
512 | 543 | { |
513 | 544 | "cell_type": "code", |
514 | 545 | "execution_count": null, |
515 | | - "id": "25", |
| 546 | + "id": "26", |
516 | 547 | "metadata": {}, |
517 | 548 | "outputs": [], |
518 | 549 | "source": [ |
|
522 | 553 | }, |
523 | 554 | { |
524 | 555 | "cell_type": "markdown", |
525 | | - "id": "26", |
| 556 | + "id": "27", |
526 | 557 | "metadata": {}, |
527 | 558 | "source": [ |
528 | 559 | "The higher-order methods use weighted intermediate steps in time and space to obtain a more accurate estimate of `dlat` and `dlon` for a given timestep.\n", |
|
533 | 564 | { |
534 | 565 | "cell_type": "code", |
535 | 566 | "execution_count": null, |
536 | | - "id": "27", |
| 567 | + "id": "28", |
537 | 568 | "metadata": {}, |
538 | 569 | "outputs": [], |
539 | 570 | "source": [ |
|
547 | 578 | { |
548 | 579 | "cell_type": "code", |
549 | 580 | "execution_count": null, |
550 | | - "id": "28", |
| 581 | + "id": "29", |
551 | 582 | "metadata": { |
552 | 583 | "tags": [ |
553 | 584 | "hide-output" |
|
582 | 613 | " f\"Begin simulation for (scheme: {advection_scheme.__name__}, dt={int(dt / np.timedelta64(1, 's'))} s)\"\n", |
583 | 614 | " )\n", |
584 | 615 | " start_time = time.time()\n", |
585 | | - " pset.execute(advection_scheme, runtime=runtime, dt=dt, output_file=pfile)\n", |
| 616 | + " pset.execute(\n", |
| 617 | + " advection_scheme,\n", |
| 618 | + " runtime=runtime,\n", |
| 619 | + " dt=dt,\n", |
| 620 | + " output_file=pfile,\n", |
| 621 | + " verbose_progress=False,\n", |
| 622 | + " )\n", |
586 | 623 | " sim_duration_ij = time.time() - start_time\n", |
587 | | - " sim_duration[i, j] = sim_duration_ij" |
| 624 | + " sim_duration[i, j] = sim_duration_ij\n", |
| 625 | + " print(f\"Simulation duration = {np.round(sim_duration_ij, 2)} seconds\")" |
588 | 626 | ] |
589 | 627 | }, |
590 | 628 | { |
591 | 629 | "cell_type": "code", |
592 | 630 | "execution_count": null, |
593 | | - "id": "29", |
| 631 | + "id": "30", |
594 | 632 | "metadata": {}, |
595 | 633 | "outputs": [], |
596 | 634 | "source": [ |
597 | 635 | "scheme_colours = np.linspace(0, 1, len(advection_schemes), endpoint=True)\n", |
598 | 636 | "# Now let's compare different advection schemes with the same timestep\n", |
599 | | - "fig, axs = plt.subplots(nrows=1, ncols=len(dt_choices), figsize=(20, 5))\n", |
| 637 | + "fig, axs = plt.subplots(nrows=2, ncols=3, figsize=(15, 10))\n", |
600 | 638 | "for i, dt in enumerate(dt_choices):\n", |
601 | | - " axs[i].set_title(f\"{str(dt)}\")\n", |
602 | | - " axs[i].set_xlabel(\"Longitude\")\n", |
| 639 | + " m = i // 3\n", |
| 640 | + " n = i % 3\n", |
| 641 | + " axs[m, n].set_title(f\"dt = {str(dt)}\")\n", |
| 642 | + " axs[m, n].set_xlabel(\"Longitude\")\n", |
603 | 643 | " for j, advection_scheme in enumerate(advection_schemes):\n", |
604 | 644 | " ds = xr.open_zarr(\n", |
605 | 645 | " f\"output/{advection_scheme.__name__}_dt_{int(dt / np.timedelta64(1, 's'))}s.zarr\"\n", |
606 | 646 | " )\n", |
607 | 647 | " labels = [f\"{advection_scheme.__name__}\"] + [None] * (ds.lon.shape[0] - 1)\n", |
608 | | - " axs[i].plot(\n", |
| 648 | + " axs[m, n].plot(\n", |
609 | 649 | " ds.lon.T,\n", |
610 | 650 | " ds.lat.T,\n", |
611 | 651 | " alpha=0.75,\n", |
612 | 652 | " color=plt.cm.viridis(scheme_colours[j]),\n", |
613 | 653 | " label=labels,\n", |
614 | 654 | " )\n", |
615 | | - " axs[i].scatter(\n", |
| 655 | + " axs[m, n].scatter(\n", |
616 | 656 | " ds.lon[:, 0], ds.lat[:, 0], c=\"r\", marker=\"s\", label=\"starting locations\"\n", |
617 | 657 | " )\n", |
618 | | - " axs[i].grid()\n", |
619 | | - " axs[0].legend()\n", |
620 | | - " axs[0].set_ylabel(\"Latitude\")\n", |
| 658 | + " axs[m, n].grid()\n", |
| 659 | + "axs[-1, -1].axis(\"off\")\n", |
| 660 | + "axs[0, 0].legend()\n", |
| 661 | + "axs[0, 0].set_ylabel(\"Latitude\")\n", |
| 662 | + "axs[1, 0].set_ylabel(\"Latitude\")\n", |
621 | 663 | "plt.show()" |
622 | 664 | ] |
623 | 665 | }, |
624 | 666 | { |
625 | 667 | "cell_type": "markdown", |
626 | | - "id": "30", |
| 668 | + "id": "31", |
627 | 669 | "metadata": {}, |
628 | 670 | "source": [ |
629 | 671 | "Clearly, for longer timesteps, the RK2 and RK4 schemes perform better. However, if the timestep is appropriate, as we have determined in the previous section, then the Explicit Euler scheme does not perform notably different." |
|
632 | 674 | { |
633 | 675 | "cell_type": "code", |
634 | 676 | "execution_count": null, |
635 | | - "id": "31", |
| 677 | + "id": "32", |
636 | 678 | "metadata": {}, |
637 | 679 | "outputs": [], |
638 | 680 | "source": [ |
639 | 681 | "dist_end = np.zeros((len(advection_schemes) - 1, len(dt_choices), npart))\n", |
640 | 682 | "\n", |
641 | | - "fig, axs = plt.subplots(nrows=1, ncols=len(dt_choices), figsize=(20, 5))\n", |
| 683 | + "fig, axs = plt.subplots(nrows=2, ncols=3, figsize=(15, 10))\n", |
| 684 | + "\n", |
642 | 685 | "for i, dt in enumerate(dt_choices):\n", |
643 | | - " axs[i].set_title(f\"dt = {str(dt)}\")\n", |
644 | | - " axs[i].set_xlabel(\"Time\")\n", |
645 | | - " axs[i].tick_params(\"x\", rotation=45)\n", |
646 | | - " axs[i].set_yscale(\"log\")\n", |
647 | | - " axs[i].set_ylim(1e-4, 1e1)\n", |
| 686 | + " m = i // 3\n", |
| 687 | + " n = i % 3\n", |
| 688 | + " axs[m, n].set_title(f\"dt = {str(dt)}\")\n", |
| 689 | + " axs[m, n].set_xlabel(\"Time\")\n", |
| 690 | + " axs[m, n].tick_params(\"x\", rotation=45)\n", |
| 691 | + " axs[m, n].set_yscale(\"log\")\n", |
| 692 | + " axs[m, n].set_ylim(1e-4, 1e1)\n", |
648 | 693 | " ds_RK4 = xr.open_zarr(\n", |
649 | 694 | " f\"output/AdvectionRK4_dt_{int(dt / np.timedelta64(1, 's'))}s.zarr\"\n", |
650 | 695 | " )\n", |
|
667 | 712 | " lat_valid = ds.lat.where(~np.isnan(ds.lat).compute(), drop=True).values\n", |
668 | 713 | " dist = dist_km(lon_valid, lon_valid_RK4, lat_valid, lat_valid_RK4)\n", |
669 | 714 | " time_valid = ds.time.where(~np.isnan(ds.time).compute(), drop=True).values\n", |
670 | | - " axs[i].plot(\n", |
| 715 | + " axs[m, n].plot(\n", |
671 | 716 | " time_valid.T,\n", |
672 | 717 | " dist.T,\n", |
673 | 718 | " alpha=0.75,\n", |
674 | 719 | " color=plt.cm.viridis(scheme_colours[j]),\n", |
675 | 720 | " label=labels,\n", |
676 | 721 | " )\n", |
677 | 722 | " dist_end[j, i] = dist[:, -1]\n", |
678 | | - " axs[i].grid()\n", |
679 | | - " axs[0].legend()\n", |
680 | | - " axs[0].set_ylabel(\"Distance (km)\")\n", |
| 723 | + " axs[m, n].grid()\n", |
| 724 | + "axs[-1, -1].axis(\"off\")\n", |
| 725 | + "axs[0, 0].legend()\n", |
| 726 | + "axs[0, 0].set_ylabel(\"Latitude\")\n", |
| 727 | + "axs[1, 0].set_ylabel(\"Latitude\")\n", |
| 728 | + "plt.tight_layout()\n", |
681 | 729 | "plt.show()" |
682 | 730 | ] |
683 | 731 | }, |
684 | 732 | { |
685 | 733 | "cell_type": "markdown", |
686 | | - "id": "32", |
| 734 | + "id": "33", |
687 | 735 | "metadata": {}, |
688 | 736 | "source": [ |
689 | 737 | "By quantifying the precision of the integration methods, we can see that for a given timestep the Runge-Kutta methods perform orders of magnitude better than the Explicit Euler method. In this example, the error associated with the selected integration methods is smaller than that of the range of timesteps." |
|
692 | 740 | { |
693 | 741 | "cell_type": "code", |
694 | 742 | "execution_count": null, |
695 | | - "id": "33", |
| 743 | + "id": "34", |
696 | 744 | "metadata": {}, |
697 | 745 | "outputs": [], |
698 | 746 | "source": [ |
|
734 | 782 | }, |
735 | 783 | { |
736 | 784 | "cell_type": "markdown", |
737 | | - "id": "34", |
| 785 | + "id": "35", |
738 | 786 | "metadata": {}, |
739 | 787 | "source": [ |
740 | 788 | "In this last figure, we see that the improvement of RK2 by orders of magnitude with respect to EE comes at a small computational cost. Since the RK2 and RK4 methods are practically indistinguishable, using the RK2 method with a timestep of 1 hour or 20 minutes, depending on the application, would be an appropriate choice for this simulation." |
741 | 789 | ] |
742 | 790 | }, |
743 | 791 | { |
744 | 792 | "cell_type": "markdown", |
745 | | - "id": "35", |
| 793 | + "id": "36", |
746 | 794 | "metadata": {}, |
747 | 795 | "source": [ |
748 | 796 | "### Flow conditions\n", |
|
762 | 810 | "Check out [this notebook](https://github.com/Parcels-code/10year-anniversary-session2/blob/main/solutions/lorenz_and_lotka_volterra_solutions.ipynb) to learn how to model the Lorenz equations with Parcels!\n", |
763 | 811 | "```" |
764 | 812 | ] |
| 813 | + }, |
| 814 | + { |
| 815 | + "cell_type": "markdown", |
| 816 | + "id": "37", |
| 817 | + "metadata": {}, |
| 818 | + "source": [ |
| 819 | + "## Summary\n", |
| 820 | + "If you want to test the accuracy and efficiency of your own simulation, here is a brief list of things to consider:\n", |
| 821 | + "- **Use the temporal and spatial resolution of the input to estimate the timescales you need to resolve.** `dt` must fit into the FieldSet dt by a factor of an integer.\n", |
| 822 | + "- **Run the simulation for the full runtime.** Many inaccuracies grow over time so we must \n", |
| 823 | + "- **Consider which step introduces the largest error.** If we compute advection and diffusion, increasing the accuracy of one process much more than the other will not improve the results." |
| 824 | + ] |
765 | 825 | } |
766 | 826 | ], |
767 | 827 | "metadata": { |
|
0 commit comments