Skip to content

Commit cf758ce

Browse files
authored
Merge pull request #290 from OceanParcels/Write_particledata_on_delete
Write particledata on delete
2 parents 1ecc9c5 + 152c733 commit cf758ce

5 files changed

Lines changed: 49 additions & 10 deletions

File tree

parcels/kernel.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,15 @@ def execute_python(self, pset, endtime, dt):
206206
else:
207207
break # Failure - stop time loop
208208

209-
def execute(self, pset, endtime, dt, recovery=None):
209+
def execute(self, pset, endtime, dt, recovery=None, output_file=None):
210210
"""Execute this Kernel over a ParticleSet for several timesteps"""
211211

212212
def remove_deleted(pset):
213213
"""Utility to remove all particles that signalled deletion"""
214214
indices = [i for i, p in enumerate(pset.particles)
215215
if p.state in [ErrorCode.Delete]]
216+
if len(indices) > 0 and output_file is not None:
217+
output_file.write(pset[indices], endtime, deleted_only=True)
216218
pset.remove(indices)
217219

218220
if recovery is None:

parcels/particlefile.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,18 @@ class ParticleFile(object):
3232
while ParticleFile is given as an argument of ParticleSet.execute()
3333
It is either a timedelta object or a positive double.
3434
:param type: Either 'array' for default matrix style, or 'indexed' for indexed ragged array
35+
:param write_ondelete: Boolean to write particle data only when they are deleted. Default is False
3536
"""
3637

37-
def __init__(self, name, particleset, outputdt=np.infty, type='array'):
38+
def __init__(self, name, particleset, outputdt=np.infty, type='array', write_ondelete=False):
3839

3940
self.type = type
4041
self.name = name
42+
self.write_ondelete = write_ondelete
43+
self.outputdt = outputdt
44+
if self.write_ondelete and self.type is 'array':
45+
logger.warning('ParticleFile.write_ondelete=True requires type="indexed". Setting that option')
46+
self.type = 'indexed'
4147
self.outputdt = outputdt
4248
self.lasttime_written = None # variable to check if time has been written already
4349
self.dataset = netCDF4.Dataset("%s.nc" % name, "w", format="NETCDF4")
@@ -122,7 +128,7 @@ def sync(self):
122128
"""Write all buffered data to disk"""
123129
self.dataset.sync()
124130

125-
def write(self, pset, time, sync=True):
131+
def write(self, pset, time, sync=True, deleted_only=False):
126132
"""Write :class:`parcels.particleset.ParticleSet` data to file
127133
128134
:param pset: ParticleSet object to write
@@ -132,14 +138,17 @@ def write(self, pset, time, sync=True):
132138
"""
133139
if isinstance(time, delta):
134140
time = time.total_seconds()
135-
if self.lasttime_written != time: # only write if 'time' hasn't been written yet
141+
if self.lasttime_written != time and \
142+
(self.write_ondelete is False or deleted_only is True):
143+
136144
self.lasttime_written = time
137145
if self.type is 'array':
138146
# Check if largest particle ID is smaller than the last ID in ParticleFile.
139147
# Otherwise, new particles have been added and netcdf will fail
140148
if pset.size > 0:
141149
if max([p.id for p in pset]) > self.id[-1]:
142150
logger.error("Number of particles appears to increase. Use type='indexed' for ParticleFile")
151+
exit(-1)
143152

144153
# Finds the indices (inds) of the particle IDs in the ParticleFile,
145154
# because particles can have been deleted
@@ -165,7 +174,7 @@ def write(self, pset, time, sync=True):
165174
logger.warning("Option to_write='once' is not fully functional in indexed mode! Particle properties of such variables are not written for newly added particles.")
166175
ind = np.arange(pset.size) + self.idx
167176
self.id[ind] = np.array([p.id for p in pset])
168-
self.time[ind] = time
177+
self.time[ind] = np.array([p.time for p in pset])
169178
self.lat[ind] = np.array([p.lat for p in pset])
170179
self.lon[ind] = np.array([p.lon for p in pset])
171180
self.z[ind] = np.array([p.depth for p in pset])

parcels/particleset.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,7 @@ def execute(self, pyfunc=AdvectionRK4, endtime=None, runtime=None, dt=1.,
333333
time = min(next_prelease, next_input, next_output, next_movie, endtime)
334334
else:
335335
time = max(next_prelease, next_input, next_output, next_movie, endtime)
336-
self.kernel.execute(self, endtime=time, dt=dt, recovery=recovery)
336+
self.kernel.execute(self, endtime=time, dt=dt, recovery=recovery, output_file=output_file)
337337
if abs(time-next_prelease) < tol:
338338
self.add(ParticleSet(fieldset=self.fieldset, time=time, lon=self.repeatlon,
339339
lat=self.repeatlat, depth=self.repeatdepth,

tests/test_kernel_execution.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@ def test_execution_runtime(fieldset, mode, start, end, substeps, dt, npart=10):
5454
pset = ParticleSet(fieldset, pclass=ptype[mode], time=start,
5555
lon=np.linspace(0, 1, npart, dtype=np.float32),
5656
lat=np.linspace(1, 0, npart, dtype=np.float32))
57-
t_step = (end - start) / substeps
57+
t_step = abs(end - start) / substeps
5858
for _ in range(substeps):
59-
pset.execute(DoNothing, runtime=abs(t_step), dt=dt)
60-
start += t_step
59+
pset.execute(DoNothing, runtime=t_step, dt=dt)
6160
assert np.allclose(np.array([p.time for p in pset]), end)
6261

6362

tests/test_particle_sets.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from parcels import FieldSet, ParticleSet, Field, ScipyParticle, JITParticle, Variable
1+
from parcels import (FieldSet, ParticleSet, Field, ScipyParticle, JITParticle,
2+
Variable, ErrorCode)
23
import numpy as np
34
import pytest
45
from netCDF4 import Dataset
@@ -329,3 +330,31 @@ class MyParticle(ptype[mode]):
329330
assert np.all([p.v_once == 11.0 for p in pset])
330331
assert (V_once.shape == (npart, ))
331332
assert (V_once[0] == 1.)
333+
334+
335+
@pytest.mark.parametrize('mode', ['scipy', 'jit'])
336+
def test_variable_written_ondelete(fieldset, mode, tmpdir, npart=3):
337+
filepath = tmpdir.join("pfile_on_delete_written_variables")
338+
339+
def move_west(particle, fieldset, time, dt):
340+
tmp = fieldset.U[time, particle.lon, particle.lat, particle.depth] # to trigger out-of-bounds error
341+
particle.lon -= 0.1 + tmp
342+
343+
def DeleteP(particle, fieldset, time, dt):
344+
particle.delete()
345+
346+
lon = np.linspace(0.05, 0.95, npart, dtype=np.float32)
347+
lat = np.linspace(0.95, 0.05, npart, dtype=np.float32)
348+
349+
(dt, runtime) = (0.1, 0.8)
350+
lon_end = lon - runtime/dt*0.1
351+
noutside = len(lon_end[lon_end < 0])
352+
353+
pset = ParticleSet(fieldset, pclass=ptype[mode], lon=lon, lat=lat)
354+
355+
outfile = pset.ParticleFile(name=filepath, write_ondelete=True, type="indexed")
356+
pset.execute(move_west, runtime=runtime, dt=dt, output_file=outfile,
357+
recovery={ErrorCode.ErrorOutOfBounds: DeleteP})
358+
ncfile = Dataset(filepath+".nc", 'r', 'NETCDF4')
359+
lon = ncfile.variables['lon'][:]
360+
assert (lon.size == noutside)

0 commit comments

Comments
 (0)