swcgeom 0.15.0__py3-none-any.whl → 0.18.3__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 (72) hide show
  1. swcgeom/__init__.py +26 -1
  2. swcgeom/analysis/__init__.py +21 -8
  3. swcgeom/analysis/feature_extractor.py +43 -18
  4. swcgeom/analysis/features.py +250 -0
  5. swcgeom/analysis/lmeasure.py +857 -0
  6. swcgeom/analysis/sholl.py +55 -29
  7. swcgeom/analysis/trunk.py +27 -11
  8. swcgeom/analysis/visualization.py +24 -9
  9. swcgeom/analysis/visualization3d.py +100 -0
  10. swcgeom/analysis/volume.py +19 -4
  11. swcgeom/core/__init__.py +32 -9
  12. swcgeom/core/branch.py +28 -7
  13. swcgeom/core/branch_tree.py +18 -4
  14. swcgeom/core/{segment.py → compartment.py} +31 -10
  15. swcgeom/core/node.py +31 -10
  16. swcgeom/core/path.py +37 -10
  17. swcgeom/core/population.py +103 -34
  18. swcgeom/core/swc.py +26 -10
  19. swcgeom/core/swc_utils/__init__.py +21 -7
  20. swcgeom/core/swc_utils/assembler.py +27 -1
  21. swcgeom/core/swc_utils/base.py +25 -12
  22. swcgeom/core/swc_utils/checker.py +31 -14
  23. swcgeom/core/swc_utils/io.py +24 -7
  24. swcgeom/core/swc_utils/normalizer.py +20 -4
  25. swcgeom/core/swc_utils/subtree.py +17 -2
  26. swcgeom/core/tree.py +85 -72
  27. swcgeom/core/tree_utils.py +31 -16
  28. swcgeom/core/tree_utils_impl.py +18 -3
  29. swcgeom/images/__init__.py +17 -2
  30. swcgeom/images/augmentation.py +24 -4
  31. swcgeom/images/contrast.py +122 -0
  32. swcgeom/images/folder.py +97 -39
  33. swcgeom/images/io.py +108 -121
  34. swcgeom/transforms/__init__.py +28 -10
  35. swcgeom/transforms/base.py +17 -2
  36. swcgeom/transforms/branch.py +74 -8
  37. swcgeom/transforms/branch_tree.py +82 -0
  38. swcgeom/transforms/geometry.py +22 -7
  39. swcgeom/transforms/image_preprocess.py +115 -0
  40. swcgeom/transforms/image_stack.py +37 -13
  41. swcgeom/transforms/images.py +184 -7
  42. swcgeom/transforms/mst.py +20 -5
  43. swcgeom/transforms/neurolucida_asc.py +508 -0
  44. swcgeom/transforms/path.py +15 -0
  45. swcgeom/transforms/population.py +16 -3
  46. swcgeom/transforms/tree.py +89 -31
  47. swcgeom/transforms/tree_assembler.py +23 -7
  48. swcgeom/utils/__init__.py +27 -11
  49. swcgeom/utils/debug.py +15 -0
  50. swcgeom/utils/download.py +59 -21
  51. swcgeom/utils/dsu.py +15 -0
  52. swcgeom/utils/ellipse.py +18 -4
  53. swcgeom/utils/file.py +15 -0
  54. swcgeom/utils/neuromorpho.py +439 -302
  55. swcgeom/utils/numpy_helper.py +29 -4
  56. swcgeom/utils/plotter_2d.py +151 -0
  57. swcgeom/utils/plotter_3d.py +48 -0
  58. swcgeom/utils/renderer.py +49 -145
  59. swcgeom/utils/sdf.py +24 -8
  60. swcgeom/utils/solid_geometry.py +16 -3
  61. swcgeom/utils/transforms.py +17 -4
  62. swcgeom/utils/volumetric_object.py +23 -10
  63. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
  64. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/METADATA +28 -24
  65. swcgeom-0.18.3.dist-info/RECORD +67 -0
  66. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/WHEEL +1 -1
  67. swcgeom/_version.py +0 -16
  68. swcgeom/analysis/branch_features.py +0 -67
  69. swcgeom/analysis/node_features.py +0 -121
  70. swcgeom/analysis/path_features.py +0 -37
  71. swcgeom-0.15.0.dist-info/RECORD +0 -62
  72. {swcgeom-0.15.0.dist-info → swcgeom-0.18.3.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,31 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Transformation in tree."""
2
17
 
3
- import warnings
4
- from typing import Callable, List, Optional, Tuple
18
+ from collections.abc import Callable
19
+ from typing import Optional
5
20
 
6
21
  import numpy as np
22
+ from typing_extensions import deprecated
7
23
 
8
- from swcgeom.core import BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
24
+ from swcgeom.core import Branch, BranchTree, DictSWC, Path, Tree, cut_tree, to_subtree
9
25
  from swcgeom.core.swc_utils import SWCTypes, get_types
10
26
  from swcgeom.transforms.base import Transform
11
- from swcgeom.transforms.branch import BranchConvSmoother
27
+ from swcgeom.transforms.branch import BranchConvSmoother, BranchIsometricResampler
28
+ from swcgeom.transforms.branch_tree import BranchTreeAssembler
12
29
  from swcgeom.transforms.geometry import Normalizer
13
30
 
14
31
  __all__ = [
@@ -19,8 +36,9 @@ __all__ = [
19
36
  "CutByType",
20
37
  "CutAxonTree",
21
38
  "CutDendriteTree",
22
- "CutByBifurcationOrder",
39
+ "CutByFurcationOrder",
23
40
  "CutShortTipBranch",
41
+ "IsometricResampler",
24
42
  ]
25
43
 
26
44
 
@@ -64,21 +82,17 @@ class TreeSmoother(Transform[Tree, Tree]): # pylint: disable=missing-class-docs
64
82
 
65
83
  return x
66
84
 
67
- def extra_repr(self):
85
+ def extra_repr(self) -> str:
68
86
  return f"n_nodes={self.n_nodes}"
69
87
 
70
88
 
89
+ @deprecated("Use `Normalizer` instead")
71
90
  class TreeNormalizer(Normalizer[Tree]):
72
- """Noramlize coordinates and radius to 0-1."""
73
-
74
- def __init__(self, *args, **kwargs) -> None:
75
- warnings.warn(
76
- "`TreeNormalizer` has been replaced by `Normalizer` since "
77
- "v0.6.0 beacuse it applies more widely, and this will be "
78
- "removed in next version",
79
- DeprecationWarning,
80
- )
81
- super().__init__(*args, **kwargs)
91
+ """Noramlize coordinates and radius to 0-1.
92
+
93
+ .. deprecated:: 0.6.0
94
+ Use :cls:`Normalizer` instead.
95
+ """
82
96
 
83
97
 
84
98
  class CutByType(Transform[Tree, Tree]):
@@ -98,7 +112,7 @@ class CutByType(Transform[Tree, Tree]):
98
112
  def __call__(self, x: Tree) -> Tree:
99
113
  removals = set(x.id()[x.type() != self.type])
100
114
 
101
- def leave(n: Tree.Node, keep_children: List[bool]) -> bool:
115
+ def leave(n: Tree.Node, keep_children: list[bool]) -> bool:
102
116
  if n.id in removals and any(keep_children):
103
117
  removals.remove(n.id)
104
118
  return n.id not in removals
@@ -107,7 +121,7 @@ class CutByType(Transform[Tree, Tree]):
107
121
  y = to_subtree(x, removals)
108
122
  return y
109
123
 
110
- def extra_repr(self):
124
+ def extra_repr(self) -> str:
111
125
  return f"type={self.type}"
112
126
 
113
127
 
@@ -127,28 +141,48 @@ class CutDendriteTree(CutByType):
127
141
  super().__init__(type=types.basal_dendrite) # TODO: apical dendrite
128
142
 
129
143
 
130
- class CutByBifurcationOrder(Transform[Tree, Tree]):
131
- """Cut tree by bifurcation order."""
144
+ class CutByFurcationOrder(Transform[Tree, Tree]):
145
+ """Cut tree by furcation order."""
132
146
 
133
- max_bifurcation_order: int
147
+ max_furcation_order: int
134
148
 
135
149
  def __init__(self, max_bifurcation_order: int) -> None:
136
- self.max_bifurcation_order = max_bifurcation_order
150
+ self.max_furcation_order = max_bifurcation_order
137
151
 
138
152
  def __call__(self, x: Tree) -> Tree:
139
153
  return cut_tree(x, enter=self._enter)
140
154
 
141
155
  def __repr__(self) -> str:
142
- return f"CutByBifurcationOrder-{self.max_bifurcation_order}"
156
+ return f"CutByBifurcationOrder-{self.max_furcation_order}"
143
157
 
144
- def _enter(self, n: Tree.Node, parent_level: int | None) -> Tuple[int, bool]:
158
+ def _enter(self, n: Tree.Node, parent_level: int | None) -> tuple[int, bool]:
145
159
  if parent_level is None:
146
160
  level = 0
147
- elif n.is_bifurcation():
161
+ elif n.is_furcation():
148
162
  level = parent_level + 1
149
163
  else:
150
164
  level = parent_level
151
- return (level, level >= self.max_bifurcation_order)
165
+ return (level, level >= self.max_furcation_order)
166
+
167
+
168
+ @deprecated("Use CutByFurcationOrder instead")
169
+ class CutByBifurcationOrder(CutByFurcationOrder):
170
+ """Cut tree by bifurcation order.
171
+
172
+ Notes
173
+ -----
174
+ Deprecated due to the wrong spelling of furcation. For now, it
175
+ is just an alias of `CutByFurcationOrder` and raise a warning. It
176
+ will be change to raise an error in the future.
177
+ """
178
+
179
+ max_furcation_order: int
180
+
181
+ def __init__(self, max_bifurcation_order: int) -> None:
182
+ super().__init__(max_bifurcation_order)
183
+
184
+ def __repr__(self) -> str:
185
+ return f"CutByBifurcationOrder-{self.max_furcation_order}"
152
186
 
153
187
 
154
188
  class CutShortTipBranch(Transform[Tree, Tree]):
@@ -160,7 +194,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
160
194
  """
161
195
 
162
196
  thre: float
163
- callbacks: List[Callable[[Tree.Branch], None]]
197
+ callbacks: list[Callable[[Tree.Branch], None]]
164
198
 
165
199
  def __init__(
166
200
  self, thre: float = 5, callback: Optional[Callable[[Tree.Branch], None]] = None
@@ -172,18 +206,18 @@ class CutShortTipBranch(Transform[Tree, Tree]):
172
206
  self.callbacks.append(callback)
173
207
 
174
208
  def __call__(self, x: Tree) -> Tree:
175
- removals: List[int] = []
209
+ removals: list[int] = []
176
210
  self.callbacks.append(lambda br: removals.append(br[1].id))
177
211
  x.traverse(leave=self._leave)
178
212
  self.callbacks.pop()
179
213
  return to_subtree(x, removals)
180
214
 
181
- def extra_repr(self):
215
+ def extra_repr(self) -> str:
182
216
  return f"threshold={self.thre}"
183
217
 
184
218
  def _leave(
185
- self, n: Tree.Node, children: List[Tuple[float, Tree.Node] | None]
186
- ) -> Tuple[float, Tree.Node] | None:
219
+ self, n: Tree.Node, children: list[tuple[float, Tree.Node] | None]
220
+ ) -> tuple[float, Tree.Node] | None:
187
221
  if len(children) == 0: # tip
188
222
  return 0, n
189
223
 
@@ -210,3 +244,27 @@ class CutShortTipBranch(Transform[Tree, Tree]):
210
244
  cb(br)
211
245
 
212
246
  return None
247
+
248
+
249
+ class Resampler(Transform[Tree, Tree]):
250
+ def __init__(self, branch_resampler: Transform[Branch, Branch]) -> None:
251
+ super().__init__()
252
+ self.resampler = branch_resampler
253
+ self.assembler = BranchTreeAssembler()
254
+
255
+ def __call__(self, x: Tree) -> Tree:
256
+ t = BranchTree.from_tree(x)
257
+ t.branches = {
258
+ k: [self.resampler(br) for br in brs] for k, brs in t.branches.items()
259
+ }
260
+ return self.assembler(t)
261
+
262
+
263
+ class IsometricResampler(Resampler):
264
+ def __init__(
265
+ self, distance: float, *, adjust_last_gap: bool = True, **kwargs
266
+ ) -> None:
267
+ branch_resampler = BranchIsometricResampler(
268
+ distance, adjust_last_gap=adjust_last_gap, **kwargs
269
+ )
270
+ super().__init__(branch_resampler)
@@ -1,7 +1,23 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Assemble a tree."""
2
17
 
18
+ from collections.abc import Iterable
3
19
  from copy import copy
4
- from typing import Iterable, List, Optional, Tuple
20
+ from typing import Optional
5
21
 
6
22
  import numpy as np
7
23
  import pandas as pd
@@ -18,7 +34,7 @@ from swcgeom.transforms.base import Transform
18
34
  EPS = 1e-5
19
35
 
20
36
 
21
- class LinesToTree(Transform[List[pd.DataFrame], Tree]):
37
+ class LinesToTree(Transform[list[pd.DataFrame], Tree]):
22
38
  """Assemble lines to swc."""
23
39
 
24
40
  def __init__(self, *, thre: float = 0.2, undirected: bool = True):
@@ -37,7 +53,7 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
37
53
 
38
54
  def __call__(
39
55
  self, lines: Iterable[pd.DataFrame], *, names: Optional[SWCNames] = None
40
- ):
56
+ ): # TODO check this
41
57
  return self.assemble(lines, names=names)
42
58
 
43
59
  def assemble(
@@ -55,8 +71,8 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
55
71
  Parameters
56
72
  ----------
57
73
  lines : List of ~pd.DataFrame
58
- An array of tables containing a line, columns should follwing
59
- the swc.
74
+ An array of tables containing a line, columns should
75
+ following the swc.
60
76
  undirected : bool, default `True`
61
77
  Forwarding to `self.try_assemble`.
62
78
  names : SWCNames, optional
@@ -97,7 +113,7 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
97
113
  undirected: bool = True,
98
114
  sort_nodes: bool = True,
99
115
  names: Optional[SWCNames] = None,
100
- ) -> Tuple[pd.DataFrame, List[pd.DataFrame]]:
116
+ ) -> tuple[pd.DataFrame, list[pd.DataFrame]]:
101
117
  """Trying assemble lines to a tree.
102
118
 
103
119
  Treat the first line as a tree, find a line whose shortest distance
@@ -169,5 +185,5 @@ class LinesToTree(Transform[List[pd.DataFrame], Tree]):
169
185
 
170
186
  return tree, lines
171
187
 
172
- def extra_repr(self):
188
+ def extra_repr(self) -> str:
173
189
  return f"thre={self.thre}, undirected={self.undirected}"
swcgeom/utils/__init__.py CHANGED
@@ -1,13 +1,29 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Utils."""
2
17
 
3
- from swcgeom.utils.debug import *
4
- from swcgeom.utils.dsu import *
5
- from swcgeom.utils.ellipse import *
6
- from swcgeom.utils.file import *
7
- from swcgeom.utils.neuromorpho import *
8
- from swcgeom.utils.numpy_helper import *
9
- from swcgeom.utils.renderer import *
10
- from swcgeom.utils.sdf import *
11
- from swcgeom.utils.solid_geometry import *
12
- from swcgeom.utils.transforms import *
13
- from swcgeom.utils.volumetric_object import *
18
+ from swcgeom.utils.debug import * # noqa: F403
19
+ from swcgeom.utils.dsu import * # noqa: F403
20
+ from swcgeom.utils.ellipse import * # noqa: F403
21
+ from swcgeom.utils.file import * # noqa: F403
22
+ from swcgeom.utils.neuromorpho import * # noqa: F403
23
+ from swcgeom.utils.numpy_helper import * # noqa: F403
24
+ from swcgeom.utils.plotter_2d import * # noqa: F403
25
+ from swcgeom.utils.renderer import * # noqa: F403
26
+ from swcgeom.utils.sdf import * # noqa: F403
27
+ from swcgeom.utils.solid_geometry import * # noqa: F403
28
+ from swcgeom.utils.transforms import * # noqa: F403
29
+ from swcgeom.utils.volumetric_object import * # noqa: F403
swcgeom/utils/debug.py CHANGED
@@ -1,3 +1,18 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Debug helpers"""
2
17
 
3
18
  import time
swcgeom/utils/download.py CHANGED
@@ -1,3 +1,18 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Download helpers.
2
17
 
3
18
  Notes
@@ -13,6 +28,7 @@ import itertools
13
28
  import logging
14
29
  import multiprocessing
15
30
  import os
31
+ from functools import partial
16
32
  from urllib.parse import urljoin
17
33
 
18
34
  __all__ = ["download", "fetch_page", "clone_index_page"]
@@ -26,7 +42,7 @@ def download(dst: str, url: str) -> None:
26
42
  r = conn.request("GET", url)
27
43
 
28
44
  dirname = os.path.dirname(dst)
29
- if not os.path.exists(dirname):
45
+ if dirname != "" and not os.path.exists(dirname):
30
46
  os.makedirs(dirname)
31
47
 
32
48
  with open(dst, "wb") as file:
@@ -41,7 +57,7 @@ def fetch_page(url: str):
41
57
  conn = connection_from_url(url)
42
58
  r = conn.request("GET", url)
43
59
  data = r.data.decode("utf-8")
44
- return BeautifulSoup(data)
60
+ return BeautifulSoup(data, features="html.parser")
45
61
 
46
62
 
47
63
  def clone_index_page(
@@ -62,31 +78,35 @@ def clone_index_page(
62
78
  multiprocess : int, default `4`
63
79
  How many process are available for download.
64
80
  """
65
- from urllib3.exceptions import HTTPError
66
-
67
81
  files = get_urls_in_index_page(index_url)
68
82
  logging.info("downloader: search `%s`, found %s files.", index_url, len(files))
69
83
 
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
84
+ task = partial(
85
+ _clone_index_page, index_url=index_url, dist_dir=dist_dir, override=override
86
+ )
87
+ with multiprocessing.Pool(multiprocess) as p:
88
+ p.map(task, files)
77
89
 
78
- logging.info("downloader: file `%s` exits, deleted.", dist)
79
- os.remove(filepath)
80
90
 
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)
91
+ def _clone_index_page(url: str, index_url: str, dist_dir: str, override: bool) -> None:
92
+ from urllib3.exceptions import HTTPError
87
93
 
88
- with multiprocessing.Pool(multiprocess) as p:
89
- p.map(task, files)
94
+ filepath = url.removeprefix(index_url)
95
+ dist = os.path.join(dist_dir, filepath)
96
+ if os.path.exists(dist):
97
+ if not override:
98
+ logging.info("downloader: file `%s` exits, skiped.", dist)
99
+ return
100
+
101
+ logging.info("downloader: file `%s` exits, deleted.", dist)
102
+ os.remove(dist)
103
+
104
+ try:
105
+ logging.info("downloader: downloading `%s` to `%s`", url, dist)
106
+ download(dist, url)
107
+ logging.info("downloader: download `%s` to `%s`", url, dist)
108
+ except HTTPError as ex:
109
+ logging.info("downloader: fails to download `%s`, except `%s`", url, ex)
90
110
 
91
111
 
92
112
  def get_urls_in_index_page(url: str) -> list[str]:
@@ -97,3 +117,21 @@ def get_urls_in_index_page(url: str) -> list[str]:
97
117
  dirs = [urljoin(url, a) for a in links if a != "../" and a.endswith("/")]
98
118
  files.extend(itertools.chain(*[get_urls_in_index_page(dir) for dir in dirs]))
99
119
  return files
120
+
121
+
122
+ if __name__ == "__main__":
123
+ import argparse
124
+
125
+ parser = argparse.ArgumentParser(description="Download files from index page.")
126
+ parser.add_argument("url", type=str, help="URL of index page.")
127
+ parser.add_argument("dist", type=str, help="Directory of dist.")
128
+ parser.add_argument(
129
+ "--override", type=bool, default=False, help="Override existing file."
130
+ )
131
+ parser.add_argument(
132
+ "--multiprocess", type=int, default=4, help="How many process are available."
133
+ )
134
+ args = parser.parse_args()
135
+
136
+ logging.basicConfig(level=logging.INFO)
137
+ clone_index_page(args.url, args.dist, args.override, args.multiprocess)
swcgeom/utils/dsu.py CHANGED
@@ -1,3 +1,18 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Disjoint Set Union Impl."""
2
17
 
3
18
  __all__ = ["DisjointSetUnion"]
swcgeom/utils/ellipse.py CHANGED
@@ -1,9 +1,23 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """Finds the Minimum Volume Enclosing Ellipsoid."""
2
17
 
3
18
  # pylint: disable=invalid-name
4
19
 
5
20
  from functools import cached_property
6
- from typing import Tuple
7
21
 
8
22
  import numpy as np
9
23
  import numpy.linalg as la
@@ -22,7 +36,7 @@ class Ellipse:
22
36
  self.centroid = centroid
23
37
 
24
38
  @property
25
- def radii(self) -> Tuple[float, float]:
39
+ def radii(self) -> tuple[float, float]:
26
40
  # x, y radii.
27
41
  _U, D, _V = self.svd
28
42
  rx, ry = 1.0 / np.sqrt(D)
@@ -39,7 +53,7 @@ class Ellipse:
39
53
  return b
40
54
 
41
55
  @property
42
- def axes(self) -> Tuple[float, float]:
56
+ def axes(self) -> tuple[float, float]:
43
57
  # Major and minor semi-axis of the ellipse.
44
58
  rx, ry = self.radii
45
59
  dx, dy = 2 * rx, 2 * ry
@@ -77,7 +91,7 @@ def mvee(points: npt.NDArray[np.floating], tol: float = 1e-3) -> Ellipse:
77
91
 
78
92
  def _mvee(
79
93
  points: npt.NDArray[np.floating], tol: float = 1e-3
80
- ) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
94
+ ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
81
95
  """Finds the Minimum Volume Enclosing Ellipsoid.
82
96
 
83
97
  Returns
swcgeom/utils/file.py CHANGED
@@ -1,3 +1,18 @@
1
+ # Copyright 2022-2025 Zexin Yuan
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+
1
16
  """File related utils.
2
17
 
3
18
  Notes