Skip to content

Commit 41d4e9a

Browse files
committed
Add a QboxContext helper class
1 parent 450f181 commit 41d4e9a

File tree

1 file changed

+112
-24
lines changed

1 file changed

+112
-24
lines changed

pysages/backends/contexts.py

Lines changed: 112 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,26 @@
66
class to hold the simulation data.
77
"""
88

9+
import weakref
10+
from dataclasses import dataclass
911
from importlib import import_module
10-
11-
from pysages.typing import Any, Callable, JaxArray, NamedTuple, Optional
12-
from pysages.utils import is_file
12+
from pathlib import Path
13+
from xml.etree import ElementTree as et
14+
15+
from pysages.typing import (
16+
Any,
17+
Callable,
18+
Iterable,
19+
JaxArray,
20+
NamedTuple,
21+
Optional,
22+
Union,
23+
)
24+
from pysages.utils import dispatch, is_file, splitlines
1325

1426
JaxMDState = Any
27+
QboxInstance = Any
28+
XMLElement = et.Element
1529

1630

1731
class JaxMDContextState(NamedTuple):
@@ -63,44 +77,118 @@ class JaxMDContext(NamedTuple):
6377
dt: float
6478

6579

80+
@dataclass(frozen=True)
6681
class QboxContextGenerator:
6782
"""
6883
Provides an interface for setting up Qbox-backed simulations.
6984
7085
Arguments
7186
---------
87+
7288
launch_command: str
7389
Specifies the command that will be used to run Qbox in interactive mode,
7490
e.g. `qb` or `mpirun -n 4 qb`.
7591
76-
input_script: str
77-
Path to the Qbox input script.
92+
script: str
93+
File or multile string with the Qbox input script.
94+
95+
nitscf: Optional[int]
96+
Same as Qbox's `run` command parameter. The maximum number of self-consistent
97+
iterations.
7898
79-
output_filename: Union[Path, str]
99+
nite: Optional[int]
100+
Same as Qbox's `run` command parameter. The number of electronic iterations
101+
performed between updates of the charge density.
102+
103+
logfile: Union[Path, str]
80104
Name for the output file. It must not exist on the working directory.
81105
Defaults to `qb.r`.
82106
"""
83107

84-
def __init__(self, launch_command, input_script, output_filename="qb.r"):
85-
self.cmd = launch_command
86-
self.script = input_script
87-
self.logfile = output_filename
108+
# NOTE: we leave `niter` as non-configurable for now.
109+
# niter: int
110+
# Same as Qbox's `run` command parameter. The number of steps during which atomic
111+
# positions are updated. Defaults to 1.
88112

89-
def __call__(self, **kwargs):
90-
if not is_file(self.script):
91-
raise FileNotFoundError(f"Unable to find or open {self.script}")
113+
launch_command: str
114+
script: str
115+
nitscf: Optional[int] = None
116+
nite: Optional[int] = None
117+
logfile: Union[Path, str] = Path("qb.r")
92118

119+
def __call__(self, **kwargs):
93120
if is_file(self.logfile):
94-
msg = f"Delete {self.logfile} or choose a different output file name"
121+
msg = f"Rename or delete {self.logfile}, or choose a different log file name"
95122
raise FileExistsError(msg)
96123

97-
pexpect = import_module("pexpect")
98-
99-
qb = pexpect.spawn(self.cmd)
100-
qb.logfile_read = open(self.logfile, "wb")
101-
qb.expect(r"\[qbox\] ")
102-
103-
qb.sendline(self.script)
104-
qb.expect(r"\[qbox\] ")
105-
106-
return qb
124+
return QboxContext(
125+
self.launch_command, self.script, self.logfile, 1, self.nitscf, self.nite
126+
)
127+
128+
129+
@dataclass(frozen=True)
130+
class QboxContext:
131+
instance: QboxInstance
132+
niter: int
133+
nitscf: Optional[int]
134+
nite: Optional[int]
135+
species_masses: dict
136+
initial_state: XMLElement
137+
state: XMLElement
138+
139+
@dispatch
140+
def __init__(
141+
self, launch_command: str, script: str, logfile: Union[Path, str], niter, nitscf, nite
142+
):
143+
pexpect = import_module("pexpect.popen_spawn")
144+
145+
def finalize(qb):
146+
if not qb.flag_eof:
147+
qb.sendline("quit")
148+
qb.expect(pexpect.EOF)
149+
150+
qb = pexpect.PopenSpawn(launch_command)
151+
weakref.finalize(qb, lambda: finalize(qb))
152+
qb.logfile_read = open(logfile, "wb")
153+
i = qb.expect([r"\[qbox\] ", pexpect.EOF])
154+
155+
if i == 1: # EOF was written to the log file
156+
preamble = (
157+
"The command:\n\n "
158+
f"{launch_command}\n\n"
159+
"for running Qbox failed, it returned the following:\n\n"
160+
)
161+
raise ChildProcessError(preamble + qb.before.decode())
162+
163+
super().__setattr__("instance", qb)
164+
super().__setattr__("niter", niter)
165+
super().__setattr__("nitscf", "" if nitscf is None else nitscf)
166+
super().__setattr__("nite", "" if nite is None else nite)
167+
168+
initial_state = qb.before
169+
state = self.process_input(script) # sets `self.state`
170+
171+
if self.state.find("error") is not None:
172+
try:
173+
qb.expect(pexpect.EOF, timeout=3)
174+
finally:
175+
raise ChildProcessError("Qbox encountered the following error:\n" + state.decode())
176+
177+
initial_state += state + b"\n</fpmd:simulation>"
178+
super().__setattr__("initial_state", et.fromstring(initial_state))
179+
180+
k = 1822.888486 # to convert amu to atomic units
181+
species = self.initial_state.iter("species")
182+
species_masses = {s.attrib["name"]: k * float(s.find("mass").text) for s in species}
183+
super().__setattr__("species_masses", species_masses)
184+
185+
def process_input(self, entries: Union[str, Iterable[str]], target=r"\[qbox\] ", timeout=None):
186+
qb = self.instance
187+
state = b""
188+
for entry in splitlines(entries):
189+
qb.sendline(entry)
190+
qb.expect(target, timeout=timeout)
191+
state += qb.before
192+
# We add tags to ensure that the state corresponds to a valid xml section
193+
super().__setattr__("state", et.fromstring(b"<root>\n" + state + b"\n</root>"))
194+
return state

0 commit comments

Comments
 (0)