Repository: qutip/qutip

Issue:
incorrect behaviour of correlation_2op_2t when using time-dependent Hamiltonian terms and collapse operators
### Bug Description

The correlation 2op_2t does not give correct results when using time-dependent hamiltonians and collapse operators.

It seems that this could be solved by changing one line of the code inside correlatation.py, in def _correlation_me_2t:

from:
H_shifted, c_op * rho * a_op, taulist, c_ops_shifted, 

to:
H_shifted, c_op * rho * a_op, taulist + tlist[t_idx], c_ops_shifted,

The accompanying code demonstrating the issue and help finding the issue and solution were provided by M. Pechal

Issue was found in qutip version 4.6.2 and 4.6.3

### Code to Reproduce the Bug

```shell
# Qubit evolving only under energy relaxation (with H = 0 and the only collapse operator being qt.destroy(2)).
# The relaxation rate is time-dependent.
#
# A simple calculation shows that <ad(t2).a(t1)> = exp( - ( int_0^t1 Gamma(t) dt + int_0^t2 Gamma(t) dt ) / 2 )

H = 0.0 * qt.identity(2)
psi0 = qt.basis(2, 1)
tlist = np.linspace(0, 1, 101)
def Gamma(t):
    return 8.0 * t
cops = [[qt.destroy(2), lambda t, args: np.sqrt(Gamma(t))]]
a = qt.destroy(2)
ad = a.dag()

testcorr = qt.correlation_2op_2t(H, psi0, tlist, tlist, cops, ad, a)
#testcorrM = qt.correlation_2op_2t_MPAF(H, psi0, tlist, tlist, cops, ad, a)

i1 = 20
i2 = 50
print('qt.correlation_2op_2t:')
print('<ad(t2).a(t1)> =',testcorr[i1, i2 - i1])

#print('qt.correlation_2op_2t MPAF:')
#print('<ad(t2).a(t1)> =',testcorrM[i1, i2 - i1])


print('------')
print('"manual" calculation using mesolve:')
rhom = qt.mesolve(H, psi0, np.linspace(0, tlist[i1], 101), cops).states[-1]
print('<ad(t2).a(t1)> =',(qt.mesolve(H, a*rhom, np.linspace(tlist[i1], tlist[i2], 101), cops).states[-1] * ad).tr())

print('------')
print('theory:')
t1 = tlist[i1]
t2 = tlist[i2]
print('<ad(t2).a(t1)> =', np.exp(-(4.0*t1**2 + 4.0*t2**2)/2))
```


### Code Output

```shell
qt.correlation_2op_2t:
<ad(t2).a(t1)> = (0.7117768902828536+0j)
------
"manual" calculation using mesolve:
<ad(t2).a(t1)> = 0.5598976820725802
------
theory:
<ad(t2).a(t1)> = 0.559898366565402
```


### Expected Behaviour

qt.correlation_2op_2t should reproduce numbers from theory and 'manual' calculations

### Your Environment

```shell
QuTiP Version:      4.6.3
Numpy Version:      1.20.3
Scipy Version:      1.7.1
Cython Version:     0.29.24
Matplotlib Version: 3.4.2
Python Version:     3.8.10
Number of CPUs:     8
BLAS Info:          OPENBLAS
OPENMP Installed:   False
INTEL MKL Ext:      False
Platform Info:      Linux (x86_64)
```


### Additional Context

_No response_


Discussion:
Hello @Ericgig. I'm participating in the unitary hack. This looks like a good issue to start in qutip. Is necessary to add tests for `correalation.py ` once the function ` correlation_me_2t` has been changed?   

Thanks!
Hi @victor-onofre, I don't think that this issue has a bounty for the unitary hack, but we would be grateful if you want to tackle it.
Yes, if you can add a test that would have failed but is now passing, it would be great. Also, I believe the same bug can be found in `_correlation_me_2t`, which should also be fixed. 
 
Hi @victor-onofre @Ericgig 

As far as I understand it, the only change needed is a single line in correlation_me_2t, as shown above. A test to show what the function should do, and how it currently does not work correctly, is shown in the code above.

I've not attempted to 'solve' this issue further myself for the following 2 reasons:

1) I'm not a theorist. I've discussed this with colleagues (as also mentioned above) but I'd like to have someone check this proposed change makes sense, preferably someone who knows why the code currently is what it is, maybe there is a reason for it?

