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.
@@ -0,0 +1,3 @@
1
+ from excitationsolve.info import __version__, HOMEPAGE
2
+ from excitationsolve.excitation_solve import excitation_solve_step
3
+ from excitationsolve.excitation_solve_qiskit import ExcitationSolveQiskit
@@ -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
@@ -0,0 +1,3 @@
1
+ __version__ = "1.1.0" # MAJOR.MINOR.PATCH, like in https://semver.org/
2
+
3
+ HOMEPAGE = "https://github.com/dlr-wf/ExcitationSolve" # GitHub page