@@ -33,6 +33,57 @@ Before introducing union-finds, priority queues, edge hashing, stride math, thre
3333
3434If a needed helper does not exist but is generally useful, add it to ` detail/ ` as a header-only utility with a focused API rather than inlining it into one algorithm. Keep the contract small and well-documented; over time ` detail/ ` is what lets multiple modules stay small and consistent.
3535
36+ ## Reusable algorithmic infrastructure
37+
38+ Some larger pieces of infrastructure sit above ` detail/ ` but are still
39+ intended to be reused across objective types. Check these before starting a
40+ new fusion-move / proposal-based / contraction-based solver:
41+
42+ - ` include/bioimage_cpp/graph/detail/fusion_contract.hxx ` — objective-agnostic
43+ agreement-projection primitive. `contract_by_agreement(graph, proposals,
44+ n_proposals, ...)` returns ` {contracted_graph, contracted_edge_of_original,
45+ root_of_node}`. Already supports N ≥ 1 proposals; reused by both pairwise
46+ and joint multi-proposal fuses.
47+ - ` include/bioimage_cpp/graph/proposal_generator.hxx ` — ` ProposalGeneratorBase `
48+ abstract class. Concrete generators (Watershed, GreedyAdditiveMulticut) live
49+ in ` proposal_generators/ ` and depend only on ` (graph, edge_costs) ` plus an
50+ RNG seed. They emit ` std::vector<std::uint64_t> ` node labelings and are
51+ therefore reusable across multicut, lifted multicut, mincut, etc.
52+ - ` include/bioimage_cpp/graph/multicut/greedy_additive.hxx ` — exposes
53+ ` GreedyAdditiveWorkspace ` so multiple invocations on different graphs
54+ share scratch buffers. Use this pattern (workspace + ` reset(graph) ` ) when
55+ a fusion-move driver calls a sub-solver inside its iteration loop.
56+ - ` UndirectedGraph::from_sorted_unique_edges(N, edges, populate_lookup=false) `
57+ — bulk graph construction without the per-edge hash insertion in
58+ ` insert_edge ` . Pair with ` populate_lookup=false ` when the consumer only
59+ walks edges / adjacency (the multicut sub-solvers do).
60+ - ` detail/threading.hxx::parallel_for_chunks ` — the only threading primitive
61+ we use. New parallel solvers should not introduce alternatives.
62+
63+ When porting fusion moves to a new objective (e.g. lifted multicut):
64+
65+ 1 . The driver loop in ` multicut/fusion_move.hxx::FusionMoveSolver::optimize `
66+ is short and dense. Duplicate it for the new objective rather than
67+ abstracting it via a template/CRTP base — the moving parts (cost
68+ aggregation, energy evaluator, sub-solver type) are objective-specific
69+ and template gymnastics buy little.
70+ 2 . Reuse ` contract_by_agreement ` unchanged; it operates on the * base* graph
71+ only.
72+ 3 . Write a new ` fuse_multi(...) ` that aggregates * both* base and lifted (or
73+ other auxiliary) weights through ` contraction.contracted_edge_of_original `
74+ and ` contraction.root_of_node ` , calls the new objective's sub-solver, and
75+ lifts labels back via ` root_of_node ` .
76+ 4 . Reuse the existing ` WatershedProposalGenerator ` and
77+ ` GreedyAdditiveMulticutProposalGenerator ` verbatim; they only depend on
78+ the base graph + base costs and emit node labelings. Add objective-specific
79+ generators (e.g. ` GreedyAdditiveLiftedMulticutProposalGenerator ` ) only if a
80+ meaningful new proposal strategy emerges.
81+ 5 . Reuse the per-thread parallel pattern from ` optimize ` : stage-1 parallel
82+ proposal generation + parallel pairwise fuse, stage-2 sequential joint
83+ multi-fuse on leftover candidates. Per-thread ` GreedyAdditiveWorkspace `
84+ becomes per-thread ` <NewObjective>Workspace ` if the new sub-solver follows
85+ the same pattern.
86+
3687## Dependencies
3788
3889** Allowed** : C++20 stdlib, ` nanobind ` , ` numpy ` , ` scikit-build-core ` , ` cmake ` , ` pytest ` , ` cibuildwheel ` (CI only).
0 commit comments