calphy 1.4.6__py3-none-any.whl → 1.4.13__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 +159 -54
- calphy/helpers.py +3 -1
- calphy/input.py +1 -2
- calphy/liquid.py +13 -0
- calphy/phase.py +55 -4
- calphy/phase_diagram.py +2 -1
- calphy/routines.py +6 -0
- calphy/solid.py +243 -186
- {calphy-1.4.6.dist-info → calphy-1.4.13.dist-info}/METADATA +1 -1
- calphy-1.4.13.dist-info/RECORD +25 -0
- {calphy-1.4.6.dist-info → calphy-1.4.13.dist-info}/WHEEL +1 -1
- calphy-1.4.6.dist-info/RECORD +0 -25
- {calphy-1.4.6.dist-info → calphy-1.4.13.dist-info}/entry_points.txt +0 -0
- {calphy-1.4.6.dist-info → calphy-1.4.13.dist-info}/licenses/LICENSE +0 -0
- {calphy-1.4.6.dist-info → calphy-1.4.13.dist-info}/top_level.txt +0 -0
|
@@ -172,7 +172,17 @@ class CompositionTransformation:
|
|
|
172
172
|
"""
|
|
173
173
|
Convert a given system to pyscal and give a dict of type mappings
|
|
174
174
|
"""
|
|
175
|
-
|
|
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
|
+
)
|
|
176
186
|
pstruct = pc.System(aseobj, format="ase")
|
|
177
187
|
|
|
178
188
|
# here we have to validate the input composition dict; and map it
|
|
@@ -188,10 +198,17 @@ class CompositionTransformation:
|
|
|
188
198
|
self.reversetypedict = dict(zip(atomtypes, atomsymbols))
|
|
189
199
|
self.natoms = self.pyscal_structure.natoms
|
|
190
200
|
|
|
191
|
-
|
|
192
|
-
|
|
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)
|
|
193
211
|
self.maxtype = self.actual_species + 1 # + self.new_species
|
|
194
|
-
# print(self.typedict)
|
|
195
212
|
|
|
196
213
|
def get_composition_transformation(self):
|
|
197
214
|
"""
|
|
@@ -215,26 +232,28 @@ class CompositionTransformation:
|
|
|
215
232
|
self.to_remove = to_remove
|
|
216
233
|
self.to_add = to_add
|
|
217
234
|
|
|
218
|
-
def get_random_index_of_species(self,
|
|
235
|
+
def get_random_index_of_species(self, species_name):
|
|
219
236
|
"""
|
|
220
|
-
Get a random index of a given species
|
|
237
|
+
Get a random index of a given species by element name
|
|
221
238
|
"""
|
|
222
|
-
ids = [count for count, x in enumerate(self.
|
|
239
|
+
ids = [count for count, x in enumerate(self.atom_species) if x == species_name]
|
|
223
240
|
return ids[np.random.randint(0, len(ids))]
|
|
224
241
|
|
|
225
242
|
def mark_atoms(self):
|
|
226
243
|
for i in range(self.natoms):
|
|
227
244
|
self.atom_mark.append(False)
|
|
228
245
|
|
|
246
|
+
# Use species (element symbols) instead of numeric types
|
|
247
|
+
self.atom_species = self.pyscal_structure.atoms.species
|
|
229
248
|
self.atom_type = self.pyscal_structure.atoms.types
|
|
230
|
-
self.mappings = [f"{x}-{x}" for x in self.
|
|
249
|
+
self.mappings = [f"{x}-{x}" for x in self.atom_species]
|
|
231
250
|
|
|
232
251
|
def update_mark_atoms(self):
|
|
233
252
|
self.marked_atoms = []
|
|
234
253
|
for key, val in self.to_remove.items():
|
|
235
|
-
#
|
|
254
|
+
# key is the element name (e.g., "Mg")
|
|
236
255
|
for i in range(100000):
|
|
237
|
-
rint = self.get_random_index_of_species(
|
|
256
|
+
rint = self.get_random_index_of_species(key)
|
|
238
257
|
if rint not in self.marked_atoms:
|
|
239
258
|
self.atom_mark[rint] = True
|
|
240
259
|
self.marked_atoms.append(rint)
|
|
@@ -257,14 +276,12 @@ class CompositionTransformation:
|
|
|
257
276
|
|
|
258
277
|
def compute_possible_mappings(self):
|
|
259
278
|
self.possible_mappings = []
|
|
260
|
-
# Now make a list of possible mappings
|
|
279
|
+
# Now make a list of possible mappings using element names
|
|
261
280
|
for key1, val1 in self.to_remove.items():
|
|
262
281
|
for key2, val2 in self.to_add.items():
|
|
263
282
|
mapping = f"{key1}-{key2}"
|
|
264
283
|
if mapping not in self.restrictions:
|
|
265
|
-
self.possible_mappings.append(
|
|
266
|
-
f"{self.typedict[key1]}-{self.typedict[key2]}"
|
|
267
|
-
)
|
|
284
|
+
self.possible_mappings.append(mapping)
|
|
268
285
|
|
|
269
286
|
def update_mappings(self):
|
|
270
287
|
marked_atoms = self.marked_atoms.copy()
|
|
@@ -272,7 +289,7 @@ class CompositionTransformation:
|
|
|
272
289
|
# now get all
|
|
273
290
|
|
|
274
291
|
# we to see if we can get val number of atoms from marked ones
|
|
275
|
-
if val
|
|
292
|
+
if val > len(marked_atoms):
|
|
276
293
|
raise ValueError(
|
|
277
294
|
f"Not enough atoms to choose {val} from {len(marked_atoms)} not possible"
|
|
278
295
|
)
|
|
@@ -284,8 +301,8 @@ class CompositionTransformation:
|
|
|
284
301
|
to_del = []
|
|
285
302
|
for x in range(len(self.marked_atoms)):
|
|
286
303
|
random_choice = np.random.choice(marked_atoms)
|
|
287
|
-
# find corresponding
|
|
288
|
-
mapping = f"{self.
|
|
304
|
+
# find corresponding mapping using species name
|
|
305
|
+
mapping = f"{self.atom_species[random_choice]}-{key}"
|
|
289
306
|
if mapping in self.possible_mappings:
|
|
290
307
|
# this is a valid choice
|
|
291
308
|
self.mappings[random_choice] = mapping
|
|
@@ -314,12 +331,8 @@ class CompositionTransformation:
|
|
|
314
331
|
mapsplit = mapping.split("-")
|
|
315
332
|
if not mapsplit[0] == mapsplit[1]:
|
|
316
333
|
transformation_dict = {}
|
|
317
|
-
transformation_dict["primary_element"] =
|
|
318
|
-
|
|
319
|
-
]
|
|
320
|
-
transformation_dict["secondary_element"] = self.reversetypedict[
|
|
321
|
-
int(mapsplit[1])
|
|
322
|
-
]
|
|
334
|
+
transformation_dict["primary_element"] = mapsplit[0]
|
|
335
|
+
transformation_dict["secondary_element"] = mapsplit[1]
|
|
323
336
|
transformation_dict["count"] = self.unique_mapping_counts[count]
|
|
324
337
|
self.transformation_list.append(transformation_dict)
|
|
325
338
|
|
|
@@ -333,25 +346,45 @@ class CompositionTransformation:
|
|
|
333
346
|
self.pair_list_new = []
|
|
334
347
|
for mapping in self.unique_mappings:
|
|
335
348
|
map_split = mapping.split("-")
|
|
336
|
-
# conserved atom
|
|
349
|
+
# conserved atom - mappings now use element names directly
|
|
337
350
|
if map_split[0] == map_split[1]:
|
|
338
|
-
self.pair_list_old.append(
|
|
339
|
-
self.pair_list_new.append(
|
|
351
|
+
self.pair_list_old.append(map_split[0])
|
|
352
|
+
self.pair_list_new.append(map_split[0])
|
|
340
353
|
else:
|
|
341
|
-
self.pair_list_old.append(
|
|
342
|
-
self.pair_list_new.append(
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
345
380
|
|
|
346
381
|
def update_types(self):
|
|
382
|
+
# Update atom_type based on mapping to new types
|
|
347
383
|
for x in range(len(self.atom_type)):
|
|
348
384
|
self.atom_type[x] = self.mappingdict[self.mappings[x]]
|
|
349
385
|
|
|
350
|
-
#
|
|
351
|
-
# npyscal = len(self.pyscal_structure.atoms.types)
|
|
386
|
+
# Update pyscal structure types
|
|
352
387
|
self.pyscal_structure.atoms.types = self.atom_type
|
|
353
|
-
# for count in range(npyscal)):
|
|
354
|
-
# self.pyscal_structure.atoms.types[count] = self.atom_type[count]
|
|
355
388
|
|
|
356
389
|
def iselement(self, symbol):
|
|
357
390
|
try:
|
|
@@ -392,7 +425,7 @@ class CompositionTransformation:
|
|
|
392
425
|
# If element_group matches our pair_list_old, replace with pair_list_new
|
|
393
426
|
# Otherwise replace with pair_list_old (for the old/reference command)
|
|
394
427
|
if element_group == self.pair_list_old or set(element_group) == set(
|
|
395
|
-
self.element
|
|
428
|
+
self.calc.element
|
|
396
429
|
):
|
|
397
430
|
# This needs special handling - we'll mark position for later
|
|
398
431
|
result_parts.append("__ELEMENTS__")
|
|
@@ -432,12 +465,22 @@ class CompositionTransformation:
|
|
|
432
465
|
|
|
433
466
|
def get_swap_types(self):
|
|
434
467
|
"""
|
|
435
|
-
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
|
|
436
479
|
"""
|
|
437
480
|
swap_list = []
|
|
438
481
|
for mapping in self.unique_mappings:
|
|
439
482
|
map_split = mapping.split("-")
|
|
440
|
-
# conserved atom
|
|
483
|
+
# conserved atom - skip
|
|
441
484
|
if map_split[0] == map_split[1]:
|
|
442
485
|
pass
|
|
443
486
|
else:
|
|
@@ -446,26 +489,88 @@ class CompositionTransformation:
|
|
|
446
489
|
first_map = f"{first_type}-{first_type}"
|
|
447
490
|
second_map = mapping
|
|
448
491
|
|
|
449
|
-
#
|
|
450
|
-
|
|
451
|
-
|
|
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])
|
|
452
505
|
|
|
453
|
-
|
|
454
|
-
return swap_list[0]
|
|
506
|
+
return swap_list[0] if swap_list else []
|
|
455
507
|
|
|
456
508
|
def write_structure(self, outfilename):
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
self.pyscal_structure.
|
|
468
|
-
|
|
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
|
+
# Support different ASE formats: "Atoms # atomic", "Atoms # full", or just "Atoms"
|
|
536
|
+
in_atoms_section = False
|
|
537
|
+
atom_idx = 0
|
|
538
|
+
for i, line in enumerate(lines):
|
|
539
|
+
# More flexible detection of Atoms section header
|
|
540
|
+
if "Atoms" in line:
|
|
541
|
+
in_atoms_section = True
|
|
542
|
+
continue
|
|
543
|
+
|
|
544
|
+
if in_atoms_section and line.strip():
|
|
545
|
+
parts = line.split()
|
|
546
|
+
if len(parts) >= 5: # atom_id type x y z
|
|
547
|
+
# Replace the type (column 1, 0-indexed) with our custom type
|
|
548
|
+
custom_type = self.pyscal_structure.atoms.types[atom_idx]
|
|
549
|
+
parts[1] = str(custom_type)
|
|
550
|
+
lines[i] = " " + " ".join(parts) + "\n"
|
|
551
|
+
atom_idx += 1
|
|
552
|
+
if atom_idx >= len(self.pyscal_structure.atoms.types):
|
|
553
|
+
break
|
|
554
|
+
|
|
555
|
+
# Verify all atoms were updated
|
|
556
|
+
expected_atoms = len(self.pyscal_structure.atoms.types)
|
|
557
|
+
if atom_idx != expected_atoms:
|
|
558
|
+
raise RuntimeError(
|
|
559
|
+
f"Failed to update all atoms in {outfilename}. "
|
|
560
|
+
f"Expected {expected_atoms} atoms but only updated {atom_idx}. "
|
|
561
|
+
f"This may indicate a problem with ASE LAMMPS file formatting."
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
# Update the number of atom types in the header
|
|
565
|
+
required_ntypes = len(self.pair_list_old)
|
|
566
|
+
for i, line in enumerate(lines):
|
|
567
|
+
if "atom types" in line:
|
|
568
|
+
lines[i] = f"{required_ntypes} atom types\n"
|
|
569
|
+
break
|
|
570
|
+
|
|
571
|
+
# Write the corrected file
|
|
572
|
+
with open(outfilename, "w") as f:
|
|
573
|
+
f.writelines(lines)
|
|
469
574
|
|
|
470
575
|
def prepare_mappings(self):
|
|
471
576
|
self.atom_mark = []
|
calphy/helpers.py
CHANGED
|
@@ -74,6 +74,8 @@ def create_object(
|
|
|
74
74
|
else:
|
|
75
75
|
if cmdargs == "":
|
|
76
76
|
cmdargs = None
|
|
77
|
+
elif isinstance(cmdargs, str):
|
|
78
|
+
cmdargs = cmdargs.split()
|
|
77
79
|
lmp = LammpsLibrary(cores=cores, working_directory=directory, cmdargs=cmdargs)
|
|
78
80
|
|
|
79
81
|
commands = [
|
|
@@ -129,7 +131,7 @@ def set_mass(lmp, options):
|
|
|
129
131
|
|
|
130
132
|
else:
|
|
131
133
|
for i in range(options.n_elements):
|
|
132
|
-
lmp.command(f"mass {i+1} {options.mass[i]}")
|
|
134
|
+
lmp.command(f"mass {i + 1} {options.mass[i]}")
|
|
133
135
|
return lmp
|
|
134
136
|
|
|
135
137
|
|
calphy/input.py
CHANGED
|
@@ -49,7 +49,7 @@ from pyscal3.core import structure_dict, element_dict, _make_crystal
|
|
|
49
49
|
from ase.io import read, write
|
|
50
50
|
import shutil
|
|
51
51
|
|
|
52
|
-
__version__ = "1.4.
|
|
52
|
+
__version__ = "1.4.13"
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def _check_equal(val):
|
|
@@ -350,7 +350,6 @@ class Calculation(BaseModel, title="Main input class"):
|
|
|
350
350
|
|
|
351
351
|
@model_validator(mode="after")
|
|
352
352
|
def _validate_all(self) -> "Input":
|
|
353
|
-
|
|
354
353
|
if not (len(self.element) == len(self.mass)):
|
|
355
354
|
raise ValueError("mass and elements should have same length")
|
|
356
355
|
|
calphy/liquid.py
CHANGED
|
@@ -23,6 +23,7 @@ sarath.menon@ruhr-uni-bochum.de/yury.lysogorskiy@icams.rub.de
|
|
|
23
23
|
|
|
24
24
|
import numpy as np
|
|
25
25
|
import yaml
|
|
26
|
+
import os
|
|
26
27
|
|
|
27
28
|
from calphy.integrators import *
|
|
28
29
|
import calphy.helpers as ph
|
|
@@ -100,6 +101,10 @@ class Liquid(cph.Phase):
|
|
|
100
101
|
# if melting cycle is over and still not melted, raise error
|
|
101
102
|
if not melted:
|
|
102
103
|
lmp.close()
|
|
104
|
+
# Preserve log file
|
|
105
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
106
|
+
if os.path.exists(logfile):
|
|
107
|
+
os.rename(logfile, os.path.join(self.simfolder, "melting.log.lammps"))
|
|
103
108
|
raise SolidifiedError(
|
|
104
109
|
"Liquid system did not melt, maybe try a higher thigh temperature."
|
|
105
110
|
)
|
|
@@ -175,6 +180,10 @@ class Liquid(cph.Phase):
|
|
|
175
180
|
self.dump_current_snapshot(lmp, "traj.equilibration_stage2.dat")
|
|
176
181
|
lmp = ph.write_data(lmp, "conf.equilibration.data")
|
|
177
182
|
lmp.close()
|
|
183
|
+
# Preserve log file
|
|
184
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
185
|
+
if os.path.exists(logfile):
|
|
186
|
+
os.rename(logfile, os.path.join(self.simfolder, "averaging.log.lammps"))
|
|
178
187
|
|
|
179
188
|
def run_integration(self, iteration=1):
|
|
180
189
|
"""
|
|
@@ -390,6 +399,10 @@ class Liquid(cph.Phase):
|
|
|
390
399
|
|
|
391
400
|
# close object
|
|
392
401
|
lmp.close()
|
|
402
|
+
# Preserve log file
|
|
403
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
404
|
+
if os.path.exists(logfile):
|
|
405
|
+
os.rename(logfile, os.path.join(self.simfolder, "integration.log.lammps"))
|
|
393
406
|
|
|
394
407
|
def thermodynamic_integration(self):
|
|
395
408
|
"""
|
calphy/phase.py
CHANGED
|
@@ -278,6 +278,12 @@ class Phase:
|
|
|
278
278
|
solids = ph.find_solid_fraction(os.path.join(self.simfolder, filename))
|
|
279
279
|
if solids / lmp.natoms < self.calc.tolerance.solid_fraction:
|
|
280
280
|
lmp.close()
|
|
281
|
+
# Preserve log file on error
|
|
282
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
283
|
+
if os.path.exists(logfile):
|
|
284
|
+
os.rename(
|
|
285
|
+
logfile, os.path.join(self.simfolder, "melted_error.log.lammps")
|
|
286
|
+
)
|
|
281
287
|
raise MeltedError(
|
|
282
288
|
"System melted, increase size or reduce temp!\n Solid detection algorithm only works with BCC/FCC/HCP/SC/DIA. Detection algorithm can be turned off by setting:\n tolerance.solid_fraction: 0"
|
|
283
289
|
)
|
|
@@ -287,6 +293,12 @@ class Phase:
|
|
|
287
293
|
solids = ph.find_solid_fraction(os.path.join(self.simfolder, filename))
|
|
288
294
|
if solids / lmp.natoms > self.calc.tolerance.liquid_fraction:
|
|
289
295
|
lmp.close()
|
|
296
|
+
# Preserve log file on error
|
|
297
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
298
|
+
if os.path.exists(logfile):
|
|
299
|
+
os.rename(
|
|
300
|
+
logfile, os.path.join(self.simfolder, "solidified_error.log.lammps")
|
|
301
|
+
)
|
|
290
302
|
raise SolidifiedError("System solidified, increase temperature")
|
|
291
303
|
|
|
292
304
|
def fix_nose_hoover(
|
|
@@ -594,6 +606,15 @@ class Phase:
|
|
|
594
606
|
|
|
595
607
|
if not converged:
|
|
596
608
|
lmp.close()
|
|
609
|
+
# Preserve log file on error
|
|
610
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
611
|
+
if os.path.exists(logfile):
|
|
612
|
+
os.rename(
|
|
613
|
+
logfile,
|
|
614
|
+
os.path.join(
|
|
615
|
+
self.simfolder, "pressure_convergence_error.log.lammps"
|
|
616
|
+
),
|
|
617
|
+
)
|
|
597
618
|
raise ValueError(
|
|
598
619
|
"Pressure did not converge after MD runs, maybe change lattice_constant and try?"
|
|
599
620
|
)
|
|
@@ -708,6 +729,15 @@ class Phase:
|
|
|
708
729
|
|
|
709
730
|
if not converged:
|
|
710
731
|
lmp.close()
|
|
732
|
+
# Preserve log file on error
|
|
733
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
734
|
+
if os.path.exists(logfile):
|
|
735
|
+
os.rename(
|
|
736
|
+
logfile,
|
|
737
|
+
os.path.join(
|
|
738
|
+
self.simfolder, "constrained_pressure_error.log.lammps"
|
|
739
|
+
),
|
|
740
|
+
)
|
|
711
741
|
raise ValueError("pressure did not converge")
|
|
712
742
|
|
|
713
743
|
def process_pressure(
|
|
@@ -1180,6 +1210,12 @@ class Phase:
|
|
|
1180
1210
|
|
|
1181
1211
|
# close the object
|
|
1182
1212
|
lmp.close()
|
|
1213
|
+
# Preserve log file
|
|
1214
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
1215
|
+
if os.path.exists(logfile):
|
|
1216
|
+
os.rename(
|
|
1217
|
+
logfile, os.path.join(self.simfolder, "reversible_scaling.log.lammps")
|
|
1218
|
+
)
|
|
1183
1219
|
|
|
1184
1220
|
self.logger.info("Please cite the following publications:")
|
|
1185
1221
|
if self.calc.mode == "mts":
|
|
@@ -1217,10 +1253,13 @@ class Phase:
|
|
|
1217
1253
|
return_values=return_values,
|
|
1218
1254
|
)
|
|
1219
1255
|
|
|
1220
|
-
self.logger.info(
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1256
|
+
self.logger.info(
|
|
1257
|
+
f"Maximum energy dissipation along the temperature scaling part: {ediss} eV/atom"
|
|
1258
|
+
)
|
|
1259
|
+
if np.abs(ediss) > 1e-4:
|
|
1260
|
+
self.logger.warning(
|
|
1261
|
+
f"Found max energy dissipation of {ediss} along the temperature scaling path. Please ensure there are no structural changes!"
|
|
1262
|
+
)
|
|
1224
1263
|
|
|
1225
1264
|
if return_values:
|
|
1226
1265
|
return res
|
|
@@ -1369,6 +1408,12 @@ class Phase:
|
|
|
1369
1408
|
lmp.command("run %d" % self.calc._n_sweep_steps)
|
|
1370
1409
|
|
|
1371
1410
|
lmp.close()
|
|
1411
|
+
# Preserve log file
|
|
1412
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
1413
|
+
if os.path.exists(logfile):
|
|
1414
|
+
os.rename(
|
|
1415
|
+
logfile, os.path.join(self.simfolder, "temperature_scaling.log.lammps")
|
|
1416
|
+
)
|
|
1372
1417
|
|
|
1373
1418
|
def pressure_scaling(self, iteration=1):
|
|
1374
1419
|
"""
|
|
@@ -1499,6 +1544,12 @@ class Phase:
|
|
|
1499
1544
|
lmp.command("run %d" % self.calc._n_sweep_steps)
|
|
1500
1545
|
|
|
1501
1546
|
lmp.close()
|
|
1547
|
+
# Preserve log file
|
|
1548
|
+
logfile = os.path.join(self.simfolder, "log.lammps")
|
|
1549
|
+
if os.path.exists(logfile):
|
|
1550
|
+
os.rename(
|
|
1551
|
+
logfile, os.path.join(self.simfolder, "pressure_scaling.log.lammps")
|
|
1552
|
+
)
|
|
1502
1553
|
|
|
1503
1554
|
self.logger.info("Please cite the following publications:")
|
|
1504
1555
|
self.logger.info("- 10.1016/j.commatsci.2022.111275")
|
calphy/phase_diagram.py
CHANGED
|
@@ -332,7 +332,8 @@ def _create_composition_array(comp_range, interval, reference):
|
|
|
332
332
|
is_reference = np.abs(comp_arr - reference) < COMPOSITION_TOLERANCE
|
|
333
333
|
elif len(comp_range) == 1:
|
|
334
334
|
comp_arr = [comp_range[0]]
|
|
335
|
-
|
|
335
|
+
# Check if this single composition equals the reference
|
|
336
|
+
is_reference = [np.abs(comp_range[0] - reference) < COMPOSITION_TOLERANCE]
|
|
336
337
|
else:
|
|
337
338
|
raise ValueError("Composition range should be scalar or list of two values!")
|
|
338
339
|
|
calphy/routines.py
CHANGED
|
@@ -94,6 +94,9 @@ class MeltingTemp:
|
|
|
94
94
|
calc["mode"] = "ts"
|
|
95
95
|
calc["temperature"] = [int(self.tmin), int(self.tmax)]
|
|
96
96
|
calc["reference_phase"] = 'solid'
|
|
97
|
+
# Preserve n_iterations from the original melting_temperature calculation
|
|
98
|
+
if "n_iterations" in data["calculations"][int(self.calc.kernel)]:
|
|
99
|
+
calc["n_iterations"] = data["calculations"][int(self.calc.kernel)]["n_iterations"]
|
|
97
100
|
calculations["calculations"].append(calc)
|
|
98
101
|
|
|
99
102
|
with open(self.calc.inputfile, 'r') as fin:
|
|
@@ -103,6 +106,9 @@ class MeltingTemp:
|
|
|
103
106
|
calc["mode"] = "ts"
|
|
104
107
|
calc["temperature"] = [int(self.tmin), int(self.tmax)]
|
|
105
108
|
calc["reference_phase"] = 'liquid'
|
|
109
|
+
# Preserve n_iterations from the original melting_temperature calculation
|
|
110
|
+
if "n_iterations" in data["calculations"][int(self.calc.kernel)]:
|
|
111
|
+
calc["n_iterations"] = data["calculations"][int(self.calc.kernel)]["n_iterations"]
|
|
106
112
|
calculations["calculations"].append(calc)
|
|
107
113
|
|
|
108
114
|
outfile = f'{self.calc.create_identifier()}.{self.attempts}.yaml'
|