mhl 0.0.1
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/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +71 -0
- data/Rakefile +1 -0
- data/lib/mhl.rb +3 -0
- data/lib/mhl/bitstring_genotype_space.rb +84 -0
- data/lib/mhl/genetic_algorithm_solver.rb +164 -0
- data/lib/mhl/integer_genotype_space.rb +106 -0
- data/lib/mhl/particle_swarm_optimization_solver.rb +122 -0
- data/lib/mhl/version.rb +3 -0
- data/mhl.gemspec +28 -0
- data/spec/mhl/genetic_algorithm_spec.rb +53 -0
- data/spec/mhl/particle_swarm_optimization_solver_spec.rb +16 -0
- data/spec/spec_helper.rb +19 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fd67377b3670dfd8f235044614d2f4b9965ead18
|
4
|
+
data.tar.gz: a3a98c4c0a8ead65030715cbf0e0f9f24f1d0a3c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a7dfe8e770e05c0bae580e3e0f21cb9da2accc690b05d19d6e1ce330dcae0e6a54ee3c95630809aabec34c9cc0af8a9befeb7ee1faabbbb87356e809522f2f46
|
7
|
+
data.tar.gz: cd3bb8ce03f86c22250d549251908d7ff2c7eb88b70a9108fe258213686afa7b9024eb0888607feb24031f912769e2073b6436c53ad7af059b90e78b462c8137
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2014 Mauro Tortonesi
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#ruby-mhl
|
2
|
+
|
3
|
+
A Ruby metaheuristics library
|
4
|
+
|
5
|
+
|
6
|
+
## Installation
|
7
|
+
|
8
|
+
### Stable version
|
9
|
+
|
10
|
+
You can get the stable version of ruby-mhl by installing the mhl gem from
|
11
|
+
RubyGems:
|
12
|
+
|
13
|
+
gem install mhl
|
14
|
+
|
15
|
+
### Development version
|
16
|
+
|
17
|
+
If you want to try the development version of ruby-mhl, instead, just place
|
18
|
+
this line:
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'mhl', git: 'https://github.com/mtortonesi/ruby-mhl.git'
|
22
|
+
```
|
23
|
+
|
24
|
+
in your Gemfile and run:
|
25
|
+
|
26
|
+
bundle install
|
27
|
+
|
28
|
+
|
29
|
+
## Examples
|
30
|
+
|
31
|
+
Here is an example demonstrating how to find the argument that minimizes the
|
32
|
+
2-dimension parabola x_1 ^ 2 + x_2 ^ 2 equation with a genetic algorithm:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
require 'mhl'
|
36
|
+
|
37
|
+
solver = MHL::GeneticAlgorithmSolver.new(
|
38
|
+
:population_size => 40,
|
39
|
+
:genotype_space_type => :integer,
|
40
|
+
:mutation_probability => 0.5,
|
41
|
+
:recombination_probability => 0.5,
|
42
|
+
:genotype_space_conf => {
|
43
|
+
:dimensions => 2,
|
44
|
+
:recombination_type => :intermediate,
|
45
|
+
:random_func => lambda { Array.new(2) { rand(20) } }
|
46
|
+
},
|
47
|
+
:exit_condition => lambda {|generation,best| best[:fitness] == 0}
|
48
|
+
)
|
49
|
+
solver.solve(Proc.new{|x| -(x[0] ** 2 + x[1] ** 2) })
|
50
|
+
```
|
51
|
+
|
52
|
+
and with particle swarm optimization:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
require 'mhl'
|
56
|
+
|
57
|
+
solver = MHL::ParticleSwarmOptimizationSolver.new(
|
58
|
+
:swarm_size => 40,
|
59
|
+
:random_position_func => lambda { Array.new(2) { rand(20) } },
|
60
|
+
:random_velocity_func => lambda { Array.new(2) { rand(10) } },
|
61
|
+
:exit_condition => lambda {|generation,best| best[:height].abs < 0.001 },
|
62
|
+
)
|
63
|
+
solver.solve(Proc.new{|x| -(x[0] ** 2 + x[1] ** 2) })
|
64
|
+
```
|
65
|
+
|
66
|
+
Other examples and a full documentation will be publised as ruby-mhl matures.
|
67
|
+
|
68
|
+
|
69
|
+
## License
|
70
|
+
|
71
|
+
MIT
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/lib/mhl.rb
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'bitstring'
|
2
|
+
|
3
|
+
module MHL
|
4
|
+
|
5
|
+
# This class implements a genotype with bitstring representation
|
6
|
+
class BitstringGenotypeSpace
|
7
|
+
def initialize(opts)
|
8
|
+
@bitstring_length = opts[:bitstring_length].to_i
|
9
|
+
unless @bitstring_length and @bitstring_length > 0
|
10
|
+
raise ArgumentError, 'Must have positive integer bitstring_length'
|
11
|
+
end
|
12
|
+
|
13
|
+
@random_func = opts[:random_func] || default_random_func(opts[:random_one_to_zero_ratio] || 1.0)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_random
|
17
|
+
@random_func.call
|
18
|
+
end
|
19
|
+
|
20
|
+
# reproduction with bitflip mutation and one-point crossover
|
21
|
+
def reproduce_from(p1, p2, mutation_rv, recombination_rv)
|
22
|
+
# make copies of p1 and p2
|
23
|
+
# (we're only interested in the :genotype key)
|
24
|
+
c1 = { :genotype => p1[:genotype].dup }
|
25
|
+
c2 = { :genotype => p2[:genotype].dup }
|
26
|
+
|
27
|
+
# mutation comes first
|
28
|
+
bitflip_mutation(c1[:genotype], mutation_rv)
|
29
|
+
bitflip_mutation(c2[:genotype], mutation_rv)
|
30
|
+
|
31
|
+
# and then recombination
|
32
|
+
c1[:genotype], c2[:genotype] =
|
33
|
+
onepoint_crossover(c1[:genotype], c2[:genotype], recombination_rv)
|
34
|
+
|
35
|
+
return c1, c2
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def bitflip_mutation(bitstring, mutation_rv)
|
41
|
+
# TODO: disable this check in non-debugging mode
|
42
|
+
unless bitstring.length == @bitstring_length
|
43
|
+
raise 'Error! Different bit string sizes!'
|
44
|
+
end
|
45
|
+
|
46
|
+
@bitstring_length.times do |i|
|
47
|
+
if mutation_rv.next < @mutation_threshold
|
48
|
+
bitval = bitstring[i]
|
49
|
+
bitstring[i] = (bitval == 1 ? '0' : '1')
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def onepoint_crossover(bitstring1, bitstring2, recombination_rv)
|
55
|
+
# TODO: disable this check in non-debugging mode
|
56
|
+
unless bitstring1.length == @bitstring_length and bitstring2.length == @bitstring_length
|
57
|
+
raise 'Error! Different bit string sizes!'
|
58
|
+
end
|
59
|
+
|
60
|
+
if recombination_rv.next < @recombination_threshold
|
61
|
+
size = bitstring1.length
|
62
|
+
point = 1 + rand(size - 2)
|
63
|
+
hi_mask = bitstring1.mask(point, BitString::LOW_END) # lowest point bits
|
64
|
+
low_mask = bitstring1.mask(size - point, BitString::HIGH_END) # highest size-point bits
|
65
|
+
new_b1 = (bitstring1 & hi_mask) | (bitstring2 & low_mask)
|
66
|
+
new_b2 = (bitstring2 & hi_mask) | (bitstring1 & low_mask)
|
67
|
+
return new_b1, new_b2
|
68
|
+
end
|
69
|
+
return bitstring1, bitstring2
|
70
|
+
end
|
71
|
+
|
72
|
+
def default_random_func(one_to_zero_ratio)
|
73
|
+
random_percentage_of_ones = one_to_zero_ratio / (1.0 + one_to_zero_ratio)
|
74
|
+
lambda do
|
75
|
+
str = (0...@bitstring_length).inject("") do |s,i|
|
76
|
+
s << ((rand < random_percentage_of_ones) ? '1' : '0')
|
77
|
+
end
|
78
|
+
BitString.new(str, size)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'concurrent'
|
2
|
+
require 'erv'
|
3
|
+
|
4
|
+
require 'mhl/bitstring_genotype_space'
|
5
|
+
require 'mhl/integer_genotype_space'
|
6
|
+
|
7
|
+
|
8
|
+
module MHL
|
9
|
+
|
10
|
+
class GeneticAlgorithmSolver
|
11
|
+
def initialize(opts)
|
12
|
+
@population_size = opts[:population_size].to_i
|
13
|
+
unless @population_size and @population_size.even?
|
14
|
+
raise ArgumentError, 'Even population size required!'
|
15
|
+
end
|
16
|
+
|
17
|
+
# perform genotype space-specific configuration
|
18
|
+
case opts[:genotype_space_type]
|
19
|
+
when :integer
|
20
|
+
@genotype_space = IntegerVectorGenotypeSpace.new(opts[:genotype_space_conf])
|
21
|
+
|
22
|
+
begin
|
23
|
+
p_m = opts[:mutation_probability].to_f
|
24
|
+
@mutation_rv = \
|
25
|
+
ERV::RandomVariable.new(:distribution => :geometric,
|
26
|
+
:probability_of_success => p_m)
|
27
|
+
rescue
|
28
|
+
raise ArgumentError, 'Mutation probability configuration is wrong.'
|
29
|
+
end
|
30
|
+
|
31
|
+
begin
|
32
|
+
p_r = opts[:recombination_probability].to_f
|
33
|
+
@recombination_rv = \
|
34
|
+
ERV::RandomVariable.new(:distribution => :uniform,
|
35
|
+
:min_value => -p_r,
|
36
|
+
:max_value => 1.0 + p_r)
|
37
|
+
rescue
|
38
|
+
raise ArgumentError, 'Recombination probability configuration is wrong.'
|
39
|
+
end
|
40
|
+
|
41
|
+
when :bitstring
|
42
|
+
@genotype_space = BitstringGenotypeSpace.new(opts[:genotype_space_conf])
|
43
|
+
@recombination_rv = ERV::RandomVariable.new(:distribution => :uniform, :max_value => 1.0)
|
44
|
+
@mutation_rv = ERV::RandomVariable.new(:distribution => :uniform, :max_value => 1.0)
|
45
|
+
|
46
|
+
else
|
47
|
+
raise ArgumentError, 'Only integer and bitstring genotype representations are supported!'
|
48
|
+
end
|
49
|
+
|
50
|
+
@exit_condition = opts[:exit_condition]
|
51
|
+
@start_population = opts[:genotype_space_conf][:start_population]
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
# This is the method that solves the optimization problem
|
56
|
+
#
|
57
|
+
# Parameter func is supposed to be a method (or a Proc, a lambda, or any callable
|
58
|
+
# object) that accepts the genotype as argument (that is, the set of
|
59
|
+
# parameters) and returns the phenotype (that is, the function result)
|
60
|
+
def solve(func)
|
61
|
+
# setup population
|
62
|
+
if @start_population.nil?
|
63
|
+
population = Array.new(@population_size) do
|
64
|
+
# generate random genotype according to the chromosome type
|
65
|
+
{ :genotype => @genotype_space.get_random }
|
66
|
+
end
|
67
|
+
else
|
68
|
+
population = @start_population.map do |x|
|
69
|
+
{ :genotype => x }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# initialize variables
|
74
|
+
gen = 0
|
75
|
+
overall_best = nil
|
76
|
+
|
77
|
+
# default behavior is to loop forever
|
78
|
+
begin
|
79
|
+
gen += 1
|
80
|
+
puts "Starting generation #{gen} at #{Time.now}"
|
81
|
+
|
82
|
+
# assess fitness for every member of the population
|
83
|
+
population.each do |s|
|
84
|
+
s[:task] = Concurrent::Future.new { func.call(s[:genotype]) }
|
85
|
+
end
|
86
|
+
|
87
|
+
# wait for all the evaluations to end
|
88
|
+
population.each do |s|
|
89
|
+
s[:fitness] = s[:task].value
|
90
|
+
end
|
91
|
+
|
92
|
+
# find fittest member
|
93
|
+
population_best = population.max_by {|x| x[:fitness] }
|
94
|
+
|
95
|
+
# calculate overall best
|
96
|
+
if overall_best.nil?
|
97
|
+
overall_best = population_best
|
98
|
+
else
|
99
|
+
overall_best = [ overall_best, population_best ].max_by {|x| x[:fitness] }
|
100
|
+
end
|
101
|
+
|
102
|
+
# print results
|
103
|
+
puts "> gen #{gen}, best: #{overall_best[:genotype]}, #{overall_best[:fitness]}"
|
104
|
+
|
105
|
+
# selection by binary tournament
|
106
|
+
children = new_generation(population)
|
107
|
+
|
108
|
+
# update population and generation number
|
109
|
+
population = children
|
110
|
+
end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
# reproduction with point mutation and one-point crossover
|
117
|
+
def new_generation(population)
|
118
|
+
population_size = population.size
|
119
|
+
|
120
|
+
# check correct population size
|
121
|
+
# TODO: disable this check in non-debugging mode
|
122
|
+
raise ArgumentError, 'Population size error!' if population_size != @population_size
|
123
|
+
|
124
|
+
# prepare children
|
125
|
+
children = []
|
126
|
+
|
127
|
+
# select members to reproduce through binary tournament
|
128
|
+
selected = Array.new(@population_size) { |i| binary_tournament(population) }
|
129
|
+
selected.shuffle!
|
130
|
+
|
131
|
+
# reproduction
|
132
|
+
selected.each_slice(2) do |p1, p2|
|
133
|
+
# get two new samples...
|
134
|
+
c1, c2 = @genotype_space.reproduce_from(p1, p2, @mutation_rv, @recombination_rv)
|
135
|
+
|
136
|
+
# ...and add them to the children population
|
137
|
+
children.push(c1, c2)
|
138
|
+
|
139
|
+
# check correct population size
|
140
|
+
# TODO: disable this check in non-debugging mode
|
141
|
+
raise 'Children size error!' if children.size > population_size
|
142
|
+
end
|
143
|
+
|
144
|
+
return children
|
145
|
+
end
|
146
|
+
|
147
|
+
# This method implements binary tournament selection, which is probably
|
148
|
+
# the most popular selection method for genetic algorithms
|
149
|
+
def binary_tournament(population)
|
150
|
+
i = rand(population.size)
|
151
|
+
j = rand(population.size - 1)
|
152
|
+
j += 1 if j >= i
|
153
|
+
|
154
|
+
select_fittest(population[i], population[j])
|
155
|
+
end
|
156
|
+
|
157
|
+
def select_fittest(*a)
|
158
|
+
# TODO: disable this check in non-debugging mode
|
159
|
+
raise 'Attempting to select the fittest sample of an empty population!' if a.empty?
|
160
|
+
a.max_by {|x| x[:fitness] }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module MHL
|
2
|
+
|
3
|
+
# This class implements a genotype with integer space representation
|
4
|
+
class IntegerVectorGenotypeSpace
|
5
|
+
def initialize(opts)
|
6
|
+
@random_func = opts[:random_func]
|
7
|
+
|
8
|
+
@dimensions = opts[:dimensions].to_i
|
9
|
+
unless @dimensions and @dimensions > 0
|
10
|
+
raise ArgumentError, 'Must have positive integer dimensions'
|
11
|
+
end
|
12
|
+
|
13
|
+
# TODO: enable to choose which recombination function to use
|
14
|
+
case opts[:recombination_type].to_s
|
15
|
+
when /intermediate/i
|
16
|
+
@recombination_func = :intermediate_recombination
|
17
|
+
when /line/i
|
18
|
+
@recombination_func = :line_recombination
|
19
|
+
else
|
20
|
+
raise ArgumentError, 'Recombination function must be either line or intermediate!'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def get_random
|
25
|
+
if @random_func
|
26
|
+
@random_func.call
|
27
|
+
else
|
28
|
+
# TODO: implement this
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# reproduction with random geometric mutation
|
33
|
+
# and intermediate recombination
|
34
|
+
def reproduce_from(p1, p2, mutation_rv, recombination_rv)
|
35
|
+
# make copies of p1 and p2
|
36
|
+
# (we're only interested in the :genotype key)
|
37
|
+
c1 = { :genotype => p1[:genotype].dup }
|
38
|
+
c2 = { :genotype => p2[:genotype].dup }
|
39
|
+
|
40
|
+
# mutation comes first
|
41
|
+
random_geometric_mutation(c1[:genotype], mutation_rv)
|
42
|
+
random_geometric_mutation(c2[:genotype], mutation_rv)
|
43
|
+
|
44
|
+
# and then recombination
|
45
|
+
send(@recombination_func, c1[:genotype], c2[:genotype], recombination_rv)
|
46
|
+
|
47
|
+
return c1, c2
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def random_geometric_mutation(g, mutation_rv)
|
54
|
+
g.each_index do |i|
|
55
|
+
# being sampled from a geometric distribution, delta will always
|
56
|
+
# be a non-negative integer (that is, 0 or greater)
|
57
|
+
delta = mutation_rv.next
|
58
|
+
|
59
|
+
if rand() >= 0.5
|
60
|
+
# half of the times the variation will be positive ...
|
61
|
+
g[i] += delta
|
62
|
+
else
|
63
|
+
# ... and half of the times it will be negative
|
64
|
+
g[i] -= delta
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def intermediate_recombination(g1, g2, recombination_rv)
|
70
|
+
# TODO: disable this check in non-debugging mode
|
71
|
+
raise ArgumentError, 'g1 and g2 must have the same dimension' unless g1.size == g2.size
|
72
|
+
|
73
|
+
# recombination
|
74
|
+
g1.each_index do |i|
|
75
|
+
begin
|
76
|
+
alpha = recombination_rv.next
|
77
|
+
beta = recombination_rv.next
|
78
|
+
t = (alpha * g1[i] + (1.0 - alpha) * g2[i] + 0.5).floor
|
79
|
+
s = ( beta * g2[i] + (1.0 - beta) * g1[i] + 0.5).floor
|
80
|
+
end # until t >= 0 and s >= 0 # TODO: implement within-bounds condition checking
|
81
|
+
g1[i] = t
|
82
|
+
g2[i] = s
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def line_recombination(g1, g2, recombination_rv)
|
87
|
+
# TODO: disable this check in non-debugging mode
|
88
|
+
raise ArgumentError, 'g1 and g2 must have the same dimension' unless g1.size == g2.size
|
89
|
+
|
90
|
+
alpha = recombination_rv.next
|
91
|
+
beta = recombination_rv.next
|
92
|
+
|
93
|
+
# recombination
|
94
|
+
g1.each_index do |i|
|
95
|
+
t = (alpha * g1[i] + (1.0 - alpha) * g2[i] + 0.5).floor
|
96
|
+
s = ( beta * g2[i] + (1.0 - beta) * g1[i] + 0.5).floor
|
97
|
+
# if t >= 0 and s >= 0 # TODO: implement within-bounds condition checking
|
98
|
+
g1[i] = t
|
99
|
+
g2[i] = s
|
100
|
+
# end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'matrix'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
module MHL
|
5
|
+
|
6
|
+
class ParticleSwarmOptimizationSolver
|
7
|
+
|
8
|
+
def initialize(opts={})
|
9
|
+
@swarm_size = opts[:swarm_size].to_i
|
10
|
+
unless @swarm_size
|
11
|
+
raise ArgumentError, 'Swarm size is a required parameter!'
|
12
|
+
end
|
13
|
+
|
14
|
+
@random_position_func = opts[:random_position_func]
|
15
|
+
@random_velocity_func = opts[:random_velocity_func]
|
16
|
+
|
17
|
+
@start_positions = opts[:start_positions]
|
18
|
+
@exit_condition = opts[:exit_condition]
|
19
|
+
end
|
20
|
+
|
21
|
+
# This is the method that solves the optimization problem
|
22
|
+
#
|
23
|
+
# Parameter func is supposed to be a method (or a Proc, a lambda, or any callable
|
24
|
+
# object) that accepts the genotype as argument (that is, the set of
|
25
|
+
# parameters) and returns the phenotype (that is, the function result)
|
26
|
+
def solve(func)
|
27
|
+
# setup particles
|
28
|
+
if @start_positions.nil?
|
29
|
+
particles = Array.new(@swarm_size) do
|
30
|
+
{ position: Vector[*@random_position_func.call], velocity: Vector[*@random_velocity_func.call] }
|
31
|
+
end
|
32
|
+
else
|
33
|
+
particles = @start_positions.each_slice(2).map do |pos,vel|
|
34
|
+
{ position: Vector[*pos], velocity: Vector[*vel] }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# initialize variables
|
39
|
+
gen = 0
|
40
|
+
overall_best = nil
|
41
|
+
|
42
|
+
# completely made up values
|
43
|
+
alpha = 0.5
|
44
|
+
beta = 0.3
|
45
|
+
gamma = 0.7
|
46
|
+
delta = 0.5
|
47
|
+
epsilon = 0.6
|
48
|
+
|
49
|
+
# default behavior is to loop forever
|
50
|
+
begin
|
51
|
+
gen += 1
|
52
|
+
puts "Starting generation #{gen} at #{Time.now}"
|
53
|
+
|
54
|
+
# assess height for every particle
|
55
|
+
particles.each do |p|
|
56
|
+
p[:task] = Concurrent::Future.new { func.call(p[:position]) }
|
57
|
+
end
|
58
|
+
|
59
|
+
# wait for all the evaluations to end
|
60
|
+
particles.each_with_index do |p,i|
|
61
|
+
p[:height] = p[:task].value
|
62
|
+
if p[:highest_value].nil? or p[:height] > p[:highest_value]
|
63
|
+
p[:highest_value] = p[:height]
|
64
|
+
p[:highest_position] = p[:position]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# find highest particle
|
69
|
+
highest_particle = particles.max_by {|x| x[:height] }
|
70
|
+
|
71
|
+
# calculate overall best
|
72
|
+
if overall_best.nil?
|
73
|
+
overall_best = highest_particle
|
74
|
+
else
|
75
|
+
overall_best = [ overall_best, highest_particle ].max_by {|x| x[:height] }
|
76
|
+
end
|
77
|
+
|
78
|
+
# mutate swarm
|
79
|
+
particles.each do |p|
|
80
|
+
# randomly sample particles and use them as informants
|
81
|
+
informants = random_portion(particles)
|
82
|
+
|
83
|
+
# make sure that p is included among the informants
|
84
|
+
informants << p unless informants.include? p
|
85
|
+
|
86
|
+
# get fittest informant
|
87
|
+
fittest_informant = informants.max_by {|x| x[:height] }
|
88
|
+
|
89
|
+
# update velocity
|
90
|
+
p[:velocity] =
|
91
|
+
alpha * p[:velocity] +
|
92
|
+
beta * (p[:highest_position] - p[:position]) +
|
93
|
+
gamma * (fittest_informant[:highest_position] - p[:position]) +
|
94
|
+
delta * (overall_best[:highest_position] - p[:position])
|
95
|
+
|
96
|
+
# update position
|
97
|
+
p[:position] = p[:position] + epsilon * p[:velocity]
|
98
|
+
end
|
99
|
+
|
100
|
+
end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def random_portion(array, ratio=0.1)
|
106
|
+
# get size of random array to return
|
107
|
+
size = (ratio * array.size).ceil
|
108
|
+
|
109
|
+
(1..size).inject([]) do |acc,i|
|
110
|
+
# randomly sample a new element
|
111
|
+
begin
|
112
|
+
new_element = array[SecureRandom.random_number(array.size)]
|
113
|
+
end while acc.include? new_element
|
114
|
+
|
115
|
+
# insert element in the accumulator
|
116
|
+
acc << new_element
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
data/lib/mhl/version.rb
ADDED
data/mhl.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'mhl/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'mhl'
|
8
|
+
spec.version = MHL::VERSION
|
9
|
+
spec.authors = ['Mauro Tortonesi']
|
10
|
+
spec.email = ['mauro.tortonesi@unife.it']
|
11
|
+
spec.description = %q{A Ruby Metaheuristics library}
|
12
|
+
spec.summary = %q{A scientific library for Ruby that provides several metaheuristics}
|
13
|
+
spec.homepage = 'https://github.com/mtortonesi/ruby-mhl'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/).reject{|x| x == '.gitignore' }
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'bitstring'
|
22
|
+
spec.add_dependency 'concurrent-ruby'
|
23
|
+
spec.add_dependency 'erv'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'rspec'
|
28
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MHL::GeneticAlgorithmSolver do
|
4
|
+
|
5
|
+
it 'should accept bitstring representation genotypes' do
|
6
|
+
lambda {
|
7
|
+
MHL::GeneticAlgorithmSolver.new(
|
8
|
+
:population_size => 128,
|
9
|
+
:genotype_space_type => :bitstring,
|
10
|
+
:mutation_threshold => 0.5,
|
11
|
+
:recombination_threshold => 0.5,
|
12
|
+
:genotype_space_conf => {
|
13
|
+
:bitstring_length => 120,
|
14
|
+
}
|
15
|
+
)
|
16
|
+
}.should_not raise_error
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'should accept integer representation genotypes' do
|
20
|
+
lambda {
|
21
|
+
MHL::GeneticAlgorithmSolver.new(
|
22
|
+
:population_size => 128,
|
23
|
+
:genotype_space_type => :integer,
|
24
|
+
:mutation_probability => 0.5,
|
25
|
+
:recombination_probability => 0.5,
|
26
|
+
:genotype_space_conf => {
|
27
|
+
:dimensions => 6,
|
28
|
+
:recombination_type => :intermediate,
|
29
|
+
}
|
30
|
+
)
|
31
|
+
}.should_not raise_error
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should solve a 2-dimension parabola in integer space' do
|
35
|
+
solver = MHL::GeneticAlgorithmSolver.new(
|
36
|
+
:population_size => 40,
|
37
|
+
:genotype_space_type => :integer,
|
38
|
+
:mutation_probability => 0.5,
|
39
|
+
:recombination_probability => 0.5,
|
40
|
+
:genotype_space_conf => {
|
41
|
+
:dimensions => 2,
|
42
|
+
:recombination_type => :intermediate,
|
43
|
+
:random_func => lambda { Array.new(2) { rand(20) } }
|
44
|
+
},
|
45
|
+
:exit_condition => lambda {|generation,best_sample| best_sample[:fitness] == 0}
|
46
|
+
)
|
47
|
+
solver.solve(Proc.new{|genotype| -(genotype[0]**2 + genotype[1]**2) })
|
48
|
+
end
|
49
|
+
|
50
|
+
# it 'should solve a 2-dimension parabola in real space'
|
51
|
+
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe MHL::ParticleSwarmOptimizationSolver do
|
4
|
+
|
5
|
+
it 'should solve a 2-dimension parabola in real space' do
|
6
|
+
solver = MHL::ParticleSwarmOptimizationSolver.new(
|
7
|
+
:swarm_size => 40,
|
8
|
+
:random_position_func => lambda { Array.new(2) { rand(20) } },
|
9
|
+
:random_velocity_func => lambda { Array.new(2) { rand(10) } },
|
10
|
+
:exit_condition => lambda {|generation,best_sample| best_sample[:height].abs < 0.001 },
|
11
|
+
)
|
12
|
+
solver.solve(Proc.new{|position| -(position[0]**2 + position[1]**2) })
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
|
6
|
+
require 'mhl'
|
7
|
+
|
8
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
9
|
+
RSpec.configure do |config|
|
10
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.filter_run :focus
|
13
|
+
|
14
|
+
# Run specs in random order to surface order dependencies. If you find an
|
15
|
+
# order dependency and want to debug it, you can fix the order by providing
|
16
|
+
# the seed, which is printed after each run.
|
17
|
+
# --seed 1234
|
18
|
+
config.order = 'random'
|
19
|
+
end
|
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mhl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mauro Tortonesi
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bitstring
|
15
|
+
version_requirements: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
requirement: !ruby/object:Gem::Requirement
|
21
|
+
requirements:
|
22
|
+
- - '>='
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: '0'
|
25
|
+
prerelease: false
|
26
|
+
type: :runtime
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: concurrent-ruby
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
prerelease: false
|
40
|
+
type: :runtime
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: erv
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - '>='
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
prerelease: false
|
54
|
+
type: :runtime
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.3'
|
62
|
+
requirement: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '1.3'
|
67
|
+
prerelease: false
|
68
|
+
type: :development
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
requirement: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - '>='
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
prerelease: false
|
82
|
+
type: :development
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
requirement: !ruby/object:Gem::Requirement
|
91
|
+
requirements:
|
92
|
+
- - '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
prerelease: false
|
96
|
+
type: :development
|
97
|
+
description: A Ruby Metaheuristics library
|
98
|
+
email:
|
99
|
+
- mauro.tortonesi@unife.it
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- Gemfile
|
105
|
+
- LICENSE
|
106
|
+
- README.md
|
107
|
+
- Rakefile
|
108
|
+
- lib/mhl.rb
|
109
|
+
- lib/mhl/bitstring_genotype_space.rb
|
110
|
+
- lib/mhl/genetic_algorithm_solver.rb
|
111
|
+
- lib/mhl/integer_genotype_space.rb
|
112
|
+
- lib/mhl/particle_swarm_optimization_solver.rb
|
113
|
+
- lib/mhl/version.rb
|
114
|
+
- mhl.gemspec
|
115
|
+
- spec/mhl/genetic_algorithm_spec.rb
|
116
|
+
- spec/mhl/particle_swarm_optimization_solver_spec.rb
|
117
|
+
- spec/spec_helper.rb
|
118
|
+
homepage: https://github.com/mtortonesi/ruby-mhl
|
119
|
+
licenses:
|
120
|
+
- MIT
|
121
|
+
metadata: {}
|
122
|
+
post_install_message:
|
123
|
+
rdoc_options: []
|
124
|
+
require_paths:
|
125
|
+
- lib
|
126
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
requirements:
|
133
|
+
- - '>='
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
version: '0'
|
136
|
+
requirements: []
|
137
|
+
rubyforge_project:
|
138
|
+
rubygems_version: 2.2.1
|
139
|
+
signing_key:
|
140
|
+
specification_version: 4
|
141
|
+
summary: A scientific library for Ruby that provides several metaheuristics
|
142
|
+
test_files:
|
143
|
+
- spec/mhl/genetic_algorithm_spec.rb
|
144
|
+
- spec/mhl/particle_swarm_optimization_solver_spec.rb
|
145
|
+
- spec/spec_helper.rb
|