Repository: qutip/qutip

Issue:
mesolve/sesolve options have a mutable default value
There is a problem with `mesolve` and `sesolve`'s default `options`, which determine whether the propagated states are stored in the `result` object:

~~~
>>> import qutip
>>> import numpy as np
>>> qutip.__version__
'4.5.0.dev0+19f172cb'
>>> H = qutip.sigmaz() + qutip.sigmax()
>>> psi = qutip.ket('0')
>>> tlist = np.linspace(0,10, 200)
>>> p0 = qutip.ket2dm(psi)
>>> res1 = qutip.mesolve(H, psi, tlist, e_ops=[p0])
>>> len(res1.states)  # should be 0
0
>>> len(res1.expect)  # should be 1
1
>>>
>>> len(res2.states)  # should be 200
200
>>> len(res2.expect)  # should be 0
0
>>> res3 = qutip.mesolve(H, psi, tlist, e_ops=[p0])
>>> len(res3.states)  # should be 0 (same as res1)
200
>>> len(res3.expect)  # should be 1
1
~~~
In `res3`, there should be no stored `states`, as I'm requesting expectation values. The problem is due to the common gotcha of [mutable default arguments](https://docs.python-guide.org/writing/gotchas/#mutable-default-arguments) in the definition of of e.g. `mesolve`:
~~~
def mesolve(H, rho0, tlist, c_ops=[], e_ops=[], args={}, options=Options(),
    progress_bar=BaseProgressBar(), _safe_mode=True)
~~~
What happens is that the default `options` get instantiated once when `qutip` is imported. In the example above `res2 = qutip.mesolve(H, psi, tlist)` will then internally set `options.store_states = True`, and this value will still be there for `res3 = qutip.mesolve(H, psi, tlist, e_ops=[p0])`.

Also, `c_ops`, `e_ops`, `args`, and `progress_bar` are all mutable in this definition, which could cause more problems. The correct thing to do is to initialize with `None` and set the mutable defaults in the body of the function.

I can prepare a pull request for this.


Discussion:

Resolving patch:
diff --git a/qutip/mesolve.py b/qutip/mesolve.py
--- a/qutip/mesolve.py
+++ b/qutip/mesolve.py
@@ -58,8 +58,8 @@
 # pass on to wavefunction solver or master equation solver depending on whether
 # any collapse operators were given.
 #
-def mesolve(H, rho0, tlist, c_ops=[], e_ops=[], args={}, options=Options(),
-            progress_bar=BaseProgressBar(), _safe_mode=True):
+def mesolve(H, rho0, tlist, c_ops=None, e_ops=None, args=None, options=None,
+            progress_bar=None, _safe_mode=True):
     """
     Master equation evolution of a density matrix for a given Hamiltonian and
     set of collapse operators, or a Liouvillian.
@@ -157,22 +157,22 @@ def mesolve(H, rho0, tlist, c_ops=[], e_ops=[], args={}, options=Options(),
     tlist : *list* / *array*
         list of times for :math:`t`.
 
-    c_ops : list of :class:`qutip.Qobj`
+    c_ops : None / list of :class:`qutip.Qobj`
         single collapse operator, or list of collapse operators, or a list
         of Liouvillian superoperators.
 
-    e_ops : list of :class:`qutip.Qobj` / callback function single
+    e_ops : None / list of :class:`qutip.Qobj` / callback function single
         single operator or list of operators for which to evaluate
         expectation values.
 
-    args : *dictionary*
+    args : None / *dictionary*
         dictionary of parameters for time-dependent Hamiltonians and
         collapse operators.
 
-    options : :class:`qutip.Options`
+    options : None / :class:`qutip.Options`
         with options for the solver.
 
-    progress_bar : BaseProgressBar
+    progress_bar : None / BaseProgressBar
         Optional instance of BaseProgressBar, or a subclass thereof, for
         showing the progress of the simulation.
 
@@ -188,11 +188,13 @@ def mesolve(H, rho0, tlist, c_ops=[], e_ops=[], args={}, options=Options(),
         operators for which to calculate the expectation values.
 
     """
-    # check whether c_ops or e_ops is is a single operator
-    # if so convert it to a list containing only that operator
+    if c_ops is None:
+        c_ops = []
     if isinstance(c_ops, (Qobj, QobjEvo)):
         c_ops = [c_ops]
 
+    if e_ops is None:
+        e_ops = []
     if isinstance(e_ops, Qobj):
         e_ops = [e_ops]
 
