Repository: Qiskit/qiskit

Issue:
Enhance appearance and correctness of Q-sphere visualization
<!-- ⚠️ 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?
Enhance appearance and correctness of Q-sphere visualization in the following ways:
- All complementary basis states are antipodal
- Phase is indicated by color of line and marker on sphere's surface
- Probability is indicated by translucency of line and volume of marker on sphere's surface

@nonhermitian and I have been collaborating on this enhancement, and are satisfied with the results. There is one, perhaps new, required dependency, namely [seaborn color palettes](https://seaborn.pydata.org/tutorial/color_palettes.html) for associating colors to phases in the Q-sphere plot as well as its legend. Here is an example Q-sphere plot with these enhancements:
<img width="509" alt="example-qsphere-plot" src="https://user-images.githubusercontent.com/9936602/62621482-edeaf580-b8e9-11e9-8373-113f281dd2ca.png">



Discussion:

Resolving patch:
diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py
--- a/qiskit/visualization/state_visualization.py
+++ b/qiskit/visualization/state_visualization.py
@@ -19,8 +19,10 @@
 """
 
 from functools import reduce
+import colorsys
 import numpy as np
 from scipy import linalg
+import seaborn as sns
 from qiskit.quantum_info.operators.pauli import pauli_group, Pauli
 from .matplotlib import HAS_MATPLOTLIB
 
@@ -28,8 +30,10 @@
     from matplotlib.ticker import MaxNLocator
     from matplotlib import pyplot as plt
     from matplotlib.patches import FancyArrowPatch
+    from matplotlib.patches import Circle
     import matplotlib.colors as mcolors
     from matplotlib.colors import Normalize, LightSource
+    import matplotlib.gridspec as gridspec
     from mpl_toolkits.mplot3d import proj3d
     from mpl_toolkits.mplot3d.art3d import Poly3DCollection
     from qiskit.visualization.exceptions import VisualizationError
@@ -444,33 +448,22 @@ def bit_string_index(s):
     return lex_index(n, k, ones)
 
 
-def phase_to_color_wheel(complex_number):
-    """Map a phase of a complex number to a color in (r,g,b).
+def phase_to_rgb(complex_number):
+    """Map a phase of a complexnumber to a color in (r,g,b).
 
     complex_number is phase is first mapped to angle in the range
-    [0, 2pi] and then to a color wheel with blue at zero phase.
+    [0, 2pi] and then to the HSL color wheel
     """
-    angles = np.angle(complex_number)
-    angle_round = int(((angles + 2 * np.pi) % (2 * np.pi))/np.pi*6)
-    color_map = {
-        0: (0, 0, 1),  # blue,
-        1: (0.5, 0, 1),  # blue-violet
-        2: (1, 0, 1),  # violet
-        3: (1, 0, 0.5),  # red-violet,
-        4: (1, 0, 0),  # red
-        5: (1, 0.5, 0),  # red-oranage,
-        6: (1, 1, 0),  # orange
-        7: (0.5, 1, 0),  # orange-yellow
-        8: (0, 1, 0),  # yellow,
-        9: (0, 1, 0.5),  # yellow-green,
-        10: (0, 1, 1),  # green,
-        11: (0, 0.5, 1)  # green-blue,
-    }
-    return color_map[angle_round]
+    angles = (np.angle(complex_number) + (np.pi * 4)) % (np.pi * 2)
+    rgb = colorsys.hls_to_rgb(angles / (np.pi * 2), 0.5, 0.5)
+    return rgb
 
 
 def plot_state_qsphere(rho, figsize=None):
     """Plot the qsphere representation of a quantum state.
+    Here, the size of the points is proportional to the probability
+    of the corresponding term in the state and the color represents
+    the phase.
 
     Args:
         rho (ndarray): State vector or density matrix representation.
@@ -489,9 +482,21 @@ def plot_state_qsphere(rho, figsize=None):
     if figsize is None:
         figsize = (7, 7)
     num = int(np.log2(len(rho)))
+
     # get the eigenvectors and eigenvalues
     we, stateall = linalg.eigh(rho)
-    for _ in range(2**num):
+
+    fig = plt.figure(figsize=figsize)
+    gs = gridspec.GridSpec(nrows=3, ncols=3)
+
+    ax = fig.add_subplot(gs[0:3, 0:3], projection='3d')
+    ax.axes.set_xlim3d(-1.0, 1.0)
+    ax.axes.set_ylim3d(-1.0, 1.0)
+    ax.axes.set_zlim3d(-1.0, 1.0)
+    ax.axes.grid(False)
+    ax.view_init(elev=5, azim=275)
+
+    for _ in range(2 ** num):
         # start with the max
         probmix = we.max()
         prob_location = we.argmax()
@@ -499,29 +504,22 @@ def plot_state_qsphere(rho, figsize=None):
             # get the max eigenvalue
             state = stateall[:, prob_location]
             loc = np.absolute(state).argmax()
+
             # get the element location closes to lowest bin representation.
-            for j in range(2**num):
+            for j in range(2 ** num):
                 test = np.absolute(np.absolute(state[j]) -
                                    np.absolute(state[loc]))
                 if test < 0.001:
                     loc = j
                     break
+
             # remove the global phase
             angles = (np.angle(state[loc]) + 2 * np.pi) % (2 * np.pi)
-            angleset = np.exp(-1j*angles)
-            # print(state)
-            # print(angles)
-            state = angleset*state
-            # print(state)
+            angleset = np.exp(-1j * angles)
+            state = angleset * state
             state.flatten()
+
             # start the plotting
-            fig = plt.figure(figsize=figsize)
-            ax = fig.add_subplot(111, projection='3d')
-            ax.axes.set_xlim3d(-1.0, 1.0)
-            ax.axes.set_ylim3d(-1.0, 1.0)
-            ax.axes.set_zlim3d(-1.0, 1.0)
-            ax.set_aspect("equal")
-            ax.axes.grid(False)
             # Plot semi-transparent sphere
             u = np.linspace(0, 2 * np.pi, 25)
             v = np.linspace(0, np.pi, 25)
@@ -530,7 +528,7 @@ def plot_state_qsphere(rho, figsize=None):
             z = np.outer(np.ones(np.size(u)), np.cos(v))
             ax.plot_surface(x, y, z, rstride=1, cstride=1, color='k',
                             alpha=0.05, linewidth=0)
-            # wireframe
+
             # Get rid of the panes
             ax.w_xaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
             ax.w_yaxis.set_pane_color((1.0, 1.0, 1.0, 0.0))
@@ -540,55 +538,91 @@ def plot_state_qsphere(rho, figsize=None):
             ax.w_xaxis.line.set_color((1.0, 1.0, 1.0, 0.0))
             ax.w_yaxis.line.set_color((1.0, 1.0, 1.0, 0.0))
             ax.w_zaxis.line.set_color((1.0, 1.0, 1.0, 0.0))
+
             # Get rid of the ticks
             ax.set_xticks([])
             ax.set_yticks([])
             ax.set_zticks([])
 
             d = num
-            for i in range(2**num):
+            for i in range(2 ** num):
+
                 # get x,y,z points
                 element = bin(i)[2:].zfill(num)
                 weight = element.count("1")
                 zvalue = -2 * weight / d + 1
                 number_of_divisions = n_choose_k(d, weight)
                 weight_order = bit_string_index(element)
-                # if weight_order >= number_of_divisions / 2:
-                #    com_key = compliment(element)
-                #    weight_order_temp = bit_string_index(com_key)
-                #    weight_order = np.floor(
-                #        number_of_divisions / 2) + weight_order_temp + 1
-                angle = weight_order * 2 * np.pi / number_of_divisions
-                xvalue = np.sqrt(1 - zvalue**2) * np.cos(angle)
-                yvalue = np.sqrt(1 - zvalue**2) * np.sin(angle)
-                ax.plot([xvalue], [yvalue], [zvalue],
-                        markerfacecolor=(.5, .5, .5),
-                        markeredgecolor=(.5, .5, .5),
-                        marker='o', markersize=10, alpha=1)
+                angle = (float(weight) / d) * (np.pi * 2) + \
+                        (weight_order * 2 * (np.pi / number_of_divisions))
+
+                if (weight > d / 2) or (((weight == d / 2) and
+                                         (weight_order >= number_of_divisions / 2))):
+                    angle = np.pi - angle - (2 * np.pi / number_of_divisions)
+
+                xvalue = np.sqrt(1 - zvalue ** 2) * np.cos(angle)
+                yvalue = np.sqrt(1 - zvalue ** 2) * np.sin(angle)
+
                 # get prob and angle - prob will be shade and angle color
                 prob = np.real(np.dot(state[i], state[i].conj()))
-                colorstate = phase_to_color_wheel(state[i])
+                colorstate = phase_to_rgb(state[i])
+
+                alfa = 1
+                if yvalue >= 0.1:
+                    alfa = 1.0 - yvalue
+
+                ax.plot([xvalue], [yvalue], [zvalue],
+                        markerfacecolor=colorstate,
+                        markeredgecolor=colorstate,
+                        marker='o', markersize=np.sqrt(prob) * 30, alpha=alfa)
+
                 a = Arrow3D([0, xvalue], [0, yvalue], [0, zvalue],
                             mutation_scale=20, alpha=prob, arrowstyle="-",
-                            color=colorstate, lw=10)
+                            color=colorstate, lw=2)
                 ax.add_artist(a)
+
             # add weight lines
             for weight in range(d + 1):
                 theta = np.linspace(-2 * np.pi, 2 * np.pi, 100)
                 z = -2 * weight / d + 1
-                r = np.sqrt(1 - z**2)
+                r = np.sqrt(1 - z ** 2)
                 x = r * np.cos(theta)
                 y = r * np.sin(theta)
-                ax.plot(x, y, z, color=(.5, .5, .5))
+                ax.plot(x, y, z, color=(.5, .5, .5), lw=1, ls=':', alpha=.5)
+
             # add center point
             ax.plot([0], [0], [0], markerfacecolor=(.5, .5, .5),
-                    markeredgecolor=(.5, .5, .5), marker='o', markersize=10,
+                    markeredgecolor=(.5, .5, .5), marker='o', markersize=3,
                     alpha=1)
             we[prob_location] = 0
         else:
             break
-    plt.tight_layout()
+
+    n = 32
+    theta = np.ones(n)
+
+    ax2 = fig.add_subplot(gs[2:, 2:])
+    ax2.pie(theta, colors=sns.color_palette("hls", n), radius=0.75)
+    ax2.add_artist(Circle((0, 0), 0.5, color='white', zorder=1))
+    ax2.text(0, 0, 'Phase', horizontalalignment='center',
+             verticalalignment='center', fontsize=14)
+
+    offset = 0.95  # since radius of sphere is one.
+
+    ax2.text(offset, 0, r'$0$', horizontalalignment='center',
+             verticalalignment='center', fontsize=14)
+    ax2.text(0, offset, r'$\pi/2$', horizontalalignment='center',
+             verticalalignment='center', fontsize=14)
+
+    ax2.text(-offset, 0, r'$\pi$', horizontalalignment='center',
+             verticalalignment='center', fontsize=14)
+
+    ax2.text(0, -offset, r'$3\pi/2$', horizontalalignment='center',
+             verticalalignment='center', fontsize=14)
+
+    fig.tight_layout()
     plt.close(fig)
+
     return fig
 
 
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,7 @@
     "ply>=3.10",
     "psutil>=5",
     "scipy>=1.0",
-    "sympy>=1.3"
+    "sympy>=1.3",
 ]
 
 # Add Cython extensions here
@@ -99,7 +99,8 @@
     python_requires=">=3.5",
     extras_require={
         'visualization': ['matplotlib>=2.1', 'nxpd>=0.2', 'ipywidgets>=7.3.0',
-                          'pydot', "pillow>=4.2.1", "pylatexenc>=1.4"],
+                          'pydot', "pillow>=4.2.1", "pylatexenc>=1.4",
+                          "seaborn>=0.9.0"],
         'full-featured-simulators': ['qiskit-aer>=0.1']
     },
     project_urls={


Test patch: