Flatten and extract nested family instances (Revit stream)

Hi,
I have some data coming from Revit and I am trying to get all the objects from a stream flattened using python sdk.
I found this flatten method from @jonathon Version Diffing with Python: Part 1
With this I can get all the walls but I want to get the Windows as well, which are nested inside the walls.

from collections.abc import Iterable, Mapping
from specklepy.objects import Base


def flatten(obj, visited=None):

    # Avoiding pesky circular references
    if visited is None:
        visited = set()

    if obj in visited:
        return

    visited.add(obj)

    # Define a logic for what objects to include in the diff
    should_include = any(
        [
            hasattr(obj, "displayValue"),
            hasattr(obj, "speckle_type")
            and obj.speckle_type == "Objects.Organization.Collection",
            hasattr(obj, "displayStyle"),
        ]
    )

    if should_include:
        yield obj

    props = obj.__dict__

    # traverse the object's nested properties -
    # which may include yieldable objects
    for prop in props:
        value = getattr(obj, prop)

        if value is None:
            continue

        if isinstance(value, Base):
            yield from flatten(value, visited)

        elif isinstance(value, Mapping):
            for dict_value in value.values():
                if isinstance(dict_value, Base):
                    yield from flatten(dict_value, visited)

        elif isinstance(value, Iterable):
            for list_value in value:
                if isinstance(list_value, Base):
                    yield from flatten(list_value, visited)

Any ideas how could i do this?

Thank you!

1 Like

Hi @andrsbtrg !
Once you get the wall, you can call its “elements” property, and loop through resulted objects, checking whether it’s “category” is “Windows”. If it is, call a “definition” property of that window. Good luck!
Disclaimer: I might be missing some details of Revit-specific object structure, so let us know is there are still issues :raised_hands:

4 Likes

Good question - and I find myself iterating over this repeatedly as I work on Automate functions.

Revit and others also have Instance definitions that inherit differently from the elements prop that @Kateryna helpfully pointed out.

My current favourite flatten function is this, it moves away from a recursive function to a stack implementation to handle potentially deeply nested hierarchies.

def flatten(base: Base) -> Iterator[Base]:
    """Traverses Speckle object hierarchies to yield `Base` 
    objects, merging instances with their definitions. 
    Tailored to Speckle's AEC data structures, it covers 
    newer hierarchical structures with Collections and also 
    patterns found in older Revit-specific data.

    Parameters:
    - base (Base): The starting point `Base` object for 
      traversal.

    Yields:
    - Base: A `Base` object.
    """
    # Stack to manage the traversal of the hierarchy.
    stack = [base]

    while stack:
        current = stack.pop()

        # Check if the current object is an instance and has a
        # definition. If so, merge the instance with its definition.
        if isinstance(current, Instance) and current.definition:
            if isinstance(current.definition, Base):
                merged_instance = merge_instance_with_definition(
                    current, current.definition)
                stack.append(merged_instance)
        else:
            yield current

            # Get the elements or @elements attributes if they
            # exist, which are typically containers for other
            # base objects in AEC models.
            elements = (getattr(current, "elements", None) 
                        or getattr(current, "@elements", None))

            if elements:
                # Extend the stack with the elements that are
                # base objects.
                stack.extend(
                    element for element in elements 
                    if isinstance(element, Base)
                )

def merge_instance_with_definition(instance: Instance, 
                                   definition: Base) -> Base:
    """Merges an instance object with its definition, ensuring 
    that unique data on the instance is preserved.

    Parameters:
    - instance (Instance): The instance object to merge.
    - definition (Base): The definition object to merge with.

    Returns:
    - Base: The merged base object.
    """
    # Create a copy of the definition to preserve the original
    merged = definition.copy()

    # Merge properties from the instance into the definition
    for key, value in instance.__dict__.items():
        if key != "definition":
            setattr(merged, key, value)

    return merged
2 Likes

@andrsbtrg at this point you’ll find a flat list of objects and all instances resolved.

You should be aware this has been written with data analysis in mind, if you are considering a flat list of geometric objects, if you were keen to write those back to a speckle model for viewing then there are other options you might want to employ.

a) handle geometric transforms, or
b) not flatten at all but traverse the graph in-situ

1 Like

Hi guys,
Thank you for the inputs! :slight_smile: I will try this function, meanwhile I also found another flatten() method in the template automate repo.
Just as as suggestion it would be great to have those integrated in to the speckle sdk, similar to how it is in the .NET sdk, but I also understand that it’s very dependent on the application where the data was pushed from.

2 Likes