File Coverage

blib/lib/AI/Evolve/Befunge/Population.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package AI::Evolve::Befunge::Population;
2 2     2   74500 use strict;
  2         15  
  2         131  
3 2     2   10 use warnings;
  2         12  
  2         112  
4 2     2   11 use File::Basename;
  2         3  
  2         433  
5 2     2   2220 use IO::File;
  2         2538  
  2         359  
6 2     2   19 use Carp;
  2         4  
  2         145  
7 2     2   1236 use Algorithm::Evolutionary::Wheel;
  0            
  0            
8             use Parallel::Iterator qw(iterate_as_array);
9             use POSIX qw(ceil);
10              
11             use aliased 'AI::Evolve::Befunge::Blueprint' => 'Blueprint';
12             use aliased 'AI::Evolve::Befunge::Physics' => 'Physics';
13             use aliased 'AI::Evolve::Befunge::Migrator' => 'Migrator';
14             use AI::Evolve::Befunge::Util;
15              
16             use base 'Class::Accessor::Fast';
17             __PACKAGE__->mk_accessors( qw{ blueprints config dimensions generation host physics popsize tokens } );
18              
19              
20             =head1 NAME
21              
22             AI::Evolve::Befunge::Population - manage a population
23              
24              
25             =head1 SYNOPSIS
26              
27             use aliased 'AI::Evolve::Befunge::Population' => 'Population';
28             use AI::Evolve::Befunge::Util qw(v nonquiet);
29              
30             $population = Population->new();
31              
32             while(1) {
33             my $gen = $population->generation;
34             nonquiet("generation $gen\n");
35             $population->fight();
36             $population->breed();
37             $population->migrate();
38             $population->save();
39             $population->generation($gen+1);
40             }
41              
42              
43             =head1 DESCRIPTION
44              
45             This manages a population of Befunge AI critters.
46              
47             This is the main evolution engine for AI::Evolve::Befunge. It has
48             all of the steps necessary to evolve a population and generate the
49             next generation. The more times you run this process, the more
50             progress it will (theoretically) make.
51              
52              
53             =head1 CONSTRUCTORS
54              
55             There are two constructors, depending on whether you want to create
56             a new population, or resume a saved one.
57              
58              
59             =head2 new
60              
61             my $population = Population->new(Generation => 50);
62              
63             Creates a Population object. The following arguments may be
64             specified (none are mandatory):
65              
66             Blueprints - a list (array reference) of critters. (Default: [])
67             Generation - the generation number. (Default: 1)
68             Host - the hostname of this Population. (Default: `hostname`)
69              
70             =cut
71              
72             sub new {
73             my ($package, %args) = @_;
74             $args{Host} = $ENV{HOST} unless defined $args{Host};
75             $args{Generation} //= 1;
76             $args{Blueprints} //= [];
77              
78             my $self = bless({
79             host => $args{Host},
80             blueprints => [],
81             generation => $args{Generation},
82             migrate => spawn_migrator(),
83             }, $package);
84              
85             $self->reload_defaults();
86             my $nd = $self->dimensions;
87             my $config = $self->config;
88             my $code_size = v(map { 4 } (1..$nd));
89             my @population;
90              
91             foreach my $code (@{$args{Blueprints}}) {
92             my $chromosome = Blueprint->new(code => $code, dimensions => $nd);
93             push @population, $chromosome;
94             }
95              
96             while(scalar(@population) < $self->popsize()) {
97             my $size = 1;
98             foreach my $component ($code_size->get_all_components()) {
99             $size *= $component;
100             }
101             my $code .= $self->new_code_fragment($size, $config->config('initial_code_density', 90));
102             my $chromosome = AI::Evolve::Befunge::Blueprint->new(code => $code, dimensions => $nd);
103             push @population, $chromosome;
104             }
105             $$self{blueprints} = [@population];
106             return $self;
107             }
108              
109              
110             =head2 load
111              
112             $population->load($filename);
113              
114             Load a savefile, allowing you to pick up where it left off.
115              
116             =cut
117              
118             sub load {
119             my ($package, $savefile) = @_;
120             use IO::File;
121             my @population;
122             my ($generation, $host);
123             $host = $ENV{HOST};
124              
125             my $file = IO::File->new($savefile);
126             croak("cannot open file $savefile") unless defined $file;
127             while(my $line = $file->getline()) {
128             chomp $line;
129             if($line =~ /^generation=(\d+)/) {
130             # the savefile is the *result* of a generation number.
131             # therefore, we start at the following number.
132             $generation = $1 + 1;
133             } elsif($line =~ /^popid=(\d+)/) {
134             # and this tells us where to start assigning new critter ids from.
135             set_popid($1);
136             } elsif($line =~ /^\[/) {
137             push(@population, AI::Evolve::Befunge::Blueprint->new_from_string($line));
138             } else {
139             confess "unknown savefile line: $line\n";
140             }
141             }
142             my $self = bless({
143             host => $host,
144             blueprints => [@population],
145             generation => $generation,
146             migrate => spawn_migrator(),
147             }, $package);
148             $self->reload_defaults();
149             return $self;
150             }
151              
152              
153             =head1 PUBLIC METHODS
154              
155             These methods are intended to be the normal user interface for this
156             module. Their APIs will not change unless I find a very good reason.
157              
158              
159             =head2 reload_defaults
160              
161             $population->reload_defaults();
162              
163             Rehashes the config file, pulls various values from there. This is
164             common initializer code, shared by new() and load(). It defines the
165             values for the following items:
166              
167             =over 4
168              
169             =item boardsize
170              
171             =item config
172              
173             =item dimensions
174              
175             =item physics
176              
177             =item popsize
178              
179             =item tokens
180              
181             =back
182              
183             =cut
184              
185             sub reload_defaults {
186             my $self = shift;
187             my @config_args = (host => $self->host, gen => $self->generation);
188             my $config = custom_config(@config_args);
189             delete($$self{boardsize});
190             my $physics = $config->config('physics', 'ttt');
191             $$self{physics} = Physics->new($physics);
192             $config = custom_config(@config_args, physics => $self->physics->name);
193             $$self{dimensions} = $config->config('dimensions', 3);
194             $$self{popsize} = $config->config('popsize', 40);
195             $$self{tokens} = $config->config('tokens', 2000);
196             $$self{config} = $config;
197             $$self{boardsize} = $$self{physics}->board_size if defined $$self{physics}->board_size;
198             }
199              
200              
201             =head2 fight
202              
203             $population->fight();
204              
205             Determines (through a series of fights) the basic fitness of each
206             critter in the population. The fight routine (see the "double_match"
207             method in Physics.pm) is called a bunch of times in parallel, and the
208             loser dies (is removed from the list). This is repeated until the total
209             population has been reduced to 25% of the "popsize" setting.
210              
211             =cut
212              
213             sub fight {
214             my $self = shift;
215             my $physics = $self->physics;
216             my $popsize = $self->popsize;
217             my $config = $self->config;
218             my $workers = $config->config("cpus", 1);
219             my @population = @{$self->blueprints};
220             my %blueprints = map { $_->name => $_ } (@population);
221             $popsize = ceil($popsize / 4);
222             while(@population > $popsize) {
223             my (@winners, @livers, @fights);
224             while(@population) {
225             my $attacker = shift @population;
226             my $attacked = shift @population;
227             if(!defined($attacked)) {
228             push(@livers, $attacker);
229             } else {
230             push(@fights, [$attacker, $attacked]);
231             }
232             }
233             my @results = iterate_as_array(
234             { workers => $workers },
235             sub {
236             my ($index, $aref) = @_;
237             my ($attacker, $attacked) = @$aref;
238             my $score;
239             $score = $physics->double_match($config, $attacker, $attacked);
240             my $winner = $attacked;
241             $winner = $attacker if $score > -1;
242             return [$winner->name, $score];
243             },
244             \@fights);
245             foreach my $result (@results) {
246             my ($winner, $score) = @$result;
247             $winner = $blueprints{$winner};
248             if($score) {
249             # they actually won
250             push(@winners, $winner);
251             } else {
252             # they merely tied
253             push(@livers, $winner);
254             }
255             }
256             @population = (@winners, @livers);
257             }
258             for(my $i = 0; $i < @population; $i++) {
259             $population[$i]->fitness(@population - $i);
260             }
261             $self->blueprints([@population]);
262             }
263              
264              
265             =head2 breed
266              
267             $population->breed();
268              
269             Bring the population count back up to the "popsize" level, by a
270             process of sexual reproduction. The newly created critters will have
271             a combination of two previously existing ("winners") genetic makeup,
272             plus some random mutation. See the L and L
273             methods, below. There is also a one of 5 chance a critter will be
274             resized, see the L and L methods, below.
275              
276             =cut
277              
278             sub breed {
279             my $self = shift;
280             my $popsize = $self->popsize;
281             my $nd = $self->dimensions;
282             my @population = @{$self->blueprints};
283             my @probs = map { $$_{fitness} } (@population);
284             while(@population < $popsize) {
285             my ($p1, $p2) = $self->pair(@probs);
286             my $child1 = AI::Evolve::Befunge::Blueprint->new(code => $p1->code, dimensions => $nd);
287             my $child2 = AI::Evolve::Befunge::Blueprint->new(code => $p2->code, dimensions => $nd, id => -1);
288             $child1 = $self->grow($child1);
289             $self->crossover($child1, $child2);
290             $self->mutate($child1);
291             $child1 = $self->crop($child1);
292             push @population, $child1;
293             }
294             $self->blueprints([@population]);
295             }
296              
297              
298             =head2 migrate
299              
300             $population->migrate();
301              
302             Send and receive critters to/from other populations. This requires an
303             external networking script to be running.
304              
305             Exported critters are saved to a "migrate-$HOST/out" folder. The
306             networking script should broadcast the contents of any files created
307             in this directory, and remove the files afterwards.
308              
309             Imported critters are read from a "migrate-$HOST/in" folder. The
310             files are removed after they have been read. The networking script
311             should save any received critters to individual files in this folder.
312              
313             =cut
314              
315             sub migrate {
316             my $self = shift;
317             $self->migrate_export();
318             $self->migrate_import();
319             }
320              
321              
322             =head2 save
323              
324             $population->save();
325              
326             Write out the current population state. Savefiles are written to a
327             "results-$HOST/" folder. Also calls L
328             to keep the results directory relatively clean, see below for the
329             description of that method.
330              
331             =cut
332              
333             sub save {
334             my $self = shift;
335             my $gen = $self->generation;
336             my $pop = $self->blueprints;
337             my $host = $self->host;
338             my $results = "results-$host";
339             mkdir($results);
340             my $fnbase = "$results/" . join('-', $host, $self->physics->name);
341             my $fn = "$fnbase-$gen";
342             unlink("$fn.tmp");
343             my $savefile = IO::File->new(">$fn.tmp");
344             my $popid = new_popid();
345             $savefile->print("generation=$gen\n");
346             $savefile->print("popid=$popid\n");
347             foreach my $critter (@$pop) {
348             $savefile->print($critter->as_string);
349             }
350             $savefile->close();
351             unlink($fn);
352             rename("$fn.tmp",$fn);
353             $self->cleanup_intermediate_savefiles();
354             }
355              
356              
357             =head1 INTERNAL METHODS
358              
359             The APIs of the following methods may change at any time.
360              
361              
362             =head2 mutate
363              
364             $population->mutate($blueprint);
365              
366             Overwrite a section of the blueprint's code with trash. The section
367             size, location, and the trash are all randomly generated.
368              
369             =cut
370              
371             sub mutate {
372             my ($self, $blueprint) = @_;
373             my $code_size = $blueprint->size;
374             my $code_density = $self->config->config('code_density', 70);
375             my $base = Language::Befunge::Vector->new(
376             map { int(rand($code_size->get_component($_))) } (0..$self->dimensions-1));
377             my $size = Language::Befunge::Vector->new(
378             map { my $d = ($code_size->get_component($_)-1) - $base->get_component($_);
379             int($d/(rand($d)+1)) } (0..$self->dimensions-1));
380             my $end = $base + $size;
381             my $code = $blueprint->code;
382             for(my $v = $base->copy(); defined($v); $v = $v->rasterize($base, $end)) {
383             my $pos = 0;
384             for my $d (0..$v->get_dims()-1) {
385             $pos *= $code_size->get_component($d);
386             $pos += $v->get_component($d);
387             }
388             vec($code,$pos,8) = ord($self->new_code_fragment(1,$code_density));
389             }
390             $blueprint->code($code);
391             delete($$blueprint{cache});
392             }
393              
394              
395             =head2 crossover
396              
397             $population->crossover($blueprint1, $blueprint2);
398              
399             Swaps a random chunk of code in the first blueprint with the same
400             section of the second blueprint. Both blueprints are modified.
401              
402             =cut
403              
404             sub crossover {
405             my ($self, $chr1, $chr2) = @_;
406             my $code_size = $chr1->size;
407             my $base = Language::Befunge::Vector->new(
408             map { int(rand($code_size->get_component($_))) } (0..$self->dimensions-1));
409             my $size = Language::Befunge::Vector->new(
410             map { my $d = ($code_size->get_component($_)-1) - $base->get_component($_);
411             int($d/(rand($d)+1)) } (0..$self->dimensions-1));
412             my $end = $base + $size;
413             my $code1 = $chr1->code;
414             my $code2 = $chr2->code;
415             # upgrade code sizes if necessary
416             $code1 .= ' 'x(length($code2)-length($code1))
417             if(length($code1) < length($code2));
418             $code2 .= ' 'x(length($code1)-length($code2))
419             if(length($code2) < length($code1));
420             for(my $v = $base->copy(); defined($v); $v = $v->rasterize($base, $end)) {
421             my $pos = 0;
422             for my $d (0..$v->get_dims()-1) {
423             $pos *= $code_size->get_component($d);
424             $pos += $v->get_component($d);
425             }
426             my $tmp = vec($code2,$pos,8);
427             vec($code2,$pos,8) = vec($code1,$pos,8);
428             vec($code1,$pos,8) = $tmp;
429             }
430             $chr1->code($code1);
431             $chr2->code($code2);
432             delete($$chr1{cache});
433             delete($$chr2{cache});
434             }
435              
436              
437             =head2 crop
438              
439             $population->crop($blueprint);
440              
441             Possibly (1 in 10 chance) reduce the size of a blueprint. Each side
442             of the hypercube shall have its length reduced by 1. The preserved
443             section of the original code will be at a random offset (0 or 1 on each
444             axis).
445              
446             =cut
447              
448             sub crop {
449             my ($self, $chromosome) = @_;
450             return $chromosome if int(rand(10));
451             my $nd = $chromosome->dims;
452             my $old_size = $chromosome->size;
453             return $chromosome if $old_size->get_component(0) < 4;
454             my $new_base = Language::Befunge::Vector->new_zeroes($nd);
455             my $old_base = $new_base->copy;
456             my $ones = Language::Befunge::Vector->new(map { 1 } (1..$nd));
457             my $old_offset = Language::Befunge::Vector->new(
458             map { int(rand()*2) } (1..$nd));
459             my $new_size = $old_size - $ones;
460             my $old_end = $old_size - $ones;
461             my $new_end = $new_size - $ones;
462             my $length = 1;
463             map { $length *= ($_) } ($new_size->get_all_components);
464             my $new_code = '';
465             my $old_code = $chromosome->code();
466             my $vec = Language::Befunge::Storage::Generic::Vec->new($nd, Wrapping => undef);
467             for(my $new_v = $new_base->copy(); defined($new_v); $new_v = $new_v->rasterize($new_base, $new_end)) {
468             my $old_v = $new_v + $old_offset;
469             my $old_offset = $vec->_offset($old_v, $new_base, $old_end);
470             my $new_offset = $vec->_offset($new_v, $new_base, $new_end);
471             $new_code .= substr($old_code, $old_offset, 1);
472             }
473             return AI::Evolve::Befunge::Blueprint->new(code => $new_code, dimensions => $nd);
474             }
475              
476              
477             =head2 grow
478              
479             $population->grow($blueprint);
480              
481             Possibly (1 in 10 chance) increase the size of a blueprint. Each side
482             of the hypercube shall have its length increased by 1. The original
483             code will begin at the origin, so that the same code executes first.
484              
485             =cut
486              
487             sub grow {
488             my ($self, $chromosome) = @_;
489             return $chromosome if int(rand(10));
490             my $nd = $chromosome->dims;
491             my $old_size = $chromosome->size;
492             my $old_base = Language::Befunge::Vector->new_zeroes($nd);
493             my $new_base = $old_base->copy();
494             my $ones = Language::Befunge::Vector->new(map { 1 } (1..$nd));
495             my $new_size = $old_size + $ones;
496             my $old_end = $old_size - $ones;
497             my $new_end = $new_base + $new_size - $ones;
498             my $length = 1;
499             map { $length *= ($_) } ($new_size->get_all_components);
500             return $chromosome if $length > $self->tokens;
501             my $new_code = ' ' x $length;
502             my $old_code = $chromosome->code();
503             my $vec = Language::Befunge::Storage::Generic::Vec->new($nd, Wrapping => undef);
504             for(my $old_v = $old_base->copy(); defined($old_v); $old_v = $old_v->rasterize($old_base, $old_end)) {
505             my $new_v = $old_v + $new_base;
506             my $old_offset = $vec->_offset($old_v, $old_base, $old_end);
507             my $new_offset = $vec->_offset($new_v, $new_base, $new_end);
508             substr($new_code, $new_offset, 1) = substr($old_code, $old_offset, 1);
509             }
510             return AI::Evolve::Befunge::Blueprint->new(code => $new_code, dimensions => $nd);
511             }
512              
513              
514             =head2 cleanup_intermediate_savefiles
515              
516             $population->cleanup_intermediate_savefiles();
517              
518             Keeps the results folder mostly clean. It preserves the milestone
519             savefiles, and tosses the rest. For example, if the current
520             generation is 4123, it would preserve only the following:
521              
522             savefile-1
523             savefile-10
524             savefile-100
525             savefile-1000
526             savefile-2000
527             savefile-3000
528             savefile-4000
529             savefile-4100
530             savefile-4110
531             savefile-4120
532             savefile-4121
533             savefile-4122
534             savefile-4123
535              
536             This allows the savefiles to accumulate and allows access to some recent
537             history, and yet use much less disk space than they would otherwise.
538              
539             =cut
540              
541             sub cleanup_intermediate_savefiles {
542             my $self = shift;
543             my $gen = $self->generation;
544             my $physics = $self->physics;
545             my $host = $self->host;
546             my $results = "results-$host";
547             mkdir($results);
548             my $fnbase = "$results/" . join('-', $host, $physics->name);
549             return unless $gen;
550             for(my $base = 1; !($gen % ($base*10)); $base *= 10) {
551             my $start = $gen - ($base*10);
552             if($base * 10 != $gen) {
553             for(1..9) {
554             my $delfn = "$fnbase-" . ($start+($_*$base));
555             unlink($delfn) if -f $delfn;
556             }
557             }
558             }
559             }
560              
561              
562             =head2 migrate_export
563              
564             $population->migrate_export();
565              
566             Possibly export some critters. if the result of rand(13) is greater
567             than 10, than the value (minus 10) number of critters are written out
568             to the migration network.
569              
570             =cut
571              
572             sub migrate_export {
573             my ($self) = @_;
574             $$self{migrate}->blocking(1);
575             # export some critters
576             for my $id (0..(rand(13)-10)) {
577             my $cid = ${$self->blueprints}[$id]{id};
578             $$self{migrate}->print(${$self->blueprints}[$id]->as_string);
579             debug("exporting critter $cid\n");
580             }
581             }
582              
583              
584             =head2 migrate_import
585              
586             $population->migrate_import();
587              
588             Look on the migration network for incoming critters, and import some
589             if we have room left. To prevent getting swamped, it will only allow
590             a total of (Popsize*1.5) critters in the array at once. If the number
591             of incoming migrations exceeds that, the remainder will be left in the
592             Migrator receive queue to be handled the next time around.
593              
594             =cut
595              
596             sub migrate_import {
597             my ($self) = @_;
598             my $critter_limit = ($self->popsize * 1.5);
599             my @new;
600             my $select = IO::Select->new($$self{migrate});
601             if($select->can_read(0)) {
602             my $data;
603             $$self{migrate}->blocking(0);
604             $$self{migrate}->sysread($data, 10000);
605             my $in;
606             while(((scalar @{$self->blueprints} + scalar @new) < $critter_limit)
607             && (($in = index($data, "\n")) > -1)) {
608             my $line = substr($data, 0, $in+1, '');
609             debug("migrate: importing critter\n");
610             my $individual =
611             AI::Evolve::Befunge::Blueprint->new_from_string($line);
612             push(@new, $individual) if defined $individual;
613             }
614             }
615             $self->blueprints([@{$self->blueprints}, @new])
616             if scalar @new;
617             }
618              
619              
620             =head2 new_code_fragment
621              
622             my $trash = $population->new_code_fragment($length, $density);
623              
624             Generate $length bytes of random Befunge code. The $density parameter
625             controls the ratio of code to whitespace, and is given as a percentage.
626             Density=0 will return all spaces; density=100 will return no spaces.
627              
628             =cut
629              
630             sub new_code_fragment {
631             my ($self, $length, $density) = @_;
632             my @safe = ('0'..'9', 'a'..'h', 'j'..'n', 'p'..'z', '{', '}', '`', '_',
633             '!', '|', '?', '<', '>', '^', '[', ']', ';', '@', '#', '+',
634             '/', '*', '%', '-', ':', '$', '\\' ,'"' ,"'");
635              
636             my $usage = 'Usage: $population->new_code_fragment($length, $density);';
637             croak($usage) unless ref($self);
638             croak($usage) unless defined($length);
639             croak($usage) unless defined($density);
640             my $physics = $self->physics;
641             push(@safe, sort keys %{$$physics{commands}})
642             if exists $$physics{commands};
643             my $rv = '';
644             foreach my $i (1..$length) {
645             my $chr = ' ';
646             if(rand()*100 < $density) {
647             $chr = $safe[int(rand()*(scalar @safe))];
648             }
649             $rv .= $chr;
650             }
651             return $rv;
652             }
653              
654              
655             =head2 pair
656              
657             my ($c1, $c2) = $population->pair(map { 1 } (@population));
658             my ($c1, $c2) = $population->pair(map { $_->fitness } (@population));
659              
660             Randomly select and return two blueprints from the blueprints array.
661             Some care is taken to ensure that the two blueprints returned are not
662             actually two copies of the same blueprint.
663              
664             The @fitness parameter is used to weight the selection process. There
665             must be one number passed per entry in the blueprints array. If you
666             pass a list of 1's, you will get an equal probability. If you pass
667             the critter's fitness scores, the more fit critters have a higher
668             chance of selection.
669              
670             =cut
671              
672             sub pair {
673             my $self = shift;
674             my @population = @{$self->blueprints};
675             my $popsize = scalar @population;
676             my $matchwheel = Algorithm::Evolutionary::Wheel->new(@_);
677             my $c1 = $matchwheel->spin();
678             my $c2 = $matchwheel->spin();
679             $c2++ if $c2 == $c1;
680             $c2 = 0 if $c2 >= $popsize;
681             $c1 = $population[$c1];
682             $c2 = $population[$c2];
683             return ($c1, $c2);
684             }
685              
686              
687             =head2 generation
688              
689             my $generation = $population->generation();
690             $population->generation(1000);
691              
692             Fetches or sets the population's generation number to the given value.
693             The value should always be numeric.
694              
695             When set, as a side effect, rehashes the config file so that new
696             generational overrides may take effect.
697              
698             =cut
699              
700             sub generation {
701             my ($self, $gen) = @_;
702             if(defined($gen)) {
703             $$self{generation} = $gen;
704             $self->reload_defaults();
705             }
706             return $$self{generation};
707             }
708              
709              
710             1;