swcgeom 0.19.4__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.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 +21 -0
  2. swcgeom/analysis/__init__.py +13 -0
  3. swcgeom/analysis/feature_extractor.py +454 -0
  4. swcgeom/analysis/features.py +218 -0
  5. swcgeom/analysis/lmeasure.py +750 -0
  6. swcgeom/analysis/sholl.py +201 -0
  7. swcgeom/analysis/trunk.py +183 -0
  8. swcgeom/analysis/visualization.py +191 -0
  9. swcgeom/analysis/visualization3d.py +81 -0
  10. swcgeom/analysis/volume.py +143 -0
  11. swcgeom/core/__init__.py +19 -0
  12. swcgeom/core/branch.py +129 -0
  13. swcgeom/core/branch_tree.py +65 -0
  14. swcgeom/core/compartment.py +107 -0
  15. swcgeom/core/node.py +130 -0
  16. swcgeom/core/path.py +155 -0
  17. swcgeom/core/population.py +341 -0
  18. swcgeom/core/swc.py +247 -0
  19. swcgeom/core/swc_utils/__init__.py +19 -0
  20. swcgeom/core/swc_utils/assembler.py +35 -0
  21. swcgeom/core/swc_utils/base.py +180 -0
  22. swcgeom/core/swc_utils/checker.py +107 -0
  23. swcgeom/core/swc_utils/io.py +204 -0
  24. swcgeom/core/swc_utils/normalizer.py +163 -0
  25. swcgeom/core/swc_utils/subtree.py +70 -0
  26. swcgeom/core/tree.py +384 -0
  27. swcgeom/core/tree_utils.py +277 -0
  28. swcgeom/core/tree_utils_impl.py +58 -0
  29. swcgeom/images/__init__.py +9 -0
  30. swcgeom/images/augmentation.py +149 -0
  31. swcgeom/images/contrast.py +87 -0
  32. swcgeom/images/folder.py +217 -0
  33. swcgeom/images/io.py +578 -0
  34. swcgeom/images/loaders/__init__.py +8 -0
  35. swcgeom/images/loaders/pbd.cpython-311-x86_64-linux-gnu.so +0 -0
  36. swcgeom/images/loaders/pbd.pyx +523 -0
  37. swcgeom/images/loaders/raw.cpython-311-x86_64-linux-gnu.so +0 -0
  38. swcgeom/images/loaders/raw.pyx +183 -0
  39. swcgeom/transforms/__init__.py +20 -0
  40. swcgeom/transforms/base.py +136 -0
  41. swcgeom/transforms/branch.py +223 -0
  42. swcgeom/transforms/branch_tree.py +74 -0
  43. swcgeom/transforms/geometry.py +270 -0
  44. swcgeom/transforms/image_preprocess.py +107 -0
  45. swcgeom/transforms/image_stack.py +219 -0
  46. swcgeom/transforms/images.py +206 -0
  47. swcgeom/transforms/mst.py +183 -0
  48. swcgeom/transforms/neurolucida_asc.py +498 -0
  49. swcgeom/transforms/path.py +56 -0
  50. swcgeom/transforms/population.py +36 -0
  51. swcgeom/transforms/tree.py +265 -0
  52. swcgeom/transforms/tree_assembler.py +161 -0
  53. swcgeom/utils/__init__.py +18 -0
  54. swcgeom/utils/debug.py +23 -0
  55. swcgeom/utils/download.py +119 -0
  56. swcgeom/utils/dsu.py +58 -0
  57. swcgeom/utils/ellipse.py +131 -0
  58. swcgeom/utils/file.py +90 -0
  59. swcgeom/utils/neuromorpho.py +581 -0
  60. swcgeom/utils/numpy_helper.py +70 -0
  61. swcgeom/utils/plotter_2d.py +134 -0
  62. swcgeom/utils/plotter_3d.py +35 -0
  63. swcgeom/utils/renderer.py +145 -0
  64. swcgeom/utils/sdf.py +324 -0
  65. swcgeom/utils/solid_geometry.py +154 -0
  66. swcgeom/utils/transforms.py +367 -0
  67. swcgeom/utils/volumetric_object.py +483 -0
  68. swcgeom-0.19.4.dist-info/METADATA +86 -0
  69. swcgeom-0.19.4.dist-info/RECORD +72 -0
  70. swcgeom-0.19.4.dist-info/WHEEL +6 -0
  71. swcgeom-0.19.4.dist-info/licenses/LICENSE +201 -0
  72. swcgeom-0.19.4.dist-info/top_level.txt +1 -0
