swcgeom 0.19.4__cp313-cp313-macosx_14_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of swcgeom might be problematic. Click here for more details.
- swcgeom/__init__.py +21 -0
- swcgeom/analysis/__init__.py +13 -0
- swcgeom/analysis/feature_extractor.py +454 -0
- swcgeom/analysis/features.py +218 -0
- swcgeom/analysis/lmeasure.py +750 -0
- swcgeom/analysis/sholl.py +201 -0
- swcgeom/analysis/trunk.py +183 -0
- swcgeom/analysis/visualization.py +191 -0
- swcgeom/analysis/visualization3d.py +81 -0
- swcgeom/analysis/volume.py +143 -0
- swcgeom/core/__init__.py +19 -0
- swcgeom/core/branch.py +129 -0
- swcgeom/core/branch_tree.py +65 -0
- swcgeom/core/compartment.py +107 -0
- swcgeom/core/node.py +130 -0
- swcgeom/core/path.py +155 -0
- swcgeom/core/population.py +341 -0
- swcgeom/core/swc.py +247 -0
- swcgeom/core/swc_utils/__init__.py +19 -0
- swcgeom/core/swc_utils/assembler.py +35 -0
- swcgeom/core/swc_utils/base.py +180 -0
- swcgeom/core/swc_utils/checker.py +107 -0
- swcgeom/core/swc_utils/io.py +204 -0
- swcgeom/core/swc_utils/normalizer.py +163 -0
- swcgeom/core/swc_utils/subtree.py +70 -0
- swcgeom/core/tree.py +384 -0
- swcgeom/core/tree_utils.py +277 -0
- swcgeom/core/tree_utils_impl.py +58 -0
- swcgeom/images/__init__.py +9 -0
- swcgeom/images/augmentation.py +149 -0
- swcgeom/images/contrast.py +87 -0
- swcgeom/images/folder.py +217 -0
- swcgeom/images/io.py +578 -0
- swcgeom/images/loaders/__init__.py +8 -0
- swcgeom/images/loaders/pbd.cpython-313-darwin.so +0 -0
- swcgeom/images/loaders/pbd.pyx +523 -0
- swcgeom/images/loaders/raw.cpython-313-darwin.so +0 -0
- swcgeom/images/loaders/raw.pyx +183 -0
- swcgeom/transforms/__init__.py +20 -0
- swcgeom/transforms/base.py +136 -0
- swcgeom/transforms/branch.py +223 -0
- swcgeom/transforms/branch_tree.py +74 -0
- swcgeom/transforms/geometry.py +270 -0
- swcgeom/transforms/image_preprocess.py +107 -0
- swcgeom/transforms/image_stack.py +219 -0
- swcgeom/transforms/images.py +206 -0
- swcgeom/transforms/mst.py +183 -0
- swcgeom/transforms/neurolucida_asc.py +498 -0
- swcgeom/transforms/path.py +56 -0
- swcgeom/transforms/population.py +36 -0
- swcgeom/transforms/tree.py +265 -0
- swcgeom/transforms/tree_assembler.py +161 -0
- swcgeom/utils/__init__.py +18 -0
- swcgeom/utils/debug.py +23 -0
- swcgeom/utils/download.py +119 -0
- swcgeom/utils/dsu.py +58 -0
- swcgeom/utils/ellipse.py +131 -0
- swcgeom/utils/file.py +90 -0
- swcgeom/utils/neuromorpho.py +581 -0
- swcgeom/utils/numpy_helper.py +70 -0
- swcgeom/utils/plotter_2d.py +134 -0
- swcgeom/utils/plotter_3d.py +35 -0
- swcgeom/utils/renderer.py +145 -0
- swcgeom/utils/sdf.py +324 -0
- swcgeom/utils/solid_geometry.py +154 -0
- swcgeom/utils/transforms.py +367 -0
- swcgeom/utils/volumetric_object.py +483 -0
- swcgeom-0.19.4.dist-info/METADATA +86 -0
- swcgeom-0.19.4.dist-info/RECORD +72 -0
- swcgeom-0.19.4.dist-info/WHEEL +5 -0
- swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
- swcgeom-0.19.4.dist-info/top_level.txt +1 -0
swcgeom/core/path.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Nueron path."""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Iterable, Iterator
|
|
9
|
+
from typing import Generic, overload
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import numpy.typing as npt
|
|
13
|
+
from typing_extensions import deprecated
|
|
14
|
+
|
|
15
|
+
from swcgeom.core.node import Node
|
|
16
|
+
from swcgeom.core.swc import DictSWC, SWCLike, SWCTypeVar
|
|
17
|
+
|
|
18
|
+
__all__ = ["Path"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Path(SWCLike, Generic[SWCTypeVar]):
|
|
22
|
+
"""Neuron path.
|
|
23
|
+
|
|
24
|
+
A path is a linear set of points without furcations.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
attach: SWCTypeVar
|
|
28
|
+
idx: npt.NDArray[np.int32]
|
|
29
|
+
|
|
30
|
+
class Node(Node["Path"]):
|
|
31
|
+
"""Node of neuron tree."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, attach: SWCTypeVar, idx: npt.ArrayLike) -> None:
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.attach = attach
|
|
36
|
+
self.names = attach.names
|
|
37
|
+
self.idx = np.array(idx, dtype=np.int32)
|
|
38
|
+
self.source = self.attach.source
|
|
39
|
+
|
|
40
|
+
def __iter__(self) -> Iterator[Node]:
|
|
41
|
+
return (self.node(i) for i in range(len(self)))
|
|
42
|
+
|
|
43
|
+
def __len__(self) -> int:
|
|
44
|
+
return self.id().shape[0]
|
|
45
|
+
|
|
46
|
+
def __repr__(self) -> str:
|
|
47
|
+
return f"Neuron path with {len(self)} nodes."
|
|
48
|
+
|
|
49
|
+
@overload
|
|
50
|
+
def __getitem__(self, key: int) -> Node: ...
|
|
51
|
+
@overload
|
|
52
|
+
def __getitem__(self, key: slice) -> list[Node]: ...
|
|
53
|
+
@overload
|
|
54
|
+
def __getitem__(self, key: str) -> npt.NDArray: ...
|
|
55
|
+
def __getitem__(self, key):
|
|
56
|
+
if isinstance(key, slice):
|
|
57
|
+
return [self.node(i) for i in range(*key.indices(len(self)))]
|
|
58
|
+
|
|
59
|
+
if isinstance(key, (int, np.integer)):
|
|
60
|
+
length = len(self)
|
|
61
|
+
if key < -length or key >= length:
|
|
62
|
+
raise IndexError(f"The index ({key}) is out of range.")
|
|
63
|
+
|
|
64
|
+
if key < 0: # Handle negative indices
|
|
65
|
+
key += length
|
|
66
|
+
|
|
67
|
+
return self.node(key)
|
|
68
|
+
|
|
69
|
+
if isinstance(key, str):
|
|
70
|
+
return self.get_ndata(key)
|
|
71
|
+
|
|
72
|
+
raise TypeError("Invalid argument type.")
|
|
73
|
+
|
|
74
|
+
def keys(self) -> Iterable[str]:
|
|
75
|
+
return self.attach.keys()
|
|
76
|
+
|
|
77
|
+
def get_ndata(self, key: str) -> npt.NDArray:
|
|
78
|
+
return self.attach.get_ndata(key)[self.idx]
|
|
79
|
+
|
|
80
|
+
@deprecated("Use `path.node` instead.")
|
|
81
|
+
def get_node(self, idx: int | np.integer) -> Node:
|
|
82
|
+
"""Get the count of intersection.
|
|
83
|
+
|
|
84
|
+
.. deprecated:: 0.16.0
|
|
85
|
+
Use :meth:`path.node` instead.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
return self.node(idx)
|
|
89
|
+
|
|
90
|
+
def node(self, idx: int | np.integer) -> Node:
|
|
91
|
+
return self.Node(self, idx)
|
|
92
|
+
|
|
93
|
+
def detach(self) -> "Path[DictSWC]":
|
|
94
|
+
"""Detach from current attached object."""
|
|
95
|
+
# pylint: disable-next=consider-using-dict-items
|
|
96
|
+
attact = DictSWC(
|
|
97
|
+
**{k: self.get_ndata(k) for k in self.keys()},
|
|
98
|
+
source=self.source,
|
|
99
|
+
names=self.names,
|
|
100
|
+
)
|
|
101
|
+
attact.ndata[self.names.id] = self.id()
|
|
102
|
+
attact.ndata[self.names.pid] = self.pid()
|
|
103
|
+
return Path(attact, self.id())
|
|
104
|
+
|
|
105
|
+
def id(self) -> npt.NDArray[np.int32]: # pylint: disable=invalid-name
|
|
106
|
+
"""Get the ids of shape (n_sample,).
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
a consecutively incremented id.
|
|
110
|
+
|
|
111
|
+
See Also:
|
|
112
|
+
self.origin_id
|
|
113
|
+
"""
|
|
114
|
+
return np.arange(len(self.origin_id()), dtype=np.int32)
|
|
115
|
+
|
|
116
|
+
def pid(self) -> npt.NDArray[np.int32]:
|
|
117
|
+
"""Get the ids of shape (n_sample,).
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
a consecutively incremented pid.
|
|
121
|
+
|
|
122
|
+
See Also:
|
|
123
|
+
self.origin_pid
|
|
124
|
+
"""
|
|
125
|
+
return np.arange(-1, len(self.origin_id()) - 1, dtype=np.int32)
|
|
126
|
+
|
|
127
|
+
def origin_id(self) -> npt.NDArray[np.int32]:
|
|
128
|
+
"""Get the original id."""
|
|
129
|
+
return self.get_ndata(self.names.id)
|
|
130
|
+
|
|
131
|
+
def origin_pid(self) -> npt.NDArray[np.int32]:
|
|
132
|
+
"""Get the original pid."""
|
|
133
|
+
return self.get_ndata(self.names.pid)
|
|
134
|
+
|
|
135
|
+
def length(self) -> float:
|
|
136
|
+
"""Sum of length of stems."""
|
|
137
|
+
xyz = self.xyz()
|
|
138
|
+
return np.sum(np.linalg.norm(xyz[1:] - xyz[:-1], axis=1)).item()
|
|
139
|
+
|
|
140
|
+
def straight_line_distance(self) -> float:
|
|
141
|
+
"""Straight-line distance of path.
|
|
142
|
+
|
|
143
|
+
The end-to-end straight-line distance between start point and end point.
|
|
144
|
+
"""
|
|
145
|
+
return np.linalg.norm(self.node(-1).xyz() - self.node(0).xyz()).item()
|
|
146
|
+
|
|
147
|
+
def tortuosity(self) -> float:
|
|
148
|
+
"""Tortuosity of path.
|
|
149
|
+
|
|
150
|
+
The straight-line distance between two consecutive branch points divided by the
|
|
151
|
+
length of the neuronal path between those points.
|
|
152
|
+
"""
|
|
153
|
+
if (length := self.length()) == 0:
|
|
154
|
+
return 1
|
|
155
|
+
return self.straight_line_distance() / length
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
|
|
2
|
+
# SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
|
|
3
|
+
#
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
"""Neuron population is a set of tree."""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import warnings
|
|
10
|
+
from collections.abc import Callable, Iterable, Iterator
|
|
11
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
12
|
+
from functools import reduce
|
|
13
|
+
from typing import Any, Protocol, TypeVar, cast, overload
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import numpy.typing as npt
|
|
17
|
+
from tqdm.contrib.concurrent import process_map
|
|
18
|
+
from typing_extensions import Self
|
|
19
|
+
|
|
20
|
+
from swcgeom.core.swc import eswc_cols
|
|
21
|
+
from swcgeom.core.tree import Tree
|
|
22
|
+
|
|
23
|
+
__all__ = ["LazyLoadingTrees", "ChainTrees", "Population", "Populations"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
T = TypeVar("T")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Trees(Protocol):
|
|
30
|
+
"""Trees protocol support index and len."""
|
|
31
|
+
|
|
32
|
+
def __getitem__(self, key: int, /) -> Tree: ...
|
|
33
|
+
def __len__(self) -> int: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LazyLoadingTrees:
|
|
37
|
+
"""Lazy loading trees."""
|
|
38
|
+
|
|
39
|
+
swcs: list[str]
|
|
40
|
+
trees: list[Tree | None]
|
|
41
|
+
kwargs: dict[str, Any]
|
|
42
|
+
|
|
43
|
+
def __init__(self, swcs: Iterable[str], **kwargs) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Args:
|
|
46
|
+
swcs: List of str
|
|
47
|
+
kwargs: Forwarding to `Tree.from_swc`
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
super().__init__()
|
|
51
|
+
self.swcs = list(swcs)
|
|
52
|
+
self.trees = [None for _ in swcs]
|
|
53
|
+
self.kwargs = kwargs
|
|
54
|
+
|
|
55
|
+
def __getitem__(self, key: int, /) -> Tree:
|
|
56
|
+
idx = _get_idx(key, len(self))
|
|
57
|
+
self.load(idx)
|
|
58
|
+
return cast(Tree, self.trees[idx])
|
|
59
|
+
|
|
60
|
+
def __len__(self) -> int:
|
|
61
|
+
return len(self.swcs)
|
|
62
|
+
|
|
63
|
+
def __iter__(self) -> Iterator[Tree]:
|
|
64
|
+
return (self[i] for i in range(self.__len__()))
|
|
65
|
+
|
|
66
|
+
def load(self, key: int) -> None:
|
|
67
|
+
if self.trees[key] is None:
|
|
68
|
+
self.trees[key] = Tree.from_swc(self.swcs[key], **self.kwargs)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ChainTrees:
|
|
72
|
+
"""Chain trees."""
|
|
73
|
+
|
|
74
|
+
trees: list[Trees]
|
|
75
|
+
cumsum: npt.NDArray[np.int64]
|
|
76
|
+
|
|
77
|
+
def __init__(self, trees: Iterable[Trees]) -> None:
|
|
78
|
+
super().__init__()
|
|
79
|
+
self.trees = list(trees)
|
|
80
|
+
self.cumsum = np.cumsum([0] + [len(ts) for ts in trees])
|
|
81
|
+
|
|
82
|
+
def __getitem__(self, key: int, /) -> Tree:
|
|
83
|
+
i, j = 1, len(self.trees) # cumsum[0] === 0
|
|
84
|
+
idx = _get_idx(key, len(self))
|
|
85
|
+
while i < j:
|
|
86
|
+
mid = (i + j) // 2
|
|
87
|
+
if self.cumsum[mid] <= idx:
|
|
88
|
+
i = mid + 1
|
|
89
|
+
else:
|
|
90
|
+
j = mid
|
|
91
|
+
|
|
92
|
+
return self.trees[i - 1][idx - self.cumsum[i - 1]]
|
|
93
|
+
|
|
94
|
+
def __len__(self) -> int:
|
|
95
|
+
return self.cumsum[-1].item()
|
|
96
|
+
|
|
97
|
+
def __iter__(self) -> Iterator[Tree]:
|
|
98
|
+
return (self[i] for i in range(self.__len__()))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class NestTrees:
|
|
102
|
+
def __init__(self, trees: Trees, idx: Iterable[int], /) -> None:
|
|
103
|
+
super().__init__()
|
|
104
|
+
self.trees = trees
|
|
105
|
+
self.idx = list(idx)
|
|
106
|
+
|
|
107
|
+
def __getitem__(self, key: int, /) -> Tree:
|
|
108
|
+
return self.trees[self.idx[key]]
|
|
109
|
+
|
|
110
|
+
def __len__(self) -> int:
|
|
111
|
+
return len(self.idx)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class Population:
|
|
115
|
+
"""Neuron population."""
|
|
116
|
+
|
|
117
|
+
trees: Trees
|
|
118
|
+
|
|
119
|
+
@overload
|
|
120
|
+
def __init__(
|
|
121
|
+
self, swcs: Iterable[str], lazy_loading: bool = ..., root: str = ..., **kwargs
|
|
122
|
+
) -> None: ...
|
|
123
|
+
@overload
|
|
124
|
+
def __init__(self, trees: Trees, /, *, root: str = "") -> None: ...
|
|
125
|
+
def __init__(self, swcs, lazy_loading=True, root="", **kwargs) -> None:
|
|
126
|
+
super().__init__()
|
|
127
|
+
if len(swcs) > 0 and isinstance(swcs[0], str):
|
|
128
|
+
warnings.warn(
|
|
129
|
+
"`Population(swcs)` has been replaced by "
|
|
130
|
+
"`Population(LazyLoadingTrees(swcs))` since v0.8.0 thus we can create "
|
|
131
|
+
"a population from a group of trees, and this will be removed in next "
|
|
132
|
+
"version",
|
|
133
|
+
DeprecationWarning,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
trees = LazyLoadingTrees(swcs, **kwargs)
|
|
137
|
+
if not lazy_loading:
|
|
138
|
+
for i in range(len(swcs)):
|
|
139
|
+
trees.load(i)
|
|
140
|
+
else:
|
|
141
|
+
trees = swcs
|
|
142
|
+
|
|
143
|
+
self.trees = trees
|
|
144
|
+
self.root = root
|
|
145
|
+
|
|
146
|
+
if len(swcs) == 0:
|
|
147
|
+
warnings.warn(f"no trees in population from '{root}'")
|
|
148
|
+
|
|
149
|
+
@overload
|
|
150
|
+
def __getitem__(self, key: slice) -> Trees: ...
|
|
151
|
+
@overload
|
|
152
|
+
def __getitem__(self, key: int) -> Tree: ...
|
|
153
|
+
def __getitem__(self, key: int | slice):
|
|
154
|
+
if isinstance(key, slice):
|
|
155
|
+
trees = NestTrees(self.trees, range(*key.indices(len(self))))
|
|
156
|
+
return cast(Trees, trees)
|
|
157
|
+
|
|
158
|
+
if isinstance(key, (int, np.integer)):
|
|
159
|
+
return cast(Tree, self.trees[int(key)])
|
|
160
|
+
|
|
161
|
+
raise TypeError("Invalid argument type.")
|
|
162
|
+
|
|
163
|
+
def __len__(self) -> int:
|
|
164
|
+
return len(self.trees)
|
|
165
|
+
|
|
166
|
+
def __iter__(self) -> Iterator[Tree]:
|
|
167
|
+
return (self[i] for i in range(self.__len__()))
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
return f"Neuron population in '{self.root}'"
|
|
171
|
+
|
|
172
|
+
def map(
|
|
173
|
+
self,
|
|
174
|
+
fn: Callable[[Tree], T],
|
|
175
|
+
*,
|
|
176
|
+
max_worker: int | None = None,
|
|
177
|
+
verbose: bool = False,
|
|
178
|
+
) -> Iterator[T]:
|
|
179
|
+
"""Map a function to all trees in the population.
|
|
180
|
+
|
|
181
|
+
This is a straightforward interface for parallelizing computations. The
|
|
182
|
+
parameters are intentionally kept simple and user-friendly. For more advanced
|
|
183
|
+
control, consider using `concurrent.futures` directly.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
trees = (t for t in self.trees)
|
|
187
|
+
|
|
188
|
+
if verbose:
|
|
189
|
+
results = process_map(fn, trees, max_workers=max_worker)
|
|
190
|
+
else:
|
|
191
|
+
with ProcessPoolExecutor(max_worker) as p:
|
|
192
|
+
results = p.map(fn, trees)
|
|
193
|
+
|
|
194
|
+
return results
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_swc(cls, root: str, ext: str = ".swc", **kwargs) -> Self:
|
|
198
|
+
if not os.path.exists(root):
|
|
199
|
+
raise FileNotFoundError(
|
|
200
|
+
f"the root does not refers to an existing directory: {root}"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
swcs = cls.find_swcs(root, ext)
|
|
204
|
+
return cls(LazyLoadingTrees(swcs, **kwargs), root=root)
|
|
205
|
+
|
|
206
|
+
@classmethod
|
|
207
|
+
def from_eswc(
|
|
208
|
+
cls,
|
|
209
|
+
root: str,
|
|
210
|
+
ext: str = ".eswc",
|
|
211
|
+
extra_cols: Iterable[str] | None = None,
|
|
212
|
+
**kwargs,
|
|
213
|
+
) -> Self:
|
|
214
|
+
extra_cols = list(extra_cols) if extra_cols is not None else []
|
|
215
|
+
extra_cols.extend(k for k, _ in eswc_cols)
|
|
216
|
+
return cls.from_swc(root, ext, extra_cols=extra_cols, **kwargs)
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def find_swcs(root: str, ext: str = ".swc", relpath: bool = False) -> list[str]:
|
|
220
|
+
"""Find all swc files."""
|
|
221
|
+
swcs: list[str] = []
|
|
222
|
+
for r, _, files in os.walk(root):
|
|
223
|
+
rr = os.path.relpath(r, root) if relpath else r
|
|
224
|
+
fs = filter(lambda f: os.path.splitext(f)[-1] == ext, files)
|
|
225
|
+
swcs.extend(os.path.join(rr, f) for f in fs)
|
|
226
|
+
|
|
227
|
+
return swcs
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class Populations:
|
|
231
|
+
"""A set of population."""
|
|
232
|
+
|
|
233
|
+
len: int
|
|
234
|
+
populations: list[Population]
|
|
235
|
+
labels: list[str]
|
|
236
|
+
|
|
237
|
+
def __init__(
|
|
238
|
+
self, populations: Iterable[Population], labels: Iterable[str] | None = None
|
|
239
|
+
) -> None:
|
|
240
|
+
self.len = min(len(p) for p in populations)
|
|
241
|
+
self.populations = list(populations)
|
|
242
|
+
|
|
243
|
+
labels = list(labels) if labels is not None else ["" for i in populations]
|
|
244
|
+
assert len(labels) == len(self.populations), (
|
|
245
|
+
f"got {len(self.populations)} populations, but has {len(labels)} labels"
|
|
246
|
+
)
|
|
247
|
+
self.labels = labels
|
|
248
|
+
|
|
249
|
+
@overload
|
|
250
|
+
def __getitem__(self, key: slice) -> list[list[Tree]]: ...
|
|
251
|
+
@overload
|
|
252
|
+
def __getitem__(self, key: int) -> list[Tree]: ...
|
|
253
|
+
def __getitem__(self, key):
|
|
254
|
+
return [p[key] for p in self.populations]
|
|
255
|
+
|
|
256
|
+
def __len__(self) -> int:
|
|
257
|
+
"""Miniumn length of populations."""
|
|
258
|
+
return self.len
|
|
259
|
+
|
|
260
|
+
def __iter__(self) -> Iterator[list[Tree]]:
|
|
261
|
+
return (self[i] for i in range(self.len))
|
|
262
|
+
|
|
263
|
+
def __repr__(self) -> str:
|
|
264
|
+
return (
|
|
265
|
+
f"A cluster of {self.num_of_populations()} neuron populations, "
|
|
266
|
+
f"each containing at least {self.len} trees"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
def num_of_populations(self) -> int:
|
|
270
|
+
return len(self.populations)
|
|
271
|
+
|
|
272
|
+
def to_population(self) -> Population:
|
|
273
|
+
return Population(ChainTrees(p.trees for p in self.populations))
|
|
274
|
+
|
|
275
|
+
@classmethod
|
|
276
|
+
def from_swc(
|
|
277
|
+
cls,
|
|
278
|
+
roots: Iterable[str],
|
|
279
|
+
ext: str = ".swc",
|
|
280
|
+
intersect: bool = True,
|
|
281
|
+
check_same: bool = False,
|
|
282
|
+
labels: Iterable[str] | None = None,
|
|
283
|
+
**kwargs,
|
|
284
|
+
) -> Self:
|
|
285
|
+
"""Get population from dirs.
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
roots: List of str
|
|
289
|
+
intersect: Take the intersection of these populations.
|
|
290
|
+
check_same: Check if the directories contains the same swc.
|
|
291
|
+
labels: Label of populations.
|
|
292
|
+
**kwargs: Forwarding to `Population`.
|
|
293
|
+
"""
|
|
294
|
+
fs = [Population.find_swcs(d, ext=ext, relpath=True) for d in roots]
|
|
295
|
+
if intersect:
|
|
296
|
+
inter = list(reduce(lambda a, b: set(a).intersection(set(b)), fs))
|
|
297
|
+
if len(inter) == 0:
|
|
298
|
+
warnings.warn("no intersection among populations")
|
|
299
|
+
|
|
300
|
+
fs = [inter for _ in roots]
|
|
301
|
+
elif check_same:
|
|
302
|
+
assert [fs[0] == a for a in fs[1:]], "not the same among populations"
|
|
303
|
+
|
|
304
|
+
populations = [
|
|
305
|
+
Population(
|
|
306
|
+
LazyLoadingTrees([os.path.join(d, p) for p in fs[i]], **kwargs), root=d
|
|
307
|
+
)
|
|
308
|
+
for i, d in enumerate(roots)
|
|
309
|
+
]
|
|
310
|
+
return cls(populations, labels=labels)
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def from_eswc(
|
|
314
|
+
cls,
|
|
315
|
+
roots: Iterable[str],
|
|
316
|
+
extra_cols: Iterable[str] | None = None,
|
|
317
|
+
*,
|
|
318
|
+
ext: str = ".eswc",
|
|
319
|
+
**kwargs,
|
|
320
|
+
) -> Self:
|
|
321
|
+
extra_cols = list(extra_cols) if extra_cols is not None else []
|
|
322
|
+
extra_cols.extend(k for k, _ in eswc_cols)
|
|
323
|
+
return cls.from_swc(roots, extra_cols=extra_cols, ext=ext, **kwargs)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _get_idx(key: int, length: int) -> int:
|
|
327
|
+
if key < -length or key >= length:
|
|
328
|
+
raise IndexError(f"The index ({key}) is out of range.")
|
|
329
|
+
|
|
330
|
+
if key < 0: # Handle negative indices
|
|
331
|
+
key += length
|
|
332
|
+
|
|
333
|
+
return key
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# experimental
|
|
337
|
+
def filter_population(pop: Population, predicate: Callable[[Tree], bool]) -> Population:
|
|
338
|
+
"""Filter trees in the population."""
|
|
339
|
+
# TODO: how to avoid load trees
|
|
340
|
+
idx = [i for i, t in enumerate(pop) if predicate(t)]
|
|
341
|
+
return Population(NestTrees(pop.trees, idx), root=pop.root)
|