Repository: qutip/qutip

Issue:
Wrong default weight parameter in steadystate function
**Issue**
In the codebase, the weight for setting the unit trace condition in the steadystate function should be set according to the documentation to the average value of the (nonzero) elements in the Liouvillian L, if not specified explicitly.

However in the code the weight is set as 
```python
 ss_args['weight'] = np.mean(np.abs(A.data.data.max()))
```
which is clearly different as it just set to the maximum value in the Liouvillian matrix A and hence the mean and abs are meaningless.

 I think this could be an overlooked mistake, or else just a mistake in the documentation of the parameter.



Discussion:
Thanks for reporting this.

This appears to have _always_ been an inconsistency between the documentation and the code.  These lines were added in one commit in #166, with no reference to any changes.  I note at the moment that calling `max` on an array of complex (like this) finds the number with the maximal _real_ part, and then takes the absolute value of that.

I suspect that the `max` call got added in after everything else was written - if you remove that call, you would retrieve the documented behaviour, though I don't know enough about the numerical strategies to know which method is safer; I don't know whether we should change the documentationi or the behaviour. @nwlambert might be better placed to answer that.
Thanks for the swift reply.
I am writing a similar code from scratch and so, I was wondering what qutip is using.
Perhaps there is some literature about this. But I guess setting a large weight makes the first row too dominant for iterative solvers, which could be the case in the current implementation.
I think a fix for this was proposed in this PR https://github.com/qutip/qutip/pull/1275  but that fix seems to still take the max over absolute values, so maybe is still inconsistent with documentation?

I also don't know  if there is a  reason to choose one over the other, I am not super familiar with the iterative solvers.     Could just be a typo or some unfixed testing,  particularly since it makes taking the mean completely pointless.

David's logic for the iterative solvers sounds like a good reason to revert it to the documented case.   If there is still some uncertainty we could ask paul (if he remembers!).


Resolving patch:
diff --git a/qutip/steadystate.py b/qutip/steadystate.py
--- a/qutip/steadystate.py
+++ b/qutip/steadystate.py
@@ -270,7 +270,8 @@ def steadystate(A, c_op_list=[], method='direct', solver=None, **kwargs):
 
     # Set weight parameter to avg abs val in L if not set explicitly
     if 'weight' not in kwargs.keys():
-        ss_args['weight'] = np.mean(np.abs(A.data.data.max()))
+        # set the weight to the mean of the non-zero absoluate values in A:
+        ss_args['weight'] = np.mean(np.abs(A.data.data))
         ss_args['info']['weight'] = ss_args['weight']
 
     if ss_args['method'] == 'direct':
@@ -344,7 +345,12 @@ def _steadystate_LU_liouvillian(L, ss_args, has_mkl=0):
         if settings.debug:
             logger.debug('Calculating Weighted Bipartite Matching ordering...')
         _wbm_start = time.time()
-        perm = weighted_bipartite_matching(L)
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore", "qutip graph functions are deprecated",
+                DeprecationWarning,
+            )
+            perm = weighted_bipartite_matching(L)
         _wbm_end = time.time()
         L = sp_permute(L, perm, [], form)
         ss_args['info']['perm'].append('wbm')
@@ -481,7 +487,7 @@ def _steadystate_direct_dense(L, ss_args):
     b[0] = ss_args['weight']
 
     L = L.full()
-    L[0, :] = np.diag(ss_args['weight']*np.ones(n)).reshape((1, n ** 2))
+    L[0, :] += np.diag(ss_args['weight']*np.ones(n)).reshape(n ** 2)
     _dense_start = time.time()
     v = np.linalg.solve(L, b)
     _dense_end = time.time()
@@ -555,8 +561,7 @@ def _iterative_precondition(A, n, ss_args):
                   fill_factor=ss_args['fill_factor'],
                   options=dict(ILU_MILU=ss_args['ILU_MILU']))
 
-        P_x = lambda x: P.solve(x)
-        M = LinearOperator((n ** 2, n ** 2), matvec=P_x)
+        M = LinearOperator((n ** 2, n ** 2), matvec=P.solve)
         _precond_end = time.time()
         ss_args['info']['permc_spec'] = ss_args['permc_spec']
         ss_args['info']['drop_tol'] = ss_args['drop_tol']
@@ -583,7 +588,7 @@ def _iterative_precondition(A, n, ss_args):
                 logger.debug('Fill factor: %f' % ((L_nnz+U_nnz)/A.nnz))
                 logger.debug('iLU condest: %f' % condest)
 
-    except:
+    except Exception:
         raise Exception("Failed to build preconditioner. Try increasing " +
                         "fill_factor and/or drop_tol.")
 
@@ -728,7 +733,6 @@ def _steadystate_power_liouvillian(L, ss_args, has_mkl=0):
     else:
         L = L.data.tocsc() - (1e-15) * sp.eye(n, n, format='csc')
         kind = 'csc'
-    orig_nnz = L.nnz
     if settings.debug:
         old_band = sp_bandwidth(L)[0]
         old_pro = sp_profile(L)[0]
@@ -739,7 +743,12 @@ def _steadystate_power_liouvillian(L, ss_args, has_mkl=0):
         if settings.debug:
             logger.debug('Calculating Weighted Bipartite Matching ordering...')
         _wbm_start = time.time()
-        perm = weighted_bipartite_matching(L)
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore", "qutip graph functions are deprecated",
+                DeprecationWarning,
+            )
+            perm = weighted_bipartite_matching(L)
         _wbm_end = time.time()
         L = sp_permute(L, perm, [], kind)
         ss_args['info']['perm'].append('wbm')
