-
Notifications
You must be signed in to change notification settings - Fork 368
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add transpiler passes for adding instruction-dependent noises #1391
Changes from 11 commits
996f8a8
66d828c
322a57c
c6fb4bc
6a336e3
2e7ee22
37e8e65
3f6239a
631108d
5342a88
2780f3c
435be1b
053130f
1672eb0
5753075
d478674
33ef087
c692a89
e959be8
6a08aef
106b261
ede0bf6
7325941
cf1e2ed
de40cbd
c40de14
b1a27cc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,18 +15,21 @@ | |
|
||
import json | ||
import logging | ||
from typing import Optional | ||
from warnings import warn, catch_warnings, filterwarnings | ||
|
||
from numpy import ndarray | ||
|
||
from qiskit.circuit import Instruction | ||
from qiskit.providers import BaseBackend, Backend | ||
from qiskit.providers.models import BackendProperties | ||
from qiskit.transpiler import PassManager | ||
from .device.models import basic_device_gate_errors | ||
from .device.models import basic_device_readout_errors | ||
from .errors.quantum_error import QuantumError | ||
from .errors.readout_error import ReadoutError | ||
from .noiseerror import NoiseError | ||
from .passes import RelaxationNoisePass | ||
from ..backends.backend_utils import BASIS_GATES | ||
|
||
logger = logging.getLogger(__name__) | ||
|
@@ -182,6 +185,8 @@ def __init__(self, basis_gates=None): | |
# dict(str: ReadoutError) | ||
# where the dict keys are the gate qubits. | ||
self._local_readout_errors = {} | ||
# Custom noise passes | ||
self._custom_noise_passes = [] | ||
|
||
@property | ||
def basis_gates(self): | ||
|
@@ -208,7 +213,8 @@ def from_backend(cls, backend, | |
gate_lengths=None, | ||
gate_length_units='ns', | ||
standard_gates=None, | ||
warnings=True): | ||
warnings=True, | ||
delay_noise=False): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In principle we don't need a kwarg for delay noise, since it is a specific type of thermal relaxation noise and should always be added when thermal relaxation is added if delay is in the basis gates. Since delay isn't listed in backends basis gates, if we want to keep an explicit option it would be better to have this option simply be to add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with the change of the meaning of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No errors should be raised (if they are that is something that needs to change). The relaxation noise noise pass should not care if the input is a scheduled circuit or not. It only looks for the specified instructions (delay in this case) and add noise if present with a non-zero duration. Scheduling is an optional step you can do to add delays (and durations to other gates). If you don't schedule your circuit, your simulation will be exactly the same as before (unless you had manually added delays to your circuit). If you do schedule your circuit then you will get the extra delay noise (this should be added to API documentation for noise model, and basic device noise tutorial) In general the relaxation noise pass should also work on a non-scheduled circuit by doing nothing (because the instructions would have This reminds me that we need to add a release note explaining new additions. In particular it should have both a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand no errors should be raised for non-scheduled circuit. In 1672eb0, I've updated the relaxation noise pass so that it can add noises to gates as well as delays relying on |
||
"""Return a noise model derived from a devices backend properties. | ||
|
||
This function generates a noise model based on: | ||
|
@@ -289,6 +295,7 @@ def from_backend(cls, backend, | |
qobj gates. If false return as unitary | ||
qobj instructions (Default: None) | ||
warnings (bool): Display warnings (Default: True). | ||
delay_noise (bool): Include delay instructions in the noise model (Default: False). | ||
|
||
Returns: | ||
NoiseModel: An approximate noise model for the device backend. | ||
|
@@ -341,9 +348,18 @@ def from_backend(cls, backend, | |
warnings=warnings) | ||
for name, qubits, error in gate_errors: | ||
noise_model.add_quantum_error(error, name, qubits, warnings=warnings) | ||
# Add delay errors | ||
if thermal_relaxation and delay_noise: | ||
delay_pass = RelaxationNoisePass( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You need to convert the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done at c692a89 |
||
t1s=[backend.properties().t1(q) for q in range(backend.configuration().num_qubits)], | ||
t2s=[backend.properties().t2(q) for q in range(backend.configuration().num_qubits)], | ||
dt=backend.configuration().dt, | ||
ops="delay", | ||
itoko marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
noise_model._custom_noise_passes.append(delay_pass) | ||
return noise_model | ||
|
||
def is_ideal(self): | ||
def is_ideal(self): # pylint: disable=too-many-return-statements | ||
"""Return True if the noise model has no noise terms.""" | ||
# Get default errors | ||
if self._default_quantum_errors: | ||
|
@@ -356,6 +372,8 @@ def is_ideal(self): | |
return False | ||
if self._nonlocal_quantum_errors: | ||
return False | ||
if self._custom_noise_passes: | ||
return False | ||
return True | ||
|
||
def __repr__(self): | ||
|
@@ -1045,3 +1063,15 @@ def _nonlocal_quantum_errors_equal(self, other): | |
if iinner_dict1[iinner_key] != iinner_dict2[iinner_key]: | ||
return False | ||
return True | ||
|
||
def _pass_manager(self) -> Optional[PassManager]: | ||
""" | ||
Return the pass manager that add custom noises defined as noise passes | ||
(stored in the _custom_noise_passes field). Note that the pass manager | ||
does not include passes to add other noises (stored in the different field). | ||
""" | ||
passes = [] | ||
passes.extend(self._custom_noise_passes) | ||
if len(passes) > 0: | ||
return PassManager(passes) | ||
return None | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The fix was included in terra 0.19 that an empty pass manager just returns the input circuits without dag conversions. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# This code is part of Qiskit. | ||
# | ||
# (C) Copyright IBM 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. | ||
|
||
""" | ||
Passes for adding noises. | ||
""" | ||
|
||
from .local_noise_pass import LocalNoisePass | ||
from .relaxation_noise_pass import RelaxationNoisePass |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
# This code is part of Qiskit. | ||
# | ||
# (C) Copyright IBM 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. | ||
""" | ||
Local noise addition pass. | ||
""" | ||
from typing import Optional, Union, Sequence, Callable, Iterable | ||
|
||
from qiskit.circuit import Instruction | ||
from qiskit.dagcircuit import DAGCircuit | ||
from qiskit.transpiler import TransformationPass | ||
from qiskit.transpiler.exceptions import TranspilerError | ||
from ..errors import QuantumError | ||
|
||
InstructionLike = Union[Instruction, QuantumError] | ||
|
||
|
||
class LocalNoisePass(TransformationPass): | ||
"""Transpiler pass to insert noise into a circuit. | ||
|
||
The noise in this pass is defined by a noise function or callable with signature | ||
|
||
.. code:: python | ||
|
||
def fn( | ||
inst: Instruction, | ||
qubits: Optional[List[int]] = None | ||
) -> InstructionLike: | ||
|
||
For every instance of one of the reference instructions in a circuit the | ||
supplied function is called on that instruction and the returned noise | ||
is added to the circuit. This noise can depend on properties of the | ||
instruction it is called on (for example parameters or duration) to | ||
allow inserting parameterized noise models. | ||
|
||
Several methods for adding the constructed errors to circuits are supported | ||
and can be set by using the ``method`` kwarg. The supported methods are | ||
|
||
* ``"append"``: add the return of the callable after the instruction. | ||
* ``"prepend"``: add the return of the callable before the instruction. | ||
* ``"replace"``: replace the instruction with the return of the callable. | ||
|
||
""" | ||
|
||
def __init__( | ||
self, | ||
func: Callable[[Instruction, Sequence[int]], InstructionLike], | ||
ops: Optional[Union[Instruction, Iterable[Instruction]]] = None, | ||
method: str = 'append' | ||
): | ||
"""Initialize noise pass. | ||
|
||
Args: | ||
func: noise function `fn(inst, qubits) -> InstructionLike`. | ||
ops: Optional, single or list of instructions to apply the | ||
noise function to. If None the noise function will be | ||
applied to all instructions in the circuit. | ||
method: method for inserting noise. Allow methods are | ||
'append', 'prepend', 'replace'. | ||
Raises: | ||
TranspilerError: if an invalid option is specified. | ||
""" | ||
if method not in {"append", "prepend", "replace"}: | ||
raise TranspilerError( | ||
f'Invalid method: {method}, it must be "append", "prepend" or "replace"' | ||
) | ||
if isinstance(ops, str): | ||
ops = [ops] | ||
super().__init__() | ||
self._func = func | ||
self._ops = set(ops) if ops else {} | ||
self._method = method | ||
|
||
def run(self, dag: DAGCircuit) -> DAGCircuit: | ||
"""Run the LocalNoisePass pass on `dag`. | ||
Args: | ||
dag: DAG to be changed. | ||
Returns: | ||
A changed DAG. | ||
Raises: | ||
TranspilerError: if generated operation is not valid. | ||
""" | ||
qubit_indices = {qubit: idx for idx, qubit in enumerate(dag.qubits)} | ||
for node in dag.topological_op_nodes(): | ||
if self._ops and node.op.name not in self._ops: | ||
continue | ||
|
||
qubits = [qubit_indices[q] for q in node.qargs] | ||
new_op = self._func(node.op, qubits) | ||
if new_op is None: | ||
continue | ||
if not isinstance(new_op, Instruction): | ||
try: | ||
new_op = new_op.to_instruction() | ||
except AttributeError as att_err: | ||
raise TranspilerError( | ||
"Function must return an object implementing 'to_instruction' method." | ||
) from att_err | ||
if new_op.num_qubits != len(node.qargs): | ||
raise TranspilerError( | ||
f"Number of qubits of generated op {new_op.num_qubits} != " | ||
f"{len(node.qargs)} that of a reference op {node.name}" | ||
) | ||
|
||
new_dag = DAGCircuit() | ||
new_dag.add_qubits(node.qargs) | ||
new_dag.add_clbits(node.cargs) | ||
if self._method == "append": | ||
new_dag.apply_operation_back(node.op, qargs=node.qargs, cargs=node.cargs) | ||
new_dag.apply_operation_back(new_op, qargs=node.qargs) | ||
if self._method == "prepend": | ||
new_dag.apply_operation_back(node.op, qargs=node.qargs, cargs=node.cargs) | ||
|
||
dag.substitute_node_with_dag(node, new_dag) | ||
|
||
return dag |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,105 @@ | ||||||
# This code is part of Qiskit. | ||||||
# | ||||||
# (C) Copyright IBM 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. | ||||||
""" | ||||||
Thermal relaxation noise pass. | ||||||
""" | ||||||
from typing import Optional, Union, Sequence, List | ||||||
|
||||||
import numpy as np | ||||||
|
||||||
from qiskit.circuit import Instruction, QuantumCircuit | ||||||
from qiskit.dagcircuit import DAGCircuit | ||||||
from qiskit.transpiler.exceptions import TranspilerError | ||||||
from .local_noise_pass import LocalNoisePass | ||||||
from ..errors.standard_errors import thermal_relaxation_error | ||||||
|
||||||
|
||||||
class RelaxationNoisePass(LocalNoisePass): | ||||||
"""Add duration dependent thermal relaxation noise after instructions.""" | ||||||
|
||||||
def __init__( | ||||||
self, | ||||||
t1s: List[float], | ||||||
t2s: List[float], | ||||||
dt: float, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. General question: Are delay units in scheduled circuits always in terms of a backends dt? |
||||||
ops: Optional[Union[Instruction, Sequence[Instruction]]] = None, | ||||||
excited_state_populations: Optional[List[float]] = None, | ||||||
): | ||||||
"""Initialize RelaxationNoisePass. | ||||||
|
||||||
Args: | ||||||
t1s: List of T1 times in seconds for each qubit. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing I thought that would be nice is if this pass could work for an arbitrary number of qubits all with the same T1 and T2 by doing |
||||||
t2s: List of T2 times in seconds for each qubit. | ||||||
dt: ... | ||||||
ops: Optional, the operations to add relaxation to. If None | ||||||
relaxation will be added to all operations. | ||||||
excited_state_populations: Optional, list of excited state populations | ||||||
for each qubit at thermal equilibrium. If not supplied or obtained | ||||||
from the backend this will be set to 0 for each qubit. | ||||||
""" | ||||||
self._t1s = np.asarray(t1s) | ||||||
self._t2s = np.asarray(t2s) | ||||||
if excited_state_populations is not None: | ||||||
self._p1s = np.asarray(excited_state_populations) | ||||||
else: | ||||||
self._p1s = np.zeros(len(t1s)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
self._dt = dt | ||||||
super().__init__(self._thermal_relaxation_error, ops=ops, method="append") | ||||||
|
||||||
def _thermal_relaxation_error( | ||||||
self, | ||||||
op: Instruction, | ||||||
qubits: Sequence[int] | ||||||
): | ||||||
"""Return thermal relaxation error on each gate qubit""" | ||||||
duration = op.duration | ||||||
if duration == 0: | ||||||
itoko marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
return None | ||||||
|
||||||
# convert time unit in seconds | ||||||
duration = duration * self._dt | ||||||
|
||||||
t1s = self._t1s[qubits] | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For arbitrary number of qubits with same T1/T2s t1s = self._t1s[qubits] if self._t1s.shape else len(qubits) * [float(self._t1s)]
t2s = self._t2s[qubits] if self._t2s.shape else len(qubits) * [float(self._t2s)]
p1s = self._p1s[qubits] if self._p1s.shape else len(qubits) * [float(self._p1s)] |
||||||
t2s = self._t2s[qubits] | ||||||
p1s = self._p1s[qubits] | ||||||
|
||||||
# pylint: disable=invalid-name | ||||||
if op.num_qubits == 1: | ||||||
t1, t2, p1 = t1s[0], t2s[0], p1s[0] | ||||||
if t1 == np.inf and t2 == np.inf: | ||||||
return None | ||||||
return thermal_relaxation_error(t1, t2, duration, p1) | ||||||
|
||||||
# General multi-qubit case | ||||||
noise = QuantumCircuit(op.num_qubits) | ||||||
for qubit, (t1, t2, p1) in enumerate(zip(t1s, t2s, p1s)): | ||||||
if t1 == np.inf and t2 == np.inf: | ||||||
# No relaxation on this qubit | ||||||
continue | ||||||
error = thermal_relaxation_error(t1, t2, duration, p1) | ||||||
noise.append(error, [qubit]) | ||||||
|
||||||
return noise | ||||||
|
||||||
def run(self, dag: DAGCircuit) -> DAGCircuit: | ||||||
"""Run the RelaxationNoisePass pass on `dag`. | ||||||
Args: | ||||||
dag: DAG to be changed. | ||||||
Returns: | ||||||
A changed DAG. | ||||||
Raises: | ||||||
TranspilerError: if failed to insert noises to the dag. | ||||||
""" | ||||||
if dag.duration is None: | ||||||
raise TranspilerError("This pass accepts only scheduled circuits") | ||||||
itoko marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
||||||
return super().run(dag) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dont think this is necessary here. If you want to do custom configuration of the noise model you should do
noise_model=NoiseModel.from_backend(backend, **custom_noise_options)
.It is strange to only have handling for one of the noise model options here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done at 053130f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also removed
delay_noise
option fromNoiseModel.from_backend
(e959be8).We now have no public way to replicate old behavior but we still have a private way with one additional line for it: