forked from 1b-agent/fieldcraft-upstream
Engine: the setup checklist (Session::progress), returned by model.check
The analysis stages in order (geometry, seeds & partitions, mesh, material, section, sets, boundary conditions, loads, step, solve, results), each done, next, todo, blocked or optional, with the command that does it. Built on the command requirements; Solve also reads the model check's first error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
fc85550534
commit
fe557054ef
3 files changed
+248
No files matched your search
@@ -57,11 +57,14 @@ fn units(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
fn check(s: &mut Session, _: &Value) -> Result<Value> {
|
fn check(s: &mut Session, _: &Value) -> Result<Value> {
|
||||||
let diagnostics = s.model.check();
|
let diagnostics = s.model.check();
|
||||||
let errors = diagnostics.iter().filter(|d| d.severity == Severity::Error).count();
|
let errors = diagnostics.iter().filter(|d| d.severity == Severity::Error).count();
|
||||||
|
let first_error = diagnostics.iter().find(|d| d.severity == Severity::Error).map(ToString::to_string);
|
||||||
|
let progress = crate::progress::to_json(&s.progress_given(first_error));
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"ready": errors == 0,
|
"ready": errors == 0,
|
||||||
"errors": errors,
|
"errors": errors,
|
||||||
"warnings": diagnostics.len() - errors,
|
"warnings": diagnostics.len() - errors,
|
||||||
"diagnostics": diagnostics,
|
"diagnostics": diagnostics,
|
||||||
|
"progress": progress,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ pub mod cmd;
|
|||||||
|
|
||||||
pub use cmd::analytic_surfaces;
|
pub use cmd::analytic_surfaces;
|
||||||
pub mod history;
|
pub mod history;
|
||||||
|
pub mod progress;
|
||||||
pub mod requirement;
|
pub mod requirement;
|
||||||
pub mod results;
|
pub mod results;
|
||||||
pub mod sample;
|
pub mod sample;
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
//! Where the model is in the analysis workflow: the setup checklist ([`Session::progress`]).
|
||||||
|
//!
|
||||||
|
//! The stages run in the usual order — geometry, preparation, mesh, material, section, sets,
|
||||||
|
//! boundary conditions, loads, step, solve, results — each with whether it is done, the next one
|
||||||
|
//! to do, still to do, blocked (its command cannot run yet) or optional, and the command that
|
||||||
|
//! provides it. It is a guide, never a gate: a script can define things in any order, and
|
||||||
|
//! [`crate::requirement`] still decides what each command needs.
|
||||||
|
//!
|
||||||
|
//! Each stage is a [`Requirement`] check (a count), and Solve also reads the model check's first
|
||||||
|
//! error, so the whole list costs one [`fieldcraft_model::Model::check`]: the desktop reads it
|
||||||
|
//! once per model change, and `model.check` returns it as `progress`.
|
||||||
|
|
||||||
|
use fieldcraft_model::{Procedure, Severity};
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::Session;
|
||||||
|
use crate::requirement::Requirement;
|
||||||
|
|
||||||
|
/// A step of the analysis workflow, in order.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Stage {
|
||||||
|
Geometry,
|
||||||
|
/// Seeds and partitions that steer the mesh (optional).
|
||||||
|
Prepare,
|
||||||
|
Mesh,
|
||||||
|
Material,
|
||||||
|
Section,
|
||||||
|
Sets,
|
||||||
|
BoundaryConditions,
|
||||||
|
Loads,
|
||||||
|
Step,
|
||||||
|
Solve,
|
||||||
|
Results,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stage {
|
||||||
|
pub const ALL: [Stage; 11] = [
|
||||||
|
Stage::Geometry,
|
||||||
|
Stage::Prepare,
|
||||||
|
Stage::Mesh,
|
||||||
|
Stage::Material,
|
||||||
|
Stage::Section,
|
||||||
|
Stage::Sets,
|
||||||
|
Stage::BoundaryConditions,
|
||||||
|
Stage::Loads,
|
||||||
|
Stage::Step,
|
||||||
|
Stage::Solve,
|
||||||
|
Stage::Results,
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Stage::Geometry => "Geometry",
|
||||||
|
Stage::Prepare => "Seeds & partitions",
|
||||||
|
Stage::Mesh => "Mesh",
|
||||||
|
Stage::Material => "Material",
|
||||||
|
Stage::Section => "Section",
|
||||||
|
Stage::Sets => "Sets",
|
||||||
|
Stage::BoundaryConditions => "Boundary conditions",
|
||||||
|
Stage::Loads => "Loads",
|
||||||
|
Stage::Step => "Step",
|
||||||
|
Stage::Solve => "Solve",
|
||||||
|
Stage::Results => "Results",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What it checks; its provider is the command the stage starts.
|
||||||
|
fn requirement(self) -> Requirement {
|
||||||
|
match self {
|
||||||
|
Stage::Geometry => Requirement::Geometry,
|
||||||
|
Stage::Prepare => Requirement::Seed,
|
||||||
|
Stage::Mesh => Requirement::Analysable,
|
||||||
|
Stage::Material => Requirement::Material,
|
||||||
|
Stage::Section => Requirement::Section,
|
||||||
|
Stage::Sets => Requirement::Set,
|
||||||
|
Stage::BoundaryConditions => Requirement::BoundaryCondition,
|
||||||
|
Stage::Loads => Requirement::Load,
|
||||||
|
Stage::Step => Requirement::Step,
|
||||||
|
Stage::Solve => Requirement::Result,
|
||||||
|
Stage::Results => Requirement::ResultsShown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a stage is.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum Status {
|
||||||
|
Done,
|
||||||
|
/// The first stage still to do.
|
||||||
|
Next,
|
||||||
|
/// Still to do after the next one; its command can already run.
|
||||||
|
Todo,
|
||||||
|
/// Its command cannot run yet (`detail` says why).
|
||||||
|
Blocked,
|
||||||
|
/// Not done, and not needed for this model.
|
||||||
|
Optional,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One row of the checklist.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize)]
|
||||||
|
pub struct StageState {
|
||||||
|
pub stage: Stage,
|
||||||
|
pub label: &'static str,
|
||||||
|
pub status: Status,
|
||||||
|
/// The command that does it.
|
||||||
|
pub command: &'static str,
|
||||||
|
/// Where that command is on the toolbar ("Properties › Material").
|
||||||
|
pub place: String,
|
||||||
|
/// Why it is blocked: what its command misses, or the model check's first error.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Session {
|
||||||
|
/// The setup checklist for the current model (one model check).
|
||||||
|
pub fn progress(&self) -> Vec<StageState> {
|
||||||
|
let first_error = self.model.check().into_iter().find(|d| d.severity == Severity::Error);
|
||||||
|
self.progress_given(first_error.map(|d| d.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The checklist, given the model check's first error (`model.check` has run it already).
|
||||||
|
pub(crate) fn progress_given(&self, first_error: Option<String>) -> Vec<StageState> {
|
||||||
|
let m = &self.model;
|
||||||
|
let analysable = Requirement::Analysable.met(self);
|
||||||
|
// Models without elements (point masses, fluid domains) need no material or section.
|
||||||
|
let elementless = analysable && m.elements().is_empty();
|
||||||
|
let modal_only = !m.steps().is_empty() && m.steps().iter().all(|st| matches!(st.procedure, Procedure::Frequency { .. }));
|
||||||
|
let mut next_given = false;
|
||||||
|
Stage::ALL
|
||||||
|
.into_iter()
|
||||||
|
.map(|stage| {
|
||||||
|
let r = stage.requirement();
|
||||||
|
let done = r.met(self) || (stage == Stage::Geometry && analysable);
|
||||||
|
let optional = match stage {
|
||||||
|
Stage::Prepare => true,
|
||||||
|
Stage::Material | Stage::Section => elementless,
|
||||||
|
Stage::Loads => modal_only,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
let command = r.provider();
|
||||||
|
let blocked = match crate::find_command(command).map(|spec| self.availability(spec)) {
|
||||||
|
Some(Err(u)) => Some(u.lines()),
|
||||||
|
_ if stage == Stage::Solve => first_error.clone(),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
let (status, detail) = if done {
|
||||||
|
(Status::Done, None)
|
||||||
|
} else if optional {
|
||||||
|
(Status::Optional, None)
|
||||||
|
} else if blocked.is_some() {
|
||||||
|
next_given = true;
|
||||||
|
(Status::Blocked, blocked)
|
||||||
|
} else if next_given {
|
||||||
|
(Status::Todo, None)
|
||||||
|
} else {
|
||||||
|
next_given = true;
|
||||||
|
(Status::Next, None)
|
||||||
|
};
|
||||||
|
StageState { stage, label: stage.label(), status, command, place: r.place(), detail }
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The checklist as JSON, with the next stage's command first for agents:
|
||||||
|
/// `{"next": {"stage", "command"} | null, "stages": [...]}`.
|
||||||
|
pub fn to_json(stages: &[StageState]) -> Value {
|
||||||
|
let next = stages.iter().find(|s| matches!(s.status, Status::Next | Status::Blocked));
|
||||||
|
json!({
|
||||||
|
"next": next.map(|s| json!({"stage": s.stage, "command": s.command, "status": s.status, "detail": s.detail})),
|
||||||
|
"stages": stages,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use Status::*;
|
||||||
|
|
||||||
|
fn statuses(s: &Session) -> Vec<(Stage, Status)> {
|
||||||
|
s.progress().into_iter().map(|st| (st.stage, st.status)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn status(s: &Session, stage: Stage) -> Status {
|
||||||
|
statuses(s).into_iter().find(|(st, _)| *st == stage).map(|(_, st)| st).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_model_starts_with_geometry() {
|
||||||
|
let s = Session::default();
|
||||||
|
let p = s.progress();
|
||||||
|
assert_eq!(p.len(), Stage::ALL.len());
|
||||||
|
assert_eq!((p[0].stage, p[0].status, p[0].command), (Stage::Geometry, Next, "geometry.open"));
|
||||||
|
assert_eq!(status(&s, Stage::Prepare), Optional);
|
||||||
|
// Meshing needs geometry; solving needs a mesh.
|
||||||
|
assert_eq!(status(&s, Stage::Mesh), Blocked);
|
||||||
|
assert!(p[2].detail.as_deref().is_some_and(|d| d.contains("geometry")));
|
||||||
|
assert_eq!(status(&s, Stage::Material), Todo);
|
||||||
|
assert_eq!(status(&s, Stage::Solve), Blocked);
|
||||||
|
assert_eq!(statuses(&s).iter().filter(|(_, st)| *st == Next).count(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_meshed_model_asks_for_its_material_next() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
let cube = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
|
||||||
|
s.execute("mesh.block", &json!({"body": "B", "kind": "hex8", "corners": cube, "divisions": [1, 1, 1]})).unwrap();
|
||||||
|
assert_eq!(status(&s, Stage::Geometry), Done);
|
||||||
|
assert_eq!(status(&s, Stage::Mesh), Done);
|
||||||
|
assert_eq!(status(&s, Stage::Material), Next);
|
||||||
|
assert_eq!(status(&s, Stage::Section), Blocked);
|
||||||
|
assert_eq!(status(&s, Stage::Step), Todo);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_defined_model_is_ready_to_solve_then_to_show_results() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
crate::cmd::solve::tests::cantilever(&mut s);
|
||||||
|
let before = statuses(&s);
|
||||||
|
for (stage, st) in &before[..9] {
|
||||||
|
assert!(matches!(st, Done | Optional), "{stage:?} {st:?}");
|
||||||
|
}
|
||||||
|
assert_eq!(status(&s, Stage::Solve), Next);
|
||||||
|
assert_eq!(to_json(&s.progress())["next"]["command"], "solve.run");
|
||||||
|
s.execute("solve.run", &json!({})).unwrap();
|
||||||
|
// A solve shows its results.
|
||||||
|
assert!(statuses(&s).iter().all(|(_, st)| matches!(st, Done | Optional)));
|
||||||
|
assert!(to_json(&s.progress())["next"].is_null());
|
||||||
|
s.execute("results.clear", &json!({})).unwrap();
|
||||||
|
assert_eq!(status(&s, Stage::Solve), Done);
|
||||||
|
assert_eq!((status(&s, Stage::Results), to_json(&s.progress())["next"]["command"].clone()), (Next, json!("results.contour")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn model_check_returns_the_checklist() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
let v = s.execute("model.check", &json!({})).unwrap();
|
||||||
|
assert_eq!(v["progress"]["next"]["command"], "geometry.open");
|
||||||
|
assert_eq!(v["progress"]["stages"].as_array().map(Vec::len), Some(Stage::ALL.len()));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user