swcgeom 0.16.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 +48 -12
- swcgeom/analysis/sholl.py +25 -28
- 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 +31 -12
- swcgeom/core/branch.py +19 -3
- swcgeom/core/branch_tree.py +18 -4
- swcgeom/core/compartment.py +18 -2
- swcgeom/core/node.py +32 -3
- swcgeom/core/path.py +21 -9
- swcgeom/core/population.py +58 -29
- swcgeom/core/swc.py +26 -10
- swcgeom/core/swc_utils/__init__.py +21 -7
- swcgeom/core/swc_utils/assembler.py +15 -0
- swcgeom/core/swc_utils/base.py +23 -17
- swcgeom/core/swc_utils/checker.py +19 -12
- 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 +56 -40
- swcgeom/core/tree_utils.py +28 -17
- swcgeom/core/tree_utils_impl.py +18 -3
- swcgeom/images/__init__.py +17 -2
- swcgeom/images/augmentation.py +18 -3
- swcgeom/images/contrast.py +15 -0
- swcgeom/images/folder.py +27 -26
- swcgeom/images/io.py +94 -117
- swcgeom/transforms/__init__.py +28 -12
- 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 +15 -0
- swcgeom/transforms/image_stack.py +36 -9
- swcgeom/transforms/images.py +121 -14
- swcgeom/transforms/mst.py +15 -0
- swcgeom/transforms/neurolucida_asc.py +20 -7
- swcgeom/transforms/path.py +15 -0
- swcgeom/transforms/population.py +16 -3
- swcgeom/transforms/tree.py +84 -30
- swcgeom/transforms/tree_assembler.py +23 -7
- swcgeom/utils/__init__.py +27 -12
- 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 +35 -23
- swcgeom/utils/numpy_helper.py +15 -0
- swcgeom/utils/plotter_2d.py +27 -6
- swcgeom/utils/plotter_3d.py +48 -0
- swcgeom/utils/renderer.py +21 -6
- swcgeom/utils/sdf.py +19 -7
- swcgeom/utils/solid_geometry.py +16 -3
- swcgeom/utils/transforms.py +17 -4
- swcgeom/utils/volumetric_object.py +23 -10
- {swcgeom-0.16.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
- {swcgeom-0.16.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
- swcgeom-0.18.3.dist-info/RECORD +67 -0
- {swcgeom-0.16.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.16.0.dist-info/RECORD +0 -67
- {swcgeom-0.16.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
swcgeom/__init__.py
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
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
|
+
|
|
1
15
|
"""A neuron geometry library for swc format."""
|
|
2
16
|
|
|
3
17
|
from swcgeom import analysis, core, images, transforms
|
|
4
|
-
from swcgeom._version import __version__, __version_tuple__
|
|
5
18
|
from swcgeom.analysis import draw
|
|
6
19
|
from swcgeom.core import BranchTree, Population, Populations, Tree
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"analysis",
|
|
23
|
+
"core",
|
|
24
|
+
"images",
|
|
25
|
+
"transforms",
|
|
26
|
+
"draw",
|
|
27
|
+
"BranchTree",
|
|
28
|
+
"Population",
|
|
29
|
+
"Populations",
|
|
30
|
+
"Tree",
|
|
31
|
+
]
|
swcgeom/analysis/__init__.py
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
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
|
"""Analysis for neuron trees."""
|
|
2
17
|
|
|
3
|
-
from swcgeom.analysis.
|
|
4
|
-
from swcgeom.analysis.
|
|
5
|
-
from swcgeom.analysis.
|
|
6
|
-
from swcgeom.analysis.
|
|
7
|
-
from swcgeom.analysis.
|
|
8
|
-
from swcgeom.analysis.
|
|
9
|
-
from swcgeom.analysis.visualization import *
|
|
10
|
-
from swcgeom.analysis.volume import *
|
|
18
|
+
from swcgeom.analysis.feature_extractor import * # noqa: F403
|
|
19
|
+
from swcgeom.analysis.features import * # noqa: F403
|
|
20
|
+
from swcgeom.analysis.sholl import * # noqa: F403
|
|
21
|
+
from swcgeom.analysis.trunk import * # noqa: F403
|
|
22
|
+
from swcgeom.analysis.visualization import * # noqa: F403
|
|
23
|
+
from swcgeom.analysis.volume import * # noqa: F403
|
|
@@ -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
|
"""Easy way to compute and visualize common features for feature.
|
|
2
17
|
|
|
3
18
|
Notes
|
|
@@ -7,23 +22,24 @@ naming specification.
|
|
|
7
22
|
"""
|
|
8
23
|
|
|
9
24
|
from abc import ABC, abstractmethod
|
|
25
|
+
from collections.abc import Callable
|
|
10
26
|
from functools import cached_property
|
|
11
27
|
from itertools import chain
|
|
12
28
|
from os.path import basename
|
|
13
|
-
from typing import Any,
|
|
29
|
+
from typing import Any, Literal, overload
|
|
14
30
|
|
|
15
31
|
import numpy as np
|
|
16
32
|
import numpy.typing as npt
|
|
17
33
|
import seaborn as sns
|
|
18
34
|
from matplotlib.axes import Axes
|
|
19
35
|
|
|
20
|
-
from swcgeom.analysis.
|
|
21
|
-
|
|
22
|
-
|
|
36
|
+
from swcgeom.analysis.features import (
|
|
37
|
+
BranchFeatures,
|
|
38
|
+
FurcationFeatures,
|
|
23
39
|
NodeFeatures,
|
|
40
|
+
PathFeatures,
|
|
24
41
|
TipFeatures,
|
|
25
42
|
)
|
|
26
|
-
from swcgeom.analysis.path_features import PathFeatures
|
|
27
43
|
from swcgeom.analysis.sholl import Sholl
|
|
28
44
|
from swcgeom.analysis.volume import get_volume
|
|
29
45
|
from swcgeom.core import Population, Populations, Tree
|
|
@@ -39,6 +55,9 @@ Feature = Literal[
|
|
|
39
55
|
"node_count",
|
|
40
56
|
"node_radial_distance",
|
|
41
57
|
"node_branch_order",
|
|
58
|
+
# furcation nodes
|
|
59
|
+
"furcation_count",
|
|
60
|
+
"furcation_radial_distance",
|
|
42
61
|
# bifurcation nodes
|
|
43
62
|
"bifurcation_count",
|
|
44
63
|
"bifurcation_radial_distance",
|
|
@@ -54,9 +73,9 @@ Feature = Literal[
|
|
|
54
73
|
]
|
|
55
74
|
|
|
56
75
|
NDArrayf32 = npt.NDArray[np.float32]
|
|
57
|
-
FeatAndKwargs = Feature |
|
|
76
|
+
FeatAndKwargs = Feature | tuple[Feature, dict[str, Any]]
|
|
58
77
|
|
|
59
|
-
Feature1D = set(["length", "volume", "node_count", "
|
|
78
|
+
Feature1D = set(["length", "volume", "node_count", "furcation_count", "tip_count"])
|
|
60
79
|
|
|
61
80
|
|
|
62
81
|
class Features:
|
|
@@ -69,7 +88,7 @@ class Features:
|
|
|
69
88
|
@cached_property
|
|
70
89
|
def node_features(self) -> NodeFeatures: return NodeFeatures(self.tree)
|
|
71
90
|
@cached_property
|
|
72
|
-
def
|
|
91
|
+
def furcation_features(self) -> FurcationFeatures: return FurcationFeatures(self.node_features)
|
|
73
92
|
@cached_property
|
|
74
93
|
def tip_features(self) -> TipFeatures: return TipFeatures(self.node_features)
|
|
75
94
|
@cached_property
|
|
@@ -121,9 +140,9 @@ class FeatureExtractor(ABC):
|
|
|
121
140
|
@overload
|
|
122
141
|
def get(self, feature: Feature, **kwargs) -> NDArrayf32: ...
|
|
123
142
|
@overload
|
|
124
|
-
def get(self, feature:
|
|
143
|
+
def get(self, feature: list[FeatAndKwargs]) -> list[NDArrayf32]: ...
|
|
125
144
|
@overload
|
|
126
|
-
def get(self, feature:
|
|
145
|
+
def get(self, feature: dict[Feature, dict[str, Any]]) -> dict[str, NDArrayf32]: ...
|
|
127
146
|
# fmt:on
|
|
128
147
|
def get(self, feature, **kwargs):
|
|
129
148
|
"""Get feature.
|
|
@@ -168,7 +187,7 @@ class FeatureExtractor(ABC):
|
|
|
168
187
|
|
|
169
188
|
# Custom Plots
|
|
170
189
|
|
|
171
|
-
def plot_node_branch_order(self, feature_kwargs:
|
|
190
|
+
def plot_node_branch_order(self, feature_kwargs: dict[str, Any], **kwargs) -> Axes:
|
|
172
191
|
vals = self._get("node_branch_order", **feature_kwargs)
|
|
173
192
|
bin_edges = np.arange(int(np.ceil(vals.max() + 1))) + 0.5
|
|
174
193
|
return self._plot_histogram_impl(vals, bin_edges, **kwargs)
|
|
@@ -213,6 +232,12 @@ class FeatureExtractor(ABC):
|
|
|
213
232
|
) -> Axes:
|
|
214
233
|
raise NotImplementedError()
|
|
215
234
|
|
|
235
|
+
def get_bifurcation_count(self, **kwargs):
|
|
236
|
+
raise DeprecationWarning("Use `furcation_count` instead.")
|
|
237
|
+
|
|
238
|
+
def get_bifurcation_radial_distance(self, **kwargs):
|
|
239
|
+
raise DeprecationWarning("Use `furcation_radial_distance` instead.")
|
|
240
|
+
|
|
216
241
|
|
|
217
242
|
class TreeFeatureExtractor(FeatureExtractor):
|
|
218
243
|
"""Extract feature from tree."""
|
|
@@ -234,7 +259,7 @@ class TreeFeatureExtractor(FeatureExtractor):
|
|
|
234
259
|
|
|
235
260
|
def plot_sholl(
|
|
236
261
|
self,
|
|
237
|
-
feature_kwargs:
|
|
262
|
+
feature_kwargs: dict[str, Any], # pylint: disable=unused-argument
|
|
238
263
|
**kwargs,
|
|
239
264
|
) -> Axes:
|
|
240
265
|
_, ax = self._features.sholl.plot(**kwargs)
|
|
@@ -264,7 +289,7 @@ class PopulationFeatureExtractor(FeatureExtractor):
|
|
|
264
289
|
"""Extract features from population."""
|
|
265
290
|
|
|
266
291
|
_population: Population
|
|
267
|
-
_features:
|
|
292
|
+
_features: list[Features]
|
|
268
293
|
|
|
269
294
|
def __init__(self, population: Population) -> None:
|
|
270
295
|
super().__init__()
|
|
@@ -279,7 +304,7 @@ class PopulationFeatureExtractor(FeatureExtractor):
|
|
|
279
304
|
|
|
280
305
|
# Custom Plots
|
|
281
306
|
|
|
282
|
-
def plot_sholl(self, feature_kwargs:
|
|
307
|
+
def plot_sholl(self, feature_kwargs: dict[str, Any], **kwargs) -> Axes:
|
|
283
308
|
vals, rs = self._get_sholl_impl(**feature_kwargs)
|
|
284
309
|
ax = self._lineplot(xs=rs, ys=vals.flatten(), **kwargs)
|
|
285
310
|
ax.set_ylabel("Count of Intersections")
|
|
@@ -295,7 +320,7 @@ class PopulationFeatureExtractor(FeatureExtractor):
|
|
|
295
320
|
|
|
296
321
|
def _get_sholl_impl(
|
|
297
322
|
self, steps: int = 20, **kwargs
|
|
298
|
-
) ->
|
|
323
|
+
) -> tuple[NDArrayf32, NDArrayf32]:
|
|
299
324
|
rmax = max(t.sholl.rmax for t in self._features)
|
|
300
325
|
rs = Sholl.get_rs(rmax=rmax, steps=steps)
|
|
301
326
|
vals = self._get_impl("sholl", steps=rs, **kwargs)
|
|
@@ -333,7 +358,7 @@ class PopulationsFeatureExtractor(FeatureExtractor):
|
|
|
333
358
|
"""Extract feature from population."""
|
|
334
359
|
|
|
335
360
|
_populations: Populations
|
|
336
|
-
_features:
|
|
361
|
+
_features: list[list[Features]]
|
|
337
362
|
|
|
338
363
|
def __init__(self, populations: Populations) -> None:
|
|
339
364
|
super().__init__()
|
|
@@ -348,7 +373,7 @@ class PopulationsFeatureExtractor(FeatureExtractor):
|
|
|
348
373
|
|
|
349
374
|
# Custom Plots
|
|
350
375
|
|
|
351
|
-
def plot_sholl(self, feature_kwargs:
|
|
376
|
+
def plot_sholl(self, feature_kwargs: dict[str, Any], **kwargs) -> Axes:
|
|
352
377
|
vals, rs = self._get_sholl_impl(**feature_kwargs)
|
|
353
378
|
ax = self._lineplot(xs=rs, ys=vals, **kwargs)
|
|
354
379
|
ax.set_ylabel("Count of Intersections")
|
|
@@ -369,7 +394,7 @@ class PopulationsFeatureExtractor(FeatureExtractor):
|
|
|
369
394
|
|
|
370
395
|
def _get_sholl_impl(
|
|
371
396
|
self, steps: int = 20, **kwargs
|
|
372
|
-
) ->
|
|
397
|
+
) -> tuple[NDArrayf32, NDArrayf32]:
|
|
373
398
|
rmaxs = chain.from_iterable((t.sholl.rmax for t in p) for p in self._features)
|
|
374
399
|
rmax = max(rmaxs)
|
|
375
400
|
rs = Sholl.get_rs(rmax=rmax, steps=steps)
|
|
@@ -0,0 +1,250 @@
|
|
|
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
|
+
"""Feature anlysis of tree."""
|
|
17
|
+
|
|
18
|
+
from abc import ABC, abstractmethod
|
|
19
|
+
from functools import cached_property
|
|
20
|
+
from typing import TypeVar
|
|
21
|
+
|
|
22
|
+
import numpy as np
|
|
23
|
+
import numpy.typing as npt
|
|
24
|
+
from typing_extensions import Self, deprecated
|
|
25
|
+
|
|
26
|
+
from swcgeom.core import Branch, BranchTree, Tree
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"NodeFeatures",
|
|
30
|
+
"BifurcationFeatures",
|
|
31
|
+
"TipFeatures",
|
|
32
|
+
"PathFeatures",
|
|
33
|
+
"BranchFeatures",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T", bound=Branch)
|
|
37
|
+
|
|
38
|
+
# Node Level
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class NodeFeatures:
|
|
42
|
+
"""Evaluate node feature of tree."""
|
|
43
|
+
|
|
44
|
+
tree: Tree
|
|
45
|
+
|
|
46
|
+
@cached_property
|
|
47
|
+
def _branch_tree(self) -> BranchTree:
|
|
48
|
+
return BranchTree.from_tree(self.tree)
|
|
49
|
+
|
|
50
|
+
def __init__(self, tree: Tree) -> None:
|
|
51
|
+
self.tree = tree
|
|
52
|
+
|
|
53
|
+
def get_count(self) -> npt.NDArray[np.float32]:
|
|
54
|
+
"""Get number of nodes.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
count : array of shape (1,)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
return np.array([self.tree.number_of_nodes()], dtype=np.float32)
|
|
62
|
+
|
|
63
|
+
def get_radial_distance(self) -> npt.NDArray[np.float32]:
|
|
64
|
+
"""Get the end-to-end straight-line distance to soma.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
radial_distance : npt.NDArray[np.float32]
|
|
69
|
+
Array of shape (N,).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
xyz = self.tree.xyz() - self.tree.soma().xyz()
|
|
73
|
+
radial_distance = np.linalg.norm(xyz, axis=1)
|
|
74
|
+
return radial_distance
|
|
75
|
+
|
|
76
|
+
def get_branch_order(self) -> npt.NDArray[np.int32]:
|
|
77
|
+
"""Get branch order of criticle nodes of tree.
|
|
78
|
+
|
|
79
|
+
Branch order is the number of bifurcations between current
|
|
80
|
+
position and the root.
|
|
81
|
+
|
|
82
|
+
Criticle node means that soma, bifucation nodes, tips.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
order : npt.NDArray[np.int32]
|
|
87
|
+
Array of shape (N,), which k is the number of branchs.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
order = np.zeros_like(self._branch_tree.id(), dtype=np.int32)
|
|
91
|
+
|
|
92
|
+
def assign_depth(n: Tree.Node, pre_depth: int | None) -> int:
|
|
93
|
+
cur_order = pre_depth + 1 if pre_depth is not None else 0
|
|
94
|
+
order[n.id] = cur_order
|
|
95
|
+
return cur_order
|
|
96
|
+
|
|
97
|
+
self._branch_tree.traverse(enter=assign_depth)
|
|
98
|
+
return order
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class _SubsetNodesFeatures(ABC):
|
|
102
|
+
_features: NodeFeatures
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
@abstractmethod
|
|
106
|
+
def nodes(self) -> npt.NDArray[np.bool_]:
|
|
107
|
+
raise NotImplementedError()
|
|
108
|
+
|
|
109
|
+
def __init__(self, features: NodeFeatures) -> None:
|
|
110
|
+
self._features = features
|
|
111
|
+
|
|
112
|
+
def get_count(self) -> npt.NDArray[np.float32]:
|
|
113
|
+
"""Get number of nodes.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
count : npt.NDArray[np.float32]
|
|
118
|
+
Array of shape (1,).
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
return np.array([np.count_nonzero(self.nodes)], dtype=np.float32)
|
|
122
|
+
|
|
123
|
+
def get_radial_distance(self) -> npt.NDArray[np.float32]:
|
|
124
|
+
"""Get the end-to-end straight-line distance to soma.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
radial_distance : npt.NDArray[np.float32]
|
|
129
|
+
Array of shape (N,).
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
return self._features.get_radial_distance()[self.nodes]
|
|
133
|
+
|
|
134
|
+
@classmethod
|
|
135
|
+
def from_tree(cls, tree: Tree) -> Self:
|
|
136
|
+
return cls(NodeFeatures(tree))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class FurcationFeatures(_SubsetNodesFeatures):
|
|
140
|
+
"""Evaluate furcation node feature of tree."""
|
|
141
|
+
|
|
142
|
+
@cached_property
|
|
143
|
+
def nodes(self) -> npt.NDArray[np.bool_]:
|
|
144
|
+
return np.array([n.is_furcation() for n in self._features.tree])
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@deprecated("Use FurcationFeatures instead")
|
|
148
|
+
class BifurcationFeatures(FurcationFeatures):
|
|
149
|
+
"""Evaluate bifurcation node feature of tree.
|
|
150
|
+
|
|
151
|
+
Notes
|
|
152
|
+
-----
|
|
153
|
+
Deprecated due to the wrong spelling of furcation. For now, it
|
|
154
|
+
is just an alias of `FurcationFeatures` and raise a warning. It
|
|
155
|
+
will be change to raise an error in the future.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class TipFeatures(_SubsetNodesFeatures):
|
|
160
|
+
"""Evaluate tip node feature of tree."""
|
|
161
|
+
|
|
162
|
+
@cached_property
|
|
163
|
+
def nodes(self) -> npt.NDArray[np.bool_]:
|
|
164
|
+
return np.array([n.is_tip() for n in self._features.tree])
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# Path Level
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PathFeatures:
|
|
171
|
+
"""Path analysis of tree."""
|
|
172
|
+
|
|
173
|
+
tree: Tree
|
|
174
|
+
|
|
175
|
+
def __init__(self, tree: Tree) -> None:
|
|
176
|
+
self.tree = tree
|
|
177
|
+
|
|
178
|
+
def get_count(self) -> int:
|
|
179
|
+
return len(self._paths)
|
|
180
|
+
|
|
181
|
+
def get_length(self) -> npt.NDArray[np.float32]:
|
|
182
|
+
"""Get length of paths."""
|
|
183
|
+
|
|
184
|
+
length = [path.length() for path in self._paths]
|
|
185
|
+
return np.array(length, dtype=np.float32)
|
|
186
|
+
|
|
187
|
+
def get_tortuosity(self) -> npt.NDArray[np.float32]:
|
|
188
|
+
"""Get tortuosity of path."""
|
|
189
|
+
|
|
190
|
+
return np.array([path.tortuosity() for path in self._paths], dtype=np.float32)
|
|
191
|
+
|
|
192
|
+
@cached_property
|
|
193
|
+
def _paths(self) -> list[Tree.Path]:
|
|
194
|
+
return self.tree.get_paths()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class BranchFeatures:
|
|
198
|
+
"""Analysis bransh of tree."""
|
|
199
|
+
|
|
200
|
+
tree: Tree
|
|
201
|
+
|
|
202
|
+
def __init__(self, tree: Tree) -> None:
|
|
203
|
+
self.tree = tree
|
|
204
|
+
|
|
205
|
+
def get_count(self) -> int:
|
|
206
|
+
return len(self._branches)
|
|
207
|
+
|
|
208
|
+
def get_length(self) -> npt.NDArray[np.float32]:
|
|
209
|
+
"""Get length of branches."""
|
|
210
|
+
|
|
211
|
+
length = [br.length() for br in self._branches]
|
|
212
|
+
return np.array(length, dtype=np.float32)
|
|
213
|
+
|
|
214
|
+
def get_tortuosity(self) -> npt.NDArray[np.float32]:
|
|
215
|
+
"""Get tortuosity of path."""
|
|
216
|
+
|
|
217
|
+
return np.array([br.tortuosity() for br in self._branches], dtype=np.float32)
|
|
218
|
+
|
|
219
|
+
def get_angle(self, eps: float = 1e-7) -> npt.NDArray[np.float32]:
|
|
220
|
+
"""Get agnle between branches.
|
|
221
|
+
|
|
222
|
+
Returns
|
|
223
|
+
-------
|
|
224
|
+
angle : npt.NDArray[np.float32]
|
|
225
|
+
An array of shape (N, N), which N is length of branches.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
return self.calc_angle(self._branches, eps=eps)
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def calc_angle(branches: list[T], eps: float = 1e-7) -> npt.NDArray[np.float32]:
|
|
232
|
+
"""Calc agnle between branches.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
angle : npt.NDArray[np.float32]
|
|
237
|
+
An array of shape (N, N), which N is length of branches.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
vector = np.array([br[-1].xyz() - br[0].xyz() for br in branches])
|
|
241
|
+
vector_dot = np.matmul(vector, vector.T)
|
|
242
|
+
vector_norm = np.linalg.norm(vector, ord=2, axis=1, keepdims=True)
|
|
243
|
+
vector_norm_dot = np.matmul(vector_norm, vector_norm.T) + eps
|
|
244
|
+
arccos = np.clip(vector_dot / vector_norm_dot, -1, 1)
|
|
245
|
+
angle = np.arccos(arccos)
|
|
246
|
+
return angle
|
|
247
|
+
|
|
248
|
+
@cached_property
|
|
249
|
+
def _branches(self) -> list[Tree.Branch]:
|
|
250
|
+
return self.tree.get_branches()
|
swcgeom/analysis/lmeasure.py
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
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
|
"""L-Measure analysis."""
|
|
2
17
|
|
|
3
18
|
import math
|
|
4
|
-
from typing import Literal
|
|
19
|
+
from typing import Literal
|
|
5
20
|
|
|
6
21
|
import numpy as np
|
|
7
22
|
import numpy.typing as npt
|
|
8
|
-
|
|
9
23
|
from swcgeom.core import Branch, Compartment, Node, Tree
|
|
10
|
-
from swcgeom.utils import angle
|
|
11
24
|
|
|
12
25
|
__all__ = ["LMeasure"]
|
|
13
26
|
|
|
@@ -69,7 +82,7 @@ class LMeasure:
|
|
|
69
82
|
--------
|
|
70
83
|
L-Measure: http://cng.gmu.edu:8080/Lm/help/N_bifs.htm
|
|
71
84
|
"""
|
|
72
|
-
return len(tree.
|
|
85
|
+
return len(tree.get_furcations())
|
|
73
86
|
|
|
74
87
|
def n_branch(self, tree: Tree) -> int:
|
|
75
88
|
"""Number of branches.
|
|
@@ -163,12 +176,15 @@ class LMeasure:
|
|
|
163
176
|
--------
|
|
164
177
|
L-Measure: http://cng.gmu.edu:8080/Lm/help/Partition_asymmetry.htm
|
|
165
178
|
"""
|
|
179
|
+
|
|
166
180
|
children = n.children()
|
|
167
181
|
assert (
|
|
168
182
|
len(children) == 2
|
|
169
183
|
), "Partition asymmetry is only defined for bifurcations"
|
|
170
184
|
n1 = len(children[0].subtree().get_tips())
|
|
171
185
|
n2 = len(children[1].subtree().get_tips())
|
|
186
|
+
if n1 == n2:
|
|
187
|
+
return 0
|
|
172
188
|
return abs(n1 - n2) / (n1 + n2 - 2)
|
|
173
189
|
|
|
174
190
|
def fractal_dim(self):
|
|
@@ -274,7 +290,7 @@ class LMeasure:
|
|
|
274
290
|
rall_power, _, _, _ = self._rall_power(bif)
|
|
275
291
|
return rall_power
|
|
276
292
|
|
|
277
|
-
def _rall_power_d(self, bif: Tree.Node) ->
|
|
293
|
+
def _rall_power_d(self, bif: Tree.Node) -> tuple[float, float, float]:
|
|
278
294
|
children = bif.children()
|
|
279
295
|
assert len(children) == 2, "Rall Power is only defined for bifurcations"
|
|
280
296
|
parent = bif.parent()
|
|
@@ -284,7 +300,7 @@ class LMeasure:
|
|
|
284
300
|
da, db = 2 * children[0].r, 2 * children[1].r
|
|
285
301
|
return dp, da, db
|
|
286
302
|
|
|
287
|
-
def _rall_power(self, bif: Tree.Node) ->
|
|
303
|
+
def _rall_power(self, bif: Tree.Node) -> tuple[float, float, float, float]:
|
|
288
304
|
dp, da, db = self._rall_power_d(bif)
|
|
289
305
|
start, stop, step = 0, 5, 5 / 1000
|
|
290
306
|
xs = np.arange(start, stop, step)
|
|
@@ -336,7 +352,7 @@ class LMeasure:
|
|
|
336
352
|
return (da**rall_power + db**rall_power) / dp**rall_power
|
|
337
353
|
|
|
338
354
|
def bif_ampl_local(self, bif: Tree.Node) -> float:
|
|
339
|
-
"""
|
|
355
|
+
"""Bifurcation angle.
|
|
340
356
|
|
|
341
357
|
Given a bifurcation, this function returns the angle between
|
|
342
358
|
the first two compartments (in degree).
|
|
@@ -350,7 +366,7 @@ class LMeasure:
|
|
|
350
366
|
return np.degrees(angle(v1, v2))
|
|
351
367
|
|
|
352
368
|
def bif_ampl_remote(self, bif: Tree.Node) -> float:
|
|
353
|
-
"""
|
|
369
|
+
"""Bifurcation angle.
|
|
354
370
|
|
|
355
371
|
This function returns the angle between two bifurcation points
|
|
356
372
|
or between bifurcation point and terminal point or between two
|
|
@@ -361,7 +377,7 @@ class LMeasure:
|
|
|
361
377
|
L-Measure: http://cng.gmu.edu:8080/Lm/help/Bif_ampl_remote.htm
|
|
362
378
|
"""
|
|
363
379
|
|
|
364
|
-
v1, v2 = self.
|
|
380
|
+
v1, v2 = self._bif_vector_remote(bif)
|
|
365
381
|
return np.degrees(angle(v1, v2))
|
|
366
382
|
|
|
367
383
|
def bif_tilt_local(self, bif: Tree.Node) -> float:
|
|
@@ -501,7 +517,7 @@ class LMeasure:
|
|
|
501
517
|
|
|
502
518
|
def _bif_vector_local(
|
|
503
519
|
self, bif: Tree.Node
|
|
504
|
-
) ->
|
|
520
|
+
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
|
|
505
521
|
children = bif.children()
|
|
506
522
|
assert len(children) == 2, "Only defined for bifurcations"
|
|
507
523
|
|
|
@@ -511,7 +527,7 @@ class LMeasure:
|
|
|
511
527
|
|
|
512
528
|
def _bif_vector_remote(
|
|
513
529
|
self, bif: Tree.Node
|
|
514
|
-
) ->
|
|
530
|
+
) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
|
|
515
531
|
children = bif.children()
|
|
516
532
|
assert len(children) == 2, "Only defined for bifurcations"
|
|
517
533
|
|
|
@@ -665,7 +681,7 @@ class LMeasure:
|
|
|
665
681
|
n = node
|
|
666
682
|
order = 0
|
|
667
683
|
while n is not None:
|
|
668
|
-
if n.
|
|
684
|
+
if n.is_furcation():
|
|
669
685
|
order += 1
|
|
670
686
|
n = n.parent()
|
|
671
687
|
return order
|
|
@@ -819,3 +835,23 @@ def pill_surface_area(ra: float, rb: float, h: float) -> float:
|
|
|
819
835
|
bottom_hemisphere_area = 2 * math.pi * rb**2
|
|
820
836
|
total_area = lateral_area + top_hemisphere_area + bottom_hemisphere_area
|
|
821
837
|
return total_area
|
|
838
|
+
|
|
839
|
+
|
|
840
|
+
# TODO: move to `utils`
|
|
841
|
+
def angle(a: npt.ArrayLike, b: npt.ArrayLike) -> float:
|
|
842
|
+
"""Get the angle of vectors.
|
|
843
|
+
|
|
844
|
+
Returns
|
|
845
|
+
-------
|
|
846
|
+
angle : float
|
|
847
|
+
Angle [0, PI) in radians.
|
|
848
|
+
"""
|
|
849
|
+
|
|
850
|
+
a, b = np.array(a), np.array(b)
|
|
851
|
+
if np.linalg.norm(a) == 0 or np.linalg.norm(b) == 0:
|
|
852
|
+
raise ValueError("Input vectors must not be zero vectors.")
|
|
853
|
+
|
|
854
|
+
costheta = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
|
|
855
|
+
costheta = np.clip(costheta, -1, 1) # avoid numerical errors
|
|
856
|
+
theta = np.arccos(costheta)
|
|
857
|
+
return theta
|