mhl 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fd67377b3670dfd8f235044614d2f4b9965ead18
4
- data.tar.gz: a3a98c4c0a8ead65030715cbf0e0f9f24f1d0a3c
3
+ metadata.gz: 6451520f742d76561c0caf54cde03a7da4643898
4
+ data.tar.gz: f37fb25a97bb1b718c5ab2e7078f6853574b3749
5
5
  SHA512:
6
- metadata.gz: a7dfe8e770e05c0bae580e3e0f21cb9da2accc690b05d19d6e1ce330dcae0e6a54ee3c95630809aabec34c9cc0af8a9befeb7ee1faabbbb87356e809522f2f46
7
- data.tar.gz: cd3bb8ce03f86c22250d549251908d7ff2c7eb88b70a9108fe258213686afa7b9024eb0888607feb24031f912769e2073b6436c53ad7af059b90e78b462c8137
6
+ metadata.gz: aa0d5beb542b0889e042486421d8d878b427042c046d8c72b9cea1c4913d9bb01a5ec0f9d38bb34c6d48de49723346c2f9430d30a593bf1eb01d9786292270ea
7
+ data.tar.gz: a848727a22034af0a4a59bd58c3db415af71715e3e9fce35d17305312d4649cbecf93d2ee78eee157e3d5dcfc8b158a6ba7b85901e2236c87ce82d159878b7ad
data/Rakefile CHANGED
@@ -1 +1,9 @@
1
1
  require 'bundler/gem_tasks'
2
+
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = Dir.glob('test/**/*_test.rb').sort
8
+ t.verbose = true
9
+ end
data/lib/mhl.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  require 'mhl/version'
2
2
  require 'mhl/genetic_algorithm_solver'
3
3
  require 'mhl/particle_swarm_optimization_solver'
