swcgeom 0.17.0__py3-none-any.whl → 0.17.1__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 (47) hide show
  1. swcgeom/_version.py +2 -2
  2. swcgeom/analysis/feature_extractor.py +13 -12
  3. swcgeom/analysis/features.py +4 -4
  4. swcgeom/analysis/lmeasure.py +5 -5
  5. swcgeom/analysis/sholl.py +4 -4
  6. swcgeom/analysis/trunk.py +12 -11
  7. swcgeom/analysis/visualization.py +9 -9
  8. swcgeom/analysis/visualization3d.py +85 -0
  9. swcgeom/analysis/volume.py +4 -4
  10. swcgeom/core/branch.py +4 -3
  11. swcgeom/core/branch_tree.py +3 -4
  12. swcgeom/core/compartment.py +3 -2
  13. swcgeom/core/node.py +2 -2
  14. swcgeom/core/path.py +3 -2
  15. swcgeom/core/population.py +16 -27
  16. swcgeom/core/swc.py +11 -10
  17. swcgeom/core/swc_utils/base.py +8 -17
  18. swcgeom/core/swc_utils/io.py +7 -6
  19. swcgeom/core/swc_utils/normalizer.py +4 -3
  20. swcgeom/core/swc_utils/subtree.py +2 -2
  21. swcgeom/core/tree.py +22 -34
  22. swcgeom/core/tree_utils.py +11 -10
  23. swcgeom/core/tree_utils_impl.py +3 -3
  24. swcgeom/images/augmentation.py +3 -3
  25. swcgeom/images/folder.py +10 -16
  26. swcgeom/images/io.py +19 -30
  27. swcgeom/transforms/image_stack.py +6 -5
  28. swcgeom/transforms/images.py +2 -3
  29. swcgeom/transforms/neurolucida_asc.py +4 -6
  30. swcgeom/transforms/population.py +1 -3
  31. swcgeom/transforms/tree.py +8 -7
  32. swcgeom/transforms/tree_assembler.py +4 -3
  33. swcgeom/utils/ellipse.py +3 -4
  34. swcgeom/utils/neuromorpho.py +17 -16
  35. swcgeom/utils/plotter_2d.py +12 -6
  36. swcgeom/utils/plotter_3d.py +31 -0
  37. swcgeom/utils/renderer.py +6 -6
  38. swcgeom/utils/sdf.py +2 -2
  39. swcgeom/utils/solid_geometry.py +1 -3
  40. swcgeom/utils/transforms.py +1 -3
  41. swcgeom/utils/volumetric_object.py +8 -10
  42. {swcgeom-0.17.0.dist-info → swcgeom-0.17.1.dist-info}/METADATA +1 -1
  43. swcgeom-0.17.1.dist-info/RECORD +67 -0
  44. swcgeom-0.17.0.dist-info/RECORD +0 -65
  45. {swcgeom-0.17.0.dist-info → swcgeom-0.17.1.dist-info}/LICENSE +0 -0
  46. {swcgeom-0.17.0.dist-info → swcgeom-0.17.1.dist-info}/WHEEL +0 -0
  47. {swcgeom-0.17.0.dist-info → swcgeom-0.17.1.dist-info}/top_level.txt +0 -0
swcgeom/images/folder.py CHANGED
@@ -4,18 +4,9 @@ import math
4
4
  import os
5
5
  import re
6
6
  import warnings
7
+ from collections.abc import Callable, Iterable
7
8
  from dataclasses import dataclass
8
- from typing import (
9
- Callable,
10
- Generic,
11
- Iterable,
12
- List,
13
- Literal,
14
- Optional,
15
- Tuple,
16
- TypeVar,
17
- overload,
18
- )
9
+ from typing import Generic, Literal, Optional, TypeVar, overload
19
10
 
20
11
  import numpy as np
21
12
  import numpy.typing as npt
@@ -33,7 +24,7 @@ T = TypeVar("T")
33
24
  class ImageStackFolderBase(Generic[ScalarType, T]):
34
25
  """Image stack folder base."""
35
26
 
36
- files: List[str]
27
+ files: list[str]
37
28
  transform: Transform[npt.NDArray[ScalarType], T]
38
29
 
39
30
  # fmt: off
