File Coverage

blib/lib/Algorithm/Evolve.pm
Criterion Covered Total %
statement 21 198 10.6
branch 0 72 0.0
condition 0 34 0.0
subroutine 7 34 20.5
pod 8 10 80.0
total 36 348 10.3


line stmt bran cond sub pod time code
1             package Algorithm::Evolve;
2              
3 1     1   13732 use strict;
  1         2  
  1         57  
4 1     1   6 use Carp qw/croak carp/;
  1         4  
  1         84  
5 1     1   7 use List::Util qw/shuffle/;
  1         7  
  1         392  
6              
7             our (%SELECTION, %REPLACEMENT);
8             our $VERSION = '0.03';
9             our $DEBUG = 0;
10              
11             my $rand_max = (1 << 31); ## close enough
12              
13             ###########################
14              
15             sub debug {
16 0 0   0 0   print @_, "\n" if $DEBUG;
17             }
18              
19             sub new {
20 0     0 1   my $pkg = shift;
21              
22 0           my $p = bless {
23             generations => 0,
24             parents_per_gen => 2,
25             @_
26             }, $pkg;
27            
28 0   0       $p->{random_seed} ||= int(rand $rand_max);
29 0           srand( $p->random_seed );
30              
31 0   0       $p->{selection} ||= $p->{replacement};
32 0   0       $p->{replacement} ||= $p->{selection};
33 0   0       $p->{children_per_gen} ||= $p->{parents_per_gen};
34              
35 0           $p->_validate_args;
36              
37 0           return $p;
38             }
39              
40             sub _validate_args {
41 0     0     my $p = shift;
42            
43             {
44 1     1   6 no strict 'refs';
  1         1  
  1         218  
  0            
45 0           croak "Invalid selection/replacement criteria"
46 0           unless *{"Algorithm::Evolve::selection::" . $p->selection}{CODE}
47 0 0 0       and *{"Algorithm::Evolve::replacement::" . $p->replacement}{CODE};
48             }
49              
50 0 0         croak "Please specify the size of the population" unless $p->size;
51 0 0         croak "parents_per_gen must be even" if $p->parents_per_gen % 2;
52 0 0         croak "parents_per_gen must divide children_per_gen"
53             if $p->children_per_gen % $p->parents_per_gen;
54 0 0 0       croak "parents_per_gen and children_per_gen must be no larger than size"
55             if $p->children_per_gen > $p->size
56             or $p->parents_per_gen > $p->size;
57            
58 0           $p->{children_per_parent} = $p->children_per_gen / $p->parents_per_gen;
59              
60             }
61              
62             ############################
63              
64             sub start {
65 0     0 0   my $p = shift;
66 0           $p->_initialize;
67            
68 0           until ($p->is_suspended) {
69 1     1   5 no strict 'refs';
  1         2  
  1         2328  
70            
71             my @parent_indices
72 0           = ("Algorithm::Evolve::selection::" . $p->selection)
73             ->($p, $p->parents_per_gen);
74              
75 0           my @children;
76 0           while (@parent_indices) {
77 0           my @parents = @{$p->critters}[ splice(@parent_indices, 0, 2) ];
  0            
78            
79             push @children, $p->critter_class->crossover(@parents)
80 0           for (1 .. $p->children_per_parent);
81             }
82              
83 0           $_->mutate for @children;
84            
85             my @replace_indices
86 0           = ("Algorithm::Evolve::replacement::" . $p->replacement)
87             ->($p, $p->children_per_gen);
88              
89             ## place the new critters first, then sort. maybe fixme:
90            
91 0           @{$p->critters}[ @replace_indices ] = @children;
  0            
92 0 0         @{$p->fitnesses}[ @replace_indices ] = () if $p->use_fitness;
  0            
93            
94 0           $p->_sort_critters;
95              
96 0           $p->{generations}++;
97 0 0         $p->callback->($p) if (ref $p->callback eq 'CODE');
98             }
99             }
100              
101             ###################
102              
103             sub suspend {
104 0     0 1   my $p = shift;
105 0           $p->{is_suspended} = 1;
106             }
107              
108             sub resume {
109 0     0 1   my $p = shift;
110 0           $p->{is_suspended} = 0;
111 0           $p->start;
112             }
113              
114             sub best_fit {
115 0     0 1   my $p = shift;
116 0 0         carp "It's hard to pick the most fit when fitness is relative!"
117             unless ($p->use_fitness);
118 0           $p->critters->[-1];
119             }
120              
121             sub avg_fitness {
122 0     0 1   my $p = shift;
123 0           my $sum = 0;
124 0           $sum += $_ for @{$p->fitnesses};
  0            
125 0           return $sum / $p->size;
126             }
127              
128             sub selection {
129 0     0 1   my ($p, $method) = @_;
130 0 0         return $p->{selection} unless defined $method;
131 0           $p->{selection} = $method;
132 0           $p->_validate_args;
133 0           return $p->{selection};
134             }
135              
136             sub replacement {
137 0     0 1   my ($p, $method) = @_;
138 0 0         return $p->{replacement} unless defined $method;
139 0           $p->{replacement} = $method;
140 0           $p->_validate_args;
141 0           return $p->{replacement};
142             }
143              
144             sub parents_children_per_gen {
145 0     0 1   my ($p, $parents, $children) = @_;
146 0 0 0       return unless defined $parents and defined $children;
147 0           $p->{parents_per_gen} = $parents;
148 0           $p->{children_per_gen} = $children;
149 0           $p->_validate_args;
150             }
151              
152             ####################
153              
154             sub _initialize {
155 0     0     my $p = shift;
156 0 0         return if defined $p->critters;
157            
158 0           $p->{critters} = [ map { $p->critter_class->new } 1 .. $p->size ];
  0            
159 0           $p->{use_fitness} = !! $p->critters->[0]->can('fitness');
160 0 0         $p->{fitnesses} = [ map { $p->critters->[$_]->fitness } 0 .. $p->size-1 ]
  0            
161             if ($p->use_fitness);
162              
163 0           $p->_sort_critters;
164             }
165              
166              
167             sub _sort_critters {
168 0     0     my $p = shift;
169              
170 0 0         return unless $p->use_fitness;
171              
172 0           my $fitnesses = $p->fitnesses;
173 0           my $critters = $p->critters;
174 0           for (0 .. $p->size-1) {
175 0 0         $fitnesses->[$_] = $critters->[$_]->fitness
176             unless defined $fitnesses->[$_];
177             }
178            
179 0           my @sorted_indices =
180 0           sort { $fitnesses->[$a] <=> $fitnesses->[$b] } 0 .. $p->size-1;
181              
182 0           $p->{critters} = [ @{$critters} [ @sorted_indices ] ];
  0            
183 0           $p->{fitnesses} = [ @{$fitnesses}[ @sorted_indices ] ];
  0            
184             }
185              
186              
187             ############################
188             ## picks N indices randomly, using the given weights
189              
190             sub _pick_n_indices_weighted {
191 0     0     my $num = shift;
192 0           my $relative_prob = shift;
193              
194 0 0         croak("Tried to pick $num items, with only " . @$relative_prob . " choices!")
195             if $num > @$relative_prob;
196            
197 0           my $sum = 0;
198 0           $sum += $_ for @$relative_prob;
199              
200 0           my @indices;
201            
202 0           while ($num--) {
203 0           my $dart = rand($sum);
204 0           my $index = -1;
205            
206 0           $dart -= $relative_prob->[++$index] while ($dart > 0);
207            
208 0           $sum -= $relative_prob->[$index];
209 0           $relative_prob->[$index] = 0;
210 0           push @indices, $index;
211             }
212            
213 0           return @indices;
214             }
215              
216             #############################
217             ## Selection / replacement routines: these take a population object and a
218             ## number, and return a list of indices. Keep in mind that the critter
219             ## array is already sorted by fitness.
220              
221             #############################
222              
223             ## these two go crazy with negative fitness values. fixme later maybe
224              
225             sub Algorithm::Evolve::selection::roulette {
226 0     0     my ($p, $num) = @_;
227 0 0         croak "Can't use roulette selection/replacement without a fitness function"
228             unless ($p->use_fitness);
229 0           _pick_n_indices_weighted( $num, [ @{$p->fitnesses} ] );
  0            
230             };
231              
232             sub Algorithm::Evolve::replacement::roulette {
233 0     0     my ($p, $num) = @_;
234 0 0         croak "Can't use roulette selection/replacement without a fitness function"
235             unless ($p->use_fitness);
236 0           _pick_n_indices_weighted( $num, [ map { 1/($_+1) } @{$p->fitnesses} ] );
  0            
  0            
237             };
238              
239             ###############
240            
241             sub Algorithm::Evolve::selection::rank {
242 0     0     my ($p, $num) = @_;
243 0 0         croak "Can't use rank selection/replacement without a fitness function"
244             unless ($p->use_fitness);
245 0           _pick_n_indices_weighted( $num, [ 1 .. $p->size ] );
246             };
247            
248             sub Algorithm::Evolve::replacement::rank {
249 0     0     my ($p, $num) = @_;
250 0 0         croak "Can't use rank selection/replacement without a fitness function"
251             unless ($p->use_fitness);
252 0           _pick_n_indices_weighted( $num, [ reverse(1 .. $p->size) ] );
253             };
254              
255             ###############
256              
257             sub Algorithm::Evolve::selection::random {
258 0     0     my ($p, $num) = @_;
259 0           _pick_n_indices_weighted( $num, [ (1) x $p->size ] );
260              
261             }
262             sub Algorithm::Evolve::replacement::random {
263 0     0     my ($p, $num) = @_;
264 0           _pick_n_indices_weighted( $num, [ (1) x $p->size ] );
265             };
266              
267             ################
268              
269             sub Algorithm::Evolve::selection::absolute {
270 0     0     my ($p, $num) = @_;
271 0 0         croak "Can't use absolute selection/replacement without a fitness function"
272             unless ($p->use_fitness);
273 0           return ( $p->size - $num .. $p->size - 1 );
274             };
275              
276             sub Algorithm::Evolve::replacement::absolute {
277 0     0     my ($p, $num) = @_;
278 0 0         croak "Can't use absolute selection/replacement without a fitness function"
279             unless ($p->use_fitness);
280 0           return ( 0 .. $num-1 );
281             };
282              
283             ################
284              
285             my @tournament_replace_indices;
286             my $tournament_warn = 0;
287            
288             sub Algorithm::Evolve::selection::tournament {
289 0     0     my ($p, $num) = @_;
290 0           my $t_size = $p->{tournament_size};
291            
292 0 0 0       croak "Invalid (or no) tournament size specified"
      0        
293             if not defined $t_size or $t_size < 2 or $t_size > $p->size;
294 0 0         croak "Tournament size * #tournaments must be no greater than population size"
295             if ($num/2) * $t_size > $p->size;
296 0 0 0       carp "Tournament selection without tournament replacement is insane"
297             unless ($p->replacement eq 'tournament' or $tournament_warn++);
298            
299 0           my $tournament_groups = $num / 2;
300 0           my @indices = shuffle(0 .. $p->size-1);
301 0           my @tournament_choose_indices =
302             @tournament_replace_indices = ();
303            
304 0           for my $i (0 .. $tournament_groups-1) {
305 0           my $beg = $t_size * $i;
306 0           my $end = $beg + $t_size - 1;
307            
308             ## the critters are already sorted by fitness within $p->critters --
309             ## so we can sort them by their index number, without having to
310             ## consult the fitness function (or fitness array) again.
311              
312 0           my @sorted_group_indices = sort { $b <=> $a } @indices[ $beg .. $end ];
  0            
313 0           push @tournament_choose_indices, @sorted_group_indices[0,1];
314 0           push @tournament_replace_indices, @sorted_group_indices[-2,-1];
315             }
316              
317 0           return @tournament_choose_indices;
318             };
319              
320             sub Algorithm::Evolve::replacement::tournament {
321 0     0     my ($p, $num) = @_;
322 0 0         croak "parents_per_gen must equal children_per_gen with tournament selection"
323             if @tournament_replace_indices != $num;
324 0 0         croak "Can't use tournament replacement without tournament selection"
325             unless ($p->selection eq 'tournament');
326            
327 0           return @tournament_replace_indices;
328             };
329              
330             #######################################
331              
332             my @gladitorial_replace_indices;
333             my $gladitorial_warn = 0;
334             my $gladitorial_attempts_warn = 0;
335              
336             sub Algorithm::Evolve::selection::gladitorial {
337 0     0     my ($p, $num) = @_;
338            
339 0 0 0       carp "Gladitorial selection without gladitorial replacement is insane"
340             unless ($p->replacement eq 'gladitorial' or $gladitorial_warn++);
341              
342 0   0       my $max_attempts = $p->{max_gladitorial_attempts} || 100;
343 0           my $fetched = 0;
344 0           my $attempts = 0;
345            
346 0           my @available_indices = 0 .. $#{$p->critters};
  0            
347 0           my @gladitorial_select_indices =
348             @gladitorial_replace_indices = ();
349            
350 0           while ($fetched != $p->parents_per_gen) {
351 0           my ($i1, $i2) = (shuffle @available_indices)[0,1];
352              
353 0 0         if ($attempts++ > $max_attempts) {
354 0 0         carp "Max gladitorial attempts exceeded -- choosing at random"
355             unless $gladitorial_attempts_warn++;
356 0           my $remaining = $p->parents_per_gen - @gladitorial_select_indices;
357              
358 0           push @gladitorial_replace_indices,
359             (shuffle @available_indices)[0 .. $remaining-1];
360 0           push @gladitorial_select_indices,
361             (shuffle @available_indices)[0 .. $remaining-1];
362              
363 0           last;
364             }
365            
366 0           my $cmp = $p->critter_class->compare( @{$p->critters}[$i1, $i2] );
  0            
367            
368 0 0         next if $cmp == 0; ## tie
369            
370 0 0         my ($select, $remove) = $cmp > 0 ? ($i1,$i2) : ($i2,$i1);
371 0           @available_indices = grep { $_ != $remove } @available_indices;
  0            
372            
373 0           push @gladitorial_replace_indices, $remove;
374 0           push @gladitorial_select_indices, $select;
375 0           $fetched++;
376             }
377              
378 0           return @gladitorial_select_indices;
379             };
380              
381             sub Algorithm::Evolve::replacement::gladitorial {
382 0     0     my ($p, $num) = @_;
383 0 0         croak "parents_per_gen must equal children_per_gen with gladitorial selection"
384             if @gladitorial_replace_indices != $num;
385 0 0         croak "Can't use gladitorial replacement without gladitorial selection"
386             unless ($p->selection eq 'gladitorial');
387            
388 0           return @gladitorial_replace_indices;
389             };
390              
391             #######################################
392              
393             BEGIN {
394             ## creates very basic readonly accessors - very loosely based on an
395             ## idea by Juerd in http://perlmonks.org/index.pl?node_id=222941
396              
397 1     1   5 my @fields = qw/critters size generations callback critter_class
398             random_seed is_suspended use_fitness fitnesses
399             parents_per_gen children_per_gen children_per_parent/;
400              
401 1     1   7 no strict 'refs';
  1         1  
  1         91  
402 1         2 for my $f (@fields) {
403 12 0   0   101 *$f = sub { carp "$f method is readonly" if $#_; $_[0]->{$f} };
  0            
  0            
404             }
405             }
406              
407             ##########################################
408             ##########################################
409             ##########################################
410             1;
411             __END__