fathom 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,7 +1,11 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
2
+ require 'uuid'
3
+
2
4
  module Fathom
3
- module BasicNode
5
+ module EnforcedName
4
6
  def initialize(opts={})
7
+ super(opts)
8
+ @name ||= UUID.generate
5
9
  end
6
10
  end
7
11
  end
@@ -0,0 +1,27 @@
1
+ # From ActiveSupport
2
+ class String
3
+ if Module.method(:const_get).arity == 1
4
+ def constantize
5
+ names = self.split('::')
6
+ names.shift if names.empty? || names.first.empty?
7
+
8
+ constant = Object
9
+ names.each do |name|
10
+ constant = constant.const_defined?(name) ? constant.const_get(name) : constant.const_missing(name)
11
+ end
12
+ constant
13
+ end
14
+ else
15
+ def constantize #:nodoc:
16
+ names = self.split('::')
17
+ names.shift if names.empty? || names.first.empty?
18
+
19
+ constant = Object
20
+ names.each do |name|
21
+ constant = constant.const_defined?(name, false) ? constant.const_get(name) : constant.const_missing(name)
22
+ end
23
+ constant
24
+ end
25
+ end
26
+
27
+ end
@@ -1,7 +1,6 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
2
2
  module Fathom
3
- class Invertor
4
- include BasicNode
3
+ class Invertor < Node
5
4
 
6
5
  def initialize(opts={})
7
6
  super(opts)
@@ -54,19 +54,42 @@ class Fathom::MonteCarloSet
54
54
  raise "No fields are defined. Have you processed this model yet?" if fields.empty?
55
55
  raise ArgumentError, "#{field} is not a field in this set." unless fields.include?(field)
56
56
  vector = self.send(field)
57
+ return vector unless vector.is_a?(GSL::Vector)
57
58
  {
58
59
  :coefficient_of_variation => (vector.sd / vector.mean),
59
60
  :max => vector.max,
60
61
  :mean => vector.mean,
61
62
  :min => vector.min,
62
- :sd => vector.sd
63
+ :sd => vector.sd,
64
+ :upper_bound => upper_bound(:mean => vector.mean, :sd => vector.sd),
65
+ :lower_bound => lower_bound(:mean => vector.mean, :sd => vector.sd)
63
66
  }
64
67
  end
68
+
69
+ def inverse_cdf(opts={})
70
+ mean = opts[:mean]
71
+ sd = opts[:sd]
72
+ lower = opts.fetch(:lower, true)
73
+ confidence_interval = opts.fetch(:confidence_interval, 0.05)
74
+ value = lower ? GSL::Cdf.gaussian_Pinv(confidence_interval, sd) : GSL::Cdf.gaussian_Qinv(confidence_interval, sd)
75
+ value + mean
76
+ end
77
+ alias :lower_bound :inverse_cdf
78
+
79
+ def upper_bound(opts={})
80
+ inverse_cdf(opts.merge(:lower => false))
81
+ end
82
+
83
+ def interval_values(opts={})
84
+ confidence_interval = opts.fetch(:confidence_interval, 0.9)
85
+ bound = (1 - confidence_interval) / 2.0
86
+ [lower_bound(opts.merge(:confidence_interval, bound)), upper_bound(opts.merge(:confidence_interval, bound))]
87
+ end
65
88
 
66
89
  def assert_sample_vectors
67
90
  vectors = @samples.inject({}) do |h, o|
68
- key, array = o.first, o.last
69
- h[key] = GSL::Vector.ary_to_gv(array)
91
+ key, obj = o.first, o.last
92
+ h[key] = (obj.is_a?(Array) and obj.first.is_a?(Numeric)) ? GSL::Vector.ary_to_gv(obj) : obj
70
93
  h
71
94
  end
72
95
  @samples = vectors
