Skip to content

Commit 4de7658

Browse files
Implement lifted MC fusion moves
1 parent b9741ef commit 4de7658

8 files changed

Lines changed: 1034 additions & 1 deletion

File tree

MIGRATION_GUIDE.md

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -538,14 +538,45 @@ energy = objective.energy(labels)
538538
- `overwrite_existing` — when `True`, lifted entries that coincide with an
539539
existing edge replace its weight; the default accumulates.
540540

541-
Available solvers (no fusion-move / ILP solvers yet):
541+
Available solvers (no ILP solvers yet):
542542

543543
| nifty factory | bioimage-cpp solver |
544544
| --- | --- |
545545
| `liftedMulticutGreedyAdditiveFactory()` | `LiftedGreedyAdditiveMulticut()` |
546546
| `liftedMulticutKernighanLinFactory(...)` | `LiftedKernighanLinMulticut(...)` |
547+
| `fusionMoveBasedFactory(...)` | `FusionMoveLiftedMulticut(...)` |
547548
| `chainedSolversFactory([...])` | `LiftedChainedSolvers([...])` |
548549

550+
`FusionMoveLiftedMulticut` mirrors `FusionMoveMulticut` (same proposal-generator
551+
plumbing, same threading + multi-proposal joint-fuse semantics, same best-of
552+
safety net). The differences are:
553+
554+
- Proposal generators operate on the *base* graph and base edge costs (only
555+
base-graph edges are candidate cut edges; lifted edges contribute to energy
556+
but cannot be contracted directly). The driver extracts the base costs from
557+
`objective.weights[:objective.number_of_base_edges]` automatically.
558+
- Each fuse contracts the base graph by agreement, aggregates *both* base and
559+
lifted weights onto the contracted lifted-multicut subproblem (lifted edges
560+
whose endpoints land on already-existing contracted base edges fold into
561+
them; the rest become new contracted lifted edges), and solves the
562+
subproblem with a `LiftedMulticutSolver`.
563+
- The default sub-solver and warm-start are `LiftedGreedyAdditiveMulticut`.
564+
Both `LiftedGreedyAdditiveMulticut` and `LiftedKernighanLinMulticut` are
565+
pluggable via `sub_solver=`.
566+
567+
```python
568+
solver = bic.graph.FusionMoveLiftedMulticut(
569+
proposal_generator=bic.graph.WatershedProposalGenerator(
570+
sigma=1.0, n_seeds_fraction=0.1, seed=0,
571+
),
572+
sub_solver=bic.graph.LiftedKernighanLinMulticut(number_of_outer_iterations=3),
573+
number_of_iterations=10,
574+
stop_if_no_improvement=4,
575+
number_of_threads=4,
576+
)
577+
labels = solver.optimize(objective)
578+
```
579+
549580
A typical warm-started solve combines greedy and KL:
550581

551582
```python

development/graph/lifted_multicut/_compatibility.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ def parser(description: str) -> argparse.ArgumentParser:
2828
default=60.0,
2929
help="Download timeout in seconds if the lifted problem is not cached.",
3030
)
31+
arg_parser.add_argument(
32+
"--threads",
33+
type=int,
34+
default=1,
35+
help="Number of threads for solvers that support it.",
36+
)
3137
arg_parser.add_argument(
3238
"--energy-bound",
3339
type=float,
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import bioimage_cpp as bic
4+
5+
from _compatibility import parser, run_comparison
6+
7+
8+
def main() -> None:
9+
args = parser(
10+
"Compare bioimage-cpp and nifty fusion-move lifted multicut at matched "
11+
"settings."
12+
).parse_args()
13+
# Match settings on both sides:
14+
# - threads / parallel-proposals = `--threads N` (default 1). We pin
15+
# P = T explicitly on the bic side so the comparison is
16+
# apples-to-apples regardless of either library's default for P.
17+
# - greedy-additive warm-start: we do it automatically when the
18+
# objective's labels are the trivial singleton labeling. nifty's
19+
# `fusionMoveBasedFactory` has no warm-start parameter, so we make it
20+
# explicit on the nifty side by chaining greedy-additive in front.
21+
# - greedy-additive sub-solver (the implicit default on both sides).
22+
# - watershed proposal generator seeded from local edges. The bic
23+
# proposal generator only sees the base graph, so seeding from
24+
# "local" mirrors that exactly; the nifty default
25+
# `SEED_FROM_LIFTED` would put seeds where bic cannot.
26+
threads = int(args.threads)
27+
# nifty's lifted-multicut fusion-move backend only implements single-
28+
# threaded execution; passing numberOfThreads > 1 raises at solve time.
29+
# Cap the nifty side at 1 so multi-threaded bic benchmarks still run, and
30+
# surface the asymmetry in the printed output.
31+
nifty_threads = 1
32+
if threads != nifty_threads:
33+
print(
34+
f"note: nifty lifted fusion-move runs single-threaded; comparing "
35+
f"bic threads={threads} vs nifty threads={nifty_threads}."
36+
)
37+
run_comparison(
38+
"lifted_fusion_move",
39+
lambda: bic.graph.FusionMoveLiftedMulticut(
40+
proposal_generator=bic.graph.WatershedProposalGenerator(),
41+
number_of_iterations=10,
42+
stop_if_no_improvement=4,
43+
number_of_threads=threads,
44+
number_of_parallel_proposals=threads,
45+
),
46+
lambda objective: objective.chainedSolversFactory(
47+
[
48+
objective.liftedMulticutGreedyAdditiveFactory(),
49+
objective.fusionMoveBasedFactory(
50+
proposalGenerator=objective.watershedProposalGenerator(
51+
seedingStrategy="SEED_FROM_LOCAL",
52+
),
53+
numberOfIterations=10,
54+
stopIfNoImprovement=4,
55+
numberOfThreads=nifty_threads,
56+
),
57+
]
58+
),
59+
args,
60+
)
61+
62+
63+
if __name__ == "__main__":
64+
main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include "bioimage_cpp/graph/lifted_multicut/fusion_move.hxx"
34
#include "bioimage_cpp/graph/lifted_multicut/greedy_additive.hxx"
45
#include "bioimage_cpp/graph/lifted_multicut/kernighan_lin.hxx"
56
#include "bioimage_cpp/graph/lifted_multicut/objective.hxx"

0 commit comments

Comments
 (0)