Skip to content

Commit

Permalink
iris.util.reverse on cubes (SciTools#3155)
Browse files Browse the repository at this point in the history
* make cube reversing official

* review: test conventions, etc.

* review: enable coord specification

* add whatsnew

* review: AssertRaises --> AssertRaisesRegexp

* cube error handling
  • Loading branch information
rcomer authored and znicholls committed Jun 15, 2019
1 parent 06748a6 commit 6ee6c69
Show file tree
Hide file tree
Showing 4 changed files with 224 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* :func:`iris.util.reverse` can now be used to reverse a cube by specifying one or more coordinates.
25 changes: 0 additions & 25 deletions lib/iris/tests/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,31 +93,6 @@ def test_monotonic_strict(self):
self.assertMonotonic(b)


class TestReverse(tests.IrisTest):
def test_simple(self):
a = np.arange(12).reshape(3, 4)
np.testing.assert_array_equal(a[::-1], iris.util.reverse(a, 0))
np.testing.assert_array_equal(a[::-1, ::-1], iris.util.reverse(a, [0, 1]))
np.testing.assert_array_equal(a[:, ::-1], iris.util.reverse(a, 1))
np.testing.assert_array_equal(a[:, ::-1], iris.util.reverse(a, [1]))
self.assertRaises(ValueError, iris.util.reverse, a, [])
self.assertRaises(ValueError, iris.util.reverse, a, -1)
self.assertRaises(ValueError, iris.util.reverse, a, 10)
self.assertRaises(ValueError, iris.util.reverse, a, [-1])
self.assertRaises(ValueError, iris.util.reverse, a, [0, -1])

def test_single(self):
a = np.arange(36).reshape(3, 4, 3)
np.testing.assert_array_equal(a[::-1], iris.util.reverse(a, 0))
np.testing.assert_array_equal(a[::-1, ::-1], iris.util.reverse(a, [0, 1]))
np.testing.assert_array_equal(a[:, ::-1, ::-1], iris.util.reverse(a, [1, 2]))
np.testing.assert_array_equal(a[..., ::-1], iris.util.reverse(a, 2))
self.assertRaises(ValueError, iris.util.reverse, a, -1)
self.assertRaises(ValueError, iris.util.reverse, a, 10)
self.assertRaises(ValueError, iris.util.reverse, a, [-1])
self.assertRaises(ValueError, iris.util.reverse, a, [0, -1])


class TestClipString(tests.IrisTest):
def setUp(self):
self.test_string = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
Expand Down
185 changes: 185 additions & 0 deletions lib/iris/tests/unit/util/test_reverse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# (C) British Crown Copyright 2018, Met Office
#
# This file is part of Iris.
#
# Iris is free software: you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License as published by the
# Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Iris is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
"""Test function :func:`iris.util.reverse`."""

from __future__ import (absolute_import, division, print_function)
from six.moves import (filter, input, map, range, zip) # noqa

# Import iris.tests first so that some things can be initialised before
# importing anything else.
import iris.tests as tests

import unittest

import iris
from iris.util import reverse
import numpy as np


class Test_array(tests.IrisTest):
def test_simple_array(self):
a = np.arange(12).reshape(3, 4)
self.assertArrayEqual(a[::-1], reverse(a, 0))
self.assertArrayEqual(a[::-1, ::-1], reverse(a, [0, 1]))
self.assertArrayEqual(a[:, ::-1], reverse(a, 1))
self.assertArrayEqual(a[:, ::-1], reverse(a, [1]))

msg = 'Reverse was expecting a single axis or a 1d array *'
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, [])

msg = 'An axis value out of range for the number of dimensions *'
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, -1)
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, 10)
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, [-1])
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, [0, -1])

msg = 'To reverse an array, provide an int *'
with self.assertRaisesRegexp(TypeError, msg):
reverse(a, 'latitude')

def test_single_array(self):
a = np.arange(36).reshape(3, 4, 3)
self.assertArrayEqual(a[::-1], reverse(a, 0))
self.assertArrayEqual(a[::-1, ::-1], reverse(a, [0, 1]))
self.assertArrayEqual(a[:, ::-1, ::-1], reverse(a, [1, 2]))
self.assertArrayEqual(a[..., ::-1], reverse(a, 2))

