Spooky / quantum /qubo_cli.py
JavideuS's picture
Deploy a665039
beeea66 verified
Raw
History Blame Contribute Delete
30.8 kB
"""
qubo_cli.py — Command-line interface for the Quantum QUBO Pathfinding solver.
This script exposes all runtime parameters via argparse, making it suitable
for deployment, scripted experiments, and integration with external systems.
For a hands-on Python example with inline comments and full control, see
qubo.py instead. This script mirrors its logic but driven entirely by CLI args.
Usage examples:
# Basic solve with DWave
python qubo_cli.py --map maps/synthetic/10x10/obs10x10_hard --problem four_robots --solver dwave
# Benchmark run with PennyLane on GPU
python qubo_cli.py --map maps/synthetic/5x5/obs5x5 --solver pennylane --device lightning.gpu --benchmark --num-runs 5
# Benchmark run with PennyLane on Windows (no lightning.gpu wheels; use lightning.qubit)
python qubo_cli.py --map maps/synthetic/5x5/obs5x5 --solver pennylane --device lightning.qubit --benchmark --num-runs 5
# Graph-based problem
python qubo_cli.py --map maps/graph/city --problem two_robots --builder graph --solver dwave --penalty-set graph
# Override penalties individually
python qubo_cli.py --map maps/synthetic/10x10/obs10x10_hard --K-hot 9 --K-adj 4.8 --K-start 6.5 --K-goal 3.0
# Suppress all output (silent mode)
python qubo_cli.py --map maps/synthetic/10x10/obs10x10_hard --verbose 0
# Solve and open an animated visualization in the browser
python qubo_cli.py --map maps/synthetic/5x5/obs5x5 --problem two_robots --visualize
# Solve and save the animation as a GIF (or .html for interactive)
python qubo_cli.py --map maps/synthetic/5x5/obs5x5 --problem two_robots --visualize -o run.gif
"""
import argparse
import sys
from pathlib import Path
from pennylane import numpy as np
from quantum.solvers import SolverFactory
from quantum.pathFormulation import PathfindingProblem
import quantum.config.parser as config_parser
from quantum.builder import (
QUBOBuilder, GraphQUBO, GridILPBuilder, GraphILPBuilder,
GridCBSBuilder, GraphCBSBuilder,
)
import quantum.benchmark.benchmark as bm_module
from quantum.utils.logger import set_verbose_level, get_logger
from quantum.utils.paths import clip_path_at_goal
import time
_HERE = Path(__file__).parent # quantum/
# ---------------------------------------------------------------------------
# Argument parser
# ---------------------------------------------------------------------------
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
prog="qubo_cli",
description=(
"Quantum QUBO Pathfinding solver CLI.\n\n"
"For a fully-annotated Python example, see qubo.py."
),
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
# ---- Problem definition ------------------------------------------------
prob = parser.add_argument_group("Problem")
prob.add_argument(
"--map",
"-m",
required=True,
metavar="PATH",
help="Path (without extension) to the map config, e.g. maps/synthetic/10x10/obs10x10_hard",
)
prob.add_argument(
"--problem",
"-p",
default="four_robots",
metavar="NAME",
help="Problem name defined inside the map config (default: four_robots)",
)
prob.add_argument(
"--builder",
"-b",
choices=["grid", "graph"],
default="grid",
help="QUBO builder type: 'grid' (QUBOBuilder) or 'graph' (GraphQUBO) (default: grid)",
)
prob.add_argument(
"--distance-scaling",
default="enhanced_linear",
metavar="MODE",
help="Distance scaling mode passed to QUBOBuilder (default: enhanced_linear)",
)
prob.add_argument(
"--window-limit",
default=[],
nargs="+",
metavar="N or ROBOT=N",
help=(
"Window step limit. Pass a single integer to cap all robots globally "
"(e.g. --window-limit 6), or 'robot_id=N' pairs for per-robot limits "
"(e.g. --window-limit robot_0=5 robot_1=3)."
),
)
prob.add_argument(
"--var-limit",
type=int,
default=None,
metavar="N",
help="Variable limit passed to QUBO builders. (default: Grid=1650, Graph=1201)",
)
prob.add_argument(
"--coordinate-format",
choices=["matrix", "cartesian"],
default="matrix",
help=(
"Coordinate convention for start/goal in the problem config and for "
"printed/visualized output paths: 'matrix' (row, col), Spooky's native "
"convention (default), or 'cartesian' (x, y) robotics/Y-up. Per-robot "
"'coordinate_format' entries in the map YAML take precedence over this."
),
)
prob.add_argument(
"--no-reduction-log",
action="store_true",
default=False,
help=(
"Disable reduction logging during QUBO preprocessing. "
"Faster, but prevents BFS-based variable unfixing during diagonal reduction. "
"Use to benchmark the overhead of reduction tracking."
),
)
# ---- Penalty set -------------------------------------------------------
pen = parser.add_argument_group("Penalties")
pen.add_argument(
"--penalty-set",
default="swap",
metavar="SET",
help=(
"Named penalty set from config.yaml to use as base "
"(default: swap). Overridden by individual --K-* flags."
),
)
# Individual overrides — if given, they take precedence over the set
pen.add_argument(
"--K-hot",
type=float,
default=None,
metavar="VAL",
help="Override K_hot penalty",
)
pen.add_argument(
"--K-adj",
type=float,
default=None,
metavar="VAL",
help="Override K_adj penalty",
)
pen.add_argument(
"--K-start",
type=float,
default=None,
metavar="VAL",
help="Override K_start penalty",
)
pen.add_argument(
"--K-goal",
type=float,
default=None,
metavar="VAL",
help="Override K_goal penalty",
)
pen.add_argument(
"--K-lock",
type=float,
default=None,
metavar="VAL",
help="Override K_lock penalty",
)
pen.add_argument(
"--K-bt", type=float, default=None, metavar="VAL", help="Override K_bt penalty"
)
pen.add_argument(
"--K-tp", type=float, default=None, metavar="VAL", help="Override K_tp penalty"
)
pen.add_argument(
"--K-crash",
type=float,
default=None,
metavar="VAL",
help="Override K_crash penalty",
)
pen.add_argument(
"--K-swap",
type=float,
default=None,
metavar="VAL",
help="Override K_swap penalty",
)
pen.add_argument(
"--K-obs",
type=float,
default=None,
metavar="VAL",
help="Override K_obs penalty",
)
pen.add_argument(
"--K-goal-approx",
type=float,
default=None,
metavar="VAL",
help="Override K_goal_approx penalty",
)
# ---- Solver ------------------------------------------------------------
sol = parser.add_argument_group("Solver")
sol.add_argument(
"--solver",
"-s",
choices=["dwave", "pennylane", "qiskit_remote", "qiskit_iqm", "ilp", "cbs"],
default="dwave",
help="Solver backend (default: dwave)",
)
sol.add_argument(
"--normalize-scale",
type=float,
default=None,
metavar="N",
help=(
"QUBO normalization scale factor. "
"Defaults: dwave=4.0, pennylane=1.0. "
"See qubo.py comments for per-qubit guidance."
),
)
sol.add_argument(
"--num-reads",
default=None,
metavar="N|auto",
help=(
"Number of solver reads. Pass an integer or 'auto' "
"(default: dwave=4, pennylane=auto)"
),
)
sol.add_argument(
"--no-preprocess",
action="store_true",
default=False,
help=(
"Disable variable reduction (QUBO: BFS logical-variable reduction, "
"diagonal pruning, and the correction loop, runs the simple "
"raw-sampler loop instead. ILP: BFS reachability pruning of the "
"decision variables, solves the unpruned model instead). Enabled "
"by default for both."
),
)
sol.add_argument(
"--pyomo-solver",
default="appsi_highs",
metavar="NAME",
help=(
"Pyomo solver backend name (only used with --solver ilp), "
"e.g. 'appsi_highs' (default), 'cbc', 'glpk'."
),
)
sol.add_argument(
"--time-limit",
type=float,
default=30,
metavar="SECONDS",
help=(
"Solver time limit in seconds (--solver ilp or cbs). ILP/HiGHS "
"returns its best incumbent found so far if the limit is hit "
"before proving optimality; CBS returns its best incumbent "
"found in its constraint-tree search so far. Default: 30."
),
)
sol.add_argument(
"--node-limit",
type=int,
default=5000,
metavar="N",
help=(
"Max constraint-tree nodes CBS will expand (only used with "
"--solver cbs) — a second safety cap alongside --time-limit, "
"whichever is hit first stops the search. Default: 5000."
),
)
# PennyLane / QAOA-specific
pl = parser.add_argument_group(
"PennyLane / QAOA (only used when --solver pennylane)"
)
pl.add_argument(
"--device",
default="lightning.gpu",
metavar="DEV",
help=(
"PennyLane device string, e.g. 'lightning.gpu', 'lightning.qubit', "
"'qiskit.remote' (default: lightning.gpu)"
),
)
pl.add_argument(
"--threads",
type=int,
default=None,
metavar="N",
help=(
"CPU thread count for lightning.qubit's OpenMP backend "
"(default.qubit/lightning.gpu unaffected). Default: unset, uses "
"every available core (OpenMP's own default)."
),
)
pl.add_argument(
"--layers",
type=int,
default=2,
metavar="N",
help="Number of QAOA layers (default: 2)",
)
pl.add_argument(
"--optimizer",
default="QNG",
metavar="OPT",
help="Optimizer name passed to PennyLane solver (default: QNG)",
)
pl.add_argument(
"--opt-steps",
type=int,
default=30,
metavar="N",
help="Number of optimizer steps (default: 30)",
)
pl.add_argument(
"--init-params",
default=None,
metavar="FILE",
help=(
"Path to a .npy file containing initial QAOA parameters. "
"If omitted, the built-in default params from qubo.py are used."
),
)
pl.add_argument(
"--machine",
default=None,
metavar="NAME",
help=(
"Pin a specific hardware backend/machine (only used with "
"--device qiskit.remote or qiskit.iqm / --solver qiskit_remote or "
"qiskit_iqm). IBM: an exact backend name, e.g. 'ibm_torino' "
"(default: least_busy). IQM: 'sirius', 'garnet', or 'emerald' "
"(default: auto-picks the smallest that fits the window)."
),
)
# ---- Run mode ----------------------------------------------------------
run = parser.add_argument_group("Run mode")
run_ex = run.add_mutually_exclusive_group()
run_ex.add_argument(
"--benchmark",
action="store_true",
help=(
"Run in benchmark mode (multiple runs, saves JSON results with path validation). "
"Use --num-runs 1 as a single validated solve with path checking instead of bare --solve."
),
)
run_ex.add_argument(
"--solve",
action="store_true",
default=True,
help="[default] Run a single solve and print the decoded path",
)
run.add_argument(
"--num-runs",
type=int,
default=10,
metavar="N",
help="Number of benchmark runs (only used with --benchmark, default: 10)",
)
run.add_argument(
"--clip-at-goal",
action="store_true",
help=(
"Trim each robot's printed path once it's parked at goal, keeping "
"only the first arrival (output-only; doesn't affect solving/windowing). "
"Useful for feeding paths to an external planner."
),
)
run.add_argument(
"--benchmark-level",
type=int,
choices=[1, 2, 3],
default=2,
metavar="1|2|3",
help=(
"Benchmark output detail level: "
"1=Summary only, 2=+Paths, 3=+Raw bits (default: 2)"
),
)
# ---- Visualization -----------------------------------------------------
viz_g = parser.add_argument_group("Visualization (single-solve mode only)")
viz_g.add_argument(
"--visualize",
nargs="?",
const="animated",
choices=["animated", "static", "steps"],
default=None,
metavar="MODE",
help=(
"Visualize the solved paths: 'animated' (default), 'static', or "
"'steps'. Opens a browser window unless -o/--output is given."
),
)
viz_g.add_argument(
"--output",
"-o",
default=None,
metavar="FILE",
help=(
"Save the visualization instead of opening a browser. Format from "
"extension: .html (interactive), .gif (animated mode only), "
".png/.svg/.pdf (static image via kaleido)."
),
)
viz_g.add_argument(
"--viz-discrete",
action="store_true",
help=(
"Animate the raw discrete timeline (one frame per QUBO timestep) "
"instead of smooth interpolated motion."
),
)
# ---- Config & misc -----------------------------------------------------
misc = parser.add_argument_group("Config & misc")
misc.add_argument(
"--config",
default=str(_HERE / "config/config.yaml"),
metavar="FILE",
help="Path to the main YAML config file (default: <package>/config/config.yaml)",
)
misc.add_argument(
"--materials",
default=str(_HERE / "config/materials.yaml"),
metavar="FILE",
help="Path to the materials YAML file (default: <package>/config/materials.yaml)",
)
misc.add_argument(
"--verbose",
"-v",
type=int,
choices=[0, 1, 2, 3],
default=None,
metavar="0-3",
help=(
"Verbose level: 0=Silent, 1=Minimal, 2=Standard, 3=Debug. "
"Overrides the value in config.yaml."
),
)
return parser
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def parse_window_limits(raw: list[str], robot_ids) -> dict:
"""
Parse window limit entries into {robot_id: max_steps}.
Accepts either:
- A single integer to cap all robots: ['6']
- Per-robot pairs: ['robot_0=5', 'robot_1=3']
"""
if not raw:
return {}
if len(raw) == 1 and "=" not in raw[0]:
try:
n = int(raw[0])
except ValueError:
raise argparse.ArgumentTypeError(
f"Invalid --window-limit value '{raw[0]}'. "
f"Expected an integer or 'robot_id=N' pairs."
)
return {robot_id: n for robot_id in robot_ids}
limits = {}
for entry in raw:
if "=" not in entry:
raise argparse.ArgumentTypeError(
f"Invalid --window-limit format '{entry}'. "
f"Use a single integer for a global limit or 'robot_id=N' pairs."
)
robot_id, n = entry.split("=", 1)
limits[robot_id.strip()] = int(n.strip())
return limits
def build_penalties(config: dict, args: argparse.Namespace) -> dict:
"""
Start from the named penalty set in config.yaml, then apply any individual
--K-* overrides supplied via the CLI.
"""
penalties = dict(config["penalty_sets"][args.penalty_set])
penalties.setdefault("name", args.penalty_set)
overrides = {
"K_hot": args.K_hot,
"K_adj": args.K_adj,
"K_start": args.K_start,
"K_goal": args.K_goal,
"K_lock": args.K_lock,
"K_bt": args.K_bt,
"K_tp": args.K_tp,
"K_crash": args.K_crash,
"K_swap": args.K_swap,
"K_obs": args.K_obs,
"K_goal_approx": args.K_goal_approx,
}
for key, val in overrides.items():
if val is not None:
penalties[key] = val
return penalties
def build_solver(args: argparse.Namespace, verbose_level: int):
"""Instantiate the correct solver from CLI arguments."""
logger = get_logger()
if args.solver == "dwave":
norm_scale = args.normalize_scale if args.normalize_scale is not None else 4.0
num_reads = (
int(args.num_reads) if args.num_reads and args.num_reads != "auto" else 4
)
logger.minimal(f"Creating DWave solver (scale={norm_scale}, reads={num_reads})")
return SolverFactory.create_solver(
solver="dwave",
normalize_scale=norm_scale,
num_reads=num_reads,
)
elif args.solver == "pennylane":
norm_scale = args.normalize_scale if args.normalize_scale is not None else 1.0
num_reads = (
int(args.num_reads)
if args.num_reads and args.num_reads != "auto"
else "auto"
)
# Initial QAOA params — load from file or use the defaults from qubo.py
if args.init_params:
init_params = np.load(args.init_params, allow_pickle=False)
init_params = np.array(init_params, requires_grad=True)
logger.minimal(f"Loaded init_params from {args.init_params}")
else:
# Default params tuned for 2-layer QAOA (see qubo.py for context)
init_params = np.array(
[[1.70579, 0.70321062], [0.49879231, 0.49412656]],
requires_grad=True,
)
logger.minimal(
f"Creating PennyLane solver (device={args.device}, "
f"layers={args.layers}, optimizer={args.optimizer}, "
f"steps={args.opt_steps}, scale={norm_scale})"
)
return SolverFactory.create_solver(
solver="pennylane",
normalize_scale=norm_scale,
num_reads=num_reads,
layers=args.layers,
optimizer=args.optimizer,
opt_steps=args.opt_steps,
device=args.device,
params=init_params,
verbose_level=verbose_level,
machine=args.machine,
threads=args.threads,
)
elif args.solver == "qiskit_remote":
# Mirrors the qiskit_hardware setup in qubo.py:
# SolverFactory.create_solver(solver="pennylane", device="qiskit.remote", ...)
# normalize_scale defaults to 4.0 (same as qubo.py's qiskit_hardware).
norm_scale = args.normalize_scale if args.normalize_scale is not None else 4.0
num_reads = (
int(args.num_reads)
if args.num_reads and args.num_reads != "auto"
else "auto"
)
device = args.device if args.device != "lightning.gpu" else "qiskit.remote"
if args.init_params:
init_params = np.load(args.init_params, allow_pickle=False)
init_params = np.array(init_params, requires_grad=True)
logger.minimal(f"Loaded init_params from {args.init_params}")
else:
init_params = np.array(
[[1.70579, 0.70321062], [0.49879231, 0.49412656]],
requires_grad=True,
)
logger.minimal(
f"Creating Qiskit-remote solver via PennyLane "
f"(device={device}, layers={args.layers}, "
f"optimizer={args.optimizer}, steps={args.opt_steps}, scale={norm_scale}, "
f"machine={args.machine or 'auto (least_busy)'})"
)
return SolverFactory.create_solver(
solver="pennylane",
normalize_scale=norm_scale,
num_reads=num_reads,
layers=args.layers,
optimizer=args.optimizer,
opt_steps=args.opt_steps,
device=device,
params=init_params,
verbose_level=verbose_level,
machine=args.machine,
)
elif args.solver == "qiskit_iqm":
norm_scale = args.normalize_scale if args.normalize_scale is not None else 4.0
num_reads = (
int(args.num_reads)
if args.num_reads and args.num_reads != "auto"
else "auto"
)
device = args.device if args.device != "lightning.gpu" else "qiskit.iqm"
if args.init_params:
init_params = np.load(args.init_params, allow_pickle=False)
init_params = np.array(init_params, requires_grad=True)
logger.minimal(f"Loaded init_params from {args.init_params}")
else:
init_params = np.array(
[[1.70579, 0.70321062], [0.49879231, 0.49412656]],
requires_grad=True,
)
logger.minimal(
f"Creating IQM solver via PennyLane "
f"(device={device}, layers={args.layers}, "
f"optimizer={args.optimizer}, steps={args.opt_steps}, scale={norm_scale}, "
f"machine={args.machine or 'auto (smallest tier that fits)'})"
)
return SolverFactory.create_solver(
solver="pennylane",
normalize_scale=norm_scale,
num_reads=num_reads,
layers=args.layers,
optimizer=args.optimizer,
opt_steps=args.opt_steps,
device=device,
params=init_params,
verbose_level=verbose_level,
machine=args.machine,
)
elif args.solver == "ilp":
logger.minimal(
f"Creating ILP solver (pyomo backend={args.pyomo_solver}, "
f"time_limit={args.time_limit}s)"
)
return SolverFactory.create_solver(
solver="ilp",
pyomo_solver_name=args.pyomo_solver,
time_limit=args.time_limit,
)
elif args.solver == "cbs":
logger.minimal(
f"Creating CBS solver (node_limit={args.node_limit}, "
f"time_limit={args.time_limit}s)"
)
return SolverFactory.create_solver(
solver="cbs",
node_limit=args.node_limit,
time_limit=args.time_limit,
)
else:
raise ValueError(f"Unknown solver: {args.solver}")
def run_visualization(args: argparse.Namespace, problem, robot_paths: dict) -> None:
"""
Render the solved paths per --visualize / --output.
No --output: opens the figure in the default browser (plotly's fig.show()).
With --output: saves to the file, format chosen by extension.
"""
logger = get_logger()
from quantum.visualizer import QuantumRoboticsVisualizer
if not robot_paths:
logger.minimal("[viz] No robot paths to visualize.")
return
mode = args.visualize
out = args.output
if out and out.lower().endswith(".gif") and mode != "animated":
logger.minimal(
f"[viz] GIF export requires the animated mode — switching from '{mode}'."
)
mode = "animated"
viz = QuantumRoboticsVisualizer(
(problem.grid.M, problem.grid.N),
title=f"{args.problem}{Path(args.map).name}",
)
obstacles = problem.grid.obstacles
if mode == "static":
fig = viz.create_static_plot(
obstacles=obstacles, robot_paths=robot_paths, problem=problem
)
elif mode == "steps":
fig = viz.create_step_by_step_plot(
obstacles, robot_paths=robot_paths, problem=problem
)
else: # animated
fig = viz.create_animated_plot(
obstacles=obstacles,
robot_paths=robot_paths,
problem=problem,
smooth=not args.viz_discrete,
)
if not out:
viz.show(fig)
elif out.lower().endswith(".gif"):
viz.write_gif(fig, out)
elif out.lower().endswith((".html", ".htm")):
viz.write_html(fig, out)
else:
viz.write_image(fig, out)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
parser = build_parser()
args = parser.parse_args()
# -- Config --------------------------------------------------------------
config = config_parser.load_config(
args.config, sections=["penalty_sets", "verbose"]
)
# Verbose: CLI > config.yaml
verbose_level = (
args.verbose if args.verbose is not None else config["verbose"]["level"]
)
set_verbose_level(verbose_level)
logger = get_logger()
logger.minimal(f"qubo_cli starting | map={args.map} | problem={args.problem}")
# -- Materials -----------------------------------------------------------
materials_data = config_parser.load_config(args.materials)["materials"]
# -- Problem -------------------------------------------------------------
problem = PathfindingProblem.from_map_config(
args.map,
problem_name=args.problem,
materials_data=materials_data,
coordinate_format=args.coordinate_format,
)
# -- Penalties -----------------------------------------------------------
if args.penalty_set not in config["penalty_sets"]:
logger.minimal(
f"[ERROR] Penalty set '{args.penalty_set}' not found in {args.config}. "
f"Available: {list(config['penalty_sets'].keys())}"
)
sys.exit(1)
penalties = build_penalties(config, args)
logger.minimal(f"Using penalty set: {args.penalty_set} | effective: {penalties}")
# -- Window limits -------------------------------------------------------
window_limits = parse_window_limits(args.window_limit, problem.robots.keys())
# -- Builder -------------------------------------------------------------
builder_kwargs = {
"penalties": penalties,
"name": args.problem,
"robot_window_limits": window_limits if window_limits else None,
"log_reductions": not args.no_reduction_log,
}
if args.var_limit is not None:
builder_kwargs["var_limit"] = args.var_limit
if args.solver == "ilp":
# ILP has no penalty weights, var_limit, or windowing — builder_kwargs
# (penalties/var_limit/robot_window_limits/log_reductions) don't apply.
if args.builder == "grid":
p = problem.as_grid_only()
builder = GridILPBuilder(p, name=args.problem, verbose_level=verbose_level)
else: # graph
p = problem.as_graph_only()
builder = GraphILPBuilder(p, name=args.problem, verbose_level=verbose_level)
elif args.solver == "cbs":
# CBS has no penalty weights, var_limit, or windowing either — same
# reasoning as ILP above.
if args.builder == "grid":
p = problem.as_grid_only()
builder = GridCBSBuilder(p, name=args.problem, verbose_level=verbose_level)
else: # graph
p = problem.as_graph_only()
builder = GraphCBSBuilder(p, name=args.problem, verbose_level=verbose_level)
elif args.builder == "grid":
p = problem.as_grid_only()
builder_kwargs["distance_scaling"] = args.distance_scaling
builder = QUBOBuilder(p, **builder_kwargs)
else: # graph
p = problem.as_graph_only()
builder = GraphQUBO(p, **builder_kwargs)
logger.minimal(
f"Builder: {args.builder.upper()} | window_limits={window_limits or 'none'}"
)
# -- Solver --------------------------------------------------------------
solver = build_solver(args, verbose_level)
# -- Run mode ------------------------------------------------------------
if args.benchmark:
if args.visualize:
logger.minimal(
"[viz] --visualize is only available in single-solve mode; ignoring."
)
logger.minimal(
f"Running benchmark: {args.num_runs} runs, level {args.benchmark_level}"
)
runner = bm_module.BenchmarkRunner(
builder,
solver,
num_runs=args.num_runs,
level=args.benchmark_level,
preprocess=not args.no_preprocess,
)
runner.run_build()
else:
# Single solve
timer = time.time()
# ILP builders rebuild themselves inside solver.solve() (see
# ILPSolver.solve()) so the preprocess flag always takes effect;
# pre-building here would just duplicate work and logging.
if not hasattr(builder, "local_index"):
builder.build()
solution = solver.solve(builder, preprocess=not args.no_preprocess)
# Use p (the grid-only/graph-only problem actually passed to the
# builder), not problem
path = solver.decode_path(solution["solution"], p)
energy = solution["energy"]
if isinstance(energy, list):
energy = sum(energy)
logger.debug(
f"Raw path: {path}"
) # Full decoded tuples — only useful at verbose=3
logger.minimal(f"Energy: {energy:.4f}")
logger.minimal(f"Time: {time.time() - timer:.4f}")
for robot_id, robot in problem.robots.items():
robot_path = robot.path
if args.clip_at_goal:
robot_path = clip_path_at_goal(robot_path, tuple(robot.goal))
formatted_path = [
(*robot.format_position((i, j)), t) for i, j, t in robot_path
]
logger.minimal(f" [{robot_id}] {formatted_path}")
if args.visualize:
# visualizer.py expects native matrix (row, col) input regardless of
# --coordinate-format, so pass the raw decoded path, not a formatted one.
run_visualization(args, problem, solver.get_robot_paths(path))
if __name__ == "__main__":
main()