swcgeom 0.20.0__cp312-cp312-macosx_15_0_arm64.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/__init__.py +21 -0
- swcgeom/analysis/__init__.py +13 -0
- swcgeom/analysis/feature_extractor.py +454 -0
- swcgeom/analysis/features.py +218 -0
- swcgeom/analysis/lmeasure.py +750 -0
- swcgeom/analysis/sholl.py +201 -0
- swcgeom/analysis/trunk.py +183 -0
- swcgeom/analysis/visualization.py +191 -0
- swcgeom/analysis/visualization3d.py +81 -0
- swcgeom/analysis/volume.py +143 -0
- swcgeom/core/__init__.py +19 -0
- swcgeom/core/branch.py +129 -0
- swcgeom/core/branch_tree.py +65 -0
- swcgeom/core/compartment.py +107 -0
- swcgeom/core/node.py +130 -0
- swcgeom/core/path.py +155 -0
- swcgeom/core/population.py +394 -0
- swcgeom/core/swc.py +247 -0
- swcgeom/core/swc_utils/__init__.py +19 -0
- swcgeom/core/swc_utils/assembler.py +35 -0
- swcgeom/core/swc_utils/base.py +180 -0
- swcgeom/core/swc_utils/checker.py +112 -0
- swcgeom/core/swc_utils/io.py +335 -0
- swcgeom/core/swc_utils/normalizer.py +163 -0
- swcgeom/core/swc_utils/subtree.py +70 -0
- swcgeom/core/tree.py +387 -0
- swcgeom/core/tree_utils.py +277 -0
- swcgeom/core/tree_utils_impl.py +58 -0
- swcgeom/images/__init__.py +9 -0
- swcgeom/images/augmentation.py +149 -0
- swcgeom/images/contrast.py +87 -0
- swcgeom/images/folder.py +217 -0
- swcgeom/images/io.py +578 -0
- swcgeom/images/loaders/__init__.py +8 -0
- swcgeom/images/loaders/pbd.cpython-312-darwin.so +0 -0
- swcgeom/images/loaders/pbd.pyx +523 -0
- swcgeom/images/loaders/raw.cpython-312-darwin.so +0 -0
- swcgeom/images/loaders/raw.pyx +183 -0
- swcgeom/transforms/__init__.py +20 -0
- swcgeom/transforms/base.py +136 -0
- swcgeom/transforms/branch.py +223 -0
- swcgeom/transforms/branch_tree.py +74 -0
- swcgeom/transforms/geometry.py +270 -0
- swcgeom/transforms/image_preprocess.py +107 -0
- swcgeom/transforms/image_stack.py +219 -0
- swcgeom/transforms/images.py +206 -0
- swcgeom/transforms/mst.py +183 -0
- swcgeom/transforms/neurolucida_asc.py +498 -0
- swcgeom/transforms/path.py +56 -0
- swcgeom/transforms/population.py +36 -0
- swcgeom/transforms/tree.py +265 -0
- swcgeom/transforms/tree_assembler.py +160 -0
- swcgeom/utils/__init__.py +18 -0
- swcgeom/utils/debug.py +23 -0
- swcgeom/utils/download.py +119 -0
- swcgeom/utils/dsu.py +58 -0
- swcgeom/utils/ellipse.py +131 -0
- swcgeom/utils/file.py +90 -0
- swcgeom/utils/neuromorpho.py +581 -0
- swcgeom/utils/numpy_helper.py +70 -0
- swcgeom/utils/plotter_2d.py +134 -0
- swcgeom/utils/plotter_3d.py +35 -0
- swcgeom/utils/renderer.py +145 -0
- swcgeom/utils/sdf.py +324 -0
- swcgeom/utils/solid_geometry.py +154 -0
- swcgeom/utils/transforms.py +367 -0
- swcgeom/utils/volumetric_object.py +483 -0
- swcgeom-0.20.0.dist-info/METADATA +86 -0
- swcgeom-0.20.0.dist-info/RECORD +72 -0
- swcgeom-0.20.0.dist-info/WHEEL +5 -0
- swcgeom-0.20.0.dist-info/licenses/LICENSE +201 -0
- swcgeom-0.20.0.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
|