Skip to content

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

pip install 'qubo-solver[extras]'
uv sync --extra extras

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.
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]))
Tabu Search solution:
  labels bitstrings  costs  counts  probs
0      0      10100   -4.0       5    1.0

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]))
{'algorithm': 'tabu_search',
 'max_bitstrings': 1,
 'max_iter': 100,
 'time_limit': inf}

  labels bitstrings  costs  counts  probs
0      0      10100   -4.0       1    1.0