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,178 @@
1
+ """Command-line adapters for the Binsparse compliance tests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from collections.abc import Sequence
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ import numpy as np
12
+
13
+ from binsparse.io import load_binsparse, save_binsparse
14
+ from .reformat import binsparse_to_coo, coo_to_binsparse, reformat
15
+ from binsparse.tensor import (
16
+ _PREDEFINED_LEVELS,
17
+ BinsparseLevel,
18
+ BinsparseTensor,
19
+ CustomTensor,
20
+ DenseLevel,
21
+ ElementLevel,
22
+ SparseLevel,
23
+ )
24
+
25
+
26
+ def npy_to_binsparse_main(argv: Sequence[str] | None = None) -> int:
27
+ """Implement ``npy_to_binsparse``."""
28
+ args = sys.argv[1:] if argv is None else list(argv)
29
+ if len(args) != 5:
30
+ print(
31
+ "usage: npy_to_binsparse <tensor_in> <pattern_in> <fill_value_in> "
32
+ "<header_in> <tensor_out>",
33
+ file=sys.stderr,
34
+ )
35
+ return 2
36
+ tensor_in, pattern_in, fill_value_in, header_in, tensor_out = args
37
+
38
+ dense = np.load(tensor_in, allow_pickle=False)
39
+ pattern = np.asarray(np.load(pattern_in, allow_pickle=False), dtype=bool)
40
+ fill_value = _load_fill_value(fill_value_in)
41
+ header = _load_header(header_in)
42
+
43
+ tensor = _tensor_from_npy(dense, pattern, fill_value, header)
44
+ save_binsparse(tensor, tensor_out, header=header)
45
+ return 0
46
+
47
+
48
+ def binsparse_to_npy_main(argv: Sequence[str] | None = None) -> int:
49
+ """Implement ``binsparse_to_npy``."""
50
+ args = sys.argv[1:] if argv is None else list(argv)
51
+ if len(args) != 4:
52
+ print(
53
+ "usage: binsparse_to_npy <tensor_in> <tensor_out> "
54
+ "<pattern_out> <fill_value_out>",
55
+ file=sys.stderr,
56
+ )
57
+ return 2
58
+ tensor_in, tensor_out, pattern_out, fill_value_out = args
59
+
60
+ tensor = load_binsparse(tensor_in, alias=False)
61
+ assert isinstance(tensor, CustomTensor)
62
+ dense, pattern, fill_value = _npy_from_tensor(tensor)
63
+ np.save(tensor_out, dense)
64
+ np.save(pattern_out, pattern)
65
+ np.save(fill_value_out, np.asarray(fill_value))
66
+ return 0
67
+
68
+
69
+ def binsparse_to_binsparse_main(argv: Sequence[str] | None = None) -> int:
70
+ """Implement ``binsparse_to_binsparse``."""
71
+ args = sys.argv[1:] if argv is None else list(argv)
72
+ if len(args) != 2:
73
+ print(
74
+ "usage: binsparse_to_binsparse <tensor_in> <tensor_out>",
75
+ file=sys.stderr,
76
+ )
77
+ return 2
78
+ tensor_in, tensor_out = args
79
+
80
+ tensor = load_binsparse(tensor_in)
81
+ save_binsparse(tensor, tensor_out)
82
+ return 0
83
+
84
+
85
+ def _load_header(path: str) -> dict[str, Any]:
86
+ with Path(path).open() as file:
87
+ header = json.load(file)
88
+ if not isinstance(header, dict):
89
+ raise TypeError("header_in must contain a JSON object")
90
+ return header
91
+
92
+
93
+ def _load_fill_value(path: str) -> Any:
94
+ fill_value = np.load(path, allow_pickle=False)
95
+ if fill_value.size != 1:
96
+ raise ValueError("fill_value_in must contain exactly one value")
97
+ return fill_value.reshape(-1)[0]
98
+
99
+
100
+ def _tensor_from_npy(
101
+ dense: np.ndarray,
102
+ pattern: np.ndarray,
103
+ fill_value: Any,
104
+ header: dict[str, Any],
105
+ ) -> BinsparseTensor:
106
+ if dense.shape != pattern.shape:
107
+ raise ValueError("tensor_in and pattern_in must have the same shape")
108
+ format, transpose = _format_from_header(header)
109
+ target_transpose = transpose or tuple(range(dense.ndim))
110
+ stored_shape = tuple(dense.shape[dimension] for dimension in target_transpose)
111
+
112
+ entries = [
113
+ (
114
+ tuple(int(coord[dimension]) for dimension in target_transpose),
115
+ dense[tuple(coord)],
116
+ )
117
+ for coord in np.argwhere(pattern)
118
+ ]
119
+ custom = coo_to_binsparse(format, entries, shape=stored_shape)
120
+ custom.shape = tuple(dense.shape)
121
+ custom.transpose = None if transpose is None else tuple(transpose)
122
+ custom.fill = True
123
+ custom.fill_value = fill_value
124
+ return reformat(custom, header)
125
+
126
+
127
+ def _format_from_header(
128
+ header: dict[str, Any],
129
+ ) -> tuple[dict[str, Any], tuple[int, ...] | None]:
130
+ format_name = header["format"]
131
+
132
+ if format_name == "custom":
133
+ custom = header.get("custom")
134
+ if not isinstance(custom, dict) or "level" not in custom:
135
+ raise ValueError("custom format requires custom.level")
136
+ transpose = custom.get("transpose")
137
+ return custom["level"], None if transpose is None else tuple(transpose)
138
+
139
+ try:
140
+ format, transpose = _PREDEFINED_LEVELS[format_name]
141
+ except KeyError as error:
142
+ raise ValueError(f"unknown Binsparse format {format_name!r}") from error
143
+ return format, transpose
144
+
145
+
146
+ def _npy_from_tensor(tensor: CustomTensor) -> tuple[np.ndarray, np.ndarray, Any]:
147
+ fill_value = tensor.fill_value if tensor.fill is True else 0
148
+ values = _element_values(tensor.level)
149
+ dtype = (
150
+ np.result_type(values.dtype, np.asarray(fill_value).dtype)
151
+ if values.size > 0
152
+ else np.asarray(fill_value).dtype
153
+ )
154
+ dense = np.full(tensor.shape, fill_value, dtype=dtype)
155
+ pattern = np.zeros(tensor.shape, dtype=bool)
156
+ transpose = tensor.transpose or tuple(range(len(tensor.shape)))
157
+ for stored_coord, value in binsparse_to_coo(tensor):
158
+ coord = tuple(stored_coord[transpose.index(d)] for d in range(len(transpose)))
159
+ dense[coord] = value
160
+ pattern[coord] = True
161
+ return dense, pattern, fill_value
162
+
163
+
164
+ def _element_values(level: BinsparseLevel | None) -> np.ndarray:
165
+ match level:
166
+ case ElementLevel(values):
167
+ return values
168
+ case DenseLevel(_, child) | SparseLevel(_, child, _, _):
169
+ return _element_values(child)
170
+ case _:
171
+ raise TypeError(f"unsupported level type {type(level).__name__}")
172
+
173
+
174
+ __all__ = [
175
+ "binsparse_to_binsparse_main",
176
+ "binsparse_to_npy_main",
177
+ "npy_to_binsparse_main",
178
+ ]
@@ -0,0 +1,81 @@
1
+ """Compliance-test executables which round-trip through optional frameworks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from collections.abc import Callable, Sequence
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+
11
+ from .converters import (
12
+ _load_fill_value,
13
+ _load_header,
14
+ _npy_from_tensor,
15
+ _tensor_from_npy,
16
+ )
17
+ from binsparse.conversions import (
18
+ from_numpy,
19
+ from_scipy,
20
+ from_sparse,
21
+ to_numpy,
22
+ to_scipy,
23
+ to_sparse,
24
+ )
25
+ from binsparse.io import load_binsparse, save_binsparse
26
+ from .reformat import alias_to_custom
27
+ from binsparse.tensor import BinsparseTensor
28
+
29
+ Converter = tuple[Callable[[BinsparseTensor], Any], Callable[[Any], BinsparseTensor]]
30
+
31
+
32
+ def _converter(name: str) -> Converter:
33
+ converters: dict[str, Converter] = {
34
+ "numpy": (to_numpy, from_numpy),
35
+ "scipy": (to_scipy, from_scipy),
36
+ "sparse": (to_sparse, from_sparse),
37
+ }
38
+ try:
39
+ return converters[name]
40
+ except KeyError as error:
41
+ raise ValueError(f"unknown framework {name!r}") from error
42
+
43
+
44
+ def _roundtrip(tensor: BinsparseTensor, framework: str) -> BinsparseTensor:
45
+ to_framework, from_framework = _converter(framework)
46
+ return from_framework(to_framework(tensor))
47
+
48
+
49
+ def main(argv: Sequence[str] | None = None) -> int:
50
+ args = list(sys.argv[1:] if argv is None else argv)
51
+ if len(args) < 2:
52
+ return 2
53
+ framework, operation, *paths = args
54
+ if operation == "npy_to_binsparse" and len(paths) == 5:
55
+ tensor_in, pattern_in, fill_in, header_in, tensor_out = paths
56
+ header = _load_header(header_in)
57
+ tensor = _tensor_from_npy(
58
+ np.load(tensor_in, allow_pickle=False),
59
+ np.asarray(np.load(pattern_in, allow_pickle=False), dtype=bool),
60
+ _load_fill_value(fill_in),
61
+ header,
62
+ )
63
+ save_binsparse(_roundtrip(tensor, framework), tensor_out, header=header)
64
+ return 0
65
+ if operation == "binsparse_to_npy" and len(paths) == 4:
66
+ tensor_in, tensor_out, pattern_out, fill_out = paths
67
+ tensor = alias_to_custom(_roundtrip(load_binsparse(tensor_in), framework))
68
+ dense, pattern, fill = _npy_from_tensor(tensor)
69
+ np.save(tensor_out, dense)
70
+ np.save(pattern_out, pattern)
71
+ np.save(fill_out, np.asarray(fill))
72
+ return 0
73
+ if operation == "binsparse_to_binsparse" and len(paths) == 2:
74
+ tensor_in, tensor_out = paths
75
+ save_binsparse(_roundtrip(load_binsparse(tensor_in), framework), tensor_out)
76
+ return 0
77
+ return 2
78
+
79
+
80
+ if __name__ == "__main__":
81
+ raise SystemExit(main())
@@ -0,0 +1,290 @@
1
+ """Reformat Binsparse tensors through coordinate lists."""
2
+
3
+ from collections.abc import Iterable, Iterator
4
+ from itertools import groupby, product
5
+ from typing import Any, cast
6
+
7
+ import numpy as np
8
+
9
+ from binsparse.container import InMemoryBinsparseContainer, str_to_dtype
10
+ from binsparse.tensor import (
11
+ _PREDEFINED_LEVELS,
12
+ BinsparseLevel,
13
+ BinsparseTensor,
14
+ CustomTensor,
15
+ DenseLevel,
16
+ ElementLevel,
17
+ SparseLevel,
18
+ )
19
+
20
+ _FORMAT_CANONICAL = {
21
+ "DMAT": "DMATR",
22
+ "COO": "COOR",
23
+ }
24
+
25
+
26
+ def _canonical_format(format_name: str) -> str:
27
+ return _FORMAT_CANONICAL.get(format_name, format_name)
28
+
29
+
30
+ def alias_to_custom(tensor: BinsparseTensor) -> CustomTensor:
31
+ """Return the custom-level representation of any Binsparse tensor."""
32
+ container = InMemoryBinsparseContainer()
33
+ tensor.serialize(container, alias=False)
34
+ custom = CustomTensor.parse(container)
35
+ assert isinstance(custom, CustomTensor)
36
+ return custom
37
+
38
+
39
+ def custom_to_alias(
40
+ tensor: BinsparseTensor, format_name: str | None = None
41
+ ) -> BinsparseTensor:
42
+ """Return the predefined alias for a custom tensor, when one exists."""
43
+ container = InMemoryBinsparseContainer()
44
+ tensor.serialize(container, alias=True)
45
+ alias = BinsparseTensor.parse(container)
46
+ if (
47
+ format_name is not None
48
+ and _canonical_format(alias.format) != _canonical_format(format_name)
49
+ ):
50
+ raise ValueError(f"custom layout is not compatible with {format_name!r}")
51
+ return alias
52
+
53
+
54
+ def binsparse_to_coo(
55
+ tensor: CustomTensor,
56
+ ) -> Iterator[tuple[tuple[Any, ...], Any]]:
57
+ """Iterate over a tensor's explicitly stored coordinate/value pairs."""
58
+ shape = (
59
+ tensor.shape
60
+ if tensor.transpose is None
61
+ else tuple(tensor.shape[dimension] for dimension in tensor.transpose)
62
+ )
63
+ root = tensor.level
64
+ assert root is not None
65
+
66
+ def level_to_coo(
67
+ level: BinsparseLevel, coords: list[tuple[int, ...]], shape: tuple[int, ...]
68
+ ) -> Iterable[tuple[tuple[Any, ...], Any]]:
69
+ match level:
70
+ case ElementLevel(values):
71
+ return zip(coords, values, strict=True)
72
+ case DenseLevel(rank, child):
73
+ coords = [
74
+ (*coord, *suffix)
75
+ for coord in coords
76
+ for suffix in product(*(range(size) for size in shape[:rank]))
77
+ ]
78
+ return level_to_coo(child, coords, shape[rank:])
79
+ case SparseLevel(rank, child, indices, pointers):
80
+ pointers = (
81
+ np.array([0, indices[0].size])
82
+ if pointers is None
83
+ else pointers
84
+ )
85
+ coords = [
86
+ (*coord, *(index[q] for index in indices))
87
+ for p, coord in enumerate(coords)
88
+ for q in range(pointers[p], pointers[p + 1])
89
+ ]
90
+ return level_to_coo(child, coords, shape[rank:])
91
+ case _:
92
+ raise TypeError(f"unsupported level type {type(level).__name__}")
93
+
94
+ yield from level_to_coo(root, [()], shape)
95
+
96
+
97
+ def coo_to_binsparse(
98
+ format: dict[str, Any],
99
+ entries: Iterable[tuple[tuple[Any, ...], Any]],
100
+ *,
101
+ shape: tuple[int, ...],
102
+ ) -> CustomTensor:
103
+ """Build a custom tensor from a nested format descriptor and COO entries."""
104
+ entries = sorted((tuple(coord), value) for coord, value in entries)
105
+ for coord, _ in entries:
106
+ if len(coord) != len(shape):
107
+ raise ValueError("coordinate rank does not match tensor rank")
108
+ if any(i < 0 or i >= n for i, n in zip(coord, shape, strict=True)):
109
+ raise ValueError("coordinate is out of bounds")
110
+
111
+ def coo_to_level(
112
+ format: dict[str, Any],
113
+ entries: list[tuple[tuple[Any, ...], Any]],
114
+ ptr: np.ndarray,
115
+ shape: tuple[int, ...],
116
+ root: bool = False,
117
+ ) -> BinsparseLevel:
118
+ match format["level_desc"]:
119
+ case "element":
120
+ if np.any(np.diff(ptr) != 1):
121
+ raise ValueError("each element position must have one value")
122
+ return ElementLevel(np.asarray([value for _, value in entries]))
123
+ case "dense":
124
+ rank = format["rank"]
125
+ groups = [
126
+ [
127
+ (coord[rank:], value)
128
+ for coord, value in entries[ptr[p] : ptr[p + 1]]
129
+ if coord[:rank] == suffix
130
+ ]
131
+ for p in range(ptr.size - 1)
132
+ for suffix in product(*(range(size) for size in shape[:rank]))
133
+ ]
134
+ entries = [entry for group in groups for entry in group]
135
+ ptr = np.cumsum([0, *(len(group) for group in groups)])
136
+ return DenseLevel(
137
+ rank,
138
+ coo_to_level(format["level"], entries, ptr, shape[rank:]),
139
+ )
140
+ case "sparse":
141
+ rank = format["rank"]
142
+ grouped = [
143
+ [
144
+ (coord, list(group))
145
+ for coord, group in groupby(
146
+ entries[ptr[p] : ptr[p + 1]],
147
+ lambda entry: entry[0][:rank],
148
+ )
149
+ ]
150
+ for p in range(ptr.size - 1)
151
+ ]
152
+ groups = [
153
+ [(coord[rank:], value) for coord, value in group]
154
+ for fiber in grouped
155
+ for _, group in fiber
156
+ ]
157
+ entries = [entry for group in groups for entry in group]
158
+ child_ptr = np.cumsum([0, *(len(group) for group in groups)])
159
+ new_indices = tuple(
160
+ np.asarray([coord[d] for fiber in grouped for coord, _ in fiber])
161
+ for d in range(rank)
162
+ )
163
+ new_pointers = np.cumsum(
164
+ [0, *(len(fiber) for fiber in grouped)], dtype=np.uint64
165
+ )
166
+ return SparseLevel(
167
+ rank,
168
+ coo_to_level(
169
+ format["level"], entries, child_ptr, shape[rank:]
170
+ ),
171
+ new_indices,
172
+ None if root else new_pointers,
173
+ )
174
+ case _:
175
+ raise ValueError(f"unknown level descriptor {format['level_desc']!r}")
176
+
177
+ return CustomTensor(
178
+ shape,
179
+ len(entries),
180
+ level=coo_to_level(
181
+ format,
182
+ entries,
183
+ np.array([0, len(entries)], dtype=np.uint64),
184
+ shape,
185
+ root=True,
186
+ ),
187
+ )
188
+
189
+ def reformat(tensor: BinsparseTensor, header: dict[str, Any]) -> BinsparseTensor:
190
+ """Reformat *tensor* according to a Binsparse format descriptor."""
191
+ if isinstance(tensor, CustomTensor):
192
+ source = tensor
193
+ else:
194
+ container = InMemoryBinsparseContainer()
195
+ tensor.serialize(container, alias=False)
196
+ parsed = CustomTensor.parse(container)
197
+ assert isinstance(parsed, CustomTensor)
198
+ source = parsed
199
+
200
+ format_name = header["format"]
201
+ if format_name == "custom":
202
+ custom = header["custom"]
203
+ format = custom["level"]
204
+ transpose = custom.get("transpose")
205
+ else:
206
+ try:
207
+ format, transpose = _PREDEFINED_LEVELS[format_name]
208
+ except KeyError as error:
209
+ raise ValueError(f"unknown Binsparse format {format_name!r}") from error
210
+
211
+ source_transpose = source.transpose or tuple(range(len(source.shape)))
212
+ target_transpose = transpose or tuple(range(len(source.shape)))
213
+ entries = [
214
+ (
215
+ tuple(
216
+ coord[source_transpose.index(d)]
217
+ for d in target_transpose
218
+ ),
219
+ value,
220
+ )
221
+ for coord, value in binsparse_to_coo(source)
222
+ ]
223
+ stored_shape = tuple(source.shape[d] for d in target_transpose)
224
+ result = coo_to_binsparse(format, entries, shape=stored_shape)
225
+ result.shape = source.shape
226
+ result.transpose = None if transpose is None else tuple(transpose)
227
+ result.fill = source.fill
228
+ result.fill_value = source.fill_value
229
+
230
+ container = InMemoryBinsparseContainer()
231
+ result.serialize(container, alias=format_name != "custom")
232
+ _recast_buffers(container, header.get("data_types"))
233
+ alias = BinsparseTensor.parse(container)
234
+ if _canonical_format(alias.format) != _canonical_format(format_name):
235
+ raise ValueError(f"custom layout is not compatible with {format_name!r}")
236
+ return alias
237
+
238
+
239
+ def _recast_buffers(container: InMemoryBinsparseContainer, data_types: Any) -> None:
240
+ if not isinstance(data_types, dict):
241
+ return
242
+ for key, data_type in data_types.items():
243
+ if key not in container.data_types:
244
+ continue
245
+ value = container.read_buffer(key, _expected_size(container, key))
246
+ container.write_buffer(key, _cast_buffer(value, data_type))
247
+ container.write_header(container.read_header())
248
+
249
+
250
+ def _expected_size(
251
+ container: InMemoryBinsparseContainer,
252
+ key: str,
253
+ ) -> int | None:
254
+ if key == "fill_value":
255
+ return 1
256
+ if key == "values":
257
+ return int(container.read_header()["number_of_stored_values"])
258
+ return None
259
+
260
+
261
+ def _cast_buffer(value: np.ndarray, data_type: Any) -> np.ndarray:
262
+ if not isinstance(data_type, str):
263
+ raise TypeError("Binsparse data type must be a string")
264
+ if data_type.startswith("iso[") and data_type.endswith("]"):
265
+ result = _cast_buffer(value, data_type[4:-1])
266
+ if result.size == 0:
267
+ return result
268
+ scalar = result.reshape(-1)[0]
269
+ if not np.all(result == scalar):
270
+ raise ValueError("ISO data type requires identical stored values")
271
+ return np.broadcast_to(np.asarray([scalar], dtype=result.dtype), result.shape)
272
+ if data_type.startswith("complex[") and data_type.endswith("]"):
273
+ inner = data_type[8:-1]
274
+ if inner == "float32":
275
+ return np.asarray(value, dtype=np.dtype("complex64"))
276
+ if inner == "float64":
277
+ return np.asarray(value, dtype=np.dtype("complex128"))
278
+ try:
279
+ return np.asarray(value, dtype=cast(np.dtype[Any], str_to_dtype[data_type]))
280
+ except KeyError as error:
281
+ raise ValueError(f"unknown Binsparse data type {data_type!r}") from error
282
+
283
+
284
+ __all__ = [
285
+ "alias_to_custom",
286
+ "binsparse_to_coo",
287
+ "coo_to_binsparse",
288
+ "custom_to_alias",
289
+ "reformat",
290
+ ]