forked from 1b-agent/fieldcraft-upstream
Explicit X2: large deformation, J2 and Johnson-Cook plasticity, damage and erosion (M10)
- model: materials gain solver-neutral plastic (bilinear isotropic/kinematic, hardening table, Johnson-Cook with adiabatic heating), damage (Johnson-Cook D1-D5) and erosion (plastic strain); explicit steps gain nonlinear_geometry (default on) and stress_rate (jaumann | green_naghdi). - fem: rotation (Hughes-Winget, Flanagan-Taylor), plasticity (radial return, capped bracketed Newton for Johnson-Cook, damage; the update takes the mechanical strain increment so thermal strains can be subtracted), closed-form hex8 gradients for deformed configurations. - explicit: updated Lagrangian hex8/tet4, per-element material state, erosion with reported energy, plastic work in the energy account. - solver: warns that the linear solver uses the elastic constants of plastic materials (results hash-identical). - decks: Abaqus *PLASTIC (+ Johnson-Cook keywords), Nastran MATS1/TABLES1, LS-DYNA *MAT_PLASTIC_KINEMATIC / *MAT_PIECEWISE_LINEAR_PLASTICITY; what a format cannot express is reported as Unsupported. - engine: model.material.plasticity, model.material.damage (catalogue ready), explicit element probe, eroded elements in the solve summary. Step commits before the rebase onto the DOF framework: branch explicit-x2-steps. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
4c0fcc80ad
commit
50764def18
38 files changed
+2839
-254
No files matched your search
@@ -82,6 +82,21 @@
|
||||
//! materials outside solid sections (shell and beam material axes need orientations), and
|
||||
//! distributing couplings that leave a translation free.
|
||||
//!
|
||||
//! # Materials
|
||||
//!
|
||||
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC`, engineering constants
|
||||
//! `TYPE=ENGINEERING CONSTANTS`, density `*DENSITY`. Von Mises plasticity is `*PLASTIC`: a
|
||||
//! hardening table as its (yield stress, plastic strain) lines; bilinear isotropic hardening as
|
||||
//! two lines, the second at a plastic strain of [`BILINEAR_REACH`] (Abaqus holds the last yield
|
||||
//! stress beyond a table's end, so the line is written out further than any analysis strains);
|
||||
//! bilinear kinematic hardening as `HARDENING=KINEMATIC` with its yield stress at plastic strains
|
||||
//! 0 and 1 (Abaqus's linear kinematic model extends that line); Johnson–Cook as
|
||||
//! `HARDENING=JOHNSON COOK` (A, B, n, m, melting and transition temperature, the transition
|
||||
//! temperature being the model's room temperature) with `*RATE DEPENDENT, TYPE=JOHNSON COOK`
|
||||
//! (C, ε̇₀) when C is not zero, and adiabatic heating as `*SPECIFIC HEAT` and
|
||||
//! `*INELASTIC HEAT FRACTION`. Damage and erosion are failure models of Abaqus/Explicit, and this
|
||||
//! writer writes Abaqus/Standard procedures: they are reported, not written.
|
||||
//!
|
||||
//! Node order: the model stores elements in Abaqus order, so connectivity passes through; the
|
||||
//! model's face numbers are Abaqus's S1–S6 for solids, and SPOS/SNEG for shell faces 1/2.
|
||||
|
||||
@@ -89,14 +104,17 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use modelcraft_model::{
|
||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Integration, Load, Model, OutputVariable, Procedure, Resolved,
|
||||
Section, SectionKind, Step, Symmetry, Target,
|
||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Integration, Load, Model, OutputVariable, Plastic,
|
||||
Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||
};
|
||||
|
||||
use crate::{Error, Result, unsupported};
|
||||
|
||||
const FORMAT: &str = "Abaqus";
|
||||
|
||||
/// The plastic strain a bilinear isotropic hardening line is written out to (1000%).
|
||||
pub const BILINEAR_REACH: f64 = 10.0;
|
||||
|
||||
/// Longest line Abaqus reads.
|
||||
pub const MAX_LINE: usize = 256;
|
||||
|
||||
@@ -251,6 +269,13 @@ fn check_expressible(model: &Model) -> Result<()> {
|
||||
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
|
||||
bad.insert(format!("step `{}`: explicit dynamic steps (not written to Abaqus input yet)", step.name));
|
||||
}
|
||||
for (name, m) in model.materials() {
|
||||
if m.damage.is_some() || m.erosion.is_some() {
|
||||
bad.insert(format!(
|
||||
"material `{name}`: damage and erosion (Abaqus/Explicit failure models; this writer writes Abaqus/Standard procedures so far)"
|
||||
));
|
||||
}
|
||||
}
|
||||
for (name, c) in model.couplings() {
|
||||
if c.kind == CouplingKind::Distributing && !(1..=3).all(|d| c.dofs.contains(d)) {
|
||||
bad.insert(format!("coupling `{name}`: an Abaqus distributing coupling always couples translations 1-3"));
|
||||
@@ -621,6 +646,50 @@ impl Deck {
|
||||
self.line("*DENSITY");
|
||||
self.numbers_line("", &[rho])?;
|
||||
}
|
||||
if let Some(p) = &m.plastic {
|
||||
self.plastic(p)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Von Mises plasticity (see the module documentation for the mapping).
|
||||
fn plastic(&mut self, p: &Plastic) -> Result<()> {
|
||||
match p {
|
||||
Plastic::Bilinear { yield_stress, hardening_modulus, hardening } => {
|
||||
if *hardening_modulus == 0.0 {
|
||||
self.line("*PLASTIC");
|
||||
self.numbers_line("", &[*yield_stress, 0.0])?;
|
||||
} else if *hardening == Hardening::Kinematic {
|
||||
self.line("*PLASTIC, HARDENING=KINEMATIC");
|
||||
self.numbers_line("", &[*yield_stress, 0.0])?;
|
||||
self.numbers_line("", &[yield_stress + hardening_modulus, 1.0])?;
|
||||
} else {
|
||||
self.line("*PLASTIC");
|
||||
self.numbers_line("", &[*yield_stress, 0.0])?;
|
||||
self.numbers_line("", &[yield_stress + hardening_modulus * BILINEAR_REACH, BILINEAR_REACH])?;
|
||||
}
|
||||
}
|
||||
Plastic::Table { points } => {
|
||||
self.line("*PLASTIC");
|
||||
for point in points {
|
||||
self.numbers_line("", &[point.stress, point.strain])?;
|
||||
}
|
||||
}
|
||||
Plastic::JohnsonCook { a, b, n, c, m, reference_strain_rate, room_temperature, melt_temperature, specific_heat, taylor_quinney } => {
|
||||
self.line("*PLASTIC, HARDENING=JOHNSON COOK");
|
||||
self.numbers_line("", &[*a, *b, *n, *m, *melt_temperature, *room_temperature])?;
|
||||
if *c != 0.0 {
|
||||
self.line("*RATE DEPENDENT, TYPE=JOHNSON COOK");
|
||||
self.numbers_line("", &[*c, *reference_strain_rate])?;
|
||||
}
|
||||
if let Some(cp) = specific_heat {
|
||||
self.line("*SPECIFIC HEAT");
|
||||
self.numbers_line("", &[*cp])?;
|
||||
self.line("*INELASTIC HEAT FRACTION");
|
||||
self.numbers_line("", &[*taylor_quinney])?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
//! `*SECTION_BEAM` (rods as trusses) and the element formulation mapped from the section's
|
||||
//! ([`elements`]); `*HOURGLASS` where a reduced-integration formulation needs it;
|
||||
//! - `*MAT_ELASTIC` for isotropic and `*MAT_ORTHOTROPIC_ELASTIC` (global material axes) for
|
||||
//! orthotropic materials;
|
||||
//! orthotropic materials; von Mises plasticity as `*MAT_PLASTIC_KINEMATIC` (bilinear: the
|
||||
//! tangent modulus E·H/(E + H) of the uniaxial curve, β = 1 isotropic or 0 kinematic, the
|
||||
//! erosion plastic strain as its failure strain) or `*MAT_PIECEWISE_LINEAR_PLASTICITY` (a
|
||||
//! hardening table as a `*DEFINE_CURVE` of yield stress against plastic strain, the erosion
|
||||
//! plastic strain as its failure strain). Johnson–Cook plasticity and damage are reported: their
|
||||
//! LS-DYNA material needs an equation of state and belongs to explicit analyses, which this
|
||||
//! writer does not write yet;
|
||||
//! - `*ELEMENT_SOLID` (tetrahedra and wedges as degenerate hexahedra, 10-node tetrahedra),
|
||||
//! `*ELEMENT_SOLID_H20`, `*ELEMENT_SHELL` and `*ELEMENT_BEAM`;
|
||||
//! - `*SET_NODE_LIST` and `*SET_SEGMENT` for the sets boundary conditions, loads and couplings
|
||||
@@ -47,8 +53,8 @@ mod elements;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use modelcraft_model::{
|
||||
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Load, Model, OutputVariable, Procedure, Resolved, Section,
|
||||
SectionKind, Step, Target,
|
||||
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Hardening, Load, Material, Model, OutputVariable, Plastic,
|
||||
Procedure, Resolved, Section, SectionKind, Step, Target,
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
@@ -117,6 +123,8 @@ struct Plan<'a> {
|
||||
uniform: BTreeMap<&'a str, ([f64; 3], u32)>,
|
||||
/// The part set gravity acts on, when it acts on a set.
|
||||
gravity_parts: Option<Vec<u32>>,
|
||||
/// Material name → the curve id of its hardening table.
|
||||
curves: BTreeMap<&'a str, u32>,
|
||||
}
|
||||
|
||||
/// Write the model as an LS-DYNA keyword deck.
|
||||
@@ -304,13 +312,25 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
|
||||
None
|
||||
}
|
||||
};
|
||||
for m in materials.keys() {
|
||||
if model.material(m).is_some_and(|m| m.density.is_none()) {
|
||||
refused.insert(format!("material `{m}` has no density, and LS-DYNA requires a mass density on every material"));
|
||||
let mut curves = BTreeMap::new();
|
||||
for name in materials.keys() {
|
||||
let Some(m) = model.material(name) else { continue };
|
||||
if m.density.is_none() {
|
||||
refused.insert(format!("material `{name}` has no density, and LS-DYNA requires a mass density on every material"));
|
||||
}
|
||||
if matches!(m.plastic, Some(Plastic::JohnsonCook { .. })) || m.damage.is_some() {
|
||||
refused.insert(format!(
|
||||
"material `{name}`: Johnson–Cook plasticity and damage (LS-DYNA's Johnson–Cook material needs an equation of state and is for explicit analyses, which this writer does not write yet)"
|
||||
));
|
||||
}
|
||||
if matches!(m.plastic, Some(Plastic::Table { .. })) {
|
||||
// Curve ids after the steps' load ramps (ids 1 to the number of steps).
|
||||
let lcid = u32::try_from(model.steps().len() + 1 + curves.len()).map_err(|_| Error::Limit("too many curves".into()))?;
|
||||
curves.insert(*name, lcid);
|
||||
}
|
||||
}
|
||||
crate::unsupported(FORMAT, refused)?;
|
||||
Ok(Plan { parts, materials, hourglasses, sets, node_sets, segment_sets, uniform, gravity_parts })
|
||||
Ok(Plan { parts, materials, hourglasses, sets, node_sets, segment_sets, uniform, gravity_parts, curves })
|
||||
}
|
||||
|
||||
/// A 1-based id from a 0-based index.
|
||||
@@ -374,6 +394,7 @@ fn materials(d: &mut Deck, model: &Model, plan: &Plan) -> Result<()> {
|
||||
let m = model.material(name).ok_or_else(|| broken(format!("material `{name}`")))?;
|
||||
let rho = m.density.unwrap_or_default();
|
||||
match m.elastic {
|
||||
Some(Elastic::Isotropic { young, poisson }) if m.plastic.is_some() => plastic_material(d, name, mid, rho, young, poisson, m, plan)?,
|
||||
Some(Elastic::Isotropic { young, poisson }) => {
|
||||
d.line("*MAT_ELASTIC_TITLE");
|
||||
d.line(name);
|
||||
@@ -401,6 +422,56 @@ fn materials(d: &mut Deck, model: &Model, plan: &Plan) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The plastic materials: `*MAT_PLASTIC_KINEMATIC` or `*MAT_PIECEWISE_LINEAR_PLASTICITY` with its
|
||||
/// curve, in place of `*MAT_ELASTIC`.
|
||||
fn plastic_material(d: &mut Deck, name: &str, mid: u32, rho: f64, young: f64, poisson: f64, m: &Material, plan: &Plan) -> Result<()> {
|
||||
let fail = m.erosion.map_or(Field::Blank, |e| Field::from(e.plastic_strain));
|
||||
match &m.plastic {
|
||||
Some(Plastic::Bilinear { yield_stress, hardening_modulus, hardening }) => {
|
||||
d.line("*MAT_PLASTIC_KINEMATIC_TITLE");
|
||||
d.line(name);
|
||||
d.header(&["mid", "ro", "e", "pr", "sigy", "etan", "beta"]);
|
||||
let tangent = young * hardening_modulus / (young + hardening_modulus);
|
||||
let beta = if *hardening == Hardening::Kinematic { 0.0 } else { 1.0 };
|
||||
d.card([
|
||||
Field::from(mid),
|
||||
Field::from(rho),
|
||||
Field::from(young),
|
||||
Field::from(poisson),
|
||||
Field::from(*yield_stress),
|
||||
Field::from(tangent),
|
||||
Field::from(beta),
|
||||
]);
|
||||
d.header(&["src", "srp", "fs"]);
|
||||
d.card([Field::Blank, Field::Blank, fail]);
|
||||
}
|
||||
Some(Plastic::Table { points }) => {
|
||||
let lcid = plan.curves.get(name).copied().ok_or_else(|| broken(format!("material `{name}` has no curve")))?;
|
||||
d.line("*MAT_PIECEWISE_LINEAR_PLASTICITY_TITLE");
|
||||
d.line(name);
|
||||
d.header(&["mid", "ro", "e", "pr", "sigy", "etan", "fail"]);
|
||||
let first = m.plastic.as_ref().map_or(0.0, Plastic::initial_yield);
|
||||
d.card([Field::from(mid), Field::from(rho), Field::from(young), Field::from(poisson), Field::from(first), Field::Blank, fail]);
|
||||
d.header(&["c", "p", "lcss"]);
|
||||
d.card([Field::Blank, Field::Blank, Field::from(lcid)]);
|
||||
d.header(&["eps1"]);
|
||||
d.card([Field::Blank]);
|
||||
d.header(&["es1"]);
|
||||
d.card([Field::Blank]);
|
||||
d.line("*DEFINE_CURVE_TITLE");
|
||||
d.line(&format!("{name} hardening"));
|
||||
d.header(&["lcid"]);
|
||||
d.card([Field::from(lcid)]);
|
||||
d.header(&["a1", "o1"]);
|
||||
for p in points {
|
||||
d.card([Field::from(p.strain), Field::from(p.stress)]);
|
||||
}
|
||||
}
|
||||
_ => return Err(broken(format!("material `{name}`: a plasticity LS-DYNA cannot take"))),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parts(d: &mut Deck, plan: &Plan) -> Result<()> {
|
||||
for p in &plan.parts {
|
||||
match p.section.kind {
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
//! - the element formulation: `PSOLID`'s integration network and scheme (see [`solid_rule`]);
|
||||
//! formulations Nastran's linear elements have no counterpart for are reported, not
|
||||
//! approximated;
|
||||
//! - isotropic materials: `MAT1`; orthotropic engineering constants: `MAT9` (the stiffness matrix
|
||||
//! in the basic axes) for solids and `MAT8` for shells lying in planes of constant Z, with the
|
||||
//! material X axis tied to the basic X axis through a `CORD2R`;
|
||||
//! - isotropic materials: `MAT1`, with `MATS1` for von Mises plasticity (bilinear by its
|
||||
//! hardening slope, a hardening table as a `TABLES1` stress–strain curve; Johnson–Cook, damage
|
||||
//! and erosion are reported, not written); a linear solution reads the elastic entry alone;
|
||||
//! - orthotropic engineering constants: `MAT9` (the stiffness matrix in the basic axes) for
|
||||
//! solids and `MAT8` for shells lying in planes of constant Z, with the material X axis tied to
|
||||
//! the basic X axis through a `CORD2R`;
|
||||
//! - boundary conditions: per step, one constraint set of `SPC1` (held degrees of freedom) and
|
||||
//! `SPC` (prescribed values); in a modes step every constrained degree of freedom is held;
|
||||
//! - forces, moments and tractions: `FORCE` and `MOMENT` at the nodal values the model resolves,
|
||||
@@ -44,8 +47,8 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
use modelcraft_model::{
|
||||
BeamProfile, Coupling, CouplingKind, Dofs, Elastic, ElementKind, Formulation, Integration, Load, MAX_LABEL, Model, OutputVariable, Procedure,
|
||||
Resolved, SectionKind, Step,
|
||||
BeamProfile, Coupling, CouplingKind, Dofs, Elastic, ElementKind, Formulation, Hardening, Integration, Load, MAX_LABEL, Model, OutputVariable,
|
||||
Plastic, Procedure, Resolved, SectionKind, Step,
|
||||
};
|
||||
|
||||
use crate::{Error, Result};
|
||||
@@ -184,6 +187,8 @@ struct MaterialIds {
|
||||
solid: Option<u32>,
|
||||
/// `MAT8`, for orthotropic materials of shells.
|
||||
shell: Option<u32>,
|
||||
/// `TABLES1` of a tabulated plasticity's stress–strain curve.
|
||||
table: Option<u32>,
|
||||
}
|
||||
|
||||
/// Which element families use a material.
|
||||
@@ -307,7 +312,7 @@ impl<'m> Deck<'m> {
|
||||
}
|
||||
|
||||
fn plan(&mut self) -> Result<()> {
|
||||
self.plan_materials();
|
||||
self.plan_materials()?;
|
||||
self.plan_sections()?;
|
||||
self.plan_couplings()?;
|
||||
self.plan_constraints()?;
|
||||
@@ -321,7 +326,7 @@ impl<'m> Deck<'m> {
|
||||
}
|
||||
|
||||
/// Material ids, for the forms the sections need.
|
||||
fn plan_materials(&mut self) {
|
||||
fn plan_materials(&mut self) -> Result<()> {
|
||||
let model = self.model;
|
||||
let mut uses: BTreeMap<&str, MaterialUse> = BTreeMap::new();
|
||||
for s in model.sections().values() {
|
||||
@@ -353,8 +358,24 @@ impl<'m> Deck<'m> {
|
||||
}
|
||||
None => MaterialIds::default(),
|
||||
};
|
||||
let mut ids = ids;
|
||||
if let Some(m) = model.material(name) {
|
||||
match &m.plastic {
|
||||
Some(Plastic::JohnsonCook { .. }) => {
|
||||
self.problems
|
||||
.insert(format!("material `{name}`: Johnson–Cook plasticity (MATS1 has no rate- and temperature-dependent flow stress)"));
|
||||
}
|
||||
Some(Plastic::Table { .. }) => ids.table = Some(next_id(&mut self.next_sid)?),
|
||||
_ => {}
|
||||
}
|
||||
if m.damage.is_some() || m.erosion.is_some() {
|
||||
self.problems
|
||||
.insert(format!("material `{name}`: damage and erosion (Nastran's linear and implicit solutions have no element failure)"));
|
||||
}
|
||||
}
|
||||
self.materials.insert(name, ids);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// A new property for `elements`.
|
||||
@@ -706,6 +727,9 @@ impl<'m> Deck<'m> {
|
||||
}
|
||||
if let (Some(card), Some(mid)) = (card, ids.isotropic) {
|
||||
write_named(out, &format!("material {name}"), mid, card)?;
|
||||
if let (Some(plastic), Some(Elastic::Isotropic { young, .. })) = (&m.plastic, m.elastic) {
|
||||
plasticity(out, name, mid, young, plastic, ids.table)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -907,6 +931,29 @@ fn beam_dimensions(p: BeamProfile) -> (&'static str, Vec<f64>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// `MATS1` (von Mises yield, `YF` 1) of an isotropic material's plasticity: a bilinear law by its
|
||||
/// work-hardening slope `H` (stress against plastic strain) and initial yield stress `LIMIT1`,
|
||||
/// isotropic (`HR` 1) or kinematic (`HR` 2); a hardening table as a `TABLES1` stress–strain
|
||||
/// curve through the origin, with the total strain σ/E + ε_p of each point.
|
||||
fn plasticity(out: &mut String, name: &str, mid: u32, young: f64, plastic: &Plastic, table: Option<u32>) -> Result<()> {
|
||||
let what = format!("plasticity of {name}");
|
||||
match (plastic, table) {
|
||||
(Plastic::Bilinear { yield_stress, hardening_modulus, hardening }, _) => {
|
||||
let hr = if *hardening == Hardening::Kinematic { 2 } else { 1 };
|
||||
let card = Card::new("MATS1").int(mid).blank().text("PLASTIC").real(*hardening_modulus).int(1).int(hr).real(*yield_stress);
|
||||
write_named(out, &what, mid, card)
|
||||
}
|
||||
(Plastic::Table { points }, Some(tid)) => {
|
||||
write_named(out, &what, mid, Card::new("MATS1").int(mid).int(tid).text("PLASTIC").blank().int(1).int(1))?;
|
||||
let curve = std::iter::once([0.0, 0.0]).chain(points.iter().map(|p| [p.stress / young + p.strain, p.stress])).flatten();
|
||||
write_named(out, &format!("stress-strain curve of {name}"), tid, Card::new("TABLES1").int(tid).end_line().reals(curve).text("ENDT"))
|
||||
}
|
||||
(Plastic::Table { .. }, None) | (Plastic::JohnsonCook { .. }, _) => {
|
||||
Err(Error::Model(modelcraft_model::Error::Invalid(format!("material `{name}`: its plasticity was not planned for MATS1"))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An entry preceded by a comment naming what it holds (`material Steel: MAT1 1`).
|
||||
fn write_named(out: &mut String, what: &str, id: u32, card: Card) -> Result<()> {
|
||||
comment(out, &format!("{what}: {} {id}", card.name()));
|
||||
|
||||
Reference in new issue
Block a user