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

Remove deepcopies when slicing cubes and copying coords #1992

Closed
wants to merge 7 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Deprecated the data-copying behaviour of Cube indexing and `Coord.copy()`.
The `share_data` attribute of `iris.FUTURE` can be used to switch to
the new data-sharing behaviour.
14 changes: 11 additions & 3 deletions lib/iris/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,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 @@ -183,20 +183,28 @@ 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. It also
controls whether `Coord.copy()` defaults to creating coordinates
whose `points` and `bounds` attributes are views onto the
original coordinate's attributes.

"""
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)

def __setattr__(self, name, value):
if name not in self.__dict__:
Expand Down
15 changes: 14 additions & 1 deletion lib/iris/coords.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,13 +516,26 @@ def copy(self, points=None, bounds=None):
.. note:: If the points argument is specified and bounds are not, the
resulting coordinate will have no bounds.

.. deprecated:: 1.10

By default the new coordinate's `points` and `bounds` will
be independent copies of the corresponding attributes of the
source coordinate.
The `share_data` attribute of `iris.FUTURE` can be used to
switch to the new data-sharing behaviour.

"""

if points is None and bounds is not None:
raise ValueError('If bounds are specified, points must also be '
'specified')

new_coord = copy.deepcopy(self)
if iris.FUTURE.share_data:
new_coord = copy.copy(self)
new_coord.attributes = copy.deepcopy(self.attributes)
new_coord.coord_system = copy.deepcopy(self.coord_system)
else:
new_coord = copy.deepcopy(self)
if points is not None:
# Explicitly not using the points property as we don't want the
# shape the new points to be constrained by the shape of
Expand Down
40 changes: 28 additions & 12 deletions lib/iris/cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -2146,6 +2146,14 @@ def __getitem__(self, keys):
requested must be applicable directly to the cube.data attribute. All
metadata will be subsequently indexed appropriately.

.. deprecated:: 1.10
The value of the `data` attribute of the result will always
be independent of the source Cube's data. As a result,
modifying data values of the result Cube will have no effect
on the source Cube, and vice versa.
The `share_data` attribute of `iris.FUTURE` can be used to
switch to the new data-sharing behaviour.

"""
# turn the keys into a full slice spec (all dims)
full_slice = iris.util._build_full_slice_given_keys(keys,
Expand All @@ -2164,23 +2172,31 @@ def __getitem__(self, keys):
self.cell_measure_dims(cm_) if
dimension_mapping[d] is not None]

try:
first_slice = next(slice_gen)
except StopIteration:
first_slice = None

if first_slice is not None:
if iris.FUTURE.share_data:
try:
first_slice = next(slice_gen)
except StopIteration:
first_slice = Ellipsis
data = self._my_data[first_slice]
else:
data = copy.deepcopy(self._my_data)
try:
first_slice = next(slice_gen)
except StopIteration:
first_slice = None

if first_slice is not None:
data = self._my_data[first_slice]
else:
data = copy.deepcopy(self._my_data)

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
# not already our own.
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 @@ -3098,7 +3114,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.

"""
warnings.warn("Cube.add_history() has been deprecated - "
Expand Down
55 changes: 55 additions & 0 deletions lib/iris/tests/unit/coords/test_AuxCoord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# (C) British Crown Copyright 2016, 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/>.
"""Unit tests for :class:`iris.coords.AuxCoord`."""

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 numpy as np

from iris.coords import AuxCoord
import iris


class Test_copy(tests.IrisTest):
def test_share_data_default(self):
original = AuxCoord(np.arange(4))
copy = original.copy()
original.points[1] = 999
self.assertArrayEqual(copy.points, [0, 1, 2, 3])

def test_share_data_false(self):
original = AuxCoord(np.arange(4))
with iris.FUTURE.context(share_data=False):
copy = original.copy()
original.points[1] = 999
self.assertArrayEqual(copy.points, [0, 1, 2, 3])

def test_share_data_true(self):
original = AuxCoord(np.arange(4))
with iris.FUTURE.context(share_data=True):
copy = original.copy()
original.points[1] = 999
self.assertArrayEqual(copy.points, [0, 999, 2, 3])


if __name__ == '__main__':
tests.main()
38 changes: 38 additions & 0 deletions lib/iris/tests/unit/cube/test_Cube.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,6 +1330,44 @@ def test_remove_cell_measure(self):
[[self.b_cell_measure, (0, 1)]])


class Test___getitem__nofuture(tests.IrisTest):
def setUp(self):
patch = mock.patch('iris.FUTURE.share_data', new=False)
self.mock_fshare = patch.start()
self.addCleanup(patch.stop)

def test_lazy_array(self):
cube = Cube(biggus.NumpyArrayAdapter(np.arange(6).reshape(2, 3)))
cube2 = cube[1:]
self.assertTrue(cube2.has_lazy_data())
cube.data
self.assertTrue(cube2.has_lazy_data())

def test_ndarray(self):
cube = Cube(np.arange(6).reshape(2, 3))
cube2 = cube[1:]
self.assertIsNot(cube.data.base, cube2.data.base)


class Test___getitem__future(tests.IrisTest):
def setUp(self):
patch = mock.patch('iris.FUTURE.share_data', new=True)
self.mock_fshare = patch.start()
self.addCleanup(patch.stop)

def test_lazy_array(self):
cube = Cube(biggus.NumpyArrayAdapter(np.arange(6).reshape(2, 3)))
cube2 = cube[1:]
self.assertTrue(cube2.has_lazy_data())
cube.data
self.assertTrue(cube2.has_lazy_data())

def test_ndarray(self):
cube = Cube(np.arange(6).reshape(2, 3))
cube2 = cube[1:]
self.assertIs(cube.data.base, cube2.data.base)


class Test__getitem_CellMeasure(tests.IrisTest):
def setUp(self):
cube = Cube(np.arange(6).reshape(2, 3))
Expand Down
6 changes: 6 additions & 0 deletions lib/iris/tests/unit/test_Future.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ def test_valid_clip_latitudes(self):
future.clip_latitudes = new_value
self.assertEqual(future.clip_latitudes, new_value)

def test_valid_share_data(self):
future = Future()
new_value = not future.share_data
future.share_data = new_value
self.assertEqual(future.share_data, new_value)

def test_invalid_attribute(self):
future = Future()
with self.assertRaises(AttributeError):
Expand Down