use crate::error::OxiPhotonError;
#[derive(Debug, Clone, PartialEq)]
pub struct StokesVector {
pub s: [f64; 4],
}
impl StokesVector {
pub fn new(s0: f64, s1: f64, s2: f64, s3: f64) -> Result<Self, OxiPhotonError> {
if s0 < -1e-12 {
return Err(OxiPhotonError::InvalidLayer(format!(
"Stokes S0 must be non-negative, got {s0}"
)));
}
let pol_sq = s1 * s1 + s2 * s2 + s3 * s3;
if pol_sq > s0 * s0 + 1e-10 {
return Err(OxiPhotonError::InvalidLayer(format!(
"Stokes vector violates physical realizability: S0²={} < S1²+S2²+S3²={}",
s0 * s0,
pol_sq
)));
}
Ok(Self {
s: [s0, s1, s2, s3],
})
}
#[inline]
fn new_unchecked(s0: f64, s1: f64, s2: f64, s3: f64) -> Self {
Self {
s: [s0, s1, s2, s3],
}
}
#[inline]
pub(crate) fn new_unchecked_pub(s0: f64, s1: f64, s2: f64, s3: f64) -> Self {
Self {
s: [s0, s1, s2, s3],
}
}
pub fn horizontal() -> Self {
Self::new_unchecked(1.0, 1.0, 0.0, 0.0)
}
pub fn vertical() -> Self {
Self::new_unchecked(1.0, -1.0, 0.0, 0.0)
}
pub fn diagonal_p45() -> Self {
Self::new_unchecked(1.0, 0.0, 1.0, 0.0)
}
pub fn diagonal_m45() -> Self {
Self::new_unchecked(1.0, 0.0, -1.0, 0.0)
}
pub fn right_circular() -> Self {
Self::new_unchecked(1.0, 0.0, 0.0, 1.0)
}
pub fn left_circular() -> Self {
Self::new_unchecked(1.0, 0.0, 0.0, -1.0)
}
pub fn unpolarized(intensity: f64) -> Self {
Self::new_unchecked(intensity.max(0.0), 0.0, 0.0, 0.0)
}
pub fn dop(&self) -> f64 {
if self.s[0].abs() < f64::EPSILON {
return 0.0;
}
let pol = (self.s[1] * self.s[1] + self.s[2] * self.s[2] + self.s[3] * self.s[3]).sqrt();
(pol / self.s[0]).min(1.0)
}
pub fn dolp(&self) -> f64 {
if self.s[0].abs() < f64::EPSILON {
return 0.0;
}
let lin = (self.s[1] * self.s[1] + self.s[2] * self.s[2]).sqrt();
(lin / self.s[0]).min(1.0)
}
pub fn docp(&self) -> f64 {
if self.s[0].abs() < f64::EPSILON {
return 0.0;
}
(self.s[3].abs() / self.s[0]).min(1.0)
}
pub fn ellipticity_angle_rad(&self) -> f64 {
let lin = (self.s[1] * self.s[1] + self.s[2] * self.s[2]).sqrt();
0.5 * self.s[3].atan2(lin)
}
pub fn azimuth_rad(&self) -> f64 {
0.5 * self.s[2].atan2(self.s[1])
}
pub fn ellipticity(&self) -> f64 {
let chi = self.ellipticity_angle_rad();
chi.abs().tan().min(1.0)
}
pub fn axial_ratio(&self) -> f64 {
let e = self.ellipticity();
if e.abs() < f64::EPSILON {
f64::INFINITY
} else {
1.0 / e
}
}
#[inline]
pub fn intensity(&self) -> f64 {
self.s[0]
}
pub fn normalize(&self) -> Self {
if self.s[0].abs() < f64::EPSILON {
return self.clone();
}
let inv = 1.0 / self.s[0];
Self::new_unchecked(1.0, self.s[1] * inv, self.s[2] * inv, self.s[3] * inv)
}
pub fn add(&self, other: &StokesVector) -> Self {
Self::new_unchecked(
self.s[0] + other.s[0],
self.s[1] + other.s[1],
self.s[2] + other.s[2],
self.s[3] + other.s[3],
)
}
pub fn polarized_component(&self) -> Self {
let dop = self.dop();
let s0_pol = self.s[0] * dop;
Self::new_unchecked(s0_pol, self.s[1], self.s[2], self.s[3])
}
pub fn unpolarized_component(&self) -> Self {
let dop = self.dop();
let s0_unpol = self.s[0] * (1.0 - dop);
Self::new_unchecked(s0_unpol, 0.0, 0.0, 0.0)
}
pub fn poincare_coords(&self) -> (f64, f64) {
let chi2 = 2.0 * self.ellipticity_angle_rad();
let psi2 = 2.0 * self.azimuth_rad();
(chi2, psi2)
}
}
pub struct PolarizationState;
impl PolarizationState {
pub fn linear(angle_rad: f64) -> StokesVector {
let two_theta = 2.0 * angle_rad;
StokesVector::new_unchecked(1.0, two_theta.cos(), two_theta.sin(), 0.0)
}
pub fn elliptical(azimuth_rad: f64, ellipticity_rad: f64) -> StokesVector {
let two_chi = 2.0 * ellipticity_rad;
let two_psi = 2.0 * azimuth_rad;
let cos2chi = two_chi.cos();
let sin2chi = two_chi.sin();
StokesVector::new_unchecked(
1.0,
cos2chi * two_psi.cos(),
cos2chi * two_psi.sin(),
sin2chi,
)
}
pub fn partial(polarized: StokesVector, dop: f64) -> StokesVector {
let dop = dop.clamp(0.0, 1.0);
let i = polarized.s[0];
let norm = polarized.normalize();
let i_pol = i * dop;
let i_unpol = i * (1.0 - dop);
StokesVector::new_unchecked(
i_pol * norm.s[0] + i_unpol,
i_pol * norm.s[1],
i_pol * norm.s[2],
i_pol * norm.s[3],
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::f64::consts::PI;
const EPS: f64 = 1e-10;
#[test]
fn test_horizontal_stokes() {
let s = StokesVector::horizontal();
assert!((s.s[0] - 1.0).abs() < EPS);
assert!((s.s[1] - 1.0).abs() < EPS);
assert!(s.s[2].abs() < EPS);
assert!(s.s[3].abs() < EPS);
assert!((s.dop() - 1.0).abs() < EPS);
}
#[test]
fn test_right_circular_stokes() {
let s = StokesVector::right_circular();
assert!((s.s[0] - 1.0).abs() < EPS);
assert!(s.s[1].abs() < EPS);
assert!(s.s[2].abs() < EPS);
assert!((s.s[3] - 1.0).abs() < EPS);
assert!((s.docp() - 1.0).abs() < EPS);
assert!(s.dolp() < EPS);
}
#[test]
fn test_unpolarized_dop() {
let s = StokesVector::unpolarized(3.5);
assert!((s.s[0] - 3.5).abs() < EPS);
assert!(s.dop() < EPS);
assert!(s.dolp() < EPS);
assert!(s.docp() < EPS);
}
#[test]
fn test_dop_invariant_to_scaling() {
let s = StokesVector::new(2.0, 2.0, 0.0, 0.0).expect("valid");
assert!((s.dop() - 1.0).abs() < EPS);
let s2 = StokesVector::new(4.0, 4.0, 0.0, 0.0).expect("valid");
assert!((s2.dop() - s.dop()).abs() < EPS);
}
#[test]
fn test_poincare_coords_horizontal() {
let s = StokesVector::horizontal();
let (lat, lon) = s.poincare_coords();
assert!(lat.abs() < EPS);
assert!(lon.abs() < EPS);
}
#[test]
fn test_add_two_unpolarized() {
let a = StokesVector::unpolarized(1.0);
let b = StokesVector::unpolarized(2.0);
let sum = a.add(&b);
assert!((sum.intensity() - 3.0).abs() < EPS);
assert!(sum.dop() < EPS);
}
#[test]
fn test_ellipticity_circular() {
let s = StokesVector::right_circular();
let e = s.ellipticity();
assert!((e - 1.0).abs() < 1e-9, "ellipticity={e}");
}
#[test]
fn test_partial_polarization() {
let pol = StokesVector::horizontal();
let partial = PolarizationState::partial(pol, 0.5);
let dop = partial.dop();
assert!((dop - 0.5).abs() < 1e-10, "DOP={dop}");
}
#[test]
fn test_linear_polarization_state() {
let h = PolarizationState::linear(0.0);
assert!((h.s[1] - 1.0).abs() < EPS);
assert!(h.s[2].abs() < EPS);
let v = PolarizationState::linear(PI / 2.0);
assert!((v.s[1] + 1.0).abs() < 1e-9);
let d = PolarizationState::linear(PI / 4.0);
assert!(d.s[1].abs() < 1e-10);
assert!((d.s[2] - 1.0).abs() < 1e-10);
}
#[test]
fn test_elliptical_state_unit_intensity() {
let s = PolarizationState::elliptical(PI / 6.0, PI / 8.0);
assert!((s.intensity() - 1.0).abs() < EPS);
assert!((s.dop() - 1.0).abs() < EPS);
}
#[test]
fn test_stokes_new_rejects_invalid() {
let res = StokesVector::new(0.5, 1.0, 0.0, 0.0);
assert!(res.is_err());
let res2 = StokesVector::new(-1.0, 0.0, 0.0, 0.0);
assert!(res2.is_err());
}
}