fathom 0.2.2 → 0.2.3

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