Hacking Math Homework
Many high school students complain about boring and repetitive homework, but I’ve found a fun way of dealing with this that I find actually helps me understand concepts even better. When faced with large rote assignments I write programs to complete the homework like no human can: instantly, perfectly and on a large scale. In the past I have written written Literary Analysis Visualizations, Punnet Square generators and Graphing Programs.
Most of the time it takes way more time to write the program than it would take to do the homework but I end up learning a lot more and having more fun. Recently I wrote my wrote my most outrageous program yet, it took 10 times longer than it should have and blew away my teacher and class.
Part of my Advanced Functions class summative this year was to create a series of piecewise functions that when graphed produce a picture. Some examples given were line drawings of a smiley face and the Batman symbol. But I had an idea that would go beyond the intended simple line drawings so I spent my weekend implementing it.
I wrote a program that takes an image and composes equations of varying densities into hundreds of massive piecewise functions so that when you graph them on a very large canvas and zoom out they replicate the image in greyscale. The output looks like this:
Additional Resources
Another part of the program outputs a massive Latex document with all the large piecewise functions that produces a huge PDF. You can download a PDF that explains all the parts and has some more examples.
The Program
The program is written in Python and uses matplotlib, Numpy and Pillow. Excuse the terrible code with the manual constants, global variables and terrible logic structure. Not only was I learning Python while writing this but I had to finish the program by the next day and then never use the program again.
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
import numpy as np | |
import math | |
import string | |
import sys | |
import matplotlib.pyplot as plt | |
from PIL import Image | |
import random | |
from bisect import bisect_left, bisect, insort | |
# Setup | |
DOC_HEADER = """ | |
\\documentclass[fleqn,legalpaper]{article} | |
\\usepackage[margin=0.5in]{geometry} | |
\\usepackage{amsmath} | |
\\usepackage{mathtools} | |
\\usepackage[active,tightpage]{preview} | |
\\renewcommand{\\PreviewBorder}{1in} | |
\\newcommand{\\Newpage}{\\end{preview}\\begin{preview}} | |
\\begin{document} | |
\\begin{preview} | |
\\title{Function Art Summative} | |
\\author{Tristan Hume} | |
\\maketitle | |
\\begin{abstract} | |
Here are all the functions that make up my artwork. They are generated by a program that takes an image file and translates the pixels into functions of varying densities. Each function represents a line of pixels in the image. | |
\\end{abstract} | |
\\section{Functions} | |
\\end{preview} | |
""" | |
DOC_FOOTER = """ | |
\\end{document} | |
""" | |
FUNCTION_HEADER = """ | |
\\begin{{preview}} | |
\\[ | |
f_{{ {0} }}(x) = | |
\\begin{{dcases}}""" | |
FUNCTION_FOOTER = """ | |
\\end{dcases} | |
\\] | |
\\end{preview} | |
""" | |
img_width = 250 | |
img_height = 100 | |
def fun_undef(x): | |
return None | |
def exp_undef(x, y, add): | |
pass | |
def fun_hline(x): | |
return 0.0 | |
def exp_hline(x, y, add): | |
s = "{0:g}".format(y) | |
add(s, x, [(0.0, 1.0)]) | |
def fun_quad(x): | |
return -x*(x-1.0) | |
def exp_quad(x, y, add): | |
s = "-x (x-{1:g}) + {0:g}".format(y,x+1.0) | |
add(s, x, [(0.0, 1.0)]) | |
def fun_poly(x): | |
return 2000*(x**2)*(x-0.2)*((x-0.5)**2)*(x-0.8)*((x-1.0)**2) | |
def exp_poly(x, y, add): | |
s = "2000 x^2 (x-{1:g}) (x-{2:g})^2 (x-{3:g}) (x-{4:g})^2 + {0:g}".format(y,x+0.2,x+0.5,x+0.8,x+1.0) | |
add(s, x, [(0.0, 1.0)]) | |
def fun_recip(x): | |
return 0.2/(10*(x-0.5)) | |
def exp_recip(x, y, add): | |
s = "\\frac{{0.2}}{{10x-{1:g} }} + {0:g}".format(y, 5+10*x) | |
add(s, x, [(0.0, 0.45),(0.54,1.0)]) | |
def fun_recip2(x): | |
return 0.01/((x-0.25)*(x-0.75)) | |
def exp_recip2(x, y, add): | |
s = "\\frac{{0.01}}{{(x-{1:g}) (x-{2:g})}} + {0:g}".format(y, x+0.25, x+0.75) | |
add(s, x, [(0.0, 0.21),(0.3, 0.7),(0.79,1.0)]) | |
def fun_abs(x): | |
return -abs(x - 0.5) + 0.5 | |
def exp_abs(x, y, add): | |
s = "-|x-{1:g}| + {0:g}".format(y+0.5,x+0.5) | |
add(s, x, [(0.0, 1.0)]) | |
def fun_log(x): | |
if x < 0.5: | |
return -0.4*math.log10(-(2.0*x-1.0)) | |
else: | |
return -0.4*math.log10(2.0*x - 1.0) | |
def exp_log(x, y, add): | |
s = "-0.4 log(-2x+{1:g})) + {0:g}".format(y, 1+2*x) | |
add(s, x, [(0.0, 0.47)]) | |
s = "-0.4 log(2x-{1:g})) + {0:g}".format(y, 2*x-1) | |
add(s, x, [(0.47, 1.0)]) | |
def fun_logist(x): | |
if x < 0.5: | |
return 0.4/(1+math.e**(-25.0*x+7)) | |
else: | |
return 0.4/(1+math.e**(25.0*x-18)) | |
def exp_logist(x, y, add): | |
s = "\\frac{{0.4}}{{1+e^{{(-25x+{1:g})}} }} + {0:g}".format(y, 7+25*x) | |
add(s, x, [(0.0, 0.5)]) | |
s = "\\frac{{0.4}}{{1+e^{{(25x-{1:g})}} }} + {0:g}".format(y, 18+25*x) | |
add(s, x, [(0.5, 1.0)]) | |
def gen_sin(den,scale=0.5): | |
def fun_sin(x): | |
return scale*math.sin(den * math.pi * x) | |
return fun_sin | |
def gen_sin_exp(den,scale=0.5): | |
def exp_sin(x, y, add): | |
s = "{3:g} sin({2:g}\\pi x) + {0:g}".format(y+0.5,x, den,scale) | |
add(s, x, [(0.0, 1.0)]) | |
return exp_sin | |
CATALOG = [ | |
("line", fun_hline, exp_hline), | |
("log", fun_log, exp_log ), | |
("abs", fun_abs, exp_abs), | |
("quad", fun_quad, exp_quad), | |
("logist", fun_logist, exp_logist), | |
("reciprocal", fun_recip, exp_recip), | |
("reciprocal2", fun_recip2, exp_recip2), | |
("smallsin4", gen_sin(4,0.3), gen_sin_exp(4, 0.3)), | |
("largesin4", gen_sin(4,0.5), gen_sin_exp(4, 0.5)), | |
("polynomial", fun_poly, exp_poly) | |
] | |
# ZONES=[5, 30, 60, 100, 140, 180, 220] | |
ZONES=[10, 30, 40, 50, 60, 100, 140, 180, 220] | |
FUNS = [fun_undef, fun_hline, fun_quad, [fun_abs, fun_log], fun_logist, fun_poly, fun_recip, fun_recip2, gen_sin(4,0.3), gen_sin(4,0.5)] | |
# FUNS = [fun_undef, fun_hline, [fun_log, fun_abs, fun_quad, fun_logist], fun_poly, fun_recip, fun_recip2, gen_sin(4,0.3), gen_sin(4,0.5)] | |
INFO = [exp_undef, exp_hline, exp_quad, [exp_abs, exp_log], exp_logist, exp_poly, exp_recip, exp_recip2, gen_sin_exp(4,0.3), gen_sin_exp(4)] | |
# Drawing | |
def load_image(file_name): | |
global img_height, img_width | |
im = Image.open(file_name) | |
img_height = int((img_width/float(im.size[0])) * im.size[1]) | |
im = im.resize((img_width, img_height),Image.BILINEAR) | |
print im.size | |
im = im.convert("L") | |
# im = im.convert("P", palette=Image.ADAPTIVE, colors=len(FUNS)) | |
# im = im.convert("L") | |
im.save("out.png") | |
# pix = np.array(im) | |
# zones = np.unique(pix) | |
return im | |
def density(lum): | |
return bisect(ZONES,255 - lum) | |
def density_to_elem(den, elems): | |
e = elems[den] | |
if isinstance(e, list): | |
e = random.choice(e) | |
return e | |
def image_to_grid(im, elems): | |
grid = [] | |
random.seed(55) | |
for row in xrange(im.size[1]): | |
row_list = [] | |
for col in xrange(im.size[0]): | |
den = density(im.getpixel((col,row))) | |
# print den | |
elem = density_to_elem(den, elems) | |
row_list.append(elem) | |
grid.append(row_list) | |
return grid | |
def row_fun(grid, row, y): | |
def fun(n): | |
col = int(n) | |
x = n % 1.0 | |
f = grid[row][col] | |
val = f(x) | |
if val == None or val < -0.5 or val > 0.5: | |
return None | |
return val + y | |
return fun | |
def plot_piece(fun): | |
global img_width | |
vfun = np.vectorize(fun) | |
x = np.linspace(0, img_width - 1, img_width * 150) | |
y = vfun(x) | |
row_plot = plt.plot(x, y, '-', linewidth=0.3) | |
plt.setp(row_plot, color='black') | |
# plt.show() | |
def plot_image(im, name="output"): | |
global img_width, img_height | |
grid = image_to_grid(im, FUNS) | |
fig = plt.figure() | |
for y in xrange(len(grid)): | |
f = row_fun(grid, len(grid) - y - 1, y) | |
plot_piece(f) | |
height = (img_height/float(img_width))*8.5 | |
fig.set_size_inches(8.5, height) | |
# ax = plt.Axes(fig, [0.0, 0., img_width, img_height]) | |
# ax.set_axis_off() | |
# fig.add_axes(ax) | |
filename = "output/{0}.png".format(name) | |
plt.savefig(filename, dpi=80) | |
# plt.show() | |
# Printing | |
function_count = 0 | |
def restriction_string(res, offset): | |
return "{0:g} \\le x < {1:g}".format(res[0] + offset, res[1] + offset) | |
def restricts(restrictions, offset = 0.0): | |
strs = [restriction_string(r, offset) for r in restrictions] | |
return string.join(strs, ', ') | |
def write_function(f, y, row): | |
def add_piece(formula, offset, restrictions): | |
global function_count | |
function_count += 1 | |
f.write("\n{0}, & {1}\\\\".format(formula, restricts(restrictions, offset))) | |
f.write(FUNCTION_HEADER.format(y)) | |
for x, exp in enumerate(row): | |
exp(x,y,add_piece) | |
f.write(FUNCTION_FOOTER) | |
def create_functions_doc(im): | |
grid = image_to_grid(im, INFO) | |
f = open('functions.tex', 'w') | |
f.write(DOC_HEADER) | |
for idx, row in enumerate(grid): | |
write_function(f, len(grid) - idx, row) | |
f.write(DOC_FOOTER) | |
def print_piece(formula, offset, restrictions): | |
print "\\[f(x) = {0}, {1}\\]".format(formula, restricts(restrictions, offset)) | |
# Mini Graphs | |
def plot_minifun(fun,name): | |
vfun = np.vectorize(fun) | |
x = np.linspace(0.0, 1.0, 400) | |
y = vfun(x) | |
fig = plt.figure() | |
fig.set_size_inches(2, 2) | |
ax = plt.Axes(fig, [0.0, 0., 1., 1.]) | |
ax.set_ylim([-0.5,0.5]) | |
# ax.set_axis_off() | |
fig.add_axes(ax) | |
row_plot = plt.plot(x, y, '-', linewidth=5) | |
plt.setp(row_plot, color='black') | |
plt.savefig(name, dpi = 80, bbox_inches='tight') | |
def minifun_darkness(name): | |
im = Image.open(name) | |
im = im.convert("L") | |
size = im.size[0] * im.size[1] | |
pix = np.array(im) | |
return np.average(pix) | |
# Test code | |
if len(sys.argv) < 2: | |
print "usage: art_gen.py imagename [doc]" | |
elif sys.argv[1] == "minifuns": | |
ordered = [] | |
for (key, val, ev) in CATALOG: | |
filename = "minifuns/{0}.png".format(key) | |
plot_minifun(val, filename) | |
darkness = minifun_darkness(filename) | |
insort(ordered, (darkness, key)) | |
print ordered | |
elif sys.argv[1] == "minidoc": | |
for (name, fun, efun) in CATALOG: | |
efun(0.0,0.0,print_piece) | |
else: | |
print sys.argv[1] | |
im = load_image(sys.argv[1]) | |
if len(sys.argv) > 2: | |
com = sys.argv[2] | |
if com == "doc": | |
create_functions_doc(im) | |
print function_count | |
else: | |
plot_image(im) |