Skip to content

Vertical Coordinate Interpolators - #3037

Open
wyatt-fluidnumerics wants to merge 18 commits into
mainfrom
vertical-coordinate-interpolators
Open

wyatt-fluidnumerics wants to merge 18 commits into
mainfrom
vertical-coordinate-interpolators

Conversation

@wyatt-fluidnumerics

@wyatt-fluidnumerics wyatt-fluidnumerics commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR addresses the need for more complex interpolation methods for time varying z dimensions, while maintaining flexibility for users and alignment with the current interpolator API.

The key change is the introduction of a VerticalCoordinateInterpolator abstract class that sits at the same level as the Vector and Scalar interpolator classes. At its core, the VerticalCoordinateInterpolator class enables any existing or user madeScalarInterpolator to be used to interpolate the vertical coordinate. This is done through its base method _columns_from_scalar, which takes in a scalar interpolator and uses it to run a horizontal interpolation on every vertical interface at each particle's horizontal position. These interfaces are then used to reconstruct the vertical columns at each particle position, which can be used for the vertical grid search.

For time varying z coordinate datasets, a vertical coordinate interpolator is set during Fieldset creation, and can be overwritten by the user.

This PR also adjusts all UxInterpolators to perform linear in time interpolation for data that falls between two model snapshots. This is required to avoid large temporal jump discontinuities in time varying vertical grids.

Checklist

  • Tests added
  • This PR targets the correct branch (main for normal development, v3-support for v3 support)

AI Disclosure

  • This PR contains AI-generated content.
    • I have tested any AI-generated content in my PR.
    • I take responsibility for any AI-generated content in my PR.
    • Describe how you used it (e.g., by pasting your prompt): I guided claude opus 5.5 through the implementation. I have thoroughly reviewed all code, and made edits where needed.

@wyatt-fluidnumerics
wyatt-fluidnumerics marked this pull request as ready for review November 10, 2026 20:33

@erikvansebille erikvansebille left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Some comments below

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this change necessary? Why did the old code not work? Or was it simply superfluous because a 4-D grid always has a Z dimension?

Comment thread src/parcels/_core/uxgrid.py Outdated
Comment on lines +78 to +86
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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I thought this warning may not be necessary anymore because the 4D depth fields can use the ChunkCachedArray or WindowedArray backend?

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":

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also need to check that the dimension order of z is such that the last is n_node or n_face? Or is that done somewhere else?

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}")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we be more explicit here what the cell centres are called in this dataset? Would we know that?


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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only realise this now, but would it not make more sense to use the order ti, tau, yi, eta, xi, xsi for the position here - in order to harmonise dimensionality throughout the codebase?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't comment on individual lines, so below some comments on the notebook

  • rename the title to use lowercase `time varying'?
  • use the hide-input tag for the dataset creation cell, so that users don't get overwhelmed by it? It's not that relevant for most users how the dataset is created?
  • Type-o in coordiante
  • Is it expected that the particles don't move horizontally on the right panel of the last animation?

self._ds = model_data
self.mesh = mesh
self._spatialhash = None
self._z_interp_method = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _interp_method is set on the Field. Why is the _z_interp_method then set on the grid? Can different Fields on the same grid not have different interpolation methods?

self._ds = model_data
self.mesh = mesh
self._spatialhash = None
self._z_interp_method = None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we now rename interp_method to h_interp_method or something similar to make explicit that these are horizontal interpolation methods? Or should/could that be done in another pR?

Comment on lines +55 to +56
def __repr__(self):
return f"{self.__class__.__name__}(...)"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move this interpolators.__repr__ to the repr_utils.py file, so they are all in one place

Comment on lines +670 to +674
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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use the offsets here? Because some models are indexed by the upper-right corner and others by the lower-left corner. Or is that not relevant here?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

2 participants