calphy 1.4.5__py3-none-any.whl → 1.4.12__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- calphy/__init__.py +1 -1
- calphy/alchemy.py +278 -158
- calphy/composition_transformation.py +298 -136
- calphy/helpers.py +45 -29
- calphy/input.py +81 -3
- calphy/liquid.py +13 -0
- calphy/phase.py +55 -4
- calphy/phase_diagram.py +196 -53
- calphy/routines.py +6 -0
- calphy/scheduler.py +100 -105
- calphy/solid.py +243 -186
- {calphy-1.4.5.dist-info → calphy-1.4.12.dist-info}/METADATA +1 -1
- calphy-1.4.12.dist-info/RECORD +25 -0
- {calphy-1.4.5.dist-info → calphy-1.4.12.dist-info}/WHEEL +1 -1
- calphy-1.4.5.dist-info/RECORD +0 -25
- {calphy-1.4.5.dist-info → calphy-1.4.12.dist-info}/entry_points.txt +0 -0
- {calphy-1.4.5.dist-info → calphy-1.4.12.dist-info}/licenses/LICENSE +0 -0
- {calphy-1.4.5.dist-info → calphy-1.4.12.dist-info}/top_level.txt +0 -0
|
@@ -3,14 +3,14 @@ calphy: a Python library and command line interface for automated free
|
|
|
3
3
|
energy calculations.
|
|
4
4
|
|
|
5
5
|
Copyright 2021 (c) Sarath Menon^1, Yury Lysogorskiy^2, Ralf Drautz^2
|
|
6
|
-
^1: Max Planck Institut für Eisenforschung, Dusseldorf, Germany
|
|
6
|
+
^1: Max Planck Institut für Eisenforschung, Dusseldorf, Germany
|
|
7
7
|
^2: Ruhr-University Bochum, Bochum, Germany
|
|
8
8
|
|
|
9
|
-
calphy is published and distributed under the Academic Software License v1.0 (ASL).
|
|
10
|
-
calphy is distributed in the hope that it will be useful for non-commercial academic research,
|
|
11
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
9
|
+
calphy is published and distributed under the Academic Software License v1.0 (ASL).
|
|
10
|
+
calphy is distributed in the hope that it will be useful for non-commercial academic research,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
|
12
12
|
calphy API is published and distributed under the BSD 3-Clause "New" or "Revised" License
|
|
13
|
-
See the LICENSE FILE for more details.
|
|
13
|
+
See the LICENSE FILE for more details.
|
|
14
14
|
|
|
15
15
|
More information about the program can be found in:
|
|
16
16
|
Menon, Sarath, Yury Lysogorskiy, Jutta Rogal, and Ralf Drautz.
|
|
@@ -32,9 +32,10 @@ from ase.atoms import Atoms
|
|
|
32
32
|
from pyscal3.core import element_dict
|
|
33
33
|
from calphy.integrators import kb
|
|
34
34
|
|
|
35
|
+
|
|
35
36
|
class CompositionTransformation:
|
|
36
37
|
"""
|
|
37
|
-
Class for performing composition transformations and
|
|
38
|
+
Class for performing composition transformations and
|
|
38
39
|
generating necessary pair styles for such transformations.
|
|
39
40
|
|
|
40
41
|
Parameters
|
|
@@ -43,11 +44,11 @@ class CompositionTransformation:
|
|
|
43
44
|
input structure which is used for composition transformation
|
|
44
45
|
|
|
45
46
|
input_chemical_formula: dict
|
|
46
|
-
dictionary of input chemical
|
|
47
|
+
dictionary of input chemical
|
|
47
48
|
|
|
48
49
|
output_chemical_formula: string
|
|
49
50
|
the required chemical composition string
|
|
50
|
-
|
|
51
|
+
|
|
51
52
|
restrictions: list of strings, optional
|
|
52
53
|
Can be used to specify restricted transformations
|
|
53
54
|
|
|
@@ -74,17 +75,17 @@ class CompositionTransformation:
|
|
|
74
75
|
transformations should not take place. The code for this is:
|
|
75
76
|
|
|
76
77
|
```
|
|
77
|
-
comp = CompositionTransformation(filename, {"Al":500, "Li":5},
|
|
78
|
+
comp = CompositionTransformation(filename, {"Al":500, "Li":5},
|
|
78
79
|
{"Al": 494, "Li": 2, "O": 3, "C":1}, restrictions=["Al-O"])
|
|
79
80
|
```
|
|
80
81
|
|
|
81
|
-
If the restrictions are not satisfiable, an error will be raised.
|
|
82
|
+
If the restrictions are not satisfiable, an error will be raised.
|
|
82
83
|
|
|
83
84
|
The LAMMPS data file or dump files do not contain any information about the species except the type numbers.
|
|
84
85
|
In general the number of atoms are respected, for example if the file has 10 atoms of type 1, 5 of type 2,
|
|
85
86
|
and 1 of type 3. If the `input_chemical_composition` is `{"Li": 5, "Al": 10, "O": 1}`, type 1 is assigned to Al,
|
|
86
87
|
type 2 is assigned to Li and type 3 is assigned to O. This is done irrespective of the order in which
|
|
87
|
-
`input_chemical_composition` is specified. However, if there are equal number of atoms, the order is respected.
|
|
88
|
+
`input_chemical_composition` is specified. However, if there are equal number of atoms, the order is respected.
|
|
88
89
|
Therefore it is important to make sure that the `input_chemical_composition` is in the same order as that of
|
|
89
90
|
types in structure file. For example, consider a NiAl structure of 10 Ni atoms and 10 Al atoms. Ni atoms are type 1 in LAMMPS terminology
|
|
90
91
|
and Al atoms are type 2. In this case, to preserve the order, `input_chemical_composition` should be `{"Ni": 5, "Al": 10}`.
|
|
@@ -112,22 +113,27 @@ class CompositionTransformation:
|
|
|
112
113
|
```
|
|
113
114
|
The output is written in LAMMPS dump format.
|
|
114
115
|
"""
|
|
116
|
+
|
|
115
117
|
def __init__(self, calc):
|
|
116
|
-
|
|
117
|
-
self.input_chemical_composition =
|
|
118
|
-
|
|
118
|
+
|
|
119
|
+
self.input_chemical_composition = (
|
|
120
|
+
calc.composition_scaling._input_chemical_composition
|
|
121
|
+
)
|
|
122
|
+
self.output_chemical_composition = (
|
|
123
|
+
calc.composition_scaling.output_chemical_composition
|
|
124
|
+
)
|
|
119
125
|
self.restrictions = calc.composition_scaling.restrictions
|
|
120
126
|
self.calc = calc
|
|
121
127
|
self.actual_species = None
|
|
122
128
|
self.new_species = None
|
|
123
|
-
self.maxtype = None
|
|
129
|
+
self.maxtype = None
|
|
124
130
|
self.atom_mark = None
|
|
125
131
|
self.atom_species = None
|
|
126
132
|
self.mappings = None
|
|
127
133
|
self.unique_mappings = None
|
|
128
134
|
self.mappingdict = None
|
|
129
135
|
self.prepare_mappings()
|
|
130
|
-
|
|
136
|
+
|
|
131
137
|
def dict_to_string(self, inputdict):
|
|
132
138
|
strlst = []
|
|
133
139
|
for key, val in inputdict.items():
|
|
@@ -141,49 +147,68 @@ class CompositionTransformation:
|
|
|
141
147
|
Find the entropy entribution of the transformation. To get
|
|
142
148
|
free energies, multiply by -T.
|
|
143
149
|
"""
|
|
150
|
+
|
|
144
151
|
def _log(val):
|
|
145
152
|
if val == 0:
|
|
146
153
|
return 0
|
|
147
154
|
else:
|
|
148
155
|
return np.log(val)
|
|
156
|
+
|
|
149
157
|
ents = []
|
|
150
158
|
for key, val in self.output_chemical_composition.items():
|
|
151
159
|
if key in self.input_chemical_composition.keys():
|
|
152
|
-
t1 = self.input_chemical_composition[key]/self.natoms
|
|
153
|
-
t2 = self.output_chemical_composition[key]/self.natoms
|
|
154
|
-
cont = t2*_log(t2) - t1*_log(t1)
|
|
160
|
+
t1 = self.input_chemical_composition[key] / self.natoms
|
|
161
|
+
t2 = self.output_chemical_composition[key] / self.natoms
|
|
162
|
+
cont = t2 * _log(t2) - t1 * _log(t1)
|
|
155
163
|
else:
|
|
156
164
|
t1 = 0
|
|
157
|
-
t2 = self.output_chemical_composition[key]/self.natoms
|
|
158
|
-
cont =
|
|
165
|
+
t2 = self.output_chemical_composition[key] / self.natoms
|
|
166
|
+
cont = t2 * _log(t2) - 0
|
|
159
167
|
ents.append(cont)
|
|
160
|
-
entropy_term = kb*np.sum(ents)
|
|
168
|
+
entropy_term = kb * np.sum(ents)
|
|
161
169
|
return entropy_term
|
|
162
|
-
|
|
170
|
+
|
|
163
171
|
def convert_to_pyscal(self):
|
|
164
172
|
"""
|
|
165
173
|
Convert a given system to pyscal and give a dict of type mappings
|
|
166
174
|
"""
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
# Create Z_of_type mapping to properly read LAMMPS data files
|
|
176
|
+
# This ensures atoms are correctly identified by their element
|
|
177
|
+
Z_of_type = dict(
|
|
178
|
+
[
|
|
179
|
+
(count + 1, element(el).atomic_number)
|
|
180
|
+
for count, el in enumerate(self.calc.element)
|
|
181
|
+
]
|
|
182
|
+
)
|
|
183
|
+
aseobj = read(
|
|
184
|
+
self.calc.lattice, format="lammps-data", style="atomic", Z_of_type=Z_of_type
|
|
185
|
+
)
|
|
186
|
+
pstruct = pc.System(aseobj, format="ase")
|
|
187
|
+
|
|
188
|
+
# here we have to validate the input composition dict; and map it
|
|
171
189
|
typelist = pstruct.atoms.species
|
|
172
190
|
types, typecounts = np.unique(typelist, return_counts=True)
|
|
173
191
|
composition = {types[x]: typecounts[x] for x in range(len(types))}
|
|
174
192
|
|
|
175
193
|
atomsymbols = self.calc.element
|
|
176
|
-
atomtypes = [x+1 for x in range(len(self.calc.element))]
|
|
177
|
-
|
|
194
|
+
atomtypes = [x + 1 for x in range(len(self.calc.element))]
|
|
195
|
+
|
|
178
196
|
self.pyscal_structure = pstruct
|
|
179
197
|
self.typedict = dict(zip(atomsymbols, atomtypes))
|
|
180
198
|
self.reversetypedict = dict(zip(atomtypes, atomsymbols))
|
|
181
199
|
self.natoms = self.pyscal_structure.natoms
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
self.
|
|
186
|
-
#
|
|
200
|
+
|
|
201
|
+
# Count of actual unique atom types present in the structure
|
|
202
|
+
# This matches what's declared in the LAMMPS data file header
|
|
203
|
+
self.actual_species_in_structure = len(types)
|
|
204
|
+
# Count from calc.element (may include types with 0 atoms)
|
|
205
|
+
self.calc_element_count = len(self.calc.element)
|
|
206
|
+
|
|
207
|
+
# Use actual structure types for pair_coeff consistency
|
|
208
|
+
# pair_coeff must match the number declared in the data file header
|
|
209
|
+
self.actual_species = self.actual_species_in_structure
|
|
210
|
+
self.new_species = len(self.output_chemical_composition) - len(types)
|
|
211
|
+
self.maxtype = self.actual_species + 1 # + self.new_species
|
|
187
212
|
|
|
188
213
|
def get_composition_transformation(self):
|
|
189
214
|
"""
|
|
@@ -203,41 +228,43 @@ class CompositionTransformation:
|
|
|
203
228
|
to_remove[key] = np.abs(val)
|
|
204
229
|
else:
|
|
205
230
|
to_add[key] = val
|
|
206
|
-
|
|
231
|
+
|
|
207
232
|
self.to_remove = to_remove
|
|
208
233
|
self.to_add = to_add
|
|
209
234
|
|
|
210
|
-
def get_random_index_of_species(self,
|
|
235
|
+
def get_random_index_of_species(self, species_name):
|
|
211
236
|
"""
|
|
212
|
-
Get a random index of a given species
|
|
237
|
+
Get a random index of a given species by element name
|
|
213
238
|
"""
|
|
214
|
-
ids = [count for count, x in enumerate(self.
|
|
239
|
+
ids = [count for count, x in enumerate(self.atom_species) if x == species_name]
|
|
215
240
|
return ids[np.random.randint(0, len(ids))]
|
|
216
|
-
|
|
241
|
+
|
|
217
242
|
def mark_atoms(self):
|
|
218
243
|
for i in range(self.natoms):
|
|
219
244
|
self.atom_mark.append(False)
|
|
220
|
-
|
|
245
|
+
|
|
246
|
+
# Use species (element symbols) instead of numeric types
|
|
247
|
+
self.atom_species = self.pyscal_structure.atoms.species
|
|
221
248
|
self.atom_type = self.pyscal_structure.atoms.types
|
|
222
|
-
self.mappings = [f"{x}-{x}" for x in self.
|
|
223
|
-
|
|
249
|
+
self.mappings = [f"{x}-{x}" for x in self.atom_species]
|
|
250
|
+
|
|
224
251
|
def update_mark_atoms(self):
|
|
225
252
|
self.marked_atoms = []
|
|
226
253
|
for key, val in self.to_remove.items():
|
|
227
|
-
#
|
|
254
|
+
# key is the element name (e.g., "Mg")
|
|
228
255
|
for i in range(100000):
|
|
229
|
-
rint = self.get_random_index_of_species(
|
|
256
|
+
rint = self.get_random_index_of_species(key)
|
|
230
257
|
if rint not in self.marked_atoms:
|
|
231
258
|
self.atom_mark[rint] = True
|
|
232
259
|
self.marked_atoms.append(rint)
|
|
233
260
|
val -= 1
|
|
234
|
-
if
|
|
235
|
-
break
|
|
236
|
-
|
|
261
|
+
if val <= 0:
|
|
262
|
+
break
|
|
263
|
+
|
|
237
264
|
def update_typedicts(self):
|
|
238
|
-
#in a cycle add things to the typedict
|
|
265
|
+
# in a cycle add things to the typedict
|
|
239
266
|
for key, val in self.to_add.items():
|
|
240
|
-
#print(f"Element {key}, count {val}")
|
|
267
|
+
# print(f"Element {key}, count {val}")
|
|
241
268
|
if key in self.typedict.keys():
|
|
242
269
|
newtype = self.typedict[key]
|
|
243
270
|
else:
|
|
@@ -245,61 +272,67 @@ class CompositionTransformation:
|
|
|
245
272
|
self.typedict[key] = self.maxtype
|
|
246
273
|
self.reversetypedict[self.maxtype] = key
|
|
247
274
|
self.maxtype += 1
|
|
248
|
-
#print(f"Element {key}, newtype {newtype}")
|
|
249
|
-
|
|
275
|
+
# print(f"Element {key}, newtype {newtype}")
|
|
276
|
+
|
|
250
277
|
def compute_possible_mappings(self):
|
|
251
278
|
self.possible_mappings = []
|
|
252
|
-
#Now make a list of possible mappings
|
|
279
|
+
# Now make a list of possible mappings using element names
|
|
253
280
|
for key1, val1 in self.to_remove.items():
|
|
254
281
|
for key2, val2 in self.to_add.items():
|
|
255
282
|
mapping = f"{key1}-{key2}"
|
|
256
283
|
if mapping not in self.restrictions:
|
|
257
|
-
self.possible_mappings.append(
|
|
258
|
-
|
|
284
|
+
self.possible_mappings.append(mapping)
|
|
285
|
+
|
|
259
286
|
def update_mappings(self):
|
|
260
287
|
marked_atoms = self.marked_atoms.copy()
|
|
261
288
|
for key, val in self.to_add.items():
|
|
262
|
-
#now get all
|
|
263
|
-
|
|
264
|
-
#we to see if we can get val number of atoms from marked ones
|
|
265
|
-
if val
|
|
266
|
-
raise ValueError(
|
|
267
|
-
|
|
268
|
-
|
|
289
|
+
# now get all
|
|
290
|
+
|
|
291
|
+
# we to see if we can get val number of atoms from marked ones
|
|
292
|
+
if val > len(marked_atoms):
|
|
293
|
+
raise ValueError(
|
|
294
|
+
f"Not enough atoms to choose {val} from {len(marked_atoms)} not possible"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# otherwise find atoms until we find enough
|
|
269
298
|
for i in range(val):
|
|
270
|
-
#choose a number from marked atoms
|
|
299
|
+
# choose a number from marked atoms
|
|
271
300
|
found = False
|
|
272
301
|
to_del = []
|
|
273
302
|
for x in range(len(self.marked_atoms)):
|
|
274
303
|
random_choice = np.random.choice(marked_atoms)
|
|
275
|
-
#find corresponding
|
|
276
|
-
mapping = f"{self.
|
|
304
|
+
# find corresponding mapping using species name
|
|
305
|
+
mapping = f"{self.atom_species[random_choice]}-{key}"
|
|
277
306
|
if mapping in self.possible_mappings:
|
|
278
|
-
#this is a valid choice
|
|
307
|
+
# this is a valid choice
|
|
279
308
|
self.mappings[random_choice] = mapping
|
|
280
309
|
found = True
|
|
281
310
|
if found:
|
|
282
|
-
#finish up, change the array, and break
|
|
283
|
-
#to_del.append(random_choice)
|
|
311
|
+
# finish up, change the array, and break
|
|
312
|
+
# to_del.append(random_choice)
|
|
284
313
|
marked_atoms.remove(random_choice)
|
|
285
314
|
break
|
|
286
|
-
#if it was not found, the loop finished, throw error
|
|
315
|
+
# if it was not found, the loop finished, throw error
|
|
287
316
|
if not found:
|
|
288
|
-
raise ValueError(
|
|
289
|
-
|
|
290
|
-
|
|
317
|
+
raise ValueError(
|
|
318
|
+
"A possible transformation could not be found, please check the restrictions"
|
|
319
|
+
)
|
|
320
|
+
# otherwise modify our marked atoms, list, and move on
|
|
321
|
+
# for item in to_del:
|
|
291
322
|
# marked_atoms.remove(item)
|
|
292
|
-
|
|
293
|
-
self.unique_mappings, self.unique_mapping_counts = np.unique(self.mappings, return_counts=True)
|
|
294
323
|
|
|
295
|
-
|
|
324
|
+
self.unique_mappings, self.unique_mapping_counts = np.unique(
|
|
325
|
+
self.mappings, return_counts=True
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# now make the transformation dict
|
|
296
329
|
self.transformation_list = []
|
|
297
330
|
for count, mapping in enumerate(self.unique_mappings):
|
|
298
331
|
mapsplit = mapping.split("-")
|
|
299
332
|
if not mapsplit[0] == mapsplit[1]:
|
|
300
333
|
transformation_dict = {}
|
|
301
|
-
transformation_dict["primary_element"] =
|
|
302
|
-
transformation_dict["secondary_element"] =
|
|
334
|
+
transformation_dict["primary_element"] = mapsplit[0]
|
|
335
|
+
transformation_dict["secondary_element"] = mapsplit[1]
|
|
303
336
|
transformation_dict["count"] = self.unique_mapping_counts[count]
|
|
304
337
|
self.transformation_list.append(transformation_dict)
|
|
305
338
|
|
|
@@ -307,104 +340,233 @@ class CompositionTransformation:
|
|
|
307
340
|
self.update_typedicts()
|
|
308
341
|
self.compute_possible_mappings()
|
|
309
342
|
self.update_mappings()
|
|
310
|
-
|
|
343
|
+
|
|
311
344
|
def prepare_pair_lists(self):
|
|
312
345
|
self.pair_list_old = []
|
|
313
346
|
self.pair_list_new = []
|
|
314
347
|
for mapping in self.unique_mappings:
|
|
315
348
|
map_split = mapping.split("-")
|
|
316
|
-
#conserved atom
|
|
317
|
-
if
|
|
318
|
-
self.pair_list_old.append(
|
|
319
|
-
self.pair_list_new.append(
|
|
349
|
+
# conserved atom - mappings now use element names directly
|
|
350
|
+
if map_split[0] == map_split[1]:
|
|
351
|
+
self.pair_list_old.append(map_split[0])
|
|
352
|
+
self.pair_list_new.append(map_split[0])
|
|
320
353
|
else:
|
|
321
|
-
self.pair_list_old.append(
|
|
322
|
-
self.pair_list_new.append(
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
354
|
+
self.pair_list_old.append(map_split[0])
|
|
355
|
+
self.pair_list_new.append(map_split[1])
|
|
356
|
+
|
|
357
|
+
# Special case: 100% transformation with only 1 mapping
|
|
358
|
+
# LAMMPS requires pair_coeff to map ALL atom types declared in data file
|
|
359
|
+
# Example: Pure Al→Mg with 2 types declared → need ['Al', 'Al'] and ['Mg', 'Mg']
|
|
360
|
+
# This ensures consistency between data file type count and pair_coeff mappings
|
|
361
|
+
if len(self.unique_mappings) == 1 and self.actual_species > 1:
|
|
362
|
+
# Duplicate the single mapping to match number of declared atom types
|
|
363
|
+
for _ in range(self.actual_species - 1):
|
|
364
|
+
self.pair_list_old.append(self.pair_list_old[0])
|
|
365
|
+
self.pair_list_new.append(self.pair_list_new[0])
|
|
366
|
+
|
|
367
|
+
# Create mapping from transformation strings to UNIQUE type numbers
|
|
368
|
+
# Each unique transformation mapping needs its own type for LAMMPS swapping
|
|
369
|
+
# Example: Al-Al, Mg-Al, Mg-Mg should map to types 1, 2, 3 respectively
|
|
370
|
+
self.mappingdict = {}
|
|
371
|
+
for idx, mapping in enumerate(self.unique_mappings, start=1):
|
|
372
|
+
self.mappingdict[mapping] = idx
|
|
373
|
+
|
|
374
|
+
# Update reversetypedict - map each type to its source element
|
|
375
|
+
# We'll handle species naming in write_structure to keep types separate
|
|
376
|
+
self.reversetypedict = {}
|
|
377
|
+
for mapping, type_num in self.mappingdict.items():
|
|
378
|
+
source_element = mapping.split("-")[0]
|
|
379
|
+
self.reversetypedict[type_num] = source_element
|
|
380
|
+
|
|
326
381
|
def update_types(self):
|
|
382
|
+
# Update atom_type based on mapping to new types
|
|
327
383
|
for x in range(len(self.atom_type)):
|
|
328
384
|
self.atom_type[x] = self.mappingdict[self.mappings[x]]
|
|
329
|
-
|
|
330
|
-
#
|
|
331
|
-
#npyscal = len(self.pyscal_structure.atoms.types)
|
|
385
|
+
|
|
386
|
+
# Update pyscal structure types
|
|
332
387
|
self.pyscal_structure.atoms.types = self.atom_type
|
|
333
|
-
|
|
334
|
-
# self.pyscal_structure.atoms.types[count] = self.atom_type[count]
|
|
335
|
-
|
|
388
|
+
|
|
336
389
|
def iselement(self, symbol):
|
|
337
390
|
try:
|
|
338
391
|
_ = element(symbol)
|
|
339
392
|
return True
|
|
340
393
|
except:
|
|
341
394
|
return False
|
|
342
|
-
|
|
395
|
+
|
|
343
396
|
def update_pair_coeff(self, pair_coeff):
|
|
397
|
+
"""
|
|
398
|
+
Update pair_coeff command with new element specifications.
|
|
399
|
+
|
|
400
|
+
Handles both single-file formats (EAM alloy):
|
|
401
|
+
pair_coeff * * potential.eam.alloy El1 El2
|
|
402
|
+
|
|
403
|
+
And two-file formats (MEAM):
|
|
404
|
+
pair_coeff * * library.meam El1 El2 potential.meam El1 El2
|
|
405
|
+
|
|
406
|
+
For MEAM potentials, both element specifications are updated identically.
|
|
407
|
+
"""
|
|
344
408
|
pcsplit = pair_coeff.strip().split()
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
409
|
+
result_parts = []
|
|
410
|
+
i = 0
|
|
411
|
+
|
|
412
|
+
while i < len(pcsplit):
|
|
413
|
+
token = pcsplit[i]
|
|
414
|
+
|
|
415
|
+
# Check if this token starts an element specification
|
|
416
|
+
# (either it's an element, or the next token is an element)
|
|
417
|
+
if self.iselement(token):
|
|
418
|
+
# Found start of element list - collect all consecutive elements
|
|
419
|
+
element_group = []
|
|
420
|
+
while i < len(pcsplit) and self.iselement(pcsplit[i]):
|
|
421
|
+
element_group.append(pcsplit[i])
|
|
422
|
+
i += 1
|
|
423
|
+
|
|
424
|
+
# Determine which element list to use based on what we found
|
|
425
|
+
# If element_group matches our pair_list_old, replace with pair_list_new
|
|
426
|
+
# Otherwise replace with pair_list_old (for the old/reference command)
|
|
427
|
+
if element_group == self.pair_list_old or set(element_group) == set(
|
|
428
|
+
self.calc.element
|
|
429
|
+
):
|
|
430
|
+
# This needs special handling - we'll mark position for later
|
|
431
|
+
result_parts.append("__ELEMENTS__")
|
|
432
|
+
else:
|
|
433
|
+
# Keep non-matching element groups as-is
|
|
434
|
+
result_parts.extend(element_group)
|
|
435
|
+
else:
|
|
436
|
+
# Non-element token (potential file, wildcards, options, etc.)
|
|
437
|
+
result_parts.append(token)
|
|
438
|
+
i += 1
|
|
439
|
+
|
|
440
|
+
# Now build old and new commands by replacing __ELEMENTS__ markers
|
|
441
|
+
pc_old_parts = [
|
|
442
|
+
self.pair_list_old if p == "__ELEMENTS__" else [p] for p in result_parts
|
|
443
|
+
]
|
|
444
|
+
pc_new_parts = [
|
|
445
|
+
self.pair_list_new if p == "__ELEMENTS__" else [p] for p in result_parts
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
# Flatten the lists
|
|
449
|
+
pc_old = " ".join(
|
|
450
|
+
[
|
|
451
|
+
item
|
|
452
|
+
for sublist in pc_old_parts
|
|
453
|
+
for item in (sublist if isinstance(sublist, list) else [sublist])
|
|
454
|
+
]
|
|
455
|
+
)
|
|
456
|
+
pc_new = " ".join(
|
|
457
|
+
[
|
|
458
|
+
item
|
|
459
|
+
for sublist in pc_new_parts
|
|
460
|
+
for item in (sublist if isinstance(sublist, list) else [sublist])
|
|
461
|
+
]
|
|
462
|
+
)
|
|
463
|
+
|
|
363
464
|
return pc_old, pc_new
|
|
364
465
|
|
|
365
466
|
def get_swap_types(self):
|
|
366
467
|
"""
|
|
367
|
-
Get swapping types
|
|
468
|
+
Get swapping types for configurational entropy calculation.
|
|
469
|
+
|
|
470
|
+
Returns types that share the same initial element but have different
|
|
471
|
+
transformation paths (e.g., Al→Al vs Al→Mg).
|
|
472
|
+
|
|
473
|
+
The order matters for reversibility:
|
|
474
|
+
- Forward pass (e.g., Mg→Al enrichment): swap between Mg types
|
|
475
|
+
- Backward pass (e.g., Al→Mg depletion): swap between Al types
|
|
476
|
+
|
|
477
|
+
Returns list ordered as: [conserved_type, transforming_type]
|
|
478
|
+
where conserved_type is X→X and transforming_type is X→Y
|
|
368
479
|
"""
|
|
369
480
|
swap_list = []
|
|
370
481
|
for mapping in self.unique_mappings:
|
|
371
482
|
map_split = mapping.split("-")
|
|
372
|
-
#conserved atom
|
|
373
|
-
if
|
|
483
|
+
# conserved atom - skip
|
|
484
|
+
if map_split[0] == map_split[1]:
|
|
374
485
|
pass
|
|
375
486
|
else:
|
|
376
487
|
first_type = map_split[0]
|
|
377
488
|
second_type = map_split[1]
|
|
378
|
-
first_map = f
|
|
489
|
+
first_map = f"{first_type}-{first_type}"
|
|
379
490
|
second_map = mapping
|
|
380
491
|
|
|
381
|
-
#
|
|
382
|
-
|
|
383
|
-
|
|
492
|
+
# Check if conserved mapping exists
|
|
493
|
+
if first_map in self.mappingdict:
|
|
494
|
+
# get the numbers from dict
|
|
495
|
+
first_swap_type = self.mappingdict[first_map]
|
|
496
|
+
second_swap_type = self.mappingdict[second_map]
|
|
497
|
+
# Order: [transforming_type, conserved_type]
|
|
498
|
+
# This represents: atoms that transform vs atoms that don't
|
|
499
|
+
swap_list.append([second_swap_type, first_swap_type])
|
|
500
|
+
else:
|
|
501
|
+
# 100% transformation case - no conserved atoms of this type
|
|
502
|
+
# Only the transforming type exists
|
|
503
|
+
second_swap_type = self.mappingdict[second_map]
|
|
504
|
+
swap_list.append([second_swap_type])
|
|
505
|
+
|
|
506
|
+
return swap_list[0] if swap_list else []
|
|
384
507
|
|
|
385
|
-
swap_list.append([first_swap_type, second_swap_type])
|
|
386
|
-
return swap_list[0]
|
|
387
|
-
|
|
388
|
-
|
|
389
508
|
def write_structure(self, outfilename):
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
509
|
+
"""Write structure to LAMMPS data file with proper type declarations.
|
|
510
|
+
|
|
511
|
+
Writes using ASE directly with custom atom types to preserve distinct
|
|
512
|
+
type numbers for each transformation mapping.
|
|
513
|
+
"""
|
|
514
|
+
from ase.io import write as ase_write
|
|
515
|
+
from ase import Atoms as ASEAtoms
|
|
516
|
+
|
|
517
|
+
# Get positions and cell from pyscal structure
|
|
518
|
+
positions = self.pyscal_structure.atoms.positions
|
|
519
|
+
cell = self.pyscal_structure.box
|
|
520
|
+
|
|
521
|
+
# Create ASE Atoms object with chemical symbols from reversetypedict
|
|
522
|
+
# All atoms get their source element symbol
|
|
523
|
+
symbols = [self.reversetypedict[t] for t in self.pyscal_structure.atoms.types]
|
|
524
|
+
|
|
525
|
+
ase_atoms = ASEAtoms(symbols=symbols, positions=positions, cell=cell, pbc=True)
|
|
526
|
+
|
|
527
|
+
# Write using ASE with atom_style
|
|
528
|
+
ase_write(outfilename, ase_atoms, format="lammps-data", atom_style="atomic")
|
|
529
|
+
|
|
530
|
+
# Post-process to fix the type column with our custom types
|
|
531
|
+
with open(outfilename, "r") as f:
|
|
532
|
+
lines = f.readlines()
|
|
533
|
+
|
|
534
|
+
# Find the Atoms section and replace type numbers
|
|
535
|
+
in_atoms_section = False
|
|
536
|
+
atom_idx = 0
|
|
537
|
+
for i, line in enumerate(lines):
|
|
538
|
+
if "Atoms" in line and "#" in line:
|
|
539
|
+
in_atoms_section = True
|
|
540
|
+
continue
|
|
541
|
+
|
|
542
|
+
if in_atoms_section and line.strip():
|
|
543
|
+
parts = line.split()
|
|
544
|
+
if len(parts) >= 5: # atom_id type x y z
|
|
545
|
+
# Replace the type (column 1, 0-indexed) with our custom type
|
|
546
|
+
custom_type = self.pyscal_structure.atoms.types[atom_idx]
|
|
547
|
+
parts[1] = str(custom_type)
|
|
548
|
+
lines[i] = " " + " ".join(parts) + "\n"
|
|
549
|
+
atom_idx += 1
|
|
550
|
+
if atom_idx >= len(self.pyscal_structure.atoms.types):
|
|
551
|
+
break
|
|
552
|
+
|
|
553
|
+
# Update the number of atom types in the header
|
|
554
|
+
required_ntypes = len(self.pair_list_old)
|
|
555
|
+
for i, line in enumerate(lines):
|
|
556
|
+
if "atom types" in line:
|
|
557
|
+
lines[i] = f"{required_ntypes} atom types\n"
|
|
558
|
+
break
|
|
559
|
+
|
|
560
|
+
# Write the corrected file
|
|
561
|
+
with open(outfilename, "w") as f:
|
|
562
|
+
f.writelines(lines)
|
|
563
|
+
|
|
402
564
|
def prepare_mappings(self):
|
|
403
565
|
self.atom_mark = []
|
|
404
566
|
self.atom_species = []
|
|
405
567
|
self.mappings = []
|
|
406
568
|
self.unique_mappings = []
|
|
407
|
-
|
|
569
|
+
|
|
408
570
|
self.get_composition_transformation()
|
|
409
571
|
self.convert_to_pyscal()
|
|
410
572
|
|
|
@@ -412,4 +574,4 @@ class CompositionTransformation:
|
|
|
412
574
|
self.update_mark_atoms()
|
|
413
575
|
self.get_mappings()
|
|
414
576
|
self.prepare_pair_lists()
|
|
415
|
-
self.update_types()
|
|
577
|
+
self.update_types()
|