Matt and I made a fancy new bench out of red oak. In case you can't tell it has subtly curved legs and stretchers, with mortice and tenon joints connecting them. The legs attach to the top with some neato through-tenons, exposing the end grain of the wood.

The design was based on images from this Wood Wisperer post. Below are some adapted plans, with the dimensions we ended up using.

Here are some plans of the individual pieces: the seat, the two long stretchers, the two short stretchers, and the four legs. All the parts except for the seat have gentle curves cut into one edge.

We started by making the seat. We edge joined two 40" pieces of 2x8" red oak. We read that you need a jointer to make edge joining work. It would probably help, but we just glued together the factory edges from the lumber yard. There was a visable glue seam in some places, but it was good enough for us.

Speaking of the lumber yard, we went to L. Sweet Lumber in Providence. They were helpful and the wood was beautiful.

Next, we cut the legs and stretchers to length. The legs were made from 2x6" and the stretchers from 1x4".

Next, we cut the mortices. We decided to go with uniform 1/8" shoulders for all of our tenons. This left us with ~1/2" wide mortices for the stretchers and ~1 1/2" for the legs. We used Phil's drill press with some forstner bits to remove most of the wood, then finished the mortices with the chisels.

This was time consuming and difficult, especially for the larger mortices on the seat. These were for through-tenons, so the result would be on display on the seat of the bench. We made some gnarly edges in some places.

Next, the tenons. We did this on the table saw, using a dado. We threw together a table saw sled for this part, and it was very helpful. I can't believe we didn't have a sled before this. We put together a 1/2" dado stack and clamped a stop for the tenon length. We ran the ends of the stretchers and legs over the dado for a few passes, until we reached the stop. The length was 3/4" for the stretcher tenons and just over 1 3/4" for the legs, so that they would barely emerge from the top of the seat. After sanding them to fit, they were looking pretty handsome.

We weren't exactly sure how we would cut the curves into the stretchers and legs. We marked the curves very approximately according to the plan. We traced the small stretcher curves with a 24" drum head. Luckily it had exactly the right radius for our design. For the long stretchers and legs, we didn't have any enormous circles on hand so we just bent a thin piece of wood and clamped it in place.

We didn't have access to a band saw, and we were worried that we'd mess it up with the jig saw. There was no other option, though. The curves looked only a little horrible immediately after cutting, and way better after some fine tuning with the palm sander. Now we had all the pieces ready.

Gluing everything together was almost a disaster. We carefully fit every tenon to its matching mortice with chisels and sand paper, but when we started gluing we realized we had done it wrong and commited to an impossible orientation. We pressed ahead, hoping everything would fit, but the glue started setting and some of the tenons felt stuck half way in. I thought we were doomed. There were some moments of "Oh god, what do we...?! Just hit it! Is it fitting? Just hit it keep hitting it!". After five minutes of terror and dozens of strikes with the mallet, the bench made it together. Somehow, it was square everywhere.

We filled the shameful gaps between the mortices and tenon ends on the top of the seat with a mixture of saw dust and wood glue. This seemed to work.

The next morning, we removed the clamps, and sanded away all the glue. On the seat, we sanded almost 1/8" of the ends of the tenons to make them flush. We worked our way to 400 grit on every surface. We tried some weird fine sanding disks called SandNet from the Home Depot, because they looked fun. It's some kind mesh material made from ancient elven fibers. The pack says it lasts longer because you can wash the sawdust out of it. Instead, they seemed to get ripped up immediately by the sander. I don't know.

Finally, we applied two coats of finishing wax.

Here's the finished product. It turned out to be very heavy and sturdy. Hopefully no one ever has to move it.


Programmatic Sol LeWitt Wall Drawing

I visited MASS MoCA with some friends recently. There was a whole building there filled with the work of Sol LeWitt, a contemporary artist famous for minimalist geometric art. We spent a while looking at Wall Drawing #797.

Helen Harrison

This drawing was made in an interesting way. One person drew a starting wavy line at the top. Then artists took turns following the bottom of the line with another line. This was repeated for the whole wall. The result has some complicated behavior. Concave parts produced ridges propagating downward and combining into larger ridges. It reminded us of caustic shapes produced by the complex surface of disturbed water. Phil pointed out that the connection with waves isn't an accident, since this procedure effectively applies Huygens' Principle. He blogged about this too, but my post is better.

We all thought it would be cool to reproduce this idea in a computer. I found a way to do it with a few lines of Mathematica.

Not a perfect match, but it has most of the features we noticed. The heart of it is the overloaded function, requiredY. The points that make up the lines are confined to discrete positiosn in x. With two arguments, it finds the new y value required to put a new point at in the column at index i based on being a distance r from the last point in the row at index j. With one argument, it finds the minimum y required by all other rows. Maybe that makes sense. The rest of the code is just defining the starting curve, repeatedly applying the function that generates a new line, and displaying the results.

