Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions calphy/input.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,49 @@ def _to_float(val):
return [float(x) for x in val]


def _extract_elements_from_pair_coeff(pair_coeff_string):
"""
Extract element symbols from pair_coeff string.
Returns None if pair_coeff doesn't contain element specifications.

Parameters
----------
pair_coeff_string : str
The pair_coeff command string (e.g., "* * potential.eam.fs Cu Zr")

Returns
-------
list or None
List of element symbols in order, or None if no elements found
"""
if pair_coeff_string is None:
return None

pcsplit = pair_coeff_string.strip().split()
elements = []

# Start collecting after we find element symbols
# Elements are typically after the potential filename
started = False

for p in pcsplit:
# Check if this looks like an element symbol
# Element symbols are 1-2 characters, start with uppercase
if len(p) <= 2 and p[0].isupper():
try:
# Verify it's a valid element using mendeleev
_ = mendeleev.element(p)
elements.append(p)
started = True
except:
# Not a valid element, might be done collecting
if started:
# We already started collecting elements and hit a non-element
break

return elements if len(elements) > 0 else None


class UFMP(BaseModel, title="UFM potential input options"):
p: Annotated[float, Field(default=50.0)]
sigma: Annotated[float, Field(default=1.5)]
Expand Down Expand Up @@ -313,6 +356,39 @@ def _validate_all(self) -> "Input":
raise ValueError("mass and elements should have same length")

self.n_elements = len(self.element)

# Validate element/mass/pair_coeff ordering consistency
# This is critical for multi-element systems where LAMMPS type numbers
# are assigned based on element order: element[0]=Type1, element[1]=Type2, etc.
if len(self.element) > 1 and self.pair_coeff is not None and len(self.pair_coeff) > 0:
extracted_elements = _extract_elements_from_pair_coeff(self.pair_coeff[0])

if extracted_elements is not None:
# pair_coeff specifies elements - check ordering
if set(extracted_elements) != set(self.element):
raise ValueError(
f"Element mismatch between 'element' and 'pair_coeff'!\n"
f" element: {self.element}\n"
f" pair_coeff: {extracted_elements}\n"
f"The elements specified must be the same."
)

if list(extracted_elements) != list(self.element):
raise ValueError(
f"Element ordering mismatch detected!\n\n"
f" element: {self.element}\n"
f" pair_coeff: {extracted_elements}\n"
f" mass: {self.mass}\n\n"
f"For multi-element systems, all three must be in the SAME order.\n\n"
f"Why this matters:\n"
f" - Element order determines LAMMPS type numbers:\n"
f" element[0] → Type 1, element[1] → Type 2, etc.\n"
f" - The pair_coeff elements must match this type order\n"
f" - The mass values must correspond to the same order\n"
f" - Composition transformations depend on this ordering\n\n"
f"Please reorder your input so element, mass, and pair_coeff\n"
f"all use the same element ordering."
)

self._pressure_input = copy.copy(self.pressure)
if self.pressure is None:
Expand Down
141 changes: 141 additions & 0 deletions tests/test_element_ordering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import pytest
from calphy.input import Calculation, read_inputfile


def test_correct_element_ordering():
"""Test that correct element ordering is accepted"""
calc_dict = {
'element': ['Cu', 'Zr'],
'mass': [63.546, 91.224],
'pair_coeff': ['* * potential.eam.fs Cu Zr'],
'pair_style': ['eam/fs'],
'mode': 'fe',
'temperature': 1000,
'pressure': 0,
'lattice': 'tests/conf1.data', # Use existing data file
}

# Should not raise any exception
calc = Calculation(**calc_dict)
assert calc.element == ['Cu', 'Zr']
assert calc.mass == [63.546, 91.224]


def test_wrong_element_ordering():
"""Test that mismatched element ordering is rejected"""
calc_dict = {
'element': ['Cu', 'Zr'],
'mass': [63.546, 91.224],
'pair_coeff': ['* * potential.eam.fs Zr Cu'], # Wrong order!
'pair_style': ['eam/fs'],
'mode': 'fe',
'temperature': 1000,
'pressure': 0,
'lattice': 'fcc',
'lattice_constant': 3.61,
}

with pytest.raises(ValueError) as exc_info:
calc = Calculation(**calc_dict)

assert 'ordering mismatch' in str(exc_info.value).lower()


def test_single_element_no_ordering_issue():
"""Test that single element systems don't trigger ordering validation"""
calc_dict = {
'element': ['Cu'],
'mass': [63.546],
'pair_coeff': ['* * potential.eam Cu'],
'pair_style': ['eam'],
'mode': 'fe',
'temperature': 1000,
'pressure': 0,
'lattice': 'fcc',
'lattice_constant': 3.61,
}

# Should not raise any exception for single element
calc = Calculation(**calc_dict)
assert calc.element == ['Cu']


def test_no_elements_in_pair_coeff():
"""Test that pair_coeff without element names skips validation"""
calc_dict = {
'element': ['Cu', 'Zr'],
'mass': [63.546, 91.224],
'pair_coeff': ['* * potential.eam'], # No elements specified
'pair_style': ['eam'],
'mode': 'fe',
'temperature': 1000,
'pressure': 0,
'lattice': 'tests/conf1.data', # Use existing data file
}

# Should not raise exception when pair_coeff has no elements
calc = Calculation(**calc_dict)
assert calc.element == ['Cu', 'Zr']


def test_element_mismatch():
"""Test that completely different elements in pair_coeff are rejected"""
calc_dict = {
'element': ['Cu', 'Zr'],
'mass': [63.546, 91.224],
'pair_coeff': ['* * potential.eam.fs Al Ni'], # Different elements!
'pair_style': ['eam/fs'],
'mode': 'fe',
'temperature': 1000,
'pressure': 0,
'lattice': 'fcc',
'lattice_constant': 3.61,
}

with pytest.raises(ValueError) as exc_info:
calc = Calculation(**calc_dict)

assert 'element mismatch' in str(exc_info.value).lower()


def test_three_element_ordering():
"""Test ordering validation works for 3+ elements"""
# Correct ordering
calc_dict = {
'element': ['Cu', 'Zr', 'Al'],
'mass': [63.546, 91.224, 26.982],
'pair_coeff': ['* * potential.eam.fs Cu Zr Al'],
'pair_style': ['eam/fs'],
'mode': 'fe',
'temperature': 1000,
'pressure': 0,
'lattice': 'tests/conf1.data', # Use existing data file
}

calc = Calculation(**calc_dict)
assert calc.element == ['Cu', 'Zr', 'Al']

# Wrong ordering
calc_dict_wrong = {
'element': ['Cu', 'Zr', 'Al'],
'mass': [63.546, 91.224, 26.982],
'pair_coeff': ['* * potential.eam.fs Al Cu Zr'], # Different order
'pair_style': ['eam/fs'],
'mode': 'fe',
'temperature': 1000,
'pressure': 0,
'lattice': 'tests/conf1.data', # Use existing data file
}

with pytest.raises(ValueError) as exc_info:
calc = Calculation(**calc_dict_wrong)

assert 'ordering mismatch' in str(exc_info.value).lower()


def test_existing_example_files():
"""Test that existing example files still load correctly"""
# Single element examples should work
calcs = read_inputfile('tests/input.yaml')
assert len(calcs) > 0
assert calcs[0].element is not None
Loading