Step-by-step tutorial

Recreation of the original IfcOpenHouse with the IfcOpenShell Python API


Welcome to this step-by-step tutorial on how to build a parametric house ๐Ÿ  from scratch in IFC with IfcOpenShell ๐Ÿš. Before getting started, you may want to pass by the home page, in case you still havenโ€™t. There, youโ€™ll find a general description about the tools ๐Ÿ› ๏ธ being used, installation instructions, as well as links to the original IfcOpenHouse project in C++, by Thomas Krijnen.

Note that this is a static website version of a Jupyter ๐Ÿช Notebook ๐Ÿ“’, optimized for readability and ease of access. If you wish to run the code, refer to the notebook itself, or alternatively you can always clone the repository.

This project relies on nbdev to fully integrate code and documentation from inside Jupyter Notebooks. However, if notebooks are not your thing and you just prefer pure Python ๐Ÿ, thereโ€™s also an autogenerated script.

Lastly, you may want to play a bit and navigate through the IFC.js preview of the house above these lines. Double click an element to gather entity info from the underlying IFC. Throughout this tutorial, youโ€™ll learn how to create each one of those elements and produce a valid IFC ๐Ÿš€.

Necessary imports

The following commands import most of the necessary libraries ๐Ÿ“š for this project. Thereโ€™s also an ios_utils module with some code placed into a separate *.py file, so as to focus on the important bits within this notebook. Feel free to also consult its content as an extra task, if desired. Open Cascade is not imported by default, since its usage will depend on the chosen method for the terrain creation.

import sys
from pathlib import Path
from collections import defaultdict
from mathutils import Vector
import numpy as np
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.owner
import ifcopenshell.api.owner.settings
import ifcopenshell.api.material
import ifcopenshell.api.geometry
import ifcopenshell.validate

from IfcOpenHouse.ios_utils import (
    IfcOpenShellPythonAPI, placement_matrix, clipping, ColourRGB, TerrainBuildMethod, 
    build_native_bspline_terrain, build_tesselated_occ_terrain, ios_entity_overwrite_hook
)
No stream support: No module named 'lark'

Definition of project data

The following information defines input data for the house creation. Note that it can be modified conveniently to parametrically reconstruct our IFC model. After such modification, remember to run again ๐Ÿƒโ€โ™€๏ธ๐Ÿƒ any cell depending on the modified parameters.

Furthermore, without requiring any action from your side, any IFC-related overwritten variable will be automatically garbage-collected ๐Ÿ—‘๏ธ by means of an audit hook (see next section).

# Data definition
project_name = 'IFC Open House'
author_details = {'given_name': 'Carlos', 'family_name': 'V', 'identification': 'CV'}
author_role = 'CIVILENGINEER'
organization_details = {'name': 'OSArch', 'identification': 'OSArch'}
site_name, building_name, storey_name = 'OSArch Land', 'Open house', 'Ground floor'

# All dimensions in meters
storey_size = Vector([10., 5., 3.])
wall_thickness = 0.36
footing_ledge = 0.05
footing_size = Vector([
    storey_size.x + 2 * (wall_thickness + footing_ledge),
    storey_size.y + 2 * (wall_thickness + footing_ledge),
    2.
])
roof_ledge = Vector([0.1, 0.22])
roof_thickness = 0.36
roof_angle = 45. # degrees
roof_angle_sin = float(np.sin(roof_angle * np.pi/180))
roof_angle_cos = float(np.cos(roof_angle * np.pi/180))
roof_height = float(
    (storey_size.y / 2 + wall_thickness + roof_ledge.y) * np.tan(roof_angle * np.pi / 180)
)
roof_size = Vector([
    storey_size.x + 2 * (wall_thickness + roof_ledge.x),
    storey_size.y + 2 * (wall_thickness + roof_ledge.y),
    roof_height
])
door_horizontal_offset = 1.6
window_base_height = 0.4
right_window_horizontal_offset = 2.
stair_width = 1.2

# Colours for surface styles
wall_colour = ColourRGB(.75, 0.73, 0.68)
footing_colour = ColourRGB(.38, 0.4, 0.42)
roof_colour = ColourRGB(.24, 0.08, 0.04)
terrain_colour = ColourRGB(.15, 0.25, 0.05)
door_colour = ColourRGB(.8, .8, .8)
window_colour = ColourRGB(.5, 0.4, 0.3, transparency=0.8)
stair_colour = ColourRGB(.45, 0.47, 0.56)

# Choice of terrain building method. Use NONE if unsure about the viewer capabilities.   
terrain_build_method = TerrainBuildMethod.TESSELATE_OCC_SHAPE

# Door and window geometric info is defined in a separate file due to its complexity
from IfcOpenHouse.opening_data import door_params, single_window_params, triple_window_params

Setting up a project

Everything is ready to start using IfcOpenShell ๐Ÿš. So without further ado, letโ€™s get down to business ๐Ÿ‘ฉโ€๐Ÿญ:

# Little trickery to ease the use of the ifcopenshell.api when scripting
ios = IfcOpenShellPythonAPI()  #q1: thoughts about a data-scientish "import ifcopenshell.api as ios"?

# standard use      -> ifcopenshell.api.run('root.create_entity', file, ifc_class='IfcWall')
# with the trickery -> ios.root.create_entity(file, ifc_class='IfcWall')

# It will reduce the overall string overhead, as well as the length of the API calls
# However, it will not help with static typing autocomplete and help
# Bear in mind that currently, this is not a canonical use of IfcOpenShell


# Setting up the project
file = ios.project.create_file(version='IFC4')
# Don't use 2X3 in 2023! It's terribly outdated and lacks many useful classes. This simple
# project uses many >=IFC4 features, and hence selecting 'IFC2X3' here would only lead to issues.
# Pending to use 4x3 (much better docs) when ios defaults to IFC4X3_TC1 and IFC.js supports it

project = ios.root.create_entity(file, ifc_class='IfcProject', name=project_name)
ios.project.assign_declaration(file, definition=project, relating_context=project)  #q2: from my ignorance, is this necessary?
ios.unit.assign_unit(
    file, length={'is_metric': True, 'raw': 'METERS'}, area={'is_metric': True, 'raw': 'METERS'},
    volume={'is_metric': True, 'raw': 'METERS'}
)
ctx = ios.context.add_context(file, context_type='Model')
body = ios.context.add_context(  #q3: isn't this screaming for "context.add_subcontext"? also, context_type may be redundant
    file, context_type='Model', context_identifier='Body', target_view='MODEL_VIEW', parent=ctx
)

# We allow for live overwriting of IfcOpenShell entities within the notebook environment
# Only use this sorcery when experimenting in Jupyter Notebooks, never in production
sys.addaudithook(
    ios_entity_overwrite_hook(file, sys.modules[__name__], do_not_delete=[project, ctx, body])
)

Ok, we just got started! The file object ๐Ÿ“ we just created will be the main binding agent for the rest of the notebook. As it can be seen, most API calls are self-explanatory. In order to look up for the most updated documentation, check out the following links:

https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/index.html

https://blenderbim.org/docs-python/ifcopenshell-python/code_examples.html

In case you might also want to have a look at the Python source code, this is the place to go:

https://github.com/IfcOpenShell/IfcOpenShell/tree/v0.7.0/src/ifcopenshell-python/ifcopenshell/api

Optional definition of owner data

IFCs can hold data from stakeholders, like a person ๐Ÿค“ or an organisation ๐Ÿ‘ฅ. With the last two lines, weโ€™re assigning the user here defined to the rest of the IfcOpenShell API actions taken from now on, as well as IfcOpenShell as the application.

application = ios.owner.add_application(file)
person = ios.owner.add_person(file, **author_details)
organisation = ios.owner.add_organisation(file, **organization_details)
user = ios.owner.add_person_and_organisation(file, person=person, organisation=organisation)
ios.owner.add_role(file, assigned_object=organisation, role=author_role)
actor = ios.owner.add_actor(file, actor=user)
ifcopenshell.api.owner.settings.get_user = lambda x: user
ifcopenshell.api.owner.settings.get_application = lambda x: application

Setting up the project spatial structure

Thatโ€™s how we define an outline of a site ๐Ÿ˜๏ธ, containing a building ๐Ÿ , which in turn contains a building storey ๐Ÿ›‹๏ธ:

site = ios.root.create_entity(file, ifc_class='IfcSite', name=site_name)
ios.aggregate.assign_object(file, product=site, relating_object=project)

building = ios.root.create_entity(file, ifc_class='IfcBuilding', name=building_name)
ios.aggregate.assign_object(file, product=building, relating_object=site)

storey = ios.root.create_entity(file, ifc_class='IfcBuildingStorey', name=storey_name)
ios.aggregate.assign_object(file, product=storey, relating_object=building);

Property Sets โœ๏ธ can be easily added with pset.add_pset and pset_edit_pset. Here, we set the total area of the site to 20x20 = 400 m2.

pset_site_common = ios.pset.add_pset(file, product=site, name='Pset_SiteCommon')
ios.pset.edit_pset(file, pset=pset_site_common, properties={'TotalArea': 400.})

Creating our first wall

The following statement will create a brand new wall entity ๐Ÿงฑ๐Ÿงฑ within our IFC file ๐Ÿ“:

south_wall = ios.root.create_entity(file, ifc_class='IfcWall', name='South wall', predefined_type='SOLIDWALL')

As expected, IfcOpenShell ๐Ÿš is tightly coupled to the underlying IFC Schema. Hence, opening a browser tab with the IFC official documentation is not at all a bad idea when using it.

  • PRO TIP ๐Ÿ‘‰ IFC4X3 docs are far better than previous versions!

Donโ€™t be scared of the bSI documentation! ๐Ÿ‘ป If youโ€™ve reached this point already, youโ€™re very close to turbocharge ๐Ÿš€ your IfcOpenShell coding skills ๐Ÿ‘ฉ๐Ÿฝโ€๐Ÿ’ป by relying on the underlying IFC schema.

In order to better illustrate it with our new wall ๐Ÿงฑ๐Ÿงฑ, letโ€™s consider the following. If we want to know among which predefined types we can choose for a certain IfcWall, going to its documentation page, scrolling to โ€œPredefined Typeโ€, and clicking into IfcWallTypeEnum will give us the appropriate list: MOVABLE, PARAPET, PARTITIONING, PLUMBINGWALL, RETAININGWALL, SHEAR, SOLIDWALL and WAVEWALL.

Clearly, our traditional structural wall for the house needs to be classified as a SOLIDWALL. As always in IFC, a predefined type could also be set as USERDEFINED if none of the enumeration options fits our use case, in which case we could monkey-patch ๐Ÿต the type as south_wall.ObjectType = 'CUSTOM TYPE'.

The previous kind of reasoning and consulting of the documentation is to be followed for the rest of this notebook. In case that at some point we needed to know which IFC class a certain IfcOpenShell ๐Ÿš entity corresponds to (so as to know where in the IFC documentation to look at), the is_a() method will give us the answer:

south_wall.is_a()
'IfcWall'

This notebook environment ๐Ÿ“’ highly encourages experimentation ๐Ÿ‘จโ€๐Ÿ”ฌ, like in the statement above. Feel free to add a new cell at any point to further explore the newly created variables. Another very useful method is get_info(), which returns a dictionary with all the entity attributes:

south_wall.get_info()
{'id': 41,
 'type': 'IfcWall',
 'GlobalId': '1LXtQ1Bub30gLlLbSNcmg8',
 'OwnerHistory': #40=IfcOwnerHistory(#19,#16,.READWRITE.,.ADDED.,1695242677,#19,#16,1695242677),
 'Name': 'South wall',
 'Description': None,
 'ObjectType': None,
 'ObjectPlacement': None,
 'Representation': None,
 'Tag': None,
 'PredefinedType': 'SOLIDWALL'}

As seen previously with user defined types, entity attributes can be easily monkey-patched ๐Ÿต (weโ€™re in Python after all! ๐Ÿ). It will work if the attribute exists in the corresponding IFC Schema. For instance, if we wanted to set the โ€œDescriptionโ€ of our wall, we would do it as follows:

south_wall.Description = 'This is my first wall with the IfcOpenShell Python API!'

In any case, the IfcOpenShell Python API ๐Ÿš takes care of most low level tasks. Actions like the generation of global IDs, the assignment of owner history to entities or the setup of entity relationships for geometric representations or placements are already dealt with automatically ๐Ÿ‘Œ๐Ÿป.

Now, letโ€™s assign the wall to the ground floor storey by means of spatial.assign_container:

ios.spatial.assign_container(file, product=south_wall, relating_structure=storey);

Itโ€™s important to keep in mind the IFC distinction between geometry and semantics. Up to now, weโ€™ve created an IfcWall entity ๐Ÿงฑ๐Ÿงฑ, only containing a name and a predefined type. However, the geometric representations of the actual wall will exist within their own IfcShapeRepresentation entity ๐Ÿ—ฟ. That is to say, one thing is the creation of the IfcWall, but then its geometry needs to be created as a separate IFC class, which in turn will be linked to the former.

The API call geometry.add_wall_representation generates a swept solid (IfcExtrudedAreaSolid), an implicit geometry representation which requires less memory ๐Ÿชถ than explicit BREPS or tesselated representations. You should know that IFC supports all of them ๐Ÿš€, among other fancy paradigms like Constructive Solid Geometry or BSplines. All in all, for the typical walls/slabs or beams/columns typically used in the AEC field, an IfcExtrudedAreaSolid is difficult to beat, and thatโ€™s why the IfcOpenShell API ๐Ÿš defaults to it. You donโ€™t necessarily need to be aware of the low level definition of a wall geometry, but just let the library do the job for you ๐Ÿ˜Ž.

south_wall_representation = ios.geometry.add_wall_representation(
    file, context=body, length=storey_size.x + 2 * wall_thickness, height=storey_size.z, 
    thickness=wall_thickness
)
ios.geometry.assign_representation(file, product=south_wall, representation=south_wall_representation)
ios.geometry.edit_object_placement(
    file, product=south_wall, matrix=placement_matrix(
        [-storey_size.x / 2 - wall_thickness, -wall_thickness / 2, 0.]
    )
);  #q4: why a matrix if Y is going to be ignored? why not just pass the placement coords + optionals x_local, z_local and scale?

Placements are assigned to a product (IfcProduct), which in IFC is anything existing within the physical space ๐Ÿ›ฐ๏ธ, like a wall, but also an annotation or a spatial element.

Now, letโ€™s add a surface style in order to assign a colour to the wall representation. If eager to learn more about styles and colour in IFC, thereโ€™s an in-depth explanation by Dion Moult in this link, highly recommended ๐Ÿ”. Alternatively, you may also check out the API docs.

wall_style = ios.style.add_style(file)
ios.style.add_surface_style(
    file, style=wall_style, ifc_class='IfcSurfaceStyleShading', attributes=wall_colour.info
)
ios.style.assign_representation_styles(
    file, shape_representation=south_wall_representation, styles=[wall_style]
);

Creating a footing

Letโ€™s repeat the same process again ๐Ÿ” to create the structural footing ๐Ÿฆถ. Creation of the element entity (an IfcElement is just a tangible IfcProduct, like a wall or a footing, but not an annotation or an IfcBuildingStorey), assignation of the storey, creation of the swept solid representation, linkage ๐Ÿ”— to the element, setting of the placement and addition of a surface style:

