ifcopenshell Workflow: IFC Parsing & BIM Geometry Extraction in Python

ifcopenshell is the primary open-source Python library for reading, writing, and tessellating Industry Foundation Classes (IFC) files — the open BIM exchange format maintained by buildingSMART International. As part of the Python Parsing & Geometry Extraction pipeline, the ifcopenshell workflow occupies the ingestion and geometry compilation stages: it opens IFC deliverables, resolves parametric representations to triangulated meshes via OpenCASCADE, and hands off georeferenced geometry to downstream GIS or visualization systems.

This page covers the full production workflow: environment setup, architectural internals, step-by-step extraction, named edge cases, validation strategies, memory management, and a FAQ drawn from real integration failures.


Prerequisites

  • Python 3.9 or higher — type hints and pathlib are used throughout.
  • ifcopenshell 0.7.x (pip install ifcopenshell) — ships with a bundled OpenCASCADE binary; no separate C++ compilation is required on standard platforms.
  • numpy ≥ 1.24 (pip install numpy) — vertex and face arrays are returned as raw Python lists that must be reshaped into numpy arrays for vectorized processing.
  • pyproj ≥ 3.5 (pip install pyproj) — PROJ 9 backend required for accurate datum-aware CRS transformations.
  • shapely ≥ 2.0 (pip install shapely) — for footprint extraction and spatial predicates. See Extracting IFC Wall Geometries to Shapely for optimized polygonization routines.
  • Assumed knowledge: familiarity with the IFC spatial hierarchy (IfcProjectIfcSiteIfcBuildingIfcBuildingStorey), basic understanding of homogeneous 4×4 transformation matrices, and awareness that IFC geometry is parametric (it must be compiled, not read directly).

Install all dependencies together:

# ifcopenshell>=0.7.0 numpy>=1.24.0 pyproj>=3.5.0 shapely>=2.0.0
pip install ifcopenshell numpy pyproj shapely

Architectural Overview

How ifcopenshell Compiles IFC Geometry

IFC stores geometry as parametric descriptions: extruded area profiles, swept solids, constructive solid geometry (CSG) trees, and boolean operations. None of these representations are ready-made mesh data — they must be compiled. ifcopenshell.geom delegates this compilation to OpenCASCADE Technology (OCCT), a C++ solid modelling kernel, which evaluates the parametric tree and produces a triangulated BRep (boundary representation) surface.

The key internal objects are:

  • IfcRepresentation — the container that references one or more IfcRepresentationItem shapes (extruded profiles, mapped items, boolean results).
  • IfcLocalPlacement — a recursive 4×4 matrix chain encoding position, rotation, and scale relative to the parent entity’s coordinate system.
  • IfcGeometricRepresentationContext — defines the coordinate dimensionality (2D/3D), precision, and world-coordinate origin.

The ifcopenshell.geom.create_shape() function resolves this entire chain: it traverses the placement hierarchy, composes the transformation matrices, evaluates the geometry, applies unit scale, and returns a compiled Shape object whose .geometry attribute holds flat vertex and face index lists.

IFC Schema Compatibility

Schema Token in FILE_SCHEMA Notable geometry types ifcopenshell support
IFC2x3 IFC2X3 IfcExtrudedAreaSolid, IfcFacetedBrep Full
IFC4 IFC4 + IfcAdvancedBrep, IfcTriangulatedFaceSet Full
IFC4x1 IFC4X1 Alignment geometry (IfcAlignmentCurve) Partial
IFC4x3 IFC4X3 Civil/infrastructure extensions Partial (0.7+)

Always read the schema token from ifc_file.schema before dispatching geometry logic; IFC4x3 alignment types require explicit handling not present in the standard geom pipeline.


Architectural Data-Flow Diagram

ifcopenshell Pipeline Data Flow Flowchart showing the four stages of the ifcopenshell workflow: IFC File ingestion, Geometry Compilation via OpenCASCADE, Coordinate Transformation via pyproj, and Serialization to GeoJSON/OBJ/3D Tiles. IFC File .ifc / .ifczip IFC2x3 · IFC4 · IFC4x3 Ingestion ifcopenshell.open() schema · units · CRS anchor Geometry Compilation ifcopenshell.geom OpenCASCADE tessellation CRS Transformation placement matrix × pyproj local → georeferenced Serialization GeoJSON · OBJ · 3D Tiles + Pset attributes as properties GeoJSON Web mapping / PostGIS OBJ / glTF 3D viewers / CAD 3D Tiles Cesium / infrastructure Shapely Footprints / spatial ops Error Isolation RuntimeError → skip log GlobalId + Name ifcopenshell pipeline: ingestion → compilation → CRS transformation → serialization

Step-by-Step Implementation

Step 1 — Open the File and Inspect Project Context

