sim_lib_interference_solve/
analysis.rs1use std::fmt;
4
5use sim_lib_interference_core::SamplingCertificate;
6
7use crate::{Observable, ProjectionIdentity, ScalarProjection, ScalarSample};
8
9#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct FieldStats {
12 pub count: usize,
14 pub minimum: f64,
16 pub maximum: f64,
18 pub mean: f64,
20 pub population_variance: f64,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub enum ExtremumKind {
27 NodeCandidate,
29 AntinodeCandidate,
31}
32
33#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct Extremum {
36 pub row: usize,
38 pub column: usize,
40 pub value: f64,
42 pub kind: ExtremumKind,
44}
45
46#[derive(Clone, Debug, PartialEq)]
48pub struct FringeReport {
49 pub sampling: SamplingCertificate,
51 pub projection: ProjectionIdentity,
53 pub stats: FieldStats,
55 pub extrema: Vec<Extremum>,
57 pub michelson_contrast: Option<f64>,
62}
63
64#[derive(Clone, Debug, PartialEq)]
66pub enum AnalysisError {
67 InvalidAmplitudeFloor {
69 value: f64,
71 },
72 IncompatibleObservable {
74 observable: Observable,
76 },
77 MaskedSample {
79 row: usize,
81 column: usize,
83 },
84 NonFiniteStatistic {
86 name: &'static str,
88 value: f64,
90 },
91 AllocationFailed {
93 cells: usize,
95 },
96}
97
98impl fmt::Display for AnalysisError {
99 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 Self::InvalidAmplitudeFloor { value } => write!(
102 formatter,
103 "analysis amplitude floor must be finite and non-negative: {value:?}"
104 ),
105 Self::IncompatibleObservable { observable } => write!(
106 formatter,
107 "fringe analysis requires amplitude or magnitude-squared, not {observable:?}"
108 ),
109 Self::MaskedSample { row, column } => {
110 write!(formatter, "analysis input is masked at ({row}, {column})")
111 }
112 Self::NonFiniteStatistic { name, value } => {
113 write!(
114 formatter,
115 "analysis statistic `{name}` is non-finite: {value:?}"
116 )
117 }
118 Self::AllocationFailed { cells } => {
119 write!(
120 formatter,
121 "could not reserve extrema storage for {cells} cells"
122 )
123 }
124 }
125 }
126}
127
128impl std::error::Error for AnalysisError {}
129
130pub fn analyze_fringes(
139 projection: &ScalarProjection,
140 amplitude_floor: f64,
141) -> Result<FringeReport, AnalysisError> {
142 if !amplitude_floor.is_finite() || amplitude_floor < 0.0 {
143 return Err(AnalysisError::InvalidAmplitudeFloor {
144 value: amplitude_floor,
145 });
146 }
147 let certificate = projection.certificate();
148 let observable = certificate.observable();
149 if !matches!(
150 observable,
151 Observable::Amplitude | Observable::MagnitudeSquared
152 ) {
153 return Err(AnalysisError::IncompatibleObservable { observable });
154 }
155
156 let stats = field_stats(projection)?;
157 let extrema = find_extrema(projection)?;
158 let maximum_amplitude = match observable {
159 Observable::Amplitude => stats.maximum,
160 Observable::MagnitudeSquared => stats.maximum.sqrt(),
161 _ => unreachable!("observable was admitted above"),
162 };
163 let michelson_contrast = if maximum_amplitude <= amplitude_floor {
164 None
165 } else {
166 let minimum_to_maximum = stats.minimum / stats.maximum;
167 let contrast = (1.0 - minimum_to_maximum) / (1.0 + minimum_to_maximum);
168 Some(require_finite("michelson-contrast", contrast)?)
169 };
170
171 Ok(FringeReport {
172 sampling: certificate.source_sampling_certificate(),
173 projection: certificate.identity(),
174 stats,
175 extrema,
176 michelson_contrast,
177 })
178}
179
180fn field_stats(projection: &ScalarProjection) -> Result<FieldStats, AnalysisError> {
181 let mut minimum = scalar_value(projection, 0)?;
182 let mut maximum = minimum;
183 let mut mean = 0.0;
184 let mut squared_deviation = 0.0;
185 for index in 0..projection.samples().len() {
186 let value = scalar_value(projection, index)?;
187 minimum = minimum.min(value);
188 maximum = maximum.max(value);
189 let count = (index + 1) as f64;
190 let delta = value - mean;
191 mean += delta / count;
192 squared_deviation += delta * (value - mean);
193 require_finite("mean", mean)?;
194 require_finite("squared-deviation", squared_deviation)?;
195 }
196 let population_variance = require_finite(
197 "population-variance",
198 squared_deviation / projection.samples().len() as f64,
199 )?;
200 Ok(FieldStats {
201 count: projection.samples().len(),
202 minimum,
203 maximum,
204 mean,
205 population_variance,
206 })
207}
208
209fn find_extrema(projection: &ScalarProjection) -> Result<Vec<Extremum>, AnalysisError> {
210 let rows = projection.rows();
211 let columns = projection.columns();
212 let mut extrema = Vec::new();
213 extrema
214 .try_reserve(projection.samples().len())
215 .map_err(|_| AnalysisError::AllocationFailed {
216 cells: projection.samples().len(),
217 })?;
218 for row in 0..rows {
219 for column in 0..columns {
220 let value = scalar_value(projection, row * columns + column)?;
221 let mut has_neighbour = false;
222 let mut below_all = true;
223 let mut above_all = true;
224 for neighbour_row in row.saturating_sub(1)..=(row + 1).min(rows - 1) {
225 for neighbour_column in column.saturating_sub(1)..=(column + 1).min(columns - 1) {
226 if neighbour_row == row && neighbour_column == column {
227 continue;
228 }
229 has_neighbour = true;
230 let neighbour =
231 scalar_value(projection, neighbour_row * columns + neighbour_column)?;
232 below_all &= value < neighbour;
233 above_all &= value > neighbour;
234 }
235 }
236 let kind = match (has_neighbour && below_all, has_neighbour && above_all) {
237 (true, false) => Some(ExtremumKind::NodeCandidate),
238 (false, true) => Some(ExtremumKind::AntinodeCandidate),
239 _ => None,
240 };
241 if let Some(kind) = kind {
242 extrema.push(Extremum {
243 row,
244 column,
245 value,
246 kind,
247 });
248 }
249 }
250 }
251 Ok(extrema)
252}
253
254fn scalar_value(projection: &ScalarProjection, index: usize) -> Result<f64, AnalysisError> {
255 match projection.samples()[index] {
256 ScalarSample::Value(value) => Ok(value),
257 ScalarSample::Masked => Err(AnalysisError::MaskedSample {
258 row: index / projection.columns(),
259 column: index % projection.columns(),
260 }),
261 }
262}
263
264fn require_finite(name: &'static str, value: f64) -> Result<f64, AnalysisError> {
265 value
266 .is_finite()
267 .then_some(value)
268 .ok_or(AnalysisError::NonFiniteStatistic { name, value })
269}
270
271#[cfg(test)]
272#[path = "analysis_tests.rs"]
273mod tests;