#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
QOI, WS 18/19
-- solve Bloch equations for a two-level system
-- with and without the resonance approximation (also known as 
   'rotating wave approximation' RWA) and for a 
   complex Rabi frequency Omega(t), that gives a pulse (envelope)
-- playing with some pictures in the Bloch sphere 
   (only 2D projections, nothing 3D yet)
"""

from pylab import *
from scipy.integrate import odeint

mysize = 16
tsize = 18
xsize = 6.5
ysize = xsize*0.5*(sqrt(5)-1)
plt.rcParams['figure.figsize'] = (xsize, ysize)
plt.rcParams['figure.autolayout'] = True
plt.rcParams['font.size'] = tsize-4
plt.rcParams['axes.labelsize'] = tsize+2
plt.rcParams['xtick.labelsize'] = tsize-4
plt.rcParams['ytick.labelsize'] = tsize-4

def setParameters(theta0 = 0.0*pi, phi0 = 0., Q_print = False):
    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, dt, \
           Omega, Rabi_env

    gamma_e = 1.
    Gamma_eg = (2.0 + 0.5)*gamma_e # minimum value is 0.5*gamma_e
    Atom_f = 200.*gamma_e
    Atom_f = 200.*gamma_e
    Detuning =  - 3.*gamma_e
    Laser_f = Atom_f + Detuning
    Rabi_f = -20.0*gamma_e*exp(+0.*pi*1j)
    pulsed = True # True = 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 not(pulsed):
        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)

    # interesting time interval
    Nb_t = 40
    tMax = 3./gamma_e
    Nb_t = 80
    tMax = 1.3/gamma_e
    tMax = max(tMax, 3.*pi/(gamma_e + abs(Rabi_f)))
    t, dt = linspace(-0.0*pulse_tau, tMax, Nb_t, retstep = True)

    if Q_print:
        print('|Rabi| = %3.4g gamma' %(abs(Rabi_f)/gamma_e))
        if pulsed:
            print('laser pulse ~ %3.4g/gamma' %(pulse_tau*gamma_e))
        else:
            print('cw laser')
        print '(c_e(0), c_g(0)) = ',; print(c0[1::-1])
        print('time 0 (%3.4g) ... %3.4g / gamma' %(gamma_e*dt, gamma_e*tMax))
    return


def RabiH(c, t):
    global Rabi_f, Laser_f, Atom_f, Gamma_eg, gamma_e
    """
    two-level Hamiltonian applied to vector of amplitudes c = [g(t), e(t)]
    written in rotating frame at laser frequency, but not making RWA
    
    i d ce / d t = 0.5*Omega(t)*exp(1j*Laser_f*t)*cg - 0.5*(Laser_f - Atom_f)*ce
    i d cg / d t = 0.5*Omega(t)*exp(-1j*Laser_f*t)*ce + 0.5*(Laser_f - Atom_f)*cg

    """
    (cg, ce) = c
    dotce = -1j*(0.5*Omega(t)*exp(1j*Laser_f*t)*cg - 0.5*(Laser_f - Atom_f)*ce)
    dotcg = -1j*(0.5*Omega(t)*exp(-1j*Laser_f*t)*ce + 0.5*(Laser_f - Atom_f)*cg)
    return [dotcg, dotce]

def RabiH_rwa(c, t):
    global Rabi_f, Laser_f, Atom_f, Gamma_eg, gamma_e
    """
    two-level Hamiltonian in resonance approximation (rotating wave, RWA),
    in same rotating frame as RabiH, but fast rotating terms dropped
    
    i d ce / d t = 0.5*Rabi_f*cg - 0.5*(Laser_f - Atom_f)*ce
    i d cg / d t = 0.5*conj(Rabi_f)*ce + 0.5*(Laser_f - Atom_f)*cg

    """
    (cg, ce) = c
    dotce = -1j*(0.5*Rabi_env(t)*cg - 0.5*(Laser_f - Atom_f)*ce)
    dotcg = -1j*(0.5*conj(Rabi_env(t))*ce + 0.5*(Laser_f - Atom_f)*cg)
    return [dotcg, dotce]


"""
The differential equation is solved with the command
odeint(r_Rabi, c0.view(float64), t, full_output = 1)
r_Rabi: command that provides the time derivative, working
with real input and output
c0: the initial condition, complex numbers
t: times where the solution is computed (a vector)

