Truss Calculations Sep 2026


Introduction
I challenged myself to write a truss solver app.

With these features:

I used Python, Numpy, Plotly, VS Code, and a fair amount of searching documentation using Google (standard) AI.

ANALYSIS RESULTS

Here is my Input File.

Here is my example live Truss Diagram.
Here is my example live Truss Results.
Notice there is slider at right center that controls the deflection scale factor.
Here is my Truss Verification spreadsheet.
The verification above is simply a spreadsheet version of the hand method taught to undergraduates:


Engineering Conclusions
Basics

Not much new here — except the example problem illustrates asymmetric results from a symmetric loading. Why does that happen? Because the truss members have asymmetric cross-sectional areas (imagine different spring rates).

Motivation for this Work

Analyzing a truss manually (paper and pencil) is something that all (hmm, wondering about recent grads) civil engineers have learned to do. It’s straight forward arithmetic, a bit of insight, rather fun. Forces and reactions are almost trivial. Calculating nodal displacements, one node and one direction at a time, ([3]) is a matter of bookkeeping but still easy. Computer methods provide speed, stamina, fewer arithmetic errors (though systematic errors might be in play), and the ability to work trusses with redundant members.

I revisited the truss problem as a sport: anticipating a few tricky places, maybe something interesting in trying out new data structures and programming tools. I’ve noticed I learn best by working a real problem. The mother of invention phrase comes to bear.

Engineering Design Discussion
A good friend reviewed this post. Based on his questions, I realize that the reader may want more insight into truss behavior and how a truss design might proceed.

  • First off, the truss described above is deficient.

    The truss looks good, let’s say capable of carrying loads, but inspecting carefully, some members have very small (really unrealistic) cross-sections. Members 2, 7, 8, and 10 are the culprits. And all these members are on the right side of the truss.

    Because of the small cross-sections and asymmetry, we are seeing large deflections at Node 2 especially, Node 6 also.

    The slider does nothing but scale the deflected picture. Think of it as similar to a topographic map that emphasizes elevation differences.

    In summary, I am showing a truss with decent geometry (nodes, coordinates, members, lengths) but insufficient cross-sections. Back to the drawing board you might say. See this section below.

  • How does the model apply to the real world?

    The presentation certainly applies to the real world. The analysis shows what would happen if such a truss would be built and loaded.

    This brings us to exactly what structural engineers do — we make mathematical models that behave in accordance with physics (plus metallurgy, plus building codes, plus much more). The modeling, analysis, design work predicts the behavior of what is to be built. The success of the project depends, of course, on all that.

  • My blog post is not the full design procedure. It is only the part where loads and deflections are calculated. Every member and every node must be designed, using building code criteria — I have shown none of that. In college that would be several semesters of instruction.
  • Addressing the area deficiencies, I built another input file in which all members have a 5.00 square inch cross section. Here is the Truss Result (all areas equal). Notice two things: the symmetry of the deflections and the magnitude of the vertical deflection at Node 2 (-0.237 inches). The truss behavior is dramatically improved. The node 2 deflection is about 14% of that previous.
  • I built the same truss model in RISA-3D (a commercial structural analysis and design program). I verified the truss results as discussed above, all good. Then I used the design portion of the program to determine what members might be used for the truss chords and truss webs. See [2] truss reference information for terminology.
    Truss Chords
    Truss Webs

    The design (of course we must say quite a preliminary design) uses DL4x4x6x6 chords and DL4x4x4x6 webs. DL means double angle steel,
    the numerical suffix identifies the specific double angle.
    Truss Chords
    DL4x4x6x6
    (Area = 5.72 square inches)
    Truss Webs
    DL4x4x6x6
    (Area = 3.86 square inches)
    So it turns out, these new areas are very near our original value of 5.00. Using these new steel shapes, Node 2 now has a vertical deflection of only 0.2063 inches. We have now improved the Node 2 deflection by a factor of 8.5! Whew.

