Example 2: Cepstral Analysis of liquid NaCl

This example shows the basic usage of sportran to compute the thermal conductivity of a classical MD simulation of molten NaCl, which requires the multi-component theory.

For reference: Bertossa et al., PRL 122, 255901 (2019)

[1]:
import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
try:
    import sportran as st
except ImportError:
    from sys import path
    path.append('..')
    import sportran as st

c = plt.rcParams['axes.prop_cycle'].by_key()['color']
[2]:
%matplotlib notebook

1. Load trajectory

Read the heat current from a simple column-formatted file. The desired columns are selected based on their header (e.g. in LAMMPS format).

For other input formats see the corresponding example.

[3]:
jfile = st.i_o.TableFile('./data/NaCl/NaCl.dat', group_vectors=True)
# Molten NaCl - Fumi-Tosi potential
# https://journals.aps.org/prl/supplemental/10.1103/PhysRevLett.122.255901/Bertossa_et_al_Supplemental_Material.pdf
# 1728 atoms, T~1400K
# NVE, dt = 1.0 fs, 100 ps, print_step = 5.0 fs
# Volume = 65013.301261 A^3
# LAMMPS metal units
Temp    c_flux[1] c_flux[2] c_flux[3] c_vcm[1][1] c_vcm[1][2] c_vcm[1][3]
 #####################################
  all_ckeys =  [('Temp', [0]), ('flux', array([1, 2, 3])), ('vcm[1]', array([4, 5, 6]))]
 #####################################
Data length =  20000

We need to load the energy flux (flux) and one mass flux (vcm[1], that is the velocity of the center of mass of one species).

[4]:
jfile.read_datalines(start_step=0, NSTEPS=0, select_ckeys=['Temp', 'flux', 'vcm[1]'])
  ckey =  [('Temp', [0]), ('flux', array([1, 2, 3])), ('vcm[1]', array([4, 5, 6]))]
  ( 20000 ) steps read.
DONE.  Elapsed time:  0.2213594913482666 seconds
[4]:
{'Temp': array([[1442.7319],
        [1440.806 ],
        [1440.6008],
        ...,
        [1402.7391],
        [1392.6822],
        [1385.4031]]),
 'flux': array([[ 250.86549   ,   20.619423  ,  200.115     ],
        [ 196.22265   ,   82.667342  ,  284.3325    ],
        [ 126.39441   ,  160.75472   ,  340.36769   ],
        ...,
        [ 179.91856   ,   18.612706  , -132.65623   ],
        [ 204.71193   ,   -0.46643315, -204.0165    ],
        [ 241.23318   ,  -18.295461  , -252.46475   ]]),
 'vcm[1]': array([[-0.15991832, -0.07137043,  0.02068792],
        [-0.13755206, -0.07100293, -0.01127988],
        [-0.10615044, -0.06238124, -0.04156812],
        ...,
        [-0.0919399 , -0.08477829,  0.06001139],
        [-0.13384949, -0.1147453 ,  0.08932355],
        [-0.18385053, -0.1369343 ,  0.1143406 ]])}

2. Heat Current

Define a HeatCurrent from the trajectory, with the correct parameters. The difference with respect to the single-component case is only here: we need to load a list of currents instead of a single one.

[5]:
DT_FS = 5.0                                # time step [fs]
TEMPERATURE = np.mean(jfile.data['Temp'])  # temperature [K]
VOLUME = 40.21**3                          # volume [A^3]
print('T = {:f} K'.format(TEMPERATURE))
print('V = {:f} A^3'.format(VOLUME))

j = st.HeatCurrent([jfile.data['flux'], jfile.data['vcm[1]']], UNITS='metal', DT_FS=DT_FS,
                   TEMPERATURE=TEMPERATURE, VOLUME=VOLUME)
T = 1399.347781 K
V = 65013.301261 A^3
Using multicomponent code.
[6]:
# trajectory
f, ax = plt.subplots(2, sharex=True)
ax[0].plot(j.timeseries()/1000., j.traj);
ax[1].plot(j.timeseries()/1000., j.otherMD[0].traj);
plt.xlim([0, 1.0])
plt.xlabel(r'$t$ [ps]')
ax[0].set_ylabel(r'$J^0$ [eV A/ps]');
ax[1].set_ylabel(r'$J^1$ [A/ps]');

Compute the Reduced periodogram \(\bar{\mathcal{S}}^0_k\) and filter it for visualization. You can notice the difference with respect to the energy-flux periodogram \(\mathcal{S}^0_k\).

[7]:
# Periodogram with given filtering window width
ax = j.plot_periodogram(PSD_FILTER_W=0.4, kappa_units=True, label=r'$\bar{\mathcal{S}}^0_k$')
print(j.Nyquist_f_THz)

# compare with the spectrum of the energy flux
jen = st.HeatCurrent(jfile.data['flux'], UNITS='metal', DT_FS=DT_FS,
                     TEMPERATURE=TEMPERATURE, VOLUME=VOLUME)
ax = jen.plot_periodogram(axes=ax, PSD_FILTER_W=0.4, kappa_units=True, label=r'$\mathcal{S}^0_k$')
plt.xlim([0, 20])
ax[0].set_ylim([0, 0.8]);
ax[1].set_ylim([7, 18]);
ax[0].legend(); ax[1].legend();
100.0
Using single component code.

3. Resampling

If the Nyquist frequency is very high (i.e. the sampling time is very small), such that the log-spectrum goes to low values, you may want resample your time series, in order to reduce the maximum frequency to \(f^*\). Before performing that operation, the time series is automatically filtered to reduce the amount of aliasing introduced. Ideally you do not want to go too low in \(f^*\), because the statistical error may increase. In an intermediate region the results should not change.

