Skip to content

Commit d235435

Browse files
committed
fix(abacus): reject conflicting duplicate species definitions
get_frame_from_stru merges repeated atom labels and keeps the metadata of the first matching row for masses, pp_files, and orb_files. A conflicting duplicate would otherwise be silently discarded on dump. Add validate_duplicate_species() to check duplicate rows for consistency and raise a clear error on conflict. Add STRU-conflict-atomtype.ch4 test data and regression tests covering the read path and all three metadata fields.
1 parent 94ac622 commit d235435

3 files changed

Lines changed: 117 additions & 0 deletions

File tree

dpdata/formats/abacus/stru.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,46 @@ def right_hand_rule(
441441
return cell, coord
442442

443443

444+
def validate_duplicate_species(atom_names, masses, pp_files, orb_files):
445+
"""Check that duplicate atom species definitions are consistent.
446+
447+
get_frame_from_stru merges repeated labels and keeps the metadata of the
448+
first matching row for masses, pp_files, and orb_files. If a duplicate
449+
row conflicts with the first one, raise an error instead of silently
450+
discarding the conflicting metadata.
451+
452+
Args:
453+
atom_names (list): list of atom names.
454+
masses (list): list of atomic masses.
455+
pp_files (list): list of pseudo potential files.
456+
orb_files (list): list of orbital files.
457+
458+
Raises
459+
------
460+
RuntimeError: if duplicate species have conflicting metadata.
461+
"""
462+
for name in dict.fromkeys(atom_names):
463+
indices = [j for j in range(len(atom_names)) if atom_names[j] == name]
464+
if len(indices) < 2:
465+
continue
466+
ref_mass = masses[indices[0]]
467+
ref_pp = pp_files[indices[0]]
468+
ref_orb = orb_files[indices[0]] if orb_files else None
469+
for j in indices[1:]:
470+
if not np.isclose(masses[j], ref_mass):
471+
raise RuntimeError(
472+
f"Conflicting duplicate species '{name}': mass {masses[j]} != {ref_mass}"
473+
)
474+
if pp_files[j] != ref_pp:
475+
raise RuntimeError(
476+
f"Conflicting duplicate species '{name}': pp_file {pp_files[j]} != {ref_pp}"
477+
)
478+
if ref_orb is not None and orb_files[j] != ref_orb:
479+
raise RuntimeError(
480+
f"Conflicting duplicate species '{name}': orb_file {orb_files[j]} != {ref_orb}"
481+
)
482+
483+
444484
def get_frame_from_stru(stru):
445485
"""Read the ABACUS STRU file and return the dpdata frame.
446486
@@ -492,6 +532,8 @@ def get_frame_from_stru(stru):
492532
blocks["ATOMIC_POSITIONS"], atom_names, celldm, cell
493533
)
494534

535+
validate_duplicate_species(atom_names, masses, pp_files, orb_files)
536+
495537
cell, coords = right_hand_rule(cell, coords)
496538
uniq_name = []
497539
uniq_atom_num = []
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
#This is the atom card containing all the information
2+
#about the lattice structure.
3+
4+
ATOMIC_SPECIES
5+
C 1.000 C_ONCV_PBE-1.0.upf #Element, Mass, Pseudopotential
6+
H 1.000 H_ONCV_PBE-1.0.upf
7+
H 2.000 H_ONCV_PBE-1.0.upf
8+
9+
NUMERICAL_ORBITAL
10+
c.orb
11+
h.orb
12+
h.orb
13+
14+
LATTICE_CONSTANT
15+
10 #Lattice constant
16+
17+
LATTICE_VECTORS
18+
1 0.0 0.0 #Lattice vector 1
19+
0.0 1 0.0 #Lattice vector 2
20+
0.0 0.0 1 #Lattice vector 3
21+
22+
ATOMIC_POSITIONS
23+
Cartesian #Cartesian(Unit is LATTICE_CONSTANT)
24+
C #Name of element
25+
0.0 #Magnetic for this element.
26+
1 #Number of atoms
27+
0.981274803 0.861285385 0.838442496 1 1 1
28+
H
29+
0.0
30+
2
31+
1.023557202 0.758025625 0.66351336 0 0 0
32+
0.78075702 0.889445935 0.837363468 1 0 1
33+
H
34+
0.0
35+
2
36+
1.064091613 1.043438905 0.840995502 1 0 1
37+
1.039321214 0.756530859 1.009609207 0 1 1

tests/test_abacus_stru_dump.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,44 @@ def test_dump_stru_with_repeat_atomtype(self):
287287
self.assertTrue("H\n0.0\n4\n" in c)
288288
os.remove("STRU_tmp")
289289

290+
def test_read_stru_with_conflicting_duplicate_atomtype(self):
291+
with self.assertRaisesRegex(
292+
RuntimeError,
293+
"Conflicting duplicate species 'H'.*mass 2.0 != 1.0",
294+
):
295+
dpdata.System("abacus.scf/STRU-conflict-atomtype.ch4", fmt="stru")
296+
297+
def test_validate_duplicate_species(self):
298+
from dpdata.formats.abacus.stru import validate_duplicate_species
299+
300+
validate_duplicate_species(
301+
["C", "H", "H"],
302+
[1.0, 1.0, 1.0],
303+
["C.upf", "H.upf", "H.upf"],
304+
["c.orb", "h.orb", "h.orb"],
305+
)
306+
with self.assertRaisesRegex(RuntimeError, "mass"):
307+
validate_duplicate_species(
308+
["C", "H", "H"],
309+
[1.0, 1.0, 2.0],
310+
["C.upf", "H.upf", "H.upf"],
311+
["c.orb", "h.orb", "h.orb"],
312+
)
313+
with self.assertRaisesRegex(RuntimeError, "pp_file"):
314+
validate_duplicate_species(
315+
["C", "H", "H"],
316+
[1.0, 1.0, 1.0],
317+
["C.upf", "H.upf", "O.upf"],
318+
["c.orb", "h.orb", "h.orb"],
319+
)
320+
with self.assertRaisesRegex(RuntimeError, "orb_file"):
321+
validate_duplicate_species(
322+
["C", "H", "H"],
323+
[1.0, 1.0, 1.0],
324+
["C.upf", "H.upf", "H.upf"],
325+
["c.orb", "h.orb", "o.orb"],
326+
)
327+
290328

291329
class TestABACUSParseStru(unittest.TestCase):
292330
def test_parse_pos_oneline(self):

0 commit comments

Comments
 (0)