fathom 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.autotest +10 -0
  2. data/Gemfile +10 -2
  3. data/Gemfile.lock +8 -0
  4. data/TODO.md +12 -25
  5. data/VERSION +1 -1
  6. data/lib/{fathom/ext → ext}/array.rb +0 -0
  7. data/lib/{fathom/ext → ext}/faster_csv.rb +0 -0
  8. data/lib/{fathom/ext → ext}/open_struct.rb +0 -0
  9. data/lib/{fathom/ext → ext}/string.rb +0 -0
  10. data/lib/fathom.rb +16 -13
  11. data/lib/fathom/agent.rb +8 -9
  12. data/lib/fathom/{causal_graph.rb → archive/causal_graph.rb} +0 -0
  13. data/lib/fathom/{concept.rb → archive/concept.rb} +0 -0
  14. data/lib/fathom/archive/conditional_probability_matrix.rb +3 -0
  15. data/lib/fathom/{inverter.rb → archive/inverter.rb} +0 -0
  16. data/lib/fathom/archive/node.rb +24 -1
  17. data/lib/fathom/distributions/discrete_uniform.rb +11 -32
  18. data/lib/fathom/import.rb +37 -34
  19. data/lib/fathom/import/yaml_import.rb +22 -1
  20. data/lib/fathom/knowledge_base.rb +34 -23
  21. data/lib/fathom/knowledge_base/search.rb +19 -0
  22. data/lib/fathom/node.rb +32 -1
  23. data/lib/fathom/node/belief_node.rb +121 -0
  24. data/lib/fathom/node/cpm_node.rb +100 -0
  25. data/lib/fathom/node/data_collection.rb +97 -0
  26. data/lib/fathom/{data_node.rb → node/data_node.rb} +1 -1
  27. data/lib/fathom/{value_aggregator.rb → node/decision.rb} +5 -5
  28. data/lib/fathom/node/discrete_node.rb +41 -0
  29. data/lib/fathom/node/fact.rb +24 -0
  30. data/lib/fathom/{mc_node.rb → node/mc_node.rb} +1 -1
  31. data/lib/fathom/{enforced_name.rb → node/node_extensions/enforced_name.rb} +1 -1
  32. data/lib/fathom/{numeric_methods.rb → node/node_extensions/numeric_methods.rb} +19 -1
  33. data/lib/fathom/{plausible_range.rb → node/plausible_range.rb} +1 -1
  34. data/spec/ext/array_spec.rb +10 -0
  35. data/spec/ext/faster_csv_spec.rb +10 -0
  36. data/spec/ext/open_struct_spec.rb +20 -0
  37. data/spec/ext/string_spec.rb +7 -0
  38. data/spec/fathom/import/csv_import_spec.rb +11 -9
  39. data/spec/fathom/import/yaml_import_spec.rb +27 -7
  40. data/spec/fathom/knowledge_base_spec.rb +8 -4
  41. data/spec/fathom/node/belief_node_spec.rb +180 -0
  42. data/spec/fathom/node/cpm_node_spec.rb +144 -0
  43. data/spec/fathom/node/data_collection_spec.rb +26 -0
  44. data/spec/fathom/{data_node_spec.rb → node/data_node_spec.rb} +1 -1
  45. data/spec/fathom/node/decision_spec.rb +15 -0
  46. data/spec/fathom/node/discrete_node_spec.rb +56 -0
  47. data/spec/fathom/node/fact_spec.rb +33 -0
  48. data/spec/fathom/{mc_node_spec.rb → node/mc_node_spec.rb} +1 -1
  49. data/spec/fathom/{enforced_name_spec.rb → node/node_extensions/enforced_name_spec.rb} +1 -1
  50. data/spec/fathom/{numeric_methods_spec.rb → node/node_extensions/numeric_methods_spec.rb} +53 -11
  51. data/spec/fathom/{plausible_range_spec.rb → node/plausible_range_spec.rb} +1 -1
  52. data/spec/fathom/node_spec.rb +17 -0
  53. data/spec/fathom_spec.rb +40 -0
  54. data/spec/spec_helper.rb +3 -0
  55. data/spec/support/fact.yml +11 -0
  56. metadata +57 -30
  57. data/lib/fathom/value_multiplier.rb +0 -18
@@ -1,4 +1,4 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'fathom'))
2
2
 
3
3
  =begin
4
4
  A DataNode is a node generated from data itself. It stores the data and reveals some statistical
@@ -1,11 +1,11 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
2
- module Fathom
3
- class ValueAggregator < ValueDescription
4
- end
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'fathom'))
2
+ class Fathom::Decision < Node
3
+ undef_method :values
4
+ undef_method :distribution
5
5
  end
6
6
 
7
7
  if __FILE__ == $0
8
8
  include Fathom
9
9
  # TODO: Is there anything you want to do to run this file on its own?
10
- # ValueAggregator.new
10
+ # Decision.new
11
11
  end
@@ -0,0 +1,41 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'fathom'))
2
+ class Fathom::DiscreteNode < Node
3
+ attr_reader :labels
4
+
5
+ def initialize(opts={})
6
+ opts[:distribution] ||= :discrete_uniform
7
+ super(opts)
8
+ assert_labels(opts)
9
+ end
10
+
11
+ def size
12
+ @size ||= self.labels.length
13
+ end
14
+ alias :length :size
15
+
16
+ def rand
17
+ self.labels[self.distribution.rand(self.size)]
18
+ end
19
+
20
+ # This makes it easier to interface to a belief node
21
+ def likelihood(ignored_value)
22
+ GSL::Vector.ary_to_gv(Array.new(self.size, 1))
23
+ end
24
+ alias :l :likelihood
25
+
26
+ protected
27
+ def assert_labels(opts)
28
+ @labels = opts[:labels]
29
+ @labels ||= self.values
30
+ @labels ||= [:true, :false]
31
+ @labels = Array[@labels] unless @labels.is_a?(Array)
32
+ @labels.uniq!
33
+ end
34
+
35
+ end
36
+
37
+ if __FILE__ == $0
38
+ include Fathom
39
+ # TODO: Is there anything you want to do to run this file on its own?
40
+ # DiscreteNode.new
41
+ end
@@ -0,0 +1,24 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'fathom'))
2
+ class Fathom::Fact < Node
3
+
4
+ attr_reader :value
5
+
6
+ def initialize(opts={})
7
+ symbolize_keys!(opts)
8
+ @value = opts[:value]
9
+ @value ||= opts[:values]
10
+ super(opts)
11
+ end
12
+
13
+ alias :rand :value
14
+
15
+ undef_method :values
16
+ undef_method :distribution
17
+
18
+ end
19
+
20
+ if __FILE__ == $0
21
+ include Fathom
22
+ # TODO: Is there anything you want to do to run this file on its own?
23
+ # Fact.new
24
+ end
@@ -1,4 +1,4 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'fathom'))
2
2
  class Fathom::MCNode < Node
3
3
 
4
4
  attr_reader :value_description, :samples_taken
@@ -1,4 +1,4 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'fathom'))
2
2
  require 'uuid'
3
3
 
4
4
  module Fathom
@@ -1,4 +1,4 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'fathom'))
2
2
  module Fathom
3
3
  module NumericMethods
4
4
  def initialize(opts={})
@@ -46,5 +46,23 @@ module Fathom
46
46
  distribution.interval_values(opts.merge(:mean => mean, :sd => sd))
47
47
  end
48
48
 
49
+ def coefficient_of_variation
50
+ return nil unless vector
51
+ vector.sd / vector.mean
52
+ end
53
+ alias :cov :coefficient_of_variation
54
+
55
+ def summary
56
+ {
57
+ :mean => mean,
58
+ :standard_deviation => standard_deviation,
59
+ :min => vector ? vector.min : nil,
60
+ :max => vector ? vector.max : nil,
61
+ :lower_bound => lower_bound,
62
+ :upper_bound => upper_bound,
63
+ :coefficient_of_variation => coefficient_of_variation
64
+ }
65
+ end
66
+
49
67
  end
50
68
  end
@@ -1,4 +1,4 @@
1
- require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'fathom'))
2
2
  module Fathom
3
3
  class PlausibleRange < Node
4
4
 
@@ -0,0 +1,10 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Array do
4
+ it "should define rand, a way to get a random variable from an array" do
5
+ a = [1,2]
6
+ a.should be_include(a.rand)
7
+ # 9.31322574615479e-10 chance of failing
8
+ 30.times.map {a.rand}.uniq.sort.should eql([1,2])
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+ require 'fastercsv'
3
+ require 'ext/faster_csv'
4
+
5
+ describe FasterCSV do
6
+ it "should have a header converter to strip values" do
7
+ FasterCSV::HeaderConverters.keys.should be_include(:strip)
8
+ FasterCSV::HeaderConverters[:strip].call(' this ').should eql('this')
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe OpenStruct do
4
+
5
+ before do
6
+ @o = OpenStruct.new(:this => 1)
7
+ end
8
+
9
+ it "should expose its table" do
10
+ @o.table.should eql({:this => 1})
11
+ end
12
+
13
+ it "should expose the keys from the table" do
14
+ @o.keys.should eql([:this])
15
+ end
16
+
17
+ it "should expose the values from the table" do
18
+ @o.values.should eql([1])
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe String do
4
+ it "should be able to constantize a string" do
5
+ "Fathom".constantize.should eql(Fathom)
6
+ end
7
+ end
@@ -37,14 +37,16 @@ describe CSVImport do
37
37
  @result.that.values.should eql([3,6,9])
