TimeStamps and calendars#

[1]:
from glob import glob

import numpy as np

from parcels import Field, download_example_dataset

Some NetCDF files, such as for example those from the World Ocean Atlas, have time calendars that can’t be parsed by xarray. These result in a ValueError: unable to decode time units, for example when the calendar is in ‘months since’ a particular date.

In these cases, a workaround in Parcels is to use the timestamps argument in Field (or FieldSet) creation. Here, we show how this works for example temperature data from the World Ocean Atlas in the Pacific Ocean

The following cell raises an error, since the calendar of the World Ocean Atlas data is in “months since 1955-01-01 00:00:00”

[2]:
example_dataset_folder = download_example_dataset("WOA_data")
tempfield = Field.from_netcdf(
    glob(f"{example_dataset_folder}/woa18_decav_*_04.nc"),
    "t_an",
    {"lon": "lon", "lat": "lat", "time": "time"},
)
WARNING: File /Users/erik/Library/Caches/parcels/WOA_data/woa18_decav_t10_04.nc could not be decoded properly by xarray (version 2022.11.0).
         It will be opened with no decoding. Filling values might be wrongly parsed.
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/coding/times.py:275, in decode_cf_datetime(num_dates, units, calendar, use_cftime)
    274 try:
--> 275     dates = _decode_datetime_with_pandas(flat_num_dates, units, calendar)
    276 except (KeyError, OutOfBoundsDatetime, OutOfBoundsTimedelta, OverflowError):

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/coding/times.py:216, in _decode_datetime_with_pandas(flat_num_dates, units, calendar)
    215 delta, ref_date = _unpack_netcdf_time_units(units)
--> 216 delta = _netcdf_to_numpy_timeunit(delta)
    217 try:

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/coding/times.py:110, in _netcdf_to_numpy_timeunit(units)
    109     units = f"{units}s"
--> 110 return {
    111     "nanoseconds": "ns",
    112     "microseconds": "us",
    113     "milliseconds": "ms",
    114     "seconds": "s",
    115     "minutes": "m",
    116     "hours": "h",
    117     "days": "D",
    118 }[units]

KeyError: 'months'

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/coding/times.py:180, in _decode_cf_datetime_dtype(data, units, calendar, use_cftime)
    179 try:
--> 180     result = decode_cf_datetime(example_value, units, calendar, use_cftime)
    181 except Exception:

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/coding/times.py:277, in decode_cf_datetime(num_dates, units, calendar, use_cftime)
    276 except (KeyError, OutOfBoundsDatetime, OutOfBoundsTimedelta, OverflowError):
--> 277     dates = _decode_datetime_with_cftime(
    278         flat_num_dates.astype(float), units, calendar
    279     )
    281     if (
    282         dates[np.nanargmin(num_dates)].year < 1678
    283         or dates[np.nanargmax(num_dates)].year >= 2262
    284     ):

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/coding/times.py:202, in _decode_datetime_with_cftime(num_dates, units, calendar)
    200 if num_dates.size > 0:
    201     return np.asarray(
--> 202         cftime.num2date(num_dates, units, calendar, only_use_cftime_datetimes=True)
    203     )
    204 else:

File src/cftime/_cftime.pyx:580, in cftime._cftime.num2date()

File src/cftime/_cftime.pyx:98, in cftime._cftime._dateparse()

ValueError: 'months since' units only allowed for '360_day' calendar

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/conventions.py:523, in decode_cf_variables(variables, attributes, concat_characters, mask_and_scale, decode_times, decode_coords, drop_variables, use_cftime, decode_timedelta)
    522 try:
--> 523     new_vars[k] = decode_cf_variable(
    524         k,
    525         v,
    526         concat_characters=concat_characters,
    527         mask_and_scale=mask_and_scale,
    528         decode_times=decode_times,
    529         stack_char_dim=stack_char_dim,
    530         use_cftime=use_cftime,
    531         decode_timedelta=decode_timedelta,
    532     )
    533 except Exception as e:

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/conventions.py:369, in decode_cf_variable(name, var, concat_characters, mask_and_scale, decode_times, decode_endianness, stack_char_dim, use_cftime, decode_timedelta)
    368 if decode_times:
--> 369     var = times.CFDatetimeCoder(use_cftime=use_cftime).decode(var, name=name)
    371 dimensions, data, attributes, encoding = variables.unpack_for_decoding(var)

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/coding/times.py:699, in CFDatetimeCoder.decode(self, variable, name)
    698 calendar = pop_to(attrs, encoding, "calendar")
--> 699 dtype = _decode_cf_datetime_dtype(data, units, calendar, self.use_cftime)
    700 transform = partial(
    701     decode_cf_datetime,
    702     units=units,
    703     calendar=calendar,
    704     use_cftime=self.use_cftime,
    705 )

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/coding/times.py:190, in _decode_cf_datetime_dtype(data, units, calendar, use_cftime)
    185     msg = (
    186         f"unable to decode time units {units!r} with {calendar_msg!r}. Try "
    187         "opening your dataset with decode_times=False or installing cftime "
    188         "if it is not installed."
    189     )
--> 190     raise ValueError(msg)
    191 else:

ValueError: unable to decode time units 'months since 1955-01-01 00:00:00' with 'the default calendar'. Try opening your dataset with decode_times=False or installing cftime if it is not installed.

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
File ~/Codes/ParcelsCode/parcels/tools/converters.py:266, in convert_xarray_time_units(ds, time)
    265 try:
--> 266     da2 = xr.decode_cf(da2)
    267 except ValueError:

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/conventions.py:659, in decode_cf(obj, concat_characters, mask_and_scale, decode_times, decode_coords, drop_variables, use_cftime, decode_timedelta)
    657     raise TypeError("can only decode Dataset or DataStore objects")
--> 659 vars, attrs, coord_names = decode_cf_variables(
    660     vars,
    661     attrs,
    662     concat_characters,
    663     mask_and_scale,
    664     decode_times,
    665     decode_coords,
    666     drop_variables=drop_variables,
    667     use_cftime=use_cftime,
    668     decode_timedelta=decode_timedelta,
    669 )
    670 ds = Dataset(vars, attrs=attrs)

File ~/opt/anaconda3/envs/py3_parcels/lib/python3.8/site-packages/xarray/conventions.py:534, in decode_cf_variables(variables, attributes, concat_characters, mask_and_scale, decode_times, decode_coords, drop_variables, use_cftime, decode_timedelta)
    533 except Exception as e:
--> 534     raise type(e)(f"Failed to decode variable {k!r}: {e}")
    535 if decode_coords in [True, "coordinates", "all"]:

ValueError: Failed to decode variable 'time': unable to decode time units 'months since 1955-01-01 00:00:00' with 'the default calendar'. Try opening your dataset with decode_times=False or installing cftime if it is not installed.

During handling of the above exception, another exception occurred:

RuntimeError                              Traceback (most recent call last)
/Users/erik/Codes/ParcelsCode/docs/examples/tutorial_timestamps.ipynb Cell 5 in <cell line: 2>()
      <a href='vscode-notebook-cell:/Users/erik/Codes/ParcelsCode/docs/examples/tutorial_timestamps.ipynb#W4sZmlsZQ%3D%3D?line=0'>1</a> example_dataset_folder = download_example_dataset("WOA_data")
----> <a href='vscode-notebook-cell:/Users/erik/Codes/ParcelsCode/docs/examples/tutorial_timestamps.ipynb#W4sZmlsZQ%3D%3D?line=1'>2</a> tempfield = Field.from_netcdf(
      <a href='vscode-notebook-cell:/Users/erik/Codes/ParcelsCode/docs/examples/tutorial_timestamps.ipynb#W4sZmlsZQ%3D%3D?line=2'>3</a>     glob(f"{example_dataset_folder}/woa18_decav_*_04.nc"),
      <a href='vscode-notebook-cell:/Users/erik/Codes/ParcelsCode/docs/examples/tutorial_timestamps.ipynb#W4sZmlsZQ%3D%3D?line=3'>4</a>     "t_an",
      <a href='vscode-notebook-cell:/Users/erik/Codes/ParcelsCode/docs/examples/tutorial_timestamps.ipynb#W4sZmlsZQ%3D%3D?line=4'>5</a>     {"lon": "lon", "lat": "lat", "time": "time"},
      <a href='vscode-notebook-cell:/Users/erik/Codes/ParcelsCode/docs/examples/tutorial_timestamps.ipynb#W4sZmlsZQ%3D%3D?line=5'>6</a> )

