Skip to content
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

Merged
merged 27 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
996f8a8
Add local noise passes
itoko Nov 25, 2021
66d828c
Add delay noise option to NoiseModel.from_backend
itoko Nov 25, 2021
322a57c
lint
itoko Nov 26, 2021
c6fb4bc
Rename fn -> func
itoko Dec 3, 2021
6a336e3
Change the meaning of `delay_noise` option
itoko Dec 3, 2021
2e7ee22
Enable delay_noise only if thermal_relaxation is enabled
itoko Dec 3, 2021
37e8e65
Relocate noise.passes module files
itoko Dec 3, 2021
3f6239a
Make NoiseModel.pass_manager private and change its spec
itoko Dec 3, 2021
631108d
Move the point to add custom pass noise
itoko Dec 3, 2021
5342a88
Update docstring
itoko Dec 3, 2021
2780f3c
lint
itoko Dec 3, 2021
435be1b
Merge remote-tracking branch 'upstream/main' into add-noise-pass
itoko Dec 6, 2021
053130f
Remove delay_noise option from AerSimulator.from_backend
itoko Dec 6, 2021
1672eb0
Enable to run relaxation pass with non-scheduled circuits
itoko Dec 6, 2021
5753075
Change to type-based instruction check from name-based
itoko Dec 7, 2021
d478674
Change to remove op when method='replace' and the return of 'func' is…
itoko Dec 7, 2021
33ef087
Fix a bug embeded in the change to type-based instruction check
itoko Dec 7, 2021
c692a89
Convert the temperature parameter in NoiseModel.from_backend into an …
itoko Dec 7, 2021
e959be8
Remove delay_noise option from NoiseModel.from_backend
itoko Dec 7, 2021
6a08aef
Deprecate BackendProperties support in NoiseModel.from_backend
itoko Dec 8, 2021
106b261
Drop support of ReadoutError in LocalNoisePass
itoko Dec 8, 2021
ede0bf6
Refactor tests
itoko Dec 8, 2021
7325941
Back to rely on Instruction.duration instead of InstructionDurations
itoko Dec 8, 2021
cf1e2ed
Improve test
itoko Dec 8, 2021
de40cbd
Add release note
itoko Dec 8, 2021
c40de14
Update release note and deprecation warning
chriseclectic Dec 8, 2021
b1a27cc
Merge branch 'main' into add-noise-pass
chriseclectic Dec 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / stateme
# pi = the PI constant
# op = operation iterator
# b = basis iterator
good-names=i,j,k,n,m,ex,v,w,x,y,z,Run,_,logger,q,c,r,qr,cr,qc,nd,pi,op,b,ar,br,
good-names=i,j,k,n,m,ex,v,w,x,y,z,Run,_,logger,q,c,r,qr,cr,qc,nd,pi,op,b,ar,br,dt,
__unittest

# Bad variable names which should always be refused, separated by a comma
Expand Down
8 changes: 4 additions & 4 deletions qiskit/providers/aer/backends/aer_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,10 +552,6 @@ def name(self):
@classmethod
def from_backend(cls, backend, **options):
"""Initialize simulator from backend."""
# pylint: disable=import-outside-toplevel
# Avoid cyclic import
from ..noise.noise_model import NoiseModel

# Get configuration and properties from backend
configuration = copy.copy(backend.configuration())
properties = copy.copy(backend.properties())
Expand All @@ -566,6 +562,10 @@ def from_backend(cls, backend, **options):

# Use automatic noise model if none is provided
if 'noise_model' not in options:
# pylint: disable=import-outside-toplevel
# Avoid cyclic import
from ..noise.noise_model import NoiseModel

noise_model = NoiseModel.from_backend(backend)
if not noise_model.is_ideal():
options['noise_model'] = noise_model
Expand Down
8 changes: 8 additions & 0 deletions qiskit/providers/aer/backends/aerbackend.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,14 @@ def _assemble_noise_model(self, circuits, **run_options):
noise_model = run_options.get(
'noise_model', getattr(self.options, 'noise_model', None))

# Add custom pass noise only to QuantumCircuit objects
if noise_model and all(isinstance(circ, QuantumCircuit) for circ in circuits):
npm = noise_model._pass_manager()
if npm is not None:
circuits = npm.run(circuits)
chriseclectic marked this conversation as resolved.
Show resolved Hide resolved
if isinstance(circuits, QuantumCircuit):
circuits = [circuits]

