qoro-divi 0.3.5__py3-none-any.whl → 0.4.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.
Potentially problematic release.
This version of qoro-divi might be problematic. Click here for more details.
- divi/backends/__init__.py +1 -1
- divi/backends/_circuit_runner.py +21 -0
- divi/backends/_parallel_simulator.py +14 -0
- divi/backends/_qoro_service.py +232 -70
- divi/backends/_qpu_system.py +77 -3
- divi/circuits/_core.py +24 -5
- divi/circuits/qasm.py +1 -3
- divi/extern/cirq/_validator.py +12 -3
- divi/qprog/__init__.py +1 -0
- divi/qprog/algorithms/_ansatze.py +20 -16
- divi/qprog/algorithms/_qaoa.py +152 -111
- divi/qprog/algorithms/_vqe.py +170 -79
- divi/qprog/batch.py +34 -1
- divi/qprog/optimizers.py +133 -50
- divi/qprog/quantum_program.py +131 -633
- divi/qprog/variational_quantum_algorithm.py +786 -0
- divi/qprog/workflows/_graph_partitioning.py +42 -6
- divi/qprog/workflows/_qubo_partitioning.py +1 -1
- divi/qprog/workflows/_vqe_sweep.py +40 -33
- divi/reporting/_reporter.py +3 -6
- divi/utils.py +65 -0
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/METADATA +15 -1
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/RECORD +27 -26
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {qoro_divi-0.3.5.dist-info → qoro_divi-0.4.0.dist-info}/WHEEL +0 -0
divi/extern/cirq/_validator.py
CHANGED
|
@@ -639,9 +639,18 @@ def validate_qasm_raise(src: str) -> None:
|
|
|
639
639
|
Parser(toks).parse()
|
|
640
640
|
|
|
641
641
|
|
|
642
|
-
def
|
|
642
|
+
def validate_qasm_count_qubits(src: str) -> int:
|
|
643
|
+
"""Validate QASM and return the total number of qubits."""
|
|
644
|
+
toks = _lex(src)
|
|
645
|
+
parser = Parser(toks)
|
|
646
|
+
parser.parse()
|
|
647
|
+
# Sum all qubit register sizes to get total qubit count
|
|
648
|
+
return sum(parser.qregs.values())
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def is_valid_qasm(src: str) -> int | str:
|
|
652
|
+
"""Validate QASM and return the number of qubits if valid, or error message if invalid."""
|
|
643
653
|
try:
|
|
644
|
-
|
|
645
|
-
return True
|
|
654
|
+
return validate_qasm_count_qubits(src)
|
|
646
655
|
except SyntaxError as e:
|
|
647
656
|
return str(e)
|
divi/qprog/__init__.py
CHANGED
|
@@ -11,7 +11,7 @@ import pennylane as qml
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Ansatz(ABC):
|
|
14
|
-
"""Abstract base class for all VQE
|
|
14
|
+
"""Abstract base class for all VQE ansätze."""
|
|
15
15
|
|
|
16
16
|
@property
|
|
17
17
|
def name(self) -> str:
|
|
@@ -25,15 +25,17 @@ class Ansatz(ABC):
|
|
|
25
25
|
raise NotImplementedError
|
|
26
26
|
|
|
27
27
|
@abstractmethod
|
|
28
|
-
def build(self, params, n_qubits: int, n_layers: int, **kwargs):
|
|
28
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
|
|
29
29
|
"""
|
|
30
|
-
Builds the ansatz circuit
|
|
30
|
+
Builds the ansatz circuit by adding operations to the active PennyLane
|
|
31
|
+
quantum function context.
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
Note: This method is called within a PennyLane quantum function context
|
|
34
|
+
(qml.qnode or qml.tape.make_qscript). Operations are added via side effects
|
|
35
|
+
to the active quantum tape/script.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
None: Operations are added to the active PennyLane context
|
|
37
39
|
"""
|
|
38
40
|
raise NotImplementedError
|
|
39
41
|
|
|
@@ -118,7 +120,7 @@ class GenericLayerAnsatz(Ansatz):
|
|
|
118
120
|
per_qubit = sum(getattr(g, "num_params", 1) for g in self.gate_sequence)
|
|
119
121
|
return per_qubit * n_qubits
|
|
120
122
|
|
|
121
|
-
def build(self, params, n_qubits: int, n_layers: int, **kwargs):
|
|
123
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
|
|
122
124
|
# calculate how many params each gate needs per qubit
|
|
123
125
|
gate_param_counts = [getattr(g, "num_params", 1) for g in self.gate_sequence]
|
|
124
126
|
per_qubit = sum(gate_param_counts)
|
|
@@ -164,7 +166,7 @@ class QAOAAnsatz(Ansatz):
|
|
|
164
166
|
"""
|
|
165
167
|
return qml.QAOAEmbedding.shape(n_layers=1, n_wires=n_qubits)[1]
|
|
166
168
|
|
|
167
|
-
def build(self, params, n_qubits: int, n_layers: int, **kwargs):
|
|
169
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
|
|
168
170
|
"""
|
|
169
171
|
Build the QAOA ansatz circuit.
|
|
170
172
|
|
|
@@ -231,7 +233,7 @@ class UCCSDAnsatz(Ansatz):
|
|
|
231
233
|
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
232
234
|
return len(s_wires) + len(d_wires)
|
|
233
235
|
|
|
234
|
-
def build(self, params, n_qubits: int, n_layers: int,
|
|
236
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
|
|
235
237
|
"""
|
|
236
238
|
Build the UCCSD ansatz circuit.
|
|
237
239
|
|
|
@@ -239,9 +241,10 @@ class UCCSDAnsatz(Ansatz):
|
|
|
239
241
|
params: Parameter array for excitation amplitudes.
|
|
240
242
|
n_qubits (int): Number of qubits.
|
|
241
243
|
n_layers (int): Number of UCCSD layers (repeats).
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
**kwargs: Additional arguments:
|
|
245
|
+
n_electrons (int): Number of electrons in the system (required).
|
|
244
246
|
"""
|
|
247
|
+
n_electrons = kwargs.pop("n_electrons")
|
|
245
248
|
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
246
249
|
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
|
|
247
250
|
hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
|
|
@@ -281,7 +284,7 @@ class HartreeFockAnsatz(Ansatz):
|
|
|
281
284
|
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
282
285
|
return len(singles) + len(doubles)
|
|
283
286
|
|
|
284
|
-
def build(self, params, n_qubits: int, n_layers: int,
|
|
287
|
+
def build(self, params, n_qubits: int, n_layers: int, **kwargs) -> None:
|
|
285
288
|
"""
|
|
286
289
|
Build the Hartree-Fock ansatz circuit.
|
|
287
290
|
|
|
@@ -289,9 +292,10 @@ class HartreeFockAnsatz(Ansatz):
|
|
|
289
292
|
params: Parameter array for excitation amplitudes.
|
|
290
293
|
n_qubits (int): Number of qubits.
|
|
291
294
|
n_layers (int): Number of ansatz layers.
|
|
292
|
-
|
|
293
|
-
|
|
295
|
+
**kwargs: Additional arguments:
|
|
296
|
+
n_electrons (int): Number of electrons in the system (required).
|
|
294
297
|
"""
|
|
298
|
+
n_electrons = kwargs.pop("n_electrons")
|
|
295
299
|
singles, doubles = qml.qchem.excitations(n_electrons, n_qubits)
|
|
296
300
|
hf_state = qml.qchem.hf_state(n_electrons, n_qubits)
|
|
297
301
|
|
divi/qprog/algorithms/_qaoa.py
CHANGED
|
@@ -21,9 +21,9 @@ from qiskit_optimization import QuadraticProgram
|
|
|
21
21
|
from qiskit_optimization.converters import QuadraticProgramToQubo
|
|
22
22
|
from qiskit_optimization.problems import VarType
|
|
23
23
|
|
|
24
|
-
from divi.circuits import MetaCircuit
|
|
25
|
-
from divi.qprog import QuantumProgram
|
|
24
|
+
from divi.circuits import Circuit, MetaCircuit
|
|
26
25
|
from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer
|
|
26
|
+
from divi.qprog.variational_quantum_algorithm import VariationalQuantumAlgorithm
|
|
27
27
|
from divi.utils import convert_qubo_matrix_to_pennylane_ising
|
|
28
28
|
|
|
29
29
|
logger = logging.getLogger(__name__)
|
|
@@ -32,15 +32,14 @@ GraphProblemTypes = nx.Graph | rx.PyGraph
|
|
|
32
32
|
QUBOProblemTypes = list | np.ndarray | sps.spmatrix | QuadraticProgram
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def draw_graph_solution_nodes(main_graph, partition_nodes):
|
|
36
|
-
"""
|
|
37
|
-
Visualize a graph with solution nodes highlighted.
|
|
35
|
+
def draw_graph_solution_nodes(main_graph: nx.Graph, partition_nodes):
|
|
36
|
+
"""Visualize a graph with solution nodes highlighted.
|
|
38
37
|
|
|
39
38
|
Draws the graph with nodes colored to distinguish solution nodes (red) from
|
|
40
39
|
other nodes (light blue).
|
|
41
40
|
|
|
42
41
|
Args:
|
|
43
|
-
main_graph: NetworkX graph to visualize.
|
|
42
|
+
main_graph (nx.Graph): NetworkX graph to visualize.
|
|
44
43
|
partition_nodes: Collection of node indices that are part of the solution.
|
|
45
44
|
"""
|
|
46
45
|
# Create a dictionary for node colors
|
|
@@ -65,6 +64,14 @@ def draw_graph_solution_nodes(main_graph, partition_nodes):
|
|
|
65
64
|
|
|
66
65
|
|
|
67
66
|
class GraphProblem(Enum):
|
|
67
|
+
"""Enumeration of supported graph problems for QAOA.
|
|
68
|
+
|
|
69
|
+
Each problem type defines:
|
|
70
|
+
- pl_string: The corresponding PennyLane function name
|
|
71
|
+
- constrained_initial_state: Recommended initial state for constrained problems
|
|
72
|
+
- unconstrained_initial_state: Recommended initial state for unconstrained problems
|
|
73
|
+
"""
|
|
74
|
+
|
|
68
75
|
MAX_CLIQUE = ("max_clique", "Zeros", "Superposition")
|
|
69
76
|
MAX_INDEPENDENT_SET = ("max_independent_set", "Zeros", "Superposition")
|
|
70
77
|
MAX_WEIGHT_CYCLE = ("max_weight_cycle", "Superposition", "Superposition")
|
|
@@ -80,6 +87,13 @@ class GraphProblem(Enum):
|
|
|
80
87
|
constrained_initial_state: str,
|
|
81
88
|
unconstrained_initial_state: str,
|
|
82
89
|
):
|
|
90
|
+
"""Initialize the GraphProblem enum value.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
pl_string (str): The corresponding PennyLane function name.
|
|
94
|
+
constrained_initial_state (str): Recommended initial state for constrained problems.
|
|
95
|
+
unconstrained_initial_state (str): Recommended initial state for unconstrained problems.
|
|
96
|
+
"""
|
|
83
97
|
self.pl_string = pl_string
|
|
84
98
|
|
|
85
99
|
# Recommended initial state as per Pennylane's documentation.
|
|
@@ -94,6 +108,17 @@ _SUPPORTED_INITIAL_STATES_LITERAL = Literal[
|
|
|
94
108
|
|
|
95
109
|
|
|
96
110
|
def _convert_quadratic_program_to_pennylane_ising(qp: QuadraticProgram):
|
|
111
|
+
"""Convert a Qiskit QuadraticProgram to a PennyLane Ising Hamiltonian.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
qp (QuadraticProgram): Qiskit QuadraticProgram to convert.
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
tuple[qml.Hamiltonian, float, int]: (pennylane_ising, constant, n_qubits) where:
|
|
118
|
+
- pennylane_ising: The Ising Hamiltonian in PennyLane format
|
|
119
|
+
- constant: The constant term
|
|
120
|
+
- n_qubits: Number of qubits required
|
|
121
|
+
"""
|
|
97
122
|
qiskit_sparse_op, constant = qp.to_ising()
|
|
98
123
|
|
|
99
124
|
pauli_list = qiskit_sparse_op.paulis
|
|
@@ -118,9 +143,16 @@ def _convert_quadratic_program_to_pennylane_ising(qp: QuadraticProgram):
|
|
|
118
143
|
def _resolve_circuit_layers(
|
|
119
144
|
initial_state, problem, graph_problem, **kwargs
|
|
120
145
|
) -> tuple[qml.operation.Operator, qml.operation.Operator, dict | None, str]:
|
|
121
|
-
"""
|
|
122
|
-
|
|
123
|
-
|
|
146
|
+
"""Generate the cost and mixer Hamiltonians for a given problem.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
initial_state (str): The initial state specification.
|
|
150
|
+
problem (GraphProblemTypes | QUBOProblemTypes): The problem to solve (graph or QUBO).
|
|
151
|
+
graph_problem (GraphProblem | None): The graph problem type (if applicable).
|
|
152
|
+
**kwargs: Additional keyword arguments.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
tuple[qml.operation.Operator, qml.operation.Operator, dict | None, str]: (cost_hamiltonian, mixer_hamiltonian, metadata, resolved_initial_state)
|
|
124
156
|
"""
|
|
125
157
|
|
|
126
158
|
if isinstance(problem, GraphProblemTypes):
|
|
@@ -159,7 +191,36 @@ def _resolve_circuit_layers(
|
|
|
159
191
|
)
|
|
160
192
|
|
|
161
193
|
|
|
162
|
-
class QAOA(
|
|
194
|
+
class QAOA(VariationalQuantumAlgorithm):
|
|
195
|
+
"""Quantum Approximate Optimization Algorithm (QAOA) implementation.
|
|
196
|
+
|
|
197
|
+
QAOA is a hybrid quantum-classical algorithm designed to solve combinatorial
|
|
198
|
+
optimization problems. It alternates between applying a cost Hamiltonian
|
|
199
|
+
(encoding the problem) and a mixer Hamiltonian (enabling exploration).
|
|
200
|
+
|
|
201
|
+
The algorithm can solve:
|
|
202
|
+
- Graph problems (MaxCut, Max Clique, etc.)
|
|
203
|
+
- QUBO (Quadratic Unconstrained Binary Optimization) problems
|
|
204
|
+
- Quadratic programs (converted to QUBO)
|
|
205
|
+
|
|
206
|
+
Attributes:
|
|
207
|
+
problem (GraphProblemTypes | QUBOProblemTypes): The problem instance to solve.
|
|
208
|
+
graph_problem (GraphProblem | None): The graph problem type (if applicable).
|
|
209
|
+
n_layers (int): Number of QAOA layers.
|
|
210
|
+
n_qubits (int): Number of qubits required.
|
|
211
|
+
cost_hamiltonian (qml.Hamiltonian): The cost Hamiltonian encoding the problem.
|
|
212
|
+
mixer_hamiltonian (qml.Hamiltonian): The mixer Hamiltonian for exploration.
|
|
213
|
+
initial_state (str): The initial quantum state.
|
|
214
|
+
problem_metadata (dict | None): Additional metadata from problem setup.
|
|
215
|
+
loss_constant (float): Constant term from the problem.
|
|
216
|
+
optimizer (Optimizer): Classical optimizer for parameter updates.
|
|
217
|
+
max_iterations (int): Maximum number of optimization iterations.
|
|
218
|
+
current_iteration (int): Current optimization iteration.
|
|
219
|
+
_n_params (int): Number of parameters per layer (always 2 for QAOA).
|
|
220
|
+
_solution_nodes (list[int] | None): Solution nodes for graph problems.
|
|
221
|
+
_solution_bitstring (np.ndarray | None): Solution bitstring for QUBO problems.
|
|
222
|
+
"""
|
|
223
|
+
|
|
163
224
|
def __init__(
|
|
164
225
|
self,
|
|
165
226
|
problem: GraphProblemTypes | QUBOProblemTypes,
|
|
@@ -171,16 +232,18 @@ class QAOA(QuantumProgram):
|
|
|
171
232
|
max_iterations: int = 10,
|
|
172
233
|
**kwargs,
|
|
173
234
|
):
|
|
174
|
-
"""
|
|
175
|
-
Initialize the QAOA problem.
|
|
235
|
+
"""Initialize the QAOA problem.
|
|
176
236
|
|
|
177
237
|
Args:
|
|
178
|
-
problem: The problem to solve, can either be a graph or a QUBO.
|
|
238
|
+
problem (GraphProblemTypes | QUBOProblemTypes): The problem to solve, can either be a graph or a QUBO.
|
|
179
239
|
For graph inputs, the graph problem to solve must be provided
|
|
180
240
|
through the `graph_problem` variable.
|
|
181
|
-
graph_problem (
|
|
182
|
-
n_layers (int):
|
|
183
|
-
initial_state (
|
|
241
|
+
graph_problem (GraphProblem | None): The graph problem to solve. Defaults to None.
|
|
242
|
+
n_layers (int): Number of QAOA layers. Defaults to 1.
|
|
243
|
+
initial_state (_SUPPORTED_INITIAL_STATES_LITERAL): The initial state of the circuit. Defaults to "Recommended".
|
|
244
|
+
optimizer (Optimizer | None): The optimizer to use. Defaults to MonteCarloOptimizer.
|
|
245
|
+
max_iterations (int): Maximum number of optimization iterations. Defaults to 10.
|
|
246
|
+
**kwargs: Additional keyword arguments passed to the parent class.
|
|
184
247
|
"""
|
|
185
248
|
|
|
186
249
|
if isinstance(problem, QUBOProblemTypes):
|
|
@@ -235,16 +298,14 @@ class QAOA(QuantumProgram):
|
|
|
235
298
|
self.max_iterations = max_iterations
|
|
236
299
|
self.current_iteration = 0
|
|
237
300
|
self._n_params = 2
|
|
238
|
-
self._is_compute_probabilites = False
|
|
239
301
|
self.optimizer = optimizer if optimizer is not None else MonteCarloOptimizer()
|
|
240
302
|
|
|
241
|
-
self._probs = {}
|
|
242
303
|
self._solution_nodes = []
|
|
243
304
|
self._solution_bitstring = []
|
|
244
305
|
|
|
245
306
|
(
|
|
246
|
-
self.
|
|
247
|
-
self.
|
|
307
|
+
self._cost_hamiltonian,
|
|
308
|
+
self._mixer_hamiltonian,
|
|
248
309
|
*problem_metadata,
|
|
249
310
|
self.initial_state,
|
|
250
311
|
) = _resolve_circuit_layers(
|
|
@@ -269,13 +330,22 @@ class QAOA(QuantumProgram):
|
|
|
269
330
|
|
|
270
331
|
self._meta_circuits = self._create_meta_circuits_dict()
|
|
271
332
|
|
|
333
|
+
@property
|
|
334
|
+
def cost_hamiltonian(self) -> qml.operation.Operator:
|
|
335
|
+
"""The cost Hamiltonian for the QAOA problem."""
|
|
336
|
+
return self._cost_hamiltonian
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def mixer_hamiltonian(self) -> qml.operation.Operator:
|
|
340
|
+
"""The mixer Hamiltonian for the QAOA problem."""
|
|
341
|
+
return self._mixer_hamiltonian
|
|
342
|
+
|
|
272
343
|
@property
|
|
273
344
|
def solution(self):
|
|
274
|
-
"""
|
|
275
|
-
Get the solution found by QAOA optimization.
|
|
345
|
+
"""Get the solution found by QAOA optimization.
|
|
276
346
|
|
|
277
347
|
Returns:
|
|
278
|
-
list: For graph problems, returns a list of selected node indices.
|
|
348
|
+
list[int] | np.ndarray: For graph problems, returns a list of selected node indices.
|
|
279
349
|
For QUBO problems, returns a list/array of binary values.
|
|
280
350
|
"""
|
|
281
351
|
return (
|
|
@@ -284,28 +354,15 @@ class QAOA(QuantumProgram):
|
|
|
284
354
|
else self._solution_bitstring
|
|
285
355
|
)
|
|
286
356
|
|
|
287
|
-
@property
|
|
288
|
-
def probs(self) -> dict:
|
|
289
|
-
"""
|
|
290
|
-
Get a copy of the probability distributions from final measurements.
|
|
291
|
-
|
|
292
|
-
Returns:
|
|
293
|
-
dict: Copy of the probability distributions dictionary. Keys are
|
|
294
|
-
circuit tags, values are probability distributions.
|
|
295
|
-
"""
|
|
296
|
-
return self._probs.copy()
|
|
297
|
-
|
|
298
|
-
@probs.setter
|
|
299
|
-
def probs(self, value: dict):
|
|
300
|
-
"""Set the probability distributions."""
|
|
301
|
-
self._probs = value
|
|
302
|
-
|
|
303
357
|
def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
|
|
304
|
-
"""
|
|
305
|
-
Generate the meta circuits for the QAOA problem.
|
|
358
|
+
"""Generate the meta circuits for the QAOA problem.
|
|
306
359
|
|
|
307
|
-
|
|
308
|
-
|
|
360
|
+
Creates both cost and measurement circuits for the QAOA algorithm.
|
|
361
|
+
The cost circuit is used during optimization, while the measurement
|
|
362
|
+
circuit is used for final solution extraction.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
dict[str, MetaCircuit]: Dictionary containing cost_circuit and meas_circuit.
|
|
309
366
|
"""
|
|
310
367
|
|
|
311
368
|
betas = sp.symarray("β", self.n_layers)
|
|
@@ -315,14 +372,16 @@ class QAOA(QuantumProgram):
|
|
|
315
372
|
|
|
316
373
|
def _qaoa_layer(params):
|
|
317
374
|
gamma, beta = params
|
|
318
|
-
pqaoa.cost_layer(gamma, self.
|
|
319
|
-
pqaoa.mixer_layer(beta, self.
|
|
375
|
+
pqaoa.cost_layer(gamma, self._cost_hamiltonian)
|
|
376
|
+
pqaoa.mixer_layer(beta, self._mixer_hamiltonian)
|
|
320
377
|
|
|
321
378
|
def _prepare_circuit(hamiltonian, params, final_measurement):
|
|
322
|
-
"""
|
|
323
|
-
|
|
379
|
+
"""Prepare the circuit for the QAOA problem.
|
|
380
|
+
|
|
324
381
|
Args:
|
|
325
|
-
hamiltonian (qml.Hamiltonian): The Hamiltonian term to measure
|
|
382
|
+
hamiltonian (qml.Hamiltonian): The Hamiltonian term to measure.
|
|
383
|
+
params (np.ndarray): The QAOA parameters (betas and gammas).
|
|
384
|
+
final_measurement (bool): Whether to perform final measurement.
|
|
326
385
|
"""
|
|
327
386
|
|
|
328
387
|
# Note: could've been done as qml.[Insert Gate](wires=range(self.n_qubits))
|
|
@@ -345,105 +404,86 @@ class QAOA(QuantumProgram):
|
|
|
345
404
|
return {
|
|
346
405
|
"cost_circuit": self._meta_circuit_factory(
|
|
347
406
|
qml.tape.make_qscript(_prepare_circuit)(
|
|
348
|
-
self.
|
|
407
|
+
self._cost_hamiltonian, sym_params, final_measurement=False
|
|
349
408
|
),
|
|
350
409
|
symbols=sym_params.flatten(),
|
|
351
410
|
),
|
|
352
411
|
"meas_circuit": self._meta_circuit_factory(
|
|
353
412
|
qml.tape.make_qscript(_prepare_circuit)(
|
|
354
|
-
self.
|
|
413
|
+
self._cost_hamiltonian, sym_params, final_measurement=True
|
|
355
414
|
),
|
|
356
415
|
symbols=sym_params.flatten(),
|
|
416
|
+
grouping_strategy="wires",
|
|
357
417
|
),
|
|
358
418
|
}
|
|
359
419
|
|
|
360
|
-
def _generate_circuits(self):
|
|
361
|
-
"""
|
|
362
|
-
Generate the circuits for the QAOA problem.
|
|
420
|
+
def _generate_circuits(self) -> list[Circuit]:
|
|
421
|
+
"""Generate the circuits for the QAOA problem.
|
|
363
422
|
|
|
364
|
-
|
|
365
|
-
|
|
423
|
+
Generates circuits for each parameter set in the current parameters.
|
|
424
|
+
The circuit type depends on whether we're computing probabilities
|
|
425
|
+
(for final solution extraction) or just expectation values (for optimization).
|
|
366
426
|
|
|
427
|
+
Returns:
|
|
428
|
+
list[Circuit]: List of Circuit objects for execution.
|
|
429
|
+
"""
|
|
367
430
|
circuit_type = (
|
|
368
431
|
"cost_circuit" if not self._is_compute_probabilites else "meas_circuit"
|
|
369
432
|
)
|
|
370
433
|
|
|
371
|
-
|
|
372
|
-
|
|
434
|
+
return [
|
|
435
|
+
self._meta_circuits[circuit_type].initialize_circuit_from_params(
|
|
373
436
|
params_group, tag_prefix=f"{p}"
|
|
374
437
|
)
|
|
438
|
+
for p, params_group in enumerate(self._curr_params)
|
|
439
|
+
]
|
|
375
440
|
|
|
376
|
-
|
|
441
|
+
def _post_process_results(self, results, **kwargs):
|
|
442
|
+
"""Post-process the results of the QAOA problem.
|
|
377
443
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
444
|
+
Args:
|
|
445
|
+
results (dict[str, dict[str, int]]): Raw results from circuit execution.
|
|
446
|
+
**kwargs: Additional keyword arguments.
|
|
447
|
+
ham_ops (str): The Hamiltonian operators to measure, semicolon-separated.
|
|
448
|
+
Only needed when the backend supports expval.
|
|
381
449
|
|
|
382
450
|
Returns:
|
|
383
|
-
|
|
451
|
+
dict[str, dict[str, float]] | dict[int, float]: The losses for each parameter set grouping, or probability
|
|
452
|
+
distributions if computing probabilities.
|
|
384
453
|
"""
|
|
385
454
|
|
|
386
455
|
if self._is_compute_probabilites:
|
|
387
|
-
return
|
|
388
|
-
outer_k: {
|
|
389
|
-
inner_k: inner_v / self.backend.shots
|
|
390
|
-
for inner_k, inner_v in outer_v.items()
|
|
391
|
-
}
|
|
392
|
-
for outer_k, outer_v in results.items()
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
losses = super()._post_process_results(results)
|
|
456
|
+
return self._process_probability_results(results)
|
|
396
457
|
|
|
458
|
+
losses = super()._post_process_results(results, **kwargs)
|
|
397
459
|
return losses
|
|
398
460
|
|
|
399
|
-
def
|
|
400
|
-
"""
|
|
401
|
-
Execute final measurement circuits to obtain probability distributions.
|
|
402
|
-
|
|
403
|
-
Runs the optimized circuit with final parameters to get the full probability
|
|
404
|
-
distribution over all measurement outcomes, which is used to extract the
|
|
405
|
-
solution bitstring.
|
|
406
|
-
"""
|
|
407
|
-
self._is_compute_probabilites = True
|
|
408
|
-
|
|
409
|
-
self._curr_params = np.array(self._final_params)
|
|
410
|
-
|
|
411
|
-
self._circuits[:] = []
|
|
412
|
-
|
|
413
|
-
self._generate_circuits()
|
|
461
|
+
def _perform_final_computation(self, **kwargs):
|
|
462
|
+
"""Extract the optimal solution from the QAOA optimization process.
|
|
414
463
|
|
|
415
|
-
self._probs.update(self._dispatch_circuits_and_process_results())
|
|
416
|
-
|
|
417
|
-
self._is_compute_probabilites = False
|
|
418
|
-
|
|
419
|
-
def _perform_final_computation(self):
|
|
420
|
-
"""
|
|
421
|
-
Computes and extracts the final solution from the QAOA optimization process.
|
|
422
464
|
This method performs the following steps:
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
- For graph problems, stores the solution as a list of node indices corresponding to '1's in the bitstring.
|
|
429
|
-
5. Returns the total circuit count and total runtime for the optimization process.
|
|
465
|
+
1. Executes measurement circuits with the best parameters (those that achieved the lowest loss).
|
|
466
|
+
2. Retrieves the bitstring representing the best solution, correcting for endianness.
|
|
467
|
+
3. Depending on the problem type:
|
|
468
|
+
- For QUBO problems, stores the solution as a NumPy array of bits.
|
|
469
|
+
- For graph problems, stores the solution as a list of node indices corresponding to '1's in the bitstring.
|
|
430
470
|
|
|
431
471
|
Returns:
|
|
432
|
-
tuple: A tuple containing:
|
|
433
|
-
|
|
434
|
-
|
|
472
|
+
tuple[int, float]: A tuple containing:
|
|
473
|
+
- int: The total number of circuits executed.
|
|
474
|
+
- float: The total runtime of the optimization process.
|
|
435
475
|
"""
|
|
436
476
|
|
|
437
|
-
self.reporter.info(message="🏁 Computing Final Solution
|
|
477
|
+
self.reporter.info(message="🏁 Computing Final Solution 🏁\r")
|
|
438
478
|
|
|
439
|
-
self.
|
|
479
|
+
self._run_solution_measurement()
|
|
440
480
|
|
|
441
|
-
|
|
481
|
+
best_measurement_probs = next(iter(self._best_probs.values()))
|
|
442
482
|
|
|
443
|
-
#
|
|
483
|
+
# Endianness is corrected in _post_process_results
|
|
444
484
|
best_solution_bitstring = max(
|
|
445
|
-
|
|
446
|
-
)
|
|
485
|
+
best_measurement_probs, key=best_measurement_probs.get
|
|
486
|
+
)
|
|
447
487
|
|
|
448
488
|
if isinstance(self.problem, QUBOProblemTypes):
|
|
449
489
|
self._solution_bitstring[:] = np.fromiter(
|
|
@@ -455,11 +495,12 @@ class QAOA(QuantumProgram):
|
|
|
455
495
|
m.start() for m in re.finditer("1", best_solution_bitstring)
|
|
456
496
|
]
|
|
457
497
|
|
|
498
|
+
self.reporter.info(message="🏁 Computed Final Solution! 🏁\r\n")
|
|
499
|
+
|
|
458
500
|
return self._total_circuit_count, self._total_run_time
|
|
459
501
|
|
|
460
502
|
def draw_solution(self):
|
|
461
|
-
"""
|
|
462
|
-
Visualize the solution found by QAOA for graph problems.
|
|
503
|
+
"""Visualize the solution found by QAOA for graph problems.
|
|
463
504
|
|
|
464
505
|
Draws the graph with solution nodes highlighted in red and other nodes
|
|
465
506
|
in light blue. If the solution hasn't been computed yet, it will be
|