Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
75 changes: 54 additions & 21 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -2194,7 +2194,7 @@ def serialize(x):
coord = self.copy(points=points, bounds=bounds)
return coord

def _guess_bounds(self, bound_position=0.5):
def _guess_bounds(self, bound_position=0.5, monthly=False):
"""Return bounds for this coordinate based on its points.

Parameters
Expand Down Expand Up @@ -2224,7 +2224,7 @@ def _guess_bounds(self, bound_position=0.5):
if self.ndim != 1:
raise iris.exceptions.CoordinateMultiDimError(self)

if self.shape[0] < 2:
if not monthly and self.shape[0] < 2:
raise ValueError("Cannot guess bounds for a coordinate of length 1.")

if self.has_bounds():
Expand All @@ -2233,31 +2233,63 @@ def _guess_bounds(self, bound_position=0.5):
"before guessing new ones."
)

if getattr(self, "circular", False):
points = np.empty(self.shape[0] + 2)
points[1:-1] = self.points
direction = 1 if self.points[-1] > self.points[0] else -1
points[0] = self.points[-1] - (self.units.modulus * direction)
points[-1] = self.points[0] + (self.units.modulus * direction)
diffs = np.diff(points)
if monthly:
dates = self.units.num2date(self.points)
lower_bounds = []
upper_bounds = []
months_and_years = []
for date in dates:
if date.month == 12:
lyear = date.year
uyear = date.year + 1
lmonth = 12
umonth = 1
else:
lyear = uyear = date.year
lmonth = date.month
umonth = date.month + 1
# if monthly:
if f"{date.month} + {date.year}" not in months_and_years:
months_and_years.append(f"{date.month} + {date.year}")
else:
raise ValueError(
"Cannot guess bounds for a coordinate with multiple "
"points in a month."
)
lower_bounds.append(date.__class__(lyear, lmonth, 1, 0, 0))
upper_bounds.append(date.__class__(uyear, umonth, 1, 0, 0))
bounds = self.units.date2num(np.array([lower_bounds, upper_bounds]).T)
contiguous = np.ma.allclose(bounds[1:, 0], bounds[:-1, 1])
if not contiguous:
raise ValueError("Cannot guess bounds for a non-contiguous coordinate.")

# if not monthly
else:
diffs = np.diff(self.points)
diffs = np.insert(diffs, 0, diffs[0])
diffs = np.append(diffs, diffs[-1])
if getattr(self, "circular", False):
points = np.empty(self.shape[0] + 2)
points[1:-1] = self.points
direction = 1 if self.points[-1] > self.points[0] else -1
points[0] = self.points[-1] - (self.units.modulus * direction)
points[-1] = self.points[0] + (self.units.modulus * direction)
diffs = np.diff(points)
else:
diffs = np.diff(self.points)
diffs = np.insert(diffs, 0, diffs[0])
diffs = np.append(diffs, diffs[-1])

min_bounds = self.points - diffs[:-1] * bound_position
max_bounds = self.points + diffs[1:] * (1 - bound_position)
min_bounds = self.points - diffs[:-1] * bound_position
max_bounds = self.points + diffs[1:] * (1 - bound_position)

bounds = np.array([min_bounds, max_bounds]).transpose()
bounds = np.array([min_bounds, max_bounds]).transpose()

if self.name() in ("latitude", "grid_latitude") and self.units == "degree":
points = self.points
if (points >= -90).all() and (points <= 90).all():
np.clip(bounds, -90, 90, out=bounds)
if self.name() in ("latitude", "grid_latitude") and self.units == "degree":
points = self.points
if (points >= -90).all() and (points <= 90).all():
np.clip(bounds, -90, 90, out=bounds)

return bounds

def guess_bounds(self, bound_position=0.5):
def guess_bounds(self, bound_position=0.5, monthly=False):
"""Add contiguous bounds to a coordinate, calculated from its points.

Puts a cell boundary at the specified fraction between each point and
Expand All @@ -2274,6 +2306,7 @@ def guess_bounds(self, bound_position=0.5):
bound_position : float, default=0.5
The desired position of the bounds relative to the position
of the points.
monthly : bool, default=False

Notes
-----
Expand All @@ -2289,7 +2322,7 @@ def guess_bounds(self, bound_position=0.5):
suitable values directly to the bounds property, instead.

"""
self.bounds = self._guess_bounds(bound_position)
self.bounds = self._guess_bounds(bound_position, monthly)

def intersect(self, other, return_indices=False):
"""Return a new coordinate from the intersection of two coordinates.
Expand Down
85 changes: 85 additions & 0 deletions lib/iris/tests/unit/coords/test_Coord.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@
import iris.tests as tests # isort:skip

import collections
from datetime import datetime
from unittest import mock
import warnings

import cf_units
import dask.array as da
import numpy as np
import pytest
Expand Down Expand Up @@ -236,6 +238,89 @@ def test_points_inside_bounds_outside_wrong_name_2(self):
self.assertArrayEqual(lat.bounds, [[-120, -40], [-40, 35], [35, 105]])


class Test_Guess_Bounds_Monthly(tests.IrisTest):
def test_monthly_multiple_points_in_month(self):
units = cf_units.Unit("days since epoch", calendar="gregorian")
points = units.date2num(
[
datetime(1990, 1, 3),
datetime(1990, 1, 28),
datetime(1990, 2, 13),
]
)
coord = iris.coords.AuxCoord(points=points, units=units, standard_name="time")
with pytest.raises(
ValueError,
match="Cannot guess bounds for a coordinate with multiple points "
"in a month.",
):
coord.guess_bounds(monthly=True)

def test_monthly_non_contiguous(self):
units = cf_units.Unit("days since epoch", calendar="gregorian")
expected = units.date2num(
[
[datetime(1990, 1, 1), datetime(1990, 2, 1)],
[datetime(1990, 2, 1), datetime(1990, 3, 1)],
[datetime(1990, 5, 1), datetime(1990, 6, 1)],
]
)
points = expected.mean(axis=1)
coord = iris.coords.AuxCoord(points=points, units=units, standard_name="time")
with pytest.raises(
ValueError, match="Cannot guess bounds for a non-contiguous coordinate."
):
coord.guess_bounds(monthly=True)

def test_monthly_end_of_month(self):
units = cf_units.Unit("days since epoch", calendar="gregorian")
expected = units.date2num(
[
[datetime(1990, 1, 1), datetime(1990, 2, 1)],
[datetime(1990, 2, 1), datetime(1990, 3, 1)],
[datetime(1990, 3, 1), datetime(1990, 4, 1)],
]
)
points = units.date2num(
[
datetime(1990, 1, 31),
datetime(1990, 2, 28),
datetime(1990, 3, 31),
]
)
coord = iris.coords.AuxCoord(points=points, units=units, standard_name="time")
coord.guess_bounds(monthly=True)
dates = units.num2date(coord.bounds)
expected_dates = units.num2date(expected)
self.assertArrayEqual(dates, expected_dates)

def test_monthly_multiple_years(self):
units = cf_units.Unit("days since epoch", calendar="gregorian")
expected = [
[datetime(1990, 10, 1), datetime(1990, 11, 1)],
[datetime(1990, 11, 1), datetime(1990, 12, 1)],
[datetime(1990, 12, 1), datetime(1991, 1, 1)],
]
expected_points = units.date2num(expected)
points = expected_points.mean(axis=1)
coord = iris.coords.AuxCoord(points=points, units=units, standard_name="time")
coord.guess_bounds(monthly=True)
dates = units.num2date(coord.bounds)
self.assertArrayEqual(dates, expected)

def test_monthly_single_point(self):
units = cf_units.Unit("days since epoch", calendar="gregorian")
expected = [
[datetime(1990, 1, 1), datetime(1990, 2, 1)],
]
expected_points = units.date2num(expected)
points = expected_points.mean(axis=1)
coord = iris.coords.AuxCoord(points=points, units=units, standard_name="time")
coord.guess_bounds(monthly=True)
dates = units.num2date(coord.bounds)
self.assertArrayEqual(dates, expected)


class Test_cell(tests.IrisTest):
def _mock_coord(self):
coord = mock.Mock(
Expand Down