← all demos

buckling

Buckling loads and modes of a slender column, checked against Euler's column formula.

uv run python examples/cli.py run buckling

A pinned column buckles into half-sine waves. Mode 1 is a single half-wave at the lowest load, the shape a real column takes. Each higher mode adds a half-wave and costs n^2 as much (mode 2 is ~4x mode 1), and is reached only if the lower ones are braced out. A support at mid-span, a node of mode 2 but not of mode 1, buys the jump to it. The shapes are the eigenvectors of K phi = -lambda K_g phi and the load factors its eigenvalues.
A pinned column buckles into half-sine waves. Mode 1 is a single half-wave at the lowest load, the shape a real column takes. Each higher mode adds a half-wave and costs n^2 as much (mode 2 is ~4x mode 1), and is reached only if the lower ones are braced out. A support at mid-span, a node of mode 2 but not of mode 1, buys the jump to it. The shapes are the eigenvectors of K phi = -lambda K_g phi and the load factors its eigenvalues.
The same slender column held four ways, buckling at loads spanning 16x. Clamping an end against rotation shortens the effective length K*L the column buckles over, from 2L free-standing down to L/2 with both ends fixed, and the load goes as 1/K^2. The measured K sits within a few percent of Euler's 2, 1, 1/2 and ~0.7; the small excess is a real continuum effect, a clamp in a solid adding a little Saint-Venant stiffening an ideal beam has none of.
The same slender column held four ways, buckling at loads spanning 16x. Clamping an end against rotation shortens the effective length K*L the column buckles over, from 2L free-standing down to L/2 with both ends fixed, and the load goes as 1/K^2. The measured K sits within a few percent of Euler's 2, 1, 1/2 and ~0.7; the small excess is a real continuum effect, a clamp in a solid adding a little Saint-Venant stiffening an ideal beam has none of.
Euler's column formula gives the buckling load of an ideal slender elastic column, P_cr = pi^2 E* I / (K L)^2. Left: sweeping the length of a pinned column, the critical load falls as 1/L^2 (a slope of -2 on log-log) and lands on it, with E* = E/(1-nu^2) the plane-strain modulus a 2D solve sees. Right: the effective-length factor K read back from each end condition's buckling load, against the textbook values.
Euler's column formula gives the buckling load of an ideal slender elastic column, P_cr = pi^2 E* I / (K L)^2. Left: sweeping the length of a pinned column, the critical load falls as 1/L^2 (a slope of -2 on log-log) and lands on it, with E* = E/(1-nu^2) the plane-strain modulus a 2D solve sees. Right: the effective-length factor K read back from each end condition's buckling load, against the textbook values.
Euler (1744): an ideal slender column buckles at P_cr = pi^2 E* I / (K L)^2.
This demo reproduces it three ways: mode shapes, end conditions, slenderness.

effective-length factor K (measured vs Euler):
  Cantilever     2.000  (Euler 2)
  Pinned-pinned  1.004  (Euler 1)
  Fixed-fixed    0.505  (Euler 0.5)
  Fixed-pinned   0.706  (Euler 0.699)
slenderness law    P_cr ~ L^-1.99   (Euler exponent -2)
buckling-load ratios (Euler 0.25 : 4 : 2.05):  Cantilever/pinned 0.25   Fixed-fixed/pinned 3.95   Fixed-pinned/pinned 2.03

What was imposed

A pinned-pinned column: both ends held across their width (u_y = 0) so they stay in line but can still rotate, one point anchoring the axial slide, and a compressive traction on the right. The transverse support and the axial load share the loaded edge, a roller carrying a tangential traction.
A pinned-pinned column: both ends held across their width (u_y = 0) so they stay in line but can still rotate, one point anchoring the axial slide, and a compressive traction on the right. The transverse support and the axial load share the loaded edge, a roller carrying a tangential traction.

Source

The functions that pose and solve the problem. The figures are below the fold.

