<commit_before># To change this license header, choose License Headers in Project Properties.
# To change this template file, choose Tools | Templates
# and open the template in the editor.

__author__ = "<NAME> <<EMAIL>>"
__date__ = "$May 20, 2014 9:46:45 AM$"


import functools
import operator

import numpy
import scipy

import scipy.spatial
import scipy.ndimage
import scipy.ndimage.morphology
import scipy.stats
import scipy.stats.mstats

import vigra

# Need in order to have logging information no matter what.
import debugging_tools


# Get the logger
logger = debugging_tools.logging.getLogger(__name__)


@debugging_tools.log_call(logger)
def renumber_label_image(new_array):
    """
        Takes a label image with non-consecutive numbering and renumbers it to be consecutive.
        Returns the relabeled image, a mapping from the old labels (by index) to the new ones,
        and a mapping from the new labels back to the old labels.
        
        Args:
            new_array(numpy.ndarray):                               the label image.
            
        Returns:
            (numpy.ndarray, numpy.ndarray, numpy.ndarray):          the relabeled label image, the forward label mapping
                                                                    and the reverse label mapping
        
        Examples:
            >>> renumber_label_image(numpy.array([1, 2, 3]))
            (array([1, 2, 3]), array([0, 1, 2, 3]), array([0, 1, 2, 3]))
            
            >>> renumber_label_image(numpy.array([1, 2, 4]))
            (array([1, 2, 3]), array([0, 1, 2, 0, 3]), array([0, 1, 2, 4]))
            
            >>> renumber_label_image(numpy.array([0, 1, 2, 3]))
            (array([0, 1, 2, 3]), array([0, 1, 2, 3]), array([0, 1, 2, 3]))
            
            >>> renumber_label_image(numpy.array([0, 1, 2, 4]))
            (array([0, 1, 2, 3]), array([0, 1, 2, 0, 3]), array([0, 1, 2, 4]))
    """

    # Get the set of reverse label mapping (ensure the background is always included)
    reverse_label_mapping = numpy.unique(numpy.array([0] + numpy.unique(new_array).tolist()))

    # Get the set of old labels excluding background
    old_labels = reverse_label_mapping[reverse_label_mapping != 0]

    # Get the set of new labels in order
    new_labels = numpy.arange(1, len(old_labels) + 1)

    # Get the forward label mapping (ensure the background is included)
    forward_label_mapping = numpy.zeros((reverse_label_mapping.max() + 1,), dtype = new_array.dtype)
    forward_label_mapping[old_labels] = new_labels

    # Get masks for each old label
    new_array_label_masks = all_permutations_equal(old_labels, new_array)

    # Create tiled where each label is expanded to the size of the new_array
    new_labels_tiled_view = expand_view(new_labels, new_array.shape)

    # Take every mask and make sure it has the appropriate sequential label
    # Then combine each of these parts of the label image together into a new sequential label image
    new_array_relabeled = (new_array_label_masks * new_labels_tiled_view).sum(axis = 0)

    return((new_array_relabeled, forward_label_mapping, reverse_label_mapping))


@debugging_tools.log_call(logger)
def index_axis_at_pos(new_array, axis, pos):
    """
        Indexes an arbitrary axis to the given position, which may be an index, a slice, or any other NumPy allowed
        indexing type. This will return a view.

        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
            axis(int):                           position for the axis to be in the final array.
            pos(int or slice):                   how to index at the given axis.

        Returns:
            (numpy.ndarray):                     a numpy array view of the original array.

        Examples:
            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 0, 0).shape
            (2, 3, 4)

            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 1, 0).shape
            (1, 3, 4)

            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 2, 0).shape
            (1, 2, 4)

            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 3, 0).shape
            (1, 2, 3)

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, 3, 0) == a[:,:,:,0]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, -1, 0) == a[:,:,:,0]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, -1, 2) == a[:,:,:,2]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, 1, 1) == a[:,1,:,:]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, 2, slice(None,None,2)) == a[:,:,::2,:]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 2, 2)[0, 1, 3] = 19; a[0, 1, 2, 3] == 19
            True

    """

    import additional_generators

    # Rescale axis inside the bounds
    axis %= (new_array.ndim)
    if axis < 0:
        axis += new_array.ndim


    # Ordering of the axes to generate
    axis_new_ordering = []
    # Place the chosen axis first (as all axes are positive semi-definite) and then 0 (if it is different from our axis)
    axis_new_ordering += sorted(set([axis, 0]), reverse = True)
    # Skip generating 0 or the chosen axis, but generate all others in normal order
    axis_new_ordering += list(additional_generators.xrange_with_skip(new_array.ndim, to_skip = [0, axis]))

    # Swaps the first with the desired axis (returns a view)
    new_array_swapped = new_array.transpose(axis_new_ordering)
    # Index to pos at the given axis
    new_subarray = new_array_swapped[pos]

    # Check to see if the chosen axis still exists (if pos were a slice)
    if new_subarray.ndim == new_array.ndim:
        # If so, generate the old ordering.
        axis_old_ordering = range(1, axis + 1) + [0] + range(axis + 1, new_array.ndim)
        # Transpose our selction to that ordering.
        new_subarray = new_subarray.transpose(axis_old_ordering)


    return( new_subarray )


@debugging_tools.log_call(logger)
def add_singleton_axis_pos(a_array, new_axis = 0):
    """
        Adds a singleton axis to the given position.
        Allows negative values for new_axis.
        Also, automatically bounds new_axis in an acceptable regime if it is not already.
        
        Args:
            a_array(numpy.ndarray):            array to add the singleton axis to.
            new_axis(int):                     position for the axis to be in the final array (defaults to zero).
        
        Returns:
            (numpy.ndarray):                   a numpy array with the singleton axis added (should be a view).
        
        Examples:
            >>> add_singleton_axis_pos(numpy.ones((7,9,6))).shape
            (1, 7, 9, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), 0).shape
            (1, 7, 9, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = 0).shape
            (1, 7, 9, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = 1).shape
            (7, 1, 9, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = 2).shape
            (7, 9, 1, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = 3).shape
            (7, 9, 6, 1)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = -1).shape
            (7, 9, 6, 1)
            
    """

    # Clean up new_axis to be within the allowable range.
    new_axis %= (a_array.ndim + 1)
    if new_axis < 0:
        new_axis += a_array.ndim + 1

    # Constructing the current ordering of axis and the singleton dime
    new_array_shape = range(1, a_array.ndim + 1)
    new_array_shape.insert(new_axis, 0)
    new_array_shape = tuple(new_array_shape)

    # Adds singleton dimension at front.
    # Then changes the order so it is elsewhere.
    new_array = a_array[None]
    new_array = new_array.transpose(new_array_shape)

    return( new_array )


@debugging_tools.log_call(logger)
def add_singleton_axis_beginning(new_array):
    """
        Adds a singleton axis to the beginning of the array.
        
        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
        
        Returns:
            (numpy.ndarray):                     a numpy array with the singleton axis added at the end (should be view)
        
        Examples:
            >>> add_singleton_axis_beginning(numpy.ones((7,9,6))).shape
            (1, 7, 9, 6)
            
            >>> add_singleton_axis_beginning(numpy.eye(3)).shape
            (1, 3, 3)
            
    """

    # return( new_array[None] )
    return( add_singleton_axis_pos(new_array, new_axis = 0) )


@debugging_tools.log_call(logger)
def add_singleton_axis_end(new_array):
    """
        Adds a singleton axis to the end of the array.
        
        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
        
        Returns:
            (numpy.ndarray):                     a numpy array with the singleton axis added at the end (should be view)
        
        Examples:
            >>> add_singleton_axis_end(numpy.ones((7,9,6))).shape
            (7, 9, 6, 1)
            
            >>> add_singleton_axis_end(numpy.eye(3)).shape
            (3, 3, 1)
            
    """

    # return( numpy.rollaxis(new_array[None], 0, new_array.ndim + 1) )
    return( add_singleton_axis_pos(new_array, new_axis = new_array.ndim) )


@debugging_tools.log_call(logger)
def add_singleton_op(op, new_array, axis):
    """
        Performs an operation on the given array on the specified axis, which otherwise would have eliminated the axis
        in question. This function will instead ensure that the given axis remains after the operation as a singleton.

        Note:
            The operation must be able to take only two arguments where the first is the array and the second is the
            axis to apply the operation along.

        Args:
            op(callable):                 callable that takes a numpy.ndarray and an int in order.
            new_array(numpy.ndarray):     array to perform operation on and add singleton axis too.
            axis(int):                    the axis to apply the operation along and turn into a singleton.

        Returns:
            (numpy.ndarray):              the array with the operation performed.

        Examples:
            >>> add_singleton_op(numpy.max, numpy.ones((7,9,6)), 0).shape
            (1, 9, 6)

            >>> add_singleton_op(numpy.max, numpy.ones((7,9,6)), -1).shape
            (7, 9, 1)

            >>> add_singleton_op(numpy.min, numpy.ones((7,9,6)), 1).shape
            (7, 1, 6)

            >>> add_singleton_op(numpy.mean, numpy.ones((7,9,6)), 1).shape
            (7, 1, 6)
    """

    return(add_singleton_axis_pos(op(new_array, axis), axis))


