material: report exact missing condition fields (#1334)
Material-card validation now names the exact condition fields missing for each property (missing_condition_fields), alongside the existing unspecified_conditions. Co-authored-by: 0xlillith <0xlillith@users.noreply.github.com>
This commit is contained in:
1 parent
fd422a3fc1
commit
8b7fa2602e
4 files changed
+85
-6
No files matched your search
@@ -3,7 +3,7 @@
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::collections::BTreeSet;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use crate::{DocError, Result};
|
||||
|
||||
@@ -116,6 +116,10 @@ pub struct Validation {
|
||||
pub property_kinds: Vec<String>,
|
||||
pub isotropic_moduli: Option<IsotropicModuli>,
|
||||
pub unspecified_conditions: Vec<String>,
|
||||
/// Property kind -> absent condition fields under the existing family policy.
|
||||
/// Empty when reading an older report that did not include this detail.
|
||||
#[serde(default)]
|
||||
pub missing_condition_fields: BTreeMap<String, Vec<String>>,
|
||||
pub source_review_required: Vec<String>,
|
||||
pub analysis_solver_available: bool,
|
||||
pub rendering_properties_available: bool,
|
||||
@@ -256,6 +260,7 @@ impl MaterialCard {
|
||||
let mut kinds = BTreeSet::new();
|
||||
let mut used_sources = BTreeSet::new();
|
||||
let mut unspecified = Vec::new();
|
||||
let mut missing_condition_fields = BTreeMap::new();
|
||||
let mut isotropic_moduli = None;
|
||||
for property in &self.properties {
|
||||
let kind = property.data.kind();
|
||||
@@ -279,12 +284,20 @@ impl MaterialCard {
|
||||
if let Some(state) = &conditions.state {
|
||||
text(state, 1024, "conditions.state")?;
|
||||
}
|
||||
if conditions.temperature_k.is_none()
|
||||
|| conditions.state.is_none()
|
||||
|| (self.family == Family::Wood && conditions.moisture_dry_basis_kg_kg.is_none())
|
||||
|| (self.family == Family::Polymer && conditions.strain_rate_per_s.is_none())
|
||||
{
|
||||
let mut missing = Vec::new();
|
||||
for (absent, field) in [
|
||||
(conditions.temperature_k.is_none(), "temperature_k"),
|
||||
(conditions.state.is_none(), "state"),
|
||||
(self.family == Family::Wood && conditions.moisture_dry_basis_kg_kg.is_none(), "moisture_dry_basis_kg_kg"),
|
||||
(self.family == Family::Polymer && conditions.strain_rate_per_s.is_none(), "strain_rate_per_s"),
|
||||
] {
|
||||
if absent {
|
||||
missing.push(field.to_string());
|
||||
}
|
||||
}
|
||||
if !missing.is_empty() {
|
||||
unspecified.push(kind.to_string());
|
||||
missing_condition_fields.insert(kind.to_string(), missing);
|
||||
}
|
||||
if let Some(moduli) = property.data.validate()? {
|
||||
isotropic_moduli = Some(moduli);
|
||||
@@ -295,6 +308,7 @@ impl MaterialCard {
|
||||
property_kinds: kinds.into_iter().collect(),
|
||||
isotropic_moduli,
|
||||
unspecified_conditions: unspecified,
|
||||
missing_condition_fields,
|
||||
source_review_required: used_sources.into_iter().collect(),
|
||||
analysis_solver_available: false,
|
||||
rendering_properties_available: false,
|
||||
|
||||
@@ -132,6 +132,58 @@ fn conditions_missing_values_and_source_claims_remain_explicit() {
|
||||
assert_eq!(serde_json::from_value::<MaterialCard>(serde_json::to_value(&parsed).unwrap()).unwrap(), parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_condition_fields_follow_family_policy_without_inventing_values() {
|
||||
for (family, extra) in
|
||||
[("wood", Some("moisture_dry_basis_kg_kg")), ("polymer", Some("strain_rate_per_s")), ("glass", None), ("metal", None), ("other", None)]
|
||||
{
|
||||
let mut value = card(json!({"kind":"density","kg_m3":600.}));
|
||||
value["family"] = json!(family);
|
||||
let mut expected = vec!["temperature_k", "state"];
|
||||
expected.extend(extra);
|
||||
let result = validate_card(&value).unwrap();
|
||||
assert_eq!(result.missing_condition_fields, BTreeMap::from([("density".into(), expected.iter().map(|s| (*s).into()).collect())]));
|
||||
assert_eq!(result.unspecified_conditions, vec!["density"]);
|
||||
|
||||
// Explicit zero dry-basis moisture is known; no wet/dry conversion or default is inferred.
|
||||
value["properties"][0]["conditions"] =
|
||||
json!({"temperature_k":293.15,"state":"synthetic","moisture_dry_basis_kg_kg":0.,"strain_rate_per_s":1.});
|
||||
assert!(validate_card(&value).unwrap().missing_condition_fields.is_empty());
|
||||
for field in expected {
|
||||
let mut partial = value.clone();
|
||||
partial["properties"][0]["conditions"].as_object_mut().unwrap().remove(field);
|
||||
let result = validate_card(&partial).unwrap();
|
||||
assert_eq!(result.missing_condition_fields, BTreeMap::from([("density".into(), vec![field.into()])]));
|
||||
assert_eq!(result.unspecified_conditions, vec!["density"]);
|
||||
}
|
||||
value["properties"] = json!([]);
|
||||
let result = validate_card(&value).unwrap();
|
||||
assert!(result.missing_condition_fields.is_empty() && result.unspecified_conditions.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_condition_fields_are_per_property_and_reports_remain_backward_readable() {
|
||||
let mut value = card(json!({"kind":"density","kg_m3":600.}));
|
||||
value["properties"][0]["conditions"] = json!({"temperature_k":293.15,"state":"synthetic"});
|
||||
value["properties"].as_array_mut().unwrap().push(json!({"data":isotropic(12e9,0.25),"source_id":"test",
|
||||
"conditions":{"state":"synthetic","moisture_dry_basis_kg_kg":1.5}}));
|
||||
let before = value.clone();
|
||||
let result = validate_card(&value).unwrap();
|
||||
let expected = json!({"density":["moisture_dry_basis_kg_kg"],"elasticity":["temperature_k"]});
|
||||
let serialized = serde_json::to_value(&result).unwrap();
|
||||
assert_eq!(serialized["missing_condition_fields"], expected);
|
||||
assert_eq!(result.unspecified_conditions, vec!["density", "elasticity"]);
|
||||
assert_eq!(value, before);
|
||||
assert_eq!(serde_json::from_value::<Validation>(serialized.clone()).unwrap(), result);
|
||||
let mut old = serialized;
|
||||
old.as_object_mut().unwrap().remove("missing_condition_fields");
|
||||
let old: Validation = serde_json::from_value(old).unwrap();
|
||||
assert!(old.missing_condition_fields.is_empty());
|
||||
assert_eq!(old.unspecified_conditions, result.unspecified_conditions);
|
||||
assert_eq!(old.source_review_required, result.source_review_required);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thermal_properties_accept_negative_expansion_but_not_negative_conductivity() {
|
||||
assert!(validate_card(&card(json!({"kind":"thermal_expansion","per_k":[-1e-6,0.,2e-6],"axes":"material123"}))).is_ok());
|
||||
|
||||
@@ -57,6 +57,7 @@ mod tests {
|
||||
let response = s.execute("material.properties.directional", &json!({"card":card,"direction":[1,2,3]})).unwrap();
|
||||
assert_eq!(response["young_modulus_pa"], 12e9);
|
||||
assert_eq!(response["validation"]["source_review_required"], json!(["test"]));
|
||||
assert_eq!(response["validation"]["missing_condition_fields"], json!({"elasticity":["temperature_k","state"]}));
|
||||
for direction in [json!([0, 0, 0]), json!([1, 2]), json!([1, 2, 3, 4]), json!([true, 0, 0]), json!(["1", 0, 0]), Value::Null] {
|
||||
assert!(s.execute("material.properties.directional", &json!({"card":card,"direction":direction})).is_err());
|
||||
}
|
||||
@@ -76,6 +77,7 @@ mod tests {
|
||||
assert_eq!(result["validation"]["isotropic_moduli"], json!({"shear_pa":4.8e9,"bulk_pa":8e9}));
|
||||
assert_eq!(result["validation"]["source_review_required"], json!(["fixture"]));
|
||||
assert_eq!(result["validation"]["rendering_properties_available"], false);
|
||||
assert_eq!(result["validation"]["missing_condition_fields"], json!({"elasticity":["temperature_k","state"]}));
|
||||
card["properties"][0]["data"]["model"]["poisson_ratio"] = json!(0.6);
|
||||
assert!(s.execute("material.properties.validate", &json!({"card":card})).is_err());
|
||||
}
|
||||
@@ -91,6 +93,7 @@ mod tests {
|
||||
let card = json!({"schema_version":1,"designation":"Synthetic empty card","family":"other","sources":[],"properties":[]});
|
||||
let result = s.execute("material.properties.validate", &json!({"card":card})).unwrap();
|
||||
assert_eq!(result["validation"]["analysis_solver_available"], false);
|
||||
assert_eq!(result["validation"]["missing_condition_fields"], json!({}));
|
||||
for p in [json!({}), json!({"card":card,"assign":true}), json!({"card":{"schema_version":9}})] {
|
||||
assert!(s.execute("material.properties.validate", &p).is_err());
|
||||
}
|
||||
|
||||
@@ -58,6 +58,16 @@ isotropic derived response and its reciprocals must also remain finite/positive.
|
||||
Moisture is kg water / kg dry material and can exceed one. It is not a wet-mass
|
||||
fraction. Unspecified temperature/state, wood moisture or polymer strain rate are
|
||||
reported in `unspecified_conditions`; they are not guessed or replaced by zero.
|
||||
The validation report also returns `missing_condition_fields`, a map from each
|
||||
incomplete property kind to its exact absent field names. Temperature and state
|
||||
apply to every family; wood additionally checks dry-basis moisture and polymers
|
||||
strain rate, following the same reporting policy as `unspecified_conditions`.
|
||||
For example, a wood density with temperature and state but no moisture reports
|
||||
`{"density":["moisture_dry_basis_kg_kg"]}`. Complete properties are omitted;
|
||||
an empty card returns an empty map, without inventing properties or conditions.
|
||||
This detail is read-only and does not change admission or the saved card schema.
|
||||
Older serialized validation reports without the map deserialize with an empty
|
||||
map; regenerate validation from the card to obtain the missing-field detail.
|
||||
The family label does not certify that a chosen constitutive model fits a material.
|
||||
Scalar strengths are individual measurements, not a three-dimensional failure
|
||||
envelope. Typed strength directions and test methods remain a separate extension.
|
||||
|
||||
Reference in new issue
Block a user