Updated 2012-09-28 16:37:34 by LkpPo

Arjen Markus (30 june 2006) Fuzzy logic, whatever you may think of it, is a useful modelling technique for certain kinds of practical problems. I want to keep things practical indeed, as much of the literature on the subject may divert into almost esoteric discourses on the meaning of fuzzy inference and the like.

I consider myself a fairly practical programmer, so I have chosen a very straightforward implementation. It is not finished yet - I want to add a decent example to this script, but it is a nice beginning in my opinion.
 # fuzzylogic.tcl --
 #     Experiment with fuzzy logic
 #
 #     Note:
 #     I wanted something fast and easy to write, so my design
 #     decisions have been guided by that. In particular:
 #     - no hedges
 #     - simply min-max rules
 #     - simple trapezoid membership functions
 #

 namespace eval ::FuzzyLogic {
     variable vars
     variable types

     namespace export fuzzyvar fuzzytype
 }

 # fuzzyvar --
 #     Register a fuzzy variable
 # Arguments:
 #     name        Name of the variable
 #     type        Type to be used for (de)fuzzification
 # Result:
 #     None
 # Side effects:
 #     The variable is actually a _global_ variable, to keep the code
 #     simple
 #
 proc ::FuzzyLogic::fuzzyvar {name type} {
     variable vars

     set vars($name) $type
     global $name
 }

 # fuzzytype --
 #     Register a fuzzy type
 # Arguments:
 #     type        Name of the type
 #     classes     List of class names and bounds
 # Result:
 #     None
 # Side effects:
 #     The type is stored in a convenient way
 #
 proc ::FuzzyLogic::fuzzytype {type classes} {
     variable types

     set types($type) {}

     set length [expr {[llength $classes]/3}]

     set worklist {}
     for {set i 0} {$i < $length} {incr i} {
         puts $worklist
         set class [lindex $classes [expr {$i*3}]]
         set typical {}
         if { $i == 0 } {
             set min1    [lindex $classes 1]
             if { $min1 == {} } {
                 set typical [lindex $classes 2]
             }
         } else {
             set min1 [lindex $classes [expr {$i*3-1}]]
         }
         set min2 [lindex $classes [expr {$i*3+1}]]
         set max2 [lindex $classes [expr {$i*3+2}]]
         if { $i == $length-1 } {
             set max1 [lindex $classes [expr {$i*3+2}]]
             if { $max1 == {} } {
                 set typical $min2
             }
         } else {
             set max1 [lindex $classes [expr {$i*3+4}]]
         }
         if { $typical == {} } {
             set typical [expr {($min2+$max2)/2.0}]
         }
         lappend worklist $class $min1 $min2 $typical $max2 $max1
     }

     set types($type) $worklist
 }

 # Type definition:
 # c1   Min1  Max1
 # c2   Min2  Max2
 # c3   Min3  Max3
 # c4   Min4  Max4
 #
 # Schematically:
 # min1 min2 max2 max1
 # Min1 Min1 Typ1 Max1 Min2
 # Max1 Min2 Typ2 Max2 Min3
 # Max2 Min3 Typ3 Max3 Min4
 # Max3 Min4 Typ4 Max4 Max4
 #

 # fuzzyrule --
 #     Register a fuzzy rule or set of rules
 # Arguments:
 #     rule        Name of the rule
 #     body        List of IF/AND/OR/THEN clauses
 # Result:
 #     None
 # Side effects:
 #     The rule is stored for later use
 #
 proc ::FuzzyLogic::fuzzyrule {rule body} {
     variable rules

     set rules($rule) [split $body "\n"]
 }

 # applyrule --
 #     Apply a particular rule or set of rules
 # Arguments:
 #     rule        Name of the rule
 # Result:
 #     None
 # Side effects:
 #     The various fuzzy variables are defuzzified and set
 #     to the new values
 # Note:
 #     The evaluation is simplistic - no parentheses and AND/OR
 #     treated sequentially - so do not mix them!
 #
 proc ::FuzzyLogic::applyrule {rule} {
     variable rules

     set possibility {}
     set assigns     {}
     foreach line $rules($rule) {
         switch -- [lindex $line 0] {
         "IF" {
              set possibility [EvalCondition [lrange $line 1 end]]
         }
         "AND" {
              set newpos [EvalCondition [lrange $line 1 end]]
              if { $newpos < $possibility } {
                  set possibility $newpos
              }
         }
         "OR"  {
              set newpos [EvalCondition [lrange $line 1 end]]
              if { $newpos > $possibility } {
                  set possibility $newpos
              }
         }
         "THEN"  {
              set assigns [concat $assigns $possibility [EvalAssign [lrange $line 1 end]]]
         }
         "" -
         "#" {
             # Do nothing
         }
         default {
              return -code error "Unknown keyword - $line"
         }
         }
    }

    Defuzzify $assigns
 }

 # Membership --
 #     Determine the membership
 # Arguments:
 #     name        Name of the variable
 #     class       Class for which the membership must be determined
 # Result:
 #     Membership
 #
 proc ::FuzzyLogic::Membership {name class} {
     variable vars
     variable types

     set type $vars($name)
     set value [set ::$name]

     set cidx [lsearch $types($type) $class]

     foreach {min1 min2 typical max2 max1} \
         [lrange $types($type) [expr {$cidx+1}] [expr {$cidx+5}]] {break}

     if { $min1 == {} } {
         if { $value < $max2 } {
             return 1.0
         }
     } else {
         if { $value < $min1 } {
             return 0.0
         }
     }
     if { $max1 == {} } {
         if { $value > $min2 } {
             return 1.0
         }
     } else {
         if { $value > $max1 } {
             return 0.0
         }
     }

     if { $value >= $min2 && $value <= $max2 } {
         return 1.0
     }

     if { $value >= $min1 && $value <= $min2 } {
         return [expr {($min1-$value)/($min1-$min2)}]
     }

     if { $value >= $max2 && $value <= $max1 } {
         return [expr {($max1-$value)/($max1-$max2)}]
     }
 }

 # EvalCondition --
 #     Evaluate the possibility of a condition
 # Arguments:
 #     line        Line of text containing the condition
 # Result:
 #     Membership/possibility
 # Note:
 #     Simply assumes the format "variable is class"
 #
 proc ::FuzzyLogic::EvalCondition {line} {
     set var   [lindex $line 0]
     set class [lindex $line 2]

     return [Membership $var $class]
 }

 # EvalAssign --
 #     Analyse an assignment and arrange its later effectuation
 # Arguments:
 #     line        Line of text containing the condition
 # Result:
 #     List with variable name and typical value
 # Note:
 #     Simply assumes the format "variable is class"
 #
 proc ::FuzzyLogic::EvalAssign {line} {
     variable vars
     variable types

     set var     [lindex $line 0]
     set class   [lindex $line 2]
     set cidx    [lsearch $types($vars($var)) $class]
     set typical [lindex $types($vars($var)) [expr {$cidx+3}]]

     return [list $var $typical]
 }

 # Defuzzify --
 #     Defuzzify the variables
 # Arguments:
 #     assigns     List of possible assignments
 # Result:
 #     None
 # Side effects:
 #     Variables are set
 #
 proc ::FuzzyLogic::Defuzzify {assigns} {

     puts "$assigns ...."
     foreach {possibility var value} $assigns {
         if { ! [info exists sum($var)] } {
             set sum($var)    0.0
             set weight($var) 0.0
         }
         set sum($var)    [expr {$sum($var)    + $possibility * $value}]
         set weight($var) [expr {$weight($var) + $possibility }]
     }

     foreach var [array names sum] {
         global $var
         if { $weight($var) != 0.0 } {
             set $var [expr {$sum($var)/$weight($var)}]
         } else {
             return -code error "Weight for variable '$var' is zero! Check the rules"
         }
     }
 }

 # main --
 #

 # Test code
 if { 0 } {
 ::FuzzyLogic::fuzzytype temperature {
     Cold    {}    0.0
     Medium  10.0 20.0
     Hot     25.0 35.0
     VeryHot 40.0 {}
 }

 puts $::FuzzyLogic::types(temperature)

 ::FuzzyLogic::fuzzyvar T temperature

 foreach T {-1.0 0.0 1.0 5.0 9.0} \
         E { 1.0 1.0 0.9 0.5 0.1} {
     puts "[::FuzzyLogic::Membership T Cold] - $E - $T"
 }
 foreach T {-1.0 2.0 5.0 10.0 20.0 24.0 30.0} \
         E { 0.0 0.2 0.5  1.0  1.0  0.2  0.0} {
     puts "[::FuzzyLogic::Membership T Medium] - $E - $T"
 }
 foreach T { 5.0 10.0 20.0 36.0 40.0 60.0} \
         E { 0.0  0.0  0.0  0.2  1.0  1.0} {
     puts "[::FuzzyLogic::Membership T VeryHot] - $E - $T"
 }

 ::FuzzyLogic::fuzzyvar temp temperature
 ::FuzzyLogic::fuzzyvar heating heating
 ::FuzzyLogic::fuzzytype heating {
     None    0   0
     Low   100 500
     High 1000  {}
 }

 set temp 4.0
 ::FuzzyLogic::fuzzyrule heating {
     IF temp is Cold
        THEN heating is High
     IF temp is Medium
        THEN heating is Low
     IF temp is Hot
        OR temp is VeryHot
           THEN heating is None
 }

 ::FuzzyLogic::applyrule heating
 puts "Heating: $heating"
 }

 # A small application: a thermostat
 # The idea is simple:
 # - The room should be at a reasonable temperature during the
 #   day, but can be anything at night.
 # - The heater needs to take care of this in a smooth way,
 #   as little fluctuations as possible.
 # - Compare the fuzzy approach with a more classical one
 #
 # Still to be done!