Files
IfcOpenShell/src/ifcquery/tests/test_clash.py
T
Bruno Postle 7cd40bf8cb Add ifcquery CLI tool for IFC model interrogation (#7845)
ifcquery is a new command-line tool for querying and inspecting IFC models. All output is JSON.

Subcommands:

    summary — schema version, entity counts, project metadata
    tree — full spatial hierarchy (Project → Site → Building → Storeys → Spaces → Elements)
    info <id> — deep inspection of any entity by step ID (attributes, psets, placement matrix, type, material)
    select <query> — filter elements using ifcopenshell selector syntax
    relations <id> — relationships for an element; --traverse up walks to IfcProject
    clash <id> — geometric intersection and clearance detection
    validate — schema/constraint validation; --rules adds EXPRESS checks
    schedule — work schedules with nested task trees
    cost — cost schedules with nested cost item trees
    schema <class> — IFC class documentation from the model's schema version
    plot — SVG plan drawing
    render — 3D geometry rendering
    contexts — geometric representation contexts
    materials — material assignments

Usage:

python3 -m ifcquery <file.ifc> <subcommand> [args]

Generated with the assistance of an AI coding tool.
2026-03-23 23:29:32 +00:00

331 lines
16 KiB
Python

# This file was generated with the assistance of an AI coding tool.
import json
import os
import subprocess
import sys
import tempfile
import ifcopenshell
import ifcopenshell.api.aggregate
import ifcopenshell.api.context
import ifcopenshell.api.geometry
import ifcopenshell.api.owner.settings
import ifcopenshell.api.project
import ifcopenshell.api.root
import ifcopenshell.api.spatial
import ifcopenshell.api.unit
import numpy as np
import pytest
from ifcquery.clash import clash
try:
import ifcopenshell.geom
HAS_GEOM = True
except ImportError:
HAS_GEOM = False
pytestmark = pytest.mark.skipif(not HAS_GEOM, reason="ifcopenshell geometry engine not available")
@pytest.fixture
def model_with_geometry():
"""Create an IFC4 model with walls that have geometric representations."""
f = ifcopenshell.api.project.create_file()
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="TestProject")
ifcopenshell.api.unit.assign_unit(f)
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="TestSite")
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="TestBuilding")
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
# Create geometry context
model_ctx = ifcopenshell.api.context.add_context(f, context_type="Model")
body = ifcopenshell.api.context.add_context(
f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model_ctx
)
# Wall 1 at origin
wall1 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall001")
rep1 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall1, representation=rep1)
ifcopenshell.api.spatial.assign_container(f, products=[wall1], relating_structure=storey)
# Wall 2 perpendicular, crossing through wall 1
wall2 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall002")
rep2 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall2, representation=rep2)
ifcopenshell.api.spatial.assign_container(f, products=[wall2], relating_structure=storey)
matrix2 = np.array([[0, -1, 0, 2.5], [1, 0, 0, -2.0], [0, 0, 1, 0], [0, 0, 0, 1]], dtype=float)
ifcopenshell.api.geometry.edit_object_placement(f, product=wall2, matrix=matrix2)
# Wall 3 far away (10m offset in Y)
wall3 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall003")
rep3 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall3, representation=rep3)
ifcopenshell.api.spatial.assign_container(f, products=[wall3], relating_structure=storey)
matrix3 = np.eye(4)
matrix3[1, 3] = 10.0 # 10m in Y direction
ifcopenshell.api.geometry.edit_object_placement(f, product=wall3, matrix=matrix3)
# Wall 4 close but not overlapping (0.3m offset in Y, wall thickness is 0.2m)
wall4 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="Wall004")
rep4 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall4, representation=rep4)
ifcopenshell.api.spatial.assign_container(f, products=[wall4], relating_structure=storey)
matrix4 = np.eye(4)
matrix4[1, 3] = 0.3 # 0.3m in Y (gap of 0.1m from wall1)
ifcopenshell.api.geometry.edit_object_placement(f, product=wall4, matrix=matrix4)
return f
@pytest.fixture
def model_two_storeys():
"""Create a model with walls in different storeys."""
f = ifcopenshell.api.project.create_file()
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="TestProject")
ifcopenshell.api.unit.assign_unit(f)
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="TestSite")
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="TestBuilding")
storey1 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="Ground Floor")
storey2 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="First Floor")
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
ifcopenshell.api.aggregate.assign_object(f, products=[storey1, storey2], relating_object=building)
model_ctx = ifcopenshell.api.context.add_context(f, context_type="Model")
body = ifcopenshell.api.context.add_context(
f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model_ctx
)
# Wall in storey 1
wall1 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="GroundWall")
rep1 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall1, representation=rep1)
ifcopenshell.api.spatial.assign_container(f, products=[wall1], relating_structure=storey1)
# Wall in storey 2, perpendicular and crossing wall1
wall2 = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="FirstFloorWall")
rep2 = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall2, representation=rep2)
ifcopenshell.api.spatial.assign_container(f, products=[wall2], relating_structure=storey2)
matrix2 = np.array([[0, -1, 0, 2.5], [1, 0, 0, -2.0], [0, 0, 1, 0], [0, 0, 0, 1]], dtype=float)
ifcopenshell.api.geometry.edit_object_placement(f, product=wall2, matrix=matrix2)
return f
class TestNoClashes:
def test_no_clashes_far_apart(self, model_with_geometry):
wall3 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall003")
result = clash(model_with_geometry, wall3)
assert result["pass"] is True
assert result["checks"]["intersection"]["pass"] is True
assert result["checks"]["intersection"]["clashes"] == []
def test_no_clashes_empty_scope(self, model_with_geometry):
"""A model where the element is the only one in scope should pass."""
# Create a model with a single wall
f = ifcopenshell.api.project.create_file()
ifcopenshell.api.owner.settings.get_user = lambda ifc: (ifc.by_type("IfcPersonAndOrganization") or [None])[0]
ifcopenshell.api.owner.settings.get_application = lambda ifc: (ifc.by_type("IfcApplication") or [None])[0]
project = ifcopenshell.api.root.create_entity(f, ifc_class="IfcProject", name="P")
ifcopenshell.api.unit.assign_unit(f)
site = ifcopenshell.api.root.create_entity(f, ifc_class="IfcSite", name="S")
building = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuilding", name="B")
storey = ifcopenshell.api.root.create_entity(f, ifc_class="IfcBuildingStorey", name="GF")
ifcopenshell.api.aggregate.assign_object(f, products=[site], relating_object=project)
ifcopenshell.api.aggregate.assign_object(f, products=[building], relating_object=site)
ifcopenshell.api.aggregate.assign_object(f, products=[storey], relating_object=building)
model_ctx = ifcopenshell.api.context.add_context(f, context_type="Model")
body = ifcopenshell.api.context.add_context(
f, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model_ctx
)
wall = ifcopenshell.api.root.create_entity(f, ifc_class="IfcWall", name="OnlyWall")
rep = ifcopenshell.api.geometry.add_wall_representation(f, context=body, length=5, height=3, thickness=0.2)
ifcopenshell.api.geometry.assign_representation(f, product=wall, representation=rep)
ifcopenshell.api.spatial.assign_container(f, products=[wall], relating_structure=storey)
result = clash(f, wall)
assert result["pass"] is True
class TestIntersectionDetected:
def test_overlapping_walls(self, model_with_geometry):
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = clash(model_with_geometry, wall1)
assert result["pass"] is False
assert result["checks"]["intersection"]["pass"] is False
clashes = result["checks"]["intersection"]["clashes"]
assert len(clashes) > 0
# Wall002 should be in the clashes (it overlaps wall1)
clash_ids = {c["element"]["id"] for c in clashes}
wall2 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall002")
assert wall2.id() in clash_ids
def test_clash_has_points(self, model_with_geometry):
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = clash(model_with_geometry, wall1)
clashes = result["checks"]["intersection"]["clashes"]
for c in clashes:
assert "p1" in c
assert "p2" in c
assert len(c["p1"]) == 3
assert len(c["p2"]) == 3
assert "type" in c
assert "distance" in c
class TestClearance:
def test_clearance_violation(self, model_with_geometry):
"""Wall004 is 0.1m from wall1; clearance of 0.5m should fail."""
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = clash(model_with_geometry, wall1, clearance=0.5)
assert "clearance" in result["checks"]
# Wall004 should violate clearance
clearance_clashes = result["checks"]["clearance"]["clashes"]
clash_ids = {c["element"]["id"] for c in clearance_clashes}
wall4 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall004")
assert wall4.id() in clash_ids
assert result["checks"]["clearance"]["pass"] is False
def test_clearance_pass(self, model_with_geometry):
"""Wall003 is 10m away; clearance of 0.5m should pass for wall003."""
wall3 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall003")
result = clash(model_with_geometry, wall3, clearance=0.5)
assert result["checks"]["clearance"]["pass"] is True
assert result["checks"]["clearance"]["clashes"] == []
def test_clearance_not_included_by_default(self, model_with_geometry):
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = clash(model_with_geometry, wall1)
assert "clearance" not in result["checks"]
class TestScope:
def test_scope_storey_excludes_other_storeys(self, model_two_storeys):
wall1 = next(w for w in model_two_storeys.by_type("IfcWall") if w.Name == "GroundWall")
result = clash(model_two_storeys, wall1, scope="storey")
assert result["scope"] == "storey"
# No clashes because the overlapping wall is in a different storey
assert result["pass"] is True
def test_scope_all_includes_other_storeys(self, model_two_storeys):
wall1 = next(w for w in model_two_storeys.by_type("IfcWall") if w.Name == "GroundWall")
result = clash(model_two_storeys, wall1, scope="all")
assert result["scope"] == "all"
# Should detect clash with the other-storey wall
assert result["pass"] is False
clash_ids = {c["element"]["id"] for c in result["checks"]["intersection"]["clashes"]}
wall2 = next(w for w in model_two_storeys.by_type("IfcWall") if w.Name == "FirstFloorWall")
assert wall2.id() in clash_ids
class TestNoGeometry:
def test_no_geometry_error(self, model):
"""Element without geometry reports error."""
wall = model.by_type("IfcWall")[0]
result = clash(model, wall)
assert result["pass"] is None
assert "error" in result
assert "No geometry" in result["error"]
class TestJsonSerializable:
def test_result_serializable(self, model_with_geometry):
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = clash(model_with_geometry, wall1)
serialized = json.dumps(result)
parsed = json.loads(serialized)
assert parsed["element"]["type"] == "IfcWall"
def test_clearance_result_serializable(self, model_with_geometry):
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = clash(model_with_geometry, wall1, clearance=0.5)
serialized = json.dumps(result)
parsed = json.loads(serialized)
assert "clearance" in parsed["checks"]
class TestCLI:
@staticmethod
def _ifc_path(model):
f = tempfile.NamedTemporaryFile(suffix=".ifc", delete=False)
model.write(f.name)
f.close()
return f.name
def test_clash_json(self, model_with_geometry):
path = self._ifc_path(model_with_geometry)
try:
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = subprocess.run(
[sys.executable, "-m", "ifcquery", path, "clash", str(wall1.id())],
capture_output=True,
text=True,
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert data["element"]["type"] == "IfcWall"
assert "checks" in data
assert "intersection" in data["checks"]
finally:
os.unlink(path)
def test_clash_with_clearance(self, model_with_geometry):
path = self._ifc_path(model_with_geometry)
try:
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = subprocess.run(
[sys.executable, "-m", "ifcquery", path, "clash", str(wall1.id()), "--clearance", "0.5"],
capture_output=True,
text=True,
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert "clearance" in data["checks"]
finally:
os.unlink(path)
def test_clash_scope_all(self, model_with_geometry):
path = self._ifc_path(model_with_geometry)
try:
wall1 = next(w for w in model_with_geometry.by_type("IfcWall") if w.Name == "Wall001")
result = subprocess.run(
[sys.executable, "-m", "ifcquery", path, "clash", str(wall1.id()), "--scope", "all"],
capture_output=True,
text=True,
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert data["scope"] == "all"
finally:
os.unlink(path)
def test_clash_bad_id(self, model_with_geometry):
path = self._ifc_path(model_with_geometry)
try:
result = subprocess.run(
[sys.executable, "-m", "ifcquery", path, "clash", "999999"],
capture_output=True,
text=True,
)
assert result.returncode != 0
assert "Error" in result.stderr
finally:
os.unlink(path)