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
@@ -0,0 +1,508 @@
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
+ """Neurolucida related transformation."""
17
+
18
+ import os
19
+ import re
20
+ from enum import Enum, auto
21
+ from io import TextIOBase
22
+ from typing import Any, NamedTuple, Optional, cast
23
+
24
+ from swcgeom.core import Tree
25
+ from swcgeom.core.swc_utils import SWCNames, SWCTypes, get_names, get_types
26
+ from swcgeom.transforms.base import Transform
27
+
28
+ __all__ = ["NeurolucidaAscToSwc"]
29
+
30
+
31
+ class NeurolucidaAscToSwc(Transform[str, Tree]):
32
+ """Convert neurolucida asc format to swc format."""
33
+
34
+ def __call__(self, x: str) -> Tree:
35
+ return self.convert(x)
36
+
37
+ @classmethod
38
+ def convert(cls, fname: str) -> Tree:
39
+ with open(fname, "r") as f:
40
+ tree = cls.from_stream(f, source=os.path.abspath(fname))
41
+
42
+ return tree
43
+
44
+ @classmethod
45
+ def from_stream(cls, x: TextIOBase, *, source: str = "") -> Tree:
46
+ parser = Parser(x, source=source)
47
+ ast = parser.parse()
48
+ tree = cls.from_ast(ast)
49
+ return tree
50
+
51
+ @staticmethod
52
+ def from_ast(
53
+ ast: "AST",
54
+ *,
55
+ names: Optional[SWCNames] = None,
56
+ types: Optional[SWCTypes] = None,
57
+ ) -> Tree:
58
+ names = get_names(names)
59
+ types = get_types(types)
60
+ ndata = {n: [] for n in names.cols()}
61
+
62
+ next_id = 0
63
+ typee = [types.undefined]
64
+
65
+ def walk_ast(root: ASTNode, pid: int = -1) -> None:
66
+ nonlocal next_id, typee
67
+ match root.type:
68
+ case ASTType.ROOT:
69
+ for n in root.children:
70
+ walk_ast(n)
71
+
72
+ case ASTType.TREE:
73
+ match root.value:
74
+ case "AXON":
75
+ typee.append(types.axon)
76
+ case "DENDRITE":
77
+ typee.append(types.basal_dendrite)
78
+
79
+ for n in root.children:
80
+ walk_ast(n)
81
+
82
+ typee.pop()
83
+
84
+ case ASTType.NODE:
85
+ x, y, z, r = root.value
86
+ idx = next_id
87
+ next_id += 1
88
+
89
+ ndata[names.id].append(idx)
90
+ ndata[names.type].append(typee[-1])
91
+ ndata[names.x].append(x)
92
+ ndata[names.y].append(y)
93
+ ndata[names.z].append(z)
94
+ ndata[names.r].append(r)
95
+ ndata[names.pid].append(pid)
96
+
97
+ for n in root.children:
98
+ walk_ast(n, pid=idx)
99
+
100
+ walk_ast(ast)
101
+ tree = Tree(
102
+ next_id,
103
+ source=ast.source,
104
+ names=names,
105
+ **ndata, # type: ignore
106
+ )
107
+ return tree
108
+
109
+
110
+ # -----------------
111
+ # ASC format parser
112
+ # -----------------
113
+
114
+ # AST
115
+
116
+
117
+ class ASTType(Enum):
118
+ ROOT = auto()
119
+ TREE = auto()
120
+ NODE = auto()
121
+ COLOR = auto()
122
+ COMMENT = auto()
123
+
124
+
125
+ class ASTNode:
126
+ parent: "ASTNode | None" = None
127
+
128
+ def __init__(
129
+ self,
130
+ type: ASTType,
131
+ value: Any = None,
132
+ tokens: Optional[list["Token"]] = None,
133
+ children: Optional[list["ASTNode"]] = None,
134
+ ):
135
+ self.type = type
136
+ self.value = value
137
+ self.tokens = tokens or []
138
+ self.children = children or []
139
+ for child in self.children:
140
+ child.parent = self
141
+
142
+ def add_child(self, child: "ASTNode") -> None:
143
+ self.children.append(child)
144
+ child.parent = self
145
+ if child.tokens is not None:
146
+ self.tokens.extend(child.tokens)
147
+
148
+ def __eq__(self, __value: object) -> bool:
149
+ """
150
+ Compare two ASTNode objects.
151
+
152
+ Notes
153
+ -----
154
+ The `parent`, `tokens` attribute is not compared.
155
+ """
156
+ return (
157
+ isinstance(__value, ASTNode)
158
+ and self.type == __value.type
159
+ and self.value == __value.value
160
+ and self.children == __value.children
161
+ )
162
+
163
+
164
+ class AST(ASTNode):
165
+ def __init__(self, children: Optional[list[ASTNode]] = None, source: str = ""):
166
+ super().__init__(ASTType.ROOT, children=children)
167
+ self.source = source
168
+
169
+
170
+ # ASC values
171
+
172
+
173
+ class ASCNode(NamedTuple):
174
+ x: float
175
+ y: float
176
+ z: float
177
+ r: float
178
+
179
+
180
+ class ASCColor(NamedTuple):
181
+ color: str
182
+
183
+ def __eq__(self, __value: object) -> bool:
184
+ return (
185
+ isinstance(__value, ASCColor)
186
+ and self.color.upper() == __value.color.upper()
187
+ )
188
+
189
+
190
+ class ASCComment(NamedTuple):
191
+ comment: str
192
+
193
+
194
+ # Error
195
+
196
+
197
+ class TokenTypeError(ValueError):
198
+ def __init__(self, token: "Token", expected: str):
199
+ super().__init__(
200
+ f"Unexpected token {token.type.name} `{token.value}` at {token.lineno}:{token.column}, expected {expected}"
201
+ )
202
+
203
+
204
+ class LiteralTokenError(ValueError):
205
+ def __init__(self, token: "Token", expected: str):
206
+ super().__init__(
207
+ f"Unexpected LITERAL token {token.value} at {token.lineno}:{token.column}, expected {expected}"
208
+ )
209
+
210
+
211
+ class AssertionTokenTypeError(Exception):
212
+ pass
213
+
214
+
215
+ # Parser
216
+
217
+
218
+ class Parser:
219
+ def __init__(self, r: TextIOBase, *, source: str = ""):
220
+ self.lexer = Lexer(r)
221
+ self.next_token = None
222
+ self.source = source
223
+ self._read_token()
224
+
225
+ def parse(self) -> AST:
226
+ try:
227
+ return self._parse()
228
+ except AssertionTokenTypeError as assertion_err:
229
+ msg = (
230
+ f"Error parsing {self.source}" if self.source != "" else "Error parsing"
231
+ )
232
+ original_error = assertion_err.__cause__
233
+ err = ValueError(msg)
234
+ if original_error is None:
235
+ raise err
236
+
237
+ ignores = ["_assert_and_cunsume", "_assert"]
238
+ current = assertion_err.__traceback__
239
+ while current is not None:
240
+ if (
241
+ current.tb_next is not None
242
+ and current.tb_next.tb_frame.f_code.co_name in ignores
243
+ ):
244
+ current.tb_next = None
245
+ else:
246
+ current = current.tb_next
247
+
248
+ original_error.__traceback__ = assertion_err.__traceback__
249
+
250
+ raise err from original_error
251
+ except Exception as original_error:
252
+ msg = f"Error parsing {self.source}" if self.source else "Error parsing"
253
+ raise ValueError(msg) from original_error
254
+
255
+ def _parse(self) -> AST:
256
+ root = AST(source=self.source)
257
+
258
+ token = self._assert_and_cunsume(TokenType.BRACKET_LEFT)
259
+ root.tokens.append(token)
260
+
261
+ while (token := self.next_token) is not None:
262
+ if token.type == TokenType.BRACKET_RIGHT:
263
+ break
264
+
265
+ if token.type != TokenType.BRACKET_LEFT:
266
+ raise TokenTypeError(token, "BRACKET_LEFT, BRACKET_RIGHT")
267
+
268
+ root.tokens.append(token)
269
+ self._consume()
270
+
271
+ token = self._assert(self.next_token, TokenType.LITERAL)
272
+ match str.upper(token.value):
273
+ case "AXON" | "DENDRITE":
274
+ self._parse_tree(root)
275
+
276
+ case "COLOR":
277
+ self._parse_color(root) # TODO: bug
278
+
279
+ case _:
280
+ raise LiteralTokenError(token, "AXON, DENDRITE, COLOR")
281
+
282
+ token = self._assert(self.next_token, TokenType.BRACKET_RIGHT)
283
+ token = self._assert_and_cunsume(TokenType.BRACKET_RIGHT)
284
+ root.tokens.append(token)
285
+ return root
286
+
287
+ def _parse_tree(self, root: ASTNode) -> None:
288
+ t1 = self._assert_and_cunsume(TokenType.LITERAL)
289
+ node = ASTNode(ASTType.TREE, str.upper(t1.value), tokens=[t1])
290
+
291
+ t2 = self._assert_and_cunsume(TokenType.BRACKET_RIGHT)
292
+ node.tokens.append(t2)
293
+
294
+ t3 = self._assert_and_cunsume(TokenType.BRACKET_LEFT)
295
+ node.tokens.append(t3)
296
+
297
+ self._parse_subtree(node)
298
+ root.add_child(node)
299
+
300
+ def _parse_subtree(self, root: ASTNode) -> None:
301
+ flag = True # flag to check if the brachet_left can be consumed
302
+ current = root
303
+ while (token := self.next_token) is not None:
304
+ match token.type:
305
+ case TokenType.BRACKET_LEFT:
306
+ self._read_token()
307
+ if flag:
308
+ flag = False
309
+ else:
310
+ self._parse_subtree(current)
311
+
312
+ case TokenType.BRACKET_RIGHT:
313
+ break
314
+
315
+ case TokenType.FLOAT:
316
+ current = self._parse_node(current)
317
+ flag = True
318
+
319
+ case TokenType.LITERAL:
320
+ match str.upper(token.value):
321
+ case "COLOR":
322
+ self._parse_color(current)
323
+ case _:
324
+ raise LiteralTokenError(token, "COLOR")
325
+
326
+ flag = True
327
+
328
+ case TokenType.OR:
329
+ current = root
330
+ self._read_token()
331
+ flag = True
332
+
333
+ case TokenType.COMMENT:
334
+ self._parse_comment(current)
335
+
336
+ case _:
337
+ excepted = (
338
+ "BRACKET_LEFT, BRACKET_RIGHT, LITERAL, FLOAT, OR, COMMENT"
339
+ )
340
+ raise TokenTypeError(token, excepted)
341
+
342
+ current.tokens.append(token)
343
+
344
+ def _parse_node(self, root: ASTNode) -> ASTNode:
345
+ # FLOAT FLOAT FLOAT FLOAT )
346
+ t1 = self._assert_and_cunsume(TokenType.FLOAT)
347
+ t2 = self._assert(self.next_token, TokenType.FLOAT)
348
+ self._read_token()
349
+ t3 = self._assert(self.next_token, TokenType.FLOAT)
350
+ self._read_token()
351
+ t4 = self._assert(self.next_token, TokenType.FLOAT)
352
+ self._read_token()
353
+ t5 = self._assert_and_cunsume(TokenType.BRACKET_RIGHT)
354
+
355
+ x, y, z, r = t1.value, t2.value, t3.value, t4.value
356
+ node = ASTNode(ASTType.NODE, ASCNode(x, y, z, r), tokens=[t1, t2, t3, t4, t5])
357
+ root.add_child(node)
358
+ return node
359
+
360
+ def _parse_color(self, root: ASTNode) -> ASTNode:
361
+ # COLOR COLOR_VALUE )
362
+ t1 = self._assert_and_cunsume(TokenType.LITERAL)
363
+ t2 = self._assert_and_cunsume(TokenType.LITERAL)
364
+ t3 = self._assert_and_cunsume(TokenType.BRACKET_RIGHT)
365
+
366
+ node = ASTNode(ASTType.COLOR, ASCColor(t2.value), tokens=[t1, t2, t3])
367
+ root.add_child(node)
368
+ return node
369
+
370
+ def _parse_comment(self, root: ASTNode) -> ASTNode:
371
+ # ; COMMENT
372
+ t1 = self._assert_and_cunsume(TokenType.COMMENT)
373
+ node = ASTNode(ASTType.COMMENT, ASCComment(t1.value), tokens=[t1])
374
+ root.add_child(node) # ? where the comment should be added
375
+ return node
376
+
377
+ def _read_token(self) -> None:
378
+ self.next_token = next(self.lexer, None)
379
+
380
+ def _assert_and_cunsume(self, type: "TokenType") -> "Token":
381
+ token = self._consume()
382
+ token = self._assert(token, type)
383
+ return cast(Token, token)
384
+
385
+ def _assert(self, token: "Token | None", type: "TokenType") -> "Token":
386
+ if token is None:
387
+ raise AssertionTokenTypeError() from ValueError("Unexpected EOF")
388
+
389
+ if token.type != type:
390
+ raise AssertionTokenTypeError() from TokenTypeError(token, type.name)
391
+
392
+ return token
393
+
394
+ def _consume(self) -> "Token | None":
395
+ token = self.next_token
396
+ self._read_token()
397
+ return token
398
+
399
+
400
+ # -----------------
401
+ # ASC format lexer
402
+ # -----------------
403
+
404
+
405
+ class TokenType(Enum):
406
+ BRACKET_LEFT = auto()
407
+ BRACKET_RIGHT = auto()
408
+ COMMENT = auto()
409
+ OR = auto()
410
+ FLOAT = auto()
411
+ LITERAL = auto()
412
+
413
+
414
+ class Token:
415
+ def __init__(self, type: TokenType, value: Any, lineno: int, column: int):
416
+ self.type = type
417
+ self.value = value
418
+ self.lineno = lineno
419
+ self.column = column
420
+
421
+ def __repr__(self) -> str:
422
+ return f"Token({self.type.name}, {self.value}, Position={self.lineno}:{self.column})"
423
+
424
+
425
+ RE_FLOAT = re.compile(r"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?")
426
+
427
+
428
+ class Lexer:
429
+ def __init__(self, r: TextIOBase):
430
+ self.r = r
431
+ self.lineno = 1
432
+ self.column = 1
433
+ self.next_char = self.r.read(1)
434
+
435
+ def __iter__(self):
436
+ return self
437
+
438
+ def __next__(self) -> Token:
439
+ match word := self._read_word():
440
+ case "":
441
+ raise StopIteration
442
+
443
+ case "(":
444
+ return self._token(TokenType.BRACKET_LEFT, word)
445
+
446
+ case ")":
447
+ return self._token(TokenType.BRACKET_RIGHT, word)
448
+
449
+ case ";":
450
+ return self._token(TokenType.COMMENT, self._read_line())
451
+
452
+ case "|":
453
+ return self._token(TokenType.OR, word)
454
+
455
+ case _ if RE_FLOAT.match(word) is not None:
456
+ return self._token(TokenType.FLOAT, float(word))
457
+
458
+ case _:
459
+ return self._token(TokenType.LITERAL, word)
460
+
461
+ def _read_char(self) -> bool:
462
+ self.next_char = self.r.read(1)
463
+ if self.next_char == "":
464
+ return False
465
+
466
+ if self.next_char == "\n":
467
+ self.lineno += 1
468
+ self.column = 1
469
+ else:
470
+ self.column += 1
471
+ return True
472
+
473
+ def _read_word(self) -> str:
474
+ # skip leading spaces
475
+ while self.next_char != "" and self.next_char in " \t\n":
476
+ self._read_char()
477
+
478
+ token = ""
479
+ while self.next_char != "" and self.next_char not in " \t\n();|":
480
+ token += self.next_char
481
+ self._read_char()
482
+
483
+ if token != "":
484
+ return token
485
+
486
+ if self.next_char == "":
487
+ return ""
488
+
489
+ ch = self.next_char
490
+ self._read_char()
491
+ return ch
492
+
493
+ def _read_line(self) -> str:
494
+ if self.next_char != "\n":
495
+ line = self.r.readline()
496
+ line = self.next_char + line
497
+ if line.endswith("\n"):
498
+ line = line[:-1]
499
+ else:
500
+ line = ""
501
+
502
+ self.lineno += 1
503
+ self.column = 1
504
+ self.next_char = self.r.read(1)
505
+ return line
506
+
507
+ def _token(self, type: TokenType, value: Any) -> Token:
508
+ return Token(type, value, self.lineno, self.column)
@@ -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 == "":