Classical Solvers
Classical solvers tackle a QUBO Instance entirely on CPU, without going through the quantum pipeline. Each one is a standalone function under solving, callable directly with the instance and its own set of keyword arguments — no Solver/SolverConfig wiring required. They differ in the trade-off between solution quality, runtime, and whether they need a starting point.
Without an initial solution
These solvers start from scratch:
solving.cplex.solve— exact MIP solve via IBM CPLEX.solving.random_sampling.solve— uniform random sampling baseline.
solving.cplex requires the extras optional dependency group
With an initial solution
These solvers take a starting point. Because they accept a starting point, they can also be used to refine a previous solution — for example, post-processing a quantum solver's output:
solving.tabu_search.solve— neighborhood search with a tabu memory. See example below.solving.simulated_annealing.solve— stochastic temperature-cooling search.solving.iterative_bitflip_local_search.solve— greedy local search that iteratively flips bits until no single flip improves the solution.
Example: Tabu Search
from qubosolver import Instance, solving, matrix, bitstrings, torch_rng, analysis
instance = Instance(
matrix.tensor(
[
[-2.0, 1.0, 0.0, 1.5, 0.0],
[1.0, -1.5, 1.0, 0.0, 0.5],
[0.0, 1.0, -2.0, 1.0, 1.0],
[1.5, 0.0, 1.0, -1.0, 0.5],
[0.0, 0.5, 1.0, 0.5, -1.5],
]
)
)
starts = bitstrings.rand(5, instance.size, rng=torch_rng(15))
solution = solving.tabu_search.solve(instance, starts=starts, time_limit=10.0)
print("Tabu Search solution:")
print(analysis.to_dataframe([solution]))
Example: refining a quantum solution
Run the quantum pipeline (see quantum solving), then try local bitflips for refinement:
from qubosolver import (
Instance,
Solution,
LocalEmulator,
embedding,
drive_shaping,
solving,
matrix,
analysis,
)
import qoolqit
instance = Instance(
matrix.tensor(
[
[-2.0, 1.0, 0.0, 1.5, 0.0],
[1.0, -1.5, 1.0, 0.0, 0.5],
[0.0, 1.0, -2.0, 1.0, 1.0],
[1.5, 0.0, 1.0, -1.0, 0.5],
[0.0, 0.5, 1.0, 0.5, -1.5],
]
)
)
device = qoolqit.AnalogDeviceWithDMM()
backend = LocalEmulator()
register = embedding.blade.embed_for_device(instance, device)
drive = drive_shaping.proportional_diagonal.build_drive(instance, register, device=device, dmm=True)
program = solving.analog_quantum_sampling.compile(register, drive, device)
job = backend.run(program)
quantum_solution = Solution.from_results(job.results(), instance)
print("Quantum solution:")
print(analysis.to_dataframe([quantum_solution]))
# Refine the quantum solution classically.
refined_solution = solving.iterative_bitflip_local_search.solve(instance, starts=quantum_solution)
print("Refined solution:")
print(analysis.to_dataframe([refined_solution]))
Quantum solution:
labels bitstrings costs counts probs
0 0 10100 -4.0 374 0.374
1 0 10001 -3.5 342 0.342
2 0 10101 -3.5 172 0.172
3 0 11001 -2.0 69 0.069
4 0 01001 -2.0 36 0.036
5 0 10000 -2.0 4 0.004
6 0 01011 -2.0 3 0.003
Refined solution:
labels bitstrings costs counts probs
0 0 10100 -4.0 550 0.550
1 0 10001 -3.5 411 0.411
2 0 01010 -2.5 3 0.003
3 0 01001 -2.0 36 0.036
For full parameter details, see the classical solvers API reference.
The Solver shortcut
For the common case, SolverConfig and Solver wrap solver selection through ClassicalSolvingConfig, and (optionally) the initial-solution sampling into a single call:
from qubosolver import (
Instance,
Solver,
SolverConfig,
ClassicalSolvingConfig,
matrix,
analysis,
)
from dataclasses import asdict
import pprint
instance = Instance(
matrix.tensor(
[
[-2.0, 1.0, 0.0, 1.5, 0.0],
[1.0, -1.5, 1.0, 0.0, 0.5],
[0.0, 1.0, -2.0, 1.0, 1.0],
[1.5, 0.0, 1.0, -1.0, 0.5],
[0.0, 0.5, 1.0, 0.5, -1.5],
]
)
)
classical_config = ClassicalSolvingConfig(
algorithm="tabu_search",
# algorithm="simulated_annealing",
# algorithm="cplex",
)
solver_config = SolverConfig(solving=classical_config)
solver = Solver(instance, solver_config)
solution = solver.solve()
print(pprint.pformat(asdict(solver_config.solving)))
print()
print(analysis.to_dataframe([solution]))