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 ADDED
@@ -0,0 +1,6 @@
1
+ from . import algorithms, plot
2
+ from .base import *
3
+ from .file_io import *
4
+ from .translate import *
5
+
6
+ __version__ = "0.0.1"
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