Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds the ability to register new functions for EpBunch objects #250

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from

Conversation

samuelduchesne
Copy link
Contributor

@samuelduchesne samuelduchesne commented Jul 25, 2019

Hi @santoshphilip,
Hi @jamiebull1,

Gentlemen, I have been working a lot with eppy lately and sometimes I needed to develop new functions for convenience. For instance, I needed to create a function to return the total conditioned area of the whole building.

My first reflex was to subclass the IDF class and to add a conditioned_area() as a property (with the @property decorator). See here:
carbon-3
This worked well for an IDF level property. But then, I also needed a similar function for zone EpBunch objects (EpBunch.key == 'ZONE') since I wanted to know the conditioned area of individual zones. Since I didn't want to subclass the EpBunch class as well, I figured I could implement a new type of function decorator instead. This is inspired by how Pandas asks developers to extend pandas functionalities.

My suggestion is this: users should decorate custom functions with the register_epbunch_function decorator. Here is an example:
carbon-4
In this example, the function has also to @property decorator because I what the function to behave like a property (no need for the empty parentheses). Furthermore, in the first decorator, the keys attribute lets you specify which type of EP object should implement the function. For example, I only want my conditioned_area method to work for ZONE objects.

To implement the new functionality, I had to change a few things in the way EpBunch class instances are populated with the default methods. I will try to comment directly in this pull request to guide you guys through my logic.

Copy link
Contributor Author

@samuelduchesne samuelduchesne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use PyCharm as an IDE, therefore a lot of the changes are simply code cleanups.

@samuelduchesne samuelduchesne marked this pull request as ready for review July 25, 2019 18:07
@santoshphilip
Copy link
Owner

very very nice. I like this.
I never liked the way eppy implemented the functions like area, volume etc.

I have not looked into your code carefully yet - my mindspace is taken up another project at the moment.

If this works and passes all tests (including ones that you may write), I'll be happy to merge it into the eppy.

We should also develop user documentation for this.

@samuelduchesne
Copy link
Contributor Author

I'm glad you like it. Before merging anything, I suggest we continue on this pull request to improve the logic and to, like you said, create documentation.

Finally, I don't see why we should not implement the decorator logic to the IDF class as well! This would allow users to add functionality without using subclasses.

Copy link
Collaborator

@jamiebull1 jamiebull1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me in general. I've not thought much about how (if at all) it will affect other tools that build on eppy - but I'll have a good look at geomeppy when I get another chunk of time.

I actually prefer this approach to the patching I've used in geomeppy, so thanks for this contribution. Just want to be sure that we don't introduce anything that might break other's code without a very good reason.

eppy/tests/test_function_helpers.py Outdated Show resolved Hide resolved
eppy/tests/test_function_helpers.py Outdated Show resolved Hide resolved
eppy/bunch_subclass.py Show resolved Hide resolved
eppy/bunch_subclass.py Outdated Show resolved Hide resolved
@jamiebull1
Copy link
Collaborator

I don't see why we should not implement the decorator logic to the IDF class as well!

Agree 100%

@santoshphilip
Copy link
Owner

@jamiebull1 , No objections to black.

Also, You make a good point about not breaking other tools built on eppy.
the public repositories that use eppy are here:
https://github.com/santoshphilip/eppy/network/dependents

There may be internal tools that are not public, that we don't know about.

@samuelduchesne
Copy link
Contributor Author

Great!

In the mean time, I don't know if you guys can help me with a small issue with the current logic: Since functions are registered before class instantiation and that the function name is added as an attribute (see this Line) of EpBunch, all functions are "available" on an instance of the class, even though not all of them will appear in __dir__. This can possibly create a weird thing such as being able to call .area on the VERSION epbunch (or any other object), which will throw an error of course. That being said, if a user calls dir(epbunch), the area won't show up since it is filtered out by the functions property.

Does that make sense to you guys?

@jamiebull1
Copy link
Collaborator

@samuelduchesne IDF.idd_index["ref2names"]["Surface Names"] should get you the surface names

@samuelduchesne
Copy link
Contributor Author

@jamiebull1 but what about possible surface types? eg.: "BuildingSurface:Detailed", "Surface:Wall", etc...

@jamiebull1
Copy link
Collaborator

jamiebull1 commented Jul 25, 2019

From memory it's names in the IDD, so surface types. I'm away from a computer right now so can't check.

@samuelduchesne
Copy link
Contributor Author

samuelduchesne commented Jul 25, 2019

