mhl 0.0.1 → 0.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 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