Tutorial 8 - Transformation of 2D free energy surfaces
Introduction
This notebook illustrates the usage of ThermoLIB to transform a 2D free energy surface in terms of collective variables (CV1,CV2) towards a new FES in terms of new collective variables (Q1,Q2) in which the relation between (CV1,CV2) and (Q1,Q2) is given as a deterministic transformation formula. For the purpose of this illustration, we will chose a transformation that encodes a simply rotation in the (CV1,CV2) plane. Furthermore, to validate the implementation, we will also project the original FES on the CV1 axis and compare with the projection of the rotated FES on the rotated CV1 axis.
[5]:
%load_ext autoreload
%autoreload 2
The autoreload extension is already loaded. To reload it, use:
%reload_ext autoreload
[6]:
from thermolib.thermodynamics.fep import FreeEnergySurface2D, SimpleFreeEnergyProfile, plot_profiles
from thermolib.thermodynamics.histogram import Histogram2D
from thermolib.tools import read_wham_input, decorrelate
from thermolib.thermodynamics.trajectory import ColVarReader
from thermolib.error import Propagator
from thermolib.units import *
import numpy as np, matplotlib.pyplot as pp, time
Define some file/path variables
[7]:
fn_meta = '/home/lvduyfhu/hpc/data_vo/shared/massimo/for_Louis/H-ZSM-5_ethylation/wham_input_2D.txt' #location of the plumed metadata file containing all information of the umbrella sampling
Construction of original 2D FES
[8]:
colvar_reader = ColVarReader([1,2], units=['au','au'])
temp, biasses, trajectories = read_wham_input(
fn_meta, colvar_reader, 'colvars/colvar_%s.dat',
bias_potential='Parabola2D', q01_unit='au', q02_unit='au', kappa1_unit='kjmol', kappa2_unit='kjmol',
)
[9]:
corrtimes = decorrelate(trajectories, plot=True)
/home/lvduyfhu/miniconda3/envs/test/lib/python3.14/site-packages/thermolib/tools.py:960: OptimizeWarning: Covariance of the parameters could not be estimated
pars, pcovs = curve_fit(function, upper_envelope_indices, upper_envelope_values, **curve_fit_kwargs)
<Figure size 640x480 with 0 Axes>
Next, we construct the 2D probability histogram on the given 2D CV grid (defined by bins) using the WHAM routine.
[10]:
bins = [np.arange(1,4.025,0.025), np.arange(-2.5,0.025,0.025)]
hist = Histogram2D.from_wham(bins, trajectories, biasses, temp, error_estimate='mle_f_cov', corrtimes=corrtimes, error_p_threshold=1e-4)
hist_noerror = Histogram2D.from_wham(bins, trajectories, biasses, temp)
WARNING: could not converge WHAM equations to convergence of 1.000e-06 in 1000 steps!
SCF did not converge!
---------------------------------------------------------------------
TIMING SUMMARY
initializing: 00h 00m 00.000s
histograms : 00h 00m 00.022s
bias poten. : 00h 00m 00.730s
solve scf : 00h 00m 00.345s
error est. : 00h 00m 13.492s
TOTAL : 00h 00m 14.592s
---------------------------------------------------------------------
WARNING: could not converge WHAM equations to convergence of 1.000e-06 in 1000 steps!
SCF did not converge!
---------------------------------------------------------------------
TIMING SUMMARY
initializing: 00h 00m 00.000s
histograms : 00h 00m 00.017s
bias poten. : 00h 00m 00.765s
solve scf : 00h 00m 00.317s
error est. : 00h 00m 00.000s
TOTAL : 00h 00m 01.100s
---------------------------------------------------------------------
[11]:
fes = FreeEnergySurface2D.from_histogram(hist, temp)
fes_noerror = FreeEnergySurface2D.from_histogram(hist_noerror, temp)
fes.set_ref(ref='min')
fes_noerror.set_ref(ref='min')
fes.plot(obss=['value', 'error'], cmap='rainbow', flims=[0,140], ncolors=14)
<Figure size 640x480 with 0 Axes>
[12]:
fes.plot(slicer=[55,slice(None)], obss=['mean', 'sample'], linewidths=[2,1], linestyles=['-', '--'], colors=['b',None])
WARNING: multivariate normal sampling failed using Cholesky decomposition, switching to method eigh.
Transformation to new CVs
We transform
to
with:
- :nbsphinx-math:`begin{align}
Q_1 &= frac{sqrt{2}}{2}left(CV_1 - 2.75 + CV_2 + 1.25right) \ Q_2 &= frac{sqrt{2}}{2}left(CV_2 + 1.25 - CV_1 + 2.75right)
end{align}`
which simply encodes a rotation over 45 degrees around the point 
[13]:
f1 = lambda x,y: np.sqrt(2)/2*(x-2.75+y+1.25)
f2 = lambda x,y: np.sqrt(2)/2*(y+1.25-x+2.75)
derivatives = lambda x,y: ((np.sqrt(2)/2, np.sqrt(2)/2), (-np.sqrt(2)/2, np.sqrt(2)/2))
q1s = np.arange(-1.2,1.4+0.025,0.025)
q2s = np.arange(-1.0,1.0+0.025,0.025)
fes2 = fes.transform_function((f1,f2), q1s, q2s, derivatives=derivatives, cv1_label='Q1', cv2_label='Q2')
WARNING: multivariate normal sampling failed using Cholesky decomposition, switching to method eigh.
[14]:
fes2.plot(obss=['value', 'error'], cmap='rainbow', flims=[0,140], ncolors=14)
<Figure size 640x480 with 0 Axes>
Both the free energy itself as well as its error bar indeed seem to represent a rotated version of the original.
Projection comparison
As a further comparison, we project the original FES on CV1 as well as the new FES on
and compare on a 1D plot (with error bar).
[15]:
fep = fes.project_cv1()
WARNING: multivariate normal sampling failed using Cholesky decomposition, switching to method eigh.
[16]:
fun = lambda x,y: np.sqrt(2)/2*(x-y)+2.75
cv1s = np.arange(1,4.025,0.025)
fep2 = fes2.project_function(fun, cv1s)
WARNING: multivariate normal sampling failed using Cholesky decomposition, switching to method eigh.
[17]:
plot_profiles([fep,fep2], labels=['before trans','after trans'], linestyles=['-','--'], colors=['b','k'], flims=[-40,140])
<Figure size 640x480 with 0 Axes>
This plot clearly illustrates that both projections are the same, further validating the implementation.