Get linked room that element resides in. Need speed increase!

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.

Update: I rewrote the above module in C# (my first real attempt at this language) to see if it would be faster. I think I have successfully converted it, but I get an error that says System.Collections cannot be found:

room = GetElementsRoom(connected_elem, updater.updater_bundle.arch_links)
IOError: [Errno 2] Could not load file or assembly 'System.Collections, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a' or one of its dependencies. The system cannot find the file specified.

I compiled the C# into a dll, then referenced it into my python script. It seems to be successfully loading from the dll until it gets to this point.

Another funny thing: Visual Studio is telling me System.Collections is an “unnecessary using” even though I am using System.Collections.Generic.List & Dictionary in my code.

Below is the C# code. Can anyone tell me how to diagnose this?

using Autodesk.Revit.DB;
using Autodesk.Revit.DB.Architecture;
using Autodesk.Revit.Exceptions;
using Autodesk.Revit.UI;
using System.Collections;

namespace room_utils_c_sharp
{
    public class room_utils : IExternalCommand
	{
		public static XYZ solid_bounding_box_min(BoundingBoxXYZ bb)
		{
			// Must add Origin to Min when
			// using BoundingBox from solid.
			XYZ origin = bb.Transform.Origin;
			return bb.Min + origin;
		}

		public static XYZ solid_bounding_box_max(BoundingBoxXYZ bb)
		{
			// Must add Origin to Max when
			// using BoundingBox from solid.
			XYZ origin = bb.Transform.Origin;
			return bb.Max + origin;
		}

		public static Solid solid_from_room(Room room, Transform transform)
		{
			foreach (Solid solid in room.ClosedShell.GetTransformed(transform))
			{
				return solid;
			}
			return null;
		}

		public static FilteredElementCollector GetElemsRoomByBBIntersection(Element elem, BoundingBoxXYZ elem_bb, Transform transform, Document link_doc)
		{
			// Compensate for link transform while constructing Outline from min & max points.
			XYZ min_pt = elem_bb.Min.Subtract(transform.Origin);
			XYZ max_pt = elem_bb.Max.Subtract(transform.Origin);
			Outline elem_outline = new Outline(min_pt, max_pt);

			try
			{
				// Collect rooms whose BoundingBox intersects the BoundingBox (Outline) of the element in question
				BoundingBoxIntersectsFilter filter = new BoundingBoxIntersectsFilter(elem_outline);

				FilteredElementCollector bb_intersecting_rooms = new FilteredElementCollector(link_doc).OfCategory(BuiltInCategory.OST_Rooms).WherePasses(filter);
				return bb_intersecting_rooms.GetElementCount() > 0 ? bb_intersecting_rooms : null;
			}

			catch (InvalidObjectException)
			{
				return null;
			}
		}

		public static Room GetElementsRoomViaCalcPoint(FamilyInstance elem, FilteredElementCollector input_rooms, Transform transform)
		{
			try
			{
				XYZ calc_point = elem.GetSpatialElementCalculationPoint().Subtract(transform.Origin);

				foreach (Room room in input_rooms)
				{
					if (room.IsPointInRoom(calc_point) != true)
					{

						continue;
					}
					else
					{
						return room;
					}
				}
			}
			catch (Autodesk.Revit.Exceptions.InvalidOperationException)
			{
				return null;
			}
			return null;
		}

		public static List<Room> GetElemsRoomBySolidIntersection(FilteredElementCollector rooms_to_check, Element elem, Transform transform)
		{
			List<Room> intersecting_rooms = new List<Room>();
			foreach (Room room in rooms_to_check)
			{
				ElementIntersectsSolidFilter filter = new ElementIntersectsSolidFilter(solid_from_room(room, transform));

				if (filter.PassesFilter(elem))
				{
					intersecting_rooms.Add(room);
				}
			}
			return intersecting_rooms;
		}

