ui: nonlinear load path as a table of fraction and factor

Kind::Points gains its columns, so a list of number rows other than
[x, y, z] gets the same typed editor; the load path uses it (#449).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-16 01:34:38 -04:00
1 parent 1c18a463e7
commit 61f0d42ec4
4 files changed
+50 -18

No files matched your search

+3 -3
View File
@@ -661,9 +661,9 @@ const PROCEDURE: Field = variants(
flag("line_search", "Line search")
.help("Scale each Newton correction to reduce the residual; helps with hard convergence.")
.def("false"),
json("load_path", "Load path")
.help("How the load factor varies through the step, as JSON pairs of step fraction and factor; a linear ramp when empty.")
.hint("[[fraction, factor], …]; a ramp when empty"),
table("load_path", "Load path", D::None, &["fraction", "factor"], fieldcraft_model::MAX_LOAD_PATH_POINTS)
.help("How the load factor varies through the step: rows of step fraction (0 to 1) and load factor; a linear ramp when empty.")
.hint("a ramp when empty"),
group(
"imperfection",
"Imperfection",
+31 -7
View File
@@ -97,9 +97,11 @@ pub enum Kind {
},
/// A list of mesh labels, nodes or elements (at most [`MAX_LABELS`]).
Labels(LabelOf),
/// A list of points `[x, y, z]`, at most `max`.
/// A list of points `[x, y, z]`, or of number rows with these `columns` (a load path's
/// `[fraction, factor]` pairs), at most `max`.
Points {
dim: Dim,
columns: &'static [&'static str],
max: usize,
},
/// Any of these values, as a list: (the value as JSON text, label), e.g. `("1", "UX")` or
@@ -489,9 +491,15 @@ pub const fn rows(key: &'static str, label: &'static str, fields: &'static [Fiel
pub const fn labels(key: &'static str, label: &'static str, of: LabelOf) -> Field {
field(key, label, Kind::Labels(of))
}
/// The columns of a point, `[x, y, z]`.
pub const XYZ: &[&str] = &["x", "y", "z"];
/// A list of points `[x, y, z]`.
pub const fn points(key: &'static str, label: &'static str, dim: Dim, max: usize) -> Field {
field(key, label, Kind::Points { dim, max })
field(key, label, Kind::Points { dim, columns: XYZ, max })
}
/// A table: a list of number rows with these columns.
pub const fn table(key: &'static str, label: &'static str, dim: Dim, columns: &'static [&'static str], max: usize) -> Field {
field(key, label, Kind::Points { dim, columns, max })
}
/// Any of `options` (each a value as JSON text, and a label), as a list.
pub const fn multi(key: &'static str, label: &'static str, options: &'static [(&'static str, &'static str)]) -> Field {
@@ -1016,14 +1024,16 @@ fn value_problem(f: &'static Field, v: &Value, path: &str, out: &mut Vec<Problem
Some(a) if a.len() <= MAX_LABELS && a.iter().all(|x| x.as_u64().is_some_and(|n| n >= 1 && n <= u64::from(u32::MAX))) => None,
_ => Some(ProblemKind::NotAList { max: MAX_LABELS, what: "labels" }),
},
Kind::Points { max, .. } => match v.as_array() {
Kind::Points { columns, max, .. } => match v.as_array() {
Some(a)
if a.len() <= max
&& a.iter().all(|p| p.as_array().is_some_and(|c| c.len() == 3 && c.iter().all(|x| x.as_f64().is_some_and(f64::is_finite)))) =>
&& a.iter().all(|p| {
p.as_array().is_some_and(|c| c.len() == columns.len() && c.iter().all(|x| x.as_f64().is_some_and(f64::is_finite)))
}) =>
{
None
}
_ => Some(ProblemKind::NotAList { max, what: "points [x, y, z]" }),
_ => Some(ProblemKind::NotAList { max, what: if columns == XYZ { "points [x, y, z]" } else { "rows of numbers" } }),
},
Kind::Multi(options) => match v.as_array() {
Some(a) if a.iter().all(|x| options.iter().any(|(o, _)| multi_value(o) == *x)) => None,
@@ -1144,8 +1154,9 @@ fn field_schema(f: &Field, depth: usize) -> Value {
Kind::Labels(of) => {
json!({"type": "array", "items": {"type": "integer", "minimum": 1, "maximum": u32::MAX}, "maxItems": MAX_LABELS, "x-labels": of.label()})
}
Kind::Points { dim, max } => {
let mut item = json!({"type": "array", "items": {"type": "number"}, "minItems": 3, "maxItems": 3});
Kind::Points { dim, columns, max } => {
let n = columns.len();
let mut item = json!({"type": "array", "items": {"type": "number"}, "minItems": n, "maxItems": n, "x-columns": columns});
if dim != Dim::None {
item["x-dimension"] = json!(format!("{dim:?}").to_lowercase());
}
@@ -1246,6 +1257,19 @@ mod tests {
assert_eq!(s["properties"]["scale"]["oneOf"][1]["const"], "auto");
}
/// A table is a list of number rows of its column count: a load path's [fraction, factor].
#[test]
fn table_rows_have_its_columns() {
const F: &[Field] = &[table("path", "Path", Dim::None, &["fraction", "factor"], 3)];
assert!(problems(F, &json!({"path": [[0, 0], [0.5, 1], [1, 1]]})).is_empty());
for bad in [json!({"path": [[0, 0, 0]]}), json!({"path": [[0]]}), json!({"path": [[0, 0], [0, 0], [0, 0], [1, 1]]})] {
assert_eq!(problems(F, &bad).len(), 1, "{bad}");
}
let s = json_schema(F);
assert_eq!(s["properties"]["path"]["items"]["minItems"], 2);
assert_eq!(s["properties"]["path"]["items"]["x-columns"], json!(["fraction", "factor"]));
}
#[test]
fn prefill_switch_and_check() {
let p = prefill(LOADISH, &json!({}));