@debugging_tools.log_call(logger)
def contains(new_array, to_contain):
    """
        Gets a mask array that is true every time something from to_contain appears in new_array.
        
        Args:
            new_array(numpy.ndarray):            array to check for matches.
            to_contain(array_like):              desired matches to find.
        
        Returns:
            (numpy.ndarray):                     a mask for new_array that selects values from to_contain.
        
        Examples:
            >>> contains(numpy.zeros((2,2)), 0)
            array([[ True,  True],
                   [ True,  True]], dtype=bool)
                    
            >>> contains(numpy.zeros((2,2)), 1)
            array([[False, False],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.zeros((2,2)), [1])
            array([[False, False],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.zeros((2,2)), numpy.array([1]))
            array([[False, False],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.arange(4).reshape((2,2)), numpy.array([0]))
            array([[ True, False],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.arange(4).reshape((2,2)), numpy.array([1]))
            array([[False,  True],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.arange(4).reshape((2,2)), numpy.array([2]))
            array([[False, False],
                   [ True, False]], dtype=bool)
                    
            >>> contains(numpy.arange(4).reshape((2,2)), numpy.array([3]))
            array([[False, False],
                   [False,  True]], dtype=bool)
    """
    return(numpy.in1d(new_array, to_contain).reshape(new_array.shape))


@debugging_tools.log_call(logger)
def array_to_matrix(a):
    """
        Flattens an array so that the row is the only original shape kept.

        Args:
            a(numpy.ndarray):                The array to flatten partially

        Returns:
            (numpy.ndarray):                 The matrix version of the array after flattening.

        Examples:
            >>> array_to_matrix(numpy.eye(3))
            array([[ 1.,  0.,  0.],
                   [ 0.,  1.,  0.],
                   [ 0.,  0.,  1.]])

            >>> array_to_matrix(numpy.arange(24).reshape(4, 3, 2))
            array([[ 0,  1,  2,  3,  4,  5],
                   [ 6,  7,  8,  9, 10, 11],
                   [12, 13, 14, 15, 16, 17],
                   [18, 19, 20, 21, 22, 23]])

            >>> array_to_matrix(numpy.arange(24).reshape(4, 6))
            array([[ 0,  1,  2,  3,  4,  5],
                   [ 6,  7,  8,  9, 10, 11],
                   [12, 13, 14, 15, 16, 17],
                   [18, 19, 20, 21, 22, 23]])

            >>> array_to_matrix(numpy.zeros((0, 4, 3, 2)))
            array([], shape=(0, 24), dtype=float64)

    """

    return(a.reshape(a.shape[0], functools.reduce(operator.mul, a.shape[1:])))


@debugging_tools.log_call(logger)
def index_array_to_bool_array(index_array, shape):
    """
        Creates a bool array mask that has each value from the index array as True.
        All other values are False. Requires a shape be specified to create the
        bool array.

        Args:
            index_array(numpy.ndarray of ints):     The index array with the indices to use
            shape(tuple of ints):                   The shape to give the bool_array

        Returns:
            (numpy.ndarray):                        The bool array with selected indices as True rest are False.

        Examples:
            >>> index_array_to_bool_array((numpy.arange(5),), (5,))
            array([ True,  True,  True,  True,  True], dtype=bool)

            >>> index_array_to_bool_array((numpy.arange(1, 4),), (5,))
            array([False,  True,  True,  True, False], dtype=bool)

            >>> index_array_to_bool_array((numpy.arange(3), numpy.arange(3)), (3, 3,))
            array([[ True, False, False],
                   [False,  True, False],
                   [False, False,  True]], dtype=bool)
    """

    bool_array = numpy.zeros(shape, dtype = bool)
    bool_array[index_array] = True

    return(bool_array)



@debugging_tools.log_call(logger)
def expand_view(new_array, reps_after = tuple(), reps_before = tuple()):
    """
        Behaves like NumPy tile except that it always returns a view and not a copy.
        Though, it differs in that additional dimensions are added for repetion as
        opposed to repeating in the same one. Also, it allows repetitions to be
        specified before or after unlike tile. Though, will behave identical to
        tile if the keyword is not specified.
        
        Uses strides to trick NumPy into providing a view.
        
        Args:
            new_array(numpy.ndarray):            array to tile.
            reps_after(tuple):                   repetitions dimension size to add before (if int will turn into tuple).
            reps_before(tuple):                  repetitions dimension size to add after (if int will turn into tuple).
        
        Returns:
            (numpy.ndarray):                     a view of a numpy array with tiling in various dimension.
        
        Examples:
            >>> expand_view(numpy.arange(6).reshape(2,3), 1)
            array([[[0],
                    [1],
                    [2]],
            <BLANKLINE>
                   [[3],
                    [4],
                    [5]]])
                    
            >>> expand_view(numpy.arange(6).reshape(2,3), (1,))
            array([[[0],
                    [1],
                    [2]],
            <BLANKLINE>
                   [[3],
                    [4],
                    [5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_after = 1)
            array([[[0],
                    [1],
                    [2]],
            <BLANKLINE>
                   [[3],
                    [4],
                    [5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_after = (1,))
            array([[[0],
                    [1],
                    [2]],
            <BLANKLINE>
                   [[3],
                    [4],
                    [5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = 1)
            array([[[0, 1, 2],
                    [3, 4, 5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = (1,))
            array([[[0, 1, 2],
                    [3, 4, 5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = (3,))
            array([[[0, 1, 2],
                    [3, 4, 5]],
            <BLANKLINE>
                   [[0, 1, 2],
                    [3, 4, 5]],
            <BLANKLINE>
                   [[0, 1, 2],
                    [3, 4, 5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_after = (4,))
            array([[[0, 0, 0, 0],
                    [1, 1, 1, 1],
                    [2, 2, 2, 2]],
            <BLANKLINE>
                   [[3, 3, 3, 3],
                    [4, 4, 4, 4],
                    [5, 5, 5, 5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = (3,), reps_after = (4,))
            array([[[[0, 0, 0, 0],
                     [1, 1, 1, 1],
                     [2, 2, 2, 2]],
            <BLANKLINE>
                    [[3, 3, 3, 3],
                     [4, 4, 4, 4],
                     [5, 5, 5, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 0, 0, 0],
                     [1, 1, 1, 1],
                     [2, 2, 2, 2]],
            <BLANKLINE>
                    [[3, 3, 3, 3],
                     [4, 4, 4, 4],
                     [5, 5, 5, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 0, 0, 0],
                     [1, 1, 1, 1],
                     [2, 2, 2, 2]],
            <BLANKLINE>
                    [[3, 3, 3, 3],
                     [4, 4, 4, 4],
                     [5, 5, 5, 5]]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_after = (4,3))
            array([[[[0, 0, 0],
                     [0, 0, 0],
                     [0, 0, 0],
                     [0, 0, 0]],
            <BLANKLINE>
                    [[1, 1, 1],
                     [1, 1, 1],
                     [1, 1, 1],
                     [1, 1, 1]],
            <BLANKLINE>
                    [[2, 2, 2],
                     [2, 2, 2],
                     [2, 2, 2],
                     [2, 2, 2]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[3, 3, 3],
                     [3, 3, 3],
                     [3, 3, 3],
                     [3, 3, 3]],
            <BLANKLINE>
                    [[4, 4, 4],
                     [4, 4, 4],
                     [4, 4, 4],
                     [4, 4, 4]],
            <BLANKLINE>
                    [[5, 5, 5],
                     [5, 5, 5],
                     [5, 5, 5],
                     [5, 5, 5]]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = (4,3))
            array([[[[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]]]])
            
    """

    if not isinstance(reps_after, tuple):
        reps_after = (reps_after,)

    if not isinstance(reps_before, tuple):
        reps_before = (reps_before,)

    if (not reps_after) and (not reps_before):
        return(new_array.view())

    return(numpy.lib.stride_tricks.as_strided(new_array, reps_before + new_array.shape + reps_after,
                                               len(reps_before) * (0,) + new_array.strides + len(reps_after) * (0,)) )


def expand_arange(start, stop = None, step = 1, dtype=numpy.int64, reps_before = tuple(), reps_after = tuple()):
    """
        Much like numpy.arange except that it applies expand_view afterwards to get a view of the same arange in a
        larger cube.

        This is very useful for situations where broadcasting is desired.

        Args:
            start(int):                          starting point (or stopping point if only one is specified).
            stop(int):                           stopping point (if the starting point is specified) (0 by default).
            step(int):                           size of steps to take between value (1 by default).
            reps_after(tuple):                   repetitions dimension size to add before (if int will turn into tuple).
            reps_before(tuple):                  repetitions dimension size to add after (if int will turn into tuple).

        Returns:
            (numpy.ndarray):                     a view of a numpy arange with tiling in various dimension.

        Examples:
            >>> expand_arange(3, reps_before=3)
            array([[0, 1, 2],
                   [0, 1, 2],
                   [0, 1, 2]])

            >>> expand_arange(3, reps_after=3)
            array([[0, 0, 0],
                   [1, 1, 1],
                   [2, 2, 2]])

            >>> expand_arange(4, reps_before=3)
            array([[0, 1, 2, 3],
                   [0, 1, 2, 3],
                   [0, 1, 2, 3]])

            >>> expand_arange(4, reps_after=3)
            array([[0, 0, 0],
                   [1, 1, 1],
                   [2, 2, 2],
                   [3, 3, 3]])

            >>> expand_arange(4, reps_before=3, reps_after=2)
            array([[[0, 0],
                    [1, 1],
                    [2, 2],
                    [3, 3]],
            <BLANKLINE>
                   [[0, 0],
                    [1, 1],
                    [2, 2],
                    [3, 3]],
            <BLANKLINE>
                   [[0, 0],
                    [1, 1],
                    [2, 2],
                    [3, 3]]])

    """

    if (stop is None):
        stop = start
        start = 0

    an_arange = numpy.arange(start = start, stop = stop, step = step, dtype = dtype)

    an_arange = expand_view(an_arange, reps_before=reps_before, reps_after=reps_after)

    return(an_arange)


