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,572 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ import logging
6
+ from enum import Enum
7
+ from typing import Any, Literal, get_args
8
+ from warnings import warn
9
+
10
+ import dimod
11
+ import matplotlib.pyplot as plt
12
+ import networkx as nx
13
+ import numpy as np
14
+ import pennylane as qml
15
+ import pennylane.qaoa as pqaoa
16
+ import rustworkx as rx
17
+ import scipy.sparse as sps
18
+ import sympy as sp
19
+
20
+ from divi.circuits import CircuitBundle, MetaCircuit
21
+ from divi.qprog._hamiltonians import (
22
+ _clean_hamiltonian,
23
+ convert_qubo_matrix_to_pennylane_ising,
24
+ )
25
+ from divi.qprog.variational_quantum_algorithm import VariationalQuantumAlgorithm
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+ GraphProblemTypes = nx.Graph | rx.PyGraph
30
+ QUBOProblemTypes = list | np.ndarray | sps.spmatrix | dimod.BinaryQuadraticModel
31
+
32
+
33
+ def _extract_loss_constant(
34
+ problem_metadata: dict, constant_from_hamiltonian: float
35
+ ) -> float:
36
+ """Extract and combine loss constants from problem metadata and hamiltonian.
37
+
38
+ Args:
39
+ problem_metadata: Metadata dictionary that may contain a "constant" key.
40
+ constant_from_hamiltonian: Constant extracted from the hamiltonian.
41
+
42
+ Returns:
43
+ Combined loss constant.
44
+ """
45
+ pre_calculated_constant = 0.0
46
+ if "constant" in problem_metadata:
47
+ pre_calculated_constant = problem_metadata.get("constant")
48
+ try:
49
+ pre_calculated_constant = pre_calculated_constant.item()
50
+ except (AttributeError, TypeError):
51
+ # If .item() doesn't exist or fails, ensure it's a float
52
+ pre_calculated_constant = float(pre_calculated_constant)
53
+
54
+ return pre_calculated_constant + constant_from_hamiltonian
55
+
56
+
57
+ def draw_graph_solution_nodes(main_graph: nx.Graph, partition_nodes):
58
+ """Visualize a graph with solution nodes highlighted.
59
+
60
+ Draws the graph with nodes colored to distinguish solution nodes (red) from
61
+ other nodes (light blue).
62
+
63
+ Args:
64
+ main_graph (nx.Graph): NetworkX graph to visualize.
65
+ partition_nodes: Collection of node indices that are part of the solution.
66
+ """
67
+ # Create a dictionary for node colors
68
+ node_colors = [
69
+ "red" if node in partition_nodes else "lightblue" for node in main_graph.nodes()
70
+ ]
71
+
72
+ plt.figure(figsize=(10, 8))
73
+
74
+ pos = nx.spring_layout(main_graph)
75
+
76
+ nx.draw_networkx_nodes(main_graph, pos, node_color=node_colors, node_size=500)
77
+ nx.draw_networkx_edges(main_graph, pos)
78
+ nx.draw_networkx_labels(main_graph, pos, font_size=10, font_weight="bold")
79
+
80
+ # Remove axes
81
+ plt.axis("off")
82
+
83
+ # Show the plot
84
+ plt.tight_layout()
85
+ plt.show()
86
+
87
+
88
+ class GraphProblem(Enum):
89
+ """Enumeration of supported graph problems for QAOA.
90
+
91
+ Each problem type defines:
92
+ - pl_string: The corresponding PennyLane function name
93
+ - constrained_initial_state: Recommended initial state for constrained problems
94
+ - unconstrained_initial_state: Recommended initial state for unconstrained problems
95
+ """
96
+
97
+ MAX_CLIQUE = ("max_clique", "Zeros", "Superposition")
98
+ MAX_INDEPENDENT_SET = ("max_independent_set", "Zeros", "Superposition")
99
+ MAX_WEIGHT_CYCLE = ("max_weight_cycle", "Superposition", "Superposition")
100
+ MAXCUT = ("maxcut", "Superposition", "Superposition")
101
+ MIN_VERTEX_COVER = ("min_vertex_cover", "Ones", "Superposition")
102
+
103
+ # This is an internal problem with no pennylane equivalent
104
+ EDGE_PARTITIONING = ("", "", "")
105
+
106
+ def __init__(
107
+ self,
108
+ pl_string: str,
109
+ constrained_initial_state: str,
110
+ unconstrained_initial_state: str,
111
+ ):
112
+ """Initialize the GraphProblem enum value.
113
+
114
+ Args:
115
+ pl_string (str): The corresponding PennyLane function name.
116
+ constrained_initial_state (str): Recommended initial state for constrained problems.
117
+ unconstrained_initial_state (str): Recommended initial state for unconstrained problems.
118
+ """
119
+ self.pl_string = pl_string
120
+
121
+ # Recommended initial state as per Pennylane's documentation.
122
+ # Value is duplicated if not applicable to the problem
123
+ self.constrained_initial_state = constrained_initial_state
124
+ self.unconstrained_initial_state = unconstrained_initial_state
125
+
126
+
127
+ _SUPPORTED_INITIAL_STATES_LITERAL = Literal[
128
+ "Zeros", "Ones", "Superposition", "Recommended"
129
+ ]
130
+
131
+
132
+ def _resolve_circuit_layers(
133
+ initial_state, problem, graph_problem, **kwargs
134
+ ) -> tuple[qml.operation.Operator, qml.operation.Operator, dict | None, str]:
135
+ """Generate the cost and mixer Hamiltonians for a given problem.
136
+
137
+ Args:
138
+ initial_state (str): The initial state specification.
139
+ problem (GraphProblemTypes | QUBOProblemTypes): The problem to solve (graph or QUBO).
140
+ graph_problem (GraphProblem | None): The graph problem type (if applicable).
141
+ **kwargs: Additional keyword arguments.
142
+
143
+ Returns:
144
+ tuple[qml.operation.Operator, qml.operation.Operator, dict | None, str]: (cost_hamiltonian, mixer_hamiltonian, metadata, resolved_initial_state)
145
+ """
146
+
147
+ if isinstance(problem, GraphProblemTypes):
148
+ is_constrained = kwargs.pop("is_constrained", True)
149
+
150
+ if graph_problem == GraphProblem.MAXCUT:
151
+ params = (problem,)
152
+ else:
153
+ params = (problem, is_constrained)
154
+
155
+ if initial_state == "Recommended":
156
+ resolved_initial_state = (
157
+ graph_problem.constrained_initial_state
158
+ if is_constrained
159
+ else graph_problem.constrained_initial_state
160
+ )
161
+ else:
162
+ resolved_initial_state = initial_state
163
+
164
+ return *getattr(pqaoa, graph_problem.pl_string)(*params), resolved_initial_state
165
+ else:
166
+ # Convert BinaryQuadraticModel to matrix if needed
167
+ if isinstance(problem, dimod.BinaryQuadraticModel):
168
+ # Manual conversion from BQM to matrix (replacing deprecated to_numpy_matrix)
169
+ variables = list(problem.variables)
170
+ var_to_idx = {v: i for i, v in enumerate(variables)}
171
+ qubo_matrix = np.diag([problem.linear.get(v, 0) for v in variables])
172
+ for (u, v), coeff in problem.quadratic.items():
173
+ i, j = var_to_idx[u], var_to_idx[v]
174
+ qubo_matrix[i, j] = qubo_matrix[j, i] = coeff
175
+ else:
176
+ qubo_matrix = problem
177
+
178
+ cost_hamiltonian, constant = convert_qubo_matrix_to_pennylane_ising(qubo_matrix)
179
+
180
+ n_qubits = qubo_matrix.shape[0]
181
+
182
+ return (
183
+ cost_hamiltonian,
184
+ pqaoa.x_mixer(range(n_qubits)),
185
+ {"constant": constant},
186
+ "Superposition",
187
+ )
188
+
189
+
190
+ class QAOA(VariationalQuantumAlgorithm):
191
+ """Quantum Approximate Optimization Algorithm (QAOA) implementation.
192
+
193
+ QAOA is a hybrid quantum-classical algorithm designed to solve combinatorial
194
+ optimization problems. It alternates between applying a cost Hamiltonian
195
+ (encoding the problem) and a mixer Hamiltonian (enabling exploration).
196
+
197
+ The algorithm can solve:
198
+ - Graph problems (MaxCut, Max Clique, etc.)
199
+ - QUBO (Quadratic Unconstrained Binary Optimization) problems
200
+ - BinaryQuadraticModel from dimod
201
+
202
+ Attributes:
203
+ problem (GraphProblemTypes | QUBOProblemTypes): The problem instance to solve.
204
+ graph_problem (GraphProblem | None): The graph problem type (if applicable).
205
+ n_layers (int): Number of QAOA layers.
206
+ n_qubits (int): Number of qubits required.
207
+ cost_hamiltonian (qml.Hamiltonian): The cost Hamiltonian encoding the problem.
208
+ mixer_hamiltonian (qml.Hamiltonian): The mixer Hamiltonian for exploration.
209
+ initial_state (str): The initial quantum state.
210
+ problem_metadata (dict | None): Additional metadata from problem setup.
211
+ loss_constant (float): Constant term from the problem.
212
+ optimizer (Optimizer): Classical optimizer for parameter updates.
213
+ max_iterations (int): Maximum number of optimization iterations.
214
+ current_iteration (int): Current optimization iteration.
215
+ _n_params (int): Number of parameters per layer (always 2 for QAOA).
216
+ _solution_nodes (list[int] | None): Solution nodes for graph problems.
217
+ _solution_bitstring (npt.NDArray[np.int32] | None): Solution bitstring for QUBO problems.
218
+ """
219
+
220
+ def __init__(
221
+ self,
222
+ problem: GraphProblemTypes | QUBOProblemTypes,
223
+ *,
224
+ graph_problem: GraphProblem | None = None,
225
+ n_layers: int = 1,
226
+ initial_state: _SUPPORTED_INITIAL_STATES_LITERAL = "Recommended",
227
+ max_iterations: int = 10,
228
+ **kwargs,
229
+ ):
230
+ """Initialize the QAOA problem.
231
+
232
+ Args:
233
+ problem (GraphProblemTypes | QUBOProblemTypes): The problem to solve, can either be a graph or a QUBO.
234
+ For graph inputs, the graph problem to solve must be provided
235
+ through the `graph_problem` variable.
236
+ graph_problem (GraphProblem | None): The graph problem to solve. Defaults to None.
237
+ n_layers (int): Number of QAOA layers. Defaults to 1.
238
+ initial_state (_SUPPORTED_INITIAL_STATES_LITERAL): The initial state of the circuit. Defaults to "Recommended".
239
+ max_iterations (int): Maximum number of optimization iterations. Defaults to 10.
240
+ **kwargs: Additional keyword arguments passed to the parent class, including `optimizer`.
241
+ """
242
+ super().__init__(**kwargs)
243
+
244
+ self.graph_problem = graph_problem
245
+
246
+ # Validate and process problem
247
+ self.problem = self._validate_and_set_problem(problem, graph_problem)
248
+
249
+ # Validate initial state
250
+ if initial_state not in get_args(_SUPPORTED_INITIAL_STATES_LITERAL):
251
+ raise ValueError(
252
+ f"Unsupported Initial State. Got {initial_state}. Must be one of: {get_args(_SUPPORTED_INITIAL_STATES_LITERAL)}"
253
+ )
254
+
255
+ # Initialize local state
256
+ self.n_layers = n_layers
257
+ self.max_iterations = max_iterations
258
+ self.current_iteration = 0
259
+ self._n_params = 2
260
+
261
+ self._solution_nodes = []
262
+ self._solution_bitstring = []
263
+
264
+ # Resolve hamiltonians and problem metadata
265
+ (
266
+ cost_hamiltonian,
267
+ self._mixer_hamiltonian,
268
+ *problem_metadata,
269
+ self.initial_state,
270
+ ) = _resolve_circuit_layers(
271
+ initial_state=initial_state,
272
+ problem=self.problem,
273
+ graph_problem=self.graph_problem,
274
+ **kwargs,
275
+ )
276
+ self.problem_metadata = problem_metadata[0] if problem_metadata else {}
277
+
278
+ # Extract and combine constants
279
+ self._cost_hamiltonian, constant_from_hamiltonian = _clean_hamiltonian(
280
+ cost_hamiltonian
281
+ )
282
+ self.loss_constant = _extract_loss_constant(
283
+ self.problem_metadata, constant_from_hamiltonian
284
+ )
285
+
286
+ # Extract wire labels from the cost Hamiltonian to ensure consistency
287
+ self._circuit_wires = tuple(self._cost_hamiltonian.wires)
288
+
289
+ def _save_subclass_state(self) -> dict[str, Any]:
290
+ """Save QAOA-specific runtime state."""
291
+ return {
292
+ "problem_metadata": self.problem_metadata,
293
+ "solution_nodes": self._solution_nodes,
294
+ "solution_bitstring": self._solution_bitstring,
295
+ "loss_constant": self.loss_constant,
296
+ }
297
+
298
+ def _load_subclass_state(self, state: dict[str, Any]) -> None:
299
+ """Load QAOA-specific state.
300
+
301
+ Raises:
302
+ KeyError: If any required state key is missing (indicates checkpoint corruption).
303
+ """
304
+ required_keys = [
305
+ "problem_metadata",
306
+ "solution_nodes", # Key must exist, but value can be None if final computation hasn't run
307
+ "solution_bitstring", # Key must exist, but value can be None if final computation hasn't run
308
+ "loss_constant",
309
+ ]
310
+ missing_keys = [key for key in required_keys if key not in state]
311
+ if missing_keys:
312
+ raise KeyError(
313
+ f"Corrupted checkpoint: missing required state keys: {missing_keys}"
314
+ )
315
+
316
+ self.problem_metadata = state["problem_metadata"]
317
+ # solution_nodes and solution_bitstring can be None if final computation hasn't run
318
+ # Convert None to empty list to match initialization behavior
319
+ self._solution_nodes = (
320
+ state["solution_nodes"] if state["solution_nodes"] is not None else []
321
+ )
322
+ self._solution_bitstring = (
323
+ state["solution_bitstring"]
324
+ if state["solution_bitstring"] is not None
325
+ else []
326
+ )
327
+ self.loss_constant = state["loss_constant"]
328
+
329
+ def _validate_and_set_problem(
330
+ self,
331
+ problem: GraphProblemTypes | QUBOProblemTypes,
332
+ graph_problem: GraphProblem | None,
333
+ ) -> GraphProblemTypes | QUBOProblemTypes:
334
+ """Validate and process the problem input, setting n_qubits and graph_problem.
335
+
336
+ Args:
337
+ problem: The problem to solve (graph or QUBO).
338
+ graph_problem: The graph problem type (if applicable).
339
+
340
+ Returns:
341
+ The processed problem instance.
342
+
343
+ Raises:
344
+ ValueError: If problem type or graph_problem is invalid.
345
+ """
346
+ if isinstance(problem, QUBOProblemTypes):
347
+ if graph_problem is not None:
348
+ warn("Ignoring the 'problem' argument as it is not applicable to QUBO.")
349
+
350
+ self.graph_problem = None
351
+ return self._process_qubo_problem(problem)
352
+ else:
353
+ return self._process_graph_problem(problem, graph_problem)
354
+
355
+ def _process_qubo_problem(self, problem: QUBOProblemTypes) -> QUBOProblemTypes:
356
+ """Process QUBO problem, converting if necessary and setting n_qubits.
357
+
358
+ Args:
359
+ problem: QUBO problem (BinaryQuadraticModel, list, array, or sparse matrix).
360
+
361
+ Returns:
362
+ Processed QUBO problem.
363
+
364
+ Raises:
365
+ ValueError: If QUBO matrix has invalid shape or BinaryQuadraticModel has non-binary variables.
366
+ """
367
+ # Handle BinaryQuadraticModel
368
+ if isinstance(problem, dimod.BinaryQuadraticModel):
369
+ if problem.vartype != dimod.Vartype.BINARY:
370
+ raise ValueError(
371
+ f"BinaryQuadraticModel must have vartype='BINARY', got {problem.vartype}"
372
+ )
373
+ self.n_qubits = len(problem.variables)
374
+ return problem
375
+
376
+ # Handle list input
377
+ if isinstance(problem, list):
378
+ problem = np.array(problem)
379
+
380
+ # Validate matrix shape
381
+ if problem.ndim != 2 or problem.shape[0] != problem.shape[1]:
382
+ raise ValueError(
383
+ "Invalid QUBO matrix."
384
+ f" Got array of shape {problem.shape}."
385
+ " Must be a square matrix."
386
+ )
387
+
388
+ self.n_qubits = problem.shape[1]
389
+
390
+ return problem
391
+
392
+ def _process_graph_problem(
393
+ self,
394
+ problem: GraphProblemTypes,
395
+ graph_problem: GraphProblem | None,
396
+ ) -> GraphProblemTypes:
397
+ """Process graph problem, validating graph_problem and setting n_qubits.
398
+
399
+ Args:
400
+ problem: Graph problem (NetworkX or RustworkX graph).
401
+ graph_problem: The graph problem type.
402
+
403
+ Returns:
404
+ The graph problem instance.
405
+
406
+ Raises:
407
+ ValueError: If graph_problem is not a valid GraphProblem enum.
408
+ """
409
+ if not isinstance(graph_problem, GraphProblem):
410
+ raise ValueError(
411
+ f"Unsupported Problem. Got '{graph_problem}'. Must be one of type divi.qprog.GraphProblem."
412
+ )
413
+
414
+ self.graph_problem = graph_problem
415
+ self.n_qubits = problem.number_of_nodes()
416
+ return problem
417
+
418
+ @property
419
+ def cost_hamiltonian(self) -> qml.operation.Operator:
420
+ """The cost Hamiltonian for the QAOA problem."""
421
+ return self._cost_hamiltonian
422
+
423
+ @property
424
+ def mixer_hamiltonian(self) -> qml.operation.Operator:
425
+ """The mixer Hamiltonian for the QAOA problem."""
426
+ return self._mixer_hamiltonian
427
+
428
+ @property
429
+ def solution(self):
430
+ """Get the solution found by QAOA optimization.
431
+
432
+ Returns:
433
+ list[int] | npt.NDArray[np.int32]: For graph problems, returns a list of selected node indices.
434
+ For QUBO problems, returns a list/array of binary values.
435
+ """
436
+ return (
437
+ self._solution_nodes
438
+ if self.graph_problem is not None
439
+ else self._solution_bitstring
440
+ )
441
+
442
+ def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
443
+ """Generate the meta circuits for the QAOA problem.
444
+
445
+ Creates both cost and measurement circuits for the QAOA algorithm.
446
+ The cost circuit is used during optimization, while the measurement
447
+ circuit is used for final solution extraction.
448
+
449
+ Returns:
450
+ dict[str, MetaCircuit]: Dictionary containing cost_circuit and meas_circuit.
451
+ """
452
+
453
+ betas = sp.symarray("β", self.n_layers)
454
+ gammas = sp.symarray("γ", self.n_layers)
455
+
456
+ sym_params = np.vstack((betas, gammas)).transpose()
457
+
458
+ ops = []
459
+ if self.initial_state == "Ones":
460
+ for wire in self._circuit_wires:
461
+ ops.append(qml.PauliX(wires=wire))
462
+ elif self.initial_state == "Superposition":
463
+ for wire in self._circuit_wires:
464
+ ops.append(qml.Hadamard(wires=wire))
465
+ for layer_params in sym_params:
466
+ gamma, beta = layer_params
467
+ ops.append(pqaoa.cost_layer(gamma, self._cost_hamiltonian))
468
+ ops.append(pqaoa.mixer_layer(beta, self._mixer_hamiltonian))
469
+
470
+ return {
471
+ "cost_circuit": self._meta_circuit_factory(
472
+ qml.tape.QuantumScript(
473
+ ops=ops, measurements=[qml.expval(self._cost_hamiltonian)]
474
+ ),
475
+ symbols=sym_params.flatten(),
476
+ ),
477
+ "meas_circuit": self._meta_circuit_factory(
478
+ qml.tape.QuantumScript(ops=ops, measurements=[qml.probs()]),
479
+ symbols=sym_params.flatten(),
480
+ grouping_strategy="wires",
481
+ ),
482
+ }
483
+
484
+ def _generate_circuits(self) -> list[CircuitBundle]:
485
+ """Generate the circuits for the QAOA problem.
486
+
487
+ Generates circuits for each parameter set in the current parameters.
488
+ The circuit type depends on whether we're computing probabilities
489
+ (for final solution extraction) or just expectation values (for optimization).
490
+
491
+ Returns:
492
+ list[CircuitBundle]: List of CircuitBundle objects for execution.
493
+ """
494
+ circuit_type = (
495
+ "cost_circuit" if not self._is_compute_probabilities else "meas_circuit"
496
+ )
497
+
498
+ return [
499
+ self.meta_circuits[circuit_type].initialize_circuit_from_params(
500
+ params_group, tag_prefix=f"{p}"
501
+ )
502
+ for p, params_group in enumerate(self._curr_params)
503
+ ]
504
+
505
+ def _perform_final_computation(self, **kwargs):
506
+ """Extract the optimal solution from the QAOA optimization process.
507
+
508
+ This method performs the following steps:
509
+ 1. Executes measurement circuits with the best parameters (those that achieved the lowest loss).
510
+ 2. Retrieves the bitstring representing the best solution, correcting for endianness.
511
+ 3. Depending on the problem type:
512
+ - For QUBO problems, stores the solution as a NumPy array of bits.
513
+ - For graph problems, stores the solution as a list of node indices corresponding to '1's in the bitstring.
514
+
515
+ Returns:
516
+ tuple[int, float]: A tuple containing:
517
+ - int: The total number of circuits executed.
518
+ - float: The total runtime of the optimization process.
519
+ """
520
+
521
+ self.reporter.info(message="🏁 Computing Final Solution 🏁", overwrite=True)
522
+
523
+ self._run_solution_measurement()
524
+
525
+ best_measurement_probs = next(iter(self._best_probs.values()))
526
+
527
+ # Endianness is corrected in _post_process_results
528
+ best_solution_bitstring = max(
529
+ best_measurement_probs, key=best_measurement_probs.get
530
+ )
531
+
532
+ if isinstance(self.problem, QUBOProblemTypes):
533
+ self._solution_bitstring[:] = np.fromiter(
534
+ best_solution_bitstring, dtype=np.int32
535
+ )
536
+
537
+ if isinstance(self.problem, GraphProblemTypes):
538
+ # Map bitstring positions to actual graph node labels
539
+ # Bitstring is already endianness-corrected, so positions map directly to circuit_wires
540
+ self._solution_nodes[:] = [
541
+ self._circuit_wires[idx]
542
+ for idx, bit in enumerate(best_solution_bitstring)
543
+ if bit == "1" and idx < len(self._circuit_wires)
544
+ ]
545
+
546
+ self.reporter.info(message="🏁 Computed Final Solution! 🏁")
547
+
548
+ return self._total_circuit_count, self._total_run_time
549
+
550
+ def draw_solution(self):
551
+ """Visualize the solution found by QAOA for graph problems.
552
+
553
+ Draws the graph with solution nodes highlighted in red and other nodes
554
+ in light blue. If the solution hasn't been computed yet, it will be
555
+ calculated first.
556
+
557
+ Raises:
558
+ RuntimeError: If called on a QUBO problem instead of a graph problem.
559
+
560
+ Note:
561
+ This method only works for graph problems. For QUBO problems, access
562
+ the solution directly via the `solution` property.
563
+ """
564
+ if self.graph_problem is None:
565
+ raise RuntimeError(
566
+ "The problem is not a graph problem. Cannot draw solution."
567
+ )
568
+
569
+ if not self._solution_nodes:
570
+ self._perform_final_computation()
571
+
572
+ draw_graph_solution_nodes(self.problem, self._solution_nodes)