bullshit 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.