Skip to content

Commit 82806ed

Browse files
andyclaude
andcommitted
tests/e2e: add Quantum ESPRESSO and GROMACS, completing the CI tier
Four codes now run end to end in CI -- MOPAC, NWChem, Quantum ESPRESSO and GROMACS -- every one of them Debian-packaged. ORCA and Gaussian stay a developer-box tier, being registration-walled and licensed. ECCE_E2E_REQUIRE names all four, so a code whose binary is not where the suite looks fails the job rather than skipping quietly. Quantum ESPRESSO: bulk Si, under a second. It needs pseudopotentials to run at all and Debian's quantum-espresso-data-sssp installs 103 of them where ai.qe already looks. The assertions are the two things worth guarding for a plane-wave code: the total energy must be a Hartree value (QE prints Ry, so ~-22.6 would mean the conversion was dropped) and STRESS must be a full symmetric 3x3 -- a transposition or a dropped element shows up in the symmetry check and nowhere else. This is the code whose units are carried in the section HEADER rather than beside the numbers, which is how it once produced wrong values rather than none. GROMACS: 216 SPC waters, 50 steps of MD. The deck deliberately runs MD with temperature coupling rather than a minimisation, because that is what makes the "Energies (kJ/mol)" table WRAP onto two rows -- TE in the first, PRESSURE in the second. CLAUDE.md records that shape as producing wrong values rather than none, and a minimisation does not wrap, so a simpler fixture would have tested nothing. Verified by hand first: the parser takes Total Energy (not Potential, which is a plausible number one column away) and reads Pressure from the second row, and [NULL] does suppress the trailing A V E R A G E S block, which is the last such table in the file. Two harness additions this needed: setup steps GROMACS runs grompp before mdrun, so a code definition can declare preprocessing commands and extra input files to copy alongside the deck. stdout pw.x writes to stdout and ECCE redirects it into qe.pwout; the harness captures it rather than making the case shell out, so it still runs the binary directly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFMUNbw2UpRPmgsqviAowb
1 parent a8226e0 commit 82806ed

8 files changed

Lines changed: 864 additions & 6 deletions

File tree

‎.github/workflows/test.yml‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,16 @@ jobs:
9797
- name: Install dependencies
9898
run: |
9999
sudo apt-get update
100-
sudo apt-get install -y perl python3 mopac nwchem nwchem-openmpi
100+
sudo apt-get install -y perl python3 mopac nwchem nwchem-openmpi \
101+
quantum-espresso quantum-espresso-data-sssp gromacs
101102
102103
# Declare what this job installed. Without this a code whose
103104
# binary is not where the suite looks simply skips, and the job
104105
# goes green with that coverage quietly gone -- which is exactly
105106
# what happened to nwchem on its first run here.
106107
- name: End-to-end suite
107108
env:
108-
ECCE_E2E_REQUIRE: mopac,nwchem
109+
ECCE_E2E_REQUIRE: mopac,nwchem,qe,gromacs
109110
run: tests/e2e/run_tests.py -v
110111

111112
apps:

‎tests/e2e/README.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,9 @@ conversion were dropped, and `ENTHALPY − ETHERM` must equal RT exactly.
127127
| Code | Tier | Notes |
128128
|---|---|---|
129129
| MOPAC | CI | Debian `mopac`; seconds per job |
130-
| NWChem | CI (todo) | Debian `nwchem-openmpi` |
131-
| Quantum ESPRESSO | CI (todo) | Debian |
132-
| GROMACS | CI (todo) | Debian |
130+
| NWChem | CI | Debian `nwchem-openmpi` |
131+
| Quantum ESPRESSO | CI | Debian `quantum-espresso` + `-data-sssp` for pseudopotentials |
132+
| GROMACS | CI | Debian `gromacs`; needs a grompp setup step |
133133
| ORCA | local only | registration-walled |
134134
| Gaussian | local only | licensed |
135135

‎tests/e2e/cases.py‎

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,96 @@ def expect_nwchem_h2o(props, report):
189189
'(got %g)' % te)
190190

191191

192+
def expect_qe_si(props, report):
193+
"""Bulk silicon, PBE, 2 atoms -- QE's scalar and tensor results.
194+
195+
Quantum ESPRESSO is a plane-wave code: no basis set, no orbitals in
196+
ECCE's sense. What it does have that no other code here does is a
197+
STRESS tensor, and that is worth guarding because the value carries
198+
its units in the section HEADER rather than beside the numbers --
199+
the shape that silently produced wrong values rather than none when
200+
QE was first integrated.
201+
"""
202+
for key in ('TE', 'STRESS', 'PRESSURE', 'VERSION'):
203+
report.check(bool(props.get(key)), '%s extracted' % key)
204+
if not (props.get('TE') and props.get('STRESS')):
205+
return
206+
207+
te = float(sect(props['TE'][-1], 'values'))
208+
# Two silicon atoms with this pseudopotential land near -11.3
209+
# Hartree. QE prints Ry, so a value near -22.6 would mean the
210+
# conversion was dropped -- the range is chosen to tell those apart
211+
# rather than to pin the number.
212+
report.check(-15.0 < te < -8.0,
213+
'total energy is a plausible Hartree value for Si2 '
214+
'(got %g; ~-22.6 would mean Ry went unconverted)' % te)
215+
216+
stress = sect(props['STRESS'][-1], 'values').split()
217+
report.check(len(stress) == 9,
218+
'STRESS is a full 3x3 tensor (got %d values)' % len(stress))
219+
if len(stress) == 9:
220+
# A stress tensor is symmetric. This is a real check on the
221+
# parse: a row/column transposition or a dropped element shows
222+
# up here and nowhere else.
223+
s = [float(x) for x in stress]
224+
sym = (abs(s[1] - s[3]) < 1e-6 and abs(s[2] - s[6]) < 1e-6
225+
and abs(s[5] - s[7]) < 1e-6)
226+
report.check(sym, 'STRESS tensor is symmetric (got %s)' % stress)
227+
228+
229+
def expect_gromacs_water(props, report):
230+
"""216 SPC waters, 50 steps of MD.
231+
232+
The point of this case is the WRAPPED ENERGY BLOCK. With enough
233+
terms GROMACS splits its "Energies (kJ/mol)" table over two rows:
234+
235+
LJ (SR) Coulomb (SR) Potential Kinetic En. Total Energy
236+
2.13e+03 -1.21e+04 -9.97e+03 2.31e+03 -7.66e+03
237+
Conserved En. Temperature Pressure (bar)
238+
-7.32e+03 4.31e+02 2.36e+03
239+
240+
so TE comes from the first row and PRESSURE from the second. A
241+
parser that reads only the first row silently reports a plausible
242+
number for the wrong quantity -- CLAUDE.md records this shape as
243+
producing wrong values rather than none. A short minimisation does
244+
not wrap, so the .mdp here deliberately runs MD with temperature
245+
coupling to make it.
246+
247+
It also guards the averages suppression: GROMACS prints one last
248+
"Energies (kJ/mol)" block under "A V E R A G E S", and [NULL] exists
249+
to stop that being taken as the final step's result.
250+
"""
251+
for key in ('TE', 'PRESSURE'):
252+
report.check(bool(props.get(key)), '%s extracted' % key)
253+
if not (props.get('TE') and props.get('PRESSURE')):
254+
return
255+
256+
te = float(sect(props['TE'][-1], 'values'))
257+
pressure = float(sect(props['PRESSURE'][-1], 'values'))
258+
259+
# TE is the Total Energy of 216 waters, a few thousand kJ/mol
260+
# negative. Loose, because it depends on the random starting
261+
# velocities; what must not happen is picking up the Potential
262+
# (~-1e4) or the Conserved En. from the wrong column.
263+
report.check(-9.5e3 < te < -5.0e3,
264+
'TE is the Total Energy, not the Potential '
265+
'(got %g; ~-1.0e4 would be the Potential)' % te)
266+
267+
# Pressure lives in the SECOND row. Reading it from the first
268+
# would pick up a kJ/mol energy of order 1e3-1e4 with the wrong
269+
# sign pattern; a real pressure here is a few thousand bar.
270+
report.check(abs(pressure) < 2.0e4,
271+
'PRESSURE came from the wrapped second row (got %g)'
272+
% pressure)
273+
274+
# The averages block is the LAST "Energies (kJ/mol)" in the file.
275+
# If [NULL] stopped suppressing it, TE would be the average rather
276+
# than the final step, and the two differ by enough to see.
277+
report.check(len(props.get('TEVEC', [])) >= 2,
278+
'TEVEC accumulated per-step energies (got %d)'
279+
% len(props.get('TEVEC', [])))
280+
281+
192282
CASES = [
193283
dict(
194284
name='mopac-ch4-mos',
@@ -222,13 +312,61 @@ def expect_nwchem_h2o(props, report):
222312
output='ecce.out',
223313
expect=expect_nwchem_h2o,
224314
),
315+
dict(
316+
# Bulk Si, 2 atoms, low cutoff -- under a second. QE needs
317+
# pseudopotentials to run at all; Debian's
318+
# quantum-espresso-data-sssp installs 103 of them in
319+
# /usr/share/espresso/pseudo, which is where ai.qe looks and
320+
# what this deck names.
321+
name='qe-si-scf',
322+
code='qe',
323+
desc='qe.desc',
324+
deck='qe/qe.pwin',
325+
parse_args=('.', 'Energy', 'PW', 'PW', '0'),
326+
output='qe.pwout',
327+
expect=expect_qe_si,
328+
),
329+
dict(
330+
# The .mdp is the deck ECCE names; conf.gro and topol.top are
331+
# copied alongside it and consumed by the grompp setup step.
332+
name='gromacs-water-md',
333+
code='gromacs',
334+
desc='gromacs.desc',
335+
deck='gromacs/gromacs.mdp',
336+
parse_args=('.', 'Energy', 'MD', 'MD', '0'),
337+
output='gromacs.log',
338+
expect=expect_gromacs_water,
339+
),
225340
]
226341

227342

228343
# How to find and invoke each code. A code that is absent is SKIPPED
229344
# with its name reported, never silently passed -- an e2e suite that
230345
# quietly tests nothing is worse than no e2e suite.
231346
CODES = {
347+
'gromacs': dict(
348+
binary='gmx',
349+
search=(),
350+
extraInputs=('conf.gro', 'topol.top'),
351+
# grompp first: GROMACS compiles the .mdp, coordinates and
352+
# topology into a .tpr, and only then runs.
353+
setup=(lambda exe, deck: [exe, 'grompp', '-f', deck,
354+
'-c', 'conf.gro', '-p', 'topol.top',
355+
'-o', 'topol.tpr', '-maxwarn', '5'],),
356+
argv=lambda exe, deck: [exe, 'mdrun', '-s', 'topol.tpr',
357+
'-g', 'gromacs.log', '-o', 'traj.trr',
358+
'-c', 'out.gro', '-e', 'ener.edr'],
359+
packaged='gromacs',
360+
),
361+
'qe': dict(
362+
binary='pw.x',
363+
search=(),
364+
# pw.x writes its output to stdout; ECCE redirects it into
365+
# qe.pwout, which is what the .edml declares as the parse file.
366+
argv=lambda exe, deck: [exe, '-in', deck],
367+
stdoutTo=True,
368+
packaged='quantum-espresso + quantum-espresso-data-sssp',
369+
),
232370
'nwchem': dict(
233371
binary='nwchem',
234372
# Debian's nwchem-openmpi ships /usr/bin/nwchem AND

0 commit comments

Comments
 (0)