swcgeom 0.15.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.

Files changed (72) hide show
  1. swcgeom/__init__.py +26 -1
  2. swcgeom/analysis/__init__.py +21 -8
  3. swcgeom/analysis/feature_extractor.py +43 -18
  4. swcgeom/analysis/features.py +250 -0
  5. swcgeom/analysis/lmeasure.py +857 -0
  6. swcgeom/analysis/sholl.py +55 -29
  7. swcgeom/analysis/trunk.py +27 -11
  8. swcgeom/analysis/visualization.py +24 -9
  9. swcgeom/analysis/visualization3d.py +100 -0
  10. swcgeom/analysis/volume.py +19 -4
  11. swcgeom/core/__init__.py +32 -9
  12. swcgeom/core/branch.py +28 -7
  13. swcgeom/core/branch_tree.py +18 -4
  14. swcgeom/core/{segment.py → compartment.py} +31 -10
  15. swcgeom/core/node.py +31 -10
  16. swcgeom/core/path.py +37 -10
  17. swcgeom/core/population.py +103 -34
  18. swcgeom/core/swc.py +26 -10
  19. swcgeom/core/swc_utils/__init__.py +21 -7
  20. swcgeom/core/swc_utils/assembler.py +27 -1
  21. swcgeom/core/swc_utils/base.py +25 -12
  22. swcgeom/core/swc_utils/checker.py +31 -14
  23. swcgeom/core/swc_utils/io.py +24 -7
  24. swcgeom/core/swc_utils/normalizer.py +20 -4
  25. swcgeom/core/swc_utils/subtree.py +17 -2
  26. swcgeom/core/tree.py +85 -72
  27. swcgeom/core/tree_utils.py +31 -16
  28. swcgeom/core/tree_utils_impl.py +18 -3
  29. swcgeom/images/__init__.py +17 -2
  30. swcgeom/images/augmentation.py +24 -4
  31. swcgeom/images/contrast.py +122 -0
  32. swcgeom/images/folder.py +97 -39
  33. swcgeom/images/io.py +108 -121
  34. swcgeom/transforms/__init__.py +28 -10
  35. swcgeom/transforms/base.py +17 -2
  36. swcgeom/transforms/branch.py +74 -8
  37. swcgeom/transforms/branch_tree.py +82 -0
  38. swcgeom/transforms/geometry.py +22 -7
  39. swcgeom/transforms/image_preprocess.py +115 -0
  40. swcgeom/transforms/image_stack.py +37 -13
  41. swcgeom/transforms/images.py +184 -7
  42. swcgeom/transforms/mst.py +20 -5
  43. swcgeom/transforms/neurolucida_asc.py +508 -0
  44. swcgeom/transforms/path.py +15 -0
  45. swcgeom/transforms/population.py +16 -3
  46. swcgeom/transforms/tree.py +89 -31
  47. swcgeom/transforms/tree_assembler.py +23 -7
  48. swcgeom/utils/__init__.py +27 -11
  49. swcgeom/utils/debug.py +15 -0
  50. swcgeom/utils/download.py +59 -21
  51. swcgeom/utils/dsu.py +15 -0
  52. swcgeom/utils/ellipse.py +18 -4
  53. swcgeom/utils/file.py +15 -0
  54. swcgeom/utils/neuromorpho.py +439 -302
  55. swcgeom/utils/numpy_helper.py +29 -4
  56. swcgeom/utils/plotter_2d.py +151 -0
  57. swcgeom/utils/plotter_3d.py +48 -0
  58. swcgeom/utils/renderer.py +49 -145
  59. swcgeom/utils/sdf.py +24 -8
  60. swcgeom/utils/solid_geometry.py +16 -3
  61. swcgeom/utils/transforms.py +17 -4
  62. swcgeom/utils/volumetric_object.py +23 -10
  63. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
  64. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
  65. swcgeom-0.18.3.dist-info/RECORD +67 -0
  66. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/WHEEL +1 -1
  67. swcgeom/_version.py +0 -16
  68. swcgeom/analysis/branch_features.py +0 -67
  69. swcgeom/analysis/node_features.py +0 -121
  70. swcgeom/analysis/path_features.py +0 -37
  71. swcgeom-0.15.0.dist-info/RECORD +0 -62
  72. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
