241 lines
6.5 KiB
Python
241 lines
6.5 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
'''
|
||
|
Norms and co-norms based on the definitions in the IEEE standard.
|
||
|
Each function here returns a FuzzyAggregationMethods object,
|
||
|
which is really just a pair of functions: (and_func, or_func).
|
||
|
I'm following Annex A in IEEE 1855-2016, definintions A.14-A.27.
|
||
|
@author: james.power@mu.ie Created on Fri Aug 10 12:48:30 2018
|
||
|
'''
|
||
|
|
||
|
|
||
|
import numpy as np
|
||
|
from skfuzzy.control.term import FuzzyAggregationMethods
|
||
|
|
||
|
|
||
|
''' Reminder of what's in term.py:
|
||
|
class FuzzyAggregationMethods(object):
|
||
|
def __init__(self, and_func=np.fmin, or_func=np.fmax):
|
||
|
self.and_func = and_func
|
||
|
self.or_func = or_func
|
||
|
'''
|
||
|
|
||
|
|
||
|
def _fam_vectorise(and_func, or_func):
|
||
|
'''
|
||
|
Vectorise an and/or function pair; return a FuzzyAggregationMethods.
|
||
|
Make sure the function names are propoagated to the resulting object.
|
||
|
'''
|
||
|
fam = FuzzyAggregationMethods(np.vectorize(and_func),
|
||
|
np.vectorize(or_func))
|
||
|
fam.and_func.__name__ = and_func.__name__
|
||
|
fam.or_func.__name__ = or_func.__name__
|
||
|
return fam
|
||
|
|
||
|
|
||
|
# This is the default:
|
||
|
# A.14: minimum t-norm, A.21: maximum t-conorm
|
||
|
MIN_MAX = FuzzyAggregationMethods(np.fmin, np.fmax)
|
||
|
|
||
|
|
||
|
def ps_prod_and(a, b):
|
||
|
'''A.15: product t-norm'''
|
||
|
return a*b
|
||
|
|
||
|
|
||
|
def ps_sum_or(a, b):
|
||
|
'''A.21: Probabilistic sum t-conorm'''
|
||
|
return a+b - (a*b)
|
||
|
|
||
|
|
||
|
PRODUCT_SUM = FuzzyAggregationMethods(ps_prod_and, ps_sum_or)
|
||
|
|
||
|
|
||
|
def bounded_and(a, b):
|
||
|
'A.16: bounded difference t-norm'
|
||
|
return np.fmax(0, a+b - 1)
|
||
|
|
||
|
|
||
|
def bounded_or(a, b):
|
||
|
'A.22: bounded sum t-conorm'
|
||
|
return np.fmin(1, a+b)
|
||
|
|
||
|
|
||
|
BOUNDED = FuzzyAggregationMethods(bounded_and, bounded_or)
|
||
|
|
||
|
|
||
|
'''This is just a synonym for bounded'''
|
||
|
LUCASIEWICZ = BOUNDED
|
||
|
|
||
|
|
||
|
def drastic_and(a, b):
|
||
|
'A.17: drastic product t-norm'
|
||
|
if a == 1:
|
||
|
return b
|
||
|
elif b == 1:
|
||
|
return a
|
||
|
else:
|
||
|
return 0
|
||
|
|
||
|
|
||
|
def drastic_or(a, b):
|
||
|
'A.23: drastic sum t-conorm'
|
||
|
if a == 0:
|
||
|
return b
|
||
|
elif b == 0:
|
||
|
return a
|
||
|
else:
|
||
|
return 1
|
||
|
|
||
|
DRASTIC = _fam_vectorise(drastic_and, drastic_or)
|
||
|
|
||
|
|
||
|
def einstein_and(a, b):
|
||
|
'A.18: Einstein product t-norm'
|
||
|
return (a*b) / (2 - (a+b - a*b))
|
||
|
|
||
|
|
||
|
def einstein_or(a, b):
|
||
|
'A.24: Einstein sum t-conorm'
|
||
|
return (a+b) / (1 + a*b)
|
||
|
|
||
|
|
||
|
EINSTEIN = FuzzyAggregationMethods(einstein_and, einstein_or)
|
||
|
|
||
|
|
||
|
# Denoninator is wrong in A.19 whihc says: ((a+b) / ((a+b) - a*b))
|
||
|
def hamacher_and(a, b):
|
||
|
'''A.19: Hamacher product t-norm; added divide-by-zero check'''
|
||
|
if a == b == 0:
|
||
|
return 0.0
|
||
|
else:
|
||
|
return (a*b) / ((a+b) - a*b)
|
||
|
|
||
|
|
||
|
def hamacher_or(a, b):
|
||
|
'A.25: Hamacher sum t-conorm; added divide-by-zero check'
|
||
|
if a == b == 1:
|
||
|
return 1.0
|
||
|
else:
|
||
|
return (a+b - 2*a*b) / (1 - a*b)
|
||
|
|
||
|
|
||
|
HAMACHER = _fam_vectorise(hamacher_and, hamacher_or)
|
||
|
|
||
|
|
||
|
def nilpotent_and(a, b):
|
||
|
'A.20: Nilpotent minum t-norm'
|
||
|
if a+b > 1:
|
||
|
return np.fmin(a, b)
|
||
|
else:
|
||
|
return 0.0
|
||
|
|
||
|
|
||
|
def nilpotent_or(a, b):
|
||
|
'A.26: Nilpotent maximum t-conorm'
|
||
|
if a+b < 1:
|
||
|
return np.fmax(a, b)
|
||
|
else:
|
||
|
return 1.0
|
||
|
|
||
|
|
||
|
NILPOTENT = _fam_vectorise(nilpotent_and, nilpotent_or)
|
||
|
|
||
|
|
||
|
# ################# ###
|
||
|
# ### Test routines ###
|
||
|
# ################# ###
|
||
|
|
||
|
def check_classic(fam):
|
||
|
'''
|
||
|
Test that norm and co-norm work like classic and/or for 0, 1 inputs.
|
||
|
'''
|
||
|
a = np.array([0, 0, 1, 1])
|
||
|
b = np.array([0, 1, 0, 1])
|
||
|
try:
|
||
|
# First check that the product works like the AND function:
|
||
|
expected = np.logical_and(a, b)
|
||
|
fprod = fam.and_func(a, b)
|
||
|
if not (fprod == expected).all():
|
||
|
print('Failed check_classic', fam.and_func.__name__, '\n',
|
||
|
'Inputs:', (a, b), '\n',
|
||
|
'Product =', fprod)
|
||
|
# Now check that the sum works like the OR function:
|
||
|
expected = np.logical_or(a, b)
|
||
|
fsum = fam.or_func(a, b)
|
||
|
if not (fsum == expected).all():
|
||
|
print('Failed check_classic', fam.or_func.__name__, '\n',
|
||
|
'Inputs:', (a, b), '\n',
|
||
|
'Sum =', fsum)
|
||
|
# A divide-by-zero error is also a problem:
|
||
|
except ZeroDivisionError as e:
|
||
|
print('check_classic',
|
||
|
fam.and_func.__name__, fam.or_func.__name__, '\n',
|
||
|
'Inputs:', (a, b), '\n',
|
||
|
'Divide by zero')
|
||
|
|
||
|
|
||
|
def check_duality(fam):
|
||
|
'''
|
||
|
Run some tests to make sure that the norm and co-norm are duals.
|
||
|
That is, they obey de Morgan's law: (a and b) = not((not a) or (not b))
|
||
|
'''
|
||
|
# Test a range of values between 0.0 and 1.0 inclusive:
|
||
|
a = np.arange(0.0, 1.1, 0.1)
|
||
|
b = np.arange(0.0, 1.1, 0.1)
|
||
|
try:
|
||
|
prod = fam.and_func(a, b)
|
||
|
# Need to round, since e.g. 1-0.7 is not 0.3 otherwise:
|
||
|
dual_sum = 1 - fam.or_func(np.round(1-a, 1), np.round(1-b, 1))
|
||
|
if not np.isclose(prod, dual_sum).all():
|
||
|
print('Failed check_duality', fam.and_func.__name__, '\n',
|
||
|
'Inputs:', (a, b), '\n',
|
||
|
'Product =', prod, '\n',
|
||
|
'Dual Sum =', dual_sum)
|
||
|
except ZeroDivisionError as e:
|
||
|
print('Failed check_duality',
|
||
|
fam.and_func.__name__, fam.or_func.__name__, '\n',
|
||
|
'Inputs:', (a, b), '\n',
|
||
|
'Divide by zero')
|
||
|
|
||
|
|
||
|
_all_norms = {
|
||
|
'Min/Max': MIN_MAX,
|
||
|
'Prod/Sum': PRODUCT_SUM,
|
||
|
'Bounded': BOUNDED,
|
||
|
'Drastic': DRASTIC,
|
||
|
'Einstein': EINSTEIN,
|
||
|
'Hamacher': HAMACHER,
|
||
|
'Nilpotent': NILPOTENT,
|
||
|
}
|
||
|
|
||
|
|
||
|
import matplotlib.pyplot as plt
|
||
|
import skfuzzy.membership as skmemb
|
||
|
|
||
|
|
||
|
def visualise_all(x, y1, y2, all_norms=_all_norms):
|
||
|
'''Plot the norm and conorm for the given sample inputs'''
|
||
|
ncols = 3
|
||
|
fig, axes = plt.subplots(nrows=len(all_norms), ncols=ncols, figsize=(8, 9))
|
||
|
fig.tight_layout()
|
||
|
fig.subplots_adjust(bottom=-.25)
|
||
|
for row, name in enumerate(_all_norms.keys()):
|
||
|
for col in range(ncols): # so all have the same (0,1) y-axis
|
||
|
axes[row][col].set_ylim([-0.05, 1.05])
|
||
|
axes[row][0].set_title('Sample inputs')
|
||
|
axes[row][0].plot(x, y1)
|
||
|
axes[row][0].plot(x, y2)
|
||
|
axes[row][1].set_title(name + ' norm')
|
||
|
axes[row][1].plot(x, _all_norms[name].and_func(y1, y2))
|
||
|
axes[row][2].set_title(name + ' co-norm')
|
||
|
axes[row][2].plot(x, _all_norms[name].or_func(y1, y2))
|
||
|
|
||
|
if __name__ == '__main__':
|
||
|
for fam in _all_norms.values():
|
||
|
check_classic(fam)
|
||
|
check_duality(fam)
|
||
|
sample_x = np.arange(0, 100)
|
||
|
sample_y1 = skmemb.trapmf(sample_x, [15, 30, 55, 75])
|
||
|
sample_y2 = skmemb.trapmf(sample_x, [25, 45, 70, 85])
|
||
|
visualise_all(sample_x, sample_y1, sample_y2)
|