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.
Files changed (81) hide show
  1. bfee2-3.2.1/BFEE2/commonTools/ploter.py +757 -0
  2. bfee2-3.2.1/BFEE2/gui.py +4186 -0
  3. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/postTreatment.py +242 -4
  4. bfee2-3.2.1/BFEE2/skills.py +335 -0
  5. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/configTemplate.py +8 -9
  6. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/updateCenters.py +7 -7
  7. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/rags.py +110 -51
  8. bfee2-3.2.1/BFEE2/version.py +4 -0
  9. bfee2-3.2.1/PKG-INFO +100 -0
  10. bfee2-3.2.1/README.md +76 -0
  11. bfee2-3.2.1/bfee2.egg-info/PKG-INFO +100 -0
  12. {bfee2-3.1.1 → bfee2-3.2.1}/bfee2.egg-info/SOURCES.txt +1 -0
  13. {bfee2-3.1.1 → bfee2-3.2.1}/bfee2.egg-info/requires.txt +1 -0
  14. {bfee2-3.1.1 → bfee2-3.2.1}/setup.cfg +1 -0
  15. bfee2-3.1.1/BFEE2/commonTools/ploter.py +0 -218
  16. bfee2-3.1.1/BFEE2/gui.py +0 -2785
  17. bfee2-3.1.1/BFEE2/version.py +0 -4
  18. bfee2-3.1.1/PKG-INFO +0 -85
  19. bfee2-3.1.1/README.md +0 -62
  20. bfee2-3.1.1/bfee2.egg-info/PKG-INFO +0 -85
  21. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/__init__.py +0 -0
  22. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/commonTools/__init__.py +0 -0
  23. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/commonTools/commonSlots.py +0 -0
  24. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/commonTools/fileParser.py +0 -0
  25. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/doc/Doc.pdf +0 -0
  26. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/doc/__init__.py +0 -0
  27. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/inputGenerator.py +0 -0
  28. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/000.colvars.template +0 -0
  29. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/000.generate_tpr_sh.template +0 -0
  30. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/000.mdp.template +0 -0
  31. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/001.colvars.template +0 -0
  32. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/001.generate_tpr_sh.template +0 -0
  33. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/001.mdp.template +0 -0
  34. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/001.readme.template +0 -0
  35. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/002.colvars.template +0 -0
  36. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/002.generate_tpr_sh.template +0 -0
  37. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/002.mdp.template +0 -0
  38. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/003.colvars.template +0 -0
  39. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/003.generate_tpr_sh.template +0 -0
  40. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/003.mdp.template +0 -0
  41. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/004.colvars.template +0 -0
  42. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/004.generate_tpr_sh.template +0 -0
  43. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/004.mdp.template +0 -0
  44. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/005.colvars.template +0 -0
  45. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/005.generate_tpr_sh.template +0 -0
  46. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/005.mdp.template +0 -0
  47. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/006.colvars.template +0 -0
  48. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/006.generate_tpr_sh.template +0 -0
  49. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/006.mdp.template +0 -0
  50. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007.colvars.template +0 -0
  51. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007.generate_tpr_sh.template +0 -0
  52. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007.mdp.template +0 -0
  53. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007_eq.colvars.template +0 -0
  54. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007_eq.generate_tpr_sh.template +0 -0
  55. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/007_min.mdp.template +0 -0
  56. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008.colvars.template +0 -0
  57. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008.generate_tpr_sh.template +0 -0
  58. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008.mdp.template +0 -0
  59. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008_eq.colvars.template +0 -0
  60. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/008_eq.generate_tpr_sh.template +0 -0
  61. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/BFEEGromacs.py +0 -0
  62. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/__init__.py +0 -0
  63. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_gromacs/find_min_max.awk +0 -0
  64. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/__init__.py +0 -0
  65. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/fep.tcl +0 -0
  66. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/fep_lddm.tcl +0 -0
  67. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/scriptTemplate.py +0 -0
  68. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/solvate.tcl +0 -0
  69. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_namd/solvate_mem.tcl +0 -0
  70. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/Readme_Gromacs_Geometrical.txt +0 -0
  71. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/Readme_NAMD_Alchemical.txt +0 -0
  72. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/Readme_NAMD_Geometrical.txt +0 -0
  73. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/templates_readme/__init__.py +0 -0
  74. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/third_party/__init__.py +0 -0
  75. {bfee2-3.1.1 → bfee2-3.2.1}/BFEE2/third_party/py_bar.py +0 -0
  76. {bfee2-3.1.1 → bfee2-3.2.1}/LICENSE +0 -0
  77. {bfee2-3.1.1 → bfee2-3.2.1}/bfee2.egg-info/dependency_links.txt +0 -0
  78. {bfee2-3.1.1 → bfee2-3.2.1}/bfee2.egg-info/top_level.txt +0 -0
  79. {bfee2-3.1.1 → bfee2-3.2.1}/bin/BFEE2Gui.py +0 -0
  80. {bfee2-3.1.1 → bfee2-3.2.1}/pyproject.toml +0 -0
  81. {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')