Repository: qutip/qutip

Issue:
unit test failures with 4.7.5 and scipy 1.12 on aarch64 linux
### Bug Description

unit test test_dqd_current fails on aarch64 linux

### Code to Reproduce the Bug

```shell
the unit test
```


### Code Output

```shell
=================================== FAILURES ===================================
_______________________________ test_dqd_current _______________________________
    def test_dqd_current():
        "Counting statistics: current and current noise in a DQD model"
    
        G = 0
        L = 1
        R = 2
    
        sz = qutip.projection(3, L, L) - qutip.projection(3, R, R)
        sx = qutip.projection(3, L, R) + qutip.projection(3, R, L)
        sR = qutip.projection(3, G, R)
        sL = qutip.projection(3, G, L)
    
        w0 = 1
        tc = 0.6 * w0
        GammaR = 0.0075 * w0
        GammaL = 0.0075 * w0
        nth = 0.00
        eps_vec = np.linspace(-1.5*w0, 1.5*w0, 20)
    
        J_ops = [GammaR * qutip.sprepost(sR, sR.dag())]
    
        c_ops = [np.sqrt(GammaR * (1 + nth)) * sR,
                 np.sqrt(GammaR * (nth)) * sR.dag(),
                 np.sqrt(GammaL * (nth)) * sL,
                 np.sqrt(GammaL * (1 + nth)) * sL.dag()]
    
        current = np.zeros(len(eps_vec))
        noise = np.zeros(len(eps_vec))
    
        for n, eps in enumerate(eps_vec):
            H = (eps/2 * sz + tc * sx)
            L = qutip.liouvillian(H, c_ops)
            rhoss = qutip.steadystate(L)
            c_, n_ = qutip.countstat_current_noise(L, [], rhoss=rhoss, J_ops=J_ops)
            current[n] = c_[0]
            noise[n] = n_[0, 0, 0]
    
            current2 = qutip.countstat_current(L, rhoss=rhoss, J_ops=J_ops)
            assert abs(current[n] - current2) < 1e-8
    
            current2 = qutip.countstat_current(L, c_ops, J_ops=J_ops)
            assert abs(current[n] - current2) < 1e-8
    
        current_target = (tc**2 * GammaR
                          / (tc**2 * (2+GammaR/GammaL) + GammaR**2/4 + eps_vec**2))
        noise_target = current_target * (
            1 - (8*GammaL*tc**2*(4 * eps_vec**2 * (GammaR - GammaL)
                                 + GammaR*(3*GammaL*GammaR + GammaR**2 + 8*tc**2))
                 / (4*tc**2*(2*GammaL + GammaR) + GammaL*GammaR**2
                    + 4*eps_vec**2*GammaL)**2)
        )
    
        np.testing.assert_allclose(current, current_target, atol=1e-4)
>       np.testing.assert_allclose(noise, noise_target, atol=1e-4)
/nix/store/7v2yn16c9sr8ysdpan9yd2a60a1sizv7-python3.11-qutip-4.7.5/lib/python3.11/site-packages/qutip/tests/test_countstat.py:58: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
args = (<function assert_allclose.<locals>.compare at 0xfffeb718f880>, array([0.0007729 , 0.00087857, 0.00099616, 0.00112145,...9, 0.00142197, 0.00144314, 0.00142247, 0.00135199,
       0.0012452 , 0.00112145, 0.00099616, 0.00087857, 0.0007729 ]))
kwds = {'equal_nan': True, 'err_msg': '', 'header': 'Not equal to tolerance rtol=1e-07, atol=0.0001', 'verbose': True}
    @wraps(func)
    def inner(*args, **kwds):
        with self._recreate_cm():
>           return func(*args, **kwds)
E           AssertionError: 
E           Not equal to tolerance rtol=1e-07, atol=0.0001
E           
E           Mismatched elements: 1 / 20 (5%)
E           Max absolute difference: 0.00035163
E           Max relative difference: 0.26008554
E            x: array([0.000773, 0.000879, 0.000996, 0.001121, 0.001245, 0.001352,
E                  0.001422, 0.001443, 0.001422, 0.001393, 0.001393, 0.001422,
E                  0.001443, 0.001422, 0.001704, 0.001245, 0.001121, 0.000996,
E                  0.000879, 0.000773])
E            y: array([0.000773, 0.000879, 0.000996, 0.001121, 0.001245, 0.001352,
E                  0.001422, 0.001443, 0.001422, 0.001393, 0.001393, 0.001422,
E                  0.001443, 0.001422, 0.001352, 0.001245, 0.001121, 0.000996,
E                  0.000879, 0.000773])
/nix/store/dz8lm4h0ivibad5kfc0ya3p3zqyd2fyf-python3-3.11.7/lib/python3.11/contextlib.py:81: AssertionError
=========================== short test summary info ============================
FAILED ../tests/test_countstat.py::test_dqd_current - AssertionError:
```


