Repository: qutip/qutip

Issue:
Bloch sphere not showing on b.show() command, only flashes window briefly
When attempting to create (and then draw on) the Bloch sphere, the command b.show() as per the documentation (after doing the correct setup) does not display the Bloch sphere. Rather a matplotlib window appears for a fraction of a second before closing itself.
Using plt.pause() after the b.show() line allows this window to remain open however it is simply black with nothing rendered on it.
I am able to get a graph of the Bloch sphere by using the plt.show() command in place of b.show(). However this also brings up another window at the same time showing only a set of 3d axes.

Here is my MWE

```python
import qutip
import matplotlib.pyplot as plt

b = qutip.Bloch()

b.make_sphere()
b.render()

```
This is the line I can replace with plt.show()
```
b.show()
```

As per the documentation I would expect b.show() to be the only command required to display the Bloch sphere. I have attempted to install correctly but I assume I am missing something still.

My environment is:
QuTiP Version:      4.6.2
Numpy Version:      1.21.1
Scipy Version:      1.6.2
Cython Version:     0.29.24
Matplotlib Version: 3.0.3
Python Version:     3.7.1
Number of CPUs:     4
BLAS Info:          OPENBLAS
OPENMP Installed:   False
INTEL MKL Ext:      False
Platform Info:      Windows (AMD64)
Installation path:  C:\ProgramData\Anaconda3\lib\site-packages\qutip

Thanks



Discussion:
Hi Chester,

I'm not that familiar with matplotlib on Windows, but I'm guessing that
something has gone wrong with the event loop being used to render the
matplotlib figures (possibly QuTiP did something wrong or maybe something
else happened).

Could you confirm that you're using the TkAgg matplotlib backend? You can
check with matplot.get_backend(). And are you using IPython or the ordinary
Python console?

Usually only:

b = qutip.Bloch()
b.show()

is required to display the plot. Behind the scenes .show() and
.make_sphere() both call .render(...). I suspect that the empty axes you
are seeing might be related to having rendered the sphere a few different
times.

Could you start a new Python console and try:

import qutip
import matplotlib.pyplot as plt
b = qutip.Bloch()
b.render()
plt.show()

I'm hoping that will give you just the plot you are looking for (and so
give you a work around for now). The reason this might be slightly more
robust is that internally b.show() renders the sphere onto the existing
axes, which could possibly be related to the issue you are seeing. After
closing the plot window, running b.render() and plt.show() should display
the sphere again.

If that fails, could you confirm that you can show any plots with
matplotlib?

You could also try updating to a more recent matplotlib, but let's perhaps
leave that as a last resort.

Yours sincerely,
Simon Cross

So I decided to reinstall Python (Anaconda specifically) to get a fresh slate as in my own debugging and trying to update packages other code started to behave weirdly.

Here is my current qutip.about():
QuTiP Version:      4.6.2
Numpy Version:      1.20.1
Scipy Version:      1.6.2
Cython Version:     0.29.23
Matplotlib Version: 3.3.4
Python Version:     3.8.8
Number of CPUs:     4
BLAS Info:          INTEL MKL
OPENMP Installed:   False
INTEL MKL Ext:      False
Platform Info:      Windows (AMD64)
Installation path:  C:\ProgramData\Anaconda3\lib\site-packages\qutip

I am using the Qt5Agg matplotlib backend, and changing to the TkAgg backend gives the same behaviour.

However, running the code you pasted in a separate (command line) console I am able to view the sphere with even the b.show() command.
I still have this behaviour in my editor however (Visual Studio Code) so I think the problem is something to so with my debug environment, this code even behaves as it should in the powershell window in VS code. 
I know this isnt the VS code issue page but if you have any suggestions I'd much appreciate it
@ChesterCamm I don't have any experience with VS code myself, but perhaps someone else reading this does. If you (or someone else) get it working, please post a reply!