@@ -61,7 +52,10 @@ class ImageStackFolderBase(Generic[ScalarType, T]):
61
52
  return read_imgs(fname, dtype=self.dtype).get_full() # type: ignore
62
53
 
63
54
  @staticmethod
64
- def scan(root: str, *, pattern: Optional[str] = None) -> List[str]:
55
+ def scan(root: str, *, pattern: Optional[str] = None) -> list[str]:
56
+ if not os.path.isdir(root):
57
+ raise NotADirectoryError(f"not a directory: {root}")
58
+
65
59
  is_valid = re.compile(pattern).match if pattern is not None else truthly
66
60
 
67
61
  fs = []
@@ -168,13 +162,13 @@ class ImageStackFolder(ImageStackFolderBase[ScalarType, T]):
168
162
  class LabeledImageStackFolder(ImageStackFolderBase[ScalarType, T]):
169
163
  """Image stack folder with label."""
170
164
 
171
- labels: List[int]
165
+ labels: list[int]
172
166
 
173
167
  def __init__(self, files: Iterable[str], labels: Iterable[int], **kwargs):
174
168
  super().__init__(files, **kwargs)
175
169
  self.labels = list(labels)
176
170
 
177
- def __getitem__(self, idx: int) -> Tuple[T, int]:
171
+ def __getitem__(self, idx: int) -> tuple[T, int]:
178
172
  return self._get(self.files[idx]), self.labels[idx]
179
173
 
180
174
  @classmethod
@@ -205,7 +199,7 @@ class PathImageStackFolder(ImageStackFolderBase[ScalarType, T]):
205
199
  super().__init__(files, **kwargs)
206
200
  self.root = root
207
201
 
208
- def __getitem__(self, idx: int) -> Tuple[T, str]:
202
+ def __getitem__(self, idx: int) -> tuple[T, str]:
209
203
  relpath = os.path.relpath(self.files[idx], self.root)
210
204
  return self._get(self.files[idx]), relpath
211
205
 
swcgeom/images/io.py CHANGED
@@ -4,20 +4,9 @@ import os
4
4
  import re
5
5
  import warnings
6
6
  from abc import ABC, abstractmethod
7
+ from collections.abc import Callable, Iterable
7
8
  from functools import cache, lru_cache
8
- from typing import (
9
- Any,
10
- Callable,
11
- Generic,
12
- Iterable,
13
- List,
14
- Literal,
15
- Optional,
16
- Tuple,
17
- TypeVar,
18
- cast,
19
- overload,
20
- )
9
+ from typing import Any, Generic, Literal, Optional, TypeVar, cast, overload
21
10
 
22
11
  import nrrd
23
12
  import numpy as np
@@ -27,7 +16,7 @@ from v3dpy.loaders import PBD, Raw
27
16
 
28
17
  __all__ = ["read_imgs", "save_tiff", "read_images"]
29
18
 
30
- Vec3i = Tuple[int, int, int]
19
+ Vec3i = tuple[int, int, int]
31
20
  ScalarType = TypeVar("ScalarType", bound=np.generic, covariant=True)
32
21
 
33
22
  RE_TERAFLY_ROOT = re.compile(r"^RES\((\d+)x(\d+)x(\d+)\)$")
@@ -58,17 +47,17 @@ class ImageStack(ABC, Generic[ScalarType]):
58
47
  def __getitem__(self, key: int) -> npt.NDArray[ScalarType]: ... # array of shape (Y, Z, C)
59
48
  @overload
60
49
  @abstractmethod
