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.

Files changed (72) hide show
  1. swcgeom/__init__.py +21 -0
  2. swcgeom/analysis/__init__.py +13 -0
  3. swcgeom/analysis/feature_extractor.py +454 -0
  4. swcgeom/analysis/features.py +218 -0
  5. swcgeom/analysis/lmeasure.py +750 -0
  6. swcgeom/analysis/sholl.py +201 -0
  7. swcgeom/analysis/trunk.py +183 -0
  8. swcgeom/analysis/visualization.py +191 -0
  9. swcgeom/analysis/visualization3d.py +81 -0
  10. swcgeom/analysis/volume.py +143 -0
  11. swcgeom/core/__init__.py +19 -0
  12. swcgeom/core/branch.py +129 -0
  13. swcgeom/core/branch_tree.py +65 -0
  14. swcgeom/core/compartment.py +107 -0
  15. swcgeom/core/node.py +130 -0
  16. swcgeom/core/path.py +155 -0
  17. swcgeom/core/population.py +341 -0
  18. swcgeom/core/swc.py +247 -0
  19. swcgeom/core/swc_utils/__init__.py +19 -0
  20. swcgeom/core/swc_utils/assembler.py +35 -0
  21. swcgeom/core/swc_utils/base.py +180 -0
  22. swcgeom/core/swc_utils/checker.py +107 -0
  23. swcgeom/core/swc_utils/io.py +204 -0
  24. swcgeom/core/swc_utils/normalizer.py +163 -0
  25. swcgeom/core/swc_utils/subtree.py +70 -0
  26. swcgeom/core/tree.py +384 -0
  27. swcgeom/core/tree_utils.py +277 -0
  28. swcgeom/core/tree_utils_impl.py +58 -0
  29. swcgeom/images/__init__.py +9 -0
  30. swcgeom/images/augmentation.py +149 -0
  31. swcgeom/images/contrast.py +87 -0
  32. swcgeom/images/folder.py +217 -0
  33. swcgeom/images/io.py +578 -0
  34. swcgeom/images/loaders/__init__.py +8 -0
  35. swcgeom/images/loaders/pbd.cp313-win_amd64.pyd +0 -0
  36. swcgeom/images/loaders/pbd.pyx +523 -0
  37. swcgeom/images/loaders/raw.cp313-win_amd64.pyd +0 -0
  38. swcgeom/images/loaders/raw.pyx +183 -0
  39. swcgeom/transforms/__init__.py +20 -0
  40. swcgeom/transforms/base.py +136 -0
  41. swcgeom/transforms/branch.py +223 -0
  42. swcgeom/transforms/branch_tree.py +74 -0
  43. swcgeom/transforms/geometry.py +270 -0
  44. swcgeom/transforms/image_preprocess.py +107 -0
  45. swcgeom/transforms/image_stack.py +219 -0
  46. swcgeom/transforms/images.py +206 -0
  47. swcgeom/transforms/mst.py +183 -0
  48. swcgeom/transforms/neurolucida_asc.py +498 -0
  49. swcgeom/transforms/path.py +56 -0
  50. swcgeom/transforms/population.py +36 -0
  51. swcgeom/transforms/tree.py +265 -0
  52. swcgeom/transforms/tree_assembler.py +161 -0
  53. swcgeom/utils/__init__.py +18 -0
  54. swcgeom/utils/debug.py +23 -0
  55. swcgeom/utils/download.py +119 -0
  56. swcgeom/utils/dsu.py +58 -0
  57. swcgeom/utils/ellipse.py +131 -0
  58. swcgeom/utils/file.py +90 -0
  59. swcgeom/utils/neuromorpho.py +581 -0
  60. swcgeom/utils/numpy_helper.py +70 -0
  61. swcgeom/utils/plotter_2d.py +134 -0
  62. swcgeom/utils/plotter_3d.py +35 -0
  63. swcgeom/utils/renderer.py +145 -0
  64. swcgeom/utils/sdf.py +324 -0
  65. swcgeom/utils/solid_geometry.py +154 -0
  66. swcgeom/utils/transforms.py +367 -0
  67. swcgeom/utils/volumetric_object.py +483 -0
  68. swcgeom-0.19.4.dist-info/METADATA +86 -0
  69. swcgeom-0.19.4.dist-info/RECORD +72 -0
  70. swcgeom-0.19.4.dist-info/WHEEL +5 -0
  71. swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
  72. 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)