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.
- {swcgeom-0.12.0 → swcgeom-0.13.0}/.vscode/settings.json +6 -1
- {swcgeom-0.12.0 → swcgeom-0.13.0}/CHANGELOG.md +25 -0
- {swcgeom-0.12.0/swcgeom.egg-info → swcgeom-0.13.0}/PKG-INFO +2 -1
- {swcgeom-0.12.0 → swcgeom-0.13.0}/pyproject.toml +1 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/_version.py +2 -2
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/feature_extractor.py +15 -6
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/sholl.py +7 -3
- swcgeom-0.13.0/swcgeom/analysis/volume.py +80 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/checker.py +29 -5
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/io.py +2 -2
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/__init__.py +2 -0
- swcgeom-0.13.0/swcgeom/utils/dsu.py +42 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/file.py +7 -6
- swcgeom-0.13.0/swcgeom/utils/geometry_object.py +299 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/neuromorpho.py +7 -2
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/renderer.py +4 -3
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/transforms.py +26 -1
- {swcgeom-0.12.0 → swcgeom-0.13.0/swcgeom.egg-info}/PKG-INFO +2 -1
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom.egg-info/SOURCES.txt +9 -1
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom.egg-info/requires.txt +1 -0
- swcgeom-0.13.0/tests/__init__.py +0 -0
- swcgeom-0.13.0/tests/utils/__init__.py +0 -0
- swcgeom-0.13.0/tests/utils/test_dsu.py +34 -0
- swcgeom-0.13.0/tests/utils/test_geometry_object.py +140 -0
- swcgeom-0.13.0/tests/utils/test_transforms.py +33 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/.github/workflows/build.yml +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/.github/workflows/github-publish.yml +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/.github/workflows/pypi-publish.yml +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/.github/workflows/test-pypi-publish.yml +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/.gitignore +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/.pylintrc +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/LICENSE +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/README.md +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/Branch.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/BranchTree.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/CollectTips.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/CutTree.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/Features.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/GeometryTransform.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/ImageStack.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/MST.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/SpectralClustering.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/Tree.ipynb +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/data/101711-10_4p5-of-16_initial.CNG.swc +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/data/101711-11_16-of-16_initial.CNG.swc +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/data/1059283677_15257_2226-X16029-Y23953.swc +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/data/toydata.swc +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/dgl/graph.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/pytorch/branch.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/pytorch/branch_dataset.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/examples/pytorch/tree_folder_dataset.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/git-conventional-commits.yaml +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/setup.cfg +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/__init__.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/__init__.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/branch_features.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/node_features.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/path_features.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/trunk.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/analysis/visualization.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/__init__.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/branch.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/branch_tree.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/node.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/path.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/population.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/segment.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/__init__.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/assembler.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/base.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/normalizer.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/swc_utils/subtree.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/tree.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/tree_utils.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/core/tree_utils_impl.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/images/__init__.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/images/augmentation.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/images/folder.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/images/io.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/__init__.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/base.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/branch.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/geometry.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/image_stack.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/images.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/mst.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/path.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/population.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/tree.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/transforms/tree_assembler.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/debug.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/download.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/ellipse.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/numpy_helper.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom/utils/sdf.py +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom.egg-info/dependency_links.txt +0 -0
- {swcgeom-0.12.0 → swcgeom-0.13.0}/swcgeom.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## **0.13.0** <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** <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.
|
|
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
|
|
@@ -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(
|
|
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=
|
|
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(
|
|
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=
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
31
|
-
s.append(idx)
|
|
32
|
-
children[pid] = s
|
|
35
|
+
children[pid].append(idx)
|
|
33
36
|
|
|
34
|
-
root = children
|
|
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 :
|
|
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 :
|
|
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 |
|
|
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 :
|
|
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,
|
|
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) ->
|
|
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,
|
|
75
|
+
if isinstance(fname, TextIOBase):
|
|
75
76
|
return fname.encoding
|
|
76
77
|
elif isinstance(fname, BytesIO):
|
|
77
78
|
data = fname.read()
|