/
linear_amplitude_function.py
196 lines (152 loc) · 7.62 KB
/
linear_amplitude_function.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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2021.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.
"""A class implementing a (piecewise-) linear function on qubit amplitudes."""
from __future__ import annotations
import numpy as np
from qiskit.circuit import QuantumCircuit
from .piecewise_linear_pauli_rotations import PiecewiseLinearPauliRotations
class LinearAmplitudeFunction(QuantumCircuit):
r"""A circuit implementing a (piecewise) linear function on qubit amplitudes.
An amplitude function :math:`F` of a function :math:`f` is a mapping
.. math::
F|x\rangle|0\rangle = \sqrt{1 - \hat{f}(x)} |x\rangle|0\rangle + \sqrt{\hat{f}(x)}
|x\rangle|1\rangle.
for a function :math:`\hat{f}: \{ 0, ..., 2^n - 1 \} \rightarrow [0, 1]`, where
:math:`|x\rangle` is a :math:`n` qubit state.
This circuit implements :math:`F` for piecewise linear functions :math:`\hat{f}`.
In this case, the mapping :math:`F` can be approximately implemented using a Taylor expansion
and linearly controlled Pauli-Y rotations, see [1, 2] for more detail. This approximation
uses a ``rescaling_factor`` to determine the accuracy of the Taylor expansion.
In general, the function of interest :math:`f` is defined from some interval :math:`[a,b]`,
the ``domain`` to :math:`[c,d]`, the ``image``, instead of :math:`\{ 1, ..., N \}` to
:math:`[0, 1]`. Using an affine transformation we can rescale :math:`f` to :math:`\hat{f}`:
.. math::
\hat{f}(x) = \frac{f(\phi(x)) - c}{d - c}
with
.. math::
\phi(x) = a + \frac{b - a}{2^n - 1} x.
If :math:`f` is a piecewise linear function on :math:`m` intervals
:math:`[p_{i-1}, p_i], i \in \{1, ..., m\}` with slopes :math:`\alpha_i` and
offsets :math:`\beta_i` it can be written as
.. math::
f(x) = \sum_{i=1}^m 1_{[p_{i-1}, p_i]}(x) (\alpha_i x + \beta_i)
where :math:`1_{[a, b]}` is an indication function that is 1 if the argument is in the interval
:math:`[a, b]` and otherwise 0. The breakpoints :math:`p_i` can be specified by the
``breakpoints`` argument.
References:
[1]: Woerner, S., & Egger, D. J. (2018).
Quantum Risk Analysis.
`arXiv:1806.06893 <http://arxiv.org/abs/1806.06893>`_
[2]: Gacon, J., Zoufal, C., & Woerner, S. (2020).
Quantum-Enhanced Simulation-Based Optimization.
`arXiv:2005.10780 <http://arxiv.org/abs/2005.10780>`_
"""
def __init__(
self,
num_state_qubits: int,
slope: float | list[float],
offset: float | list[float],
domain: tuple[float, float],
image: tuple[float, float],
rescaling_factor: float = 1,
breakpoints: list[float] | None = None,
name: str = "F",
) -> None:
r"""
Args:
num_state_qubits: The number of qubits used to encode the variable :math:`x`.
slope: The slope of the linear function. Can be a list of slopes if it is a piecewise
linear function.
offset: The offset of the linear function. Can be a list of offsets if it is a piecewise
linear function.
domain: The domain of the function as tuple :math:`(x_\min{}, x_\max{})`.
image: The image of the function as tuple :math:`(f_\min{}, f_\max{})`.
rescaling_factor: The rescaling factor to adjust the accuracy in the Taylor
approximation.
breakpoints: The breakpoints if the function is piecewise linear. If None, the function
is not piecewise.
name: Name of the circuit.
"""
if not hasattr(slope, "__len__"):
slope = [slope]
if not hasattr(offset, "__len__"):
offset = [offset]
# ensure that the breakpoints include the first point of the domain
if breakpoints is None:
breakpoints = [domain[0]]
else:
if not np.isclose(breakpoints[0], domain[0]):
breakpoints = [domain[0]] + breakpoints
_check_sizes_match(slope, offset, breakpoints)
_check_sorted_and_in_range(breakpoints, domain)
self._domain = domain
self._image = image
self._rescaling_factor = rescaling_factor
# do rescalings
a, b = domain
c, d = image
mapped_breakpoints = []
mapped_slope = []
mapped_offset = []
for i, point in enumerate(breakpoints):
mapped_breakpoint = (point - a) / (b - a) * (2**num_state_qubits - 1)
mapped_breakpoints += [mapped_breakpoint]
# factor (upper - lower) / (2^n - 1) is for the scaling of x to [l,u]
# note that the +l for mapping to [l,u] is already included in
# the offsets given as parameters
mapped_slope += [slope[i] * (b - a) / (2**num_state_qubits - 1)]
mapped_offset += [offset[i]]
# approximate linear behavior by scaling and contracting around pi/4
slope_angles = np.zeros(len(breakpoints))
offset_angles = np.pi / 4 * (1 - rescaling_factor) * np.ones(len(breakpoints))
for i in range(len(breakpoints)):
slope_angles[i] = np.pi * rescaling_factor * mapped_slope[i] / 2 / (d - c)
offset_angles[i] += np.pi * rescaling_factor * (mapped_offset[i] - c) / 2 / (d - c)
# use PWLPauliRotations to implement the function
pwl_pauli_rotation = PiecewiseLinearPauliRotations(
num_state_qubits, mapped_breakpoints, 2 * slope_angles, 2 * offset_angles, name=name
)
super().__init__(*pwl_pauli_rotation.qregs, name=name)
self.append(pwl_pauli_rotation.to_gate(), self.qubits)
def post_processing(self, scaled_value: float) -> float:
r"""Map the function value of the approximated :math:`\hat{f}` to :math:`f`.
Args:
scaled_value: A function value from the Taylor expansion of :math:`\hat{f}(x)`.
Returns:
The ``scaled_value`` mapped back to the domain of :math:`f`, by first inverting
the transformation used for the Taylor approximation and then mapping back from
:math:`[0, 1]` to the original domain.
"""
# revert the mapping applied in the Taylor approximation
value = scaled_value - 1 / 2 + np.pi / 4 * self._rescaling_factor
value *= 2 / np.pi / self._rescaling_factor
# map the value from [0, 1] back to the original domain
value *= self._image[1] - self._image[0]
value += self._image[0]
return value
def _check_sorted_and_in_range(breakpoints, domain):
if breakpoints is None:
return
# check if sorted
if not np.all(np.diff(breakpoints) > 0):
raise ValueError("Breakpoints must be unique and sorted.")
if breakpoints[0] < domain[0] or breakpoints[-1] > domain[1]:
raise ValueError("Breakpoints must be included in domain.")
def _check_sizes_match(slope, offset, breakpoints):
size = len(slope)
if len(offset) != size:
raise ValueError(f"Size mismatch of slope ({size}) and offset ({len(offset)}).")
if breakpoints is not None:
if len(breakpoints) != size:
raise ValueError(
f"Size mismatch of slope ({size}) and breakpoints ({len(breakpoints)})."
)