To perform resampling you can choose the resampling frequency \(f^*\) (fstar_THz) or the resampling step (TSKIP). If you choose \(f^*\), the code will try to find the closest value allowed. The resulting \(\rm PSD\) can be visualized to ensure that the low-frequency region was not affected by this operations

[8]:
FSTAR_THZ = 14.0
jf, ax = j.resample(fstar_THz=FSTAR_THZ, plot=True, freq_units='thz')
plt.xlim([0, 20])
ax[1].set_ylim([7, 18]);
Using multicomponent code.
-----------------------------------------------------
  RESAMPLE TIME SERIES
-----------------------------------------------------
 Original Nyquist freq  f_Ny =     100.00000 THz
 Resampling freq          f* =      14.28571 THz
 Sampling time         TSKIP =             7 steps
                             =        35.000 fs
 Original  n. of frequencies =         10001
 Resampled n. of frequencies =          1429
 PSD      @cutoff  (pre-filter&sample) ~ 392810.55342
                  (post-filter&sample) ~ 367183.46153
 log(PSD) @cutoff  (pre-filter&sample) ~     12.70308
                  (post-filter&sample) ~     12.42357
 min(PSD)          (pre-filter&sample) =      0.31536
 min(PSD)         (post-filter&sample) =  22166.11934
 % of original PSD Power f<f* (pre-filter&sample)  = 96.679 %
-----------------------------------------------------

[9]:
ax = jf.plot_periodogram(PSD_FILTER_W=0.1)
ax[1].set_ylim([7, 18]);

4. Cepstral Analysis

Perform Cepstral Analysis. The code will perform the following operations: 1. compute the parameters describing the theoretical distribution of the \(\rm PSD\) 2. compute the Cepstral coefficients by Fourier transforming the \(\log(\rm{PSD})\) 3. apply the Akaike Information Criterion, to choose the number of cepstral coefficients \(P^*\) 4. return the resulting transport coefficient (thermal conductivity) \(\kappa\)

[10]:
jf.cepstral_analysis()
-----------------------------------------------------
  CEPSTRAL ANALYSIS
-----------------------------------------------------
  cutoffK = (P*-1) = 3  (auto, AIC_Kmin = 3, corr_factor =  1.0)
  L_0*   =          15.158757 +/-   0.056227
  S_0*   =     6824108.702608 +/- 383697.095268
-----------------------------------------------------
  kappa* =           0.498310 +/-   0.028018  W/m/K
-----------------------------------------------------

[11]:
# Cepstral Coefficients
print('c_k = ', jf.cepf.logpsdK)

ax = jf.plot_ck()
ax.set_xlim([0, 25])
ax.set_ylim([-0.2, 1.0])
ax.grid();
c_k =  [ 1.43138519e+01  6.58383813e-01 -6.48049965e-03 ...  4.76081379e-03
  2.28241054e-02  3.95167451e-02]
[12]:
# AIC function
f = plt.figure()
plt.plot(jf.cepf.aic, '.-', c=c[0])
plt.xlim([0, 50])
plt.ylim([1400, 1500]);

print('K of AIC_min = {:d}'.format(jf.cepf.aic_Kmin))
print('AIC_min = {:f}'.format(jf.cepf.aic_min))
K of AIC_min = 3
AIC_min = 1410.555757

Plot the thermal conductivity \(\kappa\) as a function of the cutoff \(P^*\)

[13]:
# L_0 as a function of cutoff K
ax = jf.plot_L0_Pstar()
ax.set_xlim([0, 50])
ax.set_ylim([14.75, 16.5]);

print('K of AIC_min = {:d}'.format(jf.cepf.aic_Kmin))
print('AIC_min = {:f}'.format(jf.cepf.aic_min))
K of AIC_min = 3
AIC_min = 1410.555757
[14]:
# kappa as a function of cutoff K
ax = jf.plot_kappa_Pstar()
ax.set_xlim([0, 50])
ax.set_ylim([0, 1.0]);

print('K of AIC_min = {:d}'.format(jf.cepf.aic_Kmin))
print('AIC_min = {:f}'.format(jf.cepf.aic_min))
K of AIC_min = 3
AIC_min = 1410.555757

Print the results :)

[15]:
results = jf.cepstral_log
print(results)
-----------------------------------------------------
  CEPSTRAL ANALYSIS
-----------------------------------------------------
  cutoffK = (P*-1) = 3  (auto, AIC_Kmin = 3, corr_factor =  1.0)
  L_0*   =          15.158757 +/-   0.056227
  S_0*   =     6824108.702608 +/- 383697.095268
-----------------------------------------------------
  kappa* =           0.498310 +/-   0.028018  W/m/K
-----------------------------------------------------

You can now visualize the filtered PSD…

[16]:
# filtered log-PSD
ax = j.plot_periodogram(0.5, kappa_units=True)
ax = jf.plot_periodogram(0.5, axes=ax, kappa_units=True)
ax = jf.plot_cepstral_spectrum(axes=ax, kappa_units=True)
ax[0].axvline(x = jf.Nyquist_f_THz, ls='--', c='r')
ax[1].axvline(x = jf.Nyquist_f_THz, ls='--', c='r')
plt.xlim([0, 20])
ax[0].set_ylim([0, 0.8]);
ax[1].set_ylim([7, 18]);
ax[0].legend(['original', 'resampled', 'cepstrum-filtered'])
ax[1].legend(['original', 'resampled', 'cepstrum-filtered']);
[ ]: