Files
OneCAD-Simulation/crates/solver/tests/buckle.rs
T
jyvikoandClaude Opus 5.5 1b57e2e672 solver, model, engine: linear buckling steps
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
2026-10-14 13:30:59 -04:00

94 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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());
}