swcgeom 0.11.1__py3-none-any.whl → 0.13.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of swcgeom might be problematic. Click here for more details.

Files changed (56) hide show
  1. swcgeom/__init__.py +4 -4
  2. swcgeom/_version.py +14 -2
  3. swcgeom/analysis/__init__.py +7 -7
  4. swcgeom/analysis/branch_features.py +1 -1
  5. swcgeom/analysis/feature_extractor.py +25 -12
  6. swcgeom/analysis/node_features.py +1 -1
  7. swcgeom/analysis/path_features.py +1 -1
  8. swcgeom/analysis/sholl.py +11 -7
  9. swcgeom/analysis/trunk.py +5 -5
  10. swcgeom/analysis/visualization.py +2 -2
  11. swcgeom/analysis/volume.py +80 -0
  12. swcgeom/core/__init__.py +9 -9
  13. swcgeom/core/branch.py +8 -4
  14. swcgeom/core/branch_tree.py +4 -5
  15. swcgeom/core/node.py +5 -3
  16. swcgeom/core/path.py +6 -3
  17. swcgeom/core/population.py +2 -2
  18. swcgeom/core/segment.py +8 -4
  19. swcgeom/core/swc.py +24 -3
  20. swcgeom/core/swc_utils/__init__.py +6 -6
  21. swcgeom/core/swc_utils/assembler.py +2 -2
  22. swcgeom/core/swc_utils/base.py +30 -1
  23. swcgeom/core/swc_utils/checker.py +30 -6
  24. swcgeom/core/swc_utils/io.py +31 -30
  25. swcgeom/core/swc_utils/normalizer.py +1 -1
  26. swcgeom/core/swc_utils/subtree.py +1 -1
  27. swcgeom/core/tree.py +38 -14
  28. swcgeom/core/tree_utils.py +47 -41
  29. swcgeom/core/tree_utils_impl.py +39 -0
  30. swcgeom/images/__init__.py +2 -2
  31. swcgeom/images/folder.py +2 -2
  32. swcgeom/images/io.py +48 -9
  33. swcgeom/transforms/__init__.py +10 -8
  34. swcgeom/transforms/branch.py +3 -3
  35. swcgeom/transforms/geometry.py +11 -4
  36. swcgeom/transforms/image_stack.py +3 -3
  37. swcgeom/transforms/images.py +1 -1
  38. swcgeom/transforms/mst.py +68 -13
  39. swcgeom/transforms/path.py +48 -0
  40. swcgeom/transforms/population.py +2 -2
  41. swcgeom/transforms/tree.py +18 -9
  42. swcgeom/transforms/tree_assembler.py +7 -4
  43. swcgeom/utils/__init__.py +10 -7
  44. swcgeom/utils/dsu.py +42 -0
  45. swcgeom/utils/file.py +91 -0
  46. swcgeom/utils/geometry_object.py +299 -0
  47. swcgeom/utils/neuromorpho.py +33 -11
  48. swcgeom/utils/renderer.py +5 -4
  49. swcgeom/utils/transforms.py +26 -1
  50. {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/METADATA +8 -8
  51. swcgeom-0.13.0.dist-info/RECORD +61 -0
  52. {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/WHEEL +1 -1
  53. swcgeom-0.11.1.dist-info/RECORD +0 -55
  54. /swcgeom/utils/{numpy.py → numpy_helper.py} +0 -0
  55. {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/LICENSE +0 -0
  56. {swcgeom-0.11.1.dist-info → swcgeom-0.13.0.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ from typing import Tuple
6
6
  import numpy as np
7
7
  import numpy.typing as npt
8
8
 
9
- from .base import Transform
9
+ from swcgeom.transforms.base import Transform
10
10
 
11
11
  __all__ = ["Center"]
12
12
 
swcgeom/transforms/mst.py CHANGED
@@ -1,15 +1,16 @@
1
1
  """Minimum spanning tree."""
2
2
 
3
+ import warnings
3
4
  from typing import Optional
4
5
 
5
6
  import numpy as np
6
- import numpy.typing as npt
7
7
  import pandas as pd
8
8
  from numpy import ma
9
+ from numpy import typing as npt
9
10
 
10
- from ..core import Tree
11
- from ..core.swc_utils import SWCNames, get_names
12
- from .base import Transform
11
+ from swcgeom.core import Tree, sort_tree
12
+ from swcgeom.core.swc_utils import SWCNames, SWCTypes, get_names, get_types
13
+ from swcgeom.transforms.base import Transform
13
14
 
14
15
  __all__ = ["PointsToCuntzMST", "PointsToMST"]
15
16
 
@@ -30,7 +31,14 @@ class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
30
31
  """
31
32
 
32
33
  def __init__(
33
- self, *, bf: float = 0.4, furcations: int = 2, exclude_soma: bool = True
34
+ self,
35
+ *,
36
+ bf: float = 0.4,
37
+ furcations: int = 2,
38
+ exclude_soma: bool = True,
39
+ sort: bool = True,
40
+ names: Optional[SWCNames] = None,
41
+ types: Optional[SWCTypes] = None,
34
42
  ) -> None:
35
43
  """
36
44
  Parameters
@@ -42,10 +50,15 @@ class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
42
50
  no suppression.
43
51
  exclude_soma : bool, default `True`
44
52
  Suppress multi-furcations exclude soma.
53
+ names : SWCNames, optional
54
+ types : SWCTypes, optional
45
55
  """
46
56
  self.bf = np.clip(bf, 0, 1)
47
57
  self.furcations = furcations
48
58
  self.exclude_soma = exclude_soma
59
+ self.sort = sort
60
+ self.names = get_names(names)
61
+ self.types = get_types(types)
49
62
 
50
63
  def __call__( # pylint: disable=too-many-locals
51
64
  self,
@@ -63,7 +76,17 @@ class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
63
76
  Position of soma. If none, use the first point as soma.
64
77
  names : SWCNames, optional
65
78
  """
66
- names = get_names(names)
79
+ if names is None:
80
+ names = self.names
81
+ else:
82
+ warnings.warn(
83
+ "`PointsToCuntzMST(...)(names=...)` has been "
84
+ "replaced by `PointsToCuntzMST(...,names=...)` since "
85
+ "v0.12.0, and will be removed in next version",
86
+ DeprecationWarning,
87
+ )
88
+ names = get_names(names) # TODO: remove it
89
+
67
90
  if soma is not None:
68
91
  soma = np.array(soma)
69
92
  assert soma.shape == (3,)
@@ -103,16 +126,19 @@ class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
103
126
 
104
127
  dic = {
105
128
  names.id: np.arange(n),
106
- names.type: np.full(n, fill_value=7), # TODO
129
+ names.type: np.full(n, fill_value=self.types.glia_processes),
107
130
  names.x: points[:, 0],
108
131
  names.y: points[:, 1],
109
132
  names.z: points[:, 2],
110
133
  names.r: 1,
111
134
  names.pid: pid,
112
135
  }
113
- dic[names.type][0] = 1
136
+ dic[names.type][0] = self.types.soma
114
137
  df = pd.DataFrame.from_dict(dic)
115
- return Tree.from_data_frame(df, names=names)
138
+ t = Tree.from_data_frame(df, names=names)
139
+ if self.sort:
140
+ t = sort_tree(t)
141
+ return t
116
142
 
117
143
  def __repr__(self) -> str:
118
144
  return (
@@ -120,21 +146,50 @@ class PointsToCuntzMST(Transform[npt.NDArray[np.float32], Tree]):
120
146
  f"-bf-{self.bf}"
121
147
  f"-furcations-{self.furcations}"
122
148
  f"-{'exclude-soma' if self.exclude_soma else 'include-soma'}"
123
- )
149
+ ) # TODO: names, types
124
150
 
125
151
 
126
152
  class PointsToMST(PointsToCuntzMST): # pylint: disable=too-few-public-methods
127
153
  """Create minimum spanning tree from points."""
128
154
 
129
- def __init__(self, k_furcations: int = 2) -> None:
155
+ def __init__(
156
+ self,
157
+ furcations: int = 2,
158
+ *,
159
+ k_furcations: Optional[int] = None,
160
+ exclude_soma: bool = True,
161
+ names: Optional[SWCNames] = None,
162
+ types: Optional[SWCTypes] = None,
163
+ **kwargs,
164
+ ) -> None:
130
165
  """
131
166
  Parameters
132
167
  ----------
133
- k_furcations : int, default `2`
168
+ furcations : int, default `2`
134
169
  Suppress multifurcations which more than k. If set to -1,
135
170
  no suppression.
171
+ exclude_soma : bool, default `True`
172
+ Suppress multi-furcations exclude soma.
173
+ names : SWCNames, optional
174
+ types : SWCTypes, optional
136
175
  """
137
- super().__init__(bf=0, furcations=k_furcations)
176
+ if k_furcations is not None:
177
+ warnings.warn(
178
+ "`PointsToMST(k_furcations=...)` has been renamed to "
179
+ "`PointsToMST(furcations=...)` since v0.12.0, and will "
180
+ "be removed in next version",
181
+ DeprecationWarning,
182
+ )
183
+ furcations = k_furcations
184
+
185
+ super().__init__(
186
+ bf=0,
187
+ furcations=furcations,
188
+ exclude_soma=exclude_soma,
189
+ names=names,
190
+ types=types,
191
+ **kwargs,
192
+ )
138
193
 
139
194
  def __repr__(self) -> str:
140
195
  return (
@@ -0,0 +1,48 @@
1
+ """Transformation in path."""
2
+
3
+ from swcgeom.core import Path, Tree, redirect_tree
4
+ from swcgeom.transforms.base import Transform
5
+
6
+ __all__ = ["PathToTree", "PathReverser"]
7
+
8
+
9
+ class PathToTree(Transform[Path, Tree]):
10
+ """Transform path to tree."""
11
+
12
+ def __call__(self, x: Path) -> Tree:
13
+ t = Tree(
14
+ x.number_of_nodes(),
15
+ type=x.type(),
16
+ id=x.id(),
17
+ x=x.x(),
18
+ y=x.y(),
19
+ z=x.z(),
20
+ r=x.r(),
21
+ pid=x.pid(),
22
+ source=x.source,
23
+ comments=x.comments.copy(),
24
+ names=x.names,
25
+ )
26
+ return t
27
+
28
+
29
+ class PathReverser(Transform[Path, Path]):
30
+ r"""Reverse path.
31
+
32
+ ```text
33
+ a -> b -> ... -> y -> z
34
+ // to
35
+ a <- b <- ... <- y <- z
36
+ ```
37
+ """
38
+
39
+ def __init__(self) -> None:
40
+ super().__init__()
41
+ self.to_tree = PathToTree()
42
+
43
+ def __call__(self, x: Path) -> Path:
44
+ x[0].type, x[-1].type = x[-1].type, x[0].type
45
+ t = self.to_tree(x)
46
+ t = redirect_tree(t, x[-1].id)
47
+ p = t.get_paths()[0]
48
+ return p
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import List
4
4
 
5
- from ..core import Population, Tree
6
- from .base import Transform
5
+ from swcgeom.core import Population, Tree
6
+ from swcgeom.transforms.base import Transform
7
7
 
8
8
  __all__ = ["PopulationTransform"]
9
9
 
@@ -5,10 +5,11 @@ from typing import Callable, List, Optional, Tuple
5
5
 
6
6
  import numpy as np
7
7
 
8
- from ..core import BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
9
- from .base import Transform
10
- from .branch import BranchConvSmoother
11
- from .geometry import Normalizer
8
+ from swcgeom.core import BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
9
+ from swcgeom.core.swc_utils import SWCTypes, get_types
10
+ from swcgeom.transforms.base import Transform
11
+ from swcgeom.transforms.branch import BranchConvSmoother
12
+ from swcgeom.transforms.geometry import Normalizer
12
13
 
13
14
  __all__ = [
14
15
  "ToBranchTree",
@@ -34,10 +35,16 @@ class ToBranchTree(Transform[Tree, BranchTree]):
34
35
  class ToLongestPath(Transform[Tree, Path[DictSWC]]):
35
36
  """Transform tree to longest path."""
36
37
 
38
+ def __init__(self, *, detach: bool = True) -> None:
39
+ self.detach = detach
40
+
37
41
  def __call__(self, x: Tree) -> Path[DictSWC]:
38
42
  paths = x.get_paths()
39
43
  idx = np.argmax([p.length() for p in paths])
40
- return paths[idx].detach()
44
+ path = paths[idx]
45
+ if self.detach:
46
+ path = path.detach()
47
+ return path # type: ignore
41
48
 
42
49
 
43
50
  class TreeSmoother(Transform[Tree, Tree]): # pylint: disable=missing-class-docstring
@@ -107,8 +114,9 @@ class CutByType(Transform[Tree, Tree]):
107
114
  class CutAxonTree(CutByType):
108
115
  """Cut axon tree."""
109
116
 
110
- def __init__(self) -> None:
111
- super().__init__(type=2)
117
+ def __init__(self, types: Optional[SWCTypes] = None) -> None:
118
+ types = get_types(types)
119
+ super().__init__(type=types.axon)
112
120
 
113
121
  def __repr__(self) -> str:
114
122
  return "CutAxonTree"
@@ -117,8 +125,9 @@ class CutAxonTree(CutByType):
117
125
  class CutDendriteTree(CutByType):
118
126
  """Cut dendrite tree."""
119
127
 
120
- def __init__(self) -> None:
121
- super().__init__(type=3)
128
+ def __init__(self, types: Optional[SWCTypes] = None) -> None:
129
+ types = get_types(types)
130
+ super().__init__(type=types.basal_dendrite) # TODO: apical dendrite
122
131
 
123
132
  def __repr__(self) -> str:
124
133
  return "CutDenriteTree"
@@ -4,10 +4,13 @@ from typing import Iterable, List, Optional, Tuple
4
4
 
5
5
  import pandas as pd
6
6
 
7
- from ..core import Tree
8
- from ..core.swc_utils import SWCNames
9
- from ..core.swc_utils.assembler import assemble_lines_impl, try_assemble_lines_impl
10
- from .base import Transform
7
+ from swcgeom.core import Tree
8
+ from swcgeom.core.swc_utils import SWCNames
9
+ from swcgeom.core.swc_utils.assembler import (
10
+ assemble_lines_impl,
11
+ try_assemble_lines_impl,
12
+ )
13
+ from swcgeom.transforms.base import Transform
11
14
 
12
15
 
13
16
  class LinesToTree(Transform[List[pd.DataFrame], Tree]):
swcgeom/utils/__init__.py CHANGED
@@ -1,9 +1,12 @@
1
1
  """Utils."""
2
2
 
3
- from .debug import *
4
- from .ellipse import *
5
- from .neuromorpho import *
6
- from .numpy import *
7
- from .renderer import *
8
- from .sdf import *
9
- from .transforms import *
3
+ from swcgeom.utils.debug import *
4
+ from swcgeom.utils.dsu import *
5
+ from swcgeom.utils.ellipse import *
6
+ from swcgeom.utils.file import *
7
+ from swcgeom.utils.geometry_object import *
8
+ from swcgeom.utils.neuromorpho import *
9
+ from swcgeom.utils.numpy_helper import *
10
+ from swcgeom.utils.renderer import *
11
+ from swcgeom.utils.sdf import *
12
+ from swcgeom.utils.transforms import *
swcgeom/utils/dsu.py ADDED
@@ -0,0 +1,42 @@
1
+ """Disjoint Set Union Impl."""
2
+
3
+ __all__ = ["DisjointSetUnion"]
4
+
5
+
6
+ class DisjointSetUnion:
7
+ """Disjoint Set Union.
8
+
9
+ DSU with path compression and union by rank.
10
+ """
11
+
12
+ def __init__(self, node_number: int):
13
+ self.element_parent = [i for i in range(node_number)]
14
+ self.rank = [0 for _ in range(node_number)]
15
+
16
+ def find_parent(self, node_id: int) -> int:
17
+ if node_id != self.element_parent[node_id]:
18
+ self.element_parent[node_id] = self.find_parent(
19
+ self.element_parent[node_id]
20
+ )
21
+ return self.element_parent[node_id]
22
+
23
+ def union_sets(self, node_a: int, node_b: int) -> None:
24
+ assert self.validate_node(node_a) and self.validate_node(node_b)
25
+
26
+ root_a = self.find_parent(node_a)
27
+ root_b = self.find_parent(node_b)
28
+ if root_a != root_b:
29
+ # union by rank
30
+ if self.rank[root_a] < self.rank[root_b]:
31
+ self.element_parent[root_a] = root_b
32
+ elif self.rank[root_a] > self.rank[root_b]:
33
+ self.element_parent[root_b] = root_a
34
+ else:
35
+ self.element_parent[root_b] = root_a
36
+ self.rank[root_a] += 1
37
+
38
+ def is_same_set(self, node_a: int, node_b: int) -> bool:
39
+ return self.find_parent(node_a) == self.find_parent(node_b)
40
+
41
+ def validate_node(self, node_id: int) -> bool:
42
+ return 0 <= node_id < len(self.element_parent)
swcgeom/utils/file.py ADDED
@@ -0,0 +1,91 @@
1
+ """File related utils.
2
+
3
+ Notes
4
+ -----
5
+ If character coding is enabled, all denpendencies need to be installed,
6
+ try:
7
+
8
+ ```sh
9
+ pip install swcgeom[all]
10
+ ```
11
+ """
12
+
13
+ import warnings
14
+ from io import BytesIO, TextIOBase, TextIOWrapper
15
+ from typing import Literal
16
+
17
+ __all__ = ["FileReader", "PathOrIO"]
18
+
19
+ PathOrIO = int | str | bytes | BytesIO | TextIOBase
20
+
21
+
22
+ class FileReader:
23
+ def __init__(
24
+ self,
25
+ fname: PathOrIO,
26
+ *,
27
+ encoding: Literal["detect"] | str = "utf-8",
28
+ low_confidence: float = 0.9,
29
+ **kwargs,
30
+ ) -> None:
31
+ """Read file.
32
+
33
+ Parameters
34
+ ----------
35
+ fname : PathOrIO
36
+ encoding : str | 'detect', default `utf-8`
37
+ The name of the encoding used to decode the file. If is
38
+ `detect`, we will try to detect the character encoding.
39
+ low_confidence : float, default to 0.9
40
+ Used for detect character endocing, raising warning when
41
+ parsing with low confidence.
42
+ """
43
+ # TODO: support StringIO
44
+ self.fname, self.fb, self.f = "", None, None
45
+ if isinstance(fname, TextIOBase):
46
+ self.f = fname
47
+ encoding = fname.encoding # skip detect
48
+ elif isinstance(fname, BytesIO):
49
+ self.fb = fname
50
+ else:
51
+ self.fname = fname
52
+
53
+ if encoding == "detect":
54
+ encoding = detect_encoding(fname, low_confidence=low_confidence)
55
+ self.encoding = encoding
56
+ self.kwargs = kwargs
57
+
58
+ def __enter__(self) -> TextIOBase:
59
+ if isinstance(self.fb, BytesIO):
60
+ self.f = TextIOWrapper(self.fb, encoding=self.encoding)
61
+ elif self.f is None:
62
+ self.f = open(self.fname, "r", encoding=self.encoding, **self.kwargs)
63
+
64
+ return self.f
65
+
66
+ def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
67
+ if self.f:
68
+ self.f.close()
69
+ return True
70
+
71
+
72
+ def detect_encoding(fname: PathOrIO, *, low_confidence: float = 0.9) -> str:
73
+ import chardet
74
+
75
+ if isinstance(fname, TextIOBase):
76
+ return fname.encoding
77
+ elif isinstance(fname, BytesIO):
78
+ data = fname.read()
79
+ fname.seek(0, 0)
80
+ else:
81
+ with open(fname, "rb") as f:
82
+ data = f.read()
83
+
84
+ result = chardet.detect(data)
85
+ encoding = result["encoding"] or "utf-8"
86
+ if result["confidence"] < low_confidence:
87
+ warnings.warn(
88
+ f"parse as `{encoding}` with low confidence "
89
+ f"{result['confidence']} in `{fname}`"
90
+ )
91
+ return encoding