My problem: I feel the need for speed. As with all my forum posts, buckle up. It’s going to be a long and bumpy ride.
I have developed an updater framework that gives one the ability to quickly define an updater that operates on a category of your choosing, for families of a specific name, reacting to parameter changes that you specify, etc. It can be used to establish a link between a model element and a corresponding annotation, reading parameters from the element and writing them to the annotation. Upon opening a document, the addin will collect all elements that apply to a given updater and verify that all corresponding annotations are indeed updated. This all works wonderfully for the most part!
The updater is quite fast. You can modify up to 30 or so elements at once with instantaneous results. The speed is typically not an issue even when working in bloated models because only a handful of elements ever get modified in one transaction (during normal working conditions).
Remember how I said it will verify all elements in the model on startup? That’s where speed becomes a problem. For simpler updates, I can verify 800 elements in a tremendously slow and bloated model and it still only takes around 5 seconds on startup! This is totally reasonable, considering how much work it can save.
Some updates are not so simple, however. One of my updaters collects all circuits in the model, then reads every circuited element, compiling information such as load name and room location, then writes that info to the panel schedule, giving a complete description of the circuit automatically. Again, this works great, and on reasonably sized projects, it still only takes a few seconds. On a ghastly bloated corpse of a model, though, it can take 4 minutes when using a fast algorithm. Using a slower but more accurate algorithm is taking 20 minutes. This is not acceptable. It’s too slow to be used! I’m desperate to speed it up so that it can be used on all our projects, not just the reasonably sized ones.
After doing some profiling, I have found the most expensive function. The culprit is this algorithm for finding what linked room an element resides in:
def GetElementsRoom(elem, updater_bundle):
# Gets linked room containing the element
The function checks all links and determines if any room’s bounding box intersects the element’s bounding box. If only one room intersects, we can assume the element must be in that room (barring some edge case circumstances). This is a pretty fast operation:
for link_dict in updater_bundle.arch_link_dict.values():
# Get link data cached on updater_bundle
link_instance = link_dict['link_instance']
link_doc = link_dict['link_doc']
transform = link_dict['transform']
# Get any rooms whose BoundingBox intersects the elem's BoundingBox
bb_intersecting_rooms = GetElemsRoomByBBIntersection(elem, elem_bb, transform, link_doc)
If more than one bounding box intersection occurs, then we will need to perform some more expensive operations to clarify. Next, we check if the element’s room calculation point can give us an answer:
# Check that the collector contains elements
if bb_intersecting_rooms.GetElementCount() < 1: p('WARNING: bb_intersecting_rooms is empty.')
# If only one BoundingBox intersection, assume this is the correct room:
elif bb_intersecting_rooms.GetElementCount() == 1:
p('INFO: Only one room\'s BoundingBox intersects the element\'s. Returning room.')
intersecting_rooms.append(bb_intersecting_rooms.FirstElement())
else: # In case of multiple BoundingBox intersections:
# First, attempt to get elem's room via it's room calc point
room = GetElementsRoomViaCalcPoint(elem, bb_intersecting_rooms, transform)
if room: intersecting_rooms.append(room)
else:
That is also pretty quick. But what if the room calc point is not enabled? Then we have to create a solid from the room and see if the element geometry intersects it. This is much slower to do:
# If previous fails, attempt to get room by geometry intersection (slower)|
solid_intersecting_rooms = GetElemsRoomBySolidIntersection(bb_intersecting_rooms, elem, transform)|
# Check that any rooms were returned|
if not solid_intersecting_rooms: p('WARNING: no solid_intersecting_rooms')
else:
So as you can see, I have employed some strategies for reducing the amount of computing that needs to be done. I am using simpler approaches when I can get away with it, and I’m using more complex strategies only when more accuracy is required. Still, I need more speed!
Is there anyone out there that can spot ways to optimize further? I’m not a brilliant coder, so excuse the ugliness. Here is the full function and helper functions:
from Autodesk.Revit.DB import (Options, SolidUtils,
FilteredElementCollector, RevitLinkType,
Outline, BoundingBoxIntersectsFilter, ElementIntersectsSolidFilter,
BuiltInCategory, BuiltInParameter as bip)
from Autodesk.Revit.Exceptions import InvalidOperationException, InvalidObjectException
from Autodesk.Revit.UI import TaskDialog
# Import debug printing function
from logging_module import p, time_function
from time import time
def solid_bounding_box_min(bb):
# Must add Origin to Min when
# using BoundingBox from solid.
origin = bb.Transform.Origin
return bb.Min + origin
def solid_bounding_box_max(bb):
# Must add Origin to Max when
# using BoundingBox from solid.
origin = bb.Transform.Origin
return bb.Max + origin
@time_function
def solid_from_room(room, transform, invert=False):
for geometry_item in room.Geometry[Options()]:
try:
geometry_item = SolidUtils.CreateTransformed(geometry_item, transform.Inverse if invert else transform)
except InvalidObjectException:
# If the link was deleted, InvalidObjectException will be thrown for link_doc.
# Warn the user and then clear the cache to ensure it regenerates.
error_msg = 'ERROR: The linked document necessary for element updater may have become unloaded or deleted. \
Please check that the architectural model is currently loaded, then reload the updaters.'
p(error_msg)
TaskDialog.Show('Updater Error', error_msg)
return None
else:
return geometry_item
@time_function
def GetElemsRoomByBBIntersection(elem, elem_bb, transform, link_doc):
# Compensate for link transform while constructing Outline from min & max points.
min_pt = elem_bb.Min.Subtract(transform.Origin)
max_pt = elem_bb.Max.Subtract(transform.Origin)
elem_outline = Outline(min_pt, max_pt)
p('INFO: elem_outline for {}:'.format(elem.Id), min_pt, max_pt)
try:
# Collect rooms whose BoundingBox intersects the BoundingBox (Outline) of the element in question
bb_intersecting_rooms = FilteredElementCollector(link_doc).OfCategory(BuiltInCategory.OST_Rooms)\
.WherePasses(BoundingBoxIntersectsFilter(elem_outline))
except InvalidObjectException:
# If the link was unloaded, InvalidObjectException will be thrown for link_doc.
# Warn the user and then clear the cache to ensure it regenerates.
error_msg = 'ERROR: The linked document necessary for element updater may have become unloaded or deleted. \
Please check that the architectural model is currently loaded, then reload the updaters.'
p(error_msg)
TaskDialog.Show('Updater Error', error_msg)
updater_bundle.arch_link_dict_cache = None # Clear the cache
return None
else:
return bb_intersecting_rooms if bb_intersecting_rooms.GetElementCount() > 0 else None
@time_function
def GetElementsRoomViaCalcPoint(elem, input_rooms, transform):
try: calc_point = elem.GetSpatialElementCalculationPoint().Subtract(transform.Origin)
except InvalidOperationException:
p('WARNING: Element {} does not have the Room Calculation Point enabled. Enabling it will increase accuracy of detecting what room it is in. The model manager/BIM coordinator should enable it in the family editer and reload it into the project.'.format(elem.Id))
else:
p('room calculation point:', calc_point)
for room in input_rooms:
if not room.IsPointInRoom(calc_point):
p('WARNING: Point not in room {}. Continuing to test next point...'.format(room.Number))
continue
else:
p('SUCCESS: Point in room {}. Returning room...'.format(room.Number))
return room
@time_function
def GetElemsRoomBySolidIntersection(rooms_to_check, elem, transform):
solid_intersecting_rooms = []
# Iterate over the rooms whose bounding boxes intersect and determine which ones actually intersect
for room in rooms_to_check:
p('INFO: bb_intersecting_room: {} {}'.format(room.Number, room.get_Parameter(bip.ROOM_NAME).AsString()))
# Check if the element intersects with the room solid
room_solid = solid_from_room(room, transform)
intersection_filter = ElementIntersectsSolidFilter(room_solid)
elem_intersects = intersection_filter.PassesFilter(elem)
# If intersects, append room to output list
if elem_intersects:
p('INFO: element intersects room {}'.format(room.Number))
solid_intersecting_rooms.append(room)
return solid_intersecting_rooms
@time_function
def GetElementsRoom(elem, updater_bundle):
"""
Gets linked room containing the element
"""
# Must use this weird syntax for the Element.BoundingBox property.
# Using argument None retrieves BB of model geometry rather than view (cut) geometry.
elem_bb = elem.get_BoundingBox(None)
# Initialize empty list to hold intersecting rooms, then iterate through all links
intersecting_rooms = []
for link_dict in updater_bundle.arch_link_dict.values():
# Get link data cached on updater_bundle
link_instance = link_dict['link_instance']
link_doc = link_dict['link_doc']
transform = link_dict['transform']
# Get any rooms whose BoundingBox intersects the elem's BoundingBox
bb_intersecting_rooms = GetElemsRoomByBBIntersection(elem, elem_bb, transform, link_doc)
# Check that any BB intersections returned
if not bb_intersecting_rooms:
p('ERROR: bb_intersecting_rooms failed. {}'.format(bb_intersecting_rooms))
return None
else:
# Check that the collector contains elements
if bb_intersecting_rooms.GetElementCount() < 1: p('WARNING: bb_intersecting_rooms is empty.')
# If only one BoundingBox intersection, assume this is the correct room:
elif bb_intersecting_rooms.GetElementCount() == 1:
p('INFO: Only one room\'s BoundingBox intersects the element\'s. Returning room.')
intersecting_rooms.append(bb_intersecting_rooms.FirstElement())
else: # In case of multiple BoundingBox intersections:
# First, attempt to get elem's room via it's room calc point
room = GetElementsRoomViaCalcPoint(elem, bb_intersecting_rooms, transform)
if room: intersecting_rooms.append(room)
else:
# If previous fails, attempt to get room by geometry intersection (slower)
solid_intersecting_rooms = GetElemsRoomBySolidIntersection(bb_intersecting_rooms, elem, transform)
# Check that any rooms were returned
if not solid_intersecting_rooms: p('WARNING: no solid_intersecting_rooms')
else:
# Check how many rooms intersect
room_count = len(solid_intersecting_rooms)
if room_count == 1:
# If one room found:
room = solid_intersecting_rooms[0]
intersecting_rooms.append(room)
elif room_count > 1:
# If multiple rooms intersecting element:
p('WARNING: Multiple rooms found intersecting element {}. Calculation point has already \
failed to determine element\'s room. returning None...'.format(elem.Id))
return None
else: p('ERROR: Unnaccounted-for else condition in GetElementsRoom()')
# Output
room_count = len(intersecting_rooms)
if room_count < 1:
# If no rooms found by BB:
p('WARNING: room_count by BB len < 1.')
return None
elif room_count == 1:
p('INFO: 1 intersection room found. intersecting_rooms: {}'.format(intersecting_rooms))
return intersecting_rooms[0]
else: p('ERROR: Unnaccounted-for else condition in GetElementsRoom()')
What I’ve already tried:
Compiling my IronPython code into a dll: I heard this can increase speed 10x or so. I succeeded at compiling, but saw absolutely no performance boost. It may be that Python’s interpreted slowness was never significant to begin with. It may be that the API code itself just takes too long.
Parallel processing: Tried using Python’s multiprocessing module. Unfortunately, IronPython does not have this module, so I even attempted to use the more primitive threading module to manually create additional processes. Got that working, but after testing, saw no performance increase. Then I realized that IronPython has no GIL, so I tried using .NET’s AsParallel. This successfully ran in parallel during testing using the sleep function, but when doing any actual work, it failed to run anything in parallel. After all that, I found out that Revit’s API is single-threaded only, so you cannot make multiple simultaneous API calls from separate threads, so all that work was pointless to begin with. Such a shame, because parallel processing would result in a 12x speedup on my machine.