Skip to content

Commit 45eff9e

Browse files
committed
[Fix]: use new ind/val serialization for SparseArray
1 parent 5b5223f commit 45eff9e

1 file changed

Lines changed: 183 additions & 131 deletions

File tree

spm/__wrapper__.py

Lines changed: 183 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ def _matlab_array_types():
196196
matlab.single: np.float32,
197197
matlab.logical: np.bool,
198198
matlab.uint64: np.uint64,
199-
matlab.uint32: np.uint32,
199+
matlab.uint32: np.uint64,
200200
matlab.uint16: np.uint16,
201201
matlab.uint8: np.uint8,
202202
matlab.int64: np.int64,
@@ -244,32 +244,28 @@ def from_any(cls, other):
244244
type__ = other["type__"]
245245

246246
if type__ == "structarray":
247-
# Matlab returns a list of dictionaries in data__
247+
# MPython returns a list of dictionaries in data__
248248
# and the array shape in size__.
249249
return Struct._from_runtime(other)
250250

251251
elif type__ == "cell":
252-
# Matlab returns a list of dictionaries in data__
252+
# MPython returns a list of dictionaries in data__
253253
# and the array shape in size__.
254254
return Cell._from_runtime(other)
255255

256256
elif type__ == "object":
257-
# Matlab returns the object's fields serialized
257+
# MPython returns the object's fields serialized
258258
# in a dictionary.
259259
return MatlabClass._from_runtime(other)
260260

261261
elif type__ == "sparse":
262-
# Matlab returns a dense version of the array in data__.
263-
if sparse:
264-
return SparseArray._from_runtime(other)
265-
else:
266-
data = np.asarray(other['data__'], dtype=np.double)
267-
return data.view(Array)
262+
# MPython returns the coordinates and values in a dict.
263+
return SparseArray._from_runtime(other)
268264

269265
elif type__ == "char":
270266
# Character array that is not a row vector
271267
# (row vector are converted to str automatically)
272-
# Matlab returns all rows in a (F-ordered) cell in data__
268+
# MPython returns all rows in a (F-ordered) cell in data__
273269
# Let's use the cell constructor to return a cellstr.
274270
# -> A cellstr is a column vector, not a row vector
275271
size = np.asarray(other["size__"]).tolist()[0]
@@ -349,7 +345,7 @@ def _to_runtime(cls, obj):
349345
return obj
350346

351347
elif sparse and isinstance(obj, sparse.coo_array):
352-
return dict(type__='sparse', data__=obj.todense())
348+
return SparseArray.from_any(obj)._as_runtime()
353349

354350
else:
355351
# TODO: do we want to raise if the type is not supported by matlab?
@@ -1069,123 +1065,6 @@ def _return_delayed(self, index):
10691065
return super().__getitem__(index)
10701066

10711067

