1
- # pylint: disable=wrong-or-nonexistent-copyright-notice
2
- import warnings
3
- from typing import Sequence , TYPE_CHECKING , List
4
- from itertools import product
5
- from cirq import circuits , ops , protocols , devices
6
- import numpy as np
1
+ # Copyright 2021 The Cirq Developers
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
+ # https://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
+
15
+ """Classes for representing device noise.
16
+
17
+ NoiseProperties is an abstract class for capturing metrics of a device that can
18
+ be translated into noise models. NoiseModelFromNoiseProperties consumes those
19
+ noise models to produce a single noise model which replicates device noise.
20
+ """
21
+
22
+ import abc
23
+ from typing import Iterable , Sequence , TYPE_CHECKING , List
24
+
25
+ from cirq import _import , ops , protocols , devices
26
+ from cirq .devices .noise_utils import (
27
+ PHYSICAL_GATE_TAG ,
28
+ )
29
+
30
+ circuits = _import .LazyLoader ("circuits" , globals (), "cirq.circuits.circuit" )
7
31
8
32
if TYPE_CHECKING :
9
- from typing import Iterable
10
33
import cirq
11
34
12
35
13
- class NoiseProperties :
14
- def __init__ (
15
- self ,
16
- * ,
17
- t1_ns : float = None ,
18
- decay_constant : float = None ,
19
- xeb_fidelity : float = None ,
20
- pauli_error : float = None ,
21
- p00 : float = None ,
22
- p11 : float = None ,
23
- ) -> None :
24
- """Creates a NoiseProperties object using the provided metrics.
36
+ class NoiseProperties (abc .ABC ):
37
+ """Noise-defining properties for a quantum device."""
25
38
26
- Only one of decay_constant, xeb_fidelity, and pauli_error should be specified.
27
-
28
- Args:
29
- t1_ns: t1 decay constant in ns
30
- decay_constant: depolarization decay constant
31
- xeb_fidelity: 2-qubit XEB Fidelity
32
- pauli_error: total Pauli error
33
- p00: probability of qubit initialized as zero being measured as zero
34
- p11: probability of qubit initialized as one being measured as one
35
-
36
- Raises:
37
- ValueError: if no metrics are specified
38
- ValueError: if xeb fidelity, pauli error, p00, or p00 are less than 0 or greater than 1
39
- ValueError: if more than one of pauli error, xeb fidelity, or decay constant is specified
40
- """
41
- if not any ([t1_ns , decay_constant , xeb_fidelity , pauli_error , p00 , p11 ]):
42
- raise ValueError ('At least one metric must be specified' )
43
-
44
- for metric in [xeb_fidelity , pauli_error , p00 , p11 ]:
45
- if metric is not None and not 0.0 <= metric <= 1.0 :
46
- raise ValueError ('xeb, pauli error, p00, and p11 must be between 0 and 1' )
47
-
48
- if (
49
- np .count_nonzero (
50
- [metric is not None for metric in [xeb_fidelity , pauli_error , decay_constant ]]
51
- )
52
- > 1
53
- ):
54
- raise ValueError (
55
- 'Only one of xeb fidelity, pauli error, or decay constant should be defined'
56
- )
57
-
58
- self ._t1_ns = t1_ns
59
- self ._p = decay_constant
60
- self ._p00 = p00
61
- self ._p11 = p11
62
-
63
- if pauli_error is not None :
64
- self ._p = self .pauli_error_to_decay_constant (pauli_error )
65
- elif xeb_fidelity is not None :
66
- self ._p = self .xeb_fidelity_to_decay_constant (xeb_fidelity )
67
-
68
- @property
69
- def decay_constant (self ):
70
- return self ._p
71
-
72
- @property
73
- def p00 (self ):
74
- return self ._p00
75
-
76
- @property
77
- def p11 (self ):
78
- return self ._p11
79
-
80
- @property
81
- def pauli_error (self ):
82
- return self .decay_constant_to_pauli_error ()
83
-
84
- @property
85
- def t1_ns (self ):
86
- return self ._t1_ns
87
-
88
- @property
89
- def xeb (self ):
90
- return self .decay_constant_to_xeb_fidelity ()
91
-
92
- def decay_constant_to_xeb_fidelity (self , num_qubits : int = 2 ):
93
- """Calculates the XEB fidelity from the depolarization decay constant.
94
-
95
- Args:
96
- num_qubits: number of qubits
97
- """
98
- if self ._p is not None :
99
- N = 2 ** num_qubits
100
- return 1 - ((1 - self ._p ) * (1 - 1 / N ))
101
- return None
102
-
103
- def decay_constant_to_pauli_error (self , num_qubits : int = 1 ):
104
- """Calculates pauli error from the depolarization decay constant.
105
- Args:
106
- num_qubits: number of qubits
107
- """
108
- if self ._p is not None :
109
- N = 2 ** num_qubits
110
- return (1 - self ._p ) * (1 - 1 / N / N )
111
- return None
112
-
113
- def pauli_error_to_decay_constant (self , pauli_error : float , num_qubits : int = 1 ):
114
- """Calculates depolarization decay constant from pauli error.
115
-
116
- Args:
117
- pauli_error: The pauli error
118
- num_qubits: Number of qubits
119
- """
120
- N = 2 ** num_qubits
121
- return 1 - (pauli_error / (1 - 1 / N / N ))
122
-
123
- def xeb_fidelity_to_decay_constant (self , xeb_fidelity : float , num_qubits : int = 2 ):
124
- """Calculates the depolarization decay constant from the XEB noise_properties.
125
-
126
- Args:
127
- xeb_fidelity: The XEB noise_properties
128
- num_qubits: Number of qubits
129
- """
130
- N = 2 ** num_qubits
131
- return 1 - (1 - xeb_fidelity ) / (1 - 1 / N )
132
-
133
- def pauli_error_from_t1 (self , t : float , t1_ns : float ):
134
- """Calculates the pauli error from amplitude damping.
135
- Unlike the other methods, this computes a specific case (over time t).
136
-
137
- Args:
138
- t: the duration of the gate
139
- t1_ns: the t1 decay constant in ns
140
- """
141
- t2 = 2 * t1_ns
142
- return (1 - np .exp (- t / t2 )) / 2 + (1 - np .exp (- t / t1_ns )) / 4
143
-
144
- def pauli_error_from_depolarization (self , t : float ):
145
- """Calculates the amount of pauli error from depolarization.
146
- Unlike the other methods, this computes a specific case (over time t).
147
-
148
- If pauli error from t1 decay is more than total pauli error, just return the pauli error.
149
-
150
- Args:
151
- t: the duration of the gate
152
- """
153
- if self .t1_ns is not None :
154
- pauli_error_from_t1 = self .pauli_error_from_t1 (t , self .t1_ns )
155
- if self .pauli_error >= pauli_error_from_t1 :
156
- return self .pauli_error - pauli_error_from_t1
157
- else :
158
- warnings .warn (
159
- "Pauli error from T1 decay is greater than total Pauli error" , RuntimeWarning
160
- )
161
- return self .pauli_error
162
-
163
- def average_error (self , num_qubits : int = 1 ):
164
- """Calculates the average error from the depolarization decay constant.
165
-
166
- Args:
167
- num_qubits: the number of qubits
168
- """
169
- if self ._p is not None :
170
- N = 2 ** num_qubits
171
- return (1 - self ._p ) * (1 - 1 / N )
172
- return None
173
-
174
-
175
- def get_duration_ns (gate ):
176
- # Gate durations based on sycamore durations.
177
- # TODO: pull the gate durations from cirq_google
178
- # or allow users to pass them in
179
- if isinstance (gate , ops .FSimGate ):
180
- theta , _ = gate ._value_equality_values_ ()
181
- if np .abs (theta ) % (np .pi / 2 ) == 0 :
182
- return 12.0
183
- return 32.0
184
- elif isinstance (gate , ops .ISwapPowGate ):
185
- return 32.0
186
- elif isinstance (gate , ops .ZPowGate ):
187
- return 0.0
188
- elif isinstance (gate , ops .MeasurementGate ):
189
- return 4000.0
190
- elif isinstance (gate , ops .WaitGate ):
191
- return gate .duration .total_nanos ()
192
- return 25.0
193
-
194
-
195
- def _apply_readout_noise (p00 , p11 , moments , measurement_qubits ):
196
- if p00 is None :
197
- p = 1.0
198
- gamma = p11
199
- elif p11 is None :
200
- p = 0.0
201
- gamma = p00
202
- else :
203
- p = p11 / (p00 + p11 )
204
- gamma = p11 / p
205
- moments .append (
206
- circuits .Moment (
207
- ops .GeneralizedAmplitudeDampingChannel (p = p , gamma = gamma )(q ) for q in measurement_qubits
208
- )
209
- )
210
-
211
-
212
- def _apply_depol_noise (pauli_error , moments , system_qubits ):
213
-
214
- _sq_inds = np .arange (4 )
215
- pauli_inds = np .array (list (product (_sq_inds , repeat = 1 )))
216
- num_inds = len (pauli_inds )
217
- p_other = pauli_error / (num_inds - 1 ) # probability of X, Y, Z gates
218
- moments .append (circuits .Moment (ops .depolarize (p_other )(q ) for q in system_qubits ))
219
-
220
-
221
- def _apply_amplitude_damp_noise (duration , t1 , moments , system_qubits ):
222
- moments .append (
223
- circuits .Moment (ops .amplitude_damp (1 - np .exp (- duration / t1 )).on_each (system_qubits ))
224
- )
39
+ @abc .abstractmethod
40
+ def build_noise_models (self ) -> List ['cirq.NoiseModel' ]:
41
+ """Construct all NoiseModels associated with this NoiseProperties."""
225
42
226
43
227
44
class NoiseModelFromNoiseProperties (devices .NoiseModel ):
@@ -234,37 +51,78 @@ def __init__(self, noise_properties: NoiseProperties) -> None:
234
51
Raises:
235
52
ValueError: if no NoiseProperties object is specified.
236
53
"""
237
- if noise_properties is not None :
238
- self ._noise_properties = noise_properties
239
- else :
240
- raise ValueError ('A NoiseProperties object must be specified' )
54
+ self ._noise_properties = noise_properties
55
+ self .noise_models = self ._noise_properties .build_noise_models ()
241
56
242
- def noisy_moment (
243
- self , moment : circuits .Moment , system_qubits : Sequence ['cirq.Qid' ]
244
- ) -> 'cirq.OP_TREE' :
245
- moments : List [circuits .Moment ] = []
57
+ def virtual_predicate (self , op : 'cirq.Operation' ) -> bool :
58
+ """Returns True if an operation is virtual.
246
59
247
- if any (
248
- [protocols .is_measurement (op .gate ) for op in moment .operations ]
249
- ): # Add readout error before measurement gate
250
- p00 = self ._noise_properties .p00
251
- p11 = self ._noise_properties .p11
252
- measurement_qubits = [
253
- list (op .qubits )[0 ] for op in moment .operations if protocols .is_measurement (op .gate )
254
- ]
255
- if p00 is not None or p11 is not None :
256
- _apply_readout_noise (p00 , p11 , moments , measurement_qubits )
257
- moments .append (moment )
258
- else :
259
- moments .append (moment )
260
- if self ._noise_properties .pauli_error is not None : # Add depolarization error#
261
- duration = max ([get_duration_ns (op .gate ) for op in moment .operations ])
262
- pauli_error = self ._noise_properties .pauli_error_from_depolarization (duration )
263
- _apply_depol_noise (pauli_error , moments , system_qubits )
60
+ Device-specific subclasses should implement this method to mark any
61
+ operations which their device handles outside the quantum hardware.
264
62
265
- if self ._noise_properties .t1_ns is not None : # Add amplitude damping noise
266
- duration = max ([get_duration_ns (op .gate ) for op in moment .operations ])
267
- _apply_amplitude_damp_noise (
268
- duration , self ._noise_properties .t1_ns , moments , system_qubits
269
- )
270
- return moments
63
+ Args:
64
+ op: an operation to check for virtual indicators.
65
+
66
+ Returns:
67
+ True if `op` is virtual.
68
+ """
69
+ return False
70
+
71
+ def noisy_moments (
72
+ self , moments : Iterable ['cirq.Moment' ], system_qubits : Sequence ['cirq.Qid' ]
73
+ ) -> Sequence ['cirq.OP_TREE' ]:
74
+ # Split multi-qubit measurements into single-qubit measurements.
75
+ # These will be recombined after noise is applied.
76
+ split_measure_moments = []
77
+ multi_measurements = {}
78
+ for moment in moments :
79
+ split_measure_ops = []
80
+ for op in moment :
81
+ if not protocols .is_measurement (op ):
82
+ split_measure_ops .append (op )
83
+ continue
84
+ m_key = protocols .measurement_key_obj (op )
85
+ multi_measurements [m_key ] = op
86
+ for q in op .qubits :
87
+ split_measure_ops .append (ops .measure (q , key = m_key ))
88
+ split_measure_moments .append (circuits .Moment (split_measure_ops ))
89
+
90
+ # Append PHYSICAL_GATE_TAG to non-virtual ops in the input circuit,
91
+ # using `self.virtual_predicate` to determine virtuality.
92
+ new_moments = []
93
+ for moment in split_measure_moments :
94
+ virtual_ops = {op for op in moment if self .virtual_predicate (op )}
95
+ physical_ops = [
96
+ op .with_tags (PHYSICAL_GATE_TAG ) for op in moment if op not in virtual_ops
97
+ ]
98
+ # Both physical and virtual operations remain in the circuit, but
99
+ # only ops with PHYSICAL_GATE_TAG will receive noise.
100
+ if virtual_ops :
101
+ # Only subclasses will trigger this case.
102
+ new_moments .append (circuits .Moment (virtual_ops )) # coverage: ignore
103
+ if physical_ops :
104
+ new_moments .append (circuits .Moment (physical_ops ))
105
+
106
+ split_measure_circuit = circuits .Circuit (new_moments )
107
+
108
+ # Add noise from each noise model. The PHYSICAL_GATE_TAGs added
109
+ # previously allow noise models to distinguish physical gates from
110
+ # those added by other noise models.
111
+ noisy_circuit = split_measure_circuit .copy ()
112
+ for model in self .noise_models :
113
+ noisy_circuit = noisy_circuit .with_noise (model )
114
+
115
+ # Recombine measurements.
116
+ final_moments = []
117
+ for moment in noisy_circuit :
118
+ combined_measure_ops = []
119
+ restore_keys = set ()
120
+ for op in moment :
121
+ if not protocols .is_measurement (op ):
122
+ combined_measure_ops .append (op )
123
+ continue
124
+ restore_keys .add (protocols .measurement_key_obj (op ))
125
+ for key in restore_keys :
126
+ combined_measure_ops .append (multi_measurements [key ])
127
+ final_moments .append (circuits .Moment (combined_measure_ops ))
128
+ return final_moments
0 commit comments