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 <noreply@anthropic.com> 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
@@ -80,7 +80,7 @@ definition_commands!(
|
||||
Material,
|
||||
"material",
|
||||
"Material",
|
||||
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density)} — in the model's unit system (model.units), never converted"#,
|
||||
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density), "plastic": {…} (see model.material.plasticity), "damage": {…}, "erosion": {…} (see model.material.damage)} — in the model's unit system (model.units), never converted"#,
|
||||
add_material,
|
||||
replace_material,
|
||||
remove_material,
|
||||
@@ -89,6 +89,51 @@ definition_commands!(
|
||||
MATERIAL_DELETE
|
||||
);
|
||||
|
||||
/// A copy of material `name` with `change` applied, put back through the model's validation.
|
||||
fn change_material(s: &mut Session, p: &Value, change: impl FnOnce(&mut Material, &Value) -> Result<()>) -> Result<Value> {
|
||||
let n = name(p)?;
|
||||
let mut m = s.model.material(n).cloned().ok_or_else(|| EngineError::Params(format!("material `{n}` does not exist")))?;
|
||||
change(&mut m, p)?;
|
||||
s.model.replace_material(n, m)?;
|
||||
Ok(json!({"name": n}))
|
||||
}
|
||||
|
||||
pub const MATERIAL_PLASTICITY: CommandSpec = CommandSpec {
|
||||
id: "model.material.plasticity",
|
||||
label: "Plasticity",
|
||||
params: r#"{"name": "Steel", "plastic": {"type": "bilinear", "yield_stress": 250, "hardening_modulus": 1000, "hardening": "isotropic|kinematic"} | {"type": "table", "points": [{"strain": 0, "stress": 250}, {"strain": 0.1, "stress": 300}]} | {"type": "johnson_cook", "a": 792, "b": 510, "n": 0.26, "c": 0.014, "m": 1.03, "reference_strain_rate": 1, "room_temperature": 294, "melt_temperature": 1793, "specific_heat": 4.77e8, "taylor_quinney": 0.9} | null} — von Mises plasticity of an isotropic material (null removes it); explicit dynamics uses it, the linear solver the elastic constants alone"#,
|
||||
kind: CommandKind::Model,
|
||||
run: |s: &mut Session, p: &Value| {
|
||||
change_material(s, p, |m, p| {
|
||||
m.plastic = req(p, "plastic")?;
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
pub const MATERIAL_DAMAGE: CommandSpec = CommandSpec {
|
||||
id: "model.material.damage",
|
||||
label: "Damage",
|
||||
params: r#"{"name": "Steel", "damage": {"type": "johnson_cook", "d1": 0.05, "d2": 3.44, "d3": -2.12, "d4": 0.002, "d5": 0.61} | null, "erosion": {"plastic_strain": 1.0} | null} — damage to failure and element erosion of a plastic material (each key optional; null removes it); a failed element is eroded in explicit dynamics"#,
|
||||
kind: CommandKind::Model,
|
||||
run: |s: &mut Session, p: &Value| {
|
||||
change_material(s, p, |m, p| {
|
||||
// Absent leaves a field as it is; null removes it.
|
||||
let (damage, erosion) = (opt(p, "damage")?, opt(p, "erosion")?);
|
||||
if damage.is_none() && erosion.is_none() {
|
||||
return Err(EngineError::Params("give `damage`, `erosion` or both".into()));
|
||||
}
|
||||
if let Some(d) = damage {
|
||||
m.damage = d;
|
||||
}
|
||||
if let Some(e) = erosion {
|
||||
m.erosion = e;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
},
|
||||
};
|
||||
|
||||
definition_commands!(
|
||||
"section",
|
||||
Section,
|
||||
@@ -423,4 +468,35 @@ pub(crate) mod tests {
|
||||
let e = s.execute("model.set.resolve", &json!({"name": "root"})).unwrap_err().to_string();
|
||||
assert!(e.contains("face left of body Bar is not on the mesh (the mesh carries no geometry tags)"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plasticity_and_damage_edit_a_material() {
|
||||
let mut s = Session::default();
|
||||
defined_bar(&mut s);
|
||||
let bilinear = json!({"type": "bilinear", "yield_stress": 250, "hardening_modulus": 1000});
|
||||
s.execute("model.material.plasticity", &json!({"name": "Steel", "plastic": bilinear})).unwrap();
|
||||
assert!(s.model.material("Steel").unwrap().plastic.is_some());
|
||||
// Damage needs plasticity; D4 and D5 need Johnson–Cook: refused, the model unchanged.
|
||||
let before = s.model.clone();
|
||||
let jc_damage = json!({"type": "johnson_cook", "d1": 0.05, "d2": 3.44, "d3": -2.12, "d4": 0.002, "d5": 0.61});
|
||||
assert!(s.execute("model.material.damage", &json!({"name": "Steel", "damage": jc_damage})).is_err());
|
||||
assert_eq!(s.model, before);
|
||||
s.execute(
|
||||
"model.material.damage",
|
||||
&json!({"name": "Steel", "damage": {"type": "johnson_cook", "d1": 0.3, "d2": 0, "d3": 0}, "erosion": {"plastic_strain": 0.5}}),
|
||||
)
|
||||
.unwrap();
|
||||
let m = s.model.material("Steel").unwrap();
|
||||
assert!(m.damage.is_some() && m.erosion.is_some());
|
||||
s.execute("model.material.damage", &json!({"name": "Steel", "erosion": null})).unwrap();
|
||||
assert!(s.model.material("Steel").unwrap().erosion.is_none() && s.model.material("Steel").unwrap().damage.is_some());
|
||||
// Removing plasticity under damage is refused.
|
||||
assert!(s.execute("model.material.plasticity", &json!({"name": "Steel", "plastic": null})).is_err());
|
||||
for bad in
|
||||
[json!({"name": "Steel"}), json!({"name": "Nope", "plastic": null}), json!({"name": "Steel", "plastic": {"type": "table", "points": []}})]
|
||||
{
|
||||
assert!(s.execute("model.material.plasticity", &bad).is_err(), "{bad}");
|
||||
}
|
||||
assert!(s.execute("model.material.damage", &json!({"name": "Steel"})).is_err());
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,8 @@ pub static ALL: &[CommandSpec] = &[
|
||||
define::MATERIAL_CREATE,
|
||||
define::MATERIAL_EDIT,
|
||||
define::MATERIAL_DELETE,
|
||||
define::MATERIAL_PLASTICITY,
|
||||
define::MATERIAL_DAMAGE,
|
||||
define::SECTION_CREATE,
|
||||
define::SECTION_EDIT,
|
||||
define::SECTION_DELETE,
|
||||
|
||||
@@ -54,7 +54,7 @@ pub const FLUID: CommandSpec = CommandSpec {
|
||||
pub const PROBE: CommandSpec = CommandSpec {
|
||||
id: "solve.probe",
|
||||
label: "Probe Results",
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1, "frame": 0} — the stored result at nodes: displacement, reaction, stress and von Mises (static), the shape of mode `mode` (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default); a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1, "frame": 0} — the stored result at nodes: displacement, reaction, stress and von Mises (static), the shape of mode `mode` (frequency, from 1), or displacement and velocity in field frame `frame` (explicit, from 0; the last by default) at `nodes` and, with `elements` (explicit), each element's stress, von Mises stress, strain, equivalent plastic strain, damage, temperature and whether it has eroded; a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments). Fluid steps: {"step": "Flow", "points": [[x, y, z]], "frame": 10 (default the last)} gives velocity and pressure in the cells at the points; {"step": "Flow", "forces": ["x_min", "F2:top", "Body1/F2:top"]} gives those walls' force histories"#,
|
||||
kind: CommandKind::Model,
|
||||
run: probe,
|
||||
};
|
||||
@@ -175,6 +175,7 @@ fn explicit_summary(r: &ExplicitResult) -> Value {
|
||||
"energy_balance": r.energy.relative_error(r.initial_kinetic),
|
||||
"frames": r.frames.len(),
|
||||
"history_samples": r.history.time.len(),
|
||||
"eroded": r.eroded,
|
||||
"warnings": r.warnings,
|
||||
})
|
||||
}
|
||||
@@ -344,15 +345,15 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
format!("step `{step}` has no result; solve it first")
|
||||
})
|
||||
})?;
|
||||
if let StepResult::Fluid(r) = result.as_ref() {
|
||||
return probe_fluid(r, p);
|
||||
}
|
||||
let nodes = labels(p, "nodes", MAX_PROBE)?;
|
||||
let result = match result.as_ref() {
|
||||
StepResult::Explicit(r) => return probe_frame(r, &step, &nodes, p),
|
||||
StepResult::Explicit(r) => {
|
||||
let list = |key| if p.get(key).is_some() { labels(p, key, MAX_PROBE) } else { Ok(Vec::new()) };
|
||||
return probe_frame(r, &step, &list("nodes")?, &list("elements")?, p);
|
||||
}
|
||||
StepResult::Fluid(r) => return probe_fluid(r, p),
|
||||
StepResult::Implicit(r) => r,
|
||||
};
|
||||
let nodes = labels(p, "nodes", MAX_PROBE)?;
|
||||
let locate = |label: u32| {
|
||||
result
|
||||
.locate(label)
|
||||
@@ -399,11 +400,38 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(json!({"step": step, "nodes": out}))
|
||||
}
|
||||
|
||||
/// Displacement and velocity at nodes in one field frame of an explicit result.
|
||||
fn probe_frame(r: &ExplicitResult, step: &str, nodes: &[u32], p: &Value) -> Result<Value> {
|
||||
/// Displacement and velocity at nodes, and element results, in one field frame of an explicit
|
||||
/// result.
|
||||
fn probe_frame(r: &ExplicitResult, step: &str, nodes: &[u32], elements: &[u32], p: &Value) -> Result<Value> {
|
||||
let last = r.frames.len().saturating_sub(1);
|
||||
let k = opt::<usize>(p, "frame")?.unwrap_or(last);
|
||||
let f = r.frames.get(k).ok_or_else(|| EngineError::Params(format!("`frame` must be from 0 to {last}")))?;
|
||||
if nodes.is_empty() && elements.is_empty() {
|
||||
return Err(EngineError::Params("give `nodes`, `elements` or both".into()));
|
||||
}
|
||||
if !elements.is_empty() && f.stress.is_empty() {
|
||||
return Err(EngineError::Params(format!("step `{step}` does not output stresses")));
|
||||
}
|
||||
let element_values: Vec<Value> = elements
|
||||
.iter()
|
||||
.map(|&l| {
|
||||
let i =
|
||||
r.elements.binary_search(&l).map_err(|_| EngineError::Params(format!("element {l} has no result (it is not a solved element)")))?;
|
||||
Ok(json!({
|
||||
"element": l,
|
||||
"stress": f.stress.get(i),
|
||||
"von_mises": f.stress.get(i).map(modelcraft_fem::von_mises),
|
||||
"strain": f.strain.get(i),
|
||||
"plastic_strain": f.plastic_strain.get(i),
|
||||
"damage": f.damage.get(i),
|
||||
"temperature": f.temperature.get(i),
|
||||
"eroded": f.failed.get(i),
|
||||
}))
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
if nodes.is_empty() {
|
||||
return Ok(json!({"step": step, "frame": k, "time": f.time, "elements": element_values}));
|
||||
}
|
||||
if f.displacement.is_empty() {
|
||||
return Err(EngineError::Params(format!("step `{step}` does not output displacements")));
|
||||
}
|
||||
@@ -415,7 +443,7 @@ fn probe_frame(r: &ExplicitResult, step: &str, nodes: &[u32], p: &Value) -> Resu
|
||||
Ok(json!({"node": l, "displacement": f.displacement.get(i), "velocity": f.velocity.get(i)}))
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
Ok(json!({"step": step, "frame": k, "time": f.time, "nodes": out}))
|
||||
Ok(json!({"step": step, "frame": k, "time": f.time, "nodes": out, "elements": element_values}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -850,6 +878,11 @@ pub(crate) mod tests {
|
||||
assert_eq!(probe["nodes"][1]["velocity"][0].as_f64(), Some(-0.1), "{probe}");
|
||||
let last = s.execute("solve.probe", &json!({"step": "Impact", "nodes": [9]})).unwrap();
|
||||
assert_eq!(last["frame"], 4, "{last}");
|
||||
let el = s.execute("solve.probe", &json!({"step": "Impact", "elements": [1, 8]})).unwrap();
|
||||
let e = &el["elements"][0];
|
||||
assert!(e["stress"].as_array().unwrap().len() == 6 && e["von_mises"].as_f64().unwrap() >= 0.0 && e["eroded"] == false, "{el}");
|
||||
assert!(e["plastic_strain"].is_null(), "an elastic model has no plastic strain: {el}");
|
||||
assert_eq!(r["eroded"].as_array().map(Vec::len), Some(0), "{r}");
|
||||
let h = s.execute("solve.history", &json!({"step": "Impact", "every": 2})).unwrap();
|
||||
assert_eq!(h["time"].as_array().unwrap().len(), h["energies"].as_array().unwrap().len());
|
||||
assert!(h["nodes"][0]["velocity"].as_array().unwrap().len() > 5, "{h}");
|
||||
@@ -875,6 +908,11 @@ pub(crate) mod tests {
|
||||
json!({"step": "Impact", "nodes": [1], "frame": 99}),
|
||||
json!({"step": "Impact", "nodes": [1], "frame": -1}),
|
||||
json!({"step": "Impact", "nodes": [0, 4294967296u64]}),
|
||||
json!({"step": "Impact"}),
|
||||
json!({"step": "Impact", "elements": [999]}),
|
||||
json!({"step": "Impact", "elements": [0, -1, 1.5]}),
|
||||
json!({"step": "Impact", "elements": vec![1; 20_000]}),
|
||||
json!({"step": "Impact", "elements": "all", "nodes": [1]}),
|
||||
];
|
||||
for id in ["solve.explicit", "solve.history", "solve.probe"] {
|
||||
for p in &hostile {
|
||||
|
||||
Reference in new issue
Block a user