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