#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
Resonance fluorescence with the regression formula (SS 2017, QO II).
We have a routine that solves the Bloch equations with initial data
pe, pg, sigma (dipole). If one applies the regression hypothesis, we
need an initial state \sigma \rho whose expectation values are
Tr[ \pi_e \sigma \rho ] = 0,  since \sigma = |g>,
Tr[ \sigma \sigma \rho ] = 0,
Tr[ \sigma^\dag \sigma \rho ] = <\pi_e>
* Note that complex conjugation does not hold any longer!
* This means that we have to re-write the equations in such a way
  that conjugation is not 'hard-wired'.
"""

from pylab import *
from scipy.integrate import odeint

xsize = 6.5
ysize = 1.1*xsize*0.5*(sqrt(5.) - 1.)
mysize = 16

def setParameters(theta0 = 1.0*pi, phi0 = 0.):
    # theta0 = pi (0) is ground (excited) state
    """
    set parameters for two-level dynamics (Bloch equations)
    cw = True: cw laser
    cw = False: pulsed laser (length pulse_tau)
    c0 = initial condition for two-level vector (pure state)
    t = vector of times 0 < t < tMax where solution is plotted
    (default) initial conditions with optional parameters theta0, phi0
    """
    global Rabi_f, Laser_f, Atom_f, Gamma_eg, gamma_e, cw, pulse_tau, c0, t, N_t, dt, \
           Omega, Rabi_env

    gamma_e = 1.
    Gamma_eg = (0.0 + 0.5)*gamma_e # minimum value is 0.5*gamma_e
    Atom_f = 200.*gamma_e
    Laser_f = Atom_f + 0.0*gamma_e
    Rabi_f = 2.0*gamma_e*exp(+0.0*pi*1j)
    cw = True # False = pulsed laser
    # pulse area is Rabi_f * sqrt(2*pi) * pulse_tau
    pulse_tau = pi/abs(Rabi_f)/sqrt(2*pi)
#    pulse_tau = 0.075/gamma_e
#    pulse_tau = 1./gamma_e
    if cw:
        def Omega(t):
            return 2.*real(Rabi_f*exp(-1j*Laser_f*t))
        def Rabi_env(t):
            return Rabi_f*ones_like(t)
    else:
        def Omega(t):
            return 2.*real(Rabi_f*exp(-1j*Laser_f*t))*exp(-0.5*((t - pulse_tau)/pulse_tau)**2)
        def Rabi_env(t):
            return Rabi_f*exp(-0.5*((t - pulse_tau)/pulse_tau)**2)

    # initial conditions, parametrized by angle theta0 on Bloch sphere,
    # theta0 = 0 is excited state ('spin up')
    # phi0 is relative phase, fixes phase of initial dipole
    #
    cg0, ce0 = sin(0.5*theta0)*exp(-1j*0.5*phi0), cos(0.5*theta0)*exp(1j*0.5*phi0)
    c0 = (cg0, ce0)
    # important to signal that c0 has complex entries, otherwise
    # the conversion to real pairs does not work
    c0 = array(c0, dtype=complex)

    # interesting time interval
    tMax = 3./gamma_e
    tMax = 24.*max(3./gamma_e, 3.*pi/(gamma_e + abs(Rabi_f)))
    N_t = 1024
    t, dt = linspace(-0.0*pulse_tau, tMax, N_t, retstep = True)

    return

def steady_state(trace_0 = 1.):
    global Rabi_f, Laser_f, Atom_f, Gamma_eg, gamma_e

    Delta_L = Laser_f - Atom_f
    denom = 0.5*gamma_e + 0.5*abs(Rabi_f)**2*Gamma_eg/(Gamma_eg**2 + Delta_L**2)
    inversion = -0.5*gamma_e / denom
    dipole = 0.5j*Rabi_f*inversion/(Gamma_eg - 1j*Delta_L)
    pe = 0.5*(trace_0 + inversion)
    pg = 0.5*(trace_0 - inversion)
    return [pg, pe, dipole]

def BlochL_rwa(state, t):
    """
    Bloch equations ('superoperator or Liouvillian L') 
    worked out for complex observables with the RWA
    'state' = [pg, pe, sigma], this is *not* the Bloch vector!
    """
    global Rabi_f, Laser_f, Atom_f, Gamma_eg, gamma_e
    (pg, pe, sigma) = state
    dotsigma = -1j*(Atom_f - Laser_f)*sigma - Gamma_eg*sigma + \
               0.5*1j*Rabi_env(t)*(pe - pg)
    dotpe = - gamma_e*pe - imag(conj(Rabi_env(t))*sigma)
    dotpg = - dotpe
    return [dotpg, dotpe, dotsigma]


def r_BlochL_rwa(x, t, *args):
# mapping of BlochL to function of real parameters
    state = x.view(complex128)
    dstatedt = BlochL_rwa(state, t, *args)
    return asarray(dstatedt, dtype=complex128).view(float64)


def xBlochL_rwa(xstate, t):
    """
    extended Bloch equations ('superoperator or Liouvillian L') 
    not assuming that  = conj()
    'xstate' = [pg, pe, sigma, sigmaDag]
    """
    global Rabi_f, Laser_f, Atom_f, Gamma_eg, gamma_e
    (pg, pe, sigma, sigmaDag) = xstate
    dotsigma = -1j*(Atom_f - Laser_f)*sigma - Gamma_eg*sigma + \
               0.5*1j*Rabi_env(t)*(pe - pg)
    dotsigmaDag = 1j*(Atom_f - Laser_f)*sigmaDag - Gamma_eg*sigmaDag - \
               0.5*1j*conj(Rabi_env(t))*(pe - pg)
    dotpe = - gamma_e*pe + 0.5j*(conj(Rabi_env(t))*sigma - Rabi_env(t)*sigmaDag)
    dotpg = - dotpe
    return [dotpg, dotpe, dotsigma, dotsigmaDag]

def r_xBlochL_rwa(x, t, *args):
# mapping of xBlochL to function of real parameters
    state = x.view(complex128)
    dstatedt = xBlochL_rwa(state, t, *args)
    return asarray(dstatedt, dtype=complex128).view(float64)


if False:
    # solve equations of motion, compare two versions of Bloch equations
    #
    setParameters()
    print('solve two-level equations')

    # solve with dissipation the Bloch equations, using the
    # same initial state
    (cg0, ce0) = c0
    state0 = [abs(cg0)**2, abs(ce0)**2, conj(cg0)*ce0]
    # 'extended state', with complex conjugate (sigmaDag) separately
    xstate0 = [abs(cg0)**2, abs(ce0)**2, conj(cg0)*ce0, cg0*conj(ce0)]
    #
    # again, need to specify that these are tuples of complex numbers
    state0 = array(state0, dtype=complex)
    xstate0 = array(xstate0, dtype=complex)

    # solve the Bloch equations (RWA version only)
    #
    rwa = odeint(r_BlochL_rwa, state0.view(float64), t, full_output = 1)
    rwa = rwa[0].view(complex128)
    #
    bg_rwa = real(rwa[:,0])
    be_rwa = real(rwa[:,1])
    sigma_rwa = rwa[:,2]

    xrwa = odeint(r_xBlochL_rwa, xstate0.view(float64), t, full_output = 1)
    xrwa = xrwa[0].view(complex128)
    #
    bg_xrwa = real(xrwa[:,0])
    be_xrwa = real(xrwa[:,1])
    sigma_xrwa = xrwa[:,2]
    sigmaDag_xrwa = xrwa[:,3]

    # analytical solution for steady state
    #
    [pg, pe, dipole] = steady_state()

if True:
    #
    setParameters()
    print('solve two-level correlations')

    # analytical solution for steady state
    #
    [pg, pe, dipole] = steady_state()

    # solve Bloch equations in the regression sense, take
    # modified initial state from sigma rho_ss and from sigma rho_ss sigmaDag
    # (intensity correlations)
    # 
    xstate_ss = [dipole, 0., 0., pe]
    xstate_ss = array(xstate_ss, dtype=complex)

    # solve the RWA version of Bloch equations
    #
    corr = odeint(r_xBlochL_rwa, xstate_ss.view(float64), t, full_output = 1)
    corr = corr[0].view(complex128)
    #
    bg_corr = real(corr[:,0])
    be_corr = real(corr[:,1])
    sigma_corr = corr[:,2]
    sigmaDag_corr = corr[:,3]

    # for intensity correlations
    xstate_ss = [pe, 0., 0., 0.] # should be the ground state
    xstate_ss = array(xstate_ss, dtype=complex)

    g2 = odeint(r_xBlochL_rwa, xstate_ss.view(float64), t, full_output = 1)
    g2 = g2[0].view(complex128)
    #
    bg_g2 = real(g2[:,0])
    be_g2 = real(g2[:,1]) # this is the G2(tau) correlation function
    sigma_g2 = g2[:,2]
    sigmaDag_g2 = g2[:,3]

    # for squeezing correlations, quadrature phase
    theta = 0.5*pi
    xstate_ss = [exp(1j*theta)*dipole, exp(-1j*theta)*conj(dipole), \
                 exp(-1j*theta)*pg, exp(1j*theta)*pe]
    xstate_ss = array(xstate_ss, dtype=complex)

    sq_corr = odeint(r_xBlochL_rwa, xstate_ss.view(float64), t, full_output = 1)
    sq_corr = sq_corr[0].view(complex128)
    #
    sq_sigma = sq_corr[:,2]
    sq_sigmaDag = sq_corr[:,3]
    sq_theta = exp(1j*theta)*sq_sigma + exp(-1j*theta)*sq_sigmaDag

if False:
    # extract final (stationary?) state, can be compared to 
    # analytical results
    #
    g_ss = bg_rwa[-1]
    e_ss = be_rwa[-1]
    sigma_ss = sigma_rwa[-1]

    g_xss = bg_xrwa[-1]
    e_xss = be_xrwa[-1]
    sigma_xss = sigma_xrwa[-1]
    sigmaDag_xss = sigmaDag_xrwa[-1]

    print('stationary state (standard / complexified)')
    print('at time %3.4g / gamma_e' %(gamma_e*t[-1]))
    print('p_e = %3.4g (%3.4g)' %(e_ss, e_xss))
    print('p_g = %3.4g (%3.4g)' %(g_ss, g_xss))
    print('p_e + p_g = %3.4g (%3.4g)' %(e_ss + g_ss, e_xss + g_xss))

    print('sigma = %3.4g + %3.4g i (%3.4g + %3.4g i, %3.4g + %3.4g i)' 
           %(real(sigma_ss), imag(sigma_ss), real(sigma_xss), imag(sigma_xss),
                                     real(sigmaDag_xss), -imag(sigmaDag_xss)))

    print('theory:')
    print('p_e = %3.4g' %(pe))
    print('p_g = %3.4g' %(pg))

    print('sigma = %3.4g + %3.4g i' %(real(dipole), imag(dipole)))

if False:
   figure(2, (xsize, ysize), tight_layout = True)
   plot( t, bg_rwa, 'b', lw = 1.5, label = 'g' )
   plot( t, be_rwa, 'r', lw = 1.5, label = 'e' )
   plot( t, real(sigma_rwa), 'g', lw = 1.5, label = '$\mathsf{Re\ }d$' )
   plot( t, imag(sigma_rwa), 'g--', lw = 1.5, label = '$\mathsf{Im\ }d$' )

if False:
   figure(2, (xsize, ysize), tight_layout = True)
   plot( t, bg_xrwa, 'b+', label = 'g' )
   plot( t, be_xrwa, 'r+', label = 'e' )
   plot( t, real(sigma_xrwa), 'g+', label = '$\mathsf{Re\ }d$' )
   plot( t, imag(sigma_xrwa), 'g+', label = '$\mathsf{Im\ }d$' )
   plot( t, real(sigmaDag_xrwa), '.' )
   plot( t, -imag(sigmaDag_xrwa), '.' )
   ylim(-1, 1)
   legend()

if False:
    """
    compute spectrum of dipole correlation function, 
    should return Mollow triplet
    """

    ddcorr = sigmaDag_corr - sigmaDag_corr[-1]
    print('check stationary value of C_sigma: %3.4g + %3.4g i vs %3.4g' 
           %(real(sigmaDag_corr[-1]), imag(sigmaDag_corr[-1]), abs(dipole)**2) )
    spec = 2*real(dt*fft(ddcorr))

    figure(4, (xsize, ysize), tight_layout = True)
    plot( t, real(sigmaDag_corr), 'b' )
    plot( t, imag(sigmaDag_corr), 'r' )

    t_max = 12./gamma_e
    xlim(0, t_max)
    plot( [0, t_max], [abs(dipole)**2, abs(dipole)**2], ':' )

    spec = fftshift(spec)
    omega = fftshift(2*pi*fftfreq(N_t, d = dt))

    figure('Mollow', (xsize, ysize), tight_layout = True)
    plot(omega/gamma_e, spec, 'r-')
    om_max = max(2.5*sqrt(abs(Rabi_f)**2 + (Laser_f - Atom_f)**2), 3.*gamma_e)

    xlim(-om_max, om_max)
    xlabel(r'$\mathsf{frequency\ } (\omega - \omega_L)/\gamma$', size = mysize)
    ylabel(r'$\mathsf{spectrum\ } S_d( \omega )$', size = mysize)


if True:
    """
    compute 'squeezing spectrum' of correlation Q_sigma(t) = 
    """

    sqcorr = sigma_corr
    print('check stationary value of Q_sigma: %3.4g + %3.4g i vs %3.4g + %5.4g i' 
           %(real(sqcorr[-1]), imag(sqcorr[-1]), \
             real(dipole**2), imag(dipole**2)) )

    # make plot of correlation function
    figure(4, (xsize, ysize), tight_layout = True)
    plot( t, real(sqcorr), 'b', label = 'Re Q' )
    plot( t, imag(sqcorr), 'r', label = 'Im Q' )


    # repeat for sigma_theta correlation function
    sTheta_corr = sq_theta
    dipole_theta = exp(1j*theta)*dipole + exp(-1j*theta)*conj(dipole)
    print('check stationary value of C_theta: %3.4g + %3.4g i vs %3.4g + %5.4g i' 
           %(real(sTheta_corr[-1]), imag(sTheta_corr[-1]), \
             real(dipole_theta**2), imag(dipole_theta**2)) )

    plot( t, real(sTheta_corr), 'b--', label = r'$\mathsf{Re\ }C_\theta$' )
    plot( t, imag(sTheta_corr), 'r--', label = r'$\mathsf{Im\ } C_\theta$' )

    t_max = 12./gamma_e
    xlim(0, t_max)
    plot( [0, t_max], [real(dipole**2), real(dipole**2)], 'g:' )
    plot( [0, t_max], [imag(dipole**2), imag(dipole**2)], 'g:' )

    plot( [0, t_max], [real(dipole_theta**2), real(dipole_theta**2)], 'g:' )
    plot( [0, t_max], [imag(dipole_theta**2), imag(dipole_theta**2)], 'g:' )

    xlabel(r'$\mathsf{time\ } \gamma t$', size = mysize)
    ylabel(r'$\mathsf{correlation\ } Q_\sigma( t )$', size = mysize)
    legend(fontsize = mysize, loc = 'upper right')

    # subtract asymptotic value and take Fourier transform
    sqcorr -= sqcorr[-1]
    spec = 2*real(dt*fft(sqcorr)) # not obvious that this is the right rule
    # since the 'squeezing spectrum' can in general be complex
    #
    spec = fftshift(spec)
    omega = fftshift(2*pi*fftfreq(N_t, d = dt))

    figure('squeezing', (xsize, ysize), tight_layout = True)
    plot(omega/gamma_e, spec, 'r-', label = r'$\sigma\sigma$')

    # repeat for squeezing correlation
    # 
    sTheta_corr -= sTheta_corr[-1]
    spec = 2*real(dt*fft(sTheta_corr)) # standard rule
    # because \sigma_\theta is hermitean
    #
    spec = fftshift(spec)
    plot(omega/gamma_e, spec, 'r--', label = r'$\sigma_\theta\sigma_\theta$')

    om_max = max(2.5*sqrt(abs(Rabi_f)**2 + (Laser_f - Atom_f)**2), 3.*gamma_e)
    xlim(-om_max, om_max)
    xlabel(r'$\mathsf{frequency\ } (\omega - \omega_L)/\gamma$', size = mysize)
    ylabel(r'$\mathsf{squeezing\ } S_\sigma( \omega )$', size = mysize)
    legend(fontsize = mysize)

if False:
    """
    compute spectrum by Fourier transform of excited state pe(t),
    gives intensity correlations
    """
    # plot correlation function
    figure(2, (xsize, ysize), tight_layout = True)
    plot( gamma_e*t, be_rwa, 'r' )
    plot( gamma_e*t, be_xrwa, 'b' )

    plot( gamma_e*t, be_g2/pe, 'k', lw = 1.5 )

    t_max = 5./gamma_e
    xlim(0, t_max)
    ylim(0, 1.05*max(be_rwa))
    plot( [0, t_max], [pe, pe], ':', c = 'gray' )
    xlabel(r'$\mathsf{time\ delay\ } \gamma_e \tau$', size = mysize)
    ylabel(r'$\mathsf{correlation\ } C_I(\tau)$', size = mysize)

    IIcorr = be_rwa - be_rwa[-1] # subtract final value
    spec = 2*real(dt*fft(IIcorr))

    IIcorr = be_xrwa - be_xrwa[-1]
    xspec = 2*real(dt*fft(IIcorr))

    # plot spectrum
    figure(1, (xsize, ysize), tight_layout = True)

    spec = fftshift(spec)
    xspec = fftshift(xspec)
    omega = fftshift(2*pi*fftfreq(N_t, d = dt))

    plot(omega/gamma_e, spec, 'r-')
    plot(omega/gamma_e, xspec, 'b-')
    om_max = max(6*abs(Rabi_f), 8*gamma_e)

    xlim(-om_max, om_max)
    xlabel(r'$\mathsf{frequency\ } (\omega - \omega_L)/\gamma$', size = mysize)
    ylabel(r'$\mathsf{spectrum\ } S_I( \omega )$', size = mysize)


show(block = False)


Squeezing correlations (left) and spectra (right) for a two-level atom. Parameters as in the script.