swcgeom 0.17.0__py3-none-any.whl → 0.17.2__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 (49) hide show
  1. swcgeom/_version.py +2 -2
  2. swcgeom/analysis/feature_extractor.py +25 -15
  3. swcgeom/analysis/features.py +20 -8
  4. swcgeom/analysis/lmeasure.py +33 -12
  5. swcgeom/analysis/sholl.py +10 -28
  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 +17 -3
  14. swcgeom/core/path.py +6 -9
  15. swcgeom/core/population.py +43 -29
  16. swcgeom/core/swc.py +11 -10
  17. swcgeom/core/swc_utils/base.py +8 -17
  18. swcgeom/core/swc_utils/checker.py +3 -11
  19. swcgeom/core/swc_utils/io.py +7 -6
  20. swcgeom/core/swc_utils/normalizer.py +4 -3
  21. swcgeom/core/swc_utils/subtree.py +2 -2
  22. swcgeom/core/tree.py +41 -40
  23. swcgeom/core/tree_utils.py +13 -17
  24. swcgeom/core/tree_utils_impl.py +3 -3
  25. swcgeom/images/augmentation.py +3 -3
  26. swcgeom/images/folder.py +12 -26
  27. swcgeom/images/io.py +21 -35
  28. swcgeom/transforms/image_stack.py +20 -8
  29. swcgeom/transforms/images.py +3 -12
  30. swcgeom/transforms/neurolucida_asc.py +4 -6
  31. swcgeom/transforms/population.py +1 -3
  32. swcgeom/transforms/tree.py +38 -25
  33. swcgeom/transforms/tree_assembler.py +4 -3
  34. swcgeom/utils/download.py +44 -21
  35. swcgeom/utils/ellipse.py +3 -4
  36. swcgeom/utils/neuromorpho.py +17 -16
  37. swcgeom/utils/plotter_2d.py +12 -6
  38. swcgeom/utils/plotter_3d.py +31 -0
  39. swcgeom/utils/renderer.py +6 -6
  40. swcgeom/utils/sdf.py +4 -7
  41. swcgeom/utils/solid_geometry.py +1 -3
  42. swcgeom/utils/transforms.py +2 -4
  43. swcgeom/utils/volumetric_object.py +8 -10
  44. {swcgeom-0.17.0.dist-info → swcgeom-0.17.2.dist-info}/METADATA +19 -19
  45. swcgeom-0.17.2.dist-info/RECORD +67 -0
  46. {swcgeom-0.17.0.dist-info → swcgeom-0.17.2.dist-info}/WHEEL +1 -1
  47. swcgeom-0.17.0.dist-info/RECORD +0 -65
  48. {swcgeom-0.17.0.dist-info → swcgeom-0.17.2.dist-info}/LICENSE +0 -0
  49. {swcgeom-0.17.0.dist-info → swcgeom-0.17.2.dist-info}/top_level.txt +0 -0
@@ -1,10 +1,8 @@
1
1
  """Image stack related transform."""
2
2
 
3
- import warnings
4
- from typing import Tuple
5
-
6
3
  import numpy as np
7
4
  import numpy.typing as npt
5
+ from typing_extensions import deprecated
8
6
 
9
7
  from swcgeom.transforms.base import Identity, Transform
10
8
 
@@ -28,7 +26,7 @@ NDArrayf32 = npt.NDArray[np.float32]
28
26
  class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
29
27
  """Get image stack center."""
30
28
 
31
- def __init__(self, shape_out: int | Tuple[int, int, int]):
29
+ def __init__(self, shape_out: int | tuple[int, int, int]):
32
30
  super().__init__()