def expand_enumerate(new_array, axis = 0, start = 0, step = 1):
    """
        Builds on expand_arange, which has the same shape as the original array. Specifies the increments to occur along
        the given axis, which by default is the zeroth axis.

        Provides mechanisms for changing the starting value and also the increment.

        Args:
            new_array(numpy.ndarray):            array to enumerate
            axis(int):                           axis to enumerate along (0 by default).
            start(int):                          starting point (0 by default).
            step(int):                           size of steps to take between value (1 by default).

        Returns:
            (numpy.ndarray):                     a view of a numpy arange with tiling in various dimension.

        Examples:
            >>> expand_enumerate(numpy.ones((4,5)))
            array([[0, 0, 0, 0, 0],
                   [1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=0)
            array([[0, 0, 0, 0, 0],
                   [1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=0, start=1)
            array([[1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3],
                   [4, 4, 4, 4, 4]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=0, start=1, step=2)
            array([[1, 1, 1, 1, 1],
                   [3, 3, 3, 3, 3],
                   [5, 5, 5, 5, 5],
                   [7, 7, 7, 7, 7]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=1)
            array([[0, 1, 2, 3, 4],
                   [0, 1, 2, 3, 4],
                   [0, 1, 2, 3, 4],
                   [0, 1, 2, 3, 4]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=1, start=1)
            array([[1, 2, 3, 4, 5],
                   [1, 2, 3, 4, 5],
                   [1, 2, 3, 4, 5],
                   [1, 2, 3, 4, 5]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=1, start=1, step=2)
            array([[1, 3, 5, 7, 9],
                   [1, 3, 5, 7, 9],
                   [1, 3, 5, 7, 9],
                   [1, 3, 5, 7, 9]], dtype=uint64)

    """

    an_enumeration = expand_arange(start = start, stop = start + step * new_array.shape[axis], step = step,
                                   dtype = numpy.uint64,
                                   reps_before = new_array.shape[:axis], reps_after = new_array.shape[(axis+1):])

    return(an_enumeration)


def enumerate_masks(new_masks, axis = 0):
    """
        Takes a mask stack and replaces them by an enumerated stack. In other words, each mask is replaced by a
        consecutive integer (starts with 1 and proceeds to the length of the given axis (0 by default)).

        Note:
            The masks could be recreated by finding the values not equal to zero.

        Args:
            new_masks(numpy.ndarray):            masks to enumerate
            axis(int):                           axis to enumerate along (0 by default).

        Returns:
            (numpy.ndarray):                     an enumerated stack.

        Examples:
            >>> enumerate_masks(numpy.ones((3,3,3), dtype=bool))
            array([[[1, 1, 1],
                    [1, 1, 1],
                    [1, 1, 1]],
            <BLANKLINE>
                   [[2, 2, 2],
                    [2, 2, 2],
                    [2, 2, 2]],
            <BLANKLINE>
                   [[3, 3, 3],
                    [3, 3, 3],
                    [3, 3, 3]]], dtype=uint64)

            >>> enumerate_masks(numpy.array([[[ True, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False, False]],
            ...
            ...                              [[False, False, False, False],
            ...                               [False,  True, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False, False]],
            ...
            ...                              [[False, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False,  True, False],
            ...                               [False, False, False, False]],
            ...
            ...                              [[False, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False,  True]]], dtype=bool))
            array([[[1, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 0]],
            <BLANKLINE>
                   [[0, 0, 0, 0],
                    [0, 2, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 0]],
            <BLANKLINE>
                   [[0, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 3, 0],
                    [0, 0, 0, 0]],
            <BLANKLINE>
                   [[0, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 4]]], dtype=uint64)
    """

    new_enumerated_masks = new_masks * expand_enumerate(new_masks, axis=axis, start=1, step=1)

    return(new_enumerated_masks)


def enumerate_masks_max(new_masks, axis = 0):
    """
        Takes a mask stack and replaces them by the max of an enumerated stack. In other words, each mask is replaced by
        a consecutive integer (starts with 1 and proceeds to the length of the given axis (0 by default)). Afterwards,
        the max is taken along the given axis. However, a singleton dimension is left on the original axis.

        Note:
            The masks could be recreated by finding the values not equal to zero.

        Args:
            new_masks(numpy.ndarray):            masks to enumerate
            axis(int):                           axis to enumerate along (0 by default).

        Returns:
            (numpy.ndarray):                     an enumerated stack.

        Examples:
            >>> enumerate_masks_max(numpy.ones((3,3,3), dtype=bool))
            array([[[3, 3, 3],
                    [3, 3, 3],
                    [3, 3, 3]]], dtype=uint64)

            >>> enumerate_masks_max(numpy.array([[[ True, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False, False]],
            ...
            ...                                  [[False, False, False, False],
            ...                                   [False,  True, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False, False]],
            ...
            ...                                  [[False, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False,  True, False],
            ...                                   [False, False, False, False]],
            ...
            ...                                  [[False, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False,  True]]], dtype=bool))
            array([[[1, 0, 0, 0],
                    [0, 2, 0, 0],
                    [0, 0, 3, 0],
                    [0, 0, 0, 4]]], dtype=uint64)
    """

    if axis < 0:
        axis += new_masks.ndim

    assert(0 <= axis < new_masks.ndim)

    new_enumerated_masks_max = numpy.zeros(new_masks.shape[:axis] + (1,) + new_masks.shape[axis+1:], dtype=numpy.uint64)

    for i in xrange(new_masks.shape[axis]):
        numpy.maximum(new_enumerated_masks_max,
                      (i+1) * add_singleton_axis_pos(index_axis_at_pos(new_masks, axis, i), axis),
                      out=new_enumerated_masks_max)

    return(new_enumerated_masks_max)


@debugging_tools.log_call(logger)
def all_permutations_operation(new_op, new_array_1, new_array_2):
    """
        Takes two arrays and constructs a new array that contains the result
        of new_op on every permutation of elements in each array (like broadcasting).
        
        Suppose that new_result contained the result, then one would find that
        the result of the following operation on the specific indicies
        
        new_op( new_array_1[ i_1_1, i_1_2, ... ], new_array_2[ i_2_1, i_2_2, ... ] )
        
        would be found in new_result as shown
        
        new_result[ i_1_1, i_1_2, ..., i_2_1, i_2_2, ... ]
        
        
        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
        
        Returns:
            (numpy.ndarray):                     a numpy array with the singleton axis added at the end.
        
        Examples:
            >>> all_permutations_operation(operator.add, numpy.ones((1,3)), numpy.eye(2)).shape
            (1, 3, 2, 2)
            
            >>> all_permutations_operation(operator.add, numpy.ones((2,2)), numpy.eye(2)).shape
            (2, 2, 2, 2)
        
            >>> all_permutations_operation(operator.add, numpy.ones((2,2)), numpy.eye(2))
            array([[[[ 2.,  1.],
                     [ 1.,  2.]],
            <BLANKLINE>
                    [[ 2.,  1.],
                     [ 1.,  2.]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ 2.,  1.],
                     [ 1.,  2.]],
            <BLANKLINE>
                    [[ 2.,  1.],
                     [ 1.,  2.]]]])
            
            >>> all_permutations_operation(operator.sub, numpy.ones((2,2)), numpy.eye(2))
            array([[[[ 0.,  1.],
                     [ 1.,  0.]],
            <BLANKLINE>
                    [[ 0.,  1.],
                     [ 1.,  0.]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ 0.,  1.],
                     [ 1.,  0.]],
            <BLANKLINE>
                    [[ 0.,  1.],
                     [ 1.,  0.]]]])
            
            >>> all_permutations_operation(operator.sub, numpy.ones((2,2)), numpy.fliplr(numpy.eye(2)))
            array([[[[ 1.,  0.],
                     [ 0.,  1.]],
            <BLANKLINE>
                    [[ 1.,  0.],
                     [ 0.,  1.]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ 1.,  0.],
                     [ 0.,  1.]],
            <BLANKLINE>
                    [[ 1.,  0.],
                     [ 0.,  1.]]]])
            
            >>> all_permutations_operation(operator.sub, numpy.zeros((2,2)), numpy.eye(2))
            array([[[[-1.,  0.],
                     [ 0., -1.]],
            <BLANKLINE>
                    [[-1.,  0.],
                     [ 0., -1.]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[-1.,  0.],
                     [ 0., -1.]],
            <BLANKLINE>
                    [[-1.,  0.],
                     [ 0., -1.]]]])
            
    """

    new_array_1_tiled = expand_view(new_array_1, reps_after = new_array_2.shape)
    new_array_2_tiled = expand_view(new_array_2, reps_before = new_array_1.shape)

    return( new_op(new_array_1_tiled, new_array_2_tiled) )


