Source code for zadeh.domains

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}