@@ -1,6 +1,22 @@
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
  """The segment is a branch with two nodes."""
2
17
 
3
- from typing import Generic, Iterable, List, TypeVar
18
+ from collections.abc import Iterable
19
+ from typing import Generic, TypeVar
4
20
 
5
21
  import numpy as np
6
22
  import numpy.typing as npt
@@ -9,11 +25,11 @@ from swcgeom.core.path import Path
9
25
  from swcgeom.core.swc import DictSWC, SWCTypeVar
10
26
  from swcgeom.core.swc_utils import SWCNames, get_names
11
27
 
12
- __all__ = ["Segment", "Segments"]
28
+ __all__ = ["Compartment", "Compartments", "Segment", "Segments"]
13
29
 
14
30
 
15
- class Segment(Path, Generic[SWCTypeVar]):
16
- r"""Segment attached to external object."""
31
+ class Compartment(Path, Generic[SWCTypeVar]):
32
+ r"""Compartment attached to external object."""
17
33
 
18
34
  attach: SWCTypeVar
19
35
  idx: npt.NDArray[np.int32]
@@ -27,7 +43,7 @@ class Segment(Path, Generic[SWCTypeVar]):
27
43
  def get_ndata(self, key: str) -> npt.NDArray:
28
44
  return self.attach.get_ndata(key)[self.idx]
29
45
 
30
- def detach(self) -> "Segment[DictSWC]":
46
+ def detach(self) -> "Compartment[DictSWC]":
31
47
  """Detach from current attached object."""
32
48
  # pylint: disable=consider-using-dict-items
33
49
  attact = DictSWC(
@@ -37,18 +53,18 @@ class Segment(Path, Generic[SWCTypeVar]):
37
53
  )
38
54
  attact.ndata[self.names.id] = self.id()
39
55
  attact.ndata[self.names.pid] = self.pid()
40
- return Segment(attact, 0, 1)
56
+ return Compartment(attact, 0, 1)
41
57
 
42
58
 
43
- SegmentT = TypeVar("SegmentT", bound=Segment)
59
+ T = TypeVar("T", bound=Compartment)
44
60
 
45
61
 
46
- class Segments(List[SegmentT]):
47
- r"""Segments contains a set of segments."""
62
+ class Compartments(list[T]):
63
+ r"""Comparments contains a set of comparment."""
48
64
 
49
65
  names: SWCNames
50
66
 
51
- def __init__(self, segments: Iterable[SegmentT]) -> None:
67
+ def __init__(self, segments: Iterable[T]) -> None:
52
68
  super().__init__(segments)
53
69
  self.names = self[0].names if len(self) > 0 else get_names()
54
70
 
@@ -94,3 +110,8 @@ class Segments(List[SegmentT]):
94
110
  The order of axis 1 is (parent, current node).