@jamiebull1 I can confirm it is idf.idd_index["ref2names"]['SurfaceNames'] (no space 😉)

@samuelduchesne
Copy link
Contributor Author

Let's keep a todo list (@jamiebull1 & @santoshphilip, feel free to add to this list)

Todo

  • Add decorator logic to IDF class.
  • Create a decorator example which adds a function that has attributes (not just properties).
  • Document the new logic.

Copy link
Contributor Author

@samuelduchesne samuelduchesne left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oups, looks like CIs are not happy with that one on some platforms. Ideas why?

@santoshphilip
Copy link
Owner

ouch ! I merged the pull request #251
That merge is creating merge conflict in this pull request.

@samuelduchesne the line number for the error is visible on https://travis-ci.org/santoshphilip/eppy/jobs/563726632

If you look at https://travis-ci.org/santoshphilip/eppy/builds/563726631?utm_source=github_status&utm_medium=notification you will see that three of them failed and one passes. Click on the links to follow up and see why they are failing. If you are not able to figure it out, I'll take a look

@samuelduchesne
Copy link
Contributor Author

ouch ! I merged the pull request #251
That merge is creating merge conflict in this pull request.

@samuelduchesne the line number for the error is visible on https://travis-ci.org/santoshphilip/eppy/jobs/563726632

If you look at https://travis-ci.org/santoshphilip/eppy/builds/563726631?utm_source=github_status&utm_medium=notification you will see that three of them failed and one passes. Click on the links to follow up and see why they are failing. If you are not able to figure it out, I'll take a look

Ok, I'll have a look this week.

@jamiebull1
Copy link
Collaborator

The problem is a circular import. You're importing bunch_subclass in function_helpers, but you're also importing register_epbunch_function from function_helpers in bunch_subclass.

Two ways to fix it:

  1. Import function_helpers inside the bunch_subclass.addfunctions function
  2. Move addfunctions into function_helpers

I prefer option 2, as it keeps all the imports at the top of the module. Late imports are almost always a code smell.

@codecov-io
Copy link

codecov-io commented Aug 5, 2019

Codecov Report

Merging #250 into develop will decrease coverage by 0.21%.
The diff coverage is 52.38%.

Impacted file tree graph

@@            Coverage Diff             @@
##           develop    #250      +/-   ##
==========================================
- Coverage    43.32%   43.1%   -0.22%     
==========================================
  Files           89      90       +1     
  Lines         7568    7611      +43     
