Abaqus input writer and deck.write (M3) (#22)

* decks: Abaqus input writer with golden decks, read-back, Jacobian and unsupported tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: deck.write command (format dispatch, atomic file write), docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* decks: format table, one resolve per write, direct buffer writes; engine: shared path parameter

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 17:27:44 -04:00
1 parent 2d99a8571c
commit a71f85eb58
15 files changed
+1922 -3

No files matched your search

+70
View File
@@ -2,9 +2,19 @@
//! keyword files. Each format is its own module (`abaqus`, `nastran`, `lsdyna`), written from the
//! vendor's public reference manual in our own words; examples and expected files are our own.
//! Writers read [`modelcraft_model::Model`] and its `resolve()` result, never another format.
//!
//! Writers produce text; saving it is the caller's business (the engine's `deck.write`), so the
//! crate stays free of the file system. [`FORMATS`] lists every writer by the name `deck.write`
//! takes.
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
pub mod abaqus;
use std::collections::{BTreeMap, BTreeSet};
use modelcraft_model::{Diagnostic, Load, Model, Resolved};
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum Error {
/// The model has something this format cannot express (named, with the format).
@@ -18,3 +28,63 @@ pub enum Error {
}
pub type Result<T> = std::result::Result<T, Error>;
/// A deck format.
pub struct Format {
/// The name commands use (`abaqus`, …).
pub name: &'static str,
/// The usual file extension, without the dot.
pub extension: &'static str,
writer: fn(&Model, &Resolved) -> Result<String>,
}
/// A written deck: its text and the model check's warnings.
#[derive(Debug, Clone, PartialEq)]
pub struct Deck {
pub text: String,
pub warnings: Vec<Diagnostic>,
}
impl Format {
/// Resolve the model once and write it in this format.
pub fn write(&self, model: &Model) -> Result<Deck> {
let resolved = model.resolve()?;
let text = (self.writer)(model, &resolved)?;
Ok(Deck { text, warnings: resolved.warnings })
}
}
/// Every deck format, one row per writer.
pub const FORMATS: &[Format] = &[Format { name: "abaqus", extension: "inp", writer: abaqus::write_resolved }];
/// The format called `name`.
pub fn format(name: &str) -> Option<&'static Format> {
FORMATS.iter().find(|f| f.name == name)
}
/// Fail with [`Error::Unsupported`] naming every item, or succeed when there are none. Writers
/// collect everything their format cannot express and report it at once.
pub(crate) fn unsupported(format: &'static str, items: BTreeSet<String>) -> Result<()> {
if items.is_empty() { Ok(()) } else { Err(Error::Unsupported { format, what: items.into_iter().collect::<Vec<_>>().join("; ") }) }
}
/// The concentrated loads of a set of active loads: the resolved nodal values of every force,
/// traction (degrees of freedom 1–3) and moment (4–6), summed per node and degree of freedom so
/// loads sharing a node add up; zero sums are left out.
pub fn concentrated_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIterator<Item = &'a String>) -> BTreeMap<(u32, u8), f64> {
let mut out: BTreeMap<(u32, u8), f64> = BTreeMap::new();
for name in active {
let first_dof = match model.load(name) {
Some(Load::Moment { .. }) => 4,
Some(Load::Force { .. } | Load::Traction { .. }) => 1,
_ => continue,
};
for (node, v) in r.nodal_loads.get(name).map(Vec::as_slice).unwrap_or_default() {
for (dof, c) in (first_dof..).zip(v) {
*out.entry((*node, dof)).or_default() += c;
}
}
}
out.retain(|_, v| *v != 0.0);
out
}