Challenge: 'odeint' works with real-valued functions
the translation from complex to real numbers is done
in Python with the 'view' command. A complex number z
can be converted into a pair of real numbers (with some
floating point precision) with x = z.view(float64).
The reverse is done with z = x.view(complex128). 

The following code transforms the function RabiH that
works with complex numbers c = (cg, ce) into a function 
that accepts real numbers. Code inspired from
http://stackoverflow.com/questions/19910189/
scipy-odeint-with-complex-initial-values#19921629
"""
def r_Rabi(x, t, *args):
    c = x.view(complex128)
    dcdt = RabiH(c, t, *args)
    return asarray(dcdt, dtype=complex128).view(float64)

def r_Rabi_rwa(x, t, *args):
    c = x.view(complex128)
    dcdt = RabiH_rwa(c, t, *args)
    return asarray(dcdt, dtype=complex128).view(float64)

def BlochL(state, t):
    """
    Bloch equations ('superoperator or Liouvillian L') 
    worked out for complex observables 
    '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*Omega(t)*exp(1j*Laser_f*t)*(pe - pg)
    dotpe = - gamma_e*pe - \
               imag(Omega(t)*sigma*exp(-1j*Laser_f*t))
    dotpg = - dotpe
    return [dotpg, dotpe, dotsigma]

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(x, t, *args):
# mapping of BlochL to function of real parameters
    state = x.view(complex128)
    dstatedt = BlochL(state, t, *args)
    return asarray(dstatedt, dtype=complex128).view(float64)

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)

if True:
    # solve equations of motion, pure-state case with Rabi Hamiltonian first
    #
    print('solve two-level equations')
    setParameters(Q_print = True)

    # important to signal that c0 has complex entries, otherwise
    # the conversion to real pairs does not work
    c0 = array(c0, dtype=complex)
    sol = odeint(r_Rabi, c0.view(float64), t, full_output = 1)
    sol = sol[0].view(complex128)
    #
    # extract the probability amplitudes from the solution
    cg = sol[:,0]
    ce = sol[:,1]

    rwa = odeint(r_Rabi_rwa, c0.view(float64), t, full_output = 1)
    rwa = rwa[0].view(complex128)
    #
    # extract the probability amplitudes from the solution
    cg_rwa = rwa[:,0]
    ce_rwa = rwa[:,1]

    # take meaningful quantities
    pe_t = abs(ce)**2
    pg_t = abs(cg)**2
    dipole_t = conj(cg)*ce
    P = pe_t + pg_t

    # take meaningful quantities
    pe_rwa = abs(ce_rwa)**2
    pg_rwa = abs(cg_rwa)**2

    # now solve with dissipation the Bloch equations, using the
    # same initial state
    (cg0, ce0) = c0
    state0 = [abs(cg0)**2, abs(ce0)**2, conj(cg0)*ce0]
    #
    # again, need to specify that this is a tuple of complex numbers
    state0 = array(state0, dtype=complex)
    sol = odeint(r_BlochL, state0.view(float64), t, full_output = 1)
    sol = sol[0].view(complex128)
    #
    bg_t = real(sol[:,0])
    be_t = real(sol[:,1])
    sigma_t = sol[:,2]

    # repeat with RWA version of Bloch equations
    #
    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]

if True:
    # test whether numerical solution has real-valued populations
    fg = figure('0 reality test')
    plot( gamma_e*t, imag(sol[:,0]), 'b', label = r'$\mathsf{Im\ }p_g$' )
    plot( gamma_e*t, imag(sol[:,1]), 'r', label = r'$\mathsf{Im\ }p_e$' )

    plot( gamma_e*t, imag(rwa[:,0]), 'b+', label = r'$\mathsf{Im\ }p_g$, rwa' )
    plot( gamma_e*t, imag(rwa[:,1]), 'r+', label = r'$\mathsf{Im\ }p_e$, rwa' )

    xlabel( r'time $\gamma_e t$')
    ylabel( r'imag part of populations')
    legend()
    fg.set_tight_layout(True)

