ossify 0.2.3__tar.gz → 0.2.4__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.3 → ossify-0.2.4}/PKG-INFO +1 -1
  2. {ossify-0.2.3 → ossify-0.2.4}/pyproject.toml +2 -2
  3. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/__init__.py +1 -1
  4. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/file_io.py +101 -12
  5. {ossify-0.2.3 → ossify-0.2.4}/.github/workflows/mkdocs_publish.yml +0 -0
  6. {ossify-0.2.3 → ossify-0.2.4}/.github/workflows/python-package.yml +0 -0
  7. {ossify-0.2.3 → ossify-0.2.4}/.gitignore +0 -0
  8. {ossify-0.2.3 → ossify-0.2.4}/.pre-commit-config.yaml +0 -0
  9. {ossify-0.2.3 → ossify-0.2.4}/LICENSE +0 -0
  10. {ossify-0.2.3 → ossify-0.2.4}/README.md +0 -0
  11. {ossify-0.2.3 → ossify-0.2.4}/mkdocs.yml +0 -0
  12. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/_sync/__init__.py +0 -0
  13. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/_sync/base.py +0 -0
  14. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/_sync/graph.py +0 -0
  15. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/_sync/mapping.py +0 -0
  16. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/_sync/mesh.py +0 -0
  17. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/_sync/morph.py +0 -0
  18. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/_sync/points.py +0 -0
  19. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/_sync/table.py +0 -0
  20. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/algorithms.py +0 -0
  21. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/base.py +0 -0
  22. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/compartment_models/minnie65_ds15_us0_bd0.json +0 -0
  23. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/compartment_models/v1dd_ds15_us0_bd0.json +0 -0
  24. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/compartment_models/xgb/minnie65_ds15_us0_bd0.ubj +0 -0
  25. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/compartment_models/xgb/v1dd_ds15_us0_bd0.ubj +0 -0
  26. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/compartments.py +0 -0
  27. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/data_layers.py +0 -0
  28. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/graph_functions.py +0 -0
  29. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/plot.py +0 -0
  30. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/plot3d.py +0 -0
  31. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/plot_utils.py +0 -0
  32. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/structured_prediction.py +0 -0
  33. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/sync_classes.py +0 -0
  34. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/translate.py +0 -0
  35. {ossify-0.2.3 → ossify-0.2.4}/src/ossify/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ossify
3
- Version: 0.2.3
3
+ Version: 0.2.4
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.3"
9
+ version = "0.2.4"
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.3"
98
+ current_version = "0.2.4"
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.3"
8
+ __version__ = "0.2.4"
9
9
 
10
10
 
11
11
  def __getattr__(name):
