Skip to content

Commit 45e8bd4

Browse files
alibh95pre-commit-ci[bot]shyuep
authored
fix(outputs.py): handle missing trailing newline in ICOHPLIST.lobster (#4350)
* fix(outputs.py): handle missing trailing newline in ICOHPLIST.lobster LOBSTER 5.1.1 sometimes produces an ICOHPLIST.lobster file without a trailing newline, which causes the Icohplist parser to miscount bond entries (e.g., 1079 vs. 1080). This commit updates the file-reading logic in Icohplist.__init__ to use splitlines() and filters out any blank lines, ensuring that only valid, non-empty lines are parsed. This change prevents the ValueError ("COHPCAR and ICOHPLIST do not fit together") and aligns the bond count with COHPCAR.lobster. Ref: See related issue in lobsterpy (#389) for additional context. Signed-off-by: Ali Hussain Umar Bhatti <[email protected]> * pre-commit auto-fixes * Update outputs.py * Add test_missing_trailing_newline * Robust header detection and version parsing in Icohplist - Strip only trailing blank lines so a missing final newline still works - Compute header length dynamically (skip title and optional spin‐line) - Determine LOBSTER version by column count (6→2.2.1, 8→3.1.1, 9→5.1.0) - Preserve non‐orbitalwise LCFO entries (fixes length mismatch for non-orbitalwise LCFO lists) --------- Signed-off-by: Ali Hussain Umar Bhatti <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Shyue Ping Ong <[email protected]>
1 parent 9dfe704 commit 45e8bd4

File tree

2 files changed

+59
-21
lines changed

2 files changed

+59
-21
lines changed

src/pymatgen/io/lobster/outputs.py

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -405,24 +405,35 @@ def __init__(
405405
# and we don't need the header.
406406
if self._icohpcollection is None:
407407
with zopen(self._filename, mode="rt", encoding="utf-8") as file:
408-
all_lines = file.read().split("\n")
409-
lines = all_lines[1:-1] if "spin" not in all_lines[1] else all_lines[2:-1]
410-
if len(lines) == 0:
411-
raise RuntimeError("ICOHPLIST file contains no data.")
412-
413-
# Determine LOBSTER version
414-
if len(lines[0].split()) == 8 and "spin" not in all_lines[1]:
415-
version = "3.1.1"
416-
elif (len(lines[0].split()) == 8 or len(lines[0].split()) == 9) and "spin" in all_lines[1]:
417-
version = "5.1.0"
418-
elif len(lines[0].split()) == 6:
419-
version = "2.2.1"
420-
warnings.warn(
421-
"Please consider using a newer LOBSTER version. See www.cohp.de.",
422-
stacklevel=2,
423-
)
424-
else:
425-
raise ValueError("Unsupported LOBSTER version.")
408+
all_lines = file.read().splitlines()
409+
410+
# strip *trailing* blank lines only
411+
all_lines = [line for line in all_lines if line.strip()]
412+
# --- detect header length robustly ---
413+
header_len = 0
414+
try:
415+
int(all_lines[0].split()[0])
416+
except ValueError:
417+
header_len += 1
418+
if header_len < len(all_lines) and "spin" in all_lines[header_len].lower():
419+
header_len += 1
420+
lines = all_lines[header_len:]
421+
if not lines:
422+
raise RuntimeError("ICOHPLIST file contains no data.")
423+
# --- version by column count only ---
424+
ncol = len(lines[0].split())
425+
if ncol == 6:
426+
version = "2.2.1"
427+
warnings.warn(
428+
"Please consider using a newer LOBSTER version. See www.cohp.de.",
429+
stacklevel=2,
430+
)
431+
elif ncol == 8:
432+
version = "3.1.1"
433+
elif ncol == 9:
434+
version = "5.1.0"
435+
else:
436+
raise ValueError(f"Unsupported LOBSTER version ({ncol} columns).")
426437

427438
# If the calculation is spin polarized, the line in the middle
428439
# of the file will be another header line.
@@ -587,6 +598,10 @@ def icohplist(self) -> dict[Any, dict[str, Any]]:
587598
"translation": value._translation,
588599
"orbitals": value._orbitals,
589600
}
601+
602+
# for LCFO only files drop the single orbital resolved entry when not in orbitalwise mode
603+
if self.is_lcfo and not self.orbitalwise:
604+
icohp_dict = {k: d for k, d in icohp_dict.items() if d.get("orbitals") is None}
590605
return icohp_dict
591606

592607
@property
@@ -1720,7 +1735,12 @@ def has_good_quality_check_occupied_bands(
17201735
raise ValueError("number_occ_bands_spin_down has to be specified")
17211736

17221737
for spin in (Spin.up, Spin.down) if spin_polarized else (Spin.up,):
1723-
num_occ_bands = number_occ_bands_spin_up if spin is Spin.up else number_occ_bands_spin_down
1738+
if spin is Spin.up:
1739+
num_occ_bands = number_occ_bands_spin_up
1740+
else:
1741+
if number_occ_bands_spin_down is None:
1742+
raise ValueError("number_occ_bands_spin_down has to be specified")
1743+
num_occ_bands = number_occ_bands_spin_down
17241744

17251745
for overlap_matrix in self.band_overlaps_dict[spin]["matrices"]:
17261746
sub_array = np.asarray(overlap_matrix)[:num_occ_bands, :num_occ_bands]
@@ -2333,7 +2353,7 @@ def _parse_matrix(
23332353
file_data: list[str],
23342354
pattern: str,
23352355
e_fermi: float,
2336-
) -> tuple[list[float], dict, dict]:
2356+
) -> tuple[list[np.ndarray], dict[Any, Any], dict[Any, Any]]:
23372357
complex_matrices: dict = {}
23382358
matrix_diagonal_values = []
23392359
start_inxs_real = []

tests/io/lobster/test_outputs.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import copy
44
import json
55
import os
6+
import tempfile
67

78
import numpy as np
89
import pytest
@@ -1955,7 +1956,7 @@ def test_attributes(self):
19551956
assert self.icohp_lcfo.is_spin_polarized
19561957
assert len(self.icohp_lcfo.icohplist) == 28
19571958
assert not self.icohp_lcfo_non_orbitalwise.orbitalwise
1958-
assert len(self.icohp_lcfo_non_orbitalwise.icohplist) == 27
1959+
assert len(self.icohp_lcfo_non_orbitalwise.icohplist) == 28
19591960

19601961
def test_values(self):
19611962
icohplist_bise = {
@@ -2171,6 +2172,23 @@ def test_msonable(self):
21712172
else:
21722173
assert getattr(icohplist_from_dict, attr_name) == attr_value
21732174

2175+
def test_missing_trailing_newline(self):
2176+
content = (
2177+
"1 Co1 O1 1.00000 0 0 0 -0.50000 -1.00000\n"
2178+
"2 Co2 O2 1.10000 0 0 0 -0.60000 -1.10000"
2179+
)
2180+
2181+
with tempfile.NamedTemporaryFile("w+", delete=False) as tmp:
2182+
tmp.write(content)
2183+
tmp.flush()
2184+
fname = tmp.name
2185+
try:
2186+
ip = Icohplist(filename=fname)
2187+
assert len(ip.icohplist) == 2
2188+
assert ip.icohplist["1"]["icohp"][Spin.up] == approx(-0.5)
2189+
finally:
2190+
os.remove(fname)
2191+
21742192

21752193
class TestNciCobiList:
21762194
def setup_method(self):

0 commit comments

Comments
 (0)