Source code for vasp_step.optimization

# -*- coding: utf-8 -*-

"""Non-graphical part of the Optimization step in a VASP flowchart"""

import importlib
import logging
import textwrap

from tabulate import tabulate

import vasp_step
import molsystem
import seamm
import seamm_util.printing as printing
from seamm_util.printing import FormattedText as __

# In addition to the normal logger, two logger-like printing facilities are
# defined: "job" and "printer". "job" send output to the main job.out file for
# the job, and should be used very sparingly, typically to echo what this step
# will do in the initial summary of the job.
#
# "printer" sends output to the file "step.out" in this steps working
# directory, and is used for all normal output from this step.

logger = logging.getLogger(__name__)
job = printing.getPrinter()
printer = printing.getPrinter("VASP")

# Add this module's properties to the standard properties
path = importlib.resources.files("vasp_step") / "data"
csv_file = path / "properties.csv"
if path.exists():
    molsystem.add_properties_from_file(csv_file)


[docs] class Optimization(vasp_step.Energy): """ The non-graphical part of a Optimization step in a flowchart. Attributes ---------- parser : configargparse.ArgParser The parser object. options : tuple It contains a two item tuple containing the populated namespace and the list of remaining argument strings. subflowchart : seamm.Flowchart A SEAMM Flowchart object that represents a subflowchart, if needed. parameters : OptimizationParameters The control parameters for Optimization. See Also -------- TkOptimization, Optimization, OptimizationParameters """ def __init__( self, flowchart=None, title="Optimization", extension=None, logger=logger ): """A substep for Optimization in a subflowchart for VASP. You may wish to change the title above, which is the string displayed in the box representing the step in the flowchart. Parameters ---------- flowchart: seamm.Flowchart The non-graphical flowchart that contains this step. title: str The name displayed in the flowchart. extension: None Not yet implemented logger : Logger = logger The logger to use and pass to parent classes Returns ------- None """ logger.debug(f"Creating Optimization {self}") super().__init__( flowchart=flowchart, title=title, extension=extension, logger=logger, ) self._calculation = "Optimization" self._model = None self._metadata = vasp_step.metadata self.parameters = vasp_step.OptimizationParameters() @property def header(self): """A printable header for this section of output""" return "Step {}: {}".format(".".join(str(e) for e in self._id), self.title) @property def version(self): """The semantic version of this module.""" return vasp_step.__version__ @property def git_revision(self): """The git version of this module.""" return vasp_step.__git_revision__
[docs] def description_text(self, P=None): """Create the text description of what this step will do. The dictionary of control values is passed in as P so that the code can test values, etc. Parameters ---------- P: dict An optional dictionary of the current values of the control parameters. Returns ------- str A description of the current step. """ if not P: P = self.parameters.values_to_dict() result = "" if self._calculation == "Optimization": result = self.header + "\n" method = P["optimization method"] text = "The structure will be optimized using the {optimization method}" text += " until the maximim force on an atom is below about" text += " {convergence cutoff} with a limit of {number of steps} steps." tmp = P["optimize atom positions"] atoms = tmp == "yes" if isinstance(tmp, str) else tmp tmp = P["optimize cell shape"] shape = tmp == "yes" if isinstance(tmp, str) else tmp tmp = P["optimize cell volume"] volume = tmp == "yes" if isinstance(tmp, str) else tmp if self.is_expr(atoms) or self.is_expr(shape) or self.is_expr(volume): if self.is_expr(atoms): text += " '{optimize atom positions}' will determine whether to" text += " optimize the atoms positions." elif atoms: text += " The atom positions will be relaxed." if self.is_expr(shape): text += " '{optimize cell shape}' will determine whether to" text += " optimize the shape of the cell." elif shape: text += " The shape of the cell will be relaxed." if self.is_expr(volume): text += " '{optimize cell volume}' will determine whether to" text += " optimize the volume of the cell." elif volume: text += " The volume of the cell will be relaxed." else: if atoms and shape and volume: text += " The cell and atom positions will be fully relaxed." elif atoms: text += " The atom positions" if shape: text += " and cell shape" elif volume: text += " and cell volume" text += " will be optimized." else: if shape: text += " The cell shape" if volume: text += " and volume" elif volume: text += " The cell volume" text += " will be optimized, but the fractional coordinates of the " text += "atoms will be fixed." text += "\n" if self.is_expr(method): text += "The details of the algorithm used will be established at runtime." elif method == "RMM-DIIS": text += "The RMM-DIIS method will scale the steps by {step scale factor}" history = P["iteration history length"] if self.is_expr(history): text += " with {iteration history length} controlling how many " text += "previous steps will be used in determining the next step." elif history == "default": text += " and VASP will decide how many previous steps to use, based on" text += " the eigenvalues of the inverse approximate Hessian matrix." else: text += " and use {iteration history length} previous steps. " text += "For large systems you may find that values of 10-20 work well." elif method == "Conjugate Gradients": text += "The conjugate gradients method will scale the steps " text += "by {step scale factor}." elif method == "Damped MD": text += "The damped MD will " approach = P["damped MD approach"] if self.is_expr(approach): text += f"damp or quench the velocity based on '{approach}'." elif approach == "damping": text += "scale the previous velocity by a factor of " text += "{velocity scale factor} and the force by a factor of" text += "{force scale factor}" else: text += "scale the force by a factor of {velocity quenching factor}" text += " and zeroing the previous velocity unless it points in the " text += "same direction as the force." else: text += f"Warning: the optimization method '{method}' was not recognized." text += " The details of the model and electronic calculation are as follows." result += __(text, **P, indent=4 * " ").__str__() result += "\n\n" result += super().description_text(P) return result
[docs] def get_keywords(self, P=None): """Get the keywords and values for the calculation.""" # Get the values of the parameters, dereferencing any variables if P is None: P = self.parameters.current_values_to_dict( context=seamm.flowchart_variables._data ) # Get the keywords from the energy class keywords, descriptions = super().get_keywords(P=P) # The definition of the type of calculation: optimization keywords["ISIF"] = 3 if P["calculate stress"] else 0 keywords["NSW"] = P["number of steps"] ediffg = P["convergence cutoff"].m_as("eV/Å") keywords["EDIFFG"] = f"-{ediffg:.3f}" method = P["optimization method"] if "DIIS" in method: keywords["IBRION"] = 1 keywords["POTIM"] = P["step scale factor"] if P["iteration history length"] != "default": keywords["NFREE"] = P["iteration history length"] elif "MD" in method: keywords["IBRION"] = 3 if P["damped MD approach"] == "damping": keywords["SMASS"] = P["force scale factor"] keywords["POTIM"] = 2 * (1 - float(P["velocity scale factor"])) else: keywords["POTIM"] = -float(P["velocity quenching factor"]) else: # Conjugate gradients keywords["IBRION"] = 2 keywords["POTIM"] = P["step scale factor"] # Work out ISIF xyz = P["optimize atom positions"] shape = P["optimize cell shape"] volume = P["optimize cell volume"] if xyz: if shape: if volume: isif = 3 else: isif = 4 else: if volume: isif = 8 else: match P["calculate stress"]: case "no": isif = 0 case "only pressure": isif = 1 case _: isif = 2 else: if shape: if volume: isif = 6 else: isif = 5 else: if volume: isif = 7 else: raise RuntimeError("Not optimizing any degrees of freedom!") keywords["ISIF"] = isif # Check for default for NELMIN and set if needed if P["nelmin"] == "default": keywords["NELMIN"] = 6 return keywords, descriptions
[docs] def analyze( self, P=None, configuration=None, starting_configuration=None, indent="", text="", table=None, results={}, **kwargs, ): """Do any analysis of the output from this step. Also print important results to the local step.out file using "printer". Parameters ---------- indent: str An extra indentation for the output """ if table is None: table = { "Property": [], "Value": [], "Units": [], } # Extract the data we need from the output files. hdf5_file = self.wd / "vaspout.h5" xml_file = self.wd / "vasprun.xml" if hdf5_file.exists(): results = self.parse_hdf5(hdf5_file) elif xml_file.exists(): results = self.parse_xml(xml_file) else: results = {} text = f"Something is very wrong! Cannot find either {hdf5_file} or " text += f"{xml_file}. Did VASP fail?" printer.normal(textwrap.indent(text, self.indent + 4 * " ")) printer.normal("") return results table["Property"].append("Number of steps") table["Value"].append(results["nOptimizationSteps"]) table["Units"].append("") # Update the configuration with the final cell and fractionals if configuration is not None: configuration.cell.parameters = results["cell,iter"][-1] # Reorder the atoms back to SEAMM order tmp = results["fractionals,iter"][-1] configuration.atoms.coordinates = [tmp[i] for i in self.to_VASP_order] # Get the change of cell, density & volume for properties a0, b0, c0, alpha0, beta0, gamma0 = results["cell,iter"][0] V0 = results["V,iter"][0] a, b, c, alpha, beta, gamma = results["cell,iter"][-1] V = results["V,iter"][-1] results["delta a"] = a - a0 results["delta b"] = b - b0 results["delta c"] = c - c0 results["delta alpha"] = alpha - alpha0 results["delta beta"] = beta - beta0 results["delta gamma"] = gamma - gamma0 mass = starting_configuration.mass rho0 = (mass / V0) * (1.0e24 / 6.02214076e23) rho = (mass / V) * (1.0e24 / 6.02214076e23) results["V"] = V results["density"] = rho results["delta V"] = V - V0 results["delta density"] = rho - rho0 results["delta density %"] = (rho - rho0) / rho0 * 100 super().analyze( P=P, configuration=configuration, starting_configuration=starting_configuration, indent=indent, text=text, table=table, results=results, **kwargs, ) # Print the change in the cell a0, b0, c0, alpha0, beta0, gamma0 = results["cell,iter"][0] V0 = results["V,iter"][0] a, b, c, alpha, beta, gamma = results["cell,iter"][-1] V = results["V,iter"][-1] ctable = { "": ("𝗮", "𝗯", "𝗰", "𝞪", "𝞫", "𝞬", "V", "ρ", "%"), "Initial": ( f"{a0:.3f}", f"{b0:.3f}", f"{c0:.3f}", f"{alpha0:.1f}", f"{beta0:.1f}", f"{gamma0:.1f}", f"{V0:.1f}", f"{rho0:.3f}", "", ), "Final": ( f"{a:.3f}", f"{b:.3f}", f"{c:.3f}", f"{alpha:.1f}", f"{beta:.1f}", f"{gamma:.1f}", f"{V:.1f}", f"{rho:.3f}", "", ), "Change": ( f"{a - a0:.3f}", f"{b - b0:.3f}", f"{c - c0:.3f}", f"{alpha - alpha0:.1f}", f"{beta - beta0:.1f}", f"{gamma - gamma0:.1f}", f"{V - V0:.1f}", f"{rho - rho0:.3f}", f"{(rho - rho0) / rho0 * 100:.2f}%", ), "Units": ("Å", "Å", "Å", "°", "°", "°", \N{SUPERSCRIPT THREE}", "g/mL"), } tmp = tabulate( ctable, headers="keys", tablefmt="rounded_outline", colalign=("center", "decimal", "decimal", "decimal", "center"), disable_numparse=True, ) length = len(tmp.splitlines()[0]) text_lines = [] text_lines.append("Cell Parameters".center(length)) text_lines.append(tmp) printer.normal(textwrap.indent("\n".join(text_lines), self.indent + 7 * " ")) printer.normal("") # Print the change in the stress xx0, yy0, zz0, yz0, xz0, xy0 = results["stress,iter"][0] xx, yy, zz, yz, xz, xy = results["stress,iter"][-1] ctable = { "": ("xx", "yy", "zz", "yz", "xz", "xy"), "Initial": ( f"{xx0:.4f}", f"{yy0:.4f}", f"{zz0:.4f}", f"{yz0:.4f}", f"{xz0:.4f}", f"{xy0:.4f}", ), "Final": ( f"{xx:.4f}", f"{yy:.4f}", f"{zz:.4f}", f"{yz:.4f}", f"{xz:.4f}", f"{xy:.4f}", ), "Change": ( f"{xx - xx0:.4f}", f"{yy - yy0:.4f}", f"{zz - zz0:.4f}", f"{yz - yz0:.4f}", f"{xz - xz0:.4f}", f"{xy - xy0:.4f}", ), "Units": ("GPa",) * 6, } tmp = tabulate( ctable, headers="keys", tablefmt="rounded_outline", colalign=("center", "decimal", "decimal", "decimal", "center"), disable_numparse=True, ) length = len(tmp.splitlines()[0]) text_lines = [] text_lines.append("Stress".center(length)) text_lines.append(tmp) printer.normal(textwrap.indent("\n".join(text_lines), self.indent + 7 * " ")) printer.normal("")