File Coverage

blib/lib/AI/PSO.pm
Criterion Covered Total %
statement 147 164 89.6
branch 35 54 64.8
condition n/a
subroutine 18 19 94.7
pod 4 15 26.6
total 204 252 80.9


line stmt bran cond sub pod time code
1             package AI::PSO;
2              
3 1     1   24139 use strict;
  1         2  
  1         42  
4 1     1   6 use warnings;
  1         2  
  1         30  
5 1     1   912 use Math::Random;
  1         7356  
  1         112  
6 1     1   830 use Callback;
  1         1534  
  1         2552  
7              
8             require Exporter;
9              
10             our @ISA = qw(Exporter);
11              
12             our @EXPORT = qw(
13             pso_set_params
14             pso_register_fitness_function
15             pso_optimize
16             pso_get_solution_array
17             );
18              
19             our $VERSION = '0.86';
20              
21              
22             ######################## BEGIN MODULE CODE #################################
23              
24             #---------- BEGIN GLOBAL PARAMETERS ------------
25              
26             #-#-# search parameters #-#-#
27             my $numParticles = 'null'; # This is the number of particles that actually search the problem hyperspace
28             my $numNeighbors = 'null'; # This is the number of neighboring particles that each particle shares information with
29             # which must obviously be less than the number of particles and greater than 0.
30             # TODO: write code to preconstruct different topologies. Such as fully connected, ring, star etc.
31             # Currently, neighbors are chosen by a simple hash function.
32             # It would be fun (no theoretical benefit that I know of) to play with different topologies.
33             my $maxIterations = 'null'; # This is the maximum number of optimization iterations before exiting if the fitness goal is never reached.
34             my $exitFitness = 'null'; # this is the exit criteria. It must be a value between 0 and 1.
35             my $dimensions = 'null'; # this is the number of variables the user is optimizing
36              
37              
38             #-#-# pso position parameters #-#-#
39             my $deltaMin = 'null'; # This is the minimum scalar position change value when searching
40             my $deltaMax = 'null'; # This is the maximum scalar position change value when searching
41              
42             #-#-# my 'how much do I trust myself verses my neighbors' parameters #-#-#
43             my $meWeight = 'null'; # 'individuality' weighting constant (higher weight (than group) means trust individual more, neighbors less)
44             my $meMin = 'null'; # 'individuality' minimum random weight (this should really be between 0, 1)
45             my $meMax = 'null'; # 'individuality' maximum random weight (this should really be between 0, 1)
46             my $themWeight = 'null'; # 'social' weighting constant (higher weight (than individual) means trust group more, self less)
47             my $themMin = 'null'; # 'social' minimum random weight (this should really be between 0, 1)
48             my $themMax = 'null'; # 'social' maximum random weight (this should really be between 0, 1)
49              
50             my $psoRandomRange = 'null'; # PSO::.86 new variable to support original unmodified algorithm
51             my $useModifiedAlgorithm = 'null';
52              
53             #-#-# user/debug parameters #-#-#
54             my $verbose = 0; # This one defaults for obvious reasons...
55              
56             #NOTE: $meWeight and $themWeight should really add up to a constant value.
57             # Swarm Intelligence defines a 'pso random range' constant and then computes two random numbers
58             # within this range by first getting a random number and then subtracting it from the range.
59             # e.g.
60             # $randomRange = 4.0
61             # $meWeight = random(0, $randomRange);
62             # $themWeight = $randomRange - $meWeight.
63             #
64             #
65              
66             #---------- END GLOBAL PARAMETERS ------------
67              
68             #---------- BEGIN GLOBAL DATA STRUCTURES --------
69             #
70             # a particle is a hash of arrays of positions and velocities:
71             #
72             # The position of a particle in the problem hyperspace is defined by the values in the position array...
73             # You can think of each array value as being a dimension,
74             # so in N-dimensional hyperspace, the size of the position vector is N
75             #
76             # A particle updates its position according the Euler integration equation for physical motion:
77             # Xi(t) = Xi(t-1) + Vi(t)
78             # The velocity portion of this contains the stochastic elements of PSO and is defined as:
79             # Vi(t) = Vi(t-1) + P1*[pi - Xi(t-1)] + P2*[pg - Xi(t-1)]
80             # where P1 and P2 add are two random values who's sum adds up to the PSO random range (4.0)
81             # and pi is the individual's best location
82             # and pg is the global (or neighborhoods) best position
83             #
84             # The velocity vector is obviously updated before the position vector...
85             #
86             #
87             my @particles = ();
88             my $user_fitness_function;
89             my @solution = ();
90             #---------- END GLOBAL DATA STRUCTURES --------
91              
92              
93             #---------- BEGIN EXPORTED SUBROUTINES ----------
94              
95             #
96             # pso_set_params
97             # - sets the global module parameters from the hash passed in
98             #
99             sub pso_set_params(%) {
100 2     2 1 310 my (%params) = %{$_[0]};
  2         33  
101 2         7 my $retval = 0;
102              
103             #no strict 'refs';
104             #foreach my $key (keys(%params)) {
105             # $$key = $params{$key};
106             #}
107             #use strict 'refs';
108              
109 2 50       10 $numParticles = defined($params{numParticles}) ? $params{numParticles} : 'null';
110 2 50       5 $numNeighbors = defined($params{numNeighbors}) ? $params{numNeighbors} : 'null';
111 2 50       7 $maxIterations = defined($params{maxIterations}) ? $params{maxIterations} : 'null';
112 2 50       6 $dimensions = defined($params{dimensions}) ? $params{dimensions} : 'null';
113 2 50       7 $exitFitness = defined($params{exitFitness}) ? $params{exitFitness} : 'null';
114 2 50       9 $deltaMin = defined($params{deltaMin}) ? $params{deltaMin} : 'null';
115 2 50       9 $deltaMax = defined($params{deltaMax}) ? $params{deltaMax} : 'null';
116 2 50       7 $meWeight = defined($params{meWeight}) ? $params{meWeight} : 'null';
117 2 50       6 $meMin = defined($params{meMin}) ? $params{meMin} : 'null';
118 2 50       5 $meMax = defined($params{meMax}) ? $params{meMax} : 'null';
119 2 50       7 $themWeight = defined($params{themWeight}) ? $params{themWeight} : 'null';
120 2 50       7 $themMin = defined($params{themMin}) ? $params{themMin} : 'null';
121 2 50       7 $themMax = defined($params{themMax}) ? $params{themMax} : 'null';
122              
123 2 100       20 $psoRandomRange = defined($params{psoRandomRange}) ? $params{psoRandomRange} : 'null';
124              
125 2 50       7 $verbose = defined($params{verbose}) ? $params{verbose} : $verbose;
126              
127 2         4 my $param_string;
128 2 100       12 if($psoRandomRange =~ m/null/) {
129 1         27 $param_string = "$numParticles:$numNeighbors:$maxIterations:$dimensions:$exitFitness:$deltaMin:$deltaMax:$meWeight:$meMin:$meMax:$themWeight:$themMin:$themMax";
130             } else {
131 1         11 $param_string = "$numParticles:$numNeighbors:$maxIterations:$dimensions:$exitFitness:$deltaMin:$deltaMax:$psoRandomRange";
132             }
133            
134 2 50       10 $retval = 1 if($param_string =~ m/null/);
135              
136 2         18 return $retval;
137             }
138              
139              
140             #
141             # pso_register_fitness_function
142             # - sets the user-defined callback fitness function
143             #
144             sub pso_register_fitness_function($) {
145 2     2 1 5 my ($func) = @_;
146 2         4 $user_fitness_function = new Callback(\&{"main::$func"});
  2         20  
147 2         78 return 0;
148             }
149              
150              
151             #
152             # pso_optimize
153             # - runs the particle swarm optimization algorithm
154             #
155             sub pso_optimize() {
156 2     2 1 7 &init();
157 2         28 return &swarm();
158             }
159              
160             #
161             # pso_get_solution_array
162             # - returns the array of parameters corresponding to the best solution so far
163             sub pso_get_solution_array() {
164 2     2 1 9 return @solution;
165             }
166              
167              
168             #---------- END EXPORTED SUBROUTINES ----------
169              
170              
171              
172             #--------- BEGIN INTERNAL SUBROUTINES -----------
173              
174             #
175             # init
176             # - initializes global variables
177             # - initializes particle data structures
178             #
179             sub init() {
180 2 100   2 0 11 if($psoRandomRange =~ m/null/) {
181 1         10 $useModifiedAlgorithm = 1;
182             } else {
183 1         2 $useModifiedAlgorithm = 0;
184             }
185 2         9 &initialize_particles();
186             }
187              
188             #
189             # initialize_particles
190             # - sets up internal data structures
191             # - initializes particle positions and velocities with an element of randomness
192             #
193             sub initialize_particles() {
194 2     2 0 7 for(my $p = 0; $p < $numParticles; $p++) {
195 8         153 $particles[$p] = {}; # each particle is a hash of arrays with the array sizes being the dimensionality of the problem space
196 8         32 $particles[$p]{nextPos} = []; # nextPos is the array of positions to move to on the next positional update
197 8         21 $particles[$p]{bestPos} = []; # bestPos is the position of that has yielded the best fitness for this particle (it gets updated when a better fitness is found)
198 8         16 $particles[$p]{currPos} = []; # currPos is the current position of this particle in the problem space
199 8         15 $particles[$p]{velocity} = []; # velocity ... come on ...
200              
201 8         21 for(my $d = 0; $d < $dimensions; $d++) {
202 32         266 $particles[$p]{nextPos}[$d] = &random($deltaMin, $deltaMax);
203 32         314 $particles[$p]{currPos}[$d] = &random($deltaMin, $deltaMax);
204 32         298 $particles[$p]{bestPos}[$d] = &random($deltaMin, $deltaMax);
205 32         296 $particles[$p]{velocity}[$d] = &random($deltaMin, $deltaMax);
206             }
207             }
208             }
209              
210              
211              
212             #
213             # initialize_neighbors
214             # NOTE: I made this a separate subroutine so that different topologies of neighbors can be created and used instead of this.
215             # NOTE: This subroutine is currently not used because we access neighbors by index to the particle array rather than storing their references
216             #
217             # - adds a neighbor array to the particle hash data structure
218             # - sets the neighbor based on the default neighbor hash function
219             #
220             sub initialize_neighbors() {
221 0     0 0 0 for(my $p = 0; $p < $numParticles; $p++) {
222 0         0 for(my $n = 0; $n < $numNeighbors; $n++) {
223 0         0 $particles[$p]{neighbor}[$n] = $particles[&get_index_of_neighbor($p, $n)];
224             }
225             }
226             }
227              
228              
229             sub dump_particle($) {
230 2     2 0 8 $| = 1;
231 2         4 my ($index) = @_;
232 2         55 print STDERR "[particle $index]\n";
233 2         3 print STDERR "\t[bestPos] ==> " . &compute_fitness(@{$particles[$index]{bestPos}}) . "\n";
  2         9  
234 2         5 foreach my $pos (@{$particles[$index]{bestPos}}) {
  2         6  
235 8         54 print STDERR "\t\t$pos\n";
236             }
237 2         5 print STDERR "\t[currPos] ==> " . &compute_fitness(@{$particles[$index]{currPos}}) . "\n";
  2         7  
238 2         5 foreach my $pos (@{$particles[$index]{currPos}}) {
  2         6  
239 8         55 print STDERR "\t\t$pos\n";
240             }
241 2         6 print STDERR "\t[nextPos] ==> " . &compute_fitness(@{$particles[$index]{nextPos}}) . "\n";
  2         5  
242 2         5 foreach my $pos (@{$particles[$index]{nextPos}}) {
  2         5  
243 8         63 print STDERR "\t\t$pos\n";
244             }
245 2         10 print STDERR "\t[velocity]\n";
246 2         5 foreach my $pos (@{$particles[$index]{velocity}}) {
  2         5  
247 8         48 print STDERR "\t\t$pos\n";
248             }
249             }
250              
251             #
252             # swarm
253             # - runs the particle swarm algorithm
254             #
255             sub swarm() {
256 2     2 0 8 for(my $iter = 0; $iter < $maxIterations; $iter++) {
257 25         59 for(my $p = 0; $p < $numParticles; $p++) {
258              
259             ## update position
260 100         198 for(my $d = 0; $d < $dimensions; $d++) {
261 400         985 $particles[$p]{currPos}[$d] = $particles[$p]{nextPos}[$d];
262             }
263              
264             ## test _current_ fitness of position
265 100         116 my $fitness = &compute_fitness(@{$particles[$p]{currPos}});
  100         228  
266             # if this position in hyperspace is the best so far...
267 100 100       146 if($fitness > &compute_fitness(@{$particles[$p]{bestPos}})) {
  100         326  
268             # for each dimension, set the best position as the current position
269 16         37 for(my $d2 = 0; $d2 < $dimensions; $d2++) {
270 64         166 $particles[$p]{bestPos}[$d2] = $particles[$p]{currPos}[$d2];
271             }
272             }
273              
274             ## check for exit criteria
275 100 100       194 if($fitness >= $exitFitness) {
276             #...write solution
277 2         22 print "Y:$iter:$p:$fitness\n";
278 2         6 &save_solution(@{$particles[$p]{bestPos}});
  2         8  
279 2         7 &dump_particle($p);
280 2         36 return 0;
281             } else {
282 98 50       177 if($verbose == 1) {
283 98         1427 print "N:$iter:$p:$fitness\n"
284             }
285 98 50       341 if($verbose == 2) {
286 0         0 &dump_particle($p);
287             }
288             }
289             }
290              
291             ## at this point we've updated our position, but haven't reached the end of the search
292             ## so we turn to our neighbors for help.
293             ## (we see if they are doing any better than we are,
294             ## and if so, we try to fly over closer to their position)
295              
296 23         54 for(my $p = 0; $p < $numParticles; $p++) {
297 92         145 my $n = &get_index_of_best_fit_neighbor($p);
298 92         137 my @meDelta = (); # array of self position updates
299 92         100 my @themDelta = (); # array of neighbor position updates
300 92         196 for(my $d = 0; $d < $dimensions; $d++) {
301 368 100       532 if($useModifiedAlgorithm) { # this if shold be moved out much further, but i'm working on code refactoring first
302 272         418 my $meFactor = $meWeight * &random($meMin, $meMax);
303 272         2556 my $themFactor = $themWeight * &random($themMin, $themMax);
304 272         2597 $meDelta[$d] = $particles[$p]{bestPos}[$d] - $particles[$p]{currPos}[$d];
305 272         548 $themDelta[$d] = $particles[$n]{bestPos}[$d] - $particles[$p]{currPos}[$d];
306 272         501 my $delta = ($meFactor * $meDelta[$d]) + ($themFactor * $themDelta[$d]);
307 272         374 $delta += $particles[$p]{velocity}[$d];
308              
309             # do the PSO position and velocity updates
310 272         398 $particles[$p]{velocity}[$d] = &clamp_velocity($delta);
311 272         1101 $particles[$p]{nextPos}[$d] = $particles[$p]{currPos}[$d] + $particles[$p]{velocity}[$d];
312             } else {
313 96         161 my $rho1 = &random(0, $psoRandomRange);
314 96         941 my $rho2 = $psoRandomRange - $rho1;
315 96         218 $meDelta[$d] = $particles[$p]{bestPos}[$d] - $particles[$p]{currPos}[$d];
316 96         193 $themDelta[$d] = $particles[$n]{bestPos}[$d] - $particles[$p]{currPos}[$d];
317 96         174 my $delta = ($rho1 * $meDelta[$d]) + ($rho2 * $themDelta[$d]);
318 96         138 $delta += $particles[$p]{velocity}[$d];
319              
320             # do the PSO position and velocity updates
321 96         166 $particles[$p]{velocity}[$d] = &clamp_velocity($delta);
322 96         425 $particles[$p]{nextPos}[$d] = $particles[$p]{currPos}[$d] + $particles[$p]{velocity}[$d];
323             }
324             }
325             }
326              
327             }
328              
329             #
330             # at this point we have exceeded the maximum number of iterations, so let's at least print out the best result so far
331             #
332 0         0 print STDERR "MAX ITERATIONS REACHED WITHOUT MEETING EXIT CRITERION...printing best solution\n";
333 0         0 my $bestFit = -1;
334 0         0 my $bestPartIndex = -1;
335 0         0 for(my $p = 0; $p < $numParticles; $p++) {
336 0         0 my $endFit = &compute_fitness(@{$particles[$p]{bestPos}});
  0         0  
337 0 0       0 if($endFit >= $bestFit) {
338 0         0 $bestFit = $endFit;
339 0         0 $bestPartIndex = $p;
340             }
341            
342             }
343 0         0 &save_solution(@{$particles[$bestPartIndex]{bestPos}});
  0         0  
344 0         0 &dump_particle($bestPartIndex);
345 0         0 return 1;
346             }
347              
348             #
349             # save solution
350             # - simply copies the given array into the global solution array
351             #
352             sub save_solution(@) {
353 2     2 0 8 @solution = @_;
354             }
355              
356              
357             #
358             # compute_fitness
359             # - computes the fitness of a particle by using the user-specified fitness function
360             #
361             # NOTE: I originally had a 'fitness cache' so that particles that stumbled upon the same
362             # position wouldn't have to recalculate their fitness (which is often expensive).
363             # However, this may be undesirable behavior for the user (if you come across the same position
364             # then you may be settling in on a local maxima so you might want to randomize things and
365             # keep searching. For this reason, I'm leaving the cache out. It would be trivial
366             # for users to implement their own cache since they are passed the same array of values.
367             #
368             sub compute_fitness(@) {
369 641     641 0 1172 my (@values) = @_;
370 641         672 my $return_fitness = 0;
371              
372             # no strict 'refs';
373             # if(defined(&{"main::$user_fitness_function"})) {
374             # $return_fitness = &$user_fitness_function(@values);
375             # } else {
376             # warn "error running user_fitness_function\n";
377             # exit 1;
378             # }
379             # use strict 'refs';
380              
381 641         1640 $return_fitness = $user_fitness_function->call(@values);
382              
383 641         15387 return $return_fitness;
384             }
385              
386              
387             #
388             # random
389             # - returns a random number that is between the first and second arguments using the Math::Random module
390             #
391             sub random($$) {
392 768     768 0 923 my ($min, $max) = @_;
393 768         1719 return random_uniform(1, $min, $max)
394             }
395              
396              
397             #
398             # get_index_of_neighbor
399             #
400             # - returns the index of Nth neighbor of the index for particle P
401             # ==> A neighbor is one of the next K particles following P where K is the neighborhood size.
402             # So, particle 1 has neighbors 2, 3, 4, 5 if K = 4. particle 4 has neighbors 5, 6, 7, 8
403             # ...
404             #
405             sub get_index_of_neighbor($$) {
406 276     276 0 315 my ($particleIndex, $neighborNum) = @_;
407             # TODO: insert error checking code / defensive programming
408 276         451 return ($particleIndex + $neighborNum) % $numParticles;
409             }
410              
411              
412             #
413             # get_index_of_best_fit_neighbor
414             # - returns the index of the neighbor with the best fitness (when given a particle index)...
415             #
416             sub get_index_of_best_fit_neighbor($) {
417 92     92 0 101 my ($particleIndex) = @_;
418 92         91 my $bestNeighborFitness = 0;
419 92         95 my $bestNeighborIndex = 0;
420 92         95 my $particleNeighborIndex = 0;
421 92         208 for(my $neighbor = 0; $neighbor < $numNeighbors; $neighbor++) {
422 276         443 $particleNeighborIndex = &get_index_of_neighbor($particleIndex, $neighbor);
423 276 100       303 if(&compute_fitness(@{$particles[$particleNeighborIndex]{bestPos}}) > $bestNeighborFitness) {
  276         673  
424 159         161 $bestNeighborFitness = &compute_fitness(@{$particles[$particleNeighborIndex]{bestPos}});
  159         393  
425 159         457 $bestNeighborIndex = $particleNeighborIndex;
426             }
427             }
428             # TODO: insert error checking code / defensive programming
429 92         187 return $particleNeighborIndex;
430             }
431              
432             #
433             # clamp_velocity
434             # - restricts the change in velocity to be within a certain range (prevents large jumps in problem hyperspace)
435             #
436             sub clamp_velocity($) {
437 368     368 0 408 my ($dx) = @_;
438 368 100       866 if($dx < $deltaMin) {
    100          
439 116         135 $dx = $deltaMin;
440             } elsif($dx > $deltaMax) {
441 32         36 $dx = $deltaMax;
442             }
443 368         786 return $dx;
444             }
445             #--------- END INTERNAL SUBROUTINES -----------
446              
447              
448             1;
449             ######################## END MODULE CODE #################################
450             __END__