Skip to content

Commit eb612f5

Browse files
Merge branch 'main' into updating_v3_tutorials
2 parents e1b2ee5 + be4f5d9 commit eb612f5

9 files changed

Lines changed: 339 additions & 124 deletions

File tree

docs/development/unstructured_grid_search.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ The algorithm has two phases: **initialisation** (once, when the grid is first u
4242

4343
The hash grid is three-dimensional regardless of the source grid:
4444

45-
| Source grid | Mesh type | Hash-grid space |
46-
| ----------- | --------- | ------------------------------------- |
47-
| `XGrid` | spherical | Cartesian unit cube (lon/lat → x,y,z) |
48-
| `XGrid` | flat | 2-D lon/lat bounding box (z set to 0) |
49-
| `UxGrid` | spherical | Cartesian unit cube |
50-
| `UxGrid` | flat | 2-D lon/lat bounding box |
45+
| Source grid | Mesh type | Hash-grid space |
46+
| ----------- | --------- | ---------------------------------------- |
47+
| `XGrid` | spherical | Cartesian bounding box (lon/lat → x,y,z) |
48+
| `XGrid` | flat | 2-D lon/lat bounding box (z set to 0) |
49+
| `UxGrid` | spherical | Cartesian bounding box (lon/lat → x,y,z) |
50+
| `UxGrid` | flat | 2-D lon/lat bounding box |
5151

5252
For spherical grids, node coordinates are converted from degrees to radians and then to Cartesian (x, y, z) on the unit sphere using `parcels._core.index_search._latlon_rad_to_xyz`:
5353

@@ -57,7 +57,7 @@ y = sin(lon) * cos(lat)
5757
z = sin(lat)
5858
```
5959

60-
The hash grid then spans the unit cube `[-1, 1]³`. Working in Cartesian space avoids the longitude wrap-around discontinuity that would otherwise cause the bounding boxes of cells crossing the antimeridian to erroneously span the entire domain.
60+
The hash grid then spans the axis-aligned Cartesian bounding box of the transformed grid nodes. For a global grid this is (nearly) the unit cube `[-1, 1]³`; for a regional grid it is much tighter, so the full quantization resolution (1024 bins per axis) is spent on the region actually covered by the grid rather than the whole sphere. Working in Cartesian space avoids the longitude wrap-around discontinuity that would otherwise cause the bounding boxes of cells crossing the antimeridian to erroneously span the entire domain.
6161

6262
For flat meshes the hash grid simply spans `[lon_min, lon_max] × [lat_min, lat_max]`, with z fixed at 0.
6363

docs/user_guide/examples/explanation_kernelloop.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,14 @@ import parcels.tutorial
5959
ds_fields = parcels.tutorial.open_dataset("CopernicusMarine_data_for_Argo_tutorial/data")
6060
6161
# Create an idealised wind field and add it to the dataset
62-
tdim, ydim, xdim = (len(ds_fields.time),len(ds_fields.latitude), len(ds_fields.longitude))
62+
ydim, xdim = len(ds_fields.latitude), len(ds_fields.longitude)
6363
ds_fields["UWind"] = xr.DataArray(
64-
data=0.5 * np.ones((tdim, ydim, xdim)) * np.sin(ds_fields.latitude.values - ds_fields.latitude.values.mean())[None, :, None],
65-
coords=[ds_fields.time, ds_fields.latitude, ds_fields.longitude])
64+
data=0.5 * np.ones((ydim, xdim)) * np.sin(ds_fields.latitude.values - ds_fields.latitude.values.mean())[:, None],
65+
coords=[ds_fields.latitude, ds_fields.longitude])
6666
6767
ds_fields["VWind"] = xr.DataArray(
68-
data=np.zeros((tdim, ydim, xdim)),
69-
coords=[ds_fields.time, ds_fields.latitude, ds_fields.longitude])
68+
data=np.zeros((ydim, xdim)),
69+
coords=[ds_fields.latitude, ds_fields.longitude])
7070
7171
fields = {
7272
"U": ds_fields["uo"],

src/parcels/_core/field.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ def grid(self): # TODO PR: Remove in favour of referencing model grid directly
109109

110110
@property
111111
def time_interval(self): # TODO PR: Remove in favour of referencing model time_interval directly
112+
if "time" not in self.model.data[self.name].dims:
113+
# This field does not have a time-dimension
114+
return None
112115
return self.model.time_interval
113116

114117
def __repr__(self):

src/parcels/_core/fieldset.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import functools
44
import sys
5+
import warnings
56
from collections.abc import Iterable
67
from typing import IO, TYPE_CHECKING
78

@@ -21,6 +22,7 @@
2122
from parcels._core.utils.string import _assert_str_and_python_varname
2223
from parcels._core.utils.time import get_datetime_type_calendar
2324
from parcels._core.utils.time import is_compatible as datetime_is_compatible
25+
from parcels._core.warnings import FieldSetWarning
2426
from parcels._python import NOTSET, NotSetType
2527
from parcels._reprs import fieldset_describe
2628
from parcels.interpolators import (
@@ -73,6 +75,7 @@ def __init__(self, models: list[ModelData]):
7375
self._fields: dict[str, Field | VectorField] | None = None
7476
self.reconstruct_fields()
7577
self.context: dict[str, float] = {}
78+
_warn_if_fields_use_different_meshes(self.fields.values())
7679

7780
def __setattr__(self, name, value):
7881
"""Set field attribute by name. If context exists and name in context, raise error to prevent overwriting context variable."""
@@ -151,6 +154,7 @@ def add_field(self, field: Field, name: str | None = None):
151154
raise ValueError(f"FieldSet already has a Field with name '{name}'")
152155

153156
self.fields[name] = field
157+
_warn_if_fields_use_different_meshes(self.fields.values())
154158

155159
def to_windowed_arrays(self, *, max_levels: int | None = None):
156160
"""Wrap dask-backed field data in rolling time-window caches.
@@ -215,6 +219,7 @@ def add_constant_field(self, name: str, value, mesh: ptyping.Mesh = "spherical")
215219
self.reconstruct_fields()
216220
field = getattr(self, name)
217221
field.interp_method = XConstantField()
222+
_warn_if_fields_use_different_meshes(self.fields.values())
218223

219224
def add_context(self, name, value):
220225
"""Add context variable to the FieldSet.
@@ -362,6 +367,28 @@ def assert_compatible_fieldsets(left: FieldSet, right: FieldSet) -> None:
362367
)
363368

364369

370+
def _warn_if_fields_use_different_meshes(fields: Iterable[Field | VectorField]):
371+
"""Warn if multiple fields use different meshes on the underlying grids.
372+
373+
Parameters
374+
----------
375+
fields : Iterable[Field | VectorField]
376+
The fields to check for conflicting meshes.
377+
378+
Warns
379+
-----
380+
FieldSetWarning
381+
If the fields have different meshes on the underlying grids.
382+
"""
383+
meshes = {field.grid._mesh for field in fields}
384+
if len(meshes) > 1:
385+
warnings.warn(
386+
f"FieldSet has multiple different meshes: {meshes}. This may lead to unexpected behavior during execution.",
387+
category=FieldSetWarning,
388+
stacklevel=3,
389+
)
390+
391+
365392
class CalendarError(Exception): # TODO: Move to a parcels errors module
366393
"""Exception raised when the calendar of a field is not compatible with the rest of the Fields. The user should ensure that they only add fields to a FieldSet that have compatible CFtime calendars."""
367394

src/parcels/_core/index_search.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def _search_time_index(field: Field, time: np.ndarray):
7777
if field.time_interval is None:
7878
return {
7979
"T": {
80-
"index": np.zeros(shape=time.shape, dtype=np.int32),
81-
"bcoord": np.zeros(shape=time.shape, dtype=np.float32),
80+
"index": np.zeros(shape=np.atleast_1d(time).shape, dtype=np.int32),
81+
"bcoord": np.zeros(shape=np.atleast_1d(time).shape, dtype=np.float32),
8282
}
8383
}
8484

0 commit comments

Comments
 (0)