Homework 05: Python Classes and ABM#

✅ Put your name here.

#

✅ Put your GitHub username here.

#

Goals for this homework assignment#

By the end of this assignment, you should be able to:

  • Read and write Python classes, including classes that leverage object inheritance and composition.

  • Run and agent based model simulation.

Work through the following assignment, making sure to follow all of the directions and answer all of the questions.

This assignment is due at 11:59pm on Friday, April 12. It should be uploaded into the “Homework Assignments” submission folder for Homework #5. Submission instructions can be found at the end of the notebook.

Full disclosure: This homework was created by a Physics professor with expertise in particle simulations so I can only help with the class structure, not with the physics!

Grading#

Part 1 (16 points)

  • Question 1.1 (3 points)

  • Question 1.2 (9 points)

  • Question 1.3 (2 points)

  • Question 1.4 (2 points)

Part 2 (28 points)

  • Question 2.1 (18 points)

  • Question 2.2 (2 points)

  • Question 2.3 (8 points)

Part 3 (27 points)

  • Question 3.1 (12 points)

  • Question 3.2 (6 points)

  • Question 3.3 (9 points)

Total: 71 points

Part 1: Writing Python classes (16 points)#

Image credit: www.wikipedia

Using and enhancing a pre-existing class#

For this section, you’re going to explore a little bit of particle physics.

The Python class contained in the cell below is called ElementaryParticle and, not surprisingly, it is designed to store information about the building blocks of the universe. Currently it only has few simple methods. The text in red is called a docstring and it is a piece of text describing what the class is, does, and what its attributes are. In a new cell try running the code print(ElementaryParticle.__doc__).

import numpy as np

class ElementaryParticle:
    """
    Elementary particle class.

    Attributes
    ----------
    x : float
        x-position of the particle.

    y: float
        y-position of the particle.

    ptype: str
        Statistics obeyed by the particle.

    charge : float
        Electric charge of the particle.

    mass : float
        Rest mass in MeV of the particle.

    spin: float
        Spin of the particle.

    Methods
    -------
    info():
        Prints particles information.

    is_antiparticle(other):
        Check if the other is this particle anti-particle

    move()
        Move the particle randomly.

    place_at(coord):
        Place the particle at passed coord.


    """
    def __init__(self, charge, mass, spin):
        """Initialize the particle's attributes.

        Parameters
        ----------
        charge : float
            Electric charge of the particle.

        mass : float
            Rest mass in MeV of the particle.

        spin: float
            Spin of the particle.

        """
        self.charge = charge
        self.mass = mass
        self.spin = spin
        self.x = None
        self.y = None
        
    def info(self):
        """Print to check information about the particle."""

        print(f"The particle has a mass of {self.mass} MeV")
        print(f"The particle's charge is {self.charge} e")
        print(f"The particle's spin is {self.spin}")

    def place_at(self, coord):
        """Place particles at coordinates (x,y).

        Parameters
        ----------
        coord: tuple
            (x,y) coordinates where to place the particle.
        """
        self.x = coord[0]
        self.y = coord[1]

    def move(self):
        """Move the particle by randomly pushing it in both directions."""
        self.x += np.random.randint(low=-1, high=2)
        self.y += np.random.randint(low=-1, high=2)

✅ Task 1.1 (3 points) In the cell below create a particle called electron with mass = 0.5, charge = -1., spin = 1./2. and another particle called photon that has mass = 0.0, spin = 1.0, charge = 0.0. Then for each particle run the info method. Does the print out match your initialization parameters?

# Create two particles

#----your code here-----#

# Then this should print the information about the particles

print("{:=^60}".format(" Electron "))
# --- your code to call the electron info method here
print("{:=^60}".format(" Photon "))
# --- your code to call the photon into method here
========================= Electron =========================
========================== Photon ==========================

✅ Task 1.2 (9 points); Make sure to read everything carefully!: Using the ElementaryParticle class as a starting point, your goal is to do the following:

  • Add a new attribute, ptype, to the class so that it is set to a default of None when the class object is first initialized. This attributes will indicate whether the particle is a boson or a fermion.

  • Add a class method, called check_type, that takes no input. This method should:

    • check whether the particle is a “fermion” or a “boson”. Bosons have integer spin, fermions have fractional spin. Hint: look at the built-in methods of float. Is there one that could help you answer this question?

    • update the attribute ptype with the type of particle, "fermion" or "boson".

    • return the attribute ptype.

  • Add another class method, called compare, that takes one input, called other. Using this input, this method should:

    1. Check that other is an instance of the ElementaryParticle class. Hint.

    2. If so it must print to screen whether their charges are equal, whether their masses are equal, and whether their spin are equal.

    3. If other is not a particle prints an error message that requests the user to pass a valid particle. Additional reading if you want to get fancy.

