# -*- coding: utf-8 -*-
"""
Array Utilities
===============
Defines array utilities objects.
References
----------
- :cite:`Castro2014a` : Castro, S. (2014). Numpy: Fastest way of computing
diagonal for each row of a 2d array. Retrieved August 22, 2014, from
http://stackoverflow.com/questions/26511401/\
numpy-fastest-way-of-computing-diagonal-for-each-row-of-a-2d-array/\
26517247#26517247
- :cite:`Yorke2014a` : Yorke, R. (2014). Python: Change format of np.array or
allow tolerance in in1d function. Retrieved March 27, 2015, from
http://stackoverflow.com/a/23521245/931625
"""
from __future__ import division, unicode_literals
import numpy as np
try: # pragma: no cover
from collections import Mapping
except ImportError: # pragma: no cover
from collections.abc import Mapping
from contextlib import contextmanager
from colour.constants import DEFAULT_FLOAT_DTYPE, DEFAULT_INT_DTYPE, EPSILON
__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2020 - Colour Developers'
__license__ = 'New BSD License - https://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-developers@colour-science.org'
__status__ = 'Production'
__all__ = [
'as_array', 'as_int_array', 'as_float_array', 'as_numeric', 'as_int',
'as_float', 'as_namedtuple', 'closest_indexes', 'closest',
'normalise_maximum', 'interval', 'is_uniform', 'in_array', 'tstack',
'tsplit', 'row_as_diagonal', 'dot_vector', 'dot_matrix', 'orient',
'centroid', 'linear_conversion', 'lerp', 'fill_nan', 'ndarray_write'
]
[docs]def as_array(a, dtype=DEFAULT_FLOAT_DTYPE):
"""
Converts given :math:`a` variable to *ndarray* with given type.
Parameters
----------
a : object
Variable to convert.
dtype : object
Type to use for conversion.
Returns
-------
ndarray
:math:`a` variable converted to *ndarray*.
Examples
--------
>>> as_array([1, 2, 3])
array([ 1., 2., 3.])
>>> as_array([1, 2, 3], dtype=DEFAULT_INT_DTYPE)
array([1, 2, 3])
"""
return np.asarray(a, dtype)
[docs]def as_int_array(a):
"""
Converts given :math:`a` variable to *ndarray* using the type defined by
:attr:`colour.constant.DEFAULT_INT_DTYPE` attribute.
Parameters
----------
a : object
Variable to convert.
Returns
-------
ndarray
:math:`a` variable converted to *ndarray*.
Examples
--------
>>> as_int_array([1.0, 2.0, 3.0])
array([1, 2, 3])
"""
return as_array(a, DEFAULT_INT_DTYPE)
[docs]def as_float_array(a):
"""
Converts given :math:`a` variable to *ndarray* using the type defined by
:attr:`colour.constant.DEFAULT_FLOAT_DTYPE` attribute.
Parameters
----------
a : object
Variable to convert.
Returns
-------
ndarray
:math:`a` variable converted to *ndarray*.
Examples
--------
>>> as_float_array([1, 2, 3])
array([ 1., 2., 3.])
"""
return as_array(a, DEFAULT_FLOAT_DTYPE)
[docs]def as_numeric(a, dtype=DEFAULT_FLOAT_DTYPE):
"""
Converts given :math:`a` variable to *numeric*. In the event where
:math:`a` cannot be converted, it is passed as is.
Parameters
----------
a : object
Variable to convert.
dtype : object
Type to use for conversion.
Returns
-------
ndarray
:math:`a` variable converted to *numeric*.
Examples
--------
>>> as_numeric(np.array([1]))
1.0
>>> as_numeric(np.arange(10))
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
"""
try:
return dtype(a)
except (TypeError, ValueError):
return a
[docs]def as_int(a):
"""
Converts given :math:`a` variable to *numeric* using the type defined by
:attr:`colour.constant.DEFAULT_INT_DTYPE` attribute. In the event where
:math:`a` cannot be converted, it is converted to *ndarray* using the type
defined by :attr:`colour.constant.DEFAULT_INT_DTYPE` attribute.
Parameters
----------
a : object
Variable to convert.
Returns
-------
ndarray
:math:`a` variable converted to *numeric*.
Warnings
--------
The behaviour of this definition is different than
:func:`colour.utilities.as_numeric` definition when it comes to conversion
failure: the former will forcibly convert :math:`a` variable to *ndarray*
using the type defined by :attr:`colour.constant.DEFAULT_INT_DTYPE`
attribute while the later will pass the :math:`a` variable as is.
Examples
--------
>>> as_int(np.array([1]))
1
>>> as_int(np.arange(10))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
"""
try:
# TODO: Change to "DEFAULT_INT_DTYPE" when and if
# https://github.com/numpy/numpy/issues/11956 is addressed.
return int(a)
except TypeError:
return as_int_array(a)
[docs]def as_float(a):
"""
Converts given :math:`a` variable to *numeric* using the type defined by
:attr:`colour.constant.DEFAULT_FLOAT_DTYPE` attribute. In the event where
:math:`a` cannot be converted, it is converted to *ndarray* using the type
defined by :attr:`colour.constant.DEFAULT_FLOAT_DTYPE` attribute.
Parameters
----------
a : object
Variable to convert.
Returns
-------
ndarray
:math:`a` variable converted to *numeric*.
Warnings
--------
The behaviour of this definition is different than
:func:`colour.utilities.as_numeric` definition when it comes to conversion
failure: the former will forcibly convert :math:`a` variable to *ndarray*
using the type defined by :attr:`colour.constant.DEFAULT_FLOAT_DTYPE`
attribute while the later will pass the :math:`a` variable as is.
Examples
--------
>>> as_float(np.array([1]))
1.0
>>> as_float(np.arange(10))
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
"""
return DEFAULT_FLOAT_DTYPE(a)
[docs]def as_namedtuple(a, named_tuple):
"""
Converts given :math:`a` variable to given *namedtuple* class instance.
:math:`a` can be either a *Numpy* structured array, a *namedtuple*,
a *mapping*, or an *array_like* object. The definition will attempt to
convert it to given *namedtuple*.
Parameters
----------
a : object
Variable to convert.
named_tuple : namedtuple
*namedtuple* class.
Returns
-------
namedtuple
math:`a` variable converted to *namedtuple*.
Examples
--------
>>> from collections import namedtuple
>>> a_a = 1
>>> a_b = 2
>>> a_c = 3
>>> NamedTuple = namedtuple('NamedTuple', 'a b c')
>>> as_namedtuple(NamedTuple(a=1, b=2, c=3), NamedTuple)
NamedTuple(a=1, b=2, c=3)
>>> as_namedtuple({'a': a_a, 'b': a_b, 'c': a_c}, NamedTuple)
NamedTuple(a=1, b=2, c=3)
>>> as_namedtuple([a_a, a_b, a_c], NamedTuple)
NamedTuple(a=1, b=2, c=3)
"""
if isinstance(a, np.ndarray):
if a.dtype.fields is not None:
a = {field: a[field] for field in a.dtype.fields}
if isinstance(a, named_tuple):
return a
elif isinstance(a, Mapping):
return named_tuple(**a)
else:
return named_tuple(*a)
[docs]def closest_indexes(a, b):
"""
Returns the :math:`a` variable closest element indexes to reference
:math:`b` variable elements.
Parameters
----------
a : array_like
Variable to search for the closest element indexes.
b : numeric
Reference variable.
Returns
-------
numeric
Closest :math:`a` variable element indexes.
Examples
--------
>>> a = np.array([24.31357115, 63.62396289, 55.71528816,
... 62.70988028, 46.84480573, 25.40026416])
>>> print(closest_indexes(a, 63))
[3]
>>> print(closest_indexes(a, [63, 25]))
[3 5]
"""
a = np.ravel(a)[:, np.newaxis]
b = np.ravel(b)[np.newaxis, :]
return np.abs(a - b).argmin(axis=0)
[docs]def closest(a, b):
"""
Returns the :math:`a` variable closest elements to reference :math:`b`
variable elements.
Parameters
----------
a : array_like
Variable to search for the closest elements.
b : numeric
Reference variable.
Returns
-------
numeric
Closest :math:`a` variable elements.
Examples
--------
>>> a = np.array([24.31357115, 63.62396289, 55.71528816,
... 62.70988028, 46.84480573, 25.40026416])
>>> closest(a, 63)
array([ 62.70988028])
>>> closest(a, [63, 25])
array([ 62.70988028, 25.40026416])
"""
a = np.array(a)
return a[closest_indexes(a, b)]
[docs]def normalise_maximum(a, axis=None, factor=1, clip=True):
"""
Normalises given *array_like* :math:`a` variable values by :math:`a`
variable maximum value and optionally clip them between.
Parameters
----------
a : array_like
:math:`a` variable to normalise.
axis : numeric, optional
Normalization axis.
factor : numeric, optional
Normalization factor.
clip : bool, optional
Clip values to domain [0, 'factor'].
Returns
-------
ndarray
Maximum normalised :math:`a` variable.
Examples
--------
>>> a = np.array([0.48222001, 0.31654775, 0.22070353])
>>> normalise_maximum(a) # doctest: +ELLIPSIS
array([ 1. , 0.6564384..., 0.4576822...])
"""
a = as_float_array(a)
maximum = np.max(a, axis=axis)
a *= (1 / maximum[..., np.newaxis]) * factor
return np.clip(a, 0, factor) if clip else a
[docs]def interval(distribution, unique=True):
"""
Returns the interval size of given distribution.
Parameters
----------
distribution : array_like
Distribution to retrieve the interval.
unique : bool, optional
Whether to return unique intervals if the distribution is
non-uniformly spaced or the complete intervals
Returns
-------
ndarray
Distribution interval.
Examples
--------
Uniformly spaced variable:
>>> y = np.array([1, 2, 3, 4, 5])
>>> interval(y)
array([ 1.])
>>> interval(y, False)
array([ 1., 1., 1., 1.])
Non-uniformly spaced variable:
>>> y = np.array([1, 2, 3, 4, 8])
>>> interval(y)
array([ 1., 4.])
>>> interval(y, False)
array([ 1., 1., 1., 4.])
"""
distribution = as_float_array(distribution)
i = np.arange(distribution.size - 1)
differences = np.abs(distribution[i + 1] - distribution[i])
if unique:
return np.unique(differences)
else:
return differences
[docs]def in_array(a, b, tolerance=EPSILON):
"""
Tests whether each element of an array is also present in a second array
within given tolerance.
Parameters
----------
a : array_like
Array to test the elements from.
b : array_like
The values against which to test each value of array *a*.
tolerance : numeric, optional
Tolerance value.
Returns
-------
ndarray
A boolean array with *a* shape describing whether an element of *a* is
present in *b* within given tolerance.
References
----------
:cite:`Yorke2014a`
Examples
--------
>>> a = np.array([0.50, 0.60])
>>> b = np.linspace(0, 10, 101)
>>> np.in1d(a, b)
array([ True, False], dtype=bool)
>>> in_array(a, b)
array([ True, True], dtype=bool)
"""
a = as_float_array(a)
b = as_float_array(b)
d = np.abs(np.ravel(a) - b[..., np.newaxis])
return np.any(d <= tolerance, axis=0).reshape(a.shape)
[docs]def tstack(a, dtype=DEFAULT_FLOAT_DTYPE):
"""
Stacks arrays in sequence along the last axis (tail).
Rebuilds arrays divided by :func:`colour.utilities.tsplit`.
Parameters
----------
a : array_like
Array to perform the stacking.
dtype : object
Type to use for initial conversion to *ndarray*.
Returns
-------
ndarray
Examples
--------
>>> a = 0
>>> tstack([a, a, a])
array([ 0., 0., 0.])
>>> a = np.arange(0, 6)
>>> tstack([a, a, a])
array([[ 0., 0., 0.],
[ 1., 1., 1.],
[ 2., 2., 2.],
[ 3., 3., 3.],
[ 4., 4., 4.],
[ 5., 5., 5.]])
>>> a = np.reshape(a, (1, 6))
>>> tstack([a, a, a])
array([[[ 0., 0., 0.],
[ 1., 1., 1.],
[ 2., 2., 2.],
[ 3., 3., 3.],
[ 4., 4., 4.],
[ 5., 5., 5.]]])
>>> a = np.reshape(a, (1, 1, 6))
>>> tstack([a, a, a])
array([[[[ 0., 0., 0.],
[ 1., 1., 1.],
[ 2., 2., 2.],
[ 3., 3., 3.],
[ 4., 4., 4.],
[ 5., 5., 5.]]]])
"""
a = as_array(a, dtype)
return np.concatenate([x[..., np.newaxis] for x in a], axis=-1)
[docs]def tsplit(a, dtype=DEFAULT_FLOAT_DTYPE):
"""
Splits arrays in sequence along the last axis (tail).
Parameters
----------
a : array_like
Array to perform the splitting.
dtype : object
Type to use for initial conversion to *ndarray*.
Returns
-------
ndarray
Examples
--------
>>> a = np.array([0, 0, 0])
>>> tsplit(a)
array([ 0., 0., 0.])
>>> a = np.array(
... [[0, 0, 0],
... [1, 1, 1],
... [2, 2, 2],
... [3, 3, 3],
... [4, 4, 4],
... [5, 5, 5]]
... )
>>> tsplit(a)
array([[ 0., 1., 2., 3., 4., 5.],
[ 0., 1., 2., 3., 4., 5.],
[ 0., 1., 2., 3., 4., 5.]])
>>> a = np.array(
... [[[0, 0, 0],
... [1, 1, 1],
... [2, 2, 2],
... [3, 3, 3],
... [4, 4, 4],
... [5, 5, 5]]]
... )
>>> tsplit(a)
array([[[ 0., 1., 2., 3., 4., 5.]],
<BLANKLINE>
[[ 0., 1., 2., 3., 4., 5.]],
<BLANKLINE>
[[ 0., 1., 2., 3., 4., 5.]]])
"""
a = as_array(a, dtype)
return np.array([a[..., x] for x in range(a.shape[-1])])
[docs]def row_as_diagonal(a):
"""
Returns the per row diagonal matrices of the given array.
Parameters
----------
a : array_like
Array to perform the diagonal matrices computation.
Returns
-------
ndarray
References
----------
:cite:`Castro2014a`
Examples
--------
>>> a = np.array(
... [[0.25891593, 0.07299478, 0.36586996],
... [0.30851087, 0.37131459, 0.16274825],
... [0.71061831, 0.67718718, 0.09562581],
... [0.71588836, 0.76772047, 0.15476079],
... [0.92985142, 0.22263399, 0.88027331]]
... )
>>> row_as_diagonal(a)
array([[[ 0.25891593, 0. , 0. ],
[ 0. , 0.07299478, 0. ],
[ 0. , 0. , 0.36586996]],
<BLANKLINE>
[[ 0.30851087, 0. , 0. ],
[ 0. , 0.37131459, 0. ],
[ 0. , 0. , 0.16274825]],
<BLANKLINE>
[[ 0.71061831, 0. , 0. ],
[ 0. , 0.67718718, 0. ],
[ 0. , 0. , 0.09562581]],
<BLANKLINE>
[[ 0.71588836, 0. , 0. ],
[ 0. , 0.76772047, 0. ],
[ 0. , 0. , 0.15476079]],
<BLANKLINE>
[[ 0.92985142, 0. , 0. ],
[ 0. , 0.22263399, 0. ],
[ 0. , 0. , 0.88027331]]])
"""
a = np.expand_dims(a, -2)
return np.eye(a.shape[-1]) * a
[docs]def dot_vector(m, v):
"""
Convenient wrapper around :func:`np.einsum` with the following subscripts:
*'...ij,...j->...i'*.
It performs the dot product of two arrays where *m* parameter is expected
to be an array of 3x3 matrices and parameter *v* an array of vectors.
Parameters
----------
m : array_like
Array of 3x3 matrices.
v : array_like
Array of vectors.
Returns
-------
ndarray
Examples
--------
>>> m = np.array(
... [[0.7328, 0.4296, -0.1624],
... [-0.7036, 1.6975, 0.0061],
... [0.0030, 0.0136, 0.9834]]
... )
>>> m = np.reshape(np.tile(m, (6, 1)), (6, 3, 3))
>>> v = np.array([0.20654008, 0.12197225, 0.05136952])
>>> v = np.tile(v, (6, 1))
>>> dot_vector(m, v) # doctest: +ELLIPSIS
array([[ 0.1954094..., 0.0620396..., 0.0527952...],
[ 0.1954094..., 0.0620396..., 0.0527952...],
[ 0.1954094..., 0.0620396..., 0.0527952...],
[ 0.1954094..., 0.0620396..., 0.0527952...],
[ 0.1954094..., 0.0620396..., 0.0527952...],
[ 0.1954094..., 0.0620396..., 0.0527952...]])
"""
return np.einsum('...ij,...j->...i', m, v)
[docs]def dot_matrix(a, b):
"""
Convenient wrapper around :func:`np.einsum` with the following subscripts:
*'...ij,...jk->...ik'*.
It performs the dot product of two arrays where *a* parameter is expected
to be an array of 3x3 matrices and parameter *b* another array of of 3x3
matrices.
Parameters
----------
a : array_like
Array of 3x3 matrices.
b : array_like
Array of 3x3 matrices.
Returns
-------
ndarray
Examples
--------
>>> a = np.array(
... [[0.7328, 0.4296, -0.1624],
... [-0.7036, 1.6975, 0.0061],
... [0.0030, 0.0136, 0.9834]]
... )
>>> a = np.reshape(np.tile(a, (6, 1)), (6, 3, 3))
>>> b = a
>>> dot_matrix(a, b) # doctest: +ELLIPSIS
array([[[ 0.2342420..., 1.0418482..., -0.2760903...],
[-1.7099407..., 2.5793226..., 0.1306181...],
[-0.0044203..., 0.0377490..., 0.9666713...]],
<BLANKLINE>
[[ 0.2342420..., 1.0418482..., -0.2760903...],
[-1.7099407..., 2.5793226..., 0.1306181...],
[-0.0044203..., 0.0377490..., 0.9666713...]],
<BLANKLINE>
[[ 0.2342420..., 1.0418482..., -0.2760903...],
[-1.7099407..., 2.5793226..., 0.1306181...],
[-0.0044203..., 0.0377490..., 0.9666713...]],
<BLANKLINE>
[[ 0.2342420..., 1.0418482..., -0.2760903...],
[-1.7099407..., 2.5793226..., 0.1306181...],
[-0.0044203..., 0.0377490..., 0.9666713...]],
<BLANKLINE>
[[ 0.2342420..., 1.0418482..., -0.2760903...],
[-1.7099407..., 2.5793226..., 0.1306181...],
[-0.0044203..., 0.0377490..., 0.9666713...]],
<BLANKLINE>
[[ 0.2342420..., 1.0418482..., -0.2760903...],
[-1.7099407..., 2.5793226..., 0.1306181...],
[-0.0044203..., 0.0377490..., 0.9666713...]]])
"""
return np.einsum('...ij,...jk->...ik', a, b)
[docs]def orient(a, orientation):
"""
Orient given array according to given ``orientation`` value.
Parameters
----------
a : array_like
Array to perform the orientation onto.
orientation : unicode, optional
**{'Flip', 'Flop', '90 CW', '90 CCW', '180'}**
Orientation to perform.
Returns
-------
ndarray
Oriented array.
Examples
--------
>>> a = np.tile(np.arange(5), (5, 1))
>>> a
array([[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4],
[0, 1, 2, 3, 4]])
>>> orient(a, '90 CW')
array([[0, 0, 0, 0, 0],
[1, 1, 1, 1, 1],
[2, 2, 2, 2, 2],
[3, 3, 3, 3, 3],
[4, 4, 4, 4, 4]])
>>> orient(a, 'Flip')
array([[4, 3, 2, 1, 0],
[4, 3, 2, 1, 0],
[4, 3, 2, 1, 0],
[4, 3, 2, 1, 0],
[4, 3, 2, 1, 0]])
"""
if orientation.lower() == 'flip':
return np.fliplr(a)
elif orientation.lower() == 'flop':
return np.flipud(a)
elif orientation.lower() == '90 cw':
return np.rot90(a, 3)
elif orientation.lower() == '90 ccw':
return np.rot90(a)
elif orientation.lower() == '180':
return np.rot90(a, 2)
else:
return a
[docs]def centroid(a):
"""
Computes the centroid indexes of given :math:`a` array.
Parameters
----------
a : array_like
:math:`a` array to compute the centroid indexes.
Returns
-------
ndarray
:math:`a` array centroid indexes.
Examples
--------
>>> a = np.tile(np.arange(0, 5), (5, 1))
>>> centroid(a)
array([2, 3])
"""
a = as_float_array(a)
a_s = np.sum(a)
ranges = [np.arange(0, a.shape[i]) for i in range(a.ndim)]
coordinates = np.meshgrid(*ranges)
a_ci = []
for axis in coordinates:
axis = np.transpose(axis)
# Aligning axis for N-D arrays where N is normalised to
# range [3, :math:`\\\infty`]
for i in range(axis.ndim - 2, 0, -1):
axis = np.rollaxis(axis, i - 1, axis.ndim)
a_ci.append(np.sum(axis * a) // a_s)
return np.array(a_ci).astype(DEFAULT_INT_DTYPE)
[docs]def linear_conversion(a, old_range, new_range):
"""
Performs a simple linear conversion of given array between the old and new
ranges.
Parameters
----------
a : array_like
Array to perform the linear conversion onto.
old_range : array_like
Old range.
new_range : array_like
New range.
Returns
-------
ndarray
Linear conversion result.
Examples
--------
>>> a = np.linspace(0, 1, 10)
>>> linear_conversion(a, np.array([0, 1]), np.array([1, 10]))
array([ 1., 2., 3., 4., 5., 6., 7., 8., 9., 10.])
"""
a = as_float_array(a)
in_min, in_max = tsplit(old_range)
out_min, out_max = tsplit(new_range)
return ((a - in_min) / (in_max - in_min)) * (out_max - out_min) + out_min
[docs]def lerp(a, b, c):
"""
Performs a simple linear interpolation between given array :math:`a` and
array :math:`b` using :math:`c` value.
Parameters
----------
a : array_like
Array :math:`a`, the start of the range in which to interpolate.
b : array_like
Array :math:`b`, the end of the range in which to interpolate.
c : array_like
Array :math:`c` value to use to interpolate between array :math:`a` and
array :math:`b`.
Returns
-------
ndarray
Linear interpolation result.
Examples
--------
>>> a = 0
>>> b = 2
>>> lerp(a, b, 0.5)
1.0
"""
a = as_float_array(a)
b = as_float_array(b)
c = as_float_array(c)
return (1 - c) * a + c * b
[docs]def fill_nan(a, method='Interpolation', default=0):
"""
Fills given array NaNs according to given method.
Parameters
----------
a : array_like
Array to fill the NaNs of.
method : unicode
**{'Interpolation', 'Constant'}**,
*Interpolation* method linearly interpolates through the NaNs,
*Constant* method replaces NaNs with ``default``.
default : numeric
Value to use with the *Constant* method.
Returns
-------
ndarray
NaNs filled array.
Examples
--------
>>> a = np.array([0.1, 0.2, np.nan, 0.4, 0.5])
>>> fill_nan(a)
array([ 0.1, 0.2, 0.3, 0.4, 0.5])
>>> fill_nan(a, method='Constant')
array([ 0.1, 0.2, 0. , 0.4, 0.5])
"""
a = np.copy(a)
mask = np.isnan(a)
if method.lower() == 'interpolation':
a[mask] = np.interp(
np.flatnonzero(mask), np.flatnonzero(~mask), a[~mask])
elif method.lower() == 'constant':
a[mask] = default
return a
[docs]@contextmanager
def ndarray_write(a):
"""
A context manager setting given array writeable to perform an operation
and then read-only.
Parameters
----------
a : array_like
Array to perform an operation.
Returns
-------
ndarray
Array.
Examples
--------
>>> a = np.linspace(0, 1, 10)
>>> a.setflags(write=False)
>>> try:
... a += 1
... except ValueError:
... pass
>>> with ndarray_write(a):
... a +=1
"""
a = as_float_array(a)
a.setflags(write=True)
try:
yield a
finally:
a.setflags(write=False)