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 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"]