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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Conversions between Binsparse tensors and PyTorch tensors."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
from binsparse.conversions.numpy import from_numpy, to_numpy
|
|
8
|
+
from binsparse.tensor import (
|
|
9
|
+
BinsparseTensor,
|
|
10
|
+
COORMatrix,
|
|
11
|
+
CSCMatrix,
|
|
12
|
+
CSRMatrix,
|
|
13
|
+
CustomTensor,
|
|
14
|
+
DenseLevel,
|
|
15
|
+
DMATCMatrix,
|
|
16
|
+
DMATRMatrix,
|
|
17
|
+
DVECVector,
|
|
18
|
+
ElementLevel,
|
|
19
|
+
SparseLevel,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _torch() -> Any:
|
|
24
|
+
try:
|
|
25
|
+
import torch
|
|
26
|
+
except ImportError as error:
|
|
27
|
+
raise ImportError(
|
|
28
|
+
"PyTorch conversions require the 'torch' extra: "
|
|
29
|
+
"pip install binsparse[torch]"
|
|
30
|
+
) from error
|
|
31
|
+
return torch
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _numpy(value: Any, copy: bool | None) -> np.ndarray:
|
|
35
|
+
if copy is True:
|
|
36
|
+
try:
|
|
37
|
+
return value.numpy(force=False).copy()
|
|
38
|
+
except (RuntimeError, TypeError):
|
|
39
|
+
result = value.numpy(force=True)
|
|
40
|
+
shares_storage = (
|
|
41
|
+
value.device.type == "cpu"
|
|
42
|
+
and result.__array_interface__["data"][0] == value.data_ptr()
|
|
43
|
+
)
|
|
44
|
+
return result.copy() if shares_storage else result
|
|
45
|
+
if copy is False:
|
|
46
|
+
return value.numpy(force=False)
|
|
47
|
+
try:
|
|
48
|
+
return value.numpy(force=False)
|
|
49
|
+
except (RuntimeError, TypeError):
|
|
50
|
+
return value.numpy(force=True)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def from_torch(value: Any, *, copy: bool | None = None) -> BinsparseTensor:
|
|
54
|
+
"""Convert a PyTorch dense, COO, CSR, or CSC tensor to Binsparse."""
|
|
55
|
+
torch = _torch()
|
|
56
|
+
if not isinstance(value, torch.Tensor):
|
|
57
|
+
raise TypeError("expected a PyTorch tensor")
|
|
58
|
+
|
|
59
|
+
if value.layout == torch.strided:
|
|
60
|
+
array = _numpy(value, copy)
|
|
61
|
+
return from_numpy(array, copy=False if copy is True else copy)
|
|
62
|
+
|
|
63
|
+
if value.layout == torch.sparse_coo:
|
|
64
|
+
indices = _numpy(value._indices(), copy)
|
|
65
|
+
values = _numpy(value._values(), copy)
|
|
66
|
+
if value.dense_dim() != 0:
|
|
67
|
+
raise TypeError("hybrid sparse COO tensors are not supported")
|
|
68
|
+
if value.sparse_dim() == 2:
|
|
69
|
+
return COORMatrix(
|
|
70
|
+
tuple(value.shape),
|
|
71
|
+
int(values.size),
|
|
72
|
+
indices_0=indices[0, :],
|
|
73
|
+
indices_1=indices[1, :],
|
|
74
|
+
values=values,
|
|
75
|
+
)
|
|
76
|
+
return CustomTensor(
|
|
77
|
+
tuple(value.shape),
|
|
78
|
+
int(values.size),
|
|
79
|
+
level=SparseLevel(
|
|
80
|
+
value.sparse_dim(),
|
|
81
|
+
ElementLevel(values),
|
|
82
|
+
tuple(
|
|
83
|
+
indices[dimension, :]
|
|
84
|
+
for dimension in range(value.sparse_dim())
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if value.layout == torch.sparse_csr:
|
|
90
|
+
values = _numpy(value.values(), copy)
|
|
91
|
+
return CSRMatrix(
|
|
92
|
+
tuple(value.shape),
|
|
93
|
+
int(values.size),
|
|
94
|
+
pointers_to_1=_numpy(value.crow_indices(), copy),
|
|
95
|
+
indices_1=_numpy(value.col_indices(), copy),
|
|
96
|
+
values=values,
|
|
97
|
+
)
|
|
98
|
+
if value.layout == torch.sparse_csc:
|
|
99
|
+
values = _numpy(value.values(), copy)
|
|
100
|
+
return CSCMatrix(
|
|
101
|
+
tuple(value.shape),
|
|
102
|
+
int(values.size),
|
|
103
|
+
pointers_to_1=_numpy(value.ccol_indices(), copy),
|
|
104
|
+
indices_1=_numpy(value.row_indices(), copy),
|
|
105
|
+
values=values,
|
|
106
|
+
)
|
|
107
|
+
raise TypeError(f"unsupported PyTorch layout {value.layout}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _tensor(torch: Any, value: Any, device: Any, copy: bool | None) -> Any:
|
|
111
|
+
return torch.asarray(value, device=device, copy=copy)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def to_torch(
|
|
115
|
+
tensor: BinsparseTensor,
|
|
116
|
+
*,
|
|
117
|
+
device: Any = None,
|
|
118
|
+
copy: bool | None = None,
|
|
119
|
+
) -> Any:
|
|
120
|
+
"""Convert a supported Binsparse tensor to PyTorch, optionally on *device*."""
|
|
121
|
+
torch = _torch()
|
|
122
|
+
indices: tuple[np.ndarray, ...]
|
|
123
|
+
if (
|
|
124
|
+
isinstance(tensor, (DVECVector, DMATRMatrix, DMATCMatrix))
|
|
125
|
+
or isinstance(tensor, CustomTensor)
|
|
126
|
+
and tensor.transpose is None
|
|
127
|
+
and (
|
|
128
|
+
isinstance(tensor.level, ElementLevel)
|
|
129
|
+
or isinstance(tensor.level, DenseLevel)
|
|
130
|
+
and tensor.level.rank == len(tensor.shape)
|
|
131
|
+
and isinstance(tensor.level.level, ElementLevel)
|
|
132
|
+
)
|
|
133
|
+
):
|
|
134
|
+
return _tensor(torch, to_numpy(tensor, copy=False), device, copy)
|
|
135
|
+
|
|
136
|
+
if tensor.fill is True and tensor.fill_value != 0:
|
|
137
|
+
raise ValueError("PyTorch conversion requires a zero fill value")
|
|
138
|
+
if isinstance(tensor, CSCMatrix):
|
|
139
|
+
return torch.sparse_csc_tensor(
|
|
140
|
+
_tensor(torch, tensor.pointers_to_1, device, copy),
|
|
141
|
+
_tensor(torch, tensor.indices_1, device, copy),
|
|
142
|
+
_tensor(torch, tensor.values, device, copy),
|
|
143
|
+
size=tensor.shape,
|
|
144
|
+
device=device,
|
|
145
|
+
)
|
|
146
|
+
if isinstance(tensor, CSRMatrix):
|
|
147
|
+
return torch.sparse_csr_tensor(
|
|
148
|
+
_tensor(torch, tensor.pointers_to_1, device, copy),
|
|
149
|
+
_tensor(torch, tensor.indices_1, device, copy),
|
|
150
|
+
_tensor(torch, tensor.values, device, copy),
|
|
151
|
+
size=tensor.shape,
|
|
152
|
+
device=device,
|
|
153
|
+
)
|
|
154
|
+
if isinstance(tensor, COORMatrix):
|
|
155
|
+
indices = (tensor.indices_0, tensor.indices_1)
|
|
156
|
+
values = tensor.values
|
|
157
|
+
elif (
|
|
158
|
+
isinstance(tensor, CustomTensor)
|
|
159
|
+
and tensor.transpose is None
|
|
160
|
+
and isinstance(tensor.level, SparseLevel)
|
|
161
|
+
and tensor.level.rank == len(tensor.shape)
|
|
162
|
+
and tensor.level.pointers_to_next is None
|
|
163
|
+
and isinstance(tensor.level.level, ElementLevel)
|
|
164
|
+
):
|
|
165
|
+
indices = tensor.level.indices
|
|
166
|
+
values = tensor.level.level.values
|
|
167
|
+
else:
|
|
168
|
+
raise TypeError(f"cannot convert {type(tensor).__name__} to PyTorch")
|
|
169
|
+
if copy is False:
|
|
170
|
+
raise ValueError("copy=False cannot combine Binsparse COO coordinate arrays")
|
|
171
|
+
return torch.sparse_coo_tensor(
|
|
172
|
+
_tensor(torch, np.stack(indices), device, copy),
|
|
173
|
+
_tensor(torch, values, device, copy),
|
|
174
|
+
size=tensor.shape,
|
|
175
|
+
device=device,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
__all__ = ["from_torch", "to_torch"]
|
binsparse/errors.py
ADDED
binsparse/io.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Path-based Binsparse file I/O."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from os import PathLike
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Mapping
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from .container import (
|
|
12
|
+
HDF5BinsparseContainer,
|
|
13
|
+
NPZBinsparseContainer,
|
|
14
|
+
ZarrBinsparseContainer,
|
|
15
|
+
)
|
|
16
|
+
from .tensor import BinsparseTensor
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Pathish = str | PathLike[str]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _suffix(path: Pathish) -> str:
|
|
23
|
+
return Path(path).suffix.lower()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _unsupported(path: Pathish) -> ValueError:
|
|
27
|
+
return ValueError(f"unsupported Binsparse container extension {Path(path).suffix!r}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_binsparse(
|
|
31
|
+
path: Pathish,
|
|
32
|
+
*,
|
|
33
|
+
alias: bool | None = None,
|
|
34
|
+
copy: bool | None = None,
|
|
35
|
+
) -> BinsparseTensor:
|
|
36
|
+
"""Load a Binsparse tensor from *path*, dispatching by file extension."""
|
|
37
|
+
match _suffix(path):
|
|
38
|
+
case ".npz":
|
|
39
|
+
with np.load(path, allow_pickle=False) as archive:
|
|
40
|
+
return BinsparseTensor.parse(
|
|
41
|
+
NPZBinsparseContainer(archive),
|
|
42
|
+
alias=alias,
|
|
43
|
+
copy=copy,
|
|
44
|
+
)
|
|
45
|
+
case ".h5" | ".hdf5":
|
|
46
|
+
import h5py
|
|
47
|
+
|
|
48
|
+
with h5py.File(path, "r") as file:
|
|
49
|
+
return BinsparseTensor.parse(
|
|
50
|
+
HDF5BinsparseContainer(file),
|
|
51
|
+
alias=alias,
|
|
52
|
+
copy=copy,
|
|
53
|
+
)
|
|
54
|
+
case ".zarr":
|
|
55
|
+
zarr = _zarr()
|
|
56
|
+
group = zarr.open_group(path, mode="r")
|
|
57
|
+
return BinsparseTensor.parse(
|
|
58
|
+
ZarrBinsparseContainer(group),
|
|
59
|
+
alias=alias,
|
|
60
|
+
copy=copy,
|
|
61
|
+
)
|
|
62
|
+
case _:
|
|
63
|
+
raise _unsupported(path)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def save_binsparse(
|
|
67
|
+
tensor: BinsparseTensor,
|
|
68
|
+
path: Pathish,
|
|
69
|
+
*,
|
|
70
|
+
alias: bool | None = None,
|
|
71
|
+
copy: bool | None = None,
|
|
72
|
+
header: Mapping[str, Any] | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
"""Write *tensor* to *path*, dispatching by file extension."""
|
|
75
|
+
match _suffix(path):
|
|
76
|
+
case ".npz":
|
|
77
|
+
archive: dict[str, np.ndarray] = {}
|
|
78
|
+
npz_container = NPZBinsparseContainer(archive)
|
|
79
|
+
tensor.serialize(npz_container, alias=alias, copy=copy)
|
|
80
|
+
if header is not None:
|
|
81
|
+
_preserve_header(npz_container, header)
|
|
82
|
+
savez: Any = np.savez
|
|
83
|
+
savez(path, **archive)
|
|
84
|
+
case ".h5" | ".hdf5":
|
|
85
|
+
import h5py
|
|
86
|
+
|
|
87
|
+
with h5py.File(path, "w") as file:
|
|
88
|
+
hdf5_container = HDF5BinsparseContainer(file)
|
|
89
|
+
tensor.serialize(
|
|
90
|
+
hdf5_container,
|
|
91
|
+
alias=alias,
|
|
92
|
+
copy=copy,
|
|
93
|
+
)
|
|
94
|
+
if header is not None:
|
|
95
|
+
_preserve_header(hdf5_container, header)
|
|
96
|
+
case ".zarr":
|
|
97
|
+
zarr = _zarr()
|
|
98
|
+
group = zarr.open_group(path, mode="w")
|
|
99
|
+
zarr_container = ZarrBinsparseContainer(group)
|
|
100
|
+
tensor.serialize(
|
|
101
|
+
zarr_container,
|
|
102
|
+
alias=alias,
|
|
103
|
+
copy=copy,
|
|
104
|
+
)
|
|
105
|
+
if header is not None:
|
|
106
|
+
_preserve_header(zarr_container, header)
|
|
107
|
+
case _:
|
|
108
|
+
raise _unsupported(path)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _preserve_header(container: Any, header: Mapping[str, Any]) -> None:
|
|
112
|
+
merged = _merge_header(container.read_header(), header)
|
|
113
|
+
data_types = merged.pop("data_types")
|
|
114
|
+
container.data_types.clear()
|
|
115
|
+
container.data_types.update(data_types)
|
|
116
|
+
container.write_header(merged)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _merge_header(
|
|
120
|
+
generated: Mapping[str, Any],
|
|
121
|
+
supplied: Mapping[str, Any],
|
|
122
|
+
) -> dict[str, Any]:
|
|
123
|
+
merged = dict(generated)
|
|
124
|
+
merged.update(supplied)
|
|
125
|
+
generated_data_types = generated.get("data_types")
|
|
126
|
+
supplied_data_types = supplied.get("data_types")
|
|
127
|
+
if isinstance(generated_data_types, dict):
|
|
128
|
+
data_types = dict(generated_data_types)
|
|
129
|
+
else:
|
|
130
|
+
data_types = {}
|
|
131
|
+
if isinstance(supplied_data_types, dict):
|
|
132
|
+
data_types.update(supplied_data_types)
|
|
133
|
+
merged["data_types"] = data_types
|
|
134
|
+
return merged
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _zarr() -> Any:
|
|
138
|
+
try:
|
|
139
|
+
import zarr
|
|
140
|
+
except ImportError as error:
|
|
141
|
+
raise ImportError(
|
|
142
|
+
"Zarr Binsparse I/O requires the 'zarr' package"
|
|
143
|
+
) from error
|
|
144
|
+
return zarr
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = ["load_binsparse", "save_binsparse"]
|