hillclimber 0.1.0a2__py3-none-any.whl → 0.1.0a3__py3-none-any.whl
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.
Potentially problematic release.
This version of hillclimber might be problematic. Click here for more details.
- hillclimber/__init__.py +4 -0
- hillclimber/analysis.py +636 -0
- hillclimber/biases.py +14 -14
- hillclimber/cvs.py +60 -45
- hillclimber/metadynamics.py +100 -26
- hillclimber/opes.py +22 -7
- {hillclimber-0.1.0a2.dist-info → hillclimber-0.1.0a3.dist-info}/METADATA +59 -1
- hillclimber-0.1.0a3.dist-info/RECORD +17 -0
- hillclimber-0.1.0a2.dist-info/RECORD +0 -16
- {hillclimber-0.1.0a2.dist-info → hillclimber-0.1.0a3.dist-info}/WHEEL +0 -0
- {hillclimber-0.1.0a2.dist-info → hillclimber-0.1.0a3.dist-info}/entry_points.txt +0 -0
- {hillclimber-0.1.0a2.dist-info → hillclimber-0.1.0a3.dist-info}/licenses/LICENSE +0 -0
hillclimber/__init__.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from hillclimber.actions import PrintAction
|
|
2
|
+
from hillclimber.analysis import sum_hills, read_colvar, plot_cv_time_series
|
|
2
3
|
from hillclimber.biases import RestraintBias, UpperWallBias, LowerWallBias
|
|
3
4
|
from hillclimber.cvs import DistanceCV, AngleCV, CoordinationNumberCV, TorsionCV, RadiusOfGyrationCV
|
|
4
5
|
from hillclimber.metadynamics import MetadBias, MetaDynamicsConfig, MetaDynamicsModel
|
|
@@ -26,4 +27,7 @@ __all__ = [
|
|
|
26
27
|
"RestraintBias",
|
|
27
28
|
"UpperWallBias",
|
|
28
29
|
"LowerWallBias",
|
|
30
|
+
"sum_hills",
|
|
31
|
+
"read_colvar",
|
|
32
|
+
"plot_cv_time_series",
|
|
29
33
|
]
|
hillclimber/analysis.py
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"""Analysis utilities for metadynamics simulations.
|
|
2
|
+
|
|
3
|
+
This module provides tools for analyzing metadynamics simulations,
|
|
4
|
+
including free energy surface reconstruction and other post-processing tasks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import typing as t
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _validate_multi_cv_params(
|
|
18
|
+
min_bounds: float | list[float] | None = None,
|
|
19
|
+
max_bounds: float | list[float] | None = None,
|
|
20
|
+
bin: int | list[int] | None = None,
|
|
21
|
+
spacing: float | list[float] | None = None,
|
|
22
|
+
sigma: float | list[float] | None = None,
|
|
23
|
+
idw: str | list[str] | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
"""Validate that multi-CV parameters have consistent dimensions.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
min_bounds, max_bounds, bin, spacing, sigma, idw
|
|
30
|
+
Parameters from sum_hills that can be lists for multi-CV cases.
|
|
31
|
+
|
|
32
|
+
Raises
|
|
33
|
+
------
|
|
34
|
+
ValueError
|
|
35
|
+
If list parameters have inconsistent lengths.
|
|
36
|
+
"""
|
|
37
|
+
# Collect all list parameters and their lengths
|
|
38
|
+
list_params: dict[str, int] = {}
|
|
39
|
+
|
|
40
|
+
params_to_check = {
|
|
41
|
+
"min_bounds": min_bounds,
|
|
42
|
+
"max_bounds": max_bounds,
|
|
43
|
+
"bin": bin,
|
|
44
|
+
"spacing": spacing,
|
|
45
|
+
"sigma": sigma,
|
|
46
|
+
"idw": idw,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for name, value in params_to_check.items():
|
|
50
|
+
if isinstance(value, (list, tuple)):
|
|
51
|
+
list_params[name] = len(value)
|
|
52
|
+
|
|
53
|
+
# If no list parameters, nothing to validate (single CV case)
|
|
54
|
+
if not list_params:
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# Check that all list parameters have the same length
|
|
58
|
+
lengths = set(list_params.values())
|
|
59
|
+
if len(lengths) > 1:
|
|
60
|
+
# Build a detailed error message
|
|
61
|
+
param_details = ", ".join(
|
|
62
|
+
f"{name}={length}" for name, length in list_params.items()
|
|
63
|
+
)
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Inconsistent number of CVs in parameters. "
|
|
66
|
+
f"All list parameters must have the same length. "
|
|
67
|
+
f"Got: {param_details}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def sum_hills(
|
|
72
|
+
hills_file: str | Path,
|
|
73
|
+
plumed_bin_path: str | Path | None = None,
|
|
74
|
+
# Boolean flags
|
|
75
|
+
negbias: bool = False,
|
|
76
|
+
nohistory: bool = False,
|
|
77
|
+
mintozero: bool = False,
|
|
78
|
+
# File/histogram options
|
|
79
|
+
histo: str | Path | None = None,
|
|
80
|
+
# Grid parameters
|
|
81
|
+
stride: int | None = None,
|
|
82
|
+
min_bounds: float | list[float] | None = None,
|
|
83
|
+
max_bounds: float | list[float] | None = None,
|
|
84
|
+
bin: int | list[int] | None = None,
|
|
85
|
+
spacing: float | list[float] | None = None,
|
|
86
|
+
# Variable selection
|
|
87
|
+
idw: str | list[str] | None = None,
|
|
88
|
+
# Output options
|
|
89
|
+
outfile: str | Path | None = None,
|
|
90
|
+
outhisto: str | Path | None = None,
|
|
91
|
+
# Integration parameters
|
|
92
|
+
kt: float | None = None,
|
|
93
|
+
sigma: float | list[float] | None = None,
|
|
94
|
+
# Format
|
|
95
|
+
fmt: str | None = None,
|
|
96
|
+
# Additional options
|
|
97
|
+
verbose: bool = True,
|
|
98
|
+
check: bool = True,
|
|
99
|
+
) -> subprocess.CompletedProcess:
|
|
100
|
+
"""Run PLUMED sum_hills to reconstruct free energy surfaces from metadynamics.
|
|
101
|
+
|
|
102
|
+
This function wraps the PLUMED ``sum_hills`` command-line tool, which analyzes
|
|
103
|
+
HILLS files from metadynamics simulations to reconstruct the free energy surface.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
hills_file : str or Path
|
|
108
|
+
Path to the HILLS file to analyze. This file is generated during
|
|
109
|
+
metadynamics simulations and contains the deposited Gaussian hills.
|
|
110
|
+
plumed_bin_path : str or Path, optional
|
|
111
|
+
Path to the PLUMED installation directory (containing ``bin/`` and ``lib/``
|
|
112
|
+
subdirectories). If None, searches for ``plumed`` in the system PATH.
|
|
113
|
+
When a full installation path is provided, the function will properly set
|
|
114
|
+
LD_LIBRARY_PATH to include the PLUMED libraries.
|
|
115
|
+
negbias : bool, default=False
|
|
116
|
+
Print the negative bias instead of the free energy.
|
|
117
|
+
nohistory : bool, default=False
|
|
118
|
+
To be used with ``stride``: splits the bias/histogram without previous history.
|
|
119
|
+
mintozero : bool, default=False
|
|
120
|
+
Translate all minimum values in bias/histogram to zero.
|
|
121
|
+
histo : str or Path, optional
|
|
122
|
+
Name of the file for histogram (a COLVAR/HILLS file is good).
|
|
123
|
+
stride : int, optional
|
|
124
|
+
Stride for integrating hills file. Default is 0 (never integrate).
|
|
125
|
+
min_bounds : float or list[float], optional
|
|
126
|
+
Lower bounds for the grid. For multi-dimensional CVs, provide a list with
|
|
127
|
+
one value per CV (e.g., ``[-3.14, -3.14]`` for two torsion angles).
|
|
128
|
+
max_bounds : float or list[float], optional
|
|
129
|
+
Upper bounds for the grid. For multi-dimensional CVs, provide a list with
|
|
130
|
+
one value per CV (e.g., ``[3.14, 3.14]`` for two torsion angles).
|
|
131
|
+
bin : int or list[int], optional
|
|
132
|
+
Number of bins for the grid. For multi-dimensional CVs, provide a list with
|
|
133
|
+
one value per CV (e.g., ``[250, 250]`` for two CVs with 250 bins each).
|
|
134
|
+
spacing : float or list[float], optional
|
|
135
|
+
Grid spacing, alternative to the number of bins. For multi-dimensional CVs,
|
|
136
|
+
provide a list with one value per CV.
|
|
137
|
+
idw : str or list[str], optional
|
|
138
|
+
Variables to be used for the free-energy/histogram. For multi-dimensional CVs,
|
|
139
|
+
provide a list with one variable name per CV (e.g., ``['phi', 'psi']``).
|
|
140
|
+
outfile : str or Path, optional
|
|
141
|
+
Output file for sum_hills. Default is ``fes.dat``.
|
|
142
|
+
outhisto : str or Path, optional
|
|
143
|
+
Output file for the histogram.
|
|
144
|
+
kt : float, optional
|
|
145
|
+
Temperature in energy units (kJ/mol) for integrating out variables.
|
|
146
|
+
sigma : float or list[float], optional
|
|
147
|
+
Sigma for binning (only needed when doing histogram). For multi-dimensional CVs,
|
|
148
|
+
provide a list with one value per CV.
|
|
149
|
+
fmt : str, optional
|
|
150
|
+
Output format specification.
|
|
151
|
+
verbose : bool, default=True
|
|
152
|
+
Print command output to stdout/stderr.
|
|
153
|
+
check : bool, default=True
|
|
154
|
+
Raise exception if command fails.
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
subprocess.CompletedProcess
|
|
159
|
+
The completed process object from subprocess.run.
|
|
160
|
+
|
|
161
|
+
Raises
|
|
162
|
+
------
|
|
163
|
+
FileNotFoundError
|
|
164
|
+
If the HILLS file, PLUMED executable, or PLUMED installation directory
|
|
165
|
+
cannot be found.
|
|
166
|
+
ValueError
|
|
167
|
+
If list-based parameters (``bin``, ``min_bounds``, ``max_bounds``, etc.)
|
|
168
|
+
have inconsistent lengths when using multiple CVs.
|
|
169
|
+
subprocess.CalledProcessError
|
|
170
|
+
If the PLUMED command fails and ``check=True``.
|
|
171
|
+
|
|
172
|
+
Examples
|
|
173
|
+
--------
|
|
174
|
+
Basic usage to reconstruct a 1D free energy surface:
|
|
175
|
+
|
|
176
|
+
>>> import hillclimber as hc
|
|
177
|
+
>>> hc.sum_hills("HILLS")
|
|
178
|
+
|
|
179
|
+
With custom grid resolution and output file:
|
|
180
|
+
|
|
181
|
+
>>> hc.sum_hills(
|
|
182
|
+
... "HILLS",
|
|
183
|
+
... bin=1000,
|
|
184
|
+
... outfile="custom_fes.dat"
|
|
185
|
+
... )
|
|
186
|
+
|
|
187
|
+
For a 2D free energy surface with explicit bounds:
|
|
188
|
+
|
|
189
|
+
>>> hc.sum_hills(
|
|
190
|
+
... "HILLS",
|
|
191
|
+
... bin=[100, 100],
|
|
192
|
+
... min_bounds=[0.0, 0.0],
|
|
193
|
+
... max_bounds=[10.0, 10.0],
|
|
194
|
+
... outfile="fes_2d.dat"
|
|
195
|
+
... )
|
|
196
|
+
|
|
197
|
+
For protein backbone torsion angles (phi and psi):
|
|
198
|
+
|
|
199
|
+
>>> hc.sum_hills(
|
|
200
|
+
... "HILLS",
|
|
201
|
+
... bin=[250, 250],
|
|
202
|
+
... min_bounds=[-3.14, -3.14],
|
|
203
|
+
... max_bounds=[3.14, 3.14],
|
|
204
|
+
... idw=["phi", "psi"],
|
|
205
|
+
... outfile="ramachandran.dat"
|
|
206
|
+
... )
|
|
207
|
+
|
|
208
|
+
Resources
|
|
209
|
+
---------
|
|
210
|
+
- https://www.plumed.org/doc-master/user-doc/html/sum_hills.html
|
|
211
|
+
|
|
212
|
+
Notes
|
|
213
|
+
-----
|
|
214
|
+
The HILLS file is automatically generated during metadynamics simulations
|
|
215
|
+
when using the METAD action. Each line in the file represents a deposited
|
|
216
|
+
Gaussian hill with its position, width (sigma), and height.
|
|
217
|
+
|
|
218
|
+
The free energy surface is reconstructed by summing all deposited hills:
|
|
219
|
+
F(s) = -V(s) where V(s) is the bias potential.
|
|
220
|
+
|
|
221
|
+
**Multi-CV Consistency:**
|
|
222
|
+
When using multiple collective variables (CVs), all list-based parameters
|
|
223
|
+
must have the same length. For example, if analyzing two CVs (phi and psi),
|
|
224
|
+
then ``bin``, ``min_bounds``, ``max_bounds``, and ``idw`` (if provided as lists)
|
|
225
|
+
must all have exactly 2 elements. The function will raise a ``ValueError``
|
|
226
|
+
if inconsistent list lengths are detected.
|
|
227
|
+
"""
|
|
228
|
+
# Convert to Path object
|
|
229
|
+
hills_file = Path(hills_file)
|
|
230
|
+
|
|
231
|
+
# Verify HILLS file exists
|
|
232
|
+
if not hills_file.exists():
|
|
233
|
+
raise FileNotFoundError(f"HILLS file not found: {hills_file}")
|
|
234
|
+
|
|
235
|
+
# Find PLUMED executable and set up environment
|
|
236
|
+
env = os.environ.copy()
|
|
237
|
+
|
|
238
|
+
if plumed_bin_path is None:
|
|
239
|
+
# Try to find plumed in system PATH
|
|
240
|
+
plumed_exec = shutil.which("plumed")
|
|
241
|
+
if plumed_exec is None:
|
|
242
|
+
raise FileNotFoundError(
|
|
243
|
+
"PLUMED executable not found in system PATH. "
|
|
244
|
+
"Please install PLUMED or specify the installation path with plumed_bin_path="
|
|
245
|
+
)
|
|
246
|
+
else:
|
|
247
|
+
# Use provided PLUMED installation path
|
|
248
|
+
plumed_bin_path = Path(plumed_bin_path)
|
|
249
|
+
plumed_exec = plumed_bin_path / "bin" / "plumed"
|
|
250
|
+
lib_path = plumed_bin_path / "lib"
|
|
251
|
+
|
|
252
|
+
# Verify paths exist
|
|
253
|
+
if not plumed_exec.exists():
|
|
254
|
+
raise FileNotFoundError(
|
|
255
|
+
f"PLUMED executable not found at: {plumed_exec}\n"
|
|
256
|
+
f"Make sure plumed_bin_path points to the PLUMED installation directory "
|
|
257
|
+
f"containing bin/ and lib/ subdirectories."
|
|
258
|
+
)
|
|
259
|
+
if not lib_path.exists():
|
|
260
|
+
raise FileNotFoundError(f"PLUMED lib directory not found: {lib_path}")
|
|
261
|
+
|
|
262
|
+
# Set LD_LIBRARY_PATH for PLUMED libraries
|
|
263
|
+
current_ld_path = env.get("LD_LIBRARY_PATH", "")
|
|
264
|
+
if current_ld_path:
|
|
265
|
+
env["LD_LIBRARY_PATH"] = f"{lib_path}:{current_ld_path}"
|
|
266
|
+
else:
|
|
267
|
+
env["LD_LIBRARY_PATH"] = str(lib_path)
|
|
268
|
+
|
|
269
|
+
plumed_exec = str(plumed_exec)
|
|
270
|
+
|
|
271
|
+
# Validate multi-CV parameter consistency
|
|
272
|
+
_validate_multi_cv_params(
|
|
273
|
+
min_bounds=min_bounds,
|
|
274
|
+
max_bounds=max_bounds,
|
|
275
|
+
bin=bin,
|
|
276
|
+
spacing=spacing,
|
|
277
|
+
sigma=sigma,
|
|
278
|
+
idw=idw,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
# Build command
|
|
282
|
+
cmd_parts = [plumed_exec, "sum_hills"]
|
|
283
|
+
|
|
284
|
+
# Add hills file
|
|
285
|
+
cmd_parts.extend(["--hills", str(hills_file)])
|
|
286
|
+
|
|
287
|
+
# Add boolean flags
|
|
288
|
+
if negbias:
|
|
289
|
+
cmd_parts.append("--negbias")
|
|
290
|
+
if nohistory:
|
|
291
|
+
cmd_parts.append("--nohistory")
|
|
292
|
+
if mintozero:
|
|
293
|
+
cmd_parts.append("--mintozero")
|
|
294
|
+
|
|
295
|
+
# Helper function to format list parameters
|
|
296
|
+
def format_param(value: t.Any) -> str:
|
|
297
|
+
if isinstance(value, (list, tuple)):
|
|
298
|
+
return ",".join(str(v) for v in value)
|
|
299
|
+
return str(value)
|
|
300
|
+
|
|
301
|
+
# Add optional parameters
|
|
302
|
+
if histo is not None:
|
|
303
|
+
cmd_parts.extend(["--histo", str(histo)])
|
|
304
|
+
if stride is not None:
|
|
305
|
+
cmd_parts.extend(["--stride", str(stride)])
|
|
306
|
+
if min_bounds is not None:
|
|
307
|
+
cmd_parts.extend(["--min", format_param(min_bounds)])
|
|
308
|
+
if max_bounds is not None:
|
|
309
|
+
cmd_parts.extend(["--max", format_param(max_bounds)])
|
|
310
|
+
if bin is not None:
|
|
311
|
+
cmd_parts.extend(["--bin", format_param(bin)])
|
|
312
|
+
if spacing is not None:
|
|
313
|
+
cmd_parts.extend(["--spacing", format_param(spacing)])
|
|
314
|
+
if idw is not None:
|
|
315
|
+
cmd_parts.extend(["--idw", format_param(idw)])
|
|
316
|
+
if outfile is not None:
|
|
317
|
+
cmd_parts.extend(["--outfile", str(outfile)])
|
|
318
|
+
if outhisto is not None:
|
|
319
|
+
cmd_parts.extend(["--outhisto", str(outhisto)])
|
|
320
|
+
if kt is not None:
|
|
321
|
+
cmd_parts.extend(["--kt", str(kt)])
|
|
322
|
+
if sigma is not None:
|
|
323
|
+
cmd_parts.extend(["--sigma", format_param(sigma)])
|
|
324
|
+
if fmt is not None:
|
|
325
|
+
cmd_parts.extend(["--fmt", str(fmt)])
|
|
326
|
+
|
|
327
|
+
# Run command
|
|
328
|
+
if verbose:
|
|
329
|
+
print(f"Running: {' '.join(cmd_parts)}")
|
|
330
|
+
|
|
331
|
+
result = subprocess.run(
|
|
332
|
+
cmd_parts,
|
|
333
|
+
env=env,
|
|
334
|
+
capture_output=not verbose,
|
|
335
|
+
text=True,
|
|
336
|
+
check=check,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if verbose and result.returncode == 0:
|
|
340
|
+
print("sum_hills completed successfully")
|
|
341
|
+
|
|
342
|
+
return result
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def read_colvar(
|
|
346
|
+
colvar_file: str | Path,
|
|
347
|
+
) -> dict[str, np.ndarray]:
|
|
348
|
+
"""Read a PLUMED COLVAR file and parse its contents.
|
|
349
|
+
|
|
350
|
+
This function reads a COLVAR file produced by PLUMED, extracts the field names
|
|
351
|
+
from the header (which starts with ``#! FIELDS``), and returns the data as a
|
|
352
|
+
dictionary mapping field names to numpy arrays.
|
|
353
|
+
|
|
354
|
+
Parameters
|
|
355
|
+
----------
|
|
356
|
+
colvar_file : str or Path
|
|
357
|
+
Path to the COLVAR file to read.
|
|
358
|
+
|
|
359
|
+
Returns
|
|
360
|
+
-------
|
|
361
|
+
dict[str, np.ndarray]
|
|
362
|
+
Dictionary mapping field names to 1D numpy arrays containing the data.
|
|
363
|
+
Keys correspond to the fields specified in the COLVAR header.
|
|
364
|
+
|
|
365
|
+
Raises
|
|
366
|
+
------
|
|
367
|
+
FileNotFoundError
|
|
368
|
+
If the COLVAR file does not exist.
|
|
369
|
+
ValueError
|
|
370
|
+
If the COLVAR file does not contain a valid ``#! FIELDS`` header.
|
|
371
|
+
|
|
372
|
+
Examples
|
|
373
|
+
--------
|
|
374
|
+
>>> import hillclimber as hc
|
|
375
|
+
>>> data = hc.read_colvar("COLVAR")
|
|
376
|
+
>>> print(data.keys())
|
|
377
|
+
dict_keys(['time', 'phi', 'psi'])
|
|
378
|
+
>>> print(data['time'][:5])
|
|
379
|
+
[0. 1. 2. 3. 4.]
|
|
380
|
+
|
|
381
|
+
Notes
|
|
382
|
+
-----
|
|
383
|
+
The COLVAR file format from PLUMED starts with a header line:
|
|
384
|
+
``#! FIELDS time cv1 cv2 ...``
|
|
385
|
+
|
|
386
|
+
All subsequent lines starting with ``#`` are treated as comments and ignored.
|
|
387
|
+
Data lines are parsed as whitespace-separated numeric values.
|
|
388
|
+
|
|
389
|
+
Resources
|
|
390
|
+
---------
|
|
391
|
+
- https://www.plumed.org/doc-master/user-doc/html/colvar.html
|
|
392
|
+
"""
|
|
393
|
+
colvar_file = Path(colvar_file)
|
|
394
|
+
|
|
395
|
+
if not colvar_file.exists():
|
|
396
|
+
raise FileNotFoundError(f"COLVAR file not found: {colvar_file}")
|
|
397
|
+
|
|
398
|
+
# Read the file
|
|
399
|
+
with open(colvar_file, "r") as f:
|
|
400
|
+
lines = f.readlines()
|
|
401
|
+
|
|
402
|
+
# Find and parse the header
|
|
403
|
+
field_names: list[str] | None = None
|
|
404
|
+
for line in lines:
|
|
405
|
+
if line.startswith("#! FIELDS"):
|
|
406
|
+
# Extract field names from the header
|
|
407
|
+
# Format: "#! FIELDS time phi psi ..."
|
|
408
|
+
fields_match = re.match(r"#!\s*FIELDS\s+(.+)", line)
|
|
409
|
+
if fields_match:
|
|
410
|
+
field_names = fields_match.group(1).split()
|
|
411
|
+
break
|
|
412
|
+
|
|
413
|
+
if field_names is None:
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"COLVAR file {colvar_file} does not contain a valid '#! FIELDS' header"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
# Parse data lines (skip comments)
|
|
419
|
+
data_lines = []
|
|
420
|
+
for line in lines:
|
|
421
|
+
# Skip comments and empty lines
|
|
422
|
+
if line.startswith("#") or not line.strip():
|
|
423
|
+
continue
|
|
424
|
+
# Parse numeric data
|
|
425
|
+
values = line.split()
|
|
426
|
+
if len(values) == len(field_names):
|
|
427
|
+
data_lines.append([float(v) for v in values])
|
|
428
|
+
|
|
429
|
+
# Convert to numpy array
|
|
430
|
+
data_array = np.array(data_lines)
|
|
431
|
+
|
|
432
|
+
# Create dictionary mapping field names to columns
|
|
433
|
+
result = {name: data_array[:, i] for i, name in enumerate(field_names)}
|
|
434
|
+
|
|
435
|
+
return result
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def plot_cv_time_series(
|
|
439
|
+
colvar_file: str | Path,
|
|
440
|
+
cv_names: list[str] | None = None,
|
|
441
|
+
time_unit: str = "ps",
|
|
442
|
+
exclude_patterns: list[str] | None = None,
|
|
443
|
+
figsize: tuple[float, float] = (8, 5),
|
|
444
|
+
kde_width: str = "25%",
|
|
445
|
+
colors: list[str] | None = None,
|
|
446
|
+
alpha: float = 0.5,
|
|
447
|
+
marker: str = "x",
|
|
448
|
+
marker_size: float = 10,
|
|
449
|
+
) -> tuple[t.Any, t.Any]:
|
|
450
|
+
"""Plot collective variables over time with KDE distributions.
|
|
451
|
+
|
|
452
|
+
This function creates a visualization showing CV evolution over time as scatter
|
|
453
|
+
plots, with kernel density estimation (KDE) plots displayed on the right side
|
|
454
|
+
to show the distribution of each CV.
|
|
455
|
+
|
|
456
|
+
Parameters
|
|
457
|
+
----------
|
|
458
|
+
colvar_file : str or Path
|
|
459
|
+
Path to the COLVAR file to plot.
|
|
460
|
+
cv_names : list[str], optional
|
|
461
|
+
List of CV names to plot. If None, automatically detects CVs by excluding
|
|
462
|
+
common non-CV fields like 'time', 'sigma_*', 'height', 'biasf'.
|
|
463
|
+
time_unit : str, default='ps'
|
|
464
|
+
Unit label for the time axis.
|
|
465
|
+
exclude_patterns : list[str], optional
|
|
466
|
+
Additional regex patterns for field names to exclude from auto-detection.
|
|
467
|
+
Default excludes: 'time', 'sigma_.*', 'height', 'biasf'.
|
|
468
|
+
figsize : tuple[float, float], default=(8, 5)
|
|
469
|
+
Figure size in inches (width, height).
|
|
470
|
+
kde_width : str, default='25%'
|
|
471
|
+
Width of the KDE subplot as a percentage of the main plot width.
|
|
472
|
+
colors : list[str], optional
|
|
473
|
+
List of colors to use for each CV. If None, uses default color cycle.
|
|
474
|
+
alpha : float, default=0.5
|
|
475
|
+
Transparency for scatter points.
|
|
476
|
+
marker : str, default='x'
|
|
477
|
+
Marker style for scatter points.
|
|
478
|
+
marker_size : float, default=10
|
|
479
|
+
Size of scatter markers.
|
|
480
|
+
|
|
481
|
+
Returns
|
|
482
|
+
-------
|
|
483
|
+
fig : matplotlib.figure.Figure
|
|
484
|
+
The matplotlib figure object.
|
|
485
|
+
axes : tuple
|
|
486
|
+
Tuple of (main_axis, kde_axis) matplotlib axes objects.
|
|
487
|
+
|
|
488
|
+
Raises
|
|
489
|
+
------
|
|
490
|
+
ImportError
|
|
491
|
+
If matplotlib or seaborn is not installed.
|
|
492
|
+
FileNotFoundError
|
|
493
|
+
If the COLVAR file does not exist.
|
|
494
|
+
|
|
495
|
+
Examples
|
|
496
|
+
--------
|
|
497
|
+
Basic usage with auto-detected CVs:
|
|
498
|
+
|
|
499
|
+
>>> import hillclimber as hc
|
|
500
|
+
>>> fig, axes = hc.plot_cv_time_series("COLVAR")
|
|
501
|
+
|
|
502
|
+
Plot specific CVs:
|
|
503
|
+
|
|
504
|
+
>>> fig, axes = hc.plot_cv_time_series("COLVAR", cv_names=["phi", "psi"])
|
|
505
|
+
|
|
506
|
+
Customize appearance:
|
|
507
|
+
|
|
508
|
+
>>> fig, axes = hc.plot_cv_time_series(
|
|
509
|
+
... "COLVAR",
|
|
510
|
+
... figsize=(10, 6),
|
|
511
|
+
... colors=["blue", "red"],
|
|
512
|
+
... alpha=0.7
|
|
513
|
+
... )
|
|
514
|
+
|
|
515
|
+
Notes
|
|
516
|
+
-----
|
|
517
|
+
This function requires matplotlib and seaborn to be installed.
|
|
518
|
+
|
|
519
|
+
The function automatically detects CVs by excluding common metadata fields
|
|
520
|
+
such as 'time', 'sigma_*', 'height', and 'biasf'. You can specify additional
|
|
521
|
+
exclusion patterns or explicitly provide the CV names to plot.
|
|
522
|
+
|
|
523
|
+
Resources
|
|
524
|
+
---------
|
|
525
|
+
- https://www.plumed.org/doc-master/user-doc/html/colvar.html
|
|
526
|
+
"""
|
|
527
|
+
try:
|
|
528
|
+
import matplotlib.pyplot as plt
|
|
529
|
+
import seaborn as sns
|
|
530
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
531
|
+
except ImportError as e:
|
|
532
|
+
raise ImportError(
|
|
533
|
+
"matplotlib and seaborn are required for plotting. "
|
|
534
|
+
"Install them with: pip install matplotlib seaborn"
|
|
535
|
+
) from e
|
|
536
|
+
|
|
537
|
+
# Read the COLVAR file
|
|
538
|
+
data = read_colvar(colvar_file)
|
|
539
|
+
|
|
540
|
+
# Auto-detect CVs if not specified
|
|
541
|
+
if cv_names is None:
|
|
542
|
+
# Default exclusion patterns
|
|
543
|
+
default_exclude = [
|
|
544
|
+
r"^time$",
|
|
545
|
+
r"^sigma_.*$",
|
|
546
|
+
r"^height$",
|
|
547
|
+
r"^biasf$",
|
|
548
|
+
]
|
|
549
|
+
if exclude_patterns is not None:
|
|
550
|
+
default_exclude.extend(exclude_patterns)
|
|
551
|
+
|
|
552
|
+
# Filter field names
|
|
553
|
+
detected_cvs: list[str] = []
|
|
554
|
+
for field in data.keys():
|
|
555
|
+
# Check if field matches any exclusion pattern
|
|
556
|
+
exclude = False
|
|
557
|
+
for pattern in default_exclude:
|
|
558
|
+
if re.match(pattern, field):
|
|
559
|
+
exclude = True
|
|
560
|
+
break
|
|
561
|
+
if not exclude:
|
|
562
|
+
detected_cvs.append(field)
|
|
563
|
+
|
|
564
|
+
if not detected_cvs:
|
|
565
|
+
raise ValueError(
|
|
566
|
+
"No CVs detected in COLVAR file. "
|
|
567
|
+
"All fields were excluded by the exclusion patterns."
|
|
568
|
+
)
|
|
569
|
+
cv_names = detected_cvs
|
|
570
|
+
|
|
571
|
+
# Verify that all requested CVs exist
|
|
572
|
+
missing_cvs = [cv for cv in cv_names if cv not in data]
|
|
573
|
+
if missing_cvs:
|
|
574
|
+
raise ValueError(
|
|
575
|
+
f"CVs not found in COLVAR file: {missing_cvs}. "
|
|
576
|
+
f"Available fields: {list(data.keys())}"
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
# Get time data
|
|
580
|
+
if "time" not in data:
|
|
581
|
+
raise ValueError("COLVAR file must contain a 'time' field")
|
|
582
|
+
time = data["time"]
|
|
583
|
+
|
|
584
|
+
# Default colors if not provided
|
|
585
|
+
if colors is None:
|
|
586
|
+
colors = plt.cm.tab10.colors # type: ignore
|
|
587
|
+
|
|
588
|
+
# Set seaborn style
|
|
589
|
+
sns.set(style="whitegrid")
|
|
590
|
+
|
|
591
|
+
# Create figure
|
|
592
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
593
|
+
|
|
594
|
+
# Plot each CV
|
|
595
|
+
for i, cv_name in enumerate(cv_names):
|
|
596
|
+
color = colors[i % len(colors)]
|
|
597
|
+
cv_data = data[cv_name]
|
|
598
|
+
ax.scatter(
|
|
599
|
+
time,
|
|
600
|
+
cv_data,
|
|
601
|
+
c=[color],
|
|
602
|
+
label=cv_name,
|
|
603
|
+
marker=marker,
|
|
604
|
+
s=marker_size,
|
|
605
|
+
alpha=alpha,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
ax.set_xlabel(f"Time / {time_unit}")
|
|
609
|
+
ax.set_ylabel("CV value")
|
|
610
|
+
ax.legend()
|
|
611
|
+
|
|
612
|
+
# Create KDE subplot on the right
|
|
613
|
+
divider = make_axes_locatable(ax)
|
|
614
|
+
ax_kde = divider.append_axes("right", size=kde_width, pad=0.1, sharey=ax)
|
|
615
|
+
|
|
616
|
+
# Plot KDE for each CV
|
|
617
|
+
for i, cv_name in enumerate(cv_names):
|
|
618
|
+
color = colors[i % len(colors)]
|
|
619
|
+
cv_data = data[cv_name]
|
|
620
|
+
sns.kdeplot(
|
|
621
|
+
y=cv_data,
|
|
622
|
+
ax=ax_kde,
|
|
623
|
+
color=color,
|
|
624
|
+
fill=True,
|
|
625
|
+
alpha=0.3,
|
|
626
|
+
linewidth=1.5,
|
|
627
|
+
label=cv_name,
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
# Clean up KDE axis
|
|
631
|
+
ax_kde.set_xlabel("Density")
|
|
632
|
+
ax_kde.yaxis.set_tick_params(labelleft=False)
|
|
633
|
+
|
|
634
|
+
plt.tight_layout()
|
|
635
|
+
|
|
636
|
+
return fig, (ax, ax_kde)
|
hillclimber/biases.py
CHANGED
|
@@ -29,17 +29,17 @@ class RestraintBias(BiasProtocol):
|
|
|
29
29
|
cv : CollectiveVariable
|
|
30
30
|
The collective variable to restrain.
|
|
31
31
|
kappa : float
|
|
32
|
-
The force constant of the restraint in
|
|
33
|
-
depends on the CV (e.g.,
|
|
32
|
+
The force constant of the restraint in eV/unit^2, where unit
|
|
33
|
+
depends on the CV (e.g., eV/Ų for distances, eV/rad² for angles).
|
|
34
34
|
at : float
|
|
35
|
-
The center/target value of the restraint.
|
|
35
|
+
The center/target value of the restraint in CV units (e.g., Å for distances).
|
|
36
36
|
label : str, optional
|
|
37
37
|
A custom label for this restraint. If not provided, uses cv.prefix + "_restraint".
|
|
38
38
|
|
|
39
39
|
Examples
|
|
40
40
|
--------
|
|
41
41
|
>>> import hillclimber as hc
|
|
42
|
-
>>> # Restrain a distance around 2.5 Å
|
|
42
|
+
>>> # Restrain a distance around 2.5 Šwith force constant 200 eV/Ų
|
|
43
43
|
>>> distance_cv = hc.DistanceCV(...)
|
|
44
44
|
>>> restraint = hc.RestraintBias(cv=distance_cv, kappa=200.0, at=2.5)
|
|
45
45
|
|
|
@@ -49,10 +49,10 @@ class RestraintBias(BiasProtocol):
|
|
|
49
49
|
|
|
50
50
|
Notes
|
|
51
51
|
-----
|
|
52
|
-
|
|
53
|
-
- Distances (Å): kappa in
|
|
54
|
-
- Angles (radians): kappa in
|
|
55
|
-
- Dimensionless CVs: kappa in
|
|
52
|
+
Due to the UNITS line, kappa is in eV/unit^2 (ASE energy units). For typical CVs:
|
|
53
|
+
- Distances (Å): kappa in eV/Ų
|
|
54
|
+
- Angles (radians): kappa in eV/rad²
|
|
55
|
+
- Dimensionless CVs: kappa in eV
|
|
56
56
|
|
|
57
57
|
The restraint creates a bias force: F = -kappa * (s - at)
|
|
58
58
|
"""
|
|
@@ -109,9 +109,9 @@ class UpperWallBias(BiasProtocol):
|
|
|
109
109
|
cv : CollectiveVariable
|
|
110
110
|
The collective variable to apply the wall to.
|
|
111
111
|
at : float
|
|
112
|
-
The position of the wall (threshold value).
|
|
112
|
+
The position of the wall (threshold value) in CV units.
|
|
113
113
|
kappa : float
|
|
114
|
-
The force constant of the wall in
|
|
114
|
+
The force constant of the wall in eV.
|
|
115
115
|
exp : int, optional
|
|
116
116
|
The exponent of the wall potential. Default is 2 (harmonic).
|
|
117
117
|
Higher values create steeper walls.
|
|
@@ -125,7 +125,7 @@ class UpperWallBias(BiasProtocol):
|
|
|
125
125
|
Examples
|
|
126
126
|
--------
|
|
127
127
|
>>> import hillclimber as hc
|
|
128
|
-
>>> # Prevent distance from exceeding 3.0 Å
|
|
128
|
+
>>> # Prevent distance from exceeding 3.0 Å with force constant 100 eV
|
|
129
129
|
>>> distance_cv = hc.DistanceCV(...)
|
|
130
130
|
>>> upper_wall = hc.UpperWallBias(cv=distance_cv, at=3.0, kappa=100.0, exp=2)
|
|
131
131
|
|
|
@@ -209,9 +209,9 @@ class LowerWallBias(BiasProtocol):
|
|
|
209
209
|
cv : CollectiveVariable
|
|
210
210
|
The collective variable to apply the wall to.
|
|
211
211
|
at : float
|
|
212
|
-
The position of the wall (threshold value).
|
|
212
|
+
The position of the wall (threshold value) in CV units.
|
|
213
213
|
kappa : float
|
|
214
|
-
The force constant of the wall in
|
|
214
|
+
The force constant of the wall in eV.
|
|
215
215
|
exp : int, optional
|
|
216
216
|
The exponent of the wall potential. Default is 2 (harmonic).
|
|
217
217
|
Higher values create steeper walls.
|
|
@@ -225,7 +225,7 @@ class LowerWallBias(BiasProtocol):
|
|
|
225
225
|
Examples
|
|
226
226
|
--------
|
|
227
227
|
>>> import hillclimber as hc
|
|
228
|
-
>>> # Prevent distance from going below 1.0 Å
|
|
228
|
+
>>> # Prevent distance from going below 1.0 Å with force constant 100 eV
|
|
229
229
|
>>> distance_cv = hc.DistanceCV(...)
|
|
230
230
|
>>> lower_wall = hc.LowerWallBias(cv=distance_cv, at=1.0, kappa=100.0, exp=2)
|
|
231
231
|
|
hillclimber/cvs.py
CHANGED
|
@@ -21,7 +21,6 @@ from hillclimber.virtual_atoms import VirtualAtom
|
|
|
21
21
|
GroupReductionStrategyType = Literal[
|
|
22
22
|
"com", "cog", "first", "all", "com_per_group", "cog_per_group"
|
|
23
23
|
]
|
|
24
|
-
MultiGroupStrategyType = Literal["first", "all_pairs", "corresponding", "first_to_all"]
|
|
25
24
|
SiteIdentifier = Union[str, List[int]]
|
|
26
25
|
ColorTuple = Tuple[float, float, float]
|
|
27
26
|
AtomHighlightMap = Dict[int, ColorTuple]
|
|
@@ -132,22 +131,6 @@ class _BasePlumedCV(CollectiveVariable):
|
|
|
132
131
|
if cv_keyword in cmd and cmd.strip().startswith((prefix, f"{prefix}_"))
|
|
133
132
|
]
|
|
134
133
|
|
|
135
|
-
@staticmethod
|
|
136
|
-
def _get_index_pairs(
|
|
137
|
-
len1: int, len2: int, strategy: MultiGroupStrategyType
|
|
138
|
-
) -> List[Tuple[int, int]]:
|
|
139
|
-
"""Determines pairs of group indices based on the multi-group strategy."""
|
|
140
|
-
if strategy == "first":
|
|
141
|
-
return [(0, 0)] if len1 > 0 and len2 > 0 else []
|
|
142
|
-
if strategy == "all_pairs":
|
|
143
|
-
return [(i, j) for i in range(len1) for j in range(len2)]
|
|
144
|
-
if strategy == "corresponding":
|
|
145
|
-
n = min(len1, len2)
|
|
146
|
-
return [(i, i) for i in range(n)]
|
|
147
|
-
if strategy == "first_to_all":
|
|
148
|
-
return [(0, j) for j in range(len2)] if len1 > 0 else []
|
|
149
|
-
raise ValueError(f"Unknown multi-group strategy: {strategy}")
|
|
150
|
-
|
|
151
134
|
@staticmethod
|
|
152
135
|
def _create_virtual_site_command(
|
|
153
136
|
group: List[int], strategy: Literal["com", "cog"], label: str
|
|
@@ -901,18 +884,25 @@ class TorsionCV(_BasePlumedCV):
|
|
|
901
884
|
Calculates the torsional (dihedral) angle defined by four atoms. Each group
|
|
902
885
|
provided by the selector must contain exactly four atoms.
|
|
903
886
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
887
|
+
Parameters
|
|
888
|
+
----------
|
|
889
|
+
atoms : AtomSelector
|
|
890
|
+
Selector for one or more groups of 4 atoms. Each group must contain exactly 4 atoms.
|
|
891
|
+
prefix : str
|
|
892
|
+
Label prefix for the generated PLUMED commands.
|
|
893
|
+
strategy : {"first", "all"}, default="first"
|
|
894
|
+
Strategy for handling multiple groups from the selector:
|
|
895
|
+
- "first": Process only the first group (creates 1 CV)
|
|
896
|
+
- "all": Process all groups independently (creates N CVs)
|
|
908
897
|
|
|
909
|
-
Resources
|
|
910
|
-
|
|
898
|
+
Resources
|
|
899
|
+
---------
|
|
900
|
+
- https://www.plumed.org/doc-master/user-doc/html/TORSION
|
|
911
901
|
"""
|
|
912
902
|
|
|
913
903
|
atoms: AtomSelector
|
|
914
904
|
prefix: str
|
|
915
|
-
|
|
905
|
+
strategy: Literal["first", "all"] = "first"
|
|
916
906
|
|
|
917
907
|
def _get_atom_highlights(
|
|
918
908
|
self, atoms: Atoms, **kwargs
|
|
@@ -955,10 +945,10 @@ class TorsionCV(_BasePlumedCV):
|
|
|
955
945
|
|
|
956
946
|
def _generate_commands(self, groups: List[List[int]]) -> List[str]:
|
|
957
947
|
"""Generates all necessary PLUMED commands."""
|
|
958
|
-
#
|
|
959
|
-
if self.
|
|
948
|
+
# Determine which groups to process based on strategy
|
|
949
|
+
if self.strategy == "first" and groups:
|
|
960
950
|
indices_to_process = [0]
|
|
961
|
-
else: # "
|
|
951
|
+
else: # "all" - process all groups independently
|
|
962
952
|
indices_to_process = list(range(len(groups)))
|
|
963
953
|
|
|
964
954
|
commands = []
|
|
@@ -978,19 +968,33 @@ class RadiusOfGyrationCV(_BasePlumedCV):
|
|
|
978
968
|
Calculates the radius of gyration of a group of atoms. The radius of gyration
|
|
979
969
|
is a measure of the size of a molecular system.
|
|
980
970
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
971
|
+
Parameters
|
|
972
|
+
----------
|
|
973
|
+
atoms : AtomSelector
|
|
974
|
+
Selector for the atoms to include in the gyration calculation.
|
|
975
|
+
prefix : str
|
|
976
|
+
Label prefix for the generated PLUMED commands.
|
|
977
|
+
flatten : bool, default=False
|
|
978
|
+
How to handle multiple groups from the selector:
|
|
979
|
+
- True: Combine all groups into one and calculate single Rg (creates 1 CV)
|
|
980
|
+
- False: Keep groups separate, use strategy to determine which to process
|
|
981
|
+
strategy : {"first", "all"}, default="first"
|
|
982
|
+
Strategy for handling multiple groups when flatten=False:
|
|
983
|
+
- "first": Process only the first group (creates 1 CV)
|
|
984
|
+
- "all": Process all groups independently (creates N CVs)
|
|
985
|
+
type : str, default="RADIUS"
|
|
986
|
+
The type of gyration tensor to use.
|
|
987
|
+
Options: "RADIUS", "GTPC_1", "GTPC_2", "GTPC_3", "ASPHERICITY", "ACYLINDRICITY", "KAPPA2", etc.
|
|
986
988
|
|
|
987
|
-
Resources
|
|
988
|
-
|
|
989
|
+
Resources
|
|
990
|
+
---------
|
|
991
|
+
- https://www.plumed.org/doc-master/user-doc/html/GYRATION/
|
|
989
992
|
"""
|
|
990
993
|
|
|
991
994
|
atoms: AtomSelector
|
|
992
995
|
prefix: str
|
|
993
|
-
|
|
996
|
+
flatten: bool = False
|
|
997
|
+
strategy: Literal["first", "all"] = "first"
|
|
994
998
|
type: str = "RADIUS" # Options: RADIUS, GTPC_1, GTPC_2, GTPC_3, ASPHERICITY, ACYLINDRICITY, KAPPA2, etc.
|
|
995
999
|
|
|
996
1000
|
def _get_atom_highlights(
|
|
@@ -1021,18 +1025,29 @@ class RadiusOfGyrationCV(_BasePlumedCV):
|
|
|
1021
1025
|
|
|
1022
1026
|
def _generate_commands(self, groups: List[List[int]]) -> List[str]:
|
|
1023
1027
|
"""Generates all necessary PLUMED commands."""
|
|
1024
|
-
# For gyration, 'multi_group' determines how many groups to process.
|
|
1025
|
-
if self.multi_group in ["first", "first_to_all"] and groups:
|
|
1026
|
-
indices_to_process = [0]
|
|
1027
|
-
else: # "all_pairs" and "corresponding" imply processing all independent groups.
|
|
1028
|
-
indices_to_process = list(range(len(groups)))
|
|
1029
|
-
|
|
1030
1028
|
commands = []
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1029
|
+
|
|
1030
|
+
if self.flatten:
|
|
1031
|
+
# Combine all groups into single atom list
|
|
1032
|
+
flat_atoms = [idx for group in groups for idx in group]
|
|
1033
|
+
atom_list = ",".join(str(idx + 1) for idx in flat_atoms)
|
|
1034
|
+
command = f"{self.prefix}: GYRATION ATOMS={atom_list}"
|
|
1035
1035
|
if self.type != "RADIUS":
|
|
1036
1036
|
command += f" TYPE={self.type}"
|
|
1037
1037
|
commands.append(command)
|
|
1038
|
+
else:
|
|
1039
|
+
# Keep groups separate and use strategy to determine which to process
|
|
1040
|
+
if self.strategy == "first" and groups:
|
|
1041
|
+
indices_to_process = [0]
|
|
1042
|
+
else: # "all" - process all groups independently
|
|
1043
|
+
indices_to_process = list(range(len(groups)))
|
|
1044
|
+
|
|
1045
|
+
for i in indices_to_process:
|
|
1046
|
+
label = self.prefix if len(indices_to_process) == 1 else f"{self.prefix}_{i}"
|
|
1047
|
+
atom_list = ",".join(str(idx + 1) for idx in groups[i])
|
|
1048
|
+
command = f"{label}: GYRATION ATOMS={atom_list}"
|
|
1049
|
+
if self.type != "RADIUS":
|
|
1050
|
+
command += f" TYPE={self.type}"
|
|
1051
|
+
commands.append(command)
|
|
1052
|
+
|
|
1038
1053
|
return commands
|
hillclimber/metadynamics.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dataclasses
|
|
2
|
+
import typing as t
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
import ase.units
|
|
@@ -21,11 +22,14 @@ class MetadBias:
|
|
|
21
22
|
cv : CollectiveVariable
|
|
22
23
|
The collective variable to bias.
|
|
23
24
|
sigma : float, optional
|
|
24
|
-
The width of the Gaussian potential
|
|
25
|
+
The width of the Gaussian potential in the same units as the CV
|
|
26
|
+
(e.g., Å for distances, radians for angles), by default None.
|
|
25
27
|
grid_min : float | str, optional
|
|
26
|
-
The minimum value of the grid
|
|
28
|
+
The minimum value of the grid in CV units (or PLUMED expression like "-pi"),
|
|
29
|
+
by default None.
|
|
27
30
|
grid_max : float | str, optional
|
|
28
|
-
The maximum value of the grid
|
|
31
|
+
The maximum value of the grid in CV units (or PLUMED expression like "pi"),
|
|
32
|
+
by default None.
|
|
29
33
|
grid_bin : int, optional
|
|
30
34
|
The number of bins in the grid, by default None.
|
|
31
35
|
|
|
@@ -47,22 +51,32 @@ class MetaDynamicsConfig:
|
|
|
47
51
|
|
|
48
52
|
This contains only the global parameters that apply to all CVs.
|
|
49
53
|
|
|
54
|
+
Units
|
|
55
|
+
-----
|
|
56
|
+
hillclimber uses ASE units throughout. The UNITS line in the PLUMED input tells
|
|
57
|
+
PLUMED to interpret all values in ASE units:
|
|
58
|
+
- Distances: Ångström (Å)
|
|
59
|
+
- Energies: electronvolt (eV) - including HEIGHT, SIGMA for energy-based CVs, etc.
|
|
60
|
+
- Time: femtoseconds (fs)
|
|
61
|
+
- Temperature: Kelvin (K)
|
|
62
|
+
|
|
50
63
|
Parameters
|
|
51
64
|
----------
|
|
52
65
|
height : float, optional
|
|
53
|
-
The height of the Gaussian potential in
|
|
66
|
+
The height of the Gaussian potential in eV, by default 1.0.
|
|
54
67
|
pace : int, optional
|
|
55
|
-
The frequency of Gaussian deposition, by default 500.
|
|
68
|
+
The frequency of Gaussian deposition in MD steps, by default 500.
|
|
56
69
|
biasfactor : float, optional
|
|
57
70
|
The bias factor for well-tempered metadynamics, by default None.
|
|
58
71
|
temp : float, optional
|
|
59
72
|
The temperature of the system in Kelvin, by default 300.0.
|
|
60
73
|
file : str, optional
|
|
61
74
|
The name of the hills file, by default "HILLS".
|
|
62
|
-
adaptive :
|
|
63
|
-
The adaptive scheme to use, by default
|
|
75
|
+
adaptive : t.Literal["GEOM", "DIFF"] | None, optional
|
|
76
|
+
The adaptive scheme to use, by default None.
|
|
77
|
+
If None, no ADAPTIVE parameter is written to PLUMED.
|
|
64
78
|
flush : int | None
|
|
65
|
-
The frequency of flushing the output files.
|
|
79
|
+
The frequency of flushing the output files in MD steps.
|
|
66
80
|
If None, uses the plumed default.
|
|
67
81
|
|
|
68
82
|
Resources
|
|
@@ -76,7 +90,7 @@ class MetaDynamicsConfig:
|
|
|
76
90
|
biasfactor: float | None = None
|
|
77
91
|
temp: float = 300.0
|
|
78
92
|
file: str = "HILLS"
|
|
79
|
-
adaptive:
|
|
93
|
+
adaptive: t.Literal["GEOM", "DIFF"] | None = None
|
|
80
94
|
flush: int | None = None
|
|
81
95
|
|
|
82
96
|
|
|
@@ -187,8 +201,13 @@ class MetaDynamicsModel(zntrack.Node, NodeWithCalculator):
|
|
|
187
201
|
|
|
188
202
|
sigmas, grid_mins, grid_maxs, grid_bins = [], [], [], []
|
|
189
203
|
|
|
204
|
+
# PLUMED UNITS line specifies conversion factors from ASE units to PLUMED's native units:
|
|
205
|
+
# - LENGTH=A: ASE uses Ångström (A), PLUMED native is nm → A is a valid PLUMED unit
|
|
206
|
+
# - TIME: ASE uses fs, PLUMED native is ps → 1 fs = 0.001 ps
|
|
207
|
+
# - ENERGY: ASE uses eV, PLUMED native is kJ/mol → 1 eV = 96.485 kJ/mol
|
|
208
|
+
# See: https://www.plumed.org/doc-master/user-doc/html/ (MD engine integration docs)
|
|
190
209
|
plumed_lines.append(
|
|
191
|
-
f"UNITS LENGTH=A TIME={1
|
|
210
|
+
f"UNITS LENGTH=A TIME={1/1000} ENERGY={ase.units.mol / ase.units.kJ}"
|
|
192
211
|
)
|
|
193
212
|
|
|
194
213
|
for bias_cv in self.bias_cvs:
|
|
@@ -196,17 +215,20 @@ class MetaDynamicsModel(zntrack.Node, NodeWithCalculator):
|
|
|
196
215
|
plumed_lines.extend(cv_str)
|
|
197
216
|
all_labels.extend(labels)
|
|
198
217
|
|
|
199
|
-
# Collect per-CV parameters for later
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
218
|
+
# Collect per-CV parameters for later - repeat for each label
|
|
219
|
+
# PLUMED requires one parameter value per ARG, so if a CV generates
|
|
220
|
+
# multiple labels, we need to repeat the parameter values
|
|
221
|
+
for _ in labels:
|
|
222
|
+
sigmas.append(str(bias_cv.sigma) if bias_cv.sigma is not None else None)
|
|
223
|
+
grid_mins.append(
|
|
224
|
+
str(bias_cv.grid_min) if bias_cv.grid_min is not None else None
|
|
225
|
+
)
|
|
226
|
+
grid_maxs.append(
|
|
227
|
+
str(bias_cv.grid_max) if bias_cv.grid_max is not None else None
|
|
228
|
+
)
|
|
229
|
+
grid_bins.append(
|
|
230
|
+
str(bias_cv.grid_bin) if bias_cv.grid_bin is not None else None
|
|
231
|
+
)
|
|
210
232
|
|
|
211
233
|
metad_parts = [
|
|
212
234
|
"METAD",
|
|
@@ -215,16 +237,31 @@ class MetaDynamicsModel(zntrack.Node, NodeWithCalculator):
|
|
|
215
237
|
f"PACE={self.config.pace}",
|
|
216
238
|
f"TEMP={self.config.temp}",
|
|
217
239
|
f"FILE={self.config.file}",
|
|
218
|
-
f"ADAPTIVE={self.config.adaptive}",
|
|
219
240
|
]
|
|
241
|
+
if self.config.adaptive is not None:
|
|
242
|
+
metad_parts.append(f"ADAPTIVE={self.config.adaptive}")
|
|
220
243
|
if self.config.biasfactor is not None:
|
|
221
244
|
metad_parts.append(f"BIASFACTOR={self.config.biasfactor}")
|
|
222
245
|
|
|
223
246
|
# Add SIGMA, GRID_MIN, GRID_MAX, GRID_BIN only if any value is set
|
|
224
247
|
if any(v is not None for v in sigmas):
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
248
|
+
# When using ADAPTIVE, PLUMED requires only one sigma value
|
|
249
|
+
if self.config.adaptive is not None:
|
|
250
|
+
# Validate that all sigma values are the same when adaptive is set
|
|
251
|
+
unique_sigmas = set(v for v in sigmas if v is not None)
|
|
252
|
+
if len(unique_sigmas) > 1:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"When using ADAPTIVE={self.config.adaptive}, all CVs must have the same sigma value. "
|
|
255
|
+
f"Found different sigma values: {unique_sigmas}"
|
|
256
|
+
)
|
|
257
|
+
# Use the first non-None sigma value
|
|
258
|
+
sigma_value = next(v for v in sigmas if v is not None)
|
|
259
|
+
metad_parts.append(f"SIGMA={sigma_value}")
|
|
260
|
+
else:
|
|
261
|
+
# Standard mode: one sigma per CV
|
|
262
|
+
metad_parts.append(
|
|
263
|
+
f"SIGMA={','.join(v if v is not None else '0.0' for v in sigmas)}"
|
|
264
|
+
)
|
|
228
265
|
if any(v is not None for v in grid_mins):
|
|
229
266
|
metad_parts.append(
|
|
230
267
|
f"GRID_MIN={','.join(v if v is not None else '0.0' for v in grid_mins)}"
|
|
@@ -240,10 +277,47 @@ class MetaDynamicsModel(zntrack.Node, NodeWithCalculator):
|
|
|
240
277
|
|
|
241
278
|
plumed_lines.append(f"metad: {' '.join(metad_parts)}")
|
|
242
279
|
|
|
280
|
+
# Track defined commands to detect duplicates and conflicts
|
|
281
|
+
# Map label -> full command for labeled commands (e.g., "d: DISTANCE ...")
|
|
282
|
+
defined_commands = {}
|
|
283
|
+
for line in plumed_lines:
|
|
284
|
+
# Check if this is a labeled command (format: "label: ACTION ...")
|
|
285
|
+
if ": " in line:
|
|
286
|
+
label = line.split(": ", 1)[0]
|
|
287
|
+
defined_commands[label] = line
|
|
288
|
+
|
|
243
289
|
# Add any additional actions (restraints, walls, print actions, etc.)
|
|
244
290
|
for action in self.actions:
|
|
245
291
|
action_lines = action.to_plumed(atoms)
|
|
246
|
-
|
|
292
|
+
|
|
293
|
+
# Filter out duplicate CV definitions, but detect conflicts
|
|
294
|
+
filtered_lines = []
|
|
295
|
+
for line in action_lines:
|
|
296
|
+
# Check if this is a labeled command
|
|
297
|
+
if ": " in line:
|
|
298
|
+
label = line.split(": ", 1)[0]
|
|
299
|
+
|
|
300
|
+
# Check if this label was already defined
|
|
301
|
+
if label in defined_commands:
|
|
302
|
+
# If the command is identical, skip (deduplication)
|
|
303
|
+
if defined_commands[label] == line:
|
|
304
|
+
continue
|
|
305
|
+
# If the command is different, raise error (conflict)
|
|
306
|
+
else:
|
|
307
|
+
raise ValueError(
|
|
308
|
+
f"Conflicting definitions for label '{label}':\n"
|
|
309
|
+
f" Already defined: {defined_commands[label]}\n"
|
|
310
|
+
f" New definition: {line}"
|
|
311
|
+
)
|
|
312
|
+
else:
|
|
313
|
+
# New labeled command, track it
|
|
314
|
+
defined_commands[label] = line
|
|
315
|
+
filtered_lines.append(line)
|
|
316
|
+
else:
|
|
317
|
+
# Unlabeled command, always add
|
|
318
|
+
filtered_lines.append(line)
|
|
319
|
+
|
|
320
|
+
plumed_lines.extend(filtered_lines)
|
|
247
321
|
|
|
248
322
|
# Add FLUSH if configured
|
|
249
323
|
if self.config.flush is not None:
|
hillclimber/opes.py
CHANGED
|
@@ -27,8 +27,9 @@ class OPESBias:
|
|
|
27
27
|
cv : CollectiveVariable
|
|
28
28
|
The collective variable to bias.
|
|
29
29
|
sigma : float | str, optional
|
|
30
|
-
Initial kernel width.
|
|
31
|
-
|
|
30
|
+
Initial kernel width in CV units (e.g., Å for distances, radians for angles).
|
|
31
|
+
Use "ADAPTIVE" for automatic adaptation (recommended).
|
|
32
|
+
If numeric, specifies the initial width.
|
|
32
33
|
Default: "ADAPTIVE".
|
|
33
34
|
|
|
34
35
|
Resources
|
|
@@ -53,10 +54,19 @@ class OPESConfig:
|
|
|
53
54
|
OPES (On-the-fly Probability Enhanced Sampling) is a modern enhanced
|
|
54
55
|
sampling method that samples well-tempered target distributions.
|
|
55
56
|
|
|
57
|
+
Units
|
|
58
|
+
-----
|
|
59
|
+
hillclimber uses ASE units throughout. The UNITS line in the PLUMED input tells
|
|
60
|
+
PLUMED to interpret all values in ASE units:
|
|
61
|
+
- Distances: Ångström (Å)
|
|
62
|
+
- Energies: electronvolt (eV) - including BARRIER, SIGMA_MIN, etc.
|
|
63
|
+
- Time: femtoseconds (fs)
|
|
64
|
+
- Temperature: Kelvin (K)
|
|
65
|
+
|
|
56
66
|
Parameters
|
|
57
67
|
----------
|
|
58
68
|
barrier : float
|
|
59
|
-
Highest free energy barrier to overcome (
|
|
69
|
+
Highest free energy barrier to overcome (eV). This is the key
|
|
60
70
|
parameter that determines sampling efficiency.
|
|
61
71
|
pace : int, optional
|
|
62
72
|
Frequency of kernel deposition in MD steps (default: 500).
|
|
@@ -75,9 +85,9 @@ class OPESConfig:
|
|
|
75
85
|
file : str, optional
|
|
76
86
|
File to store deposited kernels (default: "KERNELS").
|
|
77
87
|
adaptive_sigma_stride : int, optional
|
|
78
|
-
|
|
88
|
+
MD steps between adaptive sigma measurements. If not set, uses 10×PACE.
|
|
79
89
|
sigma_min : float, optional
|
|
80
|
-
Minimum allowable sigma value for adaptive sigma.
|
|
90
|
+
Minimum allowable sigma value for adaptive sigma in CV units.
|
|
81
91
|
state_wfile : str, optional
|
|
82
92
|
State file for writing exact restart information.
|
|
83
93
|
state_rfile : str, optional
|
|
@@ -89,7 +99,7 @@ class OPESConfig:
|
|
|
89
99
|
calc_work : bool, optional
|
|
90
100
|
Calculate and output accumulated work (default: False).
|
|
91
101
|
flush : int, optional
|
|
92
|
-
Frequency of flushing output files.
|
|
102
|
+
Frequency of flushing output files in MD steps.
|
|
93
103
|
|
|
94
104
|
Resources
|
|
95
105
|
---------
|
|
@@ -272,8 +282,13 @@ class OPESModel(zntrack.Node, NodeWithCalculator):
|
|
|
272
282
|
|
|
273
283
|
sigmas = []
|
|
274
284
|
|
|
285
|
+
# PLUMED UNITS line specifies conversion factors from ASE units to PLUMED's native units:
|
|
286
|
+
# - LENGTH=A: ASE uses Ångström (A), PLUMED native is nm → A is a valid PLUMED unit
|
|
287
|
+
# - TIME: ASE uses fs, PLUMED native is ps → 1 fs = 0.001 ps
|
|
288
|
+
# - ENERGY: ASE uses eV, PLUMED native is kJ/mol → 1 eV = 96.485 kJ/mol
|
|
289
|
+
# See: https://www.plumed.org/doc-master/user-doc/html/ (MD engine integration docs)
|
|
275
290
|
plumed_lines.append(
|
|
276
|
-
f"UNITS LENGTH=A TIME={1
|
|
291
|
+
f"UNITS LENGTH=A TIME={1/1000} ENERGY={ase.units.mol / ase.units.kJ}"
|
|
277
292
|
)
|
|
278
293
|
|
|
279
294
|
for bias_cv in self.bias_cvs:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hillclimber
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0a3
|
|
4
4
|
Summary: Python interfaces for the plumed library with enhanced sampling.
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -124,6 +124,64 @@ with project:
|
|
|
124
124
|
project.build()
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
## Units
|
|
128
|
+
|
|
129
|
+
hillclimber uses **ASE units** throughout the package:
|
|
130
|
+
|
|
131
|
+
- **Distances**: Ångström (Å)
|
|
132
|
+
- **Energies**: electronvolt (eV)
|
|
133
|
+
- **Time**: femtoseconds (fs)
|
|
134
|
+
- **Temperature**: Kelvin (K)
|
|
135
|
+
- **Mass**: atomic mass units (amu)
|
|
136
|
+
|
|
137
|
+
### Unit Conversion with PLUMED
|
|
138
|
+
|
|
139
|
+
PLUMED internally uses different units (nm, kJ/mol, ps). hillclimber automatically handles the conversion by adding a `UNITS` line to the PLUMED input:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
UNITS LENGTH=A TIME=0.001 ENERGY=96.48533288249877
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This tells PLUMED to interpret all input parameters in ASE units:
|
|
146
|
+
- `LENGTH=A`: Distances are in Ångström
|
|
147
|
+
- `TIME=0.001`: Time values are in fs (1 fs = 0.001 ps)
|
|
148
|
+
- `ENERGY=96.485`: Energies are in eV (1 eV = 96.485 kJ/mol)
|
|
149
|
+
|
|
150
|
+
**All PLUMED parameters (HEIGHT, BARRIER, KAPPA, SIGMA, etc.) should be specified in ASE units (eV, Å, fs).** The UNITS line ensures PLUMED interprets them correctly.
|
|
151
|
+
|
|
152
|
+
### Example with Units
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
config = hc.MetaDynamicsConfig(
|
|
156
|
+
height=0.5, # eV - Gaussian hill height
|
|
157
|
+
pace=150, # MD steps - deposition frequency
|
|
158
|
+
biasfactor=10.0, # Dimensionless - well-tempered factor
|
|
159
|
+
temp=300.0 # Kelvin - system temperature
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
bias = hc.MetadBias(
|
|
163
|
+
cv=distance_cv,
|
|
164
|
+
sigma=0.1, # Å - Gaussian width for distance CV
|
|
165
|
+
grid_min=0.0, # Å - minimum grid value
|
|
166
|
+
grid_max=5.0, # Å - maximum grid value
|
|
167
|
+
grid_bin=100 # Number of bins
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Restraint example
|
|
171
|
+
restraint = hc.RestraintBias(
|
|
172
|
+
cv=distance_cv,
|
|
173
|
+
kappa=200.0, # eV/Ų - force constant
|
|
174
|
+
at=2.5 # Å - restraint center
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
metad_model = hc.MetaDynamicsModel(
|
|
178
|
+
config=config,
|
|
179
|
+
bias_cvs=[bias],
|
|
180
|
+
actions=[restraint],
|
|
181
|
+
timestep=0.5 # fs - MD timestep
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
127
185
|
## Collective Variables
|
|
128
186
|
|
|
129
187
|
hillclimber supports multiple types of collective variables:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
hillclimber/__init__.py,sha256=OoxVZ1pPvRVHY1VVShDBaEGIF06O5fDFxCSGUNGW3m4,1041
|
|
2
|
+
hillclimber/actions.py,sha256=7s78RWP-YnBbbxZyA65OwnyL60v8LnKHyNalui1ypAo,1514
|
|
3
|
+
hillclimber/analysis.py,sha256=2KrlSVRNdajZBFCm8AZBjTixTeZczbu5ya4laLXEZP0,20674
|
|
4
|
+
hillclimber/biases.py,sha256=TbEM19NUOuaTUchzEGNIM8M4TDmtlE5Ss8e9VBbHr5s,9448
|
|
5
|
+
hillclimber/calc.py,sha256=dqanaBF6BJwP6lHQqFqEIng-3bTN_DcddRV-gceboKs,665
|
|
6
|
+
hillclimber/cvs.py,sha256=AWC7Z3h0CyAO7_wq-WdLhNi3spH456Blik60_M99MDA,39934
|
|
7
|
+
hillclimber/interfaces.py,sha256=H4HKN1HldhNJeooqtS-HpJLrCFqpMPr80aPER4SKiao,3807
|
|
8
|
+
hillclimber/metadynamics.py,sha256=1Kn5VhlrGlcVY9g41dNCEbJX-7j5ZEStcM8UGgTkCfI,12636
|
|
9
|
+
hillclimber/nodes.py,sha256=XL9uEXd2HdW2mlbwriG_fMCkZAaz4uZBOI5edO42YDA,145
|
|
10
|
+
hillclimber/opes.py,sha256=F_1daa9xB-wsNyow6sIfxJG2nyOSyCc-vVRuP5ADWdE,13310
|
|
11
|
+
hillclimber/selectors.py,sha256=VoWMvTnKU9vr0dphqxGk1OdhrabbDkzq4GQkeprd6RQ,7931
|
|
12
|
+
hillclimber/virtual_atoms.py,sha256=GVXCJZlbx1cY_ST2G5NHQsGpdMkBLUz04aFm-cD--OA,12270
|
|
13
|
+
hillclimber-0.1.0a3.dist-info/METADATA,sha256=UOw07yd2AJiq8CY2a3myuqS9Udy8y_1nb6gMcHNd0QA,12980
|
|
14
|
+
hillclimber-0.1.0a3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
15
|
+
hillclimber-0.1.0a3.dist-info/entry_points.txt,sha256=RsCL3TDKfieatIWP9JHjmTzMtgWERqwpuuuDPdQ4t5g,124
|
|
16
|
+
hillclimber-0.1.0a3.dist-info/licenses/LICENSE,sha256=FKf4VPZYbuyRVMVSrl6HO48bnw6ih8Uur5y-h_MJAcA,13576
|
|
17
|
+
hillclimber-0.1.0a3.dist-info/RECORD,,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
hillclimber/__init__.py,sha256=MpTyTiou1ACRu6snlCe3Aja3_znqakX-YPV1notCdvA,901
|
|
2
|
-
hillclimber/actions.py,sha256=7s78RWP-YnBbbxZyA65OwnyL60v8LnKHyNalui1ypAo,1514
|
|
3
|
-
hillclimber/biases.py,sha256=OfqKdGNiN2Yk4tP03nzJ9vxBWLjLRvcjgJAS8gZeFHw,9317
|
|
4
|
-
hillclimber/calc.py,sha256=dqanaBF6BJwP6lHQqFqEIng-3bTN_DcddRV-gceboKs,665
|
|
5
|
-
hillclimber/cvs.py,sha256=ag1gr51D1-NJb7tkdeUQdjYfWy0WBfcMQKE1f-thFAQ,39548
|
|
6
|
-
hillclimber/interfaces.py,sha256=H4HKN1HldhNJeooqtS-HpJLrCFqpMPr80aPER4SKiao,3807
|
|
7
|
-
hillclimber/metadynamics.py,sha256=NwNG5THSjmJInBDLFuIKp3cB8P-kE8BAaftZdnSk9J0,8719
|
|
8
|
-
hillclimber/nodes.py,sha256=XL9uEXd2HdW2mlbwriG_fMCkZAaz4uZBOI5edO42YDA,145
|
|
9
|
-
hillclimber/opes.py,sha256=NYY2zcBtBxXrFcWQKplrIkthgnW03WTh4hi9MtDsXqo,12483
|
|
10
|
-
hillclimber/selectors.py,sha256=VoWMvTnKU9vr0dphqxGk1OdhrabbDkzq4GQkeprd6RQ,7931
|
|
11
|
-
hillclimber/virtual_atoms.py,sha256=GVXCJZlbx1cY_ST2G5NHQsGpdMkBLUz04aFm-cD--OA,12270
|
|
12
|
-
hillclimber-0.1.0a2.dist-info/METADATA,sha256=JZgeoAZPviWVz59IgjQsoWYnNuFoXbzIg-vJJDOxcTU,11254
|
|
13
|
-
hillclimber-0.1.0a2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
14
|
-
hillclimber-0.1.0a2.dist-info/entry_points.txt,sha256=RsCL3TDKfieatIWP9JHjmTzMtgWERqwpuuuDPdQ4t5g,124
|
|
15
|
-
hillclimber-0.1.0a2.dist-info/licenses/LICENSE,sha256=FKf4VPZYbuyRVMVSrl6HO48bnw6ih8Uur5y-h_MJAcA,13576
|
|
16
|
-
hillclimber-0.1.0a2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|