10 Interpolation Using A Route Speed Model

Based on the curvature of the route, the speed model gives us a crude estimate of how long it takes to get between two points. If the route is a flat straight between the points, we’ll travel 1km in much less time than if the route it tight and twisty.

The simple approxfun() interpolator uses a linear model to interpolate times between actual time points, but we might be able to improve interpolated estimates using a non-linear model based on a speed model generated over the route based on the route curvature.

  • identify distance into stage required (desired_d)
  • find distance into stage for each
  • get consecutive telemetry points between which desired distance lays (in_d, out_d, in_t, out_t)
> l = c(1.1,2.1,3.1,4.1,5.1)
> c(l[findInterval(3, l)], l[findInterval(3, l)+1])
[1] 2.1 3.1
> c(l[findInterval(1, l)], l[findInterval(1, l)+1])
[1] 1.1
> c(l[findInterval(11, l)], l[findInterval(11, l)+1])
[1] 5.1  NA
  • get speed model
  • get model time at points (in_mt=model(in_d), out_mt=model(out_d), desired_mt=model(desired_d))
  • get interpolated normalised model time between points, desired_nt = (desired_mt - in_mt)/(out_mt - in_mt)
  • get predicted time desired_t = desired_nt*(out_t - in_t) + in_t

10.1 Creating a Route Speed Model

In the To See The Invisible rally pacenotes tutorial series by David Nafría, corners are given a particular severity based on curvature. Simple models of expected speeds for corners of a particular severity are also described for different classes of rally car.

We can use this approach to create a simple speed model based on curvature of the rally route. Using the expected cornering speed as a target speed and a simple acceleration model, as well as theoretical maximum speed, we can accelerate the car into and out of each corner based and generate a speed model as a result.

For more on analysing and visualising rally stage routes, see Visualising WRC Rally Stages With rayshader and R.

The rLFT processing linear features R package provides a handy tool for modeling curvature along each point of a route in the form of a boundary convexity tool (bct()). The curvature of a moving segment along the route is determined, along with the center of curvature and a convexity measure.

library(rLFT)

# The step dist is how far we move the window at each step
stepdist = 10
# The window is the length of the route for which we find the curvature
window = 20
get_route_convexity = function(route_basis_utm, stepdist=10, window=20){
  bct(route_basis_utm,
      # distance between measurements 
      step = stepdist,
      window = window, ridName = "name") %>%
  mutate(dist =  (lead(MidMeas)-MidMeas),
         cum_dist = cumsum(dist))
}

route_convexity = get_route_convexity(stage_route_utm)
##    user  system elapsed 
##   0.180   0.033   0.214 
## [1] "Features skipped due to size: "
## logical(0)
head(route_convexity, 3)
##   FID                   RID MidMeas WindowSize RawConvexity ConvexityIndex
## 1   1 SS7/11 KakaristoHassi      10         20        0.000          0.000
## 2   1 SS7/11 KakaristoHassi      20         20        0.000          0.000
## 3   1 SS7/11 KakaristoHassi      30         20        0.788          0.079
##   Sinuosity Midpoint_X Midpoint_Y dist cum_dist
## 1     0.500   400689.0    6857086   10       10
## 2     0.500   400698.6    6857084   10       20
## 3     0.503   400708.3    6857081   10       30

Although the boundary convexity tool gives us a convexity measure, we can also create our own curvature metric that corresponds more closely to Nafría’s model. I don’t know how to write vectorised functions properly, so I’ll create a simple function that generates the curvature at a particular point on a route, and then use the Vectorize() helper function to as-if vectorise it for me.

library(devtools)
# The curvature function takes an arc defined over
# x and y coordinate lists

