fpm_rs/experiment/
led_array.rs1use 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 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}