1use sim_lib_interference_core::{
4 Emitter, FieldAmplitude, Hertz, InterferenceProblem, MetresPerSecond, NepersPerMetre, Point3M,
5 PositiveMetres, Radians, SamplingPlane, SamplingPolicy, SamplingThresholds, ScalarMedium,
6 SourceSet, UnitVector3, WorkBudget,
7};
8
9use crate::{
10 HostPhasorField, ReferencePhasorSolver, ReferenceSolveError,
11 analytic::measure_analytic_fixtures,
12 helmholtz::{MAX_OBSERVED_ORDER, MIN_OBSERVED_ORDER, measure_helmholtz_fixture_matrix},
13 verification::{
14 MAX_ANALYTIC_RELATIVE_ERROR, MAX_METAMORPHIC_RELATIVE_ERROR, VerificationError,
15 VerificationReport,
16 },
17};
18
19const FIXTURE_FREQUENCY_HERTZ: f64 = 7.0;
20const FIXTURE_SPEED_METRES_PER_SECOND: f64 = 28.0;
21
22pub fn verify_reference_solver() -> Result<VerificationReport, VerificationError> {
29 let analytic = measure_analytic_fixtures()?;
30 let metamorphic = measure_metamorphic_fixtures()?;
31 require_relative(
32 "analytic",
33 analytic.max_error(),
34 MAX_ANALYTIC_RELATIVE_ERROR,
35 )?;
36 for (check, measured) in [
37 ("point-reciprocity", metamorphic.reciprocity_relative),
38 ("linearity", metamorphic.linearity_relative),
39 ("rigid-motion", metamorphic.rigid_motion_relative),
40 ("global-phase", metamorphic.global_phase_relative),
41 ] {
42 require_relative(check, measured, MAX_METAMORPHIC_RELATIVE_ERROR)?;
43 }
44 if !metamorphic.source_permutation_identical {
45 return Err(VerificationError::SourcePermutationChanged);
46 }
47
48 let helmholtz = measure_helmholtz_fixture_matrix()?;
49 for named in &helmholtz {
50 if !(MIN_OBSERVED_ORDER..=MAX_OBSERVED_ORDER).contains(&named.measurement.observed_order) {
51 return Err(VerificationError::HelmholtzOrderOutOfRange {
52 fixture: named.name,
53 observed: named.measurement.observed_order,
54 minimum: MIN_OBSERVED_ORDER,
55 maximum: MAX_OBSERVED_ORDER,
56 });
57 }
58 }
59 let least_ideal_order = helmholtz
60 .iter()
61 .max_by(|left, right| {
62 (left.measurement.observed_order - 2.0)
63 .abs()
64 .total_cmp(&(right.measurement.observed_order - 2.0).abs())
65 })
66 .expect("the fixed Helmholtz fixture matrix is non-empty")
67 .measurement
68 .observed_order;
69
70 Ok(VerificationReport {
71 analytic_max_error: analytic.max_error(),
72 reciprocity_relative: metamorphic.reciprocity_relative,
73 linearity_relative: metamorphic.linearity_relative,
74 rigid_motion_relative: metamorphic.rigid_motion_relative,
75 global_phase_relative: metamorphic.global_phase_relative,
76 source_permutation_identical: metamorphic.source_permutation_identical,
77 helmholtz_observed_order: least_ideal_order,
78 })
79}
80
81fn require_relative(
82 check: &'static str,
83 measured: f64,
84 limit: f64,
85) -> Result<(), VerificationError> {
86 if measured.is_finite() && measured <= limit {
87 Ok(())
88 } else {
89 Err(VerificationError::RelativeErrorExceeded {
90 check,
91 measured,
92 limit,
93 })
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub(crate) struct MetamorphicMetrics {
99 pub(crate) reciprocity_relative: f64,
100 pub(crate) linearity_relative: f64,
101 pub(crate) rigid_motion_relative: f64,
102 pub(crate) global_phase_relative: f64,
103 pub(crate) source_permutation_identical: bool,
104}
105
106pub(crate) fn measure_metamorphic_fixtures() -> Result<MetamorphicMetrics, ReferenceSolveError> {
107 Ok(MetamorphicMetrics {
108 reciprocity_relative: point_reciprocity_error()?,
109 linearity_relative: linearity_error()?,
110 rigid_motion_relative: rigid_motion_error()?,
111 global_phase_relative: global_phase_error()?,
112 source_permutation_identical: source_permutation_identical()?,
113 })
114}
115
116fn point_reciprocity_error() -> Result<f64, ReferenceSolveError> {
117 let first = point(-0.75, 0.5, -1.25)?;
118 let second = point(1.25, -0.25, 0.75)?;
119 let from_first = problem(vec![point_source("first", first, 1.0, 0.0)?], 0.08)?;
120 let from_second = problem(vec![point_source("second", second, 1.0, 0.0)?], 0.08)?;
121 let first_to_second = solve(&from_first, &point_plane(second)?)?;
122 let second_to_first = solve(&from_second, &point_plane(first)?)?;
123
124 Ok(relative_complex_error(
125 first_to_second.cell(0, 0).unwrap(),
126 second_to_first.cell(0, 0).unwrap(),
127 ))
128}
129
130fn linearity_error() -> Result<f64, ReferenceSolveError> {
131 let sample_plane = fixture_plane()?;
132 let first_source = point_source("linear-point", point(-0.5, 0.25, -3.0)?, 2.0, 0.3)?;
133 let second_source = forward_plane(
134 "linear-plane",
135 point(0.0, 0.0, -1.0)?,
136 direction(0.0, 0.0, 1.0)?,
137 0.75,
138 -0.6,
139 )?;
140 let first = solve(&problem(vec![first_source.clone()], 0.02)?, &sample_plane)?;
141 let second = solve(&problem(vec![second_source.clone()], 0.02)?, &sample_plane)?;
142 let combined = solve(
143 &problem(vec![second_source, first_source], 0.02)?,
144 &sample_plane,
145 )?;
146
147 Ok(relative_field_sum_error(&combined, &first, &second))
148}
149
150fn rigid_motion_error() -> Result<f64, ReferenceSolveError> {
151 let sources = vec![
152 point_source("rigid-point", point(-1.0, 0.5, -4.0)?, 2.25, 0.4)?,
153 forward_plane(
154 "rigid-plane",
155 point(0.0, 0.0, -2.0)?,
156 direction(0.0, 0.0, 1.0)?,
157 0.8,
158 -0.7,
159 )?,
160 ];
161 let transformed_sources = sources
162 .iter()
163 .map(rigid_emitter)
164 .collect::<Result<Vec<_>, _>>()?;
165 let original_plane = fixture_plane()?;
166 let transformed_plane = rigid_plane(original_plane)?;
167 let original = solve(&problem(sources, 0.03)?, &original_plane)?;
168 let transformed = solve(&problem(transformed_sources, 0.03)?, &transformed_plane)?;
169
170 Ok(relative_field_error(&original, &transformed))
171}
172
173fn global_phase_error() -> Result<f64, ReferenceSolveError> {
174 let phase_shift = 0.625;
175 let sources = vec![
176 point_source("phase-point", point(-0.75, 0.25, -3.5)?, 1.5, 0.2)?,
177 forward_plane(
178 "phase-plane",
179 point(0.0, 0.0, -1.5)?,
180 direction(0.0, 0.0, 1.0)?,
181 0.9,
182 -0.4,
183 )?,
184 ];
185 let shifted_sources = sources
186 .iter()
187 .map(|source| phase_shifted_emitter(source, phase_shift))
188 .collect::<Result<Vec<_>, _>>()?;
189 let sample_plane = fixture_plane()?;
190 let original = solve(&problem(sources, 0.015)?, &sample_plane)?;
191 let shifted = solve(&problem(shifted_sources, 0.015)?, &sample_plane)?;
192
193 Ok(global_phase_field_error(&shifted, &original, phase_shift))
194}
195
196fn source_permutation_identical() -> Result<bool, ReferenceSolveError> {
197 let sample_plane = fixture_plane()?;
198 let solve_order = |order: [&str; 3]| -> Result<HostPhasorField, ReferenceSolveError> {
199 let sources = order
200 .into_iter()
201 .map(permutation_source)
202 .collect::<Result<Vec<_>, _>>()?;
203 solve(&problem(sources, 0.025)?, &sample_plane)
204 };
205 let baseline = solve_order(["point-a", "point-b", "plane"])?;
206 let reversed = solve_order(["plane", "point-b", "point-a"])?;
207 let rotated = solve_order(["point-b", "plane", "point-a"])?;
208
209 Ok(component_bits(&baseline) == component_bits(&reversed)
210 && component_bits(&baseline) == component_bits(&rotated))
211}
212
213fn permutation_source(id: &str) -> Result<Emitter, ReferenceSolveError> {
214 match id {
215 "point-a" => point_source("point-a", point(-0.5, 0.75, -3.0)?, 1.5, 0.1),
216 "point-b" => point_source("point-b", point(0.75, -0.25, -2.5)?, 0.75, -0.3),
217 "plane" => forward_plane(
218 "plane",
219 point(0.0, 0.0, -1.0)?,
220 direction(0.0, 0.0, 1.0)?,
221 0.5,
222 0.8,
223 ),
224 _ => unreachable!("the fixture lists only known source ids"),
225 }
226}
227
228fn solve(
229 problem: &InterferenceProblem,
230 plane: &SamplingPlane,
231) -> Result<HostPhasorField, ReferenceSolveError> {
232 ReferencePhasorSolver::new(
233 SamplingPolicy::Annotate,
234 SamplingThresholds::default(),
235 WorkBudget::default(),
236 )
237 .solve(problem, plane)
238 .map(|(field, _)| field)
239}
240
241fn problem(
242 sources: Vec<Emitter>,
243 attenuation: f64,
244) -> Result<InterferenceProblem, ReferenceSolveError> {
245 Ok(InterferenceProblem::new(
246 Hertz::new(FIXTURE_FREQUENCY_HERTZ)?,
247 ScalarMedium::new(
248 MetresPerSecond::new(FIXTURE_SPEED_METRES_PER_SECOND)?,
249 NepersPerMetre::new(attenuation)?,
250 ),
251 SourceSet::new(sources)?,
252 PositiveMetres::new(1.0e-6)?,
253 ))
254}
255
256fn point_source(
257 id: &str,
258 position: Point3M,
259 amplitude: f64,
260 phase: f64,
261) -> Result<Emitter, ReferenceSolveError> {
262 Ok(Emitter::Point {
263 id: id.to_owned(),
264 position,
265 amplitude_at_reference: FieldAmplitude::new(amplitude)?,
266 phase: Radians::new(phase)?,
267 })
268}
269
270fn forward_plane(
271 id: &str,
272 through: Point3M,
273 direction: UnitVector3,
274 amplitude: f64,
275 phase: f64,
276) -> Result<Emitter, ReferenceSolveError> {
277 Ok(Emitter::ForwardPlane {
278 id: id.to_owned(),
279 through,
280 direction,
281 amplitude: FieldAmplitude::new(amplitude)?,
282 phase: Radians::new(phase)?,
283 })
284}
285
286fn phase_shifted_emitter(source: &Emitter, shift: f64) -> Result<Emitter, ReferenceSolveError> {
287 Ok(match source {
288 Emitter::Point {
289 id,
290 position,
291 amplitude_at_reference,
292 phase,
293 } => Emitter::Point {
294 id: id.clone(),
295 position: *position,
296 amplitude_at_reference: *amplitude_at_reference,
297 phase: Radians::new(phase.get() + shift)?,
298 },
299 Emitter::ForwardPlane {
300 id,
301 through,
302 direction,
303 amplitude,
304 phase,
305 } => Emitter::ForwardPlane {
306 id: id.clone(),
307 through: *through,
308 direction: *direction,
309 amplitude: *amplitude,
310 phase: Radians::new(phase.get() + shift)?,
311 },
312 })
313}
314
315fn rigid_emitter(source: &Emitter) -> Result<Emitter, ReferenceSolveError> {
316 Ok(match source {
317 Emitter::Point {
318 id,
319 position,
320 amplitude_at_reference,
321 phase,
322 } => Emitter::Point {
323 id: id.clone(),
324 position: rigid_point(*position)?,
325 amplitude_at_reference: *amplitude_at_reference,
326 phase: *phase,
327 },
328 Emitter::ForwardPlane {
329 id,
330 through,
331 direction,
332 amplitude,
333 phase,
334 } => Emitter::ForwardPlane {
335 id: id.clone(),
336 through: rigid_point(*through)?,
337 direction: rigid_direction(*direction)?,
338 amplitude: *amplitude,
339 phase: *phase,
340 },
341 })
342}
343
344fn rigid_plane(plane: SamplingPlane) -> Result<SamplingPlane, ReferenceSolveError> {
345 SamplingPlane::new(
346 rigid_point(plane.origin())?,
347 rigid_direction(plane.u_axis())?,
348 rigid_direction(plane.v_axis())?,
349 plane.extent_u(),
350 plane.extent_v(),
351 plane.rows(),
352 plane.columns(),
353 )
354 .map_err(ReferenceSolveError::from)
355}
356
357fn rigid_point(value: Point3M) -> Result<Point3M, ReferenceSolveError> {
358 let [x, y, z] = value.coordinates_metres();
359 point(-y + 3.0, x - 2.0, z + 5.0)
360}
361
362fn rigid_direction(value: UnitVector3) -> Result<UnitVector3, ReferenceSolveError> {
363 let [x, y, z] = value.components();
364 direction(-y, x, z)
365}
366
367fn fixture_plane() -> Result<SamplingPlane, ReferenceSolveError> {
368 SamplingPlane::new(
369 point(-0.5, -0.75, 0.0)?,
370 direction(1.0, 0.0, 0.0)?,
371 direction(0.0, 1.0, 0.0)?,
372 PositiveMetres::new(1.0)?,
373 PositiveMetres::new(1.5)?,
374 3,
375 4,
376 )
377 .map_err(ReferenceSolveError::from)
378}
379
380fn point_plane(at: Point3M) -> Result<SamplingPlane, ReferenceSolveError> {
381 let [x, y, z] = at.coordinates_metres();
382 SamplingPlane::new(
383 point(x - 0.125, y - 0.125, z)?,
384 direction(1.0, 0.0, 0.0)?,
385 direction(0.0, 1.0, 0.0)?,
386 PositiveMetres::new(0.25)?,
387 PositiveMetres::new(0.25)?,
388 1,
389 1,
390 )
391 .map_err(ReferenceSolveError::from)
392}
393
394fn point(x: f64, y: f64, z: f64) -> Result<Point3M, ReferenceSolveError> {
395 Point3M::from_metres(x, y, z).map_err(ReferenceSolveError::from)
396}
397
398fn direction(x: f64, y: f64, z: f64) -> Result<UnitVector3, ReferenceSolveError> {
399 UnitVector3::new(x, y, z).map_err(ReferenceSolveError::from)
400}
401
402fn relative_complex_error(actual: (f64, f64), expected: (f64, f64)) -> f64 {
403 let difference = (actual.0 - expected.0).hypot(actual.1 - expected.1);
404 let scale = actual
405 .0
406 .hypot(actual.1)
407 .max(expected.0.hypot(expected.1))
408 .max(f64::MIN_POSITIVE);
409 difference / scale
410}
411
412fn relative_field_error(actual: &HostPhasorField, expected: &HostPhasorField) -> f64 {
413 actual
414 .real()
415 .iter()
416 .zip(actual.imaginary())
417 .zip(expected.real().iter().zip(expected.imaginary()))
418 .map(
419 |((&actual_real, &actual_imaginary), (&expected_real, &expected_imaginary))| {
420 relative_complex_error(
421 (actual_real, actual_imaginary),
422 (expected_real, expected_imaginary),
423 )
424 },
425 )
426 .fold(0.0, f64::max)
427}
428
429fn relative_field_sum_error(
430 actual: &HostPhasorField,
431 first: &HostPhasorField,
432 second: &HostPhasorField,
433) -> f64 {
434 (0..actual.len())
435 .map(|index| {
436 relative_complex_error(
437 (actual.real()[index], actual.imaginary()[index]),
438 (
439 first.real()[index] + second.real()[index],
440 first.imaginary()[index] + second.imaginary()[index],
441 ),
442 )
443 })
444 .fold(0.0, f64::max)
445}
446
447fn global_phase_field_error(
448 actual: &HostPhasorField,
449 original: &HostPhasorField,
450 phase_shift: f64,
451) -> f64 {
452 let cosine = phase_shift.cos();
453 let sine = phase_shift.sin();
454 (0..actual.len())
455 .map(|index| {
456 let real = original.real()[index];
457 let imaginary = original.imaginary()[index];
458 relative_complex_error(
459 (actual.real()[index], actual.imaginary()[index]),
460 (
461 real * cosine - imaginary * sine,
462 real * sine + imaginary * cosine,
463 ),
464 )
465 })
466 .fold(0.0, f64::max)
467}
468
469fn component_bits(field: &HostPhasorField) -> (Vec<u64>, Vec<u64>) {
470 (
471 field.real().iter().map(|value| value.to_bits()).collect(),
472 field
473 .imaginary()
474 .iter()
475 .map(|value| value.to_bits())
476 .collect(),
477 )
478}
479
480#[cfg(test)]
481mod tests {
482 use super::measure_metamorphic_fixtures;
483
484 #[test]
485 fn reciprocity_linearity_rigid_motion_phase_and_permutation_laws_hold() {
486 let metrics = measure_metamorphic_fixtures().unwrap();
487
488 assert!(metrics.reciprocity_relative <= 2.0e-15, "{metrics:?}");
489 assert!(metrics.linearity_relative <= 2.0e-15, "{metrics:?}");
490 assert!(metrics.rigid_motion_relative <= 2.0e-14, "{metrics:?}");
491 assert!(metrics.global_phase_relative <= 2.0e-15, "{metrics:?}");
492 assert!(metrics.source_permutation_identical, "{metrics:?}");
493 }
494}