@@ -202,6 +204,8 @@ def mesolve(H, rho0, tlist, c_ops=[], e_ops=[], args={}, options=Options(),
     else:
         e_ops_dict = None
 
+    if progress_bar is None:
+        progress_bar = BaseProgressBar()
     if progress_bar is True:
         progress_bar = TextProgressBar()
 
@@ -212,6 +216,8 @@ def mesolve(H, rho0, tlist, c_ops=[], e_ops=[], args={}, options=Options(),
         raise TypeError("Must have e_ops = [] when initial condition rho0 is" +
                 " a superoperator.")
 
+    if options is None:
+        options = Options()
     if options.rhs_reuse and not isinstance(H, SolverSystem):
         # TODO: deprecate when going to class based solver.
         if "mesolve" in solver_safe:
@@ -221,7 +227,9 @@ def mesolve(H, rho0, tlist, c_ops=[], e_ops=[], args={}, options=Options(),
             pass
             # raise Exception("Could not find the Hamiltonian to reuse.")
 
-    #check if should use OPENMP
+    if args is None:
+        args = {}
+
     check_use_openmp(options)
 
     use_mesolve = ((c_ops and len(c_ops) > 0)
diff --git a/qutip/sesolve.py b/qutip/sesolve.py
--- a/qutip/sesolve.py
+++ b/qutip/sesolve.py
@@ -53,8 +53,8 @@
 from qutip.ui.progressbar import (BaseProgressBar, TextProgressBar)
 from qutip.cy.openmp.utilities import check_use_openmp, openmp_components
 
-def sesolve(H, psi0, tlist, e_ops=[], args={}, options=Options(),
-            progress_bar=BaseProgressBar(), _safe_mode=True):
+def sesolve(H, psi0, tlist, e_ops=None, args=None, options=None,
+            progress_bar=None, _safe_mode=True):
     """
     Schrodinger equation evolution of a state vector or unitary matrix
     for a given Hamiltonian.
@@ -86,19 +86,19 @@ def sesolve(H, psi0, tlist, e_ops=[], args={}, options=Options(),
     tlist : *list* / *array*
         list of times for :math:`t`.
 
-    e_ops : list of :class:`qutip.qobj` / callback function
+    e_ops : None / list of :class:`qutip.qobj` / callback function
         single operator or list of operators for which to evaluate
         expectation values.
         For list operator evolution, the overlapse is computed:
             tr(e_ops[i].dag()*op(t))
 
-    args : *dictionary*
+    args : None / *dictionary*
         dictionary of parameters for time-dependent Hamiltonians
 
-    options : :class:`qutip.Qdeoptions`
+    options : None / :class:`qutip.Qdeoptions`
         with options for the ODE solver.
 
-    progress_bar : BaseProgressBar
+    progress_bar : None / BaseProgressBar
         Optional instance of BaseProgressBar, or a subclass thereof, for
         showing the progress of the simulation.
 
@@ -115,6 +115,8 @@ def sesolve(H, psi0, tlist, e_ops=[], args={}, options=Options(),
         which to calculate the expectation values.
 
     """
+    if e_ops is None:
+        e_ops = []
     if isinstance(e_ops, Qobj):
         e_ops = [e_ops]
     elif isinstance(e_ops, dict):
@@ -123,6 +125,8 @@ def sesolve(H, psi0, tlist, e_ops=[], args={}, options=Options(),
     else:
         e_ops_dict = None
 
+    if progress_bar is None:
+        progress_bar = BaseProgressBar()
     if progress_bar is True:
         progress_bar = TextProgressBar()
 
@@ -131,6 +135,8 @@ def sesolve(H, psi0, tlist, e_ops=[], args={}, options=Options(),
                         " a ket as initial state"
                         " or a unitary as initial operator.")
 
+    if options is None:
+        options = Options()
     if options.rhs_reuse and not isinstance(H, SolverSystem):
         # TODO: deprecate when going to class based solver.
         if "sesolve" in solver_safe:
@@ -140,7 +146,9 @@ def sesolve(H, psi0, tlist, e_ops=[], args={}, options=Options(),
             pass
             # raise Exception("Could not find the Hamiltonian to reuse.")
 
-    #check if should use OPENMP
+    if args is None:
+        args = {}
+
     check_use_openmp(options)
 
     if isinstance(H, SolverSystem):


Test patch: