//! Solver decks: the model written in a solver's input format. use std::path::Path; use modelcraft_io::roots::Access; use serde_json::{Value, json}; use super::req; use crate::{CommandKind, CommandSpec, EngineError, Result, Session}; pub const WRITE: CommandSpec = CommandSpec { id: "deck.write", label: "Write Solver Deck", params: r#"{"format": "abaqus|nastran|lsdyna", "path": "part.inp", "steps": ["Load"]} — writes the model as a solver deck (formats: modelcraft_decks::FORMATS); `steps` (optional) writes only those steps, as a Nastran run needs (static or frequency steps, not both); LS-DYNA writes several steps as *CASE analyses (run it with CASE); fails naming every error of the model check and everything the format cannot express"#, kind: CommandKind::Session, run: write, }; fn write(s: &mut Session, p: &Value) -> Result { let name: &str = req(p, "format")?; let format = modelcraft_decks::format(name).ok_or_else(|| { let shown: String = name.chars().take(32).collect(); let known: Vec<&str> = modelcraft_decks::FORMATS.iter().map(|f| f.name).collect(); EngineError::Params(format!("unknown deck format `{shown}`; known: {}", known.join(", "))) })?; let path = super::file_param(s, p, "path", Access::Write)?; let steps: Option> = super::opt(p, "steps")?; let deck = format.write_steps(&s.model, steps.as_deref())?; modelcraft_io::fs::write_atomic(Path::new(&path), deck.text.as_bytes(), false)?; Ok(json!({"format": format.name, "path": path, "bytes": deck.text.len(), "warnings": deck.warnings})) } #[cfg(test)] mod tests { use super::*; #[test] fn writes_the_defined_model_and_refuses_what_it_cannot() { let dir = std::env::temp_dir().join(format!("modelcraft-deck-write-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("bar.inp"); let path_str = path.to_string_lossy().into_owned(); let mut s = Session::default(); crate::cmd::define::tests::defined_bar(&mut s); let journal = s.journal.len(); let out = s.execute("deck.write", &json!({"format": "abaqus", "path": path_str})).unwrap(); let written = std::fs::read_to_string(&path).unwrap(); assert_eq!(written, modelcraft_decks::abaqus::write(&s.model).unwrap()); assert_eq!(out["bytes"], written.len()); assert_eq!(out["warnings"].as_array().map(Vec::len), Some(1), "`press` is in no step: {out}"); assert!(written.contains("*ELEMENT, TYPE=C3D8I"), "{written}"); assert_eq!(s.journal.len(), journal, "writing a deck does not change the model"); assert!(std::fs::read_dir(&dir).unwrap().count() == 1, "no partial file is left behind"); // Unknown formats, missing parameters and models that are not ready fail without writing. let other = dir.join("other.inp").to_string_lossy().into_owned(); assert!(s.execute("deck.write", &json!({"format": "nope", "path": other})).unwrap_err().to_string().contains("known: abaqus")); assert!(s.execute("deck.write", &json!({"path": other})).is_err()); assert!(s.execute("deck.write", &json!({"format": "abaqus"})).is_err()); let mut empty = Session::default(); assert!(matches!(empty.execute("deck.write", &json!({"format": "abaqus", "path": other})), Err(EngineError::Deck(_)))); assert!(!dir.join("other.inp").exists()); assert!(matches!(s.execute("deck.write", &json!({"format": "abaqus", "path": dir.to_string_lossy()})), Err(EngineError::File(_)))); // Nastran: one run is static or frequency steps, so the bar's two steps need two decks. let bdf = dir.join("bar.bdf").to_string_lossy().into_owned(); let e = s.execute("deck.write", &json!({"format": "nastran", "path": bdf})).unwrap_err().to_string(); assert!(e.contains("not supported by Nastran") && e.contains("separate decks"), "{e}"); for (step, sol) in [("Bend", "SOL 101"), ("Modes", "SOL 103")] { s.execute("deck.write", &json!({"format": "nastran", "path": bdf, "steps": [step]})).unwrap(); let deck = std::fs::read_to_string(&bdf).unwrap(); assert!(deck.contains(sol) && deck.contains("RBE2") && deck.ends_with("ENDDATA\n"), "{deck}"); } assert_eq!(s.journal.len(), journal); for steps in [json!([]), json!(["Nope"]), json!("Bend"), json!([1, 2]), json!([["Bend"]])] { assert!(s.execute("deck.write", &json!({"format": "nastran", "path": bdf, "steps": steps})).is_err(), "{steps}"); } let _ = std::fs::remove_dir_all(&dir); } #[test] fn writes_lsdyna_keyword_decks() { let dir = std::env::temp_dir().join(format!("modelcraft-deck-lsdyna-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); let path = dir.join("bar.k"); let mut s = Session::default(); crate::cmd::define::tests::defined_bar(&mut s); let out = s.execute("deck.write", &json!({"format": "lsdyna", "path": path.to_string_lossy()})).unwrap(); let written = std::fs::read_to_string(&path).unwrap(); assert_eq!(written, modelcraft_decks::lsdyna::write(&s.model).unwrap().text); assert_eq!(out["format"], "lsdyna"); assert!(written.starts_with("*KEYWORD long=y\n") && written.contains("*CASE_BEGIN_2"), "two steps, two cases"); // One chosen step: a single analysis, no cases. s.execute("deck.write", &json!({"format": "lsdyna", "path": path.to_string_lossy(), "steps": ["Modes"]})).unwrap(); let one = std::fs::read_to_string(&path).unwrap(); assert!(one.contains("*CONTROL_IMPLICIT_EIGENVALUE") && !one.contains("*CASE_BEGIN") && !one.contains("*CONTROL_IMPLICIT_SOLUTION"), "{one}"); // What LS-DYNA cannot take is named and nothing is written. s.execute("model.load.create", &json!({"name": "heat", "type": "body_force", "set": "all", "vector": [0, 0, 1]})).unwrap(); s.execute("model.step.activate", &json!({"step": "Bend", "loads": ["heat"]})).unwrap(); let other = dir.join("heat.k"); let e = s.execute("deck.write", &json!({"format": "lsdyna", "path": other.to_string_lossy()})).unwrap_err().to_string(); assert!(e.contains("not supported by LS-DYNA") && e.contains("load `heat`"), "{e}"); assert!(!other.exists()); let _ = std::fs::remove_dir_all(&dir); } }