1use runmat_meshing_core::{
4 CanonicalMeshingContract, SolverMeshAdaptationLineage, SolverMeshArtifact,
5 SolverMeshTransferMap, StableDigest,
6};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest as _, Sha256};
9
10use super::{SolverFieldTransferErrorEvidence, StructuralRecoveryEstimate};
11
12mod decision;
13mod validation;
14
15use decision::{decide, validate_policy};
16use validation::{
17 validate_estimator_against, validate_solver_result, validate_target_quantity,
18 validate_transfer_errors, validate_transfer_errors_against,
19};
20
21const CODEC_PREFIX: &[u8] = b"runmat-analysis-fea-canonical-cbor/v1\0";
22const CODEC_DOMAIN: &str = "analysis.fea.structural-adaptation-iteration/v1";
23const CODEC_LIMITS: CanonicalLimits = CanonicalLimits::new(64 * 1024 * 1024, 1_000_000, 4096, 32);
24pub const STRUCTURAL_ADAPTATION_ITERATION_SCHEMA_VERSION: u16 = 1;
25
26#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
27#[serde(deny_unknown_fields)]
28pub struct StructuralAdaptationPolicy {
29 pub estimator_tolerance: f64,
30 pub minimum_estimator_reduction: f64,
31 pub target_absolute_tolerance: f64,
32 pub target_relative_tolerance: f64,
33 pub maximum_transfer_relative_error: f64,
34}
35
36impl Default for StructuralAdaptationPolicy {
37 fn default() -> Self {
38 Self {
39 estimator_tolerance: 1.0e-3,
40 minimum_estimator_reduction: 0.05,
41 target_absolute_tolerance: 1.0e-6,
42 target_relative_tolerance: 1.0e-3,
43 maximum_transfer_relative_error: 0.05,
44 }
45 }
46}
47
48#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
49#[serde(deny_unknown_fields)]
50pub struct StructuralAdaptationSolverResult {
51 pub result_digest: StableDigest,
52 pub converged: bool,
53 pub iteration_count: u64,
54 pub normalized_residual: f64,
55}
56
57#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58#[serde(deny_unknown_fields)]
59pub struct StructuralTargetQuantity {
60 pub quantity_id: String,
61 pub value: f64,
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "snake_case")]
66pub enum StructuralAdaptationDecisionStatus {
67 Continue,
68 Converged,
69 Rejected,
70}
71
72#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct StructuralAdaptationConvergenceDecision {
75 pub status: StructuralAdaptationDecisionStatus,
76 pub estimator_reduction: Option<f64>,
77 pub target_absolute_change: Option<f64>,
78 pub target_relative_change: Option<f64>,
79 pub solver_converged: bool,
80 pub transfer_accepted: bool,
81 pub estimator_reduction_accepted: bool,
82 pub estimator_target_met: bool,
83 pub target_quantity_converged: bool,
84}
85
86#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct StructuralAdaptationIteration {
89 pub schema_version: u16,
90 pub iteration_index: u64,
91 pub previous_iteration_digest: Option<StableDigest>,
92 pub source_solver_artifact_digest: StableDigest,
93 pub target_solver_artifact_digest: StableDigest,
94 pub adaptation_lineage_digest: StableDigest,
95 pub transfer_map_digest: StableDigest,
96 pub estimator: StructuralRecoveryEstimate,
97 pub transfer_errors: Vec<SolverFieldTransferErrorEvidence>,
98 pub solver_result: StructuralAdaptationSolverResult,
99 pub target_quantity: StructuralTargetQuantity,
100 pub previous_estimator_error: Option<f64>,
101 pub previous_target_quantity_value: Option<f64>,
102 pub policy: StructuralAdaptationPolicy,
103 pub decision: StructuralAdaptationConvergenceDecision,
104}
105
106#[derive(Clone, Copy)]
107pub struct StructuralAdaptationIterationInput<'a> {
108 pub source_artifact: &'a SolverMeshArtifact,
109 pub target_artifact: &'a SolverMeshArtifact,
110 pub transfer_map: &'a SolverMeshTransferMap,
111 pub lineage: &'a SolverMeshAdaptationLineage,
112 pub estimator: &'a StructuralRecoveryEstimate,
113 pub transfer_errors: &'a [SolverFieldTransferErrorEvidence],
114 pub solver_result: &'a StructuralAdaptationSolverResult,
115 pub target_quantity: &'a StructuralTargetQuantity,
116 pub previous: Option<&'a StructuralAdaptationIteration>,
117}
118
119#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum StructuralAdaptationIterationError {
121 InvalidPolicy,
122 InvalidArtifact(String),
123 InvalidLineage(String),
124 InvalidEstimator,
125 InvalidTransferEvidence,
126 InvalidSolverResult,
127 InvalidTargetQuantity,
128 InvalidIterationChain,
129 InvalidDecision,
130 Codec(String),
131}
132
133pub fn build_structural_adaptation_iteration(
134 input: StructuralAdaptationIterationInput<'_>,
135 policy: StructuralAdaptationPolicy,
136) -> Result<StructuralAdaptationIteration, StructuralAdaptationIterationError> {
137 validate_policy(policy)?;
138 input.source_artifact.validate().map_err(|failure| {
139 StructuralAdaptationIterationError::InvalidArtifact(failure.to_string())
140 })?;
141 input.target_artifact.validate().map_err(|failure| {
142 StructuralAdaptationIterationError::InvalidArtifact(failure.to_string())
143 })?;
144 input
145 .lineage
146 .validate_against(
147 input.source_artifact,
148 input.target_artifact,
149 input.transfer_map,
150 )
151 .map_err(|failure| {
152 StructuralAdaptationIterationError::InvalidLineage(failure.to_string())
153 })?;
154 input
155 .estimator
156 .validate()
157 .map_err(|_| StructuralAdaptationIterationError::InvalidEstimator)?;
158 if input.estimator.solver_artifact_digest != input.target_artifact.canonical_digest {
159 return Err(StructuralAdaptationIterationError::InvalidEstimator);
160 }
161 validate_estimator_against(input.estimator, input.target_artifact)?;
162 validate_transfer_errors(
163 input.transfer_errors,
164 input.source_artifact.canonical_digest,
165 input.target_artifact.canonical_digest,
166 )?;
167 validate_transfer_errors_against(
168 input.transfer_errors,
169 input.source_artifact,
170 input.target_artifact,
171 )?;
172 validate_solver_result(input.solver_result)?;
173 validate_target_quantity(input.target_quantity)?;
174 let previous = previous_evidence(
175 input.previous,
176 input.source_artifact.canonical_digest,
177 &input.target_quantity.quantity_id,
178 )?;
179 let decision = decide(
180 input.estimator.total_error,
181 input.transfer_errors,
182 input.solver_result,
183 input.target_quantity.value,
184 previous.previous_estimator_error,
185 previous.previous_target_quantity_value,
186 policy,
187 );
188 let iteration = StructuralAdaptationIteration {
189 schema_version: STRUCTURAL_ADAPTATION_ITERATION_SCHEMA_VERSION,
190 iteration_index: previous.iteration_index,
191 previous_iteration_digest: previous.previous_iteration_digest,
192 source_solver_artifact_digest: input.source_artifact.canonical_digest,
193 target_solver_artifact_digest: input.target_artifact.canonical_digest,
194 adaptation_lineage_digest: input.lineage.canonical_digest().map_err(|failure| {
195 StructuralAdaptationIterationError::InvalidLineage(failure.to_string())
196 })?,
197 transfer_map_digest: input.transfer_map.canonical_digest().map_err(|failure| {
198 StructuralAdaptationIterationError::InvalidLineage(failure.to_string())
199 })?,
200 estimator: input.estimator.clone(),
201 transfer_errors: input.transfer_errors.to_vec(),
202 solver_result: input.solver_result.clone(),
203 target_quantity: input.target_quantity.clone(),
204 previous_estimator_error: previous.previous_estimator_error,
205 previous_target_quantity_value: previous.previous_target_quantity_value,
206 policy,
207 decision,
208 };
209 iteration.validate()?;
210 Ok(iteration)
211}
212
213impl StructuralAdaptationIteration {
214 pub fn validate(&self) -> Result<(), StructuralAdaptationIterationError> {
215 if self.schema_version != STRUCTURAL_ADAPTATION_ITERATION_SCHEMA_VERSION
216 || self.source_solver_artifact_digest == StableDigest::ZERO
217 || self.target_solver_artifact_digest == StableDigest::ZERO
218 || self.source_solver_artifact_digest == self.target_solver_artifact_digest
219 || self.adaptation_lineage_digest == StableDigest::ZERO
220 || self.transfer_map_digest == StableDigest::ZERO
221 || self.estimator.solver_artifact_digest != self.target_solver_artifact_digest
222 {
223 return Err(StructuralAdaptationIterationError::InvalidIterationChain);
224 }
225 validate_policy(self.policy)?;
226 self.estimator
227 .validate()
228 .map_err(|_| StructuralAdaptationIterationError::InvalidEstimator)?;
229 validate_transfer_errors(
230 &self.transfer_errors,
231 self.source_solver_artifact_digest,
232 self.target_solver_artifact_digest,
233 )?;
234 validate_solver_result(&self.solver_result)?;
235 validate_target_quantity(&self.target_quantity)?;
236 let has_previous = self.previous_iteration_digest.is_some();
237 if has_previous != (self.iteration_index > 0)
238 || has_previous != self.previous_estimator_error.is_some()
239 || has_previous != self.previous_target_quantity_value.is_some()
240 || self.previous_iteration_digest == Some(StableDigest::ZERO)
241 {
242 return Err(StructuralAdaptationIterationError::InvalidIterationChain);
243 }
244 let expected = decide(
245 self.estimator.total_error,
246 &self.transfer_errors,
247 &self.solver_result,
248 self.target_quantity.value,
249 self.previous_estimator_error,
250 self.previous_target_quantity_value,
251 self.policy,
252 );
253 if self.decision != expected {
254 return Err(StructuralAdaptationIterationError::InvalidDecision);
255 }
256 Ok(())
257 }
258
259 pub fn validate_against(
260 &self,
261 source: &SolverMeshArtifact,
262 target: &SolverMeshArtifact,
263 transfer: &SolverMeshTransferMap,
264 lineage: &SolverMeshAdaptationLineage,
265 ) -> Result<(), StructuralAdaptationIterationError> {
266 self.validate()?;
267 lineage
268 .validate_against(source, target, transfer)
269 .map_err(|failure| {
270 StructuralAdaptationIterationError::InvalidLineage(failure.to_string())
271 })?;
272 validate_estimator_against(&self.estimator, target)?;
273 validate_transfer_errors_against(&self.transfer_errors, source, target)?;
274 if self.source_solver_artifact_digest != source.canonical_digest
275 || self.target_solver_artifact_digest != target.canonical_digest
276 || self.adaptation_lineage_digest
277 != lineage.canonical_digest().map_err(|failure| {
278 StructuralAdaptationIterationError::InvalidLineage(failure.to_string())
279 })?
280 || self.transfer_map_digest
281 != transfer.canonical_digest().map_err(|failure| {
282 StructuralAdaptationIterationError::InvalidLineage(failure.to_string())
283 })?
284 {
285 return Err(StructuralAdaptationIterationError::InvalidIterationChain);
286 }
287 Ok(())
288 }
289
290 pub fn canonical_encode(&self) -> Result<Vec<u8>, StructuralAdaptationIterationError> {
291 self.validate()?;
292 runmat_canonical_codec::encode_contract(CODEC_PREFIX, CODEC_DOMAIN, self, CODEC_LIMITS)
293 .map_err(codec_error)
294 }
295
296 pub fn canonical_decode(bytes: &[u8]) -> Result<Self, StructuralAdaptationIterationError> {
297 let record: Self = runmat_canonical_codec::decode_contract(
298 CODEC_PREFIX,
299 CODEC_DOMAIN,
300 bytes,
301 CODEC_LIMITS,
302 )
303 .map_err(codec_error)?;
304 record.validate()?;
305 Ok(record)
306 }
307
308 pub fn canonical_digest(&self) -> Result<StableDigest, StructuralAdaptationIterationError> {
309 Ok(StableDigest::from_bytes(
310 Sha256::digest(self.canonical_encode()?).into(),
311 ))
312 }
313}
314
315struct PreviousIterationEvidence {
316 iteration_index: u64,
317 previous_iteration_digest: Option<StableDigest>,
318 previous_estimator_error: Option<f64>,
319 previous_target_quantity_value: Option<f64>,
320}
321
322fn previous_evidence(
323 previous: Option<&StructuralAdaptationIteration>,
324 source_digest: StableDigest,
325 target_quantity_id: &str,
326) -> Result<PreviousIterationEvidence, StructuralAdaptationIterationError> {
327 let Some(previous) = previous else {
328 return Ok(PreviousIterationEvidence {
329 iteration_index: 0,
330 previous_iteration_digest: None,
331 previous_estimator_error: None,
332 previous_target_quantity_value: None,
333 });
334 };
335 previous.validate()?;
336 if previous.target_solver_artifact_digest != source_digest
337 || previous.target_quantity.quantity_id != target_quantity_id
338 || previous.decision.status != StructuralAdaptationDecisionStatus::Continue
339 {
340 return Err(StructuralAdaptationIterationError::InvalidIterationChain);
341 }
342 let iteration_index = previous
343 .iteration_index
344 .checked_add(1)
345 .ok_or(StructuralAdaptationIterationError::InvalidIterationChain)?;
346 Ok(PreviousIterationEvidence {
347 iteration_index,
348 previous_iteration_digest: Some(previous.canonical_digest()?),
349 previous_estimator_error: Some(previous.estimator.total_error),
350 previous_target_quantity_value: Some(previous.target_quantity.value),
351 })
352}
353
354fn codec_error(error: CanonicalCodecError) -> StructuralAdaptationIterationError {
355 StructuralAdaptationIterationError::Codec(error.to_string())
356}
357
358#[cfg(test)]
359mod tests;
360use runmat_canonical_codec::{CanonicalCodecError, CanonicalLimits};