diff --git a/Changelog.rst b/Changelog.rst index 25ff44af09..8216e51b93 100644 --- a/Changelog.rst +++ b/Changelog.rst @@ -8,6 +8,8 @@ version 3.14.0 * New method: `cf.Field.to_dask_array` * New keyword parameter to `cf.write`: ``omit_data`` (https://github.com/NCAS-CMS/cf-python/issues/477) +* Extend functionality of `cf.Data.roll` and `cf.Field.roll` to allow + multiple axes to be rolled simultaneously. * Fixed bug that raised an exception when using the ``equal`` or ``exist`` keyword of `cf.aggregate` (https://github.com/NCAS-CMS/cf-python/issues/499) diff --git a/cf/data/array/cfanetcdfarray.py b/cf/data/array/cfanetcdfarray.py index 47535ca629..ce1830b982 100644 --- a/cf/data/array/cfanetcdfarray.py +++ b/cf/data/array/cfanetcdfarray.py @@ -266,13 +266,20 @@ def _subarray_shapes(self, shapes): :Parameters: - shapes: - TODODASKDOCS + shapes: `int`, sequence, `dict` or `str`, optional + Define the subarray shapes. + + Any value accepted by the *chunks* parameter of the + `dask.array.from_array` function is allowed. + + The subarray sizes implied by *chunks* for a dimension + that has been fragmented are ignored, so their + specification is arbitrary. :Returns: - `list` - The subarray sizes along each uncompressed dimension. + `tuple` + The subarray sizes along each dimension. **Examples** @@ -470,9 +477,9 @@ def _subarrays(self, subarray_shapes): u_shapes.append(c) if dim in f_dims: - # no fragmentation along this dimension f_locations.append(tuple(range(nc))) else: + # No fragmentation along this dimension f_locations.append((0,) * nc) c = tuple(accumulate((0,) + c)) diff --git a/cf/data/array/fullarray.py b/cf/data/array/fullarray.py index 38499dcf5b..6d5016e485 100644 --- a/cf/data/array/fullarray.py +++ b/cf/data/array/fullarray.py @@ -22,20 +22,37 @@ def __init__( ): """**Initialisation** - TODODASKDOCS + :Parameters: - :Parameters: + fill_value : scalar, optional + The fill value for the array. May be set to + `cf.masked` or `np.ma.masked`. - dtype : numpy.dtype - The numpy data type of the data array. + dtype: `numpy.dtype` + The data type of the array. - shape : tuple - The data array's dimension sizes. + shape: `tuple` + The array dimension sizes. - size : int - Number of elements indexin the data array. + units: `str` or `None`, optional + The units of the netCDF variable. Set to `None` to + indicate that there are no units. If unset then the + units will be set to `None` during the first + `__getitem__` call. - fill_value : scalar, optional + calendar: `str` or `None`, optional + The calendar of the netCDF variable. By default, or if + set to `None`, then the CF default calendar is + assumed, if applicable. If unset then the calendar + will be set to `None` during the first `__getitem__` + call. + + source: optional + Initialise the array from the given object. + + {{init source}} + + {{deep copy}} """ super().__init__(source=source, copy=copy) @@ -141,10 +158,19 @@ def __str__(self): return f"Filled with {fill_value!r}" def _set_units(self): - """TODODASKDOCS. + """The units and calendar properties. + + These are the values set during initialisation, defaulting to + `None` if either was not set at that time. .. versionadded:: TODODASKVER + :Returns: + + `tuple` + The units and calendar values, either of which may be + `None`. + """ # TODOCFA: Consider moving _set_units to cfdm.Array, or some # other common ancestor so that this, and other, @@ -178,7 +204,7 @@ def get_fill_value(self): :Returns: - TODODASKDOCS + The fill value. """ return self._get_component("fill_value", None) diff --git a/cf/data/array/gatheredarray.py b/cf/data/array/gatheredarray.py index 43be107f08..e085b4c2f3 100644 --- a/cf/data/array/gatheredarray.py +++ b/cf/data/array/gatheredarray.py @@ -30,7 +30,7 @@ def __repr__(self): return super().__repr__().replace("<", ">> a = d.dask_compressed_array """ ca = self.source(None) @@ -4159,33 +4166,30 @@ def data(self): def dtype(self): """The `numpy` data-type of the data. + Always returned as a `numpy` data-type instance, but may be set + as any object that converts to a `numpy` data-type. + **Examples** - TODODASKDOCS - >>> d = cf.Data([0.5, 1.5, 2.5]) + >>> d = cf.Data([1, 2.5, 3.9]) >>> d.dtype - dtype(float64') - >>> type(d.dtype) - - - >>> d = cf.Data([0.5, 1.5, 2.5]) - >>> import numpy - >>> d.dtype = numpy.dtype(int) + dtype('float64') >>> print(d.array) - [0 1 2] - >>> d.dtype = bool + [1. 2.5 3.9] + >>> d.dtype = int + >>> d.dtype + dtype('int64') >>> print(d.array) - [False True True] - >>> d.dtype = 'float64' + [1 2 3] + >>> d.dtype = 'float32' >>> print(d.array) - [ 0. 1. 1.] - - >>> d = cf.Data([0.5, 1.5, 2.5]) - >>> d.dtype = int - >>> d.dtype = bool - >>> d.dtype = float + [1. 2. 3.] + >>> import numpy as np + >>> d.dtype = np.dtype('int32') + >>> d.dtype + dtype('int32') >>> print(d.array) - [ 0.5 1.5 2.5] + [1 2 3] """ dx = self.to_dask_array() @@ -10790,32 +10794,34 @@ def range( @_inplace_enabled(default=False) @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") def roll(self, axis, shift, inplace=False, i=False): - """Roll array elements along a given axis. + """Roll array elements along one or more axes. - Equivalent in function to `numpy.roll`. + Elements that roll beyond the last position are re-introduced + at the first. - TODODASKDOCS - note that it works for multiple axes + .. seealso:: `flatten`, `insert_dimension`, `flip`, `squeeze`, + `transpose` :Parameters: - axis: `int` - Select the axis over which the elements are to be rolled. - removed. The *axis* parameter is an integer that selects - the axis corresponding to the given position in the list - of axes of the data. + axis: `int`, or `tuple` of `int` + Axis or axes along which elements are shifted. *Parameter example:* - Convolve the second axis: ``axis=1``. + Roll the second axis: ``axis=1``. *Parameter example:* - Convolve the last axis: ``axis=-1``. + Roll the last axis: ``axis=-1``. + + *Parameter example:* + Roll the first and last axes: ``axis=(0, -1)``. shift: `int`, or `tuple` of `int` The number of places by which elements are shifted. If a `tuple`, then *axis* must be a tuple of the same size, and each of the given axes is shifted by the corresponding number. If an `int` while *axis* is a - tuple of `int`, then the same value is used for all + `tuple` of `int`, then the same value is used for all given axes. {{inplace: `bool`, optional}} @@ -10825,9 +10831,51 @@ def roll(self, axis, shift, inplace=False, i=False): :Returns: `Data` or `None` + The rolled data. - """ - # TODODASKAPI - consider matching the numpy/dask api: "shift, axis=" + **Examples** + + >>> d = cf.Data([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]) + >>> print(d.roll(0, 2).array) + [10 11 0 1 2 3 4 5 6 7 8 9] + >>> print(d.roll(0, -2).array) + [ 2 3 4 5 6 7 8 9 10 11 0 1] + + >>> d2 = d.reshape(3, 4) + >>> print(d2.array) + [[ 0 1 2 3] + [ 4 5 6 7] + [ 8 9 10 11]] + >>> print(d2.roll(0, 1).array) + [[ 8 9 10 11] + [ 0 1 2 3] + [ 4 5 6 7]] + >>> print(d2.roll(0, -1).array) + [[ 4 5 6 7] + [ 8 9 10 11] + [ 0 1 2 3]] + >>> print(d2.roll(1, 1).array) + [[ 3 0 1 2] + [ 7 4 5 6] + [11 8 9 10]] + >>> print(d2.roll(1, -1).array) + [[ 1 2 3 0] + [ 5 6 7 4] + [ 9 10 11 8]] + >>> print(d2.roll((1, 0), (1, 1)).array) + [[11 8 9 10] + [ 3 0 1 2] + [ 7 4 5 6]] + >>> print(d2.roll((1, 0), (2, 1)).array) + [[10 11 8 9] + [ 2 3 0 1] + [ 6 7 4 5]] + + """ + # TODODASKAPI - consider matching the numpy/dask api: + # "shift,axis=", and the default axis behaviour + # of a flattened roll followed by shape + # restore d = _inplace_enabled_define_and_cleanup(self) diff --git a/cf/data/fragment/missingfragmentarray.py b/cf/data/fragment/missingfragmentarray.py index 8f114d26c6..45e14524ea 100644 --- a/cf/data/fragment/missingfragmentarray.py +++ b/cf/data/fragment/missingfragmentarray.py @@ -5,7 +5,7 @@ class MissingFragmentArray(FragmentArray): - """A CFA fragment array stored in a netCDF file TODODASKDOCS. + """A CFA fragment array that is wholly missing data. .. versionadded:: TODODASKVER diff --git a/cf/field.py b/cf/field.py index c69d151570..49e2ba73d0 100644 --- a/cf/field.py +++ b/cf/field.py @@ -14228,11 +14228,12 @@ def roll(self, axis, shift, inplace=False, i=False, **kwargs): :Parameters: axis: - The cyclic axis to be rolled, defined by that which would - be selected by passing the given axis description to a - call of the field construct's `domain_axis` method. For - example, for a value of ``'X'``, the domain axis construct - returned by ``f.domain_axis('X')`` is selected. + The cyclic axis to be rolled, defined by that which + would be selected by passing the given axis + description to a call of the field construct's + `domain_axis` method. For example, for a value of + ``'X'``, the domain axis construct returned by + ``f.domain_axis('X')`` is selected. shift: `int` The number of places by which the selected cyclic axis is diff --git a/cf/mixin/propertiesdata.py b/cf/mixin/propertiesdata.py index 9beb0775cb..40bbe3e3bc 100644 --- a/cf/mixin/propertiesdata.py +++ b/cf/mixin/propertiesdata.py @@ -5287,15 +5287,35 @@ def round(self, decimals=0, inplace=False, i=False): @_deprecated_kwarg_check("i", version="3.0.0", removed_at="4.0.0") @_inplace_enabled(default=False) def roll(self, iaxis, shift, inplace=False, i=False): - """Roll the data along an axis. + """Roll the data along one or more axes. + + Elements that roll beyond the last position are re-introduced + at the first. .. seealso:: `flatten`, `insert_dimension`, `flip`, `squeeze`, `transpose` :Parameters: - iaxis: `int` - TODO + axis: `int`, or `tuple` of `int` + Axis or axes along which elements are shifted. + + *Parameter example:* + Roll the second axis: ``axis=1``. + + *Parameter example:* + Roll the last axis: ``axis=-1``. + + *Parameter example:* + Roll the first and last axes: ``axis=(0, -1)``. + + shift: `int`, or `tuple` of `int` + The number of places by which elements are shifted. + If a `tuple`, then *axis* must be a tuple of the same + size, and each of the given axes is shifted by the + corresponding number. If an `int` while *axis* is a + `tuple` of `int`, then the same value is used for all + given axes. {{inplace: `bool`, optional}} @@ -5304,11 +5324,17 @@ def roll(self, iaxis, shift, inplace=False, i=False): :Returns: `{{class}}` or `None` - TODO + The construct with rolled data. If the operation was + in-place then `None` is returned. **Examples** - TODO + >>> print(f.array) + [ 0 1 2 3 4 5 6 7 8 9 10 11] + >>> print(f.roll(0, 2).array) + [10 11 0 1 2 3 4 5 6 7 8 9] + >>> print(f.roll(0, -2).array) + [ 2 3 4 5 6 7 8 9 10 11 0 1] """ return self._apply_data_oper(