title | cover title | description | programming_language | learning objectives | facilitators | estimated time | prerequisites | authors | readings | ethical considerations | projects | |||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Creating Simulations With Python |
Simulations With Python |
In this workshop, you will learn how to create very basic simulations in Python. The workshop will employ the logic of Python's 'object-oriented' methodology to create two small simulations: a dice rolling simulation and a Critter simulation that tracks the population growth of an imaginary species of critter based on a variety of factors. These exercises are intended to be a fun introduction to higher-level Python concepts, and to give you a sense of how you can use Python to create simulations that model real-world scenarios. |
jupyter |
|
|
|
|
|
|
|
|
A simulation is a model of a real-world system or process. Simulations are used to predict the behavior of a system, to test various conditions, or to analyze the effects of different variables. Simulations are often used in scientific research, but they can also be used to model social, economic, or political systems. For instance, during the Covid-19 pandemic, researchers used simulations to model the spread of the virus and to predict the effects of various public health measures. While the simulations we will create today will be very basic, they follow the same underlying logic as more complex simulations.
In this workshop, you will learn how to create very basic simulations using Jupyter notebooks and the Python programming language. The workshop will employ the logic of Python's "object-oriented" methodology to create two small simulations: a dice rolling simulation and a Critter simulation that tracks the population growth of an imaginary species of critter based on a variety of factors.
Object-oriented programming is a programming paradigm that uses objects and their interactions to design applications and computer programs. It is based on the concept of objects, which can contain data in the form of fields (often known as attributes or properties), and code in the form of procedures (often known as methods).
In general, these exercises are meant to expose you to some of the higher-level concepts integral to Python programming. So, without further ado, let's begin!
First, let's create our dice rolling simulation. We will create a program that allows us to simulate a die roll x number of times and analyze the overall results.
The general outline and goals for this part of the workshop are as follows:
1) Create a simple dice rolling program to demonstrate classes and objects on a basic level.
Concepts we'll focus on:
- Class declaration and naming conventions
- Constructors/init
- Methods (functions within classes)
- Objects and instantiation
2) Run the simulation by rolling the dice as many times as we want, and evaluate the results.
Concepts we'll focus on:
- Passing methods as arguments into other classes/functions
- Specifying iterations
- Using stats methods
- Printing results
As you can see, we will be covering quite a lot of new concepts and skills. It is natural to feel a bit overwhelmed at first, but don't worry--we will be taking things step-by-step. Let's begin!
A full source file of the program we will be writing in this section can be imported here:
You are welcome to use the source file as a reference, but we encourage you to try to write the code yourself first. If you get stuck, you can always refer to the source file for help.
Let's begin writing our own program by opening Jupyter notebooks. On the side of the screen, click the "Open Code Editor" button to slide out the Jupyter environment. First, let's create a new Python file called DiceSim
(the Python (Pyodide) version will suffice). Because we will be using random numbers, and because we want to perform statistical methods on values, we will need to import two libraries at the top:
import random
import statistics as stats
This import will allow us to use Python's random
module. Remember to press shift + enter to run the cell.
If you have used other programming languages you might be familiar with the concept of seeding. For our purposes, we do not need to explicitly seed random number generation when using Python's random library, as it will use either current system time or OS random resources by default.
Because we will be referencing the statistics library a few times in our code, we can import
it as stats
, just to shorten what we have to type every time.
Next, let's create a class
to represent our die object. You can think of a class like a user-defined blueprint or prototype from which objects are created. Classes provide a means of bundling data and functionality together. Creating a new class creates a new type of object, allowing new instances of that type to be made. Each class instance can have features attached to it for maintaining and modifying its state.
For example, if we were to define a Car
class, we might consider what attributes a car has: brand, mileage, top speed, diesel or gas, stick or auto, 2-door or 4-door, etc. We could begin defining these characteristics as part of the overall class Car
using variables. Variables particular to a class are called data members. We could then consider what actions a car can perform: e.g., drive, brake, refuel, etc., and begin to write some functions in our Car
class that describe these actions. Generally, functions that are specific to a certain class are called methods. Unlike functions proper, methods use . (dot) notation in order to be called--you'll see exactly what that means here shortly.
Because we are creating a dice simulator here, let's go ahead and create a new class Die
. In a new cell in your Jupyter notebook, type the following:
# create a new die class
class Die:
def __init__(self, sides):
self.sides = sides
We begin by defining our class. By convention, class names in Python are always Uppercase. We then initialize our class using the __init__
method.
The __init__
method is a special method that Python runs automatically whenever we create a new "instance" (a new object) of a class. This method has two leading underscores and two trailing underscores, a convention that helps prevent Python’s default method names from conflicting with your own method names. The self
parameter is required in the method definition, and it must come first before any other parameters. It must be included in the definition because whenever we call this method later, the method call will automatically pass the self
argument. Every method call associated with an object (in this case, our die object) automatically passes self
, which is a reference to the object itself; it gives the individual instance access to the attributes and methods in the class. If this still seems a bit confusing, don't worry--we'll see how this works in practice throughout the workshop.
Our other parameter is sides
. We include this in our __init__
because every die object that is created will have a certain number of sides (typically 6) as an attribute. In short, your __init__
parameters should always include characteristics that you want every newly created object in your class to have.
Next, we define our parameters, prefixed with self
. Any variable prefixed with self
is available to every method in the class, and we’ll also be able to access these variables through any instance created from the class. The line self.sides
, for example, takes the value associated with the parameter sides
and assigns it to the local variable sides
, which is then attached to the object being created.
Next, let's create a method that allows us to roll our die object. Because methods are a part of a class, make sure you are indenting the method so that it is within our overall Die
class, in the same cell.
# create a 'roll' method to return a random # between 1-6
def roll(self):
return random.randint(1, self.sides)
We first define (def
) our method, and pass in the parameter self
, which allows us to access the data members of our class. Next, we generate a random number using the random
library. The randint
method returns an integer in a range between the first value (1
) and the second (self.sides
). We also return
the value from the method so we can check the results.
Now that we have our class and our method set up, we can create (or "instantiate") a new die object and give it a roll. In a new cell, type the following:
die = Die(6)
print(die.roll())
To create a new object, you begin as you would with any variable definition. Here we indicate that our die
(lowercase!) object will belong to our class Die
. Because we also need to pass in a value for how many sides our object has, we will give it a value of 6
.
Next, we use a print
statement to display the results of our roll method. To call our roll method, we must use our die object and . (dot) notation--recall that methods need to act upon an object or instance of the class in order to be called.
And that's it! You have successfully created a new die object that can be rolled. If you run the cell, you should see your roll displayed. Additionally, each time you run the cell, you should see a new random number between 1 and 6 in your display.
If the user rolls a six, tell them that they're a winner and get a prize. If they didn't roll a 6, display that they're a loser.
Create a new variable that stores a roll. Then, use an `if` statement to check if the roll is equal to 6, and display the appropriate message using an `f-string`:r = die.roll()
if r == 6:
print(f"You won a prize for rolling {r}!")
else:
print(f"You rolled a {r}, not nearly as cool as a 6...")
How are methods called?
- `method_name()` - `method_name.method()` - `object.method_name()`* - `object.method_name.method()`Now that we have our basic die object, let's create a dice rolling simulator. We will create a program that allows us to simulate our die roll x
number of times and analyze the overall results.
In particular, let's say that we wanted to throw our die object 1000 times (instead of just once) and analyze the results of all the rolls. We can simulate this process by creating a new class devoted specifically to this task, so let's go ahead and make it now.
Let's call our class DiceSim
. As we mentioned, what we want this class to do is to roll our die x amount of times, and print the results. Thinking this through, we will likely want two methods as part of this class:
- A method that utilizes our
roll()
method we've already created above and runs it 1000 times, instead of just once. - A method that analyzes the results of those 1000 rolls.
So, let's do just that:
class DiceSim:
"""Rolls our dice x amount of times and prints the results
Parameters:
dice_method: The dice method that we'll pass in
iterations: The number of times to run the sim"""
def __init__(self, dice_method, iterations):
# take initial parameters
self.dice_method = dice_method
self.iterations = iterations
self.results = []
self.run()
We will pass in our two parameters for the class, one for our roll method we created previously and the other for the number of iterations we want the simulator to run. Like before, we prefix each data member with self
so that we can access the values from anywhere within our class. We also create a new empty list results
, so that we have a place to store the results of our dice rolls. We also have one method call, run()
. Calling a method in the initializer area like this means that it will automatically run every time a new object of the DiceSim
class is created.
Next, let's define the methods for our simulator class. We will want one to run our roll method, and one to analyze and print our results:
# run our die roll method and store the results
def run(self):
for i in range(self.iterations):
result = self.dice_method()
self.results.append(result)
self.report()
In our run()
method, we use a for
loop to cycle through by the number of iterations we will set. We then store the results of the die roll method in a local variable result
, and append()
that result to our results
list. Again, because we have called the run()
method in our class initializer, it will run every time a new object of the DiceSim
class is created. You can also see that we are running a new report()
method at the end of our run()
method, so that we have a chain of events that will occur every time a new object is created.
We then want to define our next method, report()
, to analyze the results from run()
.
# analyze and report the results
def report(self):
max_num = max(self.results)
min_num = min(self.results)
mean = stats.mean(self.results)
median = stats.median(self.results)
mode = stats.mode(self.results)
std_dev = stats.stdev(self.results)
variance = stats.variance(self.results)
print(
f"""Number of Simulations: {self.iterations}
Max: {max_num}
Min: {min_num}
Mean: {mean}
Median: {median}
Mode: {mode}
Standard Deviation: {std_dev}
Variance: {variance}"""
)
Our report()
method uses Python's built-in max()
and min()
functions to catch the highest and lowest roll in the list. Using the stats
library, we also check the mean, median, mode, standard deviation, and variance of the results and print it to the terminal.
Our last step is to create a new instance of our simulator class in a new cell, like we did before:
die1 = Die(6)
sim = DiceSim(die1.roll, 1000)
First, we create a new instance of our Die
class (remember, you can create as many new objects of a class as you'd like) and give it 6 sides. Then, we create a new sim
object. As parameters, we pass in our roll method and 1000, for the number of iterations to run.
And that's it! If you run the final cell, you should see a report of the results of the simulation.
What are some differences and similarities between a function and a method?
Definition: Function: A function is a self-contained block of code that performs a specific task and can be defined independently of any class or object. It takes input arguments, processes them, and returns a result. Method: A method is a function that is associated with an object or class. It is called on an object and can access the data within that object.
Usage: Function: Functions are standalone entities and can be called from anywhere in the code, provided they are in the scope. Method: Methods are associated with objects or classes and are called on instances of those objects or the class itself.
Syntax:
Function: Functions are defined using the def
keyword and can be called using the function name.
Method: Methods also use the def keyword but are defined within a class and are accessed using dot notation (object.method()
or class.method()
).
-
method Methods are functions that belong to a class. They are called using . (dot) notation.
-
self The keyword self represents an instance of a class and binds its attributes with the given arguments.
Goal: Create a simulation that tracks the population growth of a proposed (imaginary) species of Critter, taking into account a variety of biological and environmental factors.
General Process:
- Create global variables to define initial parameters for use in the simulation
- Create Critter class and constructors/initializers for each critter object
- Consider what critters need to survive, creating relevant methods (reproducing, gathering food, etc.).
- Create 'run year' function to simulate a yearly run-through of all variables and factors
- Create 'populate simulator' function to produce our starting population
- Print results of each aspect of simulation to monitor changes
- Add more complexity/factors to the simulator (e.g., disasters)
Concepts to focus on:
- Creating logical environment parameters
- Component-driven design: separating out functions so that each has a single general purpose
- Working with lists
- Coordinating data among member functions and methods
You can import the full source file for this section here:
Again, while you can use the source file as a reference, we encourage you to try to write the code yourself first. If you get stuck, you can always refer to the source file for help.
To begin, let's create a new Jupyter Python file called CritterSim
. Because we will be using a lot of random values in our program, we'll first want to import
the random
module at the beginning of the file, like we did in the previous section:
import random
When we write variables outside a specific function or class, they are considered global variables. Global variables can be accessed and used anywhere in your program. This is in contrast to local variables, which have limited scope (i.e., they can only be accessed in the particular function or class in which they live).
Global variables are useful when you want multiple functions to be able to easily access their values. It is important to note, however, that it is generally good coding practice to keep your use of global variables to a minimum. Too many global variables will make your programs confusing for other programmers looking over your code, and will require more overhead/computing power than is usually necessary.
Since our program is relatively small, and just to keep things simple for our example, we'll set up our initial parameters for the simulation as a series of global variables in a new cell below our import
statement, like so:
# initial global parameters - variables that can be accessed anywhere in your program
startPopulation = 50 # the beginning population
year = 0 # the starting year
resources = 2 # the number of units of food each able critter can produce
food = 0 # total value of food available (able critters * resources)
fertility_x = 10 # lowest age at which critter can give birth
fertility_y = 20 # upper age at which critter can give birth
disasterChance = 10 # chance of a disaster occurring
critterList = [] # list to hold all critter objects
As you can see, each variable is commented to give some context for its use. These variables will make more sense once we actually start to employ them, but let's do a quick run-through of their purposes:
Variable | Description |
---|---|
startPopulation | The initial number of Critters we want to start with in our simulation. Although 50 is not a realistic number (considering the real-life genetic consequences of inbreeding), it will work for the purposes of our simulation. |
year | The variable we'll use to track what year of the simulation it is. |
resources | The number of individual food units our critters can produce. For our simulation, each critter will need exactly one unit of food to survive, so to give them a fighting chance we'll let each "able" critter (more on that later) produce 2 units. |
food | The total number of units of food that all critters produce and have to eat in a given year. |
fertility_x | The lowest possible age at which a critter can give birth (so they're not too young). Let's be optimistic with these critters and propose they can live for a rather long time (perhaps 50 years or so), and set the low fertility level to 10. |
fertility_y | The highest possible age at which a critter can give birth (so they're not too old). To start, let's keep the fertility period rather short and set the maximum age to 20 (we can always adjust it later). |
disasterChance | The chance of a disaster occurring. We'll set this to 10% for now, but we can always adjust it later. |
critterList | A list that will contain the entire population of our critters. We will access and modify this list continuously in order to assess our total population growth. |
Now that we have our initial parameters, let's begin writing our Critter class in a new cell:
# create our Critter class
class Critter:
def __init__(self, age):
self.sex = random.randint(0,1) # 0 for male, 1 for female
self.age = age # we'll set the age differently based on diff factors
We begin by defining our class, passing the self
parameter like usual. Our other parameter is age
. We include this in our __init__
because every critter object that is created will have an age that we want to track and modify. In short, as a reminder, your __init__
parameters should always include characteristics that you want every newly created object in your class to have. Because we want our critters to reproduce, we also give them a variable sex
. For every critter instance created, we will assign them a random sex: 0 for male, 1 for female.
Now that we have created our Critter class, let's create a function that will populate our simulation with critters. I say function (not method), because it will live outside of our Critter class. Were we building a much more complex program, we'd likely want to create an entirely new Simulation class to control this aspect. Because our program is relatively simple, however, we'll simplify our code using a function. Take a look at the following code:
#populate our simulation with critters
def popSim():
for x in range(startPopulation):
critterList.append(Critter(random.randint(2, 45)))
print(len(critterList))
Try to read through it line by line and describe what it does. Can you explain it? After giving it a try, you can reveal the description below.
Here we have created a new function `popSim()`. This function will take our `startPopulation` variable defined above and populate our `critterList` with that many objects. To give our simulation an added element of randomness, each starting critter will be created with a random age between 2 and 45. Lastly, we print the length of our `critterList` to make sure it is working properly.To run the function, create a new cell and type the following:
popSim()
How might we see the age of each critter in our list? Remember, we defined an age
attribute in our initializer.
Now that we have set up some very basic characteristics of our critters and a population to work with, we will want to let them perform some actions. We can do this by writing methods. Let's create two very basic methods, gather()
and reproduce()
.
Each of these methods serve different purposes in our simulation. The gather()
method will allow our critters to gather food, and will also determine if any critters starve to death from a stockpile shortage (we'll attempt this latter part later). The reproduce()
method will allow our critters to reproduce (obviously), and will allow our overall population to grow as the years advance. Let's start with the gather()
method.
Because we will certainly want our critters to eat, let's create a method that allows them to gather food. Make sure you properly indent your method so it is contained within the Critter class we defined above, in the same cell.
# method for critters to gather food
def gather(food, resources):
ableCritters = 0 # start with a fresh value, then add based on current population
for critter in critterList:
if critter.age > 10 and critter.age < 40:
ableCritters +=1
food += ableCritters * resources
print(f"Food stockpile: {food}.")
print(f"Able critters: {ableCritters}.")
We first define our gather
method, and pass two arguments into it: food
and resources
, which we have already declared above as global variables. We then create a new variable, local to the gather
method: ableCritters
. Because we wouldn't want our newborn baby critters going out to gather food until they've grown up a bit, we will use ableCritters
to ensure they won't gather food until they're of a certain age. For now, let's set this value to 10 (anticipating a generous long life and old age for our critters), and let them enjoy their retirement from gathering food at 40. So, using a for
loop, we'll then cycle through our critterList
and see if any existing critters are over the age of 10 and under the age of 40. If they are, we'll add them to the number able to gather food and store the number. Next, we'll use this value, multiplied by our resource value, to determine the current total quantity of food stores. Lastly, we print out the results of both our food stockpile and our number of able critters.
To see this method in action and make sure we are getting proper results, we can call it below, in the cell where we populate our sim. As you should recall, every method needs an object or a class to act upon. In this case, we can simply designate our class like so:
popSim()
Critter.gather(food, resources)
If you run the program now, you should see the results of our gather
method displayed. If you run it again, you should see the results change, as the number of able critters will be different each time. In any case, there should always be twice the amount of food as there are able critters, because each able critter can produce two units.
For a population growth simulation, another basic action our critters should take is to reproduce. Let's specify some parameters to determine which critters are able to produce offspring. Again, within your Critter class, create a new method called reproduce()
like so:
def reproduce(fertility_x, fertility_y):
for critter in critterList:
if critter.sex == 1: # if the critter is female and of defined fertile age
if critter.age >= fertility_x:
if critter.age <= fertility_y:
if random.randint(0, 4) == 1: # give a 1 in 5 (20%) chance of pregnancy
critterList.append(Critter(0)) # add newborn critter
Our method will take the two fertility parameters we established before (which determine the ages at which a critter can reproduce) and use a for
loop to take a look at each critter in the list to see if they are suitable candidates for giving birth. We first check if they are female, and if so, check if they are within our circumscribed fertility age. If both of these conditions are satisfied, we'll then give the critter a 20% chance of getting pregnant with a simple random
calculation. If the result is a 1, we'll create a new critter and append it to the critter list with an age of 0.
We can now run the reproduce()
method in the same cell as the gather()
method, like so:
popSim()
Critter.gather(food, resources)
Critter.reproduce(fertility_x, fertility_y)
This will show you if any new critters were born in the first (and currently only) year of the simulation. Note that at the moment, you will not be able to see the new critters in the list, because we have not yet advanced the years or aged our existing critters. We will tackle those aspects soon.
Occasionally, new critters may not be born during a year. To analyze this, once the reproduction cycle has finished, print out a short message to the terminal saying "New critters have been born!", if indeed new critters were born that year.
At the start of the reproduce method, create a new local variable to record how many critters we start out with. Then, outside of the `for` loop, compare that initial value with the new quantity or length (`len()`) of the critter list. So, your modified code should look something like this: def reproduce(fertility_x, fertility_y):
# Challenge: print statement to show that new critters were born
initial_pop = len(critterList) # create a local var to store how many critters we start with
for critter in critterList:
if critter.sex == 1: # if the critter is female and of defined fertile age
if critter.age >= fertility_x:
if critter.age <= fertility_y:
if random.randint(0, 4) == 1: # give a 1 in 5 (20%) chance of pregnancy
critterList.append(Critter(0)) # add newborn critter
# If new critters have been added, print the message
if initial_pop < len(critterList):
print("New critters were born!")
Note that if you printed the message inside the for
loop (instead of outside of its scope) you would display messages every single time a new critter is created (making your readout rather messy), rather than after all new critters are created.
Now that our critters can gather food and reproduce, we will want to create the function that allows our simulation to run through a year and age our critters accordingly. Because this is a standalone function, write the following in a new cell below your popSim()
function and method calls:
def runYear(food, resources, fertility_x, fertility_y):
Critter.gather(food, resources)
Critter.reproduce(fertility_x, fertility_y)
# age our existing critters
for critter in critterList:
if critter.age > 50:
critterList.remove(critter)
else:
critter.age +=1
We will pass 4 arguments into this function: our food, resource, and fertility values. The reason we need to get at these values is because we will want to then pass them into our Critter method calls. That is the first step we take: calling our gather()
and reproduce()
functions, at the beginning of each year. Because we are calling these methods outside of their class, we need to prefix the calls with our class name Critter
. Because this function is meant to encapsulate the activities our critters undertake each year, it makes sense to call these methods here. Since we are calling them here, you can delete both method calls from the previous cell (in the cell with the popSim()
function).
Next, we will age our existing critters by one year. For now, let's set their maximum age to 50 years--once they are over 50 years, we will assume they die of old age, and we remove
the critter from our list. If they are not over 50, we simply add one year to their age.
If you call and run the runYear()
function, you'll notice that we are not yet actually advancing the years in our simulation. Our goal is to run our simulation for a certain number of years, and then print out the results for each year.
To do this, we will want to create a while
loop that will run our simulation repeatedly until certain conditions are met. To do this, we can define a new cell that encapsulates both our popSim()
function and our runYear()
function. Firstly, then, delete any cells that contain these functions calls (we will call them in the new designated cell instead).
In the new cell, type the following:
print("--------The Critter Simulation has begun!---------\n\n")
popSim()
while len(critterList) < 100 and len(critterList) > 1:
runYear(food, resources, fertility_x, fertility_y)
year += 1
print(f"Current year: {year}\n")
We first print a message notifying the user the simulation has started. Next, we run our popSim()
function to initially populate our critter species. Then, we use a while
loop to make sure the program runs until certain conditions are satisfied. Our conditions will be population limits: so, the simulation will terminate once either all critters have died (until the population reaches 0), or until the population reaches over 100. Until this happens, we will continue to run our runYear()
function, and increase the current year. We also print that year to the screen, so we can keep track of our simulation.
Run all cells in your notebook again, and you should see the simulation run through a variable number of years (however many it takes that particular instance of the simulation to reach 100 critters).
Great! We now have a functioning simulation that allows our critters to gather food, reproduce, and age. However, our critters are currently living in a kind of critter utopia, without any dangers or environmental factors to worry about. Let's add some more complexity to our simulation.
Sadly, starvation is a reality for many species. If the current food stockpile is not sufficient to feed every critter in a given year, we will (unfortunately) need to reduce our critter population accordingly.
In order to simulate this on a basic level, we will want to add a few more lines to our gather
method (because this method is concerned with food production/consumption). Let's go back to that method and add the following code:
# if there are not enough able critters to produce food in the pop, food will deplete
if food < len(critterList):
del critterList[0:int(len(critterList) - food)] # del a slice of the list based on how many critters starve
# since we are starting from the beginning of our list, we are likely killing older critters (because they will have aged)
food = 0
print(f"Some critters starved to death! :(")
else:
food -= len(critterList) # otherwise, just remove food equal to the amount of critters, the rest is stored for next year
print(f"Population after starvation/feeding is: {len(critterList)}.")
print(f"After eating, food stockpile is currently {food}.") # food = initial food - pop after eating
As mentioned, for each critter to survive they will need exactly one unit of food per year. So, in this modified method, we first check to see if our food stockpile is smaller than the amount of existing critters. If so, we use the del
statement to remove a slice of our critter list. As you can see, we are taking a slice starting from the beginning of the list and removing an amount equal to the excess amount of critters. While this is not quite as random as it could be (because we are always starting at the beginning of our list), it might make sense that more elderly critters are not able to reach the food stockpile as quickly as their younger, more able counterparts. If some critters starve, then we display a message telling us so. If no critters starve, we simply remove an amount of food equal to the critter population, and store the surplus for next year. We then print our overall results.
Lastly, let's set up a basic environmental disaster scenario. To add this functionality, we will want to add a few lines to our runYear()
function, so that every year we have a potential chance for a disaster. Let's go back to that function and add the following:
# set up chance for a disaster
if random.randint(0, 100) < disasterChance:
del critterList[0:int(random.uniform(0.05,0.2)*len(critterList))]
print("A disaster has occurred!")
print(f"There are now {len(critterList)} surviving critters.")
print(f"After reproducing and/or any disasters, critter population is currently {len(critterList)}.")
Using our disasterChance
global value we set at the outset of the program, we test to see if a random number falls in its range (i.e., if it's within 0-9, so a 10% chance). If so, we will del
(delete) some critters from our list, again taking a slice. We generate a random number between 5% and 20% to see how much damage the disaster will inflict. We multiply this value by the length of our critter list to get a 5-20% slice of our population, and then delete that slice. To keep tabs in our readout, we display a message saying a disaster has occurred, and print out the surviving population.
If you run the simulation now, you should see that we now have a more interesting simulation playing out, with the new elements we defined factoring into our critter population growth (or lack thereof!).
What is the difference between a global and a local variable?
__Global variables__ are variables that are defined outside of a function or class. They can be accessed _anywhere_ in your program.Local variables, on the other hand, are variables that are defined within a function or class/method. They are only accessible within the scope of that function or class.
- len() The length function allows us to catch the number of elements in a given list.-
remove() Removes the current single value from a list.
-
del() Removes a particular range or slice from a list.
-
while While statements will loop through a set of instructions until certain conditions are met.
Congratulations for making it this far! You now have a working simulation and a basic understanding of how to create your own classes, methods, and functions. These are key concepts in object-oriented programming, and will be useful to you as you continue to learn Python and other programming languages.
Below are some additional ideas, challenges, and resources to help you continue to build on what you've learned.
To gain more familiarity with managing larger programs, try to add some more complexity to your simulation. Here are some ideas:
- Create a new "play" method that allows your critters some much needed time for relaxation and fun. If critters are not able to play, they will become sad and unmotivated/unable to gather food for the population in a given year. If they are able to find time to play, however, they will be more motivated to gather food and will produce extra resources.
- Add a new method that allows your critters to die of disease (instead of just starvation and disasters). If a critter survives a disease, they will be immune to it for the rest of their lives.
- Create different temperaments or personalities for individual critters. For instance, some critters might be more aggressive and less likely to die of starvation, but more likely to die due to infighting with other critters. Other critters might be more motivated to find a mate and reproduce, but less likely to gather food. There are endless combinations you can envision and create!
At the moment, our simulation output isn't very user-friendly. It is quite difficult to read and assess trends in the population growth (along with just not being very pretty to look at). If you want to take your simulation to the next level, you might try using the matplotlib
library to create a visualization of your simulation. This will allow you to see the population growth in a plot graph, and will give you a better sense of how your population is evolving over time.
Let's look at a brief example of how we might achieve this functionality by plotting the population growth over time.
To do this, you will first need to import the matplotlib
library at the beginning of your program, like so:
import matplotlib.pyplot as plt
Then, you will want to create a new list to store the population of your critters at the end of each year. To do this, add the following line to your global variables:
popList = []
Next, you will want to add a new line to your runYear()
function, like so:
# add the current population to our list
popList.append(len(critterList))
This will add the current population to our popList
at the end of each year. Finally, you will want to add the following lines to the end of your simulation, after the while
loop:
# plot the results
plt.plot(popList)
plt.ylabel('Population')
plt.xlabel('Year')
plt.show()
This will plot the results of your simulation, showing the population growth over time. You can add this functionality to your program, and then run all cells to see how it works.
To get more practice, try adding more elements to your plot, such as the food stockpile, or the number of able critters. You might also create entirely new plots detailing the trends of disasters or starvation. You can also try adding a legend to your plots, or changing the color of the lines. There are many ways to customize your plots using the matplotlib
library, so check out the documentation linked in resources below and feel free to experiment!
If you want to learn more about object-oriented programming in Python, I recommend checking out this W3 schools tutorial: Python Classes and Objects
If you want to learn more about how to use matplotlib
, you can check out the official documentation.
If you are interested in doing more with simulations in Python, you might want to check out the SimPy library, which allows you to create discrete event simulations. You can find the documentation here.
PyChrono is another Python library that allows you to create simulations, but with a focus on physics. You can find more about the library here.