fuzzy_associative_memory 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []