use crate::error::{StatsError, StatsResult};
use crate::traits::{CircularDistribution, Distribution};
use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use scirs2_core::random::uniform::SampleUniform;
use scirs2_core::random::{rng, Rng};
use std::f64::consts::PI;
use std::fmt::Debug;
use std::marker::PhantomData;
#[derive(Debug, Clone)]
pub struct WrappedCauchy<F: Float> {
pub mu: F,
pub gamma: F,
_phantom: PhantomData<F>,
}
impl<F: Float + SampleUniform + Debug + 'static + std::fmt::Display> WrappedCauchy<F> {
pub fn new(mu: F, gamma: F) -> StatsResult<Self> {
if gamma <= F::zero() || gamma >= F::one() {
return Err(StatsError::InvalidArgument(format!(
"Concentration parameter gamma must be in range (0, 1), got {:?}",
gamma
)));
}
let two_pi = F::from(2.0 * PI).expect("Failed to convert to float");
let normalized_mu = ((mu % two_pi) + two_pi) % two_pi;
Ok(Self {
mu: normalized_mu,
gamma,
_phantom: PhantomData,
})
}
}
impl<F: Float + SampleUniform + Debug + 'static + std::fmt::Display> Distribution<F>
for WrappedCauchy<F>
{
fn mean(&self) -> F {
self.mu
}
fn var(&self) -> F {
F::one() - self.gamma
}
fn std(&self) -> F {
let neg_two = F::from(-2.0).expect("Failed to convert constant to float");
(neg_two * self.gamma.ln()).sqrt()
}
fn rvs(&self, size: usize) -> StatsResult<Array1<F>> {
let mut samples = Array1::zeros(size);
for i in 0..size {
samples[i] = self.rvs_single()?;
}
Ok(samples)
}
fn entropy(&self) -> F {
let two_pi = F::from(2.0 * PI).expect("Failed to convert to float");
(two_pi * (F::one() - self.gamma * self.gamma)).ln()
}
}
impl<F: Float + SampleUniform + Debug + 'static + std::fmt::Display> CircularDistribution<F>
for WrappedCauchy<F>
{
fn pdf(&self, x: F) -> F {
let two_pi = F::from(2.0 * PI).expect("Failed to convert to float");
let one_minus_gamma_sq = F::one() - self.gamma * self.gamma;
let denom = F::one() + self.gamma * self.gamma
- F::from(2.0).expect("Failed to convert constant to float")
* self.gamma
* (x - self.mu).cos();
one_minus_gamma_sq / (two_pi * denom)
}
fn cdf(&self, x: F) -> F {
let two_pi = F::from(2.0 * PI).expect("Failed to convert to float");
let pi = F::from(PI).expect("Failed to convert to float");
let x_norm = ((x % two_pi) + two_pi) % two_pi;
let cr = (F::one() + self.gamma) / (F::one() - self.gamma);
if x_norm < pi {
let half = F::from(0.5).expect("Failed to convert constant to float");
let pi_inv = F::from(1.0 / PI).expect("Failed to convert to float");
pi_inv * (cr * (x_norm * half).tan()).atan()
} else {
let half = F::from(0.5).expect("Failed to convert constant to float");
let pi_inv = F::from(1.0 / PI).expect("Failed to convert to float");
F::one() - pi_inv * (cr * ((two_pi - x_norm) * half).tan()).atan()
}
}
fn rvs_single(&self) -> StatsResult<F> {
let mut rng = scirs2_core::random::thread_rng();
let u: f64 = rng.random();
let u_f64 = F::from(u)
.expect("Failed to convert to float")
.to_f64()
.expect("Operation failed");
let gamma_f64 = self.gamma.to_f64().expect("Operation failed");
let mu_f64 = self.mu.to_f64().expect("Operation failed");
let pi_u = PI * u_f64;
let angle = 2.0_f64 * (pi_u.tan() / gamma_f64).atan();
let result = angle + mu_f64;
let normalized = ((result + PI) % (2.0_f64 * PI)) - PI;
Ok(F::from(normalized).expect("Failed to convert to float"))
}
fn circular_mean(&self) -> F {
self.mu
}
fn circular_variance(&self) -> F {
F::one() - self.mean_resultant_length()
}
fn circular_std(&self) -> F {
let neg_two = F::from(-2.0).expect("Failed to convert constant to float");
(neg_two * self.mean_resultant_length().ln()).sqrt()
}
fn mean_resultant_length(&self) -> F {
self.gamma
}
fn concentration(&self) -> F {
self.gamma
}
}
#[allow(dead_code)]
pub fn wrapped_cauchy<F: Float + SampleUniform + Debug + 'static + std::fmt::Display>(
mu: F,
gamma: F,
) -> StatsResult<WrappedCauchy<F>> {
WrappedCauchy::new(mu, gamma)
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ScientificNumber;
#[test]
fn test_wrapped_cauchy_creation() {
let wc = wrapped_cauchy(0.0f64, 0.5);
assert!(wc.is_ok());
let wc = wrapped_cauchy(0.0f64, 0.0);
assert!(wc.is_err());
let wc = wrapped_cauchy(0.0f64, 1.0);
assert!(wc.is_err());
}
#[test]
fn test_wrapped_cauchy_pdf() {
let wc = wrapped_cauchy(0.0f64, 0.5).expect("Operation failed");
let pdf_at_mean = wc.pdf(0.0);
let expected = (1.0 - 0.5 * 0.5) / (2.0 * PI * (1.0 + 0.5 * 0.5 - 2.0 * 0.5 * 1.0));
assert_abs_diff_eq!(pdf_at_mean, expected, epsilon = 1e-10);
let pdf_plus = wc.pdf(0.5);
let pdf_minus = wc.pdf(-0.5);
assert_abs_diff_eq!(pdf_plus, pdf_minus, epsilon = 1e-10);
}
#[test]
fn test_wrapped_cauchy_cdf() {
let wc = wrapped_cauchy(0.0f64, 0.5).expect("Operation failed");
let cdf_at_mean = wc.cdf(0.0);
assert_abs_diff_eq!(cdf_at_mean, 0.0, epsilon = 1e-10);
let cdf_at_pi = wc.cdf(PI);
assert_abs_diff_eq!(cdf_at_pi, 0.5, epsilon = 1e-10);
let cdf_at_2pi = wc.cdf(2.0 * PI);
let cdf_at_0 = wc.cdf(0.0);
assert_abs_diff_eq!(cdf_at_2pi, cdf_at_0, epsilon = 1e-10);
}
#[test]
fn test_wrapped_cauchy_mean_resultant_length() {
for gamma in [0.1, 0.3, 0.5, 0.7, 0.9] {
let wc = wrapped_cauchy(0.0f64, gamma).expect("Operation failed");
assert_abs_diff_eq!(wc.mean_resultant_length(), gamma, epsilon = 1e-10);
}
}
#[test]
fn test_wrapped_cauchy_rvs() {
let wc = wrapped_cauchy(0.0f64, 0.8).expect("Operation failed");
let samples = wc.rvs(1000).expect("Operation failed");
for &sample in samples.iter() {
let sample_f64 = sample.to_f64().expect("Operation failed");
assert!(sample_f64 >= -PI && sample_f64 <= PI);
}
let sin_sum: f64 = samples
.iter()
.map(|&x| x.to_f64().expect("Operation failed").sin())
.sum();
let cos_sum: f64 = samples
.iter()
.map(|&x| x.to_f64().expect("Operation failed").cos())
.sum();
let circular_mean = sin_sum.atan2(cos_sum);
let deviation = (circular_mean - 0.0).abs();
assert!(
deviation < PI,
"Circular mean deviation {} should be less than π",
deviation
);
}
}