Skip to content

Commit

Permalink
Add transpiler passes for adding instruction-dependent noises (#1391)
Browse files Browse the repository at this point in the history
Co-authored-by: Christopher J. Wood <cjwood@us.ibm.com>
  • Loading branch information
itoko and chriseclectic authored Dec 9, 2021
1 parent 53038b1 commit 130ad57
Show file tree
Hide file tree
Showing 13 changed files with 652 additions and 119 deletions.
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)
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(
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
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

0 comments on commit 130ad57

Please sign in to comment.