Thermal Transport from NEMD

Introduction

In this tutorial, we use the NEMD method to study heat transport in graphene at 300 K and zero pressure. Here we consider the ballistic regime. The diffusive regime can be found in the HNEMD example. The spectral decomposition method as described in [Fan 2019] is used here.

Importing Relevant Functions

The inputs/outputs for GPUMD are processed using the Atomic Simulation Environment (ASE) package.

[39]:
from pylab import *
from ase.build import graphene_nanoribbon
from ase.io import write

Preparing the Inputs

We consider a graphene sheet of size of about 25 nm x 43 nm (40400 atoms). The transport is in the \(y\) direction. We divide the length in the \(y\) direction into 9 groups. Group 0 (a small one) will be fixed. Group 1 (about 9 nm long) will act as a heat source and group 8 (about 9 nm long) will act as a sink. The remaining middle part is evenly divided into a few groups (group 2 to group 7). We use a Tersoff potential [Tersoff 1989] parameterized by Lindsay et al. [Lindsay 2010].

Generate the model.xyz file:

[40]:
gnr = graphene_nanoribbon(100, 101, type='armchair', sheet=True, vacuum=3.35/2)
gnr.euler_rotate(theta=90)
lx, lz, ly = gnr.cell.lengths()
gnr.cell = gnr.cell.new((lx, ly, lz))
gnr.center()
gnr.pbc = [True, True, False]
gnr
[40]:
Atoms(symbols='C40400', pbc=[True, True, False], cell=[245.95121467478057, 430.26, 3.35])

Add Groups for NEMD

[41]:
split = np.array([ly/100] + [ly/5] + [ly/10]*6)-0.4
split = [0] + list(np.cumsum(split)) + [ly]
print("y-direction boundaries:", [round(x,2) for x in split])
y-direction boundaries: [0, 3.9, 89.55, 132.18, 174.81, 217.43, 260.06, 302.68, 345.31, 430.26]
[42]:
group_label = []
for i, atom in enumerate(gnr):
    y_pos = atom.position[1]
    for j in range(len(split)):
        if y_pos > split[j] and y_pos < split[j+1]:
            group_label.append(j)
[43]:
gnr.arrays["group"] = np.array(group_label)
[44]:
for i in range(len(split)-1):
    atom_num = len(gnr[gnr.arrays["group"] == i])
    print(f"group {i}: {atom_num} atoms")
group 0: 400 atoms
group 1: 8000 atoms
group 2: 4000 atoms
group 3: 4000 atoms
group 4: 4000 atoms
group 5: 4000 atoms
group 6: 4000 atoms
group 7: 4000 atoms
group 8: 8000 atoms
[45]:
write("model.xyz", gnr)

