Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions ndcontainers/abc/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def astype(self, dtype, casting='unsafe', copy=True):

@abc.abstractmethod
def reshape(self, *shape, copy=True):
"""Gives a new shape to a sparse array without changing its data.
"""Gives a new shape to a array without changing its data.

Parameters
----------
Expand All @@ -78,15 +78,15 @@ def reshape(self, *shape, copy=True):
copy : bool, optional
Indicates whether or not attributes of self should be copied
whenever possible. The degree to which attributes are copied varies
depending on the type of sparse array being used.
depending on the type of array being used.

Returns
-------
reshaped_array : SparseArray
A sparse array with the given `shape`, not necessarily of the same
reshaped_array :Array
A array with the given `shape`, not necessarily of the same
format as the current object.`
"""
if not len(shape):
if shape == ((),):
shape = np.array([])
else:
try:
Expand All @@ -103,9 +103,13 @@ def reshape(self, *shape, copy=True):
shape[is_neg] = self.size // np.prod(shape[~is_neg])

if shape.prod() != self.size:
try:
shape = tuple(shape)
except TypeError:
shape = ()
raise ValueError(
f"cannot reshape {type(self).__name__} of size"
f" {self.size} into shape {tuple(shape)}"
f" {self.size} into shape {shape}"
)
return shape

Expand Down Expand Up @@ -181,7 +185,7 @@ def ndim(self, value):
@property
@abc.abstractmethod
def shape(self):
"""Tuple of sparse array dimensions."""
"""Tuple of array dimensions."""
raise NotImplementedError

@shape.setter
Expand All @@ -191,16 +195,19 @@ def shape(self, value):

@property
def size(self):
"""Number of elements in the sparse array including "non-zero" elements."""
return np.prod(self.shape, dtype=int)
"""Number of elements in the array including "non-zero" elements."""
try:
return np.prod(self.shape, dtype=int, initial=None)
except ValueError:
return 0

@size.setter
def size(self, value):
self._setter_not_writeable('size')

@property
def T(self):
"""The transposed sparse array."""
"""The transposed array."""
return self.transpose()

@T.setter
Expand Down
2 changes: 1 addition & 1 deletion ndcontainers/abc/tests/test_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,4 @@ def test_array_subclass(cls, bool):
]
)
def test_sparse_subclass(cls, bool):
assert issubclass(cls, abc.SparseArray) == bool
assert issubclass(cls, abc.SparseArray) == bool
25 changes: 14 additions & 11 deletions ndcontainers/sparse/coordinate.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,13 @@ def __init__(self, data, idxs, shape=None, fill_value=0, dtype=None, copy=False,
if self.data.shape[-1] != self.idxs.shape[-1]:
raise ValueError("'data' does not have 1-1 correspondence with 'idxs'")

if sum_duplicates:
self.sum_duplicates()

# currently this does not allow for 0d/null shape --> sets shape to (1,)
# the question is should a null CoordinateArray be allowed?
min_shape = self.idxs.max(axis=1, initial=0) + 1

try:
min_shape = self.idxs.max(axis=1) + 1
except ValueError:
min_shape = ()

if shape is None:
self._shape = tuple(min_shape)
else:
Expand All @@ -40,12 +41,15 @@ def __init__(self, data, idxs, shape=None, fill_value=0, dtype=None, copy=False,
raise ValueError(f"shape length does not match idxs length")
elif np.any(shape < min_shape):
raise ValueError(f"shape {tuple(shape)} values are too small for idxs")

self._shape = tuple(shape)
else:
self._shape = tuple(shape)

#TODO: dynamic fill_value default based on dtype
self.fill_value = fill_value

if sum_duplicates:
self.sum_duplicates()

def __repr__(self):
data = np.array2string(
self.data,
Expand Down Expand Up @@ -140,21 +144,20 @@ def astype(self, dtype, casting='unsafe', copy=True):
return self

def reshape(self, *shape, copy=True):
shape = super().reshape(shape)
shape = super().reshape(*shape)
raveled = np.ravel_multi_index(self.idxs, self.shape)
unraveled = np.unravel_index(raveled, shape)
idxs = np.array(unraveled, dtype=np.uint)
return type(self)(
self.data, idxs, shape, self.fill_value, self.dtype, copy=copy
)

def setflags(self, write=None, align=None, uic=None):
...

def sum_duplicates(self):
"""Eliminate duplicate entries by adding them together.
This is an in-place operation.
"""
if not self.size:
return
#FIXME: if idxs.size == 0 -> np.unique fails
self._idxs, inverse = np.unique(self.idxs, axis=1, return_inverse=True)
data = np.zeros_like(self.idxs[0], dtype=self.dtype)
Expand Down
55 changes: 34 additions & 21 deletions ndcontainers/sparse/tests/test_coordinate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,16 @@
from ndcontainers.sparse import CoordinateArray


#TODO: make these fixtures
@pytest.fixture
def coo_reshape():
data = [1, 2, 3]
idxs = [0, 10, 20]
shape = 24
coo = CoordinateArray(data, idxs, shape)
return coo


# TODO: make these fixtures?
# identity matrix
coo_ident_3x3 = CoordinateArray(
data=[1, 1, 1],
Expand All @@ -31,6 +40,15 @@
arr_prime_10 = np.array([0, 0, 2, 3, 0, 5, 0, 7, 0, 0])


# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# TESTS

def test_null_coo():
coo = CoordinateArray([], [])
assert coo.size == 0
assert coo.shape == ()


@pytest.mark.parametrize(
'coo, arr',
[(coo_ident_3x3, arr_ident_3x3), (coo_prime_10, arr_prime_10)],
Expand All @@ -48,25 +66,17 @@ def test_array_conversion(coo, arr):
testing.assert_array_equal(dense, arr)


@pytest.fixture
def coo_reshape():
data = [1, 2, 3]
idxs = [0, 10, 20]
shape = 24
coo = CoordinateArray(data, idxs, shape)
return coo


@pytest.mark.parametrize(
'newshape',
[(24, 1), (1, 24, 1), (2, 2, 2, 3), (8, 3), (-1, 4), (12, -5), (2, -1, 3)],
[(24, 1), (1, 24, 1), (2, 2, 2, 3), (8, 3), (-1, 4), (12, -5), (2, -1, 3), (-1,)],
ids=repr,
)
def test_reshape(newshape, coo_reshape):
arr = np.array(coo_reshape)

#TODO - factor out as meta parametrize
for container in (tuple, list, np.array):
# for container in (tuple, list, np.array): # ndarray raises DeprecationWarning but passes
for container in (tuple, list):
newshape = container(newshape)

coo_unpacked = coo_reshape.reshape(*newshape)
Expand All @@ -82,22 +92,25 @@ def test_reshape(newshape, coo_reshape):


@pytest.mark.parametrize(
'shape',
[(-1,), (-1, -1, 24), (2, -1, -1), (-3, -2, -4)],
'newshape',
[(-1, -1, 24), (2, -1, -1), (-3, -2, -4)],
ids=repr,
)
def test_reshape_neg_error(shape, coo_reshape):
pass
def test_reshape_neg_error(newshape, coo_reshape):
match = "can only specify one unknown dimension"
with pytest.raises(ValueError, match=match):
coo_reshape.reshape(newshape)


@pytest.mark.skip()
@pytest.mark.parametrize(
'shape',
[(), (-1, -1, 24), (2, -1, -1), (-3, -2, -4)],
'newshape',
[(), (11,), (2, 24)],
ids=repr,
)
def test_reshape_size_error(shape):
pass
def test_reshape_size_error(newshape, coo_reshape):
match = r"cannot reshape \w+ of size \d+ into shape \((\d(, )?)*\)"
with pytest.raises(ValueError, match=match):
coo_reshape.reshape(newshape)


#TODO: to emulate array(0).reshape(())