Monday, December 5, 2016

Update to "Embedding a webserver in Autodesk Revit with the RevitPythonShell"

As discussed on the RevitPythonShell forum, previous examples of using ExternalEvent don’t work anymore with Revit 2017.

Turns out, the fix was really easy: You just need to implement the IExternalEventHandler.GetName method. I have placed a comment explaining this very briefly on the page that introduces embedding a webserver in Autodesk Revit with the RevitPythonShell. The source for the updated RpsHttpServer is, of course, on GitHub: rpshttpserver.py

Thanks to Callum for finding the bug and @PMoureu for providing the solution :)

Wednesday, November 16, 2016

How to write in the URL field of a family with the RevitPythonShell

Here is some code showing how to write the URL parameter of a WallType with the RevitPythonShell. This was created as an answer to the RevitPythonShell group forum. This method should work for other families as well!

# find a WallType to work with...
collector = FilteredElementCollector(doc).OfClass(WallType)
wt = [wt for wt in collector 
      if Element.Name.__get__(wt).startswith('_dpv AW1')][0]
print Element.Name.__get__(wt)
parameter = wt.get_Parameter(BuiltInParameter.ALL_MODEL_URL)
transaction = Transaction(doc, 'setting URL')
try: 
    transaction.Start() 
    parameter.Set('http://systems.arch.ethz.ch')
    transaction.Commit()
except: 
    transaction.Rollback()

The tricky part is the line with wt.get_Parameter(BuiltInParameter.ALL_MODEL_URL). I found the ALL_MODEL_URL parameter name using the RevitLookup tool - which can be launched directly from RPS if you have it installed (just type lookup(wt)). The basic steps are:

  1. retrieve the parameter (Element.get_Parameter - you can use the BuiltInParameter enumeration for standard parameters…)
  2. set the value of the parameter (Parameter.Set)
  3. since this changes the BIM model, you need to wrap it all in a Transaction.

Monday, October 24, 2016

Merged PR #42 by @eirannejad to RevitPythonShell

This is exciting: I just merged a pull request on the RevitPythonShell repository. Sure, no big deal, except, it kinda is. Here’s why:

I don’t really have a lot of time to improve the RPS right now as I am mainly working on a totally unrelated project, the CityEnergyAnalyst. And I don’t really BIM much anymore.

Turns out, the RPS has a fan base and they’re stepping up with feature requests, but also improvements in the form of pull requests.

A pull request is a collection of commits that you propose to add to the master branch of the RPS software. You can fork your own version of the RPS and with this feature, request your changes be added back to the main version. This is probably the best way to get changes / bug fixes etc. into the software.

In this case, Ehsan Iran-Nejad stepped up and provided a small patch that adds the folder an external script is defined in to the search paths of the interpreter. This makes it easier to split your code into reusable libraries.

BTW: Check out Ehsan’s awesome project pyRevit which makes working with RPS even easier!

Friday, October 21, 2016

An example in refactoring the CEA

This post is the result of a refactoring effort between Shanshan Hsieh and I for the City Energy Analyst, particularly on the pull request #371.

Compare these two snippets of code:

Original

tHC_corr = [0, 0]
delta_ctrl = [0, 0]

# emission system room temperature control type
if control_system == 'T1':
    delta_ctrl = [2.5, -2.5]
elif control_system == 'T2':
    delta_ctrl = [1.2, -1.2]
elif control_system == 'T3':
    delta_ctrl = [0.9, -0.9]
elif control_system == 'T4':
    delta_ctrl = [1.8, -1.8]

# calculate temperature correction
if heating_system == 'T1':
    tHC_corr[0] = delta_ctrl[0] + 0.15
elif heating_system == 'T2':
    tHC_corr[0] = delta_ctrl[0] - 0.1
elif heating_system == 'T3':
    tHC_corr[0] = delta_ctrl[0] - 1.1
elif heating_system == 'T4':
    tHC_corr[0] = delta_ctrl[0] - 0.9
else:
    tHC_corr[0] = 0

if cooling_system == 'T1':
    tHC_corr[1] = delta_ctrl[1] + 0.5
elif cooling_system == 'T2':  # no emission losses but emissions for ventilation
    tHC_corr[1] = delta_ctrl[1] + 0.7
elif cooling_system == 'T3':
    tHC_corr[1] = delta_ctrl[1] + 0.5
else:
    tHC_corr[1] = 0

return tHC_corr[0], tHC_corr[1]

Refactored

control_delta_heating = {'T1': 2.5, 'T2': 1.2, 'T3': 0.9, 'T4': 1.8}
control_delta_cooling = {'T1': -2.5, 'T2': -1.2, 'T3': -0.9, 'T4': -1.8}
system_delta_heating = {'T0': 0.0, 'T1': 0.15, 'T2': -0.1, 'T3': -1.1, 'T4': -0.9}
system_delta_cooling = {'T0': 0.0, 'T1': 0.5, 'T2': 0.7, 'T3': 0.5}
try:
    result_heating = 0.0 if heating_system == 'T0' else (control_delta_heating[control_system] +
                                                         system_delta_heating[heating_system])
    result_cooling = 0.0 if cooling_system == 'T0' else (control_delta_cooling[control_system] +
                                                         system_delta_cooling[cooling_system])