1072-
# ----------------------------------------------------------------------
1073-
# SparseArray
1074-
# ----------------------------------------------------------------------
1075-
1076-
1077-
if sparse:
1078-
1079-
class WrappedSparseArray(sparse.sparray, AnyWrappedArray):
1080-
"""Base class for sparse arrays."""
1081-
1082-
def to_dense(self) -> "Array":
1083-
return Array.from_any(super().to_dense())
1084-
1085-
class SparseArray(sparse.coo_array, WrappedSparseArray):
1086-
"""
1087-
Matlab sparse arrays.
1088-
1089-
```python
1090-
# Instantiate from size
1091-
SparseArray(N, M, ...)
1092-
SparseArray([N, M, ...])
1093-
SparseArray.from_shape([N, M, ...])
1094-
1095-
# Instantiate from existing sparse or dense array
1096-
SparseArray(other_array)
1097-
SparseArray.from_any(other_array)
1098-
1099-
# Other options
1100-
SparseArray(..., dtype=None, *, copy=None)
1101-
```
1102-
1103-
!!! warning
1104-
Lists or vectors of integers can be interpreted as shapes
1105-
or as dense arrays to copy. They are interpreted as shapes
1106-
by the `SparseArray` constructor. To ensure that they are
1107-
interpreted as dense arrays to copy, use `SparseArray.from_any`.
1108-
"""
1109-
1110-
def __init__(self, *args, **kwargs) -> "SparseArray":
1111-
mode, arg, kwargs = self._parse_args(*args, **kwargs)
1112-
if mode == "shape":
1113-
return super().__init__(shape=arg, **kwargs)
1114-
else:
1115-
if not isinstance(arg, (np.ndarray, sparse.sparray)):
1116-
arg = np.asanyarray(arg)
1117-
return super().__init__(arg, **kwargs)
1118-
1119-
def _as_runtime(self) -> dict:
1120-
data = super().todense()
1121-
return dict(type__='sparse', data__=data)
1122-
1123-
@classmethod
1124-
def _from_runtime(cls, dictobj: dict) -> "SparseArray":
1125-
if dictobj['type__'] != 'sparse':
1126-
raise ValueError("Not a matlab sparse matrix")
1127-
return cls.from_any(dictobj['data__'])
1128-
1129-
@classmethod
1130-
def from_shape(cls, shape=tuple(), **kwargs) -> "Array":
1131-
"""
1132-
Build an array of a given shape.
1133-
1134-
Parameters
1135-
----------
1136-
shape : list[int]
1137-
Shape of the new array.
1138-
1139-
Other Parameters
1140-
----------------
1141-
dtype : np.dtype | None, default='double'
1142-
Target data type.
1143-
1144-
Returns
1145-
-------
1146-
array : SparseArray
1147-
New array.
1148-
"""
1149-
return cls(list(shape), **kwargs)
1150-
1151-
@classmethod
1152-
def from_any(cls, other, **kwargs) -> "SparseArray":
1153-
"""
1154-
Convert an array-like object to a numeric array.
1155-
1156-
Parameters
1157-
----------
1158-
other : ArrayLike
1159-
object to convert.
1160-
1161-
Other Parameters
1162-
----------------
1163-
dtype : np.dtype | None, default=None
1164-
Target data type. Guessed if `None`.
1165-
copy : bool | None, default=None
1166-
Whether to copy the underlying data.
1167-
* `True` : the object is copied;
1168-
* `None` : the the object is copied only if needed;
1169-
* `False`: raises a `ValueError` if a copy cannot be avoided.
1170-
1171-
Returns
1172-
-------
1173-
array : SparseArray
1174-
Converted array.
1175-
"""
1176-
copy = kwargs.pop("copy", None)
1177-
inp = other
1178-
if not isinstance(other, sparse.sparray):
1179-
other = np.asanyarray(other, **kwargs)
1180-
other = cls(other, **kwargs)
1181-
other = _spcopy_if_needed(other, inp, copy)
1182-
return other
1183-
1184-
1185-
else:
1186-
WrappedSparseArray = SparseArray = None
1187-
1188-
11891068
# ----------------------------------------------------------------------
11901069
# Array
11911070
# ----------------------------------------------------------------------
@@ -1421,6 +1300,179 @@ def __repr__(self):
14211300
return super().__repr__()
14221301

14231302