@@ -913,6 +913,68 @@ def build_point_cloud(
913
913
  return cell
914
914
 
915
915
 
916
+ def _exact_integer_id_coverage(link_ids: pd.Index, source_ids: pd.Index) -> bool:
917
+ """Test whether two integer-ID domains are exactly the same set.
918
+
919
+ Both arguments must already be canonicalized ``int64`` pandas ``Index``
920
+ objects. The comparison is order-independent and stays entirely in the
921
+ integer domain -- it never coerces identifiers through ``float64`` -- so it
922
+ is safe for IDs above ``2**53``.
923
+
924
+ Parameters
925
+ ----------
926
+ link_ids :
927
+ The linkage column for a candidate source layer.
928
+ source_ids :
929
+ That layer's vertex index.
930
+
931
+ Returns
932
+ -------
933
+ :
934
+ ``True`` iff the two contain exactly the same set of IDs.
935
+ """
936
+ if len(link_ids) != len(source_ids):
937
+ return False
938
+ # ``symmetric_difference`` is empty iff neither side has an ID the other
939
+ # lacks. Using pandas ``Index`` set ops keeps the comparison in int64 and
940
+ # avoids materializing Python ``set`` objects over large ID arrays.
941
+ return len(link_ids.symmetric_difference(source_ids)) == 0
942
+
943
+
944
+ def _linkage_source_error(linkage_pair, link_df, cell, sample_size: int = 10) -> str:
945
+ """Build a concise diagnostic message for an unresolvable linkage source.
946
+
947
+ Reports, for each endpoint, the layer vertex count, the linkage row count,
948
+ the number of unique linkage IDs, and the missing/extra ID counts, plus a
949
+ small bounded sample of the offending IDs. This replaces the enormous
950
+ pandas ``KeyError`` that a bad ``.loc`` reindex would otherwise raise.
951
+ """
952
+ lines = [
953
+ f"Could not infer a valid source layer for linkage "
954
+ f"({linkage_pair[0]!r}, {linkage_pair[1]!r}) with {len(link_df)} rows.",
955
+ "A source endpoint must map exactly one linkage row to each of its "
956
+ "vertices (unique, complete coverage). Neither endpoint qualifies:",
957
+ ]
958
+ for source in linkage_pair:
959
+ source_ids = canonicalize_ids(
960
+ pd.Index(cell._all_objects[source].vertex_index), name=source
961
+ )
962
+ link_ids = canonicalize_ids(pd.Index(link_df[source]), name=source)
963
+ unique_link_ids = link_ids.unique()
964
+ missing = source_ids.difference(link_ids)
965
+ extra = link_ids.difference(source_ids)
966
+ lines.append(
967
+ f" - {source!r}: vertices={len(source_ids)}, "
968
+ f"link_rows={len(link_ids)}, unique_link_ids={len(unique_link_ids)}, "
969
+ f"missing_source_ids={len(missing)}, extra_link_ids={len(extra)}"
970
+ )
971
+ if len(missing):
972
+ lines.append(f" missing sample: {missing[:sample_size].tolist()}")
973
+ if len(extra):
974
+ lines.append(f" extra sample: {extra[:sample_size].tolist()}")
975
+ return "\n".join(lines)
976
+
977
+
916
978
  def build_linkage(
917
979
  linkage_pair,
918
980
  tf,
@@ -922,23 +984,50 @@ def build_linkage(
922
984
  link_df = load_dataframe(f"{prefix}/linkage.feather", tf)
923
985
  # Legacy .osy files may store link columns with mixed int64/uint64 dtypes
924
986
  # (dtype optimization only downcast signed ints, leaving uint64 untouched).
925
- # Canonicalize both columns to int64 before the label-based ``.loc`` reindex
926
- # below so that lookup cannot collide two IDs above 2**53 through a float
927
- # coercion of mismatched signed/unsigned keys.
987
+ # Canonicalize both columns to int64 before any label-based reindex below so
988
+ # that lookup cannot collide two IDs above 2**53 through a float coercion of
989
+ # mismatched signed/unsigned keys.
928
990
  for col in linkage_pair:
929
991
  if col in link_df.columns:
930
992
  link_df[col] = canonicalize_ids(link_df[col], name=col)
931
- # Determine source based on the length of the vertices in the mapping and in the skeleton layer
932
- if len(link_df) == len(cell._all_objects[linkage_pair[0]].nodes):
933
- source_layer = linkage_pair[0]
934
- target_layer = linkage_pair[1]
935
- elif len(link_df) == len(cell._all_objects[linkage_pair[1]].nodes):
936
- source_layer = linkage_pair[1]
937
- target_layer = linkage_pair[0]
938
- else:
939
- raise ValueError("Linkage DataFrame does not match any layer.")
993
+
994
+ # The archive sorts the pair names, so serialized column order does not
995
+ # preserve the original source direction. Infer the source by exact
996
+ # source-domain validation rather than row count alone: a layer qualifies
997
+ # as source only when its linkage column has no missing IDs, is unique, has
998
+ # one row per vertex, and covers exactly that layer's vertex index. Row
999
+ # count alone is ambiguous whenever the two layers have equal cardinality
1000
+ # (e.g. a many-to-one graph <-> annotation link where the counts coincide).
1001
+ candidates = []
1002
+ for source, target in (
1003
+ (linkage_pair[0], linkage_pair[1]),
1004
+ (linkage_pair[1], linkage_pair[0]),
1005
+ ):
1006
+ source_ids = canonicalize_ids(
1007
+ pd.Index(cell._all_objects[source].vertex_index), name=source
1008
+ )
1009
+ link_ids = canonicalize_ids(pd.Index(link_df[source]), name=source)
1010
+ if (
1011
+ len(link_ids) == len(source_ids)
1012
+ and link_ids.is_unique
1013
+ and _exact_integer_id_coverage(link_ids, source_ids)
1014
+ ):
1015
+ candidates.append((source, target))
1016
+
1017
+ if not candidates:
1018
+ raise ValueError(_linkage_source_error(linkage_pair, link_df, cell))
1019
+
1020
+ # Zero candidates raises above. One candidate is the unambiguous source.
1021
+ # Two candidates means a true bijection: both columns are unique and cover
1022
+ # their layers exactly, so the mapping is one-to-one. MorphSync stores every
1023
+ # link in both directions (see ``add_link``), so either choice yields the
1024
+ # same bidirectional link; pick the first deterministically.
1025
+ source_layer, target_layer = candidates[0]
940
1026
 
941
1027
  layer = cell._all_objects[source_layer]
1028
+ # Validation above guarantees the source column is unique and covers exactly
1029
+ # the source vertex index, so this reindex is a total, collision-free
1030
+ # reordering -- it can no longer raise a large KeyError.
942
1031
  cell._all_objects[source_layer]._process_linkage(
943
1032
  Link(
944
1033
  link_df.set_index(source_layer).loc[layer.vertex_index][target_layer],
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