Source code for qiskit_aer.noise.passes.local_noise_pass
# 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, QuantumCircuit
from qiskit.dagcircuit import DAGCircuit
from qiskit.converters import circuit_to_dag
from qiskit.transpiler import TransformationPass
from qiskit.transpiler.exceptions import TranspilerError
from ..errors import QuantumError, ReadoutError
InstructionLike = Union[Instruction, QuantumError, QuantumCircuit]
[docs]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]], Optional[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)"
)
[docs] 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:
# Edge case where we are replacing a node with nothing (removing node)
if self._method == "replace":
dag.remove_op_node(node)
continue
if isinstance(new_op, ReadoutError):
raise TranspilerError("Insertions of ReadoutError is not yet supported.")
# Initialize new node dag
new_dag = DAGCircuit()
new_dag.add_qubits(node.qargs)
new_dag.add_clbits(node.cargs)
# If appending re-apply original op node first
if self._method == "append":
new_dag.apply_operation_back(node.op, qargs=node.qargs, cargs=node.cargs)
# If the new op is not a QuantumCircuit or Instruction, attempt
# to conver to an Instruction
if not isinstance(new_op, (QuantumCircuit, 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_clbits > 0:
raise TranspilerError("Noise must be an instruction without clbits.")
# Validate the instruction matches the number of qubits and clbits of the node
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}"
)
# Add the noise op returned by the function
if isinstance(new_op, QuantumCircuit):
# If the new op is a quantum circuit, compose its DAG with the new dag
# so that it is unrolled rather than added as an opaque instruction
new_dag.compose(circuit_to_dag(new_op), qubits=node.qargs) # never touch clbits
else:
# Otherwise append the instruction returned by the function
new_dag.apply_operation_back(new_op, qargs=node.qargs) # never touch cargs
# If prepending reapply original op node last
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