What did I learn
Long list here:

  • the analysis programming was simple and straightforward; there are only about 50 lines in the analysis (all else is input and output)
  • The parsing program is about 170 lines including comments. This was written with Google AI responses. I made only a minor change, a single line. The interesting feature in the parser program is the shlex import module.
  • The analysis uses numpy therefore the arrays are integer indexed. I set up my node and member labels as string or integer (industry standard). Therefore. I produced a map to translate my labels to and from numpy integer indices.
  • I used dataclasses for my variables. Here’s an example code excerpt:
    @dataclass
    class Node:
        x: float
        y: float
    
        # This allows np.array(obj) to work seamlessly
        def __array__(self, dtype=None):
            return np.array(astuple(self), dtype=dtype)
    
    @dataclass
    class Elem:
        start: str | int
        end: str | int
        # if no user entry, then area = 1.0
        area: float | int = 1.0

    In the Node class I added a method to convert easily to an np.array, handy as it turns out. No special coding is required to convert from the dataclass to an NP array. I just call np.array(my instance variable).

    In the Elem class I added a default initialization for area. Therefore, area can be left blank in my input file. This is useful for problems where displacements are not needed (statics problems).

  • I’ve been slowly adopting type hints in my code. I find it helpful. Here’s an excerpt illustrating TypeVar. It tells static type checkers (like mypy) that an input and an output share the exact same type.
    from typing import TypeVar
    
    T = TypeVar("T")
    
    def get_first(items: list[T]) -> T:
        return items[0]
    
    def get_second(items: list[T]) -> T:
        return items[1]
    
    def get_nth(items: list[T], n: int) -> T:
        return items[n-1]
    
    def print_tabbed(items, x:int=2):
        print("t".join(items).expandtabs(x))
    
    mixed_list = [1, 2.01, "3"]
    
    # Type checker knows x is an int because the list contains ints
    x = get_first(mixed_list)  # Inferred as int
    print("x: ", x, type(x), sep="t")
    
    y = get_second(mixed_list)  # Inferred as float
    print("y: ", y, type(y), sep="t")
    
    z = get_nth(mixed_list, 3)  # Inferred as int
    print("z: ", z, type(z), sep="t")
    
    # RESULT
    # x:    1       <class 'int'>
    # y:    2.01    <class 'float'>
    # z:    3       <class 'str'>
    
  • I built a method that adds two identical dataclass instances.
    def add_dataclasses(obj1: T, 
                        obj2: T, 
    					f1: float = 1.0, 
    					f2: float = 1.0, 
    					pick_first_string: bool = True
                       ) -> T:
        """
        Performs addition on two arbitrary identical dataclass instances.
    
        Parameters:
            -----------
            obj1: dict
                an arbitrary dataclass instance, say Foo.
            obj2: dict
                a dataclass instance of the SAME TYPE as obj1.
            f1: float = 1.0
                factor used in obtaining result
            f2: float = 1.0
                factor used in obtaining result
            pick_first_string: bool = True
    
            Example result (for numeric attributes) 
    		    = f1 * (obj1 attributes) + f2 * (obj2 attributes)
    
            Example result (for string attributes)  
    		    = obj1 str attribute if pick_first_string == True
    
            Example result (for string attributes)  
    		    = obj2 str attributes otherwise
    
        Example Dataclass:
    
        @dataclass
        class Foo:
            x: int
            y: float
            label: str = 'nada'
    
        """
        # Ensure both instances belong to the exact same class
        if type(obj1) != type(obj2):
            raise TypeError(
                f"Cannot add mismatched types: 
    			    {type(obj1).__name__} and {type(obj2).__name__}"
            )
    
        # Extract the values for each field and add them together
        summed_data = {
            field.name: (
                f1 * getattr(obj1, field.name) + f2 * getattr(obj2, field.name)
                if field.type is not str
                else (
                    getattr(obj1, field.name)
                    if pick_first_string
                    else getattr(obj2, field.name)
                )
            )
            for field in fields(obj1)
        }
    
        # Unpack the summed dictionary into a new instance of the same class
        return type(obj1)(**summed_data)

    and here’s my use case where I add factored node displacements to node coordinates using a dict comprehension.

    # sc is the scale factor
    
    nodes = {e: hda.add_dataclasses(nodes_input[e], 
    	                            displacements[e], 1, sc)
    	                            for e in nodes_input.keys()
            }
    	

  • I have used Plotly quite a lot over the past three years yet I had to relearn a few tricks to build the desired output.
    • To provide good looking PLotly figures to mobile devices use the responsive parameter: fig.show(config={"responsive": True})
    • Building the displacement plots was the most difficult for me. It required building plot groups characterizing visibility using the Plotly parameters visible, legendgroup, showlegend. I didn’t know any of this prior to my project, nor was it obvious after I got into it.
      	   
          ##############################
          # ‼️✅‼️BUILD SLIDER STEPS #
          ##############################
          # Build the slider steps
          # Each step must define a boolean visibility array matching the total number of traces
          total_traces = len(fig.data)  # 6 traces total (3 groups * 2 traces)
          steps = []
      
          for i, sc in enumerate(scalers):
              # Create a list of booleans: True if the trace belongs to the active group
              # e.g., for step 0: [True, True, False, False, False, False]
              visibility_mask = [False] * total_traces
              offset = total_traces // len(scalers)
              for tt in range(offset):
                  # visibility_mask[i * 19 + tt] = True
                  # visibility_mask[i * 2] = True  # Toggle Sine trace
                  visibility_mask[i * offset + tt] = True  # Toggle Cosine trace
      
              step = dict(method="restyle", args=["visible", visibility_mask], label=f"{sc}")
              steps.append(step)
      
          ##############################
          # ‼️✅‼️CREATE THE SLIDER  #
          ##############################
          # Create the slider component and add it to the layout
          sliders = [
              {
                  "currentvalue": {"prefix": "Current Scale: "},
                  "pad": {"t": 30},
                  "steps": steps,
                  # "pad": {"t": 10},
                  # --- SIZE & POSITIONING KEY CONTROLS ---
                  "lenmode": "fraction",  # Uses a fraction of the plot width
                  "len": 0.16,  # Makes the slider much smaller (16% of plot width)
                  "x": 1.02,  # Pushes the slider past the right edge of the axes
                  "y": 0.5,  # Centers it vertically alongside the plot
                  "xanchor": "left",
                  "yanchor": "middle",
                  "active": 0,
              }
          ]
      
I designed special Python dataclasses for my truss analysis and plotting
This was one of the most interesting parts of my work. Though very simple data structures would have sufficed, I wanted to formalize my data structures to see if it would improve my coding effort.
I built a Truss() dataclass. After the program parses the text, the parsed lists are converted to Truss(); then the Truss() is transformed to NP arrays for the analysis part; before plotting, the NP array results are transformed back to Truss() for printing; finally in the plotting routine, the Truss() is transformed to vectors (for Plotly).
Sure, it does seem too complicated except that I want a canonical data format. I intend to use this later for freezing data and processing trusses in loops. Think database.
Well, here it is.
from dataclasses import dataclass, asdict, astuple, field, fields

### ✅ GENERAL ###

@dataclass
class ListDict(dict):
    # Pass the raw data during initialization
    data: List[List[Any]] = field(default_factory=list)

    def __post_init__(self):
        # Populate self (the dict) from the list of lists
        for item in self.data:
            if item:
                key = item[0]
                value = (
                    item[1:] if len(item) > 2 else (item[1] if len(item) == 2 else [])
                )
                self[key] = value
        # Clean up the extra attribute if you don't want it stored
        del self["data"]

### ✅ TRUSS RELATED ###

@dataclass
class Node:
    x: float = 0
    y: float = 0

    # This allows np.array(obj) to work seamlessly
    def __array__(self, dtype=None, copy=None):
        return np.array(astuple(self), dtype=dtype)

    # this makes instances of the class natively iterable and unpackable
    def __iter__(self):
        return iter(astuple(self))


