Repository: qutip/qutip

Issue:
ENR basis not working with steady state solver (normal and floquet based) in 5.0.0
### Bug Description

Using a Hamiltonian that is constructed using operators returned from the `enr_destroy` function, does not work with the steady state solver in Quito 5.0.0. It does however work with Qutip 4.7 (have not checked any other versions). 

The error given is a dimension mismatch essentially, which seems to stem from an "expected dimension" of the Hamiltonian based on the number of modes. In the example below, the dimension mismatch is between 65536 (which is 2^16, the entire Hilbert space of my problem) vs 17, which is the excitation number restricted subspace. 

### Code to Reproduce the Bug

```shell
N = 16 ## number of resonators including tapers
max_excitation = 1 ## maximum excitation
num_levels = 2
dims = [num_levels] * N

ann_operators = enr_destroy(dims, excitations = max_excitation)
w_r = 2 * np.pi * 5
w = np.array([w_r] * N)

J_r = 2 * np.pi * 0.4
J_t2 = 1.353 * J_r
J_t1 = 1.035 * J_r

J = np.array([J_t2, J_t1] + [J_r] * (N-5) + [J_t1, J_t2])

kappa = 2.92 * J_r
drive_amp = 2 * np.pi * 0.0001
t = []

detuning_list = 2 * np.pi * np.linspace(-1, 1, 100)

for detuning in detuning_list:
    drive_w = (w_r - detuning)

    H_r = 0
    for i, a in enumerate(ann_operators):
        H_r += (w[i] - drive_w) * a.dag() * a

    H_int = 0
    for i in range(len(ann_operators)-1):
        H_int += J[i] * (ann_operators[i].dag() * ann_operators[i+1] + ann_operators[i+1].dag() * ann_operators[i])

    H_d = -1j * drive_amp * (ann_operators[0] - ann_operators[0].dag())

    H = H_r + H_int + H_d

    c_ops = [np.sqrt(kappa)*ann_operators[0], np.sqrt(kappa)*ann_operators[-1]]

    rho_ss = steadystate(H, c_ops)

    a_out = np.sqrt(kappa) * expect(ann_operators[-1], rho_ss)
    t.append(np.sqrt(kappa) * a_out/drive_amp)

print("J = ", np.array(J)/(2 * np.pi))
print("w = ", np.array(w)/(2 * np.pi))
print("kappa = ", kappa/2/np.pi)
t = np.array(t)
plt.plot(detuning_list/2/np.pi, 20 * np.log10(np.abs(t)))
plt.xlabel("Detuning (GHz)")
plt.ylabel("|S21|(dB)")
plt.ylim(-10,0.01)
# plt.xlim(-2, 2)
plt.grid()
```


### Code Output

```shell
{
	"name": "ValueError",
	"message": "Provided dimensions do not match the data: (65536, 65536) vs (17, 17)",
	"stack": "---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Cell In[2], line 39
     35 H = H_r + H_int + H_d
     37 c_ops = [np.sqrt(kappa)*ann_operators[0], np.sqrt(kappa)*ann_operators[-1]]
---> 39 rho_ss = steadystate(H, c_ops)
     41 a_out = np.sqrt(kappa) * expect(ann_operators[-1], rho_ss)
     42 t.append(np.sqrt(kappa) * a_out/drive_amp)

File /opt/anaconda3/envs/trial_env/lib/python3.11/site-packages/qutip/solver/steadystate.py:177, in steadystate(A, c_ops, method, solver, **kwargs)
    175     kwargs.pop(\"power_eps\", 0)
    176     kwargs.pop(\"sparse\", 0)
--> 177     return _steadystate_direct(A, kwargs.pop(\"weight\", 0),
    178                                method=solver, **kwargs)
    180 elif method == \"power\":
    181     # Remove unused kwargs, so only used and pass-through ones are included
    182     kwargs.pop(\"weight\", 0)

File /opt/anaconda3/envs/trial_env/lib/python3.11/site-packages/qutip/solver/steadystate.py:246, in _steadystate_direct(A, weight, **kw)
    243 rho_ss = _data.column_unstack(steadystate, n)
    244 rho_ss = _data.add(rho_ss, rho_ss.adjoint()) * 0.5
--> 246 return Qobj(rho_ss, dims=A.dims[0], isherm=True)

File /opt/anaconda3/envs/trial_env/lib/python3.11/site-packages/qutip/core/qobj.py:307, in Qobj.__init__(self, arg, dims, copy, superrep, isherm, isunitary)
    305 self._isherm = isherm
    306 self._isunitary = isunitary
--> 307 self._initialize_data(arg, dims, copy)
    309 if superrep is not None:
    310     self.superrep = superrep

File /opt/anaconda3/envs/trial_env/lib/python3.11/site-packages/qutip/core/qobj.py:300, in Qobj._initialize_data(self, arg, dims, copy)
    296     self._dims = Dimensions(
    297         dims or [[self._data.shape[0]], [self._data.shape[1]]]
    298     )
    299 if self._dims.shape != self._data.shape:
--> 300     raise ValueError('Provided dimensions do not match the data: ' +
    301                      f\"{self._dims.shape} vs {self._data.shape}\")

ValueError: Provided dimensions do not match the data: (65536, 65536) vs (17, 17)"
}
```


