Repository: Qiskit/qiskit

Issue:
New sparse z2symmetry cannot be empty
### Environment

- **Qiskit Terra version**: 0.23.0
- **Python version**: 3.9
- **Operating system**: MacOs


### What is happening?

The method `quantum_info.analysis.Z2symmetries` was created as a copy of the similar class from opflow to prepare the deprecation of this class.
The type inputs in this new class were changed from `list[Pauli]` to `PauliList`.
However I just discovered that PauliList cannot be instantiated with an empty list.
The method `Z2symmetries.find_z2_symmetries` creates an object of the class `Z2symmetries` which will be empty in some cases. In theses cases the output is `cls(PauliList([]), PauliList([]), [], None)` which is raising an error.




### How can we reproduce the issue?

```
from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp
from qiskit.quantum_info.analysis.z2_symmetries import Z2Symmetries
qubit_op = SparsePauliOp.from_list([("II", 0.0)])
z2_symmetries = Z2Symmetries.find_z2_symmetries(qubit_op)
```

```
---------------------------------------------------------------------------
QiskitError                               Traceback (most recent call last)
<ipython-input-1-e3e1d36b0d38> in <module>
      2 from qiskit.quantum_info.analysis.z2_symmetries import Z2Symmetries
      3 qubit_op = SparsePauliOp.from_list([("II", 0.0)])
----> 4 z2_symmetries = Z2Symmetries.find_z2_symmetries(qubit_op)

~/Documents/own_repo/qiskit-terra/qiskit/quantum_info/analysis/z2_symmetries.py in find_z2_symmetries(cls, operator)
    203 
    204         if _sparse_pauli_op_is_zero(operator):
--> 205             return cls(PauliList([]), PauliList([]), [], None)
    206 
    207         for pauli in iter(operator):

~/Documents/own_repo/qiskit-terra/qiskit/quantum_info/operators/symplectic/pauli_list.py in __init__(self, data)
    138         else:
    139             # Conversion as iterable of Paulis
--> 140             base_z, base_x, base_phase = self._from_paulis(data)
    141 
    142         # Initialize BasePauli

~/Documents/own_repo/qiskit-terra/qiskit/quantum_info/operators/symplectic/pauli_list.py in _from_paulis(data)
    179         num_paulis = len(data)
    180         if num_paulis == 0:
--> 181             raise QiskitError("Input Pauli list is empty.")
    182         paulis = []
    183         for i in data:

QiskitError: 'Input Pauli list is empty.'
```

### What should happen?

One should be able to instantiate an empty Z2Symmetries object.
`Z2symmetries.find_z2_symmetries` should not give errors on empty operators.

### Any suggestions?

My suggestion is to extend the typing `PauliList` to `list[Pauli] | PauliList`.
This would resolve the issue by replacing all `cls(PauliList([]), PauliList([]), [], None)` with `cls([], [], [], None)`.

I think it makes sense to support both because we can iterate though them the same way. We also won't use the additional features of the PauliList for applications to symmetry reduction




Discussion:

Resolving patch:
diff --git a/qiskit/quantum_info/analysis/z2_symmetries.py b/qiskit/quantum_info/analysis/z2_symmetries.py
--- a/qiskit/quantum_info/analysis/z2_symmetries.py
+++ b/qiskit/quantum_info/analysis/z2_symmetries.py
@@ -15,13 +15,14 @@
 from __future__ import annotations
 
 import itertools
+from collections.abc import Iterable
 from copy import deepcopy
 from typing import Union, cast
 
 import numpy as np
 
 from qiskit.exceptions import QiskitError
-from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp
+from qiskit.quantum_info import Pauli, SparsePauliOp
 
 
 class Z2Symmetries:
@@ -47,18 +48,18 @@ class Z2Symmetries:
 
     def __init__(
         self,
-        symmetries: PauliList,
-        sq_paulis: PauliList,
-        sq_list: list[int],
-        tapering_values: list[int] | None = None,
+        symmetries: Iterable[Pauli],
+        sq_paulis: Iterable[Pauli],
+        sq_list: Iterable[int],
+        tapering_values: Iterable[int] | None = None,
         *,
         tol: float = 1e-14,
     ):
         r"""
         Args:
-            symmetries: PauliList object representing the list of $Z_2$ symmetries. These correspond to
+            symmetries: Object representing the list of $Z_2$ symmetries. These correspond to
                 the generators of the symmetry group $\langle \tau_1, \tau_2\dots \rangle>$.
-            sq_paulis: PauliList object representing the list of single-qubit Pauli $\sigma^x_{q(i)}$
+            sq_paulis: Object representing the list of single-qubit Pauli $\sigma^x_{q(i)}$
                 anti-commuting with the symmetry $\tau_i$ and commuting with all the other symmetries
                 $\tau_{j\neq i}$. These operators are used to construct the unitary Clifford operators.
             sq_list: The list of indices $q(i)$ of the single-qubit Pauli operators used to build the
@@ -71,6 +72,11 @@ def __init__(
                 and tapering values must be of equal length. This length is the number of applied
                 symmetries and translates directly to the number of eliminated qubits.
         """
