Source code for pyiron_contrib.atomistics.atomicrex.fit_properties

import xml.etree.ElementTree as ET

import numpy as np

from pyiron_base import DataContainer, FlattenedStorage


[docs]class ARFitProperty(DataContainer): """ Class to describe properties that can be fitted using atomicrex. Property and target value have to be given, the other parameters control details of the fitting procedure. For more information what they do visit the atomicrex documentation. Direct interaction with this class shouldn't be necessary. To conveniently create ARFitProperty objects use the ARFitPropertList add_fit_property method. """ def __init__( self, prop=None, target_value=None, fit=None, relax=None, relative_weight=None, residual_style=None, output=None, tolerance=None, min_val=None, max_val=None, output_all=None, *args, **kwargs, ): super().__init__(table_name="fit_property", *args, **kwargs) self._prop = None self._residual_style = None if prop is not None: self.prop = prop if residual_style is not None: self.residual_style = residual_style self.target_value = target_value self.fit = fit self.relax = relax self.relative_weight = relative_weight self.tolerance = tolerance self.min_val = min_val self.max_val = max_val self.final_value = None self.output_all = output_all @property def prop(self): return self._prop @prop.setter def prop(self, prop): """Only allow properties that can be fitted using atomicrex Args: prop (string): name of the property Raises: ValueError: Given property can not be fitted using atomicrex. """ fittable_properties = [ "atomic-energy", "atomic-forces", "bulk-modulus", "pressure", ] # cij_list = [f"c{i}{j}" for i, j in range(1,7) if j>=i] # fittable_properties.extend(cij_list) if prop in fittable_properties: self._prop = prop else: raise ValueError(f"prop should be one of {fittable_properties}") @property def residual_style(self): return self._residual_style @residual_style.setter def residual_style(self, residual_style): """Residual styles available in atomicrex Args: residual_style (string): [description] Raises: ValueError: [description] """ res_styles = ["squared", "squared-relative", "absolute-diff"] if residual_style in res_styles: self._residual_style = residual_style else: raise ValueError(f"residual style has to be one of {res_styles}") def _is_scalar(self): """ Internal helper function, scalar properties are provided in the structure xml and vector properties (atomic forces) in the structure file Returns: [bool]: True if scalar, False if vector property. """ if self.prop != "atomic-forces": return True else: return False
[docs] def to_xml_element(self): xml = ET.Element(f"{self.prop}") if self.fit: xml.set("fit", "true") else: xml.set("fit", "false") # xml.set("relax", f"{self.relax}".lower()) xml.set("relative-weight", f"{self.relative_weight}") if self.tolerance is not None: xml.set("tolerance", f"{self.tolerance}") if self._is_scalar(): xml.set("target", f"{self.target_value}") if self.min_val is not None: xml.set("min", f"{self.min_val}") if self.max_val is not None: xml.set("min", f"{self.max_val}") xml.set("residual-style", f"{self.residual_style}") else: if self.residual_style == "squared-relative": raise ValueError( "Squared-relative residual style is not implemented for forces in atomicrex" ) if self.min_val is not None or self.max_val is not None: raise ValueError( "Min and Max val can only be given for scalar properties" ) if self.output_all: xml.set("output-all", "true") xml.set("residual-style", f"{self.residual_style}") return xml
@staticmethod def _parse_final_value(line): """ Parses the final values of properties used in the fitting process from atomicrex output. Args: line (string): string from atomicrex output containing final value of some property Returns: [(string, float)]: property, final value """ if line.startswith("atomic-forces avg/max"): return "atomic-forces", None else: line = line.split() return line[0].rstrip(":"), float(line[1])
[docs]class ARFitPropertyList(DataContainer): """ DataContainer of ARFitProperties that additionally provides utility functions that allow convenient addition of fit properties to a structure. Also provides internal functionality. """ def __init__(self, *args, **kwargs): super().__init__(table_name="fit_property", *args, **kwargs)
[docs] def add_FitProperty( self, prop, target_value, fit=True, relax=False, relative_weight=1, residual_style="squared", output=True, tolerance=None, min_val=None, max_val=None, output_all=True, ): """ Adds a fittable property to the fit properties of an atomicrex structure. Default values should be ok for most cases, but it is strongly recommended to check the purpose of each argument in the atomicrex documentation. Args: prop (string): property to be fitted target_value (float): fit (bool, optional): Include the property in the fit procedure. Defaults to True. relax (bool, optional): Calculate before or after structure relaxation. Defaults to False. relative_weight (int, optional): Weight for the objective function. Defaults to 1. residual_style (str, optional): See atomicrex documentation. Defaults to "squared-relative". output (bool, optional): Determines if the value is written to output. Could cause parsing problems if False. Defaults to True. tolerance (float, optional): See atomicrex documentation. Defaults to None. min_val (float, optional): Only scalar properties, if relaxation enabled. Defaults to None. max_val (float, optional): Only scalar properties, if relaxation enabled. Defaults to None. output_all (bool, optional): Only vector properties. Determines if full vector is written to output. Defaults to True. """ self[prop] = ARFitProperty( prop=prop, target_value=target_value, fit=fit, relax=relax, relative_weight=relative_weight, residual_style=residual_style, output=output, tolerance=tolerance, min_val=min_val, max_val=max_val, output_all=output_all, )
[docs] def to_xml_element(self): """Internal helper function converting the list into an atomicrex xml element.""" properties = ET.Element("properties") for p in self.values(): properties.append(p.to_xml_element)
Residual_Styles = ("squared", "squared-relative", "absolute-diff")
[docs]class FlattenedARProperty(FlattenedStorage): """ Class to read and write scalar properties of a structure, f.e. the energy. """ def __init__(self, num_chunks=1, num_elements=1, **kwargs): super().__init__(num_chunks=num_chunks, num_elements=num_elements, **kwargs) self._per_chunk_arrays = {} self.add_array("fit", dtype=bool, per="chunk", fill=False) self.add_array("relative_weight", per="chunk", fill=1.0) self.add_array("relax", dtype=bool, per="chunk") self.add_array("residual_style", per="chunk", dtype=np.ubyte, fill=0) self.add_array("output", dtype=bool, per="chunk", fill=False) self.add_array("tolerance", per="chunk", fill=np.nan) @property def fit(self): return self._per_chunk_arrays["fit"] @property def relative_weight(self): return self._per_chunk_arrays["relative_weight"] @property def residual_style(self): return self._per_chunk_arrays["residual_style"] @property def tolerance(self): return self._per_chunk_arrays["tolerance"]
[docs] def from_hdf(self, hdf, group_name): try: super().from_hdf(hdf, group_name=group_name) except: with hdf.open(group_name) as h: self._per_chunk_arrays["target_val"] = h["target_value"] self._per_chunk_arrays["fit"] = h["fit"] self._per_chunk_arrays["relative_weight"] = h["relative_weight"] self._per_chunk_arrays["residual_style"] = h["residual_style"] self._per_chunk_arrays["relax"] = h["relax"] self._per_chunk_arrays["tolerance"] = h["tolerance"] self._per_chunk_arrays["output"] = h["output"] self._per_chunk_arrays["final_val"] = h["final_value"]
[docs]class FlattenedARScalarProperty(FlattenedARProperty): def __init__(self, num_chunks=1, num_elements=1, **kwargs): super().__init__(num_chunks=num_chunks, num_elements=num_elements, **kwargs) self.add_array("target_val", per="chunk", fill=np.nan) self.add_array("final_val", per="chunk", fill=np.nan) @property def target_val(self): return self._per_chunk_arrays["target_val"] @property def final_val(self): return self._per_chunk_arrays["final_val"]
[docs] def to_xml_element(self, index, prop): xml = ET.Element(prop) if self._per_chunk_arrays["output"][index]: xml.set("output", "true") if self._per_chunk_arrays["fit"][index]: xml.set("fit", "true") xml.set("target", f"{self._per_chunk_arrays['target_val'][index]}") # xml.set("relax", f"{self.relax}".lower()) xml.set( "relative-weight", f"{self._per_chunk_arrays['relative_weight'][index]}" ) xml.set( "residual-style", f"{Residual_Styles[self._per_chunk_arrays['residual_style'][index]]}", ) if not np.isnan(self._per_chunk_arrays["tolerance"][index]): xml.set("tolerance", f"{self._per_chunk_arrays['tolerance'][index]}") if prop in ["lattice-parameter", "ca-ratio"]: if not np.isnan(self._per_chunk_arrays["min_val"][index]): xml.set("min", f"{self._per_chunk_arrays['min_val'][index]}") if not np.isnan(self._per_chunk_arrays["max_val"][index]): xml.set("max", f"{self._per_chunk_arrays['max_val'][index]}") return xml
[docs]class FlattenedARVectorProperty(FlattenedARProperty): """ Like AR property, but for vector properties, i.e. forces """ def __init__(self, num_chunks=1, num_elements=1, **kwargs): super().__init__(num_chunks=num_chunks, num_elements=num_elements, **kwargs) self.add_array("target_val", shape=(3,), per="element", fill=np.nan) self.add_array("final_val", shape=(3,), per="element", fill=np.nan) @property def target_val(self): return self._per_element_arrays["target_val"] @property def final_val(self): return self._per_element_arrays["final_val"]
[docs] def to_xml_element(self, index, prop): xml = ET.Element(prop) if self._per_chunk_arrays["output"][index]: xml.set("output-all", "true") if self._per_chunk_arrays["fit"][index]: xml.set("output-all", "true") xml.set("fit", "true") xml.set( "relative-weight", f"{self._per_chunk_arrays['relative_weight'][index]}" ) xml.set( "residual-style", f"{Residual_Styles[self._per_chunk_arrays['residual_style'][index]]}", ) if not np.isnan(self._per_chunk_arrays["tolerance"][index]): xml.set("tolerance", f"{self._per_chunk_arrays['tolerance'][index]}") return xml