Repository: Qiskit/qiskit

Issue:
improve the speed of sgn_prod in pauli.py
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?
Speed improvement.

Since current Pauli class use int or float vector to present `v` and `w` vector, it is inefficient if we would like to check it is X, Y, Z, or I (we need to compare `v` and `w` with 1 and 0.)

If we support native boolean representation, it will be faster. 
Nonetheless, I do not have a good idea to change whole `Pauli` class to boolean representation while supports backward-compatibility.

Thus, a workaround to improve the speed of `sgn_prod` only, we can internally convert it to boolean and then use boolean to track the phase, and then convert `v` and `w` back to int or float.

here is the prototype of my implementation on `sgn_prod`:
```python
def sgn_prod(P1, P2):
    """Multiply two Paulis P1*P2 and track the sign.

    P3 = P1*P2: X*Y
    """

    if P1.numberofqubits != P2.numberofqubits:
        print('Paulis cannot be multiplied - different number of qubits')

    p1_v = P1.v.astype(np.bool)
    p1_w = P1.w.astype(np.bool)
    p2_v = P2.v.astype(np.bool)
    p2_w = P2.w.astype(np.bool)

    v_new = np.logical_xor(p1_v, p2_v).astype(np.int)
    w_new = np.logical_xor(p1_w, p2_w).astype(np.int)

    paulinew = Pauli(v_new, w_new)
    phase = 0  # 1

    for v1, w1, v2, w2 in zip(p1_v, p1_w, p2_v, p2_w):
        if v1 and not w1:  # Z
            if w2:
                phase = phase - 1 if v2 else phase + 1
        elif not v1 and w1:  # X
            if v2:
                phase = phase + 1 if w2 else phase - 1
        elif v1 and w1:  # Y
            if not v2 and w2:  # X
                phase -= 1  # -1j * phase
            elif v2 and not w2:  # Z
                phase += 1  # 1j * phase
    phase = (1j) ** (phase % 4)
    return paulinew, phase
```

and this is my test case:
```python
from qiskit.tools.qi.pauli import sgn_prod, Pauli, label_to_pauli
import itertools
import time

num_qubits = 5

# generate 2^num_qubits paulis
paulis = []
for pauli_label in itertools.product('IXYZ', repeat=num_qubits):
    pauli_term = label_to_pauli(pauli_label)
    paulis.append(pauli_term)
print("number of paulis: {}".format(len(paulis)))

start = time.time()
for pauli_a in paulis:
    for pauli_b in paulis:
        a, sign = sgn_prod(pauli_a, pauli_b)
end = time.time()
print(end-start)
```

Here is some benchmark on macOS 10.13, python 3.6

```
num_qubit=4
new implementation: 0.477 seconds
old implementation: 1.189 seconds
```

```
num_qubit=5
new implementation: 7.593 seconds
old implementation: 21.895 seconds
```

```
num_qubit=6
new implementation: 129.231 seconds
old implementation: 403.953 seconds
```



Discussion:
@chunfuchen this is good just make a pr and submit. 

Resolving patch:
diff --git a/qiskit/tools/qi/pauli.py b/qiskit/tools/qi/pauli.py
--- a/qiskit/tools/qi/pauli.py
+++ b/qiskit/tools/qi/pauli.py
@@ -168,30 +168,31 @@ def sgn_prod(P1, P2):
 
     if P1.numberofqubits != P2.numberofqubits:
         print('Paulis cannot be multiplied - different number of qubits')
-    v_new = (P1.v + P2.v) % 2
-    w_new = (P1.w + P2.w) % 2
-    paulinew = Pauli(v_new, w_new)
-    phase = 1
-    for i in range(len(P1.v)):
-        if P1.v[i] == 1 and P1.w[i] == 0 and P2.v[i] == 0 and P2.w[i] == 1:
-            # Z*X
-            phase = 1j * phase
-        elif P1.v[i] == 0 and P1.w[i] == 1 and P2.v[i] == 1 and P2.w[i] == 0:
-            # X*Z
-            phase = -1j * phase
-        elif P1.v[i] == 0 and P1.w[i] == 1 and P2.v[i] == 1 and P2.w[i] == 1:
-            # X*Y
-            phase = 1j * phase
-        elif P1.v[i] == 1 and P1.w[i] == 1 and P2.v[i] == 0 and P2.w[i] == 1:
-            # Y*X
-            phase = -1j * phase
-        elif P1.v[i] == 1 and P1.w[i] == 1 and P2.v[i] == 1 and P2.w[i] == 0:
-            # Y*Z
-            phase = 1j * phase
-        elif P1.v[i] == 1 and P1.w[i] == 0 and P2.v[i] == 1 and P2.w[i] == 1:
-            # Z*Y
-            phase = -1j * phase
 
+    p1_v = P1.v.astype(np.bool)
+    p1_w = P1.w.astype(np.bool)
+    p2_v = P2.v.astype(np.bool)
+    p2_w = P2.w.astype(np.bool)
+
+    v_new = np.logical_xor(p1_v, p2_v).astype(np.int)
+    w_new = np.logical_xor(p1_w, p2_w).astype(np.int)
+
+    paulinew = Pauli(v_new, w_new)
+    phase_changes = 0
+
+    for v1, w1, v2, w2 in zip(p1_v, p1_w, p2_v, p2_w):
+        if v1 and not w1:  # Z
+            if w2:
+                phase_changes = phase_changes - 1 if v2 else phase_changes + 1
+        elif not v1 and w1:  # X
+            if v2:
+                phase_changes = phase_changes + 1 if w2 else phase_changes - 1
+        elif v1 and w1:  # Y
+            if not v2 and w2:  # X
+                phase_changes -= 1
+            elif v2 and not w2:  # Z
+                phase_changes += 1
+    phase = (1j) ** (phase_changes % 4)
     return paulinew, phase
 
 


Test patch: