use crate::core::scalar::ControlScalar;
use crate::fuzzy::FuzzyError;
use heapless::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TNorm {
Product,
Min,
}
impl TNorm {
#[inline]
pub fn apply<S: ControlScalar>(&self, a: S, b: S) -> S {
match self {
TNorm::Product => a * b,
TNorm::Min => {
if a < b {
a
} else {
b
}
}
}
}
pub fn fold<S: ControlScalar>(&self, values: &[S]) -> S {
values
.iter()
.copied()
.fold(S::ONE, |acc, v| self.apply(acc, v))
}
}
#[derive(Debug, Clone, Copy)]
pub struct FuzzySet<S: ControlScalar> {
pub name_hash: u32,
pub membership: S,
}
impl<S: ControlScalar> FuzzySet<S> {
pub fn new(name_hash: u32, membership: S) -> Self {
Self {
name_hash,
membership,
}
}
}
pub fn fnv1a_hash(name: &str) -> u32 {
let mut hash: u32 = 2_166_136_261;
for byte in name.bytes() {
hash ^= byte as u32;
hash = hash.wrapping_mul(16_777_619);
}
hash
}
pub struct LinguisticVar<S: ControlScalar, const N: usize> {
pub var_hash: u32,
#[allow(clippy::type_complexity)]
fns: Vec<(u32, fn(S) -> S), N>,
}
impl<S: ControlScalar, const N: usize> LinguisticVar<S, N> {
pub fn new(name: &str) -> Self {
Self {
var_hash: fnv1a_hash(name),
fns: Vec::new(),
}
}
pub fn add_term(&mut self, term_name: &str, f: fn(S) -> S) -> Result<(), FuzzyError> {
self.fns
.push((fnv1a_hash(term_name), f))
.map_err(|_| FuzzyError::CapacityExceeded)
}
pub fn fuzzify(&self, x: S) -> Vec<FuzzySet<S>, N> {
let mut out: Vec<FuzzySet<S>, N> = Vec::new();
for (hash, f) in self.fns.iter() {
let mu = f(x);
let mu_clamped = mu.clamp_val(S::ZERO, S::ONE);
let _ = out.push(FuzzySet::new(*hash, mu_clamped));
}
out
}
pub fn membership_at(&self, x: S, idx: usize) -> Option<S> {
self.fns.get(idx).map(|(_, f)| {
let mu = f(x);
mu.clamp_val(S::ZERO, S::ONE)
})
}
pub fn len(&self) -> usize {
self.fns.len()
}
pub fn is_empty(&self) -> bool {
self.fns.is_empty()
}
}
pub const MAX_ANTECEDENTS: usize = 4;
#[derive(Debug, Clone, Copy)]
pub struct Condition {
pub var_idx: usize,
pub set_idx: usize,
}
#[derive(Debug, Clone)]
pub struct Antecedent {
pub conditions: Vec<Condition, MAX_ANTECEDENTS>,
}
impl Antecedent {
pub fn new() -> Self {
Self {
conditions: Vec::new(),
}
}
pub fn add(&mut self, var_idx: usize, set_idx: usize) -> Result<(), FuzzyError> {
self.conditions
.push(Condition { var_idx, set_idx })
.map_err(|_| FuzzyError::CapacityExceeded)
}
}
impl Default for Antecedent {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy)]
pub struct Consequent {
pub var_idx: usize,
pub set_idx: usize,
pub weight: f64,
}
impl Consequent {
pub fn new(var_idx: usize, set_idx: usize, weight: f64) -> Self {
Self {
var_idx,
set_idx,
weight,
}
}
pub fn unit(var_idx: usize, set_idx: usize) -> Self {
Self::new(var_idx, set_idx, 1.0)
}
}
#[derive(Debug, Clone)]
pub struct FuzzyRule {
pub antecedent: Antecedent,
pub consequent: Consequent,
}
impl FuzzyRule {
pub fn new(antecedent: Antecedent, consequent: Consequent) -> Self {
Self {
antecedent,
consequent,
}
}
}
pub struct RuleBase<const R: usize> {
rules: Vec<FuzzyRule, R>,
pub t_norm: TNorm,
}
impl<const R: usize> RuleBase<R> {
pub fn new(t_norm: TNorm) -> Self {
Self {
rules: Vec::new(),
t_norm,
}
}
pub fn add_rule(&mut self, rule: FuzzyRule) -> Result<(), FuzzyError> {
self.rules
.push(rule)
.map_err(|_| FuzzyError::CapacityExceeded)
}
pub fn len(&self) -> usize {
self.rules.len()
}
pub fn is_empty(&self) -> bool {
self.rules.is_empty()
}
pub fn fire_rule<S: ControlScalar>(&self, rule: &FuzzyRule, input_memberships: &[&[S]]) -> S {
if rule.antecedent.conditions.is_empty() {
return S::ZERO;
}
let mut strength = S::ONE;
for cond in rule.antecedent.conditions.iter() {
let mu = match input_memberships.get(cond.var_idx) {
Some(sets) => match sets.get(cond.set_idx) {
Some(&m) => m,
None => return S::ZERO,
},
None => return S::ZERO,
};
strength = self.t_norm.apply(strength, mu);
}
let w = S::from_f64(rule.consequent.weight);
strength * w
}
pub fn fire_all<S: ControlScalar>(
&self,
input_memberships: &[&[S]],
) -> Vec<(S, Consequent), R> {
let mut out: Vec<(S, Consequent), R> = Vec::new();
for rule in self.rules.iter() {
let strength = self.fire_rule(rule, input_memberships);
let _ = out.push((strength, rule.consequent));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_rule(v0: usize, s0: usize, v1: usize, s1: usize) -> FuzzyRule {
let mut ant = Antecedent::new();
ant.add(v0, s0).unwrap();
ant.add(v1, s1).unwrap();
let con = Consequent::unit(0, 0);
FuzzyRule::new(ant, con)
}
#[test]
fn tnorm_product_both_half() {
let t = TNorm::Product;
let v: f64 = t.apply(0.5, 0.5);
assert!((v - 0.25).abs() < 1e-12);
}
#[test]
fn tnorm_min_selects_smaller() {
let t = TNorm::Min;
assert_eq!(t.apply(0.3_f64, 0.7_f64), 0.3);
}
#[test]
fn tnorm_fold_empty_is_one() {
let t = TNorm::Product;
let v: f64 = t.fold(&[]);
assert_eq!(v, 1.0);
}
#[test]
fn single_rule_fires_correctly_product() {
let mut rb: RuleBase<4> = RuleBase::new(TNorm::Product);
rb.add_rule(make_rule(0, 0, 1, 0)).unwrap();
let v0 = [0.8_f64];
let v1 = [0.6_f64];
let memberships: &[&[f64]] = &[&v0, &v1];
let fired = rb.fire_all(memberships);
assert_eq!(fired.len(), 1);
assert!((fired[0].0 - 0.48).abs() < 1e-12);
}
#[test]
fn single_rule_fires_correctly_min() {
let mut rb: RuleBase<4> = RuleBase::new(TNorm::Min);
rb.add_rule(make_rule(0, 0, 1, 0)).unwrap();
let v0 = [0.8_f64];
let v1 = [0.6_f64];
let memberships: &[&[f64]] = &[&v0, &v1];
let fired = rb.fire_all(memberships);
assert!((fired[0].0 - 0.6).abs() < 1e-12);
}
#[test]
fn contradictory_input_zero_firing() {
let mut rb: RuleBase<4> = RuleBase::new(TNorm::Min);
rb.add_rule(make_rule(0, 0, 1, 0)).unwrap();
let v0 = [0.0_f64];
let v1 = [1.0_f64];
let memberships: &[&[f64]] = &[&v0, &v1];
let fired = rb.fire_all(memberships);
assert_eq!(fired[0].0, 0.0);
}
#[test]
fn out_of_bounds_var_returns_zero() {
let rb: RuleBase<4> = RuleBase::new(TNorm::Product);
let mut ant = Antecedent::new();
ant.add(99, 0).unwrap(); let con = Consequent::unit(0, 0);
let rule = FuzzyRule::new(ant, con);
let v0 = [1.0_f64];
let memberships: &[&[f64]] = &[&v0];
let strength = rb.fire_rule::<f64>(&rule, memberships);
assert_eq!(strength, 0.0);
}
#[test]
fn rule_weight_scales_output() {
let mut rb: RuleBase<4> = RuleBase::new(TNorm::Min);
let mut ant = Antecedent::new();
ant.add(0, 0).unwrap();
let con = Consequent::new(0, 0, 0.5); rb.add_rule(FuzzyRule::new(ant, con)).unwrap();
let v0 = [1.0_f64];
let memberships: &[&[f64]] = &[&v0];
let fired = rb.fire_all(memberships);
assert!((fired[0].0 - 0.5).abs() < 1e-12);
}
#[test]
fn linguistic_var_fuzzify() {
let mut var: LinguisticVar<f64, 3> = LinguisticVar::new("temperature");
var.add_term("cold", |x| if x < 20.0 { 1.0 } else { 0.0 })
.unwrap();
var.add_term("warm", |x| {
if (15.0..=25.0).contains(&x) {
(x - 15.0) / 10.0
} else {
0.0
}
})
.unwrap();
var.add_term("hot", |x| if x > 30.0 { 1.0 } else { 0.0 })
.unwrap();
let sets = var.fuzzify(18.0);
assert_eq!(sets.len(), 3);
assert!((sets[0].membership - 1.0).abs() < 1e-9);
assert_eq!(sets[2].membership, 0.0);
}
#[test]
fn fnv1a_hash_deterministic() {
let h1 = fnv1a_hash("error");
let h2 = fnv1a_hash("error");
assert_eq!(h1, h2);
assert_ne!(fnv1a_hash("error"), fnv1a_hash("error_rate"));
}
}