Skip to content
Merged
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
3 changes: 3 additions & 0 deletions docs/releases/development.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ Next release (in development)
Only the first frame is used to generate the clim to avoid loading more data than required.
The new clim parameter allows users to specify the data limits explicitly if this is insufficient
(:pr:`191`).
* Detect time coordinates with correct dtype and some standard attributes even without a `units` attribute.
Variables with `units` are preferred
(:issue:`190`, :pr:`193`).
30 changes: 22 additions & 8 deletions src/emsarray/conventions/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -296,17 +296,31 @@ def time_coordinate(self) -> xarray.DataArray:
----------
.. [1] `CF Conventions v1.10, 4.4 Time Coordinate <https://cfconventions.org/Data/cf-conventions/cf-conventions-1.10/cf-conventions.html#time-coordinate>`_
"""
# First look for a datetime64 variable with a 'units' field in the encoding
for name in self.dataset.variables.keys():
variable = self.dataset[name]
# xarray will automatically decode all time variables
# and move the 'units' attribute over to encoding to store this change.
if 'units' in variable.encoding:
units = variable.encoding['units']
# A time variable must have units of the form '<units> since <epoc>'
if 'since' in units:
# The variable must now be a numpy datetime
if variable.dtype.type == numpy.datetime64:
# The variable must be a numpy datetime
if variable.dtype.type == numpy.datetime64:
# xarray will automatically decode all time variables
# and move the 'units' attribute over to encoding to store this change.
if 'units' in variable.encoding:
units = variable.encoding['units']
# A time variable must have units of the form '<units> since <epoc>'
if 'since' in units:
return variable

# Next, look for any datetime64 variable with an appropriate attribute
for name in self.dataset.variables.keys():
variable = self.dataset[name]
if variable.dtype.type == numpy.datetime64:
possible_attributes = {
'coordinate_type': 'time',
'standard_name': 'time',
'axis': 'T',
}
if any(variable.attrs.get(name, None) == value for name, value in possible_attributes.items()):
return variable

raise NoSuchCoordinateError("Could not find time coordinate in dataset")

@cached_property
Expand Down
35 changes: 34 additions & 1 deletion tests/conventions/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,47 @@ def apply_clip_mask(self, clip_mask: xarray.Dataset, work_dir: Pathish) -> xarra
return masking.mask_grid_dataset(self.dataset, clip_mask, work_dir)


def test_time_coordinate(datasets: pathlib.Path) -> None:
def test_time_coordinate_with_units(datasets: pathlib.Path) -> None:
dataset = xarray.open_dataset(datasets / 'times.nc')
SimpleConvention(dataset).bind()
time_coordinate = dataset.ems.time_coordinate
assert time_coordinate.name == 'time'
xarray.testing.assert_equal(time_coordinate, dataset['time'])


def test_time_coordinate_no_units(datasets: pathlib.Path) -> None:
dataset = xarray.Dataset({
'time': xarray.DataArray(
data=pandas.date_range('2025-09-08', '2025-10-08'),
attrs={'coordinate_type': 'time', 'standard_name': 'time'},
),
})

SimpleConvention(dataset).bind()
time_coordinate = dataset.ems.time_coordinate
assert time_coordinate.name == 'time'
xarray.testing.assert_equal(time_coordinate, dataset['time'])


def test_time_coordinate_with_units_first(datasets: pathlib.Path) -> None:
dataset = xarray.Dataset({
'time_no_units': xarray.DataArray(
data=pandas.date_range('2025-09-08', '2025-10-08'),
attrs={'coordinate_type': 'time', 'standard_name': 'time'},
),
'time_with_units': xarray.DataArray(
data=pandas.date_range('2025-09-08', '2025-10-08'),
attrs={'coordinate_type': 'time', 'standard_name': 'time'},
),
})

dataset['time_with_units'].encoding['units'] = 'days since 1970-01-01'

SimpleConvention(dataset).bind()
time_coordinate = dataset.ems.time_coordinate
assert time_coordinate.name == 'time_with_units'


def test_time_coordinate_missing() -> None:
dataset = xarray.Dataset()
SimpleConvention(dataset).bind()
Expand Down
Loading