import bisect
import numpy as np
from .context import get_active_context
try:
import matplotlib.pyplot as plt
except ImportError:
plt = None
try:
import ipywidgets
except ImportError:
ipywidgets = None
[docs]class Domain:
"""A domain where diffuse sets are defined"""
def __init__(self, name):
self.name = name
def _get_description(self):
raise NotImplementedError
@staticmethod
def _from_description(description):
return _domain_subclasses[description["type"]]._from_description(description)
[docs] def get_mesh(self):
"""Get a mesh representing the domain"""
raise NotImplementedError("A domain subclass must be used instead.")
[docs] def evaluate_set(self, set):
"""Get a pair of list with a mesh representing the domain and the evaluation of a membership function on it"""
mesh = self.get_mesh()
return mesh, [set(x) for x in mesh]
[docs] def plot_set(self, set, **kwargs):
"""Plot a fuzzy set"""
if plt is None:
raise ModuleNotFoundError("Matplotlib is required for plotting")
plt.plot(*self.evaluate_set(set), **kwargs)
plt.xlabel(self.name)
plt.ylabel("Membership function")
[docs] def defuzzify(self, set):
"""Calculate a crisp number from the fuzzy set"""
raise NotImplementedError("A Domain subclass must be used instead.")
[docs] def get_ipywidget(self, **kwargs):
"""Get a widget representing the domain"""
raise NotImplementedError("A Domain subclass must be used instead.")
[docs]class FloatDomain(Domain):
def __init__(self, name, min, max, steps):
"""
Args:
name (str): Name of the domain.
min (float): Minimum value of the domain
max (float): Maximum value of the domain
steps (int or float): Number of steps if int or step size if float.
"""
super().__init__(name)
self.min = min
self.max = max
self.steps = steps
def _get_description(self):
return {"type": "FloatDomain", "name": self.name, "min": self.min, "max": self.max, "steps": self.steps}
@staticmethod
def _from_description(description):
return FloatDomain(description["name"], description["min"], description["max"], description["steps"])
[docs] def get_mesh(self):
if self.steps is None or isinstance(self.steps, int):
return np.linspace(self.min, self.max, self.steps)
elif isinstance(self.steps, float):
return np.arange(self.min, self.max, self.steps)
else:
raise ValueError("Bad type for steps")
[docs] def defuzzify(self, set):
method = get_active_context().defuzzification
if method == "centroid":
return self.centroid(set)
elif method == "bisector":
return self.bisector(set)
elif method == "mom":
return self.mom(set)
elif method == "som":
return self.som(set)
elif method == "lom":
return self.lom(set)
raise ValueError("Invalid defuzzification method in context: %s" % method)
[docs] def centroid(self, set):
"""Defuzzify with the centroid (center of mass)"""
xx, mu = self.evaluate_set(set)
try:
return np.average(xx, weights=mu)
except ZeroDivisionError:
return np.nan
[docs] def bisector(self, set):
"""Defuzzify with the bisector (value separating two portions of equal area under the membership function)"""
xx, mu = self.evaluate_set(set)
cum_mu = np.cumsum(mu)
# TODO: This could actually be improved interpolating with the nearest values
mean_pos = bisect.bisect_left(cum_mu, cum_mu[-1] / 2)
return xx[mean_pos]
[docs] def mom(self, set):
"""Defuzzify with the middle of maximum"""
xx, mu = self.evaluate_set(set)
max_mu = max(mu)
xx_max = [x for x, m in zip(xx, mu) if m == max_mu]
return np.median(xx_max)
[docs] def som(self, set):
"""Defuzzify with the smaller of maximum"""
xx, mu = self.evaluate_set(set)
max_mu = max(mu)
xx_max = [x for x, m in zip(xx, mu) if m == max_mu]
return min(xx_max)
[docs] def lom(self, set):
"""Defuzzify with the largest of maximum"""
xx, mu = self.evaluate_set(set)
max_mu = max(mu)
xx_max = [x for x, m in zip(xx, mu) if m == max_mu]
return max(xx_max)
[docs] def get_ipywidget(self, **kwargs):
if ipywidgets is None:
raise ModuleNotFoundError("ipywidgets is required")
kwargs = {k: v for k, v in kwargs.items() if k in ["continuous_update"]}
return ipywidgets.FloatSlider(min=self.min, max=self.max, **kwargs)
[docs]class CategoricalDomain(Domain):
def __init__(self, name, values):
"""
Args:
name (str): Name of the domain.
values (list): List of possible valued.
"""
super().__init__(name)
self.values = values
def _get_description(self):
return {"type": "CategoricalDomain", "name": self.name, "values": self.values}
@staticmethod
def _from_description(description):
return CategoricalDomain(description["name"], description["values"])
[docs] def get_mesh(self):
return np.asarray(self.values)
[docs] def centroid(self, set):
# Return as the mode
# In case of ties, only the first value is returned
xx, mu = self.evaluate_set(set)
return xx[np.argmax(mu)]
[docs] def get_ipywidget(self, **kwargs):
if ipywidgets is None:
raise ModuleNotFoundError("ipywidgets is required")
kwargs = {k: v for k, v in kwargs.items() if k in ["continuous_update"]}
return ipywidgets.Dropdown(options=self.values, **kwargs)
_domain_subclasses = {"FloatDomain": FloatDomain, "CategoricalDomain": CategoricalDomain}