@debugging_tools.log_call(logger)
def all_permutations_equal(new_array_1, new_array_2):
    """
        Takes two arrays and constructs a new array that contains the result
        of equality comparison on every permutation of elements in each array (like broadcasting).
        
        Suppose that new_result contained the result, then one would find that
        the result of the following operation on the specific indicies
        
        new_op( new_array_1[ i_1_1, i_1_2, ... ], new_array_2[ i_2_1, i_2_2, ... ] )
        
        would be found in new_result as shown
        
        new_result[ i_1_1, i_1_2, ..., i_2_1, i_2_2, ... ]
        
        
        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
        
        Returns:
            (numpy.ndarray):                     a numpy array with the singleton axis added at the end.
        
        Examples:
            >>> all_permutations_equal(numpy.ones((1,3)), numpy.eye(2)).shape
            (1, 3, 2, 2)
            
            >>> all_permutations_equal(numpy.ones((2,2)), numpy.eye(2)).shape
            (2, 2, 2, 2)
            
            >>> all_permutations_equal(numpy.ones((2,2)), numpy.eye(2))
            array([[[[ True, False],
                     [False,  True]],
            <BLANKLINE>
                    [[ True, False],
                     [False,  True]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ True, False],
                     [False,  True]],
            <BLANKLINE>
                    [[ True, False],
                     [False,  True]]]], dtype=bool)
            
            >>> all_permutations_equal(numpy.ones((2,2)), numpy.fliplr(numpy.eye(2)))
            array([[[[False,  True],
                     [ True, False]],
            <BLANKLINE>
                    [[False,  True],
                     [ True, False]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[False,  True],
                     [ True, False]],
            <BLANKLINE>
                    [[False,  True],
                     [ True, False]]]], dtype=bool)
            
            >>> all_permutations_equal(numpy.zeros((2,2)), numpy.eye(2))
            array([[[[False,  True],
                     [ True, False]],
            <BLANKLINE>
                    [[False,  True],
                     [ True, False]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[False,  True],
                     [ True, False]],
            <BLANKLINE>
                    [[False,  True],
                     [ True, False]]]], dtype=bool)
            
            >>> all_permutations_equal(numpy.zeros((2,2)), numpy.eye(3))
            array([[[[False,  True,  True],
                     [ True, False,  True],
                     [ True,  True, False]],
            <BLANKLINE>
                    [[False,  True,  True],
                     [ True, False,  True],
                     [ True,  True, False]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[False,  True,  True],
                     [ True, False,  True],
                     [ True,  True, False]],
            <BLANKLINE>
                    [[False,  True,  True],
                     [ True, False,  True],
                     [ True,  True, False]]]], dtype=bool)
            
            >>> all_permutations_equal(numpy.arange(4).reshape((2,2)), numpy.arange(2,6).reshape((2,2)))
            array([[[[False, False],
                     [False, False]],
            <BLANKLINE>
                    [[False, False],
                     [False, False]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ True, False],
                     [False, False]],
            <BLANKLINE>
                    [[False,  True],
                     [False, False]]]], dtype=bool)
            
    """

    return( all_permutations_operation(operator.eq, new_array_1, new_array_2) )


class NotNumPyStructuredArrayType(Exception):
    """
        Designed for being thrown if a NumPy Structured Array is recieved.
    """
    pass


@debugging_tools.log_call(logger)
def numpy_structured_array_dtype_generator(new_array):
    """
        Takes a NumPy structured array and returns a generator that goes over
        each name in the structured array and yields the name, type, and shape
        (for the given name).
        
        Args:
            new_array(numpy.ndarray):       the array to get the info dtype from.
        
        Raises:
            (NotNumPyStructuredArrayType):  if it is a normal NumPy array.
        
        Returns:
            (iterator):                     An iterator yielding tuples.
    """

    # Test to see if this is a NumPy Structured Array
    if new_array.dtype.names:
        # Go through each name
        for each_name in new_array.dtype.names:
            # Get the type (want the actual type, not a str or dtype object)
            each_dtype = new_array[each_name].dtype.type
            # Get the shape (will be an empty tuple if no shape, which numpy.dtype accepts)
            each_shape = new_array.dtype[each_name].shape

            yield ( (each_name, each_dtype, each_shape) )
    else:
        raise NotNumPyStructuredArrayType("Not a NumPy structured array.")


@debugging_tools.log_call(logger)
def numpy_array_dtype_list(new_array):
    """
        Takes any NumPy array and returns either a list for a NumPy structured array
        via numpy_structured_array_dtype_generator or if it is a normal NumPy array
        it returns the type used.
        
        Args:
            new_array(numpy.ndarray):       the array to get the dtype info from.
        
        Returns:
            (list or type):                 something that can be given to numpy.dtype
                                            to obtain the new_array.dtype, but is more
                                            malleable than a numpy.dtype. 
    """

    try:
        return(list(numpy_structured_array_dtype_generator(new_array)))
    except NotNumPyStructuredArrayType:
        return(new_array.dtype.type)


@debugging_tools.log_call(logger)
def dot_product(new_vector_set_1, new_vector_set_2):
    """
        Determines the dot product between the two pairs of vectors from each set.
        
        Args:
            new_vector_set_1(numpy.ndarray):      first set of vectors.
            new_vector_set_2(numpy.ndarray):      second set of vectors.
        
        Returns:
            (numpy.ndarray):                      an array with the distances between each pair of vectors.
        
        Examples:
            >>> (dot_product(numpy.eye(2), numpy.eye(2)) == numpy.eye(2)).all()
            True
            
            >>> (dot_product(numpy.eye(10), numpy.eye(10)) == numpy.eye(10)).all()
            True
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[ 1,  0]]))
            array([[ 1.]])
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[ 0,  1]]))
            array([[ 0.]])
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[-1,  0]]))
            array([[-1.]])
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[ 0, -1]]))
            array([[ 0.]])
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[ 1,  1]]))
            array([[ 1.]])

            >>> dot_product(numpy.array([[ True,  False]]), numpy.array([[ True,  True]]))
            array([[ 1.]])
    """

    new_vector_set_1_float = new_vector_set_1.astype(float)
    new_vector_set_2_float = new_vector_set_2.astype(float)

    # Measure the dot product between any two neurons (i.e. related to the angle of separation)
    vector_pairs_dot_product = numpy.dot(new_vector_set_1_float, new_vector_set_2_float.T)

    return(vector_pairs_dot_product)


@debugging_tools.log_call(logger)
def pair_dot_product(new_vector_set):
    """
        Determines the dot product between the vectors in the set.

        Args:
            new_vector_set(numpy.ndarray):        set of vectors.

        Returns:
            (numpy.ndarray):                      an array with the distances between each pair of vectors.

        Examples:
            >>> (pair_dot_product(numpy.eye(2)) == numpy.eye(2)).all()
            True

            >>> (pair_dot_product(numpy.eye(10)) == numpy.eye(10)).all()
            True

            >>> pair_dot_product(numpy.array([[ 1,  0]]))
            array([[ 1.]])

            >>> pair_dot_product(numpy.array([[ 1.,  0.]]))
            array([[ 1.]])

            >>> pair_dot_product(numpy.array([[-1,  0]]))
            array([[ 1.]])

            >>> pair_dot_product(numpy.array([[ 0,  1]]))
            array([[ 1.]])

            >>> pair_dot_product(numpy.array([[ 1,  1]]))
            array([[ 2.]])

            >>> pair_dot_product(numpy.array([[ True,  False]]))
            array([[ 1.]])
    """

    return(dot_product(new_vector_set, new_vector_set))


@debugging_tools.log_call(logger)
def norm(new_vector_set, ord = 2):
    """
        Determines the norm of a vector or a set of vectors.
        
        Args:
            new_vector_set(numpy.ndarray):        either a single vector or a set of vectors (matrix).
            ord(optional):                        basically the same arguments as numpy.linalg.norm
                                                  (though some are redundant here).
        
        Returns:
            (numpy.ndarray):                      an array with .
        
        Examples:
            >>> norm(numpy.array([ 1,  0]), 2).ndim
            0
            
            >>> norm(numpy.array([[ 1,  0]]), 2).ndim
            1
            
            >>> norm(numpy.array([ 1,  0]), 2)
            array(1.0)
            
            >>> norm(numpy.array([ 1,  0]), 1)
            array(1.0)
            
            >>> norm(numpy.array([[ 1,  0]]), 2)
            array([ 1.])
            
            >>> norm(numpy.array([[ 1,  1]]), 1)
            array([ 2.])
            
            >>> norm(numpy.array([[ 1,  1]]), 2)
            array([ 1.41421356])

            >>> norm(numpy.array([[ True,  False]]), 1)
            array([ 1.])

            >>> norm(numpy.array([[ True,  False]]), 2)
            array([ 1.])

            >>> norm(numpy.array([[ True,  True]]), 1)
            array([ 2.])

            >>> norm(numpy.array([[ True,  True]]), 2)
            array([ 1.41421356])
            
            >>> norm(numpy.array([[ 1,  1,  1], [ 1,  0,  1]]), 1)
            array([ 3.,  2.])
            
            >>> norm(numpy.array([[ 1,  1,  1], [ 1,  0,  1]]), 2)
            array([ 1.73205081,  1.41421356])
            
            >>> norm(numpy.array([ 0,  1,  2]))
            array(2.23606797749979)
    """

    new_vector_set_float = new_vector_set.astype(float)

    # Wrap the order parameter so as to avoid passing through numpy.apply_along_axis
    # and risk having it break. Also, makes sure the same function can be used in the
    # two cases.
    def wrapped_norm(new_vector):
        return(numpy.linalg.norm(new_vector, ord = ord))

    result = None

    # Return a scalar NumPy array in the case of a single vector
    # Always return type float as the result.
    if new_vector_set.ndim == 1:
        result = numpy.array(wrapped_norm(new_vector_set_float)).astype(float)
    else:
        result = numpy.apply_along_axis(wrapped_norm, 1, new_vector_set_float).astype(float)

    return(result)


@debugging_tools.log_call(logger)<commit_msg>nanshe/expanded_numpy.py: Added an assertion to expanded_numpy.norm to ensure it has some dimensions.<commit_after># To change this license header, choose License Headers in Project Properties.
# To change this template file, choose Tools | Templates
# and open the template in the editor.

__author__ = "<NAME> <<EMAIL>>"
__date__ = "$May 20, 2014 9:46:45 AM$"


import functools
import operator

import numpy
import scipy

import scipy.spatial
import scipy.ndimage
import scipy.ndimage.morphology
import scipy.stats
import scipy.stats.mstats

import vigra

# Need in order to have logging information no matter what.
import debugging_tools


# Get the logger
logger = debugging_tools.logging.getLogger(__name__)


@debugging_tools.log_call(logger)
def renumber_label_image(new_array):
    """
        Takes a label image with non-consecutive numbering and renumbers it to be consecutive.
        Returns the relabeled image, a mapping from the old labels (by index) to the new ones,
        and a mapping from the new labels back to the old labels.
        
        Args:
            new_array(numpy.ndarray):                               the label image.
            
        Returns:
            (numpy.ndarray, numpy.ndarray, numpy.ndarray):          the relabeled label image, the forward label mapping
                                                                    and the reverse label mapping
        
        Examples:
            >>> renumber_label_image(numpy.array([1, 2, 3]))
            (array([1, 2, 3]), array([0, 1, 2, 3]), array([0, 1, 2, 3]))
            
            >>> renumber_label_image(numpy.array([1, 2, 4]))
            (array([1, 2, 3]), array([0, 1, 2, 0, 3]), array([0, 1, 2, 4]))
            
            >>> renumber_label_image(numpy.array([0, 1, 2, 3]))
            (array([0, 1, 2, 3]), array([0, 1, 2, 3]), array([0, 1, 2, 3]))
            
            >>> renumber_label_image(numpy.array([0, 1, 2, 4]))
            (array([0, 1, 2, 3]), array([0, 1, 2, 0, 3]), array([0, 1, 2, 4]))
    """

    # Get the set of reverse label mapping (ensure the background is always included)
    reverse_label_mapping = numpy.unique(numpy.array([0] + numpy.unique(new_array).tolist()))

    # Get the set of old labels excluding background
    old_labels = reverse_label_mapping[reverse_label_mapping != 0]

    # Get the set of new labels in order
    new_labels = numpy.arange(1, len(old_labels) + 1)

    # Get the forward label mapping (ensure the background is included)
    forward_label_mapping = numpy.zeros((reverse_label_mapping.max() + 1,), dtype = new_array.dtype)
    forward_label_mapping[old_labels] = new_labels

    # Get masks for each old label
    new_array_label_masks = all_permutations_equal(old_labels, new_array)

    # Create tiled where each label is expanded to the size of the new_array
    new_labels_tiled_view = expand_view(new_labels, new_array.shape)

    # Take every mask and make sure it has the appropriate sequential label
    # Then combine each of these parts of the label image together into a new sequential label image
    new_array_relabeled = (new_array_label_masks * new_labels_tiled_view).sum(axis = 0)

    return((new_array_relabeled, forward_label_mapping, reverse_label_mapping))


@debugging_tools.log_call(logger)
def index_axis_at_pos(new_array, axis, pos):
    """
        Indexes an arbitrary axis to the given position, which may be an index, a slice, or any other NumPy allowed
        indexing type. This will return a view.

        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
            axis(int):                           position for the axis to be in the final array.
            pos(int or slice):                   how to index at the given axis.

        Returns:
            (numpy.ndarray):                     a numpy array view of the original array.

        Examples:
            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 0, 0).shape
            (2, 3, 4)

            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 1, 0).shape
            (1, 3, 4)

            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 2, 0).shape
            (1, 2, 4)

            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 3, 0).shape
            (1, 2, 3)

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, 3, 0) == a[:,:,:,0]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, -1, 0) == a[:,:,:,0]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, -1, 2) == a[:,:,:,2]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, 1, 1) == a[:,1,:,:]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); (index_axis_at_pos(a, 2, slice(None,None,2)) == a[:,:,::2,:]).all()
            True

            >>> a = numpy.arange(24).reshape((1,2,3,4)); index_axis_at_pos(a, 2, 2)[0, 1, 3] = 19; a[0, 1, 2, 3] == 19
            True

    """

    import additional_generators

    # Rescale axis inside the bounds
    axis %= (new_array.ndim)
    if axis < 0:
        axis += new_array.ndim


    # Ordering of the axes to generate
    axis_new_ordering = []
    # Place the chosen axis first (as all axes are positive semi-definite) and then 0 (if it is different from our axis)
    axis_new_ordering += sorted(set([axis, 0]), reverse = True)
    # Skip generating 0 or the chosen axis, but generate all others in normal order
    axis_new_ordering += list(additional_generators.xrange_with_skip(new_array.ndim, to_skip = [0, axis]))

    # Swaps the first with the desired axis (returns a view)
    new_array_swapped = new_array.transpose(axis_new_ordering)
    # Index to pos at the given axis
    new_subarray = new_array_swapped[pos]

    # Check to see if the chosen axis still exists (if pos were a slice)
    if new_subarray.ndim == new_array.ndim:
        # If so, generate the old ordering.
        axis_old_ordering = range(1, axis + 1) + [0] + range(axis + 1, new_array.ndim)
        # Transpose our selction to that ordering.
        new_subarray = new_subarray.transpose(axis_old_ordering)


    return( new_subarray )


@debugging_tools.log_call(logger)
def add_singleton_axis_pos(a_array, new_axis = 0):
    """
        Adds a singleton axis to the given position.
        Allows negative values for new_axis.
        Also, automatically bounds new_axis in an acceptable regime if it is not already.
        
        Args:
            a_array(numpy.ndarray):            array to add the singleton axis to.
            new_axis(int):                     position for the axis to be in the final array (defaults to zero).
        
        Returns:
            (numpy.ndarray):                   a numpy array with the singleton axis added (should be a view).
        
        Examples:
            >>> add_singleton_axis_pos(numpy.ones((7,9,6))).shape
            (1, 7, 9, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), 0).shape
            (1, 7, 9, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = 0).shape
            (1, 7, 9, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = 1).shape
            (7, 1, 9, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = 2).shape
            (7, 9, 1, 6)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = 3).shape
            (7, 9, 6, 1)
            
            >>> add_singleton_axis_pos(numpy.ones((7,9,6)), new_axis = -1).shape
            (7, 9, 6, 1)
            
    """

    # Clean up new_axis to be within the allowable range.
    new_axis %= (a_array.ndim + 1)
    if new_axis < 0:
        new_axis += a_array.ndim + 1

    # Constructing the current ordering of axis and the singleton dime
    new_array_shape = range(1, a_array.ndim + 1)
    new_array_shape.insert(new_axis, 0)
    new_array_shape = tuple(new_array_shape)

    # Adds singleton dimension at front.
    # Then changes the order so it is elsewhere.
    new_array = a_array[None]
    new_array = new_array.transpose(new_array_shape)

    return( new_array )


@debugging_tools.log_call(logger)
def add_singleton_axis_beginning(new_array):
    """
        Adds a singleton axis to the beginning of the array.
        
        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
        
        Returns:
            (numpy.ndarray):                     a numpy array with the singleton axis added at the end (should be view)
        
        Examples:
            >>> add_singleton_axis_beginning(numpy.ones((7,9,6))).shape
            (1, 7, 9, 6)
            
            >>> add_singleton_axis_beginning(numpy.eye(3)).shape
            (1, 3, 3)
            
    """

    # return( new_array[None] )
    return( add_singleton_axis_pos(new_array, new_axis = 0) )


@debugging_tools.log_call(logger)
def add_singleton_axis_end(new_array):
    """
        Adds a singleton axis to the end of the array.
        
        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
        
        Returns:
            (numpy.ndarray):                     a numpy array with the singleton axis added at the end (should be view)
        
        Examples:
            >>> add_singleton_axis_end(numpy.ones((7,9,6))).shape
            (7, 9, 6, 1)
            
            >>> add_singleton_axis_end(numpy.eye(3)).shape
            (3, 3, 1)
            
    """

    # return( numpy.rollaxis(new_array[None], 0, new_array.ndim + 1) )
    return( add_singleton_axis_pos(new_array, new_axis = new_array.ndim) )


@debugging_tools.log_call(logger)
def add_singleton_op(op, new_array, axis):
    """
        Performs an operation on the given array on the specified axis, which otherwise would have eliminated the axis
        in question. This function will instead ensure that the given axis remains after the operation as a singleton.

        Note:
            The operation must be able to take only two arguments where the first is the array and the second is the
            axis to apply the operation along.

        Args:
            op(callable):                 callable that takes a numpy.ndarray and an int in order.
            new_array(numpy.ndarray):     array to perform operation on and add singleton axis too.
            axis(int):                    the axis to apply the operation along and turn into a singleton.

        Returns:
            (numpy.ndarray):              the array with the operation performed.

        Examples:
            >>> add_singleton_op(numpy.max, numpy.ones((7,9,6)), 0).shape
            (1, 9, 6)

            >>> add_singleton_op(numpy.max, numpy.ones((7,9,6)), -1).shape
            (7, 9, 1)

            >>> add_singleton_op(numpy.min, numpy.ones((7,9,6)), 1).shape
            (7, 1, 6)

            >>> add_singleton_op(numpy.mean, numpy.ones((7,9,6)), 1).shape
            (7, 1, 6)
    """

    return(add_singleton_axis_pos(op(new_array, axis), axis))


@debugging_tools.log_call(logger)
def contains(new_array, to_contain):
    """
        Gets a mask array that is true every time something from to_contain appears in new_array.
        
        Args:
            new_array(numpy.ndarray):            array to check for matches.
            to_contain(array_like):              desired matches to find.
        
        Returns:
            (numpy.ndarray):                     a mask for new_array that selects values from to_contain.
        
        Examples:
            >>> contains(numpy.zeros((2,2)), 0)
            array([[ True,  True],
                   [ True,  True]], dtype=bool)
                    
            >>> contains(numpy.zeros((2,2)), 1)
            array([[False, False],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.zeros((2,2)), [1])
            array([[False, False],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.zeros((2,2)), numpy.array([1]))
            array([[False, False],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.arange(4).reshape((2,2)), numpy.array([0]))
            array([[ True, False],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.arange(4).reshape((2,2)), numpy.array([1]))
            array([[False,  True],
                   [False, False]], dtype=bool)
                    
            >>> contains(numpy.arange(4).reshape((2,2)), numpy.array([2]))
            array([[False, False],
                   [ True, False]], dtype=bool)
                    
            >>> contains(numpy.arange(4).reshape((2,2)), numpy.array([3]))
            array([[False, False],
                   [False,  True]], dtype=bool)
    """
    return(numpy.in1d(new_array, to_contain).reshape(new_array.shape))


