Riddler: Can You Roll The Good Bowl?

[This article was first published on Posts | Joshua Cook, and kindly contributed to R-bloggers]. (You possibly can report problem in regards to the content material on this web page right here)


Wish to share your content material on R-bloggers? click on right here you probably have a weblog, or right here when you do not.

FiveThirtyEight’s Riddler Specific

hyperlink

On the current World Indoor Bowls Championships in Nice Yarmouth,
England, one of many rolls by Nick Brett went viral. Right here it’s in all
its glory:

To ensure that Nick’s inexperienced bowl to separate the 2 pink bowls, he wanted
knowledgeable precision in each the velocity of the roll and its closing angle of
method.

Suppose you have been standing in Nick’s sneakers, and also you needed to separate two
of your opponent’s bowls. Let’s simplify the maths slightly, and say
that every bowl is a sphere with a radius of 1. Let’s additional suppose
that your opponent’s two pink bowls are separated by a distance of three —
that’s, the facilities of the pink bowls are separated by a distance of
5. Outline phi because the angle between the trail your bowl is on and the
line connecting your opponent’s bowls.

For instance, right here’s how you could possibly cut up your opponent’s bowls when phi
is 75 levels:

What’s the minimal worth of phi that may permit your bowl to separate
your opponents’ bowls with out hitting them?

Plan

I’ll approximate the answer to this puzzle by simulating the sport
from many various angles. Fortunately, as a result of the sport is vertically
and horizontally symmetric, I solely must simulate the inexperienced ball
reaching the center level between the pink balls and solely must see if
it collides with the highest pink ball.

Setup

knitr::opts_chunk$set(echo = TRUE, remark = "#>", cache = TRUE)
library(glue)
library(clisymbols)
library(ggforce)
library(gganimate)
library(tidyverse)
theme_set(theme_minimal())
# Some customary colours used all through
inexperienced <- "#54c761"
pink <- "#c75454"
purple <- "#a06bdb"
light_grey <- "gray70"
gray <- "gray40"
set.seed(0)

Simulate a single cross

I cut up the code into two items. The primary simulates a bowl with a
given angle, and the second decides on the angle to slender down the
approximation. The next features care for the primary half:
simulating a bowl.

A single simulation will be run by calling run_bowl_simulation() with
an angle (in levels). The operate works by altering the hypotenuse,
beginning with h_start = 5 and lowering it to Zero by step_size steps
(the steps are held within the numeric vector h_vals). The precise place
of the ball is calculated from the size of the hypotenuse and angle
with a little bit of trigonometry in make_green_ball(). For every hypotenuse
worth, the inexperienced ball is positioned after which examined to see if it
collides with the pink ball (set at ((x,y) = (0,2.5)) as per the
riddle) utilizing the operate did_balls_collide(). This info is
recorded by constructing a single knowledge body with the info for every step of
the simulation. The information body is returned on the finish of the simulation.

# Run a simulation of the bowling sport.
run_bowl_simulation <- operate(angle,
step_size = 0.1,
red_ball_loc = checklist(x = 0, y = 2.5)) {
h_start <- 5
h_vals <- seq(h_start, 0, by = -step_size)
angle <- angle * (pi / 180)
all_ball_pos <- NULL
for (h in h_vals) {
green_ball <- make_green_ball(h, angle)
collision <- did_balls_collide(green_ball, red_ball_loc, radius = 1)
all_ball_pos <- bind_rows(
all_ball_pos,
tibble(h = h,
x = green_ball$x,
y = green_ball$y,
collision = collision)
)
}
return(all_ball_pos)
}
# Make a inexperienced ball location from the x-position and angle.
make_green_ball <- operate(h, angle) {
x <- -1 * h * cos(pi/2 - angle)
y <- h * sin(pi/2 - angle)
checklist(x = x, y = y)
}
# Determine wether the 2 balls of radius `r` collided.
did_balls_collide <- operate(ball1, ball2, radius) {
d <- sqrt((ball1$x - ball2$x)^2 + (ball1$y - ball2$y)^2)
return(d <= 2*radius)
}

Beneath are the outcomes from operating the simulation at angles between 90
levels (horizontal) and Zero levels (vertical) at 10 diploma increments.
Every line is a person simulation, and every level is a spherical of the
simulation. A pink ball is positioned as per the riddle, and the purple
factors point out the place the inexperienced ball would collide with the pink ball.
These instance simulations present that the run_bowl_simulation() operate
is working as anticipated.

map(seq(90, 0, -10), run_bowl_simulation, step_size = 0.1) %>%
map2(seq(90, 0, -10), ~ .x %>% add_column(angle = .y)) %>%
bind_rows() %>%
mutate(collision = ifelse(collision, "collision", "protected")) %>%
ggplot() +
geom_point(aes(x, y, shade = collision), measurement = 2) +
geom_circle(aes(x0 = x0, y0 = y0, r = r),
knowledge = tibble(x0 = 0, y0 = 2.5, r = 1),
shade = pink, fill = pink, alpha = 0.5) +
scale_color_manual(values = c(purple, light_grey)) +
coord_fixed() +
theme(
legend.place = c(0.15, 0.9),
legend.title = element_blank()
) +
labs(x = "x", y = "y",
title = "Instance paths of the inexperienced ball",
subtitle = "For the angles between Zero and 90 at 10 diploma intervals.")

Discover the smallest angle

The second a part of the code is to search out the smallest (narrowest) angle at
which there is no such thing as a collision. As an alternative of attempting each angle between 90
levels and Zero levels at some very small increment, I method this
downside a bit extra effectively. I constructed an algorithm than begins at 90
levels and takes giant steps till there may be an angle that causes a
collision. It then takes a step again an tries once more with a progressively
smaller step, till it not collides. This continues with the step
measurement getting smaller and smaller. The algorithm stops when the step measurement
is sufficiently small for approximation and the angle doesn't trigger a
collision. The code chunk beneath carries out this course of, printing the
info for every cross.

The aim of the angle and previous_angle parameters are pretty
apparent. The angle_delta parameter is the worth by which the angle is
decreased at every step. epsilon is used to cut back angle_delta when
there are collisions at an angle. Lastly, min_angle_delta is one in all
the stopping standards: when angle_delta will get beneath this worth, the
algorithm is sufficiently near the proper reply and it stops
attempting new angles. Thus, this parameter determines the precision of the
algorithm.
It's set comparatively excessive for now, as a result of this primary cross
is only a demonstration and prints out the outcomes of every iteration.

For effectivity, the whereas loop makes use of a memoised model of
run_bowl_simulation() as a result of when the balls collide, the earlier
step is tried once more. Due to this fact, memoising the operate saves a while
as a substitute of operating the simulation from the identical angle a number of occasions.

# The beginning angle.
angle <- 90
previous_angle <- angle
# The "studying fee" paramerters.
angle_delta <- 10
epsilon <- 0.5
min_angle_delta <- 0.01
# Begin with TRUE, although it does not matter.
collision <- TRUE
memo_bowl_sim <- memoise::memoise(run_bowl_simulation)
while (angle_delta >= min_angle_delta | collision) {
# Run the bowling simulation with the present angle.
sim_res <- memo_bowl_sim(angle = angle, step_size = 0.1)
# Have been there any collisions?
collision <- any(sim_res$collision)
# Print outcomes
msg <- "collision: {ifelse(collision, symbol$cross, symbol$tick)}" %>%
paste("{collision},") %>%
paste("angle: {spherical(angle, 4)},") %>%
paste("angle_delta: {spherical(angle_delta, 4)}")
print(glue(msg))
if (!collision) {
# Scale back the angle if there is no such thing as a collision.
previous_angle <- angle
angle <- angle - angle_delta
} else {
# Revert to the earlier angle and scale back delta if there's a collision.
angle_delta <- epsilon * angle_delta
angle <- previous_angle
}
}
#> collision: ✔ FALSE, angle: 90, angle_delta: 10
#> collision: ✔ FALSE, angle: 80, angle_delta: 10
#> collision: ✔ FALSE, angle: 70, angle_delta: 10
#> collision: ✔ FALSE, angle: 60, angle_delta: 10
#> collision: ✖ TRUE, angle: 50, angle_delta: 10
#> collision: ✔ FALSE, angle: 60, angle_delta: 5
#> collision: ✔ FALSE, angle: 55, angle_delta: 5
#> collision: ✖ TRUE, angle: 50, angle_delta: 5
#> collision: ✔ FALSE, angle: 55, angle_delta: 2.5
#> collision: ✖ TRUE, angle: 52.5, angle_delta: 2.5
#> collision: ✔ FALSE, angle: 55, angle_delta: 1.25
#> collision: ✔ FALSE, angle: 53.75, angle_delta: 1.25
#> collision: ✖ TRUE, angle: 52.5, angle_delta: 1.25
#> collision: ✔ FALSE, angle: 53.75, angle_delta: 0.625
#> collision: ✖ TRUE, angle: 53.125, angle_delta: 0.625
#> collision: ✔ FALSE, angle: 53.75, angle_delta: 0.3125
#> collision: ✔ FALSE, angle: 53.4375, angle_delta: 0.3125
#> collision: ✖ TRUE, angle: 53.125, angle_delta: 0.3125
#> collision: ✔ FALSE, angle: 53.4375, angle_delta: 0.1562
#> collision: ✔ FALSE, angle: 53.2812, angle_delta: 0.1562
#> collision: ✖ TRUE, angle: 53.125, angle_delta: 0.1562
#> collision: ✔ FALSE, angle: 53.2812, angle_delta: 0.0781
#> collision: ✔ FALSE, angle: 53.2031, angle_delta: 0.0781
#> collision: ✖ TRUE, angle: 53.125, angle_delta: 0.0781
#> collision: ✔ FALSE, angle: 53.2031, angle_delta: 0.0391
#> collision: ✔ FALSE, angle: 53.1641, angle_delta: 0.0391
#> collision: ✖ TRUE, angle: 53.125, angle_delta: 0.0391
#> collision: ✔ FALSE, angle: 53.1641, angle_delta: 0.0195
#> collision: ✔ FALSE, angle: 53.1445, angle_delta: 0.0195
#> collision: ✖ TRUE, angle: 53.125, angle_delta: 0.0195
#> collision: ✔ FALSE, angle: 53.1445, angle_delta: 0.0098

From the print-out above, we will see how the algorithm jumps again an
forth, narrowing in on an answer round 53 levels.

With that profitable proof-of-concept, the next code runs the
algorithm with a smaller min_angle_delta = 1e-5 to attain larger
precision. As an alternative of printing out the outcomes of every iteration, the
simulation outcomes and parameters are saved to sim_results_tracker and
sim_parameters_tracker, respectively, and are inspected beneath.

angle <- 90
previous_angle <- angle
angle_delta <- 10
epsilon <- 0.7
min_angle_delta <- 1e-5
collision <- TRUE
sim_results_tracker <- tibble()
sim_parameters_tracker <- tibble()
memo_bowl_sim <- memoise::memoise(run_bowl_simulation)
while (angle_delta >= min_angle_delta | collision) {
sim_res <- memo_bowl_sim(angle = angle, step_size = 0.01)
collision <- any(sim_res$collision)
sim_results_tracker <- bind_rows(sim_results_tracker,
sim_res %>% add_column(angle = angle))
sim_parameters_tracker <- bind_rows(sim_parameters_tracker,
tibble(angle, angle_delta,
collision, epsilon))
if (!collision) {
previous_angle <- angle
angle <- angle - angle_delta
} else {
angle_delta <- epsilon * angle_delta
angle <- previous_angle
}
}

