swcgeom 0.12.0__tar.gz → 0.13.0__tar.gz

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 (98) hide show
  1. {swcgeom-0.12.0 → swcgeom-0.13.0}/.vscode/settings.json +6 -1
  2. {swcgeom-0.12.0 → swcgeom-0.13.0}/CHANGELOG.md +25 -0
  3. {swcgeom-0.12.0/swcgeom.egg-info → swcgeom-0.13.0}/PKG-INFO +2 -1
  4. {swcgeom-0.12.0 → swcgeom-0.13.0}/pyproject.toml +1 -0
  5. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/_version.py +2 -2
  6. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/feature_extractor.py +15 -6
  7. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/sholl.py +7 -3
  8. swcgeom-0.13.0/swcgeom/analysis/volume.py +80 -0
  9. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/checker.py +29 -5
  10. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/io.py +2 -2
  11. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/__init__.py +2 -0
  12. swcgeom-0.13.0/swcgeom/utils/dsu.py +42 -0
  13. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/file.py +7 -6
  14. swcgeom-0.13.0/swcgeom/utils/geometry_object.py +299 -0
  15. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/neuromorpho.py +7 -2
  16. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/renderer.py +4 -3
  17. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/transforms.py +26 -1
  18. {swcgeom-0.12.0 → swcgeom-0.13.0/swcgeom.egg-info}/PKG-INFO +2 -1
  19. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom.egg-info/SOURCES.txt +9 -1
  20. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom.egg-info/requires.txt +1 -0
  21. swcgeom-0.13.0/tests/__init__.py +0 -0
  22. swcgeom-0.13.0/tests/utils/__init__.py +0 -0
  23. swcgeom-0.13.0/tests/utils/test_dsu.py +34 -0
  24. swcgeom-0.13.0/tests/utils/test_geometry_object.py +140 -0
  25. swcgeom-0.13.0/tests/utils/test_transforms.py +33 -0
  26. {swcgeom-0.12.0 → swcgeom-0.13.0}/.github/workflows/build.yml +0 -0
  27. {swcgeom-0.12.0 → swcgeom-0.13.0}/.github/workflows/github-publish.yml +0 -0
  28. {swcgeom-0.12.0 → swcgeom-0.13.0}/.github/workflows/pypi-publish.yml +0 -0
  29. {swcgeom-0.12.0 → swcgeom-0.13.0}/.github/workflows/test-pypi-publish.yml +0 -0
  30. {swcgeom-0.12.0 → swcgeom-0.13.0}/.gitignore +0 -0
  31. {swcgeom-0.12.0 → swcgeom-0.13.0}/.pylintrc +0 -0
  32. {swcgeom-0.12.0 → swcgeom-0.13.0}/LICENSE +0 -0
  33. {swcgeom-0.12.0 → swcgeom-0.13.0}/README.md +0 -0
  34. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/Branch.ipynb +0 -0
  35. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/BranchTree.ipynb +0 -0
  36. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/CollectTips.ipynb +0 -0
  37. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/CutTree.ipynb +0 -0
  38. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/Features.ipynb +0 -0
  39. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/GeometryTransform.ipynb +0 -0
  40. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/ImageStack.ipynb +0 -0
  41. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/MST.ipynb +0 -0
  42. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/SpectralClustering.ipynb +0 -0
  43. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/Tree.ipynb +0 -0
  44. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/data/101711-10_4p5-of-16_initial.CNG.swc +0 -0
  45. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/data/101711-11_16-of-16_initial.CNG.swc +0 -0
  46. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/data/1059283677_15257_2226-X16029-Y23953.swc +0 -0
  47. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/data/toydata.swc +0 -0
  48. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/dgl/graph.py +0 -0
  49. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/pytorch/branch.py +0 -0
  50. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/pytorch/branch_dataset.py +0 -0
  51. {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/pytorch/tree_folder_dataset.py +0 -0
  52. {swcgeom-0.12.0 → swcgeom-0.13.0}/git-conventional-commits.yaml +0 -0
  53. {swcgeom-0.12.0 → swcgeom-0.13.0}/setup.cfg +0 -0
  54. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/__init__.py +0 -0
  55. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/__init__.py +0 -0
  56. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/branch_features.py +0 -0
  57. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/node_features.py +0 -0
  58. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/path_features.py +0 -0
  59. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/trunk.py +0 -0
  60. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/visualization.py +0 -0
  61. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/__init__.py +0 -0
  62. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/branch.py +0 -0
  63. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/branch_tree.py +0 -0
  64. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/node.py +0 -0
  65. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/path.py +0 -0
  66. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/population.py +0 -0
  67. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/segment.py +0 -0
  68. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc.py +0 -0
  69. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/__init__.py +0 -0
  70. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/assembler.py +0 -0
  71. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/base.py +0 -0
  72. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/normalizer.py +0 -0
  73. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/subtree.py +0 -0
  74. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/tree.py +0 -0
  75. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/tree_utils.py +0 -0
  76. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/tree_utils_impl.py +0 -0
  77. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/images/__init__.py +0 -0
  78. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/images/augmentation.py +0 -0
  79. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/images/folder.py +0 -0
  80. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/images/io.py +0 -0
  81. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/__init__.py +0 -0
  82. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/base.py +0 -0
  83. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/branch.py +0 -0
  84. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/geometry.py +0 -0
  85. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/image_stack.py +0 -0
  86. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/images.py +0 -0
  87. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/mst.py +0 -0
  88. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/path.py +0 -0
  89. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/population.py +0 -0
  90. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/tree.py +0 -0
  91. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/tree_assembler.py +0 -0
  92. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/debug.py +0 -0
  93. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/download.py +0 -0
  94. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/ellipse.py +0 -0
  95. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/numpy_helper.py +0 -0
  96. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/sdf.py +0 -0
  97. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom.egg-info/dependency_links.txt +0 -0
  98. {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom.egg-info/top_level.txt +0 -0
@@ -11,5 +11,10 @@
11
11
  ],
12
12
  "[python]": {
13
13
  "editor.defaultFormatter": "ms-python.black-formatter"
14
- }
14
+ },
15
+ "python.testing.pytestArgs": [
16
+ "tests"
17
+ ],
18
+ "python.testing.unittestEnabled": false,
19
+ "python.testing.pytestEnabled": true
15
20
  }
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## **0.13.0**&emsp;<sub><sup>2023-12-14 ([e2add59...06239bd](https://github.com/yzx9/swcgeom/compare/e2add59652bfc02d802f6770ea2c5fbc3fd7d729...06239bd129e6fab329ec80326352b48049fb504e?diff=split))</sup></sub>
4
+
5
+ ### Features
6
+
7
+ ##### `analysis`
8
+ * import sholl plot ([b03b45c](https://github.com/yzx9/swcgeom/commit/b03b45c4f20f2ed57263b1c2398316533b45b837))
9
+ * calc volume of tree \(close \#9\) ([a5004da](https://github.com/yzx9/swcgeom/commit/a5004dab71e71e68fd4a512757c9310f557878cf))
10
+
11
+ ##### `core`
12
+ * check if it has a cyclic \(\#1\) ([e2add59](https://github.com/yzx9/swcgeom/commit/e2add59652bfc02d802f6770ea2c5fbc3fd7d729))
13
+
14
+ ##### `utils`
15
+ * transform batch of vec3 to vec4 ([d2d660c](https://github.com/yzx9/swcgeom/commit/d2d660ca53b9886a81b02193ea77f76da4620ffa))
16
+
17
+ ### Bug Fixes
18
+
19
+ ##### `utils`
20
+ * should support \`StringIO\` ([de439db](https://github.com/yzx9/swcgeom/commit/de439dba00ce7407d4ae18c9427eab1e5af4d95e))
21
+
22
+ ### Performance Improvements
23
+ * improve dsu ([8b414c3](https://github.com/yzx9/swcgeom/commit/8b414c37f4fc3f4ded9c8b19eb8ee0ad52dedd53))
24
+
25
+ <br>
26
+
27
+
3
28
  ## **0.12.0**&emsp;<sub><sup>2023-10-12 ([d9ba943...0824e9b](https://github.com/yzx9/swcgeom/compare/d9ba9433735c69edf979013632278e5f498a6fe0...0824e9b4110f820cd11c469ca6e319c1b2f14145?diff=split))</sup></sub>
4
29
 
5
30
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: swcgeom
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Neuron geometry library for swc format
5
5
  Author-email: yzx9 <yuan.zx@outlook.com>
6
6
  License: CC4.0 BY-NC-SA
@@ -16,6 +16,7 @@ Requires-Dist: pandas>=1.4.2
16
16
  Requires-Dist: pynrrd>=1.0.0
17
17
  Requires-Dist: scipy>=1.9.1
18
18
  Requires-Dist: seaborn>=0.12.0
19
+ Requires-Dist: sympy>=1.12
19
20
  Requires-Dist: tifffile>=2022.8.12
20
21
  Requires-Dist: typing_extensions>=4.4.0
21
22
  Requires-Dist: v3d-py-helper-0.1.0
@@ -19,6 +19,7 @@ dependencies = [
19
19
  "pynrrd>=1.0.0",
20
20
  "scipy>=1.9.1",
21
21
  "seaborn>=0.12.0",
22
+ "sympy>=1.12",
22
23
  "tifffile>=2022.8.12",
23
24
  "typing_extensions>=4.4.0",
24
25
  "v3d-py-helper-0.1.0",
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.12.0'
16
- __version_tuple__ = version_tuple = (0, 12, 0)
15
+ __version__ = version = '0.13.0'
16
+ __version_tuple__ = version_tuple = (0, 13, 0)
@@ -25,6 +25,7 @@ from swcgeom.analysis.node_features import (
25
25
  )
26
26
  from swcgeom.analysis.path_features import PathFeatures
27
27
  from swcgeom.analysis.sholl import Sholl
28
+ from swcgeom.analysis.volume import get_volume
28
29
  from swcgeom.core import Population, Populations, Tree
29
30
  from swcgeom.utils import padding1d
30
31
 
@@ -32,6 +33,7 @@ __all__ = ["Feature", "extract_feature"]
32
33
 
33
34
  Feature = Literal[
34
35
  "length",
36
+ "volume",
35
37
  "sholl",
36
38
  # node
37
39
  "node_count",
@@ -54,7 +56,7 @@ Feature = Literal[
54
56
  NDArrayf32 = npt.NDArray[np.float32]
55
57
  FeatAndKwargs = Feature | Tuple[Feature, Dict[str, Any]]
56
58
 
57
- Feature1D = set(["length", "node_count", "bifurcation_count", "tip_count"])
59
+ Feature1D = set(["length", "volume", "node_count", "bifurcation_count", "tip_count"])
58
60
 
59
61
 
60
62
  class Features:
@@ -105,6 +107,9 @@ class Features:
105
107
  def get_length(self, **kwargs) -> NDArrayf32:
106
108
  return np.array([self.tree.length(**kwargs)], dtype=np.float32)
107
109
 
110
+ def get_volume(self, **kwargs) -> NDArrayf32:
111
+ return np.array([get_volume(self.tree, **kwargs)], dtype=np.float32)
112
+
108
113
  def get_sholl(self, **kwargs) -> NDArrayf32:
109
114
  return self.sholl.get(**kwargs).astype(np.float32)
110
115
 
@@ -288,9 +293,11 @@ class PopulationFeatureExtractor(FeatureExtractor):
288
293
  v = np.stack([padding1d(len_max, v, dtype=np.float32) for v in vals])
289
294
  return v
290
295
 
291
- def _get_sholl_impl(self, **kwargs) -> Tuple[NDArrayf32, NDArrayf32]:
296
+ def _get_sholl_impl(
297
+ self, steps: int = 20, **kwargs
298
+ ) -> Tuple[NDArrayf32, NDArrayf32]:
292
299
  rmax = max(t.sholl.rmax for t in self._features)
293
- rs = Sholl.get_rs(rmax=rmax, steps=20)
300
+ rs = Sholl.get_rs(rmax=rmax, steps=steps)
294
301
  vals = self._get_impl("sholl", steps=rs, **kwargs)
295
302
  return vals, rs
296
303
 
@@ -360,10 +367,12 @@ class PopulationsFeatureExtractor(FeatureExtractor):
360
367
 
361
368
  return out
362
369
 
363
- def _get_sholl_impl(self, **kwargs) -> Tuple[NDArrayf32, NDArrayf32]:
370
+ def _get_sholl_impl(
371
+ self, steps: int = 20, **kwargs
372
+ ) -> Tuple[NDArrayf32, NDArrayf32]:
364
373
  rmaxs = chain.from_iterable((t.sholl.rmax for t in p) for p in self._features)
365
374
  rmax = max(rmaxs)
366
- rs = Sholl.get_rs(rmax=rmax, steps=20)
375
+ rs = Sholl.get_rs(rmax=rmax, steps=steps)
367
376
  vals = self._get_impl("sholl", steps=rs, **kwargs)
368
377
  return vals, rs
369
378
 
@@ -408,7 +417,7 @@ class PopulationsFeatureExtractor(FeatureExtractor):
408
417
  return ax
409
418
 
410
419
 
411
- def extract_feature(obj: Tree | Population) -> FeatureExtractor:
420
+ def extract_feature(obj: Tree | Population | Populations) -> FeatureExtractor:
412
421
  if isinstance(obj, Tree):
413
422
  return TreeFeatureExtractor(obj)
414
423
 
@@ -42,9 +42,12 @@ class Sholl:
42
42
  step: Optional[float] = None,
43
43
  ) -> None:
44
44
  tree = Tree.from_swc(tree) if isinstance(tree, str) else tree
45
- self.tree = TranslateOrigin.transform(tree) # shift
46
- self.rs = np.linalg.norm(self.tree.get_segments().xyz(), axis=2)
47
- self.rmax = self.rs.max()
45
+ try:
46
+ self.tree = TranslateOrigin.transform(tree) # shift
47
+ self.rs = np.linalg.norm(self.tree.get_segments().xyz(), axis=2)
48
+ self.rmax = self.rs.max()
49
+ except Exception as e:
50
+ raise ValueError(f"invalid tree: {tree.source or ''}") from e
48
51
 
49
52
  if step is not None:
50
53
  warnings.warn(
@@ -96,6 +99,7 @@ class Sholl:
96
99
  **kwargs :
97
100
  Forwarding to plot method.
98
101
  """
102
+
99
103
  if plot_type is not None:
100
104
  warnings.warn(
101
105
  "`plot_type` has been renamed to `kind` since v0.5.0, "
@@ -0,0 +1,80 @@
1
+ """Analysis of volume of a SWC tree."""
2
+
3
+ from typing import List
4
+
5
+ from swcgeom.core import Tree
6
+ from swcgeom.utils import GeomFrustumCone, GeomSphere
7
+
8
+ __all__ = ["get_volume"]
9
+
10
+
11
+ def get_volume(tree: Tree):
12
+ """Get the volume of the tree.
13
+
14
+ Parameters
15
+ ----------
16
+ tree : Tree
17
+ SWC tree.
18
+
19
+ Returns
20
+ -------
21
+ volume : float
22
+ Volume of the tree.
23
+
24
+ Notes
25
+ -----
26
+ The SWC format is a method for representing neurons, which includes
27
+ both the radius of individual points and their interconnectivity.
28
+ Consequently, there are multiple distinct approaches to
29
+ representation within this framework.
30
+
31
+ Currently, we support a standard approach to volume calculation.
32
+ This method involves treating each node as a sphere and
33
+ representing the connections between them as truncated cone-like
34
+ structures, or frustums, with varying radii at their top and bottom
35
+ surfaces.
36
+
37
+ More representation methods will be supported in the future.
38
+ """
39
+ volume = 0.0
40
+
41
+ def leave(node: Tree.Node, children: List[GeomSphere]) -> GeomSphere:
42
+ sphere = GeomSphere(node.xyz(), node.r)
43
+ frustum_cones = [
44
+ GeomFrustumCone(node.xyz(), node.r, c.center, c.radius) for c in children
45
+ ]
46
+
47
+ v = sphere.get_volume()
48
+ v += sum(fc.get_volume() for fc in frustum_cones)
49
+ v -= sum(sphere.get_intersect_volume(fc) for fc in frustum_cones)
50
+ v -= sum(s.get_intersect_volume(fc) for s, fc in zip(children, frustum_cones))
51
+
52
+ # TODO
53
+ # remove volume of intersection between frustum cones
54
+ # v -= sum(
55
+ # fc1.get_intersect_volume(fc2)
56
+ # for fc1 in frustum_cones
57
+ # for fc2 in frustum_cones
58
+ # if fc1 != fc2
59
+ # )
60
+
61
+ nonlocal volume
62
+ volume += v
63
+ return sphere
64
+
65
+ tree.traverse(leave=leave)
66
+ return volume
67
+
68
+
69
+ if __name__ == "__main__":
70
+ from io import StringIO
71
+
72
+ swc = """
73
+ 1 1 0 0 0 1 -1
74
+ 2 1 2 0 0 1 1
75
+ 3 1 4 0 0 1 2
76
+ """
77
+
78
+ tree = Tree.from_swc(StringIO(swc))
79
+ volume = get_volume(tree)
80
+ print(volume)
@@ -1,17 +1,22 @@
1
1
  """Check common """
2
2
 
3
3
  import warnings
4
+ from collections import defaultdict
4
5
  from typing import Optional
5
6
 
6
7
  import numpy as np
7
8
  import pandas as pd
8
9
 
9
10
  from swcgeom.core.swc_utils.base import SWCNames, Topology, get_dsu, get_names, traverse
11
+ from swcgeom.utils import DisjointSetUnion
12
+
10
13
 
11
14
  __all__ = [
12
15
  "is_single_root",
13
16
  "is_bifurcate",
14
17
  "is_sorted",
18
+ "has_cyclic",
19
+ # legacy
15
20
  "is_binary_tree",
16
21
  "check_single_root",
17
22
  ]
@@ -25,13 +30,11 @@ def is_single_root(df: pd.DataFrame, *, names: Optional[SWCNames] = None) -> boo
25
30
  def is_bifurcate(topology: Topology, *, exclude_root: bool = True) -> bool:
26
31
  """Check is it a bifurcate topology."""
27
32
 
28
- children = {}
33
+ children = defaultdict(list)
29
34
  for idx, pid in zip(*topology):
30
- s = children.get(pid, [])
31
- s.append(idx)
32
- children[pid] = s
35
+ children[pid].append(idx)
33
36
 
34
- root = children.get(-1, [])
37
+ root = children[-1]
35
38
  for k, v in children.items():
36
39
  if len(v) > 1 and (not exclude_root or k in root):
37
40
  return False
@@ -58,6 +61,27 @@ def is_sorted(topology: Topology) -> bool:
58
61
  return flag
59
62
 
60
63
 
64
+ def has_cyclic(topology: Topology) -> bool:
65
+ """Has cyclic in topology."""
66
+ node_num = len(topology[0])
67
+ dsu = DisjointSetUnion(node_number=node_num)
68
+
69
+ for i in range(node_num):
70
+ node_a = topology[0][i]
71
+ node_b = topology[1][i]
72
+ # skip the root node
73
+ if node_b == -1:
74
+ continue
75
+
76
+ # check whether it is circle
77
+ if dsu.is_same_set(node_a, node_b):
78
+ return True
79
+
80
+ dsu.union_sets(node_a, node_b)
81
+
82
+ return False
83
+
84
+
61
85
  def check_single_root(*args, **kwargs) -> bool:
62
86
  warnings.warn(
63
87
  "`check_single_root` has been renamed to `is_single_root` since"
@@ -35,7 +35,7 @@ def read_swc(
35
35
 
36
36
  Parameters
37
37
  ----------
38
- swc_file : str
38
+ swc_file : PathOrIO
39
39
  Path of swc file, the id should be consecutively incremented.
40
40
  extra_cols : Iterable[str], optional
41
41
  Read more cols in swc file.
@@ -142,7 +142,7 @@ def parse_swc(
142
142
 
143
143
  Parameters
144
144
  ----------
145
- fname : str
145
+ fname : PathOrIO
146
146
  names : SWCNames
147
147
  extra_cols : list of str, optional
148
148
  encoding : str | 'detect', default `utf-8`
@@ -1,8 +1,10 @@
1
1
  """Utils."""
2
2
 
3
3
  from swcgeom.utils.debug import *
4
+ from swcgeom.utils.dsu import *
4
5
  from swcgeom.utils.ellipse import *
5
6
  from swcgeom.utils.file import *
7
+ from swcgeom.utils.geometry_object import *
6
8
  from swcgeom.utils.neuromorpho import *
7
9
  from swcgeom.utils.numpy_helper import *
8
10
  from swcgeom.utils.renderer import *
@@ -0,0 +1,42 @@
1
+ """Disjoint Set Union Impl."""
2
+
3
+ __all__ = ["DisjointSetUnion"]
4
+
5
+
6
+ class DisjointSetUnion:
7
+ """Disjoint Set Union.
8
+
9
+ DSU with path compression and union by rank.
10
+ """
11
+
12
+ def __init__(self, node_number: int):
13
+ self.element_parent = [i for i in range(node_number)]
14
+ self.rank = [0 for _ in range(node_number)]
15
+
16
+ def find_parent(self, node_id: int) -> int:
17
+ if node_id != self.element_parent[node_id]:
18
+ self.element_parent[node_id] = self.find_parent(
19
+ self.element_parent[node_id]
20
+ )
21
+ return self.element_parent[node_id]
22
+
23
+ def union_sets(self, node_a: int, node_b: int) -> None:
24
+ assert self.validate_node(node_a) and self.validate_node(node_b)
25
+
26
+ root_a = self.find_parent(node_a)
27
+ root_b = self.find_parent(node_b)
28
+ if root_a != root_b:
29
+ # union by rank
30
+ if self.rank[root_a] < self.rank[root_b]:
31
+ self.element_parent[root_a] = root_b
32
+ elif self.rank[root_a] > self.rank[root_b]:
33
+ self.element_parent[root_b] = root_a
34
+ else:
35
+ self.element_parent[root_b] = root_a
36
+ self.rank[root_a] += 1
37
+
38
+ def is_same_set(self, node_a: int, node_b: int) -> bool:
39
+ return self.find_parent(node_a) == self.find_parent(node_b)
40
+
41
+ def validate_node(self, node_id: int) -> bool:
42
+ return 0 <= node_id < len(self.element_parent)
@@ -11,12 +11,12 @@ pip install swcgeom[all]
11
11
  """
12
12
 
13
13
  import warnings
14
- from io import BytesIO, TextIOWrapper
14
+ from io import BytesIO, TextIOBase, TextIOWrapper
15
15
  from typing import Literal
16
16
 
17
17
  __all__ = ["FileReader", "PathOrIO"]
18
18
 
19
- PathOrIO = int | str | bytes | BytesIO | TextIOWrapper
19
+ PathOrIO = int | str | bytes | BytesIO | TextIOBase
20
20
 
21
21
 
22
22
  class FileReader:
@@ -32,7 +32,7 @@ class FileReader:
32
32
 
33
33
  Parameters
34
34
  ----------
35
- fname : str
35
+ fname : PathOrIO
36
36
  encoding : str | 'detect', default `utf-8`
37
37
  The name of the encoding used to decode the file. If is
38
38
  `detect`, we will try to detect the character encoding.
@@ -40,8 +40,9 @@ class FileReader:
40
40
  Used for detect character endocing, raising warning when
41
41
  parsing with low confidence.
42
42
  """
43
+ # TODO: support StringIO
43
44
  self.fname, self.fb, self.f = "", None, None
44
- if isinstance(fname, TextIOWrapper):
45
+ if isinstance(fname, TextIOBase):
45
46
  self.f = fname
46
47
  encoding = fname.encoding # skip detect
47
48
  elif isinstance(fname, BytesIO):
@@ -54,7 +55,7 @@ class FileReader:
54
55
  self.encoding = encoding
55
56
  self.kwargs = kwargs
56
57
 
57
- def __enter__(self) -> TextIOWrapper:
58
+ def __enter__(self) -> TextIOBase:
58
59
  if isinstance(self.fb, BytesIO):
59
60
  self.f = TextIOWrapper(self.fb, encoding=self.encoding)
60
61
  elif self.f is None:
@@ -71,7 +72,7 @@ class FileReader:
71
72
  def detect_encoding(fname: PathOrIO, *, low_confidence: float = 0.9) -> str:
72
73
  import chardet
73
74
 
74
- if isinstance(fname, TextIOWrapper):
75
+ if isinstance(fname, TextIOBase):
75
76
  return fname.encoding
76
77
  elif isinstance(fname, BytesIO):
77
78
  data = fname.read()