1use std::collections::BTreeMap;
12
13use serde::{Deserialize, Serialize};
14
15use crate::{Result, error::Error};
16
17use super::{Optics, PlanarLedArray, RotatingLedArc, SphericalLedArm, SphericalLedArray};
18
19pub type SourceWeight = (usize, f64);
21pub type MultiplexingMatrix = Vec<Vec<SourceWeight>>;
23
24#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct KVector {
32 pub kx: f64,
34 pub ky: f64,
36}
37
38impl KVector {
39 pub const fn new(kx: f64, ky: f64) -> Self {
41 Self { kx, ky }
42 }
43}
44
45#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct SourcePositionList {
49 positions_m: Vec<[f64; 3]>,
50}
51
52impl SourcePositionList {
53 pub fn new(positions_m: Vec<[f64; 3]>) -> Self {
55 Self { positions_m }
56 }
57
58 pub fn source_count(&self) -> usize {
60 self.positions_m.len()
61 }
62
63 pub fn positions_m(&self) -> &[[f64; 3]] {
65 &self.positions_m
66 }
67
68 pub fn resolve(&self, optics: &Optics) -> Result<ResolvedSources> {
70 ResolvedSources::from_positions(self.positions_m.clone(), optics)
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80pub struct DirectionList {
81 unit_vectors: Vec<[f64; 3]>,
82}
83
84impl DirectionList {
85 pub fn from_unit_vectors(unit_vectors: Vec<[f64; 3]>) -> Result<Self> {
87 Self::from_vectors(unit_vectors, false)
88 }
89
90 pub fn from_vectors(mut vectors: Vec<[f64; 3]>, normalize: bool) -> Result<Self> {
92 if vectors.is_empty() {
93 return Err(invalid(
94 "unit_vectors",
95 "at least one direction is required",
96 ));
97 }
98 for vector in &mut vectors {
99 if vector.iter().any(|value| !value.is_finite()) {
100 return Err(invalid("unit_vectors", "components must be finite"));
101 }
102 let norm = vector[0].hypot(vector[1]).hypot(vector[2]);
103 if !norm.is_finite() || norm <= 0.0 {
104 return Err(invalid(
105 "unit_vectors",
106 "vectors must have non-zero finite norm",
107 ));
108 }
109 if normalize {
110 for component in vector.iter_mut() {
111 *component /= norm;
112 }
113 } else if (norm - 1.0).abs() > 128.0 * f64::EPSILON {
114 return Err(invalid("unit_vectors", "vectors must be normalized"));
115 }
116 if vector[2] < -128.0 * f64::EPSILON {
117 return Err(invalid(
118 "unit_vectors",
119 "directions must lie in the positive-z illumination hemisphere",
120 ));
121 }
122 vector[2] = vector[2].max(0.0);
123 }
124 Ok(Self {
125 unit_vectors: vectors,
126 })
127 }
128
129 pub fn from_direction_cosines(values: Vec<[f64; 2]>) -> Result<Self> {
131 Self::from_transverse_components(values, |value| value)
132 }
133
134 pub fn from_component_angles_radians(values: Vec<[f64; 2]>) -> Result<Self> {
136 Self::from_transverse_components(values, f64::sin)
137 }
138
139 pub fn from_component_angles_degrees(values: Vec<[f64; 2]>) -> Result<Self> {
141 Self::from_component_angles_radians(
142 values
143 .into_iter()
144 .map(|[x, y]| [x.to_radians(), y.to_radians()])
145 .collect(),
146 )
147 }
148
149 pub fn from_polar_angles_radians(values: Vec<[f64; 2]>) -> Result<Self> {
151 let vectors = values
152 .into_iter()
153 .map(|[theta, phi]| {
154 let (sin_theta, cos_theta) = theta.sin_cos();
155 let (sin_phi, cos_phi) = phi.sin_cos();
156 [sin_theta * cos_phi, sin_theta * sin_phi, cos_theta]
157 })
158 .collect();
159 Self::from_unit_vectors(vectors)
160 }
161
162 pub fn from_polar_angles_degrees(values: Vec<[f64; 2]>) -> Result<Self> {
164 Self::from_polar_angles_radians(
165 values
166 .into_iter()
167 .map(|[theta, phi]| [theta.to_radians(), phi.to_radians()])
168 .collect(),
169 )
170 }
171
172 fn from_transverse_components(values: Vec<[f64; 2]>, map: impl Fn(f64) -> f64) -> Result<Self> {
173 let mut vectors = Vec::with_capacity(values.len());
174 for [x, y] in values {
175 if !x.is_finite() || !y.is_finite() {
176 return Err(invalid("directions", "components must be finite"));
177 }
178 let dx = map(x);
179 let dy = map(y);
180 let transverse_squared = dx.mul_add(dx, dy * dy);
181 if transverse_squared > 1.0 + 128.0 * f64::EPSILON {
182 return Err(invalid(
183 "directions",
184 "squared transverse direction magnitude cannot exceed one",
185 ));
186 }
187 vectors.push([dx, dy, (1.0 - transverse_squared).max(0.0).sqrt()]);
188 }
189 Self::from_unit_vectors(vectors)
190 }
191
192 pub fn source_count(&self) -> usize {
194 self.unit_vectors.len()
195 }
196
197 pub fn unit_vectors(&self) -> &[[f64; 3]] {
199 &self.unit_vectors
200 }
201
202 pub fn direction_cosines(&self) -> Vec<[f64; 2]> {
204 self.unit_vectors.iter().map(|v| [v[0], v[1]]).collect()
205 }
206
207 pub fn component_angles_rad(&self) -> Vec<[f64; 2]> {
209 self.unit_vectors
210 .iter()
211 .map(|v| [v[0].asin(), v[1].asin()])
212 .collect()
213 }
214
215 pub fn component_angles_deg(&self) -> Vec<[f64; 2]> {
217 self.component_angles_rad()
218 .into_iter()
219 .map(|[x, y]| [x.to_degrees(), y.to_degrees()])
220 .collect()
221 }
222
223 pub fn polar_angles_rad(&self) -> Vec<[f64; 2]> {
225 self.unit_vectors
226 .iter()
227 .map(|v| [v[2].clamp(-1.0, 1.0).acos(), v[1].atan2(v[0])])
228 .collect()
229 }
230
231 pub fn polar_angles_deg(&self) -> Vec<[f64; 2]> {
233 self.polar_angles_rad()
234 .into_iter()
235 .map(|[theta, phi]| [theta.to_degrees(), phi.to_degrees()])
236 .collect()
237 }
238
239 pub fn resolve(&self, optics: &Optics) -> Result<ResolvedSources> {
241 optics.validate()?;
242 let k = optics.illumination_wavenumber();
243 let k_vectors = self
244 .unit_vectors
245 .iter()
246 .map(|direction| KVector::new(k * direction[0], k * direction[1]))
247 .collect();
248 Ok(ResolvedSources {
249 directions: self.unit_vectors.clone(),
250 k_vectors,
251 positions_m: None,
252 })
253 }
254}
255
256#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
260#[serde(deny_unknown_fields)]
261pub struct KVectorList {
262 k_vectors: Vec<KVector>,
263}
264
265impl KVectorList {
266 pub fn new(k_vectors: Vec<KVector>) -> Self {
268 Self { k_vectors }
269 }
270
271 pub fn source_count(&self) -> usize {
273 self.k_vectors.len()
274 }
275
276 pub fn k_vectors(&self) -> &[KVector] {
278 &self.k_vectors
279 }
280
281 pub fn resolve(&self, optics: &Optics) -> Result<ResolvedSources> {
283 validate_k_vectors(&self.k_vectors, optics)?;
284 let k = optics.illumination_wavenumber();
285 let directions = self
286 .k_vectors
287 .iter()
288 .map(|vector| {
289 let dx = vector.kx / k;
290 let dy = vector.ky / k;
291 [dx, dy, (1.0 - dx.mul_add(dx, dy * dy)).max(0.0).sqrt()]
292 })
293 .collect();
294 Ok(ResolvedSources {
295 directions,
296 k_vectors: self.k_vectors.clone(),
297 positions_m: None,
298 })
299 }
300}
301
302#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
304#[serde(tag = "kind", rename_all = "snake_case")]
305pub enum SourceGeometry {
306 #[serde(rename = "planar_led_array")]
308 PlanarArray(PlanarLedArray),
309 #[serde(rename = "spherical_led_array")]
311 SphericalArray(SphericalLedArray),
312 #[serde(rename = "spherical_led_arm")]
314 SphericalArm(SphericalLedArm),
315 #[serde(rename = "rotating_led_arc")]
317 RotatingArc(RotatingLedArc),
318 #[serde(rename = "source_position_list")]
320 Positions(SourcePositionList),
321 #[serde(rename = "direction_list")]
323 Directions(DirectionList),
324 #[serde(rename = "k_vector_list")]
326 KVectors(KVectorList),
327}
328
329impl SourceGeometry {
330 pub fn source_count(&self) -> usize {
332 match self {
333 Self::PlanarArray(value) => value.source_count(),
334 Self::SphericalArray(value) => value.source_count(),
335 Self::SphericalArm(value) => value.source_count(),
336 Self::RotatingArc(value) => value.source_count(),
337 Self::Positions(value) => value.source_count(),
338 Self::Directions(value) => value.source_count(),
339 Self::KVectors(value) => value.source_count(),
340 }
341 }
342
343 pub fn resolve(&self, optics: &Optics) -> Result<ResolvedSources> {
345 match self {
346 Self::PlanarArray(value) => value.resolve(optics),
347 Self::SphericalArray(value) => value.resolve(optics),
348 Self::SphericalArm(value) => value.resolve(optics),
349 Self::RotatingArc(value) => value.resolve(optics),
350 Self::Positions(value) => value.resolve(optics),
351 Self::Directions(value) => value.resolve(optics),
352 Self::KVectors(value) => value.resolve(optics),
353 }
354 }
355}
356
357macro_rules! geometry_from {
358 ($type:ty, $variant:ident) => {
359 impl From<$type> for SourceGeometry {
360 fn from(value: $type) -> Self {
361 Self::$variant(value)
362 }
363 }
364 };
365}
366
367geometry_from!(PlanarLedArray, PlanarArray);
368geometry_from!(SphericalLedArray, SphericalArray);
369geometry_from!(SphericalLedArm, SphericalArm);
370geometry_from!(RotatingLedArc, RotatingArc);
371geometry_from!(SourcePositionList, Positions);
372geometry_from!(DirectionList, Directions);
373geometry_from!(KVectorList, KVectors);
374
375#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
377#[serde(deny_unknown_fields)]
378pub struct ResolvedSources {
379 directions: Vec<[f64; 3]>,
380 k_vectors: Vec<KVector>,
381 positions_m: Option<Vec<[f64; 3]>>,
382}
383
384impl ResolvedSources {
385 pub(crate) fn from_positions(positions_m: Vec<[f64; 3]>, optics: &Optics) -> Result<Self> {
386 optics.validate()?;
387 if positions_m.is_empty() {
388 return Err(invalid(
389 "positions_m",
390 "at least one source position is required",
391 ));
392 }
393 let mut directions = Vec::with_capacity(positions_m.len());
394 for position in &positions_m {
395 if position.iter().any(|value| !value.is_finite()) {
396 return Err(invalid("positions_m", "position components must be finite"));
397 }
398 let norm = position[0].hypot(position[1]).hypot(position[2]);
399 if !norm.is_finite() || norm <= 0.0 {
400 return Err(invalid(
401 "positions_m",
402 "no source may coincide with the sample origin",
403 ));
404 }
405 directions.push([
406 -position[0] / norm,
407 -position[1] / norm,
408 -position[2] / norm,
409 ]);
410 }
411 let k = optics.illumination_wavenumber();
412 let k_vectors = directions
413 .iter()
414 .map(|direction| KVector::new(k * direction[0], k * direction[1]))
415 .collect();
416 Ok(Self {
417 directions,
418 k_vectors,
419 positions_m: Some(positions_m),
420 })
421 }
422
423 pub fn source_count(&self) -> usize {
425 self.k_vectors.len()
426 }
427
428 pub fn directions(&self) -> &[[f64; 3]] {
430 &self.directions
431 }
432
433 pub fn k_vectors(&self) -> &[KVector] {
435 &self.k_vectors
436 }
437
438 pub fn positions_m(&self) -> Option<&[[f64; 3]]> {
440 self.positions_m.as_deref()
441 }
442}
443
444#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
446#[serde(deny_unknown_fields)]
447pub struct SourceCalibration {
448 relative_power: Option<Vec<f64>>,
449}
450
451impl SourceCalibration {
452 pub fn new(relative_power: Option<Vec<f64>>) -> Self {
454 Self { relative_power }
455 }
456
457 pub const fn unity() -> Self {
459 Self {
460 relative_power: None,
461 }
462 }
463
464 pub fn relative_power(&self) -> Option<&[f64]> {
466 self.relative_power.as_deref()
467 }
468
469 fn resolve(&self, source_count: usize) -> Result<Vec<f64>> {
470 match &self.relative_power {
471 Some(values)
472 if values.len() != source_count
473 || values
474 .iter()
475 .any(|value| !value.is_finite() || *value < 0.0) =>
476 {
477 Err(invalid(
478 "relative_power",
479 format!("must contain {source_count} finite non-negative values"),
480 ))
481 }
482 Some(values) => Ok(values.clone()),
483 None => Ok(vec![1.0; source_count]),
484 }
485 }
486}
487
488#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
490#[serde(deny_unknown_fields)]
491pub struct SourceContribution {
492 pub source: usize,
494 pub intensity_weight: f64,
496}
497
498impl SourceContribution {
499 pub const fn new(source: usize, intensity_weight: f64) -> Self {
501 Self {
502 source,
503 intensity_weight,
504 }
505 }
506}
507
508#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
510#[serde(deny_unknown_fields)]
511pub struct IlluminationFrame {
512 pub contributions: Vec<SourceContribution>,
514 pub gain: f64,
516}
517
518impl IlluminationFrame {
519 pub fn new(contributions: Vec<SourceContribution>, gain: f64) -> Self {
521 Self {
522 contributions,
523 gain,
524 }
525 }
526}
527
528#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
534#[serde(deny_unknown_fields)]
535pub struct AcquisitionPlan {
536 frames: Vec<IlluminationFrame>,
537}
538
539impl AcquisitionPlan {
540 pub fn all_sources(source_count: usize) -> Result<Self> {
542 Self::sequential((0..source_count).collect())
543 }
544
545 pub fn sequential(order: Vec<usize>) -> Result<Self> {
550 let frames = order
551 .into_iter()
552 .map(|source| IlluminationFrame::new(vec![SourceContribution::new(source, 1.0)], 1.0))
553 .collect();
554 Self::from_sparse(frames)
555 }
556
557 pub fn from_sparse(frames: Vec<IlluminationFrame>) -> Result<Self> {
559 if frames.is_empty() {
560 return Err(invalid(
561 "frames",
562 "at least one acquisition frame is required",
563 ));
564 }
565 let mut canonical = Vec::with_capacity(frames.len());
566 for (frame_index, frame) in frames.into_iter().enumerate() {
567 if !frame.gain.is_finite() || frame.gain < 0.0 {
568 return Err(invalid(
569 "gain",
570 "frame gains must be finite and non-negative",
571 ));
572 }
573 let mut merged = BTreeMap::<usize, f64>::new();
574 for contribution in frame.contributions {
575 if !contribution.intensity_weight.is_finite() || contribution.intensity_weight < 0.0
576 {
577 return Err(invalid(
578 "intensity_weight",
579 "source weights must be finite and non-negative",
580 ));
581 }
582 if contribution.intensity_weight != 0.0 {
583 let value = merged.entry(contribution.source).or_insert(0.0);
584 *value += contribution.intensity_weight;
585 if !value.is_finite() {
586 return Err(invalid("intensity_weight", "merged weight is not finite"));
587 }
588 }
589 }
590 if merged.is_empty() {
591 return Err(invalid(
592 "contributions",
593 format!("frame {frame_index} is empty after canonicalization"),
594 ));
595 }
596 canonical.push(IlluminationFrame {
597 contributions: merged
598 .into_iter()
599 .map(|(source, intensity_weight)| {
600 SourceContribution::new(source, intensity_weight)
601 })
602 .collect(),
603 gain: frame.gain,
604 });
605 }
606 Ok(Self { frames: canonical })
607 }
608
609 pub fn from_dense(weights: Vec<Vec<f64>>) -> Result<Self> {
611 if weights.is_empty() {
612 return Err(invalid("weights", "at least one dense frame is required"));
613 }
614 let source_count = weights[0].len();
615 if source_count == 0 || weights.iter().any(|row| row.len() != source_count) {
616 return Err(invalid(
617 "weights",
618 "dense rows must have one common non-zero source count",
619 ));
620 }
621 Self::from_sparse(
622 weights
623 .into_iter()
624 .map(|row| {
625 IlluminationFrame::new(
626 row.into_iter()
627 .enumerate()
628 .map(|(source, weight)| SourceContribution::new(source, weight))
629 .collect(),
630 1.0,
631 )
632 })
633 .collect(),
634 )
635 }
636
637 pub fn frame_count(&self) -> usize {
639 self.frames.len()
640 }
641
642 pub fn frames(&self) -> &[IlluminationFrame] {
644 &self.frames
645 }
646
647 pub fn dense_weights(&self, source_count: usize) -> Result<Vec<Vec<f64>>> {
649 self.validate_indices(source_count)?;
650 let mut dense = vec![vec![0.0; source_count]; self.frames.len()];
651 for (frame_index, frame) in self.frames.iter().enumerate() {
652 for contribution in &frame.contributions {
653 dense[frame_index][contribution.source] = contribution.intensity_weight;
654 }
655 }
656 Ok(dense)
657 }
658
659 fn validate_indices(&self, source_count: usize) -> Result<()> {
660 if self
661 .frames
662 .iter()
663 .flat_map(|frame| &frame.contributions)
664 .any(|contribution| contribution.source >= source_count)
665 {
666 return Err(invalid(
667 "source",
668 format!("source indices must be less than {source_count}"),
669 ));
670 }
671 Ok(())
672 }
673}
674
675#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
677#[serde(deny_unknown_fields)]
678pub struct Illumination {
679 geometry: SourceGeometry,
680 calibration: SourceCalibration,
681 acquisition: AcquisitionPlan,
682}
683
684impl Illumination {
685 pub const fn new(
687 geometry: SourceGeometry,
688 calibration: SourceCalibration,
689 acquisition: AcquisitionPlan,
690 ) -> Self {
691 Self {
692 geometry,
693 calibration,
694 acquisition,
695 }
696 }
697
698 pub fn from_geometry(geometry: impl Into<SourceGeometry>) -> Result<Self> {
700 let geometry = geometry.into();
701 let acquisition = AcquisitionPlan::all_sources(geometry.source_count())?;
702 Ok(Self::new(geometry, SourceCalibration::unity(), acquisition))
703 }
704
705 pub const fn geometry(&self) -> &SourceGeometry {
707 &self.geometry
708 }
709
710 pub const fn calibration(&self) -> &SourceCalibration {
712 &self.calibration
713 }
714
715 pub const fn acquisition(&self) -> &AcquisitionPlan {
717 &self.acquisition
718 }
719
720 pub fn with_geometry(mut self, geometry: impl Into<SourceGeometry>) -> Self {
722 self.geometry = geometry.into();
723 self
724 }
725
726 pub fn with_calibration(mut self, calibration: SourceCalibration) -> Self {
728 self.calibration = calibration;
729 self
730 }
731
732 pub fn with_acquisition(mut self, acquisition: AcquisitionPlan) -> Self {
734 self.acquisition = acquisition;
735 self
736 }
737
738 pub fn resolve(&self, optics: &Optics) -> Result<ResolvedIllumination> {
740 let sources = self.geometry.resolve(optics)?;
741 let source_count = sources.source_count();
742 self.acquisition.validate_indices(source_count)?;
743 let source_power = self.calibration.resolve(source_count)?;
744 let frames = self
745 .acquisition
746 .frames
747 .iter()
748 .map(|frame| ResolvedFrame {
749 contributions: frame.contributions.clone(),
750 gain: frame.gain,
751 })
752 .collect();
753 Ok(ResolvedIllumination {
754 sources,
755 frames,
756 source_power,
757 })
758 }
759}
760
761#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
763#[serde(deny_unknown_fields)]
764pub struct ResolvedFrame {
765 contributions: Vec<SourceContribution>,
766 gain: f64,
767}
768
769impl ResolvedFrame {
770 pub fn contributions(&self) -> &[SourceContribution] {
772 &self.contributions
773 }
774
775 pub const fn gain(&self) -> f64 {
777 self.gain
778 }
779}
780
781#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
783#[serde(deny_unknown_fields)]
784pub struct ResolvedIllumination {
785 sources: ResolvedSources,
786 frames: Vec<ResolvedFrame>,
787 source_power: Vec<f64>,
788}
789
790impl ResolvedIllumination {
791 pub const fn sources(&self) -> &ResolvedSources {
793 &self.sources
794 }
795
796 pub fn source_count(&self) -> usize {
798 self.sources.source_count()
799 }
800
801 pub fn frame_count(&self) -> usize {
803 self.frames.len()
804 }
805
806 pub fn is_multiplexed(&self) -> bool {
808 self.frames
809 .iter()
810 .any(|frame| frame.contributions.len() > 1)
811 }
812
813 pub fn frames(&self) -> &[ResolvedFrame] {
815 &self.frames
816 }
817
818 pub fn source_power(&self) -> &[f64] {
820 &self.source_power
821 }
822
823 pub fn frame_gains(&self) -> Vec<f64> {
825 self.frames.iter().map(|frame| frame.gain).collect()
826 }
827
828 pub fn directions(&self) -> &[[f64; 3]] {
830 self.sources.directions()
831 }
832
833 pub fn positions_m(&self) -> Option<&[[f64; 3]]> {
835 self.sources.positions_m()
836 }
837
838 pub fn k_vectors(&self) -> &[KVector] {
840 self.sources.k_vectors()
841 }
842
843 pub fn dense_weights(&self) -> Vec<Vec<f64>> {
845 let mut dense = vec![vec![0.0; self.source_count()]; self.frame_count()];
846 for (frame_index, frame) in self.frames.iter().enumerate() {
847 for contribution in &frame.contributions {
848 dense[frame_index][contribution.source] = contribution.intensity_weight;
849 }
850 }
851 dense
852 }
853
854 pub(crate) fn compiled_multiplexing_matrix(&self) -> MultiplexingMatrix {
855 self.frames
856 .iter()
857 .map(|frame| {
858 frame
859 .contributions
860 .iter()
861 .map(|contribution| {
862 (
863 contribution.source,
864 contribution.intensity_weight * self.source_power[contribution.source],
865 )
866 })
867 .collect()
868 })
869 .collect()
870 }
871}
872
873fn validate_k_vectors(vectors: &[KVector], optics: &Optics) -> Result<()> {
874 optics.validate()?;
875 if vectors.is_empty() {
876 return Err(invalid("k_vectors", "at least one source is required"));
877 }
878 let maximum_transverse = optics.illumination_wavenumber() * (1.0 + 128.0 * f64::EPSILON);
879 if vectors.iter().any(|vector| {
880 !vector.kx.is_finite()
881 || !vector.ky.is_finite()
882 || vector.kx.hypot(vector.ky) > maximum_transverse
883 }) {
884 return Err(invalid(
885 "k_vectors",
886 "vectors must be finite and propagating at the configured vacuum wavelength and illumination refractive index",
887 ));
888 }
889 Ok(())
890}
891
892fn invalid(name: &'static str, reason: impl Into<String>) -> Error {
893 Error::InvalidParameter {
894 name,
895 reason: reason.into(),
896 }
897}