95
111
  """
96
112
  return np.array([s.get_ndata(key) for s in self])
113
+
114
+
115
+ # Aliases
116
+ Segment = Compartment
117
+ Segments = Compartments
swcgeom/core/node.py CHANGED
@@ -1,10 +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
  """Nueron node."""
2
17
 
3
- import warnings
4
- from typing import Any, Generic, Iterable
18
+ from collections.abc import Iterable
19
+ from typing import Any, Generic
5
20
 
6
21
  import numpy as np
7
22
  import numpy.typing as npt
23
+ from typing_extensions import deprecated
8
24
 
9
25
  from swcgeom.core.swc import DictSWC, SWCTypeVar
10
26
  from swcgeom.core.swc_utils import SWCNames
@@ -95,16 +111,21 @@ class Node(Generic[SWCTypeVar]):
95
111
  items = [self.id, self.type, x, y, z, r, self.pid]
96
112
  return " ".join(map(str, items))
97
113
 
98
- def child_ids(self) -> npt.NDArray[np.int32]:
99
- warnings.warn(
100
- "`Node.child_ids` has been deprecated since v0.3.1 and "
101
- "will be removed in next version",
102
- DeprecationWarning,
103
- )
104
- return self.attach.id()[self.attach.pid() == self.id]
114
+ def is_furcation(self) -> bool:
115
+ """Is furcation node."""
116
+ return np.count_nonzero(self.attach.pid() == self.id) > 1
105
117
 
118
+ @deprecated("Use is_furcation instead")
106
119
  def is_bifurcation(self) -> bool:
107
- return np.count_nonzero(self.attach.pid() == self.id) > 1
120
+ """Is furcation node.
121
+
122
+ Notes
123
+ -----
124
+ Deprecated due to the wrong spelling of furcation. For now, it
125
+ is just an alias of `is_furcation` and raise a warning. It will
126
+ be change to raise an error in the future.
127
+ """
128
+ return self.is_furcation()
108
129
 
109
130
  def is_tip(self) -> bool:
110
131
  return self.id not in self.attach.pid()
swcgeom/core/path.py CHANGED
@@ -1,9 +1,26 @@
1
- """Nueron node."""
2
-
3
- from typing import Generic, Iterable, Iterator, List, overload
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
+ """Nueron path."""
17
+
18
+ from collections.abc import Iterable, Iterator
19
+ from typing import Generic, overload
4
20
 
5
21
  import numpy as np
6
22
  import numpy.typing as npt
23
+ from typing_extensions import deprecated
7
24
 
8
25
  from swcgeom.core.node import Node
9
26
  from swcgeom.core.swc import DictSWC, SWCLike, SWCTypeVar
@@ -12,9 +29,9 @@ __all__ = ["Path"]
12
29
 
13
30
 
14
31
  class Path(SWCLike, Generic[SWCTypeVar]):
15
- """Neural path.
32
+ """Neuron path.
16
33
 
17
- A path is a linear set of points without bifurcations.
34
+ A path is a linear set of points without furcations.
18
35
  """
19
36
 
20
37
  attach: SWCTypeVar
@@ -31,7 +48,7 @@ class Path(SWCLike, Generic[SWCTypeVar]):
31
48
  self.source = self.attach.source
32
49
 
33
50
  def __iter__(self) -> Iterator[Node]:
34
- return (self.get_node(i) for i in range(len(self)))
51
+ return (self.node(i) for i in range(len(self)))
35
52
 
36
53
  def __len__(self) -> int:
37
54
  return self.id().shape[0]
@@ -43,14 +60,14 @@ class Path(SWCLike, Generic[SWCTypeVar]):
43
60
  @overload
44
61
  def __getitem__(self, key: int) -> Node: ...
45
62
  @overload
46
- def __getitem__(self, key: slice) -> List[Node]: ...
63
+ def __getitem__(self, key: slice) -> list[Node]: ...
47
64
  @overload
48
65
  def __getitem__(self, key: str) -> npt.NDArray: ...
49
66
  # fmt:on
50
67
 
51
68
  def __getitem__(self, key):
52
69
  if isinstance(key, slice):
53
- return [self.get_node(i) for i in range(*key.indices(len(self)))]
70
+ return [self.node(i) for i in range(*key.indices(len(self)))]
54
71
 
55
72
  if isinstance(key, (int, np.integer)):
56
73
  length = len(self)
@@ -60,7 +77,7 @@ class Path(SWCLike, Generic[SWCTypeVar]):
60
77
  if key < 0: # Handle negative indices
61
78
  key += length
62
79
 
63
- return self.get_node(key)
80
+ return self.node(key)
64
81
 
65
82
  if isinstance(key, str):
66
83
  return self.get_ndata(key)
@@ -73,7 +90,17 @@ class Path(SWCLike, Generic[SWCTypeVar]):
73
90
  def get_ndata(self, key: str) -> npt.NDArray:
74
91
  return self.attach.get_ndata(key)[self.idx]
75
92
 
93
+ @deprecated("Use `path.node` instead.")
76
94
  def get_node(self, idx: int | np.integer) -> Node:
95
+ """Get the count of intersection.
96
+
97
+ .. deprecated:: 0.16.0
98
+ Use :meth:`path.node` instead.
99
+ """
100
+
101
+ return self.node(idx)
102
+
103
+ def node(self, idx: int | np.integer) -> Node:
77
104
  return self.Node(self, idx)
78
105
 
79
106
  def detach(self) -> "Path[DictSWC]":
@@ -129,7 +156,7 @@ class Path(SWCLike, Generic[SWCTypeVar]):
129
156
  The end-to-end straight-line distance between start point and
130
157
  end point.
131
158
  """
132
- return np.linalg.norm(self.get_node(-1).xyz() - self.get_node(0).xyz()).item()
159
+ return np.linalg.norm(self.node(-1).xyz() - self.node(0).xyz()).item()
133
160
 
134
161
  def tortuosity(self) -> float:
135
162
  """Tortuosity of path.
@@ -1,22 +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
+
1
16
  """Neuron population is a set of tree."""
2
17
 
3
18
  import os
4
19
  import warnings
20
+ from collections.abc import Callable, Iterable, Iterator
21
+ from concurrent.futures import ProcessPoolExecutor
5
22
  from functools import reduce
6
- from typing import (
7
- Any,
8
- Dict,
9
- Iterable,
10
- Iterator,
11
- List,
12
- Optional,
13
- Protocol,
14
- cast,
15
- overload,
16
- )
23
+ from typing import Any, Optional, Protocol, TypeVar, cast, overload
17
24
 
18
25
  import numpy as np
19
26
  import numpy.typing as npt
27
+ from tqdm.contrib.concurrent import process_map
28
+ from typing_extensions import Self
20
29
 
21
30
  from swcgeom.core.swc import eswc_cols
22
31
  from swcgeom.core.tree import Tree
@@ -24,6 +33,9 @@ from swcgeom.core.tree import Tree
24
33
  __all__ = ["LazyLoadingTrees", "ChainTrees", "Population", "Populations"]
25
34
 
26
35
 
36
+ T = TypeVar("T")
37
+
38
+
27
39
  class Trees(Protocol):
28
40
  """Trees protocol support index and len."""
29
41
 
@@ -36,18 +48,19 @@ class Trees(Protocol):
36
48
  class LazyLoadingTrees:
37
49
  """Lazy loading trees."""
38
50
 
39
- swcs: List[str]
40
- trees: List[Tree | None]
41
- kwargs: Dict[str, Any]
51
+ swcs: list[str]
52
+ trees: list[Tree | None]
53
+ kwargs: dict[str, Any]
42
54
 
43
55
  def __init__(self, swcs: Iterable[str], **kwargs) -> None:
44
56
  """
45
57
  Paramters
46
58
  ---------
47
59
  swcs : List of str
48
- kwargs : Dict[str, Any]
60
+ kwargs : dict[str, Any]
49
61
  Forwarding to `Tree.from_swc`
50
62
  """
63
+
51
64
  super().__init__()
52
65
  self.swcs = list(swcs)
53
66
  self.trees = [None for _ in swcs]
@@ -61,6 +74,9 @@ class LazyLoadingTrees:
61
74
  def __len__(self) -> int:
62
75
  return len(self.swcs)
63
76
 
77
+ def __iter__(self) -> Iterator[Tree]:
78
+ return (self[i] for i in range(self.__len__()))
79
+
64
80
  def load(self, key: int) -> None:
65
81
  if self.trees[key] is None:
66
82
  self.trees[key] = Tree.from_swc(self.swcs[key], **self.kwargs)
@@ -69,7 +85,7 @@ class LazyLoadingTrees:
69
85
  class ChainTrees:
70
86
  """Chain trees."""
71
87
 
72
- trees: List[Trees]
88
+ trees: list[Trees]
73
89
  cumsum: npt.NDArray[np.int64]
74
90
 
75
91
  def __init__(self, trees: Iterable[Trees]) -> None:
@@ -92,6 +108,22 @@ class ChainTrees:
92
108
  def __len__(self) -> int:
93
109
  return self.cumsum[-1].item()
94
110
 
111
+ def __iter__(self) -> Iterator[Tree]:
112
+ return (self[i] for i in range(self.__len__()))
113
+
114
+
115
+ class NestTrees:
116
+ def __init__(self, trees: Trees, idx: Iterable[int], /) -> None:
117
+ super().__init__()
118
+ self.trees = trees
119
+ self.idx = list(idx)
120
+
121
+ def __getitem__(self, key: int, /) -> Tree:
122
+ return self.trees[self.idx[key]]
123
+
124
+ def __len__(self) -> int:
125
+ return len(self.idx)
126
+
95
127
 
