swcgeom 0.18.3__py3-none-any.whl → 0.19.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.
Potentially problematic release.
This version of swcgeom might be problematic. Click here for more details.
- swcgeom/analysis/feature_extractor.py +22 -24
- swcgeom/analysis/features.py +18 -40
- swcgeom/analysis/lmeasure.py +227 -323
- swcgeom/analysis/sholl.py +17 -23
- swcgeom/analysis/trunk.py +23 -28
- swcgeom/analysis/visualization.py +37 -44
- swcgeom/analysis/visualization3d.py +16 -25
- swcgeom/analysis/volume.py +33 -47
- swcgeom/core/__init__.py +1 -6
- swcgeom/core/branch.py +10 -17
- swcgeom/core/branch_tree.py +3 -2
- swcgeom/core/compartment.py +1 -1
- swcgeom/core/node.py +3 -6
- swcgeom/core/path.py +11 -16
- swcgeom/core/population.py +32 -51
- swcgeom/core/swc.py +25 -16
- swcgeom/core/swc_utils/__init__.py +4 -6
- swcgeom/core/swc_utils/assembler.py +5 -12
- swcgeom/core/swc_utils/base.py +40 -31
- swcgeom/core/swc_utils/checker.py +3 -8
- swcgeom/core/swc_utils/io.py +32 -47
- swcgeom/core/swc_utils/normalizer.py +17 -23
- swcgeom/core/swc_utils/subtree.py +13 -20
- swcgeom/core/tree.py +61 -51
- swcgeom/core/tree_utils.py +36 -49
- swcgeom/core/tree_utils_impl.py +4 -6
- swcgeom/images/augmentation.py +23 -39
- swcgeom/images/contrast.py +22 -46
- swcgeom/images/folder.py +32 -34
- swcgeom/images/io.py +80 -121
- swcgeom/transforms/base.py +28 -19
- swcgeom/transforms/branch.py +31 -41
- swcgeom/transforms/branch_tree.py +3 -1
- swcgeom/transforms/geometry.py +13 -4
- swcgeom/transforms/image_preprocess.py +2 -0
- swcgeom/transforms/image_stack.py +40 -35
- swcgeom/transforms/images.py +31 -24
- swcgeom/transforms/mst.py +27 -40
- swcgeom/transforms/neurolucida_asc.py +13 -13
- swcgeom/transforms/path.py +4 -0
- swcgeom/transforms/population.py +4 -0
- swcgeom/transforms/tree.py +16 -11
- swcgeom/transforms/tree_assembler.py +37 -54
- swcgeom/utils/download.py +7 -14
- swcgeom/utils/dsu.py +12 -0
- swcgeom/utils/ellipse.py +26 -14
- swcgeom/utils/file.py +8 -13
- swcgeom/utils/neuromorpho.py +78 -92
- swcgeom/utils/numpy_helper.py +15 -12
- swcgeom/utils/plotter_2d.py +10 -16
- swcgeom/utils/plotter_3d.py +7 -9
- swcgeom/utils/renderer.py +16 -8
- swcgeom/utils/sdf.py +12 -23
- swcgeom/utils/solid_geometry.py +58 -2
- swcgeom/utils/transforms.py +164 -100
- swcgeom/utils/volumetric_object.py +29 -53
- {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/METADATA +5 -4
- swcgeom-0.19.0.dist-info/RECORD +67 -0
- {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/WHEEL +1 -1
- swcgeom-0.18.3.dist-info/RECORD +0 -67
- {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info/licenses}/LICENSE +0 -0
- {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/top_level.txt +0 -0
swcgeom/core/population.py
CHANGED
|
@@ -20,7 +20,7 @@ import warnings
|
|
|
20
20
|
from collections.abc import Callable, Iterable, Iterator
|
|
21
21
|
from concurrent.futures import ProcessPoolExecutor
|
|
22
22
|
from functools import reduce
|
|
23
|
-
from typing import Any,
|
|
23
|
+
from typing import Any, Protocol, TypeVar, cast, overload
|
|
24
24
|
|
|
25
25
|
import numpy as np
|
|
26
26
|
import numpy.typing as npt
|
|
@@ -39,10 +39,8 @@ T = TypeVar("T")
|
|
|
39
39
|
class Trees(Protocol):
|
|
40
40
|
"""Trees protocol support index and len."""
|
|
41
41
|
|
|
42
|
-
# fmt: off
|
|
43
42
|
def __getitem__(self, key: int, /) -> Tree: ...
|
|
44
43
|
def __len__(self) -> int: ...
|
|
45
|
-
# fmt: on
|
|
46
44
|
|
|
47
45
|
|
|
48
46
|
class LazyLoadingTrees:
|
|
@@ -54,11 +52,9 @@ class LazyLoadingTrees:
|
|
|
54
52
|
|
|
55
53
|
def __init__(self, swcs: Iterable[str], **kwargs) -> None:
|
|
56
54
|
"""
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
kwargs : dict[str, Any]
|
|
61
|
-
Forwarding to `Tree.from_swc`
|
|
55
|
+
Args:
|
|
56
|
+
swcs: List of str
|
|
57
|
+
kwargs: Forwarding to `Tree.from_swc`
|
|
62
58
|
"""
|
|
63
59
|
|
|
64
60
|
super().__init__()
|
|
@@ -130,43 +126,40 @@ class Population:
|
|
|
130
126
|
|
|
131
127
|
trees: Trees
|
|
132
128
|
|
|
133
|
-
# fmt: off
|
|
134
129
|
@overload
|
|
135
|
-
def __init__(
|
|
130
|
+
def __init__(
|
|
131
|
+
self, swcs: Iterable[str], lazy_loading: bool = ..., root: str = ..., **kwargs
|
|
132
|
+
) -> None: ...
|
|
136
133
|
@overload
|
|
137
134
|
def __init__(self, trees: Trees, /, *, root: str = "") -> None: ...
|
|
138
|
-
# fmt: on
|
|
139
|
-
|
|
140
135
|
def __init__(self, swcs, lazy_loading=True, root="", **kwargs) -> None:
|
|
141
136
|
super().__init__()
|
|
142
137
|
if len(swcs) > 0 and isinstance(swcs[0], str):
|
|
143
138
|
warnings.warn(
|
|
144
139
|
"`Population(swcs)` has been replaced by "
|
|
145
|
-
"`Population(LazyLoadingTrees(swcs))` since v0.8.0 "
|
|
146
|
-
"
|
|
147
|
-
"
|
|
140
|
+
"`Population(LazyLoadingTrees(swcs))` since v0.8.0 thus we can create "
|
|
141
|
+
"a population from a group of trees, and this will be removed in next "
|
|
142
|
+
"version",
|
|
148
143
|
DeprecationWarning,
|
|
149
144
|
)
|
|
150
145
|
|
|
151
|
-
trees = LazyLoadingTrees(swcs, **kwargs)
|
|
146
|
+
trees = LazyLoadingTrees(swcs, **kwargs)
|
|
152
147
|
if not lazy_loading:
|
|
153
148
|
for i in range(len(swcs)):
|
|
154
149
|
trees.load(i)
|
|
155
150
|
else:
|
|
156
151
|
trees = swcs
|
|
157
152
|
|
|
158
|
-
self.trees = trees
|
|
153
|
+
self.trees = trees
|
|
159
154
|
self.root = root
|
|
160
155
|
|
|
161
156
|
if len(swcs) == 0:
|
|
162
157
|
warnings.warn(f"no trees in population from '{root}'")
|
|
163
158
|
|
|
164
|
-
# fmt:off
|
|
165
159
|
@overload
|
|
166
160
|
def __getitem__(self, key: slice) -> Trees: ...
|
|
167
161
|
@overload
|
|
168
162
|
def __getitem__(self, key: int) -> Tree: ...
|
|
169
|
-
# fmt:on
|
|
170
163
|
def __getitem__(self, key: int | slice):
|
|
171
164
|
if isinstance(key, slice):
|
|
172
165
|
trees = NestTrees(self.trees, range(*key.indices(len(self))))
|
|
@@ -190,15 +183,14 @@ class Population:
|
|
|
190
183
|
self,
|
|
191
184
|
fn: Callable[[Tree], T],
|
|
192
185
|
*,
|
|
193
|
-
max_worker:
|
|
186
|
+
max_worker: int | None = None,
|
|
194
187
|
verbose: bool = False,
|
|
195
188
|
) -> Iterator[T]:
|
|
196
189
|
"""Map a function to all trees in the population.
|
|
197
190
|
|
|
198
|
-
This is a straightforward interface for parallelizing
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
`concurrent.futures` directly.
|
|
191
|
+
This is a straightforward interface for parallelizing computations. The
|
|
192
|
+
parameters are intentionally kept simple and user-friendly. For more advanced
|
|
193
|
+
control, consider using `concurrent.futures` directly.
|
|
202
194
|
"""
|
|
203
195
|
|
|
204
196
|
trees = (t for t in self.trees)
|
|
@@ -226,11 +218,11 @@ class Population:
|
|
|
226
218
|
cls,
|
|
227
219
|
root: str,
|
|
228
220
|
ext: str = ".eswc",
|
|
229
|
-
extra_cols:
|
|
221
|
+
extra_cols: Iterable[str] | None = None,
|
|
230
222
|
**kwargs,
|
|
231
223
|
) -> Self:
|
|
232
224
|
extra_cols = list(extra_cols) if extra_cols is not None else []
|
|
233
|
-
extra_cols.extend(k for k,
|
|
225
|
+
extra_cols.extend(k for k, _ in eswc_cols)
|
|
234
226
|
return cls.from_swc(root, ext, extra_cols=extra_cols, **kwargs)
|
|
235
227
|
|
|
236
228
|
@staticmethod
|
|
@@ -253,23 +245,21 @@ class Populations:
|
|
|
253
245
|
labels: list[str]
|
|
254
246
|
|
|
255
247
|
def __init__(
|
|
256
|
-
self, populations: Iterable[Population], labels:
|
|
248
|
+
self, populations: Iterable[Population], labels: Iterable[str] | None = None
|
|
257
249
|
) -> None:
|
|
258
250
|
self.len = min(len(p) for p in populations)
|
|
259
251
|
self.populations = list(populations)
|
|
260
252
|
|
|
261
253
|
labels = list(labels) if labels is not None else ["" for i in populations]
|
|
262
|
-
assert len(labels) == len(
|
|
263
|
-
self.populations
|
|
264
|
-
)
|
|
254
|
+
assert len(labels) == len(self.populations), (
|
|
255
|
+
f"got {len(self.populations)} populations, but has {len(labels)} labels"
|
|
256
|
+
)
|
|
265
257
|
self.labels = labels
|
|
266
258
|
|
|
267
|
-
# fmt:off
|
|
268
259
|
@overload
|
|
269
260
|
def __getitem__(self, key: slice) -> list[list[Tree]]: ...
|
|
270
261
|
@overload
|
|
271
262
|
def __getitem__(self, key: int) -> list[Tree]: ...
|
|
272
|
-
# fmt:on
|
|
273
263
|
def __getitem__(self, key):
|
|
274
264
|
return [p[key] for p in self.populations]
|
|
275
265
|
|
|
@@ -299,24 +289,18 @@ class Populations:
|
|
|
299
289
|
ext: str = ".swc",
|
|
300
290
|
intersect: bool = True,
|
|
301
291
|
check_same: bool = False,
|
|
302
|
-
labels:
|
|
292
|
+
labels: Iterable[str] | None = None,
|
|
303
293
|
**kwargs,
|
|
304
294
|
) -> Self:
|
|
305
295
|
"""Get population from dirs.
|
|
306
296
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
Check if the directories contains the same swc.
|
|
314
|
-
labels : List of str, optional
|
|
315
|
-
Label of populations.
|
|
316
|
-
**kwargs : Any
|
|
317
|
-
Forwarding to `Population`.
|
|
297
|
+
Args:
|
|
298
|
+
roots: List of str
|
|
299
|
+
intersect: Take the intersection of these populations.
|
|
300
|
+
check_same: Check if the directories contains the same swc.
|
|
301
|
+
labels: Label of populations.
|
|
302
|
+
**kwargs: Forwarding to `Population`.
|
|
318
303
|
"""
|
|
319
|
-
|
|
320
304
|
fs = [Population.find_swcs(d, ext=ext, relpath=True) for d in roots]
|
|
321
305
|
if intersect:
|
|
322
306
|
inter = list(reduce(lambda a, b: set(a).intersection(set(b)), fs))
|
|
@@ -339,13 +323,13 @@ class Populations:
|
|
|
339
323
|
def from_eswc(
|
|
340
324
|
cls,
|
|
341
325
|
roots: Iterable[str],
|
|
342
|
-
extra_cols:
|
|
326
|
+
extra_cols: Iterable[str] | None = None,
|
|
343
327
|
*,
|
|
344
328
|
ext: str = ".eswc",
|
|
345
329
|
**kwargs,
|
|
346
330
|
) -> Self:
|
|
347
331
|
extra_cols = list(extra_cols) if extra_cols is not None else []
|
|
348
|
-
extra_cols.extend(k for k,
|
|
332
|
+
extra_cols.extend(k for k, _ in eswc_cols)
|
|
349
333
|
return cls.from_swc(roots, extra_cols=extra_cols, ext=ext, **kwargs)
|
|
350
334
|
|
|
351
335
|
|
|
@@ -360,11 +344,8 @@ def _get_idx(key: int, length: int) -> int:
|
|
|
360
344
|
|
|
361
345
|
|
|
362
346
|
# experimental
|
|
363
|
-
def filter_population(
|
|
364
|
-
pop: Population, predicate: Callable[[Tree], bool]
|
|
365
|
-
) -> "Population":
|
|
347
|
+
def filter_population(pop: Population, predicate: Callable[[Tree], bool]) -> Population:
|
|
366
348
|
"""Filter trees in the population."""
|
|
367
|
-
|
|
368
349
|
# TODO: how to avoid load trees
|
|
369
350
|
idx = [i for i, t in enumerate(pop) if predicate(t)]
|
|
370
351
|
return Population(NestTrees(pop.trees, idx), root=pop.root)
|
swcgeom/core/swc.py
CHANGED
|
@@ -19,7 +19,7 @@ import warnings
|
|
|
19
19
|
from abc import ABC, abstractmethod
|
|
20
20
|
from collections.abc import Iterable
|
|
21
21
|
from copy import deepcopy
|
|
22
|
-
from typing import Any,
|
|
22
|
+
from typing import Any, TypeVar, overload
|
|
23
23
|
|
|
24
24
|
import numpy as np
|
|
25
25
|
import numpy.typing as npt
|
|
@@ -145,17 +145,28 @@ class SWCLike(ABC):
|
|
|
145
145
|
"""Get the number of edges."""
|
|
146
146
|
return self.number_of_nodes() - 1 # for tree structure: n = e + 1
|
|
147
147
|
|
|
148
|
-
# fmt: off
|
|
149
148
|
@overload
|
|
150
|
-
def to_swc(
|
|
149
|
+
def to_swc(
|
|
150
|
+
self,
|
|
151
|
+
fname: str,
|
|
152
|
+
*,
|
|
153
|
+
extra_cols: list[str] | None = ...,
|
|
154
|
+
source: bool | str = ...,
|
|
155
|
+
id_offset: int = ...,
|
|
156
|
+
) -> None: ...
|
|
151
157
|
@overload
|
|
152
|
-
def to_swc(self, *, extra_cols: list[str] | None = ..., source: bool | str = ..., id_offset: int = ...) -> str: ...
|
|
153
|
-
# fmt: on
|
|
154
158
|
def to_swc(
|
|
155
159
|
self,
|
|
156
|
-
fname: Optional[str] = None,
|
|
157
160
|
*,
|
|
158
|
-
extra_cols:
|
|
161
|
+
extra_cols: list[str] | None = ...,
|
|
162
|
+
source: bool | str = ...,
|
|
163
|
+
id_offset: int = ...,
|
|
164
|
+
) -> str: ...
|
|
165
|
+
def to_swc(
|
|
166
|
+
self,
|
|
167
|
+
fname: str | None = None,
|
|
168
|
+
*,
|
|
169
|
+
extra_cols: list[str] | None = None,
|
|
159
170
|
source: bool | str = True,
|
|
160
171
|
comments: bool = True,
|
|
161
172
|
id_offset: int = 1,
|
|
@@ -183,17 +194,15 @@ class SWCLike(ABC):
|
|
|
183
194
|
|
|
184
195
|
return None
|
|
185
196
|
|
|
186
|
-
# fmt: off
|
|
187
197
|
@overload
|
|
188
198
|
def to_eswc(self, fname: str, **kwargs) -> None: ...
|
|
189
199
|
@overload
|
|
190
|
-
def to_eswc(self, **kwargs) -> str: ...
|
|
191
|
-
# fmt: on
|
|
200
|
+
def to_eswc(self, fname: None = ..., **kwargs) -> str: ...
|
|
192
201
|
def to_eswc(
|
|
193
202
|
self,
|
|
194
|
-
fname:
|
|
195
|
-
swc_path:
|
|
196
|
-
extra_cols:
|
|
203
|
+
fname: str | None = None,
|
|
204
|
+
swc_path: str | None = None,
|
|
205
|
+
extra_cols: list[str] | None = None,
|
|
197
206
|
**kwargs,
|
|
198
207
|
) -> str | None:
|
|
199
208
|
if swc_path is None:
|
|
@@ -205,7 +214,7 @@ class SWCLike(ABC):
|
|
|
205
214
|
fname = swc_path
|
|
206
215
|
|
|
207
216
|
extra_cols = extra_cols or []
|
|
208
|
-
extra_cols.extend(k for k,
|
|
217
|
+
extra_cols.extend(k for k, _ in eswc_cols)
|
|
209
218
|
return self.to_swc(fname, extra_cols=extra_cols, **kwargs) # type: ignore
|
|
210
219
|
|
|
211
220
|
|
|
@@ -221,8 +230,8 @@ class DictSWC(SWCLike):
|
|
|
221
230
|
self,
|
|
222
231
|
*,
|
|
223
232
|
source: str = "",
|
|
224
|
-
comments:
|
|
225
|
-
names:
|
|
233
|
+
comments: Iterable[str] | None = None,
|
|
234
|
+
names: SWCNames | None = None,
|
|
226
235
|
**kwargs: npt.NDArray,
|
|
227
236
|
):
|
|
228
237
|
super().__init__()
|
|
@@ -15,12 +15,10 @@
|
|
|
15
15
|
|
|
16
16
|
"""SWC format utils.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
If you use the method here, please review the code more frequently, we
|
|
23
|
-
will try to flag all breaking changes but NO promises.
|
|
18
|
+
NOTE: This module provides a bunch of methods to manipulating swc files, they are
|
|
19
|
+
always trivial and unstabled, so we are NOT export it by default. If you use the method
|
|
20
|
+
here, please review the code more frequently, we will try to flag all breaking changes
|
|
21
|
+
but NO promises.
|
|
24
22
|
"""
|
|
25
23
|
|
|
26
24
|
from swcgeom.core.swc_utils.assembler import * # noqa: F403
|
|
@@ -15,10 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
"""Assemble lines to swc.
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
-----
|
|
20
|
-
This module is deprecated, please use `~.transforms.LinesToTree`
|
|
21
|
-
instead.
|
|
18
|
+
NOTE: This module is deprecated, please use `~.transforms.LinesToTree` instead.
|
|
22
19
|
"""
|
|
23
20
|
|
|
24
21
|
__all__ = ["assemble_lines", "try_assemble_lines"]
|
|
@@ -30,11 +27,9 @@ def assemble_lines(*args, **kwargs):
|
|
|
30
27
|
.. deprecated:: 0.15.0
|
|
31
28
|
Use :meth:`~.transforms.LinesToTree` instead.
|
|
32
29
|
"""
|
|
33
|
-
|
|
34
30
|
raise DeprecationWarning(
|
|
35
|
-
"`assemble_lines` has been replaced by "
|
|
36
|
-
"
|
|
37
|
-
"with other tansformations.",
|
|
31
|
+
"`assemble_lines` has been replaced by `~.transforms.LinesToTree` because it "
|
|
32
|
+
"can be easy assemble with other tansformations.",
|
|
38
33
|
)
|
|
39
34
|
|
|
40
35
|
|
|
@@ -44,9 +39,7 @@ def try_assemble_lines(*args, **kwargs):
|
|
|
44
39
|
.. deprecated:: 0.15.0
|
|
45
40
|
Use :meth:`~.transforms.LinesToTree` instead.
|
|
46
41
|
"""
|
|
47
|
-
|
|
48
42
|
raise DeprecationWarning(
|
|
49
|
-
"`try_assemble_lines` has been replaced by "
|
|
50
|
-
"
|
|
51
|
-
"with other tansformations.",
|
|
43
|
+
"`try_assemble_lines` has been replaced by `~.transforms.LinesToTree` because "
|
|
44
|
+
"it can be easy assemble with other tansformations.",
|
|
52
45
|
)
|
swcgeom/core/swc_utils/base.py
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"""Base SWC format utils."""
|
|
17
17
|
|
|
18
18
|
from collections.abc import Callable
|
|
19
|
-
from typing import Literal, NamedTuple,
|
|
19
|
+
from typing import Literal, NamedTuple, TypeVar, overload
|
|
20
20
|
|
|
21
21
|
import numpy as np
|
|
22
22
|
import numpy.typing as npt
|
|
@@ -34,7 +34,8 @@ __all__ = [
|
|
|
34
34
|
"traverse",
|
|
35
35
|
]
|
|
36
36
|
|
|
37
|
-
T
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
K = TypeVar("K")
|
|
38
39
|
Topology = tuple[npt.NDArray[np.int32], npt.NDArray[np.int32]] # (id, pid)
|
|
39
40
|
|
|
40
41
|
|
|
@@ -56,17 +57,15 @@ class SWCNames(NamedTuple):
|
|
|
56
57
|
swc_names = SWCNames()
|
|
57
58
|
|
|
58
59
|
|
|
59
|
-
def get_names(names:
|
|
60
|
+
def get_names(names: SWCNames | None = None) -> SWCNames:
|
|
60
61
|
return names or swc_names
|
|
61
62
|
|
|
62
63
|
|
|
63
64
|
class SWCTypes(NamedTuple):
|
|
64
65
|
"""SWC format types.
|
|
65
66
|
|
|
66
|
-
See Also
|
|
67
|
-
|
|
68
|
-
NeuroMoprho.org - What is SWC format?
|
|
69
|
-
https://neuromorpho.org/myfaq.jsp
|
|
67
|
+
See Also:
|
|
68
|
+
NeuroMoprho.org - What is SWC format?: https://neuromorpho.org/myfaq.jsp
|
|
70
69
|
"""
|
|
71
70
|
|
|
72
71
|
undefined: int = 0
|
|
@@ -82,17 +81,17 @@ class SWCTypes(NamedTuple):
|
|
|
82
81
|
swc_types = SWCTypes()
|
|
83
82
|
|
|
84
83
|
|
|
85
|
-
def get_types(types:
|
|
84
|
+
def get_types(types: SWCTypes | None = None) -> SWCTypes:
|
|
86
85
|
return types or swc_types
|
|
87
86
|
|
|
88
87
|
|
|
89
|
-
def get_topology(df: pd.DataFrame, *, names:
|
|
88
|
+
def get_topology(df: pd.DataFrame, *, names: SWCNames | None = None) -> Topology:
|
|
90
89
|
names = get_names(names)
|
|
91
90
|
return (df[names.id].to_numpy(), df[names.pid].to_numpy())
|
|
92
91
|
|
|
93
92
|
|
|
94
93
|
def get_dsu(
|
|
95
|
-
df: pd.DataFrame, *, names:
|
|
94
|
+
df: pd.DataFrame, *, names: SWCNames | None = None
|
|
96
95
|
) -> npt.NDArray[np.int32]:
|
|
97
96
|
"""Get disjoint set union."""
|
|
98
97
|
names = get_names(names)
|
|
@@ -116,36 +115,46 @@ def get_dsu(
|
|
|
116
115
|
return dsu
|
|
117
116
|
|
|
118
117
|
|
|
119
|
-
# fmt: off
|
|
120
118
|
@overload
|
|
121
|
-
def traverse(
|
|
119
|
+
def traverse(
|
|
120
|
+
topology: Topology,
|
|
121
|
+
*,
|
|
122
|
+
enter: Callable[[int, T | None], T],
|
|
123
|
+
root: int | np.integer = ...,
|
|
124
|
+
mode: Literal["dfs"] = ...,
|
|
125
|
+
) -> None: ...
|
|
122
126
|
@overload
|
|
123
|
-
def traverse(
|
|
127
|
+
def traverse(
|
|
128
|
+
topology: Topology,
|
|
129
|
+
*,
|
|
130
|
+
leave: Callable[[int, list[K]], K],
|
|
131
|
+
root: int | np.integer = ...,
|
|
132
|
+
mode: Literal["dfs"] = ...,
|
|
133
|
+
) -> K: ...
|
|
124
134
|
@overload
|
|
125
135
|
def traverse(
|
|
126
|
-
topology: Topology,
|
|
127
|
-
|
|
136
|
+
topology: Topology,
|
|
137
|
+
*,
|
|
138
|
+
enter: Callable[[int, T | None], T],
|
|
139
|
+
leave: Callable[[int, list[K]], K],
|
|
140
|
+
root: int | np.integer = ...,
|
|
141
|
+
mode: Literal["dfs"] = ...,
|
|
128
142
|
) -> K: ...
|
|
129
|
-
# fmt: on
|
|
130
143
|
def traverse(topology: Topology, *, mode="dfs", **kwargs):
|
|
131
144
|
"""Traverse nodes.
|
|
132
145
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
root : int, default to `0`
|
|
145
|
-
Start from the root node of the subtree
|
|
146
|
-
mode : `dfs`, default to `dfs`
|
|
146
|
+
Args:
|
|
147
|
+
enter: (id: int, parent: T | None) => T, optional
|
|
148
|
+
The callback when entering node, which accepts two parameters, the current node
|
|
149
|
+
id and the return value of it parent node. In particular, the root node
|
|
150
|
+
receives an `None`.
|
|
151
|
+
leave: (id: int, children: list[T]) => T, optional
|
|
152
|
+
The callback when leaving node. When leaving a node, subtree has already been
|
|
153
|
+
traversed. Callback accepts two parameters, the current node id and list of the
|
|
154
|
+
return value of children, In particular, the leaf node receives an empty list.
|
|
155
|
+
root: Start from the root node of the subtree
|
|
156
|
+
mode: The traverse mode, only support "dfs" now.
|
|
147
157
|
"""
|
|
148
|
-
|
|
149
158
|
match mode:
|
|
150
159
|
case "dfs":
|
|
151
160
|
return _traverse_dfs(topology, **kwargs)
|
|
@@ -16,7 +16,6 @@
|
|
|
16
16
|
"""Check common"""
|
|
17
17
|
|
|
18
18
|
from collections import defaultdict
|
|
19
|
-
from typing import Optional
|
|
20
19
|
|
|
21
20
|
import numpy as np
|
|
22
21
|
import pandas as pd
|
|
@@ -36,14 +35,13 @@ __all__ = [
|
|
|
36
35
|
]
|
|
37
36
|
|
|
38
37
|
|
|
39
|
-
def is_single_root(df: pd.DataFrame, *, names:
|
|
38
|
+
def is_single_root(df: pd.DataFrame, *, names: SWCNames | None = None) -> bool:
|
|
40
39
|
"""Check is it only one root."""
|
|
41
40
|
return len(np.unique(get_dsu(df, names=names))) == 1
|
|
42
41
|
|
|
43
42
|
|
|
44
43
|
def is_bifurcate(topology: Topology, *, exclude_root: bool = True) -> bool:
|
|
45
44
|
"""Check is it a bifurcate topology."""
|
|
46
|
-
|
|
47
45
|
children = defaultdict(list)
|
|
48
46
|
for idx, pid in zip(*topology):
|
|
49
47
|
children[pid].append(idx)
|
|
@@ -59,8 +57,7 @@ def is_bifurcate(topology: Topology, *, exclude_root: bool = True) -> bool:
|
|
|
59
57
|
def is_sorted(topology: Topology) -> bool:
|
|
60
58
|
"""Check is it sorted.
|
|
61
59
|
|
|
62
|
-
In a sorted topology, parent samples should appear before any child
|
|
63
|
-
samples.
|
|
60
|
+
In a sorted topology, parent samples should appear before any child samples.
|
|
64
61
|
"""
|
|
65
62
|
flag = True
|
|
66
63
|
|
|
@@ -103,20 +100,18 @@ def check_single_root(*args, **kwargs) -> bool:
|
|
|
103
100
|
.. deprecated:: 0.5.0
|
|
104
101
|
Use :meth:`is_single_root` instead.
|
|
105
102
|
"""
|
|
106
|
-
|
|
107
103
|
return is_single_root(*args, **kwargs)
|
|
108
104
|
|
|
109
105
|
|
|
110
106
|
@deprecated("Use `is_bifurcate` instead")
|
|
111
107
|
def is_binary_tree(
|
|
112
|
-
df: pd.DataFrame, exclude_root: bool = True, *, names:
|
|
108
|
+
df: pd.DataFrame, exclude_root: bool = True, *, names: SWCNames | None = None
|
|
113
109
|
) -> bool:
|
|
114
110
|
"""Check is it a binary tree.
|
|
115
111
|
|
|
116
112
|
.. deprecated:: 0.8.0
|
|
117
113
|
Use :meth:`is_bifurcate` instead.
|
|
118
114
|
"""
|
|
119
|
-
|
|
120
115
|
names = get_names(names)
|
|
121
116
|
topo = (df[names.id].to_numpy(), df[names.pid].to_numpy())
|
|
122
117
|
return is_bifurcate(topo, exclude_root=exclude_root)
|
swcgeom/core/swc_utils/io.py
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
import re
|
|
19
19
|
import warnings
|
|
20
20
|
from collections.abc import Callable, Iterable
|
|
21
|
-
from typing import Literal
|
|
21
|
+
from typing import Literal
|
|
22
22
|
|
|
23
23
|
import numpy as np
|
|
24
24
|
import numpy.typing as npt
|
|
@@ -39,41 +39,33 @@ __all__ = ["read_swc", "to_swc"]
|
|
|
39
39
|
|
|
40
40
|
def read_swc(
|
|
41
41
|
swc_file: PathOrIO,
|
|
42
|
-
extra_cols:
|
|
42
|
+
extra_cols: Iterable[str] | None = None,
|
|
43
43
|
fix_roots: Literal["somas", "nearest", False] = False,
|
|
44
44
|
sort_nodes: bool = False,
|
|
45
45
|
reset_index: bool = True,
|
|
46
46
|
*,
|
|
47
47
|
encoding: Literal["detect"] | str = "utf-8",
|
|
48
|
-
names:
|
|
48
|
+
names: SWCNames | None = None,
|
|
49
49
|
) -> tuple[pd.DataFrame, list[str]]:
|
|
50
50
|
"""Read swc file.
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
`detect`, we will try to detect the character encoding.
|
|
69
|
-
names : SWCNames, optional
|
|
70
|
-
|
|
71
|
-
Returns
|
|
72
|
-
-------
|
|
73
|
-
df : ~pandas.DataFrame
|
|
74
|
-
comments : List of string
|
|
52
|
+
NOTE: the id should be consecutively incremented.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
extra_cols: Read more cols in swc file.
|
|
56
|
+
fix_roots: Fix multiple roots.
|
|
57
|
+
sort_nodes: Sort the indices of neuron tree.
|
|
58
|
+
After sorting the nodes, the index for each parent are always less than
|
|
59
|
+
that of its children.
|
|
60
|
+
reset_index: Reset node index to start with zero.
|
|
61
|
+
DO NOT set to false if you are not sure what will happened.
|
|
62
|
+
encoding: The name of the encoding used to decode the file.
|
|
63
|
+
If is `detect`, we will try to detect the character encoding.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
df: ~pandas.DataFrame
|
|
67
|
+
comments: List of string
|
|
75
68
|
"""
|
|
76
|
-
|
|
77
69
|
names = get_names(names)
|
|
78
70
|
df, comments = parse_swc(
|
|
79
71
|
swc_file, names=names, extra_cols=extra_cols, encoding=encoding
|
|
@@ -110,10 +102,10 @@ def read_swc(
|
|
|
110
102
|
def to_swc(
|
|
111
103
|
get_ndata: Callable[[str], npt.NDArray],
|
|
112
104
|
*,
|
|
113
|
-
extra_cols:
|
|
105
|
+
extra_cols: Iterable[str] | None = None,
|
|
114
106
|
id_offset: int = 1,
|
|
115
|
-
comments:
|
|
116
|
-
names:
|
|
107
|
+
comments: Iterable[str] | None = None,
|
|
108
|
+
names: SWCNames | None = None,
|
|
117
109
|
) -> Iterable[str]:
|
|
118
110
|
"""Convert to swc format."""
|
|
119
111
|
|
|
@@ -156,21 +148,14 @@ def parse_swc(
|
|
|
156
148
|
) -> tuple[pd.DataFrame, list[str]]:
|
|
157
149
|
"""Parse swc file.
|
|
158
150
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
names : SWCNames
|
|
163
|
-
extra_cols : List of str, optional
|
|
164
|
-
encoding : str | 'detect', default `utf-8`
|
|
165
|
-
The name of the encoding used to decode the file. If is
|
|
166
|
-
`detect`, we will try to detect the character encoding.
|
|
167
|
-
|
|
168
|
-
Returns
|
|
169
|
-
-------
|
|
170
|
-
df : ~pandas.DataFrame
|
|
171
|
-
comments : List of string
|
|
172
|
-
"""
|
|
151
|
+
Args:
|
|
152
|
+
encoding: The name of the encoding used to decode the file.
|
|
153
|
+
If is `detect`, we will try to detect the character encoding.
|
|
173
154
|
|
|
155
|
+
Returns:
|
|
156
|
+
df: ~pandas.DataFrame
|
|
157
|
+
comments: List of string
|
|
158
|
+
"""
|
|
174
159
|
# pylint: disable=too-many-locals
|
|
175
160
|
extras = list(extra_cols) if extra_cols else []
|
|
176
161
|
|
|
@@ -208,7 +193,7 @@ def parse_swc(
|
|
|
208
193
|
if (match := re_swc.search(line)) is not None:
|
|
209
194
|
if flag and match.group(last_group):
|
|
210
195
|
warnings.warn(
|
|
211
|
-
f"some fields are ignored in row {i+1} of `{fname}`"
|
|
196
|
+
f"some fields are ignored in row {i + 1} of `{fname}`"
|
|
212
197
|
)
|
|
213
198
|
flag = False
|
|
214
199
|
|
|
@@ -219,10 +204,10 @@ def parse_swc(
|
|
|
219
204
|
if not comment.startswith(ignored_comment):
|
|
220
205
|
comments.append(comment)
|
|
221
206
|
elif not line.isspace():
|
|
222
|
-
raise ValueError(f"invalid row {i+1} in `{fname}`")
|
|
207
|
+
raise ValueError(f"invalid row {i + 1} in `{fname}`")
|
|
223
208
|
except UnicodeDecodeError as e:
|
|
224
209
|
raise ValueError(
|
|
225
|
-
|
|
210
|
+
"decode failed, try to enable auto detect `encoding='detect'`"
|
|
226
211
|
) from e
|
|
227
212
|
|
|
228
213
|
df = pd.DataFrame.from_dict(dict(zip(keys, vals)))
|