Source code for nanoutils.numpy_utils

"""Utility functions related to :mod:`numpy`.

Note that these functions require the NumPy package.

See Also
--------
.. image:: https://badge.fury.io/py/numpy.svg
    :target: https://badge.fury.io/py/numpy

**NumPy** is the fundamental package needed for scientific computing with Python.
It provides:

* a powerful N-dimensional array object
* sophisticated (broadcasting) functions
* tools for integrating C/C++ and Fortran code
* useful linear algebra, Fourier transform, and random number capabilities


Index
-----
.. currentmodule:: nanoutils
.. autosummary::
{autosummary}

API
---
{autofunction}

"""

from __future__ import annotations

from math import factorial, nan
from typing import Iterable, Any
from itertools import combinations
from collections import abc

from .utils import raise_if, construct_api_doc
from .typing_utils import ArrayLike, DtypeLike

try:
    import numpy as np
    NUMPY_EX: None | Exception = None
except Exception as ex:
    NUMPY_EX = ex

from ._dtype_mapping import DTypeMapping, MutableDTypeMapping

__all__ = [
    'as_nd_array',
    'array_combinations',
    'fill_diagonal_blocks',
    'DTypeMapping',
    'MutableDTypeMapping',
]


[docs]@raise_if(NUMPY_EX) def as_nd_array( array: Iterable[Any] | ArrayLike, dtype: DtypeLike, ndmin: int = 1, copy: bool = False, ) -> np.ndarray: """Construct a numpy array from an iterable or array-like object. Examples -------- .. doctest:: python :skipif: NUMPY_EX is not None >>> from nanoutils import as_nd_array >>> as_nd_array(1, int) array([1]) >>> as_nd_array([1, 2, 3, 4], int) array([1, 2, 3, 4]) >>> iterator = iter([1, 2, 3, 4]) >>> as_nd_array(iterator, int) array([1, 2, 3, 4]) Parameters ---------- array : :class:`~collections.abc.Iterable` or array-like An array-like object or an iterable consisting of scalars. dtype : :class:`type` or :class:`numpy.dtype` The data type of the to-be returned array. ndmin : :class:`int` The minimum dimensionality of the to-be returned array. copy : :class:`bool` If :data:`True`, always return a copy. Returns ------- :class:`numpy.ndarray` A numpy array constructed from **value**. """ try: return np.array(array, dtype=dtype, ndmin=ndmin, copy=copy) except TypeError as ex: if not isinstance(array, abc.Iterable): raise ex ret: np.ndarray = np.fromiter(array, dtype=dtype) ret.shape += (ndmin - ret.ndim) * (1,) return ret
[docs]@raise_if(NUMPY_EX) def array_combinations( array: ArrayLike, r: int = 2, axis: int = -1, ) -> np.ndarray: r"""Construct an array with all :func:`~itertools.combinations` of **ar** along a use-specified axis. Examples -------- .. doctest:: python :skipif: NUMPY_EX is not None >>> from nanoutils import array_combinations >>> array = [[1, 2, 3, 4], ... [5, 6, 7, 8]] >>> array_combinations(array, r=2) array([[[1, 2], [5, 6]], <BLANKLINE> [[1, 3], [5, 7]], <BLANKLINE> [[1, 4], [5, 8]], <BLANKLINE> [[2, 3], [6, 7]], <BLANKLINE> [[2, 4], [6, 8]], <BLANKLINE> [[3, 4], [7, 8]]]) Parameters ---------- array : array-like, shape :math:`(m, \dotsc)` An :math:`n` dimensional array-like object. r : :class:`int` The length of each combination. axis : :class:`int` The axis used for constructing the combinations. Returns ------- :class:`numpy.ndarray`, shape :math:`(k, \dotsc, r)` A :math:`n+1` dimensional array with all **ar** combinations (of length ``r``) along the user-specified **axis**. The variable :math:`k` herein represents the number of combinations: :math:`k = \dfrac{m! / r!}{(m-r)!}`. """ # noqa: E501 ar = np.array(array, ndmin=1, copy=False) n = ar.shape[axis] # Identify the number of combinations try: combinations_len = int(factorial(n) / factorial(r) / factorial(n - r)) except ValueError as ex: raise ValueError(f"'r' ({r!r}) expects a positive integer larger than or equal to the " f"length of 'array' axis {axis!r} ({n!r})") from ex # Define the shape of the to-be returned array _shape = list(ar.shape) del _shape[axis] shape = (combinations_len,) + tuple(_shape) + (r,) # Create, fill and return the new array ret = np.empty(shape, dtype=ar.dtype) for i, idx in enumerate(combinations(range(n), r=r)): ret[i] = ar.take(idx, axis=axis) return ret
[docs]@raise_if(NUMPY_EX) def fill_diagonal_blocks( array: np.ndarray, i: int, j: int, val: float = nan, ) -> None: """Fill diagonal blocks in **array** of size :math:`(i, j)`. The blocks are filled along the last 2 axes in **array**. Performs an inplace update of **array**. Examples -------- .. doctest:: python :skipif: NUMPY_EX is not None >>> import numpy as np >>> from nanoutils import fill_diagonal_blocks >>> array = np.zeros((10, 15), dtype=int) >>> i = 2 >>> j = 3 >>> fill_diagonal_blocks(array, i, j, val=1) >>> print(array) [[1 1 1 0 0 0 0 0 0 0 0 0 0 0 0] [1 1 1 0 0 0 0 0 0 0 0 0 0 0 0] [0 0 0 1 1 1 0 0 0 0 0 0 0 0 0] [0 0 0 1 1 1 0 0 0 0 0 0 0 0 0] [0 0 0 0 0 0 1 1 1 0 0 0 0 0 0] [0 0 0 0 0 0 1 1 1 0 0 0 0 0 0] [0 0 0 0 0 0 0 0 0 1 1 1 0 0 0] [0 0 0 0 0 0 0 0 0 1 1 1 0 0 0] [0 0 0 0 0 0 0 0 0 0 0 0 1 1 1] [0 0 0 0 0 0 0 0 0 0 0 0 1 1 1]] Parameters ---------- array : :class:`numpy.ndarray` A >= 2D NumPy array whose diagonal blocks are to be filled. Gets modified in-place. i : :class:`int` The size of the diagonal blocks along axis -2. j : :class:`int` The size of the diagonal blocks along axis -1. val : :class:`float` Value to be written on the diagonal. Its type must be compatible with that of the array **a**. :rtype: :data:`None` """ if (j <= 0) or (i <= 0): raise ValueError(f"'i' and 'j' should be larger than 0; observed values: {i} & {j}") i0 = j0 = 0 dim1 = array.shape[-2] while dim1 > i0: array[..., i0:i0+i, j0:j0+j] = val i0 += i j0 += j
__doc__ = construct_api_doc(globals())