"""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_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 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_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)