@ChesterCamm Are you running this with one of the VS code Jupyter extensions? Or directly in VS code? (I really know nothing about what VS code looks like :).
Directly in VS code. My launch.json looks like
```json
	"version": "0.2.0",
	"configurations": [
		{
			"name": "Python: Current File",
			"type": "python",
			"request": "launch",
			"program": "${file}",
			"console": "integratedTerminal"
		}
	]
}
```
When using the plt.show() command, the second window only appears after a b.render() command, but thats also necessary for any vectors I want to draw to appear.
If you have several different Conda environments. Sometimes VSCode chooses the wrong one. You could check if the default environment used is the same as the one you used in the console at the left bottom corner. Something like:
![image](https://user-images.githubusercontent.com/12125783/126348148-3515b001-a30b-436c-8344-188787379785.png)
The `qutip:conda` is the Conda environment. Or open a terminal in VSCode and do that manually.

If you are using the Jupyter notebook extension then that's again different. Somewhere in the right-upper corner.

To try keep things simple for me, I only have the base environment which VS code activates before any code is run.
The text I have in that box is `Python 3.8.8 64-bit (conda)`. I realise I'm missing the `('base':conda)` as per https://code.visualstudio.com/docs/python/python-tutorial however I havent yet found why this is the case. I am continuing to look as I feel this might be the crux of the problem.

As another work around I found that (after updating matplotlib to my current version) I can get some of the expected behaviour from b.show() within VS code by adding the `plt.pause(interval)` command after b.show() (as descibed previously however now I can actually render someting), where the interval is how many seconds I want to view the Bloch sphere for. The b.save() function also works as expected so I can use that as a workaround for now at least.

Some more odd behaviour - likely caused by all the updating an reinstalling trying to fix things - is that the plt.show() no longer works as described before. Not a massive loss as I've gained other functionality and I am still able to render other graphs (so matplotlib isnt broken) and the bare 3d axes have stopped appearing.
Not sure if this is a conda issue now as I can run the test script in the windows powershell terminal with `python .\qutiptest.py` where qutiptest.py is
```python
import qutip
b=qutip.Bloch()
b.show()
```
and I get the same behaviour as when I run throught the VS code debugger, where the matplotlib window flashes briefly. I can also recreate the rest of the VS code behaviour like this.
This sounds like `matplotlib` just doing its normal thing when handling Windows backends. Does the problem persist if you put in the lines
```python
from matplotlib import pyplot
pyplot.ion()
```
as the first things in your script?

See also: https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.isinteractive.html
At any rate, the root problem is likely to do with how your VS Code is handling matplotlib. I also don't know anything about VS Code, but there's a chance it's forking matplotlib calls into a separate process or something, and if those are in interactive mode, they may be nonblocking so the process returns immediately. In writing this out, I think I may have got it backwards in my previous comment - also try `pyplot.ioff()` instead of `ion()` at the start.
So after starting with a fresh install of Anaconda and going through the steps described in https://qutip.org/docs/4.1/installation.html to install through conda-forge and conda, I am still seeing the same behaviour. However I have found a workaround that gives me exactly what I want.

During my debugging I ending up adding the `b.make_sphere()` command, this seems to have caused the extra set of axes to keep appearing. I found that by omitting the  `make_sphere` command and just replacing `b.show()` with `plt.show()` worked perfectly.
Heres my MWE
```python
import qutip
import matplotlib.pyplot as plt
b=qutip.Bloch()

b.add_vectors([1,0,0])
b.render()

plt.show()
```

My `qutip.about()` output is:
```
QuTiP Version:      4.6.2
Numpy Version:      1.20.3
Scipy Version:      1.6.2
Cython Version:     0.29.24
Matplotlib Version: 3.3.4
Python Version:     3.9.5
Number of CPUs:     4
BLAS Info:          INTEL MKL
OPENMP Installed:   False
INTEL MKL Ext:      False
Platform Info:      Windows (AMD64)
Installation path:  C:\Users\Chester\.conda\envs\qutip-env\lib\site-packages\qutip
```

Hopefully this helps someone.

@jakelishman I tried interactive mode toggling when this problem first occurred as that's what the behaviour reminded me of too. Gave it another go with my example in this text (replacing the `plt.show()` with `b.show()`) but still same behaviour.
I think it's to do with something similar to the problem/solution in https://stackoverflow.com/questions/12369693/how-to-make-the-plot-not-disappear/12905317 as `self.fig` within the Bloch class is initialised in `render()` (and then called in `show()`) a similar way. But I am not familiar enough with the qutip backend to know if this is an error, especially as this does not seem like a common issue.
Great that you fixed it!  Our Bloch sphere hasn't been really looked at deeply in a while.  I know Paul copied it into Qiskit, and having a look at things over there, looks like he made some changes in Qiskit/qiskit-terra#1091, which we may want to look at.  I don't think we need to worry much about the licensing (terra is Apache for reference), since we're just looking at a basic call to the mpl API.

Resolving patch:
diff --git a/qutip/bloch.py b/qutip/bloch.py
--- a/qutip/bloch.py
+++ b/qutip/bloch.py
@@ -40,12 +40,12 @@ def draw(self, renderer):
 
             self.set_positions((xs[0], ys[0]), (xs[1], ys[1]))
             FancyArrowPatch.draw(self, renderer)
-except:
+except ImportError:
     pass
 
 try:
     from IPython.display import display
-except:
+except ImportError:
     pass
 
 
@@ -112,6 +112,7 @@ def __init__(self, fig=None, axes=None, view=None, figsize=None,
                  background=False):
         # Figure and axes
         self.fig = fig
+        self._ext_fig = fig is not None
         self.axes = axes
         # Background axes, default = False
         self.background = background
@@ -178,14 +179,6 @@ def __init__(self, fig=None, axes=None, view=None, figsize=None,
         # Style of points, 'm' for multiple colors, 's' for single color
         self.point_style = []
 
-        # status of rendering
-        self._rendered = False
-        # status of showing
-        if fig is None:
-            self._shown = False
-        else:
-            self._shown = True
-
     def set_label_convention(self, convention):
         """Set x, y and z labels according to one of conventions.
 
@@ -405,7 +398,7 @@ def make_sphere(self):
         """
         Plots Bloch sphere and data sets.
         """
-        self.render(self.fig, self.axes)
+        self.render()
 
     def run_from_ipython(self):
         try:
@@ -414,30 +407,50 @@ def run_from_ipython(self):
         except NameError:
             return False
 
-    def render(self, fig=None, axes=None):
+    def _is_inline_backend(self):
+        backend = matplotlib.get_backend()
+        return backend == "module://matplotlib_inline.backend_inline"
+
+    def render(self):
         """
         Render the Bloch sphere and its data sets in on given figure and axes.
         """
-        if self._rendered:
-            self.axes.clear()
-
-        self._rendered = True
-
-        # Figure instance for Bloch sphere plot
-        if not fig:
+        if not self._ext_fig and not self._is_inline_backend():
+            # If no external figure was supplied, we check to see if the
+            # figure we created in a previous call to .render() has been
+            # closed, and re-create if has been. This has the unfortunate
+            # side effect of losing any modifications made to the axes or
+            # figure, but the alternative is to crash the matplotlib backend.
+            #
+            # The inline backend used by, e.g. jupyter notebooks, is happy to
+            # use closed figures so we leave those figures intact.
+            if (
+                self.fig is not None and
+                not plt.fignum_exists(self.fig.number)
+            ):
+                self.fig = None
+                self.axes = None
+
+        if self.fig is None:
             self.fig = plt.figure(figsize=self.figsize)
-
-        if not axes:
-            self.axes = _axes3D(self.fig, azim=self.view[0],
-                                elev=self.view[1])
-
+            if self._is_inline_backend():
+                # We immediately close the inline figure do avoid displaying
+                # the figure twice when .show() calls display.
+                plt.close(self.fig)
+
+        if self.axes is None:
+            self.axes = _axes3D(self.fig, azim=self.view[0], elev=self.view[1])
+
+        # Clearing the axes is horrifically slow and loses a lot of the
+        # axes state, but matplotlib doesn't seem to provide a better way
+        # to redraw Axes3D. :/
+        self.axes.clear()
+        self.axes.grid(False)
         if self.background:
-            self.axes.clear()
             self.axes.set_xlim3d(-1.3, 1.3)
             self.axes.set_ylim3d(-1.3, 1.3)
             self.axes.set_zlim3d(-1.3, 1.3)
         else:
-            self.plot_axes()
             self.axes.set_axis_off()
             self.axes.set_xlim3d(-0.7, 0.7)
             self.axes.set_ylim3d(-0.7, 0.7)
@@ -446,14 +459,17 @@ def render(self, fig=None, axes=None):
         # Matplotlib did this stretching for < 3.3.0, but not above.
         if parse_version(matplotlib.__version__) >= parse_version('3.3'):
             self.axes.set_box_aspect((1, 1, 1))
+        if not self.background:
+            self.plot_axes()
 
-        self.axes.grid(False)
         self.plot_back()
         self.plot_points()
         self.plot_vectors()
         self.plot_front()
         self.plot_axes_labels()
         self.plot_annotations()
+        # Trigger an update of the Bloch sphere if it is already shown:
+        self.fig.canvas.draw()
 
     def plot_back(self):
         # back half of sphere
@@ -622,14 +638,23 @@ def plot_annotations(self):
     def show(self):
         """
         Display Bloch sphere and corresponding data sets.
+
+        Notes
+        -----
+
+        When using inline plotting in Jupyter notebooks, any figure created
+        in a notebook cell is displayed after the cell executes. Thus if you
+        create a figure yourself and use it create a Bloch sphere with
+        ``b = Bloch(..., fig=fig)`` and then call ``b.show()`` in the same
+        cell, then the figure will be displayed twice. If you do create your
+        own figure, the simplest solution to this is to not call ``.show()``
+        in the cell you create the figure in.
         """
-        self.render(self.fig, self.axes)
+        self.render()
         if self.run_from_ipython():
-            if self._shown:
-                display(self.fig)
+            display(self.fig)
         else:
             self.fig.show()
-        self._shown = True
 
     def save(self, name=None, format='png', dirc=None, dpin=None):
         """Saves Bloch sphere to file of type ``format`` in directory ``dirc``.
@@ -653,7 +678,7 @@ def save(self, name=None, format='png', dirc=None, dpin=None):
         File containing plot of Bloch sphere.
 
         """
-        self.render(self.fig, self.axes)
+        self.render()
         # Conditional variable for first argument to savefig
         # that is set in subsequent if-elses
         complete_path = ""


Test patch: