Repository: Qiskit/qiskit

Issue:
Add a two-step tapering option
### What should we add?

Currently, the :meth:`taper` of the z2symmetry class does the following (I only kept the relevant parts)
```
def taper(self, operator: PauliSumOp) -> OperatorBase:
    # STEP 1 Change basis 
    if not operator.is_zero():
        for clifford in self.cliffords:
            operator = cast(PauliSumOp, clifford @ operator @ clifford)
            operator = operator.reduce(atol=0)

    # STEP 2 Reduce the operator
    if self._tapering_values is None:
        tapered_ops_list = [
            self._taper(operator, list(coeff))
            for coeff in itertools.product([1, -1], repeat=len(self._sq_list))
        ]
        tapered_ops: OperatorBase = ListOp(tapered_ops_list)
    else:
        tapered_ops = self._taper(operator, self._tapering_values)

    return tapered_ops
```
For the QEOM algorithm in qiskit-nature, we encounter the problem of tapering products of operators. In that case, the first step is multiplicative while the second step is not : STEP1(A @ B) = STEP1(A) @ STEP1(B) but STEP2(A @ B) != STEP2(A) @ STEP2(B). This multiplicativity reduces the resource requirement of the first step.

I propose the addition of two additional methods :meth:`convert_clifford` and :meth:`taper_clifford` corresponding to step 1/2 respectively, to allow a more efficient management of the tapering of products. Overall, this would also contribute to making the tapering more understandable.

I know that the future of the z2symmetries has some uncertainties so this is more of a quick, short term possible improvement.



Discussion:

Resolving patch:
diff --git a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py
--- a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py
+++ b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py
@@ -356,32 +356,59 @@ def find_Z2_symmetries(cls, operator: PauliSumOp) -> "Z2Symmetries":
 
         return cls(pauli_symmetries, sq_paulis, sq_list, None)
 
-    def taper(self, operator: PauliSumOp) -> OperatorBase:
-        """
-        Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`.
-        The `tapering_values` will be stored into the resulted operator for a record.
+    def convert_clifford(self, operator: PauliSumOp) -> OperatorBase:
+        """This method operates the first part of the tapering.
+        It converts the operator by composing it with the clifford unitaries defined in the current
+        symmetry.
 
         Args:
-            operator: the to-be-tapered operator.
+            operator: to-be-tapered operator
 
         Returns:
-            If tapering_values is None: [:class`PauliSumOp`]; otherwise, :class:`PauliSumOp`
+            :class:`PauliSumOp` corresponding to the converted operator.
+
         Raises:
             OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty
+
         """
+
         if not self._symmetries or not self._sq_paulis or not self._sq_list:
             raise OpflowError(
                 "Z2 symmetries, single qubit pauli and single qubit list cannot be empty."
             )
 
-        # If the operator is zero then we can skip the following. We still need to taper the
-        # operator to reduce its size i.e. the number of qubits so for example 0*"IIII" could
-        # taper to 0*"II" when symmetries remove two qubits.
         if not operator.is_zero():
             for clifford in self.cliffords:
                 operator = cast(PauliSumOp, clifford @ operator @ clifford)
                 operator = operator.reduce(atol=0)
 
+        return operator
+
+    def taper_clifford(self, operator: PauliSumOp) -> OperatorBase:
+        """This method operates the second part of the tapering.
+        This function assumes that the input operators have already been transformed using
+        :meth:`convert_clifford`. The redundant qubits due to the symmetries are dropped and
+        replaced by their two possible eigenvalues.
+        The `tapering_values` will be stored into the resulted operator for a record.
+
+        Args:
+            operator: Partially tapered operator resulting from a call to :meth:`convert_clifford`
+
+        Returns:
+            If tapering_values is None: [:class:`PauliSumOp`]; otherwise, :class:`PauliSumOp`
+
+        Raises:
+            OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty
+
+        """
+
+        if not self._symmetries or not self._sq_paulis or not self._sq_list:
+            raise OpflowError(
+                "Z2 symmetries, single qubit pauli and single qubit list cannot be empty."
+            )
+        # If the operator is zero then we can skip the following. We still need to taper the
+        # operator to reduce its size i.e. the number of qubits so for example 0*"IIII" could
+        # taper to 0*"II" when symmetries remove two qubits.
         if self._tapering_values is None:
             tapered_ops_list = [
                 self._taper(operator, list(coeff))
@@ -393,6 +420,42 @@ def taper(self, operator: PauliSumOp) -> OperatorBase:
 
         return tapered_ops
 
+    def taper(self, operator: PauliSumOp) -> OperatorBase:
+        """
+        Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`.
+        The `tapering_values` will be stored into the resulted operator for a record.
+
+        The tapering is a two-step algorithm which first converts the operator into a
+        :class:`PauliSumOp` with same eigenvalues but where some qubits are only acted upon
+        with the Pauli operators I or X.
+        The number M of these redundant qubits is equal to the number M of identified symmetries.
+
+        The second step of the reduction consists in replacing these qubits with the possible
+        eigenvalues of the corresponding Pauli X, giving 2^M new operators with M less qubits.
+        If an eigenvalue sector was previously identified for the solution, then this reduces to
+        1 new operator with M less qubits.
+
+        Args:
+            operator: the to-be-tapered operator
+
+        Returns:
+            If tapering_values is None: [:class:`PauliSumOp`]; otherwise, :class:`PauliSumOp`
+
+        Raises:
+            OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty
+
+        """
+
+        if not self._symmetries or not self._sq_paulis or not self._sq_list:
+            raise OpflowError(
+                "Z2 symmetries, single qubit pauli and single qubit list cannot be empty."
+            )
+
+        converted_ops = self.convert_clifford(operator)
+        tapered_ops = self.taper_clifford(converted_ops)
+
+        return tapered_ops
+
     def _taper(self, op: PauliSumOp, curr_tapering_values: List[int]) -> OperatorBase:
         pauli_list = []
         for pauli_term in op:


Test patch:
diff --git a/test/python/opflow/test_z2_symmetries.py b/test/python/opflow/test_z2_symmetries.py
--- a/test/python/opflow/test_z2_symmetries.py
+++ b/test/python/opflow/test_z2_symmetries.py
@@ -92,3 +92,21 @@ def test_truncate_tapered_op(self):
         )
         expected_op = TaperedPauliSumOp(primitive, z2_symmetries)
         self.assertEqual(tapered_op, expected_op)
+
+    def test_twostep_tapering(self):
+        """Test the two-step tapering"""
+        qubit_op = PauliSumOp.from_list(
+            [
+                ("II", -1.0537076071291125),
+                ("IZ", 0.393983679438514),
+                ("ZI", -0.39398367943851387),
+                ("ZZ", -0.01123658523318205),
+                ("XX", 0.1812888082114961),
+            ]
+        )
+        z2_symmetries = Z2Symmetries.find_Z2_symmetries(qubit_op)
+        tapered_op = z2_symmetries.taper(qubit_op)
+
+        tapered_op_firststep = z2_symmetries.convert_clifford(qubit_op)
+        tapered_op_secondstep = z2_symmetries.taper_clifford(tapered_op_firststep)
+        self.assertEqual(tapered_op, tapered_op_secondstep)