from .fmath import fuzzy_unite, fuzzy_difference, fuzzy_intersect
from .mf import very, neg, maybe, clip_upper, memberships
from .defuzz import DEFAULT_DEFUZZ
from typing import Callable, Union, Tuple
import torch
import matplotlib.pyplot as plt
from inspect import signature
RealNum = Union[float, int]
AnyNum = Union['FuzzyNumber', int, float]
default_dtype = "float32"
[docs]
class Domain:
"""
A class for representing a set of possible values of fuzzy numbers
This class manages the domain of values (it adds a universal set on which fuzzy numbers are built),
provides methods for creating
fuzzy numbers, changing the calculation method and visualization
Attributes:
_x (torch.Tensor): A set of values in the domain
step (RealNum): The step between the values in the domain
name (str): The domain name
_method (str): The method used for fuzzy operations (for example, 'minimax' or 'prob')
_vars (dict): Storage of fuzzy numbers in this domain
bounds (list): The boundaries of the arguments used when creating fuzzy numbers
membership_type (str): Type of membership function for fuzzy numbers
Args:
fset (Union[Tuple[RealNum, RealNum], Tuple[RealNum, RealNum, RealNum], torch.Tensor]):
The beginning, the end, and the step (or tensor of values) to create the domain
name (str, optional): The domain name (None by default)
method (str, optional): Method for fuzzy operations ('minimax' or 'prob', default is 'minimax')
Properties:
method (str): Returns or sets the method used for fuzzy operations
x (torch.Tensor): Returns a range of domain values
Raises:
AssertionError: If the passed parameters do not meet the expected requirements
"""
def __init__(self, fset: Union[Tuple[RealNum, RealNum], Tuple[RealNum, RealNum, RealNum], torch.Tensor],
name: str = None, method: str = 'minimax'):
assert (isinstance(fset, Tuple) and (len(fset) == 3 or len(fset) == 2)) or isinstance(fset, torch.Tensor), \
'set bust be given as torch.Tensor or tuple with start, end, step values'
assert method == 'minimax' or method == 'prob', f"Unknown method. Known methods are 'minimax' and 'prob'"
if isinstance(fset, torch.Tensor):
self._x = fset
self.step = self._x[1] - self._x[0]
elif len(fset) == 3:
start, end, step = fset
self._x = torch.arange(start, end, step)
self.step = step
elif len(fset) == 2:
start, end = fset
self._x = torch.arange(start, end, 1)
self.step = 1
self.name = name
self._method = method
self._vars = {}
self.bounds = []
self.membership_type = ""
@property
def method(self) -> str:
"""
Returns the method used for fuzzy operations
Returns:
str: method
"""
return self._method
@method.setter
def method(self, value: str):
"""
Sets the method for fuzzy operations
Args:
value (str): New method ('minimax' or 'prob')
Raises:
AssertionError: If the specified method is not 'minimax' or 'prob'
"""
assert value == 'minimax' or value == 'prob', "Unknown method. Known methods are 'minmax' and 'prob'"
self._method = value
for name, var in self._vars.items():
var._method = value
[docs]
def to(self, device: str):
"""
Moves the domain to the specified device ('cpu' or 'cuda')
Args:
device (str): The device to move (for example, 'cpu' or 'cuda')
"""
self._x = self._x.to(device)
[docs]
def copy(self) -> 'Domain':
"""
Returns a range of domain values
Returns:
Domain: Fuzzy numbers domain
"""
return Domain(self._fset, self.name, self.method)
@property
def x(self) -> torch.Tensor:
"""
Returns a range of domain values
Returns:
torch.Tensor: Range of values
"""
return self._x
[docs]
def create_number(self, membership: Union[str, Callable], *args: RealNum, name: str = None) -> 'FuzzyNumber':
"""
Creates a new fuzzy number in the domain with the specified membership function
Args:
membership (Union[str, Callable]): Name or function for calculating membership
*args (RealNum): Arguments for the membership function
name (str, optional): Name for the created fuzzy number (None by default)
Returns:
FuzzyNumber: The created fuzzy number
Raises:
AssertionError: If membership is not a string or does not match the required number of arguments
"""
assert isinstance(membership, str) or (isinstance(membership, Callable) and
len(args) == len(signature(membership).parameters))
if isinstance(membership, str):
self.membership_type = membership
membership = memberships[membership]
f = FuzzyNumber(self, membership(*args), self._method)
self.bounds = list(args)
if name:
self.__setattr__(name, f)
return f
def __setattr__(self, name: str, value: 'FuzzyNumber'):
"""
Sets an attribute for a domain, either a variable or a value
Args:
name (str): Attribute Name
value ('FuzzyNumber'): The attribute value must be FuzzyNumber or str
Raises:
AssertionError: If the value is not a FuzzyNumber (for new variables)
"""
if name in ['_x', 'step', 'name', '_method', '_vars', 'method', 'bounds', 'membership_type']:
object.__setattr__(self, name, value)
else:
assert isinstance(value, FuzzyNumber), 'Value must be FuzzyNumber'
self._vars[name] = value
def __getattr__(self, name: str) -> 'FuzzyNumber':
"""
Gets the attribute value by name
Args:
name (str): Attribute name
Returns:
FuzzyNumber: The value of the corresponding fuzzy number
Raises:
AttributeError: If the attribute with the specified name is not found
"""
if name in self._vars:
return self._vars[name]
else:
raise AttributeError(f'{name} is not a variable in domain {self.name}')
[docs]
def get(self, name: str) -> 'FuzzyNumber':
"""
Returns a fuzzy number with the specified name
Args:
name (str): The name of the fuzzy number
Returns:
FuzzyNumber: A fuzzy number with a given name
"""
return self._vars[name]
def __delattr__(self, name: str) -> 'FuzzyNumber':
"""
Deletes a fuzzy number with the specified name from the domain
Args:
name (str): Name of the fuzzy number to delete
"""
if name in self._vars:
del self._vars[name]
[docs]
def plot(self):
"""
Plots all the fuzzy numbers in the domain
Uses matplotlib to visualize the values of fuzzy numbers
"""
_, ax = plt.subplots()
for name, num in self._vars.items():
ax.plot(self.x, num.values, label=f'{name}')
plt.title(self.name)
ax.legend()
plt.show()
[docs]
class FuzzyNumber:
"""
A fuzzy number defined in a specific domain with a membership function
Attributes:
_domain Domain: The domain on which the number is based
_membership Callable: The membership function of a fuzzy number
_method str: Calculation method 'minimax' or 'prob'. The default is 'minimax'
Args:
domain Domain: The domain on which the number is based
membership Callable: The membership function of a fuzzy number (for example, a function that returns a tensor)
method str: Calculation method 'minimax' or 'prob'. The default is 'minimax'
Properties:
very:
A copy of a number with a membership function squared
negation:
A copy of a number with the opposite membership function
maybe:
A copy of a number with a membership function raised to the power of 0.5
method:
The method used for calculations
membership:
Fuzzy number membership function
domain:
The domain where the fuzzy number is located
values:
Fuzzy number values on a given domain
"""
def __init__(self, domain: Domain, membership: Callable, method: str = 'minimax'):
assert method == 'minimax' or method == 'prob', "Unknown method. Known methods are 'minmax' and 'prob'"
self._domain = domain
self._membership = membership
self._method = method
@property
def very(self) -> 'FuzzyNumber':
"""
A copy of a number with a membership function squared
Returns:
FuzzyNumber: The square of a fuzzy number
"""
return FuzzyNumber(self._domain, very(self._membership), self._method)
@property
def negation(self) -> 'FuzzyNumber':
"""
A copy of a number with the opposite membership function
Returns:
FuzzyNumber: The square of a fuzzy number
"""
return FuzzyNumber(self._domain, neg(self._membership), self._method)
@property
def maybe(self) -> 'FuzzyNumber':
"""
A copy of a number with a membership function raised to the power of 0.5
Returns:
FuzzyNumber: The square of a fuzzy number
"""
return FuzzyNumber(self._domain, maybe(self._membership), self._method)
[docs]
def copy(self) -> 'FuzzyNumber':
"""
Creates and returns a copy of the fuzzy number
Returns:
FuzzyNumber: The square of a fuzzy number
"""
return FuzzyNumber(self._domain, self._membership, self._method)
@property
def method(self) -> str:
"""
Returns the method used for calculations
Returns:
str: Returns the method
"""
return self._method
@property
def membership(self) -> Callable:
"""
Returns the membership function of a fuzzy number
Returns:
Callable: Returns the membership function of a fuzzy number
"""
return self._membership
@property
def domain(self) -> Domain:
"""
Returns the domain where the fuzzy number is located
Returns:
Domain: Returns the domain
"""
return self._domain
@property
def values(self, dtype: str = default_dtype) -> Callable:
"""
Returns the values of a fuzzy number on the specified domain
Returns:
Callable: Returns the confidence level
"""
return self.membership(self._domain.x) # .astype(dtype)
[docs]
def plot(self, ax=None):
"""
Plots a graph of a fuzzy number. Creates a new subgraph if not specified
Args:
ax (matplotlib.axes._subplots.AxesSubplot, optional):
An existing graph for adding data. If not specified, a new schedule will be created.
"""
if self.domain.x.device.type != 'cpu':
raise TypeError(f"can't convert {self.domain.x.device} device type tensor to numpy. Use Domain.to('cpu') "
f"first.")
if not ax:
_, ax = plt.subplots()
out = ax.plot(self.domain.x, self.values)
plt.show()
return out
[docs]
def alpha_cut(self, alpha: float) -> torch.Tensor:
"""
Performs alpha cropping of a fuzzy number
Args:
alpha (float): The alpha level for cutting
Returns:
torch.Tensor: Domain values for which the membership function is greater than or equal to alpha
"""
return self.domain.x[self.values >= alpha]
[docs]
def entropy(self, norm: bool = True) -> float:
"""
Calculates the entropy of a fuzzy number
Args:
norm (bool): If True, the entropy is normalized by the number of elements in the domain
Returns:
float: The value of the entropy of a fuzzy number
"""
vals = self.values
mask = vals != 0
e = -torch.sum(vals[mask] * torch.log2(vals[mask]))
if norm:
return 2. / len(self.values) * e
else:
return e
[docs]
def center_of_grav(self) -> float:
"""
Center of gravity defuzzification
Returns:
float: The meaning of defazzification
"""
weights_sum = torch.sum(self.values)
if weights_sum == 0:
return 0.0
return float(torch.sum(self.domain.x * self.values) / weights_sum)
[docs]
def left_max(self) -> float:
"""
Defuzzification by the left maximum method
Returns:
float: The meaning of defazzification
"""
h = torch.max(self.values)
return float(self.domain.x[self.values == h][0])
[docs]
def right_max(self) -> float:
"""
Defuzzification by the right maximum method
Returns:
float: The meaning of defuzzification
"""
h = torch.max(self.values)
return float(self.domain.x[self.values == h][1])
[docs]
def center_of_max(self, verbose: bool = False) -> float:
"""
Defuzzification by the central maximum method
Args:
verbose (bool): By default, False, If True, displays information about the maxima.
Returns:
float: The meaning of defuzzification
"""
h = torch.max(self.values)
maxs = self.domain.x[self.values == h]
if verbose:
print('h:', h, 'maximums are:', maxs)
float_tensor = maxs.to(torch.float32)
return float(torch.mean(float_tensor))
[docs]
def moment_of_inertia(self, center: bool = None) -> float:
"""
Defuzzification by the moment of inertia method
Args:
center (float): The center relative to which the moment of inertia is calculated. If not specified, the center of gravity is used
Returns:
float: Значение дефаззификации.
"""
if not center:
center = self.center_of_grav()
return float(torch.sum(self.values * torch.square(self.domain.x - center)))
[docs]
def defuzz(self, by: str = 'default') -> float:
"""
Defuzzification of a fuzzy number by a specific method
Args:
by (str): Choosing a defazzification method
Returns:
float: The meaning of defuzzification
"""
if by == 'default':
by = DEFAULT_DEFUZZ
if by == 'lmax':
return self.left_max()
elif by == 'rmax':
return self.right_max()
elif by == 'cmax':
return self.center_of_max()
elif by == 'cgrav':
return self.center_of_grav()
else:
raise ValueError('defuzzification can be made by lmax, rmax, cmax, cgrav or default')
[docs]
def clip_upper(self, upper: RealNum) -> 'FuzzyNumber':
"""
A method for slicing a fuzzy number along the boundary of the degree of confidence of a given clear value
from a universal set
Args:
upper (RealNum): Clear values for the slice
Returns:
FuzzyNumber: Limited fuzzy number
"""
return FuzzyNumber(self.domain, clip_upper(self._membership, upper), self._method)
# magic
def __call__(self, x: RealNum) -> torch.Tensor:
"""
A magical method for obtaining the degree of certainty of a specific clear meaning from a universal set
Args:
x (RealNum): Clear values for the slice
Returns:
torch.Tensor: The degree of confidence for a value from a universal set
"""
return self._membership(torch.tensor([x], dtype=self.domain.x.dtype, device=self.domain.x.device))
def __str__(self) -> str:
"""
String value of a fuzzy number
Returns:
str: A string value of a fuzzy number
"""
return str(self.defuzz())
def __repr__(self) -> str:
"""
String value of a fuzzy number (console output)
Returns:
str: The string value of a fuzzy number (console output)
"""
return 'Fuzzy' + str(self.defuzz())
def __add__(self, other: AnyNum) -> 'FuzzyNumber':
"""
An overloaded method for summing clear and fuzzy numbers with an instance of the fuzzy number class
Args:
other (AnyNum): A clear or fuzzy number
Returns:
FuzzyNumber: The result of the operation is a fuzzy number
"""
if isinstance(other, int) or isinstance(other, float):
def added(x):
return self._membership(x - other)
return FuzzyNumber(self.domain, added, self._method)
elif isinstance(other, FuzzyNumber):
new_mf = fuzzy_unite(self, other)
return FuzzyNumber(self.domain, new_mf, self._method)
else:
raise TypeError('can only add a number (Fuzzynumber, int or float)')
def __iadd__(self, other: AnyNum) -> 'FuzzyNumber':
"""
An overloaded method for summing clear and fuzzy numbers with an instance of the fuzzy number class
Args:
other (AnyNum): A clear or fuzzy number
Returns:
FuzzyNumber: The result of the operation is a fuzzy number
"""
return self + other
def __radd__(self, other: AnyNum) -> 'FuzzyNumber':
"""
An overloaded method for summing clear and fuzzy numbers with an instance of the fuzzy number class
Args:
other (AnyNum): A clear or fuzzy number
Returns:
FuzzyNumber: The result of the operation is a fuzzy number
"""
return self.__add__(other)
def __sub__(self, other: AnyNum) -> 'FuzzyNumber':
"""
An overloaded method for subtracting clear and fuzzy numbers with an instance of the fuzzy number class
Args:
other (AnyNum): A clear or fuzzy number
Returns:
FuzzyNumber: The result of the operation is a fuzzy number
"""
if isinstance(other, int) or isinstance(other, float):
def diff(x):
return self._membership(x + other)
return FuzzyNumber(self.domain, diff, self._method)
elif isinstance(other, FuzzyNumber):
new_mf = fuzzy_difference(self, other)
return FuzzyNumber(self.domain, new_mf, self._method)
else:
raise TypeError('can only substract a number (Fuzzynumber, int or float)')
def __isub__(self, other: AnyNum) -> 'FuzzyNumber':
"""
An overloaded method for subtracting clear and fuzzy numbers with an instance of the fuzzy number class
Args:
other (AnyNum): A clear or fuzzy number
Returns:
FuzzyNumber: The result of the operation is a fuzzy number
"""
return self - other
def __mul__(self, other: AnyNum) -> 'FuzzyNumber':
"""
An overloaded method for multiplying clear and fuzzy numbers with an instance of the fuzzy number class
Args:
other (AnyNum): A clear or fuzzy number
Returns:
FuzzyNumber: The result of the operation is a fuzzy number
"""
if isinstance(other, int) or isinstance(other, float):
# raise NotImplementedError('Multiplication by a number is not implemented yet')
def multiplied(x):
return self._membership(x * other)
return FuzzyNumber(self.domain, multiplied, self._method)
elif isinstance(other, FuzzyNumber):
new_mf = fuzzy_intersect(self, other)
return FuzzyNumber(self.domain, new_mf, self._method)
else:
raise TypeError('can only substract a number (Fuzzynumber, int or float)')
def __imul__(self, other: AnyNum) -> 'FuzzyNumber':
"""
An overloaded method for multiplying clear and fuzzy numbers with an instance of the fuzzy number class
Args:
other (AnyNum): A clear or fuzzy number
Returns:
FuzzyNumber: The result of the operation is a fuzzy number
"""
return self * other
def __rmul__(self, other: AnyNum) -> 'FuzzyNumber':
"""
An overloaded method for multiplying clear and fuzzy numbers with an instance of the fuzzy number class
Args:
other (AnyNum): A clear or fuzzy number
Returns:
FuzzyNumber: The result of the operation is a fuzzy number
"""
return self.__mul__(other)
def __truediv__(self, other: RealNum) -> 'FuzzyNumber':
# raise NotImplementedError('Division is not implemented yet')
t_o = type(other)
if t_o == int or t_o == float:
def divided(x):
return self._membership(x / other)
return FuzzyNumber(self.domain, divided, self._method)
else:
raise TypeError('can only divide by a number (int or float)')
def __idiv__(self, other: RealNum):
return self / other
def __int__(self) -> int:
"""
Integer defuzz value
Returns:
int: Integer defuzz value
"""
return int(self.defuzz())
def __float__(self) -> float:
"""
Float defuzz value
Returns:
float: Float defuzz value
"""
return self.defuzz()