clash-detection-analysis について
BIMモデル内の建築要素について、ハード・ソフト・ワークフローの干渉を検出する分析スキルです。干渉テストの設定とExcelレポートの出力に対応し、施工前のIFCモデル確認に使えます。
主な機能
- 01物理的な交差の検出
- 02クリアランス違反の確認
- 03ワークフロー競合の特定
- 04干渉テストセットの設定
- 05Excelレポートの出力
活用シーン
- 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分の1に抑えられます。」
クイックスタート
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 Website:https://datadrivenconstruction.io
次のステップ
- 時間ベースの干渉分析については
4d-simulationを参照してください - 検証ワークフローについては
bim-validation-pipelineを参照してください - 要素データについては
ifc-data-extractionを参照してください
コメント
Markdown 対応