rational_choice 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/.travis.yml +6 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +47 -0
- data/Rakefile +43 -0
- data/lib/rational_choice.rb +110 -0
- data/rational_choice.gemspec +65 -0
- data/spec/rational_choice/dimension_spec.rb +136 -0
- data/spec/rational_choice/many_dimensions_spec.rb +54 -0
- data/spec/rational_choice_spec.rb +9 -0
- data/spec/spec_helper.rb +7 -0
- metadata +128 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 4d9c8ce05833c10354ed5a50430c8a2d23c605b8
|
4
|
+
data.tar.gz: 6f3f5990f6484cd086da5226903e79bd2948a643
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 09fadb723ab85663042ce147028c5144b62fd80c605b601d638a00285963e7970bd4018e20bbd42404ccba2707f5c026a1e620e83e8535517b6f28e24ec8e07d
|
7
|
+
data.tar.gz: 68b35d64dc97aba4466da942f77dc161eb2b3933dc5a98c77a500239edd042f6031df5c1e9f6ca5b481ad03e689cedb64bae53b508b2079a24a4b98ab317581f
|
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2015 WeTransfer
|
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.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# rational_choice
|
2
|
+
|
3
|
+
Makes a fuzzy logic choice based on a continuum of values. For when a "yes or no" is not-quite-good-enough.
|
4
|
+
|
5
|
+
## Choice on one dimension of values
|
6
|
+
|
7
|
+
Primary use is for things like load balancing. Imagine you have a server handling 14 connections,
|
8
|
+
and you know that it can take about 20 maximum. When you decide whether to send the connection
|
9
|
+
number 17 to it, you want to take a little margin and only send that connection sometimes, to
|
10
|
+
balance the choices - so you want to use a softer bound (a bit of a fuzzy logic).
|
11
|
+
|
12
|
+
will_accept_connection = RationalChoice::Dimension.new(20, 10) # ten connections is always safe
|
13
|
+
if will_accept_connection.choose(server.current_connection_count + 1) # will give you a fuzzy choice
|
14
|
+
server.accept(new_client)
|
15
|
+
else
|
16
|
+
... # over capacity at the moment, try later
|
17
|
+
end
|
18
|
+
|
19
|
+
This way the return value of `choose()` will be determined by the probability of the value being `true` - i.e.
|
20
|
+
how close it is to the upper bound. Values at or below the lower bound will always choose `false`. Values at
|
21
|
+
or above the upper bound will always choose `true`.
|
22
|
+
|
23
|
+
## Choice on multiple dimensions
|
24
|
+
|
25
|
+
Useful to give rational choices on multiple values, averaged together.
|
26
|
+
|
27
|
+
num_clients = Dimension.new(200, 100)
|
28
|
+
bandwidth = Dimension.new(2048, 1024)
|
29
|
+
has_capacity = ManyDimensions.new(num_clients, bandwidth)
|
30
|
+
|
31
|
+
will_accept_connection = has_capacity.choose(current_client_count, current_bandwidth)
|
32
|
+
|
33
|
+
## Contributing to rational_choice
|
34
|
+
|
35
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
36
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
37
|
+
* Fork the project.
|
38
|
+
* Start a feature/bugfix branch.
|
39
|
+
* Commit and push until you are happy with your contribution.
|
40
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
41
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
42
|
+
|
43
|
+
## Copyright
|
44
|
+
|
45
|
+
Copyright (c) 2015 WeTransfer. See LICENSE.txt for
|
46
|
+
further details.
|
47
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
require_relative 'lib/rational_choice'
|
14
|
+
|
15
|
+
require 'jeweler'
|
16
|
+
Jeweler::Tasks.new do |gem|
|
17
|
+
# gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
|
18
|
+
gem.version = RationalChoice::VERSION
|
19
|
+
gem.name = "rational_choice"
|
20
|
+
gem.homepage = "https://github.com/wetransfer/rational_choice"
|
21
|
+
gem.license = "MIT"
|
22
|
+
gem.description = %Q{Fuzzy logic gate}
|
23
|
+
gem.summary = %Q{Makes life-concerning choices based on an informed coin toss}
|
24
|
+
gem.email = "me@julik.nl"
|
25
|
+
gem.authors = ["Julik Tarkhanov"]
|
26
|
+
# dependencies defined in Gemfile
|
27
|
+
end
|
28
|
+
Jeweler::RubygemsDotOrgTasks.new
|
29
|
+
|
30
|
+
require 'rspec/core'
|
31
|
+
require 'rspec/core/rake_task'
|
32
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
33
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
34
|
+
end
|
35
|
+
|
36
|
+
task :default => :spec
|
37
|
+
|
38
|
+
require 'yard'
|
39
|
+
YARD::Rake::YardocTask.new do |t|
|
40
|
+
t.files = ['lib/**/*.rb', 'README*'] # optional
|
41
|
+
t.options = ['--markup markdown'] # optional
|
42
|
+
t.stats_options = ['--list-undoc'] # optional
|
43
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# Tiny fuzzy-logic gate for making choices based on a continuum of permitted values
|
2
|
+
# as opposed to a hard condition.
|
3
|
+
module RationalChoice
|
4
|
+
VERSION = '1.0.0'
|
5
|
+
|
6
|
+
# Gets raised when a multidimensional choice has to be made with a wrong number
|
7
|
+
# of values versus the number of dimensions
|
8
|
+
CardinalityError = Class.new(ArgumentError)
|
9
|
+
|
10
|
+
# Gets raised when a dimension has to be created with the same parameters
|
11
|
+
DomainError = Class.new(ArgumentError)
|
12
|
+
|
13
|
+
# Represents a fuzzy choice on a single dimension (one real number)
|
14
|
+
class Dimension
|
15
|
+
# Initializes a new Dimension to evaluate values
|
16
|
+
#
|
17
|
+
# @param false_bound[#to_f, #<=>] the lower bound, at or below which the value will be considered false
|
18
|
+
# @param true_bound[#to_f, #<=>] the upper bound, at or above which the value will be considered true
|
19
|
+
def initialize(false_bound, true_bound)
|
20
|
+
raise DomainError, "Bounds were the same at #{false_bound}" if false_bound == true_bound
|
21
|
+
|
22
|
+
@lower, @upper = [false_bound, true_bound].sort
|
23
|
+
@flip_sign = [@lower, @upper].sort != [false_bound, true_bound]
|
24
|
+
end
|
25
|
+
|
26
|
+
# Evaluate a value against the given false and true bound.
|
27
|
+
#
|
28
|
+
# If the value is less than or equal to the false bound, the method will return `false`.
|
29
|
+
# If the value is larger than or equal to the true bound, the method will return 'true'.
|
30
|
+
#
|
31
|
+
# If the value is between the two bounds, the method will first determine the probability
|
32
|
+
# of the value being true, based on a linear interpolation.
|
33
|
+
# For example:
|
34
|
+
#
|
35
|
+
# d = Dimension.new(0, 1)
|
36
|
+
# d.choose(0) # => false
|
37
|
+
# d.choose(1) # => true
|
38
|
+
# d.choose(0.5) #=> will be `true` in 50% of the cases (probability of 0.5)
|
39
|
+
# d.choose(0.1) #=> will be `true` in 10% of the cases (probability of 0.1)
|
40
|
+
#
|
41
|
+
# Primary use is for things like load balancing. Imagine you have a server handling 14 connections,
|
42
|
+
# and you know that it can take about 20 maximum. When you decide whether to send the connection
|
43
|
+
# number 17 to it, you want to take a little margin and only send that connection sometimes, to
|
44
|
+
# balance the choices - so you want to use a softer bound (a bit of a fuzzy logic).
|
45
|
+
#
|
46
|
+
# will_accept_connection = Dimension.new(20, 10) # ten connections is always safe
|
47
|
+
# will_accept_connection.choose(server.current_connection_count + 1) # will give you a fuzzy choice
|
48
|
+
#
|
49
|
+
# @param value[#to_f, Comparable] a value to be evaluated (must be coercible to a Float and Comparable)
|
50
|
+
# @return [Boolean] the chosen value based on probability and randomness
|
51
|
+
def choose(value)
|
52
|
+
choice = if !fuzzy?(value)
|
53
|
+
value >= @upper
|
54
|
+
else
|
55
|
+
# Interpolate the probability of the value being true
|
56
|
+
delta = @upper.to_f - @lower.to_f
|
57
|
+
v = (value - @lower).to_f
|
58
|
+
t = (v / delta)
|
59
|
+
|
60
|
+
r = Random.new # To override in tests if needed
|
61
|
+
r.rand < t
|
62
|
+
end
|
63
|
+
choice ^ @flip_sign
|
64
|
+
end
|
65
|
+
|
66
|
+
# Tells whether the evaluation will use the probabilities or not
|
67
|
+
# (whether the given value is within the range where probability evaluation will take place).
|
68
|
+
#
|
69
|
+
# @param value[Comparable] a value to be evaluated (must be comparable)
|
70
|
+
# @return [Boolean] whether choosing on this value will use probabilities or not
|
71
|
+
def fuzzy?(value)
|
72
|
+
value > @lower && value < @upper
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Performs an evaluation based on multiple dimensions. The dimensions
|
77
|
+
# will be first coerced into one (number of truthy evaluations vs. number of falsey evaluations)
|
78
|
+
# and then a true/false value will be deduced from that.
|
79
|
+
class ManyDimensions
|
80
|
+
def initialize(*dimensions)
|
81
|
+
@dimensions = dimensions
|
82
|
+
raise CardinalityError, "%s has no dimensions to evaluate" % inspect if @dimensions.empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Performs a weighted choice, by first collecting choice results from all the dimensions,
|
86
|
+
# and then by interpolating those results by the ratio of truthy values vs falsey values.
|
87
|
+
#
|
88
|
+
# x = Dimension.new(0,1)
|
89
|
+
# y = Dimension.new(0,1)
|
90
|
+
# z = Dimension.new(0,1)
|
91
|
+
#
|
92
|
+
# within_positive_3d_space = ManyDimensions.new(x, y, z)
|
93
|
+
# within_positive_3d_space.choose(-1, -1, -0.5) #=> false
|
94
|
+
# within_positive_3d_space.choose(1.1, 123, 1) #=> true
|
95
|
+
# within_positive_3d_space.choose(1, 0.5, 0.7) #=> true or false depending on 3 probabilities
|
96
|
+
#
|
97
|
+
# @param values[Array<Comparable,#to_f>] an array of values of the same size as the `dimensions` given to `initialize`
|
98
|
+
# @return [Boolean] true or false
|
99
|
+
def choose(*values)
|
100
|
+
if @dimensions.length != values.length
|
101
|
+
raise CardinalityError, "%s has %d dimensions but %d values were given" % [inspect, @dimensions.length, values.length]
|
102
|
+
end
|
103
|
+
|
104
|
+
evaluations = values.zip(@dimensions).map { |(v, d)| d.choose(v) }
|
105
|
+
num_truthy_choices = evaluations.select{|e| e}.length
|
106
|
+
|
107
|
+
Dimension.new(0, evaluations.length).choose(num_truthy_choices)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: rational_choice 1.0.0 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "rational_choice"
|
9
|
+
s.version = "1.0.0"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.authors = ["Julik Tarkhanov"]
|
14
|
+
s.date = "2015-12-06"
|
15
|
+
s.description = "Fuzzy logic gate"
|
16
|
+
s.email = "me@julik.nl"
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE.txt",
|
19
|
+
"README.md"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".rspec",
|
24
|
+
".travis.yml",
|
25
|
+
"Gemfile",
|
26
|
+
"LICENSE.txt",
|
27
|
+
"README.md",
|
28
|
+
"Rakefile",
|
29
|
+
"lib/rational_choice.rb",
|
30
|
+
"rational_choice.gemspec",
|
31
|
+
"spec/rational_choice/dimension_spec.rb",
|
32
|
+
"spec/rational_choice/many_dimensions_spec.rb",
|
33
|
+
"spec/rational_choice_spec.rb",
|
34
|
+
"spec/spec_helper.rb"
|
35
|
+
]
|
36
|
+
s.homepage = "https://github.com/wetransfer/rational_choice"
|
37
|
+
s.licenses = ["MIT"]
|
38
|
+
s.rubygems_version = "2.2.2"
|
39
|
+
s.summary = "Makes life-concerning choices based on an informed coin toss"
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
s.specification_version = 4
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_development_dependency(%q<yard>, [">= 0"])
|
46
|
+
s.add_development_dependency(%q<rspec>, ["~> 3.4.0"])
|
47
|
+
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
48
|
+
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
49
|
+
s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
52
|
+
s.add_dependency(%q<rspec>, ["~> 3.4.0"])
|
53
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
54
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
55
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
56
|
+
end
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<yard>, [">= 0"])
|
59
|
+
s.add_dependency(%q<rspec>, ["~> 3.4.0"])
|
60
|
+
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
61
|
+
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
62
|
+
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe 'RationalChoice::Dimension' do
|
4
|
+
describe '.new' do
|
5
|
+
it 'raises a DomainError if the bounds are the same' do
|
6
|
+
expect {
|
7
|
+
RationalChoice::Dimension.new(2,2)
|
8
|
+
}.to raise_error(/Bounds were the same at 2/)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'with values at or beyound the thresholds' do
|
13
|
+
it 'always evaluates to true for values above upper threshold' do
|
14
|
+
d = RationalChoice::Dimension.new(0, 1)
|
15
|
+
10_000.times do
|
16
|
+
expect(d.choose(1.1)).to eq(true)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'always evaluates to true for values at upper threshold' do
|
21
|
+
d = RationalChoice::Dimension.new(0, 1)
|
22
|
+
10_000.times do
|
23
|
+
expect(d.choose(1)).to eq(true)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'always evaluates to false for values at lower threshold' do
|
28
|
+
d = RationalChoice::Dimension.new(0, 1)
|
29
|
+
10_000.times do
|
30
|
+
expect(d.choose(0)).to eq(false)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'always evaluates to false for values below lower threshold' do
|
35
|
+
d = RationalChoice::Dimension.new(0, 1)
|
36
|
+
10_000.times do
|
37
|
+
expect(d.choose(-0.0001)).to eq(false)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe 'with values between thresholds creates a sensible choice distribution' do
|
43
|
+
it 'for 0.5 on a continuum from 0 to 1' do
|
44
|
+
d = RationalChoice::Dimension.new(0, 1)
|
45
|
+
expect(d).to be_fuzzy(0.5)
|
46
|
+
|
47
|
+
trues = 0
|
48
|
+
|
49
|
+
n_evaluations = 100_000
|
50
|
+
n_evaluations.times { trues += 1 if d.choose(0.5) }
|
51
|
+
|
52
|
+
expect(trues).to be_within(1000).of(n_evaluations / 2)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'for 0.1 on a continuum from 0 to 1' do
|
56
|
+
d = RationalChoice::Dimension.new(0, 1)
|
57
|
+
expect(d).to be_fuzzy(0.1)
|
58
|
+
|
59
|
+
trues = 0
|
60
|
+
|
61
|
+
n_evaluations = 100_000
|
62
|
+
n_evaluations.times { trues += 1 if d.choose(0.1) }
|
63
|
+
|
64
|
+
expect(trues).to be_within(1000).of(n_evaluations / 10)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'for 0.5 on a continuum from 1 to 0' do
|
68
|
+
d = RationalChoice::Dimension.new(1, 0)
|
69
|
+
expect(d).to be_fuzzy(0.5)
|
70
|
+
|
71
|
+
falses = 0
|
72
|
+
n_evaluations = 100_000
|
73
|
+
n_evaluations.times { falses += 1 if !d.choose(0.5) }
|
74
|
+
|
75
|
+
expect(falses).to be_within(1000).of(n_evaluations / 2)
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'for 0.1 on a continuum from 1 to 0' do
|
79
|
+
d = RationalChoice::Dimension.new(1, 0)
|
80
|
+
expect(d).to be_fuzzy(0.1)
|
81
|
+
|
82
|
+
trues = 0
|
83
|
+
|
84
|
+
n_evaluations = 100_000
|
85
|
+
n_evaluations.times { trues += 1 if d.choose(0.1) }
|
86
|
+
|
87
|
+
expect(trues).to be_within(1000).of(n_evaluations / 10 * 9) # Over 90% of the choices must be trues
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'for -0.5 on a continuum from -1 to 0' do
|
91
|
+
d = RationalChoice::Dimension.new(-1, 0)
|
92
|
+
expect(d).to be_fuzzy(-0.5)
|
93
|
+
|
94
|
+
trues = 0
|
95
|
+
n_evaluations = 100_000
|
96
|
+
n_evaluations.times { trues += 1 if d.choose(-0.5) }
|
97
|
+
|
98
|
+
expect(trues).to be_within(1000).of(n_evaluations / 2)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'for -0.1 on a continuum from -1 to 0' do
|
102
|
+
d = RationalChoice::Dimension.new(-1, 0)
|
103
|
+
expect(d).to be_fuzzy(-0.1)
|
104
|
+
|
105
|
+
trues = 0
|
106
|
+
|
107
|
+
n_evaluations = 100_000
|
108
|
+
n_evaluations.times { trues += 1 if d.choose(-0.1) }
|
109
|
+
|
110
|
+
expect(trues).to be_within(1000).of(n_evaluations / 10 * 9) # Over 90% of the choices must be trues
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'for -0.5 on a continuum from 0 to -1' do
|
114
|
+
d = RationalChoice::Dimension.new(0, -1)
|
115
|
+
expect(d).to be_fuzzy(-0.5)
|
116
|
+
|
117
|
+
trues = 0
|
118
|
+
n_evaluations = 100_000
|
119
|
+
n_evaluations.times { trues += 1 if d.choose(-0.5) }
|
120
|
+
|
121
|
+
expect(trues).to be_within(1000).of(n_evaluations / 2)
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'for -0.1 on a continuum from 0 to -1' do
|
125
|
+
d = RationalChoice::Dimension.new(0, -1)
|
126
|
+
expect(d).to be_fuzzy(-0.1)
|
127
|
+
|
128
|
+
trues = 0
|
129
|
+
|
130
|
+
n_evaluations = 100_000
|
131
|
+
n_evaluations.times { trues += 1 if d.choose(-0.1) }
|
132
|
+
|
133
|
+
expect(trues).to be_within(1000).of(n_evaluations / 10) # Over 90% of the choices must be trues
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
|
3
|
+
describe 'RationalChoice::ManyDimensions' do
|
4
|
+
describe '.new' do
|
5
|
+
it 'raises a CardinalityError when no dimensions are given' do
|
6
|
+
expect {
|
7
|
+
RationalChoice::ManyDimensions.new
|
8
|
+
}.to raise_error(RationalChoice::CardinalityError)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '.choose' do
|
13
|
+
it 'raises a CardinalityError if the number of values does not match the number of dimensions' do
|
14
|
+
md = RationalChoice::ManyDimensions.new(nil, nil, nil)
|
15
|
+
expect {
|
16
|
+
md.choose(1, 2)
|
17
|
+
}.to raise_error(RationalChoice::CardinalityError)
|
18
|
+
end
|
19
|
+
|
20
|
+
let(:md) {
|
21
|
+
one_to_zero = RationalChoice::Dimension.new(0, 1)
|
22
|
+
RationalChoice::ManyDimensions.new(one_to_zero, one_to_zero, one_to_zero)
|
23
|
+
}
|
24
|
+
|
25
|
+
it 'returns "true" when all dimensions are at or above upper bound' do
|
26
|
+
10_000.times { expect(md.choose(1,1,1)).to eq(true) }
|
27
|
+
10_000.times { expect(md.choose(2,2,2)).to eq(true) }
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'returns "false" when all dimensions are at or below lower bound' do
|
31
|
+
10_000.times { expect(md.choose(0,0,0)).to eq(false) }
|
32
|
+
10_000.times { expect(md.choose(-1,-1,-1)).to eq(false) }
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'returns "true" in approximately 50% of the cases when all the values are at 0.5' do
|
36
|
+
truthy = 0
|
37
|
+
10_000.times { truthy += 1 if md.choose(0.5, 0.5, 0.5) }
|
38
|
+
expect(truthy).to be_within(100).of(10_000 / 2)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns "true" in approximately 10% of the cases when all the values are at 0.1' do
|
42
|
+
truthy = 0
|
43
|
+
10_000.times { truthy += 1 if md.choose(0.1, 0.1, 0.1) }
|
44
|
+
expect(truthy).to be_within(100).of(10_000 / 10)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'creates a very uniform distribution of values with random values across the board' do
|
48
|
+
truthy = 0
|
49
|
+
10_000.times { truthy += 1 if md.choose(rand, rand, rand) } # default rand() is 0..1
|
50
|
+
expect(truthy).to be_within(100).of(10_000 / 2)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rational_choice
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Julik Tarkhanov
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-12-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: yard
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.4.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.4.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rdoc
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.12'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: jeweler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.0.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.0.1
|
83
|
+
description: Fuzzy logic gate
|
84
|
+
email: me@julik.nl
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files:
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.md
|
90
|
+
files:
|
91
|
+
- ".document"
|
92
|
+
- ".rspec"
|
93
|
+
- ".travis.yml"
|
94
|
+
- Gemfile
|
95
|
+
- LICENSE.txt
|
96
|
+
- README.md
|
97
|
+
- Rakefile
|
98
|
+
- lib/rational_choice.rb
|
99
|
+
- rational_choice.gemspec
|
100
|
+
- spec/rational_choice/dimension_spec.rb
|
101
|
+
- spec/rational_choice/many_dimensions_spec.rb
|
102
|
+
- spec/rational_choice_spec.rb
|
103
|
+
- spec/spec_helper.rb
|
104
|
+
homepage: https://github.com/wetransfer/rational_choice
|
105
|
+
licenses:
|
106
|
+
- MIT
|
107
|
+
metadata: {}
|
108
|
+
post_install_message:
|
109
|
+
rdoc_options: []
|
110
|
+
require_paths:
|
111
|
+
- lib
|
112
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0'
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: '0'
|
122
|
+
requirements: []
|
123
|
+
rubyforge_project:
|
124
|
+
rubygems_version: 2.2.2
|
125
|
+
signing_key:
|
126
|
+
specification_version: 4
|
127
|
+
summary: Makes life-concerning choices based on an informed coin toss
|
128
|
+
test_files: []
|