swcgeom 0.18.3__py3-none-any.whl → 0.19.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 (62) hide show
  1. swcgeom/analysis/feature_extractor.py +22 -24
  2. swcgeom/analysis/features.py +18 -40
  3. swcgeom/analysis/lmeasure.py +227 -323
  4. swcgeom/analysis/sholl.py +17 -23
  5. swcgeom/analysis/trunk.py +23 -28
  6. swcgeom/analysis/visualization.py +37 -44
  7. swcgeom/analysis/visualization3d.py +16 -25
  8. swcgeom/analysis/volume.py +33 -47
  9. swcgeom/core/__init__.py +1 -6
  10. swcgeom/core/branch.py +10 -17
  11. swcgeom/core/branch_tree.py +3 -2
  12. swcgeom/core/compartment.py +1 -1
  13. swcgeom/core/node.py +3 -6
  14. swcgeom/core/path.py +11 -16
  15. swcgeom/core/population.py +32 -51
  16. swcgeom/core/swc.py +25 -16
  17. swcgeom/core/swc_utils/__init__.py +4 -6
  18. swcgeom/core/swc_utils/assembler.py +5 -12
  19. swcgeom/core/swc_utils/base.py +40 -31
  20. swcgeom/core/swc_utils/checker.py +3 -8
  21. swcgeom/core/swc_utils/io.py +32 -47
  22. swcgeom/core/swc_utils/normalizer.py +17 -23
  23. swcgeom/core/swc_utils/subtree.py +13 -20
  24. swcgeom/core/tree.py +61 -51
  25. swcgeom/core/tree_utils.py +36 -49
  26. swcgeom/core/tree_utils_impl.py +4 -6
  27. swcgeom/images/augmentation.py +23 -39
  28. swcgeom/images/contrast.py +22 -46
  29. swcgeom/images/folder.py +32 -34
  30. swcgeom/images/io.py +80 -121
  31. swcgeom/transforms/base.py +28 -19
  32. swcgeom/transforms/branch.py +31 -41
  33. swcgeom/transforms/branch_tree.py +3 -1
  34. swcgeom/transforms/geometry.py +13 -4
  35. swcgeom/transforms/image_preprocess.py +2 -0
  36. swcgeom/transforms/image_stack.py +40 -35
  37. swcgeom/transforms/images.py +31 -24
  38. swcgeom/transforms/mst.py +27 -40
  39. swcgeom/transforms/neurolucida_asc.py +13 -13
  40. swcgeom/transforms/path.py +4 -0
  41. swcgeom/transforms/population.py +4 -0
  42. swcgeom/transforms/tree.py +16 -11
  43. swcgeom/transforms/tree_assembler.py +37 -54
  44. swcgeom/utils/download.py +7 -14
  45. swcgeom/utils/dsu.py +12 -0
  46. swcgeom/utils/ellipse.py +26 -14
  47. swcgeom/utils/file.py +8 -13
  48. swcgeom/utils/neuromorpho.py +78 -92
  49. swcgeom/utils/numpy_helper.py +15 -12
  50. swcgeom/utils/plotter_2d.py +10 -16
  51. swcgeom/utils/plotter_3d.py +7 -9
  52. swcgeom/utils/renderer.py +16 -8
  53. swcgeom/utils/sdf.py +12 -23
  54. swcgeom/utils/solid_geometry.py +58 -2
  55. swcgeom/utils/transforms.py +164 -100
  56. swcgeom/utils/volumetric_object.py +29 -53
  57. {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/METADATA +5 -4
  58. swcgeom-0.19.0.dist-info/RECORD +67 -0
  59. {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/WHEEL +1 -1
  60. swcgeom-0.18.3.dist-info/RECORD +0 -67
  61. {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info/licenses}/LICENSE +0 -0
  62. {swcgeom-0.18.3.dist-info → swcgeom-0.19.0.dist-info}/top_level.txt +0 -0
@@ -20,7 +20,7 @@ import warnings
20
20
  from collections.abc import Callable, Iterable, Iterator
21
21
  from concurrent.futures import ProcessPoolExecutor
22
22
  from functools import reduce
23
- from typing import Any, Optional, Protocol, TypeVar, cast, overload
23
+ from typing import Any, Protocol, TypeVar, cast, overload
24
24
 
25
25
  import numpy as np
26
26
  import numpy.typing as npt
@@ -39,10 +39,8 @@ T = TypeVar("T")
39
39
  class Trees(Protocol):
40
40
  """Trees protocol support index and len."""
41
41
 
42
- # fmt: off
43
42
  def __getitem__(self, key: int, /) -> Tree: ...
44
43
  def __len__(self) -> int: ...
45
- # fmt: on
46
44
 
47
45
 
48
46
  class LazyLoadingTrees:
@@ -54,11 +52,9 @@ class LazyLoadingTrees:
54
52
 
55
53
  def __init__(self, swcs: Iterable[str], **kwargs) -> None:
56
54
  """
57
- Paramters
58
- ---------
59
- swcs : List of str
60
- kwargs : dict[str, Any]
61
- Forwarding to `Tree.from_swc`
55
+ Args:
56
+ swcs: List of str
57
+ kwargs: Forwarding to `Tree.from_swc`
62
58
  """
63
59
 
64
60
  super().__init__()
@@ -130,43 +126,40 @@ class Population:
130
126
 
131
127
  trees: Trees
132
128
 
133
- # fmt: off
134
129
  @overload
135
- def __init__(self, swcs: Iterable[str], lazy_loading: bool = ..., root: str = ..., **kwargs) -> None: ...
130
+ def __init__(
131
+ self, swcs: Iterable[str], lazy_loading: bool = ..., root: str = ..., **kwargs
132
+ ) -> None: ...
136
133
  @overload
137
134
  def __init__(self, trees: Trees, /, *, root: str = "") -> None: ...
138
- # fmt: on
139
-
140
135
  def __init__(self, swcs, lazy_loading=True, root="", **kwargs) -> None:
141
136
  super().__init__()
142
137
  if len(swcs) > 0 and isinstance(swcs[0], str):
143
138
  warnings.warn(
144
139
  "`Population(swcs)` has been replaced by "
145
- "`Population(LazyLoadingTrees(swcs))` since v0.8.0 "
146
- "thus we can create a population from a group of trees, "
147
- " and this will be removed in next version",
140
+ "`Population(LazyLoadingTrees(swcs))` since v0.8.0 thus we can create "
141
+ "a population from a group of trees, and this will be removed in next "
142
+ "version",
148
143
  DeprecationWarning,
149
144
  )
150
145
 
151
- trees = LazyLoadingTrees(swcs, **kwargs) # type: ignore
146
+ trees = LazyLoadingTrees(swcs, **kwargs)
152
147
  if not lazy_loading:
153
148
  for i in range(len(swcs)):
154
149
  trees.load(i)
155
150
  else:
156
151
  trees = swcs
157
152
 
158
- self.trees = trees # type: ignore
153
+ self.trees = trees
159
154
  self.root = root
160
155
 
161
156
  if len(swcs) == 0:
162
157
  warnings.warn(f"no trees in population from '{root}'")
163
158
 
164
- # fmt:off
165
159
  @overload
166
160
  def __getitem__(self, key: slice) -> Trees: ...
167
161
  @overload
168
162
  def __getitem__(self, key: int) -> Tree: ...
169
- # fmt:on
170
163
  def __getitem__(self, key: int | slice):
171
164
  if isinstance(key, slice):
172
165
  trees = NestTrees(self.trees, range(*key.indices(len(self))))
@@ -190,15 +183,14 @@ class Population:
190
183
  self,
191
184
  fn: Callable[[Tree], T],
192
185
  *,
193
- max_worker: Optional[int] = None,
186
+ max_worker: int | None = None,
194
187
  verbose: bool = False,
195
188
  ) -> Iterator[T]:
196
189
  """Map a function to all trees in the population.
197
190
 
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.
191
+ This is a straightforward interface for parallelizing computations. The
192
+ parameters are intentionally kept simple and user-friendly. For more advanced
193
+ control, consider using `concurrent.futures` directly.
202
194
  """
203
195
 
204
196
  trees = (t for t in self.trees)
@@ -226,11 +218,11 @@ class Population:
226
218
  cls,
227
219
  root: str,
228
220
  ext: str = ".eswc",
229
- extra_cols: Optional[Iterable[str]] = None,
221
+ extra_cols: Iterable[str] | None = None,
230
222
  **kwargs,
231
223
  ) -> Self:
232
224
  extra_cols = list(extra_cols) if extra_cols is not None else []
233
- extra_cols.extend(k for k, t in eswc_cols)
225
+ extra_cols.extend(k for k, _ in eswc_cols)
234
226
  return cls.from_swc(root, ext, extra_cols=extra_cols, **kwargs)
235
227
 
236
228
  @staticmethod
@@ -253,23 +245,21 @@ class Populations:
253
245
  labels: list[str]
254
246
 
255
247
  def __init__(
256
- self, populations: Iterable[Population], labels: Optional[Iterable[str]] = None
248
+ self, populations: Iterable[Population], labels: Iterable[str] | None = None
257
249
  ) -> None:
258
250
  self.len = min(len(p) for p in populations)
259
251
  self.populations = list(populations)
260
252
 
261
253
  labels = list(labels) if labels is not None else ["" for i in populations]
262
- assert len(labels) == len(
263
- self.populations
264
- ), f"got {len( self.populations)} populations, but has {len(labels)} labels"
254
+ assert len(labels) == len(self.populations), (
255
+ f"got {len(self.populations)} populations, but has {len(labels)} labels"
256
+ )
265
257
  self.labels = labels
266
258
 
267
- # fmt:off
268
259
  @overload
269
260
  def __getitem__(self, key: slice) -> list[list[Tree]]: ...
