Skip to main content

fpm_rs/model/
sampling.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::{Error, Result};
4
5/// Storage and sign convention relating physical wave vectors to Fourier indices.
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
7pub enum CoordinateConvention {
8    /// Spectra are stored with zero frequency at the array centre. Positive
9    /// illumination k shifts the crop centre toward increasing array indices.
10    CenteredPositiveK,
11}
12
13/// Physical sampling metadata for low- and high-resolution grids.
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub struct Sampling {
16    /// Sample-plane low-resolution pixel pitch in metres.
17    pub low_res_pixel_size: f64,
18    /// Sample-plane high-resolution reconstruction pixel pitch in metres.
19    pub high_res_pixel_size: f64,
20    /// Fourier angular-frequency spacing along columns (`x`), in radians per metre.
21    pub dkx: f64,
22    /// Fourier angular-frequency spacing along rows (`y`), in radians per metre.
23    pub dky: f64,
24    /// Optional vacuum illumination wavelength in metres.
25    pub wavelength: Option<f64>,
26    /// Optional dimensionless synthetic numerical aperture.
27    pub synthetic_na: Option<f64>,
28    /// Convention used to store zero frequency and map positive wave vectors.
29    pub coordinate_convention: CoordinateConvention,
30}
31
32impl Sampling {
33    /// Creates centered-positive-k sampling from positive finite SI spacings.
34    pub fn new(
35        low_res_pixel_size: f64,
36        high_res_pixel_size: f64,
37        dkx: f64,
38        dky: f64,
39    ) -> Result<Self> {
40        let sampling = Self {
41            low_res_pixel_size,
42            high_res_pixel_size,
43            dkx,
44            dky,
45            wavelength: None,
46            synthetic_na: None,
47            coordinate_convention: CoordinateConvention::CenteredPositiveK,
48        };
49        sampling.validate()?;
50        Ok(sampling)
51    }
52
53    /// Checks positive finite pixel/frequency spacings and positive finite optional
54    /// wavelength and synthetic numerical aperture.
55    pub fn validate(&self) -> Result<()> {
56        for (name, value) in [
57            ("low_res_pixel_size", self.low_res_pixel_size),
58            ("high_res_pixel_size", self.high_res_pixel_size),
59            ("dkx", self.dkx),
60            ("dky", self.dky),
61        ] {
62            if !value.is_finite() || value <= 0.0 {
63                return Err(Error::InvalidParameter {
64                    name,
65                    reason: "must be finite and positive".into(),
66                });
67            }
68        }
69        if self.high_res_pixel_size > self.low_res_pixel_size {
70            return Err(Error::InvalidParameter {
71                name: "high_res_pixel_size",
72                reason: "must not exceed low-resolution pixel size".into(),
73            });
74        }
75        if self
76            .wavelength
77            .is_some_and(|value| !value.is_finite() || value <= 0.0)
78        {
79            return Err(Error::InvalidParameter {
80                name: "wavelength",
81                reason: "must be finite and positive when present".into(),
82            });
83        }
84        if self
85            .synthetic_na
86            .is_some_and(|value| !value.is_finite() || value <= 0.0)
87        {
88            return Err(Error::InvalidParameter {
89                name: "synthetic_na",
90                reason: "must be finite and positive when present".into(),
91            });
92        }
93        Ok(())
94    }
95}