From 3f1032b2295ba2044851ea30aae6d4e54a299121 Mon Sep 17 00:00:00 2001 From: Marnik Bercx Date: Thu, 7 May 2026 18:07:37 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20`PwOutput`:=20gate=20`total=5Fenerg?= =?UTF-8?q?y`=20on=20the=20calculation=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For `nscf` and `bands` runs QE never assigns `etot` and writes `0.0` to the XML. When such a run shares its `prefix` with a prior `scf` run — the standard `scf → nscf → bands` workflow — the nscf run also overwrites the scf XML at `.save/`, so the meaningful total energy is gone from disk regardless of what the user asked for. `total_energy` is now resolved with a `Coalesce` of two branches: an XML branch gated by `Check` on `xml.input.control_variables.calculation`, and a stdout fallback. The XML branch is taken only for documented calculations types (scf, relax, vc-relax, md, vc-md). For nscf/bands runs the gate fails and `Coalesce` falls through to the stdout branch. `PwStdoutParser` now extracts the converged energy from the last `! total energy = ... Ry` line (relax/md print one per ionic step; the final value is the converged one). Side effect: the `collinear` fixture (which is itself an nscf XML) previously surfaced `total_energy: 0.0`. With the gate in place the field is correctly absent — the `0.0` was always meaningless. The regression snapshot drops it. --- src/qe_tools/outputs/parsers/pw.py | 7 + src/qe_tools/outputs/pw.py | 18 +- .../pw/nscf_etot_clobber/data-file-schema.xml | 236 ++++++++++++++++++ .../fixtures/pw/nscf_etot_clobber/scf.out | 12 + tests/outputs/test_pw_output.py | 26 ++ .../test_default_xml_211101_.yml | 1 + .../test_default_xml_220603_.yml | 1 + .../test_default_xml_230310_.yml | 1 + .../test_default_xml_240411_.yml | 1 + .../test_default_xml_250521_.yml | 1 + .../test_success_base_collinear_.yml | 1 - 11 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 tests/outputs/fixtures/pw/nscf_etot_clobber/data-file-schema.xml create mode 100644 tests/outputs/fixtures/pw/nscf_etot_clobber/scf.out diff --git a/src/qe_tools/outputs/parsers/pw.py b/src/qe_tools/outputs/parsers/pw.py index 88bced7..99ca858 100644 --- a/src/qe_tools/outputs/parsers/pw.py +++ b/src/qe_tools/outputs/parsers/pw.py @@ -58,6 +58,7 @@ def parse(content): r"([\-\d.E+]+)\s+([\-\d.E+]+)" ) _HOMO_RE = re.compile(r"highest occupied level\s*\(ev\):\s*([\-\d.E+]+)") +_TOTAL_ENERGY_RE = re.compile(r"!\s+total energy\s*=\s*([\-\d.E+]+)\s*Ry") class PwStdoutParser(BaseStdoutParser): @@ -78,4 +79,10 @@ def parse(content: str) -> dict: if match: parsed_data["highest_occupied_level"] = float(match.group(1)) + # Final SCF total energy in Ry. For relax/md runs QE prints one `!` line + # per ionic step; the final converged value is the last one. + energy_matches = _TOTAL_ENERGY_RE.findall(content) + if energy_matches: + parsed_data["total_energy"] = float(energy_matches[-1]) + return parsed_data diff --git a/src/qe_tools/outputs/pw.py b/src/qe_tools/outputs/pw.py index f185c79..68288a3 100644 --- a/src/qe_tools/outputs/pw.py +++ b/src/qe_tools/outputs/pw.py @@ -6,7 +6,7 @@ from typing import Annotated, TextIO import numpy as np -from glom import Coalesce, Spec +from glom import Check, Coalesce, Spec from dough import Unit from dough.converters import BaseConverter @@ -324,9 +324,19 @@ class _PwMapping: total_energy: Annotated[ float, Spec( - ( - "xml.output.total_energy.etot", - lambda energy: energy * CONSTANTS.hartree_to_ev, + Coalesce( + ( + # For `nscf` and `bands` QE never assigns `etot` and writes + # `0.0` (see PW/src/non_scf.f90 and + # PW/src/pw_restart_new.f90); fall through to stdout below. + Check( + "xml.input.control_variables.calculation", + one_of=("scf", "relax", "vc-relax", "md", "vc-md"), + ), + "xml.output.total_energy.etot", + lambda energy: energy * CONSTANTS.hartree_to_ev, + ), + ("stdout.total_energy", lambda energy: energy * CONSTANTS.ry_to_ev), ) ), Unit("eV"), diff --git a/tests/outputs/fixtures/pw/nscf_etot_clobber/data-file-schema.xml b/tests/outputs/fixtures/pw/nscf_etot_clobber/data-file-schema.xml new file mode 100644 index 0000000..2610794 --- /dev/null +++ b/tests/outputs/fixtures/pw/nscf_etot_clobber/data-file-schema.xml @@ -0,0 +1,236 @@ + + + + + QEXSD_25.05.21 + XML file generated by PWSCF + This run was terminated on: 16:24: 5 7 May 2026 + + + + 1 + 1 + 1 + 1 + 1 + 1 + + + + + nscf + from_scratch + MgO + ../PP + ../tmp + false + false + true + low + 10000000 + 1 + 5.000000000000000E-005 + 5.000000000000000E-004 + 5.000000000000000E-001 + low + 100000 + false + false + + + + 2.403500000000000E+001 + Mg.pbe-n-kjpaw_psl.0.3.0.UPF + + + 1.599940000000000E+001 + O.pbe-n-kjpaw_psl.0.1.UPF + + + + + 0.000000000000000E+000 0.000000000000000E+000 0.000000000000000E+000 + 4.018727859100000E+000 4.018727859100000E+000 4.018727859100000E+000 + + + -4.018727859100000E+000 0.000000000000000E+000 4.018727859100000E+000 + 0.000000000000000E+000 4.018727859100000E+000 4.018727859100000E+000 + -4.018727859100000E+000 4.018727859100000E+000 0.000000000000000E+000 + + + + PBE + + + false + false + false + + + 8 + 0.000000000000000E+000 + tetrahedra + + + false + 2.500000000000000E+001 + 2.000000000000000E+002 + + + davidson + plain + 7.000000000000000E-001 + 5.000000000000000E-007 + 8 + 100 + 100 + false + false + false + false + 0.000000000000000E+000 + false + 20 + 4 + 16 + false + + + Monkhorst-Pack + + + none + 1.000000000000000E+002 + false + false + + + none + 0.000000000000000E+000 + 0.000000000000000E+000 + all + + + false + false + false + false + false + false + + + 1 1 1 + 1 1 1 + + + false + 0 + 0.000000000000000E+000 + 0.000000000000000E+000 + + + + + + false + 1 + 0.000000000000000E+000 + + true + + + false + false + true + true + + + + 2.403500000000000E+001 + Mg.pbe-n-kjpaw_psl.0.3.0.UPF + + + 1.599940000000000E+001 + O.pbe-n-kjpaw_psl.0.1.UPF + + + + + + 0.000000000000000E+000 0.000000000000000E+000 0.000000000000000E+000 + + 4.018727859100000E+000 4.018727859100000E+000 4.018727859100000E+000 + + + -4.018727859100000E+000 0.000000000000000E+000 4.018727859100000E+000 + 0.000000000000000E+000 4.018727859100000E+000 4.018727859100000E+000 + -4.018727859100000E+000 4.018727859100000E+000 0.000000000000000E+000 + + + + false + 2.500000000000000E+001 + 2.000000000000000E+002 + + + + 17477 + 6183 + 790 + + + -1.000000000000000E+000 -1.000000000000000E+000 1.000000000000000E+000 + + 1.000000000000000E+000 1.000000000000000E+000 1.000000000000000E+000 + -1.000000000000000E+000 1.000000000000000E+000 -1.000000000000000E+000 + + + + PBE + + + false + false + false + 0.000000000000000E+000 + + + 0.000000000000000E+000 + -2.412528325153409E-002 + 5.173955844516139E+000 + -5.381547635132835E+000 + -4.655081273351107E+000 + 0.000000000000000E+000 + + + false + false + false + 8 + 8.000000000000000E+000 + 2.208720474420502E-001 + + Monkhorst-Pack + + 145 + tetrahedra + + 0.000000000000000E+000 0.000000000000000E+000 0.000000000000000E+000 + 749 + + -4.333761183969641E-001 1.964059244895351E-001 1.964059244907791E-001 + 1.964059244947730E-001 3.597282634048695E-001 + 7.717057859626861E-001 7.717057860499605E-001 7.717057861123908E-001 + + + 9.999999999999996E-001 9.999999999999996E-001 9.999999999999996E-001 + 9.999999999999996E-001 0.000000000000000E+000 + 0.000000000000000E+000 0.000000000000000E+000 0.000000000000000E+000 + + + + + 0 + 0 + + diff --git a/tests/outputs/fixtures/pw/nscf_etot_clobber/scf.out b/tests/outputs/fixtures/pw/nscf_etot_clobber/scf.out new file mode 100644 index 0000000..b231db9 --- /dev/null +++ b/tests/outputs/fixtures/pw/nscf_etot_clobber/scf.out @@ -0,0 +1,12 @@ + Program PWSCF v.7.5 starts on 7May2026 at 16:23:36 + +--- Removed: SCF cycle, eigenvalues, k-points; only keep header + final energies + JOB DONE. --- + + highest occupied level (ev): 5.3452 + +! total energy = -75.53725762 Ry + total all-electron energy = -551.438359 Ry + +=------------------------------------------------------------------------------= + JOB DONE. +=------------------------------------------------------------------------------= diff --git a/tests/outputs/test_pw_output.py b/tests/outputs/test_pw_output.py index ea316fc..b9a7b76 100644 --- a/tests/outputs/test_pw_output.py +++ b/tests/outputs/test_pw_output.py @@ -73,6 +73,32 @@ def test_failed(data_regression, to_jsonable, fixture_directory): ) +def test_total_energy_nscf_clobber(): + """Test that `total_energy` falls back to stdout when an nscf XML clobbered the scf one. + + QE writes `0.0` for `nscf` and `bands` runs (see PW/src/non_scf.f90). + In the common `scf → nscf` workflow at a shared `prefix`, the nscf run overwrites + the scf XML on disk, so the only reliable source of the SCF total energy is the + `! total energy = ...` line in the scf stdout. + """ + from qe_tools import CONSTANTS + + pw_directory = Path(__file__).parent / "fixtures" / "pw" / "nscf_etot_clobber" + + pw_out = PwOutput.from_dir(pw_directory) + + # Sanity check: the fixture really is an nscf XML with the bogus etot. + assert ( + pw_out.raw_outputs["xml"]["input"]["control_variables"]["calculation"] == "nscf" + ) + assert pw_out.raw_outputs["xml"]["output"]["total_energy"]["etot"] == 0.0 + + # Despite that, `total_energy` should resolve via the stdout fallback. + assert pw_out.get_output("total_energy") == pytest.approx( + -75.53725762 * CONSTANTS.ry_to_ev + ) + + def test_insulator_homo(robust_data_regression_check): """Test stdout-derived `highest_occupied_level` from an insulator SCF.""" diff --git a/tests/outputs/test_pw_output/test_default_xml_211101_.yml b/tests/outputs/test_pw_output/test_default_xml_211101_.yml index e982a23..bfff936 100644 --- a/tests/outputs/test_pw_output/test_default_xml_211101_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_211101_.yml @@ -99,6 +99,7 @@ base_outputs: raw_outputs: stdout: code_version: '7.0' + total_energy: -22.13271119 wall_time_seconds: 1.96 xml: '@Units': Hartree atomic units diff --git a/tests/outputs/test_pw_output/test_default_xml_220603_.yml b/tests/outputs/test_pw_output/test_default_xml_220603_.yml index 367a5cb..bc73511 100644 --- a/tests/outputs/test_pw_output/test_default_xml_220603_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_220603_.yml @@ -115,6 +115,7 @@ base_outputs: raw_outputs: stdout: code_version: '7.1' + total_energy: -22.65373597 wall_time_seconds: 2.02 xml: '@Units': Hartree atomic units diff --git a/tests/outputs/test_pw_output/test_default_xml_230310_.yml b/tests/outputs/test_pw_output/test_default_xml_230310_.yml index 8cc0629..960fe30 100644 --- a/tests/outputs/test_pw_output/test_default_xml_230310_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_230310_.yml @@ -167,6 +167,7 @@ raw_outputs: stdout: code_version: '7.2' highest_occupied_level: 9.0922 + total_energy: -396.09758618 wall_time_seconds: 100.44 xml: '@Units': Hartree atomic units diff --git a/tests/outputs/test_pw_output/test_default_xml_240411_.yml b/tests/outputs/test_pw_output/test_default_xml_240411_.yml index 30f220b..ad12c09 100644 --- a/tests/outputs/test_pw_output/test_default_xml_240411_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_240411_.yml @@ -151,6 +151,7 @@ base_outputs: raw_outputs: stdout: code_version: 7.3.1 + total_energy: -22.66279398 wall_time_seconds: 2.51 xml: '@Units': Hartree atomic units diff --git a/tests/outputs/test_pw_output/test_default_xml_250521_.yml b/tests/outputs/test_pw_output/test_default_xml_250521_.yml index 99fe7b4..a5e2b9e 100644 --- a/tests/outputs/test_pw_output/test_default_xml_250521_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_250521_.yml @@ -150,6 +150,7 @@ base_outputs: raw_outputs: stdout: code_version: '7.5' + total_energy: -252.68718165 wall_time_seconds: 2.33 xml: '@Units': Hartree atomic units diff --git a/tests/outputs/test_pw_output/test_success_base_collinear_.yml b/tests/outputs/test_pw_output/test_success_base_collinear_.yml index 9234426..85adb8f 100644 --- a/tests/outputs/test_pw_output/test_success_base_collinear_.yml +++ b/tests/outputs/test_pw_output/test_success_base_collinear_.yml @@ -109,5 +109,4 @@ base_outputs: - F - Li - F - total_energy: 0.0 total_magnetization: 0.0