'''
### ✅ Just FYI ###

class MyValue(str):
    """A custom string type that requires no dot notation to use."""
    pass

username = MyValue("Alice")
print(username)  # Outputs: Alice (no dot notation needed)'''


@dataclass
class Title_in(str):
    # if no user entry, then empty string
    val: str = ""


# this is a conventional class
class Area(float):
    pass


# this is a conventional class
class Length(float):
    pass


@dataclass
class Modulus(float):
    # if no user entry, then use steel modulus
    val: float = 29e6


@dataclass
class Elem:
    start: str | int
    end: str | int
    # if no user entry, then area = 1.0
    area: float | int = 1.0

    # this makes instances of the class natively iterable and unpackable
    def __iter__(self):
        return iter(astuple(self))


@dataclass
class Constraint:
    x: bool | int = False
    y: bool | int = False

    def __bool__(self):
        """Return whether either degree of freedom is constrained."""
        return bool(self.x) or bool(self.y)

    # This allows np.array(obj) to work seamlessly
    # this works even considering bool type
    def __array__(self, dtype=None, copy=None):
        return np.array(astuple(self), dtype=dtype)

    # this makes instances of the class natively iterable and unpackable
    def __iter__(self):
        return iter(astuple(self))


@dataclass
class ForceXY:
    x: float = 0
    y: float = 0

    # This allows np.array(obj) to work seamlessly
    def __array__(self, dtype=None, copy=None):
        return np.array(astuple(self), dtype=dtype)

    # this makes instances of the class natively iterable and unpackable
    def __iter__(self):
        return iter(astuple(self))


@dataclass(init=False)
class ForceElem:
    magnitude: float = 0

    def __init__(self, val: list | float | int):
        # Custom logic before setting data
        if isinstance(val, list):
            self.magnitude = val[0]
        else:
            self.magnitude = val

    # This allows np.array(obj) to work seamlessly
    def __array__(self, dtype=None, copy=None):
        return np.array(astuple(self), dtype=dtype)

    # this makes instances of the class natively iterable and unpackable
    def __iter__(self):
        return iter(astuple(self))


### ✅ ALIASES ###


# build aliases for a few classes
# quite nice

Displacement = Node
Load = ForceXY
Reaction = ForceXY


@dataclass
class Map:
    int_key: bool | dict[int, Any] = field(default_factory=lambda: False)
    str_key: bool | dict[str, Any] = field(default_factory=lambda: False)


@dataclass(init=False)
class Material:
    callout: str | int

    def __init__(self, val: list | str | int):
        # Custom logic before setting data
        if isinstance(val, list):
            self.callout = val[0]
        elif isinstance(val, str):
            self.callout = val
        else:
            self.callout = val

    # This allows np.array(obj) to work seamlessly
    def __array__(self, dtype=None, copy=None):
        return np.array(astuple(self), dtype=dtype)

        # this makes instances of the class natively iterable and unpackable

    def __iter__(self):
        return iter(astuple(self))


