rational_choice 2.0.1 → 2.1.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.
- 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
|