qoro-divi 0.2.2b1__py3-none-any.whl → 0.3.0b1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of qoro-divi might be problematic. Click here for more details.

@@ -3,13 +3,12 @@
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
5
  import heapq
6
- import re
7
6
  import string
8
- from collections.abc import Callable, Sequence
7
+ from collections.abc import Callable, Sequence, Set
9
8
  from concurrent.futures import ProcessPoolExecutor
10
9
  from dataclasses import dataclass
11
10
  from functools import partial
12
- from typing import Literal, Optional
11
+ from typing import Literal
13
12
  from warnings import warn
14
13
 
15
14
  import matplotlib.cm as cm
@@ -26,8 +25,10 @@ from divi.qprog import QAOA, ProgramBatch
26
25
  from divi.qprog._qaoa import (
27
26
  _SUPPORTED_INITIAL_STATES_LITERAL,
28
27
  GraphProblem,
28
+ GraphProblemTypes,
29
29
  draw_graph_solution_nodes,
30
30
  )
31
+ from divi.qprog.quantum_program import QuantumProgram
31
32
 
32
33
  from .optimizers import Optimizer
33
34
 
@@ -42,8 +43,8 @@ _MAXIMUM_AVAILABLE_QUBITS = 30
42
43
 
43
44
  @dataclass(frozen=True, eq=True)
44
45
  class PartitioningConfig:
45
- max_n_nodes_per_cluster: Optional[int] = None
46
- minimum_n_clusters: Optional[int] = None
46
+ max_n_nodes_per_cluster: int | None = None
47
+ minimum_n_clusters: int | None = None
47
48
  partitioning_algorithm: Literal["spectral", "metis", "kernighan_lin"] = "spectral"
48
49
 
49
50
  def __post_init__(self):
