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/helpers.py CHANGED
@@ -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.
@@ -34,6 +34,7 @@ from ase.io import read, write
34
34
  import pyscal3.core as pc
35
35
  from pyscal3.trajectory import Trajectory
36
36
 
37
+
37
38
  class LammpsScript:
38
39
  def __init__(self):
39
40
  self.script = []
@@ -42,12 +43,14 @@ class LammpsScript:
42
43
  self.script.append(command_str)
43
44
 
44
45
  def write(self, infile):
45
- with open(infile, 'w') as fout:
46
+ with open(infile, "w") as fout:
46
47
  for line in self.script:
47
- fout.write(f'{line}\n')
48
+ fout.write(f"{line}\n")
49
+
48
50
 
49
- def create_object(cores, directory, timestep, cmdargs="",
50
- init_commands=(), script_mode=False):
51
+ def create_object(
52
+ cores, directory, timestep, cmdargs="", init_commands=(), script_mode=False
53
+ ):
51
54
  """
52
55
  Create LAMMPS object
53
56
 
@@ -71,28 +74,30 @@ def create_object(cores, directory, timestep, cmdargs="",
71
74
  else:
72
75
  if cmdargs == "":
73
76
  cmdargs = None
74
- lmp = LammpsLibrary(
75
- cores=cores, working_directory=directory, cmdargs=cmdargs
76
- )
77
-
78
- commands = [["units", "metal"],
79
- ["boundary", "p p p"],
80
- ["atom_style", "atomic"],
81
- ["timestep", str(timestep)],
82
- ["box", "tilt large"]]
77
+ elif isinstance(cmdargs, str):
78
+ cmdargs = cmdargs.split()
79
+ lmp = LammpsLibrary(cores=cores, working_directory=directory, cmdargs=cmdargs)
80
+
81
+ commands = [
82
+ ["units", "metal"],
83
+ ["boundary", "p p p"],
84
+ ["atom_style", "atomic"],
85
+ ["timestep", str(timestep)],
86
+ ["box", "tilt large"],
87
+ ]
83
88
 
84
89
  if len(init_commands) > 0:
85
- #we need to replace some initial commands
90
+ # we need to replace some initial commands
86
91
  for rc in init_commands:
87
- #split the command
92
+ # split the command
88
93
  raw = rc.split()
89
94
  for x in range(len(commands)):
90
95
  if raw[0] == commands[x][0]:
91
- #we found a matching command
96
+ # we found a matching command
92
97
  commands[x] = [rc]
93
98
  break
94
99
  else:
95
- #its a new command, add it to the list
100
+ # its a new command, add it to the list
96
101
  commands.append([rc])
97
102
 
98
103
  for command in commands:
@@ -121,12 +126,12 @@ def create_structure(lmp, calc):
121
126
 
122
127
 
123
128
  def set_mass(lmp, options):
124
- if options.mode == 'composition_scaling':
125
- lmp.command(f'mass * {options.mass[-1]}')
129
+ if options.mode == "composition_scaling":
130
+ lmp.command(f"mass * {options.mass[-1]}")
126
131
 
127
132
  else:
128
133
  for i in range(options.n_elements):
129
- lmp.command(f'mass {i+1} {options.mass[i]}')
134
+ lmp.command(f"mass {i + 1} {options.mass[i]}")
130
135
  return lmp
131
136
 
132
137
 
@@ -144,19 +149,21 @@ def set_potential(lmp, options):
144
149
  -------
145
150
  lmp : LammpsLibrary object
146
151
  """
147
- #lmp.pair_style(options.pair_style_with_options[0])
148
- #lmp.pair_coeff(options.pair_coeff[0])
149
- lmp.command(f'pair_style {options._pair_style_with_options[0]}')
150
- lmp.command(f'pair_coeff {options.pair_coeff[0]}')
152
+ # lmp.pair_style(options.pair_style_with_options[0])
153
+ # lmp.pair_coeff(options.pair_coeff[0])
154
+ lmp.command(f"pair_style {options._pair_style_with_options[0]}")
155
+ lmp.command(f"pair_coeff {options.pair_coeff[0]}")
151
156
 
152
157
  lmp = set_mass(lmp, options)
153
158
 
154
159
  return lmp
155
160
 
161
+
156
162
  def read_data(lmp, file):
157
163
  lmp.command(f"read_data {file}")
158
164
  return lmp
159
165
 
166
+
160
167
  def get_structures(file, species, index=None):
161
168
  traj = Trajectory(file)
162
169
  if index is None:
@@ -165,6 +172,7 @@ def get_structures(file, species, index=None):
165
172
  aseobjs = traj[index].to_ase(species=species)
166
173
  return aseobjs
167
174
 
175
+
168
176
  def remap_box(lmp, x, y, z):
169
177
  lmp.command("run 0")
170
178
  lmp.command(
@@ -222,8 +230,15 @@ def write_data(lmp, file):
222
230
  lmp.command(f"write_data {file}")
223
231
  return lmp
224
232
 
233
+
225
234
  def prepare_log(file, screen=False):
226
235
  logger = logging.getLogger(__name__)
236
+
237
+ # Remove all existing handlers to prevent duplicate logging
238
+ for handler in logger.handlers[:]:
239
+ handler.close()
240
+ logger.removeHandler(handler)
241
+
227
242
  handler = logging.FileHandler(file)
228
243
  formatter = logging.Formatter("%(asctime)s %(name)-12s %(levelname)-8s %(message)s")
229
244
  handler.setFormatter(formatter)
@@ -238,6 +253,7 @@ def prepare_log(file, screen=False):
238
253
  logger.addHandler(scr)
239
254
  return logger
240
255
 
256
+
241
257
  def check_if_any_is_none(data):
242
258
  """
243
259
  Check if any elements of a list is None, if so return True
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.5"
52
+ __version__ = "1.4.12"
53
53
 
54
54
 
55
55
  def _check_equal(val):
@@ -92,6 +92,49 @@ def _to_float(val):
92
92
  return [float(x) for x in val]
93
93
 
94
94
 
95
+ def _extract_elements_from_pair_coeff(pair_coeff_string):
96
+ """
97
+ Extract element symbols from pair_coeff string.
98
+ Returns None if pair_coeff doesn't contain element specifications.
99
+
100
+ Parameters
101
+ ----------
102
+ pair_coeff_string : str
103
+ The pair_coeff command string (e.g., "* * potential.eam.fs Cu Zr")
104
+
105
+ Returns
106
+ -------
107
+ list or None
108
+ List of element symbols in order, or None if no elements found
109
+ """
110
+ if pair_coeff_string is None:
111
+ return None
112
+
113
+ pcsplit = pair_coeff_string.strip().split()
114
+ elements = []
115
+
116
+ # Start collecting after we find element symbols
117
+ # Elements are typically after the potential filename
118
+ started = False
119
+
120
+ for p in pcsplit:
121
+ # Check if this looks like an element symbol
122
+ # Element symbols are 1-2 characters, start with uppercase
123
+ if len(p) <= 2 and p[0].isupper():
124
+ try:
125
+ # Verify it's a valid element using mendeleev
126
+ _ = mendeleev.element(p)
127
+ elements.append(p)
128
+ started = True
129
+ except:
130
+ # Not a valid element, might be done collecting
131
+ if started:
132
+ # We already started collecting elements and hit a non-element
133
+ break
134
+
135
+ return elements if len(elements) > 0 else None
136
+
137
+
95
138
  class UFMP(BaseModel, title="UFM potential input options"):
96
139
  p: Annotated[float, Field(default=50.0)]
97
140
  sigma: Annotated[float, Field(default=1.5)]
@@ -165,7 +208,6 @@ class Queue(BaseModel, title="Options for configuring queue"):
165
208
  memory: Annotated[str, Field(default="3GB")]
166
209
  commands: Annotated[List, Field(default=[])]
167
210
  options: Annotated[List, Field(default=[])]
168
- modules: Annotated[List, Field(default=[])]
169
211
 
170
212
 
171
213
  class Tolerance(BaseModel, title="Tolerance settings for convergence"):
@@ -308,12 +350,48 @@ class Calculation(BaseModel, title="Main input class"):
308
350
 
309
351
  @model_validator(mode="after")
310
352
  def _validate_all(self) -> "Input":
311
-
312
353
  if not (len(self.element) == len(self.mass)):
313
354
  raise ValueError("mass and elements should have same length")
314
355
 
315
356
  self.n_elements = len(self.element)
316
357
 
358
+ # Validate element/mass/pair_coeff ordering consistency
359
+ # This is critical for multi-element systems where LAMMPS type numbers
360
+ # are assigned based on element order: element[0]=Type1, element[1]=Type2, etc.
361
+ if (
362
+ len(self.element) > 1
363
+ and self.pair_coeff is not None
364
+ and len(self.pair_coeff) > 0
365
+ ):
366
+ extracted_elements = _extract_elements_from_pair_coeff(self.pair_coeff[0])
367
+
368
+ if extracted_elements is not None:
369
+ # pair_coeff specifies elements - check ordering
370
+ if set(extracted_elements) != set(self.element):
371
+ raise ValueError(
372
+ f"Element mismatch between 'element' and 'pair_coeff'!\n"
373
+ f" element: {self.element}\n"
374
+ f" pair_coeff: {extracted_elements}\n"
375
+ f"The elements specified must be the same."
376
+ )
377
+
378
+ if list(extracted_elements) != list(self.element):
379
+ raise ValueError(
380
+ f"Element ordering mismatch detected!\n\n"
381
+ f" element: {self.element}\n"
382
+ f" pair_coeff: {extracted_elements}\n"
383
+ f" mass: {self.mass}\n\n"
384
+ f"For multi-element systems, all three must be in the SAME order.\n\n"
385
+ f"Why this matters:\n"
386
+ f" - Element order determines LAMMPS type numbers:\n"
387
+ f" element[0] → Type 1, element[1] → Type 2, etc.\n"
388
+ f" - The pair_coeff elements must match this type order\n"
389
+ f" - The mass values must correspond to the same order\n"
390
+ f" - Composition transformations depend on this ordering\n\n"
391
+ f"Please reorder your input so element, mass, and pair_coeff\n"
392
+ f"all use the same element ordering."
393
+ )
394
+
317
395
  self._pressure_input = copy.copy(self.pressure)
318
396
  if self.pressure is None:
319
397
  self._iso = True
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(f'Maximum energy dissipation along the temperature scaling part: {ediss} eV/atom')
1221
- if np.abs(ediss) > 1E-4:
1222
- self.logger.warning(f'Found max energy dissipation of {ediss} along the temperature scaling path. Please ensure there are no structural changes!')
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")