binsparse 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- binsparse/__init__.py +66 -0
- binsparse/container.py +328 -0
- binsparse/conversions/__init__.py +17 -0
- binsparse/conversions/numpy.py +64 -0
- binsparse/conversions/scipy.py +98 -0
- binsparse/conversions/sparse.py +92 -0
- binsparse/conversions/torch.py +179 -0
- binsparse/errors.py +5 -0
- binsparse/io.py +147 -0
- binsparse/tensor.py +577 -0
- binsparse/version.py +29 -0
- binsparse-0.1.0.dist-info/METADATA +18 -0
- binsparse-0.1.0.dist-info/RECORD +18 -0
- binsparse-0.1.0.dist-info/WHEEL +4 -0
- reference_cli/__init__.py +1 -0
- reference_cli/converters.py +178 -0
- reference_cli/framework.py +81 -0
- reference_cli/reformat.py +290 -0
binsparse/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Python reference implementation of the Binsparse specification."""
|
|
2
|
+
|
|
3
|
+
from .container import (
|
|
4
|
+
BINSPARSE_HEADER,
|
|
5
|
+
BinsparseContainer,
|
|
6
|
+
HDF5BinsparseContainer,
|
|
7
|
+
InMemoryBinsparseContainer,
|
|
8
|
+
NPZBinsparseContainer,
|
|
9
|
+
ZarrBinsparseContainer,
|
|
10
|
+
)
|
|
11
|
+
from .errors import BinsparseParseError
|
|
12
|
+
from .io import load_binsparse, save_binsparse
|
|
13
|
+
from .tensor import (
|
|
14
|
+
BinsparseLevel,
|
|
15
|
+
BinsparseTensor,
|
|
16
|
+
COOCMatrix,
|
|
17
|
+
COOMatrix,
|
|
18
|
+
COORMatrix,
|
|
19
|
+
CSCMatrix,
|
|
20
|
+
CSRMatrix,
|
|
21
|
+
CustomTensor,
|
|
22
|
+
DCSCMatrix,
|
|
23
|
+
DCSRMatrix,
|
|
24
|
+
DMATCMatrix,
|
|
25
|
+
DMATMatrix,
|
|
26
|
+
DMATRMatrix,
|
|
27
|
+
DVECVector,
|
|
28
|
+
CVECVector,
|
|
29
|
+
DenseLevel,
|
|
30
|
+
ElementLevel,
|
|
31
|
+
IndexableLevel,
|
|
32
|
+
SparseLevel,
|
|
33
|
+
)
|
|
34
|
+
from .version import BINSPARSE_VERSION
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"BINSPARSE_HEADER",
|
|
38
|
+
"BINSPARSE_VERSION",
|
|
39
|
+
"BinsparseContainer",
|
|
40
|
+
"BinsparseLevel",
|
|
41
|
+
"BinsparseParseError",
|
|
42
|
+
"BinsparseTensor",
|
|
43
|
+
"COOCMatrix",
|
|
44
|
+
"COOMatrix",
|
|
45
|
+
"COORMatrix",
|
|
46
|
+
"CSCMatrix",
|
|
47
|
+
"CSRMatrix",
|
|
48
|
+
"CVECVector",
|
|
49
|
+
"CustomTensor",
|
|
50
|
+
"DCSCMatrix",
|
|
51
|
+
"DCSRMatrix",
|
|
52
|
+
"DMATCMatrix",
|
|
53
|
+
"DMATMatrix",
|
|
54
|
+
"DMATRMatrix",
|
|
55
|
+
"DVECVector",
|
|
56
|
+
"DenseLevel",
|
|
57
|
+
"ElementLevel",
|
|
58
|
+
"HDF5BinsparseContainer",
|
|
59
|
+
"InMemoryBinsparseContainer",
|
|
60
|
+
"IndexableLevel",
|
|
61
|
+
"load_binsparse",
|
|
62
|
+
"NPZBinsparseContainer",
|
|
63
|
+
"SparseLevel",
|
|
64
|
+
"save_binsparse",
|
|
65
|
+
"ZarrBinsparseContainer",
|
|
66
|
+
]
|
binsparse/container.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
"""Container adapters for reading and writing Binsparse data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from collections.abc import MutableMapping
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from .errors import BinsparseParseError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
BINSPARSE_HEADER = "binsparse"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _wrap_header(value: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
20
|
+
return {BINSPARSE_HEADER: value}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _unwrap_header(value: Any) -> dict[str, Any]:
|
|
24
|
+
if not isinstance(value, dict):
|
|
25
|
+
raise BinsparseParseError("Binsparse metadata must be a JSON object")
|
|
26
|
+
try:
|
|
27
|
+
header = value[BINSPARSE_HEADER]
|
|
28
|
+
except KeyError as error:
|
|
29
|
+
raise BinsparseParseError("missing 'binsparse' JSON namespace") from error
|
|
30
|
+
if not isinstance(header, dict):
|
|
31
|
+
raise BinsparseParseError("the 'binsparse' JSON namespace must be an object")
|
|
32
|
+
return header
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
dtype_to_str = {
|
|
36
|
+
np.dtype("int8"): "int8",
|
|
37
|
+
np.dtype("int16"): "int16",
|
|
38
|
+
np.dtype("int32"): "int32",
|
|
39
|
+
np.dtype("int64"): "int64",
|
|
40
|
+
np.dtype("uint8"): "uint8",
|
|
41
|
+
np.dtype("uint16"): "uint16",
|
|
42
|
+
np.dtype("uint32"): "uint32",
|
|
43
|
+
np.dtype("uint64"): "uint64",
|
|
44
|
+
np.dtype("float32"): "float32",
|
|
45
|
+
np.dtype("float64"): "float64",
|
|
46
|
+
np.dtype("bool"): "bint8",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
str_to_dtype = {value: key for key, value in dtype_to_str.items()}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BinsparseContainer(ABC):
|
|
53
|
+
"""Common interface to a Binsparse binary container or container group."""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self.data_types: dict[str, str] = {}
|
|
57
|
+
|
|
58
|
+
def read_header(self) -> dict[str, Any]:
|
|
59
|
+
"""Return the decoded Binsparse JSON descriptor."""
|
|
60
|
+
header = self._read_header()
|
|
61
|
+
if not isinstance(header.get("data_types"), dict):
|
|
62
|
+
raise BinsparseParseError("data_types must be an object")
|
|
63
|
+
return header
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def _read_header(self) -> dict[str, Any]:
|
|
67
|
+
"""Read and decode the Binsparse header from the backend."""
|
|
68
|
+
|
|
69
|
+
def write_header(self, value: dict[str, Any]) -> None:
|
|
70
|
+
"""Store a Binsparse JSON descriptor."""
|
|
71
|
+
if not isinstance(value, dict):
|
|
72
|
+
raise TypeError("the binsparse header must be a dictionary")
|
|
73
|
+
header = {**value, "data_types": self.data_types.copy()}
|
|
74
|
+
json.dumps(header)
|
|
75
|
+
self._write_header(header)
|
|
76
|
+
|
|
77
|
+
@abstractmethod
|
|
78
|
+
def _write_header(self, value: dict[str, Any]) -> None:
|
|
79
|
+
"""Write a validated Binsparse header to the backend."""
|
|
80
|
+
|
|
81
|
+
def read_buffer(
|
|
82
|
+
self,
|
|
83
|
+
key: str,
|
|
84
|
+
expected_size: int | None = None,
|
|
85
|
+
*,
|
|
86
|
+
copy: bool | None = None,
|
|
87
|
+
) -> np.ndarray:
|
|
88
|
+
"""Read and decode a named binary array."""
|
|
89
|
+
header = self.read_header()
|
|
90
|
+
try:
|
|
91
|
+
declared = header["data_types"][key]
|
|
92
|
+
except KeyError as error:
|
|
93
|
+
raise BinsparseParseError(f"missing data type for buffer {key!r}") from error
|
|
94
|
+
if not isinstance(declared, str):
|
|
95
|
+
raise BinsparseParseError(f"data type for buffer {key!r} must be a string")
|
|
96
|
+
def decode(data_type: str, data: np.ndarray) -> np.ndarray:
|
|
97
|
+
if (match := re.fullmatch(r"iso\[(.*)\]", data_type)) is not None:
|
|
98
|
+
if expected_size is None:
|
|
99
|
+
raise BinsparseParseError(
|
|
100
|
+
"expected_size is required when reading an ISO buffer"
|
|
101
|
+
)
|
|
102
|
+
decoded = decode(match.group(1), data)
|
|
103
|
+
if decoded.size != 1:
|
|
104
|
+
raise BinsparseParseError("an ISO buffer must contain one value")
|
|
105
|
+
return np.broadcast_to(decoded.reshape(1), (expected_size,))
|
|
106
|
+
if (match := re.fullmatch(r"complex\[(.*)\]", data_type)) is not None:
|
|
107
|
+
decoded = decode(match.group(1), data)
|
|
108
|
+
if decoded.dtype not in {np.dtype("float32"), np.dtype("float64")}:
|
|
109
|
+
raise BinsparseParseError("complex values require float32 or float64")
|
|
110
|
+
if decoded.ndim != 1 or decoded.size % 2 != 0:
|
|
111
|
+
raise BinsparseParseError("invalid complex value buffer")
|
|
112
|
+
complex_dtype = (
|
|
113
|
+
np.complex64 if decoded.dtype == np.float32 else np.complex128
|
|
114
|
+
)
|
|
115
|
+
return np.ascontiguousarray(decoded).view(complex_dtype)
|
|
116
|
+
if re.fullmatch(r"[^\[\]]+", data_type) is None:
|
|
117
|
+
raise BinsparseParseError(f"unknown Binsparse type wrapper {data_type!r}")
|
|
118
|
+
try:
|
|
119
|
+
dtype = str_to_dtype[data_type]
|
|
120
|
+
except KeyError as error:
|
|
121
|
+
raise BinsparseParseError(f"unknown Binsparse type {data_type!r}") from error
|
|
122
|
+
return np.asarray(data, dtype=dtype) # type: ignore[call-overload]
|
|
123
|
+
encoded = self._read_buffer(key)
|
|
124
|
+
data = decode(declared, encoded)
|
|
125
|
+
if expected_size is not None and data.size != expected_size:
|
|
126
|
+
raise BinsparseParseError(
|
|
127
|
+
f"buffer {key!r} has size {data.size}, expected {expected_size}"
|
|
128
|
+
)
|
|
129
|
+
shares_memory = np.shares_memory(data, encoded)
|
|
130
|
+
if copy is False and data.size > 0 and not shares_memory:
|
|
131
|
+
raise ValueError(f"copy=False cannot decode buffer {key!r} without copying")
|
|
132
|
+
if copy is True and shares_memory:
|
|
133
|
+
return data.copy()
|
|
134
|
+
return data
|
|
135
|
+
|
|
136
|
+
@abstractmethod
|
|
137
|
+
def _read_buffer(self, key: str) -> np.ndarray:
|
|
138
|
+
"""Read a named binary array without applying Binsparse decoding."""
|
|
139
|
+
|
|
140
|
+
def write_buffer(
|
|
141
|
+
self,
|
|
142
|
+
key: str,
|
|
143
|
+
value: np.ndarray,
|
|
144
|
+
*,
|
|
145
|
+
copy: bool | None = None,
|
|
146
|
+
) -> None:
|
|
147
|
+
"""Encode, create or replace a named array and record its data type."""
|
|
148
|
+
def encode(
|
|
149
|
+
data: np.ndarray,
|
|
150
|
+
copy: bool | None,
|
|
151
|
+
*,
|
|
152
|
+
detect_iso: bool = True,
|
|
153
|
+
) -> tuple[np.ndarray, str]:
|
|
154
|
+
if (
|
|
155
|
+
detect_iso
|
|
156
|
+
and data.size > 0
|
|
157
|
+
and data.ndim == 1
|
|
158
|
+
and data.strides == (0,)
|
|
159
|
+
):
|
|
160
|
+
encoded, data_type = encode(data[:1], copy, detect_iso=False)
|
|
161
|
+
return encoded, f"iso[{data_type}]"
|
|
162
|
+
if data.dtype == np.bool_:
|
|
163
|
+
encoded = data.view(np.uint8)
|
|
164
|
+
return encoded.copy() if copy is True else encoded, "bint8"
|
|
165
|
+
if np.issubdtype(data.dtype, np.complexfloating):
|
|
166
|
+
if data.dtype not in {np.dtype("complex64"), np.dtype("complex128")}:
|
|
167
|
+
raise TypeError(f"unsupported complex dtype: {data.dtype}")
|
|
168
|
+
if not data.flags.c_contiguous:
|
|
169
|
+
if copy is False:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"copy=False cannot encode buffer {key!r} without copying"
|
|
172
|
+
)
|
|
173
|
+
data = np.ascontiguousarray(data)
|
|
174
|
+
dtype = np.dtype("float32" if data.dtype == np.complex64 else "float64")
|
|
175
|
+
encoded, data_type = encode(data.view(dtype), copy, detect_iso=False)
|
|
176
|
+
return encoded, f"complex[{data_type}]"
|
|
177
|
+
try:
|
|
178
|
+
data_type = dtype_to_str[data.dtype]
|
|
179
|
+
except KeyError as error:
|
|
180
|
+
raise TypeError(f"unsupported Binsparse dtype: {data.dtype}") from error
|
|
181
|
+
return data.copy() if copy is True else data, data_type
|
|
182
|
+
|
|
183
|
+
if not isinstance(value, np.ndarray):
|
|
184
|
+
raise TypeError("buffer value must be a NumPy ndarray")
|
|
185
|
+
encoded, data_type = encode(value, copy)
|
|
186
|
+
self._write_buffer(key, encoded)
|
|
187
|
+
self.data_types[key] = data_type
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
def _write_buffer(self, key: str, value: np.ndarray) -> None:
|
|
191
|
+
"""Write an already encoded named binary array."""
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class InMemoryBinsparseContainer(BinsparseContainer):
|
|
195
|
+
"""Store a Binsparse descriptor and its arrays directly in memory."""
|
|
196
|
+
|
|
197
|
+
def __init__(
|
|
198
|
+
self,
|
|
199
|
+
header: dict[str, Any] | None = None,
|
|
200
|
+
buffers: list[np.ndarray] | None = None,
|
|
201
|
+
) -> None:
|
|
202
|
+
super().__init__()
|
|
203
|
+
self.header = {} if header is None else header
|
|
204
|
+
self.buffers = [] if buffers is None else buffers
|
|
205
|
+
data_types = self.header.get("data_types")
|
|
206
|
+
if isinstance(data_types, dict):
|
|
207
|
+
self.data_types.update(data_types)
|
|
208
|
+
|
|
209
|
+
def _read_header(self) -> dict[str, Any]:
|
|
210
|
+
return self.header
|
|
211
|
+
|
|
212
|
+
def _write_header(self, value: dict[str, Any]) -> None:
|
|
213
|
+
self.header.clear()
|
|
214
|
+
self.header.update(value)
|
|
215
|
+
|
|
216
|
+
def _read_buffer(self, key: str) -> np.ndarray:
|
|
217
|
+
try:
|
|
218
|
+
index = list(self.header["data_types"]).index(key)
|
|
219
|
+
except (KeyError, ValueError) as error:
|
|
220
|
+
raise KeyError(f"in-memory container has no buffer {key!r}") from error
|
|
221
|
+
try:
|
|
222
|
+
return self.buffers[index]
|
|
223
|
+
except IndexError as error:
|
|
224
|
+
raise KeyError(f"in-memory container has no buffer {key!r}") from error
|
|
225
|
+
|
|
226
|
+
def _write_buffer(self, key: str, value: np.ndarray) -> None:
|
|
227
|
+
if key in self.data_types:
|
|
228
|
+
self.buffers[list(self.data_types).index(key)] = value
|
|
229
|
+
else:
|
|
230
|
+
self.buffers.append(value)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
class HDF5BinsparseContainer(BinsparseContainer):
|
|
234
|
+
"""Adapt an h5py ``Container`` or ``Group``."""
|
|
235
|
+
|
|
236
|
+
def __init__(self, group: Any):
|
|
237
|
+
super().__init__()
|
|
238
|
+
self.group = group
|
|
239
|
+
|
|
240
|
+
def _read_header(self) -> dict[str, Any]:
|
|
241
|
+
try:
|
|
242
|
+
value = self.group.attrs[BINSPARSE_HEADER]
|
|
243
|
+
except KeyError as error:
|
|
244
|
+
raise KeyError("HDF5 group has no 'binsparse' attribute") from error
|
|
245
|
+
return _unwrap_header(json.loads(value))
|
|
246
|
+
|
|
247
|
+
def _write_header(self, value: dict[str, Any]) -> None:
|
|
248
|
+
self.group.attrs[BINSPARSE_HEADER] = json.dumps(
|
|
249
|
+
_wrap_header(value), indent=2, sort_keys=True, separators=(",", ": ")
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def _read_buffer(self, key: str) -> np.ndarray:
|
|
253
|
+
return np.asarray(self.group[key][()])
|
|
254
|
+
|
|
255
|
+
def _write_buffer(self, key: str, value: np.ndarray) -> None:
|
|
256
|
+
if key in self.group:
|
|
257
|
+
del self.group[key]
|
|
258
|
+
self.group.create_dataset(key, data=value)
|
|
259
|
+
|
|
260
|
+
class ZarrBinsparseContainer(BinsparseContainer):
|
|
261
|
+
"""Adapt a Zarr group without requiring Zarr as a dependency."""
|
|
262
|
+
|
|
263
|
+
def __init__(self, group: Any):
|
|
264
|
+
super().__init__()
|
|
265
|
+
self.group = group
|
|
266
|
+
|
|
267
|
+
def _read_header(self) -> dict[str, Any]:
|
|
268
|
+
try:
|
|
269
|
+
return _unwrap_header(self.group.attrs[BINSPARSE_HEADER])
|
|
270
|
+
except KeyError as error:
|
|
271
|
+
raise KeyError("Zarr group has no 'binsparse' attribute") from error
|
|
272
|
+
|
|
273
|
+
def _write_header(self, value: dict[str, Any]) -> None:
|
|
274
|
+
self.group.attrs[BINSPARSE_HEADER] = _wrap_header(value)
|
|
275
|
+
|
|
276
|
+
def _read_buffer(self, key: str) -> np.ndarray:
|
|
277
|
+
return np.asarray(self.group[key][...])
|
|
278
|
+
|
|
279
|
+
def _write_buffer(self, key: str, value: np.ndarray) -> None:
|
|
280
|
+
if key in self.group:
|
|
281
|
+
del self.group[key]
|
|
282
|
+
if hasattr(self.group, "create_array"):
|
|
283
|
+
self.group.create_array(key, data=value)
|
|
284
|
+
else:
|
|
285
|
+
self.group.create_dataset(key, data=value)
|
|
286
|
+
|
|
287
|
+
class NPZBinsparseContainer(BinsparseContainer):
|
|
288
|
+
"""Adapt a mutable mapping of NPZ entry names to NumPy arrays."""
|
|
289
|
+
|
|
290
|
+
def __init__(self, file: MutableMapping[str, np.ndarray]):
|
|
291
|
+
super().__init__()
|
|
292
|
+
self.file = file
|
|
293
|
+
|
|
294
|
+
def _read_header(self) -> dict[str, Any]:
|
|
295
|
+
try:
|
|
296
|
+
value = self.file[BINSPARSE_HEADER]
|
|
297
|
+
except KeyError as error:
|
|
298
|
+
raise KeyError("NPZ archive has no 'binsparse' entry") from error
|
|
299
|
+
return _unwrap_header(json.loads(str(value.item())))
|
|
300
|
+
|
|
301
|
+
def _write_header(self, value: dict[str, Any]) -> None:
|
|
302
|
+
self.file[BINSPARSE_HEADER] = np.asarray(
|
|
303
|
+
json.dumps(
|
|
304
|
+
_wrap_header(value),
|
|
305
|
+
indent=2,
|
|
306
|
+
sort_keys=True,
|
|
307
|
+
separators=(",", ": "),
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def _read_buffer(self, key: str) -> np.ndarray:
|
|
312
|
+
if key == BINSPARSE_HEADER:
|
|
313
|
+
raise KeyError("use read_header() to access binsparse metadata")
|
|
314
|
+
return self.file[key]
|
|
315
|
+
|
|
316
|
+
def _write_buffer(self, key: str, value: np.ndarray) -> None:
|
|
317
|
+
if key == BINSPARSE_HEADER:
|
|
318
|
+
raise KeyError("use write_header() to set binsparse metadata")
|
|
319
|
+
self.file[key] = value
|
|
320
|
+
|
|
321
|
+
__all__ = [
|
|
322
|
+
"BINSPARSE_HEADER",
|
|
323
|
+
"BinsparseContainer",
|
|
324
|
+
"HDF5BinsparseContainer",
|
|
325
|
+
"InMemoryBinsparseContainer",
|
|
326
|
+
"ZarrBinsparseContainer",
|
|
327
|
+
"NPZBinsparseContainer",
|
|
328
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Optional adapters between Binsparse tensors and third-party array libraries."""
|
|
2
|
+
|
|
3
|
+
from .numpy import from_numpy, to_numpy
|
|
4
|
+
from .scipy import from_scipy, to_scipy
|
|
5
|
+
from .sparse import from_sparse, to_sparse
|
|
6
|
+
from .torch import from_torch, to_torch
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"from_numpy",
|
|
10
|
+
"from_scipy",
|
|
11
|
+
"from_sparse",
|
|
12
|
+
"from_torch",
|
|
13
|
+
"to_numpy",
|
|
14
|
+
"to_scipy",
|
|
15
|
+
"to_sparse",
|
|
16
|
+
"to_torch",
|
|
17
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Conversions between Binsparse tensors and NumPy arrays."""
|
|
2
|
+
|
|
3
|
+
from typing import Literal
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from binsparse.tensor import (
|
|
8
|
+
BinsparseTensor,
|
|
9
|
+
CustomTensor,
|
|
10
|
+
DenseLevel,
|
|
11
|
+
DMATCMatrix,
|
|
12
|
+
DMATRMatrix,
|
|
13
|
+
DVECVector,
|
|
14
|
+
ElementLevel,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def from_numpy(value: np.ndarray, *, copy: bool | None = None) -> BinsparseTensor:
|
|
19
|
+
"""Convert a NumPy array to a dense Binsparse tensor."""
|
|
20
|
+
if not isinstance(value, np.ndarray):
|
|
21
|
+
raise TypeError("expected a NumPy ndarray")
|
|
22
|
+
array = np.array(value, copy=copy, order="C")
|
|
23
|
+
values = array.reshape(-1)
|
|
24
|
+
common = (tuple(array.shape), int(array.size))
|
|
25
|
+
if array.ndim == 1:
|
|
26
|
+
return DVECVector(*common, values=values)
|
|
27
|
+
if array.ndim == 2:
|
|
28
|
+
return DMATRMatrix(*common, values=values)
|
|
29
|
+
level = ElementLevel(values)
|
|
30
|
+
return CustomTensor(
|
|
31
|
+
*common,
|
|
32
|
+
level=level if array.ndim == 0 else DenseLevel(array.ndim, level),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def to_numpy(tensor: BinsparseTensor, *, copy: bool | None = None) -> np.ndarray:
|
|
37
|
+
"""Convert a dense Binsparse tensor to a NumPy array."""
|
|
38
|
+
order: Literal["C", "F"] = "C"
|
|
39
|
+
if isinstance(tensor, (DVECVector, DMATRMatrix)):
|
|
40
|
+
values = tensor.values
|
|
41
|
+
elif isinstance(tensor, DMATCMatrix):
|
|
42
|
+
values = tensor.values
|
|
43
|
+
order = "F"
|
|
44
|
+
elif (
|
|
45
|
+
isinstance(tensor, CustomTensor)
|
|
46
|
+
and tensor.transpose is None
|
|
47
|
+
and isinstance(tensor.level, ElementLevel)
|
|
48
|
+
and not tensor.shape
|
|
49
|
+
):
|
|
50
|
+
values = tensor.level.values
|
|
51
|
+
elif (
|
|
52
|
+
isinstance(tensor, CustomTensor)
|
|
53
|
+
and tensor.transpose is None
|
|
54
|
+
and isinstance(tensor.level, DenseLevel)
|
|
55
|
+
and tensor.level.rank == len(tensor.shape)
|
|
56
|
+
and isinstance(tensor.level.level, ElementLevel)
|
|
57
|
+
):
|
|
58
|
+
values = tensor.level.level.values
|
|
59
|
+
else:
|
|
60
|
+
raise TypeError(f"cannot convert {type(tensor).__name__} to NumPy")
|
|
61
|
+
return np.array(values, copy=copy).reshape(tensor.shape, order=order)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
__all__ = ["from_numpy", "to_numpy"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Conversions between Binsparse tensors and SciPy sparse arrays."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from binsparse.tensor import BinsparseTensor, COORMatrix, CSCMatrix, CSRMatrix
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _scipy_sparse() -> Any:
|
|
9
|
+
try:
|
|
10
|
+
import scipy.sparse as scipy_sparse
|
|
11
|
+
except ImportError as error:
|
|
12
|
+
raise ImportError(
|
|
13
|
+
"SciPy conversions require the 'scipy' extra: pip install binsparse[scipy]"
|
|
14
|
+
) from error
|
|
15
|
+
return scipy_sparse
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _prepare(value: Any, copy: bool | None) -> Any:
|
|
19
|
+
if value.has_canonical_format:
|
|
20
|
+
return value.copy() if copy is True else value
|
|
21
|
+
if copy is False:
|
|
22
|
+
raise ValueError("copy=False cannot canonicalize a SciPy sparse array")
|
|
23
|
+
result = value.copy()
|
|
24
|
+
result.sum_duplicates()
|
|
25
|
+
return result
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def from_scipy(value: Any, *, copy: bool | None = None) -> BinsparseTensor:
|
|
29
|
+
"""Convert a two-dimensional SciPy CSR, CSC, or COO object to Binsparse."""
|
|
30
|
+
scipy_sparse = _scipy_sparse()
|
|
31
|
+
if not scipy_sparse.issparse(value) or value.ndim != 2:
|
|
32
|
+
raise TypeError("expected a two-dimensional SciPy sparse array or matrix")
|
|
33
|
+
if value.format not in {"coo", "csr", "csc"}:
|
|
34
|
+
raise TypeError(f"unsupported SciPy sparse format {value.format!r}")
|
|
35
|
+
value = _prepare(value, copy)
|
|
36
|
+
|
|
37
|
+
shape = tuple(value.shape)
|
|
38
|
+
count = int(value.data.size)
|
|
39
|
+
values = value.data
|
|
40
|
+
if value.format == "csr":
|
|
41
|
+
return CSRMatrix(
|
|
42
|
+
shape,
|
|
43
|
+
count,
|
|
44
|
+
pointers_to_1=value.indptr,
|
|
45
|
+
indices_1=value.indices,
|
|
46
|
+
values=values,
|
|
47
|
+
)
|
|
48
|
+
if value.format == "csc":
|
|
49
|
+
return CSCMatrix(
|
|
50
|
+
shape,
|
|
51
|
+
count,
|
|
52
|
+
pointers_to_1=value.indptr,
|
|
53
|
+
indices_1=value.indices,
|
|
54
|
+
values=values,
|
|
55
|
+
)
|
|
56
|
+
if value.format == "coo":
|
|
57
|
+
return COORMatrix(
|
|
58
|
+
shape,
|
|
59
|
+
count,
|
|
60
|
+
indices_0=value.row,
|
|
61
|
+
indices_1=value.col,
|
|
62
|
+
values=values,
|
|
63
|
+
)
|
|
64
|
+
raise TypeError(f"unsupported SciPy sparse format {value.format!r}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def to_scipy(tensor: BinsparseTensor, *, copy: bool | None = None) -> Any:
|
|
68
|
+
"""Convert a Binsparse CSR, CSC, or COO matrix to a SciPy sparse array."""
|
|
69
|
+
scipy_sparse = _scipy_sparse()
|
|
70
|
+
if tensor.fill is True and tensor.fill_value != 0:
|
|
71
|
+
raise ValueError("SciPy conversion requires a zero fill value")
|
|
72
|
+
if isinstance(tensor, CSCMatrix):
|
|
73
|
+
result = scipy_sparse.csc_array(
|
|
74
|
+
(tensor.values, tensor.indices_1, tensor.pointers_to_1),
|
|
75
|
+
shape=tensor.shape,
|
|
76
|
+
copy=copy,
|
|
77
|
+
)
|
|
78
|
+
elif isinstance(tensor, CSRMatrix):
|
|
79
|
+
result = scipy_sparse.csr_array(
|
|
80
|
+
(tensor.values, tensor.indices_1, tensor.pointers_to_1),
|
|
81
|
+
shape=tensor.shape,
|
|
82
|
+
copy=copy,
|
|
83
|
+
)
|
|
84
|
+
elif isinstance(tensor, COORMatrix):
|
|
85
|
+
result = scipy_sparse.coo_array(
|
|
86
|
+
(tensor.values, (tensor.indices_0, tensor.indices_1)),
|
|
87
|
+
shape=tensor.shape,
|
|
88
|
+
copy=copy,
|
|
89
|
+
)
|
|
90
|
+
# Binsparse COOR requires row-major sorted, duplicate-free coordinates.
|
|
91
|
+
# SciPy's constructor cannot infer that invariant from buffer inputs.
|
|
92
|
+
result.has_canonical_format = True
|
|
93
|
+
else:
|
|
94
|
+
raise TypeError(f"cannot convert {type(tensor).__name__} to SciPy")
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
__all__ = ["from_scipy", "to_scipy"]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Conversions between Binsparse tensors and PyData/Sparse arrays."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from binsparse.tensor import (
|
|
8
|
+
BinsparseTensor,
|
|
9
|
+
COORMatrix,
|
|
10
|
+
CustomTensor,
|
|
11
|
+
ElementLevel,
|
|
12
|
+
SparseLevel,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _sparse() -> Any:
|
|
17
|
+
try:
|
|
18
|
+
import sparse
|
|
19
|
+
except ImportError as error:
|
|
20
|
+
raise ImportError(
|
|
21
|
+
"PyData/Sparse conversions require the 'sparse' extra: "
|
|
22
|
+
"pip install binsparse[sparse]"
|
|
23
|
+
) from error
|
|
24
|
+
return sparse
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def from_sparse(value: Any, *, copy: bool | None = None) -> BinsparseTensor:
|
|
28
|
+
"""Convert an N-dimensional PyData/Sparse COO array to Binsparse."""
|
|
29
|
+
sparse = _sparse()
|
|
30
|
+
if not isinstance(value, sparse.COO) or value.ndim < 1:
|
|
31
|
+
raise TypeError("expected a non-scalar PyData/Sparse COO array")
|
|
32
|
+
if copy is True:
|
|
33
|
+
value = value.copy(deep=True)
|
|
34
|
+
fill_value = np.asarray(value.fill_value).item()
|
|
35
|
+
shape = tuple(value.shape)
|
|
36
|
+
count = int(value.data.size)
|
|
37
|
+
indices = tuple(value.coords[dimension, :] for dimension in range(value.ndim))
|
|
38
|
+
values = value.data
|
|
39
|
+
if value.ndim == 2:
|
|
40
|
+
return COORMatrix(
|
|
41
|
+
shape,
|
|
42
|
+
count,
|
|
43
|
+
fill=True,
|
|
44
|
+
fill_value=fill_value,
|
|
45
|
+
indices_0=indices[0],
|
|
46
|
+
indices_1=indices[1],
|
|
47
|
+
values=values,
|
|
48
|
+
)
|
|
49
|
+
return CustomTensor(
|
|
50
|
+
shape,
|
|
51
|
+
count,
|
|
52
|
+
fill=True,
|
|
53
|
+
fill_value=fill_value,
|
|
54
|
+
level=SparseLevel(value.ndim, ElementLevel(values), indices),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def to_sparse(tensor: BinsparseTensor, *, copy: bool | None = None) -> Any:
|
|
59
|
+
"""Convert a flat Binsparse COO tensor to a PyData/Sparse COO array."""
|
|
60
|
+
sparse = _sparse()
|
|
61
|
+
indices: tuple[np.ndarray, ...]
|
|
62
|
+
if isinstance(tensor, COORMatrix):
|
|
63
|
+
indices = (tensor.indices_0, tensor.indices_1)
|
|
64
|
+
values = tensor.values
|
|
65
|
+
elif (
|
|
66
|
+
isinstance(tensor, CustomTensor)
|
|
67
|
+
and tensor.transpose is None
|
|
68
|
+
and isinstance(tensor.level, SparseLevel)
|
|
69
|
+
and tensor.level.rank == len(tensor.shape)
|
|
70
|
+
and tensor.level.pointers_to_next is None
|
|
71
|
+
and isinstance(tensor.level.level, ElementLevel)
|
|
72
|
+
):
|
|
73
|
+
indices = tensor.level.indices
|
|
74
|
+
values = tensor.level.level.values
|
|
75
|
+
else:
|
|
76
|
+
raise TypeError(f"cannot convert {type(tensor).__name__} to PyData/Sparse")
|
|
77
|
+
if copy is False:
|
|
78
|
+
raise ValueError(
|
|
79
|
+
"copy=False requires Binsparse to support storing COO indices in a "
|
|
80
|
+
"single coordinate matrix"
|
|
81
|
+
)
|
|
82
|
+
coords = np.stack(indices)
|
|
83
|
+
fill_value = tensor.fill_value if tensor.fill is True else 0
|
|
84
|
+
return sparse.COO(
|
|
85
|
+
coords,
|
|
86
|
+
np.array(values, copy=True) if copy is True else values,
|
|
87
|
+
shape=tensor.shape,
|
|
88
|
+
fill_value=fill_value,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
__all__ = ["from_sparse", "to_sparse"]
|