Buckling loads and modes of a slender column, checked against Euler's column formula.
uv run python examples/cli.py run buckling
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
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 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)