Lighting a Christmas Tree

Christmas
Calculus

Let’s bring math to bear on decorating our Christmas tree! Here, I derive some equations and calculators to help get nice uniform lighting on a tree! And we kick it up a notch (do people still remember Emeril?) and create some nice patterns.

Published

December 25, 2017

Conical spiral
Figure 1

What’s a good way to decorate a Christmas tree with lights? Most people I know wrap lights around the tree starting from the top and winding around roughly following the curve of the conical spiral in Figure 1. For a long time I haphazardly strung lights around trying to avoid dark regions and make things somewhat uniform. I never know how many lights I’ll need, and I always end up back-tracking up and down the tree.

Looking online, there is much advice about how to light a Christmas tree. One article I can’t find recommends starting from the bottom and going up so that you guarantee your electrical plug is where you need it to be. Country Living has an article where they proclaim the correct way to hang Christmas lights from a tree is vertically (which strikes me as a terrible approach). Coworkers discussed different wrapping and patterns that they like to do to get things just to their liking. And some bloggers have come up with relatively complete solutions to the question of length of Christmas lighting following a conical spiral path. There’s even a book by [@fry2017indisputable] where they give an equation relating garland length to Christmas tree height and radius. Here, I’ll derive the length of the conical spirial, use that to simulate a Christmas tree, and use this simulation to test some methods of tree lighting.

Christmas Tree Model

Dimensions and coordinate system.
Figure 2

Christmas trees are generally approximately conical and green, and I’ll assume they’re perfectly conical and smooth for this model. Note: If you find a perfectly smooth conical tree at a tree lot, it’s likely not a tree and you should avoid it. Not a lot of friendly things are green and perfectly conical. You’ve been warned! Our mathematical model of the tree will be according to Figure 2. Let the tree be \(h\) tall and have a base of radius \(r\). I choose the base of the tree to be the origin and the top to be \((0, 0, h)\) in my coordinate system.

We can describe the surface of the tree in terms of the angle about the trunk and the height by the function \(f(\theta, z) : [0, 2\pi) \times [0, \infty) \rightarrow \mathbb{R}^3\) where

\[ f(\theta, z) = \left[ \begin{array}{c} r \frac{z}{h} \sin(\theta) \\ r \frac{z}{h} \cos(\theta) \\ h-z \\ \end{array} \right]. \]

Winding lights around a tree can be done many ways. One of the more regular approaches is to keep the space between loops of the lights constant (so that the slope of the lights is higher at the top and lower at the bottom) which ought to yield a more uniform coverage of the tree. I chose to express this through \(t\) - the number of turns the lights make about the tree. Note that \(t\) does not have to be an integer. Small Christmas lights have a spacing of about 3 inches between bulbs (where as the big G40 bulbs have approximately a foot between them) so one reasonable value for \(t\) could be \(h/3\) so that the vertical spacing of the lines of lights matches the horizontal spacing of the bulbs (again, this is approximate, since the bulbs won’t line up perfectly). Let \(g(z):[0,\infty) \rightarrow \mathbb{R}^3\) describe the parametric curve of the Christmas lights:

\[ g(z) = \left[ \begin{array}{c} r \frac{z}{h} ~\sin\left( 2 \pi z \frac{t}{h} \right) \\ r \frac{z}{h} ~\cos\left( 2 \pi z \frac{t}{h} \right) \\ h-z \end{array} \right]. \]

This produces a surprisingly dense lighting curve shown here in Figure 3.

Winding for mini Christmas lights.
Figure 3

This arrangement of lights is promising, but to know the amount of lights we need we first need to know the length of the path \(g(z)\).

Arclength - How Many Boxes of Lights?

To calculate the number of boxes of lights that we need we first need to know the length of the path \(g(z)\). To calculate this we simply must rely on our old Calculus learnings. Here, rather than calculating the total length, I calculate the length from the top of the tree down to \(b\), which is

\[ \int_0^b \sqrt{\nabla g(z)^\mathrm{T} \nabla g(z)} ~~\mathrm{d}z, \]

where the gradient of \(g(z)\) is

\[ \nabla g(z) = \left[ \begin{array}{c} \frac{r}{h} ~\sin\left( 2 \pi z \frac{t}{h} \right) + 2 \pi r t \frac{z}{h^2} ~\cos\left( 2 \pi z \frac{t}{h} \right) \\ \frac{r}{h} ~\cos\left( 2 \pi z \frac{t}{h} \right) - 2 \pi r t \frac{z}{h^2} ~\sin\left( 2 \pi z \frac{t}{h} \right) \\ -1 \end{array} \right]. \]

Expanding out the terms (use a CAS or a grad student) we find

\[ \sqrt{\nabla g(z)^\mathrm{T} \nabla g(z)} = \sqrt{\frac{4\,{\pi}^{2}{t}^{2}{z}^{2}}{h^4} + \frac{r^2}{h^2} + 1} \]

which can be easily solved via integration by substitution. Specifically, first factor out \(\sqrt{1 + \frac{r^2}{h^2}}\) so that we’re trying to evaluate

\[ \sqrt{1 + \frac{r^2}{h^2}} \int_0^b {\sqrt {4\,{\frac {{\pi}^{2}{r}^{2}{ t}^{2}{z}^{2}}{{h}^{2} \left( {h}^{2}+{r}^{2} \right) }}+1}} ~~\mathrm{d}z \]

and then prepare for integration by substitution (since \(\int \sqrt{u^2+1}~\mathrm{d}u\) yields a closed form solution). Define \(\phi : [0, h] \rightarrow \mathbb{R}\) to be

\[ \phi(z) = \frac{2 \pi t r}{h \sqrt{h^2 + r^2}} z \]

and note that

\[ \sqrt{\phi(z)^2 + 1} = {\sqrt {4\,{\frac {{\pi}^{2}{r}^{2}{ t}^{2}{z}^{2}}{{h}^{2} \left( {h}^{2}+{r}^{2} \right) }}+1}}. \]

