swcgeom 0.16.0__tar.gz → 0.18.3__tar.gz

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 (123) hide show
  1. {swcgeom-0.16.0 → swcgeom-0.18.3}/LICENSE +1 -1
  2. {swcgeom-0.16.0/swcgeom.egg-info → swcgeom-0.18.3}/PKG-INFO +12 -8
  3. {swcgeom-0.16.0 → swcgeom-0.18.3}/README.md +7 -3
  4. {swcgeom-0.16.0 → swcgeom-0.18.3}/pyproject.toml +4 -11
  5. swcgeom-0.18.3/swcgeom/__init__.py +31 -0
  6. swcgeom-0.18.3/swcgeom/analysis/__init__.py +23 -0
  7. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/analysis/feature_extractor.py +43 -18
  8. swcgeom-0.18.3/swcgeom/analysis/features.py +250 -0
  9. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/analysis/lmeasure.py +48 -12
  10. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/analysis/sholl.py +25 -28
  11. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/analysis/trunk.py +27 -11
  12. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/analysis/visualization.py +24 -9
  13. swcgeom-0.18.3/swcgeom/analysis/visualization3d.py +100 -0
  14. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/analysis/volume.py +19 -4
  15. swcgeom-0.18.3/swcgeom/core/__init__.py +34 -0
  16. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/branch.py +19 -3
  17. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/branch_tree.py +18 -4
  18. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/compartment.py +18 -2
  19. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/node.py +32 -3
  20. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/path.py +21 -9
  21. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/population.py +58 -29
  22. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/swc.py +26 -10
  23. swcgeom-0.18.3/swcgeom/core/swc_utils/__init__.py +31 -0
  24. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/swc_utils/assembler.py +15 -0
  25. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/swc_utils/base.py +23 -17
  26. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/swc_utils/checker.py +19 -12
  27. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/swc_utils/io.py +24 -7
  28. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/swc_utils/normalizer.py +20 -4
  29. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/swc_utils/subtree.py +17 -2
  30. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/tree.py +56 -40
  31. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/tree_utils.py +28 -17
  32. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/core/tree_utils_impl.py +18 -3
  33. swcgeom-0.18.3/swcgeom/images/__init__.py +19 -0
  34. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/images/augmentation.py +18 -3
  35. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/images/contrast.py +15 -0
  36. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/images/folder.py +27 -26
  37. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/images/io.py +94 -117
  38. swcgeom-0.18.3/swcgeom/transforms/__init__.py +30 -0
  39. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/base.py +17 -2
  40. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/branch.py +74 -8
  41. swcgeom-0.18.3/swcgeom/transforms/branch_tree.py +82 -0
  42. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/geometry.py +22 -7
  43. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/image_preprocess.py +15 -0
  44. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/image_stack.py +36 -9
  45. swcgeom-0.18.3/swcgeom/transforms/images.py +209 -0
  46. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/mst.py +15 -0
  47. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/neurolucida_asc.py +20 -7
  48. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/path.py +15 -0
  49. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/population.py +16 -3
  50. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/tree.py +84 -30
  51. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/transforms/tree_assembler.py +23 -7
  52. swcgeom-0.18.3/swcgeom/utils/__init__.py +29 -0
  53. swcgeom-0.18.3/swcgeom/utils/debug.py +34 -0
  54. swcgeom-0.18.3/swcgeom/utils/download.py +137 -0
  55. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/dsu.py +15 -0
  56. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/ellipse.py +18 -4
  57. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/file.py +15 -0
  58. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/neuromorpho.py +35 -23
  59. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/numpy_helper.py +15 -0
  60. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/plotter_2d.py +27 -6
  61. swcgeom-0.18.3/swcgeom/utils/plotter_3d.py +48 -0
  62. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/renderer.py +21 -6
  63. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/sdf.py +19 -7
  64. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/solid_geometry.py +16 -3
  65. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/transforms.py +17 -4
  66. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom/utils/volumetric_object.py +23 -10
  67. {swcgeom-0.16.0 → swcgeom-0.18.3/swcgeom.egg-info}/PKG-INFO +12 -8
  68. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom.egg-info/SOURCES.txt +5 -42
  69. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom.egg-info/requires.txt +2 -2
  70. swcgeom-0.16.0/.github/workflows/build.yml +0 -38
  71. swcgeom-0.16.0/.github/workflows/github-publish.yml +0 -35
  72. swcgeom-0.16.0/.github/workflows/pypi-publish.yml +0 -31
  73. swcgeom-0.16.0/.github/workflows/test.yml +0 -28
  74. swcgeom-0.16.0/.gitignore +0 -164
  75. swcgeom-0.16.0/.pylintrc +0 -636
  76. swcgeom-0.16.0/.vscode/settings.json +0 -15
  77. swcgeom-0.16.0/CHANGELOG.md +0 -753
  78. swcgeom-0.16.0/examples/Branch.ipynb +0 -224
  79. swcgeom-0.16.0/examples/BranchTree.ipynb +0 -94
  80. swcgeom-0.16.0/examples/CollectTips.ipynb +0 -101
  81. swcgeom-0.16.0/examples/CutTree.ipynb +0 -197
  82. swcgeom-0.16.0/examples/Features.ipynb +0 -261
  83. swcgeom-0.16.0/examples/GeometryTransform.ipynb +0 -81
  84. swcgeom-0.16.0/examples/ImageStack.ipynb +0 -89
  85. swcgeom-0.16.0/examples/MST.ipynb +0 -173
  86. swcgeom-0.16.0/examples/SpectralClustering.ipynb +0 -267
  87. swcgeom-0.16.0/examples/Tree.ipynb +0 -248
  88. swcgeom-0.16.0/examples/data/101711-10_4p5-of-16_initial.CNG.swc +0 -161
  89. swcgeom-0.16.0/examples/data/101711-11_16-of-16_initial.CNG.swc +0 -318
  90. swcgeom-0.16.0/examples/data/1059283677_15257_2226-X16029-Y23953.swc +0 -5517
  91. swcgeom-0.16.0/examples/data/toydata.swc +0 -7
  92. swcgeom-0.16.0/examples/dgl/graph.py +0 -59
  93. swcgeom-0.16.0/examples/pytorch/branch.py +0 -53
  94. swcgeom-0.16.0/examples/pytorch/branch_dataset.py +0 -98
  95. swcgeom-0.16.0/examples/pytorch/tree_folder_dataset.py +0 -53
  96. swcgeom-0.16.0/git-conventional-commits.yaml +0 -44
  97. swcgeom-0.16.0/swcgeom/__init__.py +0 -6
  98. swcgeom-0.16.0/swcgeom/_version.py +0 -16
  99. swcgeom-0.16.0/swcgeom/analysis/__init__.py +0 -10
  100. swcgeom-0.16.0/swcgeom/analysis/branch_features.py +0 -67
  101. swcgeom-0.16.0/swcgeom/analysis/node_features.py +0 -121
  102. swcgeom-0.16.0/swcgeom/analysis/path_features.py +0 -37
  103. swcgeom-0.16.0/swcgeom/core/__init__.py +0 -15
  104. swcgeom-0.16.0/swcgeom/core/swc_utils/__init__.py +0 -17
  105. swcgeom-0.16.0/swcgeom/images/__init__.py +0 -4
  106. swcgeom-0.16.0/swcgeom/transforms/__init__.py +0 -14
  107. swcgeom-0.16.0/swcgeom/transforms/images.py +0 -102
  108. swcgeom-0.16.0/swcgeom/utils/__init__.py +0 -14
  109. swcgeom-0.16.0/swcgeom/utils/debug.py +0 -19
  110. swcgeom-0.16.0/swcgeom/utils/download.py +0 -99
  111. swcgeom-0.16.0/tests/__init__.py +0 -0
  112. swcgeom-0.16.0/tests/analysis/test_volume.py +0 -57
  113. swcgeom-0.16.0/tests/transforms/test_neurolucida_asc.py +0 -150
  114. swcgeom-0.16.0/tests/utils/__init__.py +0 -0
  115. swcgeom-0.16.0/tests/utils/test_dsu.py +0 -32
  116. swcgeom-0.16.0/tests/utils/test_numpy_helper.py +0 -46
  117. swcgeom-0.16.0/tests/utils/test_sdf.py +0 -69
  118. swcgeom-0.16.0/tests/utils/test_solid_geometry.py +0 -148
  119. swcgeom-0.16.0/tests/utils/test_transforms.py +0 -33
  120. swcgeom-0.16.0/tests/utils/test_volumetric_object.py +0 -138
  121. {swcgeom-0.16.0 → swcgeom-0.18.3}/setup.cfg +0 -0
  122. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom.egg-info/dependency_links.txt +0 -0
  123. {swcgeom-0.16.0 → swcgeom-0.18.3}/swcgeom.egg-info/top_level.txt +0 -0
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright [2024] [Zexin Yuan]
189
+ Copyright [yyyy] [name of copyright owner]
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.
@@ -1,8 +1,8 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: swcgeom
3
- Version: 0.16.0
3
+ Version: 0.18.3
4
4
  Summary: Neuron geometry library for swc format
