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 11 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
16 changes: 9 additions & 7 deletions qiskit/providers/aer/backends/aer_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,10 +521,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 @@ -535,9 +531,15 @@ def from_backend(cls, backend, **options):

# Use automatic noise model if none is provided
if 'noise_model' not in options:
noise_model = NoiseModel.from_backend(backend)
if not noise_model.is_ideal():
options['noise_model'] = noise_model
# pylint: disable=import-outside-toplevel
# Avoid cyclic import
from ..noise.noise_model import NoiseModel

if 'delay_noise' in options:
Copy link
Member

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.

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 053130f

Copy link
Contributor Author

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 from NoiseModel.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:

noise_model = NoiseModel.from_backend(backend)
noise_model._custom_noise_passes = []	 # a bit hack though

noise_model = NoiseModel.from_backend(backend, delay_noise=options['delay_noise'])
del options['delay_noise']
if not noise_model.is_ideal():
options['noise_model'] = noise_model

# Initialize simulator
sim = cls(configuration=configuration,
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
34 changes: 32 additions & 2 deletions qiskit/providers/aer/noise/noise_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The 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 delay to the noise model basis gates (and have a default value of True)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with the change of the meaning of delay_noise option.
However, I have some reservations on changing its default value.
It will drastically change the default behavior of NoiseModel.from_backend. All the existing codes using default noise model will stop working and raise errors that tells circuits must be scheduled. That's why I'm hesitating to set delay_noise=True. Do you think that is OK?

Copy link
Member

@chriseclectic chriseclectic Dec 3, 2021

Choose a reason for hiding this comment

The 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 None for duration if not scheduled).

This reminds me that we need to add a release note explaining new additions. In particular it should have both a feature section (for the new passes and addition to noise model) and upgrade section that points out this change if you run a scheduled circuit or circuit with delays.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 InstructionDurations supplied instead of dt. It's different from your suggestion but what do you think of it? does it make sense? It could be seen as a problem who the relaxation noise pass should ask to know durations, InstructionDurations or Instruction.duration. (Both of them might be deprecated after circuit has durations info in its metadata, though)

"""Return a noise model derived from a devices backend properties.

This function generates a noise model based on:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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(
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=[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:
Expand All @@ -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):
Expand Down Expand Up @@ -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
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
124 changes: 124 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,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
105 changes: 105 additions & 0 deletions qiskit/providers/aer/noise/passes/relaxation_noise_pass.py
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,
Copy link
Member

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The 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 RelaxationNoisePass(t1s = t1, t2s=t2, dt, op_types), where t1,t2 are floats. (Same with excited state populations if its a float rather than list). I'll leave code comments below for how this could be implemented

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))
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
self._p1s = np.zeros(len(t1s))
self._p1s = np.zeros(self._t1s.shape)

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]
Copy link
Member

Choose a reason for hiding this comment

The 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)
Loading