Skip to main content

runmat_analysis_fea/adaptation/
iteration.rs

1//! Canonical solver-owned evidence and convergence decisions for adaptive structural iterations.
2
3use 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};