From 5216c6ee335099cbcdec9bf9ed0a75b860705f20 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 29 Oct 2026 08:12:43 +0100 Subject: [PATCH 1/2] Cleaning up particleset.__repr__ Improving readability and extending unit tests --- src/parcels/_core/particleset.py | 5 +-- src/parcels/_repr_utils.py | 19 +++++++++--- tests/test_particle.py | 10 +++--- tests/test_particleset.py | 53 ++++++++++++++++++++++++++++++-- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/parcels/_core/particleset.py b/src/parcels/_core/particleset.py index 075403b9ca..192f483ff8 100644 --- a/src/parcels/_core/particleset.py +++ b/src/parcels/_core/particleset.py @@ -23,6 +23,7 @@ from parcels._core.warnings import ParticleSetWarning from parcels._core.xgrid import XGrid from parcels._logger import logger +from parcels._repr_utils import particleset_repr __all__ = ["ParticleSet"] @@ -190,8 +191,8 @@ def __setattr__(self, name, value): def size(self): return len(self) - # def __repr__(self): - # return particleset_repr(self) + def __repr__(self): + return particleset_repr(self) def __len__(self): return len(self._data["particle_id"]) diff --git a/src/parcels/_repr_utils.py b/src/parcels/_repr_utils.py index 929a21a1d4..80b5485e0a 100644 --- a/src/parcels/_repr_utils.py +++ b/src/parcels/_repr_utils.py @@ -104,11 +104,17 @@ def particleset_repr(pset: ParticleSet) -> str: def particlesetview_repr(pview: Any) -> str: """Return a pretty repr for ParticleSetView""" - time_string = "not_yet_set" if pview.t is None or np.isnan(pview.t) else f"{pview.t:f}" - out = f"P[{pview.particle_id}]: time={time_string}, z={pview.z:f}, y={pview.y:f}, x={pview.x:f}" - vars = [v.name for v in pview._pclass.variables if v.to_write is True and v.name not in ["z", "y", "x", "time"]] + from parcels._core.utils.time import float_to_datelike + + t = pview.t + if t is None or np.isnan(t): + time_string = str(t) + else: + time_string = str(float_to_datelike(t, pview._time_interval)) + out = f"P[{pview.particle_id}]: t={time_string}, z={pview.z:f}, y={pview.y:f}, x={pview.x:f}" + vars = [v.name for v in pview._pclass.variables if v.to_write is True and v.name not in ["t", "z", "y", "x"]] for var in vars: - out += f", {var}={getattr(pview, var):f}" + out += f", {var}={getattr(pview, var)}" return textwrap.dedent(out).strip() @@ -124,7 +130,10 @@ def particleclass_repr(pclass: Any) -> str: def variable_repr(var: Any) -> str: """Return a pretty repr for Variable""" - return f"Variable(name={var._name!r}, dtype={var.dtype!r}, initial={var.initial!r}, to_write={var.to_write!r}, attrs={var.attrs!r})" + out = f"Variable(name={var._name!r}, dtype={np.dtype(var.dtype).name!r}, initial={var.initial!r}, to_write={var.to_write!r}" + if getattr(var, "attrs", None): + out += f", attrs={var.attrs!r}" + return out + ")" def timeinterval_repr(ti: Any) -> str: diff --git a/tests/test_particle.py b/tests/test_particle.py index 62eb65cffc..9a344d1eac 100644 --- a/tests/test_particle.py +++ b/tests/test_particle.py @@ -37,11 +37,11 @@ def test_variable_invalid_init(): [ ( Variable("test", np.float32, 0.0, True, {"some": "metadata"}), - "Variable(name='test', dtype=dtype('float32'), initial=0.0, to_write=True, attrs={'some': 'metadata'})", + "Variable(name='test', dtype='float32', initial=0.0, to_write=True, attrs={'some': 'metadata'})", ), ( Variable("test", np.float32, 0.0, True), - "Variable(name='test', dtype=dtype('float32'), initial=0.0, to_write=True, attrs={})", + "Variable(name='test', dtype='float32', initial=0.0, to_write=True)", ), ], ) @@ -78,9 +78,9 @@ def test_particleclass_invalid_vars(): Variable("varc", dtype=np.float32, to_write=True), ] ), - """Variable(name='vara', dtype=dtype('float32'), initial=0, to_write=True, attrs={}) -Variable(name='varb', dtype=dtype('float32'), initial=0, to_write=False, attrs={}) -Variable(name='varc', dtype=dtype('float32'), initial=0, to_write=True, attrs={})""", + """Variable(name='vara', dtype='float32', initial=0, to_write=True) +Variable(name='varb', dtype='float32', initial=0, to_write=False) +Variable(name='varc', dtype='float32', initial=0, to_write=True)""", ), ], ) diff --git a/tests/test_particleset.py b/tests/test_particleset.py index 18493a85a1..66f6a99729 100644 --- a/tests/test_particleset.py +++ b/tests/test_particleset.py @@ -5,7 +5,6 @@ import numpy as np import pytest import xarray as xr -from re_assert import Matches from parcels import ( FieldSet, @@ -37,7 +36,57 @@ def test_create_empty_pset(fieldset): def test_particleset_repr(fieldset): - Matches(r"\<.*ParticleSet object at.*\>").assert_matches(repr(ParticleSet(fieldset, pclass=Particle))) + npart = 100 + pset = ParticleSet(fieldset, x=np.linspace(0, 1, npart), y=np.ones(npart)) + expected = """\ + + Number of particles: 100 + Particles: + P[0]: t=nan, z=0.000000, y=1.000000, x=0.000000, particle_id=0 + P[1]: t=nan, z=0.000000, y=1.000000, x=0.010101, particle_id=1 + P[2]: t=nan, z=0.000000, y=1.000000, x=0.020202, particle_id=2 + P[3]: t=nan, z=0.000000, y=1.000000, x=0.030303, particle_id=3 + P[4]: t=nan, z=0.000000, y=1.000000, x=0.040404, particle_id=4 + P[5]: t=nan, z=0.000000, y=1.000000, x=0.050505, particle_id=5 + P[6]: t=nan, z=0.000000, y=1.000000, x=0.060606, particle_id=6 + ... + P[99]: t=nan, z=0.000000, y=1.000000, x=1.000000, particle_id=99 + Pclass: + Variable(name='t', dtype='float64', initial=0, to_write=True, attrs={'standard_name': 'time', 'units': 'seconds', 'axis': 'T'}) + Variable(name='z', dtype='float32', initial=0, to_write=True, attrs={'standard_name': 'vertical coordinate', 'units': 'm', 'positive': 'down'}) + Variable(name='y', dtype='float32', initial=0, to_write=True, attrs={'standard_name': 'latitude', 'units': 'degrees_north', 'axis': 'Y'}) + Variable(name='x', dtype='float32', initial=0, to_write=True, attrs={'standard_name': 'longitude', 'units': 'degrees_east', 'axis': 'X'}) + Variable(name='dz', dtype='float32', initial=0, to_write=False) + Variable(name='dy', dtype='float32', initial=0, to_write=False) + Variable(name='dx', dtype='float32', initial=0, to_write=False) + Variable(name='particle_id', dtype='int64', initial=0, to_write=True, attrs={'long_name': 'Unique identifier for each particle', 'cf_role': 'trajectory_id'}) + Variable(name='dt', dtype='float64', initial=1.0, to_write=False) + Variable(name='state', dtype='int32', initial=0, to_write=False)""" + assert str(pset) == expected + + npart = 5 + pset = ParticleSet(fieldset, x=np.linspace(0, 1, npart), y=np.ones(npart), t=npart * [np.timedelta64(0, "s")]) + expected = """\ + + Number of particles: 5 + Particles: + P[0]: t=2000-01-01T00:00:00, z=0.000000, y=1.000000, x=0.000000, particle_id=0 + P[1]: t=2000-01-01T00:00:00, z=0.000000, y=1.000000, x=0.250000, particle_id=1 + P[2]: t=2000-01-01T00:00:00, z=0.000000, y=1.000000, x=0.500000, particle_id=2 + P[3]: t=2000-01-01T00:00:00, z=0.000000, y=1.000000, x=0.750000, particle_id=3 + P[4]: t=2000-01-01T00:00:00, z=0.000000, y=1.000000, x=1.000000, particle_id=4 + Pclass: + Variable(name='t', dtype='float64', initial=0, to_write=True, attrs={'standard_name': 'time', 'units': 'seconds', 'axis': 'T'}) + Variable(name='z', dtype='float32', initial=0, to_write=True, attrs={'standard_name': 'vertical coordinate', 'units': 'm', 'positive': 'down'}) + Variable(name='y', dtype='float32', initial=0, to_write=True, attrs={'standard_name': 'latitude', 'units': 'degrees_north', 'axis': 'Y'}) + Variable(name='x', dtype='float32', initial=0, to_write=True, attrs={'standard_name': 'longitude', 'units': 'degrees_east', 'axis': 'X'}) + Variable(name='dz', dtype='float32', initial=0, to_write=False) + Variable(name='dy', dtype='float32', initial=0, to_write=False) + Variable(name='dx', dtype='float32', initial=0, to_write=False) + Variable(name='particle_id', dtype='int64', initial=0, to_write=True, attrs={'long_name': 'Unique identifier for each particle', 'cf_role': 'trajectory_id'}) + Variable(name='dt', dtype='float64', initial=1.0, to_write=False) + Variable(name='state', dtype='int32', initial=0, to_write=False)""" + assert str(pset) == expected @pytest.mark.parametrize("offset", [0, 1, 200]) From 5ca7883ff512218afb4586e130c989113c5bbee7 Mon Sep 17 00:00:00 2001 From: Erik van Sebille Date: Thu, 29 Oct 2026 08:17:17 +0100 Subject: [PATCH 2/2] Adding time_interval to ParticleSetView for repr --- src/parcels/_core/particleset.py | 2 +- src/parcels/_core/particlesetview.py | 5 +++-- tests/test_particleset.py | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/parcels/_core/particleset.py b/src/parcels/_core/particleset.py index 192f483ff8..942c880469 100644 --- a/src/parcels/_core/particleset.py +++ b/src/parcels/_core/particleset.py @@ -177,7 +177,7 @@ def __getattr__(self, name): def __getitem__(self, index): """Get a single particle by index.""" - return ParticleSetView(self._data, index=index, pclass=self._pclass) + return ParticleSetView(self._data, index=index, pclass=self._pclass, time_interval=self.fieldset.time_interval) def __setattr__(self, name, value): if name in ["_data"]: diff --git a/src/parcels/_core/particlesetview.py b/src/parcels/_core/particlesetview.py index 030ac10947..a6061a92f9 100644 --- a/src/parcels/_core/particlesetview.py +++ b/src/parcels/_core/particlesetview.py @@ -6,10 +6,11 @@ class ParticleSetView: """Class to be used in a kernel that links a View of the ParticleSet (on the kernel level) to a ParticleSet.""" - def __init__(self, data, index, pclass): + def __init__(self, data, index, pclass, time_interval=None): self._data = data self._index = index self._pclass = pclass + self._time_interval = time_interval def __getattr__(self, name): # Return a proxy that behaves like the underlying numpy array but @@ -28,7 +29,7 @@ def __getattr__(self, name): return self._data[name][self._index] def __setattr__(self, name, value): - if name in ["_data", "_index", "_pclass"]: + if name in ["_data", "_index", "_pclass", "_time_interval"]: object.__setattr__(self, name, value) else: self._data[name][self._index] = value diff --git a/tests/test_particleset.py b/tests/test_particleset.py index 66f6a99729..9d8e6f6294 100644 --- a/tests/test_particleset.py +++ b/tests/test_particleset.py @@ -88,6 +88,10 @@ def test_particleset_repr(fieldset): Variable(name='state', dtype='int32', initial=0, to_write=False)""" assert str(pset) == expected + expected = """\ +P[0]: t=2000-01-01T00:00:00, z=0.000000, y=1.000000, x=0.000000, particle_id=0""" + assert str(pset[0]) == expected + @pytest.mark.parametrize("offset", [0, 1, 200]) def test_pset_with_pids(fieldset, offset, npart=100):