Source code for yamdb.yamdb

"""yamdb ... Yet Another Materials Database."""
from hashlib import file_digest
import importlib
import functools
import json
import os
from importlib import resources as ir
import sys
import yaml

module_dict = {
    'density': None,
    'dynamic_viscosity': None,
    'expansion_coefficient': None,
    'heat_capacity': None,
    'resistivity': None,
    'sound_velocity': None,
    'surface_tension': None,
    'thermal_conductivity': None,
    'vapour_pressure': None,
}

current_module = __import__(__name__)
# hint on importlib from
# https://www.devdungeon.com/content/import-python-module-string-name
# So 16. Jan 16:31:18 CET 2022
for key in module_dict:
    module_dict[key] = importlib.import_module('yamdb.properties.%s' % key)
    setattr(current_module, key,  module_dict[key])


[docs] class SubstanceDB: """Load a database file (YAML or JSON) and build a substance object. Parameters ---------- fname : str Filename of the database file. """ def __init__(self, fname): """Load a database file (YAML or JSON) and build a substance object. Parameters ---------- fname : str Filename of the database file. """ self._load_database(fname) def _load_yaml_database(self, fname): """Load a YAML database file. Parameters ---------- fname : str Filename of the YAML file. """ with open(fname, 'r') as fp: if hasattr(yaml, 'CSafeLoader'): self.materials = yaml.load(fp, Loader=yaml.CSafeLoader) else: self.materials = yaml.load(fp, Loader=yaml.SafeLoader) # see # https://github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation # for explanations of Loader argument; otherwise a warning is thrown def _load_json_database(self, fname): """Load a JSON database file. Parameters ---------- fname : str Filename of the JSON file. """ with open(fname, 'r') as fp: self.materials = json.load(fp) # TODO: try to determine file type in a better way # (not just guessing from the extension) # FIXME: it should be possible to use a yaml parser # that is capable of yaml 1.2 to parse JSON # check and simplify def _load_database(self, fname): """Load a database file (YAML or JSON, wrapper method). Parameters ---------- fname : str Filename of the database file. """ if fname.endswith('yml') or fname.endswith('yaml'): self._load_yaml_database(fname) elif fname.endswith('json'): self._load_json_database(fname) else: print("Cannot read file: %s" % fname, file=sys.stderr) sys.exit(1)
[docs] def get_substance(self, name): """Return a properties dictionary for the requested substance. Parameters ---------- name : str Substance name (elemental symbol or composition, e.g., 'Na' or 'CaCl2-NaCl'). Returns ------- dictionary or None Dictionary of properties available for the substance. """ if name in self.materials: return self.materials[name] else: return None
[docs] def has_substance(self, name): """Check if a substance is available from the SubstanceDB. Parameters ---------- name : str Substance name (elemental symbol or composition, i.e., 'Na' or 'CaCl2-NaCl'). Returns ------- bool True if substance is found, False otherwise. """ if name in self.materials: return True else: return False
[docs] def has_component(self, name): """Check if a component is available from the SubstanceDB. Parameters ---------- name : str Component name (e.g., 'CaCl2' that could be present as part of 'CaCl2-NaCl' or 'BaCl2-CaCl2'). Returns ------- str or None List of compositions that contain the requested component. """ clist = [] for key in self.materials: if name in key.split('-'): clist.append(key) if len(clist) > 0: return clist else: return None
[docs] def list_substances(self): """List substances contained in SubstanceDB. Returns ------- str List of substances. """ return list(self.materials.keys())
[docs] class Properties: """Generate a properties object from a substance dictionary. Parameters ---------- substance : dictionary Substance dictionary to build the properties object from. """ # Tm ... melting temperature, K # Tb ... boiling temperature, K # M ... molar mass, g/mol constant_properties = ['Tm', 'Tb', 'M'] def __init__(self, substance): """Generate a properties object from a substance dictionary. Parameters ---------- substance : dictionary Substance dictionary to build the properties object from. """ if substance is None: raise ValueError("No such substance") self._initialize_substance(substance) def _initialize_substance(self, substance): """Generate a properties object from a substance dictionary. Parameters ---------- substance : dictionary Substance dictionary to build the properties object from. """ self.substance = substance for key in self.constant_properties: if key in substance: setattr(self, key, substance[key]) for key in substance: if key not in self.constant_properties and key in module_dict.keys(): self._create_func_dict(key) self._add_direct_source(key) self._add_property(key) def _create_func_dict(self, property_): """Generate a methods dictionary for a certain property. Parameters ---------- property_ : str Property for that the methods dictionary is to be assembled, e.g. 'density'. """ dict_ = {} for key in self.substance[property_].keys(): ekey = self.substance[property_][key]['equation'] function = getattr(module_dict[property_], "%s" % ekey) coef = self.substance[property_][key].copy() for cp in self.constant_properties: # do not overwrite Tm if cp not in coef.keys(): coef[cp] = self.substance[cp] dict_[key] = functools.partial(function, coef=coef) # https://stackoverflow.com/questions/27362727/partial-function-application-with-the-original-docstring-in-python # docstring kopieren mit nächster oder übernächster Zeile, # sonst geht er verloren, wie auch mit einer lambda Funktion # dict_[key].__doc__ = function.__doc__ functools.update_wrapper(dict_[key], function) # dict_[key] = lambda Temp, *args, coef=coef: function(Temp, *args, coef=coef) setattr(self, '_%s_func_dict' % property_, dict_) def _add_direct_source(self, property_): """Generate direct access methods named after sources for property. Parameters ---------- property_ : str Property for that the direct access methods are to be generated, e.g. 'density'. """ func_dict = getattr(self, '_%s_func_dict' % property_) direct_access_property = DirectProperty(func_dict) setattr(self, 'direct_%s' % property_, direct_access_property) def _density(self, Temp, *args, source=None): """Return the density of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float density in kg/m3 """ if source is None: source = self.get_default_source('density') return self._density_func_dict[source](Temp, *args) def _dynamic_viscosity(self, Temp, *args, source=None): """Return the dynamic viscosity of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float dynamic viscosity in Pa s """ if source is None: source = self.get_default_source('dynamic_viscosity') return self._dynamic_viscosity_func_dict[source](Temp, *args) def _expansion_coefficient(self, Temp, *args, source=None): """Return the expansion coefficient of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float expansion coefficient in 1/K """ if source is None: source = self.get_default_source('expansion_coefficient') return self._expansion_coefficient_func_dict[source](Temp, *args) def _heat_capacity(self, Temp, *args, source=None): """Return the heat capacity of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float heat capacity in J/(kg K) """ if source is None: source = self.get_default_source('heat_capacity') return self._heat_capacity_func_dict[source](Temp, *args) def _resistivity(self, Temp, *args, source=None): """Return the resistivity of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float resistivity in Ohm m """ if source is None: source = self.get_default_source('resistivity') return self._resistivity_func_dict[source](Temp, *args) def _sound_velocity(self, Temp, *args, source=None): """Return the sound velocity of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float sound velocity in m/s """ if source is None: source = self.get_default_source('sound_velocity') return self._sound_velocity_func_dict[source](Temp, *args) def _surface_tension(self, Temp, *args, source=None): """Return the surface tension of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float surface tension in N/m """ if source is None: source = self.get_default_source('surface_tension') return self._surface_tension_func_dict[source](Temp, *args) def _thermal_conductivity(self, Temp, *args, source=None): """Return the thermal conductivity of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float thermal conductivity in W/(m K) """ if source is None: source = self.get_default_source('thermal_conductivity') return self._thermal_conductivity_func_dict[source](Temp, *args) def _vapour_pressure(self, Temp, *args, source=None): """Return the vapour pressure of the substance at the specified temperature. Parameters ---------- Temp : float temperature in K or numpy array of temperature values in K *args : float if applicable (e.g. for salt mixtures): concentration of the first constituent in mol% source : str reference/source for coefficient values Returns ------- float vapour pressure in Pa """ if source is None: source = self.get_default_source('vapour_pressure') return self._vapour_pressure_func_dict[source](Temp, *args) # FIXME: just a failed try to generate the _density ... methods # dynamically, only the source value existing at time of # instantiation is used, no dictionary of functions is generated # # def _create_func(self, property_): # if source == None: # source = self.get_default_source(property_) # function = getattr(self, "_%s_func_dict" % property_) # setattr(self, '%s' % property_, function[source]) def _add_property(self, property_): """Add property method to substance object. If data for this property are available for the requested substance. The latter is checked externally during __init__. Parameters ---------- property_ : str property to add """ function = getattr(self, "_%s" % property_) setattr(self, "%s" % property_, function)
[docs] def get_source_list(self, prop): """Return a list of all available sources for requested property. Parameters ---------- prop : str property to be investigated Returns ------- str list of available sources """ return [*self.substance[prop]]
[docs] def get_default_source(self, prop): """Return the default source for requested property. Parameters ---------- prop : str property to be investigated Returns ------- str default source, None if not defined """ default_source = None source_list = self.get_source_list(prop) for source in source_list: if 'default' in self.substance[prop][source].keys(): default_source = source return default_source
[docs] def get_property_list(self): """Return a list of all available properties. Returns ------- str list of available properties """ return [*self.substance.keys()]
[docs] def get_equation_limits(self, prop, source, variable='T'): """Return temperature limits of the equation. Parameters ---------- prop : str property to be investigated source : str source variable : str variable who's limits should be shown ('T' for temperature (default) or 'x' for fraction) Returns ------- min : float lower bound variable range (temperature by default) max : float upper bound variable range (temperature by default) """ if variable == 'T': if 'Tmin' in self.substance[prop][source].keys(): Tmin = self.substance[prop][source]['Tmin'] else: Tmin = None if 'Tmax' in self.substance[prop][source].keys(): Tmax = self.substance[prop][source]['Tmax'] else: Tmax = None return Tmin, Tmax elif variable == 'x': if 'xmin' in self.substance[prop][source].keys(): xmin = self.substance[prop][source]['xmin'] else: xmin = None if 'xmax' in self.substance[prop][source].keys(): xmax = self.substance[prop][source]['xmax'] else: xmax = None return xmin, xmax else: return None
[docs] def get_comment(self, prop, source): """Return commment if there is one. Parameters ---------- prop : str property to be investigated source : str source for the property equation Returns ------- str comment if available else None """ comment = None if source in self.substance[prop].keys(): if 'comment' in self.substance[prop][source].keys(): comment = self.substance[prop][source]['comment'] else: print("No such source: %s" % source, file=sys.stderr) return comment
[docs] def get_reference(self, prop, source): """Return reference if there is one, else source. Parameters ---------- prop : str property to be investigated source : str source for the property equation Returns ------- str reference if available else source """ if 'reference' in self.substance[prop][source].keys(): reference = self.substance[prop][source]['reference'] else: reference = source return reference
# class Propertyc: # def __init__(self, substance): # _add_direct_source(self, property_): # for src in self.get_source_list(property_): # func_dict = getattr(self, '_%s_func_dict' % property_) # function = getattr(self, '_%s' % property_) # setattr(function, '%s' % src, func_dict[src])
[docs] class DirectProperty: """Generate direct access methods for a property from all available sources. Parameters ---------- func_dict : dictionary Function dictionary containing property methods for all available sources. """ def __init__(self, func_dict): """Generate direct access methods for a property from all available sources. Parameters ---------- func_dict : dictionary Function dictionary containing property methods for all available sources. """ for src in func_dict.keys(): setattr(self, '%s' % src, func_dict[src])
[docs] class MixtureProperties: """Generate a mixture properties object from a substance dictionary. Parameters ---------- substance : dictionary Substance dictionary to build the mixture properties object from. """ def __init__(self, substance): """Generate a mixture properties object from a substance dictionary. Parameters ---------- substance : dictionary Substance dictionary to build the mixture properties object from. """ if substance is None: raise ValueError("No such substance") self._initialize_substance(substance) def _initialize_substance(self, substance): """Generate a mixture properties object from a substance dictionary. Parameters ---------- substance : dictionary Substance dictionary to build the mixture properties object from. """ self.substance = substance keys_all = substance.keys() functions = {} for key in keys_all: functions[key] = Properties(substance[key]) setattr(self, "composition", functions)
[docs] def get_compositions_with_property(self, property_, keep_range=False): """Return a list of mixture compositions. For that information on the requested property is available. Parameters ---------- property_ : str property for that compositions are search for keep_range : bool, default: False if composition 'range' should be kept in the output list Returns ------- str list of compositions that contain property information """ plist = [] for comp in self.composition.keys(): if property_ in self.composition[comp].get_property_list(): plist.append(comp) if len(plist) > 0: plist.sort(key=extract_concentration) else: plist = None if plist and 'range' in plist and not keep_range: plist.pop() return plist
[docs] def get_compositions_with_property_source(self, property_, source, keep_range=False): """Return a list of mixture compositions. For that information on the requested property from the requested source is available. Parameters ---------- property_ : str property for that compositions are search for source : str reference/source for coefficient values keep_range : bool, default: False if composition 'range' should be kept in the output list Returns ------- str list of compositions that contain property information """ plist = [] for comp in self.composition.keys(): if property_ in self.composition[comp].get_property_list() and \ source in self.composition[comp].get_source_list(property_): plist.append(comp) if len(plist) > 0: plist.sort(key=extract_concentration) else: plist = None if 'range' in plist and not keep_range: plist.pop() return plist
_db_cache = {}
[docs] def extract_concentration(key): """Sort keys according to first concentration value (helper function). Parameters ---------- key : str Composition in mol%. Returns ------- float Fraction of first component in mol% or "999" if key is "range". """ if key.find('-') > 0: key = float(key.split('-')[0]) else: # key == "range" -> end of list key = 999 return key
[docs] def get_file_digest(fname, algorithm='sha1'): """Get file digest of file. Parameters ---------- fname : str Name of the file whose hash should be determined. algorithm : str, optional Hash algorithm to be used (the default is 'sha1'). Returns ------- str Hash as hexadecimal string. See Also -------- hashlib.file_digest """ with open(fname, 'rb') as fp: digest = file_digest(fp, algorithm).hexdigest() return digest
[docs] def get_properties_from_db(db_file, substance): """Get properties object from a specific database file. Parameters ---------- db_file : str Filename of the database file. substance : str Substance name (elemental symbol or composition, e.g., 'Na' or 'CaCl2-NaCl'). Returns ------- Properties or MixtureProperties Properties or MixtureProperties object. """ hash = get_file_digest(db_file) if hash in _db_cache.keys(): db = _db_cache[hash] else: db = SubstanceDB(db_file) _db_cache[hash] = db if substance.find('-') < 0: property_object = Properties(db.get_substance(substance)) else: property_object = MixtureProperties(db.get_substance(substance)) return property_object
def _get_properties_from_included_db(db_file, substance): """Get properties object from a database file included in yamdb/data. Parameters ---------- db_file : str Filename of the database file. substance : str Substance name (elemental symbol or composition, e.g., 'Na' or 'CaCl2-NaCl'). Returns ------- Properties or MixtureProperties Properties or MixtureProperties object. """ fname = os.path.join(ir.files('yamdb'), 'data', db_file) return get_properties_from_db(fname, substance)
[docs] def get_from_metals(substance): """Get properties object from the metals.yml file included in yamdb/data. Parameters ---------- substance : str Substance name (elemental symbol or composition, e.g., 'Na' or 'Ga'). Returns ------- Properties or MixtureProperties Properties or MixtureProperties object. """ return _get_properties_from_included_db('metals.yml', substance)
[docs] def get_from_salts(substance): """Get properties object from the salts.yml file included in yamdb/data. Parameters ---------- substance : str Substance name (elemental symbol or composition, e.g., 'NaCl' or 'CaCl2-NaCl'). Returns ------- Properties or MixtureProperties Properties or MixtureProperties object. """ return _get_properties_from_included_db('salts.yml', substance)
[docs] def get_from_Janz1992(substance): """Get properties object from the Janz1992.yml file included in yamdb/data. Parameters ---------- substance : str Substance name (elemental symbol or composition, e.g., 'NaCl' or 'CaCl2-NaCl'). Returns ------- Properties or MixtureProperties Properties or MixtureProperties object. """ return _get_properties_from_included_db('Janz1992_ed.yml', substance)
[docs] def load_yaml_references(db_file): """Load a YAML references file. Parameters ---------- db_file : str Filename of the YAML references database file, e.g. 'references.yml'. """ with open(db_file, 'r') as fp: if hasattr(yaml, 'CSafeLoader'): references = yaml.load(fp, Loader=yaml.CSafeLoader) else: references = yaml.load(fp, Loader=yaml.SafeLoader) return references
[docs] def get_references_from_db(db_file, key): """Get reference for key from db_file. Parameters ---------- db_file : str Name of the reference database YAML file, e.g. 'references.yml' key : str Citation key of the reference, e.g. 'IidaGuthrie1988'. Returns ------- str Reference corresponding to the supplied citation key. """ hash = get_file_digest(db_file) if hash in _db_cache.keys(): db = _db_cache[hash] else: db = load_yaml_references(db_file) _db_cache[hash] = db if key in db.keys(): entry = db[key] else: entry = None return entry
def _get_references_from_included_db(db_file, key): """Get reference for key from db_file supplied in yamdb/data. Parameters ---------- db_file : str Name of the reference database YAML file, typically 'references.yml' key : str Citation key of the reference, e.g. 'IidaGuthrie1988'. Returns ------- str Reference corresponding to the supplied citation key. """ fname = os.path.join(ir.files('yamdb'), 'data', db_file) return get_references_from_db(fname, key)
[docs] def get_from_references(key): """Get reference for key from the references.yml file included in yamdb/data. Parameters ---------- key : str Citation key of the reference, e.g. 'IidaGuthrie1988'. Returns ------- str Reference corresponding to the supplied citation key. """ return _get_references_from_included_db('references.yml', key)