Repository: qiskit-community/prototype-zne (language: python)

Issue:
Incorrect expectation values for the identity observable
### Device

MacBook Pro

### OS

macOS Sonoma 14.0

### Python version

3.11.7

### Release version or branch/commit

1.2.2

### What is the current behavior?

The expectation values returned by the ZNEEstimator are not correct for this particular example. I'm including the results of the "reference implementation" of the Estimator in `qiskit.primitives` and the exp values differ greatly.

### What is the expected behavior?

Expectation values return by the ZNEEstimator are similar to the reference implementation.

### Steps to reproduce the problem

```python
from qiskit.circuit.library import RealAmplitudes
from qiskit.quantum_info import SparsePauliOp
from qiskit.primitives import BackendEstimator, Estimator as RefEstimator
from qiskit.providers.fake_provider import FakeBogota
from zne import zne, ZNEStrategy
from zne.noise_amplification import TwoQubitAmplifier

ZNEEstimator = zne(BackendEstimator)
fake_nairobi = FakeBogota()

circuit = RealAmplitudes(num_qubits=5, reps=2, entanglement="full")
num_parameters = circuit.num_parameters
observable = SparsePauliOp("IIIII")
parameter_values = [[0] * num_parameters]

# Reference estimator
ref_job = RefEstimator().run(circuit, observable, parameter_values)
ref_result = ref_job.result()
print("ref result:", ref_result.values)

# ZNE estimator
zne_strategy = ZNEStrategy(noise_factors=[1, 3, 5], noise_amplifier=TwoQubitAmplifier())
zne_estimator = ZNEEstimator(backend=fake_nairobi)
zne_job = zne_estimator.run(circuit, observable, parameter_values, zne_strategy=zne_strategy)
zne_result = zne_job.result()
print("zne result", zne_result.values)

```

### Context

Tested this example with different versions of the library and the `1.0.0` was the last version that produced correct results.

### Suggestions

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct


Discussion / hints:

Fix patch:
diff --git a/zne/noise_amplification/__init__.py b/zne/noise_amplification/__init__.py
index 49db39c..7df82fc 100644
--- a/zne/noise_amplification/__init__.py
+++ b/zne/noise_amplification/__init__.py
@@ -16,6 +16,7 @@
     CxAmplifier,
     GlobalFoldingAmplifier,
     LocalFoldingAmplifier,
+    MultiQubitAmplifier,
     TwoQubitAmplifier,
 )
 from .noise_amplifier import CircuitNoiseAmplifier, DAGNoiseAmplifier, NoiseAmplifier
@@ -27,6 +28,7 @@
         LocalFoldingAmplifier,
         CxAmplifier,
         TwoQubitAmplifier,
+        MultiQubitAmplifier,
     )
 }
 
diff --git a/zne/noise_amplification/folding_amplifier/__init__.py b/zne/noise_amplification/folding_amplifier/__init__.py
index 2231b3e..c254f0c 100644
--- a/zne/noise_amplification/folding_amplifier/__init__.py
+++ b/zne/noise_amplification/folding_amplifier/__init__.py
@@ -16,6 +16,7 @@
 from .local_folding_amplifier import (
     CxAmplifier,
     LocalFoldingAmplifier,
+    MultiQubitAmplifier,
     TwoQubitAmplifier,
 )
 
@@ -24,4 +25,5 @@
     "LocalFoldingAmplifier",
     "CxAmplifier",
     "TwoQubitAmplifier",
+    "MultiQubitAmplifier",
 ]
diff --git a/zne/noise_amplification/folding_amplifier/local_folding_amplifier.py b/zne/noise_amplification/folding_amplifier/local_folding_amplifier.py
index 2d344ed..83d55fa 100644
--- a/zne/noise_amplification/folding_amplifier/local_folding_amplifier.py
+++ b/zne/noise_amplification/folding_amplifier/local_folding_amplifier.py
@@ -325,3 +325,35 @@ def __init__(  # pylint: disable=too-many-arguments,duplicate-code
             noise_factor_relative_tolerance=noise_factor_relative_tolerance,
             warn_user=warn_user,
         )
+
+
+class MultiQubitAmplifier(LocalFoldingAmplifier):
+    """
+    Digital noise amplifier acting on multi-qubit gates exclusively.
+
+    Replaces each multi-qubit gate locally with as many gates as indicated by the noise_factor.
+    """
+
+    def __init__(  # pylint: disable=too-many-arguments,duplicate-code
+        self,
+        sub_folding_option: str = "from_first",
+        barriers: bool = True,
+        random_seed: int | None = None,
+        noise_factor_relative_tolerance: float = 1e-2,
+        warn_user: bool = True,
+    ) -> None:
+        super().__init__(
+            gates_to_fold=None,
+            sub_folding_option=sub_folding_option,
+            barriers=barriers,
+            random_seed=random_seed,
+            noise_factor_relative_tolerance=noise_factor_relative_tolerance,
+            warn_user=warn_user,
+        )
+
+    def _check_gate_folds(self, operation: CircuitInstruction) -> bool:
+        instruction, qargs, _ = operation
+        if instruction.name in {"barrier", "measure"}:
+            return False
+        num_qubits = len(qargs)
+        return num_qubits > 1


Test patch:
diff --git a/test/noise_amplification/folding_amplifier/test_local_folding_amplifier.py b/test/noise_amplification/folding_amplifier/test_local_folding_amplifier.py
index f2909ce..f1573b1 100644
--- a/test/noise_amplification/folding_amplifier/test_local_folding_amplifier.py
+++ b/test/noise_amplification/folding_amplifier/test_local_folding_amplifier.py
@@ -26,6 +26,7 @@
 from zne.noise_amplification import (
     CxAmplifier,
     LocalFoldingAmplifier,
+    MultiQubitAmplifier,
     TwoQubitAmplifier,
 )
 
@@ -460,8 +461,27 @@ class TestFacades:
             (TwoQubitAmplifier, {"gates_to_fold": 2}),
         ],
     )
-    def test_two_qubit_amplifier(self, cls, configs):
+    def test_amplifier_facade(self, cls, configs):
         amplifier = cls()
         assert isinstance(amplifier, LocalFoldingAmplifier)
         for key, value in configs.items():
             assert getattr(amplifier, key) == frozenset([value])
+
+    def test_multi_qubit_amplifier(self):
+        amplifier = MultiQubitAmplifier()
+        assert isinstance(amplifier, LocalFoldingAmplifier)
+        assert amplifier.gates_to_fold is None
+        circuit = QuantumCircuit(4)
+        circuit.h(0)
+        circuit.z(3)
+        circuit.cx(0, 1)
+        circuit.x(1)
+        circuit.ccx(0, 1, 2)
+        circuit.y(2)
+        circuit.ecr(2, 3)
+        circuit.measure_all()
+        for operation in circuit.data:
+            instruction = operation[0]
+            skipped_instructions = {"barrier", "measure"}
+            expected = instruction.num_qubits > 1 and instruction.name not in skipped_instructions
+            assert amplifier._check_gate_folds(operation) == expected