96
128
  class Population:
97
129
  """Neuron population."""
@@ -131,13 +163,14 @@ class Population:
131
163
 
132
164
  # fmt:off
133
165
  @overload
134
- def __getitem__(self, key: slice) -> List[Tree]: ...
166
+ def __getitem__(self, key: slice) -> Trees: ...
135
167
  @overload
136
168
  def __getitem__(self, key: int) -> Tree: ...
137
169
  # fmt:on
138
- def __getitem__(self, key):
170
+ def __getitem__(self, key: int | slice):
139
171
  if isinstance(key, slice):
140
- return [cast(Tree, self.trees[i]) for i in range(*key.indices(len(self)))]
172
+ trees = NestTrees(self.trees, range(*key.indices(len(self))))
173
+ return cast(Trees, trees)
141
174
 
142
175
  if isinstance(key, (int, np.integer)):
143
176
  return cast(Tree, self.trees[int(key)])
@@ -153,15 +186,40 @@ class Population:
153
186
  def __repr__(self) -> str:
154
187
  return f"Neuron population in '{self.root}'"
155
188
 
189
+ def map(
190
+ self,
191
+ fn: Callable[[Tree], T],
192
+ *,
193
+ max_worker: Optional[int] = None,
194
+ verbose: bool = False,
195
+ ) -> Iterator[T]:
196
+ """Map a function to all trees in the population.
197
+
198
+ This is a straightforward interface for parallelizing
199
+ computations. The parameters are intentionally kept simple and
200
+ user-friendly. For more advanced control, consider using
201
+ `concurrent.futures` directly.
202
+ """
203
+
204
+ trees = (t for t in self.trees)
205
+
206
+ if verbose:
207
+ results = process_map(fn, trees, max_workers=max_worker)
208
+ else:
209
+ with ProcessPoolExecutor(max_worker) as p:
210
+ results = p.map(fn, trees)
211
+
212
+ return results
213
+
156
214
  @classmethod
157
- def from_swc(cls, root: str, ext: str = ".swc", **kwargs) -> "Population":
215
+ def from_swc(cls, root: str, ext: str = ".swc", **kwargs) -> Self:
158
216
  if not os.path.exists(root):
159
217
  raise FileNotFoundError(
160
218
  f"the root does not refers to an existing directory: {root}"
161
219
  )
162
220
 
163
221
  swcs = cls.find_swcs(root, ext)
164
- return Population(LazyLoadingTrees(swcs, **kwargs), root=root)
222
+ return cls(LazyLoadingTrees(swcs, **kwargs), root=root)
165
223
 
166
224
  @classmethod
167
225
  def from_eswc(
@@ -170,15 +228,15 @@ class Population:
170
228
  ext: str = ".eswc",
171
229
  extra_cols: Optional[Iterable[str]] = None,
172
230
  **kwargs,
173
- ) -> "Population":
231
+ ) -> Self:
174
232
  extra_cols = list(extra_cols) if extra_cols is not None else []
175
233
  extra_cols.extend(k for k, t in eswc_cols)
176
234
  return cls.from_swc(root, ext, extra_cols=extra_cols, **kwargs)
177
235
 
178
236
  @staticmethod
179
- def find_swcs(root: str, ext: str = ".swc", relpath: bool = False) -> List[str]:
237
+ def find_swcs(root: str, ext: str = ".swc", relpath: bool = False) -> list[str]:
180
238
  """Find all swc files."""
181
- swcs: List[str] = []
239
+ swcs: list[str] = []
182
240
  for r, _, files in os.walk(root):
183
241
  rr = os.path.relpath(r, root) if relpath else r
184
242
  fs = filter(lambda f: os.path.splitext(f)[-1] == ext, files)
@@ -191,8 +249,8 @@ class Populations:
191
249
  """A set of population."""
192
250
 
193
251
  len: int
194
- populations: List[Population]
195
- labels: List[str]
252
+ populations: list[Population]
253
+ labels: list[str]
196
254
 