except KeyError:
    raise ValueError(
        'Invalid system / control combination: %s, %s, %s' % (heating_system, cooling_system, control_system))

return result_heating, result_cooling

Ideally, these two codes produce the same result. I would argue though, that by using a table-based method, that is, keeping the data in lookup tables like dicts, makes the code a bit clearer to understand than using a lot of if...elif...else statements. This simplifies the code down to it’s core message: The result is the sum of the deltas for the control system and the system type.

Another improvement, in my mind, is getting rid of the X[0] and X[1] constructs. In the original code, these were used to group the heating and the cooling values respectively. While in a small snippet like this, that can still be understood, this paradigm breaks down quickly. One way to improve that would be to assign HEATING = 0 and COOLING = 1 and then index like this: tHC_corr[HEATING]. That communicates the intention much better. Using separate dictionaries for heating and cooling values for both emission systems and control systems sidesteps the issue alltogether.

Documentation

@shanshan and I went a step further than just changing the code. Here is a model of what the documentation of the function could look like:

"""
Model of losses in the emission and control system for space heating and cooling.

Correction factor for the heating and cooling setpoints. Extracted from EN 15316-2

(see cea\databases\CH\Systems\emission_systems.xls for valid values for the heating and cooling system values)

T0 means there's no heating/cooling systems installed, therefore, also no control systems for heating/cooling.
In short, when the input system is T0, the output set point correction should be 0.0.
So if there is no cooling systems, the setpoint_correction_for_space_emission_systems function input: (T1, T0, T1) (type_hs, type_cs, type_ctrl),
return should be (2.65, 0.0), the control system is only specified for the heating system.
In another case with no heating systems: input: (T0, T3, T1) return: (0.0, -2.0), the control system is only
specified for the heating system.

PARAMETERS
----------

:param heating_system: The heating system used. Valid values: T0, T1, T2, T3, T4
:type heating_system: str

:param cooling_system: The cooling system used. Valid values: T0, T1, T2, T3
:type cooling_system: str

:param control_system: The control system used. Valid values: T1, T2, T3, T4 - as defined in the
    contributors manual under Databases / Archetypes / Building Properties / Mechanical systems.
    T1 for none, T2 for PI control, T3 for PI control with optimum tuning, and T4 for room temperature control
    (electromagnetically/electronically).
:type control_system: str

RETURNS
-------

:returns: two delta T to correct the set point temperature, dT_heating, dT_cooling
:rtype: tuple(double, double)
"""

Note: The documentation explains what the function is for and also mentions the relevant standards (EN 15316-2) - this could also be references to research papers or whatever. Further, the input values and types are explained and also a link to the documentation for reference. A list of valid inputs is given.

What is missing? Boundary case behaviour - this should actually also be in the documentation! We test this behaviour in the next section:

Unit tests

Check out the file tests/test_sensible_loads.py:

import unittest


class TestCorrectionFactorForHeatingAndCoolingSetpoints(unittest.TestCase):
    def test_calc_t_em_ls_raises_ValueError(self):
        from cea.demand.sensible_loads import setpoint_correction_for_space_emission_systems
        self.assertRaises(ValueError, setpoint_correction_for_space_emission_systems, heating_system='T1',
                          cooling_system='T1', control_system=None)
        self.assertRaises(ValueError, setpoint_correction_for_space_emission_systems, heating_system='T1',
                          cooling_system='XYZ', control_system='T1')
        self.assertRaises(ValueError, setpoint_correction_for_space_emission_systems, heating_system='T1',
                          cooling_system=None, control_system='T1')

    def test_calc_t_em_ls_T0(self):
        from cea.demand.sensible_loads import setpoint_correction_for_space_emission_systems
        self.assertEqual(setpoint_correction_for_space_emission_systems('T1', 'T0', 'T1'), (2.65, 0.0))
        self.assertEqual(setpoint_correction_for_space_emission_systems('T0', 'T3', 'T1'), (0.0, -2.0))

Right now, it contains a single class, TestCorrectionFactorForHeatingAndCoolingSetpoints that inherits from unittest.TestCase. When the Jenkins runs, it will pick up this class, a) because the filename starts with test_ and b) because the classes inherit from TestCase. The Jenkins will then run all the test cases. Each method starting with test_ is run and checked. The self.assert* calls test expected values and actual computations of the setpoint_correction_for_space_emission_systems function. We can see that:

  • calling the method with None or 'XYZ' as one of the parameters should raise an instance of ValueError - this is an edge case that is being tested
  • the setpoint correction for heating or cooling emission systems of type 'T0' (no emission system) should be 0.0

Unit tests like this are a great way to describe how a function should behave, independant of the implementation. It also represents another “mode” of thinking, where you consider edge cases, expected values, failure modes etc. at a very low level. At the level of a “unit” of code.

I believe that especially for code like the CEA, we should use this technique to specify the expected behavior of the system, as we will be on the hook for bugs for the next decade or so. We want to be able to prove the correctnes of our implementation!

