Development notebook: Tests for QuTiP's stochastic Schrödinger equation solver

Copyright (C) 2011 and later, Paul D. Nation & Robert J. Johansson

In this notebook we test the qutip stochastic Schrödinger equation solver (ssesolve) with a few examples. The same examples are solved using the stochastic master equation solver in the notebook development-smesolve-tests.


In [1]:
%pylab inline


Populating the interactive namespace from numpy and matplotlib

In [2]:
from qutip import *

Photo-count detection

Theory

Stochastic Schrödinger equation for photocurrent detection, in Milburn's formulation, is

$$ d|\psi(t)\rangle = -iH|\psi(t)\rangle dt + \left(\frac{a}{\sqrt{\langle a^\dagger a \rangle_\psi(t)}} - 1\right) |\psi(t)\rangle dN(t) - \frac{1}{2}\gamma\left(\langle a^\dagger a \rangle_\psi(t) - a^\dagger a \right) |\psi(t)\rangle dt $$

and $dN(t)$ is a Poisson distributed increment with $E[dN(t)] = \gamma \langle a^\dagger a\rangle (t)$.

Formulation in QuTiP

In QuTiP we write the stochastic Schrödinger equation on the form:

$$ d|\psi(t)\rangle = -iH|\psi(t)\rangle dt + D_{1}[c] |\psi(t)\rangle dt + D_{2}[c] |\psi(t)\rangle dW $$

where $c = \sqrt{\gamma} a$ is the collapse operator including the rate of the process as a coefficient in the operator. We can identify

$$ D_{1}[c] |\psi(t)\rangle = - \frac{1}{2}\left(\langle c^\dagger c \rangle_\psi(t) - c^\dagger c \right) $$$$ D_{2}[c] |\psi(t)\rangle = \frac{c}{\sqrt{\langle c^\dagger c \rangle_\psi(t)}} - 1 $$$$dW = dN(t)$$

Reference solution: deterministic master equation


In [3]:
N = 10
w0 = 0.5 * 2 * pi
times = linspace(0, 15, 150)
dt = times[1] - times[0]
gamma = 0.25
A = 2.5
ntraj = 50
nsubsteps = 100

In [4]:
a = destroy(N)
x = a + a.dag()

In [5]:
H = w0 * a.dag() * a

In [6]:
psi0 = fock(N, 5)

In [7]:
sc_ops = [sqrt(gamma) * a]

In [8]:
e_ops = [a.dag() * a, a + a.dag(), (-1j)*(a - a.dag())]

In [9]:
result_ref = mesolve(H, psi0, times, sc_ops, e_ops)

In [10]:
plot_expectation_values(result_ref);


Solve using stochastic master equation


In [11]:
from qutip.cy.spmatfuncs import cy_expect_psi

In [12]:
# Function sigurature:
#
# def d(A, psi):
#
#     psi = wave function at the current time step
#
#     A[0] = c
#     A[1] = c + c.dag()
#     A[2] = c - c.dag()
#     A[3] = c.dag() * c
#
#     where c is a collapse operator. The combinations of c's stored in A are
#     precomputed before the time-evolution is started to avoid repeated
#     computations.

$\displaystyle D_{1}[a, |\psi\rangle] = \frac{1}{2} ( \langle \psi|a^\dagger a |\psi\rangle - a^\dagger a )|\psi\rangle $


In [13]:
def d1_psi_func(A, psi):
    return 0.5 * (-A[3] * psi + cy_expect_psi(A[3], psi, 1) * psi)

$\displaystyle D_{2}[a, |\psi\rangle] = \frac{a|\psi\rangle}{\sqrt{\langle \psi| a^\dagger a|\psi\rangle}} - |\psi\rangle $


In [14]:
def d2_psi_func(A, psi):
    
    psi_1 = A[0] * psi
    n1 = sqrt(cy_expect_psi(A[3], psi, 1))
    
    if n1 > 0.0:
        return [psi_1 / n1 - psi]
    else:
        return [- psi]

In [15]:
result = ssesolve(H, psi0, times, sc_ops=sc_ops, e_ops=e_ops, 
                  ntraj=ntraj, nsubsteps=nsubsteps, d1=d1_psi_func, d2=d2_psi_func,
                  distribution='poisson', store_measurement=True, m_ops=[[None]])


10.0%. Run time:  21.56s. Est. time left: 00:00:03:14
20.0%. Run time:  42.39s. Est. time left: 00:00:02:49
30.0%. Run time:  63.06s. Est. time left: 00:00:02:27
40.0%. Run time:  83.77s. Est. time left: 00:00:02:05
50.0%. Run time: 104.53s. Est. time left: 00:00:01:44
60.0%. Run time: 125.15s. Est. time left: 00:00:01:23
70.0%. Run time: 146.04s. Est. time left: 00:00:01:02
80.0%. Run time: 166.81s. Est. time left: 00:00:00:41
90.0%. Run time: 187.54s. Est. time left: 00:00:00:20
Total run time: 208.56s

In [16]:
plot_expectation_values([result, result_ref]);



In [17]:
fig, ax = subplots()

for m in result.measurement:
    ax.step(times, dt * m.real)


Note that we multiply the measurement records with dt to show the count events instead of the count rate.

Solve problem again, this time with a specified noise (from previous run)


In [18]:
result = ssesolve(H, psi0, times, sc_ops=sc_ops, e_ops=e_ops, 
                  ntraj=ntraj, nsubsteps=nsubsteps, m_ops=[[None]],
                  d1=d1_psi_func, d2=d2_psi_func, distribution='poisson',
                  store_measurement=True, noise=result.noise)


10.0%. Run time:  19.47s. Est. time left: 00:00:02:55
20.0%. Run time:  38.39s. Est. time left: 00:00:02:33
30.0%. Run time:  56.93s. Est. time left: 00:00:02:12
40.0%. Run time:  75.57s. Est. time left: 00:00:01:53
50.0%. Run time:  94.29s. Est. time left: 00:00:01:34
60.0%. Run time: 112.83s. Est. time left: 00:00:01:15
70.0%. Run time: 131.39s. Est. time left: 00:00:00:56
80.0%. Run time: 150.05s. Est. time left: 00:00:00:37
90.0%. Run time: 168.65s. Est. time left: 00:00:00:18
Total run time: 187.16s

In [19]:
plot_expectation_values([result, result_ref]);



In [20]:
fig, ax = subplots()

for m in result.measurement:
    ax.step(times, dt * m.real)


Using QuTiP built-in photo-current detection functions for $D_1$ and $D_2$


In [21]:
result = ssesolve(H, psi0, times, sc_ops, e_ops,
                  ntraj=ntraj, nsubsteps=nsubsteps,
                  method='photocurrent', store_measurement=True)


10.0%. Run time:  12.72s. Est. time left: 00:00:01:54
20.0%. Run time:  24.81s. Est. time left: 00:00:01:39
30.0%. Run time:  37.14s. Est. time left: 00:00:01:26
40.0%. Run time:  49.50s. Est. time left: 00:00:01:14
50.0%. Run time:  61.56s. Est. time left: 00:00:01:01
60.0%. Run time:  73.57s. Est. time left: 00:00:00:49
70.0%. Run time:  85.67s. Est. time left: 00:00:00:36
80.0%. Run time:  97.65s. Est. time left: 00:00:00:24
90.0%. Run time: 109.89s. Est. time left: 00:00:00:12
Total run time: 122.04s

In [22]:
plot_expectation_values([result, result_ref]);


Solve problem again, this time with a specified noise (from previous run)


In [23]:
result = ssesolve(H, psi0, times, sc_ops=sc_ops, e_ops=e_ops, ntraj=ntraj, nsubsteps=nsubsteps,
                  method='photocurrent', store_measurement=True, noise=result.noise)


10.0%. Run time:   9.45s. Est. time left: 00:00:01:25
20.0%. Run time:  18.88s. Est. time left: 00:00:01:15
30.0%. Run time:  28.30s. Est. time left: 00:00:01:06
40.0%. Run time:  37.79s. Est. time left: 00:00:00:56
50.0%. Run time:  47.28s. Est. time left: 00:00:00:47
60.0%. Run time:  56.75s. Est. time left: 00:00:00:37
70.0%. Run time:  66.17s. Est. time left: 00:00:00:28
80.0%. Run time:  75.57s. Est. time left: 00:00:00:18
90.0%. Run time:  85.06s. Est. time left: 00:00:00:09
Total run time:  94.51s

