In this part, we'll code the following features:
- Add a save and load system
- Add a way to speed up the evolution
Starting with the save and load system, we'll utilize the pickle
package.
pickle
allows saving entire Python objects, including lists, dictionaries, and custom objects (like Population
class object), making it easier to pause and resume simulations.
An excerpt from the package documentation.
The pickle module implements binary protocols for serializing and de-serializing a Python object structure. “Pickling” is the process whereby a Python object hierarchy is converted into a byte stream, and “unpickling” is the inverse operation, whereby a byte stream (from a binary file or bytes-like object) is converted back into an object hierarchy.
Serialization/pickling is the process used to save the state of an object while de-serialization/unpickling is for loading the state of an object.
Now let's import the package inside the dots.py script. Add the following line.
import pickle
Next, let's add the following methods to the Population
class to use the package.
def save(self, file):
with open(file, 'w+b') as f:
pickle.dump(self, f)
@staticmethod
def load(file):
with open(file, 'r+b') as f:
obj = pickle.load(f)
return obj
The save
method saves the current Population
object to a file. It first opens the file in binary write mode ('w+b')
and then uses pickle.dump
to serialize the current object and store it in the specified file
path.
The load
method which is a static method (@staticmethod
) loads a previously saved Population
object from the specified file
path. It first opens the file in binary read mode ('r+b')
and then uses pickle.load
to deserialize and reconstruct the Population
object. And then finally returns the loaded object.
The load
method is a static method while save
is not because save
saves a specific instance thereby needing the self
argument. While load
doesn't depend on an instance, it creates a new one.
I used @classmethod
for the crossover
method in Dot
class but never used the cls
argument. In that case too, it is better to use @staticmethod
decorator which doesn't have the cls
parameter.
You can now call population.save("path/to/file")
or Population.load("path/to/file")
to save or load respectively.
Let's add a simple system to save the Population
periodically after some generations. Add the following code inside the main execution just below the obstacles
variable.
run_dir = 'run1'
save_files = True
if save_files:
os.makedirs(run_dir)
pop_file_path = os.path.join(run_dir, 'population')
Code break down:
-
run_dir = 'run1'
: Defines the directory name where population data will be saved. If saving is enabled, a folder named 'run1' will be created to store files. -
save_files = True
: A flag that controls whether the program saves files or not. IfFalse
, saving is skipped. -
if save_files:
: This only runs ifsave_files
isTrue
. -
os.makedirs(run_dir)
creates a directory named 'run1' if it does not already exist. This ensures that files can be saved inside it. If it exists, aFileExistsError
exception will be raised. -
pop_file_path = os.path.join(run_dir, 'population')
constructs a file path for saving the population.
To use os
, you need to import the package using import os
.
Add the following code at the end of the main execution block just below print('Generation', i, ...)
to do the actual saving,
if save_files and i % 10 == 0:
population.save(f'{pop_file_path}_{i}')
This piece saves the population
every 10 generations (i
is the generation number). This prevents saving on every generation, reducing file clutter. f'{pop_file_path}_{i}'
constructs a filename like:
- 'run1/population_0' (for generation 0)
- 'run1/population_10' (for generation 10)
- 'run1/population_20' (for generation 20)
This allows tracking progress by keeping separate save files for different generations. You can modify the generations interval before each save by changing the 10
in i % 10 == 0
to your desired interval.
To load a saved population, you use population = Population.load('path/to/file')
instead of population = Population(GOAL, POPULATION)
which creates a new one.
You can now run the script. A new folder named run1 will be created in the scripts directory. And if you have let the first generation to finish, a file named population_0 will be created inside the new folder. That is the saved population file of generation one.
Sometimes, you might create obstacles that are very hard to deal with, taking thousands of generations before achieving something significant.
In that case, if we can find a way to speed up the simulation, that will be of tremendous importance. To speed up the simulation, we can stop rendering the dots and the obstacles. And more importantly, stop limiting the frames. We'll only render the texts to get a sense of what's is happening. By doing all these, the speed of the program won't be intentionally limited, it will only be limited by the calculations it does.
To do this, we'll first create a variable that controls whether or not all objects will be rendered. Add the following code just below this line save_files = True
.
render_objects = True
Setting this variable to render or not before running the program seems very static. Instead, it will be better if we can dynamically change the variable while the program is running. This will allow us to render everything or not when ever we want. We will use clicking on the screen to toggle the rendering. Clicking on the screen will turn off rendering everything and clicking it again will turn it on.
We will utilize the event for-loop to do this. But this is just one of the change we will make to the main execution block. We will also:
1- Use the variable inside the main execution block's while-loop to only render everything if the variable is set to True
.
2- Use the variable to render only the texts if the variable is set to False
.
Modify the block to match the following one in order to add the new features.
if __name__ == '__main__':
window = pg.display.set_mode((WIDTH, HEIGHT)) # Creates the window
pg.display.set_caption('Dots Simulation') # Sets window's caption
clock = pg.time.Clock() # Clock for controlling fps
font = pg.font.SysFont('comicsans', 20) # font for creating texts
obstacles = OBSTACLES3
run_dir = 'run1'
save_files = True
render_objects = True
if save_files:
os.makedirs(run_dir)
pop_file_path = os.path.join(run_dir, 'population')
if GOAL not in obstacles:
obstacles.append(GOAL)
population = Population(GOAL, POPULATION)
#population = Population.load('run1/population_100')
reached_goal = 0
for i in range(GENERATIONS):
while population.alive():
for e in pg.event.get():
# Handling window close event
if e.type == pg.QUIT:
pg.quit()
quit()
elif e.type == pg.MOUSEBUTTONDOWN: # Handling mouse/screen click
render_objects = not render_objects
alive = population.update(obstacles)
# If render_objects is set to true
if render_objects:
# Rendering all objects
window.fill('white')
for obstacle in obstacles:
obstacle.draw(window)
population.draw(window)
gen_text = font.render('Generation: ' + str(i), 1, 'black')
window.blit(gen_text, (10, 10))
alive_text = font.render('Alive: ' + str(alive), 1, 'black')
window.blit(alive_text, (10, gen_text.get_height() + 10))
reached_goal_text = font.render('Reached: ' + str(reached_goal), 1, 'black')
window.blit(reached_goal_text, (10, alive_text.get_height() * 2 + 10))
# Update the display
pg.display.flip()
clock.tick(60) # Limit frames to 60 per second
best, best_moves, reached_goal = population.generate_next_generation()
print('Generation', i, 'Best dot moves', best_moves, 'Reached Goal:', reached_goal)
# If render_objects is set to false
if not render_objects:
# Rendering only texts
window.fill('white')
gen_text = font.render('Generation: ' + str(i), 1, 'black')
window.blit(gen_text, (10, 10))
reached_goal_text = font.render('Reached: ' + str(reached_goal), 1, 'black')
window.blit(reached_goal_text, (10, gen_text.get_height() + 10))
# Update the display. Frames not limited
pg.display.flip()
# Save population after every 10 generations
if save_files and i % 10 == 0:
population.save(f'{pop_file_path}_{i}')
First, this line elif e.type == pg.MOUSEBUTTONDOWN:
checks if the user clicked the mouse (or tapped the screen on mobile). If it returns True
, then render_objects = not render_objects
will toggle the variable on or off depending on its current value. If it is True
, it will set it to False
, and if it is False
, it will set it back to True
.
Next, this line if render_objects:
checks if render_objects
is True
. If True
, the block proceeds with rendering everything onto the screen otherwise not. The block also limits the program to only 60 frames per second.
Lastly, this line if not render_objects:
checks if render_objects
is False
and if so, the block proceeds with rendering only the 'Generation' and 'Reached' texts onto the screen. When render_objects
is set to False
, rendering is only done once - after the generation has finished - without limiting the frames to help speed up the program.
Run the program and try the new feature by clicking on the screen.
You may have noticed a little delay before the objects disappear while turning off the display. This is because after clicking the screen to turn off rendering, the already rendered frame won't be updated unless the generation is finished. We can stop this by updating the display immediately after detecting a click event without waiting for the generation to finish. This is left to you, try removing it.
This will be the end of the Genetic Algorithm part of this simulation. In the coming parts (dateless), we will discard the weak chromosomes and give the dots brain and sensors (eyes) using NEAT to traverse the environment even more intelligently.
Feel free to experiment with the code by adding new obstacles, changing parameters, or even modifying the fitness function. Share your results or insights from your experiments in the comments.
Download the full source code up to this point from GitHub.
If you've followed this far, it means you're passionate about AI! Take your learning further with my book, where I covered AI search techniques with hands-on projects. Get your copy on Amazon today!
Love diving into the world of AI and creativity? Join our AICraftsLab Discord community and connect with fellow enthusiasts—let’s craft something amazing together!
Top comments (0)