@@ -1138,7 +1147,7 @@ def _pseudo_inverse_dense(L, rhoss, w=None, **pseudo_args):
     if pseudo_args['method'] == 'direct':
         try:
             LIQ = np.linalg.solve(L.full(), Q)
-        except:
+        except Exception:
             LIQ = np.linalg.lstsq(L.full(), Q)[0]
 
         R = np.dot(Q, LIQ)
@@ -1160,8 +1169,9 @@ def _pseudo_inverse_dense(L, rhoss, w=None, **pseudo_args):
                     dims=L.dims)
 
     else:
-        raise ValueError("Unsupported method '%s'. Use 'direct' or 'numpy'" %
-                         method)
+        raise ValueError(
+            "Unsupported method '%s'. Use 'direct', 'numpy', 'scipy' or"
+            " 'scipy2'" % pseudo_args['method'])
 
 
 def _pseudo_inverse_sparse(L, rhoss, w=None, **pseudo_args):


Test patch:
diff --git a/qutip/tests/test_steadystate.py b/qutip/tests/test_steadystate.py
--- a/qutip/tests/test_steadystate.py
+++ b/qutip/tests/test_steadystate.py
@@ -63,6 +63,32 @@ def test_qubit(method, kwargs):
     np.testing.assert_allclose(p_ss_analytic, p_ss, atol=1e-5)
 
 
+@pytest.mark.parametrize(['method', 'kwargs'], [
+    pytest.param('direct', {}, id="direct"),
+    pytest.param('direct', {'solver': 'mkl'}, id="direct_mkl",
+                 marks=pytest.mark.skipif(not qutip.settings.has_mkl,
+                                          reason='MKL extensions not found.')),
+    pytest.param('direct', {'sparse': False}, id="direct_dense"),
+    pytest.param('direct', {'use_rcm': True}, id="direct_rcm"),
+    pytest.param('direct', {'use_wbm': True}, id="direct_wbm"),
+    pytest.param('eigen', {}, id="eigen"),
+    pytest.param('eigen', {'use_rcm': True},  id="eigen_rcm"),
+    pytest.param('svd', {}, id="svd"),
+])
+def test_exact_solution_for_simple_methods(method, kwargs):
+    # this tests that simple methods correctly determine the steadystate
+    # with high accuracy for a small Liouvillian requiring correct weighting.
+    H = qutip.identity(2)
+    c_ops = [qutip.sigmam(), 1e-8 * qutip.sigmap()]
+    rho_ss = qutip.steadystate(H, c_ops, method=method, **kwargs)
+    expected_rho_ss = np.array([
+        [1.e-16+0.j, 0.e+00-0.j],
+        [0.e+00-0.j, 1.e+00+0.j],
+    ])
+    np.testing.assert_allclose(expected_rho_ss, rho_ss, atol=1e-16)
+    assert rho_ss.tr() == pytest.approx(1, abs=1e-14)
+
+
 @pytest.mark.parametrize(['method', 'kwargs'], [
     pytest.param('direct', {}, id="direct"),
     pytest.param('direct', {'sparse':False}, id="direct_dense"),
@@ -126,6 +152,7 @@ def test_driven_cavity(method, kwargs):
     rho_ss_analytic = qutip.coherent_dm(N, -1.0j * (Omega)/(Gamma/2))
 
     np.testing.assert_allclose(rho_ss, rho_ss_analytic, atol=1e-4)
+    assert rho_ss.tr() == pytest.approx(1, abs=1e-12)
 
 
 @pytest.mark.parametrize(['method', 'kwargs'], [
@@ -144,6 +171,7 @@ def test_pseudo_inverse(method, kwargs):
     Lpinv = qutip.pseudo_inverse(L, rho, method=method, **kwargs)
     np.testing.assert_allclose((L * Lpinv * L).full(), L.full())
     np.testing.assert_allclose((Lpinv * L * Lpinv).full(), Lpinv.full())
+    assert rho.tr() == pytest.approx(1, abs=1e-15)
 
 
 @pytest.mark.parametrize('sparse', [True, False])
@@ -185,6 +213,7 @@ def test_steadystate_floquet(sparse):
     expect_ss = qutip.expect(a_d * a, rho_ss)
 
     np.testing.assert_allclose(expect_me[-20:], expect_ss, atol=1e-3)
+    assert rho_ss.tr() == pytest.approx(1, abs=1e-15)
 
 
 def test_bad_options_steadystate():
@@ -193,11 +222,11 @@ def test_bad_options_steadystate():
     H = (a.dag() + a)
     c_ops = [a]
     with pytest.raises(ValueError):
-        rho_ss = qutip.steadystate(H, c_ops, method='not a method')
+        qutip.steadystate(H, c_ops, method='not a method')
     with pytest.raises(TypeError):
-        rho_ss = qutip.steadystate(H, c_ops, method='direct', bad_opt=True)
+        qutip.steadystate(H, c_ops, method='direct', bad_opt=True)
     with pytest.raises(ValueError):
-        rho_ss = qutip.steadystate(H, c_ops, method='direct', solver='Error')
+        qutip.steadystate(H, c_ops, method='direct', solver='Error')
 
 
 def test_bad_options_pseudo_inverse():
@@ -228,8 +257,7 @@ def test_bad_system():
     N = 4
     a = qutip.destroy(N)
     H = (a.dag() + a)
-    c_ops = [a]
-    with pytest.raises(TypeError) as err:
-        rho_ss = qutip.steadystate(H, [], method='direct')
-    with pytest.raises(TypeError) as err:
-        rho_ss = qutip.steadystate(qutip.basis(N, N-1), [], method='direct')
+    with pytest.raises(TypeError):
+        qutip.steadystate(H, [], method='direct')
+    with pytest.raises(TypeError):
+        qutip.steadystate(qutip.basis(N, N-1), [], method='direct')