The `run.in file:

The run.in input file is given below:

potential    ../../../../potentials/tersoff/Graphene_Lindsay_2010_modified.txt
velocity     300

ensemble     nvt_ber 300 300 100
fix          0
time_step    1
dump_thermo  1000
run          100000

ensemble     heat_lan 300 100 10 1 8
fix          0
compute      0 10 100 temperature
compute_shc  2 250 1 1000 400.0 group 0 4
run          1000000

The first line uses the potential keyword to define the potential to be used, which is specified in the file Graphene_Lindsay_2010_modified.txt.

The second line uses the velocity keyword and sets the velocities to be initialized with a temperature of 300 K.

There are two runs. The first run serves as the equilibration stage.

  • Here, the NVT ensemble (the Berendsen thermostat) is used. The target temperature is 300 K and the coupling constant is 100 time steps for the thermostat.

  • The time_step for integration is 1 fs.

  • Atoms in group 0 will be fixed.

  • The thermodynamic quantities will be output every 1000 steps.

  • There are \(10^5\) steps (100 ps) for this run.

The second run is for production.

  • Here, two local Langevin thermostats (ensemble) are applied to generate a nonequilibrium heat current. The time parameter in the Langevin thermostats is 0.1 ps (100 time steps). The target temperature of the heat source is \(300+10=310\) K and the target temperature of the heat sink is \(300-10=290\) K. Atoms in group 1 are in the heat source region and atoms in group 8 are in the heat sink region.

  • Atoms in group 0 will be fixed.

  • The compute is used to compute the group temperatures and energy transfer rate. Here, grouping method 0 is used, and the relevant data are sampled every 10 time steps and averaged for every 100 data points before written out.

  • The line with the compute_shc keyword is used to compute the spectral heat current (SHC). The SHC will be calculated for group 4 in grouping method 0. The relevant data will be sampled every 2 steps and the maximum correlation time is \(250 \times 2 \times 1~{\rm fs} = 500~{\rm fs}\). The transport directions is 1 (\(y\) direction). The number of frequency points is 1000 and the maximum angular frequncy is 400 THz.

  • There are \(10^6\) steps (1 ns) in the production run. This is just an example. To get more accurate results, we suggest you use \(10^7\) steps (10 ns).

Results and Discussion

Computation Time

  • Using a GeForce RTX 2080 Ti GPU, the NEMD simulation takes about 15 minutes.

Figure Properties

[20]:
aw = 2
fs = 16
font = {'size'   : fs}
matplotlib.rc('font', **font)
matplotlib.rc('axes' , linewidth=aw)

def set_fig_properties(ax_list):
    tl = 8
    tw = 2
    tlm = 4

    for ax in ax_list:
        ax.tick_params(which='major', length=tl, width=tw)
        ax.tick_params(which='minor', length=tlm, width=tw)
        ax.tick_params(axis='both', direction='in', right=True, top=True)

Plot NEMD Results

The compute.out output file is loaded and processed to create the following figure.

[26]:
compute = np.loadtxt("compute.out")
temp = compute[:, :9]
ndata = temp.shape[0]
Ein = compute[:, 9]
Eout = compute[:, 10]
temp_ave = np.mean(temp[int(ndata/2)+1:, 1:], axis =0) #unit in K

dt = 0.001  # ps
Ns = 1000  # Sample interval
t = dt*np.arange(1,ndata+1) * Ns/1000  # ns
[27]:
figure(figsize=(10,5))
subplot(1,2,1)
set_fig_properties([gca()])
group_idx = range(1,9)
plot(group_idx, temp_ave,linewidth=3,marker='o',markersize=10)
xlim([1, 8])
gca().set_xticks(group_idx)
ylim([290, 310])
gca().set_yticks(range(290,311,5))
xlabel('group index')
ylabel('T (K)')
title('(a)')

subplot(1,2,2)
set_fig_properties([gca()])
plot(t, Ein/1000, 'C3', linewidth=3)
plot(t, Eout/1000, 'C0', linewidth=3, linestyle='--' )
xlim([0, 1])
gca().set_xticks(linspace(0,1,6))
ylim([-10, 10])
gca().set_yticks(range(-10,11,5))
xlabel('t (ns)')
ylabel('Heat (keV)')
title('(b)')
tight_layout()
show()
../_images/tutorials_thermal_transport_nemd_17_0.png

(a) Temperature profile in the NEMD simulation. (b) Energies accumulated in the thermostats.

  • The figure above shows the temperature profile and the energies accumulated in the thermostats. The energy of the thermostat coupling to the heat source region is decreasing, because energy is transferred from the thermostat to the atoms in the source region. The energy of the thermostat coupling to the heat sink region is increasing, because energy is transferred from the atoms in the sink region to the thermostat. The absolute values of the slopes of the lines in (b) should be the same; otherwise it means energy is not conserved. The absolute slope is the energy transfer rate \(Q=dE/dt\).

  • The thermal conductance in this system can be calculated as

    \[G = \frac{Q/S}{\Delta T}\]

    where \(S\) is the cross-sectional area and \(\Delta T\) is the temperature difference, which is 19 K here (slightly smaller than the target value of 20 K). The calculated thermal conductance is about 10 GW m-2 K-1. This is the classical value.

[28]:
deltaT = temp_ave[0] - temp_ave[-1]  # [K]
deltaT
[28]:
19.046382565130557
[29]:
Q1 = (Ein[int(ndata/2)] - Ein[-1])/(ndata/2)/dt/Ns
Q2 = (Eout[-1] - Eout[int(ndata/2)])/(ndata/2)/dt/Ns
Q = mean([Q1, Q1])  # [eV/ps]
Q
[29]:
9.635182
[30]:
l = gnr.cell.lengths()
A = l[0]*l[2]/100  # [nm2]
G = 160*Q/deltaT/A  # [GW/m2/K]
G
[30]:
9.823666787865204

Plot Spectral Heat Current Results

The shc.out output file is loaded and processed to create the following figure.

[35]:
num_corr_points, num_omega = 250, 1000

labels_corr = ['t', 'Ki', 'Ko']
labels_omega = ['omega', 'jwi', 'jwo']

num_corr_points_in_run = num_corr_points * 2 - 1
coor_array = np.loadtxt("shc.out", max_rows=num_corr_points_in_run)
omega_array = np.loadtxt("shc.out", skiprows=num_corr_points_in_run)

shc = dict()
for label_num, key in enumerate(labels_corr):
    shc[key] = coor_array[:, label_num]

for label_num, key in enumerate(labels_omega):
    shc[key] = omega_array[:, label_num]
shc["nu"] = shc["omega"] / (2*np.pi)

shc.keys()
[35]:
dict_keys(['t', 'Ki', 'Ko', 'omega', 'jwi', 'jwo', 'nu'])
[36]:
Ly = split[5]-split[4]
Lx, Lz = l[0], l[2]
V = Lx*Ly*Lz
Gc = 1.6e4*(shc['jwi']+shc['jwo'])/V/deltaT
[37]:
figure(figsize=(10,5))
subplot(1,2,1)
set_fig_properties([gca()])
plot(shc['t'], (shc['Ki']+shc['Ko'])/Ly, linewidth=2)
xlim([-0.5, 0.5])
gca().set_xticks([-0.5, 0, 0.5])
ylim([-4, 10])
gca().set_yticks(range(-4,11,2))
ylabel('K (eV/ps)')
xlabel('Correlation time (ps)')
title('(a)')

subplot(1,2,2)
set_fig_properties([gca()])
plot(shc['nu'], Gc, linewidth=2)
xlim([0, 50])
gca().set_xticks(range(0,51,10))
ylim([0, 0.35])
gca().set_yticks(linspace(0,0.35,8))
ylabel(r'$G$($\omega$) (GW/m$^2$/K/THz)')
xlabel(r'$\omega$/2$\pi$ (THz)')
title('(b)')
tight_layout()
show()
../_images/tutorials_thermal_transport_nemd_26_0.png

(a) Virial-velocity correlation function. (b) Spectral thermal conductance.

The figure above shows the virial-velocity correlation function \(K(t)\) and the spectral thermal conductance \(G(\omega)\). See Theoretical formulations for the definitions of these quantities. Using the spectral thermal conductance, one can perform quantum correction, see [Li 2019].

[38]:
np.save('../diffusive/Gc.npy', Gc)

References