fuzzy_associative_memory 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Copyright 2013, Prylis Incorporated.
2
+
3
+ Please contact the author <cpowell@prylis.com> for questions about licensing
4
+ or for specific licensing needs.
5
+
6
+ The Ruby Fuzzy Associative Memory is free software: you can redistribute it
7
+ and/or modify it under the terms of the GNU Lesser General Public License as
8
+ published by the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ The Ruby Fuzzy Associative Memory is distributed in the hope that it will be
12
+ useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
14
+ General Public License for more details.
15
+
16
+ A copy of the GNU Lesser General Public License is at the end of this file, or see
17
+ <http://www.gnu.org/licenses/lgpl.html>.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # Fuzzy logic "Fuzzy Associative Memory"
2
+
3
+ A Fuzzy Associative Memory (FAM for short) is a Fuzzy Logic tool for decision
4
+ making. Fuzzy logic FAMs have a wide range of practical applications:
5
+
6
+ * Control systems, such as governing a fan to keep a room at the "just right" temperature
7
+ * Game AI, such as imbuing bots with human-like decision-making behavior
8
+ * Prediction systems, linking causes with effects
9
+
10
+ ## How it works
11
+
12
+ A Fuzzy Associative Memory uses Fuzzy Sets to establish a set of rules that are linguistic in nature; examples might include:
13
+
14
+ * "If the room is a bit warm, turn the fan up a little bit"
15
+ * "If the orc's hit points are a little low, retreat from the enemy"
16
+ * "If the enemy is distant and my rocket ammo is low, the rocket launcher is a poor choice"
17
+ * "If the enemy is near and my shotgun ammo is okay, the shotgun is a very desirable choice"
18
+ * "If the ship is off course by a little bit, correct just a little to the right"
19
+ * "If the bird is much slower than the flock, speed it up a lot"
20
+
21
+ The linguistic rules, and the fuzzy sets they contain, are defined by a human "expert" (presumably, you). That is to say,
22
+ the rules *codify intelligence* and map this knowledge from the human domain to the digital.
23
+
24
+ After the rules are defined, a FAM is consulted to help your AI make a descision:
25
+ * The orc retreats, attacks, strafes.
26
+ * The ship launches long range missiles or fires short range guns.
27
+ * The control rods are lowered into the reactor or raised out of it.
28
+
29
+ As you can see, the fuzzy rules are deliberately vague and use qualifiers like "a little" and "a lot". Furthermore, the lines
30
+ between fuzzy sets are intentionally blurry. This is the nature of fuzzy sets; they capture such human fuzziness in a way that extracts highly natural behavior from the fuzzy rules.
31
+ When defining these rules, it helps to imagine interviewing a bona fide expert in the domain and writing down the skills necessary to be successful in the domain.
32
+
33
+ ## Project status
34
+
35
+ This is working, functional software, suitable for use in your own game or application. It currently supports:
36
+ * Triangular & trapezoidal fuzzy sets for input/output
37
+ * Larsen Implication (scaling)
38
+ * Mamdani Implication (clipping)
39
+ * Atomic antecedent propositions (`if A then Z`)
40
+ * Composite antecedent propositions (`if A or B, then Z` / `if A and B, then Z`)
41
+
42
+ To do (in descending importance, roughly):
43
+ * Parameter validations to prevent bad shapes, improper FAM layout, etc.
44
+ * Other shapes for fuzzy sets
45
+ * Hedges ('very' and 'fairly')
46
+ * Additional examples
47
+
48
+ ## Included examples
49
+
50
+ The `bin` directory contains the following examples:
51
+ * `hvac_system_example` illustrates how a FAM could govern an HVAC fan unit to maintain a constant, comfortable temperature
52
+ * `weapon_choice_example` illustrates how a FAM could let a game bot intelligently decide which weapon to use
53
+
54
+ ## References used in the creation of this software
55
+ * "Fuzzy Thinking: The New Science of Fuzzy Logic" by Bart Kosko [(Amazon link)](http://www.amazon.com/Fuzzy-Thinking-New-Science-Logic/dp/0006547133/)
56
+ * "Fuzzy Logic: The Revolutionary Computer Technology that is Changing the World" by Daniel McNeill & Paul Freiberger [(Amazon link)](http://www.amazon.com/Fuzzy-Logic-Revolutionary-Computer-Technology/dp/0671875353/)
57
+ * "Fuzzy Logic in Decision Making and Signal Processing" by Sujit Nath Pant & Keith E. Holbert [(Link)](http://enpub.fulton.asu.edu/powerzone/fuzzylogic/index.htm)
58
+ * "Programming Game AI by Example" by Mat Buckland [(Amazon link)](http://www.amazon.com/Programming-Game-Example-Mat-Buckland/dp/1556220782)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'rake/testtask'
2
+
3
+ task :default => [:test]
4
+
5
+ desc "Run the test suite once"
6
+ task :test do
7
+ ruby "test/all_suite.rb"
8
+ end
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright 2013, Prylis Incorporated.
4
+ #
5
+ # This file is part of The Ruby Fuzzy Associative Memory
6
+ # http://github.com/cpowell/fuzzy-associative-memory
7
+ # You can redistribute and/or modify this software only in accordance with
8
+ # the terms found in the "LICENSE" file included with the library.
9
+ #
10
+ require 'fuzzy_associative_memory'
11
+
12
+ # Set me to true to enable some verbose output of my calculations...
13
+ $verbosity = false
14
+
15
+ # This example is the logic to drive an HVAC fan unit depending on the ambient
16
+ # room temperature. A hotter room needs a higher fan; in a cooler room the fan
17
+ # can slow, and in a cold room the fan should stop. For the graphs and
18
+ # explanation of this particular FAM, see "Fuzzy Thinking" by Bart Kosko:
19
+ # http://www.amazon.com/Fuzzy-Thinking-New-Science-Logic/dp/1562828398/
20
+
21
+ # The input side -- the antecedent -- is expressed as a number of fuzzy sets,
22
+ # with each set representing a natural-language description. The 'temperature
23
+ # input' variable, is the assemblage of all our fuzzy input sets.
24
+ temperature_in = FuzzyAssociativeMemory::LinguisticVariable.new("room temperature") # degrees fahrenheit
25
+
26
+ # Wider: less important, coarse control; narrower: more important, fine control
27
+ cold = FuzzyAssociativeMemory::Trapezoid.new(40, 40, 40, 50) # 20 deg wide
28
+ cool = FuzzyAssociativeMemory::Triangle.new(45, 55, 65) # 20 deg wide
29
+ just_right = FuzzyAssociativeMemory::Triangle.new(60, 65, 70) # 10 deg wide
30
+ warm = FuzzyAssociativeMemory::Triangle.new(65, 75, 85) # 20 deg wide
31
+ hot = FuzzyAssociativeMemory::Trapezoid.new(80, 90, 90, 90) # 20 deg wide
32
+
33
+ temperature_in.sets = [cold, cool, just_right, warm, hot]
34
+
35
+ # The output side -- the consequent -- expressed as a number of fuzzy sets,
36
+ # with each set representing a natural-language description. The 'resultant
37
+ # fan speed' variable, is the assemblage of all our fuzzy output sets.
38
+ fan_speed = FuzzyAssociativeMemory::LinguisticVariable.new("fan speed") # cubic feet per minute (CFM)
39
+
40
+ stop = FuzzyAssociativeMemory::Triangle.new(-30, 0, 30) # 60 CFM wide
41
+ slow = FuzzyAssociativeMemory::Triangle.new(10, 30, 50) # 40 CFM wide
42
+ medium = FuzzyAssociativeMemory::Triangle.new(40, 50, 60) # 20 CFM wide
43
+ fast = FuzzyAssociativeMemory::Triangle.new(50, 70, 90) # 40 CFM wide
44
+ blast = FuzzyAssociativeMemory::Triangle.new(70, 100, 130) # 60 CFM wide
45
+
46
+ fan_speed.sets = [stop, slow, medium, fast, blast]
47
+
48
+ # Natural-language marriage of the inputs to the outputs, e.g.
49
+ # "If the temperature is cool, the fan motor speed should be slow."
50
+ system = FuzzyAssociativeMemory::Ruleset.new("HVAC control", :larsen)
51
+
52
+ rule_1 = FuzzyAssociativeMemory::Rule.new('If room is cold, the fan motor stops', [cold], nil, stop)
53
+ rule_2 = FuzzyAssociativeMemory::Rule.new('If room is cool, the fan motor is slow', [cool], nil, slow)
54
+ rule_3 = FuzzyAssociativeMemory::Rule.new('If room is just right, the fan motor is medium', [just_right], nil, medium)
55
+ rule_4 = FuzzyAssociativeMemory::Rule.new('If room is warm, the fan motor speeds up', [warm], nil, fast)
56
+ rule_5 = FuzzyAssociativeMemory::Rule.new('If room is hot, the fan motor runs full-blast', [hot], nil, blast)
57
+
58
+ system.rules = [rule_1, rule_2, rule_3, rule_4, rule_5]
59
+
60
+ # n=64
61
+ # puts "The #{system.name} determines: for #{temperature_in.name} #{n}, the #{fan_speed.name} is #{system.calculate(n)} CFM"
62
+
63
+ # Check the logic for a wide range of temps and output the results
64
+ (40..90).each do |n|
65
+ puts "The #{system.name} determines: for #{temperature_in.name} #{n}, the #{fan_speed.name} is #{system.calculate(n)} CFM"
66
+ end
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright 2013, Prylis Incorporated.
4
+ #
5
+ # This file is part of The Ruby Fuzzy Associative Memory
6
+ # http://github.com/cpowell/fuzzy-associative-memory
7
+ # You can redistribute and/or modify this software only in accordance with
8
+ # the terms found in the "LICENSE" file included with the library.
9
+ #
10
+ require 'fuzzy_associative_memory'
11
+
12
+ # Set me to true to enable some verbose output of my calculations...
13
+ $verbosity = false
14
+
15
+ implication = :mamdani # or :larsen
16
+
17
+ # This is a complicated example; I recommend you understand the 'HVAC system
18
+ # example' before trying to grok this one.
19
+ #
20
+ # This example is inspired by the fuzzy logic described by Mat Buckland in
21
+ # "Programming Game AI by Example". In this we illustrate how an AI 'bot'
22
+ # might use fuzzy logic to pick which weapon to use. Unlike the 'HVAC system'
23
+ # example, which uses just one input, this example uses two inputs: distance
24
+ # to enemy, and ammo remaining. WE SET UP AND RUN TWO FAMS, FOR TWO DIFFERENT
25
+ # WEAPONS (ROCKET AND SHOTGUN), TO CHOOSE WHICH WEAPON TO USE.
26
+
27
+ # The input side -- the antecedents -- are expressed as a number of fuzzy sets,
28
+ # with each set representing a natural-language description. The 'distance to
29
+ # target' variable is an assemblage of our fuzzy input sets.
30
+ target_dist = FuzzyAssociativeMemory::LinguisticVariable.new("distance to target") # pixels
31
+
32
+ tgt_close = FuzzyAssociativeMemory::Trapezoid.new(-150, -25, 25, 150)
33
+ tgt_medium = FuzzyAssociativeMemory::Triangle.new(25, 150, 300)
34
+ tgt_far = FuzzyAssociativeMemory::Trapezoid.new(150, 300, 500, 650)
35
+
36
+ target_dist.sets = [tgt_close, tgt_medium, tgt_far]
37
+
38
+ # Now for the second input (or antecedent): the amount of ammo left for this
39
+ # particular weapon.
40
+ rocket_ammo_status = FuzzyAssociativeMemory::LinguisticVariable.new("rocket launcher ammo quantity")
41
+
42
+ rkt_ammo_low = FuzzyAssociativeMemory::Triangle.new(-10, 0, 10)
43
+ rkt_ammo_okay = FuzzyAssociativeMemory::Triangle.new(0, 10, 30)
44
+ rkt_ammo_loads = FuzzyAssociativeMemory::Trapezoid.new(10, 30, 40, 40)
45
+
46
+ rocket_ammo_status.sets = [rkt_ammo_low, rkt_ammo_okay, rkt_ammo_loads]
47
+
48
+ # The output side -- the consequent -- expressed as a number of fuzzy sets,
49
+ # with each set representing a natural-language description. The 'resultant
50
+ # fan speed' variable is the assemblage of all our fuzzy output sets.
51
+ desirability = FuzzyAssociativeMemory::LinguisticVariable.new("weapon desirability")
52
+
53
+ undes = FuzzyAssociativeMemory::Trapezoid.new(0, 0, 20, 50)
54
+ desir = FuzzyAssociativeMemory::Triangle.new(30, 50, 70)
55
+ v_desir = FuzzyAssociativeMemory::Trapezoid.new(50, 80, 100, 100)
56
+
57
+ desirability.sets = [undes, desir, v_desir]
58
+
59
+ # Natural-language marriage of the inputs to the outputs, e.g.
60
+ # "If the temperature is cool, the fan motor speed should be slow."
61
+ rkt_ruleset = FuzzyAssociativeMemory::Ruleset.new("Rocket launcher desirability", implication)
62
+
63
+ rule_1 = FuzzyAssociativeMemory::Rule.new('If target is far and I have loads of rocket ammo, rocket launcher is desirable', [tgt_far, rkt_ammo_loads], :and, desir)
64
+ rule_2 = FuzzyAssociativeMemory::Rule.new('If target is far and I have some rocket ammo, rocket launcher is undesirable', [tgt_far, rkt_ammo_okay], :and, undes)
65
+ rule_3 = FuzzyAssociativeMemory::Rule.new('If target is far and I have low rocket ammo, rocket launcher is undesirable', [tgt_far, rkt_ammo_low], :and, undes)
66
+ rule_4 = FuzzyAssociativeMemory::Rule.new('If target is medium-distance and I have loads of rocket ammo, rocket launcher is very desirable', [tgt_medium, rkt_ammo_loads], :and, v_desir)
67
+ rule_5 = FuzzyAssociativeMemory::Rule.new('If target is medium-distance and I have some rocket ammo, rocket launcher is very desirable', [tgt_medium, rkt_ammo_okay], :and, v_desir)
68
+ rule_6 = FuzzyAssociativeMemory::Rule.new('If target is medium-distance and I have low rocket ammo, rocket launcher is desirable', [tgt_medium, rkt_ammo_low], :and, desir)
69
+ rule_7 = FuzzyAssociativeMemory::Rule.new('If target is close and I have loads of rocket ammo, rocket launcher is undesirable', [tgt_close, rkt_ammo_loads], :and, undes)
70
+ rule_8 = FuzzyAssociativeMemory::Rule.new('If target is close and I have some rocket ammo, rocket launcher is undesirable', [tgt_close, rkt_ammo_okay], :and, undes)
71
+ rule_9 = FuzzyAssociativeMemory::Rule.new('If target is close and I have low rocket ammo, rocket launcher is undesirable', [tgt_close, rkt_ammo_low], :and, undes)
72
+
73
+ rkt_ruleset.rules = [rule_1, rule_2, rule_3, rule_4, rule_5, rule_6, rule_7, rule_8, rule_9]
74
+
75
+ d = 110
76
+ ra = 8
77
+ rocket_desirability = rkt_ruleset.calculate(d, ra)
78
+ puts "#{rkt_ruleset.name}: for #{target_dist.name} #{d} and #{rocket_ammo_status.name} #{ra}, the #{desirability.name} is #{rocket_desirability}"
79
+
80
+ ##################################
81
+ # That was ONE FAM. Now we set up a WHOLE NEW FAM based on the shotgun data.
82
+ # We can re-use the 'desirability' consequent since that's static across all
83
+ # weapons. We just need new antecedents and rules.
84
+
85
+ # Now for the second input (or antecedent): the amount of ammo left for this
86
+ # particular weapon.
87
+ shotgun_ammo_status = FuzzyAssociativeMemory::LinguisticVariable.new("shotgun ammo quantity")
88
+ gun_ammo_low = FuzzyAssociativeMemory::Triangle.new(-10, 0, 10)
89
+ gun_ammo_okay = FuzzyAssociativeMemory::Triangle.new(0, 10, 30)
90
+ gun_ammo_loads = FuzzyAssociativeMemory::Trapezoid.new(10, 30, 40, 40)
91
+ shotgun_ammo_status.sets = [gun_ammo_low, gun_ammo_okay, gun_ammo_loads]
92
+
93
+ gun_ruleset = FuzzyAssociativeMemory::Ruleset.new("Shotgun desirability", implication)
94
+ rule_1 = FuzzyAssociativeMemory::Rule.new('If target is far and I have loads of shotgun ammo, shotgun is undesirable', [tgt_far, gun_ammo_loads], :and, undes)
95
+ rule_2 = FuzzyAssociativeMemory::Rule.new('If target is far and I have some shotgun ammo, shotgun is undesirable', [tgt_far, gun_ammo_okay], :and, undes)
96
+ rule_3 = FuzzyAssociativeMemory::Rule.new('If target is far and I have low shotgun ammo, shotgun is undesirable', [tgt_far, gun_ammo_low], :and, undes)
97
+ rule_4 = FuzzyAssociativeMemory::Rule.new('If target is medium-distance and I have loads of shotgun ammo, shotgun is desirable', [tgt_medium, gun_ammo_loads], :and, desir)
98
+ rule_5 = FuzzyAssociativeMemory::Rule.new('If target is medium-distance and I have some shotgun ammo, shotgun is desirable', [tgt_medium, gun_ammo_okay], :and, desir)
99
+ rule_6 = FuzzyAssociativeMemory::Rule.new('If target is medium-distance and I have low shotgun ammo, shotgun is undesirable', [tgt_medium, gun_ammo_low], :and, undes)
100
+ rule_7 = FuzzyAssociativeMemory::Rule.new('If target is close and I have loads of shotgun ammo, shotgun is very desirable', [tgt_close, gun_ammo_loads], :and, v_desir)
101
+ rule_8 = FuzzyAssociativeMemory::Rule.new('If target is close and I have some shotgun ammo, shotgun is very desirable', [tgt_close, gun_ammo_okay], :and, v_desir)
102
+ rule_9 = FuzzyAssociativeMemory::Rule.new('If target is close and I have low shotgun ammo, shotgun is very desirable', [tgt_close, gun_ammo_low], :and, v_desir)
103
+ gun_ruleset.rules = [rule_1, rule_2, rule_3, rule_4, rule_5, rule_6, rule_7, rule_8, rule_9]
104
+
105
+ sa = 12
106
+ gun_desirability = gun_ruleset.calculate(d, sa)
107
+ puts "#{gun_ruleset.name}: for #{target_dist.name} #{d} and #{shotgun_ammo_status.name} #{sa}, the #{desirability.name} is #{gun_desirability}"
108
+
109
+ if gun_desirability < rocket_desirability
110
+ outcome='I choose the rocket launcher.'
111
+ else
112
+ outcome='I choose the shotgun.'
113
+ end
114
+
115
+ puts outcome
@@ -0,0 +1,25 @@
1
+ #
2
+ # Copyright 2013, Prylis Incorporated.
3
+ #
4
+ # This file is part of The Ruby Fuzzy Associative Memory
5
+ # http://github.com/cpowell/fuzzy-associative-memory
6
+ # You can redistribute and/or modify this software only in accordance with
7
+ # the terms found in the "LICENSE" file included with the library.
8
+ #
9
+ class FuzzyAssociativeMemory::LinguisticVariable
10
+ attr_accessor :sets
11
+ attr_reader :name
12
+
13
+ def initialize(name)
14
+ @name = name
15
+ @sets = []
16
+ end
17
+
18
+ def add_set(set)
19
+ @sets << set
20
+ end
21
+
22
+ def [](n)
23
+ @sets[n]
24
+ end
25
+ end
@@ -0,0 +1,63 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Copyright 2013, Prylis Incorporated.
4
+ #
5
+ # This file is part of The Ruby Fuzzy Associative Memory
6
+ # http://github.com/cpowell/fuzzy-associative-memory
7
+ # You can redistribute and/or modify this software only in accordance with
8
+ # the terms found in the "LICENSE" file included with the library.
9
+ #
10
+ class FuzzyAssociativeMemory::Rule
11
+ attr_reader :antecedents, :consequent, :boolean
12
+
13
+ # Marries an input fuzzy set and an output fuzzy set in an if-then
14
+ # arrangement, i.e. if (antecedent) then (consequent).
15
+ #
16
+ # * *Args* :
17
+ # - +antecedent_array+ -> an array of one or more input fuzzy sets
18
+ # - +boolean+ -> term to join the antecedents, may be: nil, 'AND', 'OR'
19
+ # - +consequent+ -> the output fuzzy set
20
+ # - +natural_language+ -> a rule description (your own words), useful in output
21
+ #
22
+ def initialize(natural_language, antecedent_array, boolean, consequent)
23
+ raise ArgumentError, "Antecedent array must contain at least one fuzzy set" unless antecedent_array.size > 0
24
+ raise ArgumentError, "Consequent must be provided" unless consequent
25
+
26
+ if antecedent_array.size > 1
27
+ raise ArgumentError, "boolean must be sym :and or :or" unless [:and, :or].include? boolean
28
+ else
29
+ raise ArgumentError, "boolean must be nil" unless boolean.nil?
30
+ end
31
+
32
+ @natural_language = natural_language
33
+ @antecedents = antecedent_array
34
+ @consequent = consequent
35
+ @boolean = boolean
36
+ end
37
+
38
+ # Triggers the rule. The antecedent(s) is/are fired with the supplied inputs
39
+ # and the µ (degree of fit) is calculated and returned.
40
+ #
41
+ # * *Args* :
42
+ # - +value_array+ -> an array of input values for the rule (degrees, distance, strength, whatever)
43
+ # * *Returns* :
44
+ # - the degree of fit for this rule
45
+ #
46
+ def fire(value_array)
47
+ mus = Array.new
48
+
49
+ @antecedents.each_with_index do |antecedent, index|
50
+ mus[index] = antecedent.mu(value_array[index])
51
+ end
52
+
53
+ if @boolean==:and
54
+ mu = mus.min # AND / Intersection == minimum
55
+ else
56
+ mu = mus.max # OR / Union == maximum
57
+ end
58
+
59
+ puts "Fired rule '#{@natural_language}': µ choices are [#{mus.join(',')}], final µ is #{mu}" if $verbosity
60
+ [@consequent, mu]
61
+ end
62
+
63
+ end
@@ -0,0 +1,79 @@
1
+ # encoding: utf-8
2
+ #
3
+ # Copyright 2013, Prylis Incorporated.
4
+ #
5
+ # This file is part of The Ruby Fuzzy Associative Memory
6
+ # http://github.com/cpowell/fuzzy-associative-memory
7
+ # You can redistribute and/or modify this software only in accordance with
8
+ # the terms found in the "LICENSE" file included with the library.
9
+ #
10
+ class FuzzyAssociativeMemory::Ruleset
11
+ attr_accessor :rules
12
+ attr_reader :name
13
+ attr_reader :implication
14
+
15
+ def initialize(name, implication_mechanism)
16
+ raise ArgumentError, 'invalid implication mechanism' unless [:larsen, :mamdani].include? implication_mechanism
17
+ @name = name
18
+ @rules = []
19
+ @implication = implication_mechanism
20
+ end
21
+
22
+ def add_rule(rule)
23
+ @rules << rule
24
+ end
25
+
26
+ def calculate(*input_values)
27
+ @consequent_mus = {}
28
+ @kept_consequents = {}
29
+ @consequents = []
30
+
31
+ puts ">>> Firing all rules..." if $verbosity
32
+ @rules.each_with_index do |rule, rule_num|
33
+ # Fire each rule to determine the µ value (degree of fit).
34
+ # Gather the µ vals by consequent, since each consequent may in fact
35
+ # have been fired more than once and we'll need that knowledge in a
36
+ # moment...
37
+ cons, mu = rule.fire(input_values)
38
+ if @consequent_mus.has_key?(cons)
39
+ @consequent_mus[cons] << mu
40
+ else
41
+ @consequent_mus[cons] = [mu]
42
+ end
43
+ end
44
+
45
+ # Since any given consequent may have been activated more than once, we
46
+ # need to get just a single µ value out -- we only care about the 'best'
47
+ # µ. A popular way of doing so is to OR the values together, i.e. keep the
48
+ # maximum µ value and discard the others.
49
+ @consequent_mus.each do |cons, mu_array|
50
+ @kept_consequents[cons] = mu_array.max
51
+ end
52
+
53
+ # Using each µ value, alter the consequent fuzzy set's polgyon. This is
54
+ # called implication, and 'weights' the consequents properly. There are
55
+ # several common ways of doing it, such as Larsen (scaling) and Mamdani
56
+ # (clipping).
57
+ @kept_consequents.each do |cons, mu|
58
+ case @implication
59
+ when :mamdani
60
+ @consequents << cons.mamdani(mu)
61
+ when :larsen
62
+ @consequents << cons.larsen(mu)
63
+ else
64
+ raise RuntimeError, "I must have been passed an unknown implication mechanism: #{@implication}"
65
+ end
66
+ end
67
+
68
+ # Defuzzify into a discrete & usable value by adding up the weighted
69
+ # consequents' contributions to the output. Again there are several ways
70
+ # of doing it, such as computing the centroid of the combined 'mass', or
71
+ # the 'mean of maximum' of the tallest set(s). Here we use the "Average
72
+ # of Maxima" summation mechanism. MaxAv is defined as:
73
+ # (∑ representative value * height) / (∑ height) for all output sets
74
+ # where 'representative value' is shape-dependent.
75
+ numerator_terms = @consequents.map { |set| set.centroid_x * set.height }
76
+ denominator_terms = @consequents.map { |set| set.height }
77
+ numerator_terms.inject{|sum,x| sum + x } / denominator_terms.inject{|sum,x| sum + x }
78
+ end
79
+ end
@@ -0,0 +1,34 @@
1
+ #
2
+ # Copyright 2013, Prylis Incorporated.
3
+ #
4
+ # This file is part of The Ruby Fuzzy Associative Memory
5
+ # http://github.com/cpowell/fuzzy-associative-memory
6
+ # You can redistribute and/or modify this software only in accordance with
7
+ # the terms found in the "LICENSE" file included with the library.
8
+ #
9
+ class FuzzyAssociativeMemory::FuzzySet
10
+
11
+ # mu (named for the symbol) is the 'membership function', sometimes known as
12
+ # the 'degree of fit' or the 'degree of membership'. This computed value
13
+ # informs HOW MUCH this set activates or fires for the provided input value.
14
+ # A DoM of zero means that this set fits the input not at all; a DoM of 1.0
15
+ # means that this set fires / fits completely.
16
+ #
17
+ # * *Args* :
18
+ # - +value+ -> the input value in question
19
+ # * *Returns* :
20
+ # - a 'fitness' degree between 0 and 1.0
21
+ #
22
+ def mu(value)
23
+ raise "Subclass must define!"
24
+ end
25
+
26
+ def centroid_x
27
+ raise "Subclass must define!"
28
+ end
29
+
30
+ def larsen(ratio)
31
+ raise "Subclass must define!"
32
+ end
33
+
34
+ end
@@ -0,0 +1,70 @@
1
+ #
2
+ # Copyright 2013, Prylis Incorporated.
3
+ #
4
+ # This file is part of The Ruby Fuzzy Associative Memory
5
+ # http://github.com/cpowell/fuzzy-associative-memory
6
+ # You can redistribute and/or modify this software only in accordance with
7
+ # the terms found in the "LICENSE" file included with the library.
8
+ #
9
+ require 'fuzzy_associative_memory/set'
10
+ class FuzzyAssociativeMemory::Trapezoid < FuzzyAssociativeMemory::FuzzySet
11
+
12
+ attr_reader :left, :top_left, :top_right, :right, :height
13
+
14
+ def initialize(left, top_left, top_right, right, height=1.0)
15
+ # TODO validations
16
+
17
+ @left = left.to_f
18
+ @top_left = top_left.to_f
19
+ @top_right = top_right.to_f
20
+ @right = right.to_f
21
+ @height = height.to_f
22
+ end
23
+
24
+ def mu(value)
25
+ if value < @left || value > @right
26
+ 0.0
27
+ elsif value >= @left && value < @top_left
28
+ (value - @left) / (@top_left - @left)
29
+ elsif value >= @top_left && value <= @top_right
30
+ 1.0
31
+ else
32
+ (@right - value) / (@right - @top_right)
33
+ end
34
+ end
35
+
36
+ def centroid_x
37
+ a = @top_right - @top_left
38
+ b = @right - @left
39
+ c = @top_left - @left
40
+
41
+ cx = (2.0*a*c + a**2 + c*b + a*b + b**2.0) / (3.0 * (a+b))
42
+ cx + @left
43
+ # cy = (@height * (2.0*a + b)) / (3.0 * (a+b))
44
+ # [cx+@left, cy]
45
+ end
46
+
47
+ def height=(new_height)
48
+ @height = new_height
49
+ end
50
+
51
+ def larsen(ratio)
52
+ t = self.dup
53
+ t.height=(t.height * ratio)
54
+ t
55
+ end
56
+
57
+ def mamdani(clip_height)
58
+ left = @left
59
+ top_left = @left + (clip_height * (@top_left - @left))
60
+ top_right = @right - (clip_height * (@right - @top_right))
61
+ right = @right
62
+
63
+ FuzzyAssociativeMemory::Trapezoid.new(left, top_left, top_right, right, clip_height)
64
+ end
65
+
66
+ def to_s
67
+ "Trapezoid {#{@left}/#{@top_left}/#{@top_right}/#{@right}, height #{@height}}"
68
+ end
69
+
70
+ end
@@ -0,0 +1,60 @@
1
+ #
2
+ # Copyright 2013, Prylis Incorporated.
3
+ #
4
+ # This file is part of The Ruby Fuzzy Associative Memory
5
+ # http://github.com/cpowell/fuzzy-associative-memory
6
+ # You can redistribute and/or modify this software only in accordance with
7
+ # the terms found in the "LICENSE" file included with the library.
8
+ #
9
+ require 'fuzzy_associative_memory/set'
10
+ class FuzzyAssociativeMemory::Triangle < FuzzyAssociativeMemory::FuzzySet
11
+
12
+ attr_reader :left, :center, :right, :height
13
+
14
+ def initialize(left, center, right, height=1.0)
15
+ # TODO validations
16
+
17
+ @center = center.to_f
18
+ @left = left.to_f
19
+ @right = right.to_f
20
+ @height = height.to_f
21
+ end
22
+
23
+ def mu(value)
24
+ if value < @left || value > @right
25
+ 0.0
26
+ else
27
+ 1 - ((@center-value).abs / ((@right - @left) / 2.0 ))
28
+ end
29
+ end
30
+
31
+ def centroid_x
32
+ cx = (@left + @right + @center) / 3.0
33
+ # cy = @height / 3.0
34
+ # [cx, cy]
35
+ end
36
+
37
+ def height=(new_height)
38
+ @height = new_height
39
+ end
40
+
41
+ def larsen(ratio)
42
+ t = self.dup
43
+ t.height=(t.height * ratio)
44
+ t
45
+ end
46
+
47
+ def mamdani(clip_height)
48
+ left = @left
49
+ top_left = @left + (clip_height * (@center - @left))
50
+ top_right = @right - (clip_height * (@right - @center))
51
+ right = @right
52
+
53
+ FuzzyAssociativeMemory::Trapezoid.new(left, top_left, top_right, right, clip_height)
54
+ end
55
+
56
+ def to_s
57
+ "Triangle {#{@left}/#{@center}/#{@right}, height #{@height}}"
58
+ end
59
+
60
+ end
@@ -0,0 +1,9 @@
1
+ class FuzzyAssociativeMemory
2
+ end
3
+
4
+ require 'fuzzy_associative_memory/linguistic_variable'
5
+ require 'fuzzy_associative_memory/triangle'
6
+ require 'fuzzy_associative_memory/trapezoid'
7
+ require 'fuzzy_associative_memory/rule'
8
+ require 'fuzzy_associative_memory/ruleset'
9
+ require 'fuzzy_associative_memory/set'
data/test/all_suite.rb ADDED
@@ -0,0 +1,11 @@
1
+ # This is a whole test suite.
2
+ #
3
+ # $ ruby test/all_suite.rb
4
+ # or just
5
+ # $ rake test
6
+
7
+ # require 'simplecov'
8
+ # SimpleCov.start
9
+
10
+ require "test/triangle_test"
11
+ require "test/trapezoid_test"
@@ -0,0 +1,96 @@
1
+ gem 'minitest'
2
+ require 'minitest/autorun'
3
+
4
+ $:.push File.expand_path('../../lib/', __FILE__)
5
+ require 'fuzzy_associative_memory'
6
+
7
+ # ruby ./test/trapezoid_test.rb
8
+
9
+ class TrapezoidTest < MiniTest::Unit::TestCase
10
+ def setup
11
+ @t = FuzzyAssociativeMemory::Trapezoid.new(7, 10, 13, 16)
12
+ end
13
+
14
+ def test_dom_is_zero_outside_left_bound
15
+ assert_equal(0, @t.mu(2))
16
+ end
17
+
18
+ def test_dom_is_zero_at_left
19
+ assert_equal(0, @t.mu(7))
20
+ end
21
+
22
+ def test_dom_is_half_at_half_left_offset
23
+ assert_equal(0.5, @t.mu(8.5))
24
+ end
25
+
26
+ def test_dom_is_one_at_top_left
27
+ assert_equal(1.0, @t.mu(10))
28
+ end
29
+
30
+ def test_dom_is_one_at_top_right
31
+ assert_equal(1.0, @t.mu(13))
32
+ end
33
+
34
+ def test_dom_is_half_at_half_right_offset
35
+ assert_equal(0.5, @t.mu(14.5))
36
+ end
37
+
38
+ def test_dom_is_zero_at_right
39
+ assert_equal(0, @t.mu(16))
40
+ end
41
+
42
+ def test_dom_is_zero_outside_right_bound
43
+ assert_equal(0, @t.mu(17))
44
+ end
45
+
46
+ def test_dom_is_correct_at_three_fifths_offset
47
+ t = FuzzyAssociativeMemory::Trapezoid.new(0, 5, 10, 15)
48
+ assert_equal(0.6, t.mu(3.0))
49
+ end
50
+
51
+ def test_dom_is_correct_at_seven_tenths_offset
52
+ t = FuzzyAssociativeMemory::Trapezoid.new(0, 5, 10, 15)
53
+ assert_equal(0.6, t.mu(12.0))
54
+ end
55
+
56
+ def test_centroid_calculation_1
57
+ t = FuzzyAssociativeMemory::Trapezoid.new(0, 20, 50, 50)
58
+ assert_equal(29.583333333333332, t.centroid_x)
59
+ end
60
+
61
+ def test_centroid_calculation_2
62
+ t = FuzzyAssociativeMemory::Trapezoid.new(0, 10, 20, 30)
63
+ assert_equal(15.0, t.centroid_x)
64
+ end
65
+
66
+ def test_centroid_calculation_when_not_starting_at_zero
67
+ t = FuzzyAssociativeMemory::Trapezoid.new(50, 80, 100, 100)
68
+ assert_equal(81.42857142857143, t.centroid_x)
69
+ end
70
+
71
+ def test_dom_1
72
+ t = FuzzyAssociativeMemory::Trapezoid.new(20, 30, 40, 50)
73
+ assert_equal(0.9, t.mu(41))
74
+ end
75
+
76
+ def test_mamdani_clipping_1
77
+ t = FuzzyAssociativeMemory::Trapezoid.new(0, 30, 100, 110)
78
+ t2 = t.mamdani(0.83)
79
+ assert_equal(0, t2.left)
80
+ assert_equal(24.9, t2.top_left)
81
+ assert_equal(101.7, t2.top_right)
82
+ assert_equal(110, t2.right)
83
+ assert_equal(0.83, t2.height)
84
+ end
85
+
86
+ def test_mamdani_clipping_2
87
+ t = FuzzyAssociativeMemory::Trapezoid.new(50, 80, 150, 160)
88
+ t2 = t.mamdani(0.83)
89
+ assert_equal(50, t2.left)
90
+ assert_equal(74.9, t2.top_left)
91
+ assert_equal(151.7, t2.top_right)
92
+ assert_equal(160, t2.right)
93
+ assert_equal(0.83, t2.height)
94
+ end
95
+
96
+ end
@@ -0,0 +1,64 @@
1
+ gem 'minitest'
2
+ require 'minitest/autorun'
3
+
4
+ $:.push File.expand_path('../../lib/', __FILE__)
5
+ require 'fuzzy_associative_memory'
6
+
7
+ # ruby ./test/triangle_test.rb
8
+
9
+ class TriangleTest < MiniTest::Unit::TestCase
10
+ def setup
11
+ @t = FuzzyAssociativeMemory::Triangle.new(7, 10, 13)
12
+ end
13
+
14
+ def test_dom_is_zero_outside_left_bound
15
+ assert_equal(0, @t.mu(2))
16
+ end
17
+
18
+ def test_dom_is_zero_outside_right_bound
19
+ assert_equal(0, @t.mu(15))
20
+ end
21
+
22
+ def test_dom_is_one_at_peak
23
+ assert_equal(1.0, @t.mu(10))
24
+ end
25
+
26
+ def test_dom_is_half_at_half_right_offset
27
+ assert_equal(0.5, @t.mu(11.5))
28
+ end
29
+
30
+ def test_dom_is_half_at_half_left_offset
31
+ assert_equal(0.5, @t.mu(8.5))
32
+ end
33
+
34
+ def test_dom_is_correct_at_three_fifths_offset
35
+ t = FuzzyAssociativeMemory::Triangle.new(0, 5, 10)
36
+ assert_equal(0.6, t.mu(3.0))
37
+ end
38
+
39
+ def test_dom_is_correct_at_seven_tenths_offset
40
+ t = FuzzyAssociativeMemory::Triangle.new(0, 5, 10)
41
+ assert_equal(0.6, t.mu(7.0))
42
+ end
43
+
44
+ def test_centroid_calculation
45
+ t = FuzzyAssociativeMemory::Triangle.new(5, 10, 15)
46
+ assert_equal(10.0, t.centroid_x)
47
+ end
48
+
49
+ def test_larsen_scaling
50
+ scaled = @t.larsen(0.15)
51
+ assert_equal(0.15, scaled.height)
52
+ end
53
+
54
+ def test_mamdani_clipping_1
55
+ t2 = @t.mamdani(0.83)
56
+ assert(t2.is_a? FuzzyAssociativeMemory::Trapezoid)
57
+ assert_equal(7, t2.left)
58
+ assert_equal(9.49, t2.top_left)
59
+ assert_equal(10.51, t2.top_right)
60
+ assert_equal(13, t2.right)
61
+ assert_equal(0.83, t2.height)
62
+ end
63
+
64
+ end
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fuzzy_associative_memory
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 1.0.0
6
+ platform: ruby
7
+ authors:
8
+ - Chris Powell
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-26 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: |2
15
+ A Fuzzy Associative Memory (FAM for short) is a Fuzzy Logic tool for decision making. Fuzzy logic FAMs have a wide range of practical applications:
16
+
17
+ * Control systems, such as governing a fan to keep a room at the "just right" temperature
18
+ * Game AI, such as imbuing bots with human-like decision-making behavior
19
+ * Prediction systems, linking causes with effects
20
+
21
+ A Fuzzy Associative Memory uses Fuzzy Sets to establish a set of rules that are linguistic in nature. The linguistic rules, and the fuzzy sets they contain, are defined by a human "expert" (presumably, you). That is to say, the rules codify intelligence and map this knowledge from the human domain to the digital.
22
+ email: cpowell@prylis.com
23
+ executables: []
24
+ extensions: []
25
+ extra_rdoc_files: []
26
+ files:
27
+ - lib/fuzzy_associative_memory.rb
28
+ - lib/fuzzy_associative_memory/linguistic_variable.rb
29
+ - lib/fuzzy_associative_memory/rule.rb
30
+ - lib/fuzzy_associative_memory/ruleset.rb
31
+ - lib/fuzzy_associative_memory/set.rb
32
+ - lib/fuzzy_associative_memory/trapezoid.rb
33
+ - lib/fuzzy_associative_memory/triangle.rb
34
+ - bin/hvac_system_example.rb
35
+ - bin/weapon_choice_example.rb
36
+ - LICENSE
37
+ - Rakefile
38
+ - README.md
39
+ - test/all_suite.rb
40
+ - test/trapezoid_test.rb
41
+ - test/triangle_test.rb
42
+ homepage: http://github.com/cpowell/fuzzy-associative-memory
43
+ licenses:
44
+ - LGPL
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --main
48
+ - README.md
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ none: false
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ none: false
63
+ requirements: []
64
+ rubyforge_project:
65
+ rubygems_version: 1.8.24
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: A fuzzy logic 'Fuzzy Associative Memory' (FAM) for fuzzy control systems, decision-making, artificial intelligence / AI, game agents & bots, etc.
69
+ test_files: []