Objectives

This project continues our work with classes, but we're moving into the domain of physics simulations instead of biological simulations.

This is the first part of a multi-part project where we will look at increasingly complex physical simulations. This week we start with a ball and some walls, focusing on gravity, elasticity, and ball-wall collisions.

Tasks

Setting Up

If you have not already done so, mount your personal space, create a new project8 folder and bring up TextWrangler and a Terminal. Make a new Python file called physics_objects.py. Then download the Zelle graphics package file graphics.py and import that package for use in your code in physics_objects.py.

You may also want to grab the Zelle documentation.

Ball Class

Your first task in the lab is to create a class Ball (in physics_objects.py) that will represent a ball in your simulation. Since this is a physics simulation, you will want to represent all of the relevant attributes such as mass, position, velocity, acceleration, and any forces acting on the ball.

The __init__() method should have self and a GraphWin object (win) as its only non-optional argument. You are free to add optional arguments, but none are required. Inside the __init__() method, create fields to hold each of the following pieces of information.

  1. mass - give it an initial value of 1.
  2. radius - give it an initial value of 1.
  3. position - this will be a two-element list, representing the x and y location values. Give the x and y positions initial values of 0.
  4. velocity - a two-element list, representing velocities in x, y directions. Both initial values are 0.
  5. acceleration - a two-element list, representing accelerations in x, y directions. Both initial values are 0.
  6. force - a two-element list, representing force in x, y directions. Both initial values are 0.
  7. win - a GraphWin object.

In addition to the physical parameters, each Ball object needs to know how to draw itself to a visualization window. We want to be able to use different units for the physical parameters versus the visualization parameters, so add a scale field and give it an initial value of 10. The scale will be part of transforming a position in "physics" coordinates to screen coordinates.

We also need to add the graphics object that will represent the ball on the screen. Create a field vis and assign it a list with one element. That element should be a Circle object, which needs an anchor point for its center and then a radius. For the anchor point x and y values, use the Ball's position x value multiplied by the scale field. The y position needs more adjustment, however, because the screen coordinates are different than the physics coordinates.

In the physics coordinates, the origin (0,0) represents the bottom left corner of the space, with the positive x direction going to the right and the positive y direction going "up" (as we've seen before, in math classes); in the screen coordinates, however, position (0,0) is in the upper left corner of the window, and the positive x direction is still to the right (just like the physics coordinates), but the positive y direction is now "down" rather than up. This means that whenever we translate from physics coordinates to screen coordinates, we need to subtract the physics y position from the window's height. So, for the y position of the Circle being constructed, you should enter win.getHeight() minus the Ball's position y value multiplied by the scale field.

The final parameter for the Circle-creation function is the radius -- give it the Ball's radius value multiplied by the scale field. Overall, then, the statement initializing the vis field might look like the following, depending on what you named your fields and how you imported the graphics module.

self.vis = [ gr.Circle( gr.Point(self.pos[0]*self.scale, win.getHeight()-self.pos[1]*self.scale), self.radius * self.scale ) ]

draw( )

Make a method draw().

def draw(self):

The method should loop over the self.vis list and have each item call its draw method, passing in the window (self.win) field.

Getters & Setters

Create "getter" and "setter" methods for each of the physical attributes of the Ball. For example, the getMass() method should return the value of the mass field of the object. The setMass() method should take in a new value as one of the parameters and assign it to the mass field of the object.

Use the following definitions for your get/set methods. You must follow these specifications or the test files will not work.

  1. def getMass(self): # Returns the mass of the object as a scalar value
  2. def setMass(self, m): # m is the new mass of the object
  3. def getRadius(self): # Returns the radius of the object as a scalar value
  4. def setRadius(self, m): # r is the new radius of the object
  5. def getPosition(self): # returns a 2-element tuple with the x, y position.
  6. def setPosition(self, p): # p is a 2-element list with the new x,y values
  7. def getVelocity(self): # returns a 2-element tuple with the x and y velocities.
  8. def setVelocity(self, v): # v is a 2-element list with the new x and y velocities
  9. def getAcceleration(self): # returns a 2-element tuple with the x and y acceleration values.
  10. def setAcceleration(self, a): # a is a 2-element list with the new x and y accelerations.
  11. def getForce(self): # returns a 2-element tuple with the current x and y force values.
  12. def setForce(self, f): # f is a 2-element list with the new x and y force values.

The only tricky method is setPosition(), which requires you to not only update the position field but also to move the visualization to the appropriate location. To implement the move, loop over self.vis (a list of Zelle graphics objects), use the getCenter() method to get the center point of the Circle object, then use the move() method to place it in the right location.

If the circle is in location A and you want it to be in location B, then moving the object by the amount (B - A) does what you want to do. In this case, B is the self.scale field multiplied by the new location. For the x-position, A is the location returned by getCenter(). For the y-position, A is the self.win.getHeight() minus the y-location returned by getCenter(). (Note: A quick look at the code in the graphics package shows that getCenter() returns a Point object. What method or methods of a Point object will you use to get data from the return value of getCenter()?)

def setPosition(self, p):
    # assign to the x coordinate in self.pos the x coordinate of p
    # assign to the y coordinate in self.pos the y coordinate of p

    # for each item in the vis field of self
        # assign to c the return value calling the getCenter method of the item
        # assign to dx the (product of self.scale and x coordinate of p) minus the X coordinate of c (c.getX())
        # assign to dy the window height minus the (product of self.scale and the y coordinate of p) minus the Y coordinate of c (c.getY())
        # call the move method of the item, passing in dx and dy

Once you have completed this step, download the file testBall_1.py. The test function has code to test some of the get/set functions, but it is not complete. As you write get/set functions, add test code to the function to make sure your functions work properly.

update( )

Write a method, update() that adjusts the internal position and velocity values based on current accelerations and forces. The method also needs to move the visualization.

The function will take in a timestep dt that indicates how much time to model. In addition, the function will use a boolean flag (the colliding parameter) to help ensure that the ball won't fall through the floor--using it, we can make sure the ball comes to rest during a collision and no further acceleration is applied to it. The algorithm is as follows.

def update( self, dt, colliding=False ):
    """Computes the ball's next state and updates the ball
    visualization on the screen. If colliding is True, then
    acceleration will not be applied."""

    # Turn off acceleration during collisions to keep ball
    # from sinking through the floor:
    # if colliding, assign 0 acceleration (in both x and y directions) to variable a
    # else assign the ball's acceleration to variable a

    # How much the ball will move (in physics coordinates) during
    # this timestep, based on its current velocity and acceleration:
    # assign to dx the x velocity times dt plus 1/2 the x acceleration times dt**2 (dt squared)
    # assign to dy the y velocity times dt plus 1/2 the y acceleration times dt**2 (dt squared)

    # Ball's new position in physics coordinates:
    # increment the x position by dx
    # increment the y position by dy

    # Ball visualization's new position in graphics coordinates
    for shape in self.vis:
        shape.move(dx*self.scale, -dy*self.scale)

    # Update velocity:
    # update the x velocity by adding the x acceleration times dt to its old value
    # update the y velocity by adding the y acceleration times dt to its old value

    # An applied force produces a temporary acceleration:
    # update the x velocity by adding dt times the x force divided by the mass to its old value
    # update the y velocity by adding dt times the y force divided by the mass to its old value.

    # Simulate damping by reducing x and y velocities:
    # multiply the x velocity by 0.998
    # multiply the y velocity by 0.998

Note, the last two statements violate a perfect physics world (no friction, air resistance, etc) and bleed a little energy from the ball's motion. It will eventually cause a Ball to come to a halt, in the absence of new forces or accelerations. When you want to test the update method, uncomment the last section of testBall_1.py's main() function. It should implement Brownian motion of the ball.

fall.py

Create a new file fall.py. In it, create a main() function and have it execute when you run the file. Your function should create a GraphWin window (with top right corner at least (50,50) in physics coordinates for the ball's position--the lower left will be (0,0) as usual), create a Ball object, position the object at location [25, 45], set its acceleration to [0, -10], and draw the ball into the window. Then it should execute a while loop while win.checkMouse is equal to None. Inside the loop:

After the loop, you can put a win.close() call.

Note that when creating a GraphWin object, it is a good idea to turn off auto-update by passing the constructor a fourth parameter with the value False.

Test your code and make sure you have a ball falling down and then re-spawning near the top of the window.


When you are done with the lab exercises, you may start on the rest of the project.


© 2018 Eric Aaron (with contributions from Colby CS colleagues).