270
261
  @overload
271
262
  def __getitem__(self, key: int) -> list[Tree]: ...
272
- # fmt:on
273
263
  def __getitem__(self, key):
274
264
  return [p[key] for p in self.populations]
275
265
 
@@ -299,24 +289,18 @@ class Populations:
299
289
  ext: str = ".swc",
300
290
  intersect: bool = True,
301
291
  check_same: bool = False,
302
- labels: Optional[Iterable[str]] = None,
292
+ labels: Iterable[str] | None = None,
303
293
  **kwargs,
304
294
  ) -> Self:
305
295
  """Get population from dirs.
306
296
 
307
- Parameters
308
- ----------
309
- roots : List of str
310
- intersect : bool, default `True`
311
- Take the intersection of these populations.
312
- check_same : bool, default `False`
313
- Check if the directories contains the same swc.
314
- labels : List of str, optional
315
- Label of populations.
316
- **kwargs : Any
317
- Forwarding to `Population`.
297
+ Args:
298
+ roots: List of str
299
+ intersect: Take the intersection of these populations.
300
+ check_same: Check if the directories contains the same swc.
301
+ labels: Label of populations.
302
+ **kwargs: Forwarding to `Population`.
318
303
  """
319
-
320
304
  fs = [Population.find_swcs(d, ext=ext, relpath=True) for d in roots]
321
305
  if intersect:
322
306
  inter = list(reduce(lambda a, b: set(a).intersection(set(b)), fs))
@@ -339,13 +323,13 @@ class Populations:
339
323
  def from_eswc(
340
324
  cls,
341
325
  roots: Iterable[str],
342
- extra_cols: Optional[Iterable[str]] = None,
326
+ extra_cols: Iterable[str] | None = None,
343
327
  *,
344
328
  ext: str = ".eswc",
345
329
  **kwargs,
346
330
  ) -> Self:
347
331
  extra_cols = list(extra_cols) if extra_cols is not None else []
348
- extra_cols.extend(k for k, t in eswc_cols)
332
+ extra_cols.extend(k for k, _ in eswc_cols)
349
333
  return cls.from_swc(roots, extra_cols=extra_cols, ext=ext, **kwargs)
350
334
 
351
335
 
@@ -360,11 +344,8 @@ def _get_idx(key: int, length: int) -> int:
360
344
 
