forked from 1b-agent/fieldcraft-upstream
Procedure::Buckle {modes, shift}: the step's loads as a linear preload, K_σ from its stresses,
and (K + λ K_σ) φ = 0 by block Lanczos in the K inner product: K⁻¹(−K_σ) on Cholesky without a
shift, the buckling transformation (K + σ K_σ)⁻¹ K on LDLᵀ with one, whose inertia counts the
load factors below σ (Grimes–Lewis–Simon 1994). Lanczos gains an operand and an eigenvalue map
(defaults keep frequency steps bit for bit) and reports converged negative eigenvalues. Results
are frequency results with a `buckling` part; modes display with their load factor.
solve.buckle (in the Static drop-down); Abaqus *BUCKLE, Nastran SOL 105 and LS-DYNA refuse it by
name. Euler columns of hex20 within 0.18 % of Engesser's load.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
94 lines
4.6 KiB
Rust
94 lines
4.6 KiB
Rust
//! Linear buckling against closed forms: Euler columns of hex20 solids (pinned–pinned by a
|
||
//! symmetry plane at midspan, and clamped–free), with Engesser's shear correction
|
||
//! P = P_e / (1 + P_e/(κ G A)) (S. P. Timoshenko, J. M. Gere, *Theory of Elastic Stability*,
|
||
//! 2nd ed., McGraw-Hill 1961, §2.1, §2.17), the shifted (LDLᵀ) form against the plain one, and
|
||
//! what buckling steps refuse.
|
||
|
||
mod common;
|
||
|
||
use common::*;
|
||
use fieldcraft_model::*;
|
||
use fieldcraft_solver::{Limits, solve_buckle};
|
||
|
||
const E: f64 = 1000.0;
|
||
const B: f64 = 1.0;
|
||
const L: f64 = 20.0;
|
||
|
||
fn buckle(modes: u32, shift: Option<f64>) -> Procedure {
|
||
Procedure::Buckle { modes, shift }
|
||
}
|
||
|
||
/// Engesser's critical load of a square column of side B and effective length `le`, ν = 0.
|
||
fn engesser(le: f64) -> f64 {
|
||
let (a, i, g) = (B * B, B.powi(4) / 12.0, E / 2.0);
|
||
let pe = std::f64::consts::PI.powi(2) * E * i / (le * le);
|
||
pe / (1.0 + pe / (5.0 / 6.0 * g * a))
|
||
}
|
||
|
||
/// A column of `length` along z, 2 × 2 × `cells` hex20, pressed by a unit pressure on its top.
|
||
fn column(length: f64, cells: usize, shift: Option<f64>) -> Grid {
|
||
let mut g = grid([2, 2, cells], ElementKind::Hex20, |x| [B * x[0], B * x[1], length * x[2]]);
|
||
g.define(iso(E, 0.0), None, Formulation::default(), buckle(3, shift));
|
||
g.face_set("top", |x| near(x[2], 1.0));
|
||
g.load("press", Load::Pressure { set: "top".into(), magnitude: 1.0 });
|
||
g
|
||
}
|
||
|
||
#[test]
|
||
fn euler_columns() {
|
||
// Pinned–pinned: half the column, the pinned end (z = 0 here is the loaded top) held
|
||
// laterally over its face (which leaves its rotation free), midspan a symmetry plane.
|
||
let mut g = grid([2, 2, 10], ElementKind::Hex20, |x| [B * x[0], B * x[1], L / 2.0 * x[2]]);
|
||
g.define(iso(E, 0.0), None, Formulation::default(), buckle(2, None));
|
||
g.face_set("end", |x| near(x[2], 0.0));
|
||
g.load("press", Load::Pressure { set: "end".into(), magnitude: 1.0 });
|
||
g.node_set("pin", |x| near(x[2], 0.0));
|
||
g.bc("pin", BoundaryCondition::Displacement { target: Target::Set("pin".into()), values: [Some(0.0), Some(0.0), None, None, None, None] });
|
||
g.symmetry("mid", Symmetry::Z, |x| near(x[2], 1.0));
|
||
let r = solve_buckle(&g.model, Some("Step"), &Limits::default()).unwrap();
|
||
let want = engesser(L) / (B * B);
|
||
let got = r.modes[0].eigenvalue;
|
||
eprintln!("pinned: λ = {got:.6} (Engesser {want:.6}, {:+.3} %), second {:.6}", 100.0 * (got / want - 1.0), r.modes[1].eigenvalue);
|
||
assert!((got / want - 1.0).abs() < 5e-3);
|
||
assert!((r.modes[1].eigenvalue / got - 1.0).abs() < 1e-6, "the square section's two planes");
|
||
assert!(r.buckling.as_ref().unwrap().negative.is_empty() || r.buckling.as_ref().unwrap().negative[0] < -got);
|
||
|
||
// Clamped–free: the base held, the top free: le = 2L.
|
||
let mut g = column(L / 2.0, 10, None);
|
||
g.fixed("base", |x| near(x[2], 0.0));
|
||
let r = solve_buckle(&g.model, Some("Step"), &Limits::default()).unwrap();
|
||
let want = engesser(L) / (B * B);
|
||
let got = r.modes[0].eigenvalue;
|
||
eprintln!("clamped–free: λ = {got:.6} (Engesser {want:.6}, {:+.3} %)", 100.0 * (got / want - 1.0));
|
||
assert!((got / want - 1.0).abs() < 5e-3);
|
||
// Scaled to a largest component of 1, which is lateral at the free top.
|
||
let big = r.modes[0].shape.iter().flatten().fold(0.0f64, |m, v| m.max(v.abs()));
|
||
assert!((big - 1.0).abs() < 1e-12);
|
||
|
||
// Shifted between the first and the higher modes (LDLᵀ of K + σ K_σ): its inertia counts
|
||
// the two load factors below the shift, and the modes above it come out.
|
||
let mut s = column(L / 2.0, 10, Some(2.0 * got));
|
||
s.fixed("base", |x| near(x[2], 0.0));
|
||
let shifted = solve_buckle(&s.model, Some("Step"), &Limits::default()).unwrap();
|
||
let info = shifted.buckling.as_ref().unwrap();
|
||
assert_eq!(info.below_shift, Some(2));
|
||
let third = r.modes[2].eigenvalue;
|
||
assert!((shifted.modes[0].eigenvalue / third - 1.0).abs() < 1e-7, "{} vs {third}", shifted.modes[0].eigenvalue);
|
||
}
|
||
|
||
#[test]
|
||
fn buckling_steps_refuse_what_they_cannot_do() {
|
||
// No load: nothing to buckle.
|
||
let mut g = column(L, 4, None);
|
||
g.model.set_load_active("Step", "press", false).unwrap();
|
||
g.fixed("base", |x| near(x[2], 0.0));
|
||
assert!(solve_buckle(&g.model, Some("Step"), &Limits::default()).is_err());
|
||
// Unsupported: a located singular stiffness.
|
||
let g = column(L, 4, None);
|
||
let e = solve_buckle(&g.model, Some("Step"), &Limits::default()).unwrap_err();
|
||
assert!(e.to_string().contains("singular"), "{e}");
|
||
// A shift that is not finite, and a step of another kind.
|
||
assert!(Procedure::Buckle { modes: 0, shift: None }.validate().is_err());
|
||
assert!(Procedure::Buckle { modes: 1, shift: Some(f64::NAN) }.validate().is_err());
|
||
}
|