Lab Exercise 8: Designing Classes
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.
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.
You may also want to grab the Zelle documentation.
Create a file named physics_objects.py. Your first task
in the lab is to create a class Ball 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.
- mass - give it an initial value of 1.
- radius - give it an initial value of 1.
- 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.
- velocity - a two-element list, with initial values of 0.
- acceleration - a two-element list, with initial values of 0.
- force - a two-element list, with initial values of 0.
- 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 transform a position in "physics"s; 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 multipled by the scale field. The y position needs adjustment because the screen coordinates are different than the physics coordinates. In the physics coordinates a move in the positive y direction is "up", but on the screen coordinates a move in the positive y direction is "down" (the upper left corner of the window is at position (0,0)). This means, that whenever we translate from physics coordinates to screen coordinate, we need to subtract the physics y position from the window's height. This means you should enter win.getHeight() minus the Ball's position y value multipled 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. This statement 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*self.scale, win.getHeight()-self.pos*self.scale), self.radius * self.scale ) ]
Make a method draw.
The method should loop over the self.vis list and have each item call its draw method, passing in the window (self.win) field.
Create get and set 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 without modification.
- def getPosition(self): # returns a 2-element tuple with the x, y position.
- def setPosition(self, p): # p is a 2-element list with the new x,y values
- def getVelocity(self): # returns a 2-element tuple with the x and y velocities.
- def setVelocity(self, v): # v is a 2-element list with the new x and y velocities
- def getAcceleration(self): # returns a 2-element tuple with the x and y acceleration values.
- def setAcceleration(self, a): # a is a 2-element list with the new x and y accelerations.
- def getForce(self): # returns a 2-element tuple with the current x and y force values.
- def setForce(self, f): # f is a 2-element list with the new x and y force values.
- def getMass(self): # Returns the mass of the object as a scalar value
- def setMass(self, m): # m is the new mass of the object
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, determine how far the circle needs to move (in physics coordinates), then use the scale factor to determine how far to move in screen coordinates. Finally, loop over self.vis (a list of Zelle graphics objects), and move each object.
def setPosition(self, p): # assign to dx_m the difference between the x coordinate of p and the x coordinate of self.pos # assign to dy_m the difference between the y coordinate of p and the y coordinate of self.pos # assign to the x-coordinate of self.pos the x coordinate of p # assign to the y-coordinate of self.pos the y coordinate of p # assign to dx the product of dx_m and self.scale # assign to dy the product of -dy_m and self.scale # for each item in the vis field of self # 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.
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 time
step, dt that indicates how much time to model. The
algorithm is as follows.
def update(self, dt): # update the x position by adding the x velocity times dt to its old value # update the y position by adding the y velocity times dt to its old value # assign to dx the x velocity times dt times the scale factor (self.scale) # assign to dy the negative of y velocity times dt times the scale factor (self.scale) # for each item in self.vis # call the move method of the graphics object with dx and dy as arguments.. # update the x velocity by adding the acceleration times dt to its old value # update the y velocity by adding the acceleration times dt to its old value # 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. # Multiply the x velocity by 0.998 (e.g. *= 0.998) # Multiply the y velocity by 0.998 (e.g. *= 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 the testBall main function. It should implement Brownian motion of the ball.
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, 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 begin a while loop while win.checkMouse is
equal to None. Inside the loop it should call the ball's update
method with a time constant of 0.1. Then it should get the position
of the ball. If the y position of the ball is less than 0, it should
set the ball's velocity back to zero, set its x position to some
random value between 0 and 50, and set its y position to some random
value between 40 and 50. The penultimate step in the loop should be
to call win.update() so that any changes in the ball's
position will be reflected in the window. The final step in the loop
should be to call the time.sleep function with 0.075 as
the argument. You can adjust that value to modify the speed of your
simulation. After the loop, you can put a win.close() call.
Test your code and make sure you have a ball falling down, then re-spawning near the top of the window.
When you are done with the lab exercises, you may begin the project.