bullshit 0.1.1 → 0.1.2

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.
@@ -0,0 +1,3 @@
1
+ pkg
2
+ Gemfile.lock
3
+ .*.sw[pon]
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 1.8.7
3
+ - 1.9.2
4
+ - ruby-head
5
+ - ree
6
+ - rbx
7
+ - jruby
data/CHANGES CHANGED
@@ -1,3 +1,6 @@
1
+ 2011-07-14 (0.1.2)
2
+ * Extract mathemacial computations into its own gem.
3
+ * Use gem_hadar to shorten Rakefile
1
4
  2009-10-17 (0.1.1)
2
5
  * Added the display of a speedup matrix in the comparison.
3
6
  * Added a bs_compare executble in order to compare data files.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # vim: set filetype=ruby et sw=2 ts=2:
2
+
3
+ source :rubygems
4
+
5
+ gemspec
File without changes
data/Rakefile CHANGED
@@ -1,85 +1,33 @@
1
- begin
2
- require 'rake/gempackagetask'
3
- rescue LoadError
4
- end
5
- require 'rake/clean'
6
- require 'rbconfig'
7
- include Config
8
-
9
- PKG_NAME = 'bullshit'
10
- PKG_VERSION = File.read('VERSION').chomp
11
- PKG_FILES = FileList['**/*'].exclude(/^(doc|CVS|pkg|coverage)/)
12
- CLEAN.include 'coverage', 'doc'
13
- CLOBBER.include FileList['data/*']
14
-
15
- desc "Run unit tests"
16
- task :test do
17
- sh %{RUBYOPT="-Ilib $RUBYOPT" testrb tests/*.rb}
18
- end
19
-
20
- desc "Testing library with coverage"
21
- task :coverage do
22
- sh 'rcov -x tests -Ilib tests/*.rb'
23
- end
24
-
25
- desc "Installing library"
26
- task :install do
27
- ruby 'install.rb'
28
- end
29
-
30
- desc "Creating documentation"
31
- task :doc do
32
- ruby 'make_doc.rb'
33
- end
34
-
35
- if defined? Gem
36
- spec = Gem::Specification.new do |s|
37
- s.name = PKG_NAME
38
- s.version = PKG_VERSION
39
- s.summary = "Benchmarking is Bullshit"
40
- s.description = ""
41
-
42
- s.add_dependency('dslkit', '>= 0.2.5')
43
-
44
- s.files = PKG_FILES
45
-
46
- s.require_path = 'lib'
47
- s.executables = 'bs_compare'
48
-
49
- s.has_rdoc = true
50
- s.rdoc_options <<
51
- '--title' << 'Bullshit -- Benchmarking in Ruby' << '--main' << 'README'
52
- s.test_files = Dir['tests/*.rb']
53
-
54
- s.author = "Florian Frank"
55
- s.email = "flori@ping.de"
56
- s.homepage = "http://flori.github.com/#{PKG_NAME}"
57
- s.rubyforge_project = PKG_NAME
58
- end
59
-
60
- Rake::GemPackageTask.new(spec) do |pkg|
61
- pkg.need_tar = true
62
- pkg.package_files += PKG_FILES
1
+ # vim: set filetype=ruby et sw=2 ts=2:
2
+
3
+ require 'gem_hadar'
4
+
5
+ GemHadar do
6
+ name 'bullshit'
7
+ author 'Florian Frank'
8
+ email 'flori@ping.de'
9
+ homepage "http://flori.github.com/#{name}"
10
+ summary 'Benchmarking is Bullshit'
11
+ description 'Library to benchmark ruby code and analyse the results'
12
+ test_dir 'tests'
13
+ ignore '.*.sw[pon]', 'pkg', 'Gemfile.lock'
14
+ readme 'README.rdoc'
15
+ title "#{name.camelize} -- Benchmarking in Ruby"
16
+ executables << 'bs_compare'
17
+
18
+ dependency 'spruz', '~>0.2'
19
+ dependency 'dslkit', '~>0.2'
20
+ dependency 'more_math', '~>0.0.1'
21
+ clobber 'data/*.{dat,log}'
22
+
23
+ install_library do
24
+ libdir = CONFIG["sitelibdir"]
25
+ install('lib/bullshit.rb', libdir, :mode => 0644)
26
+ mkdir_p subdir = File.join(libdir, 'bullshit')
27
+ for f in Dir['lib/bullshit/*.rb']
28
+ install(f, subdir)
29
+ end
30
+ bindir = CONFIG["bindir"]
31
+ install('bin/bs_compare', bindir, :mode => 0755)
63
32
  end
64
33
  end
65
-
66
- desc m = "Writing version information for #{PKG_VERSION}"
67
- task :version do
68
- puts m
69
- File.open(File.join('lib', PKG_NAME, 'version.rb'), 'w') do |v|
70
- v.puts <<EOT
71
- module Bullshit
72
- # Bullshit version
73
- VERSION = '#{PKG_VERSION}'
74
- VERSION_ARRAY = VERSION.split(/\\./).map { |x| x.to_i } # :nodoc:
75
- VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
76
- VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
77
- VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
78
- end
79
- EOT
80
- end
81
- end
82
-
83
- task :default => [ :version, :test ]
84
-
85
- task :release => [ :clobber, :version, :package ]
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
@@ -0,0 +1,43 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{bullshit}
5
+ s.version = "0.1.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Florian Frank"]
9
+ s.date = %q{2011-07-17}
10
+ s.default_executable = %q{bs_compare}
11
+ s.description = %q{Library to benchmark ruby code and analyse the results}
12
+ s.email = %q{flori@ping.de}
13
+ s.executables = ["bs_compare"]
14
+ s.extra_rdoc_files = ["README.rdoc", "lib/bullshit/version.rb", "lib/bullshit.rb"]
15
+ s.files = [".gitignore", ".travis.yml", "CHANGES", "COPYING", "Gemfile", "README.rdoc", "Rakefile", "VERSION", "bin/bs_compare", "bullshit.gemspec", "data/.keep", "examples/compare.rb", "examples/fibonacci.rb", "examples/iteration.rb", "examples/josephus.rb", "examples/sorting.rb", "examples/throw_raise.rb", "lib/bullshit.rb", "lib/bullshit/version.rb", "tests/test_bullshit.rb", "tests/test_window.rb"]
16
+ s.homepage = %q{http://flori.github.com/bullshit}
17
+ s.rdoc_options = ["--title", "Bullshit -- Benchmarking in Ruby", "--main", "README.rdoc"]
18
+ s.require_paths = ["lib"]
19
+ s.rubygems_version = %q{1.6.2}
20
+ s.summary = %q{Benchmarking is Bullshit}
21
+ s.test_files = ["tests/test_bullshit.rb", "tests/test_window.rb"]
22
+
23
+ if s.respond_to? :specification_version then
24
+ s.specification_version = 3
25
+
26
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
27
+ s.add_development_dependency(%q<gem_hadar>, ["~> 0.0.5"])
28
+ s.add_runtime_dependency(%q<spruz>, ["~> 0.2"])
29
+ s.add_runtime_dependency(%q<dslkit>, ["~> 0.2"])
30
+ s.add_runtime_dependency(%q<more_math>, ["~> 0.0.1"])
31
+ else
32
+ s.add_dependency(%q<gem_hadar>, ["~> 0.0.5"])
33
+ s.add_dependency(%q<spruz>, ["~> 0.2"])
34
+ s.add_dependency(%q<dslkit>, ["~> 0.2"])
35
+ s.add_dependency(%q<more_math>, ["~> 0.0.1"])
36
+ end
37
+ else
38
+ s.add_dependency(%q<gem_hadar>, ["~> 0.0.5"])
39
+ s.add_dependency(%q<spruz>, ["~> 0.2"])
40
+ s.add_dependency(%q<dslkit>, ["~> 0.2"])
41
+ s.add_dependency(%q<more_math>, ["~> 0.0.1"])
42
+ end
43
+ end
File without changes
@@ -1,10 +1,8 @@
1
1
  require 'dslkit'
2
2
  require 'enumerator'
3
3
 
4
- begin
5
- require 'bullshit/version'
6
- rescue LoadError
7
- end
4
+ require 'bullshit/version'
5
+ require 'more_math'
8
6
 
9
7
  # Module that includes all constants of the bullshit library.
10
8
  module Bullshit
@@ -12,147 +10,10 @@ module Bullshit
12
10
 
13
11
  NAME_COLUMN_SIZE = 5 # Number of columns used for row names.
14
12
 
15
- Infinity = 1.0 / 0 # Refers to floating point infinity.
16
-
17
13
  RUBY_DESCRIPTION = "ruby %s (%s patchlevel %s) [%s]" %
18
14
  [ RUBY_VERSION, RUBY_RELEASE_DATE, RUBY_PATCHLEVEL, RUBY_PLATFORM ]
19
15
 
20
- # This class implements a continued fraction of the form:
21
- #
22
- # b_1
23
- # a_0 + -------------------------
24
- # b_2
25
- # a_1 + --------------------
26
- # b_3
27
- # a_2 + ---------------
28
- # b_4
29
- # a_3 + ----------
30
- # b_5
31
- # a_4 + -----
32
- # ...
33
- #
34
- class ContinuedFraction
35
- # Creates a continued fraction instance. With the defaults for_a { 1 } and
36
- # for_b { 1 } it approximates the golden ration phi if evaluated.
37
- def initialize
38
- @a = proc { 1.0 }
39
- @b = proc { 1.0 }
40
- end
41
-
42
- # Creates a ContinuedFraction instances and passes its arguments to a call
43
- # to for_a.
44
- def self.for_a(arg = nil, &block)
45
- new.for_a(arg, &block)
46
- end
47
-
48
- # Creates a ContinuedFraction instances and passes its arguments to a call
49
- # to for_b.
50
- def self.for_b(arg = nil, &block)
51
- new.for_b(arg, &block)
52
- end
53
-
54
- # This method either takes a block or an argument +arg+. The argument +arg+
55
- # has to respond to an integer index n >= 0 and return the value a_n. The
56
- # block has to return the value for a_n when +n+ is passed as the first
57
- # argument to the block. If a_n is dependent on an +x+ value (see the call
58
- # method) the +x+ will be the second argument of the block.
59
- def for_a(arg = nil, &block)
60
- if arg and !block
61
- @a = arg
62
- elsif block and !arg
63
- @a = block
64
- else
65
- raise ArgumentError, "exactly one argument or one block required"
66
- end
67
- self
68
- end
69
-
70
- # This method either takes a block or an argument +arg+. The argument +arg+
71
- # has to respond to an integer index n >= 1 and return the value b_n. The
72
- # block has to return the value for b_n when +n+ is passed as the first
73
- # argument to the block. If b_n is dependent on an +x+ value (see the call
74
- # method) the +x+ will be the second argument of the block.
75
- def for_b(arg = nil, &block)
76
- if arg and !block
77
- @b = arg
78
- elsif block and !arg
79
- @b = block
80
- else
81
- raise ArgumentError, "exactly one argument or one block required"
82
- end
83
- self
84
- end
85
-
86
- # Returns the value for a_n or a_n(x).
87
- def a(n, x = nil)
88
- result = if x
89
- @a[n, x]
90
- else
91
- @a[n]
92
- end and result.to_f
93
- end
94
-
95
- # Returns the value for b_n or b_n(x).
96
- def b(n, x = nil)
97
- result = if x
98
- @b[n, x]
99
- else
100
- @b[n]
101
- end and result.to_f
102
- end
103
-
104
- # Evaluates the continued fraction for the value +x+ (if any) with the
105
- # accuracy +epsilon+ and +max_iterations+ as the maximum number of
106
- # iterations using the Wallis-method with scaling.
107
- def call(x = nil, epsilon = 1E-16, max_iterations = 1 << 31)
108
- c_0, c_1 = 1.0, a(0, x)
109
- c_1 == nil and return 0 / 0.0
110
- d_0, d_1 = 0.0, 1.0
111
- result = c_1 / d_1
112
- n = 0
113
- error = 1 / 0.0
114
- $DEBUG and warn "n=%u, a=%f, b=nil, c=%f, d=%f result=%f, error=nil" %
115
- [ n, c_1, c_1, d_1, result ]
116
- while n < max_iterations and error > epsilon
117
- n += 1
118
- a_n, b_n = a(n, x), b(n, x)
119
- a_n and b_n or break
120
- c_2 = a_n * c_1 + b_n * c_0
121
- d_2 = a_n * d_1 + b_n * d_0
122
- if c_2.infinite? or d_2.infinite?
123
- if a_n != 0
124
- c_2 = c_1 + (b_n / a_n * c_0)
125
- d_2 = d_1 + (b_n / a_n * d_0)
126
- elsif b_n != 0
127
- c_2 = (a_n / b_n * c_1) + c_0
128
- d_2 = (a_n / b_n * d_1) + d_0
129
- else
130
- raise Errno::ERANGE
131
- end
132
- end
133
- r = c_2 / d_2
134
- error = (r / result - 1).abs
135
-
136
- result = r
137
-
138
- $DEBUG and warn "n=%u, a=%f, b=%f, c=%f, d=%f, result=%f, error=%.16f" %
139
- [ n, a_n, b_n, c_1, d_1, result, error ]
140
-
141
- c_0, c_1 = c_1, c_2
142
- d_0, d_1 = d_1, d_2
143
- end
144
- n >= max_iterations and raise Errno::ERANGE
145
- result
146
- end
147
-
148
- alias [] call
149
-
150
- # Returns this continued fraction as a Proc object which takes the same
151
- # arguments like its call method does.
152
- def to_proc
153
- proc { |*a| call(*a) }
154
- end
155
- end
16
+ include MoreMath
156
17
 
157
18
  module ModuleFunctions
158
19
  module_function
@@ -203,6 +64,8 @@ module Bullshit
203
64
 
204
65
  # A Clock instance is used to take measurements while benchmarking.
205
66
  class Clock
67
+ include MoreMath
68
+
206
69
  TIMES = [ :real, :total, :user, :system ]
207
70
 
208
71
  ALL_COLUMNS = [ :scatter ] + TIMES + [ :repeat ]
@@ -314,12 +177,12 @@ module Bullshit
314
177
  self
315
178
  end
316
179
 
317
- # Returns a Hash of Analysis object for all of TIMES's time keys.
180
+ # Returns a Hash of Sequence object for all of TIMES's time keys.
318
181
  def analysis
319
182
  @analysis ||= Hash.new do |h, time|
320
183
  time = time.to_sym
321
184
  times = @times[time]
322
- h[time] = Analysis.new(times)
185
+ h[time] = MoreMath::Sequence.new(times)
323
186
  end
324
187
  end
325
188
 
@@ -342,7 +205,7 @@ module Bullshit
342
205
  def to_a
343
206
  if @repeat >= 1
344
207
  (::Bullshit::Clock::ALL_COLUMNS).map do |t|
345
- analysis[t].measurements
208
+ analysis[t].elements
346
209
  end.transpose
347
210
  else
348
211
  []
@@ -530,7 +393,7 @@ module Bullshit
530
393
  truncation = self.case.truncate_data
531
394
  slope_angle = self.case.truncate_data.slope_angle.abs
532
395
  time = self.case.compare_time.to_sym
533
- ms = analysis[time].measurements.reverse
396
+ ms = analysis[time].elements.reverse
534
397
  offset = ms.size - 1
535
398
  @slopes = []
536
399
  ModuleFunctions.array_window(ms, truncation.window_size) do |data|
@@ -544,802 +407,6 @@ module Bullshit
544
407
  end
545
408
  end
546
409
 
547
- # A histogram gives an overview of measurement time values.
548
- class Histogram
549
- # Create a Histogram for +clock+ using the measurements for +time+.
550
- def initialize(analysis, bins)
551
- @analysis = analysis
552
- @bins = bins
553
- @result = compute
554
- end
555
-
556
- # Number of bins for this Histogram.
557
- attr_reader :bins
558
-
559
- # Return the computed histogram as an array of arrays.
560
- def to_a
561
- @result
562
- end
563
-
564
- # Display this histogram to +output+, +width+ is the parameter for
565
- # +prepare_display+
566
- def display(output = $stdout, width = 50)
567
- d = prepare_display(width)
568
- for l, bar, r in d
569
- output << "%11.5f -|%s\n" % [ (l + r) / 2.0, "*" * bar ]
570
- end
571
- self
572
- end
573
-
574
- private
575
-
576
- # Returns an array of tuples (l, c, r) where +l+ is the left bin edge, +c+
577
- # the +width+-normalized frequence count value, and +r+ the right bin
578
- # edge. +width+ is usually an integer number representing the width of a
579
- # histogram bar.
580
- def prepare_display(width)
581
- r = @result.reverse
582
- factor = width.to_f / (r.transpose[1].max)
583
- r.map { |l, c, r| [ l, (c * factor).round, r ] }
584
- end
585
-
586
- # Computes the histogram and returns it as an array of tuples (l, c, r).
587
- def compute
588
- @analysis.measurements.empty? and return []
589
- last_r = -Infinity
590
- min = @analysis.min
591
- max = @analysis.max
592
- step = (max - min) / bins.to_f
593
- Array.new(bins) do |i|
594
- l = min + i * step
595
- r = min + (i + 1) * step
596
- c = 0
597
- @analysis.measurements.each do |x|
598
- x > last_r and (x <= r || i == bins - 1) and c += 1
599
- end
600
- last_r = r
601
- [ l, c, r ]
602
- end
603
- end
604
- end
605
-
606
- # This class is used to find the root of a function with Newton's bisection
607
- # method.
608
- class NewtonBisection
609
- # Creates a NewtonBisection instance for +function+, a one-argument block.
610
- def initialize(&function)
611
- @function = function
612
- end
613
-
614
- # The function, passed into the constructor.
615
- attr_reader :function
616
-
617
- # Return a bracket around a root, starting from the initial +range+. The
618
- # method returns nil, if no such bracket around a root could be found after
619
- # +n+ tries with the scaling +factor+.
620
- def bracket(range = -1..1, n = 50, factor = 1.6)
621
- x1, x2 = range.first.to_f, range.last.to_f
622
- x1 >= x2 and raise ArgumentError, "bad initial range #{range}"
623
- f1, f2 = @function[x1], @function[x2]
624
- n.times do
625
- f1 * f2 < 0 and return x1..x2
626
- if f1.abs < f2.abs
627
- f1 = @function[x1 += factor * (x1 - x2)]
628
- else
629
- f2 = @function[x2 += factor * (x2 - x1)]
630
- end
631
- end
632
- return
633
- end
634
-
635
- # Find the root of function in +range+ and return it. The method raises a
636
- # BullshitException, if no such root could be found after +n+ tries and in
637
- # the +epsilon+ environment.
638
- def solve(range = nil, n = 1 << 16, epsilon = 1E-16)
639
- if range
640
- x1, x2 = range.first.to_f, range.last.to_f
641
- x1 >= x2 and raise ArgumentError, "bad initial range #{range}"
642
- elsif range = bracket
643
- x1, x2 = range.first, range.last
644
- else
645
- raise ArgumentError, "bracket could not be determined"
646
- end
647
- f = @function[x1]
648
- fmid = @function[x2]
649
- f * fmid >= 0 and raise ArgumentError, "root must be bracketed in #{range}"
650
- root = if f < 0
651
- dx = x2 - x1
652
- x1
653
- else
654
- dx = x1 - x2
655
- x2
656
- end
657
- n.times do
658
- fmid = @function[xmid = root + (dx *= 0.5)]
659
- fmid < 0 and root = xmid
660
- dx.abs < epsilon or fmid == 0 and return root
661
- end
662
- raise BullshitException, "too many iterations (#{n})"
663
- end
664
- end
665
-
666
- module Functions
667
- module_function
668
-
669
- include Math
670
- extend Math
671
-
672
- LANCZOS_COEFFICIENTS = [
673
- 0.99999999999999709182,
674
- 57.156235665862923517,
675
- -59.597960355475491248,
676
- 14.136097974741747174,
677
- -0.49191381609762019978,
678
- 0.33994649984811888699e-4,
679
- 0.46523628927048575665e-4,
680
- -0.98374475304879564677e-4,
681
- 0.15808870322491248884e-3,
682
- -0.21026444172410488319e-3,
683
- 0.21743961811521264320e-3,
684
- -0.16431810653676389022e-3,
685
- 0.84418223983852743293e-4,
686
- -0.26190838401581408670e-4,
687
- 0.36899182659531622704e-5,
688
- ]
689
-
690
- HALF_LOG_2_PI = 0.5 * log(2 * Math::PI)
691
-
692
- # Returns the natural logarithm of Euler gamma function value for +x+ using
693
- # the Lanczos approximation.
694
- if method_defined?(:lgamma)
695
- def log_gamma(x)
696
- lgamma(x).first
697
- end
698
- else
699
- def log_gamma(x)
700
- if x.nan? || x <= 0
701
- 0 / 0.0
702
- else
703
- sum = 0.0
704
- (LANCZOS_COEFFICIENTS.size - 1).downto(1) do |i|
705
- sum += LANCZOS_COEFFICIENTS[i] / (x + i)
706
- end
707
- sum += LANCZOS_COEFFICIENTS[0]
708
- tmp = x + 607.0 / 128 + 0.5
709
- (x + 0.5) * log(tmp) - tmp + HALF_LOG_2_PI + log(sum / x)
710
- end
711
- rescue Errno::ERANGE, Errno::EDOM
712
- 0 / 0.0
713
- end
714
- end
715
-
716
- # Returns the natural logarithm of the beta function value for +(a, b)+.
717
- def log_beta(a, b)
718
- log_gamma(a) + log_gamma(b) - log_gamma(a + b)
719
- rescue Errno::ERANGE, Errno::EDOM
720
- 0 / 0.0
721
- end
722
-
723
- # Return an approximation value of Euler's regularized beta function for
724
- # +x+, +a+, and +b+ with an error <= +epsilon+, but only iterate
725
- # +max_iterations+-times.
726
- def beta_regularized(x, a, b, epsilon = 1E-16, max_iterations = 1 << 16)
727
- x, a, b = x.to_f, a.to_f, b.to_f
728
- case
729
- when a.nan? || b.nan? || x.nan? || a <= 0 || b <= 0 || x < 0 || x > 1
730
- 0 / 0.0
731
- when x > (a + 1) / (a + b + 2)
732
- 1 - beta_regularized(1 - x, b, a, epsilon, max_iterations)
733
- else
734
- fraction = ContinuedFraction.for_b do |n, x|
735
- if n % 2 == 0
736
- m = n / 2.0
737
- (m * (b - m) * x) / ((a + (2 * m) - 1) * (a + (2 * m)))
738
- else
739
- m = (n - 1) / 2.0
740
- -((a + m) * (a + b + m) * x) / ((a + 2 * m) * (a + 2 * m + 1))
741
- end
742
- end
743
- exp(a * log(x) + b * log(1.0 - x) - log(a) - log_beta(a, b)) /
744
- fraction[x, epsilon, max_iterations]
745
- end
746
- rescue Errno::ERANGE, Errno::EDOM
747
- 0 / 0.0
748
- end
749
-
750
- # Return an approximation of the regularized gammaP function for +x+ and
751
- # +a+ with an error of <= +epsilon+, but only iterate
752
- # +max_iterations+-times.
753
- def gammaP_regularized(x, a, epsilon = 1E-16, max_iterations = 1 << 16)
754
- x, a = x.to_f, a.to_f
755
- case
756
- when a.nan? || x.nan? || a <= 0 || x < 0
757
- 0 / 0.0
758
- when x == 0
759
- 0.0
760
- when 1 <= a && a < x
761
- 1 - gammaQ_regularized(x, a, epsilon, max_iterations)
762
- else
763
- n = 0
764
- an = 1 / a
765
- sum = an
766
- while an.abs > epsilon && n < max_iterations
767
- n += 1
768
- an *= x / (a + n)
769
- sum += an
770
- end
771
- if n >= max_iterations
772
- raise Errno::ERANGE
773
- else
774
- exp(-x + a * log(x) - log_gamma(a)) * sum
775
- end
776
- end
777
- rescue Errno::ERANGE, Errno::EDOM
778
- 0 / 0.0
779
- end
780
-
781
- # Return an approximation of the regularized gammaQ function for +x+ and
782
- # +a+ with an error of <= +epsilon+, but only iterate
783
- # +max_iterations+-times.
784
- def gammaQ_regularized(x, a, epsilon = 1E-16, max_iterations = 1 << 16)
785
- x, a = x.to_f, a.to_f
786
- case
787
- when a.nan? || x.nan? || a <= 0 || x < 0
788
- 0 / 0.0
789
- when x == 0
790
- 1.0
791
- when a > x || a < 1
792
- 1 - gammaP_regularized(x, a, epsilon, max_iterations)
793
- else
794
- fraction = ContinuedFraction.for_a do |n, x|
795
- (2 * n + 1) - a + x
796
- end.for_b do |n, x|
797
- n * (a - n)
798
- end
799
- exp(-x + a * log(x) - log_gamma(a)) *
800
- fraction[x, epsilon, max_iterations] ** -1
801
- end
802
- rescue Errno::ERANGE, Errno::EDOM
803
- 0 / 0.0
804
- end
805
-
806
- ROOT2 = sqrt(2)
807
-
808
- A = -8 * (Math::PI - 3) / (3 * Math::PI * (Math::PI - 4))
809
-
810
- # Returns an approximate value for the error function's value for +x+.
811
- def erf(x)
812
- r = sqrt(1 - exp(-x ** 2 * (4 / Math::PI + A * x ** 2) / (1 + A * x ** 2)))
813
- x < 0 ? -r : r
814
- end unless method_defined?(:erf)
815
- end
816
-
817
- # This class is used to compute the T-Distribution.
818
- class TDistribution
819
- include Functions
820
-
821
- # Returns a TDistribution instance for the degrees of freedom +df+.
822
- def initialize(df)
823
- @df = df
824
- end
825
-
826
- # Degrees of freedom.
827
- attr_reader :df
828
-
829
- # Returns the cumulative probability (p-value) of the TDistribution for the
830
- # t-value +x+.
831
- def probability(x)
832
- if x == 0
833
- 0.5
834
- else
835
- t = beta_regularized(@df / (@df + x ** 2.0), 0.5 * @df, 0.5)
836
- if x < 0.0
837
- 0.5 * t
838
- else
839
- 1 - 0.5 * t
840
- end
841
- end
842
- end
843
-
844
- # Returns the inverse cumulative probability (t-value) of the TDistribution
845
- # for the probability +p+.
846
- def inverse_probability(p)
847
- case
848
- when p <= 0
849
- -1 / 0.0
850
- when p >= 1
851
- 1 / 0.0
852
- else
853
- begin
854
- bisect = NewtonBisection.new { |x| probability(x) - p }
855
- range = bisect.bracket(-10..10)
856
- bisect.solve(range, 1_000_000)
857
- rescue
858
- 0 / 0.0
859
- end
860
- end
861
- end
862
- end
863
-
864
- # This class is used to compute the Normal Distribution.
865
- class NormalDistribution
866
- include Functions
867
-
868
- # Creates a NormalDistribution instance for the values +mu+ and +sigma+.
869
- def initialize(mu = 0.0, sigma = 1.0)
870
- @mu, @sigma = mu.to_f, sigma.to_f
871
- end
872
-
873
- attr_reader :mu
874
-
875
- attr_reader :sigma
876
-
877
- # Returns the cumulative probability (p-value) of the NormalDistribution
878
- # for the value +x+.
879
- def probability(x)
880
- 0.5 * (1 + erf((x - @mu) / (@sigma * ROOT2)))
881
- end
882
-
883
- # Returns the inverse cumulative probability value of the
884
- # NormalDistribution for the probability +p+.
885
- def inverse_probability(p)
886
- case
887
- when p <= 0
888
- -1 / 0.0
889
- when p >= 1
890
- 1 / 0.0
891
- when p == 0.5 # This is a bit sloppy, maybe improve this later.
892
- @mu
893
- else
894
- begin
895
- NewtonBisection.new { |x| probability(x) - p }.solve(nil, 1_000_000)
896
- rescue
897
- 0 / 0.0
898
- end
899
- end
900
- end
901
- end
902
-
903
- STD_NORMAL_DISTRIBUTION = NormalDistribution.new
904
-
905
- # This class is used to compute the Chi-Square Distribution.
906
- class ChiSquareDistribution
907
- include Functions
908
-
909
- # Creates a ChiSquareDistribution for +df+ degrees of freedom.
910
- def initialize(df)
911
- @df = df
912
- @df_half = @df / 2.0
913
- end
914
-
915
- attr_reader :df
916
-
917
- # Returns the cumulative probability (p-value) of the ChiSquareDistribution
918
- # for the value +x+.
919
- def probability(x)
920
- if x < 0
921
- 0.0
922
- else
923
- gammaP_regularized(x / 2, @df_half)
924
- end
925
- end
926
-
927
- # Returns the inverse cumulative probability value of the
928
- # NormalDistribution for the probability +p+.
929
- def inverse_probability(p)
930
- case
931
- when p <= 0, p >= 1
932
- 0.0
933
- else
934
- begin
935
- bisect = NewtonBisection.new { |x| probability(x) - p }
936
- range = bisect.bracket 0.5..10
937
- bisect.solve(range, 1_000_000)
938
- rescue
939
- 0 / 0.0
940
- end
941
- end
942
- end
943
- end
944
-
945
- # This class computes a linear regression for the given image and domain data
946
- # sets.
947
- class LinearRegression
948
- def initialize(image, domain = (0...image.size).to_a)
949
- image.size != domain.size and raise ArgumentError,
950
- "image and domain have unequal sizes"
951
- @image, @domain = image, domain
952
- compute
953
- end
954
-
955
- # The image data as an array.
956
- attr_reader :image
957
-
958
- # The domain data as an array.
959
- attr_reader :domain
960
-
961
- # The slope of the line.
962
- attr_reader :a
963
-
964
- # The offset of the line.
965
- attr_reader :b
966
-
967
- # Return true if the slope of the underlying data (not the sample data
968
- # passed into the constructor of this LinearRegression instance) is likely
969
- # (with alpha level _alpha_) to be zero.
970
- def slope_zero?(alpha = 0.05)
971
- df = @image.size - 2
972
- return true if df <= 0 # not enough values to check
973
- t = tvalue(alpha)
974
- td = TDistribution.new df
975
- t.abs <= td.inverse_probability(1 - alpha.abs / 2.0).abs
976
- end
977
-
978
- # Returns the residues of this linear regression in relation to the given
979
- # domain and image.
980
- def residues
981
- result = []
982
- @domain.zip(@image) do |x, y|
983
- result << y - (@a * x + @b)
984
- end
985
- result
986
- end
987
-
988
- private
989
-
990
- def compute
991
- size = @image.size
992
- sum_xx = sum_xy = sum_x = sum_y = 0.0
993
- @domain.zip(@image) do |x, y|
994
- x += 1
995
- sum_xx += x ** 2
996
- sum_xy += x * y
997
- sum_x += x
998
- sum_y += y
999
- end
1000
- @a = (size * sum_xy - sum_x * sum_y) / (size * sum_xx - sum_x ** 2)
1001
- @b = (sum_y - @a * sum_x) / size
1002
- self
1003
- end
1004
-
1005
- def tvalue(alpha = 0.05)
1006
- df = @image.size - 2
1007
- return 0.0 if df <= 0
1008
- sse_y = 0.0
1009
- @domain.zip(@image) do |x, y|
1010
- f_x = a * x + b
1011
- sse_y += (y - f_x) ** 2
1012
- end
1013
- mean = @image.inject(0.0) { |s, y| s + y } / @image.size
1014
- sse_x = @domain.inject(0.0) { |s, x| s + (x - mean) ** 2 }
1015
- t = a / (Math.sqrt(sse_y / df) / Math.sqrt(sse_x))
1016
- t.nan? ? 0.0 : t
1017
- end
1018
- end
1019
-
1020
- # This class is used to analyse the time measurements and compute their
1021
- # statistics.
1022
- class Analysis
1023
- def initialize(measurements)
1024
- @measurements = measurements
1025
- @measurements.freeze
1026
- end
1027
-
1028
- # Returns the array of measurements.
1029
- attr_reader :measurements
1030
-
1031
- # Returns the number of measurements, on which the analysis is based.
1032
- def size
1033
- @measurements.size
1034
- end
1035
-
1036
- # Returns the variance of the measurements.
1037
- def variance
1038
- @variance ||= sum_of_squares / size
1039
- end
1040
-
1041
- # Returns the sample_variance of the measurements.
1042
- def sample_variance
1043
- @sample_variance ||= size > 1 ? sum_of_squares / (size - 1.0) : 0.0
1044
- end
1045
-
1046
- # Returns the sum of squares (the sum of the squared deviations) of the
1047
- # measurements.
1048
- def sum_of_squares
1049
- @sum_of_squares ||= @measurements.inject(0.0) { |s, t| s + (t - arithmetic_mean) ** 2 }
1050
- end
1051
-
1052
- # Returns the standard deviation of the measurements.
1053
- def standard_deviation
1054
- @sample_deviation ||= Math.sqrt(variance)
1055
- end
1056
-
1057
- # Returns the standard deviation of the measurements in percentage of the
1058
- # arithmetic mean.
1059
- def standard_deviation_percentage
1060
- @standard_deviation_percentage ||= 100.0 * standard_deviation / arithmetic_mean
1061
- end
1062
-
1063
- # Returns the sample standard deviation of the measurements.
1064
- def sample_standard_deviation
1065
- @sample_standard_deviation ||= Math.sqrt(sample_variance)
1066
- end
1067
-
1068
- # Returns the sample standard deviation of the measurements in percentage
1069
- # of the arithmetic mean.
1070
- def sample_standard_deviation_percentage
1071
- @sample_standard_deviation_percentage ||= 100.0 * sample_standard_deviation / arithmetic_mean
1072
- end
1073
-
1074
- # Returns the sum of all measurements.
1075
- def sum
1076
- @sum ||= @measurements.inject(0.0) { |s, t| s + t }
1077
- end
1078
-
1079
- # Returns the arithmetic mean of the measurements.
1080
- def arithmetic_mean
1081
- @arithmetic_mean ||= sum / size
1082
- end
1083
-
1084
- alias mean arithmetic_mean
1085
-
1086
- # Returns the harmonic mean of the measurements. If any of the measurements
1087
- # is less than or equal to 0.0, this method returns NaN.
1088
- def harmonic_mean
1089
- @harmonic_mean ||= (
1090
- sum = @measurements.inject(0.0) { |s, t|
1091
- if t > 0
1092
- s + 1.0 / t
1093
- else
1094
- break nil
1095
- end
1096
- }
1097
- sum ? size / sum : 0 / 0.0
1098
- )
1099
- end
1100
-
1101
- # Returns the geometric mean of the measurements. If any of the
1102
- # measurements is less than 0.0, this method returns NaN.
1103
- def geometric_mean
1104
- @geometric_mean ||= (
1105
- sum = @measurements.inject(0.0) { |s, t|
1106
- case
1107
- when t > 0
1108
- s + Math.log(t)
1109
- when t == 0
1110
- break :null
1111
- else
1112
- break nil
1113
- end
1114
- }
1115
- case sum
1116
- when :null
1117
- 0.0
1118
- when Float
1119
- Math.exp(sum / size)
1120
- else
1121
- 0 / 0.0
1122
- end
1123
- )
1124
- end
1125
-
1126
- # Returns the minimum of the measurements.
1127
- def min
1128
- @min ||= @measurements.min
1129
- end
1130
-
1131
- # Returns the maximum of the measurements.
1132
- def max
1133
- @max ||= @measurements.max
1134
- end
1135
-
1136
- # Returns the +p+-percentile of the measurements.
1137
- # There are many methods to compute the percentile, this method uses the
1138
- # the weighted average at x_(n + 1)p, which allows p to be in 0...100
1139
- # (excluding the 100).
1140
- def percentile(p = 50)
1141
- (0...100).include?(p) or
1142
- raise ArgumentError, "p = #{p}, but has to be in (0...100)"
1143
- p /= 100.0
1144
- @sorted ||= @measurements.sort
1145
- r = p * (@sorted.size + 1)
1146
- r_i = r.to_i
1147
- r_f = r - r_i
1148
- if r_i >= 1
1149
- result = @sorted[r_i - 1]
1150
- if r_i < @sorted.size
1151
- result += r_f * (@sorted[r_i] - @sorted[r_i - 1])
1152
- end
1153
- else
1154
- result = @sorted[0]
1155
- end
1156
- result
1157
- end
1158
-
1159
- alias median percentile
1160
-
1161
- # Use an approximation of the Welch-Satterthwaite equation to compute the
1162
- # degrees of freedom for Welch's t-test.
1163
- def compute_welch_df(other)
1164
- (sample_variance / size + other.sample_variance / other.size) ** 2 / (
1165
- (sample_variance ** 2 / (size ** 2 * (size - 1))) +
1166
- (other.sample_variance ** 2 / (other.size ** 2 * (other.size - 1))))
1167
- end
1168
-
1169
- # Returns the t value of the Welch's t-test between this Analysis
1170
- # instance and the +other+.
1171
- def t_welch(other)
1172
- signal = arithmetic_mean - other.arithmetic_mean
1173
- noise = Math.sqrt(sample_variance / size +
1174
- other.sample_variance / other.size)
1175
- signal / noise
1176
- rescue Errno::EDOM
1177
- 0.0
1178
- end
1179
-
1180
- # Returns an estimation of the common standard deviation of the
1181
- # measurements of this and +other+.
1182
- def common_standard_deviation(other)
1183
- Math.sqrt(common_variance(other))
1184
- end
1185
-
1186
- # Returns an estimation of the common variance of the measurements of this
1187
- # and +other+.
1188
- def common_variance(other)
1189
- (size - 1) * sample_variance + (other.size - 1) * other.sample_variance /
1190
- (size + other.size - 2)
1191
- end
1192
-
1193
- # Compute the # degrees of freedom for Student's t-test.
1194
- def compute_student_df(other)
1195
- size + other.size - 2
1196
- end
1197
-
1198
- # Returns the t value of the Student's t-test between this Analysis
1199
- # instance and the +other+.
1200
- def t_student(other)
1201
- signal = arithmetic_mean - other.arithmetic_mean
1202
- noise = common_standard_deviation(other) *
1203
- Math.sqrt(size ** -1 + size ** -1)
1204
- rescue Errno::EDOM
1205
- 0.0
1206
- end
1207
-
1208
- # Compute a sample size, that will more likely yield a mean difference
1209
- # between this instance's measurements and those of +other+. Use +alpha+
1210
- # and +beta+ as levels for the first- and second-order errors.
1211
- def suggested_sample_size(other, alpha = 0.05, beta = 0.05)
1212
- alpha, beta = alpha.abs, beta.abs
1213
- signal = arithmetic_mean - other.arithmetic_mean
1214
- df = size + other.size - 2
1215
- pooled_variance_estimate = (sum_of_squares + other.sum_of_squares) / df
1216
- td = TDistribution.new df
1217
- (((td.inverse_probability(alpha) + td.inverse_probability(beta)) *
1218
- Math.sqrt(pooled_variance_estimate)) / signal) ** 2
1219
- end
1220
-
1221
- # Return true, if the Analysis instance covers the +other+, that is their
1222
- # arithmetic mean value is most likely to be equal for the +alpha+ error
1223
- # level.
1224
- def cover?(other, alpha = 0.05)
1225
- t = t_welch(other)
1226
- td = TDistribution.new(compute_welch_df(other))
1227
- t.abs < td.inverse_probability(1 - alpha.abs / 2.0)
1228
- end
1229
-
1230
- # Return the confidence interval for the arithmetic mean with alpha level +alpha+ of
1231
- # the measurements of this Analysis instance as a Range object.
1232
- def confidence_interval(alpha = 0.05)
1233
- td = TDistribution.new(size - 1)
1234
- t = td.inverse_probability(alpha / 2).abs
1235
- delta = t * sample_standard_deviation / Math.sqrt(size)
1236
- (arithmetic_mean - delta)..(arithmetic_mean + delta)
1237
- end
1238
-
1239
- # Returns the array of autovariances (of length size - 1).
1240
- def autovariance
1241
- Array.new(size - 1) do |k|
1242
- s = 0.0
1243
- 0.upto(size - k - 1) do |i|
1244
- s += (@measurements[i] - arithmetic_mean) * (@measurements[i + k] - arithmetic_mean)
1245
- end
1246
- s / size
1247
- end
1248
- end
1249
-
1250
- # Returns the array of autocorrelation values c_k / c_0 (of length size -
1251
- # 1).
1252
- def autocorrelation
1253
- c = autovariance
1254
- Array.new(c.size) { |k| c[k] / c[0] }
1255
- end
1256
-
1257
- # Returns the d-value for the Durbin-Watson statistic. The value is d << 2
1258
- # for positive, d >> 2 for negative and d around 2 for no autocorrelation.
1259
- def durbin_watson_statistic
1260
- e = linear_regression.residues
1261
- e.size <= 1 and return 2.0
1262
- (1...e.size).inject(0.0) { |s, i| s + (e[i] - e[i - 1]) ** 2 } /
1263
- e.inject(0.0) { |s, x| s + x ** 2 }
1264
- end
1265
-
1266
- # Returns the q value of the Ljung-Box statistic for the number of lags
1267
- # +lags+. A higher value might indicate autocorrelation in the measurements of
1268
- # this Analysis instance. This method returns nil if there weren't enough
1269
- # (at least lags) lags available.
1270
- def ljung_box_statistic(lags = 20)
1271
- r = autocorrelation
1272
- lags >= r.size and return
1273
- n = size
1274
- n * (n + 2) * (1..lags).inject(0.0) { |s, i| s + r[i] ** 2 / (n - i) }
1275
- end
1276
-
1277
- # This method tries to detect autocorrelation with the Ljung-Box
1278
- # statistic. If enough lags can be considered it returns a hash with
1279
- # results, otherwise nil is returned. The keys are
1280
- # :lags:: the number of lags,
1281
- # :alpha_level:: the alpha level for the test,
1282
- # :q:: the value of the ljung_box_statistic,
1283
- # :p:: the p-value computed, if p is higher than alpha no correlation was detected,
1284
- # :detected:: true if a correlation was found.
1285
- def detect_autocorrelation(lags = 20, alpha_level = 0.05)
1286
- if q = ljung_box_statistic(lags)
1287
- p = ChiSquareDistribution.new(lags).probability(q)
1288
- return {
1289
- :lags => lags,
1290
- :alpha_level => alpha_level,
1291
- :q => q,
1292
- :p => p,
1293
- :detected => p >= 1 - alpha_level,
1294
- }
1295
- end
1296
- end
1297
-
1298
- # Return a result hash with the number of :very_low, :low, :high, and
1299
- # :very_high outliers, determined by the box plotting algorithm run with
1300
- # :median and :iqr parameters. If no outliers were found or the iqr is
1301
- # less than epsilon, nil is returned.
1302
- def detect_outliers(factor = 3.0, epsilon = 1E-5)
1303
- half_factor = factor / 2.0
1304
- quartile1 = percentile(25)
1305
- quartile3 = percentile(75)
1306
- iqr = quartile3 - quartile1
1307
- iqr < epsilon and return
1308
- result = @measurements.inject(Hash.new(0)) do |h, t|
1309
- extreme =
1310
- case t
1311
- when -Infinity..(quartile1 - factor * iqr)
1312
- :very_low
1313
- when (quartile1 - factor * iqr)..(quartile1 - half_factor * iqr)
1314
- :low
1315
- when (quartile1 + half_factor * iqr)..(quartile3 + factor * iqr)
1316
- :high
1317
- when (quartile3 + factor * iqr)..Infinity
1318
- :very_high
1319
- end and h[extreme] += 1
1320
- h
1321
- end
1322
- unless result.empty?
1323
- result[:median] = median
1324
- result[:iqr] = iqr
1325
- result[:factor] = factor
1326
- result
1327
- end
1328
- end
1329
-
1330
- # Returns the LinearRegression object for the equation a * x + b which
1331
- # represents the line computed by the linear regression algorithm.
1332
- def linear_regression
1333
- @linear_regression ||= LinearRegression.new @measurements
1334
- end
1335
-
1336
- # Returns a Histogram instance with +bins+ as the number of bins for this
1337
- # analysis' measurements.
1338
- def histogram(bins)
1339
- Histogram.new(self, bins)
1340
- end
1341
- end
1342
-
1343
410
  CaseMethod = Struct.new(:name, :case, :clock)
1344
411
 
1345
412
  # This class' instance represents a method to be benchmarked.