361
345
 
362
346
  # experimental
363
- def filter_population(
364
- pop: Population, predicate: Callable[[Tree], bool]
365
- ) -> "Population":
347
+ def filter_population(pop: Population, predicate: Callable[[Tree], bool]) -> Population:
366
348
  """Filter trees in the population."""
367
-
368
349
  # TODO: how to avoid load trees
369
350
  idx = [i for i, t in enumerate(pop) if predicate(t)]
370
351
  return Population(NestTrees(pop.trees, idx), root=pop.root)
swcgeom/core/swc.py CHANGED
@@ -19,7 +19,7 @@ import warnings
19
19
  from abc import ABC, abstractmethod
20
20
  from collections.abc import Iterable
21
21
  from copy import deepcopy
22
- from typing import Any, Optional, TypeVar, overload
22
+ from typing import Any, TypeVar, overload
23
23
 
24
24
  import numpy as np
25
25
  import numpy.typing as npt
@@ -145,17 +145,28 @@ class SWCLike(ABC):
145
145
  """Get the number of edges."""
146
146
  return self.number_of_nodes() - 1 # for tree structure: n = e + 1
147
147
 
148
- # fmt: off
149
148
  @overload
150
- def to_swc(self, fname: str, *, extra_cols: list[str] | None = ..., source: bool | str = ..., id_offset: int = ...) -> None: ...
149
+ def to_swc(
150
+ self,
151
+ fname: str,
152
+ *,
153
+ extra_cols: list[str] | None = ...,
154
+ source: bool | str = ...,
155
+ id_offset: int = ...,
156
+ ) -> None: ...
151
157
  @overload
152
- def to_swc(self, *, extra_cols: list[str] | None = ..., source: bool | str = ..., id_offset: int = ...) -> str: ...
153
- # fmt: on
154
158
  def to_swc(
155
159
  self,
156
- fname: Optional[str] = None,
157
160
  *,
158
- extra_cols: Optional[list[str]] = None,
161
+ extra_cols: list[str] | None = ...,
162
+ source: bool | str = ...,
163
+ id_offset: int = ...,
164
+ ) -> str: ...
165
+ def to_swc(
166
+ self,
167
+ fname: str | None = None,
168
+ *,
169
+ extra_cols: list[str] | None = None,
159
170
  source: bool | str = True,
160
171
  comments: bool = True,
161
172
  id_offset: int = 1,
@@ -183,17 +194,15 @@ class SWCLike(ABC):
183
194
 
184
195
  return None
185
196
 
186
- # fmt: off
187
197
  @overload
188
198
  def to_eswc(self, fname: str, **kwargs) -> None: ...
189
199
  @overload
190
- def to_eswc(self, **kwargs) -> str: ...
191
- # fmt: on
200
+ def to_eswc(self, fname: None = ..., **kwargs) -> str: ...
192
201
  def to_eswc(
193
202
  self,
194
- fname: Optional[str] = None,
195
- swc_path: Optional[str] = None,
196
- extra_cols: Optional[list[str]] = None,
203
+ fname: str | None = None,
204
+ swc_path: str | None = None,
205
+ extra_cols: list[str] | None = None,
197
206
  **kwargs,
198
207
  ) -> str | None:
199
208
  if swc_path is None:
@@ -205,7 +214,7 @@ class SWCLike(ABC):
205
214
  fname = swc_path
206
215
 
207
216
  extra_cols = extra_cols or []
208
- extra_cols.extend(k for k, t in eswc_cols)
217
+ extra_cols.extend(k for k, _ in eswc_cols)
209
218
  return self.to_swc(fname, extra_cols=extra_cols, **kwargs) # type: ignore
210
219
 
211
220
 
@@ -221,8 +230,8 @@ class DictSWC(SWCLike):
221
230
  self,
222
231
  *,
223
232
  source: str = "",
224
- comments: Optional[Iterable[str]] = None,
225
- names: Optional[SWCNames] = None,
233
+ comments: Iterable[str] | None = None,
234
+ names: SWCNames | None = None,
226
235
  **kwargs: npt.NDArray,
227
236
  ):
