ossify 0.0.1__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.
- ossify/__init__.py +6 -0
- ossify/algorithms.py +514 -0
- ossify/base.py +1445 -0
- ossify/data_layers.py +2706 -0
- ossify/file_io.py +958 -0
- ossify/graph_functions.py +905 -0
- ossify/plot.py +1428 -0
- ossify/sync_classes.py +91 -0
- ossify/test.ipynb +472 -0
- ossify/translate.py +238 -0
- ossify/utils.py +252 -0
- ossify-0.0.1.dist-info/METADATA +22 -0
- ossify-0.0.1.dist-info/RECORD +15 -0
- ossify-0.0.1.dist-info/WHEEL +4 -0
- ossify-0.0.1.dist-info/licenses/LICENSE +21 -0
ossify/__init__.py
ADDED
ossify/algorithms.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Optional, Tuple, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from scipy import sparse
|
|
5
|
+
|
|
6
|
+
from .base import Cell, SkeletonLayer
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"strahler_number",
|
|
10
|
+
"smooth_features",
|
|
11
|
+
"label_axon_from_synapse_flow",
|
|
12
|
+
"label_axon_from_spectral_split",
|
|
13
|
+
"synapse_betweenness",
|
|
14
|
+
"segregation_index",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _strahler_path(baseline):
|
|
19
|
+
out = np.full(len(baseline), -1, dtype=np.int64)
|
|
20
|
+
last_val = 1
|
|
21
|
+
for ii in np.arange(len(out)):
|
|
22
|
+
if baseline[ii] > last_val:
|
|
23
|
+
last_val = baseline[ii]
|
|
24
|
+
elif baseline[ii] == last_val:
|
|
25
|
+
last_val += 1
|
|
26
|
+
out[ii] = last_val
|
|
27
|
+
return out
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _laplacian_offset(
|
|
31
|
+
skeleton: Cell,
|
|
32
|
+
) -> sparse.csr_matrix:
|
|
33
|
+
"""Compute the degree-normalized adjacency matrix part of the Laplacian matrix.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
nrn : meshwork.Meshwork
|
|
38
|
+
Neuron object
|
|
39
|
+
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
sparse.spmatrix
|
|
43
|
+
Degree-normalized adjacency matrix in sparse format.
|
|
44
|
+
"""
|
|
45
|
+
Amat = skeleton.csgraph_binary_undirected
|
|
46
|
+
deg = np.array(Amat.sum(axis=0)).squeeze()
|
|
47
|
+
Dmat = sparse.diags_array(1 / np.sqrt(deg))
|
|
48
|
+
Lmat = Dmat @ Amat @ Dmat
|
|
49
|
+
return Lmat
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def smooth_features(
|
|
53
|
+
cell: Union[Cell, SkeletonLayer],
|
|
54
|
+
feature: np.ndarray,
|
|
55
|
+
alpha: float = 0.90,
|
|
56
|
+
) -> np.ndarray:
|
|
57
|
+
"""Computes a smoothed feature spreading that is akin to steady-state solutions to the heat equation on the skeleton graph.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
cell : Cell
|
|
62
|
+
Neuron object
|
|
63
|
+
feature : np.ndarray
|
|
64
|
+
The initial feature array. Must be Nxm, where N is the number of skeleton vertices
|
|
65
|
+
alpha : float, optional
|
|
66
|
+
A neighborhood influence parameter between 0 and 1. Higher values give more influence to neighbors, by default 0.90.
|
|
67
|
+
|
|
68
|
+
Returns
|
|
69
|
+
-------
|
|
70
|
+
np.ndarray
|
|
71
|
+
The smoothed feature array
|
|
72
|
+
"""
|
|
73
|
+
if isinstance(cell, SkeletonLayer):
|
|
74
|
+
skel = cell
|
|
75
|
+
else:
|
|
76
|
+
skel = cell.skeleton
|
|
77
|
+
Smat = _laplacian_offset(skel)
|
|
78
|
+
Imat = sparse.eye(Smat.shape[0])
|
|
79
|
+
invertLap = Imat - alpha * Smat
|
|
80
|
+
feature = np.atleast_2d(feature).reshape(Smat.shape[0], -1)
|
|
81
|
+
F = sparse.linalg.spsolve(invertLap, feature)
|
|
82
|
+
return np.squeeze((1 - alpha) * F)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def strahler_number(cell: Union[Cell, SkeletonLayer]) -> np.ndarray:
|
|
86
|
+
"""Compute Strahler number on a skeleton, starting at 1 for each tip.
|
|
87
|
+
Returns a feature suitable for a SkeletonLayer.
|
|
88
|
+
|
|
89
|
+
Parameters
|
|
90
|
+
----------
|
|
91
|
+
cell : Union[Cell, SkeletonLayer]
|
|
92
|
+
The skeleton to compute the Strahler number on.
|
|
93
|
+
For convenience, you can pass a Cell object, but note
|
|
94
|
+
that the return feature is always for the skeleton.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
np.ndarray
|
|
99
|
+
The Strahler number for each vertex in the skeleton.
|
|
100
|
+
"""
|
|
101
|
+
if isinstance(cell, Cell):
|
|
102
|
+
skel: SkeletonLayer = cell.skeleton
|
|
103
|
+
if skel is None:
|
|
104
|
+
raise ValueError("Cell is does not have a skeleton.")
|
|
105
|
+
else:
|
|
106
|
+
skel: SkeletonLayer = cell
|
|
107
|
+
strahler_number = np.full(skel.n_vertices, -1, dtype=np.int32)
|
|
108
|
+
for pth in skel.cover_paths_positional[::-1]:
|
|
109
|
+
pth_vals = _strahler_path(strahler_number[pth])
|
|
110
|
+
strahler_number[pth] = pth_vals
|
|
111
|
+
pind = skel.parent_node_array[pth[-1]]
|
|
112
|
+
if pind >= 0:
|
|
113
|
+
if strahler_number[pth[-1]] > strahler_number[pind]:
|
|
114
|
+
strahler_number[pind] = strahler_number[pth[-1]]
|
|
115
|
+
elif strahler_number[pth[-1]] == strahler_number[pind]:
|
|
116
|
+
strahler_number[pind] += 1
|
|
117
|
+
return strahler_number
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _distribution_entropy(counts: np.ndarray) -> float:
|
|
121
|
+
"""Compute the distribution entropy of a Nx2 set of synapse counts per compartment."""
|
|
122
|
+
if np.sum(counts) == 0:
|
|
123
|
+
return 0
|
|
124
|
+
ps = np.divide(
|
|
125
|
+
counts,
|
|
126
|
+
np.sum(counts, axis=1)[:, np.newaxis],
|
|
127
|
+
where=np.sum(counts, axis=1)[:, np.newaxis] > 0,
|
|
128
|
+
)
|
|
129
|
+
Hpart = np.sum(np.multiply(ps, np.log2(ps, where=ps > 0)), axis=1)
|
|
130
|
+
Hws = np.sum(counts, axis=1) / np.sum(counts)
|
|
131
|
+
Htot = -np.sum(Hpart * Hws)
|
|
132
|
+
return Htot
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def segregation_index(
|
|
136
|
+
axon_pre: int,
|
|
137
|
+
axon_post: int,
|
|
138
|
+
dendrite_pre: int,
|
|
139
|
+
dendrite_post: int,
|
|
140
|
+
) -> float:
|
|
141
|
+
"""Compute the segregation index between pre and post-synaptic compartments relative a compartment-free neuron.
|
|
142
|
+
Values close to 1 indicate strong segregation, values close to 0 indicate no segregation.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
axon_pre : int
|
|
147
|
+
The number of pre-synaptic axon compartments.
|
|
148
|
+
axon_post : int
|
|
149
|
+
The number of post-synaptic axon compartments.
|
|
150
|
+
dendrite_pre : int
|
|
151
|
+
The number of pre-synaptic dendrite compartments.
|
|
152
|
+
dendrite_post : int
|
|
153
|
+
The number of post-synaptic dendrite compartments.
|
|
154
|
+
|
|
155
|
+
Returns
|
|
156
|
+
-------
|
|
157
|
+
float
|
|
158
|
+
The segregation index, between 0 and 1.
|
|
159
|
+
"""
|
|
160
|
+
if axon_pre + dendrite_pre == 0 or axon_post + dendrite_post == 0:
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
counts = np.array([[axon_pre, axon_post], [dendrite_pre, dendrite_post]])
|
|
164
|
+
observed_ent = _distribution_entropy(counts)
|
|
165
|
+
|
|
166
|
+
unsplit_ent = _distribution_entropy(
|
|
167
|
+
[[axon_pre + dendrite_pre, axon_post + dendrite_post]]
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return 1 - observed_ent / (unsplit_ent + 1e-10)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def label_axon_from_synapse_flow(
|
|
174
|
+
cell: Union[Cell, SkeletonLayer],
|
|
175
|
+
pre_syn: Union[str, np.ndarray] = "pre_syn",
|
|
176
|
+
post_syn: Union[str, np.ndarray] = "post_syn",
|
|
177
|
+
extend_feature_to_segment: bool = False,
|
|
178
|
+
ntimes: int = 1,
|
|
179
|
+
return_segregation_index: bool = False,
|
|
180
|
+
segregation_index_threshold: float = 0,
|
|
181
|
+
as_postitional: bool = False,
|
|
182
|
+
) -> Union[np.ndarray, Tuple[np.ndarray, float]]:
|
|
183
|
+
"""Split a neuron into axon and dendrite compartments using synapse locations.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
cell : Union[Cell, SkeletonLayer]
|
|
188
|
+
The neuron to split.
|
|
189
|
+
pre_syn : Union[str, np.ndarray], optional
|
|
190
|
+
The annotation associated with presynaptic sites or a list of skeleton vertex ids (see as_postitional).
|
|
191
|
+
post_syn : Union[str, np.ndarray], optional
|
|
192
|
+
The annotation associated with postsynaptic sites or a list of skeleton vertex ids (see as_postitional).
|
|
193
|
+
how : Literal["synapse_flow", "spectral"], optional
|
|
194
|
+
The method to use for splitting.
|
|
195
|
+
n_splits : int, optional
|
|
196
|
+
The number of splits to perform. Only applies to the "synapse_flow" method.
|
|
197
|
+
extend_feature_to_segment : bool, optional
|
|
198
|
+
Whether to propagate the is_axon feature to the whole segment, rather than a specific vertex.
|
|
199
|
+
This is likely more biologically accurate, but potentially a less optimal split.
|
|
200
|
+
segregation_index_threshold : float, optional
|
|
201
|
+
The minimum segregation index required to accept a split. If the best split has a segregation index
|
|
202
|
+
below this threshold, no split is performed and all vertices are featureed as dendrite.
|
|
203
|
+
as_positional : bool, optional
|
|
204
|
+
If True, assumes the pre_syn and post_syn arrays are positional indices into the skeleton vertex array.
|
|
205
|
+
If False, assumes they are the vertex indices (i.e. cell.skeleton.vertex_indices).
|
|
206
|
+
|
|
207
|
+
Returns
|
|
208
|
+
-------
|
|
209
|
+
is_axon:
|
|
210
|
+
A boolean array on Skeleton vertices with True for the axon compartments, False for the dendrite compartments.
|
|
211
|
+
"""
|
|
212
|
+
if isinstance(cell, Cell):
|
|
213
|
+
skel = cell.skeleton
|
|
214
|
+
else:
|
|
215
|
+
skel = cell
|
|
216
|
+
if isinstance(pre_syn, str):
|
|
217
|
+
if isinstance(cell, SkeletonLayer):
|
|
218
|
+
raise ValueError("If passing a SkeletonLayer, pre_syn must be an array.")
|
|
219
|
+
pre_syn_inds = cell.annotations[pre_syn].map_index_to_layer(
|
|
220
|
+
"skeleton", as_positional=True
|
|
221
|
+
)
|
|
222
|
+
else:
|
|
223
|
+
if not as_postitional:
|
|
224
|
+
pre_syn_inds = np.array(
|
|
225
|
+
[np.flatnonzero(skel.vertex_index == vid)[0] for vid in pre_syn]
|
|
226
|
+
)
|
|
227
|
+
else:
|
|
228
|
+
pre_syn_inds = np.asarray(pre_syn)
|
|
229
|
+
if isinstance(post_syn, str):
|
|
230
|
+
if isinstance(cell, SkeletonLayer):
|
|
231
|
+
raise ValueError("If passing a SkeletonLayer, post_syn must be an array.")
|
|
232
|
+
post_syn_inds = cell.annotations[post_syn].map_index_to_layer(
|
|
233
|
+
"skeleton", as_positional=True
|
|
234
|
+
)
|
|
235
|
+
else:
|
|
236
|
+
if not as_postitional:
|
|
237
|
+
post_syn_inds = np.array(
|
|
238
|
+
[np.flatnonzero(skel.vertex_index == vid)[0] for vid in post_syn]
|
|
239
|
+
)
|
|
240
|
+
else:
|
|
241
|
+
post_syn_inds = np.asarray(post_syn)
|
|
242
|
+
is_axon, Hsplit = _label_axon_synapse_flow(
|
|
243
|
+
skel, pre_syn_inds, post_syn_inds, extend_feature_to_segment
|
|
244
|
+
)
|
|
245
|
+
if Hsplit < segregation_index_threshold:
|
|
246
|
+
is_axon = np.full(skel.n_vertices, False)
|
|
247
|
+
if ntimes > 1:
|
|
248
|
+
if not isinstance(cell, Cell):
|
|
249
|
+
raise ValueError(
|
|
250
|
+
"Multiple iterations (ntimes > 1) requires a Cell object, not SkeletonLayer"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
for _ in range(ntimes - 1):
|
|
254
|
+
with cell.mask_context("skeleton", ~is_axon) as masked_cell:
|
|
255
|
+
# Recalculate synapse indices for the masked skeleton
|
|
256
|
+
if isinstance(pre_syn, str):
|
|
257
|
+
masked_pre_syn_inds = masked_cell.annotations[
|
|
258
|
+
pre_syn
|
|
259
|
+
].map_index_to_layer("skeleton", as_positional=True)
|
|
260
|
+
else:
|
|
261
|
+
# Map original indices to masked skeleton indices
|
|
262
|
+
masked_pre_syn_inds = []
|
|
263
|
+
for idx in pre_syn_inds:
|
|
264
|
+
if (
|
|
265
|
+
idx < len(is_axon) and ~is_axon[idx]
|
|
266
|
+
): # This vertex is still in masked skeleton
|
|
267
|
+
# Find its position in the masked skeleton
|
|
268
|
+
masked_idx = np.sum(~is_axon[: idx + 1]) - 1
|
|
269
|
+
masked_pre_syn_inds.append(masked_idx)
|
|
270
|
+
masked_pre_syn_inds = np.array(masked_pre_syn_inds)
|
|
271
|
+
|
|
272
|
+
if isinstance(post_syn, str):
|
|
273
|
+
masked_post_syn_inds = masked_cell.annotations[
|
|
274
|
+
post_syn
|
|
275
|
+
].map_index_to_layer("skeleton", as_positional=True)
|
|
276
|
+
else:
|
|
277
|
+
# Map original indices to masked skeleton indices
|
|
278
|
+
masked_post_syn_inds = []
|
|
279
|
+
for idx in post_syn_inds:
|
|
280
|
+
if (
|
|
281
|
+
idx < len(is_axon) and ~is_axon[idx]
|
|
282
|
+
): # This vertex is still in masked skeleton
|
|
283
|
+
# Find its position in the masked skeleton
|
|
284
|
+
masked_idx = np.sum(~is_axon[: idx + 1]) - 1
|
|
285
|
+
masked_post_syn_inds.append(masked_idx)
|
|
286
|
+
masked_post_syn_inds = np.array(masked_post_syn_inds)
|
|
287
|
+
|
|
288
|
+
is_axon_sub, Hsplit_sub = _label_axon_synapse_flow(
|
|
289
|
+
masked_cell.skeleton,
|
|
290
|
+
masked_pre_syn_inds,
|
|
291
|
+
masked_post_syn_inds,
|
|
292
|
+
extend_feature_to_segment,
|
|
293
|
+
)
|
|
294
|
+
if Hsplit_sub < segregation_index_threshold:
|
|
295
|
+
break
|
|
296
|
+
is_axon[~is_axon] = is_axon_sub
|
|
297
|
+
return (is_axon, Hsplit) if return_segregation_index else is_axon
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _split_direction_and_quality(
|
|
301
|
+
split_idx, skeleton, pre_inds, post_inds
|
|
302
|
+
) -> Tuple[bool, float]:
|
|
303
|
+
"""Evaluate a split at a given positional index with pre and post syn indices in positional indices.
|
|
304
|
+
Returns True if the downstream compartment has higher fraction of pre than post, False otherwise, and then the segregation index of the split.
|
|
305
|
+
"""
|
|
306
|
+
downstream_inds = skeleton.downstream_vertices(
|
|
307
|
+
split_idx, inclusive=True, as_positional=True
|
|
308
|
+
)
|
|
309
|
+
n_pre_ds = np.sum(np.isin(pre_inds, downstream_inds))
|
|
310
|
+
n_post_ds = np.sum(np.isin(post_inds, downstream_inds))
|
|
311
|
+
n_pre_us = len(pre_inds) - n_pre_ds
|
|
312
|
+
n_post_us = len(post_inds) - n_post_ds
|
|
313
|
+
seg_index = segregation_index(
|
|
314
|
+
n_pre_ds,
|
|
315
|
+
n_post_ds,
|
|
316
|
+
n_pre_us,
|
|
317
|
+
n_post_us,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
ds_fraction_pre = n_pre_ds / (n_post_ds + n_pre_ds + 0.0001)
|
|
321
|
+
us_fraction_pre = n_pre_us / (n_post_us + n_pre_us + 0.0001)
|
|
322
|
+
return ds_fraction_pre >= us_fraction_pre, seg_index
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _label_axon_synapse_flow(
|
|
326
|
+
skeleton: SkeletonLayer,
|
|
327
|
+
pre_syn_inds: np.ndarray,
|
|
328
|
+
post_syn_inds: np.ndarray,
|
|
329
|
+
extend_feature_to_segment: bool,
|
|
330
|
+
) -> Tuple[np.ndarray, float]:
|
|
331
|
+
"""feature an axon compartment by synapse betweenness. All parameters are as positional indices."""
|
|
332
|
+
syn_btw = synapse_betweenness(skeleton, pre_syn_inds, post_syn_inds)
|
|
333
|
+
high_vinds = np.flatnonzero(syn_btw == max(syn_btw))
|
|
334
|
+
close_vind = high_vinds[np.argmin(skeleton.distance_to_root(high_vinds))]
|
|
335
|
+
if extend_feature_to_segment:
|
|
336
|
+
relseg = skeleton.segment_map[close_vind]
|
|
337
|
+
min_ind = np.argmin(skeleton.distance_to_root(skeleton.segments[relseg]))
|
|
338
|
+
axon_split_ind = skeleton.segments[relseg][min_ind]
|
|
339
|
+
else:
|
|
340
|
+
axon_split_ind = close_vind
|
|
341
|
+
downstream_inds = skeleton.downstream_vertices(
|
|
342
|
+
axon_split_ind, inclusive=True, as_positional=True
|
|
343
|
+
)
|
|
344
|
+
axon_is_ds, Hsplit = _split_direction_and_quality(
|
|
345
|
+
axon_split_ind, skeleton, pre_syn_inds, post_syn_inds
|
|
346
|
+
)
|
|
347
|
+
if axon_is_ds:
|
|
348
|
+
is_axon = np.full(skeleton.n_vertices, False)
|
|
349
|
+
is_axon[downstream_inds] = True
|
|
350
|
+
else:
|
|
351
|
+
is_axon = np.full(skeleton.n_vertices, True)
|
|
352
|
+
is_axon[downstream_inds] = False
|
|
353
|
+
return is_axon, Hsplit
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def label_axon_from_spectral_split(
|
|
357
|
+
cell: Union[Cell, SkeletonLayer],
|
|
358
|
+
pre_syn: str = "pre_syn",
|
|
359
|
+
post_syn: str = "post_syn",
|
|
360
|
+
aggregation_distance: float = 1,
|
|
361
|
+
smoothing_alpha: float = 0.99,
|
|
362
|
+
axon_bias: float = 0,
|
|
363
|
+
raw_split: bool = False,
|
|
364
|
+
extend_feature_to_segment: bool = True,
|
|
365
|
+
max_times: Optional[int] = None,
|
|
366
|
+
segregation_index_threshold: float = 0.5,
|
|
367
|
+
return_segregation_index: bool = False,
|
|
368
|
+
) -> np.ndarray:
|
|
369
|
+
if isinstance(cell, SkeletonLayer):
|
|
370
|
+
skel = cell
|
|
371
|
+
else:
|
|
372
|
+
skel = cell.skeleton
|
|
373
|
+
if skel is None:
|
|
374
|
+
raise ValueError("Cell is does not have a skeleton.")
|
|
375
|
+
pre_density = (
|
|
376
|
+
skel.map_annotations_to_feature(
|
|
377
|
+
pre_syn,
|
|
378
|
+
distance_threshold=aggregation_distance,
|
|
379
|
+
agg="count",
|
|
380
|
+
)
|
|
381
|
+
+ axon_bias
|
|
382
|
+
)
|
|
383
|
+
post_density = skel.map_annotations_to_feature(
|
|
384
|
+
post_syn,
|
|
385
|
+
distance_threshold=aggregation_distance,
|
|
386
|
+
agg="count",
|
|
387
|
+
)
|
|
388
|
+
syn_density = np.vstack([pre_density, post_density]).T
|
|
389
|
+
smoothed_feature = smooth_features(
|
|
390
|
+
skel,
|
|
391
|
+
feature=syn_density,
|
|
392
|
+
alpha=smoothing_alpha,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
is_axon = smoothed_feature[:, 0] > smoothed_feature[:, 1]
|
|
396
|
+
|
|
397
|
+
split_edges = np.flatnonzero(
|
|
398
|
+
is_axon[skel.edges_positional[:, 0]] != is_axon[skel.edges_positional[:, 1]]
|
|
399
|
+
)
|
|
400
|
+
if len(split_edges) == 0 or raw_split:
|
|
401
|
+
return is_axon
|
|
402
|
+
# If not doing a raw split, treat each split edge as a candidate split point, and evaluate the segregation index of each split.
|
|
403
|
+
|
|
404
|
+
split_parents = skel.edges_positional[split_edges, 1]
|
|
405
|
+
if extend_feature_to_segment:
|
|
406
|
+
split_parent_segments = skel.segment_map[split_parents]
|
|
407
|
+
split_parents = np.array(
|
|
408
|
+
[
|
|
409
|
+
skel.segments_positional[seg][
|
|
410
|
+
np.argmin(
|
|
411
|
+
skel.distance_to_root(
|
|
412
|
+
skel.segments_positional[seg], as_positional=True
|
|
413
|
+
)
|
|
414
|
+
)
|
|
415
|
+
]
|
|
416
|
+
for seg in split_parent_segments
|
|
417
|
+
]
|
|
418
|
+
)
|
|
419
|
+
split_parents = np.unique(split_parents)
|
|
420
|
+
|
|
421
|
+
is_axon_final = np.full(skel.n_vertices, False)
|
|
422
|
+
best_split = 1
|
|
423
|
+
ntimes = 0
|
|
424
|
+
split_values = {}
|
|
425
|
+
while max_times is None or ntimes < max_times:
|
|
426
|
+
with cell.mask_context(layer="skeleton", mask=~is_axon_final) as masked_cell:
|
|
427
|
+
Hsplits = np.zeros(len(split_parents), dtype=np.float32)
|
|
428
|
+
pre_idx_masked = masked_cell.annotations[pre_syn].map_index_to_layer(
|
|
429
|
+
"skeleton", as_positional=True
|
|
430
|
+
)
|
|
431
|
+
post_idx_masked = masked_cell.annotations[post_syn].map_index_to_layer(
|
|
432
|
+
"skeleton", as_positional=True
|
|
433
|
+
)
|
|
434
|
+
for ii, parent in enumerate(split_parents):
|
|
435
|
+
split_vert_ind = cell.skeleton.vertex_index[parent]
|
|
436
|
+
if split_vert_ind in masked_cell.skeleton.vertex_index:
|
|
437
|
+
local_parent_index = np.flatnonzero(
|
|
438
|
+
masked_cell.skeleton.vertex_index == split_vert_ind
|
|
439
|
+
)[0]
|
|
440
|
+
Hsplits[ii] = _split_direction_and_quality(
|
|
441
|
+
local_parent_index,
|
|
442
|
+
masked_cell.skeleton,
|
|
443
|
+
pre_idx_masked,
|
|
444
|
+
post_idx_masked,
|
|
445
|
+
)[1]
|
|
446
|
+
if np.all(np.asarray(Hsplits) <= segregation_index_threshold):
|
|
447
|
+
break
|
|
448
|
+
best_split_index = split_parents[np.argmax(Hsplits)]
|
|
449
|
+
best_split = np.max(Hsplits)
|
|
450
|
+
if best_split <= segregation_index_threshold:
|
|
451
|
+
break
|
|
452
|
+
split_values[best_split_index] = best_split
|
|
453
|
+
ds_split = skel.downstream_vertices(
|
|
454
|
+
best_split_index, inclusive=True, as_positional=True
|
|
455
|
+
)
|
|
456
|
+
is_axon_final[ds_split] = True
|
|
457
|
+
ntimes += 1
|
|
458
|
+
return (
|
|
459
|
+
is_axon_final if not return_segregation_index else (is_axon_final, split_values)
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _precompute_synapse_inds(skel: SkeletonLayer, syn_inds: np.ndarray) -> tuple:
|
|
464
|
+
Nsyn = len(syn_inds)
|
|
465
|
+
n_syn = np.zeros(skel.n_vertices, dtype=int)
|
|
466
|
+
for ind in syn_inds:
|
|
467
|
+
n_syn[ind] += 1
|
|
468
|
+
return Nsyn, n_syn
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def synapse_betweenness(
|
|
472
|
+
skel: SkeletonLayer,
|
|
473
|
+
pre_inds: np.ndarray,
|
|
474
|
+
post_inds: np.ndarray,
|
|
475
|
+
) -> np.ndarray:
|
|
476
|
+
"""Compute synapse betweenness, the number of paths from all post indices to all pre indices along the graph. Vertices can be included multiple times, indicating multiple paths
|
|
477
|
+
|
|
478
|
+
Parameters
|
|
479
|
+
----------
|
|
480
|
+
sk : Skeleton
|
|
481
|
+
Skeleton to measure
|
|
482
|
+
pre_inds : list or array
|
|
483
|
+
Collection of skeleton vertex indices, each representing one output synapse (i.e. target of a path).
|
|
484
|
+
post_inds : list or array
|
|
485
|
+
Collection of skeleton certex indices, each representing one input synapse (i.e. source of a path).
|
|
486
|
+
use_entropy : bool, optional
|
|
487
|
+
If True, also returns the entropic segregatation index if one were to cut at a given vertex, by default False
|
|
488
|
+
|
|
489
|
+
Returns
|
|
490
|
+
-------
|
|
491
|
+
synapse_betweenness : np.array
|
|
492
|
+
Array with a value for each skeleton vertex, with the number of all paths from source to target vertices passing through that vertex.
|
|
493
|
+
"""
|
|
494
|
+
Npre, n_pre = _precompute_synapse_inds(skel, pre_inds)
|
|
495
|
+
Npost, n_post = _precompute_synapse_inds(skel, post_inds)
|
|
496
|
+
|
|
497
|
+
syn_btwn = np.zeros(skel.n_vertices, dtype=np.int64)
|
|
498
|
+
cov_paths_rev = skel.cover_paths_positional[::-1]
|
|
499
|
+
for path in cov_paths_rev:
|
|
500
|
+
downstream_pre = 0
|
|
501
|
+
downstream_post = 0
|
|
502
|
+
for ind in path:
|
|
503
|
+
downstream_pre += n_pre[ind]
|
|
504
|
+
downstream_post += n_post[ind]
|
|
505
|
+
syn_btwn[ind] = (
|
|
506
|
+
downstream_pre * (Npost - downstream_post)
|
|
507
|
+
+ (Npre - downstream_pre) * downstream_post
|
|
508
|
+
)
|
|
509
|
+
# Deposit each branch's synapses at the branch point.
|
|
510
|
+
bp_ind = skel.parent_node_array[path[-1]]
|
|
511
|
+
if bp_ind != -1:
|
|
512
|
+
n_pre[bp_ind] += downstream_pre
|
|
513
|
+
n_post[bp_ind] += downstream_post
|
|
514
|
+
return syn_btwn
|