Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
182688a
Draft unstructured interpolation tutorial (wip)
fluidnumerics-joe Dec 3, 2025
a66fc3b
Fix flat face coloring and add particle interpolation test section
fluidnumerics-joe Dec 4, 2025
c788cf6
Add exposition for the unstructured grid interpolation
fluidnumerics-joe Dec 4, 2025
9269711
Restrict barycentric coordinate calculation to triangle faces
fluidnumerics-joe Dec 5, 2025
704289e
Change assertions for stommel gyre test
fluidnumerics-joe Dec 5, 2025
476bdc0
Remove component of point in normal direction of element plane
fluidnumerics-joe Dec 8, 2025
1ee1111
Move "Interpolation at Boundaries" to subsubsection of structured gri…
fluidnumerics-joe Dec 8, 2025
af68cf7
Set verbose_progress=False in particleset execute commands
fluidnumerics-joe Dec 8, 2025
9a3841f
Change Tracer->tracer in particle attribute (convention)
fluidnumerics-joe Dec 8, 2025
2be8e50
Change UX->Ux interpolators prefix for unstructured grid interpolators
fluidnumerics-joe Dec 8, 2025
7e567fc
Merge branch 'v4-dev' into uxinterpolation_tutorial_v4
fluidnumerics-joe Dec 8, 2025
d5b0ce8
Add comments about additional variables defined in generated dataset
fluidnumerics-joe Dec 8, 2025
e7cefdf
Set time as float in test
fluidnumerics-joe Dec 9, 2025
c324f2f
Set time as float in test
fluidnumerics-joe Dec 9, 2025
70e20a3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 9, 2025
54ae960
Fix reference to `tracer` from `Tracer`
fluidnumerics-joe Dec 9, 2025
b8465f9
Merge branch 'uxinterpolation_tutorial_v4' of github.com:OceanParcels…
fluidnumerics-joe Dec 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
246 changes: 234 additions & 12 deletions docs/user_guide/examples/tutorial_interpolation.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@
" fieldset, pclass=SampleParticle, lon=xv.flatten(), lat=yv.flatten()\n",
" )\n",
" pset[p_interp.__name__].execute(\n",
" SampleP, runtime=np.timedelta64(1, \"D\"), dt=np.timedelta64(1, \"D\")\n",
" SampleP,\n",
" runtime=np.timedelta64(1, \"D\"),\n",
" dt=np.timedelta64(1, \"D\"),\n",
" verbose_progress=False,\n",
" )"
]
},
Expand Down Expand Up @@ -239,26 +242,245 @@
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Interpolation at boundaries\n",
"In some cases, we need to implement specific boundary conditions, for example to prevent particles from \n",
"getting \"stuck\" near land. [This guide](../examples_v3/documentation_unstuck_Agrid.ipynb) describes \n",
"how to implement this in parcels using `parcels.interpolators.XFreeslip` and `parcels.interpolators.XPartialslip`."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Interpolation on unstructured grids\n",
"```{note}\n",
"TODO: add example on simple unstructured fieldset\n",
"```\n",
"- UXPiecewiseConstantFace\n",
"- UXPiecewiseLinearNode"
"Parcels v4 supports the use of general circulation model output that is defined on unstructured grids. We include basic interpolators to help you get started, including\n",
"- `UxPiecewiseConstantFace` - this interpolator implements piecewise constant interpolation and is appropriate for data that is registered to the face centers of the unstructured grid\n",
"- `UxPiecewiseLinearNode` - this interpolator implements barycentric interpolation and is appropriate for data that is registered to the corner vertices of the unstructured grid faces\n",
"\n",
"To get started, we use a very simple generated `UxArray.UxDataset` that is included with Parcels."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"from parcels._datasets.unstructured.generated import simple_small_delaunay\n",
"\n",
"ds = simple_small_delaunay(nx=4, ny=4)\n",
"ds"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Interpolation at boundaries\n",
"In some cases, we need to implement specific boundary conditions, for example to prevent particles from \n",
"getting \"stuck\" near land. [This guide](../examples_v3/documentation_unstuck_Agrid.ipynb) describes \n",
"how to implement this in parcels using `parcels.interpolators.XFreeslip` and `parcels.interpolators.XPartialslip`."
"Next, we create the `Field` and `Fieldset` objects that will be used later in advancing particles. When creating a `Field` or `VectorField` object for unstructured grid data, we attach a `parcels.UxGrid` object and attach an `interp_method` to each object. For data that is defined on face centers, we use the `UxPiecewiseConstantFace` interpolator and for data that is defined on the face vertices, we use the `UxPiecewiseLinearNode` interpolator. In this example, we will look specifically at interpolating a tracer field that is defined by the same underlying analytical function, but is defined on both faces and vertices as separate fields."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"import parcels\n",
"\n",
"grid = parcels.UxGrid(ds.uxgrid, ds.nz, mesh=\"flat\")\n",
"\n",
"Tnode = parcels.Field(\n",
" \"T_node\",\n",
" ds[\"T_node\"],\n",
" grid,\n",
" interp_method=parcels.interpolators.UxPiecewiseLinearNode,\n",
")\n",
"Tface = parcels.Field(\n",
" \"T_face\",\n",
" ds[\"T_face\"],\n",
" grid,\n",
" interp_method=parcels.interpolators.UxPiecewiseConstantFace,\n",
")\n",
"fieldset = parcels.FieldSet([Tnode, Tface])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We can now create particles to sample data using either the node registered or face registered data."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"SampleParticle = parcels.Particle.add_variable(\n",
" parcels.Variable(\"tracer\", dtype=np.float32, initial=np.nan)\n",
")\n",
"\n",
"\n",
"def SampleTracer_Node(particles, fieldset):\n",
" particles.Tracer = fieldset.T_node[particles]\n",
"\n",
"\n",
"def SampleTracer_Face(particles, fieldset):\n",
" particles.Tracer = fieldset.T_face[particles]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, we can create a `ParticleSet` for each interpolation experiment. In one of the `ParticleSet` objects we use the `SampleTracer_Node` kernel to showcase interpolating with the `UxPiecewiseLinearNode` interpolator for node-registered data. In the other, we use the `SampleTracer_Face` to showcase interpolating with the `UxPiecewiseConstantFace` interpolator for face-registered data."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pset = {}\n",
"\n",
"xv, yv = np.meshgrid(np.linspace(0.2, 0.8, 8), np.linspace(0.2, 0.9, 8))\n",
"\n",
"pset[\"node\"] = parcels.ParticleSet(\n",
" fieldset, pclass=SampleParticle, lon=xv.flatten(), lat=yv.flatten()\n",
")\n",
"pset[\"node\"].execute(\n",
" SampleTracer_Node,\n",
" runtime=np.timedelta64(1, \"D\"),\n",
" dt=np.timedelta64(1, \"D\"),\n",
" verbose_progress=False,\n",
")\n",
"\n",
"pset[\"face\"] = parcels.ParticleSet(\n",
" fieldset, pclass=SampleParticle, lon=xv.flatten(), lat=yv.flatten()\n",
")\n",
"pset[\"face\"].execute(\n",
" SampleTracer_Face,\n",
" runtime=np.timedelta64(1, \"D\"),\n",
" dt=np.timedelta64(1, \"D\"),\n",
" verbose_progress=Falsex,\n",
")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now, we can plot the node-registered (`T_node`) and face-registered (`T_face`) fields with the values interpolated onto the particles for each field. "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import matplotlib.tri as mtri\n",
"import numpy as np\n",
"\n",
"fig, ax = plt.subplots(1, 2, figsize=(10, 5), constrained_layout=True)\n",
"\n",
"x = ds.uxgrid.node_lon.values\n",
"y = ds.uxgrid.node_lat.values\n",
"tri = ds.uxgrid.face_node_connectivity.values\n",
"triang = mtri.Triangulation(x, y, triangles=tri)\n",
"\n",
"\n",
"# Shade the triangle faces using smooth shading between the node registered data\n",
"T_node = np.squeeze(ds[\"T_node\"][0, 0, :].values)\n",
"tpc = ax[0].tripcolor(\n",
" triang,\n",
" T_node,\n",
" shading=\"gouraud\", # smooth shading\n",
" cmap=\"viridis\",\n",
" vmin=-1.0,\n",
" vmax=1.0,\n",
")\n",
"# Plot the element edges\n",
"ax[0].triplot(triang, color=\"black\", linewidth=0.5)\n",
"\n",
"# Plot the node registered data\n",
"sc1 = ax[0].scatter(\n",
" x, y, c=T_node, cmap=\"viridis\", s=150, edgecolors=\"black\", vmin=-1.0, vmax=1.0\n",
")\n",
"\n",
"ax[0].scatter(\n",
" pset[\"node\"].lon,\n",
" pset[\"node\"].lat,\n",
" c=pset[\"node\"].tracer,\n",
" cmap=\"viridis\",\n",
" edgecolors=\"k\",\n",
" s=50,\n",
" vmin=-1.0,\n",
" vmax=1.0,\n",
")\n",
"\n",
"cbar = fig.colorbar(sc1, ax=ax[0])\n",
"cbar.set_label(\"T\")\n",
"\n",
"ax[0].set_aspect(\"equal\", \"box\")\n",
"ax[0].set_xlabel(\"x\")\n",
"ax[0].set_ylabel(\"y\")\n",
"ax[0].set_title(\"Node Registered Data\")\n",
"\n",
"# # Plot the face registered data\n",
"T_face = np.squeeze(ds[\"T_face\"][0, 0, :].values)\n",
"assert triang.triangles.shape[0] == T_face.shape[0]\n",
"tpc2 = ax[1].tripcolor(\n",
" triang,\n",
" facecolors=T_face,\n",
" shading=\"flat\",\n",
" cmap=\"viridis\",\n",
" edgecolors=\"k\",\n",
" linewidth=0.5,\n",
" vmin=-1.0,\n",
" vmax=1.0,\n",
")\n",
"# Plot the element edges\n",
"ax[1].triplot(triang, color=\"black\", linewidth=0.5)\n",
"\n",
"# Plot the face registered data\n",
"xf = ds.uxgrid.face_lon.values\n",
"yf = ds.uxgrid.face_lat.values\n",
"\n",
"ax[1].scatter(\n",
" pset[\"face\"].lon,\n",
" pset[\"face\"].lat,\n",
" c=pset[\"face\"].tracer,\n",
" cmap=\"viridis\",\n",
" edgecolors=\"k\",\n",
" s=50,\n",
" vmin=-1.0,\n",
" vmax=1.0,\n",
")\n",
"\n",
"cbar = fig.colorbar(tpc, ax=ax[1])\n",
"cbar.set_label(\"T\")\n",
"\n",
"ax[1].set_aspect(\"equal\", \"box\")\n",
"ax[1].set_xlabel(\"x\")\n",
"ax[1].set_ylabel(\"y\")\n",
"ax[1].set_title(\"Face Registered Data\")"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"In both plots the black lines show the edges that define the boundaries between the faces in the unstructured grid. For the node registered field, the background coloring is done using smooth shading based on the value of `T_node` at the corner nodes. For the face registered fields, each face is colored according to the value of `T_face` at the face center. The color of the particles is the value of the function that the particles take on via the corresponding interpolation method."
]
},
{
Expand All @@ -272,7 +494,7 @@
],
"metadata": {
"kernelspec": {
"display_name": "test-notebooks",
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
Expand All @@ -286,7 +508,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.0"
"version": "3.12.11"
}
},
"nbformat": 4,
Expand Down
10 changes: 5 additions & 5 deletions docs/user_guide/examples_v3/tutorial_stommel_uxarray.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@
"\n",
"In Parcels, grid searching is conducted with respect to the faces. In other words, when a grid index `ei` is provided to an interpolation method, this refers the face index `fi` at vertical layer `zi` (when unraveled). Within the interpolation method, the `field.grid.uxgrid.face_node_connectivity` attribute can be used to obtain the node indices that surround the face. Using these connectivity tables is necessary for properly indexing node registered data.\n",
"\n",
"For the example Stommel gyre dataset in this tutorial, the `u` and `v` velocity components are face registered (similar to FESOM). Parcels includes a nearest neighbor interpolator for face registered unstructured grid data through `Parcels.application_kernels.interpolation.UXPiecewiseConstantFace`. Below, we create the `Field`s `U` and `V` and associate them with the `UxGrid` we created previously and this interpolation method."
"For the example Stommel gyre dataset in this tutorial, the `u` and `v` velocity components are face registered (similar to FESOM). Parcels includes a nearest neighbor interpolator for face registered unstructured grid data through `Parcels.application_kernels.interpolation.UxPiecewiseConstantFace`. Below, we create the `Field`s `U` and `V` and associate them with the `UxGrid` we created previously and this interpolation method."
]
},
{
Expand All @@ -121,26 +121,26 @@
"metadata": {},
"outputs": [],
"source": [
"from parcels.application_kernels.interpolation import UXPiecewiseConstantFace\n",
"from parcels.application_kernels.interpolation import UxPiecewiseConstantFace\n",
"from parcels.field import Field\n",
"\n",
"U = Field(\n",
" name=\"U\",\n",
" data=ds.U,\n",
" grid=grid,\n",
" interp_method=UXPiecewiseConstantFace,\n",
" interp_method=UxPiecewiseConstantFace,\n",
")\n",
"V = Field(\n",
" name=\"V\",\n",
" data=ds.V,\n",
" grid=grid,\n",
" interp_method=UXPiecewiseConstantFace,\n",
" interp_method=UxPiecewiseConstantFace,\n",
")\n",
"P = Field(\n",
" name=\"P\",\n",
" data=ds.p,\n",
" grid=grid,\n",
" interp_method=UXPiecewiseConstantFace,\n",
" interp_method=UxPiecewiseConstantFace,\n",
")"
]
},
Expand Down
6 changes: 3 additions & 3 deletions src/parcels/_core/fieldset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from parcels._core.xgrid import _DEFAULT_XGCM_KWARGS, XGrid
from parcels._logger import logger
from parcels._typing import Mesh
from parcels.interpolators import UXPiecewiseConstantFace, UXPiecewiseLinearNode, XConstantField, XLinear
from parcels.interpolators import UxPiecewiseConstantFace, UxPiecewiseLinearNode, XConstantField, XLinear

if TYPE_CHECKING:
from parcels._core.basegrid import BaseGrid
Expand Down Expand Up @@ -459,9 +459,9 @@ def _select_uxinterpolator(da: ux.UxDataArray):
"""Selects the appropriate uxarray interpolator for a given uxarray UxDataArray"""
supported_uxinterp_mapping = {
# (nz1,n_face): face-center laterally, layer centers vertically — piecewise constant
"nz1,n_face": UXPiecewiseConstantFace,
"nz1,n_face": UxPiecewiseConstantFace,
# (nz,n_node): node/corner laterally, layer interfaces vertically — barycentric lateral & linear vertical
"nz,n_node": UXPiecewiseLinearNode,
"nz,n_node": UxPiecewiseLinearNode,
}
# Extract only spatial dimensions, neglecting time
da_spatial_dims = tuple(d for d in da.dims if d not in ("time",))
Expand Down
Loading
Loading