Skip to main content

fpm_rs/experiment/
illumination.rs

1use serde::{Deserialize, Serialize};
2
3use crate::{Result, error::Error};
4
5use super::{LEDArray, LEDSphere, Optics, RotatingLEDArc, SphericalLEDArm};
6
7pub type SourceWeight = (usize, f64);
8pub type MultiplexingMatrix = Vec<Vec<SourceWeight>>;
9
10/// Transverse illumination wave vector in radians per metre.
11#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
12pub struct KVector {
13    pub kx: f64,
14    pub ky: f64,
15}
16
17impl KVector {
18    pub fn new(kx: f64, ky: f64) -> Self {
19        Self { kx, ky }
20    }
21}
22
23pub trait IlluminationSource {
24    fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>>;
25
26    /// Optional multiplicative intensity gain for each compiled frame.
27    fn frame_gains(&self) -> Result<Option<Vec<f64>>> {
28        Ok(None)
29    }
30
31    /// Optional incoherent source weights for each measured frame.
32    fn multiplexing_matrix(&self) -> Result<Option<MultiplexingMatrix>> {
33        Ok(None)
34    }
35}
36
37#[derive(Clone, Debug, Serialize, Deserialize)]
38pub struct AngleList {
39    /// Illumination angles `(theta_x, theta_y)` in radians.
40    pub angles: Vec<(f64, f64)>,
41}
42
43impl AngleList {
44    pub fn new(angles: Vec<(f64, f64)>) -> Self {
45        Self { angles }
46    }
47}
48
49impl IlluminationSource for AngleList {
50    fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>> {
51        optics.validate()?;
52        let k = optics.medium_wavenumber();
53        let vectors = self
54            .angles
55            .iter()
56            .map(|&(theta_x, theta_y)| {
57                if !theta_x.is_finite()
58                    || !theta_y.is_finite()
59                    || theta_x.abs() > std::f64::consts::FRAC_PI_2
60                    || theta_y.abs() > std::f64::consts::FRAC_PI_2
61                {
62                    return Err(Error::InvalidParameter {
63                        name: "illumination angles",
64                        reason: "angles must be finite and between -pi/2 and pi/2".into(),
65                    });
66                }
67                Ok(KVector::new(k * theta_x.sin(), k * theta_y.sin()))
68            })
69            .collect::<Result<Vec<_>>>()?;
70        validate_k_vectors(&vectors, optics)?;
71        Ok(vectors)
72    }
73}
74
75/// Reserved representation for known linear combinations of source frames.
76#[derive(Clone, Debug, Default, Serialize, Deserialize)]
77pub struct CodedIllumination {
78    pub source_k_vectors: Vec<KVector>,
79    pub frame_weights: MultiplexingMatrix,
80}
81
82impl CodedIllumination {
83    pub fn validate(&self, optics: &Optics) -> Result<()> {
84        validate_k_vectors(&self.source_k_vectors, optics)?;
85        validate_multiplexing_matrix(&self.frame_weights, self.source_k_vectors.len())
86    }
87}
88
89#[derive(Clone, Debug, Serialize, Deserialize)]
90pub enum Illumination {
91    KVectors(Vec<KVector>),
92    Angles(AngleList),
93    LEDArray(LEDArray),
94    LEDSphere(LEDSphere),
95    SphericalLEDArm(SphericalLEDArm),
96    RotatingLEDArc(RotatingLEDArc),
97    Coded(CodedIllumination),
98    /// Imported/calibrated source vectors with optional per-frame gains and
99    /// optional incoherent multiplexing rows.
100    Calibrated {
101        k_vectors: Vec<KVector>,
102        frame_gains: Option<Vec<f64>>,
103        frame_weights: Option<MultiplexingMatrix>,
104    },
105}
106
107impl IlluminationSource for Illumination {
108    fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>> {
109        match self {
110            Self::KVectors(vectors) => {
111                validate_k_vectors(vectors, optics)?;
112                Ok(vectors.clone())
113            }
114            Self::Angles(angles) => angles.k_vectors(optics),
115            Self::LEDArray(array) => array.k_vectors(optics),
116            Self::LEDSphere(sphere) => sphere.k_vectors(optics),
117            Self::SphericalLEDArm(arm) => arm.k_vectors(optics),
118            Self::RotatingLEDArc(arc) => arc.k_vectors(optics),
119            Self::Coded(coded) => coded.k_vectors(optics),
120            Self::Calibrated {
121                k_vectors,
122                frame_gains,
123                frame_weights,
124            } => {
125                validate_calibrated(
126                    k_vectors,
127                    frame_gains.as_deref(),
128                    frame_weights.as_deref(),
129                    optics,
130                )?;
131                Ok(k_vectors.clone())
132            }
133        }
134    }
135
136    fn frame_gains(&self) -> Result<Option<Vec<f64>>> {
137        match self {
138            Self::LEDArray(array) => array.frame_gains(),
139            Self::LEDSphere(sphere) => sphere.frame_gains(),
140            Self::SphericalLEDArm(arm) => arm.frame_gains(),
141            Self::RotatingLEDArc(arc) => arc.frame_gains(),
142            Self::Calibrated {
143                k_vectors,
144                frame_gains,
145                frame_weights,
146            } => {
147                let frame_count = frame_weights.as_ref().map_or(k_vectors.len(), Vec::len);
148                validate_frame_gains(frame_gains.as_deref(), frame_count)?;
149                Ok(frame_gains.clone())
150            }
151            Self::KVectors(_) | Self::Angles(_) | Self::Coded(_) => Ok(None),
152        }
153    }
154
155    fn multiplexing_matrix(&self) -> Result<Option<MultiplexingMatrix>> {
156        match self {
157            Self::Coded(coded) => coded.multiplexing_matrix(),
158            Self::Calibrated {
159                k_vectors,
160                frame_gains,
161                frame_weights,
162            } => {
163                let frame_count = frame_weights.as_ref().map_or(k_vectors.len(), Vec::len);
164                validate_frame_gains(frame_gains.as_deref(), frame_count)?;
165                if let Some(matrix) = frame_weights {
166                    validate_multiplexing_matrix(matrix, k_vectors.len())?;
167                }
168                Ok(frame_weights.clone())
169            }
170            Self::KVectors(_)
171            | Self::Angles(_)
172            | Self::LEDArray(_)
173            | Self::LEDSphere(_)
174            | Self::SphericalLEDArm(_)
175            | Self::RotatingLEDArc(_) => Ok(None),
176        }
177    }
178}
179
180impl IlluminationSource for CodedIllumination {
181    fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>> {
182        self.validate(optics)?;
183        Ok(self.source_k_vectors.clone())
184    }
185
186    fn multiplexing_matrix(&self) -> Result<Option<MultiplexingMatrix>> {
187        validate_multiplexing_matrix(&self.frame_weights, self.source_k_vectors.len())?;
188        Ok(Some(self.frame_weights.clone()))
189    }
190}
191
192impl IlluminationSource for Vec<KVector> {
193    fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>> {
194        validate_k_vectors(self, optics)?;
195        Ok(self.clone())
196    }
197}
198
199fn validate_k_vectors(vectors: &[KVector], optics: &Optics) -> Result<()> {
200    optics.validate()?;
201    if vectors.is_empty() {
202        return Err(Error::InvalidParameter {
203            name: "k_vectors",
204            reason: "at least one illumination source is required".into(),
205        });
206    }
207    let maximum_transverse = optics.medium_wavenumber() * (1.0 + 128.0 * f64::EPSILON);
208    if vectors.iter().any(|vector| {
209        !vector.kx.is_finite()
210            || !vector.ky.is_finite()
211            || vector.kx.hypot(vector.ky) > maximum_transverse
212    }) {
213        return Err(Error::InvalidParameter {
214            name: "k_vectors",
215            reason: "vectors must be finite propagating transverse wave vectors with magnitude no greater than the medium wavenumber"
216                .into(),
217        });
218    }
219    Ok(())
220}
221
222fn validate_multiplexing_matrix(matrix: &[Vec<SourceWeight>], source_count: usize) -> Result<()> {
223    if matrix.is_empty() {
224        return Err(Error::InvalidParameter {
225            name: "frame_weights",
226            reason: "at least one measured frame is required".into(),
227        });
228    }
229    for (frame, row) in matrix.iter().enumerate() {
230        let mut seen = vec![false; source_count];
231        if row.is_empty()
232            || row.iter().any(|&(source, weight)| {
233                let duplicate = source < source_count && seen[source];
234                if source < source_count {
235                    seen[source] = true;
236                }
237                source >= source_count || !weight.is_finite() || weight <= 0.0 || duplicate
238            })
239        {
240            return Err(Error::InvalidParameter {
241                name: "frame_weights",
242                reason: format!(
243                    "frame {frame} must contain unique valid source indices with finite positive weights"
244                ),
245            });
246        }
247    }
248    Ok(())
249}
250
251fn validate_calibrated(
252    k_vectors: &[KVector],
253    frame_gains: Option<&[f64]>,
254    frame_weights: Option<&[Vec<SourceWeight>]>,
255    optics: &Optics,
256) -> Result<()> {
257    validate_k_vectors(k_vectors, optics)?;
258    if let Some(matrix) = frame_weights {
259        validate_multiplexing_matrix(matrix, k_vectors.len())?;
260    }
261    validate_frame_gains(
262        frame_gains,
263        frame_weights.map_or(k_vectors.len(), |matrix| matrix.len()),
264    )
265}
266
267fn validate_frame_gains(frame_gains: Option<&[f64]>, frame_count: usize) -> Result<()> {
268    if frame_gains.is_some_and(|gains| {
269        gains.len() != frame_count
270            || gains
271                .iter()
272                .any(|value| !value.is_finite() || *value <= 0.0)
273    }) {
274        return Err(Error::InvalidParameter {
275            name: "frame_gains",
276            reason: format!("must contain {frame_count} finite positive values"),
277        });
278    }
279    Ok(())
280}