mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 19:07:57 +00:00
7cd40bf8cb
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.
331 lines
16 KiB
Python
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)
|