Thursday, November 19, 2015

The Code of Clocks

One of the exercises at the program I work at is reading and drawing clocks. I figured that would be a great programming exercise. Drawing lines in Pygame would be simple and I'd just put it on a clock background. First I downloaded a picture of a clock and called it "clock2.jpg."

The code for loading a background image and making it transparent is:

# Load and set up graphics.
background_image = pygame.image.load("clock2.jpg").convert()
background_image.set_colorkey(WHITE) #make it transparent

You need to write commands for drawing the hands. In Pygame a line is defined by its endpoints. Naturally the center of the screen is width/2, height/2, but the other end of the hand is a math problem! What would be the best coordinate system to use? It wouldn't be easy to point to a specific point in Cartesian (x- and y-) coordinates but it's not hard to figure out how far to rotate the hand in polar coordinates. See my book Hacking Math Class for how to explore polar coordinates using Python. Here's how to find our point using the length of the hands and the angle of rotation:


The point would be (r*cos(theta), r*sin(theta))
The length of the hand is r. I made all the hands the same length for testing. They'd be different colors, though:

# Define some colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED   = (255, 0, 0)
BLUE = (0, 0, 255)

#clock hands:
hour_length = 290
minute_length = 290
second_length = 290

Unlike the figure above, in a clock face the rotation is, well, clockwise from straight up. The easy solution to that is to switch sine and cosine. Now what's theta? All programming languages measure angles in radians, not degrees. No problem: import pi from the math or numpy module and 2*pi is a whole circle.

from math import pi, sin, cos

Each hour is a twelfth of the whole circle, or 2*pi/12. That means if it's 1 o'clock, the rotation is 1 * 2*pi divided by 12, 2 o'clock would be 2 * 2*pi /12 and so on. That makes the code for our hour hand this:

#draw hour hand
h_theta = hour*2*pi/12

and the Pygame syntax for drawing a line:

h_tip = [300+hour_length*sin(h_theta), 300-hour_length*cos(h_theta)]
pygame.draw.line(screen, BLACK, [300,300], h_tip, 2)

The "300" comes from the fact that the center of my clock is (300,300).

So that draws a perfect hour hand. Here's 1 and 2 o'clock:
hour = 1 or hour = 2

 

Let's add a minute hand. Now the circle (2*pi radians) is cut up into 60 slices, so we'll add that to our code:

#draw minute hand
m_theta = minute*2*pi/60
m_tip = [300+minute_length*sin(m_theta), 300- \ minute_length*cos(m_theta)]
pygame.draw.line(screen, BLUE, [300,300], m_tip, 2)

It'll be just as long as the hour hand, but blue instead of black. Now 1:00 looks like this:
That's correct, but when I put in 1:30 I get an incorrect clock:
hour = 1
minute = 30
The hour hand should be halfway between the 1 and the 2. Every hour the hour hand moves 2 * pi/12 radians but it should move 1/60th of that every minute. I'm changing the hour hand code to this:

h_theta = hour*2*pi/12 + minute*2*pi/(12*60)

That should fix the mistake:

We'll add a similar line for our minute hand to take into account the number of seconds, add a seconds hand and we'll have a three-handed clock.

h_theta = hour*2*pi/12 + minute*2*pi/(12*60)
...
m_theta = minute*2*pi/60 + second*2*pi/(60*60)
...
s_theta = second*2*pi/60

Of course, you need the usual Pygame code to get stuff on the screen. I highly recommend Professor Craven's tutorials, and that's where I got a template for Pygame programs.

Here's the whole code:

import pygame
from pygame.locals import *
from math import pi, sin, cos
from random import randint

# Define some colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED   = (255, 0, 0)
BLUE = (0, 0, 255)

#clock hands:
hour_length = 290
minute_length = 290
second_length = 290

#Change these numbers to show the time:
hour = 1
minute = 45
second = 50

# Create an 600x600 sized screen
screen = pygame.display.set_mode([600, 600])

# This sets the name of the window
pygame.display.set_caption('Clocks!')

# Set positions of graphics
background_position = [0, 0]

