Exchange Spectroscopy (EXSY)#

In the previous example, we simulated exchange in a 1D spectrum. Here, we perform the 2D EXSY experiment. We will then calculate the 2D EXSY spectrum, and observe how that spectrum changes as a function of a mixing time. We will also investigate the 2D spectrum as a function of correlation time of exchange.

Setup#

import SLEEPY as sl
import numpy as np
import matplotlib.pyplot as plt

Build the system#

The first step is to build the systems (ex0,ex1), which will have a single nucleus, with two different chemical shifts.

ex0=sl.ExpSys(Nucs='13C',v0H=600)    #We need a description of the experiment for both states (ex0, ex1)
ex1=ex0.copy()
ex0.set_inter(Type='CS',i=0,ppm=-5)   #Add the chemical shifts
_=ex1.set_inter(Type='CS',i=0,ppm=5)

Add the exchange process#

First, we export the systems into Liouville space, and add the exchange process. We also add some \(T_2\) relaxation to destroy transverse magnetization during the delay period for exchange and produce some broadening when the spectrum is very narrow (L.add_relax(...)).

L=sl.Liouvillian(ex0,ex1)           #Builds the two different Hamiltonians and exports them to Liouville space

tc=1     #Correlation time (1 s)
p1=0.75  #Population of state 1
p2=1-p1  #Population of state 2

L.kex=sl.Tools.twoSite_kex(tc=tc,p1=p1)

_=L.add_relax(Type='T2',i=0,T2=.01)

Run as a 2D experiment#

First, we’ll just calculate one 2D spectrum, and then later check how the spectrum evolves as a function of a delay time. We need an initial density matrix, \(S_x\), a detection matrix, \(S^+\), and propagators for evolution times, \(\pi\)/2 pulses, and a delay for the exchange process. We start with generating the propagators and density matrices.

SLEEPY has a function in sl.Tools, sl.Tools.TwoD_Builder, for executing and processing two-dimensional experiements. TwoD_Builder requires an initial density matrix (rho), and sequences for the indirect dimension evolution, the direction dimension evolution (in this example, seq is used for both), and transfer periods between the dimensions (seq_trX, seq_trY). For the transfers, one needs a sequence to convert the X component, and one for the Y component (States acquisition\(^1\)). The sequences for the direct/indirect dimension may just be delays, and can be the same sequence.

[1] D.J. States, R.A. Haberkorn, D.J. Ruben. J. Magn. Reson., 1969, 48, 286-292.

rho=sl.Rho(rho0='S0x',detect='S0p')
# L.Udelta('13C',np.pi/2,np.pi/2)*rho

Dt=1/(2*10*150)  #Delay for a spectrum about twice as broad as the chemical shift difference
seq=L.Sequence(Dt=Dt)  #Sequence for indirect and direct dimensions
seq_trX=L.Sequence()  #X-component of transfer
seq_trY=L.Sequence()  #Y-component of transfer

v1=50000     #pi/2 pulse field strength
tpi2=1/v1/4  #pi/2 pulse length
dly=5
t=[0,tpi2,dly,dly+tpi2] #pi/2 pulse, 1 second delay, pi/2 pulse
seq_trX.add_channel('13C',t=t,v1=[v1,0,v1],phase=[-np.pi/2,0,np.pi/2]) #Convert x to z, delay, convert z to x
seq_trY.add_channel('13C',t=t,v1=[v1,0,v1],phase=[0,0,np.pi/2]) #Convert y to z, delay, convert z to x

twoD=sl.Tools.TwoD_Builder(rho,seq_dir=seq,seq_in=seq,seq_trX=seq_trX,seq_trY=seq_trY)
twoD(32,64)

ax=twoD.plot()
ax.set_xlabel(r'$\delta$($^{13}$C) / kHz')
_=ax.set_ylabel(r'$\delta$($^{13}$C) / kHz')
ax.figure.set_size_inches([7,7])
../_images/279bd07ad5259f3e4a450f78ddf9ff50c339e09dfb1c6bd5e9b4b60ad8444deb.png

Sweep the delay time to observe buildup#

In order to observe the diagonal peaks decay, and the off-diagonal peaks build up, we repeat the above calculation except with different lengths for the delay. We slice through the largest peak and observe buildup of the cross-peak at the same position.

i_dir=[16,48]
i_in=[32,96]

delays=np.linspace(0.1,3,12)
fig,ax=plt.subplots(3,4,figsize=[9,7])
ax=ax.flatten()
I=list()
sm=0
for a,delay in zip(ax,delays):
    t=[0,tpi2,delay,delay+tpi2] #pi/2 pulse, 1 second delay, pi/2 pulse
    seq_trX.clear()
    seq_trY.clear()
    seq_trX.add_channel('13C',t=t,v1=[v1,0,v1],phase=[-np.pi/2,0,np.pi/2]) #Convert x to z, delay, convert z to x
    seq_trY.add_channel('13C',t=t,v1=[v1,0,v1],phase=[0,0,np.pi/2]) #Convert y to z, delay, convert z to x

    twoD=sl.Tools.TwoD_Builder(rho,seq_dir=seq,seq_in=seq,seq_trX=seq_trX,seq_trY=seq_trY)
    twoD(32,64).proc()
    
    a.plot(twoD.v_in/1e3,twoD.Sreal[i_dir[0]].real)
    sm=max(a.get_ylim()[1],sm)
    
    I.append([twoD.Sreal[i_dir[0],i_in[0]].real,twoD.Sreal[i_dir[1],i_in[1]].real,
             twoD.Sreal[i_dir[0],i_in[1]].real,twoD.Sreal[i_dir[1],i_in[0]].real])

I=np.array(I)

for a,delay in zip(ax,delays):
    a.set_ylim([-sm*.1,sm])
    a.text(0,sm*.5,r'$\tau$'+f' = {delay:.1f} s')
    a.set_yticklabels('')
    if a.is_last_row():
        a.set_xlabel(r'$\delta$($^{13}$C) / kHz')
    if a.is_first_col():
        a.set_ylabel('I / a.u.')
        
fig.tight_layout()
../_images/542dda268870984690d36bc61a2e009d9a4b55424b14b034ad5a7145c02899a3.png

Plot trajectory of the individual peaks#

We can track the four peak intensities as a function of delay time. Then, each curve represents the probability of starting in some state and ending in another state after the delay time, \(\tau\). These are given in the legend, where the diagonal peaks then decay with time and the cross peaks build up.

I=np.array(I)

ax=plt.subplots()[1]
ax.plot(delays,I)
ax.set_xlabel(r'$\tau$ / s')
ax.set_ylabel('I / a.u.')
ax.set_yticklabels('')
_=ax.legend((r'$p_1\rightarrow p_1$',r'$p_2\rightarrow p_2$',r'$p_1\rightarrow p_2$',r'$p_2\rightarrow p_1$'))
../_images/a1145d2739e35a8b5720b20941768b44c540e9bd494072631e0e3af3c9d029a7.png

Spectrum as a function of exchange rate#

We can’t use EXSY for very fast motions, because the peaks coalesce such that we are no longer able to observe the independent buildup of the cross peaks. Here, we simulate the coalescence of the four peaks by varying the correlation time.

tc0=np.logspace(-1,-6,6)
p1=0.5  #Population of state 1
p2=1-p1  #Population of state 2

fig=plt.figure(figsize=[10,8])
ax=[fig.add_subplot(2,3,k+1,projection='3d') for k in range(6)]
for a,tc in zip(ax,tc0):
    L.kex=sl.Tools.twoSite_kex(tc=tc,p1=p1)
    
    twoD=sl.Tools.TwoD_Builder(rho,seq_dir=seq,seq_in=seq,seq_trX=seq_trX,seq_trY=seq_trY)
    twoD(32,64).proc()
    
    #Plot the result
    twoD.plot(ax=a)
    a.text(1,-1,a.get_zlim()[1]*1.3,r'$\tau_c = $'+f'{tc:.1e} s')
    
fig.tight_layout()
for a in ax:a.set_zticklabels('')
../_images/bfbb5934ebafdf6fa7e738be6d8cf0292772e114ab4f70450bbc2aa9ee42a71e.png

Finally, we do the same as above, but without having symmetric exchange, i.e. \(p_1\ne p_2\). Here, we set \(p_1=0.75\) and \(p_2=0.25\). We use the sl.Tools.twoSite_kex(tc=...,p1=...) function to build the exchange matrix.

tc0=np.logspace(-1,-6,6)
p1=0.75  #Population of state 1
p2=1-p1  #Population of state 2

fig=plt.figure(figsize=[10,8])
ax=[fig.add_subplot(2,3,k+1,projection='3d') for k in range(6)]
for a,tc in zip(ax,tc0):
    L.kex=sl.Tools.twoSite_kex(tc=tc,p1=p1)
    
    twoD=sl.Tools.TwoD_Builder(rho,seq_dir=seq,seq_in=seq,seq_trX=seq_trX,seq_trY=seq_trY)
    twoD(32,64).proc()
    
    #Plot the result
    twoD.plot(ax=a)
    a.text(1,-1,a.get_zlim()[1]*1.3,r'$\tau_c = $'+f'{tc:.1e} s')
    
fig.tight_layout()
for a in ax:a.set_zticklabels('')
../_images/38fc1b334926b96c94deec2ff05f4e9a0ead4405445d5952e53db6112a1805f7.png

Explicit execution and processing of the 2D sequence#

Above, we have used a built-in class for executing and processing 2D spectra in SLEEPY, however, it may be informative to once execute the whole processing manually.

Acquisition#

# Start from L that has already been generated
L.kex=sl.Tools.twoSite_kex(tc=1,p1=0.75)

rho=sl.Rho(rho0='S0x',detect='S0p')

Uevol=seq.U()

RE=list()
IM=list()
n=64
for k in range(n):
    #First capture the real part
    rho.clear()  #Clear all data in rho
    Uevol**k*rho  #This applies the evolution operator k times
    seq_trX*rho
    rho.DetProp(Uevol,n=n)
    RE.append(rho.I[0])
    
    #Then capture the imaginary part
    rho.clear()  #Clear all data in rho
    Uevol**k*rho  #This applies the evolution operator k times
    seq_trY*rho
    rho.DetProp(Uevol,n=n)
    IM.append(rho.I[0])

Processing#

RE,IM=np.array(RE,dtype=complex),np.array(IM,dtype=complex) #Turn lists into arrays
# Divide first time points by zero
RE[:,0]/=2
RE[0,:]/=2
IM[:,0]/=2
IM[0,:]/=2
# QSINE apodization function
apod=np.cos(np.linspace(0,1,RE.shape[0])*np.pi/2)**2
RE=RE*apod
RE=(RE.T*apod).T
IM=IM*apod
IM=(IM.T*apod).T

nft=n*2
FT_RE=np.fft.fft(RE,n=nft,axis=1).real.astype(complex)
FT_IM=np.fft.fft(IM,n=nft,axis=1).real.astype(complex)
spec=np.fft.fftshift(np.fft.fft(FT_RE+1j*FT_IM,n=nft,axis=0),axes=[0,1])
v=1/(2*Dt)*np.linspace(-1,1,spec.shape[0])  #Frequency axis
v-=(v[1]-v[0])/2 #Shift to have zero at correct position
v*=1e6/ex0.v0[0]   #convert to ppm
vx,vy=np.meshgrid(v,v)  #meshgrid for plotting

Plotting#

from matplotlib import cm
fig=plt.figure(figsize=[7,7])
ax=fig.add_subplot(111,projection='3d')
ax.plot_surface(vx,vy,spec.real,cmap='coolwarm',linewidth=0)
ax.set_xlabel(r'$\delta_1 (^{13}$C) / ppm')
ax.set_ylabel(r'$\delta_2 (^{13}$C) / ppm')
ax.invert_xaxis()
ax.invert_yaxis()
../_images/c07e01f2727e1e05df6f62d2d8de248256dbe07199f5d867b953c8797857317f.png