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.
- divi/_pbar.py +1 -3
- divi/circuits.py +3 -3
- divi/exp/cirq/__init__.py +1 -0
- divi/exp/cirq/_validator.py +645 -0
- divi/parallel_simulator.py +9 -9
- divi/qasm.py +2 -3
- divi/qoro_service.py +210 -141
- divi/qprog/__init__.py +2 -2
- divi/qprog/_graph_partitioning.py +103 -66
- divi/qprog/_qaoa.py +33 -8
- divi/qprog/_qubo_partitioning.py +199 -0
- divi/qprog/_vqe.py +48 -39
- divi/qprog/_vqe_sweep.py +413 -46
- divi/qprog/batch.py +61 -14
- divi/qprog/quantum_program.py +10 -11
- divi/qpu_system.py +20 -0
- qoro_divi-0.3.0b1.dist-info/LICENSES/.license-header +3 -0
- {qoro_divi-0.2.2b1.dist-info → qoro_divi-0.3.0b1.dist-info}/METADATA +5 -2
- {qoro_divi-0.2.2b1.dist-info → qoro_divi-0.3.0b1.dist-info}/RECORD +22 -19
- divi/qprog/_mlae.py +0 -182
- {qoro_divi-0.2.2b1.dist-info → qoro_divi-0.3.0b1.dist-info}/LICENSE +0 -0
- {qoro_divi-0.2.2b1.dist-info → qoro_divi-0.3.0b1.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.2.2b1.dist-info → qoro_divi-0.3.0b1.dist-info}/WHEEL +0 -0
|
@@ -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
|
|
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:
|
|
46
|
-
minimum_n_clusters:
|
|
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(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
346
|
-
curr_solution
|
|
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
|
|
349
|
-
|
|
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[
|
|
358
|
-
or (count_1 > count_0 and curr_solution[
|
|
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[
|
|
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:
|
|
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.
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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.
|
|
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
|
-
|
|
486
|
-
|
|
533
|
+
"""
|
|
534
|
+
Aggregates the results from all QAOA subprograms to form a global solution.
|
|
487
535
|
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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 `
|
|
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
|
|
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
|
-
|
|
528
|
-
|
|
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] =
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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:
|
|
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
|