use scirs2_core::ndarray::{ArrayD, IxDyn, Zip};
use std::f64::consts::E;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum FuzzyError {
#[error("Shape mismatch: {0:?} vs {1:?}")]
ShapeMismatch(Vec<usize>, Vec<usize>),
#[error("Invalid temperature: {0} (must be positive)")]
InvalidTemperature(f64),
#[error("Invalid value: {0} (must be in [0, 1])")]
InvalidValue(f64),
}
pub type FuzzyResult<T> = Result<T, FuzzyError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FuzzyFamily {
Godel,
Product,
Lukasiewicz,
Soft,
}
impl Default for FuzzyFamily {
fn default() -> Self {
FuzzyFamily::Soft
}
}
#[derive(Debug, Clone)]
pub struct FuzzyConfig {
pub family: FuzzyFamily,
pub temperature: f64,
pub clamp_output: bool,
pub epsilon: f64,
}
impl Default for FuzzyConfig {
fn default() -> Self {
FuzzyConfig {
family: FuzzyFamily::Soft,
temperature: 1.0,
clamp_output: true,
epsilon: 1e-10,
}
}
}
impl FuzzyConfig {
pub fn godel() -> Self {
FuzzyConfig {
family: FuzzyFamily::Godel,
temperature: 1.0,
clamp_output: true,
epsilon: 1e-10,
}
}
pub fn product() -> Self {
FuzzyConfig {
family: FuzzyFamily::Product,
temperature: 1.0,
clamp_output: true,
epsilon: 1e-10,
}
}
pub fn lukasiewicz() -> Self {
FuzzyConfig {
family: FuzzyFamily::Lukasiewicz,
temperature: 1.0,
clamp_output: true,
epsilon: 1e-10,
}
}
pub fn soft(temperature: f64) -> Self {
FuzzyConfig {
family: FuzzyFamily::Soft,
temperature,
clamp_output: true,
epsilon: 1e-10,
}
}
pub fn with_temperature(mut self, temperature: f64) -> Self {
self.temperature = temperature;
self
}
pub fn with_clamping(mut self, clamp: bool) -> Self {
self.clamp_output = clamp;
self
}
pub fn with_epsilon(mut self, epsilon: f64) -> Self {
self.epsilon = epsilon;
self
}
pub fn validate(&self) -> FuzzyResult<()> {
if self.temperature <= 0.0 {
return Err(FuzzyError::InvalidTemperature(self.temperature));
}
Ok(())
}
}
pub fn soft_min(a: f64, b: f64, temperature: f64) -> f64 {
if temperature <= 1e-10 {
return a.min(b);
}
let min_val = a.min(b);
let exp_a = ((-a + min_val) / temperature).exp();
let exp_b = ((-b + min_val) / temperature).exp();
-temperature * (exp_a + exp_b).ln() + min_val
}
pub fn soft_max(a: f64, b: f64, temperature: f64) -> f64 {
if temperature <= 1e-10 {
return a.max(b);
}
let max_val = a.max(b);
let exp_a = ((a - max_val) / temperature).exp();
let exp_b = ((b - max_val) / temperature).exp();
temperature * (exp_a + exp_b).ln() + max_val
}
pub fn soft_and(a: &ArrayD<f64>, b: &ArrayD<f64>, temperature: f64) -> ArrayD<f64> {
Zip::from(a).and(b).map_collect(|&x, &y| soft_min(x, y, temperature))
}
pub fn soft_or(a: &ArrayD<f64>, b: &ArrayD<f64>, temperature: f64) -> ArrayD<f64> {
Zip::from(a).and(b).map_collect(|&x, &y| soft_max(x, y, temperature))
}
pub fn soft_not(x: &ArrayD<f64>) -> ArrayD<f64> {
x.mapv(|v| 1.0 - v)
}
pub fn soft_imply(a: &ArrayD<f64>, b: &ArrayD<f64>, config: &FuzzyConfig) -> ArrayD<f64> {
match config.family {
FuzzyFamily::Godel => Zip::from(a)
.and(b)
.map_collect(|&x, &y| if x <= y { 1.0 } else { y }),
FuzzyFamily::Product => Zip::from(a).and(b).map_collect(|&x, &y| {
if x <= config.epsilon {
1.0
} else {
(y / x).min(1.0)
}
}),
FuzzyFamily::Lukasiewicz => {
Zip::from(a).and(b).map_collect(|&x, &y| (1.0 - x + y).min(1.0))
}
FuzzyFamily::Soft => {
Zip::from(a).and(b).map_collect(|&x, &y| {
let diff = y - x;
let sigmoid_diff = 1.0 / (1.0 + (-diff / config.temperature).exp());
sigmoid_diff + (1.0 - sigmoid_diff) * y.max(0.0).min(1.0)
})
}
}
}
#[derive(Debug, Clone)]
pub struct FuzzyLogic {
config: FuzzyConfig,
}
impl FuzzyLogic {
pub fn new(config: FuzzyConfig) -> FuzzyResult<Self> {
config.validate()?;
Ok(FuzzyLogic { config })
}
pub fn default_soft() -> Self {
FuzzyLogic {
config: FuzzyConfig::default(),
}
}
pub fn config(&self) -> &FuzzyConfig {
&self.config
}
pub fn set_temperature(&mut self, temperature: f64) -> FuzzyResult<()> {
if temperature <= 0.0 {
return Err(FuzzyError::InvalidTemperature(temperature));
}
self.config.temperature = temperature;
Ok(())
}
fn maybe_clamp(&self, x: ArrayD<f64>) -> ArrayD<f64> {
if self.config.clamp_output {
x.mapv(|v| v.clamp(0.0, 1.0))
} else {
x
}
}
pub fn and(&self, a: &ArrayD<f64>, b: &ArrayD<f64>) -> FuzzyResult<ArrayD<f64>> {
if a.shape() != b.shape() {
return Err(FuzzyError::ShapeMismatch(
a.shape().to_vec(),
b.shape().to_vec(),
));
}
let result = match self.config.family {
FuzzyFamily::Godel => Zip::from(a).and(b).map_collect(|&x, &y| x.min(y)),
FuzzyFamily::Product => Zip::from(a).and(b).map_collect(|&x, &y| x * y),
FuzzyFamily::Lukasiewicz => {
Zip::from(a).and(b).map_collect(|&x, &y| (x + y - 1.0).max(0.0))
}
FuzzyFamily::Soft => soft_and(a, b, self.config.temperature),
};
Ok(self.maybe_clamp(result))
}
pub fn or(&self, a: &ArrayD<f64>, b: &ArrayD<f64>) -> FuzzyResult<ArrayD<f64>> {
if a.shape() != b.shape() {
return Err(FuzzyError::ShapeMismatch(
a.shape().to_vec(),
b.shape().to_vec(),
));
}
let result = match self.config.family {
FuzzyFamily::Godel => Zip::from(a).and(b).map_collect(|&x, &y| x.max(y)),
FuzzyFamily::Product => Zip::from(a).and(b).map_collect(|&x, &y| x + y - x * y),
FuzzyFamily::Lukasiewicz => {
Zip::from(a).and(b).map_collect(|&x, &y| (x + y).min(1.0))
}
FuzzyFamily::Soft => soft_or(a, b, self.config.temperature),
};
Ok(self.maybe_clamp(result))
}
pub fn not(&self, x: &ArrayD<f64>) -> ArrayD<f64> {
let result = soft_not(x);
self.maybe_clamp(result)
}
pub fn imply(&self, a: &ArrayD<f64>, b: &ArrayD<f64>) -> FuzzyResult<ArrayD<f64>> {
if a.shape() != b.shape() {
return Err(FuzzyError::ShapeMismatch(
a.shape().to_vec(),
b.shape().to_vec(),
));
}
let result = soft_imply(a, b, &self.config);
Ok(self.maybe_clamp(result))
}
pub fn equiv(&self, a: &ArrayD<f64>, b: &ArrayD<f64>) -> FuzzyResult<ArrayD<f64>> {
let a_implies_b = self.imply(a, b)?;
let b_implies_a = self.imply(b, a)?;
self.and(&a_implies_b, &b_implies_a)
}
pub fn xor(&self, a: &ArrayD<f64>, b: &ArrayD<f64>) -> FuzzyResult<ArrayD<f64>> {
if a.shape() != b.shape() {
return Err(FuzzyError::ShapeMismatch(
a.shape().to_vec(),
b.shape().to_vec(),
));
}
let not_a = self.not(a);
let not_b = self.not(b);
let a_and_not_b = self.and(a, ¬_b)?;
let not_a_and_b = self.and(¬_a, b)?;
self.or(&a_and_not_b, ¬_a_and_b)
}
pub fn nand(&self, a: &ArrayD<f64>, b: &ArrayD<f64>) -> FuzzyResult<ArrayD<f64>> {
let and_result = self.and(a, b)?;
Ok(self.not(&and_result))
}
pub fn nor(&self, a: &ArrayD<f64>, b: &ArrayD<f64>) -> FuzzyResult<ArrayD<f64>> {
let or_result = self.or(a, b)?;
Ok(self.not(&or_result))
}
pub fn and_many(&self, tensors: &[&ArrayD<f64>]) -> FuzzyResult<ArrayD<f64>> {
if tensors.is_empty() {
return Err(FuzzyError::InvalidValue(0.0));
}
let mut result = tensors[0].clone();
for tensor in &tensors[1..] {
result = self.and(&result, tensor)?;
}
Ok(result)
}
pub fn or_many(&self, tensors: &[&ArrayD<f64>]) -> FuzzyResult<ArrayD<f64>> {
if tensors.is_empty() {
return Err(FuzzyError::InvalidValue(0.0));
}
let mut result = tensors[0].clone();
for tensor in &tensors[1..] {
result = self.or(&result, tensor)?;
}
Ok(result)
}
}
pub fn soft_and_grad_a(a: &ArrayD<f64>, b: &ArrayD<f64>, grad: &ArrayD<f64>, temperature: f64) -> ArrayD<f64> {
if temperature <= 1e-10 {
return Zip::from(a)
.and(b)
.and(grad)
.map_collect(|&x, &y, &g| if x <= y { g } else { 0.0 });
}
Zip::from(a).and(b).and(grad).map_collect(|&x, &y, &g| {
let min_val = x.min(y);
let exp_a = ((-x + min_val) / temperature).exp();
let exp_b = ((-y + min_val) / temperature).exp();
let sum_exp = exp_a + exp_b;
g * exp_a / sum_exp
})
}
pub fn soft_and_grad_b(a: &ArrayD<f64>, b: &ArrayD<f64>, grad: &ArrayD<f64>, temperature: f64) -> ArrayD<f64> {
if temperature <= 1e-10 {
return Zip::from(a)
.and(b)
.and(grad)
.map_collect(|&x, &y, &g| if y < x { g } else { 0.0 });
}
Zip::from(a).and(b).and(grad).map_collect(|&x, &y, &g| {
let min_val = x.min(y);
let exp_a = ((-x + min_val) / temperature).exp();
let exp_b = ((-y + min_val) / temperature).exp();
let sum_exp = exp_a + exp_b;
g * exp_b / sum_exp
})
}
pub fn soft_or_grad_a(a: &ArrayD<f64>, b: &ArrayD<f64>, grad: &ArrayD<f64>, temperature: f64) -> ArrayD<f64> {
if temperature <= 1e-10 {
return Zip::from(a)
.and(b)
.and(grad)
.map_collect(|&x, &y, &g| if x >= y { g } else { 0.0 });
}
Zip::from(a).and(b).and(grad).map_collect(|&x, &y, &g| {
let max_val = x.max(y);
let exp_a = ((x - max_val) / temperature).exp();
let exp_b = ((y - max_val) / temperature).exp();
let sum_exp = exp_a + exp_b;
g * exp_a / sum_exp
})
}
pub fn soft_or_grad_b(a: &ArrayD<f64>, b: &ArrayD<f64>, grad: &ArrayD<f64>, temperature: f64) -> ArrayD<f64> {
if temperature <= 1e-10 {
return Zip::from(a)
.and(b)
.and(grad)
.map_collect(|&x, &y, &g| if y > x { g } else { 0.0 });
}
Zip::from(a).and(b).and(grad).map_collect(|&x, &y, &g| {
let max_val = x.max(y);
let exp_a = ((x - max_val) / temperature).exp();
let exp_b = ((y - max_val) / temperature).exp();
let sum_exp = exp_a + exp_b;
g * exp_b / sum_exp
})
}
#[derive(Debug, Clone, Copy)]
pub enum AnnealingSchedule {
Constant,
Linear {
t_max: f64,
t_min: f64,
steps: usize,
},
Exponential { t_max: f64, decay: f64 },
Cosine {
t_max: f64,
t_min: f64,
steps: usize,
},
}
impl AnnealingSchedule {
pub fn temperature(&self, base_temp: f64, step: usize) -> f64 {
match *self {
AnnealingSchedule::Constant => base_temp,
AnnealingSchedule::Linear { t_max, t_min, steps } => {
if step >= steps {
t_min
} else {
t_max - (t_max - t_min) * (step as f64 / steps as f64)
}
}
AnnealingSchedule::Exponential { t_max, decay } => t_max * decay.powi(step as i32),
AnnealingSchedule::Cosine { t_max, t_min, steps } => {
if step >= steps {
t_min
} else {
let progress = step as f64 / steps as f64;
t_min + 0.5 * (t_max - t_min) * (1.0 + (std::f64::consts::PI * progress).cos())
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn arr(values: Vec<f64>) -> ArrayD<f64> {
ArrayD::from_shape_vec(IxDyn(&[values.len()]), values)
.expect("test helper: valid shape and values")
}
#[test]
fn test_soft_min() {
assert!((soft_min(0.3, 0.7, 0.001) - 0.3).abs() < 0.01);
assert!((soft_min(0.8, 0.2, 0.001) - 0.2).abs() < 0.01);
let soft_result = soft_min(0.3, 0.7, 1.0);
assert!(soft_result < 0.5); assert!(soft_result > 0.3); }
#[test]
fn test_soft_max() {
assert!((soft_max(0.3, 0.7, 0.001) - 0.7).abs() < 0.01);
assert!((soft_max(0.8, 0.2, 0.001) - 0.8).abs() < 0.01);
let soft_result = soft_max(0.3, 0.7, 1.0);
assert!(soft_result > 0.5); assert!(soft_result < 0.7); }
#[test]
fn test_godel_and() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::godel())?;
let a = arr(vec![0.3, 0.8, 0.5]);
let b = arr(vec![0.7, 0.2, 0.5]);
let result = fuzzy.and(&a, &b)?;
assert!((result[[0]] - 0.3).abs() < 1e-10);
assert!((result[[1]] - 0.2).abs() < 1e-10);
assert!((result[[2]] - 0.5).abs() < 1e-10);
Ok(())
}
#[test]
fn test_godel_or() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::godel())?;
let a = arr(vec![0.3, 0.8, 0.5]);
let b = arr(vec![0.7, 0.2, 0.5]);
let result = fuzzy.or(&a, &b)?;
assert!((result[[0]] - 0.7).abs() < 1e-10);
assert!((result[[1]] - 0.8).abs() < 1e-10);
assert!((result[[2]] - 0.5).abs() < 1e-10);
Ok(())
}
#[test]
fn test_product_and() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::product())?;
let a = arr(vec![0.5, 0.8]);
let b = arr(vec![0.6, 0.5]);
let result = fuzzy.and(&a, &b)?;
assert!((result[[0]] - 0.3).abs() < 1e-10); assert!((result[[1]] - 0.4).abs() < 1e-10); Ok(())
}
#[test]
fn test_product_or() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::product())?;
let a = arr(vec![0.5, 0.8]);
let b = arr(vec![0.6, 0.5]);
let result = fuzzy.or(&a, &b)?;
assert!((result[[0]] - 0.8).abs() < 1e-10); assert!((result[[1]] - 0.9).abs() < 1e-10); Ok(())
}
#[test]
fn test_lukasiewicz_and() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::lukasiewicz())?;
let a = arr(vec![0.8, 0.3]);
let b = arr(vec![0.7, 0.4]);
let result = fuzzy.and(&a, &b)?;
assert!((result[[0]] - 0.5).abs() < 1e-10); assert!((result[[1]] - 0.0).abs() < 1e-10); Ok(())
}
#[test]
fn test_lukasiewicz_or() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::lukasiewicz())?;
let a = arr(vec![0.8, 0.3]);
let b = arr(vec![0.7, 0.4]);
let result = fuzzy.or(&a, &b)?;
assert!((result[[0]] - 1.0).abs() < 1e-10); assert!((result[[1]] - 0.7).abs() < 1e-10); Ok(())
}
#[test]
fn test_soft_and_tensor() {
let a = arr(vec![0.3, 0.8]);
let b = arr(vec![0.7, 0.2]);
let result = soft_and(&a, &b, 0.1);
assert!(result[[0]] < 0.4);
assert!(result[[0]] > 0.25);
assert!(result[[1]] < 0.3);
assert!(result[[1]] > 0.15);
}
#[test]
fn test_fuzzy_not() {
let fuzzy = FuzzyLogic::default_soft();
let x = arr(vec![0.0, 0.3, 0.5, 0.7, 1.0]);
let result = fuzzy.not(&x);
assert!((result[[0]] - 1.0).abs() < 1e-10);
assert!((result[[1]] - 0.7).abs() < 1e-10);
assert!((result[[2]] - 0.5).abs() < 1e-10);
assert!((result[[3]] - 0.3).abs() < 1e-10);
assert!((result[[4]] - 0.0).abs() < 1e-10);
}
#[test]
fn test_fuzzy_xor() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::product())?;
let a = arr(vec![0.0, 1.0, 0.0, 1.0]);
let b = arr(vec![0.0, 0.0, 1.0, 1.0]);
let result = fuzzy.xor(&a, &b)?;
assert!(result[[0]] < 0.1); assert!(result[[1]] > 0.9); assert!(result[[2]] > 0.9); assert!(result[[3]] < 0.1); Ok(())
}
#[test]
fn test_fuzzy_nand_nor() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::godel())?;
let a = arr(vec![0.8, 0.3]);
let b = arr(vec![0.6, 0.4]);
let nand_result = fuzzy.nand(&a, &b)?;
let nor_result = fuzzy.nor(&a, &b)?;
assert!((nand_result[[0]] - 0.4).abs() < 1e-10); assert!((nand_result[[1]] - 0.7).abs() < 1e-10);
assert!((nor_result[[0]] - 0.2).abs() < 1e-10); assert!((nor_result[[1]] - 0.6).abs() < 1e-10); Ok(())
}
#[test]
fn test_and_many() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::godel())?;
let a = arr(vec![0.8, 0.5]);
let b = arr(vec![0.6, 0.7]);
let c = arr(vec![0.4, 0.3]);
let result = fuzzy.and_many(&[&a, &b, &c])?;
assert!((result[[0]] - 0.4).abs() < 1e-10); assert!((result[[1]] - 0.3).abs() < 1e-10); Ok(())
}
#[test]
fn test_or_many() -> Result<(), Box<dyn std::error::Error>> {
let fuzzy = FuzzyLogic::new(FuzzyConfig::godel())?;
let a = arr(vec![0.2, 0.5]);
let b = arr(vec![0.6, 0.3]);
let c = arr(vec![0.4, 0.7]);
let result = fuzzy.or_many(&[&a, &b, &c])?;
assert!((result[[0]] - 0.6).abs() < 1e-10); assert!((result[[1]] - 0.7).abs() < 1e-10); Ok(())
}
#[test]
fn test_soft_and_gradient() {
let a = arr(vec![0.3, 0.8]);
let b = arr(vec![0.7, 0.2]);
let grad = arr(vec![1.0, 1.0]);
let grad_a = soft_and_grad_a(&a, &b, &grad, 0.1);
let grad_b = soft_and_grad_b(&a, &b, &grad, 0.1);
assert!(grad_a[[0]] > grad_b[[0]]); assert!(grad_a[[1]] < grad_b[[1]]); }
#[test]
fn test_annealing_constant() {
let schedule = AnnealingSchedule::Constant;
assert!((schedule.temperature(1.0, 0) - 1.0).abs() < 1e-10);
assert!((schedule.temperature(1.0, 100) - 1.0).abs() < 1e-10);
}
#[test]
fn test_annealing_linear() {
let schedule = AnnealingSchedule::Linear {
t_max: 1.0,
t_min: 0.1,
steps: 100,
};
assert!((schedule.temperature(1.0, 0) - 1.0).abs() < 1e-10);
assert!((schedule.temperature(1.0, 50) - 0.55).abs() < 1e-10);
assert!((schedule.temperature(1.0, 100) - 0.1).abs() < 1e-10);
}
#[test]
fn test_annealing_exponential() {
let schedule = AnnealingSchedule::Exponential {
t_max: 1.0,
decay: 0.9,
};
assert!((schedule.temperature(1.0, 0) - 1.0).abs() < 1e-10);
assert!((schedule.temperature(1.0, 1) - 0.9).abs() < 1e-10);
assert!((schedule.temperature(1.0, 2) - 0.81).abs() < 1e-10);
}
#[test]
fn test_annealing_cosine() {
let schedule = AnnealingSchedule::Cosine {
t_max: 1.0,
t_min: 0.0,
steps: 100,
};
assert!((schedule.temperature(1.0, 0) - 1.0).abs() < 1e-10);
assert!((schedule.temperature(1.0, 50) - 0.5).abs() < 0.01);
assert!((schedule.temperature(1.0, 100) - 0.0).abs() < 1e-10);
}
#[test]
fn test_config_validation() {
assert!(FuzzyConfig::soft(-1.0).validate().is_err());
assert!(FuzzyConfig::soft(0.0).validate().is_err());
assert!(FuzzyConfig::soft(0.1).validate().is_ok());
}
#[test]
fn test_shape_mismatch_error() {
let fuzzy = FuzzyLogic::default_soft();
let a = arr(vec![0.5, 0.5]);
let b = arr(vec![0.5, 0.5, 0.5]);
assert!(fuzzy.and(&a, &b).is_err());
assert!(fuzzy.or(&a, &b).is_err());
}
}