Convert IFC files (2x3, 4x1, 4x3) to Excel databases using IfcExporter CLI. Extract BIM data, properties, and geometry without proprietary software.
npx skills add https://github.com/datadrivenconstruction/DDC_Skills_for_AI_Agents_in_Construction --skill ifc-to-excel
IFC (Industry Foundation Classes) is the open BIM standard, but:
IfcExporter.exe converts IFC files to structured Excel databases, making BIM data accessible for analysis, validation, and reporting.
IfcExporter.exe <input_ifc> [options]
| Version | Schema | Description |
|---------|--------|-------------|
| IFC2x3 | MVD | Most common exchange format |
| IFC4 | ADD1 | Enhanced properties |
| IFC4x1 | Alignment | Infrastructure support |
| IFC4x3 | Latest | Full infrastructure |
| Output | Description |
|--------|-------------|
| .xlsx | Excel database with elements and properties |
| .dae | Collada 3D geometry with matching IDs |
| Option | Description |
|--------|-------------|
| bbox | Include element bounding boxes |
| -no-xlsx | Skip Excel export |
| -no-collada | Skip 3D geometry export |
# Basic conversion (XLSX + DAE)
IfcExporter.exe "C:\Models\Building.ifc"
# With bounding boxes
IfcExporter.exe "C:\Models\Building.ifc" bbox
# Excel only (no 3D geometry)
IfcExporter.exe "C:\Models\Building.ifc" -no-collada
# Batch processing
for /R "C:\IFC_Models" %f in (*.ifc) do IfcExporter.exe "%f" bbox
import subprocess
import pandas as pd
from pathlib import Path
from typing import List, Optional, Dict, Any, Set
from dataclasses import dataclass, field
from enum import Enum
import json
class IFCVersion(Enum):
"""IFC schema versions."""
IFC2X3 = "IFC2X3"
IFC4 = "IFC4"
IFC4X1 = "IFC4X1"
IFC4X3 = "IFC4X3"
class IFCEntityType(Enum):
"""Common IFC entity types."""
IFCWALL = "IfcWall"
IFCWALLSTANDARDCASE = "IfcWallStandardCase"
IFCSLAB = "IfcSlab"
IFCCOLUMN = "IfcColumn"
IFCBEAM = "IfcBeam"
IFCDOOR = "IfcDoor"
IFCWINDOW = "IfcWindow"
IFCROOF = "IfcRoof"
IFCSTAIR = "IfcStair"
IFCRAILING = "IfcRailing"
IFCFURNISHINGELEMENT = "IfcFurnishingElement"
IFCSPACE = "IfcSpace"
IFCBUILDINGSTOREY = "IfcBuildingStorey"
IFCBUILDING = "IfcBuilding"
IFCSITE = "IfcSite"
@dataclass
class IFCElement:
"""Represents an IFC element."""
global_id: str
ifc_type: str
name: str
description: Optional[str]
object_type: Optional[str]
level: Optional[str]
# Quantities
area: Optional[float] = None
volume: Optional[float] = None
length: Optional[float] = None
height: Optional[float] = None
width: Optional[float] = None
# Bounding box (if exported)
bbox_min_x: Optional[float] = None
bbox_min_y: Optional[float] = None
bbox_min_z: Optional[float] = None
bbox_max_x: Optional[float] = None
bbox_max_y: Optional[float] = None
bbox_max_z: Optional[float] = None
# Properties
properties: Dict[str, Any] = field(default_factory=dict)
materials: List[str] = field(default_factory=list)
@dataclass
class IFCProperty:
"""Represents an IFC property."""
pset_name: str
property_name: str
value: Any
value_type: str
@dataclass
class IFCMaterial:
"""Represents an IFC material."""
name: str
category: Optional[str]
thickness: Optional[float]
layer_position: Optional[int]
class IFCExporter:
"""IFC to Excel converter using DDC IfcExporter CLI."""
def __init__(self, exporter_path: str = "IfcExporter.exe"):
self.exporter = Path(exporter_path)
if not self.exporter.exists():
raise FileNotFoundError(f"IfcExporter not found: {exporter_path}")
def convert(self, ifc_file: str,
include_bbox: bool = True,
export_xlsx: bool = True,
export_collada: bool = True) -> Path:
"""Convert IFC file to Excel."""
ifc_path = Path(ifc_file)
if not ifc_path.exists():
raise FileNotFoundError(f"IFC file not found: {ifc_file}")
cmd = [str(self.exporter), str(ifc_path)]
if include_bbox:
cmd.append("bbox")
if not export_xlsx:
cmd.append("-no-xlsx")
if not export_collada:
cmd.append("-no-collada")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
raise RuntimeError(f"Export failed: {result.stderr}")
return ifc_path.with_suffix('.xlsx')
def batch_convert(self, folder: str,
include_subfolders: bool = True,
include_bbox: bool = True) -> List[Dict[str, Any]]:
"""Convert all IFC files in folder."""
folder_path = Path(folder)
pattern = "**/*.ifc" if include_subfolders else "*.ifc"
results = []
for ifc_file in folder_path.glob(pattern):
try:
output = self.convert(str(ifc_file), include_bbox)
results.append({
'input': str(ifc_file),
'output': str(output),
'status': 'success'
})
print(f"✓ Converted: {ifc_file.name}")
except Exception as e:
results.append({
'input': str(ifc_file),
'output': None,
'status': 'failed',
'error': str(e)
})
print(f"✗ Failed: {ifc_file.name} - {e}")
return results
def read_elements(self, xlsx_file: str) -> pd.DataFrame:
"""Read converted Excel as DataFrame."""
return pd.read_excel(xlsx_file, sheet_name="Elements")
def get_element_types(self, xlsx_file: str) -> pd.DataFrame:
"""Get element type summary."""
df = self.read_elements(xlsx_file)
if 'IfcType' not in df.columns:
raise ValueError("IfcType column not found")
summary = df.groupby('IfcType').agg({
'GlobalId': 'count',
'Volume': 'sum' if 'Volume' in df.columns else 'count',
'Area': 'sum' if 'Area' in df.columns else 'count'
}).reset_index()
summary.columns = ['IFC_Type', 'Count', 'Total_Volume', 'Total_Area']
return summary.sort_values('Count', ascending=False)
def get_levels(self, xlsx_file: str) -> pd.DataFrame:
"""Get building level summary."""
df = self.read_elements(xlsx_file)
level_col = None
for col in ['Level', 'BuildingStorey', 'IfcBuildingStorey']:
if col in df.columns:
level_col = col
break
if level_col is None:
return pd.DataFrame(columns=['Level', 'Element_Count'])
summary = df.groupby(level_col).agg({
'GlobalId': 'count'
}).reset_index()
summary.columns = ['Level', 'Element_Count']
return summary
def get_materials(self, xlsx_file: str) -> pd.DataFrame:
"""Get material summary."""
df = self.read_elements(xlsx_file)
if 'Material' not in df.columns:
return pd.DataFrame(columns=['Material', 'Count'])
summary = df.groupby('Material').agg({
'GlobalId': 'count'
}).reset_index()
summary.columns = ['Material', 'Element_Count']
return summary.sort_values('Element_Count', ascending=False)
def get_quantities(self, xlsx_file: str,
group_by: str = 'IfcType') -> pd.DataFrame:
"""Get quantity takeoff summary."""
df = self.read_elements(xlsx_file)
if group_by not in df.columns:
raise ValueError(f"Column {group_by} not found")
agg_dict = {'GlobalId': 'count'}
# Add numeric columns for aggregation
numeric_cols = ['Volume', 'Area', 'Length', 'Width', 'Height']
for col in numeric_cols:
if col in df.columns:
agg_dict[col] = 'sum'
summary = df.groupby(group_by).agg(agg_dict).reset_index()
return summary
def filter_by_type(self, xlsx_file: str,
ifc_types: List[str]) -> pd.DataFrame:
"""Filter elements by IFC type."""
df = self.read_elements(xlsx_file)
return df[df['IfcType'].isin(ifc_types)]
def get_properties(self, xlsx_file: str,
element_id: str) -> Dict[str, Any]:
"""Get all properties for specific element."""
df = self.read_elements(xlsx_file)
element = df[df['GlobalId'] == element_id]
if element.empty:
return {}
# Convert row to dictionary, excluding NaN values
props = element.iloc[0].dropna().to_dict()
return props
def validate_ifc_data(self, xlsx_file: str) -> Dict[str, Any]:
"""Validate IFC data quality."""
df = self.read_elements(xlsx_file)
validation = {
'total_elements': len(df),
'issues': []
}
# Check for missing GlobalIds
if 'GlobalId' in df.columns:
missing_ids = df['GlobalId'].isna().sum()
if missing_ids > 0:
validation['issues'].append(f"{missing_ids} elements missing GlobalId")
# Check for missing names
if 'Name' in df.columns:
missing_names = df['Name'].isna().sum()
if missing_names > 0:
validation['issues'].append(f"{missing_names} elements missing Name")
# Check for zero quantities
for col in ['Volume', 'Area']:
if col in df.columns:
zero_qty = (df[col] == 0).sum()
if zero_qty > 0:
validation['issues'].append(f"{zero_qty} elements with zero {col}")
# Check for duplicate GlobalIds
if 'GlobalId' in df.columns:
duplicates = df['GlobalId'].duplicated().sum()
if duplicates > 0:
validation['issues'].append(f"{duplicates} duplicate GlobalIds")
validation['is_valid'] = len(validation['issues']) == 0
return validation
class IFCQuantityTakeoff:
"""Quantity takeoff from IFC data."""
def __init__(self, exporter: IFCExporter):
self.exporter = exporter
def generate_qto(self, ifc_file: str) -> Dict[str, pd.DataFrame]:
"""Generate complete quantity takeoff."""
xlsx = self.exporter.convert(ifc_file, include_bbox=True)
df = self.exporter.read_elements(str(xlsx))
qto = {}
# Walls
walls = df[df['IfcType'].str.contains('Wall', case=False, na=False)]
if not walls.empty:
qto['Walls'] = self._summarize_elements(walls, 'Type Name')
# Slabs
slabs = df[df['IfcType'].str.contains('Slab', case=False, na=False)]
if not slabs.empty:
qto['Slabs'] = self._summarize_elements(slabs, 'Type Name')
# Columns
columns = df[df['IfcType'].str.contains('Column', case=False, na=False)]
if not columns.empty:
qto['Columns'] = self._summarize_elements(columns, 'Type Name')
# Beams
beams = df[df['IfcType'].str.contains('Beam', case=False, na=False)]
if not beams.empty:
qto['Beams'] = self._summarize_elements(beams, 'Type Name')
# Doors
doors = df[df['IfcType'].str.contains('Door', case=False, na=False)]
if not doors.empty:
qto['Doors'] = self._summarize_elements(doors, 'Type Name')
# Windows
windows = df[df['IfcType'].str.contains('Window', case=False, na=False)]
if not windows.empty:
qto['Windows'] = self._summarize_elements(windows, 'Type Name')
return qto
def _summarize_elements(self, df: pd.DataFrame,
group_col: str) -> pd.DataFrame:
"""Summarize elements by grouping column."""
if group_col not in df.columns:
group_col = 'IfcType'
agg_dict = {'GlobalId': 'count'}
for col in ['Volume', 'Area', 'Length']:
if col in df.columns:
agg_dict[col] = 'sum'
summary = df.groupby(group_col).agg(agg_dict).reset_index()
summary.rename(columns={'GlobalId': 'Count'}, inplace=True)
return summary
def export_to_excel(self, qto: Dict[str, pd.DataFrame],
output_file: str):
"""Export QTO to multi-sheet Excel."""
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
for sheet_name, df in qto.items():
df.to_excel(writer, sheet_name=sheet_name, index=False)
# Convenience functions
def convert_ifc_to_excel(ifc_file: str,
exporter_path: str = "IfcExporter.exe") -> str:
"""Quick conversion of IFC to Excel."""
exporter = IFCExporter(exporter_path)
output = exporter.convert(ifc_file)
return str(output)
def get_ifc_summary(xlsx_file: str) -> Dict[str, Any]:
"""Get summary of converted IFC data."""
df = pd.read_excel(xlsx_file, sheet_name="Elements")
return {
'total_elements': len(df),
'ifc_types': df['IfcType'].nunique() if 'IfcType' in df.columns else 0,
'levels': df['Level'].nunique() if 'Level' in df.columns else 0,
'total_volume': df['Volume'].sum() if 'Volume' in df.columns else 0,
'total_area': df['Area'].sum() if 'Area' in df.columns else 0
}
| Sheet | Content |
|-------|---------|
| Elements | All IFC elements with properties |
| Types | Element types summary |
| Levels | Building storey data |
| Materials | Material assignments |
| PropertySets | IFC property sets |
| Column | Type | Description |
|--------|------|-------------|
| GlobalId | string | IFC GUID |
| IfcType | string | IFC entity type |
| Name | string | Element name |
| Description | string | Element description |
| Level | string | Building storey |
| Material | string | Primary material |
| Volume | float | Volume (m³) |
| Area | float | Surface area (m²) |
| Length | float | Length (m) |
| Height | float | Height (m) |
| Width | float | Width (m) |
# Initialize exporter
exporter = IFCExporter("C:/DDC/IfcExporter.exe")
# Convert IFC to Excel
xlsx = exporter.convert("C:/Models/Building.ifc", include_bbox=True)
# Read elements
df = exporter.read_elements(str(xlsx))
print(f"Total elements: {len(df)}")
# Get element types
types = exporter.get_element_types(str(xlsx))
print(types)
# Get quantities by type
qto = exporter.get_quantities(str(xlsx), group_by='IfcType')
print(qto)
exporter = IFCExporter()
xlsx = exporter.convert("model.ifc")
validation = exporter.validate_ifc_data(str(xlsx))
if not validation['is_valid']:
print("Issues found:")
for issue in validation['issues']:
print(f" - {issue}")
qto_generator = IFCQuantityTakeoff(exporter)
qto = qto_generator.generate_qto("building.ifc")
for category, data in qto.items():
print(f"\n{category}:")
print(data.to_string(index=False))
xlsx = exporter.convert("building.ifc")
materials = exporter.get_materials(str(xlsx))
print(materials)
# Full pipeline: IFC → Excel → Validation → Cost Estimate
exporter = IFCExporter("C:/DDC/IfcExporter.exe")
# 1. Convert IFC
xlsx = exporter.convert("project.ifc", include_bbox=True)
# 2. Validate data
validation = exporter.validate_ifc_data(str(xlsx))
print(f"Valid: {validation['is_valid']}")
# 3. Generate QTO
qto = IFCQuantityTakeoff(exporter)
quantities = qto.generate_qto("project.ifc")
# 4. Export for cost estimation
qto.export_to_excel(quantities, "project_qto.xlsx")
Unified Python interface to 40+ bioinformatics services. Use when querying multiple databases (UniProt, KEGG, ChEMBL, Reactome) in a single workflow with consistent API. Best for cross-database analysis, ID mapping across services. For quick single-database lookups use gget; for sequence/file manipulation use biopython.
Python library for working with geospatial vector data including shapefiles, GeoJSON, and GeoPackage files. Use when working with geographic data for spatial analysis, geometric operations, coordinate transformations, spatial joins, overlay operations, choropleth mapping, or any task involving reading/writing/analyzing vector geographic data. Supports PostGIS databases, interactive maps, and integration with matplotlib/folium/cartopy. Use for tasks like buffer analysis, spatial joins between datasets, dissolving boundaries, clipping data, calculating areas/distances, reprojecting coordinate systems, creating maps, or converting between spatial file formats.
Fast CLI/Python queries to 20+ bioinformatics databases. Use for quick lookups: gene info, BLAST searches, AlphaFold structures, enrichment analysis. Best for interactive exploration, simple queries. For batch processing or advanced BLAST use biopython; for multi-database Python workflows use bioservices.
Direct REST API access to UniProt. Protein searches, FASTA retrieval, ID mapping, Swiss-Prot/TrEMBL. For Python workflows with multiple databases, prefer bioservices (unified interface to 40+ services). Use this for direct HTTP/REST work or UniProt-specific control.
Direct REST API access to UniProt. Protein searches, FASTA retrieval, ID mapping, Swiss-Prot/TrEMBL. For Python workflows with multiple databases, prefer bioservices (unified interface to 40+ services). Use this for direct HTTP/REST work or UniProt-specific control.
BullMQ expert for Redis-backed job queues, background processing, and reliable async execution in Node.js/TypeScript applications. Use when: bullmq, bull queue, redis queue, background job, job queue.
Create custom external web service APIs for Moodle LMS. Use when implementing web services for course management, user tracking, quiz operations, or custom plugin functionality. Covers parameter validation, database operations, error handling, service registration, and Moodle coding standards.
Python library for working with geospatial vector data including shapefiles, GeoJSON, and GeoPackage files. Use when working with geographic data for spatial analysis, geometric operations, coordinate transformations, spatial joins, overlay operations, choropleth mapping, or any task involving reading/writing/analyzing vector geographic data. Supports PostGIS databases, interactive maps, and integration with matplotlib/folium/cartopy. Use for tasks like buffer analysis, spatial joins between datasets, dissolving boundaries, clipping data, calculating areas/distances, reprojecting coordinate systems, creating maps, or converting between spatial file formats.
Take datadrivenconstruction/ifc-to-excel 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.