Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion MIGRATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,14 +588,45 @@ energy = objective.energy(labels)
- `overwrite_existing` — when `True`, lifted entries that coincide with an
existing edge replace its weight; the default accumulates.

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

| nifty factory | bioimage-cpp solver |
| --- | --- |
| `liftedMulticutGreedyAdditiveFactory()` | `LiftedGreedyAdditiveMulticut()` |
| `liftedMulticutKernighanLinFactory(...)` | `LiftedKernighanLinMulticut(...)` |
| `fusionMoveBasedFactory(...)` | `FusionMoveLiftedMulticut(...)` |
| `chainedSolversFactory([...])` | `LiftedChainedSolvers([...])` |

`FusionMoveLiftedMulticut` mirrors `FusionMoveMulticut` (same proposal-generator
plumbing, same threading + multi-proposal joint-fuse semantics, same best-of
safety net). The differences are:

- Proposal generators operate on the *base* graph and base edge costs (only
base-graph edges are candidate cut edges; lifted edges contribute to energy
but cannot be contracted directly). The driver extracts the base costs from
`objective.weights[:objective.number_of_base_edges]` automatically.
- Each fuse contracts the base graph by agreement, aggregates *both* base and
lifted weights onto the contracted lifted-multicut subproblem (lifted edges
whose endpoints land on already-existing contracted base edges fold into
them; the rest become new contracted lifted edges), and solves the
subproblem with a `LiftedMulticutSolver`.
- The default sub-solver and warm-start are `LiftedGreedyAdditiveMulticut`.
Both `LiftedGreedyAdditiveMulticut` and `LiftedKernighanLinMulticut` are
pluggable via `sub_solver=`.

```python
solver = bic.graph.FusionMoveLiftedMulticut(
proposal_generator=bic.graph.WatershedProposalGenerator(
sigma=1.0, n_seeds_fraction=0.1, seed=0,
),
sub_solver=bic.graph.LiftedKernighanLinMulticut(number_of_outer_iterations=3),
number_of_iterations=10,
stop_if_no_improvement=4,
number_of_threads=4,
)
labels = solver.optimize(objective)
```

A typical warm-started solve combines greedy and KL:

```python
Expand Down
431 changes: 312 additions & 119 deletions development/graph/lifted_multicut/PERFORMANCE_NOTES.md

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions development/graph/lifted_multicut/_compatibility.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ def parser(description: str) -> argparse.ArgumentParser:
default=60.0,
help="Download timeout in seconds if the lifted problem is not cached.",
)
arg_parser.add_argument(
"--threads",
type=int,
default=1,
help="Number of threads for solvers that support it.",
)
arg_parser.add_argument(
"--energy-bound",
type=float,
Expand Down
64 changes: 64 additions & 0 deletions development/graph/lifted_multicut/check_fusion_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from __future__ import annotations

import bioimage_cpp as bic

from _compatibility import parser, run_comparison


def main() -> None:
args = parser(
"Compare bioimage-cpp and nifty fusion-move lifted multicut at matched "
"settings."
).parse_args()
# Match settings on both sides:
# - threads / parallel-proposals = `--threads N` (default 1). We pin
# P = T explicitly on the bic side so the comparison is
# apples-to-apples regardless of either library's default for P.
# - greedy-additive warm-start: we do it automatically when the
# objective's labels are the trivial singleton labeling. nifty's
# `fusionMoveBasedFactory` has no warm-start parameter, so we make it
# explicit on the nifty side by chaining greedy-additive in front.
# - greedy-additive sub-solver (the implicit default on both sides).
# - watershed proposal generator seeded from local edges. The bic
# proposal generator only sees the base graph, so seeding from
# "local" mirrors that exactly; the nifty default
# `SEED_FROM_LIFTED` would put seeds where bic cannot.
threads = int(args.threads)
# nifty's lifted-multicut fusion-move backend only implements single-
# threaded execution; passing numberOfThreads > 1 raises at solve time.
# Cap the nifty side at 1 so multi-threaded bic benchmarks still run, and
# surface the asymmetry in the printed output.
nifty_threads = 1
if threads != nifty_threads:
print(
f"note: nifty lifted fusion-move runs single-threaded; comparing "
f"bic threads={threads} vs nifty threads={nifty_threads}."
)
run_comparison(
"lifted_fusion_move",
lambda: bic.graph.FusionMoveLiftedMulticut(
proposal_generator=bic.graph.WatershedProposalGenerator(),
number_of_iterations=10,
stop_if_no_improvement=4,
number_of_threads=threads,
number_of_parallel_proposals=threads,
),
lambda objective: objective.chainedSolversFactory(
[
objective.liftedMulticutGreedyAdditiveFactory(),
objective.fusionMoveBasedFactory(
proposalGenerator=objective.watershedProposalGenerator(
seedingStrategy="SEED_FROM_LOCAL",
),
numberOfIterations=10,
stopIfNoImprovement=4,
numberOfThreads=nifty_threads,
),
]
),
args,
)


if __name__ == "__main__":
main()
28 changes: 28 additions & 0 deletions development/graph/lifted_multicut/evaluate_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ class SolverConfig:
def solver_configs():
import bioimage_cpp as bic

# Fair single-threaded comparison: nifty's lifted fusion-move backend is
# single-threaded, so we pin the bic side to threads=1 and one parallel
# proposal per iteration (matching nifty's "generate one, fuse" loop) and
# chain greedy-additive in front of nifty to mirror bic's auto warm-start.
# Watershed seeding strategy is forced to SEED_FROM_LOCAL on the nifty
# side because the bic proposal generator only sees the base graph.
return {
"lifted_greedy_additive": SolverConfig(
make_bic_solver=lambda: bic.graph.LiftedGreedyAdditiveMulticut(),
Expand All @@ -39,6 +45,28 @@ def solver_configs():
]
),
),
"lifted_fusion_move": SolverConfig(
make_bic_solver=lambda: bic.graph.FusionMoveLiftedMulticut(
proposal_generator=bic.graph.WatershedProposalGenerator(),
number_of_iterations=10,
stop_if_no_improvement=4,
number_of_threads=1,
number_of_parallel_proposals=1,
),
make_nifty_factory=lambda objective: objective.chainedSolversFactory(
[
objective.liftedMulticutGreedyAdditiveFactory(),
objective.fusionMoveBasedFactory(
proposalGenerator=objective.watershedProposalGenerator(
seedingStrategy="SEED_FROM_LOCAL",
),
numberOfIterations=10,
stopIfNoImprovement=4,
numberOfThreads=1,
),
]
),
),
}


Expand Down
1 change: 1 addition & 0 deletions include/bioimage_cpp/graph/lifted_multicut.hxx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "bioimage_cpp/graph/lifted_multicut/fusion_move.hxx"
#include "bioimage_cpp/graph/lifted_multicut/greedy_additive.hxx"
#include "bioimage_cpp/graph/lifted_multicut/kernighan_lin.hxx"
#include "bioimage_cpp/graph/lifted_multicut/objective.hxx"
Loading
Loading