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

ENH: Remove deepcopies when slicing cubes and copying coords #2261

Closed
wants to merge 4 commits into from
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
3 changes: 3 additions & 0 deletions docs/iris/example_code/General/SOI_filtering.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ def low_pass_weights(window, cutoff):


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/General/anomaly_log_colouring.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/General/cross_section.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/General/custom_aggregation.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ def count_spells(data, threshold, axis, spell_length):


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/General/inset_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Load the data
with iris.FUTURE.context(netcdf_promote=True):
cube1 = iris.load_cube(iris.sample_data_path('ostia_monthly.nc'))
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/General/lineplot_with_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

fname = iris.sample_data_path('air_temp.pp')

# Load exactly one cube from the given file.
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/General/orca_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/General/polynomial_fit.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/Meteorology/COP_1d_plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/Meteorology/COP_maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ def cop_metadata_callback(cube, field, filename):


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Load e1 and a1 using the callback to update the metadata
e1 = iris.load_cube(iris.sample_data_path('E1.2098.pp'),
callback=cop_metadata_callback)
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/Meteorology/deriving_phenomena.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def limit_colorbar_ticks(contour_object):


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

fname = iris.sample_data_path('colpex.pp')

# The list of phenomena of interest
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/Meteorology/hovmoller.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/Meteorology/lagged_ensemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ def realization_metadata(cube, field, fname):


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# extract surface temperature cubes which have an ensemble member
# coordinate, adding appropriate lagged ensemble metadata
surface_temp = iris.load_cube(
Expand Down
3 changes: 3 additions & 0 deletions docs/iris/example_code/Oceanography/atlantic_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@


def main():
# Use new behaviour of data-sharing behaviour on indexing.
iris.FUTURE.share_data = True

# Enable a future option, to ensure that the netcdf load works the same way
# as in future Iris versions.
iris.FUTURE.netcdf_promote = True
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Deprecated the data-copying behaviour of Cube indexing.
The `share_data` attribute of `iris.FUTURE` can be used to switch to
the new data-sharing behaviour See :class:`iris.Future`.
13 changes: 10 additions & 3 deletions lib/iris/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class Future(threading.local):

def __init__(self, cell_datetime_objects=False, netcdf_promote=False,
strict_grib_load=False, netcdf_no_unlimited=False,
clip_latitudes=False):
clip_latitudes=False, share_data=False):
"""
A container for run-time options controls.

Expand Down Expand Up @@ -200,20 +200,27 @@ def __init__(self, cell_datetime_objects=False, netcdf_promote=False,
:meth:`iris.coords.Coord.guess_bounds()` method limits the
guessed bounds to [-90, 90] for latitudes.

The option `share_data` controls whether indexing a Cube returns
a Cube whose data is a view onto the original Cube's data, as
opposed to a independent copy of the relevant data. Conversely, when
share_data=False (currently the default), an indexed partial cube
always contains fully independent, copied data arrays.

"""
self.__dict__['cell_datetime_objects'] = cell_datetime_objects
self.__dict__['netcdf_promote'] = netcdf_promote
self.__dict__['strict_grib_load'] = strict_grib_load
self.__dict__['netcdf_no_unlimited'] = netcdf_no_unlimited
self.__dict__['clip_latitudes'] = clip_latitudes
self.__dict__['share_data'] = share_data

def __repr__(self):
msg = ('Future(cell_datetime_objects={}, netcdf_promote={}, '
'strict_grib_load={}, netcdf_no_unlimited={}, '
'clip_latitudes={})')
'clip_latitudes={}, share_data={})')
return msg.format(self.cell_datetime_objects, self.netcdf_promote,
self.strict_grib_load, self.netcdf_no_unlimited,
self.clip_latitudes)
self.clip_latitudes, self.share_data)

