Skip to content

Commit 006a2e3

Browse files
Refactor [NoiseModelFrom]NoiseProperties (quantumlib#4866)
* Update NoiseProperties class * Update calibration-to-noise experiment * Align with naming conventions * Split off SuperconductingQubitNoiseProperties * Two-qubit pauli err from calibrations * Format fix * Resolve trailing TODOs * Reduce to noise_properties. * Review comments * Document (non)virtual behaviors. * Align with moment move Co-authored-by: Cirq Bot <[email protected]>
1 parent 090112c commit 006a2e3

File tree

5 files changed

+164
-534
lines changed

5 files changed

+164
-534
lines changed

cirq/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@
9393
NO_NOISE,
9494
NOISE_MODEL_LIKE,
9595
NoiseModel,
96+
NoiseModelFromNoiseProperties,
97+
NoiseProperties,
9698
OpIdentifier,
9799
SymmetricalQidPair,
98100
UNCONSTRAINED_DEVICE,

cirq/devices/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
ThermalNoiseModel,
6363
)
6464

65+
from cirq.devices.noise_properties import (
66+
NoiseModelFromNoiseProperties,
67+
NoiseProperties,
68+
)
69+
6570
from cirq.devices.noise_utils import (
6671
OpIdentifier,
6772
decay_constant_to_xeb_fidelity,

cirq/devices/noise_properties.py

+107-249
Original file line numberDiff line numberDiff line change
@@ -1,227 +1,44 @@
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")
731

832
if TYPE_CHECKING:
9-
from typing import Iterable
1033
import cirq
1134

1235

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."""
2538

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."""
22542

22643

22744
class NoiseModelFromNoiseProperties(devices.NoiseModel):
@@ -234,37 +51,78 @@ def __init__(self, noise_properties: NoiseProperties) -> None:
23451
Raises:
23552
ValueError: if no NoiseProperties object is specified.
23653
"""
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()
24156

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.
24659
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.
26462
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

Comments
 (0)