# © 2025 Nokia
# Licensed under the BSD 3-Clause License
# SPDX-License-Identifier: BSD-3-Clause
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from scipy.interpolate import interp1d
from .intersection import find_intersection
INFINITY = np.inf
"""Infinity (:math:`\\infty`)"""
REALS = (-INFINITY, INFINITY)
"""The set of real numbers (:math:`\\mathbb{R}``)"""
[docs]
class FuzzySet(dict):
"""
A *fuzzy set* is defined by a :math:`A = (U, \\mu)` pair,
where:
- :math:`U` is a set called universe;
- :math:`m: E \\rightarrow [0, 1]` is the membership
function of :math:̀`A`.
The :py:class:`FuzzySet` class implements a
`Fuzzy set <https://en.wikipedia.org/wiki/Fuzzy_set>`__
:math:`U \\subseteq \\mathbb{R}` having either
- a piecewise linear membership function;
- a discrete membership function.
In the :py:class:`FuzzySet` class implementation, you should
choose a universe as tight as possible with respect to the
`support
<https://en.wikipedia.org/wiki/Fuzzy_set#Crisp_sets_related_to_a_fuzzy_set>`__
of the fuzzy set you need to manipulate.
*Example:*
>>> import matplotlib.pyplot as plt
>>> from fuzzy_set import FuzzySet
>>> a1 = FuzzySet(
... dict(enumerate([0, 0, 0.1, 0.2, 0.7, 1, 1, 0.7, 0.2, 0.1, 0]))
... )
>>> a1.plot(label="$a_1$", marker="x")
[<matplotlib.lines.Line2D object at ...>]
>>> plt.legend()
<matplotlib.legend.Legend object at ...>
>>> plt.grid()
"""
def __init__(
self,
mu: dict[float, float],
is_continuous: bool = True,
e: tuple[float, float] = REALS
):
"""
Constructor.
Args:
mu (dict[float, float]): The membership function.
- If `is_continuous is True`, each (key, value)
pair corresponds to an element of the fuzzy set;
- If `is_continuous is False`, each (key, value)
pair characterizes a point of the membership
function which is assumed to be piecewise linear.
is_continuous (bool): Pass `True` if the membership
function is piecewise linear. Otherwise the
membership is supposed to be discrete.
e (tuple[float, float]): The definition interval of
:math:`mu`. This parameter is only relevant if
if ``is_continuous is True``, otherwise, the
definition of set of :math:`mu` is ``set(mu.keys())``.
"""
super().__init__(mu)
assert mu
assert mu[min(mu)] == 0
assert mu[max(mu)] == 0
self.is_continuous = is_continuous
if is_continuous:
xs = list(mu.keys())
ys = list(mu.values())
self._m = interp1d(xs, ys)
assert isinstance(e, tuple)
assert len(e) == 2
assert e[0] < e[1]
self.e = e
else:
self.e = set(mu.keys())
def _xs(self, other: "FuzzySet") -> iter:
"""
Implementation method, used to iterate on the relevant
value of this fuzzy set and another one.
Args:
other (FuzzySet): The other fuzzy set.
Returns:
The corresponding iterator.
"""
xs = sorted(set(self.keys()) | set(other.keys()))
if self.is_continuous:
assert other.is_continuous
# Search for interesction points. This is needed
# for set-based operation (i.e., &, |, -) as the
# resulting fuzzy set may require to consider
# additional points.
x_prev = y1_prev = y2_prev = None
for (i, x) in enumerate(xs):
y1 = self.m(x)
y2 = other.m(x)
if i > 0:
a = (x_prev, y1_prev)
b = (x, y1)
c = (x_prev, y2_prev)
d = (x, y2)
inter = find_intersection(a, b, c, d)
if inter and x_prev < inter[0] < x:
yield inter[0]
yield x
(x_prev, y1_prev, y2_prev) = (x, y1, y2)
if i == 0:
continue
else:
assert not other.is_continuous
yield from xs
[docs]
def m(self, x: float) -> float:
"""
Retrieves the membership degrees of an element.
If this :py:class:`FuzzySet` instance is continuous,
(i.e., if `self.is_continuous`), the value use obtained
using a linear interpolation.
Args:
x (float): The considered element.
Returns:
The membership value corresponding to `x`.
"""
assert isinstance(x, (int, float)), x
if self.is_continuous:
if x < min(self.keys()) or x > max(self.keys()):
return 0.0
elif x in self:
return self[x]
else:
ret = self._m(x)
if isinstance(ret, float):
return ret
return (
ret if isinstance(ret, float)
else float(ret)
)
else:
return self.get(x)
[docs]
def fully_contains(self, x: float) -> bool:
"""
Checks whether this :py:class:`FuzzySet` instance
fully contains an element.
Args:
x (float): The considered element.
Returns:
``True`` if ``x`` is fully contained in ``self``,
``False`` otherwise.
"""
return self.m(x) == 1
[docs]
def partially_contains(self, x: float) -> bool:
"""
Checks whether this :py:class:`FuzzySet` instance
partially contains an element.
Args:
x (float): The considered element.
Returns:
``True`` if ``x`` is partially contained in ``self``,
``False`` otherwise.
"""
return 0 < self.m(x) < 1
[docs]
def doesnt_contain(self, x: float) -> bool:
"""
Checks whether this :py:class:`FuzzySet` instance
doesn't contains an element.
Args:
x (float): The considered element.
Returns:
``True`` if ``x`` isn't contained in ``self``,
``False`` otherwise.
"""
return self.m(x) == 0
# Crisp sets related to a fuzzy set
# https://en.wikipedia.org/wiki/Fuzzy_set#Crisp_sets_related_to_a_fuzzy_set
[docs]
def cut(self, alpha: float) -> set:
"""
Computes an
:math:`\\alpha`-`cut
<https://en.wikipedia.org/wiki/Fuzzy_set#Crisp_sets_related_to_a_fuzzy_set>`__
of this fuzzy set, denoted by :math:`A^{x \\ge \\alpha}`.
Args:
alpha (float): The threshold involved in the
:math:`\\alpha`-cut.
Returns:
The crisp set containing every element involved in
the :math:`\\alpha`-cut.
"""
return set(x for x in self if self.m(x) >= alpha)
[docs]
def strong_cut(self, alpha: float) -> set:
"""
Assuming this fuzzy set is discrete, computes a strong
:math:`\\alpha`-`cut
<https://en.wikipedia.org/wiki/Fuzzy_set#Crisp_sets_related_to_a_fuzzy_set>`__
of this fuzzy set, denoted by :math:`A^{x \\gt \\alpha}`.
Args:
alpha (float): The :math:`alpha value, contained in
the :math:`[0.0, 1.0]` interval.
Returns:
The crisp set containing every element involved in
the :math:`\\alpha`-cut.
"""
return set(x for x in self if self.m(x) > alpha)
[docs]
def at(self, alpha: float) -> set:
"""
Assuming this fuzzy set is discrete, retrieves the
element that matches a given arbitrary :math:`alpha` value.
Args:
alpha (float): The :math:`alpha value, contained in
the :math:`[0.0, 1.0]` interval.
"""
return set(x for x in self if self.m(x) == alpha)
[docs]
def support(self) -> set:
"""
Assuming this fuzzy set is discrete, computes the `support
<https://en.wikipedia.org/wiki/Fuzzy_set#Crisp_sets_related_to_a_fuzzy_set>`__
of this fuzzy set, denoted by :math:`A^{>0}`.
Returns:
The crisp set containing every element involved in
the support.
"""
return self.strong_cut(0)
[docs]
def core(self) -> set:
"""
Assuming this fuzzy set is discrete, computes the `core
<https://en.wikipedia.org/wiki/Fuzzy_set#Crisp_sets_related_to_a_fuzzy_set>`__
of this fuzzy set, denoted by :math:`A^{=1}`.
Returns:
The crisp set containing every element involved in
the core.
"""
return self.at(1)
# Other definitions
# https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions
[docs]
def is_empty(self) -> bool:
"""
Assuming this fuzzy set is discrete,
checks whether this fuzzy set is empty.
Returns:
``True`` if this fuzzy set is empty,
``False`` otherwise.
"""
return all(self.m(x) == 0 for x in self)
def __eq__(self, other: "FuzzySet") -> bool:
"""
Checks whether two fuzzy sets are
`equal <https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions>`__.
Args:
other (FuzzySet): The right operand.
Returns:
``True`` if the fuzzy sets are equal,
``False`` otherwise.
"""
return (
self.e == other.e
and self.is_continuous == other.is_continuous
and all(self.m(x) == other.m(x) for x in self._xs(other))
)
def __ne__(self, other: "FuzzySet") -> bool:
"""
Checks whether two fuzzy sets are `different
<https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions>`__.
Args:
other (FuzzySet): The right operand.
Returns:
``True`` if the fuzzy sets are different,
``False`` otherwise.
"""
return (not self == other)
def __le__(self, other: "FuzzySet") -> bool:
"""
Checks whether this fuzzy set is `contained
<https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions>`__
in another fuzzy set.
Args:
other (FuzzySet): The right operand.
Returns:
``True`` if this fuzzy set is included in ``other``,
``False`` otherwise.
"""
assert self.e == other.e
return all(self.m(x) <= other.m(x) for x in self._xs(other))
def __lt__(self, other: "FuzzySet") -> bool:
"""
Checks whether this fuzzy set is `strictly contained
<https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions>`__
in another fuzzy set.
Args:
other (FuzzySet): The right operand.
Returns:
``True`` if this fuzzy set is included in ``other``,
``False`` otherwise.
"""
assert self.e == other.e
return all(self.m(x) < other.m(x) for x in self._xs(other))
def __ge__(self, other: "FuzzySet") -> bool:
"""
Checks whether this fuzzy set `contains
<https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions>`__
another fuzzy set.
Args:
other (FuzzySet): The right operand.
Returns:
``True`` if this fuzzy set contains ``other``,
``False`` otherwise.
"""
assert self.e == other.e
return all(self.m(x) >= other.m(x) for x in self._xs(other))
def __gt__(self, other: "FuzzySet") -> bool:
"""
Checks whether this fuzzy set `striclty contains
<https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions>`__
another fuzzy set.
Args:
other (FuzzySet): The right operand.
Returns:
``True`` if this fuzzy set contains ``other``,
``False`` otherwise.
"""
assert self.e == other.e
return all(self.m(x) >= other.m(x) for x in self._xs(other))
[docs]
def is_crossover(self, x: float) -> bool:
"""
Checks whether a value is a `crossover point
<https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions>`__
of this fuzzy set.
Args:
x (float): A value in ``self``.
Returns:
``True`` if ``x`` is a crossover points of this fuzzy set,
``False`` otherwise.
"""
return self.at(0.5)
[docs]
def height(self) -> float:
"""
Computes the `height
<https://en.wikipedia.org/wiki/Fuzzy_set#Other_definitions>`__
of this fuzzy set.
Returns:
The height of this fuzzy set.
"""
return max(self.m(x) for x in self)
[docs]
def is_normalized(self) -> bool:
"""
Tests whether this fuzzy set is `normalized
<https://en.wikipedia.org/wiki/Fuzzy_set>`__.
Returns:
``True`` if this fuzzy set is normalized,
``False`` otherwise.
"""
return self.height() == 1
[docs]
def width(self) -> float:
"""
Computes the
`width <https://en.wikipedia.org/wiki/Fuzzy_set>`__
of this fuzzy set.
Returns:
The width of this fuzzy set.
"""
return self.height() - min(self.m(x) for x in self)
# Fuzzy set operations
# https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations
def __not__(self) -> "FuzzySet":
"""
Assuming this fuzzy set membership is piecewise linear,
computes the `complement
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of this fuzzy set.
Returns:
The complement of this fuzzy set.
"""
return FuzzySet(set(self), lambda x: 1 - self.m(x))
def __neg__(self) -> "FuzzySet":
"""
Assuming this fuzzy set membershup is piecewise linear,
computes the `negation
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of this fuzzy set.
Returns:
The negation of this fuzzy set.
"""
return not self
def _intersection_union(
self,
other: "FuzzySet",
norm: callable
) -> "FuzzySet":
"""
Implementation method, used when computing the intersection
or the union of this uzzy set and another one.
See also the
:py:meth:`FuzzySet.intersection`,
:py:meth:`FuzzySet.union`,
:py:meth:`FuzzySet.__and__`,
:py:meth:`FuzzySet.__or__` methods.
Args:
other (FuzzySet): The other fuzzy set.
Returns:
The corresponding iterator.
"""
return FuzzySet({
x: norm(self.m(x), other.m(x))
for x in self._xs(other)
})
[docs]
def intersection(
self,
other: "FuzzySet",
tnorm: callable = min
) -> "FuzzySet":
"""
Computes the `intersection
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of two fuzzy-sets according to an arbitrary
`T-norm <https://en.wikipedia.org/wiki/T-norm>`__
(e.g., :math:`min` ).
Args:
other (FuzzySet): The right operand.
tnorm (callable): A `T-norm
<https://en.wikipedia.org/wiki/T-norm>`__.
Returns:
The fuzzy set intersection.
"""
return self._intersection_union(other, tnorm)
def __and__(self, other: "FuzzySet") -> "FuzzySet":
"""
Computes the `(default) intersection
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of two fuzzy-sets.
Args:
other (FuzzySet): The right operand.
Returns:
The default fuzzy set intersection.
"""
return self.intersection(other, min)
[docs]
def union(self, other: "FuzzySet", snorm: callable = min) -> "FuzzySet":
"""
Computes the `union
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of two fuzzy-sets according to an arbitrary
`S-norm <https://en.wikipedia.org/wiki/T-norm>`__ (e.g., :math:`min` ).
Args:
other (FuzzySet): The right operand.
snorm (callable): A `T-norm
<https://en.wikipedia.org/wiki/T-norm>`__.
Returns:
The fuzzy set intersection.
"""
return self._intersection_union(other, snorm)
def __or__(self, other: "FuzzySet") -> "FuzzySet":
"""
Computes the `(default) union
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of two fuzzy-sets.
Args:
other (FuzzySet): The right operand.
Returns:
The default fuzzy set intersection.
"""
return self.union(other, max)
def __pow__(self, nu: float) -> "FuzzySet":
"""
Computes the :math:`nu`-`power
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of this fuzzy set.
Args:
nu (float): A positive real.
Returns:
The corresponding fuzzy set.
"""
assert nu >= 0
return FuzzySet({
x: y ** nu
for (x, y) in self.items()
})
[docs]
def concentration(self) -> "FuzzySet":
"""
Computes the `concentration
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of this fuzzy set.
Returns:
The corresponding fuzzy set.
"""
return self ** 2
[docs]
def difference(self, other: "FuzzySet", tnorm: callable) -> "FuzzySet":
"""
Computes the `difference
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of two fuzzy-sets.
Args:
other (FuzzySet): The right operand.
tnorm (callable): A `T-norm
<https://en.wikipedia.org/wiki/T-norm>`__.
Returns:
The fuzzy set difference.
"""
return FuzzySet({
x: self.m(x) - tnorm(self.m(x), other.m(x))
for x in self._xs(other)
})
def __sub__(self, other: "FuzzySet", tnorm: callable = min) -> "FuzzySet":
"""
Computes the `default difference
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of two fuzzy-sets.
Args:
other (FuzzySet): The right operand.
tnorm (callable): A `T-norm
<https://en.wikipedia.org/wiki/T-norm>`__.
Returns:
The default fuzzy set difference.
"""
return FuzzySet({
# x: min(self.m(x), 1 - other.m(x))
x: self.m(x) - tnorm(self.m(x), other.m(x))
for x in self._xs(other)
})
[docs]
def absolute_difference(self, other: "FuzzySet") -> "FuzzySet":
"""
Computes the `absolute default difference
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__
of two fuzzy-sets.
Args:
other (FuzzySet): The right operand.
Returns:
The default fuzzy set absolute difference.
"""
return FuzzySet({
x: abs(self.m(x) - other.m(x))
for x in self._xs(other)
})
# Disjoint fuzzy sets
# https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations
[docs]
def is_disjoint(self, other: "FuzzySet") -> bool:
"""
Tests whether this fuzzy set and another are `disjoint
<https://en.wikipedia.org/wiki/Fuzzy_set#Fuzzy_set_operations>`__.
Args:
other (FuzzySet): The fuzzy set compared to ``self``.
Returns:
``True`` if the fuzzy sets are disjoint,
``False`` otherwise.
"""
return all(
self.m(x) == 0 or other.m(x) == 0
for x in self._xs(other)
)
# Scalar cardinality
# https://en.wikipedia.org/wiki/Fuzzy_set#Scalar_cardinality
[docs]
def card(self) -> float:
"""
Computes the `scalar cardinality
<https://en.wikipedia.org/wiki/Fuzzy_set#Scalar_cardinality>`__
of a fuzzy set.
Returns:
The scalar cardinality of this fuzzy set.
"""
return sum(self.m(x) for x in self)
[docs]
def relative_card(self, other: "FuzzySet" = None) -> float:
"""
Computes the `relative cardinality
<https://en.wikipedia.org/wiki/Fuzzy_set#Scalar_cardinality>`__
of this fuzzy set or the intersection of two fuzzy set.
Args:
g (FuzzySet): Another fuzzy set.
Pass ``None`` or ``self`` is equivalent.
Returns:
The relative cardinality of this fuzzy set.
"""
if not other or other is self:
return self.card() / len(self)
else:
return (self & other) / len(self)
# Distance and similarity
# https://en.wikipedia.org/wiki/Fuzzy_set#Distance_and_similarity
[docs]
def plot(
self,
*cls,
ax: matplotlib.axes.Axes = None,
**kwargs
) -> matplotlib.collections.PathCollection:
"""
Plots this Fuzzy Set.
Args:
cls, args: See the :py:func:`plt.plot` function.
ax (matplotlib.axes.Axes): The axes of the figure where
this :py:class:`FuzzySet` instance must be plotted.
Pass `None` if not needed.
Returns:
The resulting corresponding plot.
"""
if ax is None:
ax = plt.gca()
xs = sorted(list(self))
ys = list(self.m(x) for x in xs)
return ax.plot(xs, ys, *cls, **kwargs)
def __repr__(self) -> str:
return (
f"{self.__class__.__name__}<"
f"{super().__repr__()}, "
f"{'continuous' if self.is_continuous else 'discrete'}"
">"
)
def __str__(self) -> str:
return self.__repr__()