Remove IfcSpaces with IfcOpenShell

edited October 2020 in General

I was hoping to get rid of the ifcspaces in a ifc exported from ArchiCad which I would like to link in to Revit. I tried this simple code:

import ifcopenshell
f = ifcopenshell.open(ifc_path)
for space in f.by_type("ifcspace"):
    f.remove(space)

But it seems like the basement needet the spaces to end up in the correct location.

Is there any way to check if it is safe to delete an ifcspace element? I guess the best way to start is to figure out how the basement is dependent on the spaces to get the correct elevation.

Tagged:
«1

Comments

  • You are correct that elements may be contained within a space, and therefore inherit their local relative placement. Would you like to have a shot at coding this? If not, I can show you how to do it.

    Also, would you be interested in contributing the script as an IfcPatch recipe? That way in the future all users can easily use your script if they want to remove spaces.

  • It looks like some of the elements are contained in storeys and some in spaces. I used element.ContainedInStructure which I think returns the IfcRelContainedInSpatialStructure object. Should we replace the spaces with storeys? I would love to see the solution to this, because at this point i'm just confused :D.
    I will be happy to contribute a recipe if I can figure out how.

  • Yep! Elements can be contained in any spatial element, like a space, or a storey, or a building, etc. The spatial element can also be contained in other spatial elements. This creates a "spatial tree", like Site > Building > Storey > Space > Wall (Element). So if you want to remove the Space, you need to make the wall inside the storey. The next thing you'll need to modify is the ObjectPlacement of the element (wall, in this example). The object placement may be a LocalPlacement. If it is, it is placed relative to the placement of the space, which is relative to the storey, etc all the way back to Site. As you see - the spatial tree is represented twice: once in the contained in structure / aggregation relationship, and another in the relative local object placements.

    Hope it made sense :) Let me know if you need more hints!

  • Thanks! How do I get the parent of the IfcSpace? It does not have the "ContainedInStructure" property as the IfcProducts like IfcWall?

  • @Einar when an element is contained in a spatial element, it uses ContainedInStructure, see here. However, when a spatial element is contained in another spatial element, it uses Decomposes, see here.

    Yes, agree that it seems a little odd to have two very similar relationships split into two... but :)

    Arv
  • I tried with Decomposes too, but the result is not making sense to me:

  • edited October 2020

    Sorry, I missed the difference between RelatingObject and RelatedObjects

  • Good to hear you worked it out! A method I use to remember is that when parts (many) are aggregated into a whole (one), it's a many-to-one relationship. So it's RelatedObjects for the many parts, and RelatingObject for the one whole :)

  • I was hoping to be able to iterate trough all IfcSpace's and use IsDecomposedBy to find all elements assigned to it, but since it is empty do I have to iterate throgh all IfcElements and find the spaces via ContainedInStructure[0].RelatingStructure ?

  • To answer my self again: I guess "the inverse" of wall.ContainedInStructure[0].RelatingStructure is space.ContainsElements[0].RelatedElements

  • Task 1) "Change spatial container of elements from space to bulding storey" is complete:

    spaces = f.by_type('IfcSpace')
    
    for space in spaces:
        container = space.Decomposes[0].RelatingObject
        if container.is_a('IfcBuildingStorey'):
            buildingstorey = container
        if len(space.ContainsElements) > 0:
            contained_elements = space.ContainsElements[0].RelatedElements
            for contained_element in contained_elements:
                contained_element.ContainedInStructure[0].RelatingStructure = buildingstorey
    

    Next challenge is the ObjectLocation...

  • Here is what I think i need to do, in this example a wall which is placed relative to a space.

    • Get local placement of wall with ObjectPlacement
    • Get the coordinates with RelativePlacement
    • Get the local placement of space with PlacementRelTo
    • Get the coordinates of the space's local placement with RelativePlacement
    • Modify the coordinates of the walls local placement by adding the coordinates of the spaces local placement
    • Set the walls localPlacement.PlacementRelTo to the buildings storey's local placement.

      What I don't understand is this: When the IfcSpace is deleted, the corresponding IfcLocalPlacement is not. Why would deleting the IfcSpace affect the position of the IfcWall?
  • Regarding your code snippet, I'd recommend removing the check for building storey - as it may be a building storey, but could be also a different type of spatial element, like an IfcBuilding.

    You are correct that deleting the space shouldn't have an impact on the wall placement, since the wall is placed relative to the a local placement that is not deleted. Does it work?

    The reason I recommended replacing the coordinates was because there is an old IFC2X3 implementer agreement that says the spatial tree should mirror the local placement tree. This agreement is now no longer applicable, but... just in case an application expects it...

  • It didn't work in Solibri. No difference between just deleting the spaces and changing elements containers from space to storey before deleting the spaces.

  • @Einar perhaps I can take a look at your full script and file to help debug? Also up for a screenshare - I'm available on IRC live chat at https://blenderbim.org/community.html

  • I finally found some time and energy to look at this again. Here is the full script so far. Messing with the locations made this even worse, I guess I need to figure out how I can merge two RelativePlacements with different RefDirections. @Moult I can send the file if you want to take a quick look, but Mozilla send seems to be taken down.

    ifc_path = r"ARK.ifc"
    
    import ifcopenshell
    f = ifcopenshell.open(ifc_path)
    
    # Move element one level up from space in spatial tree:
    spaces = f.by_type('IfcSpace')
    for space in spaces:
        container = space.Decomposes[0].RelatingObject
        if len(space.ContainsElements) > 0:
            contained_elements = space.ContainsElements[0].RelatedElements
            for contained_element in contained_elements:
                contained_element.ContainedInStructure[0].RelatingStructure = container
    
    # Edit placements and delete the ones that places spaces
    local_placements = f.by_type('IfcLocalPlacement')
    for lp in local_placements:
        if lp.PlacesObject[0].is_a('IfcSpace'):
            lp_space = lp
            relative_placement = lp_space.RelativePlacement
            relative_to = lp_space.PlacementRelTo
            referenced_by = lp_space.ReferencedByPlacements
            for lp_ref in referenced_by:
                merged_location = (
                    lp_ref.RelativePlacement.Location.Coordinates[0] + lp_space.RelativePlacement.Location.Coordinates[0],
                    lp_ref.RelativePlacement.Location.Coordinates[1] + lp_space.RelativePlacement.Location.Coordinates[1],
                    lp_ref.RelativePlacement.Location.Coordinates[2] + lp_space.RelativePlacement.Location.Coordinates[2]
                )
                lp_ref.RelativePlacement.Location.Coordinates = merged_location
                lp_ref.PlacementRelTo = lp_space.PlacementRelTo
            f.remove(lp_space)
    
    # Delete spaces
    for space in f.by_type("ifcspace"):
        f.remove(space)
    
    # savne new file
    f.write(ifc_path[:-4]+"-space6.ifc")
    
  • For a simple 2D-case It seems like this rotation matrix is giving me the expected result:

    import numpy as np
    
    location_space = (1000,4000)
    location_ref = (5000,5000)
    refDir_space = (-1000,0)
    refDir_ref = (3000, -1000)
    
    def Transform(pt, v_from, v_to):
        a = v_to
        b = v_from
        cos=np.dot(a,b)/np.linalg.norm(a)/np.linalg.norm(b)
        sin=np.cross(a,b)/np.linalg.norm(a)/np.linalg.norm(b)
        R = np.array(((cos, -sin), (sin, cos)))
        return np.matmul(R,pt)
    loc_space_trans = Transform(location_space, refDir_space, refDir_ref)
    print(location_ref)
    print(location_ref[0]+loc_space_trans[0], location_ref[1]+loc_space_trans[1])
    


    But the same function implemented in the complete script widely scatters the model in Solibir:

    ifc_path = r"C:\Users\eibre\OneDrive - Norconsult Group\Documents\NB3_ARK.ifc"
    
    import numpy as np
    def rotate_point(pt, v_from, v_to):
        a = v_to
        b = v_from
        cos=np.dot(a,b)/np.linalg.norm(a)/np.linalg.norm(b)
        sin=np.cross(a,b)/np.linalg.norm(a)/np.linalg.norm(b)
        R = np.array(((cos, -sin), (sin, cos)))
        return np.matmul(R,pt)
    
    import ifcopenshell
    f = ifcopenshell.open(ifc_path)
    
    # Move element one level up in from space in spatial tree:
    spaces = f.by_type('IfcSpace')
    for space in spaces:
        container = space.Decomposes[0].RelatingObject
        if len(space.ContainsElements) > 0:
            contained_elements = space.ContainsElements[0].RelatedElements
            for contained_element in contained_elements:
                contained_element.ContainedInStructure[0].RelatingStructure = container
    
    # Edit placements and delete the ones that places spaces
    local_placements = f.by_type('IfcLocalPlacement')
    for lp in local_placements:
        if lp.PlacesObject[0].is_a('IfcSpace'):
            lp_space = lp
            relative_placement = lp_space.RelativePlacement
            referenced_by = lp_space.ReferencedByPlacements
            location = (lp_space.RelativePlacement.Location.Coordinates[0],lp_space.RelativePlacement.Location.Coordinates[1])
            refDir1 = (lp_space.RelativePlacement.RefDirection[0][0], lp_space.RelativePlacement.RefDirection[0][1])
            for lp_ref in referenced_by:
                refDir2 = (lp_ref.RelativePlacement.RefDirection[0][0], lp_ref.RelativePlacement.RefDirection[0][1])
                new_point = rotate_point(location, refDir1, refDir2)
                merged_location = (
                    float(lp_ref.RelativePlacement.Location.Coordinates[0] - new_point[0]),
                    float(lp_ref.RelativePlacement.Location.Coordinates[1] - new_point[1]),
                    float(lp_ref.RelativePlacement.Location.Coordinates[2] + lp_space.RelativePlacement.Location.Coordinates[2])
                )
                lp_ref.RelativePlacement.Location.Coordinates = merged_location
                lp_ref.PlacementRelTo = lp_space.PlacementRelTo
            f.remove(lp_space)
    
    # Delete spaces
    for space in f.by_type("ifcspace"):
        f.remove(space)
    
    # savne new file
    f.write(ifc_path[:-4]+"-spaceREM.ifc")
    

  • @Einar sorry for the delay in replying. Here's some code which simply swaps the spatial container. It seems to work for me:

    import ifcopenshell
    f = ifcopenshell.open('NB3_ARK.ifc')
    spaces = f.by_type('IfcSpace')
    for space in spaces:
        container = space.Decomposes[0].RelatingObject
        for rel in space.ContainsElements:
            rel.RelatingStructure = container
    f.write('NB3_ARK-no-spaces.ifc')
    
  • edited November 2020

    Thank you very much for the answer, I really appreciate all the work you are doing here.

    The swapping part works for me too, but if I go one step further and delete the spaces from the file, the basement of the building drops down in Solibri. Then I started to mess around with the local placements in case Solibri is following the "implementers agreement" you mentioned.

    Anyways, the specific case I want to solve is this:

    • Link an IFC from Archicad with spaces in Revit.
    • Problem: In Revit the spaces come in as solid generic model which is hard to remove in views. To get the ifcSpaces converted to Revit rooms we need to import the file (file>open) which takes too long time.
    • Possible solution: Split the IFC into to parts, one without spaces and one with spaces only. The first to be linked and the other to be imported.
  • @Einar the following works for me.

    import ifcopenshell
    f = ifcopenshell.open('NB3_ARK.ifc')
    spaces = f.by_type('IfcSpace')
    for space in spaces:
        container = space.Decomposes[0].RelatingObject
        for rel in space.ContainsElements:
            rel.RelatingStructure = container
        f.remove(space)
    f.write('NB3_ARK-no-spaces.ifc')
    
  • edited November 2020

    It's probably a Solibri Bug then. I used exactly the same code as you and got this in Solibri:

  • @Einar can you try with XBIM Xplorer, which is what I tested with?

  • edited November 2020

    It works both in Xbim Xplorer and Revit, which after all was the main goal.

    I guess this is the convention you mentioned earlier? Is this also saying that ArchiCads behavior with placement relative to IfcSpace is not correct?

    For IfcElement the convention applies that it shall be placed relative:
    to the local placement of its container (IfcSite, IfcBuilding, IfcBuildingStorey)
    it should be the same container element that is referenced by the IfcRelContainedInSpatialStructure containment relationship,¨

    from: IFC2x3/TC1/HTML/ifcgeometricconstraintresource/lexical/ifclocalplacement.htm

  • @Einar I see. That is indeed the convention (there is actually another document reference, but it's not important), but I would hesitate saying it's a bug with XBim Xplorer, as the implementer agreement is only valid for IFC2X3, but not IFC4. Your file is IFC2X3, so one could argue that XBim Xplorer is "allowed" to interpret it that way. It's not ideal, but it is an excuse. I'm curious if the same behaviour occurs for IFC4, and also curious why only the bottom storey dropped out but all other storeys remained correct.

    I don't see any bug with ArchiCAD. What makes you think ArchiCAD did something wrong?

  • edited November 2020

    I don't see any bug with ArchiCAD. What makes you think ArchiCAD did something wrong?

    Simply because IfcSpace (edited) isn't among the classes listed in the paranthesis above (IfcSite, IfcBuilding, IfcBuildingStorey). It's probably not wrong, but depends on how we choose to interpret the convention.

  • @Einar do you mean IfcSpace? If so, IfcSpace is indeed in the same "category" as IfcSite, IfcBuilding, etc. Those three are provided as examples. The struct rule lies in this sentence:

    it should be the same container element that is referenced by the IfcRelContainedInSpatialStructure containment relationship

  • You are right, I indeed meant IfcSpace.

    I'm curious if the same behaviour occurs for IFC4, and also curious why only the bottom storey dropped out but all other storeys remained correct.

    Me too! It would also be interesting to see if it was possible to fix it by replacing the IfcLocalPlacements, but maybe it would be easier to just ask Solibri.

  • @Einar I've linked this thread to someone I know who is closer to the Solibri guys and may be able to get a response, I'll update this thread if anything comes out of it.

  • edited November 2020

    @Einar regarding the transformations and without getting into the details of your implementation, are you sure you are rotating the objects with respect to the right center? It looks to me your are always rotating with respect to 0, 0, 0

  • edited November 2020

    @Jesusbill I'm not sure at all! I can try to explain how I think, and maybe you have an idea if it makes sense or not:
    As an example I have a wall that is positioned relative to a space in a certain coordinate system. The space is then positioned relative to a building storey. If both locations where in the same coordinate system it would just be a matter of adding the coordinates in each point togeter the get the walls placement relative to the building storey. That is not the case here because the points are given in two different coordinate systems, where the difference between them is the direction of the x-axis. My next step is to transform the point in the space to the coordinate system of the wall before they are added together. Isn't the coordinate systems origin the most logical point to rotate about?

Sign In or Register to comment.