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}