msg = 'Reverse was expecting a single axis or a 1d array *'
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, [])

msg = 'An axis value out of range for the number of dimensions *'
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, -1)
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, 10)
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, [-1])
with self.assertRaisesRegexp(ValueError, msg):
reverse(a, [0, -1])

with self.assertRaisesRegexp(
TypeError, 'To reverse an array, provide an int *'):
reverse(a, 'latitude')


class Test_cube(tests.IrisTest):
def setUp(self):
# On this cube pair, the coordinates to perform operations on have
# matching long names but the points array on one cube is reversed
# with respect to that on the other.
data = np.arange(12).reshape(3, 4)
self.a1 = iris.coords.DimCoord([1, 2, 3], long_name='a')
self.b1 = iris.coords.DimCoord([1, 2, 3, 4], long_name='b')
a2 = iris.coords.DimCoord([3, 2, 1], long_name='a')
b2 = iris.coords.DimCoord([4, 3, 2, 1], long_name='b')
self.span = iris.coords.AuxCoord(np.arange(12).reshape(3, 4),
long_name='spanning')

self.cube1 = iris.cube.Cube(
data, dim_coords_and_dims=[(self.a1, 0), (self.b1, 1)],
aux_coords_and_dims=[(self.span, (0, 1))])

self.cube2 = iris.cube.Cube(
data, dim_coords_and_dims=[(a2, 0), (b2, 1)])

def test_cube_dim(self):
cube1_reverse0 = reverse(self.cube1, 0)
cube1_reverse1 = reverse(self.cube1, 1)
cube1_reverse_both = reverse(self.cube1, (0, 1))

self.assertArrayEqual(self.cube1.data[::-1], cube1_reverse0.data)
self.assertArrayEqual(self.cube2.coord('a').points,
cube1_reverse0.coord('a').points)
self.assertArrayEqual(self.cube1.coord('b').points,
cube1_reverse0.coord('b').points)

self.assertArrayEqual(self.cube1.data[:, ::-1], cube1_reverse1.data)
self.assertArrayEqual(self.cube1.coord('a').points,
cube1_reverse1.coord('a').points)
self.assertArrayEqual(self.cube2.coord('b').points,
cube1_reverse1.coord('b').points)

self.assertArrayEqual(self.cube1.data[::-1, ::-1],
cube1_reverse_both.data)
self.assertArrayEqual(self.cube2.coord('a').points,
cube1_reverse_both.coord('a').points)
self.assertArrayEqual(self.cube2.coord('b').points,
cube1_reverse_both.coord('b').points)

def test_cube_coord(self):
cube1_reverse0 = reverse(self.cube1, self.a1)
cube1_reverse1 = reverse(self.cube1, 'b')
cube1_reverse_both = reverse(self.cube1, (self.a1, self.b1))
cube1_reverse_spanning = reverse(self.cube1, 'spanning')

self.assertArrayEqual(self.cube1.data[::-1], cube1_reverse0.data)
self.assertArrayEqual(self.cube2.coord('a').points,
cube1_reverse0.coord('a').points)
self.assertArrayEqual(self.cube1.coord('b').points,
cube1_reverse0.coord('b').points)

self.assertArrayEqual(self.cube1.data[:, ::-1], cube1_reverse1.data)
self.assertArrayEqual(self.cube1.coord('a').points,
cube1_reverse1.coord('a').points)
self.assertArrayEqual(self.cube2.coord('b').points,
cube1_reverse1.coord('b').points)

self.assertArrayEqual(self.cube1.data[::-1, ::-1],
cube1_reverse_both.data)
self.assertArrayEqual(self.cube2.coord('a').points,
cube1_reverse_both.coord('a').points)
self.assertArrayEqual(self.cube2.coord('b').points,
cube1_reverse_both.coord('b').points)

