From 75a20b6c81e16ee47a06bb0e4db1407eb6a325b0 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Wed, 4 Nov 2026 21:34:46 +0000 Subject: [PATCH 01/16] initial vertical coordinate interpolator plumbing --- src/parcels/_core/particleset.py | 2 +- src/parcels/_core/uxgrid.py | 97 +++++++++------- src/parcels/_core/xgrid.py | 104 ++++++++++-------- src/parcels/interpolators/__init__.py | 10 ++ src/parcels/interpolators/_base.py | 13 +++ src/parcels/interpolators/_uxinterpolators.py | 18 ++- src/parcels/interpolators/_xinterpolators.py | 29 ++++- 7 files changed, 186 insertions(+), 87 deletions(-) diff --git a/src/parcels/_core/particleset.py b/src/parcels/_core/particleset.py index 192f483ff8..c39d00504e 100644 --- a/src/parcels/_core/particleset.py +++ b/src/parcels/_core/particleset.py @@ -86,7 +86,7 @@ def __init__( minz = None for field in self.fieldset.fields.values(): has_time_varying_z = (isinstance(field.grid, UxGrid) and field.grid.z.ndim == 3) or ( - isinstance(field.grid, XGrid) and "Z" in field.grid.axes and field.grid._ds["depth"].ndim == 4 + isinstance(field.grid, XGrid) and field.grid.depth.ndim == 4 ) if has_time_varying_z: raise ValueError( diff --git a/src/parcels/_core/uxgrid.py b/src/parcels/_core/uxgrid.py index 3927a8ceaa..b2c7dbf5bc 100644 --- a/src/parcels/_core/uxgrid.py +++ b/src/parcels/_core/uxgrid.py @@ -4,13 +4,14 @@ from typing import TYPE_CHECKING, Literal import numpy as np -import xarray as xr from dask import is_dask_collection from parcels._core.basegrid import BaseGrid from parcels._core.index_search import GRID_SEARCH_ERROR, _search_1d_array, _search_1d_columns, uxgrid_point_in_cell from parcels._core.mesh import BaseMesh, TMesh from parcels._core.warnings import FieldSetWarning +from parcels.interpolators._base import VerticalCoordinateInterpolator +from parcels.interpolators._uxinterpolators import UxVerticalCoordinateConstantFace, UxVerticalCoordinateLinearNode if TYPE_CHECKING: import uxarray as ux @@ -35,7 +36,7 @@ def __init__(self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh: TMesh) -> None: z : ux.UxDataArray Vertical coordinates (depths) of the layer interface heights (not the mid-layer depths). Either a 1D array, constant in time and space (flat bottom topography, no moving vertical grid), or a 3D array with dims - ("time", "zf", "n_node"), varying in time and across the mesh nodes. + ("time", "zf", "n_node") or ("time", "zf", "n_face"), varying in time and across the mesh nodes or faces. mesh : FlatMesh or SphericalMesh The mesh of the grid, e.g. ``FlatMesh()`` or ``SphericalMesh(radius=...)``. """ @@ -50,24 +51,40 @@ def __init__(self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh: TMesh) -> None: raise TypeError("z must be an instance of ux.UxDataArray") if z.ndim not in (1, 3): raise ValueError(f"z must be a 1D or 3D array of vertical coordinates, got {z.ndim}D") - if z.ndim == 3 and z.dims != ("time", "zf", "n_node"): - raise ValueError(f"A 3D z must have dims ('time', 'zf', 'n_node'), got {z.dims}") - if z.ndim == 3: - warnings.warn( - "Time-varying (3D) z coordinates are experimental and may cause significant memory overhead that " - f"leads to OOM errors. This z coordinate has sizes {dict(z.sizes)} ({z.nbytes / 1e9:.3g} GB). " - "Assumptions: z is defined at the layer interfaces ('zf') on the mesh nodes ('n_node') and is strictly " - "increasing along 'zf'; each particle's z column is interpolated barycentrically from its face's " - "nodes and linearly in time between z snapshots.", - FieldSetWarning, - stacklevel=4, - ) + self.z = z self.mesh = mesh self._spatialhash = None + self._z_interp_method = None + if z.ndim == 3: + self._init_time_varying_z() if is_dask_collection(self.uxgrid.face_node_connectivity.data): self.uxgrid.face_node_connectivity.load() + + def _init_time_varying_z(self) -> None: + """Checks the dims of a time-varying (3D) z, assigns its default z_interp_method and warns about its use.""" + z = self.z + + if z.dims not in (("time", "zf", "n_node"), ("time", "zf", "n_face")): + raise ValueError(f"A 3D z must have dims ('time', 'zf', 'n_node') or ('time', 'zf', 'n_face'), got {z.dims}") + + if z.dims[-1] == "n_node": + self.z_interp_method = UxVerticalCoordinateLinearNode() + else: + self.z_interp_method = UxVerticalCoordinateConstantFace() + + warnings.warn( + "Time-varying (3D) z coordinates are experimental and may cause significant memory overhead that " + f"leads to OOM errors. This z coordinate has sizes {dict(z.sizes)} ({z.nbytes / 1e9:.3g} GB). " + f"Assumptions: z has dims {z.dims}, i.e. it is defined at the layer interfaces ('zf') on the mesh " + "nodes or faces, and is strictly increasing along 'zf'; each particle's z column is interpolated with " + "the grid's `z_interp_method` (by default barycentrically from its face's nodes for node z, or " + "constant over its face for face z), and linearly in time between z snapshots.", + FieldSetWarning, + stacklevel=5, + ) + @property def depth(self): """ @@ -76,16 +93,32 @@ def depth(self): Included for compatibility with v3 codebase. May be removed in future. TODO v4: Evaluate """ - try: - _ = self.z.values - except KeyError: - return np.zeros(1) + # Load time varying z lazily + if self.z.ndim == 3: + return self.z return self.z.values @property def axes(self) -> list[_UXGRID_AXES]: return ["Z", "FACE"] + @property + def z_interp_method(self) -> VerticalCoordinateInterpolator | None: + return self._z_interp_method + + @z_interp_method.setter + def z_interp_method(self, value: VerticalCoordinateInterpolator): + if not isinstance(value, VerticalCoordinateInterpolator): + raise ValueError(f"z_interp_method must be a `VerticalCoordinateInterpolator` object. Got {type(value)=!r}") + if self.depth.ndim != 3: + raise ValueError("z_interp_method can only be set on a grid with a time-varying (3D) z") + if isinstance(value, UxVerticalCoordinateLinearNode) and self.z.dims[-1] != "n_node": + raise ValueError(f"UxVerticalCoordinateLinearNode needs z on the mesh nodes, got dims {self.z.dims}") + if isinstance(value, UxVerticalCoordinateConstantFace) and self.z.dims[-1] != "n_face": + raise ValueError(f"UxVerticalCoordinateConstantFace needs z on the mesh faces, got dims {self.z.dims}") + self._z_interp_method = value + + def get_axis_dim(self, axis: _UXGRID_AXES) -> int: if axis not in self.axes: raise ValueError(f"Axis {axis!r} is not part of this grid. Available axes: {self.axes}") @@ -163,32 +196,16 @@ def search(self, z, y, x, ei=None, ti=None, tau=None, tol=1e-6): return {"Z": {"index": zi, "bcoord": zeta}, "FACE": {"index": fi, "bcoord": coords}} def _search_time_varying_z(self, z, fi, coords, ti, tau): - """Vertical search of each particle in the z column interpolated barycentrically from the nodes of its face, - using temporal linear interpolation between the snapshots ti and ti + 1. - """ + """Vertical search of each particle in the z column built by the grid's z_interp_method.""" found = fi >= 0 - cols_ti = self.z.isel( - time=xr.DataArray(np.broadcast_to(ti, fi.shape)[found], dims="points"), - n_node=xr.DataArray(self.uxgrid.face_node_connectivity[fi[found], :].values, dims=("points", "nodes")), - ignore_grid=True, - ).compute() - - if self.z.shape[0] == 1: - node_columns = cols_ti - else: - cols_tnext = self.z.isel( - time=xr.DataArray(np.broadcast_to(ti + 1, fi.shape)[found], dims="points"), - n_node=xr.DataArray(self.uxgrid.face_node_connectivity[fi[found], :].values, dims=("points", "nodes")), - ignore_grid=True, - ).compute() - - tau_points = xr.DataArray(np.broadcast_to(tau, fi.shape)[found], dims="points") - node_columns = (1 - tau_points) * cols_ti + tau_points * cols_tnext + grid_positions = { + "T": {"index": np.broadcast_to(ti, fi.shape)[found], "bcoord": np.broadcast_to(tau, fi.shape)[found]}, + "FACE": {"index": fi[found], "bcoord": coords[found]}, + } - bcoords = xr.DataArray(coords[found], dims=("points", "nodes")) # Particles outside the mesh are given a NaN z column for vertical searching columns = np.full((fi.size, self.z.sizes["zf"]), np.nan) - columns[found] = xr.dot(node_columns, bcoords, dim="nodes").values + columns[found] = self.z_interp_method.columns(grid_positions, self) zi, zeta = _search_1d_columns(columns, z) # Particles outside the mesh are given a 0 vertical index and a NaN vertical barycentric coordinate diff --git a/src/parcels/_core/xgrid.py b/src/parcels/_core/xgrid.py index 20b6cd788a..f6bf7b72e9 100644 --- a/src/parcels/_core/xgrid.py +++ b/src/parcels/_core/xgrid.py @@ -15,7 +15,8 @@ from parcels._core.mesh import BaseMesh from parcels._core.warnings import FieldSetWarning from parcels._sgrid.accessor import _get_dim_to_axis_mapping, get_dim_position -from parcels.interpolators._xinterpolators import _get_offsets_dictionary +from parcels.interpolators._base import VerticalCoordinateInterpolator +from parcels.interpolators._xinterpolators import XVerticalCoordinateConstant, XVerticalCoordinateLinear _FIELD_DATA_ORDERING: Sequence[ptyping.XgcmAxisDirection] = "TZYX" _XGRID_AXES_ORDERING: Sequence[ptyping.XgridAxis] = "ZYX" @@ -129,6 +130,7 @@ def __init__(self, model_data: xr.Dataset, mesh: ptyping.TMesh): self._ds = model_data self.mesh = mesh self._spatialhash = None + self._z_interp_method = None ds = model_data # Set the coordinates for the dataset (needed to be done explicitly for curvilinear grids) @@ -146,27 +148,42 @@ def __init__(self, model_data: xr.Dataset, mesh: ptyping.TMesh): if depth.ndim not in (1, 4): raise ValueError(f"depth must be a 1D or 4D array of vertical coordinates, got {depth.ndim}D") if depth.ndim == 4: - assert self.sgrid_metadata.vertical_dimensions is not None - vertical_dim = self.sgrid_metadata.vertical_dimensions[0].node - fnp_x, fnp_y = self.sgrid_metadata.face_dimensions - expected_dims = ("time", vertical_dim, fnp_y.face, fnp_x.face) - if depth.dims != expected_dims: - raise ValueError(f"A 4D depth must have dims {expected_dims}, got {depth.dims}") - warnings.warn( - "Time-varying (4D) z coordinates are experimental and may cause significant memory overhead that " - f"leads to OOM errors. This z coordinate has sizes {dict(depth.sizes)} ({depth.nbytes / 1e9:.3g} GB). " - f"Assumptions: z has dims {expected_dims}, i.e. it is " - f"defined at the layer interfaces on the cell centres, and is strictly increasing along " - f"{vertical_dim!r}; each particle uses the z column at the centre of its grid cell, so z is constant " - "within a cell and jumps between cells, it is linearly interpolated in time between z snapshots.", - FieldSetWarning, - stacklevel=5, - ) + self._init_time_varying_depth() else: assert_valid_depth(depth) self._ds = ds + def _init_time_varying_depth(self) -> None: + """Checks the dims of a time-varying (4D) depth, assigns its default z_interp_method and warns about its use.""" + depth = self.depth + + assert self.sgrid_metadata.vertical_dimensions is not None + vertical_dim = self.sgrid_metadata.vertical_dimensions[0].node + + fnp_x, fnp_y = self.sgrid_metadata.face_dimensions + face_registered_dims = ("time", vertical_dim, fnp_y.face, fnp_x.face) + node_registered_dims = ("time", vertical_dim, fnp_y.node, fnp_x.node) + + if depth.dims not in (face_registered_dims, node_registered_dims): + raise ValueError( + f"A 4D depth must have dims {face_registered_dims} (cell centres) or {node_registered_dims} " + f"(cell corners), got {depth.dims}" + ) + + self.z_interp_method = XVerticalCoordinateLinear() + + warnings.warn( + "Time-varying (4D) z coordinates are experimental and may cause significant memory overhead that " + f"leads to OOM errors. This z coordinate has sizes {dict(depth.sizes)} ({depth.nbytes / 1e9:.3g} GB). " + f"Assumptions: z has dims {depth.dims}, i.e. it is " + f"defined at the layer interfaces on the cell centres or corners, and is strictly increasing along " + f"{vertical_dim!r}; each particle's z column is interpolated with the grid's `z_interp_method` " + "(by default bilinearly from the surrounding columns), and linearly in time between z snapshots.", + FieldSetWarning, + stacklevel=6, + ) + # def __repr__(self): # return xgrid_repr(self) @@ -174,6 +191,21 @@ def __init__(self, model_data: xr.Dataset, mesh: ptyping.TMesh): def axes(self) -> list[ptyping.XgridAxis]: return _get_xgrid_axes(self.sgrid_metadata, set(self._ds.dims)) + @property + def z_interp_method(self) -> VerticalCoordinateInterpolator | None: + return self._z_interp_method + + @z_interp_method.setter + def z_interp_method(self, value: VerticalCoordinateInterpolator): + if not isinstance(value, VerticalCoordinateInterpolator): + raise ValueError(f"z_interp_method must be a `VerticalCoordinateInterpolator` object. Got {type(value)=!r}") + if self.depth.ndim != 4: + raise ValueError("z_interp_method can only be set on a grid with a time-varying (4D) depth") + depth_dims = self.depth.dims + if isinstance(value, XVerticalCoordinateConstant) and get_dim_position(self.sgrid_metadata, depth_dims[-1]) != "face": + raise ValueError(f"XVerticalCoordinateConstant needs depth on the cell centres, got dims {depth_dims}") + self._z_interp_method = value + @property def lon(self): """ @@ -214,6 +246,9 @@ def depth(self): """ if "Z" not in self.axes: return np.zeros(1) + # Load time varying z lazily + if self._ds["depth"].ndim == 4: + return self._ds["depth"] return self._ds["depth"].values @property @@ -370,7 +405,7 @@ def search(self, z, y, x, ei=None, ti=None, tau=None): raise NotImplementedError("Searching in >2D lon/lat arrays is not implemented yet.") if "Z" in self.axes and ds.depth.ndim == 4: - zi, zeta = self._search_time_varying_depth(z, yi, xi, ti, tau) + zi, zeta = self._search_time_varying_depth(z, yi, eta, xi, xsi, ti, tau) elif "Z" in self.axes: zi, zeta = _search_1d_array(ds.depth.values, z) else: @@ -382,35 +417,18 @@ def search(self, z, y, x, ei=None, ti=None, tau=None): "X": {"index": xi, "bcoord": xsi}, } - def _search_time_varying_depth(self, z, yi, xi, ti, tau): - """Vertical search of each particle in the z column at the centre of its cell using temporal linear - interpolation between the snapshots ti and ti + 1. - """ + def _search_time_varying_depth(self, z, yi, eta, xi, xsi, ti, tau): + """Vertical search of each particle in the z column built by the grid's z_interp_method.""" found = (yi >= 0) & (xi >= 0) - offsets = _get_offsets_dictionary(self) - yi_centre = yi[found] + offsets["Y"] - xi_centre = xi[found] + offsets["X"] - - depth = self._ds["depth"] - fnp_x, fnp_y = self.sgrid_metadata.face_dimensions - cell_centres = { - fnp_y.face: xr.DataArray(yi_centre, dims="points"), - fnp_x.face: xr.DataArray(xi_centre, dims="points"), + grid_positions = { + "T": {"index": np.broadcast_to(ti, yi.shape)[found], "bcoord": np.broadcast_to(tau, yi.shape)[found]}, + "Y": {"index": yi[found], "bcoord": eta[found]}, + "X": {"index": xi[found], "bcoord": xsi[found]}, } - ti_found = np.broadcast_to(ti, yi.shape)[found] - columns_ti = depth.isel({"time": xr.DataArray(ti_found, dims="points"), **cell_centres}) - - if depth.sizes["time"] == 1: - cell_columns = columns_ti - else: - columns_tnext = depth.isel({"time": xr.DataArray(ti_found + 1, dims="points"), **cell_centres}) - tau_found = xr.DataArray(np.broadcast_to(tau, yi.shape)[found], dims="points") - cell_columns = (1 - tau_found) * columns_ti + tau_found * columns_tnext - # Particles outside the grid are given a NaN z column for vertical searching - columns = np.full((yi.size, cell_columns.shape[1]), np.nan) - columns[found] = cell_columns.values + columns = np.full((yi.size, self.depth.shape[1]), np.nan) + columns[found] = self.z_interp_method.columns(grid_positions, self) zi, zeta = _search_1d_columns(columns, z) # Particles outside the grid are given a 0 vertical index and a NaN vertical barycentric coordinate diff --git a/src/parcels/interpolators/__init__.py b/src/parcels/interpolators/__init__.py index 2f131c546d..752a519659 100644 --- a/src/parcels/interpolators/__init__.py +++ b/src/parcels/interpolators/__init__.py @@ -6,6 +6,8 @@ UxConstantFaceLinearZF, UxLinearNodeConstantZC, UxLinearNodeLinearZF, + UxVerticalCoordinateConstantFace, + UxVerticalCoordinateLinearNode, ) from ._xinterpolators import ( CGrid_Tracer, @@ -17,6 +19,9 @@ XLinearInvdistLandTracer, XNearest, XPartialslip, + XVerticalCoordinateConstant, + XVerticalCoordinateLinear, + XVerticalCoordinateNearest, ) __all__ = [ # noqa: RUF022 @@ -30,6 +35,9 @@ "XLinear_Velocity", "XNearest", "XPartialslip", + "XVerticalCoordinateConstant", + "XVerticalCoordinateLinear", + "XVerticalCoordinateNearest", # uxinterpolators "Ux_Velocity", "UxConstantFaceConstantZC", @@ -38,4 +46,6 @@ "UxConstantEdgeLinearZF", "UxLinearNodeConstantZC", "UxLinearNodeLinearZF", + "UxVerticalCoordinateConstantFace", + "UxVerticalCoordinateLinearNode", ] diff --git a/src/parcels/interpolators/_base.py b/src/parcels/interpolators/_base.py index 6a550b240e..482896e3bf 100644 --- a/src/parcels/interpolators/_base.py +++ b/src/parcels/interpolators/_base.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod from typing import Any +import numpy as np + class ScalarInterpolator(ABC): @abstractmethod @@ -18,3 +20,14 @@ def interp(self, particle_positions, grid_positions, vectorfield) -> Any: #! AP def __repr__(self): return f"{self.__class__.__name__}(...)" + + +class VerticalCoordinateInterpolator(ABC): + """Used for models with time varying z.""" + + @abstractmethod + def columns(self, grid_positions, grid) -> np.ndarray: #! API a WIP, returns an (npart, n_interfaces) array + ... + + def __repr__(self): + return f"{self.__class__.__name__}(...)" diff --git a/src/parcels/interpolators/_uxinterpolators.py b/src/parcels/interpolators/_uxinterpolators.py index 6db1000eb7..92f97efeba 100644 --- a/src/parcels/interpolators/_uxinterpolators.py +++ b/src/parcels/interpolators/_uxinterpolators.py @@ -8,11 +8,11 @@ import xarray as xr from dask import is_dask_collection -from parcels.interpolators._base import ScalarInterpolator, VectorInterpolator +from parcels.interpolators._base import ScalarInterpolator, VectorInterpolator, VerticalCoordinateInterpolator if TYPE_CHECKING: from parcels._core.field import Field, VectorField - from parcels._core.uxgrid import _UXGRID_AXES + from parcels._core.uxgrid import _UXGRID_AXES, UxGrid class UxConstantFaceConstantZC(ScalarInterpolator): @@ -243,3 +243,17 @@ def interp( else: w = np.zeros_like(u) return u, v, w + + +class UxVerticalCoordinateConstantFace(VerticalCoordinateInterpolator): + """Piecewise constant over the face (face registered z), linear in time.""" + + def columns(self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid) -> np.ndarray: + raise NotImplementedError + + +class UxVerticalCoordinateLinearNode(VerticalCoordinateInterpolator): + """Barycentric from the face's nodes (node registered z), linear in time.""" + + def columns(self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid) -> np.ndarray: + raise NotImplementedError diff --git a/src/parcels/interpolators/_xinterpolators.py b/src/parcels/interpolators/_xinterpolators.py index 0e85b07dd3..6ef5141fc3 100644 --- a/src/parcels/interpolators/_xinterpolators.py +++ b/src/parcels/interpolators/_xinterpolators.py @@ -11,7 +11,7 @@ import parcels._core.utils.interpolation as i_u import parcels._sgrid as sgrid import parcels._typing as ptyping -from parcels.interpolators._base import ScalarInterpolator, VectorInterpolator +from parcels.interpolators._base import ScalarInterpolator, VectorInterpolator, VerticalCoordinateInterpolator if TYPE_CHECKING: from parcels._core._windowed_array import WindowedArray @@ -611,3 +611,30 @@ def interp( values[exact_particles] = exact_vals[exact_particles] return values.compute() if is_dask_collection(values) else values + + +class XVerticalCoordinateConstant(VerticalCoordinateInterpolator): + """Piecewise constant over the grid cell (face registered z), linear in time.""" + + def columns( + self, grid_positions: dict[ptyping.XgridAxis, dict[str, int | float | np.ndarray]], grid: XGrid + ) -> np.ndarray: + raise NotImplementedError + + +class XVerticalCoordinateLinear(VerticalCoordinateInterpolator): + """Bilinear between the surrounding z columns (face or node registered z), linear in time.""" + + def columns( + self, grid_positions: dict[ptyping.XgridAxis, dict[str, int | float | np.ndarray]], grid: XGrid + ) -> np.ndarray: + raise NotImplementedError + + +class XVerticalCoordinateNearest(VerticalCoordinateInterpolator): + """Nearest z column (face or node registered z), linear in time.""" + + def columns( + self, grid_positions: dict[ptyping.XgridAxis, dict[str, int | float | np.ndarray]], grid: XGrid + ) -> np.ndarray: + raise NotImplementedError From 4e98ebc5f4b445bbaac8bf7b43f88fafea2c27c0 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Wed, 4 Nov 2026 22:05:32 +0000 Subject: [PATCH 02/16] implement UxLinearNodeLinearZf and corresponding Vertical coord interpolator --- src/parcels/interpolators/_uxinterpolators.py | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/src/parcels/interpolators/_uxinterpolators.py b/src/parcels/interpolators/_uxinterpolators.py index 92f97efeba..5814154e69 100644 --- a/src/parcels/interpolators/_uxinterpolators.py +++ b/src/parcels/interpolators/_uxinterpolators.py @@ -15,6 +15,30 @@ from parcels._core.uxgrid import _UXGRID_AXES, UxGrid +def _barycentric_node_values( + data: xr.DataArray, + ti: np.ndarray, + tau: np.ndarray, + node_ids: np.ndarray, + bcoords: np.ndarray, +) -> xr.DataArray: + """Barycentric (lateral) interpolation of node registered data from the nodes of each particle's face, linear in + time between the snapshots ti and ti + 1, at every vertical level. Returns a ("points", ) array.""" + tdim, _, ndim = data.dims + face_nodes = xr.DataArray(node_ids, dims=("points", "nodes")) + node_weights = xr.DataArray(bcoords, dims=("points", "nodes")) + + def _at_snapshot(t_index): + # Reduce over the "nodes" dimension by name so the result is independent of ``isel`` dim order. + node_data = data.isel({tdim: xr.DataArray(t_index, dims="points"), ndim: face_nodes}, ignore_grid=True) + return (node_data * node_weights).sum("nodes") + + if not np.any(tau > 0): + return _at_snapshot(ti) + tau_points = xr.DataArray(tau, dims="points") + return (1 - tau_points) * _at_snapshot(ti) + tau_points * _at_snapshot(np.clip(ti + 1, 0, data.sizes[tdim] - 1)) + + class UxConstantFaceConstantZC(ScalarInterpolator): """Piecewise constant interpolation kernel for face registered data that is vertically centered (on zc points)""" @@ -92,21 +116,17 @@ def interp( Effectively, it applies barycentric interpolation in the lateral direction and piecewise constant interpolation in the vertical direction. """ - ti, zi, fi = np.broadcast_arrays( - grid_positions["T"]["index"], grid_positions["Z"]["index"], grid_positions["FACE"]["index"] + ti, tau, zi, fi = np.broadcast_arrays( + grid_positions["T"]["index"], + grid_positions["T"]["bcoord"], + grid_positions["Z"]["index"], + grid_positions["FACE"]["index"], ) - bcoords = xr.DataArray(grid_positions["FACE"]["bcoord"], dims=("points", "nodes")) node_ids = field.grid.uxgrid.face_node_connectivity[fi, :].values + columns = _barycentric_node_values(field.data, ti, tau, node_ids, grid_positions["FACE"]["bcoord"]) - tdim, zdim, ndim = field.data.dims - selection_dict = { - tdim: xr.DataArray(ti, dims="points"), - zdim: xr.DataArray(zi, dims="points"), - ndim: xr.DataArray(node_ids, dims=("points", "nodes")), - } - - node_data = field.data.isel(selection_dict, ignore_grid=True) - value = (node_data * bcoords).sum("nodes").data # Barycentric interpolation in the lateral direction + zdim = field.data.dims[1] + value = columns.isel({zdim: xr.DataArray(zi, dims="points")}).data return value.compute() if is_dask_collection(value) else value @@ -124,31 +144,21 @@ def interp( Effectively, it applies barycentric interpolation in the lateral direction and piecewise linear interpolation in the vertical direction. """ - ti, zi, fi = np.broadcast_arrays( - grid_positions["T"]["index"], grid_positions["Z"]["index"], grid_positions["FACE"]["index"] + ti, tau, zi, fi = np.broadcast_arrays( + grid_positions["T"]["index"], + grid_positions["T"]["bcoord"], + grid_positions["Z"]["index"], + grid_positions["FACE"]["index"], ) zeta = grid_positions["Z"]["bcoord"] - bcoords = xr.DataArray(grid_positions["FACE"]["bcoord"], dims=("points", "nodes")) node_ids = field.grid.uxgrid.face_node_connectivity[fi, :].values - - tdim, zdim, ndim = field.data.dims - - def _zsample(z_index): - """Barycentric (lateral) interpolation of the node values at a single vertical interface level.""" - selection_dict = { - tdim: xr.DataArray(ti, dims="points"), - zdim: xr.DataArray(z_index, dims="points"), - ndim: xr.DataArray(node_ids, dims=("points", "nodes")), - } - # Reduce over the "nodes" dimension by name so the result is independent of ``isel`` dim order. - node_data = field.data.isel(selection_dict, ignore_grid=True) - return (node_data * bcoords).sum("nodes").data + columns = _barycentric_node_values(field.data, ti, tau, node_ids, grid_positions["FACE"]["bcoord"]) # The zi refers to the vertical layer index. The field in this routine are assumed to be defined at the vertical interface levels. # For interface zi, the interface indices are [zi, zi+1], so we need to use the values at zi and zi+1. - # First, do barycentric interpolation in the lateral direction for each interface level - fzk = _zsample(zi) - fzkp1 = _zsample(zi + 1) + zdim = field.data.dims[1] + fzk = columns.isel({zdim: xr.DataArray(zi, dims="points")}).data + fzkp1 = columns.isel({zdim: xr.DataArray(zi + 1, dims="points")}).data # Then, do piecewise linear interpolation in the vertical direction value = fzk * (1 - zeta) + fzkp1 * zeta @@ -256,4 +266,8 @@ class UxVerticalCoordinateLinearNode(VerticalCoordinateInterpolator): """Barycentric from the face's nodes (node registered z), linear in time.""" def columns(self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid) -> np.ndarray: - raise NotImplementedError + node_ids = grid.uxgrid.face_node_connectivity[grid_positions["FACE"]["index"], :].values + columns = _barycentric_node_values( + grid.z, grid_positions["T"]["index"], grid_positions["T"]["bcoord"], node_ids, grid_positions["FACE"]["bcoord"] + ) + return columns.values From adc20eb2f445290dcee237916ab74fabcc5592de Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:08:02 +0000 Subject: [PATCH 03/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/parcels/_core/uxgrid.py | 6 +++--- src/parcels/_core/xgrid.py | 5 ++++- src/parcels/interpolators/_uxinterpolators.py | 17 +++++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/parcels/_core/uxgrid.py b/src/parcels/_core/uxgrid.py index b2c7dbf5bc..560e41b1e3 100644 --- a/src/parcels/_core/uxgrid.py +++ b/src/parcels/_core/uxgrid.py @@ -61,13 +61,14 @@ def __init__(self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh: TMesh) -> None: if is_dask_collection(self.uxgrid.face_node_connectivity.data): self.uxgrid.face_node_connectivity.load() - def _init_time_varying_z(self) -> None: """Checks the dims of a time-varying (3D) z, assigns its default z_interp_method and warns about its use.""" z = self.z if z.dims not in (("time", "zf", "n_node"), ("time", "zf", "n_face")): - raise ValueError(f"A 3D z must have dims ('time', 'zf', 'n_node') or ('time', 'zf', 'n_face'), got {z.dims}") + raise ValueError( + f"A 3D z must have dims ('time', 'zf', 'n_node') or ('time', 'zf', 'n_face'), got {z.dims}" + ) if z.dims[-1] == "n_node": self.z_interp_method = UxVerticalCoordinateLinearNode() @@ -118,7 +119,6 @@ def z_interp_method(self, value: VerticalCoordinateInterpolator): raise ValueError(f"UxVerticalCoordinateConstantFace needs z on the mesh faces, got dims {self.z.dims}") self._z_interp_method = value - def get_axis_dim(self, axis: _UXGRID_AXES) -> int: if axis not in self.axes: raise ValueError(f"Axis {axis!r} is not part of this grid. Available axes: {self.axes}") diff --git a/src/parcels/_core/xgrid.py b/src/parcels/_core/xgrid.py index f6bf7b72e9..139ef9fa97 100644 --- a/src/parcels/_core/xgrid.py +++ b/src/parcels/_core/xgrid.py @@ -202,7 +202,10 @@ def z_interp_method(self, value: VerticalCoordinateInterpolator): if self.depth.ndim != 4: raise ValueError("z_interp_method can only be set on a grid with a time-varying (4D) depth") depth_dims = self.depth.dims - if isinstance(value, XVerticalCoordinateConstant) and get_dim_position(self.sgrid_metadata, depth_dims[-1]) != "face": + if ( + isinstance(value, XVerticalCoordinateConstant) + and get_dim_position(self.sgrid_metadata, depth_dims[-1]) != "face" + ): raise ValueError(f"XVerticalCoordinateConstant needs depth on the cell centres, got dims {depth_dims}") self._z_interp_method = value diff --git a/src/parcels/interpolators/_uxinterpolators.py b/src/parcels/interpolators/_uxinterpolators.py index 5814154e69..7f9bbe181f 100644 --- a/src/parcels/interpolators/_uxinterpolators.py +++ b/src/parcels/interpolators/_uxinterpolators.py @@ -23,7 +23,8 @@ def _barycentric_node_values( bcoords: np.ndarray, ) -> xr.DataArray: """Barycentric (lateral) interpolation of node registered data from the nodes of each particle's face, linear in - time between the snapshots ti and ti + 1, at every vertical level. Returns a ("points", ) array.""" + time between the snapshots ti and ti + 1, at every vertical level. Returns a ("points", ) array. + """ tdim, _, ndim = data.dims face_nodes = xr.DataArray(node_ids, dims=("points", "nodes")) node_weights = xr.DataArray(bcoords, dims=("points", "nodes")) @@ -258,16 +259,24 @@ def interp( class UxVerticalCoordinateConstantFace(VerticalCoordinateInterpolator): """Piecewise constant over the face (face registered z), linear in time.""" - def columns(self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid) -> np.ndarray: + def columns( + self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid + ) -> np.ndarray: raise NotImplementedError class UxVerticalCoordinateLinearNode(VerticalCoordinateInterpolator): """Barycentric from the face's nodes (node registered z), linear in time.""" - def columns(self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid) -> np.ndarray: + def columns( + self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid + ) -> np.ndarray: node_ids = grid.uxgrid.face_node_connectivity[grid_positions["FACE"]["index"], :].values columns = _barycentric_node_values( - grid.z, grid_positions["T"]["index"], grid_positions["T"]["bcoord"], node_ids, grid_positions["FACE"]["bcoord"] + grid.z, + grid_positions["T"]["index"], + grid_positions["T"]["bcoord"], + node_ids, + grid_positions["FACE"]["bcoord"], ) return columns.values From b00c9d1018190957ea46bc30ed9e9d4fc5f2a1e2 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Mon, 9 Nov 2026 16:48:52 -0500 Subject: [PATCH 04/16] Add xgrid interpolators and testing --- src/parcels/interpolators/_base.py | 23 +++ src/parcels/interpolators/_uxinterpolators.py | 96 ++++++------ src/parcels/interpolators/_xinterpolators.py | 64 +++++++- tests/test_interpolation.py | 139 +++++++++++++++++- 4 files changed, 270 insertions(+), 52 deletions(-) diff --git a/src/parcels/interpolators/_base.py b/src/parcels/interpolators/_base.py index 482896e3bf..afb54e267c 100644 --- a/src/parcels/interpolators/_base.py +++ b/src/parcels/interpolators/_base.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from types import SimpleNamespace from typing import Any import numpy as np @@ -29,5 +30,27 @@ class VerticalCoordinateInterpolator(ABC): def columns(self, grid_positions, grid) -> np.ndarray: #! API a WIP, returns an (npart, n_interfaces) array ... + def _columns_from_scalar(self, interp: ScalarInterpolator, grid_positions, grid) -> np.ndarray: + """Evaluates a scalar interpolator on the grid's time-varying depth at every vertical level, giving each + particle's z column as an (npart, nlev) array. This is effectively only a horizontal and temporal interpolation, any + input vertical position or barycentric coordinate is ignored. + """ + nlev = grid.depth.shape[1] + npart = next(np.shape(pos["index"])[0] for axis, pos in grid_positions.items() if axis != "T") + + def repeat(a): + a = np.broadcast_to(a, (npart, *np.shape(a)[1:])) + return np.repeat(a, nlev, axis=0) + + level_positions = { + axis: {"index": repeat(pos["index"]), "bcoord": repeat(pos["bcoord"])} + for axis, pos in grid_positions.items() + } + level_positions["Z"] = {"index": np.tile(np.arange(nlev), npart), "bcoord": np.zeros(npart * nlev)} + + # Scalar interpolators only use ``field.data`` and ``field.grid``, so a lightweight stand-in for the depth works + depth_field = SimpleNamespace(data=grid.depth, grid=grid) + return np.asarray(interp.interp({}, level_positions, depth_field)).reshape(npart, nlev) + def __repr__(self): return f"{self.__class__.__name__}(...)" diff --git a/src/parcels/interpolators/_uxinterpolators.py b/src/parcels/interpolators/_uxinterpolators.py index 7f9bbe181f..51afb5deb0 100644 --- a/src/parcels/interpolators/_uxinterpolators.py +++ b/src/parcels/interpolators/_uxinterpolators.py @@ -15,31 +15,6 @@ from parcels._core.uxgrid import _UXGRID_AXES, UxGrid -def _barycentric_node_values( - data: xr.DataArray, - ti: np.ndarray, - tau: np.ndarray, - node_ids: np.ndarray, - bcoords: np.ndarray, -) -> xr.DataArray: - """Barycentric (lateral) interpolation of node registered data from the nodes of each particle's face, linear in - time between the snapshots ti and ti + 1, at every vertical level. Returns a ("points", ) array. - """ - tdim, _, ndim = data.dims - face_nodes = xr.DataArray(node_ids, dims=("points", "nodes")) - node_weights = xr.DataArray(bcoords, dims=("points", "nodes")) - - def _at_snapshot(t_index): - # Reduce over the "nodes" dimension by name so the result is independent of ``isel`` dim order. - node_data = data.isel({tdim: xr.DataArray(t_index, dims="points"), ndim: face_nodes}, ignore_grid=True) - return (node_data * node_weights).sum("nodes") - - if not np.any(tau > 0): - return _at_snapshot(ti) - tau_points = xr.DataArray(tau, dims="points") - return (1 - tau_points) * _at_snapshot(ti) + tau_points * _at_snapshot(np.clip(ti + 1, 0, data.sizes[tdim] - 1)) - - class UxConstantFaceConstantZC(ScalarInterpolator): """Piecewise constant interpolation kernel for face registered data that is vertically centered (on zc points)""" @@ -50,8 +25,11 @@ def interp( field: Field, ): # Broadcast the per-axis indices to a common (npart,) shape (``ti`` may be scalar for time-constant fields) - ti, zi, fi = np.broadcast_arrays( - grid_positions["T"]["index"], grid_positions["Z"]["index"], grid_positions["FACE"]["index"] + ti, tau, zi, fi = np.broadcast_arrays( + grid_positions["T"]["index"], + grid_positions["T"]["bcoord"], + grid_positions["Z"]["index"], + grid_positions["FACE"]["index"], ) tdim, zdim, fdim = field.data.dims @@ -61,6 +39,12 @@ def interp( fdim: xr.DataArray(fi, dims="points"), } value = field.data.isel(selection_dict, ignore_grid=True).data + + # Linear interpolation in time between the snapshots ti and ti + 1 + if np.any(tau > 0): + selection_dict[tdim] = xr.DataArray(np.clip(ti + 1, 0, field.data.sizes[tdim] - 1), dims="points") + value_next = field.data.isel(selection_dict, ignore_grid=True).data + value = (1 - tau) * value + tau * value_next return value.compute() if is_dask_collection(value) else value @@ -123,11 +107,24 @@ def interp( grid_positions["Z"]["index"], grid_positions["FACE"]["index"], ) + bcoords = xr.DataArray(grid_positions["FACE"]["bcoord"], dims=("points", "nodes")) node_ids = field.grid.uxgrid.face_node_connectivity[fi, :].values - columns = _barycentric_node_values(field.data, ti, tau, node_ids, grid_positions["FACE"]["bcoord"]) - zdim = field.data.dims[1] - value = columns.isel({zdim: xr.DataArray(zi, dims="points")}).data + tdim, zdim, ndim = field.data.dims + selection_dict = { + tdim: xr.DataArray(ti, dims="points"), + zdim: xr.DataArray(zi, dims="points"), + ndim: xr.DataArray(node_ids, dims=("points", "nodes")), + } + + node_data = field.data.isel(selection_dict, ignore_grid=True) + value = (node_data * bcoords).sum("nodes").data # Barycentric interpolation in the lateral direction + + # Linear interpolation in time between the snapshots ti and ti + 1 + if np.any(tau > 0): + selection_dict[tdim] = xr.DataArray(np.clip(ti + 1, 0, field.data.sizes[tdim] - 1), dims="points") + node_data_next = field.data.isel(selection_dict, ignore_grid=True) + value = (1 - tau) * value + tau * (node_data_next * bcoords).sum("nodes").data return value.compute() if is_dask_collection(value) else value @@ -152,14 +149,33 @@ def interp( grid_positions["FACE"]["index"], ) zeta = grid_positions["Z"]["bcoord"] + bcoords = xr.DataArray(grid_positions["FACE"]["bcoord"], dims=("points", "nodes")) node_ids = field.grid.uxgrid.face_node_connectivity[fi, :].values - columns = _barycentric_node_values(field.data, ti, tau, node_ids, grid_positions["FACE"]["bcoord"]) + + tdim, zdim, ndim = field.data.dims + + def _zsample(z_index): + """Barycentric (lateral) interpolation of the node values at a single vertical interface level, linear in + time between the snapshots ti and ti + 1.""" + selection_dict = { + tdim: xr.DataArray(ti, dims="points"), + zdim: xr.DataArray(z_index, dims="points"), + ndim: xr.DataArray(node_ids, dims=("points", "nodes")), + } + # Reduce over the "nodes" dimension by name so the result is independent of ``isel`` dim order. + node_data = field.data.isel(selection_dict, ignore_grid=True) + value = (node_data * bcoords).sum("nodes").data + if np.any(tau > 0): + selection_dict[tdim] = xr.DataArray(np.clip(ti + 1, 0, field.data.sizes[tdim] - 1), dims="points") + node_data_next = field.data.isel(selection_dict, ignore_grid=True) + value = (1 - tau) * value + tau * (node_data_next * bcoords).sum("nodes").data + return value # The zi refers to the vertical layer index. The field in this routine are assumed to be defined at the vertical interface levels. # For interface zi, the interface indices are [zi, zi+1], so we need to use the values at zi and zi+1. - zdim = field.data.dims[1] - fzk = columns.isel({zdim: xr.DataArray(zi, dims="points")}).data - fzkp1 = columns.isel({zdim: xr.DataArray(zi + 1, dims="points")}).data + # First, do barycentric interpolation in the lateral direction for each interface level + fzk = _zsample(zi) + fzkp1 = _zsample(zi + 1) # Then, do piecewise linear interpolation in the vertical direction value = fzk * (1 - zeta) + fzkp1 * zeta @@ -262,7 +278,7 @@ class UxVerticalCoordinateConstantFace(VerticalCoordinateInterpolator): def columns( self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid ) -> np.ndarray: - raise NotImplementedError + return self._columns_from_scalar(UxConstantFaceConstantZC(), grid_positions, grid) class UxVerticalCoordinateLinearNode(VerticalCoordinateInterpolator): @@ -271,12 +287,4 @@ class UxVerticalCoordinateLinearNode(VerticalCoordinateInterpolator): def columns( self, grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], grid: UxGrid ) -> np.ndarray: - node_ids = grid.uxgrid.face_node_connectivity[grid_positions["FACE"]["index"], :].values - columns = _barycentric_node_values( - grid.z, - grid_positions["T"]["index"], - grid_positions["T"]["bcoord"], - node_ids, - grid_positions["FACE"]["bcoord"], - ) - return columns.values + return self._columns_from_scalar(UxLinearNodeConstantZC(), grid_positions, grid) diff --git a/src/parcels/interpolators/_xinterpolators.py b/src/parcels/interpolators/_xinterpolators.py index 6ef5141fc3..805a8fa3ea 100644 --- a/src/parcels/interpolators/_xinterpolators.py +++ b/src/parcels/interpolators/_xinterpolators.py @@ -11,6 +11,7 @@ import parcels._core.utils.interpolation as i_u import parcels._sgrid as sgrid import parcels._typing as ptyping +from parcels._sgrid.accessor import get_dim_position from parcels.interpolators._base import ScalarInterpolator, VectorInterpolator, VerticalCoordinateInterpolator if TYPE_CHECKING: @@ -614,27 +615,78 @@ def interp( class XVerticalCoordinateConstant(VerticalCoordinateInterpolator): - """Piecewise constant over the grid cell (face registered z), linear in time.""" + """Piecewise constant over the grid cell (face registered z), linear in time. + + Each particle uses the z column at the centre of its cell, so z is discontinuous between cells. + """ def columns( self, grid_positions: dict[ptyping.XgridAxis, dict[str, int | float | np.ndarray]], grid: XGrid ) -> np.ndarray: - raise NotImplementedError + # Search indices are cells (indexed by their lower node). Point each particle exactly (bcoord 0) at its cell's + # face, so XNearest picks that single column. + offsets = _get_offsets_dictionary(grid) + yi, xi = grid_positions["Y"]["index"], grid_positions["X"]["index"] + cell_centre_positions = { + "T": grid_positions["T"], + "Y": {"index": yi + offsets["Y"], "bcoord": np.zeros(np.shape(yi))}, + "X": {"index": xi + offsets["X"], "bcoord": np.zeros(np.shape(xi))}, + } + return self._columns_from_scalar(XNearest(), cell_centre_positions, grid) + + +def _position_between_cell_centres(face_index: np.ndarray, bcoord: np.ndarray) -> tuple[np.ndarray, np.ndarray]: + """Moves an index/barycentric coordinate combination back by half a cell in index space. + + The grid search gives positions relative to the nodes, but face registered data lives at the cell centres, half a + cell further along. Shifting back by half a cell gives the position relative to the cell centres, so node based + interpolators (e.g. XLinear) can interpolate face registered data. Positions before the first cell centre are + clamped onto it. + """ + upper_half = bcoord >= 0.5 + lower_face = np.where(upper_half, face_index, face_index - 1) + centre_bcoord = np.where(upper_half, bcoord - 0.5, bcoord + 0.5) + below_first_centre = lower_face < 0 + return np.where(below_first_centre, 0, lower_face), np.where(below_first_centre, 0.0, centre_bcoord) class XVerticalCoordinateLinear(VerticalCoordinateInterpolator): - """Bilinear between the surrounding z columns (face or node registered z), linear in time.""" + """Bilinear between the surrounding z columns (face or node registered z), linear in time. + + Node registered z is interpolated from the corners of the particle's cell. Face registered z is interpolated from + the four cell centres around the particle. Both are continuous in space. Beyond the outermost cell centres z is + constant. + """ def columns( self, grid_positions: dict[ptyping.XgridAxis, dict[str, int | float | np.ndarray]], grid: XGrid ) -> np.ndarray: - raise NotImplementedError + if get_dim_position(grid.sgrid_metadata, grid.depth.dims[-1]) == "face": + offsets = _get_offsets_dictionary(grid) + yi, eta = _position_between_cell_centres( + grid_positions["Y"]["index"] + offsets["Y"], grid_positions["Y"]["bcoord"] + ) + xi, xsi = _position_between_cell_centres( + grid_positions["X"]["index"] + offsets["X"], grid_positions["X"]["bcoord"] + ) + grid_positions = { + "T": grid_positions["T"], + "Y": {"index": yi, "bcoord": eta}, + "X": {"index": xi, "bcoord": xsi}, + } + return self._columns_from_scalar(XLinear(), grid_positions, grid) class XVerticalCoordinateNearest(VerticalCoordinateInterpolator): - """Nearest z column (face or node registered z), linear in time.""" + """Nearest z column (face or node registered z), linear in time. + + For node registered z this is the nearest corner of the particle's cell. For face registered z the nearest cell + centre is the particle's own cell, so this is the same as XVerticalCoordinateConstant. + """ def columns( self, grid_positions: dict[ptyping.XgridAxis, dict[str, int | float | np.ndarray]], grid: XGrid ) -> np.ndarray: - raise NotImplementedError + if get_dim_position(grid.sgrid_metadata, grid.depth.dims[-1]) == "face": + return XVerticalCoordinateConstant().columns(grid_positions, grid) + return self._columns_from_scalar(XNearest(), grid_positions, grid) diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index 5c409c7f60..a4aa7271ea 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -16,7 +16,7 @@ ) from parcels._core.index_search import _search_time_index from parcels._core.mesh import FlatMesh, SphericalMesh -from parcels._datasets.structured.generated import simple_UV_dataset +from parcels._datasets.structured.generated import simple_UV_dataset, simple_UV_sigma_dataset from parcels._sgrid.core import Padding from parcels.interpolators import ( CGrid_Velocity, @@ -25,9 +25,16 @@ XLinearInvdistLandTracer, XNearest, XPartialslip, + XVerticalCoordinateConstant, + XVerticalCoordinateLinear, + XVerticalCoordinateNearest, ) from parcels.interpolators._base import VectorInterpolator -from parcels.interpolators._xinterpolators import _get_corner_data_Agrid, _get_offsets_dictionary +from parcels.interpolators._xinterpolators import ( + _get_corner_data_Agrid, + _get_offsets_dictionary, + _position_between_cell_centres, +) from parcels.kernels import AdvectionRK4_3D from tests.utils import TEST_DATA @@ -303,6 +310,134 @@ def test_get_offsets_dictionary(): assert offsets["Z"] == 0 +# Time-varying depth used to test the vertical coordinate interpolators. It is linear in level, lateral index and time, +# so the expected z column of each interpolator can be written down exactly, and strictly increasing along the levels. +SIGMA_SHAPE = {"n_times": 3, "nz": 5, "ny": 8, "nx": 8} +DEPTH_PER_LEVEL, DEPTH_PER_Y, DEPTH_PER_X, DEPTH_PER_TIME = 10.0, 0.3, 0.2, 0.1 + + +def _linear_depth(level, y, x, t): + return DEPTH_PER_LEVEL * level + DEPTH_PER_Y * y + DEPTH_PER_X * x + DEPTH_PER_TIME * t + + +def _sigma_grid(registration): + """XGrid with a time-varying depth on the cell centres ("face") or on the cell corners ("node").""" + ds = simple_UV_sigma_dataset(**SIGMA_SHAPE) + lateral_dims = ("YC", "XC") if registration == "face" else ("YG", "XG") + t, level, y, x = np.meshgrid(*(np.arange(n) for n in SIGMA_SHAPE.values()), indexing="ij") + ds = ds.assign_coords(depth=(("time", "ZF", *lateral_dims), _linear_depth(level, y, x, t), {"axis": "Z"})) + return FieldSet.from_sgrid_conventions(ds).U.grid + + +def _random_grid_positions(npart, seed=0): + """Positions in the interior of the grid, away from the edges where the face registered columns are clamped.""" + rng = np.random.default_rng(seed) + return { + "T": {"index": rng.integers(0, SIGMA_SHAPE["n_times"] - 1, npart), "bcoord": rng.random(npart)}, + "Y": {"index": rng.integers(1, SIGMA_SHAPE["ny"] - 3, npart), "bcoord": rng.random(npart)}, + "X": {"index": rng.integers(1, SIGMA_SHAPE["nx"] - 3, npart), "bcoord": rng.random(npart)}, + } + + +@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") +@pytest.mark.parametrize( + ("z_interp_method", "registration", "expected_y", "expected_x"), + [ + # Columns at the particle's cell centre (face indices are the cell indices plus the padding offset) + pytest.param(XVerticalCoordinateConstant(), "face", lambda i, b, o: i + o, lambda i, b, o: i + o, id="constant-face"), + pytest.param(XVerticalCoordinateNearest(), "face", lambda i, b, o: i + o, lambda i, b, o: i + o, id="nearest-face"), + # Column at the nearest corner of the particle's cell + pytest.param( + XVerticalCoordinateNearest(), "node", lambda i, b, o: i + (b > 0.5), lambda i, b, o: i + (b > 0.5), id="nearest-node" + ), + # Bilinear interpolation reproduces the linear depth at the particle's position, measured in face or node indices + pytest.param( + XVerticalCoordinateLinear(), "face", lambda i, b, o: i + o + b - 0.5, lambda i, b, o: i + o + b - 0.5, id="linear-face" + ), + pytest.param(XVerticalCoordinateLinear(), "node", lambda i, b, o: i + b, lambda i, b, o: i + b, id="linear-node"), + ], +) +@pytest.mark.parametrize("time_varying", [True, False]) +def test_vertical_coordinate_columns(z_interp_method, registration, expected_y, expected_x, time_varying): + grid = _sigma_grid(registration) + grid.z_interp_method = z_interp_method + offsets = _get_offsets_dictionary(grid) + gp = _random_grid_positions(npart=200) + if not time_varying: + gp["T"] = {"index": 1, "bcoord": 0.0} # scalar time position, as on a time-constant grid + + columns = grid.z_interp_method.columns(gp, grid) + + expected = _linear_depth( + np.arange(SIGMA_SHAPE["nz"])[np.newaxis, :], + expected_y(gp["Y"]["index"], gp["Y"]["bcoord"], offsets["Y"])[:, np.newaxis], + expected_x(gp["X"]["index"], gp["X"]["bcoord"], offsets["X"])[:, np.newaxis], + np.add(gp["T"]["index"], gp["T"]["bcoord"])[..., np.newaxis] if time_varying else 1.0, + ) + assert columns.shape == (200, SIGMA_SHAPE["nz"]) + np.testing.assert_allclose(columns, expected) + + +@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") +@pytest.mark.parametrize("registration", ["face", "node"]) +def test_vertical_coordinate_linear_is_continuous(registration): + """z is continuous across cell edges, and across the cell centre (where face registered z changes the bracketing + cell centres it interpolates between).""" + ds = simple_UV_sigma_dataset(**SIGMA_SHAPE) + lateral_dims = ("YC", "XC") if registration == "face" else ("YG", "XG") + rng = np.random.default_rng(1) + depth = ds["depth"].values + rng.random(ds["depth"].shape) * 0.01 # non-linear depth, unlike _sigma_grid + ds = ds.assign_coords(depth=(("time", "ZF", *lateral_dims), depth, {"axis": "Z"})) + grid = FieldSet.from_sgrid_conventions(ds).U.grid + gp = _random_grid_positions(npart=100) + yi, npart = gp["Y"]["index"], len(gp["Y"]["index"]) + + def columns_at(y_index, y_bcoord): + return grid.z_interp_method.columns({**gp, "Y": {"index": y_index, "bcoord": np.full(npart, y_bcoord)}}, grid) + + np.testing.assert_allclose(columns_at(yi, 1.0), columns_at(yi + 1, 0.0)) + np.testing.assert_allclose(columns_at(yi, 0.5 - 1e-12), columns_at(yi, 0.5)) + + +@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") +def test_vertical_coordinate_linear_face_is_constant_beyond_last_cell_centre(): + grid = _sigma_grid("face") + gp = _random_grid_positions(npart=10) + last_cell = np.full(10, SIGMA_SHAPE["ny"] - 2) + + def columns_at(y_bcoord): + return grid.z_interp_method.columns({**gp, "Y": {"index": last_cell, "bcoord": np.full(10, y_bcoord)}}, grid) + + np.testing.assert_allclose(columns_at(0.6), columns_at(1.0)) + + +@pytest.mark.parametrize( + ("face_index", "bcoord", "expected_index", "expected_bcoord"), + [ + pytest.param(3, 0.75, 3, 0.25, id="upper-half-of-cell"), + pytest.param(3, 0.5, 3, 0.0, id="at-cell-centre"), + pytest.param(3, 0.25, 2, 0.75, id="lower-half-of-cell"), + pytest.param(0, 0.25, 0, 0.0, id="clamped-before-first-cell-centre"), + ], +) +def test_position_between_cell_centres(face_index, bcoord, expected_index, expected_bcoord): + index, centre_bcoord = _position_between_cell_centres(np.array([face_index]), np.array([bcoord])) + assert index[0] == expected_index + np.testing.assert_allclose(centre_bcoord[0], expected_bcoord) + + +@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") +def test_z_interp_method_defaults_and_validation(): + face_grid, node_grid = _sigma_grid("face"), _sigma_grid("node") + assert isinstance(face_grid.z_interp_method, XVerticalCoordinateLinear) + assert isinstance(node_grid.z_interp_method, XVerticalCoordinateLinear) + + with pytest.raises(ValueError, match="needs depth on the cell centres"): + node_grid.z_interp_method = XVerticalCoordinateConstant() + with pytest.raises(ValueError, match="must be a `VerticalCoordinateInterpolator`"): + face_grid.z_interp_method = XLinear() + + class XNearest_Velocity(VectorInterpolator): # noqa: N801 """Nearest-Neighbour interpolation on a regular grid for VectorFields of velocity.""" From 7bebb281b73f4a7c15b5427515d8e579bd50eed3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:49:10 +0000 Subject: [PATCH 05/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/parcels/interpolators/_uxinterpolators.py | 3 ++- tests/test_interpolation.py | 24 +++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/parcels/interpolators/_uxinterpolators.py b/src/parcels/interpolators/_uxinterpolators.py index 51afb5deb0..21d40b5e65 100644 --- a/src/parcels/interpolators/_uxinterpolators.py +++ b/src/parcels/interpolators/_uxinterpolators.py @@ -156,7 +156,8 @@ def interp( def _zsample(z_index): """Barycentric (lateral) interpolation of the node values at a single vertical interface level, linear in - time between the snapshots ti and ti + 1.""" + time between the snapshots ti and ti + 1. + """ selection_dict = { tdim: xr.DataArray(ti, dims="points"), zdim: xr.DataArray(z_index, dims="points"), diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index a4aa7271ea..9782fe9ca6 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -344,17 +344,31 @@ def _random_grid_positions(npart, seed=0): ("z_interp_method", "registration", "expected_y", "expected_x"), [ # Columns at the particle's cell centre (face indices are the cell indices plus the padding offset) - pytest.param(XVerticalCoordinateConstant(), "face", lambda i, b, o: i + o, lambda i, b, o: i + o, id="constant-face"), - pytest.param(XVerticalCoordinateNearest(), "face", lambda i, b, o: i + o, lambda i, b, o: i + o, id="nearest-face"), + pytest.param( + XVerticalCoordinateConstant(), "face", lambda i, b, o: i + o, lambda i, b, o: i + o, id="constant-face" + ), + pytest.param( + XVerticalCoordinateNearest(), "face", lambda i, b, o: i + o, lambda i, b, o: i + o, id="nearest-face" + ), # Column at the nearest corner of the particle's cell pytest.param( - XVerticalCoordinateNearest(), "node", lambda i, b, o: i + (b > 0.5), lambda i, b, o: i + (b > 0.5), id="nearest-node" + XVerticalCoordinateNearest(), + "node", + lambda i, b, o: i + (b > 0.5), + lambda i, b, o: i + (b > 0.5), + id="nearest-node", ), # Bilinear interpolation reproduces the linear depth at the particle's position, measured in face or node indices pytest.param( - XVerticalCoordinateLinear(), "face", lambda i, b, o: i + o + b - 0.5, lambda i, b, o: i + o + b - 0.5, id="linear-face" + XVerticalCoordinateLinear(), + "face", + lambda i, b, o: i + o + b - 0.5, + lambda i, b, o: i + o + b - 0.5, + id="linear-face", + ), + pytest.param( + XVerticalCoordinateLinear(), "node", lambda i, b, o: i + b, lambda i, b, o: i + b, id="linear-node" ), - pytest.param(XVerticalCoordinateLinear(), "node", lambda i, b, o: i + b, lambda i, b, o: i + b, id="linear-node"), ], ) @pytest.mark.parametrize("time_varying", [True, False]) From a8a5de27af2f0e102685ec023d0838f2a745f873 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 15:32:18 +0000 Subject: [PATCH 06/16] swap time indexing in uxinterpolators to be more compatable with windowed array --- src/parcels/interpolators/_uxinterpolators.py | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/parcels/interpolators/_uxinterpolators.py b/src/parcels/interpolators/_uxinterpolators.py index 21d40b5e65..f5995dcce2 100644 --- a/src/parcels/interpolators/_uxinterpolators.py +++ b/src/parcels/interpolators/_uxinterpolators.py @@ -33,18 +33,21 @@ def interp( ) tdim, zdim, fdim = field.data.dims + + if np.any(tau > 0): + t_levels = np.stack([ti, np.clip(ti + 1, 0, field.data.sizes[tdim] - 1)]) + t_weights = np.stack([1 - tau, tau]) + else: + t_levels, t_weights = ti[np.newaxis], np.ones((1, ti.size)) selection_dict = { - tdim: xr.DataArray(ti, dims="points"), + tdim: xr.DataArray(t_levels, dims=("tlevels", "points")), zdim: xr.DataArray(zi, dims="points"), fdim: xr.DataArray(fi, dims="points"), } - value = field.data.isel(selection_dict, ignore_grid=True).data + face_data = field.data.isel(selection_dict, ignore_grid=True) # Linear interpolation in time between the snapshots ti and ti + 1 - if np.any(tau > 0): - selection_dict[tdim] = xr.DataArray(np.clip(ti + 1, 0, field.data.sizes[tdim] - 1), dims="points") - value_next = field.data.isel(selection_dict, ignore_grid=True).data - value = (1 - tau) * value + tau * value_next + value = (face_data * xr.DataArray(t_weights, dims=("tlevels", "points"))).sum("tlevels").data return value.compute() if is_dask_collection(value) else value @@ -111,20 +114,22 @@ def interp( node_ids = field.grid.uxgrid.face_node_connectivity[fi, :].values tdim, zdim, ndim = field.data.dims + + if np.any(tau > 0): + t_levels = np.stack([ti, np.clip(ti + 1, 0, field.data.sizes[tdim] - 1)]) + t_weights = np.stack([1 - tau, tau]) + else: + t_levels, t_weights = ti[np.newaxis], np.ones((1, ti.size)) selection_dict = { - tdim: xr.DataArray(ti, dims="points"), + tdim: xr.DataArray(t_levels, dims=("tlevels", "points")), zdim: xr.DataArray(zi, dims="points"), ndim: xr.DataArray(node_ids, dims=("points", "nodes")), } node_data = field.data.isel(selection_dict, ignore_grid=True) - value = (node_data * bcoords).sum("nodes").data # Barycentric interpolation in the lateral direction - - # Linear interpolation in time between the snapshots ti and ti + 1 - if np.any(tau > 0): - selection_dict[tdim] = xr.DataArray(np.clip(ti + 1, 0, field.data.sizes[tdim] - 1), dims="points") - node_data_next = field.data.isel(selection_dict, ignore_grid=True) - value = (1 - tau) * value + tau * (node_data_next * bcoords).sum("nodes").data + # Barycentric interpolation in the lateral direction and linear interpolation in time + weights = bcoords * xr.DataArray(t_weights, dims=("tlevels", "points")) + value = (node_data * weights).sum(("nodes", "tlevels")).data return value.compute() if is_dask_collection(value) else value @@ -154,23 +159,26 @@ def interp( tdim, zdim, ndim = field.data.dims + if np.any(tau > 0): + t_levels = np.stack([ti, np.clip(ti + 1, 0, field.data.sizes[tdim] - 1)]) + t_weights = np.stack([1 - tau, tau]) + else: + t_levels, t_weights = ti[np.newaxis], np.ones((1, ti.size)) + # Barycentric weights in the lateral direction combined with linear weights in time + weights = bcoords * xr.DataArray(t_weights, dims=("tlevels", "points")) + def _zsample(z_index): """Barycentric (lateral) interpolation of the node values at a single vertical interface level, linear in time between the snapshots ti and ti + 1. """ selection_dict = { - tdim: xr.DataArray(ti, dims="points"), + tdim: xr.DataArray(t_levels, dims=("tlevels", "points")), zdim: xr.DataArray(z_index, dims="points"), ndim: xr.DataArray(node_ids, dims=("points", "nodes")), } - # Reduce over the "nodes" dimension by name so the result is independent of ``isel`` dim order. + # Reduce over the "nodes" and "tlevels" dimensions by name so the result is independent of ``isel`` dim order. node_data = field.data.isel(selection_dict, ignore_grid=True) - value = (node_data * bcoords).sum("nodes").data - if np.any(tau > 0): - selection_dict[tdim] = xr.DataArray(np.clip(ti + 1, 0, field.data.sizes[tdim] - 1), dims="points") - node_data_next = field.data.isel(selection_dict, ignore_grid=True) - value = (1 - tau) * value + tau * (node_data_next * bcoords).sum("nodes").data - return value + return (node_data * weights).sum(("nodes", "tlevels")).data # The zi refers to the vertical layer index. The field in this routine are assumed to be defined at the vertical interface levels. # For interface zi, the interface indices are [zi, zi+1], so we need to use the values at zi and zi+1. From dd5f01d6e09fbf096d4c8d526e97c5091fe9a246 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 16:03:09 +0000 Subject: [PATCH 07/16] make all ux interpolators linear in time --- src/parcels/interpolators/_uxinterpolators.py | 83 ++++++++++++++----- 1 file changed, 60 insertions(+), 23 deletions(-) diff --git a/src/parcels/interpolators/_uxinterpolators.py b/src/parcels/interpolators/_uxinterpolators.py index f5995dcce2..752d3f23bf 100644 --- a/src/parcels/interpolators/_uxinterpolators.py +++ b/src/parcels/interpolators/_uxinterpolators.py @@ -63,28 +63,41 @@ def interp( grid_positions: dict[_UXGRID_AXES, dict[str, int | float | np.ndarray]], field: Field, ): - ti, zi, fi = np.broadcast_arrays( - grid_positions["T"]["index"], grid_positions["Z"]["index"], grid_positions["FACE"]["index"] + ti, tau, zi, fi = np.broadcast_arrays( + grid_positions["T"]["index"], + grid_positions["T"]["bcoord"], + grid_positions["Z"]["index"], + grid_positions["FACE"]["index"], ) zeta = grid_positions["Z"]["bcoord"] tdim, zdim, fdim = field.data.dims - def _zsample(z_index): - """Pointwise ``isel`` of the face values at a single vertical interface level.""" + if np.any(tau > 0): + t_levels = np.stack([ti, np.clip(ti + 1, 0, field.data.sizes[tdim] - 1)]) + t_weights = np.stack([1 - tau, tau]) + else: + t_levels, t_weights = ti[np.newaxis], np.ones((1, ti.size)) + time_weights = xr.DataArray(t_weights, dims=("tlevels", "points")) + + def _z_and_t_sample(z_index): + """Pointwise ``isel`` of the face values at a single vertical interface level, linear in time between the + snapshots ti and ti + 1. + """ selection_dict = { - tdim: xr.DataArray(ti, dims="points"), + tdim: xr.DataArray(t_levels, dims=("tlevels", "points")), zdim: xr.DataArray(z_index, dims="points"), fdim: xr.DataArray(fi, dims="points"), } - value = field.data.isel(selection_dict, ignore_grid=True).data + face_data = field.data.isel(selection_dict, ignore_grid=True) + value = (face_data * time_weights).sum("tlevels").data return value.compute() if is_dask_collection(value) else value # The zi refers to the vertical layer index. The field in this routine are assumed to be defined at the vertical interface levels. # For interface zi, the interface indices are [zi, zi+1], so we need to use the values at zi and zi+1. # First, do barycentric interpolation in the lateral direction for each interface level - fzk = _zsample(zi) - fzkp1 = _zsample(zi + 1) + fzk = _z_and_t_sample(zi) + fzkp1 = _z_and_t_sample(zi + 1) # Then, do piecewise linear interpolation in the vertical direction return fzk * (1 - zeta) + fzkp1 * zeta @@ -167,7 +180,7 @@ def interp( # Barycentric weights in the lateral direction combined with linear weights in time weights = bcoords * xr.DataArray(t_weights, dims=("tlevels", "points")) - def _zsample(z_index): + def _z_and_t_sample(z_index): """Barycentric (lateral) interpolation of the node values at a single vertical interface level, linear in time between the snapshots ti and ti + 1. """ @@ -183,8 +196,8 @@ def _zsample(z_index): # The zi refers to the vertical layer index. The field in this routine are assumed to be defined at the vertical interface levels. # For interface zi, the interface indices are [zi, zi+1], so we need to use the values at zi and zi+1. # First, do barycentric interpolation in the lateral direction for each interface level - fzk = _zsample(zi) - fzkp1 = _zsample(zi + 1) + fzk = _z_and_t_sample(zi) + fzkp1 = _z_and_t_sample(zi + 1) # Then, do piecewise linear interpolation in the vertical direction value = fzk * (1 - zeta) + fzkp1 * zeta @@ -204,19 +217,31 @@ def interp( field: Field, ): # Broadcast the per-axis indices to a common (npart,) shape (``ti`` may be scalar for time-constant fields) - ti, zi, fi = np.broadcast_arrays( - grid_positions["T"]["index"], grid_positions["Z"]["index"], grid_positions["FACE"]["index"] + ti, tau, zi, fi = np.broadcast_arrays( + grid_positions["T"]["index"], + grid_positions["T"]["bcoord"], + grid_positions["Z"]["index"], + grid_positions["FACE"]["index"], ) edge_ids = field.grid.uxgrid.face_edge_connectivity[fi, :].values tdim, zdim, edim = field.data.dims + + if np.any(tau > 0): + t_levels = np.stack([ti, np.clip(ti + 1, 0, field.data.sizes[tdim] - 1)]) + t_weights = np.stack([1 - tau, tau]) + else: + t_levels, t_weights = ti[np.newaxis], np.ones((1, ti.size)) selection_dict = { - tdim: xr.DataArray(ti, dims="points"), + tdim: xr.DataArray(t_levels, dims=("tlevels", "points")), zdim: xr.DataArray(zi, dims="points"), edim: xr.DataArray(edge_ids, dims=("points", "edges")), } edge_data = field.data.isel(selection_dict, ignore_grid=True) - value = edge_data.mean("edges").data # Face value is the mean of its surrounding edges + face_data = edge_data.mean("edges") # Face value is the mean of its surrounding edges + + # Linear interpolation in time between the snapshots ti and ti + 1 + value = (face_data * xr.DataArray(t_weights, dims=("tlevels", "points"))).sum("tlevels").data return value.compute() if is_dask_collection(value) else value @@ -234,27 +259,39 @@ def interp( field: Field, ): # Broadcast the per-axis indices to a common (npart,) shape (``ti`` may be scalar for time-constant fields) - ti, zi, fi = np.broadcast_arrays( - grid_positions["T"]["index"], grid_positions["Z"]["index"], grid_positions["FACE"]["index"] + ti, tau, zi, fi = np.broadcast_arrays( + grid_positions["T"]["index"], + grid_positions["T"]["bcoord"], + grid_positions["Z"]["index"], + grid_positions["FACE"]["index"], ) zeta = grid_positions["Z"]["bcoord"] edge_ids = field.grid.uxgrid.face_edge_connectivity[fi, :].values tdim, zdim, edim = field.data.dims - def _zsample(z_index): - """Mean of the edge values surrounding each face at a single vertical interface level.""" + if np.any(tau > 0): + t_levels = np.stack([ti, np.clip(ti + 1, 0, field.data.sizes[tdim] - 1)]) + t_weights = np.stack([1 - tau, tau]) + else: + t_levels, t_weights = ti[np.newaxis], np.ones((1, ti.size)) + time_weights = xr.DataArray(t_weights, dims=("tlevels", "points")) + + def _z_and_t_sample(z_index): + """Mean of the edge values surrounding each face at a single vertical interface level, linear in time + between the snapshots ti and ti + 1. + """ selection_dict = { - tdim: xr.DataArray(ti, dims="points"), + tdim: xr.DataArray(t_levels, dims=("tlevels", "points")), zdim: xr.DataArray(z_index, dims="points"), edim: xr.DataArray(edge_ids, dims=("points", "edges")), } edge_data = field.data.isel(selection_dict, ignore_grid=True) - return edge_data.mean("edges").data + return (edge_data.mean("edges") * time_weights).sum("tlevels").data # zi is the vertical layer index, bounded by the interface levels zi and zi + 1 - fzk = _zsample(zi) - fzkp1 = _zsample(zi + 1) + fzk = _z_and_t_sample(zi) + fzkp1 = _z_and_t_sample(zi + 1) value = fzk * (1 - zeta) + fzkp1 * zeta return value.compute() if is_dask_collection(value) else value From c016552a10c2d362a40b898489582e5462448698 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 18:08:24 +0000 Subject: [PATCH 08/16] Move vertical interpolator testing to its own file --- tests/test_interpolation.py | 153 +----------------- .../test_vertical_coordinate_interpolators.py | 145 +++++++++++++++++ 2 files changed, 147 insertions(+), 151 deletions(-) create mode 100644 tests/test_vertical_coordinate_interpolators.py diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index 9782fe9ca6..5c409c7f60 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -16,7 +16,7 @@ ) from parcels._core.index_search import _search_time_index from parcels._core.mesh import FlatMesh, SphericalMesh -from parcels._datasets.structured.generated import simple_UV_dataset, simple_UV_sigma_dataset +from parcels._datasets.structured.generated import simple_UV_dataset from parcels._sgrid.core import Padding from parcels.interpolators import ( CGrid_Velocity, @@ -25,16 +25,9 @@ XLinearInvdistLandTracer, XNearest, XPartialslip, - XVerticalCoordinateConstant, - XVerticalCoordinateLinear, - XVerticalCoordinateNearest, ) from parcels.interpolators._base import VectorInterpolator -from parcels.interpolators._xinterpolators import ( - _get_corner_data_Agrid, - _get_offsets_dictionary, - _position_between_cell_centres, -) +from parcels.interpolators._xinterpolators import _get_corner_data_Agrid, _get_offsets_dictionary from parcels.kernels import AdvectionRK4_3D from tests.utils import TEST_DATA @@ -310,148 +303,6 @@ def test_get_offsets_dictionary(): assert offsets["Z"] == 0 -# Time-varying depth used to test the vertical coordinate interpolators. It is linear in level, lateral index and time, -# so the expected z column of each interpolator can be written down exactly, and strictly increasing along the levels. -SIGMA_SHAPE = {"n_times": 3, "nz": 5, "ny": 8, "nx": 8} -DEPTH_PER_LEVEL, DEPTH_PER_Y, DEPTH_PER_X, DEPTH_PER_TIME = 10.0, 0.3, 0.2, 0.1 - - -def _linear_depth(level, y, x, t): - return DEPTH_PER_LEVEL * level + DEPTH_PER_Y * y + DEPTH_PER_X * x + DEPTH_PER_TIME * t - - -def _sigma_grid(registration): - """XGrid with a time-varying depth on the cell centres ("face") or on the cell corners ("node").""" - ds = simple_UV_sigma_dataset(**SIGMA_SHAPE) - lateral_dims = ("YC", "XC") if registration == "face" else ("YG", "XG") - t, level, y, x = np.meshgrid(*(np.arange(n) for n in SIGMA_SHAPE.values()), indexing="ij") - ds = ds.assign_coords(depth=(("time", "ZF", *lateral_dims), _linear_depth(level, y, x, t), {"axis": "Z"})) - return FieldSet.from_sgrid_conventions(ds).U.grid - - -def _random_grid_positions(npart, seed=0): - """Positions in the interior of the grid, away from the edges where the face registered columns are clamped.""" - rng = np.random.default_rng(seed) - return { - "T": {"index": rng.integers(0, SIGMA_SHAPE["n_times"] - 1, npart), "bcoord": rng.random(npart)}, - "Y": {"index": rng.integers(1, SIGMA_SHAPE["ny"] - 3, npart), "bcoord": rng.random(npart)}, - "X": {"index": rng.integers(1, SIGMA_SHAPE["nx"] - 3, npart), "bcoord": rng.random(npart)}, - } - - -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") -@pytest.mark.parametrize( - ("z_interp_method", "registration", "expected_y", "expected_x"), - [ - # Columns at the particle's cell centre (face indices are the cell indices plus the padding offset) - pytest.param( - XVerticalCoordinateConstant(), "face", lambda i, b, o: i + o, lambda i, b, o: i + o, id="constant-face" - ), - pytest.param( - XVerticalCoordinateNearest(), "face", lambda i, b, o: i + o, lambda i, b, o: i + o, id="nearest-face" - ), - # Column at the nearest corner of the particle's cell - pytest.param( - XVerticalCoordinateNearest(), - "node", - lambda i, b, o: i + (b > 0.5), - lambda i, b, o: i + (b > 0.5), - id="nearest-node", - ), - # Bilinear interpolation reproduces the linear depth at the particle's position, measured in face or node indices - pytest.param( - XVerticalCoordinateLinear(), - "face", - lambda i, b, o: i + o + b - 0.5, - lambda i, b, o: i + o + b - 0.5, - id="linear-face", - ), - pytest.param( - XVerticalCoordinateLinear(), "node", lambda i, b, o: i + b, lambda i, b, o: i + b, id="linear-node" - ), - ], -) -@pytest.mark.parametrize("time_varying", [True, False]) -def test_vertical_coordinate_columns(z_interp_method, registration, expected_y, expected_x, time_varying): - grid = _sigma_grid(registration) - grid.z_interp_method = z_interp_method - offsets = _get_offsets_dictionary(grid) - gp = _random_grid_positions(npart=200) - if not time_varying: - gp["T"] = {"index": 1, "bcoord": 0.0} # scalar time position, as on a time-constant grid - - columns = grid.z_interp_method.columns(gp, grid) - - expected = _linear_depth( - np.arange(SIGMA_SHAPE["nz"])[np.newaxis, :], - expected_y(gp["Y"]["index"], gp["Y"]["bcoord"], offsets["Y"])[:, np.newaxis], - expected_x(gp["X"]["index"], gp["X"]["bcoord"], offsets["X"])[:, np.newaxis], - np.add(gp["T"]["index"], gp["T"]["bcoord"])[..., np.newaxis] if time_varying else 1.0, - ) - assert columns.shape == (200, SIGMA_SHAPE["nz"]) - np.testing.assert_allclose(columns, expected) - - -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") -@pytest.mark.parametrize("registration", ["face", "node"]) -def test_vertical_coordinate_linear_is_continuous(registration): - """z is continuous across cell edges, and across the cell centre (where face registered z changes the bracketing - cell centres it interpolates between).""" - ds = simple_UV_sigma_dataset(**SIGMA_SHAPE) - lateral_dims = ("YC", "XC") if registration == "face" else ("YG", "XG") - rng = np.random.default_rng(1) - depth = ds["depth"].values + rng.random(ds["depth"].shape) * 0.01 # non-linear depth, unlike _sigma_grid - ds = ds.assign_coords(depth=(("time", "ZF", *lateral_dims), depth, {"axis": "Z"})) - grid = FieldSet.from_sgrid_conventions(ds).U.grid - gp = _random_grid_positions(npart=100) - yi, npart = gp["Y"]["index"], len(gp["Y"]["index"]) - - def columns_at(y_index, y_bcoord): - return grid.z_interp_method.columns({**gp, "Y": {"index": y_index, "bcoord": np.full(npart, y_bcoord)}}, grid) - - np.testing.assert_allclose(columns_at(yi, 1.0), columns_at(yi + 1, 0.0)) - np.testing.assert_allclose(columns_at(yi, 0.5 - 1e-12), columns_at(yi, 0.5)) - - -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") -def test_vertical_coordinate_linear_face_is_constant_beyond_last_cell_centre(): - grid = _sigma_grid("face") - gp = _random_grid_positions(npart=10) - last_cell = np.full(10, SIGMA_SHAPE["ny"] - 2) - - def columns_at(y_bcoord): - return grid.z_interp_method.columns({**gp, "Y": {"index": last_cell, "bcoord": np.full(10, y_bcoord)}}, grid) - - np.testing.assert_allclose(columns_at(0.6), columns_at(1.0)) - - -@pytest.mark.parametrize( - ("face_index", "bcoord", "expected_index", "expected_bcoord"), - [ - pytest.param(3, 0.75, 3, 0.25, id="upper-half-of-cell"), - pytest.param(3, 0.5, 3, 0.0, id="at-cell-centre"), - pytest.param(3, 0.25, 2, 0.75, id="lower-half-of-cell"), - pytest.param(0, 0.25, 0, 0.0, id="clamped-before-first-cell-centre"), - ], -) -def test_position_between_cell_centres(face_index, bcoord, expected_index, expected_bcoord): - index, centre_bcoord = _position_between_cell_centres(np.array([face_index]), np.array([bcoord])) - assert index[0] == expected_index - np.testing.assert_allclose(centre_bcoord[0], expected_bcoord) - - -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") -def test_z_interp_method_defaults_and_validation(): - face_grid, node_grid = _sigma_grid("face"), _sigma_grid("node") - assert isinstance(face_grid.z_interp_method, XVerticalCoordinateLinear) - assert isinstance(node_grid.z_interp_method, XVerticalCoordinateLinear) - - with pytest.raises(ValueError, match="needs depth on the cell centres"): - node_grid.z_interp_method = XVerticalCoordinateConstant() - with pytest.raises(ValueError, match="must be a `VerticalCoordinateInterpolator`"): - face_grid.z_interp_method = XLinear() - - class XNearest_Velocity(VectorInterpolator): # noqa: N801 """Nearest-Neighbour interpolation on a regular grid for VectorFields of velocity.""" diff --git a/tests/test_vertical_coordinate_interpolators.py b/tests/test_vertical_coordinate_interpolators.py new file mode 100644 index 0000000000..ebcf2a2cfb --- /dev/null +++ b/tests/test_vertical_coordinate_interpolators.py @@ -0,0 +1,145 @@ +import numpy as np +import pytest + +from parcels import FieldSet +from parcels._datasets.structured.generated import simple_UV_sigma_dataset +from parcels.interpolators import ( + XLinear, + XVerticalCoordinateConstant, + XVerticalCoordinateLinear, + XVerticalCoordinateNearest, +) +from parcels.interpolators._xinterpolators import _get_offsets_dictionary, _position_between_cell_centres + +# Time-varying depth used to test the vertical coordinate interpolators. It is linear in level, lateral index and time, +# so the expected z column of each interpolator can be written down exactly, and strictly increasing along the levels. +SIGMA_SHAPE = {"n_times": 3, "nz": 5, "ny": 8, "nx": 8} +DEPTH_PER_LEVEL, DEPTH_PER_Y, DEPTH_PER_X, DEPTH_PER_TIME = 10.0, 0.3, 0.2, 0.1 + + +def _linear_depth(level, y, x, t): + return DEPTH_PER_LEVEL * level + DEPTH_PER_Y * y + DEPTH_PER_X * x + DEPTH_PER_TIME * t + + +def _sigma_grid(registration): + """XGrid with a time-varying depth on the cell centres ("face") or on the cell corners ("node").""" + ds = simple_UV_sigma_dataset(**SIGMA_SHAPE) + lateral_dims = ("YC", "XC") if registration == "face" else ("YG", "XG") + t, level, y, x = np.meshgrid(*(np.arange(n) for n in SIGMA_SHAPE.values()), indexing="ij") + ds = ds.assign_coords(depth=(("time", "ZF", *lateral_dims), _linear_depth(level, y, x, t), {"axis": "Z"})) + return FieldSet.from_sgrid_conventions(ds).U.grid + + +def _random_grid_positions(npart, seed=0): + """Positions in the interior of the grid, away from the edges where the face registered columns are clamped.""" + rng = np.random.default_rng(seed) + return { + "T": {"index": rng.integers(0, SIGMA_SHAPE["n_times"] - 1, npart), "bcoord": rng.random(npart)}, + "Y": {"index": rng.integers(1, SIGMA_SHAPE["ny"] - 3, npart), "bcoord": rng.random(npart)}, + "X": {"index": rng.integers(1, SIGMA_SHAPE["nx"] - 3, npart), "bcoord": rng.random(npart)}, + } + + +# Expected lateral position of a particle's z column, from its search index (i), barycentric coordinate (b) and the +# padding offset between cell and face indices (o), measured in the indices of the registration's depth +def _cell_centre(i, b, o): + return i + o + + +def _nearest_corner(i, b, o): + return i + (b > 0.5) + + +def _position_in_face_indices(i, b, o): + return i + o + b - 0.5 + + +def _position_in_node_indices(i, b, o): + return i + b + + +@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") +@pytest.mark.parametrize( + ("z_interp_method", "registration", "expected_y", "expected_x"), + [ + # Columns at the particle's cell centre (face indices are the cell indices plus the padding offset) + pytest.param(XVerticalCoordinateConstant(), "face", _cell_centre, _cell_centre, id="constant-face"), + pytest.param(XVerticalCoordinateNearest(), "face", _cell_centre, _cell_centre, id="nearest-face"), + # Column at the nearest corner of the particle's cell + pytest.param(XVerticalCoordinateNearest(), "node", _nearest_corner, _nearest_corner, id="nearest-node"), + # Bilinear interpolation reproduces the linear depth at the particle's position, measured in face or node indices + pytest.param( + XVerticalCoordinateLinear(), "face", _position_in_face_indices, _position_in_face_indices, id="linear-face" + ), + pytest.param( + XVerticalCoordinateLinear(), "node", _position_in_node_indices, _position_in_node_indices, id="linear-node" + ), + ], +) +@pytest.mark.parametrize("time_varying", [True, False]) +def test_vertical_coordinate_columns(z_interp_method, registration, expected_y, expected_x, time_varying): + grid = _sigma_grid(registration) + grid.z_interp_method = z_interp_method + offsets = _get_offsets_dictionary(grid) + gp = _random_grid_positions(npart=200) + if not time_varying: + gp["T"] = {"index": 1, "bcoord": 0.0} # scalar time position, as on a time-constant grid + + columns = grid.z_interp_method.columns(gp, grid) + + expected = _linear_depth( + np.arange(SIGMA_SHAPE["nz"])[np.newaxis, :], + expected_y(gp["Y"]["index"], gp["Y"]["bcoord"], offsets["Y"])[:, np.newaxis], + expected_x(gp["X"]["index"], gp["X"]["bcoord"], offsets["X"])[:, np.newaxis], + np.add(gp["T"]["index"], gp["T"]["bcoord"])[..., np.newaxis] if time_varying else 1.0, + ) + assert columns.shape == (200, SIGMA_SHAPE["nz"]) + np.testing.assert_allclose(columns, expected) + + +@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") +@pytest.mark.parametrize("registration", ["face", "node"]) +def test_vertical_coordinate_linear_is_continuous(registration): + """z is continuous across cell edges, and across the cell centre (where face registered z changes the bracketing + cell centres it interpolates between).""" + ds = simple_UV_sigma_dataset(**SIGMA_SHAPE) + lateral_dims = ("YC", "XC") if registration == "face" else ("YG", "XG") + rng = np.random.default_rng(1) + depth = ds["depth"].values + rng.random(ds["depth"].shape) * 0.01 # non-linear depth, unlike _sigma_grid + ds = ds.assign_coords(depth=(("time", "ZF", *lateral_dims), depth, {"axis": "Z"})) + grid = FieldSet.from_sgrid_conventions(ds).U.grid + gp = _random_grid_positions(npart=100) + yi, npart = gp["Y"]["index"], len(gp["Y"]["index"]) + + def columns_at(y_index, y_bcoord): + return grid.z_interp_method.columns({**gp, "Y": {"index": y_index, "bcoord": np.full(npart, y_bcoord)}}, grid) + + np.testing.assert_allclose(columns_at(yi, 1.0), columns_at(yi + 1, 0.0)) + np.testing.assert_allclose(columns_at(yi, 0.5 - 1e-12), columns_at(yi, 0.5)) + + +@pytest.mark.parametrize( + ("face_index", "bcoord", "expected_index", "expected_bcoord"), + [ + pytest.param(3, 0.75, 3, 0.25, id="upper-half-of-cell"), + pytest.param(3, 0.5, 3, 0.0, id="at-cell-centre"), + pytest.param(3, 0.25, 2, 0.75, id="lower-half-of-cell"), + pytest.param(0, 0.25, 0, 0.0, id="clamped-before-first-cell-centre"), + ], +) +def test_position_between_cell_centres(face_index, bcoord, expected_index, expected_bcoord): + index, centre_bcoord = _position_between_cell_centres(np.array([face_index]), np.array([bcoord])) + assert index[0] == expected_index + np.testing.assert_allclose(centre_bcoord[0], expected_bcoord) + + +@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") +def test_z_interp_method_defaults_and_validation(): + face_grid, node_grid = _sigma_grid("face"), _sigma_grid("node") + assert isinstance(face_grid.z_interp_method, XVerticalCoordinateLinear) + assert isinstance(node_grid.z_interp_method, XVerticalCoordinateLinear) + + with pytest.raises(ValueError, match="needs depth on the cell centres"): + node_grid.z_interp_method = XVerticalCoordinateConstant() + with pytest.raises(ValueError, match="must be a `VerticalCoordinateInterpolator`"): + face_grid.z_interp_method = XLinear() From 3cc828e7d9805da2825dc0a23c63ebf4eccc9cb8 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 18:32:01 +0000 Subject: [PATCH 09/16] Add default z interpolator and linear in time interpolation testing for UxGrid --- tests/test_uxarray_fieldset.py | 30 ++++++ .../test_vertical_coordinate_interpolators.py | 95 +++++++++++++++++-- 2 files changed, 115 insertions(+), 10 deletions(-) diff --git a/tests/test_uxarray_fieldset.py b/tests/test_uxarray_fieldset.py index b8a748f377..27f907692f 100644 --- a/tests/test_uxarray_fieldset.py +++ b/tests/test_uxarray_fieldset.py @@ -206,6 +206,36 @@ def test_from_ugrid_conventions_selects_correct_interpolator(): assert isinstance(getattr(fieldset, f"{lateral_dim}_{vertical_dim}").interp_method, expected_interpolator) +def test_ux_interpolators_are_linear_in_time(): + """The default interpolator of every registration is linear in time between snapshots (fields uniform in space).""" + ds = convert.icon_to_ugrid(datasets_unstructured["icon_square_delaunay_uniform_z_coordinate"].copy(deep=True)) + rng = np.random.default_rng(0) + value_at_snapshots = rng.random(ds.sizes["time"]) + registrations = [ + ("zc", "n_face"), ("zf", "n_face"), ("zc", "n_node"), ("zf", "n_node"), ("zc", "n_edge"), ("zf", "n_edge") + ] + for vertical_dim, lateral_dim in registrations: + shape = (ds.sizes["time"], ds.sizes[vertical_dim], getattr(ds.uxgrid, lateral_dim)) + ds[f"{lateral_dim}_{vertical_dim}"] = ux.UxDataArray( + np.broadcast_to(value_at_snapshots[:, np.newaxis, np.newaxis], shape), + dims=["time", vertical_dim, lateral_dim], + coords={"time": ds.time, vertical_dim: ds[vertical_dim]}, + uxgrid=ds.uxgrid, + ) + fieldset = FieldSet.from_ugrid_conventions(ds) + + # One particle at each face centre, at a random time (in seconds since the first snapshot) + xc, yc = ds.uxgrid.face_lon.values, ds.uxgrid.face_lat.values + zq = np.full(xc.shape, 300.0) + snapshot_times = (ds.time.values - ds.time.values[0]) / np.timedelta64(1, "s") + tq = rng.uniform(snapshot_times[0], snapshot_times[-1], xc.size) + + expected = np.interp(tq, snapshot_times, value_at_snapshots) + for vertical_dim, lateral_dim in registrations: + values = getattr(fieldset, f"{lateral_dim}_{vertical_dim}").eval(t=tq, z=zq, y=yc, x=xc) + np.testing.assert_allclose(values, expected, err_msg=f"{lateral_dim}_{vertical_dim}") + + _NESTEDGRIDS_NODES = np.array( [ [10.0, 15.0], diff --git a/tests/test_vertical_coordinate_interpolators.py b/tests/test_vertical_coordinate_interpolators.py index ebcf2a2cfb..3b98b687be 100644 --- a/tests/test_vertical_coordinate_interpolators.py +++ b/tests/test_vertical_coordinate_interpolators.py @@ -1,14 +1,23 @@ +from types import SimpleNamespace + import numpy as np import pytest +import uxarray as ux -from parcels import FieldSet +from parcels import FieldSet, UxGrid +from parcels._core.mesh import FlatMesh from parcels._datasets.structured.generated import simple_UV_sigma_dataset +from parcels._datasets.unstructured.generated import sigma_coordinate_lattice_dataset from parcels.interpolators import ( + UxConstantFaceConstantZC, + UxVerticalCoordinateConstantFace, + UxVerticalCoordinateLinearNode, XLinear, XVerticalCoordinateConstant, XVerticalCoordinateLinear, XVerticalCoordinateNearest, ) +from parcels.interpolators._base import ScalarInterpolator from parcels.interpolators._xinterpolators import _get_offsets_dictionary, _position_between_cell_centres # Time-varying depth used to test the vertical coordinate interpolators. It is linear in level, lateral index and time, @@ -81,17 +90,17 @@ def test_vertical_coordinate_columns(z_interp_method, registration, expected_y, grid = _sigma_grid(registration) grid.z_interp_method = z_interp_method offsets = _get_offsets_dictionary(grid) - gp = _random_grid_positions(npart=200) + grid_positions = _random_grid_positions(npart=200) if not time_varying: - gp["T"] = {"index": 1, "bcoord": 0.0} # scalar time position, as on a time-constant grid + grid_positions["T"] = {"index": 1, "bcoord": 0.0} # scalar time position, as on a time-constant grid - columns = grid.z_interp_method.columns(gp, grid) + columns = grid.z_interp_method.columns(grid_positions, grid) expected = _linear_depth( np.arange(SIGMA_SHAPE["nz"])[np.newaxis, :], - expected_y(gp["Y"]["index"], gp["Y"]["bcoord"], offsets["Y"])[:, np.newaxis], - expected_x(gp["X"]["index"], gp["X"]["bcoord"], offsets["X"])[:, np.newaxis], - np.add(gp["T"]["index"], gp["T"]["bcoord"])[..., np.newaxis] if time_varying else 1.0, + expected_y(grid_positions["Y"]["index"], grid_positions["Y"]["bcoord"], offsets["Y"])[:, np.newaxis], + expected_x(grid_positions["X"]["index"], grid_positions["X"]["bcoord"], offsets["X"])[:, np.newaxis], + np.add(grid_positions["T"]["index"], grid_positions["T"]["bcoord"])[..., np.newaxis] if time_varying else 1.0, ) assert columns.shape == (200, SIGMA_SHAPE["nz"]) np.testing.assert_allclose(columns, expected) @@ -108,11 +117,13 @@ def test_vertical_coordinate_linear_is_continuous(registration): depth = ds["depth"].values + rng.random(ds["depth"].shape) * 0.01 # non-linear depth, unlike _sigma_grid ds = ds.assign_coords(depth=(("time", "ZF", *lateral_dims), depth, {"axis": "Z"})) grid = FieldSet.from_sgrid_conventions(ds).U.grid - gp = _random_grid_positions(npart=100) - yi, npart = gp["Y"]["index"], len(gp["Y"]["index"]) + grid_positions = _random_grid_positions(npart=100) + yi, npart = grid_positions["Y"]["index"], len(grid_positions["Y"]["index"]) def columns_at(y_index, y_bcoord): - return grid.z_interp_method.columns({**gp, "Y": {"index": y_index, "bcoord": np.full(npart, y_bcoord)}}, grid) + return grid.z_interp_method.columns( + {**grid_positions, "Y": {"index": y_index, "bcoord": np.full(npart, y_bcoord)}}, grid + ) np.testing.assert_allclose(columns_at(yi, 1.0), columns_at(yi + 1, 0.0)) np.testing.assert_allclose(columns_at(yi, 0.5 - 1e-12), columns_at(yi, 0.5)) @@ -143,3 +154,67 @@ def test_z_interp_method_defaults_and_validation(): node_grid.z_interp_method = XVerticalCoordinateConstant() with pytest.raises(ValueError, match="must be a `VerticalCoordinateInterpolator`"): face_grid.z_interp_method = XLinear() + + +class _EchoInterpolator(ScalarInterpolator): + """Records the grid positions it is evaluated at, and returns 100 * time index + 10 * face index + level index.""" + + def interp(self, particle_positions, grid_positions, field): + self.grid_positions = grid_positions + return 100 * grid_positions["T"]["index"] + 10 * grid_positions["FACE"]["index"] + grid_positions["Z"]["index"] + + +@pytest.mark.parametrize("time_varying", [True, False]) +def test_columns_from_scalar(time_varying): + npart, nlev = 4, 3 + grid = SimpleNamespace(depth=np.zeros((2, nlev, 5))) # only the number of levels of depth is used + fi, face_bcoords = np.array([4, 0, 2, 3]), np.arange(npart * 3).reshape(npart, 3) / 10 + ti, tau = (np.array([0, 1, 1, 0]), np.array([0.1, 0.2, 0.3, 0.4])) if time_varying else (1, 0.0) + grid_positions = {"T": {"index": ti, "bcoord": tau}, "FACE": {"index": fi, "bcoord": face_bcoords}} + echo = _EchoInterpolator() + + columns = UxVerticalCoordinateLinearNode()._columns_from_scalar(echo, grid_positions, grid) + + np.testing.assert_array_equal(columns, 100 * np.reshape(ti, (-1, 1)) + 10 * fi[:, np.newaxis] + np.arange(nlev)) + + # Each level is evaluated exactly on the level, at its particle's face and time barycentric coordinates + recorded = echo.grid_positions + np.testing.assert_array_equal(recorded["Z"]["bcoord"], 0.0) + np.testing.assert_array_equal( + recorded["FACE"]["bcoord"].reshape(npart, nlev, 3), + np.broadcast_to(face_bcoords[:, np.newaxis], (npart, nlev, 3)), + ) + np.testing.assert_array_equal( + recorded["T"]["bcoord"].reshape(npart, nlev), + np.broadcast_to(np.reshape(tau, (-1, 1)), (npart, nlev)), + ) + + +def _time_varying_uxgrid(lateral_dim): + """UxGrid on a small lattice with a time-varying z, increasing along the levels, on the given lateral dimension.""" + uxgrid = sigma_coordinate_lattice_dataset(3, (0.0, 1.0), (0.0, 1.0), 4, bottom_depth=1.0).uxgrid + z = np.broadcast_to(np.arange(4.0)[:, np.newaxis], (2, 4, getattr(uxgrid, lateral_dim))) + return UxGrid(uxgrid, z=ux.UxDataArray(z, dims=("time", "zf", lateral_dim), uxgrid=uxgrid), mesh=FlatMesh()) + + +@pytest.mark.filterwarnings("ignore:Time-varying \\(3D\\) z coordinates are experimental") +def test_ux_z_interp_method_defaults_and_validation(): + face_grid, node_grid = _time_varying_uxgrid("n_face"), _time_varying_uxgrid("n_node") + assert isinstance(face_grid.z_interp_method, UxVerticalCoordinateConstantFace) + assert isinstance(node_grid.z_interp_method, UxVerticalCoordinateLinearNode) + + with pytest.raises(ValueError, match="needs z on the mesh faces"): + node_grid.z_interp_method = UxVerticalCoordinateConstantFace() + with pytest.raises(ValueError, match="needs z on the mesh nodes"): + face_grid.z_interp_method = UxVerticalCoordinateLinearNode() + with pytest.raises(ValueError, match="must be a `VerticalCoordinateInterpolator`"): + face_grid.z_interp_method = UxConstantFaceConstantZC() + + z_1d = ux.UxDataArray(np.arange(4.0), dims=("zf",), uxgrid=face_grid.uxgrid) + grid_with_1d_z = UxGrid(face_grid.uxgrid, z=z_1d, mesh=FlatMesh()) + assert grid_with_1d_z.z_interp_method is None + with pytest.raises(ValueError, match="can only be set on a grid with a time-varying"): + grid_with_1d_z.z_interp_method = UxVerticalCoordinateLinearNode() + + with pytest.raises(ValueError, match="A 3D z must have dims"): + _time_varying_uxgrid("n_edge") From 2da08b7569443e5a26028637d799bf559635b017 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 18:32:18 +0000 Subject: [PATCH 10/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_uxarray_fieldset.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_uxarray_fieldset.py b/tests/test_uxarray_fieldset.py index 27f907692f..3482d419d7 100644 --- a/tests/test_uxarray_fieldset.py +++ b/tests/test_uxarray_fieldset.py @@ -212,7 +212,12 @@ def test_ux_interpolators_are_linear_in_time(): rng = np.random.default_rng(0) value_at_snapshots = rng.random(ds.sizes["time"]) registrations = [ - ("zc", "n_face"), ("zf", "n_face"), ("zc", "n_node"), ("zf", "n_node"), ("zc", "n_edge"), ("zf", "n_edge") + ("zc", "n_face"), + ("zf", "n_face"), + ("zc", "n_node"), + ("zf", "n_node"), + ("zc", "n_edge"), + ("zf", "n_edge"), ] for vertical_dim, lateral_dim in registrations: shape = (ds.sizes["time"], ds.sizes[vertical_dim], getattr(ds.uxgrid, lateral_dim)) From 6f5355e3bda1f64c055144eccd1fd0054b13a215 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 19:43:28 +0000 Subject: [PATCH 11/16] Remove experimental warning on startup --- src/parcels/_core/uxgrid.py | 15 +-------------- src/parcels/_core/xgrid.py | 15 +-------------- tests/test_advection.py | 1 - tests/test_fieldset.py | 1 - tests/test_uxadvection.py | 1 - tests/test_uxarray_fieldset.py | 2 -- tests/test_vertical_coordinate_interpolators.py | 4 ---- 7 files changed, 2 insertions(+), 37 deletions(-) diff --git a/src/parcels/_core/uxgrid.py b/src/parcels/_core/uxgrid.py index 560e41b1e3..39ef536a19 100644 --- a/src/parcels/_core/uxgrid.py +++ b/src/parcels/_core/uxgrid.py @@ -1,6 +1,5 @@ from __future__ import annotations -import warnings from typing import TYPE_CHECKING, Literal import numpy as np @@ -9,7 +8,6 @@ from parcels._core.basegrid import BaseGrid from parcels._core.index_search import GRID_SEARCH_ERROR, _search_1d_array, _search_1d_columns, uxgrid_point_in_cell from parcels._core.mesh import BaseMesh, TMesh -from parcels._core.warnings import FieldSetWarning from parcels.interpolators._base import VerticalCoordinateInterpolator from parcels.interpolators._uxinterpolators import UxVerticalCoordinateConstantFace, UxVerticalCoordinateLinearNode @@ -62,7 +60,7 @@ def __init__(self, grid: ux.grid.Grid, z: ux.UxDataArray, mesh: TMesh) -> None: self.uxgrid.face_node_connectivity.load() def _init_time_varying_z(self) -> None: - """Checks the dims of a time-varying (3D) z, assigns its default z_interp_method and warns about its use.""" + """Checks the dims of a time-varying (3D) z and assigns its default z_interp_method.""" z = self.z if z.dims not in (("time", "zf", "n_node"), ("time", "zf", "n_face")): @@ -75,17 +73,6 @@ def _init_time_varying_z(self) -> None: else: self.z_interp_method = UxVerticalCoordinateConstantFace() - warnings.warn( - "Time-varying (3D) z coordinates are experimental and may cause significant memory overhead that " - f"leads to OOM errors. This z coordinate has sizes {dict(z.sizes)} ({z.nbytes / 1e9:.3g} GB). " - f"Assumptions: z has dims {z.dims}, i.e. it is defined at the layer interfaces ('zf') on the mesh " - "nodes or faces, and is strictly increasing along 'zf'; each particle's z column is interpolated with " - "the grid's `z_interp_method` (by default barycentrically from its face's nodes for node z, or " - "constant over its face for face z), and linearly in time between z snapshots.", - FieldSetWarning, - stacklevel=5, - ) - @property def depth(self): """ diff --git a/src/parcels/_core/xgrid.py b/src/parcels/_core/xgrid.py index 139ef9fa97..5339616a0c 100644 --- a/src/parcels/_core/xgrid.py +++ b/src/parcels/_core/xgrid.py @@ -1,4 +1,3 @@ -import warnings from collections.abc import Hashable, Sequence from functools import cached_property from typing import Literal, cast @@ -13,7 +12,6 @@ from parcels._core.basegrid import BaseGrid from parcels._core.index_search import _search_1d_array, _search_1d_columns, _search_indices_curvilinear_2d from parcels._core.mesh import BaseMesh -from parcels._core.warnings import FieldSetWarning from parcels._sgrid.accessor import _get_dim_to_axis_mapping, get_dim_position from parcels.interpolators._base import VerticalCoordinateInterpolator from parcels.interpolators._xinterpolators import XVerticalCoordinateConstant, XVerticalCoordinateLinear @@ -155,7 +153,7 @@ def __init__(self, model_data: xr.Dataset, mesh: ptyping.TMesh): self._ds = ds def _init_time_varying_depth(self) -> None: - """Checks the dims of a time-varying (4D) depth, assigns its default z_interp_method and warns about its use.""" + """Checks the dims of a time-varying (4D) depth and assigns its default z_interp_method.""" depth = self.depth assert self.sgrid_metadata.vertical_dimensions is not None @@ -173,17 +171,6 @@ def _init_time_varying_depth(self) -> None: self.z_interp_method = XVerticalCoordinateLinear() - warnings.warn( - "Time-varying (4D) z coordinates are experimental and may cause significant memory overhead that " - f"leads to OOM errors. This z coordinate has sizes {dict(depth.sizes)} ({depth.nbytes / 1e9:.3g} GB). " - f"Assumptions: z has dims {depth.dims}, i.e. it is " - f"defined at the layer interfaces on the cell centres or corners, and is strictly increasing along " - f"{vertical_dim!r}; each particle's z column is interpolated with the grid's `z_interp_method` " - "(by default bilinearly from the surrounding columns), and linearly in time between z snapshots.", - FieldSetWarning, - stacklevel=6, - ) - # def __repr__(self): # return xgrid_repr(self) diff --git a/tests/test_advection.py b/tests/test_advection.py index e12b05b4b5..84c843a090 100644 --- a/tests/test_advection.py +++ b/tests/test_advection.py @@ -577,7 +577,6 @@ def test_mitgcm(): np.testing.assert_allclose(pset.y, lat_v3, atol=1) -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") def test_advection_on_moving_sigma_grid_keeps_depth(): """With uniform u and w = 0, particles keep their depth while the sigma levels move past them.""" n_snapshots, nz, ny, nx = 3, 6, 5, 5 diff --git a/tests/test_fieldset.py b/tests/test_fieldset.py index 7fd63f5c9e..c1b042a43f 100644 --- a/tests/test_fieldset.py +++ b/tests/test_fieldset.py @@ -704,7 +704,6 @@ def test_fieldset_describe_backends(tmp_path): # TODO: Add test for the ChunkedArray backend (can also refactor this test at the same time) -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") @pytest.mark.parametrize( "n_snapshots", [ diff --git a/tests/test_uxadvection.py b/tests/test_uxadvection.py index 9d7babd18d..e0eeccb2dd 100644 --- a/tests/test_uxadvection.py +++ b/tests/test_uxadvection.py @@ -30,7 +30,6 @@ def test_ux_constant_flow_face_centered_2D(integrator, tmp_parquet): np.testing.assert_allclose(df["x"].iloc[-1], expected_lon, atol=1e-5) -@pytest.mark.filterwarnings("ignore:Time-varying \\(3D\\) z coordinates are experimental") def test_ux_advection_on_moving_sigma_grid_keeps_depth(): """With uniform u and w = 0, particles keep their depth while the sigma levels move past them.""" nx, nz = 5, 6 diff --git a/tests/test_uxarray_fieldset.py b/tests/test_uxarray_fieldset.py index 27f907692f..0f0e0dc1e8 100644 --- a/tests/test_uxarray_fieldset.py +++ b/tests/test_uxarray_fieldset.py @@ -294,7 +294,6 @@ def test_nestedgrids_triangulation_spherical_search(): RISING_ETA = np.broadcast_to(np.linspace(-1.0, 1.0, 3)[:, np.newaxis, np.newaxis], (3, 5, 5)) -@pytest.mark.filterwarnings("ignore:Time-varying \\(3D\\) z coordinates are experimental") @pytest.mark.parametrize( "eta, expected_n_snapshots", [ @@ -314,7 +313,6 @@ def test_fieldset_from_sigma_coordinate_dataset(eta, expected_n_snapshots): assert isinstance(field.interp_method, UxLinearNodeLinearZF) -@pytest.mark.filterwarnings("ignore:Time-varying \\(3D\\) z coordinates are experimental") def test_unstructured_model_data_rejects_z_with_different_time_coordinate(): ds = sigma_coordinate_lattice_dataset(5, (0.0, 4e3), (0.0, 4e3), 4, SLOPING_BOTTOM_DEPTH, RISING_ETA) ds_one_day_later = sigma_coordinate_lattice_dataset( diff --git a/tests/test_vertical_coordinate_interpolators.py b/tests/test_vertical_coordinate_interpolators.py index 3b98b687be..01b67bdbac 100644 --- a/tests/test_vertical_coordinate_interpolators.py +++ b/tests/test_vertical_coordinate_interpolators.py @@ -67,7 +67,6 @@ def _position_in_node_indices(i, b, o): return i + b -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") @pytest.mark.parametrize( ("z_interp_method", "registration", "expected_y", "expected_x"), [ @@ -106,7 +105,6 @@ def test_vertical_coordinate_columns(z_interp_method, registration, expected_y, np.testing.assert_allclose(columns, expected) -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") @pytest.mark.parametrize("registration", ["face", "node"]) def test_vertical_coordinate_linear_is_continuous(registration): """z is continuous across cell edges, and across the cell centre (where face registered z changes the bracketing @@ -144,7 +142,6 @@ def test_position_between_cell_centres(face_index, bcoord, expected_index, expec np.testing.assert_allclose(centre_bcoord[0], expected_bcoord) -@pytest.mark.filterwarnings("ignore:Time-varying \\(4D\\) z coordinates are experimental") def test_z_interp_method_defaults_and_validation(): face_grid, node_grid = _sigma_grid("face"), _sigma_grid("node") assert isinstance(face_grid.z_interp_method, XVerticalCoordinateLinear) @@ -197,7 +194,6 @@ def _time_varying_uxgrid(lateral_dim): return UxGrid(uxgrid, z=ux.UxDataArray(z, dims=("time", "zf", lateral_dim), uxgrid=uxgrid), mesh=FlatMesh()) -@pytest.mark.filterwarnings("ignore:Time-varying \\(3D\\) z coordinates are experimental") def test_ux_z_interp_method_defaults_and_validation(): face_grid, node_grid = _time_varying_uxgrid("n_face"), _time_varying_uxgrid("n_node") assert isinstance(face_grid.z_interp_method, UxVerticalCoordinateConstantFace) From 24ecc0465f70739f3806b59069a25775c9208fd7 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 20:03:33 +0000 Subject: [PATCH 12/16] Renamed tutorial and added vertical coord interpolator section --- .../user_guide/examples/tutorial_schism.ipynb | 4 +- .../tutorial_time_varying_z_coordinates.ipynb | 208717 +++++++++++++++ docs/user_guide/index.md | 2 +- 3 files changed, 208720 insertions(+), 3 deletions(-) create mode 100644 docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb diff --git a/docs/user_guide/examples/tutorial_schism.ipynb b/docs/user_guide/examples/tutorial_schism.ipynb index a81b4cb973..e549178b63 100644 --- a/docs/user_guide/examples/tutorial_schism.ipynb +++ b/docs/user_guide/examples/tutorial_schism.ipynb @@ -445,7 +445,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Parcels:default (3.14.8)", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -459,7 +459,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.14.8" + "version": "3.14.6" }, "mystnb": { "execution_in_temp": true diff --git a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb new file mode 100644 index 0000000000..40cc0e0075 --- /dev/null +++ b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb @@ -0,0 +1,208717 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 🖥️ Datasets with Time Varying Z Coordinates" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "(function(root) {\n", + " function now() {\n", + " return new Date();\n", + " }\n", + "\n", + " const force = true;\n", + " const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", + " const reloading = false;\n", + " const Bokeh = root.Bokeh;\n", + " const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n", + " const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n", + "\n", + " // Set a timeout for this load but only if we are not already initializing\n", + " if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_failed_load = false;\n", + " }\n", + "\n", + " function run_callbacks() {\n", + " try {\n", + " root._bokeh_onload_callbacks.forEach(function(callback) {\n", + " if (callback != null)\n", + " callback();\n", + " });\n", + " } finally {\n", + " delete root._bokeh_onload_callbacks;\n", + " }\n", + " console.debug(\"Bokeh: all callbacks have finished\");\n", + " }\n", + "\n", + " function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n", + " if (css_urls == null) css_urls = [];\n", + " if (js_urls == null) js_urls = [];\n", + " if (js_modules == null) js_modules = [];\n", + " if (js_exports == null) js_exports = {};\n", + "\n", + " root._bokeh_onload_callbacks.push(callback);\n", + "\n", + " if (root._bokeh_is_loading > 0) {\n", + " // Don't load bokeh if it is still initializing\n", + " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", + " return null;\n", + " } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", + " // There is nothing to load\n", + " run_callbacks();\n", + " return null;\n", + " }\n", + "\n", + " function on_load() {\n", + " root._bokeh_is_loading--;\n", + " if (root._bokeh_is_loading === 0) {\n", + " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", + " run_callbacks()\n", + " }\n", + " }\n", + " window._bokeh_on_load = on_load\n", + "\n", + " function on_error(e) {\n", + " const src_el = e.srcElement\n", + " console.error(\"failed to load \" + (src_el.href || src_el.src));\n", + " }\n", + "\n", + " const skip = [];\n", + " if (window.requirejs) {\n", + " window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n", + " root._bokeh_is_loading = css_urls.length + 0;\n", + " } else {\n", + " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", + " }\n", + "\n", + " const existing_stylesheets = []\n", + " const links = document.getElementsByTagName('link')\n", + " for (let i = 0; i < links.length; i++) {\n", + " const link = links[i]\n", + " if (link.href != null) {\n", + " existing_stylesheets.push(link.href)\n", + " }\n", + " }\n", + " for (let i = 0; i < css_urls.length; i++) {\n", + " const url = css_urls[i];\n", + " const escaped = encodeURI(url)\n", + " if (existing_stylesheets.indexOf(escaped) !== -1) {\n", + " on_load()\n", + " continue;\n", + " }\n", + " const element = document.createElement(\"link\");\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.rel = \"stylesheet\";\n", + " element.type = \"text/css\";\n", + " element.href = url;\n", + " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", + " document.body.appendChild(element);\n", + " } var existing_scripts = []\n", + " const scripts = document.getElementsByTagName('script')\n", + " for (let i = 0; i < scripts.length; i++) {\n", + " var script = scripts[i]\n", + " if (script.src != null) {\n", + " existing_scripts.push(script.src)\n", + " }\n", + " }\n", + " for (let i = 0; i < js_urls.length; i++) {\n", + " const url = js_urls[i];\n", + " const escaped = encodeURI(url)\n", + " const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n", + " const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n", + " const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n", + " if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " const element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (let i = 0; i < js_modules.length; i++) {\n", + " const url = js_modules[i];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (const name in js_exports) {\n", + " const url = js_exports[name];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " element.textContent = `\n", + " import ${name} from \"${url}\"\n", + " window.${name} = ${name}\n", + " window._bokeh_on_load()\n", + " `\n", + " document.head.appendChild(element);\n", + " }\n", + " if (!js_urls.length && !js_modules.length) {\n", + " on_load()\n", + " }\n", + " };\n", + "\n", + " function inject_raw_css(css) {\n", + " const element = document.createElement(\"style\");\n", + " element.appendChild(document.createTextNode(css));\n", + " document.body.appendChild(element);\n", + " }\n", + "\n", + " const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/dist/panel.min.js\"];\n", + " const js_modules = [];\n", + " const js_exports = {};\n", + " const css_urls = [];\n", + " const inline_js = [ function(Bokeh) {\n", + " Bokeh.set_log_level(\"info\");\n", + " },\n", + "function(Bokeh) {} // ensure no trailing comma for IE\n", + " ];\n", + "\n", + " function run_inline_js() {\n", + " if ((root.Bokeh !== undefined) || (force === true)) {\n", + " for (let i = 0; i < inline_js.length; i++) {\n", + " try {\n", + " inline_js[i].call(root, root.Bokeh);\n", + " } catch(e) {\n", + " if (!reloading) {\n", + " throw e;\n", + " }\n", + " }\n", + " }\n", + " } else if (Date.now() < root._bokeh_timeout) {\n", + " setTimeout(run_inline_js, 100);\n", + " } else if (!root._bokeh_failed_load) {\n", + " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", + " root._bokeh_failed_load = true;\n", + " }\n", + " root._bokeh_is_initializing = false;\n", + " }\n", + "\n", + " function load_or_wait() {\n", + " // Implement a backoff loop that tries to ensure we do not load multiple\n", + " // versions of Bokeh and its dependencies at the same time.\n", + " // In recent versions we use the root._bokeh_is_initializing flag\n", + " // to determine whether there is an ongoing attempt to initialize\n", + " // bokeh, however for backward compatibility we also try to ensure\n", + " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", + " // before older versions are fully initialized.\n", + " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", + " // If the timeout and bokeh was not successfully loaded we reset\n", + " // everything and try loading again\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_is_initializing = false;\n", + " root._bokeh_onload_callbacks = undefined;\n", + " root._bokeh_is_loading = 0;\n", + " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", + " load_or_wait();\n", + " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", + " setTimeout(load_or_wait, 100);\n", + " } else {\n", + " root._bokeh_is_initializing = true;\n", + " root._bokeh_onload_callbacks = [];\n", + " const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n", + " if (!reloading && !bokeh_loaded) {\n", + " if (root.Bokeh) {\n", + " root.Bokeh = undefined;\n", + " }\n", + " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", + " }\n", + " load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n", + " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", + " run_inline_js();\n", + " if (Bokeh != undefined && !reloading) {\n", + " const NewBokeh = root.Bokeh;\n", + " if (Bokeh.versions === undefined) {\n", + " Bokeh.versions = new Map();\n", + " }\n", + " if (NewBokeh.version !== Bokeh.version) {\n", + " Bokeh[NewBokeh.version] = NewBokeh;\n", + " Bokeh.versions.set(NewBokeh.version, NewBokeh);\n", + " }\n", + " root.Bokeh = Bokeh;\n", + " }\n", + " });\n", + " }\n", + " }\n", + " // Give older versions of the autoload script a head-start to ensure\n", + " // they initialize before we start loading newer version.\n", + " setTimeout(load_or_wait, 100)\n", + "}(window));" + ], + "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", + " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", + "}\n", + "\n", + "\n", + " function JupyterCommManager() {\n", + " }\n", + "\n", + " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", + " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " comm_manager.register_target(comm_id, function(comm) {\n", + " comm.on_msg(msg_handler);\n", + " });\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", + " comm.onMsg = msg_handler;\n", + " });\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " var content = {data: message.data, comm_id};\n", + " var buffers = []\n", + " for (var buffer of message.buffers || []) {\n", + " buffers.push(new DataView(buffer))\n", + " }\n", + " var metadata = message.metadata || {};\n", + " var msg = {content, buffers, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " })\n", + " }\n", + " }\n", + "\n", + " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", + " if (comm_id in window.PyViz.comms) {\n", + " return window.PyViz.comms[comm_id];\n", + " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", + " if (msg_handler) {\n", + " comm.on_msg(msg_handler);\n", + " }\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", + " let retries = 0;\n", + " const open = () => {\n", + " if (comm.active) {\n", + " comm.open();\n", + " } else if (retries > 3) {\n", + " console.warn('Comm target never activated')\n", + " } else {\n", + " retries += 1\n", + " setTimeout(open, 500)\n", + " }\n", + " }\n", + " if (comm.active) {\n", + " comm.open();\n", + " } else {\n", + " setTimeout(open, 500)\n", + " }\n", + " if (msg_handler) {\n", + " comm.onMsg = msg_handler;\n", + " }\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", + " comm_promise.then((comm) => {\n", + " window.PyViz.comms[comm_id] = comm;\n", + " if (msg_handler) {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " var content = {data: message.data};\n", + " var metadata = message.metadata || {comm_id};\n", + " var msg = {content, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " })\n", + " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", + " return comm_promise.then((comm) => {\n", + " comm.send(data, metadata, buffers, disposeOnDone);\n", + " });\n", + " };\n", + " var comm = {\n", + " send: sendClosure\n", + " };\n", + " }\n", + " window.PyViz.comms[comm_id] = comm;\n", + " return comm;\n", + " }\n", + " window.PyViz.comm_manager = new JupyterCommManager();\n", + " \n", + "\n", + "\n", + "var JS_MIME_TYPE = 'application/javascript';\n", + "var HTML_MIME_TYPE = 'text/html';\n", + "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", + "var CLASS_NAME = 'output';\n", + "\n", + "/**\n", + " * Render data to the DOM node\n", + " */\n", + "function render(props, node) {\n", + " var div = document.createElement(\"div\");\n", + " var script = document.createElement(\"script\");\n", + " node.appendChild(div);\n", + " node.appendChild(script);\n", + "}\n", + "\n", + "/**\n", + " * Handle when a new output is added\n", + " */\n", + "function handle_add_output(event, handle) {\n", + " var output_area = handle.output_area;\n", + " var output = handle.output;\n", + " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", + " return\n", + " }\n", + " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", + " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", + " if (id !== undefined) {\n", + " var nchildren = toinsert.length;\n", + " var html_node = toinsert[nchildren-1].children[0];\n", + " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var scripts = [];\n", + " var nodelist = html_node.querySelectorAll(\"script\");\n", + " for (var i in nodelist) {\n", + " if (nodelist.hasOwnProperty(i)) {\n", + " scripts.push(nodelist[i])\n", + " }\n", + " }\n", + "\n", + " scripts.forEach( function (oldScript) {\n", + " var newScript = document.createElement(\"script\");\n", + " var attrs = [];\n", + " var nodemap = oldScript.attributes;\n", + " for (var j in nodemap) {\n", + " if (nodemap.hasOwnProperty(j)) {\n", + " attrs.push(nodemap[j])\n", + " }\n", + " }\n", + " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", + " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", + " oldScript.parentNode.replaceChild(newScript, oldScript);\n", + " });\n", + " if (JS_MIME_TYPE in output.data) {\n", + " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", + " }\n", + " output_area._hv_plot_id = id;\n", + " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", + " window.PyViz.plot_index[id] = Bokeh.index[id];\n", + " } else {\n", + " window.PyViz.plot_index[id] = null;\n", + " }\n", + " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", + " var bk_div = document.createElement(\"div\");\n", + " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var script_attrs = bk_div.children[0].attributes;\n", + " for (var i = 0; i < script_attrs.length; i++) {\n", + " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", + " }\n", + " // store reference to server id on output_area\n", + " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle when an output is cleared or removed\n", + " */\n", + "function handle_clear_output(event, handle) {\n", + " var id = handle.cell.output_area._hv_plot_id;\n", + " var server_id = handle.cell.output_area._bokeh_server_id;\n", + " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", + " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", + " if (server_id !== null) {\n", + " comm.send({event_type: 'server_delete', 'id': server_id});\n", + " return;\n", + " } else if (comm !== null) {\n", + " comm.send({event_type: 'delete', 'id': id});\n", + " }\n", + " delete PyViz.plot_index[id];\n", + " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", + " var doc = window.Bokeh.index[id].model.document\n", + " doc.clear();\n", + " const i = window.Bokeh.documents.indexOf(doc);\n", + " if (i > -1) {\n", + " window.Bokeh.documents.splice(i, 1);\n", + " }\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle kernel restart event\n", + " */\n", + "function handle_kernel_cleanup(event, handle) {\n", + " delete PyViz.comms[\"hv-extension-comm\"];\n", + " window.PyViz.plot_index = {}\n", + "}\n", + "\n", + "/**\n", + " * Handle update_display_data messages\n", + " */\n", + "function handle_update_output(event, handle) {\n", + " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", + " handle_add_output(event, handle)\n", + "}\n", + "\n", + "function register_renderer(events, OutputArea) {\n", + " function append_mime(data, metadata, element) {\n", + " // create a DOM node to render to\n", + " var toinsert = this.create_output_subarea(\n", + " metadata,\n", + " CLASS_NAME,\n", + " EXEC_MIME_TYPE\n", + " );\n", + " this.keyboard_manager.register_events(toinsert);\n", + " // Render to node\n", + " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", + " render(props, toinsert[0]);\n", + " element.append(toinsert);\n", + " return toinsert\n", + " }\n", + "\n", + " events.on('output_added.OutputArea', handle_add_output);\n", + " events.on('output_updated.OutputArea', handle_update_output);\n", + " events.on('clear_output.CodeCell', handle_clear_output);\n", + " events.on('delete.Cell', handle_clear_output);\n", + " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", + "\n", + " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", + " safe: true,\n", + " index: 0\n", + " });\n", + "}\n", + "\n", + "if (window.Jupyter !== undefined) {\n", + " try {\n", + " var events = require('base/js/events');\n", + " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", + " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", + " register_renderer(events, OutputArea);\n", + " }\n", + " } catch(err) {\n", + " }\n", + "}\n" + ], + "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.holoviews_exec.v0+json": "", + "text/html": [ + "
\n", + "
\n", + "
\n", + "" + ] + }, + "metadata": { + "application/vnd.holoviews_exec.v0+json": { + "id": "77e5196f-acf9-4a4b-a41b-5636b9953aca" + } + }, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "(function(root) {\n", + " function now() {\n", + " return new Date();\n", + " }\n", + "\n", + " const force = false;\n", + " const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", + " const reloading = true;\n", + " const Bokeh = root.Bokeh;\n", + " const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n", + " const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n", + "\n", + " // Set a timeout for this load but only if we are not already initializing\n", + " if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_failed_load = false;\n", + " }\n", + "\n", + " function run_callbacks() {\n", + " try {\n", + " root._bokeh_onload_callbacks.forEach(function(callback) {\n", + " if (callback != null)\n", + " callback();\n", + " });\n", + " } finally {\n", + " delete root._bokeh_onload_callbacks;\n", + " }\n", + " console.debug(\"Bokeh: all callbacks have finished\");\n", + " }\n", + "\n", + " function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n", + " if (css_urls == null) css_urls = [];\n", + " if (js_urls == null) js_urls = [];\n", + " if (js_modules == null) js_modules = [];\n", + " if (js_exports == null) js_exports = {};\n", + "\n", + " root._bokeh_onload_callbacks.push(callback);\n", + "\n", + " if (root._bokeh_is_loading > 0) {\n", + " // Don't load bokeh if it is still initializing\n", + " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", + " return null;\n", + " } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", + " // There is nothing to load\n", + " run_callbacks();\n", + " return null;\n", + " }\n", + "\n", + " function on_load() {\n", + " root._bokeh_is_loading--;\n", + " if (root._bokeh_is_loading === 0) {\n", + " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", + " run_callbacks()\n", + " }\n", + " }\n", + " window._bokeh_on_load = on_load\n", + "\n", + " function on_error(e) {\n", + " const src_el = e.srcElement\n", + " console.error(\"failed to load \" + (src_el.href || src_el.src));\n", + " }\n", + "\n", + " const skip = [];\n", + " if (window.requirejs) {\n", + " window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n", + " root._bokeh_is_loading = css_urls.length + 0;\n", + " } else {\n", + " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", + " }\n", + "\n", + " const existing_stylesheets = []\n", + " const links = document.getElementsByTagName('link')\n", + " for (let i = 0; i < links.length; i++) {\n", + " const link = links[i]\n", + " if (link.href != null) {\n", + " existing_stylesheets.push(link.href)\n", + " }\n", + " }\n", + " for (let i = 0; i < css_urls.length; i++) {\n", + " const url = css_urls[i];\n", + " const escaped = encodeURI(url)\n", + " if (existing_stylesheets.indexOf(escaped) !== -1) {\n", + " on_load()\n", + " continue;\n", + " }\n", + " const element = document.createElement(\"link\");\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.rel = \"stylesheet\";\n", + " element.type = \"text/css\";\n", + " element.href = url;\n", + " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", + " document.body.appendChild(element);\n", + " } var existing_scripts = []\n", + " const scripts = document.getElementsByTagName('script')\n", + " for (let i = 0; i < scripts.length; i++) {\n", + " var script = scripts[i]\n", + " if (script.src != null) {\n", + " existing_scripts.push(script.src)\n", + " }\n", + " }\n", + " for (let i = 0; i < js_urls.length; i++) {\n", + " const url = js_urls[i];\n", + " const escaped = encodeURI(url)\n", + " const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n", + " const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n", + " const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n", + " if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " const element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (let i = 0; i < js_modules.length; i++) {\n", + " const url = js_modules[i];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onload = on_load;\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.src = url;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " document.head.appendChild(element);\n", + " }\n", + " for (const name in js_exports) {\n", + " const url = js_exports[name];\n", + " const escaped = encodeURI(url)\n", + " if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n", + " if (!window.requirejs) {\n", + " on_load();\n", + " }\n", + " continue;\n", + " }\n", + " var element = document.createElement('script');\n", + " element.onerror = on_error;\n", + " element.async = false;\n", + " element.type = \"module\";\n", + " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", + " element.textContent = `\n", + " import ${name} from \"${url}\"\n", + " window.${name} = ${name}\n", + " window._bokeh_on_load()\n", + " `\n", + " document.head.appendChild(element);\n", + " }\n", + " if (!js_urls.length && !js_modules.length) {\n", + " on_load()\n", + " }\n", + " };\n", + "\n", + " function inject_raw_css(css) {\n", + " const element = document.createElement(\"style\");\n", + " element.appendChild(document.createTextNode(css));\n", + " document.body.appendChild(element);\n", + " }\n", + "\n", + " const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n", + " const js_modules = [];\n", + " const js_exports = {};\n", + " const css_urls = [];\n", + " const inline_js = [ function(Bokeh) {\n", + " Bokeh.set_log_level(\"info\");\n", + " },\n", + "function(Bokeh) {} // ensure no trailing comma for IE\n", + " ];\n", + "\n", + " function run_inline_js() {\n", + " if ((root.Bokeh !== undefined) || (force === true)) {\n", + " for (let i = 0; i < inline_js.length; i++) {\n", + " try {\n", + " inline_js[i].call(root, root.Bokeh);\n", + " } catch(e) {\n", + " if (!reloading) {\n", + " throw e;\n", + " }\n", + " }\n", + " }\n", + " } else if (Date.now() < root._bokeh_timeout) {\n", + " setTimeout(run_inline_js, 100);\n", + " } else if (!root._bokeh_failed_load) {\n", + " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", + " root._bokeh_failed_load = true;\n", + " }\n", + " root._bokeh_is_initializing = false;\n", + " }\n", + "\n", + " function load_or_wait() {\n", + " // Implement a backoff loop that tries to ensure we do not load multiple\n", + " // versions of Bokeh and its dependencies at the same time.\n", + " // In recent versions we use the root._bokeh_is_initializing flag\n", + " // to determine whether there is an ongoing attempt to initialize\n", + " // bokeh, however for backward compatibility we also try to ensure\n", + " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", + " // before older versions are fully initialized.\n", + " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", + " // If the timeout and bokeh was not successfully loaded we reset\n", + " // everything and try loading again\n", + " root._bokeh_timeout = Date.now() + 5000;\n", + " root._bokeh_is_initializing = false;\n", + " root._bokeh_onload_callbacks = undefined;\n", + " root._bokeh_is_loading = 0;\n", + " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", + " load_or_wait();\n", + " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", + " setTimeout(load_or_wait, 100);\n", + " } else {\n", + " root._bokeh_is_initializing = true;\n", + " root._bokeh_onload_callbacks = [];\n", + " const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n", + " if (!reloading && !bokeh_loaded) {\n", + " if (root.Bokeh) {\n", + " root.Bokeh = undefined;\n", + " }\n", + " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", + " }\n", + " load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n", + " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", + " run_inline_js();\n", + " if (Bokeh != undefined && !reloading) {\n", + " const NewBokeh = root.Bokeh;\n", + " if (Bokeh.versions === undefined) {\n", + " Bokeh.versions = new Map();\n", + " }\n", + " if (NewBokeh.version !== Bokeh.version) {\n", + " Bokeh[NewBokeh.version] = NewBokeh;\n", + " Bokeh.versions.set(NewBokeh.version, NewBokeh);\n", + " }\n", + " root.Bokeh = Bokeh;\n", + " }\n", + " });\n", + " }\n", + " }\n", + " // Give older versions of the autoload script a head-start to ensure\n", + " // they initialize before we start loading newer version.\n", + " setTimeout(load_or_wait, 100)\n", + "}(window));" + ], + "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", + " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", + "}\n", + "\n", + "\n", + " function JupyterCommManager() {\n", + " }\n", + "\n", + " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", + " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " comm_manager.register_target(comm_id, function(comm) {\n", + " comm.on_msg(msg_handler);\n", + " });\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", + " comm.onMsg = msg_handler;\n", + " });\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " var content = {data: message.data, comm_id};\n", + " var buffers = []\n", + " for (var buffer of message.buffers || []) {\n", + " buffers.push(new DataView(buffer))\n", + " }\n", + " var metadata = message.metadata || {};\n", + " var msg = {content, buffers, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " })\n", + " }\n", + " }\n", + "\n", + " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", + " if (comm_id in window.PyViz.comms) {\n", + " return window.PyViz.comms[comm_id];\n", + " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", + " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", + " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", + " if (msg_handler) {\n", + " comm.on_msg(msg_handler);\n", + " }\n", + " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", + " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", + " let retries = 0;\n", + " const open = () => {\n", + " if (comm.active) {\n", + " comm.open();\n", + " } else if (retries > 3) {\n", + " console.warn('Comm target never activated')\n", + " } else {\n", + " retries += 1\n", + " setTimeout(open, 500)\n", + " }\n", + " }\n", + " if (comm.active) {\n", + " comm.open();\n", + " } else {\n", + " setTimeout(open, 500)\n", + " }\n", + " if (msg_handler) {\n", + " comm.onMsg = msg_handler;\n", + " }\n", + " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", + " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", + " comm_promise.then((comm) => {\n", + " window.PyViz.comms[comm_id] = comm;\n", + " if (msg_handler) {\n", + " var messages = comm.messages[Symbol.asyncIterator]();\n", + " function processIteratorResult(result) {\n", + " var message = result.value;\n", + " var content = {data: message.data};\n", + " var metadata = message.metadata || {comm_id};\n", + " var msg = {content, metadata}\n", + " msg_handler(msg);\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " return messages.next().then(processIteratorResult);\n", + " }\n", + " })\n", + " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", + " return comm_promise.then((comm) => {\n", + " comm.send(data, metadata, buffers, disposeOnDone);\n", + " });\n", + " };\n", + " var comm = {\n", + " send: sendClosure\n", + " };\n", + " }\n", + " window.PyViz.comms[comm_id] = comm;\n", + " return comm;\n", + " }\n", + " window.PyViz.comm_manager = new JupyterCommManager();\n", + " \n", + "\n", + "\n", + "var JS_MIME_TYPE = 'application/javascript';\n", + "var HTML_MIME_TYPE = 'text/html';\n", + "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", + "var CLASS_NAME = 'output';\n", + "\n", + "/**\n", + " * Render data to the DOM node\n", + " */\n", + "function render(props, node) {\n", + " var div = document.createElement(\"div\");\n", + " var script = document.createElement(\"script\");\n", + " node.appendChild(div);\n", + " node.appendChild(script);\n", + "}\n", + "\n", + "/**\n", + " * Handle when a new output is added\n", + " */\n", + "function handle_add_output(event, handle) {\n", + " var output_area = handle.output_area;\n", + " var output = handle.output;\n", + " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", + " return\n", + " }\n", + " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", + " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", + " if (id !== undefined) {\n", + " var nchildren = toinsert.length;\n", + " var html_node = toinsert[nchildren-1].children[0];\n", + " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var scripts = [];\n", + " var nodelist = html_node.querySelectorAll(\"script\");\n", + " for (var i in nodelist) {\n", + " if (nodelist.hasOwnProperty(i)) {\n", + " scripts.push(nodelist[i])\n", + " }\n", + " }\n", + "\n", + " scripts.forEach( function (oldScript) {\n", + " var newScript = document.createElement(\"script\");\n", + " var attrs = [];\n", + " var nodemap = oldScript.attributes;\n", + " for (var j in nodemap) {\n", + " if (nodemap.hasOwnProperty(j)) {\n", + " attrs.push(nodemap[j])\n", + " }\n", + " }\n", + " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", + " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", + " oldScript.parentNode.replaceChild(newScript, oldScript);\n", + " });\n", + " if (JS_MIME_TYPE in output.data) {\n", + " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", + " }\n", + " output_area._hv_plot_id = id;\n", + " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", + " window.PyViz.plot_index[id] = Bokeh.index[id];\n", + " } else {\n", + " window.PyViz.plot_index[id] = null;\n", + " }\n", + " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", + " var bk_div = document.createElement(\"div\");\n", + " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", + " var script_attrs = bk_div.children[0].attributes;\n", + " for (var i = 0; i < script_attrs.length; i++) {\n", + " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", + " }\n", + " // store reference to server id on output_area\n", + " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle when an output is cleared or removed\n", + " */\n", + "function handle_clear_output(event, handle) {\n", + " var id = handle.cell.output_area._hv_plot_id;\n", + " var server_id = handle.cell.output_area._bokeh_server_id;\n", + " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", + " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", + " if (server_id !== null) {\n", + " comm.send({event_type: 'server_delete', 'id': server_id});\n", + " return;\n", + " } else if (comm !== null) {\n", + " comm.send({event_type: 'delete', 'id': id});\n", + " }\n", + " delete PyViz.plot_index[id];\n", + " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", + " var doc = window.Bokeh.index[id].model.document\n", + " doc.clear();\n", + " const i = window.Bokeh.documents.indexOf(doc);\n", + " if (i > -1) {\n", + " window.Bokeh.documents.splice(i, 1);\n", + " }\n", + " }\n", + "}\n", + "\n", + "/**\n", + " * Handle kernel restart event\n", + " */\n", + "function handle_kernel_cleanup(event, handle) {\n", + " delete PyViz.comms[\"hv-extension-comm\"];\n", + " window.PyViz.plot_index = {}\n", + "}\n", + "\n", + "/**\n", + " * Handle update_display_data messages\n", + " */\n", + "function handle_update_output(event, handle) {\n", + " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", + " handle_add_output(event, handle)\n", + "}\n", + "\n", + "function register_renderer(events, OutputArea) {\n", + " function append_mime(data, metadata, element) {\n", + " // create a DOM node to render to\n", + " var toinsert = this.create_output_subarea(\n", + " metadata,\n", + " CLASS_NAME,\n", + " EXEC_MIME_TYPE\n", + " );\n", + " this.keyboard_manager.register_events(toinsert);\n", + " // Render to node\n", + " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", + " render(props, toinsert[0]);\n", + " element.append(toinsert);\n", + " return toinsert\n", + " }\n", + "\n", + " events.on('output_added.OutputArea', handle_add_output);\n", + " events.on('output_updated.OutputArea', handle_update_output);\n", + " events.on('clear_output.CodeCell', handle_clear_output);\n", + " events.on('delete.Cell', handle_clear_output);\n", + " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", + "\n", + " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", + " safe: true,\n", + " index: 0\n", + " });\n", + "}\n", + "\n", + "if (window.Jupyter !== undefined) {\n", + " try {\n", + " var events = require('base/js/events');\n", + " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", + " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", + " register_renderer(events, OutputArea);\n", + " }\n", + " } catch(err) {\n", + " }\n", + "}\n" + ], + "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from matplotlib.animation import FuncAnimation\n", + "\n", + "import parcels\n", + "from parcels._datasets.structured.generated import (\n", + " simple_UV_dataset,\n", + " simple_UV_sigma_dataset,\n", + ")\n", + "from parcels._datasets.unstructured.generated import sigma_coordinate_lattice_dataset\n", + "\n", + "plt.rcParams[\"animation.html\"] = \"jshtml\"" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building the Dataset\n", + "\n", + "For this tutorial, we construct our dataset using the parcels built in `simple_UV_sigma_dataset`, which makes a structured horizontally gridded dataset that uses a linear vertical $\\sigma$ grid. Its horizontal grid is a flat square that spans from -1000 km to 1000 km in both $x$ and $y$. Here, we assume $\\frac{\\partial \\sigma}{\\partial z}$ to be constant, so for any given z coordinate, the equivalent $\\sigma$ coordinate is a dimensionless fraction of column height, i.e. $\\sigma = \\frac{z + \\eta}{H +\\eta}$, where $H$ is the depth of the water column, and $\\eta$ is the sea surface height. It is common in $\\sigma$ coordinate models to use more advanced vertical grids for which $\\frac{\\partial \\sigma}{\\partial z}$ is not constant, this allows the concentration of vertical layers near the surface. Nonetheless, our simple example allows for a vertical coordinate system that adapts to changes in bathymetry and variations in sea surface height. We will demonstrate this shortly but begin by constructing the dataset itself. At the end of the tutorial, we show the same setup on an unstructured grid, and how to modify the interpolation method used on the multi-dimensional z coordinate." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 34MB\n",
+       "Dimensions:  (time: 241, ZF: 11, YG: 5, XG: 81, YC: 5, XC: 81)\n",
+       "Coordinates:\n",
+       "  * time     (time) datetime64[ns] 2kB 2000-01-01 ... 2000-01-11\n",
+       "  * ZF       (ZF) float64 88B 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0\n",
+       "  * YG       (YG) int64 40B 0 1 2 3 4\n",
+       "    lat      (YG) float64 40B -1e+06 -5e+05 0.0 5e+05 1e+06\n",
+       "  * XG       (XG) int64 648B 0 1 2 3 4 5 6 7 8 9 ... 72 73 74 75 76 77 78 79 80\n",
+       "    lon      (XG) float64 648B -1e+06 -9.75e+05 -9.5e+05 ... 9.75e+05 1e+06\n",
+       "  * YC       (YC) float64 40B 0.5 1.5 2.5 3.5 4.5\n",
+       "  * XC       (XC) float64 648B 0.5 1.5 2.5 3.5 4.5 ... 76.5 77.5 78.5 79.5 80.5\n",
+       "    depth    (time, ZF, YC, XC) float64 9MB 3.923 -3.923 -11.67 ... 993.3 997.8\n",
+       "Data variables:\n",
+       "    U        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
+       "    V        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
+       "    grid     int64 8B 0\n",
+       "    W        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
+       "Attributes:\n",
+       "    Conventions:  SGRID
" + ], + "text/plain": [ + " Size: 34MB\n", + "Dimensions: (time: 241, ZF: 11, YG: 5, XG: 81, YC: 5, XC: 81)\n", + "Coordinates:\n", + " * time (time) datetime64[ns] 2kB 2000-01-01 ... 2000-01-11\n", + " * ZF (ZF) float64 88B 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0\n", + " * YG (YG) int64 40B 0 1 2 3 4\n", + " lat (YG) float64 40B -1e+06 -5e+05 0.0 5e+05 1e+06\n", + " * XG (XG) int64 648B 0 1 2 3 4 5 6 7 8 9 ... 72 73 74 75 76 77 78 79 80\n", + " lon (XG) float64 648B -1e+06 -9.75e+05 -9.5e+05 ... 9.75e+05 1e+06\n", + " * YC (YC) float64 40B 0.5 1.5 2.5 3.5 4.5\n", + " * XC (XC) float64 648B 0.5 1.5 2.5 3.5 4.5 ... 76.5 77.5 78.5 79.5 80.5\n", + " depth (time, ZF, YC, XC) float64 9MB 3.923 -3.923 -11.67 ... 993.3 997.8\n", + "Data variables:\n", + " U (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", + " V (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", + " grid int64 8B 0\n", + " W (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", + "Attributes:\n", + " Conventions: SGRID" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_days = 10\n", + "n_snapshots = 24 * n_days + 1 # hourly snapshots from hour 0 to the end of day 10\n", + "n_vertical_layers = 10\n", + "ny, nx = (\n", + " 5,\n", + " 81,\n", + ") # the number of grid nodes in y and x, so that the cells are 25km wide in x\n", + "\n", + "lon = simple_UV_dataset(\n", + " n_times=n_snapshots, nz=n_vertical_layers + 1, ny=ny, nx=nx, mesh_str=\"flat\"\n", + ")[\"lon\"].values\n", + "dx = lon[1] - lon[0]\n", + "x_centres = lon - 0.5 * dx\n", + "\n", + "# we construct a depth profile that is 100m deep in the west and logarithmically increases to 1000m depth in the east\n", + "domain_length = lon[-1] - lon[0]\n", + "bottom_depth = 100 + 900 * np.log10(1 + 9 * (x_centres - lon[0]) / domain_length)\n", + "bottom_depth = np.broadcast_to(bottom_depth, (ny, nx))\n", + "\n", + "# for illustrative purposes we create a 50m surface wave with a 1000km wavelength that propagates east with time\n", + "t = np.arange(n_snapshots) * 3600.0\n", + "wavelength, wave_period = 1000e3, 2 * 86400.0\n", + "amplitude = 50\n", + "wave = amplitude * np.sin(\n", + " 2.0\n", + " * np.pi\n", + " * (x_centres[np.newaxis, :] / wavelength - t[:, np.newaxis] / wave_period)\n", + ")\n", + "wave = np.broadcast_to(wave[:, np.newaxis, :], (n_snapshots, ny, nx))\n", + "\n", + "# construct the dataset itself\n", + "ds = simple_UV_sigma_dataset(\n", + " n_times=n_snapshots,\n", + " nz=n_vertical_layers + 1,\n", + " ny=ny,\n", + " nx=nx,\n", + " bottom_depth=bottom_depth,\n", + " eta=wave,\n", + ")\n", + "ds" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now compare the evolution of the vertical grid in $\\sigma$ space to the vertical grid in z space. In z space, the vertical grid spacing is much smaller in the shallow west than in the deep east, this demonstrates the strength of a terrain following vertical grid. We can also see that the vertical grid has time dependency in z space. The z coordinate is defined at the cell centres, and by default Parcels interpolates time-varying z bilinearly in space between the surrounding cell centres, so along a row of cells the layer interfaces are straight lines between the cell centres." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "time = ds[\"time\"].values.astype(\"datetime64[s]\")\n", + "z_moving = ds[\"depth\"].values # interface depths, shape (time, depth, YC, XC)\n", + "sigma = np.linspace(0.0, 1.0, n_vertical_layers + 1)\n", + "\n", + "centre_row = ny // 2 # the row of cells in the middle of the domain\n", + "xs = x_centres / 1e3\n", + "\n", + "fig, (ax_sigma, ax_z) = plt.subplots(1, 2, figsize=(12, 4), sharex=True)\n", + "ax_sigma.plot(xs, np.broadcast_to(sigma, (xs.size, sigma.size)), \"k\", linewidth=0.8)\n", + "ax_sigma.set_ylabel(r\"$\\sigma$\")\n", + "ax_sigma.set_title(r\"Layer interfaces in $\\sigma$ space\")\n", + "\n", + "z_lines = ax_z.plot(xs, z_moving[0][:, centre_row].T, \"k\", linewidth=0.8)\n", + "ax_z.set_ylabel(\"depth [m]\")\n", + "z_title = ax_z.set_title(f\"Layer interfaces in z space at {time[0]}\")\n", + "\n", + "for ax in (ax_sigma, ax_z):\n", + " ax.invert_yaxis()\n", + " ax.set_xlabel(\"x [km]\")\n", + "plt.tight_layout()\n", + "\n", + "\n", + "def animate(snapshot):\n", + " for line, interface_depths in zip(\n", + " z_lines, z_moving[snapshot][:, centre_row], strict=True\n", + " ):\n", + " line.set_ydata(interface_depths)\n", + " z_title.set_text(f\"Layer interfaces in z space at {time[snapshot]}\")\n", + "\n", + "\n", + "anim = FuncAnimation(fig, animate, frames=range(0, time.size, 6), interval=300)\n", + "plt.close()\n", + "anim" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building the `FieldSet`\n", + "\n", + "We prescribe a uniform velocity of $u_0 = 1$ m/s in the $x$ direction, with $v = w = 0$. All three components sit on the grid nodes at the layer interfaces (`depth`), so Parcels interpolates them linearly in all three directions.\n", + "\n", + "To advect the particles on a sigma grid, for which the z grid varies in time, we use a 4D `depth` coordinate with dimensions of `(time, depth, YC, XC)`. For each particle, Parcels builds a z column by interpolating the columns at the surrounding cell centres bilinearly in space and linearly in time between the two surrounding snapshots, and then searches that column for the particle's layer." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 34MB\n",
+       "Dimensions:  (time: 241, ZF: 11, YG: 5, XG: 81, YC: 5, XC: 81)\n",
+       "Coordinates:\n",
+       "  * time     (time) datetime64[ns] 2kB 2000-01-01 ... 2000-01-11\n",
+       "  * ZF       (ZF) float64 88B 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0\n",
+       "  * YG       (YG) int64 40B 0 1 2 3 4\n",
+       "    lat      (YG) float64 40B -1e+06 -5e+05 0.0 5e+05 1e+06\n",
+       "  * XG       (XG) int64 648B 0 1 2 3 4 5 6 7 8 9 ... 72 73 74 75 76 77 78 79 80\n",
+       "    lon      (XG) float64 648B -1e+06 -9.75e+05 -9.5e+05 ... 9.75e+05 1e+06\n",
+       "  * YC       (YC) float64 40B 0.5 1.5 2.5 3.5 4.5\n",
+       "  * XC       (XC) float64 648B 0.5 1.5 2.5 3.5 4.5 ... 76.5 77.5 78.5 79.5 80.5\n",
+       "    depth    (time, ZF, YC, XC) float64 9MB 3.923 -3.923 -11.67 ... 993.3 997.8\n",
+       "Data variables:\n",
+       "    U        (time, ZF, YG, XG) float64 9MB 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0\n",
+       "    V        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
+       "    grid     int64 8B 0\n",
+       "    W        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
+       "Attributes:\n",
+       "    Conventions:  SGRID
" + ], + "text/plain": [ + " Size: 34MB\n", + "Dimensions: (time: 241, ZF: 11, YG: 5, XG: 81, YC: 5, XC: 81)\n", + "Coordinates:\n", + " * time (time) datetime64[ns] 2kB 2000-01-01 ... 2000-01-11\n", + " * ZF (ZF) float64 88B 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0\n", + " * YG (YG) int64 40B 0 1 2 3 4\n", + " lat (YG) float64 40B -1e+06 -5e+05 0.0 5e+05 1e+06\n", + " * XG (XG) int64 648B 0 1 2 3 4 5 6 7 8 9 ... 72 73 74 75 76 77 78 79 80\n", + " lon (XG) float64 648B -1e+06 -9.75e+05 -9.5e+05 ... 9.75e+05 1e+06\n", + " * YC (YC) float64 40B 0.5 1.5 2.5 3.5 4.5\n", + " * XC (XC) float64 648B 0.5 1.5 2.5 3.5 4.5 ... 76.5 77.5 78.5 79.5 80.5\n", + " depth (time, ZF, YC, XC) float64 9MB 3.923 -3.923 -11.67 ... 993.3 997.8\n", + "Data variables:\n", + " U (time, ZF, YG, XG) float64 9MB 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0\n", + " V (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", + " grid int64 8B 0\n", + " W (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", + "Attributes:\n", + " Conventions: SGRID" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "u0 = 1.0\n", + "ds[\"U\"].values[:] = u0\n", + "ds" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "| Name | Type | Grid number | Interp method / value | Parcels backend |\n", + "|:-------|:------------|--------------:|:------------------------|:------------------|\n", + "| U | Field | 0 | XLinear(...) | NumPy |\n", + "| V | Field | 0 | XLinear(...) | NumPy |\n", + "| W | Field | 0 | XLinear(...) | NumPy |\n", + "| UV | VectorField | 0 | XLinear_Velocity(...) | - |\n", + "| UVW | VectorField | 0 | XLinear_Velocity(...) | - |\n", + "\n", + "mesh: FlatMesh()\n", + "time interval: (np.datetime64('2000-01-01T00:00:00.000000000'), np.datetime64('2000-01-11T00:00:00.000000000'))\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_2922109/1955122603.py:1: FieldSetWarning: Time-varying (4D) z coordinates are experimental and may cause significant memory overhead that leads to OOM errors. This z coordinate has sizes {'time': 241, 'ZF': 11, 'YC': 5, 'XC': 81} (0.00859 GB). Assumptions: z has dims ('time', 'ZF', 'YC', 'XC'), i.e. it is defined at the layer interfaces on the cell centres or corners, and is strictly increasing along 'ZF'; each particle's z column is interpolated with the grid's `z_interp_method` (by default bilinearly from the surrounding columns), and linearly in time between z snapshots.\n", + " fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n" + ] + } + ], + "source": [ + "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n", + "fieldset.describe()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Release the particles\n", + "\n", + "We release 10 particles at the first snapshot near the shallow western end of the domain, at $x = -890$ km and $y = 0$, at depths from 60 m to 240 m. This puts the shallowest particle just below the deepest wave trough (50 m), so every particle will remain in the water column throughout the simulation." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "n_particles = 10\n", + "x0 = np.full(n_particles, -890e3)\n", + "y0 = np.zeros(n_particles)\n", + "z0 = np.linspace(60.0, 240.0, n_particles) # depths in m\n", + "\n", + "pset = parcels.ParticleSet(\n", + " fieldset=fieldset,\n", + " pclass=parcels.Particle,\n", + " t=np.full(n_particles, time[0]),\n", + " z=z0,\n", + " y=y0,\n", + " x=x0,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKQAAAGGCAYAAABFf1lKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsnQeUFNXWhS85CE+QIEklihIERKKACgKCggQDSFBRMSAmzBkDYA4oomJCFBURxYSgoiAgiuSsIDlIlJz7X995f81rhgk907l7f2vdVT0zPd1Vt9K9u87ZJ4fP5/M5IYQQQgghhBBCCCEiRM5IfZEQQgghhBBCCCGEECBBSgghhBBCCCGEEEJEFAlSQgghhBBCCCGEECKiSJASQgghhBBCCCGEEBFFgpQQQgghhBBCCCGEiCgSpIQQQgghhBBCCCFERJEgJYQQQgghhBBCCCEiigQpIYQQQgghhBBCCBFRJEgJ4ZzbuHGj+/TTT92WLVsy7Y+9e/fae5cvX57lvvvnn3/sfzdt2hQX/b57925b35UrV2bp/9asWWP/t2PHDhfPTJs2zU2YMCGg9y5YsMC2ORbZs2ePrduKFSuivSpCCCGEEEIIYUiQEgnDlClT3JgxY7L1v7NmzXKXXnqpW7RoUcrvfv31Vzdu3Lhj3otoxXvHjx+f5e+ZO3eu/S/iRaz1XVrCy/r16219J06cmKXP++WXX+z/Vq1a5eKZp556yt1+++0pPx84cMCEnaVLlx7z3pEjR9o2xyIIoazb999/H+1VEUKIbLNv3z6XI0cO98QTT4Tl/aH632iR3XWOx22N9vERCWJ9/YQQIhRIkBIJw8CBA13Xrl2z9b+lSpVynTt3dsWLF0/53bPPPutuvvnmY95bsGBBe2+lSpVcIvWdv/DiUahQIdvW8uXLu2SkcePGrlWrVik/E/GFsDN27Nhj3lujRg3rq1jkuOOOs3WrUKFCtFdFCJHk5M+f31100UUh+7zt27fbpH3QoEEuGUi27Q0W9ZeIxnF06NAh99lnn7n27du7cuXKucKFC7tatWq5559/3u3fvz/N/xk9erQ766yzXIECBVzJkiVdr1690s2oCNd70yPQz+BB9uDBg13Tpk1dzpw5sz1/COX3dejQwfZdZo3vy+r3Z/W9wW5vvH5fpviESBAuvPBCX758+UL2eZ07d/ZVqlTJF0omTJjg47SbOHGiL9b6rnr16iH7vJEjR9p2zps3z5dIbNq0ybbrmWeeifaqCCFEXMJ9mntOqNi2bZtdlwcOHOgLJXv37rXPffzxx32xRDi2N1a3NZaPD5FcZPU4GjNmjC9nzpy+W265xbds2TLfjh07fB999JGvcOHCvpYtW/qOHDmS5rj5qaee8u3atcu3ePFiX+3atX01atSw8zMS702PrHzGpZde6rv55pt9kyZN8p155pm+U045JaDviOT3ffnll/b5b775ZtDfH+n+HRmH3xcIEqREQgtSXHR+//13e71mzRrfV1995Zs+ffoxN4INGzb4Ro0a5du8ebP9/MMPP/gaNmzoK1WqlP3ea/zfnj177DU3GI+///475T2ffvqpb9y4cb61a9dmW5DyPo+T/+DBg76ff/7Z980336Ssnz+//vprynePHj3a99NPP9mNLzUzZ870ffHFF/aabWAbv/vuO9/48eN9devW9Z100klHfQ7w/fy8YsWKYz6Pz5g8ebL16V9//RWwIMU28J3sm+XLl/sCYcuWLbYeXp+yT/n/lStXpvs/69at83377bfWb2ntCzhw4IDvt99+s22YM2eO/ezP1KlTbV1h69atvrffftu2q2fPnil9NWPGDPv7/Pnz7ee0mDt3rm/s2LG2b+jT1PD7H3/8MWVbWW/2+f79+9P8vIULF9p2sX5p7evU7N6929aN48pj9uzZvs8++8xec1P5/vvvbVsD+Tx/2G4mMddcc42ve/fuvh49elj/XHnllb7bb789w//duHGjnQsch6tWrTrqb96x560z68s+T32sAedi6vOP/Z8ehw4dsvOBfcLn8nN628Z7WEf6UAgRPBKkgkOCVPT7SyQfWT2OGO8zTkvNoEGDjpkHMAYpXbq0r3Xr1ke9d9asWfbeF198MezvTY9gPoO5RVYFqUh8X0aCVCz376E4/L5AkSAlElqQOvHEE21i/Nxzz/lq1qzpa9Giha9AgQK+Jk2aHDXBRADg5EJggbvvvttXtmxZ33HHHWeRUl47fPiwb/Xq1fbe1157LeX/mcx77+nYsaOvQYMGvly5ctnEnP/JqiDFRZL38blcYJs3b+477bTTfPnz5z/qe+GFF15I+e527dr5qlatatuY+n3XX3+97/jjj7dtrFatmn0mTxMQDRDe/vOf/6R8zmWXXWb/8+eff9p6vPPO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7RzXz36vw9esWeNcddVV9hi0aNHC1s/XXHNNUF2julrHWe0vb3l8/fXXQX9r6tSpttx0L6D6WevrHsGvPvzwQ3svoOOs1x49emSqm/xu9OjRTpMmTZxq1ao5F198sT1+w4YNC1rn008/dQoXLpzp87t///6g9YYPH27rX9V9etX91OnTpx2/iNa9WyLc3+k66t3P1q1b28+g99hG6x4wlvbv3+88/PDDTtWqVe223HvvvWHXe/31150SJUoEtrlfv37O8ePH82WdSBGQyoVnnnnGmTVrlr0ZyCog1aVLF9uYci82v/nNb2wF4q2EorVOPM2cOdOWwZQpU4Iuau3bt8/29/7617/aBufatWsDNw1FixZ1/vOf/zh+oYvRLbfcku06Dz74oG0cqAEoOidSUlKcZcuW5WideNq0aZMzYsSIwEVE59pNN91kG3Znz54NrBcuABVKF2Yde/dv6VzQzb67736iSlMNjQEDBgT2U8f8oosusjdGieKLL74Iarju2rXLnm/33HNPpoBUdg11raNGs9twUBl07drVNiL9FpDStmZl3bp1Nkjzj3/8w35/4MABp169es4vf/nLHK3jNzt27HAKFSoU1NjRtUT1QXYmTJhgG8vugwx93nUT/sILLzjx8utf/9reSD7xxBNhA1K6fqhBNWTIEPt9enq607t3b6dVq1ZRXwf+oc/hY4895mzZssVei8MFpBQwUp2iwJV7jNu2bev07ds36uvE2+9//3t7g+h9uKAbKX1usqOAR5s2bezNkvzxj3+0Nwt6YOQXCqx88MEHeaqfE6EOVyDQ+1BO550e1nnrGjcgNW/evCz/jtr8utbrQYXbTlPdrDrajzZu3Gjb9G6QUXW2rvW33367k0heeeUVG1T0BgXVdvDeqyggpfuZ7Ohhn+pvvYqucepooIdFfhGte7dEuL8Lpbag7st0XKJ9DxhLK1eutIFPPeDQsQwXkFI7Wtv4+eef2+9VLyjgqkBWtNfJCQJSeZBVQGrDhg32fffCI/v27bMXMbcCjtY68aYPrJ4uegMXuqgp2rx8+XJ74xcuYKcbgjvvvDPoPUXi9STJL7Rv1113nbN48WJn8+bNtvHjpe91A68Pv5dubB944IGI1/EjnXM697Zt2xYUkNKN5MKFC+3NcSg1tHRxUg9Clxr5eqL02muvOX6jSlL7qPPU9d1339n3xo8f7yQyVZJ6mhcakFLFofM5XEBHFXKxYsWCgt1qSOv3vJW0HwJSelWFf/To0UzrKKCmSjG0UakbspMnT0a8jt/ouqrPkno0eusgBZcU3Fm1alXYbb/hhhuc7t27B733q1/9KsueSbGUVUBK56muJQquumbPnm2PvdvQi9Y68KesAlLqXadA0okTJwLvjRw50t7suTcF0VonntR20EMh9TTwUkDqvvvuy7Ie3rt3r90H7YtL+6jA1ssvv+z4KSD1l7/8xV7Lw5V3JPVzotbhv/3tb20vmdCAlAIbWdXP6tGrm3rvz/SwQb+n4I/fKJhapUqVoHsDBSrUMyi3PSj8Qj0X1RZ26bidd955gXrYe01xqTeUOhiEttN0/+QX0bp3S4T7u1C6v/nZz34W9XvAeGqfRUBKD+h0Dns9/fTTNkDqfl6jtU5OkNQ8n/JiiJvnQzR+VuM43Z9Fa514Um6CTz/91Nx5552ZcnJo+26++WZz2WWX2TGsH3zwQeBnGuevPAje/ZJ27dr5Yr+8vvrqKzN48GC7bSr3yZMnB36mfD0aDx66H23btg3sRyTr+NGiRYtMWlqaHefu9eabb5pf/vKXNjeUxlF78/msWLHCjsn27qtygGhdP+6rtknj45s1axZ4T/kBlE/Ij9ubE4sXLzZ169bN9P7dd99tBg0aZMd66/PpzUWifa5Xr54577zzAu/pvHd/5ifXXXed+fnPf27zUjz44IM2R5BL2xru2qLcdxrnHuk6fqLP1T//+U9z0003ZcpPoDxT/fr1s4vO3RdeeCHo51ntq/YzXD5AP9A2K5eD9/oTei5Gax0kFh031SnenEg6psrDozoomuvEk9oa27dvt/VtqHfeece+r3xJqr+U29O1cuVKuw/ez7z28eKLL/bdOf/000/b9qNyZF166aVB+WsiqZ8TtQ7Pqn6+5557bP1cqVIlc+ONN9q8Uy7tj9qgZcqUSYhrmbZJuQG99wbaXuVsC5f3MFEoX5RygYUeP+XR8dbDzz//fET1sPJJ6V7KL/J675ZI93cu5c5VTqlw19q83gP60bIszkXlJP3++++juk5OkNTcGJv468SJE1kWUrFixQIX/kjoBkFJFZUc2kvBJDdJbbTWySs1eLRkRw2Z0G2Qjz/+2CalVAUa+mHcvHmzueiii+z3r7zyil1HN7tKjqeLrxIzhia5i+Z+ZZeEPjs6zjre0rdvX/PSSy/Z7dIN729+8xvTv39/m1BSidvcbQ23H2oUSiTr5Ac1qLOr5AoVKmQ6deoU9mfav+eee8489thjQQkZ/+///s8GAhSoUqLKgQMHmmuuucaur4Sw2e1rfh7X3NI2qeEQGkz16/ZG6q9//auZP3++rWBdOo4ffvihbWiIklurwXH//febf/3rX/Y97XPosVP5uD/zAwWgvvnmG7vtbmJcfa3tHjp0aGBb3QkWXO5+ea+t51rHT6ZNm2avqR999FHQ+7qRU1DYTbI5ZswY+xnV9UkBu6yOq75XkEuNKX2e/SbcNuscVjDOewyjsQ7yj24+dfOdHd18q20QqazOZ/dn0Vwnr1TH60Y1K6p7OnfuHPZnf//7302LFi1MmzZtgt7XTdO4ceNsYmUFlH/xi1/Ym2Al9fae17Guh1XneB8MhNL2tmzZMvC9bthvu+02G1BSu0ztLQXcldxb5RJJ/RyPOlz7qH3Njv7/rBKO66He7NmzzcyZMwPvqQzef/9926YSXet79uxp7r33XltvZ3W+Kjiltpwfr2XaJgUHE6WOjYTqTH3+NBGMe6xE9zoKCrvn99ixY23bWAns3fXOdb3xQyLsaNy7xev+Li90rdUx1XXUKxr3gH504Bznos7baK2TEwSkjDF/+tOfso3mqYDV0I+UGryKEutD6QY3REEv9/torZNXEydODNyQZuW1114Lakh4P8T6wCqK7nXFFVcEfa8eDKqE1ZtKFzU3yKHGqlc09yscBdB0M5udL774IjDrmC483gbDn//8Z/tkUo1BPcmKZD/ita86Ztk9hdLMd1OmTMn0vp7WXHnllfa4PvHEE0E/u/XWW4Mal3/5y1/sDbFmb7v88suD9tVbuWpf/VDZhtL2hh6XWByb/PTJJ5+Yhx56yLz77rtBQXQdLzcYJXq69/DDD5tHHnnE9rxRozZcebjf+6U81MD1NnK1j3fccYcZOXJkICAVbj/cBw7ez+W51vETXWv1YECz9niFfq8GVa9evWx5uAGpRNvX7D6bei+7Y5ibdZC/vQn0YCM7vXv3No8//njEf1PHNPRhS7jPdzTWyavXX3/dzu6YFbUrpk+fHrbc1OZ8+eWXM/3slltuCXytYLLWUcNfQQ599rNrc3h7v0bbsGHDsn0Ipt4FCrq41OvApTbXH/7wB9uOUK8DtSsiqZ/jUYfrYdy5zulu3bpl6iEj//nPf+xDoLfeest07NgxqAebN8ChslLd/MADD9j2uc6TcPuqB8LqDefHa1ki1jvnogChAon6zHrbtArkePXp08cuqofd45oI5RGNe7d43fPklgJNui4p8BY6I2I07gH9qGiU2sjRPqcJSBljLxrR5EaXd+3aZWrWrBl4X9+7UyJGa528UrQ/XDfFc1GEWL0Tvv7664jW11PQnTt3Bm6OFeRzv3fp+2jtV1bdw7Xklj586qHhbrd7fLLbj0jWyQ8KSOSUnoz06NHDdOnSxXbTVZDiXMdUwpWHG9Rzvw99yusH2l49vVYj2m1cqIGnp7X5eWzyy2effWYbP2pA6Kn5uej4qfLQkwxNQ63y8HZH9h5bP5eH99oi2o/QBwyh+xHJOn6hYRuff/65efHFFyMuD++QI+1ruOuPbmb9NhWzd5s1ZbRuttzr0J49e+zDGe8xjMY6yD+a1lyBkmjSMdWQkHN9vqOxTl4p8JAbqn/VO94bfMqKhl+rh1C4etg7VFXfa4hffhk/fnyeft/bnlBAKpL6OR51uEYL5Oac1sNOPRRSD2ZvMC678tC+aPiLHvpqXzV8KBHqLNH2qid2omzvudx33332gZ8e5EbyOdLxW7BgwTnrYT0c1mfYr3J67xav+7vcUm+23bt326HD+XEP6EcXZXEuqh5xezVFa52cIIdUPtCTD30ov/zyy8B76kWihrCeAEVznXhRwEMnXGhE3X2C5KXGgYaVeC/i2n5v5aqeYIo6x3u/vNsTGvnVk04FA939UHdpBVq8x0c5efT0xN2PSNbxAz2VVDBK59y///1v2xj2UuBCN3VekyZNsq9ueWh4gQIb3n1du3at2bhxo6/21aX91VMP73moHoNqBKq7fCLRk1fdvPztb38L29gN/Uy6x08NXTcooWOkysSbj0RP6fVUPbQnTryE2w8F0UKvLRr2oYa8dz80hEI3yJGu4xf6PIb2UMyqPE6dOmWf4IaWx4QJE2wQxruvGup4rqBzvGibdaPp7T2ibdZTN/U+iOY6SCw6php+7831pmOqhyDu0NVorRPPHpE33HBDplQJapOobRJ6/dNQIvczr56Uuon01sPaR9XFfqmHs6qPVB83bNgw4vo5UepwnVcajvjqq6+au+66K+Ly0LnoBit07HTjrAfB3r+rdAne3lZ+oe1VQEb3K97t1fAv90Y+UahXm4bLa4RFuGtD6PFTXTtjxoxM9bB7bnrLQ7nTQtvb8RKteze/39+F3stecsklpn79+vlyD+hHl19+uZk6dWrQ8da5qF5wup5Ec50cyXEadNgZLWbNmmWzyasIp0+fbr8/dOhQoHQ0pbZmRHrjjTfsbCeaSeHaa68NKr1orRNrmk5X089q/8PR1MmaylRT0WtmEGXhr1u3rrN///7AOpqNQvulGWS+/PJL5/rrr3cqVqzofP/9944faNYTTSesWWk0k8mbb77pVK9e3enUqZOdbtc1adIkO+vh0KFDndGjR9sZcDRrlHcWkUjWiSfNjqdpS3Wc3HPZXdwZvXS8dFxVDhMnTnT+/Oc/29klfv7znwf9LU1Jn5qaamcs08wjKsNLL73U8StNT6pzWTPMaeZKzQoTOquR3+n81Ow7d9xxR9Cxmz9/fmCdl156ybnpppucDz/80Bk3bpxzzz332HNSn08vTXlev359Z9SoUc7rr7/ulChRws6E5BeaXUrbrpkc9VnSdN+atUfnrff61Lx5c/tZ/eKLL5xnnnnGKVy4sF0/J+v4RYsWLZxbb701y+ndH3vsMXsN/eSTT+y1ReezZmj1zrpVtWpVp1+/fnY9zbCnMluyZIkTLytWrLDn6G233WZnEnPPWe+1deDAgfZnOmffeecdO420Zm7yitY68A/NHqdzoVevXk6bNm3s15oZ0aXZe1SnaBZR1TGqozU7qOqeaK8TD7puq13p3WeXZr7SLFhqD6oe1nVds2yq/eT197//3e6Lrt26VmofdW3IzcxH+UGzt/bs2dPWu6q/1DbSNUmvOa2f/V6HT5482R6LQYMGBdXP8+bNC6wzYsQI58YbbwzUz5oZS/XzP//5z6C/pba/2tK6F/jb3/5m29Avvvii40eaoU2zrXXo0MHOdqp7AtWx3lmYE8Hvfvc7u92aKdp7/PRZdPXv39959NFHnTFjxthrSY8ePezncu3atYF1dP+jmX3VxlI9rFnY1FbW9c4vonXv5vf7O9fOnTvtsf33v/+dr/eAsZSRkRE4Rxs3bmxnCdTXS5cuDayj2ak1E6BmsdU26xzXPkydOjXq6+REiv7JW6yt4NEsRqFdZ90EcN6hSUpGOGrUKPvUWlFwjcdV90yvaK0TS+qlpfwzyskUrlueEuWqW7KejmhYiPK8DBkyJFPyXHWXVx4iJVXXUxONmdfYeb/QkB7th54QqBeJoujqfaIncl56EvLGG2/YpwnqJaT8At4ha5GuEy+aKUHdkcN5++23TePGje3XSpqqfdDTVg0F0Bj5AQMGZPodDS/SkIOjR4+arl272kSA6unnR+r1pSckiurra+Uz0djw0GPsZ8pTos9iKD29UXdkl/ZRw/rUK0h5mPSk1js7keiJkHKSKIm2ZkjUU103F5Ef6KmVzi0ND9FTGT1N17kb+sRV1yBdp5VQWUlvNSw59IlVJOvE244dO2z5K89huKfgOl4aFuT2ANLx1NPc0KF4usZqX5VXTj3AlJskdHaUWNJsj+Fy6+hJo5tIX0+Zdf3VU2V9HpXPQXkevAmMo7UO/ENJ+b29KsLlWtJnX8lmZ82aZZ/Eaqiyfs8rWuvEmvI/al/V6zUcfYZVD+tVPVxVZ2lGtlCjR4+2uVE0nE2J09Vm81M9rB6qyl+oa5OGt2g4m3o85bR+9nsdriH0bi9XLw0x9A5z1D2FcvWojah2sOrn0N44ugfQfYZ6JegeQMc9kmGd8RxurnpHszZrqJOGRYUbVeFnt99+u+3lH0o98NwUIDouqofVbtIttVsPa8RA6D3F8OHDbW9Fzf6qdULzT8VTNO/d/H5/J2oP61qq3lve2Vbz4x4wVk6cOBG2HRuaw091rM5FpXdQL0wdZ92veUVrnUgRkAIAAAAAAEBM+TOBBAAAAAAAAJIWASkAAAAAAADEFAEpAAAAAAAAxBQBKQAAAAAAAMQUASkAAAAAAADEFAEpAAAAAAAAxBQBKQAAAAAAAMRUkdj+dwAQG3PmzDE7duywX/fs2dNUqFDBLFq0yKSnp5uOHTtG9f9atWqVWb16tf26Xbt2pnbt2lH9+wAAAH7w+eefm9OnT5vChQub66+/3r731VdfmSZNmkS9/fPNN9+Yffv22a/79u1rSpYsGdW/DyD+CEgBSEovv/yyWb58uWnTpo1p2bKlDUi99dZb5ujRo1EPSK1du9aMHj3aNshGjBhBQAoAACSlwYMHm6ZNm9q2jhuQuu+++8yTTz4Z9fbPjBkzzLJly8y4cePMli1bCEgBSYiAFICkpZ5Rb775Zr7/PzfccINdatasme//FwAAQDw9+OCDZsCAAfn+/zz77LPmu+++swEpAMmJHFIAfO/kyZNm1KhRZvPmzUHvT5482cyePTvP3cEnTJhgv9aQvnnz5pljx47ZIX9jx441hw8ftj87cuSIXW/KlCnmxIkTefo/AQAA4m3btm1m5MiR5vjx44H3zpw5Y9tcGzduzPXf1ZC+Tz/91PZuEvUgVxtu9+7dZtKkSWb69OkmIyPD/uz77783Y8aMMQsXLozCHgFINPSQAuB7xYsXNxMnTrRPyhYvXhz4vk+fPrY7d279/ve/t8P43ICUvp4/f74d1teoUSPbPfzgwYPmT3/6k3n66adNw4YNbQMtNTXVLFiwgK7jAAAgYVWsWNEMGzbMTJs2zbaB5IknnrABqRUrVuTqb6oNdd1119mHe27PJg3pu/DCC82uXbvscD+1terXr2/Xe/31123+KT0I1PfvvvtuVPcRgL8RkAKQEF599VWbC+q3v/2teeqpp8ygQYNso6lTp045/ltnz541v/rVr8z48eNtD6t69eoFfrZp0yb7RE/BJz0lbNCggRkyZIhtmNWpU8f21qpVq5b58MMPzV133RXlvQQAAIiNtLQ089FHH5kOHTqYXr16mdKlS9scnOo9XqZMmRz/vf3795vevXub8uXL217s+vveQNW3335rH+atW7fOPvhTO0uTwpQoUcI+cGzbtq15/PHHycUJFCAEpAAkhFKlStkgUJcuXczMmTNtUGjo0KG56kZ+880320aRnsZVrVo16OfdunWzwSgpWrSoad26tf0dBaNEvbNatGhhNmzYEKU9AwAAiA897PvDH/5g7rzzTtsD/OGHHzaXXHJJjv+Oht517drV/r333nvPtqG8br311kDPcrWzFPAaOHCgDUaJ2lvFihWz7StmKwYKDgJSABJGu3bt7LS/mnJ47ty5dsrhnNLwPOWAUnfx0GCUlC1bNuh7Nc5CpxnWe+opBQAAkAxJyl944QXbPlIv9Nx48cUXzfnnn2/efvvtTMGorNpX3vdSUlLs79G+AgoWkpoDSBizZs0yX375pX2y9swzz+Tqbyigdffdd9s8BXlJ2AkAAJAMnn/++UAQ6bXXXsvV31C7THmi+vXrx+QvACJGQApAQvjpp59s1+6HHnrI5n7SbHi5aTTpCdwbb7xhE6L36NHDTicMAABQEKnH+HPPPWeH2SmxufJz5iahuYbgKW+UJoPRwz9mJAYQCQJSAHEKva4AAAHFSURBVBLCPffcY8qVK2dn2qtZs6adleWRRx6xyTBzE5R68803beLN7t27E5QCAAAFzpEjR8wtt9xiJ2+54oorzPXXX2/zbGrJzdA5DcFTQnQFpa6++mqCUgDOiYAUAN9bv369nRlPSc2V8FLUgHr00UftEL6saMa8kSNHmn379gVyULmz8ikopSeBd9xxh31NT08P+rmrY8eOpn379kHvKWmnkm+6Vq1aZf8fTXEMAACQCCZOnGh7iw8fPjzwnnqfa7Y7BZayoklhPv3008D36hHlTv6ioJR6StWoUcPO4Bf6c9c111xjHzB6DRgwwFSrVi3wvbZh3LhxUdhTAH6V4jiOE++NAIBoe+WVV2w3dBk2bJhp0KBBvhXyJ598YhOti/JTqXEHAACQbPQgTw/g9IDw/fffz9f/S7Mpu/k+FSirWLFivv5/AGKPgBQAAAAAAABiiiF7AAAAAAAAiCkCUgAAAAAAAIgpAlIAAAAAAACIKQJSAAAAAAAAiCkCUgAAAAAAAIgpAlIAAAAAAACIKQJSAAAAAAAAiCkCUgAAAAAAAIgpAlIAAAAAAACIKQJSAAAAAAAAiCkCUgAAAAAAADCx9P8Aq8lQ1tsElbMAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "H = z_moving[0, -1, centre_row]\n", + "eta = -z_moving[:, 0, centre_row]\n", + "# initial sigma coordinate of each particle, using its z column interpolated linearly in x between the cell centres\n", + "eta0 = np.interp(x0, x_centres, eta[0])\n", + "sigma0 = (z0 + eta0) / (np.interp(x0, x_centres, H) + eta0)\n", + "\n", + "colors = [f\"C{p}\" for p in range(n_particles)]\n", + "\n", + "fig, (ax_sigma, ax_z) = plt.subplots(1, 2, figsize=(12, 4), sharex=True)\n", + "ax_sigma.plot(xs, np.broadcast_to(sigma, (xs.size, sigma.size)), \"k\", linewidth=0.8)\n", + "ax_sigma.scatter(x0 / 1e3, sigma0, color=colors, zorder=3)\n", + "ax_sigma.set_ylabel(r\"$\\sigma$\")\n", + "ax_sigma.set_title(r\"Initial particle positions in $\\sigma$ space\")\n", + "\n", + "ax_z.plot(xs, z_moving[0][:, centre_row].T, \"k\", linewidth=0.8)\n", + "ax_z.scatter(x0 / 1e3, z0, color=colors, zorder=3)\n", + "ax_z.set_ylabel(\"depth [m]\")\n", + "ax_z.set_title(f\"Initial particle positions in z space at {time[0]}\")\n", + "\n", + "for ax in (ax_sigma, ax_z):\n", + " ax.invert_yaxis()\n", + " ax.set_xlabel(\"x [km]\")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Advect the particles\n", + "\n", + "We advect the particles for 10 days using the 2D second-order Runge-Kutta kernel {py:func}`parcels.kernels.AdvectionRK2` with a 30-minute time step, and write their positions every 6 hours. We chose the 2D version of RK2 since the vertical velocity is zero across the domain." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "tags": [ + "hide-output" + ] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: Output files are stored in sigma_particles.parquet\n" + ] + } + ], + "source": [ + "outputfile = parcels.ParticleFile(\n", + " path=\"sigma_particles.parquet\", outputdt=np.timedelta64(6, \"h\"), mode=\"w\"\n", + ")\n", + "\n", + "pset.execute(\n", + " [parcels.kernels.AdvectionRK2],\n", + " runtime=np.timedelta64(n_days, \"D\"),\n", + " dt=np.timedelta64(30, \"m\"),\n", + " output_file=outputfile,\n", + " verbose_progress=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df = parcels.read_particlefile(\"sigma_particles.parquet\").sort([\"t\", \"particle_id\"])\n", + "output_times = df[\"t\"].unique().sort()\n", + "n_frames = len(output_times)\n", + "px = df[\"x\"].to_numpy().reshape(n_frames, n_particles)\n", + "pz = df[\"z\"].to_numpy().reshape(n_frames, n_particles)\n", + "\n", + "# calculate the sigma coordinate of every particle at every output time, using its z column interpolated linearly in x\n", + "# between the cell centres\n", + "snapshots = np.searchsorted(time, output_times.to_numpy(), side=\"right\") - 1\n", + "psigma = np.empty_like(pz)\n", + "for frame, snapshot in enumerate(snapshots):\n", + " eta_p = np.interp(px[frame], x_centres, eta[snapshot])\n", + " psigma[frame] = (pz[frame] + eta_p) / (np.interp(px[frame], x_centres, H) + eta_p)\n", + "\n", + "colors = [f\"C{p}\" for p in range(n_particles)]\n", + "\n", + "fig, (ax_sigma, ax_z) = plt.subplots(1, 2, figsize=(12, 4), sharex=True)\n", + "ax_sigma.plot(xs, np.broadcast_to(sigma, (xs.size, sigma.size)), \"k\", linewidth=0.8)\n", + "z_lines = ax_z.plot(xs, z_moving[0][:, centre_row].T, \"k\", linewidth=0.8)\n", + "sigma_trails = [ax_sigma.plot([], [], color=c, linewidth=1.5)[0] for c in colors]\n", + "z_trails = [ax_z.plot([], [], color=c, linewidth=1.5)[0] for c in colors]\n", + "sigma_dots = ax_sigma.scatter(px[0] / 1e3, psigma[0], c=colors, zorder=3)\n", + "z_dots = ax_z.scatter(px[0] / 1e3, pz[0], c=colors, zorder=3)\n", + "\n", + "ax_sigma.set_ylabel(r\"$\\sigma$\")\n", + "ax_sigma.set_title(r\"Particles in $\\sigma$ space\")\n", + "ax_z.set_ylabel(\"depth [m]\")\n", + "z_title = ax_z.set_title(\"\")\n", + "for ax in (ax_sigma, ax_z):\n", + " ax.invert_yaxis()\n", + " ax.set_xlabel(\"x [km]\")\n", + "plt.tight_layout()\n", + "\n", + "\n", + "def animate(frame):\n", + " for p in range(n_particles):\n", + " sigma_trails[p].set_data(px[: frame + 1, p] / 1e3, psigma[: frame + 1, p])\n", + " z_trails[p].set_data(px[: frame + 1, p] / 1e3, pz[: frame + 1, p])\n", + " sigma_dots.set_offsets(np.c_[px[frame] / 1e3, psigma[frame]])\n", + " z_dots.set_offsets(np.c_[px[frame] / 1e3, pz[frame]])\n", + " for line, interface_depths in zip(\n", + " z_lines, z_moving[snapshots[frame]][:, centre_row], strict=True\n", + " ):\n", + " line.set_ydata(interface_depths)\n", + " z_title.set_text(f\"Particles in z space at {output_times[frame]}\")\n", + "\n", + "\n", + "animate(0)\n", + "anim = FuncAnimation(fig, animate, frames=n_frames, interval=300)\n", + "plt.close()\n", + "anim" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that, as expected, the particles move horizontally in x and cross the terrain following depth levels. However, in z space, they maintain their exact height as expected." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Sigma coordinates on unstructured grids\n", + "\n", + "The same approach works on unstructured grids. On an unstructured grid, z is a 3D `zf` coordinate with dimensions `(time, zf, n_node/n_face)`, rather than a 4D `depth` coordinate with dimensions `(time, depth, Y, X)`. \n", + "\n", + "Below we build the same basin on a triangulated lattice with `sigma_coordinate_lattice_dataset`, evaluating the bathymetry and sea surface height at the lattice nodes, and compare the two z coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "structured z: ('time', 'ZF', 'YC', 'XC') (241, 11, 5, 81)\n", + "unstructured z: ('time', 'zf', 'n_node') (241, 11, 441)\n" + ] + } + ], + "source": [ + "n_lattice_nodes = (\n", + " 21 # the number of lattice nodes in x and y, so that the nodes are 100km apart\n", + ")\n", + "x_nodes = np.linspace(-1000e3, 1000e3, n_lattice_nodes)\n", + "\n", + "# the same bathymetry and surface wave as above, evaluated at the lattice nodes, with shapes (x, y) and (time, x, y)\n", + "bottom_depth_nodes = 100 + 900 * np.log10(\n", + " 1 + 9 * (x_nodes - x_nodes[0]) / domain_length\n", + ")\n", + "bottom_depth_nodes = np.broadcast_to(\n", + " bottom_depth_nodes[:, np.newaxis], (n_lattice_nodes, n_lattice_nodes)\n", + ")\n", + "wave_nodes = amplitude * np.sin(\n", + " 2.0 * np.pi * (x_nodes[np.newaxis, :] / wavelength - t[:, np.newaxis] / wave_period)\n", + ")\n", + "wave_nodes = np.broadcast_to(\n", + " wave_nodes[:, :, np.newaxis], (n_snapshots, n_lattice_nodes, n_lattice_nodes)\n", + ")\n", + "\n", + "ux_ds = sigma_coordinate_lattice_dataset(\n", + " n_lattice_nodes,\n", + " (-1000e3, 1000e3),\n", + " (-1000e3, 1000e3),\n", + " n_vertical_layers + 1,\n", + " bottom_depth_nodes,\n", + " eta=wave_nodes,\n", + ")\n", + "print(\"structured z: \", ds[\"depth\"].dims, ds[\"depth\"].shape)\n", + "print(\"unstructured z:\", ux_ds[\"zf\"].dims, ux_ds[\"zf\"].shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "| Name | Type | Grid number | Interp method / value | Parcels backend |\n", + "|:-------|:------------|--------------:|:--------------------------|:------------------|\n", + "| U | Field | 0 | UxLinearNodeLinearZF(...) | NumPy |\n", + "| V | Field | 0 | UxLinearNodeLinearZF(...) | NumPy |\n", + "| W | Field | 0 | UxLinearNodeLinearZF(...) | NumPy |\n", + "| UV | VectorField | 0 | Ux_Velocity(...) | - |\n", + "| UVW | VectorField | 0 | Ux_Velocity(...) | - |\n", + "\n", + "mesh: FlatMesh()\n", + "time interval: (np.datetime64('2000-01-01T00:00:00.000000000'), np.datetime64('2000-01-11T00:00:00.000000000'))\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_2922109/1845485040.py:2: FieldSetWarning: Time-varying (3D) z coordinates are experimental and may cause significant memory overhead that leads to OOM errors. This z coordinate has sizes {'time': 241, 'zf': 11, 'n_node': 441} (0.00935 GB). Assumptions: z has dims ('time', 'zf', 'n_node'), i.e. it is defined at the layer interfaces ('zf') on the mesh nodes or faces, and is strictly increasing along 'zf'; each particle's z column is interpolated with the grid's `z_interp_method` (by default barycentrically from its face's nodes for node z, or constant over its face for face z), and linearly in time between z snapshots.\n", + " ux_fieldset = parcels.FieldSet.from_ugrid_conventions(ux_ds)\n" + ] + } + ], + "source": [ + "ux_ds[\"U\"].values[:] = u0\n", + "ux_fieldset = parcels.FieldSet.from_ugrid_conventions(ux_ds)\n", + "ux_fieldset.describe()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We release the same 10 particles at the same positions on the unstructured grid, and advect them with the same kernel, time step and output frequency as before." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": { + "tags": [ + "hide-output" + ] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: Output files are stored in ux_sigma_particles.parquet\n" + ] + } + ], + "source": [ + "ux_pset = parcels.ParticleSet(\n", + " fieldset=ux_fieldset,\n", + " pclass=parcels.Particle,\n", + " t=np.full(n_particles, time[0]),\n", + " z=z0,\n", + " y=y0,\n", + " x=x0,\n", + ")\n", + "ux_outputfile = parcels.ParticleFile(\n", + " path=\"ux_sigma_particles.parquet\", outputdt=np.timedelta64(6, \"h\"), mode=\"w\"\n", + ")\n", + "\n", + "ux_pset.execute(\n", + " [parcels.kernels.AdvectionRK2],\n", + " runtime=np.timedelta64(n_days, \"D\"),\n", + " dt=np.timedelta64(30, \"m\"),\n", + " output_file=ux_outputfile,\n", + " verbose_progress=False,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ux_df = parcels.read_particlefile(\"ux_sigma_particles.parquet\").sort(\n", + " [\"t\", \"particle_id\"]\n", + ")\n", + "ux_px = ux_df[\"x\"].to_numpy().reshape(n_frames, n_particles)\n", + "ux_pz = ux_df[\"z\"].to_numpy().reshape(n_frames, n_particles)\n", + "\n", + "# the particles move along the row of lattice nodes at y = 0, i.e. along triangle edges, so the z column that\n", + "# Parcels uses for each particle is the linear interpolation in x of the z columns at the nodes of that row\n", + "row = np.isclose(ux_ds.uxgrid.node_lat.values, 0.0)\n", + "z_nodes = ux_ds[\"zf\"].values[\n", + " :, :, row\n", + "] # interface depths along y = 0, shape (time, zf, x)\n", + "H_nodes, eta_nodes = z_nodes[0, -1], -z_nodes[:, 0]\n", + "ux_psigma = np.empty_like(ux_pz)\n", + "for frame, snapshot in enumerate(snapshots):\n", + " ux_eta_p = np.interp(ux_px[frame], x_nodes, eta_nodes[snapshot])\n", + " ux_psigma[frame] = (ux_pz[frame] + ux_eta_p) / (\n", + " np.interp(ux_px[frame], x_nodes, H_nodes) + ux_eta_p\n", + " )\n", + "\n", + "fig, (ax_sigma, ax_z) = plt.subplots(1, 2, figsize=(12, 4), sharex=True)\n", + "ax_sigma.plot(\n", + " x_nodes / 1e3,\n", + " np.broadcast_to(sigma, (x_nodes.size, sigma.size)),\n", + " \"k\",\n", + " linewidth=0.8,\n", + ")\n", + "z_lines = ax_z.plot(x_nodes / 1e3, z_nodes[0].T, \"k\", linewidth=0.8)\n", + "sigma_trails = [ax_sigma.plot([], [], color=c, linewidth=1.5)[0] for c in colors]\n", + "z_trails = [ax_z.plot([], [], color=c, linewidth=1.5)[0] for c in colors]\n", + "sigma_dots = ax_sigma.scatter(ux_px[0] / 1e3, ux_psigma[0], c=colors, zorder=3)\n", + "z_dots = ax_z.scatter(ux_px[0] / 1e3, ux_pz[0], c=colors, zorder=3)\n", + "\n", + "ax_sigma.set_ylabel(r\"$\\sigma$\")\n", + "ax_sigma.set_title(r\"Particles in $\\sigma$ space on the unstructured grid\")\n", + "ax_z.set_ylabel(\"depth [m]\")\n", + "z_title = ax_z.set_title(\"\")\n", + "for ax in (ax_sigma, ax_z):\n", + " ax.invert_yaxis()\n", + " ax.set_xlabel(\"x [km]\")\n", + "plt.tight_layout()\n", + "\n", + "\n", + "def animate(frame):\n", + " for p in range(n_particles):\n", + " sigma_trails[p].set_data(ux_px[: frame + 1, p] / 1e3, ux_psigma[: frame + 1, p])\n", + " z_trails[p].set_data(ux_px[: frame + 1, p] / 1e3, ux_pz[: frame + 1, p])\n", + " sigma_dots.set_offsets(np.c_[ux_px[frame] / 1e3, ux_psigma[frame]])\n", + " z_dots.set_offsets(np.c_[ux_px[frame] / 1e3, ux_pz[frame]])\n", + " for line, interface_depths in zip(z_lines, z_nodes[snapshots[frame]]):\n", + " line.set_ydata(interface_depths)\n", + " z_title.set_text(f\"Particles in z space at {output_times[frame]}\")\n", + "\n", + "\n", + "animate(0)\n", + "anim = FuncAnimation(fig, animate, frames=n_frames, interval=300)\n", + "plt.close()\n", + "anim" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As on the structured grid, the particles keep their exact height in z space while crossing the terrain following depth levels." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Choosing how z is interpolated\n", + "\n", + "With a 4d z coordinate, in order to find a particle z value at some time, we must interpolate the current z grid at that time onto the particle position. This reduces the 4d z coordinate to a 1d z coordiante where each entry is an interface depth at the particle's exact position in space. For more details on interpolation in general, see [tutorial_interpolation](tutorial_interpolation.ipynb). There are currently a number of built in interpolators for vertical coordinates:\n", + "\n", + "- `XVerticalCoordinateLinear` (structured grids, z on the cell centres or corners): bilinear interpolation between the surrounding columns, so z is continuous in space.\n", + "- `XVerticalCoordinateNearest` (structured grids, z on the cell centres or corners): piecewise constant interpolation based on whatever cell is closest to the particle.\n", + "- `XVerticalCoordinateConstant` (structured grids, z on the cell centres): piecewise constant interpolation based on whatever cell the particle is in. \n", + "- `UxVerticalCoordinateLinearNode` (unstructured grids, z on the nodes): barycentric interpolation from the three corners of the particle's triangle.\n", + "- `UxVerticalCoordinateConstantFace` (unstructured grids, z on the faces): piecewise constant interpolation based on whatever cell the particle is in. \n", + "\n", + "All of them interpolate z linearly in time between the two surrounding snapshots.\n", + "\n", + "The effect of selecting different vertical coordinate interpolators can be seen by investigating what happens to particles near the surface:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "INFO: Output files are stored in XVerticalCoordinateLinear_particles.parquet\n", + "INFO: Output files are stored in XVerticalCoordinateConstant_particles.parquet\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/tmp/ipykernel_2922109/323142981.py:24: FieldSetWarning: Time-varying (4D) z coordinates are experimental and may cause significant memory overhead that leads to OOM errors. This z coordinate has sizes {'time': 241, 'ZF': 11, 'YC': 5, 'XC': 11} (0.00117 GB). Assumptions: z has dims ('time', 'ZF', 'YC', 'XC'), i.e. it is defined at the layer interfaces on the cell centres or corners, and is strictly increasing along 'ZF'; each particle's z column is interpolated with the grid's `z_interp_method` (by default bilinearly from the surrounding columns), and linearly in time between z snapshots.\n", + " coarse_fieldset = parcels.FieldSet.from_sgrid_conventions(coarse_ds)\n" + ] + } + ], + "source": [ + "# the same basin as above on a coarser grid with 200 km wide cells, so that the difference between the interpolators is\n", + "# easy to see\n", + "coarse_nx = 11\n", + "coarse_lon = np.linspace(lon[0], lon[-1], coarse_nx)\n", + "coarse_x_centres = coarse_lon - 0.5 * (coarse_lon[1] - coarse_lon[0])\n", + "# the first cell centre lies west of the domain, where the depth profile is held at its western value of 100 m\n", + "coarse_bottom_depth = 100 + 900 * np.log10(\n", + " 1 + 9 * np.clip(coarse_x_centres - lon[0], 0.0, None) / domain_length\n", + ")\n", + "coarse_wave = amplitude * np.sin(\n", + " 2.0\n", + " * np.pi\n", + " * (coarse_x_centres[np.newaxis, :] / wavelength - t[:, np.newaxis] / wave_period)\n", + ")\n", + "coarse_ds = simple_UV_sigma_dataset(\n", + " n_times=n_snapshots,\n", + " nz=n_vertical_layers + 1,\n", + " ny=ny,\n", + " nx=coarse_nx,\n", + " bottom_depth=np.broadcast_to(coarse_bottom_depth, (ny, coarse_nx)),\n", + " eta=np.broadcast_to(coarse_wave[:, np.newaxis, :], (n_snapshots, ny, coarse_nx)),\n", + ")\n", + "coarse_ds[\"U\"].values[:] = u0\n", + "coarse_fieldset = parcels.FieldSet.from_sgrid_conventions(coarse_ds)\n", + "\n", + "\n", + "def DeleteParticle(particles, fieldset):\n", + " any_error = particles.state >= 50 # e.g. particles that are above the sea surface\n", + " particles[any_error].state = parcels.StatusCode.Delete\n", + "\n", + "\n", + "# release particles near the surface, 15 km west of the cell edge at x = -400 km, where the sea surface of the next\n", + "# cell is lower than in the cell they start in, and advect them for one day with each interpolator\n", + "surface_z0 = np.linspace(22.0, 31.0, 4) # depths in m\n", + "z_interp_methods = {\n", + " \"XVerticalCoordinateLinear\": parcels.interpolators.XVerticalCoordinateLinear(),\n", + " \"XVerticalCoordinateConstant\": parcels.interpolators.XVerticalCoordinateConstant(),\n", + "}\n", + "for name, z_interp_method in z_interp_methods.items():\n", + " coarse_fieldset.U.grid.z_interp_method = z_interp_method\n", + " surface_pset = parcels.ParticleSet(\n", + " fieldset=coarse_fieldset,\n", + " pclass=parcels.Particle,\n", + " t=np.full(surface_z0.size, time[0]),\n", + " z=surface_z0,\n", + " y=np.zeros(surface_z0.size),\n", + " x=np.full(surface_z0.size, -415e3),\n", + " )\n", + " surface_outputfile = parcels.ParticleFile(\n", + " path=f\"{name}_particles.parquet\", outputdt=np.timedelta64(1, \"h\"), mode=\"w\"\n", + " )\n", + " surface_pset.execute(\n", + " [parcels.kernels.AdvectionRK2, DeleteParticle],\n", + " runtime=np.timedelta64(1, \"D\"),\n", + " dt=np.timedelta64(30, \"m\"),\n", + " output_file=surface_outputfile,\n", + " verbose_progress=False,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
\n", + " \n", + "
\n", + " \n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
\n", + "
\n", + "
\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "n_surface_frames = 25 # hourly outputs from hour 0 to hour 24\n", + "surface_px = {}\n", + "for name in z_interp_methods:\n", + " surface_df = parcels.read_particlefile(f\"{name}_particles.parquet\")\n", + " frames = (surface_df[\"t\"].to_numpy() - time[0]) // np.timedelta64(1, \"h\")\n", + " surface_px[name] = np.full((n_surface_frames, surface_z0.size), np.nan)\n", + " surface_px[name][frames, surface_df[\"particle_id\"].to_numpy()] = surface_df[\"x\"]\n", + " # a particle that has left the water stays at its last position\n", + " for frame in range(1, n_surface_frames):\n", + " left_the_water = np.isnan(surface_px[name][frame])\n", + " surface_px[name][frame, left_the_water] = surface_px[name][frame - 1, left_the_water]\n", + "\n", + "coarse_z = coarse_ds[\"depth\"].values[:, :, centre_row] # interface depths, shape (time, depth, x)\n", + "surface_colors = [f\"C{p}\" for p in range(surface_z0.size)]\n", + "\n", + "fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True)\n", + "panels = []\n", + "for ax, name, drawstyle in zip(\n", + " axes, z_interp_methods, [\"default\", \"steps-mid\"], strict=True\n", + "):\n", + " lines = ax.plot(\n", + " coarse_x_centres / 1e3, coarse_z[0].T, \"k\", linewidth=0.8, drawstyle=drawstyle\n", + " )\n", + " dots = ax.scatter(surface_px[name][0] / 1e3, surface_z0, c=surface_colors, zorder=3)\n", + " ax.set_title(name)\n", + " ax.set_xlim(-800, 0)\n", + " ax.set_xlabel(\"x [km]\")\n", + " panels.append((name, lines, dots))\n", + "axes[0].set_ylim(300, -75) # the upper part of the water column, with depth increasing downwards\n", + "axes[0].set_ylabel(\"depth [m]\")\n", + "suptitle = fig.suptitle(f\"Particles near the surface at {time[0]}\")\n", + "plt.tight_layout()\n", + "\n", + "\n", + "def animate(frame):\n", + " for name, lines, dots in panels:\n", + " for line, interface_depths in zip(lines, coarse_z[frame], strict=True):\n", + " line.set_ydata(interface_depths)\n", + " dots.set_offsets(np.c_[surface_px[name][frame] / 1e3, surface_z0])\n", + " suptitle.set_text(f\"Particles near the surface at {time[frame]}\")\n", + "\n", + "\n", + "animate(0)\n", + "anim = FuncAnimation(fig, animate, frames=n_surface_frames, interval=300)\n", + "plt.close()\n", + "anim" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With `XVerticalCoordinateLinear`, the sea surface and the layer interfaces vary smoothly between the cell centres, while with `XVerticalCoordinateConstant` they jump at every cell edge. The discontinuity of piecewise constant vertical coordinate interpolators can cause particles to spuriously leave the domain, as shown above. \n", + "\n", + "By default, Parcels uses `XVerticalCoordinateLinear` for time-varying z on structured grids, `UxVerticalCoordinateLinearNode` for z on the nodes of an unstructured grid, and `UxVerticalCoordinateConstantFace` for z on the faces of an unstructured grid. To use a different interpolator, set the `z_interp_method` of the grid after building the `FieldSet`, which changes it for every field on that grid:\n", + "\n", + "```python\n", + "fieldset.U.grid.z_interp_method = parcels.interpolators.XVerticalCoordinateConstant()\n", + "```\n", + "\n", + "Parcels raises an error when the interpolator does not fit where z is defined, for example `XVerticalCoordinateConstant` with z on the cell corners." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.14.6" + }, + "mystnb": { + "execution_in_temp": true + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 207d713c5e..ac7d00855d 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -62,7 +62,7 @@ examples/tutorial_aifs.ipynb :name: work-with-fieldsets :titlesonly: examples/explanation_grids.ipynb -examples/tutorial_sigma_coordinates.ipynb +examples/tutorial_time_varying_z_coordinates.ipynb examples/tutorial_velocityconversion.ipynb examples/tutorial_nestedgrids.ipynb examples/tutorial_manipulating_field_data.ipynb From 012c0fed3bfdd88118738aa84e3a39cc0ee1a53e Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 20:04:12 +0000 Subject: [PATCH 13/16] Clear notebook output --- .../examples/tutorial_sigma_coordinates.ipynb | 525 - .../tutorial_time_varying_z_coordinates.ipynb | 208099 +-------------- 2 files changed, 29 insertions(+), 208595 deletions(-) delete mode 100644 docs/user_guide/examples/tutorial_sigma_coordinates.ipynb diff --git a/docs/user_guide/examples/tutorial_sigma_coordinates.ipynb b/docs/user_guide/examples/tutorial_sigma_coordinates.ipynb deleted file mode 100644 index b8720d4357..0000000000 --- a/docs/user_guide/examples/tutorial_sigma_coordinates.ipynb +++ /dev/null @@ -1,525 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# 🖥️ Sigma Coordinate Vertical Grids" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import matplotlib.pyplot as plt\n", - "import numpy as np\n", - "from matplotlib.animation import FuncAnimation\n", - "\n", - "import parcels\n", - "from parcels._datasets.structured.generated import (\n", - " simple_UV_dataset,\n", - " simple_UV_sigma_dataset,\n", - ")\n", - "from parcels._datasets.unstructured.generated import sigma_coordinate_lattice_dataset\n", - "\n", - "plt.rcParams[\"animation.html\"] = \"jshtml\"" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Building the Dataset\n", - "\n", - "For this tutorial, we construct our dataset using the parcels built in `simple_UV_sigma_dataset`, which makes a structured horizontally gridded dataset that uses a linear vertical $\\sigma$ grid. Its horizontal grid is a flat square that spans from -1000 km to 1000 km in both $x$ and $y$. Here, we assume $\\frac{\\partial \\sigma}{\\partial z}$ to be constant, so for any given z coordinate, the equivalent $\\sigma$ coordinate is a dimensionless fraction of column height, i.e. $\\sigma = \\frac{z + \\eta}{H +\\eta}$, where $H$ is the depth of the water column, and $\\eta$ is the sea surface height. It is common in $\\sigma$ coordinate models to use more advanced vertical grids for which $\\frac{\\partial \\sigma}{\\partial z}$ is not constant, this allows the concentration of vertical layers near the surface. Nonetheless, our simple example allows for a vertical coordinate system that adapts to changes in bathymetry and variations in sea surface height. We will demonstrate this shortly but begin by constructing the dataset itself. At the end of the tutorial, we show the same setup on an unstructured grid." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_days = 10\n", - "n_snapshots = 24 * n_days + 1 # hourly snapshots from hour 0 to the end of day 10\n", - "n_vertical_layers = 10\n", - "ny, nx = (\n", - " 5,\n", - " 81,\n", - ") # the number of grid nodes in y and x, so that the cells are 25km wide in x\n", - "\n", - "lon = simple_UV_dataset(\n", - " n_times=n_snapshots, nz=n_vertical_layers + 1, ny=ny, nx=nx, mesh_str=\"flat\"\n", - ")[\"lon\"].values\n", - "dx = lon[1] - lon[0]\n", - "x_centres = lon - 0.5 * dx\n", - "\n", - "# we construct a depth profile that is 100m deep in the west and logarithmically increases to 1000m depth in the east\n", - "domain_length = lon[-1] - lon[0]\n", - "bottom_depth = 100 + 900 * np.log10(1 + 9 * (x_centres - lon[0]) / domain_length)\n", - "bottom_depth = np.broadcast_to(bottom_depth, (ny, nx))\n", - "\n", - "# for illustrative purposes we create a 50m surface wave with a 1000km wavelength that propagates east with time\n", - "t = np.arange(n_snapshots) * 3600.0\n", - "wavelength, wave_period = 1000e3, 2 * 86400.0\n", - "amplitude = 50\n", - "wave = amplitude * np.sin(\n", - " 2.0\n", - " * np.pi\n", - " * (x_centres[np.newaxis, :] / wavelength - t[:, np.newaxis] / wave_period)\n", - ")\n", - "wave = np.broadcast_to(wave[:, np.newaxis, :], (n_snapshots, ny, nx))\n", - "\n", - "# construct the dataset itself\n", - "ds = simple_UV_sigma_dataset(\n", - " n_times=n_snapshots,\n", - " nz=n_vertical_layers + 1,\n", - " ny=ny,\n", - " nx=nx,\n", - " bottom_depth=bottom_depth,\n", - " eta=wave,\n", - ")\n", - "ds" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now compare the evolution of the vertical grid in $\\sigma$ space to the vertical grid in z space. In z space, the vertical grid spacing is much smaller in the shallow west than in the deep east, this demonstrates the strength of a terrain following vertical grid. We can also see that the vertical grid has time dependency in z space. Currently, for structured grids, Parcels uses piecewise constant horizontal interpolation of z values for time-varying z." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "time = ds[\"time\"].values.astype(\"datetime64[s]\")\n", - "z_moving = ds[\"depth\"].values # interface depths, shape (time, depth, YC, XC)\n", - "sigma = np.linspace(0.0, 1.0, n_vertical_layers + 1)\n", - "\n", - "centre_row = ny // 2 # the row of cells in the middle of the domain\n", - "xs = x_centres / 1e3\n", - "\n", - "fig, (ax_sigma, ax_z) = plt.subplots(1, 2, figsize=(12, 4), sharex=True)\n", - "ax_sigma.plot(xs, np.broadcast_to(sigma, (xs.size, sigma.size)), \"k\", linewidth=0.8)\n", - "ax_sigma.set_ylabel(r\"$\\sigma$\")\n", - "ax_sigma.set_title(r\"Layer interfaces in $\\sigma$ space\")\n", - "\n", - "z_lines = ax_z.plot(\n", - " xs, z_moving[0][:, centre_row].T, \"k\", linewidth=0.8, drawstyle=\"steps-mid\"\n", - ")\n", - "ax_z.set_ylabel(\"depth [m]\")\n", - "z_title = ax_z.set_title(f\"Layer interfaces in z space at {time[0]}\")\n", - "\n", - "for ax in (ax_sigma, ax_z):\n", - " ax.invert_yaxis()\n", - " ax.set_xlabel(\"x [km]\")\n", - "plt.tight_layout()\n", - "\n", - "\n", - "def animate(snapshot):\n", - " for line, interface_depths in zip(\n", - " z_lines, z_moving[snapshot][:, centre_row], strict=True\n", - " ):\n", - " line.set_ydata(interface_depths)\n", - " z_title.set_text(f\"Layer interfaces in z space at {time[snapshot]}\")\n", - "\n", - "\n", - "anim = FuncAnimation(fig, animate, frames=range(0, time.size, 6), interval=300)\n", - "plt.close()\n", - "anim" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Building the `FieldSet`\n", - "\n", - "We prescribe a uniform velocity of $u_0 = 1$ m/s in the $x$ direction, with $v = w = 0$. All three components sit on the grid nodes at the layer interfaces (`depth`), so Parcels interpolates them linearly in all three directions.\n", - "\n", - "To advect the particles on a sigma grid, for which the z grid varies in time, we use a 4D `depth` coordinate with dimensions of `(time, depth, YC, XC)`. For each particle, Parcels takes the column of the cell that the particle is in, interpolates it linearly in time between the two surrounding snapshots, and then searches that column for the particle's layer. We pass `mesh=\"flat\"` because the coordinates are Cartesian meters." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "u0 = 1.0\n", - "ds[\"U\"].values[:] = u0\n", - "ds" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n", - "fieldset.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Release the particles\n", - "\n", - "We release 10 particles at the first snapshot near the shallow western end of the domain, at $x = -890$ km and $y = 0$, at depths from 60 m to 240 m. This puts the shallowest particle just below the deepest wave trough (50 m), so every particle will remain in the water column throughout the simulation." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_particles = 10\n", - "x0 = np.full(n_particles, -890e3)\n", - "y0 = np.zeros(n_particles)\n", - "z0 = np.linspace(60.0, 240.0, n_particles) # depths in m\n", - "\n", - "pset = parcels.ParticleSet(\n", - " fieldset=fieldset,\n", - " pclass=parcels.Particle,\n", - " t=np.full(n_particles, time[0]),\n", - " z=z0,\n", - " y=y0,\n", - " x=x0,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "H = z_moving[0, -1, centre_row]\n", - "eta = -z_moving[:, 0, centre_row]\n", - "cell = np.searchsorted(\n", - " lon, x0\n", - ") # index of the cell centre whose z column Parcels uses for each particle\n", - "sigma0 = (z0 + eta[0, cell]) / (\n", - " H[cell] + eta[0, cell]\n", - ") # initial sigma coordinate of each particle\n", - "\n", - "colors = [f\"C{p}\" for p in range(n_particles)]\n", - "\n", - "fig, (ax_sigma, ax_z) = plt.subplots(1, 2, figsize=(12, 4), sharex=True)\n", - "ax_sigma.plot(xs, np.broadcast_to(sigma, (xs.size, sigma.size)), \"k\", linewidth=0.8)\n", - "ax_sigma.scatter(x0 / 1e3, sigma0, color=colors, zorder=3)\n", - "ax_sigma.set_ylabel(r\"$\\sigma$\")\n", - "ax_sigma.set_title(r\"Initial particle positions in $\\sigma$ space\")\n", - "\n", - "ax_z.plot(xs, z_moving[0][:, centre_row].T, \"k\", linewidth=0.8, drawstyle=\"steps-mid\")\n", - "ax_z.scatter(x0 / 1e3, z0, color=colors, zorder=3)\n", - "ax_z.set_ylabel(\"depth [m]\")\n", - "ax_z.set_title(f\"Initial particle positions in z space at {time[0]}\")\n", - "\n", - "for ax in (ax_sigma, ax_z):\n", - " ax.invert_yaxis()\n", - " ax.set_xlabel(\"x [km]\")\n", - "plt.tight_layout()\n", - "plt.show()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Advect the particles\n", - "\n", - "We advect the particles for 10 days using the 2D second-order Runge-Kutta kernel {py:func}`parcels.kernels.AdvectionRK2` with a 30-minute time step, and write their positions every 6 hours. We chose the 2D version of RK2 since the vertical velocity is zero across the domain." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [ - "hide-output" - ] - }, - "outputs": [], - "source": [ - "outputfile = parcels.ParticleFile(\n", - " path=\"sigma_particles.parquet\", outputdt=np.timedelta64(6, \"h\"), mode=\"w\"\n", - ")\n", - "\n", - "pset.execute(\n", - " [parcels.kernels.AdvectionRK2],\n", - " runtime=np.timedelta64(n_days, \"D\"),\n", - " dt=np.timedelta64(30, \"m\"),\n", - " output_file=outputfile,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "df = parcels.read_particlefile(\"sigma_particles.parquet\").sort([\"t\", \"particle_id\"])\n", - "output_times = df[\"t\"].unique().sort()\n", - "n_frames = len(output_times)\n", - "px = df[\"x\"].to_numpy().reshape(n_frames, n_particles)\n", - "pz = df[\"z\"].to_numpy().reshape(n_frames, n_particles)\n", - "\n", - "# calculate the sigma coordinate of every particle at every output time, using the z column of its cell\n", - "snapshots = np.searchsorted(time, output_times.to_numpy(), side=\"right\") - 1\n", - "cells = np.searchsorted(lon, px)\n", - "eta_p = eta[snapshots[:, np.newaxis], cells]\n", - "psigma = (pz + eta_p) / (H[cells] + eta_p)\n", - "\n", - "colors = [f\"C{p}\" for p in range(n_particles)]\n", - "\n", - "fig, (ax_sigma, ax_z) = plt.subplots(1, 2, figsize=(12, 4), sharex=True)\n", - "ax_sigma.plot(xs, np.broadcast_to(sigma, (xs.size, sigma.size)), \"k\", linewidth=0.8)\n", - "z_lines = ax_z.plot(\n", - " xs, z_moving[0][:, centre_row].T, \"k\", linewidth=0.8, drawstyle=\"steps-mid\"\n", - ")\n", - "sigma_trails = [ax_sigma.plot([], [], color=c, linewidth=1.5)[0] for c in colors]\n", - "z_trails = [ax_z.plot([], [], color=c, linewidth=1.5)[0] for c in colors]\n", - "sigma_dots = ax_sigma.scatter(px[0] / 1e3, psigma[0], c=colors, zorder=3)\n", - "z_dots = ax_z.scatter(px[0] / 1e3, pz[0], c=colors, zorder=3)\n", - "\n", - "ax_sigma.set_ylabel(r\"$\\sigma$\")\n", - "ax_sigma.set_title(r\"Particles in $\\sigma$ space\")\n", - "ax_z.set_ylabel(\"depth [m]\")\n", - "z_title = ax_z.set_title(\"\")\n", - "for ax in (ax_sigma, ax_z):\n", - " ax.invert_yaxis()\n", - " ax.set_xlabel(\"x [km]\")\n", - "plt.tight_layout()\n", - "\n", - "\n", - "def animate(frame):\n", - " for p in range(n_particles):\n", - " sigma_trails[p].set_data(px[: frame + 1, p] / 1e3, psigma[: frame + 1, p])\n", - " z_trails[p].set_data(px[: frame + 1, p] / 1e3, pz[: frame + 1, p])\n", - " sigma_dots.set_offsets(np.c_[px[frame] / 1e3, psigma[frame]])\n", - " z_dots.set_offsets(np.c_[px[frame] / 1e3, pz[frame]])\n", - " for line, interface_depths in zip(\n", - " z_lines, z_moving[snapshots[frame]][:, centre_row], strict=True\n", - " ):\n", - " line.set_ydata(interface_depths)\n", - " z_title.set_text(f\"Particles in z space at {output_times[frame]}\")\n", - "\n", - "\n", - "animate(0)\n", - "anim = FuncAnimation(fig, animate, frames=n_frames, interval=300)\n", - "plt.close()\n", - "anim" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can see that, as expected, the particles move horizontally in x and cross the terrain following depth levels. However, in z space, they maintain their exact height as expected." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Sigma coordinates on unstructured grids\n", - "\n", - "The same approach works on unstructured grids. On an unstructured grid, z is a 3D `zf` coordinate with dimensions `(time, zf, n_node)`, defined on the corners (nodes) of the triangular faces, rather than a 4D `depth` coordinate with dimensions `(time, depth, YC, XC)` defined on the cell centres. For each particle, Parcels interpolates the z columns of the three corners of its triangle barycentrically in space and linearly in time, so on unstructured grids the z grid varies continuously in space instead of being constant across each cell.\n", - "\n", - "Below we build the same basin on a triangulated lattice with `sigma_coordinate_lattice_dataset`, evaluating the bathymetry and sea surface height at the lattice nodes, and compare the two z coordinates." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "n_lattice_nodes = (\n", - " 21 # the number of lattice nodes in x and y, so that the nodes are 100km apart\n", - ")\n", - "x_nodes = np.linspace(-1000e3, 1000e3, n_lattice_nodes)\n", - "\n", - "# the same bathymetry and surface wave as above, evaluated at the lattice nodes, with shapes (x, y) and (time, x, y)\n", - "bottom_depth_nodes = 100 + 900 * np.log10(\n", - " 1 + 9 * (x_nodes - x_nodes[0]) / domain_length\n", - ")\n", - "bottom_depth_nodes = np.broadcast_to(\n", - " bottom_depth_nodes[:, np.newaxis], (n_lattice_nodes, n_lattice_nodes)\n", - ")\n", - "wave_nodes = amplitude * np.sin(\n", - " 2.0 * np.pi * (x_nodes[np.newaxis, :] / wavelength - t[:, np.newaxis] / wave_period)\n", - ")\n", - "wave_nodes = np.broadcast_to(\n", - " wave_nodes[:, :, np.newaxis], (n_snapshots, n_lattice_nodes, n_lattice_nodes)\n", - ")\n", - "\n", - "ux_ds = sigma_coordinate_lattice_dataset(\n", - " n_lattice_nodes,\n", - " (-1000e3, 1000e3),\n", - " (-1000e3, 1000e3),\n", - " n_vertical_layers + 1,\n", - " bottom_depth_nodes,\n", - " eta=wave_nodes,\n", - ")\n", - "print(\"structured z: \", ds[\"depth\"].dims, ds[\"depth\"].shape)\n", - "print(\"unstructured z:\", ux_ds[\"zf\"].dims, ux_ds[\"zf\"].shape)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ux_ds[\"U\"].values[:] = u0\n", - "ux_fieldset = parcels.FieldSet.from_ugrid_conventions(ux_ds)\n", - "ux_fieldset.describe()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We release the same 10 particles at the same positions on the unstructured grid, and advect them with the same kernel, time step and output frequency as before." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "tags": [ - "hide-output" - ] - }, - "outputs": [], - "source": [ - "ux_pset = parcels.ParticleSet(\n", - " fieldset=ux_fieldset,\n", - " pclass=parcels.Particle,\n", - " t=np.full(n_particles, time[0]),\n", - " z=z0,\n", - " y=y0,\n", - " x=x0,\n", - ")\n", - "ux_outputfile = parcels.ParticleFile(\n", - " path=\"ux_sigma_particles.parquet\", outputdt=np.timedelta64(6, \"h\"), mode=\"w\"\n", - ")\n", - "\n", - "ux_pset.execute(\n", - " [parcels.kernels.AdvectionRK2],\n", - " runtime=np.timedelta64(n_days, \"D\"),\n", - " dt=np.timedelta64(30, \"m\"),\n", - " output_file=ux_outputfile,\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "ux_df = parcels.read_particlefile(\"ux_sigma_particles.parquet\").sort(\n", - " [\"t\", \"particle_id\"]\n", - ")\n", - "ux_px = ux_df[\"x\"].to_numpy().reshape(n_frames, n_particles)\n", - "ux_pz = ux_df[\"z\"].to_numpy().reshape(n_frames, n_particles)\n", - "\n", - "# the particles move along the row of lattice nodes at y = 0, i.e. along triangle edges, so the z column that\n", - "# Parcels uses for each particle is the linear interpolation in x of the z columns at the nodes of that row\n", - "row = np.isclose(ux_ds.uxgrid.node_lat.values, 0.0)\n", - "z_nodes = ux_ds[\"zf\"].values[\n", - " :, :, row\n", - "] # interface depths along y = 0, shape (time, zf, x)\n", - "H_nodes, eta_nodes = z_nodes[0, -1], -z_nodes[:, 0]\n", - "ux_psigma = np.empty_like(ux_pz)\n", - "for frame, snapshot in enumerate(snapshots):\n", - " ux_eta_p = np.interp(ux_px[frame], x_nodes, eta_nodes[snapshot])\n", - " ux_psigma[frame] = (ux_pz[frame] + ux_eta_p) / (\n", - " np.interp(ux_px[frame], x_nodes, H_nodes) + ux_eta_p\n", - " )\n", - "\n", - "fig, (ax_sigma, ax_z) = plt.subplots(1, 2, figsize=(12, 4), sharex=True)\n", - "ax_sigma.plot(\n", - " x_nodes / 1e3,\n", - " np.broadcast_to(sigma, (x_nodes.size, sigma.size)),\n", - " \"k\",\n", - " linewidth=0.8,\n", - ")\n", - "z_lines = ax_z.plot(x_nodes / 1e3, z_nodes[0].T, \"k\", linewidth=0.8)\n", - "sigma_trails = [ax_sigma.plot([], [], color=c, linewidth=1.5)[0] for c in colors]\n", - "z_trails = [ax_z.plot([], [], color=c, linewidth=1.5)[0] for c in colors]\n", - "sigma_dots = ax_sigma.scatter(ux_px[0] / 1e3, ux_psigma[0], c=colors, zorder=3)\n", - "z_dots = ax_z.scatter(ux_px[0] / 1e3, ux_pz[0], c=colors, zorder=3)\n", - "\n", - "ax_sigma.set_ylabel(r\"$\\sigma$\")\n", - "ax_sigma.set_title(r\"Particles in $\\sigma$ space on the unstructured grid\")\n", - "ax_z.set_ylabel(\"depth [m]\")\n", - "z_title = ax_z.set_title(\"\")\n", - "for ax in (ax_sigma, ax_z):\n", - " ax.invert_yaxis()\n", - " ax.set_xlabel(\"x [km]\")\n", - "plt.tight_layout()\n", - "\n", - "\n", - "def animate(frame):\n", - " for p in range(n_particles):\n", - " sigma_trails[p].set_data(ux_px[: frame + 1, p] / 1e3, ux_psigma[: frame + 1, p])\n", - " z_trails[p].set_data(ux_px[: frame + 1, p] / 1e3, ux_pz[: frame + 1, p])\n", - " sigma_dots.set_offsets(np.c_[ux_px[frame] / 1e3, ux_psigma[frame]])\n", - " z_dots.set_offsets(np.c_[ux_px[frame] / 1e3, ux_pz[frame]])\n", - " for line, interface_depths in zip(z_lines, z_nodes[snapshots[frame]]):\n", - " line.set_ydata(interface_depths)\n", - " z_title.set_text(f\"Particles in z space at {output_times[frame]}\")\n", - "\n", - "\n", - "animate(0)\n", - "anim = FuncAnimation(fig, animate, frames=n_frames, interval=300)\n", - "plt.close()\n", - "anim" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "As on the structured grid, the particles keep their exact height in z space while crossing the terrain following depth levels. On the unstructured grid, however, the layer interfaces vary continuously between the nodes rather than being constant across each grid cell." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Parcels:default (3.14.8)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.14.8" - }, - "mystnb": { - "execution_in_temp": true - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb index 40cc0e0075..91a51b9141 100644 --- a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb +++ b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb @@ -9,1185 +9,9 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "(function(root) {\n", - " function now() {\n", - " return new Date();\n", - " }\n", - "\n", - " const force = true;\n", - " const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", - " const reloading = false;\n", - " const Bokeh = root.Bokeh;\n", - " const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n", - " const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n", - "\n", - " // Set a timeout for this load but only if we are not already initializing\n", - " if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n", - " root._bokeh_timeout = Date.now() + 5000;\n", - " root._bokeh_failed_load = false;\n", - " }\n", - "\n", - " function run_callbacks() {\n", - " try {\n", - " root._bokeh_onload_callbacks.forEach(function(callback) {\n", - " if (callback != null)\n", - " callback();\n", - " });\n", - " } finally {\n", - " delete root._bokeh_onload_callbacks;\n", - " }\n", - " console.debug(\"Bokeh: all callbacks have finished\");\n", - " }\n", - "\n", - " function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n", - " if (css_urls == null) css_urls = [];\n", - " if (js_urls == null) js_urls = [];\n", - " if (js_modules == null) js_modules = [];\n", - " if (js_exports == null) js_exports = {};\n", - "\n", - " root._bokeh_onload_callbacks.push(callback);\n", - "\n", - " if (root._bokeh_is_loading > 0) {\n", - " // Don't load bokeh if it is still initializing\n", - " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", - " return null;\n", - " } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", - " // There is nothing to load\n", - " run_callbacks();\n", - " return null;\n", - " }\n", - "\n", - " function on_load() {\n", - " root._bokeh_is_loading--;\n", - " if (root._bokeh_is_loading === 0) {\n", - " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", - " run_callbacks()\n", - " }\n", - " }\n", - " window._bokeh_on_load = on_load\n", - "\n", - " function on_error(e) {\n", - " const src_el = e.srcElement\n", - " console.error(\"failed to load \" + (src_el.href || src_el.src));\n", - " }\n", - "\n", - " const skip = [];\n", - " if (window.requirejs) {\n", - " window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n", - " root._bokeh_is_loading = css_urls.length + 0;\n", - " } else {\n", - " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", - " }\n", - "\n", - " const existing_stylesheets = []\n", - " const links = document.getElementsByTagName('link')\n", - " for (let i = 0; i < links.length; i++) {\n", - " const link = links[i]\n", - " if (link.href != null) {\n", - " existing_stylesheets.push(link.href)\n", - " }\n", - " }\n", - " for (let i = 0; i < css_urls.length; i++) {\n", - " const url = css_urls[i];\n", - " const escaped = encodeURI(url)\n", - " if (existing_stylesheets.indexOf(escaped) !== -1) {\n", - " on_load()\n", - " continue;\n", - " }\n", - " const element = document.createElement(\"link\");\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.rel = \"stylesheet\";\n", - " element.type = \"text/css\";\n", - " element.href = url;\n", - " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", - " document.body.appendChild(element);\n", - " } var existing_scripts = []\n", - " const scripts = document.getElementsByTagName('script')\n", - " for (let i = 0; i < scripts.length; i++) {\n", - " var script = scripts[i]\n", - " if (script.src != null) {\n", - " existing_scripts.push(script.src)\n", - " }\n", - " }\n", - " for (let i = 0; i < js_urls.length; i++) {\n", - " const url = js_urls[i];\n", - " const escaped = encodeURI(url)\n", - " const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n", - " const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n", - " const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n", - " if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n", - " if (!window.requirejs) {\n", - " on_load();\n", - " }\n", - " continue;\n", - " }\n", - " const element = document.createElement('script');\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.src = url;\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.head.appendChild(element);\n", - " }\n", - " for (let i = 0; i < js_modules.length; i++) {\n", - " const url = js_modules[i];\n", - " const escaped = encodeURI(url)\n", - " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", - " if (!window.requirejs) {\n", - " on_load();\n", - " }\n", - " continue;\n", - " }\n", - " var element = document.createElement('script');\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.src = url;\n", - " element.type = \"module\";\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.head.appendChild(element);\n", - " }\n", - " for (const name in js_exports) {\n", - " const url = js_exports[name];\n", - " const escaped = encodeURI(url)\n", - " if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n", - " if (!window.requirejs) {\n", - " on_load();\n", - " }\n", - " continue;\n", - " }\n", - " var element = document.createElement('script');\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.type = \"module\";\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " element.textContent = `\n", - " import ${name} from \"${url}\"\n", - " window.${name} = ${name}\n", - " window._bokeh_on_load()\n", - " `\n", - " document.head.appendChild(element);\n", - " }\n", - " if (!js_urls.length && !js_modules.length) {\n", - " on_load()\n", - " }\n", - " };\n", - "\n", - " function inject_raw_css(css) {\n", - " const element = document.createElement(\"style\");\n", - " element.appendChild(document.createTextNode(css));\n", - " document.body.appendChild(element);\n", - " }\n", - "\n", - " const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/dist/panel.min.js\"];\n", - " const js_modules = [];\n", - " const js_exports = {};\n", - " const css_urls = [];\n", - " const inline_js = [ function(Bokeh) {\n", - " Bokeh.set_log_level(\"info\");\n", - " },\n", - "function(Bokeh) {} // ensure no trailing comma for IE\n", - " ];\n", - "\n", - " function run_inline_js() {\n", - " if ((root.Bokeh !== undefined) || (force === true)) {\n", - " for (let i = 0; i < inline_js.length; i++) {\n", - " try {\n", - " inline_js[i].call(root, root.Bokeh);\n", - " } catch(e) {\n", - " if (!reloading) {\n", - " throw e;\n", - " }\n", - " }\n", - " }\n", - " } else if (Date.now() < root._bokeh_timeout) {\n", - " setTimeout(run_inline_js, 100);\n", - " } else if (!root._bokeh_failed_load) {\n", - " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", - " root._bokeh_failed_load = true;\n", - " }\n", - " root._bokeh_is_initializing = false;\n", - " }\n", - "\n", - " function load_or_wait() {\n", - " // Implement a backoff loop that tries to ensure we do not load multiple\n", - " // versions of Bokeh and its dependencies at the same time.\n", - " // In recent versions we use the root._bokeh_is_initializing flag\n", - " // to determine whether there is an ongoing attempt to initialize\n", - " // bokeh, however for backward compatibility we also try to ensure\n", - " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", - " // before older versions are fully initialized.\n", - " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", - " // If the timeout and bokeh was not successfully loaded we reset\n", - " // everything and try loading again\n", - " root._bokeh_timeout = Date.now() + 5000;\n", - " root._bokeh_is_initializing = false;\n", - " root._bokeh_onload_callbacks = undefined;\n", - " root._bokeh_is_loading = 0;\n", - " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", - " load_or_wait();\n", - " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", - " setTimeout(load_or_wait, 100);\n", - " } else {\n", - " root._bokeh_is_initializing = true;\n", - " root._bokeh_onload_callbacks = [];\n", - " const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n", - " if (!reloading && !bokeh_loaded) {\n", - " if (root.Bokeh) {\n", - " root.Bokeh = undefined;\n", - " }\n", - " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", - " }\n", - " load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n", - " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", - " run_inline_js();\n", - " if (Bokeh != undefined && !reloading) {\n", - " const NewBokeh = root.Bokeh;\n", - " if (Bokeh.versions === undefined) {\n", - " Bokeh.versions = new Map();\n", - " }\n", - " if (NewBokeh.version !== Bokeh.version) {\n", - " Bokeh[NewBokeh.version] = NewBokeh;\n", - " Bokeh.versions.set(NewBokeh.version, NewBokeh);\n", - " }\n", - " root.Bokeh = Bokeh;\n", - " }\n", - " });\n", - " }\n", - " }\n", - " // Give older versions of the autoload script a head-start to ensure\n", - " // they initialize before we start loading newer version.\n", - " setTimeout(load_or_wait, 100)\n", - "}(window));" - ], - "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = false;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.9.1.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.9.1.min.js\", \"https://cdn.holoviz.org/panel/1.9.3/dist/panel.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "\n", - "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", - " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", - "}\n", - "\n", - "\n", - " function JupyterCommManager() {\n", - " }\n", - "\n", - " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", - " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", - " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", - " comm_manager.register_target(comm_id, function(comm) {\n", - " comm.on_msg(msg_handler);\n", - " });\n", - " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", - " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", - " comm.onMsg = msg_handler;\n", - " });\n", - " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", - " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", - " var messages = comm.messages[Symbol.asyncIterator]();\n", - " function processIteratorResult(result) {\n", - " var message = result.value;\n", - " var content = {data: message.data, comm_id};\n", - " var buffers = []\n", - " for (var buffer of message.buffers || []) {\n", - " buffers.push(new DataView(buffer))\n", - " }\n", - " var metadata = message.metadata || {};\n", - " var msg = {content, buffers, metadata}\n", - " msg_handler(msg);\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " return messages.next().then(processIteratorResult);\n", - " })\n", - " }\n", - " }\n", - "\n", - " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", - " if (comm_id in window.PyViz.comms) {\n", - " return window.PyViz.comms[comm_id];\n", - " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", - " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", - " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", - " if (msg_handler) {\n", - " comm.on_msg(msg_handler);\n", - " }\n", - " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", - " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", - " let retries = 0;\n", - " const open = () => {\n", - " if (comm.active) {\n", - " comm.open();\n", - " } else if (retries > 3) {\n", - " console.warn('Comm target never activated')\n", - " } else {\n", - " retries += 1\n", - " setTimeout(open, 500)\n", - " }\n", - " }\n", - " if (comm.active) {\n", - " comm.open();\n", - " } else {\n", - " setTimeout(open, 500)\n", - " }\n", - " if (msg_handler) {\n", - " comm.onMsg = msg_handler;\n", - " }\n", - " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", - " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", - " comm_promise.then((comm) => {\n", - " window.PyViz.comms[comm_id] = comm;\n", - " if (msg_handler) {\n", - " var messages = comm.messages[Symbol.asyncIterator]();\n", - " function processIteratorResult(result) {\n", - " var message = result.value;\n", - " var content = {data: message.data};\n", - " var metadata = message.metadata || {comm_id};\n", - " var msg = {content, metadata}\n", - " msg_handler(msg);\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " })\n", - " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", - " return comm_promise.then((comm) => {\n", - " comm.send(data, metadata, buffers, disposeOnDone);\n", - " });\n", - " };\n", - " var comm = {\n", - " send: sendClosure\n", - " };\n", - " }\n", - " window.PyViz.comms[comm_id] = comm;\n", - " return comm;\n", - " }\n", - " window.PyViz.comm_manager = new JupyterCommManager();\n", - " \n", - "\n", - "\n", - "var JS_MIME_TYPE = 'application/javascript';\n", - "var HTML_MIME_TYPE = 'text/html';\n", - "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", - "var CLASS_NAME = 'output';\n", - "\n", - "/**\n", - " * Render data to the DOM node\n", - " */\n", - "function render(props, node) {\n", - " var div = document.createElement(\"div\");\n", - " var script = document.createElement(\"script\");\n", - " node.appendChild(div);\n", - " node.appendChild(script);\n", - "}\n", - "\n", - "/**\n", - " * Handle when a new output is added\n", - " */\n", - "function handle_add_output(event, handle) {\n", - " var output_area = handle.output_area;\n", - " var output = handle.output;\n", - " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", - " return\n", - " }\n", - " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", - " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", - " if (id !== undefined) {\n", - " var nchildren = toinsert.length;\n", - " var html_node = toinsert[nchildren-1].children[0];\n", - " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", - " var scripts = [];\n", - " var nodelist = html_node.querySelectorAll(\"script\");\n", - " for (var i in nodelist) {\n", - " if (nodelist.hasOwnProperty(i)) {\n", - " scripts.push(nodelist[i])\n", - " }\n", - " }\n", - "\n", - " scripts.forEach( function (oldScript) {\n", - " var newScript = document.createElement(\"script\");\n", - " var attrs = [];\n", - " var nodemap = oldScript.attributes;\n", - " for (var j in nodemap) {\n", - " if (nodemap.hasOwnProperty(j)) {\n", - " attrs.push(nodemap[j])\n", - " }\n", - " }\n", - " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", - " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", - " oldScript.parentNode.replaceChild(newScript, oldScript);\n", - " });\n", - " if (JS_MIME_TYPE in output.data) {\n", - " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", - " }\n", - " output_area._hv_plot_id = id;\n", - " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", - " window.PyViz.plot_index[id] = Bokeh.index[id];\n", - " } else {\n", - " window.PyViz.plot_index[id] = null;\n", - " }\n", - " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", - " var bk_div = document.createElement(\"div\");\n", - " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", - " var script_attrs = bk_div.children[0].attributes;\n", - " for (var i = 0; i < script_attrs.length; i++) {\n", - " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", - " }\n", - " // store reference to server id on output_area\n", - " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", - " }\n", - "}\n", - "\n", - "/**\n", - " * Handle when an output is cleared or removed\n", - " */\n", - "function handle_clear_output(event, handle) {\n", - " var id = handle.cell.output_area._hv_plot_id;\n", - " var server_id = handle.cell.output_area._bokeh_server_id;\n", - " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", - " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", - " if (server_id !== null) {\n", - " comm.send({event_type: 'server_delete', 'id': server_id});\n", - " return;\n", - " } else if (comm !== null) {\n", - " comm.send({event_type: 'delete', 'id': id});\n", - " }\n", - " delete PyViz.plot_index[id];\n", - " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", - " var doc = window.Bokeh.index[id].model.document\n", - " doc.clear();\n", - " const i = window.Bokeh.documents.indexOf(doc);\n", - " if (i > -1) {\n", - " window.Bokeh.documents.splice(i, 1);\n", - " }\n", - " }\n", - "}\n", - "\n", - "/**\n", - " * Handle kernel restart event\n", - " */\n", - "function handle_kernel_cleanup(event, handle) {\n", - " delete PyViz.comms[\"hv-extension-comm\"];\n", - " window.PyViz.plot_index = {}\n", - "}\n", - "\n", - "/**\n", - " * Handle update_display_data messages\n", - " */\n", - "function handle_update_output(event, handle) {\n", - " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", - " handle_add_output(event, handle)\n", - "}\n", - "\n", - "function register_renderer(events, OutputArea) {\n", - " function append_mime(data, metadata, element) {\n", - " // create a DOM node to render to\n", - " var toinsert = this.create_output_subarea(\n", - " metadata,\n", - " CLASS_NAME,\n", - " EXEC_MIME_TYPE\n", - " );\n", - " this.keyboard_manager.register_events(toinsert);\n", - " // Render to node\n", - " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", - " render(props, toinsert[0]);\n", - " element.append(toinsert);\n", - " return toinsert\n", - " }\n", - "\n", - " events.on('output_added.OutputArea', handle_add_output);\n", - " events.on('output_updated.OutputArea', handle_update_output);\n", - " events.on('clear_output.CodeCell', handle_clear_output);\n", - " events.on('delete.Cell', handle_clear_output);\n", - " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", - "\n", - " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", - " safe: true,\n", - " index: 0\n", - " });\n", - "}\n", - "\n", - "if (window.Jupyter !== undefined) {\n", - " try {\n", - " var events = require('base/js/events');\n", - " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", - " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", - " register_renderer(events, OutputArea);\n", - " }\n", - " } catch(err) {\n", - " }\n", - "}\n" - ], - "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "77e5196f-acf9-4a4b-a41b-5636b9953aca" - } - }, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "(function(root) {\n", - " function now() {\n", - " return new Date();\n", - " }\n", - "\n", - " const force = false;\n", - " const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", - " const reloading = true;\n", - " const Bokeh = root.Bokeh;\n", - " const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n", - " const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n", - "\n", - " // Set a timeout for this load but only if we are not already initializing\n", - " if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n", - " root._bokeh_timeout = Date.now() + 5000;\n", - " root._bokeh_failed_load = false;\n", - " }\n", - "\n", - " function run_callbacks() {\n", - " try {\n", - " root._bokeh_onload_callbacks.forEach(function(callback) {\n", - " if (callback != null)\n", - " callback();\n", - " });\n", - " } finally {\n", - " delete root._bokeh_onload_callbacks;\n", - " }\n", - " console.debug(\"Bokeh: all callbacks have finished\");\n", - " }\n", - "\n", - " function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n", - " if (css_urls == null) css_urls = [];\n", - " if (js_urls == null) js_urls = [];\n", - " if (js_modules == null) js_modules = [];\n", - " if (js_exports == null) js_exports = {};\n", - "\n", - " root._bokeh_onload_callbacks.push(callback);\n", - "\n", - " if (root._bokeh_is_loading > 0) {\n", - " // Don't load bokeh if it is still initializing\n", - " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", - " return null;\n", - " } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", - " // There is nothing to load\n", - " run_callbacks();\n", - " return null;\n", - " }\n", - "\n", - " function on_load() {\n", - " root._bokeh_is_loading--;\n", - " if (root._bokeh_is_loading === 0) {\n", - " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", - " run_callbacks()\n", - " }\n", - " }\n", - " window._bokeh_on_load = on_load\n", - "\n", - " function on_error(e) {\n", - " const src_el = e.srcElement\n", - " console.error(\"failed to load \" + (src_el.href || src_el.src));\n", - " }\n", - "\n", - " const skip = [];\n", - " if (window.requirejs) {\n", - " window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n", - " root._bokeh_is_loading = css_urls.length + 0;\n", - " } else {\n", - " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", - " }\n", - "\n", - " const existing_stylesheets = []\n", - " const links = document.getElementsByTagName('link')\n", - " for (let i = 0; i < links.length; i++) {\n", - " const link = links[i]\n", - " if (link.href != null) {\n", - " existing_stylesheets.push(link.href)\n", - " }\n", - " }\n", - " for (let i = 0; i < css_urls.length; i++) {\n", - " const url = css_urls[i];\n", - " const escaped = encodeURI(url)\n", - " if (existing_stylesheets.indexOf(escaped) !== -1) {\n", - " on_load()\n", - " continue;\n", - " }\n", - " const element = document.createElement(\"link\");\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.rel = \"stylesheet\";\n", - " element.type = \"text/css\";\n", - " element.href = url;\n", - " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", - " document.body.appendChild(element);\n", - " } var existing_scripts = []\n", - " const scripts = document.getElementsByTagName('script')\n", - " for (let i = 0; i < scripts.length; i++) {\n", - " var script = scripts[i]\n", - " if (script.src != null) {\n", - " existing_scripts.push(script.src)\n", - " }\n", - " }\n", - " for (let i = 0; i < js_urls.length; i++) {\n", - " const url = js_urls[i];\n", - " const escaped = encodeURI(url)\n", - " const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n", - " const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n", - " const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n", - " if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n", - " if (!window.requirejs) {\n", - " on_load();\n", - " }\n", - " continue;\n", - " }\n", - " const element = document.createElement('script');\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.src = url;\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.head.appendChild(element);\n", - " }\n", - " for (let i = 0; i < js_modules.length; i++) {\n", - " const url = js_modules[i];\n", - " const escaped = encodeURI(url)\n", - " if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n", - " if (!window.requirejs) {\n", - " on_load();\n", - " }\n", - " continue;\n", - " }\n", - " var element = document.createElement('script');\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.src = url;\n", - " element.type = \"module\";\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.head.appendChild(element);\n", - " }\n", - " for (const name in js_exports) {\n", - " const url = js_exports[name];\n", - " const escaped = encodeURI(url)\n", - " if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n", - " if (!window.requirejs) {\n", - " on_load();\n", - " }\n", - " continue;\n", - " }\n", - " var element = document.createElement('script');\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.type = \"module\";\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " element.textContent = `\n", - " import ${name} from \"${url}\"\n", - " window.${name} = ${name}\n", - " window._bokeh_on_load()\n", - " `\n", - " document.head.appendChild(element);\n", - " }\n", - " if (!js_urls.length && !js_modules.length) {\n", - " on_load()\n", - " }\n", - " };\n", - "\n", - " function inject_raw_css(css) {\n", - " const element = document.createElement(\"style\");\n", - " element.appendChild(document.createTextNode(css));\n", - " document.body.appendChild(element);\n", - " }\n", - "\n", - " const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n", - " const js_modules = [];\n", - " const js_exports = {};\n", - " const css_urls = [];\n", - " const inline_js = [ function(Bokeh) {\n", - " Bokeh.set_log_level(\"info\");\n", - " },\n", - "function(Bokeh) {} // ensure no trailing comma for IE\n", - " ];\n", - "\n", - " function run_inline_js() {\n", - " if ((root.Bokeh !== undefined) || (force === true)) {\n", - " for (let i = 0; i < inline_js.length; i++) {\n", - " try {\n", - " inline_js[i].call(root, root.Bokeh);\n", - " } catch(e) {\n", - " if (!reloading) {\n", - " throw e;\n", - " }\n", - " }\n", - " }\n", - " } else if (Date.now() < root._bokeh_timeout) {\n", - " setTimeout(run_inline_js, 100);\n", - " } else if (!root._bokeh_failed_load) {\n", - " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", - " root._bokeh_failed_load = true;\n", - " }\n", - " root._bokeh_is_initializing = false;\n", - " }\n", - "\n", - " function load_or_wait() {\n", - " // Implement a backoff loop that tries to ensure we do not load multiple\n", - " // versions of Bokeh and its dependencies at the same time.\n", - " // In recent versions we use the root._bokeh_is_initializing flag\n", - " // to determine whether there is an ongoing attempt to initialize\n", - " // bokeh, however for backward compatibility we also try to ensure\n", - " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", - " // before older versions are fully initialized.\n", - " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", - " // If the timeout and bokeh was not successfully loaded we reset\n", - " // everything and try loading again\n", - " root._bokeh_timeout = Date.now() + 5000;\n", - " root._bokeh_is_initializing = false;\n", - " root._bokeh_onload_callbacks = undefined;\n", - " root._bokeh_is_loading = 0;\n", - " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", - " load_or_wait();\n", - " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", - " setTimeout(load_or_wait, 100);\n", - " } else {\n", - " root._bokeh_is_initializing = true;\n", - " root._bokeh_onload_callbacks = [];\n", - " const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n", - " if (!reloading && !bokeh_loaded) {\n", - " if (root.Bokeh) {\n", - " root.Bokeh = undefined;\n", - " }\n", - " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", - " }\n", - " load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n", - " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", - " run_inline_js();\n", - " if (Bokeh != undefined && !reloading) {\n", - " const NewBokeh = root.Bokeh;\n", - " if (Bokeh.versions === undefined) {\n", - " Bokeh.versions = new Map();\n", - " }\n", - " if (NewBokeh.version !== Bokeh.version) {\n", - " Bokeh[NewBokeh.version] = NewBokeh;\n", - " Bokeh.versions.set(NewBokeh.version, NewBokeh);\n", - " }\n", - " root.Bokeh = Bokeh;\n", - " }\n", - " });\n", - " }\n", - " }\n", - " // Give older versions of the autoload script a head-start to ensure\n", - " // they initialize before we start loading newer version.\n", - " setTimeout(load_or_wait, 100)\n", - "}(window));" - ], - "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = false;\n const version = '3.9.1'.replace('rc', '-rc.').replace('.dev', '-dev.');\n const reloading = true;\n const Bokeh = root.Bokeh;\n const BK_RE = /^https:\\/\\/cdn\\.bokeh\\.org\\/bokeh\\/(release|dev)\\/bokeh-/;\n const PN_RE = /^https:\\/\\/cdn\\.holoviz\\.org\\/panel\\/[^/]+\\/dist\\/panel/i;\n\n // Set a timeout for this load but only if we are not already initializing\n if (typeof (root._bokeh_timeout) === \"undefined\" || (force || !root._bokeh_is_initializing)) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n // Don't load bokeh if it is still initializing\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n } else if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n // There is nothing to load\n run_callbacks();\n return null;\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error(e) {\n const src_el = e.srcElement\n console.error(\"failed to load \" + (src_el.href || src_el.src));\n }\n\n const skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {}, 'shim': {}});\n root._bokeh_is_loading = css_urls.length + 0;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n const existing_stylesheets = []\n const links = document.getElementsByTagName('link')\n for (let i = 0; i < links.length; i++) {\n const link = links[i]\n if (link.href != null) {\n existing_stylesheets.push(link.href)\n }\n }\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const escaped = encodeURI(url)\n if (existing_stylesheets.indexOf(escaped) !== -1) {\n on_load()\n continue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } var existing_scripts = []\n const scripts = document.getElementsByTagName('script')\n for (let i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n existing_scripts.push(script.src)\n }\n }\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const escaped = encodeURI(url)\n const shouldSkip = skip.includes(escaped) || existing_scripts.includes(escaped)\n const isBokehOrPanel = BK_RE.test(escaped) || PN_RE.test(escaped)\n const missingOrBroken = Bokeh == null || Bokeh.Panel == null || (Bokeh.version != version && !Bokeh.versions?.has(version)) || Bokeh.versions?.get(version)?.Panel == null;\n if (shouldSkip && !(isBokehOrPanel && missingOrBroken)) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (let i = 0; i < js_modules.length; i++) {\n const url = js_modules[i];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) !== -1 || existing_scripts.indexOf(escaped) !== -1) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n const url = js_exports[name];\n const escaped = encodeURI(url)\n if (skip.indexOf(escaped) >= 0 || root[name] != null) {\n if (!window.requirejs) {\n on_load();\n }\n continue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.holoviz.org/panel/1.9.3/dist/bundled/reactiveesm/es-module-shims@^1.10.0/dist/es-module-shims.min.js\"];\n const js_modules = [];\n const js_exports = {};\n const css_urls = [];\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (let i = 0; i < inline_js.length; i++) {\n try {\n inline_js[i].call(root, root.Bokeh);\n } catch(e) {\n if (!reloading) {\n throw e;\n }\n }\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false;\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n // If the timeout and bokeh was not successfully loaded we reset\n // everything and try loading again\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n root._bokeh_is_loading = 0;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true;\n root._bokeh_onload_callbacks = [];\n const bokeh_loaded = Bokeh != null && ((Bokeh.version === version && Bokeh.Panel) || (Bokeh.versions?.has(version) && Bokeh.versions.get(version)?.Panel));\n if (!reloading && !bokeh_loaded) {\n if (root.Bokeh) {\n root.Bokeh = undefined;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, Bokeh, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n if (Bokeh != undefined && !reloading) {\n const NewBokeh = root.Bokeh;\n if (Bokeh.versions === undefined) {\n Bokeh.versions = new Map();\n }\n if (NewBokeh.version !== Bokeh.version) {\n Bokeh[NewBokeh.version] = NewBokeh;\n Bokeh.versions.set(NewBokeh.version, NewBokeh);\n }\n root.Bokeh = Bokeh;\n }\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "\n", - "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", - " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", - "}\n", - "\n", - "\n", - " function JupyterCommManager() {\n", - " }\n", - "\n", - " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", - " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", - " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", - " comm_manager.register_target(comm_id, function(comm) {\n", - " comm.on_msg(msg_handler);\n", - " });\n", - " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", - " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", - " comm.onMsg = msg_handler;\n", - " });\n", - " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", - " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", - " var messages = comm.messages[Symbol.asyncIterator]();\n", - " function processIteratorResult(result) {\n", - " var message = result.value;\n", - " var content = {data: message.data, comm_id};\n", - " var buffers = []\n", - " for (var buffer of message.buffers || []) {\n", - " buffers.push(new DataView(buffer))\n", - " }\n", - " var metadata = message.metadata || {};\n", - " var msg = {content, buffers, metadata}\n", - " msg_handler(msg);\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " return messages.next().then(processIteratorResult);\n", - " })\n", - " }\n", - " }\n", - "\n", - " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", - " if (comm_id in window.PyViz.comms) {\n", - " return window.PyViz.comms[comm_id];\n", - " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", - " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", - " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", - " if (msg_handler) {\n", - " comm.on_msg(msg_handler);\n", - " }\n", - " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", - " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", - " let retries = 0;\n", - " const open = () => {\n", - " if (comm.active) {\n", - " comm.open();\n", - " } else if (retries > 3) {\n", - " console.warn('Comm target never activated')\n", - " } else {\n", - " retries += 1\n", - " setTimeout(open, 500)\n", - " }\n", - " }\n", - " if (comm.active) {\n", - " comm.open();\n", - " } else {\n", - " setTimeout(open, 500)\n", - " }\n", - " if (msg_handler) {\n", - " comm.onMsg = msg_handler;\n", - " }\n", - " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", - " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", - " comm_promise.then((comm) => {\n", - " window.PyViz.comms[comm_id] = comm;\n", - " if (msg_handler) {\n", - " var messages = comm.messages[Symbol.asyncIterator]();\n", - " function processIteratorResult(result) {\n", - " var message = result.value;\n", - " var content = {data: message.data};\n", - " var metadata = message.metadata || {comm_id};\n", - " var msg = {content, metadata}\n", - " msg_handler(msg);\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " })\n", - " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", - " return comm_promise.then((comm) => {\n", - " comm.send(data, metadata, buffers, disposeOnDone);\n", - " });\n", - " };\n", - " var comm = {\n", - " send: sendClosure\n", - " };\n", - " }\n", - " window.PyViz.comms[comm_id] = comm;\n", - " return comm;\n", - " }\n", - " window.PyViz.comm_manager = new JupyterCommManager();\n", - " \n", - "\n", - "\n", - "var JS_MIME_TYPE = 'application/javascript';\n", - "var HTML_MIME_TYPE = 'text/html';\n", - "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", - "var CLASS_NAME = 'output';\n", - "\n", - "/**\n", - " * Render data to the DOM node\n", - " */\n", - "function render(props, node) {\n", - " var div = document.createElement(\"div\");\n", - " var script = document.createElement(\"script\");\n", - " node.appendChild(div);\n", - " node.appendChild(script);\n", - "}\n", - "\n", - "/**\n", - " * Handle when a new output is added\n", - " */\n", - "function handle_add_output(event, handle) {\n", - " var output_area = handle.output_area;\n", - " var output = handle.output;\n", - " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", - " return\n", - " }\n", - " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", - " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", - " if (id !== undefined) {\n", - " var nchildren = toinsert.length;\n", - " var html_node = toinsert[nchildren-1].children[0];\n", - " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", - " var scripts = [];\n", - " var nodelist = html_node.querySelectorAll(\"script\");\n", - " for (var i in nodelist) {\n", - " if (nodelist.hasOwnProperty(i)) {\n", - " scripts.push(nodelist[i])\n", - " }\n", - " }\n", - "\n", - " scripts.forEach( function (oldScript) {\n", - " var newScript = document.createElement(\"script\");\n", - " var attrs = [];\n", - " var nodemap = oldScript.attributes;\n", - " for (var j in nodemap) {\n", - " if (nodemap.hasOwnProperty(j)) {\n", - " attrs.push(nodemap[j])\n", - " }\n", - " }\n", - " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", - " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", - " oldScript.parentNode.replaceChild(newScript, oldScript);\n", - " });\n", - " if (JS_MIME_TYPE in output.data) {\n", - " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", - " }\n", - " output_area._hv_plot_id = id;\n", - " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", - " window.PyViz.plot_index[id] = Bokeh.index[id];\n", - " } else {\n", - " window.PyViz.plot_index[id] = null;\n", - " }\n", - " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", - " var bk_div = document.createElement(\"div\");\n", - " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", - " var script_attrs = bk_div.children[0].attributes;\n", - " for (var i = 0; i < script_attrs.length; i++) {\n", - " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", - " }\n", - " // store reference to server id on output_area\n", - " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", - " }\n", - "}\n", - "\n", - "/**\n", - " * Handle when an output is cleared or removed\n", - " */\n", - "function handle_clear_output(event, handle) {\n", - " var id = handle.cell.output_area._hv_plot_id;\n", - " var server_id = handle.cell.output_area._bokeh_server_id;\n", - " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", - " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", - " if (server_id !== null) {\n", - " comm.send({event_type: 'server_delete', 'id': server_id});\n", - " return;\n", - " } else if (comm !== null) {\n", - " comm.send({event_type: 'delete', 'id': id});\n", - " }\n", - " delete PyViz.plot_index[id];\n", - " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", - " var doc = window.Bokeh.index[id].model.document\n", - " doc.clear();\n", - " const i = window.Bokeh.documents.indexOf(doc);\n", - " if (i > -1) {\n", - " window.Bokeh.documents.splice(i, 1);\n", - " }\n", - " }\n", - "}\n", - "\n", - "/**\n", - " * Handle kernel restart event\n", - " */\n", - "function handle_kernel_cleanup(event, handle) {\n", - " delete PyViz.comms[\"hv-extension-comm\"];\n", - " window.PyViz.plot_index = {}\n", - "}\n", - "\n", - "/**\n", - " * Handle update_display_data messages\n", - " */\n", - "function handle_update_output(event, handle) {\n", - " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", - " handle_add_output(event, handle)\n", - "}\n", - "\n", - "function register_renderer(events, OutputArea) {\n", - " function append_mime(data, metadata, element) {\n", - " // create a DOM node to render to\n", - " var toinsert = this.create_output_subarea(\n", - " metadata,\n", - " CLASS_NAME,\n", - " EXEC_MIME_TYPE\n", - " );\n", - " this.keyboard_manager.register_events(toinsert);\n", - " // Render to node\n", - " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", - " render(props, toinsert[0]);\n", - " element.append(toinsert);\n", - " return toinsert\n", - " }\n", - "\n", - " events.on('output_added.OutputArea', handle_add_output);\n", - " events.on('output_updated.OutputArea', handle_update_output);\n", - " events.on('clear_output.CodeCell', handle_clear_output);\n", - " events.on('delete.Cell', handle_clear_output);\n", - " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", - "\n", - " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", - " safe: true,\n", - " index: 0\n", - " });\n", - "}\n", - "\n", - "if (window.Jupyter !== undefined) {\n", - " try {\n", - " var events = require('base/js/events');\n", - " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", - " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", - " register_renderer(events, OutputArea);\n", - " }\n", - " } catch(err) {\n", - " }\n", - "}\n" - ], - "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n let retries = 0;\n const open = () => {\n if (comm.active) {\n comm.open();\n } else if (retries > 3) {\n console.warn('Comm target never activated')\n } else {\n retries += 1\n setTimeout(open, 500)\n }\n }\n if (comm.active) {\n comm.open();\n } else {\n setTimeout(open, 500)\n }\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n })\n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "import numpy as np\n", @@ -1214,787 +38,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 34MB\n",
-       "Dimensions:  (time: 241, ZF: 11, YG: 5, XG: 81, YC: 5, XC: 81)\n",
-       "Coordinates:\n",
-       "  * time     (time) datetime64[ns] 2kB 2000-01-01 ... 2000-01-11\n",
-       "  * ZF       (ZF) float64 88B 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0\n",
-       "  * YG       (YG) int64 40B 0 1 2 3 4\n",
-       "    lat      (YG) float64 40B -1e+06 -5e+05 0.0 5e+05 1e+06\n",
-       "  * XG       (XG) int64 648B 0 1 2 3 4 5 6 7 8 9 ... 72 73 74 75 76 77 78 79 80\n",
-       "    lon      (XG) float64 648B -1e+06 -9.75e+05 -9.5e+05 ... 9.75e+05 1e+06\n",
-       "  * YC       (YC) float64 40B 0.5 1.5 2.5 3.5 4.5\n",
-       "  * XC       (XC) float64 648B 0.5 1.5 2.5 3.5 4.5 ... 76.5 77.5 78.5 79.5 80.5\n",
-       "    depth    (time, ZF, YC, XC) float64 9MB 3.923 -3.923 -11.67 ... 993.3 997.8\n",
-       "Data variables:\n",
-       "    U        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
-       "    V        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
-       "    grid     int64 8B 0\n",
-       "    W        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
-       "Attributes:\n",
-       "    Conventions:  SGRID
" - ], - "text/plain": [ - " Size: 34MB\n", - "Dimensions: (time: 241, ZF: 11, YG: 5, XG: 81, YC: 5, XC: 81)\n", - "Coordinates:\n", - " * time (time) datetime64[ns] 2kB 2000-01-01 ... 2000-01-11\n", - " * ZF (ZF) float64 88B 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0\n", - " * YG (YG) int64 40B 0 1 2 3 4\n", - " lat (YG) float64 40B -1e+06 -5e+05 0.0 5e+05 1e+06\n", - " * XG (XG) int64 648B 0 1 2 3 4 5 6 7 8 9 ... 72 73 74 75 76 77 78 79 80\n", - " lon (XG) float64 648B -1e+06 -9.75e+05 -9.5e+05 ... 9.75e+05 1e+06\n", - " * YC (YC) float64 40B 0.5 1.5 2.5 3.5 4.5\n", - " * XC (XC) float64 648B 0.5 1.5 2.5 3.5 4.5 ... 76.5 77.5 78.5 79.5 80.5\n", - " depth (time, ZF, YC, XC) float64 9MB 3.923 -3.923 -11.67 ... 993.3 997.8\n", - "Data variables:\n", - " U (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", - " V (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", - " grid int64 8B 0\n", - " W (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", - "Attributes:\n", - " Conventions: SGRID" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "n_days = 10\n", "n_snapshots = 24 * n_days + 1 # hourly snapshots from hour 0 to the end of day 10\n", @@ -2047,45235 +93,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - " \n", - "
\n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "time = ds[\"time\"].values.astype(\"datetime64[s]\")\n", "z_moving = ds[\"depth\"].values # interface depths, shape (time, depth, YC, XC)\n", @@ -47325,787 +145,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 34MB\n",
-       "Dimensions:  (time: 241, ZF: 11, YG: 5, XG: 81, YC: 5, XC: 81)\n",
-       "Coordinates:\n",
-       "  * time     (time) datetime64[ns] 2kB 2000-01-01 ... 2000-01-11\n",
-       "  * ZF       (ZF) float64 88B 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0\n",
-       "  * YG       (YG) int64 40B 0 1 2 3 4\n",
-       "    lat      (YG) float64 40B -1e+06 -5e+05 0.0 5e+05 1e+06\n",
-       "  * XG       (XG) int64 648B 0 1 2 3 4 5 6 7 8 9 ... 72 73 74 75 76 77 78 79 80\n",
-       "    lon      (XG) float64 648B -1e+06 -9.75e+05 -9.5e+05 ... 9.75e+05 1e+06\n",
-       "  * YC       (YC) float64 40B 0.5 1.5 2.5 3.5 4.5\n",
-       "  * XC       (XC) float64 648B 0.5 1.5 2.5 3.5 4.5 ... 76.5 77.5 78.5 79.5 80.5\n",
-       "    depth    (time, ZF, YC, XC) float64 9MB 3.923 -3.923 -11.67 ... 993.3 997.8\n",
-       "Data variables:\n",
-       "    U        (time, ZF, YG, XG) float64 9MB 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0\n",
-       "    V        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
-       "    grid     int64 8B 0\n",
-       "    W        (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
-       "Attributes:\n",
-       "    Conventions:  SGRID
" - ], - "text/plain": [ - " Size: 34MB\n", - "Dimensions: (time: 241, ZF: 11, YG: 5, XG: 81, YC: 5, XC: 81)\n", - "Coordinates:\n", - " * time (time) datetime64[ns] 2kB 2000-01-01 ... 2000-01-11\n", - " * ZF (ZF) float64 88B 0.0 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0\n", - " * YG (YG) int64 40B 0 1 2 3 4\n", - " lat (YG) float64 40B -1e+06 -5e+05 0.0 5e+05 1e+06\n", - " * XG (XG) int64 648B 0 1 2 3 4 5 6 7 8 9 ... 72 73 74 75 76 77 78 79 80\n", - " lon (XG) float64 648B -1e+06 -9.75e+05 -9.5e+05 ... 9.75e+05 1e+06\n", - " * YC (YC) float64 40B 0.5 1.5 2.5 3.5 4.5\n", - " * XC (XC) float64 648B 0.5 1.5 2.5 3.5 4.5 ... 76.5 77.5 78.5 79.5 80.5\n", - " depth (time, ZF, YC, XC) float64 9MB 3.923 -3.923 -11.67 ... 993.3 997.8\n", - "Data variables:\n", - " U (time, ZF, YG, XG) float64 9MB 1.0 1.0 1.0 1.0 ... 1.0 1.0 1.0 1.0\n", - " V (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", - " grid int64 8B 0\n", - " W (time, ZF, YG, XG) float64 9MB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", - "Attributes:\n", - " Conventions: SGRID" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "u0 = 1.0\n", "ds[\"U\"].values[:] = u0\n", @@ -48114,34 +156,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "| Name | Type | Grid number | Interp method / value | Parcels backend |\n", - "|:-------|:------------|--------------:|:------------------------|:------------------|\n", - "| U | Field | 0 | XLinear(...) | NumPy |\n", - "| V | Field | 0 | XLinear(...) | NumPy |\n", - "| W | Field | 0 | XLinear(...) | NumPy |\n", - "| UV | VectorField | 0 | XLinear_Velocity(...) | - |\n", - "| UVW | VectorField | 0 | XLinear_Velocity(...) | - |\n", - "\n", - "mesh: FlatMesh()\n", - "time interval: (np.datetime64('2000-01-01T00:00:00.000000000'), np.datetime64('2000-01-11T00:00:00.000000000'))\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ipykernel_2922109/1955122603.py:1: FieldSetWarning: Time-varying (4D) z coordinates are experimental and may cause significant memory overhead that leads to OOM errors. This z coordinate has sizes {'time': 241, 'ZF': 11, 'YC': 5, 'XC': 81} (0.00859 GB). Assumptions: z has dims ('time', 'ZF', 'YC', 'XC'), i.e. it is defined at the layer interfaces on the cell centres or corners, and is strictly increasing along 'ZF'; each particle's z column is interpolated with the grid's `z_interp_method` (by default bilinearly from the surrounding columns), and linearly in time between z snapshots.\n", - " fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n" - ] - } - ], + "outputs": [], "source": [ "fieldset = parcels.FieldSet.from_sgrid_conventions(ds)\n", "fieldset.describe()" @@ -48158,7 +175,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -48179,20 +196,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABKQAAAGGCAYAAABFf1lKAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjExLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlcelbwAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzsnQeUFNXWhS85CE+QIEklihIERKKACgKCggQDSFBRMSAmzBkDYA4oomJCFBURxYSgoiAgiuSsIDlIlJz7X995f81rhgk907l7f2vdVT0zPd1Vt9K9u87ZJ4fP5/M5IYQQQgghhBBCCCEiRM5IfZEQQgghhBBCCCGEECBBSgghhBBCCCGEEEJEFAlSQgghhBBCCCGEECKiSJASQgghhBBCCCGEEBFFgpQQQgghhBBCCCGEiCgSpIQQQgghhBBCCCFERJEgJYQQQgghhBBCCCEiigQpIYQQQgghhBBCCBFRJEgJ4ZzbuHGj+/TTT92WLVsy7Y+9e/fae5cvX57lvvvnn3/sfzdt2hQX/b57925b35UrV2bp/9asWWP/t2PHDhfPTJs2zU2YMCGg9y5YsMC2ORbZs2ePrduKFSuivSpCCCGEEEIIYUiQEgnDlClT3JgxY7L1v7NmzXKXXnqpW7RoUcrvfv31Vzdu3Lhj3otoxXvHjx+f5e+ZO3eu/S/iRaz1XVrCy/r16219J06cmKXP++WXX+z/Vq1a5eKZp556yt1+++0pPx84cMCEnaVLlx7z3pEjR9o2xyIIoazb999/H+1VEUKIbLNv3z6XI0cO98QTT4Tl/aH632iR3XWOx22N9vERCWJ9/YQQIhRIkBIJw8CBA13Xrl2z9b+lSpVynTt3dsWLF0/53bPPPutuvvnmY95bsGBBe2+lSpVcIvWdv/DiUahQIdvW8uXLu2SkcePGrlWrVik/E/GFsDN27Nhj3lujRg3rq1jkuOOOs3WrUKFCtFdFCJHk5M+f31100UUh+7zt27fbpH3QoEEuGUi27Q0W9ZeIxnF06NAh99lnn7n27du7cuXKucKFC7tatWq5559/3u3fvz/N/xk9erQ766yzXIECBVzJkiVdr1690s2oCNd70yPQz+BB9uDBg13Tpk1dzpw5sz1/COX3dejQwfZdZo3vy+r3Z/W9wW5vvH5fpviESBAuvPBCX758+UL2eZ07d/ZVqlTJF0omTJjg47SbOHGiL9b6rnr16iH7vJEjR9p2zps3z5dIbNq0ybbrmWeeifaqCCFEXMJ9mntOqNi2bZtdlwcOHOgLJXv37rXPffzxx32xRDi2N1a3NZaPD5FcZPU4GjNmjC9nzpy+W265xbds2TLfjh07fB999JGvcOHCvpYtW/qOHDmS5rj5qaee8u3atcu3ePFiX+3atX01atSw8zMS702PrHzGpZde6rv55pt9kyZN8p155pm+U045JaDviOT3ffnll/b5b775ZtDfH+n+HRmH3xcIEqREQgtSXHR+//13e71mzRrfV1995Zs+ffoxN4INGzb4Ro0a5du8ebP9/MMPP/gaNmzoK1WqlP3ea/zfnj177DU3GI+///475T2ffvqpb9y4cb61a9dmW5DyPo+T/+DBg76ff/7Z980336Ssnz+//vprynePHj3a99NPP9mNLzUzZ870ffHFF/aabWAbv/vuO9/48eN9devW9Z100klHfQ7w/fy8YsWKYz6Pz5g8ebL16V9//RWwIMU28J3sm+XLl/sCYcuWLbYeXp+yT/n/lStXpvs/69at83377bfWb2ntCzhw4IDvt99+s22YM2eO/ezP1KlTbV1h69atvrffftu2q2fPnil9NWPGDPv7/Pnz7ee0mDt3rm/s2LG2b+jT1PD7H3/8MWVbWW/2+f79+9P8vIULF9p2sX5p7evU7N6929aN48pj9uzZvs8++8xec1P5/vvvbVsD+Tx/2G4mMddcc42ve/fuvh49elj/XHnllb7bb789w//duHGjnQsch6tWrTrqb96x560z68s+T32sAedi6vOP/Z8ehw4dsvOBfcLn8nN628Z7WEf6UAgRPBKkgkOCVPT7SyQfWT2OGO8zTkvNoEGDjpkHMAYpXbq0r3Xr1ke9d9asWfbeF198MezvTY9gPoO5RVYFqUh8X0aCVCz376E4/L5AkSAlElqQOvHEE21i/Nxzz/lq1qzpa9Giha9AgQK+Jk2aHDXBRADg5EJggbvvvttXtmxZ33HHHWeRUl47fPiwb/Xq1fbe1157LeX/mcx77+nYsaOvQYMGvly5ctnEnP/JqiDFRZL38blcYJs3b+477bTTfPnz5z/qe+GFF15I+e527dr5qlatatuY+n3XX3+97/jjj7dtrFatmn0mTxMQDRDe/vOf/6R8zmWXXWb/8+eff9p6vPPO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7RzXz36vw9esWeNcddVV9hi0aNHC1s/XXHNNUF2julrHWe0vb3l8/fXXQX9r6tSpttx0L6D6WevrHsGvPvzwQ3svoOOs1x49emSqm/xu9OjRTpMmTZxq1ao5F198sT1+w4YNC1rn008/dQoXLpzp87t///6g9YYPH27rX9V9etX91OnTpx2/iNa9WyLc3+k66t3P1q1b28+g99hG6x4wlvbv3+88/PDDTtWqVe223HvvvWHXe/31150SJUoEtrlfv37O8ePH82WdSBGQyoVnnnnGmTVrlr0ZyCog1aVLF9uYci82v/nNb2wF4q2EorVOPM2cOdOWwZQpU4Iuau3bt8/29/7617/aBufatWsDNw1FixZ1/vOf/zh+oYvRLbfcku06Dz74oG0cqAEoOidSUlKcZcuW5WideNq0aZMzYsSIwEVE59pNN91kG3Znz54NrBcuABVKF2Yde/dv6VzQzb67736iSlMNjQEDBgT2U8f8oosusjdGieKLL74Iarju2rXLnm/33HNPpoBUdg11raNGs9twUBl07drVNiL9FpDStmZl3bp1Nkjzj3/8w35/4MABp169es4vf/nLHK3jNzt27HAKFSoU1NjRtUT1QXYmTJhgG8vugwx93nUT/sILLzjx8utf/9reSD7xxBNhA1K6fqhBNWTIEPt9enq607t3b6dVq1ZRXwf+oc/hY4895mzZssVei8MFpBQwUp2iwJV7jNu2bev07ds36uvE2+9//3t7g+h9uKAbKX1usqOAR5s2bezNkvzxj3+0Nwt6YOQXCqx88MEHeaqfE6EOVyDQ+1BO550e1nnrGjcgNW/evCz/jtr8utbrQYXbTlPdrDrajzZu3Gjb9G6QUXW2rvW33367k0heeeUVG1T0BgXVdvDeqyggpfuZ7Ohhn+pvvYqucepooIdFfhGte7dEuL8Lpbag7st0XKJ9DxhLK1eutIFPPeDQsQwXkFI7Wtv4+eef2+9VLyjgqkBWtNfJCQJSeZBVQGrDhg32fffCI/v27bMXMbcCjtY68aYPrJ4uegMXuqgp2rx8+XJ74xcuYKcbgjvvvDPoPUXi9STJL7Rv1113nbN48WJn8+bNtvHjpe91A68Pv5dubB944IGI1/EjnXM697Zt2xYUkNKN5MKFC+3NcSg1tHRxUg9Clxr5eqL02muvOX6jSlL7qPPU9d1339n3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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "H = z_moving[0, -1, centre_row]\n", "eta = -z_moving[:, 0, centre_row]\n", @@ -48231,21 +237,13 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": { "tags": [ "hide-output" ] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Output files are stored in sigma_particles.parquet\n" - ] - } - ], + "outputs": [], "source": [ "outputfile = parcels.ParticleFile(\n", " path=\"sigma_particles.parquet\", outputdt=np.timedelta64(6, \"h\"), mode=\"w\"\n", @@ -48262,69262 +260,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - " \n", - "
\n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "df = parcels.read_particlefile(\"sigma_particles.parquet\").sort([\"t\", \"particle_id\"])\n", "output_times = df[\"t\"].unique().sort()\n", @@ -117592,18 +337,9 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "structured z: ('time', 'ZF', 'YC', 'XC') (241, 11, 5, 81)\n", - "unstructured z: ('time', 'zf', 'n_node') (241, 11, 441)\n" - ] - } - ], + "outputs": [], "source": [ "n_lattice_nodes = (\n", " 21 # the number of lattice nodes in x and y, so that the nodes are 100km apart\n", @@ -117638,34 +374,9 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "| Name | Type | Grid number | Interp method / value | Parcels backend |\n", - "|:-------|:------------|--------------:|:--------------------------|:------------------|\n", - "| U | Field | 0 | UxLinearNodeLinearZF(...) | NumPy |\n", - "| V | Field | 0 | UxLinearNodeLinearZF(...) | NumPy |\n", - "| W | Field | 0 | UxLinearNodeLinearZF(...) | NumPy |\n", - "| UV | VectorField | 0 | Ux_Velocity(...) | - |\n", - "| UVW | VectorField | 0 | Ux_Velocity(...) | - |\n", - "\n", - "mesh: FlatMesh()\n", - "time interval: (np.datetime64('2000-01-01T00:00:00.000000000'), np.datetime64('2000-01-11T00:00:00.000000000'))\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ipykernel_2922109/1845485040.py:2: FieldSetWarning: Time-varying (3D) z coordinates are experimental and may cause significant memory overhead that leads to OOM errors. This z coordinate has sizes {'time': 241, 'zf': 11, 'n_node': 441} (0.00935 GB). Assumptions: z has dims ('time', 'zf', 'n_node'), i.e. it is defined at the layer interfaces ('zf') on the mesh nodes or faces, and is strictly increasing along 'zf'; each particle's z column is interpolated with the grid's `z_interp_method` (by default barycentrically from its face's nodes for node z, or constant over its face for face z), and linearly in time between z snapshots.\n", - " ux_fieldset = parcels.FieldSet.from_ugrid_conventions(ux_ds)\n" - ] - } - ], + "outputs": [], "source": [ "ux_ds[\"U\"].values[:] = u0\n", "ux_fieldset = parcels.FieldSet.from_ugrid_conventions(ux_ds)\n", @@ -117681,21 +392,13 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": { "tags": [ "hide-output" ] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Output files are stored in ux_sigma_particles.parquet\n" - ] - } - ], + "outputs": [], "source": [ "ux_pset = parcels.ParticleSet(\n", " fieldset=ux_fieldset,\n", @@ -117720,69133 +423,9 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - " \n", - "
\n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "ux_df = parcels.read_particlefile(\"ux_sigma_particles.parquet\").sort(\n", " [\"t\", \"particle_id\"]\n", @@ -186936,26 +515,9 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "INFO: Output files are stored in XVerticalCoordinateLinear_particles.parquet\n", - "INFO: Output files are stored in XVerticalCoordinateConstant_particles.parquet\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/tmp/ipykernel_2922109/323142981.py:24: FieldSetWarning: Time-varying (4D) z coordinates are experimental and may cause significant memory overhead that leads to OOM errors. This z coordinate has sizes {'time': 241, 'ZF': 11, 'YC': 5, 'XC': 11} (0.00117 GB). Assumptions: z has dims ('time', 'ZF', 'YC', 'XC'), i.e. it is defined at the layer interfaces on the cell centres or corners, and is strictly increasing along 'ZF'; each particle's z column is interpolated with the grid's `z_interp_method` (by default bilinearly from the surrounding columns), and linearly in time between z snapshots.\n", - " coarse_fieldset = parcels.FieldSet.from_sgrid_conventions(coarse_ds)\n" - ] - } - ], + "outputs": [], "source": [ "# the same basin as above on a coarser grid with 200 km wide cells, so that the difference between the interpolators is\n", "# easy to see\n", @@ -187019,21605 +581,9 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
\n", - " \n", - "
\n", - " \n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
\n", - "
\n", - "
\n", - "\n", - "\n", - "\n" - ], - "text/plain": [ - "" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "n_surface_frames = 25 # hourly outputs from hour 0 to hour 24\n", "surface_px = {}\n", @@ -208681,13 +647,6 @@ "\n", "Parcels raises an error when the interpolator does not fit where z is defined, for example `XVerticalCoordinateConstant` with z on the cell corners." ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 13aaeb3ffc9ed0ae5cca8ac2ab465da6c2b93158 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 20:07:34 +0000 Subject: [PATCH 14/16] minor changes --- docs/user_guide/examples/tutorial_schism.ipynb | 4 ++-- .../examples/tutorial_time_varying_z_coordinates.ipynb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/user_guide/examples/tutorial_schism.ipynb b/docs/user_guide/examples/tutorial_schism.ipynb index e549178b63..a81b4cb973 100644 --- a/docs/user_guide/examples/tutorial_schism.ipynb +++ b/docs/user_guide/examples/tutorial_schism.ipynb @@ -445,7 +445,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Parcels:default (3.14.8)", "language": "python", "name": "python3" }, @@ -459,7 +459,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.14.6" + "version": "3.14.8" }, "mystnb": { "execution_in_temp": true diff --git a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb index 91a51b9141..dae56a38b8 100644 --- a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb +++ b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb @@ -140,7 +140,7 @@ "\n", "We prescribe a uniform velocity of $u_0 = 1$ m/s in the $x$ direction, with $v = w = 0$. All three components sit on the grid nodes at the layer interfaces (`depth`), so Parcels interpolates them linearly in all three directions.\n", "\n", - "To advect the particles on a sigma grid, for which the z grid varies in time, we use a 4D `depth` coordinate with dimensions of `(time, depth, YC, XC)`. For each particle, Parcels builds a z column by interpolating the columns at the surrounding cell centres bilinearly in space and linearly in time between the two surrounding snapshots, and then searches that column for the particle's layer." + "To advect the particles on a sigma grid, for which the z grid varies in time, we use a 4D `depth` coordinate with dimensions of `(time, depth, Y, X)`. For each particle, Parcels builds a z column by interpolating the columns at the surrounding cell centres bilinearly in space and linearly in time between the two surrounding snapshots, and then searches that column for the particle's layer." ] }, { From 87e87c3b52007785e9fa87727a8e274d18f921c0 Mon Sep 17 00:00:00 2001 From: Wyatt Sieminski Date: Tue, 10 Nov 2026 20:31:13 +0000 Subject: [PATCH 15/16] add py:obj to tutorial interpolators --- .../tutorial_time_varying_z_coordinates.ipynb | 30 ++----------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb index dae56a38b8..80f30a60ae 100644 --- a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb +++ b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb @@ -497,21 +497,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Choosing how z is interpolated\n", - "\n", - "With a 4d z coordinate, in order to find a particle z value at some time, we must interpolate the current z grid at that time onto the particle position. This reduces the 4d z coordinate to a 1d z coordiante where each entry is an interface depth at the particle's exact position in space. For more details on interpolation in general, see [tutorial_interpolation](tutorial_interpolation.ipynb). There are currently a number of built in interpolators for vertical coordinates:\n", - "\n", - "- `XVerticalCoordinateLinear` (structured grids, z on the cell centres or corners): bilinear interpolation between the surrounding columns, so z is continuous in space.\n", - "- `XVerticalCoordinateNearest` (structured grids, z on the cell centres or corners): piecewise constant interpolation based on whatever cell is closest to the particle.\n", - "- `XVerticalCoordinateConstant` (structured grids, z on the cell centres): piecewise constant interpolation based on whatever cell the particle is in. \n", - "- `UxVerticalCoordinateLinearNode` (unstructured grids, z on the nodes): barycentric interpolation from the three corners of the particle's triangle.\n", - "- `UxVerticalCoordinateConstantFace` (unstructured grids, z on the faces): piecewise constant interpolation based on whatever cell the particle is in. \n", - "\n", - "All of them interpolate z linearly in time between the two surrounding snapshots.\n", - "\n", - "The effect of selecting different vertical coordinate interpolators can be seen by investigating what happens to particles near the surface:" - ] + "source": "## Choosing how z is interpolated\n\nWith a 4d z coordinate, in order to find a particle z value at some time, we must interpolate the current z grid at that time onto the particle position. This reduces the 4d z coordinate to a 1d z coordiante where each entry is an interface depth at the particle's exact position in space. For more details on interpolation in general, see [tutorial_interpolation](tutorial_interpolation.ipynb). There are currently a number of built in interpolators for vertical coordinates:\n\n- {py:obj}`parcels.interpolators.XVerticalCoordinateLinear` (structured grids, z on the cell centres or corners): bilinear interpolation between the surrounding columns, so z is continuous in space.\n- {py:obj}`parcels.interpolators.XVerticalCoordinateNearest` (structured grids, z on the cell centres or corners): piecewise constant interpolation based on whatever cell is closest to the particle.\n- {py:obj}`parcels.interpolators.XVerticalCoordinateConstant` (structured grids, z on the cell centres): piecewise constant interpolation based on whatever cell the particle is in. \n- {py:obj}`parcels.interpolators.UxVerticalCoordinateLinearNode` (unstructured grids, z on the nodes): barycentric interpolation from the three corners of the particle's triangle.\n- {py:obj}`parcels.interpolators.UxVerticalCoordinateConstantFace` (unstructured grids, z on the faces): piecewise constant interpolation based on whatever cell the particle is in. \n\nAll of them interpolate z linearly in time between the two surrounding snapshots.\n\nThe effect of selecting different vertical coordinate interpolators can be seen by investigating what happens to particles near the surface:" }, { "cell_type": "code", @@ -636,17 +622,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "With `XVerticalCoordinateLinear`, the sea surface and the layer interfaces vary smoothly between the cell centres, while with `XVerticalCoordinateConstant` they jump at every cell edge. The discontinuity of piecewise constant vertical coordinate interpolators can cause particles to spuriously leave the domain, as shown above. \n", - "\n", - "By default, Parcels uses `XVerticalCoordinateLinear` for time-varying z on structured grids, `UxVerticalCoordinateLinearNode` for z on the nodes of an unstructured grid, and `UxVerticalCoordinateConstantFace` for z on the faces of an unstructured grid. To use a different interpolator, set the `z_interp_method` of the grid after building the `FieldSet`, which changes it for every field on that grid:\n", - "\n", - "```python\n", - "fieldset.U.grid.z_interp_method = parcels.interpolators.XVerticalCoordinateConstant()\n", - "```\n", - "\n", - "Parcels raises an error when the interpolator does not fit where z is defined, for example `XVerticalCoordinateConstant` with z on the cell corners." - ] + "source": "With {py:obj}`parcels.interpolators.XVerticalCoordinateLinear`, the sea surface and the layer interfaces vary smoothly between the cell centres, while with {py:obj}`parcels.interpolators.XVerticalCoordinateConstant` they jump at every cell edge. The discontinuity of piecewise constant vertical coordinate interpolators can cause particles to spuriously leave the domain, as shown above. \n\nBy default, Parcels uses `XVerticalCoordinateLinear` for time-varying z on structured grids, {py:obj}`parcels.interpolators.UxVerticalCoordinateLinearNode` for z on the nodes of an unstructured grid, and {py:obj}`parcels.interpolators.UxVerticalCoordinateConstantFace` for z on the faces of an unstructured grid. To use a different interpolator, set the `z_interp_method` of the grid after building the {py:obj}`parcels.FieldSet`, which changes it for every field on that grid:\n\n```python\nfieldset.U.grid.z_interp_method = parcels.interpolators.XVerticalCoordinateConstant()\n```\n\nParcels raises an error when the interpolator does not fit where z is defined, for example `XVerticalCoordinateConstant` with z on the cell corners." } ], "metadata": { @@ -673,4 +649,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file From 4a0b6cc571fbbc823800b8c81465d8cde139c20b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 10 Nov 2026 20:32:26 +0000 Subject: [PATCH 16/16] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .../tutorial_time_varying_z_coordinates.ipynb | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb index 80f30a60ae..d230a4a3dd 100644 --- a/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb +++ b/docs/user_guide/examples/tutorial_time_varying_z_coordinates.ipynb @@ -581,9 +581,13 @@ " # a particle that has left the water stays at its last position\n", " for frame in range(1, n_surface_frames):\n", " left_the_water = np.isnan(surface_px[name][frame])\n", - " surface_px[name][frame, left_the_water] = surface_px[name][frame - 1, left_the_water]\n", + " surface_px[name][frame, left_the_water] = surface_px[name][\n", + " frame - 1, left_the_water\n", + " ]\n", "\n", - "coarse_z = coarse_ds[\"depth\"].values[:, :, centre_row] # interface depths, shape (time, depth, x)\n", + "coarse_z = coarse_ds[\"depth\"].values[\n", + " :, :, centre_row\n", + "] # interface depths, shape (time, depth, x)\n", "surface_colors = [f\"C{p}\" for p in range(surface_z0.size)]\n", "\n", "fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True)\n", @@ -599,7 +603,9 @@ " ax.set_xlim(-800, 0)\n", " ax.set_xlabel(\"x [km]\")\n", " panels.append((name, lines, dots))\n", - "axes[0].set_ylim(300, -75) # the upper part of the water column, with depth increasing downwards\n", + "axes[0].set_ylim(\n", + " 300, -75\n", + ") # the upper part of the water column, with depth increasing downwards\n", "axes[0].set_ylabel(\"depth [m]\")\n", "suptitle = fig.suptitle(f\"Particles near the surface at {time[0]}\")\n", "plt.tight_layout()\n", @@ -649,4 +655,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +}