variation 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,3 @@
1
+ -
2
+ ChangeLog.rdoc
3
+ LICENSE.txt
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ Gemfile.lock
2
+ doc/
3
+ pkg/
4
+ vendor/cache/*.gem
5
+ .yardoc
6
+ *~
7
+ .project
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup rdoc --title "variation Documentation" --protected
data/ChangeLog.rdoc ADDED
@@ -0,0 +1,4 @@
1
+ === 0.1.0 / 2013-08-26
2
+
3
+ * Initial release:
4
+ Three types of changes: immediate, linear, and sigmoid.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2013 James Tunnell
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,53 @@
1
+ = variation
2
+
3
+ * {Homepage}[https://rubygems.org/gems/variation]
4
+ * {Documentation}[http://rubydoc.info/gems/variation/frames]
5
+ * {Email}[mailto:jamestunnell at gmail.com]
6
+
7
+ == Description
8
+
9
+ Compute values that change with time (or some independent variable), using various transitions (immediate, linear, sigmoid) between values.
10
+
11
+ == Features
12
+
13
+ A variety of transitions:
14
+ * immediate
15
+ * linear
16
+ * sigmoid (s-shaped)
17
+
18
+ == Examples
19
+
20
+ require 'variation'
21
+ include Variation
22
+
23
+ p = Profile.new(
24
+ :start_value => 1,
25
+ :changes => {
26
+ 3 => ImmediateChange.new(:end_value => 3),
27
+ 5 => SigmoidChange.new(:end_value => 4, :length => 2),
28
+ 6 => LinearChange.new(:end_value => 1, :length => 1)
29
+ }
30
+ )
31
+
32
+ # generate a function to calculate profile values
33
+ f = p.function
34
+ f.call(0) # should return 1
35
+ f.call(3 - 1e-5) # should return 1
36
+ f.call(3) # should return 3
37
+ f.call(4) # should return 3.5
38
+ f.call(5) # should return 4
39
+ f.call(5.5) # should return 2.5
40
+ f.call(6) # should return 1
41
+ f.call(99) # should return 1
42
+
43
+ == Requirements
44
+
45
+ == Install
46
+
47
+ $ gem install variation
48
+
49
+ == Copyright
50
+
51
+ Copyright (c) 2013 James Tunnell
52
+
53
+ See LICENSE.txt for details.
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ # encoding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'rake'
5
+
6
+ begin
7
+ gem 'rspec', '~> 2.4'
8
+ require 'rspec/core/rake_task'
9
+
10
+ RSpec::Core::RakeTask.new
11
+ rescue LoadError => e
12
+ task :spec do
13
+ abort "Please run `gem install rspec` to install RSpec."
14
+ end
15
+ end
16
+
17
+ task :test => :spec
18
+ task :default => :spec
19
+
20
+ require "bundler/gem_tasks"
21
+
22
+ begin
23
+ gem 'yard', '~> 0.8'
24
+ require 'yard'
25
+
26
+ YARD::Rake::YardocTask.new
27
+ rescue LoadError => e
28
+ task :yard do
29
+ abort "Please run `gem install yard` to install YARD."
30
+ end
31
+ end
32
+ task :doc => :yard
33
+
34
+ require 'rubygems/package_task'
35
+ Gem::PackageTask.new(Gem::Specification.load('variation.gemspec')) do |pkg|
36
+ pkg.need_tar = true
37
+ pkg.need_zip = false
38
+ end
data/lib/variation.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'variation/version'
2
+
3
+ require 'variation/errors'
4
+ require 'variation/range'
5
+ require 'variation/functions/constant_function'
6
+ require 'variation/functions/linear_function'
7
+ require 'variation/functions/sigmoid_function'
8
+ require 'variation/change'
9
+ require 'variation/changes/immediate_change'
10
+ require 'variation/changes/linear_change'
11
+ require 'variation/changes/sigmoid_change'
12
+ require 'variation/profile'
@@ -0,0 +1,23 @@
1
+ module Variation
2
+ class Change
3
+ attr_reader :length, :end_value
4
+
5
+ # Pass :length and :end_value by hash. Length must be > 0.
6
+ def initialize hashed_args
7
+ raise HashedArgMissingError unless hashed_args.has_key?(:length)
8
+ raise HashedArgMissingError unless hashed_args.has_key?(:end_value)
9
+
10
+ self.length = hashed_args[:length]
11
+ self.end_value = hashed_args[:end_value]
12
+ end
13
+
14
+ def length= length
15
+ raise NegativeLengthError if length < 0
16
+ @length = length
17
+ end
18
+
19
+ def end_value= end_value
20
+ @end_value = end_value
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,12 @@
1
+ module Variation
2
+ class ImmediateChange < Change
3
+ # Pass :end_value by hash.
4
+ def initialize hashed_args
5
+ super(hashed_args.merge(:length => 0))
6
+ end
7
+
8
+ def transition_function start_point
9
+ ConstantFunction.from_value end_value
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,13 @@
1
+ module Variation
2
+ class LinearChange < Change
3
+ # Pass :length and :end_value by hash. Length must be > 0.
4
+ def initialize hashed_args
5
+ super(hashed_args)
6
+ end
7
+
8
+ def transition_function start_point
9
+ end_point = [start_point[0] + length, end_value]
10
+ LinearFunction.from_points start_point, end_point
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ module Variation
2
+ class SigmoidChange < Change
3
+ # Pass :length and :end_value by hash. Length must be > 0.
4
+ def initialize hashed_args
5
+ super(hashed_args)
6
+ @abruptness = hashed_args[:abruptness] || 0.5
7
+ end
8
+
9
+ def transition_function start_point
10
+ end_point = [start_point[0] + length, end_value]
11
+ SigmoidFunction.from_points start_point, end_point, @abruptness
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,7 @@
1
+ module Variation
2
+ class NegativeLengthError < StandardError; end
3
+ class OutsideOfDomainError < StandardError; end
4
+ class NotBetweenZeroAndOneError < StandardError; end
5
+ class RangeNotIncreasingError < StandardError; end
6
+ class HashedArgMissingError < StandardError; end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Variation
2
+ class ConstantFunction
3
+ def self.from_value value
4
+ ->(x){ value }
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ module Variation
2
+ class LinearFunction
3
+ def self.from_points pt_a, pt_b, abruptness = 0.5
4
+ slope = (pt_b[1] - pt_a[1]) / (pt_b[0] - pt_a[0]).to_f
5
+ intercept = pt_a[1] - (slope * pt_a[0])
6
+
7
+ lambda do |x|
8
+ (slope * x) + intercept
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,28 @@
1
+ module Variation
2
+ class SigmoidFunction
3
+ def self.from_points pt_a, pt_b, abruptness = 0.5
4
+ raise NotBetweenZeroAndOneError unless abruptness.between?(0,1)
5
+
6
+ domain = pt_a[0]..pt_b[0]
7
+ codomain = pt_a[1]..pt_b[1]
8
+
9
+ magn = LinearFunction.from_points([0,3],[1,9]).call(abruptness)
10
+ tanh_domain = -magn..magn
11
+ tanh_codomain = Math::tanh(-magn)..Math::tanh(magn)
12
+
13
+ domain_transformer = LinearFunction.from_points(
14
+ [domain.first, tanh_domain.first],
15
+ [domain.last, tanh_domain.last]
16
+ )
17
+
18
+ codomain_transformer = LinearFunction.from_points(
19
+ [tanh_codomain.first, codomain.first],
20
+ [tanh_codomain.last, codomain.last]
21
+ )
22
+
23
+ lambda do |x|
24
+ codomain_transformer.call(Math::tanh(domain_transformer.call(x)))
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,130 @@
1
+ module Variation
2
+
3
+ # Represent a setting that can change over time.
4
+ class Profile
5
+
6
+ attr_reader :start_value, :changes
7
+
8
+ def initialize hashed_args
9
+ raise HashedArgMissingError unless hashed_args.has_key?(:start_value)
10
+ changes = hashed_args[:changes] || {}
11
+
12
+ @start_value = hashed_args[:start_value]
13
+ @changes = changes
14
+
15
+ trim_changes_if_needed @changes
16
+ end
17
+
18
+ def length
19
+ length = 0
20
+ if @changes.any?
21
+ first_offset = @changes.keys.min
22
+ last_offset = @changes.keys.max
23
+ length = (last_offset - first_offset) + @changes[first_offset].length
24
+ end
25
+ return length
26
+ end
27
+
28
+ # def select range
29
+ # raise OutsideOfDomainError unless domain.include?(x_range.first)
30
+ # raise OutsideOfDomainError unless domain.include?(x_range.last)
31
+
32
+ # changes = {}
33
+ # @changes.each do |offset, change|
34
+ # change_end = offset + change.length
35
+ # if range.include?(offset) && range.include?(change_end)
36
+ # changes[offset] = change
37
+ # elsif range.include?(offset)
38
+ # changes[offset] = change.truncate_end(range.last - offset)
39
+ # elsif range.include?(change_end)
40
+ # changes[range.first] = change.truncate_start(change_end - range.first)
41
+ # end
42
+ # end
43
+
44
+ # changes = @changes.select do |offset, change|
45
+ # range.include?(offset) || range.include?(offset + change.length)
46
+ # end
47
+
48
+ # if changes.keys.min < range.first
49
+
50
+ # if first_offset
51
+ # last_offset = changes.keys.max
52
+
53
+ # end
54
+ # Profile.new(@start_value, changes)
55
+ # end
56
+
57
+ def end_value
58
+ if @changes.any?
59
+ @changes[@changes.keys.max].end_value
60
+ else
61
+ start_value
62
+ end
63
+ end
64
+
65
+ def function
66
+ functions = {}
67
+
68
+ prev_val = start_value
69
+ prev_offset = -Float::INFINITY
70
+
71
+ if @changes.any?
72
+ sorted_offsets = @changes.keys.sort
73
+
74
+ sorted_offsets.each_index do |i|
75
+ offset = sorted_offsets[i]
76
+ change = @changes[offset]
77
+ start_of_transition = offset - change.length
78
+
79
+ unless prev_offset == start_of_transition
80
+ functions[prev_offset...start_of_transition] = ConstantFunction.from_value(prev_val)
81
+ end
82
+ functions[start_of_transition...offset] = change.transition_function([start_of_transition, prev_val])
83
+
84
+ prev_val = change.end_value
85
+ prev_offset = offset
86
+ end
87
+ end
88
+
89
+ functions[prev_offset...Float::INFINITY] = ConstantFunction.from_value(prev_val)
90
+
91
+ lambda do |x|
92
+ result = functions.find {|domain,func| domain.include?(x) }
93
+ f = result[1]
94
+ f.call(x)
95
+ end
96
+ end
97
+
98
+ def at(x)
99
+ function.call(x)
100
+ end
101
+
102
+ def data(step_size)
103
+ data = {}
104
+ if @changes.any?
105
+ f = function
106
+ domain = (@changes.keys.max - length)..length
107
+ domain.step(step_size) do |x|
108
+ data[x] = f.call(x)
109
+ end
110
+ end
111
+ return data
112
+ end
113
+
114
+ private
115
+
116
+ def trim_changes_if_needed changes
117
+ offsets = changes.keys.sort
118
+ for i in 1...offsets.count
119
+ prev_offset = offsets[i-1]
120
+ offset = offsets[i]
121
+ change = changes[offset]
122
+
123
+ if (offset - change.length) < prev_offset
124
+ change.length = offset - prev_offset
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ end
@@ -0,0 +1,66 @@
1
+ class Range
2
+ class RangeDecreasingError < StandardError; end
3
+
4
+ def decreasing?
5
+ if exclude_end?
6
+ last < first
7
+ else
8
+ last <= first
9
+ end
10
+ end
11
+
12
+ def intersect? other
13
+ raise RangeDecreasingError if other.decreasing?
14
+
15
+ if other.first > last
16
+ return false
17
+ elsif other.first == last && exclude_end?
18
+ return false
19
+ elsif other.last < first
20
+ return false
21
+ elsif other.last == first && other.exclude_end?
22
+ return false
23
+ else
24
+ return true
25
+ end
26
+ end
27
+
28
+ def intersection(other)
29
+ raise RangeDecreasingError if other.decreasing?
30
+
31
+ if intersect?(other)
32
+ if include? other.first
33
+ start = other.first
34
+ else # other.first < first
35
+ start = first
36
+ end
37
+
38
+ if exclude_end?
39
+ if other.exclude_end?
40
+ return start...(last < other.last ? last : other.last)
41
+ else # other is inclusive
42
+ if other.last <= last
43
+ return start..other.last
44
+ else
45
+ return start...last
46
+ end
47
+ end
48
+ else # self is inclusize
49
+ if other.exclude_end?
50
+ if other.last >= last
51
+ return start..last
52
+ else other.last < last
53
+ return start...other.last
54
+ end
55
+ else # other is inclusive as well
56
+ return start..(last < other.last ? last : other.last)
57
+ end
58
+ end
59
+ end
60
+
61
+ return nil
62
+ end
63
+
64
+ alias_method :&, :intersection
65
+ alias_method :intersect, :intersection
66
+ end
@@ -0,0 +1,4 @@
1
+ module Variation
2
+ # variation version
3
+ VERSION = "0.2.0"
4
+ end
@@ -0,0 +1,33 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe ImmediateChange do
4
+ describe '.new' do
5
+ context 'empty hash given' do
6
+ it 'should raise HashedArgMissingError' do
7
+ expect { ImmediateChange.new({}) }.to raise_error(HashedArgMissingError)
8
+ end
9
+ end
10
+
11
+ context ':end_value given' do
12
+ it 'should raise HashedArgMissingError' do
13
+ expect { ImmediateChange.new(:end_value => 1) }.to_not raise_error
14
+ end
15
+ end
16
+ end
17
+
18
+ describe '#transition_function' do
19
+ it 'should return a Proc with arity of 1' do
20
+ [ 2, 5.5 ].each do |value|
21
+ ImmediateChange.new(:end_value => value).transition_function([0,0]).arity.should eq(1)
22
+ end
23
+ end
24
+
25
+ it 'should return a Proc, that returns the given constant value regardless of the argument' do
26
+ [ 2, 5.5 ].each do |value|
27
+ f = ImmediateChange.new(:end_value => value).transition_function([0,0])
28
+ f.call(1).should eq(value)
29
+ f.call(1000).should eq(value)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe LinearChange do
4
+ describe '.new' do
5
+ context 'empty hash given' do
6
+ it 'should raise HashedArgMissingError' do
7
+ expect { LinearChange.new({}) }.to raise_error(HashedArgMissingError)
8
+ end
9
+ end
10
+
11
+ context ':length only given' do
12
+ it 'should raise HashedArgMissingError' do
13
+ expect { LinearChange.new(:length => 2) }.to raise_error(HashedArgMissingError)
14
+ end
15
+ end
16
+
17
+ context ':end_value only given' do
18
+ it 'should raise HashedArgMissingError' do
19
+ expect { LinearChange.new(:end_value => 1) }.to raise_error(HashedArgMissingError)
20
+ end
21
+ end
22
+
23
+ context ':length and :end_value given' do
24
+ it 'should raise HashedArgMissingError' do
25
+ expect { LinearChange.new(:end_value => 1, :length => 2) }.to_not raise_error
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#transition_function' do
31
+ it 'should return a Proc with arity of 1' do
32
+ LinearChange.new(:end_value => 1, :length => 1).transition_function([0,0]).arity.should eq(1)
33
+ LinearChange.new(:end_value => 3, :length => 2).transition_function([1,2]).arity.should eq(1)
34
+ end
35
+
36
+ context 'end value 1.2 and length 2' do
37
+ before :all do
38
+ @change = LinearChange.new(:end_value => 1.2, :length => 2)
39
+ end
40
+
41
+ context 'given start point [0,1.1]' do
42
+ it 'should produce a function that follows the equation y = 0.05x + 1.1' do
43
+ f = @change.transition_function([0,1.1])
44
+ f.call(0).should eq(1.1)
45
+ f.call(0.5).should eq(1.125)
46
+ f.call(1).should eq(1.15)
47
+ f.call(1.5).should eq(1.175)
48
+ f.call(2).should eq(1.2)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe SigmoidChange do
4
+ describe '.new' do
5
+ context 'empty hash given' do
6
+ it 'should raise HashedArgMissingError' do
7
+ expect { SigmoidChange.new({}) }.to raise_error(HashedArgMissingError)
8
+ end
9
+ end
10
+
11
+ context ':length only given' do
12
+ it 'should raise HashedArgMissingError' do
13
+ expect { SigmoidChange.new(:length => 2) }.to raise_error(HashedArgMissingError)
14
+ end
15
+ end
16
+
17
+ context ':end_value only given' do
18
+ it 'should raise HashedArgMissingError' do
19
+ expect { SigmoidChange.new(:end_value => 1) }.to raise_error(HashedArgMissingError)
20
+ end
21
+ end
22
+
23
+ context ':length and :end_value given' do
24
+ it 'should raise HashedArgMissingError' do
25
+ expect { SigmoidChange.new(:end_value => 1, :length => 2) }.to_not raise_error
26
+ end
27
+ end
28
+ end
29
+
30
+ describe '#transition_function' do
31
+ it 'should return a Proc with arity of 1' do
32
+ SigmoidChange.new(:end_value => 1, :length => 1).transition_function([0,0]).arity.should eq(1)
33
+ SigmoidChange.new(:end_value => 3, :length => 2).transition_function([1,2]).arity.should eq(1)
34
+ end
35
+
36
+ context 'end value 1.2 and length 2' do
37
+ before :all do
38
+ @change = SigmoidChange.new(:end_value => 1.25, :length => 2)
39
+ end
40
+
41
+ context 'given start point [0,1.1]' do
42
+ it 'should produce a function that...' do
43
+ f = @change.transition_function([0,1.0])
44
+ f.call(0).should eq(1.0)
45
+ f.call(1).should eq(1.125)
46
+ f.call(2).should eq(1.25)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe NegativeLengthError do
4
+ end
5
+
6
+ describe OutsideOfDomainError do
7
+ end
8
+
9
+ describe NotBetweenZeroAndOneError do
10
+ end
11
+
12
+ describe RangeNotIncreasingError do
13
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe ConstantFunction do
4
+ describe '.from_value' do
5
+ it 'should return a Proc with arity of 1' do
6
+ ConstantFunction.from_value(0).arity.should eq(1)
7
+ ConstantFunction.from_value(125).arity.should eq(1)
8
+ end
9
+
10
+ it 'should return a Proc, that returns the given constant value regardless of the argument' do
11
+ f = ConstantFunction.from_value(0)
12
+ f.call(1).should eq(0)
13
+ f.call(1000).should eq(0)
14
+
15
+ f = ConstantFunction.from_value(-1)
16
+ f.call(1).should eq(-1)
17
+ f.call(1000).should eq(-1)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe LinearFunction do
4
+ describe '.from_points' do
5
+ it 'should return a Proc with arity of 1' do
6
+ LinearFunction.from_points([0,0],[1,1]).arity.should eq(1)
7
+ LinearFunction.from_points([1,2],[2,3]).arity.should eq(1)
8
+ end
9
+
10
+ context 'given points [0,1.1] and [2,1.2]' do
11
+ it 'should produce a function that follows the equation y = 0.05x + 1.1' do
12
+ f = LinearFunction.from_points [0,1.1], [2,1.2]
13
+ f.call(0).should eq(1.1)
14
+ f.call(0.5).should eq(1.125)
15
+ f.call(1).should eq(1.15)
16
+ f.call(1.5).should eq(1.175)
17
+ f.call(2).should eq(1.2)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe SigmoidFunction do
4
+ describe '.from_points' do
5
+ context 'given abruptness less than zero' do
6
+ it 'should raise NotBetweenZeroAndOneError' do
7
+ expect { SigmoidFunction.from_points([0,0],[1,1],-0.01) }.to raise_error(NotBetweenZeroAndOneError)
8
+ end
9
+ end
10
+
11
+ context 'given abruptness greater than 1' do
12
+ it 'should raise NotBetweenZeroAndOneError' do
13
+ expect { SigmoidFunction.from_points([0,0],[1,1],1.01) }.to raise_error(NotBetweenZeroAndOneError)
14
+ end
15
+ end
16
+
17
+ context 'given abruptness between 0 and 1' do
18
+ it 'should not raise any error' do
19
+ (0..1).step(0.1) do |abruptness|
20
+ expect { SigmoidFunction.from_points([0,0],[1,1],abruptness) }.to_not raise_error
21
+ end
22
+ end
23
+ end
24
+
25
+ it 'should return a Proc with arity of 1' do
26
+ SigmoidFunction.from_points([0,0],[1,1]).arity.should eq(1)
27
+ SigmoidFunction.from_points([1,2],[2,3]).arity.should eq(1)
28
+ end
29
+
30
+ context 'given points [0,1.1] and [2,1.2]' do
31
+ it 'should produce a function that...' do
32
+ f = SigmoidFunction.from_points [0,1.0], [2,1.25]
33
+ f.call(0).should eq(1.0)
34
+ f.call(1).should eq(1.125)
35
+ f.call(2).should eq(1.25)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,186 @@
1
+ require 'spec_helper'
2
+ require 'spcore'
3
+
4
+ describe Profile do
5
+ describe '.new' do
6
+ context 'no :start_value given' do
7
+ it 'should raise HashedArgMissingError' do
8
+ expect { Profile.new({}) }.to raise_error(HashedArgMissingError)
9
+ end
10
+ end
11
+
12
+ context 'no :changes given' do
13
+ it 'should not raise error' do
14
+ expect { Profile.new(:start_value => 2) }.to_not raise_error
15
+ end
16
+
17
+ it 'should default changes to empty hash' do
18
+ Profile.new(:start_value => 2).changes.should be_empty
19
+ end
20
+ end
21
+ end
22
+
23
+ describe '#length' do
24
+ it 'should return difference from last change offset and (first change offset + first change length)' do
25
+ Profile.new(
26
+ :start_value => 1,
27
+ :changes => {
28
+ 2 => LinearChange.new(:end_value => 2, :length => 2),
29
+ 8 => SigmoidChange.new(:end_value => 0, :length => 3)
30
+ }
31
+ ).length.should eq(8)
32
+ end
33
+ end
34
+
35
+ describe '#function' do
36
+ before :all do
37
+ @profiles = [
38
+ Profile.new(
39
+ :start_value => 2.0,
40
+ :changes => {
41
+ 1 => LinearChange.new(:end_value => 3.5, :length => 1),
42
+ 4 => LinearChange.new(:end_value => 1.5, :length => 2)
43
+ }
44
+ ),
45
+ Profile.new(
46
+ :start_value => 1,
47
+ :changes => {
48
+ 2 => LinearChange.new(:end_value => 2, :length => 2),
49
+ 8 => SigmoidChange.new(:end_value => 0, :length => 3)
50
+ }
51
+ ),
52
+ Profile.new(
53
+ :start_value => -20,
54
+ :changes => {
55
+ -5 => ImmediateChange.new(:end_value => 3),
56
+ 0 => ImmediateChange.new(:end_value => 3),
57
+ 5 => SigmoidChange.new(:end_value => 2, :length => 1)
58
+ }
59
+ )
60
+ ]
61
+ end
62
+
63
+ describe '#function#arity' do
64
+ it 'should be 1' do
65
+ @profiles.each do |profile|
66
+ profile.function.arity.should eq(1)
67
+ end
68
+ end
69
+ end
70
+
71
+ describe '#function#call' do
72
+ context 'given first change offset' do
73
+ it 'should return first change value' do
74
+ @profiles.each do |profile|
75
+ first_offset = profile.changes.keys.min
76
+ first_change = profile.changes[first_offset]
77
+ profile.function.call(first_offset).should eq(first_change.end_value)
78
+ end
79
+ end
80
+ end
81
+
82
+ context 'changes with length == 0 (i.e. immediate changes)' do
83
+ before :all do
84
+ @profile = Profile.new(
85
+ :start_value => -20,
86
+ :changes => {
87
+ -5 => ImmediateChange.new(:end_value => 3),
88
+ 0 => ImmediateChange.new(:end_value => 5),
89
+ 5 => ImmediateChange.new(:end_value => 2)
90
+ }
91
+ )
92
+ @function = @profile.function
93
+ end
94
+
95
+ context 'given change offset' do
96
+ it 'should return change end value' do
97
+ @profile.changes.each do |offset, change|
98
+ @function.call(offset).should eq(change.end_value)
99
+ end
100
+ end
101
+ end
102
+
103
+ context 'given just before change offset' do
104
+ it 'should return change end value' do
105
+ @profile.changes.each do |offset, change|
106
+ @function.call(offset - 1e-5).should_not eq(change.end_value)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ context 'changes with length > 0' do
113
+ before :all do
114
+ @profile = Profile.new(
115
+ :start_value => -20,
116
+ :changes => {
117
+ -5 => LinearChange.new(:end_value => 3, :length => 2),
118
+ 0 => LinearChange.new(:end_value => 1, :length => 1),
119
+ 5 => SigmoidChange.new(:end_value => -10, :length => 2.5),
120
+ }
121
+ )
122
+ @function = @profile.function
123
+ end
124
+
125
+ context 'given change offset' do
126
+ it 'should return change end value' do
127
+ @profile.changes.each do |offset, change|
128
+ @function.call(offset).should eq(change.end_value)
129
+ end
130
+ end
131
+ end
132
+
133
+ context 'given (change offset - change length)' do
134
+ context 'first change' do
135
+ it 'should return the start value' do
136
+ first_offset = @profile.changes.keys.min
137
+ first_change = @profile.changes[first_offset]
138
+ @function.call(first_offset - first_change.length).should eq(@profile.start_value)
139
+ end
140
+ end
141
+
142
+ context 'second and later changes' do
143
+ it 'should return end value of previous change' do
144
+ sorted_offsets = @profile.changes.keys.sort
145
+ for i in 1...sorted_offsets.count
146
+ offset = sorted_offsets[i]
147
+ change = @profile.changes[offset]
148
+ prev_change = @profile.changes[sorted_offsets[i-1]]
149
+
150
+ @function.call(offset - change.length).should eq(prev_change.end_value)
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ context 'given (change offset - 1/2 change length)' do
157
+ context 'first change' do
158
+ it 'should return 1/2 between start value and change end value' do
159
+ first_offset = @profile.changes.keys.min
160
+ first_change = @profile.changes[first_offset]
161
+
162
+ halfway = first_offset - (first_change.length / 2.0)
163
+ expected = @profile.start_value + (first_change.end_value - @profile.start_value) / 2.0
164
+ @function.call(halfway).should eq(expected)
165
+ end
166
+ end
167
+
168
+ context 'second and later changes' do
169
+ it 'should return 1/2 between end value of previous change and end value of current change' do
170
+ sorted_offsets = @profile.changes.keys.sort
171
+ for i in 1...sorted_offsets.count
172
+ offset = sorted_offsets[i]
173
+ change = @profile.changes[offset]
174
+ prev_change = @profile.changes[sorted_offsets[i-1]]
175
+
176
+ halfway = offset - (change.length / 2.0)
177
+ expected = prev_change.end_value + (change.end_value - prev_change.end_value) / 2.0
178
+ @function.call(halfway).should eq(expected)
179
+ end
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
@@ -0,0 +1,104 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Range#intersection' do
4
+ it 'should raise error if the other given range is decreasing' do
5
+ expect { (0..2).intersection(1..0) }.to raise_error
6
+ end
7
+
8
+ context 'range includes end' do
9
+ context 'second range ends before the first begins' do
10
+ it 'should return nil' do
11
+ (5..10).intersection(1..4).should be_nil
12
+ end
13
+ end
14
+
15
+ context 'second range starts before the first range starts' do
16
+ context 'second range ends where the first begins' do
17
+ context 'second range excludes end' do
18
+ it 'should return nil' do
19
+ (5..10).intersection(1...5).should be_nil
20
+ end
21
+ end
22
+
23
+ context 'second range includes end' do
24
+ it 'should return a range with the same start/end' do
25
+ (5..10).intersection(1..5).should eq(5..5)
26
+ end
27
+ end
28
+ end
29
+
30
+ context 'second range ends inside first range' do
31
+ it 'should return a range that starts where the first range starts and ends where the second range ends' do
32
+ (5..10).intersection(4..6).should eq(5..6)
33
+ (5..10).intersection(4...6).should eq(5...6)
34
+ end
35
+ end
36
+ end
37
+
38
+ context 'second range starts where the first range starts' do
39
+
40
+ end
41
+
42
+ context 'second range starts inside the first range' do
43
+ context 'second range ends inside the first range' do
44
+ it 'should return the second range' do
45
+ (5..10).intersection(6..9).should eq(6..9)
46
+ (5..10).intersection(6...8).should eq(6...8)
47
+ end
48
+ end
49
+
50
+ context 'second range ends where the first range ends' do
51
+ context 'second range is inclusive' do
52
+ it 'should return the second range' do
53
+ (5..10).intersection(6..10).should eq(6..10)
54
+ end
55
+ end
56
+
57
+ context 'second range is exclusize' do
58
+ it 'should return an inclusize version of the second range' do
59
+ (5..10).intersection(6...10).should eq(6..10)
60
+ end
61
+ end
62
+ end
63
+
64
+ context 'second range ends after the first range ends' do
65
+ it 'should return a range starting with the second range and ending with the first' do
66
+ (5..10).intersection(6..11).should eq(6..10)
67
+ end
68
+ end
69
+ end
70
+
71
+ context 'second range starts where the first ends' do
72
+ it 'should return a range with the same start/end' do
73
+ (5..10).intersection(10..15).should eq(10..10)
74
+ end
75
+ end
76
+
77
+
78
+ context 'second range starts after the first ends' do
79
+ it 'should return nil' do
80
+ (5..10).intersection(11..15).should be_nil
81
+ end
82
+ end
83
+ end
84
+
85
+ context 'range excludes end' do
86
+ before :all do
87
+ @range = 5...10
88
+ end
89
+
90
+ context 'second range starts where the first ends' do
91
+ it 'should return nil' do
92
+ @range.intersection(10..15).should be_nil
93
+ end
94
+ end
95
+
96
+ context 'second range starts inside the first range' do
97
+ context 'second range ends outside the first range' do
98
+ it 'should start where second range starts and end where first range ends (but exclude end)' do
99
+ @range.intersection(5..15).should eq(5...10)
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,6 @@
1
+ gem 'rspec', '~> 2.4'
2
+ require 'rspec'
3
+ require 'variation'
4
+ require 'pry'
5
+
6
+ include Variation
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+ require 'variation'
3
+
4
+ describe Variation do
5
+ it "should have a VERSION constant" do
6
+ subject.const_get('VERSION').should_not be_empty
7
+ end
8
+ end
data/variation.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require File.expand_path('../lib/variation/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.name = "variation"
7
+ gem.version = Variation::VERSION
8
+ gem.summary = %q{Compute values that change with time, with various transitions (immediate, linear, sigmoid)}
9
+ gem.description = %q{Compute values that change with time (or some independent variable), using various transitions (immediate, linear, sigmoid) between values.}
10
+ gem.license = "MIT"
11
+ gem.authors = ["James Tunnell"]
12
+ gem.email = "jamestunnell@gmail.com"
13
+ gem.homepage = "https://rubygems.org/gems/variation"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_dependency 'gnuplot'
21
+
22
+ gem.add_development_dependency 'rspec', '~> 2.4'
23
+ gem.add_development_dependency 'yard', '~> 0.8'
24
+ gem.add_development_dependency 'pry'
25
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: variation
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - James Tunnell
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-19 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: gnuplot
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '2.4'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '2.4'
46
+ - !ruby/object:Gem::Dependency
47
+ name: yard
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '0.8'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '0.8'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: Compute values that change with time (or some independent variable),
79
+ using various transitions (immediate, linear, sigmoid) between values.
80
+ email: jamestunnell@gmail.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .document
86
+ - .gitignore
87
+ - .rspec
88
+ - .yardopts
89
+ - ChangeLog.rdoc
90
+ - Gemfile
91
+ - LICENSE.txt
92
+ - README.rdoc
93
+ - Rakefile
94
+ - lib/variation.rb
95
+ - lib/variation/change.rb
96
+ - lib/variation/changes/immediate_change.rb
97
+ - lib/variation/changes/linear_change.rb
98
+ - lib/variation/changes/sigmoid_change.rb
99
+ - lib/variation/errors.rb
100
+ - lib/variation/functions/constant_function.rb
101
+ - lib/variation/functions/linear_function.rb
102
+ - lib/variation/functions/sigmoid_function.rb
103
+ - lib/variation/profile.rb
104
+ - lib/variation/range.rb
105
+ - lib/variation/version.rb
106
+ - spec/changes/immediate_change_spec.rb
107
+ - spec/changes/linear_change_spec.rb
108
+ - spec/changes/sigmoid_change_spec.rb
109
+ - spec/errors_spec.rb
110
+ - spec/functions/constant_function_spec.rb
111
+ - spec/functions/linear_function_spec.rb
112
+ - spec/functions/sigmoid_function_spec.rb
113
+ - spec/profile_spec.rb
114
+ - spec/range_spec.rb
115
+ - spec/spec_helper.rb
116
+ - spec/variation_spec.rb
117
+ - variation.gemspec
118
+ homepage: https://rubygems.org/gems/variation
119
+ licenses:
120
+ - MIT
121
+ post_install_message:
122
+ rdoc_options: []
123
+ require_paths:
124
+ - lib
125
+ required_ruby_version: !ruby/object:Gem::Requirement
126
+ none: false
127
+ requirements:
128
+ - - ! '>='
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ none: false
133
+ requirements:
134
+ - - ! '>='
135
+ - !ruby/object:Gem::Version
136
+ version: '0'
137
+ requirements: []
138
+ rubyforge_project:
139
+ rubygems_version: 1.8.23
140
+ signing_key:
141
+ specification_version: 3
142
+ summary: Compute values that change with time, with various transitions (immediate,
143
+ linear, sigmoid)
144
+ test_files:
145
+ - spec/changes/immediate_change_spec.rb
146
+ - spec/changes/linear_change_spec.rb
147
+ - spec/changes/sigmoid_change_spec.rb
148
+ - spec/errors_spec.rb
149
+ - spec/functions/constant_function_spec.rb
150
+ - spec/functions/linear_function_spec.rb
151
+ - spec/functions/sigmoid_function_spec.rb
152
+ - spec/profile_spec.rb
153
+ - spec/range_spec.rb
154
+ - spec/spec_helper.rb
155
+ - spec/variation_spec.rb
156
+ has_rdoc: