Skip to content

Commit 8988664

Browse files
authored
Merge pull request #196 from ICAMS/el_order
update input to check element ordering explicitely
2 parents 1739be7 + 612edfc commit 8988664

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed

calphy/input.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,49 @@ def _to_float(val):
9292
return [float(x) for x in val]
9393

9494

95+
def _extract_elements_from_pair_coeff(pair_coeff_string):
96+
"""
97+
Extract element symbols from pair_coeff string.
98+
Returns None if pair_coeff doesn't contain element specifications.
99+
100+
Parameters
101+
----------
102+
pair_coeff_string : str
103+
The pair_coeff command string (e.g., "* * potential.eam.fs Cu Zr")
104+
105+
Returns
106+
-------
107+
list or None
108+
List of element symbols in order, or None if no elements found
109+
"""
110+
if pair_coeff_string is None:
111+
return None
112+
113+
pcsplit = pair_coeff_string.strip().split()
114+
elements = []
115+
116+
# Start collecting after we find element symbols
117+
# Elements are typically after the potential filename
118+
started = False
119+
120+
for p in pcsplit:
121+
# Check if this looks like an element symbol
122+
# Element symbols are 1-2 characters, start with uppercase
123+
if len(p) <= 2 and p[0].isupper():
124+
try:
125+
# Verify it's a valid element using mendeleev
126+
_ = mendeleev.element(p)
127+
elements.append(p)
128+
started = True
129+
except:
130+
# Not a valid element, might be done collecting
131+
if started:
132+
# We already started collecting elements and hit a non-element
133+
break
134+
135+
return elements if len(elements) > 0 else None
136+
137+
95138
class UFMP(BaseModel, title="UFM potential input options"):
96139
p: Annotated[float, Field(default=50.0)]
97140
sigma: Annotated[float, Field(default=1.5)]
@@ -313,6 +356,39 @@ def _validate_all(self) -> "Input":
313356
raise ValueError("mass and elements should have same length")
314357

315358
self.n_elements = len(self.element)
359+
360+
# Validate element/mass/pair_coeff ordering consistency
361+
# This is critical for multi-element systems where LAMMPS type numbers
362+
# are assigned based on element order: element[0]=Type1, element[1]=Type2, etc.
363+
if len(self.element) > 1 and self.pair_coeff is not None and len(self.pair_coeff) > 0:
364+
extracted_elements = _extract_elements_from_pair_coeff(self.pair_coeff[0])
365+
366+
if extracted_elements is not None:
367+
# pair_coeff specifies elements - check ordering
368+
if set(extracted_elements) != set(self.element):
369+
raise ValueError(
370+
f"Element mismatch between 'element' and 'pair_coeff'!\n"
371+
f" element: {self.element}\n"
372+
f" pair_coeff: {extracted_elements}\n"
373+
f"The elements specified must be the same."
374+
)
375+
376+
if list(extracted_elements) != list(self.element):
377+
raise ValueError(
378+
f"Element ordering mismatch detected!\n\n"
379+
f" element: {self.element}\n"
380+
f" pair_coeff: {extracted_elements}\n"
381+
f" mass: {self.mass}\n\n"
382+
f"For multi-element systems, all three must be in the SAME order.\n\n"
383+
f"Why this matters:\n"
384+
f" - Element order determines LAMMPS type numbers:\n"
385+
f" element[0] → Type 1, element[1] → Type 2, etc.\n"
386+
f" - The pair_coeff elements must match this type order\n"
387+
f" - The mass values must correspond to the same order\n"
388+
f" - Composition transformations depend on this ordering\n\n"
389+
f"Please reorder your input so element, mass, and pair_coeff\n"
390+
f"all use the same element ordering."
391+
)
316392

317393
self._pressure_input = copy.copy(self.pressure)
318394
if self.pressure is None:

tests/test_element_ordering.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import pytest
2+
from calphy.input import Calculation, read_inputfile
3+
4+
5+
def test_correct_element_ordering():
6+
"""Test that correct element ordering is accepted"""
7+
calc_dict = {
8+
'element': ['Cu', 'Zr'],
9+
'mass': [63.546, 91.224],
10+
'pair_coeff': ['* * potential.eam.fs Cu Zr'],
11+
'pair_style': ['eam/fs'],
12+
'mode': 'fe',
13+
'temperature': 1000,
14+
'pressure': 0,
15+
'lattice': 'tests/conf1.data', # Use existing data file
16+
}
17+
18+
# Should not raise any exception
19+
calc = Calculation(**calc_dict)
20+
assert calc.element == ['Cu', 'Zr']
21+
assert calc.mass == [63.546, 91.224]
22+
23+
24+
def test_wrong_element_ordering():
25+
"""Test that mismatched element ordering is rejected"""
26+
calc_dict = {
27+
'element': ['Cu', 'Zr'],
28+
'mass': [63.546, 91.224],
29+
'pair_coeff': ['* * potential.eam.fs Zr Cu'], # Wrong order!
30+
'pair_style': ['eam/fs'],
31+
'mode': 'fe',
32+
'temperature': 1000,
33+
'pressure': 0,
34+
'lattice': 'fcc',
35+
'lattice_constant': 3.61,
36+
}
37+
38+
with pytest.raises(ValueError) as exc_info:
39+
calc = Calculation(**calc_dict)
40+
41+
assert 'ordering mismatch' in str(exc_info.value).lower()
42+
43+
44+
def test_single_element_no_ordering_issue():
45+
"""Test that single element systems don't trigger ordering validation"""
46+
calc_dict = {
47+
'element': ['Cu'],
48+
'mass': [63.546],
49+
'pair_coeff': ['* * potential.eam Cu'],
50+
'pair_style': ['eam'],
51+
'mode': 'fe',
52+
'temperature': 1000,
53+
'pressure': 0,
54+
'lattice': 'fcc',
55+
'lattice_constant': 3.61,
56+
}
57+
58+
# Should not raise any exception for single element
59+
calc = Calculation(**calc_dict)
60+
assert calc.element == ['Cu']
61+
62+
63+
def test_no_elements_in_pair_coeff():
64+
"""Test that pair_coeff without element names skips validation"""
65+
calc_dict = {
66+
'element': ['Cu', 'Zr'],
67+
'mass': [63.546, 91.224],
68+
'pair_coeff': ['* * potential.eam'], # No elements specified
69+
'pair_style': ['eam'],
70+
'mode': 'fe',
71+
'temperature': 1000,
72+
'pressure': 0,
73+
'lattice': 'tests/conf1.data', # Use existing data file
74+
}
75+
76+
# Should not raise exception when pair_coeff has no elements
77+
calc = Calculation(**calc_dict)
78+
assert calc.element == ['Cu', 'Zr']
79+
80+
81+
def test_element_mismatch():
82+
"""Test that completely different elements in pair_coeff are rejected"""
83+
calc_dict = {
84+
'element': ['Cu', 'Zr'],
85+
'mass': [63.546, 91.224],
86+
'pair_coeff': ['* * potential.eam.fs Al Ni'], # Different elements!
87+
'pair_style': ['eam/fs'],
88+
'mode': 'fe',
89+
'temperature': 1000,
90+
'pressure': 0,
91+
'lattice': 'fcc',
92+
'lattice_constant': 3.61,
93+
}
94+
95+
with pytest.raises(ValueError) as exc_info:
96+
calc = Calculation(**calc_dict)
97+
98+
assert 'element mismatch' in str(exc_info.value).lower()
99+
100+
101+
def test_three_element_ordering():
102+
"""Test ordering validation works for 3+ elements"""
103+
# Correct ordering
104+
calc_dict = {
105+
'element': ['Cu', 'Zr', 'Al'],
106+
'mass': [63.546, 91.224, 26.982],
107+
'pair_coeff': ['* * potential.eam.fs Cu Zr Al'],
108+
'pair_style': ['eam/fs'],
109+
'mode': 'fe',
110+
'temperature': 1000,
111+
'pressure': 0,
112+
'lattice': 'tests/conf1.data', # Use existing data file
113+
}
114+
115+
calc = Calculation(**calc_dict)
116+
assert calc.element == ['Cu', 'Zr', 'Al']
117+
118+
# Wrong ordering
119+
calc_dict_wrong = {
120+
'element': ['Cu', 'Zr', 'Al'],
121+
'mass': [63.546, 91.224, 26.982],
122+
'pair_coeff': ['* * potential.eam.fs Al Cu Zr'], # Different order
123+
'pair_style': ['eam/fs'],
124+
'mode': 'fe',
125+
'temperature': 1000,
126+
'pressure': 0,
127+
'lattice': 'tests/conf1.data', # Use existing data file
128+
}
129+
130+
with pytest.raises(ValueError) as exc_info:
131+
calc = Calculation(**calc_dict_wrong)
132+
133+
assert 'ordering mismatch' in str(exc_info.value).lower()
134+
135+
136+
def test_existing_example_files():
137+
"""Test that existing example files still load correctly"""
138+
# Single element examples should work
139+
calcs = read_inputfile('tests/input.yaml')
140+
assert len(calcs) > 0
141+
assert calcs[0].element is not None

0 commit comments

Comments
 (0)