1303+
# ----------------------------------------------------------------------
1304+
# SparseArray
1305+
# ----------------------------------------------------------------------
1306+
1307+
1308+
class _SparseMixin:
1309+
"""Methods common to the scipy.sparse and dense backends."""
1310+
1311+
def _as_runtime(self) -> dict:
1312+
indices = self.nonzero()
1313+
values = self[indices].reshape([-1, 1])
1314+
indices = np.stack(indices, -1)
1315+
indices += 1
1316+
size = np.array([[*np.shape(self)]])
1317+
return dict(
1318+
type__='sparse',
1319+
size__=size,
1320+
indices__=indices,
1321+
values__=values,
1322+
)
1323+
1324+
@classmethod
1325+
def _from_runtime(cls, dictobj: dict) -> "SparseArray":
1326+
if dictobj['type__'] != 'sparse':
1327+
raise ValueError("Not a matlab sparse matrix")
1328+
size = np.array(dictobj['size__'], dtype=np.uint64).ravel()
1329+
size = size.tolist()
1330+
dtype = _matlab_array_types()[type(dictobj['values__'])]
1331+
obj = cls.from_shape(size, dtype=dtype)
1332+
indices = np.asarray(dictobj['indices__'], dtype=np.long) - 1
1333+
values = np.asarray(dictobj['values__'], dtype=dtype).ravel()
1334+
obj[tuple(indices.T)] = values
1335+
return obj
1336+
1337+
1338+
if sparse:
1339+
1340+
class WrappedSparseArray(sparse.sparray, AnyWrappedArray):
1341+
"""Base class for sparse arrays."""
1342+
1343+
def to_dense(self) -> "Array":
1344+
return Array.from_any(super().to_dense())
1345+
1346+
class SparseArray(sparse.coo_array, _SparseMixin, WrappedSparseArray):
1347+
"""
1348+
Matlab sparse arrays (scipy.sparse backend).
1349+
1350+
```python
1351+
# Instantiate from size
1352+
SparseArray(N, M, ...)
1353+
SparseArray([N, M, ...])
1354+
SparseArray.from_shape([N, M, ...])
1355+
1356+
# Instantiate from existing sparse or dense array
1357+
SparseArray(other_array)
1358+
SparseArray.from_any(other_array)
1359+
1360+
# Other options
1361+
SparseArray(..., dtype=None, *, copy=None)
1362+
```
1363+
1364+
!!! warning
1365+
Lists or vectors of integers can be interpreted as shapes
1366+
or as dense arrays to copy. They are interpreted as shapes
1367+
by the `SparseArray` constructor. To ensure that they are
1368+
interpreted as dense arrays to copy, usse `SparseArray.from_any`.
1369+
"""
1370+
1371+
def __init__(self, *args, **kwargs) -> None:
1372+
mode, arg, kwargs = self._parse_args(*args, **kwargs)
1373+
if mode == "shape":
1374+
return super().__init__(shape=arg, **kwargs)
1375+
else:
1376+
if not isinstance(arg, (np.ndarray, sparse.sparray)):
1377+
arg = np.asanyarray(arg)
1378+
return super().__init__(arg, **kwargs)
1379+
1380+
@classmethod
1381+
def from_shape(cls, shape=tuple(), **kwargs) -> "Array":
1382+
"""
1383+
Build an array of a given shape.
1384+
1385+
Parameters
1386+
----------
1387+
shape : list[int]
1388+
Shape of the new array.
1389+
1390+
Other Parameters
1391+
----------------
1392+
dtype : np.dtype | None, default='double'
1393+
Target data type.
1394+
1395+
Returns
1396+
-------
1397+
array : SparseArray
1398+
New array.
1399+
"""
1400+
return cls(list(shape), **kwargs)
1401+
1402+
@classmethod
1403+
def from_any(cls, other, **kwargs) -> "SparseArray":
1404+
"""
1405+
Convert an array-like object to a numeric array.
1406+
1407+
Parameters
1408+
----------
1409+
other : ArrayLike
1410+
object to convert.
1411+
1412+
Other Parameters
1413+
----------------
1414+
dtype : np.dtype | None, default=None
1415+
Target data type. Guessed if `None`.
1416+
copy : bool | None, default=None
1417+
Whether to copy the underlying data.
1418+
* `True` : the object is copied;
1419+
* `None` : the the object is copied only if needed;
1420+
* `False`: raises a `ValueError` if a copy cannot be avoided.
1421+
1422+
Returns
1423+
-------
1424+
array : SparseArray
1425+
Converted array.
1426+
"""
1427+
copy = kwargs.pop("copy", None)
1428+
inp = other
1429+
if not isinstance(other, sparse.sparray):
1430+
other = np.asanyarray(other, **kwargs)
1431+
other = cls(other, **kwargs)
1432+
other = _spcopy_if_needed(other, inp, copy)
1433+
return other
1434+
1435+
else:
1436+
warnings.warn(
1437+
"Since scipy.sparse is not available, sparse matrices "
1438+
"will be implemented as dense matrices, which can lead to "
1439+
"unsubstainable memory usage. If this is an issue, install "
1440+
"scipy in your python environment."
1441+
)
1442+
1443+
class SparseArray(_SparseMixin, Array):
1444+
"""
1445+
Matlab sparse arrays (dense backend).
1446+
1447+
```python
1448+
# Instantiate from size
1449+
SparseArray(N, M, ...)
1450+
SparseArray([N, M, ...])
1451+
SparseArray.from_shape([N, M, ...])
1452+
1453+
# Instantiate from existing sparse or dense array
1454+
SparseArray(other_array)
1455+
SparseArray.from_any(other_array)
1456+
1457+
# Other options
1458+
SparseArray(..., dtype=None, *, copy=None)
1459+
```
1460+
1461+
!!! warning
1462+
Lists or vectors of integers can be interpreted as shapes
1463+
or as dense arrays to copy. They are interpreted as shapes
1464+
by the `SparseArray` constructor. To ensure that they are
1465+
interpreted as dense arrays to copy, usse `SparseArray.from_any`.
1466+
1467+
!!! note
1468+
This is not really a sparse array, but a dense array that gets
1469+
converted to a sparse array when passed to matlab.
1470+
"""
1471+
1472+
def to_dense(self) -> "Array":
1473+
return np.ndarray.view(self, Array)
1474+
1475+
14241476
# ----------------------------------------------------------------------
14251477
# Cell
14261478
# ----------------------------------------------------------------------
@@ -1749,7 +1801,7 @@ def _as_runtime(self) -> dict:
17491801
def _from_runtime(cls, objdict: dict) -> "Cell":
17501802
if objdict['type__'] != 'cell':
17511803
raise TypeError('objdict is not a cell')
1752-
size = np.array(objdict['size__'], dtype=np.uint32).ravel()
1804+
size = np.array(objdict['size__'], dtype=np.uint64).ravel()
17531805
data = np.fromiter(objdict['data__'], dtype=object)
17541806
data = data.reshape(size[::-1]).transpose()
17551807
try:
@@ -2376,7 +2428,7 @@ def _as_runtime(self) -> dict:
23762428
def _from_runtime(cls, objdict: dict) -> "Struct":
23772429
if objdict['type__'] != 'structarray':
23782430
raise TypeError('objdict is not a structarray')
2379-
size = np.array(objdict['size__'], dtype=np.uint32).ravel()
2431+
size = np.array(objdict['size__'], dtype=np.uint64).ravel()
23802432
data = np.array(objdict['data__'], dtype=object)
23812433
data = data.reshape(size)
23822434
try:

0 commit comments

Comments
 (0)