		public static Element GetElementsRoom(FamilyInstance elem, List<Dictionary<string, RevitLinkInstance>> arch_links)
		{
			//Gets linked room containing the element
			// Must use this weird syntax for the Element.BoundingBox property.
			// Using null argument retrieves BB of model geometry rather than view (cut) geometry.
			BoundingBoxXYZ elem_bb = elem.get_BoundingBox(null);

			// Initialize empty list to hold intersecting rooms, then iterate through all links
			List<Element> intersecting_rooms = new List<Element>();
			foreach (Dictionary<string, RevitLinkInstance> link_dict in arch_links)
			{
				// Get link data cached on updater_bundle
				RevitLinkInstance link_instance = link_dict["link_instance"];
				Document link_doc = link_instance.GetLinkDocument();
				Transform transform = link_instance.GetTotalTransform();

				// Get any rooms whose BoundingBox intersects the elem's BoundingBox
				FilteredElementCollector bb_intersecting_rooms = GetElemsRoomByBBIntersection(elem, elem_bb, transform, link_doc);

				// Check that any BB intersections returned
				if (bb_intersecting_rooms == null)
				{
					
				}
				else
				{
					// Check that the collector contains elements
					if (bb_intersecting_rooms.GetElementCount() < 1)
					{
						
					}
					// If only one BoundingBox intersection, assume it's the correct room:
					else if (bb_intersecting_rooms.GetElementCount() == 1)
					{
						intersecting_rooms.Add(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 room = GetElementsRoomViaCalcPoint(elem, bb_intersecting_rooms, transform);
						if (room != null)
						{
							intersecting_rooms.Add(room);
						}
						else
						{
							// If previous fails, attempt to get room by geometry intersection (slower)
							List<Room> solid_intersecting_rooms = GetElemsRoomBySolidIntersection(bb_intersecting_rooms, elem, transform);

							// Check that any rooms were returned
							if (solid_intersecting_rooms.Count == 0)
							{
								
							}
							else
							{
								// Check how many rooms intersect
								int room_count_int = solid_intersecting_rooms.Count;
								if (room_count_int == 1)
								{
									// If one room found:
									Room room_out = solid_intersecting_rooms[0];
									intersecting_rooms.Add(room_out);
								}
								else if (room_count_int > 1)
								{
									// If multiple rooms intersecting element, do nothing.
								}
								else
								{
									
								}
							}
						}
					}
				}

				// If a room is already found, no need to check the other links.
				// Just return the first found, which is most likely from the main arch model.
				if (intersecting_rooms.Count > 0)
				{
					return intersecting_rooms[0];
				}
			}

			// Output - collects rooms from all links.
			int room_count = intersecting_rooms.Count;
			if (room_count < 1)
			{
				// If no rooms found by BB:
				return null;
			}
			else if (room_count == 1)
			{
				return intersecting_rooms[0];
			}
			else // If room_count > 1:
			{
				return intersecting_rooms[0]; // Return the first room found because it's more likely to be from the main arch model.
			}
		}

		public Result Execute(ExternalCommandData command_data, ref string message, ElementSet elements)
		{
			TaskDialog.Show("test", "running Execute");
			return Result.Succeeded;
		}
	}
}

List is in the:

System.Collections.Generic;

That should give you access to List.

Thank you for the response!

I actually tried that as well, but Visual studio still says it’s an unnecessary using and I get the same error when trying to import the C# dll into a python script.

I also placed the C# code into a pushbutton as it’s own script. PyRevit seems to compile it fine, but I really need to use it from a separate python script. Is there a way to import the C# module compiled by pyRevit (rather than using the one I compiled myself in VS)? That would be much more ideal anyway, so that it can be used for multiple Revit versions.

I kind of think I see what’s going on…

Visual Studio does not see System.Collections.Generic as a necessary reference, so for some reason, it’s not including it in the compiled dll.

I have confirmed that pyRevit’s compiler does include System.Collections.Generic when it compiles, so that’s why it works. The error is just some weird quirk of Visual Studio, so I don’t know how to get it to actually work properly and include the reference that I’m using in my code.

Or perhaps it has something to do with different versions of .NET?

Still hoping that I can use pyRevit to compile the module, then import it in a separate script.

Using fuslogvw.exe, I have this Assembly Binder Log:

*** Assembly Binder Log Entry  (3/14/2025 @ 4:29:34 PM) ***

The operation failed.
Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from:  C:\Windows\Microsoft.NET\Framework64\v4.0.30319\clr.dll
Running under executable  C:\Program Files\Autodesk\Revit 2023\Revit.exe
--- A detailed error log follows. 

=== Pre-bind state information ===
LOG: DisplayName = System.Collections, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
 (Fully-specified)
LOG: Appbase = file:///C:/Program Files/Autodesk/Revit 2023/
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = Revit.exe
Calling assembly : (Unknown).
===
LOG: This bind starts in default load context.
LOG: Using application configuration file: C:\Program Files\Autodesk\Revit 2023\Revit.exe.Config
LOG: Using host configuration file: 
LOG: Using machine configuration file from C:\Windows\Microsoft.NET\Framework64\v4.0.30319\config\machine.config.
LOG: Post-policy reference: System.Collections, Version=6.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a
LOG: GAC Lookup was unsuccessful.

I really don’t know what all that means. I still don’t understand why the dll can’t be found.

Way left field, but have you tried document.GetRoomAtPoint()

Not sure how well it performs at scale but it has always felt fast to me vs using geometry. Obviously not as flexible and intersection generating as a bb though.

1 Like

Have you considered a spatial index?

Check this library out if you are going for a C# implementation: G-Shark/src/GShark/Intersection/BoundingBoxTree at master · GSharker/G-Shark · GitHub

1 Like

Ive tried testing it out System.Collections is not being used as a directive only System.Collections.Generic is being used.

Way left field, but have you tried document.GetRoomAtPoint()

:man_facepalming: That’s not out of left field at all. That should have been the first thing I tried, but I guess I didn’t know that method existed. I was preoccupied with FamilyInstance.Room and did not think to look for a method on the Document class.

Thank you, Gavin, for 10x’ing my function. Using GetRoomAtPoint() with the room calculation point seems very reliable and fast. Without the room calc point, I go by the family origin, so it may not be as accurate. I will need to figure out how to handle that. I’m not sure how to determine when an element’s origin is an unreliable indicator of the room it’s in. Kind of depends on the family construction.

Either way, I’m in a much better place now, so my hat’s off to you!

2 Likes

Thank you Ahmed for testing that.

Strangely, for me, Visual Studio doesn’t recognize System.Collections.Generic as a necessary directive. Even without using it, it still compiles, but then I get runtime errors. That’s ok though, because I achieved the necessary speed with Python alone now.

I think the BoundingBoxTree is a bit over my head right now, but I may look into it in the future.

Thanks to all for the help!

Oh nice one, good to know it’s comparatively fast - I always wonder how its implemented behind the scenes, guess its written efficiently!

Glad it helped, I use this method for so many things at work.