### Expected Behaviour

In Qutip 4.7 (and python 3.9), I see the following (correct output):
<img width="541" alt="image" src="https://github.com/qutip/qutip/assets/61804899/4480c250-b1c6-446e-936d-5ee48f91db8d">


### Your Environment

```shell
QuTiP Version:      5.0.0
Numpy Version:      1.26.4
Scipy Version:      1.12.0
Cython Version:     None
Matplotlib Version: 3.8.4
Python Version:     3.11.8
Number of CPUs:     8
BLAS Info:          Generic
INTEL MKL Ext:      False
Platform Info:      Darwin (x86_64)
```


### Additional Context

_No response_


Discussion:
Hi @nwlambert ,
Thank you for looking into this! The code does run now, but I'm seeing results pretty different when compared to Qutip 4.7 when using steady state floquet solver.
Let me know if I should open up a new issue (since this is closed).
Thanks @parsidd, could you share your floquet example here and I will take a look?
Sorry for the delayed reply, here is what I've been trying to do:

N = 8
max_excitation = 1 
num_levels = 2
dims = [num_levels] * N`

ann_operators = enr_destroy(dims, excitations = max_excitation)
w_r = 2 * np.pi * 5
w = np.array([w_r] * N)

J_r = 2 * np.pi * 0.4
J_t2 = 1.353 * J_r
J_t1 = 1.035 * J_r

J = np.array([J_t2, J_t1] + [J_r] * (N-5) + [J_t1, J_t2])

kappa = 2.92 * J_r
drive_amp = 2 * np.pi * 0.0001

mod_freq = 2 * np.pi * 0.87
mod_amp = mod_freq * 0.8

t = []

detuning_list = 2 * np.pi * np.linspace(-1, 1, 501)

for detuning in detuning_list:
    drive_w = (w_r - detuning)

    H_r = 0
    for i, a in enumerate(ann_operators):
        H_r += (w[i] - drive_w) * a.dag() * a

    H_int = 0
    for i in range(len(ann_operators)-1):
        H_int += J[i] * (ann_operators[i].dag() * ann_operators[i+1] + ann_operators[i+1].dag() * ann_operators[i])

    H_d = -1j * drive_amp * (ann_operators[0] - ann_operators[0].dag())

    H = H_r + H_int + H_d
    c_ops = [np.sqrt(kappa)*ann_operators[0], np.sqrt(kappa)*ann_operators[-1]]
    Op_t = mod_amp * ann_operators[6].dag() * ann_operators[6]

    rho_ss = steadystate_floquet(H, c_ops, Op_t, w_d = mod_freq)

    a_out = np.sqrt(kappa) * expect(ann_operators[-1], rho_ss)
    t.append(np.sqrt(kappa) * a_out/drive_amp)

t = np.array(t)

dB = 0

if dB:
    plt.plot(detuning_list/2/np.pi, 20*np.log10(np.abs(t)))
    plt.xlabel("Detuning (GHz)")
    plt.ylabel("|S21|(dB)")
    plt.ylim(-10,0.01)

else:
    plt.plot(detuning_list/2/np.pi, (np.abs(t)))
    plt.xlabel("Detuning (GHz)")
    plt.ylabel("|S21|")
plt.grid()`


