|
6 | 6 | def test_read_structure_composition_single_element(): |
7 | 7 | """Test reading a structure with only one element type""" |
8 | 8 | # Use existing test structure file |
9 | | - structure_file = 'tests/conf1.data' |
10 | | - elements = ['Cu', 'Zr'] |
11 | | - |
| 9 | + structure_file = "tests/conf1.data" |
| 10 | + elements = ["Cu", "Zr"] |
| 11 | + |
12 | 12 | comp = read_structure_composition(structure_file, elements) |
13 | | - |
| 13 | + |
14 | 14 | # Should have Cu atoms and 0 Zr atoms |
15 | 15 | assert isinstance(comp, dict) |
16 | | - assert 'Cu' in comp |
17 | | - assert 'Zr' in comp |
18 | | - assert comp['Cu'] > 0 |
19 | | - assert comp['Zr'] == 0 |
| 16 | + assert "Cu" in comp |
| 17 | + assert "Zr" in comp |
| 18 | + assert comp["Cu"] > 0 |
| 19 | + assert comp["Zr"] == 0 |
20 | 20 | assert sum(comp.values()) > 0 |
21 | 21 |
|
22 | 22 |
|
23 | 23 | def test_read_structure_composition_element_not_in_structure(): |
24 | 24 | """Test that elements not present in structure get count 0""" |
25 | | - structure_file = 'tests/conf1.data' |
26 | | - elements = ['Cu', 'Zr', 'Al', 'Ni'] |
27 | | - |
| 25 | + structure_file = "tests/conf1.data" |
| 26 | + elements = ["Cu", "Zr", "Al", "Ni"] |
| 27 | + |
28 | 28 | comp = read_structure_composition(structure_file, elements) |
29 | | - |
| 29 | + |
30 | 30 | # Should have all elements with appropriate counts |
31 | 31 | assert len(comp) == 4 |
32 | | - assert comp['Cu'] > 0 |
33 | | - assert comp['Zr'] == 0 |
34 | | - assert comp['Al'] == 0 |
35 | | - assert comp['Ni'] == 0 |
| 32 | + assert comp["Cu"] > 0 |
| 33 | + assert comp["Zr"] == 0 |
| 34 | + assert comp["Al"] == 0 |
| 35 | + assert comp["Ni"] == 0 |
36 | 36 |
|
37 | 37 |
|
38 | 38 | def test_read_structure_composition_element_order_matters(): |
39 | 39 | """Test that element list order determines type mapping""" |
40 | | - structure_file = 'tests/conf1.data' |
41 | | - |
| 40 | + structure_file = "tests/conf1.data" |
| 41 | + |
42 | 42 | # Different element orders should give different results |
43 | 43 | # because element[0] maps to LAMMPS type 1, element[1] to type 2, etc. |
44 | | - elements1 = ['Cu', 'Zr'] |
45 | | - elements2 = ['Zr', 'Cu'] |
46 | | - |
| 44 | + elements1 = ["Cu", "Zr"] |
| 45 | + elements2 = ["Zr", "Cu"] |
| 46 | + |
47 | 47 | comp1 = read_structure_composition(structure_file, elements1) |
48 | 48 | comp2 = read_structure_composition(structure_file, elements2) |
49 | | - |
| 49 | + |
50 | 50 | # With Cu first, Cu should have the atoms |
51 | | - assert comp1['Cu'] > 0 |
52 | | - assert comp1['Zr'] == 0 |
53 | | - |
| 51 | + assert comp1["Cu"] > 0 |
| 52 | + assert comp1["Zr"] == 0 |
| 53 | + |
54 | 54 | # With Zr first, Zr should have the atoms (because type 1 maps to first element) |
55 | | - assert comp2['Zr'] > 0 |
56 | | - assert comp2['Cu'] == 0 |
| 55 | + assert comp2["Zr"] > 0 |
| 56 | + assert comp2["Cu"] == 0 |
57 | 57 |
|
58 | 58 |
|
59 | 59 | def test_read_structure_composition_total_atoms(): |
60 | 60 | """Test that total atom count is preserved regardless of element order""" |
61 | | - structure_file = 'tests/conf1.data' |
62 | | - |
63 | | - elements1 = ['Cu', 'Zr'] |
64 | | - elements2 = ['Zr', 'Cu'] |
65 | | - elements3 = ['Cu', 'Zr', 'Al'] |
66 | | - |
| 61 | + structure_file = "tests/conf1.data" |
| 62 | + |
| 63 | + elements1 = ["Cu", "Zr"] |
| 64 | + elements2 = ["Zr", "Cu"] |
| 65 | + elements3 = ["Cu", "Zr", "Al"] |
| 66 | + |
67 | 67 | comp1 = read_structure_composition(structure_file, elements1) |
68 | 68 | comp2 = read_structure_composition(structure_file, elements2) |
69 | 69 | comp3 = read_structure_composition(structure_file, elements3) |
70 | | - |
| 70 | + |
71 | 71 | # Total should be the same regardless of element list |
72 | 72 | total1 = sum(comp1.values()) |
73 | 73 | total2 = sum(comp2.values()) |
74 | 74 | total3 = sum(comp3.values()) |
75 | | - |
| 75 | + |
76 | 76 | assert total1 == total2 == total3 |
77 | 77 | assert total1 > 0 |
78 | 78 |
|
79 | 79 |
|
80 | 80 | def test_read_structure_composition_invalid_file(): |
81 | 81 | """Test that invalid file path raises appropriate error""" |
82 | 82 | with pytest.raises(Exception): |
83 | | - read_structure_composition('nonexistent_file.data', ['Cu', 'Zr']) |
| 83 | + read_structure_composition("nonexistent_file.data", ["Cu", "Zr"]) |
84 | 84 |
|
85 | 85 |
|
86 | 86 | def test_read_structure_composition_empty_element_list(): |
87 | 87 | """Test behavior with empty element list""" |
88 | | - structure_file = 'tests/conf1.data' |
| 88 | + structure_file = "tests/conf1.data" |
89 | 89 | elements = [] |
90 | | - |
| 90 | + |
91 | 91 | comp = read_structure_composition(structure_file, elements) |
92 | | - |
| 92 | + |
93 | 93 | # Should return empty dict |
94 | 94 | assert isinstance(comp, dict) |
95 | 95 | assert len(comp) == 0 |
96 | 96 |
|
97 | 97 |
|
98 | 98 | def test_read_structure_composition_single_element_list(): |
99 | 99 | """Test with single element in list""" |
100 | | - structure_file = 'tests/conf1.data' |
101 | | - elements = ['Cu'] |
102 | | - |
| 100 | + structure_file = "tests/conf1.data" |
| 101 | + elements = ["Cu"] |
| 102 | + |
103 | 103 | comp = read_structure_composition(structure_file, elements) |
104 | | - |
| 104 | + |
105 | 105 | assert len(comp) == 1 |
106 | | - assert 'Cu' in comp |
107 | | - assert comp['Cu'] > 0 |
| 106 | + assert "Cu" in comp |
| 107 | + assert comp["Cu"] > 0 |
108 | 108 |
|
109 | 109 |
|
110 | 110 | def test_composition_transformation_100_percent(): |
111 | 111 | """Test that 100% composition transformation is allowed (pure phase endpoint)""" |
112 | 112 | from calphy.composition_transformation import CompositionTransformation |
113 | | - |
| 113 | + |
114 | 114 | # Create a test calculation that transforms all atoms from one element to another |
115 | 115 | class TestCalc: |
116 | 116 | def __init__(self): |
117 | | - self.lattice = 'tests/conf1.data' |
118 | | - self.element = ['Cu', 'Al'] |
119 | | - self.composition_scaling = type('obj', (object,), { |
120 | | - '_input_chemical_composition': {'Cu': 500, 'Al': 0}, |
121 | | - '_output_chemical_composition': {'Cu': 0, 'Al': 500}, |
122 | | - 'input_chemical_composition': property(lambda s: s._input_chemical_composition), |
123 | | - 'output_chemical_composition': property(lambda s: s._output_chemical_composition), |
124 | | - 'restrictions': [] |
125 | | - })() |
126 | | - |
| 117 | + self.lattice = "tests/conf1.data" |
| 118 | + self.element = ["Cu", "Al"] |
| 119 | + self.composition_scaling = type( |
| 120 | + "obj", |
| 121 | + (object,), |
| 122 | + { |
| 123 | + "_input_chemical_composition": {"Cu": 500, "Al": 0}, |
| 124 | + "_output_chemical_composition": {"Cu": 0, "Al": 500}, |
| 125 | + "input_chemical_composition": property( |
| 126 | + lambda s: s._input_chemical_composition |
| 127 | + ), |
| 128 | + "output_chemical_composition": property( |
| 129 | + lambda s: s._output_chemical_composition |
| 130 | + ), |
| 131 | + "restrictions": [], |
| 132 | + }, |
| 133 | + )() |
| 134 | + |
127 | 135 | calc = TestCalc() |
128 | | - |
| 136 | + |
129 | 137 | # Should not raise an error |
130 | 138 | comp = CompositionTransformation(calc) |
131 | | - |
| 139 | + |
132 | 140 | # Verify the transformation is set up correctly |
133 | | - assert 'Cu' in comp.to_remove |
134 | | - assert 'Al' in comp.to_add |
135 | | - assert comp.to_remove['Cu'] == 500 |
136 | | - assert comp.to_add['Al'] == 500 |
| 141 | + assert "Cu" in comp.to_remove |
| 142 | + assert "Al" in comp.to_add |
| 143 | + assert comp.to_remove["Cu"] == 500 |
| 144 | + assert comp.to_add["Al"] == 500 |
137 | 145 | assert len(comp.transformation_list) == 1 |
138 | | - assert comp.transformation_list[0]['primary_element'] == 'Cu' |
139 | | - assert comp.transformation_list[0]['secondary_element'] == 'Al' |
140 | | - assert comp.transformation_list[0]['count'] == 500 |
| 146 | + assert comp.transformation_list[0]["primary_element"] == "Cu" |
| 147 | + assert comp.transformation_list[0]["secondary_element"] == "Al" |
| 148 | + assert comp.transformation_list[0]["count"] == 500 |
| 149 | + |
| 150 | + |
| 151 | +def test_composition_transformation_single_atom_type_pair_coeff(): |
| 152 | + """Test that pair_coeff matches actual atom types in structure. |
| 153 | +
|
| 154 | + Pure Cu structure (1 atom type) should generate pair_coeff with 1 element mapping, |
| 155 | + regardless of calc.element count. This ensures LAMMPS consistency: the number of |
| 156 | + elements in pair_coeff must match the number of atom types in the data file header. |
| 157 | + """ |
| 158 | + from calphy.composition_transformation import CompositionTransformation |
| 159 | + |
| 160 | + # Create a test calculation - pure Cu structure transforming to Al |
| 161 | + class TestCalc: |
| 162 | + def __init__(self): |
| 163 | + self.lattice = "tests/conf1.data" |
| 164 | + self.element = ["Cu", "Al"] # 2 elements in config |
| 165 | + self.composition_scaling = type( |
| 166 | + "obj", |
| 167 | + (object,), |
| 168 | + { |
| 169 | + "_input_chemical_composition": {"Cu": 500, "Al": 0}, |
| 170 | + "_output_chemical_composition": {"Cu": 0, "Al": 500}, |
| 171 | + "input_chemical_composition": property( |
| 172 | + lambda s: s._input_chemical_composition |
| 173 | + ), |
| 174 | + "output_chemical_composition": property( |
| 175 | + lambda s: s._output_chemical_composition |
| 176 | + ), |
| 177 | + "restrictions": [], |
| 178 | + }, |
| 179 | + )() |
| 180 | + |
| 181 | + calc = TestCalc() |
| 182 | + comp = CompositionTransformation(calc) |
| 183 | + |
| 184 | + # Structure has 1 actual atom type (pure Cu), so pair_coeff has 1 element |
| 185 | + # This matches the LAMMPS data file which declares 1 atom type |
| 186 | + assert ( |
| 187 | + len(comp.pair_list_old) == 1 |
| 188 | + ), f"Expected 1 element in pair_list_old, got {len(comp.pair_list_old)}: {comp.pair_list_old}" |
| 189 | + assert ( |
| 190 | + len(comp.pair_list_new) == 1 |
| 191 | + ), f"Expected 1 element in pair_list_new, got {len(comp.pair_list_new)}: {comp.pair_list_new}" |
| 192 | + assert comp.pair_list_old == ["Cu"] |
| 193 | + assert comp.pair_list_new == ["Al"] |
0 commit comments