diff --git a/biggus/__init__.py b/biggus/__init__.py index 79c5690..1d33dd1 100644 --- a/biggus/__init__.py +++ b/biggus/__init__.py @@ -1334,6 +1334,102 @@ def process_data(self, data): pass +class _MinStreamsHandler(_AggregationStreamsHandler): + def bootstrap(self, shape): + self.result = np.zeros(shape, dtype=self.array.dtype) + + def finalise(self): + array = self.result + # Promote array-scalar to 0-dimensional array. + if array.ndim == 0: + array = np.array(array) + chunk = Chunk(self.current_keys, array) + return chunk + + def process_data(self, data): + self.result = np.min(data, axis=self.axis) + + +class _MinMaskedStreamsHandler(_AggregationStreamsHandler): + def bootstrap(self, shape): + self.result = np.zeros(shape, dtype=self.array.dtype) + + def finalise(self): + array = self.result + # Promote array-scalar to 0-dimensional array. + if array.ndim == 0: + array = np.ma.array(array) + chunk = Chunk(self.current_keys, array) + return chunk + + def process_data(self, data): + self.result = np.min(data, axis=self.axis) + + +class _MaxStreamsHandler(_AggregationStreamsHandler): + def bootstrap(self, shape): + self.result = np.zeros(shape, dtype=self.array.dtype) + + def finalise(self): + array = self.result + # Promote array-scalar to 0-dimensional array. + if array.ndim == 0: + array = np.array(array) + chunk = Chunk(self.current_keys, array) + return chunk + + def process_data(self, data): + self.result = np.max(data, axis=self.axis) + + +class _MaxMaskedStreamsHandler(_AggregationStreamsHandler): + def bootstrap(self, shape): + self.result = np.zeros(shape, dtype=self.array.dtype) + + def finalise(self): + array = self.result + # Promote array-scalar to 0-dimensional array. + if array.ndim == 0: + array = np.ma.array(array) + chunk = Chunk(self.current_keys, array) + return chunk + + def process_data(self, data): + self.result = np.max(data, axis=self.axis) + + +class _SumStreamsHandler(_AggregationStreamsHandler): + def bootstrap(self, shape): + self.running_total = np.zeros(shape, dtype=self.array.dtype) + + def finalise(self): + array = self.running_total + # Promote array-scalar to 0-dimensional array. + if array.ndim == 0: + array = np.array(array) + chunk = Chunk(self.current_keys, array) + return chunk + + def process_data(self, data): + self.running_total += np.sum(data, axis=self.axis) + + +class _SumMaskedStreamsHandler(_AggregationStreamsHandler): + def bootstrap(self, shape): + self.running_total = np.ma.zeros(shape, dtype=self.array.dtype) + + def finalise(self): + array = self.running_total + # Promote array-scalar to 0-dimensional array. + if array.ndim == 0: + array = np.ma.array(array) + chunk = Chunk(self.current_keys, array) + return chunk + + def process_data(self, data): + self.running_total += np.sum(data, axis=self.axis) + + class _MeanStreamsHandler(_AggregationStreamsHandler): def __init__(self, array, axis, mdtol): # The mdtol argument is not applicable to non-masked arrays @@ -1587,6 +1683,93 @@ def _normalise_axis(axis, array): return axes +def min(a, axis=None): + """ + Request the minimum of an Array over any number of axes. + + .. note:: Currently limited to operating on a single axis. + + Parameters + ---------- + a : Array object + The object whose minimum is to be found. + axis : None, or int, or iterable of ints + Axis or axes along which the operation is performed. The default + (axis=None) is to perform the operation over all the dimensions of the + input array. The axis may be negative, in which case it counts from + the last to the first axis. If axis is a tuple of ints, the operation + is performed over multiple axes. + + Returns + ------- + out : Array + The Array representing the requested mean. + """ + axes = _normalise_axis(axis, a) + assert axes is not None and len(axes) == 1 + return _Aggregation(a, axes[0], + _MinStreamsHandler, _MinMaskedStreamsHandler, + a.dtype, {}) + + +def max(a, axis=None): + """ + Request the maximum of an Array over any number of axes. + + .. note:: Currently limited to operating on a single axis. + + Parameters + ---------- + a : Array object + The object whose maximum is to be found. + axis : None, or int, or iterable of ints + Axis or axes along which the operation is performed. The default + (axis=None) is to perform the operation over all the dimensions of the + input array. The axis may be negative, in which case it counts from + the last to the first axis. If axis is a tuple of ints, the operation + is performed over multiple axes. + + Returns + ------- + out : Array + The Array representing the requested max. + """ + axes = _normalise_axis(axis, a) + assert axes is not None and len(axes) == 1 + return _Aggregation(a, axes[0], + _MaxStreamsHandler, _MaxMaskedStreamsHandler, + a.dtype, {}) + + +def sum(a, axis=None): + """ + Request the sum of an Array over any number of axes. + + .. note:: Currently limited to operating on a single axis. + + Parameters + ---------- + a : Array object + The object whose summation is to be found. + axis : None, or int, or iterable of ints + Axis or axes along which the operation is performed. The default + (axis=None) is to perform the operation over all the dimensions of the + input array. The axis may be negative, in which case it counts from + the last to the first axis. If axis is a tuple of ints, the operation + is performed over multiple axes. + + Returns + ------- + out : Array + The Array representing the requested sum. + """ + axes = _normalise_axis(axis, a) + assert axes is not None and len(axes) == 1 + return _Aggregation(a, axes[0], + _SumStreamsHandler, _SumMaskedStreamsHandler, + a.dtype, {}) + + def mean(a, axis=None, mdtol=1): """ Request the mean of an Array over any number of axes. diff --git a/biggus/tests/unit/_aggregation_test_framework.py b/biggus/tests/unit/_aggregation_test_framework.py new file mode 100644 index 0000000..ef84b6a --- /dev/null +++ b/biggus/tests/unit/_aggregation_test_framework.py @@ -0,0 +1,160 @@ +# (C) British Crown Copyright 2014, Met Office +# +# This file is part of Biggus. +# +# Biggus 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. +# +# Biggus 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 Biggus. If not, see . +"""Unit tests for `biggus` aggregation operators.""" +from abc import ABCMeta, abstractproperty + +import numpy as np +import numpy.ma as ma + +import biggus + + +class Operator(object): + __metaclass__ = ABCMeta + + @abstractproperty + def biggus_operator(self): + pass + + @abstractproperty + def numpy_operator(self): + pass + + @abstractproperty + def numpy_masked_operator(self): + pass + + +class InvalidAxis(Operator): + def setUp(self): + self.array = biggus.NumpyArrayAdapter(np.arange(12)) + + def test_none(self): + with self.assertRaises(AssertionError): + self.biggus_operator(self.array) + + def test_too_large(self): + with self.assertRaises(ValueError): + self.biggus_operator(self.array, axis=1) + + def test_too_small(self): + with self.assertRaises(ValueError): + self.biggus_operator(self.array, axis=-2) + + def test_multiple(self): + array = biggus.NumpyArrayAdapter(np.arange(12).reshape(3, 4)) + with self.assertRaises(AssertionError): + self.biggus_operator(array, axis=(0, 1)) + + +class AggregationDtype(Operator): + def _check(self, source): + # Default behaviour is for operators which inherrit their dtype from + # the objects they perform the aggregation. + array = biggus.NumpyArrayAdapter(np.arange(2, dtype=source)) + agg = self.biggus_operator(array, axis=0) + self.assertEqual(agg.dtype, source) + + def test_dtype_equal_source_dtype(self): + dtypes = [np.int8, np.int16, np.int32, np.int] + for dtype in dtypes: + self._check(dtype) + + +class NumpyArrayAdapter(Operator): + def setUp(self): + self.data = np.arange(12) + + def _check(self, data, dtype=None, shape=None): + data = np.asarray(data, dtype=dtype) + if shape is not None: + data = data.reshape(shape) + array = biggus.NumpyArrayAdapter(data) + result = self.biggus_operator(array, axis=0).ndarray() + expected = self.numpy_operator(data, axis=0) + if expected.ndim == 0: + expected = np.asarray(expected) + np.testing.assert_array_equal(result, expected) + + def test_flat_int(self): + self._check(self.data) + + def test_multi_int(self): + self._check(self.data, shape=(3, 4)) + + def test_flat_float(self): + self._check(self.data, dtype=np.float) + + def test_multi_float(self): + self._check(self.data, dtype=np.float, shape=(3, 4)) + + +class NumpyArrayAdapterMasked(): + def _check(self, data): + array = biggus.NumpyArrayAdapter(data) + result = self.biggus_operator(array, axis=0).masked_array() + expected = self.numpy_masked_operator(data, axis=0) + if expected.ndim == 0: + if expected is np.ma.masked: + expected = ma.asarray(expected, dtype=array.dtype) + else: + expected = ma.asarray(expected) + np.testing.assert_array_equal(result.filled(), expected.filled()) + np.testing.assert_array_equal(result.mask, expected.mask) + + def test_no_mask_flat(self): + for dtype in [np.int, np.float]: + data = ma.arange(12, dtype=dtype) + self._check(data) + + def test_no_mask_multi(self): + for dtype in [np.int, np.float]: + data = ma.arange(12, dtype=dtype).reshape(3, 4) + self._check(data) + + def test_flat(self): + for dtype in [np.int, np.float]: + data = ma.arange(12, dtype=dtype) + data[::2] = ma.masked + self._check(data) + + data.mask = ma.nomask + data[1::2] = ma.masked + self._check(data) + + def test_all_masked(self): + data = ma.arange(12, dtype=np.int) + data[:] = ma.masked + self._check(data) + + def test_multi(self): + for dtype in [np.int, np.float]: + data = ma.arange(12, dtype=dtype) + data[::2] = ma.masked + self._check(data.reshape(3, 4)) + + data = ma.arange(12, dtype=dtype) + data[1::2] = ma.masked + self._check(data.reshape(3, 4)) + + data = ma.arange(12, dtype=dtype).reshape(3, 4) + data[::2] = ma.masked + self._check(data) + + data = ma.arange(12, dtype=dtype).reshape(3, 4) + data[1::2] = ma.masked + self._check(data) diff --git a/biggus/tests/unit/test_max.py b/biggus/tests/unit/test_max.py new file mode 100644 index 0000000..068f2f1 --- /dev/null +++ b/biggus/tests/unit/test_max.py @@ -0,0 +1,61 @@ +# (C) British Crown Copyright 2014, Met Office +# +# This file is part of Biggus. +# +# Biggus 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. +# +# Biggus 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 Biggus. If not, see . +"""Unit tests for `biggus.max`.""" + +import numpy as np +import numpy.ma as ma +import unittest + +import biggus +import biggus.tests.unit._aggregation_test_framework as test_framework + + +class Operator(object): + @property + def biggus_operator(self): + return biggus.max + + @property + def numpy_operator(self): + return np.max + + @property + def numpy_masked_operator(self): + return ma.max + + +class TestInvalidAxis(Operator, test_framework.InvalidAxis, unittest.TestCase): + pass + + +class TestAggregationDtype( + Operator, test_framework.AggregationDtype, unittest.TestCase): + pass + + +class TestNumpyArrayAdapter( + Operator, test_framework.NumpyArrayAdapter, unittest.TestCase): + pass + + +class TestNumpyArrayAdapterMasked( + Operator, test_framework.NumpyArrayAdapterMasked, unittest.TestCase): + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/biggus/tests/unit/test_min.py b/biggus/tests/unit/test_min.py new file mode 100644 index 0000000..5a4dc4a --- /dev/null +++ b/biggus/tests/unit/test_min.py @@ -0,0 +1,61 @@ +# (C) British Crown Copyright 2014, Met Office +# +# This file is part of Biggus. +# +# Biggus 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. +# +# Biggus 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 Biggus. If not, see . +"""Unit tests for `biggus.min`.""" + +import numpy as np +import numpy.ma as ma +import unittest + +import biggus +import biggus.tests.unit._aggregation_test_framework as test_framework + + +class Operator(object): + @property + def biggus_operator(self): + return biggus.min + + @property + def numpy_operator(self): + return np.min + + @property + def numpy_masked_operator(self): + return ma.min + + +class TestInvalidAxis(Operator, test_framework.InvalidAxis, unittest.TestCase): + pass + + +class TestAggregationDtype( + Operator, test_framework.AggregationDtype, unittest.TestCase): + pass + + +class TestNumpyArrayAdapter( + Operator, test_framework.NumpyArrayAdapter, unittest.TestCase): + pass + + +class TestNumpyArrayAdapterMasked( + Operator, test_framework.NumpyArrayAdapterMasked, unittest.TestCase): + pass + + +if __name__ == '__main__': + unittest.main() diff --git a/biggus/tests/unit/test_sum.py b/biggus/tests/unit/test_sum.py new file mode 100644 index 0000000..ad6c290 --- /dev/null +++ b/biggus/tests/unit/test_sum.py @@ -0,0 +1,61 @@ +# (C) British Crown Copyright 2014, Met Office +# +# This file is part of Biggus. +# +# Biggus 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. +# +# Biggus 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 Biggus. If not, see . +"""Unit tests for `biggus.sum`.""" + +import numpy as np +import numpy.ma as ma +import unittest + +import biggus +import biggus.tests.unit._aggregation_test_framework as test_framework + + +class Operator(object): + @property + def biggus_operator(self): + return biggus.sum + + @property + def numpy_operator(self): + return np.sum + + @property + def numpy_masked_operator(self): + return ma.sum + + +class TestInvalidAxis(Operator, test_framework.InvalidAxis, unittest.TestCase): + pass + + +class TestAggregationDtype( + Operator, test_framework.AggregationDtype, unittest.TestCase): + pass + + +class TestNumpyArrayAdapter( + Operator, test_framework.NumpyArrayAdapter, unittest.TestCase): + pass + + +class TestNumpyArrayAdapterMasked( + Operator, test_framework.NumpyArrayAdapterMasked, unittest.TestCase): + pass + + +if __name__ == '__main__': + unittest.main()