+        symmetries = list(symmetries)
+        sq_paulis = list(sq_paulis)
+        sq_list = list(sq_list)
+        tapering_values = None if tapering_values is None else list(tapering_values)
+
         if len(symmetries) != len(sq_paulis):
             raise QiskitError(
                 f"The number of Z2 symmetries, {len(symmetries)}, has to match the number \
@@ -97,12 +103,12 @@ def __init__(
         self.tol = tol
 
     @property
-    def symmetries(self) -> PauliList:
+    def symmetries(self) -> list[Pauli]:
         """Return symmetries."""
         return self._symmetries
 
     @property
-    def sq_paulis(self) -> PauliList:
+    def sq_paulis(self) -> list[Pauli]:
         """Return sq paulis."""
         return self._sq_paulis
 
@@ -116,7 +122,7 @@ def cliffords(self) -> list[SparsePauliOp]:
         """
         cliffords = [
             (SparsePauliOp(pauli_symm) + SparsePauliOp(sq_pauli)) / np.sqrt(2)
-            for pauli_symm, sq_pauli in zip(self._symmetries, self._sq_paulis)  # type: ignore
+            for pauli_symm, sq_pauli in zip(self._symmetries, self._sq_paulis)
         ]
         return cliffords
 
@@ -202,7 +208,7 @@ def find_z2_symmetries(cls, operator: SparsePauliOp) -> Z2Symmetries:
         }
 
         if _sparse_pauli_op_is_zero(operator):
-            return cls(PauliList([]), PauliList([]), [], None)
+            return cls([], [], [], None)
 
         for pauli in iter(operator):
             stacked_paulis.append(
@@ -213,7 +219,7 @@ def find_z2_symmetries(cls, operator: SparsePauliOp) -> Z2Symmetries:
         symmetries = _kernel_f2(stacked_matrix)
 
         if not symmetries:
-            return cls(PauliList([]), PauliList([]), [], None)
+            return cls([], [], [], None)
 
         stacked_symmetries = np.stack(symmetries)
         symm_shape = stacked_symmetries.shape
@@ -291,7 +297,7 @@ def _test_symmetry_row_col(row: int, col: int, idx_test: list, row_test: list) -
                     # We break out of the loop over columns only when one valid test is identified.
                     break
 
-        return cls(PauliList(pauli_symmetries), PauliList(sq_paulis), sq_list, None)
+        return cls(pauli_symmetries, sq_paulis, sq_list, None)
 
     def convert_clifford(self, operator: SparsePauliOp) -> SparsePauliOp:
         """This method operates the first part of the tapering.


Test patch:
diff --git a/test/python/quantum_info/test_sparse_z2_symmetries.py b/test/python/quantum_info/test_sparse_z2_symmetries.py
--- a/test/python/quantum_info/test_sparse_z2_symmetries.py
+++ b/test/python/quantum_info/test_sparse_z2_symmetries.py
@@ -23,6 +23,33 @@
 class TestSparseZ2Symmetries(QiskitTestCase):
     """Z2Symmetries tests."""
 
+    def test_create_empty(self):
+        """Test creating empty symmetry"""
+        z2_symmetries = Z2Symmetries(
+            symmetries=[],
+            sq_paulis=[],
+            sq_list=[],
+        )
+        self.assertTrue(z2_symmetries.is_empty())
+
+    def test_find_z2_symmetries_empty_op(self):
+        """Test finding symmetries of empty operator. Should return empty symmetry"""
+        qubit_op = SparsePauliOp.from_list([("II", 0.0)])
+        z2_symmetries = Z2Symmetries.find_z2_symmetries(qubit_op)
+        self.assertTrue(z2_symmetries.is_empty())
+
+    def test_find_z2_symmetries_op_without_sym(self):
+        """Test finding symmetries of operator without symmetries. Should return empty symmetry"""
+        qubit_op = SparsePauliOp.from_list(
+            [
+                ("I", -1.0424710218959303),
+                ("Z", -0.7879673588770277),
+                ("X", -0.18128880821149604),
+            ]
+        )
+        z2_symmetries = Z2Symmetries.find_z2_symmetries(qubit_op)
+        self.assertTrue(z2_symmetries.is_empty())
+
     def test_find_z2_symmetries(self):
         """test for find_z2_symmetries"""