EpBunch

Author:Santosh Philip.

EpBunch is at the heart of what makes eppy easy to use. Specifically Epbunch is what allows us to use the syntax building.Name and building.North_Axis. Some advanced coding had to be done to make this happen. Coding that would be easy for professional programmers, but not for us ordinary folk :-(

Most of us who are going to be coding eppy are not professional programmers. I was completely out of my depth when I did this coding. I had the code reviewed by programmers who do this for a living (at python meetups in the Bay Area). In their opinion, I was not doing anything fundamentally wrong.

Below is a fairly long explanation, to ease you into the code. Read through the whole thing without trying to understand every detail, just getting a birds eye veiw of the explanation. Then read it again, you will start to grok some of the details. All the code here is working code, so you can experiment with it.

Magic Methods (Dunders) of Python

To understand how EpBunch or Bunch is coded, one has to have an understanding of the magic methods of Python. (For a background on magic methods, take a look at http://www.rafekettler.com/magicmethods.html) Let us dive straight into this with some examples

adict = dict(a=10, b=20) # create a dictionary
print adict
print adict['a']
print adict['b']
{'a': 10, 'b': 20}
10
20

What happens when we say d[‘a’] ?

This is where the magic methods come in. Magic methods are methods that work behind the scenes and do some magic. So when we say d[‘a’], The dict is calling the method __getitem__('a').

Magic methods have a double underscore__”, called dunder methods for short

Let us override that method and see what happens.

class Funnydict(dict): # we are subclassing dict here
    def __getitem__(self, key):
        value = super(Funnydict, self).__getitem__(key)
        return "key = %s, value = %s" % (key, value)

funny = Funnydict(dict(a=10, b=20))
print funny
{'a': 10, 'b': 20}

The print worked as expected. Now let us try to print the values

print funny['a']
print funny['b']
key = a, value = 10
key = b, value = 20

Now that worked very differently from a dict

So it is true, funny[‘a’] does call __getitem__() that we just wrote

Let us go back to the variable adict

# to jog our memory
print adict
{'a': 10, 'b': 20}
# this should not work
print adict.a
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)

<ipython-input-5-8aa7211fcb66> in <module>()
      1 # this should not work
----> 2 print adict.a


AttributeError: 'dict' object has no attribute 'a'

What method gets called when we say adict.a ?

The magic method here is __getattr__() and __setattr__(). Shall we override them and see if we can get the dot notation to work ?

class Like_bunch(dict):
    def __getattr__(self, name):
        return self[name]
    def __setattr__(self, name, value):
        self[name] = value

lbunch = Like_bunch(dict(a=10, b=20))
print lbunch
{'a': 10, 'b': 20}

Works like a dict so far. How about lbunch.a ?

print lbunch.a
print lbunch.b
10
20

Yipeee !!! I works

How about lbunch.nota = 100

lbunch.anot = 100
print lbunch.anot
100

All good here. But don’t trust the code above too much. It was simply done as a demonstration of dunder methods and is not fully tested.

Eppy uses the bunch library to do something similar. You can read more about the bunch library in the previous section.

Open an IDF file

Once again let us open a small idf file to test.

# you would normaly install eppy by doing
# python setup.py install
# or
# pip install eppy
# or
# easy_install eppy

# if you have not done so, uncomment the following three lines
import sys
# pathnameto_eppy = 'c:/eppy'
pathnameto_eppy = '../../../'
sys.path.append(pathnameto_eppy)
from eppy import modeleditor
from eppy.modeleditor import IDF
iddfile = "../../../eppy/resources/iddfiles/Energy+V7_2_0.idd"
fname1 = "../../../eppy/resources/idffiles/V_7_2/dev1.idf"

IDF.setiddname(iddfile)
idf1 = IDF(fname1)
idf1.printidf()
VERSION,
    7.3;                      !- Version Identifier

SIMULATIONCONTROL,
    Yes,                      !- Do Zone Sizing Calculation
    Yes,                      !- Do System Sizing Calculation
    Yes,                      !- Do Plant Sizing Calculation
    No,                       !- Run Simulation for Sizing Periods
    Yes;                      !- Run Simulation for Weather File Run Periods

BUILDING,
    Empire State Building,    !- Name
    30.0,                     !- North Axis
    City,                     !- Terrain
    0.04,                     !- Loads Convergence Tolerance Value
    0.4,                      !- Temperature Convergence Tolerance Value
    FullExterior,             !- Solar Distribution
    25,                       !- Maximum Number of Warmup Days
    6;                        !- Minimum Number of Warmup Days

SITE:LOCATION,
    CHICAGO_IL_USA TMY2-94846,    !- Name
    41.78,                    !- Latitude
    -87.75,                   !- Longitude
    -6.0,                     !- Time Zone
    190.0;                    !- Elevation

MATERIAL:AIRGAP,
    F04 Wall air space resistance,    !- Name
    0.15;                     !- Thermal Resistance

MATERIAL:AIRGAP,
    F05 Ceiling air space resistance,    !- Name
    0.18;                     !- Thermal Resistance
dtls = idf1.model.dtls
dt = idf1.model.dt
idd_info = idf1.idd_info
dt['MATERIAL:AIRGAP']
[['MATERIAL:AIRGAP', 'F04 Wall air space resistance', 0.15],
 ['MATERIAL:AIRGAP', 'F05 Ceiling air space resistance', 0.18]]
obj_i = dtls.index('MATERIAL:AIRGAP')
obj_idd = idd_info[obj_i]
obj_idd
[{'memo': ['Air Space in Opaque Construction'], 'min-fields': ['2']},
 {'field': ['Name'],
  'reference': ['MaterialName'],
  'required-field': [''],
  'type': ['alpha']},
 {'field': ['Thermal Resistance'],
  'minimum>': ['0'],
  'type': ['real'],
  'units': ['m2-K/W']}]

For the rest of this section let us look at only one airgap object

airgap = dt['MATERIAL:AIRGAP'][0]
airgap
['MATERIAL:AIRGAP', 'F04 Wall air space resistance', 0.15]

Subclassing of Bunch

Let us review our knowledge of bunch

from bunch import Bunch
adict = {'a':1, 'b':2, 'c':3}
bunchdict = Bunch(adict)
print bunchdict
print bunchdict.a
print bunchdict.b
print bunchdict.c
Bunch(a=1, b=2, c=3)
1
2
3

Bunch lets us use dot notation on the keys of a dictionary. We need to find a way of making airgap.Name work. This is not straightforward because, airgap is list and Bunch works on dicts. It would be easy if airgap was in the form {'Name' : 'F04 Wall air space resistance', 'Thermal Resistance' : 0.15}.

The rest of this section is a simplified version of how EpBunch works.

class EpBunch(Bunch):
    def __init__(self, obj, objls, objidd, *args, **kwargs):
        super(EpBunch, self).__init__(*args, **kwargs)
        self.obj = obj
        self.objls = objls
        self.objidd = objidd

The above code shows how EpBunch is initialized. Three variables are passed to EpBunch to initialize it. They are obj, objls, objidd.

obj = airgap
objls = ['key', 'Name', 'Thermal_Resistance'] # a function extracts this from idf1.idd_info
objidd = obj_idd
#
print obj
print objls
# let us ignore objidd for now
['MATERIAL:AIRGAP', 'F04 Wall air space resistance', 0.15]
['key', 'Name', 'Thermal_Resistance']

Now we override __setattr__() and __getattr__() in the following way

class EpBunch(Bunch):
    def __init__(self, obj, objls, objidd, *args, **kwargs):
        super(EpBunch, self).__init__(*args, **kwargs)
        self.obj = obj
        self.objls = objls
        self.objidd = objidd

    def __getattr__(self, name):
        if name in ('obj', 'objls', 'objidd'):
            return super(EpBunch, self).__getattr__(name)
        i = self.objls.index(name)
        return self.obj[i]

    def __setattr__(self, name, value):
        if name in ('obj', 'objls', 'objidd'):
            super(EpBunch, self).__setattr__(name, value)
            return None
        i = self.objls.index(name)
        self.obj[i] = value
# Let us create a EpBunch object
bunch_airgap = EpBunch(obj, objls, objidd)
# Use this table to see how __setattr__ and __getattr__ work in EpBunch

obj   = ['MATERIAL:AIRGAP', 'F04 Wall air space resistance', 0.15                ]
objls = ['key',             'Name',                         'Thermal_Resistance']
i     =   0                  1                               2
print bunch_airgap.Name
print bunch_airgap.Thermal_Resistance
F04 Wall air space resistance
0.15
print bunch_airgap.obj
['MATERIAL:AIRGAP', 'F04 Wall air space resistance', 0.15]

Let us change some values using the dot notation

bunch_airgap.Name = 'Argon in gap'
print bunch_airgap.Name
Argon in gap
print bunch_airgap.obj
['MATERIAL:AIRGAP', 'Argon in gap', 0.15]

Using the dot notation the value is changed in the list

Let us make sure it actually has done that.

idf1.model.dt['MATERIAL:AIRGAP'][0]
['MATERIAL:AIRGAP', 'Argon in gap', 0.15]

EpBunch acts as a wrapper around idf1.model.dt['MATERIAL:AIRGAP'][0]

In other words EpBunch is just Syntactic Sugar for idf1.model.dt['MATERIAL:AIRGAP'][0]

Variables and Names in Python

At this point your reaction may, “I don’t see how all those values in idf1.model.dt changed”. If such question arises in your mind, you need to read the following:

This is especially important if you are experienced in other languages, and you expect the behavior to be a little different. Actually follow and read those links in any case.

Continuing with EpBunch

EpBunch_1

The code for EpBunch in the earlier section will work, but has been simplified for clarity. In file bunch_subclass.py take a look at the class EpBunch_1 . This class does the first override of __setattr__ and __getattr__. You will see that the code is a little more involved, dealing with edge conditions and catching exceptions.

EpBunch_1 also defines __repr__. This lets you print EpBunch in a human readable format. Further research indicates that __str__ should have been used to do this, not __repr__ :-(

EpBunch_2

EpBunch_2 is subclassed from EpBunch_1.

It overrides __setattr__ and __getattr__ to add a small functionality that has not been documented or used. The idea was to give the ability to shorten field names with alias. So building.Maximum_Number_of_Warmup_Days could be made into building.warmupdays.

I seemed like a good idea when I wrote it. Ignore it for now, although it may make a comeback :-)

EpBunch_3

EpBunch_3 is subclassed from EpBunch_2.

EpBunch_3 adds the ability to add functions to EpBunch objects. This would allow the object to make calculations using data within the object. So BuildingSurface:Detailed object has all the geometry data of the object. The function ‘area’ will let us calculate the are of the object even though area is not a field in BuildingSurface:Detailed.

So you can call idf1.idfobjects["BuildingSurface:Detailed"][0].area and get the area of the surface.

At the moment, the functions can use only data within the object for it’s calculation. We need to extend this functionality so that calculations can be done using data outside the object. This would be useful in calculating the volume of a Zone. Such a calculation would need data from the surfaces that the aone refers to.

EpBunch_4

EpBunch_4 is subclassed from EpBunch_3.

EpBunch_4 overrides _setitem__ and __getitem__. Right now airgap.Name works. This update allows airgap["Name"] to work correctly too

EpBunch_5

EpBunch_5 is subclassed from EpBunch_4.

EpBunch_5 adds functions that allows you to call functions getrange and checkrange for a field

Finally EpBunch

EpBunch = EpBunch_5

Finally EpBunch_5 is named as EpBunch. So the rest of the code uses EpBunch and in effect it uses Epbunch_5