Source code for thermomechanical_step.thermomechanical

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

"""Non-graphical part of the Thermomechanical step in a SEAMM flowchart"""

from datetime import datetime
import importlib
import json
import logging
from math import exp, log, log10, atan
import pprint  # noqa: F401
import shlex
import sys
import textwrap
import traceback

import numpy as np
from tabulate import tabulate

import thermomechanical_step
import molsystem
import seamm
from seamm_util import getParser, parse_list, Q_, units_class  # noqa: F401
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("Thermomechanical")

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


[docs] def round_value(value, digits=3): """Round a value to the given number of digits accuracy""" decimals = int(log10(abs(value))) if decimals < 0: decimals = -decimals + digits else: decimals = digits - 1 - decimals if decimals < 0: decimals = 0 return round(value, decimals)
[docs] class Thermomechanical(seamm.Node): """ The non-graphical part of a Thermomechanical 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 : ThermomechanicalParameters The control parameters for Thermomechanical. See Also -------- TkThermomechanical, Thermomechanical, ThermomechanicalParameters """ def __init__( self, flowchart=None, title="Thermomechanical", namespace="org.molssi.seamm", extension=None, logger=logger, ): """A step for Thermomechanical in a SEAMM flowchart. 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. namespace : str The namespace for the plug-ins of the subflowchart extension: None Not yet implemented logger : Logger = logger The logger to use and pass to parent classes Returns ------- None """ logger.debug(f"Creating Thermomechanical {self}") self.subflowchart = seamm.Flowchart( parent=self, name="Thermomechanical", namespace=namespace ) # yapf: disable super().__init__( flowchart=flowchart, title="Thermomechanical", extension=extension, module=__name__, logger=logger, ) # yapf: disable self._metadata = thermomechanical_step.metadata self.parameters = thermomechanical_step.ThermomechanicalParameters() self._data = {} @property def version(self): """The semantic version of this module.""" return thermomechanical_step.__version__ @property def git_revision(self): """The git version of this module.""" return thermomechanical_step.__git_revision__
[docs] def analyze(self, indent="", **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 """ # Get the first real node node = self.subflowchart.get_node("1").next() # Loop over the subnodes, asking them to do their analysis while node is not None: for value in node.description: printer.important(value) printer.important(" ") node.analyze() node = node.next()
def _calculate_elastic_constants(self, _P): """The driver for calculating elastic constants. Parameters ---------- _P : dict(str, any) The control parameters for this step Returns ------- None """ data = {} # Results to be stored if chosen by user _, configuration = self.get_system_configuration() # Save the cell and coordinates so that we can recreate the structure cell0 = configuration.cell.parameters fractionals0 = configuration.coordinates # First run the unstrained system results = {} results[0] = self._run_subflowchart(name="unstrained") configuration.cell.parameters = cell0 configuration.coordinates = fractionals0 # The stress may be a 6-vector or the six elements if "stress" in results[0]: units = results[0]["stress,units"] if units == "GPa": factor = 1 data["stress"] = results[0]["stress"] else: factor = Q_(1.0, units).m_as("GPa") data["stress"] = [v * factor for v in results[0]["stress"]] else: if "Sxx,units" in results[0]: units = results[0]["Sxx,units"] factor = Q_(1.0, units).m_as("GPa") else: factor = 1 # Save the stress data["stress"] = [ results[0][key] * factor for key in ("Sxx", "Syy", "Szz", "Syz", "Sxz", "Sxy") ] # And the strains, + & - step = _P["step size"] for indx, strain in enumerate(("xx", "yy", "zz", "yz", "xz", "xy")): strn = [0.0] * 6 if indx > 2: # Voigt off-diagonals have factor of 2 strn[indx] = -2 * step else: strn[indx] = -step configuration.strain(strn) results[(indx, "-")] = self._run_subflowchart(name=f"e{strain} -{step}") configuration.cell.parameters = cell0 configuration.coordinates = fractionals0 if indx > 2: # Voigt off-diagonals have factor of 2 strn[indx] = 2 * step else: strn[indx] = step configuration.strain(strn) results[(indx, "+")] = self._run_subflowchart(name=f"e{strain} +{step}") configuration.cell.parameters = cell0 configuration.coordinates = fractionals0 # Create the elastic constant matrix, converting to GPa on the way C = [] for i in range(6): plus = results[(i, "+")] minus = results[(i, "-")] row = [] if "stress" in plus: row = [ factor * (p - m) / (2 * step) for p, m in zip(plus["stress"], minus["stress"]) ] else: for j, strain in enumerate(("Sxx", "Syy", "Szz", "Syz", "Sxz", "Sxy")): Cij = factor * (plus[strain] - minus[strain]) / (2 * step) row.append(Cij) C.append(row) # Print the unsymmetrized matrix table = {} table[""] = ["xx", "yy", "zz", "yz", "xz", "xy"] for row, strain in zip(C, ["xx", "yy", "zz", "yz", "xz", "xy"]): table[strain] = [*row] tmp = tabulate( table, headers="keys", tablefmt="rounded_outline", floatfmt=".2f", ) length = len(tmp.splitlines()[0]) text_lines = [] header = "Unsymmetrized elastic constant matrix (GPa)" text_lines.append(header.center(length)) text_lines.append(tmp) text = textwrap.indent("\n".join(text_lines), self.indent + 8 * " ") printer.normal(text) printer.normal("") # Symmetrize the matrix for i in range(6): for j in range(i): Cij = C[i][j] Cji = C[j][i] C[j][i] = (Cij + Cji) / 2 C[i][j] = Cij - Cji # Print the symmetrized matrix table = {} table[""] = ["xx", "yy", "zz", "yz", "xz", "xy"] for row, strain in zip(C, ["xx", "yy", "zz", "yz", "xz", "xy"]): table[strain] = [*row] tmp = tabulate( table, headers="keys", tablefmt="rounded_outline", floatfmt=".1f", ) length = len(tmp.splitlines()[0]) text_lines = [] header = "Elastic constant matrix (lower) and error (upper) (GPa)" text_lines.append(header.center(length)) text_lines.append(tmp) text = textwrap.indent("\n".join(text_lines), self.indent + 8 * " ") printer.normal(text) printer.normal("") # Make full square matrix for i in range(6): for j in range(i): C[i][j] = C[j][i] data["Cij"] = C # Invert to get compliance tmp = np.array(C) S = np.linalg.inv(tmp).tolist() data["Sij"] = S # Print the compliance matrix table = {} table[""] = ["xx", "yy", "zz", "yz", "xz", "xy"] for row, strain in zip(S, ["xx", "yy", "zz", "yz", "xz", "xy"]): table[strain] = [1000 * v for v in row] tmp = tabulate( table, headers="keys", tablefmt="rounded_outline", floatfmt=".1f", ) length = len(tmp.splitlines()[0]) text_lines = [] header = "Compliance matrix (1/TPa)" text_lines.append(header.center(length)) text_lines.append(tmp) text = textwrap.indent("\n".join(text_lines), self.indent + 8 * " ") printer.normal(text) printer.normal("") # The polycrystalline moduli # Voigt Kv = ((C[0][0] + C[1][1] + C[2][2]) + 2 * (C[0][1] + C[1][2] + C[0][2])) / 9 Gv = ( (C[0][0] + C[1][1] + C[2][2]) - (C[0][1] + C[1][2] + C[0][2]) + 3 * (C[3][3] + C[4][4] + C[5][5]) ) / 15 Ev = 9 * Kv * Gv / (3 * Kv + Gv) mu_v = (3 * Kv - 2 * Gv) / (6 * Kv + 2 * Gv) data["Kv"] = Kv data["Gv"] = Gv data["Ev"] = Ev data["mu_v"] = mu_v # Reuss Kr = 1 / ((S[0][0] + S[1][1] + S[2][2]) + 2 * (S[0][1] + S[1][2] + S[0][2])) Gr = 15 / ( 4 * (S[0][0] + S[1][1] + S[2][2]) - 4 * (S[0][1] + S[1][2] + S[0][2]) + 3 * (S[3][3] + S[4][4] + S[5][5]) ) Er = 9 * Kr * Gr / (3 * Kr + Gr) mu_r = (3 * Kr - 2 * Gr) / (6 * Kr + 2 * Gr) data["Kr"] = Kr data["Gr"] = Gr data["Er"] = Er data["mu_r"] = mu_r # Hill Kh = (Kv + Kr) / 2 Gh = (Gv + Gr) / 2 Eh = 9 * Kh * Gh / (3 * Kh + Gh) mu_h = (3 * Kh - 2 * Gh) / (6 * Kh + 2 * Gh) data["Kh"] = Kh data["Gh"] = Gh data["Eh"] = Eh data["mu_h"] = mu_h # And print as a table table = { "Modulus": ["Bulk (K)", "Shear (G)", "Young (E)", "Poisson ratio"], "Voigt": [f"{Kv:.1f}", f"{Gv:.1f}", f"{Ev:.1f}", f"{mu_v:.3f}"], "Reuss": [f"{Kr:.1f}", f"{Gr:.1f}", f"{Er:.1f}", f"{mu_r:.3f}"], "Hill": [f"{Kh:.1f}", f"{Gh:.1f}", f"{Eh:.1f}", f"{mu_h:.3f}"], } tmp = tabulate( table, headers="keys", tablefmt="rounded_outline", ) length = len(tmp.splitlines()[0]) text_lines = [] header = "Polycrystalline Moduli (GPa) and Poisson Ratio" text_lines.append(header.center(length)) text_lines.append(tmp) text = textwrap.indent("\n".join(text_lines), self.indent + 8 * " ") printer.normal(text) printer.normal("") # Gruneisen parameter gamma = 3 / 2 * ((1 + mu_h) / (2 - 3 * mu_h)) # Pugh's ratio k_pugh = Gh / Kh data["k_pugh"] = k_pugh # Cauchy pressure Pcauchy = C[1][2] - C[4][4] data["Pcauchy"] = Pcauchy # Vickers hardness Hv_chen = 2 * (k_pugh**2 / Gh) ** 0.585 - 3 Hv_tian = 0.92 * k_pugh**1.137 * Gh**0.708 data["Hv_chen"] = Hv_chen data["Hv_tian"] = Hv_tian # Sound velocity and Debye temperature. Let pint take care of units Kh = Q_(Kh, "GPa") Gh = Q_(Gh, "GPa") Eh = Q_(Eh, "GPa") rho = Q_(configuration.density, "g/ml") vl = ((3 * Kh + 4 * Gh) / (3 * rho)) ** 0.5 vt = (Gh / rho) ** 0.5 vl.ito("m/s") vt.ito("m/s") vm = (((2 / vt**3) + 1 / vl**3) / 3) ** (-1 / 3) h = Q_(1, "planck_constant") kb = Q_(1, "boltzmann_constant") pi = Q_(1, "pi") Na = Q_(1, "avogadro_constant") R = (kb * Na).m_as("J/mol/K") n_atoms = configuration.n_atoms mass = Q_(configuration.mass, "g/mol") Td = h / kb * (3 * n_atoms / (4 * pi) * Na * rho / mass) ** (1 / 3) * vm Td.ito("K") # N is the number of atoms in the empirical formula unit formula, empirical_formula, Z = configuration.formula N = n_atoms / Z Ezp = 9 / 8 * N * R * Td Ezp.ito("kJ/mol") data["vl"] = vl.magnitude data["vt"] = vt.magnitude data["vm"] = vm.magnitude data["Td"] = Td.magnitude data["Ezp"] = Ezp.magnitude data["Gruneisen parameter"] = gamma # And print as a table table = { "Property": [ "Pugh's ductility criterion (k) <0.57 ductile", 'Cauchy pressure (C") >0 ductile', "Vickers hardness (Hv) [Chen]", "Vickers hardness (Hv) [Tian]", "Transverse sound velocity (vt)", "Longitudinal sound velocity (vl)", "Average sound velocity (vm)", "Debye temperature (Td)", "Zero-point energy", "Gruneisen parameter", ], "Value": [ f"{k_pugh:.2f}", f"{Pcauchy:.2f}", f"{Hv_chen:.1f}", f"{Hv_tian:.1f}", f"{vt.magnitude:.0f}", f"{vl.magnitude:.0f}", f"{vm.magnitude:.0f}", f"{Td.magnitude:.1f}", f"{Ezp.magnitude:.1f}", f"{gamma:.2f}", ], "Units": ["", "", "", "", "m/s", "m/s", "m/s", "K", "kJ/mol", ""], } tmp = tabulate( table, headers="keys", tablefmt="rounded_outline", ) length = len(tmp.splitlines()[0]) text_lines = [] header = "Other Properties" text_lines.append(header.center(length)) text_lines.append(tmp) text = textwrap.indent("\n".join(text_lines), self.indent + 8 * " ") printer.normal(text) printer.normal("") text = "The zero-point energy above and thermodynamic functions below " text += f"correspond to the empirical formula {empirical_formula}." if Z > 1: text += f", of which there are {Z} units in the cell." printer.normal(__(text, indent=self.indent + 4 * " ")) printer.normal("") # Evaluate the termochemistry using the Debye model Ts = parse_list(_P["thermochemistry Ts"]) results = self.debye_model(Td.magnitude, Ts, N=N) # And get the approximate linear coefficient of thermal expansion V = Q_(configuration.volume / Z, "Å^3") # Volume per formula unit a_factor = Q_(1, "J/mol/K") * gamma / (Kh * V * Na) / 3 # 3 for volume > linear a_factor = a_factor.m_as("1/K") * 1.0e6 alpha = [round_value(v * a_factor) for v in results["Cv"]] table = { "T": Ts, "Cv (J/mol*K)": results["Cv"], "U - U₀ (kJ/mol)": results["U"], "S (J/mol/K)": results["S"], "A - U₀ (kJ/mol)": results["A"], "alpha (10^-6/K)": alpha, } tmp = tabulate( table, headers="keys", tablefmt="rounded_outline", disable_numparse=False, ) length = len(tmp.splitlines()[0]) text_lines = [] header = "Thermodynamic functions" text_lines.append(header.center(length)) text_lines.append(tmp) # And save the data as dictionaries... data["Cv"] = {T: v for T, v in zip(Ts, results["Cv"])} data["U - U0"] = {T: v for T, v in zip(Ts, results["U"])} data["S"] = {T: v for T, v in zip(Ts, S)} data["A - U0"] = {T: v for T, v in zip(Ts, results["A"])} data["alpha"] = {T: v for T, v in zip(Ts, alpha)} text = textwrap.indent("\n".join(text_lines), self.indent + 8 * " ") printer.normal(text) printer.normal("") # Put any requested results into variables or tables self.store_results(configuration=configuration, data=data) # And graph the thermodynamic functions figure = self.create_figure( module_path=(self.__module__.split(".")[0], "seamm"), template="line.graph_template", fontsize=self.options["graph_fontsize"], title=f"Thermodynamic functions for {empirical_formula}", ) plot = figure.add_plot("thermodynamics") x_axis = plot.add_axis("x", label="T (K)") y_axis = plot.add_axis( "y", anchor=x_axis, label="Cv, S (J/mol/K)", rangemode="tozero", ) x_axis.anchor = y_axis plot.add_trace( x_axis=x_axis, y_axis=y_axis, name="Cv", x=Ts, xlabel="T", xunits="K", y=results["Cv"], ylabel="Cv", yunits="J/mol/K", color="#4dbd74", width=2, ) plot.add_trace( x_axis=x_axis, y_axis=y_axis, name="S", x=Ts, xlabel="T", xunits="K", y=results["S"], ylabel="S", yunits="J/mol/K", color="red", width=2, ) y2_axis = plot.add_axis( "y", anchor=x_axis, gridcolor="pink", label="U - U₀, -(A - U₀) (kJ/mol)", overlaying="y", rangemode="tozero", side="right", tickmode="sync", ) plot.add_trace( x_axis=x_axis, y_axis=y2_axis, name="U - U₀", x=Ts, xlabel="T", xunits="K", y=results["U"], ylabel="U - U₀", yunits="kJ/mol", color="black", width=2, ) plot.add_trace( x_axis=x_axis, y_axis=y2_axis, name="-(A - U₀)", x=Ts, xlabel="T", xunits="K", y=[-v for v in results["A"]], ylabel="-(A - U₀)", yunits="kJ/mol", color="blue", width=2, ) figure.grid_plots("thermodynamics") path = self.wd / "ThermodynamicFunctions.graph" figure.write_file(path) # Other requested formats if "graph_formats" in self.options: formats = self.options["graph_formats"] # If from seamm.ini, is a single string so parse. if isinstance(formats, str): formats = shlex.split(formats) for _format in formats: figure.write_file( path.with_suffix("." + _format), width=int(self.options["graph_width"]), height=int(self.options["graph_height"]), ) # And the linear coefficient of thermal expansion figure = self.create_figure( module_path=(self.__module__.split(".")[0], "seamm"), template="line.graph_template", fontsize=self.options["graph_fontsize"], title=f"Linear thermal expansion for {empirical_formula}", ) plot = figure.add_plot("alpha") x_axis = plot.add_axis("x", label="T (K)") y_axis = plot.add_axis( "y", anchor=x_axis, label="1/K (x 10⁻⁶)", rangemode="tozero", ) x_axis.anchor = y_axis plot.add_trace( x_axis=x_axis, y_axis=y_axis, name="Cv", x=Ts, xlabel="T", xunits="K", y=alpha, ylabel="𝛼", yunits="1/K (x 10⁻⁶)", hovertemplate="%{x} K, %{y} x 10⁻⁶/K}", color="black", width=2, ) figure.grid_plots("alpha") path = self.wd / "ThermalExpansion.graph" figure.write_file(path) # Other requested formats if "graph_formats" in self.options: formats = self.options["graph_formats"] # If from seamm.ini, is a single string so parse. if isinstance(formats, str): formats = shlex.split(formats) for _format in formats: figure.write_file( path.with_suffix("." + _format), width=int(self.options["graph_width"]), height=int(self.options["graph_height"]), )
[docs] def create_parser(self): """Setup the command-line / config file parser""" parser_name = "thermomechanical-step" parser = getParser() # Remember if the parser exists ... this type of step may have been # found before parser_exists = parser.exists(parser_name) # Create the standard options, e.g. log-level super().create_parser(name=parser_name) if not parser_exists: # Any options for thermomechanical step itself parser.add_argument( parser_name, "--graph-formats", default=tuple(), choices=("html", "png", "jpeg", "webp", "svg", "pdf"), nargs="+", help="extra formats to write for graphs", ) parser.add_argument( parser_name, "--graph-fontsize", default=15, help="Font size in graphs, defaults to 15 pixels", ) parser.add_argument( parser_name, "--graph-width", default=1024, help="Width of graphs in formats that support it, defaults to 1024", ) parser.add_argument( parser_name, "--graph-height", default=1024, help="Height of graphs in formats that support it, defaults to 1024", ) # Now need to walk through the steps in the subflowchart... self.subflowchart.reset_visited() node = self.subflowchart.get_node("1").next() while node is not None: node = node.create_parser() return self.next()
[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. """ # Make sure the subflowchart has the data from the parent flowchart self.subflowchart.root_directory = self.flowchart.root_directory self.subflowchart.executor = self.flowchart.executor self.subflowchart.in_jobserver = self.subflowchart.in_jobserver if P is None: P = self.parameters.values_to_dict() # Describe what we are going to do result = self.header + "\n\n" text = "" elastic = P["elastic constants"] step = P["step size"] if isinstance(elastic, bool) and elastic or elastic == "yes": text += ( "The elastic constants will be calculated using a strain step of " f"{step}. " ) elif self.is_expr(elastic): text += ( f"The expression {elastic} will determine whether to calculate the " f"elastic constants. If so, a strain step of {step} will be used. " ) spdef = P["state point definition"] if self.is_expr(spdef): text += ( "The expression {spdef} will determine what state points will be used. " ) elif spdef == "as given": text += ( "The current structure will be used as is, with the temperature set " "in the subflowchart if needed." ) elif spdef == "lists of Ps and Ts": text += ( "The state points will be generated by using each pressure {P} for " "each temperature {T}." ) else: text += "The state points given by {state points} will be used." result += str(__(text, indent=4 * " ", **P)) result += "\n\n" # Get the first real node node = self.subflowchart.get_node("1").next() text = "" while node is not None: try: text += __(node.description_text(), indent=8 * " ").__str__() except Exception as e: print(f"Error describing thermomechanical flowchart: {e} in {node}") logger.critical( f"Error describing thermomechanical flowchart: {e} in {node}" ) raise except Exception: print( "Unexpected error describing thermomechanical flowchart: " f"{sys.exc_info()[0]} in {str(node)}" ) logger.critical( "Unexpected error describing thermomechanical flowchart: " f"{sys.exc_info()[0]} in {str(node)}" ) raise text += "\n" node = node.next() result += text return result
[docs] def run(self): """Run the Thermomechanical step. Parameters ---------- None Returns ------- seamm.Node The next node object in the flowchart. """ next_node = super().run(printer) # Get the values of the parameters, dereferencing any variables _P = self.parameters.current_values_to_dict( context=seamm.flowchart_variables._data ) # Fix the formatting of units for printing... _PP = dict(_P) for key in _PP: if isinstance(_PP[key], units_class): _PP[key] = "{:~P}".format(_PP[key]) # Print what we are doing printer.important(__(self.description_text(_PP), indent=self.indent)) if _P["elastic constants"]: self._calculate_elastic_constants(_P) return next_node
def _run_subflowchart(self, name=None): """Run the subflowchart for training. Parameters ---------- name : str, default = None The name of the run, used to create the subdirectory and name in the output. The default of None indicates use the current directory as is. Returns ------- results : {str: any} A dictionary of the results from the subflowchart """ super().run(printer) # Make sure the subflowchart has the data from the parent flowchart self.subflowchart.root_directory = self.flowchart.root_directory self.subflowchart.executor = self.flowchart.executor self.subflowchart.in_jobserver = self.subflowchart.in_jobserver job_handler = None out_handler = None if name is None: iter_dir = self.wd iter_dir.mkdir(parents=True, exist_ok=True) # Ensure the nodes have the correct id self.set_subids(self._id) else: iter_dir = self.wd / name iter_dir.mkdir(parents=True, exist_ok=True) # Find the handler for job.out and set the level up for handler in job.handlers: if ( isinstance(handler, logging.FileHandler) and "job.out" in handler.baseFilename ): job_handler = handler job_level = job_handler.level job_handler.setLevel(printing.JOB) elif isinstance(handler, logging.StreamHandler): out_handler = handler out_level = out_handler.level out_handler.setLevel(printing.JOB) # Setup the output for this pass path = iter_dir / "Step.out" path.unlink(missing_ok=True) file_handler = logging.FileHandler(path) file_handler.setLevel(printing.NORMAL) formatter = logging.Formatter(fmt="{message:s}", style="{") file_handler.setFormatter(formatter) job.addHandler(file_handler) # Ensure the nodes have the correct id self.set_subids((*self._id, name)) # Get the first real node in the subflowchart first_node = self.subflowchart.get_node("1").next() # Set up the options for the subflowchart node = first_node self.subflowchart.reset_visited() while node is not None: node.all_options = self.all_options node = node.next() # Run through the steps in the subflowchart node = first_node try: while node is not None: try: node = node.run() except DeprecationWarning as e: printer.normal("\nDeprecation warning: " + str(e)) traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stdout) except Exception as e: printer.job(f"Caught exception in subflowchart: {str(e)}") with open(self.wd / "stderr.out", "a") as fd: traceback.print_exc(file=fd) raise finally: if job_handler is not None: job_handler.setLevel(job_level) if out_handler is not None: out_handler.setLevel(out_level) # Remove any redirection of printing. if file_handler is not None: file_handler.close() job.removeHandler(file_handler) file_handler = None # Get the results paths = sorted(iter_dir.glob("**/Results.json")) if len(paths) == 0: if name is None: raise RuntimeError( "There are no properties stored in properties.json " f"for this step, running in {iter_dir}." ) else: raise RuntimeError( "There are no properties stored in properties.json " f"for step {name} running in {iter_dir}." ) data = {} for path in paths: with path.open() as fd: tmp = json.load(fd) time = datetime.fromisoformat(tmp["iso time"]) data[time] = tmp times = sorted(data.keys()) results = data[times[0]] # Add other citations here or in the appropriate place in the code. # Add the bibtex to data/references.bib, and add a self.reference.cite # similar to the above to actually add the citation to the references. return results
[docs] def set_id(self, node_id=()): """Sequentially number the subnodes""" self.logger.debug("Setting ids for subflowchart {}".format(self)) if self.visited: return None else: self.visited = True self._id = node_id self.set_subids(self._id) return self.next()
[docs] def set_subids(self, node_id=()): """Set the ids of the nodes in the subflowchart""" self.subflowchart.reset_visited() node = self.subflowchart.get_node("1").next() n = 1 while node is not None: node = node.set_id((*node_id, str(n))) n += 1
[docs] def debye_model(self, Td, Ts, N=1): """Compute the thermodynamic functions from the Debye model""" kb = Q_(1, "boltzmann_constant") Na = Q_(1, "avogadro_constant") R = (kb * Na).m_as("J/mol/K") pi = Q_(1, "pi").to_base_units().magnitude Tmin = min(Ts) Tmax = max(Ts) # Bootstrap the integral in one degree increments from Tmax + 2 to Tmin - 1 or 0 U_sum = 0 Cv_sum = 0 U1 = {} Cv1 = {} n_steps = 100 # Number of points in each integral over 1 degree x = 0 xmin = 0 for T in range(Tmax + 2, Tmin - 1 if Tmin - 1 >= 0 else 0, -1): xmax = Td / T if x == 0: # First chunk from 0 to xmax needs a finer grid because it is long step = (xmax - xmin) / n_steps / 10000 else: step = (xmax - xmin) / n_steps while x <= xmax: if x == 0: U_sum = 0 Cv_sum = 0 else: xp = x + step / 2 xm = x - step / 2 try: U_sum += ( (xm**3 / (exp(xm) - 1) + xp**3 / (exp(xp) - 1)) / 2 * step ) except OverflowError: U_sum = pi**4 / 15 try: Cv_sum += ( ( xm**4 * (exp(xm) / (exp(xm) - 1)) / (exp(xm) - 1) + xm**4 * (exp(xp) / (exp(xp) - 1)) / (exp(xp) - 1) ) / 2 * step ) except OverflowError: Cv_sum = 4 * pi**4 / 15 x += step xmin = xmax prefactor = 9 * N * R * (T / Td) ** 3 U1[T] = prefactor * T * U_sum / 1000 # kJ/mol, not J/mol Cv1[T] = prefactor * Cv_sum # Entropy = integral Cv/T 0..T S1 = {} S_sum = Cv1[T + 1] / (T + 1) / 2 S1[1] = S_sum for T in range(2, Tmax + 2): if T <= 10: # Use asymptotic formula S_sum = Cv1[T] / 3 else: S_sum += (Cv1[T - 1] / (T - 1) + Cv1[T + 1] / (T + 1)) / 2 S1[T] = S_sum # Now find the desired values U = [] Cv = [] S = [] Ehelmholtz = [] for T in Ts: Tm = int(T) Tp = Tm + 1 value = U1[Tm] + (U1[Tp] - U1[Tm]) * (T - Tm) U.append(round_value(value)) value = Cv1[Tm] + (Cv1[Tp] - Cv1[Tm]) * (T - Tm) Cv.append(round_value(value)) value = S1[Tm] + (S1[Tp] - S1[Tm]) * (T - Tm) S.append(round_value(value)) value = U[-1] - T * S[-1] / 1000 # kJ/mol, not J/mol Ehelmholtz.append(round_value(value)) # The fit equation # # William W. Anderson; An analytic expression approximating the Debye heat # capacity function. AIP Advances 1 July 2019; 9 (7): 075108. # https://doi.org/10.1063/1.5110279 # A = [0.61833, 0.89246] B = [0.18112, -0.18189, 4.4259e-3] C = [0.14816, 0.0978, 0.117461] ni = [2, 2, 3] Cv_fit = [] U_fit = [] S_fit = [] A_fit = [] for T in Ts: x = T / Td try: eAx = [exp(A[0] / x), exp(A[1] / x)] Cx2 = [C[0] ** 2 + x**2, C[1] ** 2 + x**2, C[2] ** 2 + x**2] # Cv tmp1 = 0 for i in range(2): tmp1 += (A[i] / x) ** 2 * eAx[i] / (eAx[i] - 1) / (eAx[i] - 1) tmp1 /= 2 tmp2 = 0 for i in range(3): tmp2 += B[i] / Cx2[i] ** ni[i] tmp2 *= x**3 value = 3 * N * R * (tmp1 + tmp2) Cv_fit.append(round_value(value)) # U tmp = 0 for i in range(2): tmp += A[i] / (eAx[i] - 1) + B[i] * ( log(1 + x**2 / C[i] ** 2) - x**2 / Cx2[i] ) tmp /= 2 * x tmp += B[2] / (4 * C[2] ** 2) * x**3 / Cx2[2] ** 2 U_fit.append(3 * N * R * T * tmp / 1000) # kJ/mol, not J/mol # S tmp = 0 for i in range(2): tmp += ( A[i] / x * eAx[i] / (eAx[i] - 1) - log(eAx[i] - 1) + B[i] * (atan(x / C[i]) / C[i] - x / (C[i] ** 2 + x**2)) ) / 2 tmp += ( B[2] / (8 * C[2] ** 2) * ( (x**3 - C[2] ** 2 * x) / (C[2] ** 2 + x**2) ** 2 + atan(x / C[2]) / C[2] ) ) value = 3 * N * R * tmp S_fit.append(round_value(value)) value = U_fit[-1] - T * S_fit[-1] / 1000 # kJ/mol, not J/mol A_fit.append(round_value(value)) except OverflowError: value = 12 * pi**4 * N * R * x**3 / 5 Cv_fit.append(round_value(value)) value = 3 * pi**4 * N * R * T * x**3 / 5 / 1000 # kJ U_fit.append(round_value(value)) value = Cv_fit[-1] / 3 S_fit.append(round_value(value)) value = U_fit[-1] - T * S_fit[-1] / 1000 # kJ/mol, not J/mol A_fit.append(round_value(value)) # What is the maximum error of fit vs integral? if False: max_error = 0 for T, v0, v in zip(Ts, Cv, Cv_fit): tmp = (v - v0) / v0 if abs(tmp) > abs(max_error): max_error = tmp print(f"Maximum relative error in Cv fit is {max_error}") max_error = 0 for v0, v in zip(U, U_fit): tmp = (v - v0) / v0 if abs(tmp) > abs(max_error): max_error = tmp print(f"Maximum relative error in U fit is {max_error}") max_error = 0 for T, v0, v in zip(Ts, S, S_fit): tmp = (v - v0) / v0 if abs(tmp) > 0.1: print(f"\t{T} {v0} {v}") if abs(tmp) > abs(max_error): max_error = tmp print(f"Maximum relative error in S fit is {max_error}") return { "Cv": Cv, "U": U, "S": S, "A": Ehelmholtz, "Cv fit": Cv_fit, "U fit": U_fit, "S fit": S_fit, "A fit": A_fit, }