footing = ios.root.create_entity(file, ifc_class='IfcFooting', name='Footing', predefined_type='STRIP_FOOTING')
ios.spatial.assign_container(file, product=footing, relating_structure=storey)
footing_representation = ios.geometry.add_wall_representation(
    file, context=body, length=footing_size.x, height=footing_size.z, thickness=footing_size.y
)
ios.geometry.assign_representation(file, product=footing, representation=footing_representation)
ios.geometry.edit_object_placement(
    file, product=footing, matrix=placement_matrix(
        [-footing_size.x/2, -wall_thickness/2 - footing_ledge, -footing_size.z]
    )
)
footing_style = ios.style.add_style(file)
ios.style.add_surface_style(
    file, style=footing_style, ifc_class='IfcSurfaceStyleShading', attributes=footing_colour.info
)
ios.style.assign_representation_styles(
    file, shape_representation=footing_representation, styles=[footing_style]
);

Voiding the walls with window openings

The IfcOpenShell API ๐Ÿš also provides utilities to void elements ๐Ÿ•ณ๏ธ. An IFC representation is needed, which will correspond to an opening that will void another element. It is important to note that in accordance with the schema specs, each opening can only void one single element.

When defining the geometric representation of the voids, it is advisable to make them deeper than the element theyโ€™re voiding (along its perpendicular axis). If they measured exactly the same, computer intrinsic floating point error could leave a thin layer without voiding ๐Ÿ™…โ€โ™‚๏ธ.

west_void_margin = 0.5
west_opening = ios.root.create_entity(file, ifc_class='IfcOpeningElement')
west_opening_width = 2 * single_window_params['overall_width']
wo_representation = ios.geometry.add_wall_representation(
    file, context=body, 
    length=triple_window_params['overall_width'] + west_void_margin, 
    height=triple_window_params['overall_height'],
    thickness=west_opening_width
)
ios.geometry.assign_representation(file, product=west_opening, representation=wo_representation)
west_opening_coords = [
    (
        -storey_size.x / 2 - wall_thickness - west_void_margin 
        + single_window_params['lining_properties']['LiningOffset']
    ), 
    (
        -west_opening_width / 2 - wall_thickness / 3
        + triple_window_params['lining_properties']['LiningOffset'] 
        + triple_window_params['lining_properties']['LiningDepth']
    ), 
    window_base_height
]
ios.geometry.edit_object_placement(
    file, product=west_opening, matrix=placement_matrix(west_opening_coords)
)
ios.void.add_opening(file, opening=west_opening, element=south_wall)

south_opening = ios.root.create_entity(file, ifc_class='IfcOpeningElement')
south_opening_width = 3.
so_representation = ios.geometry.add_wall_representation(
    file, context=body, length=single_window_params['overall_width'], 
    height=single_window_params['overall_height'], thickness=south_opening_width
)
ios.geometry.assign_representation(file, product=south_opening, representation=so_representation)
ios.geometry.edit_object_placement(
    file, product=south_opening, matrix=placement_matrix(
        [right_window_horizontal_offset, -south_opening_width / 2, window_base_height]
    )
)
ios.void.add_opening(file, opening=south_opening, element=south_wall);

Creating the roof

The gable roof ๐Ÿ›– consists of two slabs, which will be aggregated into the roof entity. Since both slabs are identical, except for rotation and mirroring operations ๐Ÿชž, their IfcSlab entities will point to identical representations, though with different placements. These representations will be again an IfcExtrudedAreaSolid, created by add_wall_representation, but in this case the extruded direction will be at a certain angle. That way, the slab prism wonโ€™t have its faces perpendicular to each other, which better suits a gable roof geometry.

Local axes are set by the calls to geometry.edit_object_placement, with a local Z equivalent to global Y. That effectively rotates the representation by 90ยบ so that its previously horizontal โ€œbaseโ€ and โ€œtopโ€ now become vertical. Lastly, north-side slab is also mirrored along the X axis, so that both slabs are facing each other and the gable roof is successfully constructed ๐Ÿ‘Œ๐Ÿป.

roof = ios.root.create_entity(file, ifc_class='IfcRoof', name='Roof', predefined_type='GABLE_ROOF')
ios.spatial.assign_container(file, product=roof, relating_structure=storey)
roof_representation_south = ios.geometry.add_wall_representation(
    file, context=body, length=roof_size.x, height=roof_size.y / 2, thickness=roof_thickness, 
    x_angle=(roof_angle) * np.pi / 180
)
roof_representation_north = ifcopenshell.util.element.copy_deep(file, roof_representation_south)
#q5: add_slab_representation doesn't accept width and depth? isn't it strange calling it add_wall
#if we're using it for everything? do "add_wall_representation", "add_slab_representation" and 
#"add_profile_representation" add all of them an IfcExtrudedAreaSolid? Would it make any sense to 
#add a single "add_extruded_representation" instead? As a higher level API call than shape_builder

roof_downward_offset = (roof_ledge.y + wall_thickness / 2) * np.tan(roof_angle * np.pi / 180)

south_roof = ios.root.create_entity(file, ifc_class='IfcSlab', name='South roof', predefined_type='ROOF')
ios.geometry.assign_representation(file, product=south_roof, representation=roof_representation_south)
ios.geometry.edit_object_placement(
    file, product=south_roof, matrix=placement_matrix(
        [roof_size.x / 2, -roof_ledge.y - wall_thickness / 2, storey_size.z - roof_downward_offset], 
        x_local=[-1., 0., 0.], z_local=[0., 1., 0.]
    )
)

north_roof = ios.root.create_entity(file, ifc_class='IfcSlab', name='North roof', predefined_type='ROOF')
ios.geometry.assign_representation(file, product=north_roof, representation=roof_representation_north)
ios.geometry.edit_object_placement(
    file, product=north_roof, matrix=placement_matrix(
        [
            -roof_size.x / 2, 
            (storey_size.y + wall_thickness) / 2, 
            storey_size.z - roof_downward_offset + roof_size.z + roof_thickness / roof_angle_cos
        ], x_local=[1., 0., 0.], z_local=[0., 1., 0.]
    )
)

ios.aggregate.assign_object(file, product=south_roof, relating_object=roof)
ios.aggregate.assign_object(file, product=north_roof, relating_object=roof)

roof_style = ios.style.add_style(file)
ios.style.add_surface_style(
    file, style=roof_style, ifc_class='IfcSurfaceStyleShading', attributes=roof_colour.info
)
ios.style.assign_representation_styles(
    file, shape_representation=roof_representation_south, styles=[roof_style]
)
ios.style.assign_representation_styles(
    file, shape_representation=roof_representation_north, styles=[roof_style]
);

Creating the remaining walls

Since both geometries are identical, we can duplicate the existing south wall representation into a new northern side wall ๐Ÿงฑ๐Ÿงฑ. Again, the placement will position the new representation in the relevant coordinates.

north_wall_representation = ifcopenshell.util.element.copy_deep(file, south_wall_representation)
north_wall = ios.root.create_entity(file, ifc_class='IfcWall', name='North wall', predefined_type='SOLIDWALL')
ios.spatial.assign_container(file, product=north_wall, relating_structure=storey)
ios.geometry.assign_representation(file, product=north_wall, representation=north_wall_representation)
ios.geometry.edit_object_placement(
    file, product=north_wall, matrix=placement_matrix(
        [-storey_size.x/2 - wall_thickness, storey_size.y + wall_thickness / 2, 0.]
    )
)
ios.style.assign_representation_styles(
    file, shape_representation=north_wall_representation, styles=[wall_style]
);

A new east wall entity is added, along with its representation. This time, though, the geometry is not just an extruded rectangle, but due to the gable roof shape, a pair of clipping operations ๐Ÿ–‡๏ธ need to be performed on top. The IfcOpenShell API ๐Ÿš easily allows us to do so by passing some planes with the clippings argument to add_wall_representation.

east_wall = ios.root.create_entity(
    file, ifc_class='IfcWall', name='East wall', predefined_type='SOLIDWALL'
)
ios.spatial.assign_container(file, product=east_wall, relating_structure=storey)

south_roof_clipping = clipping(
    [0., wall_thickness / 2, storey_size.z], x_dir=[1., 0., 0.], 
    z_dir=[0., -roof_angle_sin, roof_angle_cos]
)
north_roof_clipping = clipping(
    [0., storey_size.y + 3 / 2 * wall_thickness, storey_size.z], x_dir=[1., 0., 0.], 
    z_dir=[0., roof_angle_sin, roof_angle_cos]
)

east_wall_representation = ios.geometry.add_wall_representation(
    file, context=body, length=wall_thickness, height=storey_size.z + roof_size.z, 
    thickness=storey_size.y + 2 * wall_thickness, clippings=[south_roof_clipping, north_roof_clipping]
)

ios.geometry.assign_representation(file, product=east_wall, representation=east_wall_representation)
ios.geometry.edit_object_placement(
    file, product=east_wall, matrix=placement_matrix([storey_size.x / 2, -wall_thickness / 2, 0.])
)

ios.style.assign_representation_styles(
    file, shape_representation=east_wall_representation, styles=[wall_style]
);

The methodology of duplicating a representation is is once more time ๐Ÿ” followed for the remaining wall, and an opening ๐Ÿ•ณ๏ธ is defined on it. Analogously, the opening is exactly equal to the previously defined west_opening. However, since in IFC, an opening can only be used to create a single void within a single element, a new identical opening entity is created.

west_wall = ios.root.create_entity(
    file, ifc_class='IfcWall', name='West wall', predefined_type='SOLIDWALL'
)
ios.spatial.assign_container(file, product=west_wall, relating_structure=storey)

west_wall_representation = ifcopenshell.util.element.copy_deep(file, east_wall_representation)
ios.geometry.assign_representation(file, product=west_wall, representation=west_wall_representation)
ios.geometry.edit_object_placement(
    file, product=west_wall, matrix=placement_matrix(
        [-storey_size.x / 2 - wall_thickness, -wall_thickness / 2, 0.]
    )
)

west_opening_copy = ifcopenshell.util.element.copy_deep(file, west_opening)
ios.geometry.edit_object_placement(
    file, product=west_opening_copy, matrix=placement_matrix(west_opening_coords)
)
ios.void.add_opening(file, opening=west_opening_copy, element=west_wall)