5
- Author-email: yzx9 <yuan.zx@outlook.com>
5
+ Author-email: yzx9 <pypi@yzx9.xyz>
6
6
  License: Apache-2.0
7
7
  Project-URL: repository, https://github.com/yzx9/swcgeom
8
8
  Keywords: neuronscience,neuron,neuroanatomy,neuron-morphology
@@ -13,14 +13,14 @@ Requires-Dist: imagecodecs>=2023.3.16
13
13
  Requires-Dist: matplotlib>=3.5.2
14
14
  Requires-Dist: numpy>=1.22.3
15
15
  Requires-Dist: pandas>=1.4.2
16
- Requires-Dist: pynrrd>=1.0.0
16
+ Requires-Dist: pynrrd>=1.1.0
17
17
  Requires-Dist: scipy>=1.9.1
18
18
  Requires-Dist: sdflit>=0.2.1
19
19
  Requires-Dist: seaborn>=0.12.0
20
20
  Requires-Dist: tifffile>=2022.8.12
21
21
  Requires-Dist: typing_extensions>=4.4.0
22
22
  Requires-Dist: tqdm>=4.46.1
23
- Requires-Dist: v3d-py-helper>=0.1.0
23
+ Requires-Dist: v3d-py-helper>=0.4.1
24
24
  Provides-Extra: all