38
38
  end
39
39
 
40
- it "should store the imported values in the knowledge base" do
41
- Fathom.knowledge_base[:this].should be_a(DataNode)
42
- Fathom.kb[:this].values.should eql([1,4,7])
43
- end
40
+ # KB Override
41
+ # it "should store the imported values in the knowledge base" do
42
+ # Fathom.knowledge_base[:this].should be_a(DataNode)
43
+ # Fathom.kb[:this].values.should eql([1,4,7])
44
+ # end
44
45
 
45
- it "should import from the class level" do
46
- CSVImport.import(@opts)
47
- Fathom.knowledge_base[:this].should be_a(DataNode)
48
- Fathom.kb[:this].values.should eql([1,4,7])
49
- end
46
+ # KB Override
47
+ # it "should import from the class level" do
48
+ # CSVImport.import(@opts)
49
+ # Fathom.knowledge_base[:this].should be_a(DataNode)
50
+ # Fathom.kb[:this].values.should eql([1,4,7])
51
+ # end
50
52
  end
@@ -38,16 +38,36 @@ describe YAMLImport do
38
38
  @result.co2_readings.should be_a(DataNode)
39
39
  @result.co2_readings.values.should eql([10,20,30])
40
40
  end
41
+
42
+ # KB Override
43
+ # it "should store the imported values in the knowledge base" do
44
+ # Fathom.knowledge_base['CO2 Emissions'].should be_a(PlausibleRange)
45
+ # Fathom.kb['CO2 Emissions'].min.should eql(1_000_000)
46
+ # end
47
+
48
+ # KB Override
49
+ # it "should import from the class level" do
50
+ # YAMLImport.import(@opts)
51
+ # Fathom.knowledge_base['CO2 Emissions'].should be_a(PlausibleRange)
52
+ # Fathom.kb['CO2 Emissions'].min.should eql(1_000_000)
53
+ # end
41
54
 
42
- it "should store the imported values in the knowledge base" do
43
- Fathom.knowledge_base['CO2 Emissions'].should be_a(PlausibleRange)
44
- Fathom.kb['CO2 Emissions'].min.should eql(1_000_000)
55
+ it "should not complain if there's an empty YAML file" do
56
+ filename = '/tmp/empty_yaml_test.yml'
57
+ File.open(filename, 'w') {|f| f.puts ''}
58
+ lambda{YAMLImport.import(:content => filename)}.should_not raise_error
59
+ `rm -rf #{filename}`
45
60
  end
46
61
 
47
- it "should import from the class level" do
48
- YAMLImport.import(@opts)
49
- Fathom.knowledge_base['CO2 Emissions'].should be_a(PlausibleRange)
50
- Fathom.kb['CO2 Emissions'].min.should eql(1_000_000)
62
+ it "should create a Fact if it is an object named fact and has a name and value function" do
63
+ filename = File.expand_path(File.dirname(__FILE__) + "/../../support/fact.yml")
64
+ yaml = open(filename).read
65
+ opts = {:content => yaml}
66
+ yi = YAMLImport.new(opts)
67
+ result = yi.import
68
+ result.one.value.should eql(1)
69
+ result.two.value.should eql(2)
70
+ result.three.value.should eql(3)
51
71
  end
52
72
 
53
73
  end
@@ -8,9 +8,13 @@ describe KnowledgeBase do
8
8
  @kb = KnowledgeBase.new
9
9
  end
10
10
 
11
- it "should be able to add a node" do
12
- @dn = DataNode.new(:name => :new_node, :values => [1,2,3])
13
- @kb[:new_node] = @dn
14
- @kb[:new_node].should eql(@dn)
11
+ it "should have a find accessor" do
12
+ KnowledgeBase.should respond_to(:find)
15
13
  end
14
+
15
+ # it "should be able to add a node" do
16
+ # @dn = DataNode.new(:name => :new_node, :values => [1,2,3])
17
+ # @kb[:new_node] = @dn
18
+ # @kb[:new_node].should eql(@dn)
19
+ # end
16
20
  end