ios.style.assign_representation_styles(
    file, shape_representation=west_wall_representation, styles=[wall_style]
);

We also indicate that walls ๐Ÿงฑ๐Ÿงฑ are connected ๐Ÿ”— between each other by means of geometry.connect_path.

connection_args = {'relating_connection': 'ATEND', 'related_connection': 'ATSTART'}

rel_connect_paths = [
    ios.geometry.connect_path(
        file, relating_element=south_wall, related_element=east_wall, **connection_args
    ),
    ios.geometry.connect_path(
        file, relating_element=east_wall, related_element=north_wall, **connection_args
    ),
    ios.geometry.connect_path(
        file, relating_element=north_wall, related_element=west_wall, **connection_args
    ),
    ios.geometry.connect_path(
        file, relating_element=west_wall, related_element=south_wall, **connection_args
    )
]

#q7: do IfcRelConnectsPathElements with ConnectionGeometry work in any viewer? is this done like this?
# Original IfcOpenHouse had half a wall thickness less of extension per wall end, I bet it's better for
# qto's, but how is the way to properly make connections and to have a proper viz of them in IFC?
point_list = file.create_entity('IfcCartesianPointList2D', CoordList = [[-1., -1.], [1., 1.]])
curve_on_relating = file.create_entity('IfcIndexedPolyCurve', Points=point_list)
connection_curve = file.create_entity(
    'IfcConnectionCurveGeometry', CurveOnRelatingElement=curve_on_relating
)

for path in rel_connect_paths:
    path.ConnectionGeometry = connection_curve

Creating the terrain as a NURBS surface

This section reproduces the terrain of the original IfcOpenHouse ๐Ÿก, defined by a NURBS surface โ›ฐ๏ธ. The variable terrain_build_method can be set to one of three enum values:

  • TerrainBuildMethod.NATIVE_BSPLINE: Uses an advanced BREP with the native IfcBSplineSurfaceWithKnots entity, existing from IFC4 onwards.
  • TerrainBuildMethod.TESSELATE_OCC_SHAPE: Constructs the shape in Open Cascade and tesselate it with the help of IfcOpenShell ๐Ÿš.
  • TerrainBuildMethod.NONE: You may also safely skip the terrain part.

The terrain with a fancy NURBS surface may be challenging to be correctly displayed within some IFC viewers. On the other hand, the tesselation method was followed on the original IfcOpenHouse for IFC2x3, but note that it requires the installation of Open Cascade (pythonocc-core package ๐Ÿ“ฆ). In case you want to check out how exactly these methods work, refer to the ios_utils module, but be advised they are quite complex!

  • CURIOUS NOTE ๐Ÿ‘‰ The tesselation method will create an IfcOpenShell entity with the IfcOpenShell library! ๐Ÿš๐Ÿš๐Ÿš

  • PRO TIP ๐Ÿ‘‰ If overwhelmed, you may just choose NONE and skip this fancy terrain section, since itโ€™s the least connected with real use cases in the industry.

terrain_control_points = [  # obtained from the original IfcOpenHouse
    [( -10., -10., -4.13), ( -10., -4.33, -4.13), (-10., 0., -5.13), ( -10., 4.33, -7.13), ( -10., 10., -7.13)],
    [(-3.33, -10., -5.13), (-7.67, -3.67,    5.), ( -9., 0.,    1.), (-7.67, 7.67,    6.), (-3.33, 10., -4.13)],
    [(   0., -10., -5.53), (   0., -3.67,    3.), (  0., 0.,  -12.), (   0., 7.67,   1.5), (   0., 10., -4.13)],
    [( 3.33, -10., -6.13), ( 7.67, -3.67,    6.), (  9., 0.,    5.), ( 7.67,   9.,    7.), ( 3.33, 10., -4.13)],
    [(  10., -10., -6.13), (  10., -4.33, -5.13), ( 10., 0., -4.13), (  10., 4.33, -4.13), (  10., 10., -8.13)]
]

degree, multiplicity = 4, 5

if terrain_build_method == TerrainBuildMethod.NATIVE_BSPLINE:
    terrain_representation = build_native_bspline_terrain(
        file, body, terrain_control_points, degree, multiplicity
    )
    
elif terrain_build_method == TerrainBuildMethod.TESSELATE_OCC_SHAPE:
    deflection = 0.01
    terrain_representation = build_tesselated_occ_terrain(
        file, body, terrain_control_points, degree, multiplicity, deflection
    )

if terrain_build_method != TerrainBuildMethod.NONE:
    # If we have produced an IfcShapeRepresentation, by any of the previous methods, we assign it
    # to the IfcSite and also assign it a style.
    
    ios.geometry.assign_representation(file, product=site, representation=terrain_representation)
    terrain_style = ios.style.add_style(file)
    ios.style.add_surface_style(
        file, style=terrain_style, ifc_class='IfcSurfaceStyleShading', attributes=terrain_colour.info
    )
    ios.style.assign_representation_styles(
        file, shape_representation=terrain_representation, styles=[terrain_style]
    );

Creation of material layer sets

Firstly, an IfcMaterialLayerSet is added, with a unique brick layer ๐Ÿงฑ consisting of a newly defined brick material for the entire wall thickness. If there were walls consisting of more layers, they could be simply added with the help of material.add_layer. Then, this new layer set is assigned to each wall.

