5 min read

Estimating Pi using a lightsaber battle

Happy Pi Day everyone!!

Overview: Today is 3-14, and those digits are the first three digits of Pi. To celebrate, I estimated Pi using a lightsaber battle in R. This is a fun math demonstration where we first build an army of saber-wielding critters in the park and then cut transects across the battlefield and count the number of contacts we make with lightsabers along the way. After the battle, we take a weighted ratio of hits vs. misses to estimate Pi.

Our adventure begins in a futuristic world where urban wildlife have aquired lightsabers and taken over Montreal.

Future squirels battle for food scraps near homeless man's dome tent.

Figure 1: Future squirels battle for food scraps near homeless man’s dome tent.

We are about to join the battle. It is probably a good time to learn how lightsabers work. We represent a lightsaber with a line, meaning that we assume the saber has no width. We describe that line using xy coordinates in a matrix.

set.seed(1.1)
light_saber_length <- 20
number_of_animal_sabers_in_a_park <- 5000
distance_between_transects <- 40.1

point_1 <- c(-(light_saber_length/2),(light_saber_length/2))
point_2 <- c(0,0)
single_saber <-cbind(point_1, point_2)
rownames(single_saber)<- c("point_1","point_2")
colnames(single_saber)<- c("x","y")
single_saber
##           x y
## point_1 -10 0
## point_2  10 0
Represent our lightsaber as a line with no width. A single beam of light.

Figure 2: Represent our lightsaber as a line with no width. A single beam of light.

To move the object, we multiply that matrix by transformation matrices to rotate and move the object around. We rotate the object by multiplying the object matrix by the rotation matrix specifying a random angle.

  angle <- runif(1, min=1, max=360)*pi/180
  xy <- as.matrix(single_saber)
  
  # Rotation
  cos.angle <- cos(angle)
  sin.angle <- sin(angle)
  after_rotation <- xy %*% t(matrix(c(cos.angle, sin.angle, -sin.angle, 
        cos.angle), 2, 2))
  
after_rotation
##              [,1]      [,2]
## point_1  1.100398 -9.939272
## point_2 -1.100398  9.939272
We rotate the lightsaber around the origin to change its angle.

Figure 3: We rotate the lightsaber around the origin to change its angle.

We then move the saber to a location the battlefield. Note: order is important. We must always rotate around the origin first before moving into place on the landscape. It does not work the other way.

  # Translation
  translation.x <- runif(1, min=10, max=90)
  translation.y <- runif(1, min=10, max=90)
  translation <- matrix(
    c(translation.x,translation.x,translation.y,translation.y), 2, 2)
  new_coord <- after_rotation + translation
  colnames(new_coord) <- c("x","y")
  rownames(new_coord) <- c("Point_1","Point_2")
After the angle is set, translate the lightsaber to a random location on the battlefield.

Figure 4: After the angle is set, translate the lightsaber to a random location on the battlefield.

Create a function to rotate and distribute lightsabers to all the critters on the battlefield.

critter_saber <- function(){
  single_saber <-cbind(c(-5,5),c(0,0))
  angle <- runif(1, min=1, max=360)*pi/180
  xy <- as.matrix(single_saber)
  
  # Rotation
  cos.angle <- cos(angle)
  sin.angle <- sin(angle)
  after_rotation <- xy %*% matrix(c(cos.angle, sin.angle, -sin.angle, 
        cos.angle), 2, 2, byrow=TRUE )
  
  # Translation
  translation.x <- runif(1, min=10, max=90)
  translation.y <- runif(1, min=10, max=90)
  translation <- matrix(
    c(translation.x,translation.x,translation.y,translation.y), 2, 2)
  new_coord <- after_rotation + translation
  colnames(new_coord) <- c("x","y")
  rownames(new_coord) <- c("Point_1","Point_2")
  
  return(new_coord)   
}

critter_saber()
##                x        y
## Point_1 21.93899 84.59096
## Point_2 30.33012 79.15139

Repeat it many times to create an army of lightsaber-wielding critters. We assume that all the critters are battling vigorously and indiscriminately with each other so they are randomly distributed across space and the angles of all the sabers are uniformly distributed.

for(i in 1:number_of_animal_sabers_in_a_park){
critter_saber()
}
An army of lightsaber-wielding critters in the middle of the battlefield. This is the position of each critter's lightsaber at the moment we pass them on our transect.

