Skip to content

Beam (Pint)

fpc.beam

beam.py - a unit-aware Beam dataclass.

This module shows two ideas working together:

* ``@dataclass`` models a real-world *thing* (a rectangular beam) as an
  object with named fields, instead of passing a fistful of loose floats to
  every function.
* Pint quantities give every field and result physical units, so the math
  is self-checking and the answers come back in whatever unit you ask for.

Build one like this::

from fpc.units import Q_
from fpc.beam import Beam

beam = Beam(
    length=Q_(2.0, "m"),
    width=Q_(50, "mm"),
    height=Q_(100, "mm"),
    modulus=Q_(200, "GPa"),
)
print(beam.moment_of_inertia.to("mm**4"))
print(beam.max_deflection_point_load(load=Q_(5, "kN")).to("mm"))

The properties (area, moment of inertia, mass) are computed on demand from the dimensions, so they always stay consistent with the beam's geometry.

Beam dataclass

A straight beam with a rectangular cross-section, in physical units.

Parameters:

Name Type Description Default
length Quantity

span of the beam (a [length] quantity)

required
width Quantity

cross-section width b ([length])

required
height Quantity

cross-section height h ([length]), measured in the direction the bending load is applied

required
modulus Quantity

Young's modulus E (a [pressure] quantity), defaults to structural steel

(lambda: STEEL_MODULUS)()
density Quantity

material density ([mass]/[volume]), defaults to steel

(lambda: STEEL_DENSITY)()
Source code in fpc/beam.py
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
@dataclass
class Beam:
    """A straight beam with a rectangular cross-section, in physical units.

    :param length: span of the beam (a ``[length]`` quantity)
    :param width: cross-section width b (``[length]``)
    :param height: cross-section height h (``[length]``), measured in the
        direction the bending load is applied
    :param modulus: Young's modulus E (a ``[pressure]`` quantity),
        defaults to structural steel
    :param density: material density (``[mass]/[volume]``), defaults to steel
    """

    length: Quantity
    width: Quantity
    height: Quantity
    # ``field(default_factory=...)`` is how dataclasses give a default that is
    # an object rather than a literal - here, our default material properties.
    modulus: Quantity = field(default_factory=lambda: STEEL_MODULUS)
    density: Quantity = field(default_factory=lambda: STEEL_DENSITY)

    def __post_init__(self):
        """Validate units and signs right after the object is created.

        Catching a bad input here (e.g. a time passed where a length belongs,
        or a negative width) gives a clear error instead of a wrong number
        later on.
        """
        self._require(self.length, "[length]", "length")
        self._require(self.width, "[length]", "width")
        self._require(self.height, "[length]", "height")
        self._require(self.modulus, "[pressure]", "modulus")
        self._require(self.density, "[mass] / [length] ** 3", "density")

    @staticmethod
    def _require(quantity, dimensionality, name):
        """Raise a helpful error unless ``quantity`` is positive with the right units."""
        # ``.check()`` confirms the quantity has the expected dimensionality.
        if not hasattr(quantity, "check") or not quantity.check(dimensionality):
            raise ValueError(f"{name} must be a Pint quantity with dimensionality {dimensionality}")
        if quantity.magnitude <= 0:
            raise ValueError(f"{name} must be positive, got {quantity}")

    # --- Geometry (computed on demand) ------------------------------------
    @property
    def area(self):
        """Cross-sectional area A = width * height."""
        return self.width * self.height

    @property
    def moment_of_inertia(self):
        """Second moment of area about the bending (strong) axis: I = b*h^3/12.

        This is the property that resists bending; a taller section (bigger h)
        is dramatically stiffer because h is cubed.
        """
        return self.width * self.height**3 / 12

    @property
    def mass(self):
        """Total mass = density * volume = density * area * length."""
        return self.density * self.area * self.length

    # --- Structural behaviour ---------------------------------------------
    def max_deflection_point_load(self, *, load):
        """Maximum deflection of a simply supported beam with a central load.

        delta = P * L^3 / (48 * E * I).  The result carries length units, so
        call ``.to("mm")`` on it to read it off in millimetres.

        :param load: central point load P (a ``[force]`` quantity)
        :returns: maximum deflection (a ``[length]`` quantity)
        """
        self._require(load, "[force]", "load")
        deflection = load * self.length**3 / (48 * self.modulus * self.moment_of_inertia)
        # Reduce to base units so the result reads cleanly as metres.
        return deflection.to_base_units()

    def max_bending_stress_point_load(self, *, load):
        """Peak bending stress for a simply supported beam, central point load.

        The maximum bending moment is M = P*L/4 at midspan; the peak stress is
        sigma = M*c/I with c = h/2 (distance to the outer fibre).

        :param load: central point load P (a ``[force]`` quantity)
        :returns: maximum bending stress (a ``[pressure]`` quantity)
        """
        self._require(load, "[force]", "load")
        max_moment = load * self.length / 4
        distance_to_fibre = self.height / 2
        stress = max_moment * distance_to_fibre / self.moment_of_inertia
        return stress.to_base_units()

    def euler_buckling_load(self, *, end_factor=1.0):
        """Critical axial buckling load (Euler), about the weakest axis.

        P_cr = pi^2 * E * I_min / (K*L)^2.  A column buckles about whichever
        axis has the smaller second moment of area, so we use the minimum here.

        :param end_factor: effective-length factor K (1.0 pinned-pinned,
            0.5 fixed-fixed, 2.0 fixed-free), defaults to pinned-pinned
        :returns: critical axial load (a ``[force]`` quantity)
        """
        # Second moments about both axes; buckling follows the smaller one.
        inertia_strong = self.width * self.height**3 / 12
        inertia_weak = self.height * self.width**3 / 12
        inertia_min = min(inertia_strong, inertia_weak)

        effective_length = end_factor * self.length
        load = math.pi**2 * self.modulus * inertia_min / effective_length**2
        return load.to_base_units()

area property

Cross-sectional area A = width * height.

moment_of_inertia property

Second moment of area about the bending (strong) axis: I = b*h^3/12.

This is the property that resists bending; a taller section (bigger h) is dramatically stiffer because h is cubed.

mass property

Total mass = density * volume = density * area * length.

__post_init__()

Validate units and signs right after the object is created.

Catching a bad input here (e.g. a time passed where a length belongs, or a negative width) gives a clear error instead of a wrong number later on.

Source code in fpc/beam.py
65
66
67
68
69
70
71
72
73
74
75
76
def __post_init__(self):
    """Validate units and signs right after the object is created.

    Catching a bad input here (e.g. a time passed where a length belongs,
    or a negative width) gives a clear error instead of a wrong number
    later on.
    """
    self._require(self.length, "[length]", "length")
    self._require(self.width, "[length]", "width")
    self._require(self.height, "[length]", "height")
    self._require(self.modulus, "[pressure]", "modulus")
    self._require(self.density, "[mass] / [length] ** 3", "density")

max_deflection_point_load(*, load)

Maximum deflection of a simply supported beam with a central load.

delta = P * L^3 / (48 * E * I). The result carries length units, so call .to("mm") on it to read it off in millimetres.

Parameters:

Name Type Description Default
load

central point load P (a [force] quantity)

required

Returns:

Type Description

maximum deflection (a [length] quantity)

Source code in fpc/beam.py
108
109
110
111
112
113
114
115
116
117
118
119
120
def max_deflection_point_load(self, *, load):
    """Maximum deflection of a simply supported beam with a central load.

    delta = P * L^3 / (48 * E * I).  The result carries length units, so
    call ``.to("mm")`` on it to read it off in millimetres.

    :param load: central point load P (a ``[force]`` quantity)
    :returns: maximum deflection (a ``[length]`` quantity)
    """
    self._require(load, "[force]", "load")
    deflection = load * self.length**3 / (48 * self.modulus * self.moment_of_inertia)
    # Reduce to base units so the result reads cleanly as metres.
    return deflection.to_base_units()

max_bending_stress_point_load(*, load)

Peak bending stress for a simply supported beam, central point load.

The maximum bending moment is M = PL/4 at midspan; the peak stress is sigma = Mc/I with c = h/2 (distance to the outer fibre).

Parameters:

Name Type Description Default
load

central point load P (a [force] quantity)

required

Returns:

Type Description

maximum bending stress (a [pressure] quantity)

Source code in fpc/beam.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def max_bending_stress_point_load(self, *, load):
    """Peak bending stress for a simply supported beam, central point load.

    The maximum bending moment is M = P*L/4 at midspan; the peak stress is
    sigma = M*c/I with c = h/2 (distance to the outer fibre).

    :param load: central point load P (a ``[force]`` quantity)
    :returns: maximum bending stress (a ``[pressure]`` quantity)
    """
    self._require(load, "[force]", "load")
    max_moment = load * self.length / 4
    distance_to_fibre = self.height / 2
    stress = max_moment * distance_to_fibre / self.moment_of_inertia
    return stress.to_base_units()

euler_buckling_load(*, end_factor=1.0)

Critical axial buckling load (Euler), about the weakest axis.

P_cr = pi^2 * E * I_min / (K*L)^2. A column buckles about whichever axis has the smaller second moment of area, so we use the minimum here.

Parameters:

Name Type Description Default
end_factor

effective-length factor K (1.0 pinned-pinned, 0.5 fixed-fixed, 2.0 fixed-free), defaults to pinned-pinned

1.0

Returns:

Type Description

critical axial load (a [force] quantity)

Source code in fpc/beam.py
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def euler_buckling_load(self, *, end_factor=1.0):
    """Critical axial buckling load (Euler), about the weakest axis.

    P_cr = pi^2 * E * I_min / (K*L)^2.  A column buckles about whichever
    axis has the smaller second moment of area, so we use the minimum here.

    :param end_factor: effective-length factor K (1.0 pinned-pinned,
        0.5 fixed-fixed, 2.0 fixed-free), defaults to pinned-pinned
    :returns: critical axial load (a ``[force]`` quantity)
    """
    # Second moments about both axes; buckling follows the smaller one.
    inertia_strong = self.width * self.height**3 / 12
    inertia_weak = self.height * self.width**3 / 12
    inertia_min = min(inertia_strong, inertia_weak)

    effective_length = end_factor * self.length
    load = math.pi**2 * self.modulus * inertia_min / effective_length**2
    return load.to_base_units()