LSR/tnorms.py
2020-06-04 19:21:01 +02:00

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)