Wednesday, June 22, 2016

RevitPythonShell for Revit 2017

I have just released a version of RPS for Revit 2017. It is labeled as a “Pre-Release”, since I have not really had time to test it, but you are all welcome to give it a spin and tell me about any problems you find.

Better yet: Send me pull requests with fixes to your problems!

Tuesday, October 20, 2015

Accessing specific overloads in IronPython (Document.LoadFamily)

The RevitPythonShell makes working with Revit a bit easier, because, you know, Python. The specific flavour of Python used is IronPython - a port of the Python language to the .NET platform. IronPython lets you call into .NET objects seamlessly. In theory. Except when it doesen’t. All abstractions are leaky.
This article is all about a specific leak, based on an impedance mismatch between the object model used in .NET and that used in Python: In C#, you can overload a method. A simple way to think about this is to realize that the name of the method includes its signature, the list of parameter (types) it takes. Go read a book if you want the gory details. Any book. I’m just going to get down to earth here and talk about a specific example:
The Document.LoadFamily method.
The standard method for selecting a specific overload is just calling the function and having IronPython figure it out.
I guess the place to read up on how to call overloaded methods is here: http://ironpython.net/documentation/dotnet/dotnet.html#method-overloads. To quote:
When IronPython code calls an overloaded method, IronPython tries to select one of the overloads at runtime based on the number and type of arguments passed to the method, and also names of any keyword arguments.
This works really well if the types passed in match the signatures of a specific method overload well. IronPython will try to automatically convert types, but will fail with a TypeError if more than one method overload matches.
The Document.LoadFamily method is special in that one of its parameters is marked as out in .NET - according to the standard IronPython documentation (REF) that should translate into a tuple of return values - and it does, if you know how. It is just non-intuitive - see this question on Stack Overflow:
revitpythonshell provides two very similar methods to load a family.
LoadFamily(self: Document, filename:str) -> (bool, Family)
LoadFamily(self: Document, filename:str) -> bool
So it seems like only the return values are different. I have tried to calling it in several different ways:
(success, newFamily) = doc.LoadFamily(path)
success, newFamily = doc.LoadFamily(path)
o = doc.LoadFamily(path)
But I always just get a bool back. I want the Family too.
What is happening here is that the c# definitions of the method are:
public bool LoadFamily(
    string filename
)
and
public bool LoadFamily(
    string filename,
    out Family family
)
The IronPython syntax candy for out parameters, returning a tuple of results, can’t automatically be selected here, because calling LoadFamily with just a string argument matches the first method overload.
You can get at the overload you are looking for like this:
import clr
family = clr.Reference[Family]()
# family is now an Object reference (not set to an instance of an object!)
success = doc.LoadFamily(path, family)  # explicitly choose the overload
# family is now a Revit Family object and can be used as you wish
This works by creating an object reference to pass into the function and the method overload resultion thingy now knows which one to look for.
Working under the assumption that the list of overloads shown in the RPS help is the same order as they appear, you can also do this:
success, family = doc.LoadFamily.Overloads.Functions[0](path)
and that will, indeed, return a tuple (bool, Autodesk.Revit.DB.Family). I just don’t think you should be doing it that way, as it introduces a dependency on the order of the method overloads - I wouldn’t want that smell in my code…
Note, that this has to happen inside a transaction, so a complete example might be:
import clr
t = Transaction(doc, 'loadfamily')
t.Start()
try:
    family = clr.Reference[Family]()
    success = doc.LoadFamily(path, family)
    # do stuff with the family
    t.Commit()
except:
    t.Rollback()

Tuesday, July 7, 2015

The __file__ variable in RevitPythonShell

Today I’m going to talk about a special builtin variable __file__ as it is implemented in the RevitPythonShell. This feature is only available to external scripts
and RpsAddins. This is similar to how __file__ is normally defined for Python: It is not defined in the REPL.

Simply put: __file__ contains the path to the current file being run.

With external scripts, this is a path to a python script. With RpsAddins, it is the path to the addin’s DLL, a path separator, and the name of the script (e.g. C:\Program Files (x86)\MyAddin\MyAddin.dll\helloworld.py).

Suppose you have deployed some scripts to another computer. Suppose those scripts rely on other files - a database, maybe, or icons, pictures, anything really. If you keep referencing these files as C:\Users\Gareth\AwesomeScripts\all_my_data.sqlite you are going to run into difficulties on a computer that doesn’t belong to Gareth. Meg’s computer won’t know how to find the database! And she is going to complain to Meg and meg is going to complain to her boss and her boss is going to complain to your boss and your boss is going to go get coffee, lock himself up in his office and brood for a very long time. Then, he’s going to send someone to tell you to come to his office ASAP. Now!

You don’t want that to happen! That is why you’re going to make sure that all file references are relative to the installation of your external scripts / RpsAddins. And unless you have a priori knowledge of the folder name you’re installing to, well, I’ve got your back:

def get_folder():
    import os
    # assume external script
    folder = os.path.dirname(__file__)
    if folder.lower().endswith('.dll'):
        # nope - RpsAddin
        folder = os.path.dirname(folder)
    return folder