2D Mass Spring System Implementation

Simulation and Modeling (CSCI 3010U)

Faisal Qureshi

Faculty of Science, Ontario Tech University

http://vclab.science.ontariotechu.ca

Discuss in class


Consider a mass-spring system that lives in a flat (2D) world. One end of the spring is connected to a fixed hinge sitting at location \((0,0)\). The other end of the spring is connected to a mass \(m\). The rest length of this spring is \(l\), and the Hooke’s coefficient is \(k\). Provide the Simulation class that simulates this mass spring system.

2d mass spring system

The following files are provided for you, which will use the Simulation class provided by you. It assumes that your class is available in file sim.py.

util.py

Includes routines used by mass-spring-2d.py file.

"""
author: Faisal Z. Qureshi
email: faisal.qureshi@uoit.ca
website: http://www.vclab.ca
license: BSD
"""

import pygame

# set up the colors
BLACK = (0, 0, 0, 255)
WHITE = (255, 255, 255, 0)
RED = (255, 0, 0, 255)
GREEN = (0, 255, 0, 255)
BLUE = (0, 0, 255, 255)

def load_image(name):
    image = pygame.image.load(name)
    return image

class MyCircle(pygame.sprite.Sprite):
    def __init__(self, color, width, height, alpha=255):
        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.Surface([width, height], flags=pygame.SRCALPHA)
        self.rect = self.image.get_rect()
        cx = self.rect.centerx
        cy = self.rect.centery
        pygame.draw.circle(self.image, color, (width/2, height/2), cx, cy)
#        self.rect = self.image.get_rect()

        self.picked = False

    def set_pos(self, pos):
        self.rect.x = pos[0] - self.rect.width//2
        self.rect.y = pos[1] - self.rect.height//2

    def update(self):
        pass

class MyRect(pygame.sprite.Sprite):
    def __init__(self, color, width, height, alpha=255):
        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.Surface([width, height], flags=pygame.SRCALPHA)
        self.rect = self.image.get_rect()
        pygame.draw.rect(self.image, color, self.rect)

        self.picked = False

    def set_pos(self, pos):
        self.rect.x = pos[0] - self.rect.width//2
        self.rect.y = pos[1] - self.rect.height//2

    def update(self):
        pass

def to_screen(x, y, win_width, win_height):
    return win_width//2 + x, win_height//2 - y

def from_screen(x, y, win_width, win_height):
    return x - win_width//2, win_height//2 - y

class MyText():
    def __init__(self, color, background=WHITE, antialias=True, fontname="comicsansms", fontsize=16):
        pygame.font.init()
        self.font = pygame.font.SysFont(fontname, fontsize)
        self.color = color
        self.background = background
        self.antialias = antialias
    
    def draw(self, str1, screen, pos):
        text = self.font.render(str1, self.antialias, self.color, self.background)
        screen.blit(text, pos)

mass-spring-2d.py

Calls your Simulation class to do the heaving lifting.

"""
author: Faisal Z. Qureshi
email: faisal.qureshi@uoit.ca
website: http://www.vclab.ca
license: BSD
"""

import pygame, sys
import matplotlib.pyplot as plt
import numpy as np

# import sim_rk4 as Simulation
import sim as Simulation
import util


def main():
    # sim title
    title = 'Mass-Spring System'

    # initializing pygame
    pygame.init()

    # clock object that ensure that animation has the same
    # on all machines, regardless of the actual machine speed.
    clock = pygame.time.Clock()

    # fonts
    text = util.MyText(util.BLACK)

    # setting up a sprite group, which will be drawn on the
    # screen
    ball = util.MyRect(color=util.BLUE, width=32, height=32)
    center = util.MyRect(color=util.RED, width=4, height=4)
    x_axis = util.MyRect(color=util.BLACK, width=620, height=1)
    y_axis = util.MyRect(color=util.BLACK, width=1, height=460)
    my_group = pygame.sprite.Group([x_axis, y_axis, ball, center])

    # set up drawing canvas
    # top left corner is (0,0) top right (640,0) bottom left (0,480)
    # and bottom right is (640,480).
    win_width = 640
    win_height = 480
    screen = pygame.display.set_mode((win_width, win_height))
    pygame.display.set_caption(title)

    # setting up simulation
    sim = Simulation.Simulation(title)
    # sim.init(state=np.array([200,200,0,0], dtype='float32'), mass=100., k=.01, l=200.) Try some other values
    sim.init(state=np.array([200,200,0,0], dtype='float32'), mass=10., k=10, l=200.)
    sim.set_time(0.0)
    sim.set_dt(0.1)

    print ('--------------------------------')
    print ('Usage:')
    print ('Press (r) to start/resume simulation')
    print ('Press (p) to pause simulation')
    print ('Press (q) to quit')
    print ('Press (space) to step forward simulation when paused')
    print ('Use mouse left button down to move mass around (only when simulation paused)')
    print ('--------------------------------')

    # Transformation to screen coordinates
    # Here 0,0 refers to simulation coordinates
    center.set_pos(util.to_screen(0, 0, win_width, win_height))
    x_axis.set_pos(util.to_screen(0, 0, win_width, win_height))
    y_axis.set_pos(util.to_screen(0, 0, win_width, win_height))


    while True:
        # 30 fps
        clock.tick(30)

        # update sprite x, y position using values
        # returned from the simulation
        ball.set_pos(util.to_screen(sim.state[0], sim.state[1], win_width, win_height))

        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit(0)

        if event.type == pygame.KEYDOWN and event.key == pygame.K_p:
            sim.pause()
            continue
        elif event.type == pygame.KEYDOWN and event.key == pygame.K_r:
            if not ball.picked:
                sim.resume()
            continue
        elif event.type == pygame.KEYDOWN and event.key == pygame.K_q:
            break
        elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1: # LEFT=1
            if sim.paused:
                if ball.rect.collidepoint(event.pos):
                    ball.picked = True
        elif event.type == pygame.MOUSEMOTION:
            if ball.picked:
                x, y = util.from_screen(event.pos[0], event.pos[1], win_width, win_height)
                sim.set_state(np.array([x, y, 0, 0], dtype='float32'))
        elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
            if ball.picked:
                ball.picked = False
                sim.set_state(np.array([x, y, 0, 0], dtype='float32'))
        else:
            pass

        # clear the background, and draw the sprites
        screen.fill(util.WHITE)
        my_group.update()
        my_group.draw(screen)
        text.draw("Time = %f" % sim.cur_time, screen, (10,10))
        text.draw("x = %f" % sim.state[0], screen, (10,40))
        text.draw("y = %f" % sim.state[1], screen, (10,70))
        if ball.picked:
            text.draw("Picked. (Simulation disabled)", screen, (10,100))
        pygame.display.flip()

        # update simulation
        if not sim.paused:
            sim.step()
        elif not ball.picked and event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                sim.step()
        else:
            pass
    
    pygame.quit()
    sys.exit(0)

if __name__ == '__main__':
    main()

sim.py

Starter code for your simulation class.

"""
author: Faisal Qureshi
email: faisal.qureshi@uoit.ca
website: http://www.vclab.ca
license: BSD
"""

import numpy as np

class Simulation:
    def __init__(self, title):
        self.paused = True # starting in paused mode
        self.title = title
        self.cur_time = 0
        self.dt = 0.033 # 33 millisecond, which corresponds to 30 fps
        # Fix this

    def init(self, state, mass, k, l):
        # Fix this
        pass

    def set_state(self, state):
        # Fix this
        pass

    def set_time(self, cur_time=0):
        self.cur_time = cur_time

    def set_dt(self, dt=0.033):
        self.dt = dt

    def step(self):
        # Fix this 
        pass

    def pause(self):
        self.paused = True

    def resume(self):
        self.paused = False

    def save(self, filename):
        # Ignore this
        pass

    def load(self, filename):
        # Ignore this
        pass

Code execution

We will run your code as follows:

$ python mass-spring-2d.py

Submission

Nothing to submit. Please show your work to the instructor. Due in class.