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.

Files changed (72) hide show
  1. swcgeom/__init__.py +21 -0
  2. swcgeom/analysis/__init__.py +13 -0
  3. swcgeom/analysis/feature_extractor.py +454 -0
  4. swcgeom/analysis/features.py +218 -0
  5. swcgeom/analysis/lmeasure.py +750 -0
  6. swcgeom/analysis/sholl.py +201 -0
  7. swcgeom/analysis/trunk.py +183 -0
  8. swcgeom/analysis/visualization.py +191 -0
  9. swcgeom/analysis/visualization3d.py +81 -0
  10. swcgeom/analysis/volume.py +143 -0
  11. swcgeom/core/__init__.py +19 -0
  12. swcgeom/core/branch.py +129 -0
  13. swcgeom/core/branch_tree.py +65 -0
  14. swcgeom/core/compartment.py +107 -0
  15. swcgeom/core/node.py +130 -0
  16. swcgeom/core/path.py +155 -0
  17. swcgeom/core/population.py +341 -0
  18. swcgeom/core/swc.py +247 -0
  19. swcgeom/core/swc_utils/__init__.py +19 -0
  20. swcgeom/core/swc_utils/assembler.py +35 -0
  21. swcgeom/core/swc_utils/base.py +180 -0
  22. swcgeom/core/swc_utils/checker.py +107 -0
  23. swcgeom/core/swc_utils/io.py +204 -0
  24. swcgeom/core/swc_utils/normalizer.py +163 -0
  25. swcgeom/core/swc_utils/subtree.py +70 -0
  26. swcgeom/core/tree.py +384 -0
  27. swcgeom/core/tree_utils.py +277 -0
  28. swcgeom/core/tree_utils_impl.py +58 -0
  29. swcgeom/images/__init__.py +9 -0
  30. swcgeom/images/augmentation.py +149 -0
  31. swcgeom/images/contrast.py +87 -0
  32. swcgeom/images/folder.py +217 -0
  33. swcgeom/images/io.py +578 -0
  34. swcgeom/images/loaders/__init__.py +8 -0
  35. swcgeom/images/loaders/pbd.cpython-313-darwin.so +0 -0
  36. swcgeom/images/loaders/pbd.pyx +523 -0
  37. swcgeom/images/loaders/raw.cpython-313-darwin.so +0 -0
  38. swcgeom/images/loaders/raw.pyx +183 -0
  39. swcgeom/transforms/__init__.py +20 -0
  40. swcgeom/transforms/base.py +136 -0
  41. swcgeom/transforms/branch.py +223 -0
  42. swcgeom/transforms/branch_tree.py +74 -0
  43. swcgeom/transforms/geometry.py +270 -0
  44. swcgeom/transforms/image_preprocess.py +107 -0
  45. swcgeom/transforms/image_stack.py +219 -0
  46. swcgeom/transforms/images.py +206 -0
  47. swcgeom/transforms/mst.py +183 -0
  48. swcgeom/transforms/neurolucida_asc.py +498 -0
  49. swcgeom/transforms/path.py +56 -0
  50. swcgeom/transforms/population.py +36 -0
  51. swcgeom/transforms/tree.py +265 -0
  52. swcgeom/transforms/tree_assembler.py +161 -0
  53. swcgeom/utils/__init__.py +18 -0
  54. swcgeom/utils/debug.py +23 -0
  55. swcgeom/utils/download.py +119 -0
  56. swcgeom/utils/dsu.py +58 -0
  57. swcgeom/utils/ellipse.py +131 -0
  58. swcgeom/utils/file.py +90 -0
  59. swcgeom/utils/neuromorpho.py +581 -0
  60. swcgeom/utils/numpy_helper.py +70 -0
  61. swcgeom/utils/plotter_2d.py +134 -0
  62. swcgeom/utils/plotter_3d.py +35 -0
  63. swcgeom/utils/renderer.py +145 -0
  64. swcgeom/utils/sdf.py +324 -0
  65. swcgeom/utils/solid_geometry.py +154 -0
  66. swcgeom/utils/transforms.py +367 -0
  67. swcgeom/utils/volumetric_object.py +483 -0
  68. swcgeom-0.19.4.dist-info/METADATA +86 -0
  69. swcgeom-0.19.4.dist-info/RECORD +72 -0
  70. swcgeom-0.19.4.dist-info/WHEEL +5 -0
  71. swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
  72. 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)