-
Notifications
You must be signed in to change notification settings - Fork 364
/
noise_transformation.py
431 lines (364 loc) · 16 KB
/
noise_transformation.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
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
# This code is part of Qiskit.
#
# (C) Copyright IBM 2018, 2019.
#
# 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.
"""
Noise transformation module
The goal of this module is to transform one 1-qubit noise channel
(given by the QuantumError class) into another, built from specified
"building blocks" (given as Kraus matrices) such that the new channel is
as close as possible to the original one in the Hilber-Schmidt metric.
For a typical use case, consider a simulator for circuits built from the
Clifford group. Computations on such circuits can be simulated at
polynomial time and space, but not all noise channels can be used in such
a simulation. To enable noisy Clifford simulation one can transform the
given noise channel into the closest one, Hilbert-Schmidt wise, that can be
used in a Clifford simulator.
"""
# pylint: disable=import-outside-toplevel
import copy
import logging
from typing import Sequence, List, Union, Callable
import numpy as np
from qiskit.circuit import Reset
from qiskit.circuit.library.standard_gates import (
IGate,
XGate,
YGate,
ZGate,
HGate,
SGate,
SdgGate,
)
from qiskit.compiler import transpile
from qiskit.exceptions import MissingOptionalLibraryError
from qiskit.quantum_info.operators.channel import Kraus, SuperOp, Chi
from qiskit.quantum_info.operators.channel.quantum_channel import QuantumChannel
from qiskit.transpiler.exceptions import TranspilerError
from ..noise.errors import QuantumError
from ..noise.noise_model import NoiseModel
from ..noise.noiseerror import NoiseError
logger = logging.getLogger(__name__)
def transform_noise_model(noise_model: NoiseModel, func: Callable) -> NoiseModel:
"""Return a new noise model by applyign a function to all quantum errors.
This returns a new noise model containing the resulting errors from
applying the supplied function to all QuantumErrors in the noise model.
This function should have singature `func(error: QuantumError) -> QuantumError`
where the returned quantum error is defined on the same number of qubits
as the original error.
Args:
noise_model: the noise model to be transformed.
func: function for transforming QuantumErrors.
Returns:
The transpiled noise model.
Raises:
NoiseError: if the transformation failed.
"""
# Make a deep copy of the noise model so we can update its
# internal dicts without affecting the original model
new_noise = copy.deepcopy(noise_model)
for key, error in new_noise._default_quantum_errors.items():
new_noise._default_quantum_errors[key] = func(error)
for inst_name, noise_dic in new_noise._local_quantum_errors.items():
for qubits, error in noise_dic.items():
new_noise._local_quantum_errors[inst_name][qubits] = func(error)
return new_noise
def transpile_quantum_error(error: QuantumError, **transpile_kwargs) -> QuantumError:
"""Return a new quantum error containin transpiled circuits.
This returns a new QuantumError containing the circuits resulting from
transpiling all error circuits using :func:`qiskit.transpile` with the
passed keyword agruments.
Args:
error: the quantum error to be transformed.
transpile_kwargs: kwargs for passing to qiskit transpile function.
Returns:
The transformed quantum error.
Raises:
NoiseError: if the transformation failed.
"""
try:
transpiled_circs = transpile(error.circuits, **transpile_kwargs)
except TranspilerError as err:
raise NoiseError(
f"Failed to transpile circuits in {error} with kwargs {transpile_kwargs}"
) from err
return QuantumError(zip(transpiled_circs, error.probabilities))
def transpile_noise_model(noise_model: NoiseModel, **transpile_kwargs) -> NoiseModel:
"""Return a new noise model with transpiled QuantumErrors.
This returns a new noise model containing the resulting errors from
transpiling all QuantumErrors in the noise model
using :func:`transpile_quantum_error` function with the passed
keyword agruments.
Args:
noise_model: the noise model to be transformed.
transpile_kwargs: kwargs for passing to qiskit transpile function.
Returns:
The transpiled noise model.
Raises:
NoiseError: if the transformation failed.
"""
def func(error):
return transpile_quantum_error(error, **transpile_kwargs)
return transform_noise_model(noise_model, func)
def approximate_quantum_error(
error, *, operator_string=None, operator_dict=None, operator_list=None
):
r"""
Return a ``QuantumError`` object that approximates an error
as a mixture of specified operators (channels).
The approximation is done by minimizing the Hilbert-Schmidt distance
between the process matrix of the target error channel (:math:`T`) and
the process matrix of the output channel (:math:`S = \sum_i{p_i S_i}`),
i.e. :math:`Tr[(T-S)^\dagger (T-S)]`,
where
:math:`[p_1, p_2, ..., p_n]` denote probabilities and
:math:`[S_1, S_2, ..., S_n]` denote basis operators (channels).
See `arXiv:1207.0046 <http://arxiv.org/abs/1207.0046>`_ for the details.
Args:
error (QuantumError or QuantumChannel): the error to be approximated.
The number of qubits must be 1 or 2.
operator_string (string): a name for a pre-made set of
building blocks for the output channel (Default: None).
Possible values are ``'pauli'``, ``'reset'``, ``'clifford'``.
operator_dict (dict): a dictionary whose values are the
building blocks for the output channel (Default: None).
E.g. {"x": XGate(), "y": YGate()}, keys "x" and "y"
are not used in transformation.
operator_list (list): list of building block operators for the
output channel (Default: None). E.g. [XGate(), YGate()]
Returns:
QuantumError: the approximate quantum error.
Raises:
NoiseError: if any invalid argument is specified or approximation failed.
MissingOptionalLibraryError: if cvxpy is not installed.
Note:
The operator input precedence is: ``list`` < ``dict`` < ``string``.
If a ``string`` is given, ``dict`` is overwritten;
if a ``dict`` is given, ``list`` is overwritten.
The ``string`` supports only 1- or 2-qubit errors and
its possible values are ``'pauli'``, ``'reset'``, ``'clifford'``.
The ``'clifford'`` does not support 2-qubit errors.
"""
if not isinstance(error, (QuantumError, QuantumChannel)):
raise NoiseError(f"Invalid input error type: {error.__class__.__name__}")
if error.num_qubits > 2:
raise NoiseError(
"Only 1-qubit and 2-qubit noises can be converted, "
f"{error.num_qubits}-qubit noise found in model"
)
if operator_string is not None:
valid_operator_strings = _PRESET_OPERATOR_TABLE.keys()
operator_string = operator_string.lower()
if operator_string not in valid_operator_strings:
raise NoiseError(
f"{operator_string} is not a valid operator_string. "
f"It must be one of {valid_operator_strings}"
)
try:
operator_list = _PRESET_OPERATOR_TABLE[operator_string][error.num_qubits]
except KeyError as err:
raise NoiseError(
f"Preset '{operator_string}' operators do not support the "
f"approximation of errors with {error.num_qubits} qubits"
) from err
if operator_dict is not None:
_, operator_list = zip(*operator_dict.items())
if operator_list is not None:
if not isinstance(operator_list, Sequence):
raise NoiseError(f"operator_list is not a sequence: {operator_list}")
try:
channel_list = [
op if isinstance(op, QuantumChannel) else QuantumError([(op, 1)])
for op in operator_list
] # preserve operator_list
except NoiseError as err:
raise NoiseError(f"Invalid type found in operator list: {operator_list}") from err
probabilities = _transform_by_operator_list(channel_list, error)[1:]
identity_prob = np.round(1 - sum(probabilities), 9)
if identity_prob < 0 or identity_prob > 1:
raise NoiseError(f"Channel probabilities sum to {1 - identity_prob}")
noise_ops = [((IGate(), [0]), identity_prob)]
for operator, probability in zip(operator_list, probabilities):
noise_ops.append((operator, probability))
return QuantumError(noise_ops)
raise NoiseError("Quantum error approximation failed - no approximating operators detected")
def approximate_noise_model(model, *, operator_string=None, operator_dict=None, operator_list=None):
"""
Replace all noises in a noise model with ones approximated
by a mixture of operators (channels).
Args:
model (NoiseModel): the noise model to be approximated.
All noises in the model must be 1- or 2-qubit noises.
operator_string (string): a name for a pre-made set of
building blocks for the output channel (Default: None).
Possible values are ``'pauli'``, ``'reset'``, ``'clifford'``.
operator_dict (dict): a dictionary whose values are the
building blocks for the output channel (Default: None).
E.g. {"x": XGate(), "y": YGate()}, keys "x" and "y"
are not used in transformation.
operator_list (list): list of building block operators for the
output channel (Default: None). E.g. [XGate(), YGate()]
Returns:
NoiseModel: the approximate noise model.
Raises:
NoiseError: if any invalid argument is specified or approximation failed.
MissingOptionalLibraryError: if cvxpy is not installed.
Note:
The operator input precedence is: ``list`` < ``dict`` < ``string``.
If a ``string`` is given, ``dict`` is overwritten;
if a ``dict`` is given, ``list`` is overwritten.
The ``string`` supports only 1- or 2-qubit errors and
its possible values are ``'pauli'``, ``'reset'``, ``'clifford'``.
The ``'clifford'`` does not support 2-qubit errors.
"""
def approximate(noise):
return approximate_quantum_error(
noise,
operator_string=operator_string,
operator_dict=operator_dict,
operator_list=operator_list,
)
return transform_noise_model(model, approximate)
# pauli operators
_PAULIS_Q0 = [[(IGate(), [0])], [(XGate(), [0])], [(YGate(), [0])], [(ZGate(), [0])]]
_PAULIS_Q1 = [[(IGate(), [1])], [(XGate(), [1])], [(YGate(), [1])], [(ZGate(), [1])]]
_PAULIS_Q0Q1 = [op_q0 + op_q1 for op_q0 in _PAULIS_Q0 for op_q1 in _PAULIS_Q1]
# reset operators
_RESET_Q0_TO_0 = [(Reset(), [0])]
_RESET_Q0_TO_1 = [(Reset(), [0]), (XGate(), [0])]
_RESET_Q0 = [[(IGate(), [0])], _RESET_Q0_TO_0, _RESET_Q0_TO_1]
_RESET_Q1_TO_0 = [(Reset(), [1])]
_RESET_Q1_TO_1 = [(Reset(), [1]), (XGate(), [1])]
_RESET_Q1 = [[(IGate(), [1])], _RESET_Q1_TO_0, _RESET_Q1_TO_1]
_RESET_Q0Q1 = [op_q0 + op_q1 for op_q0 in _RESET_Q0 for op_q1 in _RESET_Q1]
# clifford operators
_CLIFFORD_GATES = [
(IGate(),),
(SGate(),),
(SdgGate(),),
(ZGate(),),
# u2 gates
(HGate(),),
(HGate(), ZGate()),
(ZGate(), HGate()),
(HGate(), SGate()),
(SGate(), HGate()),
(HGate(), SdgGate()),
(SdgGate(), HGate()),
(SGate(), HGate(), SGate()),
(SdgGate(), HGate(), SGate()),
(ZGate(), HGate(), SGate()),
(SGate(), HGate(), SdgGate()),
(SdgGate(), HGate(), SdgGate()),
(ZGate(), HGate(), SdgGate()),
(SGate(), HGate(), ZGate()),
(SdgGate(), HGate(), ZGate()),
(ZGate(), HGate(), ZGate()),
# u3 gates
(XGate(),),
(YGate(),),
(SGate(), XGate()),
(SdgGate(), XGate()),
]
# preset operator table
_PRESET_OPERATOR_TABLE = {
"pauli": {
1: _PAULIS_Q0[1:],
2: _PAULIS_Q0Q1[1:],
},
"reset": {
1: _RESET_Q0[1:],
2: _RESET_Q0Q1[1:],
},
"clifford": {
1: [[(gate, [0]) for gate in _CLIFFORD_GATES[j]] for j in range(1, 24)],
},
}
def _transform_by_operator_list(
basis_ops: Sequence[Union[QuantumChannel, QuantumError]],
target: Union[QuantumChannel, QuantumError],
) -> List[float]:
r"""
Transform (or approximate) the target quantum channel into a mixture of
basis operators (channels) and return the mixing probabilities.
The approximate channel is found by minimizing the Hilbert-Schmidt
distance between the process matrix of the target channel (:math:`T`) and
the process matrix of the output channel (:math:`S = \sum_i{p_i S_i}`),
i.e. :math:`Tr[(T-S)^\dagger (T-S)]`,
where
:math:`[p_1, p_2, ..., p_n]` denote probabilities and
:math:`[S_1, S_2, ..., S_n]` denote basis operators (channels).
Such an optimization can represented as a quadratic program:
Minimize :math:`p^T A p + b^T p`,
subject to :math:`p \ge 0`, `\sum_i{p_i} = 1`, `\sum_i{p_i} = 1`.
The last constraint is for making the approximation conservative by
forcing the output channel have more error than the target channel
in the sense that a "fidelity" against identity channel should be worse.
See `arXiv:1207.0046 <http://arxiv.org/abs/1207.0046>`_ for the details.
Args:
target: Quantum channel to be transformed.
basis_ops: a list of channel objects constructing the output channel.
Returns:
list: A list of amplitudes (probabilities) of basis that define the output channel.
Raises:
MissingOptionalLibraryError: if cvxpy is not installed.
"""
# pylint: disable=invalid-name
N = 2 ** basis_ops[0].num_qubits
# add identity channel in front
basis_ops = [Kraus(np.eye(N))] + basis_ops
# create objective function coefficients
basis_ops_mats = [Chi(op).data for op in basis_ops]
T = Chi(target).data
n = len(basis_ops_mats)
A = np.zeros((n, n))
for i, S_i in enumerate(basis_ops_mats):
for j, S_j in enumerate(basis_ops_mats):
# A[i][j] = 1/2 * {Tr(S_i^† S_j) - Tr(S_j^† S_i)} = Re[Tr(S_i^† S_j)]
if i < j:
A[i][j] = _hs_inner_prod_real(S_i, S_j)
elif i > j:
A[i][j] = A[j][i]
else: # i==j
A[i][i] = _hs_norm(S_i)
b = -2 * np.array([_hs_inner_prod_real(T, S_i) for S_i in basis_ops_mats])
# c = _hs_norm(T)
# create honesty constraint coefficients
def fidelity(channel): # fidelity w.r.t. identity omitting the N^-2 factor
return float(np.abs(np.trace(SuperOp(channel))))
source_fids = np.array([fidelity(op) for op in basis_ops])
target_fid = fidelity(target)
try:
import cvxpy
except ImportError as err:
logger.error("cvxpy module needs to be installed to use this feature.")
raise MissingOptionalLibraryError(
libname="cvxpy",
name="Transformation/Approximation of noise",
pip_install="pip install cvxpy",
msg="CVXPY is required to solve an optimization problem of"
" approximating a noise channel.",
) from err
# create quadratic program
x = cvxpy.Variable(n)
prob = cvxpy.Problem(
cvxpy.Minimize(cvxpy.quad_form(x, A) + b.T @ x),
constraints=[cvxpy.sum(x) <= 1, x >= 0, source_fids.T @ x <= target_fid],
)
# solve quadratic program
prob.solve()
probabilities = x.value
return probabilities
def _hs_norm(A): # pylint: disable=invalid-name
# Tr(A^† A)
return np.trace(np.conj(A).T @ A).real
def _hs_inner_prod_real(A, B): # pylint: disable=invalid-name
# Re[Tr(A^† B)]
return np.trace(np.conj(A.T) @ B).real