swcgeom 0.15.0__py3-none-any.whl → 0.18.3__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/__init__.py +26 -1
- swcgeom/analysis/__init__.py +21 -8
- swcgeom/analysis/feature_extractor.py +43 -18
- swcgeom/analysis/features.py +250 -0
- swcgeom/analysis/lmeasure.py +857 -0
- swcgeom/analysis/sholl.py +55 -29
- swcgeom/analysis/trunk.py +27 -11
- swcgeom/analysis/visualization.py +24 -9
- swcgeom/analysis/visualization3d.py +100 -0
- swcgeom/analysis/volume.py +19 -4
- swcgeom/core/__init__.py +32 -9
- swcgeom/core/branch.py +28 -7
- swcgeom/core/branch_tree.py +18 -4
- swcgeom/core/{segment.py → compartment.py} +31 -10
- swcgeom/core/node.py +31 -10
- swcgeom/core/path.py +37 -10
- swcgeom/core/population.py +103 -34
- swcgeom/core/swc.py +26 -10
- swcgeom/core/swc_utils/__init__.py +21 -7
- swcgeom/core/swc_utils/assembler.py +27 -1
- swcgeom/core/swc_utils/base.py +25 -12
- swcgeom/core/swc_utils/checker.py +31 -14
- swcgeom/core/swc_utils/io.py +24 -7
- swcgeom/core/swc_utils/normalizer.py +20 -4
- swcgeom/core/swc_utils/subtree.py +17 -2
- swcgeom/core/tree.py +85 -72
- swcgeom/core/tree_utils.py +31 -16
- swcgeom/core/tree_utils_impl.py +18 -3
- swcgeom/images/__init__.py +17 -2
- swcgeom/images/augmentation.py +24 -4
- swcgeom/images/contrast.py +122 -0
- swcgeom/images/folder.py +97 -39
- swcgeom/images/io.py +108 -121
- swcgeom/transforms/__init__.py +28 -10
- swcgeom/transforms/base.py +17 -2
- swcgeom/transforms/branch.py +74 -8
- swcgeom/transforms/branch_tree.py +82 -0
- swcgeom/transforms/geometry.py +22 -7
- swcgeom/transforms/image_preprocess.py +115 -0
- swcgeom/transforms/image_stack.py +37 -13
- swcgeom/transforms/images.py +184 -7
- swcgeom/transforms/mst.py +20 -5
- swcgeom/transforms/neurolucida_asc.py +508 -0
- swcgeom/transforms/path.py +15 -0
- swcgeom/transforms/population.py +16 -3
- swcgeom/transforms/tree.py +89 -31
- swcgeom/transforms/tree_assembler.py +23 -7
- swcgeom/utils/__init__.py +27 -11
- swcgeom/utils/debug.py +15 -0
- swcgeom/utils/download.py +59 -21
- swcgeom/utils/dsu.py +15 -0
- swcgeom/utils/ellipse.py +18 -4
- swcgeom/utils/file.py +15 -0
- swcgeom/utils/neuromorpho.py +439 -302
- swcgeom/utils/numpy_helper.py +29 -4
- swcgeom/utils/plotter_2d.py +151 -0
- swcgeom/utils/plotter_3d.py +48 -0
- swcgeom/utils/renderer.py +49 -145
- swcgeom/utils/sdf.py +24 -8
- swcgeom/utils/solid_geometry.py +16 -3
- swcgeom/utils/transforms.py +17 -4
- swcgeom/utils/volumetric_object.py +23 -10
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
- swcgeom-0.18.3.dist-info/RECORD +67 -0
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/WHEEL +1 -1
- swcgeom/_version.py +0 -16
- swcgeom/analysis/branch_features.py +0 -67
- swcgeom/analysis/node_features.py +0 -121
- swcgeom/analysis/path_features.py +0 -37
- swcgeom-0.15.0.dist-info/RECORD +0 -62
- {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Assemble lines to swc.
|
|
2
17
|
|
|
3
18
|
Notes
|
|
@@ -6,11 +21,16 @@ This module is deprecated, please use `~.transforms.LinesToTree`
|
|
|
6
21
|
instead.
|
|
7
22
|
"""
|
|
8
23
|
|
|
9
|
-
|
|
10
24
|
__all__ = ["assemble_lines", "try_assemble_lines"]
|
|
11
25
|
|
|
12
26
|
|
|
13
27
|
def assemble_lines(*args, **kwargs):
|
|
28
|
+
"""Assemble lines to tree.
|
|
29
|
+
|
|
30
|
+
.. deprecated:: 0.15.0
|
|
31
|
+
Use :meth:`~.transforms.LinesToTree` instead.
|
|
32
|
+
"""
|
|
33
|
+
|
|
14
34
|
raise DeprecationWarning(
|
|
15
35
|
"`assemble_lines` has been replaced by "
|
|
16
36
|
"`~.transforms.LinesToTree` because it can be easy assemble "
|
|
@@ -19,6 +39,12 @@ def assemble_lines(*args, **kwargs):
|
|
|
19
39
|
|
|
20
40
|
|
|
21
41
|
def try_assemble_lines(*args, **kwargs):
|
|
42
|
+
"""Try assemble lines to tree.
|
|
43
|
+
|
|
44
|
+
.. deprecated:: 0.15.0
|
|
45
|
+
Use :meth:`~.transforms.LinesToTree` instead.
|
|
46
|
+
"""
|
|
47
|
+
|
|
22
48
|
raise DeprecationWarning(
|
|
23
49
|
"`try_assemble_lines` has been replaced by "
|
|
24
50
|
"`~.transforms.LinesToTree` because it can be easy assemble "
|
swcgeom/core/swc_utils/base.py
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Base SWC format utils."""
|
|
2
17
|
|
|
3
|
-
from
|
|
4
|
-
from typing import
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from typing import Literal, NamedTuple, Optional, TypeVar, overload
|
|
5
20
|
|
|
6
21
|
import numpy as np
|
|
7
22
|
import numpy.typing as npt
|
|
@@ -20,11 +35,10 @@ __all__ = [
|
|
|
20
35
|
]
|
|
21
36
|
|
|
22
37
|
T, K = TypeVar("T"), TypeVar("K")
|
|
23
|
-
Topology =
|
|
38
|
+
Topology = tuple[npt.NDArray[np.int32], npt.NDArray[np.int32]] # (id, pid)
|
|
24
39
|
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
class SWCNames:
|
|
41
|
+
class SWCNames(NamedTuple):
|
|
28
42
|
"""SWC format column names."""
|
|
29
43
|
|
|
30
44
|
id: str = "id"
|
|
@@ -35,7 +49,7 @@ class SWCNames:
|
|
|
35
49
|
r: str = "r"
|
|
36
50
|
pid: str = "pid"
|
|
37
51
|
|
|
38
|
-
def cols(self) ->
|
|
52
|
+
def cols(self) -> list[str]:
|
|
39
53
|
return [self.id, self.type, self.x, self.y, self.z, self.r, self.pid]
|
|
40
54
|
|
|
41
55
|
|
|
@@ -46,8 +60,7 @@ def get_names(names: Optional[SWCNames] = None) -> SWCNames:
|
|
|
46
60
|
return names or swc_names
|
|
47
61
|
|
|
48
62
|
|
|
49
|
-
|
|
50
|
-
class SWCTypes:
|
|
63
|
+
class SWCTypes(NamedTuple):
|
|
51
64
|
"""SWC format types.
|
|
52
65
|
|
|
53
66
|
See Also
|
|
@@ -107,10 +120,10 @@ def get_dsu(
|
|
|
107
120
|
@overload
|
|
108
121
|
def traverse(topology: Topology, *, enter: Callable[[int, T | None], T], root: int | np.integer = ..., mode: Literal["dfs"] = ...) -> None: ...
|
|
109
122
|
@overload
|
|
110
|
-
def traverse(topology: Topology, *, leave: Callable[[int,
|
|
123
|
+
def traverse(topology: Topology, *, leave: Callable[[int, list[K]], K], root: int | np.integer = ..., mode: Literal["dfs"] = ...) -> K: ...
|
|
111
124
|
@overload
|
|
112
125
|
def traverse(
|
|
113
|
-
topology: Topology, *, enter: Callable[[int, T | None], T], leave: Callable[[int,
|
|
126
|
+
topology: Topology, *, enter: Callable[[int, T | None], T], leave: Callable[[int, list[K]], K],
|
|
114
127
|
root: int | np.integer = ..., mode: Literal["dfs"] = ...,
|
|
115
128
|
) -> K: ...
|
|
116
129
|
# fmt: on
|
|
@@ -123,7 +136,7 @@ def traverse(topology: Topology, *, mode="dfs", **kwargs):
|
|
|
123
136
|
The callback when entering node, which accepts two parameters,
|
|
124
137
|
the current node id and the return value of it parent node. In
|
|
125
138
|
particular, the root node receives an `None`.
|
|
126
|
-
leave : (id: int, children:
|
|
139
|
+
leave : (id: int, children: list[T]) => T, optional
|
|
127
140
|
The callback when leaving node. When leaving a node, subtree
|
|
128
141
|
has already been traversed. Callback accepts two parameters,
|
|
129
142
|
the current node id and list of the return value of children,
|
|
@@ -148,7 +161,7 @@ def _traverse_dfs(topology: Topology, *, enter=None, leave=None, root=0):
|
|
|
148
161
|
children_map[pid].append(idx)
|
|
149
162
|
|
|
150
163
|
# manual dfs to avoid stack overflow in long branch
|
|
151
|
-
stack:
|
|
164
|
+
stack: list[tuple[int, bool]] = [(root, True)] # (idx, is_enter)
|
|
152
165
|
params = {root: None}
|
|
153
166
|
vals = {}
|
|
154
167
|
|
|
@@ -1,16 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""Check common"""
|
|
2
17
|
|
|
3
|
-
import warnings
|
|
4
18
|
from collections import defaultdict
|
|
5
19
|
from typing import Optional
|
|
6
20
|
|
|
7
21
|
import numpy as np
|
|
8
22
|
import pandas as pd
|
|
23
|
+
from typing_extensions import deprecated
|
|
9
24
|
|
|
10
25
|
from swcgeom.core.swc_utils.base import SWCNames, Topology, get_dsu, get_names, traverse
|
|
11
26
|
from swcgeom.utils import DisjointSetUnion
|
|
12
27
|
|
|
13
|
-
|
|
14
28
|
__all__ = [
|
|
15
29
|
"is_single_root",
|
|
16
30
|
"is_bifurcate",
|
|
@@ -82,24 +96,27 @@ def has_cyclic(topology: Topology) -> bool:
|
|
|
82
96
|
return False
|
|
83
97
|
|
|
84
98
|
|
|
99
|
+
@deprecated("Use `is_single_root` instead")
|
|
85
100
|
def check_single_root(*args, **kwargs) -> bool:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
101
|
+
"""Check if the tree is single root.
|
|
102
|
+
|
|
103
|
+
.. deprecated:: 0.5.0
|
|
104
|
+
Use :meth:`is_single_root` instead.
|
|
105
|
+
"""
|
|
106
|
+
|
|
91
107
|
return is_single_root(*args, **kwargs)
|
|
92
108
|
|
|
93
109
|
|
|
110
|
+
@deprecated("Use `is_bifurcate` instead")
|
|
94
111
|
def is_binary_tree(
|
|
95
112
|
df: pd.DataFrame, exclude_root: bool = True, *, names: Optional[SWCNames] = None
|
|
96
113
|
) -> bool:
|
|
97
|
-
"""Check is it a binary tree.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
"""Check is it a binary tree.
|
|
115
|
+
|
|
116
|
+
.. deprecated:: 0.8.0
|
|
117
|
+
Use :meth:`is_bifurcate` instead.
|
|
118
|
+
"""
|
|
119
|
+
|
|
103
120
|
names = get_names(names)
|
|
104
121
|
topo = (df[names.id].to_numpy(), df[names.pid].to_numpy())
|
|
105
122
|
return is_bifurcate(topo, exclude_root=exclude_root)
|
swcgeom/core/swc_utils/io.py
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Read and write swc format."""
|
|
2
17
|
|
|
3
18
|
import re
|
|
4
19
|
import warnings
|
|
5
|
-
from
|
|
20
|
+
from collections.abc import Callable, Iterable
|
|
21
|
+
from typing import Literal, Optional
|
|
6
22
|
|
|
7
23
|
import numpy as np
|
|
8
24
|
import numpy.typing as npt
|
|
@@ -30,7 +46,7 @@ def read_swc(
|
|
|
30
46
|
*,
|
|
31
47
|
encoding: Literal["detect"] | str = "utf-8",
|
|
32
48
|
names: Optional[SWCNames] = None,
|
|
33
|
-
) ->
|
|
49
|
+
) -> tuple[pd.DataFrame, list[str]]:
|
|
34
50
|
"""Read swc file.
|
|
35
51
|
|
|
36
52
|
Parameters
|
|
@@ -55,7 +71,7 @@ def read_swc(
|
|
|
55
71
|
Returns
|
|
56
72
|
-------
|
|
57
73
|
df : ~pandas.DataFrame
|
|
58
|
-
comments :
|
|
74
|
+
comments : List of string
|
|
59
75
|
"""
|
|
60
76
|
|
|
61
77
|
names = get_names(names)
|
|
@@ -137,14 +153,14 @@ def parse_swc(
|
|
|
137
153
|
names: SWCNames,
|
|
138
154
|
extra_cols: Iterable[str] | None = None,
|
|
139
155
|
encoding: Literal["detect"] | str = "utf-8",
|
|
140
|
-
) ->
|
|
156
|
+
) -> tuple[pd.DataFrame, list[str]]:
|
|
141
157
|
"""Parse swc file.
|
|
142
158
|
|
|
143
159
|
Parameters
|
|
144
160
|
----------
|
|
145
161
|
fname : PathOrIO
|
|
146
162
|
names : SWCNames
|
|
147
|
-
extra_cols :
|
|
163
|
+
extra_cols : List of str, optional
|
|
148
164
|
encoding : str | 'detect', default `utf-8`
|
|
149
165
|
The name of the encoding used to decode the file. If is
|
|
150
166
|
`detect`, we will try to detect the character encoding.
|
|
@@ -152,7 +168,7 @@ def parse_swc(
|
|
|
152
168
|
Returns
|
|
153
169
|
-------
|
|
154
170
|
df : ~pandas.DataFrame
|
|
155
|
-
comments :
|
|
171
|
+
comments : List of string
|
|
156
172
|
"""
|
|
157
173
|
|
|
158
174
|
# pylint: disable=too-many-locals
|
|
@@ -171,7 +187,8 @@ def parse_swc(
|
|
|
171
187
|
RE_FLOAT, # r
|
|
172
188
|
r"(-?[0-9]+)", # pid
|
|
173
189
|
] + [
|
|
174
|
-
RE_FLOAT
|
|
190
|
+
RE_FLOAT
|
|
191
|
+
for _ in extras # assert float
|
|
175
192
|
]
|
|
176
193
|
|
|
177
194
|
re_swc_cols_str = r"\s+".join(re_swc_cols)
|
|
@@ -1,9 +1,25 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""SWC format utils.
|
|
2
17
|
|
|
3
18
|
Methods ending with a underline imply an in-place transformation.
|
|
4
19
|
"""
|
|
5
20
|
|
|
6
|
-
from
|
|
21
|
+
from collections.abc import Callable
|
|
22
|
+
from typing import Literal, Optional
|
|
7
23
|
|
|
8
24
|
import numpy as np
|
|
9
25
|
import numpy.typing as npt
|
|
@@ -76,7 +92,7 @@ def link_roots_to_nearest_(
|
|
|
76
92
|
vs = df[[names.x, names.y, names.z]] - row[[names.x, names.y, names.z]]
|
|
77
93
|
dis = np.linalg.norm(vs.to_numpy(), axis=1)
|
|
78
94
|
subtree = dsu == dsu[i] # type: ignore
|
|
79
|
-
dis = np.where(subtree, np.
|
|
95
|
+
dis = np.where(subtree, np.inf, dis) # avoid link to same tree
|
|
80
96
|
dsu = np.where(subtree, dsu[dis.argmin()], dsu) # merge set
|
|
81
97
|
df.loc[i, names.pid] = df[names.id].iloc[dis.argmin()] # type: ignore
|
|
82
98
|
|
|
@@ -111,7 +127,7 @@ def sort_nodes_(df: pd.DataFrame, *, names: Optional[SWCNames] = None) -> None:
|
|
|
111
127
|
df[names.id], df[names.pid] = new_ids, new_pids
|
|
112
128
|
|
|
113
129
|
|
|
114
|
-
def sort_nodes_impl(topology: Topology) ->
|
|
130
|
+
def sort_nodes_impl(topology: Topology) -> tuple[Topology, npt.NDArray[np.int32]]:
|
|
115
131
|
"""Sort the indices of neuron tree.
|
|
116
132
|
|
|
117
133
|
Returns
|
|
@@ -127,7 +143,7 @@ def sort_nodes_impl(topology: Topology) -> Tuple[Topology, npt.NDArray[np.int32]
|
|
|
127
143
|
new_pids = np.full_like(old_ids, fill_value=-3)
|
|
128
144
|
new_id = 0
|
|
129
145
|
first_root = old_ids[(old_pids == -1).argmax()]
|
|
130
|
-
s:
|
|
146
|
+
s: list[tuple[npt.NDArray[np.int32], int]] = [(first_root, -1)]
|
|
131
147
|
while len(s) != 0:
|
|
132
148
|
old_id, new_pid = s.pop()
|
|
133
149
|
id_map[new_id] = old_id
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Cut subtree.
|
|
2
17
|
|
|
3
18
|
This module provides a series of low-level topological subtree methods,
|
|
@@ -6,7 +21,7 @@ but in more cases, you can use the high-level methods provided in
|
|
|
6
21
|
high-level API.
|
|
7
22
|
"""
|
|
8
23
|
|
|
9
|
-
from typing import
|
|
24
|
+
from typing import cast
|
|
10
25
|
|
|
11
26
|
import numpy as np
|
|
12
27
|
import numpy.typing as npt
|
|
@@ -18,7 +33,7 @@ __all__ = ["REMOVAL", "to_sub_topology", "propagate_removal"]
|
|
|
18
33
|
REMOVAL = -2 # A marker in utils, place in the ids to mark it removal
|
|
19
34
|
|
|
20
35
|
|
|
21
|
-
def to_sub_topology(sub: Topology) ->
|
|
36
|
+
def to_sub_topology(sub: Topology) -> tuple[Topology, npt.NDArray[np.int32]]:
|
|
22
37
|
"""Create sub tree from origin tree.
|
|
23
38
|
|
|
24
39
|
Mark the node to be removed, then use this method to get a child
|
swcgeom/core/tree.py
CHANGED
|
@@ -1,30 +1,34 @@
|
|
|
1
|
+
# Copyright 2022-2025 Zexin Yuan
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
1
16
|
"""Neuron tree."""
|
|
2
17
|
|
|
3
18
|
import itertools
|
|
4
19
|
import os
|
|
5
|
-
import
|
|
6
|
-
from typing import
|
|
7
|
-
Callable,
|
|
8
|
-
Dict,
|
|
9
|
-
Iterable,
|
|
10
|
-
Iterator,
|
|
11
|
-
List,
|
|
12
|
-
Literal,
|
|
13
|
-
Optional,
|
|
14
|
-
Tuple,
|
|
15
|
-
TypeVar,
|
|
16
|
-
Union,
|
|
17
|
-
overload,
|
|
18
|
-
)
|
|
20
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
21
|
+
from typing import Literal, Optional, TypeVar, Union, overload
|
|
19
22
|
|
|
20
23
|
import numpy as np
|
|
21
24
|
import numpy.typing as npt
|
|
22
25
|
import pandas as pd
|
|
26
|
+
from typing_extensions import deprecated
|
|
23
27
|
|
|
24
28
|
from swcgeom.core.branch import Branch
|
|
29
|
+
from swcgeom.core.compartment import Compartment, Compartments
|
|
25
30
|
from swcgeom.core.node import Node
|
|
26
31
|
from swcgeom.core.path import Path
|
|
27
|
-
from swcgeom.core.segment import Segment, Segments
|
|
28
32
|
from swcgeom.core.swc import DictSWC, eswc_cols
|
|
29
33
|
from swcgeom.core.swc_utils import SWCNames, get_names, read_swc, traverse
|
|
30
34
|
from swcgeom.core.tree_utils_impl import Mapping, get_subtree_impl
|
|
@@ -44,26 +48,17 @@ class Tree(DictSWC):
|
|
|
44
48
|
def parent(self) -> Union["Tree.Node", None]:
|
|
45
49
|
return Tree.Node(self.attach, self.pid) if self.pid != -1 else None
|
|
46
50
|
|
|
47
|
-
def children(self) ->
|
|
51
|
+
def children(self) -> list["Tree.Node"]:
|
|
48
52
|
children = self.attach.id()[self.attach.pid() == self.id]
|
|
49
53
|
return [Tree.Node(self.attach, idx) for idx in children]
|
|
50
54
|
|
|
51
|
-
def get_branch(self) -> "Tree.Branch":
|
|
52
|
-
warnings.warn(
|
|
53
|
-
"`Tree.Node.get_branch` has been renamed to "
|
|
54
|
-
"`Tree.Node.branch` since v0.3.1 and will be removed "
|
|
55
|
-
"in next version",
|
|
56
|
-
DeprecationWarning,
|
|
57
|
-
)
|
|
58
|
-
return self.branch()
|
|
59
|
-
|
|
60
55
|
def branch(self) -> "Tree.Branch":
|
|
61
|
-
ns:
|
|
62
|
-
while not ns[-1].
|
|
56
|
+
ns: list["Tree.Node"] = [self]
|
|
57
|
+
while not ns[-1].is_furcation() and (p := ns[-1].parent()) is not None:
|
|
63
58
|
ns.append(p)
|
|
64
59
|
|
|
65
60
|
ns.reverse()
|
|
66
|
-
while not (ns[-1].
|
|
61
|
+
while not (ns[-1].is_furcation() or ns[-1].is_tip()):
|
|
67
62
|
ns.append(ns[-1].children()[0])
|
|
68
63
|
|
|
69
64
|
return Tree.Branch(self.attach, [n.id for n in ns])
|
|
@@ -77,7 +72,7 @@ class Tree(DictSWC):
|
|
|
77
72
|
|
|
78
73
|
Parameters
|
|
79
74
|
----------
|
|
80
|
-
out_mapping : List of int or
|
|
75
|
+
out_mapping : List of int or dict[int, int], optional
|
|
81
76
|
Map from new id to old id.
|
|
82
77
|
"""
|
|
83
78
|
|
|
@@ -115,9 +110,11 @@ class Tree(DictSWC):
|
|
|
115
110
|
# TODO: should returns `Tree.Node`
|
|
116
111
|
"""Neural path."""
|
|
117
112
|
|
|
118
|
-
class
|
|
113
|
+
class Compartment(Compartment["Tree"]):
|
|
119
114
|
# TODO: should returns `Tree.Node`
|
|
120
|
-
"""Neural
|
|
115
|
+
"""Neural compartment."""
|
|
116
|
+
|
|
117
|
+
Segment = Compartment # Alias
|
|
121
118
|
|
|
122
119
|
class Branch(Branch["Tree"]):
|
|
123
120
|
# TODO: should returns `Tree.Node`
|
|
@@ -127,33 +124,33 @@ class Tree(DictSWC):
|
|
|
127
124
|
self,
|
|
128
125
|
n_nodes: int,
|
|
129
126
|
*,
|
|
130
|
-
# pylint: disable-next=redefined-builtin
|
|
131
|
-
id: Optional[npt.NDArray[np.int32]] = None,
|
|
132
|
-
# pylint: disable-next=redefined-builtin
|
|
133
|
-
type: Optional[npt.NDArray[np.int32]] = None,
|
|
134
|
-
x: Optional[npt.NDArray[np.float32]] = None,
|
|
135
|
-
y: Optional[npt.NDArray[np.float32]] = None,
|
|
136
|
-
z: Optional[npt.NDArray[np.float32]] = None,
|
|
137
|
-
r: Optional[npt.NDArray[np.float32]] = None,
|
|
138
|
-
pid: Optional[npt.NDArray[np.int32]] = None,
|
|
139
127
|
source: str = "",
|
|
140
128
|
comments: Optional[Iterable[str]] = None,
|
|
141
129
|
names: Optional[SWCNames] = None,
|
|
142
130
|
**kwargs: npt.NDArray,
|
|
143
131
|
) -> None:
|
|
144
132
|
names = get_names(names)
|
|
145
|
-
|
|
146
|
-
|
|
133
|
+
|
|
134
|
+
if names.id not in kwargs:
|
|
135
|
+
kwargs[names.id] = np.arange(0, n_nodes, step=1, dtype=np.int32)
|
|
136
|
+
|
|
137
|
+
if names.pid not in kwargs:
|
|
138
|
+
kwargs[names.pid] = np.arange(-1, n_nodes - 1, step=1, dtype=np.int32)
|
|
147
139
|
|
|
148
140
|
ndata = {
|
|
149
|
-
names.id: padding1d(n_nodes, id, dtype=np.int32),
|
|
150
|
-
names.type: padding1d(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
names.
|
|
154
|
-
names.
|
|
155
|
-
names.
|
|
141
|
+
names.id: padding1d(n_nodes, kwargs.pop(names.id, None), dtype=np.int32),
|
|
142
|
+
names.type: padding1d(
|
|
143
|
+
n_nodes, kwargs.pop(names.type, None), dtype=np.int32
|
|
144
|
+
),
|
|
145
|
+
names.x: padding1d(n_nodes, kwargs.pop(names.x, None), dtype=np.float32),
|
|
146
|
+
names.y: padding1d(n_nodes, kwargs.pop(names.y, None), dtype=np.float32),
|
|
147
|
+
names.z: padding1d(n_nodes, kwargs.pop(names.z, None), dtype=np.float32),
|
|
148
|
+
names.r: padding1d(
|
|
149
|
+
n_nodes, kwargs.pop(names.r, None), dtype=np.float32, padding_value=1
|
|
150
|
+
),
|
|
151
|
+
names.pid: padding1d(n_nodes, kwargs.pop(names.pid, None), dtype=np.int32),
|
|
156
152
|
}
|
|
153
|
+
# ? padding other columns
|
|
157
154
|
super().__init__(
|
|
158
155
|
**ndata, **kwargs, source=source, comments=comments, names=names
|
|
159
156
|
)
|
|
@@ -167,7 +164,7 @@ class Tree(DictSWC):
|
|
|
167
164
|
|
|
168
165
|
# fmt:off
|
|
169
166
|
@overload
|
|
170
|
-
def __getitem__(self, key: slice) ->
|
|
167
|
+
def __getitem__(self, key: slice) -> list[Node]: ...
|
|
171
168
|
@overload
|
|
172
169
|
def __getitem__(self, key: int) -> Node: ...
|
|
173
170
|
@overload
|
|
@@ -206,35 +203,50 @@ class Tree(DictSWC):
|
|
|
206
203
|
raise ValueError(f"no soma found in: {self.source}")
|
|
207
204
|
return n
|
|
208
205
|
|
|
209
|
-
def
|
|
210
|
-
"""Get all node of
|
|
211
|
-
|
|
206
|
+
def get_furcations(self) -> list[Node]:
|
|
207
|
+
"""Get all node of furcations."""
|
|
208
|
+
furcations: list[int] = []
|
|
212
209
|
|
|
213
|
-
def
|
|
210
|
+
def collect_furcations(n: Tree.Node, children: list[None]) -> None:
|
|
214
211
|
if len(children) > 1:
|
|
215
|
-
|
|
212
|
+
furcations.append(n.id)
|
|
213
|
+
|
|
214
|
+
self.traverse(leave=collect_furcations)
|
|
215
|
+
return [self.node(i) for i in furcations]
|
|
216
216
|
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
@deprecated("Use `get_furcations` instead")
|
|
218
|
+
def get_bifurcations(self) -> list[Node]:
|
|
219
|
+
"""Get all node of furcations.
|
|
220
|
+
|
|
221
|
+
Notes
|
|
222
|
+
-----
|
|
223
|
+
Deprecated due to the wrong spelling of furcation. For now, it
|
|
224
|
+
is just an alias of `get_furcations` and raise a warning. It
|
|
225
|
+
will be change to raise an error in the future.
|
|
226
|
+
"""
|
|
227
|
+
return self.get_furcations()
|
|
219
228
|
|
|
220
|
-
def get_tips(self) ->
|
|
229
|
+
def get_tips(self) -> list[Node]:
|
|
221
230
|
"""Get all node of tips."""
|
|
222
231
|
tip_ids = np.setdiff1d(self.id(), self.pid(), assume_unique=True)
|
|
223
232
|
return [self.node(i) for i in tip_ids]
|
|
224
233
|
|
|
225
|
-
def
|
|
226
|
-
return
|
|
234
|
+
def get_compartments(self) -> Compartments[Compartment]:
|
|
235
|
+
return Compartments(self.Compartment(self, n.pid, n.id) for n in self[1:])
|
|
227
236
|
|
|
228
|
-
def
|
|
229
|
-
|
|
237
|
+
def get_segments(self) -> Compartments[Compartment]: # Alias
|
|
238
|
+
return self.get_compartments()
|
|
230
239
|
|
|
231
|
-
|
|
240
|
+
def get_branches(self) -> list[Branch]:
|
|
241
|
+
def collect_branches(
|
|
242
|
+
node: "Tree.Node", pre: list[tuple[list[Tree.Branch], list[int]]]
|
|
243
|
+
) -> tuple[list[Tree.Branch], list[int]]:
|
|
232
244
|
if len(pre) == 1:
|
|
233
245
|
branches, child = pre[0]
|
|
234
246
|
child.append(node.id)
|
|
235
247
|
return branches, child
|
|
236
248
|
|
|
237
|
-
branches:
|
|
249
|
+
branches: list[Tree.Branch] = []
|
|
238
250
|
|
|
239
251
|
for sub_branches, child in pre:
|
|
240
252
|
child.append(node.id)
|
|
@@ -248,18 +260,19 @@ class Tree(DictSWC):
|
|
|
248
260
|
branches, _ = self.traverse(leave=collect_branches)
|
|
249
261
|
return branches
|
|
250
262
|
|
|
251
|
-
def get_paths(self) ->
|
|
263
|
+
def get_paths(self) -> list[Path]:
|
|
252
264
|
"""Get all path from soma to tips."""
|
|
253
|
-
path_dic:
|
|
254
|
-
Paths = List[List[int]]
|
|
265
|
+
path_dic: dict[int, list[int]] = {}
|
|
255
266
|
|
|
256
|
-
def assign_path(n: Tree.Node, pre_path:
|
|
267
|
+
def assign_path(n: Tree.Node, pre_path: list[int] | None) -> list[int]:
|
|
257
268
|
path = [] if pre_path is None else pre_path.copy()
|
|
258
269
|
path.append(n.id)
|
|
259
270
|
path_dic[n.id] = path
|
|
260
271
|
return path
|
|
261
272
|
|
|
262
|
-
def collect_path(
|
|
273
|
+
def collect_path(
|
|
274
|
+
n: Tree.Node, children: list[list[list[int]]]
|
|
275
|
+
) -> list[list[int]]:
|
|
263
276
|
if len(children) == 0:
|
|
264
277
|
return [path_dic[n.id]]
|
|
265
278
|
|
|
@@ -286,7 +299,7 @@ class Tree(DictSWC):
|
|
|
286
299
|
@overload
|
|
287
300
|
def traverse(self, *,
|
|
288
301
|
enter: Optional[Callable[[Node, T | None], T]] = ...,
|
|
289
|
-
leave: Callable[[Node,
|
|
302
|
+
leave: Callable[[Node, list[K]], K],
|
|
290
303
|
root: int | np.integer = ..., mode: Literal["dfs"] = ...) -> K: ...
|
|
291
304
|
# fmt: on
|
|
292
305
|
|
|
@@ -296,7 +309,7 @@ class Tree(DictSWC):
|
|
|
296
309
|
Parameters
|
|
297
310
|
----------
|
|
298
311
|
enter : (n: Node, parent: T | None) => T, optional
|
|
299
|
-
leave : (n: Node, children:
|
|
312
|
+
leave : (n: Node, children: list[T]) => T, optional
|
|
300
313
|
|
|
301
314
|
See Also
|
|
302
315
|
--------
|
|
@@ -358,7 +371,7 @@ class Tree(DictSWC):
|
|
|
358
371
|
|
|
359
372
|
@classmethod
|
|
360
373
|
def from_eswc(
|
|
361
|
-
cls, swc_file: str, extra_cols: Optional[
|
|
374
|
+
cls, swc_file: str, extra_cols: Optional[list[str]] = None, **kwargs
|
|
362
375
|
) -> "Tree":
|
|
363
376
|
"""Read neuron tree from eswc file.
|
|
364
377
|
|