@dataclass
class Truss:
    """the type of dict has no effect
    it's just a reminder"""

    title_in: dict[Title_in] = field(default_factory=dict)

    nodes: dict[Node] = field(default_factory=dict)
    constraints: dict[Constraint] = field(default_factory=dict)
    elements: dict[Elem] = field(default_factory=dict)
    moduli: dict[Modulus] = field(default_factory=dict)
    loads: dict[ForceXY] = field(default_factory=dict)
    forces: dict[ForceElem] = field(default_factory=dict)
    reactions: dict[ForceXY] = field(default_factory=dict)
    displacements: dict[Displacement] = field(default_factory=dict)
    materials: dict[Material] = field(default_factory=dict)


    ### ✅ PROPERTIES ###
	
	#####################
    # ‼️✅‼️IMPORTANT #
    #####################
    # areas and length are only class properties
    # not class attributes,
    #    therefore, we must shallow copy to get
    #    the property values
    #       seriously, who knew!
    #
	# for example:
	#
    areas = copy.copy(trussDC.areas_as_floats)
    lengths = copy.copy(trussDC.lengths_as_float)
    ####################
    # END OF IMPORTANT #
    ####################

	# will auto update; properties need no parentheses
    @property
    def title(self) -> str:
        # trying to get the single string from the input dataset
        if self.title_in:
            try:
                return_str = str(self.title_in[0][0])
            except Exception:
                return_str = ""
            return return_str
        return ""


    # will auto update; properties need no parentheses
    # this works well
    @property
    def n_nodes(self) -> int:
        # now set the number of nodes
        return len(self.nodes)


    # will auto update; properties need no parentheses
    @property
    def n_elements(self) -> int:
        # now set the number of elements
        return len(self.elements)


    # will auto update; properties need no parentheses
    @property
    def lengths(self) -> dict[str | int, List[List[float]]]:
        # now build all the member lengths
        length_list = []
        user_nodes = self.nodes
        user_elements = self.elements
        for k, v in user_elements.items():
            x1, y1 = user_nodes[v.start]
            x2, y2 = user_nodes[v.end]
            L = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
            length_list.append([k] + [float(L)])
        return {v[0]: [v[1]] for v in length_list}


    # will auto update; properties need no parentheses
    @property
    def lengths_as_float(self) -> dict[str | int, List[float]]:
        # now build all the member lengths
        length_list = []
        user_nodes = self.nodes
        user_elements = self.elements
        for k, v in user_elements.items():
            x1, y1 = user_nodes[v.start]
            x2, y2 = user_nodes[v.end]
            L = math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
            length_list.append([k] + [float(L)])
        return {v[0]: v[1] for v in length_list}

    # will auto update; properties need no parentheses
    @property
    def elements_start_end(self):
        return_dict = {k: [v.start, v.end] for k, v in self.elements.items()}
        return return_dict


    # will auto update; properties need no parentheses
    @property
    def areas(self) -> dict[str | int, List[List[float]]]:
        return_dict = {k: [v.area] for k, v in self.elements.items()}
        return return_dict


    # this makes instances of the class natively iterable and unpackable
    def __iter__(self):
        return iter(astuple(self))


    # will auto update; properties need no parentheses
    @property
    def areas_as_floats(self) -> dict[str | int, List[float]]:
        return_dict = {k: v.area for k, v in self.elements.items()}
        return return_dict

    # this makes instances of the class natively iterable and unpackable
    def __iter__(self):
        return iter(astuple(self))


    # will auto update; properties need no parentheses
    # GOOD
    @property
    def map_nodes(self):
        temp_map = Map()
        temp_map.int_key = {i: v for i, v in enumerate(self.nodes.keys())}
        temp_map.str_key = {v: k for k, v in temp_map.int_key.items()}
        return temp_map


    # will auto update; properties need no parentheses
    # GOOD
    @property
    def map_elements(self):
        temp_map = Map()
        temp_map.int_key = {i: v for i, v in enumerate(self.elements.keys())}
        temp_map.str_key = {v: k for k, v in temp_map.int_key.items()}
        return temp_map


indexed_by_nodes = [
    "constraints",
    "displacements",
    "forces",
    "loads",
    "nodes",
    "reactions",
]

indexed_by_element = ["areas", "elements", "elements_start_end", "forces", "lengths"]

# ~~~
Look Ahead (a future post maybe)

I haven’t implemented user defined units yet, nor have I implemented the STRUDL-like user commands explicitly requesting printed or plotted results.

I hope to provide a new feature — something akin to influence lines. I would like to produce ‘dx/dy’ graphs, generated by running the truss program say 100 times, or just 10 times, based on a single varying load. It should be easy to plot the input vs some output(s). Imagine a constant load at a node but placed at a varying theta, then ask what is the resulting load or deflection at a specific location in the truss.

References
[3] UNIT LOAD METHOD: Thanks to the Swiss mathematician Johann Bernoulli (early 1700s), Scottish mathematician James Clerk Maxwell (1860s), German civil engineer Otto Mohr (1870s), and German civil engineer Heinrich Müller-Breslau (late 1880s).

Leave a Reply

Your email address will not be published. Required fields are marked *