#circlefit, from pracma::
circlefit = function (xp, yp, fast = TRUE) 
{
    if (!is.vector(xp, mode = "numeric") || !is.vector(yp, mode = "numeric")) 
        stop("Arguments 'xp' and 'yp' must be numeric vectors.")
    if (length(xp) != length(yp)) 
        stop("Vectors 'xp' and 'yp' must be of the same length.")
    if (!fast) 
        warning("Option 'fast' is deprecated and will not be used!", 
            call. = FALSE, immediate. = TRUE)
    n <- length(xp)
    p <- qr.solve(cbind(xp, yp, 1), matrix(xp^2 + yp^2, ncol = 1))
    v <- c(p[1]/2, p[2]/2, sqrt((p[1]^2 + p[2]^2)/4 + p[3]))
    rms <- sqrt(sum((sqrt((xp - v[1])^2 + (yp - v[2])^2) - v[3])^2)/n)
    #cat("RMS error:", rms, "\n")
    return(v)
}

curvature = function(x,y){
  #729181.8, 729186.1, 729190.4
  #4957667 , 4957676, 4957685
  tryCatch({
      # circlefit gives an error if we pass a straight line
      # Also hide the print statement in circlefit
      # circlefit() returns the x and y coords of the circle center
      # as well as the radius of curvature
      # We could then also calculate the angle and arc length
      circlefit(x,y)[3]
    },
    error = function(err) { 
      # For a straight, return the first co-ord and Inf diameter
      # Alternatively, pass zero diameter?
      c(x[1], y[1], Inf)[3]})
}

curvature2 = function(x1, x2, x3, y1, y2, y3){
  curvature(c(x1, x2, x3), c(y1, y2, y3))
}

# The base::Vectorize function provides a lazy way of 
# vectorising a non-vectorised function
curvatures_ = Vectorize(curvature2)

curvatures = function(route_convexity){
  curvatures_(lag(route_convexity$Midpoint_X),
              route_convexity$Midpoint_X,
              lead(route_convexity$Midpoint_X),
              lag(route_convexity$Midpoint_Y),
              route_convexity$Midpoint_Y,
              lead(route_convexity$Midpoint_Y))
}

This model uses the corner centre measures calculated by the boundary convexity tool to return a radius for the curvature of each segment:

route_convexity$radius = curvatures(route_convexity)

We can now generate the cornering speed model. The corner speed model generates several things:

  • a corner index, invisble_ci, which is an integer representing the corner radius;
  • a notional segment/corner target speed, invisible_sp.
corner_speed_model = function(route_convexity,
                               invisible_speeds = c(20, 40, 50, 60, 70, 85,
                                                    100, 115, 120, 130, 170),
                               speed_modifier = 0){
  
  # Provide a simple means of increasing the cornering speeds
  invisible_speeds = invisible_speeds + speed_modifier
  
  invisible_bins = c(0, 10, 15, 20, 27.5, 35,
                    45, 60, 77.5, 100, 175, Inf)

  route_convexity$invisible_ci = cut(route_convexity$radius,
                                     breaks = invisible_bins,
                                     labels = 1:(length(invisible_bins)-1),
                                     ordered_result=TRUE)
  
  # Speeds in km/h
  #invisible_speeds = c(10, 40, 50, 60, 70, 80,
  #                     95, 110, 120, 130, 180)
 
  
  
  route_convexity$invisible_sp = cut(route_convexity$radius,
                                     breaks = invisible_bins,
                                     labels = invisible_speeds,
                                     ordered_result=TRUE)
  
  # Cast speed as factor, via character, to integer
  route_convexity$invisible_sp = as.integer(as.character(route_convexity$invisible_sp))
  
  route_convexity
}

Applying the speed model to our route gives us a corner index and notional target speed for each segment:

route_convexity = route_convexity %>% corner_speed_model()

head(route_convexity, 3)
##   FID                   RID MidMeas WindowSize RawConvexity ConvexityIndex
## 1   1 SS7/11 KakaristoHassi      10         20        0.000          0.000
## 2   1 SS7/11 KakaristoHassi      20         20        0.000          0.000
## 3   1 SS7/11 KakaristoHassi      30         20        0.788          0.079
##   Sinuosity Midpoint_X Midpoint_Y dist cum_dist radius invisible_ci
## 1     0.500   400689.0    6857086   10       10    Inf           11
## 2     0.500   400698.6    6857084   10       20    Inf           11
## 3     0.503   400708.3    6857081   10       30    Inf           11
##   invisible_sp
## 1          170
## 2          170
## 3          170

