forked from 1b-agent/fieldcraft-upstream
Explicit dynamics core: central differences, one-point hex8 with hourglass control, nodal-pressure tet4, solve.explicit (M10 X1) (#27)
* Model: explicit dynamic steps (end time, increment scale and cap, output intervals, initial velocities, rigid walls, hourglass and bulk viscosity controls); deck writers refuse them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * fem: uniform-strain hex8 and tet4 kinematics, Flanagan–Belytschko hourglass shape vectors, element frequency bound Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Explicit solver crate (L2): central differences with lumped mass, one-point hex8 with Flanagan–Belytschko hourglass control, Bonet–Burton tet4, bulk viscosity, rigid walls, energy account; wave and impact verification Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Engine: solve.explicit and solve.history commands, explicit results in the session and solve.probe; cantilever period against the modal solver Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Docs: explicit solver in the architecture, roadmap (M10 started), parity and literature (Part H) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Simplify: Model::select_step shared by both solvers, Procedure::sets/is_dynamic, fused per-increment passes without allocation, shared element tail, generic command caps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Catalogue: solve.explicit is ready; solve.history beside solve.probe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * Rebase onto the implicit couplings work: Procedure is no longer Copy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
33 files changed
+2757
-65
No files matched your search
+339
-14
@@ -1,11 +1,12 @@
|
||||
//! `solve.*`: run the model's steps with the implicit solver, keep the results in the session,
|
||||
//! and read them back.
|
||||
//! `solve.*`: run the model's steps with the implicit or the explicit solver, keep the results
|
||||
//! in the session, and read them back.
|
||||
|
||||
use modelcraft_solver::{FrequencyOptions, Limits, Location, MassKind, StepResult, solve_frequency, solve_static};
|
||||
use modelcraft_explicit::{ExplicitResult, solve_explicit};
|
||||
use modelcraft_solver::{FrequencyOptions, Limits, Location, MassKind, StepResult as ImplicitResult, solve_frequency, solve_static};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{labels, opt};
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
use super::{labels, opt, req};
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session, StepResult};
|
||||
|
||||
pub const STATIC: CommandSpec = CommandSpec {
|
||||
id: "solve.static",
|
||||
@@ -23,10 +24,26 @@ pub const FREQUENCY: CommandSpec = CommandSpec {
|
||||
run: run_frequency,
|
||||
};
|
||||
|
||||
pub const EXPLICIT: CommandSpec = CommandSpec {
|
||||
id: "solve.explicit",
|
||||
label: "Solve Explicit Dynamic Step",
|
||||
params: r#"{"step": "Impact", "max_dofs": 6000000, "max_increments": 1000000000, "max_output_values": 50000000} — transient response of an explicit dynamic step (the only one when `step` is left out) by central differences; keeps its histories and field frames in the session; the caps may only be lowered (the step's own increment cap applies too)"#,
|
||||
kind: CommandKind::Model,
|
||||
run: run_explicit,
|
||||
};
|
||||
|
||||
pub const HISTORY: CommandSpec = CommandSpec {
|
||||
id: "solve.history",
|
||||
label: "Result Histories",
|
||||
params: r#"{"step": "Impact", "every": 1} — the time histories of a solved explicit step: time, increment, energies, momentum, constraint force and the history nodes' displacement and velocity; `every` keeps one sample in so many (by default enough to return at most 10000)"#,
|
||||
kind: CommandKind::Model,
|
||||
run: history,
|
||||
};
|
||||
|
||||
pub const PROBE: CommandSpec = CommandSpec {
|
||||
id: "solve.probe",
|
||||
label: "Probe Results",
|
||||
params: r#"{"step": "Load", "nodes": [1, 2], "mode": 1} — the stored result at nodes: displacement, reaction, stress and von Mises (static), or the shape of mode `mode` (frequency, from 1); a coupled reference point's label gives its six displacements (translations, rotations) and reactions (forces, moments)"#,
|
||||
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)"#,
|
||||
kind: CommandKind::Model,
|
||||
run: probe,
|
||||
};
|
||||
@@ -34,14 +51,18 @@ pub const PROBE: CommandSpec = CommandSpec {
|
||||
/// Most nodes one probe lists.
|
||||
const MAX_PROBE: usize = 10_000;
|
||||
|
||||
/// Most history samples one `solve.history` returns.
|
||||
const MAX_HISTORY_SAMPLES: usize = 10_000;
|
||||
|
||||
/// An optional cap that may only lower the default.
|
||||
fn cap(p: &Value, key: &str, default: usize) -> Result<usize> {
|
||||
fn cap<T: Copy + TryInto<u64> + TryFrom<u64> + std::fmt::Display>(p: &Value, key: &str, default: T) -> Result<T> {
|
||||
let most: u64 = default.try_into().unwrap_or(u64::MAX);
|
||||
match p.get(key) {
|
||||
None => Ok(default),
|
||||
Some(v) => v
|
||||
.as_u64()
|
||||
.and_then(|n| usize::try_from(n).ok())
|
||||
.filter(|n| (1..=default).contains(n))
|
||||
.filter(|n| (1..=most).contains(n))
|
||||
.and_then(|n| T::try_from(n).ok())
|
||||
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a whole number from 1 to {default}"))),
|
||||
}
|
||||
}
|
||||
@@ -83,7 +104,7 @@ fn run_static(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})).collect::<Vec<_>>(),
|
||||
"warnings": r.warnings,
|
||||
});
|
||||
s.store_result(StepResult::Static(r));
|
||||
s.store_result(StepResult::Implicit(ImplicitResult::Static(r)));
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
@@ -107,21 +128,99 @@ fn run_frequency(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})).collect::<Vec<_>>(),
|
||||
"warnings": r.warnings,
|
||||
});
|
||||
s.store_result(StepResult::Frequency(r));
|
||||
s.store_result(StepResult::Implicit(ImplicitResult::Frequency(r)));
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn run_explicit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
use modelcraft_explicit::Limits as ExplicitLimits;
|
||||
let limits = ExplicitLimits {
|
||||
max_dofs: cap(p, "max_dofs", ExplicitLimits::MAX_DOFS)?,
|
||||
max_increments: cap(p, "max_increments", ExplicitLimits::MAX_INCREMENTS)?,
|
||||
max_output_values: cap(p, "max_output_values", ExplicitLimits::MAX_OUTPUT_VALUES)?,
|
||||
};
|
||||
let step = opt::<String>(p, "step")?;
|
||||
let r = solve_explicit(&s.model, step.as_deref(), &limits)?;
|
||||
let summary = explicit_summary(&r);
|
||||
s.store_result(StepResult::Explicit(r));
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn explicit_summary(r: &ExplicitResult) -> Value {
|
||||
json!({
|
||||
"step": r.step,
|
||||
"dofs": 3 * r.nodes.len(),
|
||||
"elements": r.elements.len(),
|
||||
"increments": r.increments,
|
||||
"time": r.time,
|
||||
"completed": r.completed,
|
||||
"initial_increment": r.initial_increment,
|
||||
"controlling_elements": r.controlling,
|
||||
"total_mass": r.total_mass,
|
||||
"energy": r.energy,
|
||||
"energy_balance": r.energy.relative_error(r.initial_kinetic),
|
||||
"frames": r.frames.len(),
|
||||
"history_samples": r.history.time.len(),
|
||||
"warnings": r.warnings,
|
||||
})
|
||||
}
|
||||
|
||||
fn history(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let step: String = req(p, "step")?;
|
||||
let result = s.result(&step).ok_or_else(|| EngineError::Params(format!("step `{step}` has no result; solve it first")))?;
|
||||
let StepResult::Explicit(r) = result.as_ref() else {
|
||||
return Err(EngineError::Params(format!("step `{step}` is not an explicit dynamic step; it has no histories")));
|
||||
};
|
||||
let h = &r.history;
|
||||
let samples = h.time.len();
|
||||
let every = match opt::<u64>(p, "every")? {
|
||||
Some(0) => return Err(EngineError::Params("`every` must be at least 1".into())),
|
||||
Some(k) => usize::try_from(k).unwrap_or(usize::MAX),
|
||||
None => samples.div_ceil(MAX_HISTORY_SAMPLES).max(1),
|
||||
};
|
||||
if samples.div_ceil(every) > MAX_HISTORY_SAMPLES {
|
||||
return Err(EngineError::Params(format!("{samples} samples in steps of {every} are more than {MAX_HISTORY_SAMPLES}; raise `every`")));
|
||||
}
|
||||
let pick = |v: &dyn Fn(usize) -> Value| -> Vec<Value> { (0..samples).step_by(every).map(v).collect() };
|
||||
let nodes: Vec<Value> = h
|
||||
.nodes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(k, label)| {
|
||||
json!({
|
||||
"node": label,
|
||||
"displacement": pick(&|i| json!(h.displacement.get(i).and_then(|d| d.get(k)))),
|
||||
"velocity": pick(&|i| json!(h.velocity.get(i).and_then(|d| d.get(k)))),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
Ok(json!({
|
||||
"step": step,
|
||||
"every": every,
|
||||
"time": pick(&|i| json!(h.time.get(i))),
|
||||
"increment": pick(&|i| json!(h.increment.get(i))),
|
||||
"energies": pick(&|i| json!(h.energies.get(i))),
|
||||
"momentum": pick(&|i| json!(h.momentum.get(i))),
|
||||
"constraint_force": pick(&|i| json!(h.constraint_force.get(i))),
|
||||
"nodes": nodes,
|
||||
}))
|
||||
}
|
||||
|
||||
fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let step = opt::<String>(p, "step")?.ok_or_else(|| EngineError::Params("`step` is required".into()))?;
|
||||
let result = s.result(&step).ok_or_else(|| EngineError::Params(format!("step `{step}` has no result; solve it first")))?;
|
||||
let nodes = labels(p, "nodes", MAX_PROBE)?;
|
||||
let result = match result.as_ref() {
|
||||
StepResult::Explicit(r) => return probe_frame(r, &step, &nodes, p),
|
||||
StepResult::Implicit(r) => r,
|
||||
};
|
||||
let locate = |label: u32| {
|
||||
result
|
||||
.locate(label)
|
||||
.ok_or_else(|| EngineError::Params(format!("node {label} has no result (it belongs to no solved element or coupled reference point)")))
|
||||
};
|
||||
let out: Vec<Value> = match result.as_ref() {
|
||||
StepResult::Static(r) => nodes
|
||||
let out: Vec<Value> = match result {
|
||||
ImplicitResult::Static(r) => nodes
|
||||
.iter()
|
||||
.map(|&l| {
|
||||
Ok(match locate(l)? {
|
||||
@@ -139,7 +238,7 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?,
|
||||
StepResult::Frequency(r) => {
|
||||
ImplicitResult::Frequency(r) => {
|
||||
let mode = opt::<usize>(p, "mode")?.unwrap_or(1);
|
||||
let m = mode
|
||||
.checked_sub(1)
|
||||
@@ -161,6 +260,25 @@ 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> {
|
||||
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 f.displacement.is_empty() {
|
||||
return Err(EngineError::Params(format!("step `{step}` does not output displacements")));
|
||||
}
|
||||
let out: Vec<Value> = nodes
|
||||
.iter()
|
||||
.map(|&l| {
|
||||
let i =
|
||||
r.nodes.binary_search(&l).map_err(|_| EngineError::Params(format!("node {l} has no result (it belongs to no solved element)")))?;
|
||||
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}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use super::*;
|
||||
@@ -306,4 +424,211 @@ pub(crate) mod tests {
|
||||
assert!(s.execute("solve.probe", &p).is_err(), "{p}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A cantilever L × 1 × 1 along x of `n[0]` × `n[1]` × `n[1]` hex8 elements, E = 1000, ν = 0,
|
||||
/// ρ = 1, clamped at x = 0; one node set per cross-section (`x0`, `x1`, …) and the tip's
|
||||
/// corner node `tip`; a frequency step `Modes` and the section `beam`.
|
||||
fn explicit_cantilever(s: &mut Session, length: f64, n: [usize; 2]) {
|
||||
use modelcraft_model::{ElementKind, Set};
|
||||
let [nx, ny] = n;
|
||||
let label = |i: usize, j: usize, k: usize| (1 + i + (nx + 1) * (j + (ny + 1) * k)) as u32;
|
||||
for k in 0..=ny {
|
||||
for j in 0..=ny {
|
||||
for i in 0..=nx {
|
||||
s.model.set_node(label(i, j, k), [length * i as f64 / nx as f64, j as f64 / ny as f64, k as f64 / ny as f64]).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
let corners = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
|
||||
let mut e = 1;
|
||||
for k in 0..ny {
|
||||
for j in 0..ny {
|
||||
for i in 0..nx {
|
||||
s.model.set_element(e, ElementKind::Hex8, corners.iter().map(|c| label(i + c[0], j + c[1], k + c[2])).collect()).unwrap();
|
||||
e += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
for i in 0..=nx {
|
||||
let nodes = (0..=ny).flat_map(|k| (0..=ny).map(move |j| label(i, j, k))).collect();
|
||||
s.model.add_set(&format!("x{i}"), Set { nodes, ..Set::default() }).unwrap();
|
||||
}
|
||||
s.model.add_set("tip", Set { nodes: vec![label(nx, 0, 0)], ..Set::default() }).unwrap();
|
||||
s.model.add_set("all", Set { elements: (1..e).collect(), ..Set::default() }).unwrap();
|
||||
let script = json!([
|
||||
{"command": "model.material.create", "params": {"name": "Mat", "elastic": {"type": "isotropic", "young": 1000, "poisson": 0}, "density": 1}},
|
||||
{"command": "model.section.create", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"incompatible_modes": true}}},
|
||||
{"command": "model.boundary_condition.create", "params": {"name": "clamp", "type": "fixed", "target": {"set": "x0"}}},
|
||||
{"command": "model.step.create", "params": {"name": "Modes", "procedure": {"type": "frequency", "modes": 1}, "boundary_conditions": ["clamp"]}},
|
||||
]);
|
||||
s.run_script(&script).unwrap();
|
||||
}
|
||||
|
||||
/// Upward zero crossings of a sampled signal, linearly interpolated.
|
||||
fn crossings(time: &[f64], x: &[f64]) -> Vec<f64> {
|
||||
let mut out = Vec::new();
|
||||
for k in 1..x.len() {
|
||||
let (a, b) = (x[k - 1], x[k]);
|
||||
if a < 0.0 && b >= 0.0 {
|
||||
out.push(time[k - 1] + (time[k] - time[k - 1]) * (-a) / (b - a));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Free vibration of a cantilever after an initial velocity in the shape of its static
|
||||
/// deflection: the period of the explicit run (one-point hex8) against the first natural
|
||||
/// frequency of the implicit modal solver (incompatible-mode hex8, lumped mass) on the same
|
||||
/// mesh. The one-point element's mean strain misses the linear variation of the bending
|
||||
/// strain through each element: with n elements through the depth it keeps 1 − 1/n² of the
|
||||
/// bending energy, so without hourglass stiffness (viscous control) the period is longer by
|
||||
/// (1 − 1/n²)^{−½} (3.3% for n = 4); the stiffness form restores part of it.
|
||||
#[test]
|
||||
fn explicit_cantilever_period_matches_the_modal_frequency() {
|
||||
let (length, n) = (10.0, [40usize, 4usize]);
|
||||
let mut s = Session::default();
|
||||
explicit_cantilever(&mut s, length, n);
|
||||
let modes = s.execute("solve.frequency", &json!({"mass": "lumped"})).unwrap();
|
||||
let period = 1.0 / modes["modes"][0]["frequency"].as_f64().unwrap();
|
||||
// Euler–Bernoulli: f₁ = 1.875² / (2π L²) √(E I / (ρ A)), I/A = 1/12.
|
||||
let euler = 2.0 * std::f64::consts::PI * length * length / 1.875104f64.powi(2) / (1000.0f64 / 12.0).sqrt();
|
||||
assert!((period / euler - 1.0).abs() < 0.01, "{period} vs {euler}");
|
||||
let section = json!({"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}});
|
||||
s.execute("model.section.edit", §ion).unwrap();
|
||||
// v(x) ∝ x²(3L − x): the static deflection of a tip load, close to the first mode.
|
||||
let velocities: Vec<Value> = (0..=n[0])
|
||||
.map(|i| {
|
||||
let x = length * i as f64 / n[0] as f64;
|
||||
json!({"set": format!("x{i}"), "velocity": [0, 0, 0.01 * x * x * (3.0 * length - x) / (2.0 * length.powi(3))]})
|
||||
})
|
||||
.collect();
|
||||
let one_point = period / (1.0 - 1.0 / (n[1] * n[1]) as f64).sqrt();
|
||||
let mut measured = Vec::new();
|
||||
for hourglass in ["viscous", "stiffness"] {
|
||||
let step = json!({
|
||||
"type": "explicit_dynamic", "end_time": 4.2 * period, "history_interval": period / 400.0, "output_interval": period,
|
||||
"history_nodes": "tip", "initial_velocities": velocities, "hourglass": {"type": hourglass}
|
||||
});
|
||||
let name = format!("Swing-{hourglass}");
|
||||
s.execute("model.step.create", &json!({"name": name, "procedure": step, "boundary_conditions": ["clamp"]})).unwrap();
|
||||
let r = s.execute("solve.explicit", &json!({"step": name})).unwrap();
|
||||
let h = s.execute("solve.history", &json!({"step": name})).unwrap();
|
||||
let time: Vec<f64> = h["time"].as_array().unwrap().iter().map(|t| t.as_f64().unwrap()).collect();
|
||||
let uz: Vec<f64> = h["nodes"][0]["displacement"].as_array().unwrap().iter().map(|d| d[2].as_f64().unwrap()).collect();
|
||||
// The tip starts at zero moving up: it crosses zero upwards once per period after that.
|
||||
let ups = crossings(&time, &uz);
|
||||
let t = (ups[ups.len() - 1] - ups[0]) / (ups.len() - 1) as f64;
|
||||
eprintln!(
|
||||
"cantilever {n:?}, {hourglass} hourglass: explicit period {t:.5} vs modal {period:.5} ({:+.3}%), one-point estimate {one_point:.5} ({:+.3}%), Euler–Bernoulli {euler:.5}; energy balance {:.1e}, {} increments",
|
||||
100.0 * (t / period - 1.0),
|
||||
100.0 * (t / one_point - 1.0),
|
||||
r["energy_balance"].as_f64().unwrap(),
|
||||
r["increments"]
|
||||
);
|
||||
assert!(ups.len() >= 4, "{ups:?}");
|
||||
assert!(r["energy_balance"].as_f64().unwrap() < 0.01, "{r}");
|
||||
assert!(r["warnings"].as_array().unwrap().iter().all(|w| w.as_str().unwrap().contains("unit system")), "{r}");
|
||||
measured.push(t);
|
||||
}
|
||||
// Viscous control adds no stiffness: the one-point estimate holds closely.
|
||||
assert!((measured[0] / one_point - 1.0).abs() < 0.005, "{measured:?} vs {one_point}");
|
||||
// The stiffness form lies between it and the modal period, and within 3% of the latter.
|
||||
assert!(measured[1] > period && measured[1] < measured[0], "{measured:?}");
|
||||
assert!((measured[1] / period - 1.0).abs() < 0.03, "{measured:?} vs {period}");
|
||||
}
|
||||
|
||||
/// A small impact model through commands: a hex8 bar moving onto a rigid wall.
|
||||
fn explicit_impact(s: &mut Session) {
|
||||
explicit_cantilever(s, 4.0, [8, 1]);
|
||||
let step = json!({
|
||||
"type": "explicit_dynamic", "end_time": 0.2, "history_interval": 0.002, "output_interval": 0.05, "history_nodes": "tip",
|
||||
"initial_velocities": [{"set": "all_nodes", "velocity": [-0.1, 0, 0]}],
|
||||
"rigid_walls": [{"set": "x0", "point": [0, 0, 0], "normal": [1, 0, 0]}]
|
||||
});
|
||||
let nodes: Vec<u32> = s.model.nodes().keys().copied().collect();
|
||||
let script = json!([
|
||||
{"command": "model.set.create", "params": {"name": "all_nodes", "nodes": nodes}},
|
||||
{"command": "model.section.edit", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
|
||||
{"command": "model.step.create", "params": {"name": "Impact", "procedure": step}},
|
||||
]);
|
||||
s.run_script(&script).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_results_are_kept_probed_and_guarded() {
|
||||
let mut s = Session::default();
|
||||
explicit_impact(&mut s);
|
||||
let journal = s.journal.len();
|
||||
let r = s.execute("solve.explicit", &json!({"step": "Impact"})).unwrap();
|
||||
assert_eq!((r["completed"].as_bool(), r["frames"].as_u64()), (Some(true), Some(5)), "{r}");
|
||||
// Eight elements, so eight controlling ones; on this coarse mesh the impact excites modes
|
||||
// near the stability limit, whose energy the integer-step account measures only roughly.
|
||||
assert_eq!(r["controlling_elements"].as_array().unwrap().len(), 8, "{r}");
|
||||
assert!(r["energy_balance"].as_f64().unwrap() < 0.1, "{r}");
|
||||
assert_eq!(s.journal.len(), journal, "solving does not change the model");
|
||||
let probe = s.execute("solve.probe", &json!({"step": "Impact", "nodes": [1, 9], "frame": 0})).unwrap();
|
||||
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 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}");
|
||||
assert_eq!(s.solved_steps().collect::<Vec<_>>(), ["Impact"]);
|
||||
assert!(s.execute("solve.history", &json!({"step": "Modes"})).is_err(), "not solved");
|
||||
|
||||
let hostile = [
|
||||
json!(null),
|
||||
json!({"step": 7}),
|
||||
json!({"step": "Nope"}),
|
||||
json!({"step": "Modes"}),
|
||||
json!({"step": "Impact", "max_dofs": 0}),
|
||||
json!({"step": "Impact", "max_dofs": 5}),
|
||||
json!({"step": "Impact", "max_increments": 0}),
|
||||
json!({"step": "Impact", "max_increments": 3}),
|
||||
json!({"step": "Impact", "max_increments": -1}),
|
||||
json!({"step": "Impact", "max_increments": 1e300}),
|
||||
json!({"step": "Impact", "max_output_values": 1}),
|
||||
json!({"step": "Impact", "every": 0}),
|
||||
json!({"step": "Impact", "every": -3}),
|
||||
json!({"step": "Impact", "every": 1e300}),
|
||||
json!({"step": "Impact", "every": u64::MAX}),
|
||||
json!({"step": "Impact", "nodes": [1], "frame": 99}),
|
||||
json!({"step": "Impact", "nodes": [1], "frame": -1}),
|
||||
json!({"step": "Impact", "nodes": [0, 4294967296u64]}),
|
||||
];
|
||||
for id in ["solve.explicit", "solve.history", "solve.probe"] {
|
||||
for p in &hostile {
|
||||
let mut t = s.clone();
|
||||
let before = t.model.clone();
|
||||
let results = t.solved_steps().count();
|
||||
if t.execute(id, p).is_err() {
|
||||
assert_eq!(t.model, before);
|
||||
assert_eq!(t.solved_steps().count(), results, "{id} {p}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Hostile steps are refused when they are defined; solving one that a later edit makes
|
||||
// impossible (a node behind its wall) is an error, not a panic.
|
||||
for bad in [
|
||||
json!({"type": "explicit_dynamic", "end_time": -1}),
|
||||
json!({"type": "explicit_dynamic", "end_time": 1, "dt_scale": 2}),
|
||||
json!({"type": "explicit_dynamic", "end_time": 1, "max_increments": 0}),
|
||||
json!({"type": "explicit_dynamic", "end_time": 1, "output_interval": 1e-300}),
|
||||
json!({"type": "explicit_dynamic", "end_time": 1e308, "history_interval": 1e-308}),
|
||||
json!({"type": "explicit_dynamic", "end_time": 1, "rigid_walls": [{"set": "x0", "point": [0, 0, 0], "normal": [0, 0, 0]}]}),
|
||||
json!({"type": "explicit_dynamic", "end_time": 1, "initial_velocities": [{"set": "nope", "velocity": [1, 0, 0]}]}),
|
||||
json!({"type": "explicit_dynamic", "end_time": 1, "hourglass": {"type": "magic"}}),
|
||||
] {
|
||||
assert!(s.execute("model.step.create", &json!({"name": "Bad", "procedure": bad})).is_err(), "{bad}");
|
||||
}
|
||||
let behind = json!({"type": "explicit_dynamic", "end_time": 0.1, "rigid_walls": [{"set": "x0", "point": [1, 0, 0], "normal": [1, 0, 0]}]});
|
||||
s.execute("model.step.create", &json!({"name": "Behind", "procedure": behind})).unwrap();
|
||||
assert!(s.execute("solve.explicit", &json!({"step": "Behind"})).unwrap_err().to_string().contains("behind"));
|
||||
let long = json!({"type": "explicit_dynamic", "end_time": 1e6});
|
||||
s.execute("model.step.create", &json!({"name": "Long", "procedure": long})).unwrap();
|
||||
assert!(s.execute("solve.explicit", &json!({"step": "Long"})).unwrap_err().to_string().contains("increments"));
|
||||
assert!(s.execute("model.set.delete", &json!({"name": "all_nodes"})).unwrap_err().to_string().contains("step `Impact`"));
|
||||
// Defining steps edited the model, so the impact result is stale.
|
||||
assert_eq!(s.solved_steps().count(), 0);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user