In order to check that everything is working correctly, run the following cells. It should not return any errors and should print the following:

======================== Check Type ========================
fermion
boson
======================== Comparison ========================
Electron vs Photon
The two particles have the same charge: False
The two particles have the same mass: False
The two particles have the same spin: False
# COPY THE ELEMENTARY PARTICLE CLASS FROM ABOVE AND IMPROVE IT HERE
# Check that your code is correct 
up_quark = ElementaryParticle(charge = 1./3, mass = 2.2,  spin = 1./2.)
gluon = ElementaryParticle(charge = 0.0, mass = 0.0, spin = 1.0)

# Fermions or Bosons?
print("{:=^60}".format(" Check Type "))
print(f"{up_quark.check_type()}")
print(f"{gluon.check_type()}")

# Compare
print("{:=^60}".format(" Comparison "))
print("Up Quark vs Gluon")
up_quark.compare(gluon)
======================== Check Type ========================
fermion
boson
======================== Comparison ========================
Up Quark vs Gluon
The two particles have the same charge: False
The two particles have the same mass: False
The two particles have the same spin: False

✅ Question 1.3 (2 points): You should practice writing docstrings in your code so that it is clear what your methods are doing! You can look at the provided code to get a sense for how this is done.

  • Add a docstring for both the check_type and compare methods you just wrote.

  • For the docstring for compare, be sure the output tells the user what type of input(s) compare takes.

Run the following code to print out your docstrings. You should not get any errors.

print("\n{:=^50}".format(" Documentation for check_type "))
print(ElementaryParticle.check_type.__doc__)
#
print("\n{:=^50}".format(" Documentation for compare "))
print(ElementaryParticle.compare.__doc__)
========== Documentation for check_type ==========
Check whether the particle is a Fermion or a Boson and update the corresponding attribute.

=========== Documentation for compare ============
Compare this particle with another particle.

        Parameters
        ----------

        other: ElementaryParticle
            Particle to compare with.

        

Testing all of your code is important!#

Any time you write new code, you should get in the habit of trying to test it to make sure it is working as intended, but we didn’t check all of your new functionality in the code cell above.

✅ Question 1.4 (2 points): Is everything working correctly if you don’t pass an ElementaryParticle to the compare method?

Write some code that proves that your compare method is working correctly when you are not passing an ElementaryParticle object. You have some options for how you might do this. For example, what happens (and what should happnen!) if you pass in an integer or a string?

In the markdown cell below your code, explain how you know that your method is working correctly based on the code that you provided.

# PUT YOUR CODE HERE

Do This - Erase the contents of this cell and replace it with your explanation for how you know that you compare method is working correctly.

Part 2: Inheritance and Composition (28 points)#

Once we’ve built a class, if we decide we need some sort of specialized functionality, we can create a new class that inherits all of the properties of the first one. Let’s do that now!

As you can see in the figure at the top of the notebook there are different type of particles: quarks, leptons, gauge bosons, and scalar bosons. Our ElementaryParticle class has all the functionalities and properties common amongst all these particles, but all the particles fall into one of two categories: fermions or bosons. Fermions are particles that create matter while bosons are called force carriers. SideNote: Anytime two charges repel/attract each other they are exchanging photons amongst them. 🤓

✅ Question 2.1 (18 points): Now that you have a functioning class, your next task is to create two new classes, Fermion and Boson, that inherit the ElementaryParticle class.

The new Boson class, has the following documentation:

"""
Boson: elementary particle that obeys Bose-Einstein statistics.
This class inherits the ElementaryParticle class with all its attributes and methods.
Further attributes and methods are

Attributes
----------
name: str
    Name of the particle.

Methods
-------
check_existence()
    Checks whether this Boson can exists by calling its parent's method check_type()
    Raises a ValueError if check_type() returns "fermion"
        
"""

The new Fermion class has the following documentation:

"""
Fermion: elementary particle that obeys Fermi-Dirac statistics.
This class inherits the ElementaryParticle class with all its attributes and methods.
Further attributes and methods are

Attributes
----------
name: str
    Name of the particle.

Methods
-------
check_existence()
    Checks whether this Fermion can exists by calling its parent's method check_type()
    Raises a ValueError if the check_type() returns "boson"
    
is_antiparticle(other):
    Check whether other is the anti-particle of this Fermion 
    by checking if other is an instance of Fermion first.

"""
  • These documentations tell you what methods and attributes you need to write.

  • In addition, both classes should have an __init__ method that takes in four arguments: name, charge, mass, spin. This method should use ElementaryParticle’s __init__ method to initialize the last three attributes. In addition this method should call the method check_existence to verify that we have passed the correct arguments. See documentation above.

  • As mentioned in the documentation above, the Fermion class should have a new class method, is_antiparticle, that takes one input, other, and verifies whether it is the antiparticle. An antiparticle is an exact copy of a particle with the only difference that the antiparticle has the opposite sign. As an example see electron and positron. This method should check whether other is an instance of a Fermion and return True if other is an antiparticle or False if it is not.

  • Copy the above docstrings into your code.

Run the code in the next cell as a first check. You should get this result

======================== Comparison ========================
Electron vs Positron
True
# Put your Fermion and Boson Classes here
# Run this cell to check your code from above
# Create two particles
electron = Fermion(name = "electron", charge = -1., mass = 0.5,  spin = 1./2.)
positron = Fermion(name = "positron", charge = 1.0, mass = 0.5, spin = 0.5)

# Compare
print("{:=^60}".format(" Comparison "))
print("Electron vs Positron")
print(f"{electron.is_antiparticle(positron)}")
======================== Comparison ========================
Electron vs Positron
True

Testing all of your code is important!#

Any time you write new code, you should get in the habit of trying to test it to make sure it is working as intended, but we didn’t check all of your new functionality in the code cell above.

✅ Question 2.2 (2 points): Is everything working correctly? What happens when you create a boson with fractional spin? What happens if you two Fermions have opposite charges but different masses?

Write some code that proves that your methods are working correctly. You have some options for how you might do this.

In the markdown cell below your code, explain how you know that your method is working correctly based on the code that you provided.

# PUT YOUR CODE HERE

Do This - Erase the contents of this cell and replace it with your explanation of how you would test your code.

Ok, now we have the building blocks to start creating more complex particles.

✅ Question 3.2.3 (8 points): Create a new class called CompositeParticle that has the following documentation

"""
A particle composed of several elementary particles.

Parameters
----------

name: str
    Name of the particle.

particles : list
    List of particles objects that compose this particle.

charge : float
    Electric charge of the particle.

mass : float
    Rest mass in MeV of the particle.

spin: float
    Spin of the particle.
    
"""
  • The above documentation tells you what attributes you need to write.

  • The CompositeParticle class should have an __init__ method that takes in two arguments: name, particles. This method should assign the charge, mass, and spin attributes by calculating the sum of the respective attributes of the elements of particles.

  • In the following cell create a new CompositeParticle called pion from the union of an up quark and a down antiquark. Print the value of the charge of the pion. Note for the physicists: this is not a real pion so don’t expect to obtain the correct spin.

  • Copy the above docstring into your code.

# DEFINE YOUR COMPOSITEPARTICLE CLASS HERE
# Create two particles
up_quark = Fermion(name = "up_quark", charge = 2./3., mass = 2.2,  spin = 1./2.)
down_antiquark = Fermion(name = "down_antiquark", charge = 1./3., mass = 4.7, spin = 0.5)

# A Pion is composed of an up_quark and a down_quark
pion = ...
# Compare
print("{:=^60}".format(" Pion "))
print(f"Charge = {????} e")
  Cell In[18], line 9
    print(f"Charge = {????} e")
     ^
SyntaxError: f-string: invalid syntax

🛑 STOP#

Take a moment to save your notebook


Part 3: Creating an ABM universe (26 points)#

Let’s create our universe. The next cell defines a Universe class which will be our board on which particles exist and, not surprisingly, it is designed to implement a simple ABM of the universe. Read through the code very carefully and try to understand what it does.

✅ Question 3.1 (11 points): The Universe class is lacking some information.

  1. Write docstrings for the methods.

  2. Complete the evolve method. It has a docstring that explains what it should do.

  3. Answer the questions in the check_interaction method.

  4. What are the Agents and the Rules in this ABM simulation?