@@ -0,0 +1,90 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
2
+ class Fathom::Node
3
+
4
+ attr_reader :name, :distribution, :description, :values
5
+
6
+ def initialize(opts={})
7
+ @name = opts[:name]
8
+ assert_distribution(opts)
9
+ @description = opts[:description]
10
+ @values = opts[:values]
11
+ assert_links(opts)
12
+ end
13
+
14
+ def name_sym
15
+ return nil unless self.name
16
+ @name_sym ||= self.name.to_s.downcase.gsub(/\s+|-+/, '_').to_sym
17
+ end
18
+
19
+ def parents
20
+ @parents ||= []
21
+ end
22
+
23
+ def add_parent(parent)
24
+ self.parents << parent
25
+ parent.register_child(self)
26
+ end
27
+
28
+ def register_child(child)
29
+ raise "Cannot register a child if this node is not a parent already. Use add_parent to the other node or add_child to this node." unless
30
+ child.parents.include?(self)
31
+ children << child unless children.include?(child)
32
+ true
33
+ end
34
+
35
+ def children
36
+ @children ||= []
37
+ end
38
+
39
+ def add_child(child)
40
+ self.children << child
41
+ child.register_parent(self)
42
+ end
43
+
44
+ def register_parent(parent)
45
+ raise "Cannot register a parent if this node is not a child already. Use add_child to the other node or add_parent to this node." unless
46
+ parent.children.include?(self)
47
+ parents << parent unless parents.include?(parent)
48
+ true
49
+ end
50
+
51
+ protected
52
+
53
+ def assert_links(opts)
54
+ found = opts[:parents]
55
+ found ||= opts[:parent]
56
+ found ||= []
57
+ found = [found] unless found.is_a?(Array)
58
+ found.each do |parent|
59
+ add_parent(parent)
60
+ end
61
+
62
+
63
+ found = opts[:children]
64
+ found ||= opts[:child]
65
+ found ||= []
66
+ found = [found] unless found.is_a?(Array)
67
+ found.each do |child|
68
+ add_child(child)
69
+ end
70
+ end
71
+
72
+ def assert_distribution(opts)
73
+ case opts[:distribution]
74
+ when Class
75
+ @distribution = opts[:distribution]
76
+ when Symbol
77
+ class_name = opts[:distribution].to_s.downcase.split(/_+/).map {|t| t[0].chr.upcase + t[1..-1]}.join
78
+ if Fathom::Distributions.constants.include?(class_name)
79
+ @distribution = "Fathom::Distributions::#{class_name}".constantize
80
+ end
81
+ end
82
+ @distribution ||= Fathom::Distributions::Gaussian
83
+ end
84
+ end
85
+
86
+ if __FILE__ == $0
87
+ include Fathom
88
+ # TODO: Is there anything you want to do to run this file on its own?
89
+ # Node.new
90
+ end
@@ -0,0 +1,50 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
2
+ module Fathom
3
+ module NumericMethods
4
+ def initialize(opts={})
5
+ super(opts)
6
+ end
7
+
8
+ def rand
9
+ return nil unless vector
10
+ distribution.rand(sd) + mean
11
+ end
12
+
13
+ def vector
14
+ return @vector if @vector
15
+ return nil unless values
16
+ case values
17
+ when Array
18
+ @vector = GSL::Vector.ary_to_gv(values)
19
+ when GSL::Vector
20
+ @vector = values
21
+ end
22
+ end
23
+
24
+ def standard_deviation
25
+ return nil unless vector
26
+ vector.sd
27
+ end
28
+ alias :std :standard_deviation
29
+ alias :sd :standard_deviation
30
+
31
+ def mean
32
+ return nil unless vector
33
+ vector.mean
34
+ end
35
+
36
+ def inverse_cdf(opts={})
37
+ distribution.inverse_cdf(opts.merge(:mean => mean, :sd => sd))
38
+ end
39
+ alias :lower_bound :inverse_cdf
40
+
41
+ def upper_bound(opts={})
42
+ distribution.upper_bound(opts.merge(:mean => mean, :sd => sd))
43
+ end
44
+
45
+ def interval_values(opts={})
46
+ distribution.interval_values(opts.merge(:mean => mean, :sd => sd))
47
+ end
48
+
49
+ end
50
+ end
@@ -1,12 +1,13 @@
1
1
  require File.expand_path(File.join(File.dirname(__FILE__), '..', 'fathom'))
