qoro-divi 0.2.0b1__py3-none-any.whl → 0.5.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.
Files changed (88) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +9 -0
  3. divi/backends/_circuit_runner.py +70 -0
  4. divi/backends/_execution_result.py +70 -0
  5. divi/backends/_parallel_simulator.py +486 -0
  6. divi/backends/_qoro_service.py +663 -0
  7. divi/backends/_qpu_system.py +101 -0
  8. divi/backends/_results_processing.py +133 -0
  9. divi/circuits/__init__.py +8 -0
  10. divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
  11. divi/circuits/_cirq/_parser.py +110 -0
  12. divi/circuits/_cirq/_qasm_export.py +78 -0
  13. divi/circuits/_core.py +369 -0
  14. divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
  15. divi/circuits/_qasm_validation.py +694 -0
  16. divi/qprog/__init__.py +24 -6
  17. divi/qprog/_expectation.py +181 -0
  18. divi/qprog/_hamiltonians.py +281 -0
  19. divi/qprog/algorithms/__init__.py +14 -0
  20. divi/qprog/algorithms/_ansatze.py +356 -0
  21. divi/qprog/algorithms/_qaoa.py +572 -0
  22. divi/qprog/algorithms/_vqe.py +249 -0
  23. divi/qprog/batch.py +383 -73
  24. divi/qprog/checkpointing.py +556 -0
  25. divi/qprog/exceptions.py +9 -0
  26. divi/qprog/optimizers.py +1014 -43
  27. divi/qprog/quantum_program.py +231 -413
  28. divi/qprog/variational_quantum_algorithm.py +995 -0
  29. divi/qprog/workflows/__init__.py +10 -0
  30. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
  31. divi/qprog/workflows/_qubo_partitioning.py +220 -0
  32. divi/qprog/workflows/_vqe_sweep.py +560 -0
  33. divi/reporting/__init__.py +7 -0
  34. divi/reporting/_pbar.py +127 -0
  35. divi/reporting/_qlogger.py +68 -0
  36. divi/reporting/_reporter.py +133 -0
  37. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info}/METADATA +43 -15
  38. qoro_divi-0.5.0.dist-info/RECORD +43 -0
  39. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info}/WHEEL +1 -1
  40. qoro_divi-0.5.0.dist-info/licenses/LICENSES/.license-header +3 -0
  41. divi/_pbar.py +0 -73
  42. divi/circuits.py +0 -139
  43. divi/exp/cirq/_lexer.py +0 -126
  44. divi/exp/cirq/_parser.py +0 -889
  45. divi/exp/cirq/_qasm_export.py +0 -37
  46. divi/exp/cirq/_qasm_import.py +0 -35
  47. divi/exp/cirq/exception.py +0 -21
  48. divi/exp/scipy/_cobyla.py +0 -342
  49. divi/exp/scipy/pyprima/LICENCE.txt +0 -28
  50. divi/exp/scipy/pyprima/__init__.py +0 -263
  51. divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
  52. divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
  53. divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
  54. divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
  55. divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
  56. divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
  57. divi/exp/scipy/pyprima/cobyla/update.py +0 -331
  58. divi/exp/scipy/pyprima/common/__init__.py +0 -0
  59. divi/exp/scipy/pyprima/common/_bounds.py +0 -41
  60. divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
  61. divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
  62. divi/exp/scipy/pyprima/common/_project.py +0 -224
  63. divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
  64. divi/exp/scipy/pyprima/common/consts.py +0 -48
  65. divi/exp/scipy/pyprima/common/evaluate.py +0 -101
  66. divi/exp/scipy/pyprima/common/history.py +0 -39
  67. divi/exp/scipy/pyprima/common/infos.py +0 -30
  68. divi/exp/scipy/pyprima/common/linalg.py +0 -452
  69. divi/exp/scipy/pyprima/common/message.py +0 -336
  70. divi/exp/scipy/pyprima/common/powalg.py +0 -131
  71. divi/exp/scipy/pyprima/common/preproc.py +0 -393
  72. divi/exp/scipy/pyprima/common/present.py +0 -5
  73. divi/exp/scipy/pyprima/common/ratio.py +0 -56
  74. divi/exp/scipy/pyprima/common/redrho.py +0 -49
  75. divi/exp/scipy/pyprima/common/selectx.py +0 -346
  76. divi/interfaces.py +0 -25
  77. divi/parallel_simulator.py +0 -258
  78. divi/qlogger.py +0 -119
  79. divi/qoro_service.py +0 -343
  80. divi/qprog/_mlae.py +0 -182
  81. divi/qprog/_qaoa.py +0 -440
  82. divi/qprog/_vqe.py +0 -275
  83. divi/qprog/_vqe_sweep.py +0 -144
  84. divi/utils.py +0 -116
  85. qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
  86. /divi/{qem.py → circuits/qem.py} +0 -0
  87. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info/licenses}/LICENSE +0 -0
  88. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.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, Optional
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.interfaces import CircuitRunner
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
- max_n_nodes_per_cluster: Optional[int] = None
46
- minimum_n_clusters: Optional[int] = None
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(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])
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 domninance_aggregation(
346
- curr_solution, solution_bitstring, graph, reverse_index_maps
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 graph.nodes():
349
- solution_index = reverse_index_maps[node]
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[node] == 0)
358
- or (count_1 > count_0 and curr_solution[node] == 1)
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[solution_index] = int(solution_bitstring[node])
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: nx.Graph | rx.PyGraph,
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=Optimizer.MONTE_CARLO,
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
- self._solution_nodes = None
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
- 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
- )
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
- self.programs[prog_id] = self._constructor(
459
- job_id=prog_id,
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
- losses=self._manager.list(),
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
- if len(self.programs) == 0:
486
- raise RuntimeError("No programs to aggregate. Run create_programs() first.")
540
+ """
541
+ Aggregates the results from all QAOA subprograms to form a global solution.
487
542
 
488
- if self._executor is not None:
489
- self.wait_for_all()
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
- 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
- )
548
+ The final solution is stored in `self.solution` as a list of node indices assigned to the selected partition.
495
549
 
496
- if any(len(program.probs) == 0 for program in self.programs.values()):
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 `compute_final_solutions()` first."
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 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
-
563
+ for prog_id, program in self.programs.items():
525
564
  self._bitstring_solution = self.aggregate_fn(
526
565
  self._bitstring_solution,
527
- max_prob_key,
528
- program.problem,
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] = string.ascii_uppercase[partition_id]
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
- if self._solution_nodes is None:
617
- self.aggregate_results()
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,220 @@
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 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, QUBOProblemTypes
18
+ from divi.qprog.batch import ProgramBatch
19
+ from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer, copy_optimizer
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
+ class QUBOPartitioningQAOA(ProgramBatch):
53
+ def __init__(
54
+ self,
55
+ qubo: QUBOProblemTypes,
56
+ decomposer: hybrid.traits.ProblemDecomposer,
57
+ n_layers: int,
58
+ backend: CircuitRunner,
59
+ composer: hybrid.traits.SubsamplesComposer = hybrid.SplatComposer(),
60
+ optimizer: Optimizer | None = None,
61
+ max_iterations: int = 10,
62
+ **kwargs,
63
+ ):
64
+ """
65
+ Initialize a QUBOPartitioningQAOA instance for solving QUBO problems using partitioning and QAOA.
66
+
67
+ Args:
68
+ qubo (QUBOProblemTypes): The QUBO problem to solve, provided as a supported type.
69
+ Note: Variable types are assumed to be binary (not Spin).
70
+ decomposer (hybrid.traits.ProblemDecomposer): The decomposer used to partition the QUBO problem into subproblems.
71
+ n_layers (int): Number of QAOA layers to use for each subproblem.
72
+ backend (CircuitRunner): Backend responsible for running quantum circuits.
73
+ composer (hybrid.traits.SubsamplesComposer, optional): Composer to aggregate subsamples from subproblems.
74
+ Defaults to hybrid.SplatComposer().
75
+ optimizer (Optimizer, optional): Optimizer to use for QAOA.
76
+ Defaults to Optimizer.MONTE_CARLO.
77
+ max_iterations (int, optional): Maximum number of optimization iterations.
78
+ Defaults to 10.
79
+ **kwargs: Additional keyword arguments passed to the QAOA constructor.
80
+
81
+ """
82
+ super().__init__(backend=backend)
83
+
84
+ self.main_qubo, self._bqm = _sanitize_problem_input(qubo)
85
+
86
+ self._partitioning = hybrid.Unwind(decomposer)
87
+ self._aggregating = hybrid.Reduce(hybrid.Lambda(_merge_substates)) | composer
88
+
89
+ self.max_iterations = max_iterations
90
+
91
+ self.trivial_program_ids = set()
92
+
93
+ # Store the optimizer template (will be copied for each program)
94
+ self._optimizer_template = (
95
+ optimizer if optimizer is not None else MonteCarloOptimizer()
96
+ )
97
+
98
+ self._constructor = partial(
99
+ QAOA,
100
+ max_iterations=self.max_iterations,
101
+ backend=self.backend,
102
+ n_layers=n_layers,
103
+ **kwargs,
104
+ )
105
+ pass
106
+
107
+ def create_programs(self):
108
+ """
109
+ Partition the main QUBO problem and instantiate QAOA programs for each subproblem.
110
+
111
+ This implementation:
112
+ - Uses the configured decomposer to split the main QUBO into subproblems.
113
+ - For each subproblem, creates a QAOA program with the specified parameters.
114
+ - Stores each program in `self.programs` with a unique identifier.
115
+
116
+ Unique Identifier Format:
117
+ Each key in `self.programs` is a tuple of the form (letter, size), where:
118
+ - letter: An uppercase letter ('A', 'B', 'C', ...) indicating the partition index.
119
+ - size: The number of variables in the subproblem.
120
+
121
+ Example: ('A', 5) refers to the first partition with 5 variables.
122
+ """
123
+
124
+ super().create_programs()
125
+
126
+ self.prog_id_to_bqm_subproblem_states = {}
127
+
128
+ init_state = hybrid.State.from_problem(self._bqm)
129
+ _bqm_partitions = self._partitioning.run(init_state).result()
130
+
131
+ for i, partition in enumerate(_bqm_partitions):
132
+ if i > 0:
133
+ # We only need 'problem' on the first partition since
134
+ # it will propagate to the other partitions during
135
+ # aggregation, otherwise it's a waste of memory
136
+ del partition["problem"]
137
+
138
+ prog_id = (string.ascii_uppercase[i], len(partition.subproblem))
139
+ self.prog_id_to_bqm_subproblem_states[prog_id] = partition
140
+
141
+ if partition.subproblem.num_interactions == 0:
142
+ # Skip creating a full QAOA program for this trivial case.
143
+ self.trivial_program_ids.add(prog_id)
144
+ continue
145
+
146
+ ldata, (irow, icol, qdata), _ = partition.subproblem.to_numpy_vectors(
147
+ partition.subproblem.variables
148
+ )
149
+
150
+ coo_mat = sps.coo_matrix(
151
+ (
152
+ np.r_[ldata, qdata],
153
+ (
154
+ np.r_[np.arange(len(ldata)), icol],
155
+ np.r_[np.arange(len(ldata)), irow],
156
+ ),
157
+ ),
158
+ shape=(len(ldata), len(ldata)),
159
+ )
160
+
161
+ self._programs[prog_id] = self._constructor(
162
+ program_id=prog_id,
163
+ problem=coo_mat,
164
+ optimizer=copy_optimizer(self._optimizer_template),
165
+ progress_queue=self._queue,
166
+ )
167
+
168
+ def aggregate_results(self) -> tuple[npt.NDArray[np.int32], float]:
169
+ """
170
+ Aggregate results from all QUBO subproblems into a global solution.
171
+
172
+ Collects solutions from each partitioned subproblem (both QAOA-optimized and
173
+ trivial ones) and uses the hybrid framework composer to combine them into
174
+ a final solution for the original QUBO problem.
175
+
176
+ Returns:
177
+ tuple: A tuple containing:
178
+ - solution (npt.NDArray[np.int32]): Binary solution vector for the QUBO problem.
179
+ - solution_energy (float): Energy/cost of the solution.
180
+
181
+ Raises:
182
+ RuntimeError: If programs haven't been run or if final probabilities
183
+ haven't been computed.
184
+ """
185
+ super().aggregate_results()
186
+
187
+ if any(len(program.best_probs) == 0 for program in self.programs.values()):
188
+ raise RuntimeError(
189
+ "Not all final probabilities computed yet. Please call `run()` first."
190
+ )
191
+
192
+ for (
193
+ prog_id,
194
+ bqm_subproblem_state,
195
+ ) in self.prog_id_to_bqm_subproblem_states.items():
196
+
197
+ if prog_id in self.trivial_program_ids:
198
+ # Case 1: Trivial problem. Solve classically.
199
+ # The solution is any bitstring (e.g., all zeros) with energy 0.
200
+ var_to_val = {v: 0 for v in bqm_subproblem_state.subproblem.variables}
201
+ else:
202
+ subproblem = self._programs[prog_id]
203
+ var_to_val = dict(
204
+ zip(bqm_subproblem_state.subproblem.variables, subproblem.solution)
205
+ )
206
+
207
+ sample_set = dimod.SampleSet.from_samples(
208
+ dimod.as_samples(var_to_val), "BINARY", 0
209
+ )
210
+
211
+ self.prog_id_to_bqm_subproblem_states[prog_id] = (
212
+ bqm_subproblem_state.updated(subsamples=sample_set)
213
+ )
214
+
215
+ states = hybrid.States(*list(self.prog_id_to_bqm_subproblem_states.values()))
216
+ final_state = self._aggregating.run(states).result()
217
+
218
+ self.solution, self.solution_energy, _ = final_state.samples.record[0]
219
+
220
+ return self.solution, self.solution_energy