Wot I dun: standing waves on a string project

Wot I’m doing

I was originally inspired to try Pygame for physics visualisations by Peter Collingridge’s excellent physics tutorials which I worked through a while ago to start getting the idea of Python. I’m not too sure it’s the best tool for producing the kind of thing I want (I will definitely be considering redoing this particular project as an interactive web visualisation in d3.js), but it’s been good for learning the language.

My original idea for a Pygame project was to produce some demonstrations relating to weak measurements in quantum mechanics, a subject I’m currently trying to learn more about. Despite the rather dry and factual name, the theory of weak measurement has led to all kinds of strange experiments that are bizarre even by quantum standards (e.g. the recent Quantum Cheshire Cat experiment). In particular, I wanted to create my own version of the model in this master’s project paper. I have a few notes on this, but mainly I just found myself going down the rabbit hole of trying to understand weak measurement and not doing much coding.

Then I needed to earn some more money and got a temp job for a couple of months, scanning and sorting post in a law firm, so I decided to give weak measurement a rest for a bit and pick a relatively straightforward project that I could work on half asleep in the library some evenings. After all, I wanted to be able to say I done something with my week other than scanning post. I still wanted something physics-y, so decided to make my own version of this nice Java applet I’d come across demonstrating resonance on a string as the frequency of vibration changes.

The maths is in a pdf here — the main reason I picked this project is that I hadn’t actually seen this exact problem solved before, with the oscillating boundary for the string. I started by just using Pygame to model the function given in the pdf (which is pretty terse) and will get round to deriving the maths for myself later.

Contents:

Monday 18th August

First I found a pygame project that just plots an animated sine wave running, so that I had something to build on. This took a while as I only had my netbook on me, which was missing pygame and some other stuff… but I got it working.

Then I mucked about for a while, trying to implement the function inside the sum in the equation for y here. I don’t care about damping so I set b=0 , i.e. I tried to plot the function f(x, t) = \frac{k^2}{n}\frac{1}{k_n^2-k^2}\cos\omega t (I also relabelled z as x because I preferred that).

It didn’t quite work, but that was enough for one evening.

Tuesday 19th August

I found my silly errors from the previous day and got going. I ended up with this function definition for y(x,t) :

<br /># Define standing wave function

amplitude = 50 # in px
speed = 1

def k(n):
return n*math.pi/canvas_width

def ysummand(x, n, frequency):
return (frequency**2/(k(n)**2-frequency**2))*math.sin(k(n)*(float(x)))*math.cos(frequency*(speed*time.time()))

def ysum(x, frequency):
for i in range (1, 10):
sum = 0
sum = sum + ysummand(x, i, frequency)
return sum

# some more irrelevant stuff...

for x in range(0, canvas_width):
y = int((canvas_height/2) + amplitude*ysum(x, 12))
surface.set_at((x, y), color)

Seems to work nicely, as far as I can tell by just playing with different numbers for the frequency! Will be able to test it better once I’ve implemented some kind of interactive way to change the frequency… that is definitely a problem for another day when I’m less tired.

Then I remembered I need psi, not y… I started to add this, ending up with:

<br />import pygame
import time
import math

# Some config width height settings
canvas_width = 640
canvas_height = 480

# Just define some colors we can use
color = pygame.Color(255, 255, 0, 0)
background_color = pygame.Color(0, 0, 0, 0)

pygame.init()
# Set the window title
pygame.display.set_caption("Standing wave demonstration")

# Make a screen to see
screen = pygame.display.set_mode((canvas_width, canvas_height))
screen.fill(background_color)

# Make a surface to draw on
surface = pygame.Surface((canvas_width, canvas_height))
surface.fill(background_color)

# Define standing wave function

amplitude = 50 # in px
speed = 1

def k(n):
return n*math.pi/canvas_width

def ysummand(x, n, frequency):
return (frequency**2/(k(n)**2-frequency**2))*math.sin(k(n)*(float(x)))*math.cos(frequency*(speed*time.time()))

def ysum(x, frequency):
for i in range (1, 10):
sum = 0
sum = sum + ysummand(x, i, frequency)
return sum

def y(x, frequency):
return int((2/math.pi)*amplitude*ysum(x, frequency))

# y = int((canvas_height/2) + amplitude*math.sin(frequency*((float(x)/canvas_width)*(2*math.pi) + (speed*time.time()))))

# Simple main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Redraw the background
surface.fill(background_color)

for x in range(0, canvas_width):
psi = int((canvas_height/2) + y(x, 3) + (1 - (x/canvas_width))*amplitude*math.cos(3*(speed*time.time())))
surface.set_at((x, psi), color)

# Put the surface we draw on, onto the screen
screen.blit(surface, (0, 0))

# Show it.
pygame.display.flip()

pygame.quit ()

This is definitely broken (the right end isn’t fixed and should be), but I am tired and hungry so will leave it there for now.

Thursday 21st August

Tidied the code up a bit and got it working in the process (as far as I can see – the right end is now fixed). Here it is:

<br />import pygame
import time
import math

# Size of window
canvas_width = 640
canvas_height = 480

# Define colours
color = pygame.Color(255, 255, 0, 0)
background_color = pygame.Color(0, 0, 0, 0)

pygame.init()
# Set the window title
pygame.display.set_caption("Standing wave demonstration")

# Make a screen to see
screen = pygame.display.set_mode((canvas_width, canvas_height))
screen.fill(background_color)

# Make a surface to draw on
surface = pygame.Surface((canvas_width, canvas_height))
surface.fill(background_color)

# Define standing wave function

amplitude = 50 # in px
speed = 1

def k(n):
return n*math.pi/canvas_width

def ysummand(x, n, frequency):
return (frequency**2/(k(n)**2-frequency**2))*math.sin(k(n)*(float(x)))*math.cos(frequency*(speed*time.time()))

def ysum(x, frequency):
for i in range (1, 10):
sum = 0
sum = sum + ysummand(x, i, frequency)
return sum

def y(x, frequency):
return (2/math.pi)*amplitude*ysum(x, frequency)

def psi(x, frequency):
return y(x, frequency) + amplitude*(1 - (float(x)/canvas_width))*math.cos(frequency*(speed*time.time()))

# Main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Redraw background
surface.fill(background_color)

for xrange in range(0, canvas_width):
yrange = int((canvas_height/2) + psi(xrange, 23))
surface.set_at((xrange, yrange), color)

# Add surface to screen
screen.blit(surface, (0, 0))

# Display
pygame.display.fliphttp://www.pygame.org/docs/ref/draw.html()

pygame.quit ()

Sunday 24th August

Now the maths was sorted, I wanted to make the animation look prettier. This involved changing the colours (easy) and the thickness of the curve (not quite so easy, as the very simple code I followed worked by changing the colour of single pixels). Time to use the draw function, following this Pygame drawing tutorial to rewrite code.

First I made a simple sine curve (drawsin.py):

<br /># Draw sine curve with pygame

import pygame
import time
import math

# Window size
canvas_width = 640
canvas_height = 480

# Define colours
line_colour = pygame.Color(55, 115, 212, 0)
background_colour = pygame.Color(255, 255, 255, 0)

# Make screen
screen = pygame.display.set_mode((canvas_width, canvas_height))
screen.fill(background_colour)

# Sine curve properties:
amplitude = 80

xpoints = [i for i in range(canvas_width)]
ypoints = []
for x in xpoints:
ypoints.append(int((canvas_height/2) + amplitude*math.sin((float(x)/canvas_width)*2*math.pi)))

coords = []
for i in range(canvas_width):
coords.append([xpoints[i], ypoints[i]])

print ypoints[1:10]

# Main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Draw sine curve

pygame.draw.lines(screen, line_colour, False, coords, 3)

pygame.display.flip()

pygame.quit()

Then I added this to my file (standing_wave_2.py):

<br />## Standing wave demonstration with pygame

import pygame
import time
import math

# Window size
canvas_width = 640
canvas_height = 480

# Define colours
line_colour = pygame.Color(55, 115, 212, 0)
background_colour = pygame.Color(255, 255, 255, 0)

# Make screen
screen = pygame.display.set_mode((canvas_width, canvas_height))
screen.fill(background_colour)

# Make drawing surface
surface = pygame.Surface((canvas_width, canvas_height))
surface.fill(background_colour)

# Define standing wave function

amplitude = 50 # in px
speed = 1

def k(n):
return n*math.pi/canvas_width

def ysummand(x, n, frequency):
return (frequency**2/(k(n)**2-frequency**2))*math.sin(k(n)*(float(x)))*math.cos(frequency*(speed*time.time()))

def ysum(x, frequency):
for i in range (1, 10):
sum = 0
sum = sum + ysummand(x, i, frequency)
return sum

def y(x, frequency):
return (2/math.pi)*amplitude*ysum(x, frequency)

def psi(x, frequency):
return y(x, frequency) + amplitude*(1 - (float(x)/canvas_width))*math.cos(frequency*(speed*time.time()))

# Main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Redraw background
surface.fill(background_colour)

xpoints = [i for i in range(canvas_width)]
ypoints = []
for x in xpoints:
ypoints.append(int((canvas_height/2) + psi(x,32)))

coords = []
for i in range(len(xpoints)):
coords.append([xpoints[i], ypoints[i]])

# Draw curve
pygame.draw.lines(surface, line_colour, False, coords, 3)

# Add surface to screen
screen.blit(surface, (0, 0))

pygame.display.flip()

pygame.quit()

It works fine, but it’s kind of slow… after a bit of searching around I decided to go back to the pixel-updating method of the example I followed. Here’s the code (standing_wave_4.py):

<br />## Standing wave demonstration with pygame

import pygame
import time
import math

# Window size
canvas_width = 640
canvas_height = 480

# Define colours
line_colour = pygame.Color(55, 115, 212, 0)
background_colour = pygame.Color(255, 255, 255, 0)

# Make screen
screen = pygame.display.set_mode((canvas_width, canvas_height))
screen.fill(background_colour)

# Make drawing surface
surface = pygame.Surface((canvas_width, canvas_height)).convert()
surface.fill(background_colour)

# Define standing wave function

amplitude = 50 # in px
frequency = 45

def k(n):
return n*math.pi/canvas_width

def ysummand(x, n):
return (frequency**2/(k(n)**2-frequency**2))*math.sin(k(n)*(float(x)))

def ysum(x):
for i in range (1, 10):
sum = 0
sum = sum + ysummand(x, i)
return sum

def y(x):
return (2/math.pi)*amplitude*ysum(x)

def psi(x):
return y(x) + amplitude*(1 - (float(x)/canvas_width))

#pygame.init()

# Main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Redraw background
surface.fill(background_colour)

for xrange in range(0, canvas_width):
yrange = int((canvas_height/2) + psi(xrange)*math.cos(frequency*time.time()))
for i in range(0, 4):
surface.set_at((xrange, yrange + i), line_colour)

# Add surface to screen
screen.blit(surface, (0, 0))

# Display
pygame.display.flip()

pygame.quit()

It worked OK, so my next plan was to add some interactivity.

Tuesday 26th August

I added some very crude interactivity, using left and right arrows to control frequency (using this example as a guide), plus some text at the top of the screen displaying the current frequency (based on this example).

The full code is (standing_wave_5.py):

<br />## Standing wave demonstration with pygame

import pygame
import time
import math

# Window size
canvas_width = 640
canvas_height = 480

# Define colours
line_colour = pygame.Color(55, 115, 212, 0)
background_colour = pygame.Color(255, 255, 255, 0)
slider_colour = pygame.Color(0, 0, 0, 0)

# Make screen
screen = pygame.display.set_mode((canvas_width, canvas_height))
screen.fill(background_colour)

# Make drawing surface
surface = pygame.Surface((canvas_width, canvas_height)).convert()
surface.fill(background_colour)

# Define standing wave function

amplitude = 80 # in px
frequency = 200
speed = 0.1

def k(n):
return n*math.pi/canvas_width

def ysummand(x, n):
return (frequency**2/(k(n)**2-frequency**2))*math.sin(k(n)*(float(x)))

def ysum(x):
for i in range (1, 10):
sum = 0
sum = sum + ysummand(x, i)
return sum

def y(x):
return (2/math.pi)*amplitude*ysum(x)

def psi(x):
return y(x) + amplitude*(1 - (float(x)/canvas_width))

pygame.init()

# Main loop
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False

# Redraw background
surface.fill(background_colour)

for xrange in range(0, canvas_width):
yrange = int((canvas_height/2) + psi(xrange)*math.cos(speed*frequency*time.time()))
for i in range(0, 4):
surface.set_at((xrange, yrange + i), line_colour)

# Display some text
font = pygame.font.Font(None, 36)
text = font.render("Frequency is "+str(abs(frequency)), 1, (10, 10, 10))
textpos = text.get_rect()
textpos.centerx = surface.get_rect().centerx
surface.blit(text, textpos)

# Add surface to screen
screen.blit(surface, (0, 0))

# Display
pygame.display.flip()

move_ticker = 0
keys=pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
if move_ticker == 0:
move_ticker = 10
frequency -= 0.1
if keys[pygame.K_RIGHT]:
if move_ticker == 0:
move_ticker = 10
frequency += 0.1

pygame.quit()

Took a screenshot (screenshot1)
This could do with some improvements:

  • It would be worth making it easier to change the frequency a large amount… maybe with an editable text box
  • At the moment when I change the frequency the curve is ‘resetting’ somehow so that the movement is not smooth. Definitely need to fix this one!

Thursday 28 August

Got out a pen and paper and actually started working through the maths. Will write this up some other time when it’s finished.

Monday 1 September

Wrote up some of this post and started thinking about how to change the frequency… a slider might be good. I played for a bit with a GUI library for Pygame, PGU.

Tuesday 2 September

Bit more playing with the GUI library, went home early as getting a cold. Not really sure this is the way I want to go – I think I will keep it simple for now and consider interactive options if I decide to implement this in d3.js instead of Python.

Sunday 7th September

Oh shit! I realised the code was wrong — the curve wasn’t changing shape with increasing frequency. I didn’t remember it always being wrong — had I broken it? I found one problem: ysummand was not summing correctly. I fixed that, but it was still wrong.

I decided to reproduce \psi as a simple animation in Mathematica, a program where I actually know what I’m doing. I soon convinced myself the solution was correct — just not my implementation!

Monday 8th September

I found the problem after testing various things — it’s something to do with integers and floats. Currently if the frequency increment is a float it breaks, if it is an integer it doesn’t. I must have had everything as integers to start with, which is why I didn’t spot the problem. I can also break it by defining the amplitude and speed as floats. Need to go through carefully and work out what needs to be what… tomorrow!

Tuesday 9th September

GAH I STILL CAN’T FIX IT. 😦

Monday 15th September

I’m more confused than before. I fixed one small problem and the code is now consistent whether I use floats or integers (i.e. values of psi are the same whether the frequency is 3 or 3.0). But it still doesn’t work, and moreover, psi barely changes with frequency.

I think the best option will be to play with the Mathematica animation for a bit with different parameter ranges.

Wednesday 17th September

Quick play around in Mathematica. Realised that the code is working but nothing much is really happening for the parameter range I chose — the interesting stuff is going on at much lower frequencies!

This actually makes sense. The equation for psi is going to produce sensible results if parameters like the length of the string around unity, but I have a canvas length of 640 pixels. So it was never going to be a great success!

Thursday 18th September

Picked some parameters that make the interactivity nice — the waves on the string change at a sensible rate as you adjust the frequency, not too rapidly or too irritatingly slowly.

The frequencies are currently annoyingly small numbers — sure I can scale things better when my brain is working. But anyway, here is the current version:

<br />## Standing wave demonstration with pygame

import pygame
import time
import math

# Window size
canvas_width = 640
canvas_height = 480

# Define colours
line_colour = pygame.Color(55, 115, 212, 0)
background_colour = pygame.Color(255, 255, 255, 0)
slider_colour = pygame.Color(0, 0, 0, 0)


# Make screen
screen = pygame.display.set_mode((canvas_width, canvas_height))
screen.fill(background_colour)

# Make drawing surface
surface = pygame.Surface((canvas_width, canvas_height)).convert()
surface.fill(background_colour)

# Define standing wave function

amplitude = 3 # in px
speed = 1

def k(n):
    return n*math.pi/canvas_width

def ysummand(x, n, frequency):
    return ((float(frequency)/speed)**2/n)*(1/(k(n)**2-(float(frequency)/speed)**2))*math.sin(k(n)*(float(x)))

def ysum(x, Nmax, frequency):
    sumList = [0]*Nmax
    for i in range(1, Nmax):
        sumList[i] = ysummand(x, i, frequency)

    return sum(sumList)

def y(x, frequency):
    return (2/math.pi)*amplitude*ysum(x, 10, frequency)

def psi(x, frequency):
    return y(x, frequency) + amplitude*(1 - (float(x)/float(canvas_width)))




pygame.init()

# Main loop
running = True

freq = 0.01 # set initial frequency

while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    # Redraw background
    surface.fill(background_colour)

    for xrange in range(0, canvas_width):
    yrange = int((canvas_height/2) + psi(xrange, freq)*math.cos(1000*speed*freq*time.time()))
    for i in range(0, 4):
        surface.set_at((xrange, yrange + i), line_colour)

    # Display some text
    font = pygame.font.Font(None, 36)
    text = font.render("Frequency is "+str(abs(freq)), 1, (10, 10, 10))
    textpos = text.get_rect()
    textpos.centerx = surface.get_rect().centerx
    surface.blit(text, textpos)

    move_ticker = 0
    keys=pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        if move_ticker == 0:
            move_ticker = 20
            freq -= 0.0001
    if keys[pygame.K_RIGHT]:
        if move_ticker == 0:   
            move_ticker = 20     
            freq += 0.0001




    # Add surface to screen
    screen.blit(surface, (0, 0))

    # Display 
    pygame.display.flip()



pygame.quit()


Things I could/should still do include:

  • Adding a slider… or just moving straight to d3.js and trying to produce an interactive web page version
  • Scaling the equations nicely
  • (related) Working through the maths and understanding it properly.

But right now it’s time to give this a rest for a week or two and switch focus. I have exciting paper corrections to make!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s