### Expected Behaviour

unit test succeeds

### Your Environment

```shell
QuTiP Version:      4.7.5
Numpy Version:      1.26.2
Scipy Version:      1.12.0
Cython Version:     0.29.36
Matplotlib Version: 3.8.2
Python Version:     3.11.7
Number of CPUs:     80
BLAS Info:          Generic
OPENMP Installed:   True
INTEL MKL Ext:      False
Platform Info:      Linux (aarch64)
```


### Additional Context

_No response_


Discussion:
Thank you for reporting.

However I am unable to reproduce it (on ubuntu, same scipy, numpy, cython, python3.11.5).
So I can't tell what went wrong or how to solve it...

on aarch64 linux? same test passes on x64 linux and x64 / aarch64 darwin.
On x64, I had arch Linux in mind...

I don't have a setup to test on aarch64, nor ever tried to emulate it. 
It may take some time.


Resolving patch:
diff --git a/qutip/countstat.py b/qutip/countstat.py
--- a/qutip/countstat.py
+++ b/qutip/countstat.py
@@ -70,7 +70,7 @@ def countstat_current(L, c_ops=None, rhoss=None, J_ops=None):
     return I
 
 
-def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None, 
+def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None,
                             sparse=True, method='direct'):
     """
     Compute the cross-current noise spectrum for a list of collapse operators
@@ -79,21 +79,21 @@ def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None,
     of the dissipative processes in `L`, but the `c_ops` given here does not
     necessarily need to be all collapse operators contributing to dissipation
     in the Liouvillian. Optionally, the steadystate density matrix `rhoss`
-    and the current operators `J_ops` correpsonding to the current collapse 
+    and the current operators `J_ops` correpsonding to the current collapse
     operators `c_ops` can also be specified. If either of
     `rhoss` and `J_ops` are omitted, they will be computed internally.
-    'wlist' is an optional list of frequencies at which to evaluate the noise 
-    spectrum.  
-    
+    'wlist' is an optional list of frequencies at which to evaluate the noise
+    spectrum.
+
     Note:
-    The default method is a direct solution using dense matrices, as sparse 
+    The default method is a direct solution using dense matrices, as sparse
     matrix methods fail for some examples of small systems.
     For larger systems it is reccomended to use the sparse solver
     with the direct method, as it avoids explicit calculation of the
     pseudo-inverse, as described in page 67 of "Electrons in nanostructures"
     C. Flindt, PhD Thesis, available online:
     https://orbit.dtu.dk/fedora/objects/orbit:82314/datastreams/file_4732600/content
-    
+
     Parameters
     ----------
 
@@ -106,9 +106,9 @@ def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None,
     rhoss : :class:`qutip.Qobj` (optional)
         The steadystate density matrix corresponding the system Liouvillian
         `L`.
-        
+
     wlist : array / list (optional)
-        List of frequencies at which to evaluate (if none are given, evaluates 
+        List of frequencies at which to evaluate (if none are given, evaluates
         at zero frequency)
 
     J_ops : array / list (optional)
@@ -118,9 +118,9 @@ def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None,
         Flag that indicates whether to use sparse or dense matrix methods when
         computing the pseudo inverse. Default is false, as sparse solvers
         can fail for small systems. For larger systems the sparse solvers
-        are reccomended. 
-        
-        
+        are reccomended.
+
+
     Returns
     --------
     I, S : tuple of arrays
@@ -135,18 +135,18 @@ def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None,
     if J_ops is None:
         J_ops = [sprepost(c, c.dag()) for c in c_ops]
 
-    
+
 
     N = len(J_ops)
     I = np.zeros(N)
-    
+
     if wlist is None:
         S = np.zeros((N, N,1))
         wlist=[0.]
     else:
         S = np.zeros((N, N,len(wlist)))
-        
-    if sparse == False: 
+
+    if sparse == False:
         rhoss_vec = mat2vec(rhoss.full()).ravel()
         for k,w in enumerate(wlist):
             R = pseudo_inverse(L, rhoss=rhoss, w= w, sparse = sparse, method=method)
@@ -155,44 +155,46 @@ def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None,
                     if i == j:
                         I[i] = expect_rho_vec(Ji.data, rhoss_vec, 1)
                         S[i, j,k] = I[i]
-                    S[i, j,k] -= expect_rho_vec((Ji * R * Jj 
+                    S[i, j,k] -= expect_rho_vec((Ji * R * Jj
                                                 + Jj * R * Ji).data,
                                                 rhoss_vec, 1)
     else:
         if method == "direct":
             N = np.prod(L.dims[0][0])
-            
+
             rhoss_vec = operator_to_vector(rhoss)
-            
+
             tr_op = tensor([identity(n) for n in L.dims[0][0]])
             tr_op_vec = operator_to_vector(tr_op)
-            
+
             Pop = sp.kron(rhoss_vec.data, tr_op_vec.data.T, format='csr')
             Iop = sp.eye(N*N, N*N, format='csr')
             Q = Iop - Pop
-            
+
             for k,w in enumerate(wlist):
-                
-                if w != 0.0:    
+
+                if w != 0.0:
                     L_temp = 1.0j*w*spre(tr_op) + L
-                else: #At zero frequency some solvers fail for small systems.
-                      #Adding a small finite frequency of order 1e-15
-                      #helps prevent the solvers from throwing an exception.
-                    L_temp =  1.0j*(1e-15)*spre(tr_op) + L
-                    
+                else:
+                    # At zero frequency some solvers fail for small systems.
+                    # Adding a small finite frequency of order 1e-12
+                    # helps prevent the solvers from returning wrong results.
+                    # Anything under 1e-12 is removed by the auto_tidyup.
+                    L_temp =  1.0j*(1e-12)*spre(tr_op) + L
+
                 if not settings.has_mkl:
                     A = L_temp.data.tocsc()
                 else:
                     A = L_temp.data.tocsr()
-                    A.sort_indices()                      
-                      
-                rhoss_vec = mat2vec(rhoss.full()).ravel()               
-                
+                    A.sort_indices()
+
+                rhoss_vec = mat2vec(rhoss.full()).ravel()
+
                 for j, Jj in enumerate(J_ops):
                     Qj = Q.dot( Jj.data.dot( rhoss_vec))
                     try:
                         if settings.has_mkl:
-                            X_rho_vec_j = mkl_spsolve(A,Qj)                            
+                            X_rho_vec_j = mkl_spsolve(A,Qj)
                         else:
                             X_rho_vec_j = sp.linalg.splu(A, permc_spec
                                                  ='COLAMD').solve(Qj)
@@ -201,36 +203,36 @@ def countstat_current_noise(L, c_ops, wlist=None, rhoss=None, J_ops=None,
                     for i, Ji in enumerate(J_ops):
                         Qi = Q.dot( Ji.data.dot(rhoss_vec))
                         try:
-                            if settings.has_mkl:                              
-                                X_rho_vec_i = mkl_spsolve(A,Qi)  
+                            if settings.has_mkl:
+                                X_rho_vec_i = mkl_spsolve(A,Qi)
                             else:
                                 X_rho_vec_i = sp.linalg.splu(A, permc_spec
                                                      ='COLAMD').solve(Qi)
                         except:
                              X_rho_vec_i = sp.linalg.lsqr(A,Qi)[0]
                         if i == j:
-                            I[i] = expect_rho_vec(Ji.data, 
+                            I[i] = expect_rho_vec(Ji.data,
                                                  rhoss_vec, 1)
                             S[j, i, k] = I[i]
-                        
-                        S[j, i, k] -= (expect_rho_vec(Jj.data * Q, 
-                                        X_rho_vec_i, 1) 
-                                        + expect_rho_vec(Ji.data * Q, 
+
+                        S[j, i, k] -= (expect_rho_vec(Jj.data * Q,
+                                        X_rho_vec_i, 1)
+                                        + expect_rho_vec(Ji.data * Q,
                                         X_rho_vec_j, 1))
 
         else:
             rhoss_vec = mat2vec(rhoss.full()).ravel()
             for k,w in enumerate(wlist):
 
-                R = pseudo_inverse(L,rhoss=rhoss, w= w, sparse = sparse, 
+                R = pseudo_inverse(L,rhoss=rhoss, w= w, sparse = sparse,
                                    method=method)
-                                   
+
                 for i, Ji in enumerate(J_ops):
                     for j, Jj in enumerate(J_ops):
                         if i == j:
                             I[i] = expect_rho_vec(Ji.data, rhoss_vec, 1)
                             S[i, j, k] = I[i]
-                        S[i, j, k] -= expect_rho_vec((Ji * R * Jj 
+                        S[i, j, k] -= expect_rho_vec((Ji * R * Jj
                                                      + Jj * R * Ji).data,
                                                      rhoss_vec, 1)
     return I, S


Test patch: