Skip to main content

runmat_analysis_fea/adaptation/
field_transfer.rs

1use std::collections::BTreeMap;
2
3use runmat_analysis_core::{AnalysisField, AnalysisFieldValues};
4use runmat_canonical_codec::CanonicalLimits;
5use runmat_meshing_core::{
6    ElementOrder, FieldTopologyLocation, SolverEntityTransfer, SolverMeshArtifact,
7    SolverMeshTransferMap, SolverTransferMethod, StableDigest, TETRAHEDRON_MIDSIDE_EDGE_CORNERS,
8};
9use serde::{Deserialize, Serialize};
10use sha2::{Digest as _, Sha256};
11
12const FIELD_CODEC_PREFIX: &[u8] = b"runmat-analysis-fea-field-cbor/v1\0";
13const FIELD_CODEC_DOMAIN: &str = "analysis.fea.field/v1";
14const FIELD_CODEC_LIMITS: CanonicalLimits =
15    CanonicalLimits::new(512 * 1024 * 1024, 20_000_000, 4096, 16);
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum SolverFieldTransferMethod {
20    StableIdentity,
21    QuadraticEdgeInterpolation,
22    BarycentricInterpolation,
23    CentroidProjection,
24    ConservativeProjection,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(deny_unknown_fields)]
29pub struct SolverFieldTransferEvidence {
30    pub source_artifact_digest: StableDigest,
31    pub target_artifact_digest: StableDigest,
32    pub topology_id: String,
33    pub location: FieldTopologyLocation,
34    pub component_count: usize,
35    pub copied_entity_count: usize,
36    pub projected_entity_count: usize,
37    pub methods: Vec<SolverFieldTransferMethod>,
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41#[serde(deny_unknown_fields)]
42pub struct SolverFieldTransferErrorEvidence {
43    pub transfer: SolverFieldTransferEvidence,
44    pub transferred_field_digest: StableDigest,
45    pub reference_field_digest: StableDigest,
46    pub absolute_l2_error: f64,
47    pub relative_l2_error: Option<f64>,
48}
49
50#[derive(Debug, Clone, PartialEq)]
51pub struct SolverFieldTransferResult {
52    pub field: AnalysisField,
53    pub evidence: SolverFieldTransferEvidence,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub enum SolverFieldTransferError {
58    InvalidSourceArtifact(String),
59    InvalidTargetArtifact(String),
60    InvalidTransferMap(String),
61    GeometryRevisionMismatch,
62    MissingSourceTopology,
63    MissingTargetTopology,
64    TopologyLocationMismatch,
65    InvalidFieldShape,
66    DeviceFieldRequiresHostTransfer,
67    UnsupportedTopologyChange,
68    InconsistentQuadraticConnectivity,
69    InvalidReferenceField,
70}
71
72pub fn measure_solver_field_transfer_error(
73    transfer: &SolverFieldTransferResult,
74    reference: &AnalysisField,
75) -> Result<SolverFieldTransferErrorEvidence, SolverFieldTransferError> {
76    if transfer.field.field_id != reference.field_id || transfer.field.shape != reference.shape {
77        return Err(SolverFieldTransferError::InvalidReferenceField);
78    }
79    let transferred = transfer
80        .field
81        .as_host_f64()
82        .ok_or(SolverFieldTransferError::DeviceFieldRequiresHostTransfer)?;
83    let reference_values = reference
84        .as_host_f64()
85        .ok_or(SolverFieldTransferError::DeviceFieldRequiresHostTransfer)?;
86    if transferred.len() != reference_values.len()
87        || transferred.iter().any(|value| !value.is_finite())
88        || reference_values.iter().any(|value| !value.is_finite())
89    {
90        return Err(SolverFieldTransferError::InvalidReferenceField);
91    }
92    let error_squared = transferred
93        .iter()
94        .zip(reference_values)
95        .map(|(transferred, reference)| (transferred - reference).powi(2))
96        .sum::<f64>();
97    let reference_squared = reference_values
98        .iter()
99        .map(|value| value * value)
100        .sum::<f64>();
101    let absolute_l2_error = error_squared.sqrt();
102    let relative_l2_error = if reference_squared == 0.0 {
103        if absolute_l2_error == 0.0 {
104            Some(0.0)
105        } else {
106            None
107        }
108    } else {
109        Some(absolute_l2_error / reference_squared.sqrt())
110    };
111    Ok(SolverFieldTransferErrorEvidence {
112        transfer: transfer.evidence.clone(),
113        transferred_field_digest: field_digest(&transfer.field)?,
114        reference_field_digest: field_digest(reference)?,
115        absolute_l2_error,
116        relative_l2_error,
117    })
118}
119
120fn field_digest(field: &AnalysisField) -> Result<StableDigest, SolverFieldTransferError> {
121    let encoded = runmat_canonical_codec::encode_contract(
122        FIELD_CODEC_PREFIX,
123        FIELD_CODEC_DOMAIN,
124        field,
125        FIELD_CODEC_LIMITS,
126    )
127    .map_err(|_| SolverFieldTransferError::InvalidReferenceField)?;
128    Ok(StableDigest::from_bytes(Sha256::digest(encoded).into()))
129}
130
131pub fn transfer_solver_field(
132    source: &SolverMeshArtifact,
133    target: &SolverMeshArtifact,
134    transfer_map: Option<&SolverMeshTransferMap>,
135    topology_id: &str,
136    field: &AnalysisField,
137) -> Result<SolverFieldTransferResult, SolverFieldTransferError> {
138    source
139        .validate()
140        .map_err(|failure| SolverFieldTransferError::InvalidSourceArtifact(failure.to_string()))?;
141    target
142        .validate()
143        .map_err(|failure| SolverFieldTransferError::InvalidTargetArtifact(failure.to_string()))?;
144    if source.geometry != target.geometry {
145        return Err(SolverFieldTransferError::GeometryRevisionMismatch);
146    }
147    if let Some(transfer_map) = transfer_map {
148        transfer_map
149            .validate_against(source, target)
150            .map_err(|failure| SolverFieldTransferError::InvalidTransferMap(failure.to_string()))?;
151    }
152    let source_map = source
153        .topology
154        .field_topologies
155        .iter()
156        .find(|map| map.topology_id == topology_id)
157        .ok_or(SolverFieldTransferError::MissingSourceTopology)?;
158    let target_map = target
159        .topology
160        .field_topologies
161        .iter()
162        .find(|map| map.topology_id == topology_id)
163        .ok_or(SolverFieldTransferError::MissingTargetTopology)?;
164    if source_map.location != target_map.location {
165        return Err(SolverFieldTransferError::TopologyLocationMismatch);
166    }
167    let values = match &field.values {
168        AnalysisFieldValues::HostF64(values) => values,
169        AnalysisFieldValues::DeviceRef(_) => {
170            return Err(SolverFieldTransferError::DeviceFieldRequiresHostTransfer)
171        }
172    };
173    let component_count =
174        component_count(field, source_map.ordered_entity_ids.len(), values.len())?;
175    let source_identities = stable_identities(source, source_map.location);
176    let target_identities = stable_identities(target, target_map.location);
177    let source_values = source_map
178        .ordered_entity_ids
179        .iter()
180        .enumerate()
181        .map(|(index, entity_id)| {
182            let start = index * component_count;
183            (
184                source_identities[entity_id],
185                &values[start..start + component_count],
186            )
187        })
188        .collect::<BTreeMap<_, _>>();
189    let midpoint_edges = if source_map.location == FieldTopologyLocation::Node {
190        target_midpoint_edges(target)?
191    } else {
192        BTreeMap::new()
193    };
194    let projections = transfer_map
195        .map(|transfer_map| transfer_map_for_location(transfer_map, source_map.location))
196        .unwrap_or_default();
197    let mut output = Vec::with_capacity(target_map.ordered_entity_ids.len() * component_count);
198    let mut copied_entity_count = 0;
199    let mut projected_entity_count = 0;
200    let mut applied_projection_methods = Vec::new();
201    let mut resolved_values = source_values
202        .iter()
203        .map(|(identity, values)| (*identity, values.to_vec()))
204        .collect::<BTreeMap<_, _>>();
205    for entity_id in &target_map.ordered_entity_ids {
206        let stable_identity = target_identities[entity_id];
207        if let Some(values) = source_values.get(&stable_identity) {
208            output.extend_from_slice(values);
209            copied_entity_count += 1;
210            continue;
211        }
212        let (values, method) = if let Some(projection) = projections.get(&stable_identity) {
213            (
214                apply_projection(projection, &source_values, component_count)?,
215                field_transfer_method(projection.method),
216            )
217        } else if let Some([left, right]) = midpoint_edges.get(&stable_identity).copied() {
218            let left = resolved_values
219                .get(&left)
220                .ok_or(SolverFieldTransferError::UnsupportedTopologyChange)?;
221            let right = resolved_values
222                .get(&right)
223                .ok_or(SolverFieldTransferError::UnsupportedTopologyChange)?;
224            (
225                left.iter()
226                    .zip(right)
227                    .map(|(left, right)| 0.5 * (left + right))
228                    .collect(),
229                SolverFieldTransferMethod::QuadraticEdgeInterpolation,
230            )
231        } else {
232            return Err(SolverFieldTransferError::UnsupportedTopologyChange);
233        };
234        output.extend_from_slice(&values);
235        resolved_values.insert(stable_identity, values);
236        projected_entity_count += 1;
237        if !applied_projection_methods.contains(&method) {
238            applied_projection_methods.push(method);
239        }
240    }
241    let mut shape = field.shape.clone();
242    shape[0] = target_map.ordered_entity_ids.len();
243    let mut methods = vec![SolverFieldTransferMethod::StableIdentity];
244    methods.extend(applied_projection_methods);
245    Ok(SolverFieldTransferResult {
246        field: AnalysisField::host_f64(field.field_id.clone(), shape, output),
247        evidence: SolverFieldTransferEvidence {
248            source_artifact_digest: source.canonical_digest,
249            target_artifact_digest: target.canonical_digest,
250            topology_id: topology_id.to_owned(),
251            location: source_map.location,
252            component_count,
253            copied_entity_count,
254            projected_entity_count,
255            methods,
256        },
257    })
258}
259
260fn transfer_map_for_location(
261    transfer_map: &SolverMeshTransferMap,
262    location: FieldTopologyLocation,
263) -> BTreeMap<StableDigest, &SolverEntityTransfer> {
264    let transfers = match location {
265        FieldTopologyLocation::Node => &transfer_map.node_transfers,
266        FieldTopologyLocation::VolumeElement => &transfer_map.volume_element_transfers,
267        FieldTopologyLocation::BoundaryFace => &transfer_map.boundary_face_transfers,
268        FieldTopologyLocation::BoundaryEdge => &transfer_map.boundary_edge_transfers,
269    };
270    transfers
271        .iter()
272        .map(|transfer| (transfer.target_stable_identity, transfer))
273        .collect()
274}
275
276fn apply_projection(
277    projection: &SolverEntityTransfer,
278    source_values: &BTreeMap<StableDigest, &[f64]>,
279    component_count: usize,
280) -> Result<Vec<f64>, SolverFieldTransferError> {
281    let mut result = vec![0.0; component_count];
282    for source in &projection.sources {
283        let values = source_values
284            .get(&source.stable_identity)
285            .ok_or(SolverFieldTransferError::UnsupportedTopologyChange)?;
286        for (result, value) in result.iter_mut().zip(*values) {
287            *result += source.weight * value;
288        }
289    }
290    Ok(result)
291}
292
293fn field_transfer_method(method: SolverTransferMethod) -> SolverFieldTransferMethod {
294    match method {
295        SolverTransferMethod::BarycentricInterpolation => {
296            SolverFieldTransferMethod::BarycentricInterpolation
297        }
298        SolverTransferMethod::CentroidProjection => SolverFieldTransferMethod::CentroidProjection,
299        SolverTransferMethod::ConservativeProjection => {
300            SolverFieldTransferMethod::ConservativeProjection
301        }
302    }
303}
304
305fn stable_identities(
306    artifact: &SolverMeshArtifact,
307    location: FieldTopologyLocation,
308) -> BTreeMap<u64, StableDigest> {
309    match location {
310        FieldTopologyLocation::Node => artifact
311            .topology
312            .nodes
313            .iter()
314            .map(|entity| (entity.node_id, entity.stable_identity))
315            .collect(),
316        FieldTopologyLocation::VolumeElement => artifact
317            .topology
318            .volume_elements
319            .iter()
320            .map(|entity| (entity.element_id, entity.stable_identity))
321            .collect(),
322        FieldTopologyLocation::BoundaryFace => artifact
323            .topology
324            .boundary_faces
325            .iter()
326            .map(|entity| (entity.face_id, entity.stable_identity))
327            .collect(),
328        FieldTopologyLocation::BoundaryEdge => artifact
329            .topology
330            .boundary_edges
331            .iter()
332            .map(|entity| (entity.edge_id, entity.stable_identity))
333            .collect(),
334    }
335}
336
337fn component_count(
338    field: &AnalysisField,
339    entity_count: usize,
340    value_count: usize,
341) -> Result<usize, SolverFieldTransferError> {
342    if field.shape.is_empty() || field.shape[0] != entity_count {
343        return Err(SolverFieldTransferError::InvalidFieldShape);
344    }
345    let components = field.shape[1..]
346        .iter()
347        .try_fold(1_usize, |product, count| product.checked_mul(*count))
348        .ok_or(SolverFieldTransferError::InvalidFieldShape)?;
349    if components == 0
350        || entity_count
351            .checked_mul(components)
352            .is_none_or(|expected| expected != value_count)
353    {
354        return Err(SolverFieldTransferError::InvalidFieldShape);
355    }
356    Ok(components)
357}
358
359fn target_midpoint_edges(
360    target: &SolverMeshArtifact,
361) -> Result<BTreeMap<StableDigest, [StableDigest; 2]>, SolverFieldTransferError> {
362    if target.resolved_request.element_order == ElementOrder::Tet4 {
363        return Ok(BTreeMap::new());
364    }
365    let mut result = BTreeMap::new();
366    let identities = stable_identities(target, FieldTopologyLocation::Node);
367    for element in &target.topology.volume_elements {
368        for (local_edge, corners) in TETRAHEDRON_MIDSIDE_EDGE_CORNERS.iter().enumerate() {
369            let mut edge = [
370                identities[&element.node_ids[corners[0]]],
371                identities[&element.node_ids[corners[1]]],
372            ];
373            edge.sort_unstable();
374            let midpoint = identities[&element.node_ids[4 + local_edge]];
375            if result
376                .insert(midpoint, edge)
377                .is_some_and(|existing| existing != edge)
378            {
379                return Err(SolverFieldTransferError::InconsistentQuadraticConnectivity);
380            }
381        }
382    }
383    Ok(result)
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use crate::assembly::solver_solid::tests::artifact;
390    use runmat_meshing_core::{
391        solver_boundary_edge_identity, solver_boundary_face_identity,
392        solver_volume_element_identity, CanonicalMeshingContract, SolverTransferSource,
393        SOLVER_MESH_TRANSFER_SCHEMA_VERSION,
394    };
395
396    #[test]
397    fn p_elevation_interpolates_nodes_and_preserves_element_history() {
398        let linear = artifact(ElementOrder::Tet4);
399        let quadratic = artifact(ElementOrder::Tet10);
400        let nodal = AnalysisField::host_f64(
401            "displacement",
402            vec![4, 3],
403            vec![0.0, 0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 4.0, 0.0, 0.0, 0.0, 6.0],
404        );
405        let elevated = transfer_solver_field(&linear, &quadratic, None, "nodes", &nodal).unwrap();
406        assert_eq!(elevated.field.shape, vec![10, 3]);
407        assert_eq!(elevated.evidence.copied_entity_count, 4);
408        assert_eq!(elevated.evidence.projected_entity_count, 6);
409        let values = elevated.field.as_host_f64().unwrap();
410        assert_eq!(&values[12..15], &[1.0, 0.0, 0.0]);
411        assert_eq!(&values[15..18], &[1.0, 2.0, 0.0]);
412        assert_eq!(&values[27..30], &[0.0, 2.0, 3.0]);
413
414        let history = AnalysisField::host_f64("plastic_strain", vec![1, 2], vec![0.2, 0.3]);
415        let transferred =
416            transfer_solver_field(&linear, &quadratic, None, "elements", &history).unwrap();
417        assert_eq!(transferred.field, history);
418        assert_eq!(transferred.evidence.projected_entity_count, 0);
419
420        let contact_state =
421            AnalysisField::host_f64("contact_state", vec![4], vec![0.0, 1.0, 1.0, 0.0]);
422        let transferred =
423            transfer_solver_field(&linear, &quadratic, None, "faces", &contact_state).unwrap();
424        assert_eq!(transferred.field, contact_state);
425        assert_eq!(
426            transferred.evidence.location,
427            FieldTopologyLocation::BoundaryFace
428        );
429    }
430
431    #[test]
432    fn p_restriction_preserves_corners_and_rejects_unrelated_or_device_fields() {
433        let linear = artifact(ElementOrder::Tet4);
434        let quadratic = artifact(ElementOrder::Tet10);
435        let values = (0..30).map(f64::from).collect::<Vec<_>>();
436        let quadratic_field = AnalysisField::host_f64("state", vec![10, 3], values);
437        let restricted =
438            transfer_solver_field(&quadratic, &linear, None, "nodes", &quadratic_field).unwrap();
439        assert_eq!(restricted.field.shape, vec![4, 3]);
440        assert_eq!(
441            restricted.field.as_host_f64().unwrap(),
442            &(0..12).map(f64::from).collect::<Vec<_>>()
443        );
444
445        let mut unrelated = artifact(ElementOrder::Tet10);
446        unrelated.geometry.geometry_revision += 1;
447        unrelated.seal_canonical_digest().unwrap();
448        assert_eq!(
449            transfer_solver_field(
450                &linear,
451                &unrelated,
452                None,
453                "nodes",
454                &AnalysisField::host_f64("x", vec![4], vec![0.0; 4])
455            )
456            .unwrap_err(),
457            SolverFieldTransferError::GeometryRevisionMismatch
458        );
459
460        let device = AnalysisField {
461            field_id: "x".into(),
462            shape: vec![4],
463            values: AnalysisFieldValues::DeviceRef(runmat_analysis_core::DeviceFieldRef {
464                backend: "gpu".into(),
465                token: "token".into(),
466                element_count: 4,
467            }),
468        };
469        assert_eq!(
470            transfer_solver_field(&linear, &quadratic, None, "nodes", &device).unwrap_err(),
471            SolverFieldTransferError::DeviceFieldRequiresHostTransfer
472        );
473    }
474
475    #[test]
476    fn transfer_rejects_bad_shape_and_unsealed_artifacts() {
477        let linear = artifact(ElementOrder::Tet4);
478        let quadratic = artifact(ElementOrder::Tet10);
479        assert_eq!(
480            transfer_solver_field(
481                &linear,
482                &quadratic,
483                None,
484                "nodes",
485                &AnalysisField::host_f64("bad", vec![3], vec![0.0; 3]),
486            )
487            .unwrap_err(),
488            SolverFieldTransferError::InvalidFieldShape
489        );
490        let mut tampered = quadratic;
491        tampered.topology.nodes[0].coordinates_m[0] = 0.25;
492        assert!(matches!(
493            transfer_solver_field(
494                &linear,
495                &tampered,
496                None,
497                "nodes",
498                &AnalysisField::host_f64("x", vec![4], vec![0.0; 4]),
499            ),
500            Err(SolverFieldTransferError::InvalidTargetArtifact(_))
501        ));
502    }
503
504    #[test]
505    fn transfer_uses_stable_identity_when_numeric_node_ids_shift() {
506        let source = artifact(ElementOrder::Tet4);
507        let mut target = source.clone();
508        let first_identity = target.topology.nodes[0].stable_identity;
509        let second_identity = target.topology.nodes[1].stable_identity;
510        target.topology.nodes[0].stable_identity = second_identity;
511        target.topology.nodes[1].stable_identity = first_identity;
512        let identities = target
513            .topology
514            .nodes
515            .iter()
516            .map(|node| (node.node_id, node.stable_identity))
517            .collect::<BTreeMap<_, _>>();
518        for face in &mut target.topology.boundary_faces {
519            face.stable_identity =
520                runmat_meshing_core::solver_boundary_face_identity(std::array::from_fn(|index| {
521                    identities[&face.node_ids[index]]
522                }));
523        }
524        for edge in &mut target.topology.boundary_edges {
525            edge.stable_identity =
526                runmat_meshing_core::solver_boundary_edge_identity(std::array::from_fn(|index| {
527                    identities[&edge.node_ids[index]]
528                }));
529        }
530        target.seal_canonical_digest().unwrap();
531
532        let field = AnalysisField::host_f64("temperature", vec![4], vec![10.0, 20.0, 30.0, 40.0]);
533        let transferred = transfer_solver_field(&source, &target, None, "nodes", &field).unwrap();
534        assert_eq!(
535            transferred.field.as_host_f64().unwrap(),
536            &[20.0, 10.0, 30.0, 40.0]
537        );
538    }
539
540    #[test]
541    fn weighted_h_transfer_projects_nodes_elements_and_boundary_state() {
542        let source = artifact(ElementOrder::Tet4);
543        let mut target = source.clone();
544        target.topology.nodes[0].stable_identity = StableDigest::from_bytes([99; 32]);
545        let identities = stable_identities(&target, FieldTopologyLocation::Node);
546        target.topology.volume_elements[0].stable_identity =
547            solver_volume_element_identity(std::array::from_fn(|index| {
548                identities[&target.topology.volume_elements[0].node_ids[index]]
549            }));
550        for face in &mut target.topology.boundary_faces {
551            face.stable_identity = solver_boundary_face_identity(std::array::from_fn(|index| {
552                identities[&face.node_ids[index]]
553            }));
554        }
555        for edge in &mut target.topology.boundary_edges {
556            edge.stable_identity = solver_boundary_edge_identity(std::array::from_fn(|index| {
557                identities[&edge.node_ids[index]]
558            }));
559        }
560        target.seal_canonical_digest().unwrap();
561
562        let mut transfer_map = SolverMeshTransferMap {
563            schema_version: SOLVER_MESH_TRANSFER_SCHEMA_VERSION,
564            source_artifact_digest: source.canonical_digest,
565            target_artifact_digest: target.canonical_digest,
566            geometry: source.geometry.clone(),
567            node_transfers: vec![SolverEntityTransfer {
568                target_stable_identity: target.topology.nodes[0].stable_identity,
569                method: SolverTransferMethod::BarycentricInterpolation,
570                sources: source
571                    .topology
572                    .nodes
573                    .iter()
574                    .map(|node| SolverTransferSource {
575                        stable_identity: node.stable_identity,
576                        weight: 0.25,
577                    })
578                    .collect(),
579            }],
580            volume_element_transfers: changed_entity_transfers(
581                source
582                    .topology
583                    .volume_elements
584                    .iter()
585                    .map(|entity| entity.stable_identity),
586                target
587                    .topology
588                    .volume_elements
589                    .iter()
590                    .map(|entity| entity.stable_identity),
591                SolverTransferMethod::CentroidProjection,
592            ),
593            boundary_face_transfers: changed_entity_transfers(
594                source
595                    .topology
596                    .boundary_faces
597                    .iter()
598                    .map(|entity| entity.stable_identity),
599                target
600                    .topology
601                    .boundary_faces
602                    .iter()
603                    .map(|entity| entity.stable_identity),
604                SolverTransferMethod::CentroidProjection,
605            ),
606            boundary_edge_transfers: changed_entity_transfers(
607                source
608                    .topology
609                    .boundary_edges
610                    .iter()
611                    .map(|entity| entity.stable_identity),
612                target
613                    .topology
614                    .boundary_edges
615                    .iter()
616                    .map(|entity| entity.stable_identity),
617                SolverTransferMethod::CentroidProjection,
618            ),
619        };
620        transfer_map.validate_against(&source, &target).unwrap();
621        let encoded = transfer_map.canonical_encode().unwrap();
622        assert_eq!(
623            SolverMeshTransferMap::canonical_decode(&encoded).unwrap(),
624            transfer_map
625        );
626
627        let nodal = AnalysisField::host_f64("temperature", vec![4], vec![10.0, 20.0, 30.0, 40.0]);
628        let transferred =
629            transfer_solver_field(&source, &target, Some(&transfer_map), "nodes", &nodal).unwrap();
630        assert_eq!(
631            transferred.field.as_host_f64().unwrap(),
632            &[25.0, 20.0, 30.0, 40.0]
633        );
634        assert!(transferred
635            .evidence
636            .methods
637            .contains(&SolverFieldTransferMethod::BarycentricInterpolation));
638        let reference =
639            AnalysisField::host_f64("temperature", vec![4], vec![24.0, 20.0, 30.0, 40.0]);
640        let transfer_error = measure_solver_field_transfer_error(&transferred, &reference).unwrap();
641        assert_eq!(transfer_error.absolute_l2_error, 1.0);
642        assert_eq!(
643            transfer_error.relative_l2_error,
644            Some(
645                1.0 / (24.0_f64.powi(2) + 20.0_f64.powi(2) + 30.0_f64.powi(2) + 40.0_f64.powi(2))
646                    .sqrt()
647            )
648        );
649        let wrong_reference = AnalysisField::host_f64("other", vec![4], vec![0.0; 4]);
650        assert_eq!(
651            measure_solver_field_transfer_error(&transferred, &wrong_reference),
652            Err(SolverFieldTransferError::InvalidReferenceField)
653        );
654
655        let history = AnalysisField::host_f64("history", vec![1, 2], vec![0.2, 0.3]);
656        let transferred =
657            transfer_solver_field(&source, &target, Some(&transfer_map), "elements", &history)
658                .unwrap();
659        assert_eq!(transferred.field, history);
660        assert!(transferred
661            .evidence
662            .methods
663            .contains(&SolverFieldTransferMethod::CentroidProjection));
664
665        transfer_map.node_transfers[0].sources[0].weight = 0.5;
666        assert!(matches!(
667            transfer_solver_field(&source, &target, Some(&transfer_map), "nodes", &nodal),
668            Err(SolverFieldTransferError::InvalidTransferMap(_))
669        ));
670    }
671
672    fn changed_entity_transfers(
673        source: impl Iterator<Item = StableDigest>,
674        target: impl Iterator<Item = StableDigest>,
675        method: SolverTransferMethod,
676    ) -> Vec<SolverEntityTransfer> {
677        let mut transfers = source
678            .zip(target)
679            .filter(|(source, target)| source != target)
680            .map(|(source, target)| SolverEntityTransfer {
681                target_stable_identity: target,
682                method,
683                sources: vec![SolverTransferSource {
684                    stable_identity: source,
685                    weight: 1.0,
686                }],
687            })
688            .collect::<Vec<_>>();
689        transfers.sort_by_key(|transfer| transfer.target_stable_identity);
690        transfers
691    }
692}