Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iris.util.reverse on cubes #3155

Merged
merged 6 commits into from
Oct 3, 2018
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
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
rcomer marked this conversation as resolved.
Show resolved Hide resolved
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:
Copy link
Member Author

@rcomer rcomer Oct 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is added because the tests that pass an empty list were failing at np.min (line 478 below) and that error message was less informative in context.

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