pyadata-cli 0.5.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.
- adata/__init__.py +10 -0
- adata/cli.py +1139 -0
- adata/commands/__init__.py +8 -0
- adata/commands/concat.py +21 -0
- adata/commands/create.py +93 -0
- adata/commands/export.py +105 -0
- adata/commands/import_data.py +144 -0
- adata/commands/info.py +199 -0
- adata/commands/ls.py +114 -0
- adata/commands/split.py +152 -0
- adata/commands/subset.py +17 -0
- adata/core/__init__.py +1 -0
- adata/core/concat.py +1084 -0
- adata/core/info.py +276 -0
- adata/core/query.py +268 -0
- adata/core/read.py +28 -0
- adata/core/select.py +122 -0
- adata/core/subset.py +767 -0
- adata/elements/__init__.py +10 -0
- adata/elements/read.py +310 -0
- adata/elements/spec.py +125 -0
- adata/elements/strings.py +90 -0
- adata/elements/write.py +255 -0
- adata/formats/__init__.py +1 -0
- adata/formats/array.py +109 -0
- adata/formats/common.py +51 -0
- adata/formats/dataframe.py +234 -0
- adata/formats/image.py +85 -0
- adata/formats/json_data.py +196 -0
- adata/formats/sparse.py +253 -0
- adata/formats/validate.py +180 -0
- adata/info.py +3 -0
- adata/py.typed +0 -0
- adata/read.py +19 -0
- adata/storage/__init__.py +529 -0
- adata/util/__init__.py +1 -0
- adata/util/path.py +9 -0
- pyadata_cli-0.5.0.dist-info/METADATA +135 -0
- pyadata_cli-0.5.0.dist-info/RECORD +42 -0
- pyadata_cli-0.5.0.dist-info/WHEEL +4 -0
- pyadata_cli-0.5.0.dist-info/entry_points.txt +4 -0
- pyadata_cli-0.5.0.dist-info/licenses/LICENSE +21 -0
adata/formats/image.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from PIL import Image
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from adata.elements.write import write_dense, write_mapping
|
|
11
|
+
from adata.formats.validate import DATAFRAME_PATHS, validate_dimensions
|
|
12
|
+
from adata.formats.common import _resolve
|
|
13
|
+
from adata.storage import is_dataset
|
|
14
|
+
from adata.util.path import norm_path
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def export_image(root: Any, obj: str, out: Path, console: Console) -> None:
|
|
18
|
+
h5obj = _resolve(root, obj)
|
|
19
|
+
if not is_dataset(h5obj):
|
|
20
|
+
raise ValueError("Image export requires a dataset.")
|
|
21
|
+
arr = np.asarray(h5obj[...])
|
|
22
|
+
|
|
23
|
+
if arr.ndim not in (2, 3):
|
|
24
|
+
raise ValueError(f"Expected 2D or 3D image array; got shape {arr.shape}.")
|
|
25
|
+
if arr.ndim == 3 and arr.shape[2] not in (1, 3, 4):
|
|
26
|
+
raise ValueError(
|
|
27
|
+
f"Expected last dimension (channels) to be 1, 3, or 4; got {arr.shape}."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if np.issubdtype(arr.dtype, np.floating):
|
|
31
|
+
amax = float(np.nanmax(arr)) if arr.size else 0.0
|
|
32
|
+
if amax <= 1.0:
|
|
33
|
+
arr = np.clip(arr, 0.0, 1.0) * 255.0
|
|
34
|
+
else:
|
|
35
|
+
arr = np.clip(arr, 0.0, 255.0)
|
|
36
|
+
arr = arr.astype(np.uint8)
|
|
37
|
+
elif np.issubdtype(arr.dtype, np.integer):
|
|
38
|
+
arr = np.clip(arr, 0, 255).astype(np.uint8)
|
|
39
|
+
elif arr.dtype == np.bool_:
|
|
40
|
+
arr = arr.astype(np.uint8) * 255
|
|
41
|
+
else:
|
|
42
|
+
raise ValueError(f"Unsupported image dtype: {arr.dtype}")
|
|
43
|
+
|
|
44
|
+
if arr.ndim == 3 and arr.shape[2] == 1:
|
|
45
|
+
arr = arr[:, :, 0]
|
|
46
|
+
|
|
47
|
+
img = Image.fromarray(arr)
|
|
48
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
img.save(out)
|
|
50
|
+
console.print(f"[green]Wrote[/] {out}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def import_image(root: Any, obj: str, input_file: Path, console: Console) -> None:
|
|
54
|
+
"""Read an image file into the store as a dense array.
|
|
55
|
+
|
|
56
|
+
Kept as raw pixels (H, W) or (H, W, C) with no encoding beyond `array`,
|
|
57
|
+
which is how spatial tooling stores tissue images under uns/spatial.
|
|
58
|
+
"""
|
|
59
|
+
obj = norm_path(obj)
|
|
60
|
+
arr = np.asarray(Image.open(input_file))
|
|
61
|
+
|
|
62
|
+
if arr.ndim not in (2, 3):
|
|
63
|
+
raise ValueError(f"Expected a 2D or 3D image; got shape {arr.shape}.")
|
|
64
|
+
|
|
65
|
+
if obj in DATAFRAME_PATHS:
|
|
66
|
+
raise ValueError(
|
|
67
|
+
f"'{obj}' must hold a dataframe; writing an image there would "
|
|
68
|
+
"corrupt the store. Images belong somewhere unstructured, such as "
|
|
69
|
+
"'uns/spatial/hires'."
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# An image's height is not an axis, so a path that implies one is almost
|
|
73
|
+
# certainly a mistake -- but check it rather than assume.
|
|
74
|
+
validate_dimensions(root, obj, arr.shape, console)
|
|
75
|
+
|
|
76
|
+
parts = obj.split("/")
|
|
77
|
+
parent = root
|
|
78
|
+
for part in parts[:-1]:
|
|
79
|
+
parent = parent[part] if part in parent else write_mapping(parent, part)
|
|
80
|
+
|
|
81
|
+
write_dense(parent, parts[-1], arr, replace=True)
|
|
82
|
+
console.print(
|
|
83
|
+
f"[green]Imported[/] {'x'.join(str(d) for d in arr.shape)} "
|
|
84
|
+
f"image ({arr.dtype}) into '{obj}'"
|
|
85
|
+
)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Dict
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from adata.core.read import decode_str_array
|
|
12
|
+
from adata.elements import spec
|
|
13
|
+
from adata.formats.common import _check_json_exportable, _resolve
|
|
14
|
+
from adata.elements.write import (
|
|
15
|
+
write_dense,
|
|
16
|
+
write_mapping,
|
|
17
|
+
write_null,
|
|
18
|
+
write_scalar,
|
|
19
|
+
write_string_array,
|
|
20
|
+
)
|
|
21
|
+
from adata.storage import create_dataset, is_dataset, is_group
|
|
22
|
+
from adata.util.path import norm_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def export_json(
|
|
26
|
+
root: Any,
|
|
27
|
+
obj: str,
|
|
28
|
+
out: Path | None,
|
|
29
|
+
max_elements: int,
|
|
30
|
+
include_attrs: bool,
|
|
31
|
+
console: Console,
|
|
32
|
+
) -> None:
|
|
33
|
+
h5obj = _resolve(root, obj)
|
|
34
|
+
_check_json_exportable(h5obj, max_elements=max_elements)
|
|
35
|
+
|
|
36
|
+
payload = _to_jsonable(
|
|
37
|
+
h5obj, max_elements=max_elements, include_attrs=include_attrs
|
|
38
|
+
)
|
|
39
|
+
if out is None or str(out) == "-":
|
|
40
|
+
out_fh = sys.stdout
|
|
41
|
+
else:
|
|
42
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
out_fh = open(out, "w", encoding="utf-8")
|
|
44
|
+
try:
|
|
45
|
+
json.dump(payload, out_fh, indent=2, ensure_ascii=False, sort_keys=True)
|
|
46
|
+
out_fh.write("\n")
|
|
47
|
+
finally:
|
|
48
|
+
if out_fh is not sys.stdout:
|
|
49
|
+
out_fh.close()
|
|
50
|
+
if out_fh is not sys.stdout:
|
|
51
|
+
console.print(f"[green]Wrote[/] {out}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _attrs_to_jsonable(attrs: Any, max_elements: int) -> Dict[str, Any]:
|
|
55
|
+
out: Dict[str, Any] = {}
|
|
56
|
+
for k in attrs.keys():
|
|
57
|
+
v = attrs.get(k)
|
|
58
|
+
out[str(k)] = _pyify(v, max_elements=max_elements)
|
|
59
|
+
return out
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _pyify(value: Any, max_elements: int) -> Any:
|
|
63
|
+
if isinstance(value, bytes):
|
|
64
|
+
try:
|
|
65
|
+
return value.decode("utf-8")
|
|
66
|
+
except Exception:
|
|
67
|
+
return value.decode("utf-8", errors="replace")
|
|
68
|
+
if isinstance(value, np.generic):
|
|
69
|
+
return value.item()
|
|
70
|
+
if isinstance(value, np.ndarray):
|
|
71
|
+
if value.size > max_elements:
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"Refusing to convert array of size {value.size} (> {max_elements}) to JSON."
|
|
74
|
+
)
|
|
75
|
+
if np.issubdtype(value.dtype, np.bytes_) or value.dtype.kind == "O":
|
|
76
|
+
value = decode_str_array(value)
|
|
77
|
+
return value.tolist()
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _dataset_to_jsonable(ds: Any, max_elements: int) -> Any:
|
|
82
|
+
if spec.encoding_type(ds) == spec.NULL:
|
|
83
|
+
# A null is a placeholder -- an h5py.Empty or a 0-d zarr bool -- whose
|
|
84
|
+
# stored value is meaningless. Reading it would emit that placeholder
|
|
85
|
+
# rather than restoring the None it represents.
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
if ds.shape == ():
|
|
89
|
+
v = ds[()]
|
|
90
|
+
return _pyify(v, max_elements=max_elements)
|
|
91
|
+
n = int(np.prod(ds.shape)) if ds.shape else 0
|
|
92
|
+
if n > max_elements:
|
|
93
|
+
ds_name = getattr(ds, "name", "<dataset>")
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"Refusing to convert dataset {ds_name!r} with {n} elements (> {max_elements}) to JSON."
|
|
96
|
+
)
|
|
97
|
+
arr = np.asarray(ds[...])
|
|
98
|
+
return _pyify(arr, max_elements=max_elements)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _to_jsonable(h5obj: Any, max_elements: int, include_attrs: bool) -> Any:
|
|
102
|
+
if is_dataset(h5obj):
|
|
103
|
+
return _dataset_to_jsonable(h5obj, max_elements=max_elements)
|
|
104
|
+
|
|
105
|
+
d: Dict[str, Any] = {}
|
|
106
|
+
if include_attrs and len(h5obj.attrs):
|
|
107
|
+
d["__attrs__"] = _attrs_to_jsonable(h5obj.attrs, max_elements=max_elements)
|
|
108
|
+
|
|
109
|
+
for key in h5obj.keys():
|
|
110
|
+
child = h5obj[key]
|
|
111
|
+
if is_group(child) or is_dataset(child):
|
|
112
|
+
d[str(key)] = _to_jsonable(
|
|
113
|
+
child,
|
|
114
|
+
max_elements=max_elements,
|
|
115
|
+
include_attrs=include_attrs,
|
|
116
|
+
)
|
|
117
|
+
return d
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def import_json(
|
|
121
|
+
root: Any,
|
|
122
|
+
obj: str,
|
|
123
|
+
input_file: Path,
|
|
124
|
+
console: Console,
|
|
125
|
+
) -> None:
|
|
126
|
+
obj = norm_path(obj)
|
|
127
|
+
with open(input_file, "r", encoding="utf-8") as fh:
|
|
128
|
+
payload = json.load(fh)
|
|
129
|
+
|
|
130
|
+
parts = obj.split("/")
|
|
131
|
+
parent = root
|
|
132
|
+
for part in parts[:-1]:
|
|
133
|
+
parent = parent[part] if part in parent else parent.create_group(part)
|
|
134
|
+
name = parts[-1]
|
|
135
|
+
|
|
136
|
+
if name in parent:
|
|
137
|
+
del parent[name]
|
|
138
|
+
|
|
139
|
+
_write_json_to_group(parent, name, payload)
|
|
140
|
+
|
|
141
|
+
console.print(f"[green]Imported[/] JSON data into '{obj}'")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _write_json_to_group(parent: Any, name: str, value: Any) -> None:
|
|
145
|
+
"""Write one JSON value as the AnnData element that best represents it."""
|
|
146
|
+
if isinstance(value, dict):
|
|
147
|
+
group = write_mapping(parent, name, replace=True)
|
|
148
|
+
for k, v in value.items():
|
|
149
|
+
_write_json_to_group(group, k, v)
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
if value is None:
|
|
153
|
+
write_null(parent, name, replace=True)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if isinstance(value, str):
|
|
157
|
+
write_scalar(parent, name, value, replace=True)
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if isinstance(value, (bool, int, float)):
|
|
161
|
+
write_scalar(parent, name, value, replace=True)
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
if isinstance(value, list):
|
|
165
|
+
_write_json_list(parent, name, value)
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
raise ValueError(f"Cannot convert JSON value of type {type(value).__name__}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _write_json_list(parent: Any, name: str, value: list) -> None:
|
|
172
|
+
"""Write a JSON array as a string or numeric array where it is uniform.
|
|
173
|
+
|
|
174
|
+
Ragged or mixed lists have no array representation in the spec, so they
|
|
175
|
+
are stored as their JSON text rather than silently reshaped.
|
|
176
|
+
"""
|
|
177
|
+
if all(isinstance(v, str) for v in value):
|
|
178
|
+
write_string_array(parent, name, value, replace=True)
|
|
179
|
+
return
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
arr = np.array(value)
|
|
183
|
+
except (ValueError, TypeError):
|
|
184
|
+
arr = None
|
|
185
|
+
|
|
186
|
+
if arr is not None and arr.dtype.kind in ("b", "i", "u", "f"):
|
|
187
|
+
write_dense(parent, name, arr, replace=True)
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
if arr is not None and arr.dtype.kind in ("U", "S", "O", "T"):
|
|
191
|
+
# Pass the shaped array through: flattening would turn a nested list
|
|
192
|
+
# such as [["a","b"],["c","d"]] into a length-4 vector.
|
|
193
|
+
write_string_array(parent, name, arr, replace=True)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
write_scalar(parent, name, json.dumps(value), replace=True)
|
adata/formats/sparse.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, List, Tuple
|
|
5
|
+
import sys
|
|
6
|
+
from contextlib import nullcontext
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from adata.elements import spec
|
|
12
|
+
from adata.elements.write import write_sparse
|
|
13
|
+
from adata.formats.common import _get_encoding_type, _resolve
|
|
14
|
+
from adata.formats.validate import validate_dimensions
|
|
15
|
+
from adata.storage import create_dataset, is_dataset, is_group, is_zarr_group
|
|
16
|
+
from adata.util.path import norm_path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _read_mtx(
|
|
20
|
+
input_file: Path,
|
|
21
|
+
) -> Tuple[List[Tuple[int, int, float]], Tuple[int, int], int]:
|
|
22
|
+
with open(input_file, "r", encoding="utf-8") as fh:
|
|
23
|
+
header = fh.readline()
|
|
24
|
+
if not header.startswith("%%MatrixMarket"):
|
|
25
|
+
raise ValueError("Invalid MTX file: missing MatrixMarket header.")
|
|
26
|
+
|
|
27
|
+
parts = header.lower().split()
|
|
28
|
+
field = "real"
|
|
29
|
+
for p in parts:
|
|
30
|
+
if p in ("real", "integer", "complex", "pattern"):
|
|
31
|
+
field = p
|
|
32
|
+
break
|
|
33
|
+
|
|
34
|
+
line = fh.readline()
|
|
35
|
+
while line.startswith("%"):
|
|
36
|
+
line = fh.readline()
|
|
37
|
+
|
|
38
|
+
dims = line.split()
|
|
39
|
+
n_rows, n_cols, nnz = int(dims[0]), int(dims[1]), int(dims[2])
|
|
40
|
+
|
|
41
|
+
entries = []
|
|
42
|
+
for _ in range(nnz):
|
|
43
|
+
parts = fh.readline().split()
|
|
44
|
+
r, c = int(parts[0]) - 1, int(parts[1]) - 1
|
|
45
|
+
if field == "pattern":
|
|
46
|
+
v = 1.0
|
|
47
|
+
else:
|
|
48
|
+
v = float(parts[2])
|
|
49
|
+
entries.append((r, c, v))
|
|
50
|
+
|
|
51
|
+
return entries, (n_rows, n_cols), nnz
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _create_csr_from_entries(
|
|
55
|
+
entries: List[Tuple[int, int, float]], shape: Tuple[int, int]
|
|
56
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
57
|
+
n_rows, _ = shape
|
|
58
|
+
entries.sort(key=lambda x: (x[0], x[1]))
|
|
59
|
+
|
|
60
|
+
data = np.array([e[2] for e in entries], dtype=np.float32)
|
|
61
|
+
indices = np.array([e[1] for e in entries], dtype=np.int32)
|
|
62
|
+
|
|
63
|
+
indptr = np.zeros(n_rows + 1, dtype=np.int32)
|
|
64
|
+
for r, _, _ in entries:
|
|
65
|
+
indptr[r + 1] += 1
|
|
66
|
+
indptr = np.cumsum(indptr)
|
|
67
|
+
|
|
68
|
+
return data, indices, indptr
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def export_mtx(
|
|
72
|
+
root: Any,
|
|
73
|
+
obj: str,
|
|
74
|
+
out: Path | None,
|
|
75
|
+
head: int | None,
|
|
76
|
+
chunk_elements: int,
|
|
77
|
+
in_memory: bool,
|
|
78
|
+
console: Console,
|
|
79
|
+
) -> None:
|
|
80
|
+
h5obj = _resolve(root, obj)
|
|
81
|
+
if not is_group(h5obj):
|
|
82
|
+
raise ValueError("MTX export requires a CSR/CSC matrix group (not a dataset).")
|
|
83
|
+
|
|
84
|
+
enc = _get_encoding_type(h5obj)
|
|
85
|
+
if enc not in ("csr_matrix", "csc_matrix"):
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Target group encoding-type is {enc!r}; expected 'csr_matrix' or 'csc_matrix'."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
data = h5obj.get("data")
|
|
91
|
+
indices = h5obj.get("indices")
|
|
92
|
+
indptr = h5obj.get("indptr")
|
|
93
|
+
if not (is_dataset(data) and is_dataset(indices) and is_dataset(indptr)):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
"Sparse matrix group must contain datasets: data, indices, indptr"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
shape = h5obj.attrs.get("shape", None)
|
|
99
|
+
if shape is None:
|
|
100
|
+
raise ValueError("Sparse matrix group is missing required 'shape' attribute.")
|
|
101
|
+
n_rows, n_cols = (int(shape[0]), int(shape[1]))
|
|
102
|
+
|
|
103
|
+
field = "real" if np.issubdtype(data.dtype, np.floating) else "integer"
|
|
104
|
+
|
|
105
|
+
indptr_arr = np.asarray(indptr[...], dtype=np.int64)
|
|
106
|
+
nnz_ptr = int(indptr_arr[-1]) if indptr_arr.size else 0
|
|
107
|
+
nnz_data = int(data.shape[0])
|
|
108
|
+
nnz_idx = int(indices.shape[0])
|
|
109
|
+
|
|
110
|
+
if not (nnz_ptr == nnz_data == nnz_idx):
|
|
111
|
+
raise ValueError(
|
|
112
|
+
f"Sparse matrix data inconsistency: indptr implies {nnz_ptr} nonzeros, "
|
|
113
|
+
f"but data has {nnz_data} and indices has {nnz_idx}."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
nnz = nnz_data
|
|
117
|
+
major_step = max(1, int(chunk_elements))
|
|
118
|
+
if head is not None and head > 0:
|
|
119
|
+
nnz = min(nnz_data, head)
|
|
120
|
+
|
|
121
|
+
if out is None or str(out) == "-":
|
|
122
|
+
out_fh = sys.stdout
|
|
123
|
+
else:
|
|
124
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
out_fh = open(out, "w", encoding="utf-8", newline="\n")
|
|
126
|
+
|
|
127
|
+
use_status = out_fh is not sys.stdout
|
|
128
|
+
status_ctx = (
|
|
129
|
+
console.status(f"[magenta]Exporting {obj} to {out}...[/]")
|
|
130
|
+
if use_status
|
|
131
|
+
else nullcontext()
|
|
132
|
+
)
|
|
133
|
+
try:
|
|
134
|
+
out_fh.write(f"%%MatrixMarket matrix coordinate {field} general\n")
|
|
135
|
+
out_fh.write("% generated by h5ad-cli\n")
|
|
136
|
+
if head is not None and head > 0:
|
|
137
|
+
out_fh.write(f"% output limited to first {nnz}/{nnz_data} nonzero entries\n")
|
|
138
|
+
out_fh.write(f"{n_rows} {n_cols} {nnz}\n")
|
|
139
|
+
|
|
140
|
+
if in_memory:
|
|
141
|
+
with status_ctx as status:
|
|
142
|
+
if use_status and status:
|
|
143
|
+
status.update(
|
|
144
|
+
f"[magenta]Loading entire matrix {obj} into memory...[/]"
|
|
145
|
+
)
|
|
146
|
+
data_arr = np.asarray(data[...])
|
|
147
|
+
indices_arr = np.asarray(indices[...], dtype=np.int64)
|
|
148
|
+
counts = np.diff(indptr_arr)
|
|
149
|
+
if int(counts.sum()) != nnz_data:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
"Sparse matrix indptr does not match data/indices length."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if enc == "csr_matrix":
|
|
155
|
+
major_idx = np.repeat(np.arange(n_rows, dtype=np.int64), counts)
|
|
156
|
+
row_idx = major_idx
|
|
157
|
+
col_idx = indices_arr
|
|
158
|
+
else:
|
|
159
|
+
major_idx = np.repeat(np.arange(n_cols, dtype=np.int64), counts)
|
|
160
|
+
row_idx = indices_arr
|
|
161
|
+
col_idx = major_idx
|
|
162
|
+
|
|
163
|
+
if head is not None and head > 0:
|
|
164
|
+
row_idx = row_idx[:nnz]
|
|
165
|
+
col_idx = col_idx[:nnz]
|
|
166
|
+
data_arr = data_arr[:nnz]
|
|
167
|
+
|
|
168
|
+
data_fmt = "%.18g" if field == "real" else "%d"
|
|
169
|
+
coords = np.column_stack((row_idx + 1, col_idx + 1, data_arr))
|
|
170
|
+
if use_status and status:
|
|
171
|
+
status.update(f"[magenta]Saving {nnz} entries to {out}...[/]")
|
|
172
|
+
np.savetxt(out_fh, coords, fmt=["%d", "%d", data_fmt], newline="\n")
|
|
173
|
+
else:
|
|
174
|
+
major = n_rows if enc == "csr_matrix" else n_cols
|
|
175
|
+
max_lines = head if head is not None and head > 0 else None
|
|
176
|
+
written = 0
|
|
177
|
+
with status_ctx as status:
|
|
178
|
+
for major_start in range(0, major, major_step):
|
|
179
|
+
major_end = min(major_start + major_step, major)
|
|
180
|
+
if use_status and status:
|
|
181
|
+
status.update(
|
|
182
|
+
f"[magenta]Exporting {obj}: {major_start+1}-{major_end} of {major}...[/]"
|
|
183
|
+
)
|
|
184
|
+
for major_i in range(major_start, major_end):
|
|
185
|
+
start = min(int(indptr_arr[major_i]), nnz_data)
|
|
186
|
+
end = min(int(indptr_arr[major_i + 1]), nnz_data)
|
|
187
|
+
if end <= start:
|
|
188
|
+
continue
|
|
189
|
+
idx = np.asarray(indices[start:end], dtype=np.int64)
|
|
190
|
+
vals = np.asarray(data[start:end])
|
|
191
|
+
m = min(len(idx), len(vals))
|
|
192
|
+
if m == 0:
|
|
193
|
+
raise ValueError("Sparse matrix chunk has zero length.")
|
|
194
|
+
if max_lines is not None:
|
|
195
|
+
remaining = max_lines - written
|
|
196
|
+
if remaining <= 0:
|
|
197
|
+
break
|
|
198
|
+
if m > remaining:
|
|
199
|
+
m = remaining
|
|
200
|
+
idx = idx[:m]
|
|
201
|
+
vals = vals[:m]
|
|
202
|
+
idx_list = idx.tolist()
|
|
203
|
+
vals_list = vals.tolist()
|
|
204
|
+
if enc == "csr_matrix":
|
|
205
|
+
r = major_i + 1
|
|
206
|
+
lines = [
|
|
207
|
+
f"{r} {c + 1} {v}\n"
|
|
208
|
+
for c, v in zip(idx_list, vals_list)
|
|
209
|
+
]
|
|
210
|
+
else:
|
|
211
|
+
c = major_i + 1
|
|
212
|
+
lines = [
|
|
213
|
+
f"{r + 1} {c} {v}\n"
|
|
214
|
+
for r, v in zip(idx_list, vals_list)
|
|
215
|
+
]
|
|
216
|
+
out_fh.write("".join(lines))
|
|
217
|
+
written += m
|
|
218
|
+
if max_lines is not None and written >= max_lines:
|
|
219
|
+
break
|
|
220
|
+
if max_lines is not None and written >= max_lines:
|
|
221
|
+
break
|
|
222
|
+
finally:
|
|
223
|
+
if out_fh is not sys.stdout:
|
|
224
|
+
out_fh.close()
|
|
225
|
+
if out_fh is not sys.stdout:
|
|
226
|
+
console.print(f"[green]Wrote[/] {out}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def import_mtx(
|
|
230
|
+
root: Any,
|
|
231
|
+
obj: str,
|
|
232
|
+
input_file: Path,
|
|
233
|
+
console: Console,
|
|
234
|
+
) -> None:
|
|
235
|
+
obj = norm_path(obj)
|
|
236
|
+
entries, shape, nnz = _read_mtx(input_file)
|
|
237
|
+
data, indices, indptr = _create_csr_from_entries(entries, shape)
|
|
238
|
+
|
|
239
|
+
validate_dimensions(root, obj, shape, console)
|
|
240
|
+
|
|
241
|
+
parts = obj.split("/")
|
|
242
|
+
parent = root
|
|
243
|
+
for part in parts[:-1]:
|
|
244
|
+
parent = parent[part] if part in parent else parent.create_group(part)
|
|
245
|
+
name = parts[-1]
|
|
246
|
+
|
|
247
|
+
write_sparse(
|
|
248
|
+
parent, name, data, indices, indptr, shape, spec.CSR_MATRIX, replace=True
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
console.print(
|
|
252
|
+
f"[green]Imported[/] {shape[0]}×{shape[1]} sparse matrix ({nnz} non-zero) into '{obj}'"
|
|
253
|
+
)
|