Random Art
In this part of the assignment you’ll complete a program that programmatically generates random pictures.
Background: Creating a picture from an expression
To create a picture, we’re going to begin with something unusual: a numeric expression.
Numeric expressions as a precursor to images
Suppose we have two floating-point variables, $x$ and $y$. Let’s define some expressions that depend on $x$ and $y$:
These definitions allow us to build complex expressions by combining smaller, relatively simple expressions. For example, we can build the expression $\sin(\pi \times (y \times x))$. We can also add more forms of expressions, for example, $\cos(\pi \times e)$.
We will also give ourselves one very important restriction: an expression’s value must be between $-1$ and $1$, inclusive. Note that this means $x$ and $y$’s values must have the same restriction: $-1 \leq x, y \leq 1$.
Transforming expressions to images
Now, let’s imagine a two-dimensional coordinate system composed of axes for $x$ and $y$: (see Figure 1). Because of the restrictions on $x$ and $y$’s values, we know that any point in this system lies in the range $[(-1, -1), (1,1)]$.
Furthermore, we know that evaluating an expression for any point $(x, y)$ in this $2$-by-$2$ square will give us a number between $-1$ and $1$. By scaling this number to a value between $0$ (black) and $255$ (white), we can generate a grayscale pixel for each point in the grid.
As an example, Figure 2 shows the picture that results from evaluating the expression $\mathrm{average}(x,\ y)$ at every point in the $2$-by-$2$ grid, then transforming the result to a grayscale value. (The grid lines are not part of the picture; they are just for illustration purposes.)
Of course, the expression $\mathrm{average}(x,\ y)$ is quite simple, and it leads to a somewhat simple picture. More complex expressions lead to more interesting pictures. For example, Figure 3 shows the picture that results from this slightly more complex expression: $\mathrm{average}(\cos(\pi \times x), \sin(\pi \times y))$.
If we want really interesting pictures, we might need really complex expressions. We could try to come up with some complex expressions ourselves, but a better idea would be to use Computer Science™: We can define a data structure that represents expressions, then randomly generate instances of the data structure!
Here is a randomly generated expression, whose picture appears in Figure 4:
sin(π x cos(π x average(sin(π x cos(π x sin(π x average(average(sin(π x y), sin(π x y)), sin(π x y) x sin(π x y)))) x average(sin(π x average(sin(π x y) x average(x, x), cos(π x y x y))), sin(π x cos(π x sin(π x average(x, y)))))), average(average(sin(π x cos(π x sin(π x average(x x y, x x x)))), cos(π x cos(π x cos(π x average(y, y))) x sin(π x sin(π x y) x y x x))), cos(π x cos(π x cos(π x cos(π x sin(π x cos(π x x))))))))))
To summarize, we can generate random art if we have the following things:
- A way to represent expressions.
- A way to evaluate an expression at a particular point $(x, y)$.
- A way to translate from an evaluation result to a grayscale pixel value.
- A way to evaluate an expression at all points in the coordinate system, then translate the results into grayscale pixel values.
- A way to draw a picture, given all its pixel values.
- A way to generate interesting random expressions.
The code we have provided you is a mostly complete implementation of these steps. You will fill in pieces for evaluating an expression, for representing more kinds of expressions, and for generating random expressions.
Evaluating expressions [15%]
The provided folder randomart
has a mostly-complete implementation of a random art generator.
We represent expressions with an Abstract Syntax Tree (AST), which is defined using a Haskell datatype:
data Exp = X -- x's value
| Y -- y's value
| Times Exp Exp -- product of e1 and e2
| Avg Exp Exp -- average of e1 and e2
| SinPi Exp -- sin (pi * e)
| CosPi Exp -- cos (pi * e)
deriving (Show, Read, Eq, Ord)
A point is defined to be a pair of Float
s, which represent $(x,y)$ coordinates:
type Point = (Float, Float)
The eval
function evaluates a given expression at a given $(x,y)$ coordinate:
eval :: Exp -> Point -> Float
Your task: Complete the definition of eval
in RandomArtEvaluation.hs
.
Test your code by using ghci to create a picture of the provided sample expression:
% ghci -fobject-code RandomArt.hs
GHCi, version 8.6.3: http://www.haskell.org/ghc/ :? for help
[1 of 3] Compiling RandomArtAST ( RandomArtAST.hs, RandomArtAST.o )
[2 of 3] Compiling RandomArtEvaluation ( RandomArtEvaluation.hs, RandomArtEvaluation.o )
[3 of 3] Compiling Main ( RandomArt.hs, RandomArt.o )
Ok, three modules loaded.
Prelude Main> toPNGgray "test" 300 (eval sampleExp)
Writing test.png...
Open the resulting test.png
file in an image viewer. It should look
exactly like the image in Figure 4.
Tip: When running ghci
for this assignment, your code will run much quicker if you
invoke it as
ghci -fobject-code
because that command will cause ghci
to compile the code you use in the
interactive session rather than interpret it. This assignment is
quite compute-intensive, so you’ll save a lot of time if you
remember to run this way.
Generating random art [18%]
Compile the random-art code into an executable using ghc
, as explained in the
comments at the top of the RandomArt.hs
file. Verify that you get a simple
picture when you run the program from the command line. The picture is simple
because the build
function is supposed to
generate interesting random expressions, but doesn’t.
Your task: Fix build
so that it generates interesting random
expressions.
build :: Int -> RandomFloats -> Exp
The first parameter to build
is a maximum nesting depth that the
resulting expression should have, and the second parameter is
an infinite list of random numbers between $0.0$ and $1.0$.
Here are a few tips for fixing build
:
- If every kind of expression can occur with equal probability, it is likely
that the random expression you get will be either
X
orY
(2 chances in 6), or a small expression likeTimes X Y
. Because small expressions produce simple pictures, you are required to find some way to prevent or discourage them. - The arguments to
build
can be helpful: How might you use a maximum nesting depth and random numbers to make sure the function generates complex (i.e., nested) expressions? - The helper function
splitRandomFloats
will be useful, when you need multiple streams of random numbers (e.g., for operations with multiple arguments).
You can test build
by generating a lot of pictures with different seeds and
depths. You can do so by running ghci -fobject-code
, then typing
:load RandomArt
in the interactive session.
To test the basics in grayscale, run
sequence_ [doGray 300 seed depth | seed <- [100..115], depth <- [4..10]]
which will produce 112 grayscale images. That should be enough to get a sense of how interesting your images are, and whether increased depth matters.
If you think these images are okay, and if you want, you can try generating a larger number (217) of color images, e.g., by running
sequence_ [doColor 300 seed depth | seed <- [100..130], depth <- [6..12]]
Enhance the expressiveness of your language [12%]
Here is a chance to show off some creativity! You’ll add more kinds of expressions to the AST, enhancing the kinds of pictures you can create.
Your task: Add at least three new constructors (i.e., kinds of expressions) to the
Exp
type, and update the build
and eval
functions appropriately.
Here are some guidelines for your new expressions:
-
At least one of your new constructors must have three subexpressions.
-
Remember that each new kind of expression must return a value in the range $[-1.0,1.0]$ when its arguments are within the range $[-1.0,1.0]$. If your function goes outside this range, an error will occur during output. Note that floating-point math can be imprecise, so that even if in theory your expression should not fall outside the $[-1.0,1.0]$ range, in practice (depending on the particular operations you use) it might.
-
Try to add creative expressions:
-
Avoid simple variants / combinations of existing expressions (e.g., $(e + e + e)/3$ or $\sin(2\pi\times e)$). Try to add different ideas from the provided AST.
-
Your functions don’t need to be complicated to be creative (in fact, complexity is detrimental to speed). Rather, your functions just need to “do something interesting” from the perspective of random art. There are a number of simple ideas that can be creative.
-
-
The best way to know whether your functions do something useful is to see how much of a difference they make to the pictures your program produces, so be sure to generate a bunch more. (You may need to iterate, if you see no improvement with your first choice of new expressions.)
Submit your favorite picture [5%]
Your task: Once all your code is working, and you have generated many pictures using the final version, pick exactly one favorite picture (per person, so if you’re working with a partner, each of you should submit your own), and upload it to Gradescope (there is an assignment dedicated especially for submitting the image).
Go on to the next part…
In the next part, we will work with a little language to reason about logical formulas.