"""
This module contains numerical routines for manipulating multi-indices.
"""
from __future__ import annotations
from decimal import Decimal, ROUND_HALF_UP
from math import ceil
from typing import Iterable, no_type_check
import numpy as np
from minterpy.global_settings import INT_DTYPE, NOT_FOUND
from minterpy.jit_compiled.multi_index import is_index_contained, search_lex_sorted, is_lex_smaller_or_equal, \
fill_match_positions, cross_and_sum, unique_indices
from minterpy.utils.arrays import lp_norm, cartesian_product, lp_sum, expand_dim
# if TYPE_CHECKING:
# from .tree import MultiIndexTree
[docs]
def get_poly_degree(exponents: np.ndarray, lp_degree: float) -> int:
"""Get the polynomial degree from a multi-index set for a given lp-degree.
Parameters
----------
exponents : np.ndarray
An array of exponents of a multi-index set.
lp_degree : float
The lp-degree of the multi-index set.
Returns
-------
int
The polynomial degree from the multi-index set
for the given lp-degree.
"""
norms = lp_norm(exponents, lp_degree, axis=1)
max_norm = np.max(norms)
# NOTE: Don't do round half to even (default from round())
max_norm_int = int(Decimal(max_norm).quantize(0, ROUND_HALF_UP))
if np.isclose(max_norm, max_norm_int):
# The nearest integer is equivalent to the maximum norm
# Difference are insignificant, take the integer
return max_norm_int
else:
# Take the ceiling to include all index set elements
# with smaller lp-norm
# NOTE: math.ceil() returns int, np.ceil() returns float
return ceil(max_norm)
[docs]
def get_exponent_matrix(
spatial_dimension: int, poly_degree: int, lp_degree: float | int
) -> np.ndarray:
"""
Generate exponent matrix.
:param spatial_dimension: Dimension of the domain space.
:type spatial_dimension: int
:param poly_degree: Polynomial degree described by the resulting exponent matrix.
:type poly_degree: int
:param lp_degree: Degree of the used lp-norm. This can be any integer bigger than one or `np.inf`.
:type lp_degree: int or float
:return: List of exponent arrays for all monomials, whose lp-norm of the exponents is smaller or equal to the given `poly_degree`. The list is given as a `np.ndarray` with the shape `(number_of_monomials, spatial_dimension)` and the list is lexicographically ordered.
:rtype: np.ndarray
"""
# Validate poly_degree, allow float if finite with integral value
if isinstance(poly_degree, float):
if not poly_degree.is_integer():
raise ValueError(
f"poly_degree needs to be a whole number! <{poly_degree} given."
)
if poly_degree == 0:
right_choices = np.zeros((1, spatial_dimension), dtype=INT_DTYPE)
return right_choices
if lp_degree == np.inf:
right_choices = cartesian_product(
*[np.arange(poly_degree + 1, dtype=INT_DTYPE)] * spatial_dimension
)
else:
candidates_without_diag = cartesian_product(
*[np.arange(poly_degree, dtype=INT_DTYPE)] * spatial_dimension
)
candidates = np.vstack(
(
candidates_without_diag,
np.diag([INT_DTYPE(poly_degree)] * spatial_dimension),
)
)
cond = lp_sum(candidates, lp_degree) <= poly_degree ** lp_degree
right_choices = candidates[cond]
lex_idx = np.lexsort(right_choices.T)
return right_choices[lex_idx]
def _gen_multi_index_exponents_recur(m, n, gamma, gamma2, lp_degree):
"""DEPRECATED. only for reference. TODO remove
NOTE: this is slow for larger problem instances!
build multi index "gamma" depending on lp_degree
NOTE: multi indices are being called "alpha" in the newest interpolation paper
BUG: lp = infinity does not create complete equidistant grids ((n+1)^m entries)
very memory inefficient implementation!
:param m:
:param n:
:param gamma:
:param gamma2:
:param lp_degree:
:return:
"""
# NOTE: these (and only these) copy operations are required! (tested)
gamma2 = gamma2.copy()
gamma0 = gamma.copy()
gamma0[0, m - 1] += 1
out = []
norm = NORM_FCT(gamma0.reshape(-1), lp_degree)
if norm < n and m > 1:
o1 = _gen_multi_index_exponents_recur(m - 1, n, gamma, gamma, lp_degree)
o2 = _gen_multi_index_exponents_recur(m, n, gamma0, gamma0, lp_degree)
out = np.concatenate([o1, o2], axis=0)
elif norm < n and m == 1:
out = np.concatenate(
[gamma2, _gen_multi_index_exponents_recur(m, n, gamma0, gamma0, lp_degree)],
axis=0,
)
elif norm == n and m > 1:
out = np.concatenate(
[
_gen_multi_index_exponents_recur(m - 1, n, gamma, gamma, lp_degree),
gamma0,
],
axis=0,
)
elif norm == n and m == 1:
out = np.concatenate([gamma2, gamma0], axis=0)
elif norm > n:
norm_ = NORM_FCT(gamma.reshape(-1), lp_degree)
if norm_ < n and m > 1:
for j in range(1, m):
gamma0 = gamma # TODO simplify
gamma0[j - 1] = gamma0[j - 1] + 1 # gamm0 -> 1121 broken
if NORM_FCT(gamma0.reshape(-1), lp_degree) <= n:
gamma2 = np.concatenate(
[
gamma2,
_gen_multi_index_exponents_recur(
j, n, gamma0, gamma0, lp_degree
),
],
axis=0,
)
out = gamma2
elif m == 1:
out = gamma2
elif norm_ <= n:
out = gamma
return out
def _gen_multi_index_exponents(spatial_dimension, poly_degree, lp_degree):
"""
creates the array of exponents for the MultiIndex class
DEPRECATED: reference implementations for tests TODO remove
"""
gamma_placeholder = np.zeros((1, spatial_dimension), dtype=INT_DTYPE)
exponents = _gen_multi_index_exponents_recur(
spatial_dimension, poly_degree, gamma_placeholder, gamma_placeholder, lp_degree
)
return exponents
NORM_FCT = lp_norm
def iterate_indices(indices: np.ndarray | Iterable[np.ndarray]) -> Iterable[np.ndarray]:
if isinstance(indices, np.ndarray) and indices.ndim == 1: # just a single index
yield indices
else: # already iterable as is:
yield from indices
[docs]
def gen_backward_neighbors(index: np.ndarray) -> Iterable[np.ndarray]:
"""Yield the backward neighbors of a given multi-index set element.
Parameters
----------
index : :class:`numpy:numpy.ndarray`
Multi-index set element (i.e., a vector of multi-indices),
a one-dimensional array of length ``m``, where ``m`` is the spatial
dimensions.
Returns
-------
`Iterable` [:class:`numpy:numpy.ndarray`]
A generator that yields all the backward neighbors (i.e., all vectors
of multi-indices "smaller by 1") of the given multi-index element.
Examples
--------
>>> my_backward_neighbors = gen_backward_neighbors(np.array([1, 1, 2]))
>>> for my_backward_neighbor in my_backward_neighbors:
... print(my_backward_neighbor)
[0 1 2]
[1 0 2]
[1 1 1]
"""
spatial_dimension = len(index)
for m in range(spatial_dimension):
index_in_dim = index[m]
if index_in_dim == 0:
# a value in a dimension is zero, ignore
continue
backward_neighbor = index.copy()
backward_neighbor[m] = index_in_dim - 1
yield backward_neighbor
[docs]
def gen_missing_backward_neighbors(
indices: np.ndarray
) -> Iterable[np.ndarray]:
"""Yield all the missing backward neighbors for an array of multi-indices.
Parameters
----------
indices : :class:`numpy:numpy.ndarray`
Array of lexicographically sorted multi-indices, a two-dimensional
non-negative integer array of shape ``(N, m)``, where ``N`` is
the number of multi-indices and ``m`` is the number
of spatial dimensions.
Returns
-------
`Iterable` [:class:`numpy:numpy.ndarray`]
A generator that yields all the missing backward neighbors
(i.e., all the missing vectors of multi-indices "smaller by 1")
of the array of multi-indices.
Notes
-----
- This function strictly requires a lexicographically sorted array of
multi-indices but, as a utility function, it does not check for them
for efficiency reason. Higher-level functions that call this function
must make sure that the array is sorted beforehand.
Examples
--------
>>> my_missing_neighbors = gen_missing_backward_neighbors(np.array([[3]]))
>>> for missing_neighbor in my_missing_neighbors:
... print(missing_neighbor)
[2]
>>> my_missing_neighbors = gen_missing_backward_neighbors(
... np.array([[1, 1, 1]])
... )
>>> for missing_neighbor in my_missing_neighbors:
... print(missing_neighbor)
[0 1 1]
[1 0 1]
[1 1 0]
"""
# Set up a caching system
cache = set()
# Loop over the indices
nr_indices = len(indices)
for i in reversed(range(nr_indices)):
# Start with the lexicographically largest element
index = indices[i]
for backward_index in gen_backward_neighbors(index):
# All vectors of multi-index "smaller by 1"
indices2search = indices[:i]
contained = is_index_contained(indices2search, backward_index)
# Check the cache
backward_index_tpl = tuple(backward_index) # must be hashable
in_cache = backward_index_tpl in cache
if not contained and not in_cache:
# Update the cache
cache.add(backward_index_tpl)
yield backward_index
[docs]
def is_complete(
indices: np.ndarray,
poly_degree: int,
lp_degree: float
) -> bool:
"""Check if an array of multi-indices is complete w.r.t poly- & lp-degree.
Parameters
----------
indices : :class:`numpy:numpy.ndarray`
Array of lexicographically sorted multi-indices, a two-dimensional
non-negative integer array of shape ``(N, m)``, where ``N`` is
the number of multi-indices and ``m`` is the number
of spatial dimensions.
poly_degree : int
Polynomial degree supported by the multi-index set.
lp_degree : float
:math:`p` in the :math:`l_p`-norm with respect to which
the multi-index set is defined.
Returns
-------
bool
``True`` if the multi-index set is complete and ``False``
otherwise. The function also returns ``False`` if the array of
multi-indices is not lexicographically sorted (including containing
any duplicate entries).
Notes
-----
- For a definition of completeness, refer to the relevant
:doc:`section </how-to/multi-index-set/multi-index-set-complete>`
of the Minterpy Documentation.
Examples
--------
>>> my_indices = np.array([
... [0, 0, 0],
... [1, 0, 0],
... [0, 1, 0],
... [1, 1, 0],
... [0, 0, 1],
... [1, 0, 1],
... [0, 1, 1],
... [1, 1, 1]])
>>> is_complete(my_indices, poly_degree=1, lp_degree=np.inf)
True
>>> is_complete(my_indices, poly_degree=1, lp_degree=2.0)
False
>>> is_complete(my_indices, poly_degree=2, lp_degree=1.0)
False
"""
m = indices.shape[1] # spatial dimensions
indices_complete = get_exponent_matrix(m, poly_degree, lp_degree)
try:
if np.allclose(indices, indices_complete):
return True
except ValueError:
# Possibly because inconsistent shape of arrays in the comparison above
return False
return False
[docs]
def is_disjoint(
indices_1: np.ndarray,
indices_2: np.ndarray,
expand_dim_: bool = False,
) -> bool:
"""Check if an array of multi-indices is disjoint with another.
Parameters
----------
indices_1 : :class:`numpy:numpy.ndarray`
Two-dimensional integer array of shape ``(N1, m)``, where ``N1`` is
the number of multi-indices and ``m`` is the number
of spatial dimensions.
indices_2 : :class:`numpy:numpy.ndarray`
Two-dimensional integer array of shape ``(N2, m)``, where ``N2`` is
the number of multi-indices and ``m`` is the number
of spatial dimensions.
expand_dim_ : bool, optional
Flag to allow the spatial dimension (i.e., the number of columns) of
the indices to be expanded if there is a difference.
Returns
-------
bool
``True`` if the multi-index sets are disjoint and ``False``
otherwise. The function also returns ``True`` if the number of columns
are different and ``expand_dim`` is set to ``False``.
Examples
--------
>>> my_indices_1 = np.array([
... [0, 0, 0],
... [1, 0, 0],
... [1, 1, 1]])
>>> my_indices_2 = np.array([
... [0, 0, 0],
... [1, 1, 1]])
>>> is_disjoint(my_indices_1, my_indices_2)
False
>>> my_indices_3 = np.array([
... [2, 0, 1],
... [1, 2, 1]])
>>> is_disjoint(my_indices_1, my_indices_3)
True
>>> my_indices_4 = np.array([
... [0, 0],
... [1, 0]])
>>> is_disjoint(my_indices_1, my_indices_4)
True
>>> is_disjoint(my_indices_1, my_indices_4, expand_dim_=True)
False
"""
m_1 = indices_1.shape[1]
m_2 = indices_2.shape[1]
if expand_dim_:
if m_1 < m_2:
indices_1 = expand_dim(indices_1, m_2)
if m_1 > m_2:
indices_2 = expand_dim(indices_2, m_1)
else:
if m_1 != m_2:
return True
# So the search is carried out with the smaller array
n_1 = indices_1.shape[0]
n_2 = indices_2.shape[0]
if n_1 > n_2:
main_indices = indices_1
search_indices = indices_2
else:
main_indices = indices_2
search_indices = indices_1
for search_index in search_indices:
if search_lex_sorted(main_indices, search_index) != NOT_FOUND:
return False
return True
[docs]
def is_downward_closed(indices: np.ndarray) -> bool:
"""Check if an array of multi-indices is downward-closed.
Parameters
----------
indices : :class:`numpy:numpy.ndarray`
Array of lexicographically sorted multi-indices, a two-dimensional
non-negative integer array of shape ``(N, m)``, where ``N`` is
the number of multi-indices and ``m`` is the number
of spatial dimensions.
Returns
-------
bool
``True`` if the multi-index set is downward-closed and ``False``
otherwise. The function also returns ``False`` if the array of
multi-indices is not lexicographically sorted (including containing
any duplicate entries).
Notes
-----
- A multi-index is downward-closed if it does not contain "hole" across
its spatial dimensions.
- Some synonyms for a downward-closed multi-index set are:
"monotonic", "lower", or "lexicographically complete".
Examples
--------
>>> my_indices = np.array([
... [0, 0, 0],
... [1, 0, 0],
... [0, 0, 1]])
>>> is_downward_closed(my_indices)
True
>>> my_indices = np.array([
... [0, 0, 0],
... [1, 0, 0],
... [0, 2, 0]]) # missing [0, 1, 0]
>>> is_downward_closed(my_indices)
False
"""
for _ in gen_missing_backward_neighbors(indices):
# A "hole" is found, i.e., a missing backward neighbor
return False
return True
[docs]
@no_type_check
def list_insert_single(
list_of_indices: list[np.ndarray],
index2insert: np.ndarray,
check_for_inclusion: bool = True,
):
"""inserts a single index into a given list of indices maintaining lexicographical ordering"""
# TODO check length of single index to insert!
if list_of_indices is None:
raise TypeError
nr_of_indices = len(list_of_indices)
if nr_of_indices == 0:
list_of_indices.insert(0, index2insert)
return
insertion_idx = (
nr_of_indices # default: insert at the last position, ATTENTION: -1 not working
)
for i, contained_index in enumerate(list_of_indices):
if is_lex_smaller_or_equal(index2insert, contained_index):
insertion_idx = i
break
# no insertion when an equal entry exists already
# TODO check_for_inclusion
if not np.array_equal(contained_index, index2insert):
list_of_indices.insert(insertion_idx, index2insert)
@no_type_check
def to_index_list(indices: np.ndarray | Iterable[np.ndarray]) -> list[np.ndarray]:
if type(indices) is list:
return indices # already is a list
list_of_indices = list(iterate_indices(indices)) # include all existing indices
return list_of_indices
def to_index_array(list_of_indices: list[np.ndarray]) -> np.ndarray:
# NOTE: shape is: (N, m)
index_array = np.array(list_of_indices, dtype=INT_DTYPE)
return index_array
[docs]
def insert_lexicographically(
indices: list[np.ndarray] | np.ndarray,
indices2insert: Iterable[np.ndarray] | None,
) -> np.ndarray:
"""inserts possibly multiple index vectors into a given array of indices maintaining lexicographical ordering
exploits ordering for increased efficiency:
inserts one vector after another to maintain ordering
NOTE: is expected to return the same identical array instance
if all exponents are already contained!
"""
if indices2insert is None:
return indices
nr_exponents = len(indices)
list_of_indices = None # avoid creating a list when there is no index to insert
for i, index2insert in enumerate(iterate_indices(indices2insert)):
if i == 0: # initialise list
list_of_indices = to_index_list(indices)
list_insert_single(list_of_indices, index2insert)
if (
list_of_indices is None or len(list_of_indices) == nr_exponents
): # len(list_of_indices) or len(indices)
# no index has been inserted.
# ATTENTION: return the previous array in order to easily compare for equality!
return indices
index_array = to_index_array(list_of_indices)
return index_array
def insert_partial_derivatives(list_of_indices, exponent_vector):
for deriv_vect in gen_backward_neighbors(
exponent_vector
): # all vectors "smaller by 1"
list_insert_single(list_of_indices, deriv_vect)
[docs]
def make_derivable(indices: np.ndarray) -> np.ndarray:
"""inserts all missing multi index vectors "smaller by one" """
list_of_indices: list[int] = []
nr_exponents, spatial_dimension = indices.shape
for i in reversed(range(nr_exponents)): # start with the biggest multi index
contained_exponent_vector = indices[i, :]
list_insert_single(list_of_indices, contained_exponent_vector)
insert_partial_derivatives(list_of_indices, contained_exponent_vector)
index_array = to_index_array(list_of_indices)
return index_array
[docs]
def make_complete(exponents: np.ndarray, lp_degree: float) -> np.ndarray:
"""Create a complete exponents from a given array of exponents.
A complete set of exponents contains all monomials, whose :math:`l_p`-norm
of the exponents are smaller or equal to the polynomial degree of the set.
Parameters
----------
exponents : :class:`numpy:numpy.ndarray`
A given exponent array to be completed.
lp_degree : :py:class:`float`
A given :math:`l_p` of the :math:`l_p`-norm (i.e., the lp-degree)
for the completion.
Returns
-------
:class:`numpy:numpy.ndarray`
The complete exponents with respect to the given ``exponents`` and
``lp_degree``.
Notes
-----
- The polynomial degree is inferred from the given exponents with
respect to the required ``lp_degree``. It is the smallest polynomial
degree with respect to the ``lp_degree`` to contain the given exponents.
Examples
--------
>>> exponent = np.array([[2, 2]])
>>> lp_degree = 2.0
>>> make_complete(exponent, lp_degree) # Complete w.r.t the lp_degree
array([[0, 0],
[1, 0],
[2, 0],
[3, 0],
[0, 1],
[1, 1],
[2, 1],
[0, 2],
[1, 2],
[2, 2],
[0, 3]])
"""
poly_degree = get_poly_degree(exponents, lp_degree)
spatial_dimension = exponents.shape[-1]
complete_exponents = get_exponent_matrix(
spatial_dimension, poly_degree, lp_degree
)
return complete_exponents
[docs]
def make_downward_closed(indices: np.ndarray) -> np.ndarray:
"""Make an array of multi-indices downward-closed.
Parameters
----------
indices : :class:`numpy:numpy.ndarray`
Array of lexicographically sorted multi-indices, a two-dimensional
non-negative integer array of shape ``(N, m)``, where ``N`` is
the number of multi-indices and ``m`` is the number of spatial
dimensions.
Returns
-------
:class:`numpy:numpy.ndarray`
Array of downward-closed lexicographically sorted multi-indices,
a two-dimensional array of shape ``(N1, m)``, where ``N1`` is
the number of multi-indices in the downward-closed set and
``m`` is the number of spatial dimensions.
Notes
-----
- This function strictly requires a lexicographically sorted array of
multi-indices but, as a utility function, it does not check for them
for efficiency reason. Higher-level functions that call this function
must make sure that the array is sorted beforehand.
Examples
--------
>>> my_indices = np.array([
... [0, 0],
... [2, 0], # Jump from [1, 0]
... [0, 3], # Jump from [0, 1] and [0, 2]
... ])
>>> make_downward_closed(my_indices)
array([[0, 0],
[1, 0],
[2, 0],
[0, 1],
[0, 2],
[0, 3]])
>>> my_indices = np.array([[1, 1, 1]]) # must be two-dimensional
>>> make_downward_closed(my_indices)
array([[0, 0, 0],
[1, 0, 0],
[0, 1, 0],
[1, 1, 0],
[0, 0, 1],
[1, 0, 1],
[0, 1, 1],
[1, 1, 1]])
"""
# Set up a set to store final indices while avoiding duplications
indices_set = set([tuple(index) for index in indices])
# Initialize the primary iteration condition (input indices)
backward_neighbors = indices
# Set the early break parameter
break_length = -np.inf
while not is_downward_closed(backward_neighbors):
# All missing backward neighbors will eventually become
# downward-closed; this is a cheaper break condition
# than checking for the downward-closeness of the whole set
# everytime the set is updated with the missing backward neighbors.
# Backward neighbors of the current backward neighbors
backward_neighbors = _missing_backward_neighbors(backward_neighbors)
# Update the multi-index set
indices_tuple = [tuple(index) for index in backward_neighbors]
indices_set.update(indices_tuple)
# Early breaking: If the length of indices remains the same after
# two consecutive iterations.
if break_length == len(indices_set):
break
break_length = len(indices_set)
# Make sure the downward-indices are lexicographically sorted.
indices_out = lex_sort(np.array(list(indices_set)))
return indices_out
[docs]
def find_match_between(
smaller_idx_set: np.ndarray, larger_idx_set: np.ndarray
) -> np.ndarray:
"""find the positions of each multi_index of the smaller set in the larger set
NOTE: both sets are required to be ordered lexicographically!
NOTE: the smaller set is required to be a subset of the larger set
NOTE: this function is required for 'rebasing' a polynomial onto complete multi indices (map the coefficients)
"""
nr_exp_smaller, spatial_dimension = smaller_idx_set.shape
positions = np.empty(nr_exp_smaller, dtype=INT_DTYPE)
fill_match_positions(larger_idx_set, smaller_idx_set, positions)
return positions
[docs]
def lex_sort(indices: np.ndarray) -> np.ndarray:
"""Lexicographically sort an array of multi-indices.
Parameters
----------
indices : :class:`numpy:numpy.ndarray`
Two-dimensional array of multi-indices to be sorted.
Returns
-------
:class:`numpy:numpy.ndarray`
Lexicographically sorted array, having the same type as ``indices``.
Only unique entries are kept in the sorted array.
Examples
--------
>>> xx = np.array([
... [0, 1, 2, 3],
... [0, 1, 0, 1],
... [0, 0, 0, 0],
... [0, 0, 1, 1],
... [0, 0, 0, 0],
... ])
>>> lex_sort(xx) # Sort and remove duplicates
array([[0, 0, 0, 0],
[0, 1, 0, 1],
[0, 0, 1, 1],
[0, 1, 2, 3]])
"""
indices_unique = np.unique(indices, axis=0)
indices_lex_sorted = indices_unique[np.lexsort(indices_unique.T)]
return indices_lex_sorted
[docs]
def multiply_indices(
indices_1: np.ndarray,
indices_2: np.ndarray
) -> np.ndarray:
"""Multiply an array of multi-indices with another array of multi-indices.
Parameters
----------
indices_1 : :class:`numpy:numpy.ndarray`
Two-dimensional array of multi-indices of shape ``(N1, m2)`` where
``N1`` is the number of multi-indices and ``m2`` is the number of
dimensions of the first array.
indices_2 : :class:`numpy:numpy.ndarray`
Another two-dimensional array of multi-indices of shape ``(N2, m2)``
where ``N2`` is the number of multi-indices and ``m2`` is the number
of dimensions of the second array.
Returns
-------
:class:`numpy:numpy.ndarray`
The product of ``indices_1`` with ``indices_2`` of shape ``(N3, m3)``
where ``m3`` is the maximum between ``m1`` and ``m2``. The product
array is lexicographically sorted.
Examples
--------
>>> my_indices_1 = np.array([
... [0, 0],
... [1, 0],
... [0, 1],
... [1, 1],
... ])
>>> my_indices_2 = np.array([
... [0, 0],
... [1, 0],
... [0, 1],
... ])
>>> multiply_indices(my_indices_1, my_indices_2)
array([[0, 0],
[1, 0],
[2, 0],
[0, 1],
[1, 1],
[2, 1],
[0, 2],
[1, 2]])
>>> my_indices_3 = np.array([
... [0],
... [1],
... [2],
... ])
>>> multiply_indices(my_indices_2, my_indices_3) # Different dimension
array([[0, 0],
[1, 0],
[2, 0],
[3, 0],
[0, 1],
[1, 1],
[2, 1]])
>>> my_indices_4 = np.empty((0, 2), dtype=INT_DTYPE)
>>> multiply_indices(my_indices_3, my_indices_4) # empty set
array([], shape=(0, 2), dtype=int64)
"""
# --- Adjust the dimension
m_1 = indices_1.shape[1]
m_2 = indices_2.shape[1]
if m_1 < m_2:
indices_1 = expand_dim(indices_1, m_2)
if m_1 > m_2:
indices_2 = expand_dim(indices_2, m_1)
# --- Create a cross-product and sum each pair
prod = cross_and_sum(indices_1, indices_2)
# This product may have zero element
if len(prod) != 0:
# Sort the cross-sum lexicographically
prod = prod[np.lexsort(prod.T)]
# # Take only the unique elements from the sorted array
unique_idx = unique_indices(prod)
prod = prod[unique_idx]
else:
prod = np.empty((0, np.max([m_1, m_2])), dtype=INT_DTYPE)
return prod
[docs]
def union_indices(
indices_1: np.ndarray,
indices_2: np.ndarray,
) -> np.ndarray:
"""Create a union between two arrays of multi-indices.
Parameters
----------
indices_1 : :class:`numpy:numpy.ndarray`
Two-dimensional array of multi-indices of shape ``(N1, m2)`` where
``N1`` is the number of multi-indices and ``m2`` is the number of
dimensions of the first array.
indices_2 : :class:`numpy:numpy.ndarray`
Another two-dimensional array of multi-indices of shape ``(N2, m2)``
where ``N2`` is the number of multi-indices and ``m2`` is the number
of dimensions of the second array.
Returns
-------
:class:`numpy:numpy.ndarray`
The union of ``indices_1`` and ``indices_2`` of shape ``(N3, m3)``
where ``m3`` is the maximum between ``m1`` and ``m2``.
The union is lexicographically sorted.
Examples
--------
>>> my_indices_1 = np.array([
... [0, 0],
... [1, 0],
... [0, 1],
... [1, 1],
... ])
>>> my_indices_2 = np.array([
... [0, 0],
... [1, 0],
... [0, 1],
... ])
>>> union_indices(my_indices_1, my_indices_2)
array([[0, 0],
[1, 0],
[0, 1],
[1, 1]])
>>> my_indices_3 = np.array([
... [0],
... [1],
... [2],
... ])
>>> union_indices(my_indices_1, my_indices_3) # Different dimension
array([[0, 0],
[1, 0],
[2, 0],
[0, 1],
[1, 1]])
>>> my_indices_4 = np.empty((0, 1), dtype=int)
>>> union_indices(my_indices_3, my_indices_4)
array([[0],
[1],
[2]])
"""
# --- Adjust the dimension
m_1 = indices_1.shape[1]
m_2 = indices_2.shape[1]
if m_1 < m_2:
indices_1 = expand_dim(indices_1, m_2)
if m_1 > m_2:
indices_2 = expand_dim(indices_2, m_1)
# --- Take the union between the two indices then lexicographically sort it
union = np.concatenate((indices_1, indices_2), axis=0)
union = lex_sort(union)
return union
def _missing_backward_neighbors(indices: np.ndarray) -> np.ndarray:
"""Create a lexicographically sorted array of missing backward neighbors.
Parameters
----------
indices : :class:`numpy:numpy.ndarray`
Array of lexicographically sorted multi-indices, a two-dimensional
non-negative integer array of shape ``(N, m)``, where ``N`` is
the number of multi-indices and ``m`` is the number of spatial
dimensions.
Returns
-------
:class:`numpy:numpy.ndarray`
Array of missing backward neighbors (i.e., all the missing vectors
of multi-indices "smaller by 1") of the array of multi-indices.
If ``indices`` has no backward neighbor, an empty array is returned.
"""
backward_neighbors = gen_missing_backward_neighbors(indices)
# Convert generator to a NumPy array
backward_neighbors = np.array(list(backward_neighbors))
if len(backward_neighbors) > 0:
return lex_sort(backward_neighbors)
return backward_neighbors
# def list_based(func):
# """ TODO decorator for working with list of indices rather than with static numpy arrays"""
# @functools.wraps(func)
# def list_based_wrapper(*args, **kwargs):
# to_index_list(
# value = func(*args, **kwargs)
# to_index_array(
# return value
# return list_based_wrapper
if __name__ == "__main__":
import doctest
doctest.testmod()