@debugging_tools.log_call(logger)
def array_to_matrix(a):
    """
        Flattens an array so that the row is the only original shape kept.

        Args:
            a(numpy.ndarray):                The array to flatten partially

        Returns:
            (numpy.ndarray):                 The matrix version of the array after flattening.

        Examples:
            >>> array_to_matrix(numpy.eye(3))
            array([[ 1.,  0.,  0.],
                   [ 0.,  1.,  0.],
                   [ 0.,  0.,  1.]])

            >>> array_to_matrix(numpy.arange(24).reshape(4, 3, 2))
            array([[ 0,  1,  2,  3,  4,  5],
                   [ 6,  7,  8,  9, 10, 11],
                   [12, 13, 14, 15, 16, 17],
                   [18, 19, 20, 21, 22, 23]])

            >>> array_to_matrix(numpy.arange(24).reshape(4, 6))
            array([[ 0,  1,  2,  3,  4,  5],
                   [ 6,  7,  8,  9, 10, 11],
                   [12, 13, 14, 15, 16, 17],
                   [18, 19, 20, 21, 22, 23]])

            >>> array_to_matrix(numpy.zeros((0, 4, 3, 2)))
            array([], shape=(0, 24), dtype=float64)

    """

    return(a.reshape(a.shape[0], functools.reduce(operator.mul, a.shape[1:])))


@debugging_tools.log_call(logger)
def index_array_to_bool_array(index_array, shape):
    """
        Creates a bool array mask that has each value from the index array as True.
        All other values are False. Requires a shape be specified to create the
        bool array.

        Args:
            index_array(numpy.ndarray of ints):     The index array with the indices to use
            shape(tuple of ints):                   The shape to give the bool_array

        Returns:
            (numpy.ndarray):                        The bool array with selected indices as True rest are False.

        Examples:
            >>> index_array_to_bool_array((numpy.arange(5),), (5,))
            array([ True,  True,  True,  True,  True], dtype=bool)

            >>> index_array_to_bool_array((numpy.arange(1, 4),), (5,))
            array([False,  True,  True,  True, False], dtype=bool)

            >>> index_array_to_bool_array((numpy.arange(3), numpy.arange(3)), (3, 3,))
            array([[ True, False, False],
                   [False,  True, False],
                   [False, False,  True]], dtype=bool)
    """

    bool_array = numpy.zeros(shape, dtype = bool)
    bool_array[index_array] = True

    return(bool_array)



@debugging_tools.log_call(logger)
def expand_view(new_array, reps_after = tuple(), reps_before = tuple()):
    """
        Behaves like NumPy tile except that it always returns a view and not a copy.
        Though, it differs in that additional dimensions are added for repetion as
        opposed to repeating in the same one. Also, it allows repetitions to be
        specified before or after unlike tile. Though, will behave identical to
        tile if the keyword is not specified.
        
        Uses strides to trick NumPy into providing a view.
        
        Args:
            new_array(numpy.ndarray):            array to tile.
            reps_after(tuple):                   repetitions dimension size to add before (if int will turn into tuple).
            reps_before(tuple):                  repetitions dimension size to add after (if int will turn into tuple).
        
        Returns:
            (numpy.ndarray):                     a view of a numpy array with tiling in various dimension.
        
        Examples:
            >>> expand_view(numpy.arange(6).reshape(2,3), 1)
            array([[[0],
                    [1],
                    [2]],
            <BLANKLINE>
                   [[3],
                    [4],
                    [5]]])
                    
            >>> expand_view(numpy.arange(6).reshape(2,3), (1,))
            array([[[0],
                    [1],
                    [2]],
            <BLANKLINE>
                   [[3],
                    [4],
                    [5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_after = 1)
            array([[[0],
                    [1],
                    [2]],
            <BLANKLINE>
                   [[3],
                    [4],
                    [5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_after = (1,))
            array([[[0],
                    [1],
                    [2]],
            <BLANKLINE>
                   [[3],
                    [4],
                    [5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = 1)
            array([[[0, 1, 2],
                    [3, 4, 5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = (1,))
            array([[[0, 1, 2],
                    [3, 4, 5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = (3,))
            array([[[0, 1, 2],
                    [3, 4, 5]],
            <BLANKLINE>
                   [[0, 1, 2],
                    [3, 4, 5]],
            <BLANKLINE>
                   [[0, 1, 2],
                    [3, 4, 5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_after = (4,))
            array([[[0, 0, 0, 0],
                    [1, 1, 1, 1],
                    [2, 2, 2, 2]],
            <BLANKLINE>
                   [[3, 3, 3, 3],
                    [4, 4, 4, 4],
                    [5, 5, 5, 5]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = (3,), reps_after = (4,))
            array([[[[0, 0, 0, 0],
                     [1, 1, 1, 1],
                     [2, 2, 2, 2]],
            <BLANKLINE>
                    [[3, 3, 3, 3],
                     [4, 4, 4, 4],
                     [5, 5, 5, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 0, 0, 0],
                     [1, 1, 1, 1],
                     [2, 2, 2, 2]],
            <BLANKLINE>
                    [[3, 3, 3, 3],
                     [4, 4, 4, 4],
                     [5, 5, 5, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 0, 0, 0],
                     [1, 1, 1, 1],
                     [2, 2, 2, 2]],
            <BLANKLINE>
                    [[3, 3, 3, 3],
                     [4, 4, 4, 4],
                     [5, 5, 5, 5]]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_after = (4,3))
            array([[[[0, 0, 0],
                     [0, 0, 0],
                     [0, 0, 0],
                     [0, 0, 0]],
            <BLANKLINE>
                    [[1, 1, 1],
                     [1, 1, 1],
                     [1, 1, 1],
                     [1, 1, 1]],
            <BLANKLINE>
                    [[2, 2, 2],
                     [2, 2, 2],
                     [2, 2, 2],
                     [2, 2, 2]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[3, 3, 3],
                     [3, 3, 3],
                     [3, 3, 3],
                     [3, 3, 3]],
            <BLANKLINE>
                    [[4, 4, 4],
                     [4, 4, 4],
                     [4, 4, 4],
                     [4, 4, 4]],
            <BLANKLINE>
                    [[5, 5, 5],
                     [5, 5, 5],
                     [5, 5, 5],
                     [5, 5, 5]]]])
            
            >>> expand_view(numpy.arange(6).reshape((2,3)), reps_before = (4,3))
            array([[[[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]],
            <BLANKLINE>
                    [[0, 1, 2],
                     [3, 4, 5]]]])
            
    """

    if not isinstance(reps_after, tuple):
        reps_after = (reps_after,)

    if not isinstance(reps_before, tuple):
        reps_before = (reps_before,)

    if (not reps_after) and (not reps_before):
        return(new_array.view())

    return(numpy.lib.stride_tricks.as_strided(new_array, reps_before + new_array.shape + reps_after,
                                               len(reps_before) * (0,) + new_array.strides + len(reps_after) * (0,)) )


def expand_arange(start, stop = None, step = 1, dtype=numpy.int64, reps_before = tuple(), reps_after = tuple()):
    """
        Much like numpy.arange except that it applies expand_view afterwards to get a view of the same arange in a
        larger cube.

        This is very useful for situations where broadcasting is desired.

        Args:
            start(int):                          starting point (or stopping point if only one is specified).
            stop(int):                           stopping point (if the starting point is specified) (0 by default).
            step(int):                           size of steps to take between value (1 by default).
            reps_after(tuple):                   repetitions dimension size to add before (if int will turn into tuple).
            reps_before(tuple):                  repetitions dimension size to add after (if int will turn into tuple).

        Returns:
            (numpy.ndarray):                     a view of a numpy arange with tiling in various dimension.

        Examples:
            >>> expand_arange(3, reps_before=3)
            array([[0, 1, 2],
                   [0, 1, 2],
                   [0, 1, 2]])

            >>> expand_arange(3, reps_after=3)
            array([[0, 0, 0],
                   [1, 1, 1],
                   [2, 2, 2]])

            >>> expand_arange(4, reps_before=3)
            array([[0, 1, 2, 3],
                   [0, 1, 2, 3],
                   [0, 1, 2, 3]])

            >>> expand_arange(4, reps_after=3)
            array([[0, 0, 0],
                   [1, 1, 1],
                   [2, 2, 2],
                   [3, 3, 3]])

            >>> expand_arange(4, reps_before=3, reps_after=2)
            array([[[0, 0],
                    [1, 1],
                    [2, 2],
                    [3, 3]],
            <BLANKLINE>
                   [[0, 0],
                    [1, 1],
                    [2, 2],
                    [3, 3]],
            <BLANKLINE>
                   [[0, 0],
                    [1, 1],
                    [2, 2],
                    [3, 3]]])

    """

    if (stop is None):
        stop = start
        start = 0

    an_arange = numpy.arange(start = start, stop = stop, step = step, dtype = dtype)

    an_arange = expand_view(an_arange, reps_before=reps_before, reps_after=reps_after)

    return(an_arange)