deprecated_options = {
'strict_grib_load': ('This is because "iris.fileformats.grib" is now '
Expand Down
15 changes: 11 additions & 4 deletions lib/iris/coords.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2010 - 2016, Met Office
# (C) British Crown Copyright 2010 - 2017, Met Office
#
# This file is part of Iris.
#
Expand Down Expand Up @@ -522,8 +522,13 @@ def copy(self, points=None, bounds=None):
raise ValueError('If bounds are specified, points must also be '
'specified')

new_coord = copy.deepcopy(self)
if points is not None:
# We do not perform a deepcopy when we supply new points so as to
# not unnecessarily copy the old points.
new_coord = copy.copy(self)
new_coord.attributes = copy.deepcopy(self.attributes)
new_coord.coord_system = copy.deepcopy(self.coord_system)

# Explicitly not using the points property as we don't want the
# shape the new points to be constrained by the shape of
# self.points
Expand All @@ -533,6 +538,8 @@ def copy(self, points=None, bounds=None):
# points will result in new bounds, discarding those copied from
# self.
new_coord.bounds = bounds
else:
new_coord = copy.deepcopy(self)

return new_coord

Expand Down Expand Up @@ -1502,7 +1509,7 @@ def points(self):

@points.setter
def points(self, points):
points = np.array(points, ndmin=1)
points = np.array(points, ndmin=1, copy=False)
# If points are already defined for this coordinate,
if hasattr(self, '_points') and self._points is not None:
# Check that setting these points wouldn't change self.shape
Expand Down Expand Up @@ -1538,7 +1545,7 @@ def bounds(self):
def bounds(self, bounds):
if bounds is not None:
# Ensure the bounds are a compatible shape.
bounds = np.array(bounds, ndmin=2)
bounds = np.array(bounds, ndmin=2, copy=False)
if self.shape != bounds.shape[:-1]:
raise ValueError(
"The shape of the bounds array should be "
Expand Down
30 changes: 23 additions & 7 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2010 - 2016, Met Office
# (C) British Crown Copyright 2010 - 2017, Met Office
#
# This file is part of Iris.
#
Expand Down Expand Up @@ -2153,6 +2153,14 @@ def __getitem__(self, keys):
requested must be applicable directly to the cube.data attribute. All
metadata will be subsequently indexed appropriately.

.. deprecated:: 1.13
In future, the `data` attribute of the indexing result may be a
view onto the original data array, to avoid unnecessary copying.
For the present, however, indexing always produces a full
independent copy. The `share_data` attribute of `iris.FUTURE`
should be set to True to enable this new data-sharing behaviour
(if not, a deprecation warning will be issued).

"""
# turn the keys into a full slice spec (all dims)
full_slice = iris.util._build_full_slice_given_keys(keys,
Expand All @@ -2177,7 +2185,7 @@ def new_cell_measure_dims(cm_):
try:
first_slice = next(slice_gen)
except StopIteration:
first_slice = None
first_slice = Ellipsis if iris.FUTURE.share_data else None

if first_slice is not None:
data = self._my_data[first_slice]
Expand All @@ -2187,10 +2195,18 @@ def new_cell_measure_dims(cm_):
for other_slice in slice_gen:
data = data[other_slice]

# We don't want a view of the data, so take a copy of it if it's
# not already our own.
if isinstance(data, biggus.Array) or not data.flags['OWNDATA']:
data = copy.deepcopy(data)
if not iris.FUTURE.share_data:
# We don't want a view of the data, so take a copy of it if it's
Copy link
Member

@pp-mo pp-mo Dec 8, 2016

Choose a reason for hiding this comment

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

This codepath should issue a deprecation warning.

# not already our own.
msg = ('The `data` attribute of the indexing result is currently '
'a copy of the original data array. This behaviour was '
'deprecated in v1.13.0 and will be replaced with views '
'onto the original data array where possible. Set '
'iris.FUTURE.share_data=True to switch to the new '
'behaviour.')
warn_deprecated(msg)
if isinstance(data, biggus.Array) or not data.flags['OWNDATA']:
data = copy.deepcopy(data)

# We can turn a masked array into a normal array if it's full.
if isinstance(data, ma.core.MaskedArray):
Expand Down Expand Up @@ -3108,7 +3124,7 @@ def add_history(self, string):

.. deprecated:: 1.6
Add/modify history metadata within
attr:`~iris.cube.Cube.attributes` as needed.
:attr:`~iris.cube.Cube.attributes` as needed.

"""
warn_deprecated("Cube.add_history() has been deprecated - "
Expand Down
32 changes: 23 additions & 9 deletions lib/iris/tests/test_coord_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -638,30 +638,44 @@ def test_get_set_points_and_bounds(self):
coord.bounds = [[123, 456], [234, 567], [345, 678]]
self.assertEqual(coord.shape, (3, ))
self.assertEqual(coord.bounds.shape, (3, 2))


class TestGuessBounds(tests.IrisTest):
def test_guess_bounds(self):
coord = iris.coords.DimCoord(np.array([0, 10, 20, 30]), long_name="foo", units="1")
coord = iris.coords.DimCoord(np.array([0, 10, 20, 30]),
long_name="foo", units="1")
coord.guess_bounds()
self.assertArrayEqual(coord.bounds, np.array([[-5, 5], [5, 15], [15, 25], [25, 35]]))

self.assertArrayEqual(coord.bounds, np.array([[-5, 5],
[5, 15],
[15, 25],
[25, 35]]))

coord.bounds = None
coord.guess_bounds(0.25)
self.assertArrayEqual(coord.bounds, np.array([[-5, 5], [5, 15], [15, 25], [25, 35]]) + 2.5)

self.assertArrayEqual(coord.bounds, np.array([[-5, 5],
[5, 15],
[15, 25],
[25, 35]]) + 2.5)

coord.bounds = None
coord.guess_bounds(0.75)
self.assertArrayEqual(coord.bounds, np.array([[-5, 5], [5, 15], [15, 25], [25, 35]]) - 2.5)
self.assertArrayEqual(coord.bounds, np.array([[-5, 5],
[5, 15],
[15, 25],
[25, 35]]) - 2.5)

points = coord.points.copy()
points[2] = 25
coord.points = points
coord.bounds = None
coord.guess_bounds()
self.assertArrayEqual(coord.bounds, np.array([[-5., 5.], [5., 17.5], [17.5, 27.5], [27.5, 32.5]]))

self.assertArrayEqual(coord.bounds, np.array([[-5., 5.],
[5., 17.5],
[17.5, 27.5],
[27.5, 32.5]]))

# if the points are not monotonic, then guess_bounds should fail
points = coord.points.copy()
points[2] = 32
coord = iris.coords.AuxCoord.from_coord(coord)
coord.points = points
Expand Down
8 changes: 4 additions & 4 deletions lib/iris/tests/unit/analysis/cartography/test_project.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2014 - 2016, Met Office
# (C) British Crown Copyright 2014 - 2017, Met Office
#
# This file is part of Iris.
#
Expand Down Expand Up @@ -151,9 +151,9 @@ def test_no_coord_system(self):
cube.coord('grid_latitude').coord_system = None
with iris.tests.mock.patch('warnings.warn') as warn:
_, _ = project(cube, ROBINSON)
warn.assert_called_once_with('Coordinate system of latitude and '
'longitude coordinates is not specified. '
'Assuming WGS84 Geodetic.')
msg = ('Coordinate system of latitude and longitude coordinates is '
'not specified. Assuming WGS84 Geodetic.')
self.assertIn(msg, warn.call_args_list[0][0][0])


if __name__ == '__main__':
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# (C) British Crown Copyright 2014 - 2015, Met Office
# (C) British Crown Copyright 2014 - 2017, Met Office
#
# This file is part of Iris.
#
Expand Down Expand Up @@ -143,9 +143,11 @@ def test_distinct_xy_bounds_pole(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always") # always trigger all warnings
weights = geometry_area_weights(cube, geometry)
self.assertEqual(str(w[-1].message), "The geometry exceeds the "
"cube's y dimension at the upper end.")
self.assertTrue(issubclass(w[-1].category, UserWarning))

msg = ("The geometry exceeds the cube's y dimension at the upper "
"end.")
ind = [str(rec.message) for rec in w].index(msg)
self.assertTrue(issubclass(w[ind].category, UserWarning))
target = np.array([
[0, half, half, 0],
[0, half, half, 0],
Expand Down
Loading