qoro-divi 0.2.0b1__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 +8 -0
- divi/_pbar.py +73 -0
- divi/circuits.py +139 -0
- divi/exp/cirq/__init__.py +7 -0
- divi/exp/cirq/_lexer.py +126 -0
- divi/exp/cirq/_parser.py +889 -0
- divi/exp/cirq/_qasm_export.py +37 -0
- divi/exp/cirq/_qasm_import.py +35 -0
- divi/exp/cirq/exception.py +21 -0
- divi/exp/scipy/_cobyla.py +342 -0
- divi/exp/scipy/pyprima/LICENCE.txt +28 -0
- divi/exp/scipy/pyprima/__init__.py +263 -0
- divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
- divi/exp/scipy/pyprima/cobyla/cobyla.py +599 -0
- divi/exp/scipy/pyprima/cobyla/cobylb.py +849 -0
- divi/exp/scipy/pyprima/cobyla/geometry.py +240 -0
- divi/exp/scipy/pyprima/cobyla/initialize.py +269 -0
- divi/exp/scipy/pyprima/cobyla/trustregion.py +540 -0
- divi/exp/scipy/pyprima/cobyla/update.py +331 -0
- divi/exp/scipy/pyprima/common/__init__.py +0 -0
- divi/exp/scipy/pyprima/common/_bounds.py +41 -0
- divi/exp/scipy/pyprima/common/_linear_constraints.py +46 -0
- divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +64 -0
- divi/exp/scipy/pyprima/common/_project.py +224 -0
- divi/exp/scipy/pyprima/common/checkbreak.py +107 -0
- divi/exp/scipy/pyprima/common/consts.py +48 -0
- divi/exp/scipy/pyprima/common/evaluate.py +101 -0
- divi/exp/scipy/pyprima/common/history.py +39 -0
- divi/exp/scipy/pyprima/common/infos.py +30 -0
- divi/exp/scipy/pyprima/common/linalg.py +452 -0
- divi/exp/scipy/pyprima/common/message.py +336 -0
- divi/exp/scipy/pyprima/common/powalg.py +131 -0
- divi/exp/scipy/pyprima/common/preproc.py +393 -0
- divi/exp/scipy/pyprima/common/present.py +5 -0
- divi/exp/scipy/pyprima/common/ratio.py +56 -0
- divi/exp/scipy/pyprima/common/redrho.py +49 -0
- divi/exp/scipy/pyprima/common/selectx.py +346 -0
- divi/interfaces.py +25 -0
- divi/parallel_simulator.py +258 -0
- divi/qasm.py +220 -0
- divi/qem.py +191 -0
- divi/qlogger.py +119 -0
- divi/qoro_service.py +343 -0
- divi/qprog/__init__.py +13 -0
- divi/qprog/_graph_partitioning.py +619 -0
- divi/qprog/_mlae.py +182 -0
- divi/qprog/_qaoa.py +440 -0
- divi/qprog/_vqe.py +275 -0
- divi/qprog/_vqe_sweep.py +144 -0
- divi/qprog/batch.py +235 -0
- divi/qprog/optimizers.py +75 -0
- divi/qprog/quantum_program.py +493 -0
- divi/utils.py +116 -0
- qoro_divi-0.2.0b1.dist-info/LICENSE +190 -0
- qoro_divi-0.2.0b1.dist-info/LICENSES/Apache-2.0.txt +73 -0
- qoro_divi-0.2.0b1.dist-info/METADATA +57 -0
- qoro_divi-0.2.0b1.dist-info/RECORD +58 -0
- qoro_divi-0.2.0b1.dist-info/WHEEL +4 -0
divi/qprog/_mlae.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from functools import reduce
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import scipy.optimize as optimize
|
|
9
|
+
from qiskit import QuantumCircuit
|
|
10
|
+
from qiskit_algorithms import EstimationProblem, MaximumLikelihoodAmplitudeEstimation
|
|
11
|
+
|
|
12
|
+
from divi.circuits import Circuit
|
|
13
|
+
from divi.qprog.quantum_program import QuantumProgram
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BernoulliA(QuantumCircuit):
|
|
17
|
+
"""A circuit representing the Bernoulli A operator."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, probability):
|
|
20
|
+
super().__init__(1)
|
|
21
|
+
|
|
22
|
+
theta_p = 2 * np.arcsin(np.sqrt(probability))
|
|
23
|
+
self.ry(theta_p, 0)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BernoulliQ(QuantumCircuit):
|
|
27
|
+
"""A circuit representing the Bernoulli Q operator."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, probability):
|
|
30
|
+
super().__init__(1)
|
|
31
|
+
|
|
32
|
+
self._theta_p = 2 * np.arcsin(np.sqrt(probability))
|
|
33
|
+
self.ry(2 * self._theta_p, 0)
|
|
34
|
+
|
|
35
|
+
def power(self, k):
|
|
36
|
+
# implement the efficient power of Q
|
|
37
|
+
q_k = QuantumCircuit(1)
|
|
38
|
+
q_k.ry(2 * k * self._theta_p, 0)
|
|
39
|
+
return q_k
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MLAE(QuantumProgram):
|
|
43
|
+
"""
|
|
44
|
+
An implementation of the Maximum Likelihood Amplitude Estimateion described in
|
|
45
|
+
https://arxiv.org/pdf/1904.10246
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
grovers: list[int],
|
|
51
|
+
qubits_to_measure: list[int],
|
|
52
|
+
probability: float,
|
|
53
|
+
**kwargs,
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initializes the MLAE problem.
|
|
57
|
+
args:
|
|
58
|
+
grovers (list): A list of non-negative integers corresponding to the powers of the Grover
|
|
59
|
+
operator for each iteration
|
|
60
|
+
qubits: An integer or list of integers containing the index of the qubits to measure
|
|
61
|
+
probability: The probability of being in the good state to estimate
|
|
62
|
+
shots: The number of shots to run for each circuit. Default set at 5000.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
super().__init__(**kwargs)
|
|
66
|
+
|
|
67
|
+
self.grovers = grovers
|
|
68
|
+
self.qubits_to_measure = qubits_to_measure
|
|
69
|
+
self.probability = probability
|
|
70
|
+
self.likelihood_functions = []
|
|
71
|
+
|
|
72
|
+
def _create_meta_circuits_dict(self):
|
|
73
|
+
return super()._create_meta_circuits_dict()
|
|
74
|
+
|
|
75
|
+
def _generate_circuits(self, params=None, **kwargs):
|
|
76
|
+
"""
|
|
77
|
+
Generates the circuits that perform step one of the MLAE algorithm,
|
|
78
|
+
the quantum amplitude amplification.
|
|
79
|
+
|
|
80
|
+
Inputs a selection of m values corresponding to the powers of the
|
|
81
|
+
Grover operatorfor each iteration.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
A list of QASM circuits to run on various devices
|
|
85
|
+
"""
|
|
86
|
+
self.circuits.clear()
|
|
87
|
+
|
|
88
|
+
A = BernoulliA(self.probability)
|
|
89
|
+
Q = BernoulliQ(self.probability)
|
|
90
|
+
|
|
91
|
+
problem = EstimationProblem(
|
|
92
|
+
state_preparation=A,
|
|
93
|
+
grover_operator=Q,
|
|
94
|
+
objective_qubits=self.qubits_to_measure,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
qiskit_circuits = MaximumLikelihoodAmplitudeEstimation(
|
|
98
|
+
self.grovers
|
|
99
|
+
).construct_circuits(problem)
|
|
100
|
+
|
|
101
|
+
for circuit, grover in zip(qiskit_circuits, self.grovers):
|
|
102
|
+
circuit.measure_all()
|
|
103
|
+
self.circuits.append(Circuit(circuit, tags=[f"{grover}"]))
|
|
104
|
+
|
|
105
|
+
def run(self, store_data=False, data_file=None):
|
|
106
|
+
self._generate_circuits()
|
|
107
|
+
self._dispatch_circuits_and_process_results(
|
|
108
|
+
store_data=store_data, data_file=data_file
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _post_process_results(self, results):
|
|
112
|
+
"""
|
|
113
|
+
Generates the likelihood function for each circuit of the quantum
|
|
114
|
+
amplitude amplification. These likelihood functions will then
|
|
115
|
+
be combined to create a maximum likelihood function to analyze.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
A callable maximum likelihood function
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# Define the necessary variables Nk, Mk, Lk
|
|
122
|
+
for label, shots_dict in results.items():
|
|
123
|
+
mk = int(label)
|
|
124
|
+
Nk = 0
|
|
125
|
+
hk = 0
|
|
126
|
+
for key, shots in shots_dict.items():
|
|
127
|
+
Nk += shots
|
|
128
|
+
hk += shots if key.count("1") == len(key) else 0
|
|
129
|
+
|
|
130
|
+
def likelihood_function(theta, mk=mk, hk=hk, Nk=Nk):
|
|
131
|
+
as_theta = np.arcsin(np.sqrt(theta))
|
|
132
|
+
return ((np.sin((2 * mk + 1) * as_theta)) ** (2 * hk)) * (
|
|
133
|
+
(np.cos((2 * mk + 1) * as_theta)) ** (2 * (Nk - hk))
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.likelihood_functions.append(likelihood_function)
|
|
137
|
+
|
|
138
|
+
def generate_maximum_likelihood_function(self, factor=1.0):
|
|
139
|
+
"""
|
|
140
|
+
Post-processing takes in likelihood functions.
|
|
141
|
+
|
|
142
|
+
A large factor (e.g. 1e200) should be used for visualization purposes.
|
|
143
|
+
Returns:
|
|
144
|
+
The maximum likelihood function.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def combined_likelihood_function(theta):
|
|
148
|
+
return (
|
|
149
|
+
reduce(
|
|
150
|
+
lambda result, f: result * f(theta), self.likelihood_functions, 1.0
|
|
151
|
+
)
|
|
152
|
+
* factor
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
self.maximum_likelihood_fn = combined_likelihood_function
|
|
156
|
+
|
|
157
|
+
return combined_likelihood_function
|
|
158
|
+
|
|
159
|
+
def estimate_amplitude(self, factor):
|
|
160
|
+
"""
|
|
161
|
+
Uses the maximum likelihood function to ascertain
|
|
162
|
+
a value for the amplitude.
|
|
163
|
+
|
|
164
|
+
Returns
|
|
165
|
+
Estimation of the amplitude
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def minimum_likelihood_function(theta):
|
|
169
|
+
# The factor to set to -10e30 in the older branch
|
|
170
|
+
return (
|
|
171
|
+
reduce(
|
|
172
|
+
lambda result, f: result * f(theta), self.likelihood_functions, 1.0
|
|
173
|
+
)
|
|
174
|
+
* factor
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# create the range of possible amplitudes
|
|
178
|
+
amplitudes = np.linspace(0, 1, 100)
|
|
179
|
+
|
|
180
|
+
bounds = [(min(amplitudes), max(amplitudes))]
|
|
181
|
+
|
|
182
|
+
return optimize.differential_evolution(minimum_likelihood_function, bounds).x[0]
|
divi/qprog/_qaoa.py
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from functools import reduce
|
|
8
|
+
from typing import Literal, Optional, get_args
|
|
9
|
+
from warnings import warn
|
|
10
|
+
|
|
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
|
+
from qiskit_optimization import QuadraticProgram
|
|
20
|
+
from qiskit_optimization.converters import QuadraticProgramToQubo
|
|
21
|
+
from qiskit_optimization.problems import VarType
|
|
22
|
+
|
|
23
|
+
from divi.circuits import MetaCircuit
|
|
24
|
+
from divi.qprog import QuantumProgram
|
|
25
|
+
from divi.qprog.optimizers import Optimizer
|
|
26
|
+
from divi.utils import convert_qubo_matrix_to_pennylane_ising
|
|
27
|
+
|
|
28
|
+
GraphProblemTypes = nx.Graph | rx.PyGraph
|
|
29
|
+
QUBOProblemTypes = list | np.ndarray | sps.spmatrix | QuadraticProgram
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def draw_graph_solution_nodes(main_graph, partition_nodes):
|
|
33
|
+
# Create a dictionary for node colors
|
|
34
|
+
node_colors = [
|
|
35
|
+
"red" if node in partition_nodes else "lightblue" for node in main_graph.nodes()
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
plt.figure(figsize=(10, 8))
|
|
39
|
+
|
|
40
|
+
pos = nx.spring_layout(main_graph)
|
|
41
|
+
|
|
42
|
+
nx.draw_networkx_nodes(main_graph, pos, node_color=node_colors, node_size=500)
|
|
43
|
+
nx.draw_networkx_edges(main_graph, pos)
|
|
44
|
+
nx.draw_networkx_labels(main_graph, pos, font_size=10, font_weight="bold")
|
|
45
|
+
|
|
46
|
+
# Remove axes
|
|
47
|
+
plt.axis("off")
|
|
48
|
+
|
|
49
|
+
# Show the plot
|
|
50
|
+
plt.tight_layout()
|
|
51
|
+
plt.show()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GraphProblem(Enum):
|
|
55
|
+
MAX_CLIQUE = ("max_clique", "Zeros", "Superposition")
|
|
56
|
+
MAX_INDEPENDENT_SET = ("max_independent_set", "Zeros", "Superposition")
|
|
57
|
+
MAX_WEIGHT_CYCLE = ("max_weight_cycle", "Superposition", "Superposition")
|
|
58
|
+
MAXCUT = ("maxcut", "Superposition", "Superposition")
|
|
59
|
+
MIN_VERTEX_COVER = ("min_vertex_cover", "Ones", "Superposition")
|
|
60
|
+
|
|
61
|
+
# This is an internal problem with no pennylane equivalent
|
|
62
|
+
EDGE_PARTITIONING = ("", "", "")
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
pl_string: str,
|
|
67
|
+
constrained_initial_state: str,
|
|
68
|
+
unconstrained_initial_state: str,
|
|
69
|
+
):
|
|
70
|
+
self.pl_string = pl_string
|
|
71
|
+
|
|
72
|
+
# Recommended initial state as per Pennylane's documentation.
|
|
73
|
+
# Value is duplicated if not applicable to the problem
|
|
74
|
+
self.constrained_initial_state = constrained_initial_state
|
|
75
|
+
self.unconstrained_initial_state = unconstrained_initial_state
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
_SUPPORTED_INITIAL_STATES_LITERAL = Literal[
|
|
79
|
+
"Zeros", "Ones", "Superposition", "Recommended"
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _convert_quadratic_program_to_pennylane_ising(qp: QuadraticProgram):
|
|
84
|
+
qiskit_sparse_op, constant = qp.to_ising()
|
|
85
|
+
|
|
86
|
+
pauli_list = qiskit_sparse_op.paulis
|
|
87
|
+
|
|
88
|
+
pennylane_ising = 0.0
|
|
89
|
+
for pauli_string, coeff in zip(pauli_list.z, qiskit_sparse_op.coeffs):
|
|
90
|
+
sanitized_coeff = coeff.real if np.isreal(coeff) else coeff
|
|
91
|
+
|
|
92
|
+
curr_term = (
|
|
93
|
+
reduce(
|
|
94
|
+
lambda x, y: x @ y,
|
|
95
|
+
map(lambda x: qml.Z(x), np.flatnonzero(pauli_string)),
|
|
96
|
+
)
|
|
97
|
+
* sanitized_coeff.item()
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
pennylane_ising += curr_term
|
|
101
|
+
|
|
102
|
+
return pennylane_ising, constant.item(), pauli_list.num_qubits
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _resolve_circuit_layers(
|
|
106
|
+
initial_state, problem, graph_problem, **kwargs
|
|
107
|
+
) -> tuple[qml.operation.Operator, qml.operation.Operator, Optional[dict], str]:
|
|
108
|
+
"""
|
|
109
|
+
Generates the cost and mixer hamiltonians for a given problem, in addition to
|
|
110
|
+
optional metadata returned by Pennylane if applicable
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
if isinstance(problem, GraphProblemTypes):
|
|
114
|
+
is_constrained = kwargs.pop("is_constrained", True)
|
|
115
|
+
|
|
116
|
+
if graph_problem == GraphProblem.MAXCUT:
|
|
117
|
+
params = (problem,)
|
|
118
|
+
else:
|
|
119
|
+
params = (problem, is_constrained)
|
|
120
|
+
|
|
121
|
+
if initial_state == "Recommended":
|
|
122
|
+
resolved_initial_state = (
|
|
123
|
+
graph_problem.constrained_initial_state
|
|
124
|
+
if is_constrained
|
|
125
|
+
else graph_problem.constrained_initial_state
|
|
126
|
+
)
|
|
127
|
+
else:
|
|
128
|
+
resolved_initial_state = initial_state
|
|
129
|
+
|
|
130
|
+
return *getattr(pqaoa, graph_problem.pl_string)(*params), resolved_initial_state
|
|
131
|
+
else:
|
|
132
|
+
if isinstance(problem, QuadraticProgram):
|
|
133
|
+
cost_hamiltonian, constant, n_qubits = (
|
|
134
|
+
_convert_quadratic_program_to_pennylane_ising(problem)
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
cost_hamiltonian, constant = convert_qubo_matrix_to_pennylane_ising(problem)
|
|
138
|
+
|
|
139
|
+
n_qubits = problem.shape[0]
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
cost_hamiltonian,
|
|
143
|
+
pqaoa.x_mixer(range(n_qubits)),
|
|
144
|
+
{"constant": constant},
|
|
145
|
+
"Superposition",
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class QAOA(QuantumProgram):
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
problem: GraphProblemTypes | QUBOProblemTypes,
|
|
153
|
+
graph_problem: Optional[GraphProblem] = None,
|
|
154
|
+
n_layers: int = 1,
|
|
155
|
+
initial_state: _SUPPORTED_INITIAL_STATES_LITERAL = "Recommended",
|
|
156
|
+
optimizer: Optimizer = Optimizer.MONTE_CARLO,
|
|
157
|
+
max_iterations: int = 10,
|
|
158
|
+
**kwargs,
|
|
159
|
+
):
|
|
160
|
+
"""
|
|
161
|
+
Initialize the QAOA problem.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
problem: The problem to solve, can either be a graph or a QUBO.
|
|
165
|
+
For graph inputs, the graph problem to solve must be provided
|
|
166
|
+
through the `graph_problem` variable.
|
|
167
|
+
graph_problem (str): The graph problem to solve.
|
|
168
|
+
n_layers (int): number of QAOA layers
|
|
169
|
+
initial_state (str): The initial state of the circuit
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
if isinstance(problem, QUBOProblemTypes):
|
|
173
|
+
if graph_problem is not None:
|
|
174
|
+
warn("Ignoring the 'problem' argument as it is not applicable to QUBO.")
|
|
175
|
+
|
|
176
|
+
self.graph_problem = None
|
|
177
|
+
|
|
178
|
+
if isinstance(problem, QuadraticProgram):
|
|
179
|
+
if (
|
|
180
|
+
any(var.vartype != VarType.BINARY for var in problem.variables)
|
|
181
|
+
or problem.linear_constraints
|
|
182
|
+
or problem.quadratic_constraints
|
|
183
|
+
):
|
|
184
|
+
warn(
|
|
185
|
+
"Quadratic Program contains non-binary variables. Converting to QUBO."
|
|
186
|
+
)
|
|
187
|
+
self._qp_converter = QuadraticProgramToQubo()
|
|
188
|
+
problem = self._qp_converter.convert(problem)
|
|
189
|
+
|
|
190
|
+
self.n_qubits = problem.get_num_vars()
|
|
191
|
+
else:
|
|
192
|
+
if isinstance(problem, list):
|
|
193
|
+
problem = np.array(problem)
|
|
194
|
+
|
|
195
|
+
if problem.ndim != 2 or problem.shape[0] != problem.shape[1]:
|
|
196
|
+
raise ValueError(
|
|
197
|
+
"Invalid QUBO matrix."
|
|
198
|
+
f" Got array of shape {problem.shape}."
|
|
199
|
+
" Must be a square matrix."
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
self.n_qubits = problem.shape[1]
|
|
203
|
+
else:
|
|
204
|
+
if not isinstance(graph_problem, GraphProblem):
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"Unsupported Problem. Got '{graph_problem}'. Must be one of type divi.qprog.GraphProblem."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
self.graph_problem = graph_problem
|
|
210
|
+
self.n_qubits = problem.number_of_nodes()
|
|
211
|
+
|
|
212
|
+
self.problem = problem
|
|
213
|
+
|
|
214
|
+
if initial_state not in get_args(_SUPPORTED_INITIAL_STATES_LITERAL):
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"Unsupported Initial State. Got {initial_state}. Must be one of: {get_args(_SUPPORTED_INITIAL_STATES_LITERAL)}"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Local Variables
|
|
220
|
+
self.n_layers = n_layers
|
|
221
|
+
self.optimizer = optimizer
|
|
222
|
+
self.max_iterations = max_iterations
|
|
223
|
+
self.current_iteration = 0
|
|
224
|
+
self._solution_nodes = None
|
|
225
|
+
self.n_params = 2
|
|
226
|
+
self._is_compute_probabilites = False
|
|
227
|
+
|
|
228
|
+
# Shared Variables
|
|
229
|
+
self.probs = kwargs.pop("probs", {})
|
|
230
|
+
|
|
231
|
+
(
|
|
232
|
+
self.cost_hamiltonian,
|
|
233
|
+
self.mixer_hamiltonian,
|
|
234
|
+
*problem_metadata,
|
|
235
|
+
self.initial_state,
|
|
236
|
+
) = _resolve_circuit_layers(
|
|
237
|
+
initial_state=initial_state,
|
|
238
|
+
problem=problem,
|
|
239
|
+
graph_problem=graph_problem,
|
|
240
|
+
**kwargs,
|
|
241
|
+
)
|
|
242
|
+
self.problem_metadata = problem_metadata[0] if problem_metadata else {}
|
|
243
|
+
|
|
244
|
+
self.loss_constant = self.problem_metadata.get("constant", 0.0)
|
|
245
|
+
|
|
246
|
+
kwargs.pop("is_constrained", None)
|
|
247
|
+
super().__init__(**kwargs)
|
|
248
|
+
|
|
249
|
+
self._meta_circuits = self._create_meta_circuits_dict()
|
|
250
|
+
|
|
251
|
+
@property
|
|
252
|
+
def solution(self):
|
|
253
|
+
return (
|
|
254
|
+
self._solution_nodes
|
|
255
|
+
if self.graph_problem is not None
|
|
256
|
+
else self._solution_bitstring
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
|
|
260
|
+
"""
|
|
261
|
+
Generate the meta circuits for the QAOA problem.
|
|
262
|
+
|
|
263
|
+
In this method, we generate the scaffolding for the circuits that will be
|
|
264
|
+
executed during optimization.
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
betas = sp.symarray("β", self.n_layers)
|
|
268
|
+
gammas = sp.symarray("γ", self.n_layers)
|
|
269
|
+
|
|
270
|
+
sym_params = np.vstack((betas, gammas)).transpose()
|
|
271
|
+
|
|
272
|
+
def _qaoa_layer(params):
|
|
273
|
+
gamma, beta = params
|
|
274
|
+
pqaoa.cost_layer(gamma, self.cost_hamiltonian)
|
|
275
|
+
pqaoa.mixer_layer(beta, self.mixer_hamiltonian)
|
|
276
|
+
|
|
277
|
+
def _prepare_circuit(hamiltonian, params, final_measurement):
|
|
278
|
+
"""
|
|
279
|
+
Prepare the circuit for the QAOA problem.
|
|
280
|
+
Args:
|
|
281
|
+
hamiltonian (qml.Hamiltonian): The Hamiltonian term to measure
|
|
282
|
+
"""
|
|
283
|
+
|
|
284
|
+
# Note: could've been done as qml.[Insert Gate](wires=range(self.n_qubits))
|
|
285
|
+
# but there seems to be a bug with program capture in Pennylane.
|
|
286
|
+
# Maybe check when a new version comes out?
|
|
287
|
+
if self.initial_state == "Ones":
|
|
288
|
+
for i in range(self.n_qubits):
|
|
289
|
+
qml.PauliX(wires=i)
|
|
290
|
+
elif self.initial_state == "Superposition":
|
|
291
|
+
for i in range(self.n_qubits):
|
|
292
|
+
qml.Hadamard(wires=i)
|
|
293
|
+
|
|
294
|
+
qml.layer(_qaoa_layer, self.n_layers, params)
|
|
295
|
+
|
|
296
|
+
if final_measurement:
|
|
297
|
+
return qml.probs()
|
|
298
|
+
else:
|
|
299
|
+
return qml.expval(hamiltonian)
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
"cost_circuit": self._meta_circuit_factory(
|
|
303
|
+
qml.tape.make_qscript(_prepare_circuit)(
|
|
304
|
+
self.cost_hamiltonian, sym_params, final_measurement=False
|
|
305
|
+
),
|
|
306
|
+
symbols=sym_params.flatten(),
|
|
307
|
+
),
|
|
308
|
+
"meas_circuit": self._meta_circuit_factory(
|
|
309
|
+
qml.tape.make_qscript(_prepare_circuit)(
|
|
310
|
+
self.cost_hamiltonian, sym_params, final_measurement=True
|
|
311
|
+
),
|
|
312
|
+
symbols=sym_params.flatten(),
|
|
313
|
+
),
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
def _generate_circuits(self):
|
|
317
|
+
"""
|
|
318
|
+
Generate the circuits for the QAOA problem.
|
|
319
|
+
|
|
320
|
+
In this method, we generate bulk circuits based on the selected parameters.
|
|
321
|
+
"""
|
|
322
|
+
|
|
323
|
+
circuit_type = (
|
|
324
|
+
"cost_circuit" if not self._is_compute_probabilites else "meas_circuit"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
for p, params_group in enumerate(self._curr_params):
|
|
328
|
+
circuit = self._meta_circuits[circuit_type].initialize_circuit_from_params(
|
|
329
|
+
params_group, tag_prefix=f"{p}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
self.circuits.append(circuit)
|
|
333
|
+
|
|
334
|
+
def _post_process_results(self, results):
|
|
335
|
+
"""
|
|
336
|
+
Post-process the results of the QAOA problem.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
(dict) The losses for each parameter set grouping.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
if self._is_compute_probabilites:
|
|
343
|
+
return {
|
|
344
|
+
outer_k: {
|
|
345
|
+
inner_k: inner_v / self.backend.shots
|
|
346
|
+
for inner_k, inner_v in outer_v.items()
|
|
347
|
+
}
|
|
348
|
+
for outer_k, outer_v in results.items()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
losses = super()._post_process_results(results)
|
|
352
|
+
|
|
353
|
+
return losses
|
|
354
|
+
|
|
355
|
+
def _run_final_measurement(self):
|
|
356
|
+
self._is_compute_probabilites = True
|
|
357
|
+
|
|
358
|
+
self._curr_params = np.array(self.final_params)
|
|
359
|
+
|
|
360
|
+
self.circuits[:] = []
|
|
361
|
+
|
|
362
|
+
self._generate_circuits()
|
|
363
|
+
|
|
364
|
+
self.probs.update(self._dispatch_circuits_and_process_results())
|
|
365
|
+
|
|
366
|
+
self._is_compute_probabilites = False
|
|
367
|
+
|
|
368
|
+
def compute_final_solution(self):
|
|
369
|
+
"""
|
|
370
|
+
Computes and extracts the final solution from the QAOA optimization process.
|
|
371
|
+
This method performs the following steps:
|
|
372
|
+
1. Identifies the best solution index based on the lowest loss value from the last optimization step.
|
|
373
|
+
2. Executes the final measurement circuit to obtain the probability distributions of solutions.
|
|
374
|
+
3. Retrieves the bitstring representing the best solution, correcting for endianness.
|
|
375
|
+
4. Depending on the problem type:
|
|
376
|
+
- For QUBO problems, stores the solution as a NumPy array of bits.
|
|
377
|
+
- For graph problems, stores the solution as a list of node indices corresponding to '1's in the bitstring.
|
|
378
|
+
5. Returns the total circuit count and total runtime for the optimization process.
|
|
379
|
+
Returns:
|
|
380
|
+
tuple: A tuple containing:
|
|
381
|
+
- int: The total number of circuits executed.
|
|
382
|
+
- float: The total runtime of the optimization process.
|
|
383
|
+
Raises:
|
|
384
|
+
RuntimeError: If more than one/no matching key is found for the best solution index.
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
# Convert losses dict to list to apply ordinal operations
|
|
388
|
+
final_losses_list = list(self.losses[-1].values())
|
|
389
|
+
|
|
390
|
+
# Get the index of the smallest loss in the last operation
|
|
391
|
+
best_solution_idx = min(
|
|
392
|
+
range(len(final_losses_list)),
|
|
393
|
+
key=lambda x: final_losses_list.__getitem__(x),
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
# Insert the measurement circuit here
|
|
397
|
+
self._run_final_measurement()
|
|
398
|
+
|
|
399
|
+
# Find the key matching the best_solution_idx with possible metadata in between
|
|
400
|
+
pattern = re.compile(rf"^{best_solution_idx}(?:_[^_]*)*_0$")
|
|
401
|
+
matching_keys = [k for k in self.probs.keys() if pattern.match(k)]
|
|
402
|
+
|
|
403
|
+
# Some minor sanity checks
|
|
404
|
+
if len(matching_keys) == 0:
|
|
405
|
+
raise RuntimeError("No matching key found.")
|
|
406
|
+
if len(matching_keys) > 1:
|
|
407
|
+
raise RuntimeError(f"More than one matching key found.")
|
|
408
|
+
|
|
409
|
+
best_solution_key = matching_keys[0]
|
|
410
|
+
# Retrieve the probability distribution dictionary of the best solution
|
|
411
|
+
best_solution_probs = self.probs[best_solution_key]
|
|
412
|
+
|
|
413
|
+
# Retrieve the bitstring with the actual best solution
|
|
414
|
+
# Reverse to account for the endianness difference
|
|
415
|
+
best_solution_bitstring = max(best_solution_probs, key=best_solution_probs.get)[
|
|
416
|
+
::-1
|
|
417
|
+
]
|
|
418
|
+
|
|
419
|
+
if isinstance(self.problem, QUBOProblemTypes):
|
|
420
|
+
self._solution_bitstring = np.fromiter(
|
|
421
|
+
best_solution_bitstring, dtype=np.int32
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if isinstance(self.problem, GraphProblemTypes):
|
|
425
|
+
self._solution_nodes = [
|
|
426
|
+
m.start() for m in re.finditer("1", best_solution_bitstring)
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
return self._total_circuit_count, self._total_run_time
|
|
430
|
+
|
|
431
|
+
def draw_solution(self):
|
|
432
|
+
if self.graph_problem is None:
|
|
433
|
+
raise RuntimeError(
|
|
434
|
+
"The problem is not a graph problem. Cannot draw solution."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
if not self._solution_nodes:
|
|
438
|
+
self.compute_final_solution()
|
|
439
|
+
|
|
440
|
+
draw_graph_solution_nodes(self.problem, self._solution_nodes)
|