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.
@@ -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
@@ -0,0 +1,5 @@
1
+ """Exceptions raised while decoding Binsparse containers."""
2
+
3
+
4
+ class BinsparseParseError(ValueError):
5
+ """The Binsparse descriptor or its binary arrays are invalid."""
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"]