Day 23 – Weekly Project: BIM Data Analyser CLI
🏗️ Day 23 – Weekly Project: BIM Data Analyser CLI
1. Learning Objectives
By the end of Day 23, you will be able to:
- Build a complete command‑line tool that integrates all Phase 3 concepts (generators, decorators, context managers, IFC processing, numpy, visualisation).
- Read an IFC model and extract building elements (walls, slabs, columns, beams) with their properties.
- Compute quantities (volumes, areas) grouped by type and by storey using generators for memory efficiency.
- Perform a basic clash check by testing bounding box intersections between element pairs.
- Export results as a CSV report and an interactive HTML dashboard using plotly.
- Package the tool as a self‑contained Python script that accepts command‑line arguments.
2. Concept Explanation
2.1 What We’re Building
The BIM Data Analyser CLI is a command‑line tool that processes an IFC file and produces:
- A quantity take‑off (volumes, areas, counts) grouped by element type and storey.
- A clash detection report that flags pairs of elements whose bounding boxes intersect.
- An interactive dashboard (HTML) for visualising the results.
This tool mirrors real‑world BIM automation workflows used in design offices for early‑stage coordination and quantity estimation.
2.2 Architecture
bim_analyser.py
├── CLI entry point (argparse)
├── IFC reader (generators)
├── Quantity calculator (type/storey grouping)
├── Clash detector (bounding box test)
├── Report writer (CSV + plotly HTML)
└── Main orchestrator with decorators (timing, logging)
2.3 Design Decisions
- Generators (Day 16) – lazy iteration over thousands of IFC elements without loading all into memory.
- Decorators (Day 17) – timing and logging for each analysis stage.
- Context managers (Day 18) – safe file handling for CSV and HTML output.
- IfcOpenShell (Day 19) – reading IFC entities and property sets.
- numpy (Day 20) – bounding box calculations.
- plotly (Day 21) – interactive dashboard.
3. Code Examples
3.1 Project Structure
bim_analyser/
__init__.py
cli.py # argparse entry point
ifc_reader.py # generator for IFC elements
quantities.py # quantity aggregation
clash.py # bounding box clash detection
report.py # CSV + HTML report generation
utils.py # decorators, context managers
3.2 IFC Reader (Generator)
# ifc_reader.py
import ifcopenshell
from ifcopenshell.util import element as ifc_element
def read_elements_lazy(ifc_path):
"""Generator that yields IFC elements one at a time with key properties."""
model = ifcopenshell.open(ifc_path)
# Get all storeys first for lookup
storeys = {s.GlobalId: s.Name for s in model.by_type("IfcBuildingStorey")}
for etype in ["IfcWall", "IfcSlab", "IfcColumn", "IfcBeam", "IfcWindow", "IfcDoor"]:
elements = model.by_type(etype)
for elem in elements:
# Find storey
storey_name = "Unknown"
for rel in getattr(elem, "ContainedInStructure", []):
if rel.RelatingStructure.is_a("IfcBuildingStorey"):
storey_name = rel.RelatingStructure.Name
break
# Get quantity set
qsets = ifc_element.get_psets(elem, psets_only=False)
qto_name = f"Qto_{etype.replace('Ifc','')}BaseQuantities"
volume = None
area = None
if qto_name in qsets:
volume = qsets[qto_name].get("GrossVolume")
area = qsets[qto_name].get("GrossArea")
yield {
"global_id": elem.GlobalId,
"type": etype.replace("Ifc", ""),
"name": elem.Name or "",
"storey": storey_name,
"volume": volume or 0.0,
"area": area or 0.0
}
3.3 Quantity Aggregator
# quantities.py
def aggregate_by_type(elements):
"""elements is a generator – returns dict of type -> (count, total_volume, total_area)"""
result = {}
for elem in elements:
etype = elem["type"]
if etype not in result:
result[etype] = {"count": 0, "volume": 0.0, "area": 0.0}
result[etype]["count"] += 1
result[etype]["volume"] += elem["volume"]
result[etype]["area"] += elem["area"]
return result
def aggregate_by_storey(elements):
"""Returns dict of storey -> dict of type -> (count, volume, area)"""
result = {}
for elem in elements:
storey = elem["storey"]
etype = elem["type"]
if storey not in result:
result[storey] = {}
if etype not in result[storey]:
result[storey][etype] = {"count": 0, "volume": 0.0, "area": 0.0}
result[storey][etype]["count"] += 1
result[storey][etype]["volume"] += elem["volume"]
result[storey][etype]["area"] += elem["area"]
return result
3.4 Clash Detection (Bounding Box)
# clash.py
import numpy as np
def get_bounding_box(elem):
"""Extract bounding box from IFC element geometry (simplified)."""
# In a full implementation, use ifcopenshell.geometry to get the shape
# For this simplified version, we use the quantity dimensions
# Returns (xmin, ymin, zmin, xmax, ymax, zmax) or None
try:
placement = elem.ObjectPlacement
if not placement:
return None
# Simplified: assume we have width, depth, height from quantities
# This is a placeholder – real implementation requires geometry parsing
return None # Simplified for this exercise
except:
return None
def check_clashes_naive(elements):
"""Simple clash check using element names (demonstration)."""
# In real practice, this would use bounding box overlap tests.
# For this exercise, we'll flag elements on the same storey with similar names.
clashes = []
elem_list = list(elements) # Need to consume generator
for i, e1 in enumerate(elem_list):
for j, e2 in enumerate(elem_list):
if i >= j:
continue
# Simple heuristic: same type and same storey = potential clash
if e1["type"] == e2["type"] and e1["storey"] == e2["storey"]:
clashes.append({
"element_1": e1["name"] or e1["global_id"],
"element_2": e2["name"] or e2["global_id"],
"storey": e1["storey"],
"type": e1["type"]
})
return clashes
3.5 Report Writer (CSV + HTML Dashboard)
# report.py
import csv
from contextlib import contextmanager
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
@contextmanager
def export_report(output_prefix):
"""Context manager for report export."""
print(f"Exporting reports with prefix: {output_prefix}")
yield output_prefix
print(f"Reports saved to {output_prefix}_*")
def write_quantity_csv(qty_by_type, filename):
with open(filename, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Type", "Count", "Volume (m³)", "Area (m²)"])
for etype, data in sorted(qty_by_type.items()):
writer.writerow([etype, data["count"], f"{data['volume']:.2f}", f"{data['area']:.2f}"])
print(f"Quantity CSV: {filename}")
def write_clash_csv(clashes, filename):
with open(filename, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["Element 1", "Element 2", "Storey", "Type"])
for clash in clashes:
writer.writerow([clash["element_1"], clash["element_2"], clash["storey"], clash["type"]])
print(f"Clash CSV: {filename}")
def create_dashboard(qty_by_type, qty_by_storey, clashes, output_html):
"""Create an interactive HTML dashboard with plotly."""
# Quantity by type bar chart
df_type = pd.DataFrame([
{"Type": t, "Count": d["count"], "Volume": d["volume"], "Area": d["area"]}
for t, d in qty_by_type.items()
])
fig1 = px.bar(df_type, x="Type", y="Volume", title="Volume by Element Type",
hover_data=["Count", "Area"], color="Type")
# Quantity by storey pie chart (total volume per storey)
storey_volumes = {}
for storey, types in qty_by_storey.items():
storey_volumes[storey] = sum(d["volume"] for d in types.values())
df_storey = pd.DataFrame([
{"Storey": s, "Volume": v} for s, v in storey_volumes.items()
])
fig2 = px.pie(df_storey, values="Volume", names="Storey", title="Volume Distribution by Storey")
# Clash table as a heatmap (if clashes exist)
if clashes:
clash_df = pd.DataFrame(clashes)
clash_summary = clash_df.groupby(["storey", "type"]).size().reset_index(name="clash_count")
fig3 = px.scatter(clash_summary, x="storey", y="type", size="clash_count",
title="Clash Summary (bubble size = count)")
else:
fig3 = go.Figure()
fig3.add_annotation(text="No clashes detected", x=0.5, y=0.5, showarrow=False)
# Combine into dashboard
fig = make_subplots(rows=2, cols=2,
subplot_titles=("Volume by Type", "Volume by Storey", "Clash Summary", ""),
specs=[[{"type": "bar"}, {"type": "pie"}], [{"type": "scatter"}, {}]])
for trace in fig1.data:
fig.add_trace(trace, row=1, col=1)
for trace in fig2.data:
fig.add_trace(trace, row=1, col=2)
for trace in fig3.data:
fig.add_trace(trace, row=2, col=1)
fig.update_layout(height=800, width=1200, title_text="BIM Analysis Dashboard")
fig.write_html(output_html)
print(f"Dashboard: {output_html}")
3.6 Decorators for Timing and Logging
# utils.py
import time
from datetime import datetime
def timer(func):
"""Decorator that prints execution time."""
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"[TIMER] {func.__name__} took {elapsed:.2f}s")
return result
return wrapper
def log_call(func):
"""Decorator that logs function calls."""
def wrapper(*args, **kwargs):
ts = datetime.now().strftime("%H:%M:%S")
print(f"[LOG {ts}] Calling {func.__name__}...")
result = func(*args, **kwargs)
print(f"[LOG {ts}] {func.__name__} completed.")
return result
return wrapper
3.7 CLI Entry Point
# cli.py
import argparse
from .ifc_reader import read_elements_lazy
from .quantities import aggregate_by_type, aggregate_by_storey
from .clash import check_clashes_naive
from .report import write_quantity_csv, write_clash_csv, create_dashboard, export_report
from .utils import timer, log_call
@timer
@log_call
def run_analysis(ifc_path, output_prefix):
"""Main analysis pipeline."""
print(f"\n=== BIM Analyser: {ifc_path} ===\n")
# Step 1: Read elements (lazy generator)
elements = read_elements_lazy(ifc_path)
# Step 2: Compute quantities (need to consume generator – use list for multi-pass)
# In production, you might stream to disk or use itertools.tee
elem_list = list(elements)
print(f"Total elements loaded: {len(elem_list)}")
qty_by_type = aggregate_by_type(elem_list)
qty_by_storey = aggregate_by_storey(elem_list)
# Print summary
print("\n--- Quantity Summary by Type ---")
for etype, data in sorted(qty_by_type.items()):
print(f" {etype}: {data['count']} elements, {data['volume']:.2f} m³, {data['area']:.2f} m²")
# Step 3: Clash detection
clashes = check_clashes_naive(elem_list)
print(f"\n--- Clashes: {len(clashes)} detected ---")
# Step 4: Export reports
with export_report(output_prefix):
write_quantity_csv(qty_by_type, f"{output_prefix}_quantities.csv")
write_clash_csv(clashes, f"{output_prefix}_clashes.csv")
create_dashboard(qty_by_type, qty_by_storey, clashes, f"{output_prefix}_dashboard.html")
print("\n=== Analysis Complete ===")
def main():
parser = argparse.ArgumentParser(description="BIM Data Analyser CLI")
parser.add_argument("ifc_file", help="Path to IFC file")
parser.add_argument("-o", "--output", default="bim_report", help="Output prefix")
args = parser.parse_args()
run_analysis(args.ifc_file, args.output)
if __name__ == "__main__":
main()
4. Hands‑on Exercises (3–5 Problems)
Problem 1 – Element count generator
Write a generator that iterates over an IFC file and yields only the element type and GlobalId. Use it to count how many walls are in the model without loading all elements.
Problem 2 – Volume accumulator with @timer
Decorate the aggregate_by_type function with the @timer decorator. Run it on an IFC file and observe the timing.
Problem 3 – Basic clash by name matching
Extend the clash detector to flag elements whose names contain the same number (e.g., "Wall-101" and "Column-101" on the same storey).
Problem 4 – CSV report context manager
Write a context manager csv_writer(filename, headers) that opens a CSV file, writes headers, yields a writer object, and auto‑closes on exit.
Problem 5 – Dashboard with storey filter
Modify the dashboard to include a dropdown that filters the bar chart by storey. (Use plotly dropdown update menus.)
Solutions (attempt first):
# P1
def element_ids(ifc_path):
model = ifcopenshell.open(ifc_path)
for etype in ["IfcWall", "IfcSlab", "IfcColumn", "IfcBeam"]:
for elem in model.by_type(etype):
yield elem.is_a(), elem.GlobalId
count = 0
for etype, gid in element_ids("model.ifc"):
if etype == "IfcWall":
count += 1
print(f"Walls: {count}")
# P2 – already decorated in cli.py
# P3
def clash_by_name(elements):
elem_list = list(elements)
clashes = []
for i, e1 in enumerate(elem_list):
for j, e2 in enumerate(elem_list):
if i >= j: continue
# Extract numbers from names
import re
nums1 = re.findall(r'\d+', e1.get("name",""))
nums2 = re.findall(r'\d+', e2.get("name",""))
if nums1 and nums2 and nums1[0] == nums2[0]:
clashes.append({"e1": e1["name"], "e2": e2["name"]})
return clashes
# P4
@contextmanager
def csv_writer(filename, headers):
with open(filename, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(headers)
yield writer
# P5 – (requires plotly dash or dropdowns – advanced)
5. Applied Challenge Task (The Full Project)
🏗️ Complete BIM Data Analyser CLI
Build the full tool as described in the code examples above. Your tool must:
Accept an IFC file path as a command‑line argument (use
argparse).Read elements lazily using a generator that yields dictionaries with type, name, storey, volume, area.
Compute quantities aggregated by type and by storey.
Perform basic clash detection – at minimum, flag elements of the same type on the same storey (or implement bounding box overlap using
ifcopenshell.geometryif you have it).Export:
- A CSV file with quantities by type.
- A CSV file with clash results.
- An interactive HTML dashboard with at least three charts.
Use decorators for timing (each major step) and logging (start/end messages).
Use a context manager for the report export workflow.
Print a clear summary to the console.
Bonus challenges:
- Add a
--filter-storeyargument to analyse only one storey. - Implement real bounding box clash detection using
ifcopenshell.geometry(requirespythonocc-core). - Generate a PDF report using
reportlaborweasyprint. - Package the tool with
setuptoolsso it can be installed withpip install.
Example invocation:
python -m bim_analyser.cli Duplex_A_20110907.ifc -o duplex_report
Expected console output:
=== BIM Analyser: Duplex_A_20110907.ifc ===
[LOG 14:30:01] Calling read_elements_lazy...
Total elements loaded: 342
[LOG 14:30:02] read_elements_lazy completed.
--- Quantity Summary by Type ---
Beam: 12 elements, 0.00 m³, 0.00 m²
Column: 24 elements, 0.00 m³, 0.00 m²
Slab: 6 elements, 72.50 m³, 290.00 m²
Wall: 42 elements, 85.20 m³, 0.00 m²
Window: 18 elements, 0.00 m³, 36.00 m²
Door: 12 elements, 0.00 m³, 24.00 m²
--- Clashes: 3 detected ---
Exporting reports with prefix: duplex_report
Quantity CSV: duplex_report_quantities.csv
Clash CSV: duplex_report_clashes.csv
Dashboard: duplex_report_dashboard.html
Reports saved to duplex_report_*
[TIMER] run_analysis took 4.23s
=== Analysis Complete ===
6. Phase 3 Review Summary
Over Days 16–23, you have learned:
| Day | Topic | Key AEC Skill |
|---|---|---|
| 16 | Iterators & Generators | Lazy traversal of large IFC models |
| 17 | Decorators | Timing, logging, caching for analysis functions |
| 18 | Context Managers | Safe file handling, temporary settings |
| 19 | BIM Data with IfcOpenShell | Read IFC, extract walls, query properties |
| 20 | Advanced numpy & scipy | Solve structural systems, optimise trusses |
| 21 | Advanced visualisation | Interactive 3D plots, dashboards with plotly |
| 22 | Scripting CAD environments | Automate Rhino/Grasshopper/Dynamo modelling |
| 23 | Weekly Project | Integrated BIM data analyser CLI |
You are now capable of:
- Reading and analysing real BIM models programmatically.
- Performing engineering calculations with numpy/scipy.
- Building interactive visualisations and dashboards.
- Scripting CAD tools for automated geometry generation.
- Structuring robust, reusable code with advanced Python features.
Key takeaway:
Phase 3 has given you professional‑grade skills for AEC computation. You can now work with real project data, perform analysis, and produce shareable outputs – all from Python.
7. Preview of Phase 4 (Days 24–30)
Tomorrow we begin Phase 4 – Professional AEC Automation. You will cover:
| Day | Topic | What You'll Build |
|---|---|---|
| 24 | Code optimisation & profiling | Optimise geometry algorithms for speed |
| 25 | Data structures & algorithms for AEC | Graphs for connectivity, octrees for search |
| 26 | Design patterns for AEC tools | Visitor for model traversal, Composite |
| 27 | Building a web API (Flask/FastAPI) | API that returns deflection given inputs |
| 28 | GUI development (Streamlit/Tkinter) | Energy model parameter editor |
| 29 | Testing, packaging, Git | pytest, setuptools, CI/CD for AEC tools |
| 30 | Capstone Project | Integrated design‑analysis automation system |
Prepare to build production‑ready, deployable tools that integrate with real engineering workflows.

Comments
Post a Comment