def expand_enumerate(new_array, axis = 0, start = 0, step = 1):
    """
        Builds on expand_arange, which has the same shape as the original array. Specifies the increments to occur along
        the given axis, which by default is the zeroth axis.

        Provides mechanisms for changing the starting value and also the increment.

        Args:
            new_array(numpy.ndarray):            array to enumerate
            axis(int):                           axis to enumerate along (0 by default).
            start(int):                          starting point (0 by default).
            step(int):                           size of steps to take between value (1 by default).

        Returns:
            (numpy.ndarray):                     a view of a numpy arange with tiling in various dimension.

        Examples:
            >>> expand_enumerate(numpy.ones((4,5)))
            array([[0, 0, 0, 0, 0],
                   [1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=0)
            array([[0, 0, 0, 0, 0],
                   [1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=0, start=1)
            array([[1, 1, 1, 1, 1],
                   [2, 2, 2, 2, 2],
                   [3, 3, 3, 3, 3],
                   [4, 4, 4, 4, 4]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=0, start=1, step=2)
            array([[1, 1, 1, 1, 1],
                   [3, 3, 3, 3, 3],
                   [5, 5, 5, 5, 5],
                   [7, 7, 7, 7, 7]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=1)
            array([[0, 1, 2, 3, 4],
                   [0, 1, 2, 3, 4],
                   [0, 1, 2, 3, 4],
                   [0, 1, 2, 3, 4]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=1, start=1)
            array([[1, 2, 3, 4, 5],
                   [1, 2, 3, 4, 5],
                   [1, 2, 3, 4, 5],
                   [1, 2, 3, 4, 5]], dtype=uint64)

            >>> expand_enumerate(numpy.ones((4,5)), axis=1, start=1, step=2)
            array([[1, 3, 5, 7, 9],
                   [1, 3, 5, 7, 9],
                   [1, 3, 5, 7, 9],
                   [1, 3, 5, 7, 9]], dtype=uint64)

    """

    an_enumeration = expand_arange(start = start, stop = start + step * new_array.shape[axis], step = step,
                                   dtype = numpy.uint64,
                                   reps_before = new_array.shape[:axis], reps_after = new_array.shape[(axis+1):])

    return(an_enumeration)


def enumerate_masks(new_masks, axis = 0):
    """
        Takes a mask stack and replaces them by an enumerated stack. In other words, each mask is replaced by a
        consecutive integer (starts with 1 and proceeds to the length of the given axis (0 by default)).

        Note:
            The masks could be recreated by finding the values not equal to zero.

        Args:
            new_masks(numpy.ndarray):            masks to enumerate
            axis(int):                           axis to enumerate along (0 by default).

        Returns:
            (numpy.ndarray):                     an enumerated stack.

        Examples:
            >>> enumerate_masks(numpy.ones((3,3,3), dtype=bool))
            array([[[1, 1, 1],
                    [1, 1, 1],
                    [1, 1, 1]],
            <BLANKLINE>
                   [[2, 2, 2],
                    [2, 2, 2],
                    [2, 2, 2]],
            <BLANKLINE>
                   [[3, 3, 3],
                    [3, 3, 3],
                    [3, 3, 3]]], dtype=uint64)

            >>> enumerate_masks(numpy.array([[[ True, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False, False]],
            ...
            ...                              [[False, False, False, False],
            ...                               [False,  True, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False, False]],
            ...
            ...                              [[False, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False,  True, False],
            ...                               [False, False, False, False]],
            ...
            ...                              [[False, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False, False],
            ...                               [False, False, False,  True]]], dtype=bool))
            array([[[1, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 0]],
            <BLANKLINE>
                   [[0, 0, 0, 0],
                    [0, 2, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 0]],
            <BLANKLINE>
                   [[0, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 3, 0],
                    [0, 0, 0, 0]],
            <BLANKLINE>
                   [[0, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 0],
                    [0, 0, 0, 4]]], dtype=uint64)
    """

    new_enumerated_masks = new_masks * expand_enumerate(new_masks, axis=axis, start=1, step=1)

    return(new_enumerated_masks)


def enumerate_masks_max(new_masks, axis = 0):
    """
        Takes a mask stack and replaces them by the max of an enumerated stack. In other words, each mask is replaced by
        a consecutive integer (starts with 1 and proceeds to the length of the given axis (0 by default)). Afterwards,
        the max is taken along the given axis. However, a singleton dimension is left on the original axis.

        Note:
            The masks could be recreated by finding the values not equal to zero.

        Args:
            new_masks(numpy.ndarray):            masks to enumerate
            axis(int):                           axis to enumerate along (0 by default).

        Returns:
            (numpy.ndarray):                     an enumerated stack.

        Examples:
            >>> enumerate_masks_max(numpy.ones((3,3,3), dtype=bool))
            array([[[3, 3, 3],
                    [3, 3, 3],
                    [3, 3, 3]]], dtype=uint64)

            >>> enumerate_masks_max(numpy.array([[[ True, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False, False]],
            ...
            ...                                  [[False, False, False, False],
            ...                                   [False,  True, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False, False]],
            ...
            ...                                  [[False, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False,  True, False],
            ...                                   [False, False, False, False]],
            ...
            ...                                  [[False, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False, False],
            ...                                   [False, False, False,  True]]], dtype=bool))
            array([[[1, 0, 0, 0],
                    [0, 2, 0, 0],
                    [0, 0, 3, 0],
                    [0, 0, 0, 4]]], dtype=uint64)
    """

    if axis < 0:
        axis += new_masks.ndim

    assert(0 <= axis < new_masks.ndim)

    new_enumerated_masks_max = numpy.zeros(new_masks.shape[:axis] + (1,) + new_masks.shape[axis+1:], dtype=numpy.uint64)

    for i in xrange(new_masks.shape[axis]):
        numpy.maximum(new_enumerated_masks_max,
                      (i+1) * add_singleton_axis_pos(index_axis_at_pos(new_masks, axis, i), axis),
                      out=new_enumerated_masks_max)

    return(new_enumerated_masks_max)


@debugging_tools.log_call(logger)
def all_permutations_operation(new_op, new_array_1, new_array_2):
    """
        Takes two arrays and constructs a new array that contains the result
        of new_op on every permutation of elements in each array (like broadcasting).
        
        Suppose that new_result contained the result, then one would find that
        the result of the following operation on the specific indicies
        
        new_op( new_array_1[ i_1_1, i_1_2, ... ], new_array_2[ i_2_1, i_2_2, ... ] )
        
        would be found in new_result as shown
        
        new_result[ i_1_1, i_1_2, ..., i_2_1, i_2_2, ... ]
        
        
        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
        
        Returns:
            (numpy.ndarray):                     a numpy array with the singleton axis added at the end.
        
        Examples:
            >>> all_permutations_operation(operator.add, numpy.ones((1,3)), numpy.eye(2)).shape
            (1, 3, 2, 2)
            
            >>> all_permutations_operation(operator.add, numpy.ones((2,2)), numpy.eye(2)).shape
            (2, 2, 2, 2)
        
            >>> all_permutations_operation(operator.add, numpy.ones((2,2)), numpy.eye(2))
            array([[[[ 2.,  1.],
                     [ 1.,  2.]],
            <BLANKLINE>
                    [[ 2.,  1.],
                     [ 1.,  2.]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ 2.,  1.],
                     [ 1.,  2.]],
            <BLANKLINE>
                    [[ 2.,  1.],
                     [ 1.,  2.]]]])
            
            >>> all_permutations_operation(operator.sub, numpy.ones((2,2)), numpy.eye(2))
            array([[[[ 0.,  1.],
                     [ 1.,  0.]],
            <BLANKLINE>
                    [[ 0.,  1.],
                     [ 1.,  0.]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ 0.,  1.],
                     [ 1.,  0.]],
            <BLANKLINE>
                    [[ 0.,  1.],
                     [ 1.,  0.]]]])
            
            >>> all_permutations_operation(operator.sub, numpy.ones((2,2)), numpy.fliplr(numpy.eye(2)))
            array([[[[ 1.,  0.],
                     [ 0.,  1.]],
            <BLANKLINE>
                    [[ 1.,  0.],
                     [ 0.,  1.]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ 1.,  0.],
                     [ 0.,  1.]],
            <BLANKLINE>
                    [[ 1.,  0.],
                     [ 0.,  1.]]]])
            
            >>> all_permutations_operation(operator.sub, numpy.zeros((2,2)), numpy.eye(2))
            array([[[[-1.,  0.],
                     [ 0., -1.]],
            <BLANKLINE>
                    [[-1.,  0.],
                     [ 0., -1.]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[-1.,  0.],
                     [ 0., -1.]],
            <BLANKLINE>
                    [[-1.,  0.],
                     [ 0., -1.]]]])
            
    """

    new_array_1_tiled = expand_view(new_array_1, reps_after = new_array_2.shape)
    new_array_2_tiled = expand_view(new_array_2, reps_before = new_array_1.shape)

    return( new_op(new_array_1_tiled, new_array_2_tiled) )


