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,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
|
+
]
|