Skip to content

Commit 6318821

Browse files
Changing pid_orig to trajectory_id in pset init
1 parent 97adb3b commit 6318821

3 files changed

Lines changed: 31 additions & 90 deletions

File tree

docs/v4/api.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,11 @@ Here, important things to note are:
4343
- The `grid` object, in the case of unstructured grids, will be the `Grid` class from UXarray. For structured `Grid`s, it will be an object similar to that of `xgcm.Grid` (note that it will be very different from the v3 `Grid` object hierarchy).
4444

4545
- The `Field.eval` method takes as input the t,z,y,x spatio-temporal position as required arguments; the `particle` is optional and defaults to `None` and the `applyConversion` argument is optional and defaults to `True`. Initially, we will calculate the element index for a particle. As a future optimization, we could pass via the `particle` object a "cached" index value that could be used to bypass an index search. This will effectively provide `(ti,zi,yi,xi)` on a structured grid and `(ti,zi,fi)` on an unstructured grid (where `fi` is the lateral face id); within `eval` these indices will be `ravel`'ed to a single index that can be `unravel`'ed in the `interpolate` method. The `ravel`'ed index is referred to as `rid` in the `Field.Interpolator.interpolate` method. In the `interpolate` method, we envision that a user will benefit from knowing the nearest cell/index from the `ravel`'ed index (which can be `unravel`'ed) in addition the exact coordinate that we want to interpolate onto. This can permit calculation of interpolation weights using points in the neighborhood of `(t,z,y,x)`.
46+
47+
## Changes in API
48+
49+
Below a list of changes in the API that are relevant to users:
50+
51+
- `starttime`, `endtime` and `dt` in `ParticleSet.execute()` are now `numpy.timedelta64` or `numpy.datetime64` objects. This allows for more precise time handling and is consistent with the `numpy` time handling.
52+
53+
- `pid_orig` in `ParticleSet` is removed. Instead, `trajectory_ids` is used to provide a list of "trajectory" values (integers) for the particle IDs.

parcels/particleset.py

Lines changed: 14 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ class ParticleSet:
5252
Floating precision for lon, lat, depth particle coordinates.
5353
It is either np.float32 or np.float64. Default is np.float32 if fieldset.U.interp_method is 'linear'
5454
and np.float64 if the interpolation method is 'cgrid_velocity'
55-
pid_orig :
56-
Optional list of (offsets for) the particle IDs
55+
trajectory_ids :
56+
Optional list of "trajectory" values (integers) for the particle IDs
5757
partition_function :
5858
Function to use for partitioning particles over processors. Default is to use kMeans
5959
periodic_domain_zonal :
@@ -73,7 +73,7 @@ def __init__(
7373
time=None,
7474
repeatdt=None,
7575
lonlatdepth_dtype=None,
76-
pid_orig=None,
76+
trajectory_ids=None,
7777
**kwargs,
7878
):
7979
self._data = None
@@ -96,8 +96,8 @@ def __init__(
9696
lat = np.empty(shape=0) if lat is None else convert_to_flat_array(lat)
9797
time = np.empty(shape=0) if time is None else convert_to_flat_array(time)
9898

99-
if isinstance(pid_orig, (type(None), bool)):
100-
pid_orig = np.arange(lon.size)
99+
if isinstance(trajectory_ids, (type(None), bool)):
100+
trajectory_ids = np.arange(lon.size)
101101

102102
if depth is None:
103103
mindepth = 0
@@ -142,15 +142,15 @@ def __init__(
142142
"lat": (["trajectory"], lat.astype(lonlatdepth_dtype)),
143143
"depth": (["trajectory"], depth.astype(lonlatdepth_dtype)),
144144
"time": (["trajectory"], time),
145-
"dt": (["trajectory"], np.timedelta64(1, "ns") * np.ones(len(pid_orig))),
146-
"state": (["trajectory"], np.zeros((len(pid_orig)), dtype=np.int32)),
145+
"dt": (["trajectory"], np.timedelta64(1, "ns") * np.ones(len(trajectory_ids))),
146+
"state": (["trajectory"], np.zeros((len(trajectory_ids)), dtype=np.int32)),
147147
"lon_nextloop": (["trajectory"], lon.astype(lonlatdepth_dtype)),
148148
"lat_nextloop": (["trajectory"], lat.astype(lonlatdepth_dtype)),
149149
"depth_nextloop": (["trajectory"], depth.astype(lonlatdepth_dtype)),
150150
"time_nextloop": (["trajectory"], time),
151151
},
152152
coords={
153-
"trajectory": ("trajectory", pid_orig),
153+
"trajectory": ("trajectory", trajectory_ids),
154154
},
155155
attrs={
156156
"ngrid": len(fieldset.gridset),
@@ -164,7 +164,7 @@ def __init__(
164164
if isinstance(v.initial, attrgetter):
165165
initial = v.initial(self).values
166166
else:
167-
initial = v.initial * np.ones(len(pid_orig), dtype=v.dtype)
167+
initial = v.initial * np.ones(len(trajectory_ids), dtype=v.dtype)
168168
self._data[v.name] = (["trajectory"], initial)
169169

170170
# update initial values provided on ParticleSet creation
@@ -213,6 +213,7 @@ def __getitem__(self, index):
213213

214214
@staticmethod
215215
def lonlatdepth_dtype_from_field_interp_method(field):
216+
# TODO update this when now interp methods are implemented
216217
if field.interp_method == "cgrid_velocity":
217218
return np.float64
218219
return np.float32
@@ -557,9 +558,7 @@ def from_field(
557558
)
558559

559560
@classmethod
560-
def from_particlefile(
561-
cls, fieldset, pclass, filename, restart=True, restarttime=None, repeatdt=None, lonlatdepth_dtype=None, **kwargs
562-
):
561+
def from_particlefile(cls, fieldset, pclass, filename, restart=True, restarttime=None, repeatdt=None, **kwargs):
563562
"""Initialise the ParticleSet from a zarr ParticleFile.
564563
This creates a new ParticleSet based on locations of all particles written
565564
in a zarr ParticleFile at a certain time. Particle IDs are preserved if restart=True
@@ -581,87 +580,12 @@ def from_particlefile(
581580
a callable function such as np.nanmin. The last is useful when running with dt < 0.
582581
repeatdt : datetime.timedelta or float, optional
583582
Optional interval on which to repeat the release of the ParticleSet. Either timedelta object, or float in seconds.
584-
lonlatdepth_dtype :
585-
Floating precision for lon, lat, depth particle coordinates.
586-
It is either np.float32 or np.float64. Default is np.float32 if fieldset.U.interp_method is 'linear'
587-
and np.float64 if the interpolation method is 'cgrid_velocity'
588583
**kwargs :
589584
Keyword arguments passed to the particleset constructor.
590585
"""
591-
if repeatdt is not None:
592-
warnings.warn(
593-
f"Note that the `repeatdt` argument is not retained from {filename}, and that "
594-
"setting a new repeatdt will start particles from the _new_ particle "
595-
"locations.",
596-
ParticleSetWarning,
597-
stacklevel=2,
598-
)
599-
600-
pfile = xr.open_zarr(str(filename))
601-
pfile_vars = [v for v in pfile.data_vars]
602-
603-
vars = {}
604-
to_write = {}
605-
for v in pclass.getPType().variables:
606-
if v.name in pfile_vars:
607-
vars[v.name] = np.ma.filled(pfile.variables[v.name], np.nan)
608-
elif (
609-
v.name
610-
not in [
611-
"ei",
612-
"dt",
613-
"depth",
614-
"id",
615-
"obs_written",
616-
"state",
617-
"lon_nextloop",
618-
"lat_nextloop",
619-
"depth_nextloop",
620-
"time_nextloop",
621-
]
622-
and v.to_write
623-
):
624-
raise RuntimeError(f"Variable {v.name} is in pclass but not in the particlefile")
625-
to_write[v.name] = v.to_write
626-
vars["depth"] = np.ma.filled(pfile.variables["z"], np.nan)
627-
vars["id"] = np.ma.filled(pfile.variables["trajectory"], np.nan)
628-
629-
for v in ["lon", "lat", "depth", "time"]:
630-
to_write[v] = True
631-
632-
if isinstance(vars["time"][0, 0], np.timedelta64):
633-
vars["time"] = np.array([t / np.timedelta64(1, "s") for t in vars["time"]])
634-
635-
if restarttime is None:
636-
restarttime = np.nanmax(vars["time"])
637-
elif callable(restarttime):
638-
restarttime = restarttime(vars["time"])
639-
else:
640-
restarttime = restarttime
641-
642-
inds = np.where(vars["time"] == restarttime)
643-
for v in vars:
644-
if to_write[v] is True:
645-
vars[v] = vars[v][inds]
646-
elif to_write[v] == "once":
647-
vars[v] = vars[v][inds[0]]
648-
if v not in ["lon", "lat", "depth", "time", "id"]:
649-
kwargs[v] = vars[v]
650-
651-
vars["id"] = None
652-
653-
return cls(
654-
fieldset=fieldset,
655-
pclass=pclass,
656-
lon=vars["lon"],
657-
lat=vars["lat"],
658-
depth=vars["depth"],
659-
time=vars["time"],
660-
pid_orig=vars["id"],
661-
lonlatdepth_dtype=lonlatdepth_dtype,
662-
repeatdt=repeatdt,
663-
**kwargs,
664-
)
586+
raise NotImplementedError(
587+
"ParticleSet.from_particlefile is not yet implemented in v4."
588+
) # TODO implement this when ParticleFile is implemented in v4
665589

666590
def Kernel(self, pyfunc):
667591
"""Wrapper method to convert a `pyfunc` into a :class:`parcels.kernel.Kernel` object.

tests/v4/test_particleset.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ def test_create_empty_pset(fieldset):
5858
assert pset.size == 0
5959

6060

61+
@pytest.mark.parametrize("offset", [0, 1, 200])
62+
def test_pset_with_pids(fieldset, offset, npart=100):
63+
lon = np.linspace(0, 1, npart)
64+
lat = np.linspace(1, 0, npart)
65+
trajectory_ids = np.arange(offset, npart + offset)
66+
pset = ParticleSet(fieldset, lon=lon, lat=lat, trajectory_ids=trajectory_ids)
67+
assert np.allclose([p.trajectory for p in pset], trajectory_ids, atol=1e-12)
68+
69+
6170
def test_pset_custominit_on_pset(fieldset):
6271
MyParticle = Particle.add_variable("sample_var")
6372

0 commit comments

Comments
 (0)