fathom 0.3.0 → 0.3.1

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.
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