pub fn tconorm(family: TConormFamily, a: f64, b: f64) -> f64 {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match family {
TConormFamily::Maximum => a.max(b),
TConormFamily::Probabilistic => a + b - a * b,
TConormFamily::Bounded => (a + b).min(1.0),
TConormFamily::Einstein => {
let denom = 1.0 + a * b;
if denom == 0.0 {
0.0
} else {
(a + b) / denom
}
}
TConormFamily::Hamacher => {
let denom = 1.0 - a * b;
if denom.abs() < 1e-15 {
1.0 } else {
(a + b - 2.0 * a * b) / denom
}
}
TConormFamily::Yager { p } => {
debug_assert!(p >= 1.0, "Yager p must be >= 1");
(a.powf(p) + b.powf(p)).powf(1.0 / p).min(1.0)
}
TConormFamily::Frank { s } => {
debug_assert!(s > 0.0 && s != 1.0, "Frank s must be > 0 and != 1");
let num = (s.powf(1.0 - a) - 1.0) * (s.powf(1.0 - b) - 1.0);
let denom = s - 1.0;
1.0 - (1.0 + num / denom).log(s)
}
TConormFamily::Dombi { p } => {
debug_assert!(p > 0.0, "Dombi p must be > 0");
if a <= 0.0 {
return b;
}
if b <= 0.0 {
return a;
}
if a >= 1.0 || b >= 1.0 {
return 1.0;
}
let ta = (1.0 / a - 1.0).powf(p);
let tb = (1.0 / b - 1.0).powf(p);
let inner = (ta.recip() + tb.recip()).recip();
1.0 / (1.0 + inner.powf(1.0 / p))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TConormFamily {
Maximum,
Probabilistic,
Bounded,
Einstein,
Hamacher,
Yager {
p: f64,
},
Frank {
s: f64,
},
Dombi {
p: f64,
},
}
pub fn tnorm(family: TConormFamily, a: f64, b: f64) -> f64 {
1.0 - tconorm(family, 1.0 - a, 1.0 - b)
}
pub fn tconorm_fold(family: TConormFamily, values: &[f64]) -> f64 {
match values.len() {
0 => 0.0, 1 => values[0].clamp(0.0, 1.0),
_ => values
.iter()
.skip(1)
.fold(values[0].clamp(0.0, 1.0), |acc, &v| tconorm(family, acc, v)),
}
}
pub fn tnorm_fold(family: TConormFamily, values: &[f64]) -> f64 {
match values.len() {
0 => 1.0, 1 => values[0].clamp(0.0, 1.0),
_ => values
.iter()
.skip(1)
.fold(values[0].clamp(0.0, 1.0), |acc, &v| tnorm(family, acc, v)),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn all_families() -> Vec<TConormFamily> {
vec![
TConormFamily::Maximum,
TConormFamily::Probabilistic,
TConormFamily::Bounded,
TConormFamily::Einstein,
TConormFamily::Hamacher,
TConormFamily::Yager { p: 2.0 },
TConormFamily::Frank { s: 2.0 },
TConormFamily::Dombi { p: 2.0 },
]
}
#[test]
fn identity_element() {
for f in all_families() {
let r = tconorm(f, 0.5, 0.0);
assert!(
(r - 0.5).abs() < 1e-6,
"{f:?}: S(0.5, 0) = {r}, expected 0.5"
);
}
}
#[test]
fn commutativity() {
for f in all_families() {
let r1 = tconorm(f, 0.3, 0.7);
let r2 = tconorm(f, 0.7, 0.3);
assert!(
(r1 - r2).abs() < 1e-10,
"{f:?}: S(0.3,0.7)={r1} != S(0.7,0.3)={r2}"
);
}
}
#[test]
fn monotonicity() {
for f in all_families() {
let r1 = tconorm(f, 0.3, 0.5);
let r2 = tconorm(f, 0.6, 0.5);
assert!(
r2 >= r1 - 1e-10,
"{f:?}: monotonicity violated S(0.3,0.5)={r1} > S(0.6,0.5)={r2}"
);
}
}
#[test]
fn bounded_output() {
for f in all_families() {
for &a in &[0.0, 0.25, 0.5, 0.75, 1.0] {
for &b in &[0.0, 0.25, 0.5, 0.75, 1.0] {
let r = tconorm(f, a, b);
assert!(
(-1e-10..=1.0 + 1e-10).contains(&r),
"{f:?}: S({a},{b}) = {r} out of [0,1]"
);
}
}
}
}
#[test]
fn probabilistic_known_values() {
let f = TConormFamily::Probabilistic;
assert!((tconorm(f, 0.5, 0.5) - 0.75).abs() < 1e-10);
assert!((tconorm(f, 1.0, 0.3) - 1.0).abs() < 1e-10);
}
#[test]
fn yager_p1_equals_bounded() {
let yager = TConormFamily::Yager { p: 1.0 };
let bounded = TConormFamily::Bounded;
for &a in &[0.1, 0.3, 0.5, 0.7, 0.9] {
for &b in &[0.1, 0.3, 0.5, 0.7, 0.9] {
let ry = tconorm(yager, a, b);
let rb = tconorm(bounded, a, b);
assert!(
(ry - rb).abs() < 1e-10,
"Yager(p=1) != Bounded: S({a},{b}) = {ry} vs {rb}"
);
}
}
}
#[test]
fn tnorm_duality() {
let f = TConormFamily::Probabilistic;
let t = tnorm(f, 0.5, 0.6);
assert!((t - 0.3).abs() < 1e-10, "T(0.5,0.6) = {t}, expected 0.3");
}
#[test]
fn fold_empty_returns_identity() {
let f = TConormFamily::Maximum;
assert_eq!(tconorm_fold(f, &[]), 0.0);
assert_eq!(tnorm_fold(f, &[]), 1.0);
}
#[test]
fn fold_single_value() {
let f = TConormFamily::Probabilistic;
assert!((tconorm_fold(f, &[0.7]) - 0.7).abs() < 1e-10);
}
#[test]
fn fold_associativity() {
let f = TConormFamily::Einstein;
let abc = tconorm_fold(f, &[0.3, 0.5, 0.7]);
let ab_c = tconorm(f, tconorm(f, 0.3, 0.5), 0.7);
assert!(
(abc - ab_c).abs() < 1e-10,
"fold should match left-to-right application"
);
}
}