brick = ios.material.add_material(file, name='Brick', category='brick')
wall_layerset = ios.material.add_material_set(file, name='Wall', set_type='IfcMaterialLayerSet')
brick_layer = ios.material.add_layer(file, layer_set=wall_layerset, material=brick)
ios.material.edit_layer(file, layer=brick_layer, attributes={'LayerThickness': wall_thickness})

#q9: Original IfcOpenHouse used usages. In current IFC, are usages without types a good idea? 
# Do I add usages or types?

for wall in [south_wall, north_wall, east_wall, west_wall]:
    ios.material.assign_material(file, product=wall, type='IfcMaterialLayerSet', material=wall_layerset)

Creation of a stair flight

Similarly to the previously created entities, a new IfcStairFlight is created ๐Ÿชœ, in this case with an extruded profile representation.

stair_flight_params = {'NumberOfRisers': 2, 'NumberOfTreads': 2, 'RiserHeight': 0.2, 'TreadLength': 0.25}
stair_flight = ios.root.create_entity(file, ifc_class='IfcStairFlight', name='Main entrance stair', predefined_type='STRAIGHT')
ios.spatial.assign_container(file, product=stair_flight, relating_structure=storey)
for attr, value in stair_flight_params.items():
    setattr(stair_flight, attr, value)  # the entity is monkey-patched with its IFC compliant attributes

stair_points = []  # Staircase cross-sectional profile 2D points
for step in range(stair_flight_params['NumberOfRisers'] + 1):
    next_step = 0 if step == stair_flight_params['NumberOfRisers'] else step + 1
    stair_points.extend([
        (step * stair_flight_params['TreadLength'], step * stair_flight_params['RiserHeight']),
        (next_step * stair_flight_params['TreadLength'], step * stair_flight_params['RiserHeight']),
    ])

stair_flight_curve = file.create_entity(
    'IfcIndexedPolyCurve', file.create_entity('IfcCartesianPointList2D', stair_points), None, False
)
stair_flight_profile = file.create_entity('IfcArbitraryClosedProfileDef', 'AREA', None, stair_flight_curve)
stair_flight_representation = ios.geometry.add_profile_representation(
    file, context=body, profile=stair_flight_profile, depth=stair_width
)

ios.geometry.assign_representation(file, product=stair_flight, representation=stair_flight_representation)
ios.geometry.edit_object_placement(
    file, product=stair_flight, matrix=placement_matrix(
        [footing_size.x / 2, door_horizontal_offset + (door_params['overall_width'] - stair_width) / 2, 0.],
        z_local=[0., 1., 0.]
    )
)

stair_style = ios.style.add_style(file)
ios.style.add_surface_style(
    file, style=stair_style, ifc_class='IfcSurfaceStyleShading', attributes=stair_colour.info
)
ios.style.assign_representation_styles(
    file, shape_representation=stair_flight_representation, styles=[stair_style]
);

The house needs a doorโ€ฆ

For the door ๐Ÿšช creation, we can rely on geometry.add_door_representation, which will produce a high quality representation and is highly customizable.

Note that when this API function is used outside of Blender, it requires the mathutils package to be installed within the Python environment.

door = ios.root.create_entity(file, ifc_class='IfcDoor', name='Main door', predefined_type='DOOR')
ios.spatial.assign_container(file, product=door, relating_structure=storey)

door_opening = ios.root.create_entity(file, ifc_class='IfcOpeningElement')
door_opening_representation = ios.geometry.add_wall_representation(
    file, context=body, length=door_params['overall_width'], height=door_params['overall_height'], 
    thickness=door_params['overall_width']
)
ios.geometry.assign_representation(
    file, product=door_opening, representation=door_opening_representation
)
ios.geometry.edit_object_placement(
    file, product=door_opening, matrix=placement_matrix(
        [storey_size.x / 2 - door_params['overall_width'] / 2 , door_horizontal_offset, 0.]
    )
)
ios.void.add_opening(file, opening=door_opening, element=east_wall)

door_representation = ios.geometry.add_door_representation(  # requires mathutils
    file, context=body, **door_params
)
ios.geometry.edit_object_placement(
    file, product=door, matrix=placement_matrix(
        [storey_size.x / 2 + wall_thickness / 4, door_horizontal_offset, 0.],
        x_local=[0., 1., 0.]  # door is rotated into the east wall
    )
)
ios.geometry.assign_representation(file, product=door, representation=door_representation)
ios.void.add_filling(file, opening=door_opening, element=door)

door_style = ios.style.add_style(file)
ios.style.add_surface_style(
    file, style=door_style, ifc_class='IfcSurfaceStyleShading', attributes=door_colour.info
)
ios.style.assign_representation_styles(
    file, shape_representation=door_representation, styles=[door_style]
);

โ€ฆ and some windows

Window representations ๐ŸชŸ rely on the geometry.add_window_representation call, which is also highly customizable. It also depends on the mathutils package.

