ossify 0.2.7__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.
Files changed (35) hide show
  1. {ossify-0.2.7 → ossify-0.2.8}/PKG-INFO +1 -1
  2. {ossify-0.2.7 → ossify-0.2.8}/pyproject.toml +2 -2
  3. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/__init__.py +1 -1
  4. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/base.py +54 -7
  5. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/data_layers.py +120 -52
  6. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/plot.py +25 -1
  7. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/translate.py +5 -8
  8. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/utils.py +4 -1
  9. {ossify-0.2.7 → ossify-0.2.8}/.github/workflows/mkdocs_publish.yml +0 -0
  10. {ossify-0.2.7 → ossify-0.2.8}/.github/workflows/python-package.yml +0 -0
  11. {ossify-0.2.7 → ossify-0.2.8}/.gitignore +0 -0
  12. {ossify-0.2.7 → ossify-0.2.8}/.pre-commit-config.yaml +0 -0
  13. {ossify-0.2.7 → ossify-0.2.8}/LICENSE +0 -0
  14. {ossify-0.2.7 → ossify-0.2.8}/README.md +0 -0
  15. {ossify-0.2.7 → ossify-0.2.8}/mkdocs.yml +0 -0
  16. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/_sync/__init__.py +0 -0
  17. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/_sync/base.py +0 -0
  18. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/_sync/graph.py +0 -0
  19. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/_sync/mapping.py +0 -0
  20. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/_sync/mesh.py +0 -0
  21. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/_sync/morph.py +0 -0
  22. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/_sync/points.py +0 -0
  23. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/_sync/table.py +0 -0
  24. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/algorithms.py +0 -0
  25. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/compartment_models/minnie65_ds15_us0_bd0.json +0 -0
  26. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/compartment_models/v1dd_ds15_us0_bd0.json +0 -0
  27. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/compartment_models/xgb/minnie65_ds15_us0_bd0.ubj +0 -0
  28. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/compartment_models/xgb/v1dd_ds15_us0_bd0.ubj +0 -0
  29. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/compartments.py +0 -0
  30. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/file_io.py +0 -0
  31. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/graph_functions.py +0 -0
  32. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/plot3d.py +0 -0
  33. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/plot_utils.py +0 -0
  34. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/structured_prediction.py +0 -0
  35. {ossify-0.2.7 → ossify-0.2.8}/src/ossify/sync_classes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ossify
3
- Version: 0.2.7
3
+ Version: 0.2.8
4
4
  Summary: Mesh and skeleton analysis
5
5
  Author-email: Casey Schneider-Mizell <caseysm@gmail.com>
6
6
  License-File: LICENSE
@@ -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.7"
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.7"
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
@@ -5,7 +5,7 @@ from .base import *
5
5
  from .file_io import *
6
6
  from .translate import *
7
7
 
8
- __version__ = "0.2.7"
8
+ __version__ = "0.2.8"
9
9
 
10
10
 
11
11
  def __getattr__(name):
@@ -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 faces are provided as a dataframe, faces should be in dataframe indices.
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
- The column to use as a vertex index for the mesh, if vertices are a dataframe.
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 vertex for the skeleton, required of the edges are not already consistent with a single 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
- The vertex index for the skeleton.
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
- The vertex index for the graph.
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],
@@ -798,8 +827,17 @@ class PointMixin(ABC):
798
827
  right_index=True,
799
828
  how="left",
800
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.
801
834
  if agg == "majority":
802
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
+ }
803
841
  # Group by target layer and aggregate, then reindex to ensure all target vertices are included
804
842
  grouped_result = mapping_merged.groupby(layer).agg(agg)
805
843
  target_layer_index = self._morphsync.layers[layer].nodes.index
@@ -1498,6 +1536,7 @@ class GraphLayer(PointMixin, EdgeMixin):
1498
1536
  spatial_columns: Optional[list] = None,
1499
1537
  *,
1500
1538
  vertex_index: Optional[Union[str, np.ndarray]] = None,
1539
+ edges_as_positional: Optional[bool] = None,
1501
1540
  features: Optional[Union[dict, pd.DataFrame]] = None,
1502
1541
  morphsync: MorphSync = None,
1503
1542
  linkage: Optional[Link] = None,
@@ -1515,8 +1554,9 @@ class GraphLayer(PointMixin, EdgeMixin):
1515
1554
  self._cell = None
1516
1555
 
1517
1556
  if not existing:
