Analyze BIM clash detection results and suggest resolutions. Prioritize clashes, identify patterns, assign responsibility, and track resolution status.
npx skills add https://github.com/datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill clash-resolution-analyzer
Analyze clash detection results from BIM coordination. Prioritize clashes by impact, identify patterns, suggest resolutions, assign responsibility, and track resolution progress.
Clash resolution analysis enables:
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]))
discipline_pairs[pair].append(clash)
for (d1, d2), clashes in discipline_pairs.items():
if len(clashes) >= 3: # Pattern threshold
# Further group by system
system_pairs = defaultdict(list)
for clash in clashes:
sys_pair = tuple(sorted([clash.element1.system, clash.element2.system]))
system_pairs[sys_pair].append(clash)
for (s1, s2), sys_clashes in system_pairs.items():
if len(sys_clashes) >= 2:
# Get resolution strategy
key = (d1, d2) if (d1, d2) in self.RESOLUTION_STRATEGIES else (d2, d1)
strategy = self.RESOLUTION_STRATEGIES.get(key, {})
patterns.append(ClashPattern(
pattern_type=f"{d1} vs {d2}",
disciplines=(d1, d2),
systems=(s1, s2),
clash_count=len(sys_clashes),
example_clashes=[c.id for c in sys_clashes[:3]],
suggested_resolution=strategy.get('strategy', ResolutionType.ROUTE_AROUND).value,
root_cause=f"Coordination needed between {s1} and {s2} systems"
))
self.patterns = sorted(patterns, key=lambda p: -p.clash_count)
return self.patterns
def suggest_resolution(self, clash_id: str) -> Dict:
"""Suggest resolution for a specific clash."""
if clash_id not in self.clashes:
return {'error': 'Clash not found'}
clash = self.clashes[clash_id]
d1, d2 = clash.element1.discipline, clash.element2.discipline
# Get strategy
key = (d1, d2) if (d1, d2) in self.RESOLUTION_STRATEGIES else (d2, d1)
strategy = self.RESOLUTION_STRATEGIES.get(key, {})
suggestion = {
'clash_id': clash_id,
'resolution_type': strategy.get('strategy', ResolutionType.ROUTE_AROUND),
'responsible_discipline': strategy.get('responsible', self._auto_assign(clash)),
'notes': strategy.get('notes', 'Review and coordinate'),
'similar_clashes': [],
}
# Find similar clashes
for pattern in self.patterns:
if d1 in pattern.disciplines and d2 in pattern.disciplines:
suggestion['similar_clashes'] = pattern.example_clashes
suggestion['pattern_root_cause'] = pattern.root_cause
break
return suggestion
def update_clash_status(self, clash_id: str, status: ClashStatus,
resolution_type: ResolutionType = None,
notes: str = "") -> bool:
"""Update clash status."""
if clash_id not in self.clashes:
return False
clash = self.clashes[clash_id]
old_status = clash.status
clash.status = status
if resolution_type:
clash.resolution_type = resolution_type
if notes:
clash.resolution_notes = notes
if status in [ClashStatus.RESOLVED, ClashStatus.APPROVED]:
clash.resolved_date = datetime.now()
# Track history
self.history.append({
'clash_id': clash_id,
'timestamp': datetime.now(),
'old_status': old_status.value,
'new_status': status.value,
'notes': notes
})
return True
def get_clashes_by_discipline(self, discipline: str) -> List[Clash]:
"""Get all clashes assigned to a discipline."""
return [c for c in self.clashes.values() if c.assigned_to == discipline]
def get_clashes_by_level(self, level: str) -> List[Clash]:
"""Get all clashes on a specific level."""
return [c for c in self.clashes.values()
if c.element1.level == level or c.element2.level == level]
def generate_coordination_matrix(self) -> Dict[str, Dict[str, int]]:
"""Generate matrix showing clashes between disciplines."""
matrix = defaultdict(lambda: defaultdict(int))
for clash in self.clashes.values():
d1 = clash.element1.discipline
d2 = clash.element2.discipline
matrix[d1][d2] += 1
if d1 != d2:
matrix[d2][d1] += 1
return dict(matrix)
def generate_report(self) -> ConsistencyReport:
"""Generate comprehensive clash analysis report."""
clashes_by_priority = defaultdict(int)
clashes_by_discipline = defaultdict(int)
clashes_by_status = defaultdict(int)
for clash in self.clashes.values():
clashes_by_priority[clash.priority.name] += 1
clashes_by_discipline[clash.assigned_to] += 1
clashes_by_status[clash.status.value] += 1
resolved = clashes_by_status.get('resolved', 0) + clashes_by_status.get('approved', 0)
resolution_rate = resolved / len(self.clashes) * 100 if self.clashes else 0
return ClashReport(
report_name=f"Clash Report {datetime.now().strftime('%Y-%m-%d')}",
total_clashes=len(self.clashes),
new_clashes=clashes_by_status.get('new', 0),
resolved_clashes=resolved,
clashes_by_priority=dict(clashes_by_priority),
clashes_by_discipline=dict(clashes_by_discipline),
clashes_by_status=dict(clashes_by_status),
patterns=self.patterns,
resolution_rate=resolution_rate
)
def generate_report_markdown(self) -> str:
"""Generate markdown report."""
report = self.generate_report()
lines = ["# Clash Resolution Report", ""]
lines.append(f"**Date:** {datetime.now().strftime('%Y-%m-%d')}")
lines.append(f"**Total Clashes:** {report.total_clashes}")
lines.append(f"**Resolution Rate:** {report.resolution_rate:.1f}%")
lines.append("")
# By status
lines.append("## Status Summary")
for status, count in report.clashes_by_status.items():
lines.append(f"- {status}: {count}")
lines.append("")
# By priority
lines.append("## Priority Breakdown")
for priority, count in sorted(report.clashes_by_priority.items()):
lines.append(f"- {priority}: {count}")
lines.append("")
# By discipline
lines.append("## By Responsible Discipline")
for disc, count in sorted(report.clashes_by_discipline.items(), key=lambda x: -x[1]):
lines.append(f"- {disc}: {count}")
lines.append("")
# Patterns
if report.patterns:
lines.append("## Clash Patterns Identified")
for pattern in report.patterns[:5]:
lines.append(f"\n### {pattern.pattern_type}")
lines.append(f"- **Count:** {pattern.clash_count} clashes")
lines.append(f"- **Systems:** {pattern.systems[0]} vs {pattern.systems[1]}")
lines.append(f"- **Root Cause:** {pattern.root_cause}")
lines.append(f"- **Suggested Resolution:** {pattern.suggested_resolution}")
# Critical clashes
critical = [c for c in self.clashes.values() if c.priority == ClashPriority.CRITICAL and c.status == ClashStatus.NEW]
if critical:
lines.append("\n## Critical Unresolved Clashes")
for clash in critical[:10]:
lines.append(f"- **{clash.id}**: {clash.element1.name} vs {clash.element2.name}")
lines.append(f" - Location: Level {clash.element1.level}")
lines.append(f" - Assigned: {clash.assigned_to}")
return "\n".join(lines)
# Initialize analyzer
analyzer = ClashResolutionAnalyzer()
# Import clashes (from Navisworks export)
clash_data = [
{
'id': 'CLH-001',
'name': 'Duct vs Beam',
'element1_discipline': 'Mechanical',
'element1_system': 'Supply Air',
'element1_level': 'Level 2',
'element2_discipline': 'Structural',
'element2_system': 'Steel Frame',
'element2_level': 'Level 2',
'distance': -0.15,
'clash_type': 'hard'
}
]
count = analyzer.import_clashes(clash_data)
print(f"Imported {count} clashes")
# Analyze patterns
patterns = analyzer.analyze_patterns()
for pattern in patterns:
print(f"Pattern: {pattern.pattern_type} - {pattern.clash_count} clashes")
# Get resolution suggestion
suggestion = analyzer.suggest_resolution('CLH-001')
print(f"Suggested resolution: {suggestion['resolution_type'].value}")
print(f"Responsible: {suggestion['responsible_discipline']}")
# Update status
analyzer.update_clash_status(
'CLH-001',
ClashStatus.RESOLVED,
ResolutionType.ROUTE_AROUND,
'Duct rerouted below beam'
)
# Generate report
print(analyzer.generate_report_markdown())
pip install (no external dependencies)
Integration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Take datadrivenconstruction/clash-resolution-analyzer from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip.
Without those the skill loads but fails at the first command.