4
+ require 'mhl/quantum_particle_swarm_optimization_solver'
5
+ require 'mhl/multiswarm_qpso_solver'
@@ -0,0 +1,85 @@
1
+ require 'matrix'
2
+
3
+ require 'mhl/generic_swarm'
4
+
5
+
6
+ module MHL
7
+ class ChargedSwarm < GenericSwarmBehavior
8
+
9
+ # default composition is half charged, i.e., QPSO, and half neutral, i.e.,
10
+ # traditional PSO (with inertia), swarms
11
+ DEFAULT_CHARGED_TO_NEUTRAL_RATIO = 1.0
12
+
13
+ def initialize(size, initial_positions, initial_velocities, params={})
14
+ @size = size
15
+
16
+ # retrieve ratio between charged (QPSO) and neutral (PSO w/ inertia) particles
17
+ ratio = (params[:charged_to_neutral_ratio] || DEFAULT_CHARGED_TO_NEUTRAL_RATIO).to_f
18
+ unless ratio > 0.0
19
+ raise ArgumentError, 'Parameter :charged_to_neutral_ratio should be a real greater than zero!'
20
+ end
21
+
22
+ num_charged_particles = (@size * ratio).round
23
+ @num_neutral_particles = @size - num_charged_particles
24
+
25
+ # the particles are ordered, with neutral (PSO w/ inertia) particles
26
+ # first and charged (QPSO) particles later
27
+ @particles = Array.new(@size) do |index|
28
+ if index < @num_neutral_particles
29
+ Particle.new(initial_positions[index], initial_velocities[index])
30
+ else
31
+ QuantumParticle.new(initial_positions[index])
32
+ end
33
+ end
34
+
35
+ # find problem dimension
36
+ @dimension = initial_positions[0].size
37
+
38
+ @generation = 1
39
+
40
+ # define procedure to get dynamic value for alpha
41
+ @get_alpha = if params.has_key? :alpha and params[:alpha].respond_to? :call
42
+ params[:alpha]
43
+ else
44
+ ->(gen) { (params[:alpha] || DEFAULT_ALPHA).to_f }
45
+ end
46
+
47
+ # get values for parameters C1 and C2
48
+ @c1 = (params[:c1] || DEFAULT_C1).to_f
49
+ @c2 = (params[:c1] || DEFAULT_C2).to_f
50
+
51
+ # define procedure to get dynamic value for omega
52
+ @get_omega = if params.has_key? :omega and params[:omega].respond_to? :call
53
+ params[:omega]
54
+ else
55
+ ->(gen) { (params[:omega] || DEFAULT_OMEGA).to_f }
56
+ end
57
+ end
58
+
59
+ def mutate
60
+ # get alpha parameter
61
+ alpha = @get_alpha.call(@generation)
62
+
63
+ # get omega parameter
64
+ omega = @get_omega.call(@generation)
65
+
66
+ # this calculates the C_n parameter (basically, the centroid of particle
67
+ # attractors) as defined in [SUN11], formulae 4.81 and 4.82
68
+ #
69
+ # (note: the neutral particles influence the behavior of the charged ones
70
+ # not only by defining the swarm attractor, but also by forming this centroid)
71
+ c_n = @particles.inject(Vector[*[0]*@dimension]) {|s,p| s += p.attractor[:position] } / @size.to_f
72
+
73
+ @particles.each_with_index do |p,i|
74
+ # remember: the particles are kept in a PSO-first and QPSO-last order
75
+ if i < @num_neutral_particles
76
+ p.move(omega, @c1, @c2, @swarm_attractor)
77
+ else
78
+ p.move(alpha, c_n, @swarm_attractor)
79
+ end
80
+ end
81
+
82
+ @generation += 1
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,52 @@
1
+ module MHL
2
+
3
+ class GenericParticle
4
+
5
+ attr_reader :attractor
6
+
7
+ def initialize(initial_position)
8
+ @position = initial_position
9
+ @attractor = nil
10
+ end
11
+
12
+ def evaluate(func)
13
+ # calculate particle height
14
+ @height = func.call(@position)
15
+
16
+ # update particle attractor (if needed)
17
+ if @attractor.nil? or @height > @attractor[:height]
18
+ @attractor = { height: @height, position: @position }
19
+ end
20
+ end
21
+
22
+ def remain_within(constraints)
23
+ new_pos = @position.map.with_index do |x,i|
24
+ puts "resetting #{x} within #{constraints[:min][i]} and #{constraints[:max][i]}"
25
+ d_max = constraints[:max][i]
26
+ d_min = constraints[:min][i]
27
+ d_size = d_max - d_min
28
+ if x > d_max
29
+ while x > d_max + d_size
30
+ x -= d_size
31
+ end
32
+ if x > d_max
33
+ x = 2 * d_max - x
34
+ end
35
+ elsif x < d_min
36
+ while x < d_min - d_size
37
+ x += d_size
38
+ end
39
+ if x < d_min
40
+ x = 2 * d_min - x
41
+ end
42
+ end
43
+ puts "now x is #{x}"
44
+ x
45
+ end
46
+ puts "new_pos: #{new_pos}"
47
+ @position = new_pos # Vector[new_pos]
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,41 @@
1
+ require 'forwardable'
2
+
3
+ module MHL
4
+ class GenericSwarmBehavior
5
+
6
+ # The following values were taken from [BLACKWELLBRANKE04] Tim Blackwell,
7
+ # Jürgen Branke, "Multi-swarm Optimization in Dynamic Environments",
8
+ # Applications of Evolutionary Computing, pp. 489-500, Springer, 2004.
9
+ # DOI: 10.1007/978-3-540-24653-4_50
10
+ # C_1 is the cognitive acceleration coefficient
11
+ DEFAULT_C1 = 2.05
12
+ # C_2 is the social acceleration coefficient
13
+ DEFAULT_C2 = 2.05
14
+ PHI = DEFAULT_C1 + DEFAULT_C2
15
+ # \omega is the inertia weight
16
+ DEFAULT_OMEGA = 2.0 / (2 - PHI - Math.sqrt(PHI ** 2 - 4.0 * PHI)).abs
17
+
18
+ # \alpha is the inertia weight
19
+ # According to [SUN11], this looks like a sensible default parameter
20
+ DEFAULT_ALPHA = 0.75
21
+
22
+ extend Forwardable
23
+ def_delegators :@particles, :each
24
+
25
+ include Enumerable
26
+
27
+ def update_attractor
28
+ # get the particle attractors
29
+ particle_attractors = @particles.map { |p| p.attractor }
30
+
31
+ # update swarm attractor (if needed)
32
+ if @swarm_attractor.nil?
33
+ @swarm_attractor = particle_attractors.max_by {|p| p[:height] }
34
+ else
35
+ @swarm_attractor = [ @swarm_attractor, *particle_attractors ].max_by {|p| p[:height] }
36
+ end
37
+
38
+ @swarm_attractor
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,7 @@
1
1
  require 'concurrent'
2
2
  require 'erv'
3
+ require 'facter'
4
+ require 'logger'
3
5
 
4
6
  require 'mhl/bitstring_genotype_space'
5
7
  require 'mhl/integer_genotype_space'
@@ -8,6 +10,9 @@ require 'mhl/integer_genotype_space'
8
10
  module MHL
