# coding=utf-8
"""Model Therm Properties."""
try:
from itertools import izip as zip # python 2
except ImportError:
pass # python 3
from fairyfly.extensionutil import model_extension_dicts
from fairyfly.typing import invalid_dict_error
from fairyfly.checkdup import check_duplicate_identifiers
from ..material.dictutil import dict_to_material, dict_abridged_to_material, \
MATERIAL_TYPES
from ..material.gas import PureGas, Gas
from ..material.cavity import CavityMaterial
from ..condition.steadystate import SteadyState
[docs]
class ModelThermProperties(object):
"""Therm Properties for Fairyfly Model.
Args:
host: A fairyfly_core Model object that hosts these properties.
Properties:
* host
* materials
* conditions
* gases
"""
# dictionary mapping validation error codes to a corresponding check function
ERROR_MAP = {
'210001': 'check_duplicate_material_identifiers',
'210002': 'check_duplicate_condition_identifiers',
'210003': 'check_duplicate_gas_identifiers'
}
def __init__(self, host):
"""Initialize Model therm properties."""
self._host = host
@property
def host(self):
"""Get the Model object hosting these properties."""
return self._host
@property
def materials(self):
"""Get a list of all unique materials assigned to Shapes."""
materials = []
for shape in self.host.shapes:
mat = shape.properties.therm._material
if not self._instance_in_array(mat, materials):
materials.append(mat)
return list(set(materials))
@property
def conditions(self):
"""Get a list of all unique conditions contained within the model."""
conditions = []
for bnd in self.host.boundaries:
con = bnd.properties.therm._condition
if not self._instance_in_array(con, conditions):
conditions.append(con)
return list(set(conditions))
@property
def gases(self):
"""Get a list of all unique gases contained within the model."""
gases = []
for mat in self.materials:
if isinstance(mat, CavityMaterial):
gas = mat.gas
if not self._instance_in_array(gas, gases):
gases.append(gas)
return list(set(gases))
[docs]
def check_for_extension(self, raise_exception=True, detailed=False):
"""Check that the Model is valid for Therm simulation.
This process includes all relevant fairyfly-core checks as well as checks
that apply only for Therm.
Args:
raise_exception: Boolean to note whether a ValueError should be raised
if any errors are found. If False, this method will simply
return a text string with all errors that were found. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A text string with all errors that were found or a list if detailed is True.
This string (or list) will be empty if no errors were found.
"""
# set up defaults to ensure the method runs correctly
detailed = False if raise_exception else detailed
msgs = []
tol = self.host.tolerance
# perform checks for duplicate identifiers, which might mess with other checks
msgs.append(self.host.check_all_duplicate_identifiers(False, detailed))
# perform several checks for the Fairyfly schema geometry rules
msgs.append(self.host.check_planar(tol, False, detailed))
msgs.append(self.host.check_self_intersecting(tol, False, detailed))
# output a final report of errors or raise an exception
full_msgs = [msg for msg in msgs if msg]
if detailed:
return [m for msg in full_msgs for m in msg]
full_msg = '\n'.join(full_msgs)
if raise_exception and len(full_msgs) != 0:
raise ValueError(full_msg)
return full_msg
[docs]
def check_all(self, raise_exception=True, detailed=False):
"""Check all of the aspects of the Model therm properties.
Args:
raise_exception: Boolean to note whether a ValueError should be raised
if any errors are found. If False, this method will simply
return a text string with all errors that were found. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A text string with all errors that were found or a list if detailed is True.
This string (or list) will be empty if no errors were found.
"""
# set up defaults to ensure the method runs correctly
detailed = False if raise_exception else detailed
msgs = []
# perform checks for duplicate identifiers
msgs.append(self.check_all_duplicate_identifiers(False, detailed))
# output a final report of errors or raise an exception
full_msgs = [msg for msg in msgs if msg]
if detailed:
return [m for msg in full_msgs for m in msg]
full_msg = '\n'.join(full_msgs)
if raise_exception and len(full_msgs) != 0:
raise ValueError(full_msg)
return full_msg
[docs]
def check_all_duplicate_identifiers(self, raise_exception=True, detailed=False):
"""Check that there are no duplicate identifiers for any therm objects.
This includes Materials, Gases, and Conditions.
Args:
raise_exception: Boolean to note whether a ValueError should be raised
if any duplicate identifiers are found. If False, this method will simply
return a text string with all errors that were found. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A text string with all errors that were found or a list if detailed is True.
This string (or list) will be empty if no errors were found.
"""
# set up defaults to ensure the method runs correctly
detailed = False if raise_exception else detailed
msgs = []
# perform checks for duplicate identifiers
msgs.append(self.check_duplicate_material_identifiers(False, detailed))
msgs.append(self.check_duplicate_condition_identifiers(False, detailed))
msgs.append(self.check_duplicate_gas_identifiers(False, detailed))
# output a final report of errors or raise an exception
full_msgs = [msg for msg in msgs if msg]
if detailed:
return [m for msg in full_msgs for m in msg]
full_msg = '\n'.join(full_msgs)
if raise_exception and len(full_msgs) != 0:
raise ValueError(full_msg)
return full_msg
[docs]
def check_duplicate_material_identifiers(self, raise_exception=True, detailed=False):
"""Check that there are no duplicate Material identifiers in the model.
Args:
raise_exception: Boolean to note whether a ValueError should be raised
if duplicate identifiers are found. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
return check_duplicate_identifiers(
self.materials, raise_exception, 'Material',
detailed, '210001', 'Therm', error_type='Duplicate Material Identifier')
[docs]
def check_duplicate_condition_identifiers(
self, raise_exception=True, detailed=False):
"""Check that there are no duplicate Condition identifiers in the model.
Args:
raise_exception: Boolean to note whether a ValueError should be raised
if duplicate identifiers are found. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
return check_duplicate_identifiers(
self.conditions, raise_exception, 'Condition',
detailed, '210002', 'Therm', error_type='Duplicate Condition Identifier')
[docs]
def check_duplicate_gas_identifiers(
self, raise_exception=True, detailed=False):
"""Check that there are no duplicate Gas identifiers in the model.
Args:
raise_exception: Boolean to note whether a ValueError should be raised
if duplicate identifiers are found. (Default: True).
detailed: Boolean for whether the returned object is a detailed list of
dicts with error info or a string with a message. (Default: False).
Returns:
A string with the message or a list with a dictionary if detailed is True.
"""
return check_duplicate_identifiers(
self.gases, raise_exception, 'Gas',
detailed, '020003', 'Therm', error_type='Duplicate Gas Identifier')
[docs]
def apply_properties_from_dict(self, data):
"""Apply the therm properties of a dictionary to the host Model of this object.
Args:
data: A dictionary representation of an entire fairyfly-core Model.
Note that this dictionary must have ModelThermProperties in order
for this method to successfully apply the therm properties.
"""
assert 'therm' in data['properties'], \
'Dictionary possesses no ModelThermProperties.'
materials, conditions, _, _ = self.load_properties_from_dict(data)
# collect lists of therm property dictionaries
shape_t_dicts, boundary_t_dicts = model_extension_dicts(data, 'therm', [], [])
# apply therm properties to objects using the therm property dictionaries
for shape, s_dict in zip(self.host.shapes, shape_t_dicts):
if s_dict is not None:
shape.properties.therm.apply_properties_from_dict(
s_dict, materials)
for bound, b_dict in zip(self.host.boundaries, boundary_t_dicts):
if b_dict is not None:
bound.properties.therm.apply_properties_from_dict(b_dict, conditions)
[docs]
def to_dict(self):
"""Return Model therm properties as a dictionary."""
base = {'therm': {'type': 'ModelThermProperties'}}
# add the materials to the dictionary
all_mats = self.materials
gases, pure_gases = [], []
if len(all_mats) != 0:
base['therm']['materials'] = []
for mat in all_mats:
if isinstance(mat, CavityMaterial):
base['therm']['materials'].append(mat.to_dict(abridged=True))
gases.append(mat.gas)
for pg in mat.gas.pure_gases:
pure_gases.append(pg)
else: # SolidMaterial
base['therm']['materials'].append(mat.to_dict())
if len(gases) != 0:
base['therm']['gases'] = [g.to_dict(abridged=True) for g in set(gases)]
base['therm']['pure_gases'] = [pg.to_dict() for pg in set(pure_gases)]
# add the conditions to the dictionary
all_conds = self.conditions
if len(all_conds) != 0:
base['therm']['conditions'] = []
for con in all_conds:
base['therm']['conditions'].append(con.to_dict())
return base
[docs]
def duplicate(self, new_host=None):
"""Get a copy of this object.
Args:
new_host: A new Model object that hosts these properties.
If None, the properties will be duplicated with the same host.
"""
_host = new_host or self._host
return ModelThermProperties(_host)
[docs]
@staticmethod
def load_properties_from_dict(data, skip_invalid=False):
"""Load model therm properties of a dictionary to Python objects.
Loaded objects include Materials, Boundaries, Gases, and PureGases.
The function is called when re-serializing a Model object from a dictionary
to load fairyfly_therm objects into their Python object form before
applying them to the Model geometry.
Args:
data: A dictionary representation of an entire fairyfly-core Model.
Note that this dictionary must have ModelThermProperties in order
for this method to successfully load the therm properties.
skip_invalid: A boolean to note whether objects that cannot be loaded
should be ignored (True) or whether an exception should be raised
about the invalid object (False). (Default: False).
Returns:
A tuple with eight elements
- materials -- A dictionary with identifiers of materials as keys
and Python material objects as values.
- conditions -- A dictionary with identifiers of conditions
as keys and Python boundary objects as values.
- gases -- A dictionary with identifiers of gases as keys and
Python gas objects as values.
- pure_gases -- A dictionary with identifiers of pure gases
and Python PureGas objects as values.
"""
assert 'therm' in data['properties'], \
'Dictionary possesses no ModelThermProperties.'
# process all pure gases in the ModelThermProperties dictionary
pure_gases = {}
if 'pure_gases' in data['properties']['therm'] and \
data['properties']['therm']['pure_gases'] is not None:
for pg in data['properties']['therm']['pure_gases']:
try:
pure_gases[pg['identifier']] = PureGas.from_dict(pg)
except Exception as e:
if not skip_invalid:
invalid_dict_error(pg, e)
# process all gases in the ModelThermProperties dictionary
gases = {}
if 'gases' in data['properties']['therm'] and \
data['properties']['therm']['gases'] is not None:
for g in data['properties']['therm']['gases']:
try:
if g['type'] == 'Gas':
gases[g['identifier']] = Gas.from_dict(g)
else:
gases[g['identifier']] = Gas.from_dict_abridged(g, pure_gases)
except Exception as e:
if not skip_invalid:
invalid_dict_error(g, e)
# process all materials in the ModelThermProperties dictionary
materials = {}
if 'materials' in data['properties']['therm'] and \
data['properties']['therm']['materials'] is not None:
for mat in data['properties']['therm']['materials']:
try:
if mat['type'] in MATERIAL_TYPES:
materials[mat['identifier']] = dict_to_material(mat)
else:
materials[mat['identifier']] = \
dict_abridged_to_material(mat, gases)
except Exception as e:
if not skip_invalid:
invalid_dict_error(mat, e)
# process all conditions in the ModelThermProperties dictionary
conditions = {}
if 'conditions' in data['properties']['therm'] and \
data['properties']['therm']['conditions'] is not None:
for con in data['properties']['therm']['conditions']:
try:
conditions[con['identifier']] = \
SteadyState.from_dict(con)
except Exception as e:
if not skip_invalid:
invalid_dict_error(con, e)
return materials, conditions, gases, pure_gases
@staticmethod
def _instance_in_array(object_instance, object_array):
"""Check if a specific object instance is already in an array.
This can be much faster than `if object_instance in object_array`
when you expect to be testing a lot of the same instance of an object for
inclusion in an array since the builtin method uses an == operator to
test inclusion.
"""
for val in object_array:
if val is object_instance:
return True
return False
[docs]
def ToString(self):
return self.__repr__()
def __repr__(self):
return 'Model Therm Properties: [host: {}]'.format(self.host.display_name)