Repository: PennyLaneAI/pennylane
Pull request #5996 (2024-07-13 05:34:29)

Support for converting Qiskit Noise models
**Context:** Adds support for converting Qiskit noise models to PennyLane ones. 

**Description of the Change:** Adds a `from_qiskit_noise` function that uses implementation in `pennylane-qiskit` plugin for executing the conversion.

**Benefits:** Qiskit noise models can be mapped to PL ones. 

**Possible Drawbacks:** Multi-qubit errors would be a bit harder to track since PL only has single-qubit error channels (except `qml.QubitChannel`). 

**Related GitHub Issues:** [sc-68046]


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5996?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.65%. Comparing base [(`48eef6c`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/48eef6c1c17dc69e5d467a46d5af337c85641fe0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`3dccd06`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/3dccd06252aeba4b79ea4add13db4819021baa38?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff            @@
##           master    #5996    +/-   ##
========================================
  Coverage   99.65%   99.65%            
========================================
  Files         427      427            
  Lines       40983    40704   -279     
========================================
- Hits        40841    40565   -276     
+ Misses        142      139     -3     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5996?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index f45e8223d..58dd4e495 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -18,11 +18,15 @@
   [(#5756)](https://github.com/PennyLaneAI/pennylane/pull/5756)
   [(#5987)](https://github.com/PennyLaneAI/pennylane/pull/5987)
 
+* A new `qml.from_qiskit_noise` method now allows one to convert a Qiskit ``NoiseModel`` to a
+  PennyLane ``NoiseModel`` via the Pennylane-Qiskit plugin.
+  [(#5996)](https://github.com/PennyLaneAI/pennylane/pull/5996)
+
 * A new function `qml.registers` has been added, enabling the creation of registers, which are implemented as a dictionary of `Wires` instances.
   [(#5957)](https://github.com/PennyLaneAI/pennylane/pull/5957)
 
-* The `split_to_single_terms` transform is added. This transform splits expectation values of sums 
-  into multiple single-term measurements on a single tape, providing better support for simulators 
+* The `split_to_single_terms` transform is added. This transform splits expectation values of sums
+  into multiple single-term measurements on a single tape, providing better support for simulators
   that can handle non-commuting observables but don't natively support multi-term observables.
   [(#5884)](https://github.com/PennyLaneAI/pennylane/pull/5884)
 
@@ -241,8 +245,10 @@
 <h3>Contributors ✍️</h3>
 
 This release contains contributions from (in alphabetical order):
+
 Guillermo Alonso,
-Utkarsh Azad
+Utkarsh Azad,
+Ahmed Darwish,
 Astral Cai,
 Yushao Chen,
 Gabriel Bottrill,
diff --git a/pennylane/io.py b/pennylane/io.py
index e3a3cf14e..f19bd150d 100644
--- a/pennylane/io.py
+++ b/pennylane/io.py
@@ -407,6 +407,61 @@ def from_qiskit_op(qiskit_op, params=None, wires=None):
         raise RuntimeError(_MISSING_QISKIT_PLUGIN_MESSAGE) from e
 
 
+def from_qiskit_noise(noise_model, verbose=False, decimal_places=None):
+    """Converts a Qiskit `NoiseModel <https://qiskit.github.io/qiskit-aer/stubs/qiskit_aer.noise.NoiseModel.html>`__
+    into a PennyLane :class:`~.NoiseModel`.
+
+    Args:
+        noise_model (qiskit_aer.noise.NoiseModel): a Qiskit ``NoiseModel`` instance.
+        verbose (bool): when printing a ``NoiseModel``, a complete list of Kraus matrices for each ``qml.QubitChannel``
+            is displayed with ``verbose=True``. By default, ``verbose=False`` and only the number of Kraus matrices and
+            the number of qubits they act on is displayed for brevity.
+        decimal_places (int | None): number of decimal places to round the elements of Kraus matrices when they are being
+            displayed for each ``qml.QubitChannel`` when ``verbose=True``.
+
+    Returns:
+        qml.NoiseModel: The PennyLane noise model converted from the input Qiskit ``NoiseModel`` object.
+
+    Raises:
+        ValueError: When a quantum error present in the noise model cannot be converted.
+
+    .. note::
+
+        - This function depends upon the PennyLane-Qiskit plugin, which can be installed following these
+          `installation instructions <https://docs.pennylane.ai/projects/qiskit/en/latest/installation.html>`__.
+          You may need to restart your kernel if you are running it in a notebook environment.
+        - Each quantum error present in the qiskit noise model is converted into an equivalent
+          :class:`~.QubitChannel` operator with the same canonical Kraus representation.
+        - Currently, PennyLane noise models do not support readout errors, so those will be skipped during
+          conversion.
+
+    **Example**
+
+    Consider the following noise model constructed in Qiskit:
+
+    >>> import qiskit_aer.noise as noise
+    >>> error_1 = noise.depolarizing_error(0.001, 1) # 1-qubit noise
+    >>> error_2 = noise.depolarizing_error(0.01, 2) # 2-qubit noise
+    >>> noise_model = noise.NoiseModel()
+    >>> noise_model.add_all_qubit_quantum_error(error_1, ['rz', 'ry'])
+    >>> noise_model.add_all_qubit_quantum_error(error_2, ['cx'])
+
+    This noise model can be converted into PennyLane using:
+
+    >>> import pennylane as qml
+    >>> qml.from_qiskit_noise(noise_model)
+    NoiseModel({
+        OpIn(['RZ', 'RY']): QubitChannel(num_kraus=4, num_wires=1)
+        OpIn(['CNOT']): QubitChannel(num_kraus=16, num_wires=2)
+    })
+    """
+    try:
+        plugin_converter = plugin_converters["qiskit_noise"].load()
+        return plugin_converter(noise_model, verbose=verbose, decimal_places=decimal_places)
+    except KeyError as e:
+        raise RuntimeError(_MISSING_QISKIT_PLUGIN_MESSAGE) from e
+
+
 def from_qasm(quantum_circuit: str, measurements=None):
     r"""
     Loads quantum circuits from a QASM string using the converter in the
diff --git a/pennylane/noise/conditionals.py b/pennylane/noise/conditionals.py
index 790c304f5..c31b01fe5 100644
--- a/pennylane/noise/conditionals.py
+++ b/pennylane/noise/conditionals.py
@@ -22,7 +22,8 @@ from inspect import isclass, signature
 
 import pennylane as qml
 from pennylane.boolean_fn import BooleanFn
-from pennylane.ops import Controlled
+from pennylane.measurements import MeasurementValue, MidMeasureMP
+from pennylane.ops import Adjoint, Controlled
 from pennylane.templates import ControlledSequence
 from pennylane.wires import WireError, Wires
 
@@ -182,7 +183,7 @@ class OpIn(BooleanFn):
         self._cops = _get_ops(ops)
         self.condition = self._cops
         super().__init__(
-            self._check_in_ops, f"OpIn({[getattr(op, '__name__') for op in self._cops]})"
+            self._check_in_ops, f"OpIn({[getattr(op, '__name__', op) for op in self._cops]})"
         )
 
     def _check_in_ops(self, operation):
@@ -226,7 +227,7 @@ class OpEq(BooleanFn):
         self._cond = [ops] if not isinstance(ops, (list, tuple, set)) else ops
         self._cops = _get_ops(ops)
         self.condition = self._cops
-        cops_names = list(getattr(op, "__name__") for op in self._cops)
+        cops_names = list(getattr(op, "__name__", op) for op in self._cops)
         super().__init__(
             self._check_eq_ops,
             f"OpEq({cops_names if len(cops_names) > 1 else cops_names[0]})",
@@ -267,22 +268,26 @@ def _get_ops(val):
         classes corresponding to val.
     """
     vals = val if isinstance(val, (list, tuple, set)) else [val]
-    return tuple(
-        (
-            getattr(qml.ops, val, None) or getattr(qml, val)
-            if isinstance(val, str)
-            else (val if isclass(val) else getattr(val, "__class__"))
-        )
-        for val in vals
-    )
+    op_names = []
+    for _val in vals:
+        if isinstance(_val, str):
+            op_names.append(getattr(qml.ops, _val, None) or getattr(qml, _val))
+        elif isclass(_val):
+            op_names.append(_val)
+        elif isinstance(_val, (MeasurementValue, MidMeasureMP)):
+            mid_measure = _val if isinstance(_val, MidMeasureMP) else _val.measurements[0]
+            op_names.append(["MidMeasure", "Reset"][getattr(mid_measure, "reset", 0)])
+        else:
+            op_names.append(getattr(_val, "__class__"))
+    return tuple(op_names)
 
 
 def _check_arithmetic_ops(op1, op2):
     """Helper method for comparing two arithmetic operators based on type check of the bases"""
     # pylint: disable = unnecessary-lambda-assignment
 
-    if isinstance(op1, (Controlled, ControlledSequence)) or isinstance(
-        op2, (Controlled, ControlledSequence)
+    if isinstance(op1, (Adjoint, Controlled, ControlledSequence)) or isinstance(
+        op2, (Adjoint, Controlled, ControlledSequence)
     ):
         return (
             isinstance(op1, type(op2))


Test patch:
diff --git a/tests/noise/test_conditionals.py b/tests/noise/test_conditionals.py
index 6dc6d74b9..9afe7649c 100644
--- a/tests/noise/test_conditionals.py
+++ b/tests/noise/test_conditionals.py
@@ -185,6 +185,7 @@ class TestNoiseFunctions:
             (["CZ", "RY", "CNOT"], qml.CNOT([0, 1]), True),
             (qml.Y(1), qml.RY(1.0, 1), False),
             (qml.CNOT(["a", "c"]), qml.CNOT([0, 1]), True),
+            ([qml.S("a"), qml.adjoint(qml.T)("b")], qml.adjoint(qml.T)([0]), True),
             ([qml.CZ(["a", "c"]), qml.Y(1)], qml.CZ([0, 1]), True),
             ([qml.RZ(1.9, 0), qml.Z(0) @ qml.Z(1)], qml.Z("b") @ qml.Z("a"), True),
             ([qml.Z(0) + qml.Z(1), qml.Z(2)], qml.Z("b") + qml.Z("a"), True),
@@ -211,6 +212,8 @@ class TestNoiseFunctions:
             (qml.RX(0, 1), qml.RY(1.0, 1), False),
             ("RX", qml.RX(0, 1), True),
             ([qml.RX, qml.RY], qml.RX, False),
+            (qml.measure(1, reset=True), qml.measure(2, reset=True), True),
+            (qml.measure(1, reset=True), qml.measure(1, reset=False), False),
             ([qml.RX, qml.RY], [qml.RX(1.0, 1), qml.RY(2.0, 2)], True),
             (["CZ", "RY"], [qml.CZ([0, 1]), qml.RY(1.0, [1])], True),
             (qml.Z(0) @ qml.Z(1), qml.Z("b") @ qml.Z("a"), True),
@@ -229,7 +232,7 @@ class TestNoiseFunctions:
 
         assert isinstance(func, qml.BooleanFn)
 
-        op_repr = [getattr(op, "__name__") for op in _get_ops(obj)]
+        op_repr = [getattr(op, "__name__", op) for op in _get_ops(obj)]
         assert str(func) == f"OpEq({op_repr if len(op_repr) > 1 else op_repr[0]})"
         assert func(op) == result
 
diff --git a/tests/test_io.py b/tests/test_io.py
index 2cf1b2247..2575bfcf6 100644
--- a/tests/test_io.py
+++ b/tests/test_io.py
@@ -54,6 +54,7 @@ load_entry_points = [
     "qasm_file",
     "qasm",
     "qiskit_op",
+    "qiskit_noise",
     "qiskit",
     "quil_file",
     "quil",
@@ -75,7 +76,11 @@ class TestLoad:
 
     @pytest.mark.parametrize(
         "method, entry_point_name",
-        [(qml.from_qiskit, "qiskit"), (qml.from_qiskit_op, "qiskit_op")],
+        [
+            (qml.from_qiskit, "qiskit"),
+            (qml.from_qiskit_op, "qiskit_op"),
+            (qml.from_qiskit_noise, "qiskit_noise"),
+        ],
     )
     def test_qiskit_converter_does_not_exist(self, monkeypatch, method, entry_point_name):
         """Test that a RuntimeError with an appropriate message is raised if a Qiskit convenience
@@ -94,7 +99,11 @@ class TestLoad:
 
     @pytest.mark.parametrize(
         "method, entry_point_name",
-        [(qml.from_qiskit, "qiskit"), (qml.from_qiskit_op, "qiskit_op")],
+        [
+            (qml.from_qiskit, "qiskit"),
+            (qml.from_qiskit_op, "qiskit_op"),
+            (qml.from_qiskit_noise, "qiskit_noise"),
+        ],
     )
     def test_qiskit_converter_load_fails(self, monkeypatch, method, entry_point_name):
         """Test that an exception which is raised while calling a Qiskit convenience method (but
@@ -114,6 +123,7 @@ class TestLoad:
         [
             (qml.from_qiskit, "qiskit"),
             (qml.from_qiskit_op, "qiskit_op"),
+            (qml.from_qiskit_noise, "qiskit_noise"),
             (qml.from_pyquil, "pyquil_program"),
             (qml.from_quil, "quil"),
             (qml.from_quil_file, "quil_file"),