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.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +9 -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 +8 -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 +369 -0
- divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
- divi/circuits/_qasm_validation.py +694 -0
- divi/qprog/__init__.py +24 -6
- divi/qprog/_expectation.py +181 -0
- divi/qprog/_hamiltonians.py +281 -0
- divi/qprog/algorithms/__init__.py +14 -0
- divi/qprog/algorithms/_ansatze.py +356 -0
- divi/qprog/algorithms/_qaoa.py +572 -0
- divi/qprog/algorithms/_vqe.py +249 -0
- divi/qprog/batch.py +383 -73
- divi/qprog/checkpointing.py +556 -0
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +1014 -43
- divi/qprog/quantum_program.py +231 -413
- divi/qprog/variational_quantum_algorithm.py +995 -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 +220 -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 +133 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info}/METADATA +43 -15
- qoro_divi-0.5.0.dist-info/RECORD +43 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info}/WHEEL +1 -1
- qoro_divi-0.5.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.5.0.dist-info/licenses}/LICENSE +0 -0
- {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)
|