excel-to-bim
skillPush Excel data back to BIM models. Update parameters, properties, and attributes from structured spreadsheets.
apm::install
apm install @datadrivenconstruction/excel-to-bimapm::skill.md
---
name: "excel-to-bim"
description: "Push Excel data back to BIM models. Update parameters, properties, and attributes from structured spreadsheets."
homepage: "https://datadrivenconstruction.io"
metadata: {"openclaw": {"emoji": "📄", "os": ["win32"], "homepage": "https://datadrivenconstruction.io", "requires": {"bins": ["python3"], "anyBins": ["ifcopenshell"]}}}
---
# Excel to BIM Update
## Business Case
### Problem Statement
After extracting BIM data to Excel and enriching it (cost codes, classifications, custom data):
- Changes need to flow back to the BIM model
- Manual re-entry is error-prone
- Updates must match by element ID
### Solution
Push Excel data back to BIM models, updating element parameters and properties from spreadsheet changes.
### Business Value
- **Bi-directional workflow** - BIM → Excel → BIM
- **Bulk updates** - Change thousands of parameters
- **Data enrichment** - Add classifications, codes, costs
- **Consistency** - Spreadsheet as single source of truth
## Technical Implementation
### Workflow
```
BIM Model (Revit/IFC) → Excel Export → Data Enrichment → Excel Update → BIM Model
```
### Python Implementation
```python
import pandas as pd
from pathlib import Path
from typing import Dict, Any, List, Optional, Tuple
from dataclasses import dataclass, field
from enum import Enum
import json
class UpdateType(Enum):
"""Type of BIM parameter update."""
TEXT = "text"
NUMBER = "number"
BOOLEAN = "boolean"
ELEMENT_ID = "element_id"
@dataclass
class ParameterMapping:
"""Mapping between Excel column and BIM parameter."""
excel_column: str
bim_parameter: str
update_type: UpdateType
transform: Optional[str] = None # Optional transformation
@dataclass
class UpdateResult:
"""Result of single element update."""
element_id: str
parameters_updated: List[str]
success: bool
error: Optional[str] = None
@dataclass
class BatchUpdateResult:
"""Result of batch update operation."""
total_elements: int
updated: int
failed: int
skipped: int
results: List[UpdateResult]
class ExcelToBIMUpdater:
"""Update BIM models from Excel data."""
# Standard ID column names
ID_COLUMNS = ['ElementId', 'GlobalId', 'GUID', 'Id', 'UniqueId']
def __init__(self):
self.mappings: List[ParameterMapping] = []
def add_mapping(self, excel_col: str, bim_param: str,
update_type: UpdateType = UpdateType.TEXT):
"""Add column to parameter mapping."""
self.mappings.append(ParameterMapping(
excel_column=excel_col,
bim_parameter=bim_param,
update_type=update_type
))
def load_excel(self, file_path: str,
sheet_name: str = None) -> pd.DataFrame:
"""Load Excel data for update."""
if sheet_name:
return pd.read_excel(file_path, sheet_name=sheet_name)
return pd.read_excel(file_path)
def detect_id_column(self, df: pd.DataFrame) -> Optional[str]:
"""Detect element ID column in DataFrame."""
for col in self.ID_COLUMNS:
if col in df.columns:
return col
# Case-insensitive check
for df_col in df.columns:
if df_col.lower() == col.lower():
return df_col
return None
def prepare_updates(self, df: pd.DataFrame,
id_column: str = None) -> List[Dict[str, Any]]:
"""Prepare update instructions from DataFrame."""
if id_column is None:
id_column = self.detect_id_column(df)
if id_column is None:
raise ValueError("Cannot detect ID column")
updates = []
for _, row in df.iterrows():
element_id = str(row[id_column])
params = {}
for mapping in self.mappings:
if mapping.excel_column in df.columns:
value = row[mapping.excel_column]
# Convert value based on type
if mapping.update_type == UpdateType.NUMBER:
value = float(value) if pd.notna(value) else 0
elif mapping.update_type == UpdateType.BOOLEAN:
value = bool(value) if pd.notna(value) else False
elif mapping.update_type == UpdateType.TEXT:
value = str(value) if pd.notna(value) else ""
params[mapping.bim_parameter] = value
if params:
updates.append({
'element_id': element_id,
'parameters': params
})
return updates
def generate_dynamo_script(self, updates: List[Dict],
output_path: str) -> str:
"""Generate Dynamo script for Revit updates."""
# Generate Python code for Dynamo
script = '''
# Dynamo Python Script for Revit Parameter Updates
# Generated by DDC Excel-to-BIM
import clr
clr.AddReference('RevitAPI')
clr.AddReference('RevitServices')
from RevitServices.Persistence import DocumentManager
from RevitServices.Transactions import TransactionManager
from Autodesk.Revit.DB import *
doc = DocumentManager.Instance.CurrentDBDocument
# Update data
updates = '''
script += json.dumps(updates, indent=2)
script += '''
# Apply updates
TransactionManager.Instance.EnsureInTransaction(doc)
results = []
for update in updates:
try:
element_id = int(update['element_id'])
element = doc.GetElement(ElementId(element_id))
if element:
for param_name, value in update['parameters'].items():
param = element.LookupParameter(param_name)
if param and not param.IsReadOnly:
if isinstance(value, (int, float)):
param.Set(float(value))
elif isinstance(value, bool):
param.Set(1 if value else 0)
else:
param.Set(str(value))
results.append({'id': element_id, 'status': 'success'})
else:
results.append({'id': element_id, 'status': 'not found'})
except Exception as e:
results.append({'id': update['element_id'], 'status': str(e)})
TransactionManager.Instance.TransactionTaskDone()
OUT = results
'''
with open(output_path, 'w') as f:
f.write(script)
return output_path
def generate_ifc_updates(self, updates: List[Dict],
original_ifc: str,
output_ifc: str) -> str:
"""Generate updated IFC file (requires IfcOpenShell)."""
try:
import ifcopenshell
except ImportError:
raise ImportError("IfcOpenShell required for IFC updates")
ifc = ifcopenshell.open(original_ifc)
for update in updates:
guid = update['element_id']
# Find element by GUID
element = ifc.by_guid(guid)
if not element:
continue
# Update properties
for param_name, value in update['parameters'].items():
# This is simplified - actual IFC property handling is more complex
# Would need to find/create property sets and properties
pass
ifc.write(output_ifc)
return output_ifc
def generate_update_report(self, original_df: pd.DataFrame,
updates: List[Dict],
output_path: str) -> str:
"""Generate report of planned updates."""
report_data = []
for update in updates:
for param, value in update['parameters'].items():
report_data.append({
'element_id': update['element_id'],
'parameter': param,
'new_value': value
})
report_df = pd.DataFrame(report_data)
report_df.to_excel(output_path, index=False)
return output_path
class RevitExcelUpdater(ExcelToBIMUpdater):
"""Specialized updater for Revit via ImportExcelToRevit."""
def __init__(self, tool_path: str = "ImportExcelToRevit.exe"):
super().__init__()
self.tool_path = Path(tool_path)
def update_revit(self, excel_file: str,
rvt_file: str,
sheet_name: str = "Elements") -> BatchUpdateResult:
"""Update Revit file from Excel using CLI tool."""
import subprocess
# This assumes ImportExcelToRevit CLI tool
cmd = [
str(self.tool_path),
rvt_file,
excel_file,
sheet_name
]
result = subprocess.run(cmd, capture_output=True, text=True)
# Parse results (format depends on tool output)
if result.returncode == 0:
return BatchUpdateResult(
total_elements=0, # Would parse from output
updated=0,
failed=0,
skipped=0,
results=[]
)
else:
raise RuntimeError(f"Update failed: {result.stderr}")
class DataEnrichmentWorkflow:
"""Complete workflow for data enrichment and update."""
def __init__(self):
self.updater = ExcelToBIMUpdater()
def enrich_and_update(self, original_excel: str,
enrichment_excel: str,
merge_column: str) -> pd.DataFrame:
"""Merge enrichment data with original export."""
original = pd.read_excel(original_excel)
enrichment = pd.read_excel(enrichment_excel)
# Merge on specified column
merged = original.merge(enrichment, on=merge_column, how='left',
suffixes=('', '_enriched'))
return merged
def create_classification_mapping(self, df: pd.DataFrame,
type_column: str,
classification_file: str) -> pd.DataFrame:
"""Map BIM types to classification codes."""
classifications = pd.read_excel(classification_file)
# Fuzzy matching could be added here
merged = df.merge(classifications,
left_on=type_column,
right_on='type_description',
how='left')
return merged
```
## Quick Start
```python
# Initialize updater
updater = ExcelToBIMUpdater()
# Define mappings
updater.add_mapping('Classification_Code', 'OmniClassCode', UpdateType.TEXT)
updater.add_mapping('Unit_Cost', 'Cost', UpdateType.NUMBER)
# Load enriched Excel
df = updater.load_excel("enriched_model.xlsx")
# Prepare updates
updates = updater.prepare_updates(df)
print(f"Prepared {len(updates)} updates")
# Generate Dynamo script for Revit
updater.generate_dynamo_script(updates, "update_parameters.py")
```
## Common Use Cases
### 1. Add Classification Codes
```python
updater = ExcelToBIMUpdater()
updater.add_mapping('Omniclass', 'OmniClass_Number', UpdateType.TEXT)
updater.add_mapping('Uniclass', 'Uniclass_Code', UpdateType.TEXT)
df = updater.load_excel("classified_elements.xlsx")
updates = updater.prepare_updates(df)
```
### 2. Cost Data Integration
```python
updater.add_mapping('Material_Cost', 'Pset_MaterialCost', UpdateType.NUMBER)
updater.add_mapping('Labor_Cost', 'Pset_LaborCost', UpdateType.NUMBER)
```
### 3. Generate Update Report
```python
report = updater.generate_update_report(df, updates, "planned_updates.xlsx")
```
## Integration with DDC Pipeline
```python
# Full round-trip: Revit → Excel → Enrich → Update → Revit
# 1. Export from Revit
# RvtExporter.exe model.rvt complete
# 2. Enrich in Python/Excel
df = pd.read_excel("model.xlsx")
# Add classifications, costs, etc.
df['OmniClass'] = df['Type Name'].map(classification_dict)
df.to_excel("enriched_model.xlsx")
# 3. Generate update script
updater = ExcelToBIMUpdater()
updater.add_mapping('OmniClass', 'OmniClass_Number')
updates = updater.prepare_updates(df)
updater.generate_dynamo_script(updates, "apply_updates.py")
# 4. Run in Dynamo to update Revit
```
## Resources
- **GitHub**: [DDC Update Revit from Excel](https://github.com/datadrivenconstruction/cad2data-Revit-IFC-DWG-DGN-pipeline-with-conversion-validation-qto/tree/main/DDC_Update_Revit_from_Excel)
- **DDC Book**: Chapter 2.4 - Bidirectional Data Flow