swcgeom 0.17.1__py3-none-any.whl → 0.18.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 (69) hide show
  1. swcgeom/__init__.py +14 -0
  2. swcgeom/_version.py +2 -2
  3. swcgeom/analysis/__init__.py +15 -0
  4. swcgeom/analysis/feature_extractor.py +27 -3
  5. swcgeom/analysis/features.py +31 -4
  6. swcgeom/analysis/lmeasure.py +43 -7
  7. swcgeom/analysis/sholl.py +21 -24
  8. swcgeom/analysis/trunk.py +15 -0
  9. swcgeom/analysis/visualization.py +15 -0
  10. swcgeom/analysis/visualization3d.py +15 -0
  11. swcgeom/analysis/volume.py +15 -0
  12. swcgeom/core/__init__.py +15 -0
  13. swcgeom/core/branch.py +15 -0
  14. swcgeom/core/branch_tree.py +15 -0
  15. swcgeom/core/compartment.py +15 -0
  16. swcgeom/core/node.py +30 -1
  17. swcgeom/core/path.py +18 -7
  18. swcgeom/core/population.py +43 -3
  19. swcgeom/core/swc.py +15 -0
  20. swcgeom/core/swc_utils/__init__.py +15 -1
  21. swcgeom/core/swc_utils/assembler.py +15 -0
  22. swcgeom/core/swc_utils/base.py +15 -0
  23. swcgeom/core/swc_utils/checker.py +19 -12
  24. swcgeom/core/swc_utils/io.py +17 -1
  25. swcgeom/core/swc_utils/normalizer.py +16 -1
  26. swcgeom/core/swc_utils/subtree.py +15 -0
  27. swcgeom/core/tree.py +37 -9
  28. swcgeom/core/tree_utils.py +17 -7
  29. swcgeom/core/tree_utils_impl.py +15 -0
  30. swcgeom/images/__init__.py +15 -0
  31. swcgeom/images/augmentation.py +15 -0
  32. swcgeom/images/contrast.py +15 -0
  33. swcgeom/images/folder.py +17 -10
  34. swcgeom/images/io.py +18 -6
  35. swcgeom/transforms/__init__.py +16 -0
  36. swcgeom/transforms/base.py +17 -2
  37. swcgeom/transforms/branch.py +74 -8
  38. swcgeom/transforms/branch_tree.py +82 -0
  39. swcgeom/transforms/geometry.py +22 -7
  40. swcgeom/transforms/image_preprocess.py +15 -0
  41. swcgeom/transforms/image_stack.py +30 -4
  42. swcgeom/transforms/images.py +17 -10
  43. swcgeom/transforms/mst.py +15 -0
  44. swcgeom/transforms/neurolucida_asc.py +16 -1
  45. swcgeom/transforms/path.py +15 -0
  46. swcgeom/transforms/population.py +15 -0
  47. swcgeom/transforms/tree.py +76 -23
  48. swcgeom/transforms/tree_assembler.py +19 -4
  49. swcgeom/utils/__init__.py +15 -0
  50. swcgeom/utils/debug.py +15 -0
  51. swcgeom/utils/download.py +59 -21
  52. swcgeom/utils/dsu.py +15 -0
  53. swcgeom/utils/ellipse.py +15 -0
  54. swcgeom/utils/file.py +15 -0
  55. swcgeom/utils/neuromorpho.py +18 -7
  56. swcgeom/utils/numpy_helper.py +15 -0
  57. swcgeom/utils/plotter_2d.py +15 -0
  58. swcgeom/utils/plotter_3d.py +18 -1
  59. swcgeom/utils/renderer.py +15 -0
  60. swcgeom/utils/sdf.py +17 -5
  61. swcgeom/utils/solid_geometry.py +15 -0
  62. swcgeom/utils/transforms.py +16 -1
  63. swcgeom/utils/volumetric_object.py +15 -0
  64. {swcgeom-0.17.1.dist-info → swcgeom-0.18.1.dist-info}/LICENSE +1 -1
  65. {swcgeom-0.17.1.dist-info → swcgeom-0.18.1.dist-info}/METADATA +26 -22
  66. swcgeom-0.18.1.dist-info/RECORD +68 -0
  67. {swcgeom-0.17.1.dist-info → swcgeom-0.18.1.dist-info}/WHEEL +1 -1
  68. swcgeom-0.17.1.dist-info/RECORD +0 -67
  69. {swcgeom-0.17.1.dist-info → swcgeom-0.18.1.dist-info}/top_level.txt +0 -0