2
2
  module Fathom
3
- class PlausibleRange
3
+ class PlausibleRange < Node
4
4
 
5
- include NodeUtilities
5
+ include NumericMethods
6
6
 
7
- attr_reader :upper_bound, :lower_bound, :confidence_interval, :name, :description, :hard_lower_bound, :hard_upper_bound
7
+ attr_reader :upper_bound, :lower_bound, :confidence_interval, :hard_lower_bound, :hard_upper_bound
8
8
 
9
9
  def initialize(opts={})
10
+ super(opts)
10
11
 
11
12
  opts = OptionsHash.new(opts)
12
13
 
@@ -28,9 +29,6 @@ module Fathom
28
29
  @confidence_interval ||= opts[:ci]
29
30
  @confidence_interval ||= 0.9
30
31
 
31
- @name = opts[:name]
32
- @description = opts[:description]
33
-
34
32
  end
35
33
 
36
34
  alias :min :lower_bound
@@ -40,21 +38,22 @@ module Fathom
40
38
  def midpoint
41
39
  @midpoint ||= lower_bound + (range / 2.0)
42
40
  end
41
+ alias :mean :midpoint
43
42
 
44
43
  def range
45
44
  @range ||= upper_bound - lower_bound
46
45
  end
47
46
 
48
47
  def standard_deviation
49
- @standard_deviation ||= range / 3.29
48
+ @standard_deviation ||= range / distribution.standard_deviations_under(confidence_interval)
50
49
  end
51
50
  alias :std :standard_deviation
51
+ alias :sd :standard_deviation
52
52
 
53
- # Not using specific distributions yet
54
53
  def rand
55
54
  value = get_rand
56
55
  until is_bounded?(value) do
57
- value= get_rand
56
+ value = get_rand
58
57
  end
59
58
  value
60
59
  end
@@ -82,17 +81,14 @@ module Fathom
82
81
  end
83
82
  end
84
83
 
84
+ # Uses the distribution system, but doesn't use a vector to determine the sd, mean, etc.
85
+ # So keeping this part in-house.
85
86
  def get_rand
86
- rng.gaussian(std) + midpoint
87
+ distribution.rand(sd) + mean
87
88
  end
88
89
 
89
- def rng
90
- @rng ||= GSL::Rng.alloc(GSL::Rng::MT19937_1999, Kernel.rand(100_000))
91
- end
92
90
  end
93
91
 
94
- class R < PlausibleRange
95
- end
96
92
  end
97
93
 
98
94
  if __FILE__ == $0
@@ -25,11 +25,9 @@ describe DataNode do
25
25
  @dn.name.should eql("Demo Name")
26
26
  end
27
27
 
28
- # Note, the distributions aren't defined here yet, so this will eventually be
29
- # Some sort of Fathom::Distribution::Constant eventually.
30
28
  it "should take an optional distribiution" do
31
- @dn = DataNode.new(@opts.merge(:distribution => :some_distribution))
32
- @dn.distribution.should eql(:some_distribution)
29
+ @dn = DataNode.new(@opts.merge(:distribution => :gaussian))
30
+ @dn.distribution.should eql(Fathom::Distributions::Gaussian)
33
31
  end
34
32
 
35
33
  it "should create a vector from the values" do
@@ -58,4 +56,47 @@ describe DataNode do
58
56
  dn.name_sym.should eql(:demo_node)
59
57
  end
60
58
 