if True: 
    # check norm of Bloch vector (= 1 for pure states)
    fg = figure('1 norm of Bloch vector')

    # Hamiltonian case, should be = 1
    norm_H = sqrt((pe_t - pg_t)**2 + 4*abs(dipole_t)**2)
    plot( gamma_e*t, norm_H, 'k', label = r'$\mathsf{Schr\"odinger}$' )

    # norm of Bloch vector (related to purity of state)
    norm_B = sqrt((be_t - bg_t)**2 + 4*abs(sigma_t)**2)
    plot( gamma_e*t, norm_B, 'b', label = r'$\mathsf{Bloch eqn}$' )

    norm_rwa = sqrt((be_rwa - bg_rwa)**2 + 4*abs(sigma_rwa)**2)
    plot( gamma_e*t, norm_rwa, 'b+', label = r'$\mathsf{Bloch + rwa}$' )

    plot( gamma_e*t, P, 'k.', label = '$\mathsf{total\ } p(t)$, Schr.' )
    plot( gamma_e*t, be_t + bg_t, 'b.', label = '$\mathsf{total\ } p(t)$, Bloch' )

    xlabel( r'time $\gamma_e t$')
    ylabel( r'norm $|{\bf s}(t)|$')
    legend()
    fg.set_tight_layout(True)


# compare Hamiltonian and Bloch models
#
if True:
    fh = figure('2 Schrödinger vs Bloch')
# plot data from Hamiltonian dynamics
    plot( gamma_e*t, pe_t, 'k.--', label = '$p_e(t)$' )
    plot( gamma_e*t, pg_t, 'k.--', label = '$p_g(t)$' )
#
# add data from Bloch equations (incl damping)
    plot( gamma_e*t, be_t, 'b-', label = '$p_e(t)$ (Bloch)' )
    plot( gamma_e*t, bg_t, 'm-', label = '$p_g(t)$ (Bloch)' )

    legend(loc = 'upper right')
    xlabel(r'$\mathsf{time\ }\, \gamma t$')
    ylabel('$\mathsf{populations}$')
    fh.set_tight_layout(True)


# compare Hamiltonian and Bloch models
#
if True:
    fh = figure('3 Schrödinger vs Bloch: dipole')
# plot data from Hamiltonian dynamics
    plot( gamma_e*t, pe_t, 'r.', label = '$p_e(t)$' )
#    plot( gamma_e*t, pg_t, 'b', label = '$p_g(t)$' )
    plot( gamma_e*t, real(conj(cg)*ce), 'c.', label = '$\mathsf{Re\ } d(t)$' )
#
# add data from Bloch equations (incl damping)
    plot( gamma_e*t, be_t, 'k-', label = '$p_e(t)$ (Bloch)' )
    plot( gamma_e*t, real(sigma_t), 'c-', label = '$\mathsf{Re\ } d(t)$ (Bloch)' )

    legend(loc = 'upper right')
    xlabel(r'$\mathsf{time\ }\, \gamma t$')
    fh.set_tight_layout(True)


# illustrate RWA for Hamiltonian model
#
if True:
    fh = figure('4 Hamiltonian: test RWA')
# plot data from Hamiltonian dynamics
    plot( gamma_e*t, pe_t, 'r.', label = '$p_e(t)$' )
    plot( gamma_e*t, pe_rwa, 'r-', label = '$p_e(t)$, rwa' )
