Clash Resolution Analyzer for Construction
Overview
Analyze clash detection results from BIM coordination. Prioritize clashes by impact, identify patterns, suggest resolutions, assign responsibility, and track resolution progress.
Business Case
Clash resolution analysis enables:
- Efficient Coordination: Focus on critical clashes first
- Pattern Recognition: Fix root causes, not symptoms
- Clear Accountability: Assign responsibility by trade
- Progress Tracking: Monitor resolution status
Technical Implementation
from dataclasses import dataclass, field
from typing import List, Dict, Any, Optional, Tuple
from enum import Enum
from datetime import datetime
from collections import defaultdict
class ClashPriority(Enum):
CRITICAL = 1 # Must resolve before construction
HIGH = 2 # Resolve in next coordination cycle
MEDIUM = 3 # Resolve before trade starts
LOW = 4 # Minor, can resolve in field
class ClashStatus(Enum):
NEW = "new"
ASSIGNED = "assigned"
IN_PROGRESS = "in_progress"
RESOLVED = "resolved"
APPROVED = "approved"
VOID = "void" # Not a real clash
class ResolutionType(Enum):
ROUTE_AROUND = "Route around obstruction"
RAISE_LOWER = "Raise or lower element"
RESIZE = "Resize element"
RELOCATE = "Relocate element"
STRUCTURAL_MOD = "Structural modification required"
DESIGN_CHANGE = "Design change required"
NO_CLASH = "Not a real clash (tolerance)"
SEQUENCE = "Resolve by construction sequence"
@dataclass
class ClashElement:
id: str
name: str
category: str
discipline: str
level: str
system: str
@dataclass
class Clash:
id: str
name: str
element1: ClashElement
element2: ClashElement
location: Tuple[float, float, float]
distance: float # Negative = hard clash, positive = clearance violation
clash_type: str # hard, clearance, duplicate
priority: ClashPriority = ClashPriority.MEDIUM
status: ClashStatus = ClashStatus.NEW
assigned_to: str = ""
resolution_type: Optional[ResolutionType] = None
resolution_notes: str = ""
created_date: datetime = field(default_factory=datetime.now)
resolved_date: Optional[datetime] = None
@dataclass
class ClashPattern:
pattern_type: str
disciplines: Tuple[str, str]
systems: Tuple[str, str]
clash_count: int
example_clashes: List[str]
suggested_resolution: str
root_cause: str
@dataclass
class ClashReport:
report_name: str
total_clashes: int
new_clashes: int
resolved_clashes: int
clashes_by_priority: Dict[str, int]
clashes_by_discipline: Dict[str, int]
clashes_by_status: Dict[str, int]
patterns: List[ClashPattern]
resolution_rate: float
class ClashResolutionAnalyzer:
"""Analyze and manage BIM clash detection results."""
# Discipline priority for resolution responsibility
DISCIPLINE_PRIORITY = {
'Structural': 1,
'Architectural': 2,
'Mechanical': 3,
'Plumbing': 4,
'Electrical': 5,
'Fire Protection': 6,
}
# Common resolution strategies by clash type
RESOLUTION_STRATEGIES = {
('Mechanical', 'Structural'): {
'strategy': ResolutionType.ROUTE_AROUND,
'responsible': 'Mechanical',
'notes': 'MEP typically routes around structure'
},
('Plumbing', 'Structural'): {
'strategy': ResolutionType.ROUTE_AROUND,
'responsible': 'Plumbing',
'notes': 'Coordinate sleeves/penetrations with SE'
},
('Electrical', 'Mechanical'): {
'strategy': ResolutionType.RAISE_LOWER,
'responsible': 'Electrical',
'notes': 'Conduit typically more flexible than ductwork'
},
('Mechanical', 'Mechanical'): {
'strategy': ResolutionType.RESIZE,
'responsible': 'Mechanical',
'notes': 'Review duct sizing and routing options'
},
('Fire Protection', 'Mechanical'): {
'strategy': ResolutionType.ROUTE_AROUND,
'responsible': 'Fire Protection',
'notes': 'Sprinkler typically routes around major duct'
},
}
def __init__(self):
self.clashes: Dict[str, Clash] = {}
self.patterns: List[ClashPattern] = []
self.history: List[Dict] = []
def import_clashes(self, clash_data: List[Dict]) -> int:
"""Import clashes from Navisworks or other clash detection software."""
count = 0
for data in clash_data:
clash = Clash(
id=data.get('id', f'CLH-{count}'),
name=data.get('name', ''),
element1=ClashElement(
id=data.get('element1_id', ''),
name=data.get('element1_name', ''),
category=data.get('element1_category', ''),
discipline=data.get('element1_discipline', ''),
level=data.get('element1_level', ''),
system=data.get('element1_system', '')
),
element2=ClashElement(
id=data.get('element2_id', ''),
name=data.get('element2_name', ''),
category=data.get('element2_category', ''),
discipline=data.get('element2_discipline', ''),
level=data.get('element2_level', ''),
system=data.get('element2_system', '')
),
location=(
data.get('x', 0),
data.get('y', 0),
data.get('z', 0)
),
distance=data.get('distance', 0),
clash_type=data.get('clash_type', 'hard')
)
# Auto-prioritize
clash.priority = self._auto_prioritize(clash)
# Auto-assign
clash.assigned_to = self._auto_assign(clash)
self.clashes[clash.id] = clash
count += 1
return count
def _auto_prioritize(self, clash: Clash) -> ClashPriority:
"""Automatically prioritize clash based on characteristics."""
# Hard clashes with structure are critical
if clash.element1.discipline == 'Structural' or clash.element2.discipline == 'Structural':
if clash.clash_type == 'hard':
return ClashPriority.CRITICAL
# Large penetration clashes
if abs(clash.distance) > 0.1: # More than 100mm overlap
return ClashPriority.HIGH
# MEP-MEP clashes
mep_disciplines = ['Mechanical', 'Electrical', 'Plumbing', 'Fire Protection']
if clash.element1.discipline in mep_disciplines and clash.element2.discipline in mep_disciplines:
return ClashPriority.MEDIUM
# Clearance violations
if clash.clash_type == 'clearance':
return ClashPriority.LOW
return ClashPriority.MEDIUM
def _auto_assign(self, clash: Clash) -> str:
"""Automatically assign responsibility based on discipline priority."""
d1 = clash.element1.discipline
d2 = clash.element2.discipline
# Check for known resolution strategy
key = (d1, d2) if (d1, d2) in self.RESOLUTION_STRATEGIES else (d2, d1)
if key in self.RESOLUTION_STRATEGIES:
return self.RESOLUTION_STRATEGIES[key]['responsible']
# Default to lower priority discipline (typically more flexible)
p1 = self.DISCIPLINE_PRIORITY.get(d1, 10)
p2 = self.DISCIPLINE_PRIORITY.get(d2, 10)
return d2 if p2 > p1 else d1
def analyze_patterns(self) -> List[ClashPattern]:
"""Identify patterns in clashes."""
patterns = []
# Group by discipline pair
discipline_pairs = defaultdict(list)
for clash in self.clashes.values():
pair = tuple(sorted([clash.element1.discipline, clash.element2.discipline]