File ~/Codes/ParcelsCode/parcels/field.py:443, in Field.from_netcdf(cls, filenames, variable, dimensions, indices, grid, mesh, timestamps, allow_time_extrapolation, time_periodic, deferred_load, **kwargs)
    438     raise RuntimeError('Multiple files given but no time dimension specified')
    440 if grid is None:
    441     # Concatenate time variable to determine overall dimension
    442     # across multiple files
--> 443     time, time_origin, timeslices, dataFiles = cls.collect_timeslices(timestamps, data_filenames,
    444                                                                       _grid_fb_class, dimensions,
    445                                                                       indices, netcdf_engine, netcdf_decodewarning)
    446     grid = Grid.create_grid(lon, lat, depth, time, time_origin=time_origin, mesh=mesh)
    447     grid.timeslices = timeslices

File ~/Codes/ParcelsCode/parcels/field.py:276, in Field.collect_timeslices(timestamps, data_filenames, _grid_fb_class, dimensions, indices, netcdf_engine, netcdf_decodewarning)
    273 for fname in data_filenames:
    274     with _grid_fb_class(fname, dimensions, indices, netcdf_engine=netcdf_engine,
    275                         netcdf_decodewarning=netcdf_decodewarning) as filebuffer:
--> 276         ftime = filebuffer.time
    277         timeslices.append(ftime)
    278         dataFiles.append([fname] * len(ftime))

File ~/Codes/ParcelsCode/parcels/fieldfilebuffer.py:214, in NetcdfFileBuffer.time(self)
    212 @property
    213 def time(self):
--> 214     return self.time_access()

File ~/Codes/ParcelsCode/parcels/fieldfilebuffer.py:224, in NetcdfFileBuffer.time_access(self)
    221     return np.array([None])
    223 time_da = self.dataset[self.dimensions['time']]
--> 224 convert_xarray_time_units(time_da, self.dimensions['time'])
    225 time = np.array([time_da[self.dimensions['time']].data]) if len(time_da.shape) == 0 else np.array(time_da[self.dimensions['time']])
    226 if isinstance(time[0], datetime.datetime):

File ~/Codes/ParcelsCode/parcels/tools/converters.py:268, in convert_xarray_time_units(ds, time)
    266     da2 = xr.decode_cf(da2)
    267 except ValueError:
--> 268     raise RuntimeError('Xarray could not convert the calendar. If you''re using from_netcdf, '
    269                        'try using the timestamps keyword in the construction of your Field. '
    270                        'See also the tutorial at https://docs.oceanparcels.org/en/latest/'
    271                        'examples/tutorial_timestamps.html')
    272 ds[time] = da2[time]

RuntimeError: Xarray could not convert the calendar. If youre using from_netcdf, try using the timestamps keyword in the construction of your Field. See also the tutorial at https://docs.oceanparcels.org/en/latest/examples/tutorial_timestamps.html

However, we can create our own numpy array of timestamps associated with each of the 12 snapshots in the netcdf file

[3]:
timestamps = np.expand_dims(
    np.array([np.datetime64("2001-%.2d-15" % m) for m in range(1, 13)]), axis=1
)

And then we can add the timestamps as an extra argument

[4]:
tempfield = Field.from_netcdf(
    glob(f"{example_dataset_folder}/woa18_decav_*_04.nc"),
    "t_an",
    {"lon": "lon", "lat": "lat", "time": "time"},
    netcdf_decodewarning=False,
    timestamps=timestamps,
)

Note, by the way, that adding the time_periodic=True argument to Field.from_netcdf() will also mean that the climatology can be cycled for multiple years.

Furthermore, note that we used netcdf_decodewarning=False in the FieldSet.from_netcdf() call above. This is to silence an expected warning because the time dimension in the coordinates.nc file can’t be decoded by xarray.