excitationsolve 1.1.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.
- excitationsolve/__init__.py +3 -0
- excitationsolve/excitation_solve.py +121 -0
- excitationsolve/excitation_solve_2d.py +190 -0
- excitationsolve/excitation_solve_adapt.py +160 -0
- excitationsolve/excitation_solve_pennylane.py +57 -0
- excitationsolve/excitation_solve_qiskit.py +146 -0
- excitationsolve/info.py +3 -0
- excitationsolve/trig_poly_utils.py +141 -0
- excitationsolve-1.1.0.dist-info/METADATA +303 -0
- excitationsolve-1.1.0.dist-info/RECORD +13 -0
- excitationsolve-1.1.0.dist-info/WHEEL +5 -0
- excitationsolve-1.1.0.dist-info/licenses/LICENSE.txt +206 -0
- excitationsolve-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import numpy as np
|
|
3
|
+
from excitationsolve.trig_poly_utils import fourier_series_minimum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def excitation_solve_step(
|
|
7
|
+
parameter_variations: list | np.ndarray,
|
|
8
|
+
energy_samples: list | np.ndarray,
|
|
9
|
+
return_coeffs=False,
|
|
10
|
+
non_exact=False,
|
|
11
|
+
) -> tuple[float, float] | tuple[float, float, np.ndarray]:
|
|
12
|
+
"""
|
|
13
|
+
Optimizes a single excitation parameter globally given the energies for five shifts of this parameters.
|
|
14
|
+
Recommended to use in a loop for all excitation parameters and perform multiple sweeps through the parameters (potentially shuffled) until convergence.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
parameter_variations: list or np.ndarray
|
|
18
|
+
Five variations/shifts of a single excitation parameter.
|
|
19
|
+
energy_samples: list or np.ndarray
|
|
20
|
+
Five energy samples for a single excitation parameter varied.
|
|
21
|
+
Returns:
|
|
22
|
+
float, float
|
|
23
|
+
The optimized excitation parameter and the corresponding energy.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
parameter_variations = np.array(parameter_variations).flatten()
|
|
27
|
+
energy_samples = np.array(energy_samples).flatten()
|
|
28
|
+
assert (
|
|
29
|
+
len(parameter_variations) >= 5
|
|
30
|
+
), f"The number of parameter variations must be at least 5. Got {len(parameter_variations)}."
|
|
31
|
+
assert (
|
|
32
|
+
len(energy_samples) >= 5
|
|
33
|
+
), f"The number of energy samples must be at least 5. Got {len(energy_samples)}."
|
|
34
|
+
assert len(parameter_variations) == len(
|
|
35
|
+
energy_samples
|
|
36
|
+
), f"The number of parameter variations and energy samples must match. Got {len(parameter_variations)} and {len(energy_samples)}."
|
|
37
|
+
assert (
|
|
38
|
+
parameter_variations.max() - parameter_variations.min() <= 4 * np.pi
|
|
39
|
+
), "The parameter variations must be within one period of the excitation operator (4*pi)."
|
|
40
|
+
|
|
41
|
+
# ### Fit second-order Fourier series / trigonometric polynomial to the energy samples: ###
|
|
42
|
+
# Determine linear equation system with N equations and 5 unknowns (N number of samples):
|
|
43
|
+
c1 = np.cos(parameter_variations / 2)
|
|
44
|
+
s1 = np.sin(parameter_variations / 2)
|
|
45
|
+
c2 = np.cos(parameter_variations)
|
|
46
|
+
s2 = np.sin(parameter_variations)
|
|
47
|
+
A = np.array([np.ones(len(energy_samples)), c1, s1, c2, s2]).T
|
|
48
|
+
b = np.array(energy_samples)
|
|
49
|
+
logging.debug("Linear equation system: \nA: \n%s \n b: %s", np.around(A, 3), b)
|
|
50
|
+
|
|
51
|
+
if len(parameter_variations) == 5:
|
|
52
|
+
# ## Exact reconstruction: ##
|
|
53
|
+
# solve the equation system:
|
|
54
|
+
coeffs = np.linalg.solve(A, b)
|
|
55
|
+
else:
|
|
56
|
+
# ## Fitted reconstruction (least squares fit) for potentially noisy energy values: ##
|
|
57
|
+
logging.debug(
|
|
58
|
+
"More than 5 parameter variations or energy samples provided. A least squares fit will be performed instead of an exact reconstruction."
|
|
59
|
+
)
|
|
60
|
+
coeffs, residuals, rank_A, s = np.linalg.lstsq(A, b)
|
|
61
|
+
logging.debug(
|
|
62
|
+
"Least-squares fit info:\n\tResiduals: %s \n\tRank of A: %i \n\tSingular values of A: %s",
|
|
63
|
+
residuals,
|
|
64
|
+
rank_A,
|
|
65
|
+
s,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
logging.debug("Solved coefficients: %s", coeffs)
|
|
69
|
+
|
|
70
|
+
if non_exact:
|
|
71
|
+
# reconstructed energy function:
|
|
72
|
+
def energy_function(xs):
|
|
73
|
+
c1 = np.cos(xs / 2)
|
|
74
|
+
s1 = np.sin(xs / 2)
|
|
75
|
+
c2 = np.cos(xs)
|
|
76
|
+
s2 = np.sin(xs)
|
|
77
|
+
return (
|
|
78
|
+
coeffs[0]
|
|
79
|
+
+ coeffs[1] * c1
|
|
80
|
+
+ coeffs[2] * s1
|
|
81
|
+
+ coeffs[3] * c2
|
|
82
|
+
+ coeffs[4] * s2
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
### Brute-force optimization of reconstruction
|
|
86
|
+
n_samples = int(1e3)
|
|
87
|
+
x_min, x_max = -2 * np.pi, 2 * np.pi
|
|
88
|
+
x = np.linspace(x_min, x_max, n_samples)
|
|
89
|
+
y = energy_function(x) # Note, vectorized evaluation must be supported!
|
|
90
|
+
min_idx = np.argmin(y)
|
|
91
|
+
return x[min_idx], y[min_idx]
|
|
92
|
+
|
|
93
|
+
# ### Optimize parameter globally in reconstruction (companion matrix method)
|
|
94
|
+
a = [coeffs[0], *coeffs[1::2]] # cosine coefficients
|
|
95
|
+
b = coeffs[2::2] # sine coefficients
|
|
96
|
+
assert (
|
|
97
|
+
len(a) == len(b) + 1
|
|
98
|
+
), f"The number of cosine coefficients must be one more than the number of sine coefficients. Got {len(a)} and {len(b)}."
|
|
99
|
+
min_x, min_y = fourier_series_minimum(a, b, return_y=True)
|
|
100
|
+
# rescale to original parameter range since the optimization was performed on the interval [pi, pi] with doubled frequencies instead of [-2*pi, 2*pi]:
|
|
101
|
+
min_x *= 2
|
|
102
|
+
|
|
103
|
+
if return_coeffs:
|
|
104
|
+
return min_x, min_y, coeffs
|
|
105
|
+
return min_x, min_y
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
### Demo: ###
|
|
109
|
+
if __name__ == "__main__":
|
|
110
|
+
np.random.seed(42) # For reproducibility and because 42 is the answer to everything
|
|
111
|
+
logging.basicConfig(level=logging.INFO) # Enable logging
|
|
112
|
+
# Example data:
|
|
113
|
+
parameter_variations_test = np.array([0, np.pi / 2, -np.pi / 2, np.pi, -np.pi])
|
|
114
|
+
energy_samples_test = np.random.rand(5) # random energies for demonstration
|
|
115
|
+
# Optimize excitation parameter:
|
|
116
|
+
optimized_parameter, optimized_energy = excitation_solve_step(
|
|
117
|
+
parameter_variations_test, energy_samples_test
|
|
118
|
+
)
|
|
119
|
+
print(
|
|
120
|
+
f"Optimized excitation parameter: {optimized_parameter} \nOptimized energy: {optimized_energy}"
|
|
121
|
+
)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import logging
|
|
3
|
+
import pennylane as qml
|
|
4
|
+
import pennylane.numpy as pnp
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def excitation_solve_2d_step(
|
|
8
|
+
parameter_variations: list | np.ndarray,
|
|
9
|
+
energy_samples: list | np.ndarray,
|
|
10
|
+
return_coeffs=False,
|
|
11
|
+
non_exact=False,
|
|
12
|
+
) -> tuple[float, float] | tuple[float, float, np.ndarray]:
|
|
13
|
+
"""
|
|
14
|
+
Optimizes two excitation parameters globally given the energies for 25 shifts of this parameters.
|
|
15
|
+
Recommended to use in a loop for all excitation parameters and perform multiple sweeps through the parameters (potentially shuffled) until convergence.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
parameter_variations: list or np.ndarray
|
|
19
|
+
25 variations/shifts of two excitation parameters. Shape: (25, 2)
|
|
20
|
+
energy_samples: list or np.ndarray
|
|
21
|
+
25 energy samples for two excitation parameters varied.
|
|
22
|
+
Returns:
|
|
23
|
+
float, float
|
|
24
|
+
The optimized excitation parameter and the corresponding energy.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
parameter_variations = np.array(parameter_variations)
|
|
28
|
+
energy_samples = np.array(energy_samples).flatten()
|
|
29
|
+
assert parameter_variations.ndim == 2, f"The parameter variations must be a 2D array. Got {parameter_variations.ndim} dimensions."
|
|
30
|
+
assert len(parameter_variations) >= 5**2, f"The number of parameter variations must be at least 5. Got {len(parameter_variations)}."
|
|
31
|
+
assert parameter_variations.shape[1] == 2, f"The parameter variations must have two columns. Got {parameter_variations.shape[1]}."
|
|
32
|
+
assert len(energy_samples) >= 5**2, f"The number of energy samples must be at least 5. Got {len(energy_samples)}."
|
|
33
|
+
assert len(parameter_variations) == len(
|
|
34
|
+
energy_samples
|
|
35
|
+
), f"The number of parameter variations and energy samples must match. Got {len(parameter_variations)} and {len(energy_samples)}."
|
|
36
|
+
assert (
|
|
37
|
+
parameter_variations[:, 0].max() - parameter_variations[:, 0].min() <= 4 * np.pi
|
|
38
|
+
), "The variations of the first parameter must be within one period of the excitation operator (4*pi)."
|
|
39
|
+
assert (
|
|
40
|
+
parameter_variations[:, 1].max() - parameter_variations[:, 1].min() <= 4 * np.pi
|
|
41
|
+
), "The variations of the second parameter must be within one period of the excitation operator (4*pi)."
|
|
42
|
+
|
|
43
|
+
# ### Fit second-order Fourier series / trigonometric polynomial to the energy samples: ###
|
|
44
|
+
# Determine linear equation system with N equations and 5**2 unknowns (N number of samples):
|
|
45
|
+
param_1_terms = [
|
|
46
|
+
np.ones(len(parameter_variations)),
|
|
47
|
+
np.cos(parameter_variations[:, 0] / 2),
|
|
48
|
+
np.sin(parameter_variations[:, 0] / 2),
|
|
49
|
+
np.cos(parameter_variations[:, 0]),
|
|
50
|
+
np.sin(parameter_variations[:, 0]),
|
|
51
|
+
]
|
|
52
|
+
param_2_terms = [
|
|
53
|
+
np.ones(len(parameter_variations)),
|
|
54
|
+
np.cos(parameter_variations[:, 1] / 2),
|
|
55
|
+
np.sin(parameter_variations[:, 1] / 2),
|
|
56
|
+
np.cos(parameter_variations[:, 1]),
|
|
57
|
+
np.sin(parameter_variations[:, 1]),
|
|
58
|
+
]
|
|
59
|
+
A = np.array([p1_term * p2_term for p1_term in param_1_terms for p2_term in param_2_terms]).T
|
|
60
|
+
b = np.array(energy_samples)
|
|
61
|
+
logging.debug("Linear equation system: \nA: \n%s \n b: %s", np.around(A, 3), b)
|
|
62
|
+
|
|
63
|
+
if len(parameter_variations) == 5**2:
|
|
64
|
+
# ## Exact reconstruction: ##
|
|
65
|
+
# solve the equation system:
|
|
66
|
+
coeffs = np.linalg.solve(A, b)
|
|
67
|
+
else:
|
|
68
|
+
raise NotImplementedError("Least squares fit not implemented for 2D parameter variations yet.")
|
|
69
|
+
|
|
70
|
+
logging.debug("Solved coefficients: %s", coeffs)
|
|
71
|
+
|
|
72
|
+
def energy_function(xs):
|
|
73
|
+
xs_1_terms = [
|
|
74
|
+
np.ones(len(xs)),
|
|
75
|
+
np.cos(xs[:, 0] / 2),
|
|
76
|
+
np.sin(xs[:, 0] / 2),
|
|
77
|
+
np.cos(xs[:, 0]),
|
|
78
|
+
np.sin(xs[:, 0]),
|
|
79
|
+
]
|
|
80
|
+
xs_2_terms = [
|
|
81
|
+
np.ones(len(xs)),
|
|
82
|
+
np.cos(xs[:, 1] / 2),
|
|
83
|
+
np.sin(xs[:, 1] / 2),
|
|
84
|
+
np.cos(xs[:, 1]),
|
|
85
|
+
np.sin(xs[:, 1]),
|
|
86
|
+
]
|
|
87
|
+
A = np.array([xs1_term * xs2_term for xs1_term in xs_1_terms for xs2_term in xs_2_terms]).T
|
|
88
|
+
ys = A @ coeffs
|
|
89
|
+
return ys
|
|
90
|
+
|
|
91
|
+
if non_exact:
|
|
92
|
+
# ### Optimize parameter globally in reconstruction (brute-force sampling method) ###
|
|
93
|
+
# sample in both directions of the parameter space:
|
|
94
|
+
n_samples_per_dim = int(1e3)
|
|
95
|
+
samples_one_dim = np.linspace(-2 * np.pi, 2 * np.pi, n_samples_per_dim)
|
|
96
|
+
samples = np.array([[x, y] for x in samples_one_dim for y in samples_one_dim])
|
|
97
|
+
sampled_energies = energy_function(samples)
|
|
98
|
+
min_idx = np.argmin(sampled_energies)
|
|
99
|
+
min_x, min_y = samples[min_idx], sampled_energies[min_idx]
|
|
100
|
+
else:
|
|
101
|
+
# ### Optimize parameter globally in reconstruction (Nyquist + gradient descent)
|
|
102
|
+
n_nyquist_samples_per_dim = 5
|
|
103
|
+
init_points = np.linspace(-2 * np.pi, 2 * np.pi, n_nyquist_samples_per_dim + 1)[:-1]
|
|
104
|
+
|
|
105
|
+
def energy_function_diffable(x):
|
|
106
|
+
xs_1_terms = [
|
|
107
|
+
1,
|
|
108
|
+
pnp.cos(x[0] / 2),
|
|
109
|
+
pnp.sin(x[0] / 2),
|
|
110
|
+
pnp.cos(x[0]),
|
|
111
|
+
pnp.sin(x[0]),
|
|
112
|
+
]
|
|
113
|
+
xs_2_terms = [
|
|
114
|
+
1,
|
|
115
|
+
pnp.cos(x[1] / 2),
|
|
116
|
+
pnp.sin(x[1] / 2),
|
|
117
|
+
pnp.cos(x[1]),
|
|
118
|
+
pnp.sin(x[1]),
|
|
119
|
+
]
|
|
120
|
+
A = pnp.array(
|
|
121
|
+
[xs1_term * xs2_term for xs1_term in xs_1_terms for xs2_term in xs_2_terms],
|
|
122
|
+
requires_grad=True,
|
|
123
|
+
).T
|
|
124
|
+
ys = A @ pnp.array(coeffs)
|
|
125
|
+
return ys
|
|
126
|
+
|
|
127
|
+
min_x = None
|
|
128
|
+
min_y = np.infty
|
|
129
|
+
for x1 in init_points:
|
|
130
|
+
for x2 in init_points:
|
|
131
|
+
x = pnp.array([x1, x2], requires_grad=True)
|
|
132
|
+
max_iter = 1_000_000
|
|
133
|
+
# Use gradient descent with high constant step size that still guarantees convergence:
|
|
134
|
+
grad_fn = qml.grad(energy_function_diffable)
|
|
135
|
+
step_size = 1 / (np.sum(np.abs(coeffs))) # 1/Lipschitz constant, could be improved by a better constant
|
|
136
|
+
for i in range(max_iter):
|
|
137
|
+
# Use gradient descent
|
|
138
|
+
grad = grad_fn(x)
|
|
139
|
+
update = -grad
|
|
140
|
+
x += step_size * update
|
|
141
|
+
if np.linalg.norm(update) < 1e-10:
|
|
142
|
+
if min_y > (y := energy_function_diffable(x.numpy())):
|
|
143
|
+
min_x = x.numpy()
|
|
144
|
+
min_y = y.numpy()
|
|
145
|
+
# make sure x is in the range [-2*pi, 2*pi] using modulo operation:
|
|
146
|
+
min_x = (min_x + 2 * np.pi) % (4 * np.pi) - 2 * np.pi
|
|
147
|
+
assert np.isclose(energy_function_diffable(min_x), min_y)
|
|
148
|
+
break
|
|
149
|
+
if i == max_iter - 1:
|
|
150
|
+
logging.warning(
|
|
151
|
+
"Gradient descent optimization did not converge for initial point %s, %s.",
|
|
152
|
+
x1,
|
|
153
|
+
x2,
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
logging.debug(
|
|
157
|
+
"Gradient descent optimization converged after %i iterations to point %s.",
|
|
158
|
+
i,
|
|
159
|
+
min_x,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
logging.debug("Returning point x=%s, y=%s.", min_x, min_y)
|
|
163
|
+
|
|
164
|
+
if return_coeffs:
|
|
165
|
+
return min_x, min_y, coeffs
|
|
166
|
+
return min_x, min_y
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
### Demo: ###
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
np.random.seed(42) # For reproducibility and because 42 is the answer to everything
|
|
172
|
+
logging.basicConfig(level=logging.INFO) # Enable logging
|
|
173
|
+
# Example data:
|
|
174
|
+
parameter_variations_test = np.linspace(-2 * np.pi, 2 * np.pi, 6)[:-1] # 5 variations of the excitation parameter (for demonstration)
|
|
175
|
+
parameter_2d_variations_test = np.array([[x, y] for x in parameter_variations_test for y in parameter_variations_test])
|
|
176
|
+
energy_samples_test = np.random.rand(len(parameter_2d_variations_test)) # random energies for demonstration
|
|
177
|
+
print(parameter_2d_variations_test)
|
|
178
|
+
print(energy_samples_test)
|
|
179
|
+
|
|
180
|
+
# Optimize excitation parameter:
|
|
181
|
+
optimized_parameter, optimized_energy = excitation_solve_2d_step(parameter_2d_variations_test, energy_samples_test)
|
|
182
|
+
# Double-check with brute-force optimization:
|
|
183
|
+
optimized_parameter_check, optimized_energy_check = excitation_solve_2d_step(parameter_2d_variations_test, energy_samples_test, non_exact=True)
|
|
184
|
+
print(
|
|
185
|
+
f"Optimized excitation parameter: {optimized_parameter} \nOptimized energy: {optimized_energy}\n\n"
|
|
186
|
+
f"Optimized excitation parameter (brute-force): {optimized_parameter_check} \n"
|
|
187
|
+
f"Optimized energy (brute-force): {optimized_energy_check}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert optimized_energy <= optimized_energy_check, "Optimized energy is not minimal."
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Copyright 2018-2022 Xanadu Quantum Technologies Inc.
|
|
2
|
+
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
"""Adaptive optimizer"""
|
|
15
|
+
import copy
|
|
16
|
+
from typing import Sequence, Callable
|
|
17
|
+
|
|
18
|
+
# pylint: disable= no-value-for-parameter, protected-access, not-callable
|
|
19
|
+
import pennylane as qml
|
|
20
|
+
from pennylane import numpy as pnp
|
|
21
|
+
from pennylane.tape import QuantumTape
|
|
22
|
+
from pennylane import transform
|
|
23
|
+
import excitation_solve
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@transform
|
|
27
|
+
def append_gate(tape: QuantumTape, params, gates) -> (Sequence[QuantumTape], Callable):
|
|
28
|
+
"""Append parameterized gates to an existing tape.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
tape (QuantumTape or QNode or Callable): quantum circuit to transform by adding gates
|
|
32
|
+
params (array[float]): parameters of the gates to be added
|
|
33
|
+
gates (list[Operator]): list of the gates to be added
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
new_operations = []
|
|
40
|
+
|
|
41
|
+
for i, g in enumerate(gates):
|
|
42
|
+
g = copy.copy(g)
|
|
43
|
+
new_params = (params[i], *g.data[1:])
|
|
44
|
+
g.data = new_params
|
|
45
|
+
new_operations.append(g)
|
|
46
|
+
|
|
47
|
+
new_tape = type(tape)(
|
|
48
|
+
tape.operations + new_operations, tape.measurements, shots=tape.shots
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def null_postprocessing(results):
|
|
52
|
+
"""A postprocesing function returned by a transform that only converts the batch of results
|
|
53
|
+
into a result for a single ``QuantumTape``.
|
|
54
|
+
"""
|
|
55
|
+
return results[0] # pragma: no cover
|
|
56
|
+
|
|
57
|
+
return [new_tape], null_postprocessing
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ExcitationAdaptiveOptimizer:
|
|
61
|
+
r"""Optimizer for building fully trained quantum circuits by adding gates adaptively using ExcitationSolve."""
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def _circuit(params, gates, initial_circuit):
|
|
65
|
+
"""Append parameterized gates to an existing circuit.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
params (array[float]): parameters of the gates to be added
|
|
69
|
+
gates (list[Operator]): list of the gates to be added
|
|
70
|
+
initial_circuit (function): user-defined circuit that returns an expectation value
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
function: user-defined circuit with appended gates
|
|
74
|
+
"""
|
|
75
|
+
final_circuit = append_gate(initial_circuit, params, gates)
|
|
76
|
+
|
|
77
|
+
return final_circuit()
|
|
78
|
+
|
|
79
|
+
def step(self, circuit, operator_pool, params_zero=True):
|
|
80
|
+
r"""Update the circuit with one step of the optimizer.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
circuit (.QNode): user-defined circuit returning an expectation value
|
|
84
|
+
operator_pool (list[Operator]): list of the gates to be used for adaptive optimization
|
|
85
|
+
params_zero (bool): flag to initiate circuit parameters at zero
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
.QNode: the optimized circuit
|
|
89
|
+
"""
|
|
90
|
+
return self.step_and_cost(circuit, operator_pool, params_zero=params_zero)[0]
|
|
91
|
+
|
|
92
|
+
def step_and_cost(
|
|
93
|
+
self, circuit, operator_pool, drain_pool=False, params_zero=False
|
|
94
|
+
):
|
|
95
|
+
r"""Update the circuit with one step of the optimizer, return the corresponding
|
|
96
|
+
objective function value prior to the step, and return the maximum gradient
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
circuit (.QNode): user-defined circuit returning an expectation value
|
|
100
|
+
operator_pool (list[Operator]): list of the gates to be used for adaptive optimization
|
|
101
|
+
drain_pool (bool): flag to remove selected gates from the operator pool
|
|
102
|
+
params_zero (bool): flag to initiate circuit parameters at zero
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
tuple[.QNode, float, float]: the optimized circuit, the objective function output prior
|
|
106
|
+
to the step, and the largest gradient
|
|
107
|
+
"""
|
|
108
|
+
cost = circuit()
|
|
109
|
+
qnode = copy.copy(circuit)
|
|
110
|
+
|
|
111
|
+
if drain_pool:
|
|
112
|
+
operator_pool = [
|
|
113
|
+
gate
|
|
114
|
+
for gate in operator_pool
|
|
115
|
+
if all(
|
|
116
|
+
gate.name != operation.name or gate.wires != operation.wires
|
|
117
|
+
for operation in circuit.tape.operations
|
|
118
|
+
)
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
params = pnp.array(
|
|
122
|
+
[gate.parameters[0] for gate in operator_pool], requires_grad=True
|
|
123
|
+
)
|
|
124
|
+
qnode.func = self._circuit
|
|
125
|
+
|
|
126
|
+
shifts = pnp.array([0, pnp.pi / 2, -pnp.pi / 2, pnp.pi, -pnp.pi])
|
|
127
|
+
energies = []
|
|
128
|
+
opt_params = []
|
|
129
|
+
# Select best operator to append based on ExcitationSolve
|
|
130
|
+
for param_to_vary, op in enumerate(operator_pool):
|
|
131
|
+
e_shifted = [
|
|
132
|
+
qnode(
|
|
133
|
+
[shift],
|
|
134
|
+
gates=[op],
|
|
135
|
+
initial_circuit=circuit.func,
|
|
136
|
+
)
|
|
137
|
+
for shift in shifts
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
opt_param, e_excsolve = excitation_solve.excitation_solve_step(
|
|
141
|
+
shifts, e_shifted
|
|
142
|
+
)
|
|
143
|
+
opt_params.append(opt_param)
|
|
144
|
+
energies.append(e_excsolve)
|
|
145
|
+
energies = pnp.array(energies)
|
|
146
|
+
selected_index = pnp.argmax(cost - energies)
|
|
147
|
+
selected_gates = [operator_pool[selected_index]]
|
|
148
|
+
|
|
149
|
+
# set parameter of appended op to its optimized value
|
|
150
|
+
if params_zero:
|
|
151
|
+
params = pnp.zeros(len(selected_gates))
|
|
152
|
+
else:
|
|
153
|
+
params = pnp.array(
|
|
154
|
+
[opt_params[selected_index]],
|
|
155
|
+
requires_grad=True,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
qnode.func = append_gate(circuit.func, params, selected_gates)
|
|
159
|
+
|
|
160
|
+
return qnode, cost, max(abs(cost - energies))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from excitationsolve import excitation_solve_step
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pennylane as qml
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def excitationsolve_pennylane(
|
|
7
|
+
circuit: qml.QNode,
|
|
8
|
+
params_excsolve: np.ndarray,
|
|
9
|
+
params_to_vary: list | None = None,
|
|
10
|
+
num_samples=5,
|
|
11
|
+
) -> tuple[np.ndarray, list]:
|
|
12
|
+
"""Optimizes all parameters once in the given pennylane quantum circuit.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
circuit (QNode): Quantum circuit that takes the parameters params_excsolve
|
|
16
|
+
and returns a scalar value which should be optimized
|
|
17
|
+
params_excsolve (pennylane.numpy.tensor): Tensor holding the parameters to be optimized.
|
|
18
|
+
params_to_vary (list | None): List of indices or None. If list then only
|
|
19
|
+
parameters with indices in that list are optimized.
|
|
20
|
+
If None all parameters are optimized. Defaults to None.
|
|
21
|
+
num_samples (int, optional): Number of different parameter shifts used to reconstruct
|
|
22
|
+
the energy function. Has to be equal or greater than 5.
|
|
23
|
+
If no noise is present values greater than five do not
|
|
24
|
+
improve the optimization. Defaults to 5.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
tuple(pennylane.numpy.tensor, list): Optimized parameters and energies
|
|
28
|
+
after optimizing each parameter
|
|
29
|
+
"""
|
|
30
|
+
if params_to_vary is None:
|
|
31
|
+
params_to_vary = range(len(params_excsolve))
|
|
32
|
+
|
|
33
|
+
param_dist = 4 * np.pi / num_samples
|
|
34
|
+
shifts = (np.arange(num_samples) - num_samples // 2) * param_dist
|
|
35
|
+
|
|
36
|
+
e_excsolve_list = []
|
|
37
|
+
|
|
38
|
+
for param_to_vary in params_to_vary:
|
|
39
|
+
e_shifted = [
|
|
40
|
+
circuit(
|
|
41
|
+
np.array(
|
|
42
|
+
[
|
|
43
|
+
(shift + param if i == param_to_vary else param)
|
|
44
|
+
for i, param in enumerate(params_excsolve)
|
|
45
|
+
]
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
for shift in shifts
|
|
49
|
+
]
|
|
50
|
+
param_variations = shifts + params_excsolve[param_to_vary]
|
|
51
|
+
params_excsolve[param_to_vary], e_excsolve = excitation_solve_step(
|
|
52
|
+
param_variations, e_shifted
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
e_excsolve_list.append(e_excsolve)
|
|
56
|
+
|
|
57
|
+
return params_excsolve, e_excsolve_list
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
import numpy as np
|
|
4
|
+
from qiskit_algorithms.optimizers import (
|
|
5
|
+
Optimizer,
|
|
6
|
+
OptimizerSupportLevel,
|
|
7
|
+
OptimizerResult,
|
|
8
|
+
)
|
|
9
|
+
from qiskit_algorithms.optimizers.optimizer import POINT
|
|
10
|
+
from excitationsolve import excitation_solve_step
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ExcitationSolveQiskit(Optimizer):
|
|
14
|
+
def __init__(self, maxiter, tol=1e-12, num_samples=5, hf_energy=None, save_parameters=False):
|
|
15
|
+
"""The ExcitationSolve optimizer.
|
|
16
|
+
Note that this optimizer never needs to evaluate the ansatz circuit
|
|
17
|
+
at the (current) optimal parameters, unless the optimal parameters fall onto
|
|
18
|
+
the sample points used to reconstruct the energy function.
|
|
19
|
+
Therefore, when used with a qiskit VQE object, the energies transmitted
|
|
20
|
+
to a VQE callback function, do not seem to improve or converge. Nevertheless,
|
|
21
|
+
the determined optimal energy and parameters are still returned.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
maxiter (int): Maximum number of VQE iterations (maximum number of times to optimize all parameters)
|
|
25
|
+
tol: Threshold of energy difference after subsequent VQE iterations defining convergence
|
|
26
|
+
num_samples (int, optional): Number of different parameter values at which to sample
|
|
27
|
+
the energy to reconstruct the energy function in one parameter.
|
|
28
|
+
Must be greater or equal to 5. Defaults to 5.
|
|
29
|
+
hf_energy (float | None, optional): The Hartree-Fock energy, i.e. the energy of the
|
|
30
|
+
system where all parameters in the circuit are zero. If none, this will be
|
|
31
|
+
calculated by evaluating the energy of the ansatz with all parameters set to zero.
|
|
32
|
+
If this energy is known from a prior classical calculation, e.g. a Hartree-Fock
|
|
33
|
+
calculation, one energy evaluation is saved. Defaults to None.
|
|
34
|
+
save_parameters (bool, optional) If True, params member variable contains
|
|
35
|
+
all optimal parameter values after each optimization step,
|
|
36
|
+
i.e. after optimizing each single parameter. Defaults to False.
|
|
37
|
+
"""
|
|
38
|
+
super().__init__()
|
|
39
|
+
|
|
40
|
+
self.maxiter = maxiter
|
|
41
|
+
self.tol = tol
|
|
42
|
+
assert num_samples >= 5, f"Number of samples needs to be greater or equal to 5 but is {num_samples}!"
|
|
43
|
+
self.num_samples = num_samples
|
|
44
|
+
self.hf_energy = hf_energy
|
|
45
|
+
self.save_parameters = save_parameters
|
|
46
|
+
|
|
47
|
+
self.energies = []
|
|
48
|
+
self.nfevs = []
|
|
49
|
+
self.params = []
|
|
50
|
+
|
|
51
|
+
def get_support_level(self):
|
|
52
|
+
"""Return support level dictionary"""
|
|
53
|
+
return {
|
|
54
|
+
"gradient": OptimizerSupportLevel.ignored,
|
|
55
|
+
"bounds": OptimizerSupportLevel.ignored,
|
|
56
|
+
"initial_point": OptimizerSupportLevel.required,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def minimize(
|
|
60
|
+
self,
|
|
61
|
+
fun: Callable[[POINT], float],
|
|
62
|
+
x0: POINT,
|
|
63
|
+
jac: Callable[[POINT], POINT] | None = None,
|
|
64
|
+
bounds: list[tuple[float, float]] | None = None,
|
|
65
|
+
) -> OptimizerResult:
|
|
66
|
+
self.energies = []
|
|
67
|
+
self.nfevs = []
|
|
68
|
+
self.params = []
|
|
69
|
+
|
|
70
|
+
param_dist = 4 * np.pi / self.num_samples
|
|
71
|
+
shifts = (np.arange(self.num_samples) - self.num_samples // 2) * param_dist
|
|
72
|
+
|
|
73
|
+
params_excsolve = np.array(x0.copy())
|
|
74
|
+
num_params = len(params_excsolve)
|
|
75
|
+
|
|
76
|
+
# qiskit excitation parameters result in excitation operators being \pi periodic
|
|
77
|
+
# ExcitationSolve expects them to be 2\pi periodic. Therefore, we rescale
|
|
78
|
+
# the parameters
|
|
79
|
+
param_scaling = 1 / 2
|
|
80
|
+
|
|
81
|
+
energies_once = [np.inf]
|
|
82
|
+
energy_at_zero = None
|
|
83
|
+
if self.hf_energy is not None:
|
|
84
|
+
energy_at_zero = self.hf_energy
|
|
85
|
+
self.energies = [self.hf_energy]
|
|
86
|
+
self.nfevs = [0]
|
|
87
|
+
|
|
88
|
+
nfev = 0
|
|
89
|
+
for n_iter in range(self.maxiter):
|
|
90
|
+
for param_to_vary in range(num_params):
|
|
91
|
+
e_shifted = []
|
|
92
|
+
for shift in shifts:
|
|
93
|
+
if shift == 0.0 and energy_at_zero is not None:
|
|
94
|
+
e_shifted.append(energy_at_zero)
|
|
95
|
+
continue
|
|
96
|
+
energy_tmp = fun(
|
|
97
|
+
np.array(
|
|
98
|
+
[
|
|
99
|
+
((shift + params_excsolve[i]) * param_scaling if i == param_to_vary else params_excsolve[i] * param_scaling)
|
|
100
|
+
for i in range(num_params)
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
e_shifted.append(energy_tmp)
|
|
105
|
+
if shift == 0.0 and energy_at_zero is None:
|
|
106
|
+
# Save Hartree-Fock (HF) energy as first energy with 0 as corresponding number of circuit evaluations needed
|
|
107
|
+
# since the HF energy can be known before any circuit execution by performing a classical HF calculation.
|
|
108
|
+
self.energies = [energy_tmp]
|
|
109
|
+
self.nfevs = [0]
|
|
110
|
+
nfev += 1
|
|
111
|
+
|
|
112
|
+
params_excsolve[param_to_vary], current_energy_excsolve = excitation_solve_step(
|
|
113
|
+
shifts + params_excsolve[param_to_vary],
|
|
114
|
+
e_shifted,
|
|
115
|
+
)
|
|
116
|
+
energy_at_zero = current_energy_excsolve
|
|
117
|
+
|
|
118
|
+
# Energy at current optimal parameters (not necessary to evaluate,
|
|
119
|
+
# since we already know it from the excitation_solve_step function)
|
|
120
|
+
# Nevertheless we could (unnecessarily) execute the circuit at the
|
|
121
|
+
# current optimal parameters so that a VQE callback function gets information
|
|
122
|
+
# about the optimization progress/convergence
|
|
123
|
+
# fun(params_excsolve * param_scaling)
|
|
124
|
+
|
|
125
|
+
# The optimal energies at each step are saved in the self.energies list
|
|
126
|
+
self.energies.append(current_energy_excsolve)
|
|
127
|
+
self.nfevs.append(nfev)
|
|
128
|
+
if self.save_parameters:
|
|
129
|
+
self.params.append(params_excsolve * param_scaling)
|
|
130
|
+
|
|
131
|
+
energies_once.append(current_energy_excsolve)
|
|
132
|
+
if np.abs(energies_once[-1] - energies_once[-2]) <= self.tol:
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
result = OptimizerResult()
|
|
136
|
+
result.x = params_excsolve * param_scaling
|
|
137
|
+
result.fun = current_energy_excsolve
|
|
138
|
+
result.nfev = nfev
|
|
139
|
+
result.njev = None
|
|
140
|
+
result.nit = n_iter + 1
|
|
141
|
+
|
|
142
|
+
self.energies = np.array(self.energies)
|
|
143
|
+
self.nfevs = np.array(self.nfevs)
|
|
144
|
+
self.params = np.array(self.params)
|
|
145
|
+
|
|
146
|
+
return result
|
excitationsolve/info.py
ADDED