@@ -0,0 +1,180 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ include Fathom
4
+
5
+ describe BeliefNode do
6
+
7
+ before do
8
+ @bn = BeliefNode.new(:labels => [:red, :green, :blue])
9
+ end
10
+
11
+ it "should be a Node" do
12
+ BeliefNode.ancestors.should be_include(Node)
13
+ end
14
+
15
+ it "should be a DiscreteNode as well" do
16
+ BeliefNode.ancestors.should be_include(DiscreteNode)
17
+ end
18
+
19
+ it "should initialize with a uniform probability distribution" do
20
+ @bn.probabilities.should ==(GSL::Vector[1.0/3, 1.0/3, 1.0/3])
21
+ end
22
+
23
+ it "should allow a pre-defined probability distribution" do
24
+ @bn = BeliefNode.new(:labels => [:red, :green, :blue], :probabilities => [0.1, 0.2, 0.7])
25
+ @bn.probabilities.should ==(GSL::Vector[0.1, 0.2, 0.7])
26
+ end
27
+
28
+ it "should raise an error if the supplied probability distribution is of the wrong size" do
29
+ lambda{BeliefNode.new(:labels => [:red, :green, :blue], :probabilities => [0.1, 0.2, 0.7, 0.1])}.should raise_error(/Probabilities must be 3/)
30
+ lambda{BeliefNode.new(:labels => [:red, :green, :blue], :probabilities => [0.1, 0.2])}.should raise_error(/Probabilities must be 3/)
31
+ end
32
+
33
+ it "should normalize even the supplied probabilities" do
34
+ @bn = BeliefNode.new(:labels => [:red, :green, :blue], :probabilities => [1, 2, 7])
35
+ @bn.probabilities.should ==(GSL::Vector[0.1, 0.2, 0.7])
36
+ end
37
+
38
+ it "should initialize with a uniform likelihood vector" do
39
+ @bn.likelihoods.should ==(GSL::Vector[1,1,1])
40
+ end
41
+
42
+ it "should allow pre-defined likelihoods to be supplied" do
43
+ @bn = BeliefNode.new(:labels => [:red, :green, :blue], :likelihoods => [1,1,2])
44
+ @bn.likelihoods.should ==(GSL::Vector[1,1,2])
45
+ end
46
+
47
+ it "should raise an error if the supplied likelihoods are the wrong size" do
48
+ lambda{BeliefNode.new(:labels => [:red, :green, :blue], :likelihoods => [1,1,2,1])}.should raise_error(/Likelihoods must be 3/)
49
+ lambda{BeliefNode.new(:labels => [:red, :green, :blue], :likelihoods => [1,1])}.should raise_error(/Likelihoods must be 3/)
50
+ end
51
+
52
+ it "should establish a precision threshold at 1.0e-05" do
53
+ @bn.precision_threshold.should eql(0.00001)
54
+ end
55
+
56
+ it "should allow the precision_threshold as a parameter" do
57
+ @bn = BeliefNode.new(:labels => [:red, :green, :blue], :precision_threshold => 0.01)
58
+ @bn.precision_threshold.should eql(0.01)
59
+ end
60
+
61
+ it "should allow a initialization from a values hash" do
62
+ @bn = BeliefNode.new(:values => {:red => 0.1, :green => 0.2, :yellow => 0.3, :blue => 0.4})
63
+ @bn.probabilities.to_a.sort.should eql([0.1, 0.2, 0.3, 0.4])
64
+ @bn.labels.map(&:to_s).sort.should eql(%w(blue green red yellow))
65
+ end
66
+
67
+ context "CPMNodes" do
68
+ before do
69
+ @b1 = BeliefNode.new(:name => :person, :values => {:david => 0.4, :adina => 0.6})
70
+ @b2 = BeliefNode.new(:name => :movie_preference, :values => {:drama => 0.5, :comedy => 0.4, :romance => 0.1})
71
+ end
72
+
73
+ it "should automatically create an intermediary cpm when adding a child" do
74
+ @b1.add_child(@b2)
75
+ @b1.children.first.should be_a(CPMNode)
76
+ @b1.children.first.child.should eql(@b2)
77
+ @b2.parents.first.should be_a(CPMNode)
78
+ @b2.parents.first.parent.should eql(@b1)
79
+ @cpm = @b1.children.first
80
+ @cpm.should eql(@b2.parents.first)
81
+ end
82
+
83
+ it "should create an accessor for the child, rather than the cpm" do
84
+ @b1.add_child(@b2)
85
+ @b1.should_not be_respond_to(:cpm)
86
+ @b1.should respond_to(:movie_preference)
87
+ @b1.movie_preference.should eql(@b2)
88
+ end
89
+
90
+ it "should create a cpm_for_* accessor for the cpm" do
91
+ @b1.add_child(@b2)
92
+ @b1.should respond_to(:cpm_for_movie_preference)
93
+ @b1.cpm_for_movie_preference.should eql(@b1.children.first)
94
+ end
95
+
96
+ it "should create a cpm_for_* accessor for the child" do
97
+ @b1.add_child(@b2)
98
+ @b2.should respond_to(:cpm_for_person)
99
+ @b2.cpm_for_person.should eql(@b2.parents.first)
100
+ @b2.cpm_for_person.should eql(@b1.children.first)
101
+ end
102
+
103
+ it "should automatically create an intermediary cpm when adding a parent" do
104
+ @b2.add_parent(@b1)
105
+ @b1.children.first.should be_a(CPMNode)
106
+ @b1.children.first.child.should eql(@b2)
107
+ @b2.parents.first.should be_a(CPMNode)
108
+ @b2.parents.first.parent.should eql(@b1)
109
+ @cpm = @b1.children.first
110
+ @cpm.should eql(@b2.parents.first)
111
+ end
112
+
113
+ it "should create an accessor for the parent, rather than the cpm" do
114
+ @b2.add_parent(@b1)
115
+ @b2.should_not be_respond_to(:cpm)
116
+ @b2.should respond_to(:person)
117
+ @b2.person.should eql(@b1)
118
+ end
119
+
120
+ it "should create a cpm_for_* accessor for the cpm" do
121
+ @b2.add_parent(@b1)
122
+ @b2.should respond_to(:cpm_for_person)
123
+ @b2.cpm_for_person.should eql(@b2.parents.first)
124
+ end
125
+
126
+ it "should create an inspect string that reflects the grandchild, rather than the cpm" do
127
+ @b1.add_child(@b2)
128
+ @b1.inspect.should eql("Fathom::BeliefNode: person, children:, [\"movie_preference (Fathom::BeliefNode)\", \"movie_preference (Fathom::BeliefNode)\"], parents: , []")
129
+ end
130
+
131
+ it "should create an inspect string that reflects the grandparent, rather than the cpm" do
132
+ @b2.add_parent(@b1)
133
+ @b2.inspect.should eql("Fathom::BeliefNode: movie_preference, children:, [], parents: , [\"person (Fathom::BeliefNode)\", \"person (Fathom::BeliefNode)\"]")
134
+ end
135
+
136
+ it "should create a cpm_for_* accessor for the parent" do
137
+ @b2.add_parent(@b1)
138
+ @b1.should respond_to(:cpm_for_movie_preference)
139
+ @b1.cpm_for_movie_preference.should eql(@b1.children.first)
140
+ @b1.cpm_for_movie_preference.should eql(@b2.parents.first)
141
+ end
142
+
143
+ end
144
+
145
+ context "Belief and Propagation" do
146
+
147
+ before do
148
+
149
+ @season = BeliefNode.new(:name => :season, :labels => [:winter, :spring, :summer, :fall])
150
+ @sprinkler = BeliefNode.new(:name => :sprinkler)
151
+ @rain = BeliefNode.new(:name => :rain)
152
+ @wet = BeliefNode.new(:name => :wet)
153
+ @slippery = BeliefNode.new(:name => :slippery)
154
+
155
+ @season.add_child(@sprinkler)
156
+ @sprinkler.add_child(@wet)
157
+ @season.add_child(@rain)
158
+ @rain.add_child(@wet)
159
+ @wet.add_child(@slippery)
160
+
161
+ end
162
+
163
+ it "should provide the likelihood for a particular value" do
164
+ @sprinkler.should be_respond_to(:likelihood)
165
+ output = @sprinkler.likelihood(:true)
166
+ output.should be_a(OpenStruct)
167
+ # @sprinkler.parents.map(&:name_sym).each {|key| output.table.keys.should be_include(key)}
168
+ # output.
169
+ # output.table.keys.should eql([:matrix, :parents])
170
+ end
171
+ end
172
+
173
+ # What's next:
174
+ # Deal with multiple parents and the matrix that comes in
175
+ # Propagate the likelihoods and probabilities up and down
176
+ # Calculate the belief
177
+ # Update the information directly on the node and propagate
178
+ # Receive an update from a child or parent and propagate
179
+ # Adhere to the precision_threshold when propagating
180
+ end