Class: MHL::ParticleSwarmOptimizationSolver

Inherits:
Object
  • Object
show all
Defined in:
lib/mhl/particle_swarm_optimization_solver.rb

Overview

This solver implements the “canonical” variant of PSO called Constrained PSO. For more information, refer to equation 4.30 of [SUN11].

Constant Summary collapse

DEFAULT_SWARM_SIZE =

This is the default swarm size recommended by SPSO 2011 [CLERC12].

40

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ ParticleSwarmOptimizationSolver

Returns a new instance of ParticleSwarmOptimizationSolver.



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/mhl/particle_swarm_optimization_solver.rb', line 16

def initialize(opts={})
  @swarm_size = opts[:swarm_size].try(:to_i) || DEFAULT_SWARM_SIZE

  @constraints = opts[:constraints]

  @random_position_func = opts[:random_position_func]
  @random_velocity_func = opts[:random_velocity_func]

  @start_positions  = opts[:start_positions]
  @start_velocities = opts[:start_velocities]

  @exit_condition  = opts[:exit_condition]

  case opts[:logger]
  when :stdout
    @logger = Logger.new(STDOUT)
  when :stderr
    @logger = Logger.new(STDERR)
  else
    @logger = opts[:logger]
  end

  @quiet = opts[:quiet]

  if @logger
    @logger.level = (opts[:log_level] or :warn)
  end
end

Instance Method Details

#solve(func, params = {}) ⇒ Object

This is the method that solves the optimization problem

Parameter func is supposed to be a method (or a Proc, a lambda, or any callable object) that accepts the genotype as argument (that is, the set of parameters) and returns the phenotype (that is, the function result)



50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/mhl/particle_swarm_optimization_solver.rb', line 50

def solve(func, params={})

  # initialize particle positions
  init_pos = if @start_positions
    # start positions have the highest priority
    @start_positions
  elsif @random_position_func
    # random_position_func has the second highest priority
    Array.new(@swarm_size) { @random_position_func.call }
  elsif @constraints
    # constraints were given, so we use them to initialize particle
    # positions. to this end, we adopt the SPSO 2006-2011 random position
    # initialization algorithm [CLERC12].
    Array.new(@swarm_size) do
      min = @constraints[:min]
      max = @constraints[:max]
      # randomization is independent along each dimension
      min.zip(max).map do |min_i,max_i|
        min_i + SecureRandom.random_number * (max_i - min_i)
      end
    end
  else
    raise ArgumentError, "Not enough information to initialize particle positions!"
  end

  # initialize particle velocities
  init_vel = if @start_velocities
    # start velocities have the highest priority
    @start_velocities
  elsif @random_velocity_func
    # random_velocity_func has the second highest priority
    Array.new(@swarm_size) { @random_velocity_func.call }
  elsif @constraints
    # constraints were given, so we use them to initialize particle
    # velocities. to this end, we adopt the SPSO 2011 random velocity
    # initialization algorithm [CLERC12].
    init_pos.map do |p|
      min = @constraints[:min]
      max = @constraints[:max]
      # randomization is independent along each dimension
      p.zip(min,max).map do |p_i,min_i,max_i|
        min_vel = min_i - p_i
        max_vel = max_i - p_i
        min_vel + SecureRandom.random_number * (max_vel - min_vel)
      end
    end
  else
    raise ArgumentError, "Not enough information to initialize particle velocities!"
  end

  # setup particles
  swarm = PSOSwarm.new(size: @swarm_size, initial_positions: init_pos,
                       initial_velocities: init_vel,
                       constraints: @constraints, logger: @logger)

  # initialize variables
  iter = 0
  overall_best = nil

  # default behavior is to loop forever
  begin
    iter += 1
    @logger.info("PSO - Starting iteration #{iter}") if @logger

    # assess height for every particle
    if params[:concurrent]
      # the function to optimize is thread safe: call it multiple times in
      # a concurrent fashion
      # to this end, we use the high level promise-based construct
      # recommended by the authors of ruby's (fantastic) concurrent gem
      promises = swarm.map do |particle|
        Concurrent::Promise.execute do
          # evaluate target function
          particle.evaluate(func)
        end
      end

      # wait for all the spawned threads to finish
      promises.map(&:wait)
    else
      # the function to optimize is not thread safe: call it multiple times
      # in a sequential fashion
      swarm.each do |particle|
        # evaluate target function
        particle.evaluate(func)
      end
    end

    # get swarm attractor (the highest particle)
    swarm_attractor = swarm.update_attractor

    # print results
    if @logger and !@quiet
      @logger.info "> iter #{iter}, best: #{swarm_attractor[:position]}, #{swarm_attractor[:height]}"
    end

    # calculate overall best (that plays the role of swarm attractor)
    if overall_best.nil?
      overall_best = swarm_attractor
    else
      overall_best = [ overall_best, swarm_attractor ].max_by {|x| x[:height] }
    end

    # mutate swarm
    swarm.mutate

  end while @exit_condition.nil? or !@exit_condition.call(iter, overall_best)

  overall_best
end