swcgeom 0.13.2__py3-none-any.whl → 0.15.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/_version.py +2 -2
- swcgeom/analysis/volume.py +105 -20
- swcgeom/core/branch_tree.py +2 -3
- swcgeom/core/population.py +6 -7
- swcgeom/core/swc_utils/assembler.py +12 -141
- swcgeom/core/swc_utils/subtree.py +2 -2
- swcgeom/core/tree.py +19 -12
- swcgeom/core/tree_utils.py +23 -5
- swcgeom/core/tree_utils_impl.py +22 -6
- swcgeom/images/folder.py +42 -17
- swcgeom/images/io.py +65 -36
- swcgeom/transforms/base.py +41 -21
- swcgeom/transforms/branch.py +5 -5
- swcgeom/transforms/geometry.py +42 -18
- swcgeom/transforms/image_stack.py +104 -124
- swcgeom/transforms/images.py +2 -2
- swcgeom/transforms/mst.py +5 -13
- swcgeom/transforms/population.py +2 -2
- swcgeom/transforms/tree.py +7 -13
- swcgeom/transforms/tree_assembler.py +85 -19
- swcgeom/utils/__init__.py +1 -1
- swcgeom/utils/sdf.py +167 -10
- swcgeom/utils/solid_geometry.py +26 -0
- swcgeom/utils/volumetric_object.py +504 -0
- swcgeom-0.15.0.dist-info/LICENSE +201 -0
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/METADATA +6 -5
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/RECORD +29 -29
- swcgeom/utils/geometry_object.py +0 -255
- swcgeom-0.13.2.dist-info/LICENSE +0 -3
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/WHEEL +0 -0
- {swcgeom-0.13.2.dist-info → swcgeom-0.15.0.dist-info}/top_level.txt +0 -0
swcgeom/_version.py
CHANGED
swcgeom/analysis/volume.py
CHANGED
|
@@ -1,22 +1,44 @@
|
|
|
1
1
|
"""Analysis of volume of a SWC tree."""
|
|
2
2
|
|
|
3
|
-
from typing import List
|
|
3
|
+
from typing import Dict, List, Literal
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
|
+
from sdflit import ColoredMaterial, ObjectsScene, SDFObject, UniformSampler
|
|
6
7
|
|
|
7
8
|
from swcgeom.core import Tree
|
|
8
|
-
from swcgeom.utils import
|
|
9
|
+
from swcgeom.utils import VolFrustumCone, VolSphere
|
|
9
10
|
|
|
10
11
|
__all__ = ["get_volume"]
|
|
11
12
|
|
|
13
|
+
ACCURACY_LEVEL = Literal["low", "middle", "high"]
|
|
14
|
+
ACCURACY_LEVELS: Dict[ACCURACY_LEVEL, int] = {"low": 3, "middle": 5, "high": 8}
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
|
|
17
|
+
def get_volume(
|
|
18
|
+
tree: Tree,
|
|
19
|
+
*,
|
|
20
|
+
method: Literal["frustum_cone"] = "frustum_cone",
|
|
21
|
+
accuracy: int | ACCURACY_LEVEL = "middle",
|
|
22
|
+
) -> float:
|
|
14
23
|
"""Get the volume of the tree.
|
|
15
24
|
|
|
16
25
|
Parameters
|
|
17
26
|
----------
|
|
18
27
|
tree : Tree
|
|
19
|
-
|
|
28
|
+
Neuronal tree.
|
|
29
|
+
method : {"frustum_cone"}, optional
|
|
30
|
+
Method for volume calculation.
|
|
31
|
+
accuracy : int or {"low", "middle", "high"}, optional
|
|
32
|
+
Accuracy level for volume calculation. The higher the accuracy,
|
|
33
|
+
the more accurate the volume calculation, but the slower the
|
|
34
|
+
calculation. The accuracy level can be specified either as an
|
|
35
|
+
integer or as a string.
|
|
36
|
+
|
|
37
|
+
The string values correspond to the following accuracy levels:
|
|
38
|
+
|
|
39
|
+
- "low": 3
|
|
40
|
+
- "middle": 5
|
|
41
|
+
- "high": 8
|
|
20
42
|
|
|
21
43
|
Returns
|
|
22
44
|
-------
|
|
@@ -38,26 +60,58 @@ def get_volume(tree: Tree):
|
|
|
38
60
|
|
|
39
61
|
We welcome additional representation methods through pull requests.
|
|
40
62
|
"""
|
|
63
|
+
|
|
64
|
+
if isinstance(accuracy, str):
|
|
65
|
+
accuracy = ACCURACY_LEVELS[accuracy]
|
|
66
|
+
|
|
67
|
+
assert 0 < accuracy <= 10
|
|
68
|
+
|
|
69
|
+
match method:
|
|
70
|
+
case "frustum_cone":
|
|
71
|
+
return _get_volume_frustum_cone(tree, accuracy=accuracy)
|
|
72
|
+
case _:
|
|
73
|
+
raise ValueError(f"Unsupported method: {method}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _get_volume_frustum_cone(tree: Tree, *, accuracy: int) -> float:
|
|
77
|
+
"""Get the volume of the tree using the frustum cone method.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
tree : Tree
|
|
82
|
+
Neuronal tree.
|
|
83
|
+
accuracy : int
|
|
84
|
+
1 : Sphere only
|
|
85
|
+
2 : Sphere and Frustum Cone
|
|
86
|
+
3 : Sphere, Frustum Cone, and intersection in single-branch
|
|
87
|
+
5 : Above and Sphere-Frustum Cone intersection in multi-branch
|
|
88
|
+
10 : Fully calculated by Monte Carlo method
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
if accuracy == 10:
|
|
92
|
+
return _get_volume_frustum_cone_mc_only(tree)
|
|
93
|
+
|
|
41
94
|
volume = 0.0
|
|
42
95
|
|
|
43
|
-
def leave(n: Tree.Node, children: List[
|
|
44
|
-
sphere =
|
|
45
|
-
cones = [
|
|
96
|
+
def leave(n: Tree.Node, children: List[VolSphere]) -> VolSphere:
|
|
97
|
+
sphere = VolSphere(n.xyz(), n.r)
|
|
98
|
+
cones = [VolFrustumCone(n.xyz(), n.r, c.center, c.radius) for c in children]
|
|
46
99
|
|
|
47
100
|
v = sphere.get_volume()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
101
|
+
if accuracy >= 2:
|
|
102
|
+
v += sum(fc.get_volume() for fc in cones)
|
|
103
|
+
|
|
104
|
+
if accuracy >= 3:
|
|
105
|
+
v -= sum(sphere.intersect(fc).get_volume() for fc in cones)
|
|
106
|
+
v -= sum(s.intersect(fc).get_volume() for s, fc in zip(children, cones))
|
|
107
|
+
v += sum(s.intersect(sphere).get_volume() for s in children)
|
|
108
|
+
|
|
109
|
+
if accuracy >= 5:
|
|
110
|
+
v -= sum(
|
|
111
|
+
cones[i].intersect(cones[j]).subtract(sphere).get_volume()
|
|
112
|
+
for i in range(len(cones))
|
|
113
|
+
for j in range(i + 1, len(cones))
|
|
114
|
+
)
|
|
61
115
|
|
|
62
116
|
nonlocal volume
|
|
63
117
|
volume += v
|
|
@@ -65,3 +119,34 @@ def get_volume(tree: Tree):
|
|
|
65
119
|
|
|
66
120
|
tree.traverse(leave=leave)
|
|
67
121
|
return volume
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _get_volume_frustum_cone_mc_only(tree: Tree) -> float:
|
|
125
|
+
if tree.number_of_nodes() == 0:
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
material = ColoredMaterial((1, 0, 0)).into()
|
|
129
|
+
scene = ObjectsScene()
|
|
130
|
+
scene.set_background((0, 0, 0))
|
|
131
|
+
|
|
132
|
+
def leave(n: Tree.Node, children: List[VolSphere]) -> VolSphere:
|
|
133
|
+
sphere = VolSphere(n.xyz(), n.r)
|
|
134
|
+
scene.add_object(SDFObject(sphere.sdf, material).into())
|
|
135
|
+
|
|
136
|
+
for c in children:
|
|
137
|
+
fc = VolFrustumCone(n.xyz(), n.r, c.center, c.radius)
|
|
138
|
+
scene.add_object(SDFObject(fc.sdf, material).into())
|
|
139
|
+
|
|
140
|
+
return sphere
|
|
141
|
+
|
|
142
|
+
tree.traverse(leave=leave)
|
|
143
|
+
scene.build_bvh()
|
|
144
|
+
|
|
145
|
+
# TODO: estimate the number of samples needed
|
|
146
|
+
n_samples = 100_000_000
|
|
147
|
+
|
|
148
|
+
vmin, vmax = scene.bounding_box()
|
|
149
|
+
sampler = UniformSampler(vmin, vmax)
|
|
150
|
+
data = sampler.sample(scene.into(), n_samples)
|
|
151
|
+
volume = data.sum() / n_samples * np.subtract(vmax, vmin).prod()
|
|
152
|
+
return volume
|
swcgeom/core/branch_tree.py
CHANGED
|
@@ -5,7 +5,6 @@ from typing import Dict, List
|
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
|
-
from typing_extensions import Self
|
|
9
8
|
|
|
10
9
|
from swcgeom.core.branch import Branch
|
|
11
10
|
from swcgeom.core.swc_utils import to_sub_topology
|
|
@@ -31,7 +30,7 @@ class BranchTree(Tree):
|
|
|
31
30
|
return self.branches[idx]
|
|
32
31
|
|
|
33
32
|
@classmethod
|
|
34
|
-
def from_tree(cls, tree: Tree) ->
|
|
33
|
+
def from_tree(cls, tree: Tree) -> "BranchTree":
|
|
35
34
|
"""Generating a branch tree from tree."""
|
|
36
35
|
|
|
37
36
|
branches = tree.get_branches()
|
|
@@ -56,6 +55,6 @@ class BranchTree(Tree):
|
|
|
56
55
|
return branch_tree
|
|
57
56
|
|
|
58
57
|
@classmethod
|
|
59
|
-
def from_data_frame(cls, df: pd.DataFrame, *args, **kwargs) ->
|
|
58
|
+
def from_data_frame(cls, df: pd.DataFrame, *args, **kwargs) -> "BranchTree":
|
|
60
59
|
tree = super().from_data_frame(df, *args, **kwargs)
|
|
61
60
|
return cls.from_tree(tree)
|
swcgeom/core/population.py
CHANGED
|
@@ -17,7 +17,6 @@ from typing import (
|
|
|
17
17
|
|
|
18
18
|
import numpy as np
|
|
19
19
|
import numpy.typing as npt
|
|
20
|
-
from typing_extensions import Self
|
|
21
20
|
|
|
22
21
|
from swcgeom.core.swc import eswc_cols
|
|
23
22
|
from swcgeom.core.tree import Tree
|
|
@@ -155,7 +154,7 @@ class Population:
|
|
|
155
154
|
return f"Neuron population in '{self.root}'"
|
|
156
155
|
|
|
157
156
|
@classmethod
|
|
158
|
-
def from_swc(cls, root: str, ext: str = ".swc", **kwargs) ->
|
|
157
|
+
def from_swc(cls, root: str, ext: str = ".swc", **kwargs) -> "Population":
|
|
159
158
|
if not os.path.exists(root):
|
|
160
159
|
raise FileNotFoundError(
|
|
161
160
|
f"the root does not refers to an existing directory: {root}"
|
|
@@ -171,7 +170,7 @@ class Population:
|
|
|
171
170
|
ext: str = ".eswc",
|
|
172
171
|
extra_cols: Optional[Iterable[str]] = None,
|
|
173
172
|
**kwargs,
|
|
174
|
-
) ->
|
|
173
|
+
) -> "Population":
|
|
175
174
|
extra_cols = list(extra_cols) if extra_cols is not None else []
|
|
176
175
|
extra_cols.extend(k for k, t in eswc_cols)
|
|
177
176
|
return cls.from_swc(root, ext, extra_cols=extra_cols, **kwargs)
|
|
@@ -244,7 +243,7 @@ class Populations:
|
|
|
244
243
|
check_same: bool = False,
|
|
245
244
|
labels: Optional[Iterable[str]] = None,
|
|
246
245
|
**kwargs,
|
|
247
|
-
) ->
|
|
246
|
+
) -> "Populations":
|
|
248
247
|
"""Get population from dirs.
|
|
249
248
|
|
|
250
249
|
Parameters
|
|
@@ -268,7 +267,7 @@ class Populations:
|
|
|
268
267
|
|
|
269
268
|
fs = [inter for _ in roots]
|
|
270
269
|
elif check_same:
|
|
271
|
-
assert
|
|
270
|
+
assert [fs[0] == a for a in fs[1:]], "not the same among populations"
|
|
272
271
|
|
|
273
272
|
populations = [
|
|
274
273
|
Population(
|
|
@@ -276,7 +275,7 @@ class Populations:
|
|
|
276
275
|
)
|
|
277
276
|
for i, d in enumerate(roots)
|
|
278
277
|
]
|
|
279
|
-
return
|
|
278
|
+
return Populations(populations, labels=labels)
|
|
280
279
|
|
|
281
280
|
@classmethod
|
|
282
281
|
def from_eswc(
|
|
@@ -286,7 +285,7 @@ class Populations:
|
|
|
286
285
|
*,
|
|
287
286
|
ext: str = ".eswc",
|
|
288
287
|
**kwargs,
|
|
289
|
-
) ->
|
|
288
|
+
) -> "Populations":
|
|
290
289
|
extra_cols = list(extra_cols) if extra_cols is not None else []
|
|
291
290
|
extra_cols.extend(k for k, t in eswc_cols)
|
|
292
291
|
return cls.from_swc(roots, extra_cols=extra_cols, ext=ext, **kwargs)
|
|
@@ -1,155 +1,26 @@
|
|
|
1
|
-
"""Assemble lines to swc.
|
|
1
|
+
"""Assemble lines to swc.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Notes
|
|
4
|
+
-----
|
|
5
|
+
This module is deprecated, please use `~.transforms.LinesToTree`
|
|
6
|
+
instead.
|
|
7
|
+
"""
|
|
6
8
|
|
|
7
|
-
import numpy as np
|
|
8
|
-
import pandas as pd
|
|
9
|
-
|
|
10
|
-
from swcgeom.core.swc_utils.base import SWCNames, get_names
|
|
11
|
-
from swcgeom.core.swc_utils.normalizer import link_roots_to_nearest_, sort_nodes_
|
|
12
9
|
|
|
13
10
|
__all__ = ["assemble_lines", "try_assemble_lines"]
|
|
14
11
|
|
|
15
12
|
|
|
16
|
-
def assemble_lines(*args, **kwargs)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Assemble all the lines into a set of subtrees, and then connect
|
|
20
|
-
them.
|
|
21
|
-
|
|
22
|
-
Parameters
|
|
23
|
-
----------
|
|
24
|
-
lines : List of ~pd.DataFrame
|
|
25
|
-
An array of tables containing a line, columns should follwing
|
|
26
|
-
the swc.
|
|
27
|
-
**kwargs
|
|
28
|
-
Forwarding to `try_assemble_lines`
|
|
29
|
-
|
|
30
|
-
Returns
|
|
31
|
-
-------
|
|
32
|
-
tree : ~pd.DataFrame
|
|
33
|
-
|
|
34
|
-
See Also
|
|
35
|
-
--------
|
|
36
|
-
~swcgeom.core.swc_utils.try_assemble_lines
|
|
37
|
-
"""
|
|
38
|
-
warnings.warn(
|
|
13
|
+
def assemble_lines(*args, **kwargs):
|
|
14
|
+
raise DeprecationWarning(
|
|
39
15
|
"`assemble_lines` has been replaced by "
|
|
40
16
|
"`~.transforms.LinesToTree` because it can be easy assemble "
|
|
41
|
-
"with other tansformations,
|
|
42
|
-
"version.",
|
|
43
|
-
DeprecationWarning,
|
|
17
|
+
"with other tansformations.",
|
|
44
18
|
)
|
|
45
|
-
return assemble_lines_impl(*args, **kwargs)
|
|
46
|
-
|
|
47
19
|
|
|
48
|
-
def try_assemble_lines(*args, **kwargs) -> Tuple[pd.DataFrame, List[pd.DataFrame]]:
|
|
49
|
-
"""Trying assemble lines to a tree.
|
|
50
20
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
the tree, repeat until there are no line to merge, return tree and
|
|
54
|
-
the remaining lines.
|
|
55
|
-
|
|
56
|
-
Parameters
|
|
57
|
-
----------
|
|
58
|
-
lines : List of ~pd.DataFrame
|
|
59
|
-
An array of tables containing a line, columns should follwing
|
|
60
|
-
the swc.
|
|
61
|
-
undirected : bool, default `True`
|
|
62
|
-
Both ends of a line can be considered connection point. If
|
|
63
|
-
`False`, only the starting point.
|
|
64
|
-
thre : float, default `0.2`
|
|
65
|
-
Connection threshold.
|
|
66
|
-
id_offset : int, default `0`
|
|
67
|
-
The offset of the line node id.
|
|
68
|
-
sort_nodes : bool, default `True`
|
|
69
|
-
sort nodes of subtree
|
|
70
|
-
names : SWCNames, optional
|
|
71
|
-
|
|
72
|
-
Returns
|
|
73
|
-
-------
|
|
74
|
-
tree : ~pd.DataFrame
|
|
75
|
-
remaining_lines : List of ~pd.DataFrame
|
|
76
|
-
"""
|
|
77
|
-
warnings.warn(
|
|
21
|
+
def try_assemble_lines(*args, **kwargs):
|
|
22
|
+
raise DeprecationWarning(
|
|
78
23
|
"`try_assemble_lines` has been replaced by "
|
|
79
24
|
"`~.transforms.LinesToTree` because it can be easy assemble "
|
|
80
|
-
"with other tansformations,
|
|
81
|
-
"version.",
|
|
82
|
-
DeprecationWarning,
|
|
25
|
+
"with other tansformations.",
|
|
83
26
|
)
|
|
84
|
-
return try_assemble_lines_impl(*args, **kwargs)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
# TODO: move the following codes to `transforms` module
|
|
88
|
-
|
|
89
|
-
EPS = 1e-5
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def assemble_lines_impl(lines: Iterable[pd.DataFrame], **kwargs) -> pd.DataFrame:
|
|
93
|
-
tree, lines = try_assemble_lines_impl(lines, sort_nodes=False, **kwargs)
|
|
94
|
-
while len(lines) > 0:
|
|
95
|
-
t, lines = try_assemble_lines_impl(
|
|
96
|
-
lines, id_offset=len(tree), sort_nodes=False, **kwargs
|
|
97
|
-
)
|
|
98
|
-
tree = pd.concat([tree, t])
|
|
99
|
-
|
|
100
|
-
tree = tree.reset_index()
|
|
101
|
-
link_roots_to_nearest_(tree)
|
|
102
|
-
sort_nodes_(tree)
|
|
103
|
-
return tree
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def try_assemble_lines_impl( # pylint: disable=too-many-arguments
|
|
107
|
-
lines: Iterable[pd.DataFrame],
|
|
108
|
-
undirected: bool = True,
|
|
109
|
-
thre: float = 0.2,
|
|
110
|
-
id_offset: int = 0,
|
|
111
|
-
sort_nodes: bool = True,
|
|
112
|
-
*,
|
|
113
|
-
names: Optional[SWCNames] = None,
|
|
114
|
-
) -> Tuple[pd.DataFrame, List[pd.DataFrame]]:
|
|
115
|
-
names = get_names(names)
|
|
116
|
-
lines = copy(list(lines))
|
|
117
|
-
|
|
118
|
-
tree = lines[0]
|
|
119
|
-
tree[names.id] = id_offset + np.arange(len(tree))
|
|
120
|
-
tree[names.pid] = tree[names.id] - 1
|
|
121
|
-
tree.at[0, names.pid] = -1
|
|
122
|
-
del lines[0]
|
|
123
|
-
|
|
124
|
-
while True:
|
|
125
|
-
for i, line in enumerate(lines):
|
|
126
|
-
for p in [0, -1] if undirected else [0]:
|
|
127
|
-
xyz = [names.x, names.y, names.z]
|
|
128
|
-
vs = tree[xyz] - line.iloc[p][xyz]
|
|
129
|
-
dis = np.linalg.norm(vs, axis=1)
|
|
130
|
-
ind = np.argmin(dis)
|
|
131
|
-
if dis[ind] > thre:
|
|
132
|
-
continue
|
|
133
|
-
|
|
134
|
-
if dis[ind] < EPS:
|
|
135
|
-
line = line.drop((p + len(line)) % len(line)).reset_index(drop=True)
|
|
136
|
-
|
|
137
|
-
line[names.id] = id_offset + len(tree) + np.arange(len(line))
|
|
138
|
-
line[names.pid] = line[names.id] + (-1 if p == 0 else 1)
|
|
139
|
-
line.at[(p + len(line)) % len(line), names.pid] = tree.iloc[ind][
|
|
140
|
-
names.id
|
|
141
|
-
]
|
|
142
|
-
tree = pd.concat([tree, line])
|
|
143
|
-
del lines[i]
|
|
144
|
-
break
|
|
145
|
-
else:
|
|
146
|
-
continue
|
|
147
|
-
|
|
148
|
-
break
|
|
149
|
-
else:
|
|
150
|
-
break
|
|
151
|
-
|
|
152
|
-
if sort_nodes:
|
|
153
|
-
sort_nodes_(tree)
|
|
154
|
-
|
|
155
|
-
return tree, lines
|
|
@@ -27,8 +27,8 @@ def to_sub_topology(sub: Topology) -> Tuple[Topology, npt.NDArray[np.int32]]:
|
|
|
27
27
|
Returns
|
|
28
28
|
-------
|
|
29
29
|
sub_topology : Topology
|
|
30
|
-
|
|
31
|
-
Map from new id to
|
|
30
|
+
mapping : List of int
|
|
31
|
+
Map from new id to old id.
|
|
32
32
|
|
|
33
33
|
See Also
|
|
34
34
|
--------
|
swcgeom/core/tree.py
CHANGED
|
@@ -20,7 +20,6 @@ from typing import (
|
|
|
20
20
|
import numpy as np
|
|
21
21
|
import numpy.typing as npt
|
|
22
22
|
import pandas as pd
|
|
23
|
-
from typing_extensions import Self
|
|
24
23
|
|
|
25
24
|
from swcgeom.core.branch import Branch
|
|
26
25
|
from swcgeom.core.node import Node
|
|
@@ -28,7 +27,7 @@ from swcgeom.core.path import Path
|
|
|
28
27
|
from swcgeom.core.segment import Segment, Segments
|
|
29
28
|
from swcgeom.core.swc import DictSWC, eswc_cols
|
|
30
29
|
from swcgeom.core.swc_utils import SWCNames, get_names, read_swc, traverse
|
|
31
|
-
from swcgeom.core.tree_utils_impl import get_subtree_impl
|
|
30
|
+
from swcgeom.core.tree_utils_impl import Mapping, get_subtree_impl
|
|
32
31
|
from swcgeom.utils import PathOrIO, padding1d
|
|
33
32
|
|
|
34
33
|
__all__ = ["Tree"]
|
|
@@ -73,9 +72,18 @@ class Tree(DictSWC):
|
|
|
73
72
|
"""The end-to-end straight-line distance to soma."""
|
|
74
73
|
return self.distance(self.attach.soma())
|
|
75
74
|
|
|
76
|
-
def subtree(self) -> "Tree":
|
|
77
|
-
"""Get subtree from node.
|
|
78
|
-
|
|
75
|
+
def subtree(self, *, out_mapping: Optional[Mapping] = None) -> "Tree":
|
|
76
|
+
"""Get subtree from node.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
out_mapping : List of int or Dict[int, int], optional
|
|
81
|
+
Map from new id to old id.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
n_nodes, ndata, source, names = get_subtree_impl(
|
|
85
|
+
self.attach, self.id, out_mapping=out_mapping
|
|
86
|
+
)
|
|
79
87
|
return Tree(n_nodes, **ndata, source=source, names=names)
|
|
80
88
|
|
|
81
89
|
def is_root(self) -> bool:
|
|
@@ -260,11 +268,11 @@ class Tree(DictSWC):
|
|
|
260
268
|
paths = self.traverse(enter=assign_path, leave=collect_path)
|
|
261
269
|
return [self.Path(self, idx) for idx in paths]
|
|
262
270
|
|
|
263
|
-
def get_neurites(self, type_check: bool = True) -> Iterable[
|
|
271
|
+
def get_neurites(self, type_check: bool = True) -> Iterable["Tree"]:
|
|
264
272
|
"""Get neurites from soma."""
|
|
265
273
|
return (n.subtree() for n in self.soma(type_check).children())
|
|
266
274
|
|
|
267
|
-
def get_dendrites(self, type_check: bool = True) -> Iterable[
|
|
275
|
+
def get_dendrites(self, type_check: bool = True) -> Iterable["Tree"]:
|
|
268
276
|
"""Get dendrites."""
|
|
269
277
|
types = [self.types.apical_dendrite, self.types.basal_dendrite]
|
|
270
278
|
children = self.soma(type_check).children()
|
|
@@ -312,15 +320,14 @@ class Tree(DictSWC):
|
|
|
312
320
|
"""Get length of tree."""
|
|
313
321
|
return sum(s.length() for s in self.get_segments())
|
|
314
322
|
|
|
315
|
-
@
|
|
323
|
+
@staticmethod
|
|
316
324
|
def from_data_frame(
|
|
317
|
-
cls,
|
|
318
325
|
df: pd.DataFrame,
|
|
319
326
|
source: str = "",
|
|
320
327
|
*,
|
|
321
328
|
comments: Optional[Iterable[str]] = None,
|
|
322
329
|
names: Optional[SWCNames] = None,
|
|
323
|
-
) ->
|
|
330
|
+
) -> "Tree":
|
|
324
331
|
"""Read neuron tree from data frame."""
|
|
325
332
|
names = get_names(names)
|
|
326
333
|
tree = Tree(
|
|
@@ -333,7 +340,7 @@ class Tree(DictSWC):
|
|
|
333
340
|
return tree
|
|
334
341
|
|
|
335
342
|
@classmethod
|
|
336
|
-
def from_swc(cls, swc_file: PathOrIO, **kwargs) ->
|
|
343
|
+
def from_swc(cls, swc_file: PathOrIO, **kwargs) -> "Tree":
|
|
337
344
|
"""Read neuron tree from swc file.
|
|
338
345
|
|
|
339
346
|
See Also
|
|
@@ -352,7 +359,7 @@ class Tree(DictSWC):
|
|
|
352
359
|
@classmethod
|
|
353
360
|
def from_eswc(
|
|
354
361
|
cls, swc_file: str, extra_cols: Optional[List[str]] = None, **kwargs
|
|
355
|
-
) ->
|
|
362
|
+
) -> "Tree":
|
|
356
363
|
"""Read neuron tree from eswc file.
|
|
357
364
|
|
|
358
365
|
See Also
|
swcgeom/core/tree_utils.py
CHANGED
|
@@ -17,7 +17,7 @@ from swcgeom.core.swc_utils import (
|
|
|
17
17
|
to_sub_topology,
|
|
18
18
|
)
|
|
19
19
|
from swcgeom.core.tree import Tree
|
|
20
|
-
from swcgeom.core.tree_utils_impl import get_subtree_impl, to_subtree_impl
|
|
20
|
+
from swcgeom.core.tree_utils_impl import Mapping, get_subtree_impl, to_subtree_impl
|
|
21
21
|
|
|
22
22
|
__all__ = [
|
|
23
23
|
"sort_tree",
|
|
@@ -128,7 +128,12 @@ def to_sub_tree(swc_like: SWCLike, sub: Topology) -> Tuple[Tree, Dict[int, int]]
|
|
|
128
128
|
return subtree, id_map
|
|
129
129
|
|
|
130
130
|
|
|
131
|
-
def to_subtree(
|
|
131
|
+
def to_subtree(
|
|
132
|
+
swc_like: SWCLike,
|
|
133
|
+
removals: Iterable[int],
|
|
134
|
+
*,
|
|
135
|
+
out_mapping: Optional[Mapping] = None,
|
|
136
|
+
) -> Tree:
|
|
132
137
|
"""Create subtree from origin tree.
|
|
133
138
|
|
|
134
139
|
Parameters
|
|
@@ -136,17 +141,24 @@ def to_subtree(swc_like: SWCLike, removals: Iterable[int]) -> Tree:
|
|
|
136
141
|
swc_like : SWCLike
|
|
137
142
|
removals : List of int
|
|
138
143
|
A list of id of nodes to be removed.
|
|
144
|
+
out_mapping: List of int or Dict[int, int], optional
|
|
145
|
+
Map new id to old id.
|
|
139
146
|
"""
|
|
147
|
+
|
|
140
148
|
new_ids = swc_like.id().copy()
|
|
141
149
|
for i in removals:
|
|
142
150
|
new_ids[i] = REMOVAL
|
|
143
151
|
|
|
144
152
|
sub = propagate_removal((new_ids, swc_like.pid()))
|
|
145
|
-
n_nodes, ndata, source, names = to_subtree_impl(
|
|
153
|
+
n_nodes, ndata, source, names = to_subtree_impl(
|
|
154
|
+
swc_like, sub, out_mapping=out_mapping
|
|
155
|
+
)
|
|
146
156
|
return Tree(n_nodes, **ndata, source=source, names=names)
|
|
147
157
|
|
|
148
158
|
|
|
149
|
-
def get_subtree(
|
|
159
|
+
def get_subtree(
|
|
160
|
+
swc_like: SWCLike, n: int, *, out_mapping: Optional[Mapping] = None
|
|
161
|
+
) -> Tree:
|
|
150
162
|
"""Get subtree rooted at n.
|
|
151
163
|
|
|
152
164
|
Parameters
|
|
@@ -154,8 +166,13 @@ def get_subtree(swc_like: SWCLike, n: int) -> Tree:
|
|
|
154
166
|
swc_like : SWCLike
|
|
155
167
|
n : int
|
|
156
168
|
Id of the root of the subtree.
|
|
169
|
+
out_mapping: List of int or Dict[int, int], optional
|
|
170
|
+
Map new id to old id.
|
|
157
171
|
"""
|
|
158
|
-
|
|
172
|
+
|
|
173
|
+
n_nodes, ndata, source, names = get_subtree_impl(
|
|
174
|
+
swc_like, n, out_mapping=out_mapping
|
|
175
|
+
)
|
|
159
176
|
return Tree(n_nodes, **ndata, source=source, names=names)
|
|
160
177
|
|
|
161
178
|
|
|
@@ -171,6 +188,7 @@ def redirect_tree(tree: Tree, new_root: int, sort: bool = True) -> Tree:
|
|
|
171
188
|
sort : bool, default `True`
|
|
172
189
|
If true, sort indices of nodes after redirect.
|
|
173
190
|
"""
|
|
191
|
+
|
|
174
192
|
tree = tree.copy()
|
|
175
193
|
path = [tree.node(new_root)]
|
|
176
194
|
while (p := path[-1].parent()) is not None:
|
swcgeom/core/tree_utils_impl.py
CHANGED
|
@@ -5,7 +5,7 @@ Notes
|
|
|
5
5
|
Do not import `Tree` and keep this file minimized.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
from typing import Any, Dict, Tuple
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
9
9
|
|
|
10
10
|
import numpy as np
|
|
11
11
|
import numpy.typing as npt
|
|
@@ -15,10 +15,13 @@ from swcgeom.core.swc_utils import Topology, to_sub_topology, traverse
|
|
|
15
15
|
|
|
16
16
|
__all__ = ["get_subtree_impl", "to_subtree_impl"]
|
|
17
17
|
|
|
18
|
+
Mapping = Dict[int, int] | List[int]
|
|
18
19
|
TreeArgs = Tuple[int, Dict[str, npt.NDArray[Any]], str, SWCNames]
|
|
19
20
|
|
|
20
21
|
|
|
21
|
-
def get_subtree_impl(
|
|
22
|
+
def get_subtree_impl(
|
|
23
|
+
swc_like: SWCLike, n: int, *, out_mapping: Optional[Mapping] = None
|
|
24
|
+
) -> TreeArgs:
|
|
22
25
|
ids = []
|
|
23
26
|
topo = (swc_like.id(), swc_like.pid())
|
|
24
27
|
traverse(topo, enter=lambda n, _: ids.append(n), root=n)
|
|
@@ -26,14 +29,27 @@ def get_subtree_impl(swc_like: SWCLike, n: int) -> TreeArgs:
|
|
|
26
29
|
sub_ids = np.array(ids, dtype=np.int32)
|
|
27
30
|
sub_pid = swc_like.pid()[sub_ids]
|
|
28
31
|
sub_pid[0] = -1
|
|
29
|
-
return to_subtree_impl(swc_like, (sub_ids, sub_pid))
|
|
32
|
+
return to_subtree_impl(swc_like, (sub_ids, sub_pid), out_mapping=out_mapping)
|
|
30
33
|
|
|
31
34
|
|
|
32
|
-
def to_subtree_impl(
|
|
33
|
-
|
|
35
|
+
def to_subtree_impl(
|
|
36
|
+
swc_like: SWCLike,
|
|
37
|
+
sub: Topology,
|
|
38
|
+
*,
|
|
39
|
+
out_mapping: Optional[Mapping] = None,
|
|
40
|
+
) -> TreeArgs:
|
|
41
|
+
(new_id, new_pid), mapping = to_sub_topology(sub)
|
|
34
42
|
|
|
35
43
|
n_nodes = new_id.shape[0]
|
|
36
|
-
ndata = {k: swc_like.get_ndata(k)[
|
|
44
|
+
ndata = {k: swc_like.get_ndata(k)[mapping].copy() for k in swc_like.keys()}
|
|
37
45
|
ndata.update(id=new_id, pid=new_pid)
|
|
38
46
|
|
|
47
|
+
if isinstance(out_mapping, list):
|
|
48
|
+
out_mapping.clear()
|
|
49
|
+
out_mapping.extend(mapping)
|
|
50
|
+
elif isinstance(out_mapping, dict):
|
|
51
|
+
out_mapping.clear()
|
|
52
|
+
for new_id, old_id in enumerate(mapping):
|
|
53
|
+
out_mapping[new_id] = old_id # returning a dict may leads to bad perf
|
|
54
|
+
|
|
39
55
|
return n_nodes, ndata, swc_like.source, swc_like.names
|