#    plot( gamma_e*t, pg_t, 'b', label = '$p_g(t)$' )
#    plot( gamma_e*t, P, 'k', label = '$\mathsf{total\ } p(t)$' )
    plot( gamma_e*t, real(conj(cg)*ce), 'c.', label = '$\mathsf{Re\ } d(t)$' )
    plot( gamma_e*t, real(conj(cg_rwa)*ce_rwa), 'c-', label = '$\mathsf{Re\ } d(t)$, rwa' )

    legend(loc = 'upper right')
    xlabel(r'$\mathsf{time\ }\, \gamma t$')
    fh.set_tight_layout(True)

# illustrate RWA for Bloch model
#
if True:
    fh = figure('5 Bloch: test RWA')
#
# data from Bloch equations (incl damping)
    plot( gamma_e*t, be_t, 'k', label = '$p_e(t)$ (Bloch)' )
    plot( gamma_e*t, real(sigma_t), 'c', label = '$\mathsf{Re\ } d(t)$ (Bloch)' )

    plot( gamma_e*t, be_rwa, 'k.', label = '$p_e(t)$ (Bloch), rwa' )
    plot( gamma_e*t, real(sigma_rwa), 'c.', label = '$\mathsf{Re\ } d(t)$ (Bloch), rwa' )

    legend(loc = 'center right')
    xlabel(r'$\mathsf{time\ }\, \gamma t$')
    fh.set_tight_layout(True)


# illustrate absorption and stimulated emission

if True:
    fh = figure('absorption, stimulated emission')
    #
    # data from Bloch equations (incl damping)
    # dipole is sigma_rwa, plot imaginary part, defined by
    # projection onto (negative) complex Rabi frequency
    
    plot( gamma_e*t, - imag(conj(Rabi_env(t))*sigma_rwa)/abs(Rabi_f), 'g', \
          label = '$\mathsf{Im\ } d(t) \mathsf{\ (abs/em)}$' )

    plot( gamma_e*t, - real(conj(Rabi_env(t))*sigma_rwa)/abs(Rabi_f), 'c--', \
          label = '$\mathsf{Re\ } d(t)$' )
   
    plot( gamma_e*t, be_rwa, 'k', lw = 2., label = '$\mathsf{excited\ } p_e(t)$' )

    plot( gamma_e*t, -real(Rabi_env(t))/abs(Rabi_f), 'r', label = '$\mathsf{Re\ } E(t)/|\mathsf{max\,} E|$' )

    legend(loc = 'upper right')
    xlabel(r'$\mathsf{time\ }\, \gamma t$')
    fh.set_tight_layout(True)


# plot entropy and purity vs time
#
if True:
    fh = figure(5)

#    norm_B = sqrt((be_t - bg_t)**2 + 4*abs(sigma_t)**2)
#    print('end: norm(B) = %4.4g' %(norm_B[-1]))
#    purity = 0.5*(1. + norm_B**2)
#    ent = - 0.5*(1. + norm_B)*log(0.5*(1. + norm_B))
#    for ii, nrm in enumerate(norm_B):
#        if nrm < 1:
#            ent[ii] -= 0.5*(1. - nrm)*log(0.5*(1. - nrm))
#
#    plot( t, purity, 'm.-', label = 'purity' )
#    plot( t, ent, 'b.-', label = 'entropy' )

    norm_B = sqrt((be_rwa - bg_rwa)**2 + 4*abs(sigma_rwa)**2)
    print('end: norm(B) = %4.4g' %(norm_B[-1]))
    purity = 0.5*(1. + norm_B**2)
    ent = - 0.5*(1. + norm_B)*log(0.5*(1. + norm_B))
    for ii, nrm in enumerate(norm_B):
        if nrm < 1:
            ent[ii] -= 0.5*(1. - nrm)*log(0.5*(1. - nrm))

    plot( gamma_e*t, 0*0.59 + purity, 'm.', label = 'purity (RWA)' )
    plot( gamma_e*t, ent, 'b.', label = 'entropy (RWA)' )

    xlabel(r'$\mathsf{time\ } \gamma t$')
    legend(loc = 'center right')

show(block=False)


Excitation of a two-level-system with a short pulse: results for the induced dipole
and the excited state population (numerical solution of Bloch equations).