From 64c4b4d087ed908a9c0df0cb98cb363138f7e478 Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:09:12 +0200 Subject: [PATCH 1/2] Add test --- tests/test_particlefile.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_particlefile.py b/tests/test_particlefile.py index 3100f27a9..5b9546c92 100755 --- a/tests/test_particlefile.py +++ b/tests/test_particlefile.py @@ -467,6 +467,23 @@ def test_pfile_write_custom_particle(): ... +def test_particlefile_readable_after_kernel_error(fieldset, tmp_parquet): + """Parquet output file must be readable even if the kernel raises an error mid-execution (GH-2713).""" + + def ErrorKernel(particles, fieldset): # pragma: no cover + particles.state = StatusCode.Error + + pset = ParticleSet(fieldset, pclass=Particle, x=0, y=0) + ofile = ParticleFile(tmp_parquet, outputdt=np.timedelta64(1, "s")) + + with pytest.raises(RuntimeError, match="General error occurred at"): + pset.execute(ErrorKernel, runtime=np.timedelta64(10, "s"), dt=np.timedelta64(1, "s"), output_file=ofile) + + # File must be readable despite the crash + df = pd.read_parquet(tmp_parquet) + assert len(df) >= 1 # at least the initial condition was written + + @pytest.mark.xfail( reason="set_variable_write_status should be removed - with Particle writing defined on the particle level. GH2186" ) From b8f921204a7446c10b0a70c9c6b91d96995ca6eb Mon Sep 17 00:00:00 2001 From: Vecko <36369090+VeckoTheGecko@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:03:42 +0200 Subject: [PATCH 2/2] BUG: Fix corrupt ParticleFile writing --- src/parcels/_core/particlefile.py | 6 ++++ src/parcels/_core/particleset.py | 50 +++++++++++++++---------------- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/parcels/_core/particlefile.py b/src/parcels/_core/particlefile.py index 0c9173bdf..7809aa6a2 100644 --- a/src/parcels/_core/particlefile.py +++ b/src/parcels/_core/particlefile.py @@ -183,6 +183,12 @@ def close(self): self._writer.close() self._writer = None + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + def _get_vars_to_write(particle: ParticleClass) -> list[Variable]: return [v for v in particle.variables if v.to_write is not False] diff --git a/src/parcels/_core/particleset.py b/src/parcels/_core/particleset.py index 5280d6767..450cf57b9 100644 --- a/src/parcels/_core/particleset.py +++ b/src/parcels/_core/particleset.py @@ -3,6 +3,7 @@ import types import warnings from collections.abc import Iterable +from contextlib import nullcontext from typing import Literal import numpy as np @@ -433,32 +434,31 @@ def execute( next_output = start_time + outputdt * sign_dt time = start_time - while sign_dt * (time - end_time) < 0: - if next_output is not None: - f = min if sign_dt > 0 else max - next_time = f(next_output, end_time) - else: - next_time = end_time - - self._kernel.execute(self, endtime=next_time, dt=dt) - - if next_output is not None: - if np.abs(next_time - next_output) < 0.001: - if output_file: - output_file.write(self, next_output) - if np.isfinite(outputdt): - next_output += outputdt * sign_dt - if verbose_progress: - pbar.set_description_str( - "Integration time: " + str(float_to_datelike(time, self.fieldset.time_interval)) - ) - pbar.update(sign_dt * (next_time - time)) - - time = next_time - - if output_file is not None: - output_file.close() + with output_file if output_file is not None else nullcontext(): + while sign_dt * (time - end_time) < 0: + if next_output is not None: + f = min if sign_dt > 0 else max + next_time = f(next_output, end_time) + else: + next_time = end_time + + self._kernel.execute(self, endtime=next_time, dt=dt) + + if next_output is not None: + if np.abs(next_time - next_output) < 0.001: + if output_file: + output_file.write(self, next_output) + if np.isfinite(outputdt): + next_output += outputdt * sign_dt + + if verbose_progress: + pbar.set_description_str( + "Integration time: " + str(float_to_datelike(time, self.fieldset.time_interval)) + ) + pbar.update(sign_dt * (next_time - time)) + + time = next_time if verbose_progress: pbar.close()