We can build up the speed model for the route. At each step we accelerate towards the nominal sector target speed (the invisible_sp value). We can’t accelerate infinitely fast, so our actual target accumulated speed for the segment, acc_sp, is a simple function of the current speed and the notional target speed. We can then calculate the notional time to complete that segment, invisible_time.

acceleration_model = function(route_convexity, stepdist=10,
                               acc = 0.1, dec = 0.1) {
  # Acceleration model
  sp = route_convexity$invisible_sp
  # Nominal starting target speed
  # In we don't set this, we don't get started moving
  sp[1] = 30 
  
  # Use crude acceleration / brake weights
  for (i in 2:(length(sp)-1)) {
    # Simple linear model - accumulated speed is based on
    # the current speed and the notional segment speed
    # Accelerate up
    if (sp[i-1]<=sp[i]) sp[i] = (sp[i-1] + acc * sp[i]) / (1+acc)
                                    
    # Decelerate down
    if (sp[i]>sp[i+1]) sp[i] = (dec * sp[i] + sp[i+1]) / (1+dec)
  }
  
  route_convexity$acc_sp = sp
  route_convexity$acc_sp[length(sp)] = route_convexity$invisible_sp[length(sp)]
  
  # New time model
  # Also get speed in m/s for time calculation
  meters = 1000
  seconds_per_hour = 3600 # 60 * 60
  kph_unit = meters / seconds_per_hour
  route_convexity = route_convexity %>% 
                      mutate(segment_sp = route_convexity$acc_sp * kph_unit,
                             invisible_time = dist/segment_sp,
                             acc_time = cumsum(invisible_time))

  
  # So now we need to generate kilometer marks
  route_convexity$kmsection = 1 + trunc(route_convexity$MidMeas/1000)
  # We can use this to help find the time over each km
  
  route_convexity
}

If we now apply the acceleration model to the route, we can calculate the speed over each segment, and the time taken to complete the segment:

route_convexity = acceleration_model(route_convexity)

head(route_convexity, 3)
##   FID                   RID MidMeas WindowSize RawConvexity ConvexityIndex
## 1   1 SS7/11 KakaristoHassi      10         20        0.000          0.000
## 2   1 SS7/11 KakaristoHassi      20         20        0.000          0.000
## 3   1 SS7/11 KakaristoHassi      30         20        0.788          0.079
##   Sinuosity Midpoint_X Midpoint_Y dist cum_dist radius invisible_ci
## 1     0.500   400689.0    6857086   10       10    Inf           11
## 2     0.500   400698.6    6857084   10       20    Inf           11
## 3     0.503   400708.3    6857081   10       30    Inf           11
##   invisible_sp   acc_sp segment_sp invisible_time acc_time kmsection
## 1          170 30.00000   8.333333      1.2000000 1.200000         1
## 2          170 42.72727  11.868687      0.8425532 2.042553         1
## 3          170 54.29752  15.082645      0.6630137 2.705567         1

Summing over the segment times (omitting the flying finish which has no specified ongoing segment length) gives us an estimated stage time

anticipated_time = function(route_convexity) {
  anticipated_time = sum(route_convexity$invisible_time[1:nrow(route_convexity)-1])
  cat(paste0("Anticipated stage time: ", anticipated_time %/% 60,
           'm ', round(anticipated_time %% 60, 1), 's' ))
}

anticipated_time(route_convexity)
## Anticipated stage time: 9m 11.2s

We can also view the speed model over distance into stage:

ggplot(route_convexity) + geom_line(aes(x=cum_dist, y=acc_sp))

We can also plot the time model into the stage as the accumulated time:

ggplot(route_convexity) + geom_line(aes(x=cum_dist, y=acc_time))