diff --git a/crates/engine/src/catalogue.rs b/crates/engine/src/catalogue.rs index a149666..da3399e 100644 --- a/crates/engine/src/catalogue.rs +++ b/crates/engine/src/catalogue.rs @@ -627,7 +627,7 @@ pub static CATALOGUE: &[Capability] = &[ ), ready_more("results.reactions", "Reactions", Results, "Query", "Reaction force and moment sums per set", &["results.export.csv"]), soon("results.free_body", "Free Body", Results, "Query", "M9", "Free-body forces across a cut"), - soon("results.xy", "XY Plot", Results, "Plots", "M9", "Plot a quantity against time or another"), + ready("results.xy", "XY Plot", Results, "Plots", "Plot a quantity against the load factor, time or mode"), soon("results.path", "Path Plot", Results, "Plots", "M9", "Plot a quantity along a path"), soon("results.cut", "Cut", Results, "Plots", "M9", "Section cuts through the model"), soon("results.display_group", "Display Group", Results, "Plots", "M9", "Show and hide parts of the model"), diff --git a/crates/engine/src/cmd/forms.rs b/crates/engine/src/cmd/forms.rs index 00fd2a7..70bbf10 100644 --- a/crates/engine/src/cmd/forms.rs +++ b/crates/engine/src/cmd/forms.rs @@ -1923,6 +1923,18 @@ pub const EXPORT_CSV: &[Field] = &[ int("mode", "Mode", 1, 1_000_000).help("Probe: the mode to read, for frequency and buckling steps."), int("frame", "Frame", 0, 1_000_000_000).help("Probe: the frame to read, for explicit and transient steps."), ]; +pub const RESULTS_XY: &[Field] = &[ + SOLVED, + json("y", "Quantity") + .help("What to plot: a nodal field at a node, an explicit energy or history, a history node's quantity, or \"frequency\".") + .hint(r#"{"field": "displacement_z", "node": 21}"#) + .req(), + choice("x", "Against", &[("auto", "The step's own axis"), ("load_factor", "Load factor"), ("time", "Time"), ("mode", "Mode")]) + .help("The x axis: the load factor (nonlinear static), time (explicit, heat) or mode number (frequency).") + .def("\"auto\""), + int("every", "Keep one in", 1, 1_000_000_000).help("Keep one point in this many; by default enough to fit 100000 points."), + path("path", "CSV file", true).help("Also write the series as a two-column CSV."), +]; pub const EXPORT_VTU: &[Field] = &[ path("path", "File", true).help("A .vtu file for one state, or a .pvd collection for every frame or mode.").req(), SOLVED, diff --git a/crates/engine/src/cmd/mod.rs b/crates/engine/src/cmd/mod.rs index 52161d0..db5a0af 100644 --- a/crates/engine/src/cmd/mod.rs +++ b/crates/engine/src/cmd/mod.rs @@ -35,6 +35,7 @@ mod symmetry; mod tags; pub mod update; pub(crate) mod view; +pub mod xy; use crate::schema::ProblemKind; use fieldcraft_model::MAX_LABEL; @@ -186,6 +187,7 @@ pub static ALL: &[CommandSpec] = &[ results::EXPORT_CSV, results::EXPORT_VTU, results::CLEAR, + xy::XY, diagnostics::CRASH_REPORTS, diagnostics::CRASH_REPORT, diagnostics::CRASH_REPORTS_DELETE, diff --git a/crates/engine/src/cmd/results.rs b/crates/engine/src/cmd/results.rs index 0514717..627b692 100644 --- a/crates/engine/src/cmd/results.rs +++ b/crates/engine/src/cmd/results.rs @@ -112,7 +112,7 @@ fn solved_list(s: &Session) -> String { /// The steps a command works on. #[derive(Clone, Copy, PartialEq)] -enum Want { +pub(super) enum Want { Any, Frequency, /// Steps with frames: explicit dynamics and nonlinear static (one per converged increment). @@ -138,7 +138,7 @@ impl Want { /// The step a command works on: `step`, else the step shown, else the first solved one /// (current results first) of the kind it wants. -fn step_for(s: &Session, p: &Value, want: Want) -> Result { +pub(super) fn step_for(s: &Session, p: &Value, want: Want) -> Result { if let Some(name) = opt::(p, "step")? { if s.latest_result(&name).is_none() { return Err(EngineError::Params(format!("step `{name}` has no result; {}", solved_list(s)))); @@ -660,13 +660,13 @@ fn export_csv(s: &mut Session, p: &Value) -> Result { } /// A table of cells (a number, text, or empty) under a header, written as CSV. -struct Table { - header: Vec, - rows: Vec>, +pub(super) struct Table { + pub(super) header: Vec, + pub(super) rows: Vec>, } #[derive(Clone, Debug, PartialEq)] -enum Cell { +pub(super) enum Cell { Number(f64), Text(String), Empty, @@ -675,7 +675,7 @@ enum Cell { impl Table { /// RFC 4180: comma-separated, CRLF line ends, a field with a comma, quote or line break in /// quotes (its quotes doubled); numbers in Rust's shortest round-trip form. - fn csv(&self) -> String { + pub(super) fn csv(&self) -> String { let field = |s: &str| if s.contains([',', '"', '\r', '\n']) { format!("\"{}\"", s.replace('"', "\"\"")) } else { s.to_string() }; let mut out = String::new(); let line = |cells: Vec, out: &mut String| { diff --git a/crates/engine/src/cmd/xy.rs b/crates/engine/src/cmd/xy.rs new file mode 100644 index 0000000..38608d4 --- /dev/null +++ b/crates/engine/src/cmd/xy.rs @@ -0,0 +1,490 @@ +//! `results.xy`: one quantity of a solved step against its natural axis — a nodal field per +//! frame against the load factor (nonlinear static) or time (explicit, transient heat), a mode +//! shape's value or the frequencies against the mode number, and an explicit step's histories +//! (energies, momentum, constraint force, history nodes) against time. Read-only; with `path` +//! the series is also written as a two-column CSV. + +use serde_json::{Value, json}; + +use super::opt; +use super::results::{Cell, Table, Want, step_for}; +use crate::requirement::Requirement; +use crate::results::{Field, Kind, Pick, Quantity, field_values, with_unit}; +use crate::schema::ProblemKind; +use crate::{CommandKind, CommandSpec, EngineError, Result, Session, StepResult}; + +pub const XY: CommandSpec = CommandSpec { + id: "results.xy", + label: "XY Plot", + params: r#"{"step": "Load", "y": {"field": "displacement_z", "node": 21}, "x": "auto", "every": 1, "path": "tip.csv"} — a quantity of a solved step against its natural axis: `x` "load_factor" (nonlinear static), "time" (explicit, transient heat), "mode" (frequency and buckling) or "auto" (the step's own, the default). `y` is {"field": , "node":