variation 0.2.0

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.
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: