diff --git a/docs/_static/NEMO_ghost_vel.png b/docs/_static/NEMO_ghost_vel.png new file mode 100644 index 0000000000..ea4ed39eed Binary files /dev/null and b/docs/_static/NEMO_ghost_vel.png differ diff --git a/docs/_static/NEMO_latBC.png b/docs/_static/NEMO_latBC.png new file mode 100644 index 0000000000..cd1b6f5b00 Binary files /dev/null and b/docs/_static/NEMO_latBC.png differ diff --git a/docs/community/index.md b/docs/community/index.md index 669d209322..4b78d65803 100644 --- a/docs/community/index.md +++ b/docs/community/index.md @@ -51,7 +51,7 @@ Report a bug with an Issue ````{grid-item-card} Sharing user code :shadow: md -Curious to see if someone has already written the custom `Kernel` you are thinking of or runs **Parcels** with the same hydrodynamic data? Check out the parcels_contributions repository and share examples with other users! +Curious to see if someone has already written the custom `Kernel` you are thinking of or runs Parcels with the same hydrodynamic data? Check out the parcels_contributions repository and share examples with other users! ```{image} https://img.shields.io/badge/maintainer_needed-red :width: 40% diff --git a/docs/conf.py b/docs/conf.py index 64bf04f4ff..feb8b5f4c6 100755 --- a/docs/conf.py +++ b/docs/conf.py @@ -523,6 +523,11 @@ def linkcode_resolve(domain, info): nb_execution_raise_on_error = True nb_execution_timeout = 75 suppress_warnings = ["mystnb.unknown_mime_type"] +nitpicky = True +nitpick_ignore_regex = [ + (r"py:class", r".*"), + (r"py:mod", r".*"), +] # -- Options for autoapi -------------------------------------------------- autoapi_dirs = ["../src/parcels"] diff --git a/docs/development/docsguide.md b/docs/development/docsguide.md index b48ad28444..1be5f5aaf2 100644 --- a/docs/development/docsguide.md +++ b/docs/development/docsguide.md @@ -31,7 +31,7 @@ a table showing the latest notebook execution: - **Prefer `import parcels` over `from parcels import class` in tutorials and how-to guides** so its obvious in later code cells which classes and methods are part of Parcels. - [**Avoid too much Repitition In Documentation**](https://www.writethedocs.org/guide/writing/docs-principles/#arid): - tutorials and how-to guides notebooks will often have repetition of the general **Parcels** steps, (e.g., imports ) - + tutorials and how-to guides notebooks will often have repetition of the general Parcels steps, (e.g., imports ) - this is needed so that users have complete examples that they can copy and experiment with.`. We try to limit each page in the documentation to a small number of examples. - Introduce links and cross-references to maximize discoverability of documentation. This also reduces the necessity for diff --git a/docs/index.md b/docs/index.md index 88c95b6e34..df379d2ca1 100755 --- a/docs/index.md +++ b/docs/index.md @@ -1,12 +1,12 @@ # Parcels documentation -Welcome to the documentation of Parcels. **Parcels** provides a set of Python classes and methods to create customisable particle tracking simulations using gridded output from (ocean) circulation models. Parcels can be used to track passive and active particulates such as water, plankton, [plastic](http://www.topios.org/) and [fish](https://github.com/Jacketless/IKAMOANA). +Welcome to the documentation of Parcels. **Parcels** provides a set of Python classes and methods to create customisable particle tracking simulations using gridded output from (ocean) circulation models. Parcels can be used to track passive and active particulates such as water, plankton, plastic, and fish. ```{figure} _static/homepage.gif :class: dark-light ``` -_Animation of virtual particles carried by ocean surface flow in the global oceans. The particles are advected with Parcels in data from the_ [NEMO Ocean Model](https://www.nemo-ocean.eu/). +_Animation of virtual particles carried by ocean surface flow in the global oceans. The particles are advected with Parcels in data from the [Copernicus Marine Service](https://marine.copernicus.eu). See [here](./user_guide/examples/tutorial_homepage_animation.md) for the script to create this animation yourself._ ```{note} You can browse the documentation for older versions by using the version switcher in the bottom right. @@ -14,8 +14,6 @@ You can browse the documentation for older versions by using the version switche **Useful links**: [Installation instructions](user_guide/getting_started/installation) | [Discussions on GitHub](https://github.com/Parcels-code/parcels/discussions) | [Issue on GitHub](https://github.com/Parcels-code/parcels/issues) | [Parcels website](https://parcels-code.org/) | [CLAM community website](https://clam-community.github.io/) | [API reference](reference/parcels/index) -New to **Parcels**? Check out the [installation instructions](user_guide/getting_started/installation), run the [quickstart tutorial](user_guide/getting_started/tutorial_quickstart), and learn the [key concepts](user_guide/getting_started/explanation_concepts) to understand the package. - `````{grid} 1 2 2 2 :gutter: 4 :padding: 2 2 0 0 @@ -24,7 +22,7 @@ New to **Parcels**? Check out the [installation instructions](user_guide/getting ````{grid-item-card} Getting started :shadow: md -New to **Parcels**? Check out the [installation instructions](user_guide/getting_started/installation), run the [quickstart tutorial](user_guide/getting_started/tutorial_quickstart), and learn the [key concepts](user_guide/getting_started/explanation_concepts) to understand the package. +New to Parcels? Check out the [installation instructions](user_guide/getting_started/installation), run the [quickstart tutorial](user_guide/getting_started/tutorial_quickstart), and learn the [key concepts](user_guide/getting_started/explanation_concepts) to understand the package. +++ @@ -39,7 +37,7 @@ Get started! ````{grid-item-card} How to? :shadow: md -Wondering how to load a `FieldSet` or write a `Kernel`? Find **tutorials** and explainers to these and other questions here. +Wondering how to load a `FieldSet` or write a `Kernel`? Find tutorials and explainers to these and other questions here. +++ @@ -54,7 +52,7 @@ To the user guide ````{grid-item-card} Development :shadow: md -We encourage anyone to help improve **Parcels**: read our guidelines to get started! +We encourage anyone to help improve Parcels: read our guidelines to get started! +++ @@ -69,7 +67,7 @@ Contributing guidelines ````{grid-item-card} Community :shadow: md -Want to interact with other users and **Parcels** developers? +Want to interact with other users and Parcels developers? +++ diff --git a/docs/user_guide/examples/explanation_grids.ipynb b/docs/user_guide/examples/explanation_grids.ipynb index d59eec8215..e3e9ac09ce 100644 --- a/docs/user_guide/examples/explanation_grids.ipynb +++ b/docs/user_guide/examples/explanation_grids.ipynb @@ -9,13 +9,13 @@ "\n", "In Lagrangian ocean analysis, virtual particle tracking requires the accurate interpolation of physical properties (flow velocities and tracer properties) to particle locations. The underlying data that forces the particle movement will likely be defined on a discretised grid. \n", "\n", - "Parcels can natively handle two styles of grids; structured and unstructured, where Parcels `Field` objects exist on a (structured) `parcels.XGrid` and conform to [SGRID](https://sgrid.github.io/sgrid/) conventions, or on a (unstructured) `parcels.Uxgrid` and conform to [UGRID](https://ugrid-conventions.github.io/ugrid-conventions/) conventions. Here we describe these grids on a conceptual level.\n", + "Parcels can natively handle two styles of grids; structured and unstructured, where Parcels `Field` objects exist on a (structured) {py:obj}`parcels.XGrid` and conform to [SGRID](https://sgrid.github.io/sgrid/) conventions, or on a (unstructured) {py:obj}`parcels.UxGrid` and conform to [UGRID](https://ugrid-conventions.github.io/ugrid-conventions/) conventions. Here we describe these grids on a conceptual level.\n", "\n", "```{note}\n", "Parcels comes ready with a range of `convert` functions which will convert your `xarray` dataset into a SGRID or UGRID compliant dataset. See [this list](../index.md#converting-model-data-to-fieldsets) for all the models we have `convert` functions for. If your model is not on that list, you can create your own `convert` function, or reach out on our discussion board for help.\n", "```\n", "\n", - "Under the hood, every `Field` in a `FieldSet` has a `grid` attribute. This `grid` stores the spatial and temporal information of the Field coordinates. The number of Grids in a FieldSet is thus always smaller or equal to the number of Field objects; and this is what the \"grid number\" column in `FieldSet.describe()` refers to.\n", + "Under the hood, every `Field` in a `FieldSet` has a `grid` attribute. This `grid` stores the spatial and temporal information of the Field coordinates. The number of Grids in a FieldSet is thus always smaller or equal to the number of Field objects; and this is what the \"grid number\" column in {py:func}`parcels.FieldSet.describe()` refers to.\n", "\n", "## Structured grids\n", "\n", @@ -229,7 +229,7 @@ "\n", "For structured grids, [Blanke and Raynaud]() proposed in 1997 to perform a 1D linear interpolation of the $i$ velocity in the $i$ direction, and similarly a 1D linear interpolation of the $j$ velocity in the $j$ direction. These velocities must then be rotated into zonal and meridional velocities, which Parcels handles under the hood. In such a case, your velocity field may look like figure 5.\n", "\n", - "This (uni)linear velocity interpolation is now often referred to as the Analytical interpolation scheme; this is what the [Ariane](https://ariane-code.cnrs.fr) and [TRACMASS](https://www.tracmass.org/index.html) Lagrangian codes also use. In Parcels, the time-stepping version of this interpolation is provided in the `CGrid_Velocity` Interpolator function.\n", + "This (uni)linear velocity interpolation is now often referred to as the Analytical interpolation scheme; this is what the [Ariane](https://ariane-code.cnrs.fr) and [TRACMASS](https://www.tracmass.org/index.html) Lagrangian codes also use. In Parcels, the time-stepping version of this interpolation is provided in the {py:func}`parcels.interpolators.CGrid_Velocity` Interpolator function.\n", "\n", "```{note}\n", "If you have a structured curvilinear grid, and your velocity field is oriented in the $i/j$-directions, then it is almost certain that your velocities are computed from fluxes across the cell edge, and that you will require the `CGrid_Velocity` Interpolator.\n", diff --git a/docs/user_guide/examples/explanation_interpolation.md b/docs/user_guide/examples/explanation_interpolation.md index 95cc1e852e..740b06e136 100644 --- a/docs/user_guide/examples/explanation_interpolation.md +++ b/docs/user_guide/examples/explanation_interpolation.md @@ -1,10 +1,10 @@ # πŸ“– Interpolators Overview and API Interpolation is an important functionality of Parcels. On this page we will discuss the way it is -implemented in **Parcels** and how to write a custom interpolator function. +implemented in Parcels and how to write a custom interpolator function. When we want to know the state of particles in an environmental field, such as temperature or velocity, -we _evaluate_ the `parcels.Field` at the particles real position in time and space (`t`, `z`, `y`, `x`). +we _evaluate_ the {py:obj}`parcels.Field` at the particles real position in time and space (`t`, `z`, `y`, `x`). In Parcels we can do this using square brackets: ``` @@ -89,4 +89,4 @@ grid_positions = { The `.interp()` method should return a float (in the case of a `ScalarInterpolator` or a tuple of three floats `(u, v, w)` in the case of a `VectorInterpolator`). -Writing custom interpolators is not trivial, so we recommend that you have a look at the built-in interpolators in {py:func}`parcels.interpolators._xinterpolators` or {py:func}`parcels.interpolators._uxinterpolators` to see how they are implemented. +Writing custom interpolators is not trivial, so we recommend that you have a look at the built-in [structured-grid interpolators](https://github.com/Parcels-code/Parcels/blob/main/src/parcels/interpolators/_xinterpolators.py) or the [unstructured-grid interpolators](https://github.com/Parcels-code/Parcels/blob/main/src/parcels/interpolators/_uxinterpolators.py) to see how they are implemented. diff --git a/docs/user_guide/examples/explanation_kernelloop.md b/docs/user_guide/examples/explanation_kernelloop.md index 338efbec1a..2c56269d30 100644 --- a/docs/user_guide/examples/explanation_kernelloop.md +++ b/docs/user_guide/examples/explanation_kernelloop.md @@ -18,14 +18,14 @@ In order to make sure that the displacements of a particle in the different Kern ## Basic implementation -Below is a structured overview of how the Kernel loop is implemented. Note that this is for `time` and `x` only, but the process for `y` and `z` is also applied to `y` and `z`. +Below is a structured overview of how the Kernel loop is implemented. Note that this is for `t` and `x` only, but the process for `y` and `z` is also applied to `y` and `z`. 1. Initialise an extra Variable `particles.dx=0` 2. Within the Kernel loop, for each particle: 1. Update `particles.x += particles.dx` - 2. Update `particles.time += particles.dt` (except for on the first iteration of the Kernel loop)
+ 2. Update `particles.t += particles.dt` (except for on the first iteration of the Kernel loop)
3. Set variable `particles.dx = 0` @@ -34,9 +34,9 @@ Below is a structured overview of how the Kernel loop is implemented. Note that 2. Update `particles.dx` by adding the change in x, if needed - 5. If `outputdt` is a multiple of `particles.time`, write `particles.x` and `particles.time` to zarr output file + 5. If `outputdt` is a multiple of `particles.t`, write `particles.x` and `particles.t` to zarr output file -Besides having commutable Kernels, the main advantage of this implementation is that, when using Field Sampling with e.g. `particles.temp = fieldset.Temp[particles.time, particles.z, particles.y, particles.x]`, the particle location stays the same throughout the entire Kernel loop. Additionally, this implementation ensures that the particle location is the same as the location of the sampled field in the output file. +Besides having commutable Kernels, the main advantage of this implementation is that, when using Field Sampling with e.g. `particles.var = fieldset.Var[particles.t, particles.z, particles.y, particles.x]`, the particle location stays the same throughout the entire Kernel loop. Additionally, this implementation ensures that the particle location is the same as the location of the sampled field in the output file. ## Example with currents and winds diff --git a/docs/user_guide/examples/explanation_performance.md b/docs/user_guide/examples/explanation_performance.md index 03a30e7dd5..3ee7346c8b 100644 --- a/docs/user_guide/examples/explanation_performance.md +++ b/docs/user_guide/examples/explanation_performance.md @@ -13,7 +13,7 @@ In this tutorial, we will show how to squeeze performance in Parcels by using a Note that the concept of Parcels Backends is different from [Xarray backends](https://docs.xarray.dev/en/latest/api/backends.html). ```{note} -You can check which Backend Parcels is using by calling `fieldset.describe()`. The last column shows the Backend that is used for each Field. +You can check which Backend Parcels is using by calling {py:func}`parcels.FieldSet.describe()`. The last column shows the Backend that is used for each Field. ``` ## Option 1: load the full FieldSet into memory @@ -42,7 +42,7 @@ This will make Parcels use `numpy` functions in the interpolation routines, whic _Uses Parcels Backend: Zarr_ -If your Dataset is too large to fit into memory, but your particles are only distributed over a small part of the domain, it could be efficient to use cached zarr files. This can be done by using the (experimental) `zarr.CacheStore` in combination with the `parcels.open_raw_zarr()` function. This will make Parcels only load the chunks that are needed for the particles, and cache these chunks in memory for future use. +If your Dataset is too large to fit into memory, but your particles are only distributed over a small part of the domain, it could be efficient to use cached zarr files. This can be done by using the (experimental) `zarr.CacheStore` in combination with the {py:func}`parcels.open_raw_zarr()` function. This will make Parcels only load the chunks that are needed for the particles, and cache these chunks in memory for future use. ```{code-block} python source_store = zarr.storage.LocalStore(filenames) @@ -71,7 +71,7 @@ _Uses Parcels Backend: WindowedArray_ **Best for: large Datasets (more than a few GB) and particles distributed over the entire domain** -If your Dataset is so large that it doesn't fit into memory, you can use the `fieldset.to_windowed_arrays()` method to make Parcels only hold two timeslices in memory. Note that this only works if the two timeslices still fit into memory. +If your Dataset is so large that it doesn't fit into memory, you can use the {py:func}`parcels.FieldSet.to_windowed_arrays()` method to make Parcels only hold two timeslices in memory. Note that this only works if the two timeslices still fit into memory. The two timeslices (the current and the next) are fully loaded into memory, so this method is especially useful if your particles are distributed over the entire domain, as all data will then have to be accessed anyway. diff --git a/docs/user_guide/examples/tutorial_Argofloats.ipynb b/docs/user_guide/examples/tutorial_Argofloats.ipynb index dfd4f6a268..0219ca326a 100644 --- a/docs/user_guide/examples/tutorial_Argofloats.ipynb +++ b/docs/user_guide/examples/tutorial_Argofloats.ipynb @@ -164,7 +164,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we can plot the trajectory of the Argo float with some simple calls to netCDF4 and matplotlib.\n", + "Now we can plot the trajectory of the Argo float with some simple calls to {py:obj}`parcels.read_particlefile()` and `matplotlib`.\n", "\n", "First plot the depth as a function of time, with the temperature as color (only on the upcast)." ] diff --git a/docs/user_guide/examples/tutorial_croco_3D.ipynb b/docs/user_guide/examples/tutorial_croco_3D.ipynb index d2ba42130c..ef6ead85de 100644 --- a/docs/user_guide/examples/tutorial_croco_3D.ipynb +++ b/docs/user_guide/examples/tutorial_croco_3D.ipynb @@ -57,7 +57,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we create a FieldSet object using the `convert.croco_to_sgrid()` function to first create an S-Grid compliant datatset, and then use that in `FieldSet.from_sgrid_conventions()` to create the FieldSet." + "Now we create a FieldSet object using the {py:func}`parcels.convert.croco_to_sgrid()` function to first create an S-Grid compliant datatset, and then use that in {py:func}`parcels.FieldSet.from_sgrid_conventions()` to create the FieldSet." ] }, { @@ -298,11 +298,6 @@ "z_new = sigma_new * fieldset.h[particles.t, 0, y_new, x_new, particles]\n", "```" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] } ], "metadata": { diff --git a/docs/user_guide/examples/tutorial_delaystart.ipynb b/docs/user_guide/examples/tutorial_delaystart.ipynb index c404f425d5..5568bc3613 100644 --- a/docs/user_guide/examples/tutorial_delaystart.ipynb +++ b/docs/user_guide/examples/tutorial_delaystart.ipynb @@ -43,7 +43,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "First import a `FieldSet` (from the Argo example, in this case)\n" + "First create a {py:obj}`parcels.FieldSet` (from the [Argo example](./tutorial_Argofloats.ipynb), in this case)" ] }, { @@ -71,7 +71,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Defining initial `time` as particle release\n" + "## Defining initial `t` as particle release\n" ] }, { @@ -79,7 +79,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Defining the initial times of particles is done when the `ParticleSet` is defined. Although `time` and `z` are optional arguments (with FieldSet t=0 and z=0 as defaults), it is good practice to define them explicitly to ensure expected behavior. The simplest way to delay the start of a particle is to use the `time` argument for each particle.\n" + "Defining the initial times of particles is done when the {py:obj}`parcels.ParticleSet` is defined. Although `t` and `z` are optional arguments (with FieldSet t=0 and z=0 as defaults), it is good practice to define them explicitly to ensure expected behavior. The simplest way to delay the start of a particle is to use the `t` argument for each particle." ] }, { @@ -193,7 +193,7 @@ "source": [ "
\n", "\n", - "Note that the `repeatdt` argument in `parcels.ParticleSet` used in previous versions of Parcels is no longer supported as of v4\n", + "Note that the `repeatdt` argument in {py:obj}`parcels.ParticleSet` used in previous versions of Parcels is no longer supported as of v4\n", "
" ] }, @@ -303,11 +303,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Synced `time` in the output file\n", + "## Synced time in the output file\n", "\n", "Note that, because the `outputdt` variable controls the Kernel-loop, all particles are written _at the same time_, even when they start at a non-multiple of `outputdt`.\n", "\n", - "For example, if your particles start at `time=[0, 1, 2]` and `outputdt=2`, then the times written (for `dt=1` and `endtime=4`) will be `[0, 2, 2, 2, 4, 4, 4]`" + "For example, if your particles start at `t=[0, 1, 2]` and `outputdt=2`, then the times written (for `dt=1` and `endtime=4`) will be `[0, 2, 2, 2, 4, 4, 4]`" ] }, { @@ -345,7 +345,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "And indeed, the `time` values in the output file are as expected\n" + "And indeed, the `t` values in the output file are as expected\n" ] }, { @@ -363,7 +363,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now, for some applications, this behavior may be undesirable; for example when particles need to be analyzed at a same age (instead of at a same time). In that case, we recommend either changing `outputdt` so that it is a common divisor of all start times; or doing multiple Parcels runs with subsets of the original `ParticleSet` (e.g., in the example above, one run with the Particles that start at `t=[0, 2]` and one with the Particle at `t=[1]`). In that case, you will get two files:\n" + "Now, for some applications, this behavior may be undesirable; for example when particles need to be analyzed at a same age (instead of at a same time). In that case, we recommend either changing `outputdt` so that it is a common divisor of all start times; or by [writing directly in the kernel](./tutorial_write_in_kernel.ipynb); or by doing multiple Parcels runs with subsets of the original `ParticleSet` (e.g., in the example above, one run with the Particles that start at `t=[0, 2]` and one with the Particle at `t=[1]`). In that case, you will get two files:\n" ] }, { diff --git a/docs/user_guide/examples/tutorial_delft3d.ipynb b/docs/user_guide/examples/tutorial_delft3d.ipynb index 545751929b..7b8ea473c5 100644 --- a/docs/user_guide/examples/tutorial_delft3d.ipynb +++ b/docs/user_guide/examples/tutorial_delft3d.ipynb @@ -49,7 +49,7 @@ "metadata": {}, "source": [ "```{note}\n", - "While Delft3D provides velocities on a CGrid, we currently can't use the `CGrid_Velocity` interpolator on the velocity fields. This is because the velocities have been rotated to east and north in the output - but the Parcels interpolator expects them in the `M` and `N` (along-grid) directions. Therefore, we use the `XFreeslip` interpolator instead.\n", + "While Delft3D provides velocities on a CGrid, we currently can't use the {py:obj}`parcels.interpolators.CGrid_Velocity` interpolator on the velocity fields. This is because the velocities have been rotated to east and north in the output - but the Parcels interpolator expects them in the `M` and `N` (along-grid) directions. Therefore, we use the {py:obj}`parcels.interpolators.XFreeslip` interpolator instead.\n", "```" ] }, diff --git a/docs/user_guide/examples/tutorial_diffusion.ipynb b/docs/user_guide/examples/tutorial_diffusion.ipynb index e2d9788b5c..12d1389a47 100644 --- a/docs/user_guide/examples/tutorial_diffusion.ipynb +++ b/docs/user_guide/examples/tutorial_diffusion.ipynb @@ -80,17 +80,19 @@ "\n", "## Using Advection-Diffusion Kernels in Parcels\n", "\n", - "The EM and M1 advection-diffusion approximations are available as `AdvectionDiffusionEM` and `AdvectionDiffusionM1`, respectively. The `AdvectionDiffusionM1` kernel should be the default choice, as the increased accuracy comes at negligible computational cost.\n", + "The EM and M1 advection-diffusion approximations are available as {py:func}`parcels.kernels.AdvectionDiffusionEM` and {py:func}`parcels.kernels.AdvectionDiffusionM1`, respectively. The {py:func}`parcels.kernels.AdvectionDiffusionM1` kernel should be the default choice, as the increased accuracy comes at negligible computational cost.\n", "\n", - "The advection component of these kernels is similar to that of the Explicit Euler advection kernel (`AdvectionEE`). In the special case where diffusivity is constant over the entire domain, the diffusion-only kernel `DiffusionUniformKh` can be used in combination with an advection kernel of choice. Since the diffusivity here is space-independent, gradients are not calculated, increasing efficiency. The diffusion-step can in this case be computed after or before advection, thus allowing you to chain kernels in a list.\n", + "The advection component of these kernels is similar to that of the Explicit Euler advection kernel (`AdvectionEE`). In the special case where diffusivity is constant over the entire domain, the diffusion-only kernel {py:func}`parcels.kernels.DiffusionUniformKh` can be used in combination with an advection kernel of choice. Since the diffusivity here is space-independent, gradients are not calculated, increasing efficiency. The diffusion-step can in this case be computed after or before advection, thus allowing you to chain kernels in a list.\n", "\n", - "Just like velocities, diffusivities are passed to Parcels in the form of `Field` objects. When using `DiffusionUniformKh`, they should be added to the `FieldSet` object as constant fields, e.g. `fieldset.add_constant_field(\"Kh_zonal\", 1, mesh=\"flat\")`.\n", + "Just like velocities, diffusivities are passed to Parcels in the form of {py:func}`parcels.Field` objects. When using {py:func}`parcels.kernels.DiffusionUniformKh`, they should be added to the {py:func}`parcels.FieldSet` object as constant fields, e.g. `fieldset.add_constant_field(\"Kh_zonal\", 1, mesh=\"flat\")`.\n", "\n", "To make a central difference approximation for computing the gradient in diffusivity, a resolution for this approximation `dres` is needed: _Parcels_ approximates the gradients in diffusivities by using their values at the particle's location Β± `dres` (in both $x$ and $y$). A value of `dres` must be specified and added to the FieldSet by the user (e.g. `fieldset.add_context(\"dres\", 0.01)`). Currently, it is unclear what the best value of `dres` is. From experience, the size of `dres` should be smaller than the spatial resolution of the data, but within reasonable limits of machine precision to avoid numerical errors. We are working on a method to compute gradients differently so that specifying `dres` is not necessary anymore.\n", "\n", "## Example: Impermeable Diffusivity Profile\n", "\n", - "Let's see the `AdvectionDiffusionM1` in action and see why it's preferable over the `AdvectionDiffusionEM` kernel. To do so, we create an idealized profile with diffusivities $K_\\text{zonal}$ uniform everywhere ($K_\\text{zonal} = \\bar{K}=0.5$) and $K_\\text{meridional}$ constant in the zonal direction, while having the following profile in the meridional direction:\n", + "Let's see the {py:func}`parcels.kernels.AdvectionDiffusionM1` in action and see why it's preferable over the {py:func}`parcels.kernels.AdvectionDiffusionEM` kernel. \n", + "\n", + "To do so, we create an idealized profile with diffusivities $K_\\text{zonal}$ uniform everywhere ($K_\\text{zonal} = \\bar{K}=0.5$) and $K_\\text{meridional}$ constant in the zonal direction, while having the following profile in the meridional direction:\n", "\n", "$$\n", "K_\\text{meridional}(y) = \\bar{K}\\frac{2(1+\\alpha)(1+2\\alpha)}{\\alpha^2H^{1+1/\\alpha}} \\begin{cases}\n", @@ -234,7 +236,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we will simulate the advection and diffusion of the particles using the `AdvectionDiffusionM1` kernel. We run the simulation for 0.3 seconds, with a numerical timestep $\\Delta t = 0.001$ s. We also write away particle locations at each timestep for plotting. Note that this will hinder a runtime comparison between kernels, since it will cause most time to be spent on I/O.\n" + "Now we will simulate the advection and diffusion of the particles using the {py:func}`parcels.kernels.AdvectionDiffusionM1` kernel. We run the simulation for 0.3 seconds, with a numerical timestep $\\Delta t = 0.001$ s. We also write away particle locations at each timestep for plotting. Note that this will hinder a runtime comparison between kernels, since it will cause most time to be spent on I/O.\n" ] }, { @@ -314,7 +316,7 @@ "source": [ "We see that the along the meridional direction, particles remain confined to the β€˜upper’ part of the domain, not crossing the impermeable barrier where the diffusivity drops to zero. In the zonal direction, particles follow random walks, since all terms involving gradients of the diffusivity are zero.\n", "\n", - "Now let's execute the simulation with the `AdvectionDiffusionEM` kernel instead.\n" + "Now let's execute the simulation with the {py:func}`parcels.kernels.AdvectionDiffusionEM` kernel instead.\n" ] }, { @@ -393,7 +395,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Example: Using horizontal diffusion calculated from velocity fields\n", + "## Using horizontal diffusion calculated from velocity fields\n", "\n", "In the case when velocity fields are available, there is the possibility to calculate coefficients of diffusion based on closure parameterizations. The Smagorinsky method ([Smagorinsky, 1963]()), which was originally proposed as a parameterization for horizontal eddy viscosity, is often used to parameterize horizontal eddy diffusivity as well. It computes the eddy diffusivity as\n", "\n", diff --git a/docs/user_guide/examples/tutorial_dt_integrators.ipynb b/docs/user_guide/examples/tutorial_dt_integrators.ipynb index 8dba6b1f9b..df0f7ad0db 100644 --- a/docs/user_guide/examples/tutorial_dt_integrators.ipynb +++ b/docs/user_guide/examples/tutorial_dt_integrators.ipynb @@ -130,11 +130,11 @@ "source": [ "# Use the maximum velocity at the surface to scale dx/dy\n", "U_max_surface = np.nanmax(np.hypot(ds_fields.uo, ds_fields.vo))\n", - "print(f\"U_max = {str(np.round(U_max_surface, 2))} m s-1\")\n", + "print(f\"U_max = {U_max_surface:.2f} m s-1\")\n", "\n", "# convert to degrees s-1 (at lat = 30 deg S)\n", "U_max_surface_deg = U_max_surface / (1852 * 60 * np.cos(np.deg2rad(-30)))\n", - "print(f\" == {str(np.round(U_max_surface_deg * 1e5, 2))}e-5 degrees s-1\")" + "print(f\" == {U_max_surface_deg * 1e5:.2f}e-5 degrees s-1\")" ] }, { @@ -182,7 +182,7 @@ "source": [ "### Test timesteps\n", "\n", - "Now that we have an estimate of an appropriate choice for `dt`, we can test a range of values in Parcels to understand the impact on the accuracy of our trajectories. To do so, we can run simulations using `parcels.kernels.AdvectionRK2` for a limited number of particles and quantify the differences." + "Now that we have an estimate of an appropriate choice for `dt`, we can test a range of values in Parcels to understand the impact on the accuracy of our trajectories. To do so, we can run simulations using {py:obj}`parcels.kernels.AdvectionRK2` for a limited number of particles and quantify the differences." ] }, { @@ -799,12 +799,6 @@ "- **Run the simulation for the full runtime.** Many errors grow over time so to quantify these, we must take into account the full length of the simulation.\n", "- **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." ] - }, - { - "cell_type": "markdown", - "id": "37", - "metadata": {}, - "source": [] } ], "metadata": { diff --git a/docs/user_guide/examples/tutorial_fesom.ipynb b/docs/user_guide/examples/tutorial_fesom.ipynb index 964457571b..d28d610571 100644 --- a/docs/user_guide/examples/tutorial_fesom.ipynb +++ b/docs/user_guide/examples/tutorial_fesom.ipynb @@ -14,8 +14,8 @@ "Parcels v4 supports unstructured-grid model output via [uxarray](https://uxarray.readthedocs.io/). This tutorial walks through the minimum steps to advect particles in real [FESOM2](https://fesom.de/) output. The recipe is:\n", "\n", "1. Open the FESOM grid and data files with `uxarray`.\n", - "2. Rename FESOM-specific dimensions to Parcels' UGRID conventions with `parcels.convert.fesom_to_ugrid`.\n", - "3. Build a `FieldSet` with `parcels.FieldSet.from_ugrid_conventions`.\n", + "2. Rename FESOM-specific dimensions to Parcels' UGRID conventions with {py:func}`parcels.convert.fesom_to_ugrid()`.\n", + "3. Build a {py:obj}`parcels.FieldSet` with {py:func}`parcels.FieldSet.from_ugrid_conventions()`.\n", "4. Run the simulation as on any structured grid.\n", "\n", "If you have not done so already, work through the [quickstart tutorial](../getting_started/tutorial_quickstart.md) first to get familiar with `ParticleSet`, `Kernel`, and `ParticleFile`." @@ -44,7 +44,7 @@ "source": [ "## Get the FESOM tutorial dataset\n", "\n", - "We use a small periodic-channel snapshot from a FESOM2 simulation that ships with Parcels' tutorial data registry. As in the [quickstart](../getting_started/tutorial_quickstart.md), `parcels.tutorial.open_dataset` downloads the files into a local cache on first use; subsequent calls just return the cached copy.\n", + "We use a small periodic-channel snapshot from a FESOM2 simulation that ships with Parcels' tutorial data registry. As in the [quickstart](../getting_started/tutorial_quickstart.md), {py:func}`parcels.tutorial.open_dataset()` downloads the files into a local cache on first use; subsequent calls just return the cached copy.\n", "\n", "`uxarray` expects file paths rather than an in-memory dataset, so we trigger the downloads and then point `ux.open_mfdataset` at the cached files:" ] @@ -137,7 +137,7 @@ "source": [ "## Build the `FieldSet`\n", "\n", - "With UGRID-compliant dimensions in place, `parcels.FieldSet.from_ugrid_conventions` builds the `FieldSet`. It detects `U`, `V`, `W`, assigns a `UxGrid` to each field, and picks an appropriate interpolator based on each variable's coordinate location (face- vs. node-registered, centre vs. interface). Use `mesh=\"spherical\"` so that velocities in m/s are correctly converted to deg/s along the lon/lat coordinates:" + "With UGRID-compliant dimensions in place, {py:func}`parcels.FieldSet.from_ugrid_conventions()` builds the `FieldSet`. It detects `U`, `V`, `W`, assigns a `UxGrid` to each field, and picks an appropriate interpolator based on each variable's coordinate location (face- vs. node-registered, centre vs. interface). Use `mesh=\"spherical\"` so that velocities in m/s are correctly converted to deg/s along the lon/lat coordinates:" ] }, { @@ -164,7 +164,7 @@ "source": [ "## Release particles and advect\n", "\n", - "We seed particles on a grid of four latitudes spanning the channel and ten longitudes, and integrate for two days with RK4. Because this snapshot has only a single time level, `fieldset.time_interval` is `None` and we omit the `time=` argument so that Parcels treats the flow as constant in time:" + "We seed particles on a grid of four latitudes spanning the channel and ten longitudes, and integrate for two days with RK4. Because this snapshot has only a single time level, `fieldset.time_interval` is `None` and we omit the `t=` argument so that Parcels treats the flow as constant in time:" ] }, { diff --git a/docs/user_guide/examples/tutorial_gsw_density.ipynb b/docs/user_guide/examples/tutorial_gsw_density.ipynb index 2a2b267f51..f9ca023393 100644 --- a/docs/user_guide/examples/tutorial_gsw_density.ipynb +++ b/docs/user_guide/examples/tutorial_gsw_density.ipynb @@ -13,7 +13,7 @@ "id": "1", "metadata": {}, "source": [ - "This tutorial shows how to use the [`gsw` toolbox](https://teos-10.github.io/GSW-Python/) (the Gibbs SeaWater Oceanographic Toolbox of TEOS-10) within Parcels to compute density from temperature and salinity fields. The `gsw` toolbox can be installed via `conda install gsw`." + "This tutorial shows how to use the [`gsw` toolbox](https://teos-10.github.io/GSW-Python/) (the Gibbs SeaWater Oceanographic Toolbox of TEOS-10) within Parcels to compute density from temperature and salinity fields. The `gsw` toolbox can be installed via `conda install gsw` or `pixi add gsw`." ] }, { @@ -33,7 +33,6 @@ "source": [ "import gsw # The package to calculate density from temperature, salinity and depth\n", "import numpy as np\n", - "import xarray as xr\n", "\n", "import parcels\n", "import parcels.tutorial\n", diff --git a/docs/user_guide/examples/tutorial_interaction.ipynb b/docs/user_guide/examples/tutorial_interaction.ipynb index 3020c8eb8e..0888e3c511 100644 --- a/docs/user_guide/examples/tutorial_interaction.ipynb +++ b/docs/user_guide/examples/tutorial_interaction.ipynb @@ -15,7 +15,11 @@ "id": "1", "metadata": {}, "source": [ - "In this tutorial, we show an example of the 'particle-particle-interaction' functionality in Parcels. Note that this functionality is still fairly rudimentary for now. Importantly, as particle-interaction is a many-body problem it scales as $N^2$ and can thus become very slow for large ParticleSets. There is currently **no advanced optimisation** (such as using techniques from [Smoothed Particle Hydrodynamics](https://en.wikipedia.org/wiki/Smoothed-particle_hydrodynamics)) built in to Parcels. " + "In this tutorial, we show an example of the 'particle-particle-interaction' functionality in Parcels. Note that this functionality is still fairly rudimentary for now. \n", + "\n", + "```{note}\n", + "Importantly, as particle-interaction is a many-body problem it scales as $N^2$ and can thus become very slow for large ParticleSets. There is currently **no advanced optimisation** (such as using techniques from [Smoothed Particle Hydrodynamics](https://en.wikipedia.org/wiki/Smoothed-particle_hydrodynamics)) built in to Parcels.\n", + "```" ] }, { @@ -26,7 +30,7 @@ "source": [ "## Pulling particles\n", "\n", - "Below is an example of what can be done with particle-particle interaction. We create a square grid of $N\\times N$ particles, which are all subject to Brownian Motion (via the built-in `DiffusionUniformKh` Kernel). Furthermore, two of the particles also 'attract' other particles that are within the interaction distance: these attracted particles move with a constant velocity to the attracting particles.\n" + "Below is an example of what can be done with particle-particle interaction. We create a square grid of $N\\times N$ particles, which are all subject to Brownian Motion (via the built-in {py:func}`parcels.kernels.DiffusionUniformKh` Kernel). Furthermore, two of the particles also 'attract' other particles that are within the interaction distance: these attracted particles move with a constant velocity to the attracting particles.\n" ] }, { diff --git a/docs/user_guide/examples/tutorial_manipulating_field_data.ipynb b/docs/user_guide/examples/tutorial_manipulating_field_data.ipynb index 5b0c8a8698..eefca052b2 100644 --- a/docs/user_guide/examples/tutorial_manipulating_field_data.ipynb +++ b/docs/user_guide/examples/tutorial_manipulating_field_data.ipynb @@ -13,7 +13,7 @@ "id": "1", "metadata": {}, "source": [ - "Because Parcels `Field` objects are built on top of `xarray` DataArrays, you can leverage the powerful data manipulation capabilities of `xarray` to preprocess or modify your field data before using it in particle simulations. This tutorial provides some common examples of how to leverage that power - however we highly recommend also reading the Xarray documentation!\n", + "Because {py:obj}`parcels.FieldSet` objects are built on top of `xarray` DataSets, you can leverage the powerful data manipulation capabilities of `xarray` to preprocess or modify your field data before using it in particle simulations. This tutorial provides some common examples of how to leverage that power - however we highly recommend also reading the Xarray documentation!\n", "\n", "Let's start with some imports." ] diff --git a/docs/user_guide/examples/tutorial_mitgcm.ipynb b/docs/user_guide/examples/tutorial_mitgcm.ipynb index 4135d0546d..1bdcf9e646 100644 --- a/docs/user_guide/examples/tutorial_mitgcm.ipynb +++ b/docs/user_guide/examples/tutorial_mitgcm.ipynb @@ -39,7 +39,7 @@ "id": "3", "metadata": {}, "source": [ - "We can use a combination of `parcels.convert.mitgcm_to_sgrid` and `FieldSet.from_sgrid_conventions` to read in the data. See below for an example." + "We can use a combination of {py:func}`parcels.convert.mitgcm_to_sgrid` and {py:func}`parcels.FieldSet.from_sgrid_conventions` to read in the data. See below for an example." ] }, { diff --git a/docs/user_guide/examples/tutorial_nemo.ipynb b/docs/user_guide/examples/tutorial_nemo.ipynb index 41cd84b7b9..f85195dc25 100644 --- a/docs/user_guide/examples/tutorial_nemo.ipynb +++ b/docs/user_guide/examples/tutorial_nemo.ipynb @@ -58,7 +58,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We use the `parcels.convert.nemo_to_sgrid()` function to convert the NEMO curvilinear grid to a dataset that is SGrid-compliant. We then pass this new dataset into `parcels.FieldSet.from_sgrid_conventions()`. This function automatically detects the C-grid structure and sets up the grid accordingly." + "We use the {py:func}`parcels.convert.nemo_to_sgrid()` function to convert the NEMO curvilinear grid to a dataset that is SGrid-compliant. We then pass this new dataset into {py:func}`parcels.FieldSet.from_sgrid_conventions()`. This function automatically detects the C-grid structure and sets up the grid accordingly." ] }, { @@ -300,7 +300,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We again use the combination of the `parcels.convert.nemo_to_sgrid` function and the `parcels.FieldSet.from_sgrid_conventions()` to create a FieldSet object with the correct `Grid` information for the NEMO data." + "We again use the combination of the {py:func}`parcels.convert.nemo_to_sgrid()` function and the {py:func}`parcels.FieldSet.from_sgrid_conventions()` to create a FieldSet object with the correct `Grid` information for the NEMO data." ] }, { @@ -330,7 +330,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The code below is an example of how to then create a 3D simulation with particles, starting on a meridional line through the North Sea from the mouth of the river Rhine at 100m depth, and advecting them through the North Sea using the `AdvectionRK2_3D`" + "The code below is an example of how to then create a 3D simulation with particles, starting on a meridional line through the North Sea from the mouth of the river Rhine at 100m depth, and advecting them through the North Sea using the `AdvectionRK2_3D` kernel" ] }, { diff --git a/docs/user_guide/examples/tutorial_nestedgrids.ipynb b/docs/user_guide/examples/tutorial_nestedgrids.ipynb index b6baf6e9e6..a695f41d10 100644 --- a/docs/user_guide/examples/tutorial_nestedgrids.ipynb +++ b/docs/user_guide/examples/tutorial_nestedgrids.ipynb @@ -289,7 +289,7 @@ "id": "18", "metadata": {}, "source": [ - "Then, we convert the triangulation into an (unstructured) `parcels.FieldSet`." + "Then, we convert the triangulation into an (unstructured) {py:obj}`parcels.FieldSet`." ] }, { diff --git a/docs/user_guide/examples/tutorial_peninsula_AvsCgrid.ipynb b/docs/user_guide/examples/tutorial_peninsula_AvsCgrid.ipynb index b1691e2f66..3cdb9f446f 100644 --- a/docs/user_guide/examples/tutorial_peninsula_AvsCgrid.ipynb +++ b/docs/user_guide/examples/tutorial_peninsula_AvsCgrid.ipynb @@ -11,7 +11,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The notebook below shows how sensitive trajectories are to the combination of grid and velocity interpolation schemes. We will use the idealised Peninsula flowfield and carefully compare how trajectories on the [A-grid and C-grid](https://docs.oceanparcels.org/en/latest/examples/documentation_indexing.html) version of this flow differ. To enhance the differences, we deliberately use a very coarse-resolution grid." + "The notebook below shows how sensitive trajectories are to the combination of grid and velocity interpolation schemes. We will use the idealised Peninsula flowfield and carefully compare how trajectories on the [A-grid and C-grid](./explanation_grids.ipynb) version of this flow differ. To enhance the differences, we deliberately use a very coarse-resolution grid." ] }, { @@ -76,7 +76,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Now we run six different experiments, each with a different combination of grid type (\"A\" or \"C\") and velocity interpolation scheme ([\"linear\"](https://docs.oceanparcels.org/en/latest/examples/tutorial_interpolation.html), [\"freeslip\"](https://docs.oceanparcels.org/en/latest/examples/documentation_unstuck_Agrid.html#3.-Slip-boundary-conditions), [\"cgrid_velocity\"](https://docs.oceanparcels.org/en/latest/examples/documentation_indexing.html)) # TODO also add [\"analytical\"](https://docs.oceanparcels.org/en/latest/examples/tutorial_analyticaladvection.html)." + "Now we run six different experiments, each with a different combination of grid type (\"A\" or \"C\") and velocity interpolation scheme ({py:obj}`parcels.interpolators.XLinear_Velocity`, {py:obj}`parcels.interpolators.XFreeslip`, and {py:obj}`parcels.interpolators.CGrid_Velocity`)." ] }, { @@ -154,7 +154,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We carefully compute the landmask (i.e. where the peninsula field has land), which is important for the interpretation of land that is consistent with Parcels (see also the [Preventing stuck particles tutorial](https://docs.oceanparcels.org/en/latest/examples/documentation_unstuck_Agrid.html#2.-Displacement))." + "We carefully compute the landmask (i.e. where the peninsula field has land), which is important for the interpretation of land that is consistent with Parcels (see also the {ref}`Preventing stuck particles tutorial `)." ] }, { diff --git a/docs/user_guide/examples/tutorial_sampling.ipynb b/docs/user_guide/examples/tutorial_sampling.ipynb index 53bbc82a56..430f29ebd0 100644 --- a/docs/user_guide/examples/tutorial_sampling.ipynb +++ b/docs/user_guide/examples/tutorial_sampling.ipynb @@ -8,7 +8,7 @@ "# πŸ–₯️ Sampling a Field\n", "The particle trajectories allow us to study fields like temperature, plastic concentration or chlorophyll from a Lagrangian perspective.\n", "\n", - "In this tutorial we will go through how particles can sample `Fields`, using temperature as an example. Along the way we will get to know the Parcels class `Variable` (see [here](https://parcels.readthedocs.io/en/latest/reference/particles.html#parcels.particle.Variable) for the documentation) and some of its methods. This tutorial covers several applications of a sampling setup:\n", + "In this tutorial we will go through how particles can sample a {py:obj}`parcels.Field`, using temperature as an example. Along the way we will get to know the {py:obj}`parcels.Variable` class and some of its methods. This tutorial covers several applications of a sampling setup:\n", "\n", "- [**Basic along trajectory sampling**](#basic-sampling)\n", "- [**Sampling initial Field values**](#sampling-initial-field-values)\n", @@ -320,7 +320,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Instead, you should use the code `u, v = fieldset.UV[...]`. With this code, the sampling is consistent with the actual velocity fields used in the advection Kernels. The difference is that on a curvilinear grid, `fieldset.U[..]` returns the velocity in the `i`-direction (the columns on the grid), while `fieldset.UV[...]` returns the velocities in the longitude and latitude direction. Furthermore, only `fieldset.UV[...]` sampling can correctly deal with boundary conditions such as `freeslip` and `partialslip` ([tutorial_unstuck_Agrid](tutorial_unstuck_Agrid.ipynb))\n" + "Instead, you should use the code `u, v = fieldset.UV[...]`. With this code, the sampling is consistent with the actual velocity fields used in the advection Kernels. The difference is that on a curvilinear grid, `fieldset.U[..]` returns the velocity in the `i`-direction (the columns on the grid), while `fieldset.UV[...]` returns the velocities in the longitude and latitude direction - see also the [velocity conversion tutorial](./tutorial_velocityconversion.ipynb). Furthermore, only `fieldset.UV[...]` sampling can correctly deal with boundary conditions such as `freeslip` and `partialslip` ([tutorial_unstuck_Agrid](tutorial_unstuck_Agrid.ipynb))\n" ] }, { @@ -381,7 +381,7 @@ "source": [ "
\n", "\n", - "Note that the Kernels above return the value of `U` and `V` in the units of the grid. That means that for a spherical grid, the velocities are in **degrees/s**. To convert these to **m/s**, see the [UnitConversion tutorial](https://docs.oceanparcels.org/en/latest/examples/tutorial_unitconverters.html).\n", + "Note that the Kernels above return the value of `U` and `V` in the units of the grid. That means that for a spherical grid, the velocities are in **degrees/s**. To convert these to **m/s**, see the [velocity conversion tutorial](./tutorial_velocityconversion.ipynb).\n", "
" ] } diff --git a/docs/user_guide/examples/tutorial_schism.ipynb b/docs/user_guide/examples/tutorial_schism.ipynb index b060fc7a38..b810792ed3 100644 --- a/docs/user_guide/examples/tutorial_schism.ipynb +++ b/docs/user_guide/examples/tutorial_schism.ipynb @@ -66,7 +66,7 @@ "* `horizontalVelX`, `horizontalVelY`: the 3D horizontal velocity components, defined at the mesh nodes\n", " over 32 vertical layers.\n", "\n", - "As in the [quickstart](../getting_started/tutorial_quickstart.md), `parcels.tutorial.open_dataset`\n", + "As in the [quickstart](../getting_started/tutorial_quickstart.md), {py:func}`parcels.tutorial.open_dataset()`\n", "downloads the files into a local cache on first use (subsequent calls return the cached copy) and opens\n", "them as `xarray` datasets:" ] @@ -154,10 +154,7 @@ " takes a single 1D column of layer-interface depths that applies everywhere on the mesh. We therefore\n", " supply a fictitious 1D vertical grid. This is adequate for the near-surface, horizontal transport\n", " shown here (the lateral interpolation does not depend on the vertical grid); accurate full-depth 3D\n", - " transport on an LSC2 grid would require Parcels to support a laterally varying vertical coordinate.\n", - "\n", - "We also call `.load()` so the velocities sit in memory; otherwise every interpolation step re-reads\n", - "from disk and the simulation is extremely slow." + " transport on an LSC2 grid would require Parcels to support a laterally varying vertical coordinate." ] }, { @@ -174,10 +171,10 @@ "zc = 0.5 * (zf[1:] + zf[:-1])\n", "\n", "rename = {\"nSCHISM_vgrid_layers\": \"zf\", \"nSCHISM_hgrid_node\": \"n_node\"}\n", - "U = (\n", - " u.isel(nSCHISM_vgrid_layers=slice(None, None, -1)).rename(rename).load()\n", + "U = u.isel(nSCHISM_vgrid_layers=slice(None, None, -1)).rename(\n", + " rename\n", ") # reverse to surface-first\n", - "V = v.isel(nSCHISM_vgrid_layers=slice(None, None, -1)).rename(rename).load()\n", + "V = v.isel(nSCHISM_vgrid_layers=slice(None, None, -1)).rename(rename)\n", "\n", "uxds = ux.UxDataset(\n", " xr.Dataset(\n", @@ -199,11 +196,13 @@ "source": [ "## Build the `FieldSet`\n", "\n", - "With the mesh and UGRID-compliant dimensions in place, `parcels.FieldSet.from_ugrid_conventions` builds\n", + "With the mesh and UGRID-compliant dimensions in place, {py:func}`parcels.FieldSet.from_ugrid_conventions()` builds\n", "the `FieldSet`. It detects `U` and `V`, attaches the `UxGrid`, and selects the `UxLinearNodeLinearZF`\n", "interpolator (barycentric in the horizontal, linear in the vertical) because the velocities are\n", "node-registered along the layer interfaces `zf`. We pass `mesh=\"flat\"` because the coordinates\n", - "are projected meters: velocities in m/s then advect positions in meters directly." + "are projected meters: velocities in m/s then advect positions in meters directly.\n", + "\n", + "We convert the `FieldSet` to windowed arrays with {py:func}`parcels.FieldSet.to_windowed_arrays()` for [better performance](./explanation_performance.md) when advecting many particles." ] }, { @@ -215,6 +214,8 @@ "source": [ "fieldset = parcels.FieldSet.from_ugrid_conventions(uxds, mesh=\"flat\")\n", "\n", + "fieldset.to_windowed_arrays()\n", + "\n", "fieldset.describe()" ] }, @@ -227,9 +228,9 @@ "\n", "Because SCHISM's LSC2 grid keeps below-seabed levels as `NaN`, a particle whose (fixed) depth ends up\n", "deeper than the local water column will sample `NaN`. Rather than feed it a fabricated velocity, we let\n", - "Parcels flag it: sampling `NaN` sets the particle state to `ErrorInterpolation`, and ending up outside\n", - "the mesh sets `ErrorOutOfBounds`. We add a small kernel that runs after advection, records that the\n", - "particle went out of bounds, and sets its state to `Delete` so it stops being advected (its trajectory\n", + "Parcels flag it: sampling `NaN` sets the particle state to {py:obj}`parcels.StatusCode.ErrorInterpolation`, and ending up outside\n", + "the mesh sets {py:obj}`parcels.StatusCode.ErrorOutOfBounds`. We add a small kernel that runs after advection, records that the\n", + "particle went out of bounds, and sets its state to {py:obj}`parcels.StatusCode.Delete` so it stops being advected (its trajectory\n", "up to that point is kept).\n", "\n", "```{note}\n", diff --git a/docs/user_guide/examples/tutorial_stuck_particles.ipynb b/docs/user_guide/examples/tutorial_stuck_particles.ipynb index 10711cdb8a..7875774a68 100644 --- a/docs/user_guide/examples/tutorial_stuck_particles.ipynb +++ b/docs/user_guide/examples/tutorial_stuck_particles.ipynb @@ -9,7 +9,7 @@ } }, "source": [ - "# πŸ“– Stuck particles\n" + "# πŸ“– Why particles can get stuck\n" ] }, { @@ -114,7 +114,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "hide_input": true + "hide_input": true, + "tags": [ + "hide-input" + ] }, "outputs": [], "source": [ @@ -190,7 +193,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "hide-input" + ] + }, "outputs": [], "source": [ "fig, axes = plt.subplots(1, 3, figsize=(15, 5), constrained_layout=True)\n", @@ -341,7 +348,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "hide_input": true + "hide_input": true, + "tags": [ + "hide-input" + ] }, "outputs": [], "source": [ @@ -518,7 +528,10 @@ "hide_input": true, "slideshow": { "slide_type": "subslide" - } + }, + "tags": [ + "hide-input" + ] }, "outputs": [], "source": [ @@ -609,7 +622,7 @@ "\n", "The cross-boundary component of velocity, defined at the cell faces where the boundary is defined, is set to zero at all boundaries. See the figure below. This corresponds exactly with the velocity field in a Parcels `FieldSet` shown in **figure 3**.\n", "\n", - "\n" + "\n" ] }, { @@ -619,7 +632,7 @@ "source": [ "There are different options for the along-boundary velocity in NEMO: a free-slip condition, a partial-slip condition and a no-slip condition. Since the tangential velocity is not defined at the boundary, this boundary condition is defined by a Neumann boundary condition: the normal derivative of the along-boundary velocity is specified. This derivative is schematically represented by a \"ghost\" velocity on the adjacent land node. The specified derivative is equivalent to what would result from the central difference between the along-boundary velocity at the nearest ocean cell and this \"ghost\" velocity. The type of boundary condition defines the direction and magnitude of this \"ghost\" velocity relative to the along-boundary velocity in the fluid domain. In Parcels, these \"ghost\" velocities may be used to determine how the velocity should be interpolated near the coast. By default, Parcels interpolates piecewise-constant in the direction normal to the velocity component. This means that the along-boundary velocity is the same for any distance away from the boundary and therefore equivalent to the free-slip boundary condition shown in subfigure **(a)** below.\n", "\n", - "\n" + "\n" ] }, { @@ -700,7 +713,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "hide_input": true + "hide_input": true, + "tags": [ + "hide-input" + ] }, "outputs": [], "source": [ @@ -845,7 +861,10 @@ "cell_type": "code", "execution_count": null, "metadata": { - "hide_input": true + "hide_input": true, + "tags": [ + "hide-input" + ] }, "outputs": [], "source": [ diff --git a/docs/user_guide/examples/tutorial_unstuck_Agrid.ipynb b/docs/user_guide/examples/tutorial_unstuck_Agrid.ipynb index 7b439f9b99..cb3a01bb55 100644 --- a/docs/user_guide/examples/tutorial_unstuck_Agrid.ipynb +++ b/docs/user_guide/examples/tutorial_unstuck_Agrid.ipynb @@ -172,7 +172,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "hide-input" + ] + }, "outputs": [], "source": [ "fig, axes = plt.subplots(1, 2, figsize=(10, 5), constrained_layout=True)\n", @@ -350,7 +354,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "hide-input" + ] + }, "outputs": [], "source": [ "fig, axes = plt.subplots(1, 2, figsize=(10, 5), constrained_layout=True)\n", @@ -478,7 +486,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "hide-input" + ] + }, "outputs": [], "source": [ "fig, ax = plt.subplots(1, 1, figsize=(7, 6), constrained_layout=True)\n", @@ -561,7 +573,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "hide-input" + ] + }, "outputs": [], "source": [ "fig, ax = plt.subplots(1, 1, figsize=(6, 5), constrained_layout=True)\n", @@ -767,7 +783,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "hide-input" + ] + }, "outputs": [], "source": [ "fig, ax = plt.subplots(1, 1, figsize=(6, 5), constrained_layout=True)\n", @@ -832,20 +852,23 @@ "(slip-boundary-conditions)=\n", "## 3. Slip boundary conditions\n", "\n", - "The reason trajectories do not neatly follow the coast in A grid velocity fields is that the lack of staggering causes both velocity components to go to zero in the same way towards the cell edge. This no-slip condition can be turned into a free-slip or partial-slip condition by separately considering the cross-shore and along-shore velocity components as in [a staggered C-grid](https://docs.oceanparcels.org/en/latest/examples/documentation_stuck_particles.html#2.-C-grids). Each interpolation of the velocity field must then be corrected with a factor depending on the direction of the boundary.\n", + "The reason trajectories do not neatly follow the coast in A grid velocity fields is that the lack of staggering causes both velocity components to go to zero in the same way towards the cell edge. This no-slip condition can be turned into a free-slip or partial-slip condition by separately considering the cross-shore and along-shore velocity components as in the C-grids part of the {ref}`Stuck particles ` tutorial. Each interpolation of the velocity field must then be corrected with a factor depending on the direction of the boundary.\n", "\n", - "
\n", + "```{note}\n", + "While the code below has been developed for A-grids, we have experience that it can also work well with B-grids. So if you encounter stuck particles in a B-grid model then it might be worth to use the {py:obj}`parcels.interpolators.XPartialslip()` or {py:obj}`parcels.interpolators.XFreeslip()` described below.\n", + "```\n", "\n", - "__Note__ that while the code below has been developed for A-grids, we have experience that it can also work well with B-grids. So if you encounter stuck particles in a B-grid model then it might be worth to use the `interp_method=parcels.interpolators.XPartialslip()` or `interp_method=parcels.interpolators.Xfreeslip()` solutions described below.\n", - "
\n", - "\n", - "These boundary conditions have been implemented in Parcels as `interp_method=parcels.interpolators.XPartialslip()` and `interp_method=parcels.interpolators.Xfreeslip()`, which we will show in the plot below\n" + "These boundary conditions have been implemented in Parcels as {py:obj}`parcels.interpolators.XPartialslip()` and {py:obj}`parcels.interpolators.XFreeslip()`, which we will show in the plot below\n" ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "hide-input" + ] + }, "outputs": [], "source": [ "cells_x = np.array([[0, 0], [1, 1], [2, 2]])\n", @@ -927,7 +950,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Consider a grid cell with a solid boundary to the right and vectors $(U0, V0)$ = $(1, 1)$ on the left-hand nodes, as in **figure 6**. \n", + "Consider a grid cell with a solid boundary to the right and vectors $(U0, V0)$ = $(1, 1)$ on the left-hand nodes, as in **figure 6**.\n", "Parcels bilinear interpolation will interpolate in the $x$ and $y$ directions. This cell is invariant in the $y$-direction, we will only consider the effect in the direction normal to the boundary. In the x-direction, both u and v will be interpolated along $\\xi$, the normalized $x$-coordinate within the cell. This is plotted with the blue and orange dashed lines in **figure 6A**.\n", "\n", "A free slip boundary condition is defined with $\\frac{\\delta v}{\\delta \\xi}=0$. This means that the tangential velocity is constant in the direction normal to the boundary. This can be achieved in a kernel after interpolation by dividing by $(1-\\xi)$. The resulting velocity profiles are shown in **figure 6B**.\n", @@ -1048,7 +1071,11 @@ { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "tags": [ + "hide-input" + ] + }, "outputs": [], "source": [ "df_freeslip = parcels.read_particlefile(\"freeslip_particles.parquet\")\n", diff --git a/docs/user_guide/examples/tutorial_velocityconversion.ipynb b/docs/user_guide/examples/tutorial_velocityconversion.ipynb index e75a686425..ee5b32c805 100644 --- a/docs/user_guide/examples/tutorial_velocityconversion.ipynb +++ b/docs/user_guide/examples/tutorial_velocityconversion.ipynb @@ -15,9 +15,9 @@ "source": [ "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", "\n", - "In all cases, velocities are given in m s-1. But for advection (such as `particles.dlat = v * particles.dt`), the velocity needs to be in degrees s-1. \n", + "In all cases, velocities are given in m s-1. But for advection (such as `particles.dy = v * particles.dt`), the velocity needs to be in degrees s-1. \n", "\n", - "Parcels seamlessly converts between meters s-1 and degrees s-1, under the hood. For transparency, this guide explains how this works.\n" + "Parcels seamlessly converts between meters s-1 and degrees s-1, under the hood. For transparency, this guide explains how this works." ] }, { @@ -185,7 +185,7 @@ "\n", "If you want to work with diffusivity in degrees2 s-1 (for example to move particles using a random walk), you will have to convert this yourself in your kernel. \n", "\n", - "Note that for the built-in `DiffusionUniformKh`, `AdvectionDiffusionM1` and `AdvectionDiffusionEM`, the conversion is done automatically." + "Note that for the built-in {py:obj}`parcels.kernels.DiffusionUniformKh`, {py:obj}`parcels.kernels.AdvectionDiffusionM1` and {py:obj}`parcels.kernels.AdvectionDiffusionEM`, the conversion is done automatically." ] } ], diff --git a/docs/user_guide/examples/tutorial_write_in_kernel.ipynb b/docs/user_guide/examples/tutorial_write_in_kernel.ipynb index ede2c0823d..2602c0824b 100644 --- a/docs/user_guide/examples/tutorial_write_in_kernel.ipynb +++ b/docs/user_guide/examples/tutorial_write_in_kernel.ipynb @@ -13,7 +13,7 @@ "id": "1", "metadata": {}, "source": [ - "The typical way to write particle data to a file is by specifying the `output_file` argument in a `pset.execute()` call. This will write to a file on a regular interval, specified in the `outputdt` argument of the `ParticleFile`. \n", + "The typical way to write particle data to a file is by specifying the `output_file` argument in a `pset.execute()` call. This will write to a file on a regular interval, specified in the `outputdt` argument of the {py:obj}`parcels.ParticleFile`. \n", "\n", "However, sometimes you may want more control of _when_ to write particle data to a file. For example, you may want to write when a particle is deleted, or only when it is in a certain region or when other conditions are met. This can be especially useful if you want to do connectivity studies and don't care about having the full trajectory of each particle available. \n", "\n", diff --git a/docs/user_guide/getting_started/explanation_concepts.md b/docs/user_guide/getting_started/explanation_concepts.md index 39a9d256c7..72f378ddfb 100644 --- a/docs/user_guide/getting_started/explanation_concepts.md +++ b/docs/user_guide/getting_started/explanation_concepts.md @@ -28,31 +28,33 @@ Parcels concepts diagram with key classes in blue boxes ## 1. FieldSet -Parcels provides a framework to simulate particles **within a set of fields**, such as flow velocities and temperature. To start a Parcels simulation we must define this dataset with the **`parcels.FieldSet`** class. +Parcels provides a framework to simulate particles **within a set of fields**, such as flow velocities and temperature. To start a Parcels simulation we must define this dataset with the **{py:obj}`parcels.FieldSet`** class. -The input dataset from which to create a `parcels.FieldSet` can be an [`xarray.Dataset`](https://docs.xarray.dev/en/stable/user-guide/data-structures.html#dataset) with output from a hydrodynamic model or reanalysis. Such a dataset usually contains a number of gridded variables (e.g. `"U"`), which in Parcels become `parcels.Field` objects. A list of `parcels.Field` objects is stored in a `parcels.FieldSet` in an analoguous way to how `xarray.DataArray` objects combine to make an `xarray.Dataset`. +The input dataset from which to create a {py:obj}`parcels.FieldSet` can be an [`xarray.Dataset`](https://docs.xarray.dev/en/stable/user-guide/data-structures.html#dataset) with output from a hydrodynamic model or reanalysis. Such a dataset usually contains a number of gridded variables (e.g. `"U"`), which in Parcels become {py:obj}`parcels.Field` objects. A set of {py:obj}`parcels.Field` objects is stored in a {py:obj}`parcels.FieldSet` in an analoguous way to how `xarray.DataArray` objects combine to make an `xarray.Dataset`. For several common input datasets, such as the Copernicus Marine Service analysis products, Parcels has a specific method to read and parse the data correctly: ```python -dataset = xr.open_mfdataset("insert_copernicus_data_files.nc") +ds_fields = xr.open_mfdataset("insert_copernicus_data_files.nc") fields = {"U": ds_fields["uo"], "V": ds_fields["vo"]} ds_fset = parcels.convert.copernicusmarine_to_sgrid(fields=fields) fieldset = parcels.FieldSet.from_sgrid_conventions(ds_fset) ``` -In some cases, we might want to combine fields from different sources in the same `parcels.FieldSet`, such as ocean currents from one dataset and Stokes drift from another. This is possible in Parcels by creating multiple `parcels.FieldSet` objects and combining them into a single `parcels.FieldSet`: +In some cases, we might want to combine fields from different sources in the same {py:obj}`parcels.FieldSet`, such as ocean currents from one dataset and Stokes drift from another. This is possible in Parcels by creating multiple {py:obj}`parcels.FieldSet` objects and combining them into a single {py:obj}`parcels.FieldSet`: ```python -dataset2 = xr.open_dataset("insert_stokes_data_files.nc") -fields2 = {"Ustokes": ds_fields["ustokes"], "Vstokes": ds_fields["vstokes"]} +ds_fields2 = xr.open_dataset("insert_stokes_data_files.nc") +fields2 = {"Ustokes": ds_fields2["ustokes"], "Vstokes": ds_fields2["vstokes"]} ds_fset = parcels.convert.copernicusmarine_to_sgrid(fields=fields2) fieldset += parcels.FieldSet.from_sgrid_conventions(ds_fset, vector_fields={"UVstokes": ["Ustokes", "Vstokes"]}) ``` ### Grid -Each `parcels.Field` is defined on a grid. With Parcels, we can simulate particles in fields on both structured (**`parcels.XGrid`**) and unstructured (**`parcels.UxGrid`**) grids. The grid is defined by the coordinates of grid cell nodes, edges, and faces. `parcels.XGrid` objects are based on Xarray Datasets with attached SGRID metadata, while `parcels.UxGrid` objects are based on [`uxarray.Grid`](https://uxarray.readthedocs.io/en/stable/generated/uxarray.Grid.html#uxarray.Grid) objects. +Each {py:obj}`parcels.Field` is defined on a grid. With Parcels, we can simulate particles in fields on both structured (**{py:obj}`parcels.XGrid`**) and unstructured (**{py:obj}`parcels.UxGrid`**) grids. The grid is defined by the coordinates of grid cell nodes, edges, and faces. {py:obj}`parcels.XGrid` objects are based on Xarray Datasets with attached SGRID metadata, while {py:obj}`parcels.UxGrid` objects are based on [`uxarray.Grid`](https://uxarray.readthedocs.io/en/stable/generated/uxarray.Grid.html#uxarray.Grid) objects. + +The user doesn't need to manually construct or manage grids - this is done internally by Parcels. When using {py:obj}`parcels.FieldSet.from_sgrid_conventions()` or {py:obj}`parcels.FieldSet.from_ugrid_conventions()` the correct grid object is constructed for your data. ```{admonition} πŸ“– Read more about grids :class: seealso @@ -61,7 +63,7 @@ Each `parcels.Field` is defined on a grid. With Parcels, we can simulate particl ### Interpolation -To find the value of a `parcels.Field` at any particle location, Parcels interpolates the gridded field. Depending on the variable, grid, and required accuracy, different interpolation methods may be appropriate. Parcels comes with a number of built-in **`parcels.interpolators`**. +To find the value of a {py:obj}`parcels.Field` at any particle location, Parcels interpolates the gridded field. Depending on the variable, grid, and required accuracy, different interpolation methods may be appropriate. Parcels comes with a number of built-in **{py:obj}`parcels.interpolators`**. ```{admonition} πŸ“– Read more about interpolation :class: seealso @@ -75,11 +77,11 @@ To find the value of a `parcels.Field` at any particle location, Parcels interpo ## 2. ParticleSet -Once the environment has a `parcels.FieldSet` object, you can start defining your particles in a **`parcels.ParticleSet`** object. This object requires: +Once the environment has a {py:obj}`parcels.FieldSet` object, you can start defining your particles in a **{py:obj}`parcels.ParticleSet`** object. This object requires: -1. The `parcels.FieldSet` object in which the particles will be released. -2. The type of `parcels.Particle`: A default `Particle` or a custom `Particle`-type with additional `Variable`s (see the [custom kernel example](custom-kernel)). -3. Initial conditions for each `Variable` defined in the `Particle`, most notably the release coordinates of `t`, `z`, `y` and `x`. +1. The {py:obj}`parcels.FieldSet` object in which the particles will be released. +2. The type of {py:obj}`parcels.Particle`: A default `Particle` or a custom `Particle`-type with additional {py:obj}`parcels.Variable`s (see the [custom kernel example](custom-kernel)). +3. Initial conditions for each {py:obj}`parcels.Variable` defined in the {py:obj}`parcels.Particle`, most notably the release coordinates of `t`, `z`, `y` and `x`. ```python t = np.array([0]) @@ -99,7 +101,7 @@ pset = parcels.ParticleSet(fieldset=fieldset, pclass=parcels.Particle, t=t, z=z, ## 3. Kernels -A **`parcels.Kernel`** object is a little snippet of code, which is applied to the particles in the `ParticleSet`, for every time step during a simulation. Kernels define the computation or numerical integration done by Parcels, and can represent many processes such as advection, ageing, growth, or simply the sampling of a field. +A **`kernel`** function is a little snippet of code, which is applied to the particles in the {py:obj}`parcels.ParticleSet`, for every time step during a simulation. Kernels define the computation or numerical integration done by Parcels, and can represent many processes such as advection, ageing, growth, or simply the sampling of a field. Advection of a particle by the flow, the change in position $\mathbf{x}(t) = (x(t), y(t))$ at time $t$, can be described by the equation: @@ -121,7 +123,7 @@ def AdvectionEE(particles, fieldset): particles.dy += v1 * particles.dt ``` -Basic kernels are included in Parcels to compute advection and diffusion. The standard advection kernel is `parcels.kernels.AdvectionRK2`, a [second-order Runge-Kutta integrator](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#The_Runge%E2%80%93Kutta_method) of the advection function. +Basic kernels are included in Parcels to compute advection and diffusion. The standard advection kernel is {py:obj}`parcels.kernels.AdvectionRK2`, a [second-order Runge-Kutta integrator](https://en.wikipedia.org/wiki/Runge%E2%80%93Kutta_methods#The_Runge%E2%80%93Kutta_method) of the advection function. ```{warning} It is advised _not_ to update the particle coordinates (`particles.t`, `particles.z`, `particles.y`, or `particles.x`) directly within a Kernel, as that can negatively interfere with the way that particle movements by different kernels are vectorially added. Use a change in the coordinates: `particles.dy`, `particles.dx` and/or `particles.dz`. Read the [kernel loop tutorial](../examples/explanation_kernelloop.md) to understand why. @@ -167,7 +169,7 @@ We have to be careful with kernels that sample velocities on "spherical" grids ( ## 4. Execute -The execution of the simulation is done using the method **`parcels.ParticleSet.execute()`**, given the `FieldSet`, `ParticleSet`, and `Kernels` defined in the previous steps. This method requires the following arguments: +The execution of the simulation is done using the method **{py:func}`parcels.ParticleSet.execute()`**, given the `FieldSet`, `ParticleSet`, and `Kernels` defined in the previous steps. This method requires the following arguments: 1. The kernels to be executed. 2. The `runtime` defining how long the execution loop runs. Alternatively, you may define the `endtime` at which the execution loop stops. @@ -184,7 +186,7 @@ pset.execute(kernels=kernels, dt=dt, runtime=runtime) ### Output -To analyse the particle data generated in the simulation, we need to define a `parcels.ParticleFile` and add it as an argument to `parcels.ParticleSet.execute()`. The output will be written in a [parquet format](https://parquet.apache.org/), which can be opened as a `polars.DataFrame`. The dataset will contain the particle data with at least `t`, `z`, `y` and `x`, for each particle at timesteps defined by the `outputdt` argument. +To analyse the particle data generated in the simulation, we need to define a {py:obj}`parcels.ParticleFile` and add it as an argument to {py:func}`parcels.ParticleSet.execute()`. The output will be written in a [parquet format](https://parquet.apache.org/), which can be opened as a `polars.DataFrame`. The dataset will contain the particle data with at least `t`, `z`, `y` and `x`, for each particle at timesteps defined by the `outputdt` argument. There are many ways to analyze particle output, and although we provide [a short tutorial to get started](./tutorial_output.ipynb), we recommend writing your own analysis code and checking out [related Lagrangian analysis projects in our community page](../../community/index.md#analysis-code). diff --git a/docs/user_guide/getting_started/tutorial_output.ipynb b/docs/user_guide/getting_started/tutorial_output.ipynb index 6bcb0ebf74..7c4775f05b 100644 --- a/docs/user_guide/getting_started/tutorial_output.ipynb +++ b/docs/user_guide/getting_started/tutorial_output.ipynb @@ -45,7 +45,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "First we need to create some Parcels output to analyze. We simulate a set of particles using the setup described in the [Delay start tutorial](https://docs.oceanparcels.org/en/latest/examples/tutorial_delaystart.html). We will also add some user defined metadata to the output file." + "First we need to create some Parcels output to analyze. We simulate a set of particles using the setup described in the [Delay start tutorial](../examples/tutorial_delaystart.ipynb). We will also add some user defined metadata to the output file." ] }, { @@ -145,7 +145,7 @@ "```{note}\n", "As of Parcels v4, the default output format is [`parquet`](https://parquet.apache.org/) (instead of `zarr`). The `parquet` output format is a tabular format, in which every row corresponds to an observation of a particle trajectory. The `zarr` output format is a multidimensional array format, in which the data is stored in a 2D array with dimensions `traj` and `obs`. The `parquet` format is more compact and faster to read.\n", "\n", - "However, the `parquet` format does not support the [CF-convention for trajectories data](http://cfconventions.org/cf-conventions/v1.6.0/cf-conventions.html#_multidimensional_array_representation_of_trajectories) implemented with the [NCEI trajectory template](https://www.ncei.noaa.gov/data/oceans/ncei/formats/netcdf/v2.0/trajectoryIncomplete.cdl). We have implemented a `parcels.read_particlefile()` function to facilitate reading `parquet` output files, see more information below.\n", + "However, the `parquet` format does not support the [CF-convention for trajectories data](http://cfconventions.org/cf-conventions/v1.6.0/cf-conventions.html#_multidimensional_array_representation_of_trajectories) implemented with the [NCEI trajectory template](https://www.ncei.noaa.gov/data/oceans/ncei/formats/netcdf/v2.0/trajectoryIncomplete.cdl). We have implemented a {py:func}`parcels.read_particlefile()` function to facilitate reading `parquet` output files, see more information below.\n", "\n", "There are also utilities to convert to v3 ParticleFile output - see later in this tutorial.\n", "```" @@ -195,7 +195,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As you may have noticed above, the `t` is shown as a `float64` (in seconds) in `df_polars`. That is because `polars.read_parquet` does not automatically convert the cftime. To handle this, we also provide a helper function `parcels.read_particlefile` to read ParticleFiles, which does automatically convert the cftime. " + "As you may have noticed above, the `t` is shown as a `float64` (in seconds) in `df_polars`. That is because `polars.read_parquet` does not automatically convert the cftime. To handle this, we also provide a helper function {py:func}`parcels.read_particlefile()` to read ParticleFiles, which does automatically convert the cftime. " ] }, { @@ -516,7 +516,7 @@ "metadata": { "celltoolbar": "Metagegevens bewerken", "kernelspec": { - "display_name": "Python 3", + "display_name": "Parcels:test (3.14.6)", "language": "python", "name": "python3" }, diff --git a/docs/user_guide/getting_started/tutorial_quickstart.md b/docs/user_guide/getting_started/tutorial_quickstart.md index 467063c5ac..d5922673af 100644 --- a/docs/user_guide/getting_started/tutorial_quickstart.md +++ b/docs/user_guide/getting_started/tutorial_quickstart.md @@ -6,14 +6,14 @@ kernelspec: # πŸŽ“ Quickstart tutorial -Welcome to the **Parcels** quickstart tutorial, in which we will go through all the necessary steps to run a simulation. +Welcome to the Parcels quickstart tutorial, in which we will go through all the necessary steps to run a simulation. The code in this notebook can be used as a starting point to run Parcels in your own environment. Along the way we will familiarize ourselves with some specific classes and methods. If you are ever confused about one of these and want to read more, we have a [concepts overview](./explanation_concepts.md) discussing them in more detail. Let's dive in! ## Imports -Parcels depends on `xarray`, expecting inputs in the form of [`xarray.Dataset`](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html). Output files can be read with `polars`. +Parcels depends on `xarray`, expecting inputs in the form of [`xarray.Dataset`](https://docs.xarray.dev/en/stable/generated/xarray.Dataset.html). Output files can be read with [Polars](https://pola.rs). ```{code-cell} import numpy as np @@ -40,7 +40,7 @@ As we can see, the reanalysis dataset contains eastward velocity `uo`, northward These hydrodynamic fields need to be stored in a {py:obj}`parcels.FieldSet` object. Parcels provides tooling to parse many types of models or observations into such a `parcels.FieldSet` object. This is done in a two-step approach. -First, we convert the dataset into an SGRID-compliant dataset, for example by using a version of `parcels.convert._to_sgrid()`. Then, we create the `parcels.FieldSet` from the SGRID-compliant dataset using `parcels.FieldSet.from_sgrid_conventions()`. +First, we convert the dataset into an SGRID-compliant dataset, for example by using a version of `parcels.convert._to_sgrid()`. Then, we create the {py:obj}`parcels.FieldSet` from the SGRID-compliant dataset using {py:func}`parcels.FieldSet.from_sgrid_conventions()`. Below, we use a combination of {py:func}`parcels.convert.copernicusmarine_to_sgrid()` and {py:func}`parcels.FieldSet.from_sgrid_conventions()`, providing the names of the velocity fields in the dataset in the dictionary `fields`: @@ -50,14 +50,14 @@ ds_fset = parcels.convert.copernicusmarine_to_sgrid(fields=fields) fieldset = parcels.FieldSet.from_sgrid_conventions(ds_fset) ``` -Now, in order to improve performance, we can convert the `parcels.FieldSet` to windowed arrays. This is especially useful for large datasets with many timeslices, as it allows Parcels to load only the necessary timeslices into memory during the simulation. For more information about squeezing performance out of Parcels, see the [performance tutorial](../examples/explanation_performance.md). +Now, in order to improve performance, we can convert the {py:obj}`parcels.FieldSet` to windowed arrays. This is especially useful for large datasets with many timeslices, as it allows Parcels to load only the necessary timeslices into memory during the simulation. For more information about squeezing performance out of Parcels, see the [performance tutorial](../examples/explanation_performance.md). ```{code-cell} # Convert the FieldSet to windowed arrays for better performance fieldset = fieldset.to_windowed_arrays() ``` -You can inspect the `parcels.FieldSet` object with the `describe` method in order to see which `parcels.Field`s are included, and which grid and interpolation method is used for each field. This also gives information on the type of mesh and the time interval of the `parcels.FieldSet`: +You can inspect the {py:obj}`parcels.FieldSet` object with the `describe` method in order to see which {py:obj}`parcels.Field`s are included, and which grid and interpolation method is used for each field. This also gives information on the type of mesh and the time interval of the {py:obj}`parcels.FieldSet`: ```{code-cell} fieldset.describe() @@ -72,11 +72,11 @@ velocity = ds_fields.isel(time=0, depth=0).plot.quiver(x="longitude", y="latitud ## Input virtual particles: `ParticleSet` -Now that we have created a `parcels.FieldSet` object from the hydrodynamic data, we need to provide our second input: +Now that we have created a {py:obj}`parcels.FieldSet` object from the hydrodynamic data, we need to provide our second input: the virtual particles for which we will calculate the trajectories. -We need to create a {py:obj}`parcels.ParticleSet` object with the particles' initial time and position. The `parcels.ParticleSet` -object also needs to know about the `FieldSet` in which the particles "live". Finally, we need to specify the type of +We need to create a {py:obj}`parcels.ParticleSet` object with the particles' initial time and position. The {py:obj}`parcels.ParticleSet` +object also needs to know about the {py:obj}`parcels.FieldSet` in which the particles "live". Finally, we need to specify the type of {py:obj}`parcels.ParticleClass` we want to use. The default particles have `t`, `z`, `y`, and `x`, but you can easily add other {py:obj}`parcels.Variable`s such as size, temperature, or age to create your own particles to mimic plastic or an [ARGO float](../examples/tutorial_Argofloats.ipynb). @@ -94,13 +94,6 @@ pset = parcels.ParticleSet( ) ``` -Again, you can inspect the `pset` by printing it: - -```{code-cell} -:tags: [hide-output] -print(pset) -``` - And you can plot the particles on top of the temperature and velocity field: ```{code-cell} @@ -110,7 +103,7 @@ ax = temperature.axes ax.scatter(lon, lat, s=40, c='w', edgecolors='r'); ``` -If you also want to sample the initial value of a field, you can do so by calling the field with the `ParticleSet` as an argument. For example, in the code above you would add one line (assuming that the ParticleSet has a variable `temperature`) to sample the initial temperature values, before calling `pset.execute(...)`. +If you also want to sample the initial value of a field, you can do so by calling the field with the {py:obj}`parcels.ParticleSet` as an argument. For example, in the code above you would add one line (assuming that the ParticleSet has a variable `temperature`) to sample the initial temperature values, before calling `pset.execute(...)`. ```python pset.temperature = fieldset.thetao[pset] @@ -121,7 +114,7 @@ See the [sampling tutorial](../examples/tutorial_sampling.ipynb#sampling-initial ## Compute: `Kernel` After setting up the input data and particle start locations and times, we need to specify what calculations to do with -the particles. These calculations, or numerical integrations, will be performed by what we call a {py:obj}`parcels.Kernel`, operating on +the particles. These calculations, or numerical integrations, will be performed by what we call a `kernel` function, operating on all particles in the `ParticleSet`. The most common calculation is the advection of particles through the velocity field. Parcels comes with a number of common {py:obj}`parcels.kernels`, from which we will use the Runge-Kutta advection kernel {py:obj}`parcels.kernels.AdvectionRK2`: @@ -164,7 +157,7 @@ pset.execute( ## Read output -To start analyzing the trajectories computed by **Parcels**, we can open the `ParticleFile` using the `read_particlefile()` utility, which itself uses `polars`: +To start analyzing the trajectories computed by Parcels, we can open the {py:obj}`parcels.ParticleFile` using the {py:func}`parcels.read_particlefile()` utility, which itself uses `polars`: ```{code-cell} df = parcels.read_particlefile("output-quickstart.parquet") @@ -178,15 +171,20 @@ Let's verify that Parcels has computed the advection of the virtual particles! ```{code-cell} import matplotlib.pyplot as plt +import matplotlib.dates as mdates + +# convert time to matplotlib date format for plotting +time_values = mdates.date2num(df["t"].to_list()) # plot positions and color particles by time -scatter = plt.scatter(df['x'], df['y'], c=df['t']) +scatter = plt.scatter(df['x'], df['y'], c=time_values) plt.scatter(df['x'][:npart], df['y'][:npart], facecolors="none", edgecolors='r') # starting positions plt.scatter(lon, lat, facecolors="none", edgecolors='r') # starting positions plt.xlim(31,33) plt.ylabel("Latitude [deg N]") plt.ylim(-33,-30) -plt.colorbar(scatter, label="Observation number") +colorbar = plt.colorbar(scatter, label="Time") +colorbar.ax.yaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d %H:%M")) plt.show() ``` @@ -223,22 +221,26 @@ When we check the output, we can see that the particles have returned to their o ```{code-cell} df_back = parcels.read_particlefile("output-backwards.parquet") -scatter = plt.scatter(df_back['x'], df_back['y'], c=df_back['t']) +# convert time to matplotlib date format for plotting +time_values = mdates.date2num(df_back["t"].to_list()) + +scatter = plt.scatter(df_back['x'], df_back['y'], c=time_values) particles_at_max_time = df_back.filter(pl.col("t") == df_back["t"].max()) plt.scatter(particles_at_max_time['x'], particles_at_max_time['y'], facecolors="none", edgecolors='r') # starting positions plt.xlabel("Longitude [deg E]") plt.xlim(31,33) plt.ylabel("Latitude [deg N]") plt.ylim(-33,-30) -plt.colorbar(scatter, label="Observation number") +colorbar = plt.colorbar(scatter, label="Time") +colorbar.ax.yaxis.set_major_formatter(mdates.DateFormatter("%Y-%m-%d %H:%M")) plt.show() ``` -Using Euler forward advection, the final positions are equal to the original positions with an accuracy of 2 decimals: +Using RK2 advection, the final positions are equal to the original positions with an accuracy of 5 decimals: ```{code-cell} # testing that final location == original location particles_at_min_time = df_back.filter(pl.col("t") == df_back["t"].min()) -np.testing.assert_almost_equal(particles_at_min_time["y"], lat, 2) -np.testing.assert_almost_equal(particles_at_min_time['x'], lon, 2) +np.testing.assert_almost_equal(particles_at_min_time["y"], lat, decimal=5) +np.testing.assert_almost_equal(particles_at_min_time['x'], lon, decimal=5) ``` diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index c1995fe350..533020cc1f 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -1,6 +1,6 @@ # User guide -The core of our user guide is a series of Jupyter notebooks which document how to implement specific Lagrangian simulations with the flexibility of **Parcels**. +The core of our user guide is a series of Jupyter notebooks which document how to implement specific Lagrangian simulations with the flexibility of Parcels. Before diving into these advanced _how-to_ guides (πŸ–₯️), we suggest users get started by reading the explanation (πŸ“–) of the core concepts and trying the tutorials (πŸŽ“). @@ -88,6 +88,8 @@ examples/tutorial_write_in_kernel.ipynb examples/explanation_interpolation.md examples/tutorial_interpolation.ipynb examples/tutorial_peninsula_AvsCgrid.ipynb +examples/tutorial_stuck_particles.ipynb +examples/tutorial_unstuck_Agrid.ipynb ``` @@ -122,8 +124,6 @@ examples/tutorial_interaction.ipynb :titlesonly: πŸ“– v4 development <../v4/index> πŸŽ“ v3 to v4 migration guide -examples/tutorial_stuck_particles.ipynb -examples/tutorial_unstuck_Agrid.ipynb examples/tutorial_homepage_animation.md ```