@@ -334,19 +335,43 @@ def _node_partition_graph(
334
335
  return tuple(graph for (_, _, graph) in subgraphs)
335
336
 
336
337
 
337
- def linear_aggregation(curr_solution, solution_bitstring, graph, reverse_index_maps):
338
- for node in graph.nodes():
339
- solution_index = reverse_index_maps[node]
340
- curr_solution[solution_index] = int(solution_bitstring[node])
338
+ def linear_aggregation(
339
+ curr_solution: Sequence[Literal[0] | Literal[1]],
340
+ subproblem_solution: Set[int],
341
+ subproblem_reverse_index_map: dict[int, int],
342
+ ):
343
+ """Linearly combines a subproblem's solution into the main solution vector.
344
+
345
+ This function iterates through each node of subproblem's solution. For each node,
346
+ it uses the reverse index map to find its original index in the main graph,
347
+ setting it to 1 in the current global solution, potentially overwriting any
348
+ previous states.
349
+
350
+ Args:
351
+ curr_solution (Sequence[Literal[0] | Literal[1]]): The main solution
352
+ vector being aggregated, represented as a sequence of 0s and 1s.
353
+ subproblem_solution (Set[int]): A set containing the original indices of
354
+ the nodes that form the solution for the subproblem.
355
+ subproblem_reverse_index_map (dict[int, int]): A mapping from the
356
+ subgraph's internal node labels back to their original indices in
357
+ the main solution vector.
358
+
359
+ Returns:
360
+ The updated main solution vector.
361
+ """
362
+ for node in subproblem_solution:
363
+ curr_solution[subproblem_reverse_index_map[node]] = 1
341
364
 
342
365
  return curr_solution
343
366
 
344
367
 
345
- def domninance_aggregation(
346
- curr_solution, solution_bitstring, graph, reverse_index_maps
368
+ def dominance_aggregation(
369
+ curr_solution: Sequence[Literal[0] | Literal[1]],
370
+ subproblem_solution: Set[int],
371
+ subproblem_reverse_index_map: dict[int, int],
347
372
  ):
348
- for node in graph.nodes():
349
- solution_index = reverse_index_maps[node]
373
+ for node in subproblem_solution:
374
+ original_index = subproblem_reverse_index_map[node]
350
375
 
351
376
  # Use existing assignment if dominant in previous solutions
352
377
  # (e.g., more 0s than 1s or vice versa)
@@ -354,20 +379,29 @@ def domninance_aggregation(
354
379
  count_1 = curr_solution.count(1)
355
380
 
356
381
  if (
357
- (count_0 > count_1 and curr_solution[node] == 0)
358
- or (count_1 > count_0 and curr_solution[node] == 1)
382
+ (count_0 > count_1 and curr_solution[original_index] == 0)
383
+ or (count_1 > count_0 and curr_solution[original_index] == 1)
359
384
  or (count_0 == count_1)
360
385
  ):
361
386
  # Assign based on QAOA if tie
362
- curr_solution[solution_index] = int(solution_bitstring[node])
387
+ curr_solution[original_index] = 1
363
388
 
364
389
  return curr_solution
365
390
 
366
391
 
392
+ def _run_and_compute_solution(program: QuantumProgram):
393
+
394
+ program.run()
395
+
396
+ final_sol_circuit_count, final_sol_run_time = program.compute_final_solution()
397
+
398
+ return final_sol_circuit_count, final_sol_run_time
399
+
400
+
367
401
  class GraphPartitioningQAOA(ProgramBatch):
368
402
  def __init__(
369
403
  self,
370
- graph: nx.Graph | rx.PyGraph,
404
+ graph: GraphProblemTypes,
371
405
  graph_problem: GraphProblem,
372
406
  n_layers: int,
373
407
  backend: CircuitRunner,
@@ -409,9 +443,10 @@ class GraphPartitioningQAOA(ProgramBatch):
409
443
  self.partitioning_config = partitioning_config
410
444
  self.max_iterations = max_iterations
411
445
 
446
+ self.solution = None
412
447
  self.aggregate_fn = aggregate_fn
413
448
 
414
- self._solution_nodes = None
449
+ self._task_fn = _run_and_compute_solution
415
450
 
416
451
  self._constructor = partial(
417
452
  QAOA,
@@ -425,11 +460,23 @@ class GraphPartitioningQAOA(ProgramBatch):
425
460
  )
426
461
 
427
462
  def create_programs(self):
428
- if len(self.programs) > 0:
429
- raise RuntimeError(
430
- "Some programs already exist. "
431
- "Clear the program dictionary before creating new ones by using batch.reset()."
432
- )
463
+ """
464
+ Creates and initializes QAOA programs for each partitioned subgraph.
465
+
466
+ The main graph is partitioned into node-based subgraphs
467
+ according to the specified partitioning configuration. Each subgraph is relabeled with
468
+ integer node labels for QAOA compatibility, and a reverse index map is stored for later
469
+ result aggregation.
470
+
471
+ Each program is assigned a unique program ID, which is a tuple of:
472
+ - An uppercase letter (A, B, C, ...) corresponding to the partition index.
473
+ - The number of nodes in the subgraph.
474
+
475
+ Example program ID: ('A', 5) for the first partition with 5 nodes.
476
+
477
+ The created QAOA programs are stored in the `self.programs` dictionary, keyed by their program IDs.
478
+
479
+ """
433
480
 
434
481
  super().create_programs()
435
482
 
@@ -449,24 +496,25 @@ class GraphPartitioningQAOA(ProgramBatch):
449
496
  self.reverse_index_maps = {}
450
497
 
451
498
  for i, subgraph in enumerate(subgraphs):
452
- index_map = {node: idx for idx, node in enumerate(subgraph.nodes())}
453
- self.reverse_index_maps[i] = {v: k for k, v in index_map.items()}
454
- _subgraph = nx.relabel_nodes(subgraph, index_map)
455
-
456
499
  prog_id = (string.ascii_uppercase[i], subgraph.number_of_nodes())
457
500
 
501
+ index_map = {node: idx for idx, node in enumerate(subgraph.nodes())}
502
+ self.reverse_index_maps[prog_id] = {v: k for k, v in index_map.items()}
503
+
504
+ _subgraph = nx.relabel_nodes(subgraph, index_map)
458
505
  self.programs[prog_id] = self._constructor(
459
506
  job_id=prog_id,
460
507
  problem=_subgraph,
461
508
  losses=self._manager.list(),
462
509
  probs=self._manager.dict(),
463
510
  final_params=self._manager.list(),
511
+ solution_nodes=self._manager.list(),
464
512
  progress_queue=self._queue,
465
513
  )
466
514
 
467
515
  def compute_final_solutions(self):
468
516
  if self._executor is not None:
469
- self.wait_for_all()
517
+ self.join()
470
518
 
471
519
  if self._executor is not None:
472
520
  raise RuntimeError("A batch is already being run.")
@@ -482,51 +530,34 @@ class GraphPartitioningQAOA(ProgramBatch):
482
530
  ]
483
531
 
484
532
  def aggregate_results(self):
485
- if len(self.programs) == 0:
486
- raise RuntimeError("No programs to aggregate. Run create_programs() first.")
533
+ """
534
+ Aggregates the results from all QAOA subprograms to form a global solution.
487
535
 
488
- if self._executor is not None:
489
- self.wait_for_all()
536
+ This method collects the final bitstring solutions from each partitioned subgraph's QAOA program,
537
+ using the aggregation function specified at initialization (e.g., linear or dominance aggregation).
538
+ It reconstructs the global solution by mapping each subgraph's solution back to the original node indices
539
+ using the stored reverse index maps.
490
540
 
491
- if any(len(program.losses) == 0 for program in self.programs.values()):
492
- raise RuntimeError(
493
- "Some/All programs have empty losses. Did you call run()?"
494
- )
541
+ The final solution is stored in `self.solution` as a list of node indices assigned to the selected partition.
542
+
543
+ Raises:
544
+ RuntimeError: If no programs exist, if programs have not been run, or if results are incomplete.
545
+ Returns:
546
+ list[int]: The list of node indices in the final aggregated solution.
547
+ """
548
+ super().aggregate_results()
495
549
 
496
550
  if any(len(program.probs) == 0 for program in self.programs.values()):
497
551
  raise RuntimeError(
498
- "Not all final probabilities computed yet. Please call `compute_final_solutions()` first."
552
+ "Not all final probabilities computed yet. Please call `run()` first."
499
553
  )
500
554
 
501
555
  # Extract the solutions from each program
502
- for program, reverse_index_maps in zip(
503
- self.programs.values(), self.reverse_index_maps.values()
504
- ):
505
- # Extract the final probabilities of the lowest energy
506
- last_iteration_losses = program.losses[-1]
507
- minimum_key = min(last_iteration_losses, key=last_iteration_losses.get)
508
-
509
- # Find the key matching the best_solution_idx with possible metadata in between
510
- pattern = re.compile(rf"^{minimum_key}(?:_[^_]*)*_0$")
511
- matching_keys = [k for k in program.probs.keys() if pattern.match(k)]
512
-
513
- if len(matching_keys) > 1:
514
- raise RuntimeError(f"More than one matching key found.")
515
-
516
- best_solution_key = matching_keys[0]
517
-
518
- minimum_probabilities = program.probs[best_solution_key]
519
-
520
- # The bitstring corresponding to the solution, with flip for correct endianness
521
- max_prob_key = max(minimum_probabilities, key=minimum_probabilities.get)[
522
- ::-1
523
- ]
524
-
556
+ for prog_id, program in self.programs.items():
525
557
  self._bitstring_solution = self.aggregate_fn(
526
558
  self._bitstring_solution,
527
- max_prob_key,
528
- program.problem,
529
- reverse_index_maps,
559
+ program.solution,
560
+ self.reverse_index_maps[prog_id],
530
561
  )
531
562
 
532
563
  self.solution = list(np.where(self._bitstring_solution)[0])
@@ -559,9 +590,9 @@ class GraphPartitioningQAOA(ProgramBatch):
559
590
 
560
591
  # Convert partitions to node-to-partition mapping
561
592
  node_to_partition = {}
562
- for partition_id, mapping in self.reverse_index_maps.items():
593
+ for (partition_id, _), mapping in self.reverse_index_maps.items():
563
594
  for node in mapping.values():
564
- node_to_partition[node] = string.ascii_uppercase[partition_id]
595
+ node_to_partition[node] = partition_id
565
596
 
566
597
  # Get unique partition IDs and create color map
567
598
  unique_partitions = sorted(list(set(node_to_partition.values())))
@@ -613,7 +644,13 @@ class GraphPartitioningQAOA(ProgramBatch):
613
644
  plt.show()
614
645
 
615
646
  def draw_solution(self):
616
- if self._solution_nodes is None:
647
+ """
648
+ Visualizes the main graph with nodes highlighted according to the final aggregated solution.
649
+
650
+ If the solution has not yet been computed, this method calls `aggregate_results()` to obtain it.
651
+ """
652
+
653
+ if self.solution is None:
617
654
  self.aggregate_results()
618
655
 
619
656
  draw_graph_solution_nodes(self.main_graph, self.solution)
divi/qprog/_qaoa.py CHANGED
@@ -2,10 +2,11 @@
2
2
  #
3
3
  # SPDX-License-Identifier: Apache-2.0
4
4
 
5
+ import logging
5
6
  import re
6
7
  from enum import Enum
7
8
  from functools import reduce
8
- from typing import Literal, Optional, get_args
9
+ from typing import Literal, get_args
9
10
  from warnings import warn
10
11
 
11
12
  import matplotlib.pyplot as plt
@@ -25,6 +26,8 @@ from divi.qprog import QuantumProgram
25
26
  from divi.qprog.optimizers import Optimizer
26
27
  from divi.utils import convert_qubo_matrix_to_pennylane_ising
27
28
 
29
+ logger = logging.getLogger(__name__)
30
+
28
31
  GraphProblemTypes = nx.Graph | rx.PyGraph
29
32
  QUBOProblemTypes = list | np.ndarray | sps.spmatrix | QuadraticProgram
30
33
 
@@ -104,7 +107,7 @@ def _convert_quadratic_program_to_pennylane_ising(qp: QuadraticProgram):
104
107
 
105
108
  def _resolve_circuit_layers(
106
109
  initial_state, problem, graph_problem, **kwargs
107
- ) -> tuple[qml.operation.Operator, qml.operation.Operator, Optional[dict], str]:
110
+ ) -> tuple[qml.operation.Operator, qml.operation.Operator, dict | None, str]:
108
111
  """
109
112
  Generates the cost and mixer hamiltonians for a given problem, in addition to
110
113
  optional metadata returned by Pennylane if applicable
@@ -150,7 +153,7 @@ class QAOA(QuantumProgram):
150
153
  def __init__(
151
154
  self,
152
155
  problem: GraphProblemTypes | QUBOProblemTypes,
153
- graph_problem: Optional[GraphProblem] = None,
156
+ graph_problem: GraphProblem | None = None,
154
157
  n_layers: int = 1,
155
158
  initial_state: _SUPPORTED_INITIAL_STATES_LITERAL = "Recommended",
156
159
  optimizer: Optimizer = Optimizer.MONTE_CARLO,
@@ -221,12 +224,13 @@ class QAOA(QuantumProgram):
221
224
  self.optimizer = optimizer
222
225
  self.max_iterations = max_iterations
223
226
  self.current_iteration = 0
224
- self._solution_nodes = None
225
227
  self.n_params = 2
226
228
  self._is_compute_probabilites = False
227
229
 
228
230
  # Shared Variables
229
231
  self.probs = kwargs.pop("probs", {})
232
+ self._solution_nodes = kwargs.pop("solution_nodes", [])
233
+ self._solution_bitstring = kwargs.pop("solution_bitstring", [])
230
234
 
231
235
  (
232
236
  self.cost_hamiltonian,
@@ -376,14 +380,24 @@ class QAOA(QuantumProgram):
376
380
  - For QUBO problems, stores the solution as a NumPy array of bits.
377
381
  - For graph problems, stores the solution as a list of node indices corresponding to '1's in the bitstring.
378
382
  5. Returns the total circuit count and total runtime for the optimization process.
383
+
379
384
  Returns:
380
385
  tuple: A tuple containing:
381
386
  - int: The total number of circuits executed.
382
387
  - float: The total runtime of the optimization process.
383
- Raises:
384
- RuntimeError: If more than one/no matching key is found for the best solution index.
385
388
  """
386
389
 
390
+ if self._progress_queue:
391
+ self._progress_queue.put(
392
+ {
393
+ "job_id": self.job_id,
394
+ "message": "🏁 Computing Final Solution 🏁",
395
+ "progress": 0,
396
+ }
397
+ )
398
+ else:
399
+ logger.info("🏁 Computing Final Solution 🏁")
400
+
387
401
  # Convert losses dict to list to apply ordinal operations
388
402
  final_losses_list = list(self.losses[-1].values())
389
403
 
@@ -417,15 +431,26 @@ class QAOA(QuantumProgram):
417
431
  ]
418
432
 
419
433
  if isinstance(self.problem, QUBOProblemTypes):
420
- self._solution_bitstring = np.fromiter(
434
+ self._solution_bitstring[:] = np.fromiter(
421
435
  best_solution_bitstring, dtype=np.int32
422
436
  )
423
437
 
424
438
  if isinstance(self.problem, GraphProblemTypes):
425
- self._solution_nodes = [
439
+ self._solution_nodes[:] = [
426
440
  m.start() for m in re.finditer("1", best_solution_bitstring)
427
441
  ]
428
442
 
443
+ if self._progress_queue:
444
+ self._progress_queue.put(
445
+ {
446
+ "job_id": self.job_id,
447
+ "progress": 0,
448
+ "final_status": "Success",
449
+ }
450
+ )
451
+ else:
452
+ logger.info(f"Computed Solution!")
453
+
429
454
  return self._total_circuit_count, self._total_run_time
430
455
 
431
456
  def draw_solution(self):
@@ -0,0 +1,199 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import string
6
+ from functools import partial
7
+ from typing import TypeVar
8
+
9
+ import dimod
10
+ import hybrid
11
+ import numpy as np
12
+ import scipy.sparse as sps
13
+ from dimod import BinaryQuadraticModel
14
+
15
+ from divi.interfaces import CircuitRunner
16
+ from divi.qprog._qaoa import QAOA, QUBOProblemTypes
17
+ from divi.qprog.batch import ProgramBatch
18
+ from divi.qprog.optimizers import Optimizer
19
+ from divi.qprog.quantum_program import QuantumProgram
20
+
21
+
22
+ # Helper function to merge subsamples in-place
23
+ def _merge_substates(_, substates):
24
+ a, b = substates
25
+ return a.updated(subsamples=hybrid.hstack_samplesets(a.subsamples, b.subsamples))
26
+
27
+
28
+ T = TypeVar("T", bound=QUBOProblemTypes | BinaryQuadraticModel)
29
+
30
+
31
+ def _sanitize_problem_input(qubo: T) -> tuple[T, BinaryQuadraticModel]:
32
+ if isinstance(qubo, BinaryQuadraticModel):
33
+ return qubo, qubo
34
+
35
+ if isinstance(qubo, (np.ndarray, sps.spmatrix)):
36
+ x, y = qubo.shape
37
+ if x != y:
38
+ raise ValueError("Only matrices supported.")
39
+
40
+ if isinstance(qubo, np.ndarray):
41
+ return qubo, dimod.BinaryQuadraticModel(qubo, vartype=dimod.Vartype.BINARY)
42
+
43
+ if isinstance(qubo, sps.spmatrix):
44
+ return qubo, dimod.BinaryQuadraticModel(
45
+ {(row, col): data for row, col, data in zip(qubo.row, qubo.col, qubo.data)},
46
+ vartype=dimod.Vartype.BINARY,
47
+ )
48
+
49
+ raise ValueError(f"Got an unsupported QUBO input format: {type(qubo)}")
50
+
51
+
52
+ def _run_and_compute_solution(program: QuantumProgram):
53
+
54
+ program.run()
55
+
56
+ final_sol_circuit_count, final_sol_run_time = program.compute_final_solution()
57
+
58
+ return final_sol_circuit_count, final_sol_run_time
59
+
60
+
61
+ class QUBOPartitioningQAOA(ProgramBatch):
62
+ def __init__(
63
+ self,
64
+ qubo: QUBOProblemTypes | BinaryQuadraticModel,
65
+ decomposer: hybrid.traits.ProblemDecomposer,
66
+ n_layers: int,
67
+ backend: CircuitRunner,
68
+ composer: hybrid.traits.SubsamplesComposer = hybrid.SplatComposer(),
69
+ optimizer=Optimizer.MONTE_CARLO,
70
+ max_iterations=10,
71
+ **kwargs,
72
+ ):
73
+ """
74
+ Initialize a QUBOPartitioningQAOA instance for solving QUBO problems using partitioning and QAOA.
75
+
76
+ Args:
77
+ qubo (QUBOProblemTypes | BinaryQuadraticModel): The QUBO problem to solve, provided as a supported type or a BinaryQuadraticModel.
78
+ Note: Variable types are assumed to be binary (not Spin).
79
+ decomposer (hybrid.traits.ProblemDecomposer): The decomposer used to partition the QUBO problem into subproblems.
80
+ n_layers (int): Number of QAOA layers to use for each subproblem.
81
+ backend (CircuitRunner): Backend responsible for running quantum circuits.
82
+ composer (hybrid.traits.SubsamplesComposer, optional): Composer to aggregate subsamples from subproblems.
83
+ Defaults to hybrid.SplatComposer().
84
+ optimizer (Optimizer, optional): Optimizer to use for QAOA.
85
+ Defaults to Optimizer.MONTE_CARLO.
86
+ max_iterations (int, optional): Maximum number of optimization iterations.
87
+ Defaults to 10.
88
+ **kwargs: Additional keyword arguments passed to the QAOA constructor.
89
+
90
+ """
91
+ super().__init__(backend=backend)
92
+
93
+ self.main_qubo, self._bqm = _sanitize_problem_input(qubo)
94
+
95
+ self._partitioning = hybrid.Unwind(decomposer)
96
+ self._aggregating = hybrid.Reduce(hybrid.Lambda(_merge_substates)) | composer
97
+
98
+ self._task_fn = _run_and_compute_solution
99
+
100
+ self.max_iterations = max_iterations
101
+
102
+ self._constructor = partial(
103
+ QAOA,
104
+ optimizer=optimizer,
105
+ max_iterations=self.max_iterations,
106
+ backend=self.backend,
107
+ n_layers=n_layers,
108
+ **kwargs,
109
+ )
110
+ pass
111
+
112
+ def create_programs(self):
113
+ """
114
+ Partition the main QUBO problem and instantiate QAOA programs for each subproblem.
115
+
116
+ This implementation:
117
+ - Uses the configured decomposer to split the main QUBO into subproblems.
118
+ - For each subproblem, creates a QAOA program with the specified parameters.
119
+ - Stores each program in `self.programs` with a unique identifier.
120
+
121
+ Unique Identifier Format:
122
+ Each key in `self.programs` is a tuple of the form (letter, size), where:
123
+ - letter: An uppercase letter ('A', 'B', 'C', ...) indicating the partition index.
124
+ - size: The number of variables in the subproblem.
125
+
126
+ Example: ('A', 5) refers to the first partition with 5 variables.
127
+ """
128
+
129
+ super().create_programs()
130
+
131
+ self.prog_id_to_bqm_subproblem_states = {}
132
+
133
+ init_state = hybrid.State.from_problem(self._bqm)
134
+ _bqm_partitions = self._partitioning.run(init_state).result()
135
+
136
+ for i, partition in enumerate(_bqm_partitions):
137
+ if i > 0:
138
+ # We only need 'problem' on the first partition since
139
+ # it will propagate to the other partitions during
140
+ # aggregation, otherwise it's a waste of memory
141
+ del partition["problem"]
142
+
143
+ prog_id = (string.ascii_uppercase[i], len(partition.subproblem))
144
+
145
+ ldata, (irow, icol, qdata), _ = partition.subproblem.to_numpy_vectors(
146
+ partition.subproblem.variables
147
+ )
148
+
149
+ coo_mat = sps.coo_matrix(
150
+ (
151
+ np.r_[ldata, qdata],
152
+ (
153
+ np.r_[np.arange(len(ldata)), icol],
154
+ np.r_[np.arange(len(ldata)), irow],
155
+ ),
156
+ ),
157
+ shape=(len(ldata), len(ldata)),
158
+ )
159
+ self.prog_id_to_bqm_subproblem_states[prog_id] = partition
160
+ self.programs[prog_id] = self._constructor(
161
+ job_id=prog_id,
162
+ problem=coo_mat,
163
+ losses=self._manager.list(),
164
+ probs=self._manager.dict(),
165
+ final_params=self._manager.list(),
166
+ solution_bitstring=self._manager.list(),
167
+ progress_queue=self._queue,
168
+ )
169
+
170
+ def aggregate_results(self):
171
+ super().aggregate_results()
172
+
173
+ if any(len(program.probs) == 0 for program in self.programs.values()):
174
+ raise RuntimeError(
175
+ "Not all final probabilities computed yet. Please call `run()` first."
176
+ )
177
+
178
+ for prog_id, subproblem in self.programs.items():
179
+ bqm_subproblem_state = self.prog_id_to_bqm_subproblem_states[prog_id]
180
+
181
+ curr_final_solution = subproblem.solution
182
+
183
+ var_to_val = dict(
184
+ zip(bqm_subproblem_state.subproblem.variables, curr_final_solution)
185
+ )
186
+ sample_set = dimod.SampleSet.from_samples(
187
+ dimod.as_samples(var_to_val), "BINARY", 0
188
+ )
189
+
190
+ self.prog_id_to_bqm_subproblem_states[prog_id] = (
191
+ bqm_subproblem_state.updated(subsamples=sample_set)
192
+ )
193
+
194
+ states = hybrid.States(*list(self.prog_id_to_bqm_subproblem_states.values()))
195
+ final_state = self._aggregating.run(states).result()
196
+
197
+ self.solution, self.solution_energy, _ = final_state.samples.record[0]
198
+
199
+ return self.solution, self.solution_energy