228
237
  super().__init__()
@@ -15,12 +15,10 @@
15
15
 
16
16
  """SWC format utils.
17
17
 
18
- Notes
19
- -----
20
- This module provides a bunch of methods to manipulating swc files, they
21
- are always trivial and unstabled, so we are NOT export it by default.
22
- If you use the method here, please review the code more frequently, we
23
- will try to flag all breaking changes but NO promises.
18
+ NOTE: This module provides a bunch of methods to manipulating swc files, they are
19
+ always trivial and unstabled, so we are NOT export it by default. If you use the method
20
+ here, please review the code more frequently, we will try to flag all breaking changes
21
+ but NO promises.
24
22
  """
25
23
 
26
24
  from swcgeom.core.swc_utils.assembler import * # noqa: F403
@@ -15,10 +15,7 @@
15
15
 
16
16
  """Assemble lines to swc.
17
17
 
18
- Notes
19
- -----
20
- This module is deprecated, please use `~.transforms.LinesToTree`
21
- instead.
18
+ NOTE: This module is deprecated, please use `~.transforms.LinesToTree` instead.
22
19
  """
23
20
 
24
21
  __all__ = ["assemble_lines", "try_assemble_lines"]
@@ -30,11 +27,9 @@ def assemble_lines(*args, **kwargs):
30
27
  .. deprecated:: 0.15.0
31
28
  Use :meth:`~.transforms.LinesToTree` instead.
32
29
  """
33
-
34
30
  raise DeprecationWarning(
35
- "`assemble_lines` has been replaced by "
36
- "`~.transforms.LinesToTree` because it can be easy assemble "
37
- "with other tansformations.",
31
+ "`assemble_lines` has been replaced by `~.transforms.LinesToTree` because it "
32
+ "can be easy assemble with other tansformations.",
38
33
  )
39
34
 
40
35
 
@@ -44,9 +39,7 @@ def try_assemble_lines(*args, **kwargs):
44
39
  .. deprecated:: 0.15.0
45
40
  Use :meth:`~.transforms.LinesToTree` instead.
46
41
  """
47
-
48
42
  raise DeprecationWarning(
49
- "`try_assemble_lines` has been replaced by "
50
- "`~.transforms.LinesToTree` because it can be easy assemble "
51
- "with other tansformations.",
43
+ "`try_assemble_lines` has been replaced by `~.transforms.LinesToTree` because "
44
+ "it can be easy assemble with other tansformations.",
52
45
  )
@@ -16,7 +16,7 @@
16
16
  """Base SWC format utils."""
17
17
 
18
18
  from collections.abc import Callable
19
- from typing import Literal, NamedTuple, Optional, TypeVar, overload
19
+ from typing import Literal, NamedTuple, TypeVar, overload
20
20
 
21
21
  import numpy as np
22
22
  import numpy.typing as npt
@@ -34,7 +34,8 @@ __all__ = [
34
34
  "traverse",
35
35
  ]
36
36
 
37
- T, K = TypeVar("T"), TypeVar("K")
37
+ T = TypeVar("T")
38
+ K = TypeVar("K")
38
39
  Topology = tuple[npt.NDArray[np.int32], npt.NDArray[np.int32]] # (id, pid)
39
40
 
40
41
 
@@ -56,17 +57,15 @@ class SWCNames(NamedTuple):
56
57
  swc_names = SWCNames()
57
58
 
58
59
 
59
- def get_names(names: Optional[SWCNames] = None) -> SWCNames:
60
+ def get_names(names: SWCNames | None = None) -> SWCNames:
60
61
  return names or swc_names
61
62
 
62
63
 
63
64
  class SWCTypes(NamedTuple):
64
65
  """SWC format types.
65
66
 
66
- See Also
67
- ---------
68
- NeuroMoprho.org - What is SWC format?
69
- https://neuromorpho.org/myfaq.jsp
67
+ See Also:
68
+ NeuroMoprho.org - What is SWC format?: https://neuromorpho.org/myfaq.jsp
70
69
  """
71
70
 
