Skip to content
Closed
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
47 changes: 47 additions & 0 deletions lib/iris/tests/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,53 @@
from iris.coord_systems import GeogCS, RotatedGeogCS


def geodetic(shape, with_bounds=True, xlim=None, ylim=None, circular=False):
Copy link
Member

Choose a reason for hiding this comment

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

There's already a lat_lon_cube function in this module which does a very similar (albeit more limited) job. It'd be nicer if we could just extend lat_lon_cube.

"""
Return a global lat-lon cube of specified shape.
Copy link
Member

Choose a reason for hiding this comment

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

This could do with adding "2D", clarifying "global", and describing xlim, ylim, circular. (Although this needs to be considered in the light of a merger with lat_lon_cube.)


"""
if xlim is None:
xlim = (-180, 180)
if ylim is None:
ylim = (-90, 90)
shape = np.array(shape)
assert shape.ndim == 1
assert shape.shape[0] == 2
Copy link
Member

Choose a reason for hiding this comment

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

I find the conversion to an array and subsequent assertions somewhat confusing ... how about just:

y_size, x_size = shape


data = np.arange(np.product(shape)).reshape(shape)
data = data.astype('int32')

if xlim[0] > xlim[1]:
data = data[:, ::-1]
if ylim[0] > ylim[1]:
data = data[::-1]

cube = iris.cube.Cube(data)
crs = iris.coord_systems.GeogCS(6371229)

x_bound = np.linspace(xlim[0], xlim[1], endpoint=True, num=shape[1]+1)
x_bounds = np.array([x_bound[:-1], x_bound[1:]]).T
x_points = x_bounds.mean(axis=1)

y_bound = np.linspace(ylim[0], ylim[1], endpoint=True, num=shape[0]+1)
y_bounds = np.array([y_bound[:-1], y_bound[1:]]).T
y_points = y_bounds.mean(axis=1)

if not with_bounds:
x_bounds = None
y_bounds = None
x_coord = iris.coords.DimCoord(x_points, standard_name='longitude',
units='degree_east',
coord_system=crs, bounds=x_bounds,
circular=circular)
y_coord = iris.coords.DimCoord(y_points, standard_name='latitude',
units='degree_north',
coord_system=crs, bounds=y_bounds)
cube.add_dim_coord(y_coord, 0)
cube.add_dim_coord(x_coord, 1)
return cube


def lat_lon_cube():
"""
Returns a cube with a latitude and longitude suitable for testing
Expand Down
68 changes: 68 additions & 0 deletions lib/iris/tests/unit/util/test_invert_coordinate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# (C) British Crown Copyright 2015, 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.invert_coordinate`."""

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
import iris.tests.stock as stock


class TestAll(iris.tests.IrisTest):
def setUp(self):
self.cube = stock.geodetic((2, 2))

def test_inversion_values(self):
tar = self.cube.data[::-1]

iris.util.invert_coordinate(self.cube, 'latitude')
Copy link
Member

Choose a reason for hiding this comment

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

Really you should explicitly import iris.util before using it ... there's no guarantee that import iris will import it as a side-effect. Because this function is the target of the unit test, it might be best as from iris.util import invert_coordinate.


self.assertArrayAlmostEqual(self.cube.data, tar)

tar = [45., -45.]
self.assertArrayAlmostEqual(self.cube.coord('latitude').points, tar)
Copy link
Member

Choose a reason for hiding this comment

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

Please split this out as a separate test method.


tar = [[90, 0], [0, -90]]
self.assertArrayAlmostEqual(self.cube.coord('latitude').bounds, tar)
Copy link
Member

Choose a reason for hiding this comment

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

Please split this out as a separate test method.


def test_aux_coord(self):
coord = iris.coords.AuxCoord.from_coord(self.cube.coord('latitude'))
self.cube.remove_coord('latitude')
self.cube.add_aux_coord(coord, 0)
Copy link
Member

Choose a reason for hiding this comment

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

There an iris.util... function for this ... demote_dim_coord_to_aux_coord. More on this topic later...

msg = ('Only an inversion of a dimension coordinate is supported '
'\(latitude\)')
with self.assertRaisesRegexp(RuntimeError, msg):
iris.util.invert_coordinate(self.cube, 'latitude')

def test_double_inversion(self):
# Check that only the intended changes are made by double inversion.
# We should have exactly the same thing as we started with.
tar = self.cube.copy()
iris.util.invert_coordinate(self.cube, 'latitude')
iris.util.invert_coordinate(self.cube, 'latitude')
self.assertEqual(self.cube, tar)


if __name__ == '__main__':
unittest.main()
48 changes: 48 additions & 0 deletions lib/iris/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,54 @@
import iris.exceptions


def _cube_cubes_arg(cubes):
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't really do enough to warrant existing as a separate function.

"""
Function for ensuring that we return a Cubelist, irrespective of whether
a Cube or a CubeList has been provided.

Args:

cubes (:class:`iris.cube.Cube` or :class:`iris.cube.CubeList`)

Returns:

:class:`iris.cube.CubeList`

"""
# Function for ensuring that we always deal with a cubelists
if isinstance(cubes, iris.cube.Cube):
cubes = iris.cube.CubeList([cubes])
return cubes


def invert_coordinate(cubes, coordinate):
"""
In-place operation to reverse the direction of a coordinate in a cube or
Copy link
Member

Choose a reason for hiding this comment

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

If this is going to be limited to dimension coordinates then it should say so. Have you considered allowing it to operate on 1-dimensional auxiliary coordinates as well?

Copy link
Member

Choose a reason for hiding this comment

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

Re. "In-place" ... do you need to be in-place for memory reasons? If not, you could make it more robust (and address @ajdawson's comment) by just indexing the cube, e.g. new_cube = cube[:, :, ::-1, :].

list of cubes.

Args:

* cubes (:class:`iris.cube.Cube` or :class:`iris.cube.CubeList`):
Cube(s) to have their coordinate inverted

* coordinate ('string' or :class:`iris.coord.Coord`):

"""
cubes = _cube_cubes_arg(cubes)
for cube in cubes:
coord = cube.coord(coordinate)
if coord not in cube.dim_coords:
raise RuntimeError('Only an inversion of a dimension coordinate '
'is supported ({})'.format(coord.name()))
dims = cube.coord_dims(coord)[0]

cube.data = reverse(cube.data, dims)
coord.points = reverse(coord.points, 0)

if coord.has_bounds():
coord.bounds = reverse(coord.bounds, [0, 1])


def broadcast_weights(weights, array, dims):
"""
Broadcast a weights array to the shape of another array.
Expand Down