-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcircuit_gradients.py
156 lines (121 loc) · 5.83 KB
/
circuit_gradients.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
"""A class to compute gradients of expectation values."""
from functools import reduce
from qiskit.circuit import QuantumCircuit
from .split_circuit import split
from .gradient_lookup import analytic_gradient
class StateGradient:
"""A class to compute gradients of expectation values."""
def __init__(self, operator, ansatz, state_in, target_parameters=None):
"""
Args:
operator (OperatorBase): The operator in the expectation value.
ansatz (QuantumCircuit): The ansatz in the expecation value.
state_in (Statevector): The initial, unparameterized state, upon which the ansatz acts.
target_parameters (List[Parameter]): The parameters with respect to which to derive.
If None, the derivative for all parameters is computed (also bound parameters!).
"""
self.operator = operator
self.ansatz = ansatz
self.state_in = state_in
self.target_parameters = target_parameters
if isinstance(ansatz, QuantumCircuit):
if target_parameters is None:
self.unitaries = split(ansatz)
self.paramlist = None
else:
self.unitaries, self.paramlist = split(ansatz, target_parameters,
separate_parameterized_gates=False,
return_parameters=True)
elif isinstance(ansatz, list):
self.unitaries = ansatz
self.paramlist = None
else:
raise NotImplementedError('Unsupported type of ansatz.')
def reference_gradients(self, parameter_binds=None, return_parameters=False):
op, ansatz, init = self.operator, self.ansatz, self.state_in
unitaries, paramlist = self.unitaries, self.paramlist
num_parameters = len(unitaries)
if paramlist is not None and parameter_binds is None:
raise ValueError('If you compute the gradients with respect to a ansatz with free '
'parameters, you must pass a dictionary of parameter binds.')
if parameter_binds is None:
parameter_binds = {}
paramlist = [[None]] * num_parameters
else:
ansatz = _bind(self.ansatz, parameter_binds)
bound_unitaries = _bind(unitaries, parameter_binds)
# lam = reduce(lambda x, y: x.evolve(y), ulist, self.state_in).evolve(self.operator)
lam = self.state_in.evolve(ansatz).evolve(op)
grads = []
for j in range(num_parameters):
grad = 0
deriv = analytic_gradient(unitaries[j], paramlist[j][0])
for _, gate in deriv:
_bind(gate, parameter_binds, inplace=True)
for coeff, gate in deriv:
dj_unitaries = bound_unitaries[:max(0, j)] + [gate] \
+ bound_unitaries[min(num_parameters, j + 1):]
phi = reduce(lambda x, y: x.evolve(y), dj_unitaries, init)
grad += coeff * lam.conjugate().data.dot(phi.data)
grads += [2 * grad.real]
if parameter_binds == {}:
return grads
accumulated, unique_params = self._accumulate_product_rule(grads)
if return_parameters:
return accumulated, unique_params
return accumulated
def iterative_gradients(self, parameter_binds=None, return_parameters=False):
op, ansatz, init = self.operator, self.ansatz, self.state_in
ulist, paramlist = self.unitaries, self.paramlist
num_parameters = len(ulist)
if paramlist is not None and parameter_binds is None:
raise ValueError('If you compute the gradients with respect to a ansatz with free '
'parameters, you must pass a dictionary of parameter binds.')
if parameter_binds is None:
parameter_binds = {}
paramlist = [[None]] * num_parameters
else:
ansatz = _bind(ansatz, parameter_binds)
phi = init.evolve(ansatz)
lam = phi.evolve(op)
grads = []
for j in reversed(range(num_parameters)):
uj = ulist[j]
deriv = analytic_gradient(uj, paramlist[j][0])
for _, gate in deriv:
_bind(gate, parameter_binds, inplace=True)
uj_dagger = _bind(uj, parameter_binds).inverse()
phi = phi.evolve(uj_dagger)
# TODO use projection
grad = 2 * sum(coeff * lam.conjugate().data.dot(phi.evolve(gate).data)
for coeff, gate in deriv).real
grads += [grad]
if j > 0:
lam = lam.evolve(uj_dagger)
if parameter_binds == {}:
return list(reversed(grads))
accumulated, unique_params = self._accumulate_product_rule(
list(reversed(grads)))
if return_parameters:
return accumulated, unique_params
return accumulated
def _accumulate_product_rule(self, gradients):
grads = {}
for paramlist, grad in zip(self.paramlist, gradients):
# all our gates only have one single parameter
param = paramlist[0]
grads[param] = grads.get(param, 0) + grad
return list(grads.values()), list(grads.keys())
# pylint: disable=inconsistent-return-statements
def _bind(circuits, parameter_binds, inplace=False):
if not isinstance(circuits, list):
existing_parameter_binds = {p: parameter_binds[p] for p in circuits.parameters}
return circuits.assign_parameters(existing_parameter_binds, inplace=inplace)
bound = []
for circuit in circuits:
existing_parameter_binds = {
p: parameter_binds[p] for p in circuit.parameters}
bound.append(circuit.assign_parameters(
existing_parameter_binds, inplace=inplace))
if not inplace:
return bound