use crate::core::scalar::ControlScalar;
use crate::fuzzy::FuzzyError;
fn validate_samples<S: ControlScalar>(samples: &[(S, S)]) -> Result<(), FuzzyError> {
if samples.is_empty() {
return Err(FuzzyError::InsufficientSamples);
}
Ok(())
}
pub fn centroid_of_gravity<S: ControlScalar>(mf_samples: &[(S, S)]) -> Result<S, FuzzyError> {
validate_samples(mf_samples)?;
if mf_samples.len() < 2 {
return Err(FuzzyError::InsufficientSamples);
}
let mut numerator = S::ZERO;
let mut denominator = S::ZERO;
for window in mf_samples.windows(2) {
let (x0, mu0) = window[0];
let (x1, mu1) = window[1];
let dx = x1 - x0;
let area = dx * (mu0 + mu1) * S::HALF;
let sum_mu = mu0 + mu1;
let x_strip_centroid = if sum_mu <= S::ZERO {
(x0 + x1) * S::HALF
} else {
let two = S::TWO;
let three = S::from_f64(3.0);
(x0 * (two * mu0 + mu1) + x1 * (mu0 + two * mu1)) / (three * sum_mu)
};
numerator += area * x_strip_centroid;
denominator += area;
}
if denominator <= S::ZERO {
return Err(FuzzyError::DivisionByZero);
}
Ok(numerator / denominator)
}
pub fn mean_of_maxima<S: ControlScalar>(mf_samples: &[(S, S)]) -> Result<S, FuzzyError> {
validate_samples(mf_samples)?;
let max_mu =
mf_samples
.iter()
.map(|&(_, mu)| mu)
.fold(
S::from_f64(f64::NEG_INFINITY),
|a, b| if b > a { b } else { a },
);
if max_mu <= S::ZERO {
return Err(FuzzyError::DivisionByZero);
}
let threshold = max_mu - S::EPSILON * S::from_f64(1e6);
let mut sum_x = S::ZERO;
let mut count = S::ZERO;
for &(x, mu) in mf_samples {
if mu >= threshold {
sum_x += x;
count += S::ONE;
}
}
if count <= S::ZERO {
return Err(FuzzyError::DivisionByZero);
}
Ok(sum_x / count)
}
pub fn bisector_of_area<S: ControlScalar>(mf_samples: &[(S, S)]) -> Result<S, FuzzyError> {
validate_samples(mf_samples)?;
if mf_samples.len() < 2 {
return Err(FuzzyError::InsufficientSamples);
}
let mut total_area = S::ZERO;
for window in mf_samples.windows(2) {
let (x0, mu0) = window[0];
let (x1, mu1) = window[1];
let dx = x1 - x0;
total_area += dx * (mu0 + mu1) * S::HALF;
}
if total_area <= S::ZERO {
return Err(FuzzyError::DivisionByZero);
}
let half_area = total_area * S::HALF;
let mut cumulative = S::ZERO;
for window in mf_samples.windows(2) {
let (x0, mu0) = window[0];
let (x1, mu1) = window[1];
let dx = x1 - x0;
let seg_area = dx * (mu0 + mu1) * S::HALF;
if cumulative + seg_area >= half_area {
let remaining = half_area - cumulative;
let a_coeff = (mu1 - mu0) / (S::TWO * dx);
let b_coeff = mu0;
let t = if a_coeff.abs() < S::EPSILON * S::from_f64(1e6) {
if b_coeff <= S::ZERO {
dx * S::HALF } else {
remaining / b_coeff
}
} else {
let discriminant = b_coeff * b_coeff + S::from_f64(4.0) * a_coeff * remaining;
if discriminant < S::ZERO {
S::ZERO
} else {
let two_a = S::TWO * a_coeff;
(-b_coeff + discriminant.sqrt()) / two_a
}
};
let t_clamped = t.clamp_val(S::ZERO, dx);
return Ok(x0 + t_clamped);
}
cumulative += seg_area;
}
Ok(mf_samples.last().map(|&(x, _)| x).unwrap_or(S::ZERO))
}
pub fn largest_of_maxima<S: ControlScalar>(mf_samples: &[(S, S)]) -> S {
if mf_samples.is_empty() {
return S::ZERO;
}
let max_mu =
mf_samples
.iter()
.map(|&(_, mu)| mu)
.fold(
S::from_f64(f64::NEG_INFINITY),
|a, b| if b > a { b } else { a },
);
let threshold = max_mu - S::EPSILON * S::from_f64(1e6);
let mut best_x = mf_samples[0].0;
for &(x, mu) in mf_samples {
if mu >= threshold && x > best_x {
best_x = x;
}
}
best_x
}
pub fn smallest_of_maxima<S: ControlScalar>(mf_samples: &[(S, S)]) -> S {
if mf_samples.is_empty() {
return S::ZERO;
}
let max_mu =
mf_samples
.iter()
.map(|&(_, mu)| mu)
.fold(
S::from_f64(f64::NEG_INFINITY),
|a, b| if b > a { b } else { a },
);
let threshold = max_mu - S::EPSILON * S::from_f64(1e6);
let mut best_x = mf_samples
.iter()
.find(|&&(_, mu)| mu >= threshold)
.map(|&(x, _)| x)
.unwrap_or(S::ZERO);
for &(x, mu) in mf_samples {
if mu >= threshold && x < best_x {
best_x = x;
}
}
best_x
}
#[cfg(test)]
mod tests {
use super::*;
fn symmetric_triangle_samples(n: usize) -> Vec<(f64, f64)> {
(0..=n)
.map(|i| {
let x = (i as f64) * 10.0 / (n as f64);
let mu = if x <= 5.0 { x / 5.0 } else { (10.0 - x) / 5.0 };
(x, mu)
})
.collect()
}
#[test]
fn centroid_symmetric_triangle_is_center() {
let samples = symmetric_triangle_samples(1000);
let cog = centroid_of_gravity(&samples).unwrap();
assert!(
(cog - 5.0).abs() < 0.01,
"CoG of symmetric triangle should be ~5.0, got {cog}"
);
}
#[test]
fn centroid_empty_returns_error() {
let samples: Vec<(f64, f64)> = vec![];
assert!(centroid_of_gravity(&samples).is_err());
}
#[test]
fn centroid_all_zero_returns_division_by_zero() {
let samples: Vec<(f64, f64)> = vec![(0.0, 0.0), (1.0, 0.0), (2.0, 0.0)];
assert!(matches!(
centroid_of_gravity(&samples),
Err(FuzzyError::DivisionByZero)
));
}
#[test]
fn mean_of_maxima_flat_top() {
let samples: Vec<(f64, f64)> = vec![(0.0, 0.0), (4.0, 1.0), (6.0, 1.0), (10.0, 0.0)];
let mom = mean_of_maxima(&samples).unwrap();
assert!((mom - 5.0).abs() < 0.01, "MoM should be 5.0, got {mom}");
}
#[test]
fn mean_of_maxima_empty_returns_error() {
let samples: Vec<(f64, f64)> = vec![];
assert!(mean_of_maxima(&samples).is_err());
}
#[test]
fn bisector_symmetric_triangle_is_center() {
let samples = symmetric_triangle_samples(1000);
let boa = bisector_of_area(&samples).unwrap();
assert!(
(boa - 5.0).abs() < 0.1,
"BoA of symmetric triangle should be ~5.0, got {boa}"
);
}
#[test]
fn largest_of_maxima_flat_top() {
let samples: Vec<(f64, f64)> = vec![(0.0, 0.0), (3.0, 1.0), (7.0, 1.0), (10.0, 0.0)];
let lom = largest_of_maxima(&samples);
assert!((lom - 7.0).abs() < 1e-9, "LoM should be 7.0, got {lom}");
}
#[test]
fn smallest_of_maxima_flat_top() {
let samples: Vec<(f64, f64)> = vec![(0.0, 0.0), (3.0, 1.0), (7.0, 1.0), (10.0, 0.0)];
let som = smallest_of_maxima(&samples);
assert!((som - 3.0).abs() < 1e-9, "SoM should be 3.0, got {som}");
}
#[test]
fn largest_of_maxima_empty() {
let samples: Vec<(f64, f64)> = vec![];
assert_eq!(largest_of_maxima::<f64>(&samples), 0.0);
}
#[test]
fn smallest_of_maxima_empty() {
let samples: Vec<(f64, f64)> = vec![];
assert_eq!(smallest_of_maxima::<f64>(&samples), 0.0);
}
}