ossify 0.2.6__tar.gz → 0.2.8__tar.gz
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-0.2.6 → ossify-0.2.8}/PKG-INFO +2 -2
- {ossify-0.2.6 → ossify-0.2.8}/pyproject.toml +2 -2
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/__init__.py +1 -1
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/algorithms.py +9 -3
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/base.py +54 -7
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/data_layers.py +223 -94
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/file_io.py +4 -2
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/graph_functions.py +16 -5
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/plot.py +95 -10
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/translate.py +5 -8
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/utils.py +4 -1
- {ossify-0.2.6 → ossify-0.2.8}/.github/workflows/mkdocs_publish.yml +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/.github/workflows/python-package.yml +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/.gitignore +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/.pre-commit-config.yaml +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/LICENSE +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/README.md +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/mkdocs.yml +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/_sync/__init__.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/_sync/base.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/_sync/graph.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/_sync/mapping.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/_sync/mesh.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/_sync/morph.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/_sync/points.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/_sync/table.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/compartment_models/minnie65_ds15_us0_bd0.json +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/compartment_models/v1dd_ds15_us0_bd0.json +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/compartment_models/xgb/minnie65_ds15_us0_bd0.ubj +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/compartment_models/xgb/v1dd_ds15_us0_bd0.ubj +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/compartments.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/plot3d.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/plot_utils.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/structured_prediction.py +0 -0
- {ossify-0.2.6 → ossify-0.2.8}/src/ossify/sync_classes.py +0 -0
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
allow-direct-references = true
|
|
7
7
|
[project]
|
|
8
8
|
name = "ossify"
|
|
9
|
-
version = "0.2.
|
|
9
|
+
version = "0.2.8"
|
|
10
10
|
description = "Mesh and skeleton analysis"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.11"
|
|
@@ -95,7 +95,7 @@ default-groups = ["dev", "docs", "lint", "profile", "viz"]
|
|
|
95
95
|
|
|
96
96
|
|
|
97
97
|
[tool.bumpversion]
|
|
98
|
-
current_version = "0.2.
|
|
98
|
+
current_version = "0.2.8"
|
|
99
99
|
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
100
100
|
serialize = ["{major}.{minor}.{patch}"]
|
|
101
101
|
regex = false
|
|
@@ -547,11 +547,17 @@ def _label_axon_synapse_flow(
|
|
|
547
547
|
"""feature an axon compartment by synapse betweenness. All parameters are as positional indices."""
|
|
548
548
|
syn_btw = synapse_betweenness(skeleton, pre_syn_inds, post_syn_inds)
|
|
549
549
|
high_vinds = np.flatnonzero(syn_btw == max(syn_btw))
|
|
550
|
-
|
|
550
|
+
# Everything here is positional, so every lookup must say so: on a masked
|
|
551
|
+
# skeleton (the ntimes > 1 path runs inside a mask_context) vertex indices
|
|
552
|
+
# and positional indices are different spaces.
|
|
553
|
+
close_vind = high_vinds[
|
|
554
|
+
np.argmin(skeleton.distance_to_root(high_vinds, as_positional=True))
|
|
555
|
+
]
|
|
551
556
|
if extend_feature_to_segment:
|
|
552
557
|
relseg = skeleton.segment_map[close_vind]
|
|
553
|
-
|
|
554
|
-
|
|
558
|
+
seg = skeleton.segments_positional[relseg]
|
|
559
|
+
min_ind = np.argmin(skeleton.distance_to_root(seg, as_positional=True))
|
|
560
|
+
axon_split_ind = seg[min_ind]
|
|
555
561
|
else:
|
|
556
562
|
axon_split_ind = close_vind
|
|
557
563
|
downstream_inds = skeleton.downstream_vertices(
|
|
@@ -398,6 +398,7 @@ class Cell:
|
|
|
398
398
|
features: Optional[Union[dict, pd.DataFrame]] = None,
|
|
399
399
|
*,
|
|
400
400
|
vertex_index: Optional[Union[str, np.ndarray]] = None,
|
|
401
|
+
faces_as_positional: Optional[bool] = None,
|
|
401
402
|
linkage: Optional[Link] = None,
|
|
402
403
|
spatial_columns: Optional[list] = None,
|
|
403
404
|
) -> Self:
|
|
@@ -408,11 +409,25 @@ class Cell:
|
|
|
408
409
|
vertices : Union[np.ndarray, pd.DataFrame, MeshLayer]
|
|
409
410
|
The vertices of the mesh, or a MeshLayer object.
|
|
410
411
|
faces : Union[np.ndarray, pd.DataFrame]
|
|
411
|
-
The faces of the mesh. If
|
|
412
|
+
The triangular faces of the mesh. **If ``vertex_index`` is given
|
|
413
|
+
these are positional indices into ``vertices``** and are remapped
|
|
414
|
+
onto the vertex index for you; otherwise they are vertex indices
|
|
415
|
+
already -- the index of ``vertices`` when it is a DataFrame, or
|
|
416
|
+
positional when it is a plain array. Mesh formats such as OBJ and
|
|
417
|
+
PLY give positional faces, so ``vertex_index`` is usually what you
|
|
418
|
+
want when labelling mesh vertices with ids.
|
|
412
419
|
features : Optional[Union[dict, pd.DataFrame]]
|
|
413
420
|
Additional features for the mesh. If passed as dictionary, the key is the feature name and the values are an array of feature values.
|
|
414
421
|
vertex_index : Optional[Union[str, np.ndarray]]
|
|
415
|
-
|
|
422
|
+
Vertex indices to label the mesh's vertices by: either the name of
|
|
423
|
+
a column in ``vertices``, or an array with one value per vertex.
|
|
424
|
+
Supplying this changes how ``faces`` are read -- see above.
|
|
425
|
+
faces_as_positional : Optional[bool]
|
|
426
|
+
Whether ``faces`` are positional indices into ``vertices``. The
|
|
427
|
+
default, None, infers it as described above: positional exactly
|
|
428
|
+
when ``vertex_index`` is supplied. Pass it explicitly when the two
|
|
429
|
+
spaces overlap -- a ``vertex_index`` whose values are also valid
|
|
430
|
+
positions cannot be told apart any other way.
|
|
416
431
|
linkage : Optional[Link]
|
|
417
432
|
The linkage information for the mesh.
|
|
418
433
|
spatial_columns: Optional[list] = None
|
|
@@ -435,6 +450,7 @@ class Cell:
|
|
|
435
450
|
name=self.MESH_LN,
|
|
436
451
|
vertices=vertices,
|
|
437
452
|
faces=faces,
|
|
453
|
+
faces_as_positional=faces_as_positional,
|
|
438
454
|
features=features,
|
|
439
455
|
morphsync=self._morphsync,
|
|
440
456
|
spatial_columns=spatial_columns,
|
|
@@ -453,6 +469,7 @@ class Cell:
|
|
|
453
469
|
root: Optional[int] = None,
|
|
454
470
|
*,
|
|
455
471
|
vertex_index: Optional[Union[str, np.ndarray]] = None,
|
|
472
|
+
edges_as_positional: Optional[bool] = None,
|
|
456
473
|
linkage: Optional[Link] = None,
|
|
457
474
|
spatial_columns: Optional[list] = None,
|
|
458
475
|
inherited_properties: Optional[dict] = None,
|
|
@@ -465,13 +482,28 @@ class Cell:
|
|
|
465
482
|
vertices : Union[np.ndarray, pd.DataFrame, SkeletonLayer]
|
|
466
483
|
The vertices of the skeleton, or a SkeletonLayer object.
|
|
467
484
|
edges : Union[np.ndarray, pd.DataFrame]
|
|
468
|
-
The edges of the skeleton.
|
|
485
|
+
The edges of the skeleton, as pairs of vertices. **If ``vertex_index``
|
|
486
|
+
is given these are positional indices into ``vertices``** and are
|
|
487
|
+
remapped onto the vertex index for you; otherwise they are vertex
|
|
488
|
+
indices already -- the index of ``vertices`` when it is a DataFrame,
|
|
489
|
+
or positional when it is a plain array.
|
|
469
490
|
features : Optional[Union[dict, pd.DataFrame]]
|
|
470
491
|
The features for the skeleton.
|
|
471
492
|
root : Optional[int]
|
|
472
|
-
The root
|
|
493
|
+
The root of the skeleton, as a **vertex index** -- note this is the
|
|
494
|
+
opposite convention to ``edges``, which are positional when
|
|
495
|
+
``vertex_index`` is given. Required if the edges are not already
|
|
496
|
+
consistent with a single root.
|
|
473
497
|
vertex_index : Optional[Union[str, np.ndarray]]
|
|
474
|
-
|
|
498
|
+
Vertex indices to label the skeleton's vertices by: either the name of
|
|
499
|
+
a column in ``vertices``, or an array with one value per vertex.
|
|
500
|
+
Supplying this changes how ``edges`` are read -- see above.
|
|
501
|
+
edges_as_positional : Optional[bool]
|
|
502
|
+
Whether ``edges`` are positional indices into ``vertices``. The
|
|
503
|
+
default, None, infers it as described above: positional exactly
|
|
504
|
+
when ``vertex_index`` is supplied. Pass it explicitly when the two
|
|
505
|
+
spaces overlap -- a ``vertex_index`` whose values are also valid
|
|
506
|
+
positions cannot be told apart any other way.
|
|
475
507
|
linkage : Optional[Link]
|
|
476
508
|
The linkage information for the skeleton. Typically, you will define the source vertices for the skeleton if using a graph-to-skeleton mapping.
|
|
477
509
|
spatial_columns: Optional[list] = None
|
|
@@ -495,6 +527,7 @@ class Cell:
|
|
|
495
527
|
name=self.SKEL_LN,
|
|
496
528
|
vertices=vertices,
|
|
497
529
|
edges=edges,
|
|
530
|
+
edges_as_positional=edges_as_positional,
|
|
498
531
|
features=features,
|
|
499
532
|
root=root,
|
|
500
533
|
morphsync=self._morphsync,
|
|
@@ -514,6 +547,7 @@ class Cell:
|
|
|
514
547
|
features: Optional[Union[dict, pd.DataFrame]] = None,
|
|
515
548
|
*,
|
|
516
549
|
vertex_index: Optional[Union[str, np.ndarray]] = None,
|
|
550
|
+
edges_as_positional: Optional[bool] = None,
|
|
517
551
|
spatial_columns: Optional[list] = None,
|
|
518
552
|
linkage: Optional[Link] = None,
|
|
519
553
|
) -> Self:
|
|
@@ -526,11 +560,23 @@ class Cell:
|
|
|
526
560
|
vertices : Union[np.ndarray, pd.DataFrame, SkeletonLayer]
|
|
527
561
|
The vertices of the graph.
|
|
528
562
|
edges : Union[np.ndarray, pd.DataFrame]
|
|
529
|
-
The edges of the graph.
|
|
563
|
+
The edges of the graph, as pairs of vertices. **If ``vertex_index``
|
|
564
|
+
is given these are positional indices into ``vertices``** and are
|
|
565
|
+
remapped onto the vertex index for you; otherwise they are vertex
|
|
566
|
+
indices already -- the index of ``vertices`` when it is a DataFrame,
|
|
567
|
+
or positional when it is a plain array.
|
|
530
568
|
features : Optional[Union[dict, pd.DataFrame]]
|
|
531
569
|
The features for the graph.
|
|
532
570
|
vertex_index : Optional[Union[str, np.ndarray]]
|
|
533
|
-
|
|
571
|
+
Vertex indices to label the graph's vertices by: either the name of
|
|
572
|
+
a column in ``vertices``, or an array with one value per vertex.
|
|
573
|
+
Supplying this changes how ``edges`` are read -- see above.
|
|
574
|
+
edges_as_positional : Optional[bool]
|
|
575
|
+
Whether ``edges`` are positional indices into ``vertices``. The
|
|
576
|
+
default, None, infers it as described above: positional exactly
|
|
577
|
+
when ``vertex_index`` is supplied. Pass it explicitly when the two
|
|
578
|
+
spaces overlap -- a ``vertex_index`` whose values are also valid
|
|
579
|
+
positions cannot be told apart any other way.
|
|
534
580
|
spatial_columns: Optional[list] = None
|
|
535
581
|
The spatial columns for the graph, if vertices are a dataframe.
|
|
536
582
|
|
|
@@ -552,6 +598,7 @@ class Cell:
|
|
|
552
598
|
name=self.GRAPH_LN,
|
|
553
599
|
vertices=vertices,
|
|
554
600
|
edges=edges,
|
|
601
|
+
edges_as_positional=edges_as_positional,
|
|
555
602
|
features=features,
|
|
556
603
|
morphsync=self._morphsync,
|
|
557
604
|
spatial_columns=spatial_columns,
|
|
@@ -89,26 +89,6 @@ class EdgeMixin(ABC):
|
|
|
89
89
|
return np.empty((0, 2), dtype=int)
|
|
90
90
|
return self.layer.edges_positional
|
|
91
91
|
|
|
92
|
-
def _map_edges_to_index(
|
|
93
|
-
self, edges: np.ndarray, vertex_indices: np.ndarray
|
|
94
|
-
) -> np.ndarray:
|
|
95
|
-
"""Remap positional edges to vertex indices.
|
|
96
|
-
|
|
97
|
-
Parameters
|
|
98
|
-
----------
|
|
99
|
-
edges : np.ndarray
|
|
100
|
-
Edge array with positional indices.
|
|
101
|
-
vertex_indices : np.ndarray
|
|
102
|
-
Array of vertex indices to map to.
|
|
103
|
-
|
|
104
|
-
Returns
|
|
105
|
-
-------
|
|
106
|
-
np.ndarray
|
|
107
|
-
Edge array with vertex indices instead of positional indices.
|
|
108
|
-
"""
|
|
109
|
-
index_map = {ii: v for ii, v in enumerate(vertex_indices)}
|
|
110
|
-
return fastremap.remap(edges, index_map)
|
|
111
|
-
|
|
112
92
|
@property
|
|
113
93
|
def csgraph(self) -> sparse.csr_matrix:
|
|
114
94
|
"""
|
|
@@ -318,26 +298,6 @@ class FaceMixin(ABC):
|
|
|
318
298
|
self._csgraph = None
|
|
319
299
|
self._trimesh = None
|
|
320
300
|
|
|
321
|
-
def _map_faces_to_index(
|
|
322
|
-
self, faces: np.ndarray, vertex_indices: np.ndarray
|
|
323
|
-
) -> np.ndarray:
|
|
324
|
-
"""Remap positional faces to vertex indices.
|
|
325
|
-
|
|
326
|
-
Parameters
|
|
327
|
-
----------
|
|
328
|
-
faces : np.ndarray
|
|
329
|
-
Face array with positional indices.
|
|
330
|
-
vertex_indices : np.ndarray
|
|
331
|
-
Array of vertex indices to map to.
|
|
332
|
-
|
|
333
|
-
Returns
|
|
334
|
-
-------
|
|
335
|
-
np.ndarray
|
|
336
|
-
Face array with vertex indices instead of positional indices.
|
|
337
|
-
"""
|
|
338
|
-
index_map = {ii: v for ii, v in enumerate(vertex_indices)}
|
|
339
|
-
return fastremap.remap(faces, index_map)
|
|
340
|
-
|
|
341
301
|
def surface_area(
|
|
342
302
|
self,
|
|
343
303
|
vertices: Optional[np.ndarray] = None,
|
|
@@ -444,6 +404,75 @@ class PointMixin(ABC):
|
|
|
444
404
|
)
|
|
445
405
|
self._process_linkage(linkage)
|
|
446
406
|
|
|
407
|
+
def _index_connectivity(
|
|
408
|
+
self,
|
|
409
|
+
connectivity: np.ndarray,
|
|
410
|
+
vertices: pd.DataFrame,
|
|
411
|
+
as_positional: Optional[bool],
|
|
412
|
+
vertex_index: Optional[Union[str, np.ndarray]],
|
|
413
|
+
what: str = "edges",
|
|
414
|
+
) -> np.ndarray:
|
|
415
|
+
"""Put edges or faces into vertex-index space at construction.
|
|
416
|
+
|
|
417
|
+
Every layer type takes connectivity that may be written either as
|
|
418
|
+
positional indices into ``vertices`` or as vertex indices, so the rule
|
|
419
|
+
lives here once rather than in each constructor.
|
|
420
|
+
|
|
421
|
+
``as_positional=None`` keeps the historical behaviour -- positional
|
|
422
|
+
exactly when ``vertex_index`` was supplied -- so callers that never
|
|
423
|
+
pass the flag are unaffected. An explicit ``True``/``False`` overrides
|
|
424
|
+
it, and is the only way to disambiguate a vertex index whose values
|
|
425
|
+
are also valid positions.
|
|
426
|
+
|
|
427
|
+
Parameters
|
|
428
|
+
----------
|
|
429
|
+
connectivity : np.ndarray
|
|
430
|
+
Edge pairs or face triples.
|
|
431
|
+
vertices : pd.DataFrame
|
|
432
|
+
The processed vertex table; its index supplies the vertex indices.
|
|
433
|
+
as_positional : Optional[bool]
|
|
434
|
+
Whether ``connectivity`` holds positional indices. None infers it.
|
|
435
|
+
vertex_index : Optional[Union[str, np.ndarray]]
|
|
436
|
+
The vertex index argument, used only for the inference above.
|
|
437
|
+
what : str
|
|
438
|
+
Name used in error messages -- "edges" or "faces".
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
np.ndarray
|
|
443
|
+
``connectivity`` in vertex-index space.
|
|
444
|
+
"""
|
|
445
|
+
positional = (
|
|
446
|
+
vertex_index is not None if as_positional is None else bool(as_positional)
|
|
447
|
+
)
|
|
448
|
+
if not positional:
|
|
449
|
+
return connectivity
|
|
450
|
+
|
|
451
|
+
arr = np.asarray(connectivity)
|
|
452
|
+
if arr.size:
|
|
453
|
+
# Without this check, values that are actually vertex indices
|
|
454
|
+
# surface as KeyError from inside fastremap, which says nothing
|
|
455
|
+
# about the convention that was violated.
|
|
456
|
+
low, high = int(arr.min()), int(arr.max())
|
|
457
|
+
if low < 0 or high >= len(vertices):
|
|
458
|
+
if as_positional is None:
|
|
459
|
+
hint = (
|
|
460
|
+
f"`{what}` are read as positional indices because "
|
|
461
|
+
f"`vertex_index` was given; pass "
|
|
462
|
+
f"{what}_as_positional=False if they are vertex "
|
|
463
|
+
"indices already."
|
|
464
|
+
)
|
|
465
|
+
else:
|
|
466
|
+
hint = (
|
|
467
|
+
f"`{what}_as_positional=True` was given, so `{what}` "
|
|
468
|
+
"must be positional indices."
|
|
469
|
+
)
|
|
470
|
+
raise ValueError(
|
|
471
|
+
f"{what} contain values outside the valid positional range "
|
|
472
|
+
f"0..{len(vertices) - 1} (found {low}..{high}). {hint}"
|
|
473
|
+
)
|
|
474
|
+
return fastremap.remap(arr, dict(enumerate(vertices.index)))
|
|
475
|
+
|
|
447
476
|
def _vertices_to_positional(
|
|
448
477
|
self,
|
|
449
478
|
vertices: Optional[np.ndarray],
|
|
@@ -459,18 +488,26 @@ class PointMixin(ABC):
|
|
|
459
488
|
as_positional : bool
|
|
460
489
|
Whether input vertices are positional indices (True) or vertex indices (False).
|
|
461
490
|
vertex_index : Optional[np.ndarray], optional
|
|
462
|
-
|
|
491
|
+
The index space the result is expressed in: positions are returned
|
|
492
|
+
relative to this array. If None, uses self.vertex_index. Passing a
|
|
493
|
+
different array (e.g. ``base_vertex_index``) maps this layer's
|
|
494
|
+
vertices into that other space.
|
|
463
495
|
|
|
464
496
|
Returns
|
|
465
497
|
-------
|
|
466
498
|
Tuple[np.ndarray, bool]
|
|
467
499
|
Tuple of (positional_indices, is_positional_flag).
|
|
468
500
|
"""
|
|
469
|
-
if vertex_index is None:
|
|
470
|
-
vertex_index = self.vertex_index
|
|
471
501
|
if vertices is None:
|
|
472
|
-
|
|
473
|
-
|
|
502
|
+
if vertex_index is None:
|
|
503
|
+
vertices = np.arange(len(self.vertex_index))
|
|
504
|
+
as_positional = True
|
|
505
|
+
else:
|
|
506
|
+
# "All of my vertices", but expressed in the caller's target
|
|
507
|
+
# index space -- going via vertex ids is the only way to cross
|
|
508
|
+
# between spaces, since positions do not correspond.
|
|
509
|
+
vertices = self.vertex_index
|
|
510
|
+
as_positional = False
|
|
474
511
|
else:
|
|
475
512
|
vertices = np.asarray(vertices)
|
|
476
513
|
if np.issubdtype(vertices.dtype, np.bool_):
|
|
@@ -480,11 +517,15 @@ class PointMixin(ABC):
|
|
|
480
517
|
)
|
|
481
518
|
vertices = np.flatnonzero(vertices)
|
|
482
519
|
as_positional = True
|
|
520
|
+
elif vertices.size == 0:
|
|
521
|
+
# An empty list arrives as float64, and fastremap passes the
|
|
522
|
+
# dtype straight through, leaving a result that is not a legal
|
|
523
|
+
# index array. Give it an integer dtype up front.
|
|
524
|
+
vertices = vertices.astype(np.intp)
|
|
525
|
+
if vertex_index is None:
|
|
526
|
+
vertex_index = self.vertex_index
|
|
483
527
|
if not as_positional:
|
|
484
|
-
|
|
485
|
-
vertex_index_map = self.vertex_index_map
|
|
486
|
-
else:
|
|
487
|
-
vertex_index_map = {v: i for i, v in enumerate(vertex_index)}
|
|
528
|
+
vertex_index_map = {v: i for i, v in enumerate(vertex_index)}
|
|
488
529
|
# fastremap.remap rejects 0-d arrays, so a scalar vertex would crash
|
|
489
530
|
# here. Remap on a 1-d view and restore the caller's shape, keeping
|
|
490
531
|
# scalar-in -> scalar-out (as the positional path already does).
|
|
@@ -786,8 +827,17 @@ class PointMixin(ABC):
|
|
|
786
827
|
right_index=True,
|
|
787
828
|
how="left",
|
|
788
829
|
)
|
|
830
|
+
# "majority" is ossify's own reducer, not something pandas understands,
|
|
831
|
+
# so it has to be swapped for the callable wherever it appears -- including
|
|
832
|
+
# inside a per-feature dict, which is the documented way to give different
|
|
833
|
+
# features different aggregations.
|
|
789
834
|
if agg == "majority":
|
|
790
835
|
agg = utils.majority_agg()
|
|
836
|
+
elif isinstance(agg, dict):
|
|
837
|
+
agg = {
|
|
838
|
+
key: utils.majority_agg() if value == "majority" else value
|
|
839
|
+
for key, value in agg.items()
|
|
840
|
+
}
|
|
791
841
|
# Group by target layer and aggregate, then reindex to ensure all target vertices are included
|
|
792
842
|
grouped_result = mapping_merged.groupby(layer).agg(agg)
|
|
793
843
|
target_layer_index = self._morphsync.layers[layer].nodes.index
|
|
@@ -1486,6 +1536,7 @@ class GraphLayer(PointMixin, EdgeMixin):
|
|
|
1486
1536
|
spatial_columns: Optional[list] = None,
|
|
1487
1537
|
*,
|
|
1488
1538
|
vertex_index: Optional[Union[str, np.ndarray]] = None,
|
|
1539
|
+
edges_as_positional: Optional[bool] = None,
|
|
1489
1540
|
features: Optional[Union[dict, pd.DataFrame]] = None,
|
|
1490
1541
|
morphsync: MorphSync = None,
|
|
1491
1542
|
linkage: Optional[Link] = None,
|
|
@@ -1503,8 +1554,9 @@ class GraphLayer(PointMixin, EdgeMixin):
|
|
|
1503
1554
|
self._cell = None
|
|
1504
1555
|
|
|
1505
1556
|
if not existing:
|
|
1506
|
-
|
|
1507
|
-
edges
|
|
1557
|
+
edges = self._index_connectivity(
|
|
1558
|
+
edges, vertices, edges_as_positional, vertex_index, "edges"
|
|
1559
|
+
)
|
|
1508
1560
|
self._morphsync.add_graph(
|
|
1509
1561
|
graph=(vertices, edges),
|
|
1510
1562
|
name=self.layer_name,
|
|
@@ -1857,6 +1909,7 @@ class SkeletonLayer(GraphLayer):
|
|
|
1857
1909
|
root: Optional[int] = None,
|
|
1858
1910
|
*,
|
|
1859
1911
|
vertex_index: Optional[Union[str, np.ndarray]] = None,
|
|
1912
|
+
edges_as_positional: Optional[bool] = None,
|
|
1860
1913
|
features: Optional[Union[dict, pd.DataFrame]] = None,
|
|
1861
1914
|
morphsync: MorphSync = None,
|
|
1862
1915
|
linkage: Optional[dict] = None,
|
|
@@ -1873,8 +1926,9 @@ class SkeletonLayer(GraphLayer):
|
|
|
1873
1926
|
|
|
1874
1927
|
if inherited_properties is None:
|
|
1875
1928
|
# Add as a morphsync layer
|
|
1876
|
-
|
|
1877
|
-
edges
|
|
1929
|
+
edges = self._index_connectivity(
|
|
1930
|
+
edges, vertices, edges_as_positional, vertex_index, "edges"
|
|
1931
|
+
)
|
|
1878
1932
|
self._morphsync.add_graph(
|
|
1879
1933
|
graph=(vertices, edges),
|
|
1880
1934
|
name=self.layer_name,
|
|
@@ -2164,6 +2218,56 @@ class SkeletonLayer(GraphLayer):
|
|
|
2164
2218
|
"""
|
|
2165
2219
|
return np.asarray(self._base_properties["base_parent_array"])
|
|
2166
2220
|
|
|
2221
|
+
def _vertices_to_base_positional(
|
|
2222
|
+
self, vertices: Optional[np.ndarray], as_positional: bool
|
|
2223
|
+
) -> np.ndarray:
|
|
2224
|
+
"""Map a vertex selection on this (possibly masked) layer to positional
|
|
2225
|
+
indices into the **base**, unmasked arrays.
|
|
2226
|
+
|
|
2227
|
+
``base_csgraph``/``base_csgraph_binary`` span the original unmasked
|
|
2228
|
+
skeleton, so anything used to index their dijkstra results must be in
|
|
2229
|
+
base positional space -- not this layer's. The two only coincide when
|
|
2230
|
+
unmasked. Conversion always goes via vertex ids, the one index space
|
|
2231
|
+
the masked layer and the base snapshot share.
|
|
2232
|
+
|
|
2233
|
+
Parameters
|
|
2234
|
+
----------
|
|
2235
|
+
vertices : Optional[np.ndarray]
|
|
2236
|
+
Vertex indices, positional indices, or a boolean mask over this
|
|
2237
|
+
layer's vertices. If None, every vertex of *this* layer is used, in
|
|
2238
|
+
this layer's positional order, so the result lines up 1:1 with
|
|
2239
|
+
``self.vertices``.
|
|
2240
|
+
as_positional : bool
|
|
2241
|
+
Whether ``vertices`` are positional indices into this layer. A
|
|
2242
|
+
boolean mask is implicitly positional whatever this says.
|
|
2243
|
+
|
|
2244
|
+
Returns
|
|
2245
|
+
-------
|
|
2246
|
+
np.ndarray
|
|
2247
|
+
Positional indices into the base (unmasked) arrays. Scalar in,
|
|
2248
|
+
scalar out.
|
|
2249
|
+
"""
|
|
2250
|
+
if vertices is None:
|
|
2251
|
+
vertices = self.vertex_index
|
|
2252
|
+
else:
|
|
2253
|
+
vertices = np.asarray(vertices)
|
|
2254
|
+
if np.issubdtype(vertices.dtype, np.bool_):
|
|
2255
|
+
if len(vertices) != self.n_vertices:
|
|
2256
|
+
raise ValueError(
|
|
2257
|
+
"If vertices is a boolean array, it must have the same length as the number of vertices."
|
|
2258
|
+
)
|
|
2259
|
+
vertices = self.vertex_index[np.flatnonzero(vertices)]
|
|
2260
|
+
else:
|
|
2261
|
+
if vertices.size == 0:
|
|
2262
|
+
# An empty list is float64, which cannot index an array.
|
|
2263
|
+
vertices = vertices.astype(np.intp)
|
|
2264
|
+
if as_positional:
|
|
2265
|
+
vertices = self.vertex_index[vertices]
|
|
2266
|
+
base_positional, _ = self._vertices_to_positional(
|
|
2267
|
+
vertices, as_positional=False, vertex_index=self.base_vertex_index
|
|
2268
|
+
)
|
|
2269
|
+
return base_positional
|
|
2270
|
+
|
|
2167
2271
|
def _reset_derived_properties(self) -> None:
|
|
2168
2272
|
super()._reset_derived_properties()
|
|
2169
2273
|
self._dag_cache = gf.DAGCache()
|
|
@@ -2185,24 +2289,38 @@ class SkeletonLayer(GraphLayer):
|
|
|
2185
2289
|
Parameters
|
|
2186
2290
|
----------
|
|
2187
2291
|
root : Optional[int]
|
|
2188
|
-
Proposed root
|
|
2292
|
+
Proposed root, as a **vertex index** -- the same space as
|
|
2293
|
+
``vertex_index``, not a positional index. Note this differs from
|
|
2294
|
+
``edges``, which are positional when ``vertex_index`` is supplied.
|
|
2295
|
+
If None, attempts to infer from graph structure.
|
|
2189
2296
|
|
|
2190
2297
|
Returns
|
|
2191
2298
|
-------
|
|
2192
2299
|
int
|
|
2193
|
-
The root
|
|
2300
|
+
The root vertex index.
|
|
2194
2301
|
|
|
2195
2302
|
Raises
|
|
2196
2303
|
------
|
|
2197
2304
|
ValueError
|
|
2198
|
-
If
|
|
2305
|
+
If the given root is not one of this layer's vertices, or if no
|
|
2306
|
+
root was specified and multiple potential roots were found.
|
|
2199
2307
|
"""
|
|
2200
2308
|
if root is not None:
|
|
2201
|
-
|
|
2309
|
+
root = int(root)
|
|
2310
|
+
# Numpy membership rather than building a Python set: a layer can
|
|
2311
|
+
# have millions of vertices and this runs on every construction.
|
|
2312
|
+
if not np.any(self.vertex_index == root):
|
|
2313
|
+
raise ValueError(
|
|
2314
|
+
f"root {root} is not a vertex of this layer. `root` is a "
|
|
2315
|
+
"vertex index, not a positional index -- note that `edges` "
|
|
2316
|
+
"use the opposite convention when `vertex_index` is given."
|
|
2317
|
+
)
|
|
2318
|
+
return root
|
|
2202
2319
|
else:
|
|
2203
2320
|
potential_roots = np.flatnonzero(self.csgraph_binary.sum(axis=1) == 0)
|
|
2204
2321
|
if len(potential_roots) == 1:
|
|
2205
|
-
|
|
2322
|
+
# ``potential_roots`` is positional; the root is a vertex index.
|
|
2323
|
+
return int(self.vertex_index[potential_roots[0]])
|
|
2206
2324
|
else:
|
|
2207
2325
|
raise ValueError(
|
|
2208
2326
|
"No root specified and edges are not consistent with a single root. Please set a valid root."
|
|
@@ -2274,6 +2392,16 @@ class SkeletonLayer(GraphLayer):
|
|
|
2274
2392
|
edges_positional_new[:, ii]
|
|
2275
2393
|
]
|
|
2276
2394
|
|
|
2395
|
+
# The edges just changed direction, but ``csgraph``/``csgraph_binary``
|
|
2396
|
+
# were built (by the connected_components call above) from the *old*
|
|
2397
|
+
# orientation. Everything read off the directed graph -- branch points,
|
|
2398
|
+
# end points, segments, cover paths -- would otherwise be computed from
|
|
2399
|
+
# a stale graph that disagrees with ``edges_positional``. Drop only the
|
|
2400
|
+
# edge-derived caches: the DAG cache holds root/parent state that the
|
|
2401
|
+
# callers set around this method.
|
|
2402
|
+
self._csgraph = None
|
|
2403
|
+
self._csgraph_binary = None
|
|
2404
|
+
|
|
2277
2405
|
def reroot(self, new_root: int, as_positional=False) -> Self:
|
|
2278
2406
|
"""Reroot to a new index. Important: that this will reset any inherited properties from an unmasked skeleton!
|
|
2279
2407
|
|
|
@@ -2366,35 +2494,30 @@ class SkeletonLayer(GraphLayer):
|
|
|
2366
2494
|
Parameters
|
|
2367
2495
|
----------
|
|
2368
2496
|
vertices : Optional[np.ndarray]
|
|
2369
|
-
The vertices to get the distance from the root for. If None,
|
|
2497
|
+
The vertices to get the distance from the root for. If None, every
|
|
2498
|
+
vertex of this layer is used, in this layer's positional order, so
|
|
2499
|
+
the result lines up 1:1 with ``self.vertices``.
|
|
2370
2500
|
as_positional : bool
|
|
2371
2501
|
If True, the vertices are treated as positional indices. If False, they are treated as vertex features.
|
|
2372
2502
|
|
|
2373
2503
|
Returns
|
|
2374
2504
|
-------
|
|
2375
2505
|
np.ndarray
|
|
2376
|
-
The distance from the root for each vertex.
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
vertices, as_positional, vertex_index=self.base_vertex_index
|
|
2387
|
-
)
|
|
2388
|
-
if self._dag_cache.distance_to_root is not None:
|
|
2389
|
-
dtr = self._dag_cache.distance_to_root
|
|
2390
|
-
else:
|
|
2391
|
-
dtr = sparse.csgraph.dijkstra(
|
|
2506
|
+
The distance from the root for each vertex. One value per requested
|
|
2507
|
+
vertex of *this* layer, so on a masked skeleton ``distance_to_root()``
|
|
2508
|
+
always equals ``distance_to_root(np.arange(n), as_positional=True)``
|
|
2509
|
+
and ``distance_to_root(self.vertex_index)``.
|
|
2510
|
+
"""
|
|
2511
|
+
# The dijkstra runs on the base graph, so the selection must be in base
|
|
2512
|
+
# positional space -- which is not this layer's space when masked.
|
|
2513
|
+
base_positional = self._vertices_to_base_positional(vertices, as_positional)
|
|
2514
|
+
if self._dag_cache.base_distance_to_root is None:
|
|
2515
|
+
self._dag_cache.base_distance_to_root = sparse.csgraph.dijkstra(
|
|
2392
2516
|
self.base_csgraph,
|
|
2393
2517
|
directed=False,
|
|
2394
2518
|
indices=self.base_root_positional,
|
|
2395
|
-
)
|
|
2396
|
-
|
|
2397
|
-
return dtr[vertices]
|
|
2519
|
+
).flatten()
|
|
2520
|
+
return self._dag_cache.base_distance_to_root[base_positional]
|
|
2398
2521
|
|
|
2399
2522
|
def hops_to_root(
|
|
2400
2523
|
self,
|
|
@@ -2406,26 +2529,29 @@ class SkeletonLayer(GraphLayer):
|
|
|
2406
2529
|
Parameters
|
|
2407
2530
|
----------
|
|
2408
2531
|
vertices : Optional[np.ndarray]
|
|
2409
|
-
The vertices to get the distance from the root for. If None,
|
|
2532
|
+
The vertices to get the distance from the root for. If None, every
|
|
2533
|
+
vertex of this layer is used, in this layer's positional order, so
|
|
2534
|
+
the result lines up 1:1 with ``self.vertices``.
|
|
2410
2535
|
as_positional : bool
|
|
2411
2536
|
If True, the vertices are treated as positional indices. If False, they are treated as vertex features.
|
|
2412
2537
|
|
|
2413
2538
|
Returns
|
|
2414
2539
|
-------
|
|
2415
2540
|
np.ndarray
|
|
2416
|
-
The
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2541
|
+
The hop count from the root for each vertex. One value per requested
|
|
2542
|
+
vertex of *this* layer, so on a masked skeleton ``hops_to_root()``
|
|
2543
|
+
always equals ``hops_to_root(np.arange(n), as_positional=True)`` and
|
|
2544
|
+
``hops_to_root(self.vertex_index)``.
|
|
2545
|
+
"""
|
|
2546
|
+
# Same base-space contract as distance_to_root; see that method.
|
|
2547
|
+
base_positional = self._vertices_to_base_positional(vertices, as_positional)
|
|
2548
|
+
if self._dag_cache.base_hops_to_root is None:
|
|
2549
|
+
self._dag_cache.base_hops_to_root = sparse.csgraph.dijkstra(
|
|
2423
2550
|
self.base_csgraph_binary,
|
|
2424
2551
|
directed=False,
|
|
2425
2552
|
indices=self.base_root_positional,
|
|
2426
|
-
)
|
|
2427
|
-
|
|
2428
|
-
return htr[vertices]
|
|
2553
|
+
).flatten()
|
|
2554
|
+
return self._dag_cache.base_hops_to_root[base_positional]
|
|
2429
2555
|
|
|
2430
2556
|
def child_vertices(self, vertices=None, as_positional=False) -> dict:
|
|
2431
2557
|
"""Get mapping from vertices to their child nodes.
|
|
@@ -2581,7 +2707,7 @@ class SkeletonLayer(GraphLayer):
|
|
|
2581
2707
|
A list of cover paths, each path is a list of vertex indices, ordered as the typical `cover_paths` method.
|
|
2582
2708
|
"""
|
|
2583
2709
|
sources, as_positional = self._vertices_to_positional(sources, as_positional)
|
|
2584
|
-
cps = gf.
|
|
2710
|
+
cps = gf.build_cover_paths(
|
|
2585
2711
|
sources,
|
|
2586
2712
|
self.parent_node_array,
|
|
2587
2713
|
self.distance_to_root(as_positional=True),
|
|
@@ -3327,8 +3453,9 @@ class PointCloudLayer(PointMixin):
|
|
|
3327
3453
|
if self._cell.skeleton is None:
|
|
3328
3454
|
raise ValueError("Cell does not have a Skeleton object.")
|
|
3329
3455
|
|
|
3330
|
-
|
|
3331
|
-
|
|
3456
|
+
# Do not pre-fill ``vertices`` here: it would hand vertex ids to a call
|
|
3457
|
+
# that may be reading them as positional indices. ``map_index_to_layer``
|
|
3458
|
+
# already resolves None correctly for *both* index spaces.
|
|
3332
3459
|
skel_idx = self.map_index_to_layer(
|
|
3333
3460
|
layer=SKEL_LAYER_NAME, source_index=vertices, as_positional=as_positional
|
|
3334
3461
|
)
|
|
@@ -3418,6 +3545,7 @@ class MeshLayer(FaceMixin, PointMixin):
|
|
|
3418
3545
|
spatial_columns: Optional[list] = None,
|
|
3419
3546
|
*,
|
|
3420
3547
|
vertex_index: Optional[Union[str, np.ndarray]] = None,
|
|
3548
|
+
faces_as_positional: Optional[bool] = None,
|
|
3421
3549
|
features: Optional[Union[dict, pd.DataFrame]] = None,
|
|
3422
3550
|
morphsync: MorphSync = None,
|
|
3423
3551
|
linkage: Optional[Link] = None,
|
|
@@ -3435,8 +3563,9 @@ class MeshLayer(FaceMixin, PointMixin):
|
|
|
3435
3563
|
self._cell = None
|
|
3436
3564
|
|
|
3437
3565
|
if not existing:
|
|
3438
|
-
|
|
3439
|
-
faces
|
|
3566
|
+
faces = self._index_connectivity(
|
|
3567
|
+
faces, vertices, faces_as_positional, vertex_index, "faces"
|
|
3568
|
+
)
|
|
3440
3569
|
self._morphsync.add_mesh(
|
|
3441
3570
|
mesh=(vertices, faces),
|
|
3442
3571
|
name=self.layer_name,
|
|
@@ -1077,13 +1077,15 @@ def import_legacy_meshwork(
|
|
|
1077
1077
|
|
|
1078
1078
|
def _process_pcg_skel_import(cell: Cell) -> Cell:
|
|
1079
1079
|
if "compartment" in cell.annotations:
|
|
1080
|
+
# ``map_annotations_to_feature`` already returns a frame whose column is
|
|
1081
|
+
# named "compartment", so passing ``name`` here is redundant -- and
|
|
1082
|
+
# ``add_feature`` warns that it ignores ``name`` for a DataFrame.
|
|
1080
1083
|
cell.skeleton.add_feature(
|
|
1081
1084
|
cell.skeleton.map_annotations_to_feature(
|
|
1082
1085
|
"compartment",
|
|
1083
1086
|
distance_threshold=0,
|
|
1084
1087
|
agg={"compartment": ("compartment", "mean")},
|
|
1085
|
-
)
|
|
1086
|
-
"compartment",
|
|
1088
|
+
)
|
|
1087
1089
|
)
|
|
1088
1090
|
if "segment_properties" in cell.annotations:
|
|
1089
1091
|
features = cell.skeleton.map_annotations_to_feature(
|
|
@@ -13,7 +13,14 @@ from .utils import build_csgraph, single_path_length
|
|
|
13
13
|
class DAGCache:
|
|
14
14
|
"""
|
|
15
15
|
Container for cached DAG properties to optimize repeated computations.
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
Cache values are in positional indices of the layer that owns the cache,
|
|
18
|
+
**except** the two ``base_``-prefixed fields, which are in positional
|
|
19
|
+
indices of the original unmasked skeleton (``base_csgraph`` /
|
|
20
|
+
``base_csgraph_binary``). On an unmasked layer the two spaces coincide; on
|
|
21
|
+
a masked one they do not, so never index a ``base_`` array with a
|
|
22
|
+
layer-positional index. Use
|
|
23
|
+
``SkeletonLayer._vertices_to_base_positional`` to convert.
|
|
17
24
|
|
|
18
25
|
Attributes
|
|
19
26
|
----------
|
|
@@ -37,9 +44,11 @@ class DAGCache:
|
|
|
37
44
|
branch_points: Optional[np.ndarray] = None
|
|
38
45
|
end_points: Optional[np.ndarray] = None
|
|
39
46
|
segments: Optional[List[List[int]]] = None
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
segment_map: Optional[np.ndarray] = None
|
|
48
|
+
base_distance_to_root: Optional[np.ndarray] = None
|
|
49
|
+
base_hops_to_root: Optional[np.ndarray] = None
|
|
42
50
|
cover_paths: Optional[List[List[int]]] = None
|
|
51
|
+
path_lengths: Optional[Dict[Tuple[int, int], float]] = None
|
|
43
52
|
root: Optional[int] = None
|
|
44
53
|
|
|
45
54
|
def __post_init__(self):
|
|
@@ -55,10 +64,12 @@ class DAGCache:
|
|
|
55
64
|
self.branch_points = None
|
|
56
65
|
self.end_points = None
|
|
57
66
|
self.segments = None
|
|
67
|
+
self.segment_map = None
|
|
58
68
|
self.root = None
|
|
59
|
-
self.
|
|
60
|
-
self.
|
|
69
|
+
self.base_distance_to_root = None
|
|
70
|
+
self.base_hops_to_root = None
|
|
61
71
|
self.cover_paths = None
|
|
72
|
+
self.path_lengths = None
|
|
62
73
|
|
|
63
74
|
|
|
64
75
|
def build_parent_node_array(vertices, edges) -> np.ndarray:
|
|
@@ -981,7 +981,31 @@ def plot_points(
|
|
|
981
981
|
# Single color string
|
|
982
982
|
scatter_kws["color"] = colors
|
|
983
983
|
else:
|
|
984
|
-
|
|
984
|
+
colors_arr = np.asarray(colors)
|
|
985
|
+
single_rgba = (
|
|
986
|
+
colors_arr.ndim == 1
|
|
987
|
+
and colors_arr.shape[0] in (3, 4)
|
|
988
|
+
and np.issubdtype(colors_arr.dtype, np.number)
|
|
989
|
+
and colors_arr.shape[0] != len(points_proj)
|
|
990
|
+
)
|
|
991
|
+
if single_rgba:
|
|
992
|
+
# One bare RGB/RGBA tuple means "this exact color", but as `c`
|
|
993
|
+
# it is ambiguous with one scalar value per point, and
|
|
994
|
+
# matplotlib warns. A 2-D single row is its documented way to
|
|
995
|
+
# say the former. Reached via root_marker, whose color is read
|
|
996
|
+
# off the root vertex as a single RGBA.
|
|
997
|
+
colors_arr = colors_arr.reshape(1, -1)
|
|
998
|
+
if (
|
|
999
|
+
colors_arr.ndim == 2
|
|
1000
|
+
and colors_arr.shape[1] in (3, 4)
|
|
1001
|
+
and np.issubdtype(colors_arr.dtype, np.number)
|
|
1002
|
+
):
|
|
1003
|
+
# Explicit RGB/RGBA values are not colormapped, so a cmap set
|
|
1004
|
+
# from a string palette above would be silently ignored --
|
|
1005
|
+
# matplotlib says so out loud. Drop it and its norm.
|
|
1006
|
+
for key in ("cmap", "vmin", "vmax"):
|
|
1007
|
+
scatter_kws.pop(key, None)
|
|
1008
|
+
scatter_kws["c"] = colors_arr
|
|
985
1009
|
|
|
986
1010
|
ax.scatter(
|
|
987
1011
|
x=points_proj[:, 0],
|
|
@@ -1514,6 +1538,53 @@ def plot_cell_multiview(
|
|
|
1514
1538
|
return axes
|
|
1515
1539
|
|
|
1516
1540
|
|
|
1541
|
+
_MIN_PANEL_INCHES = 0.5
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
def _pad_degenerate_axes(
|
|
1545
|
+
bounds_min: np.ndarray,
|
|
1546
|
+
bounds_max: np.ndarray,
|
|
1547
|
+
fraction: float = 0.02,
|
|
1548
|
+
) -> Tuple[np.ndarray, np.ndarray, List[int]]:
|
|
1549
|
+
"""Expand any zero-extent axis so a flat cell is still renderable.
|
|
1550
|
+
|
|
1551
|
+
A cell that is perfectly flat along one axis -- a planar reconstruction, a
|
|
1552
|
+
2D-derived skeleton stored with z=0, or a synthetic test skeleton -- has a
|
|
1553
|
+
zero extent there. Panel sizes are derived from the extents, so a zero
|
|
1554
|
+
extent yields a zero-size panel and an empty figure, and matplotlib warns
|
|
1555
|
+
when asked to set identical axis limits. Padding the degenerate axis to a
|
|
1556
|
+
small fraction of the largest extent keeps the panel a thin strip, which is
|
|
1557
|
+
an honest depiction of flat data.
|
|
1558
|
+
|
|
1559
|
+
Parameters
|
|
1560
|
+
----------
|
|
1561
|
+
bounds_min, bounds_max : np.ndarray
|
|
1562
|
+
Per-axis data bounds.
|
|
1563
|
+
fraction : float
|
|
1564
|
+
Width to give a degenerate axis, as a fraction of the largest
|
|
1565
|
+
non-degenerate extent. If every axis is degenerate (a single point),
|
|
1566
|
+
one unit is used.
|
|
1567
|
+
|
|
1568
|
+
Returns
|
|
1569
|
+
-------
|
|
1570
|
+
Tuple[np.ndarray, np.ndarray, List[int]]
|
|
1571
|
+
Padded ``(bounds_min, bounds_max)`` and the indices of the axes padded.
|
|
1572
|
+
"""
|
|
1573
|
+
bounds_min = np.array(bounds_min, dtype=float)
|
|
1574
|
+
bounds_max = np.array(bounds_max, dtype=float)
|
|
1575
|
+
extents = bounds_max - bounds_min
|
|
1576
|
+
degenerate = np.flatnonzero(extents <= 0)
|
|
1577
|
+
if len(degenerate) == 0:
|
|
1578
|
+
return bounds_min, bounds_max, []
|
|
1579
|
+
largest = float(extents.max())
|
|
1580
|
+
pad = fraction * largest if largest > 0 else 1.0
|
|
1581
|
+
for i in degenerate:
|
|
1582
|
+
center = (bounds_min[i] + bounds_max[i]) / 2
|
|
1583
|
+
bounds_min[i] = center - pad / 2
|
|
1584
|
+
bounds_max[i] = center + pad / 2
|
|
1585
|
+
return bounds_min, bounds_max, [int(i) for i in degenerate]
|
|
1586
|
+
|
|
1587
|
+
|
|
1517
1588
|
def single_panel_figure(
|
|
1518
1589
|
data_bounds_min: np.ndarray,
|
|
1519
1590
|
data_bounds_max: np.ndarray,
|
|
@@ -1548,8 +1619,12 @@ def single_panel_figure(
|
|
|
1548
1619
|
>>> fig, ax = create_single_panel_figure(bounds_min, bounds_max, 10)
|
|
1549
1620
|
>>> # Creates 10" x 5" figure with 10 units per inch
|
|
1550
1621
|
"""
|
|
1551
|
-
|
|
1552
|
-
|
|
1622
|
+
# Pad a flat axis before deriving sizes, so the panel is not zero-sized and
|
|
1623
|
+
# matplotlib is never asked for identical limits. The clamp below already
|
|
1624
|
+
# reports degenerate input, so this does not warn separately.
|
|
1625
|
+
data_bounds_min, data_bounds_max, _ = _pad_degenerate_axes(
|
|
1626
|
+
data_bounds_min, data_bounds_max
|
|
1627
|
+
)
|
|
1553
1628
|
|
|
1554
1629
|
# Calculate data extents
|
|
1555
1630
|
data_width = data_bounds_max[0] - data_bounds_min[0]
|
|
@@ -1562,7 +1637,7 @@ def single_panel_figure(
|
|
|
1562
1637
|
# Clamp degenerate dimensions so the figure is still renderable and
|
|
1563
1638
|
# compatible with peers in a lineup or panel. We warn — the data is
|
|
1564
1639
|
# degenerate, and the caller likely wants to know.
|
|
1565
|
-
_MIN_INCHES =
|
|
1640
|
+
_MIN_INCHES = _MIN_PANEL_INCHES
|
|
1566
1641
|
if fig_width < _MIN_INCHES or fig_height < _MIN_INCHES:
|
|
1567
1642
|
warnings.warn(
|
|
1568
1643
|
f"single_panel_figure received degenerate bounds "
|
|
@@ -1641,18 +1716,28 @@ def multi_panel_figure(
|
|
|
1641
1716
|
>>> fig, axes_dict = create_multi_panel_figure(bounds_min, bounds_max, 10, "side_by_side")
|
|
1642
1717
|
>>> xy_ax, zy_ax = axes_dict["xy"], axes_dict["zy"]
|
|
1643
1718
|
"""
|
|
1644
|
-
data_bounds_min =
|
|
1645
|
-
|
|
1719
|
+
data_bounds_min, data_bounds_max, degenerate = _pad_degenerate_axes(
|
|
1720
|
+
data_bounds_min, data_bounds_max
|
|
1721
|
+
)
|
|
1722
|
+
if degenerate:
|
|
1723
|
+
axis_names = ", ".join("xyz"[i] for i in degenerate)
|
|
1724
|
+
warnings.warn(
|
|
1725
|
+
f"multi_panel_figure received a degenerate {axis_names} extent "
|
|
1726
|
+
f"(0 units); padding it so the panel remains visible. The cell is "
|
|
1727
|
+
f"flat along {'these axes' if len(degenerate) > 1 else 'this axis'}.",
|
|
1728
|
+
stacklevel=2,
|
|
1729
|
+
)
|
|
1646
1730
|
|
|
1647
1731
|
# Calculate data extents for each dimension
|
|
1648
1732
|
x_extent = data_bounds_max[0] - data_bounds_min[0]
|
|
1649
1733
|
y_extent = data_bounds_max[1] - data_bounds_min[1]
|
|
1650
1734
|
z_extent = data_bounds_max[2] - data_bounds_min[2]
|
|
1651
1735
|
|
|
1652
|
-
# Convert to sizes in inches
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1736
|
+
# Convert to sizes in inches. Clamp as single_panel_figure does, so a panel
|
|
1737
|
+
# of a thin or flat cell is still large enough to see.
|
|
1738
|
+
x_inches = max(x_extent / units_per_inch, _MIN_PANEL_INCHES)
|
|
1739
|
+
y_inches = max(y_extent / units_per_inch, _MIN_PANEL_INCHES)
|
|
1740
|
+
z_inches = max(z_extent / units_per_inch, _MIN_PANEL_INCHES)
|
|
1656
1741
|
|
|
1657
1742
|
if layout == "side_by_side":
|
|
1658
1743
|
# xy | zy layout
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from enum import IntEnum
|
|
2
2
|
from typing import TYPE_CHECKING, Literal, Optional, Tuple, Union
|
|
3
3
|
|
|
4
|
-
import fastremap
|
|
5
4
|
import numpy as np
|
|
6
5
|
import pandas as pd
|
|
7
6
|
|
|
@@ -310,13 +309,10 @@ def load_cell_from_client(
|
|
|
310
309
|
l2_df = l2_df.reset_index()
|
|
311
310
|
|
|
312
311
|
if restore_graph:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
edges =
|
|
317
|
-
l2_graph,
|
|
318
|
-
l2_map,
|
|
319
|
-
)
|
|
312
|
+
# The chunkedgraph gives edges as l2_id pairs, which is what the graph
|
|
313
|
+
# is indexed by, so hand them over as-is rather than round-tripping
|
|
314
|
+
# them through positional indices and straight back again.
|
|
315
|
+
edges = client.chunkedgraph.level2_chunk_graph(root_id)
|
|
320
316
|
else:
|
|
321
317
|
edges = []
|
|
322
318
|
|
|
@@ -335,6 +331,7 @@ def load_cell_from_client(
|
|
|
335
331
|
spatial_columns=l2_spatial_columns,
|
|
336
332
|
edges=edges,
|
|
337
333
|
vertex_index="l2_id",
|
|
334
|
+
edges_as_positional=False,
|
|
338
335
|
)
|
|
339
336
|
.add_skeleton(
|
|
340
337
|
vertices=np.array(sk["vertices"]),
|
|
@@ -79,7 +79,10 @@ def process_vertices(
|
|
|
79
79
|
)
|
|
80
80
|
spatial_columns = vertices.columns
|
|
81
81
|
else:
|
|
82
|
-
|
|
82
|
+
# Only a string names a column to hold out of the implicit features; an
|
|
83
|
+
# array-like vertex_index supplies the ids directly. Plain truthiness
|
|
84
|
+
# raises on an array of more than one element.
|
|
85
|
+
if isinstance(vertex_index, str):
|
|
83
86
|
implicit_feature_columns = list(
|
|
84
87
|
vertices.columns[
|
|
85
88
|
~vertices.columns.isin(spatial_columns + [vertex_index])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|