1use std::{f64::consts::PI, fmt};
4
5use sim_lib_interference_solve::HostPhasorField;
6
7use crate::DenseF32Field;
8
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
11pub enum ConformanceMetric {
12 Real,
14 Imaginary,
16 Amplitude,
18 Phase,
20 MagnitudeSquared,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
26pub struct ScalarTolerance {
27 pub absolute: f64,
29 pub relative: f64,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct DifferentialTolerances {
36 pub component: ScalarTolerance,
38 pub amplitude: ScalarTolerance,
40 pub phase: ScalarTolerance,
42 pub magnitude_squared: ScalarTolerance,
44 pub phase_amplitude_floor: f64,
46}
47
48impl Default for DifferentialTolerances {
49 fn default() -> Self {
50 Self {
51 component: ScalarTolerance {
52 absolute: 2.0e-5,
53 relative: 2.0e-4,
54 },
55 amplitude: ScalarTolerance {
56 absolute: 2.0e-5,
57 relative: 2.0e-4,
58 },
59 phase: ScalarTolerance {
60 absolute: 3.0e-4,
61 relative: 1.0e-4,
62 },
63 magnitude_squared: ScalarTolerance {
64 absolute: 4.0e-5,
65 relative: 4.0e-4,
66 },
67 phase_amplitude_floor: 1.0e-5,
68 }
69 }
70}
71
72#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct DifferentialMaximum {
75 pub error: f64,
77 pub limit: f64,
79 pub row: usize,
81 pub column: usize,
83}
84
85impl DifferentialMaximum {
86 pub fn passed(self) -> bool {
88 self.error <= self.limit
89 }
90
91 fn ratio(self) -> f64 {
92 if self.limit == 0.0 {
93 if self.error == 0.0 {
94 0.0
95 } else {
96 f64::INFINITY
97 }
98 } else {
99 self.error / self.limit
100 }
101 }
102}
103
104#[derive(Clone, Debug, PartialEq)]
106pub struct DifferentialReport {
107 pub real: DifferentialMaximum,
109 pub imaginary: DifferentialMaximum,
111 pub amplitude: DifferentialMaximum,
113 pub phase: Option<DifferentialMaximum>,
115 pub magnitude_squared: DifferentialMaximum,
117 pub phase_cells: usize,
119 pub worst_metric: ConformanceMetric,
121 pub worst_row: usize,
123 pub worst_column: usize,
125}
126
127impl DifferentialReport {
128 pub fn passed(&self) -> bool {
130 self.real.passed()
131 && self.imaginary.passed()
132 && self.amplitude.passed()
133 && self.phase.is_none_or(DifferentialMaximum::passed)
134 && self.magnitude_squared.passed()
135 }
136
137 pub fn max_component_absolute_error(&self) -> f64 {
139 self.real.error.max(self.imaginary.error)
140 }
141
142 pub fn max_phase_absolute_error(&self) -> f64 {
147 self.phase.map_or(0.0, |maximum| maximum.error)
148 }
149}
150
151#[derive(Clone, Debug, PartialEq)]
153pub struct DifferentialError {
154 detail: String,
155}
156
157impl DifferentialError {
158 fn new(detail: impl Into<String>) -> Self {
159 Self {
160 detail: detail.into(),
161 }
162 }
163
164 pub fn detail(&self) -> &str {
166 &self.detail
167 }
168}
169
170impl fmt::Display for DifferentialError {
171 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172 formatter.write_str(&self.detail)
173 }
174}
175
176impl std::error::Error for DifferentialError {}
177
178pub fn compare_dense_to_reference(
180 reference: &HostPhasorField,
181 candidate: &DenseF32Field,
182 tolerances: DifferentialTolerances,
183) -> Result<DifferentialReport, DifferentialError> {
184 compare_candidate(
185 reference,
186 CandidateField {
187 name: "dense",
188 rows: candidate.rows(),
189 columns: candidate.columns(),
190 real: CandidateComponent::F32(candidate.real()),
191 imaginary: CandidateComponent::F32(candidate.imaginary()),
192 },
193 tolerances,
194 )
195}
196
197pub fn compare_materialized_to_reference(
204 reference: &HostPhasorField,
205 candidate: &HostPhasorField,
206 tolerances: DifferentialTolerances,
207) -> Result<DifferentialReport, DifferentialError> {
208 compare_candidate(
209 reference,
210 CandidateField {
211 name: "materialized",
212 rows: candidate.rows(),
213 columns: candidate.columns(),
214 real: CandidateComponent::F64(candidate.real()),
215 imaginary: CandidateComponent::F64(candidate.imaginary()),
216 },
217 tolerances,
218 )
219}
220
221fn compare_candidate(
222 reference: &HostPhasorField,
223 candidate: CandidateField<'_>,
224 tolerances: DifferentialTolerances,
225) -> Result<DifferentialReport, DifferentialError> {
226 validate_inputs(reference, candidate, tolerances)?;
227 let mut maxima = Maxima::default();
228 let columns = reference.columns();
229 for index in 0..reference.len() {
230 let row = index / columns;
231 let column = index % columns;
232 let rr = reference.real()[index];
233 let ri = reference.imaginary()[index];
234 let cr = candidate.real.get(index);
235 let ci = candidate.imaginary.get(index);
236 let reference_amplitude = rr.hypot(ri);
237 let candidate_amplitude = cr.hypot(ci);
238 maxima.observe(
239 ConformanceMetric::Real,
240 (rr - cr).abs(),
241 limit(tolerances.component, rr, cr),
242 row,
243 column,
244 );
245 maxima.observe(
246 ConformanceMetric::Imaginary,
247 (ri - ci).abs(),
248 limit(tolerances.component, ri, ci),
249 row,
250 column,
251 );
252 maxima.observe(
253 ConformanceMetric::Amplitude,
254 (reference_amplitude - candidate_amplitude).abs(),
255 limit(
256 tolerances.amplitude,
257 reference_amplitude,
258 candidate_amplitude,
259 ),
260 row,
261 column,
262 );
263 if reference_amplitude >= tolerances.phase_amplitude_floor {
264 maxima.phase_cells += 1;
265 let reference_phase = ri.atan2(rr);
266 let candidate_phase = ci.atan2(cr);
267 maxima.observe(
268 ConformanceMetric::Phase,
269 wrapped_phase_error(reference_phase, candidate_phase),
270 limit(tolerances.phase, reference_phase, candidate_phase),
271 row,
272 column,
273 );
274 }
275 let reference_squared = rr.mul_add(rr, ri * ri);
276 let candidate_squared = cr.mul_add(cr, ci * ci);
277 maxima.observe(
278 ConformanceMetric::MagnitudeSquared,
279 (reference_squared - candidate_squared).abs(),
280 limit(
281 tolerances.magnitude_squared,
282 reference_squared,
283 candidate_squared,
284 ),
285 row,
286 column,
287 );
288 }
289 Ok(maxima.finish())
290}
291
292fn validate_inputs(
293 reference: &HostPhasorField,
294 candidate: CandidateField<'_>,
295 tolerances: DifferentialTolerances,
296) -> Result<(), DifferentialError> {
297 if reference.rows() != candidate.rows || reference.columns() != candidate.columns {
298 return Err(DifferentialError::new(format!(
299 "reference shape [{}, {}] differs from {} shape [{}, {}]",
300 reference.rows(),
301 reference.columns(),
302 candidate.name,
303 candidate.rows,
304 candidate.columns
305 )));
306 }
307 if candidate.real.len() != reference.len() || candidate.imaginary.len() != reference.len() {
308 return Err(DifferentialError::new(format!(
309 "{} component lengths [{}, {}] differ from reference length {}",
310 candidate.name,
311 candidate.real.len(),
312 candidate.imaginary.len(),
313 reference.len()
314 )));
315 }
316 for (name, tolerance) in [
317 ("component", tolerances.component),
318 ("amplitude", tolerances.amplitude),
319 ("phase", tolerances.phase),
320 ("magnitude-squared", tolerances.magnitude_squared),
321 ] {
322 if !tolerance.absolute.is_finite()
323 || tolerance.absolute < 0.0
324 || !tolerance.relative.is_finite()
325 || tolerance.relative < 0.0
326 {
327 return Err(DifferentialError::new(format!(
328 "{name} tolerances must be finite and non-negative"
329 )));
330 }
331 }
332 if !tolerances.phase_amplitude_floor.is_finite() || tolerances.phase_amplitude_floor <= 0.0 {
333 return Err(DifferentialError::new(
334 "phase amplitude floor must be finite and positive",
335 ));
336 }
337 for (name, values) in [
338 ("reference real", reference.real()),
339 ("reference imaginary", reference.imaginary()),
340 ] {
341 if let Some((index, value)) = values
342 .iter()
343 .copied()
344 .enumerate()
345 .find(|(_, value)| !value.is_finite())
346 {
347 return Err(DifferentialError::new(format!(
348 "{name} cell {index} is non-finite: {value}"
349 )));
350 }
351 }
352 for (component, values) in [("real", candidate.real), ("imaginary", candidate.imaginary)] {
353 if let Some((index, value)) = values.first_non_finite() {
354 return Err(DifferentialError::new(format!(
355 "{} {component} cell {index} is non-finite: {value}",
356 candidate.name
357 )));
358 }
359 }
360 Ok(())
361}
362
363#[derive(Clone, Copy)]
364enum CandidateComponent<'a> {
365 F32(&'a [f32]),
366 F64(&'a [f64]),
367}
368
369impl CandidateComponent<'_> {
370 fn len(self) -> usize {
371 match self {
372 Self::F32(values) => values.len(),
373 Self::F64(values) => values.len(),
374 }
375 }
376
377 fn get(self, index: usize) -> f64 {
378 match self {
379 Self::F32(values) => f64::from(values[index]),
380 Self::F64(values) => values[index],
381 }
382 }
383
384 fn first_non_finite(self) -> Option<(usize, f64)> {
385 (0..self.len())
386 .map(|index| (index, self.get(index)))
387 .find(|(_, value)| !value.is_finite())
388 }
389}
390
391#[derive(Clone, Copy)]
392struct CandidateField<'a> {
393 name: &'static str,
394 rows: usize,
395 columns: usize,
396 real: CandidateComponent<'a>,
397 imaginary: CandidateComponent<'a>,
398}
399
400fn limit(tolerance: ScalarTolerance, reference: f64, candidate: f64) -> f64 {
401 tolerance.absolute + tolerance.relative * reference.abs().max(candidate.abs())
402}
403
404fn wrapped_phase_error(left: f64, right: f64) -> f64 {
405 let difference = (left - right).abs().rem_euclid(2.0 * PI);
406 difference.min(2.0 * PI - difference)
407}
408
409#[derive(Clone, Copy, Debug)]
410struct ObservedMaximum {
411 value: DifferentialMaximum,
412 initialized: bool,
413}
414
415impl Default for ObservedMaximum {
416 fn default() -> Self {
417 Self {
418 value: DifferentialMaximum {
419 error: 0.0,
420 limit: 0.0,
421 row: 0,
422 column: 0,
423 },
424 initialized: false,
425 }
426 }
427}
428
429impl ObservedMaximum {
430 fn observe(&mut self, error: f64, limit: f64, row: usize, column: usize) {
431 let candidate = DifferentialMaximum {
432 error,
433 limit,
434 row,
435 column,
436 };
437 if !self.initialized
438 || candidate.error > self.value.error
439 || (candidate.error == self.value.error && candidate.ratio() > self.value.ratio())
440 {
441 self.value = candidate;
442 self.initialized = true;
443 }
444 }
445}
446
447#[derive(Default)]
448struct Maxima {
449 real: ObservedMaximum,
450 imaginary: ObservedMaximum,
451 amplitude: ObservedMaximum,
452 phase: ObservedMaximum,
453 magnitude_squared: ObservedMaximum,
454 phase_cells: usize,
455 worst: Option<(ConformanceMetric, DifferentialMaximum)>,
456}
457
458impl Maxima {
459 fn observe(
460 &mut self,
461 metric: ConformanceMetric,
462 error: f64,
463 limit: f64,
464 row: usize,
465 column: usize,
466 ) {
467 let candidate = DifferentialMaximum {
468 error,
469 limit,
470 row,
471 column,
472 };
473 match metric {
474 ConformanceMetric::Real => self.real.observe(error, limit, row, column),
475 ConformanceMetric::Imaginary => self.imaginary.observe(error, limit, row, column),
476 ConformanceMetric::Amplitude => self.amplitude.observe(error, limit, row, column),
477 ConformanceMetric::Phase => self.phase.observe(error, limit, row, column),
478 ConformanceMetric::MagnitudeSquared => {
479 self.magnitude_squared.observe(error, limit, row, column);
480 }
481 }
482 if self
483 .worst
484 .is_none_or(|(_, current)| candidate.ratio() > current.ratio())
485 {
486 self.worst = Some((metric, candidate));
487 }
488 }
489
490 fn finish(self) -> DifferentialReport {
491 let phase = (self.phase_cells != 0).then_some(self.phase.value);
492 let (worst_metric, worst) = self
493 .worst
494 .expect("a valid interference field always has at least one cell");
495 DifferentialReport {
496 real: self.real.value,
497 imaginary: self.imaginary.value,
498 amplitude: self.amplitude.value,
499 phase,
500 magnitude_squared: self.magnitude_squared.value,
501 phase_cells: self.phase_cells,
502 worst_metric,
503 worst_row: worst.row,
504 worst_column: worst.column,
505 }
506 }
507}