xs = Range[200];
ys = 10 + 5 (Sin[xs/10.] + Sin[xs/6.] + Sin[xs/16.]) +
Last /@ Normal[
RandomFunction[WienerProcess[0, .75], {0, 199, 1}]][[1]];
next[xs_, ys_, r_] := Module[{requiredY, newYs},
requiredY[i_, j_] := ys[[j]] - Sqrt[r^2 - Abs[i - j]^2];
requiredY[i_] :=
Min[requiredY[i, #] & /@ Range[Max[i - r, 1], Min[i + r, 200]]];
newYs = requiredY /@ xs;
ListPlot[NestList[next[xs, #, 2] &, ys, 50], Joined -> True,
Axes -> False, PlotStyle -> {Thickness[0.005]}]
I stumbled on some other neat looking results by accident too.

Q Learning Maze Game

I've been learning about reinforcement learning. The simplest version I've heard of is called Q learning. The key to this method is maintaing a Q matrix, which stores information about the quality each state-action pair. Once trained, a player can simply look up their state and take the action with the highest value.

The goal is obviously to train this Q matrix to accurately reflect the game. This can be done by taking moves and updating Q each time in the following way:

As you can see below, when my QLearner is training, it takes random moves every time. I think the more common way to do it is to sometimes take random moves and sometimes take moves according to the current version of Q. That kind of weights the learner toward moves that seem better. It doesn't matter in this case, though. The maze problem is so easy that anything would work.

Here, the reward is simply zero unless the player reaches the goal. At first I set the discount factor, gamma, to be 1. This did not work. Since the random player will always eventually finish the maze, this resulted in every square eventually getting a value of 1. The discount factor means that faster routes to the goal are favored.

import numpy as np
from random import randint

class Maze:
def __init__(self):
self.w = 5
self.h = 5
p = Tile.player
b = Tile.block
e = Tile.empty
g = Tile.goal = [
[e, e, e, b, g],
[e, b, e, b, e],
[e, b, e, e, e],
[e, b, b, b, b],
[e, e, e, e, e],
def __repr__(self):
returnStr = ""
for i in range(self.h):
for j in range(self.w):
if (j, i) == self.playerPos:
returnStr += str(Tile.player)
returnStr += str([i][j])
returnStr += "\n"
return returnStr
def tileAt(self, (x, y)):
def isValidPos(self, (x, y)):
return all([x >= 0, y >= 0, x < self.w, y < self.h])
def reset(self):
self.playerPos = (0, 4)
self.gameWon = False
def move(self, dir):
movePos = addTuple(self.playerPos, dir)
if self.isValidPos(movePos):
if self.tileAt(movePos) == Tile.empty:
self.playerPos = movePos
return 0
elif self.tileAt(movePos) == Tile.goal:
self.playerPos = movePos
self.gameWon = True
return 1
return 0
return 0

class QLearner:
def __init__(self, maze):
self.maze = maze
self.Q = np.zeros((self.maze.w, self.maze.h, 4))
self.alpha = 0.1
self.gamma = 0.9
def learn(self, n):
for r in range(n):
while self.maze.gameWon == False:
i = randint(0,3)
def updateQ(self, i):
x0, y0 = self.maze.playerPos
moveDir = Dir.moveOptions[i]
reward = self.maze.move(moveDir)
x, y = self.maze.playerPos

maxQ = max(self.Q[x, y, [0,1,2,3]])

self.Q[x0, y0, i] = self.Q[x0, y0, i] + (self.alpha * (reward + (self.gamma * maxQ) - self.Q[x0, y0, i]))
def getBestMove(self, (x, y)):
bestMove = Dir.moveOptions[np.argmax(self.Q[x, y, [0,1,2,3]])]
return bestMove

def addTuple((a1, a2), (b1, b2)):
return (a1 + b1, a2 + b2)

class Dir:
UP = (0,-1)
DOWN = (0, 1)
LEFT = (-1, 0)
RIGHT = (1, 0)
moveOptions = [UP, RIGHT, DOWN, LEFT]

class Tile:
player = 'p'
goal = 'g'
block = 'X'
empty = ' '

maze = Maze()
learner = QLearner(maze)

print learner.Q

while maze.gameWon == False:
print maze

Pemi Loop

In the beginning of June, I hiked the Pemi Loop with my brother and some friends. Look at those happy fellas up there! We took our time, spending three nights on the trail. I mostly planned the trip decided to take us counter-clockwise to save the Franconia Ridge for the end of the trip. I think most people hike the other way, but I liked having Mt. Lafayette waiting for us across the valley.

We started by crossing the Pemigewasset River onto the easy Lincoln Woods Trail. It's a few miles of perfectly flat former railway. We reached the border of the Pemigewasset wilderness and continued on the Wilderness trail and Bondcliff trail.

It was raining a bit, and the water was high. Truth be told, I was nervous about the weather on the ridge. The Pemi Loop is all ridge. The park ranger had just told us a fun hypothermia story from earlier in the week. A different park ranger told me on the phone that we'd need microspikes but we would probably be okay without snowshoes. We didn't bring either, and that turned out to be the right move. I think we saw snow once.

As we walked, the rain cleared up, and by the time we reached the Bondcliff, the weather was perfect. The rain never returned, but the wind picked up during climb to the peak of Mt. Bond and almost carried Bobby away.

Eventually, we reached Guyot Shelter. The shelter is perched on a rock and the whole camp is tiered into the side of the mountain. It felt like a forest village from a video game.

The next morning, we left camp and quickly summitted Mt. Guyot, where we joined the Appalachian Trail. After that, there was some nice flat highland, followed by a steep climb to the top of South Twin Mountain.

After lunch, we descended a thousand feet to Galehead hut for some brownies. We ended the day by climbing to Garfield Ridge Shelter, most of the way up Mt. Garfield.

There was a nice spot to sit. I wonder what they're laughing at.

In the morning, we immedaitely reached the summit of Garfield. There was no water available between the two campsites, so we brought as much as we could. It would have been nice to have more than two liters. Lafayette looked pissed.

Eventually, we crossed the ridge and decended to the Liberty Springs tentsite.

The final day, we got up early and left the AT, climbing Mt. Liberty and Mt. Flume on the Osseo Trail.

The final descent was steep at first, but flattened out over several miles, eventually reuninting with the Lincoln Woods trail.

Then, we washed off and got burgers.