15
STRUCTURAL MODELS

Image

In this part of the book, we’ll focus on solving truss structures. Truss structures are used to support the roof of industrial warehouses (see Figure 15-1) and long-span bridges. This is a real engineering problem that is a good example of building an application that reads data from a file, builds a model out of that data, solves a system of linear equations, and presents the results graphically in a diagram.

Since solving truss structures is a big topic, we’ll break it down into several chapters. This first one will give you a rough introduction to the basics of mechanics of materials; it’s not meant to explain the concepts from scratch but should serve as a refresher. Once we’ve gone through the basics, we’ll implement two classes to model truss structures: nodes and bars. As we’ve seen in earlier chapters, the first step of solving a problem in code is to have a set of primitives that represent the entities involved in the solution.

Image

Figure 15-1: A warehouse roof is a good example of a truss structure.

Solving Structural Problems

Let’s begin with a few definitions. A structure is a set of resistant elements built to withstand the external application of loads, as well as their own weight. A truss structure is a structure in which the resistant elements are bars joined by pins in both ends, and the external forces are applied only where those bars join: at the nodes.

When working out a structural problem, we’re most interested in two things. First, can the bars of the structure handle the forces acting on them and avoid collapse? Second, how big are the displacements of the structure once it’s deformed under the action of the external loads? The first is an obvious concern: if any of the bars in the structure break, the structure may collapse, which could have catastrophic consequences (think: collapsing warehouse roofs or bridges). Our analysis should make sure this never happens.

The second concern is less obvious, but important nevertheless. If a structure is deformed enough for the naked eye to notice, even if the structure is safe and won’t collapse, people around or below it may get anxious. Think about how you would feel if you saw your living room’s ceiling noticeably curved. Keeping the deformation of the structure between some limits impacts the comfort of its users.

The solution we’re after should include the amount of stress on each bar, as well as the global displacements of the structure. We’ll code up the actual solution in the next chapter; here, we’ll define the solution model. We can expect our solution model to include these two quantities: the amount of mechanical stress on each bar and the node displacements.

Before we can do that, though, we’ll need to dive into the world of structural analysis. Be prepared to write lots of code. We’re about to solve a serious engineering problem, so the payoff for our hard work will be high.

Structural Member Internal Forces

Let’s begin by quickly recapping how elastic bodies respond to the application of external forces. This is a topic typically taught in mechanics of materials, a classic subject in mechanical engineering courses. If you’ve extensively studied this subject, feel free to skip this section or browse through it as a refresher. If not, this section is for you. Your mechanics knowledge should be enough to follow the text, but we can’t possibly cover everything in detail. You can refer to [3], one of my all-time favorites on the subject. Books on statics also cover this topic with some detail. I recommend you take a look at [9] or [11].

Elastic Bodies Subject to External Forces

Let’s use an I beam as an example of an elastic body and apply an external system of balanced forces to it. These are forces whose sum equals zero: Image. Figure 15-2 shows the beam.

Image

Figure 15-2: A beam subject to external forces

When external forces are applied to this elastic body, its atoms will fight back in an attempt to preserve the relative distances between themselves. If the external loads want to separate the atoms, they’ll try to hold each other tighter. If they’re pushed together, they’ll try not to get too close. This “fighting back” makes up the internal forces: forces inside the body itself that exist in response to the application of external forces.

To study the effects of these forces on the body, let’s take our beam from Figure 15-2 and virtually cut it with a plane, like in Figure 15-3.

Image

Figure 15-3: A section of a beam subject to external forces

Let’s remove the right chunk of the beam and analyze what happens in the left part’s cross section. Since the entire beam was in static equilibrium before we cut it, the left chunk should be in static equilibrium as well. To preserve this equilibrium, we must account for the distribution of internal forces that the now removed right chunk exerted on the left one. These forces appear because the atoms in the left chunk have been separated from their neighbors in the right chunk. The force that pulled them together needs to be added to the cut section so that the atoms stay in the same equilibrium state as before.

These forces are distributed over the whole cut surface and represented in Figure 15-4.

Image

Figure 15-4: Analyzing equilibrium in a section

The distribution of forces over an area is referred to as stress. The net effect of the stress can be substituted with an equivalent system of a resulting force Image and moment Image. Each of the components of this equivalent force and moment produces a different effect on the beam. Let’s break the components down.

Axial and Shear Forces

The equivalent internal force Image can be broken down into an equivalent system of two forces, one that is normal to the section, Image, and one tangent to it, Image (see Figure 15-5).

Image

Figure 15-5: Equivalent internal forces in the section of a beam

If the elastic body has a prismatic shape (one of its sides is considerably longer than the other two) and we cut a section normal to its directrix, the resulting normal force Image we obtain is referred to as the axial force. The name reflects the fact that this force is aligned with the prism’s main axis or directrix. Prismatic bodies are common in structural analysis; beams and columns are good examples.

The axial force can either elongate or compress the body. An axial force that pulls the body apart is called a tension force, whereas one that compresses it is known as a compression force. Figure 15-6 shows two prismatic bodies subject to these forces.

Image

Figure 15-6: Tension and compression forces

The shear force is the force tangent to the cross section (see Figure 15-7) and thus can be further decomposed into two components: Image and Image (see the diagram on the right of Figure 15-5). These two components have the same effect: they try to shear the body apart. Figure 15-7 shows the effect of shearing forces applied to a prismatic body.

Image

Figure 15-7: Shear force

In summary, the equivalent internal force in a cross section of the body may have a normal component that either elongates or compresses it; it may also have a tangent component that shears it. These are the two ways internal forces can produce deformations on a body.

Bending and Torsional Moments

We studied the possible effects of the resulting internal force on a given cross section. What effects does the resulting moment produce? As you can see in Figure 15-8, the resulting moment Image can be decomposed into a moment normal to the cross section, Image, and a moment tangent to it, Image.

Image

Figure 15-8: Equivalent internal moments in the section of a beam

These moments bend the body in arbitrary ways, but if we choose a prismatic body and cut it normal to its directrix (the same thing that we did with the forces), the moments we obtain have a predictable and well-defined effect. The moment normal to the surface, Image, generates a torsional (twisting) effect on the prism and thus receives the name of torsional moment.

Once again, the moment tangent to the section can be further broken down into two subcomponents: Image and Image (see the right illustration in Figure 15-8). These two moments have a similar effect: they bend the prism and hence are called bending moments. Figure 15-9 illustrates this effect.

Image

Figure 15-9: Bending moment

To summarize, the equivalent internal moment on a cross section of the body may have a normal component that tends to twist it around its directrix (the torsional moment) and may also have two tangent moments that tend to bend the prism (the bending moments).

Let’s now analyze in detail how prismatic bars behave when subject to axial forces. Then, we’ll see how, by using a group of these resistant prisms, we can build structures that can withstand the application of heavy loads.

Tension and Compression

Let’s focus our analysis on axial forces: those aligned with the axis of a prismatic resistant body. As we’ll see in the next section, the structures we’ll solve are made of only prismatic elements (bars) subject to axial forces.

Hooke’s Law

It’s been experimentally proven that within some limits, the elongation of a prismatic bar is proportional to the axial force applied to it. This linear relation is known as Hooke’s law. Let’s suppose a bar with length l and cross section A is subject to a pair of external forces Image and –Image, like in Figure 15-10.

Image

Figure 15-10: A bar subject to axial forces

Equation 15.1 gives Hooke’s law.

Image

In this equation,

δ    is the total elongation of the bar.

F    is the Image force’s magnitude.

E    is the proportionality constant or Young’s modulus, which is specific to the material.

Hooke’s law states that the total elongation δ of a bar subject to a pair of external forces is (1) directly proportional to the magnitude of the forces and the bar’s length and is (2) inversely proportional to its cross section and Young’s modulus. The longer a bar or the stronger the force applied is, the greater the elongation produced will be. Conversely, the bigger the cross section values are or Young’s modulus is, the smaller the elongation will be.

Recall that when a force is distributed over an area, the intensity of such force per unit area is known as stress. Stress is usually denoted by the Greek letter σ (see Equation 15.2).

Image

By convention, the stress is positive for tensile forces and negative for compression forces. The stress is a useful quantity in mechanical design; it’s used to determine whether a given component (in a structure or machine, for example) will break down during operation. The stress values a given material can undergo before failure are well studied.

We define strain as the elongation per unit length, a dimensionless quantity denoted by the Greek letter ϵ (see Equation 15.3).

Image

Using the equations for the stress and strain, Hooke’s law from Equation 15.1 can be rewritten as shown in Equation 15.4.

Image

Interestingly, by introducing stress and strain, the relation between the external actions applied to a resistant body (forces) and their effects (elongations) no longer depends on the area or length of the body. We’ve effectively removed all dimensional parameters from the equation. The proportionality constant in Equation 15.4 (E) is Young’s modulus, which is a characteristic of materials. For structural steels, for example, E is around 200 GPa, that is, 200 ⋅ 109 Pa. We can therefore predict the mechanical behavior of bodies by applying experimental results obtained for the material in use. To do this, we use stress-strain diagrams, which plot the stress versus the strain for a given material.

Stress-Strain Diagrams

Stress-strain diagrams plot the stress versus the strain for a given material and are obtained by performing tension or compression tests (see [3] for more details). We use these diagrams to predict the behavior of resistant bodies made of the same material. Recall that since we introduced the quantities stress and strain, every dimensional term has disappeared from Hooke’s equation, meaning that once we’ve experimentally determined the strain and stress a material undergoes under a given load, we can use those results for any bodies made of the same material, regardless of their shape or size.

Figure 15-11 is a plot of the approximate stress-strain diagram for structural steels. Note this graph is not to scale.

Image

Figure 15-11: The stress-strain diagram for structural steel

This diagram has an initial linear region that holds up to a given stress value known as the proportional limit, depicted by point A. For stress values greater than the proportionality limit, the stress-strain relation is no longer linear. The proportional limit is typically between 210 MPa and 350 MPa for structural steels—three orders of magnitude smaller than Young’s modulus. This region is modeled by Hooke’s law and the linear relation σ = . We’ll center our analysis here.

With a small stress increment after A, the proportional limit, we reach point B, the yield stress or yield strength. After the yield stress, big elongations happen without an increase in the stress. This phenomenon is called the yielding of the material.

After a noticeable amount of strain, we reach point C, and the material appears to harden. The stress must continue to increase to reach point D, which is the maximum amount of stress structural steel can withstand. We call this stress value the ultimate stress or ultimate strength. From this point, the material will acquire bigger strains with a reduction in the stress value.

The point E is where the material fractures. The amount of strain the material can take before it fractures can be called the fracture strain. This is the point of complete mechanical failure, but if you think about it, after the ultimate stress is reached (point D), it’s likely that the material will fracture anyway. The ultimate stress is typically used as the maximum value of stress a given material can absorb before failure.

Now that we have a good understanding of how resistant bodies respond to tensile stresses, let’s look at truss structures.

Plane Trusses

There are many structural typologies, but we’ll focus our analysis on the simplest of them: plane trusses.

A plane truss structure is a structure contained in a plane whose resistant bodies are bars subject only to axial forces and whose own weight can be ignored. There are two conditions that allow this.

  • Bars must be joined by pins at their ends.
  • External loads must always be applied to nodes.

A node is the point where several bar ends meet. Nodes join bar ends together in frictionless unions, meaning the rotation of the bars around the node is not constrained.

Plane trusses are made of triangles: three bars pinned at their ends. The triangle is the simplest rigid frame; bars joined to form a polygon of four or more sides form nonrigid frames. Figure 15-12 shows how a plane truss made of four bars can be moved from its original position and thus isn’t considered rigid. Simply by adding a new bar and creating two subtriangles, the structure becomes rigid.

Image

Figure 15-12: Example of a polygonal plane truss

Figure 15-13 is an example of a plane truss. The structure is made of eight nodes (N1, N2, . . . , N8) and thirteen bars. Nodes 1 and 5 have external supports or constraints applied. Nodes 6, 7, and 8 have external loads applied to them.

Image

Figure 15-13: A plane truss structure

Figure 15-14 is the diagram resulting from the structural analysis of the plane truss described in Figure 15-13. It was produced by the very application we’ll build in this part of the book.

Image

Figure 15-14: A plane truss structure solution diagram

In this diagram, we can appreciate the structure’s deformed geometry because it has been scaled to be noticeable. Node displacements tend to be very small (around two orders of magnitude smaller than the dimension of the structural bars), so a diagram depicting the nonscaled node displacements may be hard to tell apart from the original geometry.

You’ll notice there’s a lot of information in Figure 15-14. Every bar is labeled with the stress it’s subject to, though the font size of the labels in this figure is small, so the labels may not be easy to read. Positive numbers are tension stresses, and negative are compression. The bars are also colored in green or red depending on the load they’re subject to: green for tension and red for compression. Since the book is printed in black and white, you won’t be able to tell the colors apart, but once you’ve developed the complete application, you’ll produce the figure with your own code and will be able to explore all the details in it.

Let’s now study the mechanical response of the bars that make up plane trusses. They have an interesting particularity we’ve already mentioned: they develop axial stresses only.

