"""
calphy: a Python library and command line interface for automated free
energy calculations.
Copyright 2021 (c) Sarath Menon^1, Yury Lysogorskiy^2, Ralf Drautz^2
^1: Max Planck Institut für Eisenforschung, Dusseldorf, Germany
^2: Ruhr-University Bochum, Bochum, Germany
calphy is published and distributed under the Academic Software License v1.0 (ASL).
calphy is distributed in the hope that it will be useful for non-commercial academic research,
but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
calphy API is published and distributed under the BSD 3-Clause "New" or "Revised" License
See the LICENSE FILE for more details.
More information about the program can be found in:
Menon, Sarath, Yury Lysogorskiy, Jutta Rogal, and Ralf Drautz.
“Automated Free Energy Calculation from Atomistic Simulations.” Physical Review Materials 5(10), 2021
DOI: 10.1103/PhysRevMaterials.5.103801
For more information contact:
sarath.menon@ruhr-uni-bochum.de/yury.lysogorskiy@icams.rub.de
"""
from typing_extensions import Annotated
from typing import Any, Callable, List, ClassVar, Optional, Union
from pydantic import (
BaseModel,
Field,
ValidationError,
model_validator,
conlist,
PrivateAttr,
)
from pydantic.functional_validators import AfterValidator, BeforeValidator
from annotated_types import Len
import mendeleev
from tqdm import tqdm
import yaml
import numpy as np
import copy
import datetime
import itertools
import os
import warnings
from pyscal3 import System
from pyscal3.core import structure_dict, element_dict, _make_crystal
from ase.io import read, write
import shutil
__version__ = "1.4.2"
def _check_equal(val):
if not (val[0] == val[1] == val[2]):
return False
return True
[docs]def to_list(v: Any) -> List[Any]:
return np.atleast_1d(v)
def _to_str(val):
if np.isscalar(val):
return str(val)
else:
return [str(x) for x in val]
def _to_int(val):
if np.isscalar(val):
return int(val)
else:
return [int(x) for x in val]
def _to_none(val):
if val in [
"none",
"None",
]:
return None
return val
def _to_float(val):
if np.isscalar(val):
return float(val)
else:
return [float(x) for x in val]
[docs]class UFMP(BaseModel, title="UFM potential input options"):
p: Annotated[float, Field(default=50.0)]
sigma: Annotated[float, Field(default=1.5)]
[docs]class MonteCarlo(
BaseModel, title="Options for Monte Carlo moves during particle swap moves"
):
n_steps: Annotated[
int, Field(default=1, gt=0, description="perform swap moves every n_step")
]
n_swaps: Annotated[
int, Field(default=0, ge=0, description="number of swap moves to perform")
]
reverse_swap: Annotated[bool, Field(default=True)]
swap_types: Annotated[
conlist(int, min_length=2, max_length=2),
Field(default=[1, 2], description="which atoms to swap between"),
]
[docs]class CompositionScaling(BaseModel, title="Composition scaling input options"):
_input_chemical_composition: PrivateAttr(default=None)
output_chemical_composition: Annotated[dict, Field(default={}, required=False)]
restrictions: Annotated[
List[str], BeforeValidator(to_list), Field(default=[], required=False)
]
[docs]class MD(BaseModel, title="MD specific input options"):
timestep: Annotated[
float,
Field(
default=0.001,
description="timestep for md simulation",
example="timestep: 0.001",
),
]
n_small_steps: Annotated[int, Field(default=10000, gt=0)]
n_every_steps: Annotated[int, Field(default=10, gt=0)]
n_repeat_steps: Annotated[int, Field(default=10, gt=0)]
n_cycles: Annotated[int, Field(default=100, gt=0)]
thermostat_damping: Annotated[
Union[float, conlist(float, min_length=2, max_length=2)],
Field(default=0.1, gt=0),
]
barostat_damping: Annotated[
Union[float, conlist(float, min_length=2, max_length=2)],
Field(default=0.1, gt=0),
]
cmdargs: Annotated[str, Field(default="")]
init_commands: Annotated[List, Field(default=[])]
[docs]class NoseHoover(BaseModel, title="Specific input options for Nose-Hoover thermostat"):
thermostat_damping: Annotated[float, Field(default=0.1, gt=0)]
barostat_damping: Annotated[float, Field(default=0.1, gt=0)]
[docs]class Berendsen(BaseModel, title="Specific input options for Berendsen thermostat"):
thermostat_damping: Annotated[float, Field(default=100.0, gt=0)]
barostat_damping: Annotated[float, Field(default=100.0, gt=0)]
[docs]class Queue(BaseModel, title="Options for configuring queue"):
scheduler: Annotated[str, Field(default="local")]
cores: Annotated[int, Field(default=1, gt=0)]
jobname: Annotated[str, Field(default="calphy")]
walltime: Annotated[str, Field(default="23:59:00")]
queuename: Annotated[str, Field(default="")]
memory: Annotated[str, Field(default="3GB")]
commands: Annotated[List, Field(default=[])]
options: Annotated[List, Field(default=[])]
modules: Annotated[List, Field(default=[])]
[docs]class Tolerance(BaseModel, title="Tolerance settings for convergence"):
lattice_constant: Annotated[float, Field(default=0.0002, ge=0)]
spring_constant: Annotated[float, Field(default=0.1, gt=0)]
solid_fraction: Annotated[float, Field(default=0.7, ge=0)]
liquid_fraction: Annotated[float, Field(default=0.05, ge=0)]
pressure: Annotated[float, Field(default=0.5, ge=0)]
[docs]class MeltingTemperature(BaseModel, title="Input options for melting temperature mode"):
guess: Annotated[Union[float, None], Field(default=None, gt=0)]
step: Annotated[int, Field(default=200, ge=20)]
attempts: Annotated[int, Field(default=5, ge=1)]
[docs]class Calculation(BaseModel, title="Main input class"):
monte_carlo: Optional[MonteCarlo] = MonteCarlo()
composition_scaling: Optional[CompositionScaling] = CompositionScaling()
md: Optional[MD] = MD()
nose_hoover: Optional[NoseHoover] = NoseHoover()
berendsen: Optional[Berendsen] = Berendsen()
queue: Optional[Queue] = Queue()
tolerance: Optional[Tolerance] = Tolerance()
uhlenbeck_ford_model: Optional[UFMP] = UFMP()
melting_temperature: Optional[MeltingTemperature] = MeltingTemperature()
element: Annotated[List[str], BeforeValidator(to_list), Field(default=[])]
n_elements: Annotated[int, Field(default=0)]
mass: Annotated[List[float], BeforeValidator(to_list), Field(default=[])]
_element_dict: dict = PrivateAttr(default={})
kernel: Annotated[int, Field(default=0)]
inputfile: Annotated[str, Field(default="")]
mode: Annotated[Union[str, None], Field(default=None)]
lattice: Annotated[str, Field(default="")]
file_format: Annotated[str, Field(default="lammps-data")]
# pressure properties
pressure: Annotated[
Union[
None,
float,
conlist(float, min_length=1, max_length=2),
conlist(
conlist(float, min_length=3, max_length=3), min_length=1, max_length=2
),
],
Field(default=0),
]
_pressure_stop: float = PrivateAttr(default=None)
_pressure: float = PrivateAttr(default=None)
_pressure_stop: float = PrivateAttr(default=None)
_pressure_input: Any = PrivateAttr(default=None)
_iso: bool = PrivateAttr(default=False)
_fix_lattice: bool = PrivateAttr(default=False)
temperature: Annotated[
Union[float, conlist(float, min_length=1, max_length=2)], Field(default=0)
]
temperature_high: Annotated[float, Field(default=0.0)]
_temperature: float = PrivateAttr(default=None)
_temperature_high: float = PrivateAttr(default=None)
_temperature_stop: float = PrivateAttr(default=None)
_temperature_input: float = PrivateAttr(default=None)
melting_cycle: Annotated[bool, Field(default=True)]
pair_style: Annotated[
Union[List[str], None], BeforeValidator(to_list), Field(default=None)
]
pair_coeff: Annotated[
Union[List[str], None], BeforeValidator(to_list), Field(default=None)
]
potential_file: Annotated[Union[str, None], Field(default=None)]
fix_potential_path: Annotated[bool, Field(default=True)]
_pair_style_with_options: List[str] = PrivateAttr(default=None)
reference_phase: Annotated[str, Field(default="")]
lattice_constant: Annotated[float, Field(default=0)]
repeat: Annotated[
conlist(int, min_length=3, max_length=3), Field(default=[1, 1, 1])
]
script_mode: Annotated[bool, Field(default=False)]
lammps_executable: Annotated[Union[str, None], Field(default=None)]
mpi_executable: Annotated[Union[str, None], Field(default=None)]
npt: Annotated[bool, Field(default=True)]
n_equilibration_steps: Annotated[int, Field(default=25000)]
n_switching_steps: Annotated[
Union[int, conlist(int, min_length=2, max_length=2)],
Field(default=[50000, 50000]),
]
_n_switching_steps: int = PrivateAttr(default=50000)
_n_sweep_steps: int = PrivateAttr(default=50000)
n_print_steps: Annotated[int, Field(default=0)]
n_iterations: Annotated[int, Field(default=1)]
equilibration_control: Annotated[Union[str, None], Field(default=None)]
folder_prefix: Annotated[Union[str, None], Field(default=None)]
# add second level options; for example spring constants
spring_constants: Annotated[Union[List[float], None], Field(default=None)]
# some input keywords that will be used for the phase diagram mode
phase_name: Annotated[str, Field(default="")]
reference_composition: Annotated[float, Field(default=0.00)]
# structure items
_structure: Any = PrivateAttr(default=None)
# just check for nlements in compscale
_totalelements = PrivateAttr(default=0)
@model_validator(mode="after")
def _validate_all(self) -> "Input":
if not (len(self.element) == len(self.mass)):
raise ValueError("mass and elements should have same length")
self.n_elements = len(self.element)
self._pressure_input = copy.copy(self.pressure)
if self.pressure is None:
self._iso = True
self._fix_lattice = True
self._pressure = None
self._pressure_stop = None
elif np.isscalar(self.pressure):
self._pressure = self.pressure
self._pressure_stop = self.pressure
self._iso = True
self._fix_lattice = False
elif np.shape(self.pressure) == (1,):
self._iso = True
self._fix_lattice = False
self._pressure = self.pressure[0]
self._pressure_stop = self.pressure[0]
elif np.shape(self.pressure) == (2,):
self._iso = True
self._fix_lattice = False
self._pressure = self.pressure[0]
self._pressure_stop = self.pressure[1]
elif np.shape(self.pressure) == (1, 3):
if not _check_equal(self.pressure[0]):
raise ValueError("All pressure terms must be equal")
self._iso = False
self._fix_lattice = False
self._pressure = self.pressure[0][0]
self._pressure_stop = self.pressure[0][0]
elif np.shape(self.pressure) == (2, 3):
if not (_check_equal(self.pressure[0]) and _check_equal(self.pressure[1])):
raise ValueError("All pressure terms must be equal")
self._iso = False
self._fix_lattice = False
self._pressure = self.pressure[0][0]
self._pressure_stop = self.pressure[1][0]
else:
raise ValueError("Unknown format for pressure")
self._temperature_input = copy.copy(self.temperature)
# guess a melting temp of the system, this will be mostly ignored
# chem = mendeleev.element(self.element[0])
# self._melting_temperature = chem.melting_point
try:
chem = mendeleev.element(self.element[0])
self._melting_temperature = chem.melting_point
except:
self._melting_temperature = None
if self.temperature == 0:
# the only situation in which it can be None is if mode is melting temp
if len(self.element) > 1:
raise ValueError(
"Cannot guess start temperature for more than one species, please specify"
)
# now try to guess
if self._melting_temperature is None:
raise ValueError(
"Could not guess start temperature for more than one species, please specify"
)
self._temperature = self._melting_temperature
self._temperature_stop = self._melting_temperature
if self.temperature_high == 0:
self._temperature_high = 2 * self._melting_temperature
else:
self._temperature_high = self.temperature_high
elif np.shape(np.atleast_1d(self.temperature)) == (1,):
temp = np.atleast_1d(self.temperature)
self._temperature = temp[0]
self._temperature_stop = temp[0]
if self.temperature_high == 0:
self._temperature_high = 2 * temp[0]
else:
self._temperature_high = self.temperature_high
elif np.shape(self.temperature) == (2,):
temp = self.temperature
self._temperature = temp[0]
self._temperature_stop = temp[1]
if self.temperature_high == 0:
self._temperature_high = 2 * temp[1]
else:
self._temperature_high = self.temperature_high
# fix pair styles
# two main lists
# _pair_style_with_options, read in as it is from file
# _pair_style_names, just the names of the pair styles
_pair_style_names = []
for ps in self.pair_style:
ps_split = ps.split()
_pair_style_names.append(ps_split[0])
# only set if its None
self._pair_style_with_options = self.pair_style
self._pair_style_names = _pair_style_names
# now fix pair coeffs with path
if self.fix_potential_path:
self.pair_coeff = self.fix_paths(self.pair_coeff)
if np.isscalar(self.n_switching_steps):
self._n_sweep_steps = self.n_switching_steps
self._n_switching_steps = self.n_switching_steps
else:
self._n_sweep_steps = self.n_switching_steps[1]
self._n_switching_steps = self.n_switching_steps[0]
# here we also prepare lattice dict
for count, element in enumerate(self.element):
self._element_dict[element] = {}
self._element_dict[element]["mass"] = self.mass[count]
self._element_dict[element]["count"] = 0
self._element_dict[element]["composition"] = 0.0
self._element_dict[element]["atomic_number"] = mendeleev.element(
element
).atomic_number
# generate temporary filename if needed
write_structure_file = False
rename_structure_file = False
if self.lattice == "":
# fetch from dict
if len(self.element) > 1:
raise ValueError(
"Cannot create lattice for more than one element, provide a lammps-data file explicitly"
)
if self.element[0] in element_dict.keys():
self.lattice = element_dict[self.element[0]]["structure"]
self.lattice_constant = element_dict[self.element[0]][
"lattice_constant"
]
else:
raise ValueError(
"Could not find structure, please provide lattice and lattice_constant explicitely"
)
if self.repeat == [1, 1, 1]:
self.repeat = [5, 5, 5]
structure = _make_crystal(
self.lattice.lower(),
lattice_constant=self.lattice_constant,
repetitions=self.repeat,
element=self.element,
)
structure = structure.write.ase()
# extract composition
types, typecounts = np.unique(
structure.get_chemical_symbols(), return_counts=True
)
for c, t in enumerate(types):
self._element_dict[t]["count"] = typecounts[c]
self._element_dict[t]["composition"] = typecounts[c] / np.sum(
typecounts
)
self._natoms = len(structure)
self._original_lattice = self.lattice.lower()
write_structure_file = True
elif self.lattice.lower() in structure_dict.keys():
if len(self.element) > 1:
raise ValueError(
"Cannot create lattice for more than one element, provide a lammps-data file explicitly"
)
# this is a valid structure
if self.lattice_constant == 0:
# we try try to get lattice_constant
if self.element[0] in element_dict.keys():
self.lattice_constant = element_dict[self.element[0]][
"lattice_constant"
]
else:
raise ValueError("Please provide lattice_constant!")
# now create lattice
structure = _make_crystal(
self.lattice.lower(),
lattice_constant=self.lattice_constant,
repetitions=self.repeat,
element=self.element,
)
structure = structure.write.ase()
# extract composition
types, typecounts = np.unique(
structure.get_chemical_symbols(), return_counts=True
)
for c, t in enumerate(types):
self._element_dict[t]["count"] = typecounts[c]
self._element_dict[t]["composition"] = typecounts[c] / np.sum(
typecounts
)
# concdict_counts = {str(t): typecounts[c] for c, t in enumerate(types)}
# concdict_frac = {str(t): typecounts[c]/np.sum(typecounts) for c, t in enumerate(types)}
# self._composition = concdict_frac
# self._composition_counts = concdict_counts
self._natoms = len(structure)
self._original_lattice = self.lattice.lower()
write_structure_file = True
else:
# this is a file
if not os.path.exists(self.lattice):
raise ValueError(f"File {self.lattice} could not be found")
if self.file_format == "lammps-data":
# create atomic numbers for proper reading
Z_of_type = dict(
[
(count + 1, self._element_dict[element]["atomic_number"])
for count, element in enumerate(self.element)
]
)
structure = read(
self.lattice,
format="lammps-data",
style="atomic",
Z_of_type=Z_of_type,
)
# structure = System(aseobj, format='ase')
rename_structure_file = True
else:
raise TypeError("Only lammps-data files are supported!")
# extract composition
# this is the types read in from the file
types, typecounts = np.unique(
structure.get_chemical_symbols(), return_counts=True
)
for c, t in enumerate(types):
self._element_dict[t]["count"] = typecounts[c]
self._element_dict[t]["composition"] = typecounts[c] / np.sum(
typecounts
)
self._natoms = len(structure)
self._original_lattice = os.path.basename(self.lattice)
self.lattice = os.path.abspath(self.lattice)
# if needed, write the file out
if write_structure_file:
structure_filename = ".".join(
[self.create_identifier(), str(self.kernel), "data"]
)
structure_filename = os.path.join(os.getcwd(), structure_filename)
write(structure_filename, structure, format="lammps-data")
self.lattice = structure_filename
if rename_structure_file:
structure_filename = ".".join(
[self.create_identifier(), str(self.kernel), "data"]
)
structure_filename = os.path.join(os.getcwd(), structure_filename)
shutil.copy(self.lattice, structure_filename)
self.lattice = structure_filename
if self.mode == "composition_scaling":
# we also should check if actual contents are present
input_chem_comp = {}
for key, val in self._element_dict.items():
input_chem_comp[key] = val["count"]
self.composition_scaling._input_chemical_composition = input_chem_comp
# now we should check output chem comp and see there are no keys extra
for key in self.composition_scaling.output_chemical_composition.keys():
if (
key
not in self.composition_scaling._input_chemical_composition.keys()
):
raise ValueError(
"An element in output composition is not possible with the given potential"
)
natoms1 = np.sum(
[
val
for key, val in self.composition_scaling._input_chemical_composition.items()
]
)
natoms2 = np.sum(
[
val
for key, val in self.composition_scaling.output_chemical_composition.items()
]
)
if not (natoms1 == natoms2):
raise ValueError(
f"Input and output number of atoms are not conserved! Input {self.dict_to_string(self.input_chemical_composition)}, output {self.dict_to_string(self.output_chemical_composition)}, total atoms in structure {structure.natoms}"
)
return self
[docs] def fix_paths(self, potlist):
"""
Fix paths for potential files to complete ones
"""
fixedpots = []
for pot in potlist:
pcraw = pot.split()
if len(pcraw) >= 3:
filename = pcraw[2]
filename = os.path.abspath(filename)
if os.path.exists(filename):
pcnew = " ".join([*pcraw[:2], filename, *pcraw[3:]])
fixedpots.append(pcnew)
else:
fixedpots.append(pot)
else:
fixedpots.append(pot)
return fixedpots
[docs] def create_identifier(self):
"""
Generate an identifier
Parameters
----------
calc: dict
a calculation dict
Returns
-------
identistring: string
unique identification string
"""
# lattice processed
prefix = self.mode
ts = int(self._temperature)
if self._pressure is None:
ps = "None"
else:
ps = "%d" % (int(self._pressure))
l = self._original_lattice
l = l.split("/")
l = l[-1]
if self.folder_prefix is None:
identistring = "-".join(
[prefix, l.lower(), self.reference_phase, str(ts), str(ps)]
)
else:
identistring = "-".join(
[
self.folder_prefix,
prefix,
l.lower(),
self.reference_phase,
str(ts),
str(ps),
]
)
return identistring
[docs] def get_folder_name(self):
identistring = self.create_identifier()
simfolder = os.path.join(os.getcwd(), identistring)
return simfolder
[docs] def create_folders(self):
"""
Create the necessary folder for calculation
Parameters
----------
calc : dict
calculation block
Returns
-------
folder : string
create folder
"""
simfolder = self.get_folder_name()
# if folder exists, delete it -> then create
if os.path.exists(simfolder):
raise ValueError(
f"Simulation folder {simfolder} exists. Please remove and run again!"
)
os.mkdir(simfolder)
return simfolder
@property
def savefile(self):
simfolder = self.get_folder_name()
return os.path.join(simfolder, "job.npy")
[docs]def save_job(job):
filename = os.path.join(job.simfolder, "job.npy")
np.save(filename, job)
[docs]def load_job(filename):
job = np.load(filename, allow_pickle=True).flatten()[0]
return job
def _read_inputfile(file):
with open(file, "r") as fin:
data = yaml.safe_load(fin)
calculations = []
for count, calc in enumerate(tqdm(data["calculations"])):
calc["kernel"] = count
calc["inputfile"] = file
if "pressure" in calc.keys():
calc["pressure"] = _to_none(calc["pressure"])
calculations.append(Calculation(**calc))
return calculations
def _convert_legacy_inputfile(file, return_calcs=False):
with open(file, "r") as fin:
data = yaml.safe_load(fin)
if not "element" in data.keys():
# new format
raise ValueError("Not old format, exiting..")
# prepare combos
calculations = []
for cc, ci in enumerate(data["calculations"]):
mode = ci["mode"]
if mode == "melting_temperature":
calc = {}
for key in [
"md",
"queue",
"tolerance",
"melting_temperature",
"nose_hoover",
"berendsen",
"composition_scaling",
"temperature_high",
]:
if key in data.keys():
calc[key] = copy.copy(data[key])
for key in [
"element",
"mass",
"script_mode",
"lammps_executable",
"mpi_executable",
]:
if key in data.keys():
calc[key] = data[key]
for key in [
"mode",
"pair_style",
"pair_coeff",
"pair_style_options",
"npt",
"repeat",
"n_equilibration_steps",
"n_switching_steps",
"n_print_steps",
"n_iterations",
"potential_file",
"spring_constants",
"melting_cycle",
"equilibration_control",
"folder_prefix",
"temperature_high",
]:
if key in ci.keys():
calc[key] = ci[key]
# calc['pressure'] = float(np.atleast_1d(ci["pressure"]) if "pressure" in ci.keys() else np.atleast_1d(0))
# calc['temperature'] = float(np.atleast_1d(ci["temperature"]) if "temperature" in ci.keys() else np.atleast_1d(0))
# calc['lattice'] = str(ci["lattice"]) if "lattice" in ci.keys() else 'none'
# calc['reference_phase'] = str(ci["reference_phase"]) if "reference_phase" in ci.keys() else 'none'
# calc['lattice_constant'] = float(ci["lattice_constant"]) if "lattice_constant" in ci.keys() else 0
calc["kernel"] = cc
calc["inputfile"] = file
calculations.append(calc)
else:
pressure = np.atleast_1d(ci["pressure"])
temperature = np.atleast_1d(ci["temperature"])
lattice = np.atleast_1d(ci["lattice"])
reference_phase = np.atleast_1d(ci["reference_phase"])
if "lattice_constant" in ci.keys():
lattice_constant = np.atleast_1d(ci["lattice_constant"])
else:
lattice_constant = [0 for x in range(len(lattice))]
lat_props = [
{
"lattice": lattice[x],
"lattice_constant": lattice_constant[x],
"reference_phase": reference_phase[x],
}
for x in range(len(lattice))
]
if (mode == "fe") or (mode == "alchemy") or (mode == "composition_scaling"):
combos = itertools.product(lat_props, pressure, temperature)
elif mode == "ts" or mode == "tscale" or mode == "mts":
if not len(temperature) == 2:
raise ValueError("ts/tscale mode needs 2 temperature values")
temperature = [temperature]
combos = itertools.product(lat_props, pressure, temperature)
elif mode == "pscale":
if not len(pressure) == 2:
raise ValueError("pscale mode needs 2 pressure values")
pressure = [pressure]
combos = itertools.product(lat_props, pressure, temperature)
cc = 0
for combo in combos:
calc = {}
for key in [
"md",
"queue",
"tolerance",
"melting_temperature",
"nose_hoover",
"berendsen",
"composition_scaling",
"temperature_high",
]:
if key in data.keys():
calc[key] = copy.copy(data[key])
for key in [
"element",
"mass",
"script_mode",
"lammps_executable",
"mpi_executable",
]:
if key in data.keys():
calc[key] = data[key]
for key in [
"mode",
"pair_style",
"pair_coeff",
"pair_style_options",
"npt",
"repeat",
"n_equilibration_steps",
"n_switching_steps",
"n_print_steps",
"n_iterations",
"potential_file",
"spring_constants",
"melting_cycle",
"equilibration_control",
"folder_prefix",
"temperature_high",
]:
if key in ci.keys():
calc[key] = ci[key]
# print(combo)
calc["lattice"] = str(combo[0]["lattice"])
calc["lattice_constant"] = float(combo[0]["lattice_constant"])
calc["reference_phase"] = str(combo[0]["reference_phase"])
calc["pressure"] = _to_float(combo[1])
calc["temperature"] = _to_float(combo[2])
calc["kernel"] = cc
calc["inputfile"] = file
cc += 1
calculations.append(calc)
if return_calcs:
return calculations
else:
newdata = {}
newdata["calculations"] = calculations
# print(newdata)
outfile = "new." + file
warnings.warn(
f"Old style input file calphy < v2 found. Converted input in {outfile}. Please check!"
)
with open(outfile, "w") as fout:
yaml.safe_dump(newdata, fout)
return outfile