fathom 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.bundle/config +2 -0
- data/.document +5 -0
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +20 -0
- data/README.md +176 -0
- data/Rakefile +50 -0
- data/VERSION +1 -0
- data/autotest/discover.rb +1 -0
- data/lib/fathom.rb +68 -0
- data/lib/fathom/archive/conditional_probability_matrix.rb +116 -0
- data/lib/fathom/archive/n2.rb +198 -0
- data/lib/fathom/archive/n3.rb +119 -0
- data/lib/fathom/archive/node.rb +74 -0
- data/lib/fathom/archive/noodle.rb +136 -0
- data/lib/fathom/archive/scratch.rb +45 -0
- data/lib/fathom/basic_node.rb +8 -0
- data/lib/fathom/causal_graph.rb +12 -0
- data/lib/fathom/combined_plausibilities.rb +12 -0
- data/lib/fathom/concept.rb +83 -0
- data/lib/fathom/data_node.rb +51 -0
- data/lib/fathom/import.rb +68 -0
- data/lib/fathom/import/csv_import.rb +60 -0
- data/lib/fathom/import/yaml_import.rb +53 -0
- data/lib/fathom/inverter.rb +21 -0
- data/lib/fathom/knowledge_base.rb +23 -0
- data/lib/fathom/monte_carlo_set.rb +76 -0
- data/lib/fathom/node_utilities.rb +8 -0
- data/lib/fathom/plausible_range.rb +82 -0
- data/lib/fathom/value_aggregator.rb +11 -0
- data/lib/fathom/value_description.rb +79 -0
- data/lib/fathom/value_multiplier.rb +18 -0
- data/lib/options_hash.rb +186 -0
- data/spec/fathom/data_node_spec.rb +61 -0
- data/spec/fathom/import/csv_import_spec.rb +36 -0
- data/spec/fathom/import/yaml_import_spec.rb +40 -0
- data/spec/fathom/import_spec.rb +22 -0
- data/spec/fathom/knowledge_base_spec.rb +16 -0
- data/spec/fathom/monte_carlo_set_spec.rb +58 -0
- data/spec/fathom/plausible_range_spec.rb +130 -0
- data/spec/fathom/value_description_spec.rb +70 -0
- data/spec/fathom_spec.rb +8 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/support/demo.yml +17 -0
- metadata +135 -0
@@ -0,0 +1,76 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
|
2
|
+
class Fathom::MonteCarloSet
|
3
|
+
|
4
|
+
class << self
|
5
|
+
def define_key(key)
|
6
|
+
define_method(key.to_sym) do
|
7
|
+
self.samples[key.to_sym]
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :value_description, :samples_taken, :samples
|
13
|
+
|
14
|
+
def initialize(value_description)
|
15
|
+
@value_description = value_description
|
16
|
+
@samples = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def process(n=10_000)
|
20
|
+
@samples_taken = n
|
21
|
+
@samples_taken.times do
|
22
|
+
result = value_description.process
|
23
|
+
store(result)
|
24
|
+
end
|
25
|
+
assert_sample_vectors
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset!
|
29
|
+
@samples = {}
|
30
|
+
@keys_asserted = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def assert_sample_vectors
|
36
|
+
vectors = @samples.inject({}) do |h, o|
|
37
|
+
key, array = o.first, o.last
|
38
|
+
h[key] = GSL::Vector.ary_to_gv(array)
|
39
|
+
h
|
40
|
+
end
|
41
|
+
@samples = vectors
|
42
|
+
end
|
43
|
+
|
44
|
+
def store(result)
|
45
|
+
result = assert_result_hash(result)
|
46
|
+
assert_keys(result)
|
47
|
+
result.each do |key, value|
|
48
|
+
@samples[key.to_sym] << value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def assert_result_hash(result)
|
53
|
+
result.is_a?(Hash) ? result : {:result => result}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Assumes the same value description for all samples taken
|
57
|
+
def assert_keys(result)
|
58
|
+
return true if @keys_asserted
|
59
|
+
result.keys.each do |key|
|
60
|
+
assert_key(key)
|
61
|
+
end
|
62
|
+
@keys_asserted = true
|
63
|
+
end
|
64
|
+
|
65
|
+
def assert_key(key)
|
66
|
+
self.class.define_key(key)
|
67
|
+
@samples[key.to_sym] ||= []
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
|
72
|
+
if __FILE__ == $0
|
73
|
+
include Fathom
|
74
|
+
# TODO: Is there anything you want to do to run this file on its own?
|
75
|
+
# MonteCarloSet.new
|
76
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
|
2
|
+
module Fathom
|
3
|
+
class PlausibleRange
|
4
|
+
|
5
|
+
include NodeUtilities
|
6
|
+
|
7
|
+
attr_reader :upper_bound, :lower_bound, :confidence_interval, :name, :description, :hard_lower_bound, :hard_upper_bound
|
8
|
+
|
9
|
+
def initialize(opts={})
|
10
|
+
|
11
|
+
opts = OptionsHash.new(opts)
|
12
|
+
|
13
|
+
@hard_upper_bound = opts[:hard_upper_bound]
|
14
|
+
@upper_bound = opts[:upper_bound]
|
15
|
+
@upper_bound ||= opts[:max]
|
16
|
+
@upper_bound ||= @hard_upper_bound
|
17
|
+
@upper_bound = @hard_upper_bound if @upper_bound and @hard_upper_bound and @hard_upper_bound < @upper_bound
|
18
|
+
raise ArgumentError, "Must provide an upper bound." unless @upper_bound
|
19
|
+
|
20
|
+
@hard_lower_bound = opts[:hard_lower_bound]
|
21
|
+
@lower_bound = opts[:lower_bound]
|
22
|
+
@lower_bound ||= opts[:min]
|
23
|
+
@lower_bound ||= @hard_lower_bound
|
24
|
+
@lower_bound = @hard_lower_bound if @lower_bound and @hard_lower_bound and @hard_lower_bound > @lower_bound
|
25
|
+
raise ArgumentError, "Must provide a lower bound." unless @lower_bound
|
26
|
+
|
27
|
+
@confidence_interval = opts[:confidence_interval]
|
28
|
+
@confidence_interval ||= opts[:ci]
|
29
|
+
@confidence_interval ||= 0.9
|
30
|
+
|
31
|
+
@name = opts[:name]
|
32
|
+
@description = opts[:description]
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
alias :min :lower_bound
|
37
|
+
alias :max :upper_bound
|
38
|
+
alias :ci :confidence_interval
|
39
|
+
|
40
|
+
def midpoint
|
41
|
+
@midpoint ||= lower_bound + (range / 2.0)
|
42
|
+
end
|
43
|
+
|
44
|
+
def range
|
45
|
+
@range ||= upper_bound - lower_bound
|
46
|
+
end
|
47
|
+
|
48
|
+
def standard_deviation
|
49
|
+
@standard_deviation ||= range / 3.29
|
50
|
+
end
|
51
|
+
alias :std :standard_deviation
|
52
|
+
|
53
|
+
# Not using specific distributions yet
|
54
|
+
def rand
|
55
|
+
rng.gaussian(std) + midpoint
|
56
|
+
end
|
57
|
+
|
58
|
+
def array_of_random_values(n=10)
|
59
|
+
n.times.map {self.rand}
|
60
|
+
end
|
61
|
+
alias :to_a :array_of_random_values
|
62
|
+
|
63
|
+
def vector_of_random_values(n=10)
|
64
|
+
GSL::Vector.alloc(array_of_random_values(n))
|
65
|
+
end
|
66
|
+
alias :to_v :vector_of_random_values
|
67
|
+
|
68
|
+
protected
|
69
|
+
def rng
|
70
|
+
@rng ||= GSL::Rng.alloc
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class R < PlausibleRange
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
if __FILE__ == $0
|
79
|
+
include Fathom
|
80
|
+
# TODO: Is there anything you want to do to run this file on its own?
|
81
|
+
# PlausibleRange.new
|
82
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
|
2
|
+
module Fathom
|
3
|
+
class ValueAggregator < ValueDescription
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
if __FILE__ == $0
|
8
|
+
include Fathom
|
9
|
+
# TODO: Is there anything you want to do to run this file on its own?
|
10
|
+
# ValueAggregator.new
|
11
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
|
2
|
+
module Fathom
|
3
|
+
class ValueDescription
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# Returns a simple accessor for the node, so that the value description works a little like an OpenStruct.
|
7
|
+
def define_node_accessor(node, method = :node)
|
8
|
+
define_method(node.name_sym) do
|
9
|
+
node.send(method)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :nodes, :last_process
|
16
|
+
|
17
|
+
def initialize(*nodes, &block)
|
18
|
+
assert_nodes(nodes)
|
19
|
+
@process_block = block if block_given?
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_node(node, method = :rand)
|
23
|
+
assert_node(node, method)
|
24
|
+
end
|
25
|
+
|
26
|
+
def process
|
27
|
+
prepare_process
|
28
|
+
@process_block ? @process_block.call(@last_process) : default_process(@last_process)
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
|
34
|
+
def prepare_process
|
35
|
+
@last_process = self.nodes.inject(OpenStruct.new) do |o, node|
|
36
|
+
o.table[node.name_sym] = self.send(node.name_sym)
|
37
|
+
o
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def default_process(obj)
|
42
|
+
obj.values.inject(0.0) do |s, e|
|
43
|
+
s += e
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def assert_nodes(nodes)
|
48
|
+
@nodes ||= []
|
49
|
+
nodes.each do |node|
|
50
|
+
case node
|
51
|
+
when Hash
|
52
|
+
assert_node_from_hash(node)
|
53
|
+
else
|
54
|
+
assert_node(node)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def assert_node(node, method = :rand)
|
60
|
+
raise ArgumentError, "Must provide a node that can respond to name" unless node.respond_to?(:name)
|
61
|
+
@nodes ||= []
|
62
|
+
self.class.define_node_accessor(node, method)
|
63
|
+
@nodes << node
|
64
|
+
end
|
65
|
+
|
66
|
+
# Takes a node => :value_hash signature for the values in the hash
|
67
|
+
def assert_node_from_hash(hash)
|
68
|
+
hash.each do |node, value_method|
|
69
|
+
assert_node(node, value_method)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if __FILE__ == $0
|
76
|
+
include Fathom
|
77
|
+
# TODO: Is there anything you want to do to run this file on its own?
|
78
|
+
# ValueDescription.new
|
79
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
|
2
|
+
module Fathom
|
3
|
+
class ValueMultiplier < ValueDescription
|
4
|
+
|
5
|
+
def default_process
|
6
|
+
obj.values.inject(0.0) do |s, e|
|
7
|
+
s *= e
|
8
|
+
end
|
9
|
+
end
|
10
|
+
protected :default_process
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
if __FILE__ == $0
|
15
|
+
include Fathom
|
16
|
+
# TODO: Is there anything you want to do to run this file on its own?
|
17
|
+
# ValueMultiplier.new
|
18
|
+
end
|
data/lib/options_hash.rb
ADDED
@@ -0,0 +1,186 @@
|
|
1
|
+
# Taken brazenly from ActiveSupport
|
2
|
+
class OptionsHash < Hash
|
3
|
+
# Return a new hash with all keys converted to strings.
|
4
|
+
def stringify_keys
|
5
|
+
dup.stringify_keys!
|
6
|
+
end
|
7
|
+
|
8
|
+
# Destructively convert all keys to strings.
|
9
|
+
def stringify_keys!
|
10
|
+
keys.each do |key|
|
11
|
+
self[key.to_s] = delete(key)
|
12
|
+
end
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
# Return a new hash with all keys converted to symbols, as long as
|
17
|
+
# they respond to +to_sym+.
|
18
|
+
def symbolize_keys
|
19
|
+
dup.symbolize_keys!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Destructively convert all keys to symbols, as long as they respond
|
23
|
+
# to +to_sym+.
|
24
|
+
def symbolize_keys!
|
25
|
+
keys.each do |key|
|
26
|
+
self[(key.to_sym rescue key) || key] = delete(key)
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
alias_method :to_options, :symbolize_keys
|
32
|
+
alias_method :to_options!, :symbolize_keys!
|
33
|
+
|
34
|
+
# Validate all keys in a hash match *valid keys, raising ArgumentError on a mismatch.
|
35
|
+
# Note that keys are NOT treated indifferently, meaning if you use strings for keys but assert symbols
|
36
|
+
# as keys, this will fail.
|
37
|
+
#
|
38
|
+
# ==== Examples
|
39
|
+
# { :name => "Rob", :years => "28" }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key(s): years"
|
40
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys("name", "age") # => raises "ArgumentError: Unknown key(s): name, age"
|
41
|
+
# { :name => "Rob", :age => "28" }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
42
|
+
def assert_valid_keys(*valid_keys)
|
43
|
+
unknown_keys = keys - [valid_keys].flatten
|
44
|
+
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
def extractable_options?
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def initialize(constructor = {})
|
52
|
+
if constructor.is_a?(Hash)
|
53
|
+
super()
|
54
|
+
update(constructor)
|
55
|
+
else
|
56
|
+
super(constructor)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def default(key = nil)
|
61
|
+
if key.is_a?(Symbol) && include?(key = key.to_s)
|
62
|
+
self[key]
|
63
|
+
else
|
64
|
+
super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.new_from_hash_copying_default(hash)
|
69
|
+
OptionsHash.new(hash).tap do |new_hash|
|
70
|
+
new_hash.default = hash.default
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
75
|
+
alias_method :regular_update, :update unless method_defined?(:regular_update)
|
76
|
+
|
77
|
+
# Assigns a new value to the hash:
|
78
|
+
#
|
79
|
+
# hash = OptionsHash.new
|
80
|
+
# hash[:key] = "value"
|
81
|
+
#
|
82
|
+
def []=(key, value)
|
83
|
+
regular_writer(convert_key(key), convert_value(value))
|
84
|
+
end
|
85
|
+
|
86
|
+
# Updates the instantized hash with values from the second:
|
87
|
+
#
|
88
|
+
# hash_1 = OptionsHash.new
|
89
|
+
# hash_1[:key] = "value"
|
90
|
+
#
|
91
|
+
# hash_2 = OptionsHash.new
|
92
|
+
# hash_2[:key] = "New Value!"
|
93
|
+
#
|
94
|
+
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
|
95
|
+
#
|
96
|
+
def update(other_hash)
|
97
|
+
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
alias_method :merge!, :update
|
102
|
+
|
103
|
+
# Checks the hash for a key matching the argument passed in:
|
104
|
+
#
|
105
|
+
# hash = OptionsHash.new
|
106
|
+
# hash["key"] = "value"
|
107
|
+
# hash.key? :key # => true
|
108
|
+
# hash.key? "key" # => true
|
109
|
+
#
|
110
|
+
def key?(key)
|
111
|
+
super(convert_key(key))
|
112
|
+
end
|
113
|
+
|
114
|
+
alias_method :include?, :key?
|
115
|
+
alias_method :has_key?, :key?
|
116
|
+
alias_method :member?, :key?
|
117
|
+
|
118
|
+
# Fetches the value for the specified key, same as doing hash[key]
|
119
|
+
def fetch(key, *extras)
|
120
|
+
super(convert_key(key), *extras)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Returns an array of the values at the specified indices:
|
124
|
+
#
|
125
|
+
# hash = OptionsHash.new
|
126
|
+
# hash[:a] = "x"
|
127
|
+
# hash[:b] = "y"
|
128
|
+
# hash.values_at("a", "b") # => ["x", "y"]
|
129
|
+
#
|
130
|
+
def values_at(*indices)
|
131
|
+
indices.collect {|key| self[convert_key(key)]}
|
132
|
+
end
|
133
|
+
|
134
|
+
# Returns an exact copy of the hash.
|
135
|
+
def dup
|
136
|
+
OptionsHash.new(self)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Merges the instantized and the specified hashes together, giving precedence to the values from the second hash
|
140
|
+
# Does not overwrite the existing hash.
|
141
|
+
def merge(hash)
|
142
|
+
self.dup.update(hash)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Performs the opposite of merge, with the keys and values from the first hash taking precedence over the second.
|
146
|
+
# This overloaded definition prevents returning a regular hash, if reverse_merge is called on a OptionsHash.
|
147
|
+
def reverse_merge(other_hash)
|
148
|
+
super self.class.new_from_hash_copying_default(other_hash)
|
149
|
+
end
|
150
|
+
|
151
|
+
def reverse_merge!(other_hash)
|
152
|
+
replace(reverse_merge( other_hash ))
|
153
|
+
end
|
154
|
+
|
155
|
+
# Removes a specified key from the hash.
|
156
|
+
def delete(key)
|
157
|
+
super(convert_key(key))
|
158
|
+
end
|
159
|
+
|
160
|
+
def stringify_keys!; self end
|
161
|
+
def stringify_keys; dup end
|
162
|
+
undef :symbolize_keys!
|
163
|
+
def symbolize_keys; to_hash.symbolize_keys end
|
164
|
+
def to_options!; self end
|
165
|
+
|
166
|
+
# Convert to a Hash with String keys.
|
167
|
+
def to_hash
|
168
|
+
Hash.new(default).merge!(self)
|
169
|
+
end
|
170
|
+
|
171
|
+
protected
|
172
|
+
def convert_key(key)
|
173
|
+
key.kind_of?(Symbol) ? key.to_s : key
|
174
|
+
end
|
175
|
+
|
176
|
+
def convert_value(value)
|
177
|
+
case value
|
178
|
+
when Hash
|
179
|
+
self.class.new_from_hash_copying_default(value)
|
180
|
+
when Array
|
181
|
+
value.collect { |e| e.is_a?(Hash) ? self.class.new_from_hash_copying_default(e) : e }
|
182
|
+
else
|
183
|
+
value
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|