Skip to content

Commit 08a0a26

Browse files
Rename particle position attributes from "lon" and "lat" to "x" and "y" (#2719)
* Refactor lon lat to x y Update particle definition and all related code - particle.py particle variables - ParticleSet args - all internal mentions of .lon and .lat - test suite code * Update repr * Update interpolators * Update error messages * Update local varnames in kernels * Update migration guide * Review feedback * Restore v3 files * Update docs/user_guide/v4-migration.md Co-authored-by: Erik van Sebille <e.vansebille@uu.nl> --------- Co-authored-by: Erik van Sebille <e.vansebille@uu.nl>
1 parent 0e7c2ee commit 08a0a26

27 files changed

Lines changed: 427 additions & 420 deletions

docs/user_guide/v4-migration.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Version 4 of Parcels is unreleased at the moment. The information in this migrat
1818
- Users need to explicitly use `convert_z_to_sigma_croco` in sampling kernels (such as the `AdvectionRK4_3D_CROCO` or `SampleOMegaCroco` kernels) when working with CROCO data, as the automatic conversion from depth to sigma grids under the hood has been removed.
1919
- We added a new AdvectionRK2 Kernel. The AdvectionRK4 kernel is still available, but RK2 is now the recommended default advection scheme as it is faster while the accuracy is comparable for most applications. See also the Choosing an integration method tutorial.
2020
- Functions shouldn't be converted to Kernels before adding to a pset.execute() call. Instead, simply pass the function(s) as a list to pset.execute().
21+
- Kernel variables `lat` and `lon` have been renamed to `y` and `x`, and `dlat` and `dlon` have been renamed to `dy` and `dx`. These changes are also reflected on the ParticleSet as well as the ParticleFile output.
2122

2223
## FieldSet
2324

src/parcels/_compat.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""Import helpers for compatability between installations."""
22

33

4-
# for compat with v3 of parcels when users provide `initial=attrgetter("lon")` to a Variable
4+
# for compat with v3 of parcels when users provide `initial=attrgetter("...")` to a Variable
55
# so that particle initial state matches another variable
66
class _AttrgetterHelper:
77
"""

src/parcels/_core/field.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ def __getitem__(self, key):
184184
self._check_velocitysampling()
185185
try:
186186
if isinstance(key, ParticleSetView):
187-
return self.eval(key.time, key.z, key.lat, key.lon, key)
187+
return self.eval(key.time, key.z, key.y, key.x, key)
188188
else:
189189
return self.eval(*key)
190190
except tuple(AllParcelsErrorCodes.keys()) as error:
@@ -293,7 +293,7 @@ def eval(self, time: datetime, z, y, x, particles=None):
293293
def __getitem__(self, key):
294294
try:
295295
if isinstance(key, ParticleSetView):
296-
return self.eval(key.time, key.z, key.lat, key.lon, key)
296+
return self.eval(key.time, key.z, key.y, key.x, key)
297297
else:
298298
return self.eval(*key)
299299
except tuple(AllParcelsErrorCodes.keys()) as error:
@@ -389,7 +389,7 @@ def _assert_same_time_interval(fields: Sequence[Field]) -> None:
389389

390390
def _get_positions(field: Field, time, z, y, x, particles, _ei) -> tuple[dict, dict]:
391391
"""Initialize and populate particle_positions and grid_positions dictionaries"""
392-
particle_positions = {"time": time, "z": z, "lat": y, "lon": x}
392+
particle_positions = {"time": time, "z": z, "y": y, "x": x}
393393
grid_positions = {}
394394
grid_positions.update(_search_time_index(field, time))
395395
grid_positions.update(field.grid.search(z, y, x, ei=_ei))

src/parcels/_core/kernel.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,13 @@ def remove_deleted(self, pset):
106106
pset.remove_indices(indices)
107107

108108
def _position_update(self, particles, fieldset):
109-
particles.lon += particles.dlon
110-
particles.lat += particles.dlat
109+
particles.x += particles.dx
110+
particles.y += particles.dy
111111
particles.z += particles.dz
112112
particles.time += particles.dt
113113

114-
particles.dlon = 0
115-
particles.dlat = 0
114+
particles.dx = 0
115+
particles.dy = 0
116116
particles.dz = 0
117117

118118
if hasattr(self.fieldset, "RK45_tol"):
@@ -244,6 +244,6 @@ def execute(self, pset, endtime, dt):
244244
if error_code == StatusCode.ErrorOutsideTimeInterval:
245245
error_func(pset[inds].time)
246246
else:
247-
error_func(pset[inds].z, pset[inds].lat, pset[inds].lon)
247+
error_func(pset[inds].z, pset[inds].y, pset[inds].x)
248248

249249
return pset

src/parcels/_core/particle.py

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -141,18 +141,26 @@ def get_default_particle(spatial_dtype: type[np.float32] | type[np.float64]) ->
141141
attrs={"standard_name": "vertical coordinate", "units": "m", "positive": "down"},
142142
),
143143
Variable(
144-
"lat",
144+
"y",
145145
dtype=spatial_dtype,
146-
attrs={"standard_name": "latitude", "units": "degrees_north", "axis": "Y"},
146+
attrs={
147+
"standard_name": "latitude",
148+
"units": "degrees_north",
149+
"axis": "Y",
150+
}, # TODO v4: https://github.com/Parcels-code/Parcels/issues/2720
147151
),
148152
Variable(
149-
"lon",
153+
"x",
150154
dtype=spatial_dtype,
151-
attrs={"standard_name": "longitude", "units": "degrees_east", "axis": "X"},
155+
attrs={
156+
"standard_name": "longitude",
157+
"units": "degrees_east",
158+
"axis": "X",
159+
}, # TODO v4: https://github.com/Parcels-code/Parcels/issues/2720
152160
),
153161
Variable("dz", dtype=spatial_dtype, to_write=False),
154-
Variable("dlat", dtype=spatial_dtype, to_write=False),
155-
Variable("dlon", dtype=spatial_dtype, to_write=False),
162+
Variable("dy", dtype=spatial_dtype, to_write=False),
163+
Variable("dx", dtype=spatial_dtype, to_write=False),
156164
Variable(
157165
"particle_id",
158166
dtype=np.int64,

src/parcels/_core/particleset.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ def __init__(
6060
pclass=Particle,
6161
time=None,
6262
z=None,
63-
lat=None,
64-
lon=None,
63+
y=None,
64+
x=None,
6565
particle_ids=None,
6666
**kwargs,
6767
):
@@ -70,21 +70,21 @@ def __init__(
7070

7171
self.fieldset = fieldset
7272
time = np.empty(shape=0) if time is None else np.array(time).flatten()
73-
lat = np.empty(shape=0) if lat is None else np.array(lat).flatten()
74-
lon = np.empty(shape=0) if lon is None else np.array(lon).flatten()
73+
y = np.empty(shape=0) if y is None else np.array(y).flatten()
74+
x = np.empty(shape=0) if x is None else np.array(x).flatten()
7575

7676
if particle_ids is None:
77-
particle_ids = np.arange(lon.size)
77+
particle_ids = np.arange(x.size)
7878

7979
if z is None:
8080
minz = 0
8181
for field in self.fieldset.fields.values():
8282
if field.grid.depth is not None:
8383
minz = min(minz, field.grid.depth[0])
84-
z = np.ones(lon.size) * minz
84+
z = np.ones(x.size) * minz
8585
else:
8686
z = np.array(z).flatten()
87-
assert lon.size == lat.size and lon.size == z.size, "lon, lat, z don't all have the same lenghts"
87+
assert x.size == y.size and x.size == z.size, "lon, lat, z don't all have the same lenghts"
8888

8989
if time is None or len(time) == 0:
9090
# do not set a time yet (because sign_dt not known)
@@ -95,26 +95,26 @@ def __init__(
9595
time = timedelta_to_float(time)
9696
else:
9797
raise TypeError("particle time must be a datetime, timedelta, or date object")
98-
time = np.repeat(time, lon.size) if time.size == 1 else time
98+
time = np.repeat(time, x.size) if time.size == 1 else time
9999

100-
assert lon.size == time.size, "time and positions (lon, lat, z) do not have the same lengths."
100+
assert x.size == time.size, "time and positions (lon, lat, z) do not have the same lengths."
101101

102102
if fieldset.time_interval:
103103
_warn_particle_times_outside_fieldset_time_bounds(time, fieldset.time_interval)
104104

105105
for kwvar in kwargs:
106106
kwargs[kwvar] = np.array(kwargs[kwvar]).flatten()
107-
assert lon.size == kwargs[kwvar].size, f"{kwvar} and positions (lon, lat, z) don't have the same lengths."
107+
assert x.size == kwargs[kwvar].size, f"{kwvar} and positions (lon, lat, z) don't have the same lengths."
108108

109109
self._data = create_particle_data(
110110
pclass=pclass,
111-
nparticles=lon.size,
111+
nparticles=x.size,
112112
ngrids=len(fieldset.gridset),
113113
initial=dict(
114114
time=time,
115115
z=z,
116-
lat=lat,
117-
lon=lon,
116+
y=y,
117+
x=x,
118118
particle_id=particle_ids,
119119
),
120120
)
@@ -245,7 +245,7 @@ def remove_indices(self, indices):
245245
def populate_indices(self):
246246
"""Pre-populate guesses of particle ei (element id) indices"""
247247
for i, grid in enumerate(self.fieldset.gridset):
248-
grid_positions = grid.search(self.z, self.lat, self.lon)
248+
grid_positions = grid.search(self.z, self.y, self.x)
249249
self._data["ei"][:, i] = grid.ravel_index(
250250
{
251251
"X": grid_positions["X"]["index"],

src/parcels/_core/statuscodes.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class FieldInterpolationError(RuntimeError):
4141

4242

4343
def _raise_field_interpolation_error(z, y, x):
44-
raise FieldInterpolationError(f"Field interpolation returned NaN at (z={z}, lat={y}, lon={x})")
44+
raise FieldInterpolationError(f"Field interpolation returned NaN at (z={z}, y={y}, x={x})")
4545

4646

4747
class FieldOutOfBoundError(RuntimeError):
@@ -51,7 +51,7 @@ class FieldOutOfBoundError(RuntimeError):
5151

5252

5353
def _raise_field_out_of_bound_error(z, y, x):
54-
raise FieldOutOfBoundError(f"Field sampled out-of-bound, at (z={z}, lat={y}, lon={x})")
54+
raise FieldOutOfBoundError(f"Field sampled out-of-bound, at (z={z}, y={y}, x={x})")
5555

5656

5757
class FieldOutOfBoundSurfaceError(RuntimeError):
@@ -65,7 +65,7 @@ def format_out(val):
6565
return "unknown" if val is None else val
6666

6767
raise FieldOutOfBoundSurfaceError(
68-
f"Field sampled out-of-bound at the surface, at (z={format_out(z)}, lat={format_out(y)}, lon={format_out(x)})"
68+
f"Field sampled out-of-bound at the surface, at (z={format_out(z)}, y={format_out(y)}, x={format_out(x)})"
6969
)
7070

7171

@@ -82,7 +82,7 @@ class GridSearchingError(RuntimeError):
8282

8383

8484
def _raise_grid_searching_error(z, y, x):
85-
raise GridSearchingError(f"Grid searching failed at (z={z}, lat={y}, lon={x})")
85+
raise GridSearchingError(f"Grid searching failed at (z={z}, y={y}, x={x})")
8686

8787

8888
class GeneralError(RuntimeError):
@@ -92,7 +92,7 @@ class GeneralError(RuntimeError):
9292

9393

9494
def _raise_general_error(z, y, x):
95-
raise GeneralError(f"General error occurred at (z={z}, lat={y}, lon={x})")
95+
raise GeneralError(f"General error occurred at (z={z}, y={y}, x={x})")
9696

9797

9898
class OutsideTimeInterval(RuntimeError):

src/parcels/_reprs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ def particleset_repr(pset: ParticleSet) -> str:
9797
def particlesetview_repr(pview: Any) -> str:
9898
"""Return a pretty repr for ParticleSetView"""
9999
time_string = "not_yet_set" if pview.time is None or np.isnan(pview.time) else f"{pview.time:f}"
100-
out = f"P[{pview.particle_id}]: time={time_string}, z={pview.z:f}, lat={pview.lat:f}, lon={pview.lon:f}"
101-
vars = [v.name for v in pview._pclass.variables if v.to_write is True and v.name not in ["lon", "lat", "z", "time"]]
100+
out = f"P[{pview.particle_id}]: time={time_string}, z={pview.z:f}, y={pview.y:f}, x={pview.x:f}"
101+
vars = [v.name for v in pview._pclass.variables if v.to_write is True and v.name not in ["z", "y", "x", "time"]]
102102
for var in vars:
103103
out += f", {var}={getattr(pview, var):f}"
104104

src/parcels/interpolators/_uxinterpolators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def interp(
171171
u = vectorfield.U.interp_method.interp(particle_positions, grid_positions, vectorfield.U)
172172
v = vectorfield.V.interp_method.interp(particle_positions, grid_positions, vectorfield.V)
173173
if vectorfield.grid._mesh == "spherical":
174-
u /= 1852 * 60 * np.cos(np.deg2rad(particle_positions["lat"]))
174+
u /= 1852 * 60 * np.cos(np.deg2rad(particle_positions["y"]))
175175
v /= 1852 * 60
176176

177177
if "3D" in vectorfield.vector_type:

src/parcels/interpolators/_xinterpolators.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ def interp(
132132
field: Field,
133133
):
134134
"""Returning the single value of a Constant Field (with a size=(1,1,1,1) array)"""
135-
return field.data[0, 0, 0, 0].values * np.ones_like(particle_positions["lon"])
135+
return field.data[0, 0, 0, 0].values * np.ones_like(particle_positions["x"])
136136

137137

138138
class XLinear_Velocity(VectorInterpolator): # noqa: N801
@@ -149,7 +149,7 @@ def interp(
149149
u = _xlinear.interp(particle_positions, grid_positions, vectorfield.U)
150150
v = _xlinear.interp(particle_positions, grid_positions, vectorfield.V)
151151
if vectorfield.grid._mesh == "spherical":
152-
u /= 1852 * 60 * np.cos(np.deg2rad(particle_positions["lat"]))
152+
u /= 1852 * 60 * np.cos(np.deg2rad(particle_positions["y"]))
153153
v /= 1852 * 60
154154

155155
if vectorfield.W:
@@ -304,7 +304,7 @@ def _compute_corner_data(data, selection_dict) -> np.ndarray:
304304
v = v.compute()
305305

306306
if grid._mesh == "spherical":
307-
conversion = 1852 * 60.0 * np.cos(np.deg2rad(particle_positions["lat"]))
307+
conversion = 1852 * 60.0 * np.cos(np.deg2rad(particle_positions["y"]))
308308
u /= conversion
309309
v /= conversion
310310

0 commit comments

Comments
 (0)