-
Notifications
You must be signed in to change notification settings - Fork 0
/
model_functions.py
2822 lines (2821 loc) · 102 KB
/
model_functions.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
44
45
46
47
48
49
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Model Functions
Peter Turney, December 13, 2021
"""
import golly as g
import model_classes as mclass
import model_parameters as mparam
import random as rand
import numpy as np
import copy
import time
import pickle
import os
import re
import sys
import pyautogui # tool for taking photos of the screen
"""
Various functions for working with Golly
"""
#
# show_message(g, log_handle, message) -- returns NULL
#
def show_message(g, log_handle, message):
"""
A function for writing a message to both the Golly window
and the log file.
"""
log_handle.write(message)
g.show(message)
#
# set_mag(g) -- returns mag
#
def set_mag(g):
"""
A function for setting the Golly screen magnification.
"""
# the maximum of the X span and the Y span
g_maxspan = np.amax([g.getwidth(), g.getheight()])
# the Golly magnification ratio is 2 to the power of mag
if (g_maxspan < 80):
mag = 5 # 2^5 = 32
elif (g_maxspan < 160):
mag = 4 # 2^4 = 16
elif (g_maxspan < 320):
mag = 3 # 2^3 = 8
elif (g_maxspan < 640):
mag = 2 # 2^2 = 4
elif (g_maxspan < 1280):
mag = 1 # 2^1 = 2
else:
mag = 0 # 2^0 = 1
return mag
#
# show_parameters() -- returns a list of parameters and values
#
def show_parameters():
"""
Make a list of the parameters in mparam and show
the value of each parameter.
"""
parameter_names = sorted(dir(mparam))
display_list = []
for name in parameter_names:
# skip over system names
# - system names have the form "__file__"
if (name[0] != "_"):
value = str(getattr(mparam, name))
display_list.append(name + " = " + value)
return display_list
#
# get_minmax(g) -- returns [g_xmin, g_xmax, g_ymin, g_ymax]
#
def get_minmax(g):
"""
Calculate the min and max of the Golly toroid coordinates.
"""
# get height and width
g_xspan = g.getwidth()
g_yspan = g.getheight()
# calculate min and max
g_xmin = - int(g_xspan / 2)
g_xmax = g_xspan + g_xmin
g_ymin = - int(g_yspan / 2)
g_ymax = g_yspan + g_ymin
#
return [g_xmin, g_xmax, g_ymin, g_ymax]
#
# count_pops(g) -- returns [count1, count2]
#
def count_pops(g):
"""
Count the populations of red/orange and blue/green.
"""
#
# 0 = dead = white
# 1 = player 1 alone = red
# 2 = player 2 alone = blue
# 3 = player 1 with interaction = orange (red + yellow)
# 4 = player 2 with interaction = green (blue + yellow)
# 5 = border marker = purple
#
# find the min and max of the Golly toroid coordinates
[g_xmin, g_xmax, g_ymin, g_ymax] = get_minmax(g)
#
count1 = 0 # red/orange
count2 = 0 # blue/green
#
for x in range(g_xmin, g_xmax):
for y in range(g_ymin, g_ymax):
a = g.getcell(x, y)
if (a == 1) or (a == 3):
count1 += 1
elif (a == 2) or (a == 4):
count2 += 1
#
return [count1, count2]
#
# count_colours(g) -- returns [red, blue, orange, green]
#
def count_colours(g):
"""
Count the number of cells for each of the colours. We only
care about red, blue, orange, and green.
"""
#
# 0 = dead = white
# 1 = player 1 alone = red
# 2 = player 2 alone = blue
# 3 = player 1 with interaction = orange (red + yellow)
# 4 = player 2 with interaction = green (blue + yellow)
# 5 = border marker = purple
#
# find the min and max of the Golly toroid coordinates
[g_xmin, g_xmax, g_ymin, g_ymax] = get_minmax(g)
#
red = 0
blue = 0
orange = 0
green = 0
#
for x in range(g_xmin, g_xmax):
for y in range(g_ymin, g_ymax):
a = g.getcell(x, y)
if (a == 1):
red += 1
elif (a == 2):
blue += 1
elif (a == 3):
orange += 1
elif (a == 4):
green += 1
#
return [red, blue, orange, green]
#
# initialize_population(pop_size, s_xspan, s_yspan, seed_density)
# -- returns population
#
def initialize_population(pop_size, s_xspan, s_yspan, seed_density):
"""
Randomly initialize the population of seeds.
"""
#
# Initialize the population: a list of seeds.
#
# Here a seed is an initial Game of Life pattern (it is
# not a random number seed).
#
population = []
#
for i in range(pop_size):
# Make an empty seed (all zeros).
seed = mclass.Seed(s_xspan, s_yspan, pop_size)
# Randomly set some cells to state 1 (red).
seed.randomize(seed_density)
# Set the count of living cells.
seed.num_living = seed.count_ones()
# Set the position of the new seed in the population array.
seed.address = i
# Assign a unique ID number to the new seed.
seed.unique_ID_num = i
# Set the birth type to "random", since the initial population
# is entirely random.
seed.birth_type = "random"
# Set the parent ID numbers to -1, since the initial random
# seeds have no parents.
seed.parent_A_ID_num = -1
seed.parent_B_ID_num = -1
# Add the seed to the population.
population.append(seed)
#
return population
#
# dimensions(s1, s2, width_factor, height_factor, time_factor)
# -- returns [g_width, g_height, g_time]
#
def dimensions(s1, s2, width_factor, height_factor, time_factor):
"""
Define the dimensions of the Golly universe, based on the
sizes of the two seeds and various multiplicative factors.
"""
#
# Suggested values:
#
# width_factor = 6.0
# height_factor = 3.0
# time_factor = 6.0
#
assert width_factor > 2.0 # need space for two seeds, left and right
assert height_factor > 1.0 # need space for tallest seed
assert time_factor > 1.0 # time should increase with increased space
#
# Find the maximum of the dimensions of the two seeds.
#
max_size = np.amax([s1.xspan, s1.yspan, s2.xspan, s2.yspan])
#
# Apply the various factors.
#
g_width = int(max_size * width_factor)
g_height = int(max_size * height_factor)
g_time = int((g_width + g_height) * time_factor)
#
return [g_width, g_height, g_time]
#
# score_pair(g, seed1, seed2, width_factor, height_factor, \
# time_factor, num_trials) -- returns [score1, score2]
#
def score_pair(g, seed1, seed2, width_factor, height_factor, \
time_factor, num_trials):
"""
Put seed1 and seed2 into the Immigration Game g and see which
one wins and which one loses. Note that this function does
not update the histories of the seeds. For updating histories,
use update_history().
"""
#
# Make copies of the original two seeds, so that the following
# manipulations do not change the originals.
#
s1 = copy.deepcopy(seed1)
s2 = copy.deepcopy(seed2)
#
# Verify that the seeds are limited to 0 (white), 1 (red), and
# 5 (purple).
#
assert s1.check_colour() == True
assert s2.check_colour() == True
#
# Check the number of living cells in the seeds. If the number
# is zero, it is probably a mistake. The number is initially
# set to zero and it should be updated when the seed is filled
# with living cells. We could use s1.count_ones() here, but
# we're trying to be efficient by counting only once and
# storing the count.
#
assert s1.num_living > 0
assert s2.num_living > 0
#
# Initialize scores
#
score1 = 0.0
score2 = 0.0
#
# Run several trials with different rotations and locations.
#
for trial in range(num_trials):
#
# Randomly rotate and flip s1 and s2
#
s1 = s1.random_rotate()
s2 = s2.random_rotate()
#
# Switch cells in the second seed (s2) from state 1 (red) to state 2 (blue)
#
s2.red2blue()
#
# Rule file
#
rule_name = "Management"
#
# Set toroidal universe of height yspan and width xspan
# Base the s1ze of the universe on the s1zes of the seeds
#
# g = the Golly universe
#
[g_width, g_height, g_time] = dimensions(s1, s2, \
width_factor, height_factor, time_factor)
#
# set algorithm -- "HashLife" or "QuickLife"
#
g.setalgo("QuickLife") # use "HashLife" or "QuickLife"
g.autoupdate(False) # do not update the view unless requested
g.new(rule_name) # initialize cells to state 0
g.setrule(rule_name + ":T" + str(g_width) + "," + str(g_height)) # make a toroid
#
# Find the min and max of the Golly toroid coordinates
#
[g_xmin, g_xmax, g_ymin, g_ymax] = get_minmax(g)
#
# Set magnification for Golly viewer
#
g.setmag(set_mag(g))
#
# Randomly place seed s1 somewhere in the left s1de of the toroid
#
s1.insert(g, g_xmin, -1, g_ymin, g_ymax)
#
# Randomly place seed s2 somewhere in the right s1de of the toroid
#
s2.insert(g, +1, g_xmax, g_ymin, g_ymax)
#
# Run for a fixed number of generations.
# Base the number of generations on the sizes of the seeds.
# Note that these are generations ins1de one Game of Life, not
# generations in an evolutionary sense. Generations in the
# Game of Life correspond to growth and decay of a phenotype,
# whereas generations in evolution correspond to the reproduction
# of a genotype.
#
g.run(g_time) # run the Game of Life for g_time time steps
g.update() # need to update Golly to get counts
#
# Count the populations of the two colours. State 1 = red = seed1.
# State 2 = blue = seed2.
#
[count1, count2] = count_pops(g)
#
# We need to make an adjustment to these counts. We don't want to
# use the total count of living cells; instead we want to use
# the increase in the number of living cells over the course of
# the contest between the two organisms. The idea here is that
# we want to reward seeds according to their growth during the
# contest, not according to their initial states. This should
# avoid an evolutionary bias towards larger seeds simply due
# to size rather than due to functional properties. It should
# also encourage efficient use of living cells, as opposed to
# simply ignoring useless living cells.
#
# s1.num_living = initial number of living cells in s1
# s2.num_living = initial number of living cells in s2
#
if (s1.num_living < count1):
count1 = count1 - s1.num_living
else:
count1 = 0
#
if (s2.num_living < count2):
count2 = count2 - s2.num_living
else:
count2 = 0
#
# Now we are ready to determine the winner.
#
if (count1 > count2):
score1 = score1 + 1.0
elif (count2 > count1):
score2 = score2 + 1.0
else:
score1 = score1 + 0.5
score2 = score2 + 0.5
#
#
# Normalize the scores
#
score1 = score1 / num_trials
score2 = score2 / num_trials
#
return [score1, score2]
#
# score_management(g, seed1, seed2, width_factor, height_factor, \
# time_factor, num_trials) -- returns [score1, score2]
#
def score_management(g, seed1, seed2, width_factor, height_factor, \
time_factor, num_trials):
"""
Put seed1 and seed2 into the Management Game g and see which
one wins and which one loses, based on orange and green counts.
Note that this function does not update the histories of the seeds.
For updating histories, use update_history().
"""
#
# Make copies of the original two seeds, so that the following
# manipulations do not change the originals.
#
s1 = copy.deepcopy(seed1)
s2 = copy.deepcopy(seed2)
#
# Check the number of living cells in the seeds. If the number
# is zero, it is probably a mistake. The number is initially
# set to zero and it should be updated when the seed is filled
# with living cells. We could use s1.count_ones() here, but
# we're trying to be efficient by counting only once and
# storing the count.
#
assert s1.num_living > 0
assert s2.num_living > 0
#
# Initialize scores
#
score1 = 0.0
score2 = 0.0
#
# Run several trials with different rotations and locations.
#
for trial in range(num_trials):
#
# Randomly rotate and flip s1 and s2
#
s1 = s1.random_rotate()
s2 = s2.random_rotate()
#
# Switch cells in the second seed (s2) from state 1 (red) to state 2 (blue)
#
s2.red2blue()
#
# Rule file
#
rule_name = "Management"
#
# Set toroidal universe of height yspan and width xspan
# Base the s1ze of the universe on the s1zes of the seeds
#
# g = the Golly universe
#
[g_width, g_height, g_time] = dimensions(s1, s2, \
width_factor, height_factor, time_factor)
#
# set algorithm -- "HashLife" or "QuickLife"
#
g.setalgo("QuickLife") # use "HashLife" or "QuickLife"
g.autoupdate(False) # do not update the view unless requested
g.new(rule_name) # initialize cells to state 0
g.setrule(rule_name + ":T" + str(g_width) + "," + str(g_height)) # make a toroid
#
# Find the min and max of the Golly toroid coordinates
#
[g_xmin, g_xmax, g_ymin, g_ymax] = get_minmax(g)
#
# Set magnification for Golly viewer
#
g.setmag(set_mag(g))
#
# Randomly place seed s1 somewhere in the left s1de of the toroid
#
s1.insert(g, g_xmin, -1, g_ymin, g_ymax)
#
# Randomly place seed s2 somewhere in the right s1de of the toroid
#
s2.insert(g, +1, g_xmax, g_ymin, g_ymax)
#
# Run for a fixed number of generations.
# Base the number of generations on the sizes of the seeds.
# Note that these are generations ins1de one Game of Life, not
# generations in an evolutionary sense. Generations in the
# Game of Life correspond to growth and decay of a phenotype,
# whereas generations in evolution correspond to the reproduction
# of a genotype.
#
g.run(g_time) # run the Game of Life for g_time time steps
g.update() # need to update Golly to get counts
#
# Count orange cells for red (s1, colour 1) and count green cells
# for blue (s2, colour 2). We don't need to subtract their initial
# count at t = 0, because their initial count is necessarily zero.
#
[red, blue, orange, green] = count_colours(g)
#
count1 = orange # the red seed is rewarded for orange cells
count2 = green # the blue seed is rewarded for green cells
#
# Now we are ready to determine the winner.
#
if (count1 > count2):
score1 = score1 + 1.0
elif (count2 > count1):
score2 = score2 + 1.0
else:
score1 = score1 + 0.5
score2 = score2 + 0.5
#
#
# Normalize the scores
#
score1 = score1 / num_trials
score2 = score2 / num_trials
#
return [score1, score2]
#
# update_history(g, pop, i, j, width_factor, height_factor, \
# time_factor, num_trials) -- returns NULL
#
def update_history(g, pop, i, j, width_factor, height_factor, \
time_factor, num_trials):
"""
Put the i-th and j-th seeds into the Immigration Game g and
see which one wins and which one loses. The history of the
seeds will be updated in pop.
"""
#
# If i == j, let's just call it a tie.
#
if (i == j):
pop[i].history[i] = 0.5
return
#
# Call score_pair()
#
[scorei, scorej] = score_pair(g, pop[i], pop[j], width_factor, \
height_factor, time_factor, num_trials)
#
# Update pop[i] and pop[j] with the new scores.
#
pop[i].history[j] = scorei
pop[j].history[i] = scorej
#
# returns NULL
#
#
# update_similarity(pop, i, j) -- returns NULL
#
def update_similarity(pop, i, j):
"""
Calculate the similarity between the two given seeds and
update their internal records with the result.
"""
#
# If i == j, the similarity score is the maximum.
#
if (i == j):
pop[i].similarities[i] = 1.0
return
#
# Calculate the similarity and update the population record.
#
sim = similarity(pop[i], pop[j])
pop[i].similarities[j] = sim
pop[j].similarities[i] = sim
#
# returns NULL
#
#
# find_top_seeds(population, sample_size) -- returns sample_pop
#
def find_top_seeds(population, sample_size):
"""
Find the best (fittest) sample_size seeds in the population.
"""
pop_size = len(population)
assert pop_size >= sample_size
assert sample_size > 0
# calculate fitness for each seed in the population, from their history
scored_pop = []
for i in range(pop_size):
item = [population[i].fitness(), population[i]]
scored_pop.append(item)
# sort population in order of decreasing fitness (reverse=True)
scored_pop.sort(key = lambda x: x[0], reverse=True) # sort by fitness
# take the top sample_size items from scored_pop and
# remove their attached fitness numbers
sample_pop = []
for i in range(sample_size):
sample_pop.append(scored_pop[i][1]) # drop fitness number
# return the cleaned-up list of sample_size seeds
return sample_pop
#
# random_sample(population, sample_size) -- returns sample_pop
#
def random_sample(population, sample_size):
"""
Get a random sample of sample_size seeds from the population.
"""
#
# To avoid duplicates in the sample, randomize the order of the
# population and then take the first sample_size seeds
# from the randomized list.
#
pop_size = len(population)
assert pop_size > sample_size
assert sample_size > 0
# attach a random number to each seed in the population
randomized_pop = []
for i in range(pop_size):
# item = [random real number between 0 and 1, the i-th seed]
item = [rand.uniform(0, 1), population[i]]
randomized_pop.append(item)
# sort randomized_pop in order of the attached random numbers
randomized_pop.sort(key = lambda x: x[0]) # sort by random number
# take the top sample_size items from randomized_pop and
# remove their attached random numbers
sample_pop = []
for i in range(sample_size):
sample_pop.append(randomized_pop[i][1]) # drop random number
# return the cleaned-up list of sample_size seeds
return sample_pop
#
# find_best_seed(sample) -- returns best_seed
#
def find_best_seed(sample):
"""
In the list of seeds in sample, find the seed (not necessarily
unique) with maximum fitness.
"""
sample_size = len(sample)
assert sample_size > 0
best_seed = sample[0]
best_score = best_seed.fitness()
for i in range(len(sample)):
if (sample[i].fitness() > best_score):
best_seed = sample[i]
best_score = best_seed.fitness()
return best_seed
#
# find_worst_seed(sample) -- returns worst_seed
#
def find_worst_seed(sample):
"""
In the list of seeds in sample, find the seed (not necessarily
unique) with minimum fitness.
"""
sample_size = len(sample)
assert sample_size > 0
worst_seed = sample[0]
worst_score = worst_seed.fitness()
for i in range(len(sample)):
if (sample[i].fitness() < worst_score):
worst_seed = sample[i]
worst_score = worst_seed.fitness()
return worst_seed
#
# average_fitness(sample) -- returns average
#
def average_fitness(sample):
"""
Given a list of sample seeds, return their average fitness,
relative to the whole population.
"""
sample_size = len(sample)
assert sample_size > 0
total_fitness = 0.0
for i in range(len(sample)):
total_fitness = total_fitness + sample[i].fitness()
average = total_fitness / sample_size
return average
#
# archive_elite(population, elite_size, log_directory, log_name, run_id_number)
# -- returns NULL
#
def archive_elite(population, elite_size, log_directory, log_name, run_id_number):
"""
Store an archive file of the elite members of the population,
for future testing. The elite consists of the top elite_size
most fit seeds in the current population.
"""
history_sample = find_top_seeds(population, elite_size)
history_name = log_name + "-pickle-" + str(run_id_number)
history_path = log_directory + "/" + history_name + ".bin"
history_handle = open(history_path, "wb") # wb = write binary
pickle.dump(history_sample, history_handle)
history_handle.close()
#
# returns NULL
#
#
# fusion_storage(s2, s3, s4, n) -- returns NULL
#
def fusion_storage(s2, s3, s4, n):
"""
After fusion has occurred, store the parts (s2, s3) and their
fusion (s4) in a binary file for future analysis and inspection.
The seed s4 is the n-th child born.
"""
# make a file name for storage
fusion_path = mparam.log_directory + "/fusion_storage.bin"
# "ab+" opens a file for both appending and reading in binary mode
fusion_handle = open(fusion_path, "ab+")
# store the seeds and the generation number
pickle.dump(s2, fusion_handle) # s2 is part of s4 (after rotation)
pickle.dump(s3, fusion_handle) # s3 is part of s4 (after rotation)
pickle.dump(s4, fusion_handle) # s4 is the fusion of s2 and s3
pickle.dump(n, fusion_handle) # s4 is n-th child
# close handle
fusion_handle.close()
#
# returns NULL
#
#
# seed_storage(seed) -- returns NULL
#
def seed_storage(seed):
"""
Append the given seed to the specified file.
"""
#
# mparam.log_directory is the desired folder for storing the seed.
# For example, mparam.log_directory might be "../Experiments/run1".
# We want to append a unique subdirectory to mparam.log_directory,
# so that future experiments do not get mixed in to the file.
#
storage_path = mparam.log_directory + "/all_seed_storage.bin"
# "ab+" opens a file for both appending and reading in binary mode
storage_handle = open(storage_path, "ab+")
pickle.dump(seed, storage_handle)
storage_handle.close()
#
# returns NULL
#
#
# similarity(seed0, seed1) -- returns similarity
#
def similarity(seed0, seed1):
"""
Measure the bit-wise similarity of two seeds. If the seeds
have different sizes, return zero. If they have different
borders (buffer zones), return zero.
"""
# Make sure the seeds are the same size: the same number
# of rows and columns.
if (seed0.xspan != seed1.xspan):
return 0.0
if (seed0.yspan != seed1.yspan):
return 0.0
# Make sure that the seeds have the same borders: that is,
# they should have matching purple states (state 5).
for x in range(seed0.xspan):
for y in range(seed0.yspan):
if ((seed0.cells[x][y] == 5) and (seed1.cells[x][y] != 5)):
return 0.0
if ((seed0.cells[x][y] != 5) and (seed1.cells[x][y] == 5)):
return 0.0
# Initialize count.
num_agree = 0.0
# Count agreements.
for x in range(seed0.xspan):
for y in range(seed0.yspan):
if (seed0.cells[x][y] == seed1.cells[x][y]):
num_agree = num_agree + 1.0
# Calculate a similarity score ranging from zero to one.
similarity = num_agree / (seed0.xspan * seed0.yspan)
# Return the degree of similarity between the two seeds.
return similarity
#
# find_similar_seeds(target_seed, pop, min_similarity, max_similarity)
# -- returns similar_seeds
#
def find_similar_seeds(target_seed, pop, min_similarity, max_similarity):
"""
Given a target seed, find seeds in the population with similarities
to the target in the range from min_similarity to max_similarity.
This function assumes that target_seed is in the population and
the list target_seed.similarities is up-to-date.
"""
similar_seeds = []
for i in range(len(pop)):
if ((target_seed.similarities[i] >= min_similarity) and \
(target_seed.similarities[i] <= max_similarity) and \
(target_seed.address != i)):
similar_seeds.append(pop[i])
# return the seeds that satisfy the conditions
return similar_seeds
#
# mate(seed0, seed1) -- returns child_seed
#
def mate(seed0, seed1):
"""
Apply crossover to seed0 and seed1. We only have one crossover point,
because multiple crossover points would be more disruptive to the
structure of the seeds.
"""
# This function is designed with the assumption that the seeds are
# the same size.
assert seed0.xspan == seed1.xspan
assert seed0.yspan == seed1.yspan
# Note the spans of seed0 and seed1.
xspan = seed0.xspan
yspan = seed0.yspan
# Randomly swap the seeds. Because s0 is always the top part of
# a split that cuts across the Y axis and the left part of a split
# that cuts across the X axis, we need to swap the seeds in order
# to add some variety.
if (rand.uniform(0, 1) < 0.5):
s0 = seed0
s1 = seed1
else:
s0 = seed1
s1 = seed0
# Initialize the child to zero.
child_seed = mclass.Seed(xspan, yspan, mparam.pop_size)
# Randomly choose whether to split on the X axis or
# the Y axis.
if (rand.uniform(0, 1) < 0.5):
# Choose the Y axis split point. There will always be
# at least one row on either side of the split point.
assert yspan > 1
y_split_point = rand.randrange(yspan - 1)
for x in range(xspan):
for y in range(yspan):
if (y <= y_split_point):
child_seed.cells[x][y] = s0.cells[x][y]
else:
child_seed.cells[x][y] = s1.cells[x][y]
else:
# Choose the X axis split point. There will always be
# at least one column on either side of the split point.
assert xspan > 1
x_split_point = rand.randrange(xspan - 1)
for x in range(xspan):
for y in range(yspan):
if (x <= x_split_point):
child_seed.cells[x][y] = s0.cells[x][y]
else:
child_seed.cells[x][y] = s1.cells[x][y]
# Return the resulting child.
return child_seed
#
# region_map(seed) -- returns a map of the regions in seed
#
def region_map(seed):
"""
Given a seed, make a matrix of the same size as the seed,
such that the matrix is a map of the regions in the seed.
Each cell in the matrix will contain a number that uniquely
identifies the region it belongs to, as determined by the
purple borders in the seed.
"""
# start with a map the same size as the seed, but all zeros
map = np.zeros((seed.xspan, seed.yspan), dtype=np.int)
# write the purple borders (state 5) of the seed into the map
# -- write -1 as the new border state, since we could have six or
# more regions and we want to assign a unique number to each region
for x in range(seed.xspan):
for y in range(seed.yspan):
if (seed.cells[x][y] == 5):
# the new border
map[x][y] = -1
# initialize some variables
current_num = 1 # the number that marks the current region
total_num = 1 # the total number of regions seen so far
border_crossing = 0 # 0 = not currently crossing a border, 1 = crossing
# scan through map, left to right, top to bottom
for x in range(seed.xspan):
for y in range(seed.yspan):
# check to see if we are crossing a border
if (map[x][y] == -1):
border_crossing = 1
# if the current cell is not a border, write current_num in the map
if (border_crossing == 0):
map[x][y] = current_num
# if the border_crossing flag was triggered and we are not right
# on top of the border, then look in the neighbourhood to see
# if we have been here before
if ((border_crossing == 1) and (map[x][y] != -1)):
# look around
for delta_x in [-1, 0, 1]:
for delta_y in [-1, 0, 1]:
near_x = x + delta_x
near_y = y + delta_y
if (near_x < 0):
continue
elif (near_x >= seed.xspan):
continue
elif (near_y < 0):
continue
elif (near_y >= seed.yspan):
continue
elif (map[near_x][near_y] > 0):
# reset current_num to the value of the neighbour
current_num = map[near_x][near_y]
# write current_num in the map
map[x][y] = current_num
# reset the border flag
border_crossing = 0
# stop looking around
break
# if the border_crossing flag was triggered and looking in
# the neighbourhood didn't help, then we must be in a new
# region
if ((border_crossing == 1) and (map[x][y] != -1)):
total_num += 1
current_num = total_num
map[x][y] = current_num
border_crossing = 0
# incrementing x is like crossing a border, because y goes
# back to zero
border_crossing = 1
# return the map
return map
#
# extract_parts(seed, seed_map, target_region) -- returns target part of seed
#
def extract_parts(seed, seed_map, target_region):
"""
Given a seed, a seed_map, and a target_region, extract the region of
the seed that corresponds to cells in seed_map that have the value
target_region. Reduce the size of the extracted seed by dropping empty
outside rows and columns.
"""
#
# seed = a seed object as defined in model_classes.py
# seed_map = an array where rectangular regions are given by cell values
# target_region = the cell value that picks out the desired rectangular region
#
# find the size of the seed
num_rows = seed_map.shape[0]
num_cols = seed_map.shape[1]
# check that seed and seed map have the same size
assert seed.cells.shape[0] == seed_map.shape[0]
assert seed.cells.shape[1] == seed_map.shape[1]
# the target_region cannot be zero
assert target_region != 0
# make sure target_region is in seed_map
assert target_region in seed_map
# find first_row containing target_region
first_row = 0
for row in range(num_rows):
if (target_region in seed_map[row, :]):
first_row = row
break
# find last_row containing target_region
last_row = num_rows - 1
for row in range(num_rows-1, -1, -1):
if (target_region in seed_map[row, :]):
last_row = row
break
# find first_col containing target_region
first_col = 0
for col in range(num_cols):
if (target_region in seed_map[:, col]):
first_col = col
break
# find last_col containing target_region
last_col = num_cols - 1
for col in range(num_cols-1, -1, -1):
if (target_region in seed_map[:, col]):
last_col = col
break
# make a new seed with the new reduced size
new_num_rows = last_row - first_row + 1
new_num_cols = last_col - first_col + 1
new_seed = mclass.Seed(new_num_rows, new_num_cols, 0)
# for each cell in seed_map that matches the value target_region,
# if the corresponding value in seed is not zero, then write
# a value of one into the appropriate location in new_seed
for row in range(first_row, last_row + 1):
for col in range(first_col, last_col + 1):
if (seed.cells[row][col] > 0):
new_seed.cells[row - first_row, col - first_col] = 1
# return new_seed
return new_seed
#
# found_empty_cells(seed) -- returns True (empty) or False (not empty)
#
def found_empty_cells(seed):
"""
Check all the cells in the given seed to make sure that none of
the cells are empty.
"""
# first make a map of the seed
seed_map = region_map(seed)
# count the number of regions in the map
num_regions = np.amax(seed_map)
# examine each region
for i in range(num_regions):
# target_region ranges from 1 to num_regions (inclusive)
target_region = i + 1
# before we try to extract the seed part in the given
# target region, let's verify that the target region
# exists in the seed map
if (not (target_region in seed_map)):
return True # bad seed
# now extract the seed part from the target region
seed_part = extract_parts(seed, seed_map, target_region)
# the cells in seed_part should not all be zero
if (np.amax(seed_part.cells) == 0):
# if seed_part is all zeros, then it is True that there
# are empty cells
return True # bad seed
# if we reach here, then it is False that there are empty cells
return False # good seed
#
# uniform_asexual(candidate_seed, pop, n, next_unique_ID_number)
# -- returns [pop, message]
#
def uniform_asexual(candidate_seed, pop, n, next_unique_ID_number):
"""
Create a new seed by randomly mutating an existing seed. The
new seed is generated by selecting a parent seed and flipping
bits in the parent. The size of the seed does not change; it
is uniform.
"""
# The most fit member of the tournament.
s0 = candidate_seed
# Mutate the best seed to make a new child. The only mutation
# here is flipping bits.
mutation_rate = mparam.mutation_rate
s1 = copy.deepcopy(s0)
s1.flip_bits(mutation_rate)
# If there are empty cells in s1, then try again with uniform_asexual.
if found_empty_cells(s1):
return uniform_asexual(candidate_seed, pop, n, next_unique_ID_number)
# Update the count of living cells.