In [24]:
plot_expectation_values([result, result_ref]);



In [25]:
fig, ax = subplots()

for m in result.measurement:
    ax.step(times, dt * m.real)


Homodyne detection


In [26]:
ntraj = 50
nsubsteps = 200

In [27]:
H = w0 * a.dag() * a + A * (a + a.dag())

In [28]:
result_ref = mesolve(H, psi0, times, sc_ops, e_ops)

Theory

Stochastic master equation for homodyne can be written as (see Gardiner and Zoller, Quantum Noise)

$$ d|\psi(t)\rangle = -iH|\psi(t)\rangle dt + \frac{1}{2}\gamma \left( \langle a + a^\dagger \rangle_\psi a - a^\dagger a - \frac{1}{4}\langle a + a^\dagger \rangle_\psi^2 \right)|\psi(t)\rangle dt + \sqrt{\gamma}\left( a - \frac{1}{2} \langle a + a^\dagger \rangle_\psi \right)|\psi(t)\rangle dW $$

where $dW(t)$ is a normal distributed increment with $E[dW(t)] = \sqrt{dt}$.

In QuTiP format we have:

$$ d|\psi(t)\rangle = -iH|\psi(t)\rangle dt + D_{1}[c, |\psi(t)\rangle] dt + D_{2}[c, |\psi(t)\rangle] dW $$

where $c = \sqrt{\gamma} a$, so we can identify

$$ D_{1}[c, |\psi\rangle] = \frac{1}{2}\left( \langle c + c^\dagger \rangle_\psi c - c^\dagger c - \frac{1}{4}\langle c + c^\dagger \rangle_\psi^2 \right) |\psi(t)\rangle $$

In [29]:
def d1_psi_func(A, psi):
    e_x = cy_expect_psi(A[1], psi, 0)
    return 0.5 * (e_x * (A[0] * psi) - (A[3] * psi) - 0.25 * e_x ** 2 * psi)
$$ D_{2}[c,|\psi\rangle] = \left(c - \frac{1}{2} \langle c + c^\dagger \rangle_\psi \right)|\psi(t)\rangle $$

In [30]:
def d2_psi_func(A, psi):
    e_x = cy_expect_psi(A[1], psi, 0)
    return [(A[0] * psi) - 0.5 * e_x * psi]

In [31]:
result = ssesolve(H, psi0, times, sc_ops, e_ops, 
                  ntraj=ntraj, nsubsteps=nsubsteps,
                  d1=d1_psi_func, d2=d2_psi_func, distribution='normal', 
                  m_ops=[[(a + a.dag())]], dW_factors=[1/sqrt(gamma)], 
                  store_measurement=True)


10.0%. Run time:  27.26s. Est. time left: 00:00:04:05
20.0%. Run time:  54.18s. Est. time left: 00:00:03:36
30.0%. Run time:  81.18s. Est. time left: 00:00:03:09
40.0%. Run time: 108.60s. Est. time left: 00:00:02:42
50.0%. Run time: 135.11s. Est. time left: 00:00:02:15
60.0%. Run time: 161.86s. Est. time left: 00:00:01:47
70.0%. Run time: 188.53s. Est. time left: 00:00:01:20
80.0%. Run time: 215.31s. Est. time left: 00:00:00:53
90.0%. Run time: 241.99s. Est. time left: 00:00:00:26
Total run time: 268.54s

In [32]:
plot_expectation_values([result, result_ref]);



In [33]:
fig, ax = subplots()

for m in result.measurement:
    ax.plot(times, m[:, 0].real, 'b', alpha=0.1)

ax.plot(times,  array(result.measurement).mean(axis=0)[:,0].real, 'r', lw=2)
    
ax.plot(times, result_ref.expect[1], 'k', lw=2)
ax.set_ylim(-15, 15);


Solve problem again, this time with a specified noise (from previous run)


In [34]:
result = ssesolve(H, psi0, times, sc_ops, e_ops, 
                  ntraj=ntraj, nsubsteps=nsubsteps,
                  d1=d1_psi_func, d2=d2_psi_func, distribution='normal', 
                  m_ops=[[(a + a.dag())]], dW_factors=[1/sqrt(gamma)], 
                  store_measurement=True, noise=result.noise)


