rugal 1.0.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 +7 -0
- data/lib/genetic_algorithm.rb +110 -0
- data/lib/operators.rb +295 -0
- data/lib/rugal.rb +179 -0
- data/lib/solution.rb +181 -0
- metadata +47 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 96e1028fc4474ef75947b5ebf8651de4069c16a8ed9c866f87fb9069e88e6dbc
|
4
|
+
data.tar.gz: ce64b85b190763f57d1e82883bba6ad02c1473e3862831bbe7fe545be9bf19b7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e7e625f0dadad4179733793b23e10fd0c1dd01d2a97bb19b0d4b5c8147a77234720ad0994ae8d2a8e02f1d67b91096c937c22267e62eb6e85618b547f3223b3b
|
7
|
+
data.tar.gz: 073ff9d28d33a60082b070758e10742bad476366e49669228147d8420a87b8bd79d08a529359ecc9d9f5c0233e625ec79fd801f9654a75f04de62428912bb07c
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require_relative "operators"
|
2
|
+
require_relative "rugal"
|
3
|
+
require_relative "solution"
|
4
|
+
|
5
|
+
module GA
|
6
|
+
class Algorithm
|
7
|
+
def initialize(factory, options={})
|
8
|
+
@factory = factory
|
9
|
+
|
10
|
+
@selection = options[:selection] || GA::Operators::Selection::RouletteWheel.new
|
11
|
+
@crossover = options[:crossover] || GA::Operators::Crossover::SinglePoint.new
|
12
|
+
@mutation = options[:mutation] || GA::Operators::Mutation::Uniform.new
|
13
|
+
@operators = options[:operators] || []
|
14
|
+
|
15
|
+
@population_size = options[:population_size] || 100
|
16
|
+
@elite = options[:elite] || 0.1
|
17
|
+
|
18
|
+
GA.contract("Factory must not be nil") { factory }
|
19
|
+
GA.contract("Factory must be a SolutionFactory") { factory.is_a?(GA::Solutions::SolutionFactory) }
|
20
|
+
GA.contract("Elite must be non-negative (#@elite given)") { @elite >= 0 }
|
21
|
+
GA.contract("All the additional operators must be Operator") { @operators.all? { |op| op.is_a? GA::Operators::Operator } }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class ClassicGenetic < Algorithm
|
26
|
+
def initialize(factory, options={})
|
27
|
+
super
|
28
|
+
end
|
29
|
+
|
30
|
+
def maximize(iterations, options = {}, &fitness)
|
31
|
+
optimize(iterations, options, &fitness)
|
32
|
+
end
|
33
|
+
|
34
|
+
def minimize(iterations, options = {}, &fitness)
|
35
|
+
optimize(iterations, options) { |chromosome| -fitness.call(chromosome) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def optimize(iterations, options = {}, &fitness)
|
39
|
+
GA.contract("You must pass the fitness function as a block") { fitness }
|
40
|
+
optimum = options[:optimum] || nil
|
41
|
+
|
42
|
+
GA.contract("Iterations must be more than 0") { iterations & iterations > 0 }
|
43
|
+
GA.contract("Optimum must be numeric (or nil)") { !optimum || optimum.is_a?(Numeric) }
|
44
|
+
|
45
|
+
population = @factory.population(@population_size)
|
46
|
+
|
47
|
+
elite = []
|
48
|
+
iterations.times do |i|
|
49
|
+
GA::Logger.info "Generation #{i}"
|
50
|
+
population.each { |chromosome| chromosome.fitness = fitness.call(chromosome) }
|
51
|
+
|
52
|
+
population = population - population.min(elite.size) { |chromosome| chromosome.fitness } + elite
|
53
|
+
|
54
|
+
elite = population.max(@elite * @population_size) { |chromosome| chromosome.fitness }
|
55
|
+
if optimum
|
56
|
+
return elite[0] if elite[0].fitness == optimum
|
57
|
+
end
|
58
|
+
|
59
|
+
population = @selection.apply(population)
|
60
|
+
population = @crossover.apply(population)
|
61
|
+
population = @mutation.apply(population)
|
62
|
+
|
63
|
+
# Uses additional operators
|
64
|
+
@operators.each do |operator|
|
65
|
+
population = operator.apply(population)
|
66
|
+
end
|
67
|
+
|
68
|
+
GA::Logger.info { "Average fitness: #{population.map { |c| c.fitness }.sum.to_f / population.size}" }
|
69
|
+
end
|
70
|
+
|
71
|
+
return population.max { |chromosome| fitness.call(chromosome) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
class RandomSearch
|
76
|
+
def initialize(factory, options = {})
|
77
|
+
@factory = factory
|
78
|
+
@population_size = options[:population_size] || 1
|
79
|
+
end
|
80
|
+
|
81
|
+
def maximize(iterations, options = {}, &fitness)
|
82
|
+
optimize(iterations, options, &fitness)
|
83
|
+
end
|
84
|
+
|
85
|
+
def minimize(iterations, options = {}, &fitness)
|
86
|
+
optimize(iterations, options) { |chromosome| -fitness.call(chromosome) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def optimize(iterations, options = {}, &fitness)
|
90
|
+
optimum = options[:optimum] || nil
|
91
|
+
|
92
|
+
GA.contract("Iterations must be more than 0") { iterations > 0 }
|
93
|
+
GA.contract("Optimum must be numeric (or nil)") { !optimum || optimum.is_a?(Numeric) }
|
94
|
+
|
95
|
+
best = nil
|
96
|
+
iterations.times do |i|
|
97
|
+
GA::Logger.info "Generation #{i}"
|
98
|
+
population = @factory.population(@population_size)
|
99
|
+
population.each { |chromosome| chromosome.fitness = fitness.call(chromosome) }
|
100
|
+
|
101
|
+
current_best = population.max { |chromosome| chromosome.fitness}
|
102
|
+
best = current_best if !best || current_best.fitness > best.fitness
|
103
|
+
|
104
|
+
GA::Logger.info { "Average fitness: #{population.map { |c| c.fitness }.sum.to_f / population.size}" }
|
105
|
+
end
|
106
|
+
|
107
|
+
return best
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/operators.rb
ADDED
@@ -0,0 +1,295 @@
|
|
1
|
+
require_relative "genetic_algorithm"
|
2
|
+
require_relative "rugal"
|
3
|
+
require_relative "solution"
|
4
|
+
|
5
|
+
module GA::Operators
|
6
|
+
class Operator
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module GA::Operators::Selection
|
11
|
+
class Basic < GA::Operators::Operator
|
12
|
+
end
|
13
|
+
|
14
|
+
class RouletteWheel < Basic
|
15
|
+
def apply(population)
|
16
|
+
min_fitness = population.map(&:fitness).min.abs + 1
|
17
|
+
fitnesses = population.map { |chromosome| chromosome.fitness != 0 ? chromosome.fitness + min_fitness : 0}
|
18
|
+
|
19
|
+
total_fitness = fitnesses.sum
|
20
|
+
|
21
|
+
return population.dup if total_fitness == 0
|
22
|
+
|
23
|
+
current_total = 0.0
|
24
|
+
selection_array = []
|
25
|
+
fitnesses.each do |fitness|
|
26
|
+
current_total += fitness.to_f / total_fitness
|
27
|
+
selection_array.push current_total
|
28
|
+
end
|
29
|
+
|
30
|
+
offspring = []
|
31
|
+
population.size.times do
|
32
|
+
value = GA::Random.rand
|
33
|
+
selected_index = selection_array.index { |v| value <= v }
|
34
|
+
offspring.push population[selected_index].dup
|
35
|
+
end
|
36
|
+
|
37
|
+
return offspring
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Random < Basic
|
42
|
+
def apply(population)
|
43
|
+
offspring = []
|
44
|
+
population.size.times do
|
45
|
+
index = GA::Random.rand(population.size)
|
46
|
+
offspring.push population[index].dup
|
47
|
+
end
|
48
|
+
return offspring
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module GA::Operators::Crossover
|
54
|
+
class Basic < GA::Operators::Operator
|
55
|
+
def initialize(options = {})
|
56
|
+
@forbidden = []
|
57
|
+
end
|
58
|
+
|
59
|
+
protected
|
60
|
+
def crossover(population)
|
61
|
+
result = []
|
62
|
+
|
63
|
+
shuffled = GA::Random.shuffle(population)
|
64
|
+
shuffled.each_slice(2) do |c1, c2|
|
65
|
+
crossed = yield(c1, c2)
|
66
|
+
result += crossed
|
67
|
+
end
|
68
|
+
|
69
|
+
return result
|
70
|
+
end
|
71
|
+
|
72
|
+
def forbid(type)
|
73
|
+
@forbidden.push type
|
74
|
+
end
|
75
|
+
|
76
|
+
def apply(population)
|
77
|
+
check_population(population)
|
78
|
+
end
|
79
|
+
|
80
|
+
def check_population(population)
|
81
|
+
GA.contract("#{self.class} crossover does not accept #{@forbidden.join(", ")}") { population.none? { |c| @forbidden.include?(c.class) } }
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class SinglePoint < Basic
|
86
|
+
def initialize(options = {})
|
87
|
+
super
|
88
|
+
|
89
|
+
@probability = options[:probability] || 0.8
|
90
|
+
GA.contract("Probability must be in the range [0, 1]") { @probability.between?(0, 1) }
|
91
|
+
|
92
|
+
forbid GA::Solutions::PermutationSolution
|
93
|
+
end
|
94
|
+
|
95
|
+
def apply(population)
|
96
|
+
super
|
97
|
+
|
98
|
+
crossover(population) do |c1, c2|
|
99
|
+
c1 = c1.dup
|
100
|
+
c2 = c2.dup
|
101
|
+
|
102
|
+
unless c2
|
103
|
+
[c1]
|
104
|
+
else
|
105
|
+
if GA::Random.with_probability(@probability)
|
106
|
+
crossover_position = GA::Random.rand([c1.size, c2.size].min)
|
107
|
+
|
108
|
+
for i in 0...crossover_position
|
109
|
+
c1.data[i], c2.data[i] = c2.data[i], c1.data[i]
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
[c1, c2]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
class PMX < Basic
|
120
|
+
def initialize(options = {})
|
121
|
+
super
|
122
|
+
|
123
|
+
@probability = options[:probability] || 0.8
|
124
|
+
@percent = options[:size] || 0.5
|
125
|
+
|
126
|
+
GA.contract("Probability must be in the range [0, 1]") { @probability.between?(0, 1) }
|
127
|
+
GA.contract("Swath size percentage must be in the range (0, 1)") { @percent > 0 && @percent < 1 }
|
128
|
+
|
129
|
+
forbid GA::Solutions::IntegerSolution
|
130
|
+
forbid GA::Solutions::FloatSolution
|
131
|
+
forbid GA::Solutions::StringSolution
|
132
|
+
end
|
133
|
+
|
134
|
+
def apply(population)
|
135
|
+
super
|
136
|
+
|
137
|
+
crossover(population) do |c1, c2|
|
138
|
+
unless c2
|
139
|
+
[c1]
|
140
|
+
else
|
141
|
+
t1 = c1
|
142
|
+
t2 = c2
|
143
|
+
if GA::Random.with_probability(@probability)
|
144
|
+
size = GA::Random.rand_int(@percent * c1.size)
|
145
|
+
from = GA::Random.rand_int(c1.size - size)
|
146
|
+
to = from + size
|
147
|
+
|
148
|
+
c1, c2 = pmx(t1, t2, from, to)
|
149
|
+
end
|
150
|
+
|
151
|
+
[c1, c2]
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
def pmx(c1, c2, from, to)
|
158
|
+
range = from...to
|
159
|
+
|
160
|
+
child1 = c1.dup
|
161
|
+
child2 = c2.dup
|
162
|
+
|
163
|
+
swath1 = child1.data[range]
|
164
|
+
swath2 = child2.data[range]
|
165
|
+
|
166
|
+
uswath1 = swath1 - swath2
|
167
|
+
uswath2 = swath2 - swath1
|
168
|
+
|
169
|
+
mapping1to2 = {}
|
170
|
+
mapping2to1 = {}
|
171
|
+
for i in 0...uswath1.size
|
172
|
+
mapping1to2[uswath1[i]] = uswath2[i]
|
173
|
+
mapping2to1[uswath2[i]] = uswath1[i]
|
174
|
+
end
|
175
|
+
|
176
|
+
child1.data[range], child2.data[range] = child2.data[range], child1.data[range]
|
177
|
+
|
178
|
+
changed_indices = ((0...child1.size).to_a - range.to_a)
|
179
|
+
for i in changed_indices
|
180
|
+
child1.data[i] = mapping2to1[child1.data[i]] if swath2.include?(child1.data[i])
|
181
|
+
child2.data[i] = mapping1to2[child2.data[i]] if swath1.include?(child2.data[i])
|
182
|
+
end
|
183
|
+
|
184
|
+
# for i in ((0...child1.size).to_a - range.to_a)
|
185
|
+
# v = child2.data[i]
|
186
|
+
#
|
187
|
+
# if swath1.include?(v)
|
188
|
+
# rep = swath2[swath1.index(v)]
|
189
|
+
# child2.data[i] = rep
|
190
|
+
# end
|
191
|
+
# end
|
192
|
+
|
193
|
+
GA.postcondition("All data in the permutation must be used: #{child1.to_s}") { child1.valid? }
|
194
|
+
GA.postcondition("All data in the permutation must be used: #{child2.to_s}") { child2.valid? }
|
195
|
+
|
196
|
+
return child1, child2
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
module GA::Operators::Mutation
|
202
|
+
class Basic < GA::Operators::Operator
|
203
|
+
protected
|
204
|
+
def mutation(population)
|
205
|
+
result = []
|
206
|
+
|
207
|
+
population.each do |chromosome|
|
208
|
+
result.push yield(chromosome)
|
209
|
+
end
|
210
|
+
|
211
|
+
return result
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
class Uniform < Basic
|
216
|
+
def initialize(options = {})
|
217
|
+
@probability = options[:probability] || 0.01
|
218
|
+
@radius = options[:radius] || 1
|
219
|
+
@epsilon = options[:epsilon] || 0.01
|
220
|
+
|
221
|
+
@epsilon = @epsilon.to_f
|
222
|
+
|
223
|
+
GA.contract("Probability must be in the range [0, 1]") { @probability.between?(0, 1) }
|
224
|
+
GA.contract("Epsilon must be greater than 0") { @epsilon > 0 }
|
225
|
+
end
|
226
|
+
|
227
|
+
def apply(population)
|
228
|
+
mutation(population) do |chromosome|
|
229
|
+
if GA::Random.with_probability(@probability)
|
230
|
+
chromosome = chromosome.mutate(GA::Random.rand(chromosome.size)) do |v|
|
231
|
+
GA::Random.random_sign * [GA::Random.rand * @radius, @epsilon].max
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
chromosome
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
class Positional < Basic
|
241
|
+
def initialize(options = {})
|
242
|
+
@probability = options[:probability] || 0.01
|
243
|
+
@radius = options[:radius] || 1
|
244
|
+
@epsilon = options[:epsilon] || 1
|
245
|
+
|
246
|
+
GA.contract("Probability must be in the range [0, 1]") { @probability.between?(0, 1) }
|
247
|
+
GA.contract("Epsilon must be greater than 0") { @epsilon > 0 }
|
248
|
+
end
|
249
|
+
|
250
|
+
def apply(population)
|
251
|
+
mutation(population) do |chromosome|
|
252
|
+
if GA::Random.with_probability(@probability)
|
253
|
+
chromosome = chromosome.dup
|
254
|
+
|
255
|
+
i1 = GA::Random.rand(chromosome.size)
|
256
|
+
i2 = (i1 + [GA::Random.rand(@epsilon+1), @epsilon].max) % chromosome.size
|
257
|
+
|
258
|
+
chromosome.data[i1], chromosome.data[i2] = chromosome.data[i2], chromosome.data[i1]
|
259
|
+
end
|
260
|
+
|
261
|
+
chromosome
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
module GA::Operators::Additional
|
268
|
+
class Injector < GA::Operators::Operator
|
269
|
+
def initialize(factory, options = {})
|
270
|
+
@factory = factory
|
271
|
+
|
272
|
+
@probability = options[:probability] || 0.01
|
273
|
+
@size = options[:size] || 1
|
274
|
+
|
275
|
+
GA.contract("Factory cannot be nil") { @factory }
|
276
|
+
GA.contract("Factory must be a SolutionFactory") { @factory.is_a?(GA::Solutions::SolutionFactory) }
|
277
|
+
GA.contract("Probability must be in the range [0, 1]") { @probability.between?(0, 1) }
|
278
|
+
GA.contract("Size must be greater than 0") { @size > 0 }
|
279
|
+
end
|
280
|
+
|
281
|
+
def apply(population)
|
282
|
+
if GA::Random.with_probability @probability
|
283
|
+
population = population.dup
|
284
|
+
size = [GA::Random.rand(@size + 1), 1].max
|
285
|
+
|
286
|
+
to_remove = population.sample size
|
287
|
+
|
288
|
+
population -= to_remove
|
289
|
+
population += @factory.population size
|
290
|
+
end
|
291
|
+
|
292
|
+
return population
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
data/lib/rugal.rb
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
module Math
|
2
|
+
Random = Random
|
3
|
+
end
|
4
|
+
|
5
|
+
module GA
|
6
|
+
module Random
|
7
|
+
@@random = Math::Random.new
|
8
|
+
|
9
|
+
def self.set_seed(i)
|
10
|
+
@@random = Math::Random.new i
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.random
|
14
|
+
@@random
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.shuffle(array)
|
18
|
+
GA.contract("Shuffle needs an array") { array.is_a?(Array) }
|
19
|
+
|
20
|
+
return array.shuffle(random: @@random)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.rand(*args)
|
24
|
+
@@random.rand *args
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.rand_int(*args)
|
28
|
+
self.rand(*args).to_i
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.with_probability(probability)
|
32
|
+
GA.contract("The probability must be in the range [0, 1] (#{probability} given)") { probability.between?(0, 1) }
|
33
|
+
|
34
|
+
return self.rand <= probability
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.random_sign
|
38
|
+
value = self.rand(2)
|
39
|
+
|
40
|
+
return value == 0 ? -1 : 1
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module Logger
|
45
|
+
LEVEL_DEBUG = 0
|
46
|
+
LEVEL_INFO = 1
|
47
|
+
LEVEL_WARNING = 2
|
48
|
+
LEVEL_SEVERE = 3
|
49
|
+
|
50
|
+
@@level = 0
|
51
|
+
def self.debug(*data, &block)
|
52
|
+
return if @@level > LEVEL_DEBUG
|
53
|
+
self.log "DEBUG", *data, &block
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.info(*data, &block)
|
57
|
+
return if @@level > LEVEL_INFO
|
58
|
+
self.log "INFO", *data, &block
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.warning(*data, &block)
|
62
|
+
return if @@level > LEVEL_WARNING
|
63
|
+
self.log "WARNING", *data, &block
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.severe(*data, &block)
|
67
|
+
return if @@level > LEVEL_SEVERE
|
68
|
+
self.log "SEVERE", *data, &block
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.level=(level)
|
72
|
+
GA.contract("The logger level must be between 0 and 3 (#{level} given)") { level.between?(0, 3) }
|
73
|
+
@@level = level
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def self.log(type, *data, &block)
|
78
|
+
if block_given?
|
79
|
+
d = block.call
|
80
|
+
puts "#{type}: #{d}"
|
81
|
+
else
|
82
|
+
data.each do |d|
|
83
|
+
puts "#{type}: #{d}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.profile(profile)
|
90
|
+
case profile
|
91
|
+
when :development
|
92
|
+
@@assumptions_checking = :hard
|
93
|
+
GA::Logger.level = GA::Logger::LEVEL_DEBUG
|
94
|
+
|
95
|
+
when :testing
|
96
|
+
@@assumptions_checking = :hard
|
97
|
+
GA::Logger.level = GA::Logger::LEVEL_INFO
|
98
|
+
|
99
|
+
when :production
|
100
|
+
@@assumptions_checking = :soft
|
101
|
+
GA::Logger.level = GA::Logger::LEVEL_WARNING
|
102
|
+
|
103
|
+
when :stable_production
|
104
|
+
@@assumptions_checking = false
|
105
|
+
GA::Logger.level = GA::Logger::LEVEL_SEVERE
|
106
|
+
|
107
|
+
else
|
108
|
+
raise "Illegal profile #{profile}"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class ContractException < StandardError; end
|
113
|
+
class PostConditionException < StandardError; end
|
114
|
+
def self.contract(message = "Contract violated", condition = false, &block)
|
115
|
+
return unless @@assumptions_checking
|
116
|
+
|
117
|
+
raise TypeError, "no implicit conversion of #{message.class} into String" unless message.is_a? String
|
118
|
+
|
119
|
+
any_problem = false
|
120
|
+
if block_given?
|
121
|
+
any_problem = true unless yield
|
122
|
+
else
|
123
|
+
any_problem = true unless condition
|
124
|
+
end
|
125
|
+
|
126
|
+
if any_problem
|
127
|
+
case @@assumptions_checking
|
128
|
+
when :hard
|
129
|
+
raise ContractException, message
|
130
|
+
when :soft
|
131
|
+
GA::Logger.severe(message)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.postcondition(message = "Postcondition violated", condition = false, &block)
|
137
|
+
return unless @@assumptions_checking
|
138
|
+
|
139
|
+
raise TypeError, "no implicit conversion of #{message.class} into String" unless message.is_a? String
|
140
|
+
|
141
|
+
any_problem = false
|
142
|
+
if block_given?
|
143
|
+
any_problem = true unless yield
|
144
|
+
else
|
145
|
+
any_problem = true unless condition
|
146
|
+
end
|
147
|
+
|
148
|
+
if any_problem
|
149
|
+
case @@assumptions_checking
|
150
|
+
when :hard
|
151
|
+
raise ContractException, message
|
152
|
+
when :soft
|
153
|
+
GA::Logger.severe(message)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
GA.profile :testing
|
159
|
+
end
|
160
|
+
|
161
|
+
def gadebug(*data)
|
162
|
+
GA::Logger.debug(*data)
|
163
|
+
end
|
164
|
+
|
165
|
+
def gainfo(*data)
|
166
|
+
GA::Logger.info(*data)
|
167
|
+
end
|
168
|
+
|
169
|
+
def gawarn(*data)
|
170
|
+
GA::Logger.warning(*data)
|
171
|
+
end
|
172
|
+
|
173
|
+
def gasevere(*data)
|
174
|
+
GA::Logger.severe(*data)
|
175
|
+
end
|
176
|
+
|
177
|
+
require_relative "genetic_algorithm"
|
178
|
+
require_relative "operators"
|
179
|
+
require_relative "solution"
|
data/lib/solution.rb
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
require_relative "operators"
|
2
|
+
require_relative "rugal"
|
3
|
+
require_relative "genetic_algorithm"
|
4
|
+
|
5
|
+
module GA::Solutions
|
6
|
+
class Solution
|
7
|
+
attr_accessor :fitness
|
8
|
+
attr_reader :data
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@data = nil
|
12
|
+
@fitness = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def size
|
16
|
+
@data.size
|
17
|
+
end
|
18
|
+
|
19
|
+
def mutate(i, &block)
|
20
|
+
duplicate = self.dup
|
21
|
+
duplicate.mutate!(i, &block)
|
22
|
+
return duplicate
|
23
|
+
end
|
24
|
+
|
25
|
+
def mutate!(i)
|
26
|
+
@data[i] = neighbour(@data[i], yield(@data[i]))
|
27
|
+
end
|
28
|
+
|
29
|
+
def neighbour(value, by)
|
30
|
+
raise "Cannot infer the neighbours. Must be implemented in #{self.class}."
|
31
|
+
end
|
32
|
+
|
33
|
+
def dup
|
34
|
+
duplicate = super
|
35
|
+
duplicate.internal_dup!
|
36
|
+
|
37
|
+
return duplicate
|
38
|
+
end
|
39
|
+
|
40
|
+
def inspect
|
41
|
+
"<" + self.class.to_s + " " + @data.inspect + ": #@fitness>"
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_s
|
45
|
+
@data.to_s
|
46
|
+
end
|
47
|
+
|
48
|
+
protected
|
49
|
+
def internal_dup!
|
50
|
+
@data = @data.dup
|
51
|
+
@fitness
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class IntegerSolution < Solution
|
56
|
+
def initialize(size, options = {})
|
57
|
+
@min = options[:min] || 0
|
58
|
+
@max = options[:max] || 999999999
|
59
|
+
|
60
|
+
GA.contract("Min must be an Integer") { @min.is_a?(Integer) }
|
61
|
+
GA.contract("Max must be an Integer") { @max.is_a?(Integer) }
|
62
|
+
GA.contract("Max must be greater than min") { @max > @min }
|
63
|
+
|
64
|
+
|
65
|
+
@data = size.times.map { GA::Random.rand(@max+1) + @min }
|
66
|
+
end
|
67
|
+
|
68
|
+
def neighbour(value, by)
|
69
|
+
GA.contract("Value value must be an Integer") { value.is_a?(Integer) }
|
70
|
+
GA.contract("By must be a Numeric (#{by.class} given)") { by.is_a?(Numeric) }
|
71
|
+
GA.contract("By cannot be 0") { by != 0 }
|
72
|
+
by = by > 0 ? by.ceil : by.floor
|
73
|
+
|
74
|
+
return [[value + by, @max].min, @min].max
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class FloatSolution < Solution
|
79
|
+
def initialize(size, options = {})
|
80
|
+
@min = (options[:min] || 0.0).to_f
|
81
|
+
@max = (options[:max] || 1.0).to_f
|
82
|
+
|
83
|
+
GA.contract("Max and min must not be equal") { @max > @min }
|
84
|
+
|
85
|
+
@data = size.times.map { ((@max-@min) * GA::Random.rand) + @min }
|
86
|
+
end
|
87
|
+
|
88
|
+
def neighbour(value, by)
|
89
|
+
GA.contract("Value value must be a Float") { value.is_a?(Float) }
|
90
|
+
GA.contract("By must be a Numeric (#{by.class} given)") { by.is_a?(Numeric) }
|
91
|
+
GA.contract("By cannot be 0") { by != 0 }
|
92
|
+
|
93
|
+
return [[value + by, @max].min, @min].max
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class StringSolution < Solution
|
98
|
+
def initialize(size, options = {})
|
99
|
+
@allowed = options[:allowed] || [('A'..'Z'), ('a'..'z'), ('0'..'9')].map { |e| e.to_a }.flatten
|
100
|
+
|
101
|
+
GA.contract("Allowed must be an array (#{@allowed.class} given)") { @allowed.is_a?(Array) }
|
102
|
+
GA.contract("Allowed must be an array of characters, i.e., 1-sized Strings") { @allowed.all? { |c| c.is_a?(String) && c.size == 1 } }
|
103
|
+
GA.contract("There must be at least an allowed character") { @allowed.size > 0 }
|
104
|
+
|
105
|
+
@data = size.times.map { @allowed[GA::Random.rand(@allowed.size)]}
|
106
|
+
end
|
107
|
+
|
108
|
+
def neighbour(value, by)
|
109
|
+
GA.contract("Value must be a character (1-sized String)") { value.is_a?(String) && value.length == 1 }
|
110
|
+
GA.contract("Value must be an allowed character: <#{@allowed.join(", ")}>") { @allowed.include?(value) }
|
111
|
+
GA.contract("By must be a Numeric (#{by.class} given)") { by.is_a?(Numeric) }
|
112
|
+
GA.contract("By cannot be 0") { by != 0 }
|
113
|
+
|
114
|
+
by = by > 0 ? by.ceil : by.floor
|
115
|
+
|
116
|
+
return @allowed[@allowed.index(value) % @allowed.size]
|
117
|
+
end
|
118
|
+
|
119
|
+
def to_s
|
120
|
+
@data.join ""
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class PermutationSolution < Solution
|
125
|
+
def self.force(data)
|
126
|
+
chromosome = self.new(data)
|
127
|
+
for i in 0...chromosome.size
|
128
|
+
chromosome.data[i] = data[i]
|
129
|
+
end
|
130
|
+
|
131
|
+
GA.postcondition("Invalid forced chromosome") { chromosome.valid? }
|
132
|
+
|
133
|
+
return chromosome
|
134
|
+
end
|
135
|
+
|
136
|
+
def initialize(content)
|
137
|
+
@content = content
|
138
|
+
|
139
|
+
GA.contract("Content must not be nil") { @content }
|
140
|
+
GA.contract("Content must contain at least an element") { @content.size > 0 }
|
141
|
+
|
142
|
+
@data = GA::Random.shuffle @content
|
143
|
+
end
|
144
|
+
|
145
|
+
def mutate!(index)
|
146
|
+
by = yield(@data[index])
|
147
|
+
|
148
|
+
switch_with = (index + by) % @data.size
|
149
|
+
switch_with = (index + 1) % @data.size if switch_with == index
|
150
|
+
|
151
|
+
GA.contract("By must be a Numeric (#{by.class} given)") { by.is_a?(Numeric) }
|
152
|
+
GA.contract("By cannot be 0") { by != 0 }
|
153
|
+
|
154
|
+
@data[index], @data[switch_with] = @data[switch_with], @data[index]
|
155
|
+
end
|
156
|
+
|
157
|
+
def valid?
|
158
|
+
((@data - @content) + (@content - @data)) == []
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
class SolutionFactory
|
163
|
+
def initialize(&block)
|
164
|
+
@generator = block
|
165
|
+
end
|
166
|
+
|
167
|
+
def chromosome
|
168
|
+
return @generator.call
|
169
|
+
end
|
170
|
+
|
171
|
+
def population(size)
|
172
|
+
size.times.map { chromosome }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
class Array
|
178
|
+
def sample(n)
|
179
|
+
GA::Random.shuffle(self)[0...n]
|
180
|
+
end
|
181
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rugal
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Simone Scalabrino
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-08-18 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Pure-ruby implementation of genetic algorithms
|
14
|
+
email: s.scalabrino9@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/genetic_algorithm.rb
|
20
|
+
- lib/operators.rb
|
21
|
+
- lib/rugal.rb
|
22
|
+
- lib/solution.rb
|
23
|
+
homepage:
|
24
|
+
licenses:
|
25
|
+
- GPL-2.0
|
26
|
+
metadata: {}
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '0'
|
36
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project:
|
43
|
+
rubygems_version: 2.7.7
|
44
|
+
signing_key:
|
45
|
+
specification_version: 4
|
46
|
+
summary: Genetic algorithms for ruby
|
47
|
+
test_files: []
|