关于 clash-detection-analysis
用于 BIM 模型的分析技能,可检测建筑构件间的硬碰撞、软碰撞和工作流冲突。支持配置碰撞测试集并导出 Excel 报告,供施工前审查 IFC 模型。
主要功能
- 01检测物理交叉
- 02检查间距违规
- 03识别工作流冲突
- 04配置碰撞测试集
- 05导出 Excel 报告
适用场景
- 01查找结构与机电系统冲突
- 02施工前审查构件间距
- 03为项目团队导出碰撞摘要
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ datadrivenconstruction/ ddc_skills_for_ai_agents_in_construction --skill clash-detection-analysis安装后在 Claude Code 中输入/clash-detection-analysis确认已加载。
附带文件claw.jsoninstructions.md
详细介绍
碰撞检测分析
概述
此技能可对 BIM 模型进行自动碰撞检测。在施工前识别建筑构件之间的冲突,以避免高昂的返工成本和工期延误。
碰撞类型:
- 硬碰撞:构件发生物理交叉
- 软碰撞:净空/公差违规
- 工作流碰撞:排期/施工顺序冲突
“在设计阶段检测碰撞,与在施工现场纠正相比,可将错误修复成本降低多达 10 倍。”
快速入门
import ifcopenshell
import ifcopenshell.geom
import numpy as np
from itertools import combinations
# Open model
ifc = ifcopenshell.open("model.ifc")
# Get structural and MEP elements
structural = ifc.by_type("IfcColumn") + ifc.by_type("IfcBeam")
mep = ifc.by_type("IfcPipeSegment") + ifc.by_type("IfcDuctSegment")
# Simple bounding box clash check
settings = ifcopenshell.geom.settings()
def get_bbox(element):
try:
shape = ifcopenshell.geom.create_shape(settings, element)
verts = np.array(shape.geometry.verts).reshape(-1, 3)
return verts.min(axis=0), verts.max(axis=0)
except:
return None, None
def check_bbox_clash(bbox1, bbox2):
min1, max1 = bbox1
min2, max2 = bbox2
if min1 is None or min2 is None:
return False
return np.all(max1 >= min2) and np.all(max2 >= min1)
# Find clashes
clashes = []
for s_elem in structural:
for m_elem in mep:
bbox1 = get_bbox(s_elem)
bbox2 = get_bbox(m_elem)
if check_bbox_clash(bbox1, bbox2):
clashes.append({
'element1': s_elem.GlobalId,
'element2': m_elem.GlobalId,
'type': 'Structure-MEP'
})
print(f"Found {len(clashes)} potential clashes")
碰撞检测引擎
核心检测器类
import ifcopenshell
import ifcopenshell.geom
import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import List, Dict, Optional, Tuple
from itertools import combinations
from scipy.spatial import cKDTree
@dataclass
class Clash:
element1_id: str
element1_type: str
element1_name: str
element2_id: str
element2_type: str
element2_name: str
clash_type: str
distance: float
location: Tuple[float, float, float]
severity: str
class ClashDetector:
"""Detect clashes between BIM elements"""
def __init__(self, ifc_path: str):
self.model = ifcopenshell.open(ifc_path)
self.settings = ifcopenshell.geom.settings()
self.settings.set(self.settings.USE_WORLD_COORDS, True)
self._geometry_cache = {}
self.clashes: List[Clash] = []
def _get_geometry(self, element):
"""Get or compute element geometry"""
if element.GlobalId in self._geometry_cache:
return self._geometry_cache[element.GlobalId]
try:
shape = ifcopenshell.geom.create_shape(self.settings, element)
verts = np.array(shape.geometry.verts).reshape(-1, 3)
faces = np.array(shape.geometry.faces).reshape(-1, 3)
geom = {
'vertices': verts,
'faces': faces,
'min': verts.min(axis=0),
'max': verts.max(axis=0),
'center': verts.mean(axis=0)
}
self._geometry_cache[element.GlobalId] = geom
return geom
except:
return None
def detect_hard_clashes(self, group1_types: List[str],
group2_types: List[str]) -> List[Clash]:
"""Detect hard clashes (physical intersections) between two groups"""
group1 = []
for ifc_type in group1_types:
group1.extend(self.model.by_type(ifc_type))
group2 = []
for ifc_type in group2_types:
group2.extend(self.model.by_type(ifc_type))
clashes = []
for elem1 in group1:
geom1 = self._get_geometry(elem1)
if geom1 is None:
continue
for elem2 in group2:
if elem1.GlobalId == elem2.GlobalId:
continue
geom2 = self._get_geometry(elem2)
if geom2 is None:
continue
# Bounding box check (fast filter)
if not self._bbox_intersect(geom1, geom2):
continue
# Detailed check
intersection = self._check_intersection(geom1, geom2)
if intersection['intersects']:
clash = Clash(
element1_id=elem1.GlobalId,
element1_type=elem1.is_a(),
element1_name=elem1.Name or '',
element2_id=elem2.GlobalId,
element2_type=elem2.is_a(),
element2_name=elem2.Name or '',
clash_type='Hard',
distance=intersection['distance'],
location=tuple(intersection['point']),
severity=self._classify_severity(intersection['distance'])
)
clashes.append(clash)
self.clashes.extend(clashes)
return clashes
def detect_soft_clashes(self, group1_types: List[str],
group2_types: List[str],
clearance: float = 0.1) -> List[Clash]:
"""Detect soft clashes (clearance violations)"""
group1 = []
for ifc_type in group1_types:
group1.extend(self.model.by_type(ifc_type))
group2 = []
for ifc_type in group2_types:
group2.extend(self.model.by_type(ifc_type))
clashes = []
for elem1 in group1:
geom1 = self._get_geometry(elem1)
if geom1 is None:
continue
for elem2 in group2:
if elem1.GlobalId == elem2.GlobalId:
continue
geom2 = self._get_geometry(elem2)
if geom2 is None:
continue
# Check if within clearance distance
distance = self._min_distance(geom1, geom2)
if distance < clearance and distance > 0:
clash = Clash(
element1_id=elem1.GlobalId,
element1_type=elem1.is_a(),
element1_name=elem1.Name or '',
element2_id=elem2.GlobalId,
element2_type=elem2.is_a(),
element2_name=elem2.Name or '',
clash_type='Soft',
distance=distance,
location=tuple((geom1['center'] + geom2['center']) / 2),
severity='Medium' if distance < clearance/2 else 'Low'
)
clashes.append(clash)
self.clashes.extend(clashes)
return clashes
def _bbox_intersect(self, geom1: Dict, geom2: Dict) -> bool:
"""Check if bounding boxes intersect"""
return (np.all(geom1['max'] >= geom2['min']) and
np.all(geom2['max'] >= geom1['min']))
def _check_intersection(self, geom1: Dict, geom2: Dict) -> Dict:
"""Check for actual geometry intersection"""
# Simplified check using closest points
tree1 = cKDTree(geom1['vertices'])
distances, _ = tree1.query(geom2['vertices'], k=1)
min_dist = distances.min()
if min_dist < 0.001: # Intersection threshold
intersection_idx = np.argmin(distances)
return {
'intersects': True,
'distance': min_dist,
'point': geom2['vertices'][intersection_idx]
}
return {'intersects': False, 'distance': min_dist, 'point': None}
def _min_distance(self, geom1: Dict, geom2: Dict) -> float:
"""Calculate minimum distance between geometries"""
tree1 = cKDTree(geom1['vertices'])
distances, _ = tree1.query(geom2['vertices'], k=1)
return distances.min()
def _classify_severity(self, distance: float) -> str:
"""Classify clash severity"""
if distance < 0.01:
return 'Critical'
elif distance < 0.05:
return 'High'
elif distance < 0.1:
return 'Medium'
else:
return 'Low'
def get_clash_report(self) -> pd.DataFrame:
"""Generate clash report as DataFrame"""
if not self.clashes:
return pd.DataFrame()
return pd.DataFrame([
{
'Element1_ID': c.element1_id,
'Element1_Type': c.element1_type,
'Element1_Name': c.element1_name,
'Element2_ID': c.element2_id,
'Element2_Type': c.element2_type,
'Element2_Name': c.element2_name,
'Clash_Type': c.clash_type,
'Distance_m': c.distance,
'Location_X': c.location[0],
'Location_Y': c.location[1],
'Location_Z': c.location[2],
'Severity': c.severity
}
for c in self.clashes
])
def get_summary(self) -> Dict:
"""Get clash detection summary"""
df = self.get_clash_report()
if df.empty:
return {'total': 0}
return {
'total': len(self.clashes),
'by_type': df['Clash_Type'].value_counts().to_dict(),
'by_severity': df['Severity'].value_counts().to_dict(),
'critical_count': len(df[df['Severity'] == 'Critical']),
'element_types_involved': df['Element1_Type'].unique().tolist() +
df['Element2_Type'].unique().tolist()
}
碰撞集配置
常用碰撞测试集
# Define common clash test configurations
CLASH_SETS = {
'structure_vs_mep': {
'group1': ['IfcColumn', 'IfcBeam', 'IfcWall', 'IfcSlab'],
'group2': ['IfcPipeSegment', 'IfcDuctSegment', 'IfcCableSegment'],
'clearance': 0.05,
'description': 'Structural elements vs MEP systems'
},
'piping_vs_hvac': {
'group1': ['IfcPipeSegment', 'IfcPipeFitting'],
'group2': ['IfcDuctSegment', 'IfcDuctFitting'],
'clearance': 0.10,
'description': 'Plumbing vs HVAC conflicts'
},
'doors_clearance': {
'group1': ['IfcDoor'],
'group2': ['IfcColumn', 'IfcWall'],
'clearance': 0.90, # Door swing clearance
'description': 'Door opening clearances'
},
'electrical_vs_plumbing': {
'group1': ['IfcCableSegment', 'IfcElectricDistributionBoard'],
'group2': ['IfcPipeSegment', 'IfcSanitaryTerminal'],
'clearance': 0.15,
'description': 'Electrical safety clearance from water'
},
'ceiling_vs_mep': {
'group1': ['IfcCovering'],
'group2': ['IfcPipeSegment', 'IfcDuctSegment', 'IfcCableCarrierSegment'],
'clearance': 0.05,
'description': 'Ceiling clearance for MEP'
}
}
def run_all_clash_tests(detector: ClashDetector, clash_sets: Dict = None) -> Dict:
"""Run all configured clash tests"""
if clash_sets is None:
clash_sets = CLASH_SETS
results = {}
for test_name, config in clash_sets.items():
print(f"Running: {config['description']}...")
# Hard clashes
hard = detector.detect_hard_clashes(config['group1'], config['group2'])
# Soft clashes
soft = detector.detect_soft_clashes(
config['group1'],
config['group2'],
config['clearance']
)
results[test_name] = {
'description': config['description'],
'hard_clashes': len(hard),
'soft_clashes': len(soft),
'total': len(hard) + len(soft)
}
return results
生成报告
导出碰撞报告
def export_clash_report(detector: ClashDetector, output_path: str):
"""Export comprehensive clash report to Excel"""
df = detector.get_clash_report()
summary = detector.get_summary()
with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
# Summary sheet
summary_df = pd.DataFrame([{
'Total Clashes': summary['total'],
'Critical': summary.get('critical_count', 0),
'Hard Clashes': summary['by_type'].get('Hard', 0),
'Soft Clashes': summary['by_type'].get('Soft', 0)
}])
summary_df.to_excel(writer, sheet_name='Summary', index=False)
# All clashes
if not df.empty:
df.to_excel(writer, sheet_name='All_Clashes', index=False)
# By severity
for severity in ['Critical', 'High', 'Medium', 'Low']:
severity_df = df[df['Severity'] == severity]
if not severity_df.empty:
severity_df.to_excel(writer, sheet_name=severity, index=False)
return output_path
def generate_clash_html_report(detector: ClashDetector, output_path: str):
"""Generate HTML report with visualizations"""
df = detector.get_clash_report()
summary = detector.get_summary()
html = f"""
<!DOCTYPE html>
<html>
<head>
<title>Clash Detection Report</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.summary {{ background: #f5f5f5; padding: 20px; border-radius: 8px; }}
.critical {{ background: #ffebee; }}
.high {{ background: #fff3e0; }}
table {{ border-collapse: collapse; width: 100%; margin-top: 20px; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background: #4CAF50; color: white; }}
</style>
</head>
<body>
<h1>Clash Detection Report</h1>
<div class="summary">
<h2>Summary</h2>
<p><strong>Total Clashes:</strong> {summary['total']}</p>
<p><strong>Critical:</strong> {summary.get('critical_count', 0)}</p>
</div>
<h2>Clash Details</h2>
{df.to_html(index=False) if not df.empty else '<p>No clashes found</p>'}
</body>
</html>
"""
with open(output_path, 'w') as f:
f.write(html)
return output_path
快速参考
| 碰撞类型 | 描述 | 典型净空 |
|---|---|---|
| 硬碰撞 | 物理交叉 | 0 mm |
| 软碰撞 | 净空违规 | 50-150 mm |
| 工作流 | 排期冲突 | 不适用 |
| 严重程度 | 距离 | 所需操作 |
|---|---|---|
| 严重 | < 10 mm | 立即重新设计 |
| 高 | 10-50 mm | 优先修复 |
| 中 | 50-100 mm | 需要审查 |
| 低 | > 100 mm | 持续监控 |
资源
- IfcOpenShell:https://ifcopenshell.org
- DDC 网站:https://datadrivenconstruction.io
后续步骤
- 查看
4d-simulation,了解基于时间的碰撞分析 - 查看
bim-validation-pipeline,了解验证工作流 - 查看
ifc-data-extraction,了解构件数据
评论
支持 Markdown