Call graph algorithms as SkeletonGraph methods, not networkx module functions - #763
Closed
IanConvy wants to merge 2 commits into
Closed
Call graph algorithms as SkeletonGraph methods, not networkx module functions#763IanConvy wants to merge 2 commits into
IanConvy wants to merge 2 commits into
Conversation
Drops the hand-allocated node attribute arrays in favour of the registry helpers on SkeletonGraph, so this subclass no longer has to know which arrays exist. load() and load_somas() allocate and grow through the registry, and the graph conversions copy every registered attribute rather than the three they listed by name (node_component_id was already being dropped by both conversions). resize_node_attr is removed since grow_node_attrs replaces its only callers. Soma nodes now get the SWC soma type. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SkeletonGraph is now CSR-backed rather than an nx.Graph subclass, so nx.f(graph, ...) no longer works on it. 16 call sites across 8 files become graph.f(...): connected_components x5, number_connected_components x3, has_path x2, shortest_path x2, dfs_edges x2, node_connected_component x1, and visualization.py's default empty gt_graph becomes a SkeletonGraph. The one non-mechanical change is proposal_graph.py's two conversions, which built a graph with cls.__new__(cls) + nx.Graph.__init__ + update(source). They now use CSRGraph.copy_structure_from. networkx stays a dependency and five of the eight imports are dropped as now unused. It is still needed for ProposalComputationGraph, which is a genuine nx.Graph, and for nx.NetworkXNoPath, which two callers catch by name and CSRGraph.shortest_path still raises. Verified against the pre-swap nx baseline recorded before either repo was touched: 20 patchseq cells packed into one graph with the synthetic soma links suppressed (159,614 nodes, 158,979 edges, 635 components), the real ProposalGenerator at search_radius=25 and the real accept loop. All 11 invariants match exactly -- 3,662 proposals with an identical proposal hash, 2,074 accepts, 1,588 blocked, 7 final components, 161,053 final edges, and an identical component_id checksum -- with 0 rebuilds across the loop. has_path over 2,000 pairs went from 17.27 s to 0.031 s. Details in the capsule at scratch/refactor_verification/v1/. tests/ held only __init__.py, so tests/test_merge_integration.py adds a self-contained net: 5 tests building a fragmented graph in-process and running the real generator and accept loop, including the has_path-guarded merge loop from split_inference.py:276 and the conversion round trip. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Collaborator
Author
CI note
Locally, on Python 3.10: 🤖 Generated with Claude Code |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Converts this repo's call sites to the
SkeletonGraphAPI thatAllenInstitute/arboristnow exposes: graphalgorithms as methods rather than
networkxmodule functions, and node attribute allocation through theregistry rather than by hand. Two commits, rebased onto current
main(8fe9593).Important
Requires AllenInstitute/arborist#19 to merge first. That PR drops
nx.GraphasSkeletonGraph'sparent in favour of a CSR array backing.
FragmentsGraphsubclassesSkeletonGraph, sonx.f(graph, ...)stops working the moment it lands — the 15 call sites here are the breaking surface. This PR is a no-op
against today's arborist except for the registry cleanup, and it must not merge before arborist's.
Why this repo is affected at all
ProposalGraph(FragmentsGraph)→FragmentsGraph(SkeletonGraph)→SkeletonGraph(nx.Graph). Everynx.connected_components(self)in this repo works only because of that last link. Replacing it withSkeletonGraph(CSRGraph)breaks them, and the fix is one mechanical pattern.The motivation is memory:
nx.Graphcosts ~554 B/node, and ~258 B/node of that is charged even with zeroedges. Against the MICrONS excitatory cohort (49,803 cells / 94.2M nodes) that is 52 GB on a 62 GB machine
versus 4.0 GB CSR-backed. Full numbers in the arborist PR.
What changed
refactor: use SkeletonGraph node attribute registry—FragmentsGraph.loadallocatednode_component_id/node_radius/node_xyzby hand with their dtypes spelled out, andload_somasgrewthem through a local
resize_node_attrhelper. Both now go through the registry:self.init_node_attrs(num_nodes)replaces the threenp.zeroscallsself.grow_node_attrs(num_nodes + num_somas)replaces threeresize_node_attrcalls, and the helper isdeleted
ProposalGraph.from_fragments_graph/to_fragments_graphreplace three explicit.copy()lines withgraph.copy_node_attrs(pg)This is what makes the dtypes single-sourced: previously
radiuswas float16 here and in arborist's loaderindependently, and adding a fourth attribute meant editing three sites in two repos. The registry also
carries the SWC
typecolumn, soload_somasnow setsself.node_type[node_id] = 1— soma nodes werepreviously written out as type 2 (axon), because the loader read
typeto flag somas and then discarded it.refactor: call graph algorithms as methods, not nx module functions— 16 sites across 9 files:connected_componentsvisualization.py,geometry_util.py,groundtruth_generation.py,merge_datamodules.py,search_datasets.pynumber_connected_componentsfragments_graph.py×2,geometry_util.pyshortest_pathgroundtruth_generation.py,merge_inference.pyhas_pathfragments_graph.py,split_inference.pydfs_edgessearch_datasets.py×2node_connected_componentsearch_datasets.pynx.Graph()→SkeletonGraph()visualization.py:51, the default emptygt_graphThe one non-mechanical change is
ProposalGraph's two conversion paths, which built a graph withcls.__new__(cls)+nx.Graph.__init__(pg)+pg.update(graph).updateis an nx dict merge and has no CSRequivalent, so both become
pg._init_structure()+pg.copy_structure_from(graph)— an explicit structurecopy that shares
indptr/indicesrather than re-inserting every edge.copy_structure_fromis added by thefourth commit of the arborist PR specifically for these two sites.
networkx stays a dependency
Three files still import it, all correctly:
merge_inference.py:318andgroundtruth_generation.py:352catchnx.NetworkXNoPathby name —CSRGraph.shortest_pathraises it deliberately, so theexceptclauses are untouchedproposal_graph.py:466—ProposalComputationGraph(nx.Graph)is a separate small graph, not a skeleton,and is left alone
fragments_graph.pyandvisualization.pylose theirnetworkximports because nothing in them uses it anymore.
Testing
tests/held only__init__.py, so this addstests/test_merge_integration.py— 5 tests, and it targetsthe specific thing CSR is structurally bad at.
merge_proposalcallsadd_edgeand then immediately hasupdate_component_idsBFS the merged component throughneighbors(), so the workload interleaves mutationand adjacency reads one edge at a time. Unit coverage of the primitives (25 tests, in the arborist PR) does
not exercise that interleaving.
It builds its own 24-fragment graph rather than loading data, so it has no data dependency, and runs the real
ProposalGeneratoratsearch_radius=25. The tests check that the accept loop is self-consistent, that thehas_pathguard keeps the result a forest, thatcomponent_ids track the merges, and that theFragmentsGraph↔ProposalGraphround trip preserves structure and node attributes.5 tests pass in 4.9 s.
The full-scale check needs the real cohort and so lives outside this repo, in the capsule: 20 Patch-seq cells
with synthetic soma links suppressed to produce a genuinely fragmented graph — 159,614 nodes, 635 components,
3,662 proposals — cross-checked against a recorded pre-swap nx baseline on 11 invariants including the
proposal hash, accept/block counts, final component count and edge count, and a
component_idchecksum.All 11 match. That baseline was re-earned on this exact base rather than carried over, because #760/#761/
#762 changed
merge_proofreadingitself after it was first recorded.Incidentally measured there:
has_pathis 493× faster CSR-backed (17.27 s → 0.031 s over 2,000 calls),which is the two
has_pathsites above.One thing worth deciding separately
arboristis not declared inpyproject.toml— not in the 26 dependencies, and there is norequirements.txtorsetup.pymentioning it. The import resolves only because deployments happen topip install -ea local clone, which means nothing can refuse an incompatible arborist at install time. Thatis exactly the failure this PR's ordering constraint is about. Adding
arborist @ git+https://github.com/ AllenInstitute/arborist@<sha>would make the coupling checkable, but it is a packaging decision with its ownconsequences for how this repo is deployed, so it is not bundled in here.
🤖 Generated with Claude Code