9
11
 
10
12
  class GeneticAlgorithmSolver
13
+ # mutation_probability is the parameter that controls the intensity of mutation
14
+ attr_reader :mutation_probability
15
+
11
16
  def initialize(opts)
12
17
  @population_size = opts[:population_size].to_i
13
18
  unless @population_size and @population_size.even?
@@ -20,10 +25,10 @@ module MHL
20
25
  @genotype_space = IntegerVectorGenotypeSpace.new(opts[:genotype_space_conf])
21
26
 
22
27
  begin
23
- p_m = opts[:mutation_probability].to_f
28
+ @mutation_probability = opts[:mutation_probability].to_f
24
29
  @mutation_rv = \
25
30
  ERV::RandomVariable.new(:distribution => :geometric,
26
- :probability_of_success => p_m)
31
+ :probability_of_success => @mutation_probability)
27
32
  rescue
28
33
  raise ArgumentError, 'Mutation probability configuration is wrong.'
29
34
  end
@@ -49,8 +54,36 @@ module MHL
49
54
 
50
55
  @exit_condition = opts[:exit_condition]
51
56
  @start_population = opts[:genotype_space_conf][:start_population]
57
+
58
+ @controller = opts[:controller]
59
+
60
+ @pool = Concurrent::FixedThreadPool.new(Facter.value(:processorcount).to_i * 4)
61
+
62
+ case opts[:logger]
63
+ when :stdout
64
+ @logger = Logger.new(STDOUT)
65
+ when :stderr
66
+ @logger = Logger.new(STDERR)
67
+ else
68
+ @logger = opts[:logger]
69
+ end
70
+
71
+ @quiet = opts[:quiet]
72
+
73
+ if @logger
74
+ @logger.level = (opts[:log_level] or Logger::WARN)
75
+ end
52
76
  end
53
77
 
78
+ def mutation_probability=(new_mp)
79
+ unless new_mp > 0.0 and new_mp < 1.0
80
+ raise ArgumentError, 'Mutation probability needs to be > 0 and < 1'
81
+ end
82
+ @mutation_probability = new_mp
83
+ @mutation_rv = \
84
+ ERV::RandomVariable.new(:distribution => :geometric,
85
+ :probability_of_success => @mutation_probability)
86
+ end
54
87
 
55
88
  # This is the method that solves the optimization problem
56
89
  #
@@ -74,24 +107,42 @@ module MHL
74
107
  gen = 0
75
108
  overall_best = nil
76
109
 
110
+ population_mutex = Mutex.new
111
+
77
112
  # default behavior is to loop forever
78
113
  begin
79
114
  gen += 1
80
- puts "Starting generation #{gen} at #{Time.now}"
115
+ @logger.info("GA - Starting generation #{gen}") if @logger
116
+
117
+ # create latch to control program termination
118
+ latch = Concurrent::CountDownLatch.new(@population_size)
81
119
 
82
120
  # assess fitness for every member of the population
83
121
  population.each do |s|
84
- s[:task] = Concurrent::Future.new { func.call(s[:genotype]) }
122
+ @pool.post do
123
+ # do we need to syncronize this call through population_mutex?
124
+ # probably not.
125
+ ret = func.call(s[:genotype])
126
+
127
+ # protect write access to population struct using mutex
128
+ population_mutex.synchronize do
129
+ s[:fitness] = ret
130
+ end
131
+
132
+ # update latch
133
+ latch.count_down
134
+ end
85
135
  end
86
136
 
87
- # wait for all the evaluations to end
88
- population.each do |s|
89
- s[:fitness] = s[:task].value
90
- end
137
+ # wait for all the threads to terminate
138
+ latch.wait
91
139
 
92
140
  # find fittest member
93
141
  population_best = population.max_by {|x| x[:fitness] }
94
142
 
143
+ # print results
144
+ puts "> gen #{gen}, best: #{population_best[:genotype]}, #{population_best[:fitness]}" unless @quiet
145
+
95
146
  # calculate overall best
96
147
  if overall_best.nil?
97
148
  overall_best = population_best
@@ -99,8 +150,8 @@ module MHL
99
150
  overall_best = [ overall_best, population_best ].max_by {|x| x[:fitness] }
100
151
  end
101
152
 
102
- # print results
103
- puts "> gen #{gen}, best: #{overall_best[:genotype]}, #{overall_best[:fitness]}"
153
+ # execute controller
154
+ @controller.call(self, overall_best) if @controller
104
155
 
105
156
  # selection by binary tournament
106
157
  children = new_generation(population)
@@ -108,6 +159,9 @@ module MHL
108
159
  # update population and generation number
109
160
  population = children
110
161
  end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
162
+
163
+ # return best sample
164
+ overall_best
111
165
  end
112
166
 
113
167
 
@@ -19,13 +19,24 @@ module MHL
19
19
  else
20
20
  raise ArgumentError, 'Recombination function must be either line or intermediate!'
21
21
  end
22
+
23
+ @constraints = opts[:constraints]
24
+ if @constraints and @constraints.size != @dimensions
25
+ raise ArgumentError, 'Constraints must be provided for every dimension!'
26
+ end
27
+
28
+ @logger = opts[:logger]
22
29
  end
23
30
 
24
31
  def get_random
25
32
  if @random_func
26
33
  @random_func.call
27
34
  else
28
- # TODO: implement this
35
+ if @constraints
36
+ @constraints.map{|x| x[:from] + SecureRandom.random_number(x[:to] - x[:from]) }
37
+ else
38
+ raise 'Automated random genotype generation when no constraints are provided is not implemented yet!'
39
+ end
29
40
  end
30
41
  end
31
42
 
@@ -38,22 +49,25 @@ module MHL
38
49
  c2 = { :genotype => p2[:genotype].dup }
39
50
 
40
51
  # mutation comes first
41
- random_geometric_mutation(c1[:genotype], mutation_rv)
42
- random_geometric_mutation(c2[:genotype], mutation_rv)
52
+ random_delta_mutation(c1[:genotype], mutation_rv)
53
+ random_delta_mutation(c2[:genotype], mutation_rv)
43
54
 
44
55
  # and then recombination
45
56
  send(@recombination_func, c1[:genotype], c2[:genotype], recombination_rv)
46
57
 
58
+ if @constraints
59
+ repair_chromosome(c1[:genotype])
60
+ repair_chromosome(c2[:genotype])
61
+ end
62
+
47
63
  return c1, c2
48
64
  end
49
65
 
50
66
 
51
67
  private
52
68
 
53
- def random_geometric_mutation(g, mutation_rv)
69
+ def random_delta_mutation(g, mutation_rv)
54
70
  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
71
  delta = mutation_rv.next
58
72
 
59
73
  if rand() >= 0.5
@@ -77,7 +91,7 @@ module MHL
77
91
  beta = recombination_rv.next
78
92
  t = (alpha * g1[i] + (1.0 - alpha) * g2[i] + 0.5).floor
79
93
  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
94
+ end
81
95
  g1[i] = t
82
96
  g2[i] = s
83
97
  end
@@ -94,10 +108,24 @@ module MHL
94
108
  g1.each_index do |i|
95
109
  t = (alpha * g1[i] + (1.0 - alpha) * g2[i] + 0.5).floor
96
110
  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
111
+ g1[i] = t
112
+ g2[i] = s
113
+ end
114
+ end
115
+
116
+ def repair_chromosome(g)
117
+ g.each_index do |i|
118
+ if g[i] < @constraints[i][:from]
119
+ range = "[#{@constraints[i][:from]},#{@constraints[i][:to]}]"
120
+ @logger.debug "repairing g[#{i}] #{g[i]} to fit within #{range}" if @logger
121
+ g[i] = @constraints[i][:from]
122
+ @logger.debug "g[#{i}] repaired as: #{g[i]}" if @logger
123
+ elsif g[i] > @constraints[i][:to]
124
+ range = "[#{@constraints[i][:from]},#{@constraints[i][:to]}]"
125
+ @logger.debug "repairing g[#{i}] #{g[i]} to fit within #{range}" if @logger
126
+ g[i] = @constraints[i][:to]
127
+ @logger.debug "g[#{i}] repaired as: #{g[i]}" if @logger
128
+ end
101
129
  end
102
130
  end
103
131
 
