Skip to content

Commit 35bcc26

Browse files
Removing TimeConverter class
And first attempt at implementing native numpy.datetime64 support for time
1 parent e21a940 commit 35bcc26

18 files changed

+71
-376
lines changed

parcels/field.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
)
3030
from parcels.tools._helpers import default_repr, field_repr, timedelta_to_float
3131
from parcels.tools.converters import (
32-
TimeConverter,
3332
UnitConverter,
3433
unitconverters_map,
3534
)
@@ -137,8 +136,8 @@ class Field:
137136
A numpy array containing the timestamps for each of the files in filenames, for loading
138137
from netCDF files only. Default is None if the netCDF dimensions dictionary includes time.
139138
grid : parcels.grid.Grid
140-
:class:`parcels.grid.Grid` object containing all the lon, lat depth, time
141-
mesh and time_origin information. Can be constructed from any of the Grid objects
139+
:class:`parcels.grid.Grid` object containing all the lon, lat depth, time and mesh information.
140+
Can be constructed from any of the Grid objects
142141
fieldtype : str
143142
Type of Field to be used for UnitConverter (either 'U', 'V', 'Kh_zonal', 'Kh_meridional' or None)
144143
transpose : bool
@@ -149,8 +148,6 @@ class Field:
149148
Maximum allowed value on the field. Data above this value are set to zero
150149
cast_data_dtype : str
151150
Cast Field data to dtype. Supported dtypes are "float32" (np.float32 (default)) and "float64 (np.float64).
152-
time_origin : parcels.tools.converters.TimeConverter
153-
Time origin of the time axis (only if grid is None)
154151
interp_method : str
155152
Method for interpolation. Options are 'linear' (default), 'nearest',
156153
'linear_invdist_land_tracer', 'cgrid_velocity', 'cgrid_tracer' and 'bgrid_velocity'
@@ -195,7 +192,6 @@ def __init__(
195192
vmin: float | None = None,
196193
vmax: float | None = None,
197194
cast_data_dtype: type[np.float32] | type[np.float64] | Literal["float32", "float64"] = "float32",
198-
time_origin: TimeConverter | None = None,
199195
interp_method: InterpMethod = "linear",
200196
allow_time_extrapolation: bool | None = None,
201197
time_periodic: TimePeriodic = False,
@@ -221,12 +217,7 @@ def __init__(
221217
)
222218
self._grid = grid
223219
else:
224-
if (time is not None) and isinstance(time[0], np.datetime64):
225-
time_origin = TimeConverter(time[0])
226-
time = np.array([time_origin.reltime(t) for t in time])
227-
else:
228-
time_origin = TimeConverter(0)
229-
self._grid = Grid.create_grid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh)
220+
self._grid = Grid.create_grid(lon, lat, depth, time, mesh=mesh)
230221
self.igrid = -1
231222
self.fieldtype = self.name if fieldtype is None else fieldtype
232223
self.to_write = to_write
@@ -439,15 +430,13 @@ def _collect_timeslices(
439430
dataFiles = np.concatenate(dataFiles).ravel()
440431
if time.size == 1 and time[0] is None:
441432
time[0] = 0
442-
time_origin = TimeConverter(time[0])
443-
time = time_origin.reltime(time)
444433

445434
if not np.all((time[1:] - time[:-1]) > 0):
446435
id_not_ordered = np.where(time[1:] < time[:-1])[0][0]
447436
raise AssertionError(
448437
f"Please make sure your netCDF files are ordered in time. First pair of non-ordered files: {dataFiles[id_not_ordered]}, {dataFiles[id_not_ordered + 1]}"
449438
)
450-
return time, time_origin, timeslices, dataFiles
439+
return time, timeslices, dataFiles
451440

452441
@classmethod
453442
def from_netcdf(
@@ -650,14 +639,14 @@ def from_netcdf(
650639
# Concatenate time variable to determine overall dimension
651640
# across multiple files
652641
if "time" in dimensions or timestamps is not None:
653-
time, time_origin, timeslices, dataFiles = cls._collect_timeslices(
642+
time, timeslices, dataFiles = cls._collect_timeslices(
654643
timestamps, data_filenames, _grid_fb_class, dimensions, indices, netcdf_engine
655644
)
656-
grid = Grid.create_grid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh)
645+
grid = Grid.create_grid(lon, lat, depth, time, mesh=mesh)
657646
grid.timeslices = timeslices
658647
kwargs["dataFiles"] = dataFiles
659648
else: # e.g. for the CROCO CS_w field, see https://github.com/OceanParcels/Parcels/issues/1831
660-
grid = Grid.create_grid(lon, lat, depth, np.array([0.0]), time_origin=TimeConverter(0.0), mesh=mesh)
649+
grid = Grid.create_grid(lon, lat, depth, np.array([0.0]), mesh=mesh)
661650
grid.timeslices = [[0]]
662651
data_filenames = [data_filenames[0]]
663652
elif grid is not None and ("dataFiles" not in kwargs or kwargs["dataFiles"] is None):
@@ -805,10 +794,7 @@ def from_xarray(
805794
lon = da[dimensions["lon"]].values
806795
lat = da[dimensions["lat"]].values
807796

808-
time_origin = TimeConverter(time[0])
809-
time = time_origin.reltime(time) # type: ignore[assignment]
810-
811-
grid = Grid.create_grid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh)
797+
grid = Grid.create_grid(lon, lat, depth, time, mesh=mesh)
812798
kwargs["time_periodic"] = time_periodic
813799
return cls(
814800
name,
@@ -1273,7 +1259,7 @@ def write(self, filename, varname=None):
12731259
else:
12741260
raise NotImplementedError("Field.write only implemented for RectilinearZGrid and CurvilinearZGrid")
12751261

1276-
attrs = {"units": "seconds since " + str(self.grid.time_origin)} if self.grid.time_origin.calendar else {}
1262+
attrs = {} # TODO v4: fix units here
12771263
time_counter = xr.DataArray(self.grid.time, dims=["time_counter"], attrs=attrs)
12781264
vardata = xr.DataArray(
12791265
self.data.reshape((self.grid.tdim, self.grid.zdim, self.grid.ydim, self.grid.xdim)),
@@ -1340,7 +1326,6 @@ def computeTimeChunk(self, data, tindex):
13401326
)
13411327
filebuffer.__enter__()
13421328
time_data = filebuffer.time
1343-
time_data = g.time_origin.reltime(time_data)
13441329
filebuffer.ti = (time_data <= g.time[tindex]).argmin() - 1
13451330
if self.netcdf_engine != "xarray":
13461331
filebuffer.name = filebuffer.parse_name(self.filebuffername)

parcels/fieldset.py

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from parcels.gridset import GridSet
1616
from parcels.particlefile import ParticleFile
1717
from parcels.tools._helpers import fieldset_repr
18-
from parcels.tools.converters import TimeConverter, convert_xarray_time_units
18+
from parcels.tools.converters import convert_xarray_time_units
1919
from parcels.tools.loggers import logger
2020
from parcels.tools.statuscodes import TimeExtrapolationError
2121
from parcels.tools.warnings import FieldSetWarning
@@ -43,8 +43,6 @@ def __init__(self, U: Field | NestedField | None, V: Field | NestedField | None,
4343
self._particlefile: ParticleFile | None = None
4444
if U:
4545
self.add_field(U, "U")
46-
# see #1663 for type-ignore reason
47-
self.time_origin = self.U.grid.time_origin if isinstance(self.U, Field) else self.U[0].grid.time_origin # type: ignore
4846
if V:
4947
self.add_field(V, "V")
5048

@@ -143,14 +141,8 @@ def from_data(
143141
lon = dims["lon"]
144142
lat = dims["lat"]
145143
depth = np.zeros(1, dtype=np.float32) if "depth" not in dims else dims["depth"]
146-
time = np.zeros(1, dtype=np.float64) if "time" not in dims else dims["time"]
147-
time = np.array(time)
148-
if isinstance(time[0], np.datetime64):
149-
time_origin = TimeConverter(time[0])
150-
time = np.array([time_origin.reltime(t) for t in time])
151-
else:
152-
time_origin = kwargs.pop("time_origin", TimeConverter(0))
153-
grid = Grid.create_grid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh)
144+
time = np.zeros(1, dtype=np.datetime64) if "time" not in dims else np.array(dims["time"])
145+
grid = Grid.create_grid(lon, lat, depth, time, mesh=mesh)
154146
if "creation_log" not in kwargs.keys():
155147
kwargs["creation_log"] = "from_data"
156148

@@ -298,15 +290,6 @@ def check_velocityfields(U, V, W):
298290

299291
for g in self.gridset.grids:
300292
g._check_zonal_periodic()
301-
if len(g.time) == 1:
302-
continue
303-
assert isinstance(
304-
g.time_origin.time_origin, type(self.time_origin.time_origin)
305-
), "time origins of different grids must be have the same type"
306-
g.time = g.time + self.time_origin.reltime(g.time_origin)
307-
if g.defer_load:
308-
g.time_full = g.time_full + self.time_origin.reltime(g.time_origin)
309-
g._time_origin = self.time_origin
310293
self._add_UVfield()
311294

312295
for f in self.get_fields():
@@ -1506,7 +1489,6 @@ def computeTimeChunk(self, time=0.0, dt=1):
15061489
----------
15071490
time :
15081491
Time around which the FieldSet chunks are to be loaded.
1509-
Time is provided as a double, relatively to Fieldset.time_origin.
15101492
Default is 0.
15111493
dt :
15121494
time step of the integration scheme, needed to set the direction of time chunk loading.

parcels/grid.py

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import numpy.typing as npt
88

99
from parcels._typing import Mesh, UpdateStatus, assert_valid_mesh
10-
from parcels.tools.converters import Geographic, GeographicPolar, TimeConverter, UnitConverter
10+
from parcels.tools.converters import Geographic, GeographicPolar, UnitConverter
1111
from parcels.tools.warnings import FieldSetWarning
1212

1313
__all__ = [
@@ -46,7 +46,6 @@ def __init__(
4646
lon: npt.NDArray,
4747
lat: npt.NDArray,
4848
time: npt.NDArray | None,
49-
time_origin: TimeConverter | None,
5049
mesh: Mesh,
5150
):
5251
self._ti = -1
@@ -62,18 +61,11 @@ def __init__(
6261
lon = lon.astype(np.float32)
6362
if not lat.dtype == np.float32:
6463
lat = lat.astype(np.float32)
65-
if not time.dtype == np.float64:
66-
assert isinstance(
67-
time[0], (np.integer, np.floating, float, int)
68-
), "Time vector must be an array of int or floats"
69-
time = time.astype(np.float64)
7064

7165
self._lon = lon
7266
self._lat = lat
7367
self.time = time
7468
self.time_full = self.time # needed for deferred_loaded Fields
75-
self._time_origin = TimeConverter() if time_origin is None else time_origin
76-
assert isinstance(self.time_origin, TimeConverter), "time_origin needs to be a TimeConverter object"
7769
assert_valid_mesh(mesh)
7870
self._mesh = mesh
7971
self._cstruct = None
@@ -98,7 +90,7 @@ def __repr__(self):
9890
return (
9991
f"{type(self).__name__}("
10092
f"lon={self.lon!r}, lat={self.lat!r}, time={self.time!r}, "
101-
f"time_origin={self.time_origin!r}, mesh={self.mesh!r})"
93+
f"mesh={self.mesh!r})"
10294
)
10395

10496
@property
@@ -132,10 +124,6 @@ def meridional_halo(self):
132124
def lonlat_minmax(self):
133125
return self._lonlat_minmax
134126

135-
@property
136-
def time_origin(self):
137-
return self._time_origin
138-
139127
@property
140128
def zonal_periodic(self):
141129
return self._zonal_periodic
@@ -158,7 +146,6 @@ def create_grid(
158146
lat: npt.ArrayLike,
159147
depth,
160148
time,
161-
time_origin,
162149
mesh: Mesh,
163150
**kwargs,
164151
):
@@ -170,14 +157,14 @@ def create_grid(
170157

171158
if len(lon.shape) <= 1:
172159
if depth is None or len(depth.shape) <= 1:
173-
return RectilinearZGrid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh, **kwargs)
160+
return RectilinearZGrid(lon, lat, depth, time, mesh=mesh, **kwargs)
174161
else:
175-
return RectilinearSGrid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh, **kwargs)
162+
return RectilinearSGrid(lon, lat, depth, time, mesh=mesh, **kwargs)
176163
else:
177164
if depth is None or len(depth.shape) <= 1:
178-
return CurvilinearZGrid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh, **kwargs)
165+
return CurvilinearZGrid(lon, lat, depth, time, mesh=mesh, **kwargs)
179166
else:
180-
return CurvilinearSGrid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh, **kwargs)
167+
return CurvilinearSGrid(lon, lat, depth, time, mesh=mesh, **kwargs)
181168

182169
@property
183170
def ctypes_struct(self):
@@ -378,14 +365,14 @@ class RectilinearGrid(Grid):
378365
379366
"""
380367

381-
def __init__(self, lon, lat, time, time_origin, mesh: Mesh):
368+
def __init__(self, lon, lat, time, mesh: Mesh):
382369
assert isinstance(lon, np.ndarray) and len(lon.shape) <= 1, "lon is not a numpy vector"
383370
assert isinstance(lat, np.ndarray) and len(lat.shape) <= 1, "lat is not a numpy vector"
384371
assert isinstance(time, np.ndarray) or not time, "time is not a numpy array"
385372
if isinstance(time, np.ndarray):
386373
assert len(time.shape) == 1, "time is not a vector"
387374

388-
super().__init__(lon, lat, time, time_origin, mesh)
375+
super().__init__(lon, lat, time, mesh)
389376
self.tdim = self.time.size
390377

391378
if self.ydim > 1 and self.lat[-1] < self.lat[0]:
@@ -465,8 +452,6 @@ class RectilinearZGrid(RectilinearGrid):
465452
The depth of the different layers is thus constant.
466453
time :
467454
Vector containing the time coordinates of the grid
468-
time_origin : parcels.tools.converters.TimeConverter
469-
Time origin of the time axis
470455
mesh : str
471456
String indicating the type of mesh coordinates and
472457
units used during velocity interpolation:
@@ -476,8 +461,8 @@ class RectilinearZGrid(RectilinearGrid):
476461
2. flat: No conversion, lat/lon are assumed to be in m.
477462
"""
478463

479-
def __init__(self, lon, lat, depth=None, time=None, time_origin=None, mesh: Mesh = "flat"):
480-
super().__init__(lon, lat, time, time_origin, mesh)
464+
def __init__(self, lon, lat, depth=None, time=None, mesh: Mesh = "flat"):
465+
super().__init__(lon, lat, time, mesh)
481466
if isinstance(depth, np.ndarray):
482467
assert len(depth.shape) <= 1, "depth is not a vector"
483468

@@ -514,8 +499,6 @@ class RectilinearSGrid(RectilinearGrid):
514499
depth array is either a 4D array[xdim][ydim][zdim][tdim] or a 3D array[xdim][ydim[zdim].
515500
time :
516501
Vector containing the time coordinates of the grid
517-
time_origin : parcels.tools.converters.TimeConverter
518-
Time origin of the time axis
519502
mesh : str
520503
String indicating the type of mesh coordinates and
521504
units used during velocity interpolation:
@@ -531,10 +514,9 @@ def __init__(
531514
lat: npt.NDArray,
532515
depth: npt.NDArray,
533516
time: npt.NDArray | None = None,
534-
time_origin: TimeConverter | None = None,
535517
mesh: Mesh = "flat",
536518
):
537-
super().__init__(lon, lat, time, time_origin, mesh)
519+
super().__init__(lon, lat, time, mesh)
538520
assert isinstance(depth, np.ndarray) and len(depth.shape) in [3, 4], "depth is not a 3D or 4D numpy array"
539521

540522
self._gtype = GridType.RectilinearSGrid
@@ -576,7 +558,6 @@ def __init__(
576558
lon: npt.NDArray,
577559
lat: npt.NDArray,
578560
time: npt.NDArray | None = None,
579-
time_origin: TimeConverter | None = None,
580561
mesh: Mesh = "flat",
581562
):
582563
assert isinstance(lon, np.ndarray) and len(lon.squeeze().shape) == 2, "lon is not a 2D numpy array"
@@ -587,7 +568,7 @@ def __init__(
587568

588569
lon = lon.squeeze()
589570
lat = lat.squeeze()
590-
super().__init__(lon, lat, time, time_origin, mesh)
571+
super().__init__(lon, lat, time, mesh)
591572
self.tdim = self.time.size
592573

593574
@property
@@ -630,8 +611,6 @@ class CurvilinearZGrid(CurvilinearGrid):
630611
The depth of the different layers is thus constant.
631612
time :
632613
Vector containing the time coordinates of the grid
633-
time_origin : parcels.tools.converters.TimeConverter
634-
Time origin of the time axis
635614
mesh : str
636615
String indicating the type of mesh coordinates and
637616
units used during velocity interpolation:
@@ -647,10 +626,9 @@ def __init__(
647626
lat: npt.NDArray,
648627
depth: npt.NDArray | None = None,
649628
time: npt.NDArray | None = None,
650-
time_origin: TimeConverter | None = None,
651629
mesh: Mesh = "flat",
652630
):
653-
super().__init__(lon, lat, time, time_origin, mesh)
631+
super().__init__(lon, lat, time, mesh)
654632
if isinstance(depth, np.ndarray):
655633
assert len(depth.shape) == 1, "depth is not a vector"
656634

@@ -686,8 +664,6 @@ class CurvilinearSGrid(CurvilinearGrid):
686664
depth array is either a 4D array[xdim][ydim][zdim][tdim] or a 3D array[xdim][ydim[zdim].
687665
time :
688666
Vector containing the time coordinates of the grid
689-
time_origin : parcels.tools.converters.TimeConverter
690-
Time origin of the time axis
691667
mesh : str
692668
String indicating the type of mesh coordinates and
693669
units used during velocity interpolation:
@@ -703,10 +679,9 @@ def __init__(
703679
lat: npt.NDArray,
704680
depth: npt.NDArray,
705681
time: npt.NDArray | None = None,
706-
time_origin: TimeConverter | None = None,
707682
mesh: Mesh = "flat",
708683
):
709-
super().__init__(lon, lat, time, time_origin, mesh)
684+
super().__init__(lon, lat, time, mesh)
710685
assert isinstance(depth, np.ndarray) and len(depth.shape) in [3, 4], "depth is not a 4D numpy array"
711686

712687
self._gtype = GridType.CurvilinearSGrid

parcels/gridset.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ def add_grid(self, field):
1919
existing_grid = True
2020
break
2121
sameGrid = True
22-
if grid.time_origin != g.time_origin:
23-
continue
24-
for attr in ["lon", "lat", "depth", "time"]:
22+
for attr in ["lon", "lat", "depth"]: # HACK removed time because of np.datetime64 support
2523
gattr = getattr(g, attr)
2624
gridattr = getattr(grid, attr)
2725
if gattr.shape != gridattr.shape or not np.allclose(gattr, gridattr):

0 commit comments

Comments
 (0)