"""Buckling loads and modes of a slender column, checked against Euler's column formula.

Buckling is an eigenproblem: a reference load puts the column under a prestress,
BucklingAnalysis assembles the geometric stiffness K_g from it and solves
K phi = -lambda K_g phi, and lambda multiplies the reference load. P2 elements
throughout: the constant-strain triangle locks in bending.

`solve_buckling` solves one column under one end condition; `run` calls it for the
pinned column's modes, for each of the four classic end conditions, and over a sweep of
lengths, and returns a `BucklingStudy` of plain results. Nothing here draws:
`figures.py` does that from the study, and this file is what the gallery shows.
"""
from dataclasses import dataclass

import numpy as np

from fem.analysis.buckling import BucklingAnalysis
from fem.boundary import Dirichlet, Neumann
from fem.conditions import Conditions
from fem.elements import QuadraticTriangleElement
from fem.mesh.mesh import Mesh
from fem.mesh.structured import box_mesh
from fem.physics.equations import LinearElastic
from fem.post.solution import BucklingSolution
from fem.regions import intersect, on_plane


def column(length: float = 24.0, height: float = 1.0,
           n_length: int = 48, n_across: int = 6) -> Mesh:
    """A slender column standing upright, meshed for a buckling solve.

    Length runs along y (ends at y = 0 and y = length) so the mode shapes draw as columns
    stand, with `height` the thin cross-dimension along x.

    The through-thickness count is set independently of the aspect
    ratio: a buckling mode is bending, which needs several elements across the thin
    dimension. `n_across` is forced odd so a vertex lands on the neutral axis for a
    pinned end to anchor.
    """
    n_across += 1 - n_across % 2
    return box_mesh(corners=[[0.0, 0.0], [height, length]],
                            resolution=(n_across, n_length))

E, NU = 200.0, 0.3
E_STAR = E / (1 - NU**2)     # plane-strain effective modulus, the one bending sees
equation = LinearElastic(E, NU)


def second_moment(height):
    """Second moment of area of the rectangular section."""
    return height**3 / 12


def euler_load(span, height, K=1.0):
    """Euler (1744): an ideal slender column buckles at P_cr = pi^2 E* I / (K L)^2."""
    return np.pi**2 * E_STAR * second_moment(height) / (K * span)**2


# The four classic end conditions. What sets an end's effective-length factor is
# whether it can rotate: a traction-loaded edge (u_y free) rotates (a pin or a free
# end), an imposed uniform axial displacement (u_y fixed) cannot (a clamp). u_x = 0
# along an edge holds it transversely without touching its rotation. The column
# stands along y, so the ends are at y = 0 and y = span and the load pushes in -y.
def cantilever(span, height):   # fixed-free, K = 2
    return Conditions(
        Dirichlet(on_plane(1, 0.0), [0, 0]),
        Neumann(on_plane(1, span), [0, -1.0]),
    )


def pinned(span, height):       # pinned-pinned, K = 1
    return Conditions(
        Dirichlet(on_plane(1, 0.0), [0, None]),
        Dirichlet(intersect(on_plane(1, 0.0), on_plane(0, height / 2)), [0, 0]),
        Dirichlet(on_plane(1, span), [0, None]),
        Neumann(on_plane(1, span), [0, -1.0]),
    )


def fixed(span, height):        # fixed-fixed, K = 1/2
    return Conditions(
        Dirichlet(on_plane(1, 0.0), [0, 0]),
        Dirichlet(on_plane(1, span), [0, -0.02 * span]),
    )


def fixed_pinned(span, height):  # fixed-pinned, K ~ 0.7
    return Conditions(
        Dirichlet(on_plane(1, 0.0), [0, 0]),
        Dirichlet(on_plane(1, span), [0, None]),
        Neumann(on_plane(1, span), [0, -1.0]),
    )


ENDS = [('Cantilever', cantilever, 2.0),
        ('Pinned-pinned', pinned, 1.0),
        ('Fixed-fixed', fixed, 0.5),
        ('Fixed-pinned', fixed_pinned, 0.699)]


def solve_buckling(mesh, bc, span, height, n_modes) -> tuple[BucklingSolution, np.ndarray]:
    """The first `n_modes` buckling modes of the column and their physical loads.

    The load factor multiplies the reference load; the physical buckling load is that
    factor times the actual axial force the column carries, read at mid-span where it
    is uniform and clear of the end disturbances.
    """
    problem = equation.problem(mesh, bc, element_type=QuadraticTriangleElement)
    solution = BucklingAnalysis(n_modes=n_modes).solve(problem)
    centroids = mesh.centroids
    dy = span / (len(np.unique(mesh.vertices[:, 1])) - 1)
    midspan = np.abs(centroids[:, 1] - span / 2) < dy
    assert solution.reference is not None
    axial = -float(np.mean(solution.reference.stress[midspan, 1, 1])) * height
    return solution, solution.load_factors * axial




@dataclass
class EndCondition:
    """One way of holding the column's ends, solved for its first buckling mode."""
    name: str
    bc: Conditions
    solution: BucklingSolution
    load: float                     # the first critical load
    K_ideal: float                  # Euler's effective-length factor
    K_measured: float               # the factor read back from the computed load


@dataclass
class BucklingStudy:
    """Everything `run` computed, for the figures and the summary to read."""
    length: float
    height: float
    mesh: Mesh
    pinned_bc: Conditions
    pinned: BucklingSolution        # the pinned column's first modes
    pinned_loads: np.ndarray        # their critical loads
    ends: list[EndCondition]        # the same column held four ways
    sweep_lengths: np.ndarray       # pinned-column lengths swept for the slenderness law
    sweep_loads: np.ndarray         # the first critical load at each

    @property
    def n_modes(self) -> int:
        return len(self.pinned_loads)

    @property
    def slope(self) -> float:
        """The fitted exponent of P_cr ~ L^slope over the sweep (Euler: -2)."""
        return float(np.polyfit(np.log(self.sweep_lengths), np.log(self.sweep_loads), 1)[0])

    @property
    def load_ratios(self) -> dict[str, float]:
        """Each other end condition's critical load over the pinned column's."""
        pinned_load = next(e.load for e in self.ends if e.name == 'Pinned-pinned')
        return {e.name: e.load / pinned_load for e in self.ends if e.name != 'Pinned-pinned'}


def run(length=24.0, height=1.0, n_length=48, n_across=6, n_modes=3,
        sweep_lengths=(16.0, 20.0, 28.0, 40.0)) -> BucklingStudy:
    """Solve the pinned column's modes, the four end conditions, and the length sweep."""
    n_across += n_across % 2      # a vertex on the neutral axis, for the pinned anchor
    mesh = column(length, height, n_length, n_across)

    # 1. Mode shapes of a pinned column: the buckling analogue of vibration modes.
    pinned_bc = pinned(length, height)
    pinned_solution, pinned_loads = solve_buckling(mesh, pinned_bc, length, height, n_modes)

    # 2. Effective length: the same column, four ways to hold its ends. K is read back
    # from the computed load by inverting Euler's formula.
    ends = []
    for name, make_bc, K_ideal in ENDS:
        bc = make_bc(length, height)
        solution, loads = solve_buckling(mesh, bc, length, height, 1)
        K_measured = np.pi / length * np.sqrt(E_STAR * second_moment(height) / loads[0])
        ends.append(EndCondition(name, bc, solution, float(loads[0]), K_ideal, float(K_measured)))

    # 3. Slenderness: the pinned column's critical load over a sweep of lengths.
    sweep_loads = [solve_buckling(column(L, height, max(32, int(2 * L)), n_across),
                                  pinned(L, height), L, height, 1)[1][0] for L in sweep_lengths]
    return BucklingStudy(length, height, mesh, pinned_bc, pinned_solution, pinned_loads, ends,
                         np.array(sweep_lengths), np.array(sweep_loads))