197
255
  def __init__(
198
256
  self, populations: Iterable[Population], labels: Optional[Iterable[str]] = None
@@ -208,9 +266,9 @@ class Populations:
208
266
 
209
267
  # fmt:off
210
268
  @overload
211
- def __getitem__(self, key: slice) -> List[List[Tree]]: ...
269
+ def __getitem__(self, key: slice) -> list[list[Tree]]: ...
212
270
  @overload
213
- def __getitem__(self, key: int) -> List[Tree]: ...
271
+ def __getitem__(self, key: int) -> list[Tree]: ...
214
272
  # fmt:on
215
273
  def __getitem__(self, key):
216
274
  return [p[key] for p in self.populations]
@@ -219,7 +277,7 @@ class Populations:
219
277
  """Miniumn length of populations."""
220
278
  return self.len
221
279
 
222
- def __iter__(self) -> Iterator[List[Tree]]:
280
+ def __iter__(self) -> Iterator[list[Tree]]:
223
281
  return (self[i] for i in range(self.len))
224
282
 
225
283
  def __repr__(self) -> str:
@@ -235,7 +293,7 @@ class Populations:
235
293
  return Population(ChainTrees(p.trees for p in self.populations))
236
294
 
237
295
  @classmethod
238
- def from_swc( # pylint: disable=too-many-arguments
296
+ def from_swc(
239
297
  cls,
240
298
  roots: Iterable[str],
241
299
  ext: str = ".swc",
@@ -243,12 +301,12 @@ class Populations:
243
301
  check_same: bool = False,
244
302
  labels: Optional[Iterable[str]] = None,
245
303
  **kwargs,
246
- ) -> "Populations":
304
+ ) -> Self:
247
305
  """Get population from dirs.
248
306
 
249
307
  Parameters
250
308
  ----------
251
- roots : list of str
309
+ roots : List of str
252
310
  intersect : bool, default `True`
253
311
  Take the intersection of these populations.
254
312
  check_same : bool, default `False`
@@ -275,7 +333,7 @@ class Populations:
275
333
  )
276
334
  for i, d in enumerate(roots)
277
335
  ]
278
- return Populations(populations, labels=labels)
336
+ return cls(populations, labels=labels)
279
337
 
280
338
  @classmethod
281
339
  def from_eswc(
@@ -285,7 +343,7 @@ class Populations:
285
343
  *,
286
344
  ext: str = ".eswc",
287
345
  **kwargs,
288
- ) -> "Populations":
346
+ ) -> Self:
289
347
  extra_cols = list(extra_cols) if extra_cols is not None else []
290
348
  extra_cols.extend(k for k, t in eswc_cols)
291
349
  return cls.from_swc(roots, extra_cols=extra_cols, ext=ext, **kwargs)
@@ -299,3 +357,14 @@ def _get_idx(key: int, length: int) -> int:
299
357
  key += length
300
358
 
301
359
  return key
360
+
361
+
362
+ # experimental
363
+ def filter_population(
364
+ pop: Population, predicate: Callable[[Tree], bool]
365
+ ) -> "Population":
366
+ """Filter trees in the population."""
367
+
368
+ # TODO: how to avoid load trees
369
+ idx = [i for i, t in enumerate(pop) if predicate(t)]
370
+ return Population(NestTrees(pop.trees, idx), root=pop.root)
swcgeom/core/swc.py CHANGED
@@ -1,9 +1,25 @@
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
  """SWC format."""
2
17
 
3
18
  import warnings
4
19
  from abc import ABC, abstractmethod
20
+ from collections.abc import Iterable
5
21
  from copy import deepcopy
6
- from typing import Any, Dict, Iterable, List, Optional, Tuple, TypeVar, overload
22
+ from typing import Any, Optional, TypeVar, overload
7
23
 
8
24
  import numpy as np
9
25
  import numpy.typing as npt
@@ -32,7 +48,7 @@ __all__ = [
32
48
 
33
49
 
34
50
  swc_names_default = get_names()
35
- swc_cols: List[Tuple[str, npt.DTypeLike]] = [
51
+ swc_cols: list[tuple[str, npt.DTypeLike]] = [
36
52
  (swc_names_default.id, np.int32),
37
53
  (swc_names_default.type, np.int32),
38
54
  (swc_names_default.x, np.float32),
@@ -42,7 +58,7 @@ swc_cols: List[Tuple[str, npt.DTypeLike]] = [
42
58
  (swc_names_default.pid, np.int32),
43
59
  ]
44
60
 
45
- eswc_cols: List[Tuple[str, npt.DTypeLike]] = [
61
+ eswc_cols: list[tuple[str, npt.DTypeLike]] = [
46
62
  ("level", np.int32),
47
63
  ("mode", np.int32),
48
64
  ("timestamp", np.int32),
@@ -55,7 +71,7 @@ class SWCLike(ABC):
55
71
  """ABC of SWC."""
56
72
 
57
73
  source: str = ""
58
- comments: List[str] = []
74
+ comments: list[str] = []
59
75
  names: SWCNames
60
76
  types: SWCTypes
61
77
 
@@ -131,15 +147,15 @@ class SWCLike(ABC):
131
147
 
132
148
  # fmt: off
133
149
  @overload
134
- def to_swc(self, fname: str, *, extra_cols: List[str] | None = ..., source: bool | str = ..., id_offset: int = ...) -> None: ...
150
+ def to_swc(self, fname: str, *, extra_cols: list[str] | None = ..., source: bool | str = ..., id_offset: int = ...) -> None: ...
135
151
  @overload
136
- def to_swc(self, *, extra_cols: List[str] | None = ..., source: bool | str = ..., id_offset: int = ...) -> str: ...
152
+ def to_swc(self, *, extra_cols: list[str] | None = ..., source: bool | str = ..., id_offset: int = ...) -> str: ...
137
153
  # fmt: on
138
154
  def to_swc(
139
155
  self,
140
156
  fname: Optional[str] = None,
141
157
  *,
142
- extra_cols: Optional[List[str]] = None,
158
+ extra_cols: Optional[list[str]] = None,
143
159
  source: bool | str = True,
144
160
  comments: bool = True,
145
161
  id_offset: int = 1,
@@ -177,7 +193,7 @@ class SWCLike(ABC):
177
193
  self,
178
194
  fname: Optional[str] = None,
179
195
  swc_path: Optional[str] = None,
180
- extra_cols: Optional[List[str]] = None,
196
+ extra_cols: Optional[list[str]] = None,
181
197
  **kwargs,
182
198
  ) -> str | None:
183
199
  if swc_path is None:
@@ -199,7 +215,7 @@ SWCTypeVar = TypeVar("SWCTypeVar", bound=SWCLike)
199
215
  class DictSWC(SWCLike):
200
216
  """SWC implementation on dict."""
201
217
 
202
- ndata: Dict[str, npt.NDArray]
218
+ ndata: dict[str, npt.NDArray]
203
219
 
204
220
  def __init__(
205
221
  self,
@@ -221,7 +237,7 @@ class DictSWC(SWCLike):
221
237
  def values(self) -> Iterable[npt.NDArray[Any]]:
222
238
  return self.ndata.values()
223
239
 
224
- def items(self) -> Iterable[Tuple[str, npt.NDArray[Any]]]:
240
+ def items(self) -> Iterable[tuple[str, npt.NDArray[Any]]]:
225
241
  return self.ndata.items()
226
242
 
227
243
  def get_ndata(self, key: str) -> npt.NDArray[Any]:
@@ -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
  """SWC format utils.
2
17
 
3
18
  Notes
@@ -8,10 +23,9 @@ If you use the method here, please review the code more frequently, we
8
23
  will try to flag all breaking changes but NO promises.
9
24
  """
10
25
 
11
-
12
- from swcgeom.core.swc_utils.assembler import *
13
- from swcgeom.core.swc_utils.base import *
14
- from swcgeom.core.swc_utils.checker import *
15
- from swcgeom.core.swc_utils.io import *
16
- from swcgeom.core.swc_utils.normalizer import *
17
- from swcgeom.core.swc_utils.subtree import *
26
+ from swcgeom.core.swc_utils.assembler import * # noqa: F403
27
+ from swcgeom.core.swc_utils.base import * # noqa: F403
28
+ from swcgeom.core.swc_utils.checker import * # noqa: F403
29
+ from swcgeom.core.swc_utils.io import * # noqa: F403
30
+ from swcgeom.core.swc_utils.normalizer import * # noqa: F403
31
+ from swcgeom.core.swc_utils.subtree import * # noqa: F403