2) I'm unfamiliar with how github works, I've only used it for version control on private projects, so I actually don't know what to do to 'merge' the proposed changes into the code.

On the other hand, I have been using qutip with the change suggested above, and as far as I can tell, for my use case, it works well. 
This is definitely a bug.  Looking at correlation.py it looks like _transform_L_t_shift_new() and _args["_t0"] = tlist[t_idx]   are supposed to take care of this, but it seems broken for time-dependent collapse operators for two reasons.

1) _args["_t0"] = tlist[t_idx]  is only set if H is time-dependent, not if the c_ops are

2) The parsing of the c_ops list in _transform_shift_one_op() is  a bit broken (it seems to expect [[c,func]] to be one list deeper or something like that?)

I am wondering if the above proposed fix of just shifting tlist in the mesolve() call might clash with (potentially) working code for time-dependent Hamiltonians and introduce a new bug in that case? It could  perhaps better to fix the existing functionality by fixing the two issues above.  On the other hand, that existing functionality is fairly clunky..... any reason not just to ditch it all and have the shifted tlist in mesolve for all cases?
Hi @Ericgig, @AFvanloo, and @nwlambert, this bug has become more interested. Thanks for all the comments and suggestions. I will be busy this week, but I will dive down into the functions and propose a good solution during the weekend. It looks that the solution @nwlambert  is proposing is the best one but I want to check if there is something else to do here. 

Thanks!
I did the changes recommended by @AFvanloo. Given that this is my first time contributing to qutip, I didn't want to make so many changes for me to get used to the workflow. I think I can implement the comments of @nwlambert later but for now, these changes solve the issue.

Thanks!

Resolving patch:
diff --git a/qutip/correlation.py b/qutip/correlation.py
--- a/qutip/correlation.py
+++ b/qutip/correlation.py
@@ -1069,18 +1069,15 @@ def _correlation_me_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op,
     rho_t = mesolve(H, rho0, tlist, c_ops, [],
                     args=args, options=options).states
     corr_mat = np.zeros([np.size(tlist), np.size(taulist)], dtype=complex)
-    H_shifted, c_ops_shifted, _args = _transform_L_t_shift_new(H, c_ops, args)
+    taulist = np.asarray(taulist)
     if config.tdname:
         _cython_build_cleanup(config.tdname)
     rhs_clear()
 
     for t_idx, rho in enumerate(rho_t):
-        if not isinstance(H, Qobj):
-            _args["_t0"] = tlist[t_idx]
-
         corr_mat[t_idx, :] = mesolve(
-            H_shifted, c_op * rho * a_op, taulist, c_ops_shifted,
-            [b_op], args=_args, options=options
+            H, c_op * rho * a_op, taulist + tlist[t_idx], c_ops,
+            [b_op], args=args, options=options
         ).expect[0]
 
         if t_idx == 1:
@@ -1205,7 +1202,8 @@ def _correlation_mc_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op,
     ).states
 
     corr_mat = np.zeros([np.size(tlist), np.size(taulist)], dtype=complex)
-    H_shifted, c_ops_shifted, _args = _transform_L_t_shift_new(H, c_ops, args)
+    tlist = np.asarray(tlist)
+    taulist = np.asarray(taulist)
     if config.tdname:
         _cython_build_cleanup(config.tdname)
     rhs_clear()
@@ -1213,9 +1211,6 @@ def _correlation_mc_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op,
     # calculation of <A(t)B(t+tau)C(t)> from only knowledge of psi0 requires
     # averaging over both t and tau
     for t_idx in range(np.size(tlist)):
-        if not isinstance(H, Qobj):
-            _args["_t0"] = tlist[t_idx]
-
         for trial_idx in range(options.ntraj[0]):
             if isinstance(a_op, Qobj) and isinstance(c_op, Qobj):
                 if a_op.dag() == c_op:
@@ -1225,9 +1220,9 @@ def _correlation_mc_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op,
 
                     # evolve these states and calculate expectation value of B
                     c_tau = chi_0.norm()**2 * mcsolve(
-                        H_shifted, chi_0/chi_0.norm(), taulist, c_ops_shifted,
+                        H, chi_0/chi_0.norm(), taulist + tlist[t_idx], c_ops,
                         [b_op],
-                        args=_args, ntraj=options.ntraj[1], options=options,
+                        args=args, ntraj=options.ntraj[1], options=options,
                         progress_bar=None
                     ).expect[0]
 
