Skip to content

Commit 75e3aec

Browse files
author
Carwyn Pelley
committed
ENH: Remove deepcopies when slicing cubes and copying coords
1 parent 88db1d2 commit 75e3aec

File tree

5 files changed

+275
-12
lines changed

5 files changed

+275
-12
lines changed

lib/iris/coords.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -523,8 +523,13 @@ def copy(self, points=None, bounds=None):
523523
raise ValueError('If bounds are specified, points must also be '
524524
'specified')
525525

526-
new_coord = copy.deepcopy(self)
527526
if points is not None:
527+
# We do not perform a deepcopy when we supply new points so as to
528+
# not unnecessarily copy the old points and bounds.
529+
new_coord = copy.copy(self)
530+
new_coord.attributes = copy.deepcopy(self.attributes)
531+
new_coord.coord_system = copy.deepcopy(self.coord_system)
532+
528533
# Explicitly not using the points property as we don't want the
529534
# shape the new points to be constrained by the shape of
530535
# self.points
@@ -534,6 +539,8 @@ def copy(self, points=None, bounds=None):
534539
# points will result in new bounds, discarding those copied from
535540
# self.
536541
new_coord.bounds = bounds
542+
else:
543+
new_coord = copy.deepcopy(self)
537544

538545
return new_coord
539546

@@ -1503,7 +1510,7 @@ def points(self):
15031510