61
- def __getitem__(self, key: Tuple[int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (Z, C)
50
+ def __getitem__(self, key: tuple[int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (Z, C)
62
51
  @overload
63
52
  @abstractmethod
64
- def __getitem__(self, key: Tuple[int, int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (C,)
53
+ def __getitem__(self, key: tuple[int, int, int]) -> npt.NDArray[ScalarType]: ... # array of shape (C,)
65
54
  @overload
66
55
  @abstractmethod
67
- def __getitem__(self, key: Tuple[int, int, int, int]) -> ScalarType: ... # value
56
+ def __getitem__(self, key: tuple[int, int, int, int]) -> ScalarType: ... # value
68
57
  @overload
69
58
  @abstractmethod
70
59
  def __getitem__(
71
- self, key: slice | Tuple[slice, slice] | Tuple[slice, slice, slice] | Tuple[slice, slice, slice, slice],
60
+ self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice] | tuple[slice, slice, slice, slice],
72
61
  ) -> npt.NDArray[ScalarType]: ... # array of shape (X, Y, Z, C)
73
62
  @overload
74
63
  @abstractmethod
@@ -95,7 +84,7 @@ class ImageStack(ABC, Generic[ScalarType]):
95
84
  return self[:, :, :, :]
96
85
 
97
86
  @property
98
- def shape(self) -> Tuple[int, int, int, int]:
87
+ def shape(self) -> tuple[int, int, int, int]:
99
88
  raise NotImplementedError()
100
89
 
101
90
 
@@ -118,7 +107,7 @@ def read_imgs(fname: str, **kwargs): # type: ignore
118
107
  Casting data to specified dtype. If integer and float
119
108
  conversions occur, they will be scaled (assuming floats are
120
109
  between 0 and 1).
121
- **kwargs : Dict[str, Any]
110
+ **kwargs : dict[str, Any]
122
111
  Forwarding to the corresponding reader.
123
112
  """
124
113
 
@@ -169,7 +158,7 @@ def save_tiff(
169
158
  Compression algorithm, forwarding to `tifffile.imwrite`. If no
170
159
  algorithnm is specify specified, we will use the zlib algorithm
171
160
  with compression level 6 by default.
172
- **kwargs : Dict[str, Any]
161
+ **kwargs : dict[str, Any]
173
162
  Forwarding to `tifffile.imwrite`
174
163
  """
175
164
  if isinstance(data, ImageStack):
@@ -245,8 +234,8 @@ class NDArrayImageStack(ImageStack[ScalarType]):
245
234
  return self.imgs
246
235
 
247
236
  @property
248
- def shape(self) -> Tuple[int, int, int, int]:
249
- return cast(Tuple[int, int, int, int], self.imgs.shape)
237
+ def shape(self) -> tuple[int, int, int, int]:
238
+ return cast(tuple[int, int, int, int], self.imgs.shape)
250
239
 
251
240
 
252
241
  class TiffImageStack(NDArrayImageStack[ScalarType]):
@@ -324,7 +313,7 @@ class TeraflyImageStack(ImageStack[ScalarType]):
324
313
  use its coordinate system, remember to FLIP Y-AXIS BACK.
325
314
  """
326
315
 
327
- _listdir: Callable[[str], List[str]]
316
+ _listdir: Callable[[str], list[str]]
328
317
  _read_patch: Callable[[str], npt.NDArray]
329
318
 
330
319
  def __init__(
@@ -350,7 +339,7 @@ class TeraflyImageStack(ImageStack[ScalarType]):
350
339
  self.res, self.res_dirs, self.res_patch_sizes = self.get_resolutions(root)
351
340
 
352
341
  @cache
353
- def listdir(path: str) -> List[str]:
342
+ def listdir(path: str) -> list[str]:
354
343
  return os.listdir(path)
355
344
 
356
345
  @lru_cache(maxsize=lru_maxsize)
@@ -429,19 +418,19 @@ class TeraflyImageStack(ImageStack[ScalarType]):
429
418
  raise NotImplementedError() # TODO
430
419
 
431
420
  @property
432
- def shape(self) -> Tuple[int, int, int, int]:
421
+ def shape(self) -> tuple[int, int, int, int]:
433
422
  res_max = self.res[-1]
434
423
  return res_max[0], res_max[1], res_max[2], 1
435
424
 
436
425
  @classmethod
437
- def get_resolutions(cls, root: str) -> Tuple[List[Vec3i], List[str], List[Vec3i]]:
426
+ def get_resolutions(cls, root: str) -> tuple[list[Vec3i], list[str], list[Vec3i]]:
438
427
  """Get all resolutions.
439
428
 
440
429
  Returns
441
430
  -------
442
431
  resolutions : List of (int, int, int)
443
432
  Sequence of sorted resolutions (from small to large).
444
- roots : List[str]
433
+ roots : list[str]
445
434
  Sequence of root of resolutions respectively.
446
435
  patch_sizes : List of (int, int, int)
447
436
  Sequence of patch size of resolutions respectively.
@@ -581,7 +570,7 @@ class GrayImageStack:
581
570
  @overload
582
571
  def __getitem__(self, key: npt.NDArray[np.integer[Any]]) -> np.float32: ...
583
572
  @overload
584
- def __getitem__(self, key: slice | Tuple[slice, slice] | Tuple[slice, slice, slice]) -> npt.NDArray[np.float32]: ...
573
+ def __getitem__(self, key: slice | tuple[slice, slice] | tuple[slice, slice, slice]) -> npt.NDArray[np.float32]: ...
585
574
  # fmt: on
586
575
  def __getitem__(self, key):
587
576
  """Get pixel/patch of image stack."""
@@ -608,7 +597,7 @@ class GrayImageStack:
608
597
  return self.imgs.get_full()[:, :, :, 0]
609
598
 
610
599
  @property
611
- def shape(self) -> Tuple[int, int, int]:
600
+ def shape(self) -> tuple[int, int, int]:
612
601
  return self.imgs.shape[:-1]
613
602
 
614
603
 
@@ -12,7 +12,8 @@ pip install swcgeom[all]
12
12
  import os
13
13
  import re
14
14
  import time
15
- from typing import Iterable, List, Optional, Tuple
15
+ from collections.abc import Iterable
16
+ from typing import Optional
16
17
 
17
18
  import numpy as np
18
19
  import numpy.typing as npt
@@ -69,7 +70,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
69
70
  x: Tree,
70
71
  verbose: bool = True,
71
72
  *,
72
- ranges: Optional[Tuple[npt.ArrayLike, npt.ArrayLike]] = None,
73
+ ranges: Optional[tuple[npt.ArrayLike, npt.ArrayLike]] = None,
73
74
  ) -> Iterable[npt.NDArray[np.uint8]]:
74
75
  if verbose:
75
76
  print("To image stack: " + x.source)
@@ -133,7 +134,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
133
134
  scene = ObjectsScene()
134
135
  scene.set_background((0, 0, 0))
135
136
 
136
- def leave(n: Tree.Node, children: List[Tree.Node]) -> Tree.Node:
137
+ def leave(n: Tree.Node, children: list[Tree.Node]) -> Tree.Node:
137
138
  for c in children:
138
139
  sdf = RoundCone(_tp3f(n.xyz()), _tp3f(c.xyz()), n.r, c.r).into()
139
140
  scene.add_object(SDFObject(sdf, material).into())
@@ -175,7 +176,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
175
176
  def save_tif(
176
177
  fname: str,
177
178
  frames: Iterable[npt.NDArray[np.uint8]],
178
- resolution: Tuple[float, float] = (1, 1),
179
+ resolution: tuple[float, float] = (1, 1),
179
180
  ) -> None:
180
181
  with tifffile.TiffWriter(fname) as tif:
181
182
  for frame in frames:
@@ -191,7 +192,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
191
192
  )
192
193
 
193
194
 
194
- def _tp3f(x: npt.NDArray) -> Tuple[float, float, float]:
195
+ def _tp3f(x: npt.NDArray) -> tuple[float, float, float]:
195
196
  """Convert to tuple of 3 floats."""
196
197
  assert len(x) == 3
197
198
  return (float(x[0]), float(x[1]), float(x[2]))
@@ -1,7 +1,6 @@
1
1
  """Image stack related transform."""
2
2
 
3
3
  import warnings
4
- from typing import Tuple
5
4
 
6
5
  import numpy as np
7
6
  import numpy.typing as npt
@@ -28,7 +27,7 @@ NDArrayf32 = npt.NDArray[np.float32]
28
27
  class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
29
28
  """Get image stack center."""
30
29
 
31
- def __init__(self, shape_out: int | Tuple[int, int, int]):
30
+ def __init__(self, shape_out: int | tuple[int, int, int]):
32
31
  super().__init__()
33
32
  self.shape_out = (
34
33
  shape_out
@@ -53,7 +52,7 @@ class Center(ImagesCenterCrop):
53
52
  Use :class:`ImagesCenterCrop` instead.
54
53
  """
55
54
 
56
- def __init__(self, shape_out: int | Tuple[int, int, int]):
55
+ def __init__(self, shape_out: int | tuple[int, int, int]):
57
56
  warnings.warn(
58
57
  "`Center` is deprecated, use `ImagesCenterCrop` instead",
59
58
  DeprecationWarning,
@@ -4,9 +4,7 @@ import os
4
4
  import re
5
5
  from enum import Enum, auto
6
6
  from io import TextIOBase
7
- from typing import Any, List, NamedTuple, Optional, cast
8
-
9
- import numpy as np
7
+ from typing import Any, NamedTuple, Optional, cast
10
8
 
11
9
  from swcgeom.core import Tree
12
10
  from swcgeom.core.swc_utils import SWCNames, SWCTypes, get_names, get_types
@@ -116,8 +114,8 @@ class ASTNode:
116
114
  self,
117
115
  type: ASTType,
118
116
  value: Any = None,
119
- tokens: Optional[List["Token"]] = None,
120
- children: Optional[List["ASTNode"]] = None,
117
+ tokens: Optional[list["Token"]] = None,
118
+ children: Optional[list["ASTNode"]] = None,
121
119
  ):
122
120
  self.type = type
123
121
  self.value = value
@@ -149,7 +147,7 @@ class ASTNode:
149
147
 
150
148
 
151
149
  class AST(ASTNode):
152
- def __init__(self, children: Optional[List[ASTNode]] = None, source: str = ""):
150
+ def __init__(self, children: Optional[list[ASTNode]] = None, source: str = ""):
153
151
  super().__init__(ASTType.ROOT, children=children)
154
152
  self.source = source
155
153
 
@@ -1,7 +1,5 @@
1
1
  """Transformation in population."""
2
2
 
3
- from typing import List
4
-
5
3
  from swcgeom.core import Population, Tree
6
4
  from swcgeom.transforms.base import Transform
7
5
 
@@ -16,7 +14,7 @@ class PopulationTransform(Transform[Population, Population]):
16
14
  self.transform = transform
17
15
 
18
16
  def __call__(self, population: Population) -> Population:
19
- trees: List[Tree] = []
17
+ trees: list[Tree] = []
20
18
  for t in population:
21
19
  new_t = self.transform(t)
22
20
  if new_t.source == "":
@@ -1,7 +1,8 @@
1
1
  """Transformation in tree."""
2
2
 
3
3
  import warnings
4
- from typing import Callable, List, Optional, Tuple
4
+ from collections.abc import Callable
5
+ from typing import Optional
5
6
 
6
7
  import numpy as np
7
8
 
@@ -102,7 +103,7 @@ class CutByType(Transform[Tree, Tree]):
102
103
  def __call__(self, x: Tree) -> Tree:
103
104
  removals = set(x.id()[x.type() != self.type])
104
105
 
105
- def leave(n: Tree.Node, keep_children: List[bool]) -> bool:
106
+ def leave(n: Tree.Node, keep_children: list[bool]) -> bool:
106
107
  if n.id in removals and any(keep_children):
107
108
  removals.remove(n.id)
108
109
  return n.id not in removals
@@ -145,7 +146,7 @@ class CutByBifurcationOrder(Transform[Tree, Tree]):
145
146
  def __repr__(self) -> str:
146
147
  return f"CutByBifurcationOrder-{self.max_bifurcation_order}"
147
148
 
148
- def _enter(self, n: Tree.Node, parent_level: int | None) -> Tuple[int, bool]:
149
+ def _enter(self, n: Tree.Node, parent_level: int | None) -> tuple[int, bool]:
149
150
  if parent_level is None:
150
151
  level = 0
151
152
  elif n.is_bifurcation():
@@ -164,7 +165,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
164
165
  """
165
166
 
166
167
  thre: float
167
- callbacks: List[Callable[[Tree.Branch], None]]
168
+ callbacks: list[Callable[[Tree.Branch], None]]
168
169
 
169
170
  def __init__(
170
171
  self, thre: float = 5, callback: Optional[Callable[[Tree.Branch], None]] = None
@@ -176,7 +177,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
176
177
  self.callbacks.append(callback)
177
178
 
178
179
  def __call__(self, x: Tree) -> Tree:
179
- removals: List[int] = []
180
+ removals: list[int] = []
180
181
  self.callbacks.append(lambda br: removals.append(br[1].id))
181
182
  x.traverse(leave=self._leave)
182
183
  self.callbacks.pop()
@@ -186,8 +187,8 @@ class CutShortTipBranch(Transform[Tree, Tree]):
186
187
  return f"threshold={self.thre}"
187
188
 
188
189
  def _leave(
189
- self, n: Tree.Node, children: List[Tuple[float, Tree.Node] | None]
190
- ) -> Tuple[float, Tree.Node] | None:
190
+ self, n: Tree.Node, children: list[tuple[float, Tree.Node] | None]
191
+ ) -> tuple[float, Tree.Node] | None:
191
192
  if len(children) == 0: # tip
192
193
  return 0, n
193
194
 
@@ -1,7 +1,8 @@
1
1
  """Assemble a tree."""
2
2
 
3
+ from collections.abc import Iterable
3
4
  from copy import copy
4
- from typing import Iterable, List, Optional, Tuple
5
+ from typing import Optional
5
6
 
6
7
  import numpy as np
7
8
  import pandas as pd
@@ -18,7 +19,7 @@ from swcgeom.transforms.base import Transform
18
19
  EPS = 1e-5
19
20
 
20
21
 
21
- class LinesToTree(Transform[List[pd.DataFrame], Tree]):
22
+ class LinesToTree(Transform[list[pd.DataFrame], Tree]):
22
23
  """Assemble lines to swc."""
23
24
 
24
25
  def __init__(self, *, thre: float = 0.2, undirected: bool = True):
@@ -97,7 +98,7 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
97
98
  undirected: bool = True,
98
99
  sort_nodes: bool = True,
99
100
  names: Optional[SWCNames] = None,
100
- ) -> Tuple[pd.DataFrame, List[pd.DataFrame]]:
101
+ ) -> tuple[pd.DataFrame, list[pd.DataFrame]]:
101
102
  """Trying assemble lines to a tree.
102
103
 
103
104
  Treat the first line as a tree, find a line whose shortest distance
swcgeom/utils/ellipse.py CHANGED
@@ -3,7 +3,6 @@
3
3
  # pylint: disable=invalid-name
4
4
 
5
5
  from functools import cached_property
6
- from typing import Tuple
7
6
 
8
7
  import numpy as np
9
8
  import numpy.linalg as la
@@ -22,7 +21,7 @@ class Ellipse:
22
21
  self.centroid = centroid
23
22
 
24
23
  @property
25
- def radii(self) -> Tuple[float, float]:
24
+ def radii(self) -> tuple[float, float]:
26
25
  # x, y radii.
27
26
  _U, D, _V = self.svd
28
27
  rx, ry = 1.0 / np.sqrt(D)
@@ -39,7 +38,7 @@ class Ellipse:
39
38
  return b
40
39
 
41
40
  @property
42
- def axes(self) -> Tuple[float, float]:
41
+ def axes(self) -> tuple[float, float]:
43
42
  # Major and minor semi-axis of the ellipse.
44
43
  rx, ry = self.radii
45
44
  dx, dy = 2 * rx, 2 * ry
@@ -77,7 +76,7 @@ def mvee(points: npt.NDArray[np.floating], tol: float = 1e-3) -> Ellipse:
77
76
 
78
77
  def _mvee(
79
78
  points: npt.NDArray[np.floating], tol: float = 1e-3
80
- ) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
79
+ ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
81
80
  """Finds the Minimum Volume Enclosing Ellipsoid.
82
81
 
83
82
  Returns
@@ -81,7 +81,8 @@ import logging
81
81
  import math
82
82
  import os
83
83
  import urllib.parse
84
- from typing import Any, Callable, Dict, Iterable, List, Literal, Optional, Tuple
84
+ from collections.abc import Callable, Iterable
85
+ from typing import Any, Literal, Optional
85
86
 
86
87
  from tqdm import tqdm
87
88
 
@@ -116,7 +117,7 @@ SIZE_METADATA = 2 * GB
116
117
  SIZE_DATA = 20 * GB
117
118
 
118
119
  RESOURCES = Literal["morpho_cng", "morpho_source", "log_cng", "log_source"]
119
- DOWNLOAD_CONFIGS: Dict[RESOURCES, Tuple[str, int]] = {
120
+ DOWNLOAD_CONFIGS: dict[RESOURCES, tuple[str, int]] = {
120
121
  # name/path: (url, size)
121
122
  "morpho_cng": (URL_MORPHO_CNG, 20 * GB),
122
123
  "morpho_source": (URL_LOG_CNG, 512 * GB),
@@ -145,7 +146,7 @@ invalid_ids = [
145
146
  # fmt: on
146
147
 
147
148
 
148
- def neuromorpho_is_valid(metadata: Dict[str, Any]) -> bool:
149
+ def neuromorpho_is_valid(metadata: dict[str, Any]) -> bool:
149
150
  return metadata["neuron_id"] not in invalid_ids
150
151
 
151
152
 
@@ -209,7 +210,7 @@ class NeuroMorpho:
209
210
  self._info("skip download metadata")
210
211
 
211
212
  # file
212
- def dumps(keys: List[bytes]) -> str:
213
+ def dumps(keys: list[bytes]) -> str:
213
214
  return json.dumps([i.decode("utf-8") for i in keys])
214
215
 
215
216
  for name in resources:
@@ -238,8 +239,8 @@ class NeuroMorpho:
238
239
  self,
239
240
  dest: Optional[str] = None,
240
241
  *,
241
- group_by: Optional[str | Callable[[Dict[str, Any]], str | None]] = None,
242
- where: Optional[Callable[[Dict[str, Any]], bool]] = None,
242
+ group_by: Optional[str | Callable[[dict[str, Any]], str | None]] = None,
243
+ where: Optional[Callable[[dict[str, Any]], bool]] = None,
243
244
  encoding: str | None = "utf-8",
244
245
  ) -> None:
245
246
  r"""Convert lmdb format to SWCs.
@@ -249,11 +250,11 @@ class NeuroMorpho:
249
250
  path : str
250
251
  dest : str, optional
251
252
  If None, use `path/swc`.
252
- group_by : str | (metadata: Dict[str, Any]) -> str | None, optional
253
+ group_by : str | (metadata: dict[str, Any]) -> str | None, optional
253
254
  Group neurons by metadata. If a None is returned then no
254
255
  grouping. If a string is entered, use it as a metadata
255
256
  attribute name for grouping, e.g.: `archive`, `species`.
256
- where : (metadata: Dict[str, Any]) -> bool, optional
257
+ where : (metadata: dict[str, Any]) -> bool, optional
257
258
  Filter neurons by metadata.
258
259
  encoding : str | None, default to `utf-8`
259
260
  Change swc encoding, part of the original data is not utf-8
@@ -346,14 +347,14 @@ class NeuroMorpho:
346
347
  pages: Optional[Iterable[int]] = None,
347
348
  page_size: int = API_PAGE_SIZE_MAX,
348
349
  **kwargs,
349
- ) -> List[int]:
350
+ ) -> list[int]:
350
351
  r"""Download all neuron metadata.
351
352
 
352
353
  Parameters
353
354
  ----------
354
355
  path : str
355
356
  Path to save data.
356
- pages : list of int, optional
357
+ pages : List of int, optional
357
358
  If is None, download all pages.
358
359
  verbose : bool, default False
359
360
  Show verbose log.
@@ -362,7 +363,7 @@ class NeuroMorpho:
362
363
 
363
364
  Returns
364
365
  -------
365
- err_pages : list of int
366
+ err_pages : List of int
366
367
  Failed pages.
367
368
  """
368
369
 
@@ -402,7 +403,7 @@ class NeuroMorpho:
402
403
  override: bool = False,
403
404
  map_size: int = 512 * GB,
404
405
  **kwargs,
405
- ) -> List[bytes]:
406
+ ) -> list[bytes]:
406
407
  """Download files.
407
408
 
408
409
  Parameters
@@ -412,7 +413,7 @@ class NeuroMorpho:
412
413
  Path to save data.
413
414
  path_metadata : str
414
415
  Path to lmdb of metadata.
415
- keys : list of bytes, optional
416
+ keys : List of bytes, optional
416
417
  If exist, ignore `override` option. If None, download all key.
417
418
  override : bool, default False
418
419
  Override even exists.
@@ -422,7 +423,7 @@ class NeuroMorpho:
422
423
 
423
424
  Returns
424
425
  -------
425
- err_keys : list of str
426
+ err_keys : List of str
426
427
  Failed keys.
427
428
  """
428
429
 
@@ -459,7 +460,7 @@ class NeuroMorpho:
459
460
 
460
461
  def _get_metadata(
461
462
  self, page: int, page_size: int = API_PAGE_SIZE_MAX, **kwargs
462
- ) -> Dict[str, Any]:
463
+ ) -> dict[str, Any]:
463
464
  params = {
464
465
  "page": page,
465
466
  "size": page_size,
@@ -470,7 +471,7 @@ class NeuroMorpho:
470
471
  resp = self._get(url, **kwargs)
471
472
  return json.loads(resp)
472
473
 
473
- def _get_file(self, url: str, metadata: Dict[str, Any], **kwargs) -> bytes:
474
+ def _get_file(self, url: str, metadata: dict[str, Any], **kwargs) -> bytes:
474
475
  """Get file.
475
476
 
476
477
  Returns
@@ -1,6 +1,6 @@
1
- """Rendering related utils."""
1
+ """2D Plotting utils."""
2
2
 
3
- from typing import Optional, Tuple
3
+ from typing import Optional
4
4
 
5
5
  import matplotlib.pyplot as plt
6
6
  import numpy as np
@@ -19,7 +19,12 @@ __all__ = ["draw_lines", "draw_direction_indicator", "draw_circles", "get_fig_ax
19
19
 
20
20
 
21
21
  def draw_lines(
22
- ax: Axes, lines: npt.NDArray[np.floating], camera: Camera, **kwargs
22
+ ax: Axes,
23
+ lines: npt.NDArray[np.floating],
24
+ camera: Camera,
25
+ joinstyle="round",
26
+ capstyle="round",
27
+ **kwargs,
23
28
  ) -> LineCollection:
24
29
  """Draw lines.
25
30
 
@@ -43,11 +48,12 @@ def draw_lines(
43
48
  starts, ends = np.dot(T, starts.T).T[:, 0:2], np.dot(T, ends.T).T[:, 0:2]
44
49
 
45
50
  edges = np.stack([starts, ends], axis=1)
46
- return ax.add_collection(LineCollection(edges, **kwargs)) # type: ignore
51
+ collection = LineCollection(edges, joinstyle=joinstyle, capstyle=capstyle, **kwargs) # type: ignore
52
+ return ax.add_collection(collection) # type: ignore
47
53
 
48
54
 
49
55
  def draw_direction_indicator(
50
- ax: Axes, camera: Camera, loc: Tuple[float, float]
56
+ ax: Axes, camera: Camera, loc: tuple[float, float]
51
57
  ) -> None:
52
58
  x, y = loc
53
59
  direction = camera.MV.dot(
@@ -120,7 +126,7 @@ def draw_circles(
120
126
 
121
127
  def get_fig_ax(
122
128
  fig: Optional[Figure] = None, ax: Optional[Axes] = None
123
- ) -> Tuple[Figure, Axes]:
129
+ ) -> tuple[Figure, Axes]:
124
130
  if fig is None and ax is not None:
125
131
  fig = ax.get_figure()
126
132
  assert fig is not None, "expecting a figure from the axes"
@@ -0,0 +1,31 @@
1
+ """3D Plotting utils."""
2
+
3
+ import numpy as np
4
+ import numpy.typing as npt
5
+ from mpl_toolkits.mplot3d import Axes3D
6
+ from mpl_toolkits.mplot3d.art3d import Line3DCollection
7
+
8
+ __all__ = ["draw_lines_3d"]
9
+
10
+
11
+ def draw_lines_3d(
12
+ ax: Axes3D,
13
+ lines: npt.NDArray[np.floating],
14
+ joinstyle="round",
15
+ capstyle="round",
16
+ **kwargs,
17
+ ):
18
+ """Draw lines.
19
+
20
+ Parameters
21
+ ----------
22
+ ax : ~matplotlib.axes.Axes
23
+ lines : A collection of coords of lines
24
+ Excepting a ndarray of shape (N, 2, 3), the axis-2 holds two points,
25
+ and the axis-3 holds the coordinates (x, y, z).
26
+ **kwargs : dict[str, Unknown]
27
+ Forwarded to `~mpl_toolkits.mplot3d.art3d.Line3DCollection`.
28
+ """
29
+
30
+ line_collection = Line3DCollection(lines, joinstyle=joinstyle, capstyle=capstyle, **kwargs) # type: ignore
31
+ return ax.add_collection3d(line_collection)