Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/parcels/_core/particleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]

Expand Down Expand Up @@ -176,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)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed in order to print full dates (using parcels.float_to_datelike) for e.g. print(pset[0]). If we don't add the time-Interval, these Particle.t could only be printed as floats.

Do you agree the date-printing is worth the extra complexity of adding another argument to ParticleSetView.__init__?


def __setattr__(self, name, value):
if name in ["_data"]:
Expand All @@ -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"])
Expand Down
5 changes: 3 additions & 2 deletions src/parcels/_core/particlesetview.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
19 changes: 14 additions & 5 deletions src/parcels/_repr_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to import here because otherwise a circular import?

E   ImportError: cannot import name 'float_to_datelike' from partially initialized module 'parcels._core.utils.time' (most likely due to a circular import) (/Users/erik/Codes/parcels/src/parcels/_core/utils/time.py)


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()

Expand All @@ -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):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't print empty attrs dictionaries

out += f", attrs={var.attrs!r}"
return out + ")"


def timeinterval_repr(ti: Any) -> str:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_particle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
),
],
)
Expand Down Expand Up @@ -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)""",
),
],
)
Expand Down
57 changes: 55 additions & 2 deletions tests/test_particleset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import numpy as np
import pytest
import xarray as xr
from re_assert import Matches

from parcels import (
FieldSet,
Expand Down Expand Up @@ -37,7 +36,61 @@ 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 = """\
<ParticleSet>
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 = """\
<ParticleSet>
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

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])
Expand Down
Loading