The figures, and the demo that assembles them
"""The figures and summary of the buckling demo, drawn from a `BucklingStudy`."""
import numpy as np
from demo_registry import Demo, DemoResult, Figure

from demos._charts import hide_x_ticks, share_panel_limits
from demos.buckling import physics
from demos.buckling.physics import BucklingStudy, euler_load, run
from fem.plot.plotter import Plotter


def _buckled(s: BucklingStudy, solution, i):
    """The mesh deformed by mode `i`, scaled so its bow is a fixed fraction of span,
    and the signed transverse displacement to colour it by."""
    mode = solution.mode(i)
    transverse = mode.component(0)[:len(s.mesh.vertices)]
    scale = 0.14 * s.length / np.abs(transverse).max()
    return mode.deformed_mesh(scale), scale * transverse




def _modes_figure(s: BucklingStudy) -> Figure:
    # Upright columns in a row, with one glyph-and-colour key below all of them.
    modes = Plotter(1, s.n_modes, figsize=(3.2 * s.n_modes, 6.0), axis_labels=False,
                    title='Buckling modes of a pinned-pinned column')
    for i in range(s.n_modes):
        shape, colour = _buckled(s, s.pinned, i)
        modes.plot(shape, colour, mode='colored', idx=(0, i), cmap='coolwarm', colorbar=False,
                   title=f'Mode {i+1}: P_cr = {s.pinned_loads[i]:.3g}\n'
                         f'({i+1} half-wave{"s" if i else ""})')
        # The pin/load glyphs, on the deformed shape so the load rides the moving end.
        modes.overlay_supports(s.mesh, s.pinned_bc, idx=(0, i), coords=shape.vertices)
        hide_x_ticks(modes, (0, i))
    share_panel_limits(modes, s.n_modes)
    modes.fig.supxlabel(
        'Blue triangles: the pinned ends, held sideways but free to rotate.\n'
        'Red arrow: the compressive load.\n'
        'Colour: sideways deflection; its sign and amplitude are arbitrary.',
        fontsize='medium')
    return Figure(
        modes,
        'A pinned column buckles into half-sine waves. Mode 1 is a single half-wave '
        'at the lowest load, the shape a real column takes. Each higher mode adds a '
        'half-wave and costs n^2 as much (mode 2 is ~4x mode 1), and is reached only '
        'if the lower ones are braced out. A support at mid-span, a node of mode 2 '
        'but not of mode 1, buys the jump to it. The shapes are the eigenvectors of '
        'K phi = -lambda K_g phi and the load factors its eigenvalues.',
        'modes', thumbnail=True)


def _end_conditions_figure(s: BucklingStudy) -> Figure:
    n = len(s.ends)
    factor_plots = Plotter(1, n, figsize=(2.4 * n, 6.6), axis_labels=False,
                           title='End conditions set the effective length')
    for col, end in enumerate(s.ends):
        shape, colour = _buckled(s, end.solution, 0)
        factor_plots.plot(shape, colour, mode='colored', idx=(0, col), cmap='coolwarm',
                          colorbar=False,
                          title=f'{end.name}\nK = {end.K_measured:.2f} (Euler {end.K_ideal:g})\n'
                                f'P_cr = {end.load:.3g}')
        # Each end's supports drawn on it: a wall clamps, triangles pin, arrows load.
        factor_plots.overlay_supports(s.mesh, end.bc, idx=(0, col), coords=shape.vertices)
    share_panel_limits(factor_plots, n)
    return Figure(
        factor_plots,
        'The same slender column held four ways, buckling at loads spanning 16x. '
        'Clamping an end against rotation shortens the effective length K*L the '
        'column buckles over, from 2L free-standing down to L/2 with both ends fixed, '
        'and the load goes as 1/K^2. The measured K sits within a few percent of '
        'Euler\'s 2, 1, 1/2 and ~0.7; the small excess is a real continuum effect, a '
        'clamp in a solid adding a little Saint-Venant stiffening an ideal beam has none of.',
        'end_conditions')


