BIM Clash Detection
Business Case
Problem Statement
Coordination issues cause significant rework:
- MEP vs structural conflicts discovered on site
- Late design changes increase costs
- Manual clash review is time-consuming
- No standardized clash categorization
Solution
Automated clash detection and analysis system that identifies conflicts between building systems and provides prioritized resolution recommendations.
Business Value
- Cost savings - Detect issues before construction
- Time reduction - Automated clash identification
- Better coordination - Systematic conflict resolution
- Quality improvement - Fewer field issues
Technical Implementation
import pandas as pd
from datetime import datetime
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
import math
class ClashType(Enum):
"""Types of clashes."""
HARD = "hard" # Physical intersection
SOFT = "soft" # Clearance violation
WORKFLOW = "workflow" # Sequencing conflict
DUPLICATE = "duplicate" # Duplicated elements
class ClashStatus(Enum):
"""Clash resolution status."""
NEW = "new"
ACTIVE = "active"
RESOLVED = "resolved"
APPROVED = "approved"
IGNORED = "ignored"
class ClashSeverity(Enum):
"""Clash severity level."""
CRITICAL = "critical"
MAJOR = "major"
MINOR = "minor"
INFO = "info"
class Discipline(Enum):
"""BIM disciplines."""
ARCHITECTURAL = "architectural"
STRUCTURAL = "structural"
MECHANICAL = "mechanical"
ELECTRICAL = "electrical"
PLUMBING = "plumbing"
FIRE_PROTECTION = "fire_protection"
CIVIL = "civil"
@dataclass
class BoundingBox:
"""3D bounding box."""
min_x: float
min_y: float
min_z: float
max_x: float
max_y: float
max_z: float
def intersects(self, other: 'BoundingBox') -> bool:
"""Check if boxes intersect."""
return (self.min_x <= other.max_x and self.max_x >= other.min_x and
self.min_y <= other.max_y and self.max_y >= other.min_y and
self.min_z <= other.max_z and self.max_z >= other.min_z)
def volume(self) -> float:
"""Calculate bounding box volume."""
return ((self.max_x - self.min_x) *
(self.max_y - self.min_y) *
(self.max_z - self.min_z))
def center(self) -> Tuple[float, float, float]:
"""Get center point."""
return (
(self.min_x + self.max_x) / 2,
(self.min_y + self.max_y) / 2,
(self.min_z + self.max_z) / 2
)
@dataclass
class BIMElement:
"""BIM element representation."""
element_id: str
name: str
discipline: Discipline
category: str # e.g., "Duct", "Beam", "Pipe"
level: str
bounding_box: BoundingBox
properties: Dict[str, Any] = field(default_factory=dict)
def distance_to(self, other: 'BIMElement') -> float:
"""Calculate distance between element centers."""
c1 = self.bounding_box.center()
c2 = other.bounding_box.center()
return math.sqrt(
(c2[0] - c1[0])**2 +
(c2[1] - c1[1])**2 +
(c2[2] - c1[2])**2
)
@dataclass
class Clash:
"""Clash between two elements."""
clash_id: str
element_a: BIMElement
element_b: BIMElement
clash_type: ClashType
severity: ClashSeverity
status: ClashStatus
distance: float # Penetration depth (negative) or clearance gap
location: Tuple[float, float, float]
detected_at: datetime
resolved_at: Optional[datetime] = None
assigned_to: Optional[str] = None
notes: str = ""
def to_dict(self) -> Dict[str, Any]:
return {
'clash_id': self.clash_id,
'element_a_id': self.element_a.element_id,
'element_a_name': self.element_a.name,
'element_a_discipline': self.element_a.discipline.value,
'element_b_id': self.element_b.element_id,
'element_b_name': self.element_b.name,
'element_b_discipline': self.element_b.discipline.value,
'clash_type': self.clash_type.value,
'severity': self.severity.value,
'status': self.status.value,
'distance': round(self.distance, 3),
'location_x': self.location[0],
'location_y': self.location[1],
'location_z': self.location[2],
'level': self.element_a.level,
'detected_at': self.detected_at.isoformat(),
'assigned_to': self.assigned_to,
'notes': self.notes
}
@dataclass
class ClashTest:
"""Clash test configuration."""
name: str
discipline_a: Discipline
discipline_b: Discipline
clash_type: ClashType
tolerance: float = 0.0 # Clearance tolerance in meters
enabled: bool = True
class BIMClashDetector:
"""Detect and manage BIM clashes."""
def __init__(self):
self.elements: List[BIMElement] = []
self.clashes: List[Clash] = []
self.clash_tests: List[ClashTest] = []
self._clash_counter = 0
def load_elements(self, elements_df: pd.DataFrame) -> int:
"""Load BIM elements from DataFrame."""
loaded = 0
for _, row in elements_df.iterrows():
element = BIMElement(
element_id=str(row.get('element_id', '')),
name=str(row.get('name', '')),
discipline=Discipline(row.get('discipline', 'architectural')),
category=str(row.get('category', '')),
level=str(row.get('level', '')),
bounding_box=BoundingBox(
min_x=float(row.get('min_x', 0)),
min_y=float(row.get('min_y', 0)),
min_z=float(row.get('min_z', 0)),
max_x=float(row.get('max_x', 0)),
max_y=float(row.get('max_y', 0)),
max_z=float(row.get('max_z', 0))
)
)
self.elements.append(element)
loaded += 1
return loaded
def add_clash_test(self, test: ClashTest):
"""Add clash test configuration."""
self.clash_tests.append(test)
def setup_standard_tests(self):
"""Setup standard MEP coordination tests."""
standard_tests = [
ClashTest("MEP vs Structure", Discipline.MECHANICAL, Discipline.STRUCTURAL, ClashType.HARD),
ClashTest("Electrical vs Structure", Discipline.ELECTRICAL, Discipline.STRUCTURAL, ClashType.HARD),
ClashTest("Plumbing vs Structure", Discipline.PLUMBING, Discipline.STRUCTURAL, ClashType.HARD),
ClashTest("MEP vs MEP", Discipline.MECHANICAL, Discipline.ELECTRICAL, ClashType.HARD),
ClashTest("Duct Clearance", Discipline.MECHANICAL, Discipline.MECHANICAL, ClashType.SOFT, tolerance=0.05),
ClashTest("Fire Protection", Discipline.FIRE_PROTECTION, Discipline.STRUCTURAL, ClashType.HARD),
]
for test in standard_tests:
self.add_clash_test(test)
def run_clash_detection(self) -> List[Clash]:
"""Run all clash tests."""
new_clashes = []
for test in self.clash_tests:
if not test.enabled:
continue
# Filter elements by discipline
elements_a = [e for e in self.elements if e.discipline == test.discipline_a]
elements_b = [e for e in self.elements if e.discipline == test.discipline_b]
# Check all pairs
for elem_a in elements_a:
for elem_b in elements_b:
if elem_a.element_id == elem_b.element_id:
continue
clash = self._check_clash(elem_a, elem_b, test)
if clash:
new_clashes.append(clash)
self.clashes.extend(new_clashes)
return new_cl