# Load and set up graphics.
background_image = pygame.image.load("clock2.jpg").convert()
background_image.set_colorkey(WHITE)

# Call this function so the Pygame library can initialize itself
pygame.init()

clock = pygame.time.Clock()

done = False
#main loop:
while not done:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True
            
    screen.fill(WHITE)
    # Copy image to screen:
    screen.blit(background_image, background_position)

    #draw hour hand
    h_theta = hour*2*pi/12 +minute*2*pi/(12*60)
    h_tip = [300+hour_length*sin(h_theta), 300-hour_length*cos(h_theta)]
    pygame.draw.line(screen, BLACK, [300,300], h_tip, 2)

    #draw minute hand
    m_theta = minute*2*pi/60 + second*2*pi/(60*60)
    m_tip = [300+minute_length*sin(m_theta), 300-minute_length*cos(m_theta)]
    pygame.draw.line(screen, BLUE, [300,300], m_tip, 2)

    #draw seconds hand
    s_theta = second*2*pi/60
    s_tip = [300+second_length*sin(s_theta), 300-second_length*cos(s_theta)]
    pygame.draw.line(screen, RED, [300,300], s_tip, 2)

    pygame.display.flip()

    clock.tick(60)

pygame.quit()

You can use the time module in Python to make a clock read the current time:

>>> import time
>>> the_time = time.time()
>>> dt = time.localtime(the_time)
>>> print(dt.tm_hour,":",dt.tm_min,":",dt.tm_sec)
16 : 41 : 33

In our clocks program just set the 'hour' variable equal to 'dt.tm_hour" and so on. Yes, you have a clock on your computer and your phone already but you made this one!

Monday, November 9, 2015

Crypto-Code

A student of mine was thinking out loud about encoding messages and it got stuck in my head. The next day I challenged my Coder School students to try it. How do you even replace a letter with a number? You could use dozens of if-statements:

for letter in message:
    if letter == 'a':
        print(1)
    elif letter == 'b':
        print(2)

And so on. But an easier way is to create a string of characters:

ALPHA = "abcdefghijklmnopqrstuvwxyz ',.?"

Now each letter has a number, an index, its place in the string. The letter a is ALPHA[0], b is ALPHA[1] and so on . You can replace each letter with its index this way:

def encode(msg):
    '''takes a message and prints number code'''
    for letter in msg:
        print(ALPHA.index(letter), end=' ')

The last bit is thanks to Python 3, where print is a function. After printing the number you can specify printing something at the end, like a space. And it doesn't automatically print a line break. Running encode('call me ishmael.') we get

2 0 11 11 26 12 4 26 8 18 7 12 0 4 11 28

The decode function will require a little Python trickery. The numbers can be fed back in as a list if you're patient enough to type tons of commas, or it can be copied and pasted in, as a string inside quotes, to a function like

decode('2 0 11 11 26 12 4 26 8 18 7 12 0 4 11 28')

The decode function converts the string to a list using Python's split() function. Then you can just iterate over the list, printing the letter in the ALPHA list with that index number.

def decode(msg):
    '''takes numbers and prints decoded letters'''
    msg2 = msg.split() #converts string to list
    for item in msg2:
        print(ALPHA[int(item)],end = '')

So decoding another message, like

decode('8 26 22 0 13 19 26 19 14 26 7 14 11 3 26 24 14 20 17 26 7 0 13 3 28')

we get 

i want to hold your hand.

But that cipher wouldn't fool spies even from a thousand years ago. We need to make it a little sneakier. We could get rid of the spaces if all the numbers were 2-digits long:

def encode(msg):
    '''takes a message and returns number code'''
    code = ''
    for letter in msg:
        #get the index of letter and
        #convert it into a string
        num = str(ALPHA.index(letter))
        if len(num) == 1: #if it's only one digit
            num = '0' + num #add a zero in front
        code += num         #add that to the code
    print(code)

Now execute encode("beautiful is better than ugly.") and you'll get a more confusing-looking code:

010400201908052011260818260104191904172619070013262006112428

Now decoding it is fairly simple. You just have to take 2-digit slices from the string and print out the element of ALPHA that has that index.

def decode(msg):
    '''takes numbers and prints decoded letters'''
    for n in range(0,len(msg),2): #n goes up by 2's
        print(ALPHA[int(msg[n:n+2])],end='') #take 2-digit slices

Your enemy might notice a bunch of zeroes. My student's suggestion was to convert to binary numbers, make all the numbers 5-digits, then string them together! I already have a binary converter in my book Hacking Math Class, so we'll assume you have the binary function. You've converted the letters to numbers, then send them to this function:

def binary5(number):
    '''converts number to 5-digit binary'''
    number = binary(number) #get the binary form
    number = str(number) #convert to string form
    x = len(number)     #to find number of digits
    number = (5-x)*'0' + number #add zeroes if it's not 5
    print(number)

Now entering binary5(5) will add zeros to the front to make it a 5 digit number:

00101

I created strings of even and odd numbers for random choosing:

EVENS = '02468'

ODDS = '13579'

Here's the code (instead of printing) for converting the zeros to any even digit and the ones to any odd digit:

    #convert to random evens and odds
    binrand = ''     #string for random evens or odds
    for digit in number:        #go over every character
        if int(digit) % 2 == 0: #if its integer form is even
            #replace with random even digit
            binrand += random.choice(EVENS)
        else: #otherwise, replace it with random odd digit
            binrand += random.choice(ODDS)
    return binrand

Here's the new encode function:

def encode(msg):
    '''takes a message and returns number code'''
    code = '' #empty string for numbers
    for letter in msg: #goes over every letter
        #take the index of that letter, convert to
        #5-digit binary and add that to number string
        code += binary5(ALPHA.index(letter))
    print(code)
    print()#blank line

Now running the encode function will encode our message nicely.

>>> encode("what's my age again?")
9217424331466441621919055700927185047984178469303480480845780254853858208286417028442012044592133956

The first 5-digit slice, '92174', using the evens = '0' and odds = '1' transform, is the binary number 10110. That's 22 in decimal, or w's place in the ALPHA string.

The decode function reverses the process. First it takes the message and slices it into 5-digit slices.

def decode(msg):
    '''decodes a message'''
    msg2 = [] #list for 5-digit numbers
    for n in range(0,len(msg),5): #n goes up by 5's
        #add each 5-digit slice to list
        msg2.append(int(msg[n:n+5]))

Then it creates a list to store the 5-digit binary numbers and converts all the even digits to zeros and all the odds to ones.

    msg3 = [] #list for 5-digit binary numbers
    for number in msg2:
        number2 = str(number) #turn it into a string
        number3 = ''        #empty string for binaries
        for digit in number2: #go through digit by digit
            if digit in EVENS: #if digit is even
                digit = '0'         #replace by 0
            else: digit = '1'       #or replace by 1
            number3 += digit        #add to binary string
        msg3.append(number3)        #add binary string to message
        #print(msg3)

Finally you have to convert all the binary numbers to decimals. It's a good thing leading zeros are ignored by Python's 'int' function. That saves us a step. I already have a function for converting binary to decimal, (called "binDec") so I didn't give the code here.

    for number in msg3:     #go through binaries
        #convert to int, then decimal, then letter:
        print(ALPHA[binDec(int(number))],end = '')
    print()             #blank line after message

Now entering a nonsensical string of unbreakable code will yield a message of power and beauty:

>>> decode('164732271528482768355715312256534944844083853656139
161595498463010797645033218905045253958')
that's all, folks!

Update: Naturally my Python mentor Paddy Gaunt showed me how to do it in an eighth of the code.

import numpy as np

ALPHA = "abcdefghijklmnopqrstuvwxyz ',.?"
ALPHA = {c:i for i,c in enumerate(ALPHA)} # make it into a dict

def encode(str):
    narr = np.array([ALPHA[c] for c in str], dtype=np.uint8)
    narr = np.unpackbits(narr).reshape(-1, 8)[:,3:].reshape(-1)
    evod = np.array([np.random.choice([0,2,4,6,8], len(narr)),
                     np.random.choice([1,3,5,7,9], len(narr))])
    return evod[narr, np.arange(len(narr))]


print(encode("the quick brown fox jumps over the lazy dog"))