@@ -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
  """Transformation in branch."""
2
17
 
3
18
  from abc import ABC, abstractmethod
@@ -72,10 +87,61 @@ class BranchLinearResampler(_BranchResampler):
72
87
  r = np.interp(xvals, xp, xyzr[:, 3])
73
88
  return cast(npt.NDArray[np.float32], np.stack([x, y, z, r], axis=1))
74
89
 
75
- def extra_repr(self):
90
+ def extra_repr(self) -> str:
76
91
  return f"n_nodes={self.n_nodes}"
77
92
 
78
93
 
94
+ class BranchIsometricResampler(_BranchResampler):
95
+ def __init__(self, distance: float, *, adjust_last_gap: bool = True) -> None:
96
+ super().__init__()
97
+ self.distance = distance
98
+ self.adjust_last_gap = adjust_last_gap
99
+
100
+ def resample(self, xyzr: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
101
+ """Resampling by isometric interpolation, DO NOT keep original node.
102
+
103
+ Parameters
104
+ ----------
105
+ xyzr : np.ndarray[np.float32]
106
+ The array of shape (N, 4).
107
+
108
+ Returns
109
+ -------
110
+ new_xyzr : ~numpy.NDArray[float32]
111
+ An array of shape (n_nodes, 4).
112
+ """
113
+
114
+ # Compute the cumulative distances between consecutive points
115
+ diffs = np.diff(xyzr[:, :3], axis=0)
116
+ distances = np.sqrt((diffs**2).sum(axis=1))
117
+ cumulative_distances = np.concatenate([[0], np.cumsum(distances)])
118
+
119
+ total_length = cumulative_distances[-1]
120
+ n_nodes = int(np.ceil(total_length / self.distance)) + 1
121
+
122
+ # Determine the new distances
123
+ if self.adjust_last_gap and n_nodes > 1:
124
+ new_distances = np.linspace(0, total_length, n_nodes)
125
+ else:
126
+ new_distances = np.arange(0, total_length, self.distance)
127
+ # keep endpoint
128
+ new_distances = np.concatenate([new_distances, total_length])
129
+
130
+ # Interpolate the new points
131
+ new_xyzr = np.zeros((n_nodes, 4), dtype=np.float32)
132
+ new_xyzr[:, :3] = np.array(
133
+ [
134
+ np.interp(new_distances, cumulative_distances, xyzr[:, i])
135
+ for i in range(3)
136
+ ]
137
+ ).T
138
+ new_xyzr[:, 3] = np.interp(new_distances, cumulative_distances, xyzr[:, 3])
139
+ return new_xyzr
140
+
141
+ def extra_repr(self) -> str:
142
+ return f"distance={self.distance},adjust_last_gap={self.adjust_last_gap}"
143
+
144
+
79
145
  class BranchConvSmoother(Transform[Branch, Branch[DictSWC]]):
80
146
  r"""Smooth the branch by sliding window."""
81
147
 
@@ -88,24 +154,24 @@ class BranchConvSmoother(Transform[Branch, Branch[DictSWC]]):
88
154
  """
89
155
  super().__init__()
90
156
  self.n_nodes = n_nodes
91
- self.kernal = np.ones(n_nodes)
157
+ self.kernel = np.ones(n_nodes)
92
158
 
93
159
  def __call__(self, x: Branch) -> Branch[DictSWC]:
94
160
  x = x.detach()
95
- c = signal.convolve(np.ones(x.number_of_nodes()), self.kernal, mode="same")
161
+ c = signal.convolve(np.ones(x.number_of_nodes()), self.kernel, mode="same")
96
162
  for k in ["x", "y", "z"]:
97
163
  v = x.get_ndata(k)
98
- s = signal.convolve(v, self.kernal, mode="same")
164
+ s = signal.convolve(v, self.kernel, mode="same")
99
165
  x.attach.ndata[k][1:-1] = (s / c)[1:-1]
100
166
 
101
167
  return x
102
168
 
103
- def extra_repr(self):
169
+ def extra_repr(self) -> str:
104
170
  return f"n_nodes={self.n_nodes}"
105
171
 
106
172
 
107
173
  class BranchStandardizer(Transform[Branch, Branch[DictSWC]]):
108
- r"""Standarize branch.
174
+ r"""Standardize branch.
109
175
 
110
176
  Standardized branch starts at (0, 0, 0), ends at (1, 0, 0), up at
111
177
  y, and scale max radius to 1.
@@ -123,7 +189,7 @@ class BranchStandardizer(Transform[Branch, Branch[DictSWC]]):
123
189
 
124
190
  @staticmethod
125
191
  def get_matrix(xyz: npt.NDArray[np.float32]) -> npt.NDArray[np.float32]:
126
- r"""Get standarize transformation matrix.
192
+ r"""Get standardize transformation matrix.
127
193
 
128
194
  Standardized branch starts at (0, 0, 0), ends at (1, 0, 0), up
129
195
  at y.
@@ -136,7 +202,7 @@ class BranchStandardizer(Transform[Branch, Branch[DictSWC]]):
136
202
  Returns
137
203
  -------
138
204
  T : np.ndarray[np.float32]
139
- An homogeneous transfomation matrix of shape (4, 4).
205
+ An homogeneous transformation matrix of shape (4, 4).
140
206
  """
141
207
 
142
208
  assert (
@@ -0,0 +1,82 @@
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
+
16
+ from typing import Iterable
17
+
18
+ import numpy as np
19
+
20
+ from swcgeom.core import Branch, BranchTree, Node, Tree
21
+ from swcgeom.transforms.base import Transform
22
+
23
+ __all__ = ["BranchTreeAssembler"]
24
+
25
+
26
+ class BranchTreeAssembler(Transform[BranchTree, Tree]):
27
+ EPS = 1e-6
28
+
29
+ def __call__(self, x: BranchTree) -> Tree:
30
+ nodes = [x.soma().detach()]
31
+ stack = [(x.soma(), 0)] # n_orig, id_new
32
+ while len(stack):
33
+ n_orig, pid_new = stack.pop()
34
+ children = n_orig.children()
35
+
36
+ for br, c in self.pair(x.branches.get(n_orig.id, []), children):
37
+ s = 1 if np.linalg.norm(br[0].xyz() - n_orig.xyz()) < self.EPS else 0
38
+ e = -2 if np.linalg.norm(br[-1].xyz() - c.xyz()) < self.EPS else -1
39
+
40
+ br_nodes = [n.detach() for n in br[s:e]] + [c.detach()]
41
+ for i, n in enumerate(br_nodes):
42
+ # reindex
43
+ n.id = len(nodes) + i
44
+ n.pid = len(nodes) + i - 1
45
+
46
+ br_nodes[0].pid = pid_new
47
+ nodes.extend(br_nodes)
48
+ stack.append((c, br_nodes[-1].id))
49
+
50
+ return Tree(
51
+ len(nodes),
52
+ source=x.source,
53
+ comments=x.comments,
54
+ names=x.names,
55
+ **{
56
+ k: np.array([n.__getattribute__(k) for n in nodes])
57
+ for k in x.names.cols()
58
+ },
59
+ )
60
+
61
+ def pair(
62
+ self, branches: list[Branch], endpoints: list[Node]
63
+ ) -> Iterable[tuple[Branch, Node]]:
64
+ assert len(branches) == len(endpoints)
65
+ xyz1 = [br[-1].xyz() for br in branches]
66
+ xyz2 = [n.xyz() for n in endpoints]
67
+ v = np.reshape(xyz1, (-1, 1, 3)) - np.reshape(xyz2, (1, -1, 3))
68
+ dis = np.linalg.norm(v, axis=-1)
69
+
70
+ # greedy algorithm
71
+ pairs = []
72
+ for _ in range(len(branches)):
73
+ # find minimal
74
+ min_idx = np.argmin(dis)
75
+ min_branch_idx, min_endpoint_idx = np.unravel_index(min_idx, dis.shape)
76
+ pairs.append((branches[min_branch_idx], endpoints[min_endpoint_idx]))
77
+
78
+ # remove current node
79
+ dis[min_branch_idx, :] = np.inf
80
+ dis[:, min_endpoint_idx] = np.inf
81
+
82
+ return pairs
@@ -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
  """SWC geometry operations."""
2
17
 
3
18
  import warnings
@@ -72,7 +87,7 @@ class RadiusReseter(Generic[T], Transform[T, T]):
72
87
  new_tree.ndata[new_tree.names.r] = r
73
88
  return new_tree
74
89
 
75
- def extra_repr(self):
90
+ def extra_repr(self) -> str:
76
91
  return f"r={self.r:.4f}"
77
92
 
78
93
 
@@ -141,7 +156,7 @@ class Translate(Generic[T], AffineTransform[T]):
141
156
  super().__init__(translate3d(tx, ty, tz), **kwargs)
142
157
  self.tx, self.ty, self.tz = tx, ty, tz
143
158
 
144
- def extra_repr(self):
159
+ def extra_repr(self) -> str:
145
160
  return f"tx={self.tx:.4f}, ty={self.ty:.4f}, tz={self.tz:.4f}"
146
161
 
147
162
  @classmethod
@@ -194,8 +209,8 @@ class Rotate(Generic[T], AffineTransform[T]):
194
209
  self.theta = theta
195
210
  self.center = center
196
211
 
197
- def extra_repr(self):
198
- return f"n={self.n}, theta={self.theta:.4f}, center={self.center}" # TODO: imporve format of n
212
+ def extra_repr(self) -> str:
213
+ return f"n={self.n}, theta={self.theta:.4f}, center={self.center}" # TODO: improve format of n
199
214
 
200
215
  @classmethod
201
216
  def transform(
@@ -216,7 +231,7 @@ class RotateX(Generic[T], AffineTransform[T]):
216
231
  super().__init__(rotate3d_x(theta), center=center, **kwargs)
217
232
  self.theta = theta
218
233
 
219
- def extra_repr(self):
234
+ def extra_repr(self) -> str:
220
235
  return f"center={self.center}, theta={self.theta:.4f}"
221
236
 
222
237
  @classmethod
@@ -232,7 +247,7 @@ class RotateY(Generic[T], AffineTransform[T]):
232
247
  self.theta = theta
233
248
  self.center = center
234
249
 
235
- def extra_repr(self):
250
+ def extra_repr(self) -> str:
236
251
  return f"theta={self.theta:.4f}, center={self.center}"
237
252
 
238
253
  @classmethod
@@ -248,7 +263,7 @@ class RotateZ(Generic[T], AffineTransform[T]):
248
263
  self.theta = theta
249
264
  self.center = center
250
265
 
251
- def extra_repr(self):
266
+ def extra_repr(self) -> str:
252
267
  return f"theta={self.theta:.4f}, center={self.center}"
253
268
 
254
269
  @classmethod
@@ -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
  """Image stack pre-processing."""
2
17
 
3
18
  import numpy as np
@@ -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
  """Create image stack from morphology.
2
17
 
3
18
  Notes
@@ -27,6 +42,7 @@ from sdflit import (
27
42
  SDFObject,
28
43
  )
29
44
  from tqdm import tqdm
45
+ from typing_extensions import deprecated
30
46
 
31
47
  from swcgeom.core import Population, Tree
32
48
  from swcgeom.transforms.base import Transform
@@ -63,9 +79,9 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
63
79
  ONLY works for small image stacks, use :meth`transform_and_save`
64
80
  for big image stack.
65
81
  """
66
- return np.stack(list(self.transfrom(x, verbose=False)), axis=0)
82
+ return np.stack(list(self.transform(x, verbose=False)), axis=0)
67
83
 
68
- def transfrom(
84
+ def transform(
69
85
  self,
70
86
  x: Tree,
71
87
  verbose: bool = True,
@@ -102,10 +118,20 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
102
118
  frame = (255 * voxel[..., 0, 0]).astype(np.uint8)
103
119
  yield frame
104
120
 
121
+ @deprecated("Use transform instead")
122
+ def transfrom(
123
+ self,
124
+ x: Tree,
125
+ verbose: bool = True,
126
+ *,
127
+ ranges: Optional[tuple[npt.ArrayLike, npt.ArrayLike]] = None,
128
+ ) -> Iterable[npt.NDArray[np.uint8]]:
129
+ return self.transform(x, verbose, ranges=ranges)
130
+
105
131
  def transform_and_save(
106
132
  self, fname: str, x: Tree, verbose: bool = True, **kwargs
107
133
  ) -> None:
108
- self.save_tif(fname, self.transfrom(x, verbose=verbose, **kwargs))
134
+ self.save_tif(fname, self.transform(x, verbose=verbose, **kwargs))
109
135
 
110
136
  def transform_population(
111
137
  self, population: Population | str, verbose: bool = True
@@ -125,7 +151,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
125
151
  if not os.path.isfile(tif):
126
152
  self.transform_and_save(tif, tree, verbose=False)
127
153
 
128
- def extra_repr(self):
154
+ def extra_repr(self) -> str:
129
155
  res = ",".join(f"{a:.4f}" for a in self.resolution)
130
156
  return f"resolution=({res})"
131
157
 
@@ -1,9 +1,23 @@
1
- """Image stack related transform."""
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
+
2
15
 
3
- import warnings
16
+ """Image stack related transform."""
4
17
 
5
18
  import numpy as np
6
19
  import numpy.typing as npt
20
+ from typing_extensions import deprecated
7
21
 
8
22
  from swcgeom.transforms.base import Identity, Transform
9
23
 
@@ -45,6 +59,7 @@ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
45
59
  return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
46
60
 
47
61
 
62
+ @deprecated("use `ImagesCenterCrop` instead", stacklevel=2)
48
63
  class Center(ImagesCenterCrop):
49
64
  """Get image stack center.
50
65
 
@@ -52,14 +67,6 @@ class Center(ImagesCenterCrop):
52
67
  Use :class:`ImagesCenterCrop` instead.
53
68
  """
54
69
 
55
- def __init__(self, shape_out: int | tuple[int, int, int]):
56
- warnings.warn(
57
- "`Center` is deprecated, use `ImagesCenterCrop` instead",
58
- DeprecationWarning,
59
- stacklevel=2,
60
- )
61
- super().__init__(shape_out)
62
-
63
70
 
64
71
  class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
65
72
  def __init__(self, scaler: float) -> None:
swcgeom/transforms/mst.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
  """Minimum spanning tree."""
2
17
 
3
18
  import warnings
@@ -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
  """Neurolucida related transformation."""
2
17
 
3
18
  import os
@@ -421,7 +436,7 @@ class Lexer:
421
436
  return self
422
437
 
423
438
  def __next__(self) -> Token:
424
- match (word := self._read_word()):
439
+ match word := self._read_word():
425
440
  case "":
426
441
  raise StopIteration
427
442
 
@@ -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
  """Transformation in path."""
2
17
 
3
18
  from swcgeom.core import Path, Tree, redirect_tree
@@ -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
  """Transformation in population."""
2
17
 
3
18
  from swcgeom.core import Population, Tree
@@ -1,15 +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
18
  from collections.abc import Callable
5
19
  from typing import Optional
6
20
 
7
21
  import numpy as np
22
+ from typing_extensions import deprecated
8
23
 
9
- 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
10
25
  from swcgeom.core.swc_utils import SWCTypes, get_types
11
26
  from swcgeom.transforms.base import Transform
12
- from swcgeom.transforms.branch import BranchConvSmoother
27
+ from swcgeom.transforms.branch import BranchConvSmoother, BranchIsometricResampler
28
+ from swcgeom.transforms.branch_tree import BranchTreeAssembler
13
29
  from swcgeom.transforms.geometry import Normalizer
14
30
 
15
31
  __all__ = [
@@ -20,8 +36,9 @@ __all__ = [
20
36
  "CutByType",
21
37
  "CutAxonTree",
22
38
  "CutDendriteTree",
23
- "CutByBifurcationOrder",
39
+ "CutByFurcationOrder",
24
40
  "CutShortTipBranch",
41
+ "IsometricResampler",
25
42
  ]
26
43
 
27
44
 
@@ -65,10 +82,11 @@ class TreeSmoother(Transform[Tree, Tree]): # pylint: disable=missing-class-docs
65
82
 
66
83
  return x
67
84
 
68
- def extra_repr(self):
85
+ def extra_repr(self) -> str:
69
86
  return f"n_nodes={self.n_nodes}"
70
87
 
71
88
 
89
+ @deprecated("Use `Normalizer` instead")
72
90
  class TreeNormalizer(Normalizer[Tree]):
73
91
  """Noramlize coordinates and radius to 0-1.
74
92
 
@@ -76,15 +94,6 @@ class TreeNormalizer(Normalizer[Tree]):
76
94
  Use :cls:`Normalizer` instead.
77
95
  """
78
96
 
79
- def __init__(self, *args, **kwargs) -> None:
80
- warnings.warn(
81
- "`TreeNormalizer` has been replaced by `Normalizer` since "
82
- "v0.6.0 beacuse it applies more widely, and this will be "
83
- "removed in next version",
84
- DeprecationWarning,
85
- )
86
- super().__init__(*args, **kwargs)
87
-
88
97
 
89
98
  class CutByType(Transform[Tree, Tree]):
90
99
  """Cut tree by type.
@@ -112,7 +121,7 @@ class CutByType(Transform[Tree, Tree]):
112
121
  y = to_subtree(x, removals)
113
122
  return y
114
123
 
115
- def extra_repr(self):
124
+ def extra_repr(self) -> str:
116
125
  return f"type={self.type}"
117
126
 
118
127
 
@@ -132,28 +141,48 @@ class CutDendriteTree(CutByType):
132
141
  super().__init__(type=types.basal_dendrite) # TODO: apical dendrite
133
142
 
134
143
 
135
- class CutByBifurcationOrder(Transform[Tree, Tree]):
136
- """Cut tree by bifurcation order."""
144
+ class CutByFurcationOrder(Transform[Tree, Tree]):
145
+ """Cut tree by furcation order."""
137
146
 
138
- max_bifurcation_order: int
147
+ max_furcation_order: int
139
148
 
140
149
  def __init__(self, max_bifurcation_order: int) -> None:
141
- self.max_bifurcation_order = max_bifurcation_order
150
+ self.max_furcation_order = max_bifurcation_order
142
151
 
143
152
  def __call__(self, x: Tree) -> Tree:
144
153
  return cut_tree(x, enter=self._enter)
145
154
 
146
155
  def __repr__(self) -> str:
147
- return f"CutByBifurcationOrder-{self.max_bifurcation_order}"
156
+ return f"CutByBifurcationOrder-{self.max_furcation_order}"
148
157
 
149
158
  def _enter(self, n: Tree.Node, parent_level: int | None) -> tuple[int, bool]:
150
159
  if parent_level is None:
151
160
  level = 0
152
- elif n.is_bifurcation():
161
+ elif n.is_furcation():
153
162
  level = parent_level + 1
154
163
  else:
155
164
  level = parent_level
156
- 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}"
157
186
 
158
187
 
159
188
  class CutShortTipBranch(Transform[Tree, Tree]):
@@ -183,7 +212,7 @@ class CutShortTipBranch(Transform[Tree, Tree]):
183
212
  self.callbacks.pop()
184
213
  return to_subtree(x, removals)
185
214
 
186
- def extra_repr(self):
215
+ def extra_repr(self) -> str:
187
216
  return f"threshold={self.thre}"
188
217
 
189
218
  def _leave(
@@ -215,3 +244,27 @@ class CutShortTipBranch(Transform[Tree, Tree]):
215
244
  cb(br)
216
245
 
217
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)