window_right = ios.root.create_entity(
    file, ifc_class='IfcWindow', name='Right window', predefined_type='WINDOW'
)
window_right.PartitioningType = single_window_params['partition_type']  #q6: couldn't this fit into the previous call?
window_right_representation = ios.geometry.add_window_representation(  # requires mathutils
    file, context=body, **single_window_params
)
ios.spatial.assign_container(file, product=window_right, relating_structure=storey)
ios.geometry.edit_object_placement(
    file, product=window_right, matrix=placement_matrix(
        [right_window_horizontal_offset, -wall_thickness / 3, window_base_height]
    )
)
ios.geometry.assign_representation(
    file, product=window_right, representation=window_right_representation
)
ios.void.add_filling(file, opening=south_opening, element=window_right)

window_west = ios.root.create_entity(
    file, ifc_class='IfcWindow', name='West window', predefined_type='WINDOW'
)
window_west.PartitioningType = single_window_params['partition_type']
window_west_representation = ios.geometry.add_window_representation(
    file, context=body, **single_window_params
)
ios.spatial.assign_container(file, product=window_west, relating_structure=storey)
ios.geometry.edit_object_placement(
    file, product=window_west, matrix=placement_matrix(
        [
            -storey_size.x / 2 - wall_thickness,
            (
                + single_window_params['overall_width'] - wall_thickness / 3 
                + triple_window_params['lining_properties']['LiningOffset'] 
                + triple_window_params['lining_properties']['LiningDepth']
            ), 
            window_base_height
        ], x_local=[0., -1., 0.]
    )
)
ios.geometry.assign_representation(
    file, product=window_west, representation=window_west_representation
)
ios.void.add_filling(file, opening=west_opening_copy, element=window_west)

window_left = ios.root.create_entity(
    file, ifc_class='IfcWindow', name='Left Window', predefined_type='WINDOW'
)
window_left.PartitioningType = triple_window_params['partition_type']
window_left_representation = ios.geometry.add_window_representation(
    file, context=body, **triple_window_params
)
ios.spatial.assign_container(file, product=window_left, relating_structure=storey)
ios.geometry.edit_object_placement(
    file, product=window_left, matrix=placement_matrix(
        [
            (
                -storey_size.x / 2 - wall_thickness
                + single_window_params['lining_properties']['LiningOffset']
            ),
            -wall_thickness / 3,
            window_base_height]
    )
)
ios.geometry.assign_representation(
    file, product=window_left, representation=window_left_representation
)
ios.void.add_filling(file, opening=west_opening, element=window_left)

window_style = ios.style.add_style(file)
ios.style.add_surface_style(
    file, style=window_style, ifc_class='IfcSurfaceStyleShading', attributes=window_colour.info
)
ios.style.assign_representation_styles( #q10: Will it be possible to assign different styles to the panel and the lining?
    file, shape_representation=window_right_representation, styles=[window_style]
)
ios.style.assign_representation_styles(
    file, shape_representation=window_west_representation, styles=[window_style]
)
ios.style.assign_representation_styles(
    file, shape_representation=window_left_representation, styles=[window_style]
);

Checking the validity of our file

After all the previous steps, we have programmatically defined an IFC file. But, is it valid? โœ… have we messed up at some point? IfcOpenShell ๐Ÿš provides the validate method to validate a certain file against its corresponding schema:

json_logger = ifcopenshell.validate.json_logger()
ifcopenshell.validate.validate(file, json_logger)
json_logger.statements[:min(3, len(json_logger.statements))]  # Showing only first 3 here
[]

Letโ€™s check how many different validation issues we get by converting to a Python ๐Ÿ set. If everything is fine, the set should be empty:

set([issue['attribute'] for issue in json_logger.statements])
set()

Visualization with IFC.js

It is worth noting that a Jupyter ๐Ÿช Notebook ๐Ÿ“’ runs in the browser ๐Ÿ›œ, and is therefore subject to the laws of web development. Moreover, a couple of handy โ€œmagic methodsโ€ %%html and %%js can be placed at the start of a code cell to run HTML or JavaScript. Thanks to it, we can create a new div and use a simple prebundled script using IFC.js to render our brand new IFC on it. You may have a look at the simple JS script here.

<div class="info-panel hidden" id="id-info-div">
    <p class="info" id="id-info-p"></p>
</div>
<div id="ifcjs-container"></div>
//# Run this cell to load the IfcOpenShell file with IFC.js in the div above
//# Rerun cells as needed and run this cell again to reload viz

async function loadIfcFromJupyter(out){
    let ifcStr = out.content.data["text/plain"];
    loadIfc(ifcStr, true);
}

let callbacks = {iopub: {output: loadIfcFromJupyter}};
IPython.notebook.kernel.execute("file.to_string()", callbacks, {silent: false});

If browsing through the website version, the preview has been already shown at the top of the page. Do you fancy trying with a 60ยบ roof? or taller walls? Check out the notebook version in order to be able to produce those modified versions by yourself ๐Ÿ˜‰.

 

IfcOpenHouse generation samples

Saving our IFC file

Finally, we can dump the programmatically generated file into disk ๐Ÿš€:

ifc_path = Path('..') / 'ifc' / 'IfcOpenHouse.ifc'
file.write(ifc_path)

And that was it! we have programmatically and procedurally generated a simple house in IFC. What are you thinking to build next? ๐Ÿ˜Ž

Known missing features

If you feel something is missing or can be improve, kindly file an issue. This is a list of known missing features at the moment:

  • Using IFC Types