class Universe:
    """Universe class, Universe class, Size of the entire universe, class."""
    
    def __init__(self, width, height):
        """Create a 2D finite universe of size width x height.

        Parameters
        ----------
        width: int
            Number of imaginary pixel of the universe.

        height: int
            Number of imaginary pixel of the universe.
        """

        self.width = width
        self.height = height

        # Create empty lists for the fermions and bosons.
        self.fermions = []
        self.bosons = []
        self._all_particles = None

    def evolve(self):
        """
        Evolve the universe one step at a time.
        Move the particles, check that they didn't leave the universe,
        and check if there is any annihilation/creation of particles.

        """
        # PUT YOUR CODE HERE
        # HINT: You should be able to do this in 3 lines of code, given all the functions below here.....
  
    def move_particles(self):
        """I NEED A DOCSTRING!"""
        for ip in self._all_particles:
            ip.move()

    def boundary_conditions(self):
        """I NEED A DOCSTRING!"""
        for ip in self._all_particles:
            if ip.x > self.width:
                ip.x -= self.width

            if ip.y > self.height:
                ip.y -= self.height

            if ip.x < 0:
                ip.x += self.width

            if ip.y < 0:
                ip.y += self.height

    def draw(self):
        """I NEED A DOCSTRING!"""
        for ip in self.fermions:
            plt.scatter(ip.x, ip.y, s = 50, marker="o", c = "#8dd3c7")

        for ip in self.bosons:
            plt.scatter(ip.x, ip.y, s = 100, marker=r"$\sim$",  c = "#feffb3")

    def check_interaction(self):
        """I NEED A DOCSTRING!"""

        for ip1, p1 in enumerate(self.fermions):
            for ip2, p2 in enumerate(self.fermions[ip1:]):
                if p1.x == p2.x and p1.y == p2.y and p1.is_antiparticle(p2):
                    # QUESTION uni.1:  What do these pop do?
                    self.fermions.pop(ip1)
                    self.fermions.pop(ip2 - 1) # QUESTION uni.2: Why is there a -1 here?
                    self.bosons.append(Boson(charge=0.0, mass=0.0,spin=1.0) )
                    self.bosons[-1].place_at(coord=(p1.x, p1.y))
                    # QUESTION uni.3:  What does this continue do?
                    continue
            # QUESTION uni.4:  What does this continue do?
            continue

        # update the list
        self._all_particles = [*self.fermions, *self.bosons]

Do This - Erase the content of this cell and replace it with your answers to questions 3 and 4 above

Do This: Run the following cell to initialize our universe

import matplotlib.pyplot as plt
%matplotlib inline
from IPython.display import display, clear_output

my_uni = Universe(width = 150 , height = 150)

np.random.seed(48824)

# Create the universe with a list of particles
for p in range(100): #<--- This is the number of particles you want to create. If you're having issues with very slow code while working, reduce the number of particles, then reset to 100 when you're ready to run the full notebook. 
    my_uni.fermions.append(
        Fermion( 
            name = f"electron_{p}",
            charge = 1.0,
            mass = 0.5,
            spin = 0.5
        )
    )
    my_uni.fermions.append(
        Fermion( 
            name = f"positron_{p}",
            charge = -1.0,
            mass = 0.5,
            spin = 0.5
        )
    )
    my_uni.bosons.append(
        Boson( 
            name = f"photon_{p}",
            mass = 0., 
            charge = 0.0, 
            spin = 1.0
        )
    )

# Group both sets of particles in one list. You will see later how this is useful.
my_uni._all_particles = [*my_uni.fermions, *my_uni.bosons]

print("Done")
Done

✅ Question 4.2 (6 points): We need to initialize the position of each particle. In the next cell randomly place each particle inside the confines of my_uni, and use the function from the class to make a new plot of the universe with the particles.

# PUT YOUR CODE HERE

✅ Question 3.1 (9 points): Let’s run our ABM simulation. The following cell though will throw an error. Fix the error and then print the number of fermion annihilated and the number of boson created at each timestep.

import time 
fig, ax = plt.subplots(figsize = (5,5))

start = time.time()
times = []

for i in range(20):
    
    # RUN YOUR ABM
    my_uni.evolve()

    # # # Animation part: Comment this part if you want it to go faster
    # my_uni.draw()
    # plt.title(f"Time = {i}")
    # clear_output(wait=True) # Clear output for dynamic display
    # display(fig)            # Reset display
    # fig.clear()             # Prevent overlapping and layered plots
    # #
    end = time.time()
    times.append(end-start)
    
print("Total time =", end-start)    
Total time = 0.04915809631347656
../../../_images/2c2de8053cfa5b1f30b70d19f0f6e063c6cfa375a971b096f0ef30b24a632032.png

✅ Question 4.3.2 (3 points):

  1. How many bosons have been created in total, if any?

  2. How many fermions have been annihilated in total, if any?

  3. What can you do to speed up your simulation? (Beside commenting out the animation part)

Congratulations, you’re done!#

Submit this assignment by uploading it to the course Desire2Learn web page. Go to the “Homework Assignments” folder, find the dropbox link for Homework 5, and upload your notebook there.

© Copyright 2023, Department of Computational Mathematics, Science and Engineering at Michigan State University