swcgeom 0.12.0__py3-none-any.whl → 0.13.0__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.

swcgeom/_version.py CHANGED
@@ -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
 
swcgeom/analysis/sholl.py CHANGED
@@ -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`
swcgeom/utils/__init__.py CHANGED
@@ -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 *
swcgeom/utils/dsu.py ADDED
@@ -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)
swcgeom/utils/file.py CHANGED
@@ -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()
@@ -0,0 +1,299 @@
1
+ """Geometry object."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from functools import lru_cache
5
+ from typing import List, Tuple
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ from sympy import Eq, solve, symbols
10
+
11
+ __all__ = ["GeomObject", "GeomSphere", "GeomFrustumCone"]
12
+
13
+ eps = 1e-6
14
+
15
+
16
+ class GeomObject(ABC):
17
+ """Geometry object."""
18
+
19
+ @abstractmethod
20
+ def get_volume(self) -> float:
21
+ """Get volume."""
22
+ raise NotImplementedError()
23
+
24
+ @abstractmethod
25
+ def get_intersect_volume(self, obj: "GeomObject") -> float:
26
+ """Get intersect volume.
27
+
28
+ Parameters
29
+ ----------
30
+ obj : GeometryObject
31
+ Another geometry object.
32
+
33
+ Returns
34
+ -------
35
+ volume : float
36
+ Intersect volume.
37
+ """
38
+ raise NotImplementedError()
39
+
40
+
41
+ class GeomSphere(GeomObject):
42
+ """Geometry Sphere."""
43
+
44
+ def __init__(self, center: npt.ArrayLike, radius: float):
45
+ super().__init__()
46
+
47
+ self.center = np.array(center)
48
+ assert len(self.center) == 3
49
+
50
+ self.radius = radius
51
+
52
+ def get_volume(self) -> float:
53
+ return self.calc_volume(self.radius)
54
+
55
+ def get_volume_spherical_cap(self, h: float) -> float:
56
+ return self.calc_volume_spherical_cap(self.radius, h)
57
+
58
+ def get_intersect_volume_sphere(self, obj: "GeomSphere") -> float:
59
+ return self.calc_intersect_volume_sphere(self, obj)
60
+
61
+ def get_intersect_volume_sphere_frustum_cone(
62
+ self, frustum_cone: "GeomFrustumCone"
63
+ ) -> float:
64
+ return calc_intersect_volume_sphere_frustum_cone(self, frustum_cone)
65
+
66
+ def get_intersect_volume(self, obj: GeomObject) -> float:
67
+ if isinstance(obj, GeomSphere):
68
+ return self.get_intersect_volume_sphere(obj)
69
+
70
+ if isinstance(obj, GeomFrustumCone):
71
+ return self.get_intersect_volume_sphere_frustum_cone(obj)
72
+
73
+ classname = obj.__class__.__name__
74
+ raise NotImplementedError(f"unsupported geometry object: {classname}")
75
+
76
+ @staticmethod
77
+ def calc_volume(radius: float) -> float:
78
+ r"""Calculate volume of sphere.
79
+
80
+ \being{equation}
81
+ V = \frac{4}{3} * π * r^3
82
+ \end{equation}
83
+
84
+ Returns
85
+ -------
86
+ volume : float
87
+ Volume.
88
+ """
89
+ return 4 / 3 * np.pi * radius**3
90
+
91
+ @staticmethod
92
+ def calc_volume_spherical_cap(r: float, h: float) -> float:
93
+ r"""Calculate the volume of a spherical cap.
94
+
95
+ \being{equation}
96
+ V = π * h^2 * (3r - h) / 3
97
+ \end{equation}
98
+
99
+ Parameters
100
+ ----------
101
+ r : float
102
+ radius of the sphere
103
+ h : float
104
+ height of the spherical cap
105
+
106
+ Returns
107
+ -------
108
+ volume : float
109
+ volume of the spherical cap
110
+ """
111
+ return np.pi * h**2 * (3 * r - h) / 3
112
+
113
+ @classmethod
114
+ def calc_intersect_volume_sphere(
115
+ cls, obj1: "GeomSphere", obj2: "GeomSphere"
116
+ ) -> float:
117
+ r"""Calculate intersect volume of two spheres.
118
+
119
+ \being{equation}
120
+ V = \frac{\pi}{12d} * (r_1 + r_2 - d)^2 (d^2 + 2d r_1 - 3r_1^2 + 2d r_2 - 3r_2^2 + 6 r_1r_2)
121
+ \end{equation}
122
+
123
+ Returns
124
+ -------
125
+ volume : float
126
+ Intersect volume.
127
+ """
128
+
129
+ r1, r2 = obj1.radius, obj2.radius
130
+ d = np.linalg.norm(obj1.center - obj2.center).item()
131
+ if d > r1 + r2:
132
+ return 0
133
+
134
+ if d <= abs(r1 - r2):
135
+ return cls.calc_volume(min(r1, r2))
136
+
137
+ part1 = (np.pi / (12 * d)) * (r1 + r2 - d) ** 2
138
+ part2 = (
139
+ d**2 + 2 * d * r1 - 3 * r1**2 + 2 * d * r2 - 3 * r2**2 + 6 * r1 * r2
140
+ )
141
+ return part1 * part2
142
+
143
+
144
+ class GeomFrustumCone(GeomObject):
145
+ """Geometry Frustum."""
146
+
147
+ def __init__(self, c1: npt.ArrayLike, r1: float, c2: npt.ArrayLike, r2: float):
148
+ super().__init__()
149
+
150
+ self.c1 = np.array(c1)
151
+ assert len(self.c1) == 3
152
+
153
+ self.c2 = np.array(c2)
154
+ assert len(self.c2) == 3
155
+
156
+ self.r1 = r1
157
+ self.r2 = r2
158
+
159
+ def height(self) -> float:
160
+ """Get height of frustum."""
161
+ return np.linalg.norm(self.c1 - self.c2).item()
162
+
163
+ def get_volume(self) -> float:
164
+ return self.calc_volume(self.r1, self.r2, self.height())
165
+
166
+ def get_intersect_volume_sphere(self, sphere: GeomSphere) -> float:
167
+ return calc_intersect_volume_sphere_frustum_cone(sphere, self)
168
+
169
+ def get_intersect_volume(self, obj: GeomObject) -> float:
170
+ if isinstance(obj, GeomSphere):
171
+ return self.get_intersect_volume_sphere(obj)
172
+
173
+ classname = obj.__class__.__name__
174
+ raise NotImplementedError(f"unsupported geometry object: {classname}")
175
+
176
+ @staticmethod
177
+ def calc_volume(r1: float, r2: float, height: float) -> float:
178
+ r"""Calculate volume of frustum.
179
+
180
+ \being{equation}
181
+ V = \frac{1}{3} * π * h * (r^2 + r * R + R^2)
182
+ \end{equation}
183
+
184
+ Returns
185
+ -------
186
+ volume : float
187
+ Volume.
188
+ """
189
+ return (1 / 3) * np.pi * height * (r1**2 + r1 * r2 + r2**2)
190
+
191
+
192
+ @lru_cache
193
+ def calc_intersect_volume_sphere_frustum_cone(
194
+ sphere: GeomSphere, frustum_cone: GeomFrustumCone
195
+ ) -> float:
196
+ r"""Calculate intersect volume of sphere and frustum cone.
197
+
198
+ Returns
199
+ -------
200
+ volume : float
201
+ Intersect volume.
202
+ """
203
+ h = frustum_cone.height()
204
+ c1, r1 = sphere.center, sphere.radius
205
+ if np.allclose(c1, frustum_cone.c1) and np.allclose(r1, frustum_cone.r1):
206
+ c2, r2 = frustum_cone.c2, frustum_cone.r2
207
+ elif np.allclose(c1, frustum_cone.c2) and np.allclose(r1, frustum_cone.r2):
208
+ c2, r2 = frustum_cone.c1, frustum_cone.r1
209
+ else:
210
+ raise NotImplementedError("unsupported to calculate intersect volume")
211
+
212
+ # Fast-Path: The surface of the frustum concentric with the sphere
213
+ # is the surface with smaller radius
214
+ if r2 >= r1:
215
+ if h >= r1:
216
+ # The hemisphere is completely inside the frustum cone
217
+ return GeomSphere.calc_volume_spherical_cap(r1, r1)
218
+
219
+ # The frustum cone is lower than the hemisphere
220
+ v_himisphere = GeomSphere.calc_volume_spherical_cap(r1, r1)
221
+ v_cap = GeomSphere.calc_volume_spherical_cap(r1, r1 - h)
222
+ return v_himisphere - v_cap
223
+
224
+ up = (c2 - c1) / np.linalg.norm(c2 - c1)
225
+ v = _find_unit_vector_on_plane(up)
226
+
227
+ intersections = _find_sphere_line_intersection(c1, r1, c1 + r1 * v, c2 + r2 * v)
228
+ assert len(intersections) == 2
229
+ t, p = max(intersections, key=lambda x: x[0])
230
+
231
+ # Fast-Path: The frustum cone is completely inside the sphere
232
+ if t > 1 + eps:
233
+ return frustum_cone.get_volume()
234
+
235
+ M = _project_point_on_line(c1, up, p)
236
+ h1 = np.linalg.norm(M - c1).item()
237
+ r3 = np.linalg.norm(M - p).item()
238
+ v_cap1 = GeomSphere.calc_volume_spherical_cap(r1, r1 - h1)
239
+ v_frustum = GeomFrustumCone.calc_volume(r1, r3, h1)
240
+
241
+ # Fast-Path: The frustum cone is higher than the sphere
242
+ if h >= r1:
243
+ return v_cap1 + v_frustum
244
+
245
+ v_cap2 = GeomSphere.calc_volume_spherical_cap(r1, r1 - h)
246
+ return v_cap1 + v_frustum - v_cap2
247
+
248
+
249
+ def _find_unit_vector_on_plane(normal_vec3: npt.NDArray) -> npt.NDArray:
250
+ r = np.random.rand(3)
251
+ while np.allclose(r, normal_vec3) or np.allclose(r, -normal_vec3):
252
+ r = np.random.rand(3)
253
+
254
+ u = np.cross(r, normal_vec3)
255
+ unit_vector = u / np.linalg.norm(u)
256
+ return unit_vector
257
+
258
+
259
+ def _find_sphere_line_intersection(
260
+ sphere_center: npt.NDArray,
261
+ sphere_radius: float,
262
+ line_point_a: npt.NDArray,
263
+ line_point_b: npt.NDArray,
264
+ ) -> List[Tuple[float, npt.NDArray[np.float64]]]:
265
+ x1, y1, z1 = sphere_center
266
+ x2, y2, z2 = line_point_a
267
+ x3, y3, z3 = line_point_b
268
+ t = symbols("t")
269
+
270
+ # line
271
+ x = x2 + t * (x3 - x2)
272
+ y = y2 + t * (y3 - y2)
273
+ z = z2 + t * (z3 - z2)
274
+
275
+ # sphere
276
+ sphere_eq = Eq((x - x1) ** 2 + (y - y1) ** 2 + (z - z1) ** 2, sphere_radius**2)
277
+
278
+ # solve
279
+ t_values = solve(sphere_eq, t)
280
+ intersections = [
281
+ np.array(
282
+ [float(x.subs(t, t_val)), float(y.subs(t, t_val)), float(z.subs(t, t_val))]
283
+ )
284
+ for t_val in t_values
285
+ ]
286
+ return list(zip(t_values, intersections))
287
+
288
+
289
+ def _project_point_on_line(
290
+ point_a: npt.ArrayLike, direction_vector: npt.ArrayLike, point_p: npt.ArrayLike
291
+ ) -> npt.NDArray:
292
+ A = np.array(point_a)
293
+ n = np.array(direction_vector)
294
+ P = np.array(point_p)
295
+ print(A.dtype, n.dtype, P.dtype)
296
+
297
+ AP = P - A
298
+ projection = A + np.dot(AP, n) / np.dot(n, n) * n
299
+ return projection
@@ -97,14 +97,19 @@ URL_CNG_VERSION = (
97
97
  )
98
98
  API_NEURON_MAX_SIZE = 500
99
99
 
100
- # about 1.1 GB and 18 GB in version 8.5.25 released in 2023-08-01
101
100
  KB = 1024
102
101
  MB = 1024 * KB
103
102
  GB = 1024 * MB
103
+
104
+ # Test version: 8.5.25 (2023-08-01)
105
+ # About 1.1 GB and 18 GB
106
+ # No ETAs for future version
104
107
  SIZE_METADATA = 2 * GB
105
108
  SIZE_DATA = 20 * GB
106
109
 
107
110
  # fmt:off
111
+ # Test version: 8.5.25 (2023-08-01)
112
+ # No ETAs for future version
108
113
  invalid_ids = [
109
114
  # bad file
110
115
  81062, 86970, 79791,
@@ -171,7 +176,7 @@ def neuromorpho_convert_lmdb_to_swc(
171
176
  See Also
172
177
  --------
173
178
  neuromorpho_is_valid :
174
- Recommend filter function, use `where=neuromorpho_is_valid`
179
+ Recommended filter function, try `where=neuromorpho_is_valid`
175
180
  """
176
181
  import lmdb
177
182
  from tqdm import tqdm
swcgeom/utils/renderer.py CHANGED
@@ -9,7 +9,7 @@ import numpy.typing as npt
9
9
  from matplotlib import cm
10
10
  from matplotlib.axes import Axes
11
11
  from matplotlib.collections import LineCollection, PatchCollection
12
- from matplotlib.colors import Normalize
12
+ from matplotlib.colors import Colormap, Normalize
13
13
  from matplotlib.figure import Figure
14
14
  from matplotlib.patches import Circle
15
15
  from typing_extensions import Self
@@ -208,14 +208,15 @@ def draw_circles(
208
208
  *,
209
209
  y_min: Optional[float] = None,
210
210
  y_max: Optional[float] = None,
211
- cmap: str = "viridis",
211
+ cmap: str | Colormap = "viridis",
212
212
  ) -> PatchCollection:
213
213
  """Draw a sequential of circles."""
214
+
214
215
  y_min = y.min() if y_min is None else y_min
215
216
  y_max = y.max() if y_max is None else y_max
216
217
  norm = Normalize(y_min, y_max)
217
218
 
218
- color_map = cm.get_cmap(name=cmap)
219
+ color_map = cmap if isinstance(cmap, Colormap) else cm.get_cmap(name=cmap)
219
220
  colors = color_map(norm(y))
220
221
 
221
222
  circles = [
@@ -212,6 +212,31 @@ def to_homogeneous(xyz: npt.ArrayLike, w: float) -> npt.NDArray[np.float32]:
212
212
  Parameters
213
213
  ----------
214
214
  xyz : ArrayLike
215
+ Coordinate of shape (..., 3)
216
+ w : float
217
+ w of homogeneous coordinate, 1 for dot, 0 for vector.
218
+
219
+ Returns
220
+ -------
221
+ xyz4 : npt.NDArray[np.float32]
222
+ Array of shape (..., 4)
223
+ """
224
+ xyz = np.array(xyz)
225
+ if xyz.ndim == 1:
226
+ return _to_homogeneous(xyz[None, ...], w)[0]
227
+
228
+ shape = xyz.shape[:-1]
229
+ xyz = xyz.reshape(-1, xyz.shape[-1])
230
+ xyz4 = _to_homogeneous(xyz, w).reshape(*shape, 4)
231
+ return xyz4
232
+
233
+
234
+ def _to_homogeneous(xyz: npt.NDArray, w: float) -> npt.NDArray[np.float32]:
235
+ """Fill xyz to homogeneous coordinates.
236
+
237
+ Parameters
238
+ ----------
239
+ xyz : npt.NDArray
215
240
  Coordinate of shape (N, 3)
216
241
  w : float
217
242
  w of homogeneous coordinate, 1 for dot, 0 for vector.
@@ -221,10 +246,10 @@ def to_homogeneous(xyz: npt.ArrayLike, w: float) -> npt.NDArray[np.float32]:
221
246
  xyz4 : npt.NDArray[np.float32]
222
247
  Array of shape (N, 4)
223
248
  """
224
- xyz = np.array(xyz)
225
249
  if xyz.shape[1] == 4:
226
250
  return xyz
227
251
 
252
+ assert xyz.shape[1] == 3
228
253
  filled = np.full((xyz.shape[0], 1), fill_value=w)
229
254
  xyz4 = np.concatenate([xyz, filled], axis=1)
230
255
  return xyz4
@@ -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
@@ -1,13 +1,14 @@
1
1
  swcgeom/__init__.py,sha256=z88Zwcjv-ii7c7dYd9QPg9XrUVorQjtrgGbQCsEnQhc,265
2
- swcgeom/_version.py,sha256=HJyq47ePtlF3WPYAhFFp5LPsrHyF6oju_vSEfklwWro,413
2
+ swcgeom/_version.py,sha256=Y4u7iBqF7QJAbpBNSFA2tk5t2mrMGrI-nonxUAhVkPU,413
3
3
  swcgeom/analysis/__init__.py,sha256=c8X_oMxaOijEVjHexFseYs49WMalG_mSxmlQZr5CZeI,339
4
4
  swcgeom/analysis/branch_features.py,sha256=s6PTMwwvxrVtZXRZlQbUSIw4M9-1IG63kf-Nxc0tMB0,1958
5
- swcgeom/analysis/feature_extractor.py,sha256=HKVHCnNQsAgnyX1BimEDUX0q-caZ0_VZ8wqPV7lC_R4,13750
5
+ swcgeom/analysis/feature_extractor.py,sha256=coe07_bJau96BkimcnXzuf4KqjY5_QRLwqaumFsu_tQ,14031
6
6
  swcgeom/analysis/node_features.py,sha256=fevnyrF-t4PX39ifLypiDW6EUWB8i-a3PpBnQZU3VOc,3407
7
7
  swcgeom/analysis/path_features.py,sha256=iE21HBxAoGLxk_qK7MBwQhyUOBqNPcnk4urVHr9SVqk,889
8
- swcgeom/analysis/sholl.py,sha256=QlPb2aCSzNCXmQkVFH_W1GRATPIngTZdNtZgBXvFEHk,6259
8
+ swcgeom/analysis/sholl.py,sha256=9hSW8rZm1gvSIgcEZg8IVPT8kzBgBfwqbwP4E8R7L44,6390
9
9
  swcgeom/analysis/trunk.py,sha256=L2tjUIUmrRQpah_W3ZETGWd16bDXJ5F8Sk2XBNGms0Q,5558
10
10
  swcgeom/analysis/visualization.py,sha256=mKOpzTPkLpr1ggGL1MZPZRTG92GEg4idLT4eN5z5KOs,5654
11
+ swcgeom/analysis/volume.py,sha256=bMQ9N9Ow1-h27SLQ9AzUm9dyF_jhoTNBMNVqQWDcHMw,2103
11
12
  swcgeom/core/__init__.py,sha256=ZUudZavxAIUU6Q0lBHrQ4ybmL5lBfvzyYsTtpuih9wg,332
12
13
  swcgeom/core/branch.py,sha256=uuJCxaByRu-OdDZVWEffSFcmZWY-6ZWUhHN1M2Awj1s,3980
13
14
  swcgeom/core/branch_tree.py,sha256=sN0viBVg5A4r9dMCkGNAaVttrdR4bEoZZBbHZFKdXj4,1892
@@ -22,8 +23,8 @@ swcgeom/core/tree_utils_impl.py,sha256=5Cb63ziVVLADnkjfuq1T-ePw2TQQ5TKk4gcPZR6f_
22
23
  swcgeom/core/swc_utils/__init__.py,sha256=qghRxjtzvq5KKfN4HhvLpZNsGPfZQu-Jj2x62_5-TbQ,575
23
24
  swcgeom/core/swc_utils/assembler.py,sha256=RoMJ3RjLC4O7mk62QxXVTQ5SUHagrFmpEw3nOnnqeJo,4563
24
25
  swcgeom/core/swc_utils/base.py,sha256=huVxjuMLlTHbEb-KSEFDLgU0Ss3723t2Gr4Z_gQtl00,4737
25
- swcgeom/core/swc_utils/checker.py,sha256=piyvIvskJi7SoyURLZV68q1mDX7FYHHf5bW2BPbc-QM,2092
26
- swcgeom/core/swc_utils/io.py,sha256=-O2lIATRfcK_eQ_jkJN790oieoqY4pwaNEqhlCnevFM,6434
26
+ swcgeom/core/swc_utils/checker.py,sha256=E72GtLZ_1IqQQ7aWQGs0dZ3Z609__bw3EYQqeWrk-EI,2657
27
+ swcgeom/core/swc_utils/io.py,sha256=6_--Qoe8kDja4PWsjwqRAvPJZNMFILFgauHaeWeGikU,6444
27
28
  swcgeom/core/swc_utils/normalizer.py,sha256=_Ysi8bSJ2JBnIGB8o6BvAg2mcz6xuJp9rgNLZqpLuR8,5083
28
29
  swcgeom/core/swc_utils/subtree.py,sha256=bd4XOLmRDfQSn_ktfQM3Hn8ONpCuZ_TdTWhE9-7QXW4,1999
29
30
  swcgeom/images/__init__.py,sha256=QBP1ZGGo2nWAcV7Krz-vbvW_jN4ChqXrrpoScXcUURs,96
@@ -41,18 +42,20 @@ swcgeom/transforms/path.py,sha256=Gk2iunGQMX7vE83bdo8xoDO-KAT1Vvep0iZs7oFLzFQ,10
41
42
  swcgeom/transforms/population.py,sha256=ZrKfMAMx4l729f-JLgw0dnGIPtPUoV0ZZoNNyA5cBw8,826
42
43
  swcgeom/transforms/tree.py,sha256=Q5OnSti0ZeTNb-WpA_UZsDN7dhLN8YweEF_Siyrj66c,6420
43
44
  swcgeom/transforms/tree_assembler.py,sha256=UZ9OUg1bQNecYIY_7ippg3S8gpuoi617ZUE0jg6BrQE,3177
44
- swcgeom/utils/__init__.py,sha256=g0bfRbRa4iC_ZovrJgQfEmP6mclMt2vYlZiaAZA5Rqg,306
45
+ swcgeom/utils/__init__.py,sha256=Ys04ObDfoNOX0mft2s4PGTpgelkdgwuM0qfeZRSx6VM,382
45
46
  swcgeom/utils/debug.py,sha256=qay2qJpViLX82mzxdndxQFn-pi1vaEj9CbLGuGt8Y9k,465
46
47
  swcgeom/utils/download.py,sha256=By2qZezo6h1Ke_4YpSIhDgcisOrpjVqRmNzbhynC2xs,2834
48
+ swcgeom/utils/dsu.py,sha256=3aCbtpnl_D0OXnowTS8-kuwnCS4BKBYL5ECiFQ1fUW8,1435
47
49
  swcgeom/utils/ellipse.py,sha256=LB3q5CIy75GEUdTauIpKySwIHaDrwXzzkBhOCnjJ8Vw,3259
48
- swcgeom/utils/file.py,sha256=qrJYhTscJqQAbJ-R0vpHPDP9Hjf9u-woY4l4lQLLG8E,2477
49
- swcgeom/utils/neuromorpho.py,sha256=PDSNVgXqKWO4qQI-VjtwL10--cVzxp7k7OXGHihRtdw,14328
50
+ swcgeom/utils/file.py,sha256=1hchQDsPgn-i-Vz5OQtcogxav_ajCQ_OaEZCLmqczRg,2515
51
+ swcgeom/utils/geometry_object.py,sha256=y3Ikg0ywYQ6c_KKZOb0oq5ovHhuFiwI_VH-f4tAWZAI,8520
52
+ swcgeom/utils/neuromorpho.py,sha256=CDK2tUM2pNwHv_lEserHhQs_VlY3Rn557-jtV63EFlk,14420
50
53
  swcgeom/utils/numpy_helper.py,sha256=A-F-eFdGktCHVAQ_HcXiFB3Y1YhhSNfAmtOl8483Dvo,1292
51
- swcgeom/utils/renderer.py,sha256=AEbPSmIxxrHf4y3Y6FtLwVWgq8akh-AQls1LbpqcXmA,7228
54
+ swcgeom/utils/renderer.py,sha256=xHVZ06Z1MeKBPC3nKzuwA1HryzR0ga79y6johZA4-q0,7290
52
55
  swcgeom/utils/sdf.py,sha256=cmkHwdnh33u1gAXlzol5u9ZI3_SdMoMiY4pIkOgEcK0,5206
53
- swcgeom/utils/transforms.py,sha256=4U6flX8lfnMoAAR-uc4uSYjSd2vg-AOWGLgZ6_RVxJU,6352
54
- swcgeom-0.12.0.dist-info/LICENSE,sha256=aiTJ_1to1Xx6PaByy-pSXg42VYzE4FvF6GIt69WSDDI,202
55
- swcgeom-0.12.0.dist-info/METADATA,sha256=lyjjYsqQr4AjKf0O1nhY9hOUMz9QxX3vpB9S0gLOLb8,2577
56
- swcgeom-0.12.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
57
- swcgeom-0.12.0.dist-info/top_level.txt,sha256=hmLyUXWS61Gxl07haswFEKKefYPBVJYlUlol8ghNkjY,8
58
- swcgeom-0.12.0.dist-info/RECORD,,
56
+ swcgeom/utils/transforms.py,sha256=PmP5fL_iVguq4GR2aqXhM0TeCsiFVnrPZMZG6zLohrE,6983
57
+ swcgeom-0.13.0.dist-info/LICENSE,sha256=aiTJ_1to1Xx6PaByy-pSXg42VYzE4FvF6GIt69WSDDI,202
58
+ swcgeom-0.13.0.dist-info/METADATA,sha256=vsgLGIGz97A-V8n1DZsAhGAc-uVJuX_CdFbB2BCQZ5g,2605
59
+ swcgeom-0.13.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
60
+ swcgeom-0.13.0.dist-info/top_level.txt,sha256=hmLyUXWS61Gxl07haswFEKKefYPBVJYlUlol8ghNkjY,8
61
+ swcgeom-0.13.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.41.2)
2
+ Generator: bdist_wheel (0.42.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5