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

DaySkillHow we use it today
8String formattingGenerate member labels, formatted CSV
9File I/OWrite CSV for CAD import
10Exception handlingValidate user inputs (span > 0, etc.)
11Comprehensions / lambdaEfficiently create node lists
12Modules(Optional) structure code as a module
13numpy / matplotlibGenerate coordinates, plot truss
14Classes & OOPTruss, 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:

  1. User‑friendly CLI: Ask the user for span (m), depth (m), and panel width (m). Validate inputs with exception handling (positive floats, reasonable ranges).

  2. 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 Truss class with a _generate_geometry() method overridden in subclasses.
  3. Node and member lists as class attributes, with a method to compute member lengths.

  4. CSV export with a clean format: first section for nodes (ID, X, Y), second section for members (ID, Start, End, Type, Length).

  5. 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 dxfwrite or 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 TrussCollection class 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:

DayTopicKey AEC Skill
8String manipulationClean input, formatted reports, part marks
9File I/O (CSV, Excel, txt)Import/export schedules, pandas for spreadsheets
10Exception handlingRobust scripts that handle messy construction data
11Comprehensions, lambda, mapConcise data processing (filtering, mapping)
12Modules, packages, venvReusable code libraries, project isolation
13numpy & matplotlibGeometry array operations, plotting grids/curves
14Classes & Objects (OOP)Model AEC elements as objects (Beam, Room, Floor)
15Mini‑ProjectIntegrated 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:

DayTopicWhat You'll Build
16Iterators & GeneratorsLazy traversal of large IFC models
17DecoratorsTiming analysis, caching heavy calculations
18Context ManagersSafe file handling, automatic unit context
19BIM data with IfcOpenShell / COMPASRead IFC, extract walls, query properties
20Advanced numpy & scipySolve structural systems, optimise trusses
21Advanced visualisation (plotly, 3D)Interactive 3D building models
22Scripting CAD – Rhino/DynamoAutomate modelling tasks
23Weekly Project: BIM Data Analyser CLIRead IFC, compute quantities, clash check

Prepare to work with real building information models and professional engineering libraries.

Comments

EARN AT THE COMFORT OF YOUR HOME

Sponsored content

Popular posts from this blog

Day 5 – Functions, Scope, and Docstrings

Day 10 – Exception Handling and Debugging

Day 1 – Python Foundations for AEC Professionals