Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Driver stats

livebooks/driver_stats.livemd

Driver stats

Run the node

iex --cookie livebook --sname f1bot -S mix backtest --url "http://livetiming.formula1.com/static/2021/2021-12-05_Saudi_Arabian_Grand_Prix/2021-12-05_Race"
alias Timex.Duration
alias F1Bot.F1Session.Common.TimeSeriesStore

Fetch stats

drivers = [
  # Abu Dhabi Quali
  # {"VER @ 16", "33", 16}, 
  # {"VER @ 19", "33", 19}, 
  # {"HAM @ 18", "44", 18},
  # {"HAM @ 15", "44", 15},

  {"HAM", "44", 12},
  {"VER", "33", 12}
]

stats =
  for {abbr, num, lap} <- drivers do
    {:ok, stats} = F1Bot.driver_session_data(num)

    {
      abbr,
      %{
        laps: stats.laps,
        telemetry: stats.telemetry_hist,
        position: stats.position_hist,
        lap_number: lap
      }
    }
  end
  |> Enum.into(%{})
lap_data =
  for {abbr, data} <- stats do
    lap =
      data
      |> get_in([Access.key(:laps), Access.key(:data)])
      |> Enum.find(fn x -> x.number == data.lap_number end)

    time_from = Duration.sub(lap.timestamp, lap.time)
    time_to = lap.timestamp

    telemetry =
      stats[abbr].telemetry
      |> TimeSeriesStore.find_samples_between(time_from, time_to)

    position =
      stats[abbr].position
      |> TimeSeriesStore.find_samples_between(time_from, time_to)

    {abbr,
     %{
       time_from: time_from,
       time_to: time_to,
       time: lap.time,
       telemetry: telemetry,
       position: position
     }}
  end
  |> Enum.into(%{})

Plot position

{min_y, max_y, min_x, max_x} =
  lap_data
  |> Map.values()
  |> List.first()
  |> Map.get(:position)
  |> Enum.reduce(
    {nil, nil, nil, nil},
    fn %{x: x, y: y}, {min_y, max_y, min_x, max_x} ->
      if min_y == nil do
        {y, y, x, x}
      else
        min_y = min(min_y, y)
        max_y = max(max_y, y)
        min_x = min(min_x, x)
        max_x = max(max_x, x)

        {min_y, max_y, min_x, max_x}
      end
    end
  )

{min_y, max_y, min_x, max_x} = {min_y / 10, max_y / 10, min_x / 10, max_x / 10}

# Get a square chart
max_range = max(max_y - min_y, max_x - min_x)
max_y = min_y + max_range
max_x = min_x + max_range

padding = 50

y_range = [min_y - padding, max_y + padding]
x_range = [min_x - padding, max_x + padding]

alias VegaLite, as: Vl

size = 1000

widget =
  Vl.new(width: size, height: size)
  |> Vl.mark(:point)
  |> Vl.encode_field(:x, "x", title: "X (meters)", type: :quantitative, scale: [domain: x_range])
  |> Vl.encode_field(:y, "y", title: "Y (meters)", type: :quantitative, scale: [domain: y_range])
  |> Vl.encode_field(:color, "Driver", type: :nominal)
  |> Vl.encode_field(:shape, "Driver", type: :nominal)
  |> Kino.VegaLite.new()
  |> Kino.render()

display = [
  # "VER @ 16",
  # "VER @ 19",
  "VER @ 45",
  "HAM @ 45"
]

for {abbr, _num} <- lap_data, abbr in display do
  for %{x: x, y: y} <- lap_data[abbr].position |> Enum.slice(1..2000) do
    point = %{x: x / 10, y: y / 10, Driver: abbr}
    Kino.VegaLite.push(widget, point)
  end
end

:ok

Plot speed / acceleration

integrated =
  for {abbr, _data} <- lap_data do
    first_ts = lap_data[abbr].time_from
    [%{speed: first_speed} | _] = lap_data[abbr].telemetry

    {_total_distance, _last_ts, _last_speed, points} =
      lap_data[abbr].telemetry
      |> Enum.reduce(
        {0, first_ts, first_speed, []},
        fn point = %{timestamp: ts, speed: speed},
           {total_distance, last_ts, last_speed, points} ->
          delta_ms =
            ts
            |> Duration.sub(last_ts)
            |> Duration.to_milliseconds()

          distance_m = delta_ms * speed / 3.6 / 1000

          delta_speed = (speed - last_speed) / 3.6
          accel = delta_speed / (delta_ms / 1000)

          average_accel =
            case points do
              [a, b, c | _] ->
                (a.accel + b.accel + c.accel + accel) / 4

              # g_accel

              _ ->
                0
            end

          total_distance = total_distance + distance_m

          p =
            point
            |> Map.put(:distance, total_distance)
            |> Map.put(:accel, accel)
            |> Map.put(:accel_avg, average_accel)
            |> Map.put(:delta_ms, delta_ms)

          {total_distance, ts, speed, [p | points]}
        end
      )

    {abbr, Enum.reverse(points)}
  end
  |> Enum.into(%{})

#
# Plot
#
alias VegaLite, as: Vl

widget =
  Vl.new(width: 1000, height: 1000)
  |> Vl.mark(:line)
  |> Vl.encode_field(
    :x,
    "x",
    title: "Distance (meters)",
    type: :quantitative
  )
  |> Vl.encode_field(
    :y,
    "y",
    title: "Y",
    type: :quantitative,
    scale:
      %{
        # domain: [0, 20]
      }
  )
  |> Vl.encode_field(:color, "Driver", type: :nominal)
  # |> Vl.encode_field(:shape, "Driver", type: :nominal)
  |> Kino.VegaLite.new()
  |> Kino.render()

include_drivers = [
  # "VER @ 16",
  # "HAM @ 18"
  "HAM",
  "VER"
]

for {abbr, data} <- lap_data, abbr in include_drivers do
  %{speed: max_speed} = Enum.max_by(integrated[abbr], fn p -> p.speed end)
  IO.inspect("Max speed: #{abbr} @ #{max_speed} km/h")
  # IO.inspect({abbr, data.time})
  for p <- integrated[abbr], y = p.speed, y >= 0 do
    point = %{x: p.distance, y: y, Driver: abbr}
    Kino.VegaLite.push(widget, point)
  end
end

:ok