10.0%. Run time:  27.01s. Est. time left: 00:00:04:03
20.0%. Run time:  53.42s. Est. time left: 00:00:03:33
30.0%. Run time:  79.80s. Est. time left: 00:00:03:06
40.0%. Run time: 107.15s. Est. time left: 00:00:02:40
50.0%. Run time: 133.90s. Est. time left: 00:00:02:13
60.0%. Run time: 160.82s. Est. time left: 00:00:01:47
70.0%. Run time: 188.05s. Est. time left: 00:00:01:20
80.0%. Run time: 215.04s. Est. time left: 00:00:00:53
90.0%. Run time: 241.82s. Est. time left: 00:00:00:26
Total run time: 268.86s

In [35]:
plot_expectation_values([result, result_ref]);



In [36]:
fig, ax = subplots()

for m in result.measurement:
    ax.plot(times, m[:, 0].real, 'b', alpha=0.1)
    
ax.plot(times, array(result.measurement).mean(axis=0)[:,0].real, 'r', lw=2)

ax.plot(times, result_ref.expect[1], 'k', lw=2);
ax.set_ylim(-20, 20);


Using QuTiP built-in homodyne detection functions for $D_1$ and $D_2$


In [37]:
result = ssesolve(H, psi0, times, sc_ops, e_ops, ntraj=ntraj, nsubsteps=nsubsteps,
                  method='homodyne', store_measurement=True)


10.0%. Run time:  25.36s. Est. time left: 00:00:03:48
20.0%. Run time:  49.98s. Est. time left: 00:00:03:19
30.0%. Run time:  74.69s. Est. time left: 00:00:02:54
40.0%. Run time:  99.28s. Est. time left: 00:00:02:28
50.0%. Run time: 123.84s. Est. time left: 00:00:02:03
60.0%. Run time: 149.28s. Est. time left: 00:00:01:39
70.0%. Run time: 173.64s. Est. time left: 00:00:01:14
80.0%. Run time: 198.09s. Est. time left: 00:00:00:49
90.0%. Run time: 222.51s. Est. time left: 00:00:00:24
Total run time: 246.81s

In [38]:
plot_expectation_values([result, result_ref]);



In [39]:
fig, ax = subplots()

for m in result.measurement:
    ax.plot(times, m[:, 0].real / sqrt(gamma), 'b', alpha=0.1)
    
ax.plot(times, array(result.measurement).mean(axis=0)[:,0].real / sqrt(gamma), 'r', lw=2)

ax.plot(times, result_ref.expect[1], 'k', lw=2)
ax.set_ylim(-20, 20);


Solve problem again, this time with a specified noise (from previous run)


In [40]:
result = ssesolve(H, psi0, times, sc_ops, e_ops, ntraj=ntraj, nsubsteps=nsubsteps,
                  method='homodyne', store_measurement=True, noise=result.noise)


10.0%. Run time:  25.10s. Est. time left: 00:00:03:45
20.0%. Run time:  51.45s. Est. time left: 00:00:03:25
30.0%. Run time:  77.15s. Est. time left: 00:00:03:00
40.0%. Run time: 103.16s. Est. time left: 00:00:02:34
50.0%. Run time: 128.13s. Est. time left: 00:00:02:08
60.0%. Run time: 153.66s. Est. time left: 00:00:01:42
70.0%. Run time: 178.69s. Est. time left: 00:00:01:16
80.0%. Run time: 204.35s. Est. time left: 00:00:00:51
90.0%. Run time: 230.20s. Est. time left: 00:00:00:25
Total run time: 255.97s

In [41]:
plot_expectation_values([result, result_ref]);



In [42]:
fig, ax = subplots()

for m in result.measurement:
    ax.plot(times, m[:, 0].real / sqrt(gamma), 'b', alpha=0.1)
    
ax.plot(times, array(result.measurement).mean(axis=0)[:,0].real / sqrt(gamma), 'r', lw=2)

ax.plot(times, result_ref.expect[1], 'k', lw=2)
ax.set_ylim(-20, 20);


Heterodyne detection

Stochastic Schrödinger equation for heterodyne detection can be written as

$$ d|\psi(t)\rangle = -iH|\psi(t)\rangle dt -\frac{1}{2}\gamma\left(a^\dagger a - \langle a^\dagger \rangle a + \frac{1}{2}\langle a \rangle\langle a^\dagger \rangle)\right)|\psi(t)\rangle dt + \sqrt{\gamma/2}\left( a - \frac{1}{2} \langle a + a^\dagger \rangle_\psi \right)|\psi(t)\rangle dW_1 -i \sqrt{\gamma/2}\left( a - \frac{1}{2} \langle a - a^\dagger \rangle_\psi \right)|\psi(t)\rangle dW_2 $$

where $dW_i(t)$ is a normal distributed increment with $E[dW_i(t)] = \sqrt{dt}$.

In QuTiP format we have:

$$ d|\psi(t)\rangle = -iH|\psi(t)\rangle dt + D_{1}[c, |\psi(t)\rangle] dt + \sum_n D_{2}^{(n)}[c, |\psi(t)\rangle] dW_n $$

where $c = \sqrt{\gamma} a$, so we can identify

$$ D_1[c, |\psi(t)\rangle] = -\frac{1}{2}\left(c^\dagger c - \langle c^\dagger \rangle c + \frac{1}{2}\langle c \rangle\langle c^\dagger \rangle \right) |\psi(t)\rangle $$

In [43]:
def d1_psi_func(A, psi):
    e_c = cy_expect_psi(A[0], psi, 0)
    e_cd = cy_expect_psi(A[0].T.conj(), psi, 0)
    return (-0.5 * (A[3] * psi) + 0.5 * e_cd * (A[0] * psi) - 0.25 * e_c * e_cd * psi)
$$D_{2}^{(1)}[c, |\psi(t)\rangle] = \sqrt{1/2} (c - \langle c + c^\dagger \rangle / 2) \psi$$$$D_{2}^{(1)}[c, |\psi(t)\rangle] = -i\sqrt{1/2} (c - \langle c - c^\dagger \rangle /2) \psi$$

In [44]:
def d2_psi_func(A, psi):
    
    x = 0.5 * cy_expect_psi(A[1], psi, 0)
    y = 0.5 * cy_expect_psi(A[2], psi, 0)

    d2_1 =         np.sqrt(0.5) * ((A[0] * psi) - x * psi)
    d2_2 = -1.0j * np.sqrt(0.5) * ((A[0] * psi) - y * psi)

    return [d2_1, d2_2]

In [45]:
result = ssesolve(H, psi0, times, sc_ops, e_ops, 
                  ntraj=ntraj, nsubsteps=nsubsteps, 
                  d1=d1_psi_func, d2=d2_psi_func, d2_len=2, distribution='normal', 
                  m_ops=[[(a + a.dag()), (-1j)*(a - a.dag())]],
                  dW_factors=[2/sqrt(gamma), 2/sqrt(gamma)],
                  store_measurement=True)


10.0%. Run time:  61.07s. Est. time left: 00:00:09:09
20.0%. Run time: 121.07s. Est. time left: 00:00:08:04
30.0%. Run time: 181.14s. Est. time left: 00:00:07:02
40.0%. Run time: 241.15s. Est. time left: 00:00:06:01
50.0%. Run time: 301.12s. Est. time left: 00:00:05:01
60.0%. Run time: 361.32s. Est. time left: 00:00:04:00
70.0%. Run time: 421.50s. Est. time left: 00:00:03:00
80.0%. Run time: 481.80s. Est. time left: 00:00:02:00
90.0%. Run time: 542.40s. Est. time left: 00:00:01:00
Total run time: 602.85s

In [46]:
plot_expectation_values([result, result_ref]);



In [47]:
fig, ax = subplots()

for m in result.measurement:
    ax.plot(times, m[:, 0, 0].real, 'r', alpha=0.025)
    ax.plot(times, m[:, 0, 1].real, 'b', alpha=0.025)
    
ax.plot(times, result_ref.expect[1], 'k', lw=2);
ax.plot(times, result_ref.expect[2], 'k', lw=2);
ax.set_ylim(-20, 20)

