bfee2 3.1.1__tar.gz → 3.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bfee2-3.2.1/BFEE2/commonTools/ploter.py +757 -0
- bfee2-3.2.1/BFEE2/gui.py +4186 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/postTreatment.py +242 -4
- bfee2-3.2.1/BFEE2/skills.py +335 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/configTemplate.py +8 -9
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/updateCenters.py +7 -7
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/rags.py +110 -51
- bfee2-3.2.1/BFEE2/version.py +4 -0
- bfee2-3.2.1/PKG-INFO +100 -0
- bfee2-3.2.1/README.md +76 -0
- bfee2-3.2.1/bfee2.egg-info/PKG-INFO +100 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/bfee2.egg-info/SOURCES.txt +1 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/bfee2.egg-info/requires.txt +1 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/setup.cfg +1 -0
- bfee2-3.1.1/BFEE2/commonTools/ploter.py +0 -218
- bfee2-3.1.1/BFEE2/gui.py +0 -2785
- bfee2-3.1.1/BFEE2/version.py +0 -4
- bfee2-3.1.1/PKG-INFO +0 -85
- bfee2-3.1.1/README.md +0 -62
- bfee2-3.1.1/bfee2.egg-info/PKG-INFO +0 -85
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/__init__.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/commonTools/__init__.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/commonTools/commonSlots.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/commonTools/fileParser.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/doc/Doc.pdf +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/doc/__init__.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/inputGenerator.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/000.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/000.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/000.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/001.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/001.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/001.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/001.readme.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/002.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/002.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/002.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/003.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/003.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/003.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/004.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/004.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/004.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/005.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/005.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/005.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/006.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/006.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/006.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007_eq.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007_eq.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007_min.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008.mdp.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008_eq.colvars.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008_eq.generate_tpr_sh.template +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/BFEEGromacs.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/__init__.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/find_min_max.awk +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/__init__.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/fep.tcl +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/fep_lddm.tcl +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/scriptTemplate.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/solvate.tcl +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/solvate_mem.tcl +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/Readme_Gromacs_Geometrical.txt +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/Readme_NAMD_Alchemical.txt +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/Readme_NAMD_Geometrical.txt +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/__init__.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/third_party/__init__.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/third_party/py_bar.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/LICENSE +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/bfee2.egg-info/dependency_links.txt +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/bfee2.egg-info/top_level.txt +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/bin/BFEE2Gui.py +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/pyproject.toml +0 -0
- {bfee2-3.1.1 → bfee2-3.2.1}/setup.py +0 -0
|
@@ -0,0 +1,757 @@
|
|
|
1
|
+
# plot figures
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import pathlib
|
|
6
|
+
|
|
7
|
+
import matplotlib
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
import numpy as np
|
|
10
|
+
from scipy import interpolate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# an runtime error
|
|
14
|
+
# does not have corresponding correction for a pmf
|
|
15
|
+
class NoCorrectionFileError(RuntimeError):
|
|
16
|
+
def __init__(self, arg):
|
|
17
|
+
self.args = arg
|
|
18
|
+
|
|
19
|
+
def isGaWTM(pmfFiles):
|
|
20
|
+
"""determine whether the input PMFs indicate Ga-WTM simulations
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
pmfFiles (list[str]): path to a set of PMFs (and pmf corrections)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
bool: GaWTM simulation or not
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
for file in pmfFiles:
|
|
30
|
+
fileName = pathlib.Path(file).name
|
|
31
|
+
if fileName.endswith('.reweightamd1.cumulant.pmf') or \
|
|
32
|
+
fileName.endswith('.reweightamd1.reweight.pmf'):
|
|
33
|
+
return True
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
def getGaWTMBaseName(filePath):
|
|
37
|
+
"""Extract the base name from a GaWTM PMF file or its correction file.
|
|
38
|
+
|
|
39
|
+
For example:
|
|
40
|
+
'path/to/001.czar.pmf' -> '001'
|
|
41
|
+
'path/to/001.reweightamd1.cumulant.pmf' -> '001'
|
|
42
|
+
'path/to/step1.czar.pmf' -> 'step1'
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
filePath (str): path to the file
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
str: base name of the file (without extensions)
|
|
49
|
+
"""
|
|
50
|
+
fileName = pathlib.Path(filePath).name
|
|
51
|
+
# Remove known GaWTM suffixes
|
|
52
|
+
if fileName.endswith('.reweightamd1.cumulant.pmf'):
|
|
53
|
+
return fileName[:-len('.reweightamd1.cumulant.pmf')]
|
|
54
|
+
elif fileName.endswith('.czar.pmf'):
|
|
55
|
+
return fileName[:-len('.czar.pmf')]
|
|
56
|
+
else:
|
|
57
|
+
# For other PMF files, just remove .pmf extension
|
|
58
|
+
return pathlib.Path(fileName).stem
|
|
59
|
+
|
|
60
|
+
def getGaWTMBaseNames(filePath):
|
|
61
|
+
"""Extract all possible base names from a GaWTM PMF file for flexible matching.
|
|
62
|
+
|
|
63
|
+
This function returns a list of candidate base names to support flexible pairing.
|
|
64
|
+
For example::
|
|
65
|
+
|
|
66
|
+
'path/to/abf_1.abf1.czar.pmf' -> ['abf_1.abf1', 'abf_1']
|
|
67
|
+
'path/to/001.czar.pmf' -> ['001']
|
|
68
|
+
'path/to/step1.abf2.czar.pmf' -> ['step1.abf2', 'step1']
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
filePath (str): path to the file
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
list[str]: list of possible base names (primary first, then alternatives)
|
|
75
|
+
"""
|
|
76
|
+
primaryBaseName = getGaWTMBaseName(filePath)
|
|
77
|
+
candidates = [primaryBaseName]
|
|
78
|
+
|
|
79
|
+
# Check if the base name contains patterns like .abf1, .abf2, etc.
|
|
80
|
+
# Find the last dot and check if it's followed by 'abf' + digits
|
|
81
|
+
dotIndex = primaryBaseName.rfind('.')
|
|
82
|
+
if dotIndex > 0:
|
|
83
|
+
suffix = primaryBaseName[dotIndex + 1:]
|
|
84
|
+
if suffix.startswith('abf') and suffix[3:].isdigit():
|
|
85
|
+
alternativeBaseName = primaryBaseName[:dotIndex]
|
|
86
|
+
if alternativeBaseName not in candidates:
|
|
87
|
+
candidates.append(alternativeBaseName)
|
|
88
|
+
|
|
89
|
+
return candidates
|
|
90
|
+
|
|
91
|
+
def pairGaWTMFiles(pmfFiles):
|
|
92
|
+
"""Pair GaWTM PMF files with their corresponding correction files.
|
|
93
|
+
|
|
94
|
+
This function looks for files with matching base names:
|
|
95
|
+
- xxx.czar.pmf paired with xxx.reweightamd1.cumulant.pmf
|
|
96
|
+
|
|
97
|
+
Files without a corresponding correction file are returned as unpaired.
|
|
98
|
+
Orphan correction files (without matching czar.pmf) are also returned separately.
|
|
99
|
+
Wrong correction files (.reweightamd1.reweight.pmf instead of .cumulant.pmf) are also detected.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
pmfFiles (list[str]): list of all PMF file paths (including correction files)
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
tuple: (paired_list, unpaired_czar_list, orphan_correction_list, other_files, wrong_correction_files)
|
|
106
|
+
paired_list: list of tuples (pmf_file_path, correction_file_path)
|
|
107
|
+
unpaired_czar_list: list of czar.pmf file paths without correction
|
|
108
|
+
orphan_correction_list: list of correction file paths without matching czar.pmf
|
|
109
|
+
other_files: list of other PMF files
|
|
110
|
+
wrong_correction_files: list of .reweightamd1.reweight.pmf files (wrong type)
|
|
111
|
+
"""
|
|
112
|
+
# Separate czar.pmf files and correction files
|
|
113
|
+
czar_files = {} # baseName -> filePath
|
|
114
|
+
czar_file_candidates = {} # baseName -> list of candidate base names for matching
|
|
115
|
+
correction_files = {} # baseName -> filePath
|
|
116
|
+
other_files = [] # files that are neither
|
|
117
|
+
wrong_correction_files = [] # .reweightamd1.reweight.pmf files (wrong type)
|
|
118
|
+
|
|
119
|
+
for filePath in pmfFiles:
|
|
120
|
+
fileName = pathlib.Path(filePath).name
|
|
121
|
+
if fileName.endswith('.reweightamd1.cumulant.pmf'):
|
|
122
|
+
baseName = getGaWTMBaseName(filePath)
|
|
123
|
+
correction_files[baseName] = filePath
|
|
124
|
+
elif fileName.endswith('.reweightamd1.reweight.pmf'):
|
|
125
|
+
# Wrong correction file type
|
|
126
|
+
wrong_correction_files.append(filePath)
|
|
127
|
+
elif fileName.endswith('.czar.pmf'):
|
|
128
|
+
baseName = getGaWTMBaseName(filePath)
|
|
129
|
+
czar_files[baseName] = filePath
|
|
130
|
+
czar_file_candidates[baseName] = getGaWTMBaseNames(filePath)
|
|
131
|
+
else:
|
|
132
|
+
other_files.append(filePath)
|
|
133
|
+
|
|
134
|
+
# Pair files by base name (with flexible matching)
|
|
135
|
+
paired = []
|
|
136
|
+
unpaired_czar = []
|
|
137
|
+
matched_corrections = set() # Track which correction files have been matched
|
|
138
|
+
|
|
139
|
+
for baseName, czarPath in czar_files.items():
|
|
140
|
+
# Try all candidate base names for this czar file
|
|
141
|
+
candidates = czar_file_candidates.get(baseName, [baseName])
|
|
142
|
+
matched = False
|
|
143
|
+
for candidate in candidates:
|
|
144
|
+
if candidate in correction_files:
|
|
145
|
+
paired.append((czarPath, correction_files[candidate]))
|
|
146
|
+
matched_corrections.add(candidate)
|
|
147
|
+
matched = True
|
|
148
|
+
break
|
|
149
|
+
if not matched:
|
|
150
|
+
unpaired_czar.append(czarPath)
|
|
151
|
+
|
|
152
|
+
# Find orphan correction files (correction without czar.pmf)
|
|
153
|
+
orphan_corrections = []
|
|
154
|
+
for baseName, corrPath in correction_files.items():
|
|
155
|
+
if baseName not in matched_corrections:
|
|
156
|
+
orphan_corrections.append(corrPath)
|
|
157
|
+
|
|
158
|
+
return paired, unpaired_czar, orphan_corrections, other_files, wrong_correction_files
|
|
159
|
+
|
|
160
|
+
def correctGaWTM(pmfFile, correctionFile=None):
|
|
161
|
+
"""read a 1D namd PMF file and correct it using cumulant.pmf file
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
pmfFile (str): path to the pmf File
|
|
165
|
+
correctionFile (str, optional): path to the correction file.
|
|
166
|
+
If None, will try to find it in the same directory (legacy behavior).
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
np.array (N*2): 1D PMF
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
pmf = np.loadtxt(pmfFile)
|
|
173
|
+
|
|
174
|
+
# If correction file is not provided, try to find it (legacy behavior)
|
|
175
|
+
if correctionFile is None:
|
|
176
|
+
correctionFile = pmfFile.replace('.czar.pmf', '') + '.reweightamd1.cumulant.pmf'
|
|
177
|
+
|
|
178
|
+
if not os.path.exists(correctionFile):
|
|
179
|
+
raise NoCorrectionFileError(f'{pmfFile} does not have a corresponding correction!')
|
|
180
|
+
|
|
181
|
+
correction_data = np.loadtxt(correctionFile)
|
|
182
|
+
correction_interpolate = interpolate.interp1d(correction_data[:,0], correction_data[:,1], fill_value="extrapolate")
|
|
183
|
+
|
|
184
|
+
pmf[:,1] += correction_interpolate(pmf[:,0])
|
|
185
|
+
|
|
186
|
+
return pmf
|
|
187
|
+
|
|
188
|
+
def readPMF(pmfFile):
|
|
189
|
+
"""read a 1D namd PMF file
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
pmfFile (str): path to the pmf File
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
np.array (N*2): 1D PMF
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
return np.loadtxt(pmfFile)
|
|
199
|
+
|
|
200
|
+
def mergePMF(pmfFiles):
|
|
201
|
+
"""merge several PMF files
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
pmfFiles (list of np.arrays): list of 1D pmfs
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
np.array (N*2): merged PMF if the PMFs overlap, pmfFiles[0] otherwise
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
numPmfs = len(pmfFiles)
|
|
211
|
+
assert(numPmfs > 0)
|
|
212
|
+
|
|
213
|
+
# sort pmfs
|
|
214
|
+
pmfSort = [i for i in range(numPmfs)]
|
|
215
|
+
pmfSort.sort(key=lambda x: pmfFiles[x][0][0])
|
|
216
|
+
|
|
217
|
+
finalPMF = pmfFiles[pmfSort[0]]
|
|
218
|
+
|
|
219
|
+
if len(pmfFiles) > 1:
|
|
220
|
+
for i in range(1, len(pmfFiles)):
|
|
221
|
+
for j in range(len(finalPMF)):
|
|
222
|
+
if finalPMF[j][0] == pmfFiles[pmfSort[i]][0][0]:
|
|
223
|
+
# overlapped region
|
|
224
|
+
avgDifference = np.average(finalPMF[j:,1:] - pmfFiles[pmfSort[i]][0:len(finalPMF)-j,1:])
|
|
225
|
+
pmfFiles[pmfSort[i]][:,1:] += avgDifference
|
|
226
|
+
finalPMF[j:,1:] = (finalPMF[j:,1:] + pmfFiles[pmfSort[i]][0:len(finalPMF)-j,1:]) / 2
|
|
227
|
+
# other region
|
|
228
|
+
finalPMF = np.append(finalPMF, pmfFiles[pmfSort[i]][len(finalPMF)-j:], axis=0)
|
|
229
|
+
break
|
|
230
|
+
|
|
231
|
+
finalPMF[:,1] -= finalPMF[:,1].min()
|
|
232
|
+
|
|
233
|
+
return finalPMF
|
|
234
|
+
|
|
235
|
+
def writePMF(pmfFile, pmf):
|
|
236
|
+
"""write a 1D namd PMF file
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
pmfFile (str): path to the pmf File
|
|
240
|
+
pmf (np.array, N*2): pmf to be written
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
np.savetxt(pmfFile, pmf, fmt='%g')
|
|
244
|
+
|
|
245
|
+
# ============== History PMF functions ==============
|
|
246
|
+
|
|
247
|
+
def isGaWTMHist(pmfFiles):
|
|
248
|
+
"""determine whether the input History PMFs indicate Ga-WTM simulations
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
pmfFiles (list[str]): path to a set of History PMFs (and pmf corrections)
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
bool: GaWTM simulation or not
|
|
255
|
+
"""
|
|
256
|
+
for file in pmfFiles:
|
|
257
|
+
fileName = pathlib.Path(file).name
|
|
258
|
+
# Check for correction files (correct or wrong type)
|
|
259
|
+
if fileName.endswith('.reweightamd1.cumulant.hist.pmf') or \
|
|
260
|
+
fileName.endswith('.reweightamd1.cumulant.pmf') or \
|
|
261
|
+
fileName.endswith('.reweightamd1.reweight.hist.pmf') or \
|
|
262
|
+
fileName.endswith('.reweightamd1.reweight.pmf'):
|
|
263
|
+
return True
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
def getGaWTMHistBaseName(filePath):
|
|
267
|
+
"""Extract the base name from a GaWTM History PMF file or its correction file.
|
|
268
|
+
|
|
269
|
+
For example:
|
|
270
|
+
'path/to/001.hist.czar.pmf' -> '001'
|
|
271
|
+
'path/to/001.reweightamd1.cumulant.hist.pmf' -> '001'
|
|
272
|
+
'path/to/001.reweightamd1.cumulant.pmf' -> '001'
|
|
273
|
+
'path/to/step1.hist.czar.pmf' -> 'step1'
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
filePath (str): path to the file
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
str: base name of the file (without extensions)
|
|
280
|
+
"""
|
|
281
|
+
fileName = pathlib.Path(filePath).name
|
|
282
|
+
# Remove known GaWTM history suffixes (order matters - check longer first)
|
|
283
|
+
if fileName.endswith('.reweightamd1.cumulant.hist.pmf'):
|
|
284
|
+
return fileName[:-len('.reweightamd1.cumulant.hist.pmf')]
|
|
285
|
+
elif fileName.endswith('.reweightamd1.cumulant.pmf'):
|
|
286
|
+
return fileName[:-len('.reweightamd1.cumulant.pmf')]
|
|
287
|
+
elif fileName.endswith('.hist.czar.pmf'):
|
|
288
|
+
return fileName[:-len('.hist.czar.pmf')]
|
|
289
|
+
else:
|
|
290
|
+
# For other PMF files, try to remove common extensions
|
|
291
|
+
if fileName.endswith('.hist.pmf'):
|
|
292
|
+
return fileName[:-len('.hist.pmf')]
|
|
293
|
+
return pathlib.Path(fileName).stem
|
|
294
|
+
|
|
295
|
+
def getGaWTMHistBaseNames(filePath):
|
|
296
|
+
"""Extract all possible base names from a GaWTM History PMF file for flexible matching.
|
|
297
|
+
|
|
298
|
+
This function returns a list of candidate base names to support flexible pairing.
|
|
299
|
+
For example::
|
|
300
|
+
|
|
301
|
+
'path/to/abf_1.abf1.hist.czar.pmf' -> ['abf_1.abf1', 'abf_1']
|
|
302
|
+
'path/to/001.hist.czar.pmf' -> ['001']
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
filePath (str): path to the file
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
list[str]: list of possible base names (primary first, then alternatives)
|
|
309
|
+
"""
|
|
310
|
+
primaryBaseName = getGaWTMHistBaseName(filePath)
|
|
311
|
+
candidates = [primaryBaseName]
|
|
312
|
+
|
|
313
|
+
# Check if the base name contains patterns like .abf1, .abf2, etc.
|
|
314
|
+
# Find the last dot and check if it's followed by 'abf' + digits
|
|
315
|
+
dotIndex = primaryBaseName.rfind('.')
|
|
316
|
+
if dotIndex > 0:
|
|
317
|
+
suffix = primaryBaseName[dotIndex + 1:]
|
|
318
|
+
if suffix.startswith('abf') and suffix[3:].isdigit():
|
|
319
|
+
alternativeBaseName = primaryBaseName[:dotIndex]
|
|
320
|
+
if alternativeBaseName not in candidates:
|
|
321
|
+
candidates.append(alternativeBaseName)
|
|
322
|
+
|
|
323
|
+
return candidates
|
|
324
|
+
|
|
325
|
+
def pairGaWTMHistFiles(pmfFiles):
|
|
326
|
+
"""Pair GaWTM History PMF files with their corresponding correction files.
|
|
327
|
+
|
|
328
|
+
This function looks for files with matching base names:
|
|
329
|
+
- xxx.hist.czar.pmf paired with xxx.reweightamd1.cumulant.hist.pmf or xxx.reweightamd1.cumulant.pmf
|
|
330
|
+
|
|
331
|
+
Files without a corresponding correction file are returned as unpaired.
|
|
332
|
+
Orphan correction files (without matching hist.czar.pmf) are also returned separately.
|
|
333
|
+
Wrong correction files (.reweightamd1.reweight.pmf instead of .cumulant.pmf) are also detected.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
pmfFiles (list[str]): list of all History PMF file paths (including correction files)
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
tuple: (paired_list, unpaired_czar_list, orphan_correction_list, other_files, wrong_correction_files)
|
|
340
|
+
|
|
341
|
+
``paired_list`` is a list of tuples
|
|
342
|
+
``(pmf_file_path, correction_file_path, is_hist_correction)``.
|
|
343
|
+
``is_hist_correction`` is True if correction is .hist.pmf and False if
|
|
344
|
+
it is a single-frame .pmf. Other returned lists contain unpaired
|
|
345
|
+
hist.czar.pmf files, orphan correction files, other PMF files, and
|
|
346
|
+
wrong .reweightamd1.reweight.pmf correction files.
|
|
347
|
+
"""
|
|
348
|
+
czar_files = {} # baseName -> filePath
|
|
349
|
+
czar_file_candidates = {} # baseName -> list of candidate base names for matching
|
|
350
|
+
correction_files = {} # baseName -> (filePath, is_hist_correction)
|
|
351
|
+
other_files = []
|
|
352
|
+
wrong_correction_files = [] # .reweightamd1.reweight.pmf files (wrong type)
|
|
353
|
+
|
|
354
|
+
for filePath in pmfFiles:
|
|
355
|
+
fileName = pathlib.Path(filePath).name
|
|
356
|
+
if fileName.endswith('.reweightamd1.cumulant.hist.pmf'):
|
|
357
|
+
baseName = getGaWTMHistBaseName(filePath)
|
|
358
|
+
correction_files[baseName] = (filePath, True)
|
|
359
|
+
elif fileName.endswith('.reweightamd1.cumulant.pmf'):
|
|
360
|
+
baseName = getGaWTMHistBaseName(filePath)
|
|
361
|
+
# Only add if not already have a hist correction (prefer hist over single-frame)
|
|
362
|
+
if baseName not in correction_files:
|
|
363
|
+
correction_files[baseName] = (filePath, False)
|
|
364
|
+
elif fileName.endswith('.reweightamd1.reweight.hist.pmf') or \
|
|
365
|
+
fileName.endswith('.reweightamd1.reweight.pmf'):
|
|
366
|
+
# Wrong correction file type
|
|
367
|
+
wrong_correction_files.append(filePath)
|
|
368
|
+
elif fileName.endswith('.hist.czar.pmf'):
|
|
369
|
+
baseName = getGaWTMHistBaseName(filePath)
|
|
370
|
+
czar_files[baseName] = filePath
|
|
371
|
+
czar_file_candidates[baseName] = getGaWTMHistBaseNames(filePath)
|
|
372
|
+
else:
|
|
373
|
+
other_files.append(filePath)
|
|
374
|
+
|
|
375
|
+
# Pair files by base name (with flexible matching)
|
|
376
|
+
paired = []
|
|
377
|
+
unpaired_czar = []
|
|
378
|
+
matched_corrections = set() # Track which correction files have been matched
|
|
379
|
+
|
|
380
|
+
for baseName, czarPath in czar_files.items():
|
|
381
|
+
# Try all candidate base names for this czar file
|
|
382
|
+
candidates = czar_file_candidates.get(baseName, [baseName])
|
|
383
|
+
matched = False
|
|
384
|
+
for candidate in candidates:
|
|
385
|
+
if candidate in correction_files:
|
|
386
|
+
corrPath, is_hist = correction_files[candidate]
|
|
387
|
+
paired.append((czarPath, corrPath, is_hist))
|
|
388
|
+
matched_corrections.add(candidate)
|
|
389
|
+
matched = True
|
|
390
|
+
break
|
|
391
|
+
if not matched:
|
|
392
|
+
unpaired_czar.append(czarPath)
|
|
393
|
+
|
|
394
|
+
orphan_corrections = []
|
|
395
|
+
for baseName, (corrPath, _) in correction_files.items():
|
|
396
|
+
if baseName not in matched_corrections:
|
|
397
|
+
orphan_corrections.append(corrPath)
|
|
398
|
+
|
|
399
|
+
return paired, unpaired_czar, orphan_corrections, other_files, wrong_correction_files
|
|
400
|
+
|
|
401
|
+
def readHistPMF(histPmfFile):
|
|
402
|
+
"""Read a History PMF file and return a list of PMF frames.
|
|
403
|
+
|
|
404
|
+
Each frame is a 2D numpy array with shape (N, 2) where N is the number of points.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
histPmfFile (str): path to the history PMF file
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
list[np.array]: list of PMF frames, each as (N, 2) array
|
|
411
|
+
"""
|
|
412
|
+
frames = []
|
|
413
|
+
current_frame = []
|
|
414
|
+
|
|
415
|
+
with open(histPmfFile, 'r') as f:
|
|
416
|
+
for line in f:
|
|
417
|
+
stripped = line.strip()
|
|
418
|
+
|
|
419
|
+
# Skip empty lines
|
|
420
|
+
if not stripped:
|
|
421
|
+
# If we have accumulated data, save the frame
|
|
422
|
+
if current_frame:
|
|
423
|
+
frames.append(np.array(current_frame))
|
|
424
|
+
current_frame = []
|
|
425
|
+
continue
|
|
426
|
+
|
|
427
|
+
# Skip comment/header lines
|
|
428
|
+
if stripped.startswith('#'):
|
|
429
|
+
continue
|
|
430
|
+
|
|
431
|
+
# Parse data line
|
|
432
|
+
parts = stripped.split()
|
|
433
|
+
if len(parts) >= 2:
|
|
434
|
+
try:
|
|
435
|
+
x = float(parts[0])
|
|
436
|
+
y = float(parts[1])
|
|
437
|
+
current_frame.append([x, y])
|
|
438
|
+
except ValueError:
|
|
439
|
+
continue
|
|
440
|
+
|
|
441
|
+
# Don't forget the last frame if file doesn't end with empty line
|
|
442
|
+
if current_frame:
|
|
443
|
+
frames.append(np.array(current_frame))
|
|
444
|
+
|
|
445
|
+
return frames
|
|
446
|
+
|
|
447
|
+
def correctGaWTMHist(histPmfFile, correctionFile, is_hist_correction=True):
|
|
448
|
+
"""Apply GaWTM correction to a History PMF file.
|
|
449
|
+
|
|
450
|
+
Args:
|
|
451
|
+
histPmfFile (str): path to the history PMF file (.hist.czar.pmf)
|
|
452
|
+
correctionFile (str): path to the correction file
|
|
453
|
+
is_hist_correction (bool): True if correction file is history format (.hist.pmf),
|
|
454
|
+
False if single-frame format (.pmf)
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
list[np.array]: list of corrected PMF frames
|
|
458
|
+
"""
|
|
459
|
+
pmf_frames = readHistPMF(histPmfFile)
|
|
460
|
+
|
|
461
|
+
if not os.path.exists(correctionFile):
|
|
462
|
+
raise NoCorrectionFileError(f'{histPmfFile} does not have a corresponding correction!')
|
|
463
|
+
|
|
464
|
+
if is_hist_correction:
|
|
465
|
+
# History correction file - apply frame by frame
|
|
466
|
+
correction_frames = readHistPMF(correctionFile)
|
|
467
|
+
|
|
468
|
+
# Interpolate correction frames if needed to match PMF frames
|
|
469
|
+
if len(correction_frames) != len(pmf_frames):
|
|
470
|
+
correction_frames = interpolateHistPMFFrames([correction_frames], len(pmf_frames))[0]
|
|
471
|
+
|
|
472
|
+
corrected_frames = []
|
|
473
|
+
for i, pmf in enumerate(pmf_frames):
|
|
474
|
+
correction = correction_frames[i]
|
|
475
|
+
correction_interp = interpolate.interp1d(
|
|
476
|
+
correction[:, 0], correction[:, 1], fill_value="extrapolate"
|
|
477
|
+
)
|
|
478
|
+
corrected = pmf.copy()
|
|
479
|
+
corrected[:, 1] += correction_interp(pmf[:, 0])
|
|
480
|
+
corrected_frames.append(corrected)
|
|
481
|
+
|
|
482
|
+
return corrected_frames
|
|
483
|
+
else:
|
|
484
|
+
# Single-frame correction file - apply same correction to all frames
|
|
485
|
+
correction_data = np.loadtxt(correctionFile)
|
|
486
|
+
correction_interp = interpolate.interp1d(
|
|
487
|
+
correction_data[:, 0], correction_data[:, 1], fill_value="extrapolate"
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
corrected_frames = []
|
|
491
|
+
for pmf in pmf_frames:
|
|
492
|
+
corrected = pmf.copy()
|
|
493
|
+
corrected[:, 1] += correction_interp(pmf[:, 0])
|
|
494
|
+
corrected_frames.append(corrected)
|
|
495
|
+
|
|
496
|
+
return corrected_frames
|
|
497
|
+
|
|
498
|
+
def interpolateHistPMFFrames(all_hist_pmfs, target_num_frames):
|
|
499
|
+
"""Interpolate history PMF frames to a target number of frames.
|
|
500
|
+
|
|
501
|
+
Each PMF file may have different number of frames. This function interpolates
|
|
502
|
+
the frame values to achieve a uniform frame count across all files.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
all_hist_pmfs (list[list[np.array]]): list of history PMFs, each is a list of frames
|
|
506
|
+
target_num_frames (int): target number of frames to interpolate to
|
|
507
|
+
|
|
508
|
+
Returns:
|
|
509
|
+
list[list[np.array]]: interpolated history PMFs with uniform frame count
|
|
510
|
+
"""
|
|
511
|
+
interpolated = []
|
|
512
|
+
|
|
513
|
+
for hist_pmf in all_hist_pmfs:
|
|
514
|
+
num_frames = len(hist_pmf)
|
|
515
|
+
|
|
516
|
+
if num_frames == target_num_frames:
|
|
517
|
+
interpolated.append(hist_pmf)
|
|
518
|
+
continue
|
|
519
|
+
|
|
520
|
+
if num_frames == 0:
|
|
521
|
+
interpolated.append([])
|
|
522
|
+
continue
|
|
523
|
+
|
|
524
|
+
# Create interpolated frames
|
|
525
|
+
new_frames = []
|
|
526
|
+
for target_idx in range(target_num_frames):
|
|
527
|
+
# Calculate the source frame index (floating point)
|
|
528
|
+
source_idx = target_idx * (num_frames - 1) / (target_num_frames - 1) if target_num_frames > 1 else 0
|
|
529
|
+
|
|
530
|
+
# Get the two neighboring frames for interpolation
|
|
531
|
+
lower_idx = int(source_idx)
|
|
532
|
+
upper_idx = min(lower_idx + 1, num_frames - 1)
|
|
533
|
+
|
|
534
|
+
# Interpolation weight
|
|
535
|
+
weight = source_idx - lower_idx
|
|
536
|
+
|
|
537
|
+
lower_frame = hist_pmf[lower_idx]
|
|
538
|
+
upper_frame = hist_pmf[upper_idx]
|
|
539
|
+
|
|
540
|
+
if weight == 0 or lower_idx == upper_idx:
|
|
541
|
+
# No interpolation needed
|
|
542
|
+
new_frames.append(lower_frame.copy())
|
|
543
|
+
else:
|
|
544
|
+
# Interpolate y values (x values should be the same across frames)
|
|
545
|
+
# But to be safe, we interpolate upper_frame to lower_frame's x-coordinates
|
|
546
|
+
upper_interp = interpolate.interp1d(
|
|
547
|
+
upper_frame[:, 0], upper_frame[:, 1], fill_value="extrapolate"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
new_frame = lower_frame.copy()
|
|
551
|
+
new_frame[:, 1] = (1 - weight) * lower_frame[:, 1] + weight * upper_interp(lower_frame[:, 0])
|
|
552
|
+
new_frames.append(new_frame)
|
|
553
|
+
|
|
554
|
+
interpolated.append(new_frames)
|
|
555
|
+
|
|
556
|
+
return interpolated
|
|
557
|
+
|
|
558
|
+
def mergeHistPMF(all_hist_pmfs):
|
|
559
|
+
"""Merge multiple history PMF files frame-by-frame.
|
|
560
|
+
|
|
561
|
+
Each input is a list of frames (from different PMF windows).
|
|
562
|
+
The function first interpolates all to have the same number of frames,
|
|
563
|
+
then merges each frame using the standard mergePMF function.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
all_hist_pmfs (list[list[np.array]]): list of history PMFs,
|
|
567
|
+
each is a list of frames from one window
|
|
568
|
+
|
|
569
|
+
Returns:
|
|
570
|
+
list[np.array]: merged history PMF (list of merged frames)
|
|
571
|
+
"""
|
|
572
|
+
if not all_hist_pmfs:
|
|
573
|
+
return []
|
|
574
|
+
|
|
575
|
+
# Find the maximum number of frames
|
|
576
|
+
max_frames = max(len(hist_pmf) for hist_pmf in all_hist_pmfs)
|
|
577
|
+
|
|
578
|
+
if max_frames == 0:
|
|
579
|
+
return []
|
|
580
|
+
|
|
581
|
+
# Interpolate all to have the same number of frames
|
|
582
|
+
interpolated = interpolateHistPMFFrames(all_hist_pmfs, max_frames)
|
|
583
|
+
|
|
584
|
+
# Merge frame-by-frame
|
|
585
|
+
merged_frames = []
|
|
586
|
+
for frame_idx in range(max_frames):
|
|
587
|
+
# Collect the same frame from all windows
|
|
588
|
+
frames_to_merge = [hist_pmf[frame_idx] for hist_pmf in interpolated if hist_pmf]
|
|
589
|
+
|
|
590
|
+
if frames_to_merge:
|
|
591
|
+
merged = mergePMF(frames_to_merge)
|
|
592
|
+
merged_frames.append(merged)
|
|
593
|
+
|
|
594
|
+
return merged_frames
|
|
595
|
+
|
|
596
|
+
def writeHistPMF(histPmfFile, frames):
|
|
597
|
+
"""Write a History PMF file.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
histPmfFile (str): path to the output history PMF file
|
|
601
|
+
frames (list[np.array]): list of PMF frames to write
|
|
602
|
+
"""
|
|
603
|
+
with open(histPmfFile, 'w') as f:
|
|
604
|
+
for frame_idx, frame in enumerate(frames):
|
|
605
|
+
# Write frame header (similar to original format)
|
|
606
|
+
f.write(f'# 1\n')
|
|
607
|
+
if len(frame) > 0:
|
|
608
|
+
x_min = frame[0, 0]
|
|
609
|
+
x_max = frame[-1, 0]
|
|
610
|
+
dx = (x_max - x_min) / (len(frame) - 1) if len(frame) > 1 else 0.1
|
|
611
|
+
f.write(f'# {x_min:.14e} {dx:.14e} {len(frame)} 0\n')
|
|
612
|
+
f.write('\n')
|
|
613
|
+
|
|
614
|
+
# Write data
|
|
615
|
+
for row in frame:
|
|
616
|
+
f.write(f' {row[0]:.14e} {row[1]:.14e}\n')
|
|
617
|
+
|
|
618
|
+
# Add blank line between frames
|
|
619
|
+
f.write('\n')
|
|
620
|
+
|
|
621
|
+
def plotPMF(pmf):
|
|
622
|
+
"""plot a pmf
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
pmf (np.array, N*2): pmf to be plotted
|
|
626
|
+
"""
|
|
627
|
+
|
|
628
|
+
plt.plot(pmf[:,0], pmf[:,1])
|
|
629
|
+
plt.xlabel('Transition coordinate')
|
|
630
|
+
plt.ylabel('ΔG (kcal/mol)')
|
|
631
|
+
plt.show()
|
|
632
|
+
|
|
633
|
+
def plotHysteresis(forwardProfile, backwardProfile):
|
|
634
|
+
"""plot the profile describing the hysteresis between forward and backward
|
|
635
|
+
simulations
|
|
636
|
+
|
|
637
|
+
Args:
|
|
638
|
+
forwardProfile (np.array, N*2): forward free-energy profile to be plotted
|
|
639
|
+
backwardProfile (np.array, N*2): backward free-energy profile to be plotted
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
plt.plot(forwardProfile[:,0], forwardProfile[:,1], label='Forward')
|
|
643
|
+
plt.plot(backwardProfile[:,0], backwardProfile[:,1], label='Backward')
|
|
644
|
+
plt.xlabel('Lambda')
|
|
645
|
+
plt.ylabel('ΔG (kcal/mol)')
|
|
646
|
+
plt.legend()
|
|
647
|
+
plt.show()
|
|
648
|
+
|
|
649
|
+
def saveHysteresis(forwardProfile, backwardProfile, filePath):
|
|
650
|
+
"""save the hysteresis data to a text file
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
forwardProfile (np.array, N*2): forward free-energy profile data
|
|
654
|
+
backwardProfile (np.array, N*2): backward free-energy profile data
|
|
655
|
+
filePath (str): path to save the data file
|
|
656
|
+
"""
|
|
657
|
+
|
|
658
|
+
# Combine forward and backward data into a single array
|
|
659
|
+
# Format: Lambda, Forward_dG, Backward_dG
|
|
660
|
+
combined_data = np.column_stack([
|
|
661
|
+
forwardProfile[:,0],
|
|
662
|
+
forwardProfile[:,1],
|
|
663
|
+
backwardProfile[:,1]
|
|
664
|
+
])
|
|
665
|
+
header = 'Lambda\tForward_dG(kcal/mol)\tBackward_dG(kcal/mol)'
|
|
666
|
+
np.savetxt(filePath, combined_data, fmt='%g', header=header, delimiter='\t')
|
|
667
|
+
|
|
668
|
+
def calcRMSD(inputArray):
|
|
669
|
+
"""calculate RMSD of a np.array with respect to (0,0,0,...0)
|
|
670
|
+
|
|
671
|
+
Args:
|
|
672
|
+
inputArray (1D np.array): the input array
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
float: RMSD of a np.array with respect to (0,0,0,...0)
|
|
676
|
+
"""
|
|
677
|
+
|
|
678
|
+
sumG2 = sum(map(lambda x: x * x, inputArray))
|
|
679
|
+
return math.sqrt(sumG2 / len(inputArray))
|
|
680
|
+
|
|
681
|
+
def readFrame(input):
|
|
682
|
+
"""read a frame of Colvars hist file and calculate its RMSD with respect to zero array
|
|
683
|
+
|
|
684
|
+
Args:
|
|
685
|
+
input (python file object): input object
|
|
686
|
+
|
|
687
|
+
Returns:
|
|
688
|
+
float: RMSD with respect to zero array
|
|
689
|
+
"""
|
|
690
|
+
|
|
691
|
+
G = []
|
|
692
|
+
while True:
|
|
693
|
+
line = input.readline()
|
|
694
|
+
|
|
695
|
+
# end of file
|
|
696
|
+
if not line:
|
|
697
|
+
return False
|
|
698
|
+
|
|
699
|
+
splitedLine = line.strip().split()
|
|
700
|
+
if splitedLine == []:
|
|
701
|
+
if G == []:
|
|
702
|
+
continue
|
|
703
|
+
else:
|
|
704
|
+
break
|
|
705
|
+
if splitedLine[0].startswith('#'):
|
|
706
|
+
continue
|
|
707
|
+
|
|
708
|
+
G.append(float(splitedLine[1]))
|
|
709
|
+
|
|
710
|
+
if G != []:
|
|
711
|
+
return calcRMSD(G)
|
|
712
|
+
else:
|
|
713
|
+
return None
|
|
714
|
+
|
|
715
|
+
def parseHistFile(histPath):
|
|
716
|
+
"""parse a hist.czar.pmf file and return frame-RMSD list
|
|
717
|
+
|
|
718
|
+
Args:
|
|
719
|
+
histPath (str): path to a hist.czar.pmf file
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
1D np.array: time evolution of RMSD with respect to zero array
|
|
723
|
+
"""
|
|
724
|
+
|
|
725
|
+
rmsd = []
|
|
726
|
+
with open(histPath, 'r') as ifile:
|
|
727
|
+
while True:
|
|
728
|
+
rmsdPerFrame = readFrame(ifile)
|
|
729
|
+
if rmsdPerFrame is False:
|
|
730
|
+
break
|
|
731
|
+
rmsd.append(rmsdPerFrame)
|
|
732
|
+
return rmsd
|
|
733
|
+
|
|
734
|
+
def plotConvergence(rmsdList):
|
|
735
|
+
"""plot the time evolution of PMF rmsd
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
rmsdList (list or 1D np.array, float): time evolution of RMSD with respect to zero array
|
|
739
|
+
"""
|
|
740
|
+
|
|
741
|
+
plt.plot(range(1, len(rmsdList) + 1), rmsdList)
|
|
742
|
+
plt.xlabel('Frame')
|
|
743
|
+
plt.ylabel('RMSD (Colvars Unit)')
|
|
744
|
+
plt.show()
|
|
745
|
+
|
|
746
|
+
def saveConvergence(rmsdList, filePath):
|
|
747
|
+
"""save the PMF RMSD convergence data to a text file
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
rmsdList (list or 1D np.array, float): time evolution of RMSD with respect to zero array
|
|
751
|
+
filePath (str): path to save the data file
|
|
752
|
+
"""
|
|
753
|
+
|
|
754
|
+
frames = np.arange(1, len(rmsdList) + 1)
|
|
755
|
+
data = np.column_stack([frames, rmsdList])
|
|
756
|
+
header = 'Frame\tRMSD(Colvars_Unit)'
|
|
757
|
+
np.savetxt(filePath, data, fmt='%g', header=header, delimiter='\t')
|