def _laws_figure(s: BucklingStudy) -> Figure:
    laws = Plotter(1, 2, title="Against Euler's column theory")
    curve = laws.chart_ax(idx=(0, 0), xlabel='length L', ylabel='critical load P_cr')
    curve.loglog(s.sweep_lengths, s.sweep_loads, 'o', color='tab:blue',
                 label=f'computed (slope {s.slope:.2f})')
    dense_L = np.linspace(s.sweep_lengths.min(), s.sweep_lengths.max(), 100)
    curve.loglog(dense_L, euler_load(dense_L, s.height), '-', color='tab:red',
                 alpha=0.6, label='Euler  pi^2 E* I / L^2')
    curve.set_title('Pinned column: P_cr goes as 1/L^2')
    curve.grid(True, which='both', alpha=0.3)

    names = [e.name for e in s.ends]
    bars = laws.chart_ax(idx=(0, 1), xlabel='', ylabel='effective-length factor K')
    x = np.arange(len(names))
    bars.bar(x - 0.2, [e.K_ideal for e in s.ends], 0.4, color='tab:red', alpha=0.6,
             label='Euler')
    bars.bar(x + 0.2, [e.K_measured for e in s.ends], 0.4, color='tab:blue', label='computed')
    bars.set_xticks(x, names, rotation=20, ha='right', fontsize='small')
    bars.set_title('Effective-length factor by end condition')
    bars.grid(True, axis='y', alpha=0.3)
    return Figure(
        laws,
        'Euler\'s column formula gives the buckling load of an ideal slender elastic '
        'column, P_cr = pi^2 E* I / (K L)^2. Left: sweeping the length of a pinned '
        'column, the critical '
        'load falls as 1/L^2 (a slope of -2 on log-log) and lands on it, with '
        'E* = E/(1-nu^2) the plane-strain modulus a 2D solve sees. Right: the '
        'effective-length factor K read back from each end condition\'s buckling load, '
        'against the textbook values.',
        'laws')


def _conditions_figure(s: BucklingStudy) -> Figure:
    conditions = Plotter(panel_aspect=0.7)   # tall and narrow, matching the upright column
    conditions.plot(s.mesh, mode='bc', conditions=s.pinned_bc)
    return Figure(
        conditions,
        'A pinned-pinned column: both ends held across their width (u_y = 0) so they '
        'stay in line but can still rotate, one point anchoring the axial slide, and a '
        'compressive traction on the right. The transverse support and the axial load '
        'share the loaded edge, a roller carrying a tangential traction.',
        'conditions', setup=True)


def _summary(s: BucklingStudy) -> str:
    ratios = '   '.join(f'{name}/pinned {ratio:.2f}' for name, ratio in s.load_ratios.items())
    return ('Euler (1744): an ideal slender column buckles at P_cr = pi^2 E* I / (K L)^2.\n'
            'This demo reproduces it three ways: mode shapes, end conditions, slenderness.\n\n'
            'effective-length factor K (measured vs Euler):\n'
            + '\n'.join(f'  {e.name:<14} {e.K_measured:.3f}  (Euler {e.K_ideal:g})'
                        for e in s.ends)
            + f'\nslenderness law    P_cr ~ L^{s.slope:.2f}   (Euler exponent -2)\n'
            + f'buckling-load ratios (Euler 0.25 : 4 : 2.05):  {ratios}')


def demo(**kwargs) -> DemoResult:
    """Buckling loads and modes of a slender column, checked against Euler's column
    formula."""
    s = run(**kwargs)
    return DemoResult([
        _modes_figure(s),
        _end_conditions_figure(s),
        _laws_figure(s),
        _conditions_figure(s),
    ], text=_summary(s))


# Builds its own columns (several lengths, four end conditions), so it takes no domain.
DEMO = Demo('buckling', demo, section='Solids & structures',
            smoke_kwargs={'n_length': 12, 'n_across': 4, 'n_modes': 2,
                          'sweep_lengths': (12.0, 18.0)},
            show_source=physics)