Skip to main content

fpm_rs/experiment/
optics.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::{Error, Result};
4
5/// Physical microscope parameters in SI units.
6#[derive(Clone, Debug, Serialize, Deserialize)]
7pub struct Optics {
8    pub wavelength: f64,
9    pub objective_na: f64,
10    pub magnification: f64,
11    pub camera_pixel_size: f64,
12    pub medium_index: f64,
13    /// Axial sample displacement from the focal plane in metres.
14    pub defocus_distance: Option<f64>,
15    /// Optional sampled-pupil aberration model.
16    ///
17    /// Coefficients are interpreted by [`crate::model::Pupil::circular`] as
18    /// crate-specific radial-polynomial weights in radians, not as normalized
19    /// Zernike coefficients.
20    pub pupil_aberration: Option<PupilAberration>,
21}
22
23/// Crate-specific pupil phase and amplitude perturbation.
24///
25/// For normalized pupil radius `rho` and polar angle `theta`, the sampled phase
26/// contribution is
27///
28/// `astigmatism * rho^2 * cos(2 theta)
29///  + coma * (3 rho^3 - 2 rho) * cos(theta)
30///  + spherical * (6 rho^4 - 6 rho^2 + 1)`.
31///
32/// These are direct radian coefficients used by [`crate::model::Pupil::circular`],
33/// not normalized Zernike coefficients. Defocus is stored separately on
34/// [`Optics`] as [`Optics::defocus_distance`].
35#[derive(Clone, Debug, Default, Serialize, Deserialize)]
36pub struct PupilAberration {
37    /// Radian coefficient multiplying `rho^2 * cos(2 theta)`.
38    pub astigmatism: f64,
39    /// Radian coefficient multiplying `(3 rho^3 - 2 rho) * cos(theta)`.
40    pub coma: f64,
41    /// Radian coefficient multiplying `6 rho^4 - 6 rho^2 + 1`.
42    pub spherical: f64,
43    /// Radial pupil-amplitude decay strength. The amplitude at the pupil edge
44    /// is `exp(-edge_apodization)`.
45    pub edge_apodization: f64,
46}
47
48impl PupilAberration {
49    pub fn validate(&self) -> Result<()> {
50        if [
51            self.astigmatism,
52            self.coma,
53            self.spherical,
54            self.edge_apodization,
55        ]
56        .iter()
57        .any(|value| !value.is_finite())
58            || self.edge_apodization < 0.0
59        {
60            return Err(Error::InvalidParameter {
61                name: "pupil_aberration",
62                reason: "phase coefficients must be finite and edge apodization must be finite and non-negative"
63                    .into(),
64            });
65        }
66        Ok(())
67    }
68}
69
70impl Optics {
71    pub fn validate(&self) -> Result<()> {
72        for (name, value) in [
73            ("wavelength", self.wavelength),
74            ("objective_na", self.objective_na),
75            ("magnification", self.magnification),
76            ("camera_pixel_size", self.camera_pixel_size),
77            ("medium_index", self.medium_index),
78        ] {
79            if !value.is_finite() || value <= 0.0 {
80                return Err(Error::InvalidParameter {
81                    name,
82                    reason: format!("must be finite and positive, got {value}"),
83                });
84            }
85        }
86        if self.objective_na > self.medium_index {
87            return Err(Error::InvalidParameter {
88                name: "objective_na",
89                reason: "cannot exceed the medium refractive index".into(),
90            });
91        }
92        if self
93            .defocus_distance
94            .is_some_and(|value| !value.is_finite())
95        {
96            return Err(Error::InvalidParameter {
97                name: "defocus_distance",
98                reason: "must be finite".into(),
99            });
100        }
101        if let Some(aberration) = &self.pupil_aberration {
102            aberration.validate()?;
103        }
104        Ok(())
105    }
106
107    pub fn object_pixel_size(&self) -> f64 {
108        self.camera_pixel_size / self.magnification
109    }
110
111    pub fn medium_wavenumber(&self) -> f64 {
112        std::f64::consts::TAU * self.medium_index / self.wavelength
113    }
114}