Skip to content

Commit 7e567fc

Browse files
Merge branch 'v4-dev' into uxinterpolation_tutorial_v4
2 parents 2be8e50 + 049d549 commit 7e567fc

29 files changed

+387
-453
lines changed

docs/getting_started/tutorial_output.ipynb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -540,15 +540,9 @@
540540
"\n",
541541
"# Create animation\n",
542542
"anim = FuncAnimation(fig, animate, frames=nframes, interval=100)\n",
543+
"plt.close(fig)\n",
543544
"anim"
544545
]
545-
},
546-
{
547-
"cell_type": "code",
548-
"execution_count": null,
549-
"metadata": {},
550-
"outputs": [],
551-
"source": []
552546
}
553547
],
554548
"metadata": {

docs/user_guide/examples/explanation_kernelloop.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,12 +78,11 @@ Now we define a wind kernel that uses a forward Euler method to apply the wind f
7878

7979
```{code-cell}
8080
def wind_kernel(particles, fieldset):
81-
dt_float = particles.dt / np.timedelta64(1, 's')
8281
particles.dlon += (
83-
fieldset.UWind[particles] * dt_float
82+
fieldset.UWind[particles] * particles.dt
8483
)
8584
particles.dlat += (
86-
fieldset.VWind[particles] * dt_float
85+
fieldset.VWind[particles] * particles.dt
8786
)
8887
```
8988

docs/user_guide/examples/tutorial_Argofloats.ipynb

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,54 +30,44 @@
3030
"driftdepth = 1000 # maximum depth in m\n",
3131
"maxdepth = 2000 # maximum depth in m\n",
3232
"vertical_speed = 0.10 # sink and rise speed in m/s\n",
33-
"cycletime = (\n",
34-
" 10 * 86400\n",
35-
") # total time of cycle in seconds # TODO update to \"timedelta64[s]\"\n",
33+
"cycletime = 10 * 86400 # total time of cycle in seconds\n",
3634
"drifttime = 9 * 86400 # time of deep drift in seconds\n",
3735
"\n",
3836
"\n",
3937
"def ArgoPhase1(particles, fieldset):\n",
40-
" dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n",
41-
"\n",
4238
" def SinkingPhase(p):\n",
4339
" \"\"\"Phase 0: Sinking with vertical_speed until depth is driftdepth\"\"\"\n",
44-
" p.dz += vertical_speed * dt\n",
40+
" p.dz += vertical_speed * particles.dt\n",
4541
" p.cycle_phase = np.where(p.z + p.dz >= driftdepth, 1, p.cycle_phase)\n",
4642
" p.dz = np.where(p.z + p.dz >= driftdepth, driftdepth - p.z, p.dz)\n",
4743
"\n",
4844
" SinkingPhase(particles[particles.cycle_phase == 0])\n",
4945
"\n",
5046
"\n",
5147
"def ArgoPhase2(particles, fieldset):\n",
52-
" dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n",
53-
"\n",
5448
" def DriftingPhase(p):\n",
5549
" \"\"\"Phase 1: Drifting at depth for drifttime seconds\"\"\"\n",
56-
" p.drift_age += dt\n",
50+
" p.drift_age += particles.dt\n",
5751
" p.cycle_phase = np.where(p.drift_age >= drifttime, 2, p.cycle_phase)\n",
5852
" p.drift_age = np.where(p.drift_age >= drifttime, 0, p.drift_age)\n",
5953
"\n",
6054
" DriftingPhase(particles[particles.cycle_phase == 1])\n",
6155
"\n",
6256
"\n",
6357
"def ArgoPhase3(particles, fieldset):\n",
64-
" dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n",
65-
"\n",
6658
" def SecondSinkingPhase(p):\n",
6759
" \"\"\"Phase 2: Sinking further to maxdepth\"\"\"\n",
68-
" p.dz += vertical_speed * dt\n",
60+
" p.dz += vertical_speed * particles.dt\n",
6961
" p.cycle_phase = np.where(p.z + p.dz >= maxdepth, 3, p.cycle_phase)\n",
7062
" p.dz = np.where(p.z + p.dz >= maxdepth, maxdepth - p.z, p.dz)\n",
7163
"\n",
7264
" SecondSinkingPhase(particles[particles.cycle_phase == 2])\n",
7365
"\n",
7466
"\n",
7567
"def ArgoPhase4(particles, fieldset):\n",
76-
" dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n",
77-
"\n",
7868
" def RisingPhase(p):\n",
7969
" \"\"\"Phase 3: Rising with vertical_speed until at surface\"\"\"\n",
80-
" p.dz -= vertical_speed * dt\n",
70+
" p.dz -= vertical_speed * particles.dt\n",
8171
" p.temp = fieldset.thetao[p.time, p.z, p.lat, p.lon]\n",
8272
" p.cycle_phase = np.where(p.z + p.dz <= fieldset.mindepth, 4, p.cycle_phase)\n",
8373
" p.dz = np.where(\n",
@@ -100,8 +90,7 @@
10090
"\n",
10191
"\n",
10292
"def ArgoPhase6(particles, fieldset):\n",
103-
" dt = particles.dt / np.timedelta64(1, \"s\") # convert dt to seconds\n",
104-
" particles.cycle_age += dt # update cycle_age"
93+
" particles.cycle_age += particles.dt # update cycle_age"
10594
]
10695
},
10796
{
@@ -211,11 +200,7 @@
211200
"x = ds_particles[\"lon\"][:].squeeze()\n",
212201
"y = ds_particles[\"lat\"][:].squeeze()\n",
213202
"z = ds_particles[\"z\"][:].squeeze()\n",
214-
"time = (\n",
215-
" (ds_particles[\"time\"][:].squeeze() - fieldset.time_interval.left).astype(float)\n",
216-
" / 1e9\n",
217-
" / 86400\n",
218-
") # convert time to days\n",
203+
"time = ds_particles[\"time\"][:].squeeze()\n",
219204
"temp = ds_particles[\"temp\"][:].squeeze()"
220205
]
221206
},

