swcgeom 0.16.0__tar.gz → 0.17.1__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 (112) hide show
  1. {swcgeom-0.16.0 → swcgeom-0.17.1}/CHANGELOG.md +31 -0
  2. {swcgeom-0.16.0/swcgeom.egg-info → swcgeom-0.17.1}/PKG-INFO +1 -1
  3. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/CollectTips.ipynb +2 -3
  4. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/CutTree.ipynb +1 -2
  5. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/Features.ipynb +2 -2
  6. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/dgl/graph.py +4 -4
  7. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/pytorch/branch.py +2 -1
  8. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/pytorch/branch_dataset.py +2 -1
  9. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/_version.py +2 -2
  10. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/analysis/__init__.py +1 -3
  11. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/analysis/feature_extractor.py +16 -15
  12. swcgeom-0.16.0/swcgeom/analysis/node_features.py → swcgeom-0.17.1/swcgeom/analysis/features.py +105 -3
  13. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/analysis/lmeasure.py +5 -5
  14. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/analysis/sholl.py +4 -4
  15. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/analysis/trunk.py +12 -11
  16. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/analysis/visualization.py +9 -9
  17. swcgeom-0.17.1/swcgeom/analysis/visualization3d.py +85 -0
  18. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/analysis/volume.py +4 -4
  19. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/branch.py +4 -3
  20. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/branch_tree.py +3 -4
  21. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/compartment.py +3 -2
  22. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/node.py +2 -2
  23. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/path.py +3 -2
  24. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/population.py +16 -27
  25. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/swc.py +11 -10
  26. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/swc_utils/base.py +8 -17
  27. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/swc_utils/io.py +7 -6
  28. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/swc_utils/normalizer.py +4 -3
  29. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/swc_utils/subtree.py +2 -2
  30. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/tree.py +22 -34
  31. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/tree_utils.py +11 -10
  32. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/tree_utils_impl.py +3 -3
  33. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/images/augmentation.py +3 -3
  34. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/images/folder.py +10 -16
  35. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/images/io.py +76 -111
  36. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/image_stack.py +6 -5
  37. swcgeom-0.17.1/swcgeom/transforms/images.py +202 -0
  38. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/neurolucida_asc.py +4 -6
  39. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/population.py +1 -3
  40. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/tree.py +8 -7
  41. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/tree_assembler.py +4 -3
  42. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/ellipse.py +3 -4
  43. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/neuromorpho.py +17 -16
  44. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/plotter_2d.py +12 -6
  45. swcgeom-0.17.1/swcgeom/utils/plotter_3d.py +31 -0
  46. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/renderer.py +6 -6
  47. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/sdf.py +2 -2
  48. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/solid_geometry.py +1 -3
  49. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/transforms.py +1 -3
  50. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/volumetric_object.py +8 -10
  51. {swcgeom-0.16.0 → swcgeom-0.17.1/swcgeom.egg-info}/PKG-INFO +1 -1
  52. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom.egg-info/SOURCES.txt +3 -3
  53. swcgeom-0.16.0/swcgeom/analysis/branch_features.py +0 -67
  54. swcgeom-0.16.0/swcgeom/analysis/path_features.py +0 -37
  55. swcgeom-0.16.0/swcgeom/transforms/images.py +0 -102
  56. {swcgeom-0.16.0 → swcgeom-0.17.1}/.github/workflows/build.yml +0 -0
  57. {swcgeom-0.16.0 → swcgeom-0.17.1}/.github/workflows/github-publish.yml +0 -0
  58. {swcgeom-0.16.0 → swcgeom-0.17.1}/.github/workflows/pypi-publish.yml +0 -0
  59. {swcgeom-0.16.0 → swcgeom-0.17.1}/.github/workflows/test.yml +0 -0
  60. {swcgeom-0.16.0 → swcgeom-0.17.1}/.gitignore +0 -0
  61. {swcgeom-0.16.0 → swcgeom-0.17.1}/.pylintrc +0 -0
  62. {swcgeom-0.16.0 → swcgeom-0.17.1}/.vscode/settings.json +0 -0
  63. {swcgeom-0.16.0 → swcgeom-0.17.1}/LICENSE +0 -0
  64. {swcgeom-0.16.0 → swcgeom-0.17.1}/README.md +0 -0
  65. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/Branch.ipynb +0 -0
  66. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/BranchTree.ipynb +0 -0
  67. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/GeometryTransform.ipynb +0 -0
  68. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/ImageStack.ipynb +0 -0
  69. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/MST.ipynb +0 -0
  70. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/SpectralClustering.ipynb +0 -0
  71. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/Tree.ipynb +0 -0
  72. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/data/101711-10_4p5-of-16_initial.CNG.swc +0 -0
  73. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/data/101711-11_16-of-16_initial.CNG.swc +0 -0
  74. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/data/1059283677_15257_2226-X16029-Y23953.swc +0 -0
  75. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/data/toydata.swc +0 -0
  76. {swcgeom-0.16.0 → swcgeom-0.17.1}/examples/pytorch/tree_folder_dataset.py +0 -0
  77. {swcgeom-0.16.0 → swcgeom-0.17.1}/git-conventional-commits.yaml +0 -0
  78. {swcgeom-0.16.0 → swcgeom-0.17.1}/pyproject.toml +0 -0
  79. {swcgeom-0.16.0 → swcgeom-0.17.1}/setup.cfg +0 -0
  80. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/__init__.py +0 -0
  81. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/__init__.py +0 -0
  82. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/swc_utils/__init__.py +0 -0
  83. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/swc_utils/assembler.py +0 -0
  84. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/core/swc_utils/checker.py +0 -0
  85. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/images/__init__.py +0 -0
  86. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/images/contrast.py +0 -0
  87. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/__init__.py +0 -0
  88. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/base.py +0 -0
  89. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/branch.py +0 -0
  90. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/geometry.py +0 -0
  91. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/image_preprocess.py +0 -0
  92. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/mst.py +0 -0
  93. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/transforms/path.py +0 -0
  94. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/__init__.py +0 -0
  95. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/debug.py +0 -0
  96. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/download.py +0 -0
  97. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/dsu.py +0 -0
  98. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/file.py +0 -0
  99. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom/utils/numpy_helper.py +0 -0
  100. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom.egg-info/dependency_links.txt +0 -0
  101. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom.egg-info/requires.txt +0 -0
  102. {swcgeom-0.16.0 → swcgeom-0.17.1}/swcgeom.egg-info/top_level.txt +0 -0
  103. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/__init__.py +0 -0
  104. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/analysis/test_volume.py +0 -0
  105. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/transforms/test_neurolucida_asc.py +0 -0
  106. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/utils/__init__.py +0 -0
  107. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/utils/test_dsu.py +0 -0
  108. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/utils/test_numpy_helper.py +0 -0
  109. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/utils/test_sdf.py +0 -0
  110. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/utils/test_solid_geometry.py +0 -0
  111. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/utils/test_transforms.py +0 -0
  112. {swcgeom-0.16.0 → swcgeom-0.17.1}/tests/utils/test_volumetric_object.py +0 -0
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## **0.17.1**&emsp;<sub><sup>2024-04-05 ([a8007ac...0ef3c2b](https://github.com/yzx9/swcgeom/compare/a8007ac6c5eb7de03298dbdcc6be59d3c282e125...0ef3c2b370a570366a79ef456c1125d12f006409?diff=split))</sup></sub>
4
+
5
+ ### Features
6
+
7
+ ##### &ensp;`utils`
8
+
9
+ - set round style ([90030e3](https://github.com/yzx9/swcgeom/commit/90030e3af1dc7ba763e04c313cb93f154e2f052c))
10
+
11
+ <br>
12
+
13
+
14
+ ## **0.17.0**&emsp;<sub><sup>2024-04-04 ([59257cb...2072139](https://github.com/yzx9/swcgeom/compare/59257cbfb6264afd3ca31fcba002f692bbb69ed0...20721393086e5a890b097b3ae32aaa1e4ad6b898?diff=split))</sup></sub>
15
+
16
+ ### Features
17
+
18
+ ##### &ensp;`images`
19
+
20
+ - handle raw file in terafly ([67ecd63](https://github.com/yzx9/swcgeom/commit/67ecd63bf4124d49b00e779c2702a7b939ef710b))
21
+ - add image stack dtype support ([7313ead](https://github.com/yzx9/swcgeom/commit/7313ead09ec61970f221b9e0c3691d31c269b591))
22
+
23
+ ##### &ensp;`transforms`
24
+
25
+ - add new image transformation classes ([7b5d805](https://github.com/yzx9/swcgeom/commit/7b5d805a1aa0deac3f9d7576a9aecab36b59774b))
26
+ - add image flip and notes ([5f2a4d1](https://github.com/yzx9/swcgeom/commit/5f2a4d1a9c2b5fb68e8221c7b024a411b247ab6d))
27
+
28
+
29
+ ### BREAKING CHANGES
30
+ - `images` remove deprecated \`swap\_xy\` and \`filp\_xy\` flag ([2072139](https://github.com/yzx9/swcgeom/commit/20721393086e5a890b097b3ae32aaa1e4ad6b898))
31
+ <br>
32
+
33
+
3
34
  ## **0.16.0**&emsp;<sub><sup>2024-03-16 ([bf2bf95...7029483](https://github.com/yzx9/swcgeom/compare/bf2bf95ee9fbfb7eba871db7303292f1dfcc7b8f...70294836643e51ca43d039887bbd71de3a9b561b?diff=split))</sup></sub>
4
35
 
5
36
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: swcgeom
3
- Version: 0.16.0
3
+ Version: 0.17.1
4
4
  Summary: Neuron geometry library for swc format
5
5
  Author-email: yzx9 <yuan.zx@outlook.com>
6
6
  License: Apache-2.0
@@ -23,7 +23,6 @@
23
23
  "import os\n",
24
24
  "from functools import reduce\n",
25
25
  "from operator import add\n",
26
- "from typing import List\n",
27
26
  "\n",
28
27
  "import numpy as np\n",
29
28
  "\n",
@@ -37,11 +36,11 @@
37
36
  "metadata": {},
38
37
  "outputs": [],
39
38
  "source": [
40
- "def collect_tips(cur: swcgeom.Tree.Node, children: List[int]) -> int:\n",
39
+ "def collect_tips(cur: swcgeom.Tree.Node, children: list[int]) -> int:\n",
41
40
  " cur[\"tips\"] = max(1, reduce(add, children, 0))\n",
42
41
  " return cur[\"tips\"]\n",
43
42
  "\n",
44
- "def collect_length(cur: swcgeom.Tree.Node, children: List[swcgeom.Tree.Node]) -> swcgeom.Tree.Node:\n",
43
+ "def collect_length(cur: swcgeom.Tree.Node, children: list[swcgeom.Tree.Node]) -> swcgeom.Tree.Node:\n",
45
44
  " def length(acc: float, child: swcgeom.Tree.Node) -> float:\n",
46
45
  " return acc + cur.distance(child) + child[\"length\"]\n",
47
46
  "\n",
@@ -30,7 +30,6 @@
30
30
  "outputs": [],
31
31
  "source": [
32
32
  "import math\n",
33
- "from typing import List\n",
34
33
  "\n",
35
34
  "import numpy as np\n",
36
35
  "import matplotlib.pyplot as plt\n",
@@ -138,7 +137,7 @@
138
137
  "\n",
139
138
  " removals = []\n",
140
139
  "\n",
141
- " def collect_cut(n: Tree.Node, children: List[int]) -> int:\n",
140
+ " def collect_cut(n: Tree.Node, children: list[int]) -> int:\n",
142
141
  " if len(children) <= 1:\n",
143
142
  " return n.id\n",
144
143
  "\n",
@@ -20,7 +20,7 @@
20
20
  "metadata": {},
21
21
  "outputs": [],
22
22
  "source": [
23
- "from typing import Any, cast, Dict\n",
23
+ "from typing import Any, cast\n",
24
24
  "\n",
25
25
  "import numpy as np\n",
26
26
  "import matplotlib.pyplot as plt\n",
@@ -88,7 +88,7 @@
88
88
  ],
89
89
  "source": [
90
90
  "features = extract_feature(tree3)\n",
91
- "all_features: Dict[Feature, Dict] = {\n",
91
+ "all_features: dict[Feature, dict] = {\n",
92
92
  " \"length\": {},\n",
93
93
  " \"node_radial_distance\": {},\n",
94
94
  " \"node_branch_order\": {},\n",
@@ -1,6 +1,6 @@
1
1
  """Dgl grapth transforms."""
2
2
 
3
- from typing import List, cast
3
+ from typing import cast
4
4
 
5
5
  import dgl
6
6
 
@@ -19,13 +19,13 @@ class ToDGLGraph(Transform[Tree, dgl.DGLGraph]):
19
19
  this class is more of a toy and template.
20
20
  """
21
21
 
22
- keys: List[str] | None
22
+ keys: list[str] | None
23
23
  to_bidirected: bool
24
24
 
25
25
  def __init__(
26
26
  self,
27
27
  to_bidirected: bool = False,
28
- keys: List[str] | None = None,
28
+ keys: list[str] | None = None,
29
29
  ) -> None:
30
30
  """Transofrm tree to dgl graph.
31
31
 
@@ -33,7 +33,7 @@ class ToDGLGraph(Transform[Tree, dgl.DGLGraph]):
33
33
  ----------
34
34
  to_bidirected : bool, default to `False`
35
35
  If True, return bidirected graph.
36
- keys : List[str], optional
36
+ keys : list[str], optional
37
37
  Copy these keys as ndata of graph.
38
38
  """
39
39
 
@@ -1,6 +1,7 @@
1
1
  """Transformations in branch requires pytorch."""
2
2
 
3
- from typing import Callable, Literal
3
+ from collections.abc import Callable
4
+ from typing import Literal
4
5
 
5
6
  import numpy as np
6
7
  import numpy.typing as npt
@@ -2,7 +2,8 @@
2
2
 
3
3
  import os
4
4
  import warnings
5
- from typing import Generic, Iterable, TypeVar, cast
5
+ from collections.abc import Iterable
6
+ from typing import Generic, TypeVar, cast
6
7
 
7
8
  import torch
8
9
  import torch.utils.data
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.16.0'
16
- __version_tuple__ = version_tuple = (0, 16, 0)
15
+ __version__ = version = '0.17.1'
16
+ __version_tuple__ = version_tuple = (0, 17, 1)
@@ -1,9 +1,7 @@
1
1
  """Analysis for neuron trees."""
2
2
 
3
- from swcgeom.analysis.branch_features import *
4
3
  from swcgeom.analysis.feature_extractor import *
5
- from swcgeom.analysis.node_features import *
6
- from swcgeom.analysis.path_features import *
4
+ from swcgeom.analysis.features import *
7
5
  from swcgeom.analysis.sholl import *
8
6
  from swcgeom.analysis.trunk import *
9
7
  from swcgeom.analysis.visualization import *
@@ -7,23 +7,24 @@ naming specification.
7
7
  """
8
8
 
9
9
  from abc import ABC, abstractmethod
10
+ from collections.abc import Callable
10
11
  from functools import cached_property
11
12
  from itertools import chain
12
13
  from os.path import basename
13
- from typing import Any, Callable, Dict, List, Literal, Tuple, overload
14
+ from typing import Any, Literal, overload
14
15
 
15
16
  import numpy as np
16
17
  import numpy.typing as npt
17
18
  import seaborn as sns
18
19
  from matplotlib.axes import Axes
19
20
 
20
- from swcgeom.analysis.branch_features import BranchFeatures
21
- from swcgeom.analysis.node_features import (
21
+ from swcgeom.analysis.features import (
22
22
  BifurcationFeatures,
23
+ BranchFeatures,
23
24
  NodeFeatures,
25
+ PathFeatures,
24
26
  TipFeatures,
25
27
  )
26
- from swcgeom.analysis.path_features import PathFeatures
27
28
  from swcgeom.analysis.sholl import Sholl
28
29
  from swcgeom.analysis.volume import get_volume
29
30
  from swcgeom.core import Population, Populations, Tree
@@ -54,7 +55,7 @@ Feature = Literal[
54
55
  ]
55
56
 
56
57
  NDArrayf32 = npt.NDArray[np.float32]
57
- FeatAndKwargs = Feature | Tuple[Feature, Dict[str, Any]]
58
+ FeatAndKwargs = Feature | tuple[Feature, dict[str, Any]]
58
59
 
59
60
  Feature1D = set(["length", "volume", "node_count", "bifurcation_count", "tip_count"])
60
61
 
@@ -121,9 +122,9 @@ class FeatureExtractor(ABC):
121
122
  @overload
122
123
  def get(self, feature: Feature, **kwargs) -> NDArrayf32: ...
123
124
  @overload
124
- def get(self, feature: List[FeatAndKwargs]) -> List[NDArrayf32]: ...
125
+ def get(self, feature: list[FeatAndKwargs]) -> list[NDArrayf32]: ...
125
126
  @overload
126
- def get(self, feature: Dict[Feature, Dict[str, Any]]) -> Dict[str, NDArrayf32]: ...
127
+ def get(self, feature: dict[Feature, dict[str, Any]]) -> dict[str, NDArrayf32]: ...
127
128
  # fmt:on
128
129
  def get(self, feature, **kwargs):
129
130
  """Get feature.
@@ -168,7 +169,7 @@ class FeatureExtractor(ABC):
168
169
 
169
170
  # Custom Plots
170
171
 
171
- def plot_node_branch_order(self, feature_kwargs: Dict[str, Any], **kwargs) -> Axes:
172
+ def plot_node_branch_order(self, feature_kwargs: dict[str, Any], **kwargs) -> Axes:
172
173
  vals = self._get("node_branch_order", **feature_kwargs)
173
174
  bin_edges = np.arange(int(np.ceil(vals.max() + 1))) + 0.5
174
175
  return self._plot_histogram_impl(vals, bin_edges, **kwargs)
@@ -234,7 +235,7 @@ class TreeFeatureExtractor(FeatureExtractor):
234
235
 
235
236
  def plot_sholl(
236
237
  self,
237
- feature_kwargs: Dict[str, Any], # pylint: disable=unused-argument
238
+ feature_kwargs: dict[str, Any], # pylint: disable=unused-argument
238
239
  **kwargs,
239
240
  ) -> Axes:
240
241
  _, ax = self._features.sholl.plot(**kwargs)
@@ -264,7 +265,7 @@ class PopulationFeatureExtractor(FeatureExtractor):
264
265
  """Extract features from population."""
265
266
 
266
267
  _population: Population
267
- _features: List[Features]
268
+ _features: list[Features]
268
269
 
269
270
  def __init__(self, population: Population) -> None:
270
271
  super().__init__()
@@ -279,7 +280,7 @@ class PopulationFeatureExtractor(FeatureExtractor):
279
280
 
280
281
  # Custom Plots
281
282
 
282
- def plot_sholl(self, feature_kwargs: Dict[str, Any], **kwargs) -> Axes:
283
+ def plot_sholl(self, feature_kwargs: dict[str, Any], **kwargs) -> Axes:
283
284
  vals, rs = self._get_sholl_impl(**feature_kwargs)
284
285
  ax = self._lineplot(xs=rs, ys=vals.flatten(), **kwargs)
285
286
  ax.set_ylabel("Count of Intersections")
@@ -295,7 +296,7 @@ class PopulationFeatureExtractor(FeatureExtractor):
295
296
 
296
297
  def _get_sholl_impl(
297
298
  self, steps: int = 20, **kwargs
298
- ) -> Tuple[NDArrayf32, NDArrayf32]:
299
+ ) -> tuple[NDArrayf32, NDArrayf32]:
299
300
  rmax = max(t.sholl.rmax for t in self._features)
300
301
  rs = Sholl.get_rs(rmax=rmax, steps=steps)
301
302
  vals = self._get_impl("sholl", steps=rs, **kwargs)
@@ -333,7 +334,7 @@ class PopulationsFeatureExtractor(FeatureExtractor):
333
334
  """Extract feature from population."""
334
335
 
335
336
  _populations: Populations
336
- _features: List[List[Features]]
337
+ _features: list[list[Features]]
337
338
 
338
339
  def __init__(self, populations: Populations) -> None:
339
340
  super().__init__()
@@ -348,7 +349,7 @@ class PopulationsFeatureExtractor(FeatureExtractor):
348
349
 
349
350
  # Custom Plots
350
351
 
351
- def plot_sholl(self, feature_kwargs: Dict[str, Any], **kwargs) -> Axes:
352
+ def plot_sholl(self, feature_kwargs: dict[str, Any], **kwargs) -> Axes:
352
353
  vals, rs = self._get_sholl_impl(**feature_kwargs)
353
354
  ax = self._lineplot(xs=rs, ys=vals, **kwargs)
354
355
  ax.set_ylabel("Count of Intersections")
@@ -369,7 +370,7 @@ class PopulationsFeatureExtractor(FeatureExtractor):
369
370
 
370
371
  def _get_sholl_impl(
371
372
  self, steps: int = 20, **kwargs
372
- ) -> Tuple[NDArrayf32, NDArrayf32]:
373
+ ) -> tuple[NDArrayf32, NDArrayf32]:
373
374
  rmaxs = chain.from_iterable((t.sholl.rmax for t in p) for p in self._features)
374
375
  rmax = max(rmaxs)
375
376
  rs = Sholl.get_rs(rmax=rmax, steps=steps)
@@ -1,15 +1,26 @@
1
- """Depth distribution of tree."""
1
+ """Feature anlysis of tree."""
2
2
 
3
3
  from abc import ABC, abstractmethod
4
4
  from functools import cached_property
5
+ from typing import TypeVar
5
6
 
6
7
  import numpy as np
7
8
  import numpy.typing as npt
8
9
  from typing_extensions import Self
9
10
 
10
- from swcgeom.core import BranchTree, Tree
11
+ from swcgeom.core import Branch, BranchTree, Tree
11
12
 
12
- __all__ = ["NodeFeatures", "BifurcationFeatures", "TipFeatures"]
13
+ __all__ = [
14
+ "NodeFeatures",
15
+ "BifurcationFeatures",
16
+ "TipFeatures",
17
+ "PathFeatures",
18
+ "BranchFeatures",
19
+ ]
20
+
21
+ T = TypeVar("T", bound=Branch)
22
+
23
+ # Node Level
13
24
 
14
25
 
15
26
  class NodeFeatures:
@@ -31,6 +42,7 @@ class NodeFeatures:
31
42
  -------
32
43
  count : array of shape (1,)
33
44
  """
45
+
34
46
  return np.array([self.tree.number_of_nodes()], dtype=np.float32)
35
47
 
36
48
  def get_radial_distance(self) -> npt.NDArray[np.float32]:
@@ -41,6 +53,7 @@ class NodeFeatures:
41
53
  radial_distance : npt.NDArray[np.float32]
42
54
  Array of shape (N,).
43
55
  """
56
+
44
57
  xyz = self.tree.xyz() - self.tree.soma().xyz()
45
58
  radial_distance = np.linalg.norm(xyz, axis=1)
46
59
  return radial_distance
@@ -58,6 +71,7 @@ class NodeFeatures:
58
71
  order : npt.NDArray[np.int32]
59
72
  Array of shape (N,), which k is the number of branchs.
60
73
  """
74
+
61
75
  order = np.zeros_like(self._branch_tree.id(), dtype=np.int32)
62
76
 
63
77
  def assign_depth(n: Tree.Node, pre_depth: int | None) -> int:
@@ -88,6 +102,7 @@ class _SubsetNodesFeatures(ABC):
88
102
  count : npt.NDArray[np.float32]
89
103
  Array of shape (1,).
90
104
  """
105
+
91
106
  return np.array([np.count_nonzero(self.nodes)], dtype=np.float32)
92
107
 
93
108
  def get_radial_distance(self) -> npt.NDArray[np.float32]:
@@ -98,6 +113,7 @@ class _SubsetNodesFeatures(ABC):
98
113
  radial_distance : npt.NDArray[np.float32]
99
114
  Array of shape (N,).
100
115
  """
116
+
101
117
  return self._features.get_radial_distance()[self.nodes]
102
118
 
103
119
  @classmethod
@@ -119,3 +135,89 @@ class TipFeatures(_SubsetNodesFeatures):
119
135
  @cached_property
120
136
  def nodes(self) -> npt.NDArray[np.bool_]:
121
137
  return np.array([n.is_tip() for n in self._features.tree])
138
+
139
+
140
+ # Path Level
141
+
142
+
143
+ class PathFeatures:
144
+ """Path analysis of tree."""
145
+
146
+ tree: Tree
147
+
148
+ def __init__(self, tree: Tree) -> None:
149
+ self.tree = tree
150
+
151
+ def get_count(self) -> int:
152
+ return len(self._paths)
153
+
154
+ def get_length(self) -> npt.NDArray[np.float32]:
155
+ """Get length of paths."""
156
+
157
+ length = [path.length() for path in self._paths]
158
+ return np.array(length, dtype=np.float32)
159
+
160
+ def get_tortuosity(self) -> npt.NDArray[np.float32]:
161
+ """Get tortuosity of path."""
162
+
163
+ return np.array([path.tortuosity() for path in self._paths], dtype=np.float32)
164
+
165
+ @cached_property
166
+ def _paths(self) -> list[Tree.Path]:
167
+ return self.tree.get_paths()
168
+
169
+
170
+ class BranchFeatures:
171
+ """Analysis bransh 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._branches)
180
+
181
+ def get_length(self) -> npt.NDArray[np.float32]:
182
+ """Get length of branches."""
183
+
184
+ length = [br.length() for br in self._branches]
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([br.tortuosity() for br in self._branches], dtype=np.float32)
191
+
192
+ def get_angle(self, eps: float = 1e-7) -> npt.NDArray[np.float32]:
193
+ """Get agnle between branches.
194
+
195
+ Returns
196
+ -------
197
+ angle : npt.NDArray[np.float32]
198
+ An array of shape (N, N), which N is length of branches.
199
+ """
200
+
201
+ return self.calc_angle(self._branches, eps=eps)
202
+
203
+ @staticmethod
204
+ def calc_angle(branches: list[T], eps: float = 1e-7) -> npt.NDArray[np.float32]:
205
+ """Calc agnle between branches.
206
+
207
+ Returns
208
+ -------
209
+ angle : npt.NDArray[np.float32]
210
+ An array of shape (N, N), which N is length of branches.
211
+ """
212
+
213
+ vector = np.array([br[-1].xyz() - br[0].xyz() for br in branches])
214
+ vector_dot = np.matmul(vector, vector.T)
215
+ vector_norm = np.linalg.norm(vector, ord=2, axis=1, keepdims=True)
216
+ vector_norm_dot = np.matmul(vector_norm, vector_norm.T) + eps
217
+ arccos = np.clip(vector_dot / vector_norm_dot, -1, 1)
218
+ angle = np.arccos(arccos)
219
+ return angle
220
+
221
+ @cached_property
222
+ def _branches(self) -> list[Tree.Branch]:
223
+ return self.tree.get_branches()
@@ -1,7 +1,7 @@
1
1
  """L-Measure analysis."""
2
2
 
3
3
  import math
4
- from typing import Literal, Tuple
4
+ from typing import Literal
5
5
 
6
6
  import numpy as np
7
7
  import numpy.typing as npt
@@ -274,7 +274,7 @@ class LMeasure:
274
274
  rall_power, _, _, _ = self._rall_power(bif)
275
275
  return rall_power
276
276
 
277
- def _rall_power_d(self, bif: Tree.Node) -> Tuple[float, float, float]:
277
+ def _rall_power_d(self, bif: Tree.Node) -> tuple[float, float, float]:
278
278
  children = bif.children()
279
279
  assert len(children) == 2, "Rall Power is only defined for bifurcations"
280
280
  parent = bif.parent()
@@ -284,7 +284,7 @@ class LMeasure:
284
284
  da, db = 2 * children[0].r, 2 * children[1].r
285
285
  return dp, da, db
286
286
 
287
- def _rall_power(self, bif: Tree.Node) -> Tuple[float, float, float, float]:
287
+ def _rall_power(self, bif: Tree.Node) -> tuple[float, float, float, float]:
288
288
  dp, da, db = self._rall_power_d(bif)
289
289
  start, stop, step = 0, 5, 5 / 1000
290
290
  xs = np.arange(start, stop, step)
@@ -501,7 +501,7 @@ class LMeasure:
501
501
 
502
502
  def _bif_vector_local(
503
503
  self, bif: Tree.Node
504
- ) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
504
+ ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
505
505
  children = bif.children()
506
506
  assert len(children) == 2, "Only defined for bifurcations"
507
507
 
@@ -511,7 +511,7 @@ class LMeasure:
511
511
 
512
512
  def _bif_vector_remote(
513
513
  self, bif: Tree.Node
514
- ) -> Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
514
+ ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
515
515
  children = bif.children()
516
516
  assert len(children) == 2, "Only defined for bifurcations"
517
517
 
@@ -1,7 +1,7 @@
1
1
  """Sholl analysis."""
2
2
 
3
3
  import warnings
4
- from typing import List, Literal, Optional, Tuple
4
+ from typing import Literal, Optional
5
5
 
6
6
  import numpy as np
7
7
  import numpy.typing as npt
@@ -84,18 +84,18 @@ class Sholl:
84
84
 
85
85
  def plot( # pylint: disable=too-many-arguments
86
86
  self,
87
- steps: List[float] | int = 20,
87
+ steps: list[float] | int = 20,
88
88
  plot_type: str | None = None,
89
89
  kind: Literal["bar", "linechart", "circles"] = "circles",
90
90
  fig: Figure | None = None,
91
91
  ax: Axes | None = None,
92
92
  **kwargs,
93
- ) -> Tuple[Figure, Axes]:
93
+ ) -> tuple[Figure, Axes]:
94
94
  """Plot Sholl analysis.
95
95
 
96
96
  Parameters
97
97
  ----------
98
- steps : int or List[float], default to 20
98
+ steps : int or list[float], default to 20
99
99
  Steps of raius of circle. If steps is int, then it will be
100
100
  evenly divided into n radii.
101
101
  kind : "bar" | "linechart" | "circles", default `circles`
@@ -2,8 +2,9 @@
2
2
 
3
3
  # pylint: disable=invalid-name
4
4
 
5
+ from collections.abc import Iterable
5
6
  from itertools import chain
6
- from typing import Any, Dict, Iterable, List, Literal, Optional, Tuple, cast
7
+ from typing import Any, Literal, Optional, cast
7
8
 
8
9
  import numpy as np
9
10
  import numpy.typing as npt
@@ -28,28 +29,28 @@ def draw_trunk(
28
29
  *,
29
30
  fig: Optional[Figure] = None,
30
31
  ax: Optional[Axes] = None,
31
- bound: Bounds | Tuple[Bounds, Dict[str, Any]] | None = "ellipse",
32
- point: bool | Dict[str, Any] = True,
32
+ bound: Bounds | tuple[Bounds, dict[str, Any]] | None = "ellipse",
33
+ point: bool | dict[str, Any] = True,
33
34
  projection: Projection = "2d",
34
35
  cmap: Any = "viridis",
35
36
  **kwargs,
36
- ) -> Tuple[Figure, Axes]:
37
+ ) -> tuple[Figure, Axes]:
37
38
  """Draw trunk tree.
38
39
 
39
40
  Parameters
40
41
  ----------
41
42
  t : Tree
42
- florets : List of (int | List of int)
43
+ florets : List of (int | list of int)
43
44
  The florets that needs to be removed, each floret can be a
44
45
  subtree or multiple subtrees (e.g., dendrites are a bunch of
45
46
  subtrees), each number is the id of a tree node.
46
47
  fig : ~matplotlib.figure.Figure, optional
47
48
  ax : ~matplotlib.axes.Axes, optional
48
- bound : Bounds | (Bounds, Dict[str, Any]) | None, default 'ellipse'
49
+ bound : Bounds | (Bounds, dict[str, Any]) | None, default 'ellipse'
49
50
  Kind of bound, support 'aabb', 'ellipse'. If bound is None, no
50
51
  bound will be drawn. If bound is a tuple, the second item will
51
52
  used as kwargs and forward to draw function.
52
- point : bool | Dict[str, Any], default True
53
+ point : bool | dict[str, Any], default True
53
54
  Draw point at the start of a subtree. If point is False, no
54
55
  point will be drawn. If point is a dict, this will used a
55
56
  kwargs and forward to draw function.
@@ -57,7 +58,7 @@ def draw_trunk(
57
58
  Colormap, any value supported by ~matplotlib.cm.Colormap. We
58
59
  will use the ratio of the length of the subtree to the total
59
60
  length of the tree to determine the color.
60
- **kwargs : Dict[str, Any]
61
+ **kwargs : dict[str, Any]
61
62
  Forward to ~swcgeom.analysis.draw.
62
63
  """
63
64
  # pylint: disable=too-many-locals
@@ -83,14 +84,14 @@ def draw_trunk(
83
84
 
84
85
  def split_florets(
85
86
  t: Tree, florets: Iterable[int | Iterable[int]]
86
- ) -> Tuple[Tree, List[List[Tree]]]:
87
+ ) -> tuple[Tree, list[list[Tree]]]:
87
88
  florets = [[i] if isinstance(i, (int, np.integer)) else i for i in florets]
88
89
  subtrees = [[get_subtree(t, ff) for ff in f] for f in florets]
89
90
  trunk = to_subtree(t, chain(*florets))
90
91
  return trunk, subtrees
91
92
 
92
93
 
93
- def get_length_ratio(t: Tree, tss: List[List[Tree]]) -> Any:
94
+ def get_length_ratio(t: Tree, tss: list[list[Tree]]) -> Any:
94
95
  lens = np.array([sum(t.length() for t in ts) for ts in tss])
95
96
  return lens / t.length()
96
97
 
@@ -101,7 +102,7 @@ def get_length_ratio(t: Tree, tss: List[List[Tree]]) -> Any:
101
102
  def draw_bound(
102
103
  ts: Iterable[Tree],
103
104
  ax: Axes,
104
- bound: Bounds | Tuple[Bounds, Dict[str, Any]],
105
+ bound: Bounds | tuple[Bounds, dict[str, Any]],
105
106
  projection: Projection,
106
107
  **kwargs,
107
108
  ) -> None:
@@ -2,7 +2,7 @@
2
2
 
3
3
  import os
4
4
  import weakref
5
- from typing import Any, Dict, List, Literal, Optional, Tuple
5
+ from typing import Any, Literal, Optional
6
6
 
7
7
  import numpy as np
8
8
  from matplotlib.axes import Axes
@@ -21,15 +21,15 @@ from swcgeom.utils import (
21
21
 
22
22
  __all__ = ["draw"]
23
23
 
24
- Positions = Literal["lt", "lb", "rt", "rb"] | Tuple[float, float]
25
- locations: Dict[Literal["lt", "lb", "rt", "rb"], Tuple[float, float]] = {
24
+ Positions = Literal["lt", "lb", "rt", "rb"] | tuple[float, float]
25
+ locations: dict[Literal["lt", "lb", "rt", "rb"], tuple[float, float]] = {
26
26
  "lt": (0.10, 0.90),
27
27
  "lb": (0.10, 0.10),
28
28
  "rt": (0.90, 0.90),
29
29
  "rb": (0.90, 0.10),
30
30
  }
31
31
 
32
- ax_weak_memo = weakref.WeakKeyDictionary[Axes, Dict[str, Any]]({})
32
+ ax_weak_memo = weakref.WeakKeyDictionary[Axes, dict[str, Any]]({})
33
33
 
34
34
 
35
35
  def draw(
@@ -39,7 +39,7 @@ def draw(
39
39
  ax: Optional[Axes] = None,
40
40
  show: bool | None = None,
41
41
  camera: CameraOptions = "xy",
42
- color: Optional[Dict[int, str] | str] = None,
42
+ color: Optional[dict[int, str] | str] = None,
43
43
  label: str | bool = True,
44
44
  direction_indicator: Positions | Literal[False] = "rb",
45
45
  unit: Optional[str] = None,
@@ -64,7 +64,7 @@ def draw(
64
64
  vector, then then threat it as (look-at, up), so camera is
65
65
  ((0, 0, 0), look-at, up). An easy way is to use the presets
66
66
  "xy", "yz" and "zx".
67
- color : Dict[int, str] | "vaa3d" | str, optional
67
+ color : dict[int, str] | "vaa3d" | str, optional
68
68
  Color map. If is dict, segments will be colored by the type of
69
69
  parent node.If is string, the value will be use for any type.
70
70
  label : str | bool, default True
@@ -120,14 +120,14 @@ def draw(
120
120
  return fig, ax
121
121
 
122
122
 
123
- def get_ax_swc(ax: Axes) -> List[SWCLike]:
123
+ def get_ax_swc(ax: Axes) -> list[SWCLike]:
124
124
  ax_weak_memo.setdefault(ax, {})
125
125
  return ax_weak_memo[ax]["swc"]
126
126
 
127
127
 
128
128
  def get_ax_color(
129
- ax: Axes, swc: SWCLike, color: Optional[Dict[int, str] | str] = None
130
- ) -> str | List[str]:
129
+ ax: Axes, swc: SWCLike, color: Optional[dict[int, str] | str] = None
130
+ ) -> str | list[str]:
131
131
  if color == "vaa3d":
132
132
  color = palette.vaa3d
133
133
  elif isinstance(color, str):