ui: one number or one per axis for conductivity, expansion and point mass

A PerAxis field kind: one value for every direction, or Per axis ticked
for one per axis. Thermal conductivity and expansion and the point mass
use it instead of a JSON box (#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:38:26 -04:00
1 parent b18195ff89
commit e4d1f6c7da
4 files changed
+106 -9

No files matched your search

+10 -7
View File
@@ -186,18 +186,19 @@ const HYPERELASTIC: Field = variants(
.help("A large-strain rubber model (nonlinear steps), built on the isotropic constants.")
.hint("large strain in nonlinear steps, from the isotropic constants");
/// The material axes 1, 2, 3 (the global X, Y, Z until material orientations land).
const AXES_123: &[&str] = &["1", "2", "3"];
const THERMAL: Field = group(
"thermal",
"Thermal",
&[
json("conductivity", "Conductivity")
.help("Thermal conductivity, one value or one per axis. Steel ≈ 50 W/(m·K), i.e. 50 in mm-t-s-N.")
.hint("k or [k1, k2, k3]"),
per_axis("conductivity", "Conductivity", D::Conductivity, Some(0.0), AXES_123)
.help("Thermal conductivity, one value or one per material axis. Steel ≈ 50 W/(m·K), i.e. 50 in mm-t-s-N."),
positive("specific_heat", "Specific heat", D::SpecificHeat)
.help("Heat that raises a unit mass by one degree. Steel ≈ 460 J/(kg·K), 4.6e8 in mm-t-s."),
json("expansion", "Expansion")
.help("Thermal expansion coefficient, one value or one per axis, in 1/K. Steel ≈ 1.2e-5, aluminium ≈ 2.3e-5.")
.hint("α or [α1, α2, α3], 1/K"),
per_axis("expansion", "Expansion", D::Expansion, None, AXES_123)
.help("Thermal expansion coefficient, one value or one per material axis. Steel ≈ 1.2e-5, aluminium ≈ 2.3e-5."),
num("reference_temperature", "Reference temperature", D::Temperature).help("The temperature at which there is no thermal strain.").def("0"),
],
)
@@ -371,7 +372,9 @@ pub const COUPLING: &[Field] = &[
pub const MASS: &[Field] = &[
NAME,
TARGET,
positive("mass", "Mass", D::Mass).help("A lumped mass at each node of the target.").hint("per node; [mx, my, mz] in JSON"),
per_axis("mass", "Mass", D::Mass, Some(0.0), XYZ)
.help("A lumped mass at each node of the target: one value, or one per direction X, Y, Z.")
.hint("per node"),
vector("inertia", "Inertia", D::Inertia, &["I11", "I22", "I33", "I12", "I13", "I23"])
.help("Rotary inertia about the reference point: three moments and three products.")
.hint("about the node; needs a reference point"),
+53 -1
View File
@@ -112,6 +112,13 @@ pub enum Kind {
dim: Dim,
min: Option<f64>,
},
/// One number for every direction, or one per axis (`labels`, e.g. the material axes
/// 1, 2, 3), each at least `min`.
PerAxis {
dim: Dim,
min: Option<f64>,
labels: &'static [&'static str],
},
/// Any JSON: what the schema does not describe.
Json,
}
@@ -390,7 +397,11 @@ impl Field {
/// Its physical dimension, for a number or a vector of numbers.
pub fn dim(&self) -> Option<Dim> {
match self.kind {
Kind::Number { dim, .. } | Kind::Vector { dim, .. } | Kind::Points { dim, .. } | Kind::NumberOrAuto { dim, .. } => Some(dim),
Kind::Number { dim, .. }
| Kind::Vector { dim, .. }
| Kind::Points { dim, .. }
| Kind::NumberOrAuto { dim, .. }
| Kind::PerAxis { dim, .. } => Some(dim),
_ => None,
}
}
@@ -509,6 +520,10 @@ pub const fn multi(key: &'static str, label: &'static str, options: &'static [(&
pub const fn number_or_auto(key: &'static str, label: &'static str, dim: Dim, min: Option<f64>) -> Field {
field(key, label, Kind::NumberOrAuto { dim, min })
}
/// One number at least `min`, or one per axis of `labels`.
pub const fn per_axis(key: &'static str, label: &'static str, dim: Dim, min: Option<f64>, labels: &'static [&'static str]) -> Field {
field(key, label, Kind::PerAxis { dim, min, labels })
}
pub const fn json(key: &'static str, label: &'static str) -> Field {
field(key, label, Kind::Json)
}
@@ -1045,6 +1060,18 @@ fn value_problem(f: &'static Field, v: &Value, path: &str, out: &mut Vec<Problem
(_, Some(_)) => Some(ProblemKind::Below(min.unwrap_or(0.0))),
_ => Some(ProblemKind::Invalid("a number or \"auto\"".into())),
},
Kind::PerAxis { min, labels, .. } => {
let values = match v {
Value::Array(a) if a.len() == labels.len() => a.iter().map(|x| x.as_f64().filter(|x| x.is_finite())).collect::<Option<Vec<_>>>(),
Value::Array(_) => None,
_ => v.as_f64().filter(|x| x.is_finite()).map(|x| vec![x]),
};
match values {
Some(xs) if xs.iter().all(|&x| min.is_none_or(|lo| x >= lo)) => None,
Some(_) => Some(ProblemKind::Below(min.unwrap_or(0.0))),
None => Some(ProblemKind::Invalid(format!("a number, or {} numbers (one per axis)", labels.len()))),
}
}
Kind::Rows { fields, max } => match v.as_array() {
Some(a) if a.len() <= max => {
for (i, row) in a.iter().enumerate() {
@@ -1172,6 +1199,18 @@ fn field_schema(f: &Field, depth: usize) -> Value {
}
json!({"oneOf": [n, {"const": "auto"}]})
}
Kind::PerAxis { dim, min, labels } => {
let mut n = json!({"type": "number"});
if let Some(lo) = min {
n["minimum"] = json!(lo);
}
let mut s =
json!({"oneOf": [n.clone(), {"type": "array", "items": n, "minItems": labels.len(), "maxItems": labels.len(), "x-labels": labels}]});
if dim != Dim::None {
s["x-dimension"] = json!(format!("{dim:?}").to_lowercase());
}
s
}
_ => json!({}),
};
s["title"] = json!(f.label);
@@ -1270,6 +1309,19 @@ mod tests {
assert_eq!(s["properties"]["path"]["items"]["x-columns"], json!(["fraction", "factor"]));
}
/// A per-axis field takes one number or one per axis, each at least its minimum.
#[test]
fn per_axis_takes_one_number_or_one_per_axis() {
const F: &[Field] = &[per_axis("k", "K", Dim::Conductivity, Some(0.0), &["1", "2", "3"])];
assert!(problems(F, &json!({"k": 50})).is_empty());
assert!(problems(F, &json!({"k": [50, 20, 20.5]})).is_empty());
for bad in [json!({"k": -1}), json!({"k": [1, 2]}), json!({"k": [1, 2, -3]}), json!({"k": "x"}), json!({"k": [1, 2, "3"]})] {
assert_eq!(problems(F, &bad).len(), 1, "{bad}");
}
let s = json_schema(F);
assert_eq!(s["properties"]["k"]["oneOf"][1]["maxItems"], 3);
}
#[test]
fn prefill_switch_and_check() {
let p = prefill(LOADISH, &json!({}));