-
Notifications
You must be signed in to change notification settings - Fork 0
/
model_functions.py
1639 lines (1638 loc) · 57.9 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, February 9, 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 state 1 (red) and state 2 (blue)
"""
# find the min and max of the Golly toroid coordinates
[g_xmin, g_xmax, g_ymin, g_ymax] = get_minmax(g)
#
count1 = 0
count2 = 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):
count1 = count1 + 1
if (a == 2):
count2 = count2 + 1
#
return [count1, count2]
#
# 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
# 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)
#
# 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 = "Immigration"
#
# 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]
#
# 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
#
#
# 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.
"""
# Make sure the seeds are the same size.
if (seed0.xspan != seed1.xspan):
return 0.0
if (seed0.yspan != seed1.yspan):
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
#
# uniform_asexual(candidate_seed, pop, n) -- returns [pop, message]
#
def uniform_asexual(candidate_seed, pop, n):
"""
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)
s1.num_living = s1.count_ones() # update count of living cells
# Find the least fit old seed in the population. It's not a problem
# if there are ties.
s2 = find_worst_seed(pop)
# Now we have:
#
# s0 = fit parent seed
# s1 = the mutated new child
# s2 = the least fit old seed, which will be replaced by the mutated child
#
# Replace the least fit old seed in the population (s2) with the
# new child (s1).
i = s2.address # find the position of the old seed (s2)
s1.address = i # copy the old position of the old seed into s1, the child
pop[i] = s1 # replace s2 (old seed) in population (pop) with s1 (new child)
# Build a history for the new seed, by matching it against all seeds
# in the population.
width_factor = mparam.width_factor
height_factor = mparam.height_factor
time_factor = mparam.time_factor
num_trials = mparam.num_trials
pop_size = len(pop)
for j in range(pop_size):
update_history(g, pop, i, j, width_factor, height_factor, \
time_factor, num_trials)
update_similarity(pop, i, j)
# Report on the new history of the new seed
message = "Run: {}".format(n) + \
" Parent fitness (s0): {:.3f}".format(s0.fitness()) + \
" Child fitness (s1): {:.3f}".format(s1.fitness()) + \
" Replaced seed fitness (s2): {:.3f}\n".format(s2.fitness())
# It is possible that s1 is worse than s2, if there was a bad mutation in s1.
# Let's not worry about that, since s1 will soon be replaced if it is less
# fit than the least fit seed (that is, s2).
return [pop, message]
#
# variable_asexual(candidate_seed, pop, n, max_seed_area)
# -- returns [pop, message]
#
def variable_asexual(candidate_seed, pop, n, max_seed_area):
"""
Create a new seed by randomly mutating, growing, and shrinking
an existing seed. The new seed is generated by selecting a parent
seed and randomly flipping bits, removing rows and columns, or
adding rows and columns. The size of the seed is variable; it
may increase or decrease in size.
"""
# The most fit member of the tournament.
s0 = candidate_seed
# Mutate the best seed to make a new child. The mutations here
# are flipping bits, removing rows and columns (shrinking), and
# adding rows and columns (growing).
prob_grow = mparam.prob_grow
prob_flip = mparam.prob_flip
prob_shrink = mparam.prob_shrink
seed_density = mparam.seed_density
mutation_rate = mparam.mutation_rate
s1 = copy.deepcopy(s0)
s1 = s1.mutate(prob_grow, prob_flip, prob_shrink, seed_density, mutation_rate)
s1.num_living = s1.count_ones() # update count of living cells
# Make sure the area of the new seed is not greater than the maximum.
# If it is too big, then default to uniform_asexual reproduction.
if ((s1.xspan * s1.yspan) > max_seed_area):
return uniform_asexual(candidate_seed, pop, n)
# Find the least fit old seed in the population. It's not a problem
# if there are ties.
s2 = find_worst_seed(pop)
# Now we have:
#
# s0 = fit parent seed
# s1 = the mutated new child
# s2 = the least fit old seed, which will be replaced by the mutated child
#
# Replace the least fit old seed in the population (s2) with the
# new child (s1).
i = s2.address # find the position of the old seed (s2)
s1.address = i # copy the old position of the old seed into s1, the child
pop[i] = s1 # replace s2 (old seed) in population (pop) with s1 (new child)
# Build a history for the new seed, by matching it against all seeds
# in the population.
width_factor = mparam.width_factor
height_factor = mparam.height_factor
time_factor = mparam.time_factor
num_trials = mparam.num_trials
pop_size = len(pop)
for j in range(pop_size):
update_history(g, pop, i, j, width_factor, height_factor, \
time_factor, num_trials)
update_similarity(pop, i, j)
# Report on the new history of the new seed
message = "Run: {}".format(n) + \
" Parent fitness (s0): {:.3f}".format(s0.fitness()) + \
" Child fitness (s1): {:.3f}".format(s1.fitness()) + \
" Replaced seed fitness (s2): {:.3f}\n".format(s2.fitness())
# It is possible that s1 is worse than s2, if there was a bad mutation in s1.
# Let's not worry about that, since s1 will soon be replaced if it is less
# fit than the least fit seed (that is, s2).
return [pop, message]
#
# sexual(candidate_seed, pop, n, max_seed_area) -- returns [pop, message]
#
def sexual(candidate_seed, pop, n, max_seed_area):
"""
Create a new seed either asexually or sexually. First a single parent
is chosen from the population. If a second parent can be found that
is sufficiently similar to the first parent, then the child will have
two parents (sexual reproduction). If no similar second parent can be
found, then the child will have one parent (asexual reproduction).
"""
# Let s0 be the most fit member of the tournament.
s0 = candidate_seed
# Find similar seeds in the population (members of the same species).
min_similarity = mparam.min_similarity
max_similarity = mparam.max_similarity
similar_seeds = find_similar_seeds(s0, pop, min_similarity, max_similarity)
num_similar_seeds = len(similar_seeds)
# If no similar seeds were found, then use variable asexual reproduction.
if (num_similar_seeds == 0):
return variable_asexual(candidate_seed, pop, n, max_seed_area)
# Run a new tournament to select a second seed s1 as a mate for s0.
tournament_size = mparam.tournament_size
if (num_similar_seeds <= tournament_size):
s1 = find_best_seed(similar_seeds)
else:
tournament_sample = random_sample(similar_seeds, tournament_size)
s1 = find_best_seed(tournament_sample)
# Mate the parents to make a new child.
s2 = mate(s0, s1)
# Mutate the child.
prob_grow = mparam.prob_grow
prob_flip = mparam.prob_flip
prob_shrink = mparam.prob_shrink
seed_density = mparam.seed_density
mutation_rate = mparam.mutation_rate
s3 = s2.mutate(prob_grow, prob_flip, prob_shrink, seed_density, mutation_rate)
s3.num_living = s3.count_ones() # update count of living cells
# Make sure the area of the new seed is not greater than the maximum.
# If it is too big, then default to uniform_asexual reproduction.
if ((s3.xspan * s3.yspan) > max_seed_area):
return uniform_asexual(candidate_seed, pop, n)
# Find the least fit old seed in the population. It's not a problem
# if there are ties.
s4 = find_worst_seed(pop)
# Now we have:
#
# s0 = parent 0
# s1 = parent 1
# s2 = the new child, before mutation
# s3 = the mutated new child
# s4 = the least fit old seed, which will be replaced by the mutated child
#
# Replace the least fit old seed in the population (s4) with the
# new child (s3).
i = s4.address # find the position of the old seed (s4)
s3.address = i # copy the old position of the old seed into s3, the child
pop[i] = s3 # replace s4 (old seed) in population (pop) with s3 (new child)
# Build a history for the new seed, by matching it against all seeds
# in the population.
width_factor = mparam.width_factor
height_factor = mparam.height_factor
time_factor = mparam.time_factor
num_trials = mparam.num_trials
pop_size = len(pop)
for j in range(pop_size):
update_history(g, pop, i, j, width_factor, height_factor, \
time_factor, num_trials)
update_similarity(pop, i, j)
# Report on the new history of the new seed
message = "Run: {}".format(n) + \
" Parent 0 fitness (s0): {:.3f}".format(s0.fitness()) + \
" Parent 1 fitness (s1): {:.3f}".format(s1.fitness()) + \
" Child fitness (s3): {:.3f}".format(s3.fitness()) + \
" Replaced seed fitness (s4): {:.3f}\n".format(s4.fitness())
# It is possible that s3 is worse than s4, if there was a bad mutation in s3.
# Let's not worry about that, since s3 will soon be replaced if it is less
# fit than the least fit seed (that is, s4).
return [pop, message]
#
# fusion(candidate_seed, pop, n, max_seed_area)
# -- returns [pop, message]
#
def fusion(candidate_seed, pop, n, max_seed_area):
"""
Fuse two seeds together. Randomly rotate the seeds before
joining them. Let's put one seed on the left and the other
seed on the right. Insert one empty column between the two
seeds, as a kind of buffer, so that the two seeds do not
immediately interact. This empty column also helps fission
later on, to split joined seeds at the same point where they
were initially joined.
"""
# The most fit member of the tournament.
s0 = candidate_seed
# Run another tournament to select a second seed. The second
# seed might be identical to the first seed. That's OK.
tournament_size = mparam.tournament_size
tournament_sample = random_sample(pop, tournament_size)
s1 = find_best_seed(tournament_sample)
# If the flag fusion_test_flag is set to 1, then randomize s1
# by shuffling its cells. This operation is expected to reduce
# the fitness of the new fusion seed. Usually fusion_test_flag
# should be set to 0. Note that s1.shuffle() makes a copy, so the
# original of s1 is not affected by the shuffling.
if (mparam.fusion_test_flag == 1):
s1 = s1.shuffle()
# Randomly rotate the seeds. These rotations (s2 and s3) are copies.
# The originals (s0 and s1) are not affected by the rotations.
s2 = s0.random_rotate()
s3 = s1.random_rotate()
# Get dimensions for the new fusion seed.
pop_size = mparam.pop_size
xspan = s2.xspan + s3.xspan + 1 # left width + right width + empty gap
yspan = max(s2.yspan, s3.yspan) # the larger of the two heights
# Make sure the area of the new seed is not greater than the maximum.
# If it is too big, then default to sexual reproduction.
if ((xspan * yspan) > max_seed_area):
return sexual(candidate_seed, pop, n, max_seed_area)
# Copy s2 into the left side of s4.
s4 = mclass.Seed(xspan, yspan, pop_size) # cells initialized to zero
for x in range(s2.xspan):
for y in range(s2.yspan):
s4.cells[x][y] = s2.cells[x][y]
# Copy s3 into the right side of s4.
for x in range(s3.xspan):
for y in range(s3.yspan):
s4.cells[x + s2.xspan + 1][y] = s3.cells[x][y]
# Update count of living cells
s4.num_living = s4.count_ones()
# Find the least fit old seed in the population. It's not a problem
# if there are ties.
s5 = find_worst_seed(pop)
# Now we have:
#
# s0 = seed 0
# s1 = seed 1
# s2 = rotated seed 0
# s3 = rotated seed 1
# s4 = the fusion of s2 and s3
# s5 = the least fit old seed, which will be replaced by s4
#
# NOTE: we're not applying mutation here, because this is not a form
# of reproduction. It's a merger of two seeds.
#
# Replace the least fit old seed in the population (s5) with the
# new fusion seed (s4).
i = s5.address # find the position of the old seed (s5)
s4.address = i # copy the old position of the old seed into s4, the new fusion seed
pop[i] = s4 # replace s5 (old seed) in population (pop) with s4 (new fusion seed)
# Build a history for the new seed, by matching it against all seeds
# in the population.
width_factor = mparam.width_factor
height_factor = mparam.height_factor
time_factor = mparam.time_factor
num_trials = mparam.num_trials
for j in range(pop_size):
update_history(g, pop, i, j, width_factor, height_factor, \
time_factor, num_trials)
update_similarity(pop, i, j)
# If the flag immediate_symbiosis_flag is set to "1", then
# we must test to see whether s4 is more fit than both s1 and s2.
if (mparam.immediate_symbiosis_flag == 1):
if ((s0.fitness() >= s4.fitness()) or (s1.fitness() >= s4.fitness)):
# If either of the parts (s0 or s1) has a fitness greater than
# or equal to the fitness of s4, then default to sexual reproduction.
# Symbiosis means that the whole is more fit than the parts.
# When the flag immediate_symbiosis_flag is set to "1", we
# insist that symbiosis should happen immediately, rather than
# hoping that it will happen in some future generation.
return sexual(candidate_seed, pop, n, max_seed_area)
# Report on the new history of the new seed.
message = "Run: {}".format(n) + \
" Seed 0 fitness (s0): {:.3f}".format(s0.fitness()) + \
" Seed 1 fitness (s1): {:.3f}".format(s1.fitness()) + \
" Fusion fitness (s4): {:.3f}".format(s4.fitness()) + \
" Replaced seed fitness (s5): {:.3f}\n".format(s5.fitness())
# Store the new seed (s4) and its parts (s2, s3) for future analysis.
fusion_storage(s2, s3, s4, n)
# Return with the updated population and a message.
return [pop, message]
#
# fission(candidate_seed, pop, n, max_seed_area)
# -- returns [pop, message]
#
def fission(candidate_seed, pop, n, max_seed_area):
"""
In fusion, we use the convention of putting one seed on
the left and the other seed on the right, before we fuse
the two seeds. In fission, we assume that fission will
split the left part from the right part. Find the most
sparse column in the candidate seed and split the seed along
this column. If both parts are at least the minimum allowed
seed size, randomly choose one of them. If only one part
is at least the minimum allowed seed size, choose that
one part. If neither part is at least the minimum allowed
seed size, then default to sexual reproduction.
"""
# The most fit member of the tournament.
s0 = candidate_seed
# Minimum xspan. Only xspan is relevant, since we are splitting
# left and right parts.
min_s_xspan = mparam.min_s_xspan
# See whether the seed is big enough to split. If it is too
# small, then default to sexual reproduction.
if (s0.xspan <= min_s_xspan):
return sexual(candidate_seed, pop, n, max_seed_area)
# Location of the most sparse column. If there are ties, the
# first sparse column will be chosen.
sparse_col = np.argmin(np.sum(s0.cells, axis = 0))
# Left and right parts.
left_cells = s0.cells[0:sparse_col, :]
right_cells = s0.cells[(sparse_col + 1):, :]
# Initialize a seed for the left or right part.
s1 = copy.deepcopy(s0)
# If both parts are big enough, randomly choose one of them.
if ((left_cells.shape[0] >= min_s_xspan) \
and (right_cells.shape[0] >= min_s_xspan)):
if (rand.uniform(0, 1) < 0.5):
s1.cells = left_cells
else:
s1.cells = right_cells
# If only the left part is big enough, use the left part.
elif (left_cells.shape[0] >= min_s_xspan):
s1.cells = left_cells
# If only the right part is big enough, use the right part.
elif (right_cells.shape[0] >= min_s_xspan):
s1.cells = right_cells
# If neither part is big enough, use sexual reproduction
else:
return sexual(candidate_seed, pop, n, max_seed_area)
# Set the correct dimensions for the new seed
s1.xspan = s1.cells.shape[0]
s1.yspan = s1.cells.shape[1]
# Update count of living cells
s1.num_living = s1.count_ones()
# Find the least fit old seed in the population. It's not a problem
# if there are ties.
s2 = find_worst_seed(pop)
# Now we have:
#
# s0 = seed 0
# s1 = left or right side of seed 0
# s2 = the least fit old seed, which will be replaced by s1
#
# Replace the least fit old seed in the population (s2) with the
# chosen part (s1).
i = s2.address # find the position of the old seed (s2)
s1.address = i # copy the old position of the old seed into s1
pop[i] = s1 # replace s2 (old seed) in population (pop) with s1
# Build a history for the new seed, by matching it against all seeds
# in the population.
width_factor = mparam.width_factor
height_factor = mparam.height_factor
time_factor = mparam.time_factor
num_trials = mparam.num_trials
pop_size = len(pop)
for j in range(pop_size):
update_history(g, pop, i, j, width_factor, height_factor, \
time_factor, num_trials)
update_similarity(pop, i, j)
# Report on the new history of the new seed
message = "Run: {}".format(n) + \
" Whole fitness (s0): {:.3f}".format(s0.fitness()) + \
" Fragment fitness (s1): {:.3f}".format(s1.fitness()) + \
" Replaced seed fitness (s2): {:.3f}\n".format(s2.fitness())
# Return with the updated population and a message.
return [pop, message]
#
# symbiotic(candidate_seed, pop, n, max_seed_area)
# -- returns [pop, message]
#
def symbiotic(candidate_seed, pop, n, max_seed_area):
"""
Create a new seed by joining two existing seeds (fusion) or
by splitting one seed into two seeds (fission). If fission
is chosen, only one of the two resulting seeds is used.
If neither fission nor fusion is chosen, we default to
sexual reproduction.
"""
# Decide whether to use fission, fusion, or sexual reproduction.
# To avoid bias, it makes sense to set these two probabilities to