Skip to main content

fpm_rs/experiment/
led_array.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::{Error, Result};
4
5use super::{IlluminationSource, KVector, Optics};
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
8pub struct LEDArray {
9    pub grid_shape: (usize, usize),
10    pub pitch: f64,
11    pub distance: f64,
12    /// LED-grid coordinate that lies on the optical axis, `(column, row)`.
13    pub center: (f64, f64),
14    pub wavelength_override: Option<f64>,
15    pub illumination_order: Option<Vec<usize>>,
16    pub intensity_weights: Option<Vec<f64>>,
17    pub rotation_radians: f64,
18}
19
20impl Default for LEDArray {
21    fn default() -> Self {
22        Self {
23            grid_shape: (1, 1),
24            pitch: 4e-3,
25            distance: 90e-3,
26            center: (0.0, 0.0),
27            wavelength_override: None,
28            illumination_order: None,
29            intensity_weights: None,
30            rotation_radians: 0.0,
31        }
32    }
33}
34
35impl LEDArray {
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    pub fn grid_shape(mut self, shape: (usize, usize)) -> Self {
41        self.grid_shape = shape;
42        self
43    }
44
45    pub fn pitch(mut self, pitch: f64) -> Self {
46        self.pitch = pitch;
47        self
48    }
49
50    pub fn distance(mut self, distance: f64) -> Self {
51        self.distance = distance;
52        self
53    }
54
55    pub fn center(mut self, center: (f64, f64)) -> Self {
56        self.center = center;
57        self
58    }
59
60    pub fn wavelength_override(mut self, wavelength: f64) -> Self {
61        self.wavelength_override = Some(wavelength);
62        self
63    }
64
65    pub fn illumination_order(mut self, order: Vec<usize>) -> Self {
66        self.illumination_order = Some(order);
67        self
68    }
69
70    pub fn intensity_weights(mut self, weights: Vec<f64>) -> Self {
71        self.intensity_weights = Some(weights);
72        self
73    }
74
75    pub fn rotation_deg(mut self, degrees: f64) -> Self {
76        self.rotation_radians = degrees.to_radians();
77        self
78    }
79
80    pub fn validate(&self) -> Result<()> {
81        let count = self
82            .grid_shape
83            .0
84            .checked_mul(self.grid_shape.1)
85            .ok_or_else(|| Error::InvalidParameter {
86                name: "grid_shape",
87                reason: "LED count overflows".into(),
88            })?;
89        if count == 0 {
90            return Err(Error::InvalidParameter {
91                name: "grid_shape",
92                reason: "dimensions must be non-zero".into(),
93            });
94        }
95        for (name, value) in [("pitch", self.pitch), ("distance", self.distance)] {
96            if !value.is_finite() || value <= 0.0 {
97                return Err(Error::InvalidParameter {
98                    name,
99                    reason: "must be finite and positive".into(),
100                });
101            }
102        }
103        if !self.center.0.is_finite()
104            || !self.center.1.is_finite()
105            || !self.rotation_radians.is_finite()
106        {
107            return Err(Error::InvalidParameter {
108                name: "LED geometry",
109                reason: "center and rotation must be finite".into(),
110            });
111        }
112        if let Some(order) = &self.illumination_order {
113            if order.len() != count || order.iter().any(|&index| index >= count) {
114                return Err(Error::InvalidParameter {
115                    name: "illumination_order",
116                    reason: format!("must contain {count} valid indices"),
117                });
118            }
119            let mut sorted = order.clone();
120            sorted.sort_unstable();
121            sorted.dedup();
122            if sorted.len() != count {
123                return Err(Error::InvalidParameter {
124                    name: "illumination_order",
125                    reason: "indices must form a permutation".into(),
126                });
127            }
128        }
129        if self.intensity_weights.as_ref().is_some_and(|weights| {
130            weights.len() != count
131                || weights
132                    .iter()
133                    .any(|value| !value.is_finite() || *value <= 0.0)
134        }) {
135            return Err(Error::InvalidParameter {
136                name: "intensity_weights",
137                reason: format!("must contain {count} finite positive values"),
138            });
139        }
140        Ok(())
141    }
142}
143
144impl IlluminationSource for LEDArray {
145    fn k_vectors(&self, optics: &Optics) -> Result<Vec<KVector>> {
146        self.validate()?;
147        optics.validate()?;
148        let wavelength = self.wavelength_override.unwrap_or(optics.wavelength);
149        if !wavelength.is_finite() || wavelength <= 0.0 {
150            return Err(Error::InvalidParameter {
151                name: "wavelength_override",
152                reason: "must be finite and positive".into(),
153            });
154        }
155        let wavenumber = std::f64::consts::TAU * optics.medium_index / wavelength;
156        let (sin_rotation, cos_rotation) = self.rotation_radians.sin_cos();
157        let mut natural = Vec::with_capacity(self.grid_shape.0 * self.grid_shape.1);
158        for row in 0..self.grid_shape.0 {
159            for column in 0..self.grid_shape.1 {
160                let x = (column as f64 - self.center.0) * self.pitch;
161                let y = (row as f64 - self.center.1) * self.pitch;
162                let rotated_x = cos_rotation * x - sin_rotation * y;
163                let rotated_y = sin_rotation * x + cos_rotation * y;
164                let distance =
165                    (rotated_x * rotated_x + rotated_y * rotated_y + self.distance * self.distance)
166                        .sqrt();
167                natural.push(KVector::new(
168                    wavenumber * rotated_x / distance,
169                    wavenumber * rotated_y / distance,
170                ));
171            }
172        }
173        Ok(if let Some(order) = &self.illumination_order {
174            order.iter().map(|&index| natural[index]).collect()
175        } else {
176            natural
177        })
178    }
179
180    fn frame_gains(&self) -> Result<Option<Vec<f64>>> {
181        self.validate()?;
182        Ok(self.intensity_weights.as_ref().map(|weights| {
183            self.illumination_order.as_ref().map_or_else(
184                || weights.clone(),
185                |order| order.iter().map(|&index| weights[index]).collect(),
186            )
187        }))
188    }
189}