# ifcopenshell>=0.7.0
import ifcopenshell
import ifcopenshell.util.unit
from pathlib import Path

def open_ifc(path: Path):
    ifc = ifcopenshell.open(str(path))
    schema = ifc.schema          # 'IFC2X3', 'IFC4', 'IFC4X3', etc.
    unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc)
    # unit_scale converts native units to meters; 0.001 means file is in mm
    return ifc, schema, unit_scale

calculate_unit_scale reads IfcUnitAssignment and returns a float that converts the file’s native length unit to meters. Apply this scalar to all extracted vertex arrays before CRS projection.

Step 2 — Enumerate the Spatial Hierarchy

def get_spatial_tree(ifc):
    """Return a flat list of (storey_name, [products]) pairs."""
    result = []
    for storey in ifc.by_type("IfcBuildingStorey"):
        products = [
            p for p in ifc.by_type("IfcProduct")
            if getattr(p, "ObjectPlacement", None) is not None
        ]
        result.append((storey.Name or "unnamed", products))
    return result

Processing by IfcBuildingStorey enables chunked extraction (see Performance & Scale below) and produces spatially coherent output tiles.

Step 3 — Configure the Geometry Settings

import ifcopenshell.geom

def make_settings(use_world_coords: bool = True) -> ifcopenshell.geom.settings:
    # ifcopenshell>=0.7.0
    s = ifcopenshell.geom.settings()
    s.set(s.USE_WORLD_COORDS, use_world_coords)     # bake placement matrices in
    s.set(s.SEW_SHELLS, True)                        # watertight meshes
    s.set(s.INCLUDE_CURVES, False)                   # skip 2D annotation geometry
    s.set(s.EXCLUDE_SOLIDS_AND_SURFACES, False)      # keep solid bodies
    return s

USE_WORLD_COORDS is the single most impactful flag: when True, OpenCASCADE applies every IfcLocalPlacement matrix in the hierarchy and returns vertices in project world coordinates, eliminating the need for manual matrix composition in most pipelines.

Step 4 — Extract and Triangulate Geometry

import numpy as np

def extract_geometry(ifc_file, product, settings):
    """
    Returns (vertices: np.ndarray shape (N,3), faces: np.ndarray shape (M,3))
    or None if the product has no valid solid geometry.
    """
    try:
        shape = ifcopenshell.geom.create_shape(settings, product)
    except RuntimeError as exc:
        gid = getattr(product, "GlobalId", "?")
        name = getattr(product, "Name", "?")
        print(f"[SKIP] {gid} ({name}): {exc}")
        return None

    verts = np.array(shape.geometry.verts, dtype=np.float64).reshape(-1, 3)
    faces = np.array(shape.geometry.faces, dtype=np.int32).reshape(-1, 3)
    return verts, faces

The vertex array is flat ([x0,y0,z0, x1,y1,z1, ...]), so the .reshape(-1, 3) call is mandatory. Face indices are zero-based.

Step 5 — Apply Unit Scale and Project to Target CRS

import pyproj

def project_vertices(
    verts: np.ndarray,
    unit_scale: float,
    source_crs: str = "EPSG:32633",
    target_crs: str = "EPSG:4326",
) -> np.ndarray:
    # Apply unit conversion (mm → m if unit_scale=0.001)
    verts = verts * unit_scale

    transformer = pyproj.Transformer.from_crs(
        source_crs, target_crs, always_xy=True
    )
    x, y, z = transformer.transform(verts[:, 0], verts[:, 1], verts[:, 2])
    return np.column_stack([x, y, z])

always_xy=True prevents axis-order surprises when the source CRS uses a latitude-first convention (common with legacy EPSG codes). See Converting CAD Local Coordinates to EPSG:4326 for a worked example with real survey data.

Step 6 — Extract Property Sets and Serialize to GeoJSON

import json
import ifcopenshell.util.element

def product_to_geojson_feature(product, verts, faces):
    psets = ifcopenshell.util.element.get_psets(product)
    # Flatten to a single dict; last writer wins on key collisions
    attributes = {}
    for pset_name, pset_values in psets.items():
        for k, v in pset_values.items():
            attributes[f"{pset_name}.{k}"] = v

    # Build a minimal footprint polygon from the lowest Z vertices
    min_z = verts[:, 2].min()
    floor_mask = verts[:, 2] < min_z + 0.1
    floor_verts = verts[floor_mask]
    from shapely.geometry import MultiPoint, mapping
    if len(floor_verts) >= 3:
        footprint = MultiPoint(floor_verts[:, :2]).convex_hull
        geom = mapping(footprint)
    else:
        geom = {"type": "Point", "coordinates": verts[0, :2].tolist()}

    return {
        "type": "Feature",
        "geometry": geom,
        "properties": {
            "GlobalId": product.GlobalId,
            "Name": product.Name,
            "Type": product.is_a(),
            **attributes,
        },
    }