Also note that \(\phi'(z) = \phi(1)\). Thus we can do integration by substitution by noting

\[ \sqrt{1 + \frac{r^2}{h^2}} \frac{1}{\phi(1)} \int_0^b \sqrt{\phi(z)^2 + 1} ~\phi(1) ~~\mathrm{d}z = \sqrt{1 + \frac{r^2}{h^2}} \frac{1}{\phi(1)} \int_{\phi(0)}^{\phi(b)} \sqrt{u^2 + 1} ~~\mathrm{d}u \]

yielding the final expression

\[ \ell(b) = \sqrt{1 + \frac{r^2}{h^2}} \frac{1}{2 \phi(1)}\left[ \phi(b) \sqrt{\phi(b)^2 + 1} + \ln\left( \phi(b) + \sqrt{\phi(b)^2+1} \right) \right]. \]

This allows us to generate a simple calculator.

Total Length Calculator
 

Note that, when shopping for Christmas lights, retailers often give the bulb count, overall length, and lighted length. By taking the lighted length and dividing by one less than the bulb count you can get the inter-bulb spacing. Also, note that chaining many lights together can be a fire hazard, and nothing ruins the spirit of Christmas like an infernal blaze.

The next step in constructing a model of the Christmas tree is to identify light locations, which is unfortunately non-trivial, but almost trivial.

Finding Light Locations

To discover the location of individual bulbs we need to know how far apart they are. Unfortunately, \(\ell(b)\) is not invertable, so we must either approximate the inverse function or use numerical search to find the solution. Initially I attempted to fit a least squares approximate surrogate function but that didn’t pan out. My second approach was to use Newton’s method which yielded very good results. Note that

\[ \frac{\partial \ell}{\partial b} = \sqrt{\frac{4 \pi^2 t^2 b^2}{h^4} + \frac{r^2}{h^2} + 1}. \]

Let’s say we want to find the location of the bulb that’s \(b_0\) along the line. When we iterate through the convergent sequence \(\{b_i\}_{i=1}^\infty \rightarrow b_0\) using Newton’s method

\[ b_{k+1} = b_k - \frac{\ell(b_k) - b_0}{\ell'(b_k)} \]

to quickly converge to a good value. I used this to produce the spacings of the bulbs along the wire.

Similarly, we can use Newton’s method to find the number of turns \(t\) to make when we know the length of lights \(l\), the tree base radius \(r\), and the tree height \(h\). The solution has a closed form but it’s ugly so I’ve tucked it into a javascript function in another calculator. Use this one to determine how many turns to make, and thus how to ensure your light string starts at the top of the tree and ends at the bottom.

Total Turn Calculator
 

Back to the distribution of bulbs along the line. Figure 4 shows what my Christmas tree model would look like decked out with bulbs having the same spacing between bulbs as the vertical distance between loops of the line.

Theoretical christmas tree with lights.
Figure 4

If I apply the five color pallette common for Christmas lights of red, magenta, blue, orange, and green, this results in the spacing shown in Figure 4.

Overall I think this is a pretty good spread, but after staring at it for a while certain patterns emergy. For example, on this tree model (7 ft. tall and 2 ft. radius) there are 3 bands of lights where the top and the bottom lights line up by color very closely (and thus really bothering me). From the bottom center moving up look at bands 3 and 4, bands 14 and 15, and bands 17 and 18. I think I could get over that if need be.

Simulating Walking Around the Tree

Consider the projection of the lights orthogonally away from the tree, ignoring the lights obscured by the tree. For a given direction \(\theta\), we take all of the lights between \(\theta - \frac{\pi}{2}\) and \(\theta + \frac{\pi}{2}\) and project them in the direction \(\theta\).

Define the vectors \(\mathbf{v}_x\), \(\mathbf{v}_y\), and \(\mathbf{v}_\text{out}\) as

\[ \mathbf{v}_x = \left[ \begin{array}{c} \sin(\theta) \\ \cos(\theta) \\ 0 \end{array} \right], \quad \mathbf{v}_y = \left[ \begin{array}{c} 0 \\ 0 \\ 1 \end{array} \right], \quad\text{and}\quad \mathbf{v}_\text{out} = \left[ \begin{array}{c} \cos(\theta) \\ -\sin(\theta) \\ 0 \end{array} \right]. \]

For a particular value of \(\theta\) we only concern outselves with points \(\mathbf{p}\) that have the property \(\mathbf{p}^\mathrm{T} \mathbf{v}_\text{out} \ge 0\). The planar projection for \(\mathbf{p}\) is then

\[ \left[ \begin{array}{cc} \mathbf{p}^\mathrm{T} \mathbf{v}_x \\ \mathbf{p}^\mathrm{T} \mathbf{v}_y \end{array} \right]. \]

Animated, these projections look like figure 5:

Projections of red lights.
Figure 5

If you stare at this long enough you may start to notice a wave pattern emerging in the lower section. The highly regular spacing of the lights is something that at some point jumps out at you.

Multi-Phase Lighting Arrangements

Our initial derivation was for a single contiguous strand of lights that would be wound around the tree, but there is no reason why we can’t do \(k\) strands of lights, each offset at \(\frac{2 \pi}{k+1}\) radians from the others, and adjust the spacing so that they all blend together. Each light-phase will be identical but if they’re far enough apart then this regularity might not be a bad thing.

Redefine the parametric Christmas light curve \(g(z; \psi)\) to now have phase \(\psi \in [0, 2 \pi)\), as

\[ g(z; \psi) = \left[ \begin{array}{c} r \frac{z}{h} ~\sin\left( 2 \pi z \frac{t}{h} + \psi \right) \\ r \frac{z}{h} ~\cos\left( 2 \pi z \frac{t}{h} + \psi \right) \\ h-z \end{array} \right]. \]

A three-phase example is then given in Figure 6.

Projections of red lights.
Figure 6

None of the R code I used had to be changed to graph this setup, the height \(z_k\) of the \(k\)th bulb is the same for each phase strand, the only difference is the angular offset. The only difference is that, even though the lights are 3 in. apart, we need the strands to now have 9 in. of vertical separation from themselves since we’re cramming 3 of them in there. In general, if we have \(m\) strands at \(\frac{2 \pi}{m+1} i\) for \(i=0,1,\ldots,m\), then we’d want to use \(t = h / (3 m)\). The result of the three-phase model is given in Figure 7.

Projections of red lights. Projections of red lights.

Figure 7

There is a pleasantness in the plots of Figure 7 which I think comes from the fact that the colors of lights seem to oscilate down the tree. It’s much more appealing to me than the waves in Figure 5. But it happens to be a fluke of the particular size of this tree model. If we color each strand red, green, and blue respectively then we see that the pattern of Figure 7 isn’t general.

Projections of red lights.
Figure 8

Something interesting happens when we take the number of phases of strands to a rediculous number (e.g. 12), as show in Figure 9. Here, note that the steep slope makes the top of the tree very regular and pulls the eye up, whereas the decreasing slope lower in the tree causes the lights to spread out more. I don’t know, I just think it’s cool.

Projections of red lights.
Figure 9

Trial Run and Christmas 2018

When Target put all of their holiday gear on sale I snatched up a bunch of monocolor lights (red, white, and green in color), as well as ornament sets that were red, silver, white, and green. The lighting arrangement I plan to attempt is using a 12-phase approach but grouping colors together, to produce the look in Figure 10.

Projections of red lights.
Figure 10

In addition to the ornaments and lights I also picked up some fake white poinsettia garland from Michael’s for 70% off, and so this too will be interleaved throughout the design, but I have yet to work out the specifics. One specific of some concern is how to place ornaments on the tree - a space-filling design that will have to wait for another blog post.

Me lighting a Christmas Tree
Figure 11

Having purchased the new lights I needed to test them out while they were still in Target’s return window (if clearance items are even eligible for return). I borrowed a bright pink miniature Christmas Tree and took out one of the newly purchased strands of lights. The specific approach I used was to take the lighted length of the string and determine the vertical spacing between bands to ensure that the string would start at the top and end at the bottom. Reality proved to be a harsh teacher.

Inter-light spacing
Figure 12

A few things became clear. First, it’s important to remember that we place light string on the interior of the tree, not on the surface like in my 3-D renderings. Thus, the true radius at which the light strand rests is about an inch less than the radius of the tree at the same spot. Second, even though the space between lights was about 3 inches when they were stretched out (see Figure 12) the new lights tended to contract to assume the shape they had in the box during shipping. This contraction meant that I was only getting about 2.5” of length between bulbs on average, the realied distance along the surface of the tree between bulbs was highly variable, and the optimal wrapping of the tree with my single strand didn’t quite make it to the bottom of the tree. Even when stretching out the string of lights, the spacing wasn’t quite exactly uniform suggesting quality control concerns (I’m looking at you Philips). I decided that the best thing to do was wrap the lights on spools and let them sit until next Christmas.

I’ll close here with a picture of my favorite Christmas Tree this year. Figure 13 is a Christmas Tree I saw at a business plaza in San Francisco. Each branch was wrapped individually and the interior of the tree is full of drunk Santas from SantaCon. I took this photo right before security came in and chased us all out.

San Francisco Santa Con
Figure 13

Appendix: R Examples

I’m putting in some example code for the graphics in this post. It’s not organized per se but it gives you an idea how to make your own plots.

library(rgl)


##
## Factory for surfaces and curves
##
cone.factory <- function(r, h, t){
    f <- function(theta, z) {
        cbind(
            r * z/h * sin(theta), 
            r * z/h * cos(theta), 
            h-z
        )
    }
    return(f)
}


##
## Factory for the parametric curve (with phase)
##
curve.factory <- function(r, h, t){
    f.curve <- function(z, phase=0){
        cbind(
            r * z/h * sin(phase + 2 * pi * z * t/h), 
            r * z/h * cos(phase + 2 * pi * z * t/h), 
            h-z
        )   
    }
    return(f.curve)
}


##
## First the regular example of a conical helix
##
r <- 24
h <- 84
t <- 6
f <- cone.factory(r, h, t)
f.curve <- curve.factory(r, h, t)
open3d()
aspect3d("iso")
par3d(windowRect = c(20, 30, 400, 700))
rgl.clear()
plot3d(
    f, 
    slim = c(0, 2 * pi), 
    tlim = c(0, h), 
    col = "#99CC99", 
    alpha = 0.5, 
    axes=FALSE,
    box=FALSE,
    aspect=FALSE,
    main="",
    sub=""
)
plot3d(
    f.curve(seq(from=0, to=h, length.out = 1000)), 
    type = "l", 
    add = TRUE, 
    lwd = 1, 
    depth_test = "always",
    depth_mask=TRUE,
    alpha=1,
    col="black"
)
rgl.viewpoint( theta = 0, phi = -90, fov = 30, zoom=0.5)
rgl.snapshot( 'images/helical-first.png', fmt = "png", top = TRUE )
rgl.close()


##
## Good spacing
##
r <- 24
h <- 84
t <- h/3
f <- cone.factory(r, h, t)
f.curve <- curve.factory(r, h, t)
open3d()
aspect3d("iso")
par3d(windowRect = c(20, 30, 400, 700))
rgl.clear()
plot3d(
    f, 
    slim = c(0, 2 * pi), 
    tlim = c(0, h), 
    col = "#99CC99", 
    alpha = 0.5, 
    axes=F,
    box=F,
    aspect=FALSE,
    main="",
    sub=""
)
plot3d(
    f.curve(seq(from=0, to=h, length.out = 1000)), 
    type = "l", 
    add = TRUE, 
    lwd = 1, 
    depth_test = "always",
    depth_mask=TRUE,
    alpha=1,
    col="black"
)
rgl.viewpoint( theta = 0, phi = -90, fov = 30, zoom=0.5)
rgl.snapshot( 'images/helical-second.png', fmt = "png", top = TRUE )
rgl.close()


##
## Closed form and Optimal Spacing Colored Lights
##
phi.factory <- function(r, h, t){
    phi <- function(z){
        2*pi*t*r*z / (h * sqrt(h^2 + r^2))
    }
    return(phi)
}
ell.factory <- function(r, h, t){
    phi <- phi.factory(r, h, t)
    phi1 <- phi(1)
    ell <- function(b){
        phib <- phi(b)
        sqrt(1+r^2/h^2) * (1 / (2*phi1)) * (
            phib*sqrt(phib^2 + 1)
            +
            log(
                phib + sqrt(phib^2 + 1)
            )
        )
    }
    return(ell)
}
ell.d1.factory <- function(r, h, t){
    ell.d1 <- function(b){
        sqrt((4 * pi * t^2 * b^2) / (h^2) + (r^2) / (h^2) + 1)
    }
}
ell <- ell.factory(r, h, t)
ell.d1 <- ell.d1.factory(r, h, t)
string.length <- ell(h)
bulb.count <- floor(string.length / 3)
z0 <- seq(from=0, to=h, length.out=bulb.count)
z1 <- rep(h, bulb.count)
delta <- 100
while(delta > 1e-4){
    z1 <- z0 - (ell(z0)-3*(1:bulb.count))/ell.d1(z0)
    delta <- max(abs(z1-z0))
    print(delta)
    z0 <- z1
}
f.curve.fudge <- function(z){
    v <- cbind(
        r * z / h * sin(2 * pi * z * t/h), 
        r * z / h * cos(2 * pi * z * t/h), 
        h - z
    )   
    l <- sqrt(v[,1]^2 + v[,2]^2)
    cbind(
        v[,1] + 0.25* v[,1] / l,
        v[,2] + 0.25* v[,2] / l,
        v[,3]
    )
}
bulb.locations <- f.curve.fudge(z0)
color.cycle <- c(
    '#ff3232',
    '#be29ec',
    '#3232ff',
    '#ffae19',
    '#198C19'
)
use.colors <- rep(color.cycle, ceiling(bulb.count/5))[1:bulb.count]
open3d()
aspect3d("iso")
par3d(windowRect = c(20, 30, 400, 700))
rgl.clear()
plot3d(
    f, 
    slim = c(0, 2 * pi), 
    tlim = c(0, h), 
    col = "#99CC99", 
    alpha = 0.8, 
    axes=F,
    box=F,
    aspect=FALSE,
    main="",
    sub=""
)
rgl.points(
    bulb.locations[,1], 
    bulb.locations[,2], 
    bulb.locations[,3], 
    color=use.colors, 
    size=5, 
    shininess=0.,
    depth_test="less",
    depth_mask=TRUE
)
rgl.viewpoint( theta = 0, phi = -90, fov = 30, zoom=0.5)
rgl.snapshot( 'images/lighted-tree-color.png', fmt = "png", top = TRUE )
rgl.close()


##
## Animate the lights!
##
library(animation)
saveGIF({
    for(i in 0:100){
        theta <- 2*pi*(100-i) / 101
        X <- bulb.locations
        v.x <- matrix(c(sin(theta), cos(theta), 0), ncol=1)
        v.y <- matrix(c(0, 0, 1), ncol=1)
        v.out <- matrix(c(cos(theta), -sin(theta), 0), ncol=1)
        idx.in <- which(X %*% v.out >= 0)
        idx.out <- which(X %*% v.out < 0)
        Y <- cbind(
            X %*% v.x,
            X %*% v.y
        )[idx.in, ]
        plot(Y, xlim=c(-28, 28), ylim=c(0, 84), col=use.colors[idx.in], pch=19, cex=3, xlab="x", ylab="y", main="Projections", cex.main=3, cex.lab=3, cex.axis=3)   
        Y <- cbind(
            X %*% v.x,
            X %*% v.y
        )[idx.out, ]
        points(Y, cex=2, col="#00000020", pch=19)
}
}, interval = 0.1, ani.width = 1500, ani.height = 2625 - 225,
movie.name = "images/animation.gif" )


##
## Three phase
##
r <- 24
h <- 84
t <- 5
f <- cone.factory(r, h, t)
f.curve <- curve.factory(r, h, t)
open3d()
aspect3d("iso")
par3d(windowRect = c(20, 30, 400, 700))
rgl.clear()
plot3d(
    f, 
    slim = c(0, 2 * pi), 
    tlim = c(0, h), 
    col = "#FFFFFF", 
    alpha = 0.75, 
    axes=F,
    box=F,
    aspect=FALSE,
    main="",
    sub="",
    lit=FALSE,
    depth_test = "less",
    depth_mask=TRUE,
)
plot3d(
    f.curve(seq(from=0, to=h, length.out = 1000), phase=0), 
    type = "l", 
    add = TRUE, 
    lwd = 1, 
    depth_test = "always",
    depth_mask=TRUE,
    alpha=1,
    col="red"
)
plot3d(
    f.curve(seq(from=0, to=h, length.out = 1000), phase=2*pi/3), 
    type = "l", 
    add = TRUE, 
    lwd = 1, 
    depth_test = "always",
    depth_mask=TRUE,
    alpha=1,
    col="#00CC00"
)
plot3d(
    f.curve(seq(from=0, to=h, length.out = 1000), phase=4*pi/3), 
    type = "l", 
    add = TRUE, 
    lwd = 1, 
    depth_test = "always",
    depth_mask=TRUE,
    alpha=1,
    col="blue"
)
rgl.viewpoint( theta = 0, phi = -90, fov = 30, zoom=0.5)
rgl.snapshot( 'images/multiphase-example.png', fmt = "png", top = TRUE )
rgl.close()