calphy 1.4.5__py3-none-any.whl → 1.4.6__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 CHANGED
@@ -4,7 +4,7 @@ from calphy.solid import Solid
4
4
  from calphy.alchemy import Alchemy
5
5
  from calphy.routines import MeltingTemp
6
6
 
7
- __version__ = "1.4.5"
7
+ __version__ = "1.4.6"
8
8
 
9
9
  def addtest(a,b):
10
10
  return a+b
@@ -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 = calc.composition_scaling._input_chemical_composition
118
- self.output_chemical_composition = calc.composition_scaling.output_chemical_composition
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,51 @@ 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 = t2*_log(t2) - 0
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
- aseobj = read(self.calc.lattice, format='lammps-data', style='atomic')
168
- pstruct = pc.System(aseobj, format='ase')
169
-
170
- #here we have to validate the input composition dict; and map it
175
+ aseobj = read(self.calc.lattice, format="lammps-data", style="atomic")
176
+ pstruct = pc.System(aseobj, format="ase")
177
+
178
+ # here we have to validate the input composition dict; and map it
171
179
  typelist = pstruct.atoms.species
172
180
  types, typecounts = np.unique(typelist, return_counts=True)
173
181
  composition = {types[x]: typecounts[x] for x in range(len(types))}
174
182
 
175
183
  atomsymbols = self.calc.element
176
- atomtypes = [x+1 for x in range(len(self.calc.element))]
177
-
184
+ atomtypes = [x + 1 for x in range(len(self.calc.element))]
185
+
178
186
  self.pyscal_structure = pstruct
179
187
  self.typedict = dict(zip(atomsymbols, atomtypes))
180
188
  self.reversetypedict = dict(zip(atomtypes, atomsymbols))
181
189
  self.natoms = self.pyscal_structure.natoms
182
-
190
+
183
191
  self.actual_species = len(self.typedict)
184
192
  self.new_species = len(self.output_chemical_composition) - len(self.typedict)
185
- self.maxtype = self.actual_species + 1 #+ self.new_species
186
- #print(self.typedict)
193
+ self.maxtype = self.actual_species + 1 # + self.new_species
194
+ # print(self.typedict)
187
195
 
188
196
  def get_composition_transformation(self):
189
197
  """
@@ -203,7 +211,7 @@ class CompositionTransformation:
203
211
  to_remove[key] = np.abs(val)
204
212
  else:
205
213
  to_add[key] = val
206
-
214
+
207
215
  self.to_remove = to_remove
208
216
  self.to_add = to_add
209
217
 
@@ -211,33 +219,33 @@ class CompositionTransformation:
211
219
  """
212
220
  Get a random index of a given species
213
221
  """
214
- ids = [count for count, x in enumerate(self.atom_type) if x==species]
222
+ ids = [count for count, x in enumerate(self.atom_type) if x == species]
215
223
  return ids[np.random.randint(0, len(ids))]
216
-
224
+
217
225
  def mark_atoms(self):
218
226
  for i in range(self.natoms):
219
227
  self.atom_mark.append(False)
220
-
228
+
221
229
  self.atom_type = self.pyscal_structure.atoms.types
222
230
  self.mappings = [f"{x}-{x}" for x in self.atom_type]
223
-
231
+
224
232
  def update_mark_atoms(self):
225
233
  self.marked_atoms = []
226
234
  for key, val in self.to_remove.items():
227
- #print(f"Element {key}, count {val}")
235
+ # print(f"Element {key}, count {val}")
228
236
  for i in range(100000):
229
237
  rint = self.get_random_index_of_species(self.typedict[key])
230
238
  if rint not in self.marked_atoms:
231
239
  self.atom_mark[rint] = True
232
240
  self.marked_atoms.append(rint)
233
241
  val -= 1
234
- if (val <= 0):
235
- break
236
-
242
+ if val <= 0:
243
+ break
244
+
237
245
  def update_typedicts(self):
238
- #in a cycle add things to the typedict
246
+ # in a cycle add things to the typedict
239
247
  for key, val in self.to_add.items():
240
- #print(f"Element {key}, count {val}")
248
+ # print(f"Element {key}, count {val}")
241
249
  if key in self.typedict.keys():
242
250
  newtype = self.typedict[key]
243
251
  else:
@@ -245,61 +253,73 @@ class CompositionTransformation:
245
253
  self.typedict[key] = self.maxtype
246
254
  self.reversetypedict[self.maxtype] = key
247
255
  self.maxtype += 1
248
- #print(f"Element {key}, newtype {newtype}")
249
-
256
+ # print(f"Element {key}, newtype {newtype}")
257
+
250
258
  def compute_possible_mappings(self):
251
259
  self.possible_mappings = []
252
- #Now make a list of possible mappings
260
+ # Now make a list of possible mappings
253
261
  for key1, val1 in self.to_remove.items():
254
262
  for key2, val2 in self.to_add.items():
255
263
  mapping = f"{key1}-{key2}"
256
264
  if mapping not in self.restrictions:
257
- self.possible_mappings.append(f"{self.typedict[key1]}-{self.typedict[key2]}")
258
-
265
+ self.possible_mappings.append(
266
+ f"{self.typedict[key1]}-{self.typedict[key2]}"
267
+ )
268
+
259
269
  def update_mappings(self):
260
270
  marked_atoms = self.marked_atoms.copy()
261
271
  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
272
+ # now get all
273
+
274
+ # we to see if we can get val number of atoms from marked ones
265
275
  if val < len(marked_atoms):
266
- raise ValueError(f'Not enough atoms to choose {val} from {len(marked_atoms)} not possible')
267
-
268
- #otherwise find atoms until we find enough
276
+ raise ValueError(
277
+ f"Not enough atoms to choose {val} from {len(marked_atoms)} not possible"
278
+ )
279
+
280
+ # otherwise find atoms until we find enough
269
281
  for i in range(val):
270
- #choose a number from marked atoms
282
+ # choose a number from marked atoms
271
283
  found = False
272
284
  to_del = []
273
285
  for x in range(len(self.marked_atoms)):
274
286
  random_choice = np.random.choice(marked_atoms)
275
- #find corresponding mappiong
287
+ # find corresponding mappiong
276
288
  mapping = f"{self.atom_type[random_choice]}-{self.typedict[key]}"
277
289
  if mapping in self.possible_mappings:
278
- #this is a valid choice
290
+ # this is a valid choice
279
291
  self.mappings[random_choice] = mapping
280
292
  found = True
281
293
  if found:
282
- #finish up, change the array, and break
283
- #to_del.append(random_choice)
294
+ # finish up, change the array, and break
295
+ # to_del.append(random_choice)
284
296
  marked_atoms.remove(random_choice)
285
297
  break
286
- #if it was not found, the loop finished, throw error
298
+ # if it was not found, the loop finished, throw error
287
299
  if not found:
288
- raise ValueError("A possible transformation could not be found, please check the restrictions")
289
- #otherwise modify our marked atoms, list, and move on
290
- #for item in to_del:
300
+ raise ValueError(
301
+ "A possible transformation could not be found, please check the restrictions"
302
+ )
303
+ # otherwise modify our marked atoms, list, and move on
304
+ # for item in to_del:
291
305
  # marked_atoms.remove(item)
292
-
293
- self.unique_mappings, self.unique_mapping_counts = np.unique(self.mappings, return_counts=True)
294
306
 
295
- #now make the transformation dict
307
+ self.unique_mappings, self.unique_mapping_counts = np.unique(
308
+ self.mappings, return_counts=True
309
+ )
310
+
311
+ # now make the transformation dict
296
312
  self.transformation_list = []
297
313
  for count, mapping in enumerate(self.unique_mappings):
298
314
  mapsplit = mapping.split("-")
299
315
  if not mapsplit[0] == mapsplit[1]:
300
316
  transformation_dict = {}
301
- transformation_dict["primary_element"] = self.reversetypedict[int(mapsplit[0])]
302
- transformation_dict["secondary_element"] = self.reversetypedict[int(mapsplit[1])]
317
+ transformation_dict["primary_element"] = self.reversetypedict[
318
+ int(mapsplit[0])
319
+ ]
320
+ transformation_dict["secondary_element"] = self.reversetypedict[
321
+ int(mapsplit[1])
322
+ ]
303
323
  transformation_dict["count"] = self.unique_mapping_counts[count]
304
324
  self.transformation_list.append(transformation_dict)
305
325
 
@@ -307,59 +327,107 @@ class CompositionTransformation:
307
327
  self.update_typedicts()
308
328
  self.compute_possible_mappings()
309
329
  self.update_mappings()
310
-
330
+
311
331
  def prepare_pair_lists(self):
312
332
  self.pair_list_old = []
313
333
  self.pair_list_new = []
314
334
  for mapping in self.unique_mappings:
315
335
  map_split = mapping.split("-")
316
- #conserved atom
317
- if (map_split[0]==map_split[1]):
336
+ # conserved atom
337
+ if map_split[0] == map_split[1]:
318
338
  self.pair_list_old.append(self.reversetypedict[int(map_split[0])])
319
339
  self.pair_list_new.append(self.reversetypedict[int(map_split[0])])
320
340
  else:
321
341
  self.pair_list_old.append(self.reversetypedict[int(map_split[0])])
322
- self.pair_list_new.append(self.reversetypedict[int(map_split[1])])
323
- self.new_atomtype = np.array(range(len(self.unique_mappings)))+1
342
+ self.pair_list_new.append(self.reversetypedict[int(map_split[1])])
343
+ self.new_atomtype = np.array(range(len(self.unique_mappings))) + 1
324
344
  self.mappingdict = dict(zip(self.unique_mappings, self.new_atomtype))
325
-
345
+
326
346
  def update_types(self):
327
347
  for x in range(len(self.atom_type)):
328
348
  self.atom_type[x] = self.mappingdict[self.mappings[x]]
329
-
330
- #smartify these loops
331
- #npyscal = len(self.pyscal_structure.atoms.types)
349
+
350
+ # smartify these loops
351
+ # npyscal = len(self.pyscal_structure.atoms.types)
332
352
  self.pyscal_structure.atoms.types = self.atom_type
333
- #for count in range(npyscal)):
353
+ # for count in range(npyscal)):
334
354
  # self.pyscal_structure.atoms.types[count] = self.atom_type[count]
335
-
355
+
336
356
  def iselement(self, symbol):
337
357
  try:
338
358
  _ = element(symbol)
339
359
  return True
340
360
  except:
341
361
  return False
342
-
362
+
343
363
  def update_pair_coeff(self, pair_coeff):
364
+ """
365
+ Update pair_coeff command with new element specifications.
366
+
367
+ Handles both single-file formats (EAM alloy):
368
+ pair_coeff * * potential.eam.alloy El1 El2
369
+
370
+ And two-file formats (MEAM):
371
+ pair_coeff * * library.meam El1 El2 potential.meam El1 El2
372
+
373
+ For MEAM potentials, both element specifications are updated identically.
374
+ """
344
375
  pcsplit = pair_coeff.strip().split()
345
- pc_before = []
346
- pc_after = []
347
-
348
- started = False
349
- stopped = False
350
-
351
- for p in pcsplit:
352
- if ((not self.iselement(p)) and (not started)):
353
- pc_before.append(p)
354
- elif (self.iselement(p) and (not started)):
355
- started = True
356
- elif ((not self.iselement(p)) and started):
357
- stopped = True
358
- elif ((not self.iselement(p)) and stopped):
359
- pc_after.append(p)
360
-
361
- pc_old = " ".join([*pc_before, *self.pair_list_old, *pc_after])
362
- pc_new = " ".join([*pc_before, *self.pair_list_new, *pc_after])
376
+ result_parts = []
377
+ i = 0
378
+
379
+ while i < len(pcsplit):
380
+ token = pcsplit[i]
381
+
382
+ # Check if this token starts an element specification
383
+ # (either it's an element, or the next token is an element)
384
+ if self.iselement(token):
385
+ # Found start of element list - collect all consecutive elements
386
+ element_group = []
387
+ while i < len(pcsplit) and self.iselement(pcsplit[i]):
388
+ element_group.append(pcsplit[i])
389
+ i += 1
390
+
391
+ # Determine which element list to use based on what we found
392
+ # If element_group matches our pair_list_old, replace with pair_list_new
393
+ # Otherwise replace with pair_list_old (for the old/reference command)
394
+ if element_group == self.pair_list_old or set(element_group) == set(
395
+ self.element
396
+ ):
397
+ # This needs special handling - we'll mark position for later
398
+ result_parts.append("__ELEMENTS__")
399
+ else:
400
+ # Keep non-matching element groups as-is
401
+ result_parts.extend(element_group)
402
+ else:
403
+ # Non-element token (potential file, wildcards, options, etc.)
404
+ result_parts.append(token)
405
+ i += 1
406
+
407
+ # Now build old and new commands by replacing __ELEMENTS__ markers
408
+ pc_old_parts = [
409
+ self.pair_list_old if p == "__ELEMENTS__" else [p] for p in result_parts
410
+ ]
411
+ pc_new_parts = [
412
+ self.pair_list_new if p == "__ELEMENTS__" else [p] for p in result_parts
413
+ ]
414
+
415
+ # Flatten the lists
416
+ pc_old = " ".join(
417
+ [
418
+ item
419
+ for sublist in pc_old_parts
420
+ for item in (sublist if isinstance(sublist, list) else [sublist])
421
+ ]
422
+ )
423
+ pc_new = " ".join(
424
+ [
425
+ item
426
+ for sublist in pc_new_parts
427
+ for item in (sublist if isinstance(sublist, list) else [sublist])
428
+ ]
429
+ )
430
+
363
431
  return pc_old, pc_new
364
432
 
365
433
  def get_swap_types(self):
@@ -369,42 +437,42 @@ class CompositionTransformation:
369
437
  swap_list = []
370
438
  for mapping in self.unique_mappings:
371
439
  map_split = mapping.split("-")
372
- #conserved atom
373
- if (map_split[0]==map_split[1]):
440
+ # conserved atom
441
+ if map_split[0] == map_split[1]:
374
442
  pass
375
443
  else:
376
444
  first_type = map_split[0]
377
445
  second_type = map_split[1]
378
- first_map = f'{first_type}-{first_type}'
446
+ first_map = f"{first_type}-{first_type}"
379
447
  second_map = mapping
380
448
 
381
- #get the numbers from dict
449
+ # get the numbers from dict
382
450
  first_swap_type = self.mappingdict[first_map]
383
451
  second_swap_type = self.mappingdict[second_map]
384
452
 
385
453
  swap_list.append([first_swap_type, second_swap_type])
386
454
  return swap_list[0]
387
-
388
-
455
+
389
456
  def write_structure(self, outfilename):
390
- #create some species dict
391
- #just to trick ase to write
457
+ # create some species dict
458
+ # just to trick ase to write
392
459
  utypes = np.unique(self.pyscal_structure.atoms["types"])
393
- element_list = list(element_dict.keys())
394
- element_type_dict = {str(u):element_list[count] for count, u in enumerate(utypes)}
395
- species = [element_type_dict[str(x)] for x in self.pyscal_structure.atoms["types"]]
460
+ element_list = list(element_dict.keys())
461
+ element_type_dict = {
462
+ str(u): element_list[count] for count, u in enumerate(utypes)
463
+ }
464
+ species = [
465
+ element_type_dict[str(x)] for x in self.pyscal_structure.atoms["types"]
466
+ ]
396
467
  self.pyscal_structure.atoms["species"] = species
397
- self.pyscal_structure.write.file(outfilename, format='lammps-data')
398
-
468
+ self.pyscal_structure.write.file(outfilename, format="lammps-data")
399
469
 
400
-
401
-
402
470
  def prepare_mappings(self):
403
471
  self.atom_mark = []
404
472
  self.atom_species = []
405
473
  self.mappings = []
406
474
  self.unique_mappings = []
407
-
475
+
408
476
  self.get_composition_transformation()
409
477
  self.convert_to_pyscal()
410
478
 
@@ -412,4 +480,4 @@ class CompositionTransformation:
412
480
  self.update_mark_atoms()
413
481
  self.get_mappings()
414
482
  self.prepare_pair_lists()
415
- self.update_types()
483
+ self.update_types()