15041511
@points.setter
15051512
def points(self, points):
1506-
points = np.array(points, ndmin=1)
1513+
points = np.array(points, ndmin=1, copy=False)
15071514
# If points are already defined for this coordinate,
15081515
if hasattr(self, '_points') and self._points is not None:
15091516
# Check that setting these points wouldn't change self.shape
@@ -1539,7 +1546,7 @@ def bounds(self):
15391546
def bounds(self, bounds):
15401547
if bounds is not None:
15411548
# Ensure the bounds are a compatible shape.
1542-
bounds = np.array(bounds, ndmin=2)
1549+
bounds = np.array(bounds, ndmin=2, copy=False)
15431550
if self.shape != bounds.shape[:-1]:
15441551
raise ValueError(
15451552
"The shape of the bounds array should be "

lib/iris/cube.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2239,25 +2239,18 @@ def new_cell_measure_dims(cm_):
22392239
try:
22402240
first_slice = next(slice_gen)
22412241
except StopIteration:
2242-
first_slice = None
2242+
first_slice = Ellipsis
22432243

22442244
if self.has_lazy_data():
22452245
cube_data = self._dask_array
22462246
else:
22472247
cube_data = self._numpy_array
22482248

2249-
if first_slice is not None:
2250-
data = cube_data[first_slice]
2251-
else:
2252-
data = copy.deepcopy(cube_data)
2249+
data = cube_data[first_slice]
22532250

22542251
for other_slice in slice_gen:
22552252
data = data[other_slice]
22562253

2257-
# We don't want a view of the data, so take a copy of it if it's
2258-
# not already our own.
2259-
data = copy.deepcopy(data)
2260-
22612254
# We can turn a masked array into a normal array if it's full.
22622255
if ma.isMaskedArray(data):
22632256
if ma.count_masked(data) == 0:
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# (C) British Crown Copyright 2017, Met Office
2+
#
3+
# This file is part of Iris.
4+
#
5+
# Iris is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# Iris is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
17+
"""Unit tests for :class:`iris.coords.AuxCoord`."""
18+
19+
from __future__ import (absolute_import, division, print_function)
20+
from six.moves import (filter, input, map, range, zip) # noqa
21+
22+
# Import iris.tests first so that some things can be initialised before
23+
# importing anything else.
24+
import iris.tests as tests
25+
26+
import dask
27+
import numpy as np
28+
29+
from iris._lazy_data import as_lazy_data
30+
from iris.coords import AuxCoord
31+
32+
33+
class Test___init__(tests.IrisTest):
34+
def test_writeable(self):
35+
coord = AuxCoord([1, 2], bounds=[[1, 2], [2, 3]])
36+
self.assertTrue(coord.points.flags.writeable)
37+
self.assertTrue(coord.bounds.flags.writeable)
38+
39+
40+
def fetch_base(ndarray):
41+
if ndarray.base is not None:
42+
return fetch_base(ndarray.base)
43+
return ndarray
44+
45+
46+
class Test___getitem__(tests.IrisTest):
47+
def test_share_data(self):
48+
# Ensure that slicing a coordinate behaves like slicing a numpy array
49+
# i.e. that the points and bounds are views of the original.
50+
original = AuxCoord([1, 2], bounds=[[1, 2], [2, 3]],
51+
attributes={'dummy1': None},
52+
coord_system=tests.mock.sentinel.coord_system)
53+
sliced_coord = original[:]
54+
self.assertIs(fetch_base(sliced_coord._points),
55+
fetch_base(original._points))
56+
self.assertIs(fetch_base(sliced_coord._bounds),
57+
fetch_base(original._bounds))
58+
self.assertIsNot(sliced_coord.coord_system, original.coord_system)
59+
self.assertIsNot(sliced_coord.attributes, original.attributes)
60+
61+
def test_lazy_data_realisation(self):
62+
# Capture the fact that we realise the data when slicing.
63+
points = np.array([1, 2])
64+
points = as_lazy_data(points)
65+
66+
bounds = np.array([[1, 2], [2, 3]])
67+
bounds = as_lazy_data(bounds)
68+
69+
original = AuxCoord(points, bounds=bounds,
70+
attributes={'dummy1': None},
71+
coord_system=tests.mock.sentinel.coord_system)
72+
sliced_coord = original[:]
73+
# Returned coord is realised.
74+
self.assertIsInstance(sliced_coord._points, dask.array.core.Array)
75+
self.assertIsInstance(sliced_coord._bounds, dask.array.core.Array)
76+
77+
# Original coord remains unrealised.
78+
self.assertIsInstance(points, dask.array.core.Array)
79+
self.assertIsInstance(bounds, dask.array.core.Array)
80+
81+
82+
class Test_copy(tests.IrisTest):
83+
def setUp(self):
84+
self.original = AuxCoord([1, 2], bounds=[[1, 2], [2, 3]],
85+
attributes={'dummy1': None},
86+
coord_system=tests.mock.sentinel.coord_system)
87+
88+
def assert_data_no_share(self, coord_copy):
89+
self.assertIsNot(fetch_base(coord_copy._points),
90+
fetch_base(self.original._points))
91+
self.assertIsNot(fetch_base(coord_copy._bounds),
92+
fetch_base(self.original._bounds))
93+
self.assertIsNot(coord_copy.coord_system, self.original.coord_system)
94+
self.assertIsNot(coord_copy.attributes, self.original.attributes)
95+
96+
def test_existing_points(self):
97+
# Ensure that copying a coordinate does not return a view of its
98+
# points or bounds.
99+
coord_copy = self.original.copy()
100+
self.assert_data_no_share(coord_copy)
101+
102+
def test_existing_points_deepcopy_call(self):
103+
# Ensure that the coordinate object itself is deepcopied called.
104+
with tests.mock.patch('copy.deepcopy') as mock_copy:
105+
self.original.copy()
106+
mock_copy.assert_called_once_with(self.original)
107+
108+
def test_new_points(self):
109+
coord_copy = self.original.copy([1, 2], bounds=[[1, 2], [2, 3]])
110+
self.assert_data_no_share(coord_copy)
111+
112+
def test_new_points_shallowcopy_call(self):
113+
# Ensure that the coordinate object itself is shallow copied so that
114+
# the points and bounds are not unnecessarily copied.
115+
with tests.mock.patch('copy.copy') as mock_copy:
116+
self.original.copy([1, 2], bounds=[[1, 2], [2, 3]])
117+
mock_copy.assert_called_once_with(self.original)
118+
119+
120+
if __name__ == '__main__':
121+
tests.main()
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# (C) British Crown Copyright 2017, Met Office
2+
#
3+
# This file is part of Iris.
4+
#
5+
# Iris is free software: you can redistribute it and/or modify it under
6+
# the terms of the GNU Lesser General Public License as published by the
7+
# Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# Iris is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public License
16+
# along with Iris. If not, see <http://www.gnu.org/licenses/>.
17+
"""Unit tests for :class:`iris.coords.DimCoord`."""
18+
19+
from __future__ import (absolute_import, division, print_function)
20+
from six.moves import (filter, input, map, range, zip) # noqa
21+
22+
# Import iris.tests first so that some things can be initialised before
23+
# importing anything else.
24+
import iris.tests as tests
25+
26+
import copy
27+
28+
import dask
29+
import numpy as np
30+
31+
from iris._lazy_data import as_lazy_data
32+
from iris.coords import DimCoord
33+
34+
35+
class Test___init__(tests.IrisTest):
36+
def test_writeable(self):
37+
coord = DimCoord([1, 2], bounds=[[1, 2], [2, 3]])
38+
self.assertFalse(coord.points.flags.writeable)
39+
self.assertFalse(coord.bounds.flags.writeable)
40+
41+
42+
def fetch_base(ndarray):
43+
if ndarray.base is not None:
44+
return fetch_base(ndarray.base)
45+
return ndarray
46+
47+
48+
class Test___getitem__(tests.IrisTest):
49+
def test_share_data(self):
50+
# Ensure that slicing a coordinate behaves like slicing a numpy array
51+
# i.e. that the points and bounds are views of the original.
52+
original = DimCoord([1, 2], bounds=[[1, 2], [2, 3]],
53+
attributes={'dummy1': None},
54+
coord_system=tests.mock.sentinel.coord_system)
55+
sliced_coord = original[:]
56+
self.assertIs(fetch_base(sliced_coord._points),
57+
fetch_base(original._points))
58+
self.assertIs(fetch_base(sliced_coord._bounds),
59+
fetch_base(original._bounds))
60+
self.assertIsNot(sliced_coord.coord_system, original.coord_system)
61+
self.assertIsNot(sliced_coord.attributes, original.attributes)
62+
63+
def test_lazy_data_realisation(self):
64+
# Capture the fact that we realise the data when slicing.
65+
points = np.array([1, 2])
66+
points = as_lazy_data(points)
67+
68+
bounds = np.array([[1, 2], [2, 3]])
69+
bounds = as_lazy_data(bounds)
70+
71+
original = DimCoord(points, bounds=bounds,
72+
attributes={'dummy1': None},
73+
coord_system=tests.mock.sentinel.coord_system)
74+
sliced_coord = original[:]
75+
# Returned coord is realised.
76+
self.assertIsInstance(sliced_coord._points, np.ndarray)
77+
self.assertIsInstance(sliced_coord._bounds, np.ndarray)
78+
79+
# Original coord remains unrealised.
80+
self.assertIsInstance(points, dask.array.core.Array)
81+
self.assertIsInstance(bounds, dask.array.core.Array)
82+
83+
84+
class Test_copy(tests.IrisTest):
85+
def setUp(self):
86+
self.original = DimCoord([1, 2], bounds=[[1, 2], [2, 3]],
87+
attributes={'dummy1': None},
88+
coord_system=tests.mock.sentinel.coord_system)
89+
90+
def assert_data_no_share(self, coord_copy):
91+
self.assertIsNot(fetch_base(coord_copy._points),
92+
fetch_base(self.original._points))
93+
self.assertIsNot(fetch_base(coord_copy._bounds),
94+
fetch_base(self.original._bounds))
95+
self.assertIsNot(coord_copy.coord_system, self.original.coord_system)
96+
self.assertIsNot(coord_copy.attributes, self.original.attributes)
97+
98+
def test_existing_points(self):
99+
# Ensure that copying a coordinate does not return a view of its
100+
# points or bounds.
101+
coord_copy = self.original.copy()
102+
self.assert_data_no_share(coord_copy)
103+
104+
def test_existing_points_deepcopy_call(self):
105+
# Ensure that the coordinate object itself is deepcopied called.
106+
cp_orig = copy.deepcopy(self.original)
107+
with tests.mock.patch('copy.deepcopy', return_value=cp_orig) as \
108+
mock_copy:
109+
self.original.copy()
110+
mock_copy.assert_called_once_with(self.original)
111+
112+
def test_new_points(self):
113+
coord_copy = self.original.copy([1, 2], bounds=[[1, 2], [2, 3]])
114+
self.assert_data_no_share(coord_copy)
115+
116+
def test_new_points_shallowcopy_call(self):
117+
# Ensure that the coordinate object itself is shallow copied so that
118+
# the points and bounds are not unnecessarily copied.
119+
cp_orig = copy.copy(self.original)
120+
with tests.mock.patch('copy.copy', return_value=cp_orig) as mock_copy:
121+
self.original.copy([1, 2], bounds=[[1, 2], [2, 3]])
122+
mock_copy.assert_called_once_with(self.original)
123+
124+
125+
if __name__ == '__main__':
126+
tests.main()

lib/iris/tests/unit/cube/test_Cube.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,6 +1670,22 @@ def test_remove_cell_measure(self):
16701670
[[self.b_cell_measure, (0, 1)]])
16711671

16721672

1673+
class Test___getitem__lazy(tests.IrisTest):
1674+
def test_lazy_array(self):
1675+
data = np.arange(6).reshape(2, 3)
1676+
data = as_lazy_data(data)
1677+
cube = Cube(data)
1678+
cube2 = cube[1:]
1679+
self.assertTrue(cube2.has_lazy_data())
1680+
cube.data
1681+
self.assertTrue(cube2.has_lazy_data())
1682+
1683+
def test_ndarray(self):
1684+
cube = Cube(np.arange(6).reshape(2, 3))
1685+
cube2 = cube[1:]
1686+
self.assertIs(cube.data.base, cube2.data.base)
1687+
1688+
16731689
class Test__getitem_CellMeasure(tests.IrisTest):
16741690
def setUp(self):
16751691
cube = Cube(np.arange(6).reshape(2, 3))

0 commit comments

Comments
 (0)