Qutip 4.7:
![image](https://github.com/qutip/qutip/assets/61804899/95a303ef-dcef-4fae-bace-5d6f5b7b24a1)

Qutip 5.1.0 (installed by downloading a zip from the git as on April 11th):
![image](https://github.com/qutip/qutip/assets/61804899/04d3e168-2893-435b-9818-91fe917c5d6b)

Thanks, this helps track it down.  It seems not to be an issue with ENR per se, but with the floquet steadystate solver itself (it also appears without using ENR states). 

I think I pinned it down to two small bugs in that seem to have crept in to the solver with the data layer updates, I will make a PR for it and ping you for some feedback, if thats ok.  We lack a sufficiently robust test for this solver really, if you can suggest something tractable (very small example, potentially analytical solution we can check against), it would be very helpful.



Resolving patch:
diff --git a/qutip/solver/steadystate.py b/qutip/solver/steadystate.py
--- a/qutip/solver/steadystate.py
+++ b/qutip/solver/steadystate.py
@@ -356,9 +356,10 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False,
         - "mkl_spsolve"
           sparse solver by mkl.
 
-        Extensions to qutip, such as qutip-tensorflow, may provide their own solvers.
-        When ``H_0`` and ``c_ops`` use these data backends, see their documentation
-        for the names and details of additional solvers they may provide.
+        Extensions to qutip, such as qutip-tensorflow, may provide their own
+        solvers. When ``H_0`` and ``c_ops`` use these data backends, see their
+        documentation for the names and details of additional solvers they may
+        provide.
 
     **kwargs:
         Extra options to pass to the linear system solver. See the
@@ -373,18 +374,20 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False,
     Notes
     -----
     See: Sze Meng Tan,
-    https://copilot.caltech.edu/documents/16743/qousersguide.pdf,
-    Section (10.16)
+    https://painterlab.caltech.edu/wp-content/uploads/2019/06/qe_quantum_optics_toolbox.pdf,
+    Section (16)
 
     """
 
     L_0 = liouvillian(H_0, c_ops)
-    L_m = L_p = 0.5 * liouvillian(Op_t)
+    L_m = 0.5 * liouvillian(Op_t)
+    L_p = 0.5 * liouvillian(Op_t)
     # L_p and L_m correspond to the positive and negative
     # frequency terms respectively.
     # They are independent in the model, so we keep both names.
     Id = qeye_like(L_0)
-    S = T = qzero_like(L_0)
+    S = qzero_like(L_0)
+    T = qzero_like(L_0)
 
     if isinstance(H_0.data, _data.CSR) and not sparse:
         L_0 = L_0.to("Dense")
@@ -395,7 +398,7 @@ def steadystate_floquet(H_0, c_ops, Op_t, w_d=1.0, n_it=3, sparse=False,
     for n_i in np.arange(n_it, 0, -1):
         L = L_0 - 1j * n_i * w_d * Id + L_m @ S
         S.data = - _data.solve(L.data, L_p.data, solver, kwargs)
-        L = L_0 - 1j * n_i * w_d * Id + L_p @ T
+        L = L_0 + 1j * n_i * w_d * Id + L_p @ T
         T.data = - _data.solve(L.data, L_m.data, solver, kwargs)
 
     M_subs = L_0 + L_m @ S + L_p @ T


Test patch:
diff --git a/qutip/tests/solver/test_steadystate.py b/qutip/tests/solver/test_steadystate.py
--- a/qutip/tests/solver/test_steadystate.py
+++ b/qutip/tests/solver/test_steadystate.py
@@ -194,39 +194,40 @@ def test_steadystate_floquet(sparse):
     Test the steadystate solution for a periodically
     driven system.
     """
-    N_c = 20
-
-    a = qutip.destroy(N_c)
-    a_d = a.dag()
-    X_c = a + a_d
+    sz = qutip.sigmaz()
+    sx = qutip.sigmax()
 
     w_c = 1
-
-    A_l = 0.001
+    A_l = 0.5
     w_l = w_c
     gam = 0.01
 
-    H = w_c * a_d * a
+    H = 0.5 * w_c * sz
 
-    H_t = [H, [X_c, lambda t, args: args["A_l"] * np.cos(args["w_l"] * t)]]
+    H_t = [H, [sx, lambda t, args: args["A_l"] * np.cos(args["w_l"] * t)]]
 
-    psi0 = qutip.fock(N_c, 0)
+    psi0 = qutip.basis(2, 0)
 
     args = {"A_l": A_l, "w_l": w_l}
 
     c_ops = []
-    c_ops.append(np.sqrt(gam) * a)
-
-    t_l = np.linspace(0, 20 / gam, 2000)
+    c_ops.append(np.sqrt(gam) * qutip.destroy(2).dag())
+    T_max = 20 * 2 * np.pi / gam
+    t_l = np.linspace(0, T_max, 20000)
 
     expect_me = qutip.mesolve(H_t, psi0, t_l,
-                        c_ops, [a_d * a], args=args).expect[0]
+                              c_ops, [sz], args=args).expect[0]
 
     rho_ss = qutip.steadystate_floquet(H, c_ops,
-                                       A_l * X_c, w_l, n_it=3, sparse=sparse)
-    expect_ss = qutip.expect(a_d * a, rho_ss)
+                                       A_l * sx, w_l, n_it=3, sparse=sparse)
+    expect_ss = qutip.expect(sz, rho_ss)
+
+    dt = T_max / len(t_l)
+    one_period = int(1/(w_l/(2*np.pi)) / dt)
+
+    average_ex = sum(expect_me[-one_period:]) / float(one_period)
 
-    np.testing.assert_allclose(expect_me[-20:], expect_ss, atol=1e-3)
+    np.testing.assert_allclose(average_ex, expect_ss, atol=1e-2)
     assert rho_ss.tr() == pytest.approx(1, abs=1e-15)