diff --git a/src/qe_tools/outputs/parsers/pw.py b/src/qe_tools/outputs/parsers/pw.py index 99ca858..c5bf33e 100644 --- a/src/qe_tools/outputs/parsers/pw.py +++ b/src/qe_tools/outputs/parsers/pw.py @@ -59,6 +59,21 @@ def parse(content): ) _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") +_ALAT_RE = re.compile(r"lattice parameter \(alat\)\s*=\s*([\-\d.E+]+)\s*a\.u\.") + +# Captures the `number of k points= N` line plus the following +# `cart. coord. in units 2pi/alat` block of `k( i ) = ( x y z ), wk = w` lines. +# `[^\n]*` after the count tolerates trailing variants (`(tetrahedron method)`, +# `Marzari-Vanderbilt smearing, ...`, or nothing). +_KPOINTS_BLOCK_RE = re.compile( + r"number of k points=\s*(?P\d+)[^\n]*\n" + r"\s*cart\. coord\. in units 2pi/alat\s*\n" + r"(?P(?:\s*k\(\s*\d+\s*\)\s*=\s*\(\s*[\-\d.E+]+\s+[\-\d.E+]+\s+[\-\d.E+]+\s*\),\s*wk\s*=\s*[\-\d.E+]+\s*\n)+)" +) +_KPOINT_ROW_RE = re.compile( + r"k\(\s*\d+\s*\)\s*=\s*\(\s*([\-\d.E+]+)\s+([\-\d.E+]+)\s+([\-\d.E+]+)\s*\)," + r"\s*wk\s*=\s*([\-\d.E+]+)" +) class PwStdoutParser(BaseStdoutParser): @@ -85,4 +100,27 @@ def parse(content: str) -> dict: if energy_matches: parsed_data["total_energy"] = float(energy_matches[-1]) + # k-points: take the last block, since vc-relax reprints after relaxation. + # The stdout values are in 2pi/alat; convert to 1/Å using `alat` (also from + # stdout, in bohr) so downstream Specs match the XML-derived units. + kpoint_blocks = list(_KPOINTS_BLOCK_RE.finditer(content)) + alat_match = _ALAT_RE.search(content) + if kpoint_blocks and alat_match: + import math + + from qe_tools import CONSTANTS + + alat_ang = float(alat_match.group(1)) * CONSTANTS.bohr_to_ang + scale = 2 * math.pi / alat_ang + block = kpoint_blocks[-1] + cartesian = [] + weights = [] + for row in _KPOINT_ROW_RE.finditer(block.group("rows")): + kx, ky, kz, wk = (float(v) for v in row.groups()) + cartesian.append([kx * scale, ky * scale, kz * scale]) + weights.append(wk) + parsed_data["number_of_k_points"] = int(block.group("nks")) + parsed_data["k_points_cartesian"] = cartesian + parsed_data["k_points_weights"] = weights + return parsed_data diff --git a/src/qe_tools/outputs/pw.py b/src/qe_tools/outputs/pw.py index f02e16c..96aacf9 100644 --- a/src/qe_tools/outputs/pw.py +++ b/src/qe_tools/outputs/pw.py @@ -272,15 +272,26 @@ class _PwMapping: Only available when ``tot_magnetization`` is set in ``SYSTEM``. """ - number_of_k_points: Annotated[int, Spec("xml.output.band_structure.nks")] + number_of_k_points: Annotated[ + int, + Spec( + Coalesce( + "xml.output.band_structure.nks", + "stdout.number_of_k_points", + ) + ), + ] """Number of k-points at which the Kohn-Sham states were computed.""" k_points_weights: Annotated[ list, Spec( - ( - "xml.output.band_structure.ks_energies", - [lambda ks: ks["k_point"]["@weight"]], + Coalesce( + ( + "xml.output.band_structure.ks_energies", + [lambda ks: ks["k_point"]["@weight"]], + ), + "stdout.k_points_weights", ) ), ] @@ -292,18 +303,24 @@ class _PwMapping: k_points_cartesian: Annotated[ list, Spec( - ( - "xml.output", - lambda output: [ - [ - kp - * 2 - * math.pi - / (output["atomic_structure"]["@alat"] * CONSTANTS.bohr_to_ang) - for kp in ks["k_point"]["$"] - ] - for ks in output["band_structure"]["ks_energies"] - ], + Coalesce( + ( + "xml.output", + lambda output: [ + [ + kp + * 2 + * math.pi + / ( + output["atomic_structure"]["@alat"] + * CONSTANTS.bohr_to_ang + ) + for kp in ks["k_point"]["$"] + ] + for ks in output["band_structure"]["ks_energies"] + ], + ), + "stdout.k_points_cartesian", ) ), Unit("1/angstrom"), diff --git a/tests/outputs/fixtures/pw/default_xml_220603/pw.out b/tests/outputs/fixtures/pw/default_xml_220603/pw.out index 8ce8bdd..fc0779c 100644 --- a/tests/outputs/fixtures/pw/default_xml_220603/pw.out +++ b/tests/outputs/fixtures/pw/default_xml_220603/pw.out @@ -86,7 +86,12 @@ --- SYMMETRIES REMOVED --- ---- K-POINTS REMOVED --- + number of k points= 4 Marzari-Vanderbilt smearing, width (Ry)= 0.0100 + cart. coord. in units 2pi/alat + k( 1) = ( 0.0000000 0.0000000 0.0000000), wk = 0.2500000 + k( 2) = ( 0.0000000 0.0000000 -0.6123724), wk = 0.2500000 + k( 3) = ( 0.0000000 -0.5773503 0.2041241), wk = 0.7500000 + k( 4) = ( 0.0000000 -0.5773503 -0.4082483), wk = 0.7500000 --- MEMORY INFO REMOVED --- diff --git a/tests/outputs/fixtures/pw/default_xml_250521/pw.out b/tests/outputs/fixtures/pw/default_xml_250521/pw.out index c64de71..30c4410 100644 --- a/tests/outputs/fixtures/pw/default_xml_250521/pw.out +++ b/tests/outputs/fixtures/pw/default_xml_250521/pw.out @@ -95,6 +95,11 @@ F 7.00 18.99840 F ( 1.00) Li 3.00 6.94100 Li( 1.00) + number of k points= 2 Marzari-Vanderbilt smearing, width (Ry)= 0.0100 + cart. coord. in units 2pi/alat + k( 1) = ( 0.0000000 0.0000000 0.0000000), wk = 1.0000000 + k( 2) = ( -0.5000000 0.0000000 0.0000000), wk = 1.0000000 + Dense grid: 50733 G-vectors FFT dimensions: ( 45, 45, 45) Smooth grid: 17941 G-vectors FFT dimensions: ( 36, 36, 36) diff --git a/tests/outputs/test_pw_output.py b/tests/outputs/test_pw_output.py index b9a7b76..d578439 100644 --- a/tests/outputs/test_pw_output.py +++ b/tests/outputs/test_pw_output.py @@ -1,5 +1,6 @@ from pathlib import Path +import numpy as np import pytest from qe_tools.outputs.pw import PwOutput @@ -99,6 +100,44 @@ def test_total_energy_nscf_clobber(): ) +def test_k_points_stdout_xml_agree(): + """Stdout-derived k-points agree with the XML values within stdout's printed precision. + + QE prints k-points in `cart. coord. in units 2pi/alat` to 7 digits and `alat` to + 5 digits, so cartesian coordinates in 1/Å can only match the XML at ~1e-3. + """ + pw_directory = Path(__file__).parent / "fixtures" / "pw" / "default_xml_211101" + + pw_out = PwOutput.from_dir(pw_directory) + + assert pw_out.get_output("k_points_weights") == pytest.approx( + pw_out.raw_outputs["stdout"]["k_points_weights"] + ) + np.testing.assert_allclose( + pw_out.get_output("k_points_cartesian"), + pw_out.raw_outputs["stdout"]["k_points_cartesian"], + atol=1e-3, + ) + + +def test_k_points_stdout_fallback_without_xml(): + """`PwOutput.from_files(stdout=...)` exposes k-point outputs via the stdout fallback.""" + stdout_file = ( + Path(__file__).parent / "fixtures" / "pw" / "default_xml_211101" / "pw.out" + ) + + pw_out = PwOutput.from_files(stdout=stdout_file) + + assert "xml" not in pw_out.raw_outputs + assert pw_out.get_output("number_of_k_points") == 2 + assert len(pw_out.get_output("k_points_weights")) == pw_out.get_output( + "number_of_k_points" + ) + assert len(pw_out.get_output("k_points_cartesian")) == pw_out.get_output( + "number_of_k_points" + ) + + 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 9b61f76..45ce06f 100644 --- a/tests/outputs/test_pw_output/test_default_xml_211101_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_211101_.yml @@ -101,6 +101,17 @@ base_outputs: raw_outputs: stdout: code_version: '7.0' + k_points_cartesian: + - - 0.0 + - 0.0 + - 0.0 + - - 0.8124187117851777 + - 0.4690501057329147 + - 0.33166847673261757 + k_points_weights: + - 1.0 + - 1.0 + number_of_k_points: 2 total_energy: -22.13271119 volume_bohr3: 275.9279 wall_time_seconds: 1.96 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 95807b0..9e971a2 100644 --- a/tests/outputs/test_pw_output/test_default_xml_220603_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_220603_.yml @@ -117,6 +117,25 @@ base_outputs: raw_outputs: stdout: code_version: '7.1' + k_points_cartesian: + - - 0.0 + - 0.0 + - 0.0 + - - 0.0 + - 0.0 + - -0.9950055926815952 + - - 0.0 + - -0.9381003739495717 + - 0.33166847673261757 + - - 0.0 + - -0.9381003739495717 + - -0.6633371159489775 + k_points_weights: + - 0.25 + - 0.25 + - 0.75 + - 0.75 + number_of_k_points: 4 total_energy: -22.65373597 volume_bohr3: 275.9279 wall_time_seconds: 2.02 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 a97763e..1938e80 100644 --- a/tests/outputs/test_pw_output/test_default_xml_230310_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_230310_.yml @@ -169,6 +169,61 @@ raw_outputs: stdout: code_version: '7.2' highest_occupied_level: 9.0922 + k_points_cartesian: + - - 0.0 + - 0.0 + - 0.0 + - - -0.5577988981584857 + - -0.3220453424463573 + - 0.11177181862660332 + - - 1.1155977963169714 + - 0.6440906848927146 + - -0.22354363725320664 + - - -0.5577988981584857 + - 0.3220453424463573 + - 0.22354363725320664 + - - 1.1155977963169714 + - 1.2881813697854292 + - -0.11177181862660332 + - - 0.5577988981584857 + - 0.9661360273390719 + - 0.0 + - - 1.1155977963169714 + - -0.6440906848927146 + - -0.44708714779493075 + - - 0.0 + - 0.0 + - 0.33531545587981 + - - 1.673396694475457 + - 0.9661360273390719 + - 0.0 + - - 1.1155977963169714 + - 0.6440906848927146 + - 0.11177181862660332 + - - 1.673396694475457 + - -0.9661360273390719 + - -0.33531545587981 + - - 1.1155977963169714 + - -1.2881813697854292 + - -0.22354363725320664 + - - 0.0 + - 0.0 + - -0.6706307850481373 + k_points_weights: + - 0.03125 + - 0.1875 + - 0.09375 + - 0.1875 + - 0.375 + - 0.1875 + - 0.09375 + - 0.0625 + - 0.1875 + - 0.1875 + - 0.1875 + - 0.1875 + - 0.03125 + number_of_k_points: 13 total_energy: -396.09758618 volume_bohr3: 217.1091 wall_time_seconds: 100.44 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 d9a747a..2f7fd59 100644 --- a/tests/outputs/test_pw_output/test_default_xml_240411_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_240411_.yml @@ -153,6 +153,61 @@ base_outputs: raw_outputs: stdout: code_version: 7.3.1 + k_points_cartesian: + - - 0.0 + - 0.0 + - 0.0 + - - -0.0 + - -0.0 + - 0.49963377062181 + - - 0.0 + - 0.0 + - -0.9992677037273625 + - - -0.0 + - 0.47105921720715943 + - -0.16654453604602257 + - - -0.0 + - 0.47105921720715943 + - 0.3330892345757875 + - - 0.0 + - 0.47105921720715943 + - -1.165812239773385 + - - 0.0 + - 0.47105921720715943 + - -0.666178469151575 + - - 0.0 + - -0.9421184344143189 + - 0.3330892345757875 + - - 0.0 + - -0.9421184344143189 + - 0.8327230051975975 + - - 0.0 + - -0.9421184344143189 + - -0.666178469151575 + - - 0.40794923180574794 + - -1.1776480430178984 + - 0.16654453604602257 + - - 0.40794923180574794 + - -1.1776480430178984 + - 0.666178469151575 + - - 0.40794923180574794 + - -1.1776480430178984 + - -0.3330892345757875 + k_points_weights: + - 0.03125 + - 0.0625 + - 0.03125 + - 0.1875 + - 0.1875 + - 0.1875 + - 0.1875 + - 0.09375 + - 0.1875 + - 0.09375 + - 0.375 + - 0.1875 + - 0.1875 + number_of_k_points: 13 total_energy: -22.66279398 volume_bohr3: 272.4124 wall_time_seconds: 2.51 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 06cb845..cbfc64c 100644 --- a/tests/outputs/test_pw_output/test_default_xml_250521_.yml +++ b/tests/outputs/test_pw_output/test_default_xml_250521_.yml @@ -152,6 +152,17 @@ base_outputs: raw_outputs: stdout: code_version: '7.5' + k_points_cartesian: + - - 0.0 + - 0.0 + - 0.0 + - - -0.7795511497938687 + - 0.0 + - 0.0 + k_points_weights: + - 1.0 + - 1.0 + number_of_k_points: 2 total_energy: -252.68718165 volume_bohr3: 441.6841 wall_time_seconds: 2.33