Skip to content

Commit 115d9ca

Browse files
committed
support for PLUMED extras in SimulationOutput
1 parent 7f8a183 commit 115d9ca

File tree

5 files changed

+87
-11
lines changed

5 files changed

+87
-11
lines changed

psiflow/functions.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ def format_output(
2222
energy: float,
2323
forces: np.ndarray | None = None,
2424
stress: np.ndarray | None = None,
25+
extras: dict | None = None,
2526
**kwargs,
2627
) -> dict:
2728
""""""
2829
forces = np.zeros(shape=(len(geometry), 3)) if forces is None else forces
2930
stress = np.zeros(shape=(3, 3)) if stress is None else stress
3031
if stress.size == 6:
3132
stress = voigt_6_to_full_3x3_stress(stress)
32-
return {"energy": energy, "forces": forces, "stress": stress}
33+
extras = {} if extras is None else extras
34+
return {"energy": energy, "forces": forces, "stress": stress, 'extras': extras}
3335

3436

3537
@typeguard.typechecked
@@ -92,6 +94,7 @@ def __call__(
9294
@dataclass
9395
class PlumedFunction(EnergyFunction):
9496
plumed_input: str
97+
plumed_extras: list[str]
9598
external: Optional[Union[str, Path]] = None
9699

97100
def __post_init__(self):
@@ -126,6 +129,20 @@ def __call__(
126129
plumed_.cmd("init")
127130
for line in plumed_input.split("\n"):
128131
plumed_.cmd("readInputLine", line)
132+
133+
self.plumed_data = {}
134+
plumed_extras = self.plumed_extras
135+
for x in plumed_extras:
136+
rank = np.zeros(1, dtype=np.int_)
137+
plumed_.cmd(f"getDataRank {x}", rank)
138+
if rank[0] > 1:
139+
raise ValueError("Cannot retrieve variables with rank > 1")
140+
shape = np.zeros(rank[0], dtype=np.int_)
141+
plumed_.cmd(f"getDataShape {x}", shape)
142+
if shape[0] > 1:
143+
raise ValueError("Cannot retrieve variables with size > 1")
144+
self.plumed_data[x] = np.zeros(shape, dtype=np.double)
145+
plumed_.cmd(f"setMemoryForData {x}", self.plumed_data[x]) # internal PLUMED variables are piped to arrays
129146
os.remove(tmp.name) # remove whatever plumed has created
130147
self.plumed_instances[key] = plumed_
131148

@@ -147,12 +164,19 @@ def __call__(
147164
plumed_.cmd("setForces", forces)
148165
plumed_.cmd("setVirial", virial)
149166
plumed_.cmd("prepareCalc")
150-
plumed_.cmd("performCalcNoUpdate")
167+
if "METAD" in self.plumed_input:
168+
plumed_.cmd("performCalcNoUpdate")
169+
else:
170+
plumed_.cmd("performCalc") # TODO: why update? i-PI does not do this
151171
plumed_.cmd("getBias", energy)
152172
stress = None
153173
if geometry.periodic:
154174
stress = virial / np.linalg.det(geometry.cell)
155-
return format_output(geometry, float(energy.item()), forces, stress)
175+
176+
extras = {}
177+
for x in self.plumed_data:
178+
extras[str(x)] = self.plumed_data[x].copy()[0]
179+
return format_output(geometry, float(energy.item()), forces, stress, extras)
156180

157181
@staticmethod
158182
def _geometry_to_key(geometry: Geometry) -> tuple:

psiflow/hamiltonians.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import urllib
22
from functools import partial
3+
import re
34
from pathlib import Path
45
from typing import ClassVar, Optional, Union, Callable, Sequence
56

@@ -282,6 +283,8 @@ def __init__(
282283
):
283284
super().__init__()
284285

286+
match = re.search(r'^\s*PRINT\s+.*\bARG=(\S+)', plumed_input, re.MULTILINE) # parse plumed quantities from input file
287+
self.plumed_extras = match.group(1).split(",") if match else []
285288
self.plumed_input = remove_comments_printflush(plumed_input)
286289
if type(external) in [str, Path]:
287290
external = File(str(external))
@@ -303,7 +306,7 @@ def parameters(self) -> dict:
303306
external = copy_app_future(self.external.filepath, inputs=[self.external])
304307
else:
305308
external = None
306-
return {"plumed_input": self.plumed_input, "external": external}
309+
return {"plumed_input": self.plumed_input, "plumed_extras": self.plumed_extras, "external": external}
307310

308311
def __eq__(self, other: Hamiltonian) -> bool:
309312
if (

psiflow/sampling/driver.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Updated version of the Psiflow driver included in i-Pi
33
"""
44

5+
import json
56
import numpy as np
67

78
from ipi.pes.dummy import Dummy_driver
@@ -78,6 +79,10 @@ def compute_structure(self, cell, pos):
7879
vir_ipi = np.array(
7980
unit_to_internal("energy", "electronvolt", vir_calc.T), dtype=np.float64
8081
)
81-
extras = ""
82+
extras = outputs["extras"]
83+
if extras == {}:
84+
extras = ""
85+
else:
86+
extras = json.dumps(extras)
8287

8388
return pot_ipi, force_ipi, vir_ipi, extras

psiflow/sampling/sampling.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ def setup_output(
322322
trajectory = ET.Element(
323323
"trajectory",
324324
filename="trajectory",
325-
stride=str(step),
325+
stride=str(step), # TODO: separate stride for trajectory and properties?
326326
format="ase",
327327
bead="0",
328328
)
@@ -335,6 +335,21 @@ def setup_output(
335335
)
336336
properties.text = create_xml_list(observables)
337337
output.append(properties)
338+
extras_list = []
339+
for comp in components:
340+
if comp.name.startswith("Plumed"): # technically other hamiltonians could also have extras
341+
extras_list += comp.hamiltonian.plumed_extras # maybe extras should be a general property of the Hamiltonian class?
342+
observables += [extra + "{au}" for extra in extras_list] # for SimulationOutput TODO: what to do with units?
343+
extras = ",".join(list(set(extras_list)))
344+
extras_element = ET.Element(
345+
"trajectory",
346+
filename=f"extras",
347+
stride=str(step),
348+
extra_type=extras,
349+
bead="0",
350+
)
351+
extras_element.text = f" extras "
352+
output.append(extras_element)
338353
return output, observables
339354

340355

@@ -512,7 +527,7 @@ def _sample(
512527
if step is not None:
513528
start = math.floor(start / step) # start is applied on subsampled quantities
514529
if step is None:
515-
keep_trajectory = False
530+
keep_trajectory = False # TODO: warning here?
516531
# TODO: check whether observables are valid?
517532
output, observables = setup_output(
518533
hamiltonian_components, # for potential components

psiflow/sampling/server.py

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from ase.units import Bohr, Ha, _e, _hbar
1414
from ipi.engine.simulation import Simulation
1515
from ipi.utils.softexit import softexit
16+
from ipi.utils.parsing import read_output
1617

1718
from psiflow.geometry import Geometry
1819
from psiflow.sampling.utils import create_xml_list
@@ -93,11 +94,34 @@ def wait_for_clients(input_xml, timeout: int = 60) -> None:
9394
raise ConnectionError(msg)
9495

9596

97+
def add_extras(noutputs: int) -> None:
98+
# property headers
99+
file_props = next(Path.cwd().glob(f"*0*.properties")) # properties should be the same for all coupled walkers?
100+
_, info_dict = read_output(file_props)
101+
props_headers = ["# column {} --> {}{} : {}".format(i, key, "{" + info_dict[key][0] + "}", info_dict[key][1]) for i, key in enumerate(info_dict)]
102+
# extras headers
103+
file_extras = next(Path.cwd().glob(f"*0*.extras*")) # extras should be the same for all coupled walkers?
104+
assert file_extras is not None, "No extras file found."
105+
with open(file_extras, "r") as f:
106+
line = f.readline()
107+
start = line.find("(") + 1
108+
end = line.find(")")
109+
extra_names = line[start:end].split(",")
110+
extras_headers = ["# column {} --> {}{} : {}".format(i + len(props_headers), name, "{au}", "PLUMED variable") for i, name in enumerate(extra_names)]
111+
# add extras values to properties files
112+
for idx in range(noutputs):
113+
file_props = next(Path.cwd().glob(f"*{idx}*.properties"))
114+
file_extras = next(Path.cwd().glob(f"*{idx}*.extras*"))
115+
np.savetxt(file_props, np.hstack((np.loadtxt(file_props), np.loadtxt(file_extras))), fmt='%10.8e', delimiter=' ', header="\n".join(props_headers) + "\n" + "\n".join(extras_headers), comments="")
116+
# granted i-Pi properties file has some weird indentations so the files do not perfectly match
117+
# but output parser still works so who cares
118+
119+
96120
def run(start_xyz: str, input_xml: str):
97121
# prepare starting geometries from context_dir
98122
data_start: list[ase.Atoms] = read(start_xyz, index=":")
99123
for i, at in enumerate(data_start):
100-
print(at.pbc)
124+
print(at.pbc) # TODO: why print?
101125
if not any(at.pbc): # set fake large cell for i-PI
102126
at.pbc = True
103127
at.cell = Cell(NONPERIODIC_CELL)
@@ -134,6 +158,14 @@ def cleanup(output_xyz: str, output_props: str, output_trajs: str) -> None:
134158
_write_frames(*states, outputs=[output_xyz])
135159
print("Moved checkpoint geometries")
136160

161+
output_props = _.split(",") if (_ := output_props) else []
162+
output_trajs = _.split(",") if (_ := output_trajs) else []
163+
164+
# Add collective variables to simulation properties, if they exist
165+
if output_props:
166+
add_extras(len(output_props))
167+
print("Added i-Pi extras output to properties files")
168+
137169
prefix = ""
138170
if "remd" in content:
139171
# unshuffle simulation output according to ensemble
@@ -144,9 +176,6 @@ def cleanup(output_xyz: str, output_props: str, output_trajs: str) -> None:
144176
assert out.returncode == 0 # TODO: what if it isn't?
145177
print("REMDSORT")
146178

147-
output_props = _.split(",") if (_ := output_props) else []
148-
output_trajs = _.split(",") if (_ := output_trajs) else []
149-
150179
# move recorded simulation observables
151180
if len(output_props):
152181
assert len(states) == len(output_props)

0 commit comments

Comments
 (0)