==========================================
+ Hits          3279    3281       +2     
- Misses        4289    4330      +41
Impacted Files Coverage Δ
eppy/tests/test_function_helpers.py 0% <0%> (ø)
eppy/tests/conftest.py 0% <0%> (ø) ⬆️
eppy/tests/test_bunch_subclass.py 0% <0%> (ø) ⬆️
eppy/modeleditor.py 95% <100%> (ø) ⬆️
eppy/function_helpers.py 68.75% <65.57%> (-9.22%) ⬇️
eppy/bunch_subclass.py 96.78% <89.28%> (+0.17%) ⬆️
eppy/tests/test_runner.py 0% <0%> (ø) ⬆️
eppy/runner/run_functions.py 88.8% <0%> (+0.31%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update deca838...751a720. Read the comment docs.

Copy link
Collaborator

@jamiebull1 jamiebull1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. Good test coverage. I've suggested a few minor changes, but mainly style things. There's also one issue with a test that I think is not testing what it should be (testing for area vs new_area). I may have misunderstood the intent though.

snames = [sname.upper() for sname in snames]
func_dict = {
"area": (area, snames),
"height": (height, snames), # not working correctly
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these comments from before the changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are not mine

# 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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Filtering is fine, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is for @santoshphilip I think. Not my code.

eppy/tests/test_bunch_subclass.py Outdated Show resolved Hide resolved
eppy/tests/test_function_helpers.py Outdated Show resolved Hide resolved
IDF.setiddname(iddfhandle)
yield IDF

@pytest.fixture()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer these two fixtures to be in conftest.py. In fact I think there may be equivalent ones there already.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected in 3080f0d

eppy/bunch_subclass.py Show resolved Hide resolved
eppy/tests/test_function_helpers.py Outdated Show resolved Hide resolved
@samuelduchesne
Copy link
Contributor Author

I don't understand why Travis is not happy. I think it's because the modeleditor.IDF's idd has already been set. I still don't understand why we can't change the IDD for another instance of modeleditor.IDF

@jamiebull1
Copy link
Collaborator

jamiebull1 commented Aug 12, 2019

@samuelduchesne

When I suggested reusing an existing fixture, I meant to use one to give you the base IDF. You can then just add the objects you need in your test. For example:

@pytest.fixture()
def building(base_idf):
    """A building fixture created from base_idf and which adds a Zone, and a BuildingSurface:Detailed
    object"""
    base_idf.newidfobject("Zone", Name="West Zone")
    base_idf.newidfobject(
        "BuildingSurface:Detailed", Name="West Floor", Zone_Name="West Zone", Surface_Type="Floor",
        Vertex_1_Xcoordinate=0,
        Vertex_1_Ycoordinate=0,
        Vertex_1_Zcoordinate=0,
        Vertex_2_Xcoordinate=0,
        Vertex_2_Ycoordinate=10,
        Vertex_2_Zcoordinate=0,
        Vertex_3_Xcoordinate=10,
        Vertex_3_Ycoordinate=10,
        Vertex_3_Zcoordinate=0,
        Vertex_4_Xcoordinate=10,
        Vertex_4_Ycoordinate=0,
        Vertex_4_Zcoordinate=0,
    )
    yield base_idf

This gives you a zone with just a floor surface. You could add a wall and a ceiling to ensure they're filtered out, but that's more testing the conditioned_area algorithm than testing registering functions, so belongs in your application's tests, not eppy's.

As an aside, I'd suggest using almost_equal for the test for surface.tilt == 180.0 since floating point errors often accumulate in calculations on IDF geometry. Alternatively, you could just check if the Surface_Type is Floor, if you know that it will have been set correctly in your IDF.

@samuelduchesne
Copy link
Contributor Author

samuelduchesne commented Aug 12, 2019

@samuelduchesne

When I suggested reusing an existing fixture, I meant to use one to give you the base IDF. You can then just add the objects you need in your test. For example:

@pytest.fixture()
def building(base_idf):
    """A building fixture created from base_idf and which adds a Zone, and a BuildingSurface:Detailed
    object"""
    base_idf.newidfobject("Zone", Name="West Zone")
    base_idf.newidfobject(
        "BuildingSurface:Detailed", Name="West Floor", Zone_Name="West Zone", Surface_Type="Floor",
        Vertex_1_Xcoordinate=0,
        Vertex_1_Ycoordinate=0,
        Vertex_1_Zcoordinate=0,
        Vertex_2_Xcoordinate=0,
        Vertex_2_Ycoordinate=10,
        Vertex_2_Zcoordinate=0,
        Vertex_3_Xcoordinate=10,
        Vertex_3_Ycoordinate=10,
        Vertex_3_Zcoordinate=0,
        Vertex_4_Xcoordinate=10,
        Vertex_4_Ycoordinate=0,
        Vertex_4_Zcoordinate=0,
    )
    yield base_idf

This gives you a zone with just a floor surface. You could add a wall and a ceiling to ensure they're filtered out, but that's more testing the conditioned_area algorithm than testing registering functions, so belongs in your application's tests, not eppy's.

As an aside, I'd suggest using almost_equal for the test for surface.tilt == 180.0 since floating point errors often accumulate in calculations on IDF geometry. Alternatively, you could just check if the Surface_Type is Floor, if you know that it will have been set correctly in your IDF.

Totally makes sense. Adjusted in f37e882

@jamiebull1
Copy link
Collaborator

Looks good to me. Anything to add, @santoshphilip ?

@samuelduchesne
Copy link
Contributor Author

Looks good to me. Anything to add, @santoshphilip ?

Currently working on these:

Todo

  • Add decorator logic to IDF class.
  • Create a decorator example which adds a function that has attributes (not just properties).
  • Document the new logic.

@samuelduchesne
Copy link
Contributor Author

@jamiebull1,
On the latest commit (320dbaa), somehow I can't get the tests to work independently. If I run the TestRegisterFunction class everything works fine, but if I run all tests (eg.: run pystest in tests) functions are not registered. Something to do with fixture scopes? Are you able to make it work?

@jamiebull1
Copy link
Collaborator

jamiebull1 commented Aug 12, 2019

First thing - from eppy.pytest_helpers import almostequal should replace isclose which I think is only for python >3.5.

Second, I get the same error as you for the IDF helper function registration -works when just running that module, but not if running the full test suite. Not sure what's breaking there, and don't have the time to look into it right now. It shouldn't be the fixture scope as that's function by default. I can only think that there must be something else set at a global level that should be cleared after the yield base_idf statement in the fixture.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants