# Copyright (c) 2012, 2020, 2022 Santosh Philip
# Copyright (c) 2016 Jamie Bull
# Copyright (c) 2020 Cheng Cui
# =======================================================================
# Distributed under the MIT License.
# (See accompanying file LICENSE or copy at
# http://opensource.org/licenses/MIT)
# =======================================================================
"""Sub class Bunch to represent an IDF object.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import copy
import itertools
from munch import Munch as Bunch
from eppy.bunchhelpers import matchfieldnames, scientificnotation, makefieldname
import eppy.function_helpers as fh
import eppy.ext_field_functions as extff
[docs]class BadEPFieldError(AttributeError):
"""An Exception"""
pass
[docs]class RangeError(ValueError):
"""An Exception"""
pass
[docs]def almostequal(first, second, places=7, printit=True):
"""
Test if two values are equal to a given number of places.
This is based on python's unittest so may be covered by Python's
license.
"""
if first == second:
return True
if round(abs(second - first), places) != 0:
if printit:
print(round(abs(second - first), places))
print("notalmost: %s != %s to %i places" % (first, second, places))
return False
else:
return True
[docs]def somevalues(ddtt):
"""returns some values"""
return ddtt.Name, ddtt.Construction_Name, ddtt.obj
[docs]def extendlist(lst, i, value=""):
"""extend the list so that you have i-th value"""
if i < len(lst):
pass
else:
lst.extend([value] * (i - len(lst) + 1))
[docs]def return42(self, *args, **kwargs):
# proof of concept - to be removed
return 42
[docs]def addfunctions(abunch):
"""add functions to epbunch"""
key = abunch.obj[0].upper()
# -----------------
# TODO : alternate strategy to avoid listing the objkeys in snames
# check if epbunch has field "Zone_Name" or "Building_Surface_Name"
# and is in group u'Thermal Zones and Surfaces'
# then it is likely to be a surface.
# of course we need to recode for surfaces that do not have coordinates :-(
# or we can filter those out since they do not have
# the field "Number_of_Vertices"
snames = [
"BuildingSurface:Detailed",
"Wall:Detailed",
"RoofCeiling:Detailed",
"Floor:Detailed",
"FenestrationSurface:Detailed",
"Shading:Site:Detailed",
"Shading:Building:Detailed",
"Shading:Zone:Detailed",
]
snames = [sname.upper() for sname in snames]
if key in snames:
func_dict = {
"area": fh.area,
"height": fh.height, # not working correctly
"width": fh.width, # not working correctly
"azimuth": fh.azimuth,
"true_azimuth": fh.true_azimuth,
"tilt": fh.tilt,
"coords": fh.getcoords, # needed for debugging
}
abunch.__functions.update(func_dict)
# -----------------
# print(abunch.getfieldidd )
names = [
"CONSTRUCTION",
"MATERIAL",
"MATERIAL:AIRGAP",
"MATERIAL:INFRAREDTRANSPARENT",
"MATERIAL:NOMASS",
"MATERIAL:ROOFVEGETATION",
"WINDOWMATERIAL:BLIND",
"WINDOWMATERIAL:GLAZING",
"WINDOWMATERIAL:GLAZING:REFRACTIONEXTINCTIONMETHOD",
"WINDOWMATERIAL:GAP",
"WINDOWMATERIAL:GAS",
"WINDOWMATERIAL:GASMIXTURE",
"WINDOWMATERIAL:GLAZINGGROUP:THERMOCHROMIC",
"WINDOWMATERIAL:SCREEN",
"WINDOWMATERIAL:SHADE",
"WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM",
]
if key in names:
func_dict = {
"rvalue": fh.rvalue,
"ufactor": fh.ufactor,
"rvalue_ip": fh.rvalue_ip, # quick fix for Santosh. Needs to thought thru
"ufactor_ip": fh.ufactor_ip, # quick fix for Santosh. Needs to thought thru
"heatcapacity": fh.heatcapacity,
}
abunch.__functions.update(func_dict)
names = [
"FAN:CONSTANTVOLUME",
"FAN:VARIABLEVOLUME",
"FAN:ONOFF",
"FAN:ZONEEXHAUST",
"FANPERFORMANCE:NIGHTVENTILATION",
]
if key in names:
func_dict = {
"f_fanpower_bhp": fh.fanpower_bhp,
"f_fanpower_watts": fh.fanpower_watts,
"f_fan_maxcfm": fh.fan_maxcfm,
}
abunch.__functions.update(func_dict)
# =====
# code for references
# -----------------
# add function zonesurfaces
if key == "ZONE":
func_dict = {"zonesurfaces": fh.zonesurfaces}
abunch.__functions.update(func_dict)
# -----------------
# add function subsurfaces
# going to cheat here a bit
# check if epbunch has field "Zone_Name"
# and is in group u'Thermal Zones and Surfaces'
# then it is likely to be a surface attached to a zone
fields = abunch.fieldnames
try:
group = abunch.getfieldidd("key")["group"]
except KeyError as e: # some pytests don't have group
group = None
if group == "Thermal Zones and Surfaces":
if "Zone_Name" in fields:
func_dict = {"subsurfaces": fh.subsurfaces}
abunch.__functions.update(func_dict)
return abunch
[docs]class EpBunch(Bunch):
"""
Fields, values, and descriptions of fields in an EnergyPlus IDF object
stored in a `bunch` which is a `dict` extended to allow access to dict
fields as attributes as well as by keys.
"""
def __init__(self, obj, objls, objidd, *args, **kwargs):
super(EpBunch, self).__init__(*args, **kwargs)
self.obj = obj # field names
self.objls = objls # field values
self.objidd = objidd # field metadata (minimum, maximum, type, etc.)
self.theidf = None # pointer to the idf this epbunch belongs to
# This is None if there is no idf - a standalone epbunch
# This will be set by Idf_MSequence
self["__functions"] = {} # initialize the functions
addfunctions(self)
@property
def fieldnames(self):
"""Friendly name for objls."""
return self.objls
@property
def fieldvalues(self):
"""Friendly name for obj."""
return self.obj
[docs] def checkrange(self, fieldname):
"""Check if the value for a field is within the allowed range."""
return checkrange(self, fieldname)
[docs] def getrange(self, fieldname):
"""Get the allowed range of values for a field."""
return getrange(self, fieldname)
[docs] def getfieldidd(self, fieldname):
"""get the idd dict for this field
Will return {} if the fieldname does not exist"""
return getfieldidd(self, fieldname)
[docs] def getfieldidd_item(self, fieldname, iddkey):
"""return an item from the fieldidd, given the iddkey
will return and empty list if it does not have the iddkey
or if the fieldname does not exist"""
return getfieldidd_item(self, fieldname, iddkey)
[docs] def get_retaincase(self, fieldname):
"""check if the field should retain case"""
return get_retaincase(self, fieldname)
[docs] def isequal(self, fieldname, value, places=7):
"""return True if the field == value
Will retain case if get_retaincase == True
for real value will compare to decimal 'places'
"""
return isequal(self, fieldname, value, places=places)
[docs] def getreferingobjs(self, iddgroups=None, fields=None):
"""Get a list of objects that refer to this object"""
return getreferingobjs(self, iddgroups=iddgroups, fields=fields)
[docs] def get_referenced_object(self, fieldname):
"""
Get an object referred to by a field in another object.
For example an object of type Construction has fields for each layer, each
of which refers to a Material. This functions allows the object
representing a Material to be fetched using the name of the layer.
Returns the first item found since if there is more than one matching item,
it is a malformed IDF.
Parameters
----------
referring_object : EpBunch
The object which contains a reference to another object,
fieldname : str
The name of the field in the referring object which contains the
reference to another object.
Returns
-------
EpBunch
"""
return get_referenced_object(self, fieldname)
def __setattr__(self, name, value):
try:
origname = self["__functions"][name]
# TODO: unit test never hits here so what is it for?
self[origname] = value
except KeyError:
pass
try:
name = self["__aliases"][name] # get original name of the alias
except KeyError:
pass
if name in ("__functions", "__aliases"): # just set the new value
self[name] = value
return None
elif name in ("obj", "objls", "objidd", "theidf"): # let Bunch handle it
super(EpBunch, self).__setattr__(name, value)
return None
elif name in self.fieldnames: # set the value, extending if needed
i = self.fieldnames.index(name)
try:
self.fieldvalues[i] = value
except IndexError:
extendlist(self.fieldvalues, i)
self.fieldvalues[i] = value
# elif name_is_extensible(name):
# # do one field at a time - mto start with
# extendinIDD
# do the previous elif
elif extff.getextensible(self.objidd): # idfobject has extensible fields
if extff.islegalextensiblefield(
self.objidd, name
): # is the field a legal extensible field
# What is the integer on that field
name_int = extff.extfieldint(name)
# get the int in the last extensible field
last_extfield = self.objidd[-1]["field"][0]
last_extfield_int = extff.extfieldint(last_extfield, sep=" ")
# calculate the number of new field sets to be generated
newextensibles = name_int - last_extfield_int
# generate the new fileds in eppy's IDD
key_i = self.theidf.model.dtls.index(self.key)
mult = extff.getextensible(self.objidd)
extff.increaseIDDfields(
self.theidf.block,
self.theidf.idd_info,
key_i,
self.key,
newextensibles * mult,
)
# need to update objls and objidd here
self.objidd = self.theidf.idd_info[key_i]
objfields = [comm.get("field") for comm in self.objidd]
objfields[0] = ["key"]
objfields = [field[0] for field in objfields]
obj_fields = [makefieldname(field) for field in objfields]
self.objls = obj_fields
if name in self.fieldnames: # set the value, extending if needed
i = self.fieldnames.index(name)
try:
self.fieldvalues[i] = value
except IndexError:
extendlist(self.fieldvalues, i)
self.fieldvalues[i] = value
else:
pass
else:
astr = "unable to find field %s" % (name,)
raise BadEPFieldError(astr) # TODO: could raise AttributeError
else:
astr = "unable to find field %s" % (name,)
raise BadEPFieldError(astr) # TODO: could raise AttributeError
def __getattr__(self, name):
try:
func = self["__functions"][name]
return func(self)
except KeyError:
pass
try:
name = self["__aliases"][name]
except KeyError:
pass
if name == "__functions":
return self["__functions"]
elif name in ("__aliases", "obj", "objls", "objidd", "theidf"):
# unit test
return super(EpBunch, self).__getattr__(name)
elif name in self.fieldnames:
i = self.fieldnames.index(name)
try:
return self.fieldvalues[i]
except IndexError:
return ""
elif extff.getextensible(self.objidd): # idfobject has extensible fields
if extff.islegalextensiblefield(
self.objidd, name
): # is the field a legal extensible field
# no point creating a field
return ""
else:
astr = "unable to find field %s" % (name,)
raise BadEPFieldError(astr) # TODO: could raise AttributeError
astr = "unable to find field %s" % (name,)
raise BadEPFieldError(astr)
else:
astr = "unable to find field %s" % (name,)
raise BadEPFieldError(astr)
def __getitem__(self, key):
if key in ("obj", "objls", "objidd", "__functions", "__aliases", "theidf"):
return super(EpBunch, self).__getitem__(key)
elif key in self.fieldnames:
i = self.fieldnames.index(key)
try:
return self.fieldvalues[i]
except IndexError:
return ""
elif extff.getextensible(self.objidd): # idfobject has extensible fields
if extff.islegalextensiblefield(
self.objidd, key
): # is the field a legal extensible field
# no point creating a field
return ""
else:
astr = "unable to find field %s" % (key,)
raise BadEPFieldError(astr)
else:
# TODO: Do similar strategy as in __getattr__
astr = "unknown field %s" % (key,)
raise BadEPFieldError(astr)
def __setitem__(self, key, value):
if key in ("obj", "objls", "objidd", "__functions", "__aliases", "theidf"):
super(EpBunch, self).__setitem__(key, value)
return None
elif key in self.fieldnames:
i = self.fieldnames.index(key)
try:
self.fieldvalues[i] = value
except IndexError:
extendlist(self.fieldvalues, i)
self.fieldvalues[i] = value
elif extff.getextensible(self.objidd): # idfobject has extensible fields
if extff.islegalextensiblefield(
self.objidd, key
): # is the field a legal extensible field
# What is the integer on that field
name_int = extff.extfieldint(key)
# get the int in the last extensible field
last_extfield = self.objidd[-1]["field"][0]
last_extfield_int = extff.extfieldint(last_extfield, sep=" ")
# calculate the number of new fields to be generated
newextensibles = name_int - last_extfield_int
# generate the new fileds in eppy's IDD
key_i = self.theidf.model.dtls.index(self.key)
mult = extff.getextensible(self.objidd)
extff.increaseIDDfields(
self.theidf.block,
self.theidf.idd_info,
key_i,
self.key,
newextensibles * mult,
)
# need to update objls and objidd here
self.objidd = self.theidf.idd_info[key_i]
objfields = [comm.get("field") for comm in self.objidd]
objfields[0] = ["key"]
objfields = [field[0] for field in objfields]
obj_fields = [makefieldname(field) for field in objfields]
self.objls = obj_fields
if key in self.fieldnames: # set the value, extending if needed
i = self.fieldnames.index(key)
try:
self.fieldvalues[i] = value
except IndexError:
extendlist(self.fieldvalues, i)
self.fieldvalues[i] = value
else:
pass
else:
astr = "unknown field %s" % (key,)
raise BadEPFieldError(astr)
else:
astr = "unknown field %s" % (key,)
raise BadEPFieldError(astr)
def __repr__(self):
"""print this as an idf snippet"""
# lines = [str(val) for val in self.obj]
# replace the above line with code that will print an integer without decimals
lines = []
for val in self.obj:
try:
value = int(val)
if value != val:
value = val
except ValueError as e:
value = val
lines.append(value)
# ------------
comments = [comm.replace("_", " ") for comm in self.objls]
lines[0] = "%s," % (lines[0],) # comma after first line
for i, line in enumerate(lines[1:-1]):
line = scientificnotation(
line, width=18
) # E+ cannot read wide numbers, convert to 1e+3
lines[i + 1] = " %s," % (line,) # indent and comma
lines[-1] = " %s;" % (lines[-1],) # ';' after last line
lines = lines[:1] + [line.ljust(26) for line in lines[1:]] # ljsut the lines
filler = "%s !- %s"
nlines = [
filler % (line, comm) for line, comm in zip(lines[1:], comments[1:])
] # adds comments to line
nlines.insert(0, lines[0]) # first line without comment
astr = "\n".join(nlines)
return "\n%s\n" % (astr,)
def __str__(self):
"""same as __repr__"""
# needed if YAML is installed. See issue 67
# unit test
return self.__repr__()
def __dir__(self):
fnames = self.fieldnames
func_names = list(self["__functions"].keys())
return super(EpBunch, self).__dir__() + fnames + func_names
[docs]def getrange(bch, fieldname):
"""get the ranges for this field"""
keys = ["maximum", "minimum", "maximum<", "minimum>", "type"]
index = bch.objls.index(fieldname)
fielddct_orig = bch.objidd[index]
fielddct = copy.deepcopy(fielddct_orig)
therange = {}
for key in keys:
therange[key] = fielddct.setdefault(key, None)
if therange["type"]:
therange["type"] = therange["type"][0]
if therange["type"] == "real":
for key in keys[:-1]:
if therange[key]:
therange[key] = float(therange[key][0])
if therange["type"] == "integer":
for key in keys[:-1]:
if therange[key]:
therange[key] = int(therange[key][0])
return therange
[docs]def checkrange(bch, fieldname):
"""throw exception if the out of range"""
fieldvalue = bch[fieldname]
therange = bch.getrange(fieldname)
if therange["maximum"] != None:
if fieldvalue > therange["maximum"]:
astr = "Value %s is not less or equal to the 'maximum' of %s"
astr = astr % (fieldvalue, therange["maximum"])
raise RangeError(astr)
if therange["minimum"] != None:
if fieldvalue < therange["minimum"]:
astr = "Value %s is not greater or equal to the 'minimum' of %s"
astr = astr % (fieldvalue, therange["minimum"])
raise RangeError(astr)
if therange["maximum<"] != None:
if fieldvalue >= therange["maximum<"]:
astr = "Value %s is not less than the 'maximum<' of %s"
astr = astr % (fieldvalue, therange["maximum<"])
raise RangeError(astr)
if therange["minimum>"] != None:
if fieldvalue <= therange["minimum>"]:
astr = "Value %s is not greater than the 'minimum>' of %s"
astr = astr % (fieldvalue, therange["minimum>"])
raise RangeError(astr)
return fieldvalue
"""get the idd dict for this field
Will return {} if the fieldname does not exist"""
[docs]def getfieldidd(bch, fieldname):
"""get the idd dict for this field
Will return {} if the fieldname does not exist"""
# print(bch)
try:
fieldindex = bch.objls.index(fieldname)
except ValueError as e:
return {} # the fieldname does not exist
# so there is no idd
fieldidd = bch.objidd[fieldindex]
return fieldidd
[docs]def getfieldidd_item(bch, fieldname, iddkey):
"""return an item from the fieldidd, given the iddkey
will return and empty list if it does not have the iddkey
or if the fieldname does not exist"""
fieldidd = getfieldidd(bch, fieldname)
try:
return fieldidd[iddkey]
except KeyError as e:
return []
[docs]def get_retaincase(bch, fieldname):
"""Check if the field should retain case"""
fieldidd = bch.getfieldidd(fieldname)
return "retaincase" in fieldidd
[docs]def isequal(bch, fieldname, value, places=7):
"""return True if the field is equal to value"""
def equalalphanumeric(bch, fieldname, value):
if bch.get_retaincase(fieldname):
return bch[fieldname] == value
else:
return bch[fieldname].upper() == value.upper()
fieldidd = bch.getfieldidd(fieldname)
try:
ftype = fieldidd["type"][0]
if ftype in ["real", "integer"]:
return almostequal(bch[fieldname], float(value), places=places)
else:
return equalalphanumeric(bch, fieldname, value)
except KeyError as e:
return equalalphanumeric(bch, fieldname, value)
[docs]def getreferingobjs(referedobj, iddgroups=None, fields=None):
"""Get a list of objects that refer to this object"""
# pseudocode for code below
# referringobjs = []
# referedobj has: -> Name
# -> reference
# for each obj in idf:
# [optional filter -> objects in iddgroup]
# each field of obj:
# [optional filter -> field in fields]
# has object-list [refname]:
# if refname in reference:
# if Name = field value:
# referringobjs.append()
referringobjs = []
idf = referedobj.theidf
referedidd = referedobj.getfieldidd("Name")
try:
references = referedidd["reference"]
except KeyError as e:
return referringobjs
idfobjs = idf.idfobjects.values()
idfobjs = list(itertools.chain.from_iterable(idfobjs)) # flatten list
if iddgroups: # optional filter
idfobjs = [
anobj for anobj in idfobjs if anobj.getfieldidd("key")["group"] in iddgroups
]
for anobj in idfobjs:
if not fields:
thefields = anobj.objls
else:
thefields = fields
for field in thefields:
try:
itsidd = anobj.getfieldidd(field)
except ValueError as e:
continue
if "object-list" in itsidd:
refname = itsidd["object-list"][0]
if refname in references:
if referedobj.isequal("Name", anobj[field]):
referringobjs.append(anobj)
return referringobjs
[docs]def get_referenced_object(referring_object, fieldname):
"""
Get an object referred to by a field in another object.
For example an object of type Construction has fields for each layer, each
of which refers to a Material. This functions allows the object
representing a Material to be fetched using the name of the layer.
Returns the first item found since if there is more than one matching item,
it is a malformed IDF.
Parameters
----------
referring_object : EpBunch
The object which contains a reference to another object,
fieldname : str
The name of the field in the referring object which contains the
reference to another object.
Returns
-------
EpBunch
"""
idf = referring_object.theidf
object_list = referring_object.getfieldidd_item(fieldname, "object-list")
for obj_type in idf.idfobjects:
for obj in idf.idfobjects[obj_type]:
valid_object_lists = obj.getfieldidd_item("Name", "reference")
if set(object_list).intersection(set(valid_object_lists)):
referenced_obj_name = referring_object[fieldname]
if obj.Name == referenced_obj_name:
return obj