Skip to content

Commit

Permalink
add_noise transform for adding noise models (#5718)
Browse files Browse the repository at this point in the history
**Context:** Adds a transform for adding `NoiseModels`.

**Description of the Change:** Adds `add_noise.py` under
`pennylane/transforms` that gives the said method for inserting
operations according to a provided noise model.

**Benefits:** We support noise models. 

**Possible Drawbacks:** None.

**Related GitHub Issues:** [sc-64843]

---------

Co-authored-by: Jay Soni <jbsoni@uwaterloo.ca>
  • Loading branch information
obliviateandsurrender and Jaybsoni committed Jun 19, 2024
1 parent a0871e5 commit 248a808
Show file tree
Hide file tree
Showing 7 changed files with 727 additions and 13 deletions.
10 changes: 9 additions & 1 deletion doc/code/qml_noise.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ qml.noise
This module contains the functionality for building and manipulating insertion-based noise models,
where noisy gates and channels are inserted based on the target operations.

.. _intro_noise_model:

Overview
--------

Expand All @@ -22,7 +24,13 @@ noise-related metadata can also be supplied to construct a noise model using:
Each conditional in the ``model_map`` evaluates the gate operations in the quantum circuit based on
some condition of its attributes (e.g., type, parameters, wires, etc.) and use the corresponding
callable to apply the noise operations, using the user-provided metadata (e.g., hardware topologies
or relaxation times), whenever the condition results true.
or relaxation times), whenever the condition results true. A noise model once built can be attached
to a circuit or device via the following transform:

.. autosummary::
:toctree: api

~add_noise

.. _intro_boolean_fn:

Expand Down
39 changes: 30 additions & 9 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,41 @@
* The `default.tensor` device is introduced to perform tensor network simulations of quantum circuits using the `mps` (Matrix Product State) method.
[(#5699)](https://github.com/PennyLaneAI/pennylane/pull/5699)

* A new `qml.noise` module which contains utililty functions for building `NoiseModels`.
* A new `qml.noise` module which contains utility function for building `NoiseModels`
and an `add_noise` tranform for addding it to quantum circuits.
[(#5674)](https://github.com/PennyLaneAI/pennylane/pull/5674)
[(#5684)](https://github.com/PennyLaneAI/pennylane/pull/5684)
[(#5718)](https://github.com/PennyLaneAI/pennylane/pull/5718)

```python
fcond = qml.noise.op_eq(qml.X) | qml.noise.op_eq(qml.Y)
noise = qml.noise.partial_wires(qml.AmplitudeDamping, 0.4)
```pycon
>>> fcond1 = qml.noise.op_eq(qml.RX) & qml.noise.wires_in([0, 1])
>>> noise1 = qml.noise.partial_wires(qml.PhaseDamping, 0.4)
>>> fcond2 = qml.noise.op_in([qml.RY, qml.RZ])
>>> def noise2(op, **kwargs):
... qml.ThermalRelaxationError(op.parameters[0] * 0.05, kwargs["t1"], 0.2, 0.6, op.wires)
>>> noise_model = qml.NoiseModel({fcond1: noise1, fcond2: noise2}, t1=2.0)
>>> noise_model
NoiseModel({
OpEq(RX) & WiresIn([0, 1]) = PhaseDamping(gamma=0.4)
OpIn(['RY', 'RZ']) = noise2
}, t1 = 2.0)
```

```pycon
>>> qml.NoiseModel({fcond: noise}, t1=0.04)
NoiseModel({
OpEq(PauliX) | OpEq(PauliY) = AmplitudeDamping(gamma=0.4)
}, t1 = 0.04)
>>> @partial(qml.transforms.add_noise, noise_model=noise_model)
... @qml.qnode(dev)
... def circuit(w, x, y, z):
... qml.RX(w, wires=0)
... qml.RY(x, wires=1)
... qml.CNOT(wires=[0, 1])
... qml.RY(y, wires=0)
... qml.RX(z, wires=1)
... return qml.expval(qml.Z(0) @ qml.Z(1))
>>> print(qml.draw(circuit)(0.9, 0.4, 0.5, 0.6))
0: ──RX(0.90)──PhaseDamping(0.40)──────────────────────────╭●──RY(0.50)
1: ──RY(0.40)──ThermalRelaxationError(0.02,2.00,0.20,0.60)─╰X──RX(0.60)
───ThermalRelaxationError(0.03,2.00,0.20,0.60)─┤ ╭<Z@Z>
───PhaseDamping(0.40)──────────────────────────┤ ╰<Z@Z>
```

* The ``from_openfermion`` and ``to_openfermion`` functions are added to convert between
Expand All @@ -72,8 +93,8 @@
of_op = openfermion.FermionOperator('0^ 2')
pl_op = qml.from_openfermion(of_op)
of_op_new = qml.to_openfermion(pl_op)

```

```pycon
>>> print(pl_op)
a⁺(0) a(2)
Expand Down
1 change: 1 addition & 0 deletions pennylane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
pattern_matching,
pattern_matching_optimization,
clifford_t_decomposition,
add_noise,
)
from pennylane.ops.functions import (
dot,
Expand Down
4 changes: 2 additions & 2 deletions pennylane/noise/noise_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class NoiseModel:
- The ``conditional`` should be either a function decorated with :class:`~.BooleanFn`,
a callable object built via :ref:`constructor functions <intro_boolean_fn>` in
the ``noise`` module, or their bit-wise combination.
the ``qml.noise`` module, or their bit-wise combination.
- The definition of ``noise_fn(op, **kwargs)`` should have the operations in same the order
in which they are to be queued for an operation ``op``, whenever the corresponding
``conditional`` evaluates to ``True``.
Expand Down Expand Up @@ -129,7 +129,7 @@ def check_model(model):
for condition, noise in model.items():
if not isinstance(condition, qml.BooleanFn):
raise ValueError(
f"{condition} must be a boolean conditional, i.e., an instance of"
f"{condition} must be a boolean conditional, i.e., an instance of "
"BooleanFn or one of its subclasses."
)

Expand Down
3 changes: 2 additions & 1 deletion pennylane/transforms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
~batch_params
~batch_input
~transforms.insert
~transforms.add_noise
~defer_measurements
~transforms.split_non_commuting
~transforms.broadcast_expand
Expand Down Expand Up @@ -284,7 +285,7 @@ def circuit(x, y):
from .batch_partial import batch_partial
from .convert_to_numpy_parameters import convert_to_numpy_parameters
from .compile import compile

from .add_noise import add_noise

from .decompositions import clifford_t_decomposition
from .defer_measurements import defer_measurements
Expand Down
232 changes: 232 additions & 0 deletions pennylane/transforms/add_noise.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
# Copyright 2018-2024 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Transform for adding a noise model to a quantum circuit or device"""
from copy import copy
from functools import lru_cache

import pennylane as qml
from pennylane.transforms.core import TransformContainer, transform


@transform
def add_noise(tape, noise_model, level=None):
"""Insert operations according to a provided noise model.
Circuits passed through this transform will be updated to apply the
insertion-based :class:`~.NoiseModel`, which contains a mapping
``{BooleanFn: Callable}`` from conditions to the corresponding noise
gates. Each condition of the noise model will be evaluated on the
operations contained within the given circuit. For conditions that
evaluate to ``True``, the noisy gates contained within the ``Callable``
will be inserted after the operation under consideration.
Args:
tape (QNode or QuantumTape or Callable or pennylane.devices.Device): the input circuit or
device to be transformed
noise_model (~pennylane.NoiseModel): noise model according to which noise has to be inserted
level (None, str, int, slice): An indication of which stage in the transform program the
noise model should be applied to. Only relevant when transforming a ``QNode``. More details
on the following permissible values can be found in the :func:`~.workflow.get_transform_program` -
* ``None``: expands the tape to have no ``Adjoint`` and ``Templates``.
* ``str``: acceptable keys are ``"top"``, ``"user"``, ``"device"``, and ``"gradient"``
* ``int``: how many transforms to include, starting from the front of the program
* ``slice``: a slice to select out components of the transform program.
Returns:
qnode (QNode) or quantum function (Callable) or tuple[List[.QuantumTape], function] or device (pennylane.devices.Device):
Transformed circuit as described in :func:`qml.transform <pennylane.transform>`.
Raises:
ValueError: argument ``noise_model`` is not a valid noise model.
.. note::
For a given ``model_map`` within a ``NoiseModel``, if multiple conditionals in the ``model_map``
evaluate to ``True`` for an operation, then the noise operations defined via their respective
noisy quantum functions will be added in the same order in which the conditionals appear in the
``model_map``.
**Example:**
The following QNode can be transformed to add noise to the circuit:
.. code-block:: python3
from functools import partial
dev = qml.device("default.mixed", wires=2)
fcond1 = qml.noise.op_eq(qml.RX) & qml.noise.wires_in([0, 1])
noise1 = qml.noise.partial_wires(qml.PhaseDamping, 0.4)
fcond2 = qml.noise.op_in([qml.RX, qml.RZ])
def noise2(op, **kwargs):
qml.ThermalRelaxationError(op.parameters[0] * 0.5, kwargs["t1"], kwargs["t2"], 0.6, op.wires)
noise_model = qml.NoiseModel({fcond1: noise1, fcond2: noise2}, t1=2.0, t2=0.2)
@partial(qml.transforms.add_noise, noise_model=noise_model)
@qml.qnode(dev)
def circuit(w, x, y, z):
qml.RX(w, wires=0)
qml.RY(x, wires=1)
qml.CNOT(wires=[0, 1])
qml.RY(y, wires=0)
qml.RX(z, wires=1)
return qml.expval(qml.Z(0) @ qml.Z(1))
Executions of this circuit will differ from the noise-free value:
>>> circuit(0.9, 0.4, 0.5, 0.6)
tensor(0.60722291, requires_grad=True)
>>> print(qml.draw(f)(0.9, 0.4, 0.5, 0.6))
0: ──RX(0.9)──PhaseDamping(0.4)───────────────────────╭●──RY(0.5)───ThermalRelaxationError(0.2,2.0,0.2,0.6)─┤ ╭<Z@Z>
1: ──RY(0.4)──ThermalRelaxationError(0.2,2.0,0.2,0.6)─╰X──RX(0.6)───PhaseDamping(0.4)───────────────────────┤ ╰<Z@Z>
.. details::
:title: Tranform Levels
:href: add-noise-levels
When transforming an already constructed ``QNode``, the ``add_noise`` transform will be
added at the end of the "user" transforms by default, i.e., after all the transforms
that have been manually applied to the QNode until that point.
.. code-block:: python3
dev = qml.device("default.mixed", wires=2)
@qml.metric_tensor
@qml.transforms.undo_swaps
@qml.transforms.merge_rotations
@qml.transforms.cancel_inverses
@qml.qnode(dev)
def circuit(w, x, y, z):
qml.RX(w, wires=0)
qml.RY(x, wires=1)
qml.CNOT(wires=[0, 1])
qml.RY(y, wires=0)
qml.RX(z, wires=1)
return qml.expval(qml.Z(0) @ qml.Z(1))
noisy_circuit = qml.transforms.add_noise(circuit, noise_model)
>>> qml.workflow.get_transform_program(circuit)
TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, batch_transform, expand_fn, metric_tensor)
>>> qml.workflow.get_transform_program(noisy_circuit)
TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, add_noise, batch_transform, expand_fn, metric_tensor)
However, one can request inserting it at any specific point of the transform program. Specifying the ``level`` keyword argument while
transforming a ``QNode``, will allow addition of the transform at the end of the transform program extracted at a designated level via
:func:`get_transform_program <pennylane.workflow.get_transform_program>`. For example, one could specify ``None`` to add it at the end,
which will also ensure that the tape is expanded to have no ``Adjoint`` and ``Templates``:
>>> qml.transforms.add_noise(circuit, noise_model, level=None).transform_program
TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, batch_transform, expand_fn, add_noise, metric_tensor)
Other, acceptable values for the level are ``"top"``, ``"user"``, ``"device"``, and ``"gradient"``. Among these, `"top"` will allow addition
to an empty transform program, `"user"` will allow addition at the end of user specified transforms, `"device"` will allow addition at the
end of device-specific transforms, and `"gradient"` will allow addition at the end of transform that expands trainable operations. For example:
>>> qml.transforms.add_noise(circuit, noise_model, level="top").transform_program
TransformProgram(add_noise)
>>> qml.transforms.add_noise(circuit, noise_model, level="user").transform_program
TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, add_noise, metric_tensor)
>>> qml.transforms.add_noise(circuit, noise_model, level="device").transform_program
TransformProgram(cancel_inverses, merge_rotations, undo_swaps, _expand_metric_tensor, batch_transform, expand_fn, add_noise, metric_tensor)
Finally, more precise control over the insertion of the transform can be achieved by specifying
an integer or slice for indexing for extracting the transform program. For example, one can do:
>>> qml.transforms.add_noise(circuit, noise_model, level=2).transform_program
TransformProgram(cancel_inverses, merge_rotations, add_noise)
>>> qml.transforms.add_noise(circuit, noise_model, level=slice(1,3)).transform_program
TransformProgram(merge_rotations, undo_swaps, add_noise)
"""
if not hasattr(noise_model, "model_map") or not hasattr(noise_model, "metadata"):
raise ValueError(
f"Provided noise model object must define model_map and metatadata attributes, got {noise_model}."
)

if level is None or level == "user": # decompose templates and their adjoints

def stop_at(obj):
if not isinstance(obj, qml.operation.Operator):
return True
if not obj.has_decomposition:
return True
return not (hasattr(qml.templates, obj.name) or isinstance(obj, qml.ops.Adjoint))

error_type = (qml.operation.DecompositionUndefinedError,)
decompose = qml.devices.preprocess.decompose
[tape], _ = decompose(tape, stopping_condition=stop_at, name="add_noise", error=error_type)

conditions, noises = [], []
metadata = noise_model.metadata
for condition, noise in noise_model.model_map.items():
conditions.append(lru_cache(maxsize=512)(condition))
noises.append(qml.tape.make_qscript(noise))

new_operations = []
for operation in tape.operations:
curr_ops = [operation]
for condition, noise in zip(conditions, noises):
if condition(operation):
noise_ops = noise(operation, **metadata).operations
if operation in noise_ops and _check_queue_op(operation, noise, metadata):
ops_indx = noise_ops.index(operation)
curr_ops = noise_ops[:ops_indx] + curr_ops + noise_ops[ops_indx + 1 :]
else:
curr_ops.extend(noise_ops)
new_operations.extend(curr_ops)

new_tape = type(tape)(new_operations, tape.measurements, shots=tape.shots)
post_processing_fn = qml.devices.preprocess.null_postprocessing

return [new_tape], post_processing_fn


def _check_queue_op(operation, noise_func, metadata):
"""Performs a secondary check for existence of an operation in the queue using a randomized ID"""

test_id = "f49968bfc4W0H86df3A733bf6e92904d21a_!$-T-@!_c131S549b169b061I25b85398bfd8ec1S3c"
test_queue = noise_func(
qml.noise.partial_wires(operation, id=test_id)(operation.wires), **metadata
).operations

return any(test_id == getattr(o, "id", "") for o in test_queue)


# pylint:disable = protected-access
@add_noise.custom_qnode_transform
def custom_qnode_wrapper(self, qnode, targs, tkwargs):
"""QNode execution wrapper for supporting ``add_noise`` with levels"""
cqnode = copy(qnode)
level = tkwargs.get("level", "user")

transform_program = qml.workflow.get_transform_program(qnode, level=level)

cqnode._transform_program = transform_program
cqnode.add_transform(
TransformContainer(
self._transform,
targs,
{**tkwargs},
self._classical_cotransform,
self._is_informative,
self._final_transform,
)
)

return cqnode
Loading

0 comments on commit 248a808

Please sign in to comment.