Source code for zadeh.sets

from math import exp

import numpy as np

from .context import get_active_context

try:
    from math import prod  # Python >= 3.8
except ImportError:
[docs] def prod(xx): result = 1 for x in xx: result *= x return result
def _clip(x, min=0, max=1): """Clip to interval, defaults to [0, 1]""" return np.clip(x, min, max)
[docs]class FuzzySet: """A fuzzy set""" def __init__(self, mu=None): self.mu = mu def _get_description(self): raise NotImplementedError("Descriptions are only available for primitive types") @staticmethod def _from_description(description): return _set_types[description["type"]]._from_description(description) def __call__(self, x): assert self.mu is not None, "A membership function has to be defined" return self.mu(x) def _to_c(self, name): raise NotImplementedError("C code generation not available. Overwrite the _to_c method if you know what " "you are doing.") def __mul__(self, other): if isinstance(other, (float, int)): return FuzzySetScaled(self, other) raise NotImplementedError("Multiplication is only defined with crisp numbers") def __rmul__(self, other): return self.__mul__(other) # TODO: More generalized fuzzy set operations could be defined def __neg__(self): return FuzzySetNeg(self) def __or__(self, other): return FuzzySetOr([self, other]) def __and__(self, other): return FuzzySetAnd([self, other])
[docs]class FuzzySetNeg(FuzzySet): """A negation operation on a Fuzzy set""" def __init__(self, set): super().__init__() self.set = set def __call__(self, x): return 1 - self.set(x) def _to_c(self, name): return "1 - (%s)" % self.set._to_c(name)
[docs]class FuzzySetOr(FuzzySet): """An OR operation between Fuzzy sets""" def __init__(self, sets, method=None): super().__init__() self.sets = sets self.method = method def __call__(self, x): method = get_active_context().OR if self.method is None else self.method if method == "max": return max(s(x) for s in self.sets) elif method == "psum": return 1 - prod(1 - s(x) for s in self.sets) elif method == "bsum": return min(1, sum(s(x) for s in self.sets)) else: raise ValueError("Invalid OR method in context: %s" % method) def _to_c(self, name): method = get_active_context().OR if self.method is None else self.method if method == "max": return "max(%d, %s)" % (len(self.sets), ", ".join(s._to_c(name) for s in self.sets)) elif method == "psum": return "1 - %s" % " * ".join("(1 - %s)" % s._to_c(name) for s in self.sets) elif method == "bsum": return "min(2, 1, %s)" % " + ".join("(%s)" % s._to_c(name) for s in self.sets) else: raise ValueError("Invalid OR method in context: %s" % method)
[docs]class FuzzySetAnd(FuzzySet): """An AND operation between Fuzzy sets""" def __init__(self, sets, method=None): super().__init__() self.sets = sets self.method = method def __call__(self, x): method = get_active_context().AND if self.method is None else self.method if method == "min": return min(s(x) for s in self.sets) elif method == "product": return prod(s(x) for s in self.sets) elif method == "lukasiewicz": return max(0, sum(s(x) for s in self.sets) - (len(self.sets) - 1)) else: raise ValueError("Invalid AND method in context: %s" % method) def _to_c(self, name): method = get_active_context().AND if self.method is None else self.method if method == "min": return "min(%d, %s)" % (len(self.sets), ", ".join(s._to_c(name) for s in self.sets)) elif method == "product": return " * ".join("(%s)" % s._to_c(name) for s in self.sets) elif method == "lukasiewicz": return "max(2, 0, %s - %d)" % (" + ".join(s._to_c(name) for s in self.sets), len(self.sets) - 1) else: raise ValueError("Invalid OR method in context: %s" % method)
[docs]class FuzzySetScaled(FuzzySet): """A scaled Fuzzy sets""" def __init__(self, set, scale): super().__init__() self.set = set self.scale = scale def __call__(self, x): return self.set(x) * self.scale def _to_c(self, name): return "%f * (%s)" % (self.scale, self.set._to_c(name))
[docs]class SingletonSet(FuzzySet): """A singleton fuzzy set (Kronecker delta)""" def __init__(self, x): """ Args: x: The unique value where membership is 1. """ super().__init__(lambda y: 1 if x == y else 0) self.x = x def _get_description(self): return {"type": "singleton", "x": self.x} @staticmethod def _from_description(description): return SingletonSet(description["x"]) def _to_c(self, name): return "({x}=={a})?1.0:0.0".format(x=name, a=self.x)
[docs]class DiscreteFuzzySet(FuzzySet): """A discrete fuzzy set (non-null in a discrete set of points)""" def __init__(self, d): """ Args: d (dict): Mapping of values to non-null membership values """ self.d = d super().__init__() def __call__(self, x): return self.d.get(x, 0) def _to_c(self, name): raise NotImplementedError("C code not available for DiscreteFuzzySet") def _get_description(self): return {"type": "discrete", "d": self.d} @staticmethod def _from_description(description): return DiscreteFuzzySet(description["d"])
[docs]class TriangularFuzzySet(FuzzySet): """A fuzzy set defined by a triangular function""" def __init__(self, a, b, c): self.a = a self.b = b self.c = c super().__init__() def _get_description(self): return {"type": "triangular", "a": self.a, "b": self.b, "c": self.c} @staticmethod def _from_description(description): return TriangularFuzzySet(description["a"], description["b"], description["c"]) def __call__(self, x): if x < self.a or x > self.c: return 0.0 if x < self.b: if self.b == self.a: return 1.0 return (x - self.a) / (self.b - self.a) if self.c == self.b: return 1.0 return (self.c - x) / (self.c - self.b) def _to_c(self, name): return "triangular({a},{b},{c},{x})".format(x=name, a=self.a, b=self.b, c=self.c)
[docs]class TrapezoidalFuzzySet(FuzzySet): """A fuzzy set defined by a trapezoidal function""" def __init__(self, a, b, c, d): self.a = a self.b = b self.c = c self.d = d super().__init__() def _get_description(self): return {"type": "trapezoidal", "a": self.a, "b": self.b, "c": self.c, "d": self.d} @staticmethod def _from_description(description): return TrapezoidalFuzzySet(description["a"], description["b"], description["c"], description["d"]) def __call__(self, x): if x < self.a or x > self.d: return 0.0 if x < self.b: if self.b == self.a: return 1.0 return (x - self.a) / (self.b - self.a) if x > self.c: if self.d == self.c: return 1.0 return (self.d - x) / (self.d - self.c) return 1.0 def _to_c(self, name): return "trapezoidal({a},{b},{c},{d},{x})".format(x=name, a=self.a, b=self.b, c=self.c, d=self.d)
def _gauss(x, s, a): return exp(-((x - a) / s) ** 2 / 2)
[docs]class GaussianFuzzySet(FuzzySet): """A fuzzy set defined by a Gaussian function .. math:: G_{s,a}(x) = \\mathrm{e}^{-\\frac{{(x - a)}^2}{2 s^2}} Parameters: s: Width of the Gaussian. a: Position of the peak of the Gaussian. """ def __init__(self, s, a): self.s = s self.a = a super().__init__() def _get_description(self): return {"type": "gaussian", "s": self.s, "a": self.a} @staticmethod def _from_description(description): return GaussianFuzzySet(description["s"], description["a"]) def __call__(self, x): return _gauss(x, self.s, self.a) def _to_c(self, name): return "gauss({x}, {s}, {a})".format(x=name, s=self.s, a=self.a)
def _gauss2(x, s1, a1, s2, a2): if a1 <= x <= a2: return 1 if x < a1: return _gauss(x, s1, a1) return _gauss(x, s2, a2)
[docs]class Gaussian2FuzzySet(FuzzySet): """ A fuzzy set defined by two Gaussian functions .. math:: G^2_{s_1,a_1,s_2,a_2}(x) = \\begin{cases} G_{s_1, a_1}(x), &\\quad\\text{if } x\\leq a_1\\\\ 1, &\\quad\\text{if } a_1 \\leq x \\leq a_2\\\\ G_{s_2, a_2}(x), &\\quad\\text{if } x\\geq a_2\\\\ \\end{cases} Parameters: s1: Width of the first Gaussian. a1: Start of the membership=1.0 plateau. s2: Width of the second Gaussian. a2: End of the membership=1.0 plateau. """ def __init__(self, s1, a1, s2, a2): assert a1 <= a2, "Positions must be ordered (a1 <= a2)" self.s1 = s1 self.a1 = a1 self.s2 = s2 self.a2 = a2 super().__init__() def _get_description(self): return {"type": "gaussian2", "s1": self.s1, "a1": self.a1, "s2": self.s2, "a2": self.a2} @staticmethod def _from_description(description): return Gaussian2FuzzySet(description["s1"], description["a1"], description["s2"], description["a2"]) def __call__(self, x): return _gauss2(x, self.s1, self.a1, self.s2, self.a2) def _to_c(self, name): return "gauss2({x}, {s1}, {a1}, {s2}, {a2})".format(x=name, s1=self.s1, a1=self.a1, s2=self.s2, a2=self.a2)
[docs]class BellFuzzySet(FuzzySet): """A fuzzy set defined by a generalized Bell MF :math:`\\mu_{a,b,c}(x)= \\frac{1}{1+\\left|\frac{x-c}{a}\\right|^{2b}}` """ def __init__(self, a, b, c): self.a = a self.b = b self.c = c super().__init__() def _get_description(self): return {"type": "bell", "a": self.a, "b": self.b, "c": self.c} @staticmethod def _from_description(description): return BellFuzzySet(description["a"], description["b"], description["c"]) def __call__(self, x): return 1 / (1 + ((x - self.c) / self.a) ** (2 * self.b)) def _to_c(self, name): return "1 / (1 + pow(({x} - {c}) / {a}, 2*{b}) )".format(x=name, a=self.a, b=self.b, c=self.c)
[docs]class SigmoidalFuzzySet(FuzzySet): """ A fuzzy set defined by a sigmoid function :math:`\\sigma_{a,c}(x)= \\frac{1}{1+e^{\\left(-a (x-c)\\right)}}` If a>0, the sigmoid is increasing, otherwise decreasing. The magnitude of "a" defines the width of the transition, "c" defines its location. """ def __init__(self, a, c): self.a = a self.c = c super().__init__() def _get_description(self): return {"type": "sigmoidal", "a": self.a, "c": self.c} @staticmethod def _from_description(description): return SigmoidalFuzzySet(description["a"], description["c"]) def __call__(self, x): return 1 / (1 + exp(-self.a * (x - self.c))) def _to_c(self, name): return "1 / (1 + exp(-{a} * ({x} - {c})))".format(x=name, a=self.a, c=self.c)
[docs]class SigmoidalProductFuzzySet(FuzzySet): """ A fuzzy set defined by the product of two sigmoid functions :math:`\\sigma_{a_1,c_1,a_2,c_2}^\\mathrm{p}(x)=\\sigma_{a_1,c_1}(x)\\cdot \\sigma_{a_2,c_2}(x)` Typical unimodal membership functions are defined setting the opposite sign for (a1, a2), and choosing (c1, c2) enough apart for both sigmoids reach ~1 in a common subset. """ def __init__(self, a1, c1, a2, c2): self.a1 = a1 self.c1 = c1 self.a2 = a2 self.c2 = c2 super().__init__() def _get_description(self): return {"type": "sigmoidal_product", "a1": self.a1, "c1": self.c1, "a2": self.a2, "c2": self.c2} @staticmethod def _from_description(description): return SigmoidalProductFuzzySet(description["a1"], description["c1"], description["a2"], description["c2"]) def __call__(self, x): return (1 / (1 + exp(-self.a1 * (x - self.c1)))) * (1 / (1 + exp(-self.a2 * (x - self.c2)))) def _to_c(self, name): return "(1 / (1 + exp(-{a1} * ({x} - {c1})))) * (1 / (1 + exp(-{a2} * ({x} - {c2}))))".format(x=name, a1=self.a1, c1=self.c1, a2=self.a2, c2=self.c2)
[docs]class SigmoidalDifferenceFuzzySet(FuzzySet): """ A fuzzy set defined by the difference of two sigmoid functions clipped to [0, 1] :math:`\\sigma_{a_1,c_1,a_2,c_2}^\\mathrm{d}(x)=\\mathrm{clip}_{0,1}(\\sigma_{a_1,c_1}(x) - \\sigma_{a_2,c_2}(x))` Typical unimodal membership functions are defined setting the same sign for (a1, a2), and choosing (c1, c2) enough apart for both sigmoids reach ~1 in a common subset. """ def __init__(self, a1, c1, a2, c2): self.a1 = a1 self.c1 = c1 self.a2 = a2 self.c2 = c2 super().__init__() def _get_description(self): return {"type": "sigmoidal_difference", "a1": self.a1, "c1": self.c1, "a2": self.a2, "c2": self.c2} @staticmethod def _from_description(description): return SigmoidalDifferenceFuzzySet(description["a1"], description["c1"], description["a2"], description["c2"]) def __call__(self, x): return _clip((1 / (1 + exp(-self.a1 * (x - self.c1)))) - (1 / (1 + exp(-self.a2 * (x - self.c2))))) def _to_c(self, name): return "clip((1 / (1 + exp(-{a1} * ({x} - {c1})))) - (1 / (1 + exp(-{a2} * ({x} - {c2})))))".format(x=name, a1=self.a1, c1=self.c1, a2=self.a2, c2=self.c2)
def _s_shaped(x, a, b): if x <= a: return 0.0 if x >= b: return 1.0 if x <= (a + b) / 2: # (a, (a+b)/2] return 2.0 * ((x - a) / (b - a)) ** 2 # ((a+b)/2, b) return 1.0 - 2.0 * ((x - b) / (b - a)) ** 2
[docs]class SFuzzySet(FuzzySet): """ A fuzzy set defined by an S-shaped function. .. math:: S_{a,b}(x) = \\begin{cases} 0, &\\quad\\text{if } x\\leq a\\\\ 2\\left(\\frac{x-a}{b-a}\\right)^2, &\\quad\\text{if } a \\leq x\\leq \\frac{a+b}{2}\\\\ 1-2\\left(\\frac{x-b}{b-a}\\right)^2, &\\quad\\text{if } \\frac{a+b}{2} \\leq x \\leq b\\\\ 1, &\\quad\\text{if } x\\geq b\\\\ \\end{cases} """ def __init__(self, a, b): self.a = a self.b = b super().__init__() def _get_description(self): return {"type": "s_shaped", "a": self.a, "b": self.b} @staticmethod def _from_description(description): return SFuzzySet(description["a"], description["b"]) def __call__(self, x): return _s_shaped(x, self.a, self.b) def _to_c(self, name): return "s_shaped({x}, {a}, {b})".format(x=name, a=self.a, b=self.b)
def _z_shaped(x, a, b): if x <= a: return 1.0 if x >= b: return 0.0 if x <= (a + b) / 2: # (a, (a+b)/2] return 1.0 - 2.0 * ((x - a) / (b - a)) ** 2 # ((a+b)/2, b) return 2.0 * ((x - b) / (b - a)) ** 2
[docs]class ZFuzzySet(FuzzySet): """ A fuzzy set defined by a Z-shaped function .. math:: Z_{a,b}(x) = \\begin{cases} 0, &\\quad\\text{if } x\\leq a\\\\ 1-2\\left(\\frac{x-a}{b-a}\\right)^2, &\\quad\\text{if } a \\leq x\\leq \\frac{a+b}{2}\\\\ 2\\left(\\frac{x-b}{b-a}\\right)^2, &\\quad\\text{if } \\frac{a+b}{2} \\leq x \\leq b\\\\ 1, &\\quad\\text{if } x\\geq b\\\\ \\end{cases} """ def __init__(self, a, b): self.a = a self.b = b super().__init__() def _get_description(self): return {"type": "z_shaped", "a": self.a, "b": self.b} @staticmethod def _from_description(description): return ZFuzzySet(description["a"], description["b"]) def __call__(self, x): return _z_shaped(x, self.a, self.b) def _to_c(self, name): return "z_shaped({x}, {a}, {b})".format(x=name, a=self.a, b=self.b)
[docs]class PiFuzzySet(FuzzySet): """ A fuzzy set defined by a Pi-shaped function (a combination of S-shaped MF followed by a Z-shaped MF) .. math:: Z_{a,b,c,d}(x) = S_{a,b}(x)\\cdot Z_{c,d}(x) """ def __init__(self, a, b, c, d): self.a = a self.b = b self.c = c self.d = d super().__init__() def _get_description(self): return {"type": "pi_shaped", "a": self.a, "b": self.b, "c": self.c, "d": self.d} @staticmethod def _from_description(description): return PiFuzzySet(description["a"], description["b"], description["c"], description["d"]) def __call__(self, x): return _s_shaped(x, self.a, self.b) * _z_shaped(x, self.c, self.d) def _to_c(self, name): return "s_shaped({x}, {a}, {b}) * z_shaped({x}, {c}, {d})".format(x=name, a=self.a, b=self.b, c=self.c, d=self.d)
_set_types = {"singleton": SingletonSet, "discrete": DiscreteFuzzySet, "sigmoid": SigmoidalFuzzySet, "sigmoidal_product": SigmoidalProductFuzzySet, "sigmoidal_difference": SigmoidalDifferenceFuzzySet, "s_shaped": SFuzzySet, "z_shaped": ZFuzzySet, "pi_shaped": PiFuzzySet, "bell": BellFuzzySet, "gaussian": GaussianFuzzySet, "gaussian2": Gaussian2FuzzySet, "triangular": TriangularFuzzySet, "trapezoidal": TrapezoidalFuzzySet}