swcgeom 0.16.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 +48 -12
  6. swcgeom/analysis/sholl.py +25 -28
  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 +31 -12
  12. swcgeom/core/branch.py +19 -3
  13. swcgeom/core/branch_tree.py +18 -4
  14. swcgeom/core/compartment.py +18 -2
  15. swcgeom/core/node.py +32 -3
  16. swcgeom/core/path.py +21 -9
  17. swcgeom/core/population.py +58 -29
  18. swcgeom/core/swc.py +26 -10
  19. swcgeom/core/swc_utils/__init__.py +21 -7
  20. swcgeom/core/swc_utils/assembler.py +15 -0
  21. swcgeom/core/swc_utils/base.py +23 -17
  22. swcgeom/core/swc_utils/checker.py +19 -12
  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 +56 -40
  27. swcgeom/core/tree_utils.py +28 -17
  28. swcgeom/core/tree_utils_impl.py +18 -3
  29. swcgeom/images/__init__.py +17 -2
  30. swcgeom/images/augmentation.py +18 -3
  31. swcgeom/images/contrast.py +15 -0
  32. swcgeom/images/folder.py +27 -26
  33. swcgeom/images/io.py +94 -117
  34. swcgeom/transforms/__init__.py +28 -12
  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 +15 -0
  40. swcgeom/transforms/image_stack.py +36 -9
  41. swcgeom/transforms/images.py +121 -14
  42. swcgeom/transforms/mst.py +15 -0
  43. swcgeom/transforms/neurolucida_asc.py +20 -7
  44. swcgeom/transforms/path.py +15 -0
  45. swcgeom/transforms/population.py +16 -3
  46. swcgeom/transforms/tree.py +84 -30
  47. swcgeom/transforms/tree_assembler.py +23 -7
  48. swcgeom/utils/__init__.py +27 -12
  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 +35 -23
  55. swcgeom/utils/numpy_helper.py +15 -0
  56. swcgeom/utils/plotter_2d.py +27 -6
  57. swcgeom/utils/plotter_3d.py +48 -0
  58. swcgeom/utils/renderer.py +21 -6
  59. swcgeom/utils/sdf.py +19 -7
  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.16.0.dist-info → swcgeom-0.18.3.dist-info}/LICENSE +1 -1
  64. {swcgeom-0.16.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.16.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.16.0.dist-info/RECORD +0 -67
  72. {swcgeom-0.16.0.dist-info → swcgeom-0.18.3.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
  """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
@@ -12,7 +27,8 @@ pip install swcgeom[all]
12
27
  import os
13
28
  import re
14
29
  import time
15
- from typing import Iterable, List, Optional, Tuple
30
+ from collections.abc import Iterable
31
+ from typing import Optional
16
32
 
17
33
  import numpy as np
18
34
  import numpy.typing as npt
@@ -26,6 +42,7 @@ from sdflit import (
26
42
  SDFObject,
27
43
  )
28
44
  from tqdm import tqdm
45
+ from typing_extensions import deprecated
29
46
 
30
47
  from swcgeom.core import Population, Tree
31
48
  from swcgeom.transforms.base import Transform
@@ -62,14 +79,14 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
62
79
  ONLY works for small image stacks, use :meth`transform_and_save`
63
80
  for big image stack.
64
81
  """
65
- return np.stack(list(self.transfrom(x, verbose=False)), axis=0)
82
+ return np.stack(list(self.transform(x, verbose=False)), axis=0)
66
83
 
67
- def transfrom(
84
+ def transform(
68
85
  self,
69
86
  x: Tree,
70
87
  verbose: bool = True,
71
88
  *,
72
- ranges: Optional[Tuple[npt.ArrayLike, npt.ArrayLike]] = None,
89
+ ranges: Optional[tuple[npt.ArrayLike, npt.ArrayLike]] = None,
73
90
  ) -> Iterable[npt.NDArray[np.uint8]]:
74
91
  if verbose:
75
92
  print("To image stack: " + x.source)
@@ -101,10 +118,20 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
101
118
  frame = (255 * voxel[..., 0, 0]).astype(np.uint8)
102
119
  yield frame
103
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
+
104
131
  def transform_and_save(
105
132
  self, fname: str, x: Tree, verbose: bool = True, **kwargs
106
133
  ) -> None:
107
- self.save_tif(fname, self.transfrom(x, verbose=verbose, **kwargs))
134
+ self.save_tif(fname, self.transform(x, verbose=verbose, **kwargs))
108
135
 
109
136
  def transform_population(
110
137
  self, population: Population | str, verbose: bool = True
@@ -124,7 +151,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
124
151
  if not os.path.isfile(tif):
125
152
  self.transform_and_save(tif, tree, verbose=False)
126
153
 
127
- def extra_repr(self):
154
+ def extra_repr(self) -> str:
128
155
  res = ",".join(f"{a:.4f}" for a in self.resolution)
129
156
  return f"resolution=({res})"
130
157
 
@@ -133,7 +160,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
133
160
  scene = ObjectsScene()
134
161
  scene.set_background((0, 0, 0))
135
162
 
136
- def leave(n: Tree.Node, children: List[Tree.Node]) -> Tree.Node:
163
+ def leave(n: Tree.Node, children: list[Tree.Node]) -> Tree.Node:
137
164
  for c in children:
138
165
  sdf = RoundCone(_tp3f(n.xyz()), _tp3f(c.xyz()), n.r, c.r).into()
139
166
  scene.add_object(SDFObject(sdf, material).into())
@@ -175,7 +202,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
175
202
  def save_tif(
176
203
  fname: str,
177
204
  frames: Iterable[npt.NDArray[np.uint8]],
178
- resolution: Tuple[float, float] = (1, 1),
205
+ resolution: tuple[float, float] = (1, 1),
179
206
  ) -> None:
180
207
  with tifffile.TiffWriter(fname) as tif:
181
208
  for frame in frames:
@@ -191,7 +218,7 @@ class ToImageStack(Transform[Tree, npt.NDArray[np.uint8]]):
191
218
  )
192
219
 
193
220
 
194
- def _tp3f(x: npt.NDArray) -> Tuple[float, float, float]:
221
+ def _tp3f(x: npt.NDArray) -> tuple[float, float, float]:
195
222
  """Convert to tuple of 3 floats."""
196
223
  assert len(x) == 3
197
224
  return (float(x[0]), float(x[1]), float(x[2]))
@@ -1,19 +1,36 @@
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
4
- from typing import Tuple
16
+ """Image stack related transform."""
5
17
 
6
18
  import numpy as np
7
19
  import numpy.typing as npt
20
+ from typing_extensions import deprecated
8
21
 
9
- from swcgeom.transforms.base import Transform
22
+ from swcgeom.transforms.base import Identity, Transform
10
23
 
11
24
  __all__ = [
12
25
  "ImagesCenterCrop",
13
26
  "ImagesScale",
14
27
  "ImagesClip",
28
+ "ImagesFlip",
29
+ "ImagesFlipY",
15
30
  "ImagesNormalizer",
16
31
  "ImagesMeanVarianceAdjustment",
32
+ "ImagesScaleToUnitRange",
33
+ "ImagesHistogramEqualization",
17
34
  "Center", # legacy
18
35
  ]
19
36
 
@@ -24,7 +41,7 @@ NDArrayf32 = npt.NDArray[np.float32]
24
41
  class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
25
42
  """Get image stack center."""
26
43
 
27
- def __init__(self, shape_out: int | Tuple[int, int, int]):
44
+ def __init__(self, shape_out: int | tuple[int, int, int]):
28
45
  super().__init__()
29
46
  self.shape_out = (
30
47
  shape_out
@@ -42,21 +59,14 @@ class ImagesCenterCrop(Transform[NDArrayf32, NDArrayf32]):
42
59
  return f"shape_out=({','.join(str(a) for a in self.shape_out)})"
43
60
 
44
61
 
62
+ @deprecated("use `ImagesCenterCrop` instead", stacklevel=2)
45
63
  class Center(ImagesCenterCrop):
46
64
  """Get image stack center.
47
65
 
48
- .. deprecated:: 0.5.0
66
+ .. deprecated:: 0.16.0
49
67
  Use :class:`ImagesCenterCrop` instead.
50
68
  """
51
69
 
52
- def __init__(self, shape_out: int | Tuple[int, int, int]):
53
- warnings.warn(
54
- "`Center` is deprecated, use `ImagesCenterCrop` instead",
55
- DeprecationWarning,
56
- stacklevel=2,
57
- )
58
- super().__init__(shape_out)
59
-
60
70
 
61
71
  class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
62
72
  def __init__(self, scaler: float) -> None:
@@ -66,6 +76,9 @@ class ImagesScale(Transform[NDArrayf32, NDArrayf32]):
66
76
  def __call__(self, x: NDArrayf32) -> NDArrayf32:
67
77
  return self.scaler * x
68
78
 
79
+ def extra_repr(self) -> str:
80
+ return f"scaler={self.scaler}"
81
+
69
82
 
70
83
  class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
71
84
  def __init__(self, vmin: float = 0, vmax: float = 1, /) -> None:
@@ -75,6 +88,41 @@ class ImagesClip(Transform[NDArrayf32, NDArrayf32]):
75
88
  def __call__(self, x: NDArrayf32) -> NDArrayf32:
76
89
  return np.clip(x, self.vmin, self.vmax)
77
90
 
91
+ def extra_repr(self) -> str:
92
+ return f"vmin={self.vmin}, vmax={self.vmax}"
93
+
94
+
95
+ class ImagesFlip(Transform[NDArrayf32, NDArrayf32]):
96
+ """Flip image stack along axis."""
97
+
98
+ def __init__(self, axis: int, /) -> None:
99
+ super().__init__()
100
+ self.axis = axis
101
+
102
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
103
+ return np.flip(x, axis=self.axis)
104
+
105
+ def extra_repr(self) -> str:
106
+ return f"axis={self.axis}"
107
+
108
+
109
+ class ImagesFlipY(ImagesFlip):
110
+ """Flip image stack along Y-axis.
111
+
112
+ See Also
113
+ --------
114
+ ~.images.io.TeraflyImageStack:
115
+ Terafly and Vaa3d use a especial right-handed coordinate system
116
+ (with origin point in the left-top and z-axis points front),
117
+ but we flip y-axis to makes it a left-handed coordinate system
118
+ (with orgin point in the left-bottom and z-axis points front).
119
+ If you need to use its coordinate system, remember to FLIP
120
+ Y-AXIS BACK.
121
+ """
122
+
123
+ def __init__(self, axis: int = 1, /) -> None:
124
+ super().__init__(axis) # (X, Y, Z, C)
125
+
78
126
 
79
127
  class ImagesNormalizer(Transform[NDArrayf32, NDArrayf32]):
80
128
  """Normalize image stack."""
@@ -100,3 +148,62 @@ class ImagesMeanVarianceAdjustment(Transform[NDArrayf32, NDArrayf32]):
100
148
 
101
149
  def __call__(self, x: NDArrayf32) -> NDArrayf32:
102
150
  return (x - self.mean) / self.variance
151
+
152
+ def extra_repr(self) -> str:
153
+ return f"mean={self.mean}, variance={self.variance}"
154
+
155
+
156
+ class ImagesScaleToUnitRange(Transform[NDArrayf32, NDArrayf32]):
157
+ """Scale image stack to unit range."""
158
+
159
+ def __init__(self, vmin: float, vmax: float, *, clip: bool = True) -> None:
160
+ """Scale image stack to unit range.
161
+
162
+ Parameters
163
+ ----------
164
+ vmin : float
165
+ Minimum value.
166
+ vmax : float
167
+ Maximum value.
168
+ clip : bool, default True
169
+ Clip values to [0, 1] to avoid numerical issues.
170
+ """
171
+
172
+ super().__init__()
173
+ self.vmin = vmin
174
+ self.vmax = vmax
175
+ self.diff = vmax - vmin
176
+ self.clip = clip
177
+ self.post = ImagesClip(0, 1) if self.clip else Identity()
178
+
179
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
180
+ return self.post((x - self.vmin) / self.diff)
181
+
182
+ def extra_repr(self) -> str:
183
+ return f"vmin={self.vmin}, vmax={self.vmax}, clip={self.clip}"
184
+
185
+
186
+ class ImagesHistogramEqualization(Transform[NDArrayf32, NDArrayf32]):
187
+ """Image histogram equalization.
188
+
189
+ References
190
+ ----------
191
+ http://www.janeriksolem.net/histogram-equalization-with-python-and.html
192
+ """
193
+
194
+ def __init__(self, bins: int = 256) -> None:
195
+ super().__init__()
196
+ self.bins = bins
197
+
198
+ def __call__(self, x: NDArrayf32) -> NDArrayf32:
199
+ # get image histogram
200
+ hist, bin_edges = np.histogram(x.flatten(), self.bins, density=True)
201
+ cdf = hist.cumsum() # cumulative distribution function
202
+ cdf = cdf / cdf[-1] # normalize
203
+
204
+ # use linear interpolation of cdf to find new pixel values
205
+ equalized = np.interp(x.flatten(), bin_edges[:-1], cdf)
206
+ return equalized.reshape(x.shape).astype(np.float32)
207
+
208
+ def extra_repr(self) -> str:
209
+ return f"bins={self.bins}"
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,12 +1,25 @@
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
4
19
  import re
5
20
  from enum import Enum, auto
6
21
  from io import TextIOBase
7
- from typing import Any, List, NamedTuple, Optional, cast
8
-
9
- import numpy as np
22
+ from typing import Any, NamedTuple, Optional, cast
10
23
 
11
24
  from swcgeom.core import Tree
12
25
  from swcgeom.core.swc_utils import SWCNames, SWCTypes, get_names, get_types
@@ -116,8 +129,8 @@ class ASTNode:
116
129
  self,
117
130
  type: ASTType,
118
131
  value: Any = None,
119
- tokens: Optional[List["Token"]] = None,
120
- children: Optional[List["ASTNode"]] = None,
132
+ tokens: Optional[list["Token"]] = None,
133
+ children: Optional[list["ASTNode"]] = None,
121
134
  ):
122
135
  self.type = type
123
136
  self.value = value
@@ -149,7 +162,7 @@ class ASTNode:
149
162
 
150
163
 
151
164
  class AST(ASTNode):
152
- def __init__(self, children: Optional[List[ASTNode]] = None, source: str = ""):
165
+ def __init__(self, children: Optional[list[ASTNode]] = None, source: str = ""):
153
166
  super().__init__(ASTType.ROOT, children=children)
154
167
  self.source = source
155
168
 
@@ -423,7 +436,7 @@ class Lexer:
423
436
  return self
424
437
 
425
438
  def __next__(self) -> Token:
426
- match (word := self._read_word()):
439
+ match word := self._read_word():
427
440
  case "":
428
441
  raise StopIteration
429
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,6 +1,19 @@
1
- """Transformation in population."""
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
- from typing import List
16
+ """Transformation in population."""
4
17
 
5
18
  from swcgeom.core import Population, Tree
6
19
  from swcgeom.transforms.base import Transform
@@ -16,7 +29,7 @@ class PopulationTransform(Transform[Population, Population]):
16
29
  self.transform = transform
17
30
 
18
31
  def __call__(self, population: Population) -> Population:
19
- trees: List[Tree] = []
32
+ trees: list[Tree] = []
20
33
  for t in population:
21
34
  new_t = self.transform(t)
22
35
  if new_t.source == "":