carapace_wasm/
material_probe.rs1use carapace_core::analysis::{
9 Algorithm, Analysis, AnalysisBuilder, AnalysisError, ConstraintHandler, ConvergenceTest,
10 Integrator, TangentStrategy,
11};
12use carapace_core::model::{Domain, Element, Material, Node, NodeId, ZeroLength};
13use serde::{Deserialize, Serialize};
14
15use crate::input_v1::materials::{resolve_materials, MaterialSpec};
16
17#[derive(Debug, Clone, Deserialize, tsify::Tsify)]
18#[serde(rename_all = "camelCase")]
19pub struct MaterialProbeConfig {
20 pub material: MaterialSpec,
21 #[serde(default)]
24 pub arena: Vec<MaterialSpec>,
25 #[serde(default)]
26 pub initial_strain: Option<f64>,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Serialize, tsify::Tsify)]
30#[serde(rename_all = "camelCase")]
31pub struct MaterialProbeResponse {
32 pub strain: f64,
33 pub stress: f64,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, tsify::Tsify)]
37#[serde(tag = "kind", rename_all = "camelCase", rename_all_fields = "camelCase")]
38pub enum MaterialProbeError {
39 UnsupportedMaterial { material: &'static str },
41 InvalidTarget { target: f64 },
43 InvalidProbe { reason: String },
44 SolverFailure { target: f64, detail: String },
46}
47
48pub struct MaterialProbe {
49 material: Material,
50 initial_strain: f64,
51 analysis: Analysis,
52 node: NodeId,
53 strain: f64,
54}
55
56fn kind_name(spec: &MaterialSpec) -> Option<&'static str> {
57 match spec {
58 MaterialSpec::Gap { .. } => Some("gap"),
59 _ => None,
60 }
61}
62
63fn build(material: &Material) -> (Analysis, NodeId) {
64 let mut domain = Domain::new();
65 let ground = domain.add_node(Node::new([0.0, 0.0]).fix(0).fix(1).fix(2));
66 let node = domain.add_node(Node::new([0.0, 0.0]).fix(0).fix(1).fix(2));
67 domain.add_element(Element::ZeroLength(
68 ZeroLength::new(ground, node).with_material(0, material.clone()),
69 ));
70 let analysis = AnalysisBuilder::new()
73 .constraint_handler(ConstraintHandler::Plain)
74 .integrator(Integrator::LoadControl { increment: 0.0 })
75 .algorithm(Algorithm::Newton {
76 tangent: TangentStrategy::Current,
77 line_search: None,
78 })
79 .test(ConvergenceTest::NormUnbalance {
80 tol: 1e-9,
81 max_iter: 10,
82 })
83 .build(domain);
84 (analysis, node)
85}
86
87impl MaterialProbe {
88 pub fn new(config: &MaterialProbeConfig) -> Result<Self, MaterialProbeError> {
89 if let Some(material) = config
90 .arena
91 .iter()
92 .chain(std::iter::once(&config.material))
93 .find_map(kind_name)
94 {
95 return Err(MaterialProbeError::UnsupportedMaterial { material });
96 }
97 let initial_strain = config.initial_strain.unwrap_or(0.0);
98 if !initial_strain.is_finite() {
99 return Err(MaterialProbeError::InvalidTarget {
100 target: initial_strain,
101 });
102 }
103 let mut arena = config.arena.clone();
105 arena.push(config.material.clone());
106 let material = resolve_materials(&arena)
107 .map_err(|error| MaterialProbeError::InvalidProbe {
108 reason: format!("{error:?}"),
109 })?
110 .pop()
111 .expect("arena is non-empty");
112 let mut probe = Self::fresh(material, initial_strain);
113 probe.apply_strain(initial_strain)?;
114 Ok(probe)
115 }
116
117 fn fresh(material: Material, initial_strain: f64) -> Self {
118 let (analysis, node) = build(&material);
119 MaterialProbe {
120 material,
121 initial_strain,
122 analysis,
123 node,
124 strain: 0.0,
125 }
126 }
127
128 pub fn apply_strain(&mut self, target: f64) -> Result<MaterialProbeResponse, MaterialProbeError> {
133 if !target.is_finite() {
134 return Err(MaterialProbeError::InvalidTarget { target });
135 }
136 if target != self.strain {
137 self.analysis
138 .step_prescribed(&[(self.node, 0, target)])
139 .map_err(|error: AnalysisError| MaterialProbeError::SolverFailure {
140 target,
141 detail: format!("{error:?}"),
142 })?;
143 self.strain = target;
144 }
145 Ok(MaterialProbeResponse {
146 strain: self.strain,
147 stress: self.analysis.domain().reaction(self.node, 0, 0.0),
148 })
149 }
150
151 pub fn reset(&mut self) -> Result<(), MaterialProbeError> {
153 *self = Self::fresh(self.material.clone(), self.initial_strain);
154 self.apply_strain(self.initial_strain).map(|_| ())
155 }
156}