Solvers
DecomposeQuboSolver(instance, config, solver_factory)
Section titled “
DecomposeQuboSolver(instance, config, solver_factory)
”
Bases: BaseSolver
Solver that leverages qubo decomposition techniques to solve subproblems and merge subsolutions to propose solutions of the original instance. Note, the QUBO instance is seen as a graph where variables are vertices, and coefficients represents weighted edges.
Scope: the decomposition only accepts qubo with positive coefficients off-diagonal coefficients.
Algorithm
Note, only one bitstring solution is returned.
Initialize the DecomposeQuboSolver with the given problem and configuration.
| PARAMETER | DESCRIPTION |
|---|---|
instance
|
The QUBO problem to solve.
TYPE:
|
config
|
Solver settings including backend and device.
TYPE:
|
solver_factory
|
solver class for subproblems.
TYPE:
|
Source code in qubosolver/solver.py
def __init__( self, instance: QUBOInstance, config: SolverConfig | None, solver_factory: Callable[[QUBOInstance, SolverConfig], BaseSolver],): """ Initialize the DecomposeQuboSolver with the given problem and configuration.
Args: instance (QUBOInstance): The QUBO problem to solve. config (SolverConfig): Solver settings including backend and device. solver_factory (Callable[[QUBOInstance, SolverConfig], BaseSolver]): solver class for subproblems. """ if ( instance.coefficients[~torch.eye(*instance.coefficients.shape, dtype=torch.bool)] < 0 ).any(): raise ValueError("Decomposition does not handle off-diagonal negative coefficients")
# default is a quantum solver as we apply device-dependent decomposition super().__init__( QUBOInstance(instance.coefficients), config or SolverConfig(use_quantum=True), ) self._solver_factory = solver_factory
self.backend = self.config.backend self.device = self.config.device._device
self.decomposition_config: DecompositionConfig = ( self.config.decompose or DecompositionConfig() )
# A cached version of `config` that we're going # to use for problems we do not wish to decompose. self._config_subproblems = SolverConfig.from_kwargs( **self.config.model_dump(exclude={"decompose"}) )
solve()
Section titled “
solve()
”Execute a solver by decomposing the instance. Note, the QUBO instance is seen as a graph where variables are vertices, and coefficients represents weighted edges.
Algorithm
| RETURNS | DESCRIPTION |
|---|---|
QUBOSolution
|
Final result after execution and postprocessing. Note, only one bitstring solution is returned.
TYPE:
|
Source code in qubosolver/solver.py
def solve(self) -> QUBOSolution: """ Execute a solver by decomposing the instance. Note, the QUBO instance is seen as a graph where variables are vertices, and coefficients represents weighted edges.
Algorithm: 1. Initialize global solution and vertices to place. 2. While we can apply decomposition: - Select a random vertex to place using device layout. - Apply a geometric search to obtain a set of vertices that can be placed on a device to form a subproblem. - Solve the subproblem with a solver. - Update the global solution and the vertices left to place. 3. For remaining variables, we solve classically.
Returns: QUBOSolution: Final result after execution and postprocessing. Note, only one bitstring solution is returned. """
self.number_iterations = 0 assert self.instance.size if self.instance.size <= self.decomposition_config.decompose_stop_number: solver = QuboSolverClassical( self.instance, SolverConfig(use_quantum=False, decompose=None), ) return solver.solve()
else: from qubosolver.algorithms.decompose import ( compute_distance_interaction_matrix, geometric_search, interaction_matrix_from_placed, last_target_matrix, transfer_edge_values, update_global_solution, vertices_to_place, )
global_solution = torch.full((self.instance.size,), -1) qubo_mat = self.instance.coefficients.clone() dist_matrix = compute_distance_interaction_matrix( self.device, qubo_mat, neglecting_inter_distance=self.decomposition_config.neglecting_inter_distance, neglecting_max_coefficient=self.decomposition_config.neglecting_max_coefficient, )
# The following dictionary contain vertices to apply the decomposition search # where each vertex key maps to other blocking, separated and neighbors vertices # and gets updated as we iterate in the decomposition dict_vertices_to_place = vertices_to_place( dist_matrix, qubo_mat, separation_threshold=self.decomposition_config.neglecting_inter_distance, )
transfer_edge_values(dict_vertices_to_place, dict(), global_solution, qubo_mat) while len(dict_vertices_to_place) > self.decomposition_config.decompose_stop_number: # find a first vertex to start the geometric search # random works better according to some performed numerics first_vertex_search = random.choice(list(dict_vertices_to_place.keys())) placed_vertices = geometric_search( qubo_mat, dict_vertices_to_place, first_vertex_search, self.decomposition_config.decompose_threshold, self.device, ) if len(placed_vertices) <= self.decomposition_config.decompose_break_placement: break self.number_iterations += 1
matrix_to_solve, map_index_vertices = interaction_matrix_from_placed( placed_vertices, self.device ) qubo = QUBOInstance(matrix_to_solve) subsolver = self._solver_factory(qubo, self._config_subproblems)
# only one bitstring is kept as per design choice of the # decomposition algorithm sub_solution = subsolver.solve().bitstrings[0] update_global_solution( global_solution=global_solution, sub_solution=sub_solution, mapping=map_index_vertices, )
transfer_edge_values( dict_vertices_to_place, placed_vertices, global_solution, qubo_mat, )
# classical resolution of last matrix matrix_to_solve, mapping_target_vertices = last_target_matrix( list(dict_vertices_to_place.keys()), qubo_mat ) qubo = QUBOInstance(matrix_to_solve) lastsolver = QuboSolverClassical( qubo, SolverConfig(use_quantum=False, decompose=None), ) sub_solution = lastsolver.solve().bitstrings[0] update_global_solution( global_solution=global_solution, sub_solution=sub_solution, mapping=mapping_target_vertices, ) # Probabilities and counts are ignored as we return one solution qubosol = QUBOSolution( bitstrings=global_solution.unsqueeze(0).to(dtype=torch.float32), counts=torch.tensor([1], dtype=torch.int), costs=torch.Tensor(), probabilities=None, ) qubosol.costs = qubosol.compute_costs(self.instance) qubosol.bitstrings = qubosol.bitstrings.int() return qubosol
QuboSolver(instance, config=None)
Section titled “
QuboSolver(instance, config=None)
”
Bases: BaseSolver
Dispatcher that selects the appropriate solver (quantum or classical) based on the SolverConfig and delegates execution to it.
Source code in qubosolver/solver.py
def __init__(self, instance: QUBOInstance, config: SolverConfig | None = None): super().__init__(instance, config) self._solver: BaseSolver
if config is None: self._solver = QuboSolverClassical(instance, self.config) else: if config.decompose: if self.config.use_quantum: solver_factory: type[BaseSolver] = QuboSolverQuantum else: solver_factory = QuboSolverClassical self._solver = DecomposeQuboSolver(instance, self.config, solver_factory)
elif config.use_quantum: self._solver = QuboSolverQuantum(instance, config) else: self._solver = QuboSolverClassical(instance, config)
QuboSolverClassical(instance, config=None)
Section titled “
QuboSolverClassical(instance, config=None)
”
Bases: BaseSolver
Classical solver for QUBO problems. This implementation delegates the classical solving task to the external classical solver module (e.g., CPLEX, D-Wave SA, or D-Wave Tabu), as selected via the SolverConfig.
After obtaining the raw solution, postprocessing (e.g., bit-flip local search) is applied.
Source code in qubosolver/solver.py
def __init__(self, instance: QUBOInstance, config: SolverConfig | None = None): super().__init__(instance, config) # Optionally, you could instantiate Fixtures here for postprocessing: self.fixtures = Fixtures(self.instance, self.config)
QuboSolverQuantum(instance, config=None)
Section titled “
QuboSolverQuantum(instance, config=None)
”
Bases: BaseSolver
Quantum solver that orchestrates the solving of a QUBO problem using embedding, drive shaping, and quantum execution pipelines.
Note: Negative off-diagonal coefficients are not supported.
Initialize the QuboSolver with the given problem and configuration.
| PARAMETER | DESCRIPTION |
|---|---|
instance
|
The QUBO problem to solve.
TYPE:
|
config
|
Solver settings including backend and device.
TYPE:
|
Source code in qubosolver/solver.py
def __init__(self, instance: QUBOInstance, config: SolverConfig | None = None): """ Initialize the QuboSolver with the given problem and configuration.
Args: instance (QUBOInstance): The QUBO problem to solve. config (SolverConfig): Solver settings including backend and device. """ super().__init__(instance, config or SolverConfig(use_quantum=True))
if ( instance.coefficients[~torch.eye(*instance.coefficients.shape, dtype=torch.bool)] < 0 ).any(): raise ValueError("Quantum solver does not handle off-diagonal negative coefficients")
self._check_size_limit()
self.backend = self.config.backend self.embedder = get_embedder(self.instance, self.config, self.backend) self.drive_shaper = get_drive_shaper(self.instance, self.config, self.backend)
self._register: Register | None = None self._drive: Drive | None = None
drive(embedding)
Section titled “
drive(embedding)
”Generate the drive sequence based on the given embedding.
| PARAMETER | DESCRIPTION |
|---|---|
embedding
|
The embedded register layout.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
tuple
|
A tuple of - Drive: Drive schedule for quantum execution. - QUBOSolution: Initial solution of generated from drive shaper
TYPE:
|
Source code in qubosolver/solver.py
def drive(self, embedding: Register) -> tuple: """ Generate the drive sequence based on the given embedding.
Args: embedding (Register): The embedded register layout.
Returns: tuple: A tuple of - Drive: Drive schedule for quantum execution. - QUBOSolution: Initial solution of generated from drive shaper
""" drive, qubo_solution = self.drive_shaper.generate(embedding)
self._drive = drive return drive, qubo_solution
embedding()
Section titled “
embedding()
”Generate a physical embedding (register) for the QUBO variables.
| RETURNS | DESCRIPTION |
|---|---|
Register
|
Atom layout suitable for quantum hardware.
TYPE:
|
Source code in qubosolver/solver.py
def embedding(self) -> Register: """ Generate a physical embedding (register) for the QUBO variables.
Returns: Register: Atom layout suitable for quantum hardware. """ self.embedder.instance = self.instance self._register = self.embedder.embed() return self._register
solve()
Section titled “
solve()
”Execute the full quantum pipeline: preprocess, embed, drive, execute, postprocess.
| RETURNS | DESCRIPTION |
|---|---|
QUBOSolution
|
Final result after execution and postprocessing.
TYPE:
|
Source code in qubosolver/solver.py
def solve(self) -> QUBOSolution: """ Execute the full quantum pipeline: preprocess, embed, drive, execute, postprocess.
Returns: QUBOSolution: Final result after execution and postprocessing. """ # 1) try trivial else delegate to quantum solver if self.config.activate_trivial_solutions: trivial = self._trivial_solution() if trivial is not None: return trivial self._check_size_limit()
# 2) Apply preprocessing if requested self.preprocess()
embedding = self.embedding()
drive, qubo_solution = self.drive(embedding)
bitstrings, counts, _, _ = ( qubo_solution.bitstrings, qubo_solution.counts, qubo_solution.probabilities, qubo_solution.costs, ) if ( len(bitstrings) == 0 and qubo_solution.counts is None ) or self.config.drive_shaping.optimized_re_execute_opt_drive: bitstrings, counts = self.execute(drive, embedding)
bitstring_strs = bitstrings bitstrings_tensor = torch.tensor( [list(map(int, bs)) for bs in bitstring_strs], dtype=torch.float32 ) if counts is None: counts_tensor = torch.empty((0,), dtype=torch.int32) elif isinstance(counts, dict) or isinstance(counts, Counter): count_values = [counts.get(bs, 0) for bs in bitstring_strs] counts_tensor = torch.tensor(count_values, dtype=torch.int32) else: counts_tensor = counts
solution = QUBOSolution( bitstrings=bitstrings_tensor, counts=counts_tensor, costs=torch.Tensor(), probabilities=None, )
# Post-process fixations of the preprocessing and restore the original QUBO solution = self.post_process_fixation(solution) solution = self.post_process(solution)
solution.costs = solution.compute_costs(self.instance) solution.probabilities = solution.compute_probabilities() solution.sort_by_cost()
solution.bitstrings = solution.bitstrings.int() return solution