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
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 13:30:59 -04:00
1 parent 9a8ae50bca
commit 1b57e2e672
20 files changed
+511 -27

No files matched your search

+2 -1
View File
@@ -322,6 +322,7 @@ impl Model {
Procedure::ExplicitDynamic(_) => "is dynamic",
Procedure::HeatTransfer { transient: Some(_) } => "is transient heat transfer",
Procedure::Static {}
| Procedure::Buckle { .. }
| Procedure::NonlinearStatic(_)
| Procedure::Fluid { .. }
| Procedure::HeatTransfer { transient: None }
@@ -486,7 +487,7 @@ impl Model {
}
}
match &s.procedure {
Procedure::Static {} | Procedure::NonlinearStatic(_) => {
Procedure::Static {} | Procedure::NonlinearStatic(_) | Procedure::Buckle { .. } => {
if s.loads.is_empty() && !prescribes_motion {
d.warning(&subject, "nothing loads the model in this step");
}
+18 -1
View File
@@ -55,6 +55,14 @@ pub enum Procedure {
#[serde(default, skip_serializing_if = "Option::is_none")]
max_hz: Option<f64>,
},
/// Linear buckling: the load factors λ at which the step's loads (and prescribed motions),
/// applied as a linear preload, make K + λ K_σ singular, and their mode shapes; the lowest
/// `modes` positive ones, or those nearest above `shift` when one is given.
Buckle {
modes: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
shift: Option<f64>,
},
/// Transient dynamics by explicit time integration up to `end_time`.
ExplicitDynamic(ExplicitDynamic),
/// Time-dependent incompressible flow in a fluid domain, from rest (or the domain's initial
@@ -546,6 +554,7 @@ impl Procedure {
match self {
Procedure::Static {} => "static",
Procedure::Frequency { .. } => "frequency",
Procedure::Buckle { .. } => "buckle",
Procedure::ExplicitDynamic(_) => "explicit_dynamic",
Procedure::HeatTransfer { .. } => "heat_transfer",
Procedure::CoupledTemperatureDisplacement {} => "coupled_temperature_displacement",
@@ -618,6 +627,14 @@ impl Procedure {
if let Procedure::HeatTransfer { transient: Some(t) } = self {
return t.validate();
}
if let Procedure::Buckle { modes, shift } = *self {
if !(1..=MAX_MODES).contains(&modes) {
return Err(Error::Invalid(format!("a buckling step extracts 1 to {MAX_MODES} modes, got {modes}")));
}
if let Some(s) = shift {
finite("buckling shift", s)?;
}
}
if let Procedure::Frequency { modes, min_hz, max_hz } = *self {
if !(1..=MAX_MODES).contains(&modes) {
return Err(Error::Invalid(format!("a frequency step extracts 1 to {MAX_MODES} modes, got {modes}")));
@@ -640,7 +657,7 @@ impl Procedure {
pub fn default_output(&self) -> BTreeSet<OutputVariable> {
match self {
Procedure::Static {} | Procedure::NonlinearStatic(_) => OutputVariable::ALL.into_iter().collect(),
Procedure::Frequency { .. } => [OutputVariable::Displacement].into_iter().collect(),
Procedure::Frequency { .. } | Procedure::Buckle { .. } => [OutputVariable::Displacement].into_iter().collect(),
Procedure::ExplicitDynamic(_) => [OutputVariable::Displacement, OutputVariable::Stress].into_iter().collect(),
// Velocity, pressure and wall forces, always.
Procedure::Fluid { .. } => BTreeSet::new(),