Source code for pennylane.optimize.adaptive
# Copyright 2018-2022 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Adaptive optimizer"""
import copy
# pylint: disable= no-value-for-parameter, protected-access, not-callable
import pennylane as qml
from pennylane import numpy as pnp
from pennylane import transform
from pennylane.tape import QuantumScript, QuantumScriptBatch
from pennylane.typing import PostprocessingFn
@transform
def append_gate(tape: QuantumScript, params, gates) -> tuple[QuantumScriptBatch, PostprocessingFn]:
"""Append parametrized gates to an existing tape.
Args:
tape (QuantumTape or QNode or Callable): quantum circuit to transform by adding gates
params (array[float]): parameters of the gates to be added
gates (list[Operator]): list of the gates to be added
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`.
"""
new_operations = []
for i, g in enumerate(gates):
g = copy.copy(g)
new_params = (params[i], *g.data[1:])
g.data = new_params
new_operations.append(g)
new_tape = type(tape)(tape.operations + new_operations, tape.measurements, shots=tape.shots)
def null_postprocessing(results):
"""A postprocesing function returned by a transform that only converts the batch of results
into a result for a single ``QuantumTape``.
"""
return results[0] # pragma: no cover
return [new_tape], null_postprocessing
[docs]class AdaptiveOptimizer:
r"""Optimizer for building fully trained quantum circuits by adding gates adaptively.
Quantum circuits can be built by adding gates
`adaptively <https://www.nature.com/articles/s41467-019-10988-2>`_. The adaptive optimizer
implements an algorithm that grows and optimizes an input quantum circuit by selecting and
adding gates from a user-defined collection of operators. The algorithm starts by adding all
the gates to the circuit and computing the circuit gradients with respect to the gate
parameters. The algorithm then retains the gate which has the largest gradient and optimizes its
parameter. The process of growing the circuit can be repeated until the computed gradients
converge to zero within a given threshold. The optimizer returns the fully trained and
adaptively-built circuit. The adaptive optimizer can be used to implement
algorithms such as `ADAPT-VQE <https://www.nature.com/articles/s41467-019-10988-2>`_.
Args:
param_steps (int): number of steps for optimizing the parameter of a selected gate
stepsize (float): step size for optimizing the parameter of a selected gate
**Example**
This examples shows an implementation of the
`ADAPT-VQE <https://www.nature.com/articles/s41467-019-10988-2>`_ algorithm for building an
adaptive circuit for the :math:`\text{H}_3^+` cation.
>>> import pennylane as qml
>>> from pennylane import numpy as np
The molecule is defined and the Hamiltonian is computed with:
>>> symbols = ["H", "H", "H"]
>>> geometry = np.array([[0.01076341, 0.04449877, 0.0],
... [0.98729513, 1.63059094, 0.0],
... [1.87262415, -0.00815842, 0.0]], requires_grad=False)
>>> H, qubits = qml.qchem.molecular_hamiltonian(symbols, geometry, charge = 1)
The collection of gates to grow the circuit adaptively contains all single and double
excitations:
>>> n_electrons = 2
>>> singles, doubles = qml.qchem.excitations(n_electrons, qubits)
>>> singles_excitations = [qml.SingleExcitation(0.0, x) for x in singles]
>>> doubles_excitations = [qml.DoubleExcitation(0.0, x) for x in doubles]
>>> operator_pool = doubles_excitations + singles_excitations
An initial circuit preparing the Hartree-Fock state and returning the expectation value of the
Hamiltonian is defined:
>>> hf_state = qml.qchem.hf_state(n_electrons, qubits)
>>> dev = qml.device("default.qubit", wires=qubits)
>>> @qml.qnode(dev)
... def circuit():
... qml.BasisState(hf_state, wires=range(qubits))
... return qml.expval(H)
The optimizer is instantiated and then the circuit is created and optimized adaptively:
>>> opt = AdaptiveOptimizer()
>>> for i in range(len(operator_pool)):
... circuit, energy, gradient = opt.step_and_cost(circuit, operator_pool, drain_pool=True)
... print('Energy:', energy)
... print(qml.draw(circuit, show_matrices=False)())
... print('Largest Gradient:', gradient)
... print()
... if gradient < 1e-3:
... break
.. code-block :: pycon
Energy: -1.2465499384199699
0: ─╭|Ψ⟩─╭G²(0.20)─┤ ╭<𝓗>
1: ─├|Ψ⟩─├G²(0.20)─┤ ├<𝓗>
2: ─├|Ψ⟩─│─────────┤ ├<𝓗>
3: ─├|Ψ⟩─│─────────┤ ├<𝓗>
4: ─├|Ψ⟩─├G²(0.20)─┤ ├<𝓗>
5: ─╰|Ψ⟩─╰G²(0.20)─┤ ╰<𝓗>
Largest Gradient: 0.1439987277673651
Energy: -1.2613740231522532
0: ─╭|Ψ⟩─╭G²(0.20)─╭G²(0.19)─┤ ╭<𝓗>
1: ─├|Ψ⟩─├G²(0.20)─├G²(0.19)─┤ ├<𝓗>
2: ─├|Ψ⟩─│─────────├G²(0.19)─┤ ├<𝓗>
3: ─├|Ψ⟩─│─────────╰G²(0.19)─┤ ├<𝓗>
4: ─├|Ψ⟩─├G²(0.20)───────────┤ ├<𝓗>
5: ─╰|Ψ⟩─╰G²(0.20)───────────┤ ╰<𝓗>
Largest Gradient: 0.13493495624211427
Energy: -1.2743971719772815
0: ─╭|Ψ⟩─╭G²(0.20)─╭G²(0.19)──────────┤ ╭<𝓗>
1: ─├|Ψ⟩─├G²(0.20)─├G²(0.19)─╭G(0.00)─┤ ├<𝓗>
2: ─├|Ψ⟩─│─────────├G²(0.19)─│────────┤ ├<𝓗>
3: ─├|Ψ⟩─│─────────╰G²(0.19)─╰G(0.00)─┤ ├<𝓗>
4: ─├|Ψ⟩─├G²(0.20)────────────────────┤ ├<𝓗>
5: ─╰|Ψ⟩─╰G²(0.20)────────────────────┤ ╰<𝓗>
Largest Gradient: 0.0004084175253678331
"""
def __init__(self, param_steps=10, stepsize=0.5):
self.param_steps = param_steps
self.stepsize = stepsize
@staticmethod
def _circuit(params, gates, initial_circuit):
"""Append parametrized gates to an existing circuit.
Args:
params (array[float]): parameters of the gates to be added
gates (list[Operator]): list of the gates to be added
initial_circuit (function): user-defined circuit that returns an expectation value
Returns:
function: user-defined circuit with appended gates
"""
final_circuit = append_gate(initial_circuit, params, gates)
return final_circuit()
[docs] def step(self, circuit, operator_pool, params_zero=True):
r"""Update the circuit with one step of the optimizer.
Args:
circuit (.QNode): user-defined circuit returning an expectation value
operator_pool (list[Operator]): list of the gates to be used for adaptive optimization
params_zero (bool): flag to initiate circuit parameters at zero
Returns:
.QNode: the optimized circuit
"""
return self.step_and_cost(circuit, operator_pool, params_zero=params_zero)[0]
[docs] def step_and_cost(self, circuit, operator_pool, drain_pool=False, params_zero=True):
r"""Update the circuit with one step of the optimizer, return the corresponding
objective function value prior to the step, and return the maximum gradient
Args:
circuit (.QNode): user-defined circuit returning an expectation value
operator_pool (list[Operator]): list of the gates to be used for adaptive optimization
drain_pool (bool): flag to remove selected gates from the operator pool
params_zero (bool): flag to initiate circuit parameters at zero
Returns:
tuple[.QNode, float, float]: the optimized circuit, the objective function output prior
to the step, and the largest gradient
"""
cost = circuit()
qnode = copy.copy(circuit)
if drain_pool:
operator_pool = [
gate
for gate in operator_pool
if all(
gate.name != operation.name or gate.wires != operation.wires
for operation in circuit.tape.operations
)
]
params = pnp.array([gate.parameters[0] for gate in operator_pool], requires_grad=True)
qnode.func = self._circuit
grads = qml.grad(qnode)(params, gates=operator_pool, initial_circuit=circuit.func)
selected_gates = [operator_pool[pnp.argmax(abs(grads))]]
optimizer = qml.GradientDescentOptimizer(stepsize=self.stepsize)
if params_zero:
params = pnp.zeros(len(selected_gates))
else:
params = pnp.array([gate.parameters[0] for gate in selected_gates], requires_grad=True)
for _ in range(self.param_steps):
params, _ = optimizer.step_and_cost(
qnode, params, gates=selected_gates, initial_circuit=circuit.func
)
qnode.func = append_gate(circuit.func, params, selected_gates)
return qnode, cost, max(abs(qml.math.toarray(grads)))
_modules/pennylane/optimize/adaptive
Download Python script
Download Notebook
View on GitHub