@@ -0,0 +1,140 @@
1
+ require 'concurrent'
2
+ require 'facter'
3
+ require 'logger'
4
+ require 'matrix'
5
+
6
+ require 'mhl/charged_swarm'
7
+
8
+
9
+ module MHL
10
+ # This solver implements the multiswarm QPSO algorithm, based on a number of
11
+ # charged (QPSO Type 2) and neutral (PSO) swarms.
12
+ #
13
+ # For more information, refer to:
14
+ # [BLACKWELLBRANKE04] Tim Blackwell, Jürgen Branke, "Multi-swarm Optimization
15
+ # in Dynamic Environments", Applications of Evolutionary Computing, pp.
16
+ # 489-500, Springer, 2004. DOI: 10.1007/978-3-540-24653-4_50
17
+ class MultiSwarmQPSOSolver
18
+
19
+ def initialize(opts={})
20
+ @swarm_size = opts[:swarm_size].to_i
21
+ unless @swarm_size
22
+ raise ArgumentError, 'Swarm size is a required parameter!'
23
+ end
24
+
25
+ @num_swarms = opts[:num_swarms].to_i
26
+ unless @num_swarms
27
+ raise ArgumentError, 'Number of swarms is a required parameter!'
28
+ end
29
+
30
+ @random_position_func = opts[:random_position_func]
31
+ @random_velocity_func = opts[:random_velocity_func]
32
+
33
+ @start_positions = opts[:start_positions]
34
+ @exit_condition = opts[:exit_condition]
35
+
36
+ @pool = Concurrent::FixedThreadPool.new(Facter.value(:processorcount).to_i * 4)
37
+
38
+ case opts[:logger]
39
+ when :stdout
40
+ @logger = Logger.new(STDOUT)
41
+ when :stderr
42
+ @logger = Logger.new(STDERR)
43
+ else
44
+ @logger = opts[:logger]
45
+ end
46
+
47
+ @quiet = opts[:quiet]
48
+
49
+ if @logger
50
+ @logger.level = opts[:log_level] or Logger::WARN
51
+ end
52
+ end
53
+
54
+ # This is the method that solves the optimization problem
55
+ #
56
+ # Parameter func is supposed to be a method (or a Proc, a lambda, or any callable
57
+ # object) that accepts the genotype as argument (that is, the set of
58
+ # parameters) and returns the phenotype (that is, the function result)
59
+ def solve(func, params={})
60
+
61
+ # setup particles
62
+ if @start_positions.nil?
63
+ swarms = Array.new(@num_swarms) do |index|
64
+ ChargedSwarm.new(@swarm_size,
65
+ Array.new(@swarm_size) { Vector[*@random_position_func.call] },
66
+ Array.new(@swarm_size / 2) { Vector[*@random_velocity_func.call] },
67
+ params)
68
+ end
69
+ else
70
+ raise 'Unimplemented yet!'
71
+ # particles = @start_positions.each_slice(2).map do |pos,vel|
72
+ # { position: Vector[*pos] }
73
+ # end
74
+ end
75
+
76
+ # initialize variables
77
+ gen = 0
78
+ overall_best = nil
79
+
80
+ # default behavior is to loop forever
81
+ begin
82
+ gen += 1
83
+ @logger.info "MSQPSO - Starting generation #{gen}" if @logger
84
+
85
+ # create latch to control program termination
86
+ latch = Concurrent::CountDownLatch.new(@num_swarms * @swarm_size)
87
+
88
+ # assess height for every particle
89
+ swarms.each do |s|
90
+ s.each do |particle|
91
+ @pool.post do
92
+ # evaluate target function
93
+ particle.evaluate(func)
94
+ # update latch
95
+ latch.count_down
96
+ end
97
+ end
98
+ end
99
+
100
+ # wait for all the evaluations to end
101
+ latch.wait
102
+
103
+ # update attractors (the highest particle in each swarm)
104
+ swarm_attractors = swarms.map {|s| s.update_attractor }
105
+
106
+ best_attractor = swarm_attractors.max_by {|x| x[:height] }
107
+
108
+ # print results
109
+ puts "> gen #{gen}, best: #{best_attractor[:position]}, #{best_attractor[:height]}" unless @quiet
110
+
111
+ # calculate overall best
112
+ if overall_best.nil?
113
+ overall_best = best_attractor
114
+ else
115
+ overall_best = [ overall_best, best_attractor ].max_by {|x| x[:height] }
116
+ end
117
+
118
+ # exclusion phase
119
+ # this phase is necessary to preserve diversity between swarms. we need
120
+ # to ensure that swarm attractors are distant at least r_{excl} units
121
+ # from each other. if the attractors of two swarms are closer than
122
+ # r_{excl}, we randomly reinitialize the worst of those swarms.
123
+ # TODO: IMPLEMENT
124
+
125
+ # anti-convergence phase
126
+ # this phase is necessary to ensure that a swarm is "spread" enough to
127
+ # effectively follow the movements of a "peak" in the solution space.
128
+ # TODO: IMPLEMENT
129
+
130
+ # mutate swarms
131
+ swarms.each {|s| s.mutate }
132
+
133
+ end while @exit_condition.nil? or !@exit_condition.call(gen, overall_best)
134
+
135
+ overall_best
136
+ end
137
+
138
+ end
139
+
140
+ end