self.assertArrayEqual(self.cube1.data[::-1, ::-1],
cube1_reverse_spanning.data)
self.assertArrayEqual(self.cube2.coord('a').points,
cube1_reverse_spanning.coord('a').points)
self.assertArrayEqual(self.cube2.coord('b').points,
cube1_reverse_spanning.coord('b').points)
self.assertArrayEqual(
self.span.points[::-1, ::-1],
cube1_reverse_spanning.coord('spanning').points)

msg = 'Expected to find exactly 1 latitude coordinate, but found none.'
with self.assertRaisesRegexp(
iris.exceptions.CoordinateNotFoundError, msg):
reverse(self.cube1, 'latitude')

msg = 'Reverse was expecting a single axis or a 1d array *'
with self.assertRaisesRegexp(ValueError, msg):
reverse(self.cube1, [])

msg = ('coords_or_dims must be int, str, coordinate or sequence of '
'these. Got cube.')
with self.assertRaisesRegexp(TypeError, msg):
reverse(self.cube1, self.cube1)

msg = ('coords_or_dims must be int, str, coordinate or sequence of '
'these.')
with self.assertRaisesRegexp(TypeError, msg):
reverse(self.cube1, 3.)


if __name__ == '__main__':
unittest.main()
50 changes: 38 additions & 12 deletions lib/iris/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@

import iris
import iris.exceptions
import iris.cube


def broadcast_to_shape(array, shape, dim_map):
Expand Down Expand Up @@ -406,16 +407,19 @@ def between(lh, rh, lh_inclusive=True, rh_inclusive=True):
return lambda c: lh < c < rh


def reverse(array, axes):
def reverse(cube_or_array, coords_or_dims):
"""
Reverse the array along the given axes.
Reverse the cube or array along the given dimensions.
Args:
* array
The array to reverse
* axes
A single value or array of values of axes to reverse
* cube_or_array: :class:`iris.cube.Cube` or :class:`numpy.ndarray`
The cube or array to reverse.
* coords_or_dims: int, str, :class:`iris.coords.Coord` or sequence of these
Identify one or more dimensions to reverse. If cube_or_array is a
numpy array, use int or a sequence of ints, as in the examples below.
If cube_or_array is a Cube, a Coord or coordinate name (or sequence of
these) may be specified instead.
::
Expand Down Expand Up @@ -447,20 +451,42 @@ def reverse(array, axes):
[15 14 13 12]]]
"""
index = [slice(None, None)] * array.ndim
axes = np.array(axes, ndmin=1)
if axes.ndim != 1:
index = [slice(None, None)] * cube_or_array.ndim

if isinstance(coords_or_dims, iris.cube.Cube):
raise TypeError('coords_or_dims must be int, str, coordinate or '
'sequence of these. Got cube.')

if iris.cube._is_single_item(coords_or_dims):
coords_or_dims = [coords_or_dims]

axes = set()
for coord_or_dim in coords_or_dims:
if isinstance(coord_or_dim, int):
axes.add(coord_or_dim)
elif isinstance(cube_or_array, np.ndarray):
raise TypeError(
'To reverse an array, provide an int or sequence of ints.')
else:
try:
axes.update(cube_or_array.coord_dims(coord_or_dim))
except AttributeError:
raise TypeError('coords_or_dims must be int, str, coordinate '
'or sequence of these.')

axes = np.array(list(axes), ndmin=1)
if axes.ndim != 1 or axes.size == 0:
raise ValueError('Reverse was expecting a single axis or a 1d array '
'of axes, got %r' % axes)
if np.min(axes) < 0 or np.max(axes) > array.ndim-1:
if np.min(axes) < 0 or np.max(axes) > cube_or_array.ndim-1:
raise ValueError('An axis value out of range for the number of '
'dimensions from the given array (%s) was received. '
'Got: %r' % (array.ndim, axes))
'Got: %r' % (cube_or_array.ndim, axes))

for axis in axes:
index[axis] = slice(None, None, -1)

return array[tuple(index)]
return cube_or_array[tuple(index)]


def monotonic(array, strict=False, return_direction=False):
Expand Down

0 comments on commit 6ee6c69

Please sign in to comment.