Two-Force Members

As we’ve already discussed, plane truss bars are pinned at their ends, and loads are always applied at the nodes; because of this, the bars are subject only to axial forces. We can apply an external force only to the ends of the bar, using the contact of the pinned joint with the node. Because these unions are frictionless, they can only transmit forces to bars and just in the direction of the bar’s directrix.

Figure 15-15 shows how an external force applied to a node is transferred to the bars. These forces are aligned with the bars’ directrices and thus produce axial stresses only.

Image

Figure 15-15: The transmission of forces in a node

Since bars have two pinned ends where external forces are applied, they are subject to two forces. To be in equilibrium, such a body requires the two forces to be collinear, equal in magnitude and with opposite directions. In the case of a bar (a long prismatic body), these two forces have to be in the direction of the bar’s directrix (Figure 15-16) and, hence, produce axial stresses only. We call these bars with two collinear forces applied two-force members (see Figure 15-16).

Image

Figure 15-16: A two-force member

The forces applied to the bar in Figure 15-16 are labeled Image and –Image to signify that the two forces must be equal in magnitude and point in opposite directions. In this case, the forces would produce tension stresses on the bar.

Thanks to Hooke’s law, we know how materials respond to the external application of loads. We’ve also explored two-force members, and we’ve seen that the bars in plane trusses are two-force members. Let’s now derive a set of equations to relate these two forces with the displacements they produce on such two-force members.

Stiffness Matrices in Global Coordinates

Going back to the original formulation of Hooke’s law in Equation 15.1, we can isolate the force term to get the following:

Image

Here, the term Image is the bar’s proportionality constant relating the force applied, F, with the elongation it produces, δ. This term also receives the name stiffness. As you can see, the stiffness depends on the bar’s Young’s modulus (E), which is material dependent, and geometry (A and l).

Now look at the bar in Figure 15-17. If we consider a local system of reference whose x-axis is aligned with the bar directrix, this bar has two degrees of freedom (DOF), in other words, two different ways it can independently move. These are the displacements in the local x-axis of both nodes, denoted by Image and Image. Each node has a force applied: F1 and F2.

NOTE

A note on the nomenclature: we’ll use primes to label DOFs referred to by the bar’s local system of coordinates. For example, Image refers to the x displacement of the node 1 referred to the bar’s local system of reference: (x,y). By contrast, nonprime values, such as u1, are referred to the global system of reference: (x,y).

Image

Figure 15-17: A bar with two degrees of freedom

Using the previous equation, we can relate the force in each node to the displacements Image and Image like so:

Image

The two equations above can be written in matrix notation (Equation 15.5),

Image

where [k] is referred to as the local stiffness matrix for the bar. This stiffness matrix relates the displacements in the two nodes of the bar with the external forces applied to them, all in the bar’s local system of reference. Using this local system of reference, the bar has only two degrees of freedom, which are the displacements of each of the two nodes in the local x-axis direction (Image and Image).

Let’s now consider a bar rotated with respect to the global system of coordinates. Take Figure 15-18 as an example. This bar has its own local system of reference (x, y), which forms an angle of θ with respect to the global system of reference (x,y).

Image

Figure 15-18: A bar’s local reference frame

From the global system of reference’s perspective, each node of the bar has two degrees of freedom: each node can move in both the x and y directions. Projected in this system of reference, the four DOFs are u1, v1, u2, and v2.

To transform the bar’s local stiffness matrix [k] into a global [k] stiffness matrix, we have to apply a transformation matrix. We can find such a matrix by breaking down the local displacements Image and Image into their global components. Figure 15-19 shows this operation.

Image

Figure 15-19: The local displacement projections

Let’s find a mathematical expression to compute the global displacements based on their local counterparts:

Image

Written in its matrix form, it looks like

Image

where [L] is the transformation matrix. To compute the global stiffness matrix from the local [k], we can use the following equation (refer to [2] or [10] for the details on how to derive this expression),

[k] = [L][k][L]

which, shortening the notation to c = cosθ and s = sinθ, yields Equation 15.6.

Image

We now have a system of equations that relates the external forces applied to a bar’s nodes to their displacements in global coordinates (see Equation 15.7).

Image

Let’s now use this knowledge to start building our structural model in code.

Original Structure Model

In our Mechanics project, create a new Python package named structures. In structures, create another package: model. Here’s where we’ll define the classes that make up the structural model. Create another package in structures named solution. This is where we’ll have the classes that model the resolved structure. Also create a tests folder in structures to contain the unit tests we’ll develop. Your project’s structure should look something like this:

    Mechanics
      |- apps
      |- eqs
      |- geom2d
      |- graphic
      |- structures
      |    |- model
      |    |   |- __init__.py
      |    |- solution
      |    |   |- __init__.py
      |    |- tests
      |    |   |- __init__.py
      |    |- __init__.py
      |- utils

The next step is to create a class that represents structural nodes.

The Node Class

Create a new file in model named node.py and enter the code in Listing 15-1. This is the basic definition for a structural node.

import operator
from functools import reduce

from geom2d import Point, Vector


class StrNode:

    def __init__(
        self,
      _id: int,
        position: Point,
        loads=None,
        dx_constrained=False,
        dy_constrained=False
    ):
        self.id = _id
        self.position = position
      self.loads = loads or []
        self.dx_constrained = dx_constrained
        self.dy_constrained = dy_constrained

    @property
    def loads_count(self):
        return len(self.loads)

    @property
    def net_load(self):
      return reduce(
            operator.add,
            self.loads,
            Vector(0, 0)
        )

Listing 15-1: Structure node class

In this listing, we define the new class StrNode. This class defines an id, which will serve to identify each of its instances.

Note that the parameter passed to the constructor uses an underscore: _id . Python already has an id global function defined, so if we named our parameter the same (instead of using the underscore), we’d be shadowing the global id function definition inside the constructor. This means id wouldn’t refer to Python’s function inside the constructor but to our passed-in value instead. Although we’re not using Python’s id function inside this class’s constructor, we’ll try to avoid shadowing global functions.

The StrNode also includes an instance of the Point class that determines the node’s position and a list of loads applied to the node with a default value of None. The structure may have quite a few nodes without external loads applied to them; thus, we make the loads argument optional (and provide a default value of None). When the loads argument is None, we assign the self.loads attribute an empty list ([]) .

You might be wondering how the or operator works in :

self.loads = loads or []

The or operator returns the first “truthy” value from its operands or None. Take a look at the following examples:

>>> 'Hello' or 'Good bye'
'Hello'

>>> None or 'Good bye'
'Good bye'

>>> False or True
True

>>> False or 'Hello'
'Hello'

>>> False or None
# nothing returned here

>>> False or None or 'Hi'
'Hi'

As you might have guessed, in a boolean context, None is evaluated as “falsy.”

There are two more attributes that we have to pass the constructor; these are given a default value in the constructor: dx_constrained and dy_constrained. These attributes determine whether the displacements in the x and y directions are externally constrained. We initialize them as False, which means the node isn’t externally constrained unless we say otherwise.

We’ve defined two properties in the class: loads_count and net_load. The first, loads_count, simply returns the length of the loads list.

NOTE

If you remember the law of Demeter from Chapter 5, anyone from outside the StrNode class who wants to know the number of loads applied to the node should be able to ask StrNode directly. But asking StrNode to return the list of loads and then use the len function to get its length would violate this important principle.

The net_load property uses reduce to compute the sum of all the loads . Note that we’re passing in a third argument to the reduce function: Vector(0, 0). This third argument is the initial value for the reduction. In the perfectly valid case that the list of loads is empty, we’ll return this initial value. Otherwise, the first step in the reduction process will combine this initial value with the list’s first item. If we didn’t provide an initial value, reducing the loads list would raise the following error:

TypeError: reduce() of empty sequence with no initial value

Next, we’ll add a method to add loads to the node’s list of loads; enter the method in Listing 15-2.

class StrNode:
   --snip--

   def add_load(self, load: Vector):
       self.loads.append(load)

Listing 15-2: Adding loads to the node

Lastly, let’s implement the equality comparison for the StrNode class. There are a few attributes in the class, but we’ll consider two nodes equal only if they are located at equal positions in the plane. This comparison deems overlapping nodes to be equal, regardless of their other attributes.

If we want nodes in a structure to be truly unique, we could rely on an equality comparison that compares all of the attributes of a node, including the list of loads and external constraints. In our case, we’re interested only in making sure that we have no overlapping nodes, though. If we included more fields in the equality check, it could happen that two overlapping nodes (nodes with the same position) were evaluated as different because they have a different list of loads. We’d be allowing two overlapping nodes to exist in the structure.

Enter the __eq__ method implementation in Listing 15-3.

class StrNode:
   --snip--

    def __eq__(self, other):
        if self is other:
            return True

        if not isinstance(other, StrNode):
            return False

        return self.position == other.position

Listing 15-3: Nodes equality

Our StrNode class is now ready! Listing 15-4 contains the resulting StrNode class.

import operator
from functools import reduce

from geom2d import Point, Vector


class StrNode:

    def __init__(
            self,
            _id: int,
            position: Point,
            loads=None,
            dx_constrained=False,
            dy_constrained=False
    ):
        self.id = _id
        self.position = position
        self.loads = loads or []
        self.dx_constrained = dx_constrained
        self.dy_constrained = dy_constrained

    @property
    def loads_count(self):
        return len(self.loads)

    @property
    def net_load(self):
        return reduce(
            operator.add,
            self.loads,
            Vector(0, 0)
        )

    def add_load(self, load: Vector):
        self.loads.append(load)

    def __eq__(self, other):
        if self is other:
            return True

        if not isinstance(other, StrNode):
            return False

        return self.position == other.position

Listing 15-4: Node class result

Let’s now implement a class to represent structural bars.

The Bar Class

Structural bars are defined between two nodes modeled by the StrNode class. Bars need to store values for the two resistant properties required for the stiffness matrix calculation (Equation 15.6): the Young’s modulus and cross section.

Implementing the Bar Class

In model create a new file named bar.py and enter the initial definition for the StrBar class (Listing 15-5).

from geom2d import Segment
from .node import StrNode


class StrBar:

    def __init__(
            self,
            _id: int,
            start_node: StrNode,
            end_node: StrNode,
            cross_section: float,
            young_mod: float
    ):
        self.id = _id
        self.start_node = start_node
        self.end_node = end_node
        self.cross_section = cross_section
        self.young_mod = young_mod

    @property
    def geometry(self):
        return Segment(
            self.start_node.position,
            self.end_node.position
        )

    @property
    def length(self):
        return self.geometry.length

Listing 15-5: Structure bar class

In this listing we define the StrBar class with five attributes: the ID that serves as identifier, the start and end nodes, the cross section value, and the Young’s modulus value. These are passed in to the constructor and stored inside the class.

We also define two properties using the @property decorator: geometry and length. The geometry of the bar is a segment going from the start node position to the end node position, and the length of the bar is this segment’s length.

The last thing we need to implement is a method to compute the bar’s stiffness matrix in global coordinates as defined in Equation 15.6. Enter the method in Listing 15-6.

from eqs import Matrix
from geom2d import Segment
from .node import StrNode


class StrBar:
    --snip--

    def global_stiffness_matrix(self) -> Matrix:
        direction = self.geometry.direction_vector
        eal = self.young_mod * self.cross_section / self.length
        c = direction.cosine
        s = direction.sine

        c2_eal = (c ** 2) * eal
        s2_eal = (s ** 2) * eal
        sc_eal = (s * c) * eal

        return Matrix(4, 4).set_data([
            c2_eal, sc_eal, -c2_eal, -sc_eal,
            sc_eal, s2_eal, -sc_eal, -s2_eal,
            -c2_eal, -sc_eal, c2_eal, sc_eal,
            -sc_eal, -s2_eal, sc_eal, s2_eal
        ])

Listing 15-6: Bar stiffness matrix in global coordinates

Don’t forget to import Matrix, shown here:

from eqs import Matrix

We’ve added the global_stiffness_matrix method. This method creates a 4 × 4 matrix and sets its values to the appropriate stiffness terms as given in Equation 15.6 and repeated here for convenience:

Image

To compute each of the values, we first get the bar geometry’s direction vector and get its sine and cosine. Because every term in [k] is multiplied by Image, we compute it and store the result in the eal variable. From the sixteen terms in the matrix, there are really only three different values we need to compute. These are stored in c2_eal, s2_eal, and sc_eal, and they are later referenced in the set_data method.

Testing the Bar Class

The stiffness matrix computation is core to our structural analysis problem; a bug in this code would result in completely incorrect results, like, for instance, huge deformations in the bars. Let’s add a unit test to make sure all the terms in the stiffness matrix are computed correctly. We first need to create a new test file in the structures/tests directory named bar_test.py. In the file, enter the code in Listing 15-7.

import unittest
from math import sqrt

from eqs import Matrix
from geom2d import Point
from structures.model.node import StrNode
from structures.model.bar import StrBar


class BarTest(unittest.TestCase):
    section = sqrt(5)
    young = 5

    node_a = StrNode(1, Point(0, 0))
    node_b = StrNode(2, Point(2, 1))
    bar = StrBar(1, node_a, node_b, section, young)

    def test_global_stiffness_matrix(self):
        expected = Matrix(4, 4).set_data([
            4, 2, -4, -2,
            2, 1, -2, -1,
            -4, -2, 4, 2,
            -2, -1, 2, 1
        ])
        actual = self.bar.global_stiffness_matrix()
        self.assertEqual(expected, actual)

Listing 15-7: Testing the bar’s stiffness matrix

In this test we create a bar with nodes located at (0, 0) and (2, 1), a section of Image, and a Young’s modulus of 5. We chose these numbers so all the values in the expected stiffness matrix would be integers, which makes it convenient for us to write the assertion, particularly in this case: Image, Image, and Image.

You can run the test from the IDE by clicking the green play button or from the shell.

$ python3 -m unittest structures/tests/bar_test.py

This should produce the following output:

Ran 1 test in 0.000s

OK

Your StrBar class should look similar to Listing 15-8.

from eqs import Matrix
from geom2d import Segment
from .node import StrNode


class StrBar:

    def __init__(
            self,
            _id: int,
            start_node: StrNode,
            end_node: StrNode,
            cross_section: float,
            young_mod: float
    ):
        self.id = _id
        self.start_node = start_node
        self.end_node = end_node
        self.cross_section = cross_section
        self.young_mod = young_mod

    @property
    def geometry(self):
        return Segment(
            self.start_node.position,
            self.end_node.position
        )

    @property
    def length(self):
        return self.geometry.length

    def global_stiffness_matrix(self) -> Matrix:
        direction = self.geometry.direction_vector
        eal = self.young_mod * self.cross_section / self.length
        c = direction.cosine
        s = direction.sine

        c2_eal = (c ** 2) * eal
        s2_eal = (s ** 2) * eal
        sc_eal = (s * c) * eal

        return Matrix(4, 4).set_data([
            c2_eal, sc_eal, -c2_eal, -sc_eal,
            sc_eal, s2_eal, -sc_eal, -s2_eal,
            -c2_eal, -sc_eal, c2_eal, sc_eal,
            -sc_eal, -s2_eal, sc_eal, s2_eal
        ])

Listing 15-8: Bar class result

We need one last class to bundle nodes and bars together: the structure itself.

The Structure Class

Create a new Python file named structure.py in structures/model and enter the Structure class’s code (Listing 15-9).

from functools import reduce

from .node import StrNode
from .bar import StrBar


class Structure:
    def __init__(self, nodes: [StrNode], bars: [StrBar]):
        self.__bars = bars
        self.__nodes = nodes

    @property
    def nodes_count(self):
        return len(self.__nodes)

    @property
    def bars_count(self):
        return len(self.__bars)

    @property
    def loads_count(self):
        return reduce(
            lambda count, node: count + node.loads_count,
            self.__nodes,
            0
        )

Listing 15-9: Structure class

This class is quite simple at the moment, but in a later chapter, we’ll write the code responsible for assembling the structure’s global stiffness matrix, generating the system of equations, solving it, and creating the solution. For now, all the class does is store a list of nodes and a list of bars passed in to the constructor, along with a few computations that deal with the number of items it holds.

The loads_count property sums the load count from every node. To accomplish this, we pass a lambda function as the first argument to the reduce function. This lambda takes two arguments: the current count of loads and the next node in the self.__nodes list. This reduction requires an initial value (which is the third argument, the 0), which we add the first node’s count to. Without this initial value, the reduction couldn’t take place, because the reduce function wouldn’t know what value the lambda’s first parameter, count, had for the first iteration.

We now have the complete model that defines the structure!

Creating a Structure from the Python Shell

Let’s try to construct the truss structure in Figure 15-20 using our model classes.

Image

Figure 15-20: Example truss structure

To define the structure, first import the following classes in the Python shell:

>>> from geom2d import Point, Vector
>>> from structures.model.node import StrNode
>>> from structures.model.bar import StrBar
>>> from structures.model.structure import Structure

Then enter the following code:

>>> node_one = StrNode(1, Point(0, 0), None, True, True)
>>> node_two = StrNode(2, Point(100, 0), None, False, True)
>>> node_three = StrNode(3, Point(100, 100), (Vector(50, -100)))

>>> bar_one = (1, node_one, node_two, 20, 20000000)
>>> bar_two = (2, node_two, node_three, 20, 20000000)
>>> bar_three = (3, node_three, node_one, 20, 20000000)

>>> structure = Structure(
    (node_one, node_two, node_three),
    (bar_one, bar_two, bar_three)
)

As you can see, creating the model for a truss structure in code is a piece of cake. In any case, we’ll most often load the model from an external definition file, as we’ll learn in Chapter 17. Working an example by hand, nevertheless, is a great exercise to understand how our model classes work.

To finish this chapter, let’s create the model for the structure’s solution: the classes that will store the node displacements and bar stresses.

The Structure Solution Model

We’ll tackle resolving the structure in the next chapter, but we’ll prepare the classes to store the solution values here. For now, let’s imagine we have the resolution algorithm ready and require the solution classes to store the solution’s data.

When we resolve a structure, we first obtain the node displacements in global coordinates. From the new positions of the structure’s nodes, we can compute all the rest (strains, stresses, and reaction values). We need a new class to represent displaced nodes, which are similar to the nodes we’ve just defined using the StrNode class, but with the addition of a displacement vector.

These node displacements will elongate or compress the structure’s bars. Remember that bars develop strains and stresses, which are their mechanical response to being extended or compressed. The strain and stress values are important pieces of data in the structural solution: they’ll determine whether the structure can withstand the loads applied to it.

We’ll create a new class to represent the solution bars as well. This class will reference the displaced nodes and compute the strain and stress values.

The Solution Nodes

Let’s create the class that represents nodes in the structure’s solution. In the structures/solution package, create a new file named node.py and enter the code in Listing 15-10.

from geom2d import Vector
from structures.model.node import StrNode


class StrNodeSolution:
    def __init__(
            self,
            original_node: StrNode,
            global_disp: Vector
    ):
        self.__original_node = original_node
        self.global_disp = global_disp

    @property
  def id(self):
        return self.__original_node.id

    @property
  def original_pos(self):
        return self.__original_node.position

    @property
  def is_constrained(self):
        return self.__original_node.dx_constrained 
               or self.__original_node.dy_constrained

    @property
  def loads(self):
        return self.__original_node.loads

    @property
  def is_loaded(self):
       return self.__original_node.loads_count > 0

    @property
  def net_load(self):
        return self.__original_node.net_load

Listing 15-10: Solution node class

This listing declares the StrNodeSolution class. As you can see, this class’s constructor gets passed the original node and its displacement vector in global coordinates—that’s all we need. The original node is kept private to the class (__original_node), but some of its properties are exposed. For example, the id property simply returns the original node’s ID, and the same goes for loads.

The original_pos property returns the original node’s position: the position before applying the displacement obtained as part of the structure’s resolution. The naming here is important, as we’ll shortly add another property to expose the new position of the node after being displaced.

The is_constrained property checks whether the original node had any of its degrees of freedom (the displacement in x or y) externally constrained. We’ll use this information to know whether a reaction force needs to be computed for the node or not. Reaction forces are those external forces exerted by the supports or constraints in a node. We want to know the magnitude of the force a support absorbs to properly design and dimension this support.

Lastly, we have three properties related to the external loads: loads , is_loaded , and net_load . The first simply returns the original node’s list of forces. We’ll use this information when drawing the solution to a vector image like in Figure 15-14. Property is_loaded lets us know whether the node has any load applied. This property will be handy when we need to check which solution nodes have a load applied to them to draw those loads to the result diagram. Property net_load returns the original node’s net load, which we’ll use to compute the reaction force in the node.

Displaced Position

Let’s include the displaced position as a property. Since displacements tend to be orders of magnitude smaller than the structure’s dimensions, we’ll want to include a method that scales the displacement vector to plot the resulting deformed geometry. This ensures that we’ll be able to tell the deformed geometry apart from the original geometry in the resulting diagram.

Enter the code shown in Listing 15-11 in the StrNodeSolution class.

class StrNodeSolution:
   --snip--

   @property
   def displaced_pos(self):
       return self.original_pos.displaced(self.global_disp)

   def displaced_pos_scaled(self, scale=1):
       return self.original_pos.displaced(self.global_disp, scale)

Listing 15-11: Solution node displacement

The displaced_pos method returns the position of the original node after applying the global_disp vector to it. The displaced_pos_scaled method does something similar, but with a scale value that will allow us to increase the displacement’s size.

The End Result

If you’ve followed along, your StrNodeSolution class should look like Listing 15-12.

from geom2d import Vector
from structures.model.node import StrNode


class StrNodeSolution:
    def __init__(
            self,
            original_node: StrNode,
            global_disp: Vector
    ):
        self.__original_node = original_node
        self.global_disp = global_disp

    @property
    def id(self):
        return self.__original_node.id

    @property
    def original_pos(self):
        return self.__original_node.position

    @property
    def is_constrained(self):
        return self.__original_node.dx_constrained 
               or self.__original_node.dy_constrained 

    @property
    def loads(self):
        return self.__original_node.loads

    @property
    def is_loaded(self):
        return self.__original_node.loads_count > 0

    @property
    def displaced_pos(self):
        return self.original_pos.displaced(self.global_disp)

    def displaced_position_scaled(self, scale=1):
        return self.original_pos.displaced(self.global_disp, scale)

Listing 15-12: Solution node class result

Let’s now implement the bar’s solution class.

The Solution Bars

Knowing the displacements of a bar’s nodes is all we need to compute its strain and axial stress. We’ll explain why this is as we develop the StrBarSolution class.

Create a new file in structures/solution named bar.py and enter the code in Listing 15-13.

from structures.model.bar import StrBar
from .node import StrNodeSolution


class StrBarSolution:
    def __init__(
            self,
            original_bar: StrBar,
            start_node: StrNodeSolution,
            end_node: StrNodeSolution
    ):
        if original_bar.start_node.id != start_node.id:
            raise ValueError('Wrong start node')

        if original_bar.end_node.id != end_node.id:
            raise ValueError('Wrong end node')

        self.__original_bar = original_bar
        self.start_node = start_node
        self.end_node = end_node

    @property
    def id(self):
        return self.__original_bar.id

    @property
    def cross_section(self):
        return self.__original_bar.cross_section

    @property
    def young_mod(self):
        return self.__original_bar.young_mod

Listing 15-13: Solution bar class

The StrBarSolution class is initialized with the original bar and the two solution nodes. In the constructor, we check that we got the correct solution nodes passed in by comparing their IDs with the original bar nodes’ IDs. If we detect a wrong node is being passed, we raise a ValueError that will halt execution. If we continued executing the program, the results would be incorrect because the solution bar would be linked with nodes it wasn’t connected to in the original definition of the structure. This will prevent us from making mistakes when constructing the structure’s solution classes.

The class also defines the id, cross_section, and young_mod properties. These simply return the original bar’s values.

Elongation, Stress, and Strain

Let’s now work out the strain and stress values one step at a time. The stress can be derived from the strain (using Equation 15.4), so we’ll start with the strain. The strain is the bar’s elongation per unit of length (see Equation 15.3), so we need to find out this elongation value. For this, we first want to know both the bar’s original and resulting geometries. Enter the properties shown in Listing 15-14.

from geom2d import Segment
from structures.model.bar import StrBar
from .node import StrNodeSolution


class StrBarSolution:
   --snip--

   @property
   def original_geometry(self):
       return self.__original_bar.geometry

   @property
   def final_geometry(self):
       return Segment(
           self.start_node.displaced_pos,
           self.end_node.displaced_pos
       )

Listing 15-14: Solution bar geometry

The original geometry was already a property in StrBar. The final geometry is also a segment, this time between the displaced start and end nodes. It’s important to understand that since the bars of a truss structure are two-force members, they’re only subject to axial forces. Thus, the directrix of the bars will always remain a straight segment. Figure 15-21 depicts the original bar and the deformed bar that results when displacing the position of the original nodes Image and Image.

Image

Figure 15-21: A bar’s length increment

Assuming the original bar had a length of lo and that lf is the final length, the elongation of the bar is simply Δl = lf – lo. The elongation value will be positive if the bar stretches and negative if it compresses. Note that this agrees with our stress sign convention: positive for tension and negative for compression. Enter the properties in Listing 15-15.

class StrBarSolution:
   --snip--

   @property
   def original_length(self):
       return self.original_geometry.length

   @property
   def final_length(self):
       return self.final_geometry.length

   @property
   def elongation(self):
       return self.final_length - self.original_length

Listing 15-15: Solution bar length

Now that we know the bar’s elongation, we can easily compute the strain and also the stress. Enter the strain and stress properties in the StrBarSolution class as in Listing 15-16.

class StrBarSolution:
   --snip--

   @property
   def strain(self):
       return self.elongation / self.original_length

   @property
   def stress(self):
       return self.young_mod * self.strain

Listing 15-16: Bar strain and stress

Finally! As you can see, the strain, given by Equation 15.3, is the quotient between the bar’s elongation and the original length. With the strain value we can obtain the stress by simple multiplication with the material’s Young’s modulus. This is Hooke’s law as formulated in Equation 15.4.

Internal Forces

To compute the reaction forces, we’ll use the static equilibrium condition in each of the nodes: the net force in a node is always zero. In this sum of forces, every bar that is connected to the node exerts a force equal in value and opposite in direction to its internal force (this is illustrated in Figure 15-23). This internal force is computed as the bar’s stress times its cross section (see Equation 15.2).

We need both the magnitude and the direction of the internal force in each of the bar’s nodes, because, if you recall, for this two-force member to be in equilibrium, the forces in both ends need to have equal magnitude and opposite directions. Let’s see how we’d go about doing this.

Enter the code in Listing 15-17.

from geom2d import Segment, make_vector_between
from structures.model.bar import StrBar
from .node import StrNodeSolution


class StrBarSolution:
    --snip--

    @property
    def internal_force_value(self):
        return self.stress * self.cross_section

    def force_in_node(self, node: StrNodeSolution):
      if node is self.start_node:
            return make_vector_between(
                self.end_node.displaced_pos,
                self.start_node.displaced_pos
            ).with_length(
                self.internal_force_value
            )
      elif node is self.end_node:
            return make_vector_between(
                self.start_node.displaced_pos,
                self.end_node.displaced_pos
            ).with_length(
                self.internal_force_value
            )

        raise ValueError(
            f'Bar {self.id} does not know about node {node.id}'
        )

Listing 15-17: Bar internal force

In this code, we first define the internal_force_value property, which yields the magnitude, positive or negative, of the internal force computed according to Equation 15.2.

Then comes the force_in_node method, which, given either the start or end node of the bar, returns the force vector in that node. The magnitude of the force vector is internal_force_value in both cases. It’s the direction that changes depending on the passed-in node.

Our sign convention is that tension forces are considered positive and compression forces negative. If we choose the direction of the internal force to be positive in each of the nodes, the force vector will always have the correct direction. This is because later we’ll give it a length of internal_force_value, which is negative for a compressing force, and, as you know, assigning a negative length to one of our Vector instances reverses its direction.

Look back at the code. If the passed-in node is the start node , the force vector is created to go from the end node’s final position to the start’s. Then, the resulting vector is scaled according to internal_force_value.

Conversely, if the passed-in node is the end node , the force vector is the opposite, but the scaling part remains the same.

Lastly, if the passed-in node is neither of the two bar nodes, we raise an error.

Bar Has Node?

We’re almost done with the bar solution class; we just need two more methods, and our class will be ready. The first one checks whether any node in the structure is one of the end nodes in the bar. We’ll use this method to draw the results. Enter the method in Listing 15-18.

class StrBarSolution:
   --snip--

   def has_node(self, node: StrNodeSolution):
       return node is self.start_node or node is self.end_node

Listing 15-18: Bar has node?

Lastly, we need a method to generate the bar’s final geometry but with a scale applied to the displacements.

Scaled Final Geometry

If you remember, we already implemented a method in the StrNodeSolution class that yields its position with a scale applied to the displacement. Let’s harness this implementation to build the segment representing the deformed bar’s geometry with a scale applied. Enter the code in Listing 15-19.

class StrBarSolution:
   --snip--

   def final_geometry_scaling_displacement(self, scale: float):
       return Segment(
           self.start_node.displaced_pos_scaled(scale),
           self.end_node.displaced_pos_scaled(scale)
       )

Listing 15-19: Bar scaled geometry

The final_geometry_scaling_displacement method returns a segment whose end points are the bar nodes’ final positions with a scale applied to the displacement vector. This is the segment we’ll draw to the result plot to visualize how the original bar got displaced from its original position.

Again, because the displacements are fairly small compared to the size of the structure itself, we’ll want to scale the node displacements so we can clearly see how the structure gets deformed in the solution diagram.

The End Result

If you followed along, your StrBarSolution should look like Listing 15-20.

from geom2d import Segment, make_vector_between
from structures.model.bar import StrBar
from .node import StrNodeSolution


class StrBarSolution:
    def __init__(
            self,
            original_bar: StrBar,
            start_node: StrNodeSolution,
            end_node: StrNodeSolution
    ):
        if original_bar.start_node.id != start_node.id:
            raise ValueError('Wrong start node')

        if original_bar.end_node.id != end_node.id:
            raise ValueError('Wrong end node')

        self.__original_bar = original_bar
        self.start_node = start_node
        self.end_node = end_node

    @property
    def id(self):
        return self.__original_bar.id

    @property
    def cross_section(self):
        return self.__original_bar.cross_section

    @property
    def young_mod(self):
        return self.__original_bar.young_mod

    @property
    def original_geometry(self):
        return self.__original_bar.geometry

    @property
    def final_geometry(self):
        return Segment(
            self.start_node.displaced_pos,
            self.end_node.displaced_pos
        )

    @property
    def original_length(self):
        return self.original_geometry.length

    @property
    def final_length(self):
        return self.final_geometry.length

    @property
    def elongation(self):
        return self.final_length - self.original_length

    @property
    def strain(self):
        return self.elongation / self.original_length

    @property
    def stress(self):
        return self.young_mod * self.strain

    @property
    def internal_force_value(self):
        return self.stress * self.cross_section

    def force_in_node(self, node: StrNodeSolution):
        if node is self.start_node:
            return make_vector_between(
                self.end_node.displaced_pos,
                self.start_node.displaced_pos
            ).with_length(
                self.internal_force_value
            )
        elif node is self.end_node:
            return make_vector_between(
                self.start_node.displaced_pos,
                self.end_node.displaced_pos
            ).with_length(
                self.internal_force_value
            )

        raise ValueError(
            f'Bar {self.id} does not know about node {node.id}'
        )

    def has_node(self, node: StrNodeSolution):
        return node is self.start_node or node is self.end_node

    def final_geometry_scaling_displacement(self, scale: float):
        return Segment(
            self.start_node.displaced_position_scaled(scale),
            self.end_node.displaced_position_scaled(scale)
        )

Listing 15-20: Solution bar class result

There’s one last class we want to define: the structure solution.

The Structure Solution

Just as we had a class for the original structure model, we want a class representing the structure’s solution. The goal of this class is to put the solution nodes and bars together.

Create a new file in the structures/solution folder named structure.py. In the file, enter the basic definition for the class (Listing 15-21).

from .bar import StrBarSolution
from .node import StrNodeSolution


class StructureSolution:
    def __init__(
            self,
            nodes: [StrNodeSolution],
            bars: [StrBarSolution]
    ):
        self.nodes = nodes
        self.bars = bars

Listing 15-21: Structure solution class

The StructureSolution class is initialized with the list of nodes and bars that make up the solution. This is similar to the original structure’s definition. But because we’re using this class to generate results—reports and diagrams—we’ll need some additional attributes.

Structure Rectangular Bounds

When plotting the structural analysis results, we’ll want to know how much space we need to draw the complete structure. Knowing the rectangular bounds of the entire structure will allow us to compute the viewBox for the SVG plot later. Let’s compute these bounds and add in some margin as well (see Figure 15-22) so that there’s some extra room for drawing things like the arrows that represent loads.

Image

Figure 15-22: Bounding a structure

In the class, enter the bounds_rect method (Listing 15-22).

from geom2d import make_rect_containing_with_margin
from .bar import StrBarSolution
from .node import StrNodeSolution


class StructureSolution:
   --snip--

    def bounds_rect(self, margin: float, scale=1):
        d_pos = [
            node.displaced_pos_scaled(scale)
            for node in self.nodes
        ]
        return make_rect_containing_with_margin(d_pos, margin)

Listing 15-22: Structure graphical bounds

We first import the make_rect_containing_with_margin function. We implemented this function in Part II of the book; it creates a Rect primitive containing all the passed-in points, along with some margin.

The bounds_rect method we’ve written initializes the d_pos variable as a list with all the structure nodes’ displaced positions and passes it to the function, which generates the rectangle. Note that we’re using the scaled version of the displacements to make sure the rectangular bounds contain all the nodes in the positions where they’ll be drawn.

Node Reaction Forces

Lastly, because the StructureSolution class has access to all the nodes and bars of the structure, it will be in charge of calculating the reaction forces for each of the nodes. The StrNodeSolution class couldn’t do this computation itself, as it doesn’t have access to the list of bars that meet in that node.

Now how do we go about computing the reaction force in a node? Let’s suppose we have a node like that in Figure 15-23. Two bars, bar 1 and bar 2, meet in this node and are subject to internal forces Image and Image, respectively. An external load Image is applied to the node as well. This node is externally constrained, and Image is the reaction force we’re after.

Image

Figure 15-23: The reaction forces in a node

From these quantities, only Image is unknown. The bar internal forces, Image and Image, are computed using the force_in_node method we implemented in Listing 15-17, and the external load Image is given as part of the problem’s statement.

Provided the node is under static equilibrium, the following condition must be held.

Image

You may have noticed that in this condition the bar forces appear with a negative sign. Those are the reaction forces the node receives from the bars’ forces, in accordance with Newton’s third law. If a bar is subject to a pair of forces that compress it, the bar pulls the node toward itself. On the other hand, if a bar tends to expand, it’ll push the nodes away from itself.

We can easily isolate Image from the previous equation,

Image

or in a more generic fashion (Equation 15.8),

Image

where Image is the sum of all bar forces, and Image is the sum of all external loads applied to the node (the node’s net load).

Let’s implement this in our class. Enter the code in Listing 15-23.

import operator
from functools import reduce

from geom2d import make_rect_containing_with_margin, Vector
from .bar import StrBarSolution
from .node import StrNodeSolution



class StructureSolution:
   --snip--


    def reaction_for_node(self, node: StrNodeSolution):
      if not node.is_constrained:
            return Vector(0, 0)


      forces = [
            bar.force_in_node(node)
            for bar in self.bars
            if bar.has_node(node)
        ]


        if node.is_loaded:
          forces.append(node.net_load.opposite())


      return reduce(operator.add, forces)

Listing 15-23: Node reaction force

We’ve defined the reaction_for_node method, which, given a node, computes its reaction force. Don’t forget that reaction forces exist only for those nodes that have external supports or constraints. That’s in fact the first thing we check : if the node is not constrained, we return a zero vector (meaning no reaction force).

The second step is to search for all bars in the structure that are linked to the passed-in node and get their internal forces in that given node . We do this using a list comprehension that iterates through all the bars in the structure, filtering those that pass the bar.has_node(node) test and finally mapping each of them to its internal force in the given node. This is the Image in Equation 15.8.

Next, we append the net external load to the forces list if the node is externally loaded . Note that the net load received from the node appears with a negative sign in Equation 15.8, which is why we call the opposite method on it. Also note that we don’t need to sum these loads (as the Image in Equation 15.8 suggests) because the StrNodeSolution class already does that for us and provides us with the net load.

Lastly, all the forces in the list are summed using the reduce function with the operator.add operator .

The End Result

For your reference, Listing 15-24 shows the complete StructureSolution class implementation.

import operator
from functools import reduce

from geom2d import make_rect_containing_with_margin, Vector
from .bar import StrBarSolution
from .node import StrNodeSolution


class StructureSolution:
    def __init__(
            self,
            nodes: [StrNodeSolution],
            bars: [StrBarSolution]
    ):
        self.nodes = nodes
        self.bars = bars

    def bounds_rect(self, margin: float, scale=1):
        d_pos = [
            node.displaced_pos_scaled(scale)
            for node in self.nodes
        ]
        return make_rect_containing_with_margin(d_pos, margin)

    def reaction_for_node(self, node: StrNodeSolution):
        if not node.is_constrained:
            return Vector(0, 0)

        forces = [
            bar.force_in_node(node)
            for bar in self.bars
            if bar.has_node(node)
        ]

        if node.is_loaded:
            forces.append(node.net_load.opposite())

        return reduce(operator.add, forces)

Listing 15-24: Structure solution class result

It’s important to unit test this class to make sure we haven’t made any mistakes. Nevertheless, to test it, we need to learn about an advanced testing technique: mocking. We’ll be exploring this topic in the next chapter, so we’ll come back to this implementation.

Summary

We started this chapter reviewing some mechanics of materials topics such as the internal forces developed by elastic bodies as a response to being externally loaded. We introduced the concepts of stress and strain, both central to structural analysis. We were particularly interested in the axial stresses developed in prismatic bodies, as those are crucial in plane truss structures, the focus of this part of the book.

We then took a look at plane trusses and their particularities and formulated the relation between forces and displacements on a bar using the concept of a stiffness matrix. As we’ll see in the next chapter, these matrices play a crucial role in the resolution of the structure.

Lastly, we implemented the structure’s modeling classes: StrNode, StrBar, and Structure. We implemented the structure’s solution classes as well: StrNodeSolution, StrBarSolution, and StructureSolution. These two sets of classes represent the structure as originally designed and the structure solution, including the stress value for each bar and the displacements of every node. We’ll cover how we go from the original definition to the solution in the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.206.13.112