ax.plot(times, array(result.measurement).mean(axis=0)[:,0,0].real, 'r', lw=2);
ax.plot(times, array(result.measurement).mean(axis=0)[:,0,1].real, 'b', lw=2);


Using QuTiP built-in heterodyne detection functions for $D_1$ and $D_2$


In [48]:
result = ssesolve(H, psi0, times, sc_ops, e_ops, ntraj=ntraj, nsubsteps=nsubsteps,
                  method='heterodyne', store_measurement=True)


10.0%. Run time:  58.33s. Est. time left: 00:00:08:44
20.0%. Run time: 116.58s. Est. time left: 00:00:07:46
30.0%. Run time: 174.46s. Est. time left: 00:00:06:47
40.0%. Run time: 232.22s. Est. time left: 00:00:05:48
50.0%. Run time: 290.26s. Est. time left: 00:00:04:50
60.0%. Run time: 352.45s. Est. time left: 00:00:03:54
70.0%. Run time: 414.95s. Est. time left: 00:00:02:57
80.0%. Run time: 475.69s. Est. time left: 00:00:01:58
90.0%. Run time: 536.19s. Est. time left: 00:00:00:59
Total run time: 595.14s

In [49]:
plot_expectation_values([result, result_ref]);



In [50]:
fig, ax = subplots()

for m in result.measurement:
    ax.plot(times, m[:, 0, 0].real / sqrt(gamma), 'r', alpha=0.025)
    ax.plot(times, m[:, 0, 1].real / sqrt(gamma), 'b', alpha=0.025)

ax.plot(times, result_ref.expect[1], 'k', lw=2);
ax.plot(times, result_ref.expect[2], 'k', lw=2);
#ax.set_ylim(-15, 15)

ax.plot(times, array(result.measurement).mean(axis=0)[:,0,0].real / sqrt(gamma), 'r', lw=2);
ax.plot(times, array(result.measurement).mean(axis=0)[:,0,1].real / sqrt(gamma), 'b', lw=2);


Solve problem again, this time with a specified noise (from previous run)


In [51]:
result = ssesolve(H, psi0, times, sc_ops, e_ops, ntraj=ntraj, nsubsteps=nsubsteps,
                  method='heterodyne', store_measurement=True, noise=result.noise)


10.0%. Run time:  59.71s. Est. time left: 00:00:08:57
20.0%. Run time: 118.09s. Est. time left: 00:00:07:52
30.0%. Run time: 178.62s. Est. time left: 00:00:06:56
40.0%. Run time: 240.86s. Est. time left: 00:00:06:01
50.0%. Run time: 300.65s. Est. time left: 00:00:05:00
60.0%. Run time: 362.20s. Est. time left: 00:00:04:01
70.0%. Run time: 423.87s. Est. time left: 00:00:03:01
80.0%. Run time: 483.25s. Est. time left: 00:00:02:00
90.0%. Run time: 542.51s. Est. time left: 00:00:01:00
Total run time: 601.77s

In [52]:
plot_expectation_values([result, result_ref]);



In [53]:
fig, ax = subplots(figsize=(12,6))

for m in result.measurement:
    ax.plot(times, m[:, 0, 0].real / sqrt(gamma), 'r', alpha=0.025)
    ax.plot(times, m[:, 0, 1].real / sqrt(gamma), 'b', alpha=0.025)
    
ax.plot(times, result_ref.expect[1], 'k', lw=2);
ax.plot(times, result_ref.expect[2], 'k', lw=2);
ax.set_ylim(-25, 25)

ax.plot(times, array(result.measurement).mean(axis=0)[:,0,0].real / sqrt(gamma), 'r', lw=2);
ax.plot(times, array(result.measurement).mean(axis=0)[:,0,1].real / sqrt(gamma), 'b', lw=2);


Software version


In [54]:
from qutip.ipynbtools import version_table

version_table()


Out[54]:
SoftwareVersion
OSposix [linux]
Python3.3.2+ (default, Oct 9 2013, 14:50:09) [GCC 4.8.1]
Numpy1.9.0.dev-d4c7c3a
QuTiP3.0.0.dev-8072d41
SciPy0.15.0.dev-c04c506
IPython2.0.0-b1
Cython0.20.post0
matplotlib1.4.x
Fri Mar 14 15:51:47 2014 JST