def run_task(self, fw_spec): from pymatgen.analysis.eos import EOS tag = self["tag"] db_file = env_chk(self.get("db_file"), fw_spec) summary_dict = {"eos": self["eos"]} mmdb = MMVaspDb.from_db_file(db_file, admin=True) # get the optimized structure d = mmdb.collection.find_one({"task_label": "{} structure optimization".format(tag)}) structure = Structure.from_dict(d["calcs_reversed"][-1]["output"]['structure']) summary_dict["structure"] = structure.as_dict() # get the data(energy, volume, force constant) from the deformation runs docs = mmdb.collection.find({"task_label": {"$regex": "{} bulk_modulus*".format(tag)}, "formula_pretty": structure.composition.reduced_formula}) energies = [] volumes = [] for d in docs: s = Structure.from_dict(d["calcs_reversed"][-1]["output"]['structure']) energies.append(d["calcs_reversed"][-1]["output"]['energy']) volumes.append(s.volume) summary_dict["energies"] = energies summary_dict["volumes"] = volumes # fit the equation of state eos = EOS(self["eos"]) eos_fit = eos.fit(volumes, energies) summary_dict["results"] = dict(eos_fit.results) with open("bulk_modulus.json", "w") as f: f.write(json.dumps(summary_dict, default=DATETIME_HANDLER)) logger.info("BULK MODULUS CALCULATION COMPLETE")
def birch_murnaghan(self, title=None): """ Fit E,V data with Birch-Murnaghan EOS Parameters ---------- title : (str) Plot title Returns ------- plt : Matplotlib object. eos_fit : Pymatgen EosBase object. """ V, E = [], [] for j in self.jobs: V.append(j.initial_structure.lattice.volume) E.append(j.final_energy) eos = EOS(eos_name='birch_murnaghan') eos_fit = eos.fit(V, E) eos_fit.plot(width=10, height=10, text='', markersize=15, label='Birch-Murnaghan fit') plt.legend(loc=2, prop={'size': 20}) if title: plt.title(title, size=25) plt.tight_layout() return plt, eos_fit
def __init__(self, energies, volumes, structure, t_min=5, t_step=5, t_max=2000.0, eos="vinet", poisson=0.363615, gruneisen=True, bp2gru=1., mass_average_mode='arithmetic'): self.energies = energies self.volumes = volumes self.structure = structure self.temperatures = np.arange(t_min, t_max+t_step, t_step) self.eos_name = eos self.poisson = poisson self.bp2gru = bp2gru self.gruneisen = gruneisen self.natoms = self.structure.composition.num_atoms self.kb = physical_constants["Boltzmann constant in eV/K"][0] # calculate the average masses masses = np.array([e.atomic_mass for e in self.structure.species]) * physical_constants["atomic mass constant"][0] if mass_average_mode == 'arithmetic': self.avg_mass = np.mean(masses) elif mass_average_mode == 'geometric': self.avg_mass = gmean(masses) else: raise ValueError("DebyeModel mass_average_mode must be either 'arithmetic' or 'geometric'") # fit E and V and get the bulk modulus(used to compute the Debye temperature) self.eos = EOS(eos) self.ev_eos_fit = self.eos.fit(volumes, energies) self.bulk_modulus = self.ev_eos_fit.b0_GPa # in GPa self.calculate_F_el()
def eos_fit(self, eos_name="murnaghan"): """ Fit E(V) For the list of available models, see EOS.MODELS TODO: which default? all should return a list of fits """ # Read volumes and energies from the GSR files. energies, volumes = [], [] for label, gsr in self: energies.append(gsr.energy) volumes.append(gsr.structure.volume) # Note that eos.fit expects lengths in Angstrom, and energies in eV. if eos_name != "all": return EOS(eos_name=eos_name).fit(volumes, energies) else: # Use all the available models. fits, rows = [], [] for eos_name in EOS.MODELS: fit = EOS(eos_name=eos_name).fit(volumes, energies) fits.append(fit) rows.append(fit.results) import pandas as pd frame = pd.DataFrame(rows, index=EOS.MODELS, columns=list(rows[0].keys())) return fits, frame
def set_eos(self, eos_name): """ Updates the EOS used for the fit. Args: eos_name: string indicating the expression used to fit the energies. See pymatgen.analysis.eos.EOS. """ self.eos = EOS(eos_name)
def test_run_all_models(self): # these have been checked for plausibility, # but are not benchmarked against independently known values test_output = { "birch": { "b0": 0.5369258244952931, "b1": 4.178644231838501, "e0": -10.8428039082307, "v0": 40.98926572870838, }, "birch_murnaghan": { "b0": 0.5369258245417454, "b1": 4.178644235500821, "e0": -10.842803908240892, "v0": 40.98926572528106, }, "deltafactor": { "b0": 0.5369258245611414, "b1": 4.178644231924639, "e0": -10.842803908299294, "v0": 40.989265727927936, }, "murnaghan": { "b0": 0.5144967693786603, "b1": 3.9123862262572264, "e0": -10.836794514626673, "v0": 41.13757930387086, }, "numerical_eos": { "b0": 0.5557257614101998, "b1": 4.344039148405489, "e0": -10.847490826530702, "v0": 40.857200064982536, }, "pourier_tarantola": { "b0": 0.5667729960804602, "b1": 4.331688936974368, "e0": -10.851486685041658, "v0": 40.86770643373908, }, "vinet": { "b0": 0.5493839425156859, "b1": 4.3051929654936885, "e0": -10.846160810560756, "v0": 40.916875663779784, }, } for eos_name in EOS.MODELS: eos = EOS(eos_name=eos_name) _ = eos.fit(self.volumes, self.energies) for param in ("b0", "b1", "e0", "b0"): # TODO: solutions only stable to 2 decimal places # between different machines, this seems far too low? self.assertArrayAlmostEqual(_.results[param], test_output[eos_name][param], decimal=1)
def get_eos_fits_dataframe(self, eos_names="murnaghan"): """ Fit energy as function of volume to get the equation of state, equilibrium volume, bulk modulus and its derivative wrt to pressure. Args: eos_names: String or list of strings with EOS names. For the list of available models, see pymatgen.analysis.eos. Return: (fits, dataframe) namedtuple. fits is a list of ``EOSFit object`` dataframe is a |pandas-DataFrame| with the final results. """ # Read volumes and energies from the GSR files. energies, volumes = [], [] for label, gsr in self.items(): energies.append(float(gsr.energy)) volumes.append(float(gsr.structure.volume)) # Order data by volumes if needed. if np.any(np.diff(volumes) < 0): ves = sorted(zip(volumes, energies), key=lambda t: t[0]) volumes = [t[0] for t in ves] energies = [t[1] for t in ves] # Note that eos.fit expects lengths in Angstrom, and energies in eV. # I'm also monkey-patching the plot method. from pymatgen.analysis.eos import EOS if eos_names == "all": # Use all the available models. eos_names = [ n for n in EOS.MODELS if n not in ("deltafactor", "numerical_eos") ] else: eos_names = list_strings(eos_names) fits, index, rows = [], [], [] for eos_name in eos_names: try: fit = EOS(eos_name=eos_name).fit(volumes, energies) except Exception as exc: cprint("EOS %s raised exception:\n%s" % (eos_name, str(exc))) continue # Replace plot with plot_ax method fit.plot = fit.plot_ax fits.append(fit) index.append(eos_name) rows.append( OrderedDict([(aname, getattr(fit, aname)) for aname in ("v0", "e0", "b0_GPa", "b1")])) dataframe = pd.DataFrame( rows, index=index, columns=list(rows[0].keys()) if rows else None) return dict2namedtuple(fits=fits, dataframe=dataframe)
def run_task(self, fw_spec): from pymatgen.analysis.eos import EOS eos = self.get("eos", "vinet") tag = self["tag"] db_file = env_chk(self.get("db_file"), fw_spec) summary_dict = {"eos": eos} to_db = self.get("to_db", True) # collect and store task_id of all related tasks to make unique links with "tasks" collection all_task_ids = [] mmdb = VaspCalcDb.from_db_file(db_file, admin=True) # get the optimized structure d = mmdb.collection.find_one({"task_label": "{} structure optimization".format(tag)}) all_task_ids.append(d["task_id"]) structure = Structure.from_dict(d["calcs_reversed"][-1]["output"]['structure']) summary_dict["structure"] = structure.as_dict() summary_dict["formula_pretty"] = structure.composition.reduced_formula # get the data(energy, volume, force constant) from the deformation runs docs = mmdb.collection.find({"task_label": {"$regex": "{} bulk_modulus*".format(tag)}, "formula_pretty": structure.composition.reduced_formula}) energies = [] volumes = [] for d in docs: s = Structure.from_dict(d["calcs_reversed"][-1]["output"]['structure']) energies.append(d["calcs_reversed"][-1]["output"]['energy']) volumes.append(s.volume) all_task_ids.append(d["task_id"]) summary_dict["energies"] = energies summary_dict["volumes"] = volumes summary_dict["all_task_ids"] = all_task_ids # fit the equation of state eos = EOS(eos) eos_fit = eos.fit(volumes, energies) summary_dict["bulk_modulus"] = eos_fit.b0_GPa # TODO: find a better way for passing tags of the entire workflow to db - albalu if fw_spec.get("tags", None): summary_dict["tags"] = fw_spec["tags"] summary_dict["results"] = dict(eos_fit.results) summary_dict["created_at"] = datetime.utcnow() # db_file itself is required but the user can choose to pass the results to db or not if to_db: mmdb.collection = mmdb.db["eos"] mmdb.collection.insert_one(summary_dict) else: with open("bulk_modulus.json", "w") as f: f.write(json.dumps(summary_dict, default=DATETIME_HANDLER)) # TODO: @matk86 - there needs to be a builder to put it into materials collection... -computron logger.info("Bulk modulus calculation complete.")
def gibbs_minimizer(energies, volumes, mass, natoms, temperature=298.0, pressure=0, poisson=0.25, eos="murnaghan"): """ Fit the input energies and volumes to the equation of state to obtain the bulk modulus which is subsequently used to obtain the debye temperature. The debye temperature is then used to compute the vibrational free energy and the gibbs free energy as a function of volume, temperature and pressure. A second fit is preformed to get the functional form of gibbs free energy:(G, V, T, P). Finally G(V, P, T) is minimized with respect to V and the optimum value of G evaluated at V_opt, G_opt(V_opt, T, P), is returned. Args: energies (list): list of energies volumes (list): list of volumes mass (float): total mass natoms (int): number of atoms temperature (float): temperature in K pressure (float): pressure in GPa poisson (float): poisson ratio eos (str): name of the equation of state supported by pymatgen. See pymatgen.analysis.eos.py Returns: float: gibbs free energy at the given temperature and pressure minimized wrt volume. """ try: from scipy.optimize import minimize from scipy.integrate import quadrature except ImportError: import sys print("Install scipy. Exiting.") sys.exit() integrator = quadrature eos = EOS(eos) eos_fit_1 = eos.fit(volumes, energies) G_V = [] for i, v in enumerate(volumes): debye = debye_temperature_gibbs(v, mass, natoms, eos_fit_1.b0_GPa, poisson=poisson) G_V.append(energies[i] + pressure * v + A_vib(temperature, debye, natoms, integrator)) # G(V, T, P) eos_fit_2 = eos.fit(volumes, G_V) params = eos_fit_2.eos_params.tolist() # G_opt(V_opt, T, P) return params[0]
def __init__(self, energies, volumes, structure, t_min=300.0, t_step=100, t_max=300.0, eos="vinet", pressure=0.0, poisson=0.25, use_mie_gruneisen=False, anharmonic_contribution=False): """ Args: energies (list): list of DFT energies in eV volumes (list): list of volumes in Ang^3 structure (Structure): t_min (float): min temperature t_step (float): temperature step t_max (float): max temperature eos (str): equation of state used for fitting the energies and the volumes. options supported by pymatgen: "quadratic", "murnaghan", "birch", "birch_murnaghan", "pourier_tarantola", "vinet", "deltafactor", "numerical_eos" pressure (float): in GPa, optional. poisson (float): poisson ratio. use_mie_gruneisen (bool): whether or not to use the mie-gruneisen formulation to compute the gruneisen parameter. The default is the slater-gamma formulation. anharmonic_contribution (bool): whether or not to consider the anharmonic contribution to the Debye temperature. Cannot be used with use_mie_gruneisen. Defaults to False. """ self.energies = energies self.volumes = volumes self.structure = structure self.temperature_min = t_min self.temperature_max = t_max self.temperature_step = t_step self.eos_name = eos self.pressure = pressure self.poisson = poisson self.use_mie_gruneisen = use_mie_gruneisen self.anharmonic_contribution = anharmonic_contribution if self.use_mie_gruneisen and self.anharmonic_contribution: raise ValueError('The Mie-Gruneisen formulation and anharmonic contribution are circular referenced and ' 'cannot be used together.') self.mass = sum([e.atomic_mass for e in self.structure.species]) self.natoms = self.structure.composition.num_atoms self.avg_mass = physical_constants["atomic mass constant"][0] * self.mass / self.natoms # kg self.kb = physical_constants["Boltzmann constant in eV/K"][0] self.hbar = physical_constants["Planck constant over 2 pi in eV s"][0] self.gpa_to_ev_ang = 1./160.21766208 # 1 GPa in ev/Ang^3 self.gibbs_free_energy = [] # optimized values, eV # list of temperatures for which the optimized values are available, K self.temperatures = [] self.optimum_volumes = [] # in Ang^3 # fit E and V and get the bulk modulus(used to compute the Debye # temperature) logger.info("Fitting E and V") self.eos = EOS(eos) self.ev_eos_fit = self.eos.fit(volumes, energies) self.bulk_modulus = self.ev_eos_fit.b0_GPa # in GPa self.optimize_gibbs_free_energy()
def OnFitButton(self, event): model = self.model_choice.GetStringSelection() try: eos = EOS(eos_name=model) fit = eos.fit(self.volumes, self.energies, vol_unit=self.vol_unit, ene_unit=self.ene_unit) print(fit) fit.plot() except: awx.showErrorMessage(self)
def get_eos_fits_dataframe(self, eos_names="murnaghan"): """ Fit energy as function of volume to get the equation of state, equilibrium volume, bulk modulus and its derivative wrt to pressure. Args: eos_names: String or list of strings with EOS names. For the list of available models, see pymatgen.analysis.eos. Return: (fits, dataframe) namedtuple. fits is a list of ``EOSFit object`` dataframe is a |pandas-DataFrame| with the final results. """ # Read volumes and energies from the GSR files. energies, volumes = [], [] for label, gsr in self.items(): energies.append(float(gsr.energy)) volumes.append(float(gsr.structure.volume)) # Order data by volumes if needed. if np.any(np.diff(volumes) < 0): ves = sorted(zip(volumes, energies), key=lambda t: t[0]) volumes = [t[0] for t in ves] energies = [t[1] for t in ves] # Note that eos.fit expects lengths in Angstrom, and energies in eV. # I'm also monkey-patching the plot method. from pymatgen.analysis.eos import EOS if eos_names == "all": # Use all the available models. eos_names = [n for n in EOS.MODELS if n not in ("deltafactor", "numerical_eos")] else: eos_names = list_strings(eos_names) fits, index, rows = [], [], [] for eos_name in eos_names: try: fit = EOS(eos_name=eos_name).fit(volumes, energies) except Exception as exc: cprint("EOS %s raised exception:\n%s" % (eos_name, str(exc))) continue # Replace plot with plot_ax method fit.plot = fit.plot_ax fits.append(fit) index.append(eos_name) rows.append(OrderedDict([(aname, getattr(fit, aname)) for aname in ("v0", "e0", "b0_GPa", "b1")])) dataframe = pd.DataFrame(rows, index=index, columns=list(rows[0].keys()) if rows else None) return dict2namedtuple(fits=fits, dataframe=dataframe)
def __init__(self, structures, energies, eos_name='vinet', pressure=0): """ Args: structures: list of structures at different volumes. energies: list of SCF energies for the structures in eV. eos_name: string indicating the expression used to fit the energies. See pymatgen.analysis.eos.EOS. pressure: value of the pressure in GPa that will be considered in the p*V contribution to the energy. """ self.structures = structures self.energies = np.array(energies) self.eos = EOS(eos_name) self.pressure = pressure self.volumes = np.array([s.volume for s in structures]) self.iv0 = np.argmin(energies)
def getnwrite_eosdata(self, write_json=True): """ This method is called when all tasks reach S_OK. It reads the energies and the volumes from the GSR file, computes the EOS and produce a JSON file `eos_data.json` in outdata. """ energies_ev, volumes = [], [] for task in self: with task.open_gsr() as gsr: volumes.append(float(gsr.structure.volume)) energies_ev.append(float(gsr.energy)) from pymatgen.analysis.eos import EOS eos_data = { "input_volumes_ang3": self.input_volumes, "volumes_ang3": volumes, "energies_ev": energies_ev } for model in EOS.MODELS: if model in ("deltafactor", "numerical_eos"): continue try: fit = EOS(model).fit(volumes, energies_ev) eos_data[model] = {k: float(v) for k, v in fit.results.items()} except Exception as exc: eos_data[model] = {"exception": str(exc)} if write_json: with open(self.outdir.path_in("eos_data.json"), "wt") as fh: json.dump(eos_data, fh, indent=4, sort_keys=True) return eos_data
def __init__(self, energies, volumes, structure, t_min=300.0, t_step=100, t_max=300.0, eos="vinet", pressure=0.0, poisson=0.25, use_mie_gruneisen=False, anharmonic_contribution=False): self.energies = energies self.volumes = volumes self.structure = structure self.temperature_min = t_min self.temperature_max = t_max self.temperature_step = t_step self.eos_name = eos self.pressure = pressure self.poisson = poisson self.use_mie_gruneisen = use_mie_gruneisen self.anharmonic_contribution = anharmonic_contribution if self.use_mie_gruneisen and self.anharmonic_contribution: raise ValueError('The Mie-Gruneisen formulation and anharmonic contribution are circular referenced and cannot be used together.') self.mass = sum([e.atomic_mass for e in self.structure.species]) self.natoms = self.structure.composition.num_atoms self.avg_mass = physical_constants["atomic mass constant"][0] \ * self.mass / self.natoms # kg self.kb = physical_constants["Boltzmann constant in eV/K"][0] self.hbar = physical_constants["Planck constant over 2 pi in eV s"][0] self.gpa_to_ev_ang = 1./160.21766208 # 1 GPa in ev/Ang^3 self.gibbs_free_energy = [] # optimized values, eV # list of temperatures for which the optimized values are available, K self.temperatures = [] self.optimum_volumes = [] # in Ang^3 # fit E and V and get the bulk modulus(used to compute the Debye # temperature) print("Fitting E and V") self.eos = EOS(eos) self.ev_eos_fit = self.eos.fit(volumes, energies) self.bulk_modulus = self.ev_eos_fit.b0_GPa # in GPa self.optimize_gibbs_free_energy()
def test_eosfit_stderr(): volume = [ 64.26025658624827, 66.6402902061661, 69.02026278463558, 71.40028395651238, 73.7802955243384, 76.16031916837127, 78.5402791170494, 80.94268976633485 ] energy = [ -34.69037673, -34.88126365, -34.98844492, -35.02444959, -34.99974727, -34.92873799, -34.8383195, -34.69550342 ] eos = EOS('vinet') eos_fit = eos.fit(volume, energy) fit_value = eos_fit.func(volume) print(fit_value) stderr = eosfit_stderr(eos_fit, volume, energy) assert (stderr == pytest.approx(1.18844e-5, abs=1e-7))
def __init__(self, energies, volumes, structure, t_min=300.0, t_step=100, t_max=300.0, eos="vinet", pressure=0.0, poisson=0.25, use_mie_gruneisen=False, anharmonic_contribution=False): self.energies = energies self.volumes = volumes self.structure = structure self.temperature_min = t_min self.temperature_max = t_max self.temperature_step = t_step self.eos_name = eos self.pressure = pressure self.poisson = poisson self.use_mie_gruneisen = use_mie_gruneisen self.anharmonic_contribution = anharmonic_contribution if self.use_mie_gruneisen and self.anharmonic_contribution: raise ValueError( 'The Mie-Gruneisen formulation and anharmonic contribution are circular referenced and cannot be used together.' ) self.mass = sum([e.atomic_mass for e in self.structure.species]) self.natoms = self.structure.composition.num_atoms self.avg_mass = physical_constants["atomic mass constant"][0] \ * self.mass / self.natoms # kg self.kb = physical_constants["Boltzmann constant in eV/K"][0] self.hbar = physical_constants["Planck constant over 2 pi in eV s"][0] self.gpa_to_ev_ang = 1. / 160.21766208 # 1 GPa in ev/Ang^3 self.gibbs_free_energy = [] # optimized values, eV # list of temperatures for which the optimized values are available, K self.temperatures = [] self.optimum_volumes = [] # in Ang^3 # fit E and V and get the bulk modulus(used to compute the Debye # temperature) print("Fitting E and V") self.eos = EOS(eos) self.ev_eos_fit = self.eos.fit(volumes, energies) self.bulk_modulus = self.ev_eos_fit.b0_GPa # in GPa self.optimize_gibbs_free_energy()
def __init__(self, energies, volumes, structure, dos_objects=None, F_vib=None, S_vib=None, C_vib=None, t_min=5, t_step=5, t_max=2000.0, eos="vinet", pressure=0.0, poisson=0.25, bp2gru=1., vib_kwargs=None): self.energies = np.array(energies) self.volumes = np.array(volumes) self.natoms = len(structure) self.temperatures = np.arange(t_min, t_max+t_step, t_step) self.eos_name = eos self.pressure = pressure self.gpa_to_ev_ang = 1./160.21766208 # 1 GPa in ev/Ang^3 self.eos = EOS(eos) # get the vibrational properties as a function of V and T if F_vib is None: # use the Debye model vib_kwargs = vib_kwargs or {} debye_model = DebyeModel(energies, volumes, structure, t_min=t_min, t_step=t_step, t_max=t_max, eos=eos, poisson=poisson, bp2gru=bp2gru, **vib_kwargs) self.F_vib = debye_model.F_vib # vibrational free energy as a function of volume and temperature self.S_vib = debye_model.S_vib # vibrational entropy as a function of volume and temperature self.C_vib = debye_model.C_vib # vibrational heat capacity as a function of volume and temperature self.D_vib = debye_model.D_vib # Debye temperature else: self.F_vib = F_vib self.S_vib = S_vib self.C_vib = C_vib # get the electronic properties as a function of V and T if dos_objects: # we set natom to 1 always because we want the property per formula unit here. thermal_electronic_props = [calculate_thermal_electronic_contribution(dos, t0=t_min, t1=t_max, td=t_step, natom=1) for dos in dos_objects] self.F_el = [p['free_energy'] for p in thermal_electronic_props] else: self.F_el = np.zeros((self.volumes.size, self.temperatures.size)) # Set up the array of Gibbs energies # G = E_0(V) + F_vib(V,T) + F_el(V,T) + PV self.G = self.energies[:, np.newaxis] + self.F_vib + self.F_el + self.pressure * self.volumes[:, np.newaxis] * self.gpa_to_ev_ang # set up the final variables of the optimized Gibbs energies self.gibbs_free_energy = [] # optimized values, eV self.optimum_volumes = [] # in Ang^3 self.optimize_gibbs_free_energy()
def test_run_all_models(self): # these have been checked for plausibility, # but are not benchmarked against independently known values test_output = { 'birch': {'b0': 0.5369258244952931, 'b1': 4.178644231838501, 'e0': -10.8428039082307, 'v0': 40.98926572870838}, 'birch_murnaghan': {'b0': 0.5369258245417454, 'b1': 4.178644235500821, 'e0': -10.842803908240892, 'v0': 40.98926572528106}, 'deltafactor': {'b0': 0.5369258245611414, 'b1': 4.178644231924639, 'e0': -10.842803908299294, 'v0': 40.989265727927936}, 'murnaghan': {'b0': 0.5144967693786603, 'b1': 3.9123862262572264, 'e0': -10.836794514626673, 'v0': 41.13757930387086}, 'numerical_eos': {'b0': 0.5557257614101998, 'b1': 4.344039148405489, 'e0': -10.847490826530702, 'v0': 40.857200064982536}, 'pourier_tarantola': {'b0': 0.5667729960804602, 'b1': 4.331688936974368, 'e0': -10.851486685041658, 'v0': 40.86770643373908}, 'vinet': {'b0': 0.5493839425156859, 'b1': 4.3051929654936885, 'e0': -10.846160810560756, 'v0': 40.916875663779784} } for eos_name in EOS.MODELS: eos = EOS(eos_name=eos_name) _ = eos.fit(self.volumes, self.energies) for param in ('b0', 'b1', 'e0', 'b0'): # TODO: solutions only stable to 2 decimal places # between different machines, this seems far too low? self.assertAlmostEqual(_.results[param], test_output[eos_name][param], places=1)
def test_eos_func(self): # list vs np.array arguments np.testing.assert_almost_equal(self.num_eos_fit.func([0, 1, 2]), self.num_eos_fit.func(np.array([0, 1, 2])), decimal=10) # func vs _func np.testing.assert_almost_equal(self.num_eos_fit.func(0.), self.num_eos_fit._func( 0., self.num_eos_fit.eos_params), decimal=10) # test the eos function: energy = f(volume) # numerical eos evaluated at volume=0 == a0 of the fit polynomial np.testing.assert_almost_equal(self.num_eos_fit.func(0.), self.num_eos_fit.eos_params[-1], decimal=6) birch_eos = EOS(eos_name="birch") birch_eos_fit = birch_eos.fit(self.volumes, self.energies) # birch eos evaluated at v0 == e0 np.testing.assert_almost_equal(birch_eos_fit.func(birch_eos_fit.v0), birch_eos_fit.e0, decimal=6)
def test_eos_func(self): # list vs np.array arguments np.testing.assert_almost_equal(self.num_eos_fit.func([0,1,2]), self.num_eos_fit.func(np.array([0,1,2])), decimal=10) # func vs _func np.testing.assert_almost_equal(self.num_eos_fit.func(0.), self.num_eos_fit._func( 0., self.num_eos_fit.eos_params), decimal=10) # test the eos function: energy = f(volume) # numerical eos evaluated at volume=0 == a0 of the fit polynomial np.testing.assert_almost_equal(self.num_eos_fit.func(0.), self.num_eos_fit.eos_params[-1], decimal=6) birch_eos = EOS(eos_name="birch") birch_eos_fit = birch_eos.fit(self.volumes, self.energies) # birch eos evaluated at v0 == e0 np.testing.assert_almost_equal(birch_eos_fit.func(birch_eos_fit.v0), birch_eos_fit.e0, decimal=6)
def run_task(self, fw_spec): from pymatgen.analysis.eos import EOS tag = self["tag"] db_file = env_chk(self.get("db_file"), fw_spec) summary_dict = {"eos": self["eos"]} mmdb = MMVaspDb.from_db_file(db_file, admin=True) # get the optimized structure d = mmdb.collection.find_one( {"task_label": "{} structure optimization".format(tag)}) structure = Structure.from_dict( d["calcs_reversed"][-1]["output"]['structure']) summary_dict["structure"] = structure.as_dict() # get the data(energy, volume, force constant) from the deformation runs docs = mmdb.collection.find({ "task_label": { "$regex": "{} bulk_modulus*".format(tag) }, "formula_pretty": structure.composition.reduced_formula }) energies = [] volumes = [] for d in docs: s = Structure.from_dict( d["calcs_reversed"][-1]["output"]['structure']) energies.append(d["calcs_reversed"][-1]["output"]['energy']) volumes.append(s.volume) summary_dict["energies"] = energies summary_dict["volumes"] = volumes # fit the equation of state eos = EOS(self["eos"]) eos_fit = eos.fit(volumes, energies) summary_dict["results"] = dict(eos_fit.results) with open("bulk_modulus.json", "w") as f: f.write(json.dumps(summary_dict, default=DATETIME_HANDLER)) logger.info("BULK MODULUS CALCULATION COMPLETE")
def setUp(self): # Si data from Cormac self.volumes = [ 25.987454833, 26.9045702104, 27.8430241908, 28.8029649591, 29.7848370694, 30.7887887064, 31.814968055, 32.8638196693, 33.9353435494, 35.0299842495, 36.1477417695, 37.2892088485, 38.4543854865, 39.6437162376, 40.857201102, 42.095136449, 43.3579668329, 44.6456922537, 45.9587572656, 47.2973100535, 48.6614988019, 50.0517680652, 51.4682660281, 52.9112890601, 54.3808371612, 55.8775030703, 57.4014349722, 58.9526328669 ] self.energies = [ -7.63622156576, -8.16831294894, -8.63871612686, -9.05181213218, -9.41170988374, -9.72238224345, -9.98744832526, -10.210309552, -10.3943401353, -10.5427238068, -10.6584266073, -10.7442240979, -10.8027285713, -10.8363890521, -10.8474912964, -10.838157792, -10.8103477586, -10.7659387815, -10.7066179666, -10.6339907853, -10.5495538639, -10.4546677714, -10.3506386542, -10.2386366017, -10.1197772808, -9.99504030111, -9.86535084973, -9.73155247952 ] num_eos = EOS(eos_name="numerical_eos") self.num_eos_fit = num_eos.fit(self.volumes, self.energies)
def run_task(self, fw_spec): db_file = env_chk(self.get("db_file"), fw_spec) tag = self["tag"] vasp_db = VaspCalcDb.from_db_file(db_file, admin=True) static_calculations = vasp_db.collection.find({"metadata.tag": tag}) energies = [] volumes = [] structure = None # single Structure for QHA calculation for calc in static_calculations: energies.append(calc['output']['energy']) volumes.append(calc['output']['structure']['lattice']['volume']) if structure is None: structure = Structure.from_dict(calc['output']['structure']) eos = EOS(self.get('eos')) ev_eos_fit = eos.fit(volumes, energies) equil_volume = ev_eos_fit.v0 structure.scale_lattice(equil_volume) analysis_result = ev_eos_fit.results analysis_result['b0_GPa'] = float(ev_eos_fit.b0_GPa) analysis_result['structure'] = structure.as_dict() analysis_result[ 'formula_pretty'] = structure.composition.reduced_formula analysis_result['metadata'] = self.get('metadata', {}) analysis_result['energies'] = energies analysis_result['volumes'] = volumes # write to JSON for debugging purposes import json with open('eos_summary.json', 'w') as fp: json.dump(analysis_result, fp) vasp_db.db['eos'].insert_one(analysis_result)
def setUp(self): # Si data from Cormac self.volumes = [25.987454833, 26.9045702104, 27.8430241908, 28.8029649591, 29.7848370694, 30.7887887064, 31.814968055, 32.8638196693, 33.9353435494, 35.0299842495, 36.1477417695, 37.2892088485, 38.4543854865, 39.6437162376, 40.857201102, 42.095136449, 43.3579668329, 44.6456922537, 45.9587572656, 47.2973100535, 48.6614988019, 50.0517680652, 51.4682660281, 52.9112890601, 54.3808371612, 55.8775030703, 57.4014349722, 58.9526328669] self.energies = [-7.63622156576, -8.16831294894, -8.63871612686, -9.05181213218, -9.41170988374, -9.72238224345, -9.98744832526, -10.210309552, -10.3943401353, -10.5427238068, -10.6584266073, -10.7442240979, -10.8027285713, -10.8363890521, -10.8474912964, -10.838157792, -10.8103477586, -10.7659387815, -10.7066179666, -10.6339907853, -10.5495538639, -10.4546677714, -10.3506386542, -10.2386366017, -10.1197772808, -9.99504030111, -9.86535084973, -9.73155247952] num_eos = EOS(eos_name="numerical_eos") self.num_eos_fit = num_eos.fit(self.volumes, self.energies)
class DebyeModel(object): """ Calculate the vibrational free energy for volumes/temperatures using the Debye model Note that the properties are per unit formula! Parameters ---------- energies : list List of DFT energies in eV volumes : list List of volumes in Ang^3 structure : pymatgen.Structure One of the structures on the E-V curve (can be any volume). dos_objects : list List of pymatgen Dos objects corresponding to the volumes. If passed, will enable the electronic contribution. t_min : float Minimum temperature t_step : float Temperature step size t_max : float Maximum temperature (inclusive) eos : str Equation of state used for fitting the energies and the volumes. Options supported by pymatgen: "quadratic", "murnaghan", "birch", "birch_murnaghan", "pourier_tarantola", "vinet", "deltafactor", "numerical_eos". Default is "vinet". pressure : float Pressure to apply to the E-V curve/Gibbs energies in GPa. Defaults to 0. poisson : float Poisson ratio, defaults to 0.363615, corresponding to the cubic scaling factor of 0.617 by Moruzzi gruneisen : bool Whether to use the Debye-Gruneisen model. Defaults to True. bp2gru : float Fitting parameter for dBdP in the Gruneisen parameter. 2/3 is the high temperature value and 1 is the low temperature value. Defaults to 1. mass_average_mode : str Either 'arithmetic' or 'geometric'. Default is 'arithmetic' """ def __init__(self, energies, volumes, structure, t_min=5, t_step=5, t_max=2000.0, eos="vinet", poisson=0.363615, gruneisen=True, bp2gru=1., mass_average_mode='arithmetic'): self.energies = energies self.volumes = volumes self.structure = structure self.temperatures = np.arange(t_min, t_max+t_step, t_step) self.eos_name = eos self.poisson = poisson self.bp2gru = bp2gru self.gruneisen = gruneisen self.natoms = self.structure.composition.num_atoms self.kb = physical_constants["Boltzmann constant in eV/K"][0] # calculate the average masses masses = np.array([e.atomic_mass for e in self.structure.species]) * physical_constants["atomic mass constant"][0] if mass_average_mode == 'arithmetic': self.avg_mass = np.mean(masses) elif mass_average_mode == 'geometric': self.avg_mass = gmean(masses) else: raise ValueError("DebyeModel mass_average_mode must be either 'arithmetic' or 'geometric'") # fit E and V and get the bulk modulus(used to compute the Debye temperature) self.eos = EOS(eos) self.ev_eos_fit = self.eos.fit(volumes, energies) self.bulk_modulus = self.ev_eos_fit.b0_GPa # in GPa self.calculate_F_el() def calculate_F_el(self): """ Calculate the Helmholtz vibrational free energy """ self.F_vib = np.zeros((len(self.volumes), self.temperatures.size )) for v_idx, vol in enumerate(self.volumes): for t_idx, temp in enumerate(self.temperatures): self.F_vib[v_idx, t_idx] = self.vibrational_free_energy(temp, vol) def vibrational_free_energy(self, temperature, volume): """ Vibrational Helmholtz free energy, A_vib(V, T). Eq(4) in doi.org/10.1016/j.comphy.2003.12.001 Args: temperature (float): temperature in K volume (float) Returns: float: vibrational free energy in eV """ y = self.debye_temperature(volume) / temperature return self.kb * self.natoms * temperature * (9./8. * y + 3 * np.log(1 - np.exp(-y)) - self.debye_integral(y)) def debye_temperature(self, volume): """ Calculates the debye temperature. Eq(6) in doi.org/10.1016/j.comphy.2003.12.001. Thanks to Joey. Eq(6) above is equivalent to Eq(3) in doi.org/10.1103/PhysRevB.37.790 which does not consider anharmonic effects. Eq(20) in the same paper and Eq(18) in doi.org/10.1016/j.commatsci.2009.12.006 both consider anharmonic contributions to the Debye temperature through the Gruneisen parameter at 0K (Gruneisen constant). The anharmonic contribution is toggled by setting the anharmonic_contribution to True or False in the QuasiharmonicDebyeApprox constructor. Args: volume (float): in Ang^3 Returns: float: debye temperature in K """ term1 = (2./3. * (1. + self.poisson) / (1. - 2. * self.poisson))**1.5 term2 = (1./3. * (1. + self.poisson) / (1. - self.poisson))**1.5 f = (3. / (2. * term1 + term2))**(1. / 3.) debye = 2.9772e-11 * (volume / self.natoms) ** (-1. / 6.) * f * np.sqrt(self.bulk_modulus/self.avg_mass) if self.gruneisen: # bp2gru should be the correction to the Gruneisen constant. # High temperature limit: 2/3 # Low temperature limit: 1 # take 0 K E-V curve properties dBdP = self.ev_eos_fit.b1 # bulk modulus/pressure derivative gamma = (1+dBdP)/2 - self.bp2gru # 0K equilibrium Gruneisen parameter return debye * (self.ev_eos_fit.v0 / volume) ** (gamma) else: return debye @staticmethod def debye_integral(y): """ Debye integral. Eq(5) in doi.org/10.1016/j.comphy.2003.12.001 Args: y (float): debye temperature/T, upper limit Returns: float: unitless """ # floating point limit is reached around y=155, so values beyond that # are set to the limiting value(T-->0, y --> \infty) of # 6.4939394 (from wolfram alpha). factor = 3. / y ** 3 if y < 155: integral = quadrature(lambda x: x ** 3 / (np.exp(x) - 1.), 0, y) return list(integral)[0] * factor else: return 6.493939 * factor
def test_bulk_modulus(self): eos = EOS(self.eos) eos_fit = eos.fit(self.volumes, self.energies) bulk_modulus = float(str(eos_fit.b0_GPa).split()[0]) bulk_modulus_ans = float(str(self.qhda.bulk_modulus).split()[0]) np.testing.assert_almost_equal(bulk_modulus, bulk_modulus_ans, 3)
print(f'Data printed in "{system_name}_eos.dat"') import matplotlib.pyplot as plt import numpy as np import matplotlib matplotlib.rcParams.update({'font.size': 22}) data = np.loadtxt(f"{system_name}_eos.dat", 'f') V = data[:, 0] E = data[:, 1] # making b-m fit and plot with pymatgen eos = EOS(eos_name='birch_murnaghan') eos_fit = eos.fit(V, E) eos_fit.plot(width=10, height=10, text='', markersize=15, label='Birch-Murnaghan fit') plt.legend(loc=2, prop={'size': 20}) plt.title(f'{system_name}') plt.tight_layout() #plt.show() plt.savefig(f'{system_name}_fit_eos.png') print('') print(f'Plot saved as "{system_name}_fit_eos.png"') # getting fitted parameters
class AbstractQHA(six.with_metaclass(abc.ABCMeta, object)): """ Abstract class for the quasi-harmonic approximation analysis. Provides some basic methods and plotting utils, plus a converter to write input files for phonopy-qha or to generate an instance of phonopy.qha.QHA. These can be used to obtain other quantities and plots. Does not include electronic entropic contributions for metals. """ def __init__(self, structures, energies, eos_name='vinet', pressure=0): """ Args: structures: list of structures at different volumes. energies: list of SCF energies for the structures in eV. eos_name: string indicating the expression used to fit the energies. See pymatgen.analysis.eos.EOS. pressure: value of the pressure in GPa that will be considered in the p*V contribution to the energy. """ self.structures = structures self.energies = np.array(energies) self.eos = EOS(eos_name) self.pressure = pressure self.volumes = np.array([s.volume for s in structures]) self.iv0 = np.argmin(energies) def fit_energies(self, tstart=0, tstop=800, num=100): """ Performs a fit of the energies as a function of the volume at different temperatures. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. Returns: `namedtuple` with the following attributes:: tot_en: numpy array with shape (nvols, num) with the energies used for the fit fits: list of subclasses of pymatgen.analysis.eos.EOSBase, depending on the type of eos chosen. Contains the fit for the energies at the different temperatures. min_en: numpy array with the minimum energies for the list of temperatures min_vol: numpy array with the minimum volumes for the list of temperatures temp: numpy array with the temperatures considered """ tmesh = np.linspace(tstart, tstop, num) # array with phonon energies and shape (n_vol, n_temp) ph_energies = self.get_vib_free_energies(tstart, tstop, num) tot_en = self.energies[np.newaxis, :].T + ph_energies + self.volumes[np.newaxis, :].T * self.pressure / abu.eVA3_GPa # list of fits objects, one for each temperature fits = [self.eos.fit(self.volumes, e) for e in tot_en.T] # list of minimum volumes and energies, one for each temperature min_volumes = np.array([fit.v0 for fit in fits]) min_energies = np.array([fit.e0 for fit in fits]) return dict2namedtuple(tot_en=tot_en, fits=fits, min_en=min_energies, min_vol=min_volumes, temp=tmesh) @abc.abstractmethod def get_vib_free_energies(self, tstart=0, tstop=800, num=100): """ Generates the vibrational free energy corresponding to all the structures. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. Returns: A numpy array of `num` values of of the vibrational contribution to the free energy """ pass @abc.abstractmethod def get_thermodynamic_properties(self, tstart=0, tstop=800, num=100): """ Generates all the thermodynamic properties corresponding to all the volumes. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. Returns: `namedtuple` with the following attributes for all the volumes: tmesh: numpy array with the list of temperatures. Shape (num). cv: constant-volume specific heat, in eV/K. Shape (nvols, num). free_energy: free energy, in eV. Shape (nvols, num). entropy: entropy, in eV/K. Shape (nvols, num). zpe: zero point energy in eV. Shape (nvols). """ pass @property def nvols(self): return len(self.volumes) @property def natoms(self): return len(self.structures[0]) def set_eos(self, eos_name): """ Updates the EOS used for the fit. Args: eos_name: string indicating the expression used to fit the energies. See pymatgen.analysis.eos.EOS. """ self.eos = EOS(eos_name) @add_fig_kwargs def plot_energies(self, tstart=0, tstop=800, num=10, ax=None, **kwargs): """ Plots the energies as a function of volume at different temperatures. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 10. ax: |matplotlib-Axes| or None if a new figure should be created. Returns: |matplotlib-Figure| """ f = self.fit_energies(tstart, tstop, num) ax, fig, plt = get_ax_fig_plt(ax) xmin, xmax = np.floor(self.volumes.min() * 0.97), np.ceil(self.volumes.max() * 1.03) x = np.linspace(xmin, xmax, 100) for fit, e, t in zip(f.fits, f.tot_en.T - self.energies[self.iv0], f.temp): ax.scatter(self.volumes, e, label=t, color='b', marker='x', s=5) ax.plot(x, fit.func(x) - self.energies[self.iv0], color='b', lw=1) ax.plot(f.min_vol, f.min_en - self.energies[self.iv0] , color='r', lw=1, marker='x', ms=5) ax.set_xlabel(r'V (${\AA}^3$)') ax.set_ylabel('E (eV)') return fig def get_thermal_expansion_coeff(self, tstart=0, tstop=800, num=100): """ Calculates the thermal expansion coefficient as a function of temperature, using finite difference on the fitted values of the volume as a function of temperature. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. Returns: |Function1D| """ f = self.fit_energies(tstart, tstop, num) dt = f.temp[1] - f.temp[0] alpha = (f.min_vol[2:] - f.min_vol[:-2]) / (2 * dt) / f.min_vol[1:-1] return Function1D(f.temp[1:-1], alpha) @add_fig_kwargs def plot_thermal_expansion_coeff(self, tstart=0, tstop=800, num=100, ax=None, **kwargs): """ Plots the thermal expansion coefficient as a function of the temperature. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. ax: |matplotlib-Axes| or None if a new figure should be created. Returns: |matplotlib-Figure| """ ax, fig, plt = get_ax_fig_plt(ax) if 'linewidth' not in kwargs and 'lw' not in kwargs: kwargs['linewidth'] = 2 if "color" not in kwargs: kwargs["color"] = "b" alpha = self.get_thermal_expansion_coeff(tstart, tstop, num) ax.plot(alpha.mesh, alpha.values, **kwargs) ax.set_xlabel(r'T (K)') ax.set_ylabel(r'$\alpha$ (K$^{-1}$)') ax.set_xlim(tstart, tstop) ax.get_yaxis().get_major_formatter().set_powerlimits((0, 0)) return fig @add_fig_kwargs def plot_vol_vs_t(self, tstart=0, tstop=800, num=100, ax=None, **kwargs): """ Plots the volume as a function of the temperature. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. ax: |matplotlib-Axes| or None if a new figure should be created. Returns: |matplotlib-Figure| """ ax, fig, plt = get_ax_fig_plt(ax) f = self.fit_energies(tstart, tstop, num) if 'linewidth' not in kwargs and 'lw' not in kwargs: kwargs['linewidth'] = 2 if "color" not in kwargs: kwargs["color"] = "b" ax.plot(f.temp, f.min_vol, **kwargs) ax.set_xlabel('T (K)') ax.set_ylabel(r'V (${\AA}^3$)') ax.set_xlim(tstart, tstop) return fig @add_fig_kwargs def plot_phbs(self, phbands, temperatures=None, t_max=1000, colormap="plasma", **kwargs): """ Given a list of |PhononBands| plots the band structures with a color depending on the temperature using a |PhononBandsPlotter|. If temperatures are not given they will be deduced inverting the dependence of volume with respect to the temperature. If a unique volume could not be identified an error will be raised. Args: phbands: list of |PhononBands| objects. temperatures: list of temperatures. t_max: maximum temperature considered for plotting. colormap: matplotlib color map. Returns: |matplotlib-Figure| """ if temperatures is None: tv = self.get_t_for_vols([b.structure.volume for b in phbands], t_max=t_max) temperatures = [] for b, t in zip(phbands, tv): if len(t) != 1: raise ValueError("Couldn't find a single temperature for structure with " "volume {}. Found {}: {}".format(b.structure.volume, len(t), list(t))) temperatures.append(t[0]) temperatures_str = ["{:.0f} K".format(t) for t in temperatures] import matplotlib.pyplot as plt cmap = plt.get_cmap(colormap) colors = [cmap(t / max(temperatures)) for t in temperatures] labels_phbs = zip(temperatures_str, phbands) pbp = PhononBandsPlotter(labels_phbs) pbp._LINE_COLORS = colors pbp._LINE_STYLES = ['-'] fig = pbp.combiplot(show=False, **kwargs) return fig def get_vol_at_t(self, t): """ Calculates the volume corresponding to a specific temperature. Args: t: a temperature in K Returns: The volume """ f = self.fit_energies(t, t, 1) return f.min_vol[0] def get_t_for_vols(self, vols, t_max=1000): """ Find the temperatures corresponding to a specific volume. The search is performed interpolating the V(T) dependence with a spline and finding the roots with of V(t) - v. It may return more than one temperature for a volume in case of non monotonic behavior. Args: vols: list of volumes t_max: maximum temperature considered for the fit Returns: A list of lists of temperatures. For each volume more than one temperature can be identified. """ if not isinstance(vols, (list, tuple, np.ndarray)): vols = [vols] f = self.fit_energies(0, t_max, t_max+1) temps = [] for v in vols: spline = UnivariateSpline(f.temp, f.min_vol - v, s=0) temps.append(spline.roots()) return temps def write_phonopy_qha_inputs(self, tstart=0, tstop=2100, num=211, path=None): """ Writes nvols thermal_properties-i.yaml files that can be used as inputs for phonopy-qha. Notice that phonopy apparently requires the value of the 300 K temperature to be present in the list. Choose the values of tstart, tstop and num to satisfy this condition. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 211. path: a path to a folder where the files will be stored """ if path is None: path = os.getcwd() thermo = self.get_thermodynamic_properties(tstart=tstart, tstop=tstop, num=num) np.savetxt(os.path.join(path, 'e-v.dat'), np.array([self.volumes, self.energies]).T, fmt='%.10f') # generator for thermal_properties.yaml extracted from phonopy: # phonopy.phonon.thermal_properties.ThermalProperties._get_tp_yaml_lines for j in range(self.nvols): lines = [] lines.append("# Thermal properties / unit cell (natom)") lines.append("") lines.append("unit:") lines.append(" temperature: K") lines.append(" free_energy: kJ/mol") lines.append(" entropy: J/K/mol") lines.append(" heat_capacity: J/K/mol") lines.append("") lines.append("natom: %5d" % (self.natoms)) lines.append("num_modes: %d" % (3 * self.natoms)) lines.append("num_integrated_modes: %d" % (3 * self.natoms)) lines.append("") lines.append("zero_point_energy: %15.7f" % (thermo.zpe[j] * abu.e_Cb * abu.Avogadro )) lines.append("high_T_entropy: %15.7f" % 0) # high_T_entropy is not used in QHA lines.append("") lines.append("thermal_properties:") fe = thermo.free_energy[j] * abu.e_Cb * abu.Avogadro entropy = thermo.entropy[j] * abu.e_Cb * abu.Avogadro cv = thermo.cv[j] * abu.e_Cb * abu.Avogadro temperatures = thermo.tmesh for i, t in enumerate(temperatures): lines.append("- temperature: %15.7f" % t) lines.append(" free_energy: %15.7f" % (fe[i] / 1000)) lines.append(" entropy: %15.7f" % entropy[i]) # Sometimes 'nan' of C_V is returned at low temperature. if np.isnan(cv[i]): lines.append(" heat_capacity: %15.7f" % 0) else: lines.append(" heat_capacity: %15.7f" % cv[i]) lines.append(" energy: %15.7f" % (fe[i] / 1000 + entropy[i] * t / 1000)) lines.append("") with open(os.path.join(path, "thermal_properties-{}.yaml".format(j)), 'wt') as f: f.write("\n".join(lines)) def get_phonopy_qha(self, tstart=0, tstop=2100, num=211, eos='vinet', t_max=None, energy_plot_factor=None): """ Creates an instance of phonopy.qha.QHA that can be used generate further plots and output data. The object is returned right after the construction. The "run()" method should be executed before getting results and plots. Notice that phonopy apparently requires the value of the 300 K temperature to be present in the list. Choose the values of tstart, tstop and num to satisfy this condition. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 211. eos: the expression used to fit the energies in phonopy. Possible values are "vinet", "murnaghan" and "birch_murnaghan". Passed to phonopy's QHA. t_max: maximum temperature. Passed to phonopy's QHA. energy_plot_factor: factor multiplying the energies. Passed to phonopy's QHA. Returns: An instance of phonopy.qha.QHA """ try: from phonopy.qha import QHA as QHA_phonopy except ImportError as exc: print("Phonopy is required to generate the QHA phonopy object") raise exc thermo = self.get_thermodynamic_properties(tstart=tstart, tstop=tstop, num=num) fe = thermo.free_energy.T * abu.e_Cb * abu.Avogadro / 1000 entropy = thermo.entropy.T * abu.e_Cb * abu.Avogadro cv = thermo.cv.T * abu.e_Cb * abu.Avogadro temperatures = thermo.tmesh en = self.energies + self.volumes * self.pressure / abu.eVA3_GPa qha_p = QHA_phonopy(self.volumes, en, temperatures, cv, entropy, fe, eos, t_max, energy_plot_factor) return qha_p
def test_fitting(self): # courtesy of @katherinelatimer2013 # known correct values for Vinet # Mg mp153_volumes = [ 16.69182365, 17.25441763, 17.82951915, 30.47573817, 18.41725977, 29.65211363, 28.84346369, 19.01777055, 28.04965916, 19.63120886, 27.27053682, 26.5059864, 20.25769112, 25.75586879, 20.89736201, 25.02003097, 21.55035204, 24.29834347, 22.21681221, 23.59066888, 22.89687316 ] mp153_energies = [ -1.269884575, -1.339411225, -1.39879471, -1.424480995, -1.44884184, -1.45297499, -1.4796246, -1.49033594, -1.504198485, -1.52397006, -1.5264432, -1.54609291, -1.550269435, -1.56284009, -1.569937375, -1.576420935, -1.583470925, -1.58647189, -1.591436505, -1.592563495, -1.594347355 ] mp153_known_energies_vinet = [ -1.270038831, -1.339366487, -1.398683238, -1.424556061, -1.448746649, -1.453000456, -1.479614511, -1.490266797, -1.504163502, -1.523910268, -1.526395734, -1.546038792, -1.550298657, -1.562800797, -1.570015274, -1.576368392, -1.583605186, -1.586404575, -1.591578378, -1.592547954, -1.594410995 ] # C: 4.590843262 # B: 2.031381599 mp153_known_e0_vinet = -1.594429229 mp153_known_v0_vinet = 22.95764159 eos = EOS(eos_name='vinet') fit = eos.fit(mp153_volumes, mp153_energies) np.testing.assert_array_almost_equal(fit.func(mp153_volumes), mp153_known_energies_vinet, decimal=5) self.assertAlmostEqual(mp153_known_e0_vinet, fit.e0, places=4) self.assertAlmostEqual(mp153_known_v0_vinet, fit.v0, places=4) # expt. value 35.5, known fit 36.16 self.assertAlmostEqual(fit.b0_GPa, 36.16258657649159) # Si mp149_volumes = [ 15.40611854, 14.90378698, 16.44439516, 21.0636307, 17.52829835, 16.98058208, 18.08767363, 18.65882487, 19.83693435, 15.91961152, 22.33987173, 21.69548924, 22.99688883, 23.66666322, 20.44414922, 25.75374305, 19.24187473, 24.34931029, 25.04496106, 27.21116571, 26.4757653 ] mp149_energies = [ -4.866909695, -4.7120965, -5.10921253, -5.42036228, -5.27448405, -5.200810795, -5.331915665, -5.3744186, -5.420058145, -4.99862686, -5.3836163, -5.40610838, -5.353700425, -5.31714654, -5.425263555, -5.174988295, -5.403353105, -5.27481447, -5.227210275, -5.058992615, -5.118805775 ] mp149_known_energies_vinet = [ -4.866834585, -4.711786499, -5.109642598, -5.420093739, -5.274605844, -5.201025714, -5.331899365, -5.374315789, -5.419671568, -4.998827503, -5.383703409, -5.406038887, -5.353926272, -5.317484252, -5.424963418, -5.175090887, -5.403166824, -5.275096644, -5.227427635, -5.058639193, -5.118654229 ] # C: 4.986513158 # B: 4.964976215 mp149_known_e0_vinet = -5.424963506 mp149_known_v0_vinet = 20.44670279 eos = EOS(eos_name='vinet') fit = eos.fit(mp149_volumes, mp149_energies) np.testing.assert_array_almost_equal(fit.func(mp149_volumes), mp149_known_energies_vinet, decimal=5) self.assertAlmostEqual(mp149_known_e0_vinet, fit.e0, places=4) self.assertAlmostEqual(mp149_known_v0_vinet, fit.v0, places=4) # expt. value 97.9, known fit 88.39 self.assertAlmostEqual(fit.b0_GPa, 88.38629264585195) # Ti mp72_volumes = [ 12.49233296, 12.91339188, 13.34380224, 22.80836212, 22.19195533, 13.78367177, 21.58675559, 14.23310328, 20.99266009, 20.4095592, 14.69220297, 19.83736385, 15.16106697, 19.2759643, 15.63980711, 18.72525771, 16.12851491, 18.18514127, 16.62729878, 17.65550599, 17.13626153 ] mp72_energies = [ -7.189983803, -7.33985647, -7.468745423, -7.47892835, -7.54945107, -7.578012237, -7.61513166, -7.66891898, -7.67549721, -7.73000681, -7.74290386, -7.77803379, -7.801246383, -7.818964483, -7.84488189, -7.85211192, -7.87486651, -7.876767777, -7.892161533, -7.892199957, -7.897605303 ] mp72_known_energies_vinet = [ -7.189911138, -7.339810181, -7.468716095, -7.478678021, -7.549402394, -7.578034391, -7.615240977, -7.669091347, -7.675683891, -7.730188653, -7.74314028, -7.778175824, -7.801363213, -7.819030923, -7.844878053, -7.852099741, -7.874737806, -7.876686864, -7.891937429, -7.892053535, -7.897414664 ] # C: 3.958192998 # B: 6.326790098 mp72_known_e0_vinet = -7.897414997 mp72_known_v0_vinet = 17.13223229 eos = EOS(eos_name='vinet') fit = eos.fit(mp72_volumes, mp72_energies) np.testing.assert_array_almost_equal(fit.func(mp72_volumes), mp72_known_energies_vinet, decimal=5) self.assertAlmostEqual(mp72_known_e0_vinet, fit.e0, places=4) self.assertAlmostEqual(mp72_known_v0_vinet, fit.v0, places=4) # expt. value 107.3, known fit 112.63 self.assertAlmostEqual(fit.b0_GPa, 112.62927094503254)
class Quasiharmonic(object): """ Class to perform quasiharmonic calculations. In principle, helps to abstract away where different energy contributions come from. Parameters ---------- energies : list List of DFT energies in eV volumes : list List of volumes in Ang^3 structure : pymatgen.Structure One of the structures on the E-V curve (can be any volume). dos_objects : list List of pymatgen Dos objects corresponding to the volumes. If passed, will enable the electronic contribution. f_vib : numpy.ndarray Array of F_vib(V,T) of shape (len(volumes), len(temperatures)). If absent, will use the Debye model. t_min : float Minimum temperature t_step : float Temperature step size t_max : float Maximum temperature (inclusive) eos : str Equation of state used for fitting the energies and the volumes. Options supported by pymatgen: "quadratic", "murnaghan", "birch", "birch_murnaghan", "pourier_tarantola", "vinet", "deltafactor", "numerical_eos". Default is "vinet". pressure : float Pressure to apply to the E-V curve/Gibbs energies in GPa. Defaults to 0. poisson : float Poisson ratio, defaults to 0.25. Only used in QHA bp2gru : float Fitting parameter for dBdP in the Gruneisen parameter. 2/3 is the high temperature value and 1 is the low temperature value. Defaults to 1. vib_kwargs : dict Additional keyword arguments to pass to the vibrational calculator """ def __init__(self, energies, volumes, structure, dos_objects=None, F_vib=None, t_min=5, t_step=5, t_max=2000.0, eos="vinet", pressure=0.0, poisson=0.25, bp2gru=1., vib_kwargs=None): self.energies = np.array(energies) self.volumes = np.array(volumes) self.natoms = len(structure) self.temperatures = np.arange(t_min, t_max + t_step, t_step) self.eos_name = eos self.pressure = pressure self.gpa_to_ev_ang = 1. / 160.21766208 # 1 GPa in ev/Ang^3 self.eos = EOS(eos) # get the vibrational properties as a function of V and T if F_vib is None: # use the Debye model vib_kwargs = vib_kwargs or {} debye_model = DebyeModel(energies, volumes, structure, t_min=t_min, t_step=t_step, t_max=t_max, eos=eos, poisson=poisson, bp2gru=bp2gru, **vib_kwargs) self.F_vib = debye_model.F_vib # vibrational free energy as a function of volume and temperature else: self.F_vib = F_vib # get the electronic properties as a function of V and T if dos_objects: # we set natom to 1 always because we want the property per formula unit here. thermal_electronic_props = [ calculate_thermal_electronic_contribution(dos, t0=t_min, t1=t_max, td=t_step, natom=1) for dos in dos_objects ] self.F_el = [p['free_energy'] for p in thermal_electronic_props] else: self.F_el = np.zeros((self.volumes.size, self.temperatures.size)) # Set up the array of Gibbs energies # G = E_0(V) + F_vib(V,T) + F_el(V,T) + PV self.G = self.energies[:, np. newaxis] + self.F_vib + self.F_el + self.pressure * self.volumes[:, np . newaxis] * self.gpa_to_ev_ang # set up the final variables of the optimized Gibbs energies self.gibbs_free_energy = [] # optimized values, eV self.optimum_volumes = [] # in Ang^3 self.optimize_gibbs_free_energy() def optimize_gibbs_free_energy(self): """ Evaluate the gibbs free energy as a function of V, T and P i.e G(V, T, P), minimize G(V, T, P) wrt V for each T and store the optimum values. Note: The data points for which the equation of state fitting fails are skipped. """ for temp_idx in range(self.temperatures.size): G_opt, V_opt = self.optimizer(temp_idx) self.gibbs_free_energy.append(float(G_opt)) self.optimum_volumes.append(float(V_opt)) def optimizer(self, temp_idx): """ Evaluate G(V, T, P) at the given temperature(and pressure) and minimize it wrt V. 1. Compute the vibrational helmholtz free energy, A_vib. 2. Compute the gibbs free energy as a function of volume, temperature and pressure, G(V,T,P). 3. Preform an equation of state fit to get the functional form of gibbs free energy:G(V, T, P). 4. Finally G(V, P, T) is minimized with respect to V. Args: temp_idx : int Index of the temperature of interest from self.temperatures Returns: float, float: G_opt(V_opt, T, P) in eV and V_opt in Ang^3. """ G_V = self.G[:, temp_idx] # fit equation of state, G(V, T, P) try: eos_fit = self.eos.fit(self.volumes, G_V) except EOSError: return np.nan, np.nan # minimize the fit eos wrt volume # Note: the ref energy and the ref volume(E0 and V0) not necessarily # the same as minimum energy and min volume. volume_guess = eos_fit.volumes[np.argmin(eos_fit.energies)] min_wrt_vol = minimize(eos_fit.func, volume_guess) # G_opt=G(V_opt, T, P), V_opt return min_wrt_vol.fun, min_wrt_vol.x[0] def get_summary_dict(self): """ Returns a dict with a summary of the computed properties. """ d = defaultdict(list) d["pressure"] = self.pressure d["natoms"] = int(self.natoms) d["gibbs_free_energy"] = self.gibbs_free_energy d["temperatures"] = self.temperatures d["optimum_volumes"] = self.optimum_volumes return d
class AbstractQHA(six.with_metaclass(abc.ABCMeta, object)): """ Abstract class for the quasi-harmonic approximation analysis. Provides some basic methods and plotting utils, plus a converter to write input files for phonopy-qha or to generate an instance of phonopy.qha.QHA. These can be used to obtain other quantities and plots. Does not include electronic entropic contributions for metals. """ def __init__(self, structures, energies, eos_name='vinet', pressure=0): """ Args: structures: list of structures at different volumes. energies: list of SCF energies for the structures in eV. eos_name: string indicating the expression used to fit the energies. See pymatgen.analysis.eos.EOS. pressure: value of the pressure in GPa that will be considered in the p*V contribution to the energy. """ self.structures = structures self.energies = np.array(energies) self.eos = EOS(eos_name) self.pressure = pressure self.volumes = np.array([s.volume for s in structures]) self.iv0 = np.argmin(energies) def fit_energies(self, tstart=0, tstop=800, num=100): """ Performs a fit of the energies as a function of the volume at different temperatures. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. Returns: `namedtuple` with the following attributes:: tot_en: numpy array with shape (nvols, num) with the energies used for the fit fits: list of subclasses of pymatgen.analysis.eos.EOSBase, depending on the type of eos chosen. Contains the fit for the energies at the different temperatures. min_en: numpy array with the minimum energies for the list of temperatures min_vol: numpy array with the minimum volumes for the list of temperatures temp: numpy array with the temperatures considered """ tmesh = np.linspace(tstart, tstop, num) # array with phonon energies and shape (n_vol, n_temp) ph_energies = self.get_vib_free_energies(tstart, tstop, num) tot_en = self.energies[np.newaxis, :].T + ph_energies + self.volumes[ np.newaxis, :].T * self.pressure / abu.eVA3_GPa # list of fits objects, one for each temperature fits = [self.eos.fit(self.volumes, e) for e in tot_en.T] # list of minimum volumes and energies, one for each temperature min_volumes = np.array([fit.v0 for fit in fits]) min_energies = np.array([fit.e0 for fit in fits]) return dict2namedtuple(tot_en=tot_en, fits=fits, min_en=min_energies, min_vol=min_volumes, temp=tmesh) @abc.abstractmethod def get_vib_free_energies(self, tstart=0, tstop=800, num=100): """ Generates the vibrational free energy corresponding to all the structures. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. Returns: A numpy array of `num` values of of the vibrational contribution to the free energy """ pass @abc.abstractmethod def get_thermodynamic_properties(self, tstart=0, tstop=800, num=100): """ Generates all the thermodynamic properties corresponding to all the volumes. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. Returns: `namedtuple` with the following attributes for all the volumes: tmesh: numpy array with the list of temperatures. Shape (num). cv: constant-volume specific heat, in eV/K. Shape (nvols, num). free_energy: free energy, in eV. Shape (nvols, num). entropy: entropy, in eV/K. Shape (nvols, num). zpe: zero point energy in eV. Shape (nvols). """ pass @property def nvols(self): return len(self.volumes) @property def natoms(self): return len(self.structures[0]) def set_eos(self, eos_name): """ Updates the EOS used for the fit. Args: eos_name: string indicating the expression used to fit the energies. See pymatgen.analysis.eos.EOS. """ self.eos = EOS(eos_name) @add_fig_kwargs def plot_energies(self, tstart=0, tstop=800, num=10, ax=None, **kwargs): """ Plots the energies as a function of volume at different temperatures. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 10. ax: |matplotlib-Axes| or None if a new figure should be created. Returns: |matplotlib-Figure| """ f = self.fit_energies(tstart, tstop, num) ax, fig, plt = get_ax_fig_plt(ax) xmin, xmax = np.floor(self.volumes.min() * 0.97), np.ceil( self.volumes.max() * 1.03) x = np.linspace(xmin, xmax, 100) for fit, e, t in zip(f.fits, f.tot_en.T - self.energies[self.iv0], f.temp): ax.scatter(self.volumes, e, label=t, color='b', marker='x', s=5) ax.plot(x, fit.func(x) - self.energies[self.iv0], color='b', lw=1) ax.plot(f.min_vol, f.min_en - self.energies[self.iv0], color='r', lw=1, marker='x', ms=5) ax.set_xlabel(r'V (${\AA}^3$)') ax.set_ylabel('E (eV)') return fig def get_thermal_expansion_coeff(self, tstart=0, tstop=800, num=100): """ Calculates the thermal expansion coefficient as a function of temperature, using finite difference on the fitted values of the volume as a function of temperature. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. Returns: |Function1D| """ f = self.fit_energies(tstart, tstop, num) dt = f.temp[1] - f.temp[0] alpha = (f.min_vol[2:] - f.min_vol[:-2]) / (2 * dt) / f.min_vol[1:-1] return Function1D(f.temp[1:-1], alpha) @add_fig_kwargs def plot_thermal_expansion_coeff(self, tstart=0, tstop=800, num=100, ax=None, **kwargs): """ Plots the thermal expansion coefficient as a function of the temperature. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. ax: |matplotlib-Axes| or None if a new figure should be created. Returns: |matplotlib-Figure| """ ax, fig, plt = get_ax_fig_plt(ax) if 'linewidth' not in kwargs and 'lw' not in kwargs: kwargs['linewidth'] = 2 if "color" not in kwargs: kwargs["color"] = "b" alpha = self.get_thermal_expansion_coeff(tstart, tstop, num) ax.plot(alpha.mesh, alpha.values, **kwargs) ax.set_xlabel(r'T (K)') ax.set_ylabel(r'$\alpha$ (K$^{-1}$)') ax.set_xlim(tstart, tstop) ax.get_yaxis().get_major_formatter().set_powerlimits((0, 0)) return fig @add_fig_kwargs def plot_vol_vs_t(self, tstart=0, tstop=800, num=100, ax=None, **kwargs): """ Plots the volume as a function of the temperature. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 100. ax: |matplotlib-Axes| or None if a new figure should be created. Returns: |matplotlib-Figure| """ ax, fig, plt = get_ax_fig_plt(ax) f = self.fit_energies(tstart, tstop, num) if 'linewidth' not in kwargs and 'lw' not in kwargs: kwargs['linewidth'] = 2 if "color" not in kwargs: kwargs["color"] = "b" ax.plot(f.temp, f.min_vol, **kwargs) ax.set_xlabel('T (K)') ax.set_ylabel(r'V (${\AA}^3$)') ax.set_xlim(tstart, tstop) return fig @add_fig_kwargs def plot_phbs(self, phbands, temperatures=None, t_max=1000, colormap="plasma", **kwargs): """ Given a list of |PhononBands| plots the band structures with a color depending on the temperature using a |PhononBandsPlotter|. If temperatures are not given they will be deduced inverting the dependence of volume with respect to the temperature. If a unique volume could not be identified an error will be raised. Args: phbands: list of |PhononBands| objects. temperatures: list of temperatures. t_max: maximum temperature considered for plotting. colormap: matplotlib color map. Returns: |matplotlib-Figure| """ if temperatures is None: tv = self.get_t_for_vols([b.structure.volume for b in phbands], t_max=t_max) temperatures = [] for b, t in zip(phbands, tv): if len(t) != 1: raise ValueError( "Couldn't find a single temperature for structure with " "volume {}. Found {}: {}".format( b.structure.volume, len(t), list(t))) temperatures.append(t[0]) temperatures_str = ["{:.0f} K".format(t) for t in temperatures] import matplotlib.pyplot as plt cmap = plt.get_cmap(colormap) colors = [cmap(t / max(temperatures)) for t in temperatures] labels_phbs = zip(temperatures_str, phbands) pbp = PhononBandsPlotter(labels_phbs) pbp._LINE_COLORS = colors pbp._LINE_STYLES = ['-'] fig = pbp.combiplot(show=False, **kwargs) return fig def get_vol_at_t(self, t): """ Calculates the volume corresponding to a specific temperature. Args: t: a temperature in K Returns: The volume """ f = self.fit_energies(t, t, 1) return f.min_vol[0] def get_t_for_vols(self, vols, t_max=1000): """ Find the temperatures corresponding to a specific volume. The search is performed interpolating the V(T) dependence with a spline and finding the roots with of V(t) - v. It may return more than one temperature for a volume in case of non monotonic behavior. Args: vols: list of volumes t_max: maximum temperature considered for the fit Returns: A list of lists of temperatures. For each volume more than one temperature can be identified. """ if not isinstance(vols, (list, tuple, np.ndarray)): vols = [vols] f = self.fit_energies(0, t_max, t_max + 1) temps = [] for v in vols: spline = UnivariateSpline(f.temp, f.min_vol - v, s=0) temps.append(spline.roots()) return temps def write_phonopy_qha_inputs(self, tstart=0, tstop=2100, num=211, path=None): """ Writes nvols thermal_properties-i.yaml files that can be used as inputs for phonopy-qha. Notice that phonopy apparently requires the value of the 300 K temperature to be present in the list. Choose the values of tstart, tstop and num to satisfy this condition. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 211. path: a path to a folder where the files will be stored """ if path is None: path = os.getcwd() thermo = self.get_thermodynamic_properties(tstart=tstart, tstop=tstop, num=num) np.savetxt(os.path.join(path, 'e-v.dat'), np.array([self.volumes, self.energies]).T, fmt='%.10f') # generator for thermal_properties.yaml extracted from phonopy: # phonopy.phonon.thermal_properties.ThermalProperties._get_tp_yaml_lines for j in range(self.nvols): lines = [] lines.append("# Thermal properties / unit cell (natom)") lines.append("") lines.append("unit:") lines.append(" temperature: K") lines.append(" free_energy: kJ/mol") lines.append(" entropy: J/K/mol") lines.append(" heat_capacity: J/K/mol") lines.append("") lines.append("natom: %5d" % (self.natoms)) lines.append("num_modes: %d" % (3 * self.natoms)) lines.append("num_integrated_modes: %d" % (3 * self.natoms)) lines.append("") lines.append("zero_point_energy: %15.7f" % (thermo.zpe[j] * abu.e_Cb * abu.Avogadro)) lines.append("high_T_entropy: %15.7f" % 0) # high_T_entropy is not used in QHA lines.append("") lines.append("thermal_properties:") fe = thermo.free_energy[j] * abu.e_Cb * abu.Avogadro entropy = thermo.entropy[j] * abu.e_Cb * abu.Avogadro cv = thermo.cv[j] * abu.e_Cb * abu.Avogadro temperatures = thermo.tmesh for i, t in enumerate(temperatures): lines.append("- temperature: %15.7f" % t) lines.append(" free_energy: %15.7f" % (fe[i] / 1000)) lines.append(" entropy: %15.7f" % entropy[i]) # Sometimes 'nan' of C_V is returned at low temperature. if np.isnan(cv[i]): lines.append(" heat_capacity: %15.7f" % 0) else: lines.append(" heat_capacity: %15.7f" % cv[i]) lines.append(" energy: %15.7f" % (fe[i] / 1000 + entropy[i] * t / 1000)) lines.append("") with open( os.path.join(path, "thermal_properties-{}.yaml".format(j)), 'wt') as f: f.write("\n".join(lines)) def get_phonopy_qha(self, tstart=0, tstop=2100, num=211, eos='vinet', t_max=None, energy_plot_factor=None): """ Creates an instance of phonopy.qha.QHA that can be used generate further plots and output data. The object is returned right after the construction. The "run()" method should be executed before getting results and plots. Notice that phonopy apparently requires the value of the 300 K temperature to be present in the list. Choose the values of tstart, tstop and num to satisfy this condition. Args: tstart: The starting value (in Kelvin) of the temperature mesh. tstop: The end value (in Kelvin) of the mesh. num: int, optional Number of samples to generate. Default is 211. eos: the expression used to fit the energies in phonopy. Possible values are "vinet", "murnaghan" and "birch_murnaghan". Passed to phonopy's QHA. t_max: maximum temperature. Passed to phonopy's QHA. energy_plot_factor: factor multiplying the energies. Passed to phonopy's QHA. Returns: An instance of phonopy.qha.QHA """ try: from phonopy.qha import QHA as QHA_phonopy except ImportError as exc: print("Phonopy is required to generate the QHA phonopy object") raise exc thermo = self.get_thermodynamic_properties(tstart=tstart, tstop=tstop, num=num) fe = thermo.free_energy.T * abu.e_Cb * abu.Avogadro / 1000 entropy = thermo.entropy.T * abu.e_Cb * abu.Avogadro cv = thermo.cv.T * abu.e_Cb * abu.Avogadro temperatures = thermo.tmesh en = self.energies + self.volumes * self.pressure / abu.eVA3_GPa qha_p = QHA_phonopy(self.volumes, en, temperatures, cv, entropy, fe, eos, t_max, energy_plot_factor) return qha_p
def test_fitting(self): # courtesy of @katherinelatimer2013 # known correct values for Vinet # Mg mp153_volumes = [ 16.69182365, 17.25441763, 17.82951915, 30.47573817, 18.41725977, 29.65211363, 28.84346369, 19.01777055, 28.04965916, 19.63120886, 27.27053682, 26.5059864, 20.25769112, 25.75586879, 20.89736201, 25.02003097, 21.55035204, 24.29834347, 22.21681221, 23.59066888, 22.89687316, ] mp153_energies = [ -1.269884575, -1.339411225, -1.39879471, -1.424480995, -1.44884184, -1.45297499, -1.4796246, -1.49033594, -1.504198485, -1.52397006, -1.5264432, -1.54609291, -1.550269435, -1.56284009, -1.569937375, -1.576420935, -1.583470925, -1.58647189, -1.591436505, -1.592563495, -1.594347355, ] mp153_known_energies_vinet = [ -1.270038831, -1.339366487, -1.398683238, -1.424556061, -1.448746649, -1.453000456, -1.479614511, -1.490266797, -1.504163502, -1.523910268, -1.526395734, -1.546038792, -1.550298657, -1.562800797, -1.570015274, -1.576368392, -1.583605186, -1.586404575, -1.591578378, -1.592547954, -1.594410995, ] # C: 4.590843262 # B: 2.031381599 mp153_known_e0_vinet = -1.594429229 mp153_known_v0_vinet = 22.95764159 eos = EOS(eos_name="vinet") fit = eos.fit(mp153_volumes, mp153_energies) np.testing.assert_array_almost_equal(fit.func(mp153_volumes), mp153_known_energies_vinet, decimal=5) self.assertAlmostEqual(mp153_known_e0_vinet, fit.e0, places=4) self.assertAlmostEqual(mp153_known_v0_vinet, fit.v0, places=4) # expt. value 35.5, known fit 36.16 self.assertAlmostEqual(fit.b0_GPa, 36.16258687442761, 4) # Si mp149_volumes = [ 15.40611854, 14.90378698, 16.44439516, 21.0636307, 17.52829835, 16.98058208, 18.08767363, 18.65882487, 19.83693435, 15.91961152, 22.33987173, 21.69548924, 22.99688883, 23.66666322, 20.44414922, 25.75374305, 19.24187473, 24.34931029, 25.04496106, 27.21116571, 26.4757653, ] mp149_energies = [ -4.866909695, -4.7120965, -5.10921253, -5.42036228, -5.27448405, -5.200810795, -5.331915665, -5.3744186, -5.420058145, -4.99862686, -5.3836163, -5.40610838, -5.353700425, -5.31714654, -5.425263555, -5.174988295, -5.403353105, -5.27481447, -5.227210275, -5.058992615, -5.118805775, ] mp149_known_energies_vinet = [ -4.866834585, -4.711786499, -5.109642598, -5.420093739, -5.274605844, -5.201025714, -5.331899365, -5.374315789, -5.419671568, -4.998827503, -5.383703409, -5.406038887, -5.353926272, -5.317484252, -5.424963418, -5.175090887, -5.403166824, -5.275096644, -5.227427635, -5.058639193, -5.118654229, ] # C: 4.986513158 # B: 4.964976215 mp149_known_e0_vinet = -5.424963506 mp149_known_v0_vinet = 20.44670279 eos = EOS(eos_name="vinet") fit = eos.fit(mp149_volumes, mp149_energies) np.testing.assert_array_almost_equal(fit.func(mp149_volumes), mp149_known_energies_vinet, decimal=5) self.assertAlmostEqual(mp149_known_e0_vinet, fit.e0, places=4) self.assertAlmostEqual(mp149_known_v0_vinet, fit.v0, places=4) # expt. value 97.9, known fit 88.39 self.assertAlmostEqual(fit.b0_GPa, 88.38629337404822, 4) # Ti mp72_volumes = [ 12.49233296, 12.91339188, 13.34380224, 22.80836212, 22.19195533, 13.78367177, 21.58675559, 14.23310328, 20.99266009, 20.4095592, 14.69220297, 19.83736385, 15.16106697, 19.2759643, 15.63980711, 18.72525771, 16.12851491, 18.18514127, 16.62729878, 17.65550599, 17.13626153, ] mp72_energies = [ -7.189983803, -7.33985647, -7.468745423, -7.47892835, -7.54945107, -7.578012237, -7.61513166, -7.66891898, -7.67549721, -7.73000681, -7.74290386, -7.77803379, -7.801246383, -7.818964483, -7.84488189, -7.85211192, -7.87486651, -7.876767777, -7.892161533, -7.892199957, -7.897605303, ] mp72_known_energies_vinet = [ -7.189911138, -7.339810181, -7.468716095, -7.478678021, -7.549402394, -7.578034391, -7.615240977, -7.669091347, -7.675683891, -7.730188653, -7.74314028, -7.778175824, -7.801363213, -7.819030923, -7.844878053, -7.852099741, -7.874737806, -7.876686864, -7.891937429, -7.892053535, -7.897414664, ] # C: 3.958192998 # B: 6.326790098 mp72_known_e0_vinet = -7.897414997 mp72_known_v0_vinet = 17.13223229 eos = EOS(eos_name="vinet") fit = eos.fit(mp72_volumes, mp72_energies) np.testing.assert_array_almost_equal(fit.func(mp72_volumes), mp72_known_energies_vinet, decimal=5) self.assertAlmostEqual(mp72_known_e0_vinet, fit.e0, places=4) self.assertAlmostEqual(mp72_known_v0_vinet, fit.v0, places=4) # expt. value 107.3, known fit 112.63 self.assertAlmostEqual(fit.b0_GPa, 112.62927187296167, 4)
def test_run_all_models(self): for eos_name in EOS.MODELS: eos = EOS(eos_name=eos_name) _ = eos.fit(self.volumes, self.energies)
class QuasiharmonicDebyeApprox: """ Quasiharmonic approximation. """ def __init__( self, energies, volumes, structure, t_min=300.0, t_step=100, t_max=300.0, eos="vinet", pressure=0.0, poisson=0.25, use_mie_gruneisen=False, anharmonic_contribution=False, ): """ Args: energies (list): list of DFT energies in eV volumes (list): list of volumes in Ang^3 structure (Structure): t_min (float): min temperature t_step (float): temperature step t_max (float): max temperature eos (str): equation of state used for fitting the energies and the volumes. options supported by pymatgen: "quadratic", "murnaghan", "birch", "birch_murnaghan", "pourier_tarantola", "vinet", "deltafactor", "numerical_eos" pressure (float): in GPa, optional. poisson (float): poisson ratio. use_mie_gruneisen (bool): whether or not to use the mie-gruneisen formulation to compute the gruneisen parameter. The default is the slater-gamma formulation. anharmonic_contribution (bool): whether or not to consider the anharmonic contribution to the Debye temperature. Cannot be used with use_mie_gruneisen. Defaults to False. """ self.energies = energies self.volumes = volumes self.structure = structure self.temperature_min = t_min self.temperature_max = t_max self.temperature_step = t_step self.eos_name = eos self.pressure = pressure self.poisson = poisson self.use_mie_gruneisen = use_mie_gruneisen self.anharmonic_contribution = anharmonic_contribution if self.use_mie_gruneisen and self.anharmonic_contribution: raise ValueError( "The Mie-Gruneisen formulation and anharmonic contribution are circular referenced and " "cannot be used together." ) self.mass = sum(e.atomic_mass for e in self.structure.species) self.natoms = self.structure.composition.num_atoms self.avg_mass = physical_constants["atomic mass constant"][0] * self.mass / self.natoms # kg self.kb = physical_constants["Boltzmann constant in eV/K"][0] self.hbar = physical_constants["Planck constant over 2 pi in eV s"][0] self.gpa_to_ev_ang = 1.0 / 160.21766208 # 1 GPa in ev/Ang^3 self.gibbs_free_energy = [] # optimized values, eV # list of temperatures for which the optimized values are available, K self.temperatures = [] self.optimum_volumes = [] # in Ang^3 # fit E and V and get the bulk modulus(used to compute the Debye # temperature) logger.info("Fitting E and V") self.eos = EOS(eos) self.ev_eos_fit = self.eos.fit(volumes, energies) self.bulk_modulus = self.ev_eos_fit.b0_GPa # in GPa self.optimize_gibbs_free_energy() def optimize_gibbs_free_energy(self): """ Evaluate the gibbs free energy as a function of V, T and P i.e G(V, T, P), minimize G(V, T, P) wrt V for each T and store the optimum values. Note: The data points for which the equation of state fitting fails are skipped. """ temperatures = np.linspace( self.temperature_min, self.temperature_max, int(np.ceil((self.temperature_max - self.temperature_min) / self.temperature_step) + 1), ) for t in temperatures: try: G_opt, V_opt = self.optimizer(t) except Exception: if len(temperatures) <= 1: raise logger.info(f"EOS fitting failed, so skipping this data point, {t}") self.gibbs_free_energy.append(G_opt) self.temperatures.append(t) self.optimum_volumes.append(V_opt) def optimizer(self, temperature): """ Evaluate G(V, T, P) at the given temperature(and pressure) and minimize it wrt V. 1. Compute the vibrational helmholtz free energy, A_vib. 2. Compute the gibbs free energy as a function of volume, temperature and pressure, G(V,T,P). 3. Preform an equation of state fit to get the functional form of gibbs free energy:G(V, T, P). 4. Finally G(V, P, T) is minimized with respect to V. Args: temperature (float): temperature in K Returns: float, float: G_opt(V_opt, T, P) in eV and V_opt in Ang^3. """ G_V = [] # G for each volume # G = E(V) + PV + A_vib(V, T) for i, v in enumerate(self.volumes): G_V.append( self.energies[i] + self.pressure * v * self.gpa_to_ev_ang + self.vibrational_free_energy(temperature, v) ) # fit equation of state, G(V, T, P) eos_fit = self.eos.fit(self.volumes, G_V) # minimize the fit eos wrt volume # Note: the ref energy and the ref volume(E0 and V0) not necessarily # the same as minimum energy and min volume. volume_guess = eos_fit.volumes[np.argmin(eos_fit.energies)] min_wrt_vol = minimize(eos_fit.func, volume_guess) # G_opt=G(V_opt, T, P), V_opt return min_wrt_vol.fun, min_wrt_vol.x[0] def vibrational_free_energy(self, temperature, volume): """ Vibrational Helmholtz free energy, A_vib(V, T). Eq(4) in doi.org/10.1016/j.comphy.2003.12.001 Args: temperature (float): temperature in K volume (float) Returns: float: vibrational free energy in eV """ y = self.debye_temperature(volume) / temperature return ( self.kb * self.natoms * temperature * (9.0 / 8.0 * y + 3 * np.log(1 - np.exp(-y)) - self.debye_integral(y)) ) def vibrational_internal_energy(self, temperature, volume): """ Vibrational internal energy, U_vib(V, T). Eq(4) in doi.org/10.1016/j.comphy.2003.12.001 Args: temperature (float): temperature in K volume (float): in Ang^3 Returns: float: vibrational internal energy in eV """ y = self.debye_temperature(volume) / temperature return self.kb * self.natoms * temperature * (9.0 / 8.0 * y + 3 * self.debye_integral(y)) def debye_temperature(self, volume): """ Calculates the debye temperature. Eq(6) in doi.org/10.1016/j.comphy.2003.12.001. Thanks to Joey. Eq(6) above is equivalent to Eq(3) in doi.org/10.1103/PhysRevB.37.790 which does not consider anharmonic effects. Eq(20) in the same paper and Eq(18) in doi.org/10.1016/j.commatsci.2009.12.006 both consider anharmonic contributions to the Debye temperature through the Gruneisen parameter at 0K (Gruneisen constant). The anharmonic contribution is toggled by setting the anharmonic_contribution to True or False in the QuasiharmonicDebyeApprox constructor. Args: volume (float): in Ang^3 Returns: float: debye temperature in K """ term1 = (2.0 / 3.0 * (1.0 + self.poisson) / (1.0 - 2.0 * self.poisson)) ** 1.5 term2 = (1.0 / 3.0 * (1.0 + self.poisson) / (1.0 - self.poisson)) ** 1.5 f = (3.0 / (2.0 * term1 + term2)) ** (1.0 / 3.0) debye = 2.9772e-11 * (volume / self.natoms) ** (-1.0 / 6.0) * f * np.sqrt(self.bulk_modulus / self.avg_mass) if self.anharmonic_contribution: gamma = self.gruneisen_parameter(0, self.ev_eos_fit.v0) # 0K equilibrium Gruneisen parameter return debye * (self.ev_eos_fit.v0 / volume) ** (gamma) return debye @staticmethod def debye_integral(y): """ Debye integral. Eq(5) in doi.org/10.1016/j.comphy.2003.12.001 Args: y (float): debye temperature/T, upper limit Returns: float: unitless """ # floating point limit is reached around y=155, so values beyond that # are set to the limiting value(T-->0, y --> \infty) of # 6.4939394 (from wolfram alpha). factor = 3.0 / y ** 3 if y < 155: integral = quadrature(lambda x: x ** 3 / (np.exp(x) - 1.0), 0, y) return list(integral)[0] * factor return 6.493939 * factor def gruneisen_parameter(self, temperature, volume): """ Slater-gamma formulation(the default): gruneisen paramter = - d log(theta)/ d log(V) = - ( 1/6 + 0.5 d log(B)/ d log(V) ) = - (1/6 + 0.5 V/B dB/dV), where dB/dV = d^2E/dV^2 + V * d^3E/dV^3 Mie-gruneisen formulation: Eq(31) in doi.org/10.1016/j.comphy.2003.12.001 Eq(7) in Blanco et. al. Joumal of Molecular Structure (Theochem) 368 (1996) 245-255 Also se J.P. Poirier, Introduction to the Physics of the Earth’s Interior, 2nd ed. (Cambridge University Press, Cambridge, 2000) Eq(3.53) Args: temperature (float): temperature in K volume (float): in Ang^3 Returns: float: unitless """ if isinstance(self.eos, PolynomialEOS): p = np.poly1d(self.eos.eos_params) # pylint: disable=E1101 # first derivative of energy at 0K wrt volume evaluated at the # given volume, in eV/Ang^3 dEdV = np.polyder(p, 1)(volume) # second derivative of energy at 0K wrt volume evaluated at the # given volume, in eV/Ang^6 d2EdV2 = np.polyder(p, 2)(volume) # third derivative of energy at 0K wrt volume evaluated at the # given volume, in eV/Ang^9 d3EdV3 = np.polyder(p, 3)(volume) else: func = self.ev_eos_fit.func dEdV = derivative(func, volume, dx=1e-3) d2EdV2 = derivative(func, volume, dx=1e-3, n=2, order=5) d3EdV3 = derivative(func, volume, dx=1e-3, n=3, order=7) # Mie-gruneisen formulation if self.use_mie_gruneisen: p0 = dEdV return ( self.gpa_to_ev_ang * volume * (self.pressure + p0 / self.gpa_to_ev_ang) / self.vibrational_internal_energy(temperature, volume) ) # Slater-gamma formulation # first derivative of bulk modulus wrt volume, eV/Ang^6 dBdV = d2EdV2 + d3EdV3 * volume return -(1.0 / 6.0 + 0.5 * volume * dBdV / FloatWithUnit(self.ev_eos_fit.b0_GPa, "GPa").to("eV ang^-3")) def thermal_conductivity(self, temperature, volume): """ Eq(17) in 10.1103/PhysRevB.90.174107 Args: temperature (float): temperature in K volume (float): in Ang^3 Returns: float: thermal conductivity in W/K/m """ gamma = self.gruneisen_parameter(temperature, volume) theta_d = self.debye_temperature(volume) # K theta_a = theta_d * self.natoms ** (-1.0 / 3.0) # K prefactor = (0.849 * 3 * 4 ** (1.0 / 3.0)) / (20.0 * np.pi ** 3) # kg/K^3/s^3 prefactor = prefactor * (self.kb / self.hbar) ** 3 * self.avg_mass kappa = prefactor / (gamma ** 2 - 0.514 * gamma + 0.228) # kg/K/s^3 * Ang = (kg m/s^2)/(Ks)*1e-10 # = N/(Ks)*1e-10 = Nm/(Kms)*1e-10 = W/K/m*1e-10 kappa = kappa * theta_a ** 2 * volume ** (1.0 / 3.0) * 1e-10 return kappa def get_summary_dict(self): """ Returns a dict with a summary of the computed properties. """ d = defaultdict(list) d["pressure"] = self.pressure d["poisson"] = self.poisson d["mass"] = self.mass d["natoms"] = int(self.natoms) d["bulk_modulus"] = self.bulk_modulus d["gibbs_free_energy"] = self.gibbs_free_energy d["temperatures"] = self.temperatures d["optimum_volumes"] = self.optimum_volumes for v, t in zip(self.optimum_volumes, self.temperatures): d["debye_temperature"].append(self.debye_temperature(v)) d["gruneisen_parameter"].append(self.gruneisen_parameter(t, v)) d["thermal_conductivity"].append(self.thermal_conductivity(t, v)) return d
def volume_workflow_is_converged(pwd, max_num_sub, min_num_vols, volume_tolerance): workflow_converged_list = [] E, V = [], [] minE = 100 for root, dirs, files in os.walk(pwd): for file in files: if file == 'POTCAR' and check_vasp_input(root) == True: if os.path.exists(os.path.join(root, 'vasprun.xml')): try: Vr = Vasprun(os.path.join(root, 'vasprun.xml')) fizzled = False except: fizzled = True workflow_converged_list.append(False) if fizzled == False: job = is_converged(root) if job == 'converged': workflow_converged_list.append(True) vol = Poscar.from_file(os.path.join( root, 'POSCAR')).structure.volume if Vr.final_energy < minE: minE = Vr.final_energy minV = vol minE_path = root minE_formula = str( Poscar.from_file( os.path.join(path, 'POSCAR')). structure.composition.reduced_formula) E.append(Vr.final_energy) V.append(vol) elif fizzled == True and get_incar_value( path, 'STAGE_NUMBER' ) == 0: #job is failing on initial relaxation num_sub = get_number_of_subs(root) if num_sub == max_num_sub: os.remove(os.path.join(root, 'POTCAR')) #job failed too many times.... just ignore this job for the remainder of the workflow else: workflow_converged_list.append(False) num_jobs = check_num_jobs_in_workflow(pwd) if num_jobs < min_num_vols and len(E) > 0: scale_around_min = [0.98, 1.02] for s in scale_around_min: write_path = os.path.join(pwd, minE_formula + str(s * minV)) os.mkdir(write_path) structure = Poscar.from_file(os.path.join(minE_path, 'POSCAR')).structure structure.scale_lattice(s * minV) Poscar.write_file(structure, os.path.join(write_path, 'POSCAR')) files_copy = [ 'backup/Init/INCAR', 'CONVERGENCE', 'KPOINTS', 'POTCAR' ] for fc in files_copy: copy_from_path = os.path.join(minE_path, fc) if os.path.exists(copy_from_path): copy(copy_from_path, write_path) remove_sys_incar(write_path) #create new jobs if False not in workflow_converged_list: if len(E) > min_num_vols - 1: volumes = V energies = E eos = EOS(eos_name='murnaghan') eos_fit = eos.fit(volumes, energies) eos_minV = eos_fit.v0 if abs(eos_minV - minV) < volume_tolerance: # ang^3 cutoff return True #eos_fit.plot() else: scale_around_min = [0.99, 1, 1.01] for s in scale_around_min: write_path = os.path.join(pwd, minE_formula + str(s * eos_minV)) os.mkdir(write_path) structure = Poscar.from_file( os.path.join(minE_path, 'POSCAR')).structure structure.scale_lattice(s * eos_minV) Poscar.write_file(structure, os.path.join(write_path, 'POSCAR')) files_copy = [ 'backup/Init/INCAR', 'CONVERGENCE', 'KPOINTS', 'POTCAR' ] for fc in files_copy: copy_from_path = os.path.join(minE_path, fc) if os.path.exists(copy_from_path): copy(copy_from_path, write_path) remove_sys_incar(write_path) return False else: return False
class QuasiharmonicDebyeApprox(object): """ Args: energies (list): list of DFT energies in eV volumes (list): list of volumes in Ang^3 structure (Structure): t_min (float): min temperature t_step (float): temperature step t_max (float): max temperature eos (str): equation of state used for fitting the energies and the volumes. options supported by pymatgen: "quadratic", "murnaghan", "birch", "birch_murnaghan", "pourier_tarantola", "vinet", "deltafactor", "numerical_eos" pressure (float): in GPa, optional. poisson (float): poisson ratio. use_mie_gruneisen (bool): whether or not to use the mie-gruneisen formulation to compute the gruneisen parameter. The default is the slater-gamma formulation. anharmonic_contribution (bool): whether or not to consider the anharmonic contribution to the Debye temperature. Cannot be used with use_mie_gruneisen. Defaults to False. """ def __init__(self, energies, volumes, structure, t_min=300.0, t_step=100, t_max=300.0, eos="vinet", pressure=0.0, poisson=0.25, use_mie_gruneisen=False, anharmonic_contribution=False): self.energies = energies self.volumes = volumes self.structure = structure self.temperature_min = t_min self.temperature_max = t_max self.temperature_step = t_step self.eos_name = eos self.pressure = pressure self.poisson = poisson self.use_mie_gruneisen = use_mie_gruneisen self.anharmonic_contribution = anharmonic_contribution if self.use_mie_gruneisen and self.anharmonic_contribution: raise ValueError('The Mie-Gruneisen formulation and anharmonic contribution are circular referenced and cannot be used together.') self.mass = sum([e.atomic_mass for e in self.structure.species]) self.natoms = self.structure.composition.num_atoms self.avg_mass = physical_constants["atomic mass constant"][0] \ * self.mass / self.natoms # kg self.kb = physical_constants["Boltzmann constant in eV/K"][0] self.hbar = physical_constants["Planck constant over 2 pi in eV s"][0] self.gpa_to_ev_ang = 1./160.21766208 # 1 GPa in ev/Ang^3 self.gibbs_free_energy = [] # optimized values, eV # list of temperatures for which the optimized values are available, K self.temperatures = [] self.optimum_volumes = [] # in Ang^3 # fit E and V and get the bulk modulus(used to compute the Debye # temperature) print("Fitting E and V") self.eos = EOS(eos) self.ev_eos_fit = self.eos.fit(volumes, energies) self.bulk_modulus = self.ev_eos_fit.b0_GPa # in GPa self.optimize_gibbs_free_energy() def optimize_gibbs_free_energy(self): """ Evaluate the gibbs free energy as a function of V, T and P i.e G(V, T, P), minimize G(V, T, P) wrt V for each T and store the optimum values. Note: The data points for which the equation of state fitting fails are skipped. """ temperatures = np.linspace( self.temperature_min, self.temperature_max, int(np.ceil((self.temperature_max - self.temperature_min) / self.temperature_step) + 1)) for t in temperatures: try: G_opt, V_opt = self.optimizer(t) except: if len(temperatures) > 1: print("EOS fitting failed, so skipping this data point, {}". format(t)) continue else: raise self.gibbs_free_energy.append(G_opt) self.temperatures.append(t) self.optimum_volumes.append(V_opt) def optimizer(self, temperature): """ Evaluate G(V, T, P) at the given temperature(and pressure) and minimize it wrt V. 1. Compute the vibrational helmholtz free energy, A_vib. 2. Compute the gibbs free energy as a function of volume, temperature and pressure, G(V,T,P). 3. Preform an equation of state fit to get the functional form of gibbs free energy:G(V, T, P). 4. Finally G(V, P, T) is minimized with respect to V. Args: temperature (float): temperature in K Returns: float, float: G_opt(V_opt, T, P) in eV and V_opt in Ang^3. """ G_V = [] # G for each volume # G = E(V) + PV + A_vib(V, T) for i, v in enumerate(self.volumes): G_V.append(self.energies[i] + self.pressure * v * self.gpa_to_ev_ang + self.vibrational_free_energy(temperature, v)) # fit equation of state, G(V, T, P) eos_fit = self.eos.fit(self.volumes, G_V) # minimize the fit eos wrt volume # Note: the ref energy and the ref volume(E0 and V0) not necessarily # the same as minimum energy and min volume. volume_guess = eos_fit.volumes[np.argmin(eos_fit.energies)] min_wrt_vol = minimize(eos_fit.func, volume_guess) # G_opt=G(V_opt, T, P), V_opt return min_wrt_vol.fun, min_wrt_vol.x[0] def vibrational_free_energy(self, temperature, volume): """ Vibrational Helmholtz free energy, A_vib(V, T). Eq(4) in doi.org/10.1016/j.comphy.2003.12.001 Args: temperature (float): temperature in K volume (float) Returns: float: vibrational free energy in eV """ y = self.debye_temperature(volume) / temperature return self.kb * self.natoms * temperature * ( 9./8. * y + 3 * np.log(1 - np.exp(-y)) - self.debye_integral(y)) def vibrational_internal_energy(self, temperature, volume): """ Vibrational internal energy, U_vib(V, T). Eq(4) in doi.org/10.1016/j.comphy.2003.12.001 Args: temperature (float): temperature in K volume (float): in Ang^3 Returns: float: vibrational internal energy in eV """ y = self.debye_temperature(volume) / temperature return self.kb * self.natoms * temperature * (9./8. * y + 3*self.debye_integral(y)) def debye_temperature(self, volume): """ Calculates the debye temperature. Eq(6) in doi.org/10.1016/j.comphy.2003.12.001. Thanks to Joey. Eq(6) above is equivalent to Eq(3) in doi.org/10.1103/PhysRevB.37.790 which does not consider anharmonic effects. Eq(20) in the same paper and Eq(18) in doi.org/10.1016/j.commatsci.2009.12.006 both consider anharmonic contributions to the Debye temperature through the Gruneisen parameter at 0K (Gruneisen constant). The anharmonic contribution is toggled by setting the anharmonic_contribution to True or False in the QuasiharmonicDebyeApprox constructor. Args: volume (float): in Ang^3 Returns: float: debye temperature in K """ term1 = (2./3. * (1. + self.poisson) / (1. - 2. * self.poisson))**1.5 term2 = (1./3. * (1. + self.poisson) / (1. - self.poisson))**1.5 f = (3. / (2. * term1 + term2))**(1. / 3.) debye = 2.9772e-11 * (volume / self.natoms) ** (-1. / 6.) * f * \ np.sqrt(self.bulk_modulus/self.avg_mass) if self.anharmonic_contribution: gamma = self.gruneisen_parameter(0, self.ev_eos_fit.v0) # 0K equilibrium Gruneisen parameter return debye * (self.ev_eos_fit.v0 / volume) ** (gamma) else: return debye @staticmethod def debye_integral(y): """ Debye integral. Eq(5) in doi.org/10.1016/j.comphy.2003.12.001 Args: y (float): debye temperature/T, upper limit Returns: float: unitless """ # floating point limit is reached around y=155, so values beyond that # are set to the limiting value(T-->0, y --> \infty) of # 6.4939394 (from wolfram alpha). factor = 3. / y ** 3 if y < 155: integral = quadrature(lambda x: x ** 3 / (np.exp(x) - 1.), 0, y) return list(integral)[0] * factor else: return 6.493939 * factor def gruneisen_parameter(self, temperature, volume): """ Slater-gamma formulation(the default): gruneisen paramter = - d log(theta)/ d log(V) = - ( 1/6 + 0.5 d log(B)/ d log(V) ) = - (1/6 + 0.5 V/B dB/dV), where dB/dV = d^2E/dV^2 + V * d^3E/dV^3 Mie-gruneisen formulation: Eq(31) in doi.org/10.1016/j.comphy.2003.12.001 Eq(7) in Blanco et. al. Joumal of Molecular Structure (Theochem) 368 (1996) 245-255 Also se J.P. Poirier, Introduction to the Physics of the Earth’s Interior, 2nd ed. (Cambridge University Press, Cambridge, 2000) Eq(3.53) Args: temperature (float): temperature in K volume (float): in Ang^3 Returns: float: unitless """ if isinstance(self.eos, PolynomialEOS): p = np.poly1d(self.eos.eos_params) # first derivative of energy at 0K wrt volume evaluated at the # given volume, in eV/Ang^3 dEdV = np.polyder(p, 1)(volume) # second derivative of energy at 0K wrt volume evaluated at the # given volume, in eV/Ang^6 d2EdV2 = np.polyder(p, 2)(volume) # third derivative of energy at 0K wrt volume evaluated at the # given volume, in eV/Ang^9 d3EdV3 = np.polyder(p, 3)(volume) else: func = self.ev_eos_fit.func dEdV = derivative(func, volume, dx=1e-3) d2EdV2 = derivative(func, volume, dx=1e-3, n=2, order=5) d3EdV3 = derivative(func, volume, dx=1e-3, n=3, order=7) # Mie-gruneisen formulation if self.use_mie_gruneisen: p0 = dEdV return (self.gpa_to_ev_ang * volume * (self.pressure + p0 / self.gpa_to_ev_ang) / self.vibrational_internal_energy(temperature, volume)) # Slater-gamma formulation # first derivative of bulk modulus wrt volume, eV/Ang^6 dBdV = d2EdV2 + d3EdV3 * volume return -(1./6. + 0.5 * volume * dBdV / FloatWithUnit(self.ev_eos_fit.b0_GPa, "GPa").to("eV ang^-3")) def thermal_conductivity(self, temperature, volume): """ Eq(17) in 10.1103/PhysRevB.90.174107 Args: temperature (float): temperature in K volume (float): in Ang^3 Returns: float: thermal conductivity in W/K/m """ gamma = self.gruneisen_parameter(temperature, volume) theta_d = self.debye_temperature(volume) # K theta_a = theta_d * self.natoms**(-1./3.) # K prefactor = (0.849 * 3 * 4**(1./3.)) / (20. * np.pi**3) # kg/K^3/s^3 prefactor = prefactor * (self.kb/self.hbar)**3 * self.avg_mass kappa = prefactor / (gamma**2 - 0.514 * gamma + 0.228) # kg/K/s^3 * Ang = (kg m/s^2)/(Ks)*1e-10 # = N/(Ks)*1e-10 = Nm/(Kms)*1e-10 = W/K/m*1e-10 kappa = kappa * theta_a**2 * volume**(1./3.) * 1e-10 return kappa def get_summary_dict(self): """ Returns a dict with a summary of the computed properties. """ d = defaultdict(list) d["pressure"] = self.pressure d["poisson"] = self.poisson d["mass"] = self.mass d["natoms"] = int(self.natoms) d["bulk_modulus"] = self.bulk_modulus d["gibbs_free_energy"] = self.gibbs_free_energy d["temperatures"] = self.temperatures d["optimum_volumes"] = self.optimum_volumes for v, t in zip(self.optimum_volumes, self.temperatures): d["debye_temperature"].append(self.debye_temperature(v)) d["gruneisen_parameter"].append(self.gruneisen_parameter(t, v)) d["thermal_conductivity"].append(self.thermal_conductivity(t, v)) return d
def analyze_eos_flow(flow, **kwargs): work = flow[0] etotals = work.read_etotals(unit="eV") eos_fit = EOS(eos_name="birch_murnaghan").fit(flow.volumes, etotals) return eos_fit.plot(**kwargs)
def test_fit(self): """Test EOS fit""" for eos_name in EOS.MODELS: eos = EOS(eos_name=eos_name) fit = eos.fit(self.volumes, self.energies) print(fit)
def check_fit(self, volumes, energies): eos = EOS('vinet') self.eos_fit = eos.fit(volumes, energies)