25
25
  Requires-Dist: beautifulsoup4>=4.11.1; extra == "all"
26
26
  Requires-Dist: certifi>=2023.5.7; extra == "all"
@@ -56,16 +56,20 @@ pip install build
56
56
  pip install --editable .
57
57
  ```
58
58
 
59
- Static analysis don't support import hook used in editable install for [PEP660](https://peps.python.org/pep-0660/) since upgrade to setuptools v64+, detail infomation at [setuptools#3518](https://github.com/pypa/setuptools/issues/3518), a workaround for vscode with pylance:
59
+ Static analysis don't support import hook used in editable install for
60
+ [PEP660](https://peps.python.org/pep-0660/) since upgrade to setuptools v64+,
61
+ detail information at [setuptools#3518](https://github.com/pypa/setuptools/issues/3518),
62
+ a workaround for vscode with pylance:
60
63
 
61
64
  ```json
62
65
  {
63
- "python.analysis.extraPaths": ["/path/to/this/project"]
66
+ "python.analysis.extraPaths": ["/path/to/this/project"]
64
67
  }
65
68
  ```
66
69
 
67
70
  ## LICENSE
68
71
 
69
- This work is licensed under a <a rel="license" href="https://www.apache.org/licenses/">Apache-2.0</a>.
72
+ This work is licensed under a
73
+ <a rel="license" href="https://www.apache.org/licenses/">Apache-2.0</a>.
70
74
 
71
75
  Copyright (c) 2022-present, Zexin Yuan
@@ -25,16 +25,20 @@ pip install build
25
25
  pip install --editable .
26
26
  ```
27
27
 