The simulation took 89 steps. The plot beneath exhibits the angle and
angle_delta at every step, coloured by whether or not there was a collision or
not.

sim_parameters_tracker %>%
mutate(row_idx = row_number()) %>%
pivot_longer(-c(row_idx, epsilon, collision)) %>%
ggplot(aes(x = row_idx, y = worth)) +
facet_wrap(~ identify, nrow = 2, scales = "free") +
geom_point(aes(shade = collision), measurement = 0.9) +
scale_color_manual(values = c(light_grey, purple)) +
labs(x = "iteration quantity",
y = "worth",
title = "Simulation parameters")

The next plot exhibits every of the paths tried, once more, coloring the
areas of collisions in purple.

sim_results_tracker %>%
mutate(collision = ifelse(collision, "collision", "protected")) %>%
ggplot() +
geom_point(aes(x = x, y = y, shade = collision),
measurement = 0.1) +
scale_color_manual(values = c(collision = purple,
protected = light_grey)) +
coord_fixed() +
theme(legend.place = "none") +
labs(x = "x",
y = "y",
title = "Paths of the inexperienced ball",
subtitle = "Factors marked in purple have been collisions with the pink ball.")

Lastly, we will discover the approximated angle by taking the smallest angle
tried within the rounds of simulation that didn't have any collisions.

smallest_angle <- sim_parameters_tracker %>%
filter(collision == FALSE) %>%
top_n(1, wt = -angle) %>%
pull(angle) %>%
distinctive()

The algorithm approximates the answer to be: 53.1301 levels (0.9273
in radians).

The simulation with this angle is proven in an animated plot beneath.

final_result <- sim_results_tracker %>%
filter(angle == smallest_angle) %>%
mutate(row_idx = row_number()) %>%
filter(row_idx == 1)
bind_rows(
final_result,
final_result %>%
mutate(x = -1 * x, y = -1 * y)
) %>%
mutate(row_idx = row_number()) %>%
ggplot() +
geom_point(aes(x = x, y = y),
shade = inexperienced, measurement = 2) +
geom_circle(aes(x0 = x, y0 = y, r = 1),
fill = inexperienced, alpha = 0.2, measurement = 0) +
geom_point(aes(x, y),
knowledge = tibble(x = 0, y = 2.5),
shade = pink, measurement = 2) +
geom_circle(aes(x0 = x, y0 = y, r = r),
knowledge = tibble(x = 0, y = 2.5, r = 1),
fill = pink, alpha = 0.2, measurement = 0) +
geom_point(aes(x, y),
knowledge = tibble(x = 0, y = -2.5),
shade = pink, measurement = 2) +
geom_circle(aes(x0 = x, y0 = y, r = r),
knowledge = tibble(x = 0, y = -2.5, r = 1),
fill = pink, alpha = 0.2, measurement = 0) +
coord_fixed() +
labs(
x = "x",
y = "y",
title = glue( "The tightest angle of the proper bowl: {spherical(smallest_angle, 3)} deg."
)) +
transition_states(row_idx, transition_length = 2,
state_length = 0, wrap = FALSE) +
ease_aes("sine-in-out")


Acknowledgements

Repetitive duties have been sped up utilizing the
‘memoise’ package deal for
memoization. Plotting was completed utilizing
‘ggplot2’,
‘ggforce’, and
‘gganimate’.

To go away a remark for the writer, please comply with the hyperlink and touch upon their weblog: Posts | Joshua Prepare dinner.

R-bloggers.com affords day by day e-mail updates about R information and tutorials about studying R and lots of different matters. Click on right here when you're trying to submit or discover an R/data-science job.


Wish to share your content material on R-bloggers? click on right here you probably have a weblog, or right here when you do not.

Leave a Reply

Your email address will not be published. Required fields are marked *