72
71
  undefined: int = 0
@@ -82,17 +81,17 @@ class SWCTypes(NamedTuple):
82
81
  swc_types = SWCTypes()
83
82
 
84
83
 
85
- def get_types(types: Optional[SWCTypes] = None) -> SWCTypes:
84
+ def get_types(types: SWCTypes | None = None) -> SWCTypes:
86
85
  return types or swc_types
87
86
 
88
87
 
89
- def get_topology(df: pd.DataFrame, *, names: Optional[SWCNames] = None) -> Topology:
88
+ def get_topology(df: pd.DataFrame, *, names: SWCNames | None = None) -> Topology:
90
89
  names = get_names(names)
91
90
  return (df[names.id].to_numpy(), df[names.pid].to_numpy())
92
91
 
93
92
 
94
93
  def get_dsu(
95
- df: pd.DataFrame, *, names: Optional[SWCNames] = None
94
+ df: pd.DataFrame, *, names: SWCNames | None = None
96
95
  ) -> npt.NDArray[np.int32]:
97
96
  """Get disjoint set union."""
98
97
  names = get_names(names)
@@ -116,36 +115,46 @@ def get_dsu(
116
115
  return dsu
117
116
 
118
117
 
119
- # fmt: off
120
118
  @overload
121
- def traverse(topology: Topology, *, enter: Callable[[int, T | None], T], root: int | np.integer = ..., mode: Literal["dfs"] = ...) -> None: ...
119
+ def traverse(
120
+ topology: Topology,
121
+ *,
122
+ enter: Callable[[int, T | None], T],
123
+ root: int | np.integer = ...,
124
+ mode: Literal["dfs"] = ...,
125
+ ) -> None: ...
122
126
  @overload
123
- def traverse(topology: Topology, *, leave: Callable[[int, list[K]], K], root: int | np.integer = ..., mode: Literal["dfs"] = ...) -> K: ...
127
+ def traverse(
128
+ topology: Topology,
129
+ *,
130
+ leave: Callable[[int, list[K]], K],
131
+ root: int | np.integer = ...,
132
+ mode: Literal["dfs"] = ...,
133
+ ) -> K: ...
124
134
  @overload
125
135
  def traverse(
126
- topology: Topology, *, enter: Callable[[int, T | None], T], leave: Callable[[int, list[K]], K],
127
- root: int | np.integer = ..., mode: Literal["dfs"] = ...,
136
+ topology: Topology,
137
+ *,
138
+ enter: Callable[[int, T | None], T],
139
+ leave: Callable[[int, list[K]], K],
140
+ root: int | np.integer = ...,
141
+ mode: Literal["dfs"] = ...,
128
142
  ) -> K: ...
129
- # fmt: on
130
143
  def traverse(topology: Topology, *, mode="dfs", **kwargs):
131
144
  """Traverse nodes.
132
145
 
133
- Parameters
134
- ----------
135
- enter : (id: int, parent: T | None) => T, optional
136
- The callback when entering node, which accepts two parameters,
137
- the current node id and the return value of it parent node. In
138
- particular, the root node receives an `None`.
139
- leave : (id: int, children: list[T]) => T, optional
140
- The callback when leaving node. When leaving a node, subtree
141
- has already been traversed. Callback accepts two parameters,
142
- the current node id and list of the return value of children,
143
- In particular, the leaf node receives an empty list.
144
- root : int, default to `0`
145
- Start from the root node of the subtree
146
- mode : `dfs`, default to `dfs`
146
+ Args:
147
+ enter: (id: int, parent: T | None) => T, optional
148
+ The callback when entering node, which accepts two parameters, the current node
149
+ id and the return value of it parent node. In particular, the root node
150
+ receives an `None`.
151
+ leave: (id: int, children: list[T]) => T, optional
152
+ The callback when leaving node. When leaving a node, subtree has already been
153
+ traversed. Callback accepts two parameters, the current node id and list of the
154
+ return value of children, In particular, the leaf node receives an empty list.
155
+ root: Start from the root node of the subtree
156
+ mode: The traverse mode, only support "dfs" now.
147
157
  """
148
-
149
158
  match mode:
150
159
  case "dfs":
151
160
  return _traverse_dfs(topology, **kwargs)
@@ -16,7 +16,6 @@
16
16
  """Check common"""
17
17
 
18
18
  from collections import defaultdict
19
- from typing import Optional
20
19
 
21
20
  import numpy as np
22
21
  import pandas as pd
@@ -36,14 +35,13 @@ __all__ = [
36
35
  ]
37
36
 
38
37
 
39
- def is_single_root(df: pd.DataFrame, *, names: Optional[SWCNames] = None) -> bool:
38
+ def is_single_root(df: pd.DataFrame, *, names: SWCNames | None = None) -> bool:
40
39
  """Check is it only one root."""
41
40
  return len(np.unique(get_dsu(df, names=names))) == 1
42
41
 
43
42
 
44
43
  def is_bifurcate(topology: Topology, *, exclude_root: bool = True) -> bool:
45
44
  """Check is it a bifurcate topology."""
46
-
47
45
  children = defaultdict(list)
48
46
  for idx, pid in zip(*topology):
49
47
  children[pid].append(idx)
@@ -59,8 +57,7 @@ def is_bifurcate(topology: Topology, *, exclude_root: bool = True) -> bool:
59
57
  def is_sorted(topology: Topology) -> bool:
60
58
  """Check is it sorted.
61
59
 
62
- In a sorted topology, parent samples should appear before any child
63
- samples.
60
+ In a sorted topology, parent samples should appear before any child samples.
64
61
  """
65
62
  flag = True
66
63
 
@@ -103,20 +100,18 @@ def check_single_root(*args, **kwargs) -> bool:
103
100
  .. deprecated:: 0.5.0
104
101
  Use :meth:`is_single_root` instead.
105
102
  """
106
-
107
103
  return is_single_root(*args, **kwargs)
108
104
 
109
105
 
110
106
  @deprecated("Use `is_bifurcate` instead")
111
107
  def is_binary_tree(
112
- df: pd.DataFrame, exclude_root: bool = True, *, names: Optional[SWCNames] = None
108
+ df: pd.DataFrame, exclude_root: bool = True, *, names: SWCNames | None = None
113
109
  ) -> bool:
114
110
  """Check is it a binary tree.
115
111
 
116
112
  .. deprecated:: 0.8.0
117
113
  Use :meth:`is_bifurcate` instead.
118
114
  """
119
-
120
115
  names = get_names(names)
121
116
  topo = (df[names.id].to_numpy(), df[names.pid].to_numpy())
122
117
  return is_bifurcate(topo, exclude_root=exclude_root)
@@ -18,7 +18,7 @@
18
18
  import re
19
19
  import warnings
20
20
  from collections.abc import Callable, Iterable
21
- from typing import Literal, Optional
21
+ from typing import Literal
22
22
 
23
23
  import numpy as np
24
24
  import numpy.typing as npt
@@ -39,41 +39,33 @@ __all__ = ["read_swc", "to_swc"]
39
39
 
40
40
  def read_swc(
41
41
  swc_file: PathOrIO,
42
- extra_cols: Optional[Iterable[str]] = None,
42
+ extra_cols: Iterable[str] | None = None,
43
43
  fix_roots: Literal["somas", "nearest", False] = False,
44
44
  sort_nodes: bool = False,
45
45
  reset_index: bool = True,
46
46
  *,
47
47
  encoding: Literal["detect"] | str = "utf-8",
48
- names: Optional[SWCNames] = None,
48
+ names: SWCNames | None = None,
49
49
  ) -> tuple[pd.DataFrame, list[str]]:
50
50
  """Read swc file.
51
51
 
52
- Parameters
53
- ----------
54
- swc_file : PathOrIO
55
- Path of swc file, the id should be consecutively incremented.
56
- extra_cols : Iterable[str], optional
57
- Read more cols in swc file.
58
- fix_roots : `somas`|`nearest`|False, default `False`
59
- Fix multiple roots.
60
- sort_nodes : bool, default `False`
61
- Sort the indices of neuron tree, the index for parent are
62
- always less than children.
63
- reset_index : bool, default `True`
64
- Reset node index to start with zero, DO NOT set to false if
65
- you are not sure what will happend.
66
- encoding : str | 'detect', default `utf-8`
67
- The name of the encoding used to decode the file. If is
68
- `detect`, we will try to detect the character encoding.
69
- names : SWCNames, optional
70
-
71
- Returns
72
- -------
73
- df : ~pandas.DataFrame
74
- comments : List of string
52
+ NOTE: the id should be consecutively incremented.
53
+
54
+ Args:
55
+ extra_cols: Read more cols in swc file.
56
+ fix_roots: Fix multiple roots.
57
+ sort_nodes: Sort the indices of neuron tree.
58
+ After sorting the nodes, the index for each parent are always less than
59
+ that of its children.
60
+ reset_index: Reset node index to start with zero.
61
+ DO NOT set to false if you are not sure what will happened.
62
+ encoding: The name of the encoding used to decode the file.
63
+ If is `detect`, we will try to detect the character encoding.
64
+
65
+ Returns:
66
+ df: ~pandas.DataFrame
67
+ comments: List of string
75
68
  """
76
-
77
69
  names = get_names(names)
78
70
  df, comments = parse_swc(
79
71
  swc_file, names=names, extra_cols=extra_cols, encoding=encoding
@@ -110,10 +102,10 @@ def read_swc(
110
102
  def to_swc(
111
103
  get_ndata: Callable[[str], npt.NDArray],
112
104
  *,
113
- extra_cols: Optional[Iterable[str]] = None,
105
+ extra_cols: Iterable[str] | None = None,
114
106
  id_offset: int = 1,
115
- comments: Optional[Iterable[str]] = None,
116
- names: Optional[SWCNames] = None,
107
+ comments: Iterable[str] | None = None,
108
+ names: SWCNames | None = None,
117
109
  ) -> Iterable[str]:
118
110
  """Convert to swc format."""
119
111
 
@@ -156,21 +148,14 @@ def parse_swc(
156
148
  ) -> tuple[pd.DataFrame, list[str]]:
157
149
  """Parse swc file.
158
150
 
159
- Parameters
160
- ----------
161
- fname : PathOrIO
162
- names : SWCNames
163
- extra_cols : List of str, optional
164
- encoding : str | 'detect', default `utf-8`
165
- The name of the encoding used to decode the file. If is
166
- `detect`, we will try to detect the character encoding.
167
-
168
- Returns
169
- -------
170
- df : ~pandas.DataFrame
171
- comments : List of string
172
- """
151
+ Args:
152
+ encoding: The name of the encoding used to decode the file.
153
+ If is `detect`, we will try to detect the character encoding.
173
154
 
155
+ Returns:
156
+ df: ~pandas.DataFrame
157
+ comments: List of string
158
+ """
174
159
  # pylint: disable=too-many-locals
175
160
  extras = list(extra_cols) if extra_cols else []
176
161
 
@@ -208,7 +193,7 @@ def parse_swc(
208
193
  if (match := re_swc.search(line)) is not None:
209
194
  if flag and match.group(last_group):
210
195
  warnings.warn(
211
- f"some fields are ignored in row {i+1} of `{fname}`"
196
+ f"some fields are ignored in row {i + 1} of `{fname}`"
212
197
  )
213
198
  flag = False
214
199
 
@@ -219,10 +204,10 @@ def parse_swc(
219
204
  if not comment.startswith(ignored_comment):
220
205
  comments.append(comment)
221
206
  elif not line.isspace():
222
- raise ValueError(f"invalid row {i+1} in `{fname}`")
207
+ raise ValueError(f"invalid row {i + 1} in `{fname}`")
223
208
  except UnicodeDecodeError as e:
224
209
  raise ValueError(
225
- f"decode failed, try to enable auto detect `encoding='detect'`"
210
+ "decode failed, try to enable auto detect `encoding='detect'`"
226
211
  ) from e
227
212
 
228
213
  df = pd.DataFrame.from_dict(dict(zip(keys, vals)))