#![forbid(unsafe_code)]
#![cfg_attr(not(feature = "std"), no_std)]
#![warn(missing_docs)]
use num_traits::Float;
mod formulas;
mod operators;
mod ordinal;
use formulas::*;
pub use operators::{
implication, log_product, negation, owa, power_mean, power_mean_weighted, product_from_log,
representable_uninorm, ImplicationFamily, MixedRegion, NegationFamily,
};
pub use ordinal::{OrdinalSegment, OrdinalSum};
pub fn tconorm(family: TConormFamily, a: f64, b: f64) -> f64 {
tconorm_generic(family, a, b)
}
pub fn tconorm_generic<T: Float>(family: TConormFamily, a: T, b: T) -> T {
let a = clamp01(a);
let b = clamp01(b);
match family {
TConormFamily::Maximum => a.max(b),
TConormFamily::Probabilistic => a + b - a * b,
TConormFamily::Bounded => (a + b).min(T::one()),
TConormFamily::Einstein => {
let denom = T::one() + a * b;
if denom == T::zero() {
T::zero()
} else {
(a + b) / denom
}
}
TConormFamily::Hamacher => {
let denom = T::one() - a * b;
if denom.abs() < cast(1e-15) {
T::one()
} else {
(a + b - cast::<T>(2.0) * a * b) / denom
}
}
TConormFamily::Yager { p } => {
debug_assert!(p > 0.0, "Yager p must be > 0");
scaled_lp_norm_2(a, b, cast(p)).min(T::one())
}
TConormFamily::Frank { s } => {
debug_assert!(s > 0.0, "Frank s must be > 0");
frank_tconorm(a, b, cast(s))
}
TConormFamily::Dombi { p } => {
debug_assert!(p > 0.0, "Dombi p must be > 0");
dombi_tconorm(a, b, cast(p))
}
TConormFamily::Drastic => drastic_tconorm(a, b),
TConormFamily::NilpotentMinimum => nilpotent_minimum_tconorm(a, b),
TConormFamily::SchweizerSklar { p } => {
T::one() - schweizer_sklar_tnorm(T::one() - a, T::one() - b, cast(p))
}
TConormFamily::SugenoWeber { p } => {
T::one() - sugeno_weber_tnorm(T::one() - a, T::one() - b, cast(p))
}
TConormFamily::AczelAlsina { p } => {
T::one() - aczel_alsina_tnorm(T::one() - a, T::one() - b, cast(p))
}
TConormFamily::MayorTorrens { p } => {
T::one() - mayor_torrens_tnorm(T::one() - a, T::one() - b, cast(p))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TConormFamily {
Maximum,
Probabilistic,
Bounded,
Einstein,
Hamacher,
Yager {
p: f64,
},
Frank {
s: f64,
},
Dombi {
p: f64,
},
Drastic,
NilpotentMinimum,
SchweizerSklar {
p: f64,
},
SugenoWeber {
p: f64,
},
AczelAlsina {
p: f64,
},
MayorTorrens {
p: f64,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CopulaCompatibility {
Always,
Never,
ParameterDependent,
Unknown,
}
pub type Family = TConormFamily;
pub const GODEL: Family = Family::Maximum;
pub const PRODUCT: Family = Family::Probabilistic;
pub const LUKASIEWICZ: Family = Family::Bounded;
impl TConormFamily {
pub fn copula_compatibility(self) -> CopulaCompatibility {
match self {
Self::Maximum | Self::Probabilistic | Self::Bounded | Self::Frank { .. } => {
CopulaCompatibility::Always
}
Self::Drastic | Self::NilpotentMinimum => CopulaCompatibility::Never,
Self::Dombi { .. }
| Self::SchweizerSklar { .. }
| Self::SugenoWeber { .. }
| Self::AczelAlsina { .. }
| Self::MayorTorrens { .. } => CopulaCompatibility::ParameterDependent,
Self::Einstein | Self::Hamacher | Self::Yager { .. } => CopulaCompatibility::Unknown,
}
}
pub fn is_copula(self) -> Option<bool> {
match self.copula_compatibility() {
CopulaCompatibility::Always => Some(true),
CopulaCompatibility::Never => Some(false),
CopulaCompatibility::ParameterDependent | CopulaCompatibility::Unknown => None,
}
}
pub fn generator(self) -> Option<GeneratorFamily> {
match self {
Self::Maximum | Self::Drastic | Self::NilpotentMinimum | Self::MayorTorrens { .. } => {
None
}
Self::Probabilistic => Some(GeneratorFamily::Product),
Self::Bounded => Some(GeneratorFamily::Lukasiewicz),
Self::Einstein => Some(GeneratorFamily::Hamacher { p: 2.0 }),
Self::Hamacher => Some(GeneratorFamily::Hamacher { p: 0.0 }),
Self::Yager { p } if p > 0.0 => Some(GeneratorFamily::Yager { p }),
Self::Frank { s } if s > 0.0 => Some(GeneratorFamily::Frank { s }),
Self::Dombi { p } if p > 0.0 => Some(GeneratorFamily::Dombi { p }),
Self::SchweizerSklar { p } if p.is_finite() => {
Some(GeneratorFamily::SchweizerSklar { p })
}
Self::SugenoWeber { p } if p > -1.0 && p.is_finite() => {
Some(GeneratorFamily::SugenoWeber { p })
}
Self::AczelAlsina { p } if p > 0.0 && p.is_finite() => {
Some(GeneratorFamily::AczelAlsina { p })
}
_ => None,
}
}
#[inline]
pub fn tconorm(self, a: f64, b: f64) -> f64 {
tconorm(self, a, b)
}
#[inline]
pub fn tconorm_generic<T: Float>(self, a: T, b: T) -> T {
tconorm_generic(self, a, b)
}
#[inline]
pub fn tnorm(self, a: f64, b: f64) -> f64 {
tnorm(self, a, b)
}
#[inline]
pub fn tnorm_generic<T: Float>(self, a: T, b: T) -> T {
tnorm_generic(self, a, b)
}
pub fn residuum(self, a: f64, b: f64) -> Option<f64> {
residuum(self, a, b)
}
#[inline]
pub fn tnorm_gradient(self, a: f64, b: f64) -> BinaryGradient<f64> {
tnorm_gradient(self, a, b)
}
#[inline]
pub fn tconorm_gradient(self, a: f64, b: f64) -> BinaryGradient<f64> {
tconorm_gradient(self, a, b)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GeneratorFamily {
Product,
Lukasiewicz,
Hamacher {
p: f64,
},
Frank {
s: f64,
},
Yager {
p: f64,
},
Dombi {
p: f64,
},
SchweizerSklar {
p: f64,
},
SugenoWeber {
p: f64,
},
AczelAlsina {
p: f64,
},
}
impl GeneratorFamily {
pub fn value(self, x: f64) -> f64 {
let x = x.clamp(0.0, 1.0);
match self {
Self::Product => {
if x <= 0.0 {
f64::INFINITY
} else {
-x.ln()
}
}
Self::Lukasiewicz => 1.0 - x,
Self::Hamacher { p } => hamacher_generator(x, p),
Self::Frank { s } => frank_generator(x, s),
Self::Yager { p } => (1.0 - x).powf(p),
Self::Dombi { p } => {
if x <= 0.0 {
f64::INFINITY
} else if x >= 1.0 {
0.0
} else {
((1.0 - x) / x).powf(p)
}
}
Self::SchweizerSklar { p } => {
if p.abs() <= FRANK_PRODUCT_LIMIT_EPS {
Self::Product.value(x)
} else if x <= 0.0 && p <= 0.0 {
f64::INFINITY
} else {
(1.0 - x.powf(p)) / p
}
}
Self::SugenoWeber { p } => {
if p.abs() <= FRANK_PRODUCT_LIMIT_EPS {
1.0 - x
} else {
1.0 - (p.mul_add(x, 1.0)).ln() / (1.0 + p).ln()
}
}
Self::AczelAlsina { p } => {
if x <= 0.0 {
f64::INFINITY
} else {
(-x.ln()).powf(p)
}
}
}
}
pub fn inverse(self, y: f64) -> f64 {
let y = y.max(0.0);
match self {
Self::Product => (-y).exp(),
Self::Lukasiewicz => (1.0 - y).clamp(0.0, 1.0),
Self::Hamacher { p } => hamacher_generator_inverse(y, p),
Self::Frank { s } => frank_generator_inverse(y, s),
Self::Yager { p } => (1.0 - y.powf(1.0 / p)).clamp(0.0, 1.0),
Self::Dombi { p } => 1.0 / (1.0 + y.powf(1.0 / p)),
Self::SchweizerSklar { p } => {
if p.abs() <= FRANK_PRODUCT_LIMIT_EPS {
Self::Product.inverse(y)
} else {
(1.0 - p * y).max(0.0).powf(1.0 / p).clamp(0.0, 1.0)
}
}
Self::SugenoWeber { p } => {
if p.abs() <= FRANK_PRODUCT_LIMIT_EPS {
(1.0 - y).clamp(0.0, 1.0)
} else {
(((1.0 + p).powf(1.0 - y) - 1.0) / p).clamp(0.0, 1.0)
}
}
Self::AczelAlsina { p } => (-y.powf(1.0 / p)).exp(),
}
}
pub fn tnorm(self, a: f64, b: f64) -> f64 {
self.inverse(self.value(a) + self.value(b))
}
pub fn residuum(self, a: f64, b: f64) -> f64 {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
if a <= b {
1.0
} else {
self.inverse((self.value(b) - self.value(a)).max(0.0))
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogicFamily {
Godel,
Product,
Lukasiewicz,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct BinaryGradient<T> {
pub da: T,
pub db: T,
}
impl<T> BinaryGradient<T> {
pub const fn new(da: T, db: T) -> Self {
Self { da, db }
}
}
impl LogicFamily {
pub const fn family(self) -> Family {
match self {
Self::Godel => GODEL,
Self::Product => PRODUCT,
Self::Lukasiewicz => LUKASIEWICZ,
}
}
pub fn tnorm(self, a: f64, b: f64) -> f64 {
tnorm(self.family(), a, b)
}
pub fn tconorm(self, a: f64, b: f64) -> f64 {
tconorm(self.family(), a, b)
}
pub fn residuum(self, a: f64, b: f64) -> f64 {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => {
if a <= b {
1.0
} else {
b
}
}
Self::Product => {
if a <= b {
1.0
} else {
b / a
}
}
Self::Lukasiewicz => (1.0 - a + b).min(1.0),
}
}
pub fn neg(self, a: f64) -> f64 {
self.residuum(a, 0.0)
}
pub fn tnorm_gradient(self, a: f64, b: f64) -> BinaryGradient<f64> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => min_gradient(a, b),
Self::Product => BinaryGradient::new(b, a),
Self::Lukasiewicz => {
if a + b > 1.0 {
BinaryGradient::new(1.0, 1.0)
} else {
BinaryGradient::new(0.0, 0.0)
}
}
}
}
pub fn tconorm_gradient(self, a: f64, b: f64) -> BinaryGradient<f64> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => max_gradient(a, b),
Self::Product => BinaryGradient::new(1.0 - b, 1.0 - a),
Self::Lukasiewicz => {
if a + b < 1.0 {
BinaryGradient::new(1.0, 1.0)
} else {
BinaryGradient::new(0.0, 0.0)
}
}
}
}
pub fn residuum_gradient(self, a: f64, b: f64) -> BinaryGradient<f64> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => {
if a > b {
BinaryGradient::new(0.0, 1.0)
} else {
BinaryGradient::new(0.0, 0.0)
}
}
Self::Product => {
if a > b {
BinaryGradient::new(-b / (a * a), 1.0 / a)
} else {
BinaryGradient::new(0.0, 0.0)
}
}
Self::Lukasiewicz => {
if a > b {
BinaryGradient::new(-1.0, 1.0)
} else {
BinaryGradient::new(0.0, 0.0)
}
}
}
}
pub fn neg_gradient(self, _a: f64) -> f64 {
match self {
Self::Godel | Self::Product => 0.0,
Self::Lukasiewicz => -1.0,
}
}
pub fn tnorm_f32(self, a: f32, b: f32) -> f32 {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => a.min(b),
Self::Product => a * b,
Self::Lukasiewicz => (a + b - 1.0).max(0.0),
}
}
pub fn tconorm_f32(self, a: f32, b: f32) -> f32 {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => a.max(b),
Self::Product => a + b - a * b,
Self::Lukasiewicz => (a + b).min(1.0),
}
}
pub fn residuum_f32(self, a: f32, b: f32) -> f32 {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => {
if a <= b {
1.0
} else {
b
}
}
Self::Product => {
if a <= b {
1.0
} else {
b / a
}
}
Self::Lukasiewicz => (1.0 - a + b).min(1.0),
}
}
pub fn neg_f32(self, a: f32) -> f32 {
self.residuum_f32(a, 0.0)
}
pub fn tnorm_gradient_f32(self, a: f32, b: f32) -> BinaryGradient<f32> {
let gradient = self.tnorm_gradient(f64::from(a), f64::from(b));
BinaryGradient::new(gradient.da as f32, gradient.db as f32)
}
pub fn tconorm_gradient_f32(self, a: f32, b: f32) -> BinaryGradient<f32> {
let gradient = self.tconorm_gradient(f64::from(a), f64::from(b));
BinaryGradient::new(gradient.da as f32, gradient.db as f32)
}
pub fn residuum_gradient_f32(self, a: f32, b: f32) -> BinaryGradient<f32> {
let gradient = self.residuum_gradient(f64::from(a), f64::from(b));
BinaryGradient::new(gradient.da as f32, gradient.db as f32)
}
pub fn neg_gradient_f32(self, a: f32) -> f32 {
self.neg_gradient(f64::from(a)) as f32
}
#[cfg(feature = "burn")]
pub fn tnorm_tensor<B: burn::tensor::backend::Backend, const D: usize>(
self,
a: burn::tensor::Tensor<B, D>,
b: burn::tensor::Tensor<B, D>,
) -> burn::tensor::Tensor<B, D> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => a.min_pair(b),
Self::Product => a * b,
Self::Lukasiewicz => (a + b - 1.0).clamp_min(0.0),
}
}
#[cfg(feature = "burn")]
pub fn tconorm_tensor<B: burn::tensor::backend::Backend, const D: usize>(
self,
a: burn::tensor::Tensor<B, D>,
b: burn::tensor::Tensor<B, D>,
) -> burn::tensor::Tensor<B, D> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => a.max_pair(b),
Self::Product => {
let product = a.clone() * b.clone();
a + b - product
}
Self::Lukasiewicz => (a + b).clamp_max(1.0),
}
}
#[cfg(feature = "burn")]
pub fn residuum_tensor<B: burn::tensor::backend::Backend, const D: usize>(
self,
a: burn::tensor::Tensor<B, D>,
b: burn::tensor::Tensor<B, D>,
) -> burn::tensor::Tensor<B, D> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match self {
Self::Godel => {
let mask = a.lower_equal(b.clone());
let one = b.ones_like();
b.mask_where(mask, one)
}
Self::Product => {
let mask = a.clone().lower_equal(b.clone());
let one = a.ones_like();
let denominator = a.clamp_min(1e-12);
let quotient = b / denominator;
quotient.mask_where(mask, one)
}
Self::Lukasiewicz => (a.neg().add_scalar(1.0) + b).clamp_max(1.0),
}
}
#[cfg(feature = "burn")]
pub fn neg_tensor<B: burn::tensor::backend::Backend, const D: usize>(
self,
a: burn::tensor::Tensor<B, D>,
) -> burn::tensor::Tensor<B, D> {
let a = a.clamp(0.0, 1.0);
match self {
Self::Godel | Self::Product => {
let zeros = a.zeros_like();
let ones = a.ones_like();
zeros.mask_where(a.lower_equal_elem(0.0), ones)
}
Self::Lukasiewicz => a.neg().add_scalar(1.0),
}
}
}
pub trait TruthAlgebra {
const FAMILY: LogicFamily;
#[inline]
fn top() -> f64 {
1.0
}
#[inline]
fn bottom() -> f64 {
0.0
}
#[inline]
fn tnorm(a: f64, b: f64) -> f64 {
Self::FAMILY.tnorm(a, b)
}
#[inline]
fn tconorm(a: f64, b: f64) -> f64 {
Self::FAMILY.tconorm(a, b)
}
#[inline]
fn residuum(a: f64, b: f64) -> f64 {
Self::FAMILY.residuum(a, b)
}
#[inline]
fn neg(a: f64) -> f64 {
Self::FAMILY.neg(a)
}
#[inline]
fn tnorm_gradient(a: f64, b: f64) -> BinaryGradient<f64> {
Self::FAMILY.tnorm_gradient(a, b)
}
#[inline]
fn tconorm_gradient(a: f64, b: f64) -> BinaryGradient<f64> {
Self::FAMILY.tconorm_gradient(a, b)
}
#[inline]
fn residuum_gradient(a: f64, b: f64) -> BinaryGradient<f64> {
Self::FAMILY.residuum_gradient(a, b)
}
#[inline]
fn neg_gradient(a: f64) -> f64 {
Self::FAMILY.neg_gradient(a)
}
#[inline]
fn top_f32() -> f32 {
1.0
}
#[inline]
fn bottom_f32() -> f32 {
0.0
}
#[inline]
fn tnorm_f32(a: f32, b: f32) -> f32 {
Self::FAMILY.tnorm_f32(a, b)
}
#[inline]
fn tconorm_f32(a: f32, b: f32) -> f32 {
Self::FAMILY.tconorm_f32(a, b)
}
#[inline]
fn residuum_f32(a: f32, b: f32) -> f32 {
Self::FAMILY.residuum_f32(a, b)
}
#[inline]
fn neg_f32(a: f32) -> f32 {
Self::FAMILY.neg_f32(a)
}
#[inline]
fn tnorm_gradient_f32(a: f32, b: f32) -> BinaryGradient<f32> {
Self::FAMILY.tnorm_gradient_f32(a, b)
}
#[inline]
fn tconorm_gradient_f32(a: f32, b: f32) -> BinaryGradient<f32> {
Self::FAMILY.tconorm_gradient_f32(a, b)
}
#[inline]
fn residuum_gradient_f32(a: f32, b: f32) -> BinaryGradient<f32> {
Self::FAMILY.residuum_gradient_f32(a, b)
}
#[inline]
fn neg_gradient_f32(a: f32) -> f32 {
Self::FAMILY.neg_gradient_f32(a)
}
#[cfg(feature = "burn")]
#[inline]
fn tnorm_tensor<B: burn::tensor::backend::Backend, const D: usize>(
a: burn::tensor::Tensor<B, D>,
b: burn::tensor::Tensor<B, D>,
) -> burn::tensor::Tensor<B, D> {
Self::FAMILY.tnorm_tensor(a, b)
}
#[cfg(feature = "burn")]
#[inline]
fn tconorm_tensor<B: burn::tensor::backend::Backend, const D: usize>(
a: burn::tensor::Tensor<B, D>,
b: burn::tensor::Tensor<B, D>,
) -> burn::tensor::Tensor<B, D> {
Self::FAMILY.tconorm_tensor(a, b)
}
#[cfg(feature = "burn")]
#[inline]
fn residuum_tensor<B: burn::tensor::backend::Backend, const D: usize>(
a: burn::tensor::Tensor<B, D>,
b: burn::tensor::Tensor<B, D>,
) -> burn::tensor::Tensor<B, D> {
Self::FAMILY.residuum_tensor(a, b)
}
#[cfg(feature = "burn")]
#[inline]
fn neg_tensor<B: burn::tensor::backend::Backend, const D: usize>(
a: burn::tensor::Tensor<B, D>,
) -> burn::tensor::Tensor<B, D> {
Self::FAMILY.neg_tensor(a)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Godel;
impl TruthAlgebra for Godel {
const FAMILY: LogicFamily = LogicFamily::Godel;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Product;
impl TruthAlgebra for Product {
const FAMILY: LogicFamily = LogicFamily::Product;
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Lukasiewicz;
impl TruthAlgebra for Lukasiewicz {
const FAMILY: LogicFamily = LogicFamily::Lukasiewicz;
}
pub fn tnorm(family: TConormFamily, a: f64, b: f64) -> f64 {
tnorm_generic(family, a, b)
}
pub fn tnorm_generic<T: Float>(family: TConormFamily, a: T, b: T) -> T {
let a = clamp01(a);
let b = clamp01(b);
match family {
TConormFamily::Maximum => a.min(b),
TConormFamily::Probabilistic => a * b,
TConormFamily::Bounded => (a + b - T::one()).max(T::zero()),
TConormFamily::Drastic => drastic_tnorm(a, b),
TConormFamily::NilpotentMinimum => nilpotent_minimum_tnorm(a, b),
TConormFamily::SchweizerSklar { p } => schweizer_sklar_tnorm(a, b, cast(p)),
TConormFamily::SugenoWeber { p } => sugeno_weber_tnorm(a, b, cast(p)),
TConormFamily::AczelAlsina { p } => aczel_alsina_tnorm(a, b, cast(p)),
TConormFamily::MayorTorrens { p } => mayor_torrens_tnorm(a, b, cast(p)),
_ => T::one() - tconorm_generic(family, T::one() - a, T::one() - b),
}
}
pub fn residuum(family: TConormFamily, a: f64, b: f64) -> Option<f64> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
if a <= b {
return Some(1.0);
}
match family {
TConormFamily::Maximum => Some(b),
TConormFamily::NilpotentMinimum => Some((1.0 - a).max(b)),
TConormFamily::Drastic => None,
TConormFamily::MayorTorrens { .. } => Some(residuum_numeric(family, a, b, 64)),
_ => family.generator().map(|generator| generator.residuum(a, b)),
}
}
pub fn residuum_numeric(family: TConormFamily, a: f64, b: f64, iterations: usize) -> f64 {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
if a <= b {
return 1.0;
}
let mut lo = 0.0;
let mut hi = 1.0;
for _ in 0..iterations {
let mid = (lo + hi) * 0.5;
if tnorm(family, a, mid) <= b {
lo = mid;
} else {
hi = mid;
}
}
lo
}
pub fn tnorm_gradient(family: TConormFamily, a: f64, b: f64) -> BinaryGradient<f64> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
match family {
TConormFamily::Maximum => min_gradient(a, b),
TConormFamily::Probabilistic => BinaryGradient::new(b, a),
TConormFamily::Bounded => {
if a + b > 1.0 {
BinaryGradient::new(1.0, 1.0)
} else {
BinaryGradient::new(0.0, 0.0)
}
}
TConormFamily::Einstein => hamacher_tnorm_gradient(a, b, 2.0),
TConormFamily::Hamacher => hamacher_tnorm_gradient(a, b, 0.0),
TConormFamily::Yager { p } => yager_tnorm_gradient(a, b, p),
TConormFamily::Frank { s } => frank_tnorm_gradient(a, b, s),
TConormFamily::Dombi { p } => dombi_tnorm_gradient(a, b, p),
TConormFamily::Drastic => drastic_tnorm_gradient(a, b),
TConormFamily::NilpotentMinimum => nilpotent_minimum_tnorm_gradient(a, b),
TConormFamily::SchweizerSklar { p } => schweizer_sklar_tnorm_gradient(a, b, p),
TConormFamily::SugenoWeber { p } => sugeno_weber_tnorm_gradient(a, b, p),
TConormFamily::AczelAlsina { p } => aczel_alsina_tnorm_gradient(a, b, p),
TConormFamily::MayorTorrens { p } => mayor_torrens_tnorm_gradient(a, b, p),
}
}
pub fn tconorm_gradient(family: TConormFamily, a: f64, b: f64) -> BinaryGradient<f64> {
let a = a.clamp(0.0, 1.0);
let b = b.clamp(0.0, 1.0);
let dual = tnorm_gradient(family, 1.0 - a, 1.0 - b);
BinaryGradient::new(dual.da, dual.db)
}
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;