Edge Cases & Gotchas

1. Unit Scale Not Applied

Symptom: Extracted buildings appear at millimeter scale (a 10m wall becomes 0.01m). Root cause: The file was authored in millimeters but unit_scale was never applied. Fix: Always call ifcopenshell.util.unit.calculate_unit_scale(ifc) and multiply vertex arrays before projection. Never assume meters.

# ifcopenshell>=0.7.0
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc)
verts = verts * unit_scale  # convert to meters before pyproj

2. Missing Geometric Representation Context

Symptom: create_shape raises RuntimeError: No geometric representation found. Root cause: Some IFC products (space boundaries, annotation elements, type definitions) carry no body geometry. Fix: Filter before extraction using product.Representation is not None.

geometric_products = [
    p for p in ifc.by_type("IfcProduct")
    if p.Representation is not None
]

3. Duplicate Global IDs in Federated Files

Symptom: Post-merge GeoJSON contains features with identical GlobalId values from two separate design disciplines. Root cause: GlobalId uniqueness is only guaranteed within a single IFC file, not across federated models. Fix: Prefix the GlobalId with a short file hash when merging.

import hashlib
file_tag = hashlib.md5(str(path).encode()).hexdigest()[:6]
unique_id = f"{file_tag}_{product.GlobalId}"

4. USE_WORLD_COORDS and Nested Mapped Items

Symptom: Geometry for repeated components (structural columns, windows) appears at the origin instead of their placed positions. Root cause: IfcMappedItem references share a single compiled shape; when USE_WORLD_COORDS=True, the per-instance placement is baked in, but some ifcopenshell builds handle mapped items inconsistently. Fix: Use ifcopenshell.geom.iterator with include_openings=False for batch processing — it handles all placement types correctly including mapped items.

# ifcopenshell>=0.7.0 — handles mapped items correctly
iterator = ifcopenshell.geom.iterator(settings, ifc, multiprocessing=True)
if iterator.initialize():
    while True:
        shape = iterator.get()
        process_shape(shape)
        if not iterator.next():
            break

5. Memory Exhaustion on Large Files (>500MB)

Symptom: Python process killed by OOM after processing several hundred products. Root cause: OpenCASCADE allocates native C++ heap that Python’s garbage collector cannot reclaim. Fix: Use subprocess isolation — spawn a worker process per storey, serialize results to disk, terminate the process. This forces OS-level reclamation of the OCCT heap. See Performance & Scale below.

6. CRS Anchor Not Read From File

Symptom: Extracted geometry lands in the correct shape but at the wrong geographic location. Root cause: IFC4x1+ files may embed a georeferencing anchor via IfcMapConversion and IfcProjectedCRS. Ignoring these means the file’s local origin is treated as (0,0,0) in the source CRS. Fix: Read the map conversion before projecting.

# ifcopenshell>=0.7.0
import ifcopenshell.util.geolocation

crs_info = ifcopenshell.util.geolocation.get_crs(ifc)
# crs_info may contain authority (e.g. 'EPSG:32633') and offset

Validation & Testing

Geometry extraction errors are silent by default — create_shape either succeeds or raises. Build explicit assertions to catch precision loss and topology failures before they propagate downstream.

# ifcopenshell>=0.7.0 numpy>=1.24.0
import numpy as np

def validate_mesh(verts: np.ndarray, faces: np.ndarray, product_id: str):
    assert verts.ndim == 2 and verts.shape[1] == 3, \
        f"{product_id}: unexpected vertex shape {verts.shape}"
    assert faces.ndim == 2 and faces.shape[1] == 3, \
        f"{product_id}: unexpected face shape {faces.shape}"
    assert faces.max() < len(verts), \
        f"{product_id}: face index {faces.max()} out of range (nverts={len(verts)})"

    # Check bounding box is not degenerate
    bbox_size = verts.max(axis=0) - verts.min(axis=0)
    if bbox_size.max() < 1e-6:
        raise ValueError(f"{product_id}: zero-volume bounding box — likely unit scale error")

    # Check for NaN/Inf vertices
    if not np.isfinite(verts).all():
        raise ValueError(f"{product_id}: non-finite vertex coordinates detected")

Add a control-point check for CRS transformation: take one known anchor point from the IFC file’s site geometry, project it independently, and assert that the transformed coordinates match a surveyed reference within an acceptable tolerance (typically 0.05m for infrastructure projects).