33
31
  self.shape_out = (
34
32
  shape_out
@@ -46,6 +44,7 @@ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
46
44
  return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
47
45
 
48
46
 
47
+ @deprecated("use `ImagesCenterCrop` instead", stacklevel=2)
49
48
  class Center(ImagesCenterCrop):
50
49
  """Get image stack center.
51
50
 
@@ -53,14 +52,6 @@ class Center(ImagesCenterCrop):
53
52
  Use :class:`ImagesCenterCrop` instead.
54
53
  """
55
54
 
56
- def __init__(self, shape_out: int | Tuple[int, int, int]):
57
- warnings.warn(
58
- "`Center` is deprecated, use `ImagesCenterCrop` instead",
59
- DeprecationWarning,
60
- stacklevel=2,
61
- )
62
- super().__init__(shape_out)
63
-
64
55
 
65
56
  class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
66
57
  def __init__(self, scaler: float) -> None:
@@ -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,9 +1,10 @@
1
1
  """Transformation in tree."""
2
2
 
3
- import warnings
4
- from typing import Callable, List, Optional, Tuple
3
+ from collections.abc import Callable
4
+ from typing import Optional
5
5
 
6
6
  import numpy as np
7
+ from typing_extensions import deprecated
7
8
 
8
9
  from swcgeom.core import BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
9
10
  from swcgeom.core.swc_utils import SWCTypes, get_types
@@ -19,7 +20,7 @@ __all__ = [
19
20
  "CutByType",
20
21
  "CutAxonTree",
21
22
  "CutDendriteTree",
22
- "CutByBifurcationOrder",
23
+ "CutByFurcationOrder",
23
24
  "CutShortTipBranch",
24
25
  ]
25
26
 
@@ -68,6 +69,7 @@ class TreeSmoother(Transform[Tree, Tree]): # pylint: disable=missing-class-docs
68
69
  return f"n_nodes={self.n_nodes}"
69
70
 
70
71
 
72
+ @deprecated("Use `Normalizer` instead")
71
73
  class TreeNormalizer(Normalizer[Tree]):
72
74
  """Noramlize coordinates and radius to 0-1.
73
75
 
@@ -75,15 +77,6 @@ class TreeNormalizer(Normalizer[Tree]):
75
77
  Use :cls:`Normalizer` instead.
76
78
  """
77
79
 
78
- def __init__(self, *args, **kwargs) -> None:
79
- warnings.warn(
80
- "`TreeNormalizer` has been replaced by `Normalizer` since "
81
- "v0.6.0 beacuse it applies more widely, and this will be "
82
- "removed in next version",
83
- DeprecationWarning,
84
- )
85
- super().__init__(*args, **kwargs)
86
-
87
80
 
88
81
  class CutByType(Transform[Tree, Tree]):
89
82
  """Cut tree by type.
@@ -102,7 +95,7 @@ class CutByType(Transform[Tree, Tree]):
102
95
  def __call__(self, x: Tree) -> Tree:
103
96
  removals = set(x.id()[x.type() != self.type])
104
97
 
105
- def leave(n: Tree.Node, keep_children: List[bool]) -> bool:
98
+ def leave(n: Tree.Node, keep_children: list[bool]) -> bool:
106
99
  if n.id in removals and any(keep_children):
107
100
  removals.remove(n.id)
108
101
  return n.id not in removals
@@ -131,28 +124,48 @@ class CutDendriteTree(CutByType):
131
124
  super().__init__(type=types.basal_dendrite) # TODO: apical dendrite
132
125
 
133
126
 
134
- class CutByBifurcationOrder(Transform[Tree, Tree]):
135
- """Cut tree by bifurcation order."""
127
+ class CutByFurcationOrder(Transform[Tree, Tree]):
128
+ """Cut tree by furcation order."""
136
129
 
137
- max_bifurcation_order: int
130
+ max_furcation_order: int
138
131
 
139
132
  def __init__(self, max_bifurcation_order: int) -> None:
140
- self.max_bifurcation_order = max_bifurcation_order
133
+ self.max_furcation_order = max_bifurcation_order
141
134
 
142
135
  def __call__(self, x: Tree) -> Tree:
143
136
  return cut_tree(x, enter=self._enter)
144
137
 
145
138
  def __repr__(self) -> str:
146
- return f"CutByBifurcationOrder-{self.max_bifurcation_order}"
139
+ return f"CutByBifurcationOrder-{self.max_furcation_order}"
147
140
 
148
- def _enter(self, n: Tree.Node, parent_level: int | None) -> Tuple[int, bool]:
141
+ def _enter(self, n: Tree.Node, parent_level: int | None) -> tuple[int, bool]:
149
142
  if parent_level is None:
150
143
  level = 0
151
- elif n.is_bifurcation():
144
+ elif n.is_furcation():
152
145
  level = parent_level + 1
153
146
  else:
154
147
  level = parent_level
155
- return (level, level >= self.max_bifurcation_order)
148
+ return (level, level >= self.max_furcation_order)
149
+
150
+
151
+ @deprecated("Use CutByFurcationOrder instead")
152
+ class CutByBifurcationOrder(CutByFurcationOrder):
153
+ """Cut tree by bifurcation order.
154
+
155
+ Notes
156
+ -----
157
+ Deprecated due to the wrong spelling of furcation. For now, it
158
+ is just an alias of `CutByFurcationOrder` and raise a warning. It
159
+ will be change to raise an error in the future.
160
+ """
161
+
162
+ max_furcation_order: int
163
+
164
+ def __init__(self, max_bifurcation_order: int) -> None:
165
+ super().__init__(max_bifurcation_order)
166
+
167
+ def __repr__(self) -> str:
168
+ return f"CutByBifurcationOrder-{self.max_furcation_order}"
156
169
 
157
170
 
158
171
  class CutShortTipBranch(Transform[Tree, Tree]):
@@ -164,7 +177,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
164
177
  """
165
178
 
166
179
  thre: float
167
- callbacks: List[Callable[[Tree.Branch], None]]
180
+ callbacks: list[Callable[[Tree.Branch], None]]
168
181
 
169
182
  def __init__(
170
183
  self, thre: float = 5, callback: Optional[Callable[[Tree.Branch], None]] = None
@@ -176,7 +189,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
176
189
  self.callbacks.append(callback)
177
190
 
178
191
  def __call__(self, x: Tree) -> Tree:
179
- removals: List[int] = []
192
+ removals: list[int] = []
180
193
  self.callbacks.append(lambda br: removals.append(br[1].id))
181
194
  x.traverse(leave=self._leave)
182
195
  self.callbacks.pop()
@@ -186,8 +199,8 @@ class CutShortTipBranch(Transform[Tree, Tree]):
186
199
  return f"threshold={self.thre}"
187
200
 
188
201
  def _leave(
189
- self, n: Tree.Node, children: List[Tuple[float, Tree.Node] | None]
190
- ) -> Tuple[float, Tree.Node] | None:
202
+ self, n: Tree.Node, children: list[tuple[float, Tree.Node] | None]
203
+ ) -> tuple[float, Tree.Node] | None:
191
204
  if len(children) == 0: # tip
192
205
  return 0, n
193
206
 
@@ -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/download.py CHANGED
@@ -13,6 +13,7 @@ import itertools
13
13
  import logging
14
14
  import multiprocessing
15
15
  import os
16
+ from functools import partial
16
17
  from urllib.parse import urljoin
17
18
 
18
19
  __all__ = ["download", "fetch_page", "clone_index_page"]
@@ -26,7 +27,7 @@ def download(dst: str, url: str) -> None:
26
27
  r = conn.request("GET", url)
27
28
 
28
29
  dirname = os.path.dirname(dst)
29
- if not os.path.exists(dirname):
30
+ if dirname != "" and not os.path.exists(dirname):
30
31
  os.makedirs(dirname)
31
32
 
32
33
  with open(dst, "wb") as file:
@@ -41,7 +42,7 @@ def fetch_page(url: str):
41
42
  conn = connection_from_url(url)
42
43
  r = conn.request("GET", url)
43
44
  data = r.data.decode("utf-8")
44
- return BeautifulSoup(data)
45
+ return BeautifulSoup(data, features="html.parser")
45
46
 
46
47
 
47
48
  def clone_index_page(
@@ -62,31 +63,35 @@ def clone_index_page(
62
63
  multiprocess : int, default `4`
63
64
  How many process are available for download.
64
65
  """
65
- from urllib3.exceptions import HTTPError
66
-
67
66
  files = get_urls_in_index_page(index_url)
68
67
  logging.info("downloader: search `%s`, found %s files.", index_url, len(files))
69
68
 
70
- def task(url: str) -> None:
71
- filepath = url.removeprefix(index_url)
72
- dist = os.path.join(dist_dir, filepath)
73
- if os.path.exists(filepath):
74
- if not override:
75
- logging.info("downloader: file `%s` exits, skiped.", dist)
76
- return
69
+ task = partial(
70
+ _clone_index_page, index_url=index_url, dist_dir=dist_dir, override=override
71
+ )
72
+ with multiprocessing.Pool(multiprocess) as p:
73
+ p.map(task, files)
77
74
 
78
- logging.info("downloader: file `%s` exits, deleted.", dist)
79
- os.remove(filepath)
80
75
 
81
- try:
82
- logging.info("downloader: downloading `%s` to `%s`", url, dist)
83
- download(filepath, url)
84
- logging.info("downloader: download `%s` to `%s`", url, dist)
85
- except HTTPError as ex:
86
- logging.info("downloader: fails to download `%s`, except `%s`", url, ex)
76
+ def _clone_index_page(url: str, index_url: str, dist_dir: str, override: bool) -> None:
77
+ from urllib3.exceptions import HTTPError
87
78
 
88
- with multiprocessing.Pool(multiprocess) as p:
89
- p.map(task, files)
79
+ filepath = url.removeprefix(index_url)
80
+ dist = os.path.join(dist_dir, filepath)
81
+ if os.path.exists(dist):
82
+ if not override:
83
+ logging.info("downloader: file `%s` exits, skiped.", dist)
84
+ return
85
+
86
+ logging.info("downloader: file `%s` exits, deleted.", dist)
87
+ os.remove(dist)
88
+
89
+ try:
90
+ logging.info("downloader: downloading `%s` to `%s`", url, dist)
91
+ download(dist, url)
92
+ logging.info("downloader: download `%s` to `%s`", url, dist)
93
+ except HTTPError as ex:
94
+ logging.info("downloader: fails to download `%s`, except `%s`", url, ex)
90
95
 
91
96
 
92
97
  def get_urls_in_index_page(url: str) -> list[str]:
@@ -97,3 +102,21 @@ def get_urls_in_index_page(url: str) -> list[str]:
97
102
  dirs = [urljoin(url, a) for a in links if a != "../" and a.endswith("/")]
98
103
  files.extend(itertools.chain(*[get_urls_in_index_page(dir) for dir in dirs]))
99
104
  return files
105
+
106
+
107
+ if __name__ == "__main__":
108
+ import argparse
109
+
110
+ parser = argparse.ArgumentParser(description="Download files from index page.")
111
+ parser.add_argument("url", type=str, help="URL of index page.")
112
+ parser.add_argument("dist", type=str, help="Directory of dist.")
113
+ parser.add_argument(
114
+ "--override", type=bool, default=False, help="Override existing file."
115
+ )
116
+ parser.add_argument(
117
+ "--multiprocess", type=int, default=4, help="How many process are available."
118
+ )
119
+ args = parser.parse_args()
120
+
121
+ logging.basicConfig(level=logging.INFO)
122
+ clone_index_page(args.url, args.dist, args.override, args.multiprocess)
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)
swcgeom/utils/renderer.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Rendering related utils."""
2
2
 
3
3
  from functools import cached_property
4
- from typing import Dict, Literal, Tuple, cast
4
+ from typing import Literal, cast
5
5
 
6
6
  import numpy as np
7
7
  import numpy.typing as npt
@@ -15,9 +15,9 @@ from swcgeom.utils.transforms import (
15
15
 
16
16
  __all__ = ["CameraOptions", "Camera", "SimpleCamera", "palette"]
17
17
 
18
- CameraOption = Vec3f | Tuple[Vec3f, Vec3f] | Tuple[Vec3f, Vec3f, Vec3f]
18
+ CameraOption = Vec3f | tuple[Vec3f, Vec3f] | tuple[Vec3f, Vec3f, Vec3f]
19
19
  CameraPreset = Literal["xy", "yz", "zx", "yx", "zy", "xz"]
20
- CameraPresets: Dict[CameraPreset, Tuple[Vec3f, Vec3f, Vec3f]] = {
20
+ CameraPresets: dict[CameraPreset, tuple[Vec3f, Vec3f, Vec3f]] = {
21
21
  "xy": ((0.0, 0.0, 0.0), (+0.0, +0.0, -1.0), (+0.0, +1.0, +0.0)),
22
22
  "yz": ((0.0, 0.0, 0.0), (-1.0, +0.0, +0.0), (+0.0, +0.0, +1.0)),
23
23
  "zx": ((0.0, 0.0, 0.0), (+0.0, -1.0, +0.0), (+1.0, +0.0, +0.0)),
@@ -77,7 +77,7 @@ class SimpleCamera(Camera):
77
77
  if isinstance(camera[0], tuple):
78
78
  return cls((0, 0, 0), cast(Vec3f, camera), (0, 1, 0))
79
79
 
80
- return cls(*cast(Tuple[Vec3f, Vec3f, Vec3f], camera))
80
+ return cls(*cast(tuple[Vec3f, Vec3f, Vec3f], camera))
81
81
 
82
82
 
83
83
  class Palette:
@@ -85,8 +85,8 @@ class Palette:
85
85
 
86
86
  # pylint: disable=too-few-public-methods
87
87
 
88
- default: Dict[int, str]
89
- vaa3d: Dict[int, str]
88
+ default: dict[int, str]
89
+ vaa3d: dict[int, str]
90
90
 
91
91
  def __init__(self):
92
92
  default = [
swcgeom/utils/sdf.py CHANGED
@@ -10,10 +10,11 @@ the future, use `sdflit` instead.
10
10
 
11
11
  import warnings
12
12
  from abc import ABC, abstractmethod
13
- from typing import Iterable, Tuple
13
+ from collections.abc import Iterable
14
14
 
15
15
  import numpy as np
16
16
  import numpy.typing as npt
17
+ from typing_extensions import deprecated
17
18
 
18
19
  from swcgeom.utils.solid_geometry import project_vector_on_plane
19
20
 
@@ -29,7 +30,7 @@ __all__ = [
29
30
  ]
30
31
 
31
32
  # Axis-aligned bounding box, tuple of array of shape (3,)
32
- AABB = Tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]
33
+ AABB = tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]
33
34
 
34
35
 
35
36
  class SDF(ABC):
@@ -173,6 +174,7 @@ class SDFDifference(SDF):
173
174
  return flags
174
175
 
175
176
 
177
+ @deprecated("Use `SDFUnion` instead")
176
178
  class SDFCompose(SDFUnion):
177
179
  """Compose multiple SDFs.
178
180
 
@@ -181,11 +183,6 @@ class SDFCompose(SDFUnion):
181
183
  """
182
184
 
183
185
  def __init__(self, sdfs: Iterable[SDF]) -> None:
184
- warnings.warn(
185
- "`SDFCompose` has been replace by `SDFUnion` since v0.14.0, "
186
- "and will be removed in next version",
187
- DeprecationWarning,
188
- )
189
186
  sdfs = list(sdfs)
190
187
  if len(sdfs) == 1:
191
188
  warnings.warn("compose only one SDF, use SDFCompose.compose instead")