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:
authored and GitHub committed 2026-10-13 18:28:32 -04:00
1 parent 48039639cb
commit d45a2da5b5
33 files changed
+2757 -65

No files matched your search

+2 -25
View File
@@ -121,29 +121,6 @@ pub struct FrequencyOptions {
pub shift: Option<f64>,
}
/// The step named `name`, or the only step of the procedure when no name is given; it must be
/// of that procedure.
fn select_step<'m>(model: &'m Model, name: Option<&str>, wanted: fn(&Procedure) -> bool, what: &str) -> Result<&'m Step> {
let step = match name {
Some(n) => model.step(n).ok_or_else(|| Error::Invalid(format!("step `{n}` does not exist")))?,
None => {
let matching: Vec<&Step> = model.steps().iter().filter(|s| wanted(&s.procedure)).collect();
match matching.as_slice() {
[one] => *one,
[] => return Err(Error::Invalid(format!("the model has no {what} step"))),
many => {
let names: Vec<&str> = many.iter().map(|s| s.name.as_str()).collect();
return Err(Error::Invalid(format!("name the step: the model has {what} steps {}", names.join(", "))));
}
}
}
};
if !wanted(&step.procedure) {
return Err(Error::Invalid(format!("step `{}` is a {} step, not a {what} one", step.name, step.procedure.name())));
}
Ok(step)
}
/// The assembled matrices and the model's mass.
struct Assembled {
k: Csr,
@@ -218,7 +195,7 @@ fn budget(limits: &Limits, graph: &NodeGraph, matrices: usize) -> Budget {
/// Solve a linear static step: the step named `step`, or the model's only static step.
pub fn solve_static(model: &Model, step: Option<&str>, limits: &Limits) -> Result<StaticResult> {
let resolved = model.resolve()?;
let step = select_step(model, step, |p| matches!(p, Procedure::Static {}), "static")?;
let step = model.select_step(step, |p| matches!(p, Procedure::Static {}), "static")?;
let mesh = Mesh::new(model, &resolved, limits.max_dofs)?;
let mut warnings: Vec<String> = resolved.warnings.iter().map(ToString::to_string).collect();
let graph = NodeGraph::new(mesh.labels.len(), mesh.elements.iter().map(|e| e.nodes.as_slice()));
@@ -337,7 +314,7 @@ const TOLERANCE: f64 = 1e-9;
/// lowest natural modes within the step's frequency range.
pub fn solve_frequency(model: &Model, step: Option<&str>, options: &FrequencyOptions, limits: &Limits) -> Result<FrequencyResult> {
let resolved: Resolved = model.resolve()?;
let step = select_step(model, step, |p| matches!(p, Procedure::Frequency { .. }), "frequency")?;
let step = model.select_step(step, |p| matches!(p, Procedure::Frequency { .. }), "frequency")?;
let Procedure::Frequency { modes, min_hz, max_hz } = step.procedure else {
return Err(Error::Internal("frequency step".into()));
};