@debugging_tools.log_call(logger)
def all_permutations_equal(new_array_1, new_array_2):
    """
        Takes two arrays and constructs a new array that contains the result
        of equality comparison on every permutation of elements in each array (like broadcasting).
        
        Suppose that new_result contained the result, then one would find that
        the result of the following operation on the specific indicies
        
        new_op( new_array_1[ i_1_1, i_1_2, ... ], new_array_2[ i_2_1, i_2_2, ... ] )
        
        would be found in new_result as shown
        
        new_result[ i_1_1, i_1_2, ..., i_2_1, i_2_2, ... ]
        
        
        Args:
            new_array(numpy.ndarray):            array to add the singleton axis to.
        
        Returns:
            (numpy.ndarray):                     a numpy array with the singleton axis added at the end.
        
        Examples:
            >>> all_permutations_equal(numpy.ones((1,3)), numpy.eye(2)).shape
            (1, 3, 2, 2)
            
            >>> all_permutations_equal(numpy.ones((2,2)), numpy.eye(2)).shape
            (2, 2, 2, 2)
            
            >>> all_permutations_equal(numpy.ones((2,2)), numpy.eye(2))
            array([[[[ True, False],
                     [False,  True]],
            <BLANKLINE>
                    [[ True, False],
                     [False,  True]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ True, False],
                     [False,  True]],
            <BLANKLINE>
                    [[ True, False],
                     [False,  True]]]], dtype=bool)
            
            >>> all_permutations_equal(numpy.ones((2,2)), numpy.fliplr(numpy.eye(2)))
            array([[[[False,  True],
                     [ True, False]],
            <BLANKLINE>
                    [[False,  True],
                     [ True, False]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[False,  True],
                     [ True, False]],
            <BLANKLINE>
                    [[False,  True],
                     [ True, False]]]], dtype=bool)
            
            >>> all_permutations_equal(numpy.zeros((2,2)), numpy.eye(2))
            array([[[[False,  True],
                     [ True, False]],
            <BLANKLINE>
                    [[False,  True],
                     [ True, False]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[False,  True],
                     [ True, False]],
            <BLANKLINE>
                    [[False,  True],
                     [ True, False]]]], dtype=bool)
            
            >>> all_permutations_equal(numpy.zeros((2,2)), numpy.eye(3))
            array([[[[False,  True,  True],
                     [ True, False,  True],
                     [ True,  True, False]],
            <BLANKLINE>
                    [[False,  True,  True],
                     [ True, False,  True],
                     [ True,  True, False]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[False,  True,  True],
                     [ True, False,  True],
                     [ True,  True, False]],
            <BLANKLINE>
                    [[False,  True,  True],
                     [ True, False,  True],
                     [ True,  True, False]]]], dtype=bool)
            
            >>> all_permutations_equal(numpy.arange(4).reshape((2,2)), numpy.arange(2,6).reshape((2,2)))
            array([[[[False, False],
                     [False, False]],
            <BLANKLINE>
                    [[False, False],
                     [False, False]]],
            <BLANKLINE>
            <BLANKLINE>
                   [[[ True, False],
                     [False, False]],
            <BLANKLINE>
                    [[False,  True],
                     [False, False]]]], dtype=bool)
            
    """

    return( all_permutations_operation(operator.eq, new_array_1, new_array_2) )


class NotNumPyStructuredArrayType(Exception):
    """
        Designed for being thrown if a NumPy Structured Array is recieved.
    """
    pass


@debugging_tools.log_call(logger)
def numpy_structured_array_dtype_generator(new_array):
    """
        Takes a NumPy structured array and returns a generator that goes over
        each name in the structured array and yields the name, type, and shape
        (for the given name).
        
        Args:
            new_array(numpy.ndarray):       the array to get the info dtype from.
        
        Raises:
            (NotNumPyStructuredArrayType):  if it is a normal NumPy array.
        
        Returns:
            (iterator):                     An iterator yielding tuples.
    """

    # Test to see if this is a NumPy Structured Array
    if new_array.dtype.names:
        # Go through each name
        for each_name in new_array.dtype.names:
            # Get the type (want the actual type, not a str or dtype object)
            each_dtype = new_array[each_name].dtype.type
            # Get the shape (will be an empty tuple if no shape, which numpy.dtype accepts)
            each_shape = new_array.dtype[each_name].shape

            yield ( (each_name, each_dtype, each_shape) )
    else:
        raise NotNumPyStructuredArrayType("Not a NumPy structured array.")


@debugging_tools.log_call(logger)
def numpy_array_dtype_list(new_array):
    """
        Takes any NumPy array and returns either a list for a NumPy structured array
        via numpy_structured_array_dtype_generator or if it is a normal NumPy array
        it returns the type used.
        
        Args:
            new_array(numpy.ndarray):       the array to get the dtype info from.
        
        Returns:
            (list or type):                 something that can be given to numpy.dtype
                                            to obtain the new_array.dtype, but is more
                                            malleable than a numpy.dtype. 
    """

    try:
        return(list(numpy_structured_array_dtype_generator(new_array)))
    except NotNumPyStructuredArrayType:
        return(new_array.dtype.type)


@debugging_tools.log_call(logger)
def dot_product(new_vector_set_1, new_vector_set_2):
    """
        Determines the dot product between the two pairs of vectors from each set.
        
        Args:
            new_vector_set_1(numpy.ndarray):      first set of vectors.
            new_vector_set_2(numpy.ndarray):      second set of vectors.
        
        Returns:
            (numpy.ndarray):                      an array with the distances between each pair of vectors.
        
        Examples:
            >>> (dot_product(numpy.eye(2), numpy.eye(2)) == numpy.eye(2)).all()
            True
            
            >>> (dot_product(numpy.eye(10), numpy.eye(10)) == numpy.eye(10)).all()
            True
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[ 1,  0]]))
            array([[ 1.]])
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[ 0,  1]]))
            array([[ 0.]])
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[-1,  0]]))
            array([[-1.]])
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[ 0, -1]]))
            array([[ 0.]])
            
            >>> dot_product(numpy.array([[ 1,  0]]), numpy.array([[ 1,  1]]))
            array([[ 1.]])

            >>> dot_product(numpy.array([[ True,  False]]), numpy.array([[ True,  True]]))
            array([[ 1.]])
    """

    new_vector_set_1_float = new_vector_set_1.astype(float)
    new_vector_set_2_float = new_vector_set_2.astype(float)

    # Measure the dot product between any two neurons (i.e. related to the angle of separation)
    vector_pairs_dot_product = numpy.dot(new_vector_set_1_float, new_vector_set_2_float.T)

    return(vector_pairs_dot_product)


@debugging_tools.log_call(logger)
def pair_dot_product(new_vector_set):
    """
        Determines the dot product between the vectors in the set.

        Args:
            new_vector_set(numpy.ndarray):        set of vectors.

        Returns:
            (numpy.ndarray):                      an array with the distances between each pair of vectors.

        Examples:
            >>> (pair_dot_product(numpy.eye(2)) == numpy.eye(2)).all()
            True

            >>> (pair_dot_product(numpy.eye(10)) == numpy.eye(10)).all()
            True

            >>> pair_dot_product(numpy.array([[ 1,  0]]))
            array([[ 1.]])

            >>> pair_dot_product(numpy.array([[ 1.,  0.]]))
            array([[ 1.]])

            >>> pair_dot_product(numpy.array([[-1,  0]]))
            array([[ 1.]])

            >>> pair_dot_product(numpy.array([[ 0,  1]]))
            array([[ 1.]])

            >>> pair_dot_product(numpy.array([[ 1,  1]]))
            array([[ 2.]])

            >>> pair_dot_product(numpy.array([[ True,  False]]))
            array([[ 1.]])
    """

    return(dot_product(new_vector_set, new_vector_set))


@debugging_tools.log_call(logger)
def norm(new_vector_set, ord = 2):
    """
        Determines the norm of a vector or a set of vectors.
        
        Args:
            new_vector_set(numpy.ndarray):        either a single vector or a set of vectors (matrix).
            ord(optional):                        basically the same arguments as numpy.linalg.norm
                                                  (though some are redundant here).
        
        Returns:
            (numpy.ndarray):                      an array with .
        
        Examples:
            >>> norm(numpy.array([ 1,  0]), 2).ndim
            0
            
            >>> norm(numpy.array([[ 1,  0]]), 2).ndim
            1
            
            >>> norm(numpy.array([ 1,  0]), 2)
            array(1.0)
            
            >>> norm(numpy.array([ 1,  0]), 1)
            array(1.0)
            
            >>> norm(numpy.array([[ 1,  0]]), 2)
            array([ 1.])
            
            >>> norm(numpy.array([[ 1,  1]]), 1)
            array([ 2.])
            
            >>> norm(numpy.array([[ 1,  1]]), 2)
            array([ 1.41421356])

            >>> norm(numpy.array([[ True,  False]]), 1)
            array([ 1.])

            >>> norm(numpy.array([[ True,  False]]), 2)
            array([ 1.])

            >>> norm(numpy.array([[ True,  True]]), 1)
            array([ 2.])

            >>> norm(numpy.array([[ True,  True]]), 2)
            array([ 1.41421356])
            
            >>> norm(numpy.array([[ 1,  1,  1], [ 1,  0,  1]]), 1)
            array([ 3.,  2.])
            
            >>> norm(numpy.array([[ 1,  1,  1], [ 1,  0,  1]]), 2)
            array([ 1.73205081,  1.41421356])
            
            >>> norm(numpy.array([ 0,  1,  2]))
            array(2.23606797749979)
    """

    # Needs to have at least one vector
    assert(new_vector_set.ndim >= 1)

    new_vector_set_float = new_vector_set.astype(float)

    # Wrap the order parameter so as to avoid passing through numpy.apply_along_axis
    # and risk having it break. Also, makes sure the same function can be used in the
    # two cases.
    def wrapped_norm(new_vector):
        return(numpy.linalg.norm(new_vector, ord = ord))

    result = None

    # Return a scalar NumPy array in the case of a single vector
    # Always return type float as the result.
    if new_vector_set.ndim == 1:
        result = numpy.array(wrapped_norm(new_vector_set_float)).astype(float)
    else:
        result = numpy.apply_along_axis(wrapped_norm, 1, new_vector_set_float).astype(float)

    return(result)


@debugging_tools.log_call(logger)
{'fraction_non_alphanumeric': 0.12890294923847406, 'fraction_numerical': 0.032284990004328024, 'mean_word_length': 1.345223422509002, 'pattern_counts': {'":': 0, '<': 173, '<?xml version=': 0, '>': 848, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 168, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6240525', 'n_tokens_mistral': 31078, 'n_tokens_neox': 28310, 'n_words': 7731}