Source code for odl.util.normalize

# Copyright 2014-2019 The ODL contributors
#
# This file is part of ODL.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

"""Utilities for normalization of user input."""

from __future__ import print_function, division, absolute_import
import numpy as np


__all__ = ('normalized_scalar_param_list', 'normalized_index_expression',
           'normalized_nodes_on_bdry', 'normalized_axes_tuple',
           'safe_int_conv')


[docs]def normalized_scalar_param_list(param, length, param_conv=None, keep_none=True, return_nonconv=False): """Return a list of given length from a scalar parameter. The typical use case is when a single value or a sequence of values is accepted as input. This function makes a list from a given sequence or a list of identical elements from a single value, with cast to a given parameter type if desired. To distinguish a parameter sequence from a single parameter, the following rules are applied: * If ``param`` is not a sequence, it is treated as a single parameter (e.g. ``1``). * If ``len(param) == length == 1``, then ``param`` is interpreted as a single parameter (e.g. ``[1]`` or ``'1'``). * If ``len(param) == length != 1``, then ``param`` is interpreted as sequence of parameters. * Otherwise, ``param`` is interpreted as a single parameter. Note that this function is not applicable to parameters which are themselves iterable (e.g. ``'abc'`` with ``length=3`` will be interpreted as equivalent to ``['a', 'b', 'c']``). Parameters ---------- param : Input parameter to turn into a list. length : nonnegative int Desired length of the output list. param_conv : callable, optional Conversion applied to each list element. ``None`` means no conversion. keep_none : bool, optional If ``True``, ``None`` is not converted. return_nonconv : bool, optional If ``True``, return also the list where no conversion has been applied. Returns ------- plist : list Input parameter turned into a list of length ``length``. nonconv : list The same as ``plist``, but without conversion. This is only returned if ``return_nonconv == True``. Examples -------- Turn input into a list of given length, possibly by broadcasting. By default, no conversion is performed. >>> normalized_scalar_param_list((1, 2, 3), 3) [1, 2, 3] >>> normalized_scalar_param_list((1, None, 3.0), 3) [1, None, 3.0] Single parameters are broadcast to the given length. >>> normalized_scalar_param_list(1, 3) [1, 1, 1] >>> normalized_scalar_param_list('10', 3) ['10', '10', '10'] >>> normalized_scalar_param_list(None, 3) [None, None, None] List entries can be explicitly converted using ``param_conv``. If ``None`` should be kept, set ``keep_none`` to ``True``: >>> normalized_scalar_param_list(1, 3, param_conv=float) [1.0, 1.0, 1.0] >>> normalized_scalar_param_list('10', 3, param_conv=int) [10, 10, 10] >>> normalized_scalar_param_list((1, None, 3.0), 3, param_conv=int, ... keep_none=True) # default [1, None, 3] The conversion parameter can be any callable: >>> def myconv(x): ... return False if x is None else bool(x) >>> normalized_scalar_param_list((0, None, 3.0), 3, param_conv=myconv, ... keep_none=False) [False, False, True] """ length, length_in = int(length), length if length < 0: raise ValueError('`length` must be nonnegative, got {}' ''.format(length_in)) param = np.array(param, dtype=object, copy=True, ndmin=1) nonconv_list = list(np.broadcast_to(param, (length,))) if len(nonconv_list) != length: raise ValueError('sequence `param` has length {}, expected {}' ''.format(len(nonconv_list), length)) if param_conv is None: out_list = list(nonconv_list) else: out_list = [] for p in nonconv_list: if p is None and keep_none: out_list.append(p) else: out_list.append(param_conv(p)) if return_nonconv: return out_list, nonconv_list else: return out_list
[docs]def normalized_index_expression(indices, shape, int_to_slice=False): """Enable indexing with almost Numpy-like capabilities. Implements the following features: - Usage of general slices and sequences of slices - Conversion of `Ellipsis` into an adequate number of ``slice(None)`` objects - Fewer indices than axes by filling up with an `Ellipsis` - Error checking with respect to a given shape - Conversion of integer indices into corresponding slices Parameters ---------- indices : int, `slice`, `Ellipsis` or sequence of these Index expression to be normalized. shape : sequence of ints Target shape for error checking of out-of-bounds indices. Also needed to determine the number of axes. int_to_slice : bool, optional If ``True``, turn integers into corresponding slice objects. Returns ------- normalized : tuple of ints or `slice`'s Normalized index expression Examples -------- Sequences are turned into tuples. We can have at most as many entries as the length of ``shape``, but fewer are allowed - the remaining list places are filled up by ``slice(None)``: >>> normalized_index_expression([1, 2, 3], shape=(3, 4, 5)) (1, 2, 3) >>> normalized_index_expression([1, 2], shape=(3, 4, 5)) (1, 2, slice(None, None, None)) >>> normalized_index_expression([slice(2), 2], shape=(3, 4, 5)) (slice(None, 2, None), 2, slice(None, None, None)) >>> normalized_index_expression([1, Ellipsis], shape=(3, 4, 5)) (1, slice(None, None, None), slice(None, None, None)) By default, integer indices are kept. If they should be converted to slices, use ``int_to_slice=True``. This can be useful to guarantee that the result of slicing with the returned object is of the same type as the array into which is sliced and has the same number of axes: >>> x = np.zeros(shape=(3, 4, 5)) >>> idx1 = normalized_index_expression([1, 2, 3], shape=(3, 4, 5), ... int_to_slice=True) >>> idx1 (slice(1, 2, None), slice(2, 3, None), slice(3, 4, None)) >>> x[idx1] array([[[ 0.]]]) >>> idx2 = normalized_index_expression([1, 2, 3], shape=(3, 4, 5), ... int_to_slice=False) >>> idx2 (1, 2, 3) >>> x[idx2] 0.0 """ ndim = len(shape) # Support indexing with fewer indices as indexing along the first # corresponding axes. In the other cases, normalize the input. if np.isscalar(indices): indices = [indices, Ellipsis] elif (isinstance(indices, slice) or indices is Ellipsis): indices = [indices] indices = list(indices) if len(indices) < ndim and Ellipsis not in indices: indices.append(Ellipsis) # Turn Ellipsis into the correct number of slice(None) if Ellipsis in indices: if indices.count(Ellipsis) > 1: raise ValueError('cannot use more than one Ellipsis.') eidx = indices.index(Ellipsis) extra_dims = ndim - len(indices) + 1 indices = (indices[:eidx] + [slice(None)] * extra_dims + indices[eidx + 1:]) # Turn single indices into length-1 slices if desired for (i, idx), n in zip(enumerate(indices), shape): if np.isscalar(idx): if idx < 0: idx += n if idx >= n: raise IndexError('Index {} is out of bounds for axis ' '{} with size {}.' ''.format(idx, i, n)) if int_to_slice: indices[i] = slice(idx, idx + 1) # Catch most common errors if any(s.start == s.stop and s.start is not None or s.start == n for s, n in zip(indices, shape) if isinstance(s, slice)): raise ValueError('Slices with empty axes not allowed.') if None in indices: raise ValueError('creating new axes is not supported.') if len(indices) > ndim: raise IndexError('too may indices: {} > {}.' ''.format(len(indices), ndim)) return tuple(indices)
[docs]def normalized_nodes_on_bdry(nodes_on_bdry, length): """Return a list of 2-tuples of bool from the input parameter. This function is intended to normalize a ``nodes_on_bdry`` parameter that can be given as a single boolean (global) or as a sequence (per axis). Each entry of the sequence can either be a single boolean (global for the axis) or a boolean sequence of length 2. Parameters ---------- nodes_on_bdry : bool or sequence Input parameter to be normalized according to the above scheme. length : positive int Desired length of the returned list. Returns ------- normalized : list of 2-tuples of bool Normalized list with ``length`` entries, each of which is a 2-tuple of boolean values. Examples -------- Global for all axes: >>> normalized_nodes_on_bdry(True, length=2) [(True, True), (True, True)] Global per axis: >>> normalized_nodes_on_bdry([True, False], length=2) [(True, True), (False, False)] Mixing global and explicit per axis: >>> normalized_nodes_on_bdry([[True, False], False, True], length=3) [(True, False), (False, False), (True, True)] """ shape = np.shape(nodes_on_bdry) if shape == (): out_list = [(bool(nodes_on_bdry), bool(nodes_on_bdry))] * length elif length == 1 and shape == (2,): out_list = [(bool(nodes_on_bdry[0]), bool(nodes_on_bdry[1]))] elif len(nodes_on_bdry) == length: out_list = [] for i, on_bdry in enumerate(nodes_on_bdry): shape_i = np.shape(on_bdry) if shape_i == (): out_list.append((bool(on_bdry), bool(on_bdry))) elif shape_i == (2,): out_list.append((bool(on_bdry[0]), bool(on_bdry[1]))) else: raise ValueError('in axis {}: `nodes_on_bdry` has shape {}, ' 'expected (2,)' .format(i, shape_i)) else: raise ValueError('`nodes_on_bdry` has shape {}, expected ({},)' ''.format(shape, length)) return out_list
[docs]def normalized_axes_tuple(axes, ndim): """Return a tuple of ``axes`` converted to positive integers. This function turns negative entries into equivalent positive ones according to standard Python indexing "from the right". Parameters ---------- axes : int or sequence of ints Single integer or integer sequence of arbitrary length. Duplicate entries are not allowed. All entries must fulfill ``-ndim <= axis <= ndim - 1``. ndim : positive int Number of available axes determining the valid axis range. Returns ------- axes_list : tuple of ints The converted tuple of axes. Examples -------- Normalizing a sequence of axes: >>> normalized_axes_tuple([0, -1, 2], ndim=3) (0, 2, 2) Single integer works, too: >>> normalized_axes_tuple(-3, ndim=3) (0,) """ try: axes, axes_in = (int(axes),), axes except TypeError: axes, axes_in = tuple(int(axis) for axis in axes), axes if any(axis != axis_in for axis, axis_in in zip(axes, axes_in)): raise ValueError('`axes` may only contain integers, got {}' ''.format(axes_in)) else: if axes[0] != axes_in: raise TypeError('`axes` must be integer or sequence, got {}' ''.format(axes_in)) if len(set(axes)) != len(axes): raise ValueError('`axes` may not contain duplicate entries') ndim, ndim_in = int(ndim), ndim if ndim <= 0: raise ValueError('`ndim` must be positive, got {}'.format(ndim_in)) axes_arr = np.array(axes) axes_arr[axes_arr < 0] += ndim if np.any((axes_arr < 0) | (axes_arr >= ndim)): raise ValueError('all `axes` entries must satisfy -{0} <= axis < {0}, ' 'got {1}'.format(ndim, axes_in)) return tuple(axes_arr)
[docs]def safe_int_conv(number): """Safely convert a single number to integer.""" try: return int(np.array(number).astype(int, casting='safe')) except TypeError: raise ValueError('cannot safely convert {} to integer'.format(number))
if __name__ == '__main__': from odl.util.testutils import run_doctests run_doctests()