# Check if circuits contain quantum error instructions
run_circuits = []
for circ in circuits:
Expand Down
51 changes: 48 additions & 3 deletions qiskit/providers/aer/noise/noise_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@

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.circuit import Instruction, Delay
from qiskit.providers import BaseBackend, Backend
from qiskit.providers.models import BackendProperties
from qiskit.transpiler import PassManager
from .device.models import _excited_population
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__)
Expand Down Expand Up @@ -182,6 +186,8 @@ def __init__(self, basis_gates=None):
# dict(tuple: 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):
Expand Down Expand Up @@ -271,7 +277,7 @@ def from_backend(cls, backend,
If non-default values are used gate_lengths should be a list

Args:
backend (Backend or BackendProperties): backend properties.
backend (Backend): backend.
gate_error (bool): Include depolarizing gate errors (Default: True).
readout_error (Bool): Include readout errors in model
(Default: True).
Expand Down Expand Up @@ -299,15 +305,25 @@ def from_backend(cls, backend,
if isinstance(backend, (BaseBackend, Backend)):
properties = backend.properties()
basis_gates = backend.configuration().basis_gates
num_qubits = backend.configuration().num_qubits
dt = backend.configuration().dt
if not properties:
raise NoiseError('Qiskit backend {} does not have a '
'BackendProperties'.format(backend))
elif isinstance(backend, BackendProperties):
warn(
'Passing BackendProperties instead of a "backend" object '
'has been deprecated as of qiskit-aer 0.10.0 and will be '
'removed no earlier than 3 months from that release date. '
'Duration dependent delay relaxation noise requires a '
'backend object.', DeprecationWarning, stacklevel=2)
properties = backend
basis_gates = set()
for prop in properties.gates:
basis_gates.add(prop.gate)
basis_gates = list(basis_gates)
num_qubits = len(properties.qubits)
dt = 0 # disable delay noise if dt is unknown
else:
raise NoiseError('{} is not a Qiskit backend or'
' BackendProperties'.format(backend))
Expand Down Expand Up @@ -341,9 +357,24 @@ 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:
delay_pass = RelaxationNoisePass(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to convert the temperature parameter of this function into an excited state population as is done for rest of the thermal relaxation errors so that it matches. This depends on the frequency of each qubit which is obtained from the backend configuration (see _excited_population function in models.py).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done at c692a89

t1s=[properties.t1(q) for q in range(num_qubits)],
t2s=[properties.t2(q) for q in range(num_qubits)],
dt=dt,
op_types=Delay,
excited_state_populations=[
_excited_population(
freq=properties.frequency(q),
temperature=temperature
) for q in range(num_qubits)
]
)
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:
Expand All @@ -356,6 +387,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):
Expand Down Expand Up @@ -1034,3 +1067,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
Copy link
Member

Choose a reason for hiding this comment

The 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.

18 changes: 18 additions & 0 deletions qiskit/providers/aer/noise/passes/__init__.py
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
133 changes: 133 additions & 0 deletions qiskit/providers/aer/noise/passes/local_noise_pass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 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, ReadoutError

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 func(
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.
If the return is None, the instruction will be removed.

"""

def __init__(
self,
func: Callable[[Instruction, Sequence[int]], InstructionLike],
op_types: Optional[Union[type, Iterable[type]]] = None,
method: str = 'append'
):
"""Initialize noise pass.

Args:
func: noise function `func(inst, qubits) -> InstructionLike`.
op_types: Optional, single or list of instruction types 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(op_types, type):
op_types = (op_types,)
super().__init__()
self._func = func
self._ops = tuple(op_types) if op_types else tuple()
self._method = method
if not all(isinstance(op, type) for op in self._ops):
raise TranspilerError(
f"Invalid ops: '{op_types}', expecting single or list of operation types (or None)"
)

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 not isinstance(node.op, self._ops):
continue

qubits = [qubit_indices[q] for q in node.qargs]
new_op = self._func(node.op, qubits)
if new_op is None:
if self._method == "replace":
dag.remove_op_node(node)
continue
if isinstance(new_op, ReadoutError):
raise TranspilerError("Insertions of ReadoutError is not yet supported.")
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
Loading