qoro-divi 0.2.0b1__py3-none-any.whl → 0.6.0__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.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +10 -0
- divi/backends/_backend_properties_conversion.py +227 -0
- divi/backends/_circuit_runner.py +70 -0
- divi/backends/_execution_result.py +70 -0
- divi/backends/_parallel_simulator.py +486 -0
- divi/backends/_qoro_service.py +663 -0
- divi/backends/_qpu_system.py +101 -0
- divi/backends/_results_processing.py +133 -0
- divi/circuits/__init__.py +13 -0
- divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
- divi/circuits/_cirq/_parser.py +110 -0
- divi/circuits/_cirq/_qasm_export.py +78 -0
- divi/circuits/_core.py +391 -0
- divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
- divi/circuits/_qasm_validation.py +694 -0
- divi/qprog/__init__.py +27 -8
- divi/qprog/_expectation.py +181 -0
- divi/qprog/_hamiltonians.py +281 -0
- divi/qprog/algorithms/__init__.py +16 -0
- divi/qprog/algorithms/_ansatze.py +368 -0
- divi/qprog/algorithms/_custom_vqa.py +263 -0
- divi/qprog/algorithms/_pce.py +262 -0
- divi/qprog/algorithms/_qaoa.py +579 -0
- divi/qprog/algorithms/_vqe.py +262 -0
- divi/qprog/batch.py +387 -74
- divi/qprog/checkpointing.py +556 -0
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +1014 -43
- divi/qprog/quantum_program.py +243 -412
- divi/qprog/typing.py +62 -0
- divi/qprog/variational_quantum_algorithm.py +1208 -0
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
- divi/qprog/workflows/_qubo_partitioning.py +221 -0
- divi/qprog/workflows/_vqe_sweep.py +560 -0
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +127 -0
- divi/reporting/_qlogger.py +68 -0
- divi/reporting/_reporter.py +155 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/METADATA +43 -15
- qoro_divi-0.6.0.dist-info/RECORD +47 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/WHEEL +1 -1
- qoro_divi-0.6.0.dist-info/licenses/LICENSES/.license-header +3 -0
- divi/_pbar.py +0 -73
- divi/circuits.py +0 -139
- divi/exp/cirq/_lexer.py +0 -126
- divi/exp/cirq/_parser.py +0 -889
- divi/exp/cirq/_qasm_export.py +0 -37
- divi/exp/cirq/_qasm_import.py +0 -35
- divi/exp/cirq/exception.py +0 -21
- divi/exp/scipy/_cobyla.py +0 -342
- divi/exp/scipy/pyprima/LICENCE.txt +0 -28
- divi/exp/scipy/pyprima/__init__.py +0 -263
- divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
- divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
- divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
- divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
- divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
- divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
- divi/exp/scipy/pyprima/cobyla/update.py +0 -331
- divi/exp/scipy/pyprima/common/__init__.py +0 -0
- divi/exp/scipy/pyprima/common/_bounds.py +0 -41
- divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
- divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
- divi/exp/scipy/pyprima/common/_project.py +0 -224
- divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
- divi/exp/scipy/pyprima/common/consts.py +0 -48
- divi/exp/scipy/pyprima/common/evaluate.py +0 -101
- divi/exp/scipy/pyprima/common/history.py +0 -39
- divi/exp/scipy/pyprima/common/infos.py +0 -30
- divi/exp/scipy/pyprima/common/linalg.py +0 -452
- divi/exp/scipy/pyprima/common/message.py +0 -336
- divi/exp/scipy/pyprima/common/powalg.py +0 -131
- divi/exp/scipy/pyprima/common/preproc.py +0 -393
- divi/exp/scipy/pyprima/common/present.py +0 -5
- divi/exp/scipy/pyprima/common/ratio.py +0 -56
- divi/exp/scipy/pyprima/common/redrho.py +0 -49
- divi/exp/scipy/pyprima/common/selectx.py +0 -346
- divi/interfaces.py +0 -25
- divi/parallel_simulator.py +0 -258
- divi/qlogger.py +0 -119
- divi/qoro_service.py +0 -343
- divi/qprog/_mlae.py +0 -182
- divi/qprog/_qaoa.py +0 -440
- divi/qprog/_vqe.py +0 -275
- divi/qprog/_vqe_sweep.py +0 -144
- divi/utils.py +0 -116
- qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
- /divi/{qem.py → circuits/qem.py} +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSE +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from ._graph_partitioning import (
|
|
6
|
+
GraphPartitioningQAOA,
|
|
7
|
+
PartitioningConfig,
|
|
8
|
+
)
|
|
9
|
+
from ._qubo_partitioning import QUBOPartitioningQAOA
|
|
10
|
+
from ._vqe_sweep import MoleculeTransformer, VQEHyperparameterSweep
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
5
|
import heapq
|
|
6
|
-
import re
|
|
7
6
|
import string
|
|
8
7
|
from collections.abc import Callable, Sequence
|
|
9
|
-
from concurrent.futures import ProcessPoolExecutor
|
|
10
8
|
from dataclasses import dataclass
|
|
11
9
|
from functools import partial
|
|
12
|
-
from typing import Literal
|
|
10
|
+
from typing import Literal
|
|
13
11
|
from warnings import warn
|
|
14
12
|
|
|
15
13
|
import matplotlib.cm as cm
|
|
@@ -21,15 +19,15 @@ import scipy.sparse.linalg as spla
|
|
|
21
19
|
from pymetis import part_graph
|
|
22
20
|
from sklearn.cluster import SpectralClustering
|
|
23
21
|
|
|
24
|
-
from divi.
|
|
22
|
+
from divi.backends import CircuitRunner
|
|
25
23
|
from divi.qprog import QAOA, ProgramBatch
|
|
26
|
-
from divi.qprog._qaoa import (
|
|
24
|
+
from divi.qprog.algorithms._qaoa import (
|
|
27
25
|
_SUPPORTED_INITIAL_STATES_LITERAL,
|
|
28
26
|
GraphProblem,
|
|
27
|
+
GraphProblemTypes,
|
|
29
28
|
draw_graph_solution_nodes,
|
|
30
29
|
)
|
|
31
|
-
|
|
32
|
-
from .optimizers import Optimizer
|
|
30
|
+
from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer, copy_optimizer
|
|
33
31
|
|
|
34
32
|
AggregateFn = Callable[
|
|
35
33
|
[list[int], str, nx.Graph | rx.PyGraph, dict[int, int]], list[int]
|
|
@@ -42,8 +40,45 @@ _MAXIMUM_AVAILABLE_QUBITS = 30
|
|
|
42
40
|
|
|
43
41
|
@dataclass(frozen=True, eq=True)
|
|
44
42
|
class PartitioningConfig:
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
"""Configuration for graph partitioning algorithms.
|
|
44
|
+
|
|
45
|
+
This class defines the parameters and constraints for partitioning large graphs
|
|
46
|
+
into smaller subgraphs for quantum algorithm execution. It supports multiple
|
|
47
|
+
partitioning algorithms and allows specification of size constraints.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
max_n_nodes_per_cluster: Maximum number of nodes allowed in each cluster.
|
|
51
|
+
If None, no upper limit is enforced. Must be a positive integer.
|
|
52
|
+
minimum_n_clusters: Minimum number of clusters to create. If None, no
|
|
53
|
+
lower limit is enforced. Must be a positive integer.
|
|
54
|
+
partitioning_algorithm: Algorithm to use for partitioning. Options are:
|
|
55
|
+
- "spectral": Spectral partitioning using Fiedler vector (default)
|
|
56
|
+
- "metis": METIS graph partitioning library
|
|
57
|
+
- "kernighan_lin": Kernighan-Lin algorithm
|
|
58
|
+
|
|
59
|
+
Note:
|
|
60
|
+
At least one of `max_n_nodes_per_cluster` or `minimum_n_clusters` must be
|
|
61
|
+
specified. Both constraints cannot be None.
|
|
62
|
+
|
|
63
|
+
Examples:
|
|
64
|
+
>>> # Partition into clusters of at most 10 nodes
|
|
65
|
+
>>> config = PartitioningConfig(max_n_nodes_per_cluster=10)
|
|
66
|
+
|
|
67
|
+
>>> # Create at least 5 clusters using METIS
|
|
68
|
+
>>> config = PartitioningConfig(
|
|
69
|
+
... minimum_n_clusters=5,
|
|
70
|
+
... partitioning_algorithm="metis"
|
|
71
|
+
... )
|
|
72
|
+
|
|
73
|
+
>>> # Both constraints: clusters of max 8 nodes, min 3 clusters
|
|
74
|
+
>>> config = PartitioningConfig(
|
|
75
|
+
... max_n_nodes_per_cluster=8,
|
|
76
|
+
... minimum_n_clusters=3
|
|
77
|
+
... )
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
max_n_nodes_per_cluster: int | None = None
|
|
81
|
+
minimum_n_clusters: int | None = None
|
|
47
82
|
partitioning_algorithm: Literal["spectral", "metis", "kernighan_lin"] = "spectral"
|
|
48
83
|
|
|
49
84
|
def __post_init__(self):
|
|
@@ -334,19 +369,43 @@ def _node_partition_graph(
|
|
|
334
369
|
return tuple(graph for (_, _, graph) in subgraphs)
|
|
335
370
|
|
|
336
371
|
|
|
337
|
-
def linear_aggregation(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
def linear_aggregation(
|
|
373
|
+
curr_solution: Sequence[Literal[0] | Literal[1]],
|
|
374
|
+
subproblem_solution: set[int],
|
|
375
|
+
subproblem_reverse_index_map: dict[int, int],
|
|
376
|
+
):
|
|
377
|
+
"""Linearly combines a subproblem's solution into the main solution vector.
|
|
378
|
+
|
|
379
|
+
This function iterates through each node of subproblem's solution. For each node,
|
|
380
|
+
it uses the reverse index map to find its original index in the main graph,
|
|
381
|
+
setting it to 1 in the current global solution, potentially overwriting any
|
|
382
|
+
previous states.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
curr_solution (Sequence[Literal[0] | Literal[1]]): The main solution
|
|
386
|
+
vector being aggregated, represented as a sequence of 0s and 1s.
|
|
387
|
+
subproblem_solution (Set[int]): A set containing the original indices of
|
|
388
|
+
the nodes that form the solution for the subproblem.
|
|
389
|
+
subproblem_reverse_index_map (dict[int, int]): A mapping from the
|
|
390
|
+
subgraph's internal node labels back to their original indices in
|
|
391
|
+
the main solution vector.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
The updated main solution vector.
|
|
395
|
+
"""
|
|
396
|
+
for node in subproblem_solution:
|
|
397
|
+
curr_solution[subproblem_reverse_index_map[node]] = 1
|
|
341
398
|
|
|
342
399
|
return curr_solution
|
|
343
400
|
|
|
344
401
|
|
|
345
|
-
def
|
|
346
|
-
curr_solution
|
|
402
|
+
def dominance_aggregation(
|
|
403
|
+
curr_solution: Sequence[Literal[0] | Literal[1]],
|
|
404
|
+
subproblem_solution: set[int],
|
|
405
|
+
subproblem_reverse_index_map: dict[int, int],
|
|
347
406
|
):
|
|
348
|
-
for node in
|
|
349
|
-
|
|
407
|
+
for node in subproblem_solution:
|
|
408
|
+
original_index = subproblem_reverse_index_map[node]
|
|
350
409
|
|
|
351
410
|
# Use existing assignment if dominant in previous solutions
|
|
352
411
|
# (e.g., more 0s than 1s or vice versa)
|
|
@@ -354,12 +413,12 @@ def domninance_aggregation(
|
|
|
354
413
|
count_1 = curr_solution.count(1)
|
|
355
414
|
|
|
356
415
|
if (
|
|
357
|
-
(count_0 > count_1 and curr_solution[
|
|
358
|
-
or (count_1 > count_0 and curr_solution[
|
|
416
|
+
(count_0 > count_1 and curr_solution[original_index] == 0)
|
|
417
|
+
or (count_1 > count_0 and curr_solution[original_index] == 1)
|
|
359
418
|
or (count_0 == count_1)
|
|
360
419
|
):
|
|
361
420
|
# Assign based on QAOA if tie
|
|
362
|
-
curr_solution[
|
|
421
|
+
curr_solution[original_index] = 1
|
|
363
422
|
|
|
364
423
|
return curr_solution
|
|
365
424
|
|
|
@@ -367,14 +426,14 @@ def domninance_aggregation(
|
|
|
367
426
|
class GraphPartitioningQAOA(ProgramBatch):
|
|
368
427
|
def __init__(
|
|
369
428
|
self,
|
|
370
|
-
graph:
|
|
429
|
+
graph: GraphProblemTypes,
|
|
371
430
|
graph_problem: GraphProblem,
|
|
372
431
|
n_layers: int,
|
|
373
432
|
backend: CircuitRunner,
|
|
374
433
|
partitioning_config: PartitioningConfig,
|
|
375
434
|
initial_state: _SUPPORTED_INITIAL_STATES_LITERAL = "Recommended",
|
|
376
435
|
aggregate_fn: AggregateFn = linear_aggregation,
|
|
377
|
-
optimizer=
|
|
436
|
+
optimizer: Optimizer | None = None,
|
|
378
437
|
max_iterations=10,
|
|
379
438
|
**kwargs,
|
|
380
439
|
):
|
|
@@ -409,15 +468,18 @@ class GraphPartitioningQAOA(ProgramBatch):
|
|
|
409
468
|
self.partitioning_config = partitioning_config
|
|
410
469
|
self.max_iterations = max_iterations
|
|
411
470
|
|
|
471
|
+
self.solution = None
|
|
412
472
|
self.aggregate_fn = aggregate_fn
|
|
413
473
|
|
|
414
|
-
|
|
474
|
+
# Store the optimizer template (will be copied for each program)
|
|
475
|
+
self._optimizer_template = (
|
|
476
|
+
optimizer if optimizer is not None else MonteCarloOptimizer()
|
|
477
|
+
)
|
|
415
478
|
|
|
416
479
|
self._constructor = partial(
|
|
417
480
|
QAOA,
|
|
418
481
|
initial_state=initial_state,
|
|
419
482
|
graph_problem=graph_problem,
|
|
420
|
-
optimizer=optimizer,
|
|
421
483
|
max_iterations=self.max_iterations,
|
|
422
484
|
backend=self.backend,
|
|
423
485
|
n_layers=n_layers,
|
|
@@ -425,11 +487,23 @@ class GraphPartitioningQAOA(ProgramBatch):
|
|
|
425
487
|
)
|
|
426
488
|
|
|
427
489
|
def create_programs(self):
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
490
|
+
"""
|
|
491
|
+
Creates and initializes QAOA programs for each partitioned subgraph.
|
|
492
|
+
|
|
493
|
+
The main graph is partitioned into node-based subgraphs
|
|
494
|
+
according to the specified partitioning configuration. Each subgraph is relabeled with
|
|
495
|
+
integer node labels for QAOA compatibility, and a reverse index map is stored for later
|
|
496
|
+
result aggregation.
|
|
497
|
+
|
|
498
|
+
Each program is assigned a unique program ID, which is a tuple of:
|
|
499
|
+
- An uppercase letter (A, B, C, ...) corresponding to the partition index.
|
|
500
|
+
- The number of nodes in the subgraph.
|
|
501
|
+
|
|
502
|
+
Example program ID: ('A', 5) for the first partition with 5 nodes.
|
|
503
|
+
|
|
504
|
+
The created QAOA programs are stored in the `self.programs` dictionary, keyed by their program IDs.
|
|
505
|
+
|
|
506
|
+
"""
|
|
433
507
|
|
|
434
508
|
super().create_programs()
|
|
435
509
|
|
|
@@ -449,84 +523,48 @@ class GraphPartitioningQAOA(ProgramBatch):
|
|
|
449
523
|
self.reverse_index_maps = {}
|
|
450
524
|
|
|
451
525
|
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
526
|
prog_id = (string.ascii_uppercase[i], subgraph.number_of_nodes())
|
|
457
527
|
|
|
458
|
-
|
|
459
|
-
|
|
528
|
+
index_map = {node: idx for idx, node in enumerate(subgraph.nodes())}
|
|
529
|
+
self.reverse_index_maps[prog_id] = {v: k for k, v in index_map.items()}
|
|
530
|
+
|
|
531
|
+
_subgraph = nx.relabel_nodes(subgraph, index_map)
|
|
532
|
+
self._programs[prog_id] = self._constructor(
|
|
533
|
+
program_id=prog_id,
|
|
460
534
|
problem=_subgraph,
|
|
461
|
-
|
|
462
|
-
probs=self._manager.dict(),
|
|
463
|
-
final_params=self._manager.list(),
|
|
535
|
+
optimizer=copy_optimizer(self._optimizer_template),
|
|
464
536
|
progress_queue=self._queue,
|
|
465
537
|
)
|
|
466
538
|
|
|
467
|
-
def compute_final_solutions(self):
|
|
468
|
-
if self._executor is not None:
|
|
469
|
-
self.wait_for_all()
|
|
470
|
-
|
|
471
|
-
if self._executor is not None:
|
|
472
|
-
raise RuntimeError("A batch is already being run.")
|
|
473
|
-
|
|
474
|
-
if len(self.programs) == 0:
|
|
475
|
-
raise RuntimeError("No programs to run.")
|
|
476
|
-
|
|
477
|
-
self._executor = ProcessPoolExecutor()
|
|
478
|
-
|
|
479
|
-
self.futures = [
|
|
480
|
-
self._executor.submit(program.compute_final_solution)
|
|
481
|
-
for program in self.programs.values()
|
|
482
|
-
]
|
|
483
|
-
|
|
484
539
|
def aggregate_results(self):
|
|
485
|
-
|
|
486
|
-
|
|
540
|
+
"""
|
|
541
|
+
Aggregates the results from all QAOA subprograms to form a global solution.
|
|
487
542
|
|
|
488
|
-
|
|
489
|
-
|
|
543
|
+
This method collects the final bitstring solutions from each partitioned subgraph's QAOA program,
|
|
544
|
+
using the aggregation function specified at initialization (e.g., linear or dominance aggregation).
|
|
545
|
+
It reconstructs the global solution by mapping each subgraph's solution back to the original node indices
|
|
546
|
+
using the stored reverse index maps.
|
|
490
547
|
|
|
491
|
-
|
|
492
|
-
raise RuntimeError(
|
|
493
|
-
"Some/All programs have empty losses. Did you call run()?"
|
|
494
|
-
)
|
|
548
|
+
The final solution is stored in `self.solution` as a list of node indices assigned to the selected partition.
|
|
495
549
|
|
|
496
|
-
|
|
550
|
+
Raises:
|
|
551
|
+
RuntimeError: If no programs exist, if programs have not been run, or if results are incomplete.
|
|
552
|
+
Returns:
|
|
553
|
+
list[int]: The list of node indices in the final aggregated solution.
|
|
554
|
+
"""
|
|
555
|
+
super().aggregate_results()
|
|
556
|
+
|
|
557
|
+
if any(len(program.best_probs) == 0 for program in self.programs.values()):
|
|
497
558
|
raise RuntimeError(
|
|
498
|
-
"Not all final probabilities computed yet. Please call `
|
|
559
|
+
"Not all final probabilities computed yet. Please call `run()` first."
|
|
499
560
|
)
|
|
500
561
|
|
|
501
562
|
# 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
|
-
|
|
563
|
+
for prog_id, program in self.programs.items():
|
|
525
564
|
self._bitstring_solution = self.aggregate_fn(
|
|
526
565
|
self._bitstring_solution,
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
reverse_index_maps,
|
|
566
|
+
program.solution,
|
|
567
|
+
self.reverse_index_maps[prog_id],
|
|
530
568
|
)
|
|
531
569
|
|
|
532
570
|
self.solution = list(np.where(self._bitstring_solution)[0])
|
|
@@ -559,9 +597,9 @@ class GraphPartitioningQAOA(ProgramBatch):
|
|
|
559
597
|
|
|
560
598
|
# Convert partitions to node-to-partition mapping
|
|
561
599
|
node_to_partition = {}
|
|
562
|
-
for partition_id, mapping in self.reverse_index_maps.items():
|
|
600
|
+
for (partition_id, _), mapping in self.reverse_index_maps.items():
|
|
563
601
|
for node in mapping.values():
|
|
564
|
-
node_to_partition[node] =
|
|
602
|
+
node_to_partition[node] = partition_id
|
|
565
603
|
|
|
566
604
|
# Get unique partition IDs and create color map
|
|
567
605
|
unique_partitions = sorted(list(set(node_to_partition.values())))
|
|
@@ -613,7 +651,13 @@ class GraphPartitioningQAOA(ProgramBatch):
|
|
|
613
651
|
plt.show()
|
|
614
652
|
|
|
615
653
|
def draw_solution(self):
|
|
616
|
-
|
|
617
|
-
|
|
654
|
+
"""
|
|
655
|
+
Visualizes the main graph with nodes highlighted according to the final aggregated solution.
|
|
656
|
+
|
|
657
|
+
If the solution has not yet been computed, this method calls `aggregate_results()` to obtain it.
|
|
658
|
+
"""
|
|
659
|
+
|
|
660
|
+
if self.solution is None:
|
|
661
|
+
self.solution = self.aggregate_results()
|
|
618
662
|
|
|
619
663
|
draw_graph_solution_nodes(self.main_graph, self.solution)
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 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 numpy.typing as npt
|
|
13
|
+
import scipy.sparse as sps
|
|
14
|
+
from dimod import BinaryQuadraticModel
|
|
15
|
+
|
|
16
|
+
from divi.backends import CircuitRunner
|
|
17
|
+
from divi.qprog.algorithms import QAOA
|
|
18
|
+
from divi.qprog.batch import ProgramBatch
|
|
19
|
+
from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer, copy_optimizer
|
|
20
|
+
from divi.qprog.typing import QUBOProblemTypes
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Helper function to merge subsamples in-place
|
|
24
|
+
def _merge_substates(_, substates):
|
|
25
|
+
a, b = substates
|
|
26
|
+
return a.updated(subsamples=hybrid.hstack_samplesets(a.subsamples, b.subsamples))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
T = TypeVar("T", bound=QUBOProblemTypes | BinaryQuadraticModel)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _sanitize_problem_input(qubo: T) -> tuple[T, BinaryQuadraticModel]:
|
|
33
|
+
if isinstance(qubo, BinaryQuadraticModel):
|
|
34
|
+
return qubo, qubo
|
|
35
|
+
|
|
36
|
+
if isinstance(qubo, (np.ndarray, sps.spmatrix)):
|
|
37
|
+
x, y = qubo.shape
|
|
38
|
+
if x != y:
|
|
39
|
+
raise ValueError("Only matrices supported.")
|
|
40
|
+
|
|
41
|
+
if isinstance(qubo, np.ndarray):
|
|
42
|
+
return qubo, dimod.BinaryQuadraticModel(qubo, vartype=dimod.Vartype.BINARY)
|
|
43
|
+
|
|
44
|
+
if isinstance(qubo, sps.spmatrix):
|
|
45
|
+
return qubo, dimod.BinaryQuadraticModel(
|
|
46
|
+
{(row, col): data for row, col, data in zip(qubo.row, qubo.col, qubo.data)},
|
|
47
|
+
vartype=dimod.Vartype.BINARY,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
raise ValueError(f"Got an unsupported QUBO input format: {type(qubo)}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class QUBOPartitioningQAOA(ProgramBatch):
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
qubo: QUBOProblemTypes,
|
|
57
|
+
decomposer: hybrid.traits.ProblemDecomposer,
|
|
58
|
+
n_layers: int,
|
|
59
|
+
backend: CircuitRunner,
|
|
60
|
+
composer: hybrid.traits.SubsamplesComposer = hybrid.SplatComposer(),
|
|
61
|
+
optimizer: Optimizer | None = None,
|
|
62
|
+
max_iterations: int = 10,
|
|
63
|
+
**kwargs,
|
|
64
|
+
):
|
|
65
|
+
"""
|
|
66
|
+
Initialize a QUBOPartitioningQAOA instance for solving QUBO problems using partitioning and QAOA.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
qubo (QUBOProblemTypes): The QUBO problem to solve, provided as a supported type.
|
|
70
|
+
Note: Variable types are assumed to be binary (not Spin).
|
|
71
|
+
decomposer (hybrid.traits.ProblemDecomposer): The decomposer used to partition the QUBO problem into subproblems.
|
|
72
|
+
n_layers (int): Number of QAOA layers to use for each subproblem.
|
|
73
|
+
backend (CircuitRunner): Backend responsible for running quantum circuits.
|
|
74
|
+
composer (hybrid.traits.SubsamplesComposer, optional): Composer to aggregate subsamples from subproblems.
|
|
75
|
+
Defaults to hybrid.SplatComposer().
|
|
76
|
+
optimizer (Optimizer, optional): Optimizer to use for QAOA.
|
|
77
|
+
Defaults to Optimizer.MONTE_CARLO.
|
|
78
|
+
max_iterations (int, optional): Maximum number of optimization iterations.
|
|
79
|
+
Defaults to 10.
|
|
80
|
+
**kwargs: Additional keyword arguments passed to the QAOA constructor.
|
|
81
|
+
|
|
82
|
+
"""
|
|
83
|
+
super().__init__(backend=backend)
|
|
84
|
+
|
|
85
|
+
self.main_qubo, self._bqm = _sanitize_problem_input(qubo)
|
|
86
|
+
|
|
87
|
+
self._partitioning = hybrid.Unwind(decomposer)
|
|
88
|
+
self._aggregating = hybrid.Reduce(hybrid.Lambda(_merge_substates)) | composer
|
|
89
|
+
|
|
90
|
+
self.max_iterations = max_iterations
|
|
91
|
+
|
|
92
|
+
self.trivial_program_ids = set()
|
|
93
|
+
|
|
94
|
+
# Store the optimizer template (will be copied for each program)
|
|
95
|
+
self._optimizer_template = (
|
|
96
|
+
optimizer if optimizer is not None else MonteCarloOptimizer()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self._constructor = partial(
|
|
100
|
+
QAOA,
|
|
101
|
+
max_iterations=self.max_iterations,
|
|
102
|
+
backend=self.backend,
|
|
103
|
+
n_layers=n_layers,
|
|
104
|
+
**kwargs,
|
|
105
|
+
)
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
def create_programs(self):
|
|
109
|
+
"""
|
|
110
|
+
Partition the main QUBO problem and instantiate QAOA programs for each subproblem.
|
|
111
|
+
|
|
112
|
+
This implementation:
|
|
113
|
+
- Uses the configured decomposer to split the main QUBO into subproblems.
|
|
114
|
+
- For each subproblem, creates a QAOA program with the specified parameters.
|
|
115
|
+
- Stores each program in `self.programs` with a unique identifier.
|
|
116
|
+
|
|
117
|
+
Unique Identifier Format:
|
|
118
|
+
Each key in `self.programs` is a tuple of the form (letter, size), where:
|
|
119
|
+
- letter: An uppercase letter ('A', 'B', 'C', ...) indicating the partition index.
|
|
120
|
+
- size: The number of variables in the subproblem.
|
|
121
|
+
|
|
122
|
+
Example: ('A', 5) refers to the first partition with 5 variables.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
super().create_programs()
|
|
126
|
+
|
|
127
|
+
self.prog_id_to_bqm_subproblem_states = {}
|
|
128
|
+
|
|
129
|
+
init_state = hybrid.State.from_problem(self._bqm)
|
|
130
|
+
_bqm_partitions = self._partitioning.run(init_state).result()
|
|
131
|
+
|
|
132
|
+
for i, partition in enumerate(_bqm_partitions):
|
|
133
|
+
if i > 0:
|
|
134
|
+
# We only need 'problem' on the first partition since
|
|
135
|
+
# it will propagate to the other partitions during
|
|
136
|
+
# aggregation, otherwise it's a waste of memory
|
|
137
|
+
del partition["problem"]
|
|
138
|
+
|
|
139
|
+
prog_id = (string.ascii_uppercase[i], len(partition.subproblem))
|
|
140
|
+
self.prog_id_to_bqm_subproblem_states[prog_id] = partition
|
|
141
|
+
|
|
142
|
+
if partition.subproblem.num_interactions == 0:
|
|
143
|
+
# Skip creating a full QAOA program for this trivial case.
|
|
144
|
+
self.trivial_program_ids.add(prog_id)
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
ldata, (irow, icol, qdata), _ = partition.subproblem.to_numpy_vectors(
|
|
148
|
+
partition.subproblem.variables
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
coo_mat = sps.coo_matrix(
|
|
152
|
+
(
|
|
153
|
+
np.r_[ldata, qdata],
|
|
154
|
+
(
|
|
155
|
+
np.r_[np.arange(len(ldata)), icol],
|
|
156
|
+
np.r_[np.arange(len(ldata)), irow],
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
shape=(len(ldata), len(ldata)),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._programs[prog_id] = self._constructor(
|
|
163
|
+
program_id=prog_id,
|
|
164
|
+
problem=coo_mat,
|
|
165
|
+
optimizer=copy_optimizer(self._optimizer_template),
|
|
166
|
+
progress_queue=self._queue,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def aggregate_results(self) -> tuple[npt.NDArray[np.int32], float]:
|
|
170
|
+
"""
|
|
171
|
+
Aggregate results from all QUBO subproblems into a global solution.
|
|
172
|
+
|
|
173
|
+
Collects solutions from each partitioned subproblem (both QAOA-optimized and
|
|
174
|
+
trivial ones) and uses the hybrid framework composer to combine them into
|
|
175
|
+
a final solution for the original QUBO problem.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
tuple: A tuple containing:
|
|
179
|
+
- solution (npt.NDArray[np.int32]): Binary solution vector for the QUBO problem.
|
|
180
|
+
- solution_energy (float): Energy/cost of the solution.
|
|
181
|
+
|
|
182
|
+
Raises:
|
|
183
|
+
RuntimeError: If programs haven't been run or if final probabilities
|
|
184
|
+
haven't been computed.
|
|
185
|
+
"""
|
|
186
|
+
super().aggregate_results()
|
|
187
|
+
|
|
188
|
+
if any(len(program.best_probs) == 0 for program in self.programs.values()):
|
|
189
|
+
raise RuntimeError(
|
|
190
|
+
"Not all final probabilities computed yet. Please call `run()` first."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
for (
|
|
194
|
+
prog_id,
|
|
195
|
+
bqm_subproblem_state,
|
|
196
|
+
) in self.prog_id_to_bqm_subproblem_states.items():
|
|
197
|
+
|
|
198
|
+
if prog_id in self.trivial_program_ids:
|
|
199
|
+
# Case 1: Trivial problem. Solve classically.
|
|
200
|
+
# The solution is any bitstring (e.g., all zeros) with energy 0.
|
|
201
|
+
var_to_val = {v: 0 for v in bqm_subproblem_state.subproblem.variables}
|
|
202
|
+
else:
|
|
203
|
+
subproblem = self._programs[prog_id]
|
|
204
|
+
var_to_val = dict(
|
|
205
|
+
zip(bqm_subproblem_state.subproblem.variables, subproblem.solution)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
sample_set = dimod.SampleSet.from_samples(
|
|
209
|
+
dimod.as_samples(var_to_val), "BINARY", 0
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
self.prog_id_to_bqm_subproblem_states[prog_id] = (
|
|
213
|
+
bqm_subproblem_state.updated(subsamples=sample_set)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
states = hybrid.States(*list(self.prog_id_to_bqm_subproblem_states.values()))
|
|
217
|
+
final_state = self._aggregating.run(states).result()
|
|
218
|
+
|
|
219
|
+
self.solution, self.solution_energy, _ = final_state.samples.record[0]
|
|
220
|
+
|
|
221
|
+
return self.solution, self.solution_energy
|