@@ -0,0 +1,750 @@
1
+ # SPDX-FileCopyrightText: 2022 - 2025 Zexin Yuan <pypi@yzx9.xyz>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ """L-Measure analysis."""
6
+
7
+ import math
8
+ from typing import Literal
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ from swcgeom.core import Branch, Compartment, Node, Tree
14
+
15
+ __all__ = ["LMeasure"]
16
+
17
+
18
+ class LMeasure:
19
+ """L-Measure analysis.
20
+
21
+ The L-Measure analysis provide a set of morphometric features for multiple levels
22
+ analysis, as described in the paper [1]_
23
+
24
+ References:
25
+ .. [1] Scorcioni R, Polavaram S, Ascoli GA. L-Measure: a web-accessible tool for
26
+ the analysis, comparison and search of digital reconstructions of neuronal
27
+ morphologies. Nat Protoc. 2008;3(5):866-76. doi: 10.1038/nprot.2008.51.
28
+ PMID: 18451794; PMCID: PMC4340709.
29
+
30
+ See Also:
31
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/index.htm
32
+ """
33
+
34
+ def __init__(self, compartment_point: Literal[0, -1] = -1):
35
+ """
36
+ Args:
37
+ compartment_point:
38
+ The point of the compartment to be used for the measurements. If 0, the
39
+ first point of the compartment is used. If -1, the last point of the
40
+ compartment is used.
41
+ """
42
+ super().__init__()
43
+ self.compartment_point = compartment_point
44
+
45
+ # Topological measurements
46
+
47
+ def n_stems(self, tree: Tree) -> int:
48
+ """Number of stems that is connected to soma.
49
+
50
+ This function returns the number of stems attached to the soma. When the type
51
+ of the Compartment changes from type=1 to others it is labeled a stem. These
52
+ stems can also be considered as independent subtrees for subtree level analysis.
53
+
54
+ See Also:
55
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/N_stems.htm
56
+ """
57
+ return len(tree.soma().children())
58
+
59
+ def n_bifs(self, tree: Tree) -> int:
60
+ """Number of bifurcations.
61
+
62
+ This function returns the number of bifurcations for the given input neuron. A
63
+ bifurcation point has two daughters.
64
+
65
+ See Also:
66
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/N_bifs.htm
67
+ """
68
+ return len(tree.get_furcations())
69
+
70
+ def n_branch(self, tree: Tree) -> int:
71
+ """Number of branches.
72
+
73
+ This function returns the number of branches in the given input neuron. A
74
+ branch is one or more compartments that lie between two branching points or
75
+ between one branching point and a termination point.
76
+
77
+ See Also:
78
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/N_branch.htm
79
+ """
80
+ return len(tree.get_branches())
81
+
82
+ def n_tips(self, tree: Tree) -> int:
83
+ """Number of terminal tips.
84
+
85
+ This function returns the number of terminal tips for the given input neuron.
86
+ This function counts the number of compartments that terminate as terminal
87
+ endpoints.
88
+
89
+ See Also:
90
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/N_tips.htm
91
+ """
92
+ return len(tree.get_tips())
93
+
94
+ def terminal_segment(self) -> int:
95
+ """Terminal Segment.
96
+
97
+ TerminalSegment is the branch that ends as a terminal branch. This function
98
+ returns "1" for all the compartments in the terminal branch.
99
+
100
+ See Also:
101
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/TerminalSegment.htm
102
+ """
103
+ raise NotImplementedError()
104
+
105
+ def branch_pathlength(self, branch: Branch) -> float:
106
+ """Length of the branch.
107
+
108
+ This function returns the sum of the length of all compartments forming the
109
+ giveN_branch.
110
+
111
+ See Also:
112
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Branch_pathlength.htm
113
+ """
114
+ # ? as a topological measurement, this should accept a tree
115
+ return branch.length()
116
+
117
+ def contraction(self, branch: Branch) -> float:
118
+ """Contraction of the branch.
119
+
120
+ This function returns the ratio between Euclidean distance of a branch and its
121
+ path length.
122
+
123
+ See Also:
124
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Contraction.htm
125
+ """
126
+ # ? as a topological measurement, this should accept a tree
127
+ euclidean = branch[0].distance(branch[-1])
128
+ return euclidean / branch.length()
129
+
130
+ def fragmentation(self, branch: Branch) -> int:
131
+ """Number of compartments.
132
+
133
+ This function returns the total number of compartments that constitute a branch
134
+ between two bifurcation points or between a bifurcation point and a terminal tip.
135
+
136
+ See Also:
137
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Fragmentation.htm
138
+ """
139
+ # ? as a topological measurement, this should accept a tree
140
+ return branch.number_of_edges()
141
+
142
+ def partition_asymmetry(self, n: Tree.Node) -> float:
143
+ """Partition asymmetry.
144
+
145
+ This is computed only on bifurcation. If `n1` is the number of tips on the left
146
+ and `n2` on the right. Asymmetry return `abs(n1-n2)/(n1+n2-2)`.
147
+
148
+ See Also:
149
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Partition_asymmetry.htm
150
+ """
151
+
152
+ children = n.children()
153
+ assert len(children) == 2, (
154
+ "Partition asymmetry is only defined for bifurcations"
155
+ )
156
+ n1 = len(children[0].subtree().get_tips())
157
+ n2 = len(children[1].subtree().get_tips())
158
+ if n1 == n2:
159
+ return 0
160
+ return abs(n1 - n2) / (n1 + n2 - 2)
161
+
162
+ def fractal_dim(self):
163
+ """Fractal dimension.
164
+
165
+ Fractal dimension (D) of neuronal branches is computedas the slope of linear
166
+ fit of regression line obtained from the log-log plot of Path distance vs
167
+ Euclidean distance.
168
+
169
+ This method of measuring the fractal follows the reference given below by
170
+ Marks & Burke, J Comp Neurol. 2007. [1]_
171
+ - When D = 1, the particle moves in a straight line.
172
+ - When D = 2, the motion is a space-filling random walk
173
+ - When D is only slightly larger than 1, the particle trajectory resembles a
174
+ country road or a dendrite branch.
175
+
176
+ References:
177
+ .. [1] Marks WB, Burke RE. Simulation of motoneuron morphology in three
178
+ dimensions. I. Building individual dendritic trees. J Comp Neurol. 2007 Aug
179
+ 10;503(5):685-700. doi: 10.1002/cne.21418. PMID: 17559104.
180
+
181
+ See Also:
182
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Fractal_Dim.htm
183
+ """
184
+ raise NotImplementedError()
185
+
186
+ # Branch level measurements (thickness and taper)
187
+
188
+ def taper_1(self, branch: Branch) -> float:
189
+ """The Burke Taper.
190
+
191
+ This function returns the Burke Taper. This function is measured between two
192
+ bifurcation points.
193
+
194
+ See Also:
195
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Taper_1.htm
196
+ """
197
+ da, db = 2 * branch[0].r, 2 * branch[-1].r
198
+ return (da - db) / branch.length()
199
+
200
+ def taper_2(self, branch: Branch) -> float:
201
+ """The Hillman taper.
202
+
203
+ This function returns the Hillman taper. This is measured between two
204
+ bifurcation points.
205
+
206
+ See Also:
207
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Taper_2.htm
208
+ """
209
+ da, db = 2 * branch[0].r, 2 * branch[-1].r
210
+ return (da - db) / da
211
+
212
+ def daughter_ratio(self):
213
+ """Daughter ratio.
214
+
215
+ The function returns the ratio between the bigger daughter and the other one. A
216
+ daughter is the next immediate compartment connected to a bifurcation point.
217
+
218
+ See Also:
219
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Daughter_Ratio.htm
220
+ """
221
+ raise NotImplementedError()
222
+
223
+ def parent_daughter_ratio(self) -> float:
224
+ """Parent daughter ratio.
225
+
226
+ This function returns the ratio between the diameter of a daughter and its
227
+ father. One values for each daughter is returned at each bifurcation point.
228
+
229
+ See Also:
230
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Parent_Daughter_Ratio.htm
231
+ """
232
+ raise NotImplementedError()
233
+
234
+ def rall_power(self, bif: Tree.Node) -> float:
235
+ """Rall Power.
236
+
237
+ Rall value is computed as the best value that fits the equation
238
+ `(Bif_Dia)^Rall=(Daughter1_dia^Rall+Daughter2_dia^Rall)`. According to Rall’s
239
+ rule we compute rall’s power by linking the diameter of two daughter branches
240
+ to the diameter of the bifurcating parent. We compute the best fit rall’s power
241
+ within the boundary values of `[0, 5]` at incremental steps of 1000
242
+ compartments. The final rall value is the idealistic n value that can propagate
243
+ the signal transmission without loss from the starting point to the terminal
244
+ point in a cable model assumption.
245
+
246
+ See Also:
247
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Rall_Power.htm
248
+ """
249
+ rall_power, _, _, _ = self._rall_power(bif)
250
+ return rall_power
251
+
252
+ def _rall_power_d(self, bif: Tree.Node) -> tuple[float, float, float]:
253
+ children = bif.children()
254
+ assert len(children) == 2, "Rall Power is only defined for bifurcations"
255
+ parent = bif.parent()
256
+ assert parent is not None, "Rall Power is not defined for root"
257
+
258
+ dp = 2 * parent.r
259
+ da, db = 2 * children[0].r, 2 * children[1].r
260
+ return dp, da, db
261
+
262
+ def _rall_power(self, bif: Tree.Node) -> tuple[float, float, float, float]:
263
+ dp, da, db = self._rall_power_d(bif)
264
+ start, stop, step = 0, 5, 5 / 1000
265
+ xs = np.arange(start, stop, step)
266
+ ys = (da**xs + db**xs) - dp**xs
267
+ return xs[np.argmin(ys)], dp, da, db
268
+
269
+ def pk(self, bif: Tree.Node) -> float:
270
+ """Ratio of rall power increased.
271
+
272
+ After computing the average value for Rall_Power, this function returns the
273
+ ratio of `(d1^rall+d2^rall)/(bifurcDiam^rall)`.
274
+
275
+ See Also:
276
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Pk.htm
277
+ """
278
+ rall_power, dp, da, db = self._rall_power(bif)
279
+ return (da**rall_power + db**rall_power) / dp**rall_power
280
+
281
+ def pk_classic(self, bif: Tree.Node) -> float:
282
+ """Ratio of rall power increased with fixed rall power 1.5.
283
+
284
+ This function returns the same value as Pk, but with `Rall_Power` sets to 1.5.
285
+
286
+ See Also:
287
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Pk_classic.htm
288
+ """
289
+ dp, da, db = self._rall_power_d(bif)
290
+ rall_power = 1.5
291
+ return (da**rall_power + db**rall_power) / dp**rall_power
292
+
293
+ def pk_2(self, bif: Tree.Node) -> float:
294
+ """Ratio of rall power increased with fixed rall power 2.
295
+
296
+ This function returns the same value as Pk, but with `Rall_Power` sets to 2 .
297
+
298
+ See Also:
299
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Pk_2.htm
300
+ """
301
+ dp, da, db = self._rall_power_d(bif)
302
+ rall_power = 2
303
+ return (da**rall_power + db**rall_power) / dp**rall_power
304
+
305
+ def bif_ampl_local(self, bif: Tree.Node) -> float:
306
+ """Bifurcation angle.
307
+
308
+ Given a bifurcation, this function returns the angle between the first two
309
+ compartments (in degree).
310
+
311
+ See Also:
312
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Bif_ampl_local.htm
313
+ """
314
+ v1, v2 = self._bif_vector_local(bif)
315
+ return np.degrees(angle(v1, v2))
316
+
317
+ def bif_ampl_remote(self, bif: Tree.Node) -> float:
318
+ """Bifurcation angle.
319
+
320
+ This function returns the angle between two bifurcation points or between
321
+ bifurcation point and terminal point or between two terminal points.
322
+
323
+ See Also:
324
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Bif_ampl_remote.htm
325
+ """
326
+ v1, v2 = self._bif_vector_remote(bif)
327
+ return np.degrees(angle(v1, v2))
328
+
329
+ def bif_tilt_local(self, bif: Tree.Node) -> float:
330
+ """Bifuarcation tilt.
331
+
332
+ This function returns the angle between the previous compartment of bifurcating
333
+ father and the two daughter compartments of the same bifurcation. The smaller
334
+ of the two angles is returned as the result.
335
+
336
+ Tilt is measured as outer angle between parent and child compartments.
337
+ L-Measure returns smaller angle of the two children, but intuitively this can
338
+ be viewed as the angle of deflection of the parent orientation compared to the
339
+ orientation of the mid line of the bifurcation amplitude angle.
340
+
341
+ (i.e.) new tilt(NT) = pi - 1/2 Bif_amp_remote - old tilt(OT)
342
+
343
+ See Also:
344
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Bif_tilt_local.htm
345
+ """
346
+ parent = bif.parent()
347
+ assert parent is not None, "Bifurcation tilt is not defined for root"
348
+ v = parent.xyz() - bif.xyz()
349
+ v1, v2 = self._bif_vector_local(bif)
350
+
351
+ angle1 = np.degrees(angle(v, v1))
352
+ angle2 = np.degrees(angle(v, v2))
353
+ return min(angle1, angle2)
354
+
355
+ def bif_tilt_remote(self, bif: Tree.Node) -> float:
356
+ """Bifuarcation tilt.
357
+
358
+ This function returns the angle between the previous father node of the current
359
+ bifurcating father and its two daughter nodes. A node is a terminating point or
360
+ a bifurcation point or a root point. Smaller of the two angles is returned as
361
+ the result. This angle is not computed for the root node.
362
+
363
+ Tilt is measured as outer angle between parent and child compartments. L-Measure
364
+ returns smaller angle of the two children, but intuitively this can be viewed as
365
+ the angle of deflection of the parent orientation compared to the orientation of
366
+ the mid line of the bifurcation amplitude angle.
367
+
368
+ (i.e.) new tilt(NT) = pi - 1/2 Bif_amp_remote - old tilt(OT)
369
+
370
+ See Also:
371
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Bif_tilt_remote.htm
372
+ """
373
+ parent = bif.parent()
374
+ assert parent is not None, "Bifurcation tilt is not defined for root"
375
+ v = parent.xyz() - bif.xyz()
376
+ v1, v2 = self._bif_vector_remote(bif)
377
+
378
+ angle1 = np.degrees(angle(v, v1))
379
+ angle2 = np.degrees(angle(v, v2))
380
+ return min(angle1, angle2)
381
+
382
+ def bif_torque_local(self, bif: Tree.Node) -> float:
383
+ """Bifurcation torque.
384
+
385
+ This function returns the angle between the plane of previous bifurcation and
386
+ the current bifurcation. Bifurcation plane is identified by the two daughter
387
+ compartments leaving the bifurcation.
388
+
389
+ A torque is the inner angle measured between two planes (current & parent) of
390
+ bifurcations. Plane DCE has current bifurcation and plane CAB has parent
391
+ bifurcation. Although LM returns the absolute angle between CAB and DCE as the
392
+ result, it must be noted that intuitively what we are measuring is relative
393
+ change in the angle of second plane (DCE) with respect to the first plane (CAB).
394
+ Therefore, angles > 90 degrees should be considered as pi - angle.
395
+
396
+ i.e. in Example1 tilt = 30deg. and in Example2 tilt = 180-110 = 70deg.
397
+
398
+ See Also:
399
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Bif_torque_local.htm
400
+ """
401
+ parent = bif.parent()
402
+ assert parent is not None, "Bifurcation torque is not defined for root"
403
+ idx = parent.branch().origin_id()[0]
404
+ n = parent.branch().attach.node(idx)
405
+
406
+ v1, v2 = self._bif_vector_local(n)
407
+ u1, u2 = self._bif_vector_local(bif)
408
+
409
+ n1, n2 = np.cross(v1, v2), np.cross(u1, u2)
410
+ theta_deg = np.degrees(angle(n1, n2))
411
+ raise theta_deg
412
+
413
+ def bif_torque_remote(self, bif: Tree.Node) -> float:
414
+ """Bifurcation torque.
415
+
416
+ This function returns the angle between, current plane of bifurcation and
417
+ previous plane of bifurcation. This is a bifurcation level metric and from the
418
+ figure, the current plane of bifurcation is formed between C D and E where all
419
+ three are bifurcation points and the previous plane is formed between A B and
420
+ C bifurcation points.
421
+
422
+ A torque is the inner angle measured between two planes (current & parent) of
423
+ bifurcations. Plane DCE has current bifurcation and plane CAB has parent
424
+ bifurcation. Although LM returns the absolute angle between CAB and DCE as the
425
+ result, it must be noted that intuitively what we are measuring is relative
426
+ change in the angle of second plane (DCE) with respect to the first plane (CAB).
427
+ Therefore, angles > 90 degrees should be considered as pi - angle.
428
+
429
+ i.e. in Example1 tilt = 30deg. and in Example2 tilt = 180-110 = 70deg.
430
+
431
+ See Also:
432
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Bif_torque_remote.htm
433
+ """
434
+ parent = bif.parent()
435
+ assert parent is not None, "Bifurcation torque is not defined for root"
436
+ idx = parent.branch().origin_id()[0]
437
+ n = parent.branch().attach.node(idx)
438
+
439
+ v1, v2 = self._bif_vector_remote(n)
440
+ u1, u2 = self._bif_vector_remote(bif)
441
+
442
+ n1, n2 = np.cross(v1, v2), np.cross(u1, u2)
443
+ theta_deg = np.degrees(angle(n1, n2))
444
+ raise theta_deg
445
+
446
+ def _bif_vector_local(
447
+ self, bif: Tree.Node
448
+ ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
449
+ children = bif.children()
450
+ assert len(children) == 2, "Only defined for bifurcations"
451
+
452
+ v1 = children[0].xyz() - bif.xyz()
453
+ v2 = children[1].xyz() - bif.xyz()
454
+ return v1, v2
455
+
456
+ def _bif_vector_remote(
457
+ self, bif: Tree.Node
458
+ ) -> tuple[npt.NDArray[np.float32], npt.NDArray[np.float32]]:
459
+ children = bif.children()
460
+ assert len(children) == 2, "Only defined for bifurcations"
461
+
462
+ v1 = children[0].branch()[-1].xyz() - bif.xyz()
463
+ v2 = children[1].branch()[-1].xyz() - bif.xyz()
464
+ return v1, v2
465
+
466
+ def last_parent_diam(self, branch: Branch) -> float:
467
+ """The diameter of last bifurcation before the terminal tips.
468
+
469
+ This function returns the diameter of last bifurcation before the terminal tips.
470
+
471
+ See Also:
472
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Last_parent_diam.htm
473
+ """
474
+
475
+ raise NotImplementedError()
476
+
477
+ def diam_threshold(self, branch: Branch) -> float:
478
+ """
479
+
480
+ This function returns Diameter of first compartment after the last bifurcation
481
+ leading to a terminal tip.
482
+
483
+ See Also:
484
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Diam_threshold.htm
485
+ """
486
+ raise NotImplementedError()
487
+
488
+ def hillman_threshold(self, branch: Branch) -> float:
489
+ """Hillman Threshold.
490
+
491
+ Computes the weighted average between 50% of father and 25% of daughter
492
+ diameters of the terminal bifurcation.
493
+
494
+ See Also:
495
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/HillmanThreshold.htm
496
+ """
497
+ raise NotImplementedError()
498
+
499
+ # Compartment level geometrical measurements
500
+
501
+ def diameter(self, node: Node) -> float:
502
+ """This function returns diameter of each compartment the neuron.
503
+
504
+ See Also:
505
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Diameter.htm
506
+ """
507
+ return 2 * node.r
508
+
509
+ def diameter_pow(self, node: Node) -> float:
510
+ """Computes the diameter raised to the power 1.5 for each compartment.
511
+
512
+ See Also:
513
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Diameter_pow.htm
514
+ """
515
+ return self.diameter(node) ** 1.5
516
+
517
+ def length(self, compartment: Compartment) -> float:
518
+ """Length of the compartment.
519
+
520
+ This function returns the length of compartments by computing the distance
521
+ between the two end points of a compartment.
522
+
523
+ See Also:
524
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Length.htm
525
+ """
526
+ return compartment.length()
527
+
528
+ def surface(self, compartment: Compartment) -> float:
529
+ """This function returns surface of the compartment.
530
+
531
+ See Also:
532
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Surface.htm
533
+ """
534
+ p = compartment[self.compartment_point]
535
+ return cylinder_side_surface_area(p.r, compartment.length())
536
+
537
+ def section_area(self, node: Node) -> float:
538
+ """This function returns the SectionArea of the compartment.
539
+
540
+ See Also:
541
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/SectionArea.htm
542
+ """
543
+ return circle_area(node.r)
544
+
545
+ def volume(self, compartment: Compartment) -> float:
546
+ """This function returns the volume of the compartment.
547
+
548
+ See Also:
549
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Volume.htm
550
+ """
551
+ p = compartment[self.compartment_point]
552
+ return cylinder_volume(p.r, compartment.length())
553
+
554
+ def euc_distance(self, node: Tree.Node) -> float:
555
+ """Euclidean distance from compartment to soma.
556
+
557
+ This function returns the Euclidean distance of a compartment with respect to
558
+ soma.
559
+
560
+ See Also:
561
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/EucDistance.htm
562
+ """
563
+
564
+ soma = node.attach.soma()
565
+ return node.distance(soma)
566
+
567
+ def path_distance(self, node: Tree.Node) -> float:
568
+ """Path distance from compartment to soma.
569
+
570
+ This function returns the PathDistance of a compartment.
571
+
572
+ See Also:
573
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/PathDistance.htm
574
+ """
575
+ n = node
576
+ length = 0
577
+ while (parent := n.parent()) is not None:
578
+ length += n.distance(parent)
579
+ n = parent
580
+ return length
581
+
582
+ def branch_order(self, node: Tree.Node) -> int:
583
+ """The order of the compartment.
584
+
585
+ This function returns the order of the branch with respect to soma.
586
+
587
+ See Also:
588
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Branch_Order.htm
589
+ """
590
+ n = node
591
+ order = 0
592
+ while n is not None:
593
+ if n.is_furcation():
594
+ order += 1
595
+ n = n.parent()
596
+ return order
597
+
598
+ def terminal_degree(self, node: Tree.Node) -> int:
599
+ """The number of tips of the compartment.
600
+
601
+ This function gives the total number of tips that each compartment will
602
+ terminate into.
603
+
604
+ See Also:
605
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Terminal_degree.htm
606
+ """
607
+ return len(node.subtree().get_tips())
608
+
609
+ def helix(self, compartment: Tree.Compartment) -> float:
610
+ """Helix of the compartment.
611
+
612
+ The function computes the helix by choosing the 3 segments at a time (or four
613
+ points at a time) and computes the normal form on the 3 vectors to find the 4th
614
+ vector.
615
+
616
+ See Also:
617
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Helix.htm
618
+ """
619
+ n1 = compartment.attach.node(compartment.origin_id()[0])
620
+ n2 = compartment.attach.node(compartment.origin_id()[1])
621
+ parent = n1.parent()
622
+ assert parent is not None
623
+ grandparent = parent.parent()
624
+ assert grandparent is not None
625
+
626
+ a = n1.xyz() - parent.xyz()
627
+ b = parent.xyz() - grandparent.xyz()
628
+ c = n2.xyz() - n1.xyz()
629
+ return np.dot(np.cross(a, b), c) / (
630
+ 3 * np.linalg.norm(a) * np.linalg.norm(b) * np.linalg.norm(c)
631
+ )
632
+
633
+ # Whole-arbor measurements
634
+
635
+ def width(self, tree: Tree) -> float:
636
+ """With of the neuron.
637
+
638
+ Width is computed on the x-coordinates and it is the difference of minimum and
639
+ maximum x-values after eliminating the outer points on the either ends by using
640
+ the 95% approximation of the x-values of the given input neuron.
641
+
642
+ See Also:
643
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Width.htm
644
+ """
645
+ return max_filtered_difference(tree.x(), 95)
646
+
647
+ def height(self, tree: Tree) -> float:
648
+ """Height of the neuron.
649
+
650
+ Height is computed on the y-coordinates and it is the difference of minimum and
651
+ maximum y-values after eliminating the outer points on the either ends by using
652
+ the 95% approximation of the y-values of the given input neuron.
653
+
654
+ See Also:
655
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Height.htm
656
+ """
657
+ return max_filtered_difference(tree.y(), 95)
658
+
659
+ def depth(self, tree: Tree) -> float:
660
+ """Depth of the neuron.
661
+
662
+ Depth is computed on the x-coordinates and it is the difference of minimum and
663
+ maximum x-values after eliminating the outer points on the either ends by using
664
+ the 95% approximation of the x-values of the given input neuron.
665
+
666
+ See Also:
667
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Depth.htm
668
+ """
669
+ return max_filtered_difference(tree.z(), 95)
670
+
671
+ # Specific-arbor type measurements
672
+
673
+ def soma_surface(self, tree: Tree) -> float:
674
+ """Soma surface area.
675
+
676
+ This function computes the surface of the soma (Type=1). If the soma is
677
+ composed of just one compartment, then it uses the sphere assumption, otherwise
678
+ it returns the sum of the external cylindrical surfaces of compartments forming
679
+ the soma. There can be multiple soma's, one soma or no soma at all.
680
+
681
+ See Also:
682
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Soma_Surface.htm
683
+ """
684
+ return sphere_surface_area(tree.soma().r) # TODO: handle multiple soma
685
+
686
+ def type(self):
687
+ """The type of compartment.
688
+
689
+ This function returns type of compartment. Each compartment of the neuron is of
690
+ a particular type; soma = 1, axon = 2, basal dendrites = 3, apical dendrites= 4.
691
+ The type values are assigned directly from the given input neuron.
692
+
693
+ See Also:
694
+ L-Measure: http://cng.gmu.edu:8080/Lm/help/Type.htm
695
+ """
696
+ raise NotImplementedError()
697
+
698
+
699
+ def max_filtered_difference(
700
+ values: npt.NDArray[np.float32], percentile: float = 95
701
+ ) -> float:
702
+ assert 0 < percentile < 100, "Percentile must be between 0 and 100"
703
+ sorted = np.sort(values)
704
+ lower_index = int(len(sorted) * (100 - percentile) / 200)
705
+ upper_index = int(len(sorted) * (1 - (100 - percentile) / 200))
706
+ filtered = sorted[lower_index:upper_index]
707
+ difference = filtered[-1] - filtered[0]
708
+ return difference
709
+
710
+
711
+ def circle_area(r: float) -> float:
712
+ return math.pi * r**2
713
+
714
+
715
+ def sphere_surface_area(r: float):
716
+ return 4 * math.pi * r**2
717
+
718
+
719
+ def cylinder_volume(r: float, h: float) -> float:
720
+ return math.pi * r**2 * h
721
+
722
+
723
+ def cylinder_side_surface_area(r: float, h: float):
724
+ return 2 * math.pi * r * h
725
+
726
+
727
+ def pill_surface_area(ra: float, rb: float, h: float) -> float:
728
+ lateral_area = math.pi * (ra + rb) * math.sqrt((ra - rb) ** 2 + h**2)
729
+ top_hemisphere_area = 2 * math.pi * ra**2
730
+ bottom_hemisphere_area = 2 * math.pi * rb**2
731
+ total_area = lateral_area + top_hemisphere_area + bottom_hemisphere_area
732
+ return total_area
733
+
734
+
735
+ # TODO: move to `utils`
736
+ def angle(a: npt.ArrayLike, b: npt.ArrayLike) -> float:
737
+ """Get the angle of vectors.
738
+
739
+ Returns:
740
+ angle: Angle [0, PI) in radians.
741
+ """
742
+
743
+ a, b = np.array(a), np.array(b)
744
+ if np.linalg.norm(a) == 0 or np.linalg.norm(b) == 0:
745
+ raise ValueError("Input vectors must not be zero vectors.")
746
+
747
+ costheta = np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
748
+ costheta = np.clip(costheta, -1, 1) # avoid numerical errors
749
+ theta = np.arccos(costheta)
750
+ return theta