swcgeom 0.14.0__py3-none-any.whl → 0.16.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.

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