Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add additional functions to pyxx.arrays #17

Merged
merged 10 commits into from
Sep 24, 2022
Merged
10 changes: 10 additions & 0 deletions docs/source/_templates/api_reference_arrays_equality.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.. spelling:word-list::

np


{{ fullname | escape | underline}}

.. currentmodule:: {{ module }}

.. auto{{ objtype }}:: {{ objname }}
24 changes: 23 additions & 1 deletion docs/source/api_reference/arrays.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
.. spelling:word-list::

args
np
tol


pyxx.arrays
===========

Expand All @@ -18,6 +25,19 @@ convert one or more arrays of one type to a different type.
convert_to_tuple


Array Equality
--------------

The functions in this section are intended to check whether arrays have equal
size and/or content.

.. autosummary::
:toctree: ./api
:template: ../_templates/api_reference_arrays_equality.rst

is_array_equal


Array Size
----------

Expand All @@ -26,6 +46,8 @@ array-like objects.

.. autosummary::
:toctree: ./api
:template: ../_templates/api_reference_arrays_max_len.rst
:template: ../_templates/api_reference_arrays_size.rst

check_len_equal
is_len_equal
max_list_item_len
6 changes: 6 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

sys.path.append(str(pathlib.Path(__file__).resolve().parents[2]))
import pyxx
from pyxx.arrays.functions.equality import Array_or_Number_or_String


# -- Project information -----------------------------------------------------
Expand Down Expand Up @@ -127,6 +128,11 @@
'show-inheritance': True,
}

# Type aliases
autodoc_type_aliases = {
Array_or_Number_or_String: Array_or_Number_or_String,
}


# -- Matplotlib plotting extension options -----------------------------------
# https://matplotlib.org/stable/api/sphinxext_plot_directive_api.html
Expand Down
6 changes: 1 addition & 5 deletions pyxx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,4 @@


# PROGRAM VERSION ------------------------------------------------------------
_VERSION_MAJOR = 1
_VERSION_MINOR = 0
_VERSION_PATCH = 0

__version__ = f'{_VERSION_MAJOR}.{_VERSION_MINOR}.{_VERSION_PATCH}'
__version__ = '1.0.0'
7 changes: 6 additions & 1 deletion pyxx/arrays/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@
"""

from .functions.convert import convert_to_tuple
from .functions.size import max_list_item_len
from .functions.equality import is_array_equal
from .functions.size import (
check_len_equal,
is_len_equal,
max_list_item_len,
)
157 changes: 157 additions & 0 deletions pyxx/arrays/functions/equality.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""Functions for evaluating equality of array-like objects
"""

from __future__ import annotations
from numbers import Number
from typing import List, Tuple, Union

import numpy as np

from .size import is_len_equal


# Type alias: type for each `item` argument in `is_array_equal()`
__element_types = Union[Number, np.ndarray, str]
Array_or_Number_or_String = Union[List[__element_types],
Tuple[__element_types],
__element_types]


def is_array_equal(item1: Array_or_Number_or_String,
item2: Array_or_Number_or_String,
*args: Array_or_Number_or_String,
tol: float = 1e-16):
"""Checks that arrays are equal in shape and content

Returns ``True`` if all arrays passed as arguments are of the same shape
and all elements are equal within a given tolerance ``tol`` (for numeric
elements) or exactly equal (for string elements), and returns ``False``
otherwise. Inputs can be lists, tuples, NumPy arrays, numbers, strings,
or nested arrays composed of any of these types.

Parameters
----------
item1 : list or tuple or np.ndarray or Number or str
First array to evaluate
item2 : list or tuple or np.ndarray or Number or str
Second item to evaluate
*args : list or tuple or np.ndarray or Number or str, optional
Any other arrays to be evaluated
tol : float, optional
Maximum difference between numeric values to consider equivalent
(default is ``1e-16``)

Returns
-------
bool
Whether ``item1``, ``item2``, ``*args`` have the same shape, and
all elements are equal within tolerance ``tol`` (for numeric elements)
and exactly equal (for string elements)

Warnings
--------
- The shape of the input arrays must be identical for the arrays to be
considered equal. The shape of numbers is considered different from the
shape of lists, so observe that ``0`` and ``[0]`` are **not** considered
equal in shape.

- By default, NumPy arrays are of homogeneous type. This means that, for
instance, ``pyxx.arrays.is_array_equal(np.array([1, 'a']), [1, 'a'])``
evaluates to ``False`` (because the NumPy array is converted to all
strings). To avoid this issue, it is possible to create NumPy arrays
with the ``dtype=object`` argument and allow mixed types. For example,
``pyxx.arrays.is_array_equal(np.array([1, 'a'], dtype=object), [1, 'a'])``
evaluates to ``True``.

Notes
-----
**Recursion Limit:** Internally, :py:func:`is_array_equal` is a recursive
function. It is possible that for extremely large nested arrays, Python's
recursion limit may be reached. If this occurs and it is necessary to
compare such a large array, consider increasing the recursion limit using
the `sys.setrecursionlimit() <https://docs.python.org/3/library/sys.html
#sys.setrecursionlimit>`__ function.

**Purpose:** One question that may arise is, *why is this function
necessary?* NumPy already offers functions like `numpy.array_equal()
<https://numpy.org/doc/stable/reference/generated
/numpy.array_equal.html>`__, `numpy.isclose() <https://numpy.org/doc
/stable/reference/generated/numpy.isclose.html>`__, and `numpy.allclose()
<https://numpy.org/doc/stable/reference/generated/numpy.allclose.html>`__.

There are several main advantages of :py:func:`is_array_equal`:

- NumPy requires that arrays are numeric and are not "ragged" (sub-lists
must all have the same length, recursively. For example, the array
``x = [[1,2,3], [1,2]]`` is "ragged" since ``len(x[0]) != len(x[1])``).
In contrast, :py:func:`is_array_equal` can compare arrays with a mix of
strings, numbers, lists, and tuples, as well as "ragged" arrays.

- The NumPy functions mentioned will typically throw an exception if the
array sizes being compared differ, but :py:func:`is_array_equal` simply
returns ``False`` in this case. This can eliminate the need to catch
exceptions for certain applications.
"""
# Create list of array(s) to compare with `item1`
items = [item2] + list(args)