Figure 5: An army of lightsaber-wielding critters in the middle of the battlefield. This is the position of each critter’s lightsaber at the moment we pass them on our transect.

We better get in there! Things aren't looking good.

Figure 6: We better get in there! Things aren’t looking good.

surface_line <- function(line_height = 0){
  matrix(c(0,100,line_height,line_height), 2, 2, byrow=FALSE)
  }
##      [,1] [,2]
## [1,]    0   20
## [2,]  100   20
My transect through the army of critters.

Figure 7: My transect through the army of critters.

one_saber <- critter_saber()
one_surface_line <- surface_line(30)
line.line.intersection(one_saber[1,], one_saber[2,], one_surface_line[1,], 
                       one_surface_line[2,], interior.only = TRUE)
## [1] 75.73794 30.00000
Build a mechanism to identify when a lightsaber has crossed my transect line.

Figure 8: Build a mechanism to identify when a lightsaber has crossed my transect line.

Team prepares for transects

Figure 9: Team prepares for transects

Prepare a list of all transects and all critter sabers so we can calculate contacts between them

list_of_matches <- array(NA,dim = c(2,2,number_of_animal_sabers_in_a_park))
list_of_surface_lines <- array(NA,dim = c(2,2,6))

for(i in 1:number_of_animal_sabers_in_a_park){
  list_of_matches[,,i] <- critter_saber()
}

line_placement <- seq(0,100, by=20)
for(i in 1:6){
  list_of_surface_lines[,,i] <- surface_line(line_placement[i])
}

The battle is on! We rush along our transects and dual with every lightsaber that crosses our path. We are so skilled that we hit every lightsaber to cross our transect but never receive a hit or hit anything off of our transect line.

The battle is on. We charge across the battlefield and hit every critter saber along our transect.

Figure 10: The battle is on. We charge across the battlefield and hit every critter saber along our transect.

The battle is over! How did we do? We better count up all our hits and compare them to our misses.

crosses <- matrix(NA, 1, 10, byrow=TRUE)

for(i in 1:dim(list_of_matches)[3]){
for(j in 1:dim(list_of_surface_lines)[3]){
  crosses <- rbind(crosses, c(
    line.line.intersection(list_of_matches[1,,i], list_of_matches[2,,i], 
                           list_of_surface_lines[1,,j],     
    list_of_surface_lines[2,,j], interior.only = TRUE), 
    
    list_of_matches[1,,i], list_of_matches[2,,i], list_of_surface_lines[1,,j], 
    list_of_surface_lines[2,,j])
    )
  }
}

crosses <- crosses[which(is.na(crosses[,1]) == FALSE),]
colnames(crosses) <- c("x.cross", "y.cross", "x.p1", "y.p1", "x.p2", 
                    "y.p2", "x.surface.p1", "y.surface.p1", "x.surface.p2", 
                    "y.surface.p2")
head(crosses)
##       x.cross y.cross     x.p1     y.p1     x.p2     y.p2 x.surface.p1
## [1,] 63.08661      20 64.26028 15.53502 61.71804 25.20647            0
## [2,] 14.37017      80 12.61642 83.36074 17.24275 74.49524            0
## [3,] 49.44207      80 44.92515 85.23335 51.45904 77.66313            0
## [4,] 47.22221      20 49.37535 17.30391 43.13500 25.11787            0
## [5,] 24.43356      20 23.31048 15.67796 25.82545 25.35654            0
## [6,] 56.12789      20 53.77911 24.62028 58.31078 15.70602            0
##      y.surface.p1 x.surface.p2 y.surface.p2
## [1,]           20          100           20
## [2,]           80          100           80
## [3,]           80          100           80
## [4,]           20          100           20
## [5,]           20          100           20
## [6,]           20          100           20
All the lightsabers we struck on our transects.

Figure 11: All the lightsabers we struck on our transects.

We take the ratio of the lightsabers we made contact with against the ones we missed, scaled by distance, and we get a great approximation of Pi!

pi_estimate <- (2* light_saber_length * number_of_animal_sabers_in_a_park)/(distance_between_transects * length(crosses[,1]))
options(digits=20)
pi_estimate
## [1] 3.1427417593382642735

Happy Pi Day!