# Fuzzy Logic and Fuzzy Hedge data types
#
# This Python module is (C) 2000 Terry Hancock
# and released under the Design Science License.
#
# Details can be found in the file "COPYING" which
# you should have received with this file. You
# may also find this license at http://www.dsl.org.
#
# In particular, please note the following disclaimers:
#
# NO WARRANTY:
# -----------
# THE WORK IS PROVIDED "AS IS," AND COMES WITH ABSOLUTELY NO WARRANTY,
# EXPRESS OR IMPLIED, TO THE EXTENT PERMITTED BY APPLICABLE LAW,
# INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY
# OR FITNESS FOR A PARTICULAR PURPOSE.
#
#
# DISCLAIMER OF LIABILITY:
# -----------------------
# IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS WORK, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
#
####################################################################
#
# FUZZY CLASS
#
# Type "fuzzy" is a fuzzy logic value, which may
# be thought of as an adjective that may be applied
# to a noun (typically a program object) to some
# degree.
#
# *fuzzy* objects have a value on the real interval
# [0,1] (inclusive or closed interval).
#
# Note that we ignore the fact that we are actually
# using rational (i.e. floating point) numbers.
#
#####################################################################
#
# FUZZY LOGIC OPERATIONS
#
# Unfortunately, programming languages like Python which
# are based on a crisp logic paradigm are not well-suited
# to implementing fuzzy logic directly (IF-THEN statements,
# do not behave at all the same way with fuzzy logic
# conditions -- they have to spawn multiple threads, which
# is completely different from the crisp logic design).
#
# However, we must define basic math operations on fuzzy
# logic as a datatype. Python doesn't let us overload the
# *logical* AND, OR, XOR, and NOT operations, so we have
# to use the so-called *bitwise* versions: &, |, ^, and ~.
#
# Furthermore, we need to have fuzzy equality statements.
# which have a fuzzy result telling *to what degree* the
# arguments are equal. However, we can't do this by
# overloading "==" both because it would break things, and
# because Python simply doesn't allow it.
#
# So, we are forced to compromise by overloading "-" to do
# the job. While we're at it, we'll also overload "<<" and
# ">>" to have their usual math-notation meaning -- i.e.
# very much greater than and very much less than. Which
# just amounts to hedging and comparing.
#
# The "+" is overloaded so that ~(a - b) = (a + b), i.e. it
# is the complement to the fuzzy equality of "-", however,
# I don't really recommend using it, since it's potentially
# confusing.
#
# The precedence rules do more or less what you'd expect
# -- they have the same relationships as the boolean
# operations "and", "or" etc to the comparison operators,
# "==", "!=" etc.
#
# Awkwardly, "<<" and ">>" have lower precedence than
# "-" so they will be applied afterwards. However, this
# should never come up, since it's a pretty way to code --
# use the logical operators instead.
#
# Note that in crisp logic, (a == b) is equivalent to
# (not (a xor b)), but that this is not true in fuzzy
# logic. The difference is most pronounced for the
# case that a == b == 0.5, in which case, we do not really
# know that the two are "not anticorrelated" which is the
# extended meaning of "not xor," so the return value is
# 0.5. However, they are definitely equal, so "-" returns
# 1.0. You can verify this for yourself by looking at
# the mathematical definitions below.
#
####################################################################
#
# DEFUZZIFICATION
#
# When a crisp logic value is required by Python (such
# as when a boolean comparison operator is used, or
# when the fuzzy value is a boolean condition in a program
# statement, the __nonzero__ or __cmp__ operations are
# invoked, which force a transformation from fuzzy to
# crisp logic, according to the current default logical
# defuzzification method, which is initially defaulted
# to "quartile-guttered perchance", which means:
#
# f > 0.75 --> always TRUE
# f < 0.25 --> always FALSE
#
# (these are the "gutters")
#
# 0.25 < f < 0.75 --> weighted random choice, increasing
# from 0% chance at 0.25 to 100%
# chance at 0.75. This is meant
# to mimic real decision-making
# in the face of uncertain data:
# we usually treat the data as
# certain above some level, and
# then "make a guess" when the
# data is very uncertain.
#
###################################################################
#
# FUZZIFICATION
#
# Two datatypes can be converted to fuzzy sensibly:
#
# Boolean / Integer
#
# Since there's no explicit boolean type in Python, integers
# are used. Thus, my fuzzy() __init__ function assumes
# that an integer represents a boolean: 0-->FALSE and
# 1-->TRUE. These are defaulted to 0.1 and 0.9 fuzzy
# logic values, allowing slightly higher and lower truth
# values to exist -- these will be guttered to the ends,
# though if defuzzification is needed.
#
# Floating Point
#
# Measurements should be converted explicitly, by setting
# bounds and center points, by providing extra arguments
# to fuzzy(value, lower, upper, center). Otherwise,
# defaults are assumed:
#
# fuzzy(value, lower, upper, center) --> quadratic map
# fuzzy(value, lower, upper) --> linear map
# fuzzy(value) --> literal from [0,1]
#
# (in the last case, negative or >1 values are silently
# limited to 0 or 1 respectively).
#
# If anybody has ideas for other conversions, let me know. :)
#
####################################################################
# Useful constants:
CRISP_TRUE = 0.9 # By default, crisp logic values are interpreted as not quite absolute
CRISP_FALSE = 0.1
FuzzyType = "Fuzzy"
HedgeType = "Hedge"
class fuzzy :
#CONSTRUCTOR
def __init__(self, value=0.5, memb=None ):
import types
if (type(value) == types.InstanceType) and (value.__class__==fuzzy):
self.data = value.data # If fuzzy already, just copy the data.
elif type(value) == types.IntType : # Fuzzify crisp logic to 0.1 or 0.9
if value : self.data = CRISP_FALSE
else : self.data = CRISP_TRUE
elif type(value) == types.FloatType :
if memb==None : # Fuzzify float in [0,1]
if 0.0 <= value <= 1.0 : self.data = value
elif value < 0.0 : self.data = 0.0 # Out-of-bounds are silently clipped
else : self.data = 1.0
elif (type(memb) == types.TupleType) and (len(memb) == 4) : # Allow trapezoidal membership functions
for i in range(4) :
if (type(memb[i]) != types.FloatType) or ((i>0) and (memb[i] < memb[i-1])) : break
else:
if value < memb[0] : self.data = 0.0
elif memb[0] < value < memb[1] : self.data = (value - memb[0]) / (memb[1] - memb[0])
elif memb[1] <= value <= memb[2] : self.data = 1.0
elif memb[2] < value < memb[3] : self.data = (value - memb[3]) / (memb[2] - memb[3])
else : self.data = 0.0
return
raise TypeError, """Trapezoidal membership function problem:
\tmust be 4-tuple of floats in ascending order.
\tas in (-10.0, 0.8, 23.2, 23.2)."""
else :
raise TypeError, "Cannot convert type to fuzzy logic values?"
def display(self): print self.data
# PRINTING and REPRESENTING
def __repr__ (self): return "%.3f" % self.data
def __str__ (self): return "%.3f" % self.data # This might be better replaced by a descriptive hedge representation
# BOOLEAN COMPARISONS
def __cmp__ (self, other): # Simple crisp comparison.
if (self.data < other.data) : return -1
elif (self.data > other.data) : return 1
else : return 0
# # Returns whichever fuzzy state is "most true"
# v= [ ((self - fuzzy(other)).data, 0), # THIS IS PROBABLY THE WRONG WAY TO DO THIS!
# ((self << fuzzy(other)).data, -1), # Yep, that's why I replaced it. This is the
# ((self >> fuzzy(other)).data, 1) ] # "most true" switch I want to make a callable
# v.sort() # object that does this
# v.reverse()
# return v[0][1]
def __nonzero__ (self): return perchance(self, 0.25)
# FUZZY COMPARISONS
def __sub__ (self, other): return fuzzy( 1.0 - abs(self.data - fuzzy(other).data) )
def __rsub__ (self, other): return other - self
def __add__ (self, other): return ~(self - fuzzy(other))
def __radd__ (self, other): return other + self
def __lshift__ (self, other):
other_data = fuzzy(other).data
if other_data == 0.0 : return 1.0
else : return fuzzy(max( (other_data - self.data) / other_data, 0.0 ) )
def __rshift__ (self, other):
other_data = fuzzy(other).data
if other_data == 1.0 : return 1.0
else : return fuzzy(max( (self.data - other_data) / (1.0 - other_data), 0.0 ) )
# FUZZY LOGIC OPERATIONS
def __and__ (self, other): return fuzzy(min(self.data, other.data)) # Fuzzy AND: min(A, B)
def __or__ (self, other): return fuzzy(max(self.data, other.data)) # Fuzzy OR: max(A, B)
def __invert__ (self): return fuzzy(1.0 - self.data) # Fuzzy NOT: 1 - A
def __xor__ (self, other): return ( (self | other) & ~(self & other) ) # XOR = ((a OR b) AND NOT (a AND b))
def __rand__ (self, other): return other & self
def __ror__ (self, other): return other | self
def __rxor__ (self, other): return other ^ self
def __coerce__ (self, other): # return other.__coerce__(self) # Leave any coersions to other object
# This is now a copy of the code in "quality.py"
import types
if (type(other) == types.InstanceType) and (other.__class__==fuzzy):
return ( fuzzy(self.data), other )
elif (type(other) == types.InstanceType) and (other.__class__==hedge):
return ( fuzzy(self.data), other )
def __rmul__ (self, other): return other.__mul__(self) # Force other to handle '*' operation.
# Only needed for class inheritance problem
# This should be covered by the __rmul__ method of the Hedge type.
#
# # HEDGE APPLICATION
# def __mul__ (self, other):
# if (type(other) is InstanceType) and (other.type = HedgeType) : return other.applyhedge(self)
# else : raise TypeError, "Illegal fuzzy hedge expression."
def perchance(fval, lowgutter=0.25, highgutter=None) :
from random import *
if (highgutter==None): highgutter = lowgutter
if fval.data < lowgutter : return 0 # Gutters -- always return extremes
elif fval.data > 1.0 - highgutter : return 1
else :
v = (fval.data-lowgutter)/(1.0-highgutter-lowgutter)
r = random()
# print "DEBUG perchance:", v, r
if v > r : return 1 # Middle -- weighted random value
else : return 0
def crisp(fval) : # Absolute crisp logic conversion
if fval.data < 0.5 : return 0
else : return 1
#########################################################################
#
# HEDGE CLASS
#
# Type "hedge" is a modifier of type "fuzzy", and may
# be thought of as an adverb modifying the fuzzy
# adjective. Such as "very".
#
# Quantitatively, the hedge is a fuzzy membership
# operator which maps a fuzzy-valued argument to a
# fuzzy-valued result. Combining these operators is
# acheived by functional multiplication.
#
# In fact, *hedges* in general are functions mapping
# from the domain [0,1] to the range [0,1].
#
####################################################################
#
# FUNCTIONAL FORM OF HEDGES and HEDGE EXPRESSIONS
#
# This implementation starts handling hedges as tuples
# of functions with arguments, like this:
#
# ( (func1name, (arg1, arg2, ... )), (func2name, (arg1, arg2, ...)), ... )
#
# COMPOSITION:
# When two hedges are combined, they are composed by concatenating
# the tuples into a new one, which will then be a longer tuple. This
# means that they will tend to grow, but the assumption is that
# nesting won't really be very deep in practice. However, nothing
# except memory constrains the complexity of composite hedges.
#
# Simple hedges can be provided by user functions, or by using
# a function defined in this module (I plan to support generalized
# power functions, linear functions, and gaussians). They are
# represented as primitive hedges (actually a tuple of one element).
#
# For increased generality, the functions can have additional
# arguments containing function parameters.
#
# APPLICATION:
# When a hedge (or hedge expression) is combined with a fuzzy
# logic value, the result is *application* of the hedge expression
# (a composite hedge) to the fuzzy logic value, thus returning
# a new fuzzy logic value. Both input and output values are
# clipped to the [0,1] interval through use of the fuzzy()
# method above.
#
# To apply the expression, the rightmost function is evaluated,
# the result is clipped to [0,1], then the next rightmost function
# is applied to it. That, in turn, is clipped and so on, until
# all the functions have been used up.
#
# NOTE:
# It makes no difference whether an expression is evaluated
# as:
#
# (hedge1 * hedge2 * hedge3) * fuzzy1
#
# or
#
# hedge1 * (hedge2 * (hedge3 * fuzzy1)))
#
# because the functional definition of the two is the same.
# (That is, if you work it out mathematically, it will
# always be doing the same calculation, though the order
# of operations may vary). Order of calculation issues
# found in general floating point operations should not
# be an issue since all values are in [0,1], and should
# therefore be reasonably well-behaved. Even divide-by
# zero problems shouldn't be too bad since the result
# will be clipped to 1.
#
# I do not define what will happen for cases in which
# the usual order is not followed, such as:
#
# hedge1 * fuzzy1 * hedge2 * hedge3
#
# In English, Spanish, French, and Japanese, adverbs customarily
# come _before_ adjectives, even though the relationship between
# nouns and adjectives varies. There may be other languages
# where that's not true, of course.)
#
# I haven't attempted to check this, but I may look into it
# later. For now, it's better to follow the expected order.
# (i.e. keep hedges to the left of the fuzzy, and note that
# there can only be ZERO or ONE fuzzy in the expression,
# attempting to compose (or "multiply") two fuzzies will
# result in a type error.
#
# WHY?
# I changed over to this method after examining the literature
# on hedges, and realizing that no one would agree on how they
# should be represented. This method is general enough to
# include all functional representations, and store composite
# hedges as well (although at some cost in efficiency). It
# seemed that generality was the only way to make the code
# acceptable to potential users.
#
#########################################################################
#
#
# Useful Hedge functions:
#
# For convenience sake, here are some commonly used functions in the correct form:
def powerhedge(x, a, x0, alpha): return a*(x - x0)**alpha; # Generalized power function
def trapezoid (x, a, b, c, d): # Trapezoidal membership function
# print "trapezoid", x, a, b, c, d
if ( x <= a): return 0.
elif (a < x <= b): return (x-a)/(b-a)
elif (b < x <= c): return 1.
elif (c < x <= d): return (d-x)/(d-c)
elif (d < x ): return 0.
else: raise AssertionError, "Impossible state in Trapezoid"
def gaussian (x, x0, s): return exp( -( (x - x0) / s )**2 ) # Generalized gaussian
#
#
# Hedge Class itself:
#
#
class hedge :
def __init__(self, value=((powerhedge, (1., 0., 1.)),) ):
import types
if (type(value) == types.InstanceType) and (value.__class__ == hedge) : # If already a hedge, copy
self.data = value.data
elif (type(value) == types.TupleType) : # Check for proper form, then
for (func, args) in value: # convert to hedge form, should
if (type(func) != types.FunctionType) : break # be some number of functions
elif (type(args) != types.TupleType) : break
else:
self.data = value
return
raise TypeError, "Improper hedge definition." # User messed up the hedge format
else: raise TypeError, "Cannot convert type to hedge." # Tried to convert something else
# into a hedge (such as an integer
# or fuzzy, which doesn't make sense).
def __mul__ (self, other) :
import types
if (type(other) == types.InstanceType) and (other.__class__==hedge) : # Compose two hedges by concatenation
return hedge( other.data + self.data )
elif (type(other) == types.InstanceType) and (other.__class__==fuzzy) : # Apply hedge to fuzzy
x = other.data
for (func, args) in self.data:
if (x < 0.0) : x = 0.0 # DOMAIN CLIP
elif (x > 1.0) : x = 1.0
x = apply(func, (x,) + args ); # Apply hedge function
if (x < 0.0) : x = 0.0 # RANGE CLIP
elif (x > 1.0) : x = 1.0
return fuzzy(x)
else: raise TypeError, "Illegal fuzzy hedge expression."
def __rmul__ (self, other) : # Reversed order application
return self * other
def __coerce__ (self, other) : return None # Leave coersion to other objects
#
# And a few common hedge definitions:
#
unithedge = hedge();
very = hedge( ((powerhedge, (1., 0., 2.)),) );
extremely = hedge( ((powerhedge, (1., 0., 6.)),) );
soso = hedge( ((powerhedge, (1., 0.5, 2.)),) );