59
+ it "should offer the lower bounds at a default confidence level of 0.05" do
60
+ GSL::Cdf.should_receive(:gaussian_Pinv).with(0.05, @dn.vector.sd).and_return(0.0)
61
+ @dn.inverse_cdf
62
+ end
63
+
64
+ it "should offer the lower bounds at an arbitrary confidence level" do
65
+ GSL::Cdf.should_receive(:gaussian_Pinv).with(0.95, @dn.vector.sd).and_return(0.0)
66
+ @dn.inverse_cdf(:confidence_interval => 0.95)
67
+ end
68
+
69
+ it "should be able to calculate the upper bound by passing upper as a parameter" do
70
+ GSL::Cdf.should_receive(:gaussian_Qinv).with(0.05, @dn.vector.sd).and_return(0.0)
71
+ @dn.inverse_cdf(:confidence_interval => 0.05, :upper => true)
72
+ end
73
+
74
+ it "should be able to calculate a lower_bound, an alias for inverse_cdf" do
75
+ GSL::Cdf.should_receive(:gaussian_Pinv).with(0.05, @dn.vector.sd).and_return(0.0)
76
+ @dn.lower_bound
77
+ end
78
+
79
+ it "should be able to pass arguments to lower_bound" do
80
+ GSL::Cdf.should_receive(:gaussian_Pinv).with(0.1, @dn.vector.sd).and_return(0.0)
81
+ @dn.lower_bound(:confidence_interval => 0.1)
82
+ end
83
+
84
+ it "should be able to calculate an upper_bound, a shortcut for inverse_cdf(confidence_interval, false)" do
85
+ GSL::Cdf.should_receive(:gaussian_Qinv).with(0.05, @dn.vector.sd).and_return(0.0)
86
+ @dn.upper_bound
87
+ end
88
+
89
+ it "should be able to pass arguments to upper_bound" do
90
+ GSL::Cdf.should_receive(:gaussian_Qinv).with(0.1, @dn.vector.sd).and_return(0.0)
91
+ @dn.upper_bound(:confidence_interval => 0.1)
92
+ end
93
+
94
+ it "should offset the cdf results by the mean" do
95
+ @dn.lower_bound.should eql(GSL::Cdf.gaussian_Pinv(0.05, @dn.vector.sd) + @dn.vector.mean)
96
+ end
97
+
98
+ it "should return the lower and upper bounds of a confidence interval" do
99
+ @dn.interval_values.should eql([@dn.lower_bound, @dn.upper_bound])
100
+ end
101
+
61
102
  end
