use std::fmt;
use sim_lib_interference_core::SamplingCertificate;
use crate::{Observable, ProjectionIdentity, ScalarProjection, ScalarSample};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FieldStats {
pub count: usize,
pub minimum: f64,
pub maximum: f64,
pub mean: f64,
pub population_variance: f64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExtremumKind {
NodeCandidate,
AntinodeCandidate,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Extremum {
pub row: usize,
pub column: usize,
pub value: f64,
pub kind: ExtremumKind,
}
#[derive(Clone, Debug, PartialEq)]
pub struct FringeReport {
pub sampling: SamplingCertificate,
pub projection: ProjectionIdentity,
pub stats: FieldStats,
pub extrema: Vec<Extremum>,
pub michelson_contrast: Option<f64>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum AnalysisError {
InvalidAmplitudeFloor {
value: f64,
},
IncompatibleObservable {
observable: Observable,
},
MaskedSample {
row: usize,
column: usize,
},
NonFiniteStatistic {
name: &'static str,
value: f64,
},
AllocationFailed {
cells: usize,
},
}
impl fmt::Display for AnalysisError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidAmplitudeFloor { value } => write!(
formatter,
"analysis amplitude floor must be finite and non-negative: {value:?}"
),
Self::IncompatibleObservable { observable } => write!(
formatter,
"fringe analysis requires amplitude or magnitude-squared, not {observable:?}"
),
Self::MaskedSample { row, column } => {
write!(formatter, "analysis input is masked at ({row}, {column})")
}
Self::NonFiniteStatistic { name, value } => {
write!(
formatter,
"analysis statistic `{name}` is non-finite: {value:?}"
)
}
Self::AllocationFailed { cells } => {
write!(
formatter,
"could not reserve extrema storage for {cells} cells"
)
}
}
}
}
impl std::error::Error for AnalysisError {}
pub fn analyze_fringes(
projection: &ScalarProjection,
amplitude_floor: f64,
) -> Result<FringeReport, AnalysisError> {
if !amplitude_floor.is_finite() || amplitude_floor < 0.0 {
return Err(AnalysisError::InvalidAmplitudeFloor {
value: amplitude_floor,
});
}
let certificate = projection.certificate();
let observable = certificate.observable();
if !matches!(
observable,
Observable::Amplitude | Observable::MagnitudeSquared
) {
return Err(AnalysisError::IncompatibleObservable { observable });
}
let stats = field_stats(projection)?;
let extrema = find_extrema(projection)?;
let maximum_amplitude = match observable {
Observable::Amplitude => stats.maximum,
Observable::MagnitudeSquared => stats.maximum.sqrt(),
_ => unreachable!("observable was admitted above"),
};
let michelson_contrast = if maximum_amplitude <= amplitude_floor {
None
} else {
let minimum_to_maximum = stats.minimum / stats.maximum;
let contrast = (1.0 - minimum_to_maximum) / (1.0 + minimum_to_maximum);
Some(require_finite("michelson-contrast", contrast)?)
};
Ok(FringeReport {
sampling: certificate.source_sampling_certificate(),
projection: certificate.identity(),
stats,
extrema,
michelson_contrast,
})
}
fn field_stats(projection: &ScalarProjection) -> Result<FieldStats, AnalysisError> {
let mut minimum = scalar_value(projection, 0)?;
let mut maximum = minimum;
let mut mean = 0.0;
let mut squared_deviation = 0.0;
for index in 0..projection.samples().len() {
let value = scalar_value(projection, index)?;
minimum = minimum.min(value);
maximum = maximum.max(value);
let count = (index + 1) as f64;
let delta = value - mean;
mean += delta / count;
squared_deviation += delta * (value - mean);
require_finite("mean", mean)?;
require_finite("squared-deviation", squared_deviation)?;
}
let population_variance = require_finite(
"population-variance",
squared_deviation / projection.samples().len() as f64,
)?;
Ok(FieldStats {
count: projection.samples().len(),
minimum,
maximum,
mean,
population_variance,
})
}
fn find_extrema(projection: &ScalarProjection) -> Result<Vec<Extremum>, AnalysisError> {
let rows = projection.rows();
let columns = projection.columns();
let mut extrema = Vec::new();
extrema
.try_reserve(projection.samples().len())
.map_err(|_| AnalysisError::AllocationFailed {
cells: projection.samples().len(),
})?;
for row in 0..rows {
for column in 0..columns {
let value = scalar_value(projection, row * columns + column)?;
let mut has_neighbour = false;
let mut below_all = true;
let mut above_all = true;
for neighbour_row in row.saturating_sub(1)..=(row + 1).min(rows - 1) {
for neighbour_column in column.saturating_sub(1)..=(column + 1).min(columns - 1) {
if neighbour_row == row && neighbour_column == column {
continue;
}
has_neighbour = true;
let neighbour =
scalar_value(projection, neighbour_row * columns + neighbour_column)?;
below_all &= value < neighbour;
above_all &= value > neighbour;
}
}
let kind = match (has_neighbour && below_all, has_neighbour && above_all) {
(true, false) => Some(ExtremumKind::NodeCandidate),
(false, true) => Some(ExtremumKind::AntinodeCandidate),
_ => None,
};
if let Some(kind) = kind {
extrema.push(Extremum {
row,
column,
value,
kind,
});
}
}
}
Ok(extrema)
}
fn scalar_value(projection: &ScalarProjection, index: usize) -> Result<f64, AnalysisError> {
match projection.samples()[index] {
ScalarSample::Value(value) => Ok(value),
ScalarSample::Masked => Err(AnalysisError::MaskedSample {
row: index / projection.columns(),
column: index % projection.columns(),
}),
}
}
fn require_finite(name: &'static str, value: f64) -> Result<f64, AnalysisError> {
value
.is_finite()
.then_some(value)
.ok_or(AnalysisError::NonFiniteStatistic { name, value })
}
#[cfg(test)]
#[path = "analysis_tests.rs"]
mod tests;