Skip to main content

fpm_rs/
evaluation.rs

1//! Composed evaluation of a reconstruction against reference data.
2
3use ndarray::ArrayView2;
4use num_complex::Complex64;
5use serde::{Deserialize, Serialize};
6
7use crate::{
8    Result,
9    array_layout::checked_len_2d,
10    measurements::MeasurementRead,
11    metrics::{
12        complex_field::{ComplexFieldComparisonMetrics, compare_complex_fields_masked},
13        intensity::{IntensityComparisonMetrics, compare_intensity_u8_masked},
14        model::{
15            FrameGainComparisonMetrics, IlluminationPositionMetrics, PupilComparisonMetrics,
16            compare_frame_gains, compare_illumination_positions, compare_pupils,
17        },
18    },
19    model::{ForwardModel, FourierOffset, ImagePlaneModel},
20    reconstruction::{ReconstructionProblem, ReconstructionResult},
21};
22
23/// Predicted-versus-measured intensity metrics in acquisition-frame order.
24#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct FrameIntensityEvaluation {
26    /// One comparison record per acquisition frame.
27    pub per_frame: Vec<IntensityComparisonMetrics>,
28}
29
30/// Ground-truth comparisons for reconstructed object, model calibration, and frames.
31#[derive(Clone, Debug, Serialize, Deserialize)]
32pub struct ReconstructionEvaluation {
33    /// Complex-field metrics after the documented global alignment.
34    pub object: ComplexFieldComparisonMetrics,
35    /// Optional complex-pupil comparison when a reference model is supplied.
36    pub pupil: Option<PupilComparisonMetrics>,
37    /// Optional source-position comparison in Fourier-grid pixels.
38    pub illumination: Option<IlluminationPositionMetrics>,
39    /// Optional acquisition-frame gain comparison.
40    pub frame_gains: Option<FrameGainComparisonMetrics>,
41    /// Optional predicted-versus-measured intensity comparison.
42    pub intensity: Option<FrameIntensityEvaluation>,
43}
44
45/// Compares a result with a reference complex object and optional reference model.
46///
47/// `valid_object_mask`, when present, is a same-shaped binary `(row, column)` mask.
48pub fn evaluate_reconstruction(
49    result: &ReconstructionResult,
50    reference_object: ArrayView2<'_, Complex64>,
51    reference_model: Option<&ImagePlaneModel>,
52    valid_object_mask: Option<ArrayView2<'_, u8>>,
53) -> Result<ReconstructionEvaluation> {
54    let object =
55        compare_complex_fields_masked(reference_object, result.object.view(), valid_object_mask)?;
56    let (pupil, illumination, frame_gains) = if let Some(reference_model) = reference_model {
57        let pupil = Some(compare_pupils(
58            reference_model.pupil.values(),
59            result.recovered_pupil.values(),
60            reference_model.pupil.support(),
61        )?);
62        let illumination = None;
63        let frame_gains = result
64            .recovered_frame_gains
65            .as_ref()
66            .map(|candidate| {
67                let reference = reference_model.frame_gains.as_deref().unwrap_or(&[]);
68                if reference.is_empty() {
69                    compare_frame_gains(&vec![1.0; reference_model.frame_count()], candidate)
70                } else {
71                    compare_frame_gains(reference, candidate)
72                }
73            })
74            .transpose()?;
75        (pupil, illumination, frame_gains)
76    } else {
77        (None, None, None)
78    };
79    Ok(ReconstructionEvaluation {
80        object,
81        pupil,
82        illumination,
83        frame_gains,
84        intensity: None,
85    })
86}
87
88/// Evaluates ground truth and also predicts every measurement in `problem`.
89pub fn evaluate_reconstruction_with_problem<M: MeasurementRead>(
90    result: &ReconstructionResult,
91    problem: &ReconstructionProblem<M>,
92    reference_object: ArrayView2<'_, Complex64>,
93    reference_model: Option<&ImagePlaneModel>,
94    valid_object_mask: Option<ArrayView2<'_, u8>>,
95) -> Result<ReconstructionEvaluation> {
96    let mut evaluation =
97        evaluate_reconstruction(result, reference_object, reference_model, valid_object_mask)?;
98    problem.validate()?;
99    let intensity = evaluate_frame_intensity(result, problem)?;
100    if let Some(reference_model) = reference_model {
101        evaluation.illumination = Some(compare_illumination_positions(
102            reference_model,
103            &problem.model,
104            result.calibrated_illumination.as_deref(),
105        )?);
106    }
107    evaluation.intensity = Some(intensity);
108    Ok(evaluation)
109}
110
111/// Evaluates predicted result intensities against measured acquisition frames.
112pub fn evaluate_frame_intensity<M: MeasurementRead>(
113    result: &ReconstructionResult,
114    problem: &ReconstructionProblem<M>,
115) -> Result<FrameIntensityEvaluation> {
116    problem.validate()?;
117    let model = model_with_result_calibration(problem, result)?;
118    let forward = ForwardModel::new(&model)?;
119    let mut workspace = forward.workspace()?;
120    let mut candidate = vec![0.0; checked_len_2d(model.image_shape)?];
121    let mut per_frame = Vec::with_capacity(model.frame_count());
122    for frame in 0..model.frame_count() {
123        if problem.measurements.frame_weight(frame)? == 0.0 {
124            per_frame.push(compare_intensity_u8_masked(
125                &candidate, &candidate, None, None,
126            )?);
127            continue;
128        }
129        forward.forward_intensity_into(
130            result.object_spectrum.view(),
131            &result.recovered_pupil,
132            frame,
133            &mut workspace,
134            &mut candidate,
135        )?;
136        let reference = problem.measurements.frame(frame)?;
137        per_frame.push(compare_intensity_u8_masked(
138            &reference,
139            &candidate,
140            problem.measurements.frame_mask(frame)?,
141            None,
142        )?);
143    }
144    Ok(FrameIntensityEvaluation { per_frame })
145}
146
147fn model_with_result_calibration<M: MeasurementRead>(
148    problem: &ReconstructionProblem<M>,
149    result: &ReconstructionResult,
150) -> Result<ImagePlaneModel> {
151    let mut model = problem.model.clone();
152    model.frame_gains = result.recovered_frame_gains.clone();
153    model.background = result.recovered_background.clone();
154    if let Some(corrections) = &result.calibrated_illumination {
155        model.subpixel_offsets = Some(
156            corrections
157                .iter()
158                .enumerate()
159                .map(|(source, &(row, column))| {
160                    let base = problem.model.source_offset(source)?;
161                    Ok(FourierOffset::new(base.row + row, base.column + column))
162                })
163                .collect::<Result<Vec<_>>>()?,
164        );
165    }
166    model.validate()?;
167    Ok(model)
168}