1518
- if vertex_index:
1519
- edges = self._map_edges_to_index(edges, vertices.index)
1557
+ edges = self._index_connectivity(
1558
+ edges, vertices, edges_as_positional, vertex_index, "edges"
1559
+ )
1520
1560
  self._morphsync.add_graph(
1521
1561
  graph=(vertices, edges),
1522
1562
  name=self.layer_name,
@@ -1869,6 +1909,7 @@ class SkeletonLayer(GraphLayer):
1869
1909
  root: Optional[int] = None,
1870
1910
  *,
1871
1911
  vertex_index: Optional[Union[str, np.ndarray]] = None,
1912
+ edges_as_positional: Optional[bool] = None,
1872
1913
  features: Optional[Union[dict, pd.DataFrame]] = None,
1873
1914
  morphsync: MorphSync = None,
1874
1915
  linkage: Optional[dict] = None,
@@ -1885,8 +1926,9 @@ class SkeletonLayer(GraphLayer):
1885
1926
 
1886
1927
  if inherited_properties is None:
1887
1928
  # Add as a morphsync layer
1888
- if vertex_index:
1889
- edges = self._map_edges_to_index(edges, vertices.index)
1929
+ edges = self._index_connectivity(
1930
+ edges, vertices, edges_as_positional, vertex_index, "edges"
1931
+ )
1890
1932
  self._morphsync.add_graph(
1891
1933
  graph=(vertices, edges),
1892
1934
  name=self.layer_name,
@@ -2247,24 +2289,38 @@ class SkeletonLayer(GraphLayer):
2247
2289
  Parameters
2248
2290
  ----------
2249
2291
  root : Optional[int]
2250
- Proposed root node index. If None, attempts to infer from graph structure.
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.
2251
2296
 
2252
2297
  Returns
2253
2298
  -------
2254
2299
  int
2255
- The root node index.
2300
+ The root vertex index.
2256
2301
 
2257
2302
  Raises
2258
2303
  ------
2259
2304
  ValueError
2260
- If no root specified and multiple potential roots found.
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.
2261
2307
  """
2262
2308
  if root is not None:
2263
- return int(root)
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
2264
2319
  else:
2265
2320
  potential_roots = np.flatnonzero(self.csgraph_binary.sum(axis=1) == 0)
2266
2321
  if len(potential_roots) == 1:
2267
- return int(potential_roots[0])
2322
+ # ``potential_roots`` is positional; the root is a vertex index.
2323
+ return int(self.vertex_index[potential_roots[0]])
2268
2324
  else:
2269
2325
  raise ValueError(
2270
2326
  "No root specified and edges are not consistent with a single root. Please set a valid root."
@@ -2336,6 +2392,16 @@ class SkeletonLayer(GraphLayer):
2336
2392
  edges_positional_new[:, ii]
2337
2393
  ]
2338
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
+
2339
2405
  def reroot(self, new_root: int, as_positional=False) -> Self:
2340
2406
  """Reroot to a new index. Important: that this will reset any inherited properties from an unmasked skeleton!
2341
2407
 
@@ -2641,7 +2707,7 @@ class SkeletonLayer(GraphLayer):
2641
2707
  A list of cover paths, each path is a list of vertex indices, ordered as the typical `cover_paths` method.
2642
2708
  """
2643
2709
  sources, as_positional = self._vertices_to_positional(sources, as_positional)
2644
- cps = gf.compute_cover_paths(
2710
+ cps = gf.build_cover_paths(
2645
2711
  sources,
2646
2712
  self.parent_node_array,
2647
2713
  self.distance_to_root(as_positional=True),
@@ -3479,6 +3545,7 @@ class MeshLayer(FaceMixin, PointMixin):
3479
3545
  spatial_columns: Optional[list] = None,
3480
3546
  *,
3481
3547
  vertex_index: Optional[Union[str, np.ndarray]] = None,
3548
+ faces_as_positional: Optional[bool] = None,
3482
3549
  features: Optional[Union[dict, pd.DataFrame]] = None,
3483
3550
  morphsync: MorphSync = None,
3484
3551
  linkage: Optional[Link] = None,
@@ -3496,8 +3563,9 @@ class MeshLayer(FaceMixin, PointMixin):
3496
3563
  self._cell = None
3497
3564
 
3498
3565
  if not existing:
3499
- if vertex_index:
3500
- faces = self._map_faces_to_index(faces, vertices.index)
3566
+ faces = self._index_connectivity(
3567
+ faces, vertices, faces_as_positional, vertex_index, "faces"
3568
+ )
3501
3569
  self._morphsync.add_mesh(
3502
3570
  mesh=(vertices, faces),
3503
3571
  name=self.layer_name,
@@ -981,7 +981,31 @@ def plot_points(
981
981
  # Single color string
982
982
  scatter_kws["color"] = colors
983
983
  else:
984
- scatter_kws["c"] = colors
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],
@@ -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
- l2_graph = client.chunkedgraph.level2_chunk_graph(root_id)
314
- l2_map = {v: k for k, v in l2_df["l2_id"].to_dict().items()}
315
-
316
- edges = fastremap.remap(
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
- if vertex_index:
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