Day 15 – Weekly Mini‑Project: Parametric Truss Generator
🏗️ Day 15 – Weekly Mini‑Project: Parametric Truss Generator
1. Learning Objectives
By the end of Day 15, you will be able to:
- Integrate all Phase 2 concepts (functions, modules, file I/O, numpy, OOP) into a single tool.
- Design a parametric component generator that creates a standard truss geometry from a given span.
- Use classes to model the truss, members, and nodes.
- Export geometry as a CSV file ready for import into CAD (AutoCAD, Rhino, Revit).
- Write clean, reusable code with proper documentation and error handling.
2. Concept Explanation
2.1 What is a Parametric Component Generator?
A parametric component generator is a script that takes design parameters (like span, height, load) and automatically produces geometry, calculations, or fabrication data. In AEC practice, these generators are used for:
- Standard trusses and roof structures
- Staircases with variable rise/run
- Curtain wall grids
- Reinforcement layouts
Today you will build a Pratt truss generator – a common roof truss type. Given a span and a desired depth, the script will:
- Calculate the number of panels based on a standard bay width.
- Generate all node coordinates.
- Create member connections (top chord, bottom chord, verticals, diagonals).
- Compute approximate member lengths.
- Export a CSV with node coordinates and member connectivity.
2.2 Prerequisites – What We Bring From Phase 2
| Day | Skill | How we use it today |
|---|---|---|
| 8 | String formatting | Generate member labels, formatted CSV |
| 9 | File I/O | Write CSV for CAD import |
| 10 | Exception handling | Validate user inputs (span > 0, etc.) |
| 11 | Comprehensions / lambda | Efficiently create node lists |
| 12 | Modules | (Optional) structure code as a module |
| 13 | numpy / matplotlib | Generate coordinates, plot truss |
| 14 | Classes & OOP | Truss, Node, Member classes |
3. Code Example – Parametric Truss Generator
3.1 Class Design
We’ll use three classes: Node, Member, and Truss.
import numpy as np
import csv
import math
class Node:
"""A truss node with (x, y) coordinates and an ID."""
def __init__(self, node_id, x, y):
self.id = node_id
self.x = x
self.y = y
def __str__(self):
return f"N{self.id}({self.x:.3f}, {self.y:.3f})"
class Member:
"""A truss member connecting two nodes."""
def __init__(self, member_id, start_node, end_node, member_type="chord"):
self.id = member_id
self.start = start_node
self.end = end_node
self.type = member_type # "chord", "vertical", "diagonal"
self.length = self._compute_length()
def _compute_length(self):
dx = self.end.x - self.start.x
dy = self.end.y - self.start.y
return math.sqrt(dx**2 + dy**2)
def __str__(self):
return f"M{self.id}: {self.start.id}→{self.end.id} ({self.type}, L={self.length:.3f}m)"
class Truss:
"""A Pratt truss defined by span and depth."""
def __init__(self, span, depth, panel_width=2.0):
"""
span: total truss span (m)
depth: distance between chords (m)
panel_width: width of each bay (m)
"""
self.span = span
self.depth = depth
self.panel_width = panel_width
# Derived parameters
self.num_panels = int(math.ceil(span / panel_width))
self.actual_span = self.num_panels * panel_width # adjust span
self.nodes = [] # list of Node objects
self.members = [] # list of Member objects
self._generate_geometry()
def _generate_geometry(self):
"""Create nodes and members for a Pratt truss."""
n_panels = self.num_panels
pw = self.panel_width
d = self.depth
# --- Create nodes ---
# Bottom chord nodes: left to right
for i in range(n_panels + 1):
x = i * pw
y = 0.0
self.nodes.append(Node(f"B{i}", x, y))
# Top chord nodes: left to right (offset by half panel? No, Pratt has verticals at each panel point)
# For a Pratt truss, top chord nodes align with verticals at each panel point
for i in range(n_panels + 1):
x = i * pw
y = d
self.nodes.append(Node(f"T{i}", x, y))
# --- Create members ---
# Bottom chord
for i in range(n_panels):
m = Member(f"BC{i+1}", self.nodes[i], self.nodes[i+1], "chord")
self.members.append(m)
# Top chord
offset = n_panels + 1 # index offset for top nodes
for i in range(n_panels):
m = Member(f"TC{i+1}", self.nodes[offset+i], self.nodes[offset+i+1], "chord")
self.members.append(m)
# Verticals
# Panel points 1 to n_panels-1 have verticals (interior). Ends might have verticals as end posts.
for i in range(1, n_panels): # interior verticals
bottom_node = self.nodes[i]
top_node = self.nodes[offset+i]
m = Member(f"V{i}", bottom_node, top_node, "vertical")
self.members.append(m)
# End verticals (if Pratt has verticals at ends? Usually yes – end posts are vertical)
# For simplicity, add end verticals as well
# Left end
m = Member("V0", self.nodes[0], self.nodes[offset+0], "vertical")
self.members.append(m)
# Right end
m = Member(f"V{n_panels}", self.nodes[n_panels], self.nodes[offset+n_panels], "vertical")
self.members.append(m)
# Diagonals (slope down towards centre – typical Pratt)
# Diagonals go from bottom i to top i+1 (for i=0 to n_panels-2)
for i in range(n_panels - 1):
bottom = self.nodes[i+1]
top = self.nodes[offset+i]
m = Member(f"D{i+1}", bottom, top, "diagonal")
self.members.append(m)
# Also diagonals from bottom i to top i-1? Standard Pratt has diagonals in tension.
# We'll add the crossing diagonals for completeness (Warren style?)
# Actually a Pratt truss has diagonals sloping toward the centre.
# For simplicity, we have one set. The user can modify.
def total_member_length(self):
"""Sum of all member lengths."""
return sum(m.length for m in self.members)
def number_of_nodes(self):
return len(self.nodes)
def number_of_members(self):
return len(self.members)
def export_csv(self, filename="truss_geometry.csv"):
"""Export node coordinates and member connectivity to CSV."""
with open(filename, "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["NODE_ID", "X_m", "Y_m"])
for node in self.nodes:
writer.writerow([node.id, f"{node.x:.4f}", f"{node.y:.4f}"])
writer.writerow([]) # blank separator
writer.writerow(["MEMBER_ID", "START_NODE", "END_NODE", "TYPE", "LENGTH_m"])
for m in self.members:
writer.writerow([m.id, m.start.id, m.end.id, m.type, f"{m.length:.4f}"])
print(f"Geometry exported to {filename}")
def summary(self):
"""Print a summary of the truss."""
print("=" * 50)
print("TRUSS SUMMARY")
print("=" * 50)
print(f"Span: {self.span:.2f} m (actual: {self.actual_span:.2f} m)")
print(f"Depth: {self.depth:.2f} m")
print(f"Panel width: {self.panel_width:.2f} m")
print(f"Number of panels: {self.num_panels}")
print(f"Nodes: {self.number_of_nodes()}")
print(f"Members: {self.number_of_members()}")
print(f"Total member length: {self.total_member_length():.2f} m")
print("=" * 50)
# --- Demo ---
if __name__ == "__main__":
# Generate a truss with 12m span, 2m depth, 2m panel width
truss = Truss(span=12.0, depth=2.0, panel_width=2.0)
truss.summary()
truss.export_csv("truss_12m.csv")
# Also generate a 6m span for comparison
truss2 = Truss(span=6.0, depth=1.2, panel_width=1.5)
truss2.summary()
truss2.export_csv("truss_6m.csv")
3.2 Expected CSV Output
truss_12m.csv (first few lines):
NODE_ID,X_m,Y_m
B0,0.0000,0.0000
B1,2.0000,0.0000
B2,4.0000,0.0000
...
T0,0.0000,2.0000
T1,2.0000,2.0000
...
MEMBER_ID,START_NODE,END_NODE,TYPE,LENGTH_m
BC1,B0,B1,chord,2.0000
BC2,B1,B2,chord,2.0000
...
TC1,T0,T1,chord,2.0000
...
V1,B1,T1,vertical,2.0000
...
D1,B1,T0,diagonal,2.8284
...
3.3 Visualising the Truss (Optional, if matplotlib is installed)
import matplotlib.pyplot as plt
def plot_truss(truss, title="Truss Geometry"):
"""Plot the truss using matplotlib."""
fig, ax = plt.subplots(figsize=(12, 4))
# Plot members
for m in truss.members:
xs = [m.start.x, m.end.x]
ys = [m.start.y, m.end.y]
if m.type == "chord":
ax.plot(xs, ys, 'b-', linewidth=2)
elif m.type == "vertical":
ax.plot(xs, ys, 'g-', linewidth=1.5)
elif m.type == "diagonal":
ax.plot(xs, ys, 'r--', linewidth=1)
# Plot nodes
for n in truss.nodes:
ax.plot(n.x, n.y, 'ko', markersize=4)
ax.text(n.x + 0.1, n.y + 0.1, n.id, fontsize=7)
ax.set_xlabel("X (m)")
ax.set_ylabel("Y (m)")
ax.set_title(title)
ax.set_aspect('equal')
ax.grid(True, linestyle=':', alpha=0.5)
plt.tight_layout()
plt.show()
# Plot the demo truss
plot_truss(truss, "Pratt Truss – 12m Span")
4. Hands‑on Exercises (3–5 Problems)
Before diving into the full mini‑project, try these smaller exercises to warm up:
Problem 1 – Node class with distance method
Add a method distance_to(other_node) to the Node class that returns the Euclidean distance to another node. Test it.
Problem 2 – Member type colour mapping
Write a function that returns a colour string for a member type: "chord" → "blue", "vertical" → "green", "diagonal" → "red".
Problem 3 – Truss weight estimation
Add a method estimate_weight(kg_per_m) to the Truss class that returns total weight given a mass per metre of steel. Assume all members are the same section. Test with 80 kg/m.
Problem 4 – Export to DXF header
Write a small function that writes a minimal DXF‑style header (just 0\nSECTION\n2\nHEADER\n...) but for this exercise, just prepend a header to the CSV explaining the data.
Problem 5 – Generate a Warren truss variant
Create a subclass WarrenTruss that overrides the diagonal generation to produce a Warren truss pattern (diagonals all sloping the same direction or alternating). Hint: modify _generate_geometry().
Solutions (attempt first):
# P1
def distance_to(self, other):
dx = self.x - other.x
dy = self.y - other.y
return math.sqrt(dx**2 + dy**2)
# Add inside Node class
# P2
def member_colour(member_type):
colours = {"chord": "blue", "vertical": "green", "diagonal": "red"}
return colours.get(member_type, "gray")
# P3
def estimate_weight(self, kg_per_m):
return self.total_member_length() * kg_per_m
# P4
def export_with_header(self, filename="truss.txt"):
with open(filename, "w") as f:
f.write("# Truss geometry – Pratt\n")
f.write(f"# Span={self.span}m, Depth={self.depth}m, Panels={self.num_panels}\n")
f.write("# NODE_ID, X_m, Y_m\n")
for n in self.nodes:
f.write(f"{n.id}, {n.x:.4f}, {n.y:.4f}\n")
f.write("# MEMBER_ID, START, END, TYPE\n")
for m in self.members:
f.write(f"{m.id}, {m.start.id}, {m.end.id}, {m.type}\n")
# P5
class WarrenTruss(Truss):
def _generate_geometry(self):
# Reuse parent's node generation? Or override fully.
# For simplicity, modify diagonal pattern after base generation?
# Actually we need to redefine.
super()._generate_geometry()
# Then replace diagonals: clear and recreate with alternating pattern
# This is more complex; better to override fully.
# We'll skip full implementation but outline:
# Create nodes same way.
# Then diagonals from bottom i to top i+1 for all i. (Warren has diagonals all same direction)
pass
5. Applied Challenge Task (The Weekly Mini‑Project)
🏗️ Full Parametric Truss Generator
Your task is to build a complete, robust parametric truss generator. Extend the starter code above to include:
Core requirements:
User‑friendly CLI: Ask the user for span (m), depth (m), and panel width (m). Validate inputs with exception handling (positive floats, reasonable ranges).
Multiple truss types: Implement at least two truss types:
- Pratt truss (diagonals slope toward centre)
- Warren truss (diagonals all same direction or alternating)
Use inheritance: a base
Trussclass with a_generate_geometry()method overridden in subclasses.
Node and member lists as class attributes, with a method to compute member lengths.
CSV export with a clean format: first section for nodes (ID, X, Y), second section for members (ID, Start, End, Type, Length).
Summary printed to console (span, depth, nodes, members, total steel length, estimated weight).
Bonus features (choose at least two):
- Plot the truss using matplotlib (include labels for nodes and members).
- Export a simple DXF file (just lines) that can be opened in CAD. (Use
dxfwriteor manual DXF format.). - Calculate approximate forces using method of joints (simplified – assume all diagonals carry equal load). Just compute axial force in each member given a total UDL on the top chord.
- Generate a material take‑off CSV listing each member with its length, type, and a weight estimate.
- Add a
TrussCollectionclass that can store multiple trusses (e.g., for a roof of multiple bays) and export all at once.
Example interaction:
=== PARAMETRIC TRUSS GENERATOR ===
Enter truss type (Pratt/Warren): Pratt
Enter span (m): 12
Enter depth (m): 2.0
Enter panel width (m): 2.0
TRUSS SUMMARY
==================================================
Type: Pratt
Span: 12.00 m (actual: 12.00 m)
Depth: 2.00 m
Panels: 6
Nodes: 14
Members: 25
Total member length: 62.63 m
Estimated weight (80 kg/m): 5010.4 kg
==================================================
Exporting to truss_Pratt_12.0m.csv...
Plotting truss...
Evaluation criteria:
- Code quality: clear class structure, docstrings, consistent naming.
- Correct geometry: nodes and members form a valid truss.
- Robustness: exception handling for invalid inputs.
- Output quality: CSV imports cleanly into CAD (test with a simple import).
- Bonus features: demonstrate extra functionality.
6. Phase 2 Review Summary
Over Days 8–15, you have learned:
| Day | Topic | Key AEC Skill |
|---|---|---|
| 8 | String manipulation | Clean input, formatted reports, part marks |
| 9 | File I/O (CSV, Excel, txt) | Import/export schedules, pandas for spreadsheets |
| 10 | Exception handling | Robust scripts that handle messy construction data |
| 11 | Comprehensions, lambda, map | Concise data processing (filtering, mapping) |
| 12 | Modules, packages, venv | Reusable code libraries, project isolation |
| 13 | numpy & matplotlib | Geometry array operations, plotting grids/curves |
| 14 | Classes & Objects (OOP) | Model AEC elements as objects (Beam, Room, Floor) |
| 15 | Mini‑Project | Integrated parametric truss generator |
You are now capable of:
- Writing structured, reusable programs that automate AEC design tasks.
- Importing data from spreadsheets, processing it, and exporting results.
- Modelling building components as objects with behaviour.
- Generating and visualising geometry programmatically.
Key takeaway:
Phase 2 has equipped you with the intermediate skills needed to build professional‑grade automation tools. The truss generator is a milestone – it demonstrates how all these pieces fit together in a real‑world application.
7. Preview of Phase 3 (Days 16–23)
Tomorrow we begin Phase 3 – Advanced AEC Computation. You will dive into:
| Day | Topic | What You'll Build |
|---|---|---|
| 16 | Iterators & Generators | Lazy traversal of large IFC models |
| 17 | Decorators | Timing analysis, caching heavy calculations |
| 18 | Context Managers | Safe file handling, automatic unit context |
| 19 | BIM data with IfcOpenShell / COMPAS | Read IFC, extract walls, query properties |
| 20 | Advanced numpy & scipy | Solve structural systems, optimise trusses |
| 21 | Advanced visualisation (plotly, 3D) | Interactive 3D building models |
| 22 | Scripting CAD – Rhino/Dynamo | Automate modelling tasks |
| 23 | Weekly Project: BIM Data Analyser CLI | Read IFC, compute quantities, clash check |
Prepare to work with real building information models and professional engineering libraries.

Comments
Post a Comment