28
- Static analysis don't support import hook used in editable install for [PEP660](https://peps.python.org/pep-0660/) since upgrade to setuptools v64+, detail infomation at [setuptools#3518](https://github.com/pypa/setuptools/issues/3518), a workaround for vscode with pylance:
28
+ Static analysis don't support import hook used in editable install for
29
+ [PEP660](https://peps.python.org/pep-0660/) since upgrade to setuptools v64+,
30
+ detail information at [setuptools#3518](https://github.com/pypa/setuptools/issues/3518),
31
+ a workaround for vscode with pylance:
29
32
 
30
33
  ```json
31
34
  {
32
- "python.analysis.extraPaths": ["/path/to/this/project"]
35
+ "python.analysis.extraPaths": ["/path/to/this/project"]
33
36
  }
34
37
  ```
35
38
 
36
39
  ## LICENSE
37
40
 
38
- This work is licensed under a <a rel="license" href="https://www.apache.org/licenses/">Apache-2.0</a>.
41
+ This work is licensed under a
42
+ <a rel="license" href="https://www.apache.org/licenses/">Apache-2.0</a>.
39
43
 
40
44
  Copyright (c) 2022-present, Zexin Yuan
@@ -1,14 +1,10 @@
1
- [build-system]
2
- requires = ["setuptools>=64", "setuptools_scm[toml]>=6.2"]
3
- build-backend = "setuptools.build_meta"
4
-
5
1
  [project]
6
2
  name = "swcgeom"
7
- dynamic = ["version"]
3
+ version = "0.18.3"
8
4
  description = "Neuron geometry library for swc format"
9
5
  readme = "README.md"
10
6
  requires-python = ">=3.10"
11
- authors = [{ name = "yzx9", email = "yuan.zx@outlook.com" }]
7
+ authors = [{ name = "yzx9", email = "pypi@yzx9.xyz" }]
12
8
  keywords = ["neuronscience", "neuron", "neuroanatomy", "neuron-morphology"]
13
9
  license = { text = "Apache-2.0" }
14
10
  dependencies = [
@@ -16,14 +12,14 @@ dependencies = [
16
12
  "matplotlib>=3.5.2",
17
13
  "numpy>=1.22.3",
18
14
  "pandas>=1.4.2",
19
- "pynrrd>=1.0.0",
15
+ "pynrrd>=1.1.0",
20
16
  "scipy>=1.9.1",
21
17
  "sdflit>=0.2.1",
22
18
  "seaborn>=0.12.0",
23
19
  "tifffile>=2022.8.12",
24
20
  "typing_extensions>=4.4.0",
25
21
  "tqdm>=4.46.1",
26
- "v3d-py-helper>=0.1.0",
22
+ "v3d-py-helper>=0.4.1",
27
23
  ]
28
24
 
29
25
  [project.optional-dependencies]
@@ -38,6 +34,3 @@ all = [
38
34
 
39
35
  [project.urls]
40
36
  repository = "https://github.com/yzx9/swcgeom"
41
-
42
- [tool.setuptools_scm]
43
- write_to = "swcgeom/_version.py"
@@ -0,0 +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
+
15
+ """A neuron geometry library for swc format."""
16
+
17
+ from swcgeom import analysis, core, images, transforms
18
+ from swcgeom.analysis import draw
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
+ ]
@@ -0,0 +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
+
16
+ """Analysis for neuron trees."""
17
+
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, Callable, Dict, List, Literal, Tuple, overload
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.branch_features import BranchFeatures
21
- from swcgeom.analysis.node_features import (
22
- BifurcationFeatures,
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 | Tuple[Feature, Dict[str, Any]]
76
+ FeatAndKwargs = Feature | tuple[Feature, dict[str, Any]]
58
77
 
59
- Feature1D = set(["length", "volume", "node_count", "bifurcation_count", "tip_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 bifurcation_features(self) -> BifurcationFeatures: return BifurcationFeatures(self.node_features)
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: List[FeatAndKwargs]) -> List[NDArrayf32]: ...
143
+ def get(self, feature: list[FeatAndKwargs]) -> list[NDArrayf32]: ...
125
144
  @overload
126
- def get(self, feature: Dict[Feature, Dict[str, Any]]) -> Dict[str, NDArrayf32]: ...
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: Dict[str, Any], **kwargs) -> Axes:
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: Dict[str, Any], # pylint: disable=unused-argument
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: List[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: Dict[str, Any], **kwargs) -> Axes:
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
- ) -> Tuple[NDArrayf32, NDArrayf32]:
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: List[List[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: Dict[str, Any], **kwargs) -> Axes:
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
- ) -> Tuple[NDArrayf32, NDArrayf32]:
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()