docs/user_guide/examples/tutorial_delaystart.ipynb

Lines changed: 13 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@
2424
"metadata": {},
2525
"outputs": [],
2626
"source": [
27-
"from datetime import timedelta\n",
28-
"\n",
2927
"import matplotlib.pyplot as plt\n",
3028
"import numpy as np\n",
3129
"import xarray as xr\n",
32-
"from IPython.display import HTML\n",
3330
"from matplotlib.animation import FuncAnimation\n",
3431
"\n",
35-
"import parcels"
32+
"import parcels\n",
33+
"\n",
34+
"# for interactive display of animations\n",
35+
"plt.rcParams[\"animation.html\"] = \"jshtml\""
3636
]
3737
},
3838
{
@@ -138,8 +138,6 @@
138138
"metadata": {},
139139
"outputs": [],
140140
"source": [
141-
"%%capture\n",
142-
"\n",
143141
"ds_particles = xr.open_zarr(\"delayparticle_time.zarr\")\n",
144142
"\n",
145143
"fig = plt.figure(figsize=(7, 5), constrained_layout=True)\n",
@@ -173,16 +171,9 @@
173171
" )\n",
174172
"\n",
175173
"\n",
176-
"anim = FuncAnimation(fig, animate, frames=len(timerange), interval=100)"
177-
]
178-
},
179-
{
180-
"cell_type": "code",
181-
"execution_count": null,
182-
"metadata": {},
183-
"outputs": [],
184-
"source": [
185-
"HTML(anim.to_jshtml())"
174+
"anim = FuncAnimation(fig, animate, frames=len(timerange), interval=100)\n",
175+
"plt.close()\n",
176+
"anim"
186177
]
187178
},
188179
{
@@ -261,13 +252,13 @@
261252
"outputs": [],
262253
"source": [
263254
"output_file = parcels.ParticleFile(\n",
264-
" \"delayparticle_releasedt.zarr\", outputdt=timedelta(hours=1)\n",
255+
" \"delayparticle_releasedt.zarr\", outputdt=np.timedelta64(1, \"h\")\n",
265256
")\n",
266257
"\n",
267258
"pset.execute(\n",
268259
" parcels.kernels.AdvectionRK4,\n",
269-
" runtime=timedelta(hours=24),\n",
270-
" dt=timedelta(minutes=5),\n",
260+
" runtime=np.timedelta64(24, \"h\"),\n",
261+
" dt=np.timedelta64(5, \"h\"),\n",
271262
" output_file=output_file,\n",
272263
")"
273264
]
@@ -286,8 +277,6 @@
286277
"metadata": {},
287278
"outputs": [],
288279
"source": [
289-
"%%capture\n",
290-
"\n",
291280
"ds_particles = xr.open_zarr(\"delayparticle_releasedt.zarr\")\n",
292281
"\n",
293282
"fig = plt.figure(figsize=(7, 5), constrained_layout=True)\n",
@@ -321,16 +310,9 @@
321310
" )\n",
322311
"\n",
323312
"\n",
324-
"anim = FuncAnimation(fig, animate, frames=len(timerange), interval=100)"
325-
]
326-
},
327-
{
328-
"cell_type": "code",
329-
"execution_count": null,
330-
"metadata": {},
331-
"outputs": [],
332-
"source": [
333-
"HTML(anim.to_jshtml())"
313+
"anim = FuncAnimation(fig, animate, frames=len(timerange), interval=100)\n",
314+
"plt.close(fig)\n",
315+
"anim"
334316
]
335317
},
336318
{

docs/user_guide/examples/tutorial_diffusion.ipynb

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,6 @@
265265
"outputs": [],
266266
"source": [
267267
"testParticles = get_test_particles()\n",
268-
"testParticles.update_dt_dtype(\"timedelta64[ms]\")\n",
269268
"output_file = parcels.ParticleFile(\n",
270269
" store=\"M1_out.zarr\",\n",
271270
" chunks=(len(testParticles), 50),\n",
@@ -345,7 +344,6 @@
345344
"outputs": [],
346345
"source": [
347346
"testParticles = get_test_particles()\n",
348-
"testParticles.update_dt_dtype(\"timedelta64[ms]\")\n",
349347
"output_file = parcels.ParticleFile(\n",
350348
" store=\"EM_out.zarr\",\n",
351349
" chunks=(len(testParticles), 50),\n",
@@ -431,7 +429,6 @@
431429
"outputs": [],
432430
"source": [
433431
"def smagdiff(particles, fieldset):\n",
434-
" dt = particles.dt / np.timedelta64(1, \"s\")\n",
435432
" dx = 0.01\n",
436433
" # gradients are computed by using a local central difference.\n",
437434
" updx, vpdx = fieldset.UV[\n",
@@ -458,8 +455,12 @@
458455
" A = A / sq_deg_to_sq_m\n",
459456
" Kh = fieldset.Cs * A * np.sqrt(dudx**2 + 0.5 * (dudy + dvdx) ** 2 + dvdy**2)\n",
460457
"\n",
461-
" dlat = np.random.normal(0.0, 1.0, dt.shape) * np.sqrt(2 * np.fabs(dt) * Kh)\n",
462-
" dlon = np.random.normal(0.0, 1.0, dt.shape) * np.sqrt(2 * np.fabs(dt) * Kh)\n",
458+
" dlat = np.random.normal(0.0, 1.0, particles.dt.shape) * np.sqrt(\n",
459+
" 2 * np.fabs(particles.dt) * Kh\n",
460+
" )\n",
461+
" dlon = np.random.normal(0.0, 1.0, particles.dt.shape) * np.sqrt(\n",
462+
" 2 * np.fabs(particles.dt) * Kh\n",
463+
" )\n",
463464
" particles.dlat += dlat\n",
464465
" particles.dlon += dlon"
465466
]

docs/user_guide/v4-migration.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ Version 4 of Parcels is unreleased at the moment. The information in this migrat
1010
- `particle.delete()` is no longer valid. Instead, use `particle.state = StatusCode.Delete`.
1111
- Sharing state between kernels must be done via the particle data (as the kernels are not combined under the hood anymore).
1212
- `particl_dlon`, `particle_dlat` etc have been renamed to `particle.dlon` and `particle.dlat`.
13-
- `particle.dt` is a np.timedelta64 object; be careful when multiplying `particle.dt` with a velocity, as its value may be cast to nanoseconds.
1413
- The `time` argument in the Kernel signature has been removed in the Kernel API, so can't be used. Use `particle.time` instead.
1514
- The `particle` argument in the Kernel signature has been renamed to `particles`.
1615
- `math` functions should be replaced with array compatible equivalents (e.g., `math.sin` -> `np.sin`). Instead of `ParcelsRandom` you should use numpy's random functions.
@@ -27,7 +26,7 @@ Version 4 of Parcels is unreleased at the moment. The information in this migrat
2726
## ParticleSet
2827

2928
- `repeatdt` and `lonlatdepth_dtype` have been removed from the ParticleSet.
30-
- ParticleSet.execute() expects `numpy.datetime64`/`numpy.timedelta.64` for `runtime`, `endtime` and `dt`.
29+
- ParticleSet.execute() expects `numpy.datetime64`/`numpy.timedelta.64` for `endtime`. While floats are supported for `runtime` and `dt`, using `numpy.datetime64`/`numpy.timedelta.64` for these arguments too is encouraged.
3130
- `ParticleSet.from_field()`, `ParticleSet.from_line()`, `ParticleSet.from_list()` have been removed.
3231

3332
## ParticleFile

src/parcels/_core/index_search.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import numpy as np
77

88
from parcels._core.statuscodes import _raise_outside_time_interval_error
9+
from parcels._core.utils.time import timedelta_to_float
910

1011
if TYPE_CHECKING:
1112
from parcels._core.field import Field
@@ -85,7 +86,8 @@ def _search_time_index(field: Field, time: datetime):
8586
if not field.time_interval.is_all_time_in_interval(time):
8687
_raise_outside_time_interval_error(time, field=None)
8788

88-
ti, tau = _search_1d_array(field.data.time.data, time)
89+
time_flt = timedelta_to_float(field.data.time.data - field.time_interval.left)
90+
ti, tau = _search_1d_array(time_flt, time)
8991

9092
return {"T": {"index": np.atleast_1d(ti), "bcoord": np.atleast_1d(tau)}}
9193

src/parcels/_core/kernel.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ def PositionUpdate(particles, fieldset): # pragma: no cover
123123
particles.dlat = 0
124124
particles.dz = 0
125125

126+
if hasattr(self.fieldset, "RK45_tol"):
127+
# Update dt in case it's increased in RK45 kernel
128+
particles.dt = particles.next_dt
129+
126130
self._pyfuncs = (PositionUpdate + self)._pyfuncs
127131

128132
def check_fieldsets_in_kernels(self, pyfunc): # TODO v4: this can go into another method? assert_is_compatible()?
@@ -138,6 +142,8 @@ def check_fieldsets_in_kernels(self, pyfunc): # TODO v4: this can go into anoth
138142
if self._fieldset.U.grid._gtype not in [GridType.CurvilinearZGrid, GridType.RectilinearZGrid]:
139143
raise NotImplementedError("Analytical Advection only works with Z-grids in the vertical")
140144
elif pyfunc is AdvectionRK45:
145+
if "next_dt" not in [v.name for v in self.ptype.variables]:
146+
raise ValueError('ParticleClass requires a "next_dt" for AdvectionRK45 Kernel.')
141147
if not hasattr(self.fieldset, "RK45_tol"):
142148
warnings.warn(
143149
"Setting RK45 tolerance to 10 m. Use fieldset.add_constant('RK45_tol', [distance]) to change.",

src/parcels/_core/particle.py

Lines changed: 6 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from __future__ import annotations
22

3-
import enum
43
import operator
54
from typing import Literal
65

@@ -15,8 +14,6 @@
1514
__all__ = ["KernelParticle", "Particle", "ParticleClass", "Variable"]
1615
_TO_WRITE_OPTIONS = [True, False, "once"]
1716

18-
_SAME_AS_FIELDSET_TIME_INTERVAL = enum.Enum("_SAME_AS_FIELDSET_TIME_INTERVAL", "VALUE")
19-
2017

2118
class Variable:
2219
"""Descriptor class that delegates data access to particle data.
@@ -40,7 +37,7 @@ class Variable:
4037
def __init__(
4138
self,
4239
name,
43-
dtype: np.dtype | _SAME_AS_FIELDSET_TIME_INTERVAL = np.float32,
40+
dtype: np.dtype = np.float32,
4441
initial=0,
4542
to_write: bool | Literal["once"] = True,
4643
attrs: dict | None = None,
@@ -50,8 +47,7 @@ def __init__(
5047
try:
5148
dtype = np.dtype(dtype)
5249
except (TypeError, ValueError) as e:
53-
if dtype is not _SAME_AS_FIELDSET_TIME_INTERVAL.VALUE:
54-
raise TypeError(f"Variable dtype must be a valid numpy dtype. Got {dtype=!r}") from e
50+
raise TypeError(f"Variable dtype must be a valid numpy dtype. Got {dtype=!r}") from e
5551

5652
if to_write not in _TO_WRITE_OPTIONS:
5753
raise ValueError(f"to_write must be one of {_TO_WRITE_OPTIONS!r}. Got {to_write=!r}")
@@ -177,7 +173,7 @@ def get_default_particle(spatial_dtype: np.float32 | np.float64) -> ParticleClas
177173
Variable("dz", dtype=spatial_dtype, to_write=False),
178174
Variable(
179175
"time",
180-
dtype=_SAME_AS_FIELDSET_TIME_INTERVAL.VALUE,
176+
dtype=np.float64,
181177
attrs={"standard_name": "time", "units": "seconds", "axis": "T"},
182178
),
183179
Variable(
@@ -190,7 +186,7 @@ def get_default_particle(spatial_dtype: np.float32 | np.float64) -> ParticleClas
190186
},
191187
),
192188
Variable("obs_written", dtype=np.int32, initial=0, to_write=False),
193-
Variable("dt", dtype="timedelta64[s]", initial=np.timedelta64(1, "s"), to_write=False),
189+
Variable("dt", dtype=np.float64, initial=1.0, to_write=False),
194190
Variable("state", dtype=np.int32, initial=StatusCode.Evaluate, to_write=False),
195191
]
196192
)
@@ -214,14 +210,7 @@ def create_particle_data(
214210

215211
assert "ei" not in initial, "'ei' is for internal use, and is unique since is only non 1D array"
216212

217-
time_interval_dtype = _get_time_interval_dtype(time_interval)
218-
219-
dtypes = {}
220-
for var in variables.values():
221-
if var.dtype is _SAME_AS_FIELDSET_TIME_INTERVAL.VALUE:
222-
dtypes[var.name] = time_interval_dtype
223-
else:
224-
dtypes[var.name] = var.dtype
213+
dtypes = {var.name: var.dtype for var in variables.values()}
225214

226215
for var_name in initial:
227216
if var_name not in variables:
@@ -250,20 +239,8 @@ def _create_array_for_variable(variable: Variable, nparticles: int, time_interva
250239
assert not isinstance(variable.initial, operator.attrgetter), (
251240
"This function cannot handle attrgetter initial values."
252241
)
253-
if (dtype := variable.dtype) is _SAME_AS_FIELDSET_TIME_INTERVAL.VALUE:
254-
dtype = _get_time_interval_dtype(time_interval)
255242
return np.full(
256243
shape=(nparticles,),
257244
fill_value=variable.initial,
258-
dtype=dtype,
245+
dtype=variable.dtype,
259246
)
260-
261-
262-
def _get_time_interval_dtype(time_interval: TimeInterval | None) -> np.dtype:
263-
if time_interval is None:
264-
return np.timedelta64(1, "ns")
265-
time = time_interval.left
266-
if isinstance(time, (np.datetime64, np.timedelta64)):
267-
return time.dtype
268-
else:
269-
return object # cftime objects needs to be stored as object dtype

0 commit comments

Comments
 (0)