rational_choice 2.0.1 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/rational_choice.rb +12 -7
- data/spec/rational_choice/dimension_spec.rb +8 -0
- data/spec/rational_choice/many_dimensions_spec.rb +11 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7ae25e7a180ad02a6837d0e085639330ad54577f880211ea5ed9b8bdcbfd24ad
|
4
|
+
data.tar.gz: 9415891d43f63156400471ab1ff71dc388cc76fa30943e5c2dfaf4337cb07d81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 598b15197987bf19787f9affafde489cf2fbb8b8b0260b36b16f93d9d23eee53d10548170dc719c4ef49e37d12568a151e2854c187708013bb99a77678b7f0a1
|
7
|
+
data.tar.gz: d6ffe67d2879780a6418fc722bc19a9bfd70b9def2fbd83826dccc077b4ebef54f47531400a6c93e35a9ea8565e2bd03edb24e8f4175d413d5568189f6ac9a1d
|
data/lib/rational_choice.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Tiny fuzzy-logic gate for making choices based on a continuum of permitted values
|
2
2
|
# as opposed to a hard condition.
|
3
3
|
module RationalChoice
|
4
|
-
VERSION = '2.0
|
4
|
+
VERSION = '2.1.0'
|
5
5
|
|
6
6
|
# Gets raised when a multidimensional choice has to be made with a wrong number
|
7
7
|
# of values versus the number of dimensions
|
@@ -16,9 +16,11 @@ module RationalChoice
|
|
16
16
|
#
|
17
17
|
# @param false_at_or_below[#to_f, #<=>] the lower bound, at or below which the value will be considered false
|
18
18
|
# @param true_at_or_above[#to_f, #<=>] the upper bound, at or above which the value will be considered true
|
19
|
-
|
19
|
+
# @param random[Random] the RNG, defaults to a new Random
|
20
|
+
def initialize(false_at_or_below:, true_at_or_above:, random: Random.new)
|
20
21
|
raise DomainError, "Bounds were the same at #{false_at_or_below}" if false_at_or_below == true_at_or_above
|
21
22
|
|
23
|
+
@random = random
|
22
24
|
@lower, @upper = [false_at_or_below, true_at_or_above].sort
|
23
25
|
@flip_sign = [@lower, @upper].sort != [false_at_or_below, true_at_or_above]
|
24
26
|
end
|
@@ -55,9 +57,7 @@ module RationalChoice
|
|
55
57
|
delta = @upper.to_f - @lower.to_f
|
56
58
|
v = (value - @lower).to_f
|
57
59
|
t = (v / delta)
|
58
|
-
|
59
|
-
r = Random.new # To override in tests if needed
|
60
|
-
r.rand < t
|
60
|
+
@random.rand < t
|
61
61
|
else
|
62
62
|
# just seen where it is (below or above)
|
63
63
|
value >= @upper
|
@@ -79,8 +79,13 @@ module RationalChoice
|
|
79
79
|
# will be first coerced into one (number of truthy evaluations vs. number of falsey evaluations)
|
80
80
|
# and then a true/false value will be deduced from that.
|
81
81
|
class ManyDimensions
|
82
|
-
|
82
|
+
# Initializes a new Dimension to evaluate values
|
83
|
+
#
|
84
|
+
# @param dimensions[Array<Dimension>] the dimensions to make a choice over
|
85
|
+
# @param random[Random] the RNG, defaults to a new Random
|
86
|
+
def initialize(*dimensions, random: Random.new)
|
83
87
|
@dimensions = dimensions
|
88
|
+
@random = random
|
84
89
|
raise CardinalityError, '%s has no dimensions to evaluate' % inspect if @dimensions.empty?
|
85
90
|
end
|
86
91
|
|
@@ -106,7 +111,7 @@ module RationalChoice
|
|
106
111
|
evaluations = values.zip(@dimensions).map { |(v, d)| d.choose(v) }
|
107
112
|
num_truthy_choices = evaluations.select { |e| e }.length
|
108
113
|
|
109
|
-
Dimension.new(false_at_or_below: 0, true_at_or_above: evaluations.length).choose(num_truthy_choices)
|
114
|
+
Dimension.new(false_at_or_below: 0, true_at_or_above: evaluations.length, random: @random).choose(num_truthy_choices)
|
110
115
|
end
|
111
116
|
end
|
112
117
|
end
|
@@ -39,6 +39,14 @@ describe 'RationalChoice::Dimension' do
|
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
42
|
+
describe 'with a given Random' do
|
43
|
+
it 'creates a predictable sequence of choices' do
|
44
|
+
d = RationalChoice::Dimension.new(false_at_or_below: 0, true_at_or_above: 1, random: Random.new(42))
|
45
|
+
choices = (1..10).map { d.choose(0.5) }
|
46
|
+
expect(choices).to eq([true, false, false, false, true, true, true, false, false, false])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
42
50
|
describe 'with values between thresholds creates a sensible choice distribution' do
|
43
51
|
it 'for 0.5 on a continuum from 0 to 1' do
|
44
52
|
d = RationalChoice::Dimension.new(false_at_or_below: 0, true_at_or_above: 1)
|
@@ -22,6 +22,16 @@ describe 'RationalChoice::ManyDimensions' do
|
|
22
22
|
RationalChoice::ManyDimensions.new(one_to_zero, one_to_zero, one_to_zero)
|
23
23
|
}
|
24
24
|
|
25
|
+
it 'accepts a custom seed and uses it to generate predictable choices' do
|
26
|
+
r = Random.new(42)
|
27
|
+
d1 = RationalChoice::Dimension.new(false_at_or_below: 0, true_at_or_above: 1, random: r)
|
28
|
+
d2 = RationalChoice::Dimension.new(false_at_or_below: 0, true_at_or_above: 1, random: r)
|
29
|
+
d3 = RationalChoice::Dimension.new(false_at_or_below: 0, true_at_or_above: 1, random: r)
|
30
|
+
multi_d = RationalChoice::ManyDimensions.new(d1, d2, d3, random: r)
|
31
|
+
choices = (1..10).map { multi_d.choose(0.5, 0.2, 0.6) }
|
32
|
+
expect(choices).to eq([false, true, false, true, true, false, true, true, true, false])
|
33
|
+
end
|
34
|
+
|
25
35
|
it 'returns "true" when all dimensions are at or above upper bound' do
|
26
36
|
10_000.times { expect(md.choose(1, 1, 1)).to eq(true) }
|
27
37
|
10_000.times { expect(md.choose(2, 2, 2)).to eq(true) }
|
@@ -35,7 +45,7 @@ describe 'RationalChoice::ManyDimensions' do
|
|
35
45
|
it 'returns "true" in approximately 50% of the cases when all the values are at 0.5' do
|
36
46
|
truthy = 0
|
37
47
|
10_000.times { truthy += 1 if md.choose(0.5, 0.5, 0.5) }
|
38
|
-
expect(truthy).to be_within(
|
48
|
+
expect(truthy).to be_within(200).of(10_000 / 2)
|
39
49
|
end
|
40
50
|
|
41
51
|
it 'returns "true" in approximately 10% of the cases when all the values are at 0.1' do
|