forked from 1b-agent/fieldcraft-upstream
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:
15 files changed
+1922
-3
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user