@@ -0,0 +1,64 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ include Fathom::Distributions
4
+
5
+ describe DiscreteGaussian do
6
+
7
+ # before do
8
+ # @opts = {:mean => 0, :sd => 0.1, :confidence_interval => 0.05}
9
+ # end
10
+ #
11
+ # it "should provide a GSL::Rng through rng" do
12
+ # Gaussian.rng.should be_a(GSL::Rng)
13
+ # end
14
+ #
15
+ # it "should be able to generate a random variable through GSL::Rng#gaussian" do
16
+ # Gaussian.rng.should_receive(:gaussian).and_return(0.5)
17
+ # Gaussian.rand(1)
18
+ # end
19
+ #
20
+ # it "should be able to generate an inverse CDF" do
21
+ # Gaussian.inverse_cdf(@opts).should be_close(-0.16448, 0.00001)
22
+ # end
23
+ #
24
+ # it "should require mean as an option for an inverse CDF" do
25
+ # @opts.delete(:mean)
26
+ # lambda{Gaussian.inverse_cdf(@opts)}.should raise_error
27
+ # end
28
+ #
29
+ # it "should require a standard deviation for an inverse CDF" do
30
+ # @opts.delete(:sd)
31
+ # lambda{Gaussian.inverse_cdf(@opts)}.should raise_error
32
+ # end
33
+ #
34
+ # it "should take std as an alias for sd when creating an inverse CDF" do
35
+ # @opts.delete(:sd)
36
+ # Gaussian.inverse_cdf(@opts.merge(:std => 0.1)).should be_close(-0.16448, 0.00001)
37
+ # end
38
+ #
39
+ # it "should take standard_deviation as an alias for sd when creating an inverse CDF" do
40
+ # @opts.delete(:sd)
41
+ # Gaussian.inverse_cdf(@opts.merge(:standard_deviation => 0.1)).should be_close(-0.16448, 0.00001)
42
+ # end
43
+ #
44
+ # it "should be able to set upper to true and get Q instead of P" do
45
+ # Gaussian.inverse_cdf(@opts.merge(:upper =>true)).should be_close(0.16448, 0.00001)
46
+ # end
47
+ #
48
+ # it "should be able to take a different confidence interval" do
49
+ # Gaussian.inverse_cdf(@opts.merge(:confidence_interval => 0.1)).should be_close(-0.12815, 0.00001)
50
+ # end
51
+ #
52
+ # it "should have a lower_bound alias for inverse_cdf" do
53
+ # Gaussian.lower_bound(@opts).should eql(Gaussian.inverse_cdf(@opts))
54
+ # end
55
+ #
56
+ # it "should have an upper_bound shortcut for inverse_cdf(:upper => true, ...)" do
57
+ # Gaussian.upper_bound(@opts).should eql(Gaussian.inverse_cdf(@opts.merge(:upper => true)))
58
+ # end
59
+ #
60
+ # it "should provide interval values, an array of the lower and upper bounds" do
61
+ # Gaussian.interval_values(@opts.merge(:confidence_interval => 0.9)).should eql([Gaussian.lower_bound(@opts), Gaussian.upper_bound(@opts)])
62
+ # end
63
+ end
64
+
@@ -0,0 +1,64 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')
2
+
3
+ include Fathom::Distributions
4
+
5
+ describe Gaussian do
6
+
7
+ before do
8
+ @opts = {:mean => 0, :sd => 0.1, :confidence_interval => 0.05}
9
+ end
10
+
11
+ it "should provide a GSL::Rng through rng" do
12
+ Gaussian.rng.should be_a(GSL::Rng)
13
+ end
14
+
15
+ it "should be able to generate a random variable through GSL::Rng#gaussian" do
16
+ Gaussian.rng.should_receive(:gaussian).and_return(0.5)
17
+ Gaussian.rand(1)
18
+ end
19
+
20
+ it "should be able to generate an inverse CDF" do
21
+ Gaussian.inverse_cdf(@opts).should be_close(-0.16448, 0.00001)
22
+ end
23
+
24
+ it "should require mean as an option for an inverse CDF" do
25
+ @opts.delete(:mean)
26
+ lambda{Gaussian.inverse_cdf(@opts)}.should raise_error
27
+ end
28
+
29
+ it "should require a Gaussian deviation for an inverse CDF" do
30
+ @opts.delete(:sd)
31
+ lambda{Gaussian.inverse_cdf(@opts)}.should raise_error
32
+ end
33
+
34
+ it "should take std as an alias for sd when creating an inverse CDF" do
35
+ @opts.delete(:sd)
36
+ Gaussian.inverse_cdf(@opts.merge(:std => 0.1)).should be_close(-0.16448, 0.00001)
37
+ end
38
+
39
+ it "should take standard_deviation as an alias for sd when creating an inverse CDF" do
40
+ @opts.delete(:sd)
41
+ Gaussian.inverse_cdf(@opts.merge(:standard_deviation => 0.1)).should be_close(-0.16448, 0.00001)
42
+ end
43
+
44
+ it "should be able to set upper to true and get Q instead of P" do
45
+ Gaussian.inverse_cdf(@opts.merge(:upper =>true)).should be_close(0.16448, 0.00001)
46
+ end
47
+
48
+ it "should be able to take a different confidence interval" do
49
+ Gaussian.inverse_cdf(@opts.merge(:confidence_interval => 0.1)).should be_close(-0.12815, 0.00001)
50
+ end
51
+
52
+ it "should have a lower_bound alias for inverse_cdf" do
53
+ Gaussian.lower_bound(@opts).should eql(Gaussian.inverse_cdf(@opts))
54
+ end
55
+
56
+ it "should have an upper_bound shortcut for inverse_cdf(:upper => true, ...)" do
57
+ Gaussian.upper_bound(@opts).should eql(Gaussian.inverse_cdf(@opts.merge(:upper => true)))
58
+ end
59
+
60
+ it "should provide interval values, an array of the lower and upper bounds" do
61
+ Gaussian.interval_values(@opts.merge(:confidence_interval => 0.9)).should eql([Gaussian.lower_bound(@opts), Gaussian.upper_bound(@opts)])
62
+ end
63
+ end
64
+