Skip to content

Commit 9aa624b

Browse files
Add comprehensive solver checks
1 parent cb6455a commit 9aa624b

2 files changed

Lines changed: 455 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
from dataclasses import dataclass
5+
from statistics import mean
6+
from time import perf_counter
7+
from typing import Callable
8+
9+
import numpy as np
10+
11+
12+
PROBLEMS = ("2d", "3d", "grid")
13+
14+
15+
@dataclass(frozen=True)
16+
class SolverConfig:
17+
make_bic_solver: Callable[[], object]
18+
make_nifty_factory: Callable[[object], object]
19+
20+
21+
def solver_configs():
22+
import bioimage_cpp as bic
23+
24+
return {
25+
"lifted_greedy_additive": SolverConfig(
26+
make_bic_solver=lambda: bic.graph.LiftedGreedyAdditiveMulticut(),
27+
make_nifty_factory=lambda objective: objective.liftedMulticutGreedyAdditiveFactory(),
28+
),
29+
"lifted_kernighan_lin": SolverConfig(
30+
make_bic_solver=lambda: bic.graph.LiftedKernighanLinMulticut(
31+
number_of_outer_iterations=10
32+
),
33+
make_nifty_factory=lambda objective: objective.chainedSolversFactory(
34+
[
35+
objective.liftedMulticutGreedyAdditiveFactory(),
36+
objective.liftedMulticutKernighanLinFactory(
37+
numberOfOuterIterations=10
38+
),
39+
]
40+
),
41+
),
42+
}
43+
44+
45+
def load_problem(size: str, *, timeout: float):
46+
import bioimage_cpp as bic
47+
import nifty
48+
import nifty.graph.opt.lifted_multicut as nlmc
49+
50+
problem = bic.graph.load_lifted_multicut_problem(size, timeout=timeout)
51+
bic_graph = bic.graph.UndirectedGraph.from_edges(problem.n_nodes, problem.local_uvs)
52+
nifty_graph = nifty.graph.undirectedGraph(int(problem.n_nodes))
53+
nifty_graph.insertEdges(problem.local_uvs.astype(np.uint64, copy=False))
54+
55+
def make_nifty_objective():
56+
objective = nlmc.liftedMulticutObjective(nifty_graph)
57+
objective.setGraphEdgesCosts(problem.local_costs)
58+
if problem.lifted_uvs.shape[0] > 0:
59+
objective.setCosts(
60+
problem.lifted_uvs.astype(np.uint64, copy=False),
61+
problem.lifted_costs.astype(np.float64, copy=False),
62+
)
63+
return objective
64+
65+
return bic_graph, nifty_graph, make_nifty_objective, problem
66+
67+
68+
def make_bic_objective(bic_graph, problem):
69+
import bioimage_cpp as bic
70+
71+
return bic.graph.LiftedMulticutObjective(
72+
bic_graph,
73+
problem.local_costs,
74+
lifted_uvs=problem.lifted_uvs,
75+
lifted_costs=problem.lifted_costs,
76+
)
77+
78+
79+
def bic_energy(bic_graph, problem, labels: np.ndarray) -> float:
80+
return float(make_bic_objective(bic_graph, problem).energy(labels))
81+
82+
83+
def nifty_energy(nifty_objective, labels: np.ndarray) -> float:
84+
return float(nifty_objective.evalNodeLabels(labels.astype(np.uint64, copy=False)))
85+
86+
87+
def evaluate(problem_name: str, solver_name: str, config: SolverConfig, args):
88+
bic_graph, _, make_nifty_objective, problem = load_problem(
89+
problem_name, timeout=args.timeout
90+
)
91+
bic_energies = []
92+
nifty_energies = []
93+
bic_runtimes = []
94+
nifty_runtimes = []
95+
96+
for _ in range(args.n_repeats):
97+
bic_objective = make_bic_objective(bic_graph, problem)
98+
start = perf_counter()
99+
bic_labels = config.make_bic_solver().optimize(bic_objective)
100+
bic_runtimes.append(perf_counter() - start)
101+
bic_energies.append(bic_energy(bic_graph, problem, bic_labels))
102+
103+
nifty_objective = make_nifty_objective()
104+
start = perf_counter()
105+
nifty_labels = (
106+
config.make_nifty_factory(nifty_objective)
107+
.create(nifty_objective)
108+
.optimize()
109+
)
110+
nifty_runtimes.append(perf_counter() - start)
111+
nifty_energies.append(nifty_energy(nifty_objective, np.asarray(nifty_labels)))
112+
113+
bic_runtime = mean(bic_runtimes)
114+
nifty_runtime = mean(nifty_runtimes)
115+
return {
116+
"problem": problem_name,
117+
"solver": solver_name,
118+
"nodes": int(problem.n_nodes),
119+
"local_edges": int(problem.local_uvs.shape[0]),
120+
"lifted_edges": int(problem.lifted_uvs.shape[0]),
121+
"bic_energy": mean(bic_energies),
122+
"nifty_energy": mean(nifty_energies),
123+
"energy_diff": mean(bic_energies) - mean(nifty_energies),
124+
"bic_runtime_s": bic_runtime,
125+
"nifty_runtime_s": nifty_runtime,
126+
"runtime_ratio": nifty_runtime / bic_runtime if bic_runtime > 0 else float("inf"),
127+
}
128+
129+
130+
def format_float(value: float) -> str:
131+
return f"{value:.6g}"
132+
133+
134+
def print_markdown_table(rows: list[dict]) -> None:
135+
headers = [
136+
"problem",
137+
"solver",
138+
"nodes",
139+
"local_edges",
140+
"lifted_edges",
141+
"bic_energy",
142+
"nifty_energy",
143+
"energy_diff",
144+
"bic_runtime_s",
145+
"nifty_runtime_s",
146+
"runtime_ratio_nifty_over_bic",
147+
]
148+
print("| " + " | ".join(headers) + " |")
149+
print("| " + " | ".join(["---"] * len(headers)) + " |")
150+
for row in rows:
151+
values = [
152+
row["problem"],
153+
row["solver"],
154+
str(row["nodes"]),
155+
str(row["local_edges"]),
156+
str(row["lifted_edges"]),
157+
format_float(row["bic_energy"]),
158+
format_float(row["nifty_energy"]),
159+
format_float(row["energy_diff"]),
160+
format_float(row["bic_runtime_s"]),
161+
format_float(row["nifty_runtime_s"]),
162+
format_float(row["runtime_ratio"]),
163+
]
164+
print("| " + " | ".join(values) + " |")
165+
166+
167+
def main() -> None:
168+
configs = solver_configs()
169+
parser = argparse.ArgumentParser(
170+
description=(
171+
"Evaluate matched bioimage-cpp and nifty lifted multicut solvers "
172+
"on registered lifted multicut problems."
173+
)
174+
)
175+
parser.add_argument(
176+
"--solvers",
177+
nargs="+",
178+
choices=tuple(configs.keys()),
179+
default=tuple(configs.keys()),
180+
help="Solvers to evaluate. Defaults to all.",
181+
)
182+
parser.add_argument(
183+
"--problems",
184+
nargs="+",
185+
choices=PROBLEMS,
186+
default=PROBLEMS,
187+
help="Problems to evaluate. Defaults to all.",
188+
)
189+
parser.add_argument("--n-repeats", type=int, default=1)
190+
parser.add_argument("--timeout", type=float, default=60.0)
191+
args = parser.parse_args()
192+
193+
if args.n_repeats < 1:
194+
raise ValueError("--n-repeats must be at least 1")
195+
196+
rows = []
197+
for problem_name in args.problems:
198+
for solver_name in args.solvers:
199+
rows.append(evaluate(problem_name, solver_name, configs[solver_name], args))
200+
print_markdown_table(rows)
201+
202+
203+
if __name__ == "__main__":
204+
main()

0 commit comments

Comments
 (0)