@@ -1252,9 +1247,9 @@ def _correlation_mc_2t(H, state0, tlist, taulist, c_ops, a_op, b_op, c_op,
                 # evolve these states and calculate expectation value of B
                 c_tau = [
                     chi.norm()**2 * mcsolve(
-                        H_shifted, chi/chi.norm(), taulist, c_ops_shifted,
+                        H, chi/chi.norm(), taulist + tlist[t_idx], c_ops,
                         [b_op],
-                        args=_args, ntraj=options.ntraj[1], options=options,
+                        args=args, ntraj=options.ntraj[1], options=options,
                         progress_bar=None
                     ).expect[0]
                     for chi in chi_0
@@ -1316,55 +1311,3 @@ def _spectrum_pi(H, wlist, c_ops, a_op, b_op, use_pinv=False):
         spectrum[idx] = -2 * np.real(s[0, 0])
 
     return spectrum
-
-
-# auxiliary
-def _transform_shift_one_coeff(op, args):
-    if isinstance(op, types.FunctionType):
-        # function-list based time-dependence
-        if isinstance(args, dict):
-            def fn(t, args_i):
-                return op(t + args_i["_t0"], args_i)
-            fn = lambda t, args_i: \
-                op(t + args_i["_t0"], args_i)
-        else:
-            def fn(t, args_i):
-                return op(t + args_i["_t0"], args_i["_user_args"])
-    else:
-        fn = sub("(?<=[^0-9a-zA-Z_])t(?=[^0-9a-zA-Z_])",
-                 "(t+_t0)", " " + op + " ")
-    return fn
-
-
-def _transform_shift_one_op(op, args={}):
-    if isinstance(op, Qobj):
-        new_op = op
-    elif isinstance(op, QobjEvo):
-        new_op = op
-        new_op._shift
-    elif callable(op):
-        def new_op(t, args_i):
-            return op(t + args_i["_t0"], args_i)
-    elif isinstance(op, list):
-        new_op = []
-        for block in op:
-            if isinstance(block, list):
-                new_op.append([block[0],
-                               _transform_shift_one_coeff(block[1], args)])
-            else:
-                new_op.append(block)
-    return new_op
-
-
-def _transform_L_t_shift_new(H, c_ops, args={}):
-    H_shifted = _transform_shift_one_op(H, args)
-    c_ops_shifted = [_transform_shift_one_op(op, args) for op in c_ops]
-    if args is None:
-        _args = {"_t0": 0}
-    elif isinstance(args, dict):
-        _args = args.copy()
-        _args["_t0"] = 0
-    else:
-        _args = {"_user_args": args, "_t0": 0}
-
-    return H_shifted, c_ops_shifted, _args


Test patch:
diff --git a/qutip/tests/test_correlation.py b/qutip/tests/test_correlation.py
--- a/qutip/tests/test_correlation.py
+++ b/qutip/tests/test_correlation.py
@@ -153,6 +153,10 @@ def dependence_2ls(request):
     return request.param
 
 
+def coeff(t, _):
+    return np.sqrt(8*t)
+
+
 class TestTimeDependence:
     """
     Test correlations with time-dependent operators using a two-level system
@@ -203,6 +207,24 @@ def test_coefficient_c_ops_3ls(self, dependence_3ls):
         g2_ab_0 = _trapz_2d(np.real(correlation_ab), times)
         assert abs(g2_ab_0 - 0.185) < 1e-2
 
+    @pytest.mark.parametrize("solver", ["me", "mc"])
+    def test_correlation_c_ops_td(self, solver):
+        t1 = 0.2
+        t2 = 0.5
+        tlist = [0, 0.2, 0.5]
+        H = qutip.qzero(2)
+        psi0 = qutip.basis(2, 1)
+        cops = [[qutip.destroy(2), coeff]]
+        a = qutip.destroy(2)
+        ad = a.dag()
+        options = qutip.Options(nsteps=1e5)
+        result = qutip.correlation_2op_2t(
+            H, psi0, [0, t1], [0, t2-t1], cops, ad, a, solver=solver
+        )[1, 1].real
+        expected = np.exp(-(4.0 * t1 ** 2 + 4.0 * t2 ** 2) / 2)
+        atol = 2e-6 if solver == "me" else 0.2 # 4 sigma
+        np.testing.assert_allclose(result, expected, atol=atol)
+
 
 def _step(t):
     return np.arctan(t)/np.pi + 0.5