swcgeom 0.19.4__cp313-cp313-win_amd64.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/__init__.py +21 -0
- swcgeom/analysis/__init__.py +13 -0
- swcgeom/analysis/feature_extractor.py +454 -0
- swcgeom/analysis/features.py +218 -0
- swcgeom/analysis/lmeasure.py +750 -0
- swcgeom/analysis/sholl.py +201 -0
- swcgeom/analysis/trunk.py +183 -0
- swcgeom/analysis/visualization.py +191 -0
- swcgeom/analysis/visualization3d.py +81 -0
- swcgeom/analysis/volume.py +143 -0
- swcgeom/core/__init__.py +19 -0
- swcgeom/core/branch.py +129 -0
- swcgeom/core/branch_tree.py +65 -0
- swcgeom/core/compartment.py +107 -0
- swcgeom/core/node.py +130 -0
- swcgeom/core/path.py +155 -0
- swcgeom/core/population.py +341 -0
- swcgeom/core/swc.py +247 -0
- swcgeom/core/swc_utils/__init__.py +19 -0
- swcgeom/core/swc_utils/assembler.py +35 -0
- swcgeom/core/swc_utils/base.py +180 -0
- swcgeom/core/swc_utils/checker.py +107 -0
- swcgeom/core/swc_utils/io.py +204 -0
- swcgeom/core/swc_utils/normalizer.py +163 -0
- swcgeom/core/swc_utils/subtree.py +70 -0
- swcgeom/core/tree.py +384 -0
- swcgeom/core/tree_utils.py +277 -0
- swcgeom/core/tree_utils_impl.py +58 -0
- swcgeom/images/__init__.py +9 -0
- swcgeom/images/augmentation.py +149 -0
- swcgeom/images/contrast.py +87 -0
- swcgeom/images/folder.py +217 -0
- swcgeom/images/io.py +578 -0
- swcgeom/images/loaders/__init__.py +8 -0
- swcgeom/images/loaders/pbd.cp313-win_amd64.pyd +0 -0
- swcgeom/images/loaders/pbd.pyx +523 -0
- swcgeom/images/loaders/raw.cp313-win_amd64.pyd +0 -0
- swcgeom/images/loaders/raw.pyx +183 -0
- swcgeom/transforms/__init__.py +20 -0
- swcgeom/transforms/base.py +136 -0
- swcgeom/transforms/branch.py +223 -0
- swcgeom/transforms/branch_tree.py +74 -0
- swcgeom/transforms/geometry.py +270 -0
- swcgeom/transforms/image_preprocess.py +107 -0
- swcgeom/transforms/image_stack.py +219 -0
- swcgeom/transforms/images.py +206 -0
- swcgeom/transforms/mst.py +183 -0
- swcgeom/transforms/neurolucida_asc.py +498 -0
- swcgeom/transforms/path.py +56 -0
- swcgeom/transforms/population.py +36 -0
- swcgeom/transforms/tree.py +265 -0
- swcgeom/transforms/tree_assembler.py +161 -0
- swcgeom/utils/__init__.py +18 -0
- swcgeom/utils/debug.py +23 -0
- swcgeom/utils/download.py +119 -0
- swcgeom/utils/dsu.py +58 -0
- swcgeom/utils/ellipse.py +131 -0
- swcgeom/utils/file.py +90 -0
- swcgeom/utils/neuromorpho.py +581 -0
- swcgeom/utils/numpy_helper.py +70 -0
- swcgeom/utils/plotter_2d.py +134 -0
- swcgeom/utils/plotter_3d.py +35 -0
- swcgeom/utils/renderer.py +145 -0
- swcgeom/utils/sdf.py +324 -0
- swcgeom/utils/solid_geometry.py +154 -0
- swcgeom/utils/transforms.py +367 -0
- swcgeom/utils/volumetric_object.py +483 -0
- swcgeom-0.19.4.dist-info/METADATA +86 -0
- swcgeom-0.19.4.dist-info/RECORD +72 -0
- swcgeom-0.19.4.dist-info/WHEEL +5 -0
- swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
- swcgeom-0.19.4.dist-info/top_level.txt +1 -0
swcgeom/core/swc.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""SWC format."""
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
from typing import Any, TypeVar, overload
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import numpy.typing as npt
|
|
16
|
+
import scipy.sparse as sp
|
|
17
|
+
from typing_extensions import Self
|
|
18
|
+
|
|
19
|
+
from swcgeom.core.swc_utils import (
|
|
20
|
+
SWCNames,
|
|
21
|
+
SWCTypes,
|
|
22
|
+
get_names,
|
|
23
|
+
get_types,
|
|
24
|
+
read_swc,
|
|
25
|
+
to_swc,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"swc_cols",
|
|
30
|
+
"eswc_cols",
|
|
31
|
+
"SWCLike",
|
|
32
|
+
"DictSWC",
|
|
33
|
+
"SWCTypeVar",
|
|
34
|
+
# TODO: `read_swc` has been deprecated and will be removed in next
|
|
35
|
+
# version, import from `swcgeom.core.swc_utils` instead
|
|
36
|
+
"read_swc",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
swc_names_default = get_names()
|
|
41
|
+
swc_cols: list[tuple[str, npt.DTypeLike]] = [
|
|
42
|
+
(swc_names_default.id, np.int32),
|
|
43
|
+
(swc_names_default.type, np.int32),
|
|
44
|
+
(swc_names_default.x, np.float32),
|
|
45
|
+
(swc_names_default.y, np.float32),
|
|
46
|
+
(swc_names_default.z, np.float32),
|
|
47
|
+
(swc_names_default.r, np.float32),
|
|
48
|
+
(swc_names_default.pid, np.int32),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
eswc_cols: list[tuple[str, npt.DTypeLike]] = [
|
|
52
|
+
("level", np.int32),
|
|
53
|
+
("mode", np.int32),
|
|
54
|
+
("timestamp", np.int32),
|
|
55
|
+
("teraflyindex", np.int32),
|
|
56
|
+
("feature_value", np.int32),
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SWCLike(ABC):
|
|
61
|
+
"""ABC of SWC."""
|
|
62
|
+
|
|
63
|
+
source: str = ""
|
|
64
|
+
comments: list[str] = []
|
|
65
|
+
names: SWCNames
|
|
66
|
+
types: SWCTypes
|
|
67
|
+
|
|
68
|
+
def __init__(self) -> None:
|
|
69
|
+
super().__init__()
|
|
70
|
+
self.types = get_types()
|
|
71
|
+
|
|
72
|
+
def __len__(self) -> int:
|
|
73
|
+
return self.number_of_nodes()
|
|
74
|
+
|
|
75
|
+
def id(self) -> npt.NDArray[np.int32]: # pylint: disable=invalid-name
|
|
76
|
+
"""Get the ids of shape (n_sample,)."""
|
|
77
|
+
return self.get_ndata(self.names.id)
|
|
78
|
+
|
|
79
|
+
def type(self) -> npt.NDArray[np.int32]:
|
|
80
|
+
"""Get the types of shape (n_sample,)."""
|
|
81
|
+
return self.get_ndata(self.names.type)
|
|
82
|
+
|
|
83
|
+
def x(self) -> npt.NDArray[np.float32]:
|
|
84
|
+
"""Get the x coordinates of shape (n_sample,)."""
|
|
85
|
+
return self.get_ndata(self.names.x)
|
|
86
|
+
|
|
87
|
+
def y(self) -> npt.NDArray[np.float32]:
|
|
88
|
+
"""Get the y coordinates of shape (n_sample,)."""
|
|
89
|
+
return self.get_ndata(self.names.y)
|
|
90
|
+
|
|
91
|
+
def z(self) -> npt.NDArray[np.float32]:
|
|
92
|
+
"""Get the z coordinates of shape (n_sample,)."""
|
|
93
|
+
return self.get_ndata(self.names.z)
|
|
94
|
+
|
|
95
|
+
def r(self) -> npt.NDArray[np.float32]:
|
|
96
|
+
"""Get the radius of shape (n_sample,)."""
|
|
97
|
+
return self.get_ndata(self.names.r)
|
|
98
|
+
|
|
99
|
+
def pid(self) -> npt.NDArray[np.int32]:
|
|
100
|
+
"""Get the ids of parent of shape (n_sample,)."""
|
|
101
|
+
return self.get_ndata(self.names.pid)
|
|
102
|
+
|
|
103
|
+
def xyz(self) -> npt.NDArray[np.float32]:
|
|
104
|
+
"""Get the coordinates of shape(n_sample, 3)."""
|
|
105
|
+
return np.stack([self.x(), self.y(), self.z()], axis=1)
|
|
106
|
+
|
|
107
|
+
def xyzw(self) -> npt.NDArray[np.float32]:
|
|
108
|
+
"""Get the homogeneous coordinates of shape(n_sample, 4)."""
|
|
109
|
+
w = np.ones_like(self.x())
|
|
110
|
+
return np.stack([self.x(), self.y(), self.z(), w], axis=1)
|
|
111
|
+
|
|
112
|
+
def xyzr(self) -> npt.NDArray[np.float32]:
|
|
113
|
+
"""Get the coordinates and radius array of shape(n_sample, 4)."""
|
|
114
|
+
return np.stack([self.x(), self.y(), self.z(), self.r()], axis=1)
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def keys(self) -> Iterable[str]:
|
|
118
|
+
raise NotImplementedError()
|
|
119
|
+
|
|
120
|
+
@abstractmethod
|
|
121
|
+
def get_ndata(self, key: str) -> npt.NDArray[Any]:
|
|
122
|
+
raise NotImplementedError()
|
|
123
|
+
|
|
124
|
+
def get_adjacency_matrix(self) -> sp.coo_matrix:
|
|
125
|
+
n_nodes = len(self)
|
|
126
|
+
row, col = self.pid()[1:], self.id()[1:] # ignore root
|
|
127
|
+
triad = (np.ones_like(row), (row, col))
|
|
128
|
+
return sp.coo_matrix(triad, shape=(n_nodes, n_nodes), dtype=np.int32)
|
|
129
|
+
|
|
130
|
+
def number_of_nodes(self) -> int:
|
|
131
|
+
"""Get the number of nodes."""
|
|
132
|
+
return self.id().shape[0]
|
|
133
|
+
|
|
134
|
+
def number_of_edges(self) -> int:
|
|
135
|
+
"""Get the number of edges."""
|
|
136
|
+
return self.number_of_nodes() - 1 # for tree structure: n = e + 1
|
|
137
|
+
|
|
138
|
+
@overload
|
|
139
|
+
def to_swc(
|
|
140
|
+
self,
|
|
141
|
+
fname: str,
|
|
142
|
+
*,
|
|
143
|
+
extra_cols: list[str] | None = ...,
|
|
144
|
+
source: bool | str = ...,
|
|
145
|
+
id_offset: int = ...,
|
|
146
|
+
) -> None: ...
|
|
147
|
+
@overload
|
|
148
|
+
def to_swc(
|
|
149
|
+
self,
|
|
150
|
+
*,
|
|
151
|
+
extra_cols: list[str] | None = ...,
|
|
152
|
+
source: bool | str = ...,
|
|
153
|
+
id_offset: int = ...,
|
|
154
|
+
) -> str: ...
|
|
155
|
+
def to_swc(
|
|
156
|
+
self,
|
|
157
|
+
fname: str | None = None,
|
|
158
|
+
*,
|
|
159
|
+
extra_cols: list[str] | None = None,
|
|
160
|
+
source: bool | str = True,
|
|
161
|
+
comments: bool = True,
|
|
162
|
+
id_offset: int = 1,
|
|
163
|
+
) -> str | None:
|
|
164
|
+
"""Write swc file."""
|
|
165
|
+
data = []
|
|
166
|
+
if source is not False:
|
|
167
|
+
if not isinstance(source, str):
|
|
168
|
+
source = self.source if self.source else "Unknown"
|
|
169
|
+
data.append(f"source: {source}")
|
|
170
|
+
data.append("")
|
|
171
|
+
|
|
172
|
+
if comments is True:
|
|
173
|
+
data.extend(self.comments)
|
|
174
|
+
|
|
175
|
+
it = to_swc(
|
|
176
|
+
self.get_ndata, comments=data, extra_cols=extra_cols, id_offset=id_offset
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if fname is None:
|
|
180
|
+
return "".join(it)
|
|
181
|
+
|
|
182
|
+
with open(fname, "w", encoding="utf-8") as f:
|
|
183
|
+
f.writelines(it)
|
|
184
|
+
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
@overload
|
|
188
|
+
def to_eswc(self, fname: str, **kwargs) -> None: ...
|
|
189
|
+
@overload
|
|
190
|
+
def to_eswc(self, fname: None = ..., **kwargs) -> str: ...
|
|
191
|
+
def to_eswc(
|
|
192
|
+
self,
|
|
193
|
+
fname: str | None = None,
|
|
194
|
+
swc_path: str | None = None,
|
|
195
|
+
extra_cols: list[str] | None = None,
|
|
196
|
+
**kwargs,
|
|
197
|
+
) -> str | None:
|
|
198
|
+
if swc_path is None:
|
|
199
|
+
warnings.warn(
|
|
200
|
+
"`swc_path` has been renamed to `fname` since v0.5.1, "
|
|
201
|
+
"and will be removed in next version",
|
|
202
|
+
DeprecationWarning,
|
|
203
|
+
)
|
|
204
|
+
fname = swc_path
|
|
205
|
+
|
|
206
|
+
extra_cols = extra_cols or []
|
|
207
|
+
extra_cols.extend(k for k, _ in eswc_cols)
|
|
208
|
+
return self.to_swc(fname, extra_cols=extra_cols, **kwargs) # type: ignore
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
SWCTypeVar = TypeVar("SWCTypeVar", bound=SWCLike)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class DictSWC(SWCLike):
|
|
215
|
+
"""SWC implementation on dict."""
|
|
216
|
+
|
|
217
|
+
ndata: dict[str, npt.NDArray]
|
|
218
|
+
|
|
219
|
+
def __init__(
|
|
220
|
+
self,
|
|
221
|
+
*,
|
|
222
|
+
source: str = "",
|
|
223
|
+
comments: Iterable[str] | None = None,
|
|
224
|
+
names: SWCNames | None = None,
|
|
225
|
+
**kwargs: npt.NDArray,
|
|
226
|
+
):
|
|
227
|
+
super().__init__()
|
|
228
|
+
self.source = source
|
|
229
|
+
self.comments = list(comments) if comments is not None else []
|
|
230
|
+
self.names = get_names(names)
|
|
231
|
+
self.ndata = kwargs
|
|
232
|
+
|
|
233
|
+
def keys(self) -> Iterable[str]:
|
|
234
|
+
return self.ndata.keys()
|
|
235
|
+
|
|
236
|
+
def values(self) -> Iterable[npt.NDArray[Any]]:
|
|
237
|
+
return self.ndata.values()
|
|
238
|
+
|
|
239
|
+
def items(self) -> Iterable[tuple[str, npt.NDArray[Any]]]:
|
|
240
|
+
return self.ndata.items()
|
|
241
|
+
|
|
242
|
+
def get_ndata(self, key: str) -> npt.NDArray[Any]:
|
|
243
|
+
return self.ndata[key]
|
|
244
|
+
|
|
245
|
+
def copy(self) -> Self:
|
|
246
|
+
"""Make a copy."""
|
|
247
|
+
return deepcopy(self)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""SWC format utils.
|
|
7
|
+
|
|
8
|
+
NOTE: This module provides a bunch of methods to manipulating swc files, they are
|
|
9
|
+
always trivial and unstabled, so we are NOT export it by default. If you use the method
|
|
10
|
+
here, please review the code more frequently, we will try to flag all breaking changes
|
|
11
|
+
but NO promises.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from swcgeom.core.swc_utils.assembler import * # noqa: F403
|
|
15
|
+
from swcgeom.core.swc_utils.base import * # noqa: F403
|
|
16
|
+
from swcgeom.core.swc_utils.checker import * # noqa: F403
|
|
17
|
+
from swcgeom.core.swc_utils.io import * # noqa: F403
|
|
18
|
+
from swcgeom.core.swc_utils.normalizer import * # noqa: F403
|
|
19
|
+
from swcgeom.core.swc_utils.subtree import * # noqa: F403
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Assemble lines to swc.
|
|
7
|
+
|
|
8
|
+
NOTE: This module is deprecated, please use `~.transforms.LinesToTree` instead.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__all__ = ["assemble_lines", "try_assemble_lines"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def assemble_lines(*args, **kwargs):
|
|
15
|
+
"""Assemble lines to tree.
|
|
16
|
+
|
|
17
|
+
.. deprecated:: 0.15.0
|
|
18
|
+
Use :meth:`~.transforms.LinesToTree` instead.
|
|
19
|
+
"""
|
|
20
|
+
raise DeprecationWarning(
|
|
21
|
+
"`assemble_lines` has been replaced by `~.transforms.LinesToTree` because it "
|
|
22
|
+
"can be easy assemble with other tansformations.",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def try_assemble_lines(*args, **kwargs):
|
|
27
|
+
"""Try assemble lines to tree.
|
|
28
|
+
|
|
29
|
+
.. deprecated:: 0.15.0
|
|
30
|
+
Use :meth:`~.transforms.LinesToTree` instead.
|
|
31
|
+
"""
|
|
32
|
+
raise DeprecationWarning(
|
|
33
|
+
"`try_assemble_lines` has been replaced by `~.transforms.LinesToTree` because "
|
|
34
|
+
"it can be easy assemble with other tansformations.",
|
|
35
|
+
)
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Base SWC format utils."""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from typing import Literal, NamedTuple, TypeVar, overload
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import numpy.typing as npt
|
|
13
|
+
import pandas as pd
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Topology",
|
|
17
|
+
"SWCNames",
|
|
18
|
+
"swc_names", # may not need to export
|
|
19
|
+
"get_names",
|
|
20
|
+
"SWCTypes",
|
|
21
|
+
"get_types",
|
|
22
|
+
"get_topology",
|
|
23
|
+
"get_dsu",
|
|
24
|
+
"traverse",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
T = TypeVar("T")
|
|
28
|
+
K = TypeVar("K")
|
|
29
|
+
Topology = tuple[npt.NDArray[np.int32], npt.NDArray[np.int32]] # (id, pid)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SWCNames(NamedTuple):
|
|
33
|
+
"""SWC format column names."""
|
|
34
|
+
|
|
35
|
+
id: str = "id"
|
|
36
|
+
type: str = "type"
|
|
37
|
+
x: str = "x"
|
|
38
|
+
y: str = "y"
|
|
39
|
+
z: str = "z"
|
|
40
|
+
r: str = "r"
|
|
41
|
+
pid: str = "pid"
|
|
42
|
+
|
|
43
|
+
def cols(self) -> list[str]:
|
|
44
|
+
return [self.id, self.type, self.x, self.y, self.z, self.r, self.pid]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
swc_names = SWCNames()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_names(names: SWCNames | None = None) -> SWCNames:
|
|
51
|
+
return names or swc_names
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class SWCTypes(NamedTuple):
|
|
55
|
+
"""SWC format types.
|
|
56
|
+
|
|
57
|
+
See Also:
|
|
58
|
+
NeuroMoprho.org - What is SWC format?: https://neuromorpho.org/myfaq.jsp
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
undefined: int = 0
|
|
62
|
+
soma: int = 1
|
|
63
|
+
axon: int = 2
|
|
64
|
+
basal_dendrite: int = 3
|
|
65
|
+
apical_dendrite: int = 4
|
|
66
|
+
custom: int = 5 # user-defined preferences
|
|
67
|
+
unspecified_neurites: int = 6
|
|
68
|
+
glia_processes: int = 7
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
swc_types = SWCTypes()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_types(types: SWCTypes | None = None) -> SWCTypes:
|
|
75
|
+
return types or swc_types
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_topology(df: pd.DataFrame, *, names: SWCNames | None = None) -> Topology:
|
|
79
|
+
names = get_names(names)
|
|
80
|
+
return (df[names.id].to_numpy(), df[names.pid].to_numpy())
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_dsu(
|
|
84
|
+
df: pd.DataFrame, *, names: SWCNames | None = None
|
|
85
|
+
) -> npt.NDArray[np.int32]:
|
|
86
|
+
"""Get disjoint set union."""
|
|
87
|
+
names = get_names(names)
|
|
88
|
+
dsu = np.where(
|
|
89
|
+
df[names.pid] == -1, df[names.id], df[names.pid]
|
|
90
|
+
) # Disjoint Set Union
|
|
91
|
+
|
|
92
|
+
id2idx = dict(zip(df[names.id], range(len(df))))
|
|
93
|
+
dsu = np.array([id2idx[i] for i in dsu], dtype=np.int32)
|
|
94
|
+
|
|
95
|
+
while True:
|
|
96
|
+
flag = True
|
|
97
|
+
for i, p in enumerate(dsu):
|
|
98
|
+
if dsu[i] != dsu[p]:
|
|
99
|
+
dsu[i] = dsu[p]
|
|
100
|
+
flag = False
|
|
101
|
+
|
|
102
|
+
if flag:
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
return dsu
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@overload
|
|
109
|
+
def traverse(
|
|
110
|
+
topology: Topology,
|
|
111
|
+
*,
|
|
112
|
+
enter: Callable[[int, T | None], T],
|
|
113
|
+
root: int | np.integer = ...,
|
|
114
|
+
mode: Literal["dfs"] = ...,
|
|
115
|
+
) -> None: ...
|
|
116
|
+
@overload
|
|
117
|
+
def traverse(
|
|
118
|
+
topology: Topology,
|
|
119
|
+
*,
|
|
120
|
+
leave: Callable[[int, list[K]], K],
|
|
121
|
+
root: int | np.integer = ...,
|
|
122
|
+
mode: Literal["dfs"] = ...,
|
|
123
|
+
) -> K: ...
|
|
124
|
+
@overload
|
|
125
|
+
def traverse(
|
|
126
|
+
topology: Topology,
|
|
127
|
+
*,
|
|
128
|
+
enter: Callable[[int, T | None], T],
|
|
129
|
+
leave: Callable[[int, list[K]], K],
|
|
130
|
+
root: int | np.integer = ...,
|
|
131
|
+
mode: Literal["dfs"] = ...,
|
|
132
|
+
) -> K: ...
|
|
133
|
+
def traverse(topology: Topology, *, mode="dfs", **kwargs):
|
|
134
|
+
"""Traverse nodes.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
enter: (id: int, parent: T | None) => T, optional
|
|
138
|
+
The callback when entering node, which accepts two parameters, the current node
|
|
139
|
+
id and the return value of it parent node. In particular, the root node
|
|
140
|
+
receives an `None`.
|
|
141
|
+
leave: (id: int, children: list[T]) => T, optional
|
|
142
|
+
The callback when leaving node. When leaving a node, subtree has already been
|
|
143
|
+
traversed. Callback accepts two parameters, the current node id and list of the
|
|
144
|
+
return value of children, In particular, the leaf node receives an empty list.
|
|
145
|
+
root: Start from the root node of the subtree
|
|
146
|
+
mode: The traverse mode, only support "dfs" now.
|
|
147
|
+
"""
|
|
148
|
+
match mode:
|
|
149
|
+
case "dfs":
|
|
150
|
+
return _traverse_dfs(topology, **kwargs)
|
|
151
|
+
case _:
|
|
152
|
+
raise ValueError(f"unsupported mode: `{mode}`")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _traverse_dfs(topology: Topology, *, enter=None, leave=None, root=0):
|
|
156
|
+
"""Traverse each nodes by dfs."""
|
|
157
|
+
children_map = dict[int, list[int]]()
|
|
158
|
+
for idx, pid in zip(*topology):
|
|
159
|
+
children_map.setdefault(pid, [])
|
|
160
|
+
children_map[pid].append(idx)
|
|
161
|
+
|
|
162
|
+
# manual dfs to avoid stack overflow in long branch
|
|
163
|
+
stack: list[tuple[int, bool]] = [(root, True)] # (idx, is_enter)
|
|
164
|
+
params = {root: None}
|
|
165
|
+
vals = {}
|
|
166
|
+
|
|
167
|
+
while len(stack) != 0:
|
|
168
|
+
idx, is_enter = stack.pop()
|
|
169
|
+
if is_enter: # enter
|
|
170
|
+
pre = params.pop(idx)
|
|
171
|
+
cur = enter(idx, pre) if enter is not None else None
|
|
172
|
+
stack.append((idx, False))
|
|
173
|
+
for child in children_map.get(idx, []):
|
|
174
|
+
stack.append((child, True))
|
|
175
|
+
params[child] = cur
|
|
176
|
+
else: # leave
|
|
177
|
+
children = [vals.pop(i) for i in children_map.get(idx, [])]
|
|
178
|
+
vals[idx] = leave(idx, children) if leave is not None else None
|
|
179
|
+
|
|
180
|
+
return vals[root]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Check common"""
|
|
7
|
+
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
from typing_extensions import deprecated
|
|
13
|
+
|
|
14
|
+
from swcgeom.core.swc_utils.base import SWCNames, Topology, get_dsu, get_names, traverse
|
|
15
|
+
from swcgeom.utils import DisjointSetUnion
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"is_single_root",
|
|
19
|
+
"is_bifurcate",
|
|
20
|
+
"is_sorted",
|
|
21
|
+
"has_cyclic",
|
|
22
|
+
# legacy
|
|
23
|
+
"is_binary_tree",
|
|
24
|
+
"check_single_root",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_single_root(df: pd.DataFrame, *, names: SWCNames | None = None) -> bool:
|
|
29
|
+
"""Check is it only one root."""
|
|
30
|
+
return len(np.unique(get_dsu(df, names=names))) == 1
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def is_bifurcate(topology: Topology, *, exclude_root: bool = True) -> bool:
|
|
34
|
+
"""Check is it a bifurcate topology."""
|
|
35
|
+
children = defaultdict(list)
|
|
36
|
+
for idx, pid in zip(*topology):
|
|
37
|
+
children[pid].append(idx)
|
|
38
|
+
|
|
39
|
+
root = children[-1]
|
|
40
|
+
for k, v in children.items():
|
|
41
|
+
if len(v) > 1 and (not exclude_root or k in root):
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
return True
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def is_sorted(topology: Topology) -> bool:
|
|
48
|
+
"""Check is it sorted.
|
|
49
|
+
|
|
50
|
+
In a sorted topology, parent samples should appear before any child samples.
|
|
51
|
+
"""
|
|
52
|
+
flag = True
|
|
53
|
+
|
|
54
|
+
def enter(idx: int, parent: int | None) -> int:
|
|
55
|
+
nonlocal flag
|
|
56
|
+
if parent is not None and idx < parent:
|
|
57
|
+
flag = False
|
|
58
|
+
|
|
59
|
+
return idx
|
|
60
|
+
|
|
61
|
+
traverse(topology=topology, enter=enter)
|
|
62
|
+
return flag
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def has_cyclic(topology: Topology) -> bool:
|
|
66
|
+
"""Has cyclic in topology."""
|
|
67
|
+
node_num = len(topology[0])
|
|
68
|
+
dsu = DisjointSetUnion(node_number=node_num)
|
|
69
|
+
|
|
70
|
+
for i in range(node_num):
|
|
71
|
+
node_a = topology[0][i]
|
|
72
|
+
node_b = topology[1][i]
|
|
73
|
+
# skip the root node
|
|
74
|
+
if node_b == -1:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
# check whether it is circle
|
|
78
|
+
if dsu.is_same_set(node_a, node_b):
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
dsu.union_sets(node_a, node_b)
|
|
82
|
+
|
|
83
|
+
return False
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@deprecated("Use `is_single_root` instead")
|
|
87
|
+
def check_single_root(*args, **kwargs) -> bool:
|
|
88
|
+
"""Check if the tree is single root.
|
|
89
|
+
|
|
90
|
+
.. deprecated:: 0.5.0
|
|
91
|
+
Use :meth:`is_single_root` instead.
|
|
92
|
+
"""
|
|
93
|
+
return is_single_root(*args, **kwargs)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@deprecated("Use `is_bifurcate` instead")
|
|
97
|
+
def is_binary_tree(
|
|
98
|
+
df: pd.DataFrame, exclude_root: bool = True, *, names: SWCNames | None = None
|
|
99
|
+
) -> bool:
|
|
100
|
+
"""Check is it a binary tree.
|
|
101
|
+
|
|
102
|
+
.. deprecated:: 0.8.0
|
|
103
|
+
Use :meth:`is_bifurcate` instead.
|
|
104
|
+
"""
|
|
105
|
+
names = get_names(names)
|
|
106
|
+
topo = (df[names.id].to_numpy(), df[names.pid].to_numpy())
|
|
107
|
+
return is_bifurcate(topo, exclude_root=exclude_root)
|