def test_crs_transform(ifc, transformer, reference_xy, tolerance=0.05):
    # reference_xy: known (lon, lat) from survey control point
    site = ifc.by_type("IfcSite")[0]
    ref_lat = site.RefLatitude
    ref_lon = site.RefLongitude
    if ref_lat and ref_lon:
        projected = transformer.transform(ref_lon[0], ref_lat[0])
        dist = np.hypot(projected[0] - reference_xy[0], projected[1] - reference_xy[1])
        assert dist < tolerance, f"CRS anchor mismatch: {dist:.4f}m > {tolerance}m"

Performance & Scale

Iterator-Based Batch Processing

For files with thousands of products, ifcopenshell.geom.create_shape called in a loop pays per-call overhead for shape cache lookups. The ifcopenshell.geom.iterator API batches products, enables multi-core compilation, and handles mapped item instantiation correctly.

# ifcopenshell>=0.7.0
import multiprocessing

def batch_extract(ifc_path: str, output_path: str):
    ifc = ifcopenshell.open(ifc_path)
    settings = make_settings(use_world_coords=True)
    iterator = ifcopenshell.geom.iterator(
        settings, ifc,
        multiprocessing=multiprocessing.cpu_count()
    )
    features = []
    if iterator.initialize():
        while True:
            shape = iterator.get()
            verts = np.array(shape.geometry.verts, dtype=np.float64).reshape(-1, 3)
            faces = np.array(shape.geometry.faces, dtype=np.int32).reshape(-1, 3)
            # ... project and serialize
            features.append({"id": shape.guid, "nverts": len(verts)})
            if not iterator.next():
                break
    return features

Subprocess Isolation for Memory Safety

OpenCASCADE native heap does not cooperate with Python’s gc.collect(). For pipelines processing hundreds of IFC files in a daemon, spawn a dedicated worker process per file using multiprocessing.Process. When the process terminates, the OS reclaims all OCCT allocations unconditionally.

# ifcopenshell>=0.7.0 — subprocess isolation pattern
import multiprocessing
import json

def _worker(ifc_path, result_queue):
    ifc = ifcopenshell.open(ifc_path)
    unit_scale = ifcopenshell.util.unit.calculate_unit_scale(ifc)
    settings = make_settings()
    features = []
    for product in ifc.by_type("IfcBuildingElement"):
        result = extract_geometry(ifc, product, settings)
        if result:
            verts, faces = result
            verts = verts * unit_scale
            features.append({"id": product.GlobalId, "nverts": len(verts)})
    result_queue.put(features)

def safe_extract(ifc_path: str):
    q = multiprocessing.Queue()
    p = multiprocessing.Process(target=_worker, args=(ifc_path, q))
    p.start()
    p.join()          # OS reclaims OCCT heap on process exit
    return q.get() if not q.empty() else []

Memory Budget Guidelines

File size Expected products Recommended strategy
< 100MB < 5,000 Direct create_shape in-process
100–500MB 5,000–50,000 geom.iterator with multiprocessing
500MB–2GB 50,000–200,000 Subprocess isolation per storey chunk
> 2GB > 200,000 Split file with ifcpatch, process chunks independently

FAQ

Does ifcopenshell support both IFC2x3 and IFC4?

Yes. ifcopenshell auto-detects the schema from the FILE_SCHEMA header token and exposes it via ifc.schema. Most geometry APIs work identically across both schemas. The primary difference is that IFC4 introduces IfcFacetedBrep and IfcAdvancedBrep types that do not exist in IFC2x3 — check ifc.schema before branching on geometry type.

Why does ifcopenshell.geom.create_shape raise RuntimeError for some products?

Parametric representations that contain zero-area faces, degenerate edges, or broken boolean trees fail OpenCASCADE’s BRep validation. This is common with structural elements authored in Revit using complex void families. Wrap every create_shape call in try/except RuntimeError, log the GlobalId and Name, and skip the product rather than aborting the batch.

How do I determine the IFC file's native unit?

Use ifcopenshell.util.unit.calculate_unit_scale(ifc) — it returns a float that converts native units to meters. A value of 1.0 means the file is already in meters; 0.001 means millimeters. Alternatively, inspect ifc.by_type("IfcUnitAssignment")[0] directly and look for IfcSIUnit entries where UnitType == 'LENGTHUNIT'.

What is the best way to extract property sets (Psets)?

Use ifcopenshell.util.element.get_psets(product) rather than manually traversing IsDefinedBy relationships. This helper returns a nested dict keyed by Pset name, flattening both IfcPropertySingleValue and IfcPropertyEnumeratedValue into plain Python types. The result is directly JSON-serializable without further processing.

Can ifcopenshell handle federated (multi-model) IFC files?

Federated IFC is not a single file format — federation is a host-application concept. In Python, open each constituent file separately with ifcopenshell.open(), extract geometry independently, and merge spatial hierarchies by matching on GlobalId or by applying a shared coordinate origin from IfcMapConversion. Prefix GlobalId values with a per-file hash to avoid collisions.