# Check whether each of the input arguments is an array-like object
is_array = [isinstance(x, (list, tuple, np.ndarray))
for x in [item1, *items]]

# If inputs are numbers, directly compare them (requiring difference
# between numbers to be less than or equal to `tol` to consider the
# inputs equal)
if isinstance(item1, Number) \
or (isinstance(item1, np.ndarray) and item1.ndim == 0):
for x in items:
# Check whether `item2` or any of `args` are an array. If so,
# this indicates that the array shapes are not equal
if isinstance(x, (list, tuple)) \
or (isinstance(x, np.ndarray) and x.ndim > 0):
return False

# Argument `item1` is known to be a number, so attempt to see
# whether each corresponding element in the other input arrays
# is within tolerance `tol`
try:
# Disable Mypy warnings on the following line, since errors
# will be handled with the try statement
if abs(x - item1) > tol: # type: ignore
return False

except TypeError:
return False

return True

# If inputs are array-like objects, compare their contents
elif any(is_array):
# Verify that inputs are array-like objects
if not all(is_array):
return False

# Verify that all inputs have equal length
if not is_len_equal(item1, *items):
return False

# Check whether each sub-array's elements are equal (recursively)
for i, x in enumerate(item1):

# Disable Mypy warnings on the following line, since we've
# already checked that the lengths of `x` and all elements
# in `items` are equal
if not is_array_equal(x, *[item[i] for item in items], # type: ignore
tol=tol):
return False

return True

else:
# Inputs are not numbers or array-like objects, so try to directly
# compare them. This allows strings, user-defined classes/types,
# or other objects to be compared
for x in items:
if item1 != x:
return False
return True
83 changes: 82 additions & 1 deletion pyxx/arrays/functions/size.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,88 @@
"""Functions for evaluating or comparing sizes of array-like objects
"""

from typing import Union
from typing import Any, Union


def check_len_equal(item1: Any, item2: Any, *args: Any):
"""Checks whether the lengths of a set of sequence-type objects
are equal

Evaluates the lengths of a set of items (such as lists, tuples, or
strings), returning whether all items have the same length as well as
either the length of all items (if all lengths are equal) or a list
containing the lengths of each item (if they are not equal).

Parameters
----------
item1 : Any
First item whose length to evaluate
item2 : Any
Second item whose length to evaluate
*args : Any, optional
Any other items whose lengths are to be evaluated

Returns
-------
bool
Whether all items have the same length
int or list
Returns an integer containing the length of all items (if all
lengths are equal), or a list containing the lengths of each
item (if lengths differ)

See Also
--------
is_len_equal :
Identical functionality, but returns only the ``bool`` output and
may theoretically run slightly faster in cases where the length(s)
of the inputs does not need to be returned
"""
lengths = [len(item1)] + [len(item2)] + [len(i) for i in args]

if len(set(lengths)) == 1:
return True, lengths[0]
else:
return False, lengths


def is_len_equal(item1: Any, item2: Any, *args: Any):
"""Checks whether the lengths of a set of sequence-type objects
are equal

Evaluates the lengths of a set of items (such as lists, tuples, or
strings), returning whether all items have the same length. This
function should be slightly faster than :py:func:`check_len_equal`
for applications where the lengths of the input arguments do not
need to be returned.

Parameters
----------
item1 : Any
First item whose length to evaluate
item2 : Any
Second item whose length to evaluate
*args : Any, optional
Any other items whose lengths are to be evaluated

Returns
-------
bool
Whether all items have the same length

See Also
--------
check_len_equal :
Identical functionality, but additionally returns the length of the
input arguments
"""
length1 = len(item1)

for item in [item2] + list(args):
if not len(item) == length1:
return False

return True


def max_list_item_len(input_list: Union[list, tuple]):
Expand Down
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@
# that you use the `.vscode/requirements.txt` file to install dependencies.

##############################################################################

numpy
13 changes: 13 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import io
import pathlib
import os
import shutil
import sys


# Define variables available to all tests
Expand All @@ -10,6 +12,17 @@


# Define context managers to facilitate testing
class CapturePrint:
"""Captures text printed to the terminal when running commands"""
def __enter__(self):
self.terminal_stdout = io.StringIO()
sys.stdout = self.terminal_stdout
return self.terminal_stdout

def __exit__(self, *args, **kwargs):
sys.stdout = sys.__stdout__


class CreateTempTestDir:
"""Sets up temporary folder for reading/writing test files"""
def __enter__(self):
Expand Down
1 change: 1 addition & 0 deletions tests/arrays/functions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .test_convert import *
from .test_equality import *
from .test_size import *
Loading